fix(frontend): make Pi operator tabs keyboard accessible
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { CheckCircle2, CircleAlert, ClipboardCheck, FlaskConical, LoaderCircle, ScrollText, X } from "lucide-react";
|
||||
import {
|
||||
@@ -213,7 +213,30 @@ function PiInstructionSteps({ details }: { details: PiPlatformDetails }) {
|
||||
|
||||
function PiPlatformInstructions() {
|
||||
const [activePlatform, setActivePlatform] = useState<PiPlatform>();
|
||||
const activeDetails = piPlatforms.find((platform) => platform.id === activePlatform);
|
||||
const [focusedPlatform, setFocusedPlatform] = useState<PiPlatform>("linux");
|
||||
const tabRefs = useRef<Record<PiPlatform, HTMLButtonElement | null>>({ linux: null, macos: null, windows: null });
|
||||
|
||||
function activateAndFocus(platform: PiPlatform) {
|
||||
setFocusedPlatform(platform);
|
||||
setActivePlatform(platform);
|
||||
tabRefs.current[platform]?.focus();
|
||||
}
|
||||
|
||||
function handleTabKeyDown(event: React.KeyboardEvent<HTMLButtonElement>, platform: PiPlatform) {
|
||||
const index = piPlatforms.findIndex((item) => item.id === platform);
|
||||
const nextPlatform = event.key === "ArrowRight"
|
||||
? piPlatforms[(index + 1) % piPlatforms.length]
|
||||
: event.key === "ArrowLeft"
|
||||
? piPlatforms[(index - 1 + piPlatforms.length) % piPlatforms.length]
|
||||
: event.key === "Home"
|
||||
? piPlatforms[0]
|
||||
: event.key === "End"
|
||||
? piPlatforms[piPlatforms.length - 1]
|
||||
: undefined;
|
||||
if (!nextPlatform) return;
|
||||
event.preventDefault();
|
||||
activateAndFocus(nextPlatform.id);
|
||||
}
|
||||
|
||||
return (
|
||||
<section aria-label="Pi operator workflow" className="mt-2 grid gap-2 rounded-md border border-amber-500/30 bg-amber-500/5 p-3 text-xs">
|
||||
@@ -223,19 +246,39 @@ function PiPlatformInstructions() {
|
||||
<button
|
||||
key={platform.id}
|
||||
id={`pi-${platform.id}-tab`}
|
||||
ref={(element) => { tabRefs.current[platform.id] = element; }}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activePlatform === platform.id}
|
||||
aria-controls={`pi-${platform.id}-panel`}
|
||||
tabIndex={focusedPlatform === platform.id ? 0 : -1}
|
||||
className={activePlatform === platform.id ? "rounded bg-background px-2 py-1.5 font-semibold text-foreground shadow-sm" : "rounded px-2 py-1.5 font-semibold text-muted-foreground hover:bg-background/70 hover:text-foreground"}
|
||||
onClick={() => setActivePlatform(platform.id)}
|
||||
onFocus={() => setFocusedPlatform(platform.id)}
|
||||
onKeyDown={(event) => handleTabKeyDown(event, platform.id)}
|
||||
onClick={() => {
|
||||
setFocusedPlatform(platform.id);
|
||||
setActivePlatform((current) => current === platform.id ? undefined : platform.id);
|
||||
}}
|
||||
>
|
||||
{platform.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div data-testid="pi-platform-instructions-scroll" className="min-h-0 max-h-[min(34vh,22rem)] overflow-y-scroll overscroll-contain pr-2">
|
||||
{activeDetails && <div id={`pi-${activeDetails.id}-panel`} role="tabpanel" aria-label={activeDetails.label} aria-labelledby={`pi-${activeDetails.id}-tab`} tabIndex={0} className="outline-none"><PiInstructionSteps details={activeDetails.details} /></div>}
|
||||
{piPlatforms.map((platform) => (
|
||||
<div
|
||||
key={platform.id}
|
||||
id={`pi-${platform.id}-panel`}
|
||||
role="tabpanel"
|
||||
aria-label={platform.label}
|
||||
aria-labelledby={`pi-${platform.id}-tab`}
|
||||
tabIndex={activePlatform === platform.id ? 0 : -1}
|
||||
hidden={activePlatform !== platform.id}
|
||||
className="outline-none"
|
||||
>
|
||||
<PiInstructionSteps details={platform.details} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user