Skip to content

Commit

Permalink
Use react-intersection-observer
Browse files Browse the repository at this point in the history
  • Loading branch information
RunDevelopment committed Feb 28, 2024
1 parent 3a0067d commit 4bcb886
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 153 deletions.
21 changes: 21 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@
"react-hotkeys-hook": "^3.4.6",
"react-i18next": "^12.0.0",
"react-icons": "^4.10.1",
"react-intersection-observer": "^9.8.1",
"react-markdown": "^8.0.3",
"react-query": "^3.39.3",
"react-time-ago": "^7.2.1",
Expand Down
35 changes: 35 additions & 0 deletions src/renderer/components/IfVisible.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Box } from '@chakra-ui/react';
import { memo } from 'react';
import { useInView } from 'react-intersection-observer';

export interface IfVisibleProps {
height: string | number;
visibleOffset?: number;
forceVisible?: boolean;
children: React.ReactNode;
}
export const IfVisible = memo(
({ height, visibleOffset = 200, forceVisible = false, children }: IfVisibleProps) => {
const { ref, entry } = useInView({
rootMargin: `${visibleOffset}px 0px ${visibleOffset}px 0px`,
});

const finalVisibility = forceVisible || (entry?.isIntersecting ?? false);

return (
<Box
height={typeof height === 'number' ? `${height}px` : height}
ref={ref}
style={{ contain: 'layout size' }}
>
{finalVisibility && (
<>
<Box display="flex" />
{children}
<Box display="flex" />
</>
)}
</Box>
);
}
);
75 changes: 43 additions & 32 deletions src/renderer/components/NodeDocumentation/NodesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { groupBy } from '../../../common/util';
import { BackendContext } from '../../contexts/BackendContext';
import { NodeDocumentationContext } from '../../contexts/NodeDocumentationContext';
import { IconFactory } from '../CustomIcons';
import { IfVisible } from '../IfVisible';
import { SearchBar } from '../SearchBar';

export interface NodesListProps {
Expand Down Expand Up @@ -135,41 +136,51 @@ export const NodesList = memo(
<Text>{category.name}</Text>
</HStack>
</Center>
{categoryNodesByScore.map((node) => {
const isSelected = node.schemaId === selectedSchemaId;
return (
<HStack
_hover={{
backgroundColor: 'var(--bg-700)',
}}
backgroundColor={
isSelected
? 'var(--bg-700)'
: 'var(--bg-800)'
}
borderBottomColor="gray.500"
borderBottomWidth="1px"
borderLeftColor={category.color}
borderLeftWidth={isSelected ? 8 : 4}
cursor="pointer"
key={node.schemaId}
p={2}
ref={isSelected ? selectedElement : undefined}
w="full"
onClick={() => {
setSelectedSchemaId(node.schemaId);
}}
>
<IconFactory icon={node.icon} />
<Text
<IfVisible
forceVisible={categoryNodesByScore.some(
(node) => node.schemaId === selectedSchemaId
)}
height={41 * categoryNodesByScore.length}
>
{categoryNodesByScore.map((node) => {
const isSelected =
node.schemaId === selectedSchemaId;
return (
<HStack
_hover={{
backgroundColor: 'var(--bg-700)',
}}
backgroundColor={
isSelected
? 'var(--bg-700)'
: 'var(--bg-800)'
}
borderBottomColor="gray.500"
borderBottomWidth="1px"
borderLeftColor={category.color}
borderLeftWidth={isSelected ? 8 : 4}
cursor="pointer"
key={node.schemaId}
p={2}
ref={
isSelected ? selectedElement : undefined
}
w="full"
onClick={() => {
setSelectedSchemaId(node.schemaId);
}}
>
{node.name}
</Text>
</HStack>
);
})}
<IconFactory icon={node.icon} />
<Text
cursor="pointer"
key={node.schemaId}
>
{node.name}
</Text>
</HStack>
);
})}
</IfVisible>
</Box>
);
})
Expand Down
108 changes: 6 additions & 102 deletions src/renderer/components/NodeSelectorPanel/RegularAccordionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import {
Text,
Tooltip,
} from '@chakra-ui/react';
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
import React, { memo } from 'react';
import { Category, NodeSchema } from '../../../common/common-types';
import { EMPTY_ARRAY, groupBy } from '../../../common/util';
import { groupBy } from '../../../common/util';
import { IconFactory } from '../CustomIcons';
import { IfVisible } from '../IfVisible';
import { RepresentativeNodeWrapper } from './RepresentativeNodeWrapper';
import { SubcategoryHeading } from './SubcategoryHeading';
import { TextBox } from './TextBox';
Expand Down Expand Up @@ -80,100 +81,6 @@ export const RegularAccordionItem = memo(
}
);

