@commercetools/nimbus-mcp 3.4.0 → 3.5.1

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 (170) hide show
  1. package/data/docs/routes/components-data-display-data-table.json +172 -14
  2. package/data/docs/routes/home-getting-started-installation.json +2 -2
  3. package/data/docs/types/AccordionContent.json +27 -27
  4. package/data/docs/types/AccordionHeader.json +89 -89
  5. package/data/docs/types/AccordionItem.json +22 -22
  6. package/data/docs/types/AccordionRoot.json +14 -14
  7. package/data/docs/types/ActivityIndicator.json +2 -2
  8. package/data/docs/types/AlertDescription.json +8 -8
  9. package/data/docs/types/AlertDismissButton.json +80 -80
  10. package/data/docs/types/AlertTitle.json +8 -8
  11. package/data/docs/types/Avatar.json +2 -2
  12. package/data/docs/types/Badge.json +3 -3
  13. package/data/docs/types/Body.json +6 -6
  14. package/data/docs/types/Box.json +6 -6
  15. package/data/docs/types/BreadcrumbsItem.json +18 -18
  16. package/data/docs/types/BreadcrumbsRoot.json +2 -2
  17. package/data/docs/types/Button.json +88 -88
  18. package/data/docs/types/Calendar.json +56 -56
  19. package/data/docs/types/Caption.json +6 -6
  20. package/data/docs/types/CardRoot.json +2 -2
  21. package/data/docs/types/Cell.json +20 -20
  22. package/data/docs/types/ChatMessageAvatar.json +2 -2
  23. package/data/docs/types/ChatMessageListRoot.json +2 -2
  24. package/data/docs/types/ChatMessageRoot.json +2 -2
  25. package/data/docs/types/Checkbox.json +88 -88
  26. package/data/docs/types/Code.json +10 -10
  27. package/data/docs/types/CollapsibleMotionContent.json +2 -2
  28. package/data/docs/types/CollapsibleMotionRoot.json +2 -2
  29. package/data/docs/types/CollapsibleMotionTrigger.json +4 -4
  30. package/data/docs/types/Column.json +8 -8
  31. package/data/docs/types/ColumnGroup.json +8 -8
  32. package/data/docs/types/ColumnHeader.json +18 -18
  33. package/data/docs/types/ComboBoxListBox.json +70 -70
  34. package/data/docs/types/ComboBoxOption.json +64 -64
  35. package/data/docs/types/ComboBoxPopover.json +148 -72
  36. package/data/docs/types/ComboBoxRoot.json +8 -8
  37. package/data/docs/types/ComboBoxSection.json +25 -25
  38. package/data/docs/types/ComboBoxTrigger.json +6 -6
  39. package/data/docs/types/Content.json +2 -2
  40. package/data/docs/types/DataTable.json +38 -23
  41. package/data/docs/types/DataTableBody.json +60 -79
  42. package/data/docs/types/DataTableCell.json +434 -0
  43. package/data/docs/types/DataTableColumn.json +540 -0
  44. package/data/docs/types/DataTableHeader.json +62 -85
  45. package/data/docs/types/DataTableRoot.json +38 -23
  46. package/data/docs/types/DataTableRow.json +790 -0
  47. package/data/docs/types/DataTableTable.json +20 -20
  48. package/data/docs/types/DateInput.json +72 -72
  49. package/data/docs/types/DatePicker.json +58 -58
  50. package/data/docs/types/DateRangePicker.json +86 -86
  51. package/data/docs/types/DateRangePickerField.json +86 -86
  52. package/data/docs/types/DefaultPageBackLink.json +16 -16
  53. package/data/docs/types/DefaultPageRoot.json +2 -2
  54. package/data/docs/types/DialogCloseTrigger.json +78 -78
  55. package/data/docs/types/DialogTrigger.json +2 -2
  56. package/data/docs/types/DraggableListField.json +62 -62
  57. package/data/docs/types/DraggableListItem.json +100 -53
  58. package/data/docs/types/DraggableListRoot.json +62 -62
  59. package/data/docs/types/DrawerCloseTrigger.json +78 -78
  60. package/data/docs/types/DrawerTrigger.json +2 -2
  61. package/data/docs/types/DropZone.json +14 -14
  62. package/data/docs/types/FieldErrors.json +2 -2
  63. package/data/docs/types/Flex.json +22 -22
  64. package/data/docs/types/FloatingActionButton.json +88 -88
  65. package/data/docs/types/Footer.json +6 -6
  66. package/data/docs/types/FormFieldRoot.json +2 -2
  67. package/data/docs/types/Grid.json +24 -24
  68. package/data/docs/types/Group.json +12 -12
  69. package/data/docs/types/Header.json +6 -6
  70. package/data/docs/types/Heading.json +8 -8
  71. package/data/docs/types/Icon.json +4 -4
  72. package/data/docs/types/IconButton.json +88 -88
  73. package/data/docs/types/IconToggleButton.json +72 -72
  74. package/data/docs/types/Image.json +30 -30
  75. package/data/docs/types/Indicator.json +6 -6
  76. package/data/docs/types/InlineSvg.json +2 -2
  77. package/data/docs/types/Item.json +6 -6
  78. package/data/docs/types/ItemRoot.json +16 -16
  79. package/data/docs/types/Kbd.json +8 -8
  80. package/data/docs/types/Link.json +30 -30
  81. package/data/docs/types/ListIndicator.json +6 -6
  82. package/data/docs/types/ListItem.json +6 -6
  83. package/data/docs/types/ListRoot.json +10 -10
  84. package/data/docs/types/LoadingSpinner.json +2 -2
  85. package/data/docs/types/MakeElementFocusable.json +18 -18
  86. package/data/docs/types/Markdown.json +6 -6
  87. package/data/docs/types/MenuItem.json +63 -63
  88. package/data/docs/types/MenuRoot.json +56 -56
  89. package/data/docs/types/MenuSection.json +31 -31
  90. package/data/docs/types/MenuSubmenuTrigger.json +4 -4
  91. package/data/docs/types/MenuTrigger.json +89 -89
  92. package/data/docs/types/MultilineTextInput.json +104 -104
  93. package/data/docs/types/MultilineTextInputField.json +102 -102
  94. package/data/docs/types/NumberInput.json +86 -86
  95. package/data/docs/types/NumberInputField.json +82 -82
  96. package/data/docs/types/PageContentColumn.json +6 -6
  97. package/data/docs/types/PageContentRoot.json +6 -6
  98. package/data/docs/types/PasswordInput.json +100 -100
  99. package/data/docs/types/PasswordInputField.json +100 -100
  100. package/data/docs/types/ProgressBar.json +13 -13
  101. package/data/docs/types/RadioInputOption.json +56 -56
  102. package/data/docs/types/RadioInputRoot.json +48 -48
  103. package/data/docs/types/RangeCalendar.json +58 -58
  104. package/data/docs/types/RangeSlider.json +18 -18
  105. package/data/docs/types/Region.json +6 -6
  106. package/data/docs/types/RegionTarget.json +6 -6
  107. package/data/docs/types/RichTextInput.json +2 -2
  108. package/data/docs/types/Root.json +10 -10
  109. package/data/docs/types/Row.json +6 -6
  110. package/data/docs/types/ScrollArea.json +2 -2
  111. package/data/docs/types/SearchInput.json +106 -106
  112. package/data/docs/types/SearchInputField.json +102 -102
  113. package/data/docs/types/SelectOption.json +54 -54
  114. package/data/docs/types/SelectOptionGroup.json +18 -18
  115. package/data/docs/types/SelectOptions.json +64 -64
  116. package/data/docs/types/SelectRoot.json +92 -92
  117. package/data/docs/types/Separator.json +4 -4
  118. package/data/docs/types/SimpleGrid.json +28 -28
  119. package/data/docs/types/Skeleton.json +2 -2
  120. package/data/docs/types/SkeletonCircle.json +2 -2
  121. package/data/docs/types/Slider.json +18 -18
  122. package/data/docs/types/SplitButton.json +10 -10
  123. package/data/docs/types/Stack.json +2 -2
  124. package/data/docs/types/StepsNextTrigger.json +2 -2
  125. package/data/docs/types/StepsPrevTrigger.json +2 -2
  126. package/data/docs/types/StepsRoot.json +2 -2
  127. package/data/docs/types/StepsTrigger.json +2 -2
  128. package/data/docs/types/Switch.json +36 -36
  129. package/data/docs/types/TabNavItem.json +18 -18
  130. package/data/docs/types/TabNavRoot.json +2 -2
  131. package/data/docs/types/TableBody.json +6 -6
  132. package/data/docs/types/TableCaption.json +6 -6
  133. package/data/docs/types/TableCell.json +20 -20
  134. package/data/docs/types/TableColumn.json +8 -8
  135. package/data/docs/types/TableColumnGroup.json +8 -8
  136. package/data/docs/types/TableColumnHeader.json +18 -18
  137. package/data/docs/types/TableFooter.json +6 -6
  138. package/data/docs/types/TableHeader.json +6 -6
  139. package/data/docs/types/TableRoot.json +32 -32
  140. package/data/docs/types/TableRow.json +6 -6
  141. package/data/docs/types/TableScrollArea.json +6 -6
  142. package/data/docs/types/TabsTab.json +2 -2
  143. package/data/docs/types/TagGroupRoot.json +24 -24
  144. package/data/docs/types/TagGroupTag.json +56 -56
  145. package/data/docs/types/TagGroupTagList.json +15 -15
  146. package/data/docs/types/Text.json +8 -8
  147. package/data/docs/types/TextInput.json +102 -102
  148. package/data/docs/types/TextInputField.json +100 -100
  149. package/data/docs/types/TimeInput.json +68 -68
  150. package/data/docs/types/ToggleButton.json +74 -74
  151. package/data/docs/types/ToggleButtonGroupButton.json +32 -32
  152. package/data/docs/types/ToggleButtonGroupRoot.json +20 -20
  153. package/data/docs/types/Toolbar.json +9 -9
  154. package/data/docs/types/TooltipContent.json +28 -28
  155. package/data/docs/types/TooltipRoot.json +16 -16
  156. package/data/docs/types/TreeIndicator.json +82 -82
  157. package/data/docs/types/TreeItem.json +100 -51
  158. package/data/docs/types/TreeRoot.json +57 -57
  159. package/data/docs/types/Trigger.json +4 -4
  160. package/data/docs/types/VisuallyHidden.json +6 -6
  161. package/data/docs/types/createItemsFromCsvDrop.json +4 -4
  162. package/data/docs/types/createItemsFromDirectoryDrop.json +4 -4
  163. package/data/docs/types/createItemsFromFileDrop.json +4 -4
  164. package/data/docs/types/createItemsFromImageDrop.json +4 -4
  165. package/data/docs/types/createItemsFromJsonDrop.json +4 -4
  166. package/data/docs/types/manifest.json +3 -0
  167. package/data/docs/types/useRegion.json +2 -2
  168. package/data/docs/types/useTree.json +24 -24
  169. package/data/tokens.json +42 -0
  170. package/package.json +7 -7
@@ -189,7 +189,7 @@
189
189
  ]
190
190
  },
