From db359b071d877b0cf68b5302ece72371fa1ba06b Mon Sep 17 00:00:00 2001 From: mckervinc Date: Sat, 30 Sep 2023 10:47:00 -0400 Subject: [PATCH] add footerHeight + performance + fix ts warnings --- CHANGELOG.md | 10 + example/src/Props.tsx | 20 +- example/src/examples/07-controlled.tsx | 32 +- index.d.ts | 4 + package.json | 2 +- src/AutoSizer.tsx | 90 ++-- src/Footer.tsx | 5 +- src/Header.tsx | 7 +- src/Row.tsx | 5 +- src/Table.tsx | 683 ++++++++++++------------- src/TableContext.tsx | 5 +- src/TableWrapper.tsx | 15 +- src/constants.ts | 1 + src/main.css | 1 - 14 files changed, 466 insertions(+), 414 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8ee5c5..380eefd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # CHANGELOG +## 0.4.10 + +_2023-09-30_ + +### + +- added ability to specify the `footerHeight` +- removed some typescript warnings +- slight performance tweaks + ## 0.4.7 _2023-09-28_ diff --git a/example/src/Props.tsx b/example/src/Props.tsx index aee135b..a62c5cc 100644 --- a/example/src/Props.tsx +++ b/example/src/Props.tsx @@ -145,6 +145,16 @@ const data: PropData[] = [ description: "This is a fixed height of each row. Subcomponents will not be affected by this value" }, + { + prop: "headerHeight", + type: "number", + description: "This is an optional fixed height of the header" + }, + { + prop: "footerHeight", + type: "number", + description: "This is an optional fixed height of the footer" + }, { prop: "minColumnWidth", type: "number", @@ -411,7 +421,8 @@ const Props = () => (
Interfaces
- { * specify a fixed header height */ headerHeight?: number; + /** + * specify a fixed footer height + */ + footerHeight?: number; /** * Enable or disable row borders. Default: \`false\`. */ @@ -716,7 +731,8 @@ interface TableProps { */ ref?: ForwardedRef; } -`} /> +`} + /> ); diff --git a/example/src/examples/07-controlled.tsx b/example/src/examples/07-controlled.tsx index 0eabd9e..fb5059e 100644 --- a/example/src/examples/07-controlled.tsx +++ b/example/src/examples/07-controlled.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ import { randCatchPhrase, randCity, @@ -115,19 +114,34 @@ const Controlled = ({ data, height, columns: variableColumns }: ControlledProps) ); }; +interface ToggleType { + data: boolean; + height: boolean; + columns: boolean; +} + +interface DataType { + data: TestData[]; + height: number; + columns: ColumnProps[]; +} + +type keyM = keyof ToggleType; +type keyD = keyof DataType; + const viewableTypes = new Set(["string", "number", "boolean"]); const Example7 = () => { // hooks - const [toggles, setToggles] = useState({ + const [toggles, setToggles] = useState({ data: false, height: false, columns: false }); // variables - const keys = Object.keys(toggles); - const props = { + const keys: keyM[] = ["data", "height", "columns"]; + const props: DataType = { data: toggles.data ? testData2 : testData, height: toggles.height ? 200 : 400, columns: toggles.columns @@ -187,13 +201,10 @@ const Example7 = () => { {"{\n"} {keys.map((key, index) => { const ending = index !== keys.length - 1 ? ",\n" : "\n"; - // @ts-ignore const val = viewableTypes.has(typeof props[key]); let color = "rgb(166, 226, 46)"; - // @ts-ignore if (typeof props[key] === "number") { color = "rgb(174, 129, 255)"; - // @ts-ignore } else if (typeof props[key] === "boolean") { color = "rgb(102, 217, 239)"; } @@ -201,8 +212,11 @@ const Example7 = () => { {` ${key}: `} - {/* @ts-ignore */} - {val ? props[key] : toggles[key] ? '"altered"' : '"original"'} + {val + ? (props[key as keyD] as string | number) + : toggles[key] + ? '"altered"' + : '"original"'} {ending} diff --git a/index.d.ts b/index.d.ts index 9b1b80b..e70fd41 100644 --- a/index.d.ts +++ b/index.d.ts @@ -243,6 +243,10 @@ export interface TableProps { * specify a fixed header height */ headerHeight?: number; + /** + * specify a fixed footer height + */ + footerHeight?: number; /** * Enable or disable row borders. Default: `false`. */ diff --git a/package.json b/package.json index d437d53..22c3a75 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-fluid-table", - "version": "0.4.9", + "version": "0.4.10", "description": "A React table inspired by react-window", "author": "Mckervin Ceme ", "license": "MIT", diff --git a/src/AutoSizer.tsx b/src/AutoSizer.tsx index 45309c9..f4063c2 100644 --- a/src/AutoSizer.tsx +++ b/src/AutoSizer.tsx @@ -1,12 +1,13 @@ import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { TableContext } from "./TableContext"; -import { DEFAULT_HEADER_HEIGHT, DEFAULT_ROW_HEIGHT } from "./constants"; +import { DEFAULT_FOOTER_HEIGHT, DEFAULT_HEADER_HEIGHT, DEFAULT_ROW_HEIGHT } from "./constants"; import { findFooterByUuid, findHeaderByUuid } from "./util"; interface AutoSizerProps { numRows: number; rowHeight?: number; headerHeight?: number; + footerHeight?: number; tableWidth?: number; tableHeight?: number; minTableHeight?: number; @@ -64,29 +65,34 @@ const findCorrectHeight = ({ const calculateHeight = ( rowHeight: number, headerHeight: number, + footerHeight: number, uuid: string, size: number, hasFooter: boolean ) => { - // get the header, footer and nodes + // get the header and the rows of the table const header = findHeaderByUuid(uuid); const nodes = [...(header?.nextElementSibling?.children || [])] as HTMLElement[]; - let footerHeight = findFooterByUuid(uuid)?.offsetHeight || 0; - if (!!header && !!nodes.length) { - // header height to use - const headerOffset = headerHeight > 0 ? headerHeight : header.offsetHeight; + // calculate header & footer offsets + const headerOffset = + headerHeight > 0 ? headerHeight : header?.offsetHeight || DEFAULT_HEADER_HEIGHT; + let footerOffset = 0; + if (hasFooter) { + footerOffset = + footerHeight > 0 + ? footerHeight + : findFooterByUuid(uuid)?.offsetHeight || DEFAULT_FOOTER_HEIGHT; + } - // get border height - let borders = 0; - const table = header.parentElement?.parentElement; - if (!!table) { - borders = table.offsetHeight - table.clientHeight; - } + // calculate border offset + const table = header?.parentElement?.parentElement; + const borderOffset = !!table ? table.offsetHeight - table.clientHeight : 0; - // perform calculation + // if there are rows, let's do the calculation + if (!!nodes.length) { if (rowHeight > 0) { - return headerOffset + nodes.length * rowHeight + footerHeight + borders; + return headerOffset + nodes.length * rowHeight + footerOffset + borderOffset; } let overscan = 0; @@ -97,20 +103,14 @@ const calculateHeight = ( return pv + c.offsetHeight; }, 0) + overscan + - footerHeight + - borders + footerOffset + + borderOffset ); } - // try and guess the header and footer height - const headerOffset = headerHeight || DEFAULT_HEADER_HEIGHT; - if (!footerHeight && hasFooter) { - footerHeight = headerOffset; - } - - // if the header and nodes are not specified, guess the height + // if the nodes are not specified, guess the height const height = Math.max(rowHeight || DEFAULT_ROW_HEIGHT, 10); - return height * Math.min(size || 10, 10) + headerOffset + footerHeight; + return headerOffset + height * Math.min(size || 10, 10) + footerOffset + borderOffset; }; /** @@ -127,21 +127,22 @@ const AutoSizer = ({ minTableHeight, maxTableHeight, headerHeight, + footerHeight, children }: AutoSizerProps) => { // hooks const resizeRef = useRef(0); const ref = useRef(null); - const { uuid, footerComponent, columns } = useContext(TableContext); + const { uuid, columns, footerComponent } = useContext(TableContext); const [dimensions, setDimensions] = useState({ containerHeight: 0, containerWidth: 0 }); // variables const { containerHeight, containerWidth } = dimensions; - - // check if footer exists - const hasFooter = useMemo(() => { - return !!footerComponent || !!columns.find(c => !!c.footer); - }, [footerComponent, columns]); + 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(() => { @@ -149,8 +150,15 @@ const AutoSizer = ({ return tableHeight; } - return calculateHeight(rowHeight || 0, headerHeight || 0, uuid, numRows, hasFooter); - }, [tableHeight, rowHeight, headerHeight, numRows, uuid, hasFooter]); + return calculateHeight( + rowHeight || 0, + headerHeight || 0, + footerHeight || 0, + uuid, + numRows, + hasFooter + ); + }, [tableHeight, rowHeight, headerHeight, footerHeight, numRows, uuid, hasFooter]); // calculate the actual height of the table const height = findCorrectHeight({ @@ -167,7 +175,7 @@ const AutoSizer = ({ // functions const calculateDimensions = useCallback(() => { // base cases - if (!ref.current?.parentElement) { + if (!ref.current?.parentElement || fixedTableSize) { return; } @@ -184,10 +192,15 @@ const AutoSizer = ({ const newWidth = Math.max((parent.offsetWidth || 0) - paddingLeft - paddingRight, 0); // update state - if (newHeight !== containerHeight || newWidth !== containerWidth) { - setDimensions({ containerHeight: newHeight, containerWidth: newWidth }); - } - }, [containerHeight, containerWidth]); + 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); @@ -196,10 +209,11 @@ const AutoSizer = ({ // effects // on mount, calculate the dimensions - useEffect(() => calculateDimensions(), []); + 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 () => { diff --git a/src/Footer.tsx b/src/Footer.tsx index c1140fc..b7724f0 100644 --- a/src/Footer.tsx +++ b/src/Footer.tsx @@ -14,9 +14,10 @@ const FooterCell = React.memo(function (props: InnerFooterCellProps) { // instance const { width, column } = props; + const cellWidth = width ? `${width}px` : undefined; const style: React.CSSProperties = { - width: width ? `${width}px` : undefined, - minWidth: width ? `${width}px` : undefined, + width: cellWidth, + minWidth: cellWidth, padding: !column.footer ? 0 : undefined }; diff --git a/src/Header.tsx b/src/Header.tsx index 9ba3ce3..f0276d0 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -19,12 +19,13 @@ const HeaderCell = React.memo(function ({ column, width }: HeaderCellProps const { dispatch, sortColumn: col, sortDirection, onSort } = useContext(TableContext); // constants + const cellWidth = width ? `${width}px` : undefined; const dir = sortDirection ? (sortDirection.toUpperCase() as SortDirection) : null; const style: React.CSSProperties = { cursor: column.sortable ? "pointer" : undefined, - width: width ? `${width}px` : undefined, - minWidth: width ? `${width}px` : undefined + width: cellWidth, + minWidth: cellWidth }; // function(s) @@ -61,7 +62,7 @@ const HeaderCell = React.memo(function ({ column, width }: HeaderCellProps
{column.header ?
{column.header}
: null} {column.key !== col ? null : ( -
+
)}
); diff --git a/src/Row.tsx b/src/Row.tsx index c209944..0c7fcda 100644 --- a/src/Row.tsx +++ b/src/Row.tsx @@ -75,9 +75,10 @@ const TableCell = React.memo(function ({ onExpanderClick }: TableCellProps) { // cell width + const cellWidth = width ? `${width}px` : undefined; const style: React.CSSProperties = { - width: width ? `${width}px` : undefined, - minWidth: width ? `${width}px` : undefined + width: cellWidth, + minWidth: cellWidth }; // expander diff --git a/src/Table.tsx b/src/Table.tsx index 58a9661..2d85289 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -21,7 +21,6 @@ import { DEFAULT_HEADER_HEIGHT, DEFAULT_ROW_HEIGHT, NO_NODE } from "./constants" import { arraysMatch, calculateColumnWidths, - cx, findHeaderByUuid, findRowByUuidAndKey, randomString @@ -41,386 +40,366 @@ interface ListProps extends Omit, "columns" | "borders"> { /** * The main table component */ -const ListComponent = forwardRef( - ( - { - data, - width, - height, - itemKey, - rowHeight, - className, - headerHeight, - footerComponent, - ...rest - }: ListProps, - ref: React.ForwardedRef - ) => { - // hooks - const timeoutRef = useRef(0); - const prevRef = useRef(width); - const cacheRef = useRef({}); - const listRef = useRef(null); - const treeRef = useRef(new NumberTree()); - const tableRef = useRef(null); - const { dispatch, uuid, columns, minColumnWidth, fixedWidth, remainingCols, pixelWidths } = - useContext(TableContext); - const [useRowWidth, setUseRowWidth] = useState(true); - const [defaultSize, setDefaultSize] = useState(rowHeight || DEFAULT_ROW_HEIGHT); - - // constants - const hasFooter = useMemo(() => { - return !!footerComponent || !!columns.find(c => !!c.footer); - }, [footerComponent, columns]); - - // functions - const generateKeyFromRow = useCallback( - (row: any, defaultValue: number) => { - const generatedKey = itemKey ? itemKey(row) : undefined; - return generatedKey !== undefined ? generatedKey : defaultValue; - }, - [itemKey] - ); +const ListComponent = forwardRef(function ( + { + data, + width, + height, + itemKey, + rowHeight, + className, + headerHeight, + footerComponent, + ...rest + }: ListProps, + ref: React.ForwardedRef +) { + // hooks + const timeoutRef = useRef(0); + const prevRef = useRef(width); + const cacheRef = useRef({}); + const listRef = useRef(null); + const treeRef = useRef(new NumberTree()); + const tableRef = useRef(null); + const { dispatch, uuid, columns, minColumnWidth, fixedWidth, remainingCols, pixelWidths } = + useContext(TableContext); + const [useRowWidth, setUseRowWidth] = useState(true); + const [defaultSize, setDefaultSize] = useState(rowHeight || DEFAULT_ROW_HEIGHT); + + // constants + const hasFooter = useMemo(() => { + return !!footerComponent || !!columns.find(c => !!c.footer); + }, [footerComponent, columns]); + + // functions + const generateKeyFromRow = useCallback( + (row: any, defaultValue: number) => { + const generatedKey = itemKey ? itemKey(row) : undefined; + return generatedKey !== undefined ? generatedKey : defaultValue; + }, + [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] + ); + + const calculateHeight = useCallback( + (queryParam: number | HTMLElement, optionalDataIndex: number | null = null) => { + const dataIndex = (typeof queryParam === "number" ? queryParam : optionalDataIndex) as number; + const key = generateKeyFromRow(data[dataIndex], dataIndex); + const row = typeof queryParam === "number" ? findRowByUuidAndKey(uuid, key) : queryParam; + + if (!row) { + return cacheRef.current[dataIndex] || defaultSize; + } - 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] + const arr = [...row.children].slice(rowHeight ? 1 : 0) as HTMLElement[]; + const res = (rowHeight || 0) + arr.reduce((pv, c) => pv + c.offsetHeight, 0); + + // update the calculated height ref + cacheRef.current[dataIndex] = res; + return res; + }, + [uuid, data, rowHeight, defaultSize, generateKeyFromRow] + ); + + const updatePixelWidths = useCallback(() => { + const widths = calculateColumnWidths( + tableRef.current, + remainingCols, + fixedWidth, + minColumnWidth, + columns ); - - const calculateHeight = useCallback( - (queryParam: number | HTMLElement, optionalDataIndex: number | null = null) => { - const dataIndex = ( - typeof queryParam === "number" ? queryParam : optionalDataIndex - ) as number; - const key = generateKeyFromRow(data[dataIndex], dataIndex); - const row = typeof queryParam === "number" ? findRowByUuidAndKey(uuid, key) : queryParam; - - if (!row) { - return cacheRef.current[dataIndex] || defaultSize; + if (!arraysMatch(widths, pixelWidths)) { + dispatch({ type: "updatePixelWidths", widths }); + } + }, [dispatch, remainingCols, fixedWidth, minColumnWidth, pixelWidths, columns]); + + const shouldUseRowWidth = useCallback(() => { + const parentElement = tableRef.current?.parentElement || NO_NODE; + setUseRowWidth(parentElement.scrollWidth <= parentElement.clientWidth); + }, [tableRef]); + + // effects + /* initializers */ + // initialize whether or not to use rowWidth (useful for bottom border) + useEffect(() => { + const widths = tableRef.current || NO_NODE; + 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]); + + // check if we should use the row width when width changes + useEffect(() => shouldUseRowWidth(), [width]); + + // manually alter the height of each row if height is incorrect + // to help with flicker on resize + useLayoutEffect(() => { + if (prevRef.current !== width) { + treeRef.current.clearFromIndex(0); + setTimeout(() => { + if (!tableRef.current || !listRef.current) { + return; } - const arr = [...row.children].slice(rowHeight ? 1 : 0); - const res = - (rowHeight || 0) + arr.reduce((pv, c) => pv + (c as HTMLElement).offsetHeight, 0); + // variables + let prevTop = 0; + let prevHeight = 0; + const cache = listRef.current._instanceProps.itemMetadataMap || {}; + const elements = [...tableRef.current.children[1].children] as HTMLElement[]; - // update the calculated height ref - cacheRef.current[dataIndex] = res; - return res; - }, - [uuid, data, rowHeight, defaultSize, generateKeyFromRow] - ); + // manually change the `top` and `height` for visible rows + elements.forEach((node, i) => { + const dataIndex = parseInt(node.dataset.index || "0"); - const updatePixelWidths = useCallback(() => { - const widths = calculateColumnWidths( - tableRef.current, - remainingCols, - fixedWidth, - minColumnWidth, - columns - ); - if (!arraysMatch(widths, pixelWidths)) { - dispatch({ type: "updatePixelWidths", widths }); - } - }, [dispatch, remainingCols, fixedWidth, minColumnWidth, pixelWidths, columns]); - - const shouldUseRowWidth = useCallback(() => { - const parentElement = tableRef.current?.parentElement || NO_NODE; - setUseRowWidth(parentElement.scrollWidth <= parentElement.clientWidth); - }, [tableRef]); - - // effects - /* initializers */ - // initialize whether or not to use rowWidth (useful for bottom border) - useEffect(() => { - const widths = tableRef.current || NO_NODE; - 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]); - - // check if we should use the row width when width changes - useEffect(() => shouldUseRowWidth(), [width]); - - // manually alter the height of each row if height is incorrect - // to help with flicker on resize - useLayoutEffect(() => { - if (prevRef.current !== width) { - treeRef.current.clearFromIndex(0); - setTimeout(() => { - if (!tableRef.current || !listRef.current) { - return; - } + // if the row is incorrect, update the tops going forward + const height = cache[dataIndex + 1].size; + const computed = calculateHeight(node, dataIndex); - // variables - let prevTop = 0; - let prevHeight = 0; - const cache = listRef.current._instanceProps.itemMetadataMap || {}; - const elements = [...tableRef.current.children[1].children]; - - // manually change the `top` and `height` for visible rows - elements.forEach((e, i) => { - const node = e as HTMLDivElement; - const dataIndex = parseInt(node.dataset.index || "0"); - - // if the row is incorrect, update the tops going forward - const height = cache[dataIndex + 1].size; - const computed = calculateHeight(node, dataIndex); - - // case 0: the first element, where the top is correct - if (i === 0) { - prevTop = parseInt(node.style.top); - prevHeight = computed; - - if (height !== computed) { - node.style.height = `${computed}px`; - } - return; - } + // case 0: the first element, where the top is correct + if (i === 0) { + prevTop = parseInt(node.style.top); + prevHeight = computed; - // case 1: every other element - const newTop = prevTop + prevHeight; - node.style.top = `${newTop}px`; if (height !== computed) { node.style.height = `${computed}px`; } + return; + } - prevTop = newTop; - prevHeight = computed; - }); - }, 0); - } + // case 1: every other element + const newTop = prevTop + prevHeight; + node.style.top = `${newTop}px`; + if (height !== computed) { + node.style.height = `${computed}px`; + } - prevRef.current = width; - }, [width, tableRef, listRef, calculateHeight]); + prevTop = newTop; + prevHeight = computed; + }); + }, 0); + } - // for the footer: set the rows in the context with the data. - // this is useful for any aggregate calculations. - // NOTE: maybe we should do this for the header too - useEffect(() => { - if (hasFooter) { - dispatch({ type: "updateRows", rows: data }); - } - }, [hasFooter, data, dispatch]); + prevRef.current = width; + }, [width, tableRef, listRef, calculateHeight]); - /* cleanup */ - useEffect(() => { - return () => { - if (timeoutRef.current) { - window.clearTimeout(timeoutRef.current); - } - }; - }, [timeoutRef]); - - /* misc */ - // provide access to window functions - useImperativeHandle(ref, () => ({ - scrollTo: (scrollOffset: number): void => listRef.current.scrollTo(scrollOffset), - scrollToItem: (index: number, align: string = "auto"): void => - listRef.current.scrollToItem(index, align) - })); - - return ( - ) => { - if (!index) return `${uuid}-header`; - const row = data.rows[index - 1]; - return generateKeyFromRow(row, index); - }} - itemSize={index => { - if (!index) { - if (!!headerHeight) { - return headerHeight; - } + // for the footer: set the rows in the context with the data. + // this is useful for any aggregate calculations. + // NOTE: maybe we should do this for the header too + useEffect(() => { + if (hasFooter) { + dispatch({ type: "updateRows", rows: data }); + } + }, [hasFooter, data, dispatch]); - const header = findHeaderByUuid(uuid); - return header - ? (header.children[0] as HTMLElement).offsetHeight - : DEFAULT_HEADER_HEIGHT; + /* cleanup */ + useEffect(() => { + return () => { + if (timeoutRef.current) { + window.clearTimeout(timeoutRef.current); + } + }; + }, [timeoutRef]); + + /* misc */ + // provide access to window functions + useImperativeHandle(ref, () => ({ + scrollTo: (scrollOffset: number): void => listRef.current.scrollTo(scrollOffset), + scrollToItem: (index: number, align: string = "auto"): void => + listRef.current.scrollToItem(index, align) + })); + + return ( + ) => { + if (!index) return `${uuid}-header`; + const row = data.rows[index - 1]; + return generateKeyFromRow(row, index); + }} + itemSize={index => { + if (!index) { + if (!!headerHeight && headerHeight > 0) { + return headerHeight; } - return calculateHeight(index - 1); - }} - onItemsRendered={() => { - // find median height of rows if no rowHeight provided - if (rowHeight || !tableRef.current) { - return; - } + const header = findHeaderByUuid(uuid); + return header ? (header.children[0] as HTMLElement).offsetHeight : DEFAULT_HEADER_HEIGHT; + } - // add calculated height to tree - [...tableRef.current.children[1].children].forEach(e => { - const node = e as HTMLDivElement; - const dataIndex = parseInt(node.dataset.index || "0"); - if (!treeRef.current.hasIndex(dataIndex)) { - treeRef.current.insert({ - index: dataIndex, - height: calculateHeight(node, dataIndex) - }); - } - }); + return calculateHeight(index - 1); + }} + onItemsRendered={() => { + // find median height of rows if no rowHeight provided + if (rowHeight || !tableRef.current) { + return; + } - const median = treeRef.current.getMedian(); - if (median && defaultSize !== median) { - setDefaultSize(median); + // add calculated height to tree + [...tableRef.current.children[1].children].forEach(e => { + const node = e as HTMLDivElement; + const dataIndex = parseInt(node.dataset.index || "0"); + if (!treeRef.current.hasIndex(dataIndex)) { + treeRef.current.insert({ + index: dataIndex, + height: calculateHeight(node, dataIndex) + }); } - }} - itemData={{ - rows: data, - rowHeight, - useRowWidth, - clearSizeCache, - calculateHeight, - generateKeyFromRow, - ...rest - }} - > - {RowWrapper} - - ); - } -); + }); + + const median = treeRef.current.getMedian(); + if (median && defaultSize !== median) { + setDefaultSize(median); + } + }} + itemData={{ + rows: data, + rowHeight, + useRowWidth, + clearSizeCache, + calculateHeight, + generateKeyFromRow, + ...rest + }} + > + {RowWrapper} + + ); +}); ListComponent.displayName = "ListComponent"; -const Table = forwardRef( - ( - { - id, - columns, - onSort, - sortColumn, - sortDirection, - tableHeight, - tableWidth, - tableStyle, - headerStyle, - headerClassname, - footerComponent, - footerStyle, - footerClassname, - maxTableHeight, - minTableHeight, - borders = false, - minColumnWidth = 80, - stickyFooter = false, - ...rest - }: TableProps, - ref: React.ForwardedRef - ) => { - // TODO: do all prop validation here - const [uuid] = useState(`${id || "data-table"}-${randomString(5)}`); - - // warn if a minHeight is set without a maxHeight - let maxHeight = maxTableHeight; +const Table = forwardRef(function ( + { + id, + columns, + onSort, + sortColumn, + sortDirection, + tableHeight, + tableWidth, + tableStyle, + headerStyle, + headerClassname, + footerComponent, + footerStyle, + footerClassname, + maxTableHeight, + minTableHeight, + borders = false, + minColumnWidth = 80, + stickyFooter = false, + ...rest + }: TableProps, + ref: React.ForwardedRef +) { + // TODO: do all prop validation here + const [uuid] = useState(`${id || "data-table"}-${randomString(5)}`); + + // warn if a minHeight is set without a maxHeight + let maxHeight = maxTableHeight; + if (!!minTableHeight && minTableHeight > 0 && (!maxTableHeight || maxTableHeight <= 0)) { + maxHeight = minTableHeight + 400; + } + + // handle warning + useEffect(() => { if (!!minTableHeight && minTableHeight > 0 && (!maxTableHeight || maxTableHeight <= 0)) { - maxHeight = minTableHeight + 400; + 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 + }px. To avoid this warning, please specify a maxTableHeight.` + ); } - - // handle warning - useEffect(() => { - 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 - }px. To avoid this warning, please specify a maxTableHeight.` - ); - } - }, [minTableHeight, maxTableHeight]); - - return ( - + - {typeof tableHeight === "number" && typeof tableWidth === "number" ? ( - - ) : ( - - {({ height, width }) => { - return ( - - ); - }} - - )} - - ); - } -); + {({ height, width }) => { + return ( + + ); + }} + + + ); +}); Table.displayName = "Table"; diff --git a/src/TableContext.tsx b/src/TableContext.tsx index ffcf95d..608e120 100644 --- a/src/TableContext.tsx +++ b/src/TableContext.tsx @@ -54,7 +54,9 @@ const baseState: TableState = { stickyFooter: false }; -const fields = [ +type TableStateKey = keyof Omit; + +const fields: TableStateKey[] = [ "sortColumn", "sortDirection", "minColumnWidth", @@ -111,7 +113,6 @@ const getChangedFields = ( ) => { const changedFields = new Set(); fields.forEach(field => { - // @ts-ignore if (prevState[field] !== currState[field]) { changedFields.add(field); } diff --git a/src/TableWrapper.tsx b/src/TableWrapper.tsx index 423fea1..71c13ab 100644 --- a/src/TableWrapper.tsx +++ b/src/TableWrapper.tsx @@ -1,6 +1,7 @@ import React, { forwardRef, useContext } from "react"; import Footer from "./Footer"; import { TableContext } from "./TableContext"; +import { cx } from "./util"; interface TableWrapperProps { style: React.CSSProperties; @@ -10,7 +11,10 @@ interface TableWrapperProps { } const TableWrapper = forwardRef( - ({ style, children, ...rest }: TableWrapperProps, ref: React.ForwardedRef) => { + ( + { style, children, className, ...rest }: TableWrapperProps, + ref: React.ForwardedRef + ) => { // hooks const { id, tableStyle, uuid } = useContext(TableContext); @@ -21,7 +25,14 @@ const TableWrapper = forwardRef( }; return ( -
+
{children}
diff --git a/src/constants.ts b/src/constants.ts index cfc6718..594c132 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,4 @@ export const DEFAULT_ROW_HEIGHT = 37; export const DEFAULT_HEADER_HEIGHT = 37; +export const DEFAULT_FOOTER_HEIGHT = 37; export const NO_NODE = { scrollWidth: 0, clientWidth: 0 }; diff --git a/src/main.css b/src/main.css index 114d078..b0efe8d 100644 --- a/src/main.css +++ b/src/main.css @@ -42,7 +42,6 @@ } .header-cell-text { - font-family: Helvetica; font-weight: bold; color: #7d7d7d; overflow: hidden;