Skip to content

Commit 591107e

Browse files
adibhannaampagent
andcommitted
Fix(search): the first click on Create lands while a tag is still typed
In the New note form (note search, #826), typing a tag in the Tags field and reaching for Create with the mouse lost the click. Pressing the mouse on the button moved focus to it, the Tags field blurred and committed the typed word as a chip, the suggestion row under the fields unmounted, and the footer moved up before the mouse was released. No click fired. In the built app the button moved 44 px, the release landed on the backdrop, and the palette closed with the typed name and tag gone; with a taller list the release lands inside the card and a second click works, which is how it was noticed. The footer buttons and the "Open it" button no longer take focus on mousedown, the way the folder and tag rows under the fields already did not, so nothing moves until the click completes. `create()` was already counting text still typed in Tags, the way Enter does. Found during the phone pass on core 2.54.0; the phones get it with the shell releases that adopt this core. Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a0c45f-844c-77b8-846b-3e25b2447dab
1 parent 0ccfa0b commit 591107e

2 files changed

Lines changed: 68 additions & 1 deletion

File tree

‎packages/app-core/src/components/SearchCreateForm.tsx‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,9 @@ export function SearchCreateForm({
397397
variant="ghost"
398398
size="sm"
399399
className="text-xs"
400+
// Same reason as the footer: a blur here commits the typed tag
401+
// as a chip, and a chips row that wraps moves this button.
402+
onMouseDown={(e) => e.preventDefault()}
400403
onClick={() => onOpenExisting(collision.note)}
401404
>
402405
Open it <kbd className="rounded bg-paper-200 px-1 text-ink-500">Shift+↵</kbd>
@@ -484,7 +487,15 @@ export function SearchCreateForm({
484487
<kbd className="rounded bg-paper-200 px-1">esc</kbd> back
485488
</span>
486489
</div>
487-
<div className="flex items-center gap-2">
490+
<div
491+
className="flex items-center gap-2"
492+
// The buttons act on click and never need focus. Taking it on
493+
// mousedown would blur the field, unmount the folder or tag list
494+
// under the fields and move this footer before mouseup, so the
495+
// click never fired: the first Create with a tag still typed was
496+
// lost. `create()` counts that text, the way Enter does.
497+
onMouseDown={(e) => e.preventDefault()}
498+
>
488499
<Button variant="secondary" size="sm" onClick={onBack}>
489500
Back
490501
</Button>

‎packages/app-core/src/components/SearchPalette.test.ts‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,62 @@ describe('SearchPalette: the create row opens a New note form', () => {
456456
expect(createAndOpen).toHaveBeenCalledWith('inbox', '', { title: 'Runbook', tags: ['oncall'] })
457457
})
458458

459+
// A browser moves focus to a pressed button as mousedown's default action,
460+
// unless the event is cancelled; jsdom leaves that step to the test. The
461+
// blur used to unmount the list under the fields and move the footer
462+
// before mouseup, so the click never fired and the first Create was lost.
463+
const press = async (button: HTMLElement, focusedField: HTMLInputElement): Promise<void> => {
464+
await act(async () => {
465+
const uncancelled = button.dispatchEvent(
466+
new MouseEvent('mousedown', { bubbles: true, cancelable: true })
467+
)
468+
if (uncancelled) focusedField.blur()
469+
await Promise.resolve()
470+
})
471+
}
472+
473+
it('pressing the mouse on Create keeps the Tags field focused, so the click lands and the typed tag counts', async () => {
474+
type(search(), 'Runbook')
475+
await key(search(), 'Enter', { shiftKey: true })
476+
focus(field('tags'))
477+
type(field('tags'), 'oncall')
478+
expect(formRows('tag')).toEqual(['oncall'])
479+
480+
await press(createButton(), field('tags'))
481+
expect(document.activeElement).toBe(field('tags'))
482+
expect(formRows('tag')).toEqual(['oncall'])
483+
expect(chipTags()).toEqual([])
484+
485+
await act(async () => {
486+
createButton().click()
487+
await Promise.resolve()
488+
})
489+
expect(createAndOpen).toHaveBeenCalledWith('inbox', '', { title: 'Runbook', tags: ['oncall'] })
490+
})
491+
492+
it('pressing the mouse on Back keeps the folder list until the click', async () => {
493+
type(search(), 'Runbook')
494+
await key(search(), 'Enter', { shiftKey: true })
495+
focus(field('folder'))
496+
expect(formRows('folder')).toEqual(['', 'projects', 'projects/ideas', 'quick', 'archive', 'archive/old'])
497+
498+
const back = [...document.querySelectorAll<HTMLButtonElement>('button')].find(
499+
(b) => b.textContent === 'Back'
500+
)
501+
if (!back) throw new Error('Back button not rendered')
502+
await press(back, field('folder'))
503+
expect(document.activeElement).toBe(field('folder'))
504+
expect(formRows('folder')).toHaveLength(6)
505+
506+
await act(async () => {
507+
back.click()
508+
await Promise.resolve()
509+
})
510+
expect(form()).toBeNull()
511+
expect(search().value).toBe('Runbook')
512+
expect(createAndOpen).not.toHaveBeenCalled()
513+
})
514+
459515
it('Ctrl+Enter or Cmd+Enter creates from any field', async () => {
460516
type(search(), 'Runbook')
461517
await key(search(), 'Enter', { shiftKey: true })

0 commit comments

Comments
 (0)