From e76dd9509849b3689faebc12798811c93448bdcb Mon Sep 17 00:00:00 2001 From: radubrehar Date: Thu, 17 Oct 2024 13:28:34 +0300 Subject: [PATCH] improve exports and release version patch --- .../context-menus/custom-filter-menu.page.tsx | 223 ++++++++++++++++++ .../InfiniteTableColumnHeaderFilter.tsx | 4 +- .../InfiniteTableHeaderCell.tsx | 47 ++-- .../hooks/useInfinitePortalContainer.ts | 13 + source/src/components/InfiniteTable/index.tsx | 2 + .../types/InfiniteTableColumn.ts | 3 + .../components/InfiniteTable/types/index.ts | 2 + .../components/InfiniteTable/utilities.css.ts | 1 + .../src/components/hooks/useOverlay/index.tsx | 63 +++-- .../docs/learn/columns/column-headers.page.md | 14 ++ ...column-filters-visibility-example.page.tsx | 74 ++++++ .../reference/datasource-props/index.page.md | 12 + .../reference/infinite-table-props.page.md | 15 ++ 13 files changed, 432 insertions(+), 41 deletions(-) create mode 100644 examples/src/pages/tests/table/context-menus/custom-filter-menu.page.tsx create mode 100644 source/src/components/InfiniteTable/hooks/useInfinitePortalContainer.ts create mode 100644 www/content/docs/reference/column-filters-visibility-example.page.tsx diff --git a/examples/src/pages/tests/table/context-menus/custom-filter-menu.page.tsx b/examples/src/pages/tests/table/context-menus/custom-filter-menu.page.tsx new file mode 100644 index 00000000..30b554c9 --- /dev/null +++ b/examples/src/pages/tests/table/context-menus/custom-filter-menu.page.tsx @@ -0,0 +1,223 @@ +import * as React from 'react'; + +import { + InfiniteTable, + InfiniteTablePropColumns, + DataSource, + useInfiniteHeaderCell, + useInfinitePortalContainer, + alignNode, + InfiniteTableColumn, +} from '@infinite-table/infinite-react'; +import { createPortal } from 'react-dom'; +import { FilterIcon } from 'lucide-react'; + +type Developer = { + id: number; + + firstName: string; + lastName: string; + + currency: string; + preferredLanguage: string; + stack: string; + canDesign: 'yes' | 'no'; + + age: number; +}; + +const data: Developer[] = [ + { + id: 1, + firstName: 'John', + lastName: 'Bob', + age: 20, + canDesign: 'yes', + currency: 'USD', + preferredLanguage: 'JavaScript', + stack: 'frontend', + }, + { + id: 2, + firstName: 'Marry', + lastName: 'Bob', + age: 25, + canDesign: 'yes', + currency: 'USD', + preferredLanguage: 'JavaScript', + stack: 'frontend', + }, + { + id: 3, + firstName: 'Bill', + lastName: 'Bobson', + age: 30, + canDesign: 'no', + currency: 'CAD', + preferredLanguage: 'TypeScript', + stack: 'frontend', + }, + { + id: 4, + firstName: 'Mark', + lastName: 'Twain', + age: 31, + canDesign: 'yes', + currency: 'CAD', + preferredLanguage: 'Rust', + stack: 'backend', + }, + { + id: 5, + firstName: 'Matthew', + lastName: 'Hilson', + age: 29, + canDesign: 'yes', + currency: 'CAD', + preferredLanguage: 'Go', + stack: 'backend', + }, +]; + +function ColumnFilterMenuIcon() { + const { renderBag, htmlElementRef: alignToRef } = useInfiniteHeaderCell(); + + const portalContainer = useInfinitePortalContainer(); + + const [visible, setVisible] = React.useState(false); + + React.useEffect(() => { + if (!domRef.current || !alignToRef.current) { + return; + } + + alignNode(domRef.current, { + alignTo: alignToRef.current, + alignPosition: [ + ['TopRight', 'BottomRight'], + ['TopRight', 'BottomRight'], + ], + }); + }); + + const domRef = React.useRef(null); + + return ( +
{ + if (e.nativeEvent) { + // @ts-ignore + e.nativeEvent.__insideMenu = true; + } + }} + onPointerDown={(event) => { + event.stopPropagation(); + + if (visible) { + setVisible(false); + return; + } + + setVisible(true); + + function handleMouseDown(event: MouseEvent) { + // @ts-ignore + if (event.__insideMenu !== true) { + setVisible(false); + document.documentElement.removeEventListener( + 'mousedown', + handleMouseDown, + ); + } + } + document.documentElement.addEventListener('mousedown', handleMouseDown); + }} + > + + {createPortal( +
+ {renderBag.filterEditor} +
, + portalContainer!, + )} +
+ ); +} + +const customHeader: InfiniteTableColumn['renderHeader'] = ({ + renderBag, +}) => { + return ( + <> + {renderBag.header} +
+ + {renderBag.filterIcon} + + {renderBag.menuIcon} + + + ); +}; + +const columns: InfiniteTablePropColumns = { + firstName: { + field: 'firstName', + renderHeader: customHeader, + defaultFilterable: false, + }, + age: { + field: 'age', + type: 'number', + + renderHeader: customHeader, + }, + + stack: { field: 'stack', renderMenuIcon: false }, + currency: { field: 'currency' }, +}; + +export default () => { + return ( + <> + + + data={data} + primaryKey="id" + defaultFilterValue={[]} + > + + domProps={{ + style: { + margin: '5px', + + height: '80vh', + width: '80vw', + border: '1px solid gray', + position: 'relative', + }, + }} + showColumnFilters={true} + columnDefaultWidth={300} + columnMinWidth={50} + columns={columns} + /> + + + + ); +}; diff --git a/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableColumnHeaderFilter.tsx b/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableColumnHeaderFilter.tsx index e0a339b5..bcdcb9f9 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableColumnHeaderFilter.tsx +++ b/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableColumnHeaderFilter.tsx @@ -4,6 +4,7 @@ import { join } from '../../../../utils/join'; import { useInfiniteTable } from '../../hooks/useInfiniteTable'; import { useInfiniteTableState } from '../../hooks/useInfiniteTableState'; +import { height } from '../../utilities.css'; import { FilterIcon } from '../icons/FilterIcon'; import { getColumnLabel } from './getColumnLabel'; @@ -109,8 +110,7 @@ export function InfiniteTableColumnHeaderFilterEmpty() { onPointerDown={stopPropagation} className={`${InfiniteTableColumnHeaderFilterClassName} ${HeaderFilterRecipe( { active: false }, - )}`} - style={{ height: '100%' }} + )} ${height['50%']}`} /> ); } diff --git a/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeaderCell.tsx b/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeaderCell.tsx index 044f0bfc..3e650a66 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeaderCell.tsx +++ b/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeaderCell.tsx @@ -268,10 +268,13 @@ export function InfiniteTableHeaderCell( column.components?.MenuIcon || components?.MenuIcon || MenuIcon; const menuIcon = ; + const domRef = useRef(null); + const initialRenderParam: InfiniteTableColumnHeaderParam = { horizontalLayoutPageIndex, dragging, domRef: ref, + htmlElementRef: domRef, insideColumnMenu: false, column, columnsMap, @@ -466,8 +469,6 @@ export function InfiniteTableHeaderCell( return all; }; - const domRef = useRef(null); - useEffect(() => { let clearOnResize: null | (() => void) = null; const node = domRef.current; @@ -593,6 +594,27 @@ export function InfiniteTableHeaderCell( 'data-sort-index': `${column.computedSortIndex ?? -1}`, }; + const columnFilterEditor = column.computedFilterable ? ( + + ) : ( + + ); + + renderParam.renderBag.filterEditor = columnFilterEditor; + return ( @@ -643,26 +665,7 @@ export function InfiniteTableHeaderCell( cssEllipsis={headerCSSEllipsis} afterChildren={ <> - {showColumnFilters ? ( - column.computedFilterable ? ( - - ) : ( - - ) - ) : null} + {showColumnFilters ? columnFilterEditor : null} {resizeHandle} } diff --git a/source/src/components/InfiniteTable/hooks/useInfinitePortalContainer.ts b/source/src/components/InfiniteTable/hooks/useInfinitePortalContainer.ts new file mode 100644 index 00000000..e99b948d --- /dev/null +++ b/source/src/components/InfiniteTable/hooks/useInfinitePortalContainer.ts @@ -0,0 +1,13 @@ +import { useMasterDetailContext } from '../../DataSource/publicHooks/useDataSourceState'; +import { useInfiniteTable } from './useInfiniteTable'; + +export function useInfinitePortalContainer() { + const masterContext = useMasterDetailContext(); + + const masterState = masterContext ? masterContext.getMasterState() : null; + const infiniteState = useInfiniteTable().getState(); + + const portalContainer = (masterState || infiniteState).portalDOMRef.current; + + return portalContainer; +} diff --git a/source/src/components/InfiniteTable/index.tsx b/source/src/components/InfiniteTable/index.tsx index 70ada168..f3c89f10 100644 --- a/source/src/components/InfiniteTable/index.tsx +++ b/source/src/components/InfiniteTable/index.tsx @@ -93,6 +93,7 @@ import { DEBUG_NAME } from './InfiniteDebugName'; import { useToggleWrapRowsHorizontally } from './hooks/useToggleWrapRowsHorizontally'; import { useHorizontalLayout } from './hooks/useHorizontalLayout'; import { useDebugMode } from './hooks/useDebugMode'; +import { useInfinitePortalContainer } from './hooks/useInfinitePortalContainer'; export const InfiniteTableClassName = internalProps.rootClassName; @@ -605,6 +606,7 @@ export { useInfiniteHeaderCell, useInfiniteColumnEditor, useInfiniteColumnFilterEditor, + useInfinitePortalContainer, eventMatchesKeyboardShortcut, useGridScroll, useVisibleColumnSizes, diff --git a/source/src/components/InfiniteTable/types/InfiniteTableColumn.ts b/source/src/components/InfiniteTable/types/InfiniteTableColumn.ts index 10042c0c..44438810 100644 --- a/source/src/components/InfiniteTable/types/InfiniteTableColumn.ts +++ b/source/src/components/InfiniteTable/types/InfiniteTableColumn.ts @@ -68,11 +68,13 @@ export type InfiniteTableColumnHeaderParam< menuIcon?: Renderable; menuIconProps?: MenuIconProps; filterIcon?: Renderable; + filterEditor?: Renderable; selectionCheckBox?: Renderable; }; } & ( | { domRef: InfiniteTableCellProps['domRef']; + htmlElementRef: React.MutableRefObject; insideColumnMenu: false; } | { @@ -160,6 +162,7 @@ export type InfiniteTableColumnCellContextType< export type InfiniteTableHeaderCellContextType = InfiniteTableColumnHeaderParam & { domRef: InfiniteTableCellProps['domRef']; + htmlElementRef: React.MutableRefObject; insideColumnMenu: false; }; diff --git a/source/src/components/InfiniteTable/types/index.ts b/source/src/components/InfiniteTable/types/index.ts index 4143dc28..a9082646 100644 --- a/source/src/components/InfiniteTable/types/index.ts +++ b/source/src/components/InfiniteTable/types/index.ts @@ -57,6 +57,7 @@ import type { InfiniteTablePropMultiSortBehavior, InfiniteTablePropKeyboardShorcut, InfiniteTablePropColumnGroupVisibility, + InfiniteTablePropGetContextMenuItems, } from './InfiniteTableProps'; import type { InfiniteTableState } from './InfiniteTableState'; @@ -115,4 +116,5 @@ export type { InfiniteTableColumnValueFormatterParams, ScrollStopInfo, TableRenderRange, + InfiniteTablePropGetContextMenuItems, }; diff --git a/source/src/components/InfiniteTable/utilities.css.ts b/source/src/components/InfiniteTable/utilities.css.ts index fce7e64d..e11d228e 100644 --- a/source/src/components/InfiniteTable/utilities.css.ts +++ b/source/src/components/InfiniteTable/utilities.css.ts @@ -105,6 +105,7 @@ export const userSelect = styleVariants({ export const height = styleVariants({ '100%': { height: '100%' }, + '50%': { height: '50%' }, '0': { height: '0' }, }); export const width = styleVariants({ diff --git a/source/src/components/hooks/useOverlay/index.tsx b/source/src/components/hooks/useOverlay/index.tsx index 6b589139..7b412921 100644 --- a/source/src/components/hooks/useOverlay/index.tsx +++ b/source/src/components/hooks/useOverlay/index.tsx @@ -118,7 +118,7 @@ function OverlayContent( useEffect(() => { return props.realign.onChange((handle) => { if (nodeRef.current && handle) { - alignOverlayNode(nodeRef.current, handle); + alignNode(nodeRef.current, handle); } }); }, [props.realign]); @@ -128,7 +128,7 @@ function OverlayContent( style={{ position: 'absolute', top: 0, left: 0 }} ref={useCallback((node: HTMLDivElement) => { if (node) { - alignOverlayNode(node, props); + alignNode(node, props); // const rect = alignOverlayNode(node, props); // const realignEvent = new CustomEvent('realign', { // bubbles: true, @@ -147,7 +147,7 @@ function OverlayContent( ); } -async function alignOverlayNode( +export async function alignNode( node: HTMLDivElement, params: OverlayShowParams, ) { @@ -314,10 +314,15 @@ globalThis.allhandles = {}; //@ts-ignore globalThis.thehandles = {}; +export type UpdateOverlayContentFn = ( + content: ReactNode | (() => ReactNode), + options?: { skipRealign?: boolean }, +) => void; + export type ShowOverlayFn = ( content: ReactNode | (() => ReactNode), params: OverlayShowParams, -) => VoidFunction | undefined; +) => UpdateOverlayContentFn; export function useOverlay(params: OverlayParams) { const rootParams = params; @@ -354,22 +359,46 @@ export function useOverlay(params: OverlayParams) { let handle = handles.get(key); - const childrenFn = () => { - const children = typeof content === 'function' ? content() : content; + const getChildrenFnForContent = ( + content: ReactNode | (() => ReactNode), + ) => { + return () => { + const children = typeof content === 'function' ? content() : content; + + return injectPortalContainerAndConstrainInMenuChild( + children, + rootParams.portalContainer, + params.constrainTo ?? rootParams.constrainTo, + ); + }; + }; + + const childrenFn = getChildrenFnForContent(content); + + const updateOverlay: UpdateOverlayContentFn = ( + overlayContent, + options, + ) => { + if (!handle) { + return; + } + const childrenFn = getChildrenFnForContent(overlayContent); + + Object.assign(handle, { children: childrenFn }); + updateContent(); + const skipRealign = !!options?.skipRealign; + const shouldRealign = !skipRealign; - return injectPortalContainerAndConstrainInMenuChild( - children, - rootParams.portalContainer, - params.constrainTo ?? rootParams.constrainTo, - ); + if (shouldRealign) { + setHandleToRealign(handle.key); + setRealignTimestamp(Date.now()); + } }; if (handle) { - Object.assign(handle, params, { children: childrenFn }); - updateContent(); - setHandleToRealign(handle.key); - setRealignTimestamp(Date.now()); - return; + Object.assign(handle, params); + updateOverlay(content); + return updateOverlay; } handle = { @@ -385,7 +414,7 @@ export function useOverlay(params: OverlayParams) { updateContent(); - return () => updateContent(); + return updateOverlay; }, [handles, rootParams.portalContainer, updateContent], ); diff --git a/www/content/docs/learn/columns/column-headers.page.md b/www/content/docs/learn/columns/column-headers.page.md index f0758ed6..3c84c3c1 100644 --- a/www/content/docs/learn/columns/column-headers.page.md +++ b/www/content/docs/learn/columns/column-headers.page.md @@ -86,6 +86,18 @@ This function is called with the same object as the first argument, but it also So if you specify a custom renderHeader function, it's up to you to use the results of the previous functions in the pipeline, in order to fully take control of the column header. +#### Available properties on the renderBag + +The `renderBag` object contains the following properties available to the render functions of the column header: + +- `header` - the label of the column header. +- `sortIcon` - the default sort icon +- `filterIcon` - the filter icon - displayed when the current column is used in filtering +- `filterEditor` - the current filter editor +- `menuIcon` - the menu icon that can be clicked to open the column menu +- `selectionCheckBox` - the selection check box - displays the current selection status and controls the selection for all rows. +- `all` - all of the above combined together in a `React.Fragment`. + ### Customizing the Sort Icon For customizing the sort icon, use the column.renderSortIcon function. @@ -229,6 +241,8 @@ The `firstName` column will show a custom filter icon when filtered. + + ### Customizing the Selection Checkbox For customizing the selection checkbox in the column header, use the column.renderHeaderSelectionCheckBox function. diff --git a/www/content/docs/reference/column-filters-visibility-example.page.tsx b/www/content/docs/reference/column-filters-visibility-example.page.tsx new file mode 100644 index 00000000..8d2c8c7b --- /dev/null +++ b/www/content/docs/reference/column-filters-visibility-example.page.tsx @@ -0,0 +1,74 @@ +import * as React from 'react'; + +import { + DataSourceData, + InfiniteTable, + InfiniteTablePropColumns, + DataSource, +} from '@infinite-table/infinite-react'; + +type Developer = { + id: number; + firstName: string; + lastName: string; + + currency: string; + preferredLanguage: string; + stack: string; + canDesign: 'yes' | 'no'; + + salary: number; +}; + +const data: DataSourceData = () => { + return fetch(process.env.NEXT_PUBLIC_BASE_URL + `/developers1k-sql?`) + .then((r) => r.json()) + .then((data: Developer[]) => data); +}; + +const columns: InfiniteTablePropColumns = { + id: { + field: 'id', + type: 'number', + defaultWidth: 100, + }, + salary: { + field: 'salary', + type: 'number', + }, + + firstName: { + field: 'firstName', + }, + stack: { field: 'stack' }, + currency: { field: 'currency', defaultFilterable: false }, +}; + +export default () => { + const [showColumnFilters, setShowColumnFilters] = React.useState(true); + return ( + <> + +
+ +
+ + data={data} + primaryKey="id" + defaultFilterValue={[]} + filterDelay={0} + filterMode="local" + > + + columnDefaultWidth={150} + columnMinWidth={50} + columns={columns} + showColumnFilters={showColumnFilters} + /> + +
+ + ); +}; diff --git a/www/content/docs/reference/datasource-props/index.page.md b/www/content/docs/reference/datasource-props/index.page.md index ce8ffb31..f057c1d4 100644 --- a/www/content/docs/reference/datasource-props/index.page.md +++ b/www/content/docs/reference/datasource-props/index.page.md @@ -304,6 +304,12 @@ For the controlled version, and more details on the shape of the objects in the + + +You can control the visibility of the column filters by using the prop. + + + @@ -798,6 +804,12 @@ The objects in this array have the following shape: + + +You can control the visibility of the column filters by using the prop. + + + diff --git a/www/content/docs/reference/infinite-table-props.page.md b/www/content/docs/reference/infinite-table-props.page.md index 421ef1f9..8adc63e6 100644 --- a/www/content/docs/reference/infinite-table-props.page.md +++ b/www/content/docs/reference/infinite-table-props.page.md @@ -181,6 +181,21 @@ The details for each city shows a DataGrid with developers in that city. + + +> Whether to show the column filters or not (only applicable when the `` is configured with filtering - either with or ). + +When the `` is configured with , the column filters will be shown by default. Specify this prop as `false` to hide the column filters. + + + +```tsx file="column-filters-visibility-example.page.tsx" +``` + + + + + > Specifies the default expanded/collapsed state of row details.