From ef8c5c7dc1f1d87c3233cf8f594d7f6ee8b163e6 Mon Sep 17 00:00:00 2001 From: mckervinc Date: Mon, 2 Oct 2023 00:32:26 -0400 Subject: [PATCH 1/6] use resize observer library --- package.json | 7 +++-- src/AutoSizer.tsx | 75 ++++++++------------------------------------ src/Row.tsx | 10 +++--- src/Table.tsx | 66 +++++++++++++++----------------------- src/TableContext.tsx | 2 +- yarn.lock | 7 +++++ 6 files changed, 56 insertions(+), 111 deletions(-) diff --git a/package.json b/package.json index 22c3a75..7043dee 100644 --- a/package.json +++ b/package.json @@ -65,28 +65,29 @@ "@testing-library/react-hooks": "^8.0.1", "@types/react-window": "^1.8.5", "cross-env": "^7.0.3", - "eslint-config-standard-react": "^13.0.0", + "eslint": "8.50.0", "eslint-config-standard": "^17.1.0", + "eslint-config-standard-react": "^13.0.0", "eslint-plugin-flowtype": "^8.0.3", "eslint-plugin-import": "^2.28.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-react": "^7.33.2", - "eslint": "8.50.0", "gh-pages": "^6.0.0", "postcss": "^8.4.30", "prettier": "^3.0.3", "react": "^18.2.0", + "rollup": "^3.29.3", "rollup-plugin-analyzer": "^4.0.0", "rollup-plugin-bundle-size": "^1.0.3", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-visualizer": "^5.9.2", - "rollup": "^3.29.3", "typescript": "^5.2.2" }, "dependencies": { + "react-resize-detector": "^9.1.0", "react-window": "^1.8.9" }, "volta": { diff --git a/src/AutoSizer.tsx b/src/AutoSizer.tsx index f4063c2..de674c5 100644 --- a/src/AutoSizer.tsx +++ b/src/AutoSizer.tsx @@ -1,4 +1,5 @@ -import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import React, { useContext, useMemo } from "react"; +import { useResizeDetector } from "react-resize-detector"; import { TableContext } from "./TableContext"; import { DEFAULT_FOOTER_HEIGHT, DEFAULT_HEADER_HEIGHT, DEFAULT_ROW_HEIGHT } from "./constants"; import { findFooterByUuid, findHeaderByUuid } from "./util"; @@ -90,7 +91,7 @@ const calculateHeight = ( const borderOffset = !!table ? table.offsetHeight - table.clientHeight : 0; // if there are rows, let's do the calculation - if (!!nodes.length) { + if (nodes.length) { if (rowHeight > 0) { return headerOffset + nodes.length * rowHeight + footerOffset + borderOffset; } @@ -116,8 +117,8 @@ const calculateHeight = ( /** * This is a skinny AutoSizer based on react-virtualized-auto-sizer. * This removes the `bailout` functionality in order to allow the Table - * to generate its own height. This also ignores a resize if the - * dimensions of the window did not actually change (one less render). + * to generate its own height. This uses ResizeObserver to observe the + * container when it changes in order to provide the correct height */ const AutoSizer = ({ numRows, @@ -131,22 +132,22 @@ const AutoSizer = ({ children }: AutoSizerProps) => { // hooks - const resizeRef = useRef(0); - const ref = useRef(null); + const { + ref, + width: containerWidth, + height: containerHeight + } = useResizeDetector(); const { uuid, columns, footerComponent } = useContext(TableContext); - const [dimensions, setDimensions] = useState({ containerHeight: 0, containerWidth: 0 }); // variables - const { containerHeight, containerWidth } = dimensions; const hasFooter = useMemo( () => !!footerComponent || !!columns.find(c => !!c.footer), [columns, footerComponent] ); - const fixedTableSize = !!tableHeight && tableHeight > 0 && !!tableWidth && tableWidth > 0; // calculate the computed height const computedHeight = useMemo(() => { - if (!!tableHeight && tableHeight > 0) { + if (tableHeight && tableHeight > 0) { return tableHeight; } @@ -163,64 +164,14 @@ const AutoSizer = ({ // calculate the actual height of the table const height = findCorrectHeight({ computedHeight, - containerHeight, + containerHeight: containerHeight || 0, tableHeight: tableHeight || 0, minHeight: minTableHeight || 0, maxHeight: maxTableHeight || 0 }); // get actual width - const width = !!tableWidth && tableWidth > 0 ? tableWidth : containerWidth; - - // functions - const calculateDimensions = useCallback(() => { - // base cases - if (!ref.current?.parentElement || fixedTableSize) { - return; - } - - // get style - const parent = ref.current.parentElement; - const style = window.getComputedStyle(parent); - const paddingLeft = parseInt(style.paddingLeft) || 0; - const paddingRight = parseInt(style.paddingRight) || 0; - const paddingTop = parseInt(style.paddingTop) || 0; - const paddingBottom = parseInt(style.paddingBottom) || 0; - - // find new dimensions - const newHeight = Math.max((parent.offsetHeight || 0) - paddingTop - paddingBottom, 0); - const newWidth = Math.max((parent.offsetWidth || 0) - paddingLeft - paddingRight, 0); - - // update state - setDimensions(prev => { - const { containerHeight: oldHeight, containerWidth: oldWidth } = prev; - if (oldHeight !== newHeight || oldWidth !== newWidth) { - return { containerHeight: newHeight, containerWidth: newWidth }; - } - - return prev; - }); - }, [fixedTableSize]); - - const onResize = useCallback(() => { - window.clearTimeout(resizeRef.current); - resizeRef.current = window.setTimeout(calculateDimensions, 40); - }, [calculateDimensions]); - - // effects - // on mount, calculate the dimensions - useEffect(() => calculateDimensions(), [calculateDimensions]); - - // on resize, we have to re-calculate the dimensions - useEffect(() => { - window.removeEventListener("resize", onResize); - window.addEventListener("resize", onResize); - const m = resizeRef.current; - return () => { - window.clearTimeout(m); - window.removeEventListener("resize", onResize); - }; - }, [onResize]); + const width = tableWidth && tableWidth > 0 ? tableWidth : containerWidth || 0; return
{height || width ? children({ height, width }) : null}
; }; diff --git a/src/Row.tsx b/src/Row.tsx index 0c7fcda..94ac1a2 100644 --- a/src/Row.tsx +++ b/src/Row.tsx @@ -2,10 +2,10 @@ import React, { SVGProps, useCallback, useContext, useLayoutEffect, useRef } fro import { CacheFunction, ColumnProps, RowRenderProps, SubComponentProps } from "../index"; //@ts-ignore TS2307 import Minus from "./svg/minus-circle.svg"; +import { ListChildComponentProps } from "react-window"; +import { TableContext } from "./TableContext"; //@ts-ignore TS2307 import Plus from "./svg/plus-circle.svg"; -import { TableContext } from "./TableContext"; -import { ListChildComponentProps } from "react-window"; import { cx } from "./util"; interface TableCellProps { @@ -197,7 +197,7 @@ function Row({ const containerHeight = !rowHeight ? undefined : isExpanded && SubComponent ? rowHeight : "100%"; // sub component props - const subProps = { row, index, isExpanded, clearSizeCache }; + const subProps: SubComponentProps = { row, index, isExpanded, clearSizeCache }; // row styling const borderBottom = borders ? undefined : "none"; @@ -221,7 +221,7 @@ function Row({ if (height !== calculateHeight(rowRef.current, index)) { clearSizeCache(index); } - }, [rowRef, index, height, calculateHeight, clearSizeCache, pixelWidths]); + }, [index, height, calculateHeight, clearSizeCache, pixelWidths]); // effects // on expansion, clear the cache @@ -234,7 +234,7 @@ function Row({ } expandedCalledRef.current = false; - }, [isExpanded, expandedCalledRef, resetHeight, index, clearSizeCache]); + }, [isExpanded, resetHeight, index, clearSizeCache]); return (
({}); const listRef = useRef(null); + const prevWidthRef = useRef(width); const treeRef = useRef(new NumberTree()); const tableRef = useRef(null); const { dispatch, uuid, columns, minColumnWidth, fixedWidth, remainingCols, pixelWidths } = @@ -80,28 +80,25 @@ const ListComponent = forwardRef(function ( [itemKey] ); - const clearSizeCache = useCallback( - (dataIndex: number, forceUpdate = false) => { - if (!listRef.current) { - return; - } + const clearSizeCache = useCallback((dataIndex: number, forceUpdate = false) => { + if (!listRef.current) { + return; + } - window.clearTimeout(timeoutRef.current); - if (forceUpdate) { - treeRef.current.clearFromIndex(dataIndex); - listRef.current.resetAfterIndex(dataIndex + 1); - return; - } + window.clearTimeout(timeoutRef.current); + if (forceUpdate) { + treeRef.current.clearFromIndex(dataIndex); + listRef.current.resetAfterIndex(dataIndex + 1); + return; + } - timeoutRef.current = window.setTimeout(() => { - const node = tableRef.current?.children[1].children[0] as HTMLElement; - const resetIndex = parseInt(node?.dataset.index || "0") + 1; - treeRef.current.clearFromIndex(resetIndex); - listRef.current.resetAfterIndex(resetIndex); - }, 50); - }, - [listRef, tableRef, timeoutRef, treeRef] - ); + timeoutRef.current = window.setTimeout(() => { + const node = tableRef.current?.children[1].children[0] as HTMLElement; + const resetIndex = parseInt(node?.dataset.index || "0") + 1; + treeRef.current.clearFromIndex(resetIndex); + listRef.current.resetAfterIndex(resetIndex); + }, 50); + }, []); const calculateHeight = useCallback( (queryParam: number | HTMLElement, optionalDataIndex: number | null = null) => { @@ -114,7 +111,7 @@ const ListComponent = forwardRef(function ( } const arr = [...row.children].slice(rowHeight ? 1 : 0) as HTMLElement[]; - const res = (rowHeight || 0) + arr.reduce((pv, c) => pv + c.offsetHeight, 0); + const res = arr.reduce((pv, c) => pv + c.offsetHeight, rowHeight || 0) || defaultSize; // update the calculated height ref cacheRef.current[dataIndex] = res; @@ -139,7 +136,7 @@ const ListComponent = forwardRef(function ( const shouldUseRowWidth = useCallback(() => { const parentElement = tableRef.current?.parentElement || NO_NODE; setUseRowWidth(parentElement.scrollWidth <= parentElement.clientWidth); - }, [tableRef]); + }, []); // effects /* initializers */ @@ -149,17 +146,6 @@ const ListComponent = forwardRef(function ( setUseRowWidth(widths.scrollWidth <= widths.clientWidth); }, []); - // force clear cache to update header size - useEffect(() => { - clearSizeCache(-1, true); - // figure out how to wait for scrollbar to appear - // before recalculating. using 100ms heuristic - setTimeout(() => { - updatePixelWidths(); - shouldUseRowWidth(); - }, 100); - }, []); - /* updates */ // update pixel widths every time the width changes useLayoutEffect(() => updatePixelWidths(), [width]); @@ -170,7 +156,7 @@ const ListComponent = forwardRef(function ( // manually alter the height of each row if height is incorrect // to help with flicker on resize useLayoutEffect(() => { - if (prevRef.current !== width) { + if (prevWidthRef.current !== width) { treeRef.current.clearFromIndex(0); setTimeout(() => { if (!tableRef.current || !listRef.current) { @@ -215,8 +201,8 @@ const ListComponent = forwardRef(function ( }, 0); } - prevRef.current = width; - }, [width, tableRef, listRef, calculateHeight]); + prevWidthRef.current = width; + }, [width, calculateHeight]); // for the footer: set the rows in the context with the data. // this is useful for any aggregate calculations. @@ -261,7 +247,7 @@ const ListComponent = forwardRef(function ( }} itemSize={index => { if (!index) { - if (!!headerHeight && headerHeight > 0) { + if (headerHeight && headerHeight > 0) { return headerHeight; } @@ -340,13 +326,13 @@ const Table = forwardRef(function ( // warn if a minHeight is set without a maxHeight let maxHeight = maxTableHeight; - if (!!minTableHeight && minTableHeight > 0 && (!maxTableHeight || maxTableHeight <= 0)) { + if (minTableHeight && minTableHeight > 0 && (!maxTableHeight || maxTableHeight <= 0)) { maxHeight = minTableHeight + 400; } // handle warning useEffect(() => { - if (!!minTableHeight && minTableHeight > 0 && (!maxTableHeight || maxTableHeight <= 0)) { + if (minTableHeight && minTableHeight > 0 && (!maxTableHeight || maxTableHeight <= 0)) { console.warn( `maxTableHeight was either not present, or is <= 0, but you provided a minTableHeight of ${minTableHeight}px. As a result, the maxTableHeight will be set to ${ minTableHeight + 400 diff --git a/src/TableContext.tsx b/src/TableContext.tsx index 608e120..7ed368c 100644 --- a/src/TableContext.tsx +++ b/src/TableContext.tsx @@ -156,7 +156,7 @@ const TableContextProvider = ({ children, initialState }: ProviderProps) => { _stateOnMount.current = initialState; dispatch({ type: "refresh", initialState: refreshed }); } - }, [_stateOnMount, initialState]); + }, [initialState]); return {children}; }; diff --git a/yarn.lock b/yarn.lock index 7a5fa9c..a6dc123 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4220,6 +4220,13 @@ react-is@^16.13.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-resize-detector@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-9.1.0.tgz#45ea7176e57f1a568abd0e3eafcbfd3532fb284e" + integrity sha512-vGFbfkIZp4itJqR4yl+GhjrZHtdlQvou1r10ek0yZUMkizKbPdekKTpPb003IV3b8E5BJFThVG0oocjE3lNsug== + dependencies: + lodash "^4.17.21" + react-window@^1.8.9: version "1.8.9" resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.9.tgz#24bc346be73d0468cdf91998aac94e32bc7fa6a8" From eecc4dcd0530d586918872cfb778b66e08ced474 Mon Sep 17 00:00:00 2001 From: mckervinc Date: Mon, 2 Oct 2023 20:27:18 -0400 Subject: [PATCH 2/6] more types --- src/Table.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Table.tsx b/src/Table.tsx index 5eff0b0..f823397 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -56,11 +56,11 @@ const ListComponent = forwardRef(function ( ) { // hooks const timeoutRef = useRef(0); - const cacheRef = useRef({}); const listRef = useRef(null); const prevWidthRef = useRef(width); const treeRef = useRef(new NumberTree()); const tableRef = useRef(null); + const cacheRef = useRef<{ [index: number]: number }>({}); const { dispatch, uuid, columns, minColumnWidth, fixedWidth, remainingCols, pixelWidths } = useContext(TableContext); const [useRowWidth, setUseRowWidth] = useState(true); @@ -73,7 +73,7 @@ const ListComponent = forwardRef(function ( // functions const generateKeyFromRow = useCallback( - (row: any, defaultValue: number) => { + function (row: T, defaultValue: number) { const generatedKey = itemKey ? itemKey(row) : undefined; return generatedKey !== undefined ? generatedKey : defaultValue; }, @@ -174,7 +174,7 @@ const ListComponent = forwardRef(function ( const dataIndex = parseInt(node.dataset.index || "0"); // if the row is incorrect, update the tops going forward - const height = cache[dataIndex + 1].size; + const height: number = cache[dataIndex + 1].size; const computed = calculateHeight(node, dataIndex); // case 0: the first element, where the top is correct From fbaa5cb9fc90abbe47cc685cc169b94c8cb3d639 Mon Sep 17 00:00:00 2001 From: mckervinc Date: Mon, 2 Oct 2023 20:35:45 -0400 Subject: [PATCH 3/6] update readme and changelog --- CHANGELOG.md | 12 +++++++++++- README.md | 6 +++--- example/src/ColumnProps.tsx | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 380eefd..a01346a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,20 @@ # CHANGELOG +## 0.5.0 + +_2023-10-02_ + +### Features + +- uses a wrapper library to handle observing the table container for width/height changes +- initial flicker for variable-row-size tables should be mitigated +- more typescript specifications + ## 0.4.10 _2023-09-30_ -### +### Features - added ability to specify the `footerHeight` - removed some typescript warnings diff --git a/README.md b/README.md index eb5c90f..310b04a 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,9 @@ import { Table } from "react-fluid-table"; const data = _.range(100).map(i => ({ id: i + 1, - firstName: faker.name.firstName(), - lastName: faker.name.lastName(), - email: faker.internet.email() + firstName: randFirstName(), + lastName: randLastName(), + email: randEmail() })); const columns = [ diff --git a/example/src/ColumnProps.tsx b/example/src/ColumnProps.tsx index 3740212..23db4d4 100644 --- a/example/src/ColumnProps.tsx +++ b/example/src/ColumnProps.tsx @@ -137,6 +137,6 @@ const data: PropData[] = [ } ]; -const ColumnPropsTable = () => ; +const ColumnPropsTable = () => ; export default ColumnPropsTable; From a02968317dd3e768257f9ab62320f69d00f1ae7f Mon Sep 17 00:00:00 2001 From: mckervinc Date: Mon, 2 Oct 2023 20:36:18 -0400 Subject: [PATCH 4/6] version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7043dee..9b31e55 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-fluid-table", - "version": "0.4.10", + "version": "0.5.0", "description": "A React table inspired by react-window", "author": "Mckervin Ceme ", "license": "MIT", From 5c032b17eea4466ea8ae3bd01c8fd95736926987 Mon Sep 17 00:00:00 2001 From: mckervinc Date: Mon, 2 Oct 2023 20:37:36 -0400 Subject: [PATCH 5/6] lint --- example/src/ColumnProps.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/example/src/ColumnProps.tsx b/example/src/ColumnProps.tsx index 23db4d4..4dc7021 100644 --- a/example/src/ColumnProps.tsx +++ b/example/src/ColumnProps.tsx @@ -129,14 +129,16 @@ const data: PropData[] = [ type: "FooterElement", description: (
- This property allows you to customize the content inside of a footer cell. The library will create - a cell container for you with the proper column widths and resizability. If this field is - defined, then this will get rendered inside of the cell container in the footer. + This property allows you to customize the content inside of a footer cell. The library will + create a cell container for you with the proper column widths and resizability. If this + field is defined, then this will get rendered inside of the cell container in the footer.
) } ]; -const ColumnPropsTable = () => ; +const ColumnPropsTable = () => ( + +); export default ColumnPropsTable; From 11d26d81667bade81e9643abd0780ccc6ada1cdb Mon Sep 17 00:00:00 2001 From: mckervinc Date: Mon, 2 Oct 2023 20:46:50 -0400 Subject: [PATCH 6/6] tweak rollup --- rollup.config.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rollup.config.mjs b/rollup.config.mjs index b56fb00..0a8efdf 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -37,7 +37,8 @@ const config = { url({ exclude: ["**/*.svg"] }), babel({ extensions, - exclude: "node_modules/**" + exclude: "node_modules/**", + babelHelpers: "bundled" }), resolve({ extensions }), commonjs(),