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:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user