feat(accessibility): harden chat and keyboard interactions
Stream capability-authorized chat reads with cancellation, virtualize viewer rendering, centralize modal focus management, and add keyboard-accessible queue and VOD menus. Expose command palette combobox state and keep the document language synchronized with the selected locale.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { runInNewContext } from 'node:vm';
|
||||
import { transpileModule, ModuleKind, ScriptTarget } from 'typescript';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
interface RendererAccessibilityApi {
|
||||
RenderGeneration: new () => { next(): number; isCurrent(generation: number): boolean; cancel(): void };
|
||||
getVirtualRange(scrollTop: number, viewportHeight: number, itemCount: number, rowHeight: number, overscan: number): { start: number; end: number };
|
||||
getNextFocusIndex(activeIndex: number, count: number, shiftKey: boolean): number;
|
||||
getNextMenuIndex(activeIndex: number, count: number, key: string): number | null;
|
||||
setDocumentLanguage(language: string): string;
|
||||
}
|
||||
|
||||
function loadAccessibility(documentElement: { lang: string } = { lang: '' }): RendererAccessibilityApi {
|
||||
const source = readFileSync(join(__dirname, 'renderer-accessibility.ts'), 'utf8');
|
||||
const context = {
|
||||
document: { documentElement, addEventListener: () => undefined },
|
||||
requestAnimationFrame: (callback: () => void) => callback(),
|
||||
HTMLElement: class {},
|
||||
Element: class {},
|
||||
Node: class {},
|
||||
};
|
||||
const output = transpileModule(source, { compilerOptions: { module: ModuleKind.None, target: ScriptTarget.ES2022 } }).outputText;
|
||||
runInNewContext(output, context);
|
||||
return (context as unknown as { RendererAccessibility: RendererAccessibilityApi }).RendererAccessibility;
|
||||
}
|
||||
|
||||
describe('renderer accessibility helpers', () => {
|
||||
it('replaces stale chat work after rapid filters and close cancellation', () => {
|
||||
const { RenderGeneration } = loadAccessibility();
|
||||
const generations = new RenderGeneration();
|
||||
const initial = generations.next();
|
||||
const rapidFilter = generations.next();
|
||||
generations.cancel();
|
||||
|
||||
expect(generations.isCurrent(initial)).toBe(false);
|
||||
expect(generations.isCurrent(rapidFilter)).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps a large chat render window bounded to the visible rows', () => {
|
||||
const { getVirtualRange } = loadAccessibility();
|
||||
expect(getVirtualRange(29_000, 580, 50_000, 29, 12)).toEqual({ start: 988, end: 1_032 });
|
||||
});
|
||||
|
||||
it('wraps dialog tab order at the first and last focusable controls', () => {
|
||||
const { getNextFocusIndex } = loadAccessibility();
|
||||
expect(getNextFocusIndex(2, 3, false)).toBe(0);
|
||||
expect(getNextFocusIndex(0, 3, true)).toBe(2);
|
||||
});
|
||||
|
||||
it('moves keyboard menus with arrows, Home, End and Escape semantics', () => {
|
||||
const { getNextMenuIndex } = loadAccessibility();
|
||||
expect(getNextMenuIndex(1, 4, 'ArrowDown')).toBe(2);
|
||||
expect(getNextMenuIndex(0, 4, 'ArrowUp')).toBe(3);
|
||||
expect(getNextMenuIndex(2, 4, 'Home')).toBe(0);
|
||||
expect(getNextMenuIndex(1, 4, 'End')).toBe(3);
|
||||
expect(getNextMenuIndex(1, 4, 'Escape')).toBeNull();
|
||||
});
|
||||
|
||||
it('updates the document language when the application language changes', () => {
|
||||
const documentElement = { lang: 'en' };
|
||||
const { setDocumentLanguage } = loadAccessibility(documentElement);
|
||||
expect(setDocumentLanguage('de')).toBe('de');
|
||||
expect(documentElement.lang).toBe('de');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user