feat: table virtualization (#1408)
* feat: poc table virtualization * feat: table virtualization * feat: add overscan of 15 * fix: increase overscan to 50 * fix: dead code * fix: debug mode * feat: styled space
This commit is contained in:
@ -16,6 +16,7 @@
|
|||||||
"@hello-pangea/dnd": "^16.2.0",
|
"@hello-pangea/dnd": "^16.2.0",
|
||||||
"@hookform/resolvers": "^3.1.1",
|
"@hookform/resolvers": "^3.1.1",
|
||||||
"@tabler/icons-react": "^2.30.0",
|
"@tabler/icons-react": "^2.30.0",
|
||||||
|
"@tanstack/react-virtual": "^3.0.0-alpha.0",
|
||||||
"@types/node": "^16.18.4",
|
"@types/node": "^16.18.4",
|
||||||
"@types/react": "^18.0.25",
|
"@types/react": "^18.0.25",
|
||||||
"@types/react-dom": "^18.0.9",
|
"@types/react-dom": "^18.0.9",
|
||||||
|
|||||||
@ -44,6 +44,7 @@ export function useApolloFactory() {
|
|||||||
fetchPolicy: 'cache-first',
|
fetchPolicy: 'cache-first',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
connectToDevTools: isDebugMode,
|
||||||
// We don't want to re-create the client on token change or it will cause infinite loop
|
// We don't want to re-create the client on token change or it will cause infinite loop
|
||||||
initialTokenPair: tokenPair,
|
initialTokenPair: tokenPair,
|
||||||
onTokenPairChange(tokenPair) {
|
onTokenPairChange(tokenPair) {
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useVirtual } from '@tanstack/react-virtual';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { isNavbarSwitchingSizeState } from '@/ui/layout/states/isNavbarSwitchingSizeState';
|
import { isNavbarSwitchingSizeState } from '@/ui/layout/states/isNavbarSwitchingSizeState';
|
||||||
|
import { useScrollWrapperScopedRef } from '@/ui/utilities/scroll/hooks/useScrollWrapperScopedRef';
|
||||||
|
|
||||||
import { RowIdContext } from '../contexts/RowIdContext';
|
import { RowIdContext } from '../contexts/RowIdContext';
|
||||||
import { RowIndexContext } from '../contexts/RowIndexContext';
|
import { RowIndexContext } from '../contexts/RowIndexContext';
|
||||||
@ -9,28 +12,70 @@ import { tableRowIdsState } from '../states/tableRowIdsState';
|
|||||||
|
|
||||||
import { EntityTableRow } from './EntityTableRow';
|
import { EntityTableRow } from './EntityTableRow';
|
||||||
|
|
||||||
|
type SpaceProps = {
|
||||||
|
top?: number;
|
||||||
|
bottom?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledSpace = styled.td<SpaceProps>`
|
||||||
|
${({ top }) => top && `padding-top: ${top}px;`}
|
||||||
|
${({ bottom }) => bottom && `padding-bottom: ${bottom}px;`}
|
||||||
|
`;
|
||||||
|
|
||||||
export function EntityTableBody() {
|
export function EntityTableBody() {
|
||||||
|
const scrollWrapperRef = useScrollWrapperScopedRef();
|
||||||
|
|
||||||
const rowIds = useRecoilValue(tableRowIdsState);
|
const rowIds = useRecoilValue(tableRowIdsState);
|
||||||
|
|
||||||
const isNavbarSwitchingSize = useRecoilValue(isNavbarSwitchingSizeState);
|
const isNavbarSwitchingSize = useRecoilValue(isNavbarSwitchingSizeState);
|
||||||
|
|
||||||
const isFetchingEntityTableData = useRecoilValue(
|
const isFetchingEntityTableData = useRecoilValue(
|
||||||
isFetchingEntityTableDataState,
|
isFetchingEntityTableDataState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const rowVirtualizer = useVirtual({
|
||||||
|
size: rowIds.length,
|
||||||
|
parentRef: scrollWrapperRef,
|
||||||
|
overscan: 50,
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = rowVirtualizer.virtualItems;
|
||||||
|
const paddingTop = items.length > 0 ? items[0].start : 0;
|
||||||
|
const paddingBottom =
|
||||||
|
items.length > 0
|
||||||
|
? rowVirtualizer.totalSize - items[items.length - 1].end
|
||||||
|
: 0;
|
||||||
|
|
||||||
if (isFetchingEntityTableData || isNavbarSwitchingSize) {
|
if (isFetchingEntityTableData || isNavbarSwitchingSize) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tbody>
|
<>
|
||||||
{rowIds.map((rowId, index) => (
|
{paddingTop > 0 && (
|
||||||
<RowIdContext.Provider value={rowId} key={rowId}>
|
<tr>
|
||||||
<RowIndexContext.Provider value={index}>
|
<StyledSpace top={paddingTop} />
|
||||||
<EntityTableRow rowId={rowId} />
|
</tr>
|
||||||
</RowIndexContext.Provider>
|
)}
|
||||||
</RowIdContext.Provider>
|
{items.map((virtualItem) => {
|
||||||
))}
|
const rowId = rowIds[virtualItem.index];
|
||||||
</tbody>
|
|
||||||
|
return (
|
||||||
|
<RowIdContext.Provider value={rowId} key={rowId}>
|
||||||
|
<RowIndexContext.Provider value={virtualItem.index}>
|
||||||
|
<EntityTableRow
|
||||||
|
key={virtualItem.index}
|
||||||
|
ref={virtualItem.measureRef}
|
||||||
|
rowId={rowId}
|
||||||
|
/>
|
||||||
|
</RowIndexContext.Provider>
|
||||||
|
</RowIdContext.Provider>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{paddingBottom > 0 && (
|
||||||
|
<tr>
|
||||||
|
<StyledSpace bottom={paddingBottom} />
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { forwardRef } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||||
@ -15,7 +16,14 @@ const StyledRow = styled.tr<{ selected: boolean }>`
|
|||||||
props.selected ? props.theme.accent.quaternary : 'none'};
|
props.selected ? props.theme.accent.quaternary : 'none'};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function EntityTableRow({ rowId }: { rowId: string }) {
|
type EntityTableRowProps = {
|
||||||
|
rowId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EntityTableRow = forwardRef<
|
||||||
|
HTMLTableRowElement,
|
||||||
|
EntityTableRowProps
|
||||||
|
>(function EntityTableRow({ rowId }, ref) {
|
||||||
const columns = useRecoilScopedValue(
|
const columns = useRecoilScopedValue(
|
||||||
visibleTableColumnsScopedSelector,
|
visibleTableColumnsScopedSelector,
|
||||||
TableRecoilScopeContext,
|
TableRecoilScopeContext,
|
||||||
@ -24,6 +32,7 @@ export function EntityTableRow({ rowId }: { rowId: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledRow
|
<StyledRow
|
||||||
|
ref={ref}
|
||||||
data-testid={`row-id-${rowId}`}
|
data-testid={`row-id-${rowId}`}
|
||||||
selected={currentRowSelected}
|
selected={currentRowSelected}
|
||||||
data-selectable-id={rowId}
|
data-selectable-id={rowId}
|
||||||
@ -41,4 +50,4 @@ export function EntityTableRow({ rowId }: { rowId: string }) {
|
|||||||
<td></td>
|
<td></td>
|
||||||
</StyledRow>
|
</StyledRow>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
import { useRef } from 'react';
|
import { createContext, RefObject, useRef } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { useListenScroll } from '../hooks/useListenScroll';
|
import { useListenScroll } from '../hooks/useListenScroll';
|
||||||
|
|
||||||
|
export const ScrollWrapperContext = createContext<RefObject<HTMLDivElement>>({
|
||||||
|
current: null,
|
||||||
|
});
|
||||||
|
|
||||||
const StyledScrollWrapper = styled.div`
|
const StyledScrollWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -28,8 +32,10 @@ export function ScrollWrapper({ children, className }: ScrollWrapperProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledScrollWrapper ref={scrollableRef} className={className}>
|
<ScrollWrapperContext.Provider value={scrollableRef}>
|
||||||
{children}
|
<StyledScrollWrapper ref={scrollableRef} className={className}>
|
||||||
</StyledScrollWrapper>
|
{children}
|
||||||
|
</StyledScrollWrapper>
|
||||||
|
</ScrollWrapperContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
|
||||||
|
import { ScrollWrapperContext } from '../components/ScrollWrapper';
|
||||||
|
|
||||||
|
export function useScrollWrapperScopedRef() {
|
||||||
|
const scrollWrapperRef = useContext(ScrollWrapperContext);
|
||||||
|
|
||||||
|
if (!scrollWrapperRef)
|
||||||
|
throw new Error(
|
||||||
|
`Using a scoped ref without a ScrollWrapper : verify that you are using a ScrollWrapper if you intended to do so.`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return scrollWrapperRef;
|
||||||
|
}
|
||||||
@ -1217,6 +1217,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.14.0"
|
regenerator-runtime "^0.14.0"
|
||||||
|
|
||||||
|
"@babel/runtime@^7.16.7":
|
||||||
|
version "7.22.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.11.tgz#7a9ba3bbe406ad6f9e8dd4da2ece453eb23a77a4"
|
||||||
|
integrity sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA==
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime "^0.14.0"
|
||||||
|
|
||||||
"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.22.5", "@babel/template@^7.3.3":
|
"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.22.5", "@babel/template@^7.3.3":
|
||||||
version "7.22.5"
|
version "7.22.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec"
|
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec"
|
||||||
@ -3633,6 +3640,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
|
|
||||||
|
"@reach/observe-rect@^1.1.0":
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2"
|
||||||
|
integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==
|
||||||
|
|
||||||
"@remirror/core-constants@^2.0.2":
|
"@remirror/core-constants@^2.0.2":
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-2.0.2.tgz#f05eccdc69e3a65e7d524b52548f567904a11a1a"
|
resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-2.0.2.tgz#f05eccdc69e3a65e7d524b52548f567904a11a1a"
|
||||||
@ -4923,6 +4935,14 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-2.30.0.tgz#4ea3c4da56fd5653bb9d0be0dc7feaa33602555c"
|
resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-2.30.0.tgz#4ea3c4da56fd5653bb9d0be0dc7feaa33602555c"
|
||||||
integrity sha512-tvtmkI4ALjKThVVORh++sB9JnkFY7eGInKxNy+Df7WVQiF7T85tlvGADzlgX4Ic+CK5MIUzZ0jhOlQ/RRlgXpg==
|
integrity sha512-tvtmkI4ALjKThVVORh++sB9JnkFY7eGInKxNy+Df7WVQiF7T85tlvGADzlgX4Ic+CK5MIUzZ0jhOlQ/RRlgXpg==
|
||||||
|
|
||||||
|
"@tanstack/react-virtual@^3.0.0-alpha.0":
|
||||||
|
version "3.0.0-alpha.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.0.0-alpha.0.tgz#2ca5a75fa609eca2b2ba622024d3aa3ee097bc30"
|
||||||
|
integrity sha512-WpHU/dt34NwZZ8qtiE05TF+nX/b1W6qrWZarO+s8jJFpPVicrTbJKp5Bjt4eSJuk7aYw272oEfsH3ABBRgj+3A==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.16.7"
|
||||||
|
"@reach/observe-rect" "^1.1.0"
|
||||||
|
|
||||||
"@testing-library/dom@>=7":
|
"@testing-library/dom@>=7":
|
||||||
version "9.3.1"
|
version "9.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.3.1.tgz#8094f560e9389fb973fe957af41bf766937a9ee9"
|
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.3.1.tgz#8094f560e9389fb973fe957af41bf766937a9ee9"
|
||||||
|
|||||||
Reference in New Issue
Block a user