@charcuterie/ui 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Combobox/Combobox.d.ts +54 -0
- package/dist/Combobox/Combobox.d.ts.map +1 -0
- package/dist/Combobox/Combobox.js +216 -0
- package/dist/Combobox/Combobox.js.map +1 -0
- package/dist/Combobox/ComboboxOption.d.ts +36 -0
- package/dist/Combobox/ComboboxOption.d.ts.map +1 -0
- package/dist/Combobox/ComboboxOption.js +32 -0
- package/dist/Combobox/ComboboxOption.js.map +1 -0
- package/dist/Dialog/Dialog.d.ts +47 -0
- package/dist/Dialog/Dialog.d.ts.map +1 -0
- package/dist/Dialog/Dialog.js +52 -0
- package/dist/Dialog/Dialog.js.map +1 -0
- package/dist/Lightbox/Lightbox.d.ts +7 -7
- package/dist/Lightbox/Lightbox.js +9 -9
- package/dist/Lightbox/Lightbox.js.map +1 -1
- package/dist/Listbox/Listbox.d.ts +56 -0
- package/dist/Listbox/Listbox.d.ts.map +1 -0
- package/dist/Listbox/Listbox.js +151 -0
- package/dist/Listbox/Listbox.js.map +1 -0
- package/dist/Listbox/ListboxOption.d.ts +36 -0
- package/dist/Listbox/ListboxOption.d.ts.map +1 -0
- package/dist/Listbox/ListboxOption.js +42 -0
- package/dist/Listbox/ListboxOption.js.map +1 -0
- package/dist/Menu/Menu.d.ts.map +1 -1
- package/dist/Menu/Menu.js +47 -97
- package/dist/Menu/Menu.js.map +1 -1
- package/dist/Modal/Modal.d.ts +11 -59
- package/dist/Modal/Modal.d.ts.map +1 -1
- package/dist/Modal/Modal.js +28 -101
- package/dist/Modal/Modal.js.map +1 -1
- package/dist/Overlay/OverlayPanel.d.ts +50 -0
- package/dist/Overlay/OverlayPanel.d.ts.map +1 -0
- package/dist/Overlay/OverlayPanel.js +72 -0
- package/dist/Overlay/OverlayPanel.js.map +1 -0
- package/dist/Overlay/OverlayStack.d.ts +47 -0
- package/dist/Overlay/OverlayStack.d.ts.map +1 -0
- package/dist/Overlay/OverlayStack.js +46 -0
- package/dist/Overlay/OverlayStack.js.map +1 -0
- package/dist/Overlay/SharedBackdrop.d.ts +24 -0
- package/dist/Overlay/SharedBackdrop.d.ts.map +1 -0
- package/dist/Overlay/SharedBackdrop.js +28 -0
- package/dist/Overlay/SharedBackdrop.js.map +1 -0
- package/dist/Overlay/lockScrollBehind.d.ts.map +1 -0
- package/dist/Overlay/lockScrollBehind.js.map +1 -0
- package/dist/Overlay/overlayPanelClass.d.ts +17 -0
- package/dist/Overlay/overlayPanelClass.d.ts.map +1 -0
- package/dist/Overlay/overlayPanelClass.js +17 -0
- package/dist/Overlay/overlayPanelClass.js.map +1 -0
- package/dist/Overlay/useAnchoredOverlay.d.ts +97 -0
- package/dist/Overlay/useAnchoredOverlay.d.ts.map +1 -0
- package/dist/Overlay/useAnchoredOverlay.js +81 -0
- package/dist/Overlay/useAnchoredOverlay.js.map +1 -0
- package/dist/Popover/Popover.d.ts +16 -24
- package/dist/Popover/Popover.d.ts.map +1 -1
- package/dist/Popover/Popover.js +32 -68
- package/dist/Popover/Popover.js.map +1 -1
- package/dist/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/Tooltip/Tooltip.js +25 -25
- package/dist/Tooltip/Tooltip.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/mountStory.testHelpers.d.ts +1 -0
- package/dist/mountStory.testHelpers.d.ts.map +1 -1
- package/dist/mountStory.testHelpers.js +21 -1
- package/dist/mountStory.testHelpers.js.map +1 -1
- package/package.json +3 -2
- package/src/Combobox/Combobox.mdx +71 -0
- package/src/Combobox/Combobox.stories.tsx +245 -0
- package/src/Combobox/Combobox.test.tsx +289 -0
- package/src/Combobox/Combobox.tsx +513 -0
- package/src/Combobox/ComboboxOption.tsx +88 -0
- package/src/Dialog/Dialog.mdx +87 -0
- package/src/Dialog/Dialog.stories.tsx +347 -0
- package/src/Dialog/Dialog.test.tsx +312 -0
- package/src/Dialog/Dialog.tsx +127 -0
- package/src/Field/Field.test.tsx +6 -4
- package/src/Lightbox/Lightbox.mdx +9 -8
- package/src/Lightbox/Lightbox.stories.tsx +5 -5
- package/src/Lightbox/Lightbox.test.tsx +25 -36
- package/src/Lightbox/Lightbox.tsx +10 -10
- package/src/Listbox/Listbox.mdx +68 -0
- package/src/Listbox/Listbox.stories.tsx +205 -0
- package/src/Listbox/Listbox.test.tsx +198 -0
- package/src/Listbox/Listbox.tsx +295 -0
- package/src/Listbox/ListboxOption.tsx +104 -0
- package/src/Menu/Menu.mdx +11 -13
- package/src/Menu/Menu.test.tsx +47 -38
- package/src/Menu/Menu.tsx +81 -142
- package/src/Modal/Modal.mdx +29 -82
- package/src/Modal/Modal.stories.tsx +55 -188
- package/src/Modal/Modal.test.tsx +44 -177
- package/src/Modal/Modal.tsx +54 -204
- package/src/Overlay/OverlayPanel.tsx +195 -0
- package/src/Overlay/OverlayStack.test.tsx +235 -0
- package/src/Overlay/OverlayStack.tsx +132 -0
- package/src/Overlay/SharedBackdrop.tsx +40 -0
- package/src/Overlay/overlayPanelClass.ts +17 -0
- package/src/Overlay/useAnchoredOverlay.ts +197 -0
- package/src/Popover/Popover.mdx +17 -20
- package/src/Popover/Popover.stories.tsx +33 -0
- package/src/Popover/Popover.test.tsx +68 -32
- package/src/Popover/Popover.tsx +56 -95
- package/src/Tooltip/Tooltip.mdx +12 -9
- package/src/Tooltip/Tooltip.test.tsx +17 -19
- package/src/Tooltip/Tooltip.tsx +35 -32
- package/src/index.ts +13 -4
- package/src/mountStory.testHelpers.ts +25 -1
- package/src/sourceRules.test.ts +12 -3
- package/src/storyControls.test.ts +25 -12
- package/dist/Modal/lockScrollBehind.d.ts.map +0 -1
- package/dist/Modal/lockScrollBehind.js.map +0 -1
- /package/dist/{Modal → Overlay}/lockScrollBehind.d.ts +0 -0
- /package/dist/{Modal → Overlay}/lockScrollBehind.js +0 -0
- /package/src/{Modal → Overlay}/lockScrollBehind.ts +0 -0
|
@@ -2,29 +2,16 @@ import { useVisibility } from "@charcuterie/logic"
|
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
3
|
import type { ReactNode } from "react"
|
|
4
4
|
|
|
5
|
-
import { toStoryChoice } from "../argTypes.storyHelpers.ts"
|
|
6
5
|
import { Button } from "../Button/Button.tsx"
|
|
7
|
-
import { StoryRow } from "../board.storyHelpers.tsx"
|
|
8
|
-
import type { ModalSize } from "./Modal.tsx"
|
|
9
6
|
import { Modal } from "./Modal.tsx"
|
|
10
7
|
|
|
11
8
|
const meta = {
|
|
12
9
|
title: "Components/Modal",
|
|
13
10
|
component: Modal,
|
|
14
11
|
parameters: { layout: "padded" },
|
|
15
|
-
argTypes: {
|
|
16
|
-
size: toStoryChoice([
|
|
17
|
-
"sm",
|
|
18
|
-
"md",
|
|
19
|
-
"lg",
|
|
20
|
-
"xl",
|
|
21
|
-
"full",
|
|
22
|
-
] as const),
|
|
23
|
-
},
|
|
24
12
|
args: {
|
|
25
|
-
headingLevel: 2,
|
|
26
13
|
isDismissable: true,
|
|
27
|
-
|
|
14
|
+
role: "dialog",
|
|
28
15
|
},
|
|
29
16
|
} satisfies Meta<typeof Modal>
|
|
30
17
|
|
|
@@ -33,32 +20,21 @@ export default meta
|
|
|
33
20
|
type Story = StoryObj<typeof meta>
|
|
34
21
|
|
|
35
22
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* boards show instead is the real arrangement — a page, a scrim,
|
|
41
|
-
* and one dialog over both.
|
|
42
|
-
*
|
|
43
|
-
* `footer` is a function of `hide` because a dialog's own buttons
|
|
44
|
-
* are the normal way out of it, and they need the same `hide` the
|
|
45
|
-
* close button gets. One `useVisibility` per demo, no second source
|
|
46
|
-
* of truth.
|
|
23
|
+
* The base layer has no chrome, so a demo supplies its own — the
|
|
24
|
+
* point being that `Modal` is the backdrop-plus-dismiss-plus-portal
|
|
25
|
+
* foundation and the content is entirely the caller's. `Dialog` is
|
|
26
|
+
* the batteries-included version of exactly this.
|
|
47
27
|
*/
|
|
48
28
|
const ModalDemo = ({
|
|
49
29
|
buttonLabel,
|
|
50
30
|
children,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
isDismissable = true,
|
|
54
|
-
size,
|
|
31
|
+
label,
|
|
32
|
+
role,
|
|
55
33
|
}: {
|
|
56
34
|
buttonLabel: string
|
|
57
35
|
children?: ReactNode
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
isDismissable?: boolean
|
|
61
|
-
size?: ModalSize
|
|
36
|
+
label: string
|
|
37
|
+
role?: "alertdialog" | "dialog"
|
|
62
38
|
}): ReactNode => {
|
|
63
39
|
const { hide, isVisible, show } = useVisibility()
|
|
64
40
|
|
|
@@ -69,21 +45,29 @@ const ModalDemo = ({
|
|
|
69
45
|
</Button>
|
|
70
46
|
|
|
71
47
|
<Modal
|
|
72
|
-
|
|
73
|
-
heading={heading}
|
|
74
|
-
isDismissable={isDismissable}
|
|
48
|
+
aria-label={label}
|
|
75
49
|
isVisible={isVisible}
|
|
76
50
|
onClose={hide}
|
|
77
|
-
|
|
51
|
+
role={role}
|
|
78
52
|
>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
53
|
+
<div className="flex flex-col gap-3 p-4">
|
|
54
|
+
{children ?? (
|
|
55
|
+
<p className="text-content-secondary text-sm">
|
|
56
|
+
A bare `Modal`: a scrim, a focus trap, Escape,
|
|
57
|
+
and an outside press. Everything you see
|
|
58
|
+
inside is the caller's own markup.
|
|
59
|
+
</p>
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
<Button
|
|
63
|
+
appearance="soft"
|
|
64
|
+
intent="neutral"
|
|
65
|
+
onClick={hide}
|
|
66
|
+
size="sm"
|
|
67
|
+
>
|
|
68
|
+
Close
|
|
69
|
+
</Button>
|
|
70
|
+
</div>
|
|
87
71
|
</Modal>
|
|
88
72
|
</>
|
|
89
73
|
)
|
|
@@ -91,182 +75,65 @@ const ModalDemo = ({
|
|
|
91
75
|
|
|
92
76
|
export const Default: Story = {
|
|
93
77
|
args: {
|
|
94
|
-
|
|
78
|
+
"aria-label": "Read error on title 4",
|
|
79
|
+
children: null,
|
|
95
80
|
isVisible: false,
|
|
96
81
|
onClose: () => {},
|
|
97
82
|
},
|
|
98
83
|
render: () => (
|
|
99
84
|
<ModalDemo
|
|
100
|
-
buttonLabel="
|
|
101
|
-
|
|
85
|
+
buttonLabel="Open the base modal"
|
|
86
|
+
label="Read error on title 4"
|
|
102
87
|
/>
|
|
103
88
|
),
|
|
104
89
|
}
|
|
105
90
|
|
|
106
|
-
const SIZES: ModalSize[] = ["sm", "md", "lg", "xl", "full"]
|
|
107
|
-
|
|
108
|
-
export const AllVariants: Story = {
|
|
109
|
-
args: {
|
|
110
|
-
heading: "Bay 3",
|
|
111
|
-
isVisible: false,
|
|
112
|
-
onClose: () => {},
|
|
113
|
-
},
|
|
114
|
-
render: () => (
|
|
115
|
-
<StoryRow>
|
|
116
|
-
{SIZES.map((size) => (
|
|
117
|
-
<ModalDemo
|
|
118
|
-
buttonLabel={`Open ${size}`}
|
|
119
|
-
heading={`Read error — ${size}`}
|
|
120
|
-
key={size}
|
|
121
|
-
size={size}
|
|
122
|
-
/>
|
|
123
|
-
))}
|
|
124
|
-
</StoryRow>
|
|
125
|
-
),
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export const AllStates: Story = {
|
|
129
|
-
args: {
|
|
130
|
-
heading: "Bay 3",
|
|
131
|
-
isVisible: false,
|
|
132
|
-
onClose: () => {},
|
|
133
|
-
},
|
|
134
|
-
render: () => (
|
|
135
|
-
<StoryRow>
|
|
136
|
-
<ModalDemo
|
|
137
|
-
buttonLabel="With a footer"
|
|
138
|
-
footer={(hide) => (
|
|
139
|
-
<>
|
|
140
|
-
<Button
|
|
141
|
-
appearance="soft"
|
|
142
|
-
intent="neutral"
|
|
143
|
-
onClick={hide}
|
|
144
|
-
size="sm"
|
|
145
|
-
>
|
|
146
|
-
Keep going
|
|
147
|
-
</Button>
|
|
148
|
-
|
|
149
|
-
<Button intent="danger" size="sm">
|
|
150
|
-
Stop the rip
|
|
151
|
-
</Button>
|
|
152
|
-
</>
|
|
153
|
-
)}
|
|
154
|
-
heading="Stop the rip?"
|
|
155
|
-
/>
|
|
156
|
-
|
|
157
|
-
<ModalDemo
|
|
158
|
-
buttonLabel="Not dismissable"
|
|
159
|
-
footer={(hide) => (
|
|
160
|
-
<>
|
|
161
|
-
<Button
|
|
162
|
-
appearance="soft"
|
|
163
|
-
intent="neutral"
|
|
164
|
-
onClick={hide}
|
|
165
|
-
size="sm"
|
|
166
|
-
>
|
|
167
|
-
Keep the titles
|
|
168
|
-
</Button>
|
|
169
|
-
|
|
170
|
-
<Button intent="danger" size="sm">
|
|
171
|
-
Erase and re-rip
|
|
172
|
-
</Button>
|
|
173
|
-
</>
|
|
174
|
-
)}
|
|
175
|
-
heading="This erases eight completed titles"
|
|
176
|
-
isDismissable={false}
|
|
177
|
-
/>
|
|
178
|
-
|
|
179
|
-
<ModalDemo
|
|
180
|
-
buttonLabel="Long body"
|
|
181
|
-
heading="MakeMKV log"
|
|
182
|
-
>
|
|
183
|
-
<div className="flex flex-col gap-2">
|
|
184
|
-
{Array.from(
|
|
185
|
-
{ length: 24 },
|
|
186
|
-
(_, index) =>
|
|
187
|
-
`14:0${index % 10}:22 title ${
|
|
188
|
-
index + 1
|
|
189
|
-
} AACS handshake complete, reading…`,
|
|
190
|
-
).map((line) => (
|
|
191
|
-
<p
|
|
192
|
-
className="font-mono text-content-secondary text-xs"
|
|
193
|
-
key={line}
|
|
194
|
-
>
|
|
195
|
-
{line}
|
|
196
|
-
</p>
|
|
197
|
-
))}
|
|
198
|
-
</div>
|
|
199
|
-
</ModalDemo>
|
|
200
|
-
</StoryRow>
|
|
201
|
-
),
|
|
202
|
-
}
|
|
203
|
-
|
|
204
91
|
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
* covering the viewport — and boarding it at three container widths
|
|
209
|
-
* would be measuring something that has no bearing on it.
|
|
210
|
-
*
|
|
211
|
-
* What does vary is the clamp: every size caps at `85dvh`, and
|
|
212
|
-
* `full` takes the lot. `dvh` rather than `vh` because on a phone
|
|
213
|
-
* the two differ by the address bar, and a dialog whose footer is
|
|
214
|
-
* underneath it is a dialog with no buttons.
|
|
92
|
+
* `role="alertdialog"` is the one configuration the base exposes that
|
|
93
|
+
* `Dialog` does not surface directly — an interruption the user has
|
|
94
|
+
* to answer, announced as an alert rather than a plain dialog.
|
|
215
95
|
*/
|
|
216
|
-
export const
|
|
96
|
+
export const Alert: Story = {
|
|
217
97
|
args: {
|
|
218
|
-
|
|
98
|
+
"aria-label": "Confirm erase",
|
|
99
|
+
children: null,
|
|
219
100
|
isVisible: false,
|
|
220
101
|
onClose: () => {},
|
|
221
102
|
},
|
|
222
103
|
render: () => (
|
|
223
104
|
<ModalDemo
|
|
224
|
-
buttonLabel="Open
|
|
225
|
-
|
|
226
|
-
|
|
105
|
+
buttonLabel="Open an alertdialog"
|
|
106
|
+
label="Confirm erase"
|
|
107
|
+
role="alertdialog"
|
|
227
108
|
>
|
|
228
109
|
<p className="text-content-secondary text-sm">
|
|
229
|
-
|
|
230
|
-
the screen, so there is nothing behind it worth
|
|
231
|
-
showing.
|
|
110
|
+
This erases eight completed titles.
|
|
232
111
|
</p>
|
|
233
112
|
</ModalDemo>
|
|
234
113
|
),
|
|
235
114
|
}
|
|
236
115
|
|
|
237
116
|
/**
|
|
238
|
-
* The keyboard contract, driven. `
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* `onCancel` → `hide()` → the effect → `close()`, rather than the
|
|
242
|
-
* browser closing the dialog behind the app's back.
|
|
117
|
+
* The keyboard contract, driven. `OverlayPanel`'s
|
|
118
|
+
* `FloatingFocusManager` traps focus and restores it to the trigger;
|
|
119
|
+
* `useDismiss` routes Escape and an outside press through `onClose`.
|
|
243
120
|
*/
|
|
244
121
|
export const Interactive: Story = {
|
|
245
122
|
args: {
|
|
246
|
-
|
|
123
|
+
"aria-label": "Stop the rip?",
|
|
124
|
+
children: null,
|
|
247
125
|
isVisible: false,
|
|
248
126
|
onClose: () => {},
|
|
249
127
|
},
|
|
250
128
|
render: () => (
|
|
251
129
|
<ModalDemo
|
|
252
130
|
buttonLabel="Stop the rip"
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
>
|
|
261
|
-
Keep going
|
|
262
|
-
</Button>
|
|
263
|
-
|
|
264
|
-
<Button intent="danger" size="sm">
|
|
265
|
-
Stop it now
|
|
266
|
-
</Button>
|
|
267
|
-
</>
|
|
268
|
-
)}
|
|
269
|
-
heading="Stop the rip?"
|
|
270
|
-
/>
|
|
131
|
+
label="Stop the rip?"
|
|
132
|
+
>
|
|
133
|
+
<p className="text-content-secondary text-sm">
|
|
134
|
+
Escape, an outside press, or the button below all
|
|
135
|
+
land on the same `onClose`.
|
|
136
|
+
</p>
|
|
137
|
+
</ModalDemo>
|
|
271
138
|
),
|
|
272
139
|
}
|
package/src/Modal/Modal.test.tsx
CHANGED
|
@@ -7,216 +7,85 @@ import { mountStory } from "../mountStory.testHelpers.ts"
|
|
|
7
7
|
import { expectAgentDrivable } from "../testing/index.ts"
|
|
8
8
|
import * as stories from "./Modal.stories.tsx"
|
|
9
9
|
|
|
10
|
-
const {
|
|
11
|
-
|
|
12
|
-
AllVariants,
|
|
13
|
-
Default,
|
|
14
|
-
Interactive,
|
|
15
|
-
Responsive,
|
|
16
|
-
} = composeStories(stories)
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Escape, as far as a test can press it — and the limit is worth
|
|
20
|
-
* stating rather than working around quietly.
|
|
21
|
-
*
|
|
22
|
-
* `userEvent` is `@testing-library/user-event`, which dispatches
|
|
23
|
-
* **untrusted** events. The browser runs a default action only for
|
|
24
|
-
* trusted input, so a synthetic Escape keydown does not make a
|
|
25
|
-
* native `<dialog>` fire `cancel` — it does nothing at all. A test
|
|
26
|
-
* asserting "Escape did not close the non-dismissable dialog"
|
|
27
|
-
* against a synthetic keypress therefore passes for the wrong
|
|
28
|
-
* reason, forever, which is worse than not testing it.
|
|
29
|
-
*
|
|
30
|
-
* So the close request is dispatched where the browser would raise
|
|
31
|
-
* it. That splits the contract honestly: the keystroke → `cancel`
|
|
32
|
-
* half is the platform's and is not ours to test, and everything
|
|
33
|
-
* downstream of `cancel` — `preventDefault`, `hide()`, the effect,
|
|
34
|
-
* `close()`, focus restore — is ours and is asserted here.
|
|
35
|
-
*/
|
|
36
|
-
const requestClose = (dialog: HTMLElement) => {
|
|
37
|
-
dialog.dispatchEvent(
|
|
38
|
-
new Event("cancel", { cancelable: true }),
|
|
39
|
-
)
|
|
40
|
-
}
|
|
10
|
+
const { Alert, Default, Interactive } =
|
|
11
|
+
composeStories(stories)
|
|
41
12
|
|
|
42
|
-
test("the
|
|
43
|
-
const { canvas, canvasElement } =
|
|
13
|
+
test("the base modal portals to the body, named by its aria-label", async () => {
|
|
14
|
+
const { body, canvas, canvasElement } =
|
|
44
15
|
await mountStory(Default)
|
|
45
16
|
|
|
46
17
|
await userEvent.click(
|
|
47
18
|
expectAgentDrivable(canvas, {
|
|
48
|
-
name: "
|
|
19
|
+
name: "Open the base modal",
|
|
49
20
|
role: "button",
|
|
50
21
|
}),
|
|
51
22
|
)
|
|
52
23
|
|
|
53
|
-
|
|
54
|
-
// the dialog paints in the top layer while staying inside the
|
|
55
|
-
// story root, so an agent's scoped query still reaches it.
|
|
56
|
-
const dialog = expectAgentDrivable(canvas, {
|
|
24
|
+
const modal = expectAgentDrivable(body, {
|
|
57
25
|
name: "Read error on title 4",
|
|
58
26
|
role: "dialog",
|
|
59
27
|
})
|
|
60
28
|
|
|
61
|
-
|
|
29
|
+
// Portalled out of the canvas — the whole point over the top layer.
|
|
30
|
+
await expect(canvasElement).not.toContainElement(modal)
|
|
62
31
|
|
|
63
|
-
|
|
64
|
-
// so the *open* dialog audits itself.
|
|
65
|
-
await expectNoAxeViolations(canvasElement)
|
|
32
|
+
await expectNoAxeViolations(modal)
|
|
66
33
|
})
|
|
67
34
|
|
|
68
|
-
test("
|
|
69
|
-
const { canvas } = await mountStory(
|
|
35
|
+
test("an alertdialog role is carried through", async () => {
|
|
36
|
+
const { body, canvas } = await mountStory(Alert)
|
|
70
37
|
|
|
71
38
|
await userEvent.click(
|
|
72
39
|
expectAgentDrivable(canvas, {
|
|
73
|
-
name: "Open
|
|
40
|
+
name: "Open an alertdialog",
|
|
74
41
|
role: "button",
|
|
75
42
|
}),
|
|
76
43
|
)
|
|
77
44
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
45
|
+
// The one configuration the base surfaces that `Dialog` does not.
|
|
46
|
+
expectAgentDrivable(body, {
|
|
47
|
+
name: "Confirm erase",
|
|
48
|
+
role: "alertdialog",
|
|
81
49
|
})
|
|
82
|
-
|
|
83
|
-
await expect(
|
|
84
|
-
dialog.getBoundingClientRect().width,
|
|
85
|
-
).toBeGreaterThan(400)
|
|
86
50
|
})
|
|
87
51
|
|
|
88
|
-
test("
|
|
89
|
-
const { canvas } = await mountStory(
|
|
90
|
-
|
|
91
|
-
await userEvent.click(
|
|
92
|
-
expectAgentDrivable(canvas, {
|
|
93
|
-
name: "Not dismissable",
|
|
94
|
-
role: "button",
|
|
95
|
-
}),
|
|
96
|
-
)
|
|
52
|
+
test("Escape and an outside press both close through onClose", async () => {
|
|
53
|
+
const { body, canvas } = await mountStory(Default)
|
|
97
54
|
|
|
98
|
-
const
|
|
99
|
-
name: "
|
|
100
|
-
role: "
|
|
55
|
+
const trigger = expectAgentDrivable(canvas, {
|
|
56
|
+
name: "Open the base modal",
|
|
57
|
+
role: "button",
|
|
101
58
|
})
|
|
102
59
|
|
|
103
|
-
|
|
104
|
-
// rather than ignored — letting the browser close it would leave
|
|
105
|
-
// `isVisible` true and the element shut, which is the desync the
|
|
106
|
-
// component exists to prevent.
|
|
107
|
-
await expect(
|
|
108
|
-
canvas.queryByRole("button", { name: "Close" }),
|
|
109
|
-
).toBeNull()
|
|
110
|
-
|
|
111
|
-
requestClose(dialog)
|
|
112
|
-
|
|
113
|
-
// `find*` is async, which is what gives React's update from that
|
|
114
|
-
// dispatch a chance to flush. If the request *had* got through,
|
|
115
|
-
// this query would fail rather than the assertion below passing by
|
|
116
|
-
// being too quick.
|
|
117
|
-
const keepButton = await canvas.findByRole("button", {
|
|
118
|
-
name: "Keep the titles",
|
|
119
|
-
})
|
|
60
|
+
await userEvent.click(trigger)
|
|
120
61
|
|
|
121
|
-
|
|
62
|
+
expectAgentDrivable(body, {
|
|
63
|
+
name: "Read error on title 4",
|
|
64
|
+
role: "dialog",
|
|
65
|
+
})
|
|
122
66
|
|
|
123
|
-
|
|
124
|
-
// satisfied by a dialog nothing can close at all — a green
|
|
125
|
-
// assertion about a component that does not work.
|
|
126
|
-
await userEvent.click(keepButton)
|
|
67
|
+
await userEvent.keyboard("{Escape}")
|
|
127
68
|
|
|
128
69
|
await waitFor(() => {
|
|
129
|
-
expect(
|
|
70
|
+
expect(body.queryByRole("dialog")).toBeNull()
|
|
130
71
|
})
|
|
131
|
-
})
|
|
132
72
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
await mountStory(AllStates)
|
|
136
|
-
|
|
137
|
-
await userEvent.click(
|
|
138
|
-
canvas.getByRole("button", { name: "Long body" }),
|
|
139
|
-
)
|
|
73
|
+
// Reopened, then dismissed by pressing the page behind it.
|
|
74
|
+
await userEvent.click(trigger)
|
|
140
75
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
role: "dialog",
|
|
76
|
+
await waitFor(() => {
|
|
77
|
+
expect(body.getByRole("dialog")).toBeInTheDocument()
|
|
144
78
|
})
|
|
145
79
|
|
|
146
|
-
|
|
147
|
-
// stops at `85dvh` rather than running off the screen.
|
|
148
|
-
await expect(
|
|
149
|
-
log.getBoundingClientRect().height,
|
|
150
|
-
).toBeLessThan(globalThis.innerHeight)
|
|
80
|
+
await userEvent.click(document.body)
|
|
151
81
|
|
|
152
|
-
await
|
|
153
|
-
|
|
154
|
-
).toBeInTheDocument()
|
|
155
|
-
|
|
156
|
-
// The case that would trip `scrollable-region-focusable` if the
|
|
157
|
-
// body scrolled with nothing inside it to focus.
|
|
158
|
-
await expectNoAxeViolations(canvasElement)
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
test("the scrim is the token, not Chromium's default backdrop", async () => {
|
|
162
|
-
const { canvas } = await mountStory(Responsive)
|
|
163
|
-
|
|
164
|
-
await userEvent.click(
|
|
165
|
-
expectAgentDrivable(canvas, {
|
|
166
|
-
name: "Open full-bleed",
|
|
167
|
-
role: "button",
|
|
168
|
-
}),
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
const dialog = expectAgentDrivable(canvas, {
|
|
172
|
-
name: "MakeMKV log — full",
|
|
173
|
-
role: "dialog",
|
|
82
|
+
await waitFor(() => {
|
|
83
|
+
expect(body.queryByRole("dialog")).toBeNull()
|
|
174
84
|
})
|
|
175
|
-
|
|
176
|
-
await expect(
|
|
177
|
-
dialog.getBoundingClientRect().height,
|
|
178
|
-
).toBeGreaterThan(globalThis.innerHeight * 0.95)
|
|
179
|
-
|
|
180
|
-
// `::backdrop` inheriting custom properties from its originating
|
|
181
|
-
// element is the half that could silently not work.
|
|
182
|
-
//
|
|
183
|
-
// Compared against the token's *resolved* value rather than
|
|
184
|
-
// against "not transparent", which M4 learned the hard way:
|
|
185
|
-
// Chromium's own `::backdrop` is `rgba(0, 0, 0, 0.1)`, so the
|
|
186
|
-
// loose version of this assertion stayed green against a token
|
|
187
|
-
// build where `bg-scrim` generated no CSS at all.
|
|
188
|
-
//
|
|
189
|
-
// The probe is how the two become comparable — the same `var()`
|
|
190
|
-
// put through `background-color` comes back in the identical
|
|
191
|
-
// serialisation the backdrop uses.
|
|
192
|
-
const probe = document.createElement("div")
|
|
193
|
-
|
|
194
|
-
probe.style.backgroundColor = "var(--color-scrim)"
|
|
195
|
-
|
|
196
|
-
document.body.append(probe)
|
|
197
|
-
|
|
198
|
-
const expectedScrim =
|
|
199
|
-
globalThis.getComputedStyle(probe).backgroundColor
|
|
200
|
-
|
|
201
|
-
probe.remove()
|
|
202
|
-
|
|
203
|
-
await expect(expectedScrim).not.toBe("rgba(0, 0, 0, 0)")
|
|
204
|
-
|
|
205
|
-
await expect(
|
|
206
|
-
globalThis.getComputedStyle(dialog, "::backdrop")
|
|
207
|
-
.backgroundColor,
|
|
208
|
-
).toBe(expectedScrim)
|
|
209
85
|
})
|
|
210
86
|
|
|
211
|
-
/**
|
|
212
|
-
* `showModal()` supplies the focus trap and the focus restore; what
|
|
213
|
-
* this asserts is that the state layer never disagrees with the
|
|
214
|
-
* element — Escape routes through `onCancel` → `hide()` → the
|
|
215
|
-
* effect → `close()`, rather than the browser closing the dialog
|
|
216
|
-
* behind the app's back.
|
|
217
|
-
*/
|
|
218
87
|
test("focus is trapped inside, and restored to the trigger on close", async () => {
|
|
219
|
-
const { canvas } = await mountStory(Interactive)
|
|
88
|
+
const { body, canvas } = await mountStory(Interactive)
|
|
220
89
|
|
|
221
90
|
const trigger = expectAgentDrivable(canvas, {
|
|
222
91
|
name: "Stop the rip",
|
|
@@ -225,24 +94,22 @@ test("focus is trapped inside, and restored to the trigger on close", async () =
|
|
|
225
94
|
|
|
226
95
|
await userEvent.click(trigger)
|
|
227
96
|
|
|
228
|
-
const
|
|
97
|
+
const modal = expectAgentDrivable(body, {
|
|
229
98
|
name: "Stop the rip?",
|
|
230
99
|
role: "dialog",
|
|
231
100
|
})
|
|
232
101
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
)
|
|
102
|
+
await waitFor(() => {
|
|
103
|
+
expect(modal.contains(document.activeElement)).toBe(
|
|
104
|
+
true,
|
|
105
|
+
)
|
|
106
|
+
})
|
|
238
107
|
|
|
239
|
-
|
|
108
|
+
await userEvent.keyboard("{Escape}")
|
|
240
109
|
|
|
241
110
|
await waitFor(() => {
|
|
242
|
-
expect(
|
|
111
|
+
expect(body.queryByRole("dialog")).toBeNull()
|
|
243
112
|
})
|
|
244
113
|
|
|
245
|
-
// Focus restored to what opened it — also the platform's, and the
|
|
246
|
-
// thing hand-rolled modals in the fleet lose.
|
|
247
114
|
await expect(document.activeElement).toBe(trigger)
|
|
248
115
|
})
|