FN-6118: make workflow lane headers tappable
Replace the empty lane toggle button with an accessible tappable header for workflow rows on mobile. - make the full workflow lane header act as the collapse toggle - add keyboard support plus hover and focus-visible states for the header control - update Lane tests to cover header click, keyboard toggles, and removal of the empty icon button Files changed: packages/dashboard/app/components/Lane.css | 11 +++++++ packages/dashboard/app/components/Lane.tsx | 30 +++++++++++-------- packages/dashboard/app/components/__tests__/Lane.test.tsx | 35 +++++++++++++++++++--- 3 files changed, 60 insertions(+), 16 deletions(-) Fusion-Task-Id: FN-6118 Fusion-Task-Lineage: 0cc1b632-1e59-4d1d-b274-f1da53a503f4
This commit is contained in:
@@ -95,6 +95,17 @@
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.lane-header:hover {
|
||||
background: var(--surface-hover, var(--surface));
|
||||
}
|
||||
|
||||
.lane-header:focus-visible {
|
||||
outline: 2px solid var(--focus-ring, var(--color-primary));
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.lane-name {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "./Lane.css";
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, type KeyboardEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { Task, TaskDetail, Column as ColumnType, TaskCreateInput, GithubIssueAction } from "@fusion/core";
|
||||
import { Column } from "./Column";
|
||||
@@ -134,6 +134,11 @@ function LaneComponent(props: LaneProps) {
|
||||
}, []);
|
||||
|
||||
const handleToggle = useCallback(() => onToggleCollapse(workflow.id), [onToggleCollapse, workflow.id]);
|
||||
const handleHeaderKeyDown = useCallback((event: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault();
|
||||
handleToggle();
|
||||
}, [handleToggle]);
|
||||
|
||||
const makeCanDrop = useCallback(
|
||||
(targetColumnId: string) => (taskId: string) => props.canDropTask(taskId, targetColumnId, workflow.id),
|
||||
@@ -142,17 +147,18 @@ function LaneComponent(props: LaneProps) {
|
||||
|
||||
return (
|
||||
<section className="lane" data-lane={workflow.id} aria-label={workflow.name}>
|
||||
<div className="lane-header">
|
||||
<button
|
||||
type="button"
|
||||
className="lane-collapse-toggle btn btn-icon btn-sm"
|
||||
onClick={handleToggle}
|
||||
aria-expanded={!collapsed}
|
||||
aria-label={collapsed
|
||||
? t("lane.expand", "Expand {{name}} lane", { name: workflow.name })
|
||||
: t("lane.collapse", "Collapse {{name}} lane", { name: workflow.name })}
|
||||
data-testid={`lane-toggle-${workflow.id}`}
|
||||
/>
|
||||
<div
|
||||
className="lane-header"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={handleToggle}
|
||||
onKeyDown={handleHeaderKeyDown}
|
||||
aria-expanded={!collapsed}
|
||||
aria-label={collapsed
|
||||
? t("lane.expand", "Expand {{name}} lane", { name: workflow.name })
|
||||
: t("lane.collapse", "Collapse {{name}} lane", { name: workflow.name })}
|
||||
data-testid={`lane-header-${workflow.id}`}
|
||||
>
|
||||
<h2 className="lane-name">{workflow.name}</h2>
|
||||
<span className="lane-count" data-testid={`lane-count-${workflow.id}`}>{tasks.length}</span>
|
||||
</div>
|
||||
|
||||
@@ -113,16 +113,43 @@ describe("Lane", () => {
|
||||
expect(screen.queryByText("Triage")).toBeNull();
|
||||
});
|
||||
|
||||
it("invokes onToggleCollapse with the workflow id", () => {
|
||||
it("invokes onToggleCollapse with the workflow id from the lane header", () => {
|
||||
const props = baseProps();
|
||||
render(<Lane {...props} />);
|
||||
fireEvent.click(screen.getByTestId("lane-toggle-builtin:coding"));
|
||||
fireEvent.click(screen.getByTestId("lane-header-builtin:coding"));
|
||||
expect(props.onToggleCollapse).toHaveBeenCalledWith("builtin:coding");
|
||||
});
|
||||
|
||||
it("does not render a chevron icon in the lane toggle", () => {
|
||||
it("exposes the lane header as the accessible collapse toggle", () => {
|
||||
render(<Lane {...baseProps()} />);
|
||||
expect(screen.getByTestId("lane-toggle-builtin:coding").querySelector("svg")).toBeNull();
|
||||
const header = screen.getByRole("button", { name: "Collapse Coding (built-in) lane" });
|
||||
expect(header.getAttribute("aria-expanded")).toBe("true");
|
||||
expect(header.getAttribute("tabindex")).toBe("0");
|
||||
});
|
||||
|
||||
it("toggles the lane header with Enter and Space", () => {
|
||||
const props = baseProps();
|
||||
render(<Lane {...props} />);
|
||||
const header = screen.getByTestId("lane-header-builtin:coding");
|
||||
|
||||
fireEvent.keyDown(header, { key: "Enter" });
|
||||
fireEvent.keyDown(header, { key: " " });
|
||||
|
||||
expect(props.onToggleCollapse).toHaveBeenCalledTimes(2);
|
||||
expect(props.onToggleCollapse).toHaveBeenNthCalledWith(1, "builtin:coding");
|
||||
expect(props.onToggleCollapse).toHaveBeenNthCalledWith(2, "builtin:coding");
|
||||
});
|
||||
|
||||
it("does not render a chevron icon in the lane header", () => {
|
||||
render(<Lane {...baseProps()} />);
|
||||
expect(screen.getByTestId("lane-header-builtin:coding").querySelector("svg")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render an empty btn-icon toggle in the lane header", () => {
|
||||
render(<Lane {...baseProps()} />);
|
||||
const header = screen.getByTestId("lane-header-builtin:coding");
|
||||
expect(header.querySelector(".btn-icon")).toBeNull();
|
||||
expect(screen.queryByTestId("lane-toggle-builtin:coding")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows the auto-merge toggle for human-review workflow columns", () => {
|
||||
|
||||
Reference in New Issue
Block a user