fix(picker): honest combobox — keyboard navigation, listbox roles, no-results state
The input declared role=combobox + aria-autocomplete=list while arrow keys did nothing (WCAG 4.1.2). Wired the useTypeahead activeIndex the same way PersonTypeahead does: ArrowUp/Down cycle, Enter/Space select, Escape closes, aria-activedescendant tracks the active option; the list is a real listbox with option roles again (the interim role downgrade is reverted). Zero hits now render a 'Keine Treffer' row instead of silently vanishing, aria-expanded matches the visible state, and the hook sets loading at setQuery so the debounce window can't read as 'no results'. DocumentMultiSelect renders the shared error state too. _uid counter replaced with $props.id(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -106,6 +106,17 @@ describe('createTypeahead', () => {
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('sets loading immediately on setQuery so empty results read as pending, not "no results"', async () => {
|
||||
const fetchUrl = vi.fn().mockResolvedValue([]);
|
||||
const ta = createTypeahead({ fetchUrl, debounceMs: 300 });
|
||||
ta.setQuery('foo');
|
||||
// During the debounce window no fetch has run yet — callers must be able to
|
||||
// distinguish "still searching" from "searched, zero hits".
|
||||
expect(ta.loading).toBe(true);
|
||||
await vi.advanceTimersByTimeAsync(300);
|
||||
expect(ta.loading).toBe(false);
|
||||
});
|
||||
|
||||
it('setActiveIndex updates activeIndex', () => {
|
||||
const ta = createTypeahead({ fetchUrl: vi.fn().mockResolvedValue([]) });
|
||||
expect(ta.activeIndex).toBe(-1);
|
||||
|
||||
@@ -19,9 +19,11 @@ export function createTypeahead<T>(options: Options<T>) {
|
||||
function setQuery(q: string) {
|
||||
query = q;
|
||||
isOpen = true;
|
||||
// Set loading before the debounce fires so callers can distinguish
|
||||
// "still searching" from "searched, zero hits" during the debounce window.
|
||||
loading = true;
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(async () => {
|
||||
loading = true;
|
||||
error = false;
|
||||
try {
|
||||
results = await fetchUrl(q);
|
||||
|
||||
Reference in New Issue
Block a user