@duro-app/ui 5.2.0 → 5.4.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/{Table-C70zdSkM.js → Table-Cm5cCnZD.js} +730 -625
- package/dist/Table-Cm5cCnZD.js.map +1 -0
- package/dist/components/ActionBar/styles.css.d.ts +1 -1
- package/dist/components/ActionBar/styles.css.d.ts.map +1 -1
- package/dist/components/Badge/Badge.d.ts +1 -1
- package/dist/components/Badge/Badge.d.ts.map +1 -1
- package/dist/components/Badge/Badge.stories.d.ts +3 -0
- package/dist/components/Badge/Badge.stories.d.ts.map +1 -1
- package/dist/components/Badge/styles.css.d.ts +4 -0
- package/dist/components/Badge/styles.css.d.ts.map +1 -1
- package/dist/components/Button/Button.d.ts.map +1 -1
- package/dist/components/ButtonGroup/ButtonGroup.d.ts +9 -2
- package/dist/components/ButtonGroup/ButtonGroup.d.ts.map +1 -1
- package/dist/components/ButtonGroup/ButtonGroup.meta.d.ts.map +1 -1
- package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts +19 -0
- package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts.map +1 -1
- package/dist/components/ButtonGroup/ButtonGroupContext.d.ts +13 -0
- package/dist/components/ButtonGroup/ButtonGroupContext.d.ts.map +1 -0
- package/dist/components/ButtonGroup/attached.css.d.ts +34 -0
- package/dist/components/ButtonGroup/attached.css.d.ts.map +1 -0
- package/dist/components/ButtonGroup/styles.css.d.ts +7 -0
- package/dist/components/ButtonGroup/styles.css.d.ts.map +1 -1
- package/dist/components/ButtonGroup/useGroupedControl.d.ts +25 -0
- package/dist/components/ButtonGroup/useGroupedControl.d.ts.map +1 -0
- package/dist/components/Dialog/Dialog.d.ts.map +1 -1
- package/dist/components/Dialog/styles.css.d.ts +2 -2
- package/dist/components/Dialog/styles.css.d.ts.map +1 -1
- package/dist/components/DragDrop/DragDrop.d.ts.map +1 -1
- package/dist/components/DragDrop/styles.css.d.ts +1 -1
- package/dist/components/DragDrop/styles.css.d.ts.map +1 -1
- package/dist/components/Drawer/Drawer.d.ts.map +1 -1
- package/dist/components/Drawer/styles.css.d.ts +2 -2
- package/dist/components/Drawer/styles.css.d.ts.map +1 -1
- package/dist/components/Listbox/styles.css.d.ts +1 -1
- package/dist/components/Listbox/styles.css.d.ts.map +1 -1
- package/dist/components/Menu/Menu.d.ts.map +1 -1
- package/dist/components/Menu/styles.css.d.ts +1 -1
- package/dist/components/Menu/styles.css.d.ts.map +1 -1
- package/dist/components/Popover/Popover.d.ts +7 -2
- package/dist/components/Popover/Popover.d.ts.map +1 -1
- package/dist/components/Popover/styles.css.d.ts +4 -1
- package/dist/components/Popover/styles.css.d.ts.map +1 -1
- package/dist/components/ScrollArea/styles.css.d.ts +1 -1
- package/dist/components/ScrollArea/styles.css.d.ts.map +1 -1
- package/dist/components/Select/Select.d.ts.map +1 -1
- package/dist/components/Select/Select.stories.d.ts +2 -0
- package/dist/components/Select/Select.stories.d.ts.map +1 -1
- package/dist/components/Select/styles.css.d.ts +2 -2
- package/dist/components/Select/styles.css.d.ts.map +1 -1
- package/dist/components/Select/useSelectRoot.d.ts.map +1 -1
- package/dist/components/ThemeProvider/Layers.stories.d.ts +24 -0
- package/dist/components/ThemeProvider/Layers.stories.d.ts.map +1 -0
- package/dist/components/ThemeProvider/ThemeProvider.d.ts.map +1 -1
- package/dist/components/Toast/styles.css.d.ts +1 -0
- package/dist/components/Toast/styles.css.d.ts.map +1 -1
- package/dist/components/Toggle/Toggle.d.ts.map +1 -1
- package/dist/components/Toggle/styles.css.d.ts +3 -0
- package/dist/components/Toggle/styles.css.d.ts.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.d.ts.map +1 -1
- package/dist/components/ToggleGroup/styles.css.d.ts +5 -0
- package/dist/components/ToggleGroup/styles.css.d.ts.map +1 -1
- package/dist/components/Toolbar/ControlContextBoundary.d.ts +10 -0
- package/dist/components/Toolbar/ControlContextBoundary.d.ts.map +1 -0
- package/dist/components/Toolbar/Toolbar.d.ts +22 -0
- package/dist/components/Toolbar/Toolbar.d.ts.map +1 -0
- package/dist/components/Toolbar/Toolbar.meta.d.ts +3 -0
- package/dist/components/Toolbar/Toolbar.meta.d.ts.map +1 -0
- package/dist/components/Toolbar/Toolbar.stories.d.ts +28 -0
- package/dist/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
- package/dist/components/Toolbar/ToolbarContext.d.ts +10 -0
- package/dist/components/Toolbar/ToolbarContext.d.ts.map +1 -0
- package/dist/components/Toolbar/styles.css.d.ts +16 -0
- package/dist/components/Toolbar/styles.css.d.ts.map +1 -0
- package/dist/components/Tooltip/styles.css.d.ts +1 -1
- package/dist/components/Tooltip/styles.css.d.ts.map +1 -1
- package/dist/components/VirtualTable/styles.css.d.ts +1 -1
- package/dist/components/VirtualTable/styles.css.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3164 -2917
- package/dist/index.js.map +1 -1
- package/dist/shared/ModalContext.d.ts +14 -0
- package/dist/shared/ModalContext.d.ts.map +1 -0
- package/dist/shared/mergeRefs.d.ts +4 -0
- package/dist/shared/mergeRefs.d.ts.map +1 -0
- package/dist/shared/useOrderedRegistry.d.ts +26 -0
- package/dist/shared/useOrderedRegistry.d.ts.map +1 -0
- package/dist/table.js +45 -45
- package/dist/table.js.map +1 -1
- package/package.json +2 -2
- package/src/components/ActionBar/styles.css.ts +2 -1
- package/src/components/Badge/Badge.stories.tsx +62 -1
- package/src/components/Badge/Badge.tsx +1 -1
- package/src/components/Badge/styles.css.ts +5 -0
- package/src/components/Button/Button.tsx +7 -1
- package/src/components/ButtonGroup/ButtonGroup.meta.ts +14 -1
- package/src/components/ButtonGroup/ButtonGroup.stories.tsx +271 -1
- package/src/components/ButtonGroup/ButtonGroup.tsx +46 -14
- package/src/components/ButtonGroup/ButtonGroupContext.ts +19 -0
- package/src/components/ButtonGroup/attached.css.ts +44 -0
- package/src/components/ButtonGroup/styles.css.ts +8 -0
- package/src/components/ButtonGroup/useGroupedControl.ts +75 -0
- package/src/components/Dialog/Dialog.tsx +5 -1
- package/src/components/Dialog/styles.css.ts +3 -2
- package/src/components/DragDrop/DragDrop.tsx +15 -8
- package/src/components/DragDrop/styles.css.ts +4 -1
- package/src/components/Drawer/Drawer.tsx +5 -1
- package/src/components/Drawer/styles.css.ts +3 -2
- package/src/components/Listbox/styles.css.ts +2 -1
- package/src/components/Menu/Menu.tsx +10 -3
- package/src/components/Menu/styles.css.ts +2 -1
- package/src/components/Popover/Popover.tsx +26 -5
- package/src/components/Popover/styles.css.ts +9 -1
- package/src/components/ScrollArea/styles.css.ts +2 -1
- package/src/components/Select/Select.stories.tsx +20 -1
- package/src/components/Select/Select.tsx +9 -3
- package/src/components/Select/styles.css.ts +3 -2
- package/src/components/Select/useSelectRoot.ts +17 -0
- package/src/components/ThemeProvider/Layers.stories.tsx +379 -0
- package/src/components/ThemeProvider/ThemeProvider.tsx +4 -2
- package/src/components/Toast/styles.css.ts +3 -0
- package/src/components/Toggle/Toggle.tsx +20 -4
- package/src/components/Toggle/styles.css.ts +6 -0
- package/src/components/ToggleGroup/ToggleGroup.tsx +16 -4
- package/src/components/ToggleGroup/styles.css.ts +7 -0
- package/src/components/Toolbar/ControlContextBoundary.tsx +16 -0
- package/src/components/Toolbar/Toolbar.meta.ts +48 -0
- package/src/components/Toolbar/Toolbar.stories.tsx +468 -0
- package/src/components/Toolbar/Toolbar.tsx +113 -0
- package/src/components/Toolbar/ToolbarContext.ts +15 -0
- package/src/components/Toolbar/styles.css.ts +17 -0
- package/src/components/Tooltip/styles.css.ts +2 -1
- package/src/components/VirtualTable/styles.css.ts +2 -1
- package/src/index.ts +2 -1
- package/src/shared/ModalContext.ts +18 -0
- package/src/shared/mergeRefs.ts +11 -0
- package/src/shared/useOrderedRegistry.ts +57 -0
- package/dist/Table-C70zdSkM.js.map +0 -1
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import {useEffect, useRef, useState} from 'react'
|
|
2
|
+
import {hydrateRoot} from 'react-dom/client'
|
|
3
|
+
import {renderToString} from 'react-dom/server'
|
|
4
|
+
import type {Meta, StoryObj} from '@storybook/react'
|
|
5
|
+
import {html} from 'react-strict-dom'
|
|
6
|
+
import {expect, userEvent, waitFor, within} from 'storybook/test'
|
|
7
|
+
import {RADII_PX} from '@duro-app/tokens/keys'
|
|
8
|
+
import {Toolbar} from './Toolbar'
|
|
9
|
+
import {Button} from '../Button/Button'
|
|
10
|
+
import {ButtonGroup} from '../ButtonGroup/ButtonGroup'
|
|
11
|
+
import {Field} from '../Field/Field'
|
|
12
|
+
import {Input} from '../Input/Input'
|
|
13
|
+
import {Menu} from '../Menu/Menu'
|
|
14
|
+
import {Popover} from '../Popover/Popover'
|
|
15
|
+
import {Select} from '../Select/Select'
|
|
16
|
+
import {Stack} from '../Stack/Stack'
|
|
17
|
+
import {Toggle} from '../Toggle/Toggle'
|
|
18
|
+
|
|
19
|
+
const meta: Meta<typeof Toolbar> = {
|
|
20
|
+
title: 'Components/Toolbar',
|
|
21
|
+
component: Toolbar,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default meta
|
|
25
|
+
type Story = StoryObj<typeof Toolbar>
|
|
26
|
+
|
|
27
|
+
const page = () => within(document.body)
|
|
28
|
+
const R = `${RADII_PX.sm}px`
|
|
29
|
+
|
|
30
|
+
function TextStyle() {
|
|
31
|
+
return (
|
|
32
|
+
<ButtonGroup attached aria-label="Text style">
|
|
33
|
+
<Select.Root defaultValue="normal">
|
|
34
|
+
<Select.Trigger aria-label="Block type">
|
|
35
|
+
<Select.Value />
|
|
36
|
+
<Select.Icon />
|
|
37
|
+
</Select.Trigger>
|
|
38
|
+
<Select.Popup>
|
|
39
|
+
<Select.Item value="normal">Normal</Select.Item>
|
|
40
|
+
<Select.Item value="h1">Heading 1</Select.Item>
|
|
41
|
+
</Select.Popup>
|
|
42
|
+
</Select.Root>
|
|
43
|
+
<Select.Root defaultValue="arial">
|
|
44
|
+
<Select.Trigger aria-label="Font">
|
|
45
|
+
<Select.Value />
|
|
46
|
+
<Select.Icon />
|
|
47
|
+
</Select.Trigger>
|
|
48
|
+
<Select.Popup>
|
|
49
|
+
<Select.Item value="arial">Arial</Select.Item>
|
|
50
|
+
<Select.Item value="georgia">Georgia</Select.Item>
|
|
51
|
+
</Select.Popup>
|
|
52
|
+
</Select.Root>
|
|
53
|
+
<Select.Root defaultValue="15">
|
|
54
|
+
<Select.Trigger aria-label="Font size">
|
|
55
|
+
<Select.Value />
|
|
56
|
+
<Select.Icon />
|
|
57
|
+
</Select.Trigger>
|
|
58
|
+
<Select.Popup>
|
|
59
|
+
<Select.Item value="13">13px</Select.Item>
|
|
60
|
+
<Select.Item value="15">15px</Select.Item>
|
|
61
|
+
</Select.Popup>
|
|
62
|
+
</Select.Root>
|
|
63
|
+
</ButtonGroup>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function LinkPopover() {
|
|
68
|
+
return (
|
|
69
|
+
<Popover.Root>
|
|
70
|
+
<Popover.Trigger aria-label="Link">↗</Popover.Trigger>
|
|
71
|
+
<Popover.Popup label="Link">
|
|
72
|
+
<Stack gap="sm">
|
|
73
|
+
<Field.Root>
|
|
74
|
+
<Field.Label>URL</Field.Label>
|
|
75
|
+
<Input defaultValue="https://duro.app" />
|
|
76
|
+
</Field.Root>
|
|
77
|
+
<Button size="small">Apply</Button>
|
|
78
|
+
</Stack>
|
|
79
|
+
</Popover.Popup>
|
|
80
|
+
</Popover.Root>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function Format() {
|
|
85
|
+
const [pressed, setPressed] = useState<Record<string, boolean>>({})
|
|
86
|
+
const toggle = (key: string) => (next: boolean) => setPressed((p) => ({...p, [key]: next}))
|
|
87
|
+
return (
|
|
88
|
+
<ButtonGroup attached aria-label="Format">
|
|
89
|
+
<Toggle aria-label="Bold" pressed={!!pressed.b} onPressedChange={toggle('b')}>
|
|
90
|
+
B
|
|
91
|
+
</Toggle>
|
|
92
|
+
<Toggle aria-label="Italic" pressed={!!pressed.i} onPressedChange={toggle('i')}>
|
|
93
|
+
I
|
|
94
|
+
</Toggle>
|
|
95
|
+
<Toggle aria-label="Underline" pressed={!!pressed.u} onPressedChange={toggle('u')}>
|
|
96
|
+
U
|
|
97
|
+
</Toggle>
|
|
98
|
+
<Toggle aria-label="Code" pressed={!!pressed.code} onPressedChange={toggle('code')}>
|
|
99
|
+
{'<>'}
|
|
100
|
+
</Toggle>
|
|
101
|
+
<LinkPopover />
|
|
102
|
+
<Button variant="secondary" aria-label="Clear formatting">
|
|
103
|
+
T
|
|
104
|
+
</Button>
|
|
105
|
+
<Popover.Root>
|
|
106
|
+
<Popover.Trigger aria-label="Highlight">H</Popover.Trigger>
|
|
107
|
+
<Popover.Popup label="Highlight">
|
|
108
|
+
<Button size="small">Purple</Button>
|
|
109
|
+
</Popover.Popup>
|
|
110
|
+
</Popover.Root>
|
|
111
|
+
</ButtonGroup>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function InsertMenu() {
|
|
116
|
+
return (
|
|
117
|
+
<Menu.Root>
|
|
118
|
+
<Menu.Trigger>Insert</Menu.Trigger>
|
|
119
|
+
<Menu.Popup>
|
|
120
|
+
<Menu.Item>Table</Menu.Item>
|
|
121
|
+
<Menu.Item>Image</Menu.Item>
|
|
122
|
+
</Menu.Popup>
|
|
123
|
+
</Menu.Root>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function Editor() {
|
|
128
|
+
return (
|
|
129
|
+
<Toolbar aria-label="Formatting">
|
|
130
|
+
<TextStyle />
|
|
131
|
+
<Format />
|
|
132
|
+
<InsertMenu />
|
|
133
|
+
</Toolbar>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Every focusable control of the toolbar, in order: 3 + 7 + 1. */
|
|
138
|
+
const CONTROLS = [
|
|
139
|
+
'Block type',
|
|
140
|
+
'Font',
|
|
141
|
+
'Font size',
|
|
142
|
+
'Bold',
|
|
143
|
+
'Italic',
|
|
144
|
+
'Underline',
|
|
145
|
+
'Code',
|
|
146
|
+
'Link',
|
|
147
|
+
'Clear formatting',
|
|
148
|
+
'Highlight',
|
|
149
|
+
'Insert',
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
function control(name: string) {
|
|
153
|
+
return page().getByRole(
|
|
154
|
+
['Block type', 'Font', 'Font size'].includes(name) ? 'combobox' : 'button',
|
|
155
|
+
{name},
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** [Normal | Arial | 15px] and [B | I | U | <> | link | T | highlight], then Insert. */
|
|
160
|
+
export const Default: Story = {
|
|
161
|
+
render: () => <Editor />,
|
|
162
|
+
play: async ({canvas}) => {
|
|
163
|
+
await expect(canvas.getByRole('toolbar', {name: 'Formatting'})).toBeInTheDocument()
|
|
164
|
+
// One tab stop.
|
|
165
|
+
await waitFor(() =>
|
|
166
|
+
expect(CONTROLS.map((name) => control(name).tabIndex)).toEqual(
|
|
167
|
+
CONTROLS.map((_, i) => (i === 0 ? 0 : -1)),
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Tab enters once; ArrowRight crosses the groups; End and Home reach the ends. */
|
|
174
|
+
export const ArrowKeys: Story = {
|
|
175
|
+
render: () => (
|
|
176
|
+
<Stack gap="sm">
|
|
177
|
+
<Button variant="secondary">Before</Button>
|
|
178
|
+
<Editor />
|
|
179
|
+
</Stack>
|
|
180
|
+
),
|
|
181
|
+
play: async ({canvas}) => {
|
|
182
|
+
canvas.getByRole('button', {name: 'Before'}).focus()
|
|
183
|
+
await userEvent.tab()
|
|
184
|
+
await expect(control(CONTROLS[0])).toHaveFocus()
|
|
185
|
+
for (let i = 1; i <= 8; i++) {
|
|
186
|
+
await userEvent.keyboard('{ArrowRight}')
|
|
187
|
+
await expect(control(CONTROLS[i])).toHaveFocus()
|
|
188
|
+
}
|
|
189
|
+
await userEvent.keyboard('{End}')
|
|
190
|
+
await expect(control(CONTROLS[CONTROLS.length - 1])).toHaveFocus()
|
|
191
|
+
await userEvent.keyboard('{Home}')
|
|
192
|
+
await expect(control(CONTROLS[0])).toHaveFocus()
|
|
193
|
+
await userEvent.keyboard('{ArrowLeft}')
|
|
194
|
+
await expect(control(CONTROLS[0])).toHaveFocus()
|
|
195
|
+
// Tab leaves the toolbar in one press, and Shift+Tab comes back to the
|
|
196
|
+
// control last focused.
|
|
197
|
+
await userEvent.keyboard('{End}')
|
|
198
|
+
await userEvent.tab()
|
|
199
|
+
await expect(document.activeElement?.closest('[role="toolbar"]')).toBeNull()
|
|
200
|
+
await userEvent.tab({shift: true})
|
|
201
|
+
await expect(control('Insert')).toHaveFocus()
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** A closed Select takes ArrowDown: it opens, and Escape closes it on its trigger. */
|
|
206
|
+
export const SelectArrowDown: Story = {
|
|
207
|
+
render: () => <Editor />,
|
|
208
|
+
play: async () => {
|
|
209
|
+
const font = control('Font')
|
|
210
|
+
font.focus()
|
|
211
|
+
await userEvent.keyboard('{ArrowDown}')
|
|
212
|
+
await expect(font).toHaveAttribute('aria-expanded', 'true')
|
|
213
|
+
await expect(font).toHaveFocus()
|
|
214
|
+
await expect(page().getByRole('listbox')).toBeVisible()
|
|
215
|
+
await userEvent.keyboard('{Escape}')
|
|
216
|
+
await expect(font).toHaveAttribute('aria-expanded', 'false')
|
|
217
|
+
await expect(font).toHaveFocus()
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** ArrowRight while a Select is open: focus stays on its trigger, the listbox stays open. */
|
|
222
|
+
export const SelectOpenKeepsArrows: Story = {
|
|
223
|
+
render: () => <Editor />,
|
|
224
|
+
play: async () => {
|
|
225
|
+
const font = control('Font')
|
|
226
|
+
font.focus()
|
|
227
|
+
await userEvent.keyboard('{ArrowUp}')
|
|
228
|
+
await expect(font).toHaveAttribute('aria-expanded', 'true')
|
|
229
|
+
await userEvent.keyboard('{ArrowRight}')
|
|
230
|
+
await expect(font).toHaveFocus()
|
|
231
|
+
await expect(font).toHaveAttribute('aria-expanded', 'true')
|
|
232
|
+
await expect(page().getByRole('listbox')).toBeVisible()
|
|
233
|
+
await userEvent.keyboard('{Escape}')
|
|
234
|
+
},
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The link Popover's contents are outside the group and the toolbar: its
|
|
238
|
+
* Apply button is one Tab away and has all four corners round. */
|
|
239
|
+
export const PopoverInsideToolbar: Story = {
|
|
240
|
+
render: () => <Editor />,
|
|
241
|
+
play: async ({userEvent}) => {
|
|
242
|
+
await userEvent.click(control('Link'))
|
|
243
|
+
const url = await page().findByRole('textbox', {name: 'URL'})
|
|
244
|
+
await waitFor(() => expect(url).toHaveFocus())
|
|
245
|
+
await userEvent.tab()
|
|
246
|
+
const apply = page().getByRole('button', {name: 'Apply'})
|
|
247
|
+
await expect(apply).toHaveFocus()
|
|
248
|
+
const cs = getComputedStyle(apply)
|
|
249
|
+
await expect([
|
|
250
|
+
cs.borderTopLeftRadius,
|
|
251
|
+
cs.borderTopRightRadius,
|
|
252
|
+
cs.borderBottomRightRadius,
|
|
253
|
+
cs.borderBottomLeftRadius,
|
|
254
|
+
]).toEqual([R, R, R, R])
|
|
255
|
+
await expect(apply).not.toHaveAttribute('tabindex', '-1')
|
|
256
|
+
},
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** ArrowLeft typed in the Popover's input moves the caret, not toolbar focus. */
|
|
260
|
+
export const PopoverInputKeepsArrows: Story = {
|
|
261
|
+
render: () => <Editor />,
|
|
262
|
+
play: async ({userEvent}) => {
|
|
263
|
+
await userEvent.click(control('Link'))
|
|
264
|
+
const url = (await page().findByRole('textbox', {name: 'URL'})) as HTMLInputElement
|
|
265
|
+
await waitFor(() => expect(url).toHaveFocus())
|
|
266
|
+
url.setSelectionRange(url.value.length, url.value.length)
|
|
267
|
+
const end = url.selectionStart ?? 0
|
|
268
|
+
await userEvent.keyboard('{ArrowLeft}{ArrowLeft}')
|
|
269
|
+
await expect(url).toHaveFocus()
|
|
270
|
+
await expect(url.selectionStart).toBe(end - 2)
|
|
271
|
+
},
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** A control that handles ArrowRight itself (preventDefault) keeps it. */
|
|
275
|
+
function GreedyButton() {
|
|
276
|
+
const ref = useRef<HTMLButtonElement>(null)
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
const el = ref.current
|
|
279
|
+
if (!el) return
|
|
280
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
281
|
+
if (e.key === 'ArrowRight') e.preventDefault()
|
|
282
|
+
}
|
|
283
|
+
el.addEventListener('keydown', onKeyDown)
|
|
284
|
+
return () => el.removeEventListener('keydown', onKeyDown)
|
|
285
|
+
}, [])
|
|
286
|
+
return (
|
|
287
|
+
<Button ref={ref} variant="secondary">
|
|
288
|
+
Stepper
|
|
289
|
+
</Button>
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export const ControlKeepsHandledKeys: Story = {
|
|
294
|
+
render: () => (
|
|
295
|
+
<Toolbar aria-label="Playback">
|
|
296
|
+
<GreedyButton />
|
|
297
|
+
<Button variant="secondary">Next</Button>
|
|
298
|
+
</Toolbar>
|
|
299
|
+
),
|
|
300
|
+
play: async ({canvas}) => {
|
|
301
|
+
const stepper = canvas.getByRole('button', {name: 'Stepper'})
|
|
302
|
+
stepper.focus()
|
|
303
|
+
await userEvent.keyboard('{ArrowRight}')
|
|
304
|
+
await expect(stepper).toHaveFocus()
|
|
305
|
+
},
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Vertical: Up/Down move. */
|
|
309
|
+
export const Vertical: Story = {
|
|
310
|
+
render: () => (
|
|
311
|
+
<Toolbar aria-label="Tools" orientation="vertical">
|
|
312
|
+
<ButtonGroup attached orientation="vertical" aria-label="Draw">
|
|
313
|
+
<Button variant="secondary">Pen</Button>
|
|
314
|
+
<Button variant="secondary">Eraser</Button>
|
|
315
|
+
</ButtonGroup>
|
|
316
|
+
<Button variant="secondary">Undo</Button>
|
|
317
|
+
</Toolbar>
|
|
318
|
+
),
|
|
319
|
+
play: async ({canvas}) => {
|
|
320
|
+
const pen = canvas.getByRole('button', {name: 'Pen'})
|
|
321
|
+
pen.focus()
|
|
322
|
+
await userEvent.keyboard('{ArrowDown}')
|
|
323
|
+
await expect(canvas.getByRole('button', {name: 'Eraser'})).toHaveFocus()
|
|
324
|
+
await userEvent.keyboard('{ArrowDown}')
|
|
325
|
+
await expect(canvas.getByRole('button', {name: 'Undo'})).toHaveFocus()
|
|
326
|
+
await userEvent.keyboard('{ArrowUp}')
|
|
327
|
+
await expect(canvas.getByRole('button', {name: 'Eraser'})).toHaveFocus()
|
|
328
|
+
},
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Server render: no control has registered yet, so every control in an
|
|
333
|
+
* attached group renders `middle` (square, joined) and every control is in
|
|
334
|
+
* the tab order. Hydration matches that markup exactly, then the outer
|
|
335
|
+
* corners round and the toolbar takes one tab stop.
|
|
336
|
+
*/
|
|
337
|
+
function SsrToolbar() {
|
|
338
|
+
return (
|
|
339
|
+
<Toolbar aria-label="Server">
|
|
340
|
+
<ButtonGroup attached aria-label="Actions">
|
|
341
|
+
<Button variant="secondary">One</Button>
|
|
342
|
+
<Button variant="secondary">Two</Button>
|
|
343
|
+
<Button variant="secondary">Three</Button>
|
|
344
|
+
</ButtonGroup>
|
|
345
|
+
</Toolbar>
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export const ServerRender: Story = {
|
|
350
|
+
render: () => <html.div data-ssr-host="" />,
|
|
351
|
+
play: async ({canvasElement}) => {
|
|
352
|
+
const host = canvasElement.querySelector('[data-ssr-host]') as HTMLElement
|
|
353
|
+
host.innerHTML = renderToString(<SsrToolbar />)
|
|
354
|
+
const buttons = () => [...host.querySelectorAll('button')]
|
|
355
|
+
const radii = (el: Element) => {
|
|
356
|
+
const cs = getComputedStyle(el)
|
|
357
|
+
return [
|
|
358
|
+
cs.borderTopLeftRadius,
|
|
359
|
+
cs.borderTopRightRadius,
|
|
360
|
+
cs.borderBottomRightRadius,
|
|
361
|
+
cs.borderBottomLeftRadius,
|
|
362
|
+
]
|
|
363
|
+
}
|
|
364
|
+
// Server markup: all middle, no roving tabindex yet.
|
|
365
|
+
for (const b of buttons()) {
|
|
366
|
+
await expect(radii(b)).toEqual(['0px', '0px', '0px', '0px'])
|
|
367
|
+
await expect(b.hasAttribute('tabindex')).toBe(false)
|
|
368
|
+
}
|
|
369
|
+
const errors: unknown[] = []
|
|
370
|
+
const consoleError = console.error
|
|
371
|
+
console.error = (...args: unknown[]) => {
|
|
372
|
+
errors.push(args)
|
|
373
|
+
consoleError(...args)
|
|
374
|
+
}
|
|
375
|
+
const root = hydrateRoot(host, <SsrToolbar />, {
|
|
376
|
+
onRecoverableError: (error) => errors.push(error),
|
|
377
|
+
})
|
|
378
|
+
try {
|
|
379
|
+
await waitFor(() => expect(radii(buttons()[0])).toEqual([R, '0px', '0px', R]))
|
|
380
|
+
await expect(radii(buttons()[1])).toEqual(['0px', '0px', '0px', '0px'])
|
|
381
|
+
await expect(radii(buttons()[2])).toEqual(['0px', R, R, '0px'])
|
|
382
|
+
await expect(buttons().map((b) => b.tabIndex)).toEqual([0, -1, -1])
|
|
383
|
+
await expect(errors).toEqual([])
|
|
384
|
+
} finally {
|
|
385
|
+
console.error = consoleError
|
|
386
|
+
root.unmount()
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Keyed controls reordered without remounting: corners and arrow order
|
|
392
|
+
* follow the new DOM order. */
|
|
393
|
+
function Reorderable() {
|
|
394
|
+
const [names, setNames] = useState(['One', 'Two', 'Three'])
|
|
395
|
+
return (
|
|
396
|
+
<Stack gap="md">
|
|
397
|
+
<Button variant="secondary" onClick={() => setNames((n) => [...n].reverse())}>
|
|
398
|
+
Reverse
|
|
399
|
+
</Button>
|
|
400
|
+
<Toolbar aria-label="Ordered">
|
|
401
|
+
<ButtonGroup attached aria-label="Items">
|
|
402
|
+
{names.map((name) => (
|
|
403
|
+
<Button key={name} variant="secondary">
|
|
404
|
+
{name}
|
|
405
|
+
</Button>
|
|
406
|
+
))}
|
|
407
|
+
</ButtonGroup>
|
|
408
|
+
</Toolbar>
|
|
409
|
+
</Stack>
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export const ReorderKeyedChildren: Story = {
|
|
414
|
+
render: () => <Reorderable />,
|
|
415
|
+
play: async ({canvas}) => {
|
|
416
|
+
const one = canvas.getByRole('button', {name: 'One'})
|
|
417
|
+
await userEvent.click(canvas.getByRole('button', {name: 'Reverse'}))
|
|
418
|
+
// Same element, moved: no remount.
|
|
419
|
+
await expect(canvas.getByRole('button', {name: 'One'})).toBe(one)
|
|
420
|
+
const radii = (el: Element) => {
|
|
421
|
+
const cs = getComputedStyle(el)
|
|
422
|
+
return [
|
|
423
|
+
cs.borderTopLeftRadius,
|
|
424
|
+
cs.borderTopRightRadius,
|
|
425
|
+
cs.borderBottomRightRadius,
|
|
426
|
+
cs.borderBottomLeftRadius,
|
|
427
|
+
]
|
|
428
|
+
}
|
|
429
|
+
const three = canvas.getByRole('button', {name: 'Three'})
|
|
430
|
+
await waitFor(() => expect(radii(three)).toEqual([R, '0px', '0px', R]))
|
|
431
|
+
await expect(radii(one)).toEqual(['0px', R, R, '0px'])
|
|
432
|
+
await expect(three.tabIndex).toBe(0)
|
|
433
|
+
three.focus()
|
|
434
|
+
await userEvent.keyboard('{ArrowRight}')
|
|
435
|
+
await expect(canvas.getByRole('button', {name: 'Two'})).toHaveFocus()
|
|
436
|
+
await userEvent.keyboard('{ArrowRight}')
|
|
437
|
+
await expect(one).toHaveFocus()
|
|
438
|
+
},
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** Vertical, with a Menu: a closed Menu.Trigger handles no arrow key, so
|
|
442
|
+
* ArrowDown moves on (Enter or Space opens it). Unlike a Select, a Menu can
|
|
443
|
+
* sit in a vertical toolbar. */
|
|
444
|
+
export const VerticalWithMenu: Story = {
|
|
445
|
+
render: () => (
|
|
446
|
+
<Toolbar aria-label="Tools" orientation="vertical">
|
|
447
|
+
<Menu.Root>
|
|
448
|
+
<Menu.Trigger>Insert</Menu.Trigger>
|
|
449
|
+
<Menu.Popup>
|
|
450
|
+
<Menu.Item>Table</Menu.Item>
|
|
451
|
+
</Menu.Popup>
|
|
452
|
+
</Menu.Root>
|
|
453
|
+
<Button variant="secondary">Undo</Button>
|
|
454
|
+
</Toolbar>
|
|
455
|
+
),
|
|
456
|
+
play: async ({canvas}) => {
|
|
457
|
+
const insert = canvas.getByRole('button', {name: 'Insert'})
|
|
458
|
+
insert.focus()
|
|
459
|
+
await userEvent.keyboard('{ArrowDown}')
|
|
460
|
+
await expect(insert).toHaveAttribute('aria-expanded', 'false')
|
|
461
|
+
await expect(canvas.getByRole('button', {name: 'Undo'})).toHaveFocus()
|
|
462
|
+
await userEvent.keyboard('{ArrowUp}')
|
|
463
|
+
await expect(insert).toHaveFocus()
|
|
464
|
+
await userEvent.keyboard('{Enter}')
|
|
465
|
+
await expect(insert).toHaveAttribute('aria-expanded', 'true')
|
|
466
|
+
await userEvent.keyboard('{Escape}')
|
|
467
|
+
},
|
|
468
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {type ReactNode, useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
|
2
|
+
import {html} from 'react-strict-dom'
|
|
3
|
+
import type {Orientation} from '../../shared/types'
|
|
4
|
+
import {useOrderedRegistry} from '../../shared/useOrderedRegistry'
|
|
5
|
+
import {ToolbarContext, type ToolbarContextValue} from './ToolbarContext'
|
|
6
|
+
import {styles} from './styles.css'
|
|
7
|
+
|
|
8
|
+
export interface ToolbarProps {
|
|
9
|
+
/** Names the toolbar ("Formatting"). Required: a toolbar is a landmark-like widget. */
|
|
10
|
+
'aria-label': string
|
|
11
|
+
/**
|
|
12
|
+
* Arrow keys follow the orientation: Left/Right (default) or Up/Down. A
|
|
13
|
+
* vertical toolbar cannot hold a Select, whose closed trigger opens on
|
|
14
|
+
* ArrowDown/ArrowUp. A Menu can: its closed trigger handles no arrow key,
|
|
15
|
+
* so Up/Down move past it and Enter or Space opens it.
|
|
16
|
+
*/
|
|
17
|
+
orientation?: Orientation
|
|
18
|
+
children: ReactNode
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A row of controls with one tab stop (the WAI-ARIA toolbar pattern). Every
|
|
23
|
+
* Button, Toggle and Select/Menu/Popover trigger inside it, through any
|
|
24
|
+
* ButtonGroup, joins one roving tabindex: Tab enters at the last focused
|
|
25
|
+
* control, Left/Right (Up/Down when vertical) move, Home/End jump to the ends.
|
|
26
|
+
*/
|
|
27
|
+
export function Toolbar({
|
|
28
|
+
'aria-label': ariaLabel,
|
|
29
|
+
orientation = 'horizontal',
|
|
30
|
+
children,
|
|
31
|
+
}: ToolbarProps) {
|
|
32
|
+
const rootRef = useRef<HTMLDivElement>(null)
|
|
33
|
+
const {elements, order, register} = useOrderedRegistry()
|
|
34
|
+
const [focusedId, setFocusedId] = useState<string | null>(null)
|
|
35
|
+
|
|
36
|
+
const enabled = order.filter((entry) => !entry.disabled)
|
|
37
|
+
const tabStop =
|
|
38
|
+
focusedId !== null && enabled.some((entry) => entry.id === focusedId)
|
|
39
|
+
? focusedId
|
|
40
|
+
: (enabled[0]?.id ?? null)
|
|
41
|
+
|
|
42
|
+
const tabIndexOf = useCallback<ToolbarContextValue['tabIndexOf']>(
|
|
43
|
+
(id) => {
|
|
44
|
+
if (!order.some((entry) => entry.id === id)) return undefined
|
|
45
|
+
return id === tabStop ? 0 : -1
|
|
46
|
+
},
|
|
47
|
+
[order, tabStop],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
// One listener on the document, in the bubble phase, so it runs after the
|
|
51
|
+
// controls' own handlers (React dispatches from the app root, below the
|
|
52
|
+
// document). It acts only on a key aimed at one of this toolbar's own
|
|
53
|
+
// registered controls, so keys typed in a portalled popup (a Popover's
|
|
54
|
+
// input) never move toolbar focus. A control whose popup is open
|
|
55
|
+
// (aria-expanded="true") keeps its keys, and so does one that already
|
|
56
|
+
// handled the key (defaultPrevented).
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const doc = rootRef.current?.ownerDocument
|
|
59
|
+
if (!doc) return
|
|
60
|
+
const [prev, next] =
|
|
61
|
+
orientation === 'vertical' ? ['ArrowUp', 'ArrowDown'] : ['ArrowLeft', 'ArrowRight']
|
|
62
|
+
function onKeyDown(e: KeyboardEvent) {
|
|
63
|
+
if (e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey) return
|
|
64
|
+
const els = order
|
|
65
|
+
.filter((entry) => !entry.disabled)
|
|
66
|
+
.map((entry) => elements.current.get(entry.id)?.el)
|
|
67
|
+
.filter((el): el is HTMLElement => el !== undefined)
|
|
68
|
+
const target = e.target as HTMLElement
|
|
69
|
+
const i = els.indexOf(target)
|
|
70
|
+
if (i < 0 || target.getAttribute('aria-expanded') === 'true') return
|
|
71
|
+
let to: HTMLElement | undefined
|
|
72
|
+
switch (e.key) {
|
|
73
|
+
case next:
|
|
74
|
+
to = els[Math.min(i + 1, els.length - 1)]
|
|
75
|
+
break
|
|
76
|
+
case prev:
|
|
77
|
+
to = els[Math.max(i - 1, 0)]
|
|
78
|
+
break
|
|
79
|
+
case 'Home':
|
|
80
|
+
to = els[0]
|
|
81
|
+
break
|
|
82
|
+
case 'End':
|
|
83
|
+
to = els[els.length - 1]
|
|
84
|
+
break
|
|
85
|
+
default:
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
e.preventDefault()
|
|
89
|
+
to?.focus()
|
|
90
|
+
}
|
|
91
|
+
doc.addEventListener('keydown', onKeyDown)
|
|
92
|
+
return () => doc.removeEventListener('keydown', onKeyDown)
|
|
93
|
+
}, [order, orientation, elements])
|
|
94
|
+
|
|
95
|
+
const ctx = useMemo<ToolbarContextValue>(
|
|
96
|
+
() => ({register, tabIndexOf, onItemFocus: setFocusedId}),
|
|
97
|
+
[register, tabIndexOf],
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<ToolbarContext.Provider value={ctx}>
|
|
102
|
+
<html.div
|
|
103
|
+
ref={rootRef}
|
|
104
|
+
role="toolbar"
|
|
105
|
+
aria-label={ariaLabel}
|
|
106
|
+
aria-orientation={orientation}
|
|
107
|
+
style={[styles.root, orientation === 'vertical' && styles.vertical]}
|
|
108
|
+
>
|
|
109
|
+
{children}
|
|
110
|
+
</html.div>
|
|
111
|
+
</ToolbarContext.Provider>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {createContext, useContext} from 'react'
|
|
2
|
+
|
|
3
|
+
export interface ToolbarContextValue {
|
|
4
|
+
register: (id: string, el: HTMLElement, disabled: boolean) => () => void
|
|
5
|
+
/** 0 for the toolbar's one tab stop, -1 for the rest; undefined before
|
|
6
|
+
* the control has registered (server render, first paint). */
|
|
7
|
+
tabIndexOf: (id: string) => 0 | -1 | undefined
|
|
8
|
+
onItemFocus: (id: string) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ToolbarContext = createContext<ToolbarContextValue | null>(null)
|
|
12
|
+
|
|
13
|
+
export function useToolbar() {
|
|
14
|
+
return useContext(ToolbarContext)
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {spacing} from '@duro-app/tokens/tokens/spacing.css'
|
|
3
|
+
|
|
4
|
+
export const styles = css.create({
|
|
5
|
+
root: {
|
|
6
|
+
display: 'flex',
|
|
7
|
+
flexDirection: 'row',
|
|
8
|
+
flexWrap: 'wrap',
|
|
9
|
+
alignItems: 'center',
|
|
10
|
+
gap: spacing.sm,
|
|
11
|
+
},
|
|
12
|
+
vertical: {
|
|
13
|
+
flexDirection: 'column',
|
|
14
|
+
flexWrap: 'nowrap',
|
|
15
|
+
alignItems: 'stretch',
|
|
16
|
+
},
|
|
17
|
+
})
|
|
@@ -4,6 +4,7 @@ import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
|
|
|
4
4
|
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
5
|
import {shadows} from '@duro-app/tokens/tokens/shadows.css'
|
|
6
6
|
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
7
|
+
import {layers} from '@duro-app/tokens/tokens/layers.css'
|
|
7
8
|
|
|
8
9
|
export const styles = css.create({
|
|
9
10
|
root: {
|
|
@@ -12,7 +13,7 @@ export const styles = css.create({
|
|
|
12
13
|
},
|
|
13
14
|
popup: {
|
|
14
15
|
position: 'absolute',
|
|
15
|
-
zIndex:
|
|
16
|
+
zIndex: layers.floating,
|
|
16
17
|
paddingTop: spacing.xs,
|
|
17
18
|
paddingBottom: spacing.xs,
|
|
18
19
|
paddingLeft: spacing.sm,
|
|
@@ -3,6 +3,7 @@ import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
|
3
3
|
import {microSpacing, radii, spacing} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
4
|
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
5
|
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
6
|
+
import {layers} from '@duro-app/tokens/tokens/layers.css'
|
|
6
7
|
|
|
7
8
|
export const styles = css.create({
|
|
8
9
|
wrap: {
|
|
@@ -27,7 +28,7 @@ export const styles = css.create({
|
|
|
27
28
|
head: {
|
|
28
29
|
position: 'sticky',
|
|
29
30
|
top: 0,
|
|
30
|
-
zIndex:
|
|
31
|
+
zIndex: layers.raised,
|
|
31
32
|
backgroundColor: colors.bgCard,
|
|
32
33
|
},
|
|
33
34
|
headRow: {
|
package/src/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ export {Alert, type AlertVariant} from './components/Alert/Alert'
|
|
|
13
13
|
export {Icon, type IconName} from './components/Icon'
|
|
14
14
|
export {Badge, type BadgeVariant, type BadgeSize} from './components/Badge/Badge'
|
|
15
15
|
export {Button, type ButtonVariant, type ButtonSize} from './components/Button/Button'
|
|
16
|
-
export {ButtonGroup} from './components/ButtonGroup/ButtonGroup'
|
|
16
|
+
export {ButtonGroup, type ButtonGroupProps} from './components/ButtonGroup/ButtonGroup'
|
|
17
17
|
export {Callout, type CalloutVariant} from './components/Callout/Callout'
|
|
18
18
|
export {Card, type CardVariant, type CardSize} from './components/Card/Card'
|
|
19
19
|
export {Checkbox} from './components/Checkbox/Checkbox'
|
|
@@ -94,6 +94,7 @@ export {Text, type TextVariant, type TextColor} from './components/Text/Text'
|
|
|
94
94
|
export {Toggle, type ToggleSize} from './components/Toggle/Toggle'
|
|
95
95
|
export {ToggleGroup} from './components/ToggleGroup/ToggleGroup'
|
|
96
96
|
export {Tooltip} from './components/Tooltip/Tooltip'
|
|
97
|
+
export {Toolbar, type ToolbarProps} from './components/Toolbar/Toolbar'
|
|
97
98
|
export {Popover} from './components/Popover/Popover'
|
|
98
99
|
export type {PopoverHandle} from './components/Popover/Popover'
|
|
99
100
|
export {Switch} from './components/Switch/Switch'
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {createContext, useContext} from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* True inside a Dialog or Drawer. A Popover reads it to pick its layer: inside
|
|
5
|
+
* a modal it must clear that modal (`layers.popover`); outside one it stays at
|
|
6
|
+
* `layers.floating`, so a modal opened later, even from inside the Popover,
|
|
7
|
+
* covers it.
|
|
8
|
+
*
|
|
9
|
+
* holds-until: a Dialog opened from a Popover that itself sits inside a Dialog
|
|
10
|
+
* still renders under that Popover (1000/1001 < 1040). It is fixed when a
|
|
11
|
+
* nested Dialog lifts its layer above its opener, or when the parent Popover
|
|
12
|
+
* closes on a nested modal opening; no consumer needs it yet.
|
|
13
|
+
*/
|
|
14
|
+
export const ModalContext = createContext(false)
|
|
15
|
+
|
|
16
|
+
export function useInModal() {
|
|
17
|
+
return useContext(ModalContext)
|
|
18
|
+
}
|