From e5b941f6d2219310851aa9ce52c12fdd88d2e62e Mon Sep 17 00:00:00 2001 From: mckervinc Date: Thu, 28 Sep 2023 11:47:13 -0400 Subject: [PATCH 1/2] update footer props --- .npmignore | 1 + example/src/Props.tsx | 33 +++++++++-- example/src/Snippet.tsx | 8 +-- example/src/examples/04-custom.tsx | 10 ++-- example/src/examples/10-footer.tsx | 94 +++++++++++++++++++++++++----- index.d.ts | 25 +++++++- package.json | 2 +- src/Footer.tsx | 73 +++++++++++++++++++++-- src/Table.tsx | 37 ++++++++---- src/TableContext.tsx | 8 ++- src/main.css | 7 +++ src/util.ts | 7 ++- 12 files changed, 254 insertions(+), 51 deletions(-) diff --git a/.npmignore b/.npmignore index 3011260..880cb96 100644 --- a/.npmignore +++ b/.npmignore @@ -34,3 +34,4 @@ rollup.config.js .env.test.local .env.production.local .travis.yml +.github diff --git a/example/src/Props.tsx b/example/src/Props.tsx index 14aacf1..9cf9519 100644 --- a/example/src/Props.tsx +++ b/example/src/Props.tsx @@ -121,6 +121,17 @@ const data: PropData[] = [ description: "The height of the table in pixels. If no height is specified, this will try to fill the height of the parent. If the parent node does not have a height specified, a height for the table will be calculated based on the default header height and the rowHeight or the defaultRowHeight (37)." }, + { + prop: "minTableHeight", + type: "number", + description: "The min height of the table in pixels." + }, + { + prop: "maxTableHeight", + type: "number", + description: + "The max height of the table in pixels. If tableHeight is specified, this is ignored." + }, { prop: "tableWidth", type: "number", @@ -179,7 +190,7 @@ const data: PropData[] = [ }, { prop: "headerClassname", - type: "object", + type: "string", description: "Add custom css className to the table header" }, { @@ -212,9 +223,19 @@ const data: PropData[] = [ }, { prop: "footerComponent", - type: "() => Element", + type: "(props: FooterProps) => Element", description: "You can provide an optional footer" }, + { + prop: "footerStyle", + type: "object", + description: "Add custom css styles to the table footer" + }, + { + prop: "footerClassname", + type: "string", + description: "Add custom css className to the table footer" + }, { prop: "stickyFooter", type: "boolean", @@ -266,7 +287,7 @@ const Contact = ({ row, index, style, clearSizeCache }) => { mounted.current = true; }, [showInfo]); - return ; + return ; }; const columns = [{ @@ -336,7 +357,7 @@ const Props = () => ( The HeaderElement is an element that takes in props that contains a style, onclick, and sortDirection. See below for an example: - + @@ -352,7 +373,7 @@ const Props = () => ( index in the data array, and a function to reset the rowHeight (if needed). See below for an example: - + @@ -363,7 +384,7 @@ const Props = () => ( The ExpandedElement is an element that takes in props that contains whether or not the row is expanded, as well as a function to toggle the row expansion. See below for an example: - +
diff --git a/example/src/Snippet.tsx b/example/src/Snippet.tsx index d015737..b69879b 100644 --- a/example/src/Snippet.tsx +++ b/example/src/Snippet.tsx @@ -1,12 +1,12 @@ import { useRef, useState } from "react"; import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter"; -import jsx from "react-syntax-highlighter/dist/esm/languages/prism/jsx"; +import tsx from "react-syntax-highlighter/dist/esm/languages/prism/tsx"; import okaidia from "react-syntax-highlighter/dist/esm/styles/prism/okaidia"; import { Icon, Menu, Popup } from "semantic-ui-react"; import styled from "styled-components"; import { copy } from "./util"; -SyntaxHighlighter.registerLanguage("jsx", jsx); +SyntaxHighlighter.registerLanguage("tsx", tsx); const Container = styled.div` position: relative; @@ -56,7 +56,7 @@ interface SnippetProps { edit?: boolean; } -const Snippet = ({ code, copy: showCopy = true, edit = true }: SnippetProps) => { +const Snippet = ({ code, copy: showCopy = true, edit = false }: SnippetProps) => { const ref = useRef(0); const [open, setOpen] = useState(false); const onOpen = () => { @@ -98,7 +98,7 @@ const Snippet = ({ code, copy: showCopy = true, edit = true }: SnippetProps) => )} - + {code.trim()} diff --git a/example/src/examples/04-custom.tsx b/example/src/examples/04-custom.tsx index d5eaaf7..b89aff8 100644 --- a/example/src/examples/04-custom.tsx +++ b/example/src/examples/04-custom.tsx @@ -336,29 +336,29 @@ const columns: ColumnProps[] = [ key: "avatar", header: "Profile Photo", width: 150, - content: ({ row}) => + content: ({ row }) => }, { key: "email", header: "Email", - content: ({ row}) => + content: ({ row }) => }, { key: "firstName", header: "First", width: 100, - content: ({ row}) => {row.firstName} + content: ({ row }) => {row.firstName} }, { key: "lastName", header: "Last", width: 100, - content: ({ row}) => {row.lastName} + content: ({ row }) => {row.lastName} }, { key: "country", header: "Country", - content: ({ row}) => + content: ({ row }) => !countryMap[row.country] ? ( \`No flag for this country: \${row.country.toUpperCase()}\` ) : ( diff --git a/example/src/examples/10-footer.tsx b/example/src/examples/10-footer.tsx index 73c7108..b2bfdf6 100644 --- a/example/src/examples/10-footer.tsx +++ b/example/src/examples/10-footer.tsx @@ -19,37 +19,33 @@ const columns: ColumnProps[] = [ { key: "id", header: "ID", - sortable: true, width: 50 }, { key: "firstName", header: "First", - sortable: true, width: 120 }, { key: "lastName", header: "Last", - sortable: true, width: 120 }, { key: "email", header: "Email", - sortable: true, width: 250 } ]; const Footer = styled.div` background-color: white; - padding: 8px; `; const Example10 = () => { // hooks const [sticky, setSticky] = useState(true); + const [simple, setSimple] = useState(true); return ( <> @@ -57,11 +53,20 @@ const Example10 = () => {
-

Change footer stickyness

+

Change footer properties

setSimple(prev => !prev)} + /> + + + setSticky(prev => !prev)} @@ -73,7 +78,9 @@ const Example10 = () => {

Controlled Props:

-                {"{\n  stickyFooter: "}
+                {"{\n  simpleFooter: "}
+                {simple.toString()}
+                {",\n  stickyFooter: "}
                 {sticky.toString()}
                 {"\n}"}
               
@@ -87,7 +94,30 @@ const Example10 = () => { columns={columns} stickyFooter={sticky} tableHeight={400} - footerComponent={() =>
Footer content
} + footerStyle={{ backgroundColor: "white" }} + footerComponent={({ widths }) => ( +
+ {simple ? ( + "Hello, World" + ) : ( +
+ {columns.map((c, i) => { + const width = `${widths[i]}px`; + const style: React.CSSProperties = { + width, + minWidth: width, + padding: "8px" + }; + return ( +
+ Footer Cell +
+ ); + })} +
+ )} +
+ )} /> ); @@ -96,12 +126,27 @@ const Example10 = () => { const Source = ` const data = [/* ... */]; -const Footer = styled.div\` - background-color: white; - padding: 8px; -\`; +const Footer = ({ children }) => ( +
+ {children} +
+); + +const SimpleFooter = ({ stickyFooter }) => { + return ( +
Hello, World
} + /> + ); +}; -const Controlled = ({ stickyFooter }) => { +const ComplexFooter = ({ stickyFooter }) => { return ( { columns={columns} tableHeight={400} stickyFooter={stickyFooter} - footerComponent={() =>
Footer content
} + footerStyle={{ backgroundColor: "white" }} + footerComponent={({ widths }) => ( +
+
+ {columns.map((c, i) => { + const width = \`\${widths[i]}px\`; + const style: React.CSSProperties = { + width, + minWidth: width, + padding: "8px" + }; + return ( +
+ Footer Cell +
+ ); + })} +
+
+ )} /> ); }; diff --git a/index.d.ts b/index.d.ts index cc00a7e..9632840 100644 --- a/index.d.ts +++ b/index.d.ts @@ -104,6 +104,13 @@ export interface SubComponentProps { clearSizeCache: CacheFunction; } +export interface FooterProps { + /** + * exposes the widths of each column to the footer + */ + widths: number[]; +} + export interface ColumnProps { /** * The unique identifier for a particular column. This is also used as an index @@ -190,6 +197,14 @@ export interface TableProps { * Specify the height of the table in pixels. */ tableHeight?: number; + /** + * Specify the minimum height of the table in pixels. + */ + minTableHeight?: number; + /** + * Specify the maximum height of the table in pixels. + */ + maxTableHeight?: number; /** * Specify the width of the table in pixels. */ @@ -234,6 +249,14 @@ export interface TableProps { * a function that takes the index of the row and returns an object. */ rowClassname?: string | ((index: number) => string); + /** + * React styles used for customizing the footer. + */ + footerStyle?: CSSProperties; + /** + * a className used to customize the footer + */ + footerClassname?: string; /** * generates a unique identifier for the row * @param row the row @@ -248,7 +271,7 @@ export interface TableProps { /** * optionally add a footer */ - footerComponent?: () => React.ReactNode; + footerComponent?: (props: FooterProps) => React.ReactNode; /** * When a column has `expander`, this component will be rendered under the row. */ diff --git a/package.json b/package.json index a224d0b..57b74a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-fluid-table", - "version": "0.4.5", + "version": "0.4.6", "description": "A React table inspired by react-window", "author": "Mckervin Ceme ", "license": "MIT", diff --git a/src/Footer.tsx b/src/Footer.tsx index e94b184..2ffb0cd 100644 --- a/src/Footer.tsx +++ b/src/Footer.tsx @@ -1,9 +1,69 @@ -import React, { useContext } from "react"; +import React, { useCallback, useContext, useEffect, useRef } from "react"; import { TableContext } from "./TableContext"; -import { cx } from "./util"; +import { cx, findTableByUuid } from "./util"; const Footer = () => { - const { uuid, stickyFooter, footerComponent: FooterComponent } = useContext(TableContext); + const { + uuid, + stickyFooter, + pixelWidths, + footerStyle, + footerClassname, + footerComponent: FooterComponent + } = useContext(TableContext); + const scroll = useRef(false); + const ref = useRef(null); + + // constants + const width = pixelWidths.reduce((pv, c) => pv + c, 0); + const style: React.CSSProperties = { + minWidth: stickyFooter ? undefined : `${width}px`, + ...(footerStyle || {}) + }; + + // functions + const onScroll = useCallback( + (target: HTMLElement | null, element: HTMLElement | null) => { + if (scroll.current || !element || !stickyFooter) { + return; + } + + const left = target?.scrollLeft || 0; + scroll.current = true; + element.scrollLeft = left; + setTimeout(() => { + scroll.current = false; + }, 0); + }, + [uuid, stickyFooter] + ); + + const listener = useCallback( + (ev: Event) => { + onScroll(ev.target as HTMLDivElement, ref.current); + }, + [onScroll] + ); + + // effects + // add scroll listener + useEffect(() => { + if (uuid) { + const e = findTableByUuid(uuid); + if (e) { + e.addEventListener("scroll", listener); + } + } + + return () => { + if (uuid) { + const e = findTableByUuid(uuid); + if (e) { + e.removeEventListener("scroll", listener); + } + } + }; + }, [uuid, listener]); // render if (!FooterComponent) { @@ -12,10 +72,13 @@ const Footer = () => { return (
onScroll(e.target as HTMLDivElement, findTableByUuid(uuid))} > - +
); }; diff --git a/src/Table.tsx b/src/Table.tsx index 73a0907..1120ce9 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -310,6 +310,10 @@ const Table = forwardRef( headerStyle, headerClassname, footerComponent, + footerStyle, + footerClassname, + maxTableHeight, + minTableHeight, borders = false, minColumnWidth = 80, stickyFooter = false, @@ -336,7 +340,9 @@ const Table = forwardRef( headerStyle, headerClassname, stickyFooter, - footerComponent + footerComponent, + footerClassname, + footerStyle }} > {typeof tableHeight === "number" && typeof tableWidth === "number" ? ( @@ -349,15 +355,26 @@ const Table = forwardRef( /> ) : ( - {({ height, width }) => ( - - )} + {({ height, width }) => { + const componentHeight = + tableHeight || + (maxTableHeight !== undefined && maxTableHeight >= 0 + ? Math.min( + height || guessTableHeight(rest.rowHeight || 0, rest.data.length), + maxTableHeight + ) + : height || guessTableHeight(rest.rowHeight || 0)); + + return ( + + ); + }} )} diff --git a/src/TableContext.tsx b/src/TableContext.tsx index 0c67c34..57d281a 100644 --- a/src/TableContext.tsx +++ b/src/TableContext.tsx @@ -1,5 +1,5 @@ import React, { createContext, useEffect, useReducer, useRef } from "react"; -import { ColumnProps, SortDirection } from "../index"; +import { ColumnProps, FooterProps, SortDirection } from "../index"; interface Action { type: string; @@ -24,7 +24,7 @@ interface TableState extends ReactContext { sortColumn: string | null; sortDirection: SortDirection; stickyFooter: boolean; - footerComponent?: () => React.ReactNode; + footerComponent?: (props: FooterProps) => React.ReactNode; expanded: { [key: string | number]: boolean; }; @@ -33,6 +33,8 @@ interface TableState extends ReactContext { tableStyle?: React.CSSProperties; headerStyle?: React.CSSProperties; headerClassname?: string; + footerStyle?: React.CSSProperties; + footerClassname?: string; } const baseState: TableState = { @@ -58,6 +60,8 @@ const fields = [ "tableStyle", "headerStyle", "headerClassname", + "footerStyle", + "footerClassname", "stickyFooter", "footerComponent" ]; diff --git a/src/main.css b/src/main.css index bac21c7..2b49490 100644 --- a/src/main.css +++ b/src/main.css @@ -90,6 +90,13 @@ bottom: 0; left: 0; z-index: 1; + overflow-x: auto; + -ms-overflow-style: none; /* Internet Explorer 10+ */ + scrollbar-width: none; /* Firefox */ +} + +.react-fluid-table-footer.sticky::-webkit-scrollbar { + display: none; /* Safari and Chrome */ } .row-container { diff --git a/src/util.ts b/src/util.ts index d2c0f2c..ee83e36 100644 --- a/src/util.ts +++ b/src/util.ts @@ -37,6 +37,9 @@ export const randomString = (num: number) => { return result; }; +export const findTableByUuid = (uuid: string): HTMLElement | null => + document.querySelector(`[data-table-key='${uuid}']`); + export const findHeaderByUuid = (uuid: string): HTMLElement | null => document.querySelector(`[data-header-key='${uuid}-header']`); @@ -44,9 +47,9 @@ export const findRowByUuidAndKey = (uuid: string, key: string | number): HTMLEle document.querySelector(`[data-row-key='${uuid}-${key}']`); // table utilities -export const guessTableHeight = (rowHeight?: number) => { +export const guessTableHeight = (rowHeight: number, size = 10) => { const height = Math.max(rowHeight || DEFAULT_ROW_HEIGHT, 10); - return height * 10 + DEFAULT_HEADER_HEIGHT; + return height * size + DEFAULT_HEADER_HEIGHT; }; export const calculateColumnWidths = ( From bfb8a3850520e1bd5c166c960645a07f671bd240 Mon Sep 17 00:00:00 2001 From: mckervinc Date: Thu, 28 Sep 2023 11:55:32 -0400 Subject: [PATCH 2/2] update changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e1ce60..0350f61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # CHANGELOG +## 0.4.6 + +_2023-09-28_ + +### Features + +- added `minTableHeight` and `maxTableHeight` +- added footer styling with `footerStyle` and `footerClassname` +- expose column widths to `footerComponent` to help expose widths + ## 0.4.5 _2023-09-27_