@neo4j-ndl/react
Version:
React implementation of Neo4j Design System
1,818 lines (1,625 loc) • 82.5 kB
Markdown
# 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' 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}