@charcuterie/ui 0.1.1 → 1.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.
Files changed (115) hide show
  1. package/README.md +9 -0
  2. package/dist/Accordion/Accordion.d.ts +82 -0
  3. package/dist/Accordion/Accordion.d.ts.map +1 -0
  4. package/dist/Accordion/Accordion.js +108 -0
  5. package/dist/Accordion/Accordion.js.map +1 -0
  6. package/dist/Accordion/AccordionSection.d.ts +28 -0
  7. package/dist/Accordion/AccordionSection.d.ts.map +1 -0
  8. package/dist/Accordion/AccordionSection.js +53 -0
  9. package/dist/Accordion/AccordionSection.js.map +1 -0
  10. package/dist/Field/Field.d.ts +104 -0
  11. package/dist/Field/Field.d.ts.map +1 -0
  12. package/dist/Field/Field.js +115 -0
  13. package/dist/Field/Field.js.map +1 -0
  14. package/dist/Field/FieldGroup.d.ts +58 -0
  15. package/dist/Field/FieldGroup.d.ts.map +1 -0
  16. package/dist/Field/FieldGroup.js +65 -0
  17. package/dist/Field/FieldGroup.js.map +1 -0
  18. package/dist/FileDropZone/FileDropZone.d.ts +63 -0
  19. package/dist/FileDropZone/FileDropZone.d.ts.map +1 -0
  20. package/dist/FileDropZone/FileDropZone.js +122 -0
  21. package/dist/FileDropZone/FileDropZone.js.map +1 -0
  22. package/dist/LogViewer/LogViewer.d.ts +90 -0
  23. package/dist/LogViewer/LogViewer.d.ts.map +1 -0
  24. package/dist/LogViewer/LogViewer.js +165 -0
  25. package/dist/LogViewer/LogViewer.js.map +1 -0
  26. package/dist/Menu/Menu.d.ts +81 -0
  27. package/dist/Menu/Menu.d.ts.map +1 -0
  28. package/dist/Menu/Menu.js +199 -0
  29. package/dist/Menu/Menu.js.map +1 -0
  30. package/dist/Menu/MenuAction.d.ts +24 -0
  31. package/dist/Menu/MenuAction.d.ts.map +1 -0
  32. package/dist/Menu/MenuAction.js +37 -0
  33. package/dist/Menu/MenuAction.js.map +1 -0
  34. package/dist/Select/Select.d.ts +87 -0
  35. package/dist/Select/Select.d.ts.map +1 -0
  36. package/dist/Select/Select.js +51 -0
  37. package/dist/Select/Select.js.map +1 -0
  38. package/dist/SortableTableHeader/SortableTableHeader.d.ts +61 -0
  39. package/dist/SortableTableHeader/SortableTableHeader.d.ts.map +1 -0
  40. package/dist/SortableTableHeader/SortableTableHeader.js +56 -0
  41. package/dist/SortableTableHeader/SortableTableHeader.js.map +1 -0
  42. package/dist/Toast/Toast.d.ts +54 -0
  43. package/dist/Toast/Toast.d.ts.map +1 -0
  44. package/dist/Toast/Toast.js +121 -0
  45. package/dist/Toast/Toast.js.map +1 -0
  46. package/dist/Toast/ToastRegion.d.ts +57 -0
  47. package/dist/Toast/ToastRegion.d.ts.map +1 -0
  48. package/dist/Toast/ToastRegion.js +41 -0
  49. package/dist/Toast/ToastRegion.js.map +1 -0
  50. package/dist/Toast/toastLifecycle.d.ts +34 -0
  51. package/dist/Toast/toastLifecycle.d.ts.map +1 -0
  52. package/dist/Toast/toastLifecycle.js +25 -0
  53. package/dist/Toast/toastLifecycle.js.map +1 -0
  54. package/dist/Tooltip/Tooltip.d.ts +82 -0
  55. package/dist/Tooltip/Tooltip.d.ts.map +1 -0
  56. package/dist/Tooltip/Tooltip.js +124 -0
  57. package/dist/Tooltip/Tooltip.js.map +1 -0
  58. package/dist/index.d.ts +26 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +13 -0
  61. package/dist/index.js.map +1 -1
  62. package/dist/mountStory.testHelpers.d.ts +0 -11
  63. package/dist/mountStory.testHelpers.d.ts.map +1 -1
  64. package/dist/mountStory.testHelpers.js +27 -0
  65. package/dist/mountStory.testHelpers.js.map +1 -1
  66. package/dist/slotProps.d.ts +110 -0
  67. package/dist/slotProps.d.ts.map +1 -0
  68. package/dist/slotProps.js +39 -0
  69. package/dist/slotProps.js.map +1 -0
  70. package/package.json +3 -3
  71. package/src/Accordion/Accordion.mdx +83 -0
  72. package/src/Accordion/Accordion.stories.tsx +146 -0
  73. package/src/Accordion/Accordion.test.tsx +163 -0
  74. package/src/Accordion/Accordion.tsx +182 -0
  75. package/src/Accordion/AccordionSection.tsx +163 -0
  76. package/src/Field/Field.mdx +144 -0
  77. package/src/Field/Field.stories.tsx +280 -0
  78. package/src/Field/Field.test.tsx +344 -0
  79. package/src/Field/Field.tsx +212 -0
  80. package/src/Field/FieldGroup.tsx +138 -0
  81. package/src/FileDropZone/FileDropZone.mdx +78 -0
  82. package/src/FileDropZone/FileDropZone.stories.tsx +136 -0
  83. package/src/FileDropZone/FileDropZone.test.tsx +230 -0
  84. package/src/FileDropZone/FileDropZone.tsx +234 -0
  85. package/src/LogViewer/LogViewer.mdx +123 -0
  86. package/src/LogViewer/LogViewer.stories.tsx +169 -0
  87. package/src/LogViewer/LogViewer.test.tsx +236 -0
  88. package/src/LogViewer/LogViewer.tsx +261 -0
  89. package/src/Menu/Menu.mdx +103 -0
  90. package/src/Menu/Menu.stories.tsx +161 -0
  91. package/src/Menu/Menu.test.tsx +202 -0
  92. package/src/Menu/Menu.tsx +300 -0
  93. package/src/Menu/MenuAction.tsx +95 -0
  94. package/src/Select/Select.mdx +74 -0
  95. package/src/Select/Select.stories.tsx +126 -0
  96. package/src/Select/Select.test.tsx +129 -0
  97. package/src/Select/Select.tsx +200 -0
  98. package/src/SortableTableHeader/SortableTableHeader.mdx +67 -0
  99. package/src/SortableTableHeader/SortableTableHeader.stories.tsx +207 -0
  100. package/src/SortableTableHeader/SortableTableHeader.test.tsx +180 -0
  101. package/src/SortableTableHeader/SortableTableHeader.tsx +131 -0
  102. package/src/Toast/Toast.mdx +84 -0
  103. package/src/Toast/Toast.stories.tsx +192 -0
  104. package/src/Toast/Toast.test.tsx +217 -0
  105. package/src/Toast/Toast.tsx +224 -0
  106. package/src/Toast/ToastRegion.tsx +98 -0
  107. package/src/Toast/toastLifecycle.ts +45 -0
  108. package/src/Tooltip/Tooltip.mdx +69 -0
  109. package/src/Tooltip/Tooltip.stories.tsx +104 -0
  110. package/src/Tooltip/Tooltip.test.tsx +138 -0
  111. package/src/Tooltip/Tooltip.tsx +193 -0
  112. package/src/index.ts +47 -0
  113. package/src/mountStory.testHelpers.ts +30 -0
  114. package/src/slotProps.ts +143 -0
  115. package/src/sourceRules.test.ts +221 -30
