UNPKG

@neo4j-ndl/react

Version:

React implementation of Neo4j Design System

1,818 lines (1,625 loc) 82.5 kB
# DataGrid Import: `import { DataGrid } from '@neo4j-ndl/react'` ## Props ### DataGrid | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `components` | `DataGridComponentsInterface<T>` | | | Table sub-component overrides | | `isAutoResizingColumns` | `boolean` | | `true` | Enables automatic resizing of columns. If `isResizable` is `true`, the columns will only be automatically resized on initial render of the table. If `isResizable` is `false`, the columns will be resized automatically to always fit the table width (assuming that column maxWidths allow for it). Note: Action columns are a fixed size of 40px, and Pinned columns are not resized automatically. @defaultValue true | | `isKeyboardNavigable` | `boolean` | | `true` | Enables keyboard navigation. To control keyboard navigation programmatically for edge cases, pass a `ref` to `tableNavRef` | | `isLoading` | `boolean` | | `false` | Enables table's loading state | | `isResizable` | `boolean` | | `true` | Resizing the width of columns | | `isSkeletonLoading` | `boolean` | | `false` | | | `portalTarget` | `HTMLElement \| null` | | `document.body` | The target element for portaled elements to be rendered in, e.g. the modal root if the data grid is inside a modal. This is used in for example the DropdownCell, RowsPerPage, and ColumnControls. | | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. | | `rootProps` | `HtmlAttributes<"div">` | | | @deprecated Use htmlAttributes, className and style on the root element instead | | `skeletonProps` | `(SkeletonProps & { as?: ElementType<any, keyof IntrinsicElements>; } & BaseProps<ElementType<any, keyof IntrinsicElements>>)` | | | | | `styleRules` | `{ apply: { color?: { value: string; isDisabled?: boolean; }; colorRange?: { maxColor: string; maxValue: number; minColor: string; minValue: number; onColumn: string; isDisabled?: boolean \| undefined; midColor?: string \| undefined; midValue?: number \| undefined; } \| undefined; textColor?: { .....` | | | Rule-based conditional styling for cells and rows. | | `styleRuleValueConverters` | `DataGridStyleRuleValueConverters` | | | Per-column converters that teach the style-rule engine how to interpret complex cell values. | | `styling` | `Partial<{ hasZebraStriping: boolean; borderStyle: "all-sides" \| "horizontal" \| "vertical" \| "none"; headerStyle: "filled" \| "clean"; hasHoverEffects: boolean; isCompact: boolean; }>` | | `{}` | | | `tableInstance` | `Table<T>` | ✅ | | Table instance created from `useTable` | | `tableNavRef` | `RefObject<any>` | | | Reference to the tableNav instance for programmatic control | ### DataGrid.ActionButton | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `action` | `MenuItemProps` | ✅ | | | | `as` | `ElementType<any, keyof IntrinsicElements>` | | | An override of the default HTML tag of the root of the component. Can also be another React component. | | `onClose` | `() => void` | ✅ | | | | `ref` | `any` | | | A ref to apply to the root element. | ### DataGrid.ActionCell | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `cell` | `CellContext<T, unknown>` | | | | | `innerCleanIconButtonProps` | `Partial<Omit<CleanIconButtonButtonProps & { as?: ElementType<any, keyof IntrinsicElements>; } & BaseProps<ElementType<any, keyof IntrinsicElements>>, "ref">>` | | | | | `innerMenuProps` | `Partial<MenuProps & { as?: ElementType<any, keyof IntrinsicElements>; } & BaseProps<ElementType<any, keyof IntrinsicElements>>>` | | | | ### DataGrid.Body | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `children` | `ReactNode` | | | | | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. | ### DataGrid.BodyCell | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `cell` | `Cell<T, unknown>` | ✅ | | | | `children` | `ReactNode` | | | | | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. | ### DataGrid.BodyRow | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `children` | `ReactNode` | | | | | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. | | `row` | `Row<T>` | ✅ | | | ### DataGrid.ColumnControls | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `cell` | `Header<T, unknown>` | ✅ | | | | `children` | `ReactNode` | | | | | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. | ### DataGrid.DropDownCell | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `ariaLabel` | `string` | ✅ | | Aria label of the select component. Required for accessibility. | | `cell` | `CellContext<T, unknown>` | ✅ | | | | `components` | `Partial<SelectComponents<DropDownCellOption, boolean, GroupBase<DropDownCellOption>>>` | | | Override the underlying react-select components (e.g. `Option`, `SingleValue`, `MultiValue`). Merged over the Needle defaults, so only the components you pass are replaced. | | `errorText` | `ReactNode` | | | Error message. If it exists the select will be in error state. | | `helpText` | `ReactNode` | | | Help text of the select component. | | `isDisabled` | `boolean` | | | Whether the select component is disabled | | `options` | `{ value: string; label: string; }[]` | ✅ | | | | `portalTarget` | `HTMLElement` | | `document.body` | The target element to portal the menu of the select component to, e.g. the modal root if the data grid is inside a modal. Defaults to the portalTarget prop of the DataGrid root component. | | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. | ### DataGrid.Header | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `children` | `ReactNode` | | | | | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. | ### DataGrid.HeaderCell | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `cell` | `Header<T, unknown>` | ✅ | | | | `children` | `ReactNode` | | | | | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. | ### DataGrid.HeaderTitle | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `cell` | `Header<T, unknown>` | ✅ | | | | `children` | `ReactNode` | | | | | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. | ### DataGrid.InlineEditCell | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `ariaLabel` | `string` | ✅ | | | | `cell` | `CellContext<T, unknown>` | ✅ | | | | `ref` | `Ref<HTMLInputElement>` | | | A ref to apply to the root element. | | `value` | `string` | ✅ | | | ### DataGrid.LoadingPlaceholder | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `children` | `ReactNode` | | | | | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. | ### DataGrid.Navigation | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `children` | `ReactNode` | | | | | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. | ### DataGrid.NoDataIcon _No props documented._ ### DataGrid.NoDataPlaceholder | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `children` | `ReactNode` | | | | | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. | ### DataGrid.NoDataPlaceholderContentWrapper | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `children` | `ReactNode` | | | | | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. | ### DataGrid.Pagination | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `children` | `ReactNode` | | | | | `ref` | `Ref<HTMLElement>` | | | A ref to apply to the root element. | ### DataGrid.PaginationArrowButton | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `action` | `'next' \| 'previous'` | ✅ | | | | `children` | `ReactNode` | | | | | `onClick` | `(e: MouseEvent<HTMLButtonElement, MouseEvent>) => void` | | | | | `ref` | `Ref<HTMLButtonElement>` | | | A ref to apply to the root element. | ### DataGrid.PaginationNumericButton | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `children` | `ReactNode` | | | | | `currentIndex` | `string \| number` | ✅ | | | | `isSelected` | `boolean` | ✅ | | | | `onClick` | `(e: MouseEvent<HTMLButtonElement, MouseEvent>) => void` | | | | | `ref` | `Ref<HTMLButtonElement>` | | | A ref to apply to the root element. | ### DataGrid.ResizingBar | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `header` | `Header<T, unknown>` | ✅ | | | | `ref` | `Ref<HTMLButtonElement>` | | | A ref to apply to the root element. | ### DataGrid.RowsPerPage | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `children` | `ReactNode` | | | | | `portalTarget` | `HTMLElement` | | `document.body` | The target element to portal the menu of the select component to. Should be document.body normally, or the modal root if the select is inside a modal. Defaults to the portalTarget prop of the DataGrid root component. | | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. | ### DataGrid.Scrollable | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `children` | `ReactNode` | | | | | `ref` | `Ref<HTMLDivElement> & RefObject<HTMLDivElement>` | | | A ref to apply to the root element. | ### DataGrid.TableResults | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `children` | `ReactNode` | | | | | `manualPagination` | `{ from: number; to: number; totalRows: number; }` | | | | | `ref` | `Ref<HTMLSpanElement>` | | | A ref to apply to the root element. | | `resultsLabel` | `string` | | | The label to use for the results, meaning the word "results" in the sentence "Showing X-Y of Z results". @defaultValue 'results' | ## Examples ### Datagrid Column Filtering ```tsx import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { DataGrid } from '@neo4j-ndl/react'; import { createColumnHelper, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, useReactTable, } from '@tanstack/react-table'; import { TABLE_DEMO_DATA } from './staticData'; type TestDataFormat = { name: string; age: number; cypherCommand: string; }; const Component = () => { const columnHelper = createColumnHelper<TestDataFormat>(); const columns = [ columnHelper.accessor('name', { cell: (info) => info.getValue(), footer: (info) => info.column.id, header: () => 'Name', maxSize: 300, minSize: 180, size: 220, }), columnHelper.accessor('age', { cell: (info) => info.renderValue(), footer: (info) => info.column.id, header: () => 'Age', minSize: 80, }), columnHelper.accessor('cypherCommand', { cell: (info) => info.getValue(), footer: (info) => info.column.id, header: () => 'Cypher Command', minSize: 400, size: 400, }), ]; const table = useReactTable({ columnResizeMode: 'onChange', columns, data: TABLE_DEMO_DATA, debugColumns: true, debugHeaders: true, debugTable: true, defaultColumn: { maxSize: 800, minSize: 80, }, enableSorting: false, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: getPaginationRowModel(), initialState: { pagination: { pageSize: 5, }, }, }); return ( <div className="n-w-full n-bg-light-neutral-text-weakest"> <DataGrid isResizable={true} tableInstance={table} isKeyboardNavigable={true} /> </div> ); }; export default Component; ``` ### Datagrid Compact Mode ```tsx import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { DataGrid } from '@neo4j-ndl/react'; import { createColumnHelper, getCoreRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, } from '@tanstack/react-table'; import { TABLE_DEMO_DATA } from './staticData'; type TestDataFormat = { name: string; age: number; cypherCommand: string; }; const Component = () => { const columnHelper = createColumnHelper<TestDataFormat>(); const columns = [ columnHelper.accessor('name', { cell: (info) => info.getValue(), footer: (info) => info.column.id, header: () => 'Name', maxSize: 300, minSize: 180, size: 220, }), columnHelper.accessor('age', { cell: (info) => info.renderValue(), footer: (info) => info.column.id, header: () => 'Age', minSize: 80, }), columnHelper.accessor('cypherCommand', { cell: (info) => info.getValue(), footer: (info) => info.column.id, header: () => 'Cypher Command', minSize: 400, size: 400, }), ]; const table = useReactTable({ columnResizeMode: 'onChange', columns, data: TABLE_DEMO_DATA, debugColumns: true, debugHeaders: true, debugTable: true, defaultColumn: { maxSize: 800, minSize: 80, }, enableSorting: true, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), initialState: { pagination: { pageSize: 5, }, }, }); return ( <div className="n-w-full n-bg-light-neutral-text-weakest"> <DataGrid isResizable={false} tableInstance={table} isKeyboardNavigable={false} styling={{ borderStyle: 'all-sides', hasHoverEffects: true, hasZebraStriping: false, isCompact: true, }} /> </div> ); }; export default Component; ``` ### Datagrid Component Overriding ```tsx import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { DataGrid, Tooltip, Typography } from '@neo4j-ndl/react'; import { createColumnHelper, getCoreRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, } from '@tanstack/react-table'; import { TABLE_DEMO_DATA } from './staticData'; type TestDataFormat = { name: string; age: number; cypherCommand: string; }; const Component = () => { const columnHelper = createColumnHelper<TestDataFormat>(); const columns = [ columnHelper.accessor('name', { cell: (info) => info.getValue(), footer: (info) => info.column.id, header: () => 'Name', maxSize: 300, minSize: 180, size: 220, }), columnHelper.accessor('age', { cell: (info) => info.renderValue(), footer: (info) => info.column.id, header: () => 'Age', minSize: 80, }), columnHelper.accessor('cypherCommand', { cell: (info) => info.getValue(), footer: (info) => info.column.id, header: () => 'Cypher Command', minSize: 400, size: 400, }), ]; const table = useReactTable({ columnResizeMode: 'onChange', columns, data: TABLE_DEMO_DATA, debugColumns: true, debugHeaders: true, debugTable: true, defaultColumn: { maxSize: 800, minSize: 80, }, enableSorting: true, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), initialState: { pagination: { pageSize: 5, }, }, }); return ( <div className="n-flex n-flex-col n-gap-token-8"> <Typography variant="body-medium" as="div" className="n-flex n-flex-col n-gap-token-8 n-p-token-24 n-pb-token-16" > <div>Current container height: 400px</div> <div>Try updating height with Storybook&apos; s story controls👇</div> </Typography> <div style={{ height: '400px', }} > <DataGrid className="n-h-full" isResizable={true} tableInstance={table} components={{ Body: (props) => <DataGrid.Body {...props} />, HeaderTitle: ({ cell, children }) => { const { column, id } = cell; const title = typeof column.columnDef.header === 'string' ? column.columnDef.header : id; return ( <Tooltip type="simple"> <Tooltip.Trigger hasButtonWrapper> <span>{children}</span> </Tooltip.Trigger> <Tooltip.Content>{title}</Tooltip.Content> </Tooltip> ); }, PaginationNumericButton: ({ isSelected, style, ...restProps }) => { return ( <DataGrid.PaginationNumericButton {...restProps} isSelected={isSelected} style={{ ...style, ...(isSelected && { backgroundImage: 'linear-gradient(90deg, #00C0FF 0%, #FFCF00 20%, #FC4F4F 40%, #00C0FF 100%)', backgroundSize: '200% auto', borderRadius: '10px', boxShadow: '0 0 20px #eee', }), }} /> ); }, TableResults: () => ( <DataGrid.TableResults> A table with a fixed height of 100px, custom result section and custom pagination button </DataGrid.TableResults> ), }} /> </div> </div> ); }; export default Component; ``` ### Datagrid Custom Cell With Style Rules ```tsx import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { DataGrid, type DataGridStyleRule, useDataGridCellStyle, } from '@neo4j-ndl/react'; import { type Cell, createColumnHelper, flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, type RowData, useReactTable, } from '@tanstack/react-table'; import { TABLE_DEMO_DATA, type TestDataFormat } from './staticData'; const styleRules: DataGridStyleRule[] = [ // Row coloring: all cells styled when name contains "Alicia" { apply: { color: { value: '#000000' }, textColor: { value: '#ffffff' }, }, column: null, priority: 3, where: { contains: [{ column: 'name' }, 'Alicia'] }, }, // Cell coloring: cells in the "age" column with value 51 get green { apply: { color: { value: '#00bb00' } }, column: 'age', priority: 3, where: { equal: [{ column: 'age' }, 51] }, }, // Dual color ranges on the "age" column: independent gradients for background // (driven by age, full 0-100 range) and text color (driven by age too, but // with a narrower 20-80 band so the text contrast is calibrated separately). { apply: { colorRange: { maxColor: '#f00000', maxValue: 100, midColor: '#a00000', midValue: 50, minColor: '#800000', minValue: 0, onColumn: 'age', }, textColorRange: { maxColor: '#aaaaaa', maxValue: 80, minColor: '#ffffff', minValue: 20, onColumn: 'age', }, }, column: 'age', priority: 2, // Lower priority so explicit green rule overrides }, // Multi-column rule: all cells styled when age > 50 AND name contains "o" { apply: { color: { value: '#fef3c7' }, textColor: { value: '#000000' }, }, column: null, priority: 0, // Lower priority so other rules take precedence where: { and: [ { greaterThan: [{ column: 'age' }, 50] }, { contains: [{ column: 'name' }, 'o'] }, ], }, }, ]; /** * A custom BodyCell that uses the `useDataGridCellStyle` hook to manually apply style rules to the cell. */ const CustomBodyCell = <T extends RowData>({ cell, }: { cell: Cell<T, unknown>; }) => { const ruleStyle = useDataGridCellStyle(cell); return ( <div className="ndl-data-grid-td" role="cell" style={{ ...ruleStyle, width: cell.column.getSize(), }} > {flexRender(cell.column.columnDef.cell, cell.getContext())} </div> ); }; const Component = () => { const columnHelper = createColumnHelper<TestDataFormat>(); const columns = [ columnHelper.accessor('name', { cell: (info) => info.getValue(), header: () => 'Name', minSize: 180, size: 220, }), columnHelper.accessor('age', { cell: (info) => info.renderValue(), header: () => 'Age', minSize: 80, }), columnHelper.accessor('cypherCommand', { cell: (info) => info.getValue(), header: () => 'Cypher Command', minSize: 400, size: 400, }), ]; const table = useReactTable({ columnResizeMode: 'onChange', columns, data: TABLE_DEMO_DATA, defaultColumn: { maxSize: 800, minSize: 80, }, enableSorting: true, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), initialState: { pagination: { pageSize: 10, }, }, }); return ( <div className="n-w-full n-bg-light-neutral-text-weakest"> <DataGrid isResizable={false} tableInstance={table} isKeyboardNavigable={false} styleRules={styleRules} components={{ BodyCell: CustomBodyCell, }} styling={{ borderStyle: 'all-sides', hasHoverEffects: true, hasZebraStriping: false, }} /> </div> ); }; export default Component; ``` ### Datagrid In Dialog ```tsx import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { DataGrid, type DataGridNav, Dialog, FilledButton, Select, useDataGridContext, } from '@neo4j-ndl/react'; import { type ColumnDef, type ColumnMeta, createColumnHelper, getCoreRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, } from '@tanstack/react-table'; import { useRef, useState } from 'react'; import { TABLE_DEMO_DATA } from './staticData'; type TestDataFormat = { name: string; age: number; cypherCommand: string; }; const columnHelper = createColumnHelper<TestDataFormat>(); const actionsColumn: (tableNav: DataGridNav) => ColumnDef<TestDataFormat> = ( tableNav, ) => ({ cell: ({ row }) => { const options = ['Edit', 'Delete', 'View']; const dropdownOptions = options.map((o) => ({ label: o, value: o })); // eslint-disable-next-line react-hooks/rules-of-hooks const [selected, setSelected] = useState(dropdownOptions[0]); // eslint-disable-next-line react-hooks/rules-of-hooks const { portalTarget } = useDataGridContext(); return ( <div data-testid={`ndl-actions-cell-row-${row.index}`} className="n-flex n-flex-row n-gap-token-8" > <Select ariaLabel={`actions-select-row-${row.index}`} type="select" selectProps={{ menuPortalTarget: portalTarget, menuPosition: 'fixed', onChange: (e) => { if (!e) { return; } setSelected(e); }, onKeyDown: (e) => { // Stop propagation of arrow down to the table, to avoid moving to the next item in the cell if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.stopPropagation(); } }, onMenuClose: () => { console.info( 'Start listening to keyboard events for table navigation', ); tableNav.enable(); }, onMenuOpen: () => { console.info( 'Stop listening to keyboard events for table navigation', ); tableNav.disable(); }, options: dropdownOptions, value: selected, }} /> </div> ); }, header: 'Custom Select', id: 'custom-select', minSize: 150, }); const Component = () => { const [data, setData] = useState(TABLE_DEMO_DATA); const [isAgeFilterApplied, setIsAgeFilterApplied] = useState(false); const ColumnActions: ColumnMeta<TestDataFormat, unknown> = { columnActions: { actions: [ { onClick: () => { if (isAgeFilterApplied) { setData(TABLE_DEMO_DATA); setIsAgeFilterApplied(false); return; } else { const filteredData = data.filter((item) => item.age >= 18); setData(filteredData); setIsAgeFilterApplied(true); } }, title: 'Age >= 18', }, ], }, }; const regularColumns = [ columnHelper.accessor('name', { cell: (cell) => ( <DataGrid.InlineEditCell cell={cell} value={cell.getValue()} ariaLabel="edit name" /> ), maxSize: 300, meta: { isInlineEditCell: { onEditChange: (newValue, cell) => { setData((oldData) => { return oldData.map((row) => { if (row.name === cell.row.original.name) { return { ...row, name: newValue, }; } return row; }); }); }, }, }, minSize: 180, size: 220, }), columnHelper.accessor('age', { cell: (info) => { return ( <DataGrid.DropDownCell cell={info} options={[ { label: '12', value: '12', }, { label: '32', value: '32', }, { label: '37', value: '37', }, { label: '39', value: '39', }, { label: '51', value: '51', }, ]} ariaLabel="select age" portalTarget={document.getElementById('default-menu')} /> ); }, meta: { isDropDownCell: { onChange: (newValue, cell) => { if (newValue === undefined) { // No change return; } const parsedValue = parseInt(newValue, 10); if (isNaN(parsedValue)) { return; } setData((oldData) => { return oldData.map((row) => { if (row.name === cell.row.original.name) { return { ...row, age: parsedValue, }; } return row; }); }); }, }, ...ColumnActions, }, minSize: 120, }), columnHelper.accessor('cypherCommand', { cell: (info) => info.getValue(), minSize: 400, size: 400, }), ]; const allColumns = (tableNav: DataGridNav) => [ ...regularColumns, actionsColumn(tableNav), ]; const tableNavRef = useRef<DataGridNav>(null!); const table = useReactTable({ autoResetPageIndex: false, columnResizeMode: 'onChange', columns: allColumns(tableNavRef.current), data: data, debugColumns: true, debugHeaders: true, debugTable: true, defaultColumn: { maxSize: 800, minSize: 80, }, enableSorting: true, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), initialState: { pagination: { pageSize: 5, }, }, }); // Modal code const [isOpen, setIsOpen] = useState<boolean>(false); const handleClick = () => setIsOpen((prev) => !prev); const handleClose = () => setIsOpen(false); // DataGrid code return ( <div className="n-w-full"> <div className="n-flex n-justify-center"> <FilledButton onClick={handleClick}>Open Dialog</FilledButton> </div> <Dialog className="n-p-token-8 n-bg-neutral-10 n-max-w-max n-rounded-lg" htmlAttributes={{ id: 'default-menu', }} onClose={handleClose} isOpen={isOpen} hasDisabledCloseButton={true} > <Dialog.Content> <DataGrid portalTarget={document.getElementById('default-menu')} isResizable={true} tableInstance={table} tableNavRef={tableNavRef} isKeyboardNavigable={true} styling={{ borderStyle: 'horizontal', hasZebraStriping: false, }} /> </Dialog.Content> </Dialog> </div> ); }; export default Component; ``` ### Datagrid Keyboard Navigation ```tsx import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { Checkbox, DataGrid, type DataGridNav, IconButton, Select, useDataGridContext, } from '@neo4j-ndl/react'; import { MegaphoneIconOutline } from '@neo4j-ndl/react/icons'; import { type ColumnDef, createColumnHelper, getCoreRowModel, getPaginationRowModel, useReactTable, } from '@tanstack/react-table'; import { useRef, useState } from 'react'; import { TABLE_DEMO_DATA } from './staticData'; type TestDataFormat = { name: string; age: number; cypherCommand: string; }; const selectableColumn: ColumnDef<TestDataFormat> = { cell: ({ row }) => ( <Checkbox ariaLabel={`row-${row.index}`} isChecked={row.getIsSelected() || false} onChange={row.getToggleSelectedHandler()} /> ), header: ({ table }) => ( <Checkbox ariaLabel="Select all rows" isChecked={table.getIsAllRowsSelected() || false} onChange={table.getToggleAllRowsSelectedHandler()} isIndeterminate={table.getIsSomeRowsSelected()} /> ), id: 'select', maxSize: 80, size: 80, }; const actionsColumn: (tableNav: DataGridNav) => ColumnDef<TestDataFormat> = ( tableNav, ) => ({ cell: ({ row }) => { const options = ['Edit', 'Delete', 'View']; const dropdownOptions = options.map((o) => ({ label: o, value: o })); // eslint-disable-next-line react-hooks/rules-of-hooks const [selected, setSelected] = useState(dropdownOptions[0]); // eslint-disable-next-line react-hooks/rules-of-hooks const { portalTarget } = useDataGridContext(); return ( <div data-testid={`ndl-actions-cell-row-${row.index}`} className="n-flex n-flex-row n-gap-token-8" > <IconButton description="Megaphone" onClick={() => { alert("Let's get loud 📣"); }} htmlAttributes={{ type: 'button', }} > <MegaphoneIconOutline /> </IconButton> <Select ariaLabel={`actions-select-row-${row.index}`} type="select" selectProps={{ menuPortalTarget: portalTarget, onChange: (e) => { if (!e) { return; } setSelected(e); }, onKeyDown: (e) => { // Stop propagation of arrow down to the table, to avoid moving to the next item in the cell if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.stopPropagation(); } }, onMenuClose: () => { console.info( 'Start listening to keyboard events for table navigation', ); tableNav.enable(); }, onMenuOpen: () => { console.info( 'Stop listening to keyboard events for table navigation', ); tableNav.disable(); }, options: dropdownOptions, value: selected, }} /> </div> ); }, header: 'actions', id: 'actions', }); const allColumns = (tableNav: DataGridNav) => { const columnHelper = createColumnHelper<TestDataFormat>(); const commonColumns = [ columnHelper.accessor('name', { cell: (info) => info.getValue(), footer: (info) => info.column.id, header: () => 'Name', maxSize: 300, minSize: 180, size: 220, }), columnHelper.accessor('age', { cell: (info) => info.renderValue(), footer: (info) => info.column.id, header: () => 'Age', minSize: 80, }), ]; return [selectableColumn, ...commonColumns, actionsColumn(tableNav)]; }; const Component = () => { const [rowSelection, setRowSelection] = useState({}); const ref = useRef<HTMLDivElement>(null); const tableNavRef = useRef<DataGridNav>(null!); const table = useReactTable({ columnResizeMode: 'onChange', columns: allColumns(tableNavRef.current), data: TABLE_DEMO_DATA, enableSorting: false, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), initialState: { pagination: { pageSize: 5, }, }, onRowSelectionChange: setRowSelection, state: { rowSelection, }, }); return ( <> <DataGrid isResizable={false} isKeyboardNavigable={true} styling={{ borderStyle: 'all-sides', }} tableNavRef={tableNavRef} tableInstance={table} ref={ref} portalTarget={document.body} /> </> ); }; export default Component; ``` ### Datagrid Kitchen Sink ```tsx import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { DataGrid, Menu } from '@neo4j-ndl/react'; import { ChevronDownIconSolid } from '@neo4j-ndl/react/icons'; import { type ColumnDef, type ColumnMeta, createColumnHelper, getCoreRowModel, getPaginationRowModel, getSortedRowModel, type TableOptions, useReactTable, } from '@tanstack/react-table'; import { Leva, useControls } from 'leva'; import { type BorderStyle, type HeaderStyle } from '../DataGrid'; import { TABLE_DEMO_DATA } from './staticData'; type TestDataFormat = { name: string; age: number; cypherCommand: string; }; const columnHelper = createColumnHelper<TestDataFormat>(); const ColumnActions: ColumnMeta<TestDataFormat, unknown> = { columnActions: { actions: [ { onClick: () => { alert('Action triggered'); }, title: 'Example Column Action', }, ], }, }; type ColumnGeneratorOptions = { shouldIncludeRowActionColumn: boolean; }; const COMMON_COLUMNS = (options: ColumnGeneratorOptions) => { // oxlint-disable-next-line @typescript-eslint/no-explicit-any const columns: ColumnDef<TestDataFormat, any>[] = [ columnHelper.accessor('name', { cell: (info) => info.getValue(), maxSize: 300, minSize: 180, size: 220, }), columnHelper.accessor('age', { cell: (info) => info.getValue(), header: () => <div>Age</div>, minSize: 80, ...(options.shouldIncludeRowActionColumn ? { meta: { ...ColumnActions, }, } : {}), }), columnHelper.accessor('cypherCommand', { cell: (info) => info.getValue(), minSize: 400, size: 400, }), ]; if (options.shouldIncludeRowActionColumn) { columns.push( columnHelper.display({ cell: (cell) => { const isSpecialRow = cell.row.original.name === TABLE_DEMO_DATA[3].name || undefined; return ( <DataGrid.ActionCell cell={cell} innerCleanIconButtonProps={{ children: isSpecialRow && <ChevronDownIconSolid />, description: `Actions for ${cell.row.original.name}`, }} innerMenuProps={ isSpecialRow && { children: <Menu.Item title="Custom icon and menu props" />, } } /> ); }, id: 'actions', meta: { isActionCell: { actions: [ { onClick: (_event, cell) => { alert( `Row Action triggered for row with index: ${ cell.row.index }`, ); }, title: 'Example Row Action', }, ], onOpenChange: (open) => console.info(`onOpenChange: ${open}`), }, }, }), ); } return columns; }; const COMMON_TABLE_PROPS = { columnResizeMode: 'onChange', data: TABLE_DEMO_DATA, // https://tanstack.com/table/v8/docs/guide/column-sizing#overview defaultColumn: { minSize: 80, }, enableSorting: false, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), initialState: { pagination: { pageSize: 5, }, }, meta: { actions: [ { onClick: () => { alert('wowowow'); }, title: "Let's party", }, ], }, } as Omit<TableOptions<TestDataFormat>, 'columns'>; const Component = () => { const { keyboard: isKeyboardNavigable, zebraStriping: hasZebraStriping, borderStyle, headerStyle, includeRowActionColumn: shouldIncludeRowActionColumn, } = useControls({ borderStyle: { options: ['all-sides', 'horizontal', 'vertical', 'none'], value: 'all-sides', }, headerStyle: { options: ['filled', 'clean'], value: 'filled', }, includeRowActionColumn: false, keyboard: false, zebraStriping: false, }); const table = useReactTable({ ...COMMON_TABLE_PROPS, columns: COMMON_COLUMNS({ shouldIncludeRowActionColumn, }), enableSorting: true, getSortedRowModel: getSortedRowModel(), }); return ( <div className="n-h-[1000px] n-w-full n-bg-primary-bg-weak n-p-token-32"> <Leva isRoot /> <DataGrid isResizable={true} tableInstance={table} isKeyboardNavigable={isKeyboardNavigable} styling={{ borderStyle: borderStyle as BorderStyle, hasZebraStriping, headerStyle: headerStyle as HeaderStyle, }} /> </div> ); }; export default Component; ``` ### Datagrid No Pagination Table ```tsx import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { DataGrid } from '@neo4j-ndl/react'; import { createColumnHelper, getCoreRowModel, useReactTable, } from '@tanstack/react-table'; import { PRUNED_TABLE_DEMO_DATA } from './staticData'; type TestDataFormat = { name: string; age: number; cypherCommand: string; }; const Component = () => { const columnHelper = createColumnHelper<TestDataFormat>(); const columns = [ columnHelper.accessor('name', { cell: (info) => info.getValue(), footer: (info) => info.column.id, header: () => 'Name', maxSize: 300, minSize: 180, size: 220, }), columnHelper.accessor('age', { cell: (info) => info.renderValue(), footer: (info) => info.column.id, header: () => 'Age', minSize: 80, }), columnHelper.accessor('cypherCommand', { cell: (info) => info.getValue(), footer: (info) => info.column.id, header: () => 'Cypher Command', minSize: 400, size: 400, }), ]; const table = useReactTable({ columns, data: PRUNED_TABLE_DEMO_DATA, enableSorting: false, getCoreRowModel: getCoreRowModel(), }); return ( <DataGrid components={{ Navigation: null }} isResizable={false} tableInstance={table} /> ); }; export default Component; ``` ### Datagrid Percentage Width Columns ```tsx import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { DataGrid } from '@neo4j-ndl/react'; import { createColumnHelper, getCoreRowModel, getSortedRowModel, useReactTable, } from '@tanstack/react-table'; import { TABLE_DEMO_DATA } from './staticData'; type TestDataFormat = { name: string; age: number; cypherCommand: string; }; const Component = () => { const columnHelper = createColumnHelper<TestDataFormat>(); const columns = [ columnHelper.accessor('name', { cell: (info) => info.getValue(), footer: (info) => info.column.id, header: () => 'Name', size: 200, // 200 / 1000 = 20% of the table width }), columnHelper.accessor('age', { cell: (info) => info.renderValue(), footer: (info) => info.column.id, header: () => 'Age', minSize: 100, // 100 / 1000 = 10% of the table width }), columnHelper.accessor('cypherCommand', { cell: (info) => info.getValue(), footer: (info) => info.column.id, header: () => 'Cypher Command', size: 700, // 700 / 1000 = 70% of the table width }), ]; const table = useReactTable({ columnResizeMode: 'onChange', columns: columns, data: TABLE_DEMO_DATA.slice(0, 5), debugColumns: true, debugHeaders: true, debugTable: true, enableSorting: false, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), }); // Disable resizing of columns and enable automatic resizing of columns, to allow for percentage width columns const isResizable = false; const isAutoResizingColumns = true; return ( <div className="n-w-full n-bg-light-neutral-text-weakest"> <DataGrid components={{ Navigation: null, }} isAutoResizingColumns={isAutoResizingColumns} isKeyboardNavigable={false} isResizable={isResizable} styling={{ borderStyle: 'all-sides', hasHoverEffects: false, hasZebraStriping: false, }} tableInstance={table} /> </div> ); }; export default Component; ``` ### Datagrid Placeholder States ```tsx import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { DataGrid } from '@neo4j-ndl/react'; import { createColumnHelper, getCoreRowModel, getPaginationRowModel, useReactTable, } from '@tanstack/react-table'; import { TABLE_DEMO_DATA } from './staticData'; type TestDataFormat = { name: string; age: number; cypherCommand: string; }; const Component = () => { const columnHelper = createColumnHelper<TestDataFormat>(); const columns = [ columnHelper.accessor('name', { cell: (info) => info.getValue(), footer: (info) => info.column.id, header: () => 'Name', maxSize: 300, minSize: 180, size: 220, }), columnHelper.accessor('age', { cell: (info) => info.renderValue(), footer: (info) => info.column.id, header: () => 'Age', minSize: 80, }), columnHelper.accessor('cypherCommand', { cell: (info) => info.getValue(), footer: (info) => info.column.id, header: () => 'Cypher Command', minSize: 400, size: 400, }), ]; const table = useReactTable({ columnResizeMode: 'onChange', columns, data: TABLE_DEMO_DATA, debugColumns: true, debugHeaders: true, debugTable: true, defaultColumn: { maxSize: 800, minSize: 80, }, enableSorting: false, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), initialState: { pagination: { pageSize: 5, }, }, }); return ( <div className="n-w-full n-bg-light-neutral-text-weakest"> <DataGrid isResizable={true} tableInstance={table} isKeyboardNavigable={true} /> </div> ); }; export default Component; ``` ### Datagrid Resizable Column ```tsx import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { DataGrid } from '@neo4j-ndl/react'; import { createColumnHelper, getCoreRowModel, getPaginationRowModel, useReactTable, } from '@tanstack/react-table'; import { TABLE_DEMO_DATA } from './staticData'; type TestDataFormat = { name: string; age: number; cypherCommand: string; }; const Component = ({ isKeyboardNavigable = true, components, }: { isKeyboardNavigable?: boolean; components?: React.ComponentProps<typeof DataGrid>['components']; } = {}) => { const columnHelper = createColumnHelper<TestDataFormat>(); const columns = [ columnHelper.accessor('name', { cell: (info) => info.getValue(), footer: (info) => info.column.id, header: () => 'Name', maxSize: 300, minSize: 180, size: 220, }), columnHelper.accessor('age', { cell: (info) => info.renderValue(), footer: (info) => info.column.id, header: () => 'Age', minSize: 80, }), columnHelper.accessor('cypherCommand', { cell: (info) => info.getValue(), footer: (info) => info.column.id, header: () => 'Cypher Command', minSize: 400, size: 400, }), ]; const table = useReactTable({ columnResizeMode: 'onChange', columns, data: TABLE_DEMO_DATA, debugColumns: true, debugHeaders: true, debugTable: true, defaultColumn: { maxSize: 800, minSize: 80, }, enableSorting: false, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), initialState: { pagination: { pageSize: 5, }, }, }); return ( <div className="n-w-full n-bg-light-neutral-text-weakest"> <DataGrid<TestDataFormat> isResizable={true} tableInstance={table} isKeyboardNavigable={isKeyboardNavigable} // @ts-expect-error limitations of storybooks makes it hard to handle generics components={components} /> </div> ); }; export default Component; ``` ### Datagrid Rule Based Styling ```tsx import '@neo4j-ndl/base/lib/neo4j-ds-styles.css'; import { DataGrid } from '@neo4j-ndl/react'; import { createColumnHelper, getCoreRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, } from '@tanstack/react-table'; import { type DataGridStyleRule } from '../style-rules/types'; import { TABLE_DEMO_DATA, type TestDataFormat } from './staticData'; const styleRules: DataGridStyleRule[] = [ // Row coloring: all cells styled when name contains "Alicia" { apply: { color: { value: '#000000' }, textColor: { value: '#ffffff' }, }, column: null, priority: 3, where: { contains: [{ column: 'name' }, 'Alicia'] }, }, // Cell coloring: cells in the "age" column with value 51 get green { apply: { color: { value: '#00bb00' } }, column: 'age', priority: 3, where: { equal: [{ column: 'age' }, 51] }, }, // Dual color ranges on the "age" column: independent gradients for background // (driven by age, full 0-100 range) and text color (driven by age too, but // with a narrower 20-80 band so the text contrast is calibrated separately). { apply: { colorRange: { maxColor: '#f00000', maxValue: 100, midColor: '#a00000', midValue: 50, minColor: '#800000', minValue: 0, onColumn: 'age', }, textColorRange: { maxColor: '#aaaaaa', maxValue: 80, minColor: '#ffffff', minValue: 20, onColumn: 'age', }, }, column: 'age', priority: 2, // Lower priority so explicit green rule overrides }, // Multi-column rule: all cells styled when age > 50 AND name contains "o" { apply: { color: { value: '#fef3c7' }, textColor: { value: '#000000' }, }, column: null, priority: 0, // Lower priority so other rules take precedence where: { and: [ { greaterThan: [{ column: 'age' }, 50] }, { contains: [{ column: 'name' }, 'o'] }, ], }, }, ]; const Component = () => { const columnHelper = createColumnHelper<TestDataFormat>(); const columns = [ columnHelper.accessor('name', { cell: (info) => info.getValue(), header: () => 'Name', minSize: 180, size: 220, }), columnHelper.accessor('age', { cell: (info) => info.renderValue(), header: () => 'Age', minSize: 80, }), columnHelper.accessor('cypherCommand', { cell: (info) => info.getValue(), header: () => 'Cypher Command', minSize: 400, size: 400, }), ]; const table = useReactTable({ columnResizeMode: 'onChange', columns, data: TABLE_DEMO_DATA, defaultColumn: { maxSize: 800, minSize: 80, }, enableSorting: true, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), initialState: { pagination: { pageSize: 10, }, }, }); return ( <div className="n-w-full n-bg-light-neutral-text-weakest"> <DataGrid isResizable={false} tableInstance={table}