@commercetools/nimbus-mcp 2.11.0 → 3.0.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.
- package/data/docs/route-manifest.json +203 -0
- package/data/docs/routes/components-data-display-card.json +71 -5
- package/data/docs/routes/components-feedback-toast.json +1 -1
- package/data/docs/routes/components-layout-defaultpage.json +4 -4
- package/data/docs/routes/components-layout-modalpage.json +4 -4
- package/data/docs/routes/components-layout-scrollarea.json +3 -3
- package/data/docs/routes/components-media-avatar.json +24 -2
- package/data/docs/routes/patterns-actions-form-action-bar.json +412 -0
- package/data/docs/routes/patterns-dialogs-info-dialog.json +315 -0
- package/data/docs/routes/patterns-dialogs.json +78 -0
- package/data/docs/search-index.json +1 -1
- package/data/docs/types/AccordionRoot.json +2 -2
- package/data/docs/types/AlertDescription.json +8 -8
- package/data/docs/types/AlertDismissButton.json +2 -2
- package/data/docs/types/AlertTitle.json +8 -8
- package/data/docs/types/Avatar.json +8 -8
- package/data/docs/types/Badge.json +2 -2
- package/data/docs/types/Body.json +6 -6
- package/data/docs/types/Box.json +6 -6
- package/data/docs/types/Button.json +8 -8
- package/data/docs/types/Caption.json +6 -6
- package/data/docs/types/Card.json +1 -1
- package/data/docs/types/{CardContent.json → CardBody.json} +2 -2
- package/data/docs/types/CardFooter.json +27 -0
- package/data/docs/types/CardRoot.json +18 -48
- package/data/docs/types/Cell.json +6 -6
- package/data/docs/types/Checkbox.json +2 -2
- package/data/docs/types/Code.json +10 -10
- package/data/docs/types/Column.json +6 -6
- package/data/docs/types/ColumnGroup.json +6 -6
- package/data/docs/types/ColumnHeader.json +6 -6
- package/data/docs/types/ComboBoxListBox.json +6 -6
- package/data/docs/types/ComboBoxPopover.json +8 -8
- package/data/docs/types/ComboBoxRoot.json +8 -8
- package/data/docs/types/ComboBoxSection.json +6 -6
- package/data/docs/types/ComboBoxTrigger.json +6 -6
- package/data/docs/types/DataTable.json +2 -2
- package/data/docs/types/DataTableBody.json +6 -6
- package/data/docs/types/DataTableHeader.json +7 -7
- package/data/docs/types/DataTableRoot.json +2 -2
- package/data/docs/types/DataTableTable.json +6 -6
- package/data/docs/types/DatePicker.json +2 -2
- package/data/docs/types/DefaultPageRoot.json +2 -2
- package/data/docs/types/DialogCloseTrigger.json +2 -2
- package/data/docs/types/DraggableListField.json +10 -10
- package/data/docs/types/DraggableListItem.json +6 -6
- package/data/docs/types/DraggableListRoot.json +8 -8
- package/data/docs/types/DrawerCloseTrigger.json +2 -2
- package/data/docs/types/FieldErrors.json +2 -2
- package/data/docs/types/Flex.json +22 -22
- package/data/docs/types/Footer.json +6 -6
- package/data/docs/types/FormActionBar.json +200 -0
- package/data/docs/types/FormFieldRoot.json +2 -2
- package/data/docs/types/Grid.json +24 -24
- package/data/docs/types/Group.json +2 -2
- package/data/docs/types/Header.json +6 -6
- package/data/docs/types/Heading.json +8 -8
- package/data/docs/types/Icon.json +4 -4
- package/data/docs/types/IconButton.json +8 -8
- package/data/docs/types/Image.json +10 -10
- package/data/docs/types/Indicator.json +6 -6
- package/data/docs/types/InfoDialog.json +104 -0
- package/data/docs/types/InlineSvg.json +2 -2
- package/data/docs/types/Item.json +6 -6
- package/data/docs/types/Kbd.json +8 -8
- package/data/docs/types/Link.json +8 -8
- package/data/docs/types/ListIndicator.json +6 -6
- package/data/docs/types/ListItem.json +6 -6
- package/data/docs/types/ListRoot.json +10 -10
- package/data/docs/types/LoadingSpinner.json +2 -2
- package/data/docs/types/MultilineTextInput.json +2 -2
- package/data/docs/types/MultilineTextInputField.json +2 -2
- package/data/docs/types/NumberInput.json +2 -2
- package/data/docs/types/NumberInputField.json +2 -2
- package/data/docs/types/PageContentColumn.json +6 -6
- package/data/docs/types/PageContentRoot.json +6 -6
- package/data/docs/types/RichTextInput.json +2 -2
- package/data/docs/types/Root.json +10 -10
- package/data/docs/types/Row.json +6 -6
- package/data/docs/types/ScrollArea.json +6 -6
- package/data/docs/types/Separator.json +4 -4
- package/data/docs/types/SimpleGrid.json +28 -28
- package/data/docs/types/SplitButton.json +2 -2
- package/data/docs/types/Stack.json +2 -2
- package/data/docs/types/StepsRoot.json +2 -2
- package/data/docs/types/Switch.json +2 -2
- package/data/docs/types/TabNavItem.json +2 -2
- package/data/docs/types/TabNavRoot.json +2 -2
- package/data/docs/types/TableBody.json +6 -6
- package/data/docs/types/TableCaption.json +6 -6
- package/data/docs/types/TableCell.json +6 -6
- package/data/docs/types/TableColumn.json +6 -6
- package/data/docs/types/TableColumnGroup.json +6 -6
- package/data/docs/types/TableColumnHeader.json +6 -6
- package/data/docs/types/TableFooter.json +6 -6
- package/data/docs/types/TableHeader.json +6 -6
- package/data/docs/types/TableRoot.json +18 -18
- package/data/docs/types/TableRow.json +6 -6
- package/data/docs/types/TableScrollArea.json +6 -6
- package/data/docs/types/Text.json +8 -8
- package/data/docs/types/ToggleButtonGroupButton.json +6 -6
- package/data/docs/types/ToggleButtonGroupRoot.json +6 -6
- package/data/docs/types/Toolbar.json +2 -2
- package/data/docs/types/TooltipContent.json +2 -2
- package/data/docs/types/manifest.json +4 -1
- package/data/docs/types/toast.json +0 -15
- package/package.json +6 -6
|
@@ -207,10 +207,10 @@
|
|
|
207
207
|
}
|
|
208
208
|
]
|
|
209
209
|
},
|
|
210
|
-
"mdx": "\n## Overview\n\nModalPage is a fullscreen overlay that covers nearly the entire viewport,\nproviding a dedicated context for complex workflows. It is the Nimbus\nequivalent of the Merchant Center Application Kit's modal page patterns\n(form, info, and tabular variants).\n\nUnlike a regular Drawer, ModalPage has a fixed, structured layout with a\ntop navigation bar, header, scrollable content area, and optional footer.\nIt is always controlled — consumers manage the open state with `isOpen` and\n`onClose`.\n\n### Key features\n\n- **Controlled-only API**: Explicit `isOpen` + `onClose` — no hidden state\n- **Breadcrumb navigation**: `ModalPage.TopBar` shows previous/current path with an accessible back button\n- **Structured layout**: TopBar / Header / Content (scrollable) / Footer\n- **Tab navigation**: `ModalPage.TabNav` for tabular page patterns\n- **Multi-column content**: Use `PageContent.Root` inside `ModalPage.Content` for column layouts\n- **Accessibility first**: WCAG 2.1 AA compliant with focus management, keyboard navigation, and screen reader support\n\n### Resources\n\n[React Aria Dialog Docs](https://react-spectrum.adobe.com/react-aria/Dialog.html)\n[ARIA Dialog Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)\n\n## Variables\n\nGet familiar with the features.\n\n### Info page\n\nA read-only detail view. Footer is optional.\n\n```jsx live\nconst App = () => {\n const [isOpen, setIsOpen] = React.useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Info Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Product Details\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Product Details</ModalPage.Title>\n <ModalPage.Subtitle>View the product information</ModalPage.Subtitle>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Product information goes here.</Text>\n </ModalPage.Content>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Form page\n\nUse when the user needs to create or edit an entity. Always include a Footer\nwith Save/Cancel actions.\n\n```jsx live\nconst App = () => {\n const [isOpen, setIsOpen] = React.useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Form Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Add Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Add Product</ModalPage.Title>\n <ModalPage.Subtitle>Fill in the product details</ModalPage.Subtitle>\n <ModalPage.Actions>\n <Button size=\"sm\" variant=\"outline\">Preview</Button>\n </ModalPage.Actions>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Form fields go here.</Text>\n </ModalPage.Content>\n <ModalPage.Footer>\n <Button slot=\"close\" variant=\"outline\">Cancel</Button>\n <Button colorPalette=\"primary\" variant=\"solid\">Save</Button>\n </ModalPage.Footer>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Tabular page\n\nUse when content is organized into multiple sections via tab navigation.\nPlace `ModalPage.TabNav` inside the header and display the active section\nin the content area.\n\n```jsx live\nconst App = () => {\n const [isOpen, setIsOpen] = React.useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Tabular Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Orders\"\n currentPathLabel=\"Order #12345\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Order #12345</ModalPage.Title>\n <ModalPage.Subtitle>Placed on 2024-01-15</ModalPage.Subtitle>\n <ModalPage.TabNav>\n <TabNav.Root aria-label=\"Order sections\">\n <TabNav.Item href=\"#general\" isCurrent>General</TabNav.Item>\n <TabNav.Item href=\"#items\">Items</TabNav.Item>\n <TabNav.Item href=\"#shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </ModalPage.TabNav>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>General information</Text>\n </ModalPage.Content>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Multi-column content\n\nUse `PageContent.Root` with `columns` inside `ModalPage.Content` for\nside-by-side layouts:\n\n```jsx live\nconst App = () => {\n const [isOpen, setIsOpen] = React.useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Two-Column Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n </ModalPage.Header>\n <ModalPage.Content>\n <PageContent.Root variant=\"wide\" columns=\"2/1\">\n <PageContent.Column>\n <Text>Main form area (2/3 width)</Text>\n </PageContent.Column>\n <PageContent.Column sticky>\n <Text>Sidebar summary (1/3 width, sticky)</Text>\n </PageContent.Column>\n </PageContent.Root>\n </ModalPage.Content>\n <ModalPage.Footer>\n <Button slot=\"close\" variant=\"outline\">Cancel</Button>\n <Button colorPalette=\"primary\" variant=\"solid\">Save</Button>\n </ModalPage.Footer>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Stacked pages\n\nUse when a workflow requires drilling into a sub-task — for example, opening\n\"Add Variant\" while the \"Edit Product\" page is open. Place the second\n`ModalPage.Root` inside the first `ModalPage.Content`. Each page manages its\nown independent open state.\n\n```jsx live\nconst App = () => {\n const [isFirstOpen, setIsFirstOpen] = React.useState(false);\n const [isSecondOpen, setIsSecondOpen] = React.useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsFirstOpen(true)}>Open Edit Product</Button>\n <ModalPage.Root\n isOpen={isFirstOpen}\n onClose={() => setIsFirstOpen(false)}\n >\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n <ModalPage.Subtitle>Update the product details</ModalPage.Subtitle>\n </ModalPage.Header>\n <ModalPage.Content>\n <Stack>\n <Text>Product form content goes here.</Text>\n <Button onPress={() => setIsSecondOpen(true)}>\n Open Add Variant\n </Button>\n </Stack>\n\n <ModalPage.Root\n isOpen={isSecondOpen}\n onClose={() => setIsSecondOpen(false)}\n >\n <ModalPage.TopBar\n previousPathLabel=\"Edit Product\"\n currentPathLabel=\"Add Variant\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Add Variant</ModalPage.Title>\n <ModalPage.Subtitle>Define a new product variant</ModalPage.Subtitle>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Variant form content goes here.</Text>\n </ModalPage.Content>\n <ModalPage.Footer>\n <Button slot=\"close\" variant=\"outline\">Cancel</Button>\n <Button colorPalette=\"primary\" variant=\"solid\">Save Variant</Button>\n </ModalPage.Footer>\n </ModalPage.Root>\n </ModalPage.Content>\n <ModalPage.Footer>\n <Button slot=\"close\" variant=\"outline\">Cancel</Button>\n <Button colorPalette=\"primary\" variant=\"solid\">Save Product</Button>\n </ModalPage.Footer>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n## Guidelines\n\nUse ModalPage strategically to enhance user workflow without disrupting the\nexperience.\n\n### Best practices\n\n- **Always provide a title**: Use `ModalPage.Title` so screen readers can identify the dialog\n- **Use TopBar navigation**: Always supply meaningful `previousPathLabel` and `currentPathLabel`\n- **Footer actions for forms**: Include Save/Cancel in `ModalPage.Footer` for form pages\n- **Prefer Escape key close**: Don't disable keyboard dismissal — it matches OS conventions and WCAG 2.1\n- **Backdrop click is disabled**: Users must close via back button, Cancel, or Escape to prevent accidental data loss\n\n> [!TIP]\\\n> When to use\n\n- **Complex editing workflows**: Creating or editing entities with many fields\n- **Multi-section detail views**: When content is too rich for a standard Drawer\n- **Tabular data workflows**: When content requires tabs (general, items, shipping)\n- **Full-page focused context**: When the user needs to focus entirely on the task\n\n> [!CAUTION]\\\n> When not to use\n\n- **Simple confirmation dialogs**: Use `Dialog` instead\n- **Single-form quick edits**: Use `Drawer` instead — ModalPage is for complex, multi-field scenarios\n- **Navigation overlays**: Use proper routing for section changes\n\n## Specs\n\n<PropsTable id=\"ModalPage\" />\n\n## Accessibility\n\n### Keyboard Navigation\n\n| Key | Action |\n|-----|--------|\n| `Escape` | Closes the modal page |\n| `Tab` / `Shift+Tab` | Moves focus between interactive elements |\n\nFocus is trapped inside the modal while open and returns to the triggering\nelement when the modal closes.\n\n### Screen Reader Support\n\n- The dialog has `role=\"dialog\"` and `aria-modal=\"true\"`\n- `ModalPage.Title` automatically provides the accessible name for the dialog\n- The back button has a localized label referencing the previous path (e.g. \"Go back to Products\")\n- The breadcrumb separator is hidden from assistive technology\n- The current path label is marked as the current page\n",
|
|
210
|
+
"mdx": "\n## Overview\n\nModalPage is a fullscreen overlay that covers nearly the entire viewport,\nproviding a dedicated context for complex workflows. It is the Nimbus\nequivalent of the Merchant Center Application Kit's modal page patterns\n(form, info, and tabular variants).\n\nUnlike a regular Drawer, ModalPage has a fixed, structured layout with a\ntop navigation bar, header, scrollable content area, and optional footer.\nIt is always controlled — consumers manage the open state with `isOpen` and\n`onClose`.\n\n### Key features\n\n- **Controlled-only API**: Explicit `isOpen` + `onClose` — no hidden state\n- **Breadcrumb navigation**: `ModalPage.TopBar` shows previous/current path with an accessible back button\n- **Structured layout**: TopBar / Header / Content (scrollable) / Footer\n- **Tab navigation**: `ModalPage.TabNav` for tabular page patterns\n- **Multi-column content**: Use `PageContent.Root` inside `ModalPage.Content` for column layouts\n- **Accessibility first**: WCAG 2.1 AA compliant with focus management, keyboard navigation, and screen reader support\n\n### Resources\n\n[React Aria Dialog Docs](https://react-spectrum.adobe.com/react-aria/Dialog.html)\n[ARIA Dialog Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)\n\n## Variables\n\nGet familiar with the features.\n\n### Info page\n\nA read-only detail view. Footer is optional.\n\n```jsx live\nconst App = () => {\n const [isOpen, setIsOpen] = React.useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Info Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Product Details\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Product Details</ModalPage.Title>\n <ModalPage.Subtitle>View the product information</ModalPage.Subtitle>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Product information goes here.</Text>\n </ModalPage.Content>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Form page\n\nUse when the user needs to create or edit an entity. Always include a Footer\nwith Save/Cancel actions.\n\n```jsx live\nconst App = () => {\n const [isOpen, setIsOpen] = React.useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Form Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Add Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Add Product</ModalPage.Title>\n <ModalPage.Subtitle>Fill in the product details</ModalPage.Subtitle>\n <ModalPage.Actions>\n <Button size=\"sm\" variant=\"outline\">Preview</Button>\n </ModalPage.Actions>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Form fields go here.</Text>\n </ModalPage.Content>\n <ModalPage.Footer>\n <FormActionBar\n onSave={() => {}}\n onCancel={() => {}}\n cancelSlot=\"close\"\n />\n </ModalPage.Footer>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Tabular page\n\nUse when content is organized into multiple sections via tab navigation.\nPlace `ModalPage.TabNav` inside the header and display the active section\nin the content area.\n\n```jsx live\nconst App = () => {\n const [isOpen, setIsOpen] = React.useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Tabular Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Orders\"\n currentPathLabel=\"Order #12345\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Order #12345</ModalPage.Title>\n <ModalPage.Subtitle>Placed on 2024-01-15</ModalPage.Subtitle>\n <ModalPage.TabNav>\n <TabNav.Root aria-label=\"Order sections\">\n <TabNav.Item href=\"#general\" isCurrent>General</TabNav.Item>\n <TabNav.Item href=\"#items\">Items</TabNav.Item>\n <TabNav.Item href=\"#shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </ModalPage.TabNav>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>General information</Text>\n </ModalPage.Content>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Multi-column content\n\nUse `PageContent.Root` with `columns` inside `ModalPage.Content` for\nside-by-side layouts:\n\n```jsx live\nconst App = () => {\n const [isOpen, setIsOpen] = React.useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Two-Column Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n </ModalPage.Header>\n <ModalPage.Content>\n <PageContent.Root variant=\"wide\" columns=\"2/1\">\n <PageContent.Column>\n <Text>Main form area (2/3 width)</Text>\n </PageContent.Column>\n <PageContent.Column sticky>\n <Text>Sidebar summary (1/3 width, sticky)</Text>\n </PageContent.Column>\n </PageContent.Root>\n </ModalPage.Content>\n <ModalPage.Footer>\n <FormActionBar\n onSave={() => {}}\n onCancel={() => {}}\n cancelSlot=\"close\"\n />\n </ModalPage.Footer>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Stacked pages\n\nUse when a workflow requires drilling into a sub-task — for example, opening\n\"Add Variant\" while the \"Edit Product\" page is open. Place the second\n`ModalPage.Root` inside the first `ModalPage.Content`. Each page manages its\nown independent open state.\n\n```jsx live\nconst App = () => {\n const [isFirstOpen, setIsFirstOpen] = React.useState(false);\n const [isSecondOpen, setIsSecondOpen] = React.useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsFirstOpen(true)}>Open Edit Product</Button>\n <ModalPage.Root\n isOpen={isFirstOpen}\n onClose={() => setIsFirstOpen(false)}\n >\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n <ModalPage.Subtitle>Update the product details</ModalPage.Subtitle>\n </ModalPage.Header>\n <ModalPage.Content>\n <Stack>\n <Text>Product form content goes here.</Text>\n <Button onPress={() => setIsSecondOpen(true)}>\n Open Add Variant\n </Button>\n </Stack>\n\n <ModalPage.Root\n isOpen={isSecondOpen}\n onClose={() => setIsSecondOpen(false)}\n >\n <ModalPage.TopBar\n previousPathLabel=\"Edit Product\"\n currentPathLabel=\"Add Variant\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Add Variant</ModalPage.Title>\n <ModalPage.Subtitle>Define a new product variant</ModalPage.Subtitle>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Variant form content goes here.</Text>\n </ModalPage.Content>\n <ModalPage.Footer>\n <FormActionBar\n saveLabel=\"Save Variant\"\n onSave={() => {}}\n onCancel={() => {}}\n cancelSlot=\"close\"\n />\n </ModalPage.Footer>\n </ModalPage.Root>\n </ModalPage.Content>\n <ModalPage.Footer>\n <FormActionBar\n saveLabel=\"Save Product\"\n onSave={() => {}}\n onCancel={() => {}}\n cancelSlot=\"close\"\n />\n </ModalPage.Footer>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n## Guidelines\n\nUse ModalPage strategically to enhance user workflow without disrupting the\nexperience.\n\n### Best practices\n\n- **Always provide a title**: Use `ModalPage.Title` so screen readers can identify the dialog\n- **Use TopBar navigation**: Always supply meaningful `previousPathLabel` and `currentPathLabel`\n- **Footer actions for forms**: Include Save/Cancel in `ModalPage.Footer` for form pages\n- **Prefer Escape key close**: Don't disable keyboard dismissal — it matches OS conventions and WCAG 2.1\n- **Backdrop click is disabled**: Users must close via back button, Cancel, or Escape to prevent accidental data loss\n\n> [!TIP]\\\n> When to use\n\n- **Complex editing workflows**: Creating or editing entities with many fields\n- **Multi-section detail views**: When content is too rich for a standard Drawer\n- **Tabular data workflows**: When content requires tabs (general, items, shipping)\n- **Full-page focused context**: When the user needs to focus entirely on the task\n\n> [!CAUTION]\\\n> When not to use\n\n- **Simple confirmation dialogs**: Use `Dialog` instead\n- **Single-form quick edits**: Use `Drawer` instead — ModalPage is for complex, multi-field scenarios\n- **Navigation overlays**: Use proper routing for section changes\n\n## Specs\n\n<PropsTable id=\"ModalPage\" />\n\n## Accessibility\n\n### Keyboard Navigation\n\n| Key | Action |\n|-----|--------|\n| `Escape` | Closes the modal page |\n| `Tab` / `Shift+Tab` | Moves focus between interactive elements |\n\nFocus is trapped inside the modal while open and returns to the triggering\nelement when the modal closes.\n\n### Screen Reader Support\n\n- The dialog has `role=\"dialog\"` and `aria-modal=\"true\"`\n- `ModalPage.Title` automatically provides the accessible name for the dialog\n- The back button has a localized label referencing the previous path (e.g. \"Go back to Products\")\n- The breadcrumb separator is hidden from assistive technology\n- The current path label is marked as the current page\n",
|
|
211
211
|
"views": {
|
|
212
212
|
"overview": {
|
|
213
|
-
"mdx": "\n## Overview\n\nModalPage is a fullscreen overlay that covers nearly the entire viewport,\nproviding a dedicated context for complex workflows. It is the Nimbus\nequivalent of the Merchant Center Application Kit's modal page patterns\n(form, info, and tabular variants).\n\nUnlike a regular Drawer, ModalPage has a fixed, structured layout with a\ntop navigation bar, header, scrollable content area, and optional footer.\nIt is always controlled — consumers manage the open state with `isOpen` and\n`onClose`.\n\n### Key features\n\n- **Controlled-only API**: Explicit `isOpen` + `onClose` — no hidden state\n- **Breadcrumb navigation**: `ModalPage.TopBar` shows previous/current path with an accessible back button\n- **Structured layout**: TopBar / Header / Content (scrollable) / Footer\n- **Tab navigation**: `ModalPage.TabNav` for tabular page patterns\n- **Multi-column content**: Use `PageContent.Root` inside `ModalPage.Content` for column layouts\n- **Accessibility first**: WCAG 2.1 AA compliant with focus management, keyboard navigation, and screen reader support\n\n### Resources\n\n[React Aria Dialog Docs](https://react-spectrum.adobe.com/react-aria/Dialog.html)\n[ARIA Dialog Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)\n\n## Variables\n\nGet familiar with the features.\n\n### Info page\n\nA read-only detail view. Footer is optional.\n\n```jsx live\nconst App = () => {\n const [isOpen, setIsOpen] = React.useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Info Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Product Details\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Product Details</ModalPage.Title>\n <ModalPage.Subtitle>View the product information</ModalPage.Subtitle>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Product information goes here.</Text>\n </ModalPage.Content>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Form page\n\nUse when the user needs to create or edit an entity. Always include a Footer\nwith Save/Cancel actions.\n\n```jsx live\nconst App = () => {\n const [isOpen, setIsOpen] = React.useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Form Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Add Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Add Product</ModalPage.Title>\n <ModalPage.Subtitle>Fill in the product details</ModalPage.Subtitle>\n <ModalPage.Actions>\n <Button size=\"sm\" variant=\"outline\">Preview</Button>\n </ModalPage.Actions>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Form fields go here.</Text>\n </ModalPage.Content>\n <ModalPage.Footer>\n <Button slot=\"close\" variant=\"outline\">Cancel</Button>\n <Button colorPalette=\"primary\" variant=\"solid\">Save</Button>\n </ModalPage.Footer>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Tabular page\n\nUse when content is organized into multiple sections via tab navigation.\nPlace `ModalPage.TabNav` inside the header and display the active section\nin the content area.\n\n```jsx live\nconst App = () => {\n const [isOpen, setIsOpen] = React.useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Tabular Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Orders\"\n currentPathLabel=\"Order #12345\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Order #12345</ModalPage.Title>\n <ModalPage.Subtitle>Placed on 2024-01-15</ModalPage.Subtitle>\n <ModalPage.TabNav>\n <TabNav.Root aria-label=\"Order sections\">\n <TabNav.Item href=\"#general\" isCurrent>General</TabNav.Item>\n <TabNav.Item href=\"#items\">Items</TabNav.Item>\n <TabNav.Item href=\"#shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </ModalPage.TabNav>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>General information</Text>\n </ModalPage.Content>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Multi-column content\n\nUse `PageContent.Root` with `columns` inside `ModalPage.Content` for\nside-by-side layouts:\n\n```jsx live\nconst App = () => {\n const [isOpen, setIsOpen] = React.useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Two-Column Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n </ModalPage.Header>\n <ModalPage.Content>\n <PageContent.Root variant=\"wide\" columns=\"2/1\">\n <PageContent.Column>\n <Text>Main form area (2/3 width)</Text>\n </PageContent.Column>\n <PageContent.Column sticky>\n <Text>Sidebar summary (1/3 width, sticky)</Text>\n </PageContent.Column>\n </PageContent.Root>\n </ModalPage.Content>\n <ModalPage.Footer>\n <Button slot=\"close\" variant=\"outline\">Cancel</Button>\n <Button colorPalette=\"primary\" variant=\"solid\">Save</Button>\n </ModalPage.Footer>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Stacked pages\n\nUse when a workflow requires drilling into a sub-task — for example, opening\n\"Add Variant\" while the \"Edit Product\" page is open. Place the second\n`ModalPage.Root` inside the first `ModalPage.Content`. Each page manages its\nown independent open state.\n\n```jsx live\nconst App = () => {\n const [isFirstOpen, setIsFirstOpen] = React.useState(false);\n const [isSecondOpen, setIsSecondOpen] = React.useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsFirstOpen(true)}>Open Edit Product</Button>\n <ModalPage.Root\n isOpen={isFirstOpen}\n onClose={() => setIsFirstOpen(false)}\n >\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n <ModalPage.Subtitle>Update the product details</ModalPage.Subtitle>\n </ModalPage.Header>\n <ModalPage.Content>\n <Stack>\n <Text>Product form content goes here.</Text>\n <Button onPress={() => setIsSecondOpen(true)}>\n Open Add Variant\n </Button>\n </Stack>\n\n <ModalPage.Root\n isOpen={isSecondOpen}\n onClose={() => setIsSecondOpen(false)}\n >\n <ModalPage.TopBar\n previousPathLabel=\"Edit Product\"\n currentPathLabel=\"Add Variant\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Add Variant</ModalPage.Title>\n <ModalPage.Subtitle>Define a new product variant</ModalPage.Subtitle>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Variant form content goes here.</Text>\n </ModalPage.Content>\n <ModalPage.Footer>\n <Button slot=\"close\" variant=\"outline\">Cancel</Button>\n <Button colorPalette=\"primary\" variant=\"solid\">Save Variant</Button>\n </ModalPage.Footer>\n </ModalPage.Root>\n </ModalPage.Content>\n <ModalPage.Footer>\n <Button slot=\"close\" variant=\"outline\">Cancel</Button>\n <Button colorPalette=\"primary\" variant=\"solid\">Save Product</Button>\n </ModalPage.Footer>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n## Guidelines\n\nUse ModalPage strategically to enhance user workflow without disrupting the\nexperience.\n\n### Best practices\n\n- **Always provide a title**: Use `ModalPage.Title` so screen readers can identify the dialog\n- **Use TopBar navigation**: Always supply meaningful `previousPathLabel` and `currentPathLabel`\n- **Footer actions for forms**: Include Save/Cancel in `ModalPage.Footer` for form pages\n- **Prefer Escape key close**: Don't disable keyboard dismissal — it matches OS conventions and WCAG 2.1\n- **Backdrop click is disabled**: Users must close via back button, Cancel, or Escape to prevent accidental data loss\n\n> [!TIP]\\\n> When to use\n\n- **Complex editing workflows**: Creating or editing entities with many fields\n- **Multi-section detail views**: When content is too rich for a standard Drawer\n- **Tabular data workflows**: When content requires tabs (general, items, shipping)\n- **Full-page focused context**: When the user needs to focus entirely on the task\n\n> [!CAUTION]\\\n> When not to use\n\n- **Simple confirmation dialogs**: Use `Dialog` instead\n- **Single-form quick edits**: Use `Drawer` instead — ModalPage is for complex, multi-field scenarios\n- **Navigation overlays**: Use proper routing for section changes\n\n## Specs\n\n<PropsTable id=\"ModalPage\" />\n\n## Accessibility\n\n### Keyboard Navigation\n\n| Key | Action |\n|-----|--------|\n| `Escape` | Closes the modal page |\n| `Tab` / `Shift+Tab` | Moves focus between interactive elements |\n\nFocus is trapped inside the modal while open and returns to the triggering\nelement when the modal closes.\n\n### Screen Reader Support\n\n- The dialog has `role=\"dialog\"` and `aria-modal=\"true\"`\n- `ModalPage.Title` automatically provides the accessible name for the dialog\n- The back button has a localized label referencing the previous path (e.g. \"Go back to Products\")\n- The breadcrumb separator is hidden from assistive technology\n- The current path label is marked as the current page\n",
|
|
213
|
+
"mdx": "\n## Overview\n\nModalPage is a fullscreen overlay that covers nearly the entire viewport,\nproviding a dedicated context for complex workflows. It is the Nimbus\nequivalent of the Merchant Center Application Kit's modal page patterns\n(form, info, and tabular variants).\n\nUnlike a regular Drawer, ModalPage has a fixed, structured layout with a\ntop navigation bar, header, scrollable content area, and optional footer.\nIt is always controlled — consumers manage the open state with `isOpen` and\n`onClose`.\n\n### Key features\n\n- **Controlled-only API**: Explicit `isOpen` + `onClose` — no hidden state\n- **Breadcrumb navigation**: `ModalPage.TopBar` shows previous/current path with an accessible back button\n- **Structured layout**: TopBar / Header / Content (scrollable) / Footer\n- **Tab navigation**: `ModalPage.TabNav` for tabular page patterns\n- **Multi-column content**: Use `PageContent.Root` inside `ModalPage.Content` for column layouts\n- **Accessibility first**: WCAG 2.1 AA compliant with focus management, keyboard navigation, and screen reader support\n\n### Resources\n\n[React Aria Dialog Docs](https://react-spectrum.adobe.com/react-aria/Dialog.html)\n[ARIA Dialog Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)\n\n## Variables\n\nGet familiar with the features.\n\n### Info page\n\nA read-only detail view. Footer is optional.\n\n```jsx live\nconst App = () => {\n const [isOpen, setIsOpen] = React.useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Info Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Product Details\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Product Details</ModalPage.Title>\n <ModalPage.Subtitle>View the product information</ModalPage.Subtitle>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Product information goes here.</Text>\n </ModalPage.Content>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Form page\n\nUse when the user needs to create or edit an entity. Always include a Footer\nwith Save/Cancel actions.\n\n```jsx live\nconst App = () => {\n const [isOpen, setIsOpen] = React.useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Form Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Add Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Add Product</ModalPage.Title>\n <ModalPage.Subtitle>Fill in the product details</ModalPage.Subtitle>\n <ModalPage.Actions>\n <Button size=\"sm\" variant=\"outline\">Preview</Button>\n </ModalPage.Actions>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Form fields go here.</Text>\n </ModalPage.Content>\n <ModalPage.Footer>\n <FormActionBar\n onSave={() => {}}\n onCancel={() => {}}\n cancelSlot=\"close\"\n />\n </ModalPage.Footer>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Tabular page\n\nUse when content is organized into multiple sections via tab navigation.\nPlace `ModalPage.TabNav` inside the header and display the active section\nin the content area.\n\n```jsx live\nconst App = () => {\n const [isOpen, setIsOpen] = React.useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Tabular Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Orders\"\n currentPathLabel=\"Order #12345\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Order #12345</ModalPage.Title>\n <ModalPage.Subtitle>Placed on 2024-01-15</ModalPage.Subtitle>\n <ModalPage.TabNav>\n <TabNav.Root aria-label=\"Order sections\">\n <TabNav.Item href=\"#general\" isCurrent>General</TabNav.Item>\n <TabNav.Item href=\"#items\">Items</TabNav.Item>\n <TabNav.Item href=\"#shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </ModalPage.TabNav>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>General information</Text>\n </ModalPage.Content>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Multi-column content\n\nUse `PageContent.Root` with `columns` inside `ModalPage.Content` for\nside-by-side layouts:\n\n```jsx live\nconst App = () => {\n const [isOpen, setIsOpen] = React.useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Two-Column Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n </ModalPage.Header>\n <ModalPage.Content>\n <PageContent.Root variant=\"wide\" columns=\"2/1\">\n <PageContent.Column>\n <Text>Main form area (2/3 width)</Text>\n </PageContent.Column>\n <PageContent.Column sticky>\n <Text>Sidebar summary (1/3 width, sticky)</Text>\n </PageContent.Column>\n </PageContent.Root>\n </ModalPage.Content>\n <ModalPage.Footer>\n <FormActionBar\n onSave={() => {}}\n onCancel={() => {}}\n cancelSlot=\"close\"\n />\n </ModalPage.Footer>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Stacked pages\n\nUse when a workflow requires drilling into a sub-task — for example, opening\n\"Add Variant\" while the \"Edit Product\" page is open. Place the second\n`ModalPage.Root` inside the first `ModalPage.Content`. Each page manages its\nown independent open state.\n\n```jsx live\nconst App = () => {\n const [isFirstOpen, setIsFirstOpen] = React.useState(false);\n const [isSecondOpen, setIsSecondOpen] = React.useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsFirstOpen(true)}>Open Edit Product</Button>\n <ModalPage.Root\n isOpen={isFirstOpen}\n onClose={() => setIsFirstOpen(false)}\n >\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n <ModalPage.Subtitle>Update the product details</ModalPage.Subtitle>\n </ModalPage.Header>\n <ModalPage.Content>\n <Stack>\n <Text>Product form content goes here.</Text>\n <Button onPress={() => setIsSecondOpen(true)}>\n Open Add Variant\n </Button>\n </Stack>\n\n <ModalPage.Root\n isOpen={isSecondOpen}\n onClose={() => setIsSecondOpen(false)}\n >\n <ModalPage.TopBar\n previousPathLabel=\"Edit Product\"\n currentPathLabel=\"Add Variant\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Add Variant</ModalPage.Title>\n <ModalPage.Subtitle>Define a new product variant</ModalPage.Subtitle>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Variant form content goes here.</Text>\n </ModalPage.Content>\n <ModalPage.Footer>\n <FormActionBar\n saveLabel=\"Save Variant\"\n onSave={() => {}}\n onCancel={() => {}}\n cancelSlot=\"close\"\n />\n </ModalPage.Footer>\n </ModalPage.Root>\n </ModalPage.Content>\n <ModalPage.Footer>\n <FormActionBar\n saveLabel=\"Save Product\"\n onSave={() => {}}\n onCancel={() => {}}\n cancelSlot=\"close\"\n />\n </ModalPage.Footer>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n## Guidelines\n\nUse ModalPage strategically to enhance user workflow without disrupting the\nexperience.\n\n### Best practices\n\n- **Always provide a title**: Use `ModalPage.Title` so screen readers can identify the dialog\n- **Use TopBar navigation**: Always supply meaningful `previousPathLabel` and `currentPathLabel`\n- **Footer actions for forms**: Include Save/Cancel in `ModalPage.Footer` for form pages\n- **Prefer Escape key close**: Don't disable keyboard dismissal — it matches OS conventions and WCAG 2.1\n- **Backdrop click is disabled**: Users must close via back button, Cancel, or Escape to prevent accidental data loss\n\n> [!TIP]\\\n> When to use\n\n- **Complex editing workflows**: Creating or editing entities with many fields\n- **Multi-section detail views**: When content is too rich for a standard Drawer\n- **Tabular data workflows**: When content requires tabs (general, items, shipping)\n- **Full-page focused context**: When the user needs to focus entirely on the task\n\n> [!CAUTION]\\\n> When not to use\n\n- **Simple confirmation dialogs**: Use `Dialog` instead\n- **Single-form quick edits**: Use `Drawer` instead — ModalPage is for complex, multi-field scenarios\n- **Navigation overlays**: Use proper routing for section changes\n\n## Specs\n\n<PropsTable id=\"ModalPage\" />\n\n## Accessibility\n\n### Keyboard Navigation\n\n| Key | Action |\n|-----|--------|\n| `Escape` | Closes the modal page |\n| `Tab` / `Shift+Tab` | Moves focus between interactive elements |\n\nFocus is trapped inside the modal while open and returns to the triggering\nelement when the modal closes.\n\n### Screen Reader Support\n\n- The dialog has `role=\"dialog\"` and `aria-modal=\"true\"`\n- `ModalPage.Title` automatically provides the accessible name for the dialog\n- The back button has a localized label referencing the previous path (e.g. \"Go back to Products\")\n- The breadcrumb separator is hidden from assistive technology\n- The current path label is marked as the current page\n",
|
|
214
214
|
"toc": [
|
|
215
215
|
{
|
|
216
216
|
"value": "Overview",
|
|
@@ -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 = () => {\n const [isOpen, setIsOpen] = React.useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Modal Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n <ModalPage.Subtitle>Update the product details</ModalPage.Subtitle>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>\n This modal page demonstrates accessibility features: dialog role,\n focus trap, Escape key dismissal, and breadcrumb navigation.\n </Text>\n </ModalPage.Content>\n <ModalPage.Footer>\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 = () => {\n const [isOpen, setIsOpen] = React.useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Modal Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n <ModalPage.Subtitle>Update the product details</ModalPage.Subtitle>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>\n This modal page demonstrates accessibility features: dialog role,\n focus trap, Escape key dismissal, and breadcrumb navigation.\n </Text>\n </ModalPage.Content>\n <ModalPage.Footer>\n <FormActionBar\n onSave={() => {}}\n onCancel={() => {}}\n cancelSlot=\"close\"\n />\n </ModalPage.Footer>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Accessibility standards\n\n- **Role identification**: ModalPage has `role=\"dialog\"` with proper labeling via `ModalPage.Title`.\n- **Focus management**: Focus moves to the back button on open and returns to the trigger on close.\n- **Focus containment**: Tab navigation stays within the modal while it is open.\n- **Accessible names**: `ModalPage.Title` automatically provides the dialog's accessible name via `aria-labelledby`.\n- **Accessible description**: `ModalPage.Subtitle`, when present, automatically provides the dialog's accessible description via `aria-describedby`.\n- **Modal indication**: `aria-modal=\"true\"` is set, blocking screen reader interaction with the background.\n- **State announcements**: Screen readers announce when the dialog opens and closes.\n- **Breadcrumb accessibility**: The `/` separator is `aria-hidden=\"true\"`; the current path has `aria-current=\"page\"`.\n- **Back button label**: The back button in `ModalPage.TopBar` uses a localized `aria-label` referencing the `previousPathLabel`.\n- **Breadcrumb previous path**: The `previousPathLabel` text is `aria-hidden=\"true\"` since the back button's label already conveys this information.\n- **Keyboard dismissal**: Escape key always closes the modal (WCAG 2.1 SC 2.1.2 — No Keyboard Trap).\n- **No keyboard trap**: Focus is contained but always escapable via Escape key.\n- **Focus order**: Logical focus progression through all interactive elements.\n- **Focus visible**: Clear focus indicators on all interactive elements.\n- **Contrast**: All text meets minimum contrast requirements.\n- **Non-text contrast**: Focus indicators and UI components meet contrast standards.\n- **Name, role, value**: Proper semantic markup and ARIA attributes throughout.\n\n### Resources\n\n- [W3C ARIA Authoring Practices Guide (APG) - Dialog](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)\n- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)\n- [WCAG 2.1 SC 2.1.2 - No Keyboard Trap](https://www.w3.org/WAI/WCAG21/Understanding/no-keyboard-trap.html)\n",
|
|
379
379
|
"toc": [
|
|
380
380
|
{
|
|
381
381
|
"value": "Accessibility",
|
|
@@ -412,7 +412,7 @@
|
|
|
412
412
|
]
|
|
413
413
|
},
|
|
414
414
|
"dev": {
|
|
415
|
-
"mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { ModalPage } from '@commercetools/nimbus';\n```\n\n### Basic usage\n\nModalPage is a controlled compound component. The basic implementation shows\nhow to manage open state and compose the required sub-components:\n\n```jsx live-dev\nconst App = () => {\n const [isOpen, setIsOpen] = useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Modal Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n <ModalPage.Subtitle>Update the product details</ModalPage.Subtitle>\n <ModalPage.Actions>\n <Button size=\"sm\" variant=\"outline\">Preview</Button>\n </ModalPage.Actions>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Form content goes here.</Text>\n </ModalPage.Content>\n <ModalPage.Footer>\n <Button slot=\"close\" variant=\"outline\">Cancel</Button>\n <Button colorPalette=\"primary\" variant=\"solid\">Save</Button>\n </ModalPage.Footer>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n## Usage examples\n\n### Closing the modal page\n\nThere are three ways to close a ModalPage — all route through the same\n`onClose` callback:\n\n1. **Back button in TopBar** — acts as a close trigger, calling `onClose`\n2. **Any button with `slot=\"close\"`** — automatically triggers `onClose`\n3. **Escape key** — always enabled; matches browser/OS convention and WCAG 2.1\n\n```jsx live-dev\nconst App = () => {\n const [isOpen, setIsOpen] = useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>\n Close via: back button in TopBar, slot=\"close\" button, or Escape key.\n </Text>\n </ModalPage.Content>\n <ModalPage.Footer>\n <Button slot=\"close\" variant=\"outline\">Cancel</Button>\n <Button colorPalette=\"primary\" variant=\"solid\">Save</Button>\n </ModalPage.Footer>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Multi-column content\n\nUse `PageContent.Root` with `columns` inside `ModalPage.Content` for\nside-by-side layouts:\n\n```jsx live-dev\nconst App = () => {\n const [isOpen, setIsOpen] = useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Two-Column Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n </ModalPage.Header>\n <ModalPage.Content>\n <PageContent.Root variant=\"wide\" columns=\"2/1\">\n <PageContent.Column>\n <Text>Main form (2/3 width)</Text>\n </PageContent.Column>\n <PageContent.Column sticky>\n <Text>Summary sidebar (1/3 width, sticky)</Text>\n </PageContent.Column>\n </PageContent.Root>\n </ModalPage.Content>\n <ModalPage.Footer>\n <Button slot=\"close\" variant=\"outline\">Cancel</Button>\n <Button colorPalette=\"primary\" variant=\"solid\">Save</Button>\n </ModalPage.Footer>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Header actions\n\nUse `ModalPage.Actions` to place secondary actions (e.g. Preview, Export) in\nthe right column of the header grid:\n\n```jsx live-dev\nconst App = () => {\n const [isOpen, setIsOpen] = useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open with Header Actions</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n <ModalPage.Subtitle>Last saved 2 minutes ago</ModalPage.Subtitle>\n <ModalPage.Actions>\n <Button size=\"sm\" variant=\"ghost\">Preview</Button>\n <Button size=\"sm\" variant=\"outline\">Export</Button>\n </ModalPage.Actions>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Content area</Text>\n </ModalPage.Content>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Tabular page with TabNav\n\nUse `ModalPage.TabNav` inside the header for tabular page patterns with\nroute-based tab navigation:\n\n```jsx live-dev\nconst App = () => {\n const [isOpen, setIsOpen] = useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Tabular Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Orders\"\n currentPathLabel=\"Order #12345\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Order #12345</ModalPage.Title>\n <ModalPage.Subtitle>Placed on 2024-01-15</ModalPage.Subtitle>\n <ModalPage.TabNav>\n <TabNav.Root aria-label=\"Order sections\">\n <TabNav.Item href=\"#general\" isCurrent>General</TabNav.Item>\n <TabNav.Item href=\"#items\">Items</TabNav.Item>\n <TabNav.Item href=\"#shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </ModalPage.TabNav>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>General information</Text>\n </ModalPage.Content>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Stacked modal pages\n\nWhen a workflow requires drilling into a sub-task (e.g. adding a variant\nwhile editing a product), place a second `ModalPage.Root` inside the first\n`ModalPage.Content`. Each modal page manages its own independent state.\n\nThe breadcrumb in `ModalPage.TopBar` naturally reflects the depth:\nset `previousPathLabel` to the parent page's title so the back button\nreads contextually.\n\nBehaviour that works automatically:\n- Escape closes only the topmost page\n- Focus is trapped in the topmost page; previous page is inert\n- When the second page closes, focus returns to its trigger inside the first page\n- When the first page closes, focus returns to the original page trigger\n\n```jsx live-dev\nconst App = () => {\n const [isFirstOpen, setIsFirstOpen] = useState(false);\n const [isSecondOpen, setIsSecondOpen] = useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsFirstOpen(true)}>Open Edit Product</Button>\n <ModalPage.Root\n isOpen={isFirstOpen}\n onClose={() => setIsFirstOpen(false)}\n >\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n <ModalPage.Subtitle>Update the product details</ModalPage.Subtitle>\n </ModalPage.Header>\n <ModalPage.Content>\n <Stack>\n <Text>Product form content goes here.</Text>\n <Button onPress={() => setIsSecondOpen(true)}>\n Open Add Variant\n </Button>\n </Stack>\n\n <ModalPage.Root\n isOpen={isSecondOpen}\n onClose={() => setIsSecondOpen(false)}\n >\n <ModalPage.TopBar\n previousPathLabel=\"Edit Product\"\n currentPathLabel=\"Add Variant\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Add Variant</ModalPage.Title>\n <ModalPage.Subtitle>Define a new product variant</ModalPage.Subtitle>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Variant form content goes here.</Text>\n </ModalPage.Content>\n <ModalPage.Footer>\n <Button slot=\"close\" variant=\"outline\">Cancel</Button>\n <Button colorPalette=\"primary\" variant=\"solid\">Save Variant</Button>\n </ModalPage.Footer>\n </ModalPage.Root>\n </ModalPage.Content>\n <ModalPage.Footer>\n <Button slot=\"close\" variant=\"outline\">Cancel</Button>\n <Button colorPalette=\"primary\" variant=\"solid\">Save Product</Button>\n </ModalPage.Footer>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n## Component requirements\n\n### Accessible dialog label\n\nThe dialog must have an accessible name. Use `ModalPage.Title` (recommended):\n\n```tsx\n<ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n</ModalPage.Header>\n```\n\n`ModalPage.Title` automatically provides the accessible name for the dialog.\nScreen readers will announce the title text when the dialog opens.\n\n### Custom page width\n\nThe default width is near-fullscreen. Pass a `width` prop to\n`ModalPage.Root` to override:\n\n```tsx\n<ModalPage.Root isOpen={isOpen} onClose={handleClose} width=\"xl\">\n ...\n</ModalPage.Root>\n```\n\n### Footer button spacing\n\n`ModalPage.Footer` automatically spaces its children — place buttons directly\nwithout a layout wrapper:\n\n```tsx\n<ModalPage.Footer>\n <Button slot=\"close\" variant=\"outline\">Cancel</Button>\n <Button colorPalette=\"primary\" variant=\"solid\">Save</Button>\n</ModalPage.Footer>\n```\n\n### Controlled state\n\nModalPage is controlled-only. You must always supply both `isOpen` and\n`onClose`:\n\n```tsx\nconst [isOpen, setIsOpen] = useState(false);\n\n<ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n ...\n</ModalPage.Root>\n```\n\n## Accessibility\n\nModalPage handles most accessibility requirements internally:\n\n- The dialog has `role=\"dialog\"` and `aria-modal=\"true\"`\n- Focus is trapped inside the modal while open\n- Focus moves to the back button on open and returns to the trigger on close\n- Escape key dismissal is always active\n- Backdrop click is disabled — full-page forms should not close accidentally\n\n#### Keyboard navigation\n\n| Key | Action |\n|-----|--------|\n| `Escape` | Closes the modal page |\n| `Tab` / `Shift+Tab` | Navigate focusable elements within the modal |\n\n#### ARIA attributes\n\n- `role=\"dialog\"`: Applied to the dialog container\n- `aria-labelledby`: Automatically linked to `ModalPage.Title`\n- `aria-modal=\"true\"`: Marks the overlay as modal to screen readers\n- `aria-current=\"page\"`: Applied to the current path label in `ModalPage.TopBar`\n- `aria-hidden=\"true\"`: Applied to the breadcrumb separator\n\n## API reference\n\n<PropsTable id=\"ModalPage\" />\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using ModalPage\nin your application. As the component's internal functionality is already tested\nby Nimbus, these patterns help you verify your integration and application-specific\nlogic.\n\n### Basic usage\n\n```tsx\nimport { useState } from \"react\";\nimport { render, screen, fireEvent } from \"@testing-library/react\";\nimport { describe, it, expect, vi } from \"vitest\";\nimport {\n Button,\n ModalPage,\n NimbusProvider,\n PageContent,\n Stack,\n TabNav,\n Text,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"ModalPage - Basic usage\", () => {\n it(\"opens and closes via controlled state\", () => {\n const onClose = vi.fn();\n\n const Example = () => {\n const [isOpen, setIsOpen] = useState(false);\n return (\n <NimbusProvider>\n <Button onPress={() => setIsOpen(true)}>Open</Button>\n <ModalPage.Root\n isOpen={isOpen}\n onClose={() => {\n onClose();\n setIsOpen(false);\n }}\n >\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n <ModalPage.Subtitle>\n Update the product details\n </ModalPage.Subtitle>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Form content</Text>\n </ModalPage.Content>\n <ModalPage.Footer>\n <Button slot=\"close\" variant=\"outline\">\n Cancel\n </Button>\n <Button colorPalette=\"primary\" variant=\"solid\">\n Save\n </Button>\n </ModalPage.Footer>\n </ModalPage.Root>\n </NimbusProvider>\n );\n };\n\n render(<Example />);\n\n // Dialog is not in DOM before opening\n expect(screen.queryByRole(\"dialog\")).not.toBeInTheDocument();\n\n // Open the modal\n fireEvent.click(screen.getByRole(\"button\", { name: \"Open\" }));\n expect(screen.getByRole(\"dialog\")).toBeInTheDocument();\n expect(\n screen.getByRole(\"heading\", { name: \"Edit Product\" })\n ).toBeInTheDocument();\n\n // Close via slot=\"close\" button\n fireEvent.click(screen.getByRole(\"button\", { name: \"Cancel\" }));\n expect(onClose).toHaveBeenCalledOnce();\n });\n});\n```\n\n### Form page with header actions and footer\n\n```tsx\nimport { useState } from \"react\";\nimport { render, screen, fireEvent } from \"@testing-library/react\";\nimport { describe, it, expect, vi } from \"vitest\";\nimport {\n Button,\n ModalPage,\n NimbusProvider,\n PageContent,\n Stack,\n TabNav,\n Text,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"ModalPage - Form page\", () => {\n it(\"renders header actions and footer buttons\", () => {\n render(\n <NimbusProvider>\n <ModalPage.Root isOpen onClose={() => {}}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Add Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Add Product</ModalPage.Title>\n <ModalPage.Subtitle>Fill in the product details</ModalPage.Subtitle>\n <ModalPage.Actions>\n <Button size=\"sm\" variant=\"outline\">\n Preview\n </Button>\n </ModalPage.Actions>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Form fields go here</Text>\n </ModalPage.Content>\n <ModalPage.Footer>\n <Button slot=\"close\" variant=\"outline\">\n Cancel\n </Button>\n <Button colorPalette=\"primary\" variant=\"solid\">\n Save\n </Button>\n </ModalPage.Footer>\n </ModalPage.Root>\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"button\", { name: \"Preview\" })).toBeInTheDocument();\n expect(screen.getByRole(\"button\", { name: \"Cancel\" })).toBeInTheDocument();\n expect(screen.getByRole(\"button\", { name: \"Save\" })).toBeInTheDocument();\n });\n});\n```\n\n### Multi-column layout\n\n```tsx\nimport { useState } from \"react\";\nimport { render, screen, fireEvent } from \"@testing-library/react\";\nimport { describe, it, expect, vi } from \"vitest\";\nimport {\n Button,\n ModalPage,\n NimbusProvider,\n PageContent,\n Stack,\n TabNav,\n Text,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"ModalPage - Multi-column layout\", () => {\n it(\"renders a 2/1 column layout\", () => {\n render(\n <NimbusProvider>\n <ModalPage.Root isOpen onClose={() => {}}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n </ModalPage.Header>\n <ModalPage.Content>\n <PageContent.Root variant=\"wide\" columns=\"2/1\">\n <PageContent.Column>\n <Text>Main form area</Text>\n </PageContent.Column>\n <PageContent.Column sticky>\n <Text>Summary sidebar</Text>\n </PageContent.Column>\n </PageContent.Root>\n </ModalPage.Content>\n </ModalPage.Root>\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Main form area\")).toBeInTheDocument();\n expect(screen.getByText(\"Summary sidebar\")).toBeInTheDocument();\n });\n});\n```\n\n### Tabular page with TabNav\n\n```tsx\nimport { useState } from \"react\";\nimport { render, screen, fireEvent } from \"@testing-library/react\";\nimport { describe, it, expect, vi } from \"vitest\";\nimport {\n Button,\n ModalPage,\n NimbusProvider,\n PageContent,\n Stack,\n TabNav,\n Text,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"ModalPage - Tabular page\", () => {\n it(\"renders tab navigation inside the header\", () => {\n render(\n <NimbusProvider>\n <ModalPage.Root isOpen onClose={() => {}}>\n <ModalPage.TopBar\n previousPathLabel=\"Orders\"\n currentPathLabel=\"Order #12345\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Order #12345</ModalPage.Title>\n <ModalPage.Subtitle>Placed on 2024-01-15</ModalPage.Subtitle>\n <ModalPage.TabNav>\n <TabNav.Root aria-label=\"Order sections\">\n <TabNav.Item href=\"#general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"#items\">Items</TabNav.Item>\n <TabNav.Item href=\"#shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </ModalPage.TabNav>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>General information</Text>\n </ModalPage.Content>\n </ModalPage.Root>\n </NimbusProvider>\n );\n\n expect(\n screen.getByRole(\"heading\", { name: \"Order #12345\" })\n ).toBeInTheDocument();\n expect(screen.getByText(\"General\")).toBeInTheDocument();\n expect(screen.getByText(\"Items\")).toBeInTheDocument();\n expect(screen.getByText(\"Shipping\")).toBeInTheDocument();\n });\n});\n```\n\n### Stacked modal pages\n\n```tsx\nimport { useState } from \"react\";\nimport { render, screen, fireEvent } from \"@testing-library/react\";\nimport { describe, it, expect, vi } from \"vitest\";\nimport {\n Button,\n ModalPage,\n NimbusProvider,\n PageContent,\n Stack,\n TabNav,\n Text,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"ModalPage - Stacked pages\", () => {\n it(\"renders nested modal pages independently\", () => {\n const Example = () => {\n const [isFirstOpen, setIsFirstOpen] = useState(true);\n const [isSecondOpen, setIsSecondOpen] = useState(false);\n return (\n <NimbusProvider>\n <ModalPage.Root\n isOpen={isFirstOpen}\n onClose={() => setIsFirstOpen(false)}\n >\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n </ModalPage.Header>\n <ModalPage.Content>\n <Stack>\n <Text>Product form content</Text>\n <Button onPress={() => setIsSecondOpen(true)}>\n Add Variant\n </Button>\n </Stack>\n <ModalPage.Root\n isOpen={isSecondOpen}\n onClose={() => setIsSecondOpen(false)}\n >\n <ModalPage.TopBar\n previousPathLabel=\"Edit Product\"\n currentPathLabel=\"Add Variant\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Add Variant</ModalPage.Title>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Variant form content</Text>\n </ModalPage.Content>\n </ModalPage.Root>\n </ModalPage.Content>\n </ModalPage.Root>\n </NimbusProvider>\n );\n };\n\n render(<Example />);\n\n expect(screen.getByText(\"Product form content\")).toBeInTheDocument();\n\n // Open stacked page\n fireEvent.click(screen.getByRole(\"button\", { name: \"Add Variant\" }));\n expect(screen.getByText(\"Variant form content\")).toBeInTheDocument();\n });\n});\n```\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-layout-modalpage--docs)\n- [React Aria Dialog](https://react-spectrum.adobe.com/react-aria/Dialog.html)\n- [ARIA Dialog Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)\n",
|
|
415
|
+
"mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { ModalPage } from '@commercetools/nimbus';\n```\n\n### Basic usage\n\nModalPage is a controlled compound component. The basic implementation shows\nhow to manage open state and compose the required sub-components:\n\n```jsx live-dev\nconst App = () => {\n const [isOpen, setIsOpen] = useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Modal Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n <ModalPage.Subtitle>Update the product details</ModalPage.Subtitle>\n <ModalPage.Actions>\n <Button size=\"sm\" variant=\"outline\">Preview</Button>\n </ModalPage.Actions>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Form content goes here.</Text>\n </ModalPage.Content>\n <ModalPage.Footer>\n <FormActionBar\n onSave={() => {}}\n onCancel={() => {}}\n cancelSlot=\"close\"\n />\n </ModalPage.Footer>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n## Usage examples\n\n### Closing the modal page\n\nThere are three ways to close a ModalPage — all route through the same\n`onClose` callback:\n\n1. **Back button in TopBar** — acts as a close trigger, calling `onClose`\n2. **Any button with `slot=\"close\"`** — automatically triggers `onClose`\n3. **Escape key** — always enabled; matches browser/OS convention and WCAG 2.1\n\n```jsx live-dev\nconst App = () => {\n const [isOpen, setIsOpen] = useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>\n Close via: back button in TopBar, slot=\"close\" button, or Escape key.\n </Text>\n </ModalPage.Content>\n <ModalPage.Footer>\n <FormActionBar\n onSave={() => {}}\n onCancel={() => {}}\n cancelSlot=\"close\"\n />\n </ModalPage.Footer>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Multi-column content\n\nUse `PageContent.Root` with `columns` inside `ModalPage.Content` for\nside-by-side layouts:\n\n```jsx live-dev\nconst App = () => {\n const [isOpen, setIsOpen] = useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Two-Column Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n </ModalPage.Header>\n <ModalPage.Content>\n <PageContent.Root variant=\"wide\" columns=\"2/1\">\n <PageContent.Column>\n <Text>Main form (2/3 width)</Text>\n </PageContent.Column>\n <PageContent.Column sticky>\n <Text>Summary sidebar (1/3 width, sticky)</Text>\n </PageContent.Column>\n </PageContent.Root>\n </ModalPage.Content>\n <ModalPage.Footer>\n <FormActionBar\n onSave={() => {}}\n onCancel={() => {}}\n cancelSlot=\"close\"\n />\n </ModalPage.Footer>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Header actions\n\nUse `ModalPage.Actions` to place secondary actions (e.g. Preview, Export) in\nthe right column of the header grid:\n\n```jsx live-dev\nconst App = () => {\n const [isOpen, setIsOpen] = useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open with Header Actions</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n <ModalPage.Subtitle>Last saved 2 minutes ago</ModalPage.Subtitle>\n <ModalPage.Actions>\n <Button size=\"sm\" variant=\"ghost\">Preview</Button>\n <Button size=\"sm\" variant=\"outline\">Export</Button>\n </ModalPage.Actions>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Content area</Text>\n </ModalPage.Content>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Tabular page with TabNav\n\nUse `ModalPage.TabNav` inside the header for tabular page patterns with\nroute-based tab navigation:\n\n```jsx live-dev\nconst App = () => {\n const [isOpen, setIsOpen] = useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsOpen(true)}>Open Tabular Page</Button>\n <ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n <ModalPage.TopBar\n previousPathLabel=\"Orders\"\n currentPathLabel=\"Order #12345\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Order #12345</ModalPage.Title>\n <ModalPage.Subtitle>Placed on 2024-01-15</ModalPage.Subtitle>\n <ModalPage.TabNav>\n <TabNav.Root aria-label=\"Order sections\">\n <TabNav.Item href=\"#general\" isCurrent>General</TabNav.Item>\n <TabNav.Item href=\"#items\">Items</TabNav.Item>\n <TabNav.Item href=\"#shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </ModalPage.TabNav>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>General information</Text>\n </ModalPage.Content>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n### Stacked modal pages\n\nWhen a workflow requires drilling into a sub-task (e.g. adding a variant\nwhile editing a product), place a second `ModalPage.Root` inside the first\n`ModalPage.Content`. Each modal page manages its own independent state.\n\nThe breadcrumb in `ModalPage.TopBar` naturally reflects the depth:\nset `previousPathLabel` to the parent page's title so the back button\nreads contextually.\n\nBehaviour that works automatically:\n- Escape closes only the topmost page\n- Focus is trapped in the topmost page; previous page is inert\n- When the second page closes, focus returns to its trigger inside the first page\n- When the first page closes, focus returns to the original page trigger\n\n```jsx live-dev\nconst App = () => {\n const [isFirstOpen, setIsFirstOpen] = useState(false);\n const [isSecondOpen, setIsSecondOpen] = useState(false);\n return (\n <Stack>\n <Button onPress={() => setIsFirstOpen(true)}>Open Edit Product</Button>\n <ModalPage.Root\n isOpen={isFirstOpen}\n onClose={() => setIsFirstOpen(false)}\n >\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n <ModalPage.Subtitle>Update the product details</ModalPage.Subtitle>\n </ModalPage.Header>\n <ModalPage.Content>\n <Stack>\n <Text>Product form content goes here.</Text>\n <Button onPress={() => setIsSecondOpen(true)}>\n Open Add Variant\n </Button>\n </Stack>\n\n <ModalPage.Root\n isOpen={isSecondOpen}\n onClose={() => setIsSecondOpen(false)}\n >\n <ModalPage.TopBar\n previousPathLabel=\"Edit Product\"\n currentPathLabel=\"Add Variant\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Add Variant</ModalPage.Title>\n <ModalPage.Subtitle>Define a new product variant</ModalPage.Subtitle>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Variant form content goes here.</Text>\n </ModalPage.Content>\n <ModalPage.Footer>\n <FormActionBar\n saveLabel=\"Save Variant\"\n onSave={() => {}}\n onCancel={() => {}}\n cancelSlot=\"close\"\n />\n </ModalPage.Footer>\n </ModalPage.Root>\n </ModalPage.Content>\n <ModalPage.Footer>\n <FormActionBar\n saveLabel=\"Save Product\"\n onSave={() => {}}\n onCancel={() => {}}\n cancelSlot=\"close\"\n />\n </ModalPage.Footer>\n </ModalPage.Root>\n </Stack>\n );\n}\n```\n\n## Component requirements\n\n### Accessible dialog label\n\nThe dialog must have an accessible name. Use `ModalPage.Title` (recommended):\n\n```tsx\n<ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n</ModalPage.Header>\n```\n\n`ModalPage.Title` automatically provides the accessible name for the dialog.\nScreen readers will announce the title text when the dialog opens.\n\n### Custom page width\n\nThe default width is near-fullscreen. Pass a `width` prop to\n`ModalPage.Root` to override:\n\n```tsx\n<ModalPage.Root isOpen={isOpen} onClose={handleClose} width=\"xl\">\n ...\n</ModalPage.Root>\n```\n\n### Footer button spacing\n\n`ModalPage.Footer` automatically spaces its children — place buttons directly\nwithout a layout wrapper:\n\n```tsx\n<ModalPage.Footer>\n <Button slot=\"close\" variant=\"outline\">Cancel</Button>\n <Button colorPalette=\"primary\" variant=\"solid\">Save</Button>\n</ModalPage.Footer>\n```\n\n### Controlled state\n\nModalPage is controlled-only. You must always supply both `isOpen` and\n`onClose`:\n\n```tsx\nconst [isOpen, setIsOpen] = useState(false);\n\n<ModalPage.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>\n ...\n</ModalPage.Root>\n```\n\n## Accessibility\n\nModalPage handles most accessibility requirements internally:\n\n- The dialog has `role=\"dialog\"` and `aria-modal=\"true\"`\n- Focus is trapped inside the modal while open\n- Focus moves to the back button on open and returns to the trigger on close\n- Escape key dismissal is always active\n- Backdrop click is disabled — full-page forms should not close accidentally\n\n#### Keyboard navigation\n\n| Key | Action |\n|-----|--------|\n| `Escape` | Closes the modal page |\n| `Tab` / `Shift+Tab` | Navigate focusable elements within the modal |\n\n#### ARIA attributes\n\n- `role=\"dialog\"`: Applied to the dialog container\n- `aria-labelledby`: Automatically linked to `ModalPage.Title`\n- `aria-modal=\"true\"`: Marks the overlay as modal to screen readers\n- `aria-current=\"page\"`: Applied to the current path label in `ModalPage.TopBar`\n- `aria-hidden=\"true\"`: Applied to the breadcrumb separator\n\n## API reference\n\n<PropsTable id=\"ModalPage\" />\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using ModalPage\nin your application. As the component's internal functionality is already tested\nby Nimbus, these patterns help you verify your integration and application-specific\nlogic.\n\n### Basic usage\n\n```tsx\nimport { useState } from \"react\";\nimport { render, screen, fireEvent } from \"@testing-library/react\";\nimport { describe, it, expect, vi } from \"vitest\";\nimport {\n Button,\n ModalPage,\n NimbusProvider,\n PageContent,\n Stack,\n TabNav,\n Text,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"ModalPage - Basic usage\", () => {\n it(\"opens and closes via controlled state\", () => {\n const onClose = vi.fn();\n\n const Example = () => {\n const [isOpen, setIsOpen] = useState(false);\n return (\n <NimbusProvider>\n <Button onPress={() => setIsOpen(true)}>Open</Button>\n <ModalPage.Root\n isOpen={isOpen}\n onClose={() => {\n onClose();\n setIsOpen(false);\n }}\n >\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n <ModalPage.Subtitle>\n Update the product details\n </ModalPage.Subtitle>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Form content</Text>\n </ModalPage.Content>\n <ModalPage.Footer>\n <Button slot=\"close\" variant=\"outline\">\n Cancel\n </Button>\n <Button colorPalette=\"primary\" variant=\"solid\">\n Save\n </Button>\n </ModalPage.Footer>\n </ModalPage.Root>\n </NimbusProvider>\n );\n };\n\n render(<Example />);\n\n // Dialog is not in DOM before opening\n expect(screen.queryByRole(\"dialog\")).not.toBeInTheDocument();\n\n // Open the modal\n fireEvent.click(screen.getByRole(\"button\", { name: \"Open\" }));\n expect(screen.getByRole(\"dialog\")).toBeInTheDocument();\n expect(\n screen.getByRole(\"heading\", { name: \"Edit Product\" })\n ).toBeInTheDocument();\n\n // Close via slot=\"close\" button\n fireEvent.click(screen.getByRole(\"button\", { name: \"Cancel\" }));\n expect(onClose).toHaveBeenCalledOnce();\n });\n});\n```\n\n### Form page with header actions and footer\n\n```tsx\nimport { useState } from \"react\";\nimport { render, screen, fireEvent } from \"@testing-library/react\";\nimport { describe, it, expect, vi } from \"vitest\";\nimport {\n Button,\n ModalPage,\n NimbusProvider,\n PageContent,\n Stack,\n TabNav,\n Text,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"ModalPage - Form page\", () => {\n it(\"renders header actions and footer buttons\", () => {\n render(\n <NimbusProvider>\n <ModalPage.Root isOpen onClose={() => {}}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Add Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Add Product</ModalPage.Title>\n <ModalPage.Subtitle>Fill in the product details</ModalPage.Subtitle>\n <ModalPage.Actions>\n <Button size=\"sm\" variant=\"outline\">\n Preview\n </Button>\n </ModalPage.Actions>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Form fields go here</Text>\n </ModalPage.Content>\n <ModalPage.Footer>\n <Button slot=\"close\" variant=\"outline\">\n Cancel\n </Button>\n <Button colorPalette=\"primary\" variant=\"solid\">\n Save\n </Button>\n </ModalPage.Footer>\n </ModalPage.Root>\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"button\", { name: \"Preview\" })).toBeInTheDocument();\n expect(screen.getByRole(\"button\", { name: \"Cancel\" })).toBeInTheDocument();\n expect(screen.getByRole(\"button\", { name: \"Save\" })).toBeInTheDocument();\n });\n});\n```\n\n### Multi-column layout\n\n```tsx\nimport { useState } from \"react\";\nimport { render, screen, fireEvent } from \"@testing-library/react\";\nimport { describe, it, expect, vi } from \"vitest\";\nimport {\n Button,\n ModalPage,\n NimbusProvider,\n PageContent,\n Stack,\n TabNav,\n Text,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"ModalPage - Multi-column layout\", () => {\n it(\"renders a 2/1 column layout\", () => {\n render(\n <NimbusProvider>\n <ModalPage.Root isOpen onClose={() => {}}>\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n </ModalPage.Header>\n <ModalPage.Content>\n <PageContent.Root variant=\"wide\" columns=\"2/1\">\n <PageContent.Column>\n <Text>Main form area</Text>\n </PageContent.Column>\n <PageContent.Column sticky>\n <Text>Summary sidebar</Text>\n </PageContent.Column>\n </PageContent.Root>\n </ModalPage.Content>\n </ModalPage.Root>\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Main form area\")).toBeInTheDocument();\n expect(screen.getByText(\"Summary sidebar\")).toBeInTheDocument();\n });\n});\n```\n\n### Tabular page with TabNav\n\n```tsx\nimport { useState } from \"react\";\nimport { render, screen, fireEvent } from \"@testing-library/react\";\nimport { describe, it, expect, vi } from \"vitest\";\nimport {\n Button,\n ModalPage,\n NimbusProvider,\n PageContent,\n Stack,\n TabNav,\n Text,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"ModalPage - Tabular page\", () => {\n it(\"renders tab navigation inside the header\", () => {\n render(\n <NimbusProvider>\n <ModalPage.Root isOpen onClose={() => {}}>\n <ModalPage.TopBar\n previousPathLabel=\"Orders\"\n currentPathLabel=\"Order #12345\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Order #12345</ModalPage.Title>\n <ModalPage.Subtitle>Placed on 2024-01-15</ModalPage.Subtitle>\n <ModalPage.TabNav>\n <TabNav.Root aria-label=\"Order sections\">\n <TabNav.Item href=\"#general\" isCurrent>\n General\n </TabNav.Item>\n <TabNav.Item href=\"#items\">Items</TabNav.Item>\n <TabNav.Item href=\"#shipping\">Shipping</TabNav.Item>\n </TabNav.Root>\n </ModalPage.TabNav>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>General information</Text>\n </ModalPage.Content>\n </ModalPage.Root>\n </NimbusProvider>\n );\n\n expect(\n screen.getByRole(\"heading\", { name: \"Order #12345\" })\n ).toBeInTheDocument();\n expect(screen.getByText(\"General\")).toBeInTheDocument();\n expect(screen.getByText(\"Items\")).toBeInTheDocument();\n expect(screen.getByText(\"Shipping\")).toBeInTheDocument();\n });\n});\n```\n\n### Stacked modal pages\n\n```tsx\nimport { useState } from \"react\";\nimport { render, screen, fireEvent } from \"@testing-library/react\";\nimport { describe, it, expect, vi } from \"vitest\";\nimport {\n Button,\n ModalPage,\n NimbusProvider,\n PageContent,\n Stack,\n TabNav,\n Text,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"ModalPage - Stacked pages\", () => {\n it(\"renders nested modal pages independently\", () => {\n const Example = () => {\n const [isFirstOpen, setIsFirstOpen] = useState(true);\n const [isSecondOpen, setIsSecondOpen] = useState(false);\n return (\n <NimbusProvider>\n <ModalPage.Root\n isOpen={isFirstOpen}\n onClose={() => setIsFirstOpen(false)}\n >\n <ModalPage.TopBar\n previousPathLabel=\"Products\"\n currentPathLabel=\"Edit Product\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Edit Product</ModalPage.Title>\n </ModalPage.Header>\n <ModalPage.Content>\n <Stack>\n <Text>Product form content</Text>\n <Button onPress={() => setIsSecondOpen(true)}>\n Add Variant\n </Button>\n </Stack>\n <ModalPage.Root\n isOpen={isSecondOpen}\n onClose={() => setIsSecondOpen(false)}\n >\n <ModalPage.TopBar\n previousPathLabel=\"Edit Product\"\n currentPathLabel=\"Add Variant\"\n />\n <ModalPage.Header>\n <ModalPage.Title>Add Variant</ModalPage.Title>\n </ModalPage.Header>\n <ModalPage.Content>\n <Text>Variant form content</Text>\n </ModalPage.Content>\n </ModalPage.Root>\n </ModalPage.Content>\n </ModalPage.Root>\n </NimbusProvider>\n );\n };\n\n render(<Example />);\n\n expect(screen.getByText(\"Product form content\")).toBeInTheDocument();\n\n // Open stacked page\n fireEvent.click(screen.getByRole(\"button\", { name: \"Add Variant\" }));\n expect(screen.getByText(\"Variant form content\")).toBeInTheDocument();\n });\n});\n```\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-layout-modalpage--docs)\n- [React Aria Dialog](https://react-spectrum.adobe.com/react-aria/Dialog.html)\n- [ARIA Dialog Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)\n",
|
|
416
416
|
"toc": [
|
|
417
417
|
{
|
|
418
418
|
"value": "Getting started",
|
|
@@ -105,10 +105,10 @@
|
|
|
105
105
|
}
|
|
106
106
|
]
|
|
107
107
|
},
|
|
108
|
-
"mdx": "\n## Overview\n\nScrollArea constrains content within a fixed region and provides themed\nscrollbar overlays that replace native browser scrollbars. The scrollbar\nappears on hover or during scrolling by default, keeping the interface clean\nuntil the user needs to scroll. The component is keyboard accessible — when\ncontent overflows, the area becomes focusable via Tab and scrollable with\narrow keys.\n\n### Resources\n\nDeep dive into implementation details and access the Nimbus design library.\n\n[Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-scrollarea--docs)\n[Chakra UI ScrollArea](https://chakra-ui.com/docs/components/scroll-area)\n\n## Variables\n\nGet familiar with the features.\n\n### Orientation\n\nControl which axes are scrollable. The default is
|
|
108
|
+
"mdx": "\n## Overview\n\nScrollArea constrains content within a fixed region and provides themed\nscrollbar overlays that replace native browser scrollbars. The scrollbar\nappears on hover or during scrolling by default, keeping the interface clean\nuntil the user needs to scroll. The component is keyboard accessible — when\ncontent overflows, the area becomes focusable via Tab and scrollable with\narrow keys.\n\n### Resources\n\nDeep dive into implementation details and access the Nimbus design library.\n\n[Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-scrollarea--docs)\n[Chakra UI ScrollArea](https://chakra-ui.com/docs/components/scroll-area)\n\n## Variables\n\nGet familiar with the features.\n\n### Orientation\n\nControl which axes are scrollable. The default is `both`, so overflow on\neither axis surfaces a visible scrollbar.\n\n```jsx live\nconst App = () => (\n <Stack gap=\"600\">\n <Box>\n <Text fontSize=\"sm\" mb=\"200\" fontWeight=\"bold\">Both (default)</Text>\n <ScrollArea maxH=\"120px\" maxW=\"300px\" variant=\"always\">\n <Box whiteSpace=\"nowrap\">\n {Array.from({ length: 15 }, (_, i) => (\n <Text key={i} fontSize=\"sm\">{\"Bidirectional content \".repeat(15)}</Text>\n ))}\n </Box>\n </ScrollArea>\n </Box>\n <Box>\n <Text fontSize=\"sm\" mb=\"200\" fontWeight=\"bold\">Vertical</Text>\n <ScrollArea maxH=\"120px\" w=\"300px\" orientation=\"vertical\" variant=\"always\">\n {Array.from({ length: 15 }, (_, i) => (\n <Text key={i} fontSize=\"sm\">Line {i + 1}: Sample content for vertical scrolling.</Text>\n ))}\n </ScrollArea>\n </Box>\n <Box>\n <Text fontSize=\"sm\" mb=\"200\" fontWeight=\"bold\">Horizontal</Text>\n <ScrollArea maxW=\"300px\" orientation=\"horizontal\" variant=\"always\">\n <Box whiteSpace=\"nowrap\">\n {Array.from({ length: 5 }, (_, i) => (\n <Text key={i} fontSize=\"sm\">{\"Horizontal content \".repeat(15)}</Text>\n ))}\n </Box>\n </ScrollArea>\n </Box>\n </Stack>\n);\n```\n\n### Visibility\n\nThe scrollbar can appear on hover (default) or stay permanently visible.\n\n```jsx live\nconst App = () => (\n <Stack gap=\"600\" direction=\"row\">\n <Box>\n <Text fontSize=\"sm\" mb=\"200\" fontWeight=\"bold\">Hover (default)</Text>\n <ScrollArea maxH=\"120px\" w=\"200px\">\n {Array.from({ length: 15 }, (_, i) => (\n <Text key={i} fontSize=\"sm\">Line {i + 1}</Text>\n ))}\n </ScrollArea>\n </Box>\n <Box>\n <Text fontSize=\"sm\" mb=\"200\" fontWeight=\"bold\">Always visible</Text>\n <ScrollArea maxH=\"120px\" w=\"200px\" variant=\"always\">\n {Array.from({ length: 15 }, (_, i) => (\n <Text key={i} fontSize=\"sm\">Line {i + 1}</Text>\n ))}\n </ScrollArea>\n </Box>\n </Stack>\n);\n```\n\n### Size\n\nControl the scrollbar thickness. Available sizes: `xs`, `sm` (default), `md`, `lg`.\n\n```jsx live\nconst App = () => (\n <Stack gap=\"600\" direction=\"row\" flexWrap=\"wrap\">\n {[\"xs\", \"sm\", \"md\", \"lg\"].map((size) => (\n <Box key={size}>\n <Text fontSize=\"sm\" mb=\"200\" fontWeight=\"bold\">{size}</Text>\n <ScrollArea maxH=\"120px\" w=\"180px\" size={size} variant=\"always\">\n {Array.from({ length: 15 }, (_, i) => (\n <Text key={i} fontSize=\"sm\">Line {i + 1}</Text>\n ))}\n </ScrollArea>\n </Box>\n ))}\n </Stack>\n);\n```\n",
|
|
109
109
|
"views": {
|
|
110
110
|
"overview": {
|
|
111
|
-
"mdx": "\n## Overview\n\nScrollArea constrains content within a fixed region and provides themed\nscrollbar overlays that replace native browser scrollbars. The scrollbar\nappears on hover or during scrolling by default, keeping the interface clean\nuntil the user needs to scroll. The component is keyboard accessible — when\ncontent overflows, the area becomes focusable via Tab and scrollable with\narrow keys.\n\n### Resources\n\nDeep dive into implementation details and access the Nimbus design library.\n\n[Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-scrollarea--docs)\n[Chakra UI ScrollArea](https://chakra-ui.com/docs/components/scroll-area)\n\n## Variables\n\nGet familiar with the features.\n\n### Orientation\n\nControl which axes are scrollable. The default is
|
|
111
|
+
"mdx": "\n## Overview\n\nScrollArea constrains content within a fixed region and provides themed\nscrollbar overlays that replace native browser scrollbars. The scrollbar\nappears on hover or during scrolling by default, keeping the interface clean\nuntil the user needs to scroll. The component is keyboard accessible — when\ncontent overflows, the area becomes focusable via Tab and scrollable with\narrow keys.\n\n### Resources\n\nDeep dive into implementation details and access the Nimbus design library.\n\n[Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-scrollarea--docs)\n[Chakra UI ScrollArea](https://chakra-ui.com/docs/components/scroll-area)\n\n## Variables\n\nGet familiar with the features.\n\n### Orientation\n\nControl which axes are scrollable. The default is `both`, so overflow on\neither axis surfaces a visible scrollbar.\n\n```jsx live\nconst App = () => (\n <Stack gap=\"600\">\n <Box>\n <Text fontSize=\"sm\" mb=\"200\" fontWeight=\"bold\">Both (default)</Text>\n <ScrollArea maxH=\"120px\" maxW=\"300px\" variant=\"always\">\n <Box whiteSpace=\"nowrap\">\n {Array.from({ length: 15 }, (_, i) => (\n <Text key={i} fontSize=\"sm\">{\"Bidirectional content \".repeat(15)}</Text>\n ))}\n </Box>\n </ScrollArea>\n </Box>\n <Box>\n <Text fontSize=\"sm\" mb=\"200\" fontWeight=\"bold\">Vertical</Text>\n <ScrollArea maxH=\"120px\" w=\"300px\" orientation=\"vertical\" variant=\"always\">\n {Array.from({ length: 15 }, (_, i) => (\n <Text key={i} fontSize=\"sm\">Line {i + 1}: Sample content for vertical scrolling.</Text>\n ))}\n </ScrollArea>\n </Box>\n <Box>\n <Text fontSize=\"sm\" mb=\"200\" fontWeight=\"bold\">Horizontal</Text>\n <ScrollArea maxW=\"300px\" orientation=\"horizontal\" variant=\"always\">\n <Box whiteSpace=\"nowrap\">\n {Array.from({ length: 5 }, (_, i) => (\n <Text key={i} fontSize=\"sm\">{\"Horizontal content \".repeat(15)}</Text>\n ))}\n </Box>\n </ScrollArea>\n </Box>\n </Stack>\n);\n```\n\n### Visibility\n\nThe scrollbar can appear on hover (default) or stay permanently visible.\n\n```jsx live\nconst App = () => (\n <Stack gap=\"600\" direction=\"row\">\n <Box>\n <Text fontSize=\"sm\" mb=\"200\" fontWeight=\"bold\">Hover (default)</Text>\n <ScrollArea maxH=\"120px\" w=\"200px\">\n {Array.from({ length: 15 }, (_, i) => (\n <Text key={i} fontSize=\"sm\">Line {i + 1}</Text>\n ))}\n </ScrollArea>\n </Box>\n <Box>\n <Text fontSize=\"sm\" mb=\"200\" fontWeight=\"bold\">Always visible</Text>\n <ScrollArea maxH=\"120px\" w=\"200px\" variant=\"always\">\n {Array.from({ length: 15 }, (_, i) => (\n <Text key={i} fontSize=\"sm\">Line {i + 1}</Text>\n ))}\n </ScrollArea>\n </Box>\n </Stack>\n);\n```\n\n### Size\n\nControl the scrollbar thickness. Available sizes: `xs`, `sm` (default), `md`, `lg`.\n\n```jsx live\nconst App = () => (\n <Stack gap=\"600\" direction=\"row\" flexWrap=\"wrap\">\n {[\"xs\", \"sm\", \"md\", \"lg\"].map((size) => (\n <Box key={size}>\n <Text fontSize=\"sm\" mb=\"200\" fontWeight=\"bold\">{size}</Text>\n <ScrollArea maxH=\"120px\" w=\"180px\" size={size} variant=\"always\">\n {Array.from({ length: 15 }, (_, i) => (\n <Text key={i} fontSize=\"sm\">Line {i + 1}</Text>\n ))}\n </ScrollArea>\n </Box>\n ))}\n </Stack>\n);\n```\n",
|
|
112
112
|
"toc": [
|
|
113
113
|
{
|
|
114
114
|
"value": "Overview",
|
|
@@ -203,7 +203,7 @@
|
|
|
203
203
|
]
|
|
204
204
|
},
|
|
205
205
|
"dev": {
|
|
206
|
-
"mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { ScrollArea, type ScrollAreaProps } from '@commercetools/nimbus';\n```\n\n### Basic usage\n\nWrap content in a `ScrollArea` with a size constraint. The scrollbar appears on\nhover or during scrolling:\n\n```jsx live-dev\nconst App = () => (\n <ScrollArea maxH=\"200px\" w=\"400px\">\n {Array.from({ length: 20 }, (_, i) => (\n <Text key={i} fontSize=\"sm\">\n Line {i + 1}: Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n </Text>\n ))}\n </ScrollArea>\n)\n```\n\n## Usage examples\n\n### Orientation\n\nUse the `orientation` prop to control which scrollbar axes are rendered:\n\n```jsx live-dev\nconst App = () => (\n <Stack gap=\"600\">\n <Box>\n <Text fontSize=\"sm\" mb=\"200\" fontWeight=\"bold\">vertical (default)</Text>\n <ScrollArea maxH=\"120px\" w=\"300px\" variant=\"always\">\n {Array.from({ length: 15 }, (_, i) => (\n <Text key={i} fontSize=\"sm\">Line {i + 1}</Text>\n ))}\n </ScrollArea>\n </Box>\n <Box>\n <Text fontSize=\"sm\" mb=\"200\" fontWeight=\"bold\">horizontal</Text>\n <ScrollArea maxW=\"300px\" orientation=\"horizontal\" variant=\"always\">\n <Box whiteSpace=\"nowrap\">\n {Array.from({ length: 5 }, (_, i) => (\n <Text key={i} fontSize=\"sm\">{\"Long horizontal content \".repeat(15)}</Text>\n ))}\n </Box>\n </ScrollArea>\n </Box>\n <Box>\n <Text fontSize=\"sm\" mb=\"200\" fontWeight=\"bold\">both</Text>\n <ScrollArea maxH=\"120px\" maxW=\"300px\" orientation=\"both\" variant=\"always\">\n <Box whiteSpace=\"nowrap\">\n {Array.from({ length: 15 }, (_, i) => (\n <Text key={i} fontSize=\"sm\">{\"Both axes content \".repeat(15)}</Text>\n ))}\n </Box>\n </ScrollArea>\n </Box>\n </Stack>\n)\n```\n\n### Content padding\n\nPadding props on `ScrollArea` are forwarded to the content area inside the\nscrollable viewport, so `p`, `px`, `py`, etc. work as expected:\n\n```jsx live-dev\nconst App = () => (\n <ScrollArea maxH=\"200px\" w=\"400px\" bg=\"neutral.2\" borderRadius=\"300\" variant=\"always\" p=\"200\">\n {Array.from({ length: 20 }, (_, i) => (\n <Text key={i} fontSize=\"sm\">\n Line {i + 1}: Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n </Text>\n ))}\n </ScrollArea>\n)\n```\n\n### Programmatic scroll access\n\n#### Using `viewportRef`\n\nUse the `viewportRef` prop to get a direct reference to the scrollable viewport\nelement. This is the recommended approach when you need to programmatically\nscroll, attach scroll event listeners, or read scroll position. The ref gives\nyou direct access to the native DOM `scrollTo()` method:\n\n```tsx\nconst viewportRef = useRef<HTMLDivElement>(null);\n\n<ScrollArea viewportRef={viewportRef} maxH=\"400px\">\n {content}\n</ScrollArea>\n\n// Native DOM scrollTo — same API as any scrollable element\nviewportRef.current.scrollTo({ top: 0, behavior: 'smooth' })\n// Or: viewportRef.current.addEventListener('scroll', handler)\n```\n\n#### Using `ids`\n\nWhen ref-forwarding is impractical (e.g., the scroll container is rendered in a\ndifferent component tree), use the `ids` prop to set a known ID on the viewport\nelement for `getElementById` access:\n\n```tsx\n<ScrollArea ids={{ viewport: 'my-scroll-viewport' }} maxH=\"400px\">\n {content}\n</ScrollArea>\n\n// Later: document.getElementById('my-scroll-viewport').scrollTop\n```\n\n#### Using `useScrollArea` (external control)\n\nWhen you need to read scroll state or call scroll methods from *outside* the\n`ScrollArea` tree (e.g., in a parent component), create the machine externally\nwith `useScrollArea` and pass it via the `value` prop. Unlike `viewportRef`\n(which gives you a native DOM element), the `useScrollArea` hook returns a\nstate-machine API with scroll state and convenience methods:\n\n```tsx\nimport { ScrollArea, useScrollArea } from '@commercetools/nimbus';\n\nfunction ParentComponent() {\n const scrollArea = useScrollArea();\n\n return (\n <>\n <button onClick={() => scrollArea.scrollToEdge({ edge: 'top' })}>\n Scroll to top\n </button>\n <p>Has vertical overflow: {String(scrollArea.hasOverflowY)}</p>\n <ScrollArea maxH=\"400px\" value={scrollArea}>\n {content}\n </ScrollArea>\n </>\n );\n}\n```\n\nThe returned object exposes state (`hasOverflowX`, `hasOverflowY`, `isAtTop`,\n`isAtBottom`, `isAtLeft`, `isAtRight`) and methods (`scrollTo()`,\n`scrollToEdge()`, `getScrollProgress()`).\n\n#### Using `useScrollAreaContext` (internal access)\n\nFor components rendered *inside* a `ScrollArea`, the `useScrollAreaContext` hook\nreads the existing machine from React context — no `value` prop needed:\n\n```tsx\nimport { useScrollAreaContext } from '@commercetools/nimbus';\n\nfunction ScrollToTopButton() {\n const { scrollToEdge, isAtTop } = useScrollAreaContext();\n if (isAtTop) return null;\n return <Button onPress={() => scrollToEdge({ edge: 'top', behavior: 'smooth' })}>Back to top</Button>;\n}\n```\n\n## Component requirements\n\n- The component must have a size constraint (`maxH`, `maxW`, `h`, `w`) for scrolling to occur\n\n## Accessibility\n\nThe ScrollArea component ensures keyboard accessibility:\n\n- **Keyboard focusable**: The scrollable viewport receives `tabIndex={0}` when content overflows, making it reachable via Tab\n- **Focus ring**: A Nimbus focus ring appears on the root element when the viewport receives keyboard focus (via `:focus-visible`)\n- **ARIA landmarks**: When the scroll area represents a distinct page section, consider adding `role=\"region\"` with an `aria-label` or `aria-labelledby` to create a named landmark for screen reader navigation\n\n## API reference\n\n<PropsTable id=\"ScrollArea\" />\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using ScrollArea 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 Usage\n\nBasic vertical scrollable container\n\n```tsx\nimport React from \"react\";\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport {\n ScrollArea,\n NimbusProvider,\n useScrollArea,\n Box,\n Text,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"ScrollArea - Basic usage\", () => {\n it(\"renders children inside a scrollable container\", () => {\n render(\n <NimbusProvider>\n <ScrollArea maxH=\"200px\">\n <OverflowingContent />\n </ScrollArea>\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Line 1\")).toBeInTheDocument();\n });\n\n it(\"renders with always-visible scrollbars\", () => {\n const { container } = render(\n <NimbusProvider>\n <ScrollArea maxH=\"200px\" variant=\"always\">\n <OverflowingContent />\n </ScrollArea>\n </NimbusProvider>\n );\n\n expect(\n container.querySelector('[data-part=\"scrollbar\"]')\n ).toBeInTheDocument();\n });\n});\n```\n\n### Orientation\n\nControlling which scrollbar axes render\n\n```tsx\nimport React from \"react\";\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport {\n ScrollArea,\n NimbusProvider,\n useScrollArea,\n Box,\n Text,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"ScrollArea - Orientation\", () => {\n it(\"renders horizontal scrollbar\", () => {\n const { container } = render(\n <NimbusProvider>\n <ScrollArea maxW=\"300px\" orientation=\"horizontal\">\n <Box whiteSpace=\"nowrap\">\n <Text>{\"Wide content \".repeat(30)}</Text>\n </Box>\n </ScrollArea>\n </NimbusProvider>\n );\n\n const scrollbar = container.querySelector('[data-part=\"scrollbar\"]');\n expect(scrollbar).toHaveAttribute(\"data-orientation\", \"horizontal\");\n });\n\n it(\"renders both axes with a corner\", () => {\n const { container } = render(\n <NimbusProvider>\n <ScrollArea maxH=\"200px\" maxW=\"300px\" orientation=\"both\">\n <Box whiteSpace=\"nowrap\">\n <OverflowingContent />\n </Box>\n </ScrollArea>\n </NimbusProvider>\n );\n\n const scrollbars = container.querySelectorAll('[data-part=\"scrollbar\"]');\n expect(scrollbars).toHaveLength(2);\n expect(container.querySelector('[data-part=\"corner\"]')).toBeInTheDocument();\n });\n});\n```\n\n### Programmatic scroll access\n\nAccessing the scrollable viewport via ref or external hook\n\n```tsx\nimport React from \"react\";\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport {\n ScrollArea,\n NimbusProvider,\n useScrollArea,\n Box,\n Text,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"ScrollArea - Programmatic access\", () => {\n it(\"forwards a ref to the viewport via viewportRef\", () => {\n const ref = React.createRef<HTMLDivElement>();\n\n render(\n <NimbusProvider>\n <ScrollArea maxH=\"200px\" viewportRef={ref}>\n <OverflowingContent />\n </ScrollArea>\n </NimbusProvider>\n );\n\n expect(ref.current).toHaveAttribute(\"data-part\", \"viewport\");\n expect(typeof ref.current?.scrollTo).toBe(\"function\");\n });\n\n it(\"supports external control via useScrollArea + value prop\", () => {\n const ExternalControl = () => {\n const scrollArea = useScrollArea();\n return (\n <ScrollArea maxH=\"200px\" value={scrollArea}>\n <OverflowingContent />\n </ScrollArea>\n );\n };\n\n render(\n <NimbusProvider>\n <ExternalControl />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Line 1\")).toBeInTheDocument();\n });\n});\n```\n\n### Content Padding\n\nApplying padding via an inner Box wrapper\n\n```tsx\nimport React from \"react\";\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport {\n ScrollArea,\n NimbusProvider,\n useScrollArea,\n Box,\n Text,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"ScrollArea - Content padding\", () => {\n it(\"renders padded content via a nested Box\", () => {\n render(\n <NimbusProvider>\n <ScrollArea maxH=\"200px\">\n <Box p=\"200\">\n <OverflowingContent />\n </Box>\n </ScrollArea>\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Line 1\")).toBeInTheDocument();\n });\n});\n```\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-scrollarea--docs)\n- [Chakra UI ScrollArea](https://chakra-ui.com/docs/components/scroll-area)\n",
|
|
206
|
+
"mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { ScrollArea, type ScrollAreaProps } from '@commercetools/nimbus';\n```\n\n### Basic usage\n\nWrap content in a `ScrollArea` with a size constraint. The scrollbar appears on\nhover or during scrolling:\n\n```jsx live-dev\nconst App = () => (\n <ScrollArea maxH=\"200px\" w=\"400px\">\n {Array.from({ length: 20 }, (_, i) => (\n <Text key={i} fontSize=\"sm\">\n Line {i + 1}: Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n </Text>\n ))}\n </ScrollArea>\n)\n```\n\n## Usage examples\n\n### Orientation\n\nUse the `orientation` prop to control which scrollbar axes are rendered:\n\n```jsx live-dev\nconst App = () => (\n <Stack gap=\"600\">\n <Box>\n <Text fontSize=\"sm\" mb=\"200\" fontWeight=\"bold\">both (default)</Text>\n <ScrollArea maxH=\"120px\" maxW=\"300px\" variant=\"always\">\n <Box whiteSpace=\"nowrap\">\n {Array.from({ length: 15 }, (_, i) => (\n <Text key={i} fontSize=\"sm\">{\"Both axes content \".repeat(15)}</Text>\n ))}\n </Box>\n </ScrollArea>\n </Box>\n <Box>\n <Text fontSize=\"sm\" mb=\"200\" fontWeight=\"bold\">vertical</Text>\n <ScrollArea maxH=\"120px\" w=\"300px\" orientation=\"vertical\" variant=\"always\">\n {Array.from({ length: 15 }, (_, i) => (\n <Text key={i} fontSize=\"sm\">Line {i + 1}</Text>\n ))}\n </ScrollArea>\n </Box>\n <Box>\n <Text fontSize=\"sm\" mb=\"200\" fontWeight=\"bold\">horizontal</Text>\n <ScrollArea maxW=\"300px\" orientation=\"horizontal\" variant=\"always\">\n <Box whiteSpace=\"nowrap\">\n {Array.from({ length: 5 }, (_, i) => (\n <Text key={i} fontSize=\"sm\">{\"Long horizontal content \".repeat(15)}</Text>\n ))}\n </Box>\n </ScrollArea>\n </Box>\n </Stack>\n)\n```\n\n### Content padding\n\nPadding props on `ScrollArea` are forwarded to the content area inside the\nscrollable viewport, so `p`, `px`, `py`, etc. work as expected:\n\n```jsx live-dev\nconst App = () => (\n <ScrollArea maxH=\"200px\" w=\"400px\" bg=\"neutral.2\" borderRadius=\"300\" variant=\"always\" p=\"200\">\n {Array.from({ length: 20 }, (_, i) => (\n <Text key={i} fontSize=\"sm\">\n Line {i + 1}: Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n </Text>\n ))}\n </ScrollArea>\n)\n```\n\n### Programmatic scroll access\n\n#### Using `viewportRef`\n\nUse the `viewportRef` prop to get a direct reference to the scrollable viewport\nelement. This is the recommended approach when you need to programmatically\nscroll, attach scroll event listeners, or read scroll position. The ref gives\nyou direct access to the native DOM `scrollTo()` method:\n\n```tsx\nconst viewportRef = useRef<HTMLDivElement>(null);\n\n<ScrollArea viewportRef={viewportRef} maxH=\"400px\">\n {content}\n</ScrollArea>\n\n// Native DOM scrollTo — same API as any scrollable element\nviewportRef.current.scrollTo({ top: 0, behavior: 'smooth' })\n// Or: viewportRef.current.addEventListener('scroll', handler)\n```\n\n#### Using `ids`\n\nWhen ref-forwarding is impractical (e.g., the scroll container is rendered in a\ndifferent component tree), use the `ids` prop to set a known ID on the viewport\nelement for `getElementById` access:\n\n```tsx\n<ScrollArea ids={{ viewport: 'my-scroll-viewport' }} maxH=\"400px\">\n {content}\n</ScrollArea>\n\n// Later: document.getElementById('my-scroll-viewport').scrollTop\n```\n\n#### Using `useScrollArea` (external control)\n\nWhen you need to read scroll state or call scroll methods from *outside* the\n`ScrollArea` tree (e.g., in a parent component), create the machine externally\nwith `useScrollArea` and pass it via the `value` prop. Unlike `viewportRef`\n(which gives you a native DOM element), the `useScrollArea` hook returns a\nstate-machine API with scroll state and convenience methods:\n\n```tsx\nimport { ScrollArea, useScrollArea } from '@commercetools/nimbus';\n\nfunction ParentComponent() {\n const scrollArea = useScrollArea();\n\n return (\n <>\n <button onClick={() => scrollArea.scrollToEdge({ edge: 'top' })}>\n Scroll to top\n </button>\n <p>Has vertical overflow: {String(scrollArea.hasOverflowY)}</p>\n <ScrollArea maxH=\"400px\" value={scrollArea}>\n {content}\n </ScrollArea>\n </>\n );\n}\n```\n\nThe returned object exposes state (`hasOverflowX`, `hasOverflowY`, `isAtTop`,\n`isAtBottom`, `isAtLeft`, `isAtRight`) and methods (`scrollTo()`,\n`scrollToEdge()`, `getScrollProgress()`).\n\n#### Using `useScrollAreaContext` (internal access)\n\nFor components rendered *inside* a `ScrollArea`, the `useScrollAreaContext` hook\nreads the existing machine from React context — no `value` prop needed:\n\n```tsx\nimport { useScrollAreaContext } from '@commercetools/nimbus';\n\nfunction ScrollToTopButton() {\n const { scrollToEdge, isAtTop } = useScrollAreaContext();\n if (isAtTop) return null;\n return <Button onPress={() => scrollToEdge({ edge: 'top', behavior: 'smooth' })}>Back to top</Button>;\n}\n```\n\n## Component requirements\n\n- The component must have a size constraint (`maxH`, `maxW`, `h`, `w`) for scrolling to occur\n\n## Accessibility\n\nThe ScrollArea component ensures keyboard accessibility:\n\n- **Keyboard focusable**: The scrollable viewport receives `tabIndex={0}` when content overflows, making it reachable via Tab\n- **Focus ring**: A Nimbus focus ring appears on the root element when the viewport receives keyboard focus (via `:focus-visible`)\n- **ARIA landmarks**: When the scroll area represents a distinct page section, consider adding `role=\"region\"` with an `aria-label` or `aria-labelledby` to create a named landmark for screen reader navigation\n\n## API reference\n\n<PropsTable id=\"ScrollArea\" />\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using ScrollArea 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 Usage\n\nBasic vertical scrollable container\n\n```tsx\nimport React from \"react\";\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport {\n ScrollArea,\n NimbusProvider,\n useScrollArea,\n Box,\n Text,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"ScrollArea - Basic usage\", () => {\n it(\"renders children inside a scrollable container\", () => {\n render(\n <NimbusProvider>\n <ScrollArea maxH=\"200px\">\n <OverflowingContent />\n </ScrollArea>\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Line 1\")).toBeInTheDocument();\n });\n\n it(\"renders with always-visible scrollbars\", () => {\n const { container } = render(\n <NimbusProvider>\n <ScrollArea maxH=\"200px\" variant=\"always\">\n <OverflowingContent />\n </ScrollArea>\n </NimbusProvider>\n );\n\n expect(\n container.querySelector('[data-part=\"scrollbar\"]')\n ).toBeInTheDocument();\n });\n});\n```\n\n### Orientation\n\nControlling which scrollbar axes render\n\n```tsx\nimport React from \"react\";\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport {\n ScrollArea,\n NimbusProvider,\n useScrollArea,\n Box,\n Text,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"ScrollArea - Orientation\", () => {\n it(\"renders horizontal scrollbar\", () => {\n const { container } = render(\n <NimbusProvider>\n <ScrollArea maxW=\"300px\" orientation=\"horizontal\">\n <Box whiteSpace=\"nowrap\">\n <Text>{\"Wide content \".repeat(30)}</Text>\n </Box>\n </ScrollArea>\n </NimbusProvider>\n );\n\n const scrollbar = container.querySelector('[data-part=\"scrollbar\"]');\n expect(scrollbar).toHaveAttribute(\"data-orientation\", \"horizontal\");\n });\n\n it(\"renders both axes with a corner\", () => {\n const { container } = render(\n <NimbusProvider>\n <ScrollArea maxH=\"200px\" maxW=\"300px\" orientation=\"both\">\n <Box whiteSpace=\"nowrap\">\n <OverflowingContent />\n </Box>\n </ScrollArea>\n </NimbusProvider>\n );\n\n const scrollbars = container.querySelectorAll('[data-part=\"scrollbar\"]');\n expect(scrollbars).toHaveLength(2);\n expect(container.querySelector('[data-part=\"corner\"]')).toBeInTheDocument();\n });\n});\n```\n\n### Programmatic scroll access\n\nAccessing the scrollable viewport via ref or external hook\n\n```tsx\nimport React from \"react\";\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport {\n ScrollArea,\n NimbusProvider,\n useScrollArea,\n Box,\n Text,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"ScrollArea - Programmatic access\", () => {\n it(\"forwards a ref to the viewport via viewportRef\", () => {\n const ref = React.createRef<HTMLDivElement>();\n\n render(\n <NimbusProvider>\n <ScrollArea maxH=\"200px\" viewportRef={ref}>\n <OverflowingContent />\n </ScrollArea>\n </NimbusProvider>\n );\n\n expect(ref.current).toHaveAttribute(\"data-part\", \"viewport\");\n expect(typeof ref.current?.scrollTo).toBe(\"function\");\n });\n\n it(\"supports external control via useScrollArea + value prop\", () => {\n const ExternalControl = () => {\n const scrollArea = useScrollArea();\n return (\n <ScrollArea maxH=\"200px\" value={scrollArea}>\n <OverflowingContent />\n </ScrollArea>\n );\n };\n\n render(\n <NimbusProvider>\n <ExternalControl />\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Line 1\")).toBeInTheDocument();\n });\n});\n```\n\n### Content Padding\n\nApplying padding via an inner Box wrapper\n\n```tsx\nimport React from \"react\";\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport {\n ScrollArea,\n NimbusProvider,\n useScrollArea,\n Box,\n Text,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"ScrollArea - Content padding\", () => {\n it(\"renders padded content via a nested Box\", () => {\n render(\n <NimbusProvider>\n <ScrollArea maxH=\"200px\">\n <Box p=\"200\">\n <OverflowingContent />\n </Box>\n </ScrollArea>\n </NimbusProvider>\n );\n\n expect(screen.getByText(\"Line 1\")).toBeInTheDocument();\n });\n});\n```\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-scrollarea--docs)\n- [Chakra UI ScrollArea](https://chakra-ui.com/docs/components/scroll-area)\n",
|
|
207
207
|
"toc": [
|
|
208
208
|
{
|
|
209
209
|
"value": "Getting started",
|
|
@@ -180,7 +180,7 @@
|
|
|
180
180
|
]
|
|
181
181
|
},
|
|
182
182
|
"a11y": {
|
|
183
|
-
"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 = () => (\n <Avatar firstName=\"Adam\" lastName=\"Vadam\" src=\"https://thispersondoesnotexist.com/ \" />\n)\n```\n\n### Accessibility standards\n\n- **Non-text content:** Avatars must have a text alternative. Use the `alt`\n attribute on the `<img>` tag (if the avatar is an image) or through `ARIA` if\n the avatar is more complex. The alt text should describe the avatar's purpose\n or what it represents (e.g., \"User profile picture,\" \"John Doe's avatar,\"\n \"Company logo\"). If the avatar is purely decorative and doesn't convey any\n meaningful information, the alt attribute should be empty (`alt=\"\"`).\n- **Info and relationships:** If the avatar is associated with other information\n (e.g., a username, profile link), the relationship should be programmatically\n determinable. This can be achieved using semantic `HTML` (e.g., placing the\n avatar and username within a single element) or ARIA attributes (e.g.,\n `aria-labelledby` or `aria-describedby`).\n- **Use of color:** Don't rely on color alone to convey information about the\n avatar (e.g., using a colored border to indicate status). Any information\n conveyed by color must also be available through other means.\n- **Color contrast:** If the avatar contains any text (which is less common),\n that text must meet minimum contrast ratio requirements.\n- **Keyboard accessibility:** If the avatar is interactive (e.g., clicking it\n opens a profile), it must be operable using the keyboard. Users should be able\n to navigate to the avatar using the Tab key and activate it using the Enter or\n Space key.\n- **Link purpose:** If the avatar is a link, its purpose should be clear from\n the surrounding context or from the avatar's alt text. Avoid generic link text\n like \"Click here.\"\n- **Focus visible:** If the avatar is interactive, it should have a visible\n focus indicator when it receives keyboard focus.\n- **On focus:** If the avatar is interactive and its appearance changes on\n focus, the change should not be disorienting or unexpected.\n- **Name, role, value:** Assistive technologies should be able to correctly\n interpret the role of the avatar (e.g., `image`, `link`) and its name (from\n the alt text).\n",
|
|
183
|
+
"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 = () => (\n <Avatar firstName=\"Adam\" lastName=\"Vadam\" src=\"https://thispersondoesnotexist.com/ \" />\n)\n```\n\n### Accessibility standards\n\n- **Non-text content:** Avatars must have a text alternative. Use the `alt`\n attribute on the `<img>` tag (if the avatar is an image) or through `ARIA` if\n the avatar is more complex. The alt text should describe the avatar's purpose\n or what it represents (e.g., \"User profile picture,\" \"John Doe's avatar,\"\n \"Company logo\"). If the avatar is purely decorative and doesn't convey any\n meaningful information, the alt attribute should be empty (`alt=\"\"`).\n- **Generic label fallback:** When `firstName` and `lastName` are both\n missing, empty, or whitespace-only, the Avatar renders a generic\n `Person` icon and applies a localized generic `aria-label`\n (`\"User avatar\"` in English, with translations for all supported\n locales). This guarantees screen readers always announce a meaningful\n label even when user identity data is incomplete. Consumers can still\n override with an explicit `aria-label` prop when more specific context\n is available.\n- **Info and relationships:** If the avatar is associated with other information\n (e.g., a username, profile link), the relationship should be programmatically\n determinable. This can be achieved using semantic `HTML` (e.g., placing the\n avatar and username within a single element) or ARIA attributes (e.g.,\n `aria-labelledby` or `aria-describedby`).\n- **Use of color:** Don't rely on color alone to convey information about the\n avatar (e.g., using a colored border to indicate status). Any information\n conveyed by color must also be available through other means.\n- **Color contrast:** If the avatar contains any text (which is less common),\n that text must meet minimum contrast ratio requirements.\n- **Keyboard accessibility:** If the avatar is interactive (e.g., clicking it\n opens a profile), it must be operable using the keyboard. Users should be able\n to navigate to the avatar using the Tab key and activate it using the Enter or\n Space key.\n- **Link purpose:** If the avatar is a link, its purpose should be clear from\n the surrounding context or from the avatar's alt text. Avoid generic link text\n like \"Click here.\"\n- **Focus visible:** If the avatar is interactive, it should have a visible\n focus indicator when it receives keyboard focus.\n- **On focus:** If the avatar is interactive and its appearance changes on\n focus, the change should not be disorienting or unexpected.\n- **Name, role, value:** Assistive technologies should be able to correctly\n interpret the role of the avatar (e.g., `image`, `link`) and its name (from\n the alt text).\n",
|
|
184
184
|
"toc": [
|
|
185
185
|
{
|
|
186
186
|
"value": "Accessibility",
|
|
@@ -206,7 +206,7 @@
|
|
|
206
206
|
]
|
|
207
207
|
},
|
|
208
208
|
"dev": {
|
|
209
|
-
"mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { Avatar, type AvatarProps } from \"@commercetools/nimbus\";\n```\n\n### Basic usage\n\nThe simplest implementation displays an avatar with initials derived from first\nand last names:\n\n```jsx live-dev\nconst App = () => (\n <Avatar firstName=\"John\" lastName=\"Doe\" />\n)\n```\n\n## Usage examples\n\n### Size options\n\nThe `2xs`, `xs`, and `md` size variants are available to match your interface\ndensity:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"2xs\" />\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"xs\" />\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"md\" />\n </Stack>\n)\n```\n\n### Color palettes\n\nAvatars support different color palettes to convey semantic meaning or match\nyour design theme:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Avatar firstName=\"John\" lastName=\"Doe\" colorPalette=\"primary\" />\n <Avatar firstName=\"Jane\" lastName=\"Smith\" colorPalette=\"positive\" />\n <Avatar firstName=\"Alex\" lastName=\"Chen\" colorPalette=\"info\" />\n <Avatar firstName=\"Maria\" lastName=\"Garcia\" colorPalette=\"critical\" />\n <Avatar firstName=\"Sam\" lastName=\"Wilson\" colorPalette=\"cyan\" />\n </Stack>\n)\n```\n\n### With image\n\nWhen a `src` prop is provided, the Avatar displays the image. If the image fails\nto load, it automatically falls back to initials:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Avatar\n firstName=\"Jane\"\n lastName=\"Smith\"\n src=\"https://i.pravatar.cc/150?img=1\"\n alt=\"Jane Smith's profile picture\"\n />\n <Avatar\n firstName=\"Alex\"\n lastName=\"Johnson\"\n src=\"https://i.pravatar.cc/150?img=2\"\n alt=\"Alex Johnson's profile picture\"\n />\n </Stack>\n)\n```\n\n### Initials fallback\n\nThe Avatar automatically extracts and displays initials from the provided first\nand last names. This serves as a fallback when no image is available or when the\nimage fails to load:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\">\n <Avatar\n firstName=\"Maria\"\n lastName=\"Garcia\"\n src=\"https://www.gravatar.com/avatar/thisWill404?s=200&d=404\" />\n <Avatar\n firstName=\"Chen\"\n lastName=\"Wei\"\n src=\"https://www.gravatar.com/avatar/thisWill404?s=200&d=404\" />\n <Avatar\n firstName=\"Aisha\"\n lastName=\"Patel\"\n src=\"https://www.gravatar.com/avatar/thisWill404?s=200&d=404\" />\n </Stack>\n)\n```\n\n## Component requirements\n\n## Accessibility\n\nThe Avatar component handles most accessibility requirements internally,\nincluding automatic labeling with the user's full name.\n\n- **Labeling**: The component automatically generates an internationalized\n accessible label from the `firstName` and `lastName` props. When providing an\n image via the `src` prop, always include an `alt` attribute for screen\n readers.\n- **Role**: Renders as a `<figure>` element with an `aria-label` that includes\n the full name (e.g., \"Avatar image for John Doe\").\n\nIf your use case requires tracking and analytics for this component, it is good\npractice to add a **persistent**, **unique** id to the component:\n\n```tsx\nconst PERSISTENT_ID = \"example-avatar\";\n\nexport const Example = () => (\n <Avatar id={PERSISTENT_ID} firstName=\"John\" lastName=\"Doe\" />\n);\n```\n\n#### Keyboard navigation\n\nAvatars are non-interactive elements and do not receive focus or support\nkeyboard interaction by default. If you need to make an avatar interactive\n(e.g., clickable), wrap it in a button or interactive element:\n\n```tsx\n<button onClick={handleClick} aria-label=\"View profile\">\n <Avatar firstName=\"John\" lastName=\"Doe\" />\n</button>\n```\n\n## API reference\n\n<PropsTable id=\"Avatar\" />\n\n## Common patterns\n\n### User profile header\n\nCombine Avatar with text components to create a user profile header:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Avatar\n firstName=\"Sarah\"\n lastName=\"Johnson\"\n src=\"https://i.pravatar.cc/150?img=5\"\n size=\"md\"\n />\n <Stack direction=\"column\" gap=\"100\">\n <Text fontSize=\"md\" fontWeight=\"600\">Sarah Johnson</Text>\n <Text fontSize=\"sm\" color=\"neutral.11\">Product Manager</Text>\n </Stack>\n </Stack>\n)\n```\n\n### Comment or message list\n\nDisplay avatars alongside user content in comments or messages:\n\n```jsx live-dev\nconst App = () => {\n const comments = [\n { id: 1, author: { firstName: \"Alex\", lastName: \"Chen\" }, text: \"Great work on this feature!\", time: \"2 hours ago\" },\n { id: 2, author: { firstName: \"Maria\", lastName: \"Garcia\" }, text: \"I agree, this is really helpful.\", time: \"1 hour ago\" },\n { id: 3, author: { firstName: \"James\", lastName: \"Wilson\" }, text: \"Looking forward to the next update.\", time: \"30 minutes ago\" },\n ];\n\n return (\n <Stack direction=\"column\" gap=\"600\">\n {comments.map((comment) => (\n <Stack key={comment.id} direction=\"row\" gap=\"400\">\n <Avatar\n firstName={comment.author.firstName}\n lastName={comment.author.lastName}\n size=\"xs\"\n />\n <Stack direction=\"column\" gap=\"100\" flex=\"1\">\n <Stack direction=\"row\" gap=\"200\" alignItems=\"baseline\">\n <Text fontSize=\"sm\" fontWeight=\"600\">\n {comment.author.firstName} {comment.author.lastName}\n </Text>\n <Text fontSize=\"xs\" color=\"neutral.11\">{comment.time}</Text>\n </Stack>\n <Text fontSize=\"sm\">{comment.text}</Text>\n </Stack>\n </Stack>\n ))}\n </Stack>\n );\n}\n```\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using Avatar\nwithin your application. As the component's internal functionality is already\ntested by Nimbus, these patterns help you verify your integration and\napplication-specific logic.\n\n### Basic rendering tests\n\nVerify the Avatar renders with expected elements and labels\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport { Avatar, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Avatar - Basic rendering\", () => {\n it(\"renders avatar with initials\", () => {\n render(\n <NimbusProvider>\n <Avatar firstName=\"John\" lastName=\"Doe\" />\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"figure\")).toBeInTheDocument();\n expect(screen.getByLabelText(/John Doe/i)).toBeInTheDocument();\n expect(screen.getByText(\"JD\")).toBeInTheDocument();\n });\n\n it(\"renders avatar with image\", () => {\n render(\n <NimbusProvider>\n <Avatar\n firstName=\"Jane\"\n lastName=\"Smith\"\n src=\"https://example.com/avatar.jpg\"\n alt=\"Jane Smith profile\"\n />\n </NimbusProvider>\n );\n\n const image = screen.getByAltText(\"Jane Smith profile\");\n expect(image).toBeInTheDocument();\n expect(image).toHaveAttribute(\"src\", \"https://example.com/avatar.jpg\");\n });\n});\n```\n\n### Size variant tests\n\nTest different size options\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport { Avatar, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Avatar - Size variants\", () => {\n it(\"renders different sizes correctly\", () => {\n const { rerender } = render(\n <NimbusProvider>\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"2xs\" />\n </NimbusProvider>\n );\n\n let avatar = screen.getByRole(\"figure\");\n expect(avatar).toBeInTheDocument();\n\n rerender(\n <NimbusProvider>\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"xs\" />\n </NimbusProvider>\n );\n\n avatar = screen.getByRole(\"figure\");\n expect(avatar).toBeInTheDocument();\n\n rerender(\n <NimbusProvider>\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"md\" />\n </NimbusProvider>\n );\n\n avatar = screen.getByRole(\"figure\");\n expect(avatar).toBeInTheDocument();\n });\n});\n```\n\n### Accessibility tests\n\nVerify accessibility attributes and labeling\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport { Avatar, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Avatar - Accessibility\", () => {\n it(\"has correct aria-label with full name\", () => {\n render(\n <NimbusProvider>\n <Avatar firstName=\"Maria\" lastName=\"Garcia\" />\n </NimbusProvider>\n );\n\n const avatar = screen.getByLabelText(/Maria Garcia/i);\n expect(avatar).toBeInTheDocument();\n expect(avatar.tagName).toBe(\"FIGURE\");\n });\n\n it(\"applies custom id for tracking\", () => {\n const PERSISTENT_ID = \"test-avatar-id\";\n\n render(\n <NimbusProvider>\n <Avatar id={PERSISTENT_ID} firstName=\"John\" lastName=\"Doe\" />\n </NimbusProvider>\n );\n\n const avatar = screen.getByRole(\"figure\");\n expect(avatar).toHaveAttribute(\"id\", PERSISTENT_ID);\n });\n});\n```\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-avatar--docs)\n",
|
|
209
|
+
"mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { Avatar, type AvatarProps } from \"@commercetools/nimbus\";\n```\n\n### Basic usage\n\nThe simplest implementation displays an avatar with initials derived from first\nand last names:\n\n```jsx live-dev\nconst App = () => (\n <Avatar firstName=\"John\" lastName=\"Doe\" />\n)\n```\n\n## Usage examples\n\n### Size options\n\nThe `2xs`, `xs`, and `md` size variants are available to match your interface\ndensity:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"2xs\" />\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"xs\" />\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"md\" />\n </Stack>\n)\n```\n\n### Color palettes\n\nAvatars support different color palettes to convey semantic meaning or match\nyour design theme:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Avatar firstName=\"John\" lastName=\"Doe\" colorPalette=\"primary\" />\n <Avatar firstName=\"Jane\" lastName=\"Smith\" colorPalette=\"positive\" />\n <Avatar firstName=\"Alex\" lastName=\"Chen\" colorPalette=\"info\" />\n <Avatar firstName=\"Maria\" lastName=\"Garcia\" colorPalette=\"critical\" />\n <Avatar firstName=\"Sam\" lastName=\"Wilson\" colorPalette=\"cyan\" />\n </Stack>\n)\n```\n\n### With image\n\nWhen a `src` prop is provided, the Avatar displays the image. If the image fails\nto load, it automatically falls back to initials:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Avatar\n firstName=\"Jane\"\n lastName=\"Smith\"\n src=\"https://i.pravatar.cc/150?img=1\"\n alt=\"Jane Smith's profile picture\"\n />\n <Avatar\n firstName=\"Alex\"\n lastName=\"Johnson\"\n src=\"https://i.pravatar.cc/150?img=2\"\n alt=\"Alex Johnson's profile picture\"\n />\n </Stack>\n)\n```\n\n### Initials fallback\n\nThe Avatar automatically extracts and displays initials from the provided first\nand last names. This serves as a fallback when no image is available or when the\nimage fails to load:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\">\n <Avatar\n firstName=\"Maria\"\n lastName=\"Garcia\"\n src=\"https://www.gravatar.com/avatar/thisWill404?s=200&d=404\" />\n <Avatar\n firstName=\"Chen\"\n lastName=\"Wei\"\n src=\"https://www.gravatar.com/avatar/thisWill404?s=200&d=404\" />\n <Avatar\n firstName=\"Aisha\"\n lastName=\"Patel\"\n src=\"https://www.gravatar.com/avatar/thisWill404?s=200&d=404\" />\n </Stack>\n)\n```\n\n### Missing or partial names\n\n`firstName` and `lastName` are both **optional**. The Avatar handles missing,\nempty, and whitespace-only values defensively:\n\n- If only one name is usable (after trimming whitespace), a single initial\n is rendered.\n- If neither name yields a usable character, a generic `Person` icon is\n rendered as the fallback and a localized generic `aria-label`\n (`\"User avatar\"`) is applied.\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Avatar firstName=\"John\" /> {/* renders \"J\" */}\n <Avatar lastName=\"Doe\" /> {/* renders \"D\" */}\n <Avatar firstName=\"\" lastName=\"\" /> {/* renders the Person icon */}\n <Avatar /> {/* renders the Person icon */}\n </Stack>\n)\n```\n\nThis is intended for cases where user records have incomplete name data —\ncommon in legacy systems and partial profiles. The component's TypeScript\ncontract permits `undefined` so call sites do not need non-null assertions\nor empty-string fallbacks.\n\nInitials extraction is also Unicode codepoint-safe (emoji and astral-plane\ncharacters are not split mid-surrogate) and trim-aware (leading/trailing\nwhitespace is discarded before extracting the first character).\n\n## Component requirements\n\n## Accessibility\n\nThe Avatar component handles most accessibility requirements internally,\nincluding automatic labeling with the user's full name.\n\n- **Labeling**: The component automatically generates an internationalized\n accessible label from the `firstName` and `lastName` props. When providing an\n image via the `src` prop, always include an `alt` attribute for screen\n readers.\n- **Role**: Renders as a `<figure>` element with an `aria-label` that includes\n the full name (e.g., \"Avatar image for John Doe\").\n\nIf your use case requires tracking and analytics for this component, it is good\npractice to add a **persistent**, **unique** id to the component:\n\n```tsx\nconst PERSISTENT_ID = \"example-avatar\";\n\nexport const Example = () => (\n <Avatar id={PERSISTENT_ID} firstName=\"John\" lastName=\"Doe\" />\n);\n```\n\n#### Keyboard navigation\n\nAvatars are non-interactive elements and do not receive focus or support\nkeyboard interaction by default. If you need to make an avatar interactive\n(e.g., clickable), wrap it in a button or interactive element:\n\n```tsx\n<button onClick={handleClick} aria-label=\"View profile\">\n <Avatar firstName=\"John\" lastName=\"Doe\" />\n</button>\n```\n\n## API reference\n\n<PropsTable id=\"Avatar\" />\n\n## Common patterns\n\n### User profile header\n\nCombine Avatar with text components to create a user profile header:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Avatar\n firstName=\"Sarah\"\n lastName=\"Johnson\"\n src=\"https://i.pravatar.cc/150?img=5\"\n size=\"md\"\n />\n <Stack direction=\"column\" gap=\"100\">\n <Text fontSize=\"md\" fontWeight=\"600\">Sarah Johnson</Text>\n <Text fontSize=\"sm\" color=\"neutral.11\">Product Manager</Text>\n </Stack>\n </Stack>\n)\n```\n\n### Comment or message list\n\nDisplay avatars alongside user content in comments or messages:\n\n```jsx live-dev\nconst App = () => {\n const comments = [\n { id: 1, author: { firstName: \"Alex\", lastName: \"Chen\" }, text: \"Great work on this feature!\", time: \"2 hours ago\" },\n { id: 2, author: { firstName: \"Maria\", lastName: \"Garcia\" }, text: \"I agree, this is really helpful.\", time: \"1 hour ago\" },\n { id: 3, author: { firstName: \"James\", lastName: \"Wilson\" }, text: \"Looking forward to the next update.\", time: \"30 minutes ago\" },\n ];\n\n return (\n <Stack direction=\"column\" gap=\"600\">\n {comments.map((comment) => (\n <Stack key={comment.id} direction=\"row\" gap=\"400\">\n <Avatar\n firstName={comment.author.firstName}\n lastName={comment.author.lastName}\n size=\"xs\"\n />\n <Stack direction=\"column\" gap=\"100\" flex=\"1\">\n <Stack direction=\"row\" gap=\"200\" alignItems=\"baseline\">\n <Text fontSize=\"sm\" fontWeight=\"600\">\n {comment.author.firstName} {comment.author.lastName}\n </Text>\n <Text fontSize=\"xs\" color=\"neutral.11\">{comment.time}</Text>\n </Stack>\n <Text fontSize=\"sm\">{comment.text}</Text>\n </Stack>\n </Stack>\n ))}\n </Stack>\n );\n}\n```\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using Avatar\nwithin your application. As the component's internal functionality is already\ntested by Nimbus, these patterns help you verify your integration and\napplication-specific logic.\n\n### Basic rendering tests\n\nVerify the Avatar renders with expected elements and labels\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport { Avatar, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Avatar - Basic rendering\", () => {\n it(\"renders avatar with initials\", () => {\n render(\n <NimbusProvider>\n <Avatar firstName=\"John\" lastName=\"Doe\" />\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"figure\")).toBeInTheDocument();\n expect(screen.getByLabelText(/John Doe/i)).toBeInTheDocument();\n expect(screen.getByText(\"JD\")).toBeInTheDocument();\n });\n\n it(\"renders avatar with image\", () => {\n render(\n <NimbusProvider>\n <Avatar\n firstName=\"Jane\"\n lastName=\"Smith\"\n src=\"https://example.com/avatar.jpg\"\n alt=\"Jane Smith profile\"\n />\n </NimbusProvider>\n );\n\n const image = screen.getByAltText(\"Jane Smith profile\");\n expect(image).toBeInTheDocument();\n expect(image).toHaveAttribute(\"src\", \"https://example.com/avatar.jpg\");\n });\n});\n```\n\n### Size variant tests\n\nTest different size options\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport { Avatar, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Avatar - Size variants\", () => {\n it(\"renders different sizes correctly\", () => {\n const { rerender } = render(\n <NimbusProvider>\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"2xs\" />\n </NimbusProvider>\n );\n\n let avatar = screen.getByRole(\"figure\");\n expect(avatar).toBeInTheDocument();\n\n rerender(\n <NimbusProvider>\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"xs\" />\n </NimbusProvider>\n );\n\n avatar = screen.getByRole(\"figure\");\n expect(avatar).toBeInTheDocument();\n\n rerender(\n <NimbusProvider>\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"md\" />\n </NimbusProvider>\n );\n\n avatar = screen.getByRole(\"figure\");\n expect(avatar).toBeInTheDocument();\n });\n});\n```\n\n### Accessibility tests\n\nVerify accessibility attributes and labeling\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport { Avatar, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Avatar - Accessibility\", () => {\n it(\"has correct aria-label with full name\", () => {\n render(\n <NimbusProvider>\n <Avatar firstName=\"Maria\" lastName=\"Garcia\" />\n </NimbusProvider>\n );\n\n const avatar = screen.getByLabelText(/Maria Garcia/i);\n expect(avatar).toBeInTheDocument();\n expect(avatar.tagName).toBe(\"FIGURE\");\n });\n\n it(\"applies custom id for tracking\", () => {\n const PERSISTENT_ID = \"test-avatar-id\";\n\n render(\n <NimbusProvider>\n <Avatar id={PERSISTENT_ID} firstName=\"John\" lastName=\"Doe\" />\n </NimbusProvider>\n );\n\n const avatar = screen.getByRole(\"figure\");\n expect(avatar).toHaveAttribute(\"id\", PERSISTENT_ID);\n });\n});\n```\n\n### Missing-name fallback tests\n\nVerify the Avatar renders a generic icon and label when\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport { Avatar, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Avatar - Missing names\", () => {\n it(\"renders the Person icon and generic aria-label when both names are missing\", () => {\n render(\n <NimbusProvider>\n <Avatar />\n </NimbusProvider>\n );\n\n const avatar = screen.getByRole(\"figure\");\n // Generic localized aria-label (\"Generic user avatar\" in English)\n expect(avatar).toHaveAttribute(\"aria-label\", \"Generic user avatar\");\n // Person icon is rendered as the visual fallback\n expect(avatar.querySelector(\"svg\")).not.toBeNull();\n // No initials text is rendered\n expect(avatar.textContent?.trim()).toBe(\"\");\n });\n\n it(\"renders a single initial when only firstName is provided\", () => {\n render(\n <NimbusProvider>\n <Avatar firstName=\"John\" />\n </NimbusProvider>\n );\n\n const avatar = screen.getByRole(\"figure\");\n expect(avatar.textContent?.trim()).toBe(\"J\");\n expect(avatar.querySelector(\"svg\")).toBeNull();\n });\n});\n```\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-avatar--docs)\n",
|
|
210
210
|
"toc": [
|
|
211
211
|
{
|
|
212
212
|
"value": "Getting started",
|
|
@@ -294,6 +294,17 @@
|
|
|
294
294
|
],
|
|
295
295
|
"parent": "root"
|
|
296
296
|
},
|
|
297
|
+
{
|
|
298
|
+
"value": "Missing or partial names",
|
|
299
|
+
"href": "#missing-or-partial-names",
|
|
300
|
+
"depth": 3,
|
|
301
|
+
"numbering": [
|
|
302
|
+
1,
|
|
303
|
+
2,
|
|
304
|
+
5
|
|
305
|
+
],
|
|
306
|
+
"parent": "root"
|
|
307
|
+
},
|
|
297
308
|
{
|
|
298
309
|
"value": "Component requirements",
|
|
299
310
|
"href": "#component-requirements",
|
|
@@ -411,6 +422,17 @@
|
|
|
411
422
|
],
|
|
412
423
|
"parent": "root"
|
|
413
424
|
},
|
|
425
|
+
{
|
|
426
|
+
"value": "Missing-name fallback tests",
|
|
427
|
+
"href": "#missing-name-fallback-tests",
|
|
428
|
+
"depth": 3,
|
|
429
|
+
"numbering": [
|
|
430
|
+
1,
|
|
431
|
+
7,
|
|
432
|
+
4
|
|
433
|
+
],
|
|
434
|
+
"parent": "root"
|
|
435
|
+
},
|
|
414
436
|
{
|
|
415
437
|
"value": "Resources",
|
|
416
438
|
"href": "#resources",
|