191
191
  "dev": {
192
- "mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport {\n DataTable,\n type DataTableProps,\n type DataTableColumnItem,\n type DataTableRowItem,\n type SortDescriptor,\n} from \"@commercetools/nimbus\";\n```\n\n### Basic usage\n\nThe DataTable renders data in a tabular format with support for sorting,\nselection, resizing, and nested content. At minimum, you need to provide\n`columns` and `rows`:\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'email', header: 'Email', accessor: (row) => row.email },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n ];\n\n const rows = [\n { id: '1', name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin' },\n { id: '2', name: 'Bob Smith', email: 'bob@example.com', role: 'User' },\n { id: '3', name: 'Carol Williams', email: 'carol@example.com', role: 'Editor' },\n ];\n\n return <DataTable columns={columns} rows={rows} />;\n}\n```\n\n## When to use DataTable\n\nUse the **DataTable** component when you need **interactive data management features** like sorting, filtering, selection, or pagination. It's built for complex, data-intensive interfaces.\n\n### Keep it simple\n\n**Don't use DataTable for static content.** If you only need to display read-only data without interactive features, use the simpler [Table component](/components/data-display/table) instead. It's lighter, faster, and easier to implement.\n\n**Use the Table component when you:**\n- Display static reference data (specifications, comparisons, pricing)\n- Have small datasets (between 10 - 20 rows)\n- Don't need sorting, filtering, or selection\n- Want a straightforward implementation without state management\n\n**Use DataTable when you need:**\n- Sorting columns in ascending or descending order\n- Row selection for bulk actions\n- Column management (show/hide, reorder, resize)\n- Search and filtering across data\n- Pagination for large datasets (> 50 rows)\n- Server-side data operations\n- Nested/hierarchical data with expansion\n- Dense data visualization with configurable row spacing\n\n**Example decision:**\n\n```tsx\n// ✗ Avoid - Overkill for static data\n<DataTable\n columns={threeStaticColumns}\n rows={fiveStaticRows}\n/>\n\n// ✓ Better - Use Table for static content\n<Table.Root>\n <Table.Header>{/* specification headers */}</Table.Header>\n <Table.Body>{/* specification rows */}</Table.Body>\n</Table.Root>\n```\n\n→ **[See Table component documentation](/components/data-display/table)** for simple table layouts.\n\n## Working with columns and rows\n\n### Column configuration\n\nEach column requires an `id`, `header`, and `accessor` function. Additional\noptions control sorting, resizing, and custom rendering:\n\n```tsx\nimport type { DataTableColumnItem } from \"@commercetools/nimbus\";\n\nconst columns: DataTableColumnItem[] = [\n {\n id: \"name\",\n header: \"Product Name\",\n accessor: (row) => row.name,\n isSortable: true, // Enable sorting for this column\n isResizable: true, // Enable column resizing\n isRowHeader: true, // Mark as row header for accessibility\n width: 200, // Initial width in pixels\n minWidth: 100, // Minimum width when resizing\n maxWidth: 400, // Maximum width when resizing\n },\n {\n id: \"status\",\n header: \"Status\",\n accessor: (row) => row.status,\n // Custom rendering with the render prop\n render: ({ value }) => (\n <Badge colorPalette={value === \"Active\" ? \"positive\" : \"neutral\"}>\n {value}\n </Badge>\n ),\n },\n {\n id: \"actions\",\n header: \"Actions\",\n accessor: (row) => row.id,\n headerIcon: (\n <IconButton aria-label=\"Info\" size=\"2xs\" variant=\"ghost\">\n <Info />\n </IconButton>\n ),\n isSortable: false,\n },\n];\n```\n\n### Row data structure\n\nEach row must have a unique `id` property. Additional properties are accessed\nvia column `accessor` functions:\n\n```tsx\nimport type { DataTableRowItem } from \"@commercetools/nimbus\";\n\nconst rows: DataTableRowItem[] = [\n { id: \"1\", name: \"Product A\", status: \"Active\", price: 99.99 },\n {\n id: \"2\",\n name: \"Product B\",\n status: \"Draft\",\n price: 149.99,\n isDisabled: true,\n },\n { id: \"3\", name: \"Product C\", status: \"Active\", price: 79.99 },\n];\n```\n\n## Usage examples\n\n### Density options\n\nThe `density` prop controls row height and padding. Use `condensed` for\ndata-dense interfaces:\n\n```jsx live-dev\nconst App = () => {\n const [density, setDensity] = useState('default');\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n ];\n\n const rows = [\n { id: '1', name: 'Alice', role: 'Admin' },\n { id: '2', name: 'Bob', role: 'User' },\n { id: '3', name: 'Carol', role: 'Editor' },\n ];\n\n return (\n <Stack gap=\"400\">\n <Stack direction=\"row\" gap=\"300\">\n <Button\n variant={density === 'default' ? 'solid' : 'outline'}\n colorPalette=\"primary\"\n onPress={() => setDensity('default')}\n >\n Default\n </Button>\n <Button\n variant={density === 'condensed' ? 'solid' : 'outline'}\n colorPalette=\"primary\"\n onPress={() => setDensity('condensed')}\n >\n Condensed\n </Button>\n </Stack>\n <DataTable columns={columns} rows={rows} density={density} />\n </Stack>\n );\n}\n```\n\n### Sorting\n\nEnable sorting with `allowsSorting`. Individual columns can opt-out using\n`isSortable: false`:\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name, isSortable: true },\n { id: 'age', header: 'Age', accessor: (row) => row.age, isSortable: true },\n { id: 'role', header: 'Role', accessor: (row) => row.role, isSortable: false },\n ];\n\n const rows = [\n { id: '1', name: 'Alice', age: 30, role: 'Admin' },\n { id: '2', name: 'Bob', age: 25, role: 'User' },\n { id: '3', name: 'Carol', age: 28, role: 'Editor' },\n ];\n\n return (\n <Stack gap=\"300\">\n <Text>Click column headers to sort. The \"Role\" column is not sortable.</Text>\n <DataTable\n columns={columns}\n rows={rows}\n allowsSorting\n defaultSortDescriptor={{ column: 'name', direction: 'ascending' }}\n />\n </Stack>\n );\n}\n```\n\n### Controlled sorting\n\nFor external sort state management, use `sortDescriptor` and `onSortChange`:\n\n```jsx live-dev\nconst App = () => {\n const [sortDescriptor, setSortDescriptor] = useState({\n column: 'name',\n direction: 'ascending',\n });\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name, isSortable: true },\n { id: 'age', header: 'Age', accessor: (row) => row.age, isSortable: true },\n ];\n\n const rows = [\n { id: '1', name: 'Alice', age: 30 },\n { id: '2', name: 'Bob', age: 25 },\n { id: '3', name: 'Carol', age: 28 },\n ];\n\n return (\n <Stack gap=\"400\">\n <Text>\n Sorting by: <Text as=\"strong\">{sortDescriptor.column}</Text> ({sortDescriptor.direction})\n </Text>\n <Stack direction=\"row\" gap=\"300\">\n <Button\n variant=\"outline\"\n size=\"md\"\n colorPalette=\"primary\"\n onPress={() => setSortDescriptor({ column: 'name', direction: 'ascending' })}\n >\n Sort by Name (A-Z)\n </Button>\n <Button\n variant=\"outline\"\n size=\"md\"\n colorPalette=\"primary\"\n onPress={() => setSortDescriptor({ column: 'age', direction: 'descending' })}\n >\n Sort by Age (High-Low)\n </Button>\n </Stack>\n <DataTable\n columns={columns}\n rows={rows}\n allowsSorting\n sortDescriptor={sortDescriptor}\n onSortChange={setSortDescriptor}\n />\n </Stack>\n );\n}\n```\n\n### Row selection\n\nEnable selection with `selectionMode`. Supports `none`, `single`, or `multiple`:\n\n```jsx live-dev\nconst App = () => {\n const [selectedKeys, setSelectedKeys] = useState(new Set(['1']));\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'email', header: 'Email', accessor: (row) => row.email },\n ];\n\n const rows = [\n { id: '1', name: 'Alice', email: 'alice@example.com' },\n { id: '2', name: 'Bob', email: 'bob@example.com' },\n { id: '3', name: 'Carol', email: 'carol@example.com' },\n ];\n\n const selectedCount = selectedKeys === 'all' ? rows.length : selectedKeys.size;\n\n return (\n <Stack gap=\"400\">\n <Text>Selected: {selectedCount} row(s)</Text>\n <Stack direction=\"row\" gap=\"300\">\n <Button\n variant=\"outline\"\n colorPalette=\"primary\"\n size=\"md\"\n onPress={() => setSelectedKeys(new Set())}>\n Clear\n </Button>\n <Button size=\"md\" colorPalette=\"primary\" variant=\"outline\" onPress={() => setSelectedKeys('all')}>\n Select All\n </Button>\n </Stack>\n <DataTable\n columns={columns}\n rows={rows}\n selectionMode=\"multiple\"\n selectedKeys={selectedKeys}\n onSelectionChange={setSelectedKeys}\n />\n </Stack>\n );\n}\n```\n\n### Resizable columns\n\nEnable column resizing with `isResizable` on the table or individual columns:\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name, isResizable: true, width: 150 },\n { id: 'description', header: 'Description', accessor: (row) => row.description, isResizable: true },\n { id: 'status', header: 'Status', accessor: (row) => row.status, isResizable: false },\n ];\n\n const rows = [\n { id: '1', name: 'Product A', description: 'A great product for everyday use', status: 'Active' },\n { id: '2', name: 'Product B', description: 'Premium quality item', status: 'Draft' },\n ];\n\n return (\n <Stack gap=\"300\">\n <Text>Drag column borders to resize. The \"Status\" column is fixed width.</Text>\n <DataTable columns={columns} rows={rows} isResizable />\n </Stack>\n );\n}\n```\n\n### Sticky header with scrolling\n\nUse `maxHeight` to enable vertical scrolling with a sticky header:\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n ];\n\n const rows = Array.from({ length: 20 }, (_, i) => ({\n id: String(i + 1),\n name: `User ${i + 1}`,\n role: i % 3 === 0 ? 'Admin' : i % 3 === 1 ? 'Editor' : 'Viewer',\n }));\n\n return (\n <DataTable\n columns={columns}\n rows={rows}\n maxHeight=\"300px\"\n />\n );\n}\n```\n\n### Text truncation\n\nEnable `isTruncated` to truncate long text with ellipsis:\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'description', header: 'Description', accessor: (row) => row.description },\n ];\n\n const rows = [\n {\n id: '1',\n name: 'Product A',\n description: 'This is a very long description that should be truncated when displayed in the table cell to prevent layout issues.',\n },\n {\n id: '2',\n name: 'Product B',\n description: 'Another lengthy description that demonstrates how truncation works with ellipsis and maintains a clean table layout.',\n },\n ];\n\n return <DataTable columns={columns} rows={rows} isTruncated />;\n}\n```\n\n### Clickable rows\n\nUse `onRowClick` to handle row click events for navigation or detail views:\n\n```jsx live-dev\nconst App = () => {\n const [clickedRow, setClickedRow] = useState(null);\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'email', header: 'Email', accessor: (row) => row.email },\n ];\n\n const rows = [\n { id: '1', name: 'Alice', email: 'alice@example.com' },\n { id: '2', name: 'Bob', email: 'bob@example.com' },\n { id: '3', name: 'Carol', email: 'carol@example.com' },\n ];\n\n return (\n <Stack gap=\"400\">\n <Text>\n {clickedRow ? `Clicked: ${clickedRow.name}` : 'Click a row to see details'}\n </Text>\n <DataTable\n columns={columns}\n rows={rows}\n onRowClick={(row) => setClickedRow(row)}\n />\n </Stack>\n );\n}\n```\n\n### Search and filtering\n\nUse the `search` prop to filter rows across all visible columns:\n\n```jsx live-dev\nconst App = () => {\n const [search, setSearch] = useState('');\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n { id: 'department', header: 'Department', accessor: (row) => row.department },\n ];\n\n const rows = [\n { id: '1', name: 'Alice Johnson', role: 'Admin', department: 'Engineering' },\n { id: '2', name: 'Bob Smith', role: 'Developer', department: 'Engineering' },\n { id: '3', name: 'Carol Williams', role: 'Designer', department: 'Design' },\n { id: '4', name: 'David Brown', role: 'Manager', department: 'Sales' },\n ];\n\n return (\n <Stack gap=\"400\">\n <TextInput\n value={search}\n onChange={setSearch}\n placeholder=\"Search...\"\n width=\"300px\"\n aria-label=\"Search table\"\n />\n <DataTable columns={columns} rows={rows} search={search} />\n </Stack>\n );\n}\n```\n\n### Column visibility\n\nControl which columns are displayed using `visibleColumns`:\n\n```jsx live-dev\nconst App = () => {\n const [visibleColumns, setVisibleColumns] = useState(['name', 'email']);\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'email', header: 'Email', accessor: (row) => row.email },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n { id: 'department', header: 'Department', accessor: (row) => row.department },\n ];\n\n const rows = [\n { id: '1', name: 'Alice', email: 'alice@example.com', role: 'Admin', department: 'Engineering' },\n { id: '2', name: 'Bob', email: 'bob@example.com', role: 'User', department: 'Sales' },\n ];\n\n const toggleColumn = (colId) => {\n setVisibleColumns((prev) =>\n prev.includes(colId) ? prev.filter((id) => id !== colId) : [...prev, colId]\n );\n };\n\n return (\n <Stack gap=\"400\">\n <Stack direction=\"row\" gap=\"300\" wrap=\"wrap\">\n {columns.map((col) => (\n <Checkbox\n key={col.id}\n isSelected={visibleColumns.includes(col.id)}\n onChange={() => toggleColumn(col.id)}\n >\n {col.header}\n </Checkbox>\n ))}\n </Stack>\n <DataTable columns={columns} rows={rows} visibleColumns={visibleColumns} />\n </Stack>\n );\n}\n```\n\n### Nested rows\n\nUse `nestedKey` to enable expandable nested content:\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Category', accessor: (row) => row.name },\n { id: 'count', header: 'Products', accessor: (row) => row.count },\n ];\n\n const rows = [\n {\n id: '1',\n name: 'Electronics',\n count: 3,\n children: [\n { id: '1-1', name: 'Laptops', count: 15 },\n { id: '1-2', name: 'Phones', count: 25 },\n { id: '1-3', name: 'Tablets', count: 10 },\n ],\n },\n {\n id: '2',\n name: 'Clothing',\n count: 2,\n children: [\n { id: '2-1', name: 'Shirts', count: 50 },\n { id: '2-2', name: 'Pants', count: 30 },\n ],\n },\n { id: '3', name: 'Books', count: 0 },\n ];\n\n return (\n <Stack gap=\"300\">\n <Text>Click the expand button to view nested items.</Text>\n <DataTable columns={columns} rows={rows} nestedKey=\"children\" />\n </Stack>\n );\n}\n```\n\n### With footer\n\nAdd pagination or summary content using the `footer` prop:\n\n```jsx live-dev\nconst App = () => {\n const [page, setPage] = useState(1);\n const pageSize = 3;\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n ];\n\n const allRows = Array.from({ length: 10 }, (_, i) => ({\n id: String(i + 1),\n name: `User ${i + 1}`,\n role: i % 2 === 0 ? 'Admin' : 'User',\n }));\n\n const totalPages = Math.ceil(allRows.length / pageSize);\n const rows = allRows.slice((page - 1) * pageSize, page * pageSize);\n\n return (\n <DataTable\n columns={columns}\n rows={rows}\n footer={\n <Stack direction=\"row\" justify=\"space-between\" align=\"center\" width=\"full\" px=\"400\" py=\"300\">\n <Text fontSize=\"sm\">\n Showing {(page - 1) * pageSize + 1}-{Math.min(page * pageSize, allRows.length)} of {allRows.length}\n </Text>\n <Stack direction=\"row\" gap=\"200\">\n <Button\n size=\"md\"\n colorPalette=\"primary\"\n variant=\"outline\"\n onPress={() => setPage((p) => Math.max(1, p - 1))}\n isDisabled={page === 1}\n >\n Previous\n </Button>\n <Button\n size=\"md\"\n variant=\"outline\"\n colorPalette=\"primary\"\n onPress={() => setPage((p) => Math.min(totalPages, p + 1))}\n isDisabled={page === totalPages}\n >\n Next\n </Button>\n </Stack>\n </Stack>\n }\n />\n );\n}\n```\n\n### Disabled rows\n\nUse `disabledKeys` to prevent interaction with specific rows:\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'status', header: 'Status', accessor: (row) => row.status },\n ];\n\n const rows = [\n { id: '1', name: 'Active Item', status: 'Available' },\n { id: '2', name: 'Disabled Item', status: 'Unavailable' },\n { id: '3', name: 'Another Active', status: 'Available' },\n ];\n\n return (\n <DataTable\n columns={columns}\n rows={rows}\n selectionMode=\"multiple\"\n disabledKeys={new Set(['2'])}\n />\n );\n}\n```\n\n### Row pinning\n\nPin important rows to the top of the table:\n\n```jsx live-dev\nconst App = () => {\n const [pinnedRows, setPinnedRows] = useState(new Set(['1']));\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'priority', header: 'Priority', accessor: (row) => row.priority },\n ];\n\n const rows = [\n { id: '1', name: 'High Priority Task', priority: 'High' },\n { id: '2', name: 'Normal Task', priority: 'Normal' },\n { id: '3', name: 'Low Priority Task', priority: 'Low' },\n ];\n\n return (\n <Stack gap=\"400\">\n <Text>Row 1 is pinned to the top. Pinned rows stay at top when sorting.</Text>\n <DataTable\n columns={columns}\n rows={rows}\n pinnedRows={pinnedRows}\n onPinToggle={(rowId) => {\n setPinnedRows((prev) => {\n const next = new Set(prev);\n if (next.has(rowId)) {\n next.delete(rowId);\n } else {\n next.add(rowId);\n }\n return next;\n });\n }}\n allowsSorting\n />\n </Stack>\n );\n}\n```\n\n## Drag and drop\n\nDataTable supports row reordering via drag and drop using the shared `useDragAndDrop` hook. Pass `dragAndDropHooks` to `DataTable` to enable it.\n\n### Reorderable rows\n\n```jsx live-dev\nimport { DataTable, useDragAndDrop, createArrayHandlers } from '@commercetools/nimbus';\n\nconst App = () => {\n const [rows, setRows] = useState([\n { id: '1', name: 'Alice', role: 'Admin' },\n { id: '2', name: 'Bob', role: 'User' },\n { id: '3', name: 'Carol', role: 'Manager' },\n ]);\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n ];\n\n const { dragAndDropHooks } = useDragAndDrop({\n ...createArrayHandlers(setRows, (row) => row.id),\n });\n\n return (\n <DataTable\n columns={columns}\n rows={rows}\n dragAndDropHooks={dragAndDropHooks}\n />\n );\n}\n```\n\nWhen `dragAndDropHooks` is provided:\n- A **drag handle column** with a grip icon is automatically added as the first column\n- Rows can be dragged using mouse or touch from anywhere on the row\n- **Keyboard accessible**: focus the drag handle, press Enter to start drag, use arrow keys to move, Enter to drop, Escape to cancel\n- A **drop indicator** shows where the row will be placed\n- **Double-click text selection** is preserved via programmatic word selection\n- Drag and drop works alongside **row selection** and **clickable rows**\n\n> **Note:** When combining drag and drop with **row pinning**, pinned rows can\n> be reordered among themselves and non-pinned rows among themselves, but items\n> cannot be dragged between pinned and non-pinned groups.\n\n### Namespace isolation\n\nUse `dragNamespace` to prevent drag between unrelated tables:\n\n```tsx\nconst { dragAndDropHooks } = useDragAndDrop({\n dragNamespace: 'my-table',\n ...createArrayHandlers(setRows, (row) => row.id),\n});\n```\n\nSee the [`useDragAndDrop` hook documentation](/hooks/use-drag-and-drop) for external drop support, outgoing formats, and helper functions.\n\n## Compound component API\n\nFor advanced customization, use the compound component pattern:\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n ];\n\n const rows = [\n { id: '1', name: 'Alice', role: 'Admin' },\n { id: '2', name: 'Bob', role: 'User' },\n ];\n\n return (\n <DataTable.Root columns={columns} rows={rows} allowsSorting>\n <DataTable.Manager />\n <DataTable.Table aria-label=\"Users table\">\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n <DataTable.Footer>\n <Stack direction=\"row\" justify=\"center\" py=\"300\">\n <Text fontSize=\"sm\" color=\"neutral.11\">Custom footer content</Text>\n </Stack>\n </DataTable.Footer>\n </DataTable.Root>\n );\n}\n```\n\n### Available sub-components\n\n| Component | Description |\n| ------------------- | ------------------------------------------------------------------------------------------------------- |\n| `DataTable.Root` | Provider component that manages state and context |\n| `DataTable.Table` | The main table element |\n| `DataTable.Header` | Table header with column headers |\n| `DataTable.Body` | Table body containing rows |\n| `DataTable.Footer` | Footer section for pagination or summaries |\n| `DataTable.Manager` | Settings panel for column visibility and layout - [see detailed docs below](#advanced-datatablemanager) |\n\n## Advanced: DataTable.Manager\n\nThe `DataTable.Manager` component provides a settings drawer for managing table\nconfiguration. It enables users to control column visibility, reorder columns,\nand adjust layout settings through an intuitive interface.\n\n### Manager overview\n\nDataTable.Manager must be used within a DataTable.Root to access table state and\ncolumns. It renders as a settings button that opens a drawer with multiple\nconfiguration tabs:\n\n```jsx live-dev\nconst App = () => {\n const [visibleColumns, setVisibleColumns] = useState(['name', 'email', 'role']);\n const [isTruncated, setIsTruncated] = useState(false);\n const [density, setDensity] = useState('default');\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'email', header: 'Email', accessor: (row) => row.email },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n ];\n\n const rows = [\n { id: '1', name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin' },\n { id: '2', name: 'Bob Smith', email: 'bob@example.com', role: 'User' },\n { id: '3', name: 'Carol Williams', email: 'carol@example.com', role: 'Editor' },\n ];\n\n const handleSettingsChange = (action) => {\n if (action === 'toggleTextVisibility') {\n setIsTruncated(!isTruncated);\n } else if (action === 'toggleRowDensity') {\n setDensity(density === 'default' ? 'condensed' : 'default');\n }\n };\n\n return (\n <DataTable.Root\n columns={columns}\n rows={rows}\n visibleColumns={visibleColumns}\n onColumnsChange={(updatedColumns) => {\n setVisibleColumns(updatedColumns.map(col => col.id));\n }}\n isTruncated={isTruncated}\n density={density}\n onSettingsChange={handleSettingsChange}\n >\n <Stack direction=\"row\" justifyContent=\"space-between\" mb=\"400\">\n <Text fontSize=\"lg\" fontWeight=\"600\">Users</Text>\n <DataTable.Manager />\n </Stack>\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n );\n}\n```\n\n### Manager: Column visibility\n\nThe Manager provides a dual-list interface for managing visible and hidden\ncolumns. Users can drag columns between lists, reorder visible columns, and\nsearch hidden columns:\n\n```jsx live-dev\nconst App = () => {\n const [visibleColumns, setVisibleColumns] = useState(['name', 'email']);\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'email', header: 'Email', accessor: (row) => row.email },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n { id: 'department', header: 'Department', accessor: (row) => row.department },\n ];\n\n const rows = [\n { id: '1', name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin', department: 'Engineering' },\n { id: '2', name: 'Bob Smith', email: 'bob@example.com', role: 'User', department: 'Sales' },\n ];\n\n return (\n <DataTable.Root\n columns={columns}\n rows={rows}\n visibleColumns={visibleColumns}\n onColumnsChange={(updatedColumns) => {\n setVisibleColumns(updatedColumns.map(col => col.id));\n }}\n >\n <Stack direction=\"row\" justifyContent=\"space-between\" mb=\"400\">\n <Text fontSize=\"lg\" fontWeight=\"600\">Team Members</Text>\n <DataTable.Manager />\n </Stack>\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n );\n}\n```\n\n### Manager: Layout settings\n\nThe Layout Settings tab allows users to control text truncation and row density.\nThese settings update the table's visual appearance in real-time:\n\n```jsx live-dev\nconst App = () => {\n const [visibleColumns, setVisibleColumns] = useState(['name', 'description']);\n const [isTruncated, setIsTruncated] = useState(false);\n const [density, setDensity] = useState('default');\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'description', header: 'Description', accessor: (row) => row.description },\n ];\n\n const rows = [\n {\n id: '1',\n name: 'Product A',\n description: 'This is a very long description that will demonstrate text truncation when enabled'\n },\n {\n id: '2',\n name: 'Product B',\n description: 'Another lengthy description to show how the layout settings affect content display'\n },\n ];\n\n const handleSettingsChange = (action) => {\n if (action === 'toggleTextVisibility') {\n setIsTruncated(!isTruncated);\n } else if (action === 'toggleRowDensity') {\n setDensity(density === 'default' ? 'condensed' : 'default');\n }\n };\n\n return (\n <DataTable.Root\n columns={columns}\n rows={rows}\n visibleColumns={visibleColumns}\n onColumnsChange={(updatedColumns) => {\n setVisibleColumns(updatedColumns.map(col => col.id));\n }}\n isTruncated={isTruncated}\n density={density}\n onSettingsChange={handleSettingsChange}\n >\n <Stack direction=\"row\" justifyContent=\"space-between\" mb=\"400\">\n <Text fontSize=\"lg\" fontWeight=\"600\">Products</Text>\n <DataTable.Manager />\n </Stack>\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n );\n}\n```\n\n### Manager: Custom settings tab\n\nAdd a custom settings tab by providing the `customSettings` prop to\nDataTable.Root. This allows extending the Manager with application-specific\nconfiguration:\n\n```jsx live-dev\nconst App = () => {\n const [visibleColumns, setVisibleColumns] = useState(['name', 'status']);\n const [filterStatus, setFilterStatus] = useState('all');\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'status', header: 'Status', accessor: (row) => row.status },\n ];\n\n const allRows = [\n { id: '1', name: 'Task A', status: 'Active' },\n { id: '2', name: 'Task B', status: 'Inactive' },\n { id: '3', name: 'Task C', status: 'Active' },\n ];\n\n const filteredRows = filterStatus === 'all'\n ? allRows\n : allRows.filter(row => row.status.toLowerCase() === filterStatus);\n\n const customSettingsPanel = (\n <Stack direction=\"column\" gap=\"400\">\n <Text fontWeight=\"600\">Filter Options</Text>\n <ToggleButtonGroup.Root\n selectedKeys={[filterStatus]}\n onSelectionChange={(keys) => setFilterStatus(Array.from(keys)[0])}\n >\n <ToggleButton id=\"all\">All</ToggleButton>\n <ToggleButton id=\"active\">Active</ToggleButton>\n <ToggleButton id=\"inactive\">Inactive</ToggleButton>\n </ToggleButtonGroup.Root>\n </Stack>\n );\n\n return (\n <DataTable.Root\n columns={columns}\n rows={filteredRows}\n visibleColumns={visibleColumns}\n onColumnsChange={(updatedColumns) => {\n setVisibleColumns(updatedColumns.map(col => col.id));\n }}\n customSettings={{\n label: 'Filters',\n icon: <Icons.FilterList />,\n panel: customSettingsPanel,\n }}\n >\n <Stack direction=\"row\" justifyContent=\"space-between\" mb=\"400\">\n <Text fontSize=\"lg\" fontWeight=\"600\">Tasks</Text>\n <DataTable.Manager />\n </Stack>\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n );\n}\n```\n\n### Manager: Controlled column state with persistence\n\nManage column configuration externally for features like saving user\npreferences:\n\n```jsx live-dev\nconst App = () => {\n const [visibleColumns, setVisibleColumns] = useState(['name', 'email', 'role']);\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'email', header: 'Email', accessor: (row) => row.email },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n { id: 'department', header: 'Department', accessor: (row) => row.department },\n { id: 'location', header: 'Location', accessor: (row) => row.location },\n ];\n\n const rows = [\n {\n id: '1',\n name: 'Alice Johnson',\n email: 'alice@example.com',\n role: 'Admin',\n department: 'Engineering',\n location: 'New York'\n },\n ];\n\n const handleColumnsChange = (updatedColumns) => {\n const newVisibleColumns = updatedColumns.map(col => col.id);\n setVisibleColumns(newVisibleColumns);\n\n // Example: Save to localStorage\n localStorage.setItem('tableColumns', JSON.stringify(newVisibleColumns));\n };\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <DataTable.Root\n columns={columns}\n rows={rows}\n visibleColumns={visibleColumns}\n onColumnsChange={handleColumnsChange}\n >\n <Stack direction=\"row\" justifyContent=\"space-between\" mb=\"400\">\n <Text fontSize=\"lg\" fontWeight=\"600\">Team Directory</Text>\n <DataTable.Manager />\n </Stack>\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n <Text fontSize=\"sm\" color=\"gray.9\">\n Visible columns: {visibleColumns.join(', ')}\n </Text>\n </Stack>\n );\n}\n```\n\n### Manager: Context requirements\n\nDataTable.Manager **must** be used within a DataTable.Root component. It relies\non the DataTable context for:\n\n- Column configuration (`columns`, `visibleColumns`)\n- Layout settings (`isTruncated`, `density`)\n- Callbacks (`onColumnsChange`, `onSettingsChange`)\n- Custom settings configuration\n\nUsing Manager outside of DataTable.Root will throw an error.\n\n## Component requirements\n\n## Accessibility\n\nThe DataTable uses React Aria's Table components for full accessibility support:\n\n- **ARIA grid pattern**: The table implements the ARIA grid pattern with proper\n roles\n- **Keyboard navigation**: Full keyboard support with arrow keys, Tab, Enter,\n and Space\n- **Screen reader support**: Proper announcements for sorting, selection, and\n expansion\n\nAlways provide an accessible label for the table:\n\n```tsx\n<DataTable columns={columns} rows={rows} aria-label=\"Product inventory table\" />\n```\n\nOr with the compound component API:\n\n```tsx\n<DataTable.Root columns={columns} rows={rows}>\n <DataTable.Table aria-label=\"Product inventory table\">\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n</DataTable.Root>\n```\n\n#### Persistent ID\n\nIf your use case requires tracking and analytics for this component, it is good\npractice to add a **persistent**, **unique** id to the component:\n\n```tsx\nconst PERSISTENT_ID = \"product-inventory-table\";\n\nexport const Example = () => (\n <DataTable\n id={PERSISTENT_ID}\n columns={columns}\n rows={rows}\n aria-label=\"Product inventory table\"\n />\n);\n```\n\n#### Keyboard navigation\n\nThe component supports full keyboard interaction:\n\n- `Tab` / `Shift+Tab`: Move focus between interactive elements\n- `Arrow keys`: Navigate between cells and rows\n- `Enter` / `Space`: Activate sorting, selection, or expansion\n- `Escape`: Cancel current interaction\n\nThe Manager drawer also supports keyboard navigation:\n\n- `Tab` / `Shift+Tab`: Navigate between the settings button, tabs, and controls\n- `Enter` / `Space`: Open the Manager drawer, activate tabs, toggle buttons\n- `Arrow keys`: Navigate between tab panels, reorder items in drag-and-drop\n lists\n- `Escape`: Close the Manager drawer\n- Drag-and-drop keyboard operations: `Enter` / `Space` to pick up, arrow keys to move,\n `Enter` / `Space` to drop\n\n## API reference\n\n<PropsTable id=\"DataTable\" />\n\n### DataTableColumnItem\n\n```tsx\ntype DataTableColumnItem<T extends object = Record<string, unknown>> = {\n id: string; // Unique column identifier\n header: ReactNode; // Column header content\n accessor: (row: T) => ReactNode; // Function to extract cell value\n render?: (cell: {\n // Custom cell renderer\n value: unknown;\n row: T;\n column: DataTableColumnItem<T>;\n }) => ReactNode;\n isResizable?: boolean; // Enable column resizing\n width?: number | null; // Initial width in pixels\n defaultWidth?: number | null; // Default width\n minWidth?: number | null; // Minimum width when resizing\n maxWidth?: number | null; // Maximum width when resizing\n sticky?: boolean; // Stick column to edge\n isSortable?: boolean; // Enable sorting for column\n isRowHeader?: boolean; // Mark as row header (accessibility)\n headerIcon?: ReactNode; // Icon in column header\n};\n```\n\n### DataTableRowItem\n\n```tsx\ntype DataTableRowItem<T extends object = Record<string, unknown>> = T & {\n id: string; // Unique row identifier (required)\n isDisabled?: boolean; // Disable row interactions\n [key: string]: unknown;\n};\n```\n\n### SortDescriptor\n\n```tsx\ntype SortDescriptor = {\n column: string; // Column ID to sort by\n direction: \"ascending\" | \"descending\"; // Sort direction\n};\n```\n\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using DataTable\nin your application. As the component's internal functionality is already tested\nby Nimbus, these patterns help you verify your integration and\napplication-specific logic.\n\n### Basic Rendering Tests\n\nVerify the DataTable renders with correct structure\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable - Basic rendering\", () => {\n it(\"renders table with grid role\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} />\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"grid\")).toBeInTheDocument();\n });\n\n it(\"renders column headers\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Name\")).toBeInTheDocument();\n expect(screen.getByText(\"Email\")).toBeInTheDocument();\n expect(screen.getByText(\"Role\")).toBeInTheDocument();\n });\n\n it(\"renders all data rows\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Alice Johnson\")).toBeInTheDocument();\n expect(screen.getByText(\"Bob Smith\")).toBeInTheDocument();\n expect(screen.getByText(\"Carol Williams\")).toBeInTheDocument();\n });\n\n it(\"renders correct number of rows\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} />\n </NimbusProvider>\n );\n\n // 1 header row + 3 data rows\n const allRows = screen.getAllByRole(\"row\");\n expect(allRows.length).toBe(4);\n });\n});\n```\n\n### Sorting Tests\n\nTest column sorting functionality\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable - Sorting\", () => {\n it(\"renders sortable column headers when allowsSorting is true\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} allowsSorting />\n </NimbusProvider>\n );\n\n const nameHeader = screen\n .getByText(\"Name\")\n .closest('[role=\"columnheader\"]');\n expect(nameHeader).toBeInTheDocument();\n });\n\n it(\"calls onSortChange when column header is clicked\", async () => {\n const user = userEvent.setup();\n const handleSortChange = vi.fn();\n\n render(\n <NimbusProvider>\n <DataTable\n columns={columns}\n rows={rows}\n allowsSorting\n onSortChange={handleSortChange}\n />\n </NimbusProvider>\n );\n\n await user.click(screen.getByText(\"Name\"));\n\n expect(handleSortChange).toHaveBeenCalledWith({\n column: \"name\",\n direction: expect.any(String),\n });\n });\n\n it(\"displays sort indicator on sorted column\", async () => {\n const user = userEvent.setup();\n\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} allowsSorting />\n </NimbusProvider>\n );\n\n await user.click(screen.getByText(\"Name\"));\n\n await waitFor(() => {\n const columnHeader = screen\n .getByText(\"Name\")\n .closest('[role=\"columnheader\"]');\n expect(columnHeader).toHaveAttribute(\"aria-sort\");\n });\n });\n});\n```\n\n### Selection Tests\n\nTest row selection functionality\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable - Selection\", () => {\n it(\"renders checkboxes when selectionMode is multiple\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} selectionMode=\"multiple\" />\n </NimbusProvider>\n );\n\n const checkboxes = screen.getAllByRole(\"checkbox\");\n // 1 select-all checkbox + 3 row checkboxes\n expect(checkboxes.length).toBe(4);\n });\n\n it(\"allows selecting a row\", async () => {\n const user = userEvent.setup();\n const handleSelectionChange = vi.fn();\n\n render(\n <NimbusProvider>\n <DataTable\n columns={columns}\n rows={rows}\n selectionMode=\"multiple\"\n onSelectionChange={handleSelectionChange}\n />\n </NimbusProvider>\n );\n\n const checkboxes = screen.getAllByRole(\"checkbox\");\n await user.click(checkboxes[1]); // First data row checkbox\n\n expect(handleSelectionChange).toHaveBeenCalled();\n });\n\n it(\"allows selecting all rows with header checkbox\", async () => {\n const user = userEvent.setup();\n const handleSelectionChange = vi.fn();\n\n render(\n <NimbusProvider>\n <DataTable\n columns={columns}\n rows={rows}\n selectionMode=\"multiple\"\n onSelectionChange={handleSelectionChange}\n />\n </NimbusProvider>\n );\n\n const selectAllCheckbox = screen.getAllByRole(\"checkbox\")[0];\n await user.click(selectAllCheckbox);\n\n await waitFor(() => {\n expect(handleSelectionChange).toHaveBeenCalledWith(\"all\");\n });\n });\n\n it(\"supports single selection mode\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} selectionMode=\"single\" />\n </NimbusProvider>\n );\n\n const radios = screen.getAllByRole(\"checkbox\");\n expect(radios.length).toBe(3);\n });\n});\n```\n\n### Row Interaction Tests\n\nTest row click and interaction functionality\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable - Row interactions\", () => {\n it(\"calls onRowClick when a row is clicked\", async () => {\n const user = userEvent.setup();\n const handleRowClick = vi.fn();\n\n render(\n <NimbusProvider>\n <DataTable\n columns={columns}\n rows={rows}\n search=\"\"\n onRowClick={handleRowClick}\n />\n </NimbusProvider>\n );\n\n const allRows = screen.getAllByRole(\"row\");\n const firstDataRow = allRows[1]; // Second data row (index 2 because of header)\n const clickableCell = within(firstDataRow).getAllByRole(\"rowheader\");\n await user.click(clickableCell[0]);\n\n await waitFor(() => {\n expect(handleRowClick).toHaveBeenCalled();\n });\n });\n\n it(\"applies disabled state to specified rows\", () => {\n render(\n <NimbusProvider>\n <DataTable\n columns={columns}\n rows={rows}\n selectionMode=\"multiple\"\n disabledKeys={new Set([\"2\"])}\n />\n </NimbusProvider>\n );\n\n const allRows = screen.getAllByRole(\"row\");\n const disabledRow = allRows[2]; // Second data row (index 2 because of header)\n const checkbox = within(disabledRow).getByRole(\"checkbox\");\n expect(checkbox).toBeDisabled();\n });\n});\n```\n\n### Search and Filtering Tests\n\nTest search functionality\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable - Search and filtering\", () => {\n it(\"filters rows based on search term\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} search=\"Alice\" />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Alice\", { selector: \"mark\" })).toBeInTheDocument();\n expect(screen.getByText(\"Johnson\")).toBeInTheDocument();\n expect(screen.queryByText(\"Bob Smith\")).not.toBeInTheDocument();\n expect(screen.queryByText(\"Carol Williams\")).not.toBeInTheDocument();\n });\n\n it(\"shows all rows when search is empty\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} search=\"\" />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Alice Johnson\")).toBeInTheDocument();\n expect(screen.getByText(\"Bob Smith\")).toBeInTheDocument();\n expect(screen.getByText(\"Carol Williams\")).toBeInTheDocument();\n });\n\n it(\"filters across multiple columns\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} search=\"Admin\" />\n </NimbusProvider>\n );\n\n // Alice is the Admin\n expect(screen.getByText(\"Alice Johnson\")).toBeInTheDocument();\n expect(screen.queryByText(\"Bob Smith\")).not.toBeInTheDocument();\n });\n});\n```\n\n### Column Visibility Tests\n\nTest column visibility control\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable - Column visibility\", () => {\n it(\"shows only specified visible columns\", () => {\n render(\n <NimbusProvider>\n <DataTable\n columns={columns}\n rows={rows}\n visibleColumns={[\"name\", \"role\"]}\n />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Name\")).toBeInTheDocument();\n expect(screen.getByText(\"Role\")).toBeInTheDocument();\n expect(screen.queryByText(\"Email\")).not.toBeInTheDocument();\n });\n\n it(\"shows all columns when visibleColumns is not specified\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Name\")).toBeInTheDocument();\n expect(screen.getByText(\"Email\")).toBeInTheDocument();\n expect(screen.getByText(\"Role\")).toBeInTheDocument();\n });\n});\n```\n\n### Density Tests\n\nTest density variants\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable - Density\", () => {\n it(\"renders with default density\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} density=\"default\" />\n </NimbusProvider>\n );\n\n const table = screen.getByRole(\"grid\");\n expect(table).toBeInTheDocument();\n });\n\n it(\"renders with condensed density\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} density=\"condensed\" />\n </NimbusProvider>\n );\n\n const table = screen.getByRole(\"grid\");\n expect(table).toBeInTheDocument();\n });\n});\n```\n\n### Nested Rows Tests\n\nTest expandable nested row functionality\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable - Nested rows\", () => {\n const nestedRows: DataTableRowItem[] = [\n {\n id: \"1\",\n name: \"Parent Item\",\n email: \"parent@example.com\",\n role: \"Admin\",\n children: (\n <Box\n p=\"500\"\n bg=\"neutral.2\"\n borderRadius=\"md\"\n border=\"1px solid\"\n borderColor=\"neutral.6\"\n m=\"200 0\"\n >\n Child Item 1\n </Box>\n ),\n },\n {\n id: \"2\",\n name: \"No Children\",\n email: \"single@example.com\",\n role: \"User\",\n },\n ];\n\n it(\"renders expand button for rows with children\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={nestedRows} nestedKey=\"children\" />\n </NimbusProvider>\n );\n\n const expandButton = screen.getByRole(\"button\", { name: /expand/i });\n expect(expandButton).toBeInTheDocument();\n });\n\n it(\"expands nested content when expand button is clicked\", async () => {\n const user = userEvent.setup();\n\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={nestedRows} nestedKey=\"children\" />\n </NimbusProvider>\n );\n\n // Initially nested content should not be visible\n expect(screen.queryByText(\"Child Item 1\")).not.toBeInTheDocument();\n\n const expandButton = screen.getByRole(\"button\", { name: /expand/i });\n expect(expandButton).toBeInTheDocument();\n await user.click(expandButton);\n\n await waitFor(async () => {\n await expect(screen.getByText(\"Child Item 1\")).toBeInTheDocument();\n });\n });\n\n it(\"collapses nested content when collapse button is clicked\", async () => {\n const user = userEvent.setup();\n\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={nestedRows} nestedKey=\"children\" />\n </NimbusProvider>\n );\n\n // Expand first\n const expandButton = screen.getByRole(\"button\", { name: /expand/i });\n await user.click(expandButton);\n\n await waitFor(() => {\n expect(screen.getByText(\"Child Item 1\")).toBeInTheDocument();\n });\n\n // Collapse\n const collapseButton = screen.getByRole(\"button\", { name: /collapse/i });\n await user.click(collapseButton);\n\n await waitFor(() => {\n expect(screen.queryByText(\"Child Item 1\")).not.toBeInTheDocument();\n });\n });\n});\n```\n\n### Empty State Tests\n\nTest empty state rendering\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable - Empty state\", () => {\n it(\"renders custom empty state when no rows\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={[]} />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"No Data\")).toBeInTheDocument();\n });\n\n it(\"renders empty state when search matches no rows\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} search=\"nonexistent\" />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"No Data\")).toBeInTheDocument();\n });\n});\n```\n\n### Accessibility Tests\n\nTest accessibility features\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable - Accessibility\", () => {\n it(\"has accessible grid role\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} aria-label=\"Users table\" />\n </NimbusProvider>\n );\n\n const table = screen.getByRole(\"grid\");\n expect(table).toBeInTheDocument();\n });\n\n it(\"has proper row structure\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} />\n </NimbusProvider>\n );\n\n const tableRows = screen.getAllByRole(\"row\");\n expect(tableRows.length).toBeGreaterThan(0);\n\n // Check that rows contain cells\n const firstDataRow = tableRows[1];\n const cells = within(firstDataRow).getAllByRole(\"gridcell\");\n expect(cells.length).toBeGreaterThan(0);\n });\n\n it(\"has sortable columns with aria-sort attribute\", async () => {\n const user = userEvent.setup();\n\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} allowsSorting />\n </NimbusProvider>\n );\n\n await user.click(screen.getByText(\"Name\"));\n\n await waitFor(() => {\n const columnHeader = screen\n .getByText(\"Name\")\n .closest('[role=\"columnheader\"]');\n expect(columnHeader).toHaveAttribute(\"aria-sort\");\n });\n });\n});\n```\n\n### Manager: Basic Rendering Tests\n\nVerify the Manager button renders and opens the settings drawer\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable.Manager - Basic rendering\", () => {\n it(\"renders the settings button\", () => {\n const managerColumns: DataTableColumnItem[] = [\n {\n id: \"name\",\n header: \"Name\",\n accessor: (row: Record<string, unknown>) => row.name as React.ReactNode,\n },\n {\n id: \"email\",\n header: \"Email\",\n accessor: (row: Record<string, unknown>) =>\n row.email as React.ReactNode,\n },\n ];\n const managerRows: DataTableRowItem[] = [\n { id: \"1\", name: \"Alice\", email: \"alice@example.com\" },\n ];\n\n render(\n <NimbusProvider>\n <DataTable.Root\n columns={managerColumns}\n rows={managerRows}\n visibleColumns={[\"name\"]}\n >\n <DataTable.Manager />\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n </NimbusProvider>\n );\n\n expect(\n screen.getByRole(\"button\", { name: /settings/i })\n ).toBeInTheDocument();\n });\n\n it(\"settings button is clickable\", async () => {\n const user = userEvent.setup();\n const managerColumns: DataTableColumnItem[] = [\n {\n id: \"name\",\n header: \"Name\",\n accessor: (row: Record<string, unknown>) => row.name as React.ReactNode,\n },\n {\n id: \"email\",\n header: \"Email\",\n accessor: (row: Record<string, unknown>) =>\n row.email as React.ReactNode,\n },\n ];\n const managerRows: DataTableRowItem[] = [\n { id: \"1\", name: \"Alice\", email: \"alice@example.com\" },\n ];\n\n render(\n <NimbusProvider>\n <DataTable.Root\n columns={managerColumns}\n rows={managerRows}\n visibleColumns={[\"name\"]}\n >\n <DataTable.Manager />\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n </NimbusProvider>\n );\n\n const settingsButton = screen.getByRole(\"button\", { name: /settings/i });\n\n // Verify button can be clicked\n await user.click(settingsButton);\n\n // Button should still be in the document after click\n expect(settingsButton).toBeInTheDocument();\n });\n});\n```\n\n### Manager: Column Management Tests\n\nTest column visibility and reordering functionality\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable.Manager - Column management\", () => {\n it(\"displays visible and hidden columns in separate lists\", async () => {\n const user = userEvent.setup();\n const managerColumns: DataTableColumnItem[] = [\n {\n id: \"name\",\n header: \"Name\",\n accessor: (row: Record<string, unknown>) => row.name as React.ReactNode,\n },\n {\n id: \"email\",\n header: \"Email\",\n accessor: (row: Record<string, unknown>) =>\n row.email as React.ReactNode,\n },\n {\n id: \"role\",\n header: \"Role\",\n accessor: (row: Record<string, unknown>) => row.role as React.ReactNode,\n },\n ];\n const managerRows: DataTableRowItem[] = [\n { id: \"1\", name: \"Alice\", email: \"alice@example.com\", role: \"Admin\" },\n ];\n\n render(\n <NimbusProvider>\n <DataTable.Root\n columns={managerColumns}\n rows={managerRows}\n visibleColumns={[\"name\", \"email\"]}\n >\n <DataTable.Manager />\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n </NimbusProvider>\n );\n\n const settingsButton = screen.getByRole(\"button\", { name: /settings/i });\n await user.click(settingsButton);\n\n await waitFor(() => {\n const visibleList = screen.getByTestId(\"visible-columns-list\");\n const hiddenList = screen.getByTestId(\"hidden-columns-list\");\n\n expect(within(visibleList).getByText(\"Name\")).toBeInTheDocument();\n expect(within(visibleList).getByText(\"Email\")).toBeInTheDocument();\n expect(within(hiddenList).getByText(\"Role\")).toBeInTheDocument();\n });\n });\n\n it(\"calls onColumnsChange when column visibility changes\", async () => {\n const user = userEvent.setup();\n const handleColumnsChange = vi.fn();\n const managerColumns: DataTableColumnItem[] = [\n {\n id: \"name\",\n header: \"Name\",\n accessor: (row: Record<string, unknown>) => row.name as React.ReactNode,\n },\n {\n id: \"email\",\n header: \"Email\",\n accessor: (row: Record<string, unknown>) =>\n row.email as React.ReactNode,\n },\n ];\n const managerRows: DataTableRowItem[] = [\n { id: \"1\", name: \"Alice\", email: \"alice@example.com\" },\n ];\n\n render(\n <NimbusProvider>\n <DataTable.Root\n columns={managerColumns}\n rows={managerRows}\n visibleColumns={[\"name\"]}\n onColumnsChange={handleColumnsChange}\n >\n <DataTable.Manager />\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n </NimbusProvider>\n );\n\n const settingsButton = screen.getByRole(\"button\", { name: /settings/i });\n await user.click(settingsButton);\n\n // Note: Full drag-and-drop testing would require more complex setup\n // This verifies the callback is available\n expect(handleColumnsChange).toBeDefined();\n });\n});\n```\n\n### Manager: Layout Settings Tests\n\nTest text visibility and row density controls\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable.Manager - Layout settings\", () => {\n it(\"displays layout settings tab\", async () => {\n const user = userEvent.setup();\n const managerColumns: DataTableColumnItem[] = [\n {\n id: \"name\",\n header: \"Name\",\n accessor: (row: Record<string, unknown>) => row.name as React.ReactNode,\n },\n {\n id: \"email\",\n header: \"Email\",\n accessor: (row: Record<string, unknown>) =>\n row.email as React.ReactNode,\n },\n ];\n const managerRows: DataTableRowItem[] = [\n { id: \"1\", name: \"Alice\", email: \"alice@example.com\" },\n ];\n\n render(\n <NimbusProvider>\n <DataTable.Root\n columns={managerColumns}\n rows={managerRows}\n visibleColumns={[\"name\"]}\n >\n <DataTable.Manager />\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n </NimbusProvider>\n );\n\n const settingsButton = screen.getByRole(\"button\", { name: /settings/i });\n await user.click(settingsButton);\n\n await waitFor(() => {\n expect(screen.getByText(/layout settings/i)).toBeInTheDocument();\n });\n });\n\n it(\"calls onSettingsChange when layout settings change\", async () => {\n const user = userEvent.setup();\n const handleSettingsChange = vi.fn();\n const managerColumns: DataTableColumnItem[] = [\n {\n id: \"name\",\n header: \"Name\",\n accessor: (row: Record<string, unknown>) => row.name as React.ReactNode,\n },\n {\n id: \"email\",\n header: \"Email\",\n accessor: (row: Record<string, unknown>) =>\n row.email as React.ReactNode,\n },\n ];\n const managerRows: DataTableRowItem[] = [\n { id: \"1\", name: \"Alice\", email: \"alice@example.com\" },\n ];\n\n render(\n <NimbusProvider>\n <DataTable.Root\n columns={managerColumns}\n rows={managerRows}\n visibleColumns={[\"name\"]}\n onSettingsChange={handleSettingsChange}\n >\n <DataTable.Manager />\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n </NimbusProvider>\n );\n\n const settingsButton = screen.getByRole(\"button\", { name: /settings/i });\n await user.click(settingsButton);\n\n // Note: Actual toggle interaction testing would require finding specific buttons\n // This verifies the callback is wired up\n expect(handleSettingsChange).toBeDefined();\n });\n});\n```\n\n### Manager: Custom Settings Tests\n\nTest custom settings tab integration\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable.Manager - Custom settings\", () => {\n it(\"displays custom settings tab when provided\", async () => {\n const user = userEvent.setup();\n const managerColumns: DataTableColumnItem[] = [\n {\n id: \"name\",\n header: \"Name\",\n accessor: (row: Record<string, unknown>) => row.name as React.ReactNode,\n },\n {\n id: \"email\",\n header: \"Email\",\n accessor: (row: Record<string, unknown>) =>\n row.email as React.ReactNode,\n },\n ];\n const managerRows: DataTableRowItem[] = [\n { id: \"1\", name: \"Alice\", email: \"alice@example.com\" },\n ];\n\n const customSettings = {\n label: \"Filters\",\n panel: <div>Custom Filter Panel</div>,\n };\n\n render(\n <NimbusProvider>\n <DataTable.Root\n columns={managerColumns}\n rows={managerRows}\n visibleColumns={[\"name\"]}\n customSettings={customSettings}\n >\n <DataTable.Manager />\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n </NimbusProvider>\n );\n\n const settingsButton = screen.getByRole(\"button\", { name: /settings/i });\n await user.click(settingsButton);\n\n await waitFor(() => {\n expect(screen.getByText(\"Filters\")).toBeInTheDocument();\n });\n });\n\n it(\"renders custom settings panel content\", async () => {\n const user = userEvent.setup();\n const managerColumns: DataTableColumnItem[] = [\n {\n id: \"name\",\n header: \"Name\",\n accessor: (row: Record<string, unknown>) => row.name as React.ReactNode,\n },\n {\n id: \"email\",\n header: \"Email\",\n accessor: (row: Record<string, unknown>) =>\n row.email as React.ReactNode,\n },\n ];\n const managerRows: DataTableRowItem[] = [\n { id: \"1\", name: \"Alice\", email: \"alice@example.com\" },\n ];\n\n const customSettings = {\n label: \"Advanced\",\n panel: <div data-testid=\"custom-panel\">Custom Panel Content</div>,\n };\n\n render(\n <NimbusProvider>\n <DataTable.Root\n columns={managerColumns}\n rows={managerRows}\n visibleColumns={[\"name\"]}\n customSettings={customSettings}\n >\n <DataTable.Manager />\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n </NimbusProvider>\n );\n\n const settingsButton = screen.getByRole(\"button\", { name: /settings/i });\n await user.click(settingsButton);\n\n await waitFor(() => {\n const advancedTab = screen.getByText(\"Advanced\");\n expect(advancedTab).toBeInTheDocument();\n });\n\n // Click the custom tab\n const advancedTab = screen.getByText(\"Advanced\");\n await user.click(advancedTab);\n\n await waitFor(() => {\n expect(screen.getByTestId(\"custom-panel\")).toBeInTheDocument();\n expect(screen.getByText(\"Custom Panel Content\")).toBeInTheDocument();\n });\n });\n});\n```\n\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-datatable--docs)\n- [React Aria Table](https://react-spectrum.adobe.com/react-aria/Table.html)\n- [ARIA Grid Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/)\n\n**Related components**\n\n- **[Table Component](/components/data-display/table)** - Use the simpler Table component for static, read-only data without interactive features\n",
192
+ "mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport {\n DataTable,\n type DataTableProps,\n type DataTableColumnItem,\n type DataTableRowItem,\n type SortDescriptor,\n type DataTableColumnRenderProps,\n type DataTableRowRenderProps,\n type DataTableCellRenderProps,\n} from \"@commercetools/nimbus\";\n```\n\n### Basic usage\n\nThe DataTable renders data in a tabular format with support for sorting,\nselection, resizing, and nested content. At minimum, you need to provide\n`columns` and `rows`:\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'email', header: 'Email', accessor: (row) => row.email },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n ];\n\n const rows = [\n { id: '1', name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin' },\n { id: '2', name: 'Bob Smith', email: 'bob@example.com', role: 'User' },\n { id: '3', name: 'Carol Williams', email: 'carol@example.com', role: 'Editor' },\n ];\n\n return <DataTable columns={columns} rows={rows} />;\n}\n```\n\n## When to use DataTable\n\nUse the **DataTable** component when you need **interactive data management features** like sorting, filtering, selection, or pagination. It's built for complex, data-intensive interfaces.\n\n### Keep it simple\n\n**Don't use DataTable for static content.** If you only need to display read-only data without interactive features, use the simpler [Table component](/components/data-display/table) instead. It's lighter, faster, and easier to implement.\n\n**Use the Table component when you:**\n- Display static reference data (specifications, comparisons, pricing)\n- Have small datasets (between 10 - 20 rows)\n- Don't need sorting, filtering, or selection\n- Want a straightforward implementation without state management\n\n**Use DataTable when you need:**\n- Sorting columns in ascending or descending order\n- Row selection for bulk actions\n- Column management (show/hide, reorder, resize)\n- Search and filtering across data\n- Pagination for large datasets (> 50 rows)\n- Server-side data operations\n- Nested/hierarchical data with expansion\n- Dense data visualization with configurable row spacing\n\n**Example decision:**\n\n```tsx\n// ✗ Avoid - Overkill for static data\n<DataTable\n columns={threeStaticColumns}\n rows={fiveStaticRows}\n/>\n\n// ✓ Better - Use Table for static content\n<Table.Root>\n <Table.Header>{/* specification headers */}</Table.Header>\n <Table.Body>{/* specification rows */}</Table.Body>\n</Table.Root>\n```\n\n→ **[See Table component documentation](/components/data-display/table)** for simple table layouts.\n\n## Working with columns and rows\n\n### Column configuration\n\nEach column requires an `id`, `header`, and `accessor` function. Additional\noptions control sorting, resizing, and custom rendering:\n\n```tsx\nimport type { DataTableColumnItem } from \"@commercetools/nimbus\";\n\nconst columns: DataTableColumnItem[] = [\n {\n id: \"name\",\n header: \"Product Name\",\n accessor: (row) => row.name,\n isSortable: true, // Enable sorting for this column\n isResizable: true, // Enable column resizing\n isRowHeader: true, // Mark as row header for accessibility\n width: 200, // Initial width in pixels\n minWidth: 100, // Minimum width when resizing\n maxWidth: 400, // Maximum width when resizing\n },\n {\n id: \"status\",\n header: \"Status\",\n accessor: (row) => row.status,\n // Custom rendering with the render prop\n render: ({ value }) => (\n <Badge colorPalette={value === \"Active\" ? \"positive\" : \"neutral\"}>\n {value}\n </Badge>\n ),\n },\n {\n id: \"actions\",\n header: \"Actions\",\n accessor: (row) => row.id,\n headerIcon: (\n <IconButton aria-label=\"Info\" size=\"2xs\" variant=\"ghost\">\n <Info />\n </IconButton>\n ),\n isSortable: false,\n },\n];\n```\n\n### Row data structure\n\nEach row must have a unique `id` property. Additional properties are accessed\nvia column `accessor` functions:\n\n```tsx\nimport type { DataTableRowItem } from \"@commercetools/nimbus\";\n\nconst rows: DataTableRowItem[] = [\n { id: \"1\", name: \"Product A\", status: \"Active\", price: 99.99 },\n {\n id: \"2\",\n name: \"Product B\",\n status: \"Draft\",\n price: 149.99,\n isDisabled: true,\n },\n { id: \"3\", name: \"Product C\", status: \"Active\", price: 79.99 },\n];\n```\n\n## Usage examples\n\n### Density options\n\nThe `density` prop controls row height and padding. Use `condensed` for\ndata-dense interfaces:\n\n```jsx live-dev\nconst App = () => {\n const [density, setDensity] = useState('default');\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n ];\n\n const rows = [\n { id: '1', name: 'Alice', role: 'Admin' },\n { id: '2', name: 'Bob', role: 'User' },\n { id: '3', name: 'Carol', role: 'Editor' },\n ];\n\n return (\n <Stack gap=\"400\">\n <Stack direction=\"row\" gap=\"300\">\n <Button\n variant={density === 'default' ? 'solid' : 'outline'}\n colorPalette=\"primary\"\n onPress={() => setDensity('default')}\n >\n Default\n </Button>\n <Button\n variant={density === 'condensed' ? 'solid' : 'outline'}\n colorPalette=\"primary\"\n onPress={() => setDensity('condensed')}\n >\n Condensed\n </Button>\n </Stack>\n <DataTable columns={columns} rows={rows} density={density} />\n </Stack>\n );\n}\n```\n\n### Sorting\n\nEnable sorting with `allowsSorting`. Individual columns can opt-out using\n`isSortable: false`:\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name, isSortable: true },\n { id: 'age', header: 'Age', accessor: (row) => row.age, isSortable: true },\n { id: 'role', header: 'Role', accessor: (row) => row.role, isSortable: false },\n ];\n\n const rows = [\n { id: '1', name: 'Alice', age: 30, role: 'Admin' },\n { id: '2', name: 'Bob', age: 25, role: 'User' },\n { id: '3', name: 'Carol', age: 28, role: 'Editor' },\n ];\n\n return (\n <Stack gap=\"300\">\n <Text>Click column headers to sort. The \"Role\" column is not sortable.</Text>\n <DataTable\n columns={columns}\n rows={rows}\n allowsSorting\n defaultSortDescriptor={{ column: 'name', direction: 'ascending' }}\n />\n </Stack>\n );\n}\n```\n\n### Controlled sorting\n\nFor external sort state management, use `sortDescriptor` and `onSortChange`:\n\n```jsx live-dev\nconst App = () => {\n const [sortDescriptor, setSortDescriptor] = useState({\n column: 'name',\n direction: 'ascending',\n });\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name, isSortable: true },\n { id: 'age', header: 'Age', accessor: (row) => row.age, isSortable: true },\n ];\n\n const rows = [\n { id: '1', name: 'Alice', age: 30 },\n { id: '2', name: 'Bob', age: 25 },\n { id: '3', name: 'Carol', age: 28 },\n ];\n\n return (\n <Stack gap=\"400\">\n <Text>\n Sorting by: <Text as=\"strong\">{sortDescriptor.column}</Text> ({sortDescriptor.direction})\n </Text>\n <Stack direction=\"row\" gap=\"300\">\n <Button\n variant=\"outline\"\n size=\"md\"\n colorPalette=\"primary\"\n onPress={() => setSortDescriptor({ column: 'name', direction: 'ascending' })}\n >\n Sort by Name (A-Z)\n </Button>\n <Button\n variant=\"outline\"\n size=\"md\"\n colorPalette=\"primary\"\n onPress={() => setSortDescriptor({ column: 'age', direction: 'descending' })}\n >\n Sort by Age (High-Low)\n </Button>\n </Stack>\n <DataTable\n columns={columns}\n rows={rows}\n allowsSorting\n sortDescriptor={sortDescriptor}\n onSortChange={setSortDescriptor}\n />\n </Stack>\n );\n}\n```\n\n### Row selection\n\nEnable selection with `selectionMode`. Supports `none`, `single`, or `multiple`:\n\n```jsx live-dev\nconst App = () => {\n const [selectedKeys, setSelectedKeys] = useState(new Set(['1']));\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'email', header: 'Email', accessor: (row) => row.email },\n ];\n\n const rows = [\n { id: '1', name: 'Alice', email: 'alice@example.com' },\n { id: '2', name: 'Bob', email: 'bob@example.com' },\n { id: '3', name: 'Carol', email: 'carol@example.com' },\n ];\n\n const selectedCount = selectedKeys === 'all' ? rows.length : selectedKeys.size;\n\n return (\n <Stack gap=\"400\">\n <Text>Selected: {selectedCount} row(s)</Text>\n <Stack direction=\"row\" gap=\"300\">\n <Button\n variant=\"outline\"\n colorPalette=\"primary\"\n size=\"md\"\n onPress={() => setSelectedKeys(new Set())}>\n Clear\n </Button>\n <Button size=\"md\" colorPalette=\"primary\" variant=\"outline\" onPress={() => setSelectedKeys('all')}>\n Select All\n </Button>\n </Stack>\n <DataTable\n columns={columns}\n rows={rows}\n selectionMode=\"multiple\"\n selectedKeys={selectedKeys}\n onSelectionChange={setSelectedKeys}\n />\n </Stack>\n );\n}\n```\n\n### Resizable columns\n\nEnable column resizing with `isResizable` on the table or individual columns:\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name, isResizable: true, width: 150 },\n { id: 'description', header: 'Description', accessor: (row) => row.description, isResizable: true },\n { id: 'status', header: 'Status', accessor: (row) => row.status, isResizable: false },\n ];\n\n const rows = [\n { id: '1', name: 'Product A', description: 'A great product for everyday use', status: 'Active' },\n { id: '2', name: 'Product B', description: 'Premium quality item', status: 'Draft' },\n ];\n\n return (\n <Stack gap=\"300\">\n <Text>Drag column borders to resize. The \"Status\" column is fixed width.</Text>\n <DataTable columns={columns} rows={rows} isResizable />\n </Stack>\n );\n}\n```\n\n### Sticky header with scrolling\n\nUse `maxHeight` to enable vertical scrolling with a sticky header:\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n ];\n\n const rows = Array.from({ length: 20 }, (_, i) => ({\n id: String(i + 1),\n name: `User ${i + 1}`,\n role: i % 3 === 0 ? 'Admin' : i % 3 === 1 ? 'Editor' : 'Viewer',\n }));\n\n return (\n <DataTable\n columns={columns}\n rows={rows}\n maxHeight=\"300px\"\n />\n );\n}\n```\n\n### Text truncation\n\nEnable `isTruncated` to truncate long text with ellipsis:\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'description', header: 'Description', accessor: (row) => row.description },\n ];\n\n const rows = [\n {\n id: '1',\n name: 'Product A',\n description: 'This is a very long description that should be truncated when displayed in the table cell to prevent layout issues.',\n },\n {\n id: '2',\n name: 'Product B',\n description: 'Another lengthy description that demonstrates how truncation works with ellipsis and maintains a clean table layout.',\n },\n ];\n\n return <DataTable columns={columns} rows={rows} isTruncated />;\n}\n```\n\n### Clickable rows\n\nUse `onRowClick` to handle row click events for navigation or detail views:\n\n```jsx live-dev\nconst App = () => {\n const [clickedRow, setClickedRow] = useState(null);\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'email', header: 'Email', accessor: (row) => row.email },\n ];\n\n const rows = [\n { id: '1', name: 'Alice', email: 'alice@example.com' },\n { id: '2', name: 'Bob', email: 'bob@example.com' },\n { id: '3', name: 'Carol', email: 'carol@example.com' },\n ];\n\n return (\n <Stack gap=\"400\">\n <Text>\n {clickedRow ? `Clicked: ${clickedRow.name}` : 'Click a row to see details'}\n </Text>\n <DataTable\n columns={columns}\n rows={rows}\n onRowClick={(row) => setClickedRow(row)}\n />\n </Stack>\n );\n}\n```\n\n### Search and filtering\n\nUse the `search` prop to filter rows across all visible columns:\n\n```jsx live-dev\nconst App = () => {\n const [search, setSearch] = useState('');\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n { id: 'department', header: 'Department', accessor: (row) => row.department },\n ];\n\n const rows = [\n { id: '1', name: 'Alice Johnson', role: 'Admin', department: 'Engineering' },\n { id: '2', name: 'Bob Smith', role: 'Developer', department: 'Engineering' },\n { id: '3', name: 'Carol Williams', role: 'Designer', department: 'Design' },\n { id: '4', name: 'David Brown', role: 'Manager', department: 'Sales' },\n ];\n\n return (\n <Stack gap=\"400\">\n <TextInput\n value={search}\n onChange={setSearch}\n placeholder=\"Search...\"\n width=\"300px\"\n aria-label=\"Search table\"\n />\n <DataTable columns={columns} rows={rows} search={search} />\n </Stack>\n );\n}\n```\n\n### Column visibility\n\nControl which columns are displayed using `visibleColumns`:\n\n```jsx live-dev\nconst App = () => {\n const [visibleColumns, setVisibleColumns] = useState(['name', 'email']);\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'email', header: 'Email', accessor: (row) => row.email },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n { id: 'department', header: 'Department', accessor: (row) => row.department },\n ];\n\n const rows = [\n { id: '1', name: 'Alice', email: 'alice@example.com', role: 'Admin', department: 'Engineering' },\n { id: '2', name: 'Bob', email: 'bob@example.com', role: 'User', department: 'Sales' },\n ];\n\n const toggleColumn = (colId) => {\n setVisibleColumns((prev) =>\n prev.includes(colId) ? prev.filter((id) => id !== colId) : [...prev, colId]\n );\n };\n\n return (\n <Stack gap=\"400\">\n <Stack direction=\"row\" gap=\"300\" wrap=\"wrap\">\n {columns.map((col) => (\n <Checkbox\n key={col.id}\n isSelected={visibleColumns.includes(col.id)}\n onChange={() => toggleColumn(col.id)}\n >\n {col.header}\n </Checkbox>\n ))}\n </Stack>\n <DataTable columns={columns} rows={rows} visibleColumns={visibleColumns} />\n </Stack>\n );\n}\n```\n\n### Nested rows\n\nUse `nestedKey` to enable expandable nested content:\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Category', accessor: (row) => row.name },\n { id: 'count', header: 'Products', accessor: (row) => row.count },\n ];\n\n const rows = [\n {\n id: '1',\n name: 'Electronics',\n count: 3,\n children: [\n { id: '1-1', name: 'Laptops', count: 15 },\n { id: '1-2', name: 'Phones', count: 25 },\n { id: '1-3', name: 'Tablets', count: 10 },\n ],\n },\n {\n id: '2',\n name: 'Clothing',\n count: 2,\n children: [\n { id: '2-1', name: 'Shirts', count: 50 },\n { id: '2-2', name: 'Pants', count: 30 },\n ],\n },\n { id: '3', name: 'Books', count: 0 },\n ];\n\n return (\n <Stack gap=\"300\">\n <Text>Click the expand button to view nested items.</Text>\n <DataTable columns={columns} rows={rows} nestedKey=\"children\" />\n </Stack>\n );\n}\n```\n\n### Nested content via render function\n\nUse `renderNestedContent` to render a uniform component template below each row\nwhen expanded. This is ideal when every row should show the same layout with its\nown data. For per-row heterogeneous content, use `nestedKey` instead.\n\nAn expand column with a chevron button is shown by default. Set\n`allowsExpandColumn={false}` to hide the chevron and expand via row click\ninstead. This works alongside selection and other features.\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Product', accessor: (row) => row.name },\n { id: 'category', header: 'Category', accessor: (row) => row.category },\n { id: 'price', header: 'Price', accessor: (row) => row.price },\n ];\n\n const rows = [\n { id: '1', name: 'Laptop', category: 'Electronics', price: '$999', description: 'A high-performance laptop with 16GB RAM and 512GB SSD.' },\n { id: '2', name: 'Headphones', category: 'Audio', price: '$199', description: 'Noise-cancelling wireless headphones with 30-hour battery life.' },\n { id: '3', name: 'Monitor', category: 'Electronics', price: '$449', description: '27-inch 4K monitor with HDR support and USB-C connectivity.' },\n ];\n\n return (\n <Stack gap=\"300\">\n <Text>Click the expand button to toggle nested content.</Text>\n <DataTable\n columns={columns}\n rows={rows}\n renderNestedContent={(row) => (\n <Box p=\"400\">\n <Heading as=\"h4\" size=\"sm\">{row.name}</Heading>\n <Text>{row.description}</Text>\n </Box>\n )}\n />\n </Stack>\n );\n}\n```\n\n### Overriding renderNestedContent with nestedKey\n\nWhen both `renderNestedContent` and `nestedKey` are provided, rows that have\ndata at `row[nestedKey]` render that content instead of the default template.\nRows without `nestedKey` data fall through to `renderNestedContent`. This lets\nyou define a default layout for most rows while providing custom content for\nspecific ones.\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n ];\n\n const rows = [\n { id: '1', name: 'Alice', role: 'Admin' },\n { id: '2', name: 'Bob', role: 'User', details: <Box p=\"400\" bg=\"yellow.2\"><Text fontWeight=\"bold\">Custom content for Bob</Text></Box> },\n { id: '3', name: 'Carol', role: 'User' },\n ];\n\n return (\n <DataTable\n columns={columns}\n rows={rows}\n nestedKey=\"details\"\n renderNestedContent={(row) => (\n <Box p=\"400\">\n <Text>Default: {row.name} ({row.role})</Text>\n </Box>\n )}\n />\n );\n}\n```\n\n### With footer\n\nAdd pagination or summary content using the `footer` prop:\n\n```jsx live-dev\nconst App = () => {\n const [page, setPage] = useState(1);\n const pageSize = 3;\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n ];\n\n const allRows = Array.from({ length: 10 }, (_, i) => ({\n id: String(i + 1),\n name: `User ${i + 1}`,\n role: i % 2 === 0 ? 'Admin' : 'User',\n }));\n\n const totalPages = Math.ceil(allRows.length / pageSize);\n const rows = allRows.slice((page - 1) * pageSize, page * pageSize);\n\n return (\n <DataTable\n columns={columns}\n rows={rows}\n footer={\n <Stack direction=\"row\" justify=\"space-between\" align=\"center\" width=\"full\" px=\"400\" py=\"300\">\n <Text fontSize=\"sm\">\n Showing {(page - 1) * pageSize + 1}-{Math.min(page * pageSize, allRows.length)} of {allRows.length}\n </Text>\n <Stack direction=\"row\" gap=\"200\">\n <Button\n size=\"md\"\n colorPalette=\"primary\"\n variant=\"outline\"\n onPress={() => setPage((p) => Math.max(1, p - 1))}\n isDisabled={page === 1}\n >\n Previous\n </Button>\n <Button\n size=\"md\"\n variant=\"outline\"\n colorPalette=\"primary\"\n onPress={() => setPage((p) => Math.min(totalPages, p + 1))}\n isDisabled={page === totalPages}\n >\n Next\n </Button>\n </Stack>\n </Stack>\n }\n />\n );\n}\n```\n\n### Disabled rows\n\nUse `disabledKeys` to prevent interaction with specific rows:\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'status', header: 'Status', accessor: (row) => row.status },\n ];\n\n const rows = [\n { id: '1', name: 'Active Item', status: 'Available' },\n { id: '2', name: 'Disabled Item', status: 'Unavailable' },\n { id: '3', name: 'Another Active', status: 'Available' },\n ];\n\n return (\n <DataTable\n columns={columns}\n rows={rows}\n selectionMode=\"multiple\"\n disabledKeys={new Set(['2'])}\n />\n );\n}\n```\n\n### Row pinning\n\nPin important rows to the top of the table:\n\n```jsx live-dev\nconst App = () => {\n const [pinnedRows, setPinnedRows] = useState(new Set(['1']));\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'priority', header: 'Priority', accessor: (row) => row.priority },\n ];\n\n const rows = [\n { id: '1', name: 'High Priority Task', priority: 'High' },\n { id: '2', name: 'Normal Task', priority: 'Normal' },\n { id: '3', name: 'Low Priority Task', priority: 'Low' },\n ];\n\n return (\n <Stack gap=\"400\">\n <Text>Row 1 is pinned to the top. Pinned rows stay at top when sorting.</Text>\n <DataTable\n columns={columns}\n rows={rows}\n pinnedRows={pinnedRows}\n onPinToggle={(rowId) => {\n setPinnedRows((prev) => {\n const next = new Set(prev);\n if (next.has(rowId)) {\n next.delete(rowId);\n } else {\n next.add(rowId);\n }\n return next;\n });\n }}\n allowsSorting\n />\n </Stack>\n );\n}\n```\n\n### Hiding the pin column\n\nSet `allowsPinning={false}` to remove the pin column entirely. The pin toggle\nbuttons and pin column header will not render:\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n ];\n\n const rows = [\n { id: '1', name: 'Alice', role: 'Admin' },\n { id: '2', name: 'Bob', role: 'User' },\n { id: '3', name: 'Carol', role: 'Editor' },\n ];\n\n return <DataTable columns={columns} rows={rows} allowsPinning={false} />;\n}\n```\n\n### Hiding the expand column\n\nSet `allowsExpandColumn={false}` to hide the expand chevron column. When no\n`onRowClick` handler is provided, rows with nested content can still be expanded\nby clicking anywhere on the row:\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Category', accessor: (row) => row.name },\n { id: 'count', header: 'Products', accessor: (row) => row.count },\n ];\n\n const rows = [\n {\n id: '1',\n name: 'Electronics',\n count: 3,\n children: [\n { id: '1-1', name: 'Laptops', count: 15 },\n { id: '1-2', name: 'Phones', count: 25 },\n ],\n },\n {\n id: '2',\n name: 'Clothing',\n count: 2,\n children: [\n { id: '2-1', name: 'Shirts', count: 50 },\n ],\n },\n { id: '3', name: 'Books', count: 0 },\n ];\n\n return (\n <Stack gap=\"300\">\n <Text>Click the expand button to toggle nested content.</Text>\n <DataTable\n columns={columns}\n rows={rows}\n nestedKey=\"children\"\n allowsExpandColumn={false}\n />\n </Stack>\n );\n}\n```\n\n### Hidden expand column with custom row click\n\nWhen `onRowClick` is provided alongside `allowsExpandColumn={false}`, the\n`onRowClick` handler takes precedence and the automatic expand-on-click behavior\nis disabled. To still support expansion, use controlled expansion state:\n\n```jsx live-dev\nconst App = () => {\n const [expanded, setExpanded] = useState(new Set());\n\n const columns = [\n { id: 'name', header: 'Category', accessor: (row) => row.name },\n { id: 'count', header: 'Products', accessor: (row) => row.count },\n ];\n\n const rows = [\n {\n id: '1',\n name: 'Electronics',\n count: 3,\n children: [\n { id: '1-1', name: 'Laptops', count: 15 },\n { id: '1-2', name: 'Phones', count: 25 },\n ],\n },\n {\n id: '2',\n name: 'Clothing',\n count: 2,\n children: [\n { id: '2-1', name: 'Shirts', count: 50 },\n ],\n },\n { id: '3', name: 'Books', count: 0 },\n ];\n\n return (\n <Stack gap=\"300\">\n <Text>Click a row to toggle expansion and trigger custom logic.</Text>\n <DataTable\n columns={columns}\n rows={rows}\n nestedKey=\"children\"\n allowsExpandColumn={false}\n expandedRows={expanded}\n onExpandRowsChange={setExpanded}\n onRowClick={(row) => {\n const next = new Set(expanded);\n if (next.has(row.id)) {\n next.delete(row.id);\n } else {\n next.add(row.id);\n }\n setExpanded(next);\n }}\n />\n </Stack>\n );\n}\n```\n\n## Drag and drop\n\nDataTable supports row reordering via drag and drop using the shared `useDragAndDrop` hook. Pass `dragAndDropHooks` to `DataTable` to enable it.\n\n### Reorderable rows\n\n```jsx live-dev\nimport { DataTable, useDragAndDrop, createArrayHandlers } from '@commercetools/nimbus';\n\nconst App = () => {\n const [rows, setRows] = useState([\n { id: '1', name: 'Alice', role: 'Admin' },\n { id: '2', name: 'Bob', role: 'User' },\n { id: '3', name: 'Carol', role: 'Manager' },\n ]);\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n ];\n\n const { dragAndDropHooks } = useDragAndDrop({\n ...createArrayHandlers(setRows, (row) => row.id),\n });\n\n return (\n <DataTable\n columns={columns}\n rows={rows}\n dragAndDropHooks={dragAndDropHooks}\n />\n );\n}\n```\n\nWhen `dragAndDropHooks` is provided:\n- A **drag handle column** with a grip icon is automatically added as the first column\n- Rows can be dragged using mouse or touch from anywhere on the row\n- **Keyboard accessible**: focus the drag handle, press Enter to start drag, use arrow keys to move, Enter to drop, Escape to cancel\n- A **drop indicator** shows where the row will be placed\n- **Double-click text selection** is preserved via programmatic word selection\n- Drag and drop works alongside **row selection** and **clickable rows**\n\n> **Note:** When combining drag and drop with **row pinning**, pinned rows can\n> be reordered among themselves and non-pinned rows among themselves, but items\n> cannot be dragged between pinned and non-pinned groups.\n\n### Namespace isolation\n\nUse `dragNamespace` to prevent drag between unrelated tables:\n\n```tsx\nconst { dragAndDropHooks } = useDragAndDrop({\n dragNamespace: 'my-table',\n ...createArrayHandlers(setRows, (row) => row.id),\n});\n```\n\nSee the [`useDragAndDrop` hook documentation](/hooks/use-drag-and-drop) for external drop support, outgoing formats, and helper functions.\n\n## Compound component API\n\nFor advanced customization, use the compound component pattern:\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n ];\n\n const rows = [\n { id: '1', name: 'Alice', role: 'Admin' },\n { id: '2', name: 'Bob', role: 'User' },\n ];\n\n return (\n <DataTable.Root columns={columns} rows={rows} allowsSorting>\n <DataTable.Manager />\n <DataTable.Table aria-label=\"Users table\">\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n <DataTable.Footer>\n <Stack direction=\"row\" justify=\"center\" py=\"300\">\n <Text fontSize=\"sm\" color=\"neutral.11\">Custom footer content</Text>\n </Stack>\n </DataTable.Footer>\n </DataTable.Root>\n );\n}\n```\n\n### Available sub-components\n\n| Component | Description |\n| -------------------- | ------------------------------------------------------------------------------------------------------- |\n| `DataTable.Root` | Provider component that manages state and context |\n| `DataTable.Table` | The main table element |\n| `DataTable.Header` | Table header with column headers — accepts render function children for custom columns |\n| `DataTable.Body` | Table body containing rows — accepts render function children for custom row rendering |\n| `DataTable.Row` | Individual row component — accepts render function children for custom cell rendering |\n| `DataTable.Cell` | Individual cell component for custom cell content |\n| `DataTable.Column` | Individual column header component with sorting support |\n| `DataTable.Footer` | Footer section for pagination or summaries |\n| `DataTable.Manager` | Settings panel for column visibility and layout - [see detailed docs below](#advanced-datatablemanager) |\n\n### Compound composition\n\nThe compound sub-components (`Body`, `Header`, `Row`) accept render function\nchildren for fine-grained control over rendering. When no children are provided,\nthe default rendering is used. Internal columns (selection checkboxes, expand\nbuttons, pin toggles) are always rendered automatically — custom children only\nreplace the **data columns/cells**.\n\n> **Reactivity constraint:** render functions passed to `DataTable.Body` and\n> `DataTable.Row` are only re-invoked when row data, expansion, or pin state\n> changes. They should derive output purely from the `row` and `renderProps`\n> arguments — closing over external React state (e.g. a `useState` value from\n> a parent component) will produce stale renders. Route reactive values through\n> the `rows` array or row keys instead. If you need rows to re-render in\n> response to additional values, pass a `dependencies` array to\n> `DataTable.Body` — those values are merged with the internal dependency\n> tracking so expansion and pin invalidation are never lost.\n\n#### Custom row styling\n\nPass a render function to `DataTable.Body` to control how each row renders.\nThe function receives the row data and render props (`isExpanded`, `isPinned`,\netc.). Spread the render props onto `DataTable.Row` to preserve expansion and\npinning behavior:\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'email', header: 'Email', accessor: (row) => row.email },\n { id: 'department', header: 'Department', accessor: (row) => row.department },\n ];\n\n const rows = [\n { id: '1', name: 'Alice', email: 'alice@example.com', department: 'Eng' },\n { id: '2', name: 'Bob', email: 'bob@example.com', department: 'Design' },\n { id: '3', name: 'Carol', email: 'carol@example.com', department: 'PM' },\n ];\n\n return (\n <DataTable.Root columns={columns} rows={rows}>\n <DataTable.Table aria-label=\"Team members with row styling\">\n <DataTable.Header />\n <DataTable.Body>\n {(row, rowRenderProps) => (\n <DataTable.Row\n row={row}\n {...rowRenderProps}\n bg={\n row.department === 'Eng'\n ? 'primary.2'\n : row.department === 'Design'\n ? 'warning.2'\n : undefined\n }\n />\n )}\n </DataTable.Body>\n </DataTable.Table>\n </DataTable.Root>\n );\n}\n```\n\n#### Custom cell rendering\n\nPass a render function to `DataTable.Row` to control individual cells. The\nfunction receives `columns`, `row`, and `isDisabled`. You are responsible for\nrendering a `DataTable.Cell` for each column:\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'email', header: 'Email', accessor: (row) => row.email },\n { id: 'department', header: 'Department', accessor: (row) => row.department },\n ];\n\n const rows = [\n { id: '1', name: 'Alice', email: 'alice@example.com', department: 'Eng' },\n { id: '2', name: 'Bob', email: 'bob@example.com', department: 'Design' },\n { id: '3', name: 'Carol', email: 'carol@example.com', department: 'PM' },\n ];\n\n return (\n <DataTable.Root columns={columns} rows={rows}>\n <DataTable.Table aria-label=\"Team members with custom cells\">\n <DataTable.Header />\n <DataTable.Body>\n {(row, rowRenderProps) => (\n <DataTable.Row row={row} {...rowRenderProps}>\n {({ columns: cols }) =>\n cols.map((col) => (\n <DataTable.Cell\n key={col.id}\n data-column-id={col.id}\n fontWeight={col.id === 'name' ? 'bold' : undefined}\n bg={\n col.id === 'department' && row.department === 'Eng'\n ? 'primary.2'\n : undefined\n }\n >\n {col.accessor(row)}\n </DataTable.Cell>\n ))\n }\n </DataTable.Row>\n )}\n </DataTable.Body>\n </DataTable.Table>\n </DataTable.Root>\n );\n}\n```\n\n#### Custom column headers\n\nPass a render function to `DataTable.Header` to control how data column headers\nrender. The function receives the active columns array and `{ allowsSorting }`.\nInternal columns (selection, expand, pin) are still rendered automatically:\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'email', header: 'Email', accessor: (row) => row.email },\n { id: 'department', header: 'Department', accessor: (row) => row.department },\n ];\n\n const rows = [\n { id: '1', name: 'Alice', email: 'alice@example.com', department: 'Eng' },\n { id: '2', name: 'Bob', email: 'bob@example.com', department: 'Design' },\n { id: '3', name: 'Carol', email: 'carol@example.com', department: 'PM' },\n ];\n\n return (\n <DataTable.Root columns={columns} rows={rows} allowsSorting>\n <DataTable.Table aria-label=\"Team members with custom headers\">\n <DataTable.Header>\n {({ columns: cols, allowsSorting: sorting }) =>\n cols.map((col) => (\n <DataTable.Column\n key={col.id}\n id={col.id}\n column={col}\n allowsSorting={sorting}\n isRowHeader\n minWidth={col.minWidth ?? 150}\n bg={col.id === 'name' ? 'primary.2' : undefined}\n >\n {col.id === 'name' ? (\n <Text fontWeight=\"bold\" color=\"primary.11\">\n {col.header}\n </Text>\n ) : (\n <span data-multiline-header>{col.header}</span>\n )}\n </DataTable.Column>\n ))\n }\n </DataTable.Header>\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n );\n}\n```\n\n#### Compound composition with features\n\nCustom children work seamlessly with selection, expansion, sorting, and other\nDataTable features. Internal columns are always handled automatically:\n\n```jsx live-dev\nconst App = () => {\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'email', header: 'Email', accessor: (row) => row.email },\n { id: 'department', header: 'Department', accessor: (row) => row.department },\n ];\n\n const rows = [\n { id: '1', name: 'Alice', email: 'alice@example.com', department: 'Eng' },\n { id: '2', name: 'Bob', email: 'bob@example.com', department: 'Design' },\n { id: '3', name: 'Carol', email: 'carol@example.com', department: 'PM' },\n ];\n\n return (\n <DataTable.Root\n columns={columns}\n rows={rows}\n selectionMode=\"multiple\"\n renderNestedContent={(row) => (\n <Box p=\"400\">\n <Text fontWeight=\"bold\">Details for {row.name}</Text>\n <Text>Email: {row.email}</Text>\n </Box>\n )}\n >\n <DataTable.Table aria-label=\"Team members with selection and expansion\">\n <DataTable.Header />\n <DataTable.Body>\n {(row, rowRenderProps) => (\n <DataTable.Row\n row={row}\n {...rowRenderProps}\n bg={row.department === 'Eng' ? 'primary.2' : undefined}\n />\n )}\n </DataTable.Body>\n </DataTable.Table>\n </DataTable.Root>\n );\n}\n```\n\n## Advanced: DataTable.Manager\n\nThe `DataTable.Manager` component provides a settings drawer for managing table\nconfiguration. It enables users to control column visibility, reorder columns,\nand adjust layout settings through an intuitive interface.\n\n### Manager overview\n\nDataTable.Manager must be used within a DataTable.Root to access table state and\ncolumns. It renders as a settings button that opens a drawer with multiple\nconfiguration tabs:\n\n```jsx live-dev\nconst App = () => {\n const [visibleColumns, setVisibleColumns] = useState(['name', 'email', 'role']);\n const [isTruncated, setIsTruncated] = useState(false);\n const [density, setDensity] = useState('default');\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'email', header: 'Email', accessor: (row) => row.email },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n ];\n\n const rows = [\n { id: '1', name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin' },\n { id: '2', name: 'Bob Smith', email: 'bob@example.com', role: 'User' },\n { id: '3', name: 'Carol Williams', email: 'carol@example.com', role: 'Editor' },\n ];\n\n const handleSettingsChange = (action) => {\n if (action === 'toggleTextVisibility') {\n setIsTruncated(!isTruncated);\n } else if (action === 'toggleRowDensity') {\n setDensity(density === 'default' ? 'condensed' : 'default');\n }\n };\n\n return (\n <DataTable.Root\n columns={columns}\n rows={rows}\n visibleColumns={visibleColumns}\n onColumnsChange={(updatedColumns) => {\n setVisibleColumns(updatedColumns.map(col => col.id));\n }}\n isTruncated={isTruncated}\n density={density}\n onSettingsChange={handleSettingsChange}\n >\n <Stack direction=\"row\" justifyContent=\"space-between\" mb=\"400\">\n <Text fontSize=\"lg\" fontWeight=\"600\">Users</Text>\n <DataTable.Manager />\n </Stack>\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n );\n}\n```\n\n### Manager: Column visibility\n\nThe Manager provides a dual-list interface for managing visible and hidden\ncolumns. Users can drag columns between lists, reorder visible columns, and\nsearch hidden columns:\n\n```jsx live-dev\nconst App = () => {\n const [visibleColumns, setVisibleColumns] = useState(['name', 'email']);\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'email', header: 'Email', accessor: (row) => row.email },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n { id: 'department', header: 'Department', accessor: (row) => row.department },\n ];\n\n const rows = [\n { id: '1', name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin', department: 'Engineering' },\n { id: '2', name: 'Bob Smith', email: 'bob@example.com', role: 'User', department: 'Sales' },\n ];\n\n return (\n <DataTable.Root\n columns={columns}\n rows={rows}\n visibleColumns={visibleColumns}\n onColumnsChange={(updatedColumns) => {\n setVisibleColumns(updatedColumns.map(col => col.id));\n }}\n >\n <Stack direction=\"row\" justifyContent=\"space-between\" mb=\"400\">\n <Text fontSize=\"lg\" fontWeight=\"600\">Team Members</Text>\n <DataTable.Manager />\n </Stack>\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n );\n}\n```\n\n### Manager: Layout settings\n\nThe Layout Settings tab allows users to control text truncation and row density.\nThese settings update the table's visual appearance in real-time:\n\n```jsx live-dev\nconst App = () => {\n const [visibleColumns, setVisibleColumns] = useState(['name', 'description']);\n const [isTruncated, setIsTruncated] = useState(false);\n const [density, setDensity] = useState('default');\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'description', header: 'Description', accessor: (row) => row.description },\n ];\n\n const rows = [\n {\n id: '1',\n name: 'Product A',\n description: 'This is a very long description that will demonstrate text truncation when enabled'\n },\n {\n id: '2',\n name: 'Product B',\n description: 'Another lengthy description to show how the layout settings affect content display'\n },\n ];\n\n const handleSettingsChange = (action) => {\n if (action === 'toggleTextVisibility') {\n setIsTruncated(!isTruncated);\n } else if (action === 'toggleRowDensity') {\n setDensity(density === 'default' ? 'condensed' : 'default');\n }\n };\n\n return (\n <DataTable.Root\n columns={columns}\n rows={rows}\n visibleColumns={visibleColumns}\n onColumnsChange={(updatedColumns) => {\n setVisibleColumns(updatedColumns.map(col => col.id));\n }}\n isTruncated={isTruncated}\n density={density}\n onSettingsChange={handleSettingsChange}\n >\n <Stack direction=\"row\" justifyContent=\"space-between\" mb=\"400\">\n <Text fontSize=\"lg\" fontWeight=\"600\">Products</Text>\n <DataTable.Manager />\n </Stack>\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n );\n}\n```\n\n### Manager: Custom settings tab\n\nAdd a custom settings tab by providing the `customSettings` prop to\nDataTable.Root. This allows extending the Manager with application-specific\nconfiguration:\n\n```jsx live-dev\nconst App = () => {\n const [visibleColumns, setVisibleColumns] = useState(['name', 'status']);\n const [filterStatus, setFilterStatus] = useState('all');\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'status', header: 'Status', accessor: (row) => row.status },\n ];\n\n const allRows = [\n { id: '1', name: 'Task A', status: 'Active' },\n { id: '2', name: 'Task B', status: 'Inactive' },\n { id: '3', name: 'Task C', status: 'Active' },\n ];\n\n const filteredRows = filterStatus === 'all'\n ? allRows\n : allRows.filter(row => row.status.toLowerCase() === filterStatus);\n\n const customSettingsPanel = (\n <Stack direction=\"column\" gap=\"400\">\n <Text fontWeight=\"600\">Filter Options</Text>\n <ToggleButtonGroup.Root\n selectedKeys={[filterStatus]}\n onSelectionChange={(keys) => setFilterStatus(Array.from(keys)[0])}\n >\n <ToggleButton id=\"all\">All</ToggleButton>\n <ToggleButton id=\"active\">Active</ToggleButton>\n <ToggleButton id=\"inactive\">Inactive</ToggleButton>\n </ToggleButtonGroup.Root>\n </Stack>\n );\n\n return (\n <DataTable.Root\n columns={columns}\n rows={filteredRows}\n visibleColumns={visibleColumns}\n onColumnsChange={(updatedColumns) => {\n setVisibleColumns(updatedColumns.map(col => col.id));\n }}\n customSettings={{\n label: 'Filters',\n icon: <Icons.FilterList />,\n panel: customSettingsPanel,\n }}\n >\n <Stack direction=\"row\" justifyContent=\"space-between\" mb=\"400\">\n <Text fontSize=\"lg\" fontWeight=\"600\">Tasks</Text>\n <DataTable.Manager />\n </Stack>\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n );\n}\n```\n\n### Manager: Controlled column state with persistence\n\nManage column configuration externally for features like saving user\npreferences:\n\n```jsx live-dev\nconst App = () => {\n const [visibleColumns, setVisibleColumns] = useState(['name', 'email', 'role']);\n\n const columns = [\n { id: 'name', header: 'Name', accessor: (row) => row.name },\n { id: 'email', header: 'Email', accessor: (row) => row.email },\n { id: 'role', header: 'Role', accessor: (row) => row.role },\n { id: 'department', header: 'Department', accessor: (row) => row.department },\n { id: 'location', header: 'Location', accessor: (row) => row.location },\n ];\n\n const rows = [\n {\n id: '1',\n name: 'Alice Johnson',\n email: 'alice@example.com',\n role: 'Admin',\n department: 'Engineering',\n location: 'New York'\n },\n ];\n\n const handleColumnsChange = (updatedColumns) => {\n const newVisibleColumns = updatedColumns.map(col => col.id);\n setVisibleColumns(newVisibleColumns);\n\n // Example: Save to localStorage\n localStorage.setItem('tableColumns', JSON.stringify(newVisibleColumns));\n };\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <DataTable.Root\n columns={columns}\n rows={rows}\n visibleColumns={visibleColumns}\n onColumnsChange={handleColumnsChange}\n >\n <Stack direction=\"row\" justifyContent=\"space-between\" mb=\"400\">\n <Text fontSize=\"lg\" fontWeight=\"600\">Team Directory</Text>\n <DataTable.Manager />\n </Stack>\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n <Text fontSize=\"sm\" color=\"gray.9\">\n Visible columns: {visibleColumns.join(', ')}\n </Text>\n </Stack>\n );\n}\n```\n\n### Manager: Context requirements\n\nDataTable.Manager **must** be used within a DataTable.Root component. It relies\non the DataTable context for:\n\n- Column configuration (`columns`, `visibleColumns`)\n- Layout settings (`isTruncated`, `density`)\n- Callbacks (`onColumnsChange`, `onSettingsChange`)\n- Custom settings configuration\n\nUsing Manager outside of DataTable.Root will throw an error.\n\n## Component requirements\n\n## Accessibility\n\nThe DataTable uses React Aria's Table components for full accessibility support:\n\n- **ARIA grid pattern**: The table implements the ARIA grid pattern with proper\n roles\n- **Keyboard navigation**: Full keyboard support with arrow keys, Tab, Enter,\n and Space\n- **Screen reader support**: Proper announcements for sorting, selection, and\n expansion\n\nAlways provide an accessible label for the table:\n\n```tsx\n<DataTable columns={columns} rows={rows} aria-label=\"Product inventory table\" />\n```\n\nOr with the compound component API:\n\n```tsx\n<DataTable.Root columns={columns} rows={rows}>\n <DataTable.Table aria-label=\"Product inventory table\">\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n</DataTable.Root>\n```\n\n#### Persistent ID\n\nIf your use case requires tracking and analytics for this component, it is good\npractice to add a **persistent**, **unique** id to the component:\n\n```tsx\nconst PERSISTENT_ID = \"product-inventory-table\";\n\nexport const Example = () => (\n <DataTable\n id={PERSISTENT_ID}\n columns={columns}\n rows={rows}\n aria-label=\"Product inventory table\"\n />\n);\n```\n\n#### Keyboard navigation\n\nThe component supports full keyboard interaction:\n\n- `Tab` / `Shift+Tab`: Move focus between interactive elements\n- `Arrow keys`: Navigate between cells and rows\n- `Enter` / `Space`: Activate sorting, selection, or expansion\n- `Escape`: Cancel current interaction\n\nThe Manager drawer also supports keyboard navigation:\n\n- `Tab` / `Shift+Tab`: Navigate between the settings button, tabs, and controls\n- `Enter` / `Space`: Open the Manager drawer, activate tabs, toggle buttons\n- `Arrow keys`: Navigate between tab panels, reorder items in drag-and-drop\n lists\n- `Escape`: Close the Manager drawer\n- Drag-and-drop keyboard operations: `Enter` / `Space` to pick up, arrow keys to move,\n `Enter` / `Space` to drop\n\n## API reference\n\n<PropsTable id=\"DataTable\" />\n\n### DataTableColumnItem\n\n```tsx\ntype DataTableColumnItem<T extends object = Record<string, unknown>> = {\n id: string; // Unique column identifier\n header: ReactNode; // Column header content\n accessor: (row: T) => ReactNode; // Function to extract cell value\n render?: (cell: {\n // Custom cell renderer\n value: unknown;\n row: T;\n column: DataTableColumnItem<T>;\n }) => ReactNode;\n isResizable?: boolean; // Enable column resizing\n width?: number | null; // Initial width in pixels\n defaultWidth?: number | null; // Default width\n minWidth?: number | null; // Minimum width when resizing\n maxWidth?: number | null; // Maximum width when resizing\n sticky?: boolean; // Stick column to edge\n isSortable?: boolean; // Enable sorting for column\n isRowHeader?: boolean; // Mark as row header (accessibility)\n headerIcon?: ReactNode; // Icon in column header\n};\n```\n\n### DataTableRowItem\n\n```tsx\ntype DataTableRowItem<T extends object = Record<string, unknown>> = T & {\n id: string; // Unique row identifier (required)\n isDisabled?: boolean; // Disable row interactions\n [key: string]: unknown;\n};\n```\n\n### SortDescriptor\n\n```tsx\ntype SortDescriptor = {\n column: string; // Column ID to sort by\n direction: \"ascending\" | \"descending\"; // Sort direction\n};\n```\n\n### DataTableColumnRenderProps\n\nPassed to `DataTable.Header` children for custom column rendering:\n\n```tsx\ntype DataTableColumnRenderProps = {\n columns: DataTableColumnItem[]; // Active columns to render\n allowsSorting: boolean; // Whether sorting is enabled\n};\n```\n\n### DataTableRowRenderProps\n\nPassed to `DataTable.Body` children alongside the row data:\n\n```tsx\ntype DataTableRowRenderProps = {\n isExpanded: boolean;\n isPinned: boolean;\n isFirstPinned: boolean;\n isLastPinned: boolean;\n isSinglePinned: boolean;\n};\n```\n\n### DataTableCellRenderProps\n\nPassed to `DataTable.Row` children for custom cell rendering:\n\n```tsx\ntype DataTableCellRenderProps<T extends object = Record<string, unknown>> = {\n columns: DataTableColumnItem<T>[]; // Active columns to render\n row: DataTableRowItem<T>; // Current row data\n isDisabled: boolean; // Whether the row is disabled\n};\n```\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using DataTable\nin your application. As the component's internal functionality is already tested\nby Nimbus, these patterns help you verify your integration and\napplication-specific logic.\n\n### Basic Rendering Tests\n\nVerify the DataTable renders with correct structure\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable - Basic rendering\", () => {\n it(\"renders table with grid role\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} />\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"grid\")).toBeInTheDocument();\n });\n\n it(\"renders column headers\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Name\")).toBeInTheDocument();\n expect(screen.getByText(\"Email\")).toBeInTheDocument();\n expect(screen.getByText(\"Role\")).toBeInTheDocument();\n });\n\n it(\"renders all data rows\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Alice Johnson\")).toBeInTheDocument();\n expect(screen.getByText(\"Bob Smith\")).toBeInTheDocument();\n expect(screen.getByText(\"Carol Williams\")).toBeInTheDocument();\n });\n\n it(\"renders correct number of rows\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} />\n </NimbusProvider>\n );\n\n // 1 header row + 3 data rows\n const allRows = screen.getAllByRole(\"row\");\n expect(allRows.length).toBe(4);\n });\n});\n```\n\n### Sorting Tests\n\nTest column sorting functionality\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable - Sorting\", () => {\n it(\"renders sortable column headers when allowsSorting is true\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} allowsSorting />\n </NimbusProvider>\n );\n\n const nameHeader = screen\n .getByText(\"Name\")\n .closest('[role=\"columnheader\"]');\n expect(nameHeader).toBeInTheDocument();\n });\n\n it(\"calls onSortChange when column header is clicked\", async () => {\n const user = userEvent.setup();\n const handleSortChange = vi.fn();\n\n render(\n <NimbusProvider>\n <DataTable\n columns={columns}\n rows={rows}\n allowsSorting\n onSortChange={handleSortChange}\n />\n </NimbusProvider>\n );\n\n await user.click(screen.getByText(\"Name\"));\n\n expect(handleSortChange).toHaveBeenCalledWith({\n column: \"name\",\n direction: expect.any(String),\n });\n });\n\n it(\"displays sort indicator on sorted column\", async () => {\n const user = userEvent.setup();\n\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} allowsSorting />\n </NimbusProvider>\n );\n\n await user.click(screen.getByText(\"Name\"));\n\n await waitFor(() => {\n const columnHeader = screen\n .getByText(\"Name\")\n .closest('[role=\"columnheader\"]');\n expect(columnHeader).toHaveAttribute(\"aria-sort\");\n });\n });\n});\n```\n\n### Selection Tests\n\nTest row selection functionality\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable - Selection\", () => {\n it(\"renders checkboxes when selectionMode is multiple\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} selectionMode=\"multiple\" />\n </NimbusProvider>\n );\n\n const checkboxes = screen.getAllByRole(\"checkbox\");\n // 1 select-all checkbox + 3 row checkboxes\n expect(checkboxes.length).toBe(4);\n });\n\n it(\"allows selecting a row\", async () => {\n const user = userEvent.setup();\n const handleSelectionChange = vi.fn();\n\n render(\n <NimbusProvider>\n <DataTable\n columns={columns}\n rows={rows}\n selectionMode=\"multiple\"\n onSelectionChange={handleSelectionChange}\n />\n </NimbusProvider>\n );\n\n const checkboxes = screen.getAllByRole(\"checkbox\");\n await user.click(checkboxes[1]); // First data row checkbox\n\n expect(handleSelectionChange).toHaveBeenCalled();\n });\n\n it(\"allows selecting all rows with header checkbox\", async () => {\n const user = userEvent.setup();\n const handleSelectionChange = vi.fn();\n\n render(\n <NimbusProvider>\n <DataTable\n columns={columns}\n rows={rows}\n selectionMode=\"multiple\"\n onSelectionChange={handleSelectionChange}\n />\n </NimbusProvider>\n );\n\n const selectAllCheckbox = screen.getAllByRole(\"checkbox\")[0];\n await user.click(selectAllCheckbox);\n\n await waitFor(() => {\n expect(handleSelectionChange).toHaveBeenCalledWith(\"all\");\n });\n });\n\n it(\"supports single selection mode\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} selectionMode=\"single\" />\n </NimbusProvider>\n );\n\n const radios = screen.getAllByRole(\"checkbox\");\n expect(radios.length).toBe(3);\n });\n});\n```\n\n### Row Interaction Tests\n\nTest row click and interaction functionality\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable - Row interactions\", () => {\n it(\"calls onRowClick when a row is clicked\", async () => {\n const user = userEvent.setup();\n const handleRowClick = vi.fn();\n\n render(\n <NimbusProvider>\n <DataTable\n columns={columns}\n rows={rows}\n search=\"\"\n onRowClick={handleRowClick}\n />\n </NimbusProvider>\n );\n\n const allRows = screen.getAllByRole(\"row\");\n const firstDataRow = allRows[1]; // Second data row (index 2 because of header)\n const clickableCell = within(firstDataRow).getAllByRole(\"rowheader\");\n await user.click(clickableCell[0]);\n\n await waitFor(() => {\n expect(handleRowClick).toHaveBeenCalled();\n });\n });\n\n it(\"applies disabled state to specified rows\", () => {\n render(\n <NimbusProvider>\n <DataTable\n columns={columns}\n rows={rows}\n selectionMode=\"multiple\"\n disabledKeys={new Set([\"2\"])}\n />\n </NimbusProvider>\n );\n\n const allRows = screen.getAllByRole(\"row\");\n const disabledRow = allRows[2]; // Second data row (index 2 because of header)\n const checkbox = within(disabledRow).getByRole(\"checkbox\");\n expect(checkbox).toBeDisabled();\n });\n});\n```\n\n### Row Nested Content Tests\n\nTest renderNestedContent prop for inline nested content panels\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable - Row nested content panels\", () => {\n it(\"renders a nested content panel below a row when expanded\", async () => {\n const user = userEvent.setup();\n\n render(\n <NimbusProvider>\n <DataTable\n columns={columns}\n rows={rows}\n renderNestedContent={(row) => (\n <Box data-testid={`detail-${row.id}`}>\n Nested content for {row.id}\n </Box>\n )}\n />\n </NimbusProvider>\n );\n\n // No nested content panels open initially\n expect(screen.queryByTestId(\"detail-1\")).not.toBeInTheDocument();\n\n // Click the expand button on the first data row\n const allRows = screen.getAllByRole(\"row\");\n const firstDataRow = allRows[1];\n const expandButton = within(firstDataRow).getByRole(\"button\", {\n name: \"Expand\",\n });\n await user.click(expandButton);\n\n await waitFor(() => {\n expect(screen.getByTestId(\"detail-1\")).toBeInTheDocument();\n expect(screen.getByText(\"Nested content for 1\")).toBeInTheDocument();\n });\n });\n\n it(\"toggles nested content panel closed on second click\", async () => {\n const user = userEvent.setup();\n\n render(\n <NimbusProvider>\n <DataTable\n columns={columns}\n rows={rows}\n renderNestedContent={(row) => (\n <Box data-testid={`detail-${row.id}`}>\n Nested content for {row.id}\n </Box>\n )}\n />\n </NimbusProvider>\n );\n\n const allRows = screen.getAllByRole(\"row\");\n const firstDataRow = allRows[1];\n const expandButton = within(firstDataRow).getByRole(\"button\", {\n name: \"Expand\",\n });\n\n // Open\n await user.click(expandButton);\n await waitFor(() => {\n expect(screen.getByTestId(\"detail-1\")).toBeInTheDocument();\n });\n\n // Close\n const collapseButton = within(firstDataRow).getByRole(\"button\", {\n name: \"Collapse\",\n });\n await user.click(collapseButton);\n await waitFor(() => {\n expect(screen.queryByTestId(\"detail-1\")).not.toBeInTheDocument();\n });\n });\n\n it(\"fires onRowClick alongside expand toggle\", async () => {\n const user = userEvent.setup();\n const handleRowClick = vi.fn();\n\n render(\n <NimbusProvider>\n <DataTable\n columns={columns}\n rows={rows}\n onRowClick={handleRowClick}\n renderNestedContent={(row) => (\n <Box data-testid={`detail-${row.id}`}>\n Nested content for {row.id}\n </Box>\n )}\n />\n </NimbusProvider>\n );\n\n // Expand via chevron — onRowClick should NOT fire\n const allRows = screen.getAllByRole(\"row\");\n const firstDataRow = allRows[1];\n const expandButton = within(firstDataRow).getByRole(\"button\", {\n name: \"Expand\",\n });\n await user.click(expandButton);\n\n await waitFor(() => {\n expect(screen.getByTestId(\"detail-1\")).toBeInTheDocument();\n });\n expect(handleRowClick).not.toHaveBeenCalled();\n\n // Click a data cell — onRowClick should fire\n const dataCell = within(firstDataRow).getAllByRole(\"gridcell\").at(-1)!;\n await user.click(dataCell);\n\n await waitFor(() => {\n expect(handleRowClick).toHaveBeenCalled();\n });\n });\n\n it(\"provides a close callback to dismiss the nested content panel\", async () => {\n const user = userEvent.setup();\n\n render(\n <NimbusProvider>\n <DataTable\n columns={columns}\n rows={rows}\n renderNestedContent={(row, { close }) => (\n <Box data-testid={`detail-${row.id}`}>\n <button data-testid={`close-${row.id}`} onClick={close}>\n Close\n </button>\n </Box>\n )}\n />\n </NimbusProvider>\n );\n\n // Open via expand button\n const allRows = screen.getAllByRole(\"row\");\n const firstDataRow = allRows[1];\n const expandButton = within(firstDataRow).getByRole(\"button\", {\n name: \"Expand\",\n });\n await user.click(expandButton);\n\n await waitFor(() => {\n expect(screen.getByTestId(\"detail-1\")).toBeInTheDocument();\n });\n\n // Close via the close callback\n await user.click(screen.getByTestId(\"close-1\"));\n\n await waitFor(() => {\n expect(screen.queryByTestId(\"detail-1\")).not.toBeInTheDocument();\n });\n });\n\n it(\"sets aria-controls on the row when expanded\", async () => {\n const user = userEvent.setup();\n\n render(\n <NimbusProvider>\n <DataTable\n columns={columns}\n rows={rows}\n renderNestedContent={(row) => (\n <Box data-testid={`detail-${row.id}`}>Detail</Box>\n )}\n />\n </NimbusProvider>\n );\n\n const allRows = screen.getAllByRole(\"row\");\n const firstDataRow = allRows[1];\n\n // aria-controls should not be present when collapsed\n expect(firstDataRow).not.toHaveAttribute(\"aria-controls\");\n\n // Expand the row\n const expandButton = within(firstDataRow).getByRole(\"button\", {\n name: \"Expand\",\n });\n await user.click(expandButton);\n\n await waitFor(() => {\n expect(firstDataRow).toHaveAttribute(\"aria-controls\", \"nested-content-1\");\n });\n });\n});\n```\n\n### Search and Filtering Tests\n\nTest search functionality\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable - Search and filtering\", () => {\n it(\"filters rows based on search term\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} search=\"Alice\" />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Alice\", { selector: \"mark\" })).toBeInTheDocument();\n expect(screen.getByText(\"Johnson\")).toBeInTheDocument();\n expect(screen.queryByText(\"Bob Smith\")).not.toBeInTheDocument();\n expect(screen.queryByText(\"Carol Williams\")).not.toBeInTheDocument();\n });\n\n it(\"shows all rows when search is empty\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} search=\"\" />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Alice Johnson\")).toBeInTheDocument();\n expect(screen.getByText(\"Bob Smith\")).toBeInTheDocument();\n expect(screen.getByText(\"Carol Williams\")).toBeInTheDocument();\n });\n\n it(\"filters across multiple columns\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} search=\"Admin\" />\n </NimbusProvider>\n );\n\n // Alice is the Admin\n expect(screen.getByText(\"Alice Johnson\")).toBeInTheDocument();\n expect(screen.queryByText(\"Bob Smith\")).not.toBeInTheDocument();\n });\n});\n```\n\n### Column Visibility Tests\n\nTest column visibility control\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable - Column visibility\", () => {\n it(\"shows only specified visible columns\", () => {\n render(\n <NimbusProvider>\n <DataTable\n columns={columns}\n rows={rows}\n visibleColumns={[\"name\", \"role\"]}\n />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Name\")).toBeInTheDocument();\n expect(screen.getByText(\"Role\")).toBeInTheDocument();\n expect(screen.queryByText(\"Email\")).not.toBeInTheDocument();\n });\n\n it(\"shows all columns when visibleColumns is not specified\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Name\")).toBeInTheDocument();\n expect(screen.getByText(\"Email\")).toBeInTheDocument();\n expect(screen.getByText(\"Role\")).toBeInTheDocument();\n });\n});\n```\n\n### Density Tests\n\nTest density variants\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable - Density\", () => {\n it(\"renders with default density\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} density=\"default\" />\n </NimbusProvider>\n );\n\n const table = screen.getByRole(\"grid\");\n expect(table).toBeInTheDocument();\n });\n\n it(\"renders with condensed density\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} density=\"condensed\" />\n </NimbusProvider>\n );\n\n const table = screen.getByRole(\"grid\");\n expect(table).toBeInTheDocument();\n });\n});\n```\n\n### Nested Rows Tests\n\nTest expandable nested row functionality\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable - Nested rows\", () => {\n const nestedRows: DataTableRowItem[] = [\n {\n id: \"1\",\n name: \"Parent Item\",\n email: \"parent@example.com\",\n role: \"Admin\",\n children: (\n <Box\n p=\"500\"\n bg=\"neutral.2\"\n borderRadius=\"md\"\n border=\"1px solid\"\n borderColor=\"neutral.6\"\n m=\"200 0\"\n >\n Child Item 1\n </Box>\n ),\n },\n {\n id: \"2\",\n name: \"No Children\",\n email: \"single@example.com\",\n role: \"User\",\n },\n ];\n\n it(\"renders expand button for rows with children\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={nestedRows} nestedKey=\"children\" />\n </NimbusProvider>\n );\n\n const expandButton = screen.getByRole(\"button\", { name: /expand/i });\n expect(expandButton).toBeInTheDocument();\n });\n\n it(\"expands nested content when expand button is clicked\", async () => {\n const user = userEvent.setup();\n\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={nestedRows} nestedKey=\"children\" />\n </NimbusProvider>\n );\n\n // Initially nested content should not be visible\n expect(screen.queryByText(\"Child Item 1\")).not.toBeInTheDocument();\n\n const expandButton = screen.getByRole(\"button\", { name: /expand/i });\n expect(expandButton).toBeInTheDocument();\n await user.click(expandButton);\n\n await waitFor(async () => {\n await expect(screen.getByText(\"Child Item 1\")).toBeInTheDocument();\n });\n });\n\n it(\"collapses nested content when collapse button is clicked\", async () => {\n const user = userEvent.setup();\n\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={nestedRows} nestedKey=\"children\" />\n </NimbusProvider>\n );\n\n // Expand first\n const expandButton = screen.getByRole(\"button\", { name: /expand/i });\n await user.click(expandButton);\n\n await waitFor(() => {\n expect(screen.getByText(\"Child Item 1\")).toBeInTheDocument();\n });\n\n // Collapse\n const collapseButton = screen.getByRole(\"button\", { name: /collapse/i });\n await user.click(collapseButton);\n\n await waitFor(() => {\n expect(screen.queryByText(\"Child Item 1\")).not.toBeInTheDocument();\n });\n });\n});\n```\n\n### Empty State Tests\n\nTest empty state rendering\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable - Empty state\", () => {\n it(\"renders custom empty state when no rows\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={[]} />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"No Data\")).toBeInTheDocument();\n });\n\n it(\"renders empty state when search matches no rows\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} search=\"nonexistent\" />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"No Data\")).toBeInTheDocument();\n });\n});\n```\n\n### Accessibility Tests\n\nTest accessibility features\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable - Accessibility\", () => {\n it(\"has accessible grid role\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} aria-label=\"Users table\" />\n </NimbusProvider>\n );\n\n const table = screen.getByRole(\"grid\");\n expect(table).toBeInTheDocument();\n });\n\n it(\"has proper row structure\", () => {\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} />\n </NimbusProvider>\n );\n\n const tableRows = screen.getAllByRole(\"row\");\n expect(tableRows.length).toBeGreaterThan(0);\n\n // Check that rows contain cells\n const firstDataRow = tableRows[1];\n const cells = within(firstDataRow).getAllByRole(\"gridcell\");\n expect(cells.length).toBeGreaterThan(0);\n });\n\n it(\"has sortable columns with aria-sort attribute\", async () => {\n const user = userEvent.setup();\n\n render(\n <NimbusProvider>\n <DataTable columns={columns} rows={rows} allowsSorting />\n </NimbusProvider>\n );\n\n await user.click(screen.getByText(\"Name\"));\n\n await waitFor(() => {\n const columnHeader = screen\n .getByText(\"Name\")\n .closest('[role=\"columnheader\"]');\n expect(columnHeader).toHaveAttribute(\"aria-sort\");\n });\n });\n});\n```\n\n### Manager: Basic Rendering Tests\n\nVerify the Manager button renders and opens the settings drawer\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable.Manager - Basic rendering\", () => {\n it(\"renders the settings button\", () => {\n const managerColumns: DataTableColumnItem[] = [\n {\n id: \"name\",\n header: \"Name\",\n accessor: (row: Record<string, unknown>) => row.name as React.ReactNode,\n },\n {\n id: \"email\",\n header: \"Email\",\n accessor: (row: Record<string, unknown>) =>\n row.email as React.ReactNode,\n },\n ];\n const managerRows: DataTableRowItem[] = [\n { id: \"1\", name: \"Alice\", email: \"alice@example.com\" },\n ];\n\n render(\n <NimbusProvider>\n <DataTable.Root\n columns={managerColumns}\n rows={managerRows}\n visibleColumns={[\"name\"]}\n >\n <DataTable.Manager />\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n </NimbusProvider>\n );\n\n expect(\n screen.getByRole(\"button\", { name: /settings/i })\n ).toBeInTheDocument();\n });\n\n it(\"settings button is clickable\", async () => {\n const user = userEvent.setup();\n const managerColumns: DataTableColumnItem[] = [\n {\n id: \"name\",\n header: \"Name\",\n accessor: (row: Record<string, unknown>) => row.name as React.ReactNode,\n },\n {\n id: \"email\",\n header: \"Email\",\n accessor: (row: Record<string, unknown>) =>\n row.email as React.ReactNode,\n },\n ];\n const managerRows: DataTableRowItem[] = [\n { id: \"1\", name: \"Alice\", email: \"alice@example.com\" },\n ];\n\n render(\n <NimbusProvider>\n <DataTable.Root\n columns={managerColumns}\n rows={managerRows}\n visibleColumns={[\"name\"]}\n >\n <DataTable.Manager />\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n </NimbusProvider>\n );\n\n const settingsButton = screen.getByRole(\"button\", { name: /settings/i });\n\n // Verify button can be clicked\n await user.click(settingsButton);\n\n // Button should still be in the document after click\n expect(settingsButton).toBeInTheDocument();\n });\n});\n```\n\n### Manager: Column Management Tests\n\nTest column visibility and reordering functionality\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable.Manager - Column management\", () => {\n it(\"displays visible and hidden columns in separate lists\", async () => {\n const user = userEvent.setup();\n const managerColumns: DataTableColumnItem[] = [\n {\n id: \"name\",\n header: \"Name\",\n accessor: (row: Record<string, unknown>) => row.name as React.ReactNode,\n },\n {\n id: \"email\",\n header: \"Email\",\n accessor: (row: Record<string, unknown>) =>\n row.email as React.ReactNode,\n },\n {\n id: \"role\",\n header: \"Role\",\n accessor: (row: Record<string, unknown>) => row.role as React.ReactNode,\n },\n ];\n const managerRows: DataTableRowItem[] = [\n { id: \"1\", name: \"Alice\", email: \"alice@example.com\", role: \"Admin\" },\n ];\n\n render(\n <NimbusProvider>\n <DataTable.Root\n columns={managerColumns}\n rows={managerRows}\n visibleColumns={[\"name\", \"email\"]}\n >\n <DataTable.Manager />\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n </NimbusProvider>\n );\n\n const settingsButton = screen.getByRole(\"button\", { name: /settings/i });\n await user.click(settingsButton);\n\n await waitFor(() => {\n const visibleList = screen.getByTestId(\"visible-columns-list\");\n const hiddenList = screen.getByTestId(\"hidden-columns-list\");\n\n expect(within(visibleList).getByText(\"Name\")).toBeInTheDocument();\n expect(within(visibleList).getByText(\"Email\")).toBeInTheDocument();\n expect(within(hiddenList).getByText(\"Role\")).toBeInTheDocument();\n });\n });\n\n it(\"calls onColumnsChange when column visibility changes\", async () => {\n const user = userEvent.setup();\n const handleColumnsChange = vi.fn();\n const managerColumns: DataTableColumnItem[] = [\n {\n id: \"name\",\n header: \"Name\",\n accessor: (row: Record<string, unknown>) => row.name as React.ReactNode,\n },\n {\n id: \"email\",\n header: \"Email\",\n accessor: (row: Record<string, unknown>) =>\n row.email as React.ReactNode,\n },\n ];\n const managerRows: DataTableRowItem[] = [\n { id: \"1\", name: \"Alice\", email: \"alice@example.com\" },\n ];\n\n render(\n <NimbusProvider>\n <DataTable.Root\n columns={managerColumns}\n rows={managerRows}\n visibleColumns={[\"name\"]}\n onColumnsChange={handleColumnsChange}\n >\n <DataTable.Manager />\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n </NimbusProvider>\n );\n\n const settingsButton = screen.getByRole(\"button\", { name: /settings/i });\n await user.click(settingsButton);\n\n // Note: Full drag-and-drop testing would require more complex setup\n // This verifies the callback is available\n expect(handleColumnsChange).toBeDefined();\n });\n});\n```\n\n### Manager: Layout Settings Tests\n\nTest text visibility and row density controls\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable.Manager - Layout settings\", () => {\n it(\"displays layout settings tab\", async () => {\n const user = userEvent.setup();\n const managerColumns: DataTableColumnItem[] = [\n {\n id: \"name\",\n header: \"Name\",\n accessor: (row: Record<string, unknown>) => row.name as React.ReactNode,\n },\n {\n id: \"email\",\n header: \"Email\",\n accessor: (row: Record<string, unknown>) =>\n row.email as React.ReactNode,\n },\n ];\n const managerRows: DataTableRowItem[] = [\n { id: \"1\", name: \"Alice\", email: \"alice@example.com\" },\n ];\n\n render(\n <NimbusProvider>\n <DataTable.Root\n columns={managerColumns}\n rows={managerRows}\n visibleColumns={[\"name\"]}\n >\n <DataTable.Manager />\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n </NimbusProvider>\n );\n\n const settingsButton = screen.getByRole(\"button\", { name: /settings/i });\n await user.click(settingsButton);\n\n await waitFor(() => {\n expect(screen.getByText(/layout settings/i)).toBeInTheDocument();\n });\n });\n\n it(\"calls onSettingsChange when layout settings change\", async () => {\n const user = userEvent.setup();\n const handleSettingsChange = vi.fn();\n const managerColumns: DataTableColumnItem[] = [\n {\n id: \"name\",\n header: \"Name\",\n accessor: (row: Record<string, unknown>) => row.name as React.ReactNode,\n },\n {\n id: \"email\",\n header: \"Email\",\n accessor: (row: Record<string, unknown>) =>\n row.email as React.ReactNode,\n },\n ];\n const managerRows: DataTableRowItem[] = [\n { id: \"1\", name: \"Alice\", email: \"alice@example.com\" },\n ];\n\n render(\n <NimbusProvider>\n <DataTable.Root\n columns={managerColumns}\n rows={managerRows}\n visibleColumns={[\"name\"]}\n onSettingsChange={handleSettingsChange}\n >\n <DataTable.Manager />\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n </NimbusProvider>\n );\n\n const settingsButton = screen.getByRole(\"button\", { name: /settings/i });\n await user.click(settingsButton);\n\n // Note: Actual toggle interaction testing would require finding specific buttons\n // This verifies the callback is wired up\n expect(handleSettingsChange).toBeDefined();\n });\n});\n```\n\n### Manager: Custom Settings Tests\n\nTest custom settings tab integration\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor, within } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DataTable, NimbusProvider, Box } from \"@commercetools/nimbus\";\nimport type { DataTableColumnItem, DataTableRowItem } from \"./data-table.types\";\n\ndescribe(\"DataTable.Manager - Custom settings\", () => {\n it(\"displays custom settings tab when provided\", async () => {\n const user = userEvent.setup();\n const managerColumns: DataTableColumnItem[] = [\n {\n id: \"name\",\n header: \"Name\",\n accessor: (row: Record<string, unknown>) => row.name as React.ReactNode,\n },\n {\n id: \"email\",\n header: \"Email\",\n accessor: (row: Record<string, unknown>) =>\n row.email as React.ReactNode,\n },\n ];\n const managerRows: DataTableRowItem[] = [\n { id: \"1\", name: \"Alice\", email: \"alice@example.com\" },\n ];\n\n const customSettings = {\n label: \"Filters\",\n panel: <div>Custom Filter Panel</div>,\n };\n\n render(\n <NimbusProvider>\n <DataTable.Root\n columns={managerColumns}\n rows={managerRows}\n visibleColumns={[\"name\"]}\n customSettings={customSettings}\n >\n <DataTable.Manager />\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n </NimbusProvider>\n );\n\n const settingsButton = screen.getByRole(\"button\", { name: /settings/i });\n await user.click(settingsButton);\n\n await waitFor(() => {\n expect(screen.getByText(\"Filters\")).toBeInTheDocument();\n });\n });\n\n it(\"renders custom settings panel content\", async () => {\n const user = userEvent.setup();\n const managerColumns: DataTableColumnItem[] = [\n {\n id: \"name\",\n header: \"Name\",\n accessor: (row: Record<string, unknown>) => row.name as React.ReactNode,\n },\n {\n id: \"email\",\n header: \"Email\",\n accessor: (row: Record<string, unknown>) =>\n row.email as React.ReactNode,\n },\n ];\n const managerRows: DataTableRowItem[] = [\n { id: \"1\", name: \"Alice\", email: \"alice@example.com\" },\n ];\n\n const customSettings = {\n label: \"Advanced\",\n panel: <div data-testid=\"custom-panel\">Custom Panel Content</div>,\n };\n\n render(\n <NimbusProvider>\n <DataTable.Root\n columns={managerColumns}\n rows={managerRows}\n visibleColumns={[\"name\"]}\n customSettings={customSettings}\n >\n <DataTable.Manager />\n <DataTable.Table>\n <DataTable.Header />\n <DataTable.Body />\n </DataTable.Table>\n </DataTable.Root>\n </NimbusProvider>\n );\n\n const settingsButton = screen.getByRole(\"button\", { name: /settings/i });\n await user.click(settingsButton);\n\n await waitFor(() => {\n const advancedTab = screen.getByText(\"Advanced\");\n expect(advancedTab).toBeInTheDocument();\n });\n\n // Click the custom tab\n const advancedTab = screen.getByText(\"Advanced\");\n await user.click(advancedTab);\n\n await waitFor(() => {\n expect(screen.getByTestId(\"custom-panel\")).toBeInTheDocument();\n expect(screen.getByText(\"Custom Panel Content\")).toBeInTheDocument();\n });\n });\n});\n```\n\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-datatable--docs)\n- [React Aria Table](https://react-spectrum.adobe.com/react-aria/Table.html)\n- [ARIA Grid Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/)\n\n**Related components**\n\n- **[Table Component](/components/data-display/table)** - Use the simpler Table component for static, read-only data without interactive features\n",
193
193
  "toc": [
194
194
  {
195
195
  "value": "Getting started",
@@ -407,6 +407,28 @@
407
407
  ],
408
408
  "parent": "root"
409
409
  },
410
+ {
411
+ "value": "Nested content via render function",
412
+ "href": "#nested-content-via-render-function",
413
+ "depth": 3,
414
+ "numbering": [
415
+ 1,
416
+ 4,
417
+ 12
418
+ ],
419
+ "parent": "root"
420
+ },
421
+ {
422
+ "value": "Overriding renderNestedContent with nestedKey",
423
+ "href": "#overriding-rendernestedcontent-with-nestedkey",
424
+ "depth": 3,
425
+ "numbering": [
426
+ 1,
427
+ 4,
428
+ 13
429
+ ],
430
+ "parent": "root"
431
+ },
410
432
  {
411
433
  "value": "With footer",
412
434
  "href": "#with-footer",
@@ -414,7 +436,7 @@
414
436
  "numbering": [
415
437
  1,
416
438
  4,
417
- 12
439
+ 14
418
440
  ],
419
441
  "parent": "root"
420
442
  },
@@ -425,7 +447,7 @@
425
447
  "numbering": [
426
448
  1,
427
449
  4,
428
- 13
450
+ 15
429
451
  ],
430
452
  "parent": "root"
431
453
  },
