mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 10:12:34 +00:00
Report pane resize handle size to assistive tech (APG window splitter)
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
import { beforeEach, describe, expect, test } from 'vitest';
|
||||
import { clampSideWidth, HANDLES_TOTAL, PANE_DEFAULT, PANE_MIN, usePanesStore } from './PanesStore';
|
||||
import {
|
||||
clampSideWidth,
|
||||
HANDLES_TOTAL,
|
||||
maxSideWidth,
|
||||
PANE_DEFAULT,
|
||||
PANE_MIN,
|
||||
sideWidthValue,
|
||||
usePanesStore,
|
||||
} from './PanesStore';
|
||||
|
||||
const store = () => usePanesStore.getState();
|
||||
|
||||
@@ -32,6 +40,38 @@ describe('clampSideWidth', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('sideWidthValue (aria-valuenow, 0–100 per WAI-ARIA APG window splitter)', () => {
|
||||
const W = 1400;
|
||||
|
||||
test('reports 0 at the pane minimum and 100 at the pane maximum', () => {
|
||||
const other = PANE_DEFAULT.preview;
|
||||
const max = maxSideWidth('library', W, other);
|
||||
expect(sideWidthValue('library', PANE_MIN.library, W, other)).toBe(0);
|
||||
expect(sideWidthValue('library', max, W, other)).toBe(100);
|
||||
});
|
||||
|
||||
test('reports the midpoint as ~50', () => {
|
||||
const other = PANE_DEFAULT.preview;
|
||||
const mid = (PANE_MIN.library + maxSideWidth('library', W, other)) / 2;
|
||||
expect(sideWidthValue('library', mid, W, other)).toBe(50);
|
||||
});
|
||||
|
||||
test('clamps out-of-range widths into 0–100', () => {
|
||||
const other = PANE_DEFAULT.preview;
|
||||
expect(sideWidthValue('library', 0, W, other)).toBe(0);
|
||||
expect(sideWidthValue('library', 99999, W, other)).toBe(100);
|
||||
});
|
||||
|
||||
test('returns null before layout is known (container width 0)', () => {
|
||||
expect(sideWidthValue('library', 280, 0, PANE_DEFAULT.preview)).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null when the range has collapsed (container too narrow)', () => {
|
||||
// 500px can't fit all the minimums, so min == max and there is no range.
|
||||
expect(sideWidthValue('library', 200, 500, PANE_MIN.preview)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('usePanesStore', () => {
|
||||
beforeEach(() =>
|
||||
store().hydrate({ libraryWidth: PANE_DEFAULT.library, previewWidth: PANE_DEFAULT.preview }),
|
||||
|
||||
@@ -31,11 +31,19 @@ export const PANE_DEFAULT = { library: 280, preview: 360 } as const;
|
||||
/** Combined width (px) the resize handles occupy between the panes. */
|
||||
export const HANDLES_TOTAL = 12;
|
||||
|
||||
/**
|
||||
* The widest a side pane can be while the editor still meets its minimum.
|
||||
* `containerWidth` is the full panes-row width; `otherWidth` is the opposite
|
||||
* side pane's current width. Never reports below the pane's own minimum.
|
||||
*/
|
||||
export function maxSideWidth(side: PaneSide, containerWidth: number, otherWidth: number): number {
|
||||
return Math.max(PANE_MIN[side], containerWidth - otherWidth - HANDLES_TOTAL - PANE_MIN.editor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp a desired side-pane width so it stays at least its own minimum and
|
||||
* leaves the editor at least its minimum. Pure — the single place the resize
|
||||
* constraint lives. `containerWidth` is the full panes-row width; `otherWidth`
|
||||
* is the opposite side pane's current width.
|
||||
* constraint lives.
|
||||
*/
|
||||
export function clampSideWidth(
|
||||
side: PaneSide,
|
||||
@@ -43,11 +51,32 @@ export function clampSideWidth(
|
||||
containerWidth: number,
|
||||
otherWidth: number,
|
||||
): number {
|
||||
return Math.max(
|
||||
PANE_MIN[side],
|
||||
Math.min(desired, maxSideWidth(side, containerWidth, otherWidth)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a side pane's width to the 0–100 position a window splitter reports
|
||||
* via `aria-valuenow` (WAI-ARIA APG → Window Splitter): 0 = pane at its minimum
|
||||
* size, 100 = pane at its maximum. Returns null when the container width isn't
|
||||
* known yet (pre-layout) or the range has collapsed, so the caller omits the
|
||||
* attribute rather than emitting NaN. The 0–100 scale is APG's "typical" choice
|
||||
* and announces as a percentage — more meaningful than a moving pixel count.
|
||||
*/
|
||||
export function sideWidthValue(
|
||||
side: PaneSide,
|
||||
width: number,
|
||||
containerWidth: number,
|
||||
otherWidth: number,
|
||||
): number | null {
|
||||
if (containerWidth <= 0) return null;
|
||||
const min = PANE_MIN[side];
|
||||
// The widest this pane can be while the editor still meets its minimum.
|
||||
const max = containerWidth - otherWidth - HANDLES_TOTAL - PANE_MIN.editor;
|
||||
// If the container is too narrow for everyone, the min wins (never below it).
|
||||
return Math.max(min, Math.min(desired, Math.max(min, max)));
|
||||
const max = maxSideWidth(side, containerWidth, otherWidth);
|
||||
if (max <= min) return null;
|
||||
const pct = ((width - min) / (max - min)) * 100;
|
||||
return Math.round(Math.min(100, Math.max(0, pct)));
|
||||
}
|
||||
|
||||
export interface PanesState {
|
||||
|
||||
Reference in New Issue
Block a user