@commercetools/nimbus-mcp 3.0.0 → 3.1.0

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 (180) hide show
  1. package/data/docs/route-manifest.json +1407 -794
  2. package/data/docs/routes/components-data-display-data-table.json +66 -34
  3. package/data/docs/routes/components-data-display-draggable-list.json +62 -7
  4. package/data/docs/routes/components-inputs-checkbox.json +2 -2
  5. package/data/docs/routes/components-layout-splitter.json +654 -0
  6. package/data/docs/routes/components-utilities-region.json +265 -0
  7. package/data/docs/routes/home-getting-started-bundler-plugins.json +248 -0
  8. package/data/docs/routes/hooks-usedraganddrop.json +310 -0
  9. package/data/docs/routes/patterns-actions.json +78 -0
  10. package/data/docs/routes/patterns-dialogs-confirmation-dialog.json +391 -0
  11. package/data/docs/routes/patterns-dialogs-form-dialog.json +358 -0
  12. package/data/docs/routes/patterns-dialogs.json +2 -2
  13. package/data/docs/routes/patterns-pages-public-page-layout.json +371 -0
  14. package/data/docs/routes/patterns-pages.json +78 -0
  15. package/data/docs/search-index.json +1 -1
  16. package/data/docs/types/AccordionContent.json +32 -32
  17. package/data/docs/types/AccordionHeader.json +102 -102
  18. package/data/docs/types/AccordionItem.json +28 -28
  19. package/data/docs/types/AccordionRoot.json +15 -15
  20. package/data/docs/types/AlertDescription.json +8 -8
  21. package/data/docs/types/AlertDismissButton.json +89 -89
  22. package/data/docs/types/AlertTitle.json +8 -8
  23. package/data/docs/types/Avatar.json +2 -2
  24. package/data/docs/types/Badge.json +2 -2
  25. package/data/docs/types/Body.json +6 -6
  26. package/data/docs/types/Box.json +6 -6
  27. package/data/docs/types/Button.json +97 -97
  28. package/data/docs/types/Calendar.json +111 -65
  29. package/data/docs/types/Caption.json +6 -6
  30. package/data/docs/types/CardRoot.json +2 -2
  31. package/data/docs/types/Cell.json +20 -20
  32. package/data/docs/types/Checkbox.json +99 -99
  33. package/data/docs/types/Code.json +10 -10
  34. package/data/docs/types/CollapsibleMotionContent.json +2 -2
  35. package/data/docs/types/CollapsibleMotionRoot.json +2 -2
  36. package/data/docs/types/CollapsibleMotionTrigger.json +4 -4
  37. package/data/docs/types/Column.json +8 -8
  38. package/data/docs/types/ColumnGroup.json +8 -8
  39. package/data/docs/types/ColumnHeader.json +18 -18
  40. package/data/docs/types/ComboBoxListBox.json +80 -80
  41. package/data/docs/types/ComboBoxOption.json +77 -77
  42. package/data/docs/types/ComboBoxPopover.json +77 -77
  43. package/data/docs/types/ComboBoxRoot.json +8 -8
  44. package/data/docs/types/ComboBoxSection.json +29 -29
  45. package/data/docs/types/ComboBoxTrigger.json +6 -6
  46. package/data/docs/types/ConfirmationDialog.json +224 -0
  47. package/data/docs/types/Content.json +2 -2
  48. package/data/docs/types/DataTable.json +17 -2
  49. package/data/docs/types/DataTableBody.json +24 -24
  50. package/data/docs/types/DataTableHeader.json +31 -31
  51. package/data/docs/types/DataTableRoot.json +17 -2
  52. package/data/docs/types/DataTableTable.json +35 -20
  53. package/data/docs/types/DateInput.json +84 -84
  54. package/data/docs/types/DatePicker.json +65 -65
  55. package/data/docs/types/DateRangePicker.json +99 -99
  56. package/data/docs/types/DateRangePickerField.json +99 -99
  57. package/data/docs/types/DefaultPageBackLink.json +16 -16
  58. package/data/docs/types/DefaultPageRoot.json +2 -2
  59. package/data/docs/types/DialogCloseTrigger.json +87 -87
  60. package/data/docs/types/DialogTrigger.json +2 -2
  61. package/data/docs/types/DragAndDropItemData.json +9 -0
  62. package/data/docs/types/DragAndDropProps.json +9 -0
  63. package/data/docs/types/DraggableListField.json +159 -70
  64. package/data/docs/types/DraggableListItem.json +63 -63
  65. package/data/docs/types/DraggableListRoot.json +159 -70
  66. package/data/docs/types/DrawerCloseTrigger.json +87 -87
  67. package/data/docs/types/DrawerTrigger.json +2 -2
  68. package/data/docs/types/FieldErrors.json +2 -2
  69. package/data/docs/types/Flex.json +22 -22
  70. package/data/docs/types/Footer.json +6 -6
  71. package/data/docs/types/FormDialog.json +198 -0
  72. package/data/docs/types/FormFieldRoot.json +2 -2
  73. package/data/docs/types/Grid.json +24 -24
  74. package/data/docs/types/Group.json +12 -12
  75. package/data/docs/types/Header.json +6 -6
  76. package/data/docs/types/Heading.json +8 -8
  77. package/data/docs/types/Icon.json +4 -4
  78. package/data/docs/types/IconButton.json +97 -97
  79. package/data/docs/types/IconToggleButton.json +84 -84
  80. package/data/docs/types/Image.json +30 -30
  81. package/data/docs/types/Indicator.json +6 -6
  82. package/data/docs/types/InlineSvg.json +2 -2
  83. package/data/docs/types/Item.json +6 -6
  84. package/data/docs/types/Kbd.json +8 -8
  85. package/data/docs/types/Link.json +31 -31
  86. package/data/docs/types/ListIndicator.json +6 -6
  87. package/data/docs/types/ListItem.json +6 -6
  88. package/data/docs/types/ListRoot.json +10 -10
  89. package/data/docs/types/LoadingSpinner.json +2 -2
  90. package/data/docs/types/MakeElementFocusable.json +19 -19
  91. package/data/docs/types/MenuItem.json +75 -75
  92. package/data/docs/types/MenuRoot.json +63 -63
  93. package/data/docs/types/MenuSection.json +35 -54
  94. package/data/docs/types/MenuSubmenuTrigger.json +5 -5
  95. package/data/docs/types/MenuTrigger.json +102 -102
  96. package/data/docs/types/MultilineTextInput.json +134 -134
  97. package/data/docs/types/MultilineTextInputField.json +131 -131
  98. package/data/docs/types/NumberInput.json +100 -100
  99. package/data/docs/types/NumberInputField.json +95 -95
  100. package/data/docs/types/PageContentColumn.json +6 -6
  101. package/data/docs/types/PageContentRoot.json +6 -6
  102. package/data/docs/types/PasswordInput.json +129 -129
  103. package/data/docs/types/PasswordInputField.json +129 -129
  104. package/data/docs/types/ProgressBar.json +14 -14
  105. package/data/docs/types/PublicPageLayout.json +99 -0
  106. package/data/docs/types/RadioInputOption.json +64 -64
  107. package/data/docs/types/RadioInputRoot.json +55 -55
  108. package/data/docs/types/RangeCalendar.json +90 -71
  109. package/data/docs/types/Region.json +114 -0
  110. package/data/docs/types/RegionProvider.json +25 -0
  111. package/data/docs/types/RegionTarget.json +112 -0
  112. package/data/docs/types/RichTextInput.json +2 -2
  113. package/data/docs/types/Root.json +10 -10
  114. package/data/docs/types/Row.json +6 -6
  115. package/data/docs/types/SPLITTER_SIZE_TOKENS.json +9 -0
  116. package/data/docs/types/ScrollArea.json +2 -2
  117. package/data/docs/types/SearchInput.json +136 -136
  118. package/data/docs/types/SearchInputField.json +131 -131
  119. package/data/docs/types/SelectOption.json +66 -66
  120. package/data/docs/types/SelectOptionGroup.json +22 -22
  121. package/data/docs/types/SelectOptions.json +74 -74
  122. package/data/docs/types/SelectRoot.json +102 -102
  123. package/data/docs/types/Separator.json +4 -4
  124. package/data/docs/types/SimpleGrid.json +28 -28
  125. package/data/docs/types/SplitButton.json +12 -12
  126. package/data/docs/types/Splitter.json +12 -0
  127. package/data/docs/types/SplitterAside.json +27 -0
  128. package/data/docs/types/SplitterHandle.json +27 -0
  129. package/data/docs/types/SplitterMain.json +27 -0
  130. package/data/docs/types/SplitterRoot.json +271 -0
  131. package/data/docs/types/SplitterSizeToken.json +9 -0
  132. package/data/docs/types/Stack.json +2 -2
  133. package/data/docs/types/StepsNextTrigger.json +2 -2
  134. package/data/docs/types/StepsPrevTrigger.json +2 -2
  135. package/data/docs/types/StepsRoot.json +2 -2
  136. package/data/docs/types/StepsTrigger.json +2 -2
  137. package/data/docs/types/Switch.json +38 -38
  138. package/data/docs/types/TabNavItem.json +18 -18
  139. package/data/docs/types/TabNavRoot.json +2 -2
  140. package/data/docs/types/TableBody.json +6 -6
  141. package/data/docs/types/TableCaption.json +6 -6
  142. package/data/docs/types/TableCell.json +20 -20
  143. package/data/docs/types/TableColumn.json +8 -8
  144. package/data/docs/types/TableColumnGroup.json +8 -8
  145. package/data/docs/types/TableColumnHeader.json +18 -18
  146. package/data/docs/types/TableFooter.json +6 -6
  147. package/data/docs/types/TableHeader.json +6 -6
  148. package/data/docs/types/TableRoot.json +32 -32
  149. package/data/docs/types/TableRow.json +6 -6
  150. package/data/docs/types/TableScrollArea.json +6 -6
  151. package/data/docs/types/TabsTab.json +2 -2
  152. package/data/docs/types/TagGroupRoot.json +27 -27
  153. package/data/docs/types/TagGroupTag.json +68 -68
  154. package/data/docs/types/TagGroupTagList.json +18 -18
  155. package/data/docs/types/Text.json +8 -8
  156. package/data/docs/types/TextInput.json +132 -132
  157. package/data/docs/types/TextInputField.json +129 -129
  158. package/data/docs/types/TimeInput.json +78 -78
  159. package/data/docs/types/ToggleButton.json +86 -86
  160. package/data/docs/types/ToggleButtonGroupButton.json +33 -33
  161. package/data/docs/types/ToggleButtonGroupRoot.json +20 -20
  162. package/data/docs/types/Toolbar.json +12 -12
  163. package/data/docs/types/TooltipContent.json +31 -31
  164. package/data/docs/types/TooltipRoot.json +18 -18
  165. package/data/docs/types/Trigger.json +4 -4
  166. package/data/docs/types/UseDragAndDropOptions.json +9 -0
  167. package/data/docs/types/VisuallyHidden.json +7 -7
  168. package/data/docs/types/createArrayHandlers.json +12 -0
  169. package/data/docs/types/createItemsFromCsvDrop.json +833 -0
  170. package/data/docs/types/createItemsFromDirectoryDrop.json +833 -0
  171. package/data/docs/types/createItemsFromFileDrop.json +833 -0
  172. package/data/docs/types/createItemsFromImageDrop.json +833 -0
  173. package/data/docs/types/createItemsFromJsonDrop.json +833 -0
  174. package/data/docs/types/createItemsFromTextDrop.json +12 -0
  175. package/data/docs/types/createListDataHandlers.json +102 -0
  176. package/data/docs/types/manifest.json +29 -2
  177. package/data/docs/types/useDragAndDrop.json +174 -0
  178. package/data/docs/types/useRegion.json +1052 -0
  179. package/data/docs/types/useResponsiveSplitterSizes.json +143 -0
  180. package/package.json +6 -6
@@ -185,7 +185,7 @@
185
185
  ]
186
186
  },
