fix: address console review findings
This commit is contained in:
@@ -12,6 +12,7 @@ export const BeatRail = ({ activeBeat, jump }: BeatRailProps) => (
|
||||
key={beat.id}
|
||||
type="button"
|
||||
data-active={beat.id === activeBeat}
|
||||
aria-current={beat.id === activeBeat ? "step" : undefined}
|
||||
onClick={() => jump(beat.id)}
|
||||
>
|
||||
<span>{beat.lifecycleStep}</span>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useEffect, useRef, type KeyboardEvent } from "react";
|
||||
import { presentationNodes } from "./WorkflowGraphStage.js";
|
||||
|
||||
type NodeSpotlightProps = {
|
||||
@@ -7,22 +8,57 @@ type NodeSpotlightProps = {
|
||||
|
||||
const nodeDescription = (nodeId: string): string => {
|
||||
if (nodeId === "review_issues") {
|
||||
return "NodeUse of a typed interrupt boundary. It exposes request and resume schemas and waits for a submitted or cancelled outcome.";
|
||||
return "Typed interrupt boundary. It exposes request and resume schemas, then waits for a submitted or cancelled outcome.";
|
||||
}
|
||||
if (nodeId === "create_issues") {
|
||||
return "NodeUse that writes selected review items into the local issue-board source.";
|
||||
return "Workflow step that writes selected review items into the local issue-board source.";
|
||||
}
|
||||
return "NodeUse in the prepared report workflow. The presentation graph is curated, but every node maps back to real workflow/run evidence.";
|
||||
return "Prepared report workflow step. The presentation graph is curated, but each node maps back to workflow or run evidence.";
|
||||
};
|
||||
|
||||
export const NodeSpotlight = ({ nodeId, close }: NodeSpotlightProps) => {
|
||||
const dialogRef = useRef<HTMLElement>(null);
|
||||
const closeButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const node = presentationNodes.find((candidate) => candidate.id === nodeId);
|
||||
|
||||
useEffect(() => {
|
||||
const previouslyFocused = document.activeElement instanceof HTMLElement
|
||||
? document.activeElement
|
||||
: null;
|
||||
closeButtonRef.current?.focus();
|
||||
return () => previouslyFocused?.focus();
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key !== "Tab") return;
|
||||
const focusable = [...(dialogRef.current?.querySelectorAll<HTMLElement>(
|
||||
"button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])",
|
||||
) ?? [])].filter((element) => !element.hasAttribute("disabled"));
|
||||
if (focusable.length === 0) return;
|
||||
const first = focusable[0]!;
|
||||
const last = focusable[focusable.length - 1]!;
|
||||
if (event.shiftKey && document.activeElement === first) {
|
||||
event.preventDefault();
|
||||
last.focus();
|
||||
} else if (!event.shiftKey && document.activeElement === last) {
|
||||
event.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
};
|
||||
|
||||
if (!node) return null;
|
||||
|
||||
return (
|
||||
<aside className="node-spotlight" role="dialog" aria-modal="true" aria-label={node.label}>
|
||||
<button type="button" onClick={close}>Close</button>
|
||||
<p>NodeUse</p>
|
||||
<aside
|
||||
ref={dialogRef}
|
||||
className="node-spotlight"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={node.label}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<button type="button" ref={closeButtonRef} onClick={close}>Close</button>
|
||||
<p>Workflow node</p>
|
||||
<h2>{node.label}</h2>
|
||||
<p>{nodeDescription(node.id)}</p>
|
||||
</aside>
|
||||
|
||||
@@ -25,8 +25,8 @@ export const OperationBlock = ({ event }: OperationBlockProps) => (
|
||||
</section>
|
||||
</div>
|
||||
<footer>
|
||||
<span>{event.resultingIds.deploymentId}</span>
|
||||
{event.resultingIds.runId && <span>{event.resultingIds.runId}</span>}
|
||||
<span>Deployment: {event.resultingIds.deploymentId}</span>
|
||||
{event.resultingIds.runId && <span>Run: {event.resultingIds.runId}</span>}
|
||||
</footer>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -16,7 +16,11 @@ export const OperatorChat = ({ state }: OperatorChatProps) => (
|
||||
</div>
|
||||
<div className="chat-message chat-message--system">
|
||||
<strong>lda.chat</strong>
|
||||
<p>Replay mode is active. Live execution is available when connected.</p>
|
||||
<p>
|
||||
{state.playbackMode === "replay"
|
||||
? "Replay mode is active. Live execution is available when connected."
|
||||
: "Live execution is active. Operations are being sent to the connected workflow server."}
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ describe("PresentationRoute", () => {
|
||||
|
||||
expect(screen.getByText(/Human approval is a typed workflow boundary/i)).toBeInTheDocument();
|
||||
|
||||
window.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight" }));
|
||||
await userEvent.keyboard("{ArrowRight}");
|
||||
expect(await screen.findByText(/Resuming commits the approved branch/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -36,7 +36,7 @@ describe("PresentationRoute", () => {
|
||||
await userEvent.click(screen.getByRole("button", { name: /issue review/i }));
|
||||
|
||||
expect(screen.getByRole("dialog", { name: /issue review/i })).toBeInTheDocument();
|
||||
expect(screen.getByText("NodeUse")).toBeInTheDocument();
|
||||
expect(screen.getByText("Workflow node")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("can advance replay far enough to show a product operation block", async () => {
|
||||
|
||||
@@ -29,12 +29,24 @@ export const PresentationRoute = () => {
|
||||
}
|
||||
}, [state.beat]);
|
||||
|
||||
useEffect(() => {
|
||||
const onHashChange = () => {
|
||||
dispatch({ type: "jump_hash", hash: window.location.hash });
|
||||
};
|
||||
window.addEventListener("hashchange", onHashChange);
|
||||
return () => window.removeEventListener("hashchange", onHashChange);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
const target = event.target;
|
||||
const isBodyEvent = target == null || target === window || target === document.body || target === document.documentElement;
|
||||
if (event.key === " " || event.key === "ArrowRight") {
|
||||
if (!isBodyEvent) return;
|
||||
event.preventDefault();
|
||||
dispatch({ type: "next" });
|
||||
} else if (event.key === "ArrowLeft") {
|
||||
if (!isBodyEvent) return;
|
||||
event.preventDefault();
|
||||
dispatch({ type: "previous" });
|
||||
} else if (event.key === "Escape") {
|
||||
@@ -57,6 +69,10 @@ export const PresentationRoute = () => {
|
||||
}
|
||||
}, [demo.state.phase, demo.state.mode, demo.start]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ type: "set_playback_mode", mode: demo.state.mode });
|
||||
}, [demo.state.mode]);
|
||||
|
||||
return (
|
||||
<main className="presentation-route" aria-label="lda.chat presentation">
|
||||
<PresentationStage
|
||||
|
||||
@@ -28,6 +28,7 @@ export const WorkflowGraphStage = ({ selectedNodeId, selectNode }: WorkflowGraph
|
||||
className="workflow-graph-stage__node"
|
||||
data-kind={node.kind}
|
||||
data-selected={selectedNodeId === node.id}
|
||||
aria-pressed={selectedNodeId === node.id}
|
||||
style={{ left: `${node.x}%`, top: `${node.y}%` }}
|
||||
onClick={() => selectNode(node.id)}
|
||||
>
|
||||
|
||||
@@ -34,7 +34,7 @@ const withDerivedModes = (state: PresentationState, beat: BeatId): PresentationS
|
||||
...state,
|
||||
beat,
|
||||
chatMode: beat === "intro" || beat === "chat-request" ? "full" : "rail",
|
||||
evidenceMode: beat === "trace-evidence" ? "peek" : state.evidenceMode,
|
||||
evidenceMode: beat === "trace-evidence" ? "peek" : "hidden",
|
||||
});
|
||||
|
||||
export const presentationReducer = (
|
||||
|
||||
Reference in New Issue
Block a user