Skip to content

Commit 08383b3

Browse files
cevhericlaude
andcommitted
a11y: single live region for toasts, gate RUN shimmer on reduced-motion, complete palette combobox pattern
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7f9f94d commit 08383b3

2 files changed

Lines changed: 20 additions & 3 deletions

File tree

src/components/studio/CommandPalette.astro

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
placeholder="Type a command or search sections…"
1616
class="w-full border-b border-edge bg-canvas px-4 py-3 text-[14px] text-fg placeholder:text-faint focus:outline-none"
1717
aria-label="Command palette input"
18+
role="combobox"
19+
aria-expanded="false"
20+
aria-controls="palette-listbox"
21+
aria-autocomplete="list"
1822
/>
19-
<ul data-palette-list role="listbox" aria-label="Command palette results" class="max-h-[50vh] overflow-y-auto py-1 text-[13px]"></ul>
23+
<ul id="palette-listbox" data-palette-list role="listbox" aria-label="Command palette results" class="max-h-[50vh] overflow-y-auto py-1 text-[13px]"></ul>
2024
</div>
2125
</div>

src/scripts/studio.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,6 @@ function renderToast(msg: ConsoleMessage): HTMLElement {
129129
const el = document.createElement('div');
130130
el.className =
131131
'pointer-events-auto flex w-[min(92vw,440px)] items-start gap-2 border border-edge bg-panel px-3 py-2 text-[12.5px] leading-relaxed shadow-lg';
132-
el.setAttribute('role', 'status');
133132

134133
const body = document.createElement('div');
135134
body.className = 'flex-1';
@@ -192,7 +191,8 @@ function runQuery() {
192191
const id = currentHash();
193192
const meta = sectionById[id] ?? sectionById['home'];
194193
const pane = document.querySelector<HTMLElement>(`[data-section="${id}"] .studio-results`);
195-
if (pane) {
194+
const allowMotion = !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
195+
if (pane && allowMotion) {
196196
pane.classList.remove('is-rerunning');
197197
void pane.offsetWidth; // restart animation
198198
pane.classList.add('is-rerunning');
@@ -291,12 +291,14 @@ let paletteLastFocus: HTMLElement | null = null;
291291

292292
function renderPalette(query: string) {
293293
const list = document.querySelector<HTMLElement>('[data-palette-list]');
294+
const input = document.querySelector<HTMLElement>('[data-palette-input]');
294295
if (!list) return;
295296
paletteFiltered = filterItems(query, paletteItems(), (it) => it.label + ' ' + it.hint);
296297
paletteHighlight = 0;
297298
list.innerHTML = '';
298299
paletteFiltered.forEach((it, i) => {
299300
const li = document.createElement('li');
301+
li.id = `palette-opt-${i}`;
300302
li.setAttribute('role', 'option');
301303
li.setAttribute('aria-selected', String(i === 0));
302304
li.className = `flex cursor-pointer items-center justify-between px-4 py-2 ${i === 0 ? 'bg-raised text-bright' : 'text-fg'}`;
@@ -311,6 +313,11 @@ function renderPalette(query: string) {
311313
li.addEventListener('click', () => { it.run(); closePalette(); });
312314
list.appendChild(li);
313315
});
316+
if (paletteFiltered.length > 0) {
317+
input?.setAttribute('aria-activedescendant', 'palette-opt-0');
318+
} else {
319+
input?.removeAttribute('aria-activedescendant');
320+
}
314321
}
315322

316323
function setHighlight(i: number) {
@@ -321,6 +328,8 @@ function setHighlight(i: number) {
321328
li.className = `flex cursor-pointer items-center justify-between px-4 py-2 ${idx === i ? 'bg-raised text-bright' : 'text-fg'}`;
322329
(li as HTMLElement).setAttribute('aria-selected', String(idx === i));
323330
});
331+
const input = document.querySelector<HTMLElement>('[data-palette-input]');
332+
input?.setAttribute('aria-activedescendant', `palette-opt-${i}`);
324333
}
325334

326335
function openPalette() {
@@ -330,13 +339,17 @@ function openPalette() {
330339
paletteLastFocus = document.activeElement as HTMLElement;
331340
root.removeAttribute('hidden');
332341
input.value = '';
342+
input.setAttribute('aria-expanded', 'true');
333343
renderPalette('');
334344
input.focus();
335345
}
336346

337347
function closePalette() {
338348
const root = document.querySelector<HTMLElement>('[data-palette-root]');
349+
const input = document.querySelector<HTMLElement>('[data-palette-input]');
339350
root?.setAttribute('hidden', '');
351+
input?.setAttribute('aria-expanded', 'false');
352+
input?.removeAttribute('aria-activedescendant');
340353
paletteLastFocus?.focus();
341354
}
342355

0 commit comments

Comments
 (0)