187
187
  "dev": {
188
- "mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { DraggableList, type DraggableListRootProps } from '@commercetools/nimbus';\n```\n\n### Basic usage\n\nThe simplest implementation uses an array of items with `key` and `label` properties. The component automatically handles drag-and-drop reordering:\n\n```jsx live-dev\nconst App = () => {\n const items = [\n { key: '1', label: 'First item' },\n { key: '2', label: 'Second item' },\n { key: '3', label: 'Third item' },\n ];\n\n return (\n <DraggableList.Root\n items={items}\n aria-label=\"Task list\"\n />\n );\n}\n```\n\n**Important:** You must provide either an `aria-label` or `aria-labelledby` prop for accessibility. The drag-and-drop functionality requires an accessible label.\n\n## Working with React Aria drag-and-drop\n\nThe DraggableList relies on React Aria Components' GridList and drag-and-drop hooks for accessible drag-and-drop functionality. This integration provides keyboard navigation, screen reader support, and touch-friendly interactions.\n\n### Drag-and-drop behavior\n\nReact Aria's drag-and-drop system provides:\n- **Visual feedback**: Items show grab cursors and drag previews during interactions\n- **Drop indicators**: Visual cues show where items will be placed\n- **Move operations**: Items are moved (not copied) between lists\n- **Keyboard support**: Full keyboard navigation for drag-and-drop\n\n### Data format\n\nItems being dragged use a custom format (`nimbus-draggable-list-item`) that allows items to be dragged between multiple DraggableList instances while preventing drops from external sources.\n\n### State management\n\nThe component uses React Stately's `useListData` hook internally to manage item state and reordering operations. This provides:\n- Efficient list operations (insert, remove, move)\n- Selection state management\n- Synchronization with external state via `onUpdateItems`\n\n> [!TIP]\\\n> See [React Aria Drag and Drop](https://react-spectrum.adobe.com/react-aria/dnd.html) for complete API reference and advanced usage.\n\n## Usage examples\n\n### Size options\n\nThe `sm` and `md` size variants are available to match your interface density:\n\n```jsx live-dev\nconst App = () => {\n const [smallItems, setSmallItems] = useState([\n { key: '1', label: 'Task 1' },\n { key: '2', label: 'Task 2' },\n ]);\n\n const [mediumItems, setMediumItems] = useState([\n { key: '3', label: 'Task 3' },\n { key: '4', label: 'Task 4' },\n ]);\n\n return (\n <Stack direction=\"row\" gap=\"400\">\n <DraggableList.Root\n items={smallItems}\n onUpdateItems={setSmallItems}\n size=\"sm\"\n aria-label=\"Small list\"\n />\n <DraggableList.Root\n items={mediumItems}\n onUpdateItems={setMediumItems}\n size=\"md\"\n aria-label=\"Medium list\"\n />\n </Stack>\n );\n}\n```\n\n### Item removal\n\nEnable item removal by setting `removableItems={true}`. When items are removed, the `onUpdateItems` callback receives the updated list:\n\n```jsx live-dev\nconst App = () => {\n const [items, setItems] = useState([\n { key: '1', label: 'Item 1' },\n { key: '2', label: 'Item 2' },\n { key: '3', label: 'Item 3' },\n ]);\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <DraggableList.Root\n items={items}\n removableItems\n onUpdateItems={setItems}\n aria-label=\"Removable items\"\n />\n <Text fontSize=\"sm\">Items: {items.length}</Text>\n </Stack>\n );\n}\n```\n\n### Custom item rendering\n\nFor items without `key` and `label` properties, or when you need custom rendering, provide a render function as children:\n\n```jsx live-dev\nconst App = () => {\n const items = [\n { id: 'task-1', title: 'Design mockups', priority: 'High' },\n { id: 'task-2', title: 'Write tests', priority: 'Medium' },\n { id: 'task-3', title: 'Review PR', priority: 'Low' },\n ];\n\n return (\n <DraggableList.Root\n items={items}\n getKey={(item) => item.id}\n aria-label=\"Task priorities\"\n >\n {(item) => (\n <DraggableList.Item id={item.id}>\n <Stack direction=\"column\" gap=\"100\" padding=\"200\">\n <Text fontWeight=\"500\">{item.title}</Text>\n <Text fontSize=\"sm\" color=\"fg.muted\">\n Priority: {item.priority}\n </Text>\n </Stack>\n </DraggableList.Item>\n )}\n </DraggableList.Root>\n );\n}\n```\n\n**Note:** When using custom rendering, you must provide a `getKey` function to extract unique keys from your items. Wrap this function in `useCallback` to prevent unnecessary re-synchronization:\n\n```tsx\nconst getKey = useCallback((item) => item.id, []);\n```\n\n### Controlled updates\n\nUse the `onUpdateItems` callback to track changes when items are reordered or removed:\n\n```jsx live-dev\nconst App = () => {\n const [items, setItems] = useState([\n { key: '1', label: 'High priority' },\n { key: '2', label: 'Medium priority' },\n { key: '3', label: 'Low priority' },\n ]);\n\n const [updateCount, setUpdateCount] = useState(0);\n\n const handleUpdateItems = useCallback((updatedItems) => {\n setItems(updatedItems);\n setUpdateCount(prev => prev + 1);\n }, []);\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <DraggableList.Root\n items={items}\n onUpdateItems={handleUpdateItems}\n aria-label=\"Priority list\"\n />\n <Text fontSize=\"sm\">\n Order: {items.map(item => item.label).join(', ')}\n </Text>\n <Text fontSize=\"sm\">Updates: {updateCount}</Text>\n </Stack>\n );\n}\n```\n\n### Custom empty state\n\nCustomize the message displayed when the list is empty:\n\n```jsx live-dev\nconst App = () => {\n const [items, setItems] = useState([]);\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <DraggableList.Root\n items={items}\n onUpdateItems={setItems}\n renderEmptyState=\"No tasks yet. Add some items!\"\n aria-label=\"Empty task list\"\n />\n <Button\n size=\"md\"\n onClick={() => {\n const newItem = {\n key: Date.now().toString(),\n label: `Task ${items.length + 1}`,\n };\n setItems([...items, newItem]);\n }}\n >\n Add Task\n </Button>\n </Stack>\n );\n}\n```\n\n### Multiple lists\n\nItems can be dragged between multiple DraggableList instances:\n\n```jsx live-dev\nconst App = () => {\n const [todoItems, setTodoItems] = useState([\n { key: '1', label: 'Design homepage' },\n { key: '2', label: 'Write docs' },\n ]);\n\n const [doneItems, setDoneItems] = useState([\n { key: '3', label: 'Setup project' },\n ]);\n\n return (\n <Stack direction=\"row\" gap=\"400\">\n <Stack direction=\"column\" gap=\"200\">\n <Text fontWeight=\"500\">To Do</Text>\n <DraggableList.Root\n items={todoItems}\n onUpdateItems={setTodoItems}\n aria-label=\"Todo list\"\n />\n </Stack>\n <Stack direction=\"column\" gap=\"200\">\n <Text fontWeight=\"500\">Done</Text>\n <DraggableList.Root\n items={doneItems}\n onUpdateItems={setDoneItems}\n aria-label=\"Done list\"\n />\n </Stack>\n </Stack>\n );\n}\n```\n\n### TypeScript generics\n\nUse TypeScript generics to ensure type safety with your custom item data:\n\n```jsx live-dev\nconst App = () => {\n type TaskItem = {\n key: string;\n label: string;\n assignee: string;\n status: 'pending' | 'active' | 'done';\n };\n\n const [tasks, setTasks] = useState<TaskItem[]>([\n { key: '1', label: 'Review code', assignee: 'Alice', status: 'active' },\n { key: '2', label: 'Write tests', assignee: 'Bob', status: 'pending' },\n ]);\n\n return (\n <DraggableList.Root<TaskItem>\n items={tasks}\n onUpdateItems={setTasks}\n aria-label=\"Team tasks\"\n >\n {(item) => (\n <DraggableList.Item id={item.key}>\n <Stack direction=\"column\" gap=\"100\" padding=\"200\">\n <Text fontWeight=\"500\">{item.label}</Text>\n <Text fontSize=\"sm\" color=\"fg.muted\">\n {item.assignee} • {item.status}\n </Text>\n </Stack>\n </DraggableList.Item>\n )}\n </DraggableList.Root>\n );\n}\n```\n\n## Component requirements\n\n## Accessibility\n\nThe DraggableList handles most accessibility requirements internally through React Aria's GridList component. However, you must always provide an accessible label using one of these methods:\n\n**Using DraggableList.Field (recommended):**\n\n```tsx\n<DraggableList.Field\n label={msg.format(labelMessage)}\n items={items}\n/>\n```\n\n**Using aria-labelledby:**\n\n```tsx\n<label id=\"list-label\">\n {msg.format(labelMessage)}\n</label>\n<DraggableList.Root\n aria-labelledby=\"list-label\"\n items={items}\n/>\n```\n\n**Using aria-label:**\n\n```tsx\n<DraggableList.Root\n aria-label={msg.format(labelMessage)}\n items={items}\n/>\n```\n\nIf your use case requires tracking and analytics for this component, it is good practice to add a **persistent**, **unique** id to the component:\n\n```tsx\nconst PERSISTENT_ID = \"example-draggable-list\";\n\nexport const Example = () => (\n <DraggableList.Root\n id={PERSISTENT_ID}\n aria-label=\"Task list\"\n items={items}\n />\n);\n```\n\n#### Keyboard navigation\n\nThe component supports full keyboard interaction:\n- `Tab` / `Shift+Tab`: Move focus between items and drag handles\n- `Arrow keys`: Navigate between items in the list\n- `Enter` / `Space`: Activate drag mode for the focused item\n- `Arrow keys` (during drag): Move the item up or down in the list\n- `Enter` / `Space` (during drag): Drop the item at the current position\n- `Escape` (during drag): Cancel the drag operation\n\n## API reference\n\n<PropsTable id=\"DraggableList\" />\n\n## Common patterns\n\n### Form integration with validation\n\nIntegrate DraggableList with form libraries like Formik for validated priority lists:\n\n```jsx live-dev\nconst App = () => {\n const [items, setItems] = useState([\n { key: '1', label: 'Critical bug' },\n { key: '2', label: 'Feature request' },\n { key: '3', label: 'Documentation' },\n ]);\n\n const [touched, setTouched] = useState(false);\n const hasMinimumItems = items.length >= 2;\n\n const handleUpdateItems = useCallback((updated) => {\n setItems(updated);\n setTouched(true);\n }, []);\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <DraggableList.Field\n label=\"Priority Order\"\n description=\"Drag to reorder by priority\"\n items={items}\n onUpdateItems={handleUpdateItems}\n error={touched && !hasMinimumItems ? 'At least 2 items required' : undefined}\n isInvalid={touched && !hasMinimumItems}\n aria-label=\"Priority list\"\n />\n <Text fontSize=\"sm\" color={hasMinimumItems ? 'positive.11' : 'critical.11'}>\n {hasMinimumItems ? '✓ Valid' : '✗ Invalid: Add more items'}\n </Text>\n </Stack>\n );\n}\n```\n\n### Dynamic item management\n\nAllow users to add, remove, and reorder items dynamically:\n\n```jsx live-dev\nconst App = () => {\n const [items, setItems] = useState([\n { key: '1', label: 'Task 1' },\n { key: '2', label: 'Task 2' },\n ]);\n const [nextId, setNextId] = useState(3);\n\n const addItem = () => {\n const newItem = {\n key: nextId.toString(),\n label: `Task ${nextId}`,\n };\n setItems([...items, newItem]);\n setNextId(nextId + 1);\n };\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <DraggableList.Root\n items={items}\n removableItems\n onUpdateItems={setItems}\n aria-label=\"Task manager\"\n />\n <Button size=\"md\" onClick={addItem}>\n Add Task\n </Button>\n </Stack>\n );\n}\n```\n\n### Synchronized state with external storage\n\nSync list order with external state management or API:\n\n```jsx live-dev\nconst App = () => {\n const [items, setItems] = useState([\n { key: '1', label: 'Design system' },\n { key: '2', label: 'API integration' },\n { key: '3', label: 'Testing' },\n ]);\n\n const [lastSaved, setLastSaved] = useState('Not saved');\n\n const handleUpdateItems = useCallback((updatedItems) => {\n setItems(updatedItems);\n\n // Simulate API save\n setTimeout(() => {\n setLastSaved(new Date().toLocaleTimeString());\n }, 500);\n }, []);\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <DraggableList.Root\n items={items}\n onUpdateItems={handleUpdateItems}\n aria-label=\"Project phases\"\n />\n <Text fontSize=\"sm\" color=\"fg.muted\">\n Last saved: {lastSaved}\n </Text>\n </Stack>\n );\n}\n```\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using DraggableList within your application. As the component's internal functionality is already tested by Nimbus, these patterns help you verify your integration and application-specific logic.\n\n### Basic Rendering Tests\n\nVerify the component renders with expected elements and items\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DraggableList, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"DraggableList - Basic rendering\", () => {\n it(\"renders list with items\", () => {\n render(\n <NimbusProvider>\n <DraggableList.Root items={mockItems} aria-label=\"Task list\" />\n </NimbusProvider>\n );\n\n // Verify list renders with grid role\n expect(\n screen.getByRole(\"grid\", { name: /task list/i })\n ).toBeInTheDocument();\n\n // Verify all items are rendered\n expect(screen.getAllByRole(\"row\")).toHaveLength(mockItems.length);\n });\n\n it(\"renders items with accessible labels\", () => {\n render(\n <NimbusProvider>\n <DraggableList.Root items={mockItems} aria-label=\"Test list\" />\n </NimbusProvider>\n );\n\n mockItems.forEach((item) => {\n expect(screen.getByLabelText(item.label)).toBeInTheDocument();\n });\n });\n});\n```\n\n### Size Variant Tests\n\nVerify different size variants render correctly\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DraggableList, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"DraggableList - Size variants\", () => {\n it(\"renders with small size\", () => {\n const items = [{ key: \"1\", label: \"Item 1\" }];\n\n render(\n <NimbusProvider>\n <DraggableList.Root items={items} size=\"sm\" aria-label=\"Small list\" />\n </NimbusProvider>\n );\n\n const grid = screen.getByRole(\"grid\", { name: /small list/i });\n expect(grid).toBeInTheDocument();\n });\n\n it(\"renders with medium size\", () => {\n const items = [{ key: \"1\", label: \"Item 1\" }];\n\n render(\n <NimbusProvider>\n <DraggableList.Root items={items} size=\"md\" aria-label=\"Medium list\" />\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"grid\")).toBeInTheDocument();\n });\n});\n```\n\n### Item Removal Tests\n\nTest removable items functionality\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DraggableList, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"DraggableList - Item removal\", () => {\n it(\"calls onUpdateItems when item is removed\", async () => {\n const user = userEvent.setup();\n const handleUpdateItems = vi.fn();\n const items = [\n { key: \"1\", label: \"Item 1\" },\n { key: \"2\", label: \"Item 2\" },\n { key: \"3\", label: \"Item 3\" },\n ];\n\n render(\n <NimbusProvider>\n <DraggableList.Root\n items={items}\n removableItems\n onUpdateItems={handleUpdateItems}\n aria-label=\"Test list\"\n />\n </NimbusProvider>\n );\n\n // Find and click remove button for first item\n const removeButtons = screen.getAllByRole(\"button\", {\n name: /remove/i,\n });\n await user.click(removeButtons[0]);\n\n // Verify callback was called with updated list\n // Note: The callback receives the updated items array\n expect(handleUpdateItems).toHaveBeenCalled();\n const lastCall =\n handleUpdateItems.mock.calls[handleUpdateItems.mock.calls.length - 1][0];\n expect(lastCall.length).toBeLessThan(items.length);\n });\n});\n```\n\n### Controlled Mode Tests\n\nTest controlled state management with onUpdateItems\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DraggableList, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"DraggableList - Controlled mode\", () => {\n it(\"calls onUpdateItems when items are reordered\", () => {\n const handleUpdateItems = vi.fn();\n const items = [\n { key: \"1\", label: \"Item 1\" },\n { key: \"2\", label: \"Item 2\" },\n ];\n\n render(\n <NimbusProvider>\n <DraggableList.Root\n items={items}\n onUpdateItems={handleUpdateItems}\n aria-label=\"Test list\"\n />\n </NimbusProvider>\n );\n\n // Test that the callback is provided\n expect(handleUpdateItems).toBeDefined();\n });\n\n it(\"calls onUpdateItems when items change\", () => {\n const handleUpdate = vi.fn();\n const items = [\n { key: \"1\", label: \"Item 1\" },\n { key: \"2\", label: \"Item 2\" },\n ];\n\n render(\n <NimbusProvider>\n <DraggableList.Root\n items={items}\n onUpdateItems={handleUpdate}\n aria-label=\"Test list\"\n />\n </NimbusProvider>\n );\n\n // Note: Testing actual drag-and-drop interactions requires browser-based testing\n // (like Storybook tests). These tests verify your integration patterns.\n expect(screen.getByRole(\"grid\")).toBeInTheDocument();\n });\n});\n```\n\n### Custom Rendering Tests\n\nTest custom item rendering with render functions\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DraggableList, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"DraggableList - Custom rendering\", () => {\n it(\"renders items using custom render function\", () => {\n const items = [\n { id: \"1\", title: \"Task 1\", priority: \"High\" },\n { id: \"2\", title: \"Task 2\", priority: \"Low\" },\n ];\n\n render(\n <NimbusProvider>\n <DraggableList.Root\n items={items}\n getKey={(item) => item.id}\n aria-label=\"Custom rendered list\"\n >\n {(item) => (\n <DraggableList.Item id={item.id} textValue={item.title}>\n <div>\n <div>{item.title}</div>\n <div>Priority: {item.priority}</div>\n </div>\n </DraggableList.Item>\n )}\n </DraggableList.Root>\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Task 1\")).toBeInTheDocument();\n expect(screen.getByText(\"Priority: High\")).toBeInTheDocument();\n expect(screen.getByText(\"Task 2\")).toBeInTheDocument();\n });\n});\n```\n\n### Controlled Mode Tests\n\nTest controlled updates with onUpdateItems callback\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DraggableList, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"DraggableList - Controlled updates\", () => {\n it(\"calls onUpdateItems when items are reordered\", async () => {\n const items = [\n { key: \"1\", label: \"Item 1\" },\n { key: \"2\", label: \"Item 2\" },\n ];\n const handleUpdate = vi.fn();\n\n render(\n <NimbusProvider>\n <DraggableList.Root\n items={items}\n onUpdateItems={handleUpdate}\n aria-label=\"Controlled list\"\n />\n </NimbusProvider>\n );\n\n // Note: Full drag-and-drop testing requires complex keyboard/mouse simulation\n // This test verifies the callback is properly connected\n // The callback may be called on mount for internal state synchronization\n expect(screen.getByRole(\"grid\")).toBeInTheDocument();\n });\n\n it(\"syncs external state changes to list\", () => {\n const { rerender } = render(\n <NimbusProvider>\n <DraggableList.Root\n items={[{ key: \"1\", label: \"Item 1\" }]}\n aria-label=\"Synced list\"\n />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Item 1\")).toBeInTheDocument();\n\n // Update items prop\n rerender(\n <NimbusProvider>\n <DraggableList.Root\n items={[\n { key: \"1\", label: \"Item 1\" },\n { key: \"2\", label: \"Item 2\" },\n ]}\n aria-label=\"Synced list\"\n />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Item 2\")).toBeInTheDocument();\n });\n});\n```\n\n### Empty State Tests\n\nTest empty state rendering and customization\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DraggableList, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"DraggableList - Empty state\", () => {\n it(\"displays default empty state when list has no items\", () => {\n render(\n <NimbusProvider>\n <DraggableList.Root items={[]} aria-label=\"Empty list\" />\n </NimbusProvider>\n );\n\n expect(screen.getByText(/drop items here/i)).toBeInTheDocument();\n });\n\n it(\"displays custom empty state message\", () => {\n render(\n <NimbusProvider>\n <DraggableList.Root\n items={[]}\n renderEmptyState=\"No tasks available\"\n aria-label=\"Empty list\"\n />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"No tasks available\")).toBeInTheDocument();\n });\n});\n```\n\n### Field Pattern Tests\n\nTest DraggableList.Field form integration\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DraggableList, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"DraggableList.Field - Form integration\", () => {\n it(\"renders field with label and description\", () => {\n const items = [\n { key: \"1\", label: \"Task 1\" },\n { key: \"2\", label: \"Task 2\" },\n ];\n\n render(\n <NimbusProvider>\n <DraggableList.Field\n label=\"Task Priority\"\n description=\"Drag to reorder\"\n items={items}\n aria-label=\"Priority field\"\n />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Task Priority\")).toBeInTheDocument();\n expect(screen.getByText(\"Drag to reorder\")).toBeInTheDocument();\n });\n\n it(\"displays error message when validation fails\", () => {\n render(\n <NimbusProvider>\n <DraggableList.Field\n label=\"Required List\"\n items={[]}\n error=\"This field is required\"\n isInvalid={true}\n aria-label=\"Required field\"\n />\n </NimbusProvider>\n );\n\n // The error is wrapped in FormField.Error component\n expect(screen.getByText(\"This field is required\")).toBeInTheDocument();\n });\n});\n```\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-draggablelist--docs)\n- [React Aria Drag and Drop](https://react-spectrum.adobe.com/react-aria/dnd.html)\n",
188
+ "mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { DraggableList, type DraggableListRootProps } from '@commercetools/nimbus';\n```\n\n### Basic usage\n\nThe simplest implementation uses an array of items with `key` and `label` properties. The component automatically handles drag-and-drop reordering:\n\n```jsx live-dev\nconst App = () => {\n const items = [\n { key: '1', label: 'First item' },\n { key: '2', label: 'Second item' },\n { key: '3', label: 'Third item' },\n ];\n\n return (\n <DraggableList.Root\n items={items}\n aria-label=\"Task list\"\n />\n );\n}\n```\n\n**Important:** You must provide either an `aria-label` or `aria-labelledby` prop for accessibility. The drag-and-drop functionality requires an accessible label.\n\n## Working with React Aria drag-and-drop\n\nThe DraggableList relies on React Aria Components' GridList and drag-and-drop hooks for accessible drag-and-drop functionality. This integration provides keyboard navigation, screen reader support, and touch-friendly interactions.\n\n### Drag-and-drop behavior\n\nReact Aria's drag-and-drop system provides:\n- **Visual feedback**: Items show grab cursors and drag previews during interactions\n- **Drop indicators**: Visual cues show where items will be placed\n- **Move operations**: Items are moved (not copied) between lists\n- **Keyboard support**: Full keyboard navigation for drag-and-drop\n\n### Data format\n\nItems being dragged use a custom format (`nimbus-draggable-list-item`) that allows items to be dragged between multiple DraggableList instances while preventing drops from external sources. When the `dragNamespace` prop is set, the format is scoped to that namespace (e.g. `nimbus-draggable-list-item:fruits`), so only lists sharing the same namespace can exchange items.\n\n### State management\n\nThe component uses React Stately's `useListData` hook internally to manage item state and reordering operations. This provides:\n- Efficient list operations (insert, remove, move)\n- Selection state management\n- Synchronization with external state via `onUpdateItems`\n\n> [!TIP]\\\n> See [React Aria Drag and Drop](https://react-spectrum.adobe.com/react-aria/dnd.html) for complete API reference and advanced usage.\n\n## Usage examples\n\n### Size options\n\nThe `sm` and `md` size variants are available to match your interface density:\n\n```jsx live-dev\nconst App = () => {\n const [smallItems, setSmallItems] = useState([\n { key: '1', label: 'Task 1' },\n { key: '2', label: 'Task 2' },\n ]);\n\n const [mediumItems, setMediumItems] = useState([\n { key: '3', label: 'Task 3' },\n { key: '4', label: 'Task 4' },\n ]);\n\n return (\n <Stack direction=\"row\" gap=\"400\">\n <DraggableList.Root\n items={smallItems}\n onUpdateItems={setSmallItems}\n size=\"sm\"\n aria-label=\"Small list\"\n />\n <DraggableList.Root\n items={mediumItems}\n onUpdateItems={setMediumItems}\n size=\"md\"\n aria-label=\"Medium list\"\n />\n </Stack>\n );\n}\n```\n\n### Single selection\n\nUse `selectionMode=\"single\"` to allow users to select one item at a time. Clicking a row selects it; clicking another row changes the selection. No checkboxes are rendered — the row itself is the click target:\n\n```jsx live-dev\nconst App = () => {\n const [selected, setSelected] = useState(new Set());\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <DraggableList.Root\n items={[\n { key: '1', label: 'Option A' },\n { key: '2', label: 'Option B' },\n { key: '3', label: 'Option C' },\n ]}\n selectionMode=\"single\"\n selectedKeys={selected}\n onSelectionChange={setSelected}\n aria-label=\"Single selection list\"\n />\n <Text fontSize=\"sm\">\n Selected: {[...selected].join(', ') || 'none'}\n </Text>\n </Stack>\n );\n}\n```\n\n### Multiple selection\n\nUse `selectionMode=\"multiple\"` to allow users to select any number of items. Each item renders a checkbox for toggling selection independently:\n\n```jsx live-dev\nconst App = () => {\n const [selected, setSelected] = useState(new Set());\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <DraggableList.Root\n items={[\n { key: '1', label: 'Task A' },\n { key: '2', label: 'Task B' },\n { key: '3', label: 'Task C' },\n ]}\n selectionMode=\"multiple\"\n selectedKeys={selected}\n onSelectionChange={setSelected}\n aria-label=\"Multiple selection list\"\n />\n <Text fontSize=\"sm\">\n Selected: {[...selected].join(', ') || 'none'}\n </Text>\n </Stack>\n );\n}\n```\n\n> [!NOTE]\n> Selection props (`selectionMode`, `selectedKeys`, `onSelectionChange`, `defaultSelectedKeys`, `disallowEmptySelection`) are passed through to the underlying React Aria GridList. See the [React Aria GridList docs](https://react-spectrum.adobe.com/react-aria/GridList.html#selection) for the full selection API.\n\n### Item removal\n\nEnable item removal by setting `removableItems={true}`. When items are removed, the `onUpdateItems` callback receives the updated list:\n\n```jsx live-dev\nconst App = () => {\n const [items, setItems] = useState([\n { key: '1', label: 'Item 1' },\n { key: '2', label: 'Item 2' },\n { key: '3', label: 'Item 3' },\n ]);\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <DraggableList.Root\n items={items}\n removableItems\n onUpdateItems={setItems}\n aria-label=\"Removable items\"\n />\n <Text fontSize=\"sm\">Items: {items.length}</Text>\n </Stack>\n );\n}\n```\n\n### Custom item rendering\n\nFor items without `key` and `label` properties, or when you need custom rendering, provide a render function as children:\n\n```jsx live-dev\nconst App = () => {\n const items = [\n { id: 'task-1', title: 'Design mockups', priority: 'High' },\n { id: 'task-2', title: 'Write tests', priority: 'Medium' },\n { id: 'task-3', title: 'Review PR', priority: 'Low' },\n ];\n\n return (\n <DraggableList.Root\n items={items}\n getKey={(item) => item.id}\n aria-label=\"Task priorities\"\n >\n {(item) => (\n <DraggableList.Item id={item.id}>\n <Stack direction=\"column\" gap=\"100\" padding=\"200\">\n <Text fontWeight=\"500\">{item.title}</Text>\n <Text fontSize=\"sm\" color=\"fg.muted\">\n Priority: {item.priority}\n </Text>\n </Stack>\n </DraggableList.Item>\n )}\n </DraggableList.Root>\n );\n}\n```\n\n**Note:** When using custom rendering, you must provide a `getKey` function to extract unique keys from your items. Wrap this function in `useCallback` to prevent unnecessary re-synchronization:\n\n```tsx\nconst getKey = useCallback((item) => item.id, []);\n```\n\n### Controlled updates\n\nUse the `onUpdateItems` callback to track changes when items are reordered or removed:\n\n```jsx live-dev\nconst App = () => {\n const [items, setItems] = useState([\n { key: '1', label: 'High priority' },\n { key: '2', label: 'Medium priority' },\n { key: '3', label: 'Low priority' },\n ]);\n\n const [updateCount, setUpdateCount] = useState(0);\n\n const handleUpdateItems = useCallback((updatedItems) => {\n setItems(updatedItems);\n setUpdateCount(prev => prev + 1);\n }, []);\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <DraggableList.Root\n items={items}\n onUpdateItems={handleUpdateItems}\n aria-label=\"Priority list\"\n />\n <Text fontSize=\"sm\">\n Order: {items.map(item => item.label).join(', ')}\n </Text>\n <Text fontSize=\"sm\">Updates: {updateCount}</Text>\n </Stack>\n );\n}\n```\n\n### Custom empty state\n\nCustomize the message displayed when the list is empty:\n\n```jsx live-dev\nconst App = () => {\n const [items, setItems] = useState([]);\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <DraggableList.Root\n items={items}\n onUpdateItems={setItems}\n renderEmptyState=\"No tasks yet. Add some items!\"\n aria-label=\"Empty task list\"\n />\n <Button\n size=\"md\"\n onClick={() => {\n const newItem = {\n key: Date.now().toString(),\n label: `Task ${items.length + 1}`,\n };\n setItems([...items, newItem]);\n }}\n >\n Add Task\n </Button>\n </Stack>\n );\n}\n```\n\n### Multiple lists\n\nItems can be dragged between multiple DraggableList instances:\n\n```jsx live-dev\nconst App = () => {\n const [todoItems, setTodoItems] = useState([\n { key: '1', label: 'Design homepage' },\n { key: '2', label: 'Write docs' },\n ]);\n\n const [doneItems, setDoneItems] = useState([\n { key: '3', label: 'Setup project' },\n ]);\n\n return (\n <Stack direction=\"row\" gap=\"400\">\n <Stack direction=\"column\" gap=\"200\">\n <Text fontWeight=\"500\">To Do</Text>\n <DraggableList.Root\n items={todoItems}\n onUpdateItems={setTodoItems}\n aria-label=\"Todo list\"\n />\n </Stack>\n <Stack direction=\"column\" gap=\"200\">\n <Text fontWeight=\"500\">Done</Text>\n <DraggableList.Root\n items={doneItems}\n onUpdateItems={setDoneItems}\n aria-label=\"Done list\"\n />\n </Stack>\n </Stack>\n );\n}\n```\n\n### Isolating lists with `dragNamespace`\n\nBy default, all DraggableList instances on a page can exchange items. Use the `dragNamespace` prop to isolate groups of lists so that only lists sharing the same namespace accept each other's items:\n\n```jsx live-dev\nconst App = () => {\n const [fruitsA, setFruitsA] = useState([\n { key: '1', label: 'Apple' },\n { key: '2', label: 'Banana' },\n ]);\n const [fruitsB, setFruitsB] = useState([\n { key: '3', label: 'Cherry' },\n ]);\n const [veggies, setVeggies] = useState([\n { key: '4', label: 'Carrot' },\n { key: '5', label: 'Broccoli' },\n ]);\n\n return (\n <Stack direction=\"row\" gap=\"400\">\n <Stack direction=\"column\" gap=\"200\">\n <Text fontWeight=\"500\">Fruits A</Text>\n <DraggableList.Root\n items={fruitsA}\n onUpdateItems={setFruitsA}\n dragNamespace=\"fruits\"\n aria-label=\"Fruits list A\"\n />\n </Stack>\n <Stack direction=\"column\" gap=\"200\">\n <Text fontWeight=\"500\">Fruits B</Text>\n <DraggableList.Root\n items={fruitsB}\n onUpdateItems={setFruitsB}\n dragNamespace=\"fruits\"\n aria-label=\"Fruits list B\"\n />\n </Stack>\n <Stack direction=\"column\" gap=\"200\">\n <Text fontWeight=\"500\">Vegetables</Text>\n <DraggableList.Root\n items={veggies}\n onUpdateItems={setVeggies}\n dragNamespace=\"vegetables\"\n aria-label=\"Vegetables list\"\n />\n </Stack>\n </Stack>\n );\n}\n```\n\nItems can be dragged between Fruits A and Fruits B (same namespace), but the Vegetables list is isolated and will not accept fruit items or vice versa. Omitting `dragNamespace` preserves the default behavior where all lists interoperate.\n\n### Accepting external drops\n\nUse `onExternalDrop` and `acceptExternalTypes` to accept drops from outside Nimbus. Helper functions are provided for common drop types:\n\n```jsx live-dev\nconst App = () => {\n const [items, setItems] = useState([\n { key: '1', label: 'Existing item' },\n ]);\n\n return (\n <DraggableList.Root\n items={items}\n onUpdateItems={setItems}\n acceptExternalTypes={['text/plain']}\n onExternalDrop={async (dropItems) => {\n const results = [];\n for (const item of dropItems) {\n if (item.kind === 'text') {\n const text = await item.getText('text/plain');\n results.push({ key: crypto.randomUUID(), label: text });\n }\n }\n return results;\n }}\n aria-label=\"Drop text here\"\n />\n );\n}\n```\n\nFor file and directory drops, use the composable helpers:\n\n```tsx\nimport {\n createItemsFromFileDrop,\n createItemsFromDirectoryDrop,\n} from '@commercetools/nimbus';\n\n<DraggableList.Root\n items={items}\n onUpdateItems={setItems}\n acceptExternalTypes={['image/png', 'application/pdf']}\n onExternalDrop={async (dropItems) => [\n ...(await createItemsFromFileDrop(dropItems)),\n ...(await createItemsFromDirectoryDrop(dropItems)),\n ]}\n aria-label=\"Drop files here\"\n/>\n```\n\nSee the [`useDragAndDrop` hook documentation](/hooks/use-drag-and-drop) for the full list of helpers and advanced usage.\n\n### Outgoing drag formats\n\nUse `serializeDragItem` to make items droppable into non-Nimbus targets by providing additional format representations:\n\n```tsx\n<DraggableList.Root\n items={items}\n onUpdateItems={setItems}\n serializeDragItem={(item) => ({\n 'text/plain': item.label,\n 'text/html': `<li>${item.label}</li>`,\n })}\n aria-label=\"Drag items out\"\n/>\n```\n\n### TypeScript generics\n\nUse TypeScript generics to ensure type safety with your custom item data:\n\n```jsx live-dev\nconst App = () => {\n type TaskItem = {\n key: string;\n label: string;\n assignee: string;\n status: 'pending' | 'active' | 'done';\n };\n\n const [tasks, setTasks] = useState<TaskItem[]>([\n { key: '1', label: 'Review code', assignee: 'Alice', status: 'active' },\n { key: '2', label: 'Write tests', assignee: 'Bob', status: 'pending' },\n ]);\n\n return (\n <DraggableList.Root<TaskItem>\n items={tasks}\n onUpdateItems={setTasks}\n aria-label=\"Team tasks\"\n >\n {(item) => (\n <DraggableList.Item id={item.key}>\n <Stack direction=\"column\" gap=\"100\" padding=\"200\">\n <Text fontWeight=\"500\">{item.label}</Text>\n <Text fontSize=\"sm\" color=\"fg.muted\">\n {item.assignee} • {item.status}\n </Text>\n </Stack>\n </DraggableList.Item>\n )}\n </DraggableList.Root>\n );\n}\n```\n\n## Component requirements\n\n## Accessibility\n\nThe DraggableList handles most accessibility requirements internally through React Aria's GridList component. However, you must always provide an accessible label using one of these methods:\n\n**Using DraggableList.Field (recommended):**\n\n```tsx\n<DraggableList.Field\n label={msg.format(labelMessage)}\n items={items}\n/>\n```\n\n**Using aria-labelledby:**\n\n```tsx\n<label id=\"list-label\">\n {msg.format(labelMessage)}\n</label>\n<DraggableList.Root\n aria-labelledby=\"list-label\"\n items={items}\n/>\n```\n\n**Using aria-label:**\n\n```tsx\n<DraggableList.Root\n aria-label={msg.format(labelMessage)}\n items={items}\n/>\n```\n\nIf your use case requires tracking and analytics for this component, it is good practice to add a **persistent**, **unique** id to the component:\n\n```tsx\nconst PERSISTENT_ID = \"example-draggable-list\";\n\nexport const Example = () => (\n <DraggableList.Root\n id={PERSISTENT_ID}\n aria-label=\"Task list\"\n items={items}\n />\n);\n```\n\n#### Keyboard navigation\n\nThe component supports full keyboard interaction:\n- `Tab` / `Shift+Tab`: Move focus between items and drag handles\n- `Arrow keys`: Navigate between items in the list\n- `Enter` / `Space`: Activate drag mode for the focused item\n- `Arrow keys` (during drag): Move the item up or down in the list\n- `Enter` / `Space` (during drag): Drop the item at the current position\n- `Escape` (during drag): Cancel the drag operation\n\n## API reference\n\n<PropsTable id=\"DraggableList\" />\n\n## Common patterns\n\n### Form integration with validation\n\nIntegrate DraggableList with form libraries like Formik for validated priority lists:\n\n```jsx live-dev\nconst App = () => {\n const [items, setItems] = useState([\n { key: '1', label: 'Critical bug' },\n { key: '2', label: 'Feature request' },\n { key: '3', label: 'Documentation' },\n ]);\n\n const [touched, setTouched] = useState(false);\n const hasMinimumItems = items.length >= 2;\n\n const handleUpdateItems = useCallback((updated) => {\n setItems(updated);\n setTouched(true);\n }, []);\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <DraggableList.Field\n label=\"Priority Order\"\n description=\"Drag to reorder by priority\"\n items={items}\n onUpdateItems={handleUpdateItems}\n error={touched && !hasMinimumItems ? 'At least 2 items required' : undefined}\n isInvalid={touched && !hasMinimumItems}\n aria-label=\"Priority list\"\n />\n <Text fontSize=\"sm\" color={hasMinimumItems ? 'positive.11' : 'critical.11'}>\n {hasMinimumItems ? '✓ Valid' : '✗ Invalid: Add more items'}\n </Text>\n </Stack>\n );\n}\n```\n\n### Dynamic item management\n\nAllow users to add, remove, and reorder items dynamically:\n\n```jsx live-dev\nconst App = () => {\n const [items, setItems] = useState([\n { key: '1', label: 'Task 1' },\n { key: '2', label: 'Task 2' },\n ]);\n const [nextId, setNextId] = useState(3);\n\n const addItem = () => {\n const newItem = {\n key: nextId.toString(),\n label: `Task ${nextId}`,\n };\n setItems([...items, newItem]);\n setNextId(nextId + 1);\n };\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <DraggableList.Root\n items={items}\n removableItems\n onUpdateItems={setItems}\n aria-label=\"Task manager\"\n />\n <Button size=\"md\" onClick={addItem}>\n Add Task\n </Button>\n </Stack>\n );\n}\n```\n\n### Synchronized state with external storage\n\nSync list order with external state management or API:\n\n```jsx live-dev\nconst App = () => {\n const [items, setItems] = useState([\n { key: '1', label: 'Design system' },\n { key: '2', label: 'API integration' },\n { key: '3', label: 'Testing' },\n ]);\n\n const [lastSaved, setLastSaved] = useState('Not saved');\n\n const handleUpdateItems = useCallback((updatedItems) => {\n setItems(updatedItems);\n\n // Simulate API save\n setTimeout(() => {\n setLastSaved(new Date().toLocaleTimeString());\n }, 500);\n }, []);\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <DraggableList.Root\n items={items}\n onUpdateItems={handleUpdateItems}\n aria-label=\"Project phases\"\n />\n <Text fontSize=\"sm\" color=\"fg.muted\">\n Last saved: {lastSaved}\n </Text>\n </Stack>\n );\n}\n```\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using DraggableList within your application. As the component's internal functionality is already tested by Nimbus, these patterns help you verify your integration and application-specific logic.\n\n### Basic Rendering Tests\n\nVerify the component renders with expected elements and items\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DraggableList, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"DraggableList - Basic rendering\", () => {\n it(\"renders list with items\", () => {\n render(\n <NimbusProvider>\n <DraggableList.Root items={mockItems} aria-label=\"Task list\" />\n </NimbusProvider>\n );\n\n // Verify list renders with grid role\n expect(\n screen.getByRole(\"grid\", { name: /task list/i })\n ).toBeInTheDocument();\n\n // Verify all items are rendered\n expect(screen.getAllByRole(\"row\")).toHaveLength(mockItems.length);\n });\n\n it(\"renders items with accessible labels\", () => {\n render(\n <NimbusProvider>\n <DraggableList.Root items={mockItems} aria-label=\"Test list\" />\n </NimbusProvider>\n );\n\n mockItems.forEach((item) => {\n expect(screen.getByLabelText(item.label)).toBeInTheDocument();\n });\n });\n});\n```\n\n### Size Variant Tests\n\nVerify different size variants render correctly\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DraggableList, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"DraggableList - Size variants\", () => {\n it(\"renders with small size\", () => {\n const items = [{ key: \"1\", label: \"Item 1\" }];\n\n render(\n <NimbusProvider>\n <DraggableList.Root items={items} size=\"sm\" aria-label=\"Small list\" />\n </NimbusProvider>\n );\n\n const grid = screen.getByRole(\"grid\", { name: /small list/i });\n expect(grid).toBeInTheDocument();\n });\n\n it(\"renders with medium size\", () => {\n const items = [{ key: \"1\", label: \"Item 1\" }];\n\n render(\n <NimbusProvider>\n <DraggableList.Root items={items} size=\"md\" aria-label=\"Medium list\" />\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"grid\")).toBeInTheDocument();\n });\n});\n```\n\n### Item Removal Tests\n\nTest removable items functionality\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DraggableList, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"DraggableList - Item removal\", () => {\n it(\"calls onUpdateItems when item is removed\", async () => {\n const user = userEvent.setup();\n const handleUpdateItems = vi.fn();\n const items = [\n { key: \"1\", label: \"Item 1\" },\n { key: \"2\", label: \"Item 2\" },\n { key: \"3\", label: \"Item 3\" },\n ];\n\n render(\n <NimbusProvider>\n <DraggableList.Root\n items={items}\n removableItems\n onUpdateItems={handleUpdateItems}\n aria-label=\"Test list\"\n />\n </NimbusProvider>\n );\n\n // Find and click remove button for first item\n const removeButtons = screen.getAllByRole(\"button\", {\n name: /remove/i,\n });\n await user.click(removeButtons[0]);\n\n // Verify callback was called with updated list\n // Note: The callback receives the updated items array\n expect(handleUpdateItems).toHaveBeenCalled();\n const lastCall =\n handleUpdateItems.mock.calls[handleUpdateItems.mock.calls.length - 1][0];\n expect(lastCall.length).toBeLessThan(items.length);\n });\n});\n```\n\n### Controlled Mode Tests\n\nTest controlled state management with onUpdateItems\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DraggableList, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"DraggableList - Controlled mode\", () => {\n it(\"calls onUpdateItems when items are reordered\", () => {\n const handleUpdateItems = vi.fn();\n const items = [\n { key: \"1\", label: \"Item 1\" },\n { key: \"2\", label: \"Item 2\" },\n ];\n\n render(\n <NimbusProvider>\n <DraggableList.Root\n items={items}\n onUpdateItems={handleUpdateItems}\n aria-label=\"Test list\"\n />\n </NimbusProvider>\n );\n\n // Test that the callback is provided\n expect(handleUpdateItems).toBeDefined();\n });\n\n it(\"calls onUpdateItems when items change\", () => {\n const handleUpdate = vi.fn();\n const items = [\n { key: \"1\", label: \"Item 1\" },\n { key: \"2\", label: \"Item 2\" },\n ];\n\n render(\n <NimbusProvider>\n <DraggableList.Root\n items={items}\n onUpdateItems={handleUpdate}\n aria-label=\"Test list\"\n />\n </NimbusProvider>\n );\n\n // Note: Testing actual drag-and-drop interactions requires browser-based testing\n // (like Storybook tests). These tests verify your integration patterns.\n expect(screen.getByRole(\"grid\")).toBeInTheDocument();\n });\n});\n```\n\n### Custom Rendering Tests\n\nTest custom item rendering with render functions\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DraggableList, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"DraggableList - Custom rendering\", () => {\n it(\"renders items using custom render function\", () => {\n const items = [\n { id: \"1\", title: \"Task 1\", priority: \"High\" },\n { id: \"2\", title: \"Task 2\", priority: \"Low\" },\n ];\n\n render(\n <NimbusProvider>\n <DraggableList.Root\n items={items}\n getKey={(item) => item.id}\n aria-label=\"Custom rendered list\"\n >\n {(item) => (\n <DraggableList.Item id={item.id} textValue={item.title}>\n <div>\n <div>{item.title}</div>\n <div>Priority: {item.priority}</div>\n </div>\n </DraggableList.Item>\n )}\n </DraggableList.Root>\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Task 1\")).toBeInTheDocument();\n expect(screen.getByText(\"Priority: High\")).toBeInTheDocument();\n expect(screen.getByText(\"Task 2\")).toBeInTheDocument();\n });\n});\n```\n\n### Controlled Mode Tests\n\nTest controlled updates with onUpdateItems callback\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DraggableList, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"DraggableList - Controlled updates\", () => {\n it(\"calls onUpdateItems when items are reordered\", async () => {\n const items = [\n { key: \"1\", label: \"Item 1\" },\n { key: \"2\", label: \"Item 2\" },\n ];\n const handleUpdate = vi.fn();\n\n render(\n <NimbusProvider>\n <DraggableList.Root\n items={items}\n onUpdateItems={handleUpdate}\n aria-label=\"Controlled list\"\n />\n </NimbusProvider>\n );\n\n // Note: Full drag-and-drop testing requires complex keyboard/mouse simulation\n // This test verifies the callback is properly connected\n // The callback may be called on mount for internal state synchronization\n expect(screen.getByRole(\"grid\")).toBeInTheDocument();\n });\n\n it(\"syncs external state changes to list\", () => {\n const { rerender } = render(\n <NimbusProvider>\n <DraggableList.Root\n items={[{ key: \"1\", label: \"Item 1\" }]}\n aria-label=\"Synced list\"\n />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Item 1\")).toBeInTheDocument();\n\n // Update items prop\n rerender(\n <NimbusProvider>\n <DraggableList.Root\n items={[\n { key: \"1\", label: \"Item 1\" },\n { key: \"2\", label: \"Item 2\" },\n ]}\n aria-label=\"Synced list\"\n />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Item 2\")).toBeInTheDocument();\n });\n});\n```\n\n### Empty State Tests\n\nTest empty state rendering and customization\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DraggableList, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"DraggableList - Empty state\", () => {\n it(\"displays default empty state when list has no items\", () => {\n render(\n <NimbusProvider>\n <DraggableList.Root items={[]} aria-label=\"Empty list\" />\n </NimbusProvider>\n );\n\n expect(screen.getByText(/drop items here/i)).toBeInTheDocument();\n });\n\n it(\"displays custom empty state message\", () => {\n render(\n <NimbusProvider>\n <DraggableList.Root\n items={[]}\n renderEmptyState=\"No tasks available\"\n aria-label=\"Empty list\"\n />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"No tasks available\")).toBeInTheDocument();\n });\n});\n```\n\n### Field Pattern Tests\n\nTest DraggableList.Field form integration\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { DraggableList, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"DraggableList.Field - Form integration\", () => {\n it(\"renders field with label and description\", () => {\n const items = [\n { key: \"1\", label: \"Task 1\" },\n { key: \"2\", label: \"Task 2\" },\n ];\n\n render(\n <NimbusProvider>\n <DraggableList.Field\n label=\"Task Priority\"\n description=\"Drag to reorder\"\n items={items}\n aria-label=\"Priority field\"\n />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Task Priority\")).toBeInTheDocument();\n expect(screen.getByText(\"Drag to reorder\")).toBeInTheDocument();\n });\n\n it(\"displays error message when validation fails\", () => {\n render(\n <NimbusProvider>\n <DraggableList.Field\n label=\"Required List\"\n items={[]}\n error=\"This field is required\"\n isInvalid={true}\n aria-label=\"Required field\"\n />\n </NimbusProvider>\n );\n\n // The error is wrapped in FormField.Error component\n expect(screen.getByText(\"This field is required\")).toBeInTheDocument();\n });\n});\n```\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-draggablelist--docs)\n- [React Aria Drag and Drop](https://react-spectrum.adobe.com/react-aria/dnd.html)\n",
189
189
  "toc": [
190
190
  {
191
191
  "value": "Getting started",
@@ -283,6 +283,28 @@
283
283
  ],
284
284
  "parent": "root"
285
285
  },
286
+ {
287
+ "value": "Single selection",
288
+ "href": "#single-selection",
289
+ "depth": 3,
290
+ "numbering": [
291
+ 1,
292
+ 3,
293
+ 2
294
+ ],
295
+ "parent": "root"
296
+ },
297
+ {
298
+ "value": "Multiple selection",
299
+ "href": "#multiple-selection",
300
+ "depth": 3,
301
+ "numbering": [
302
+ 1,
303
+ 3,
304
+ 3
305
+ ],
306
+ "parent": "root"
307
+ },
286
308
  {
287
309
  "value": "Item removal",
288
310
  "href": "#item-removal",
@@ -290,7 +312,7 @@
290
312
  "numbering": [
291
313
  1,
292
314
  3,
293
- 2
315
+ 4
294
316
  ],
295
317
  "parent": "root"
296
318
  },
@@ -301,7 +323,7 @@
301
323
  "numbering": [
302
324
  1,
303
325
  3,
304
- 3
326
+ 5
305
327
  ],
306
328
  "parent": "root"
307
329
  },
