a11y: keyboard-operable editor chrome + topbar aria-labels

Clickable <div> rows/tiles (page list, layer tree, template cards, asset
picker grid) now expose role="button", a tab stop, and Enter/Space
activation via a shared clickableProps() helper, matching their existing
onClick behavior. TopBar icon-only controls (device switcher, undo/redo,
save, publish, templates, code, preview, back) gain aria-label alongside
their existing title tooltips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 14:50:50 -07:00
parent a036843728
commit 46ebd253f3
7 changed files with 100 additions and 12 deletions
+51
View File
@@ -0,0 +1,51 @@
import { describe, test, expect, vi } from 'vitest';
import { clickableProps } from './a11y';
function makeKeyEvent(key: string) {
return {
key,
preventDefault: vi.fn(),
} as unknown as Parameters<ReturnType<typeof clickableProps>['onKeyDown']>[0];
}
describe('clickableProps', () => {
test('sets role=button and tabIndex=0', () => {
const props = clickableProps(() => {});
expect(props.role).toBe('button');
expect(props.tabIndex).toBe(0);
});
test('onClick invokes the callback', () => {
const onActivate = vi.fn();
const props = clickableProps(onActivate);
props.onClick();
expect(onActivate).toHaveBeenCalledTimes(1);
});
test('onKeyDown invokes the callback and preventDefaults on Enter', () => {
const onActivate = vi.fn();
const props = clickableProps(onActivate);
const e = makeKeyEvent('Enter');
props.onKeyDown(e);
expect(onActivate).toHaveBeenCalledTimes(1);
expect(e.preventDefault).toHaveBeenCalledTimes(1);
});
test('onKeyDown invokes the callback and preventDefaults on Space', () => {
const onActivate = vi.fn();
const props = clickableProps(onActivate);
const e = makeKeyEvent(' ');
props.onKeyDown(e);
expect(onActivate).toHaveBeenCalledTimes(1);
expect(e.preventDefault).toHaveBeenCalledTimes(1);
});
test('onKeyDown ignores other keys', () => {
const onActivate = vi.fn();
const props = clickableProps(onActivate);
const e = makeKeyEvent('Tab');
props.onKeyDown(e);
expect(onActivate).not.toHaveBeenCalled();
expect(e.preventDefault).not.toHaveBeenCalled();
});
});
+30
View File
@@ -0,0 +1,30 @@
import type { KeyboardEvent } from 'react';
export interface ClickableProps {
role: 'button';
tabIndex: number;
onClick: () => void;
onKeyDown: (e: KeyboardEvent) => void;
}
/**
* Props to spread onto a non-interactive element (typically a `<div>`) that
* acts like a button (e.g. a clickable list row or grid tile), so it
* becomes keyboard-operable: `role="button"`, a tab stop, and Enter/Space
* activation in addition to the existing click behavior.
*
* Usage: `<div {...clickableProps(() => doThing())}>...</div>`
*/
export function clickableProps(onActivate: () => void): ClickableProps {
return {
role: 'button',
tabIndex: 0,
onClick: onActivate,
onKeyDown: (e: KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onActivate();
}
},
};
}