Add pane show/hide toggle strip with persisted visibility (M6, §01A)

A persistent left-rail toolbar (APG toolbar + roving tabindex) toggles the
library / editor / preview panes; a hidden pane frees its space and the rest
redistribute proportionally to their remembered widths. Visibility persists to
astrolabe:ux-prefs (separate key from widths, so a hidden pane keeps its width).
Adds the positional pane-* glyph sub-family to Icon and a Datasets shortcut
button in the strip.
This commit is contained in:
2026-06-07 17:15:58 +03:00
parent f92118712c
commit 8c0a6b9239
11 changed files with 601 additions and 45 deletions
@@ -0,0 +1,99 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { usePanesStore } from '../stores/PanesStore';
import { PaneToggleStrip } from './PaneToggleStrip';
vi.mock('../modals/ModalCoordinator', () => ({ openModal: vi.fn() }));
import { openModal } from '../modals/ModalCoordinator';
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
let container: HTMLDivElement;
let root: Root;
const toggleButtons = () =>
Array.from(container.querySelectorAll<HTMLButtonElement>('button[aria-pressed]'));
const datasetsButton = () =>
container.querySelector<HTMLButtonElement>('button[aria-label="Datasets"]')!;
beforeEach(() => {
usePanesStore.getState().hydrate({}, {});
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
act(() => root.render(<PaneToggleStrip />));
});
afterEach(() => {
act(() => root.unmount());
container.remove();
vi.clearAllMocks();
});
describe('PaneToggleStrip', () => {
test('renders an APG toolbar: three pane toggles + a Datasets command button', () => {
expect(container.querySelector('[role="toolbar"][aria-orientation="vertical"]')).not.toBeNull();
expect(toggleButtons()).toHaveLength(3);
// The Datasets control is a command button, not a toggle (no aria-pressed).
expect(datasetsButton().hasAttribute('aria-pressed')).toBe(false);
});
test('aria-pressed mirrors pane visibility and flips on click', () => {
const [library] = toggleButtons();
expect(library.getAttribute('aria-pressed')).toBe('true');
act(() => library.click());
expect(usePanesStore.getState().libraryVisible).toBe(false);
expect(library.getAttribute('aria-pressed')).toBe('false');
});
test('the accessible name is stable across the pressed/not-pressed flip (APG)', () => {
const [library] = toggleButtons();
expect(library.getAttribute('aria-label')).toBe('Library pane');
act(() => library.click());
expect(library.getAttribute('aria-label')).toBe('Library pane');
});
test('roving tabindex: exactly one control is in the tab order at a time', () => {
const all = [...toggleButtons(), datasetsButton()];
expect(all.filter((b) => b.tabIndex === 0)).toHaveLength(1);
expect(all[0].tabIndex).toBe(0); // first control by default
});
test('ArrowDown moves the roving tab stop to the next control', () => {
const all = [...toggleButtons(), datasetsButton()];
act(() => {
all[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
});
expect(all[0].tabIndex).toBe(-1);
expect(all[1].tabIndex).toBe(0);
});
test('End jumps to the last control (Datasets); Home returns to the first', () => {
const all = [...toggleButtons(), datasetsButton()];
act(() => {
all[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'End', bubbles: true }));
});
expect(datasetsButton().tabIndex).toBe(0);
act(() => {
datasetsButton().dispatchEvent(new KeyboardEvent('keydown', { key: 'Home', bubbles: true }));
});
expect(all[0].tabIndex).toBe(0);
});
test('ArrowUp from the first control wraps to the last', () => {
const all = [...toggleButtons(), datasetsButton()];
act(() => {
all[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }));
});
expect(datasetsButton().tabIndex).toBe(0);
});
test('the Datasets button opens the Datasets manager', () => {
act(() => datasetsButton().click());
expect(openModal).toHaveBeenCalledWith('datasets');
});
});