@@ -436,7 +458,40 @@
436
458
  "numbering": [
437
459
  1,
438
460
  4,
439
- 14
461
+ 16
462
+ ],
463
+ "parent": "root"
464
+ },
465
+ {
466
+ "value": "Hiding the pin column",
467
+ "href": "#hiding-the-pin-column",
468
+ "depth": 3,
469
+ "numbering": [
470
+ 1,
471
+ 4,
472
+ 17
473
+ ],
474
+ "parent": "root"
475
+ },
476
+ {
477
+ "value": "Hiding the expand column",
478
+ "href": "#hiding-the-expand-column",
479
+ "depth": 3,
480
+ "numbering": [
481
+ 1,
482
+ 4,
483
+ 18
484
+ ],
485
+ "parent": "root"
486
+ },
487
+ {
488
+ "value": "Hidden expand column with custom row click",
489
+ "href": "#hidden-expand-column-with-custom-row-click",
490
+ "depth": 3,
491
+ "numbering": [
492
+ 1,
493
+ 4,
494
+ 19
440
495
  ],
441
496
  "parent": "root"
442
497
  },
@@ -493,6 +548,65 @@
493
548
  ],
494
549
  "parent": "root"
495
550
  },
551
+ {
552
+ "value": "Compound composition",
553
+ "href": "#compound-composition",
554
+ "depth": 3,
555
+ "numbering": [
556
+ 1,
557
+ 6,
558
+ 2
559
+ ],
560
+ "parent": "root"
561
+ },
562
+ {
563
+ "value": "Custom row styling",
564
+ "href": "#custom-row-styling",
565
+ "depth": 4,
566
+ "numbering": [
567
+ 1,
568
+ 6,
569
+ 2,
570
+ 1
571
+ ],
572
+ "parent": "root"
573
+ },
574
+ {
575
+ "value": "Custom cell rendering",
576
+ "href": "#custom-cell-rendering",
577
+ "depth": 4,
578
+ "numbering": [
579
+ 1,
580
+ 6,
581
+ 2,
582
+ 2
583
+ ],
584
+ "parent": "root"
585
+ },
586
+ {
587
+ "value": "Custom column headers",
588
+ "href": "#custom-column-headers",
589
+ "depth": 4,
590
+ "numbering": [
591
+ 1,
592
+ 6,
593
+ 2,
594
+ 3
595
+ ],
596
+ "parent": "root"
597
+ },
598
+ {
599
+ "value": "Compound composition with features",
600
+ "href": "#compound-composition-with-features",
601
+ "depth": 4,
602
+ "numbering": [
603
+ 1,
604
+ 6,
605
+ 2,
606
+ 4
607
+ ],
608
+ "parent": "root"
609
+ },
496
610
  {
497
611
  "value": "Advanced: DataTable.Manager",
498
612
  "href": "#advanced-datatablemanager",
@@ -656,6 +770,39 @@
656
770
  ],