@@ -0,0 +1,103 @@
1
+ import { Canvas, Controls, Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ import * as MenuStories from "./Menu.stories";
4
+
5
+ <Meta of={MenuStories} />
6
+
7
+ # Menu
8
+
9
+ A menu is **not** a listbox, and mux-magic's `TypePicker` is the fleet's one attempt at
10
+ telling them apart — it renders `role="menu"` over items that set a value, which is a
11
+ listbox wearing the wrong role.
12
+
13
+ <Canvas of={MenuStories.Default} />
14
+
15
+ ## Props
16
+
17
+ <Controls />
18
+
19
+ ## `menuitem` versus `option`
20
+
21
+ | | `menuitem` | `option` |
22
+ | --- | --- | --- |
23
+ | What it is | an **action** — Retry, Skip, Eject | a **value** you are choosing |
24
+ | Announced as | "menu, 3 items" | "listbox, selected, 2 of 4" |
25
+ | Leaves state behind | no | yes |
26
+ | Lives in | this component | `Select` |
27
+
28
+ The last row is why it matters to an agent driving the app: after choosing an `option` it
29
+ needs to know what the control now holds. After invoking a `menuitem` there is nothing to
30
+ read back. Getting the role wrong is invisible in a browser and invisible to axe.
31
+
32
+ ## A `[popover]` that was never shown is `display: none`
33
+
34
+ The panel calls `showPopover()` from its ref callback, like `Popover` does — plus an
35
+ `isConnected` guard and an effect that `Popover` does not need.
36
+
37
+ `FloatingFocusManager` renders a pair of focus guards around its child, so React commits this
38
+ subtree in an order where the ref can fire while the node is still **detached**.
39
+ `showPopover()` on a disconnected element throws `InvalidStateError`, which surfaces as an
40
+ unhandled rejection rather than a failing assertion — and if it is simply swallowed, the menu
41
+ renders, typechecks, and is invisible to `getByRole` and to a user alike.
42
+
43
+ The effect is the backstop, because effects run after the tree is connected. Both are
44
+ idempotent: `:popover-open` is the same question.
45
+
46
+ ## There is no `label` prop, and that corrects a lesson
47
+
48
+ `Spinner` (M3), `Popover` and `Modal` (M4) all take a **required** name, because a
49
+ `role="status"` and a `role="dialog"` take none from their content. The obvious move here was
50
+ a `label` prop doing the same job — it was written that way first, and then measured as
51
+ doing **nothing**.
52
+
53
+ `useRole(context, { role: "menu" })` puts `aria-labelledby` on the panel pointing at the
54
+ trigger, and `aria-labelledby` beats `aria-label`. The menu was already named "Bay 3", from
55
+ the button, and the prop was silently discarded. That is also the APG menu-button pattern.
56
+
57
+ So the rule the other three teach is narrower than it looked:
58
+
59
+ > An overlay with **no trigger relationship** needs its own name.
60
+
61
+ A menu has one, so the requirement moves to the trigger — which `Button` and `IconButton`
62
+ already enforce.
63
+
64
+ ## One kind, and that is the other half of the `Tabs` argument
65
+
66
+ `Tabs` needed **two** state kinds because a tab is chosen *and* focused, and in `manual`
67
+ activation those two disagree on purpose. A menu needs **one** — `RovingFocus` — because a
68
+ menu item is never selected, only invoked.
69
+
70
+ Two components, opposite conclusions, same model. That is the composition claim being paid
71
+ off rather than asserted.
72
+
73
+ <Canvas of={MenuStories.AllVariants} />
74
+
75
+ ## Focus moves in, once
76
+
77
+ A menu is one of the few patterns where focus must jump on open, or the keyboard user who
78
+ pressed Enter is still on the trigger with a menu they cannot reach.
79
+
80
+ It happens in the panel's own effect rather than through `FloatingFocusManager`'s
81
+ `initialFocus`, because the element to focus is whichever one `RovingFocus` says is active —
82
+ and that is not known until the items have registered. `initialFocus={-1}` turns the
83
+ manager's own aim off, so there is one owner for the caret.
84
+
85
+ `modal={false}`, so Tab **leaves** the menu and lands on the next control in the page, which
86
+ is what closes it. A trapped menu is a dialog.
87
+
88
+ <Canvas of={MenuStories.AllStates} />
89
+
90
+ ## Registration is membership
91
+
92
+ A disabled item never registers with the roving group, so the arrow keys skip it with
93
+ nothing in `RovingFocus` knowing what "disabled" means. It stays in the DOM and stays
94
+ announced — "not right now", rather than "does not exist".
95
+
96
+ ## No type-ahead, deliberately
97
+
98
+ The APG lists type-ahead as **optional** for a menu and **required** for a listbox, and none
99
+ of the fleet's menus are long enough to need it — the largest has six items. Adding it means
100
+ a keystroke buffer and a timer in a component that has neither, for a menu you can cross in
101
+ three arrow presses.
102
+
103
+ <Canvas of={MenuStories.Interactive} />
@@ -0,0 +1,161 @@
1
+ import { useVisibility } from "@charcuterie/logic"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import type { ReactNode } from "react"
4
+
5
+ import { placementArgType } from "../argTypes.storyHelpers.ts"
6
+ import { Button } from "../Button/Button.tsx"
7
+ import {
8
+ StoryCell,
9
+ StoryGrid,
10
+ } from "../board.storyHelpers.tsx"
11
+ import {
12
+ RedoIcon,
13
+ SettingsIcon,
14
+ UndoIcon,
15
+ } from "../icons.storyHelpers.tsx"
16
+ import type { MenuItem } from "./Menu.tsx"
17
+ import { Menu } from "./Menu.tsx"
18
+
19
+ const noop = () => undefined
20
+
21
+ const BAY_ACTIONS: MenuItem[] = [
22
+ {
23
+ icon: <UndoIcon />,
24
+ key: "retry",
25
+ label: "Retry title",
26
+ onSelect: noop,
27
+ },
28
+ {
29
+ icon: <RedoIcon />,
30
+ key: "skip",
31
+ label: "Skip title",
32
+ onSelect: noop,
33
+ },
34
+ {
35
+ icon: <SettingsIcon />,
36
+ isDisabled: true,
37
+ key: "eject",
38
+ label: "Eject disc",
39
+ onSelect: noop,
40
+ },
41
+ ]
42
+
43
+ /**
44
+ * A menu's visibility belongs to the app, exactly like `Popover`'s —
45
+ * so the stories hold it in a `useVisibility`, which is what a
46
+ * consumer writes too.
47
+ */
48
+ const MenuHarness = ({
49
+ isInitiallyVisible = false,
50
+ items = BAY_ACTIONS,
51
+ triggerLabel,
52
+ }: {
53
+ isInitiallyVisible?: boolean
54
+ items?: MenuItem[]
55
+ triggerLabel: string
56
+ }): ReactNode => {
57
+ const { hide, isVisible, toggle } = useVisibility({
58
+ isVisible: isInitiallyVisible,
59
+ })
60
+
61
+ return (
62
+ <Menu
63
+ isVisible={isVisible}
64
+ items={items}
65
+ onDismiss={hide}
66
+ trigger={
67
+ <Button appearance="outline" onClick={toggle}>
68
+ {triggerLabel}
69
+ </Button>
70
+ }
71
+ />
72
+ )
73
+ }
74
+
75
+ const meta = {
76
+ title: "Components/Menu",
77
+ component: Menu,
78
+ parameters: { layout: "centered" },
79
+ argTypes: { placement: placementArgType },
80
+ args: {
81
+ items: BAY_ACTIONS,
82
+ placement: "bottom-start",
83
+ },
84
+ } satisfies Meta<typeof Menu>
85
+
86
+ export default meta
87
+
88
+ type Story = StoryObj<typeof meta>
89
+
90
+ export const Default: Story = {
91
+ args: {
92
+ isVisible: false,
93
+ onDismiss: noop,
94
+ trigger: <Button appearance="outline">Actions</Button>,
95
+ },
96
+ render: () => <MenuHarness triggerLabel="Actions" />,
97
+ }
98
+
99
+ /**
100
+ * A `menuitem` **does** something; an `option` **is** something. A
101
+ * screen reader announces "menu, 3 items" for one and "listbox,
102
+ * selected, 2 of 4" for the other, and mux-magic's `TypePicker`
103
+ * renders `role="menu"` over items that set a value — a listbox
104
+ * wearing the wrong role.
105
+ */
106
+ export const AllVariants: Story = {
107
+ args: {
108
+ isVisible: false,
109
+ onDismiss: noop,
110
+ trigger: <Button appearance="outline">Actions</Button>,
111
+ },
112
+ render: () => (
113
+ <StoryGrid columns={2}>
114
+ <StoryCell label="closed">
115
+ <MenuHarness triggerLabel="Bay 1" />
116
+ </StoryCell>
117
+
118
+ <StoryCell label="open">
119
+ <MenuHarness
120
+ isInitiallyVisible
121
+ triggerLabel="Bay 2"
122
+ />
123
+ </StoryCell>
124
+ </StoryGrid>
125
+ ),
126
+ }
127
+
128
+ /**
129
+ * A disabled item is simply **not registered** with the roving
130
+ * group, so the arrow keys skip it without any command in
131
+ * `RovingFocus` knowing what "disabled" means. It stays in the DOM
132
+ * and stays announced — the difference between "not right now" and
133
+ * "does not exist".
134
+ */
135
+ export const AllStates: Story = {
136
+ args: {
137
+ isVisible: false,
138
+ onDismiss: noop,
139
+ trigger: <Button appearance="outline">Actions</Button>,
140
+ },
141
+ render: () => (
142
+ <MenuHarness isInitiallyVisible triggerLabel="Bay 3" />
143
+ ),
144
+ }
145
+
146
+ /**
147
+ * Open it and arrow down. Focus lands on the first item **on
148
+ * opening** — a menu that leaves the caret on the trigger is one a
149
+ * keyboard user cannot reach — and Escape closes it without the
150
+ * page moving.
151
+ */
152
+ export const Interactive: Story = {
153
+ args: {
154
+ isVisible: false,
155
+ onDismiss: noop,
156
+ trigger: <Button appearance="outline">Actions</Button>,
157
+ },
158
+ render: () => (
159
+ <MenuHarness triggerLabel="Open bay 4 menu" />
160
+ ),
161
+ }
@@ -0,0 +1,202 @@
1
+ import { composeStories } from "@storybook/react"
2
+ import { expect, userEvent, waitFor } from "storybook/test"
3
+ import { test } from "vitest"
4
+
5
+ import { expectNoAxeViolations } from "../expectNoAxeViolations.testHelpers.ts"
6
+ import { mountStory } from "../mountStory.testHelpers.ts"
7
+ import { expectAgentDrivable } from "../testing/index.ts"
8
+ import * as stories from "./Menu.stories.tsx"
9
+
10
+ const { AllStates, AllVariants, Interactive } =
11
+ composeStories(stories)
12
+
13
+ test("the menu is named, and its items are menuitems", async () => {
14
+ const { canvas, canvasElement } =
15
+ await mountStory(AllStates)
16
+
17
+ // **Named by its trigger**, not by a `label` prop. `useRole` puts
18
+ // `aria-labelledby` on the panel pointing at the button, and
19
+ // `aria-labelledby` beats `aria-label` — so the `label` prop this
20
+ // component was first written with did nothing at all, silently.
21
+ //
22
+ // That narrows the M3/M4 lesson rather than contradicting it: an
23
+ // overlay with **no trigger relationship** (`Spinner`'s
24
+ // `role="status"`, `Popover`'s `role="dialog"`) needs its own
25
+ // name. A menu has one.
26
+ expectAgentDrivable(canvas, {
27
+ name: "Bay 3",
28
+ role: "menu",
29
+ })
30
+
31
+ expectAgentDrivable(canvas, {
32
+ name: "Retry title",
33
+ role: "menuitem",
34
+ })
35
+
36
+ await expectNoAxeViolations(canvasElement)
37
+ })
38
+
39
+ /**
40
+ * A menu is one of the few patterns where focus must jump on open —
41
+ * otherwise the keyboard user who pressed Enter on the trigger is
42
+ * still on the trigger, with a menu they cannot reach.
43
+ */
44
+ test("opening moves focus into the menu", async () => {
45
+ const { canvas } = await mountStory(Interactive)
46
+
47
+ await userEvent.click(
48
+ expectAgentDrivable(canvas, {
49
+ name: "Open bay 4 menu",
50
+ role: "button",
51
+ }),
52
+ )
53
+
54
+ await waitFor(() => {
55
+ expect(
56
+ expectAgentDrivable(canvas, {
57
+ name: "Retry title",
58
+ role: "menuitem",
59
+ }),
60
+ ).toHaveFocus()
61
+ })
62
+ })
63
+
64
+ test("the arrow keys move within the menu and skip the disabled item", async () => {
65
+ const { canvas } = await mountStory(AllStates)
66
+
67
+ const skip = expectAgentDrivable(canvas, {
68
+ name: "Skip title",
69
+ role: "menuitem",
70
+ })
71
+
72
+ const eject = expectAgentDrivable(canvas, {
73
+ name: "Eject disc",
74
+ role: "menuitem",
75
+ })
76
+
77
+ await expect(eject).toBeDisabled()
78
+
79
+ // Not a click to get started — **choosing dismisses the menu**, so
80
+ // clicking an item to "focus" it closes the thing under test. This
81
+ // story opens already visible, and opening is what puts focus on
82
+ // the first item.
83
+ const retry = expectAgentDrivable(canvas, {
84
+ name: "Retry title",
85
+ role: "menuitem",
86
+ })
87
+
88
+ await waitFor(() => {
89
+ expect(retry).toHaveFocus()
90
+ })
91
+
92
+ await userEvent.keyboard("{ArrowDown}")
93
+
94
+ await waitFor(() => {
95
+ expect(skip).toHaveFocus()
96
+ })
97
+
98
+ // Registration is membership: the disabled item never joined the
99
+ // roving group, so wrapping goes past it back to the first.
100
+ await userEvent.keyboard("{ArrowDown}")
101
+
102
+ await waitFor(() => {
103
+ expect(retry).toHaveFocus()
104
+ })
105
+
106
+ await expect(eject).not.toHaveFocus()
107
+ })
108
+
109
+ /**
110
+ * Exactly one tab stop for the whole menu — the roving-tabindex
111
+ * rule, read straight from `selectTabIndex`. `expectAgentDrivable`
112
+ * asserts it for every composite member it is handed; this states it
113
+ * outright because a menu's members all carry `tabindex="-1"` and
114
+ * that is the pattern, not a bug.
115
+ */
116
+ test("the menu has one tab stop", async () => {
117
+ const { canvas } = await mountStory(AllStates)
118
+
119
+ const menu = expectAgentDrivable(canvas, {
120
+ name: "Bay 3",
121
+ role: "menu",
122
+ })
123
+
124
+ const tabbable = Array.from(
125
+ menu.querySelectorAll<HTMLButtonElement>(
126
+ '[role="menuitem"]',
127
+ ),
128
+ ).filter((one) => one.tabIndex === 0)
129
+
130
+ await expect(tabbable).toHaveLength(1)
131
+ })
132
+
133
+ test("choosing an item dismisses the menu", async () => {
134
+ const { canvas } = await mountStory(Interactive)
135
+
136
+ await userEvent.click(
137
+ expectAgentDrivable(canvas, {
138
+ name: "Open bay 4 menu",
139
+ role: "button",
140
+ }),
141
+ )
142
+
143
+ await userEvent.click(
144
+ expectAgentDrivable(canvas, {
145
+ name: "Retry title",
146
+ role: "menuitem",
147
+ }),
148
+ )
149
+
150
+ // A menu that stays open after an action has fired is the one
151
+ // interaction users read as "it didn't work".
152
+ await waitFor(() => {
153
+ expect(canvas.queryAllByRole("menuitem")).toHaveLength(
154
+ 0,
155
+ )
156
+ })
157
+ })
158
+
159
+ test("Escape dismisses without choosing", async () => {
160
+ const { canvas } = await mountStory(Interactive)
161
+
162
+ await userEvent.click(
163
+ expectAgentDrivable(canvas, {
164
+ name: "Open bay 4 menu",
165
+ role: "button",
166
+ }),
167
+ )
168
+
169
+ await waitFor(() => {
170
+ expect(
171
+ canvas.queryAllByRole("menuitem").length,
172
+ ).toBeGreaterThan(0)
173
+ })
174
+
175
+ await userEvent.keyboard("{Escape}")
176
+
177
+ await waitFor(() => {
178
+ expect(canvas.queryAllByRole("menuitem")).toHaveLength(
179
+ 0,
180
+ )
181
+ })
182
+ })
183
+
184
+ /**
185
+ * The panel is in the top layer via `popover="manual"` and **stays
186
+ * in the DOM where it was written** — so a scoped query still finds
187
+ * it. A portalled menu would move to `document.body` and every story
188
+ * here would have to reach for `screen`.
189
+ */
190
+ test("the menu is not portalled out of its canvas", async () => {
191
+ const { canvas, canvasElement } =
192
+ await mountStory(AllVariants)
193
+
194
+ const menu = expectAgentDrivable(canvas, {
195
+ name: "Bay 2",
196
+ role: "menu",
197
+ })
198
+
199
+ await expect(canvasElement).toContainElement(menu)
200
+
201
+ await expectNoAxeViolations(canvasElement)
202
+ })