fix(accessibility): stabilize virtual viewer interactions

Use cancellable, generation-aware reads and removable virtual scrolling for chat and event viewers. Constrain virtual rows to fixed single-line heights, clean busy state, stop Escape propagation, and cover dialog, menu, palette, and virtual list interactions.
This commit is contained in:
Sucukdeluxe
2026-08-12 01:55:50 +02:00
parent 9c17269a54
commit 348c37cbdb
4 changed files with 581 additions and 72 deletions
+42
View File
@@ -8,6 +8,13 @@ interface RendererOpenDialogState {
onEscape?: () => void;
}
interface RendererFixedHeightVirtualListOptions {
itemCount: () => number;
rowHeight: number;
overscan: number;
render: (range: { start: number; end: number }) => void;
}
const RendererAccessibility = (() => {
const dialogStack: string[] = [];
const dialogStates = new Map<string, RendererOpenDialogState>();
@@ -61,6 +68,38 @@ const RendererAccessibility = (() => {
return normalized;
}
function setBusy(element: HTMLElement, busy: boolean): void {
if (busy) element.setAttribute('aria-busy', 'true');
else element.removeAttribute('aria-busy');
}
function createFixedHeightVirtualList(list: HTMLElement, options: RendererFixedHeightVirtualListOptions): { dispose(): void; render(): void } {
let disposed = false;
let scheduled = false;
const render = (): void => {
if (disposed) return;
options.render(getVirtualRange(list.scrollTop, list.clientHeight, options.itemCount(), options.rowHeight, options.overscan));
};
const onScroll = (): void => {
if (scheduled || disposed) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
render();
});
};
list.addEventListener('scroll', onScroll, { passive: true });
render();
return {
dispose: () => {
if (disposed) return;
disposed = true;
list.removeEventListener('scroll', onScroll);
},
render,
};
}
function getFocusable(dialog: HTMLElement): HTMLElement[] {
return Array.from(dialog.querySelectorAll<HTMLElement>(focusSelector)).filter((element) => element.getAttribute('aria-hidden') !== 'true');
}
@@ -153,6 +192,7 @@ const RendererAccessibility = (() => {
if (!(dialog instanceof HTMLElement)) return;
if (event.key === 'Escape') {
event.preventDefault();
event.stopImmediatePropagation();
closeTopmostDialog();
return;
}
@@ -177,6 +217,8 @@ const RendererAccessibility = (() => {
getNextFocusIndex,
getNextMenuIndex,
setDocumentLanguage,
setBusy,
createFixedHeightVirtualList,
isDialogOpen,
openDialog,
closeDialog,