interface OnlyIfVisibleProps {
height: string;
visibleOffset?: number;
children: React.ReactNode;
}
export const OnlyIfVisible = memo(
({ height, visibleOffset = 1000, children }: OnlyIfVisibleProps) => {
interface Foo {
visible: boolean;
timestamp: number;
}
const [dataPoints, setDataPoints] = useState<readonly Foo[]>(EMPTY_ARRAY);
const addDataPoint = useCallback((visible: boolean) => {
setDataPoints((prev) => {
const now = Date.now();
return [
...prev.filter((p) => Math.abs(now - p.timestamp) < 1000),
{ visible, timestamp: now },
];
});
}, []);

const intersectionRef = useRef<HTMLDivElement>(null);

const [checkAgain, setCheckAgain] = useState(-1);

// Set visibility with intersection observer
useEffect(() => {
const root = document;
if (intersectionRef.current) {
const localRef = intersectionRef.current;
const observer = new IntersectionObserver(
(entries) => {
console.log('change');
// addDataPoint(entries[0].isIntersecting);
setCheckAgain(0);
window.requestIdleCallback(
() => {
// setCheckAgain((prev) => prev + 1);
},
{
timeout: 600,
}
);
},
{ root, rootMargin: `${visibleOffset}px 0px ${visibleOffset}px 0px` }
);

observer.observe(localRef);
return () => {
observer.unobserve(localRef);
};
}
}, [visibleOffset, addDataPoint]);

useEffect(() => {
if (intersectionRef.current && checkAgain < 10) {
const localRef = intersectionRef.current;
const timerId = setTimeout(() => {
const rect = localRef.getBoundingClientRect();
const visible =
rect.left < window.innerWidth &&
rect.right >= 0 &&
rect.top - visibleOffset < window.innerHeight &&
rect.bottom + visibleOffset >= 0;
addDataPoint(visible);
setCheckAgain((prev) => prev + 1);
}, 2 ** checkAgain + 1);

return () => clearTimeout(timerId);
}
}, [checkAgain, visibleOffset, addDataPoint]);

const isVisible =
dataPoints.length > 0 &&
dataPoints.filter((p) => p.visible).length >= dataPoints.length / 2;

return (
<Box
height={height}
ref={intersectionRef}
>
{isVisible && (
<>
<Box display="flex" />
{children}
<Box display="flex" />
</>
)}
</Box>
);
}
);

interface SubcategoriesProps {
collapsed: boolean;
category: Category;
Expand All @@ -190,8 +97,6 @@ export const Subcategories = memo(({ collapsed, category, categoryNodes }: Subca

const nodeHeight = 28;
const nodePadding = 6;
const placeholderHeight =
nodeHeight * nodes.length + nodePadding * (nodes.length - 1) + 12;

return (
<Box key={group.id}>
Expand All @@ -201,9 +106,8 @@ export const Subcategories = memo(({ collapsed, category, categoryNodes }: Subca
group={group}
/>
</Center>
<OnlyIfVisible
height={`${placeholderHeight}px`}
key={group.id}
<IfVisible
height={nodeHeight * nodes.length + nodePadding * (nodes.length + 1)}
visibleOffset={600}
>
<Box>
Expand All @@ -215,7 +119,7 @@ export const Subcategories = memo(({ collapsed, category, categoryNodes }: Subca
/>
))}
</Box>
</OnlyIfVisible>
</IfVisible>
</Box>
);
})}
Expand Down
53 changes: 34 additions & 19 deletions src/renderer/hooks/usePaneNodeSearchMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
stringifyTargetHandle,
} from '../../common/util';
import { IconFactory } from '../components/CustomIcons';
import { IfVisible } from '../components/IfVisible';
import { BackendContext } from '../contexts/BackendContext';
import { ContextMenuContext } from '../contexts/ContextMenuContext';
import { GlobalContext, GlobalVolatileContext } from '../contexts/GlobalNodeState';
Expand Down Expand Up @@ -293,6 +294,12 @@ const Menu = memo(({ onSelect, schemata, favorites, categories }: MenuProps) =>
.slice(0, groupIndex)
.reduce((acc, g) => acc + g.schemata.length, 0);

const nodeHeight = 28;
const nodePadding = 2;
const placeholderHeight =
nodeHeight * group.schemata.length +
nodePadding * (group.schemata.length + 1);

return (
<Box key={group.categoryId ?? 'favs'}>
<HStack
Expand All @@ -318,25 +325,33 @@ const Menu = memo(({ onSelect, schemata, favorites, categories }: MenuProps) =>
<Text fontSize="xs">{group.name}</Text>
</HStack>

{group.schemata.map((schema, schemaIndex) => {
const index = indexOffset + schemaIndex;
const isSelected = selectedIndex === index;

return (
<SchemaItem
accentColor={getCategoryAccentColor(
categories,
schema.category
)}
isFavorite={favorites.has(schema.schemaId)}
isSelected={isSelected}
key={schema.schemaId}
schema={schema}
scrollRef={isSelected ? scrollRef : undefined}
onClick={onClickHandler}
/>
);
})}
<IfVisible
forceVisible={
indexOffset <= selectedIndex &&
selectedIndex < indexOffset + group.schemata.length
}
height={placeholderHeight}
>
{group.schemata.map((schema, schemaIndex) => {
const index = indexOffset + schemaIndex;
const isSelected = selectedIndex === index;

return (
<SchemaItem
accentColor={getCategoryAccentColor(
categories,
schema.category
)}
isFavorite={favorites.has(schema.schemaId)}
isSelected={isSelected}
key={schema.schemaId}
schema={schema}
scrollRef={isSelected ? scrollRef : undefined}
onClick={onClickHandler}
/>
);
})}
</IfVisible>
</Box>
);
})}
Expand Down

0 comments on commit 4bcb886

Please sign in to comment.