@neo4j-ndl/react 4.16.8 → 4.16.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/package.json +5 -2
  2. package/skills/ndl-react/SKILL.md +152 -0
  3. package/skills/ndl-react/components/accordion.md +386 -0
  4. package/skills/ndl-react/components/ai/code-preview.md +128 -0
  5. package/skills/ndl-react/components/ai/file-tag.md +90 -0
  6. package/skills/ndl-react/components/ai/image-tag.md +91 -0
  7. package/skills/ndl-react/components/ai/more-files.md +73 -0
  8. package/skills/ndl-react/components/ai/presence.md +54 -0
  9. package/skills/ndl-react/components/ai/preview.md +498 -0
  10. package/skills/ndl-react/components/ai/prompt.md +362 -0
  11. package/skills/ndl-react/components/ai/reasoning.md +95 -0
  12. package/skills/ndl-react/components/ai/response.md +1109 -0
  13. package/skills/ndl-react/components/ai/suggestion.md +84 -0
  14. package/skills/ndl-react/components/ai/thinking.md +43 -0
  15. package/skills/ndl-react/components/ai/tool-call.md +43 -0
  16. package/skills/ndl-react/components/ai/user-bubble.md +187 -0
  17. package/skills/ndl-react/components/avatar.md +169 -0
  18. package/skills/ndl-react/components/banner.md +225 -0
  19. package/skills/ndl-react/components/box.md +39 -0
  20. package/skills/ndl-react/components/breadcrumbs.md +245 -0
  21. package/skills/ndl-react/components/checkbox.md +269 -0
  22. package/skills/ndl-react/components/clean-icon-button.md +294 -0
  23. package/skills/ndl-react/components/clipboard-button.md +154 -0
  24. package/skills/ndl-react/components/code-block.md +276 -0
  25. package/skills/ndl-react/components/code.md +68 -0
  26. package/skills/ndl-react/components/color-picker.md +131 -0
  27. package/skills/ndl-react/components/conditional-wrap.md +46 -0
  28. package/skills/ndl-react/components/data-grid.md +3121 -0
  29. package/skills/ndl-react/components/date-picker.md +652 -0
  30. package/skills/ndl-react/components/dialog.md +485 -0
  31. package/skills/ndl-react/components/dismissible-tag.md +136 -0
  32. package/skills/ndl-react/components/divider.md +96 -0
  33. package/skills/ndl-react/components/drawer.md +543 -0
  34. package/skills/ndl-react/components/dropdown-button.md +161 -0
  35. package/skills/ndl-react/components/dropzone.md +371 -0
  36. package/skills/ndl-react/components/filled-button.md +26 -0
  37. package/skills/ndl-react/components/flex.md +82 -0
  38. package/skills/ndl-react/components/graph-label.md +167 -0
  39. package/skills/ndl-react/components/icon-button-array.md +187 -0
  40. package/skills/ndl-react/components/icon-button.md +193 -0
  41. package/skills/ndl-react/components/inline-edit.md +209 -0
  42. package/skills/ndl-react/components/kbd.md +63 -0
  43. package/skills/ndl-react/components/loading-bar.md +43 -0
  44. package/skills/ndl-react/components/loading-spinner.md +57 -0
  45. package/skills/ndl-react/components/logo.md +57 -0
  46. package/skills/ndl-react/components/menu.md +993 -0
  47. package/skills/ndl-react/components/modal.md +27 -0
  48. package/skills/ndl-react/components/next/tree-view.md +966 -0
  49. package/skills/ndl-react/components/outlined-button.md +26 -0
  50. package/skills/ndl-react/components/popover.md +283 -0
  51. package/skills/ndl-react/components/progress-bar.md +70 -0
  52. package/skills/ndl-react/components/radio.md +150 -0
  53. package/skills/ndl-react/components/read-only-tag.md +96 -0
  54. package/skills/ndl-react/components/segmented-control.md +200 -0
  55. package/skills/ndl-react/components/select-icon-button.md +183 -0
  56. package/skills/ndl-react/components/select.md +530 -0
  57. package/skills/ndl-react/components/selectable-tag.md +203 -0
  58. package/skills/ndl-react/components/side-navigation.md +652 -0
  59. package/skills/ndl-react/components/skeleton.md +274 -0
  60. package/skills/ndl-react/components/slider.md +308 -0
  61. package/skills/ndl-react/components/spotlight.md +796 -0
  62. package/skills/ndl-react/components/status-indicator.md +57 -0
  63. package/skills/ndl-react/components/status-label.md +132 -0
  64. package/skills/ndl-react/components/switch.md +167 -0
  65. package/skills/ndl-react/components/tabs.md +433 -0
  66. package/skills/ndl-react/components/tag.md +91 -0
  67. package/skills/ndl-react/components/text-area.md +178 -0
  68. package/skills/ndl-react/components/text-button.md +25 -0
  69. package/skills/ndl-react/components/text-input.md +475 -0
  70. package/skills/ndl-react/components/text-link.md +119 -0
  71. package/skills/ndl-react/components/time-picker.md +137 -0
  72. package/skills/ndl-react/components/timezone-picker.md +496 -0
  73. package/skills/ndl-react/components/tooltip.md +478 -0
  74. package/skills/ndl-react/components/tree-view.md +545 -0
  75. package/skills/ndl-react/components/typography.md +72 -0
  76. package/skills/ndl-react/components/wizard.md +152 -0
