@commercetools/nimbus-mcp 3.5.0 → 3.6.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 +275 -9
- package/data/docs/routes/components-buttons-toggle-button-group.json +143 -18
- package/data/docs/routes/components-buttons-toggle-button.json +72 -15
- package/data/docs/routes/components-chat.json +36 -0
- package/data/docs/routes/components-content.json +36 -0
- package/data/docs/routes/components-data-display-data-table.json +1 -1
- package/data/docs/routes/components-feedback-popover.json +643 -0
- package/data/docs/routes/components-inputs-combo-box.json +33 -11
- package/data/docs/routes/components-inputs-search-input.json +29 -7
- package/data/docs/routes/components-inputs-select-input.json +23 -1
- package/data/docs/routes/components-layout-stack.json +1 -1
- package/data/docs/routes/components-navigation-toolbar.json +1 -1
- package/data/docs/routes/components-navigation-tree.json +2 -2
- package/data/docs/routes/home-contribute.json +29 -8
- package/data/docs/routes/home-getting-started-collaboration.json +657 -0
- package/data/docs/search-index.json +1 -1
- package/data/docs/types/AccordionContent.json +12 -12
- package/data/docs/types/AccordionHeader.json +46 -46
- package/data/docs/types/AccordionItem.json +20 -20
- package/data/docs/types/AccordionRoot.json +12 -12
- package/data/docs/types/ActivityIndicator.json +3 -2
- package/data/docs/types/AlertDismissButton.json +42 -42
- package/data/docs/types/Button.json +47 -46
- package/data/docs/types/Calendar.json +43 -42
- package/data/docs/types/Checkbox.json +30 -29
- package/data/docs/types/Code.json +2 -1
- package/data/docs/types/ComboBoxListBox.json +34 -34
- package/data/docs/types/ComboBoxOption.json +20 -20
- package/data/docs/types/ComboBoxPopover.json +67 -86
- package/data/docs/types/ComboBoxRoot.json +15 -0
- package/data/docs/types/ComboBoxSection.json +14 -14
- package/data/docs/types/DataTableBody.json +10 -10
- package/data/docs/types/DataTableCell.json +18 -18
- package/data/docs/types/DataTableColumn.json +28 -28
- package/data/docs/types/DataTableHeader.json +10 -10
- package/data/docs/types/DataTableManager.json +3 -5
- package/data/docs/types/DataTableRow.json +24 -24
- package/data/docs/types/DateInput.json +33 -32
- package/data/docs/types/DatePicker.json +32 -32
- package/data/docs/types/DateRangePicker.json +46 -46
- package/data/docs/types/DateRangePickerField.json +46 -46
- package/data/docs/types/DialogCloseTrigger.json +42 -42
- package/data/docs/types/DraggableListField.json +28 -28
- package/data/docs/types/DraggableListItem.json +18 -18
- package/data/docs/types/DraggableListRoot.json +30 -30
- package/data/docs/types/DrawerCloseTrigger.json +42 -42
- package/data/docs/types/DropZone.json +14 -14
- package/data/docs/types/FloatingActionButton.json +44 -44
- package/data/docs/types/Group.json +9 -7
- package/data/docs/types/Heading.json +3 -2
- package/data/docs/types/Icon.json +3 -2
- package/data/docs/types/IconButton.json +47 -46
- package/data/docs/types/IconToggleButton.json +71 -35
- package/data/docs/types/Link.json +3 -2
- package/data/docs/types/LoadingSpinner.json +3 -2
- package/data/docs/types/LocalizedField.json +3 -2
- package/data/docs/types/MakeElementFocusable.json +2 -2
- package/data/docs/types/MenuItem.json +22 -22
- package/data/docs/types/MenuRoot.json +31 -31
- package/data/docs/types/MenuSection.json +16 -16
- package/data/docs/types/MenuSubmenuTrigger.json +4 -4
- package/data/docs/types/MenuTrigger.json +46 -46
- package/data/docs/types/MoneyInput.json +4 -2
- package/data/docs/types/MultilineTextInput.json +27 -26
- package/data/docs/types/MultilineTextInputField.json +27 -26
- package/data/docs/types/NumberInput.json +14 -12
- package/data/docs/types/NumberInputField.json +10 -10
- package/data/docs/types/PasswordInput.json +24 -24
- package/data/docs/types/PasswordInputField.json +24 -24
- package/data/docs/types/Popover.json +13 -0
- package/data/docs/types/PopoverContent.json +42 -0
- package/data/docs/types/PopoverRoot.json +651 -0
- package/data/docs/types/PopoverTrigger.json +914 -0
- package/data/docs/types/ProgressBar.json +17 -15
- package/data/docs/types/RadioInputOption.json +17 -17
- package/data/docs/types/RadioInputRoot.json +10 -10
- package/data/docs/types/RangeCalendar.json +42 -42
- package/data/docs/types/RangeSlider.json +14 -14
- package/data/docs/types/Region.json +3 -2
- package/data/docs/types/RichTextInput.json +4 -2
- package/data/docs/types/SearchInput.json +60 -28
- package/data/docs/types/SearchInputField.json +60 -28
- package/data/docs/types/SelectOption.json +20 -20
- package/data/docs/types/SelectOptionGroup.json +14 -14
- package/data/docs/types/SelectOptions.json +34 -34
- package/data/docs/types/SelectRoot.json +67 -52
- package/data/docs/types/Separator.json +4 -2
- package/data/docs/types/Slider.json +18 -16
- package/data/docs/types/SplitButton.json +12 -10
- package/data/docs/types/Switch.json +11 -10
- package/data/docs/types/TagGroupRoot.json +12 -12
- package/data/docs/types/TagGroupTag.json +16 -16
- package/data/docs/types/TagGroupTagList.json +12 -12
- package/data/docs/types/TextInput.json +27 -26
- package/data/docs/types/TextInputField.json +27 -26
- package/data/docs/types/TimeInput.json +30 -28
- package/data/docs/types/ToastOutlet.json +4 -2
- package/data/docs/types/ToggleButton.json +74 -37
- package/data/docs/types/ToggleButtonGroup.json +4 -2
- package/data/docs/types/ToggleButtonGroupButton.json +547 -55
- package/data/docs/types/ToggleButtonGroupRoot.json +48 -16
- package/data/docs/types/Toolbar.json +11 -9
- package/data/docs/types/Tooltip.json +3 -2
- package/data/docs/types/TooltipContent.json +26 -26
- package/data/docs/types/TooltipRoot.json +16 -16
- package/data/docs/types/TreeIndicator.json +40 -40
- package/data/docs/types/TreeItem.json +20 -20
- package/data/docs/types/TreeRoot.json +26 -26
- package/data/docs/types/VisuallyHidden.json +4 -4
- package/data/docs/types/createItemsFromCsvDrop.json +2 -2
- package/data/docs/types/createItemsFromDirectoryDrop.json +2 -2
- package/data/docs/types/createItemsFromFileDrop.json +2 -2
- package/data/docs/types/createItemsFromImageDrop.json +2 -2
- package/data/docs/types/createItemsFromJsonDrop.json +2 -2
- package/data/docs/types/manifest.json +4 -0
- package/data/docs/types/useTree.json +8 -8
- package/data/style-props-summary.json +408 -0
- package/dist/index.js +156 -43
- package/package.json +8 -8
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"id": "Components-Popover",
|
|
4
|
+
"title": "Popover",
|
|
5
|
+
"exportName": "Popover",
|
|
6
|
+
"description": "A positioned overlay for rich, interactive content anchored to a trigger. Built with React Aria Components for accessibility and WCAG 2.1 AA compliance.",
|
|
7
|
+
"lifecycleState": "Beta",
|
|
8
|
+
"order": 999,
|
|
9
|
+
"repoPath": "packages/nimbus/src/components/popover/popover.mdx",
|
|
10
|
+
"menu": [
|
|
11
|
+
"Components",
|
|
12
|
+
"Feedback",
|
|
13
|
+
"Popover"
|
|
14
|
+
],
|
|
15
|
+
"route": "components/feedback/popover",
|
|
16
|
+
"tags": [
|
|
17
|
+
"component",
|
|
18
|
+
"overlay",
|
|
19
|
+
"popover",
|
|
20
|
+
"dialog",
|
|
21
|
+
"interactive"
|
|
22
|
+
],
|
|
23
|
+
"toc": [
|
|
24
|
+
{
|
|
25
|
+
"value": "Overview",
|
|
26
|
+
"href": "#overview",
|
|
27
|
+
"depth": 2,
|
|
28
|
+
"numbering": [
|
|
29
|
+
1,
|
|
30
|
+
1
|
|
31
|
+
],
|
|
32
|
+
"parent": "root"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"value": "Resources",
|
|
36
|
+
"href": "#resources",
|
|
37
|
+
"depth": 3,
|
|
38
|
+
"numbering": [
|
|
39
|
+
1,
|
|
40
|
+
1,
|
|
41
|
+
1
|
|
42
|
+
],
|
|
43
|
+
"parent": "root"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"value": "Variables",
|
|
47
|
+
"href": "#variables",
|
|
48
|
+
"depth": 2,
|
|
49
|
+
"numbering": [
|
|
50
|
+
1,
|
|
51
|
+
2
|
|
52
|
+
],
|
|
53
|
+
"parent": "root"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"value": "Basic usage",
|
|
57
|
+
"href": "#basic-usage",
|
|
58
|
+
"depth": 3,
|
|
59
|
+
"numbering": [
|
|
60
|
+
1,
|
|
61
|
+
2,
|
|
62
|
+
1
|
|
63
|
+
],
|
|
64
|
+
"parent": "root"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"value": "Interactive content",
|
|
68
|
+
"href": "#interactive-content",
|
|
69
|
+
"depth": 3,
|
|
70
|
+
"numbering": [
|
|
71
|
+
1,
|
|
72
|
+
2,
|
|
73
|
+
2
|
|
74
|
+
],
|
|
75
|
+
"parent": "root"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"value": "Placement",
|
|
79
|
+
"href": "#placement",
|
|
80
|
+
"depth": 3,
|
|
81
|
+
"numbering": [
|
|
82
|
+
1,
|
|
83
|
+
2,
|
|
84
|
+
3
|
|
85
|
+
],
|
|
86
|
+
"parent": "root"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"value": "Spacing",
|
|
90
|
+
"href": "#spacing",
|
|
91
|
+
"depth": 3,
|
|
92
|
+
"numbering": [
|
|
93
|
+
1,
|
|
94
|
+
2,
|
|
95
|
+
4
|
|
96
|
+
],
|
|
97
|
+
"parent": "root"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"value": "Width",
|
|
101
|
+
"href": "#width",
|
|
102
|
+
"depth": 3,
|
|
103
|
+
"numbering": [
|
|
104
|
+
1,
|
|
105
|
+
2,
|
|
106
|
+
5
|
|
107
|
+
],
|
|
108
|
+
"parent": "root"
|
|
109
|
+
}
|
|
110
|
+
],
|
|
111
|
+
"layout": "app-frame",
|
|
112
|
+
"tabs": [
|
|
113
|
+
{
|
|
114
|
+
"key": "overview",
|
|
115
|
+
"title": "Overview",
|
|
116
|
+
"order": 0
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"key": "guidelines",
|
|
120
|
+
"title": "Guidelines",
|
|
121
|
+
"order": 2
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"key": "dev",
|
|
125
|
+
"title": "Implementation",
|
|
126
|
+
"order": 3
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"key": "a11y",
|
|
130
|
+
"title": "Accessibility",
|
|
131
|
+
"order": 4
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
"mdx": "\n## Overview\n\nPopover shows interactive content in a floating surface anchored to the element\nthat opened it. It sits between Tooltip and Dialog: its content can be focused\nand operated, unlike a tooltip, but it stays tethered to its trigger instead of\ntaking over the page.\n\nVisually it is a single elevated surface — a background, a corner radius and a\nshadow — with no header, footer or arrow. That plainness is deliberate: the\ncontent is authored by the consumer, so the component supplies elevation and\npositioning and stays out of the way.\n\nReach for it when the content is genuinely interactive: a filter panel, a short\nedit form, a set of context-sensitive actions.\n\nUse `Tooltip` instead when the hint is short and non-interactive, and `Dialog`\nor `Drawer` when the task should take over the page. The Guidelines tab compares\nall four side by side.\n\n### Resources\n\n[ARIA dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)\n[React Aria Popover](https://react-spectrum.adobe.com/react-aria/Popover.html)\n\n## Variables\n\nPopover has no size or color variants. One surface treatment covers every use,\nbecause the content decides how much room it needs. What varies is where the\nsurface sits relative to its trigger, and how much space it takes.\n\n### Basic usage\n\nThree parts: a `Root` that owns open state, a `Trigger`, and a `Content` surface.\n\n```jsx live\nconst App = () => (\n <Popover.Root>\n <Popover.Trigger>Open popover</Popover.Trigger>\n <Popover.Content aria-label=\"Example popover\">\n <Stack gap=\"200\">\n <Text fontWeight=\"700\">Popover title</Text>\n <Text>Rich, interactive content lives here.</Text>\n </Stack>\n </Popover.Content>\n </Popover.Root>\n);\n```\n\n### Interactive content\n\nInputs and buttons inside the popover behave normally. Typing does not dismiss\nit.\n\n```jsx live\nconst App = () => (\n <Popover.Root>\n <Popover.Trigger>Edit name</Popover.Trigger>\n <Popover.Content aria-label=\"Edit name\">\n <Stack gap=\"200\">\n <TextInput aria-label=\"Name\" />\n <Button>Save</Button>\n </Stack>\n </Popover.Content>\n </Popover.Root>\n);\n```\n\n### Placement\n\n`placement` takes a side, optionally followed by an alignment. Sides may be\nphysical (`top`, `bottom`, `left`, `right`) or logical (`start`, `end`, which\nfollow the writing direction). The alignment vocabulary depends on the axis:\nvertical sides take `left`, `right`, `start` or `end`, while horizontal and\nlogical sides take `top` or `bottom`. The default is `bottom`.\n\nPlacement is a preference, not a guarantee — the popover flips and shifts to stay\ninside the viewport.\n\n```jsx live\nconst App = () => (\n <Stack direction=\"horizontal\" gap=\"400\">\n <Popover.Root placement=\"bottom start\">\n <Popover.Trigger>Bottom start</Popover.Trigger>\n <Popover.Content aria-label=\"Bottom start\">\n <Text>placement=\"bottom start\"</Text>\n </Popover.Content>\n </Popover.Root>\n <Popover.Root placement=\"right top\">\n <Popover.Trigger>Right top</Popover.Trigger>\n <Popover.Content aria-label=\"Right top\">\n <Text>placement=\"right top\"</Text>\n </Popover.Content>\n </Popover.Root>\n <Popover.Root placement=\"top\">\n <Popover.Trigger>Top</Popover.Trigger>\n <Popover.Content aria-label=\"Top\">\n <Text>placement=\"top\"</Text>\n </Popover.Content>\n </Popover.Root>\n </Stack>\n);\n```\n\n### Spacing\n\n`offset` moves the surface along the trigger's axis; `crossOffset` moves it\nalong the perpendicular one. Both take pixels. Use them to fine-tune the gap,\nnot to fake an alignment that `placement` already expresses.\n\n```jsx live\nconst App = () => (\n <Stack direction=\"horizontal\" gap=\"400\">\n <Popover.Root>\n <Popover.Trigger>Default gap</Popover.Trigger>\n <Popover.Content aria-label=\"Default gap\">\n <Text>{\"Default offset (8)\"}</Text>\n </Popover.Content>\n </Popover.Root>\n <Popover.Root offset={16}>\n <Popover.Trigger>Wider gap</Popover.Trigger>\n <Popover.Content aria-label=\"Wider gap\">\n <Text>{\"offset={16}\"}</Text>\n </Popover.Content>\n </Popover.Root>\n </Stack>\n);\n```\n\n### Width\n\nContent sizes to itself. Constrain it with the standard style props when the\ncontent would otherwise stretch too wide to read.\n\n```jsx live\nconst App = () => (\n <Popover.Root>\n <Popover.Trigger>Constrained width</Popover.Trigger>\n <Popover.Content maxWidth=\"4800\" aria-label=\"Constrained width\">\n <Text>\n A longer passage wraps instead of stretching into one unreadable line.\n </Text>\n </Popover.Content>\n </Popover.Root>\n);\n```\n",
|
|
136
|
+
"views": {
|
|
137
|
+
"overview": {
|
|
138
|
+
"mdx": "\n## Overview\n\nPopover shows interactive content in a floating surface anchored to the element\nthat opened it. It sits between Tooltip and Dialog: its content can be focused\nand operated, unlike a tooltip, but it stays tethered to its trigger instead of\ntaking over the page.\n\nVisually it is a single elevated surface — a background, a corner radius and a\nshadow — with no header, footer or arrow. That plainness is deliberate: the\ncontent is authored by the consumer, so the component supplies elevation and\npositioning and stays out of the way.\n\nReach for it when the content is genuinely interactive: a filter panel, a short\nedit form, a set of context-sensitive actions.\n\nUse `Tooltip` instead when the hint is short and non-interactive, and `Dialog`\nor `Drawer` when the task should take over the page. The Guidelines tab compares\nall four side by side.\n\n### Resources\n\n[ARIA dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)\n[React Aria Popover](https://react-spectrum.adobe.com/react-aria/Popover.html)\n\n## Variables\n\nPopover has no size or color variants. One surface treatment covers every use,\nbecause the content decides how much room it needs. What varies is where the\nsurface sits relative to its trigger, and how much space it takes.\n\n### Basic usage\n\nThree parts: a `Root` that owns open state, a `Trigger`, and a `Content` surface.\n\n```jsx live\nconst App = () => (\n <Popover.Root>\n <Popover.Trigger>Open popover</Popover.Trigger>\n <Popover.Content aria-label=\"Example popover\">\n <Stack gap=\"200\">\n <Text fontWeight=\"700\">Popover title</Text>\n <Text>Rich, interactive content lives here.</Text>\n </Stack>\n </Popover.Content>\n </Popover.Root>\n);\n```\n\n### Interactive content\n\nInputs and buttons inside the popover behave normally. Typing does not dismiss\nit.\n\n```jsx live\nconst App = () => (\n <Popover.Root>\n <Popover.Trigger>Edit name</Popover.Trigger>\n <Popover.Content aria-label=\"Edit name\">\n <Stack gap=\"200\">\n <TextInput aria-label=\"Name\" />\n <Button>Save</Button>\n </Stack>\n </Popover.Content>\n </Popover.Root>\n);\n```\n\n### Placement\n\n`placement` takes a side, optionally followed by an alignment. Sides may be\nphysical (`top`, `bottom`, `left`, `right`) or logical (`start`, `end`, which\nfollow the writing direction). The alignment vocabulary depends on the axis:\nvertical sides take `left`, `right`, `start` or `end`, while horizontal and\nlogical sides take `top` or `bottom`. The default is `bottom`.\n\nPlacement is a preference, not a guarantee — the popover flips and shifts to stay\ninside the viewport.\n\n```jsx live\nconst App = () => (\n <Stack direction=\"horizontal\" gap=\"400\">\n <Popover.Root placement=\"bottom start\">\n <Popover.Trigger>Bottom start</Popover.Trigger>\n <Popover.Content aria-label=\"Bottom start\">\n <Text>placement=\"bottom start\"</Text>\n </Popover.Content>\n </Popover.Root>\n <Popover.Root placement=\"right top\">\n <Popover.Trigger>Right top</Popover.Trigger>\n <Popover.Content aria-label=\"Right top\">\n <Text>placement=\"right top\"</Text>\n </Popover.Content>\n </Popover.Root>\n <Popover.Root placement=\"top\">\n <Popover.Trigger>Top</Popover.Trigger>\n <Popover.Content aria-label=\"Top\">\n <Text>placement=\"top\"</Text>\n </Popover.Content>\n </Popover.Root>\n </Stack>\n);\n```\n\n### Spacing\n\n`offset` moves the surface along the trigger's axis; `crossOffset` moves it\nalong the perpendicular one. Both take pixels. Use them to fine-tune the gap,\nnot to fake an alignment that `placement` already expresses.\n\n```jsx live\nconst App = () => (\n <Stack direction=\"horizontal\" gap=\"400\">\n <Popover.Root>\n <Popover.Trigger>Default gap</Popover.Trigger>\n <Popover.Content aria-label=\"Default gap\">\n <Text>{\"Default offset (8)\"}</Text>\n </Popover.Content>\n </Popover.Root>\n <Popover.Root offset={16}>\n <Popover.Trigger>Wider gap</Popover.Trigger>\n <Popover.Content aria-label=\"Wider gap\">\n <Text>{\"offset={16}\"}</Text>\n </Popover.Content>\n </Popover.Root>\n </Stack>\n);\n```\n\n### Width\n\nContent sizes to itself. Constrain it with the standard style props when the\ncontent would otherwise stretch too wide to read.\n\n```jsx live\nconst App = () => (\n <Popover.Root>\n <Popover.Trigger>Constrained width</Popover.Trigger>\n <Popover.Content maxWidth=\"4800\" aria-label=\"Constrained width\">\n <Text>\n A longer passage wraps instead of stretching into one unreadable line.\n </Text>\n </Popover.Content>\n </Popover.Root>\n);\n```\n",
|
|
139
|
+
"toc": [
|
|
140
|
+
{
|
|
141
|
+
"value": "Overview",
|
|
142
|
+
"href": "#overview",
|
|
143
|
+
"depth": 2,
|
|
144
|
+
"numbering": [
|
|
145
|
+
1,
|
|
146
|
+
1
|
|
147
|
+
],
|
|
148
|
+
"parent": "root"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"value": "Resources",
|
|
152
|
+
"href": "#resources",
|
|
153
|
+
"depth": 3,
|
|
154
|
+
"numbering": [
|
|
155
|
+
1,
|
|
156
|
+
1,
|
|
157
|
+
1
|
|
158
|
+
],
|
|
159
|
+
"parent": "root"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"value": "Variables",
|
|
163
|
+
"href": "#variables",
|
|
164
|
+
"depth": 2,
|
|
165
|
+
"numbering": [
|
|
166
|
+
1,
|
|
167
|
+
2
|
|
168
|
+
],
|
|
169
|
+
"parent": "root"
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"value": "Basic usage",
|
|
173
|
+
"href": "#basic-usage",
|
|
174
|
+
"depth": 3,
|
|
175
|
+
"numbering": [
|
|
176
|
+
1,
|
|
177
|
+
2,
|
|
178
|
+
1
|
|
179
|
+
],
|
|
180
|
+
"parent": "root"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"value": "Interactive content",
|
|
184
|
+
"href": "#interactive-content",
|
|
185
|
+
"depth": 3,
|
|
186
|
+
"numbering": [
|
|
187
|
+
1,
|
|
188
|
+
2,
|
|
189
|
+
2
|
|
190
|
+
],
|
|
191
|
+
"parent": "root"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"value": "Placement",
|
|
195
|
+
"href": "#placement",
|
|
196
|
+
"depth": 3,
|
|
197
|
+
"numbering": [
|
|
198
|
+
1,
|
|
199
|
+
2,
|
|
200
|
+
3
|
|
201
|
+
],
|
|
202
|
+
"parent": "root"
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"value": "Spacing",
|
|
206
|
+
"href": "#spacing",
|
|
207
|
+
"depth": 3,
|
|
208
|
+
"numbering": [
|
|
209
|
+
1,
|
|
210
|
+
2,
|
|
211
|
+
4
|
|
212
|
+
],
|
|
213
|
+
"parent": "root"
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"value": "Width",
|
|
217
|
+
"href": "#width",
|
|
218
|
+
"depth": 3,
|
|
219
|
+
"numbering": [
|
|
220
|
+
1,
|
|
221
|
+
2,
|
|
222
|
+
5
|
|
223
|
+
],
|
|
224
|
+
"parent": "root"
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
},
|
|
228
|
+
"a11y": {
|
|
229
|
+
"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 <Popover.Root>\n <Popover.Trigger>Open popover</Popover.Trigger>\n <Popover.Content aria-label=\"Accessible popover\">\n <Stack gap=\"200\">\n <Text>Focus moves here when the popover opens.</Text>\n <Button>Focusable action</Button>\n </Stack>\n </Popover.Content>\n </Popover.Root>\n);\n```\n\n### Give every popover a name\n\nThis is the one thing a consumer should supply. `Popover.Content` renders a\ndialog, and React Aria falls back to labelling that dialog with the **trigger's**\naccessible name when nothing else names it. That fallback is a floor, not a\nresult: an icon-only trigger announces the dialog as \"Options\", and a trigger\nwhose label describes an action (\"Edit\") names a surface that is not an action.\nNote that a visible heading does not name the dialog by itself — it has to be\nreferenced, as in the second example below.\n\nEither label it directly:\n\n```jsx live\nconst App = () => (\n <Popover.Root>\n <Popover.Trigger>Filters</Popover.Trigger>\n <Popover.Content aria-label=\"Filters\">\n <Checkbox>Only active</Checkbox>\n </Popover.Content>\n </Popover.Root>\n);\n```\n\nOr point at a heading inside it, which is preferable when the heading is visible\nanyway — the name then stays in sync with what is on screen:\n\n```jsx live\nconst App = () => (\n <Popover.Root>\n <Popover.Trigger>Filters</Popover.Trigger>\n <Popover.Content aria-labelledby=\"filters-heading\">\n <Stack gap=\"200\">\n <Heading id=\"filters-heading\" size=\"sm\">\n Filters\n </Heading>\n <Checkbox>Only active</Checkbox>\n </Stack>\n </Popover.Content>\n </Popover.Root>\n);\n```\n\n### Keyboard interaction\n\n| Key | Result |\n| --- | --- |\n| `Enter` or `Space` on the trigger | Toggles the popover |\n| `Tab` | Moves focus between focusable elements in the popover |\n| `Escape` | Closes the popover and returns focus to the trigger |\n\n### Focus behavior\n\nFocus moves into the popover's dialog when it opens, and returns to the trigger\nwhen it closes — whether it was closed by Escape, by an outside press, or by a\n`close` callback from inside. The one exception is a dismissing press that lands\non another focusable element: focus stays where the press put it rather than\njumping back to the trigger.\n\nBy default focus is contained within the popover and interaction outside\ndismisses it. This is React Aria's default for a dialog inside a popover, and it\nis the right behavior for almost every case: it keeps the interaction sequence\npredictable for screen reader and keyboard users.\n\n`isNonModal` does not lift that containment — focus still cycles inside the\ndialog and an outside press still dismisses it. React Aria's guidance is that\nmost popovers should not use it, because screen readers can announce the\nsurrounding page in ways that make the popover harder to follow; see the\nImplementation tab for what it does change.\n\n### ARIA attributes\n\nThe trigger exposes `aria-expanded` reflecting the open state, and\n`aria-controls` referencing the overlay while it is open. `Popover.Content`\nrenders an element with `role=\"dialog\"`.\n\nThe trigger does **not** carry `aria-haspopup`. React Aria deliberately sets it\nonly for `menu` and `listbox` overlays, because screen readers often announce\nother values as \"menu\" — which would be actively misleading here. We follow that\ndecision rather than overriding it.\n\n### Accessibility standards\n\n- **WCAG 2.1 AA** — the component meets 2.1.1 Keyboard, 2.4.3 Focus Order,\n 2.4.7 Focus Visible, and 4.1.2 Name, Role, Value, provided the consumer\n supplies an accessible name\n- **Contrast** — the surface uses design tokens that meet AA contrast for text\n placed on it; content authored with non-token colors must be checked\n separately\n- **Target size** — triggers built from Nimbus buttons meet the minimum target\n size; a custom `asChild` trigger is the consumer's responsibility\n\n### Resources\n\n- [ARIA dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)\n- [React Aria Popover](https://react-spectrum.adobe.com/react-aria/Popover.html)\n- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)\n",
|
|
230
|
+
"toc": [
|
|
231
|
+
{
|
|
232
|
+
"value": "Accessibility",
|
|
233
|
+
"href": "#accessibility",
|
|
234
|
+
"depth": 2,
|
|
235
|
+
"numbering": [
|
|
236
|
+
1,
|
|
237
|
+
1
|
|
238
|
+
],
|
|
239
|
+
"parent": "root"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"value": "Give every popover a name",
|
|
243
|
+
"href": "#give-every-popover-a-name",
|
|
244
|
+
"depth": 3,
|
|
245
|
+
"numbering": [
|
|
246
|
+
1,
|
|
247
|
+
1,
|
|
248
|
+
1
|
|
249
|
+
],
|
|
250
|
+
"parent": "root"
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"value": "Keyboard interaction",
|
|
254
|
+
"href": "#keyboard-interaction",
|
|
255
|
+
"depth": 3,
|
|
256
|
+
"numbering": [
|
|
257
|
+
1,
|
|
258
|
+
1,
|
|
259
|
+
2
|
|
260
|
+
],
|
|
261
|
+
"parent": "root"
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"value": "Focus behavior",
|
|
265
|
+
"href": "#focus-behavior",
|
|
266
|
+
"depth": 3,
|
|
267
|
+
"numbering": [
|
|
268
|
+
1,
|
|
269
|
+
1,
|
|
270
|
+
3
|
|
271
|
+
],
|
|
272
|
+
"parent": "root"
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"value": "ARIA attributes",
|
|
276
|
+
"href": "#aria-attributes",
|
|
277
|
+
"depth": 3,
|
|
278
|
+
"numbering": [
|
|
279
|
+
1,
|
|
280
|
+
1,
|
|
281
|
+
4
|
|
282
|
+
],
|
|
283
|
+
"parent": "root"
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"value": "Accessibility standards",
|
|
287
|
+
"href": "#accessibility-standards",
|
|
288
|
+
"depth": 3,
|
|
289
|
+
"numbering": [
|
|
290
|
+
1,
|
|
291
|
+
1,
|
|
292
|
+
5
|
|
293
|
+
],
|
|
294
|
+
"parent": "root"
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
"value": "Resources",
|
|
298
|
+
"href": "#resources",
|
|
299
|
+
"depth": 3,
|
|
300
|
+
"numbering": [
|
|
301
|
+
1,
|
|
302
|
+
1,
|
|
303
|
+
6
|
|
304
|
+
],
|
|
305
|
+
"parent": "root"
|
|
306
|
+
}
|
|
307
|
+
]
|
|
308
|
+
},
|
|
309
|
+
"dev": {
|
|
310
|
+
"mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport {\n Popover,\n type PopoverRootProps,\n type PopoverTriggerProps,\n type PopoverContentProps,\n} from \"@commercetools/nimbus\";\n```\n\n### Basic usage\n\nPopover is a compound component with three parts. `Root` owns the open state and\nthe styling context, `Trigger` opens it, and `Content` is the positioned surface.\n\n```jsx live-dev\nconst App = () => (\n <Popover.Root>\n <Popover.Trigger>Open popover</Popover.Trigger>\n <Popover.Content aria-label=\"Example popover\">\n <Text>Content goes here.</Text>\n </Popover.Content>\n </Popover.Root>\n);\n```\n\nThree things about this structure are worth knowing up front:\n\n- **`Root` renders no DOM element.** It wraps React Aria's `DialogTrigger`, which\n is a context provider, so it adds nothing to your layout. A trigger inside a\n flex row stays where you put it.\n- **`Content` supplies its own dialog.** You do not add `<Dialog>` yourself.\n React Aria needs a dialog inside a popover for the role, name and Escape\n handling to work, so `Content` owns it.\n- **`Trigger` renders a bare `button`.** Its slot adds a focus ring and nothing\n else, so the form above gives you browser default chrome. Reach for `asChild`\n with a `Button` or `IconButton` when you want Nimbus styling, as the later\n examples do.\n\n## Usage examples\n\n### Uncontrolled mode\n\n`defaultOpen` sets the initial state and lets the component manage it from\nthere.\n\nThis one is shown as code rather than rendered live: a `defaultOpen` popover is\nmodal from the moment it mounts, so a live example would open over the page,\ncontain focus and hide the surrounding documentation from assistive technology\nas soon as you scrolled to it.\n\n```tsx\n<Popover.Root defaultOpen>\n <Popover.Trigger>Open popover</Popover.Trigger>\n <Popover.Content aria-label=\"Initially open\">\n <Text>Open on mount.</Text>\n </Popover.Content>\n</Popover.Root>\n```\n\n### Controlled mode\n\nPass `isOpen` and `onOpenChange` to own the state. In controlled mode the popover\ndoes not close itself — it reports the request and waits for `isOpen` to change.\n\n```jsx live-dev\nconst App = () => {\n const [isOpen, setIsOpen] = useState(false);\n\n return (\n <Stack gap=\"200\">\n <Popover.Root isOpen={isOpen} onOpenChange={setIsOpen}>\n <Popover.Trigger>Toggle popover</Popover.Trigger>\n <Popover.Content aria-label=\"Controlled popover\">\n <Text>Controlled by the parent.</Text>\n </Popover.Content>\n </Popover.Root>\n <Text>State: {isOpen ? \"open\" : \"closed\"}</Text>\n </Stack>\n );\n};\n```\n\n### Closing from inside the content\n\nBecause `Content` owns the dialog, buttons inside it can use `slot=\"close\"`,\nthe same way they do in `Dialog` and `Drawer`:\n\n```jsx live-dev\nconst App = () => (\n <Popover.Root>\n <Popover.Trigger>Discard changes</Popover.Trigger>\n <Popover.Content aria-label=\"Discard changes\">\n <Stack gap=\"200\">\n <Text>Discard your changes?</Text>\n <Button slot=\"close\">Discard</Button>\n </Stack>\n </Popover.Content>\n </Popover.Root>\n);\n```\n\nReach for a function child instead when the action has to do something *before*\nit dismisses — `Content` forwards React Aria's `close` render prop, so the\ncallback can run inside your own handler. Either way, prefer these over wiring\nup controlled state just to close the popover.\n\n```jsx live-dev\nconst App = () => (\n <Popover.Root>\n <Popover.Trigger>Discard changes</Popover.Trigger>\n <Popover.Content aria-label=\"Discard changes\">\n {({ close }) => (\n <Stack gap=\"200\">\n <Text>Discard your changes?</Text>\n <Stack direction=\"horizontal\" gap=\"200\">\n <Button variant=\"outline\" onPress={close}>\n Cancel\n </Button>\n <Button onPress={close}>Discard</Button>\n </Stack>\n </Stack>\n )}\n </Popover.Content>\n </Popover.Root>\n);\n```\n\n### Custom triggers\n\nWithout `asChild`, `Trigger` renders its own button. With `asChild`, it applies\ntrigger behavior to the element you supply, which avoids nesting one interactive\nelement inside another.\n\n```jsx live-dev\nconst App = () => (\n <Popover.Root>\n <Popover.Trigger asChild>\n <IconButton aria-label=\"Options\" size=\"xs\" variant=\"ghost\">\n <Icons.MoreVert />\n </IconButton>\n </Popover.Trigger>\n <Popover.Content aria-label=\"Options\">\n <Text>Options list</Text>\n </Popover.Content>\n </Popover.Root>\n);\n```\n\nThe two modes accept different props. React Aria delivers the trigger contract —\npress handling, `aria-expanded`, `aria-controls` and the trigger ref — to the\nrendered element through context, so with `asChild` there is nothing for\n`Trigger` to forward. Put the element's own props (`id`, `aria-label`,\n`isDisabled`) on the element itself; `Trigger` accepts only style props in this\nmode, and rejects the rest at compile time rather than accepting them and having\nno effect.\n\n### Positioning\n\n`placement` accepts a side, optionally followed by an alignment. The alignment\nvalues depend on the axis — this trips people up, so it is worth stating\nexplicitly:\n\n| Side | Valid alignments |\n| --- | --- |\n| `top`, `bottom` | `left`, `right`, `start`, `end` |\n| `left`, `right`, `start`, `end` | `top`, `bottom` |\n\nSo `bottom start` and `right top` are valid; `right start` is not, and will fail\nto typecheck.\n\n```jsx live-dev\nconst App = () => (\n <Popover.Root placement=\"right top\" offset={12} crossOffset={4}>\n <Popover.Trigger>Positioned</Popover.Trigger>\n <Popover.Content aria-label=\"Positioned popover\">\n <Text>placement=\"right top\"</Text>\n </Popover.Content>\n </Popover.Root>\n);\n```\n\n`offset` adjusts distance along the trigger's axis, `crossOffset` along the\nperpendicular one, both in pixels. `shouldFlip` (default `true`) controls whether\nthe popover may flip to the opposite side to stay in the viewport.\n\n### Dismissal behavior\n\nBy default the popover closes on Escape and on an outside press. Both can be\nadjusted:\n\n```jsx live-dev\nconst App = () => (\n <Popover.Root isKeyboardDismissDisabled>\n <Popover.Trigger>Keyboard dismiss disabled</Popover.Trigger>\n <Popover.Content aria-label=\"No Escape dismissal\">\n <Text>Escape will not close this. Click outside instead.</Text>\n </Popover.Content>\n </Popover.Root>\n);\n```\n\n`shouldCloseOnInteractOutside` takes a predicate receiving the interacted\nelement, so you can exempt specific regions from dismissing the popover.\n\n> [!CAUTION]\\\n> If you set `isKeyboardDismissDisabled`, provide another keyboard route to close\n> the popover. Otherwise keyboard users can open something they cannot dismiss.\n\n`isNonModal` covers the other half of modality, and it lifts less than the name\nsuggests. By default React Aria marks the rest of the page `inert` and locks page\nscroll; `isNonModal` leaves both alone, so assistive technologies can reach the\nsurrounding content. Focus is still contained and an outside press still\ndismisses — that containment comes from the dialog `Popover.Content` renders, not\nfrom modality. React Aria's guidance is that most popovers should not use it; it\nexists for components like combobox, where the trigger must keep focus while the\noverlay is open.\n\n## Component requirements\n\n### Accessibility\n\nThe Popover component handles most accessibility requirements internally via\nReact Aria.\n\n- **Labeling**: `Popover.Content` renders a dialog, so it needs an accessible\n name. Pass `aria-label`, or `aria-labelledby` pointing at a heading inside the\n content. This is the one accessibility requirement the component cannot satisfy\n for you.\n- **Role**: `Content` renders an element with `role=\"dialog\"`.\n- **Focus management**: focus moves into the dialog on open and returns to the\n trigger on close. The exception is a dismissing press on another focusable\n element, which leaves focus on that element.\n- **ARIA state**: the trigger carries `aria-expanded`, plus `aria-controls` while\n open. It deliberately does not carry `aria-haspopup` — React Aria sets that only\n for `menu` and `listbox` overlays, since screen readers often announce other\n values as \"menu\".\n- **Modality**: focus is contained and outside interaction dismisses it;\n `isNonModal` relaxes neither — see [Dismissal behavior](#dismissal-behavior).\n\nIf your use case requires tracking and analytics for this component, it is good\npractice to add a **persistent**, **unique** id:\n\n```tsx\nconst PERSISTENT_ID = \"filters-popover-trigger\";\n\nexport const Example = () => (\n <Popover.Root>\n <Popover.Trigger id={PERSISTENT_ID}>Filters</Popover.Trigger>\n <Popover.Content aria-label=\"Filters\">…</Popover.Content>\n </Popover.Root>\n);\n```\n\n`Popover.Content` takes an `id` too. It lands on the dialog element, and React\nAria re-points the trigger's `aria-controls` at it, so the trigger-to-overlay\nlink is kept. `data-*` attributes and click handlers on `Popover.Content` reach\nthe same element. `Popover.Root` takes neither — it renders nothing.\n\n#### Keyboard navigation\n\nThe component supports full keyboard interaction:\n\n- `Tab`: moves focus between focusable elements inside the popover\n- `Enter` or `Space`: toggles the popover when the trigger is focused\n- `Escape`: closes the popover and returns focus to the trigger\n\n### Composition constraints\n\n- `Popover.Trigger` and `Popover.Content` must be inside a `Popover.Root`.\n- Do not add your own `Dialog` inside `Popover.Content` — it already renders one,\n and nesting a second breaks the naming and focus contract.\n- Do not nest a Popover inside another Popover.\n\n## Common patterns\n\n### Filter panel\n\n```jsx live-dev\nconst App = () => {\n const [onlyActive, setOnlyActive] = useState(false);\n\n return (\n <Popover.Root>\n <Popover.Trigger>Filters</Popover.Trigger>\n <Popover.Content aria-labelledby=\"filters-title\" maxWidth=\"4800\">\n {({ close }) => (\n <Stack gap=\"300\">\n <Heading id=\"filters-title\" size=\"sm\">\n Filters\n </Heading>\n <Checkbox isSelected={onlyActive} onChange={setOnlyActive}>\n Only active\n </Checkbox>\n <Button onPress={close}>Apply filters</Button>\n </Stack>\n )}\n </Popover.Content>\n </Popover.Root>\n );\n};\n```\n\n### Inline edit\n\n```jsx live-dev\nconst App = () => {\n const [name, setName] = useState(\"Ada Lovelace\");\n const [draft, setDraft] = useState(name);\n\n return (\n <Stack direction=\"horizontal\" gap=\"200\" alignItems=\"center\">\n <Text>{name}</Text>\n <Popover.Root onOpenChange={(open) => open && setDraft(name)}>\n <Popover.Trigger asChild>\n <IconButton aria-label=\"Edit name\" size=\"2xs\" variant=\"ghost\">\n <Icons.Edit />\n </IconButton>\n </Popover.Trigger>\n <Popover.Content aria-label=\"Edit name\">\n {({ close }) => (\n <Stack gap=\"200\">\n <TextInput\n aria-label=\"Name\"\n value={draft}\n onChange={setDraft}\n />\n <Button\n onPress={() => {\n setName(draft);\n close();\n }}\n >\n Save\n </Button>\n </Stack>\n )}\n </Popover.Content>\n </Popover.Root>\n </Stack>\n );\n};\n```\n\n## API reference\n\n<PropsTable id=\"Popover\" />\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using Popover 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### Form Submission Tests\n\nValidate a form inside a popover and dismiss it only once the input is valid\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport { userEvent } from \"@testing-library/user-event\";\nimport { useState } from \"react\";\nimport {\n Popover,\n Button,\n Checkbox,\n Stack,\n Text,\n TextInput,\n NimbusProvider,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"Popover - Form submission\", () => {\n const RenamePopover = ({ onSave }: { onSave: (name: string) => void }) => {\n const [draft, setDraft] = useState(\"\");\n const [error, setError] = useState<string | null>(null);\n\n return (\n <NimbusProvider>\n <Popover.Root>\n <Popover.Trigger>Rename</Popover.Trigger>\n <Popover.Content aria-label=\"Rename\">\n {({ close }) => (\n <form\n onSubmit={(event) => {\n event.preventDefault();\n if (!draft.trim()) {\n setError(\"Name is required\");\n return;\n }\n onSave(draft);\n close();\n }}\n >\n <Stack gap=\"200\">\n <TextInput\n aria-label=\"Name\"\n value={draft}\n onChange={(value) => setDraft(value)}\n />\n {error && <Text>{error}</Text>}\n <Button type=\"submit\">Save</Button>\n </Stack>\n </form>\n )}\n </Popover.Content>\n </Popover.Root>\n </NimbusProvider>\n );\n };\n\n it(\"reports the error and stays open when the input is invalid\", async () => {\n const user = userEvent.setup();\n const handleSave = vi.fn();\n\n render(<RenamePopover onSave={handleSave} />);\n\n await user.click(screen.getByRole(\"button\", { name: \"Rename\" }));\n await waitFor(() => {\n expect(screen.getByRole(\"dialog\")).toBeInTheDocument();\n });\n\n await user.click(screen.getByRole(\"button\", { name: \"Save\" }));\n\n await waitFor(() => {\n expect(screen.getByText(\"Name is required\")).toBeInTheDocument();\n });\n expect(handleSave).not.toHaveBeenCalled();\n expect(screen.getByRole(\"dialog\")).toBeInTheDocument();\n });\n\n it(\"saves and dismisses the popover when the input is valid\", async () => {\n const user = userEvent.setup();\n const handleSave = vi.fn();\n\n render(<RenamePopover onSave={handleSave} />);\n\n await user.click(screen.getByRole(\"button\", { name: \"Rename\" }));\n await waitFor(() => {\n expect(screen.getByRole(\"dialog\")).toBeInTheDocument();\n });\n\n await user.type(screen.getByRole(\"textbox\", { name: \"Name\" }), \"Ada\");\n await user.click(screen.getByRole(\"button\", { name: \"Save\" }));\n\n expect(handleSave).toHaveBeenCalledWith(\"Ada\");\n await waitFor(() => {\n expect(screen.queryByRole(\"dialog\")).not.toBeInTheDocument();\n });\n });\n});\n```\n\n### Async Data Loading Tests\n\nFetch the popover's contents when it opens, showing a loading state until the request resolves\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport { userEvent } from \"@testing-library/user-event\";\nimport { useState } from \"react\";\nimport {\n Popover,\n Button,\n Checkbox,\n Stack,\n Text,\n TextInput,\n NimbusProvider,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"Popover - Async data loading\", () => {\n const DetailsPopover = ({ load }: { load: () => Promise<string> }) => {\n const [detail, setDetail] = useState<string | null>(null);\n\n return (\n <NimbusProvider>\n <Popover.Root\n onOpenChange={(isOpen) => {\n if (!isOpen || detail) return;\n void load().then(setDetail);\n }}\n >\n <Popover.Trigger>Details</Popover.Trigger>\n <Popover.Content aria-label=\"Details\">\n <Text>{detail ?? \"Loading…\"}</Text>\n </Popover.Content>\n </Popover.Root>\n </NimbusProvider>\n );\n };\n\n it(\"shows a loading state until the request resolves\", async () => {\n const user = userEvent.setup();\n let resolveLoad!: (value: string) => void;\n const load = vi.fn(\n () =>\n new Promise<string>((resolve) => {\n resolveLoad = resolve;\n })\n );\n\n render(<DetailsPopover load={load} />);\n\n await user.click(screen.getByRole(\"button\", { name: \"Details\" }));\n await waitFor(() => {\n expect(screen.getByText(\"Loading…\")).toBeInTheDocument();\n });\n\n resolveLoad(\"Created 3 days ago\");\n\n await waitFor(() => {\n expect(screen.getByText(\"Created 3 days ago\")).toBeInTheDocument();\n });\n expect(load).toHaveBeenCalledTimes(1);\n });\n\n it(\"does not refetch when the popover is reopened\", async () => {\n const user = userEvent.setup();\n const load = vi.fn(() => Promise.resolve(\"Created 3 days ago\"));\n\n render(<DetailsPopover load={load} />);\n\n const trigger = screen.getByRole(\"button\", { name: \"Details\" });\n await user.click(trigger);\n await waitFor(() => {\n expect(screen.getByText(\"Created 3 days ago\")).toBeInTheDocument();\n });\n\n await user.click(trigger);\n await waitFor(() => {\n expect(screen.queryByRole(\"dialog\")).not.toBeInTheDocument();\n });\n await user.click(trigger);\n\n await waitFor(() => {\n expect(screen.getByText(\"Created 3 days ago\")).toBeInTheDocument();\n });\n expect(load).toHaveBeenCalledTimes(1);\n });\n});\n```\n\n### Multi-Component Workflow Tests\n\nApply filters chosen in a popover to a list rendered outside it\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport { userEvent } from \"@testing-library/user-event\";\nimport { useState } from \"react\";\nimport {\n Popover,\n Button,\n Checkbox,\n Stack,\n Text,\n TextInput,\n NimbusProvider,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"Popover - Filter workflow\", () => {\n const ITEMS = [\n { id: \"1\", name: \"Active item\", isActive: true },\n { id: \"2\", name: \"Archived item\", isActive: false },\n ];\n\n const FilterableList = () => {\n const [onlyActive, setOnlyActive] = useState(false);\n const [draft, setDraft] = useState(false);\n const visible = onlyActive ? ITEMS.filter((item) => item.isActive) : ITEMS;\n\n return (\n <NimbusProvider>\n <Stack gap=\"200\">\n <Popover.Root\n onOpenChange={(isOpen) => {\n // Seed the draft from the applied value each time it opens, so\n // an abandoned popover does not leak changes into the list.\n if (isOpen) setDraft(onlyActive);\n }}\n >\n <Popover.Trigger>Filters</Popover.Trigger>\n <Popover.Content aria-label=\"Filters\">\n {({ close }) => (\n <Stack gap=\"200\">\n <Checkbox isSelected={draft} onChange={setDraft}>\n Only active\n </Checkbox>\n <Button\n onPress={() => {\n setOnlyActive(draft);\n close();\n }}\n >\n Apply filters\n </Button>\n </Stack>\n )}\n </Popover.Content>\n </Popover.Root>\n <ul>\n {visible.map((item) => (\n <li key={item.id}>{item.name}</li>\n ))}\n </ul>\n </Stack>\n </NimbusProvider>\n );\n };\n\n it(\"filters the list once the popover applies its selection\", async () => {\n const user = userEvent.setup();\n\n render(<FilterableList />);\n\n expect(screen.getByText(\"Active item\")).toBeInTheDocument();\n expect(screen.getByText(\"Archived item\")).toBeInTheDocument();\n\n await user.click(screen.getByRole(\"button\", { name: \"Filters\" }));\n await waitFor(() => {\n expect(screen.getByRole(\"dialog\")).toBeInTheDocument();\n });\n\n await user.click(screen.getByRole(\"checkbox\", { name: \"Only active\" }));\n await user.click(screen.getByRole(\"button\", { name: \"Apply filters\" }));\n\n await waitFor(() => {\n expect(screen.queryByText(\"Archived item\")).not.toBeInTheDocument();\n });\n expect(screen.getByText(\"Active item\")).toBeInTheDocument();\n });\n\n it(\"leaves the list untouched when the popover is abandoned\", async () => {\n const user = userEvent.setup();\n\n render(<FilterableList />);\n\n await user.click(screen.getByRole(\"button\", { name: \"Filters\" }));\n await waitFor(() => {\n expect(screen.getByRole(\"dialog\")).toBeInTheDocument();\n });\n\n await user.click(screen.getByRole(\"checkbox\", { name: \"Only active\" }));\n await user.keyboard(\"{Escape}\");\n\n await waitFor(() => {\n expect(screen.queryByRole(\"dialog\")).not.toBeInTheDocument();\n });\n expect(screen.getByText(\"Archived item\")).toBeInTheDocument();\n });\n});\n```\n\n### Controlled Dismissal Tests\n\nOwn the open state so the popover cannot be dismissed while there are unsaved edits\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport { userEvent } from \"@testing-library/user-event\";\nimport { useState } from \"react\";\nimport {\n Popover,\n Button,\n Checkbox,\n Stack,\n Text,\n TextInput,\n NimbusProvider,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"Popover - Guarded dismissal\", () => {\n const GuardedPopover = () => {\n const [isOpen, setIsOpen] = useState(false);\n const [isDirty, setIsDirty] = useState(false);\n\n return (\n <NimbusProvider>\n <Popover.Root\n isOpen={isOpen}\n onOpenChange={(next) => {\n // In controlled mode the popover reports the request rather than\n // acting on it, so unsaved edits can veto the dismissal.\n if (!next && isDirty) return;\n setIsOpen(next);\n }}\n >\n <Popover.Trigger>Edit</Popover.Trigger>\n <Popover.Content aria-label=\"Edit\">\n <Stack gap=\"200\">\n <Button onPress={() => setIsDirty(true)}>Make an edit</Button>\n <Button\n onPress={() => {\n setIsDirty(false);\n setIsOpen(false);\n }}\n >\n Discard\n </Button>\n </Stack>\n </Popover.Content>\n </Popover.Root>\n </NimbusProvider>\n );\n };\n\n it(\"dismisses normally while there are no unsaved edits\", async () => {\n const user = userEvent.setup();\n\n render(<GuardedPopover />);\n\n await user.click(screen.getByRole(\"button\", { name: \"Edit\" }));\n await waitFor(() => {\n expect(screen.getByRole(\"dialog\")).toBeInTheDocument();\n });\n\n await user.keyboard(\"{Escape}\");\n\n await waitFor(() => {\n expect(screen.queryByRole(\"dialog\")).not.toBeInTheDocument();\n });\n });\n\n it(\"refuses to dismiss while there are unsaved edits\", async () => {\n const user = userEvent.setup();\n\n render(<GuardedPopover />);\n\n await user.click(screen.getByRole(\"button\", { name: \"Edit\" }));\n await waitFor(() => {\n expect(screen.getByRole(\"dialog\")).toBeInTheDocument();\n });\n\n await user.click(screen.getByRole(\"button\", { name: \"Make an edit\" }));\n await user.keyboard(\"{Escape}\");\n\n expect(screen.getByRole(\"dialog\")).toBeInTheDocument();\n\n await user.click(screen.getByRole(\"button\", { name: \"Discard\" }));\n\n await waitFor(() => {\n expect(screen.queryByRole(\"dialog\")).not.toBeInTheDocument();\n });\n });\n});\n```\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-popover--docs)\n- [React Aria Popover](https://react-spectrum.adobe.com/react-aria/Popover.html)\n- [ARIA Dialog Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)\n",
|
|
311
|
+
"toc": [
|
|
312
|
+
{
|
|
313
|
+
"value": "Getting started",
|
|
314
|
+
"href": "#getting-started",
|
|
315
|
+
"depth": 2,
|
|
316
|
+
"numbering": [
|
|
317
|
+
1,
|
|
318
|
+
1
|
|
319
|
+
],
|
|
320
|
+
"parent": "root"
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
"value": "Import",
|
|
324
|
+
"href": "#import",
|
|
325
|
+
"depth": 3,
|
|
326
|
+
"numbering": [
|
|
327
|
+
1,
|
|
328
|
+
1,
|
|
329
|
+
1
|
|
330
|
+
],
|
|
331
|
+
"parent": "root"
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
"value": "Basic usage",
|
|
335
|
+
"href": "#basic-usage",
|
|
336
|
+
"depth": 3,
|
|
337
|
+
"numbering": [
|
|
338
|
+
1,
|
|
339
|
+
1,
|
|
340
|
+
2
|
|
341
|
+
],
|
|
342
|
+
"parent": "root"
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
"value": "Usage examples",
|
|
346
|
+
"href": "#usage-examples",
|
|
347
|
+
"depth": 2,
|
|
348
|
+
"numbering": [
|
|
349
|
+
1,
|
|
350
|
+
2
|
|
351
|
+
],
|
|
352
|
+
"parent": "root"
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
"value": "Uncontrolled mode",
|
|
356
|
+
"href": "#uncontrolled-mode",
|
|
357
|
+
"depth": 3,
|
|
358
|
+
"numbering": [
|
|
359
|
+
1,
|
|
360
|
+
2,
|
|
361
|
+
1
|
|
362
|
+
],
|
|
363
|
+
"parent": "root"
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
"value": "Controlled mode",
|
|
367
|
+
"href": "#controlled-mode",
|
|
368
|
+
"depth": 3,
|
|
369
|
+
"numbering": [
|
|
370
|
+
1,
|
|
371
|
+
2,
|
|
372
|
+
2
|
|
373
|
+
],
|
|
374
|
+
"parent": "root"
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
"value": "Closing from inside the content",
|
|
378
|
+
"href": "#closing-from-inside-the-content",
|
|
379
|
+
"depth": 3,
|
|
380
|
+
"numbering": [
|
|
381
|
+
1,
|
|
382
|
+
2,
|
|
383
|
+
3
|
|
384
|
+
],
|
|
385
|
+
"parent": "root"
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
"value": "Custom triggers",
|
|
389
|
+
"href": "#custom-triggers",
|
|
390
|
+
"depth": 3,
|
|
391
|
+
"numbering": [
|
|
392
|
+
1,
|
|
393
|
+
2,
|
|
394
|
+
4
|
|
395
|
+
],
|
|
396
|
+
"parent": "root"
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
"value": "Positioning",
|
|
400
|
+
"href": "#positioning",
|
|
401
|
+
"depth": 3,
|
|
402
|
+
"numbering": [
|
|
403
|
+
1,
|
|
404
|
+
2,
|
|
405
|
+
5
|
|
406
|
+
],
|
|
407
|
+
"parent": "root"
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
"value": "Dismissal behavior",
|
|
411
|
+
"href": "#dismissal-behavior",
|
|
412
|
+
"depth": 3,
|
|
413
|
+
"numbering": [
|
|
414
|
+
1,
|
|
415
|
+
2,
|
|
416
|
+
6
|
|
417
|
+
],
|
|
418
|
+
"parent": "root"
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
"value": "Component requirements",
|
|
422
|
+
"href": "#component-requirements",
|
|
423
|
+
"depth": 2,
|
|
424
|
+
"numbering": [
|
|
425
|
+
1,
|
|
426
|
+
3
|
|
427
|
+
],
|
|
428
|
+
"parent": "root"
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
"value": "Accessibility",
|
|
432
|
+
"href": "#accessibility",
|
|
433
|
+
"depth": 3,
|
|
434
|
+
"numbering": [
|
|
435
|
+
1,
|
|
436
|
+
3,
|
|
437
|
+
1
|
|
438
|
+
],
|
|
439
|
+
"parent": "root"
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
"value": "Keyboard navigation",
|
|
443
|
+
"href": "#keyboard-navigation",
|
|
444
|
+
"depth": 4,
|
|
445
|
+
"numbering": [
|
|
446
|
+
1,
|
|
447
|
+
3,
|
|
448
|
+
1,
|
|
449
|
+
1
|
|
450
|
+
],
|
|
451
|
+
"parent": "root"
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
"value": "Composition constraints",
|
|
455
|
+
"href": "#composition-constraints",
|
|
456
|
+
"depth": 3,
|
|
457
|
+
"numbering": [
|
|
458
|
+
1,
|
|
459
|
+
3,
|
|
460
|
+
2
|
|
461
|
+
],
|
|
462
|
+
"parent": "root"
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
"value": "Common patterns",
|
|
466
|
+
"href": "#common-patterns",
|
|
467
|
+
"depth": 2,
|
|
468
|
+
"numbering": [
|
|
469
|
+
1,
|
|
470
|
+
4
|
|
471
|
+
],
|
|
472
|
+
"parent": "root"
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
"value": "Filter panel",
|
|
476
|
+
"href": "#filter-panel",
|
|
477
|
+
"depth": 3,
|
|
478
|
+
"numbering": [
|
|
479
|
+
1,
|
|
480
|
+
4,
|
|
481
|
+
1
|
|
482
|
+
],
|
|
483
|
+
"parent": "root"
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
"value": "Inline edit",
|
|
487
|
+
"href": "#inline-edit",
|
|
488
|
+
"depth": 3,
|
|
489
|
+
"numbering": [
|
|
490
|
+
1,
|
|
491
|
+
4,
|
|
492
|
+
2
|
|
493
|
+
],
|
|
494
|
+
"parent": "root"
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
"value": "API reference",
|
|
498
|
+
"href": "#api-reference",
|
|
499
|
+
"depth": 2,
|
|
500
|
+
"numbering": [
|
|
501
|
+
1,
|
|
502
|
+
5
|
|
503
|
+
],
|
|
504
|
+
"parent": "root"
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
"value": "Testing your implementation",
|
|
508
|
+
"href": "#testing-your-implementation",
|
|
509
|
+
"depth": 2,
|
|
510
|
+
"numbering": [
|
|
511
|
+
1,
|
|
512
|
+
6
|
|
513
|
+
],
|
|
514
|
+
"parent": "root"
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
"value": "Form Submission Tests",
|
|
518
|
+
"href": "#form-submission-tests",
|
|
519
|
+
"depth": 3,
|
|
520
|
+
"numbering": [
|
|
521
|
+
1,
|
|
522
|
+
6,
|
|
523
|
+
1
|
|
524
|
+
],
|
|
525
|
+
"parent": "root"
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
"value": "Async Data Loading Tests",
|
|
529
|
+
"href": "#async-data-loading-tests",
|
|
530
|
+
"depth": 3,
|
|
531
|
+
"numbering": [
|
|
532
|
+
1,
|
|
533
|
+
6,
|
|
534
|
+
2
|
|
535
|
+
],
|
|
536
|
+
"parent": "root"
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
"value": "Multi-Component Workflow Tests",
|
|
540
|
+
"href": "#multi-component-workflow-tests",
|
|
541
|
+
"depth": 3,
|
|
542
|
+
"numbering": [
|
|
543
|
+
1,
|
|
544
|
+
6,
|
|
545
|
+
3
|
|
546
|
+
],
|
|
547
|
+
"parent": "root"
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
"value": "Controlled Dismissal Tests",
|
|
551
|
+
"href": "#controlled-dismissal-tests",
|
|
552
|
+
"depth": 3,
|
|
553
|
+
"numbering": [
|
|
554
|
+
1,
|
|
555
|
+
6,
|
|
556
|
+
4
|
|
557
|
+
],
|
|
558
|
+
"parent": "root"
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
"value": "Resources",
|
|
562
|
+
"href": "#resources",
|
|
563
|
+
"depth": 2,
|
|
564
|
+
"numbering": [
|
|
565
|
+
1,
|
|
566
|
+
7
|
|
567
|
+
],
|
|
568
|
+
"parent": "root"
|
|
569
|
+
}
|
|
570
|
+
]
|
|
571
|
+
},
|
|
572
|
+
"guidelines": {
|
|
573
|
+
"mdx": "\n## Guidelines\n\nA popover borrows the user's attention without taking the page from them. Use it\nwhen the next step is small enough to finish in place.\n\n### When to use\n\nUse a popover when **all** of these hold:\n\n- The content is interactive — something to press, type in, or choose\n- It belongs to a specific element on the page, and that relationship matters\n- It is short enough to finish without scrolling\n- Losing it to a stray click outside would cost the user nothing important\n\nIf the last point fails, the work deserves a `Dialog`. An outside press\ndismisses a popover with no confirmation.\n\n### Choosing between overlays\n\n| Component | Content | Focus | Anchored |\n| --- | --- | --- | --- |\n| **Tooltip** | Short, non-interactive | Stays on trigger | Yes |\n| **Popover** | Interactive, small | Moves into overlay | Yes |\n| **Dialog** | Interactive, any size | Moves into overlay | No |\n| **Drawer** | Interactive, large | Moves into overlay | No |\n\nThe most common mistake is using a popover for a text hint. Tooltips open on\nhover and focus and leave focus alone; a popover requires a press and pulls focus\nin. For a hint, that is friction with no benefit.\n\nThe second most common is using a popover for a destructive confirmation. It can\nbe dismissed by clicking anywhere else, which is the opposite of what a\nconfirmation needs.\n\n### Best practices\n\n> [!TIP]\\\n> **Do** name every popover. Give `Popover.Content` an `aria-label`, or point\n> `aria-labelledby` at a heading inside it. An unnamed dialog is announced as\n> just that.\n\n> [!TIP]\\\n> **Do** let the trigger describe what opens. \"Filters\" tells the user what to\n> expect; an unlabelled icon does not.\n\n> [!CAUTION]\\\n> **Do not** stack a popover on a popover. Nested overlays are hard to dismiss\n> predictably and hard to follow with a screen reader. Anchor both to the same\n> surface, or promote the second step to a `Dialog`.\n\n> [!CAUTION]\\\n> **Do not** hide the only route to a task inside a popover. Anything a user must\n> be able to find should be reachable without discovering a trigger first.\n\n> [!TIP]\\\n> **Do** constrain the width when the content is prose. Text that stretches the\n> full surface is harder to read than text with a sensible measure.\n\n### Content\n\nKeep the content to one job. A popover that has grown a header, several sections\nand a scrollbar has outgrown the pattern.\n\nOrder the content the way it will be used: what the user came for first, and the\naction that closes the popover last. Where an action dismisses the popover, say\nwhat it does — \"Apply filters\" rather than \"OK\".\n\n### Placement\n\nLet the default `bottom` placement stand unless there is a reason not to. It is\npredictable, and it is where users look after pressing something.\n\nChange it when the trigger sits near an edge and a different side keeps the\ncontent closer to where the user is working. Placement is a preference either\nway: the popover flips and shifts to stay on screen, so it may not honour the\nside you asked for.\n\nUse `offset` to adjust the gap, not to nudge the surface into an alignment that\n`placement` already expresses.\n",
|
|
574
|
+
"toc": [
|
|
575
|
+
{
|
|
576
|
+
"value": "Guidelines",
|
|
577
|
+
"href": "#guidelines",
|
|
578
|
+
"depth": 2,
|
|
579
|
+
"numbering": [
|
|
580
|
+
1,
|
|
581
|
+
1
|
|
582
|
+
],
|
|
583
|
+
"parent": "root"
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
"value": "When to use",
|
|
587
|
+
"href": "#when-to-use",
|
|
588
|
+
"depth": 3,
|
|
589
|
+
"numbering": [
|
|
590
|
+
1,
|
|
591
|
+
1,
|
|
592
|
+
1
|
|
593
|
+
],
|
|
594
|
+
"parent": "root"
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
"value": "Choosing between overlays",
|
|
598
|
+
"href": "#choosing-between-overlays",
|
|
599
|
+
"depth": 3,
|
|
600
|
+
"numbering": [
|
|
601
|
+
1,
|
|
602
|
+
1,
|
|
603
|
+
2
|
|
604
|
+
],
|
|
605
|
+
"parent": "root"
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
"value": "Best practices",
|
|
609
|
+
"href": "#best-practices",
|
|
610
|
+
"depth": 3,
|
|
611
|
+
"numbering": [
|
|
612
|
+
1,
|
|
613
|
+
1,
|
|
614
|
+
3
|
|
615
|
+
],
|
|
616
|
+
"parent": "root"
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
"value": "Content",
|
|
620
|
+
"href": "#content",
|
|
621
|
+
"depth": 3,
|
|
622
|
+
"numbering": [
|
|
623
|
+
1,
|
|
624
|
+
1,
|
|
625
|
+
4
|
|
626
|
+
],
|
|
627
|
+
"parent": "root"
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
"value": "Placement",
|
|
631
|
+
"href": "#placement",
|
|
632
|
+
"depth": 3,
|
|
633
|
+
"numbering": [
|
|
634
|
+
1,
|
|
635
|
+
1,
|
|
636
|
+
5
|
|
637
|
+
],
|
|
638
|
+
"parent": "root"
|
|
639
|
+
}
|
|
640
|
+
]
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|