@estiva-app/ui 0.16.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/CommandPalette.d.ts +8 -1
- package/dist/CommandPalette.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +2 -2
- package/package.json +1 -1
- package/src/CommandPalette.mdx +11 -5
- package/src/CommandPalette.stories.tsx +3 -1
- package/src/CommandPalette.test.tsx +24 -1
- package/src/CommandPalette.tsx +23 -2
package/package.json
CHANGED
package/src/CommandPalette.mdx
CHANGED
|
@@ -76,10 +76,16 @@ import { CommandPalette, CommandPaletteSearch } from '@estiva-app/ui'
|
|
|
76
76
|
- **`pending` while rows are still on their way; `empty` only once none are.**
|
|
77
77
|
Pass `empty` while a search is running and the page says "nothing" before it
|
|
78
78
|
knows.
|
|
79
|
-
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
- **`notes` are what a person should know about the rows and cannot act on** —
|
|
80
|
+
"2 more where you cannot open them", "the search was refused, so this list
|
|
81
|
+
is incomplete". Quiet lines under the rows, never rows: the arrows do not stop
|
|
82
|
+
on them.
|
|
83
|
+
- **Rows that arrive late do not take the highlight you moved to.** Base UI keeps
|
|
84
|
+
the highlight's position rather than its row, so a group arriving above the
|
|
85
|
+
lit row would hand the highlight — and Enter — to something else. Once you have
|
|
86
|
+
moved the highlight on a level, the palette walks it back to the row you had.
|
|
87
|
+
Until you have, the first row is lit, whichever row that becomes. Typing starts
|
|
88
|
+
again from the first row.
|
|
83
89
|
- **Home and End move the text cursor, and nothing else.** Base UI would also
|
|
84
90
|
send the highlight to the first or last row; the palette stops that.
|
|
85
91
|
- **The field is named by its placeholder**, so say what it asks for.
|
|
@@ -117,7 +123,7 @@ import { CommandPalette, CommandPaletteSearch } from '@estiva-app/ui'
|
|
|
117
123
|
|
|
118
124
|
**`CommandPaletteSearch`** — `query` and `onQueryChange` (the field, controlled),
|
|
119
125
|
`placeholder` (also the field's name), `groups` (`{ label, rows }`, a group
|
|
120
|
-
with no rows is not drawn), `chip?` (`{ label, leading?, onBack }`), `pending?`,
|
|
126
|
+
with no rows is not drawn), `chip?` (`{ label, leading?, onBack }`), `pending?`, `notes?`,
|
|
121
127
|
`empty?`, and `children` for what sits above the rows.
|
|
122
128
|
|
|
123
129
|
**A row** — `id`, `label`, `description?`, `icon?` or `leading?`, `onSelect`,
|
|
@@ -365,6 +365,8 @@ function Demo({ start, where, modKey, label }: { start: Start; where?: string; m
|
|
|
365
365
|
: undefined
|
|
366
366
|
}
|
|
367
367
|
pending={level.kind === 'first' && searching ? 'Searching…' : undefined}
|
|
368
|
+
// What the later group left out: a line to read, not a row to pick.
|
|
369
|
+
notes={level.kind === 'first' && q && !searching && LATER.some((l) => l.toLowerCase().includes(q)) ? ['1 more in a place you cannot open.'] : undefined}
|
|
368
370
|
empty={
|
|
369
371
|
level.kind === 'first' && q && !searching
|
|
370
372
|
? `Nothing is called “${q}”.`
|
|
@@ -388,7 +390,7 @@ const story = (start: Start): Story => ({
|
|
|
388
390
|
/** The first level: rows in groups, the first one lit. ↑ ↓ stop at the ends, Tab goes into a place, Ctrl+Backspace forgets a recent row. */
|
|
389
391
|
export const FirstLevel: Story = story('first')
|
|
390
392
|
|
|
391
|
-
/** Typing: the rows that match now, a line while a later group is on its way,
|
|
393
|
+
/** Typing: the rows that match now, a line while a later group is on its way, that group arriving without moving the lit row, and a note about what it left out. */
|
|
392
394
|
export const WhileRowsArrive: Story = story('arriving')
|
|
393
395
|
|
|
394
396
|
/** Nothing matches: the empty line, where the first row would be. */
|
|
@@ -39,6 +39,7 @@ function Search({
|
|
|
39
39
|
onOpenChange = () => {},
|
|
40
40
|
modKey,
|
|
41
41
|
pending,
|
|
42
|
+
notes,
|
|
42
43
|
empty,
|
|
43
44
|
}: {
|
|
44
45
|
groups: CommandPaletteGroup[]
|
|
@@ -47,12 +48,13 @@ function Search({
|
|
|
47
48
|
onOpenChange?: (open: boolean) => void
|
|
48
49
|
modKey?: string
|
|
49
50
|
pending?: string
|
|
51
|
+
notes?: string[]
|
|
50
52
|
empty?: string
|
|
51
53
|
}) {
|
|
52
54
|
const [query, setQuery] = useState(initialQuery)
|
|
53
55
|
return (
|
|
54
56
|
<CommandPalette open onOpenChange={onOpenChange} label="Palette" where="In Item one" modKey={modKey}>
|
|
55
|
-
<CommandPaletteSearch query={query} onQueryChange={setQuery} placeholder="Search" groups={groups} chip={chip} pending={pending} empty={empty} />
|
|
57
|
+
<CommandPaletteSearch query={query} onQueryChange={setQuery} placeholder="Search" groups={groups} chip={chip} pending={pending} notes={notes} empty={empty} />
|
|
56
58
|
</CommandPalette>
|
|
57
59
|
)
|
|
58
60
|
}
|
|
@@ -201,6 +203,17 @@ describe('CommandPaletteSearch — rows', () => {
|
|
|
201
203
|
await waitFor(() => expect(lit()).toBe('Two'))
|
|
202
204
|
})
|
|
203
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
|
+
|
|
204
217
|
it('names in the footer only the keys that work on the lit row', async () => {
|
|
205
218
|
const user = userEvent.setup()
|
|
206
219
|
render(
|
|
@@ -232,6 +245,16 @@ describe('CommandPaletteSearch — rows', () => {
|
|
|
232
245
|
expect(await screen.findByText('Nothing here yet.')).toBeTruthy()
|
|
233
246
|
})
|
|
234
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
|
+
|
|
235
258
|
it('says rows are on their way', async () => {
|
|
236
259
|
render(<Search groups={[{ label: 'Group', rows: [row('One')] }]} pending="Searching…" />)
|
|
237
260
|
expect(await screen.findByText('Searching…')).toBeTruthy()
|
package/src/CommandPalette.tsx
CHANGED
|
@@ -221,6 +221,13 @@ export interface CommandPaletteSearchProps {
|
|
|
221
221
|
chip?: CommandPaletteChip
|
|
222
222
|
/** A line under the rows while more are on their way — "Searching…". */
|
|
223
223
|
pending?: string
|
|
224
|
+
/**
|
|
225
|
+
* Quiet lines under the rows, for what a person should know about them and
|
|
226
|
+
* cannot act on — "2 more where you cannot open them", "the search was
|
|
227
|
+
* refused, so this list is incomplete". Plain lines, never rows: the arrows
|
|
228
|
+
* do not stop on them.
|
|
229
|
+
*/
|
|
230
|
+
notes?: string[]
|
|
224
231
|
/** The line when there are no rows. Leave it out while rows are still on their way. */
|
|
225
232
|
empty?: string
|
|
226
233
|
/** Above the rows: a `CommandPaletteWorking`, `CommandPaletteAnswer` or `CommandPaletteQuote`. */
|
|
@@ -229,7 +236,7 @@ export interface CommandPaletteSearchProps {
|
|
|
229
236
|
|
|
230
237
|
type ListGroup = { value: string; items: CommandPaletteRow[] }
|
|
231
238
|
|
|
232
|
-
export function CommandPaletteSearch({ query, onQueryChange, placeholder, groups, chip, pending, empty, children }: CommandPaletteSearchProps) {
|
|
239
|
+
export function CommandPaletteSearch({ query, onQueryChange, placeholder, groups, chip, pending, notes = [], empty, children }: CommandPaletteSearchProps) {
|
|
233
240
|
const { modKey, popupRef } = usePalette('CommandPaletteSearch')
|
|
234
241
|
const back = useBack(chip, popupRef)
|
|
235
242
|
const items = useMemo<ListGroup[]>(() => groups.filter((g) => g.rows.length > 0).map((g) => ({ value: g.label, items: g.rows })), [groups])
|
|
@@ -254,14 +261,22 @@ export function CommandPaletteSearch({ query, onQueryChange, placeholder, groups
|
|
|
254
261
|
arrow keys, exactly as a person would. It does so only when Base UI moved
|
|
255
262
|
the highlight by itself ("none") — never after an arrow, the pointer, or
|
|
256
263
|
typing, which starts again from the first row.
|
|
264
|
+
|
|
265
|
+
And only once the person has moved the highlight on this level. Until then
|
|
266
|
+
the lit row is simply the first one, and a row that arrives above it — a
|
|
267
|
+
thread's own rows, a moment after opening — becomes the first one.
|
|
257
268
|
*/
|
|
258
269
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
259
270
|
const reported = useRef<{ id?: string; reason?: string }>({})
|
|
260
271
|
const settled = useRef<{ id?: string; query: string; level?: string } | null>(null)
|
|
272
|
+
/** The person has moved the highlight since the text or the level last changed. */
|
|
273
|
+
const moved = useRef(false)
|
|
261
274
|
useLayoutEffect(() => {
|
|
262
275
|
const before = settled.current
|
|
263
276
|
const now = reported.current
|
|
264
|
-
|
|
277
|
+
const sameLevel = before?.query === query && before?.level === chip?.label
|
|
278
|
+
if (!sameLevel) moved.current = false
|
|
279
|
+
if (moved.current && before?.id && now.reason === 'none' && now.id !== before.id && sameLevel) {
|
|
265
280
|
const ids = rows.map((r) => r.id)
|
|
266
281
|
const want = ids.indexOf(before.id)
|
|
267
282
|
const at = now.id === undefined ? -1 : ids.indexOf(now.id)
|
|
@@ -347,6 +362,7 @@ export function CommandPaletteSearch({ query, onQueryChange, placeholder, groups
|
|
|
347
362
|
onItemHighlighted={(row, details) => {
|
|
348
363
|
const id = (row as CommandPaletteRow | undefined)?.id
|
|
349
364
|
reported.current = { id, reason: details.reason }
|
|
365
|
+
if (details.reason !== 'none') moved.current = true
|
|
350
366
|
setLitId(id)
|
|
351
367
|
}}
|
|
352
368
|
>
|
|
@@ -410,6 +426,11 @@ export function CommandPaletteSearch({ query, onQueryChange, placeholder, groups
|
|
|
410
426
|
</>
|
|
411
427
|
)}
|
|
412
428
|
</Autocomplete.Status>
|
|
429
|
+
{notes.map((note) => (
|
|
430
|
+
<div key={note} className="flex min-h-8 items-center px-3">
|
|
431
|
+
<FieldLine>{note}</FieldLine>
|
|
432
|
+
</div>
|
|
433
|
+
))}
|
|
413
434
|
<Autocomplete.Empty className="flex items-center px-3 [&:not(:empty)]:min-h-10">
|
|
414
435
|
{empty && <EmptyState scope="section" message={empty} />}
|
|
415
436
|
</Autocomplete.Empty>
|