@estiva-app/ui 0.9.0 → 0.10.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/dist/Avatar.d.ts +13 -1
- package/dist/Avatar.d.ts.map +1 -1
- package/dist/AvatarGroup.d.ts.map +1 -1
- package/dist/Banner.d.ts +16 -1
- package/dist/Banner.d.ts.map +1 -1
- package/dist/Breadcrumb.d.ts +9 -0
- package/dist/Breadcrumb.d.ts.map +1 -1
- package/dist/Button.d.ts +12 -2
- package/dist/Button.d.ts.map +1 -1
- package/dist/Checkbox.d.ts +2 -0
- package/dist/Checkbox.d.ts.map +1 -1
- package/dist/Chip.d.ts +3 -2
- package/dist/Chip.d.ts.map +1 -1
- package/dist/ChipInput.d.ts +3 -1
- package/dist/ChipInput.d.ts.map +1 -1
- package/dist/ConfirmDialog.d.ts +5 -0
- package/dist/ConfirmDialog.d.ts.map +1 -1
- package/dist/DialogShell.d.ts +27 -3
- package/dist/DialogShell.d.ts.map +1 -1
- package/dist/EditableText.d.ts +6 -0
- package/dist/EditableText.d.ts.map +1 -1
- package/dist/Field.d.ts +42 -25
- package/dist/Field.d.ts.map +1 -1
- package/dist/IconButton.d.ts +3 -2
- package/dist/IconButton.d.ts.map +1 -1
- package/dist/IdentityMenu.d.ts +25 -13
- package/dist/IdentityMenu.d.ts.map +1 -1
- package/dist/Menu.d.ts +65 -82
- package/dist/Menu.d.ts.map +1 -1
- package/dist/PersonTrigger.d.ts.map +1 -1
- package/dist/Popover.d.ts +86 -0
- package/dist/Popover.d.ts.map +1 -0
- package/dist/PreviewCard.d.ts +39 -0
- package/dist/PreviewCard.d.ts.map +1 -0
- package/dist/Reaction.d.ts.map +1 -1
- package/dist/ReactionPicker.d.ts +55 -0
- package/dist/ReactionPicker.d.ts.map +1 -0
- package/dist/SearchInput.d.ts +5 -0
- package/dist/SearchInput.d.ts.map +1 -1
- package/dist/Select.d.ts +26 -16
- package/dist/Select.d.ts.map +1 -1
- package/dist/Tabs.d.ts +11 -1
- package/dist/Tabs.d.ts.map +1 -1
- package/dist/TextInput.d.ts +7 -1
- package/dist/TextInput.d.ts.map +1 -1
- package/dist/Textarea.d.ts +8 -1
- package/dist/Textarea.d.ts.map +1 -1
- package/dist/Toolbar.d.ts +95 -0
- package/dist/Toolbar.d.ts.map +1 -0
- package/dist/Tooltip.d.ts +36 -10
- package/dist/Tooltip.d.ts.map +1 -1
- package/dist/cn.d.ts.map +1 -1
- package/dist/fit.d.ts +7 -72
- package/dist/fit.d.ts.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +882 -763
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/src/Avatar.mdx +17 -0
- package/src/Avatar.name.test.tsx +99 -0
- package/src/Avatar.tsx +25 -3
- package/src/AvatarGroup.tsx +4 -1
- package/src/Banner.mdx +10 -2
- package/src/Banner.stories.tsx +29 -0
- package/src/Banner.test.tsx +60 -0
- package/src/Banner.tsx +35 -3
- package/src/Breadcrumb.mdx +5 -2
- package/src/Breadcrumb.test.tsx +44 -0
- package/src/Breadcrumb.tsx +10 -2
- package/src/Button.compose.test.tsx +119 -0
- package/src/Button.mdx +7 -5
- package/src/Button.test.tsx +2 -1
- package/src/Button.tsx +21 -5
- package/src/Checkbox.tsx +3 -0
- package/src/Chip.tsx +3 -2
- package/src/ChipInput.tsx +4 -0
- package/src/ConfirmDialog.mdx +20 -1
- package/src/ConfirmDialog.test.tsx +109 -0
- package/src/ConfirmDialog.tsx +6 -0
- package/src/DialogShell.mdx +18 -4
- package/src/DialogShell.stories.tsx +56 -6
- package/src/DialogShell.test.tsx +184 -0
- package/src/DialogShell.tsx +108 -39
- package/src/EditableText.mdx +6 -1
- package/src/EditableText.test.tsx +156 -0
- package/src/EditableText.tsx +23 -5
- package/src/Field.mdx +23 -3
- package/src/Field.stories.tsx +35 -0
- package/src/Field.test.tsx +117 -27
- package/src/Field.tsx +86 -58
- package/src/IconButton.mdx +6 -2
- package/src/IconButton.test.tsx +5 -2
- package/src/IconButton.tsx +12 -6
- package/src/IdentityMenu.mdx +29 -6
- package/src/IdentityMenu.stories.tsx +41 -6
- package/src/IdentityMenu.tsx +74 -47
- package/src/Kbd.stories.tsx +3 -3
- package/src/Menu.mdx +88 -75
- package/src/Menu.stories.tsx +120 -52
- package/src/Menu.test.tsx +315 -0
- package/src/Menu.tsx +341 -251
- package/src/MenuItem.stories.tsx +135 -98
- package/src/NavItem.mdx +1 -1
- package/src/PersonTrigger.mdx +20 -14
- package/src/PersonTrigger.tsx +23 -1
- package/src/Popover.mdx +130 -0
- package/src/Popover.stories.tsx +222 -0
- package/src/Popover.test.tsx +188 -0
- package/src/Popover.tsx +153 -0
- package/src/PreviewCard.mdx +98 -0
- package/src/PreviewCard.stories.tsx +168 -0
- package/src/PreviewCard.test.tsx +83 -0
- package/src/PreviewCard.tsx +91 -0
- package/src/Rail.mdx +63 -0
- package/src/Rail.stories.tsx +81 -0
- package/src/Reaction.mdx +4 -2
- package/src/Reaction.tsx +4 -2
- package/src/ReactionPicker.mdx +85 -0
- package/src/ReactionPicker.stories.tsx +120 -0
- package/src/ReactionPicker.test.tsx +118 -0
- package/src/ReactionPicker.tsx +88 -0
- package/src/SearchInput.mdx +6 -2
- package/src/SearchInput.tsx +7 -1
- package/src/Select.mdx +16 -5
- package/src/Select.test.tsx +157 -0
- package/src/Select.tsx +89 -214
- package/src/Sidebar.mdx +6 -2
- package/src/Tabs.mdx +5 -0
- package/src/Tabs.test.tsx +15 -0
- package/src/Tabs.tsx +17 -2
- package/src/TextInput.mdx +8 -2
- package/src/TextInput.tsx +10 -7
- package/src/Textarea.mdx +6 -2
- package/src/Textarea.tsx +14 -9
- package/src/Toolbar.mdx +104 -0
- package/src/Toolbar.stories.tsx +142 -0
- package/src/Toolbar.test.tsx +198 -0
- package/src/Toolbar.tsx +158 -0
- package/src/Tooltip.mdx +43 -5
- package/src/Tooltip.stories.tsx +26 -0
- package/src/Tooltip.test.tsx +195 -0
- package/src/Tooltip.tsx +150 -48
- package/src/cn.ts +1 -1
- package/src/fit.test.ts +101 -0
- package/src/fit.ts +19 -63
- package/src/index.ts +6 -3
- package/stories/Choosing.mdx +8 -3
- package/tailwind-preset.js +7 -0
- package/src/Menu.fit.test.ts +0 -90
- package/src/Select.fit.test.ts +0 -101
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* What the Menu page claims, pinned.
|
|
4
|
+
*
|
|
5
|
+
* The shell had no component test — only `Menu.fit.test.ts`, which tested the
|
|
6
|
+
* pure geometry rather than the menu. Placement is Floating UI's now and is
|
|
7
|
+
* measured in a browser (jsdom lays nothing out); what is testable here is the
|
|
8
|
+
* behaviour, and stage 4 is where most of it started existing.
|
|
9
|
+
*/
|
|
10
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
11
|
+
import { cleanup, render, screen, waitFor } from '@testing-library/react'
|
|
12
|
+
import userEvent from '@testing-library/user-event'
|
|
13
|
+
import { Button } from './Button'
|
|
14
|
+
import { Menu, MenuItem, MenuPanel, MenuSection, MenuSub } from './Menu'
|
|
15
|
+
|
|
16
|
+
afterEach(cleanup)
|
|
17
|
+
|
|
18
|
+
/** A trigger and the menu it opens — which is now the whole of a caller's
|
|
19
|
+
* code: no open state, no anchor, no dismiss. */
|
|
20
|
+
function Basic({ onOpenChange }: { onOpenChange?: (open: boolean) => void }) {
|
|
21
|
+
return (
|
|
22
|
+
<Menu trigger={<Button variant="outlined">Trigger</Button>} onOpenChange={onOpenChange}>
|
|
23
|
+
<MenuItem label="Rename" onClick={() => {}} />
|
|
24
|
+
<MenuItem label="Duplicate" onClick={() => {}} />
|
|
25
|
+
<MenuItem label="Delete" destructive onClick={() => {}} />
|
|
26
|
+
</Menu>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('Menu', () => {
|
|
31
|
+
it('opens from its own trigger, and the rows are menu items', async () => {
|
|
32
|
+
const user = userEvent.setup()
|
|
33
|
+
render(<Basic />)
|
|
34
|
+
await user.click(screen.getByRole('button', { name: 'Trigger' }))
|
|
35
|
+
expect(await screen.findByRole('menu')).toBeTruthy()
|
|
36
|
+
const items = screen.getAllByRole('menuitem')
|
|
37
|
+
expect(items).toHaveLength(3)
|
|
38
|
+
// `render` keeps the <button> the design was drawn with; the part would
|
|
39
|
+
// otherwise draw a <div>.
|
|
40
|
+
expect(items.every((i) => i.tagName === 'BUTTON')).toBe(true)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('the trigger says whether its menu is open', async () => {
|
|
44
|
+
const user = userEvent.setup()
|
|
45
|
+
render(<Basic />)
|
|
46
|
+
const trigger = screen.getByRole('button', { name: 'Trigger' })
|
|
47
|
+
expect(trigger.getAttribute('aria-expanded')).toBe('false')
|
|
48
|
+
await user.click(trigger)
|
|
49
|
+
await screen.findByRole('menu')
|
|
50
|
+
expect(trigger.getAttribute('aria-expanded')).toBe('true')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('portals: the menu is not inside the element that rendered it', async () => {
|
|
54
|
+
const user = userEvent.setup()
|
|
55
|
+
const { container } = render(<Basic />)
|
|
56
|
+
await user.click(screen.getByRole('button', { name: 'Trigger' }))
|
|
57
|
+
const menu = await screen.findByRole('menu')
|
|
58
|
+
expect(container.contains(menu)).toBe(false)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('closes on Escape and gives focus back to the trigger', async () => {
|
|
62
|
+
const user = userEvent.setup()
|
|
63
|
+
render(<Basic />)
|
|
64
|
+
const trigger = screen.getByRole('button', { name: 'Trigger' })
|
|
65
|
+
await user.click(trigger)
|
|
66
|
+
await screen.findByRole('menu')
|
|
67
|
+
await user.keyboard('{Escape}')
|
|
68
|
+
await waitFor(() => expect(screen.queryByRole('menu')).toBeNull())
|
|
69
|
+
expect(document.activeElement).toBe(trigger)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('closes on a press outside', async () => {
|
|
73
|
+
const user = userEvent.setup()
|
|
74
|
+
render(
|
|
75
|
+
<>
|
|
76
|
+
<Basic />
|
|
77
|
+
<button type="button">Elsewhere</button>
|
|
78
|
+
</>,
|
|
79
|
+
)
|
|
80
|
+
await user.click(screen.getByRole('button', { name: 'Trigger' }))
|
|
81
|
+
await screen.findByRole('menu')
|
|
82
|
+
await user.click(screen.getByRole('button', { name: 'Elsewhere' }))
|
|
83
|
+
await waitFor(() => expect(screen.queryByRole('menu')).toBeNull())
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The trigger is the menu's own, so a press on it toggles. This used to be a
|
|
88
|
+
* trap: the press dismissed the menu and the caller's `onClick` reopened it
|
|
89
|
+
* in the same gesture, and the workaround was `onMouseDown` with
|
|
90
|
+
* `stopPropagation`. There is no caller `onClick` any more.
|
|
91
|
+
*/
|
|
92
|
+
it('a second press on the trigger closes it', async () => {
|
|
93
|
+
const user = userEvent.setup()
|
|
94
|
+
render(<Basic />)
|
|
95
|
+
const trigger = screen.getByRole('button', { name: 'Trigger' })
|
|
96
|
+
await user.click(trigger)
|
|
97
|
+
await screen.findByRole('menu')
|
|
98
|
+
await user.click(trigger)
|
|
99
|
+
await waitFor(() => expect(screen.queryByRole('menu')).toBeNull())
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('reports opening and closing to a caller that asks', async () => {
|
|
103
|
+
const user = userEvent.setup()
|
|
104
|
+
const onOpenChange = vi.fn()
|
|
105
|
+
render(<Basic onOpenChange={onOpenChange} />)
|
|
106
|
+
await user.click(screen.getByRole('button', { name: 'Trigger' }))
|
|
107
|
+
await screen.findByRole('menu')
|
|
108
|
+
expect(onOpenChange).toHaveBeenCalledWith(true)
|
|
109
|
+
await user.keyboard('{Escape}')
|
|
110
|
+
expect(onOpenChange).toHaveBeenCalledWith(false)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('choosing a row runs its handler and closes the menu', async () => {
|
|
114
|
+
const user = userEvent.setup()
|
|
115
|
+
const onPick = vi.fn()
|
|
116
|
+
render(
|
|
117
|
+
<Menu trigger={<Button variant="outlined">Trigger</Button>}>
|
|
118
|
+
<MenuItem label="Rename" onClick={onPick} />
|
|
119
|
+
</Menu>,
|
|
120
|
+
)
|
|
121
|
+
await user.click(screen.getByRole('button', { name: 'Trigger' }))
|
|
122
|
+
await user.click(await screen.findByRole('menuitem', { name: 'Rename' }))
|
|
123
|
+
expect(onPick).toHaveBeenCalledTimes(1)
|
|
124
|
+
await waitFor(() => expect(screen.queryByRole('menu')).toBeNull())
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('a section is a labelled group, so its rows are announced together', async () => {
|
|
128
|
+
const user = userEvent.setup()
|
|
129
|
+
render(
|
|
130
|
+
<Menu trigger={<Button variant="outlined">Trigger</Button>}>
|
|
131
|
+
<MenuSection label="Section">
|
|
132
|
+
<MenuItem label="Item one" onClick={() => {}} />
|
|
133
|
+
<MenuItem label="Item two" onClick={() => {}} />
|
|
134
|
+
</MenuSection>
|
|
135
|
+
</Menu>,
|
|
136
|
+
)
|
|
137
|
+
await user.click(screen.getByRole('button', { name: 'Trigger' }))
|
|
138
|
+
const group = await screen.findByRole('group')
|
|
139
|
+
expect(group.textContent).toContain('Section')
|
|
140
|
+
expect(group.querySelectorAll('[role="menuitem"]')).toHaveLength(2)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The Keys table, pinned. Every row of it is Base UI's, and none of it
|
|
145
|
+
* existed before stage 4 — which is exactly why it is tested here: a
|
|
146
|
+
* behaviour nobody wrote is a behaviour nobody notices losing.
|
|
147
|
+
*
|
|
148
|
+
* `data-highlighted` is the assertion because it is what the row's fill
|
|
149
|
+
* keys off, so a test that passes and a menu that looks wrong cannot come
|
|
150
|
+
* apart.
|
|
151
|
+
*/
|
|
152
|
+
const highlighted = () => screen.getAllByRole('menuitem').find((i) => i.hasAttribute('data-highlighted'))?.textContent
|
|
153
|
+
|
|
154
|
+
it('opens from the keyboard with the first row highlighted', async () => {
|
|
155
|
+
const user = userEvent.setup()
|
|
156
|
+
render(<Basic />)
|
|
157
|
+
screen.getByRole('button', { name: 'Trigger' }).focus()
|
|
158
|
+
await user.keyboard('{Enter}')
|
|
159
|
+
await screen.findByRole('menu')
|
|
160
|
+
expect(highlighted()).toBe('Rename')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('the arrow keys walk the rows and wrap at both ends', async () => {
|
|
164
|
+
const user = userEvent.setup()
|
|
165
|
+
render(<Basic />)
|
|
166
|
+
screen.getByRole('button', { name: 'Trigger' }).focus()
|
|
167
|
+
await user.keyboard('{Enter}')
|
|
168
|
+
await screen.findByRole('menu')
|
|
169
|
+
await user.keyboard('{ArrowDown}')
|
|
170
|
+
expect(highlighted()).toBe('Duplicate')
|
|
171
|
+
await user.keyboard('{ArrowDown}')
|
|
172
|
+
expect(highlighted()).toBe('Delete')
|
|
173
|
+
await user.keyboard('{ArrowDown}')
|
|
174
|
+
expect(highlighted()).toBe('Rename')
|
|
175
|
+
await user.keyboard('{ArrowUp}')
|
|
176
|
+
expect(highlighted()).toBe('Delete')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('Home and End jump to the ends', async () => {
|
|
180
|
+
const user = userEvent.setup()
|
|
181
|
+
render(<Basic />)
|
|
182
|
+
screen.getByRole('button', { name: 'Trigger' }).focus()
|
|
183
|
+
await user.keyboard('{Enter}')
|
|
184
|
+
await screen.findByRole('menu')
|
|
185
|
+
await user.keyboard('{End}')
|
|
186
|
+
expect(highlighted()).toBe('Delete')
|
|
187
|
+
await user.keyboard('{Home}')
|
|
188
|
+
expect(highlighted()).toBe('Rename')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('typing a row’s first letters jumps to it', async () => {
|
|
192
|
+
const user = userEvent.setup()
|
|
193
|
+
render(<Basic />)
|
|
194
|
+
await user.click(screen.getByRole('button', { name: 'Trigger' }))
|
|
195
|
+
await screen.findByRole('menu')
|
|
196
|
+
await user.keyboard('de')
|
|
197
|
+
expect(highlighted()).toBe('Delete')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('Enter on the highlighted row activates it and closes the menu', async () => {
|
|
201
|
+
const user = userEvent.setup()
|
|
202
|
+
const onPick = vi.fn()
|
|
203
|
+
render(
|
|
204
|
+
<Menu trigger={<Button variant="outlined">Trigger</Button>}>
|
|
205
|
+
<MenuItem label="Rename" onClick={onPick} />
|
|
206
|
+
<MenuItem label="Delete" destructive onClick={() => {}} />
|
|
207
|
+
</Menu>,
|
|
208
|
+
)
|
|
209
|
+
screen.getByRole('button', { name: 'Trigger' }).focus()
|
|
210
|
+
await user.keyboard('{Enter}')
|
|
211
|
+
await screen.findByRole('menu')
|
|
212
|
+
await user.keyboard('{Enter}')
|
|
213
|
+
expect(onPick).toHaveBeenCalledTimes(1)
|
|
214
|
+
await waitFor(() => expect(screen.queryByRole('menu')).toBeNull())
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* → opens the submenu **and takes the highlight into it**; the parent row
|
|
219
|
+
* gives it up. The page said the highlight stayed on the row and ↓ stepped
|
|
220
|
+
* in, which was written from Base UI's documentation rather than from a
|
|
221
|
+
* measurement, and is wrong in a browser and here (2026-09-08).
|
|
222
|
+
*/
|
|
223
|
+
it('→ opens a submenu onto its first row, and ← comes back', async () => {
|
|
224
|
+
const user = userEvent.setup()
|
|
225
|
+
render(
|
|
226
|
+
<Menu trigger={<Button variant="outlined">Trigger</Button>}>
|
|
227
|
+
<MenuItem label="Rename" onClick={() => {}} />
|
|
228
|
+
<MenuSub label="Move to…">
|
|
229
|
+
<MenuItem label="Item one" onClick={() => {}} />
|
|
230
|
+
<MenuItem label="Item two" onClick={() => {}} />
|
|
231
|
+
</MenuSub>
|
|
232
|
+
</Menu>,
|
|
233
|
+
)
|
|
234
|
+
screen.getByRole('button', { name: 'Trigger' }).focus()
|
|
235
|
+
await user.keyboard('{Enter}')
|
|
236
|
+
await screen.findByRole('menu')
|
|
237
|
+
await user.keyboard('{ArrowDown}')
|
|
238
|
+
expect(highlighted()).toBe('Move to…')
|
|
239
|
+
await user.keyboard('{ArrowRight}')
|
|
240
|
+
expect(await screen.findByRole('menuitem', { name: 'Item one' })).toBeTruthy()
|
|
241
|
+
expect(highlighted()).toBe('Item one')
|
|
242
|
+
await user.keyboard('{ArrowLeft}')
|
|
243
|
+
await waitFor(() => expect(screen.queryByRole('menuitem', { name: 'Item one' })).toBeNull())
|
|
244
|
+
expect(highlighted()).toBe('Move to…')
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('a submenu row says it opens one', async () => {
|
|
248
|
+
const user = userEvent.setup()
|
|
249
|
+
render(
|
|
250
|
+
<Menu trigger={<Button variant="outlined">Trigger</Button>}>
|
|
251
|
+
<MenuItem label="Rename" onClick={() => {}} />
|
|
252
|
+
<MenuSub label="Move to…">
|
|
253
|
+
<MenuItem label="Item one" onClick={() => {}} />
|
|
254
|
+
</MenuSub>
|
|
255
|
+
</Menu>,
|
|
256
|
+
)
|
|
257
|
+
await user.click(screen.getByRole('button', { name: 'Trigger' }))
|
|
258
|
+
await screen.findByRole('menu')
|
|
259
|
+
const sub = screen.getByRole('menuitem', { name: 'Move to…' })
|
|
260
|
+
expect(sub.getAttribute('aria-haspopup')).toBe('menu')
|
|
261
|
+
expect(sub.getAttribute('aria-expanded')).toBe('false')
|
|
262
|
+
expect(sub.tagName).toBe('BUTTON')
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The rows outside a menu. Peek's `@`, `/` and `[` pickers and its compose
|
|
268
|
+
* menu draw the surface without the behaviour, because the editor's suggestion
|
|
269
|
+
* plugin already owns the keyboard — so `MenuItem` and `MenuSection` have to
|
|
270
|
+
* work with no `Menu` above them.
|
|
271
|
+
*/
|
|
272
|
+
describe('rows on a bare MenuPanel', () => {
|
|
273
|
+
/**
|
|
274
|
+
* It is a button, and it does NOT claim to be a menu item. ARIA requires a
|
|
275
|
+
* `menuitem` to sit inside a `menu` or a `menubar`; `MenuPanel` is a `<div>`
|
|
276
|
+
* with no role, so the role used to make an orphan in all four of the Peek
|
|
277
|
+
* files that draw rows this way.
|
|
278
|
+
*/
|
|
279
|
+
it('MenuItem is a plain button outside a menu, claiming no menu role', () => {
|
|
280
|
+
render(
|
|
281
|
+
<MenuPanel>
|
|
282
|
+
<MenuItem label="Heading" shortcut="#" onClick={() => {}} />
|
|
283
|
+
</MenuPanel>,
|
|
284
|
+
)
|
|
285
|
+
const item = screen.getByRole('button', { name: /Heading/ })
|
|
286
|
+
expect(item.tagName).toBe('BUTTON')
|
|
287
|
+
expect(item.getAttribute('role')).toBeNull()
|
|
288
|
+
expect(screen.queryByRole('menuitem')).toBeNull()
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('its onClick still runs', async () => {
|
|
292
|
+
const user = userEvent.setup()
|
|
293
|
+
const onPick = vi.fn()
|
|
294
|
+
render(
|
|
295
|
+
<MenuPanel>
|
|
296
|
+
<MenuItem label="Heading" onClick={onPick} />
|
|
297
|
+
</MenuPanel>,
|
|
298
|
+
)
|
|
299
|
+
await user.click(screen.getByRole('button', { name: 'Heading' }))
|
|
300
|
+
expect(onPick).toHaveBeenCalledTimes(1)
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('MenuSection draws its heading and its rows, and is not a group', () => {
|
|
304
|
+
render(
|
|
305
|
+
<MenuPanel>
|
|
306
|
+
<MenuSection label="Section">
|
|
307
|
+
<MenuItem label="Heading" onClick={() => {}} />
|
|
308
|
+
</MenuSection>
|
|
309
|
+
</MenuPanel>,
|
|
310
|
+
)
|
|
311
|
+
expect(screen.getByText('Section')).toBeTruthy()
|
|
312
|
+
expect(screen.getAllByRole('button')).toHaveLength(1)
|
|
313
|
+
expect(screen.queryByRole('group')).toBeNull()
|
|
314
|
+
})
|
|
315
|
+
})
|