@@ -312,7 +334,7 @@
312
334
  "numbering": [
313
335
  1,
314
336
  3,
315
- 4
337
+ 6
316
338
  ],
317
339
  "parent": "root"
318
340
  },
@@ -323,7 +345,7 @@
323
345
  "numbering": [
324
346
  1,
325
347
  3,
326
- 5
348
+ 7
327
349
  ],
328
350
  "parent": "root"
329
351
  },
@@ -334,7 +356,40 @@
334
356
  "numbering": [
335
357
  1,
336
358
  3,
337
- 6
359
+ 8
360
+ ],
361
+ "parent": "root"
362
+ },
363
+ {
364
+ "value": "Isolating lists with dragNamespace",
365
+ "href": "#isolating-lists-with-dragnamespace",
366
+ "depth": 3,
367
+ "numbering": [
368
+ 1,
369
+ 3,
370
+ 9
371
+ ],
372
+ "parent": "root"
373
+ },
374
+ {
375
+ "value": "Accepting external drops",
376
+ "href": "#accepting-external-drops",
377
+ "depth": 3,
378
+ "numbering": [
379
+ 1,
380
+ 3,
381
+ 10
382
+ ],
383
+ "parent": "root"
384
+ },
385
+ {
386
+ "value": "Outgoing drag formats",
387
+ "href": "#outgoing-drag-formats",
388
+ "depth": 3,
389
+ "numbering": [
390
+ 1,
391
+ 3,
392
+ 11
338
393
  ],
339
394
  "parent": "root"
340
395
  },
