Implement table record virtualizer back (#2839)

Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com>
Co-authored-by: v1b3m <vibenjamin6@gmail.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
This commit is contained in:
gitstart-twenty 2023-12-06 19:05:00 +05:45 committed by GitHub
parent 9df83c9a5a
commit b09100e3f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 146 additions and 18 deletions

View File

@ -1,3 +1,4 @@
import { useContext } from 'react';
import { useInView } from 'react-intersection-observer';
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
@ -12,6 +13,8 @@ import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable';
import { isFetchingRecordTableDataState } from '@/ui/object/record-table/states/isFetchingRecordTableDataState';
import { tableRowIdsState } from '@/ui/object/record-table/states/tableRowIdsState';
import { getRecordTableScopedStates } from '@/ui/object/record-table/utils/getRecordTableScopedStates';
import { ScrollWrapperContext } from '@/ui/utilities/scroll/components/ScrollWrapper';
import RenderIfVisible from '@/ui/utilities/virtualizer/RenderIfVisible';
export const RecordTableBody = () => {
const { scopeId } = useRecordTable();
@ -41,36 +44,49 @@ export const RecordTableBody = () => {
const isFetchingRecordTableData = useRecoilValue(
isFetchingRecordTableDataState,
);
const lastRowId = tableRowIds[tableRowIds.length - 1];
const scrollWrapperRef = useContext(ScrollWrapperContext);
if (isFetchingRecordTableData) {
return <></>;
}
return (
<tbody>
<>
{tableRowIds.map((rowId, rowIndex) => (
<RowIdContext.Provider value={rowId} key={rowId}>
<RowIndexContext.Provider value={rowIndex}>
<RecordTableRow
key={rowId}
ref={
rowId === lastRowId && rowIndex > 30
? lastTableRowRef
: undefined
}
rowId={rowId}
/>
<RenderIfVisible
rootElement="tbody"
placeholderElement="tr"
defaultHeight={32}
initialVisible={rowIndex < 30}
root={scrollWrapperRef.current}
>
<RecordTableRow
key={rowId}
ref={
rowId === lastRowId && rowIndex > 30
? lastTableRowRef
: undefined
}
rowId={rowId}
/>
</RenderIfVisible>
</RowIndexContext.Provider>
</RowIdContext.Provider>
))}
{isFetchingMoreObjects && (
<StyledRow selected={false}>
<td style={{ height: 50 }} colSpan={1000}>
Loading more...
</td>
</StyledRow>
)}
</tbody>
<tbody>
{isFetchingMoreObjects && (
<StyledRow selected={false}>
<td style={{ height: 50 }} colSpan={1000}>
Loading more...
</td>
</StyledRow>
)}
</tbody>
</>
);
};

View File

@ -0,0 +1,112 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
type RenderIfVisibleProps = {
/**
* Whether the element should be visible initially or not.
* Useful e.g. for always setting the first N items to visible.
* Default: false
*/
initialVisible?: boolean;
/** An estimate of the element's height */
defaultHeight?: number;
/** How far outside the viewport in pixels should elements be considered visible? */
visibleOffset?: number;
/** Should the element stay rendered after it becomes visible? */
stayRendered?: boolean;
root?: HTMLElement | null;
/** E.g. 'span', 'tbody'. Default = 'div' */
rootElement?: string;
rootElementClass?: string;
/** E.g. 'span', 'tr'. Default = 'div' */
placeholderElement?: string;
placeholderElementClass?: string;
children: React.ReactNode;
};
const RenderIfVisible = ({
initialVisible = false,
defaultHeight = 300,
visibleOffset = 1000,
stayRendered = false,
root = null,
rootElement = 'div',
rootElementClass = '',
placeholderElement = 'div',
placeholderElementClass = '',
children,
}: RenderIfVisibleProps) => {
const [isVisible, setIsVisible] = useState<boolean>(initialVisible);
// eslint-disable-next-line twenty/no-state-useref
const wasVisible = useRef<boolean>(initialVisible);
// eslint-disable-next-line twenty/no-state-useref
const placeholderHeight = useRef<number>(defaultHeight);
const intersectionRef = useRef<HTMLDivElement>(null);
// Set visibility with intersection observer
useEffect(() => {
if (intersectionRef.current) {
const localRef = intersectionRef.current;
const observer = new IntersectionObserver(
(entries) => {
// Before switching off `isVisible`, set the height of the placeholder
if (!entries[0].isIntersecting) {
placeholderHeight.current = localRef!.offsetHeight;
}
if (typeof window !== undefined && window.requestIdleCallback) {
window.requestIdleCallback(
() => setIsVisible(entries[0].isIntersecting),
{
timeout: 600,
},
);
} else {
setIsVisible(entries[0].isIntersecting);
}
},
{ root, rootMargin: `${visibleOffset}px 0px ${visibleOffset}px 0px` },
);
observer.observe(localRef);
return () => {
if (localRef) {
observer.unobserve(localRef);
}
};
}
return () => {};
}, [root, visibleOffset]);
useEffect(() => {
if (isVisible) {
wasVisible.current = true;
}
}, [isVisible]);
const placeholderStyle = { height: placeholderHeight.current };
const rootClasses = useMemo(
() => `renderIfVisible ${rootElementClass}`,
[rootElementClass],
);
const placeholderClasses = useMemo(
() => `renderIfVisible-placeholder ${placeholderElementClass}`,
[placeholderElementClass],
);
// eslint-disable-next-line react/no-children-prop
return React.createElement(rootElement, {
children:
isVisible || (stayRendered && wasVisible.current) ? (
<>{children}</>
) : (
React.createElement(placeholderElement, {
className: placeholderClasses,
style: placeholderStyle,
})
),
ref: intersectionRef,
className: rootClasses,
});
};
export default RenderIfVisible;