@estiva-app/ui 0.15.0 → 0.16.1
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/Breadcrumb.d.ts.map +1 -1
- package/dist/CommandPalette.d.ts +121 -0
- package/dist/CommandPalette.d.ts.map +1 -0
- package/dist/Menu.d.ts +4 -0
- package/dist/Menu.d.ts.map +1 -1
- package/dist/Toast.d.ts.map +1 -1
- package/dist/eslint/has-a-page-and-a-story.d.ts +4 -0
- package/dist/eslint/has-a-page-and-a-story.d.ts.map +1 -0
- package/dist/eslint/index.d.ts +15 -1
- package/dist/eslint/index.d.ts.map +1 -1
- package/dist/eslint/index.js +176 -8
- package/dist/eslint/index.js.map +3 -3
- package/dist/eslint/no-hand-rolled-behaviour.d.ts +3 -0
- package/dist/eslint/no-hand-rolled-behaviour.d.ts.map +1 -0
- package/dist/eslint/raw-element-outside-a-wrapper.d.ts +29 -0
- package/dist/eslint/raw-element-outside-a-wrapper.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +614 -287
- package/dist/index.js.map +4 -4
- package/package.json +5 -1
- package/src/Breadcrumb.tsx +6 -2
- package/src/CommandPalette.mdx +139 -0
- package/src/CommandPalette.stories.tsx +418 -0
- package/src/CommandPalette.test.tsx +415 -0
- package/src/CommandPalette.tsx +664 -0
- package/src/Menu.tsx +4 -2
- package/src/Toast.tsx +12 -24
- package/src/eslint/has-a-page-and-a-story.test.ts +57 -0
- package/src/eslint/has-a-page-and-a-story.ts +97 -0
- package/src/eslint/index.test.ts +35 -3
- package/src/eslint/index.ts +58 -13
- package/src/eslint/no-hand-rolled-behaviour.test.ts +70 -0
- package/src/eslint/no-hand-rolled-behaviour.ts +116 -0
- package/src/eslint/raw-element-outside-a-wrapper.test.ts +82 -0
- package/src/eslint/raw-element-outside-a-wrapper.ts +85 -0
- package/src/index.ts +17 -0
- package/stories/Choosing.mdx +1 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* What the CommandPalette page claims, pinned.
|
|
4
|
+
*
|
|
5
|
+
* Every key on the page's Keys table has a test here. They were first walked
|
|
6
|
+
* in Chrome on the stories (UIG-29, 16 September 2026), because a key that
|
|
7
|
+
* works in jsdom can still fail in a browser; what is here keeps them from
|
|
8
|
+
* quietly changing afterwards. Where Base UI did something the key list does
|
|
9
|
+
* not want — Home and End moving the highlight, a late row taking it — the
|
|
10
|
+
* test is the measured case.
|
|
11
|
+
*/
|
|
12
|
+
import { useState } from 'react'
|
|
13
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
14
|
+
import { cleanup, render, screen, waitFor, within } from '@testing-library/react'
|
|
15
|
+
import userEvent from '@testing-library/user-event'
|
|
16
|
+
import {
|
|
17
|
+
CommandPalette,
|
|
18
|
+
CommandPaletteAnswer,
|
|
19
|
+
CommandPaletteForm,
|
|
20
|
+
CommandPaletteQuote,
|
|
21
|
+
CommandPaletteSearch,
|
|
22
|
+
CommandPaletteWorking,
|
|
23
|
+
type CommandPaletteChip,
|
|
24
|
+
type CommandPaletteGroup,
|
|
25
|
+
type CommandPaletteRow,
|
|
26
|
+
} from './CommandPalette'
|
|
27
|
+
import { Field } from './Field'
|
|
28
|
+
import { Select } from './Select'
|
|
29
|
+
import { TextInput } from './TextInput'
|
|
30
|
+
|
|
31
|
+
afterEach(cleanup)
|
|
32
|
+
|
|
33
|
+
const row = (id: string, extra: Partial<CommandPaletteRow> = {}): CommandPaletteRow => ({ id, label: id, onSelect: () => {}, ...extra })
|
|
34
|
+
|
|
35
|
+
function Search({
|
|
36
|
+
groups,
|
|
37
|
+
chip,
|
|
38
|
+
initialQuery = '',
|
|
39
|
+
onOpenChange = () => {},
|
|
40
|
+
modKey,
|
|
41
|
+
pending,
|
|
42
|
+
notes,
|
|
43
|
+
empty,
|
|
44
|
+
}: {
|
|
45
|
+
groups: CommandPaletteGroup[]
|
|
46
|
+
chip?: CommandPaletteChip
|
|
47
|
+
initialQuery?: string
|
|
48
|
+
onOpenChange?: (open: boolean) => void
|
|
49
|
+
modKey?: string
|
|
50
|
+
pending?: string
|
|
51
|
+
notes?: string[]
|
|
52
|
+
empty?: string
|
|
53
|
+
}) {
|
|
54
|
+
const [query, setQuery] = useState(initialQuery)
|
|
55
|
+
return (
|
|
56
|
+
<CommandPalette open onOpenChange={onOpenChange} label="Palette" where="In Item one" modKey={modKey}>
|
|
57
|
+
<CommandPaletteSearch query={query} onQueryChange={setQuery} placeholder="Search" groups={groups} chip={chip} pending={pending} notes={notes} empty={empty} />
|
|
58
|
+
</CommandPalette>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const field = () => screen.getByRole('combobox', { name: 'Search' }) as HTMLInputElement
|
|
63
|
+
const lit = () => document.querySelector('[role="option"][data-highlighted] .truncate')?.textContent ?? null
|
|
64
|
+
const footer = () => document.querySelector('.border-t.h-9')?.textContent ?? ''
|
|
65
|
+
|
|
66
|
+
/** The field, once the dialog has put focus in it. */
|
|
67
|
+
async function focusedField() {
|
|
68
|
+
await waitFor(() => expect(document.activeElement).toBe(field()))
|
|
69
|
+
return field()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe('CommandPalette — the window', () => {
|
|
73
|
+
it('is a dialog named by its label, and starts in the field', async () => {
|
|
74
|
+
render(<Search groups={[{ label: 'Group', rows: [row('One')] }]} />)
|
|
75
|
+
expect(screen.getByRole('dialog', { name: 'Palette' })).toBeTruthy()
|
|
76
|
+
await focusedField()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('closes on Escape, with text in the field too', async () => {
|
|
80
|
+
const onOpenChange = vi.fn()
|
|
81
|
+
const user = userEvent.setup()
|
|
82
|
+
render(<Search groups={[{ label: 'Group', rows: [row('One')] }]} onOpenChange={onOpenChange} />)
|
|
83
|
+
await focusedField()
|
|
84
|
+
await user.keyboard('abc{Escape}')
|
|
85
|
+
await waitFor(() => expect(onOpenChange).toHaveBeenCalledWith(false))
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('CommandPaletteSearch — rows', () => {
|
|
90
|
+
it('draws a heading per group, and no group that has no rows', () => {
|
|
91
|
+
render(<Search groups={[{ label: 'First', rows: [row('One')] }, { label: 'Empty', rows: [] }, { label: 'Second', rows: [row('Two')] }]} />)
|
|
92
|
+
const groups = screen.getAllByRole('group')
|
|
93
|
+
expect(groups.map((g) => g.firstElementChild?.textContent)).toEqual(['First', 'Second'])
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('lights the first row, and Enter does what it does', async () => {
|
|
97
|
+
const onSelect = vi.fn()
|
|
98
|
+
const user = userEvent.setup()
|
|
99
|
+
render(<Search groups={[{ label: 'Group', rows: [row('One', { onSelect }), row('Two')] }]} />)
|
|
100
|
+
await focusedField()
|
|
101
|
+
await waitFor(() => expect(lit()).toContain('One'))
|
|
102
|
+
await user.keyboard('{Enter}')
|
|
103
|
+
expect(onSelect).toHaveBeenCalledTimes(1)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('stops the arrows at both ends', async () => {
|
|
107
|
+
const user = userEvent.setup()
|
|
108
|
+
render(<Search groups={[{ label: 'Group', rows: [row('One'), row('Two')] }]} />)
|
|
109
|
+
await focusedField()
|
|
110
|
+
await user.keyboard('{ArrowUp}')
|
|
111
|
+
expect(lit()).toContain('One')
|
|
112
|
+
await user.keyboard('{ArrowDown}{ArrowDown}{ArrowDown}')
|
|
113
|
+
expect(lit()).toContain('Two')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('goes in with Tab on a row that leads somewhere, and does nothing with Tab anywhere else', async () => {
|
|
117
|
+
const onGoIn = vi.fn()
|
|
118
|
+
const user = userEvent.setup()
|
|
119
|
+
render(<Search groups={[{ label: 'Group', rows: [row('One'), row('Place', { onGoIn })] }]} />)
|
|
120
|
+
const input = await focusedField()
|
|
121
|
+
await user.keyboard('{Tab}')
|
|
122
|
+
expect(onGoIn).not.toHaveBeenCalled()
|
|
123
|
+
expect(document.activeElement).toBe(input)
|
|
124
|
+
await user.keyboard('{ArrowDown}{Tab}')
|
|
125
|
+
expect(onGoIn).toHaveBeenCalledTimes(1)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('goes in with → only at the end of the text', async () => {
|
|
129
|
+
const onGoIn = vi.fn()
|
|
130
|
+
const user = userEvent.setup()
|
|
131
|
+
render(<Search groups={[{ label: 'Group', rows: [row('Place', { onGoIn })] }]} initialQuery="pl" />)
|
|
132
|
+
const input = await focusedField()
|
|
133
|
+
input.setSelectionRange(1, 1)
|
|
134
|
+
await user.keyboard('{ArrowRight}')
|
|
135
|
+
expect(onGoIn).not.toHaveBeenCalled()
|
|
136
|
+
input.setSelectionRange(2, 2)
|
|
137
|
+
await user.keyboard('{ArrowRight}')
|
|
138
|
+
expect(onGoIn).toHaveBeenCalledTimes(1)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('forgets a row that can be forgotten with Ctrl+Backspace, and only that', async () => {
|
|
142
|
+
const onForget = vi.fn()
|
|
143
|
+
const user = userEvent.setup()
|
|
144
|
+
render(<Search groups={[{ label: 'Group', rows: [row('Plain'), row('Recent', { onForget })] }]} />)
|
|
145
|
+
await focusedField()
|
|
146
|
+
await user.keyboard('{Control>}{Backspace}{/Control}')
|
|
147
|
+
expect(onForget).not.toHaveBeenCalled()
|
|
148
|
+
await user.keyboard('{ArrowDown}{Control>}{Backspace}{/Control}')
|
|
149
|
+
expect(onForget).toHaveBeenCalledTimes(1)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('goes back with Backspace at the start of the field, inside a level only', async () => {
|
|
153
|
+
const onBack = vi.fn()
|
|
154
|
+
const user = userEvent.setup()
|
|
155
|
+
const { unmount } = render(<Search groups={[{ label: 'Group', rows: [row('One')] }]} />)
|
|
156
|
+
await focusedField()
|
|
157
|
+
await user.keyboard('{Backspace}')
|
|
158
|
+
unmount()
|
|
159
|
+
|
|
160
|
+
render(<Search groups={[{ label: 'Group', rows: [row('One')] }]} chip={{ label: 'Place', onBack }} initialQuery="ab" />)
|
|
161
|
+
const input = await focusedField()
|
|
162
|
+
input.setSelectionRange(2, 2)
|
|
163
|
+
await user.keyboard('{Backspace}')
|
|
164
|
+
expect(onBack).not.toHaveBeenCalled()
|
|
165
|
+
expect(input.value).toBe('a')
|
|
166
|
+
input.setSelectionRange(0, 0)
|
|
167
|
+
await user.keyboard('{Backspace}')
|
|
168
|
+
expect(onBack).toHaveBeenCalledTimes(1)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it("goes back from the chip's ✕, and leaves focus in the field", async () => {
|
|
172
|
+
const onBack = vi.fn()
|
|
173
|
+
const user = userEvent.setup()
|
|
174
|
+
render(<Search groups={[{ label: 'Group', rows: [row('One')] }]} chip={{ label: 'Place', onBack }} />)
|
|
175
|
+
const input = await focusedField()
|
|
176
|
+
await user.click(screen.getByRole('button', { name: 'Leave Place' }))
|
|
177
|
+
expect(onBack).toHaveBeenCalledTimes(1)
|
|
178
|
+
await waitFor(() => expect(document.activeElement).toBe(input))
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('keeps the lit row where it is on Home and End', async () => {
|
|
182
|
+
const user = userEvent.setup()
|
|
183
|
+
render(<Search groups={[{ label: 'Group', rows: [row('One'), row('Two'), row('Three')] }]} initialQuery="text" />)
|
|
184
|
+
await focusedField()
|
|
185
|
+
await user.keyboard('{ArrowDown}')
|
|
186
|
+
expect(lit()).toContain('Two')
|
|
187
|
+
await user.keyboard('{Home}')
|
|
188
|
+
expect(lit()).toContain('Two')
|
|
189
|
+
await user.keyboard('{End}')
|
|
190
|
+
expect(lit()).toContain('Two')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('keeps the lit row lit when rows arrive above it (F7)', async () => {
|
|
194
|
+
const user = userEvent.setup()
|
|
195
|
+
const early: CommandPaletteGroup = { label: 'Early', rows: [row('One'), row('Two')] }
|
|
196
|
+
const late: CommandPaletteGroup = { label: 'Late', rows: [row('Late one'), row('Late two')] }
|
|
197
|
+
const { rerender } = render(<Search groups={[early]} />)
|
|
198
|
+
await focusedField()
|
|
199
|
+
await user.keyboard('{ArrowDown}')
|
|
200
|
+
expect(lit()).toContain('Two')
|
|
201
|
+
rerender(<Search groups={[late, early]} />)
|
|
202
|
+
await waitFor(() => expect(screen.getAllByRole('option')).toHaveLength(4))
|
|
203
|
+
await waitFor(() => expect(lit()).toBe('Two'))
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('lets rows arriving above take the first place while the highlight has not been moved', async () => {
|
|
207
|
+
const early: CommandPaletteGroup = { label: 'Early', rows: [row('One'), row('Two')] }
|
|
208
|
+
const late: CommandPaletteGroup = { label: 'Late', rows: [row('Late one')] }
|
|
209
|
+
const { rerender } = render(<Search groups={[early]} />)
|
|
210
|
+
await focusedField()
|
|
211
|
+
await waitFor(() => expect(lit()).toBe('One'))
|
|
212
|
+
rerender(<Search groups={[late, early]} />)
|
|
213
|
+
await waitFor(() => expect(screen.getAllByRole('option')).toHaveLength(3))
|
|
214
|
+
await waitFor(() => expect(lit()).toBe('Late one'))
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('names in the footer only the keys that work on the lit row', async () => {
|
|
218
|
+
const user = userEvent.setup()
|
|
219
|
+
render(
|
|
220
|
+
<Search
|
|
221
|
+
modKey="Cmd"
|
|
222
|
+
chip={{ label: 'Place', onBack: () => {} }}
|
|
223
|
+
groups={[{ label: 'Group', rows: [row('Plain'), row('Place', { onGoIn: () => {} }), row('Recent', { onForget: () => {} })] }]}
|
|
224
|
+
/>,
|
|
225
|
+
)
|
|
226
|
+
await focusedField()
|
|
227
|
+
expect(footer()).toContain('Enter')
|
|
228
|
+
expect(footer()).not.toContain('forget')
|
|
229
|
+
expect(footer()).toContain('Backspace') // back, while the field is empty
|
|
230
|
+
await user.keyboard('{ArrowDown}')
|
|
231
|
+
expect(footer()).toContain('Tab')
|
|
232
|
+
await user.keyboard('{ArrowDown}')
|
|
233
|
+
expect(footer()).toContain('Cmd+Backspace')
|
|
234
|
+
await user.keyboard('x')
|
|
235
|
+
expect(footer()).not.toContain('back')
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('names no arrows with one row, and only Esc with none', async () => {
|
|
239
|
+
const { unmount } = render(<Search groups={[{ label: 'Group', rows: [row('One')] }]} />)
|
|
240
|
+
expect(footer()).not.toContain('move')
|
|
241
|
+
unmount()
|
|
242
|
+
render(<Search groups={[]} empty="Nothing here yet." />)
|
|
243
|
+
expect(footer()).toContain('Esc')
|
|
244
|
+
expect(footer()).not.toContain('open')
|
|
245
|
+
expect(await screen.findByText('Nothing here yet.')).toBeTruthy()
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('draws notes as lines under the rows, which the arrows do not stop on', async () => {
|
|
249
|
+
const user = userEvent.setup()
|
|
250
|
+
render(<Search groups={[{ label: 'Group', rows: [row('One'), row('Two')] }]} notes={['2 more you cannot open.']} />)
|
|
251
|
+
await focusedField()
|
|
252
|
+
expect(await screen.findByText('2 more you cannot open.')).toBeTruthy()
|
|
253
|
+
expect(screen.getAllByRole('option')).toHaveLength(2)
|
|
254
|
+
await user.keyboard('{ArrowDown}{ArrowDown}{ArrowDown}')
|
|
255
|
+
expect(lit()).toBe('Two')
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('says rows are on their way', async () => {
|
|
259
|
+
render(<Search groups={[{ label: 'Group', rows: [row('One')] }]} pending="Searching…" />)
|
|
260
|
+
expect(await screen.findByText('Searching…')).toBeTruthy()
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
function Form({
|
|
265
|
+
onSubmit = () => {},
|
|
266
|
+
onBack = () => {},
|
|
267
|
+
submitWaits,
|
|
268
|
+
working,
|
|
269
|
+
error,
|
|
270
|
+
oneList = false,
|
|
271
|
+
}: {
|
|
272
|
+
onSubmit?: () => void
|
|
273
|
+
onBack?: () => void
|
|
274
|
+
submitWaits?: string
|
|
275
|
+
working?: { button: string; line: string }
|
|
276
|
+
error?: string
|
|
277
|
+
oneList?: boolean
|
|
278
|
+
}) {
|
|
279
|
+
const [title, setTitle] = useState('')
|
|
280
|
+
const [kind, setKind] = useState('one')
|
|
281
|
+
const [missing, setMissing] = useState<string | undefined>()
|
|
282
|
+
return (
|
|
283
|
+
<CommandPalette open onOpenChange={() => {}} label="Palette">
|
|
284
|
+
<CommandPaletteForm
|
|
285
|
+
chip={{ label: 'Action one', onBack }}
|
|
286
|
+
submitLabel="Create item"
|
|
287
|
+
onSubmit={() => {
|
|
288
|
+
if (!oneList && !title) setMissing('Give it a title.')
|
|
289
|
+
else onSubmit()
|
|
290
|
+
}}
|
|
291
|
+
submitWaits={submitWaits}
|
|
292
|
+
working={working}
|
|
293
|
+
error={error}
|
|
294
|
+
>
|
|
295
|
+
{!oneList && (
|
|
296
|
+
<Field label="Title" required error={missing}>
|
|
297
|
+
<TextInput value={title} onChange={(e) => setTitle(e.target.value)} />
|
|
298
|
+
</Field>
|
|
299
|
+
)}
|
|
300
|
+
<Field label="Kind">
|
|
301
|
+
<Select value={kind} onChange={setKind} options={[{ value: 'one', label: 'Kind one' }, { value: 'two', label: 'Kind two' }]} />
|
|
302
|
+
</Field>
|
|
303
|
+
</CommandPaletteForm>
|
|
304
|
+
</CommandPalette>
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
describe('CommandPaletteForm', () => {
|
|
309
|
+
it('starts on the first field and submits with Ctrl+Enter', async () => {
|
|
310
|
+
const onSubmit = vi.fn()
|
|
311
|
+
const user = userEvent.setup()
|
|
312
|
+
render(<Form onSubmit={onSubmit} />)
|
|
313
|
+
const title = screen.getByRole('textbox', { name: 'Title' })
|
|
314
|
+
await waitFor(() => expect(document.activeElement).toBe(title))
|
|
315
|
+
await user.keyboard('A{Control>}{Enter}{/Control}')
|
|
316
|
+
expect(onSubmit).toHaveBeenCalledTimes(1)
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('puts focus on the first field that needs something', async () => {
|
|
320
|
+
const user = userEvent.setup()
|
|
321
|
+
render(<Form />)
|
|
322
|
+
const title = screen.getByRole('textbox', { name: 'Title' })
|
|
323
|
+
await waitFor(() => expect(document.activeElement).toBe(title))
|
|
324
|
+
await user.tab()
|
|
325
|
+
await user.keyboard('{Control>}{Enter}{/Control}')
|
|
326
|
+
await waitFor(() => expect(document.activeElement).toBe(title))
|
|
327
|
+
expect(screen.getByText('Give it a title.')).toBeTruthy()
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('waits, and says why, while there is nothing to submit', async () => {
|
|
331
|
+
const onSubmit = vi.fn()
|
|
332
|
+
const user = userEvent.setup()
|
|
333
|
+
render(<Form onSubmit={onSubmit} oneList submitWaits="Choose a different kind first" />)
|
|
334
|
+
await waitFor(() => expect(document.activeElement?.getAttribute('role')).toBe('combobox'))
|
|
335
|
+
await user.keyboard('{Control>}{Enter}{/Control}')
|
|
336
|
+
expect(onSubmit).not.toHaveBeenCalled()
|
|
337
|
+
expect(footer()).not.toContain('Enter')
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('locks while working, keeps focus inside, ignores Ctrl+Enter, and gives focus back after', async () => {
|
|
341
|
+
const onSubmit = vi.fn()
|
|
342
|
+
const user = userEvent.setup()
|
|
343
|
+
const { rerender } = render(<Form onSubmit={onSubmit} />)
|
|
344
|
+
const title = screen.getByRole('textbox', { name: 'Title' }) as HTMLInputElement
|
|
345
|
+
await waitFor(() => expect(document.activeElement).toBe(title))
|
|
346
|
+
await user.keyboard('A{Control>}{Enter}{/Control}')
|
|
347
|
+
expect(onSubmit).toHaveBeenCalledTimes(1)
|
|
348
|
+
|
|
349
|
+
rerender(<Form onSubmit={onSubmit} working={{ button: 'Creating…', line: 'Creating the item…' }} />)
|
|
350
|
+
expect(screen.getByRole('button', { name: /Creating…/ })).toBeTruthy()
|
|
351
|
+
expect(screen.getByText('Creating the item…')).toBeTruthy()
|
|
352
|
+
expect(title.closest('fieldset')?.disabled).toBe(true)
|
|
353
|
+
const dialog = screen.getByRole('dialog')
|
|
354
|
+
expect(dialog.contains(document.activeElement)).toBe(true)
|
|
355
|
+
// Chrome drops focus to the page from a field that becomes disabled
|
|
356
|
+
// (measured in the prototype); jsdom leaves it there. So what is pinned
|
|
357
|
+
// is that focus does not sit on a locked field.
|
|
358
|
+
expect(document.activeElement?.matches(':disabled')).toBe(false)
|
|
359
|
+
await user.keyboard('{Control>}{Enter}{/Control}')
|
|
360
|
+
expect(onSubmit).toHaveBeenCalledTimes(1)
|
|
361
|
+
|
|
362
|
+
rerender(<Form onSubmit={onSubmit} error="Refused." />)
|
|
363
|
+
expect(within(dialog).getByText('Refused.')).toBeTruthy()
|
|
364
|
+
expect(document.activeElement).toBe(title)
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
it('goes back with Backspace from an empty text field only, keeping a list where it is', async () => {
|
|
368
|
+
const onBack = vi.fn()
|
|
369
|
+
const user = userEvent.setup()
|
|
370
|
+
render(<Form onBack={onBack} />)
|
|
371
|
+
const title = screen.getByRole('textbox', { name: 'Title' })
|
|
372
|
+
await waitFor(() => expect(document.activeElement).toBe(title))
|
|
373
|
+
await user.keyboard('A{Backspace}')
|
|
374
|
+
expect(onBack).not.toHaveBeenCalled()
|
|
375
|
+
await user.tab()
|
|
376
|
+
expect(document.activeElement?.getAttribute('role')).toBe('combobox')
|
|
377
|
+
await user.keyboard('{Backspace}')
|
|
378
|
+
expect(onBack).not.toHaveBeenCalled()
|
|
379
|
+
await user.tab({ shift: true })
|
|
380
|
+
await user.keyboard('{Backspace}')
|
|
381
|
+
expect(onBack).toHaveBeenCalledTimes(1)
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('goes back with Backspace from anywhere in a form that is one list', async () => {
|
|
385
|
+
const onBack = vi.fn()
|
|
386
|
+
const user = userEvent.setup()
|
|
387
|
+
render(<Form onBack={onBack} oneList />)
|
|
388
|
+
await waitFor(() => expect(document.activeElement?.getAttribute('role')).toBe('combobox'))
|
|
389
|
+
await user.keyboard('{Backspace}')
|
|
390
|
+
expect(onBack).toHaveBeenCalledTimes(1)
|
|
391
|
+
})
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
describe('what sits above the rows', () => {
|
|
395
|
+
it('draws an answer as paragraphs, with its marks on the word before them', () => {
|
|
396
|
+
render(<CommandPaletteAnswer note="Press Enter to ask again.">{'One moved [1], two waits [2].\n\nNothing else.'}</CommandPaletteAnswer>)
|
|
397
|
+
const paragraphs = document.querySelectorAll('p.text-body-2')
|
|
398
|
+
expect(paragraphs).toHaveLength(2)
|
|
399
|
+
expect(paragraphs[0].textContent).toBe('One moved1, two waits2.')
|
|
400
|
+
expect([...paragraphs[0].querySelectorAll('sup')].map((s) => s.textContent)).toEqual(['1', '2'])
|
|
401
|
+
expect(screen.getByRole('status').textContent).toBe('Press Enter to ask again.')
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it('keeps a quote’s line breaks', () => {
|
|
405
|
+
render(<CommandPaletteQuote>{'Line one\nLine two'}</CommandPaletteQuote>)
|
|
406
|
+
expect(screen.getByText(/Line one/).textContent).toBe('Line one\nLine two')
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('announces what is being worked on, with its grey bars', () => {
|
|
410
|
+
const { container } = render(<CommandPaletteWorking bars={3}>Reading…</CommandPaletteWorking>)
|
|
411
|
+
expect(screen.getByRole('status').textContent).toContain('Reading…')
|
|
412
|
+
expect(container.querySelectorAll('.animate-pulse')).toHaveLength(3)
|
|
413
|
+
})
|
|
414
|
+
})
|
|
415
|
+
|