@@ -345,7 +400,7 @@
345
400
  "numbering": [
346
401
  1,
347
402
  3,
348
- 7
403
+ 12
349
404
  ],
350
405
  "parent": "root"
351
406
  },
@@ -375,7 +375,7 @@
375
375
  ]
376
376
  },
377
377
  "a11y": {
378
- "mdx": "\n## Accessibility\n\nAccessibility ensures that digital content and functionality are usable by\neveryone, including people with disabilities, by addressing visual, auditory,\ncognitive, and physical limitations.\n\n```jsx live\nconst App = () => <Checkbox>Test me</Checkbox>\n```\n\n### Accessibility standards\n\n- **Info and relationships:** Programmatically determine or provide information\n and relationships conveyed through presentation in text. Use the `<label>`\n element and the `for` attribute to associate every checkbox with a clear,\n descriptive label. Ensure screen readers can access and interpret the checkbox\n label.\n- **Group related checkboxes:** Group related checkboxes using `<fieldset>` and\n `<legend>` to provide context. Label the checkbox group using `<legend>`.\n- **Use of color:** Avoid relying solely on color to convey information or\n checkbox state. Ensure the checked/unchecked state is programmatically\n determinable and conveyed through visual appearance (e.g., the checkbox's\n state).\n- **Keyboard accessibility:** Make all checkbox functionality operable through a\n keyboard interface. Enable users to navigate to the checkbox using the Tab\n key. Allow users to change the checkbox state using the Spacebar or Enter key.\n- **Clear purpose:** Make the purpose of the checkbox clear from the surrounding\n context or its label. Avoid generic labels like \"Option\" or \"Select.\" Provide\n specific and descriptive labels.\n- **Focus visible:** Provide a visible keyboard focus indicator for checkboxes.\n Visually indicate when a checkbox has focus (e.g., highlight, outline, change\n in appearance). Avoid relying solely on color change for focus indication.\n- **Labels or instructions:** Provide clear labels or instructions for\n checkboxes. Reinforce the information provided by the `<label>` element. Aid\n users with cognitive disabilities by providing clear instructions.\n",
378
+ "mdx": "\n## Accessibility\n\nAccessibility ensures that digital content and functionality are usable by\neveryone, including people with disabilities, by addressing visual, auditory,\ncognitive, and physical limitations.\n\n```jsx live\nconst App = () => <Checkbox>Test me</Checkbox>\n```\n\n### Accessibility standards\n\n- **Info and relationships:** Programmatically determine or provide information\n and relationships conveyed through presentation in text. Use the `<label>`\n element and the `for` attribute to associate every checkbox with a clear,\n descriptive label. Ensure screen readers can access and interpret the checkbox\n label.\n- **Group related checkboxes:** Group related checkboxes using `<fieldset>` and\n `<legend>` to provide context. Label the checkbox group using `<legend>`.\n- **Use of color:** Avoid relying solely on color to convey information or\n checkbox state. Ensure the checked/unchecked state is programmatically\n determinable and conveyed through visual appearance (e.g., the checkbox's\n state).\n- **Keyboard accessibility:** Make all checkbox functionality operable through a\n keyboard interface. Enable users to navigate to the checkbox using the Tab\n key. Allow users to change the checkbox state using the Spacebar.\n- **Clear purpose:** Make the purpose of the checkbox clear from the surrounding\n context or its label. Avoid generic labels like \"Option\" or \"Select.\" Provide\n specific and descriptive labels.\n- **Focus visible:** Provide a visible keyboard focus indicator for checkboxes.\n Visually indicate when a checkbox has focus (e.g., highlight, outline, change\n in appearance). Avoid relying solely on color change for focus indication.\n- **Labels or instructions:** Provide clear labels or instructions for\n checkboxes. Reinforce the information provided by the `<label>` element. Aid\n users with cognitive disabilities by providing clear instructions.\n",
379
379
  "toc": [
380
380
  {
381
381
  "value": "Accessibility",
@@ -401,7 +401,7 @@
401
401
  ]
402
402
  },
403
403
  "dev": {
404
- "mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { Checkbox, type CheckboxProps } from '@commercetools/nimbus';\n```\n\n### Basic usage\n\nThe simplest implementation uses uncontrolled mode:\n\n```jsx live-dev\nconst App = () => (\n <Checkbox>Accept terms and conditions</Checkbox>\n)\n```\n\n## Usage examples\n\n### Selection states\n\nThe Checkbox supports three selection states: unchecked (default), checked, and indeterminate.\n\n#### Unchecked\n\nThe default state when no selection has been made:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <Checkbox>Option 1</Checkbox>\n <Checkbox>Option 2</Checkbox>\n </Stack>\n)\n```\n\n#### Checked\n\nUse the `isSelected` prop to indicate a checked state:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <Checkbox isSelected>Option 1</Checkbox>\n <Checkbox isSelected>Option 2</Checkbox>\n </Stack>\n)\n```\n\n#### Indeterminate\n\nUse the `isIndeterminate` prop to show a partial selection state, commonly used for \"select all\" scenarios:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <Checkbox isIndeterminate>Select all</Checkbox>\n <Checkbox isSelected>Option 1</Checkbox>\n <Checkbox>Option 2</Checkbox>\n </Stack>\n)\n```\n\n### Validation and disabled states\n\n#### Invalid state\n\nUse the `isInvalid` prop to indicate validation errors:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <Checkbox isInvalid>Required field</Checkbox>\n <Checkbox isSelected isInvalid>Invalid selection</Checkbox>\n </Stack>\n)\n```\n\n#### Disabled state\n\nDisable checkbox interaction with the `isDisabled` prop:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <Checkbox isDisabled>Disabled checkbox</Checkbox>\n <Checkbox isSelected isDisabled>Disabled and checked</Checkbox>\n <Checkbox isIndeterminate isDisabled>Disabled and indeterminate</Checkbox>\n </Stack>\n)\n```\n\n### Uncontrolled mode\n\nFor simpler use cases, use uncontrolled mode with `defaultValue` and `onChange`:\n\n```jsx live-dev\nconst App = () => {\n const [displayValue, setDisplayValue] = useState<boolean>(false);\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <Text fontSize=\"sm\">\n {displayValue ? 'Notifications enabled' : 'Notifications disabled'}\n </Text>\n <Checkbox\n defaultValue={false}\n onChange={(isSelected) => {\n setDisplayValue(isSelected);\n }}\n >\n Enable notifications\n </Checkbox>\n </Stack>\n );\n}\n```\n\nUse uncontrolled mode when you need to capture the selected state without managing state yourself.\n\n### Controlled mode\n\nFor scenarios requiring programmatic control or coordination with other components, use controlled mode:\n\n```jsx live-dev\nconst App = () => {\n const [isSelected, setIsSelected] = useState<boolean>(false);\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <Text fontSize=\"sm\">\n {isSelected ? 'Terms accepted' : 'Terms not accepted'}\n </Text>\n <Checkbox\n isSelected={isSelected}\n onChange={setIsSelected}\n >\n Accept terms and conditions\n </Checkbox>\n <Button\n size=\"sm\"\n onClick={() => setIsSelected(!isSelected)}\n >\n {isSelected ? 'Uncheck' : 'Check'}\n </Button>\n </Stack>\n );\n}\n```\n\nUse controlled mode when you need to:\n- Synchronize the value with external state\n- Validate or transform selections\n- Clear or programmatically set the value\n- React to changes in real-time\n\n## Component requirements\n\n## Accessibility\n\nThe Checkbox handles most accessibility requirements internally. However, you must always associate an internationalized label with the component. Visual labels are preferable, and can be set by:\n\n- Using the `CheckboxField` pattern component (recommended, if available)\n- Providing label content via the `children` prop:\n\n```tsx\n<Checkbox>\n {msg.format(checkboxLabelMessage)}\n</Checkbox>\n```\n\n- Associating a `<label>` element with the `Checkbox` using `aria-labelledby`:\n\n```tsx\n<label id=\"checkbox-label\">\n {msg.format(labelMessage)}\n</label>\n<Checkbox aria-labelledby=\"checkbox-label\" />\n```\n\n- Associating a `<label>` element with the `Checkbox` using `htmlFor`:\n\n```tsx\n<label htmlFor=\"checkbox-id\">\n {msg.format(labelMessage)}\n</label>\n<Checkbox id=\"checkbox-id\" />\n```\n\nIf your design requires that the label should not be visible, the label should be set using the `aria-label` prop:\n\n```tsx\n<Checkbox aria-label={msg.format(labelMessage)} />\n```\n\nIf your use case requires tracking and analytics for this component, it is good practice to add a **persistent**, **unique** id to the component:\n\n```tsx\nconst PERSISTENT_ID = \"terms-acceptance-checkbox\";\n\nexport const TermsCheckbox = () => (\n <Checkbox id={PERSISTENT_ID}>\n Accept terms and conditions\n </Checkbox>\n);\n```\n\n#### Keyboard navigation\n\nThe component supports full keyboard interaction:\n- `Tab` / `Shift+Tab`: Navigate to/from the checkbox\n- `Space` / `Enter`: Toggle the checkbox state when focused\n\n## API reference\n\n<PropsTable id=\"Checkbox\" />\n\n## Common patterns\n\n### Checkbox group\n\nGroup related checkboxes together for multiple selections:\n\n```jsx live-dev\nconst App = () => {\n const [selectedOptions, setSelectedOptions] = useState<string[]>([]);\n\n const options = [\n { id: 'option1', label: 'Email notifications' },\n { id: 'option2', label: 'SMS notifications' },\n { id: 'option3', label: 'Push notifications' },\n ];\n\n const handleToggle = (optionId: string) => {\n setSelectedOptions((prev) =>\n prev.includes(optionId)\n ? prev.filter((id) => id !== optionId)\n : [...prev, optionId]\n );\n };\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n {options.map((option) => (\n <Checkbox\n key={option.id}\n isSelected={selectedOptions.includes(option.id)}\n onChange={() => handleToggle(option.id)}\n >\n {option.label}\n </Checkbox>\n ))}\n <Text fontSize=\"sm\">\n Selected: {selectedOptions.length} of {options.length}\n </Text>\n </Stack>\n );\n}\n```\n\n### Select all pattern\n\nImplement a \"select all\" checkbox with indeterminate state:\n\n```jsx live-dev\nconst App = () => {\n const [items, setItems] = useState([\n { id: '1', label: 'Item 1', selected: false },\n { id: '2', label: 'Item 2', selected: true },\n { id: '3', label: 'Item 3', selected: false },\n ]);\n\n const allSelected = items.every((item) => item.selected);\n const someSelected = items.some((item) => item.selected) && !allSelected;\n\n const handleSelectAll = (isSelected: boolean) => {\n setItems((prev) =>\n prev.map((item) => ({ ...item, selected: isSelected }))\n );\n };\n\n const handleToggleItem = (id: string) => {\n setItems((prev) =>\n prev.map((item) =>\n item.id === id ? { ...item, selected: !item.selected } : item\n )\n );\n };\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <Checkbox\n isSelected={allSelected}\n isIndeterminate={someSelected}\n onChange={handleSelectAll}\n >\n Select all\n </Checkbox>\n <Stack direction=\"column\" gap=\"200\" ml=\"600\">\n {items.map((item) => (\n <Checkbox\n key={item.id}\n isSelected={item.selected}\n onChange={() => handleToggleItem(item.id)}\n >\n {item.label}\n </Checkbox>\n ))}\n </Stack>\n </Stack>\n );\n}\n```\n\n### Form integration example\n\nIntegrate checkboxes with form state management:\n\n```jsx live-dev\nconst App = () => {\n const [formData, setFormData] = useState({\n acceptTerms: false,\n subscribeNewsletter: false,\n shareData: false,\n });\n\n const handleChange = (field: string) => (isSelected: boolean) => {\n setFormData((prev) => ({ ...prev, [field]: isSelected }));\n };\n\n const handleSubmit = () => {\n if (!formData.acceptTerms) {\n alert('Please accept the terms and conditions');\n return;\n }\n alert('Form submitted!');\n };\n\n return (\n <Stack direction=\"column\" gap=\"600\">\n <Checkbox\n isSelected={formData.acceptTerms}\n onChange={handleChange('acceptTerms')}\n isInvalid={!formData.acceptTerms}\n >\n I accept the terms and conditions\n </Checkbox>\n <Checkbox\n isSelected={formData.subscribeNewsletter}\n onChange={handleChange('subscribeNewsletter')}\n >\n Subscribe to newsletter\n </Checkbox>\n <Checkbox\n isSelected={formData.shareData}\n onChange={handleChange('shareData')}\n >\n Allow data sharing for analytics\n </Checkbox>\n <Button onClick={handleSubmit}>Submit</Button>\n </Stack>\n );\n}\n```\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using Checkbox within your application. As the component's internal functionality is already tested by Nimbus, these patterns help you verify your integration and application-specific logic.\n\n### Basic Rendering Tests\n\nVerify the component renders with expected elements\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Checkbox, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Checkbox - Basic rendering\", () => {\n it(\"renders checkbox element\", () => {\n render(\n <NimbusProvider>\n <Checkbox>Test checkbox</Checkbox>\n </NimbusProvider>\n );\n\n // Verify checkbox is present\n expect(screen.getByRole(\"checkbox\")).toBeInTheDocument();\n });\n\n it(\"renders with label text\", () => {\n render(\n <NimbusProvider>\n <Checkbox>Accept terms</Checkbox>\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Accept terms\")).toBeInTheDocument();\n });\n\n it(\"renders with aria-label when no children provided\", () => {\n render(\n <NimbusProvider>\n <Checkbox aria-label=\"Test checkbox\" />\n </NimbusProvider>\n );\n\n expect(\n screen.getByRole(\"checkbox\", { name: /test checkbox/i })\n ).toBeInTheDocument();\n });\n\n it(\"renders unchecked by default\", () => {\n render(\n <NimbusProvider>\n <Checkbox>Unchecked checkbox</Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n expect(checkbox).not.toBeChecked();\n });\n});\n```\n\n### Selection State Tests\n\nTest different selection states (checked, unchecked, indeterminate)\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Checkbox, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Checkbox - Selection states\", () => {\n it(\"renders checked state\", () => {\n render(\n <NimbusProvider>\n <Checkbox isSelected>Checked checkbox</Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n expect(checkbox).toBeChecked();\n });\n\n it(\"renders indeterminate state\", () => {\n render(\n <NimbusProvider>\n <Checkbox isIndeterminate>Indeterminate checkbox</Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n // React Aria sets indeterminate state on the input element, not aria-checked\n expect(checkbox).toHaveProperty(\"indeterminate\", true);\n });\n\n it(\"renders unchecked state explicitly\", () => {\n render(\n <NimbusProvider>\n <Checkbox isSelected={false}>Unchecked checkbox</Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n expect(checkbox).not.toBeChecked();\n });\n});\n```\n\n### Interaction Tests\n\nTest user interactions with the component\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Checkbox, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Checkbox - Interactions\", () => {\n it(\"toggles when clicked\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <Checkbox>Toggle me</Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n expect(checkbox).not.toBeChecked();\n\n await user.click(checkbox);\n\n expect(checkbox).toBeChecked();\n });\n\n it(\"calls onChange callback when toggled\", async () => {\n const user = userEvent.setup();\n const handleChange = vi.fn();\n render(\n <NimbusProvider>\n <Checkbox onChange={handleChange}>Test checkbox</Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n await user.click(checkbox);\n\n expect(handleChange).toHaveBeenCalledWith(true);\n });\n\n it(\"toggles with spacebar when focused\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <Checkbox>Keyboard toggle</Checkbox>\n </NimbusProvider>\n );\n\n // Use userEvent.tab() instead of element.focus() to avoid act() warnings\n await user.tab();\n await user.keyboard(\" \");\n\n expect(screen.getByRole(\"checkbox\")).toBeChecked();\n });\n\n it(\"toggles with enter key when focused\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <Checkbox>Enter toggle</Checkbox>\n </NimbusProvider>\n );\n\n // Use userEvent.tab() instead of element.focus() to avoid act() warnings\n await user.tab();\n await user.keyboard(\"{Enter}\");\n\n expect(screen.getByRole(\"checkbox\")).toBeChecked();\n });\n});\n```\n\n### Testing Controlled Mode\n\nTest controlled component behavior\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Checkbox, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Checkbox - Controlled mode\", () => {\n it(\"displays controlled value\", () => {\n render(\n <NimbusProvider>\n <Checkbox isSelected={true} onChange={() => {}}>\n Controlled checkbox\n </Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n expect(checkbox).toBeChecked();\n });\n\n it(\"updates when controlled value changes\", () => {\n const { rerender } = render(\n <NimbusProvider>\n <Checkbox isSelected={false} onChange={() => {}}>\n Controlled checkbox\n </Checkbox>\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"checkbox\")).not.toBeChecked();\n\n rerender(\n <NimbusProvider>\n <Checkbox isSelected={true} onChange={() => {}}>\n Controlled checkbox\n </Checkbox>\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"checkbox\")).toBeChecked();\n });\n\n it(\"calls onChange when user interacts with controlled checkbox\", async () => {\n const user = userEvent.setup();\n const handleChange = vi.fn();\n render(\n <NimbusProvider>\n <Checkbox isSelected={false} onChange={handleChange}>\n Controlled checkbox\n </Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n await user.click(checkbox);\n\n expect(handleChange).toHaveBeenCalledWith(true);\n });\n});\n```\n\n### Testing Validation States\n\nTest different validation and state variations\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Checkbox, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Checkbox - Validation states\", () => {\n it(\"renders disabled state\", () => {\n render(\n <NimbusProvider>\n <Checkbox isDisabled>Disabled checkbox</Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n expect(checkbox).toBeDisabled();\n });\n\n it(\"does not toggle when disabled\", async () => {\n const user = userEvent.setup();\n const handleChange = vi.fn();\n render(\n <NimbusProvider>\n <Checkbox isDisabled onChange={handleChange}>\n Disabled checkbox\n </Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n await user.click(checkbox);\n\n expect(checkbox).not.toBeChecked();\n expect(handleChange).not.toHaveBeenCalled();\n });\n\n it(\"renders invalid state\", () => {\n render(\n <NimbusProvider>\n <Checkbox isInvalid>Invalid checkbox</Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n expect(checkbox).toHaveAttribute(\"aria-invalid\", \"true\");\n });\n\n it(\"renders checked and disabled state\", () => {\n render(\n <NimbusProvider>\n <Checkbox isSelected isDisabled>\n Checked and disabled\n </Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n expect(checkbox).toBeChecked();\n expect(checkbox).toBeDisabled();\n });\n\n it(\"renders indeterminate and invalid state\", () => {\n render(\n <NimbusProvider>\n <Checkbox isIndeterminate isInvalid>\n Indeterminate and invalid\n </Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n // React Aria sets indeterminate state on the input element, not aria-checked\n expect(checkbox).toHaveProperty(\"indeterminate\", true);\n expect(checkbox).toHaveAttribute(\"aria-invalid\", \"true\");\n });\n});\n```\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-checkbox--docs)\n- [React Aria Checkbox](https://react-spectrum.adobe.com/react-aria/Checkbox.html)\n- [ARIA Checkbox Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/)\n\n",
404
+ "mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { Checkbox, type CheckboxProps } from '@commercetools/nimbus';\n```\n\n### Basic usage\n\nThe simplest implementation uses uncontrolled mode:\n\n```jsx live-dev\nconst App = () => (\n <Checkbox>Accept terms and conditions</Checkbox>\n)\n```\n\n## Usage examples\n\n### Selection states\n\nThe Checkbox supports three selection states: unchecked (default), checked, and indeterminate.\n\n#### Unchecked\n\nThe default state when no selection has been made:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <Checkbox>Option 1</Checkbox>\n <Checkbox>Option 2</Checkbox>\n </Stack>\n)\n```\n\n#### Checked\n\nUse the `isSelected` prop to indicate a checked state:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <Checkbox isSelected>Option 1</Checkbox>\n <Checkbox isSelected>Option 2</Checkbox>\n </Stack>\n)\n```\n\n#### Indeterminate\n\nUse the `isIndeterminate` prop to show a partial selection state, commonly used for \"select all\" scenarios:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <Checkbox isIndeterminate>Select all</Checkbox>\n <Checkbox isSelected>Option 1</Checkbox>\n <Checkbox>Option 2</Checkbox>\n </Stack>\n)\n```\n\n### Validation and disabled states\n\n#### Invalid state\n\nUse the `isInvalid` prop to indicate validation errors:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <Checkbox isInvalid>Required field</Checkbox>\n <Checkbox isSelected isInvalid>Invalid selection</Checkbox>\n </Stack>\n)\n```\n\n#### Disabled state\n\nDisable checkbox interaction with the `isDisabled` prop:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <Checkbox isDisabled>Disabled checkbox</Checkbox>\n <Checkbox isSelected isDisabled>Disabled and checked</Checkbox>\n <Checkbox isIndeterminate isDisabled>Disabled and indeterminate</Checkbox>\n </Stack>\n)\n```\n\n### Uncontrolled mode\n\nFor simpler use cases, use uncontrolled mode with `defaultValue` and `onChange`:\n\n```jsx live-dev\nconst App = () => {\n const [displayValue, setDisplayValue] = useState<boolean>(false);\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <Text fontSize=\"sm\">\n {displayValue ? 'Notifications enabled' : 'Notifications disabled'}\n </Text>\n <Checkbox\n defaultValue={false}\n onChange={(isSelected) => {\n setDisplayValue(isSelected);\n }}\n >\n Enable notifications\n </Checkbox>\n </Stack>\n );\n}\n```\n\nUse uncontrolled mode when you need to capture the selected state without managing state yourself.\n\n### Controlled mode\n\nFor scenarios requiring programmatic control or coordination with other components, use controlled mode:\n\n```jsx live-dev\nconst App = () => {\n const [isSelected, setIsSelected] = useState<boolean>(false);\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <Text fontSize=\"sm\">\n {isSelected ? 'Terms accepted' : 'Terms not accepted'}\n </Text>\n <Checkbox\n isSelected={isSelected}\n onChange={setIsSelected}\n >\n Accept terms and conditions\n </Checkbox>\n <Button\n size=\"sm\"\n onClick={() => setIsSelected(!isSelected)}\n >\n {isSelected ? 'Uncheck' : 'Check'}\n </Button>\n </Stack>\n );\n}\n```\n\nUse controlled mode when you need to:\n- Synchronize the value with external state\n- Validate or transform selections\n- Clear or programmatically set the value\n- React to changes in real-time\n\n## Component requirements\n\n## Accessibility\n\nThe Checkbox handles most accessibility requirements internally. However, you must always associate an internationalized label with the component. Visual labels are preferable, and can be set by:\n\n- Using the `CheckboxField` pattern component (recommended, if available)\n- Providing label content via the `children` prop:\n\n```tsx\n<Checkbox>\n {msg.format(checkboxLabelMessage)}\n</Checkbox>\n```\n\n- Associating a `<label>` element with the `Checkbox` using `aria-labelledby`:\n\n```tsx\n<label id=\"checkbox-label\">\n {msg.format(labelMessage)}\n</label>\n<Checkbox aria-labelledby=\"checkbox-label\" />\n```\n\n- Associating a `<label>` element with the `Checkbox` using `htmlFor`:\n\n```tsx\n<label htmlFor=\"checkbox-id\">\n {msg.format(labelMessage)}\n</label>\n<Checkbox id=\"checkbox-id\" />\n```\n\nIf your design requires that the label should not be visible, the label should be set using the `aria-label` prop:\n\n```tsx\n<Checkbox aria-label={msg.format(labelMessage)} />\n```\n\nIf your use case requires tracking and analytics for this component, it is good practice to add a **persistent**, **unique** id to the component:\n\n```tsx\nconst PERSISTENT_ID = \"terms-acceptance-checkbox\";\n\nexport const TermsCheckbox = () => (\n <Checkbox id={PERSISTENT_ID}>\n Accept terms and conditions\n </Checkbox>\n);\n```\n\n#### Keyboard navigation\n\nThe component supports full keyboard interaction:\n- `Tab` / `Shift+Tab`: Navigate to/from the checkbox\n- `Space`: Toggle the checkbox state when focused\n\n## API reference\n\n<PropsTable id=\"Checkbox\" />\n\n## Common patterns\n\n### Checkbox group\n\nGroup related checkboxes together for multiple selections:\n\n```jsx live-dev\nconst App = () => {\n const [selectedOptions, setSelectedOptions] = useState<string[]>([]);\n\n const options = [\n { id: 'option1', label: 'Email notifications' },\n { id: 'option2', label: 'SMS notifications' },\n { id: 'option3', label: 'Push notifications' },\n ];\n\n const handleToggle = (optionId: string) => {\n setSelectedOptions((prev) =>\n prev.includes(optionId)\n ? prev.filter((id) => id !== optionId)\n : [...prev, optionId]\n );\n };\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n {options.map((option) => (\n <Checkbox\n key={option.id}\n isSelected={selectedOptions.includes(option.id)}\n onChange={() => handleToggle(option.id)}\n >\n {option.label}\n </Checkbox>\n ))}\n <Text fontSize=\"sm\">\n Selected: {selectedOptions.length} of {options.length}\n </Text>\n </Stack>\n );\n}\n```\n\n### Select all pattern\n\nImplement a \"select all\" checkbox with indeterminate state:\n\n```jsx live-dev\nconst App = () => {\n const [items, setItems] = useState([\n { id: '1', label: 'Item 1', selected: false },\n { id: '2', label: 'Item 2', selected: true },\n { id: '3', label: 'Item 3', selected: false },\n ]);\n\n const allSelected = items.every((item) => item.selected);\n const someSelected = items.some((item) => item.selected) && !allSelected;\n\n const handleSelectAll = (isSelected: boolean) => {\n setItems((prev) =>\n prev.map((item) => ({ ...item, selected: isSelected }))\n );\n };\n\n const handleToggleItem = (id: string) => {\n setItems((prev) =>\n prev.map((item) =>\n item.id === id ? { ...item, selected: !item.selected } : item\n )\n );\n };\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <Checkbox\n isSelected={allSelected}\n isIndeterminate={someSelected}\n onChange={handleSelectAll}\n >\n Select all\n </Checkbox>\n <Stack direction=\"column\" gap=\"200\" ml=\"600\">\n {items.map((item) => (\n <Checkbox\n key={item.id}\n isSelected={item.selected}\n onChange={() => handleToggleItem(item.id)}\n >\n {item.label}\n </Checkbox>\n ))}\n </Stack>\n </Stack>\n );\n}\n```\n\n### Form integration example\n\nIntegrate checkboxes with form state management:\n\n```jsx live-dev\nconst App = () => {\n const [formData, setFormData] = useState({\n acceptTerms: false,\n subscribeNewsletter: false,\n shareData: false,\n });\n\n const handleChange = (field: string) => (isSelected: boolean) => {\n setFormData((prev) => ({ ...prev, [field]: isSelected }));\n };\n\n const handleSubmit = () => {\n if (!formData.acceptTerms) {\n alert('Please accept the terms and conditions');\n return;\n }\n alert('Form submitted!');\n };\n\n return (\n <Stack direction=\"column\" gap=\"600\">\n <Checkbox\n isSelected={formData.acceptTerms}\n onChange={handleChange('acceptTerms')}\n isInvalid={!formData.acceptTerms}\n >\n I accept the terms and conditions\n </Checkbox>\n <Checkbox\n isSelected={formData.subscribeNewsletter}\n onChange={handleChange('subscribeNewsletter')}\n >\n Subscribe to newsletter\n </Checkbox>\n <Checkbox\n isSelected={formData.shareData}\n onChange={handleChange('shareData')}\n >\n Allow data sharing for analytics\n </Checkbox>\n <Button onClick={handleSubmit}>Submit</Button>\n </Stack>\n );\n}\n```\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using Checkbox within your application. As the component's internal functionality is already tested by Nimbus, these patterns help you verify your integration and application-specific logic.\n\n### Basic Rendering Tests\n\nVerify the component renders with expected elements\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Checkbox, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Checkbox - Basic rendering\", () => {\n it(\"renders checkbox element\", () => {\n render(\n <NimbusProvider>\n <Checkbox>Test checkbox</Checkbox>\n </NimbusProvider>\n );\n\n // Verify checkbox is present\n expect(screen.getByRole(\"checkbox\")).toBeInTheDocument();\n });\n\n it(\"renders with label text\", () => {\n render(\n <NimbusProvider>\n <Checkbox>Accept terms</Checkbox>\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Accept terms\")).toBeInTheDocument();\n });\n\n it(\"renders with aria-label when no children provided\", () => {\n render(\n <NimbusProvider>\n <Checkbox aria-label=\"Test checkbox\" />\n </NimbusProvider>\n );\n\n expect(\n screen.getByRole(\"checkbox\", { name: /test checkbox/i })\n ).toBeInTheDocument();\n });\n\n it(\"renders unchecked by default\", () => {\n render(\n <NimbusProvider>\n <Checkbox>Unchecked checkbox</Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n expect(checkbox).not.toBeChecked();\n });\n});\n```\n\n### Selection State Tests\n\nTest different selection states (checked, unchecked, indeterminate)\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Checkbox, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Checkbox - Selection states\", () => {\n it(\"renders checked state\", () => {\n render(\n <NimbusProvider>\n <Checkbox isSelected>Checked checkbox</Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n expect(checkbox).toBeChecked();\n });\n\n it(\"renders indeterminate state\", () => {\n render(\n <NimbusProvider>\n <Checkbox isIndeterminate>Indeterminate checkbox</Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n // React Aria sets indeterminate state on the input element, not aria-checked\n expect(checkbox).toHaveProperty(\"indeterminate\", true);\n });\n\n it(\"renders unchecked state explicitly\", () => {\n render(\n <NimbusProvider>\n <Checkbox isSelected={false}>Unchecked checkbox</Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n expect(checkbox).not.toBeChecked();\n });\n});\n```\n\n### Interaction Tests\n\nTest user interactions with the component\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Checkbox, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Checkbox - Interactions\", () => {\n it(\"toggles when clicked\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <Checkbox>Toggle me</Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n expect(checkbox).not.toBeChecked();\n\n await user.click(checkbox);\n\n expect(checkbox).toBeChecked();\n });\n\n it(\"calls onChange callback when toggled\", async () => {\n const user = userEvent.setup();\n const handleChange = vi.fn();\n render(\n <NimbusProvider>\n <Checkbox onChange={handleChange}>Test checkbox</Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n await user.click(checkbox);\n\n expect(handleChange).toHaveBeenCalledWith(true);\n });\n\n it(\"toggles with spacebar when focused\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <Checkbox>Keyboard toggle</Checkbox>\n </NimbusProvider>\n );\n\n // Use userEvent.tab() instead of element.focus() to avoid act() warnings\n await user.tab();\n await user.keyboard(\" \");\n\n expect(screen.getByRole(\"checkbox\")).toBeChecked();\n });\n});\n```\n\n### Testing Controlled Mode\n\nTest controlled component behavior\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Checkbox, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Checkbox - Controlled mode\", () => {\n it(\"displays controlled value\", () => {\n render(\n <NimbusProvider>\n <Checkbox isSelected={true} onChange={() => {}}>\n Controlled checkbox\n </Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n expect(checkbox).toBeChecked();\n });\n\n it(\"updates when controlled value changes\", () => {\n const { rerender } = render(\n <NimbusProvider>\n <Checkbox isSelected={false} onChange={() => {}}>\n Controlled checkbox\n </Checkbox>\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"checkbox\")).not.toBeChecked();\n\n rerender(\n <NimbusProvider>\n <Checkbox isSelected={true} onChange={() => {}}>\n Controlled checkbox\n </Checkbox>\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"checkbox\")).toBeChecked();\n });\n\n it(\"calls onChange when user interacts with controlled checkbox\", async () => {\n const user = userEvent.setup();\n const handleChange = vi.fn();\n render(\n <NimbusProvider>\n <Checkbox isSelected={false} onChange={handleChange}>\n Controlled checkbox\n </Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n await user.click(checkbox);\n\n expect(handleChange).toHaveBeenCalledWith(true);\n });\n});\n```\n\n### Testing Validation States\n\nTest different validation and state variations\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { Checkbox, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Checkbox - Validation states\", () => {\n it(\"renders disabled state\", () => {\n render(\n <NimbusProvider>\n <Checkbox isDisabled>Disabled checkbox</Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n expect(checkbox).toBeDisabled();\n });\n\n it(\"does not toggle when disabled\", async () => {\n const user = userEvent.setup();\n const handleChange = vi.fn();\n render(\n <NimbusProvider>\n <Checkbox isDisabled onChange={handleChange}>\n Disabled checkbox\n </Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n await user.click(checkbox);\n\n expect(checkbox).not.toBeChecked();\n expect(handleChange).not.toHaveBeenCalled();\n });\n\n it(\"renders invalid state\", () => {\n render(\n <NimbusProvider>\n <Checkbox isInvalid>Invalid checkbox</Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n expect(checkbox).toHaveAttribute(\"aria-invalid\", \"true\");\n });\n\n it(\"renders checked and disabled state\", () => {\n render(\n <NimbusProvider>\n <Checkbox isSelected isDisabled>\n Checked and disabled\n </Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n expect(checkbox).toBeChecked();\n expect(checkbox).toBeDisabled();\n });\n\n it(\"renders indeterminate and invalid state\", () => {\n render(\n <NimbusProvider>\n <Checkbox isIndeterminate isInvalid>\n Indeterminate and invalid\n </Checkbox>\n </NimbusProvider>\n );\n\n const checkbox = screen.getByRole(\"checkbox\");\n // React Aria sets indeterminate state on the input element, not aria-checked\n expect(checkbox).toHaveProperty(\"indeterminate\", true);\n expect(checkbox).toHaveAttribute(\"aria-invalid\", \"true\");\n });\n});\n```\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-checkbox--docs)\n- [React Aria Checkbox](https://react-spectrum.adobe.com/react-aria/Checkbox.html)\n- [ARIA Checkbox Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/)\n\n",
405
405
  "toc": [
406
406
  {
407
407
  "value": "Getting started",