@@ -0,0 +1,3121 @@
1
+ # DataGrid
2
+
3
+ Import: `import { DataGrid } from '@neo4j-ndl/react'`
4
+
5
+ ## Props
6
+
7
+ ### DataGrid
8
+
9
+ | Prop | Type | Required | Default | Description |
10
+ |------|------|----------|---------|-------------|
11
+ | `components` | `DataGridComponentsInterface<T>` | | | Table sub-component overrides |
12
+ | `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 |
13
+ | `isKeyboardNavigable` | `boolean` | | `true` | Enables keyboard navigation. To control keyboard navigation programmatically for edge cases, pass a `ref` to `tableNavRef` |
14
+ | `isLoading` | `boolean` | | `false` | Enables table's loading state |
15
+ | `isResizable` | `boolean` | | `true` | Resizing the width of columns |
16
+ | `isSkeletonLoading` | `boolean` | | `false` | |
17
+ | `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. |
18
+ | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
19
+ | `rootProps` | `HtmlAttributes<"div">` | | | @deprecated Use htmlAttributes, className and style on the root element instead |
20
+ | `skeletonProps` | `(SkeletonProps & { as?: ElementType<any, keyof IntrinsicElements>; } & BaseProps<ElementType<any, keyof IntrinsicElements>>)` | | | |
21
+ | `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. |
22
+ | `styleRuleValueConverters` | `DataGridStyleRuleValueConverters` | | | Per-column converters that teach the style-rule engine how to interpret complex cell values. |
23
+ | `styling` | `Partial<{ hasZebraStriping: boolean; borderStyle: "all-sides" \| "horizontal" \| "vertical" \| "none"; headerStyle: "filled" \| "clean"; hasHoverEffects: boolean; isCompact: boolean; }>` | | `{}` | |
24
+ | `tableInstance` | `Table<T>` | ✅ | | Table instance created from `useTable` |
25
+ | `tableNavRef` | `RefObject<any>` | | | Reference to the tableNav instance for programmatic control |
26
+
27
+ ### DataGrid.ActionButton
28
+
29
+ | Prop | Type | Required | Default | Description |
30
+ |------|------|----------|---------|-------------|
31
+ | `action` | `MenuItemProps` | ✅ | | |
32
+ | `as` | `ElementType<any, keyof IntrinsicElements>` | | | An override of the default HTML tag of the root of the component. Can also be another React component. |
33
+ | `onClose` | `() => void` | ✅ | | |
34
+ | `ref` | `any` | | | A ref to apply to the root element. |
35
+
36
+ ### DataGrid.ActionCell
37
+
38
+ | Prop | Type | Required | Default | Description |
39
+ |------|------|----------|---------|-------------|
40
+ | `cell` | `CellContext<T, unknown>` | | | |
41
+ | `innerCleanIconButtonProps` | `Partial<Omit<CleanIconButtonButtonProps & { as?: ElementType<any, keyof IntrinsicElements>; } & BaseProps<ElementType<any, keyof IntrinsicElements>>, "ref">>` | | | |
42
+ | `innerMenuProps` | `Partial<MenuProps & { as?: ElementType<any, keyof IntrinsicElements>; } & BaseProps<ElementType<any, keyof IntrinsicElements>>>` | | | |
43
+
44
+ ### DataGrid.Body
45
+
46
+ | Prop | Type | Required | Default | Description |
47
+ |------|------|----------|---------|-------------|
48
+ | `children` | `ReactNode` | | | |
49
+ | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
50
+
51
+ ### DataGrid.BodyCell
52
+
53
+ | Prop | Type | Required | Default | Description |
54
+ |------|------|----------|---------|-------------|
55
+ | `cell` | `Cell<T, unknown>` | ✅ | | |
56
+ | `children` | `ReactNode` | | | |
57
+ | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
58
+
59
+ ### DataGrid.BodyRow
60
+
61
+ | Prop | Type | Required | Default | Description |
62
+ |------|------|----------|---------|-------------|
63
+ | `children` | `ReactNode` | | | |
64
+ | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
65
+ | `row` | `Row<T>` | ✅ | | |
66
+
67
+ ### DataGrid.ColumnControls
68
+
69
+ | Prop | Type | Required | Default | Description |
70
+ |------|------|----------|---------|-------------|
71
+ | `cell` | `Header<T, unknown>` | ✅ | | |
72
+ | `children` | `ReactNode` | | | |
73
+ | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
74
+
75
+ ### DataGrid.DropDownCell
76
+
77
+ | Prop | Type | Required | Default | Description |
78
+ |------|------|----------|---------|-------------|
79
+ | `ariaLabel` | `string` | ✅ | | Aria label of the select component. Required for accessibility. |
80
+ | `cell` | `CellContext<T, unknown>` | ✅ | | |
81
+ | `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. |
82
+ | `errorText` | `ReactNode` | | | Error message. If it exists the select will be in error state. |
83
+ | `helpText` | `ReactNode` | | | Help text of the select component. |
84
+ | `isDisabled` | `boolean` | | | Whether the select component is disabled |
85
+ | `options` | `{ value: string; label: string; }[]` | ✅ | | |
86
+ | `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. |
87
+ | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
88
+
89
+ ### DataGrid.Header
90
+
91
+ | Prop | Type | Required | Default | Description |
92
+ |------|------|----------|---------|-------------|
93
+ | `children` | `ReactNode` | | | |
94
+ | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
95
+
96
+ ### DataGrid.HeaderCell
97
+
98
+ | Prop | Type | Required | Default | Description |
99
+ |------|------|----------|---------|-------------|
100
+ | `cell` | `Header<T, unknown>` | ✅ | | |
101
+ | `children` | `ReactNode` | | | |
102
+ | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
103
+
104
+ ### DataGrid.HeaderTitle
105
+
106
+ | Prop | Type | Required | Default | Description |
107
+ |------|------|----------|---------|-------------|
108
+ | `cell` | `Header<T, unknown>` | ✅ | | |
109
+ | `children` | `ReactNode` | | | |
110
+ | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
111
+
112
+ ### DataGrid.InlineEditCell
113
+
114
+ | Prop | Type | Required | Default | Description |
115
+ |------|------|----------|---------|-------------|
116
+ | `ariaLabel` | `string` | ✅ | | |
117
+ | `cell` | `CellContext<T, unknown>` | ✅ | | |
118
+ | `ref` | `Ref<HTMLInputElement>` | | | A ref to apply to the root element. |
119
+ | `value` | `string` | ✅ | | |
120
+
121
+ ### DataGrid.LoadingPlaceholder
122
+
123
+ | Prop | Type | Required | Default | Description |
124
+ |------|------|----------|---------|-------------|
125
+ | `children` | `ReactNode` | | | |
126
+ | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
127
+
128
+ ### DataGrid.Navigation
129
+
130
+ | Prop | Type | Required | Default | Description |
131
+ |------|------|----------|---------|-------------|
132
+ | `children` | `ReactNode` | | | |
133
+ | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
134
+
135
+ ### DataGrid.NoDataIcon
136
+
137
+ _No props documented._
138
+ ### DataGrid.NoDataPlaceholder
139
+
140
+ | Prop | Type | Required | Default | Description |
141
+ |------|------|----------|---------|-------------|
142
+ | `children` | `ReactNode` | | | |
143
+ | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
144
+
145
+ ### DataGrid.NoDataPlaceholderContentWrapper
146
+
147
+ | Prop | Type | Required | Default | Description |
148
+ |------|------|----------|---------|-------------|
149
+ | `children` | `ReactNode` | | | |
150
+ | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
151
+
152
+ ### DataGrid.Pagination
153
+
154
+ | Prop | Type | Required | Default | Description |
155
+ |------|------|----------|---------|-------------|
156
+ | `children` | `ReactNode` | | | |
157
+ | `ref` | `Ref<HTMLElement>` | | | A ref to apply to the root element. |
158
+
159
+ ### DataGrid.PaginationArrowButton
160
+
161
+ | Prop | Type | Required | Default | Description |
162
+ |------|------|----------|---------|-------------|
163
+ | `action` | `'next' \| 'previous'` | ✅ | | |
164
+ | `children` | `ReactNode` | | | |
165
+ | `onClick` | `(e: MouseEvent<HTMLButtonElement, MouseEvent>) => void` | | | |
166
+ | `ref` | `Ref<HTMLButtonElement>` | | | A ref to apply to the root element. |
167
+
168
+ ### DataGrid.PaginationNumericButton
169
+
170
+ | Prop | Type | Required | Default | Description |
171
+ |------|------|----------|---------|-------------|
172
+ | `children` | `ReactNode` | | | |
173
+ | `currentIndex` | `string \| number` | ✅ | | |
174
+ | `isSelected` | `boolean` | ✅ | | |
175
+ | `onClick` | `(e: MouseEvent<HTMLButtonElement, MouseEvent>) => void` | | | |
176
+ | `ref` | `Ref<HTMLButtonElement>` | | | A ref to apply to the root element. |
177
+
178
+ ### DataGrid.ResizingBar
179
+
180
+ | Prop | Type | Required | Default | Description |
181
+ |------|------|----------|---------|-------------|
182
+ | `header` | `Header<T, unknown>` | ✅ | | |
183
+ | `ref` | `Ref<HTMLButtonElement>` | | | A ref to apply to the root element. |
184
+
185
+ ### DataGrid.RowsPerPage
186
+
187
+ | Prop | Type | Required | Default | Description |
188
+ |------|------|----------|---------|-------------|
189
+ | `children` | `ReactNode` | | | |
190
+ | `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. |
191
+ | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
192
+
193
+ ### DataGrid.Scrollable
194
+
195
+ | Prop | Type | Required | Default | Description |
196
+ |------|------|----------|---------|-------------|
197
+ | `children` | `ReactNode` | | | |
198
+ | `ref` | `Ref<HTMLDivElement> & RefObject<HTMLDivElement>` | | | A ref to apply to the root element. |
199
+
200
+ ### DataGrid.TableResults
201
+
202
+ | Prop | Type | Required | Default | Description |
203
+ |------|------|----------|---------|-------------|
204
+ | `children` | `ReactNode` | | | |
205
+ | `manualPagination` | `{ from: number; to: number; totalRows: number; }` | | | |
206
+ | `ref` | `Ref<HTMLSpanElement>` | | | A ref to apply to the root element. |
207
+ | `resultsLabel` | `string` | | | The label to use for the results, meaning the word "results" in the sentence "Showing X-Y of Z results". @defaultValue 'results' |
208
+
209
+ ## Examples
210
+
211
+ ### Datagrid Column Filtering
212
+
213
+ ```tsx
214
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
215
+
216
+ import { DataGrid } from '@neo4j-ndl/react';
217
+ import {
218
+ createColumnHelper,
219
+ getCoreRowModel,
220
+ getFilteredRowModel,
221
+ getPaginationRowModel,
222
+ useReactTable,
223
+ } from '@tanstack/react-table';
224
+
225
+ import { TABLE_DEMO_DATA } from './staticData';
226
+
227
+ type TestDataFormat = {
228
+ name: string;
229
+ age: number;
230
+ cypherCommand: string;
231
+ };
232
+
233
+ const Component = () => {
234
+ const columnHelper = createColumnHelper<TestDataFormat>();
235
+
236
+ const columns = [
237
+ columnHelper.accessor('name', {
238
+ cell: (info) => info.getValue(),
239
+ footer: (info) => info.column.id,
240
+ header: () => 'Name',
241
+ maxSize: 300,
242
+ minSize: 180,
243
+ size: 220,
244
+ }),
245
+ columnHelper.accessor('age', {
246
+ cell: (info) => info.renderValue(),
247
+ footer: (info) => info.column.id,
248
+ header: () => 'Age',
249
+ minSize: 80,
250
+ }),
251
+ columnHelper.accessor('cypherCommand', {
252
+ cell: (info) => info.getValue(),
253
+ footer: (info) => info.column.id,
254
+ header: () => 'Cypher Command',
255
+ minSize: 400,
256
+ size: 400,
257
+ }),
258
+ ];
259
+
260
+ const table = useReactTable({
261
+ columnResizeMode: 'onChange',
262
+ columns,
263
+ data: TABLE_DEMO_DATA,
264
+ debugColumns: true,
265
+ debugHeaders: true,
266
+ debugTable: true,
267
+ defaultColumn: {
268
+ maxSize: 800,
269
+ minSize: 80,
270
+ },
271
+ enableSorting: false,
272
+ getCoreRowModel: getCoreRowModel(),
273
+ getFilteredRowModel: getFilteredRowModel(),
274
+ getPaginationRowModel: getPaginationRowModel(),
275
+ initialState: {
276
+ pagination: {
277
+ pageSize: 5,
278
+ },
279
+ },
280
+ });
281
+
282
+ return (
283
+ <div className="n-w-full n-bg-light-neutral-text-weakest">
284
+ <DataGrid
285
+ isResizable={true}
286
+ tableInstance={table}
287
+ isKeyboardNavigable={true}
288
+ />
289
+ </div>
290
+ );
291
+ };
292
+
293
+ export default Component;
294
+ ```
295
+
296
+ ### Datagrid Compact Mode
297
+
298
+ ```tsx
299
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
300
+
301
+ import { DataGrid } from '@neo4j-ndl/react';
302
+ import {
303
+ createColumnHelper,
304
+ getCoreRowModel,
305
+ getPaginationRowModel,
306
+ getSortedRowModel,
307
+ useReactTable,
308
+ } from '@tanstack/react-table';
309
+
310
+ import { TABLE_DEMO_DATA } from './staticData';
311
+
312
+ type TestDataFormat = {
313
+ name: string;
314
+ age: number;
315
+ cypherCommand: string;
316
+ };
317
+
318
+ const Component = () => {
319
+ const columnHelper = createColumnHelper<TestDataFormat>();
320
+
321
+ const columns = [
322
+ columnHelper.accessor('name', {
323
+ cell: (info) => info.getValue(),
324
+ footer: (info) => info.column.id,
325
+ header: () => 'Name',
326
+ maxSize: 300,
327
+ minSize: 180,
328
+ size: 220,
329
+ }),
330
+ columnHelper.accessor('age', {
331
+ cell: (info) => info.renderValue(),
332
+ footer: (info) => info.column.id,
333
+ header: () => 'Age',
334
+ minSize: 80,
335
+ }),
336
+ columnHelper.accessor('cypherCommand', {
337
+ cell: (info) => info.getValue(),
338
+ footer: (info) => info.column.id,
339
+ header: () => 'Cypher Command',
340
+ minSize: 400,
341
+ size: 400,
342
+ }),
343
+ ];
344
+
345
+ const table = useReactTable({
346
+ columnResizeMode: 'onChange',
347
+ columns,
348
+ data: TABLE_DEMO_DATA,
349
+ debugColumns: true,
350
+ debugHeaders: true,
351
+ debugTable: true,
352
+ defaultColumn: {
353
+ maxSize: 800,
354
+ minSize: 80,
355
+ },
356
+ enableSorting: true,
357
+ getCoreRowModel: getCoreRowModel(),
358
+ getPaginationRowModel: getPaginationRowModel(),
359
+ getSortedRowModel: getSortedRowModel(),
360
+ initialState: {
361
+ pagination: {
362
+ pageSize: 5,
363
+ },
364
+ },
365
+ });
366
+
367
+ return (
368
+ <div className="n-w-full n-bg-light-neutral-text-weakest">
369
+ <DataGrid
370
+ isResizable={false}
371
+ tableInstance={table}
372
+ isKeyboardNavigable={false}
373
+ styling={{
374
+ borderStyle: 'all-sides',
375
+ hasHoverEffects: true,
376
+ hasZebraStriping: false,
377
+ isCompact: true,
378
+ }}
379
+ />
380
+ </div>
381
+ );
382
+ };
383
+
384
+ export default Component;
385
+ ```
386
+
387
+ ### Datagrid Component Overriding
388
+
389
+ ```tsx
390
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
391
+
392
+ import { DataGrid, Tooltip, Typography } from '@neo4j-ndl/react';
393
+ import {
394
+ createColumnHelper,
395
+ getCoreRowModel,
396
+ getPaginationRowModel,
397
+ getSortedRowModel,
398
+ useReactTable,
399
+ } from '@tanstack/react-table';
400
+
401
+ import { TABLE_DEMO_DATA } from './staticData';
402
+
403
+ type TestDataFormat = {
404
+ name: string;
405
+ age: number;
406
+ cypherCommand: string;
407
+ };
408
+
409
+ const Component = () => {
410
+ const columnHelper = createColumnHelper<TestDataFormat>();
411
+
412
+ const columns = [
413
+ columnHelper.accessor('name', {
414
+ cell: (info) => info.getValue(),
415
+ footer: (info) => info.column.id,
416
+ header: () => 'Name',
417
+ maxSize: 300,
418
+ minSize: 180,
419
+ size: 220,
420
+ }),
421
+ columnHelper.accessor('age', {
422
+ cell: (info) => info.renderValue(),
423
+ footer: (info) => info.column.id,
424
+ header: () => 'Age',
425
+ minSize: 80,
426
+ }),
427
+ columnHelper.accessor('cypherCommand', {
428
+ cell: (info) => info.getValue(),
429
+ footer: (info) => info.column.id,
430
+ header: () => 'Cypher Command',
431
+ minSize: 400,
432
+ size: 400,
433
+ }),
434
+ ];
435
+
436
+ const table = useReactTable({
437
+ columnResizeMode: 'onChange',
438
+ columns,
439
+ data: TABLE_DEMO_DATA,
440
+ debugColumns: true,
441
+ debugHeaders: true,
442
+ debugTable: true,
443
+ defaultColumn: {
444
+ maxSize: 800,
445
+ minSize: 80,
446
+ },
447
+ enableSorting: true,
448
+ getCoreRowModel: getCoreRowModel(),
449
+ getPaginationRowModel: getPaginationRowModel(),
450
+ getSortedRowModel: getSortedRowModel(),
451
+ initialState: {
452
+ pagination: {
453
+ pageSize: 5,
454
+ },
455
+ },
456
+ });
457
+
458
+ return (
459
+ <div className="n-flex n-flex-col n-gap-token-8">
460
+ <Typography
461
+ variant="body-medium"
462
+ as="div"
463
+ className="n-flex n-flex-col n-gap-token-8 n-p-token-24 n-pb-token-16"
464
+ >
465
+ <div>Current container height: 400px</div>
466
+ <div>Try updating height with Storybook&apos; s story controls👇</div>
467
+ </Typography>
468
+
469
+ <div
470
+ style={{
471
+ height: '400px',
472
+ }}
473
+ >
474
+ <DataGrid
475
+ className="n-h-full"
476
+ isResizable={true}
477
+ tableInstance={table}
478
+ components={{
479
+ Body: (props) => <DataGrid.Body {...props} />,
480
+ HeaderTitle: ({ cell, children }) => {
481
+ const { column, id } = cell;
482
+ const title =
483
+ typeof column.columnDef.header === 'string'
484
+ ? column.columnDef.header
485
+ : id;
486
+ return (
487
+ <Tooltip type="simple">
488
+ <Tooltip.Trigger hasButtonWrapper>
489
+ <span>{children}</span>
490
+ </Tooltip.Trigger>
491
+ <Tooltip.Content>{title}</Tooltip.Content>
492
+ </Tooltip>
493
+ );
494
+ },
495
+ PaginationNumericButton: ({ isSelected, style, ...restProps }) => {
496
+ return (
497
+ <DataGrid.PaginationNumericButton
498
+ {...restProps}
499
+ isSelected={isSelected}
500
+ style={{
501
+ ...style,
502
+ ...(isSelected && {
503
+ backgroundImage:
504
+ 'linear-gradient(90deg, #00C0FF 0%, #FFCF00 20%, #FC4F4F 40%, #00C0FF 100%)',
505
+ backgroundSize: '200% auto',
506
+ borderRadius: '10px',
507
+ boxShadow: '0 0 20px #eee',
508
+ }),
509
+ }}
510
+ />
511
+ );
512
+ },
513
+ TableResults: () => (
514
+ <DataGrid.TableResults>
515
+ A table with a fixed height of 100px, custom result section and
516
+ custom pagination button
517
+ </DataGrid.TableResults>
518
+ ),
519
+ }}
520
+ />
521
+ </div>
522
+ </div>
523
+ );
524
+ };
525
+
526
+ export default Component;
527
+ ```
528
+
529
+ ### Datagrid Custom Cell With Style Rules
530
+
531
+ ```tsx
532
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
533
+
534
+ import {
535
+ DataGrid,
536
+ type DataGridStyleRule,
537
+ useDataGridCellStyle,
538
+ } from '@neo4j-ndl/react';
539
+ import {
540
+ type Cell,
541
+ createColumnHelper,
542
+ flexRender,
543
+ getCoreRowModel,
544
+ getPaginationRowModel,
545
+ getSortedRowModel,
546
+ type RowData,
547
+ useReactTable,
548
+ } from '@tanstack/react-table';
549
+
550
+ import { TABLE_DEMO_DATA, type TestDataFormat } from './staticData';
551
+
552
+ const styleRules: DataGridStyleRule[] = [
553
+ // Row coloring: all cells styled when name contains "Alicia"
554
+ {
555
+ apply: {
556
+ color: { value: '#000000' },
557
+ textColor: { value: '#ffffff' },
558
+ },
559
+ column: null,
560
+ priority: 3,
561
+ where: { contains: [{ column: 'name' }, 'Alicia'] },
562
+ },
563
+ // Cell coloring: cells in the "age" column with value 51 get green
564
+ {
565
+ apply: { color: { value: '#00bb00' } },
566
+ column: 'age',
567
+ priority: 3,
568
+ where: { equal: [{ column: 'age' }, 51] },
569
+ },
570
+ // Dual color ranges on the "age" column: independent gradients for background
571
+ // (driven by age, full 0-100 range) and text color (driven by age too, but
572
+ // with a narrower 20-80 band so the text contrast is calibrated separately).
573
+ {
574
+ apply: {
575
+ colorRange: {
576
+ maxColor: '#f00000',
577
+ maxValue: 100,
578
+ midColor: '#a00000',
579
+ midValue: 50,
580
+ minColor: '#800000',
581
+ minValue: 0,
582
+ onColumn: 'age',
583
+ },
584
+ textColorRange: {
585
+ maxColor: '#aaaaaa',
586
+ maxValue: 80,
587
+ minColor: '#ffffff',
588
+ minValue: 20,
589
+ onColumn: 'age',
590
+ },
591
+ },
592
+ column: 'age',
593
+ priority: 2, // Lower priority so explicit green rule overrides
594
+ },
595
+ // Multi-column rule: all cells styled when age > 50 AND name contains "o"
596
+ {
597
+ apply: {
598
+ color: { value: '#fef3c7' },
599
+ textColor: { value: '#000000' },
600
+ },
601
+ column: null,
602
+ priority: 0, // Lower priority so other rules take precedence
603
+ where: {
604
+ and: [
605
+ { greaterThan: [{ column: 'age' }, 50] },
606
+ { contains: [{ column: 'name' }, 'o'] },
607
+ ],
608
+ },
609
+ },
610
+ ];
611
+
612
+ /**
613
+ * A custom BodyCell that uses the `useDataGridCellStyle` hook to manually apply style rules to the cell.
614
+ */
615
+ const CustomBodyCell = <T extends RowData>({
616
+ cell,
617
+ }: {
618
+ cell: Cell<T, unknown>;
619
+ }) => {
620
+ const ruleStyle = useDataGridCellStyle(cell);
621
+
622
+ return (
623
+ <div
624
+ className="ndl-data-grid-td"
625
+ role="cell"
626
+ style={{
627
+ ...ruleStyle,
628
+ width: cell.column.getSize(),
629
+ }}
630
+ >
631
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
632
+ </div>
633
+ );
634
+ };
635
+
636
+ const Component = () => {
637
+ const columnHelper = createColumnHelper<TestDataFormat>();
638
+
639
+ const columns = [
640
+ columnHelper.accessor('name', {
641
+ cell: (info) => info.getValue(),
642
+ header: () => 'Name',
643
+ minSize: 180,
644
+ size: 220,
645
+ }),
646
+ columnHelper.accessor('age', {
647
+ cell: (info) => info.renderValue(),
648
+ header: () => 'Age',
649
+ minSize: 80,
650
+ }),
651
+ columnHelper.accessor('cypherCommand', {
652
+ cell: (info) => info.getValue(),
653
+ header: () => 'Cypher Command',
654
+ minSize: 400,
655
+ size: 400,
656
+ }),
657
+ ];
658
+
659
+ const table = useReactTable({
660
+ columnResizeMode: 'onChange',
661
+ columns,
662
+ data: TABLE_DEMO_DATA,
663
+ defaultColumn: {
664
+ maxSize: 800,
665
+ minSize: 80,
666
+ },
667
+ enableSorting: true,
668
+ getCoreRowModel: getCoreRowModel(),
669
+ getPaginationRowModel: getPaginationRowModel(),
670
+ getSortedRowModel: getSortedRowModel(),
671
+ initialState: {
672
+ pagination: {
673
+ pageSize: 10,
674
+ },
675
+ },
676
+ });
677
+
678
+ return (
679
+ <div className="n-w-full n-bg-light-neutral-text-weakest">
680
+ <DataGrid
681
+ isResizable={false}
682
+ tableInstance={table}
683
+ isKeyboardNavigable={false}
684
+ styleRules={styleRules}
685
+ components={{
686
+ BodyCell: CustomBodyCell,
687
+ }}
688
+ styling={{
689
+ borderStyle: 'all-sides',
690
+ hasHoverEffects: true,
691
+ hasZebraStriping: false,
692
+ }}
693
+ />
694
+ </div>
695
+ );
696
+ };
697
+
698
+ export default Component;
699
+ ```
700
+
701
+ ### Datagrid In Dialog
702
+
703
+ ```tsx
704
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
705
+
706
+ import {
707
+ DataGrid,
708
+ type DataGridNav,
709
+ Dialog,
710
+ FilledButton,
711
+ Select,
712
+ useDataGridContext,
713
+ } from '@neo4j-ndl/react';
714
+ import {
715
+ type ColumnDef,
716
+ type ColumnMeta,
717
+ createColumnHelper,
718
+ getCoreRowModel,
719
+ getPaginationRowModel,
720
+ getSortedRowModel,
721
+ useReactTable,
722
+ } from '@tanstack/react-table';
723
+ import { useRef, useState } from 'react';
724
+
725
+ import { TABLE_DEMO_DATA } from './staticData';
726
+
727
+ type TestDataFormat = {
728
+ name: string;
729
+ age: number;
730
+ cypherCommand: string;
731
+ };
732
+
733
+ const columnHelper = createColumnHelper<TestDataFormat>();
734
+
735
+ const actionsColumn: (tableNav: DataGridNav) => ColumnDef<TestDataFormat> = (
736
+ tableNav,
737
+ ) => ({
738
+ cell: ({ row }) => {
739
+ const options = ['Edit', 'Delete', 'View'];
740
+ const dropdownOptions = options.map((o) => ({ label: o, value: o }));
741
+ // eslint-disable-next-line react-hooks/rules-of-hooks
742
+ const [selected, setSelected] = useState(dropdownOptions[0]);
743
+ // eslint-disable-next-line react-hooks/rules-of-hooks
744
+ const { portalTarget } = useDataGridContext();
745
+
746
+ return (
747
+ <div
748
+ data-testid={`ndl-actions-cell-row-${row.index}`}
749
+ className="n-flex n-flex-row n-gap-token-8"
750
+ >
751
+ <Select
752
+ ariaLabel={`actions-select-row-${row.index}`}
753
+ type="select"
754
+ selectProps={{
755
+ menuPortalTarget: portalTarget,
756
+ menuPosition: 'fixed',
757
+ onChange: (e) => {
758
+ if (!e) {
759
+ return;
760
+ }
761
+ setSelected(e);
762
+ },
763
+ onKeyDown: (e) => {
764
+ // Stop propagation of arrow down to the table, to avoid moving to the next item in the cell
765
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
766
+ e.stopPropagation();
767
+ }
768
+ },
769
+ onMenuClose: () => {
770
+ console.info(
771
+ 'Start listening to keyboard events for table navigation',
772
+ );
773
+ tableNav.enable();
774
+ },
775
+ onMenuOpen: () => {
776
+ console.info(
777
+ 'Stop listening to keyboard events for table navigation',
778
+ );
779
+ tableNav.disable();
780
+ },
781
+ options: dropdownOptions,
782
+ value: selected,
783
+ }}
784
+ />
785
+ </div>
786
+ );
787
+ },
788
+ header: 'Custom Select',
789
+ id: 'custom-select',
790
+ minSize: 150,
791
+ });
792
+
793
+ const Component = () => {
794
+ const [data, setData] = useState(TABLE_DEMO_DATA);
795
+ const [isAgeFilterApplied, setIsAgeFilterApplied] = useState(false);
796
+
797
+ const ColumnActions: ColumnMeta<TestDataFormat, unknown> = {
798
+ columnActions: {
799
+ actions: [
800
+ {
801
+ onClick: () => {
802
+ if (isAgeFilterApplied) {
803
+ setData(TABLE_DEMO_DATA);
804
+ setIsAgeFilterApplied(false);
805
+ return;
806
+ } else {
807
+ const filteredData = data.filter((item) => item.age >= 18);
808
+ setData(filteredData);
809
+ setIsAgeFilterApplied(true);
810
+ }
811
+ },
812
+ title: 'Age >= 18',
813
+ },
814
+ ],
815
+ },
816
+ };
817
+
818
+ const regularColumns = [
819
+ columnHelper.accessor('name', {
820
+ cell: (cell) => (
821
+ <DataGrid.InlineEditCell
822
+ cell={cell}
823
+ value={cell.getValue()}
824
+ ariaLabel="edit name"
825
+ />
826
+ ),
827
+ maxSize: 300,
828
+ meta: {
829
+ isInlineEditCell: {
830
+ onEditChange: (newValue, cell) => {
831
+ setData((oldData) => {
832
+ return oldData.map((row) => {
833
+ if (row.name === cell.row.original.name) {
834
+ return {
835
+ ...row,
836
+ name: newValue,
837
+ };
838
+ }
839
+ return row;
840
+ });
841
+ });
842
+ },
843
+ },
844
+ },
845
+ minSize: 180,
846
+ size: 220,
847
+ }),
848
+ columnHelper.accessor('age', {
849
+ cell: (info) => {
850
+ return (
851
+ <DataGrid.DropDownCell
852
+ cell={info}
853
+ options={[
854
+ {
855
+ label: '12',
856
+ value: '12',
857
+ },
858
+ {
859
+ label: '32',
860
+ value: '32',
861
+ },
862
+ {
863
+ label: '37',
864
+ value: '37',
865
+ },
866
+ {
867
+ label: '39',
868
+ value: '39',
869
+ },
870
+ {
871
+ label: '51',
872
+ value: '51',
873
+ },
874
+ ]}
875
+ ariaLabel="select age"
876
+ portalTarget={document.getElementById('default-menu')}
877
+ />
878
+ );
879
+ },
880
+ meta: {
881
+ isDropDownCell: {
882
+ onChange: (newValue, cell) => {
883
+ if (newValue === undefined) {
884
+ // No change
885
+ return;
886
+ }
887
+
888
+ const parsedValue = parseInt(newValue, 10);
889
+
890
+ if (isNaN(parsedValue)) {
891
+ return;
892
+ }
893
+
894
+ setData((oldData) => {
895
+ return oldData.map((row) => {
896
+ if (row.name === cell.row.original.name) {
897
+ return {
898
+ ...row,
899
+ age: parsedValue,
900
+ };
901
+ }
902
+ return row;
903
+ });
904
+ });
905
+ },
906
+ },
907
+ ...ColumnActions,
908
+ },
909
+ minSize: 120,
910
+ }),
911
+ columnHelper.accessor('cypherCommand', {
912
+ cell: (info) => info.getValue(),
913
+ minSize: 400,
914
+ size: 400,
915
+ }),
916
+ ];
917
+
918
+ const allColumns = (tableNav: DataGridNav) => [
919
+ ...regularColumns,
920
+ actionsColumn(tableNav),
921
+ ];
922
+
923
+ const tableNavRef = useRef<DataGridNav>(null!);
924
+ const table = useReactTable({
925
+ autoResetPageIndex: false,
926
+ columnResizeMode: 'onChange',
927
+ columns: allColumns(tableNavRef.current),
928
+ data: data,
929
+ debugColumns: true,
930
+ debugHeaders: true,
931
+ debugTable: true,
932
+ defaultColumn: {
933
+ maxSize: 800,
934
+ minSize: 80,
935
+ },
936
+ enableSorting: true,
937
+ getCoreRowModel: getCoreRowModel(),
938
+ getPaginationRowModel: getPaginationRowModel(),
939
+ getSortedRowModel: getSortedRowModel(),
940
+ initialState: {
941
+ pagination: {
942
+ pageSize: 5,
943
+ },
944
+ },
945
+ });
946
+
947
+ // Modal code
948
+ const [isOpen, setIsOpen] = useState<boolean>(false);
949
+ const handleClick = () => setIsOpen((prev) => !prev);
950
+ const handleClose = () => setIsOpen(false);
951
+
952
+ // DataGrid code
953
+ return (
954
+ <div className="n-w-full">
955
+ <div className="n-flex n-justify-center">
956
+ <FilledButton onClick={handleClick}>Open Dialog</FilledButton>
957
+ </div>
958
+ <Dialog
959
+ className="n-p-token-8 n-bg-neutral-10 n-max-w-max n-rounded-lg"
960
+ htmlAttributes={{
961
+ id: 'default-menu',
962
+ }}
963
+ onClose={handleClose}
964
+ isOpen={isOpen}
965
+ hasDisabledCloseButton={true}
966
+ >
967
+ <Dialog.Content>
968
+ <DataGrid
969
+ portalTarget={document.getElementById('default-menu')}
970
+ isResizable={true}
971
+ tableInstance={table}
972
+ tableNavRef={tableNavRef}
973
+ isKeyboardNavigable={true}
974
+ styling={{
975
+ borderStyle: 'horizontal',
976
+ hasZebraStriping: false,
977
+ }}
978
+ />
979
+ </Dialog.Content>
980
+ </Dialog>
981
+ </div>
982
+ );
983
+ };
984
+
985
+ export default Component;
986
+ ```
987
+
988
+ ### Datagrid Keyboard Navigation
989
+
990
+ ```tsx
991
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
992
+
993
+ import {
994
+ Checkbox,
995
+ DataGrid,
996
+ type DataGridNav,
997
+ IconButton,
998
+ Select,
999
+ useDataGridContext,
1000
+ } from '@neo4j-ndl/react';
1001
+ import { MegaphoneIconOutline } from '@neo4j-ndl/react/icons';
1002
+ import {
1003
+ type ColumnDef,
1004
+ createColumnHelper,
1005
+ getCoreRowModel,
1006
+ getPaginationRowModel,
1007
+ useReactTable,
1008
+ } from '@tanstack/react-table';
1009
+ import { useRef, useState } from 'react';
1010
+
1011
+ import { TABLE_DEMO_DATA } from './staticData';
1012
+
1013
+ type TestDataFormat = {
1014
+ name: string;
1015
+ age: number;
1016
+ cypherCommand: string;
1017
+ };
1018
+
1019
+ const selectableColumn: ColumnDef<TestDataFormat> = {
1020
+ cell: ({ row }) => (
1021
+ <Checkbox
1022
+ ariaLabel={`row-${row.index}`}
1023
+ isChecked={row.getIsSelected() || false}
1024
+ onChange={row.getToggleSelectedHandler()}
1025
+ />
1026
+ ),
1027
+ header: ({ table }) => (
1028
+ <Checkbox
1029
+ ariaLabel="Select all rows"
1030
+ isChecked={table.getIsAllRowsSelected() || false}
1031
+ onChange={table.getToggleAllRowsSelectedHandler()}
1032
+ isIndeterminate={table.getIsSomeRowsSelected()}
1033
+ />
1034
+ ),
1035
+ id: 'select',
1036
+ maxSize: 80,
1037
+ size: 80,
1038
+ };
1039
+
1040
+ const actionsColumn: (tableNav: DataGridNav) => ColumnDef<TestDataFormat> = (
1041
+ tableNav,
1042
+ ) => ({
1043
+ cell: ({ row }) => {
1044
+ const options = ['Edit', 'Delete', 'View'];
1045
+ const dropdownOptions = options.map((o) => ({ label: o, value: o }));
1046
+ // eslint-disable-next-line react-hooks/rules-of-hooks
1047
+ const [selected, setSelected] = useState(dropdownOptions[0]);
1048
+ // eslint-disable-next-line react-hooks/rules-of-hooks
1049
+ const { portalTarget } = useDataGridContext();
1050
+
1051
+ return (
1052
+ <div
1053
+ data-testid={`ndl-actions-cell-row-${row.index}`}
1054
+ className="n-flex n-flex-row n-gap-token-8"
1055
+ >
1056
+ <IconButton
1057
+ description="Megaphone"
1058
+ onClick={() => {
1059
+ alert("Let's get loud 📣");
1060
+ }}
1061
+ htmlAttributes={{
1062
+ type: 'button',
1063
+ }}
1064
+ >
1065
+ <MegaphoneIconOutline />
1066
+ </IconButton>
1067
+ <Select
1068
+ ariaLabel={`actions-select-row-${row.index}`}
1069
+ type="select"
1070
+ selectProps={{
1071
+ menuPortalTarget: portalTarget,
1072
+ onChange: (e) => {
1073
+ if (!e) {
1074
+ return;
1075
+ }
1076
+ setSelected(e);
1077
+ },
1078
+ onKeyDown: (e) => {
1079
+ // Stop propagation of arrow down to the table, to avoid moving to the next item in the cell
1080
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
1081
+ e.stopPropagation();
1082
+ }
1083
+ },
1084
+ onMenuClose: () => {
1085
+ console.info(
1086
+ 'Start listening to keyboard events for table navigation',
1087
+ );
1088
+ tableNav.enable();
1089
+ },
1090
+ onMenuOpen: () => {
1091
+ console.info(
1092
+ 'Stop listening to keyboard events for table navigation',
1093
+ );
1094
+ tableNav.disable();
1095
+ },
1096
+ options: dropdownOptions,
1097
+ value: selected,
1098
+ }}
1099
+ />
1100
+ </div>
1101
+ );
1102
+ },
1103
+ header: 'actions',
1104
+ id: 'actions',
1105
+ });
1106
+
1107
+ const allColumns = (tableNav: DataGridNav) => {
1108
+ const columnHelper = createColumnHelper<TestDataFormat>();
1109
+
1110
+ const commonColumns = [
1111
+ columnHelper.accessor('name', {
1112
+ cell: (info) => info.getValue(),
1113
+ footer: (info) => info.column.id,
1114
+ header: () => 'Name',
1115
+ maxSize: 300,
1116
+ minSize: 180,
1117
+ size: 220,
1118
+ }),
1119
+ columnHelper.accessor('age', {
1120
+ cell: (info) => info.renderValue(),
1121
+ footer: (info) => info.column.id,
1122
+ header: () => 'Age',
1123
+ minSize: 80,
1124
+ }),
1125
+ ];
1126
+
1127
+ return [selectableColumn, ...commonColumns, actionsColumn(tableNav)];
1128
+ };
1129
+
1130
+ const Component = () => {
1131
+ const [rowSelection, setRowSelection] = useState({});
1132
+ const ref = useRef<HTMLDivElement>(null);
1133
+ const tableNavRef = useRef<DataGridNav>(null!);
1134
+
1135
+ const table = useReactTable({
1136
+ columnResizeMode: 'onChange',
1137
+ columns: allColumns(tableNavRef.current),
1138
+ data: TABLE_DEMO_DATA,
1139
+ enableSorting: false,
1140
+ getCoreRowModel: getCoreRowModel(),
1141
+ getPaginationRowModel: getPaginationRowModel(),
1142
+ initialState: {
1143
+ pagination: {
1144
+ pageSize: 5,
1145
+ },
1146
+ },
1147
+ onRowSelectionChange: setRowSelection,
1148
+ state: {
1149
+ rowSelection,
1150
+ },
1151
+ });
1152
+
1153
+ return (
1154
+ <>
1155
+ <DataGrid
1156
+ isResizable={false}
1157
+ isKeyboardNavigable={true}
1158
+ styling={{
1159
+ borderStyle: 'all-sides',
1160
+ }}
1161
+ tableNavRef={tableNavRef}
1162
+ tableInstance={table}
1163
+ ref={ref}
1164
+ portalTarget={document.body}
1165
+ />
1166
+ </>
1167
+ );
1168
+ };
1169
+
1170
+ export default Component;
1171
+ ```
1172
+
1173
+ ### Datagrid Kitchen Sink
1174
+
1175
+ ```tsx
1176
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
1177
+
1178
+ import { DataGrid, Menu } from '@neo4j-ndl/react';
1179
+ import { ChevronDownIconSolid } from '@neo4j-ndl/react/icons';
1180
+ import {
1181
+ type ColumnDef,
1182
+ type ColumnMeta,
1183
+ createColumnHelper,
1184
+ getCoreRowModel,
1185
+ getPaginationRowModel,
1186
+ getSortedRowModel,
1187
+ type TableOptions,
1188
+ useReactTable,
1189
+ } from '@tanstack/react-table';
1190
+ import { Leva, useControls } from 'leva';
1191
+
1192
+ import { type BorderStyle, type HeaderStyle } from '../DataGrid';
1193
+ import { TABLE_DEMO_DATA } from './staticData';
1194
+
1195
+ type TestDataFormat = {
1196
+ name: string;
1197
+ age: number;
1198
+ cypherCommand: string;
1199
+ };
1200
+
1201
+ const columnHelper = createColumnHelper<TestDataFormat>();
1202
+
1203
+ const ColumnActions: ColumnMeta<TestDataFormat, unknown> = {
1204
+ columnActions: {
1205
+ actions: [
1206
+ {
1207
+ onClick: () => {
1208
+ alert('Action triggered');
1209
+ },
1210
+ title: 'Example Column Action',
1211
+ },
1212
+ ],
1213
+ },
1214
+ };
1215
+
1216
+ type ColumnGeneratorOptions = {
1217
+ shouldIncludeRowActionColumn: boolean;
1218
+ };
1219
+ const COMMON_COLUMNS = (options: ColumnGeneratorOptions) => {
1220
+ // oxlint-disable-next-line @typescript-eslint/no-explicit-any
1221
+ const columns: ColumnDef<TestDataFormat, any>[] = [
1222
+ columnHelper.accessor('name', {
1223
+ cell: (info) => info.getValue(),
1224
+ maxSize: 300,
1225
+ minSize: 180,
1226
+ size: 220,
1227
+ }),
1228
+ columnHelper.accessor('age', {
1229
+ cell: (info) => info.getValue(),
1230
+ header: () => <div>Age</div>,
1231
+ minSize: 80,
1232
+ ...(options.shouldIncludeRowActionColumn
1233
+ ? {
1234
+ meta: {
1235
+ ...ColumnActions,
1236
+ },
1237
+ }
1238
+ : {}),
1239
+ }),
1240
+ columnHelper.accessor('cypherCommand', {
1241
+ cell: (info) => info.getValue(),
1242
+ minSize: 400,
1243
+ size: 400,
1244
+ }),
1245
+ ];
1246
+
1247
+ if (options.shouldIncludeRowActionColumn) {
1248
+ columns.push(
1249
+ columnHelper.display({
1250
+ cell: (cell) => {
1251
+ const isSpecialRow =
1252
+ cell.row.original.name === TABLE_DEMO_DATA[3].name || undefined;
1253
+ return (
1254
+ <DataGrid.ActionCell
1255
+ cell={cell}
1256
+ innerCleanIconButtonProps={{
1257
+ children: isSpecialRow && <ChevronDownIconSolid />,
1258
+ description: `Actions for ${cell.row.original.name}`,
1259
+ }}
1260
+ innerMenuProps={
1261
+ isSpecialRow && {
1262
+ children: <Menu.Item title="Custom icon and menu props" />,
1263
+ }
1264
+ }
1265
+ />
1266
+ );
1267
+ },
1268
+ id: 'actions',
1269
+ meta: {
1270
+ isActionCell: {
1271
+ actions: [
1272
+ {
1273
+ onClick: (_event, cell) => {
1274
+ alert(
1275
+ `Row Action triggered for row with index: ${
1276
+ cell.row.index
1277
+ }`,
1278
+ );
1279
+ },
1280
+ title: 'Example Row Action',
1281
+ },
1282
+ ],
1283
+ onOpenChange: (open) => console.info(`onOpenChange: ${open}`),
1284
+ },
1285
+ },
1286
+ }),
1287
+ );
1288
+ }
1289
+
1290
+ return columns;
1291
+ };
1292
+
1293
+ const COMMON_TABLE_PROPS = {
1294
+ columnResizeMode: 'onChange',
1295
+ data: TABLE_DEMO_DATA,
1296
+ // https://tanstack.com/table/v8/docs/guide/column-sizing#overview
1297
+ defaultColumn: {
1298
+ minSize: 80,
1299
+ },
1300
+ enableSorting: false,
1301
+ getCoreRowModel: getCoreRowModel(),
1302
+ getPaginationRowModel: getPaginationRowModel(),
1303
+ initialState: {
1304
+ pagination: {
1305
+ pageSize: 5,
1306
+ },
1307
+ },
1308
+ meta: {
1309
+ actions: [
1310
+ {
1311
+ onClick: () => {
1312
+ alert('wowowow');
1313
+ },
1314
+ title: "Let's party",
1315
+ },
1316
+ ],
1317
+ },
1318
+ } as Omit<TableOptions<TestDataFormat>, 'columns'>;
1319
+
1320
+ const Component = () => {
1321
+ const {
1322
+ keyboard: isKeyboardNavigable,
1323
+ zebraStriping: hasZebraStriping,
1324
+ borderStyle,
1325
+ headerStyle,
1326
+ includeRowActionColumn: shouldIncludeRowActionColumn,
1327
+ } = useControls({
1328
+ borderStyle: {
1329
+ options: ['all-sides', 'horizontal', 'vertical', 'none'],
1330
+ value: 'all-sides',
1331
+ },
1332
+ headerStyle: {
1333
+ options: ['filled', 'clean'],
1334
+ value: 'filled',
1335
+ },
1336
+ includeRowActionColumn: false,
1337
+ keyboard: false,
1338
+ zebraStriping: false,
1339
+ });
1340
+
1341
+ const table = useReactTable({
1342
+ ...COMMON_TABLE_PROPS,
1343
+ columns: COMMON_COLUMNS({
1344
+ shouldIncludeRowActionColumn,
1345
+ }),
1346
+ enableSorting: true,
1347
+ getSortedRowModel: getSortedRowModel(),
1348
+ });
1349
+
1350
+ return (
1351
+ <div className="n-h-[1000px] n-w-full n-bg-primary-bg-weak n-p-token-32">
1352
+ <Leva isRoot />
1353
+ <DataGrid
1354
+ isResizable={true}
1355
+ tableInstance={table}
1356
+ isKeyboardNavigable={isKeyboardNavigable}
1357
+ styling={{
1358
+ borderStyle: borderStyle as BorderStyle,
1359
+ hasZebraStriping,
1360
+ headerStyle: headerStyle as HeaderStyle,
1361
+ }}
1362
+ />
1363
+ </div>
1364
+ );
1365
+ };
1366
+
1367
+ export default Component;
1368
+ ```
1369
+
1370
+ ### Datagrid No Pagination Table
1371
+
1372
+ ```tsx
1373
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
1374
+
1375
+ import { DataGrid } from '@neo4j-ndl/react';
1376
+ import {
1377
+ createColumnHelper,
1378
+ getCoreRowModel,
1379
+ useReactTable,
1380
+ } from '@tanstack/react-table';
1381
+
1382
+ import { PRUNED_TABLE_DEMO_DATA } from './staticData';
1383
+
1384
+ type TestDataFormat = {
1385
+ name: string;
1386
+ age: number;
1387
+ cypherCommand: string;
1388
+ };
1389
+
1390
+ const Component = () => {
1391
+ const columnHelper = createColumnHelper<TestDataFormat>();
1392
+
1393
+ const columns = [
1394
+ columnHelper.accessor('name', {
1395
+ cell: (info) => info.getValue(),
1396
+ footer: (info) => info.column.id,
1397
+ header: () => 'Name',
1398
+ maxSize: 300,
1399
+ minSize: 180,
1400
+ size: 220,
1401
+ }),
1402
+ columnHelper.accessor('age', {
1403
+ cell: (info) => info.renderValue(),
1404
+ footer: (info) => info.column.id,
1405
+ header: () => 'Age',
1406
+ minSize: 80,
1407
+ }),
1408
+ columnHelper.accessor('cypherCommand', {
1409
+ cell: (info) => info.getValue(),
1410
+ footer: (info) => info.column.id,
1411
+ header: () => 'Cypher Command',
1412
+ minSize: 400,
1413
+ size: 400,
1414
+ }),
1415
+ ];
1416
+
1417
+ const table = useReactTable({
1418
+ columns,
1419
+ data: PRUNED_TABLE_DEMO_DATA,
1420
+ enableSorting: false,
1421
+ getCoreRowModel: getCoreRowModel(),
1422
+ });
1423
+
1424
+ return (
1425
+ <DataGrid
1426
+ components={{ Navigation: null }}
1427
+ isResizable={false}
1428
+ tableInstance={table}
1429
+ />
1430
+ );
1431
+ };
1432
+
1433
+ export default Component;
1434
+ ```
1435
+
1436
+ ### Datagrid Percentage Width Columns
1437
+
1438
+ ```tsx
1439
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
1440
+
1441
+ import { DataGrid } from '@neo4j-ndl/react';
1442
+ import {
1443
+ createColumnHelper,
1444
+ getCoreRowModel,
1445
+ getSortedRowModel,
1446
+ useReactTable,
1447
+ } from '@tanstack/react-table';
1448
+
1449
+ import { TABLE_DEMO_DATA } from './staticData';
1450
+
1451
+ type TestDataFormat = {
1452
+ name: string;
1453
+ age: number;
1454
+ cypherCommand: string;
1455
+ };
1456
+
1457
+ const Component = () => {
1458
+ const columnHelper = createColumnHelper<TestDataFormat>();
1459
+
1460
+ const columns = [
1461
+ columnHelper.accessor('name', {
1462
+ cell: (info) => info.getValue(),
1463
+ footer: (info) => info.column.id,
1464
+ header: () => 'Name',
1465
+ size: 200, // 200 / 1000 = 20% of the table width
1466
+ }),
1467
+ columnHelper.accessor('age', {
1468
+ cell: (info) => info.renderValue(),
1469
+ footer: (info) => info.column.id,
1470
+ header: () => 'Age',
1471
+ minSize: 100, // 100 / 1000 = 10% of the table width
1472
+ }),
1473
+ columnHelper.accessor('cypherCommand', {
1474
+ cell: (info) => info.getValue(),
1475
+ footer: (info) => info.column.id,
1476
+ header: () => 'Cypher Command',
1477
+ size: 700, // 700 / 1000 = 70% of the table width
1478
+ }),
1479
+ ];
1480
+
1481
+ const table = useReactTable({
1482
+ columnResizeMode: 'onChange',
1483
+ columns: columns,
1484
+ data: TABLE_DEMO_DATA.slice(0, 5),
1485
+ debugColumns: true,
1486
+ debugHeaders: true,
1487
+ debugTable: true,
1488
+ enableSorting: false,
1489
+ getCoreRowModel: getCoreRowModel(),
1490
+ getSortedRowModel: getSortedRowModel(),
1491
+ });
1492
+
1493
+ // Disable resizing of columns and enable automatic resizing of columns, to allow for percentage width columns
1494
+ const isResizable = false;
1495
+ const isAutoResizingColumns = true;
1496
+
1497
+ return (
1498
+ <div className="n-w-full n-bg-light-neutral-text-weakest">
1499
+ <DataGrid
1500
+ components={{
1501
+ Navigation: null,
1502
+ }}
1503
+ isAutoResizingColumns={isAutoResizingColumns}
1504
+ isKeyboardNavigable={false}
1505
+ isResizable={isResizable}
1506
+ styling={{
1507
+ borderStyle: 'all-sides',
1508
+ hasHoverEffects: false,
1509
+ hasZebraStriping: false,
1510
+ }}
1511
+ tableInstance={table}
1512
+ />
1513
+ </div>
1514
+ );
1515
+ };
1516
+
1517
+ export default Component;
1518
+ ```
1519
+
1520
+ ### Datagrid Placeholder States
1521
+
1522
+ ```tsx
1523
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
1524
+
1525
+ import { DataGrid } from '@neo4j-ndl/react';
1526
+ import {
1527
+ createColumnHelper,
1528
+ getCoreRowModel,
1529
+ getPaginationRowModel,
1530
+ useReactTable,
1531
+ } from '@tanstack/react-table';
1532
+
1533
+ import { TABLE_DEMO_DATA } from './staticData';
1534
+
1535
+ type TestDataFormat = {
1536
+ name: string;
1537
+ age: number;
1538
+ cypherCommand: string;
1539
+ };
1540
+
1541
+ const Component = () => {
1542
+ const columnHelper = createColumnHelper<TestDataFormat>();
1543
+
1544
+ const columns = [
1545
+ columnHelper.accessor('name', {
1546
+ cell: (info) => info.getValue(),
1547
+ footer: (info) => info.column.id,
1548
+ header: () => 'Name',
1549
+ maxSize: 300,
1550
+ minSize: 180,
1551
+ size: 220,
1552
+ }),
1553
+ columnHelper.accessor('age', {
1554
+ cell: (info) => info.renderValue(),
1555
+ footer: (info) => info.column.id,
1556
+ header: () => 'Age',
1557
+ minSize: 80,
1558
+ }),
1559
+ columnHelper.accessor('cypherCommand', {
1560
+ cell: (info) => info.getValue(),
1561
+ footer: (info) => info.column.id,
1562
+ header: () => 'Cypher Command',
1563
+ minSize: 400,
1564
+ size: 400,
1565
+ }),
1566
+ ];
1567
+
1568
+ const table = useReactTable({
1569
+ columnResizeMode: 'onChange',
1570
+ columns,
1571
+ data: TABLE_DEMO_DATA,
1572
+ debugColumns: true,
1573
+ debugHeaders: true,
1574
+ debugTable: true,
1575
+ defaultColumn: {
1576
+ maxSize: 800,
1577
+ minSize: 80,
1578
+ },
1579
+ enableSorting: false,
1580
+ getCoreRowModel: getCoreRowModel(),
1581
+ getPaginationRowModel: getPaginationRowModel(),
1582
+ initialState: {
1583
+ pagination: {
1584
+ pageSize: 5,
1585
+ },
1586
+ },
1587
+ });
1588
+
1589
+ return (
1590
+ <div className="n-w-full n-bg-light-neutral-text-weakest">
1591
+ <DataGrid
1592
+ isResizable={true}
1593
+ tableInstance={table}
1594
+ isKeyboardNavigable={true}
1595
+ />
1596
+ </div>
1597
+ );
1598
+ };
1599
+
1600
+ export default Component;
1601
+ ```
1602
+
1603
+ ### Datagrid Resizable Column
1604
+
1605
+ ```tsx
1606
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
1607
+
1608
+ import { DataGrid } from '@neo4j-ndl/react';
1609
+ import {
1610
+ createColumnHelper,
1611
+ getCoreRowModel,
1612
+ getPaginationRowModel,
1613
+ useReactTable,
1614
+ } from '@tanstack/react-table';
1615
+
1616
+ import { TABLE_DEMO_DATA } from './staticData';
1617
+
1618
+ type TestDataFormat = {
1619
+ name: string;
1620
+ age: number;
1621
+ cypherCommand: string;
1622
+ };
1623
+
1624
+ const Component = ({
1625
+ isKeyboardNavigable = true,
1626
+ components,
1627
+ }: {
1628
+ isKeyboardNavigable?: boolean;
1629
+ components?: React.ComponentProps<typeof DataGrid>['components'];
1630
+ } = {}) => {
1631
+ const columnHelper = createColumnHelper<TestDataFormat>();
1632
+
1633
+ const columns = [
1634
+ columnHelper.accessor('name', {
1635
+ cell: (info) => info.getValue(),
1636
+ footer: (info) => info.column.id,
1637
+ header: () => 'Name',
1638
+ maxSize: 300,
1639
+ minSize: 180,
1640
+ size: 220,
1641
+ }),
1642
+ columnHelper.accessor('age', {
1643
+ cell: (info) => info.renderValue(),
1644
+ footer: (info) => info.column.id,
1645
+ header: () => 'Age',
1646
+ minSize: 80,
1647
+ }),
1648
+ columnHelper.accessor('cypherCommand', {
1649
+ cell: (info) => info.getValue(),
1650
+ footer: (info) => info.column.id,
1651
+ header: () => 'Cypher Command',
1652
+ minSize: 400,
1653
+ size: 400,
1654
+ }),
1655
+ ];
1656
+
1657
+ const table = useReactTable({
1658
+ columnResizeMode: 'onChange',
1659
+ columns,
1660
+ data: TABLE_DEMO_DATA,
1661
+ debugColumns: true,
1662
+ debugHeaders: true,
1663
+ debugTable: true,
1664
+ defaultColumn: {
1665
+ maxSize: 800,
1666
+ minSize: 80,
1667
+ },
1668
+ enableSorting: false,
1669
+ getCoreRowModel: getCoreRowModel(),
1670
+ getPaginationRowModel: getPaginationRowModel(),
1671
+ initialState: {
1672
+ pagination: {
1673
+ pageSize: 5,
1674
+ },
1675
+ },
1676
+ });
1677
+
1678
+ return (
1679
+ <div className="n-w-full n-bg-light-neutral-text-weakest">
1680
+ <DataGrid<TestDataFormat>
1681
+ isResizable={true}
1682
+ tableInstance={table}
1683
+ isKeyboardNavigable={isKeyboardNavigable}
1684
+ // @ts-expect-error limitations of storybooks makes it hard to handle generics
1685
+ components={components}
1686
+ />
1687
+ </div>
1688
+ );
1689
+ };
1690
+
1691
+ export default Component;
1692
+ ```
1693
+
1694
+ ### Datagrid Rule Based Styling
1695
+
1696
+ ```tsx
1697
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
1698
+
1699
+ import { DataGrid } from '@neo4j-ndl/react';
1700
+ import {
1701
+ createColumnHelper,
1702
+ getCoreRowModel,
1703
+ getPaginationRowModel,
1704
+ getSortedRowModel,
1705
+ useReactTable,
1706
+ } from '@tanstack/react-table';
1707
+
1708
+ import { type DataGridStyleRule } from '../style-rules/types';
1709
+ import { TABLE_DEMO_DATA, type TestDataFormat } from './staticData';
1710
+
1711
+ const styleRules: DataGridStyleRule[] = [
1712
+ // Row coloring: all cells styled when name contains "Alicia"
1713
+ {
1714
+ apply: {
1715
+ color: { value: '#000000' },
1716
+ textColor: { value: '#ffffff' },
1717
+ },
1718
+ column: null,
1719
+ priority: 3,
1720
+ where: { contains: [{ column: 'name' }, 'Alicia'] },
1721
+ },
1722
+ // Cell coloring: cells in the "age" column with value 51 get green
1723
+ {
1724
+ apply: { color: { value: '#00bb00' } },
1725
+ column: 'age',
1726
+ priority: 3,
1727
+ where: { equal: [{ column: 'age' }, 51] },
1728
+ },
1729
+ // Dual color ranges on the "age" column: independent gradients for background
1730
+ // (driven by age, full 0-100 range) and text color (driven by age too, but
1731
+ // with a narrower 20-80 band so the text contrast is calibrated separately).
1732
+ {
1733
+ apply: {
1734
+ colorRange: {
1735
+ maxColor: '#f00000',
1736
+ maxValue: 100,
1737
+ midColor: '#a00000',
1738
+ midValue: 50,
1739
+ minColor: '#800000',
1740
+ minValue: 0,
1741
+ onColumn: 'age',
1742
+ },
1743
+ textColorRange: {
1744
+ maxColor: '#aaaaaa',
1745
+ maxValue: 80,
1746
+ minColor: '#ffffff',
1747
+ minValue: 20,
1748
+ onColumn: 'age',
1749
+ },
1750
+ },
1751
+ column: 'age',
1752
+ priority: 2, // Lower priority so explicit green rule overrides
1753
+ },
1754
+ // Multi-column rule: all cells styled when age > 50 AND name contains "o"
1755
+ {
1756
+ apply: {
1757
+ color: { value: '#fef3c7' },
1758
+ textColor: { value: '#000000' },
1759
+ },
1760
+ column: null,
1761
+ priority: 0, // Lower priority so other rules take precedence
1762
+ where: {
1763
+ and: [
1764
+ { greaterThan: [{ column: 'age' }, 50] },
1765
+ { contains: [{ column: 'name' }, 'o'] },
1766
+ ],
1767
+ },
1768
+ },
1769
+ ];
1770
+
1771
+ const Component = () => {
1772
+ const columnHelper = createColumnHelper<TestDataFormat>();
1773
+
1774
+ const columns = [
1775
+ columnHelper.accessor('name', {
1776
+ cell: (info) => info.getValue(),
1777
+ header: () => 'Name',
1778
+ minSize: 180,
1779
+ size: 220,
1780
+ }),
1781
+ columnHelper.accessor('age', {
1782
+ cell: (info) => info.renderValue(),
1783
+ header: () => 'Age',
1784
+ minSize: 80,
1785
+ }),
1786
+ columnHelper.accessor('cypherCommand', {
1787
+ cell: (info) => info.getValue(),
1788
+ header: () => 'Cypher Command',
1789
+ minSize: 400,
1790
+ size: 400,
1791
+ }),
1792
+ ];
1793
+
1794
+ const table = useReactTable({
1795
+ columnResizeMode: 'onChange',
1796
+ columns,
1797
+ data: TABLE_DEMO_DATA,
1798
+ defaultColumn: {
1799
+ maxSize: 800,
1800
+ minSize: 80,
1801
+ },
1802
+ enableSorting: true,
1803
+ getCoreRowModel: getCoreRowModel(),
1804
+ getPaginationRowModel: getPaginationRowModel(),
1805
+ getSortedRowModel: getSortedRowModel(),
1806
+ initialState: {
1807
+ pagination: {
1808
+ pageSize: 10,
1809
+ },
1810
+ },
1811
+ });
1812
+
1813
+ return (
1814
+ <div className="n-w-full n-bg-light-neutral-text-weakest">
1815
+ <DataGrid
1816
+ isResizable={false}
1817
+ tableInstance={table}
1818
+ isKeyboardNavigable={false}
1819
+ styleRules={styleRules}
1820
+ styling={{
1821
+ borderStyle: 'all-sides',
1822
+ hasHoverEffects: true,
1823
+ hasZebraStriping: false,
1824
+ }}
1825
+ />
1826
+ </div>
1827
+ );
1828
+ };
1829
+
1830
+ export default Component;
1831
+ ```
1832
+
1833
+ ### Datagrid Rule Based Styling Datetime
1834
+
1835
+ ```tsx
1836
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
1837
+
1838
+ import { DataGrid } from '@neo4j-ndl/react';
1839
+ import {
1840
+ createColumnHelper,
1841
+ getCoreRowModel,
1842
+ getPaginationRowModel,
1843
+ getSortedRowModel,
1844
+ useReactTable,
1845
+ } from '@tanstack/react-table';
1846
+
1847
+ import { type DataGridStyleRule } from '../style-rules/types';
1848
+
1849
+ type EventRow = {
1850
+ event: string;
1851
+ /**
1852
+ * JS Date coerced from the event's local ISO string. Natively converted to
1853
+ * Unix ms by the engine and compared against { datetime: "..." } literals.
1854
+ */
1855
+ scheduledDate: Date;
1856
+ /**
1857
+ * UTC milliseconds since midnight. Compared against { time: "..." } literals
1858
+ * that are defined in Stockholm / CEST (+02:00) time — the engine normalises
1859
+ * both sides to UTC before comparing.
1860
+ */
1861
+ scheduledTimeMs: number;
1862
+ /** Human-readable local time shown in the cell, e.g. "09:00 CEST (+02:00)". */
1863
+ localTimeDisplay: string;
1864
+ status: 'confirmed' | 'delayed' | 'cancelled';
1865
+ };
1866
+
1867
+ const data: EventRow[] = [
1868
+ // ── Past events (before start of 2026-06-25 in Stockholm / CEST) ──────────
1869
+ {
1870
+ event: 'Quarterly review',
1871
+ localTimeDisplay: '09:00 CEST (+02:00)',
1872
+ // 09:00 Stockholm CEST = 07:00 UTC
1873
+ scheduledDate: new Date('2026-06-10T09:00:00+02:00'),
1874
+ scheduledTimeMs: 7 * 3_600_000,
1875
+ status: 'confirmed',
1876
+ },
1877
+ {
1878
+ event: 'Security audit',
1879
+ localTimeDisplay: '07:30 BST (+01:00)',
1880
+ // 07:30 London BST (+01:00) = 06:30 UTC — before Stockholm opens
1881
+ scheduledDate: new Date('2026-06-18T07:30:00+01:00'),
1882
+ scheduledTimeMs: 6 * 3_600_000 + 30 * 60_000,
1883
+ status: 'delayed',
1884
+ },
1885
+ // ── Today (2026-06-25 in Stockholm / CEST) ─────────────────────────────────
1886
+ {
1887
+ event: 'Customer demo',
1888
+ localTimeDisplay: '09:00 EDT (-04:00)',
1889
+ // 09:00 New York EDT (-04:00) = 13:00 UTC
1890
+ scheduledDate: new Date('2026-06-25T09:00:00-04:00'),
1891
+ scheduledTimeMs: 13 * 3_600_000,
1892
+ status: 'confirmed',
1893
+ },
1894
+ {
1895
+ event: 'Platform migration',
1896
+ localTimeDisplay: '22:00 CEST (+02:00)',
1897
+ // 22:00 Stockholm CEST (+02:00) = 20:00 UTC — after Stockholm closes
1898
+ scheduledDate: new Date('2026-06-25T22:00:00+02:00'),
1899
+ scheduledTimeMs: 20 * 3_600_000,
1900
+ status: 'confirmed',
1901
+ },
1902
+ // ── Near future (up to and including 2026-07-14 in Stockholm) ─────────────
1903
+ {
1904
+ event: 'Board presentation',
1905
+ localTimeDisplay: '10:00 SGT (+08:00)',
1906
+ // 10:00 Singapore SGT (+08:00) = 02:00 UTC — before Stockholm opens
1907
+ scheduledDate: new Date('2026-07-02T10:00:00+08:00'),
1908
+ scheduledTimeMs: 2 * 3_600_000,
1909
+ status: 'confirmed',
1910
+ },
1911
+ {
1912
+ event: 'Partner onboarding',
1913
+ localTimeDisplay: '13:15 CEST (+02:00)',
1914
+ // 13:15 Paris CEST (+02:00) = 11:15 UTC
1915
+ scheduledDate: new Date('2026-07-08T13:15:00+02:00'),
1916
+ scheduledTimeMs: 11 * 3_600_000 + 15 * 60_000,
1917
+ status: 'cancelled',
1918
+ },
1919
+ // ── Far future (after 2026-07-15T00:00 Stockholm / CEST = 2026-07-14T22:00Z)
1920
+ {
1921
+ event: 'Architecture summit',
1922
+ localTimeDisplay: '08:00 CEST (+02:00)',
1923
+ // 08:00 Amsterdam CEST (+02:00) = 06:00 UTC.
1924
+ // Although the calendar date is "July 15", 08:00 CEST is still within the
1925
+ // same UTC day as July 14 22:00Z, so it crosses the far-future boundary.
1926
+ scheduledDate: new Date('2026-07-15T08:00:00+02:00'),
1927
+ scheduledTimeMs: 6 * 3_600_000,
1928
+ status: 'confirmed',
1929
+ },
1930
+ {
1931
+ event: 'Annual conference',
1932
+ localTimeDisplay: '09:00 PDT (-07:00)',
1933
+ // 09:00 San Francisco PDT (-07:00) = 16:00 UTC — after Stockholm closes
1934
+ scheduledDate: new Date('2026-08-03T09:00:00-07:00'),
1935
+ scheduledTimeMs: 16 * 3_600_000,
1936
+ status: 'confirmed',
1937
+ },
1938
+ {
1939
+ event: 'Year-end planning',
1940
+ localTimeDisplay: '09:00 JST (+09:00)',
1941
+ // 09:00 Tokyo JST (+09:00) = 00:00 UTC — midnight, well before Stockholm opens
1942
+ scheduledDate: new Date('2026-11-20T09:00:00+09:00'),
1943
+ scheduledTimeMs: 0,
1944
+ status: 'confirmed',
1945
+ },
1946
+ ];
1947
+
1948
+ /**
1949
+ * All date and time boundaries are anchored to Stockholm in June–July 2026.
1950
+ * Date literals use RFC 9557 IANA bracket notation — the UTC offset is inferred
1951
+ * from the named zone (CEST = +02:00 in this period); writing `+02:00` explicitly
1952
+ * is optional. Time literals use UTC-offset notation.
1953
+ *
1954
+ * "Today" from a Stockholm perspective:
1955
+ * 2026-06-25T00:00:00[Europe/Stockholm] = 2026-06-24T22:00:00Z
1956
+ * 2026-06-26T00:00:00[Europe/Stockholm] = 2026-06-25T22:00:00Z
1957
+ *
1958
+ * "Far future" threshold:
1959
+ * 2026-07-15T00:00:00[Europe/Stockholm] = 2026-07-14T22:00:00Z
1960
+ *
1961
+ * Business hours (Stockholm):
1962
+ * { time: '09:00:00+02:00' } resolves to 07:00 UTC (25 200 000 ms)
1963
+ * { time: '17:00:00+02:00' } resolves to 15:00 UTC (54 000 000 ms)
1964
+ */
1965
+ const styleRules: DataGridStyleRule[] = [
1966
+ // Past events — entire row in muted red
1967
+ {
1968
+ apply: {
1969
+ color: { value: '#fee2e2' },
1970
+ textColor: { value: '#7f1d1d' },
1971
+ },
1972
+ column: null,
1973
+ priority: 1,
1974
+ where: {
1975
+ lessThan: [
1976
+ { column: 'scheduledDate' },
1977
+ { datetime: '2026-06-25T00:00:00[Europe/Stockholm]' },
1978
+ ],
1979
+ },
1980
+ },
1981
+ // Today (in Stockholm timezone) — entire row in amber
1982
+ {
1983
+ apply: {
1984
+ color: { value: '#fef9c3' },
1985
+ textColor: { value: '#713f12' },
1986
+ },
1987
+ column: null,
1988
+ priority: 1,
1989
+ where: {
1990
+ and: [
1991
+ {
1992
+ greaterThanOrEqual: [
1993
+ { column: 'scheduledDate' },
1994
+ { datetime: '2026-06-25T00:00:00[Europe/Stockholm]' },
1995
+ ],
1996
+ },
1997
+ {
1998
+ lessThan: [
1999
+ { column: 'scheduledDate' },
2000
+ { datetime: '2026-06-26T00:00:00[Europe/Stockholm]' },
2001
+ ],
2002
+ },
2003
+ ],
2004
+ },
2005
+ },
2006
+ // Far future (after 2026-07-15 midnight Stockholm time) — entire row in green
2007
+ {
2008
+ apply: {
2009
+ color: { value: '#dcfce7' },
2010
+ textColor: { value: '#14532d' },
2011
+ },
2012
+ column: null,
2013
+ priority: 1,
2014
+ where: {
2015
+ greaterThanOrEqual: [
2016
+ { column: 'scheduledDate' },
2017
+ { datetime: '2026-07-15T00:00:00[Europe/Stockholm]' },
2018
+ ],
2019
+ },
2020
+ },
2021
+ // Outside Stockholm business hours (before 09:00 CEST or from 17:00 CEST) —
2022
+ // time cell in orange. The { time: "..." } literals carry an offset so the
2023
+ // engine normalises them to UTC before comparing against scheduledTimeMs.
2024
+ {
2025
+ apply: {
2026
+ color: { value: '#ffedd5' },
2027
+ textColor: { value: '#7c2d12' },
2028
+ },
2029
+ column: 'scheduledTimeMs',
2030
+ priority: 2,
2031
+ where: {
2032
+ or: [
2033
+ {
2034
+ lessThan: [{ column: 'scheduledTimeMs' }, { time: '09:00:00+02:00' }],
2035
+ },
2036
+ {
2037
+ greaterThanOrEqual: [
2038
+ { column: 'scheduledTimeMs' },
2039
+ { time: '17:00:00+02:00' },
2040
+ ],
2041
+ },
2042
+ ],
2043
+ },
2044
+ },
2045
+ // Cancelled events — all cells grayed out at highest priority
2046
+ {
2047
+ apply: {
2048
+ color: { value: '#f3f4f6' },
2049
+ textColor: { value: '#9ca3af' },
2050
+ },
2051
+ column: null,
2052
+ priority: 5,
2053
+ where: { equal: [{ column: 'status' }, 'cancelled'] },
2054
+ },
2055
+ ];
2056
+
2057
+ const Component = () => {
2058
+ const columnHelper = createColumnHelper<EventRow>();
2059
+
2060
+ const columns = [
2061
+ columnHelper.accessor('event', {
2062
+ cell: (info) => info.getValue(),
2063
+ header: () => 'Event',
2064
+ minSize: 180,
2065
+ size: 220,
2066
+ }),
2067
+ columnHelper.accessor('scheduledDate', {
2068
+ cell: (info) =>
2069
+ info.getValue().toLocaleDateString('en-GB', {
2070
+ day: 'numeric',
2071
+ month: 'short',
2072
+ timeZone: 'Europe/Stockholm',
2073
+ year: 'numeric',
2074
+ }),
2075
+ header: () => 'Date (Stockholm)',
2076
+ minSize: 150,
2077
+ size: 170,
2078
+ }),
2079
+ columnHelper.accessor('localTimeDisplay', {
2080
+ cell: (info) => info.getValue(),
2081
+ header: () => 'Local time',
2082
+ minSize: 160,
2083
+ size: 180,
2084
+ }),
2085
+ columnHelper.accessor('scheduledTimeMs', {
2086
+ cell: (info) => {
2087
+ const ms = info.getValue();
2088
+ const h = Math.floor(ms / 3_600_000)
2089
+ .toString()
2090
+ .padStart(2, '0');
2091
+ const m = Math.floor((ms % 3_600_000) / 60_000)
2092
+ .toString()
2093
+ .padStart(2, '0');
2094
+ return `${h}:${m} UTC`;
2095
+ },
2096
+ header: () => 'Time (UTC)',
2097
+ minSize: 110,
2098
+ size: 130,
2099
+ }),
2100
+ columnHelper.accessor('status', {
2101
+ cell: (info) => info.getValue(),
2102
+ header: () => 'Status',
2103
+ minSize: 100,
2104
+ size: 120,
2105
+ }),
2106
+ ];
2107
+
2108
+ const table = useReactTable({
2109
+ columnResizeMode: 'onChange',
2110
+ columns,
2111
+ data,
2112
+ defaultColumn: {
2113
+ maxSize: 800,
2114
+ minSize: 80,
2115
+ },
2116
+ enableSorting: true,
2117
+ getCoreRowModel: getCoreRowModel(),
2118
+ getPaginationRowModel: getPaginationRowModel(),
2119
+ getSortedRowModel: getSortedRowModel(),
2120
+ initialState: {
2121
+ pagination: { pageSize: 10 },
2122
+ },
2123
+ });
2124
+
2125
+ return (
2126
+ <div className="n-w-full n-bg-light-neutral-text-weakest">
2127
+ <DataGrid
2128
+ isResizable={false}
2129
+ isKeyboardNavigable={false}
2130
+ tableInstance={table}
2131
+ styleRules={styleRules}
2132
+ styling={{
2133
+ borderStyle: 'all-sides',
2134
+ hasHoverEffects: true,
2135
+ hasZebraStriping: false,
2136
+ }}
2137
+ />
2138
+ </div>
2139
+ );
2140
+ };
2141
+
2142
+ export default Component;
2143
+ ```
2144
+
2145
+ ### Datagrid Selectable
2146
+
2147
+ ```tsx
2148
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
2149
+
2150
+ import { Checkbox, DataGrid } from '@neo4j-ndl/react';
2151
+ import {
2152
+ type ColumnDef,
2153
+ createColumnHelper,
2154
+ getCoreRowModel,
2155
+ getPaginationRowModel,
2156
+ useReactTable,
2157
+ } from '@tanstack/react-table';
2158
+ import { useState } from 'react';
2159
+
2160
+ import { TABLE_DEMO_DATA } from './staticData';
2161
+
2162
+ type TestDataFormat = {
2163
+ name: string;
2164
+ age: number;
2165
+ cypherCommand: string;
2166
+ };
2167
+
2168
+ const selectableColumn: ColumnDef<TestDataFormat> = {
2169
+ cell: ({ row }) => (
2170
+ <div style={{ padding: '4px 0' }}>
2171
+ <Checkbox
2172
+ ariaLabel={`row-${row.index}`}
2173
+ isChecked={row.getIsSelected() || false}
2174
+ onChange={row.getToggleSelectedHandler()}
2175
+ />
2176
+ </div>
2177
+ ),
2178
+ header: ({ table }) => (
2179
+ <Checkbox
2180
+ ariaLabel="Select all rows"
2181
+ isChecked={table.getIsAllRowsSelected() || false}
2182
+ // If you want to select all pages from the current page use:
2183
+ // https://tanstack.com/table/v8/docs/api/features/row-selection#gettoggleallpagerowsselectedhandler
2184
+ onChange={table.getToggleAllRowsSelectedHandler()}
2185
+ isIndeterminate={table.getIsSomeRowsSelected()}
2186
+ />
2187
+ ),
2188
+ id: 'select',
2189
+ maxSize: 80,
2190
+ minSize: 48,
2191
+ size: 80,
2192
+ };
2193
+ const columnHelper = createColumnHelper<TestDataFormat>();
2194
+
2195
+ const commonColumns = [
2196
+ columnHelper.accessor('name', {
2197
+ cell: (info) => info.getValue(),
2198
+ footer: (info) => info.column.id,
2199
+ header: () => 'Name',
2200
+ maxSize: 300,
2201
+ minSize: 180,
2202
+ size: 220,
2203
+ }),
2204
+ columnHelper.accessor('age', {
2205
+ cell: (info) => info.renderValue(),
2206
+ footer: (info) => info.column.id,
2207
+ header: () => 'Age',
2208
+ minSize: 80,
2209
+ }),
2210
+ columnHelper.accessor('cypherCommand', {
2211
+ cell: (info) => info.getValue(),
2212
+ footer: (info) => info.column.id,
2213
+ header: () => 'Cypher Command',
2214
+ minSize: 400,
2215
+ size: 400,
2216
+ }),
2217
+ ];
2218
+
2219
+ const allColumns = [selectableColumn, ...commonColumns];
2220
+
2221
+ const Component = () => {
2222
+ const [rowSelection, setRowSelection] = useState({});
2223
+
2224
+ const table = useReactTable({
2225
+ columnResizeMode: 'onChange',
2226
+ columns: allColumns,
2227
+ data: TABLE_DEMO_DATA,
2228
+ enableSorting: false,
2229
+ getCoreRowModel: getCoreRowModel(),
2230
+ getPaginationRowModel: getPaginationRowModel(),
2231
+ initialState: {
2232
+ pagination: {
2233
+ pageSize: 5,
2234
+ },
2235
+ },
2236
+ onRowSelectionChange: setRowSelection,
2237
+ state: {
2238
+ rowSelection,
2239
+ },
2240
+ });
2241
+
2242
+ return (
2243
+ <DataGrid
2244
+ isResizable={false}
2245
+ tableInstance={table}
2246
+ isKeyboardNavigable={false}
2247
+ />
2248
+ );
2249
+ };
2250
+
2251
+ export default Component;
2252
+ ```
2253
+
2254
+ ### Datagrid Simple
2255
+
2256
+ ```tsx
2257
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
2258
+
2259
+ import { DataGrid } from '@neo4j-ndl/react';
2260
+ import {
2261
+ createColumnHelper,
2262
+ getCoreRowModel,
2263
+ getPaginationRowModel,
2264
+ getSortedRowModel,
2265
+ useReactTable,
2266
+ } from '@tanstack/react-table';
2267
+
2268
+ import { TABLE_DEMO_DATA } from './staticData';
2269
+
2270
+ type TestDataFormat = {
2271
+ name: string;
2272
+ age: number;
2273
+ cypherCommand: string;
2274
+ };
2275
+
2276
+ const Component = () => {
2277
+ const columnHelper = createColumnHelper<TestDataFormat>();
2278
+
2279
+ const columns = [
2280
+ columnHelper.accessor('name', {
2281
+ cell: (info) => info.getValue(),
2282
+ footer: (info) => info.column.id,
2283
+ header: () => 'Name',
2284
+ maxSize: 300,
2285
+ minSize: 180,
2286
+ size: 220,
2287
+ }),
2288
+ columnHelper.accessor('age', {
2289
+ cell: (info) => info.renderValue(),
2290
+ footer: (info) => info.column.id,
2291
+ header: () => 'Age',
2292
+ minSize: 80,
2293
+ }),
2294
+ columnHelper.accessor('cypherCommand', {
2295
+ cell: (info) => info.getValue(),
2296
+ footer: (info) => info.column.id,
2297
+ header: () => 'Cypher Command',
2298
+ minSize: 400,
2299
+ size: 400,
2300
+ }),
2301
+ ];
2302
+
2303
+ const table = useReactTable({
2304
+ columnResizeMode: 'onChange',
2305
+ columns,
2306
+ data: TABLE_DEMO_DATA,
2307
+ debugColumns: true,
2308
+ debugHeaders: true,
2309
+ debugTable: true,
2310
+ defaultColumn: {
2311
+ maxSize: 800,
2312
+ minSize: 80,
2313
+ },
2314
+ enableSorting: true,
2315
+ getCoreRowModel: getCoreRowModel(),
2316
+ getPaginationRowModel: getPaginationRowModel(),
2317
+ getSortedRowModel: getSortedRowModel(),
2318
+ initialState: {
2319
+ pagination: {
2320
+ pageSize: 5,
2321
+ },
2322
+ },
2323
+ });
2324
+
2325
+ return (
2326
+ <div className="n-w-full n-bg-light-neutral-text-weakest">
2327
+ <DataGrid
2328
+ isResizable={false}
2329
+ tableInstance={table}
2330
+ isKeyboardNavigable={false}
2331
+ styling={{
2332
+ borderStyle: 'all-sides',
2333
+ hasHoverEffects: true,
2334
+ hasZebraStriping: false,
2335
+ }}
2336
+ />
2337
+ </div>
2338
+ );
2339
+ };
2340
+
2341
+ export default Component;
2342
+ ```
2343
+
2344
+ ### Datagrid Single Selectable
2345
+
2346
+ ```tsx
2347
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
2348
+
2349
+ import { DataGrid, Radio, useDataGridContext } from '@neo4j-ndl/react';
2350
+ import {
2351
+ createColumnHelper,
2352
+ getCoreRowModel,
2353
+ getPaginationRowModel,
2354
+ useReactTable,
2355
+ } from '@tanstack/react-table';
2356
+
2357
+ type PricingEntry = {
2358
+ cpuCount: number;
2359
+ id: number;
2360
+ memory: string;
2361
+ storage: string;
2362
+ };
2363
+
2364
+ const columnHelper = createColumnHelper<PricingEntry>();
2365
+
2366
+ const DEMO_PRICING: PricingEntry[] = [
2367
+ {
2368
+ cpuCount: 1,
2369
+ id: 1,
2370
+ memory: '1 GB',
2371
+ storage: '10 GB',
2372
+ },
2373
+ {
2374
+ cpuCount: 2,
2375
+ id: 2,
2376
+ memory: '2 GB',
2377
+ storage: '20 GB',
2378
+ },
2379
+ {
2380
+ cpuCount: 3,
2381
+ id: 3,
2382
+ memory: '3 GB',
2383
+ storage: '30 GB',
2384
+ },
2385
+ ];
2386
+
2387
+ const columns = [
2388
+ columnHelper.accessor('id', {
2389
+ cell: ({ cell, row }) => (
2390
+ <Radio
2391
+ ariaLabel={`value-${cell.getValue()}`}
2392
+ isChecked={row.getIsSelected()}
2393
+ />
2394
+ ),
2395
+ header: () => null,
2396
+ maxSize: 50,
2397
+ size: 50,
2398
+ }),
2399
+ columnHelper.accessor('memory', {
2400
+ cell: (info) => info.getValue(),
2401
+ minSize: 120,
2402
+ }),
2403
+ columnHelper.accessor('cpuCount', {
2404
+ cell: (info) => info.getValue(),
2405
+ }),
2406
+ columnHelper.accessor('storage', {
2407
+ cell: (info) => info.getValue(),
2408
+ }),
2409
+ ];
2410
+
2411
+ const CustomBodyRow = ({
2412
+ row,
2413
+ }: React.ComponentProps<typeof DataGrid.BodyRow<PricingEntry>>) => {
2414
+ const { components } = useDataGridContext<PricingEntry>();
2415
+
2416
+ return (
2417
+ <div key={row.id} role="row" className="ndl-data-grid-tr">
2418
+ {row.getVisibleCells().map(
2419
+ (cell) =>
2420
+ components.BodyCell && (
2421
+ <components.BodyCell
2422
+ key={cell.id}
2423
+ cell={cell}
2424
+ htmlAttributes={{
2425
+ onClick: () => row.toggleSelected(!row.getIsSelected()),
2426
+ }}
2427
+ />
2428
+ ),
2429
+ )}
2430
+ </div>
2431
+ );
2432
+ };
2433
+
2434
+ const Component = () => {
2435
+ const table = useReactTable({
2436
+ columnResizeMode: 'onChange',
2437
+ columns,
2438
+ data: DEMO_PRICING,
2439
+ enableMultiRowSelection: false,
2440
+ enableSorting: false,
2441
+ getCoreRowModel: getCoreRowModel(),
2442
+ getPaginationRowModel: getPaginationRowModel(),
2443
+ initialState: {
2444
+ rowSelection: {
2445
+ 0: true,
2446
+ },
2447
+ },
2448
+ });
2449
+
2450
+ return (
2451
+ <div className="n-w-full">
2452
+ <DataGrid
2453
+ components={{
2454
+ BodyRow: CustomBodyRow,
2455
+ Navigation: null,
2456
+ }}
2457
+ isKeyboardNavigable={false}
2458
+ isResizable={false}
2459
+ tableInstance={table}
2460
+ />
2461
+ </div>
2462
+ );
2463
+ };
2464
+
2465
+ export default Component;
2466
+ ```
2467
+
2468
+ ### Datagrid Skeleton Loading
2469
+
2470
+ ```tsx
2471
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
2472
+
2473
+ import { DataGrid } from '@neo4j-ndl/react';
2474
+ import {
2475
+ createColumnHelper,
2476
+ getCoreRowModel,
2477
+ getPaginationRowModel,
2478
+ useReactTable,
2479
+ } from '@tanstack/react-table';
2480
+
2481
+ import { TABLE_DEMO_DATA } from './staticData';
2482
+
2483
+ type TestDataFormat = {
2484
+ name: string;
2485
+ age: number;
2486
+ cypherCommand: string;
2487
+ };
2488
+
2489
+ const Component = () => {
2490
+ const columnHelper = createColumnHelper<TestDataFormat>();
2491
+
2492
+ const columns = [
2493
+ columnHelper.accessor('name', {
2494
+ cell: (info) => info.getValue(),
2495
+ footer: (info) => info.column.id,
2496
+ header: () => 'Name',
2497
+ maxSize: 300,
2498
+ minSize: 180,
2499
+ size: 220,
2500
+ }),
2501
+ columnHelper.accessor('age', {
2502
+ cell: (info) => info.renderValue(),
2503
+ footer: (info) => info.column.id,
2504
+ header: () => 'Age',
2505
+ minSize: 80,
2506
+ }),
2507
+ columnHelper.accessor('cypherCommand', {
2508
+ cell: (info) => info.getValue(),
2509
+ footer: (info) => info.column.id,
2510
+ header: () => 'Cypher Command',
2511
+ minSize: 400,
2512
+ size: 400,
2513
+ }),
2514
+ ];
2515
+
2516
+ const table = useReactTable({
2517
+ columnResizeMode: 'onChange',
2518
+ columns,
2519
+ data: TABLE_DEMO_DATA,
2520
+ debugColumns: true,
2521
+ debugHeaders: true,
2522
+ debugTable: true,
2523
+ defaultColumn: {
2524
+ maxSize: 800,
2525
+ minSize: 80,
2526
+ },
2527
+ enableSorting: false,
2528
+ getCoreRowModel: getCoreRowModel(),
2529
+ getPaginationRowModel: getPaginationRowModel(),
2530
+ initialState: {
2531
+ pagination: {
2532
+ pageSize: 5,
2533
+ },
2534
+ },
2535
+ });
2536
+
2537
+ return (
2538
+ <div className="n-w-full n-bg-light-neutral-text-weakest">
2539
+ <DataGrid
2540
+ isResizable={true}
2541
+ tableInstance={table}
2542
+ isKeyboardNavigable={true}
2543
+ isSkeletonLoading={true}
2544
+ />
2545
+ </div>
2546
+ );
2547
+ };
2548
+
2549
+ export default Component;
2550
+ ```
2551
+
2552
+ ### Datagrid Sortable
2553
+
2554
+ ```tsx
2555
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
2556
+
2557
+ import { DataGrid } from '@neo4j-ndl/react';
2558
+ import {
2559
+ createColumnHelper,
2560
+ getCoreRowModel,
2561
+ getPaginationRowModel,
2562
+ getSortedRowModel,
2563
+ useReactTable,
2564
+ } from '@tanstack/react-table';
2565
+
2566
+ import { TABLE_DEMO_DATA } from './staticData';
2567
+
2568
+ type TestDataFormat = {
2569
+ name: string;
2570
+ age: number;
2571
+ cypherCommand: string;
2572
+ };
2573
+
2574
+ const Component = () => {
2575
+ const columnHelper = createColumnHelper<TestDataFormat>();
2576
+
2577
+ const columns = [
2578
+ columnHelper.accessor('name', {
2579
+ cell: (info) => info.getValue(),
2580
+ footer: (info) => info.column.id,
2581
+ header: () => 'Name',
2582
+ maxSize: 300,
2583
+ minSize: 180,
2584
+ size: 220,
2585
+ }),
2586
+ columnHelper.accessor('age', {
2587
+ cell: (info) => info.renderValue(),
2588
+ footer: (info) => info.column.id,
2589
+ header: () => 'Age',
2590
+ minSize: 80,
2591
+ }),
2592
+ columnHelper.accessor('cypherCommand', {
2593
+ cell: (info) => info.getValue(),
2594
+ footer: (info) => info.column.id,
2595
+ header: () => 'Cypher Command',
2596
+ minSize: 400,
2597
+ size: 400,
2598
+ }),
2599
+ ];
2600
+
2601
+ const table = useReactTable({
2602
+ columnResizeMode: 'onChange',
2603
+ columns,
2604
+ data: TABLE_DEMO_DATA,
2605
+ debugColumns: true,
2606
+ debugHeaders: true,
2607
+ debugTable: true,
2608
+ defaultColumn: {
2609
+ maxSize: 800,
2610
+ minSize: 80,
2611
+ },
2612
+ enableSorting: true,
2613
+ getCoreRowModel: getCoreRowModel(),
2614
+ getPaginationRowModel: getPaginationRowModel(),
2615
+ getSortedRowModel: getSortedRowModel(),
2616
+ initialState: {
2617
+ pagination: {
2618
+ pageSize: 5,
2619
+ },
2620
+ },
2621
+ });
2622
+
2623
+ return (
2624
+ <div className="n-w-full n-bg-light-neutral-text-weakest">
2625
+ <DataGrid
2626
+ isResizable={true}
2627
+ tableInstance={table}
2628
+ isKeyboardNavigable={true}
2629
+ />
2630
+ </div>
2631
+ );
2632
+ };
2633
+
2634
+ export default Component;
2635
+ ```
2636
+
2637
+ ### Datagrid Using Editable Cells
2638
+
2639
+ ```tsx
2640
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
2641
+
2642
+ import { tokens } from '@neo4j-ndl/base';
2643
+ import { DataGrid, GraphLabel } from '@neo4j-ndl/react';
2644
+ import { ExclamationTriangleIconSolid } from '@neo4j-ndl/react/icons';
2645
+ import {
2646
+ createColumnHelper,
2647
+ getCoreRowModel,
2648
+ getPaginationRowModel,
2649
+ getSortedRowModel,
2650
+ useReactTable,
2651
+ } from '@tanstack/react-table';
2652
+ import { useState } from 'react';
2653
+ import { type MultiValueProps } from 'react-select';
2654
+
2655
+ type TestDataFormat = {
2656
+ name: string;
2657
+ role: string;
2658
+ permissions: string[];
2659
+ labels: string[];
2660
+ status: string;
2661
+ };
2662
+
2663
+ const ROLE_OPTIONS = [
2664
+ { label: 'Admin', value: 'admin' },
2665
+ { label: 'Editor', value: 'editor' },
2666
+ { label: 'Viewer', value: 'viewer' },
2667
+ ];
2668
+
2669
+ const PERMISSION_OPTIONS = [
2670
+ { label: 'Read', value: 'read' },
2671
+ { label: 'Write', value: 'write' },
2672
+ { label: 'Delete', value: 'delete' },
2673
+ ];
2674
+
2675
+ const LABEL_OPTIONS = [
2676
+ { label: 'Person', value: 'person' },
2677
+ { label: 'Movie', value: 'movie' },
2678
+ { label: 'Director', value: 'director' },
2679
+ { label: 'Actor', value: 'actor' },
2680
+ ];
2681
+
2682
+ const STATUS_OPTIONS = [
2683
+ { label: 'Active', value: 'active' },
2684
+ { label: 'Inactive', value: 'inactive' },
2685
+ { label: 'Pending', value: 'pending' },
2686
+ ];
2687
+
2688
+ const VALID_STATUSES = STATUS_OPTIONS.map((option) => option.value);
2689
+
2690
+ type DropDownOption = { value: string; label: string };
2691
+
2692
+ const LABEL_COLORS: Record<string, string> = {
2693
+ actor: tokens.graph['7'],
2694
+ director: tokens.graph['4'],
2695
+ movie: tokens.graph['3'],
2696
+ person: tokens.graph['1'],
2697
+ };
2698
+
2699
+ // Render each selected value as a non-interactive GraphLabel (as a span)
2700
+ // instead of the default Tag chip, demonstrating the components override.
2701
+ const LabelMultiValue = (props: MultiValueProps<DropDownOption, boolean>) => (
2702
+ <GraphLabel
2703
+ as="span"
2704
+ type="node"
2705
+ size="small"
2706
+ color={LABEL_COLORS[props.data.value]}
2707
+ >
2708
+ {props.data.label}
2709
+ </GraphLabel>
2710
+ );
2711
+
2712
+ // The "Dallas Kuhn" row has a status that is not part of the available
2713
+ // options, so its dropdown cell renders in the error state.
2714
+ const DEMO_DATA: TestDataFormat[] = [
2715
+ {
2716
+ labels: ['person', 'actor'],
2717
+ name: 'Alicia Sanford',
2718
+ permissions: ['read', 'write'],
2719
+ role: 'admin',
2720
+ status: 'active',
2721
+ },
2722
+ {
2723
+ labels: ['movie'],
2724
+ name: 'Dallas Kuhn',
2725
+ permissions: ['read'],
2726
+ role: 'editor',
2727
+ status: 'unknown',
2728
+ },
2729
+ {
2730
+ labels: ['director'],
2731
+ name: 'Susan Daugherty MD',
2732
+ permissions: ['read', 'write', 'delete'],
2733
+ role: 'viewer',
2734
+ status: 'pending',
2735
+ },
2736
+ {
2737
+ labels: ['person'],
2738
+ name: 'Karen Huel',
2739
+ permissions: ['read'],
2740
+ role: 'editor',
2741
+ status: 'inactive',
2742
+ },
2743
+ {
2744
+ labels: ['movie', 'director'],
2745
+ name: 'Ken Kessler',
2746
+ permissions: ['write', 'delete'],
2747
+ role: 'admin',
2748
+ status: 'active',
2749
+ },
2750
+ {
2751
+ labels: [],
2752
+ name: 'Courtney McClure',
2753
+ permissions: [],
2754
+ role: 'viewer',
2755
+ status: 'pending',
2756
+ },
2757
+ ];
2758
+
2759
+ const columnHelper = createColumnHelper<TestDataFormat>();
2760
+
2761
+ const Component = () => {
2762
+ const [data, setData] = useState(DEMO_DATA);
2763
+
2764
+ const updateRow = (name: string, update: Partial<TestDataFormat>): void => {
2765
+ setData((oldData) =>
2766
+ oldData.map((row) => (row.name === name ? { ...row, ...update } : row)),
2767
+ );
2768
+ };
2769
+
2770
+ const COMMON_COLUMNS = [
2771
+ // Editable inline text cell.
2772
+ columnHelper.accessor('name', {
2773
+ cell: (cell) => (
2774
+ <DataGrid.InlineEditCell
2775
+ cell={cell}
2776
+ value={cell.getValue()}
2777
+ ariaLabel="edit name"
2778
+ />
2779
+ ),
2780
+ meta: {
2781
+ isInlineEditCell: {
2782
+ onEditChange: (newValue, cell) => {
2783
+ updateRow(cell.row.original.name, { name: newValue });
2784
+ },
2785
+ },
2786
+ },
2787
+ minSize: 180,
2788
+ size: 220,
2789
+ }),
2790
+ // Default single-select dropdown.
2791
+ columnHelper.accessor('role', {
2792
+ cell: (info) => (
2793
+ <DataGrid.DropDownCell
2794
+ cell={info}
2795
+ options={ROLE_OPTIONS}
2796
+ ariaLabel="select role"
2797
+ />
2798
+ ),
2799
+ meta: {
2800
+ isDropDownCell: {
2801
+ onChange: (newValue, cell) => {
2802
+ if (newValue === undefined) {
2803
+ return;
2804
+ }
2805
+ updateRow(cell.row.original.name, { role: newValue });
2806
+ },
2807
+ },
2808
+ },
2809
+ minSize: 160,
2810
+ size: 180,
2811
+ }),
2812
+ // Default multi-select dropdown (default Tag chips).
2813
+ columnHelper.accessor('permissions', {
2814
+ cell: (info) => (
2815
+ <DataGrid.DropDownCell
2816
+ cell={info}
2817
+ options={PERMISSION_OPTIONS}
2818
+ ariaLabel="select permissions"
2819
+ />
2820
+ ),
2821
+ enableSorting: false,
2822
+ meta: {
2823
+ isDropDownCell: {
2824
+ isMulti: true,
2825
+ onChange: (newValue, cell) => {
2826
+ updateRow(cell.row.original.name, { permissions: newValue });
2827
+ },
2828
+ },
2829
+ },
2830
+ minSize: 220,
2831
+ size: 260,
2832
+ }),
2833
+ // Multi-select dropdown with a custom MultiValue (GraphLabel chips).
2834
+ columnHelper.accessor('labels', {
2835
+ cell: (info) => (
2836
+ <DataGrid.DropDownCell
2837
+ cell={info}
2838
+ options={LABEL_OPTIONS}
2839
+ ariaLabel="select labels"
2840
+ components={{ MultiValue: LabelMultiValue }}
2841
+ />
2842
+ ),
2843
+ enableSorting: false,
2844
+ meta: {
2845
+ isDropDownCell: {
2846
+ isMulti: true,
2847
+ onChange: (newValue, cell) => {
2848
+ updateRow(cell.row.original.name, { labels: newValue });
2849
+ },
2850
+ },
2851
+ },
2852
+ minSize: 240,
2853
+ size: 280,
2854
+ }),
2855
+ // Single-select dropdown in an error state.
2856
+ columnHelper.accessor('status', {
2857
+ cell: (info) => {
2858
+ const isInvalid = !VALID_STATUSES.includes(info.getValue());
2859
+
2860
+ const isPending = info.getValue() === 'pending';
2861
+ const isInactive = info.getValue() === 'inactive';
2862
+
2863
+ const warningHelpText = (
2864
+ <span className="n-flex n-items-center n-gap-token-2 n-text-warning-text">
2865
+ <ExclamationTriangleIconSolid className="n-size-token-16 n-text-warning-icon" />
2866
+ Inactive users cannot sign in
2867
+ </span>
2868
+ );
2869
+
2870
+ return (
2871
+ <DataGrid.DropDownCell
2872
+ cell={info}
2873
+ options={STATUS_OPTIONS}
2874
+ ariaLabel="select status"
2875
+ errorText={isInvalid ? 'Please select a valid status' : undefined}
2876
+ helpText={
2877
+ isInactive
2878
+ ? warningHelpText
2879
+ : isPending
2880
+ ? 'Waiting for admin approval'
2881
+ : undefined
2882
+ }
2883
+ />
2884
+ );
2885
+ },
2886
+ meta: {
2887
+ isDropDownCell: {
2888
+ onChange: (newValue, cell) => {
2889
+ if (newValue === undefined) {
2890
+ return;
2891
+ }
2892
+ updateRow(cell.row.original.name, { status: newValue });
2893
+ },
2894
+ },
2895
+ },
2896
+ minSize: 200,
2897
+ size: 240,
2898
+ }),
2899
+ ];
2900
+ const table = useReactTable({
2901
+ autoResetPageIndex: false,
2902
+ columnResizeMode: 'onChange',
2903
+ columns: COMMON_COLUMNS,
2904
+ data: data,
2905
+ defaultColumn: {
2906
+ maxSize: 800,
2907
+ minSize: 80,
2908
+ },
2909
+ enableSorting: true,
2910
+ getCoreRowModel: getCoreRowModel(),
2911
+ getPaginationRowModel: getPaginationRowModel(),
2912
+ getSortedRowModel: getSortedRowModel(),
2913
+ initialState: {
2914
+ pagination: {
2915
+ pageSize: 5,
2916
+ },
2917
+ },
2918
+ });
2919
+
2920
+ return (
2921
+ <div className="n-w-full n-bg-light-neutral-text-weakest">
2922
+ <DataGrid
2923
+ isKeyboardNavigable={true}
2924
+ isResizable={true}
2925
+ styling={{
2926
+ borderStyle: 'all-sides',
2927
+ hasZebraStriping: false,
2928
+ }}
2929
+ tableInstance={table}
2930
+ />
2931
+ </div>
2932
+ );
2933
+ };
2934
+
2935
+ export default Component;
2936
+ ```
2937
+
2938
+ ### Datagrid Virtualization
2939
+
2940
+ ```tsx
2941
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
2942
+
2943
+ import { DataGrid } from '@neo4j-ndl/react';
2944
+ import {
2945
+ createColumnHelper,
2946
+ getCoreRowModel,
2947
+ getPaginationRowModel,
2948
+ getSortedRowModel,
2949
+ type Table as DataGridType,
2950
+ useReactTable,
2951
+ } from '@tanstack/react-table';
2952
+ import { useVirtualizer, type Virtualizer } from '@tanstack/react-virtual';
2953
+ import { type RefObject, useMemo, useRef } from 'react';
2954
+
2955
+ import { type CommonProps } from '../../_common/types';
2956
+
2957
+ type TestDataFormat = {
2958
+ name: string;
2959
+ age: number;
2960
+ cypherCommand: string;
2961
+ };
2962
+
2963
+ const TABLE_DEMO_DATA: TestDataFormat[] = Array.from(
2964
+ { length: 1000 },
2965
+ (_, i) => ({
2966
+ age: Math.floor(Math.random() * 100),
2967
+ cypherCommand: `MATCH (n) RETURN n LIMIT ${i + 1}`,
2968
+ name: `Person ${i + 1}`,
2969
+ }),
2970
+ );
2971
+
2972
+ const BodyCurryingFunction =
2973
+ (
2974
+ rowVirtualizer: Virtualizer<HTMLDivElement, Element>,
2975
+ ref: RefObject<HTMLDivElement | null>,
2976
+ table: DataGridType<TestDataFormat>,
2977
+ ) =>
2978
+ // eslint-disable-next-line react/display-name
2979
+ ({
2980
+ style,
2981
+ className,
2982
+ ...props
2983
+ }: CommonProps<'div', { children?: React.ReactNode }>) => {
2984
+ const { rows } = table.getSortedRowModel();
2985
+ rowVirtualizer.setOptions({
2986
+ ...rowVirtualizer.options,
2987
+ count: rows.length,
2988
+ });
2989
+ const items = rowVirtualizer.getVirtualItems();
2990
+
2991
+ return (
2992
+ <div ref={ref} className="n-overflow-auto n-h-[290px]">
2993
+ <DataGrid.Body
2994
+ {...props}
2995
+ className={className}
2996
+ style={{
2997
+ ...style,
2998
+ height: `${rowVirtualizer.getTotalSize()}px`,
2999
+ position: 'relative',
3000
+ }}
3001
+ >
3002
+ {items.map((row: { index: number; size: number; start: number }) => {
3003
+ const foundRow = rows[row.index];
3004
+ if (foundRow === undefined) {
3005
+ return null;
3006
+ }
3007
+
3008
+ return (
3009
+ <DataGrid.BodyRow
3010
+ key={`table-row-${row.index}`}
3011
+ row={foundRow}
3012
+ style={{
3013
+ height: `${row.size}px`,
3014
+ left: 0,
3015
+ position: 'absolute',
3016
+ top: 0,
3017
+ transform: `translateY(${row.start}px)`,
3018
+ width: '100%',
3019
+ }}
3020
+ />
3021
+ );
3022
+ })}
3023
+ </DataGrid.Body>
3024
+ </div>
3025
+ );
3026
+ };
3027
+
3028
+ /** Repeats array `n` times */
3029
+ const repeat = (arr: unknown[], n: number) => Array(n).fill(arr).flat();
3030
+
3031
+ const columnHelper = createColumnHelper<TestDataFormat>();
3032
+
3033
+ const columns = [
3034
+ columnHelper.accessor('name', {
3035
+ cell: (info) => info.getValue(),
3036
+ footer: (info) => info.column.id,
3037
+ header: () => 'Name',
3038
+ maxSize: 300,
3039
+ minSize: 180,
3040
+ size: 220,
3041
+ }),
3042
+ columnHelper.accessor('age', {
3043
+ cell: (info) => info.renderValue(),
3044
+ footer: (info) => info.column.id,
3045
+ header: () => 'Age',
3046
+ minSize: 80,
3047
+ }),
3048
+ columnHelper.accessor('cypherCommand', {
3049
+ cell: (info) => info.getValue(),
3050
+ footer: (info) => info.column.id,
3051
+ header: () => 'Cypher Command',
3052
+ minSize: 400,
3053
+ size: 400,
3054
+ }),
3055
+ ];
3056
+
3057
+ const ExtendedProps = {
3058
+ columnResizeMode: 'onChange' as const,
3059
+ columns,
3060
+ data: [...repeat(TABLE_DEMO_DATA, 500)],
3061
+ debugColumns: true,
3062
+ debugHeaders: true,
3063
+ debugTable: true,
3064
+ defaultColumn: {
3065
+ maxSize: 800,
3066
+ minSize: 80,
3067
+ },
3068
+ enableSorting: false,
3069
+ getCoreRowModel: getCoreRowModel(),
3070
+ getPaginationRowModel: getPaginationRowModel(),
3071
+ };
3072
+
3073
+ const Component = () => {
3074
+ const scrollingParentRef = useRef<HTMLDivElement | null>(null);
3075
+
3076
+ const rowVirtualizer = useVirtualizer({
3077
+ count: ExtendedProps.data.length,
3078
+ estimateSize: () => 40,
3079
+ getScrollElement: () => scrollingParentRef.current,
3080
+ // 40 is the size for a basic data type row so fallback to that
3081
+ overscan: 15,
3082
+ });
3083
+
3084
+ const table = useReactTable({
3085
+ ...ExtendedProps,
3086
+ enableSorting: true,
3087
+ getSortedRowModel: getSortedRowModel(),
3088
+ initialState: {
3089
+ pagination: {
3090
+ pageSize: ExtendedProps.data.length,
3091
+ },
3092
+ },
3093
+ });
3094
+
3095
+ const CustomBody = useMemo(() => {
3096
+ return BodyCurryingFunction(rowVirtualizer, scrollingParentRef, table);
3097
+ }, [rowVirtualizer, scrollingParentRef, table]);
3098
+
3099
+ return (
3100
+ <div
3101
+ className="n-w-full n-bg-light-neutral-text-weakest"
3102
+ style={{ height: '400px' }}
3103
+ >
3104
+ <DataGrid
3105
+ components={{
3106
+ Body: CustomBody,
3107
+ PaginationNumericButton: null,
3108
+ RowsPerPage: null,
3109
+ }}
3110
+ isResizable={false}
3111
+ tableInstance={table}
3112
+ rootProps={{
3113
+ className: 'n-h-full',
3114
+ }}
3115
+ />
3116
+ </div>
3117
+ );
3118
+ };
3119
+
3120
+ export default Component;
3121
+ ```