feat(FN-1330): consolidate mobile Board/List tabs into Tasks tab with header toggle
- Merge mobile Board and List tabs into unified Tasks tab in MobileNavBar - Add Board/List toggle to Header component for switching views within Tasks tab - Add comprehensive Header tests covering toggle functionality and keyboard navigation - Update mobile feature access tests to reflect new tab structure - Add mobile task view toggle CSS styles
This commit is contained in:
@@ -93,22 +93,22 @@ describe("Mobile Feature Access Regression Guard", () => {
|
|||||||
const props = createDefaultMobileNavProps();
|
const props = createDefaultMobileNavProps();
|
||||||
render(<MobileNavBar {...props} view="board" />);
|
render(<MobileNavBar {...props} view="board" />);
|
||||||
|
|
||||||
const listTab = screen.getByTestId("mobile-nav-tab-list");
|
const tasksTab = screen.getByTestId("mobile-nav-tab-tasks");
|
||||||
expect(listTab.textContent).toContain("List");
|
expect(tasksTab.textContent).toContain("Tasks");
|
||||||
|
|
||||||
fireEvent.click(listTab);
|
fireEvent.click(tasksTab);
|
||||||
expect(props.onChangeView).toHaveBeenCalledWith("list");
|
expect(props.onChangeView).toHaveBeenCalledWith("board");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("board view is accessible via mobile nav bar", () => {
|
it("board view is accessible via mobile nav bar", () => {
|
||||||
const props = createDefaultMobileNavProps();
|
const props = createDefaultMobileNavProps();
|
||||||
render(<MobileNavBar {...props} view="board" />);
|
render(<MobileNavBar {...props} view="list" />);
|
||||||
|
|
||||||
const boardTab = screen.getByTestId("mobile-nav-tab-board");
|
const tasksTab = screen.getByTestId("mobile-nav-tab-tasks");
|
||||||
expect(boardTab.textContent).toContain("Board");
|
expect(tasksTab.textContent).toContain("Tasks");
|
||||||
|
|
||||||
fireEvent.click(boardTab);
|
fireEvent.click(tasksTab);
|
||||||
expect(props.onChangeView).toHaveBeenCalledWith("board");
|
expect(props.onChangeView).toHaveBeenCalledWith("list");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("agents view is accessible via mobile nav bar", () => {
|
it("agents view is accessible via mobile nav bar", () => {
|
||||||
@@ -214,7 +214,7 @@ describe("Mobile Feature Access Regression Guard", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("mobile-nav-tab-board"));
|
fireEvent.click(screen.getByTestId("mobile-nav-tab-tasks"));
|
||||||
fireEvent.click(screen.getByTestId("mobile-nav-tab-agents"));
|
fireEvent.click(screen.getByTestId("mobile-nav-tab-agents"));
|
||||||
|
|
||||||
expect(mobileNavOnChangeView).toHaveBeenCalledWith("board");
|
expect(mobileNavOnChangeView).toHaveBeenCalledWith("board");
|
||||||
|
|||||||
@@ -588,6 +588,32 @@ export function Header({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="header-actions">
|
<div className="header-actions">
|
||||||
|
{/* Mobile View Toggle - compact board/list switcher in header when mobile nav is active */}
|
||||||
|
{hideFullNav && onChangeView && (view === "board" || view === "list") && (
|
||||||
|
<div className="view-toggle" data-testid="mobile-view-toggle">
|
||||||
|
<button
|
||||||
|
className={`view-toggle-btn${view === "board" ? " active" : ""}`}
|
||||||
|
onClick={() => onChangeView("board")}
|
||||||
|
title="Board view"
|
||||||
|
aria-label="Board view"
|
||||||
|
aria-pressed={view === "board"}
|
||||||
|
data-testid="mobile-view-toggle-board"
|
||||||
|
>
|
||||||
|
<LayoutGrid size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`view-toggle-btn${view === "list" ? " active" : ""}`}
|
||||||
|
onClick={() => onChangeView("list")}
|
||||||
|
title="List view"
|
||||||
|
aria-label="List view"
|
||||||
|
aria-pressed={view === "list"}
|
||||||
|
data-testid="mobile-view-toggle-list"
|
||||||
|
>
|
||||||
|
<List size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Mobile Search Trigger - only on mobile, show trigger button in header */}
|
{/* Mobile Search Trigger - only on mobile, show trigger button in header */}
|
||||||
{onSearchChange && isMobile && (hideFullNav || view === "board" || view === "list") && !shouldShowMobileSearch && (
|
{onSearchChange && isMobile && (hideFullNav || view === "board" || view === "list") && !shouldShowMobileSearch && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
GitBranch,
|
GitBranch,
|
||||||
Grid3X3,
|
Grid3X3,
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
List,
|
|
||||||
Lightbulb,
|
Lightbulb,
|
||||||
Loader2,
|
Loader2,
|
||||||
Mail,
|
Mail,
|
||||||
@@ -172,25 +171,21 @@ export function MobileNavBar({
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`mobile-nav-tab${view === "board" ? " mobile-nav-tab--active" : ""}`}
|
className={`mobile-nav-tab${view === "board" || view === "list" ? " mobile-nav-tab--active" : ""}`}
|
||||||
data-testid="mobile-nav-tab-board"
|
data-testid="mobile-nav-tab-tasks"
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-selected={view === "board"}
|
aria-selected={view === "board" || view === "list"}
|
||||||
onClick={() => onChangeView("board")}
|
onClick={() => {
|
||||||
|
// If already on a tasks view, stay there; otherwise go to board
|
||||||
|
if (view === "board" || view === "list") {
|
||||||
|
onChangeView(view);
|
||||||
|
} else {
|
||||||
|
onChangeView("board");
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<LayoutGrid />
|
<LayoutGrid />
|
||||||
<span className="mobile-nav-tab-label">Board</span>
|
<span className="mobile-nav-tab-label">Tasks</span>
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`mobile-nav-tab${view === "list" ? " mobile-nav-tab--active" : ""}`}
|
|
||||||
data-testid="mobile-nav-tab-list"
|
|
||||||
role="tab"
|
|
||||||
aria-selected={view === "list"}
|
|
||||||
onClick={() => onChangeView("list")}
|
|
||||||
>
|
|
||||||
<List />
|
|
||||||
<span className="mobile-nav-tab-label">List</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -921,10 +921,74 @@ describe("Header", () => {
|
|||||||
expect(screen.getByTitle("List view")).toBeDefined();
|
expect(screen.getByTitle("List view")).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("hides view toggle when mobileNavEnabled is true", () => {
|
it("hides desktop view toggle when mobileNavEnabled is true (mobile view toggle shown separately)", () => {
|
||||||
|
// When mobileNavEnabled, the full desktop-style view toggle (with agents, missions, etc.)
|
||||||
|
// should be hidden. Instead, a compact board/list-only toggle appears via mobile-view-toggle.
|
||||||
render(<Header view="board" onChangeView={vi.fn()} mobileNavEnabled={true} />);
|
render(<Header view="board" onChangeView={vi.fn()} mobileNavEnabled={true} />);
|
||||||
expect(screen.queryByTitle("Board view")).toBeNull();
|
// The full desktop toggle is hidden
|
||||||
expect(screen.queryByTitle("List view")).toBeNull();
|
expect(screen.queryByTitle("Agents view")).toBeNull();
|
||||||
|
expect(screen.queryByTitle("Missions view")).toBeNull();
|
||||||
|
// But the mobile compact toggle is shown
|
||||||
|
expect(screen.getByTestId("mobile-view-toggle")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders mobile view toggle when mobileNavEnabled and view is board", () => {
|
||||||
|
const onChangeView = vi.fn();
|
||||||
|
render(<Header view="board" onChangeView={onChangeView} mobileNavEnabled={true} />);
|
||||||
|
expect(screen.getByTestId("mobile-view-toggle")).toBeDefined();
|
||||||
|
expect(screen.getByTestId("mobile-view-toggle-board")).toBeDefined();
|
||||||
|
expect(screen.getByTestId("mobile-view-toggle-list")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders mobile view toggle when mobileNavEnabled and view is list", () => {
|
||||||
|
render(<Header view="list" onChangeView={vi.fn()} mobileNavEnabled={true} />);
|
||||||
|
expect(screen.getByTestId("mobile-view-toggle")).toBeDefined();
|
||||||
|
expect(screen.getByTestId("mobile-view-toggle-board")).toBeDefined();
|
||||||
|
expect(screen.getByTestId("mobile-view-toggle-list")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render mobile view toggle when mobileNavEnabled and view is agents", () => {
|
||||||
|
render(<Header view="agents" onChangeView={vi.fn()} mobileNavEnabled={true} />);
|
||||||
|
expect(screen.queryByTestId("mobile-view-toggle")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render mobile view toggle when mobileNavEnabled and view is missions", () => {
|
||||||
|
render(<Header view="missions" onChangeView={vi.fn()} mobileNavEnabled={true} />);
|
||||||
|
expect(screen.queryByTestId("mobile-view-toggle")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("mobile view toggle board button calls onChangeView('board')", () => {
|
||||||
|
const onChangeView = vi.fn();
|
||||||
|
render(<Header view="list" onChangeView={onChangeView} mobileNavEnabled={true} />);
|
||||||
|
fireEvent.click(screen.getByTestId("mobile-view-toggle-board"));
|
||||||
|
expect(onChangeView).toHaveBeenCalledWith("board");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("mobile view toggle list button calls onChangeView('list')", () => {
|
||||||
|
const onChangeView = vi.fn();
|
||||||
|
render(<Header view="board" onChangeView={onChangeView} mobileNavEnabled={true} />);
|
||||||
|
fireEvent.click(screen.getByTestId("mobile-view-toggle-list"));
|
||||||
|
expect(onChangeView).toHaveBeenCalledWith("list");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("mobile view toggle board button is active when view is board", () => {
|
||||||
|
render(<Header view="board" onChangeView={vi.fn()} mobileNavEnabled={true} />);
|
||||||
|
const boardBtn = screen.getByTestId("mobile-view-toggle-board");
|
||||||
|
const listBtn = screen.getByTestId("mobile-view-toggle-list");
|
||||||
|
expect(boardBtn.className).toContain("active");
|
||||||
|
expect(boardBtn.getAttribute("aria-pressed")).toBe("true");
|
||||||
|
expect(listBtn.className).not.toContain("active");
|
||||||
|
expect(listBtn.getAttribute("aria-pressed")).toBe("false");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("mobile view toggle list button is active when view is list", () => {
|
||||||
|
render(<Header view="list" onChangeView={vi.fn()} mobileNavEnabled={true} />);
|
||||||
|
const boardBtn = screen.getByTestId("mobile-view-toggle-board");
|
||||||
|
const listBtn = screen.getByTestId("mobile-view-toggle-list");
|
||||||
|
expect(listBtn.className).toContain("active");
|
||||||
|
expect(listBtn.getAttribute("aria-pressed")).toBe("true");
|
||||||
|
expect(boardBtn.className).not.toContain("active");
|
||||||
|
expect(boardBtn.getAttribute("aria-pressed")).toBe("false");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("hides overflow trigger when mobileNavEnabled is true", () => {
|
it("hides overflow trigger when mobileNavEnabled is true", () => {
|
||||||
|
|||||||
@@ -56,11 +56,10 @@ describe("MobileNavBar", () => {
|
|||||||
mockViewport("mobile");
|
mockViewport("mobile");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders nine tab buttons (board + list + agents + missions + chat + mailbox + skills + roadmaps + more)", () => {
|
it("renders eight tab buttons (tasks + agents + missions + chat + mailbox + skills + roadmaps + more)", () => {
|
||||||
render(<MobileNavBar {...createDefaultProps()} />);
|
render(<MobileNavBar {...createDefaultProps()} />);
|
||||||
|
|
||||||
expect(screen.getByTestId("mobile-nav-tab-board")).toBeDefined();
|
expect(screen.getByTestId("mobile-nav-tab-tasks")).toBeDefined();
|
||||||
expect(screen.getByTestId("mobile-nav-tab-list")).toBeDefined();
|
|
||||||
expect(screen.getByTestId("mobile-nav-tab-agents")).toBeDefined();
|
expect(screen.getByTestId("mobile-nav-tab-agents")).toBeDefined();
|
||||||
expect(screen.getByTestId("mobile-nav-tab-missions")).toBeDefined();
|
expect(screen.getByTestId("mobile-nav-tab-missions")).toBeDefined();
|
||||||
expect(screen.getByTestId("mobile-nav-tab-chat")).toBeDefined();
|
expect(screen.getByTestId("mobile-nav-tab-chat")).toBeDefined();
|
||||||
@@ -95,38 +94,43 @@ describe("MobileNavBar", () => {
|
|||||||
expect(screen.getByTestId("mobile-nav-tab-agents").className).toContain("mobile-nav-tab--active");
|
expect(screen.getByTestId("mobile-nav-tab-agents").className).toContain("mobile-nav-tab--active");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("board tab calls onChangeView with 'board'", () => {
|
it("tasks tab calls onChangeView with 'board' when coming from a non-tasks view", () => {
|
||||||
const props = createDefaultProps();
|
const props = createDefaultProps();
|
||||||
render(<MobileNavBar {...props} view="list" />);
|
render(<MobileNavBar {...props} view="agents" />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("mobile-nav-tab-board"));
|
fireEvent.click(screen.getByTestId("mobile-nav-tab-tasks"));
|
||||||
expect(props.onChangeView).toHaveBeenCalledWith("board");
|
expect(props.onChangeView).toHaveBeenCalledWith("board");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("list tab calls onChangeView with 'list'", () => {
|
it("tasks tab calls onChangeView with 'board' when already on board", () => {
|
||||||
const props = createDefaultProps();
|
const props = createDefaultProps();
|
||||||
render(<MobileNavBar {...props} view="board" />);
|
render(<MobileNavBar {...props} view="board" />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("mobile-nav-tab-list"));
|
fireEvent.click(screen.getByTestId("mobile-nav-tab-tasks"));
|
||||||
|
expect(props.onChangeView).toHaveBeenCalledWith("board");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tasks tab calls onChangeView with 'list' when already on list", () => {
|
||||||
|
const props = createDefaultProps();
|
||||||
|
render(<MobileNavBar {...props} view="list" />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId("mobile-nav-tab-tasks"));
|
||||||
expect(props.onChangeView).toHaveBeenCalledWith("list");
|
expect(props.onChangeView).toHaveBeenCalledWith("list");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("board tab is active when view is 'board'", () => {
|
it("tasks tab is active when view is 'board'", () => {
|
||||||
render(<MobileNavBar {...createDefaultProps()} view="board" />);
|
render(<MobileNavBar {...createDefaultProps()} view="board" />);
|
||||||
expect(screen.getByTestId("mobile-nav-tab-board").className).toContain("mobile-nav-tab--active");
|
expect(screen.getByTestId("mobile-nav-tab-tasks").className).toContain("mobile-nav-tab--active");
|
||||||
expect(screen.getByTestId("mobile-nav-tab-list").className).not.toContain("mobile-nav-tab--active");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("list tab is active when view is 'list'", () => {
|
it("tasks tab is active when view is 'list'", () => {
|
||||||
render(<MobileNavBar {...createDefaultProps()} view="list" />);
|
render(<MobileNavBar {...createDefaultProps()} view="list" />);
|
||||||
expect(screen.getByTestId("mobile-nav-tab-list").className).toContain("mobile-nav-tab--active");
|
expect(screen.getByTestId("mobile-nav-tab-tasks").className).toContain("mobile-nav-tab--active");
|
||||||
expect(screen.getByTestId("mobile-nav-tab-board").className).not.toContain("mobile-nav-tab--active");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("board and list tabs are not active when view is 'agents'", () => {
|
it("tasks tab is not active when view is 'agents'", () => {
|
||||||
render(<MobileNavBar {...createDefaultProps()} view="agents" />);
|
render(<MobileNavBar {...createDefaultProps()} view="agents" />);
|
||||||
expect(screen.getByTestId("mobile-nav-tab-board").className).not.toContain("mobile-nav-tab--active");
|
expect(screen.getByTestId("mobile-nav-tab-tasks").className).not.toContain("mobile-nav-tab--active");
|
||||||
expect(screen.getByTestId("mobile-nav-tab-list").className).not.toContain("mobile-nav-tab--active");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("missions tab calls onChangeView with 'missions'", () => {
|
it("missions tab calls onChangeView with 'missions'", () => {
|
||||||
|
|||||||
@@ -6683,6 +6683,20 @@ body {
|
|||||||
padding-right: max(var(--space-md), env(safe-area-inset-right, 0px));
|
padding-right: max(var(--space-md), env(safe-area-inset-right, 0px));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile view toggle in header — compact 2-button board/list switcher */
|
||||||
|
.header-actions > .view-toggle {
|
||||||
|
height: 32px;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions > .view-toggle .view-toggle-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
min-width: unset;
|
||||||
|
min-height: unset;
|
||||||
|
}
|
||||||
|
|
||||||
/* Modal: full-screen on mobile */
|
/* Modal: full-screen on mobile */
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user