mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-10 03:31:40 -10:00
fix(topology): TerminalPreviewPopover renderTrigger=false prop kills duplicate tab stop
Forward-fix #1 on 5f92ecc. Guard a11y finding: each TerminalView card
exposed TWO focusable controls — the full-card absolute-inset trigger
button AND the popover's internal trigger button (rendered with
buttonClassName="sr-only" so visually hidden but still keyboard-
reachable). Sighted keyboard users would hit two tab stops for the
same action; sensible-tab-order check failed.
Resolution per guard recommendation (a): teach TerminalPreviewPopover
an external-trigger mode.
- New optional prop `renderTrigger?: boolean` on
TerminalPreviewPopover (default true preserves graph + table view
callers). When false, the popover skips its internal button
entirely; the wrapper div + event-listening logic + portal popover
still mount so the card's own trigger can dispatch
TERMINAL_PREVIEW_EVENT and have this instance render the positioned
popover.
- SeatTerminalCard updated from buttonClassName="sr-only" →
renderTrigger={false}. Same anchor positioning via wrapperClassName;
zero focusable button claim.
Regression test added in p5-topology-terminal.test.tsx:
'each terminal card exposes exactly one tab-reachable trigger button
(no duplicate sr-only popover button)' — renders 2 seats, asserts:
- 2 card-level triggers (button[data-testid^='terminal-card-trigger-'])
- 0 popover-trigger buttons (button[data-testid$='-terminal-open'])
Discrimination verified: temporarily reverted ONLY
TopologyTerminalView.tsx back to buttonClassName="sr-only" (kept
TerminalPreviewPopover + new test) → the assertion fails (2 popover
trigger buttons render). Restored renderTrigger={false} → 11/11
PASS.
Tests:
- p5-topology-terminal.test.tsx: 11/11 PASS (1 new).
- Full daemon suite: 3379 passed / 1 skipped / 0 failed.
- git diff --check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
5f92eccc6e
commit
31050c7ae2
3 files changed
+56
-11
No files matched your search
@@ -41,6 +41,13 @@ interface TerminalPreviewPopoverProps {
|
||||
buttonClassName?: string;
|
||||
popoverClassName?: string;
|
||||
testIdPrefix: string;
|
||||
/** V0.3.1 slice 14 forward-fix #1 (a11y): when false, the popover
|
||||
* does NOT render its own trigger button. Used by surfaces that
|
||||
* own the trigger externally (e.g., TerminalView cards where the
|
||||
* whole card acts as the trigger) so the popover doesn't add a
|
||||
* duplicate keyboard tab stop. Default true preserves the
|
||||
* existing graph-view + table-view button rendering. */
|
||||
renderTrigger?: boolean;
|
||||
}
|
||||
|
||||
function rectFromElement(el: HTMLElement | null): AnchorRect {
|
||||
@@ -90,6 +97,7 @@ export function TerminalPreviewPopover({
|
||||
buttonClassName,
|
||||
popoverClassName,
|
||||
testIdPrefix,
|
||||
renderTrigger = true,
|
||||
}: TerminalPreviewPopoverProps) {
|
||||
const key = `${rigId ?? "unknown"}:${logicalId}`;
|
||||
const rootRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -188,16 +196,18 @@ export function TerminalPreviewPopover({
|
||||
|
||||
return (
|
||||
<div ref={rootRef} className={cn("relative inline-flex", wrapperClassName)}>
|
||||
<button
|
||||
type="button"
|
||||
data-testid={`${testIdPrefix}-terminal-open`}
|
||||
aria-label={`View ${logicalId} terminal`}
|
||||
title="View terminal"
|
||||
onClick={openPreview}
|
||||
className={buttonClassName}
|
||||
>
|
||||
<ToolMark tool="terminal" size="sm" />
|
||||
</button>
|
||||
{renderTrigger ? (
|
||||
<button
|
||||
type="button"
|
||||
data-testid={`${testIdPrefix}-terminal-open`}
|
||||
aria-label={`View ${logicalId} terminal`}
|
||||
title="View terminal"
|
||||
onClick={openPreview}
|
||||
className={buttonClassName}
|
||||
>
|
||||
<ToolMark tool="terminal" size="sm" />
|
||||
</button>
|
||||
) : null}
|
||||
{popover}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -134,7 +134,7 @@ function SeatTerminalCard({ seat }: { seat: NodeInventoryEntry }) {
|
||||
reducedMotion={false}
|
||||
testIdPrefix={`terminal-grid-${seat.rigId}-${seat.logicalId}`}
|
||||
wrapperClassName="absolute right-1 top-1 z-20"
|
||||
buttonClassName="sr-only"
|
||||
renderTrigger={false}
|
||||
/>
|
||||
) : null}
|
||||
<header className="relative z-0 flex items-center justify-between gap-2 pointer-events-none">
|
||||
|
||||
@@ -223,6 +223,41 @@ describe("TopologyTerminalView P5-7 grid", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// V0.3.1 slice 14 walk-item 17 forward-fix #1 (a11y): each terminal
|
||||
// card exposes EXACTLY ONE tabbable trigger control. The TerminalPreview-
|
||||
// Popover instance is mounted with `renderTrigger={false}` so the card's
|
||||
// full-card overlay button is the sole interaction surface; a duplicate
|
||||
// invisible-but-reachable sr-only popover button would fail
|
||||
// sensible-tab-order a11y review.
|
||||
describe("TerminalView card a11y (slice 14 FF#1)", () => {
|
||||
it("each terminal card exposes exactly one tab-reachable trigger button (no duplicate sr-only popover button)", async () => {
|
||||
const seats = [
|
||||
makeSeat({ logicalId: "alpha" }),
|
||||
makeSeat({ logicalId: "beta" }),
|
||||
];
|
||||
setupFetch({ rigs: [{ id: "rig-1", name: "test-rig" }], seatsByRig: { "rig-1": seats } });
|
||||
const { findByTestId, container } = withQueryClient(
|
||||
<TopologyTerminalView scope="rig" rigId="rig-1" />,
|
||||
);
|
||||
await findByTestId("topology-terminal-grid");
|
||||
|
||||
// Exactly one card-level trigger button per seat.
|
||||
const cardTriggers = container.querySelectorAll(
|
||||
"button[data-testid^='terminal-card-trigger-']",
|
||||
);
|
||||
expect(cardTriggers).toHaveLength(2);
|
||||
|
||||
// ZERO duplicate sr-only popover buttons — the FF removes them.
|
||||
// Pre-fix: each card mounted a `terminal-grid-<rig>-<seat>-terminal-open`
|
||||
// button via the popover's default trigger render. With
|
||||
// renderTrigger={false} the popover skips its button entirely.
|
||||
const popoverTriggerButtons = container.querySelectorAll(
|
||||
"button[data-testid$='-terminal-open']",
|
||||
);
|
||||
expect(popoverTriggerButtons).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("globals.css pulsing-ring CSS contract (ritual #7 pseudo-element-paint)", () => {
|
||||
const cssPath = path.resolve(__dirname, "../src/globals.css");
|
||||
it("globals.css declares @keyframes terminal-card-active-frames", () => {
|
||||
|
||||
Reference in new issue
Block a user