fix(picker): ARIA combobox — listbox only when results; no tabindex on options

Status/error/empty messages rendered outside <ul role="listbox"> so the
element only contains role="option" children (fixes aria-required-children
axe violation). Options no longer carry tabindex or onkeydown — keyboard
navigation stays on the input per ARIA combobox pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-10 19:30:18 +02:00
parent a65a55448e
commit 585244d65b
2 changed files with 71 additions and 24 deletions

View File

@@ -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');
});
});
});