657
771
  "parent": "root"
658
772
  },
773
+ {
774
+ "value": "DataTableColumnRenderProps",
775
+ "href": "#datatablecolumnrenderprops",
776
+ "depth": 3,
777
+ "numbering": [
778
+ 1,
779
+ 10,
780
+ 4
781
+ ],
782
+ "parent": "root"
783
+ },
784
+ {
785
+ "value": "DataTableRowRenderProps",
786
+ "href": "#datatablerowrenderprops",
787
+ "depth": 3,
788
+ "numbering": [
789
+ 1,
790
+ 10,
791
+ 5
792
+ ],
793
+ "parent": "root"
794
+ },
795
+ {
796
+ "value": "DataTableCellRenderProps",
797
+ "href": "#datatablecellrenderprops",
798
+ "depth": 3,
799
+ "numbering": [
800
+ 1,
801
+ 10,
802
+ 6
803
+ ],
804
+ "parent": "root"
805
+ },
659
806
  {
660
807
  "value": "Testing your implementation",
661
808
  "href": "#testing-your-implementation",
@@ -710,6 +857,17 @@
710
857
  ],
711
858
  "parent": "root"
712
859
  },
860
+ {
861
+ "value": "Row Nested Content Tests",
862
+ "href": "#row-nested-content-tests",
863
+ "depth": 3,
864
+ "numbering": [
865
+ 1,
866
+ 11,
867
+ 5
868
+ ],
869
+ "parent": "root"
870
+ },
713
871
  {
714
872
  "value": "Search and Filtering Tests",
715
873
  "href": "#search-and-filtering-tests",
@@ -717,7 +875,7 @@
717
875
  "numbering": [
718
876
  1,
719
877
  11,
720
- 5
878
+ 6
721
879
  ],
722
880
  "parent": "root"
723
881
  },
