From 742e45136f644a8263a938ea022bf5aa36388bf8 Mon Sep 17 00:00:00 2001 From: mckervinc Date: Wed, 4 Oct 2023 08:42:45 -0400 Subject: [PATCH 1/2] add ability to freeze columns --- .prettierrc | 8 ++++-- CHANGELOG.md | 8 ++++++ example/src/App.tsx | 9 ++++++ example/src/ColumnProps.tsx | 10 +++++++ example/src/Page.tsx | 26 ++++++++++++++++-- example/src/Props.tsx | 4 +++ example/src/examples/01-base.tsx | 6 ++-- example/src/examples/10-footer.tsx | 12 +++++--- example/src/examples/12-frozen.tsx | 44 ++++++++++++++++++++++++++++++ index.d.ts | 4 +++ package.json | 3 +- src/Footer.tsx | 27 ++++++++++-------- src/Header.tsx | 35 ++++++++++++++++++------ src/Row.tsx | 32 ++++++++++++++++------ src/main.css | 7 +++++ 15 files changed, 194 insertions(+), 41 deletions(-) create mode 100644 example/src/examples/12-frozen.tsx diff --git a/.prettierrc b/.prettierrc index 2b593d7..7562b27 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,7 @@ { - "printWidth": 100, - "arrowParens": "avoid", - "trailingComma": "none" + "printWidth": 100, + "arrowParens": "avoid", + "trailingComma": "none", + "tabWidth": 2, + "semi": true } diff --git a/CHANGELOG.md b/CHANGELOG.md index a01346a..a14d958 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## 0.5.1 + +_2023-10-04_ + +### Features + +- adds the ability to freeze columns by adding the `frozen` property + ## 0.5.0 _2023-10-02_ diff --git a/example/src/App.tsx b/example/src/App.tsx index 7aed9ab..13b26df 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -12,6 +12,7 @@ import { Example8, Source as Example8Code } from "./examples/08-header"; import { Example9, Source as Example9Code } from "./examples/09-scroll"; import { Example10, Source as Example10Code } from "./examples/10-footer"; import { Example11, Source as Example11Code } from "./examples/11-heights"; +import { Example12, Source as Example12Code } from "./examples/12-frozen"; const router = createHashRouter([ { @@ -102,6 +103,14 @@ const router = createHashRouter([ ) }, + { + path: "/frozen", + element: ( + + + + ) + }, { path: "/props", element: ( diff --git a/example/src/ColumnProps.tsx b/example/src/ColumnProps.tsx index 4dc7021..1313c42 100644 --- a/example/src/ColumnProps.tsx +++ b/example/src/ColumnProps.tsx @@ -88,6 +88,16 @@ const data: PropData[] = [ type: "number", description: "This sets the maximum pixel width of the column exactly" }, + { + prop: "sortable", + type: "boolean", + description: "This determines if a column is sortable" + }, + { + prop: "frozen", + type: "boolean", + description: "This determines if a column is frozen" + }, { prop: "expander", type: "boolean | ExpanderElement", diff --git a/example/src/Page.tsx b/example/src/Page.tsx index 8b94c95..d176ff4 100644 --- a/example/src/Page.tsx +++ b/example/src/Page.tsx @@ -21,6 +21,10 @@ const Content = styled.div` box-shadow: 0 2px 8px #bbb; `; +const BaseSegment = styled(Segment)` + flex-grow: 1; +`; + const Title = ({ title }: { title: string }) => (
{title}
@@ -28,9 +32,9 @@ const Title = ({ title }: { title: string }) => ( ); const Wrapper = ({ children }: { children: React.ReactNode }) => ( - + {children} - + ); const Application = styled(Sidebar.Pushable)` @@ -45,12 +49,21 @@ const PageContent = styled(Sidebar.Pusher)` overflow-y: auto; } + @media screen and (min-width: 769px) { + display: flex; + flex-direction: column; + } + @media screen and (max-width: 768px) { width: 100%; transform: translate3d(0, 0, 0) !important; } `; +const SnippetWrapper = styled.div` + flex: 1 1; +`; + const Banner = styled.code` color: #50f97a; white-space: pre; @@ -102,6 +115,9 @@ const LinkContainer = () => ( Footer + + Frozen + Table Props @@ -161,7 +177,11 @@ const Page = ({ title, code, children }: PageProps) => { {!!title && } <Wrapper>{children}</Wrapper> - {!!code && <Snippet code={code} />} + {!!code && ( + <SnippetWrapper> + <Snippet code={code} /> + </SnippetWrapper> + )} </PageContent> </Application> ); diff --git a/example/src/Props.tsx b/example/src/Props.tsx index a62c5cc..dc41b10 100644 --- a/example/src/Props.tsx +++ b/example/src/Props.tsx @@ -570,6 +570,10 @@ interface ColumnProps<T> { * Determines whether or not a column is sortable. */ sortable?: boolean; + /** + * Determines whether or not the column is frozen during horizontal scrolling. + */ + frozen?: boolean; /** * Marks this cell as an expansion cell. The style is pre-determined, and does the * functionalitty of collapsing/expanding a row. diff --git a/example/src/examples/01-base.tsx b/example/src/examples/01-base.tsx index 5a11b7d..dbea011 100644 --- a/example/src/examples/01-base.tsx +++ b/example/src/examples/01-base.tsx @@ -36,9 +36,9 @@ interface TestData { const data: TestData[] = _.range(3000).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: ColumnProps<TestData>[] = [ diff --git a/example/src/examples/10-footer.tsx b/example/src/examples/10-footer.tsx index b8e30d8..95e022d 100644 --- a/example/src/examples/10-footer.tsx +++ b/example/src/examples/10-footer.tsx @@ -107,11 +107,13 @@ const Example10 = () => { ) : ( <div style={{ display: "flex" }}> {columns.map((c, i) => { - const width = `${widths[i]}px`; + const width = widths[i]; const style: React.CSSProperties = { width, minWidth: width, - padding: "8px" + padding: "8px", + position: c.frozen ? "sticky" : undefined, + left: c.frozen ? widths.slice(0, i).reduce((pv, c) => pv + c, 0) : undefined }; return ( <div key={c.key} style={style}> @@ -296,11 +298,13 @@ const ComplexFooter = ({ stickyFooter }) => { <Footer> <div style={{ display: "flex" }}> {columns.map((c, i) => { - const width = \`\${widths[i]}px\`; + const width = widths[i]; const style: React.CSSProperties = { width, minWidth: width, - padding: "8px" + padding: "8px", + position: c.frozen ? "sticky" : undefined, + left: c.frozen ? widths.slice(0, i).reduce((pv, c) => pv + c, 0) : undefined }; return ( <div key={c.key} style={style}> diff --git a/example/src/examples/12-frozen.tsx b/example/src/examples/12-frozen.tsx new file mode 100644 index 0000000..4919264 --- /dev/null +++ b/example/src/examples/12-frozen.tsx @@ -0,0 +1,44 @@ +import { ColumnProps, Table } from "react-fluid-table"; +import { TestData, testData } from "../data"; + +const columns: ColumnProps<TestData>[] = [ + { + key: "id", + header: "ID", + width: 50, + frozen: true + }, + { + key: "firstName", + header: "First", + width: 120, + frozen: true + }, + { + key: "lastName", + header: "Last", + width: 120 + }, + { + key: "email", + header: "Email", + width: 250 + } +]; + +const Example12 = () => <Table data={testData} columns={columns} tableWidth={400} />; + +const Source = ` +const data = [/* ... */]; + +const columns: ColumnProps<TestData>[] = [ + { key: "id", header: "ID", width: 50, frozen: true }, + { key: "firstName", header: "First", width: 120, frozen: true }, + { key: "lastName", header: "Last", width: 120 }, + { key: "email", header: "Email", width: 250 } +]; + +const Example = () => <Table data={data} columns={columns} tableWidth={400} />; +`; + +export { Example12, Source }; diff --git a/index.d.ts b/index.d.ts index 7b83691..995edd0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -150,6 +150,10 @@ export interface ColumnProps<T> { * Determines whether or not a column is sortable. */ sortable?: boolean; + /** + * Determines whether or not the column is frozen during horizontal scrolling. + */ + frozen?: boolean; /** * Marks this cell as an expansion cell. The style is pre-determined, and does the * functionalitty of collapsing/expanding a row. diff --git a/package.json b/package.json index 9b31e55..a6d9dba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-fluid-table", - "version": "0.5.0", + "version": "0.5.1", "description": "A React table inspired by react-window", "author": "Mckervin Ceme <mckervinc@live.com>", "license": "MIT", @@ -25,6 +25,7 @@ "npm": ">=5" }, "scripts": { + "clean": "rm -rf dist", "build": "rm -rf dist ||: && rollup -c --environment BUILD:production", "start": "rollup -c -w --environment BUILD:development", "site:build": "cd example && yarn install && yarn build" diff --git a/src/Footer.tsx b/src/Footer.tsx index b7724f0..89315dd 100644 --- a/src/Footer.tsx +++ b/src/Footer.tsx @@ -6,25 +6,26 @@ import { cx, findTableByUuid } from "./util"; interface InnerFooterCellProps<T> { width: number; column: ColumnProps<T>; + prevWidths: number[]; } -const FooterCell = React.memo(function <T>(props: InnerFooterCellProps<T>) { +const FooterCell = React.memo(function <T>({ prevWidths, ...rest }: InnerFooterCellProps<T>) { // hooks const { rows } = useContext(TableContext); // instance - const { width, column } = props; - const cellWidth = width ? `${width}px` : undefined; + const { width, column } = rest; const style: React.CSSProperties = { - width: cellWidth, - minWidth: cellWidth, - padding: !column.footer ? 0 : undefined + width: width || undefined, + minWidth: width || undefined, + padding: !column.footer ? 0 : undefined, + left: column.frozen ? prevWidths.reduce((pv, c) => pv + c, 0) : undefined }; const FooterCellComponent = column.footer; return ( - <div className="cell" style={style}> - {!!FooterCellComponent && <FooterCellComponent rows={rows} {...props} />} + <div className={cx(["cell", column.frozen && "frozen"])} style={style}> + {!!FooterCellComponent && <FooterCellComponent rows={rows} {...rest} />} </div> ); }); @@ -46,9 +47,8 @@ const Footer = () => { const ref = useRef<HTMLDivElement>(null); // constants - const width = pixelWidths.reduce((pv, c) => pv + c, 0); const style: React.CSSProperties = { - minWidth: stickyFooter ? undefined : `${width}px`, + minWidth: stickyFooter ? undefined : pixelWidths.reduce((pv, c) => pv + c, 0), ...(footerStyle || {}) }; @@ -109,7 +109,12 @@ const Footer = () => { > <div className="row-container"> {columns.map((c, i) => ( - <FooterCell key={c.key} column={c} width={pixelWidths[i]} /> + <FooterCell + key={c.key} + column={c} + width={pixelWidths[i]} + prevWidths={pixelWidths.slice(0, i)} + /> ))} </div> </div> diff --git a/src/Header.tsx b/src/Header.tsx index f0276d0..29d95aa 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -7,6 +7,7 @@ import { cx } from "./util"; interface HeaderCellProps<T> { width: number; column: ColumnProps<T>; + prevWidths: number[]; } interface HeaderProps { @@ -14,18 +15,18 @@ interface HeaderProps { style: React.CSSProperties; } -const HeaderCell = React.memo(function <T>({ column, width }: HeaderCellProps<T>) { +const HeaderCell = React.memo(function <T>({ column, width, prevWidths }: HeaderCellProps<T>) { // hooks 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: cellWidth, - minWidth: cellWidth + width: width || undefined, + minWidth: width || undefined, + left: column.frozen ? prevWidths.reduce((pv, c) => pv + c, 0) : undefined }; // function(s) @@ -59,7 +60,11 @@ const HeaderCell = React.memo(function <T>({ column, width }: HeaderCellProps<T> if (!column.header || typeof column.header === "string") { return ( - <div className="header-cell" onClick={onClick} style={style}> + <div + className={cx(["header-cell", column.frozen && "frozen"])} + onClick={onClick} + style={style} + > {column.header ? <div className="header-cell-text">{column.header}</div> : null} {column.key !== col ? null : ( <div className={cx(["header-cell-arrow", dir?.toLowerCase()])}></div> @@ -70,7 +75,13 @@ const HeaderCell = React.memo(function <T>({ column, width }: HeaderCellProps<T> const ColumnCell = column.header; const headerDir = column.key === col ? dir || null : null; - return <ColumnCell style={style} onClick={onClick} sortDirection={headerDir} />; + return ( + <ColumnCell + onClick={onClick} + sortDirection={headerDir} + style={{ ...style, position: "sticky", zIndex: 1 }} + /> + ); }); HeaderCell.displayName = "HeaderCell"; @@ -82,6 +93,9 @@ const Header = forwardRef(({ children, ...rest }: HeaderProps, ref: any) => { // variables const { scrollWidth, clientWidth } = ref.current || NO_NODE; const width = scrollWidth <= clientWidth ? "100%" : undefined; + const stickyStyle: React.CSSProperties = { + zIndex: columns.find(c => c.frozen) ? 2 : undefined + }; return ( <div @@ -90,11 +104,16 @@ const Header = forwardRef(({ children, ...rest }: HeaderProps, ref: any) => { data-container-key={`${uuid}-container`} {...rest} > - <div className="sticky-header" data-header-key={`${uuid}-header`}> + <div className="sticky-header" data-header-key={`${uuid}-header`} style={stickyStyle}> <div className="row-wrapper" style={{ width }}> <div className={cx(["react-fluid-table-header", headerClassname])} style={headerStyle}> {columns.map((c, i) => ( - <HeaderCell key={c.key} column={c} width={pixelWidths[i]} /> + <HeaderCell + key={c.key} + column={c} + width={pixelWidths[i]} + prevWidths={pixelWidths.slice(0, i)} + /> ))} </div> </div> diff --git a/src/Row.tsx b/src/Row.tsx index f38c5eb..ee956e6 100644 --- a/src/Row.tsx +++ b/src/Row.tsx @@ -10,6 +10,7 @@ interface TableCellProps<T> { row: T; index: number; width?: number; + prevWidths: number[]; column: ColumnProps<T>; isExpanded: boolean; clearSizeCache: CacheFunction; @@ -69,14 +70,15 @@ const TableCell = React.memo(function <T>({ width, column, isExpanded, + prevWidths, clearSizeCache, onExpanderClick }: TableCellProps<T>) { - // cell width - const cellWidth = width ? `${width}px` : undefined; + // cell style const style: React.CSSProperties = { - width: cellWidth, - minWidth: cellWidth + width: width || undefined, + minWidth: width || undefined, + left: column.frozen ? prevWidths.reduce((pv, c) => pv + c, 0) : undefined }; // expander @@ -85,14 +87,20 @@ const TableCell = React.memo(function <T>({ const Logo = isExpanded ? Minus : Plus; return ( - <div className="cell" style={style}> + <div className={cx(["cell", column.frozen && "frozen"])} style={style}> <Logo className="expander" onClick={onExpanderClick} /> </div> ); } const Expander = column.expander; - return <Expander style={style} isExpanded={isExpanded} onClick={onExpanderClick} />; + return ( + <Expander + style={{ ...style, position: "sticky", zIndex: 1 }} + isExpanded={isExpanded} + onClick={onExpanderClick} + /> + ); } // basic styling @@ -108,7 +116,7 @@ const TableCell = React.memo(function <T>({ } } return ( - <div className="cell" style={style}> + <div className={cx(["cell", column.frozen && "frozen"])} style={style}> {content} </div> ); @@ -116,7 +124,14 @@ const TableCell = React.memo(function <T>({ // custom cell styling const CustomCell = column.cell; - return <CustomCell row={row} index={index} style={style} clearSizeCache={clearSizeCache} />; + return ( + <CustomCell + row={row} + index={index} + clearSizeCache={clearSizeCache} + style={{ ...style, position: "sticky", zIndex: 1 }} + /> + ); }); TableCell.displayName = "TableCell"; @@ -260,6 +275,7 @@ function Row<T>({ isExpanded={isExpanded} clearSizeCache={clearSizeCache} onExpanderClick={onExpanderClick} + prevWidths={pixelWidths.slice(0, i)} /> ))} </RowContainer> diff --git a/src/main.css b/src/main.css index b0efe8d..f7fa486 100644 --- a/src/main.css +++ b/src/main.css @@ -10,6 +10,13 @@ padding: 8px; } +.react-fluid-table .cell.frozen, +.react-fluid-table .header-cell.frozen { + position: -webkit-sticky; + position: sticky; + z-index: 1; +} + .react-fluid-table-container { will-change: height; } From 330e731e8cd21b34c1f329147e47400a430980f0 Mon Sep 17 00:00:00 2001 From: mckervinc <mckervinc@live.com> Date: Wed, 4 Oct 2023 08:49:34 -0400 Subject: [PATCH 2/2] performance tweak for frozen --- src/Footer.tsx | 8 ++++---- src/Header.tsx | 8 ++++---- src/Row.tsx | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Footer.tsx b/src/Footer.tsx index 89315dd..a58ccb2 100644 --- a/src/Footer.tsx +++ b/src/Footer.tsx @@ -6,10 +6,10 @@ import { cx, findTableByUuid } from "./util"; interface InnerFooterCellProps<T> { width: number; column: ColumnProps<T>; - prevWidths: number[]; + prevWidth: number; } -const FooterCell = React.memo(function <T>({ prevWidths, ...rest }: InnerFooterCellProps<T>) { +const FooterCell = React.memo(function <T>({ prevWidth, ...rest }: InnerFooterCellProps<T>) { // hooks const { rows } = useContext(TableContext); @@ -19,7 +19,7 @@ const FooterCell = React.memo(function <T>({ prevWidths, ...rest }: InnerFooterC width: width || undefined, minWidth: width || undefined, padding: !column.footer ? 0 : undefined, - left: column.frozen ? prevWidths.reduce((pv, c) => pv + c, 0) : undefined + left: column.frozen ? prevWidth : undefined }; const FooterCellComponent = column.footer; @@ -113,7 +113,7 @@ const Footer = () => { key={c.key} column={c} width={pixelWidths[i]} - prevWidths={pixelWidths.slice(0, i)} + prevWidth={c.frozen ? pixelWidths.slice(0, i).reduce((pv, c) => pv + c, 0) : 0} /> ))} </div> diff --git a/src/Header.tsx b/src/Header.tsx index 29d95aa..480475d 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -7,7 +7,7 @@ import { cx } from "./util"; interface HeaderCellProps<T> { width: number; column: ColumnProps<T>; - prevWidths: number[]; + prevWidth: number; } interface HeaderProps { @@ -15,7 +15,7 @@ interface HeaderProps { style: React.CSSProperties; } -const HeaderCell = React.memo(function <T>({ column, width, prevWidths }: HeaderCellProps<T>) { +const HeaderCell = React.memo(function <T>({ column, width, prevWidth }: HeaderCellProps<T>) { // hooks const { dispatch, sortColumn: col, sortDirection, onSort } = useContext(TableContext); @@ -26,7 +26,7 @@ const HeaderCell = React.memo(function <T>({ column, width, prevWidths }: Header cursor: column.sortable ? "pointer" : undefined, width: width || undefined, minWidth: width || undefined, - left: column.frozen ? prevWidths.reduce((pv, c) => pv + c, 0) : undefined + left: column.frozen ? prevWidth : undefined }; // function(s) @@ -112,7 +112,7 @@ const Header = forwardRef(({ children, ...rest }: HeaderProps, ref: any) => { key={c.key} column={c} width={pixelWidths[i]} - prevWidths={pixelWidths.slice(0, i)} + prevWidth={c.frozen ? pixelWidths.slice(0, i).reduce((pv, c) => pv + c, 0) : 0} /> ))} </div> diff --git a/src/Row.tsx b/src/Row.tsx index ee956e6..c28aa3a 100644 --- a/src/Row.tsx +++ b/src/Row.tsx @@ -10,7 +10,7 @@ interface TableCellProps<T> { row: T; index: number; width?: number; - prevWidths: number[]; + prevWidth: number; column: ColumnProps<T>; isExpanded: boolean; clearSizeCache: CacheFunction; @@ -70,7 +70,7 @@ const TableCell = React.memo(function <T>({ width, column, isExpanded, - prevWidths, + prevWidth, clearSizeCache, onExpanderClick }: TableCellProps<T>) { @@ -78,7 +78,7 @@ const TableCell = React.memo(function <T>({ const style: React.CSSProperties = { width: width || undefined, minWidth: width || undefined, - left: column.frozen ? prevWidths.reduce((pv, c) => pv + c, 0) : undefined + left: column.frozen ? prevWidth : undefined }; // expander @@ -275,7 +275,7 @@ function Row<T>({ isExpanded={isExpanded} clearSizeCache={clearSizeCache} onExpanderClick={onExpanderClick} - prevWidths={pixelWidths.slice(0, i)} + prevWidth={c.frozen ? pixelWidths.slice(0, i).reduce((pv, c) => pv + c, 0) : 0} /> ))} </RowContainer>