@lovett/ui 0.0.9 → 0.0.11
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/index.d.ts +823 -135
- package/dist/index.js +2048 -358
- package/dist/index.js.map +1 -1
- package/dist/styles.css +44 -2
- package/dist/theme-v2.css +228 -0
- package/dist/tokens.css +123 -8
- package/package.json +1 -1
- package/src/__tests__/anchor.test.tsx +422 -0
- package/src/__tests__/combobox.test.tsx +677 -0
- package/src/__tests__/dropdown-menu.test.tsx +418 -0
- package/src/__tests__/helpers/geometry.ts +58 -0
- package/src/__tests__/layer-stack.test.tsx +228 -0
- package/src/__tests__/modal.test.tsx +180 -6
- package/src/__tests__/popover.test.tsx +460 -0
- package/src/__tests__/select.test.tsx +543 -0
- package/src/__tests__/tooltip.test.tsx +355 -0
- package/src/calculator-shell-v2.tsx +19 -39
- package/src/code-block.tsx +15 -26
- package/src/combobox.tsx +796 -0
- package/src/dropdown-menu.tsx +142 -152
- package/src/icons/brand.tsx +81 -2
- package/src/index.ts +111 -0
- package/src/lib/anchor.ts +427 -0
- package/src/lib/focus.ts +32 -0
- package/src/lib/layer-stack.ts +188 -0
- package/src/lib/refs.ts +31 -0
- package/src/metric-card.tsx +57 -22
- package/src/modal.tsx +149 -9
- package/src/page-shell.tsx +91 -2
- package/src/popover.tsx +407 -0
- package/src/segmented-pill.tsx +33 -10
- package/src/select.tsx +646 -0
- package/src/stat-row.tsx +108 -70
- package/src/styles.css +44 -2
- package/src/theme-v2.css +7 -245
- package/src/tokens.css +123 -8
- package/src/tooltip.tsx +297 -0
- package/src/react-syntax-highlighter-prism.d.ts +0 -34
- package/src/v2/README.md +0 -208
- package/src/v2/__demo__/showcase.tsx +0 -1045
- package/src/v2/action.tsx +0 -91
- package/src/v2/callout.tsx +0 -76
- package/src/v2/document-section.tsx +0 -82
- package/src/v2/document-shell.tsx +0 -0
- package/src/v2/field-row.tsx +0 -113
- package/src/v2/icons.tsx +0 -165
- package/src/v2/index.ts +0 -147
- package/src/v2/layout.tsx +0 -293
- package/src/v2/progress-track.tsx +0 -89
- package/src/v2/stat-tile.tsx +0 -129
- package/src/v2/states.tsx +0 -271
- package/src/v2/status-pill.tsx +0 -74
- package/src/v2/theme.css +0 -1861
- package/src/v2/timeline.tsx +0 -81
- package/src/v2/tokens.ts +0 -228
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/layer-stack — the topmost-wins dismiss order shared by Modal,
|
|
3
|
+
* DropdownMenu and Popover.
|
|
4
|
+
*
|
|
5
|
+
* The load-bearing test is the Modal one: a layer registered AFTER a Modal
|
|
6
|
+
* opens takes Escape away from it. Revert Modal to a private `openPanels`
|
|
7
|
+
* array and both fire — that test goes red.
|
|
8
|
+
*/
|
|
9
|
+
import { useEffect } from 'react'
|
|
10
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
11
|
+
import { render } from '@testing-library/react'
|
|
12
|
+
import userEvent from '@testing-library/user-event'
|
|
13
|
+
import Modal from '../modal'
|
|
14
|
+
import { useEscapeKey, useLayer, type LayerHandle, type LayerKind } from '../lib/layer-stack'
|
|
15
|
+
|
|
16
|
+
function EscapeListener({
|
|
17
|
+
onEscape,
|
|
18
|
+
enabled = true,
|
|
19
|
+
}: {
|
|
20
|
+
onEscape: () => void
|
|
21
|
+
enabled?: boolean
|
|
22
|
+
}) {
|
|
23
|
+
useEscapeKey(onEscape, { enabled })
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function LayerProbe({
|
|
28
|
+
kind,
|
|
29
|
+
onHandle,
|
|
30
|
+
}: {
|
|
31
|
+
kind: LayerKind
|
|
32
|
+
onHandle: (handle: LayerHandle) => void
|
|
33
|
+
}) {
|
|
34
|
+
const handle = useLayer({ enabled: true, kind, onEscape: () => {} })
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
onHandle(handle)
|
|
37
|
+
}, [handle, onHandle])
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('useEscapeKey / useLayer', () => {
|
|
42
|
+
it('dispatches Escape to the most recently registered layer only', async () => {
|
|
43
|
+
const first = vi.fn()
|
|
44
|
+
const second = vi.fn()
|
|
45
|
+
render(
|
|
46
|
+
<>
|
|
47
|
+
<EscapeListener onEscape={first} />
|
|
48
|
+
<EscapeListener onEscape={second} />
|
|
49
|
+
</>,
|
|
50
|
+
)
|
|
51
|
+
await userEvent.keyboard('{Escape}')
|
|
52
|
+
expect(second).toHaveBeenCalledTimes(1)
|
|
53
|
+
expect(first).not.toHaveBeenCalled()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('falls back to the layer below when the top one unregisters', async () => {
|
|
57
|
+
const first = vi.fn()
|
|
58
|
+
const second = vi.fn()
|
|
59
|
+
const { rerender } = render(
|
|
60
|
+
<>
|
|
61
|
+
<EscapeListener onEscape={first} />
|
|
62
|
+
<EscapeListener onEscape={second} />
|
|
63
|
+
</>,
|
|
64
|
+
)
|
|
65
|
+
rerender(
|
|
66
|
+
<>
|
|
67
|
+
<EscapeListener onEscape={first} />
|
|
68
|
+
<EscapeListener onEscape={second} enabled={false} />
|
|
69
|
+
</>,
|
|
70
|
+
)
|
|
71
|
+
await userEvent.keyboard('{Escape}')
|
|
72
|
+
expect(first).toHaveBeenCalledTimes(1)
|
|
73
|
+
expect(second).not.toHaveBeenCalled()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('does nothing once every layer has unregistered', async () => {
|
|
77
|
+
const handler = vi.fn()
|
|
78
|
+
const { unmount } = render(<EscapeListener onEscape={handler} />)
|
|
79
|
+
unmount()
|
|
80
|
+
await userEvent.keyboard('{Escape}')
|
|
81
|
+
expect(handler).not.toHaveBeenCalled()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('ignores keys other than Escape', async () => {
|
|
85
|
+
const handler = vi.fn()
|
|
86
|
+
render(<EscapeListener onEscape={handler} />)
|
|
87
|
+
await userEvent.keyboard('{Enter}a{Tab}')
|
|
88
|
+
expect(handler).not.toHaveBeenCalled()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('stops propagation so window-level listeners do not see a consumed Escape', async () => {
|
|
92
|
+
const windowSpy = vi.fn()
|
|
93
|
+
window.addEventListener('keydown', windowSpy)
|
|
94
|
+
try {
|
|
95
|
+
const handler = vi.fn()
|
|
96
|
+
const { unmount } = render(<EscapeListener onEscape={handler} />)
|
|
97
|
+
await userEvent.keyboard('{Escape}')
|
|
98
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
99
|
+
expect(windowSpy).not.toHaveBeenCalled()
|
|
100
|
+
|
|
101
|
+
// With no layer open the key reaches window as before.
|
|
102
|
+
unmount()
|
|
103
|
+
await userEvent.keyboard('{Escape}')
|
|
104
|
+
expect(windowSpy).toHaveBeenCalledTimes(1)
|
|
105
|
+
} finally {
|
|
106
|
+
window.removeEventListener('keydown', windowSpy)
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('reads the latest handler without re-registering (order is preserved)', async () => {
|
|
111
|
+
const stale = vi.fn()
|
|
112
|
+
const fresh = vi.fn()
|
|
113
|
+
const above = vi.fn()
|
|
114
|
+
const { rerender } = render(
|
|
115
|
+
<>
|
|
116
|
+
<EscapeListener onEscape={stale} />
|
|
117
|
+
<EscapeListener onEscape={above} />
|
|
118
|
+
</>,
|
|
119
|
+
)
|
|
120
|
+
// Swapping the LOWER layer's handler must not move it to the top.
|
|
121
|
+
rerender(
|
|
122
|
+
<>
|
|
123
|
+
<EscapeListener onEscape={fresh} />
|
|
124
|
+
<EscapeListener onEscape={above} />
|
|
125
|
+
</>,
|
|
126
|
+
)
|
|
127
|
+
await userEvent.keyboard('{Escape}')
|
|
128
|
+
expect(above).toHaveBeenCalledTimes(1)
|
|
129
|
+
expect(stale).not.toHaveBeenCalled()
|
|
130
|
+
expect(fresh).not.toHaveBeenCalled()
|
|
131
|
+
|
|
132
|
+
rerender(
|
|
133
|
+
<>
|
|
134
|
+
<EscapeListener onEscape={fresh} />
|
|
135
|
+
<EscapeListener onEscape={above} enabled={false} />
|
|
136
|
+
</>,
|
|
137
|
+
)
|
|
138
|
+
await userEvent.keyboard('{Escape}')
|
|
139
|
+
expect(fresh).toHaveBeenCalledTimes(1)
|
|
140
|
+
expect(stale).not.toHaveBeenCalled()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('isTop / isTopOfKind: a popover above a modal is top, but not top-of-kind for the modal', () => {
|
|
144
|
+
let modal: LayerHandle | undefined
|
|
145
|
+
let popover: LayerHandle | undefined
|
|
146
|
+
render(
|
|
147
|
+
<>
|
|
148
|
+
<LayerProbe kind="modal" onHandle={(h) => (modal = h)} />
|
|
149
|
+
<LayerProbe kind="popover" onHandle={(h) => (popover = h)} />
|
|
150
|
+
</>,
|
|
151
|
+
)
|
|
152
|
+
expect(popover?.isTop()).toBe(true)
|
|
153
|
+
expect(popover?.isTopOfKind()).toBe(true)
|
|
154
|
+
expect(modal?.isTop()).toBe(false)
|
|
155
|
+
expect(modal?.isTopOfKind()).toBe(true)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('a second modal above the first takes both isTop and isTopOfKind', () => {
|
|
159
|
+
let lower: LayerHandle | undefined
|
|
160
|
+
let upper: LayerHandle | undefined
|
|
161
|
+
render(
|
|
162
|
+
<>
|
|
163
|
+
<LayerProbe kind="modal" onHandle={(h) => (lower = h)} />
|
|
164
|
+
<LayerProbe kind="modal" onHandle={(h) => (upper = h)} />
|
|
165
|
+
</>,
|
|
166
|
+
)
|
|
167
|
+
expect(upper?.isTop()).toBe(true)
|
|
168
|
+
expect(lower?.isTopOfKind()).toBe(false)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('an unregistered handle answers false everywhere', () => {
|
|
172
|
+
let handle: LayerHandle | undefined
|
|
173
|
+
function Disabled() {
|
|
174
|
+
handle = useLayer({ enabled: false, kind: 'popover', onEscape: () => {} })
|
|
175
|
+
return null
|
|
176
|
+
}
|
|
177
|
+
render(<Disabled />)
|
|
178
|
+
expect(handle?.isTop()).toBe(false)
|
|
179
|
+
expect(handle?.isTopOfKind()).toBe(false)
|
|
180
|
+
expect(handle?.containsInLayerAbove(document.body)).toBe(false)
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
describe('shared with Modal', () => {
|
|
185
|
+
it('a layer opened above a Modal takes Escape; the Modal gets the next one', async () => {
|
|
186
|
+
const onClose = vi.fn()
|
|
187
|
+
const onEscape = vi.fn()
|
|
188
|
+
const { rerender } = render(
|
|
189
|
+
<>
|
|
190
|
+
<Modal isOpen onClose={onClose} title="Confirm">
|
|
191
|
+
body
|
|
192
|
+
</Modal>
|
|
193
|
+
<EscapeListener onEscape={onEscape} />
|
|
194
|
+
</>,
|
|
195
|
+
)
|
|
196
|
+
await userEvent.keyboard('{Escape}')
|
|
197
|
+
expect(onEscape).toHaveBeenCalledTimes(1)
|
|
198
|
+
expect(onClose).not.toHaveBeenCalled()
|
|
199
|
+
|
|
200
|
+
rerender(
|
|
201
|
+
<>
|
|
202
|
+
<Modal isOpen onClose={onClose} title="Confirm">
|
|
203
|
+
body
|
|
204
|
+
</Modal>
|
|
205
|
+
<EscapeListener onEscape={onEscape} enabled={false} />
|
|
206
|
+
</>,
|
|
207
|
+
)
|
|
208
|
+
await userEvent.keyboard('{Escape}')
|
|
209
|
+
expect(onClose).toHaveBeenCalledTimes(1)
|
|
210
|
+
expect(onEscape).toHaveBeenCalledTimes(1)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('a Modal opened above an existing layer takes Escape from it', async () => {
|
|
214
|
+
const onClose = vi.fn()
|
|
215
|
+
const onEscape = vi.fn()
|
|
216
|
+
render(
|
|
217
|
+
<>
|
|
218
|
+
<EscapeListener onEscape={onEscape} />
|
|
219
|
+
<Modal isOpen onClose={onClose} title="Confirm">
|
|
220
|
+
body
|
|
221
|
+
</Modal>
|
|
222
|
+
</>,
|
|
223
|
+
)
|
|
224
|
+
await userEvent.keyboard('{Escape}')
|
|
225
|
+
expect(onClose).toHaveBeenCalledTimes(1)
|
|
226
|
+
expect(onEscape).not.toHaveBeenCalled()
|
|
227
|
+
})
|
|
228
|
+
})
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Modal — 51 consuming files plus 113 sub-component call sites.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* The panel is a role="dialog" aria-modal="true" region: named by its own
|
|
5
|
+
* <h2> (or the `ariaLabel` prop when titleless), focused on open, focus
|
|
6
|
+
* RESTORED to the opener on close, and Tab/Shift+Tab wrapped inside it.
|
|
7
|
+
* The "a11y gap" block that used to stand here — no dialog role, no trap,
|
|
8
|
+
* no restore — is closed, and the describe() below is what closes it.
|
|
9
|
+
*
|
|
10
|
+
* Background inertness is by focus trap, NOT the `inert` attribute: Modal
|
|
11
|
+
* renders inline inside #root, so inerting the app root would inert the
|
|
12
|
+
* modal too. See the note in modal.tsx.
|
|
10
13
|
*/
|
|
14
|
+
import { useState } from 'react'
|
|
11
15
|
import { describe, expect, it, vi } from 'vitest'
|
|
12
16
|
import { render, screen } from '@testing-library/react'
|
|
13
17
|
import userEvent from '@testing-library/user-event'
|
|
@@ -142,6 +146,176 @@ describe('Modal', () => {
|
|
|
142
146
|
})
|
|
143
147
|
})
|
|
144
148
|
|
|
149
|
+
describe('accessibility', () => {
|
|
150
|
+
it('exposes the panel as a modal dialog named by its title', () => {
|
|
151
|
+
render(
|
|
152
|
+
<Modal isOpen onClose={noop} title="Confirm">
|
|
153
|
+
body
|
|
154
|
+
</Modal>,
|
|
155
|
+
)
|
|
156
|
+
const dialog = screen.getByRole('dialog', { name: 'Confirm' })
|
|
157
|
+
expect(dialog).toHaveAttribute('aria-modal', 'true')
|
|
158
|
+
expect(dialog).toHaveClass('ds-card-surface')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('gives each open modal its own title id', () => {
|
|
162
|
+
render(
|
|
163
|
+
<>
|
|
164
|
+
<Modal isOpen onClose={noop} title="First">
|
|
165
|
+
a
|
|
166
|
+
</Modal>
|
|
167
|
+
<Modal isOpen onClose={noop} title="Second">
|
|
168
|
+
b
|
|
169
|
+
</Modal>
|
|
170
|
+
</>,
|
|
171
|
+
)
|
|
172
|
+
const [first, second] = screen.getAllByRole('dialog')
|
|
173
|
+
expect(first!.getAttribute('aria-labelledby')).not.toBe(
|
|
174
|
+
second!.getAttribute('aria-labelledby'),
|
|
175
|
+
)
|
|
176
|
+
expect(screen.getByRole('dialog', { name: 'First' })).toBeInTheDocument()
|
|
177
|
+
expect(screen.getByRole('dialog', { name: 'Second' })).toBeInTheDocument()
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('falls back to ariaLabel when there is no title', () => {
|
|
181
|
+
render(
|
|
182
|
+
<Modal isOpen onClose={noop} title="" ariaLabel="Asset details">
|
|
183
|
+
body
|
|
184
|
+
</Modal>,
|
|
185
|
+
)
|
|
186
|
+
const dialog = screen.getByRole('dialog', { name: 'Asset details' })
|
|
187
|
+
expect(dialog).not.toHaveAttribute('aria-labelledby')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('moves focus to the panel on open', () => {
|
|
191
|
+
render(
|
|
192
|
+
<Modal isOpen onClose={noop} title="Confirm">
|
|
193
|
+
<button type="button">Inside</button>
|
|
194
|
+
</Modal>,
|
|
195
|
+
)
|
|
196
|
+
expect(document.activeElement).toBe(screen.getByRole('dialog'))
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('honours a [data-autofocus] control', () => {
|
|
200
|
+
render(
|
|
201
|
+
<Modal isOpen onClose={noop} title="Confirm">
|
|
202
|
+
<input data-autofocus aria-label="Name" />
|
|
203
|
+
</Modal>,
|
|
204
|
+
)
|
|
205
|
+
expect(document.activeElement).toBe(screen.getByRole('textbox'))
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('restores focus to the opener on close', async () => {
|
|
209
|
+
function Harness() {
|
|
210
|
+
const [open, setOpen] = useState(false)
|
|
211
|
+
return (
|
|
212
|
+
<>
|
|
213
|
+
<button type="button" onClick={() => setOpen(true)}>
|
|
214
|
+
Open
|
|
215
|
+
</button>
|
|
216
|
+
<Modal isOpen={open} onClose={() => setOpen(false)} title="Confirm">
|
|
217
|
+
body
|
|
218
|
+
</Modal>
|
|
219
|
+
</>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
render(<Harness />)
|
|
223
|
+
const opener = screen.getByRole('button', { name: 'Open' })
|
|
224
|
+
await userEvent.click(opener)
|
|
225
|
+
expect(document.activeElement).toBe(screen.getByRole('dialog'))
|
|
226
|
+
|
|
227
|
+
await userEvent.click(screen.getByRole('button', { name: 'Close' }))
|
|
228
|
+
expect(screen.queryByRole('dialog')).toBeNull()
|
|
229
|
+
expect(document.activeElement).toBe(opener)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('restores focus when the modal unmounts outright', async () => {
|
|
233
|
+
const opener = document.createElement('button')
|
|
234
|
+
document.body.appendChild(opener)
|
|
235
|
+
opener.focus()
|
|
236
|
+
const { unmount } = render(
|
|
237
|
+
<Modal isOpen onClose={noop} title="Confirm">
|
|
238
|
+
body
|
|
239
|
+
</Modal>,
|
|
240
|
+
)
|
|
241
|
+
expect(document.activeElement).not.toBe(opener)
|
|
242
|
+
unmount()
|
|
243
|
+
expect(document.activeElement).toBe(opener)
|
|
244
|
+
opener.remove()
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('wraps Tab from the last control back to the first', async () => {
|
|
248
|
+
render(
|
|
249
|
+
<Modal isOpen onClose={noop} title="Confirm">
|
|
250
|
+
<button type="button">Last</button>
|
|
251
|
+
</Modal>,
|
|
252
|
+
)
|
|
253
|
+
const close = screen.getByRole('button', { name: 'Close' })
|
|
254
|
+
const last = screen.getByRole('button', { name: 'Last' })
|
|
255
|
+
last.focus()
|
|
256
|
+
await userEvent.tab()
|
|
257
|
+
expect(document.activeElement).toBe(close)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('wraps Shift+Tab from the first control round to the last', async () => {
|
|
261
|
+
render(
|
|
262
|
+
<Modal isOpen onClose={noop} title="Confirm">
|
|
263
|
+
<button type="button">Last</button>
|
|
264
|
+
</Modal>,
|
|
265
|
+
)
|
|
266
|
+
const close = screen.getByRole('button', { name: 'Close' })
|
|
267
|
+
close.focus()
|
|
268
|
+
await userEvent.tab({ shift: true })
|
|
269
|
+
expect(document.activeElement).toBe(screen.getByRole('button', { name: 'Last' }))
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('wraps Shift+Tab from the panel itself round to the last control', async () => {
|
|
273
|
+
render(
|
|
274
|
+
<Modal isOpen onClose={noop} title="Confirm">
|
|
275
|
+
<button type="button">Last</button>
|
|
276
|
+
</Modal>,
|
|
277
|
+
)
|
|
278
|
+
expect(document.activeElement).toBe(screen.getByRole('dialog'))
|
|
279
|
+
await userEvent.tab({ shift: true })
|
|
280
|
+
expect(document.activeElement).toBe(screen.getByRole('button', { name: 'Last' }))
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('does not let Tab leave the panel for the page behind it', async () => {
|
|
284
|
+
const outside = document.createElement('button')
|
|
285
|
+
outside.textContent = 'Behind'
|
|
286
|
+
document.body.appendChild(outside)
|
|
287
|
+
render(
|
|
288
|
+
<Modal isOpen onClose={noop} title="Confirm">
|
|
289
|
+
<button type="button">Last</button>
|
|
290
|
+
</Modal>,
|
|
291
|
+
)
|
|
292
|
+
const dialog = screen.getByRole('dialog')
|
|
293
|
+
for (let i = 0; i < 6; i++) {
|
|
294
|
+
await userEvent.tab()
|
|
295
|
+
expect(dialog.contains(document.activeElement)).toBe(true)
|
|
296
|
+
}
|
|
297
|
+
outside.remove()
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('closes only the topmost modal on Escape', async () => {
|
|
301
|
+
const closeOuter = vi.fn()
|
|
302
|
+
const closeInner = vi.fn()
|
|
303
|
+
render(
|
|
304
|
+
<>
|
|
305
|
+
<Modal isOpen onClose={closeOuter} title="Outer">
|
|
306
|
+
outer
|
|
307
|
+
</Modal>
|
|
308
|
+
<Modal isOpen onClose={closeInner} title="Inner">
|
|
309
|
+
inner
|
|
310
|
+
</Modal>
|
|
311
|
+
</>,
|
|
312
|
+
)
|
|
313
|
+
await userEvent.keyboard('{Escape}')
|
|
314
|
+
expect(closeInner).toHaveBeenCalledTimes(1)
|
|
315
|
+
expect(closeOuter).not.toHaveBeenCalled()
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
|
|
145
319
|
it('merges a caller className onto the panel', () => {
|
|
146
320
|
render(
|
|
147
321
|
<Modal isOpen onClose={noop} title="Confirm" className="max-w-sm">
|