Emit pane aria-controls only while the pane is mounted

This commit is contained in:
2026-06-07 23:11:32 +03:00
parent 1d7798abc5
commit cdd5cf149f
2 changed files with 19 additions and 5 deletions
@@ -56,6 +56,18 @@ describe('PaneToggleStrip', () => {
expect(library.getAttribute('aria-label')).toBe('Library pane'); expect(library.getAttribute('aria-label')).toBe('Library pane');
}); });
test('aria-controls points at the pane only while it is shown (no dangling IDREF)', () => {
// App unmounts a hidden pane's <section>, so emitting aria-controls then would
// leave the IDREF pointing at nothing. It must appear only when pressed/shown.
const [library] = toggleButtons();
expect(library.hasAttribute('aria-controls')).toBe(true);
act(() => library.click()); // hide the pane
expect(library.getAttribute('aria-pressed')).toBe('false');
expect(library.hasAttribute('aria-controls')).toBe(false);
});
test('roving tabindex: exactly one control is in the tab order at a time', () => { test('roving tabindex: exactly one control is in the tab order at a time', () => {
const all = [...toggleButtons(), datasetsButton()]; const all = [...toggleButtons(), datasetsButton()];
expect(all.filter((b) => b.tabIndex === 0)).toHaveLength(1); expect(all.filter((b) => b.tabIndex === 0)).toHaveLength(1);
+7 -5
View File
@@ -102,11 +102,13 @@ export function PaneToggleStrip() {
type="button" type="button"
aria-pressed={visible[item.pane]} aria-pressed={visible[item.pane]}
aria-label={item.label} aria-label={item.label}
// TODO: when a pane is hidden, App unmounts its <section>, so this // Only point at the pane while it's actually mounted: App unmounts a
// aria-controls IDREF dangles until the pane is shown again. Harmless // hidden pane's <section>, so emitting aria-controls then would leave a
// (AT ignores unresolved IDREFs) but technically invalid — consider // dangling IDREF (invalid, even if AT ignores it). The association is
// keeping the section mounted-but-hidden, or dropping aria-controls. // meaningful only when the target exists, and the aria-pressed state
aria-controls={item.controls} // already conveys hidden/shown either way (contract: arch/10 → toggle
// strip — the bullet lists aria-pressed + a stable name, not controls).
aria-controls={visible[item.pane] ? item.controls : undefined}
// Description (not the name): hints what activating does. The name stays // Description (not the name): hints what activating does. The name stays
// stable (aria-label) per APG's toggle-button rule. // stable (aria-label) per APG's toggle-button rule.
title={`${visible[item.pane] ? 'Hide' : 'Show'} ${item.label.toLowerCase()}`} title={`${visible[item.pane] ? 'Hide' : 'Show'} ${item.label.toLowerCase()}`}