@@ -728,7 +886,7 @@
728
886
  "numbering": [
729
887
  1,
730
888
  11,
731
- 6
889
+ 7
732
890
  ],
733
891
  "parent": "root"
734
892
  },
@@ -739,7 +897,7 @@
739
897
  "numbering": [
740
898
  1,
741
899
  11,
742
- 7
900
+ 8
743
901
  ],
744
902
  "parent": "root"
745
903
  },
@@ -750,7 +908,7 @@
750
908
  "numbering": [
751
909
  1,
752
910
  11,
753
- 8
911
+ 9
754
912
  ],
755
913
  "parent": "root"
756
914
  },
@@ -761,7 +919,7 @@
761
919
  "numbering": [
762
920
  1,
763
921
  11,
764
- 9
922
+ 10
765
923
  ],
766
924
  "parent": "root"
767
925
  },
@@ -772,7 +930,7 @@
772
930
  "numbering": [
773
931
  1,
774
932
  11,
775
- 10
933
+ 11
776
934
  ],
777
935
  "parent": "root"
778
936
  },
@@ -783,7 +941,7 @@
783
941
  "numbering": [
784
942
  1,
785
943
  11,
786
- 11
944
+ 12
787
945
  ],
788
946
  "parent": "root"
789
947
  },
@@ -794,7 +952,7 @@
794
952
  "numbering": [
795
953
  1,
796
954
  11,
797
- 12
955
+ 13
798
956
  ],
799
957
  "parent": "root"
800
958
  },
@@ -805,7 +963,7 @@
805
963
  "numbering": [
806
964
  1,
807
965
  11,
808
- 13
966
+ 14
809
967
  ],
810
968
  "parent": "root"
811
969
  },
@@ -816,7 +974,7 @@
816
974
  "numbering": [
817
975
  1,
818
976
  11,
819
- 14
977
+ 15
820
978
  ],
821
979
  "parent": "root"
822
980
  },