diff --git a/frontend/src/lib/document/DocumentPickerDropdown.svelte b/frontend/src/lib/document/DocumentPickerDropdown.svelte index c55f6cc0..0d59575d 100644 --- a/frontend/src/lib/document/DocumentPickerDropdown.svelte +++ b/frontend/src/lib/document/DocumentPickerDropdown.svelte @@ -75,13 +75,6 @@ function handleKeydown(e: KeyboardEvent) { picker.close(); } } - -function handleOptionKeydown(e: KeyboardEvent, doc: DocumentOption) { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - handleSelect(doc); - } -}
picker.close()} class="relative"> @@ -102,34 +95,45 @@ function handleOptionKeydown(e: KeyboardEvent, doc: DocumentOption) { /> {#if picker.isOpen} - + {/if} {/if}
diff --git a/frontend/src/lib/document/DocumentPickerDropdown.svelte.spec.ts b/frontend/src/lib/document/DocumentPickerDropdown.svelte.spec.ts index 046d5767..2c3d4987 100644 --- a/frontend/src/lib/document/DocumentPickerDropdown.svelte.spec.ts +++ b/frontend/src/lib/document/DocumentPickerDropdown.svelte.spec.ts @@ -198,3 +198,46 @@ describe('DocumentPickerDropdown — search failure', () => { await expect.element(page.getByText(m.comp_typeahead_error())).toBeInTheDocument(); }); }); + +describe('DocumentPickerDropdown — ARIA listbox integrity', () => { + it('does not render a listbox when results are empty (no aria-required-children violation)', async () => { + mockSearchResponse([]); + render(DocumentPickerDropdown, { onSelect: vi.fn() }); + + await userEvent.fill(page.getByRole('combobox'), 'xyz'); + await waitForDebounce(); + + // no-results message must be visible, but NOT inside a listbox + await expect.element(page.getByText(m.comp_typeahead_no_results())).toBeInTheDocument(); + expect(document.querySelector('[role="listbox"]')).toBeNull(); + }); + + it('does not render a listbox when loading (no aria-required-children violation)', async () => { + let resolveSearch!: (v: unknown) => void; + vi.stubGlobal( + 'fetch', + vi.fn().mockReturnValue(new Promise((resolve) => (resolveSearch = resolve))) + ); + render(DocumentPickerDropdown, { onSelect: vi.fn() }); + + await userEvent.fill(page.getByRole('combobox'), 'Brief'); + + // While in-flight, no listbox should exist + expect(document.querySelector('[role="listbox"]')).toBeNull(); + resolveSearch({ ok: true, json: () => Promise.resolve({ items: [] }) }); + }); + + it('option elements do not have tabindex (combobox pattern: focus stays on input)', async () => { + mockSearchResponse([docFactory('d1', 'Brief A'), docFactory('d2', 'Brief B')]); + render(DocumentPickerDropdown, { onSelect: vi.fn() }); + + await userEvent.fill(page.getByRole('combobox'), 'Brief'); + await waitForDebounce(); + + const options = document.querySelectorAll('[role="listbox"] [role="option"]'); + expect(options.length).toBeGreaterThan(0); + options.forEach((opt) => { + expect(opt).not.toHaveAttribute('tabindex'); + }); + }); +});