feat(FN-2025): move secondary header views into overflow menu
- Add view-toggle overflow state and refs in Header with Escape and outside-click close behavior - Replace inline Skills/Roadmaps/Insights buttons with a More views trigger and dropdown menu actions - Style the view-toggle overflow menu and items for positioning, hover, and active states - Update Header, tablet header controls, and App integration tests to validate overflow interactions and active view behavior
This commit is contained in:
@@ -98,6 +98,41 @@ describe("tablet header controls", () => {
|
||||
expect(screen.getByTitle("Board view")).toBeDefined();
|
||||
expect(screen.getByTitle("List view")).toBeDefined();
|
||||
expect(screen.getByTitle("Agents view")).toBeDefined();
|
||||
// Skills, Roadmaps, Insights are NOT inline (they're in overflow)
|
||||
expect(screen.queryByTitle("Skills view")).toBeNull();
|
||||
expect(screen.queryByTitle("Roadmaps view")).toBeNull();
|
||||
expect(screen.queryByTitle("Insights view")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders view toggle overflow trigger on tablet", () => {
|
||||
renderTabletHeader({ onChangeView: noop });
|
||||
expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeDefined();
|
||||
});
|
||||
|
||||
it("opens overflow menu with Insights, Roadmaps, Skills on tablet when trigger is clicked", () => {
|
||||
renderTabletHeader({ onChangeView: noop, showSkillsTab: true, experimentalFeatures: { insights: true, roadmap: true } });
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
expect(screen.getByTestId("view-overflow-insights")).toBeDefined();
|
||||
expect(screen.getByTestId("view-overflow-roadmaps")).toBeDefined();
|
||||
expect(screen.getByTestId("view-overflow-skills")).toBeDefined();
|
||||
});
|
||||
|
||||
it("calls onChangeView from overflow menu on tablet", () => {
|
||||
const onChangeView = vi.fn();
|
||||
renderTabletHeader({ onChangeView, experimentalFeatures: { insights: true } });
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
fireEvent.click(screen.getByTestId("view-overflow-insights"));
|
||||
expect(onChangeView).toHaveBeenCalledWith("insights");
|
||||
});
|
||||
|
||||
it("closes overflow menu on tablet after selecting an item", async () => {
|
||||
renderTabletHeader({ onChangeView: noop, showSkillsTab: true, experimentalFeatures: { insights: true, roadmap: true } });
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
expect(screen.getByTestId("view-overflow-insights")).toBeDefined();
|
||||
fireEvent.click(screen.getByTestId("view-overflow-skills"));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("view-overflow-insights")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Lower-priority actions move to overflow on tablet ──────────
|
||||
|
||||
@@ -268,6 +268,7 @@ export function Header({
|
||||
const [isTerminalSubmenuOpen, setIsTerminalSubmenuOpen] = useState(false);
|
||||
const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false);
|
||||
const [isMobileProjectSwitchOpen, setIsMobileProjectSwitchOpen] = useState(false);
|
||||
const [isViewOverflowOpen, setIsViewOverflowOpen] = useState(false);
|
||||
const [overflowScripts, setOverflowScripts] = useState<Record<string, string>>({});
|
||||
const [overflowScriptsLoading, setOverflowScriptsLoading] = useState(false);
|
||||
const overflowButtonRef = useRef<HTMLButtonElement>(null);
|
||||
@@ -277,6 +278,8 @@ export function Header({
|
||||
const terminalSubmenuOpenRef = useRef(false);
|
||||
const nodeSelectorRef = useRef<HTMLDivElement>(null);
|
||||
const mobileProjectSwitchRef = useRef<HTMLDivElement>(null);
|
||||
const viewOverflowRef = useRef<HTMLDivElement>(null);
|
||||
const viewOverflowTriggerRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
// Get remote nodes only (exclude local node type)
|
||||
const remoteNodes = useMemo(() =>
|
||||
@@ -380,6 +383,7 @@ export function Header({
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
setIsViewOverflowOpen(false);
|
||||
if (terminalSubmenuOpenRef.current) {
|
||||
setIsTerminalSubmenuOpen(false);
|
||||
return;
|
||||
@@ -412,6 +416,25 @@ export function Header({
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, [isMobileProjectSwitchOpen]);
|
||||
|
||||
// Close view toggle overflow on outside click
|
||||
useEffect(() => {
|
||||
if (!isViewOverflowOpen) return;
|
||||
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (
|
||||
viewOverflowRef.current &&
|
||||
!viewOverflowRef.current.contains(e.target as Node) &&
|
||||
viewOverflowTriggerRef.current &&
|
||||
!viewOverflowTriggerRef.current.contains(e.target as Node)
|
||||
) {
|
||||
setIsViewOverflowOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, [isViewOverflowOpen]);
|
||||
|
||||
const handleMobileSearchToggle = useCallback(() => {
|
||||
setIsMobileSearchOpen((prev) => !prev);
|
||||
}, []);
|
||||
@@ -744,38 +767,68 @@ export function Header({
|
||||
>
|
||||
<Mail size={16} />
|
||||
</button>
|
||||
{showSkillsTab && (
|
||||
<button
|
||||
className={`view-toggle-btn${view === "skills" ? " active" : ""}`}
|
||||
onClick={() => onChangeView("skills")}
|
||||
title="Skills view"
|
||||
aria-label="Skills view"
|
||||
aria-pressed={view === "skills"}
|
||||
<button
|
||||
ref={viewOverflowTriggerRef}
|
||||
className={`view-toggle-btn${["skills", "roadmaps", "insights"].includes(view) ? " active" : ""}`}
|
||||
onClick={() => setIsViewOverflowOpen((prev) => !prev)}
|
||||
title="More views"
|
||||
aria-label="More views"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={isViewOverflowOpen}
|
||||
data-testid="view-toggle-overflow-trigger"
|
||||
>
|
||||
<ChevronDown size={12} />
|
||||
</button>
|
||||
{isViewOverflowOpen && (
|
||||
<div
|
||||
ref={viewOverflowRef}
|
||||
className="view-toggle-overflow-menu"
|
||||
role="menu"
|
||||
aria-label="More views"
|
||||
>
|
||||
<Zap size={16} />
|
||||
</button>
|
||||
)}
|
||||
{experimentalFeatures?.roadmap && (
|
||||
<button
|
||||
className={`view-toggle-btn${view === "roadmaps" ? " active" : ""}`}
|
||||
onClick={() => onChangeView("roadmaps")}
|
||||
title="Roadmaps view"
|
||||
aria-label="Roadmaps view"
|
||||
aria-pressed={view === "roadmaps"}
|
||||
>
|
||||
<Map size={16} />
|
||||
</button>
|
||||
)}
|
||||
{experimentalFeatures?.insights && (
|
||||
<button
|
||||
className={`view-toggle-btn${view === "insights" ? " active" : ""}`}
|
||||
onClick={() => onChangeView("insights")}
|
||||
title="Insights view"
|
||||
aria-label="Insights view"
|
||||
aria-pressed={view === "insights"}
|
||||
>
|
||||
<Sparkles size={16} />
|
||||
</button>
|
||||
{experimentalFeatures?.insights && (
|
||||
<button
|
||||
className={`view-toggle-overflow-item${view === "insights" ? " active" : ""}`}
|
||||
onClick={() => {
|
||||
onChangeView("insights");
|
||||
setIsViewOverflowOpen(false);
|
||||
}}
|
||||
role="menuitem"
|
||||
data-testid="view-overflow-insights"
|
||||
>
|
||||
<Sparkles size={14} />
|
||||
<span>Insights</span>
|
||||
</button>
|
||||
)}
|
||||
{experimentalFeatures?.roadmap && (
|
||||
<button
|
||||
className={`view-toggle-overflow-item${view === "roadmaps" ? " active" : ""}`}
|
||||
onClick={() => {
|
||||
onChangeView("roadmaps");
|
||||
setIsViewOverflowOpen(false);
|
||||
}}
|
||||
role="menuitem"
|
||||
data-testid="view-overflow-roadmaps"
|
||||
>
|
||||
<Map size={14} />
|
||||
<span>Roadmaps</span>
|
||||
</button>
|
||||
)}
|
||||
{showSkillsTab && (
|
||||
<button
|
||||
className={`view-toggle-overflow-item${view === "skills" ? " active" : ""}`}
|
||||
onClick={() => {
|
||||
onChangeView("skills");
|
||||
setIsViewOverflowOpen(false);
|
||||
}}
|
||||
role="menuitem"
|
||||
data-testid="view-overflow-skills"
|
||||
>
|
||||
<Zap size={14} />
|
||||
<span>Skills</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1254,11 +1254,12 @@ describe("App view switching", () => {
|
||||
|
||||
// Wait for the header to render
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTitle("Insights view")).toBeTruthy();
|
||||
expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Click to switch to insights view
|
||||
fireEvent.click(screen.getByTitle("Insights view"));
|
||||
// Open the overflow menu and click Insights
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
fireEvent.click(screen.getByTestId("view-overflow-insights"));
|
||||
|
||||
// Insights view should be rendered (it has a insights-view container)
|
||||
await waitFor(() => {
|
||||
@@ -1277,10 +1278,11 @@ describe("App view switching", () => {
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTitle("Insights view")).toBeTruthy();
|
||||
expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTitle("Insights view"));
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
fireEvent.click(screen.getByTestId("view-overflow-insights"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(localStorage.getItem(taskViewStorageKey())).toBe("insights");
|
||||
@@ -1296,7 +1298,8 @@ describe("App view switching", () => {
|
||||
expect(document.querySelector(".insights-view")).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(screen.getByTitle("Insights view").className).toContain("active");
|
||||
// Overflow trigger should be active when view is insights
|
||||
expect(screen.getByTestId("view-toggle-overflow-trigger").className).toContain("active");
|
||||
|
||||
localStorage.removeItem(taskViewStorageKey());
|
||||
});
|
||||
@@ -1319,8 +1322,8 @@ describe("App view switching", () => {
|
||||
expect(document.querySelector(".insights-view")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Verify insights is active
|
||||
expect(screen.getByTitle("Insights view").className).toContain("active");
|
||||
// Verify overflow trigger is active
|
||||
expect(screen.getByTestId("view-toggle-overflow-trigger").className).toContain("active");
|
||||
|
||||
// Cleanup
|
||||
localStorage.removeItem("kb:proj_a:kb-dashboard-task-view");
|
||||
@@ -1341,8 +1344,9 @@ describe("App view switching", () => {
|
||||
expect(screen.getByTitle("Board view")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Insights button should not be rendered
|
||||
expect(screen.queryByTitle("Insights view")).toBeNull();
|
||||
// Open the overflow menu - Insights item should not be rendered
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
expect(screen.queryByTestId("view-overflow-insights")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -358,205 +358,143 @@ describe("Header", () => {
|
||||
expect(missionsBtn.getAttribute("aria-pressed")).toBe("false");
|
||||
});
|
||||
|
||||
// ── Skills View Toggle ─────────────────────────────────────────
|
||||
// ── View Toggle Overflow ─────────────────────────────────────────
|
||||
|
||||
it("renders skills view button in view toggle when onChangeView is provided", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} showSkillsTab={true} />);
|
||||
const skillsBtn = screen.getByTitle("Skills view");
|
||||
expect(skillsBtn).toBeDefined();
|
||||
});
|
||||
describe("View Toggle Overflow", () => {
|
||||
it("renders overflow trigger button when onChangeView is provided", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} />);
|
||||
expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeDefined();
|
||||
});
|
||||
|
||||
it("calls onChangeView with 'skills' when skills view button is clicked", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} showSkillsTab={true} />);
|
||||
const skillsBtn = screen.getByTitle("Skills view");
|
||||
fireEvent.click(skillsBtn);
|
||||
expect(onChangeView).toHaveBeenCalledWith("skills");
|
||||
});
|
||||
it("does not render overflow trigger when onChangeView is not provided", () => {
|
||||
render(<Header view="board" />);
|
||||
expect(screen.queryByTestId("view-toggle-overflow-trigger")).toBeNull();
|
||||
});
|
||||
|
||||
it("marks skills view button as active when view is 'skills'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="skills" onChangeView={onChangeView} showSkillsTab={true} />);
|
||||
const skillsBtn = screen.getByTitle("Skills view");
|
||||
expect(skillsBtn.className).toContain("active");
|
||||
expect(skillsBtn.getAttribute("aria-pressed")).toBe("true");
|
||||
});
|
||||
it("opens overflow menu when trigger is clicked", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} showSkillsTab experimentalFeatures={{ insights: true, roadmap: true }} />);
|
||||
const trigger = screen.getByTestId("view-toggle-overflow-trigger");
|
||||
fireEvent.click(trigger);
|
||||
expect(screen.getByTestId("view-overflow-insights")).toBeDefined();
|
||||
expect(screen.getByTestId("view-overflow-roadmaps")).toBeDefined();
|
||||
expect(screen.getByTestId("view-overflow-skills")).toBeDefined();
|
||||
});
|
||||
|
||||
it("does not mark skills view button as active when view is 'board'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} showSkillsTab={true} />);
|
||||
const skillsBtn = screen.getByTitle("Skills view");
|
||||
expect(skillsBtn.className).not.toContain("active");
|
||||
expect(skillsBtn.getAttribute("aria-pressed")).toBe("false");
|
||||
});
|
||||
it("closes overflow menu when trigger is clicked again", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} showSkillsTab experimentalFeatures={{ insights: true, roadmap: true }} />);
|
||||
const trigger = screen.getByTestId("view-toggle-overflow-trigger");
|
||||
fireEvent.click(trigger);
|
||||
expect(screen.getByTestId("view-overflow-insights")).toBeDefined();
|
||||
fireEvent.click(trigger);
|
||||
expect(screen.queryByTestId("view-overflow-insights")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render skills view button when showSkillsTab is false", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} showSkillsTab={false} />);
|
||||
expect(screen.queryByTitle("Skills view")).toBeNull();
|
||||
});
|
||||
it("calls onChangeView with 'insights' when Insights overflow item is clicked", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ insights: true }} />);
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
fireEvent.click(screen.getByTestId("view-overflow-insights"));
|
||||
expect(onChangeView).toHaveBeenCalledWith("insights");
|
||||
});
|
||||
|
||||
it("does not render skills view button when showSkillsTab is omitted", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} />);
|
||||
expect(screen.queryByTitle("Skills view")).toBeNull();
|
||||
});
|
||||
it("calls onChangeView with 'roadmaps' when Roadmaps overflow item is clicked", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ roadmap: true }} />);
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
fireEvent.click(screen.getByTestId("view-overflow-roadmaps"));
|
||||
expect(onChangeView).toHaveBeenCalledWith("roadmaps");
|
||||
});
|
||||
|
||||
// ── Chat View Toggle ─────────────────────────────────────────
|
||||
it("calls onChangeView with 'skills' when Skills overflow item is clicked", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} showSkillsTab />);
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
fireEvent.click(screen.getByTestId("view-overflow-skills"));
|
||||
expect(onChangeView).toHaveBeenCalledWith("skills");
|
||||
});
|
||||
|
||||
it("renders chat view button in view toggle when onChangeView is provided", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} />);
|
||||
const chatBtn = screen.getByTitle("Chat view");
|
||||
expect(chatBtn).toBeDefined();
|
||||
});
|
||||
it("shows overflow trigger as active when view is 'insights'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="insights" onChangeView={onChangeView} experimentalFeatures={{ insights: true }} />);
|
||||
expect(screen.getByTestId("view-toggle-overflow-trigger").className).toContain("active");
|
||||
});
|
||||
|
||||
it("calls onChangeView with 'chat' when chat view button is clicked", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} />);
|
||||
const chatBtn = screen.getByTitle("Chat view");
|
||||
fireEvent.click(chatBtn);
|
||||
expect(onChangeView).toHaveBeenCalledWith("chat");
|
||||
});
|
||||
it("shows overflow trigger as active when view is 'roadmaps'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="roadmaps" onChangeView={onChangeView} experimentalFeatures={{ roadmap: true }} />);
|
||||
expect(screen.getByTestId("view-toggle-overflow-trigger").className).toContain("active");
|
||||
});
|
||||
|
||||
it("marks chat view button as active when view is 'chat'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="chat" onChangeView={onChangeView} />);
|
||||
const chatBtn = screen.getByTitle("Chat view");
|
||||
expect(chatBtn.className).toContain("active");
|
||||
expect(chatBtn.getAttribute("aria-pressed")).toBe("true");
|
||||
});
|
||||
it("shows overflow trigger as active when view is 'skills'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="skills" onChangeView={onChangeView} showSkillsTab />);
|
||||
expect(screen.getByTestId("view-toggle-overflow-trigger").className).toContain("active");
|
||||
});
|
||||
|
||||
it("does not mark chat view button as active when view is 'board'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} />);
|
||||
const chatBtn = screen.getByTitle("Chat view");
|
||||
expect(chatBtn.className).not.toContain("active");
|
||||
expect(chatBtn.getAttribute("aria-pressed")).toBe("false");
|
||||
});
|
||||
it("overflow trigger is not active when view is 'board'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} />);
|
||||
expect(screen.getByTestId("view-toggle-overflow-trigger").className).not.toContain("active");
|
||||
});
|
||||
|
||||
// ── Mailbox View Toggle ─────────────────────────────────────
|
||||
it("overflow trigger is not active when view is 'list'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="list" onChangeView={onChangeView} />);
|
||||
expect(screen.getByTestId("view-toggle-overflow-trigger").className).not.toContain("active");
|
||||
});
|
||||
|
||||
it("renders mailbox view button in view toggle when onChangeView is provided", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} />);
|
||||
const mailboxBtn = screen.getByTitle("Mailbox view");
|
||||
expect(mailboxBtn).toBeDefined();
|
||||
});
|
||||
it("closes overflow menu on Escape key", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} showSkillsTab experimentalFeatures={{ insights: true }} />);
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
expect(screen.getByTestId("view-overflow-insights")).toBeDefined();
|
||||
fireEvent.keyDown(document, { key: "Escape" });
|
||||
expect(screen.queryByTestId("view-overflow-insights")).toBeNull();
|
||||
});
|
||||
|
||||
it("calls onChangeView with 'mailbox' when mailbox view button is clicked", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} />);
|
||||
const mailboxBtn = screen.getByTitle("Mailbox view");
|
||||
fireEvent.click(mailboxBtn);
|
||||
expect(onChangeView).toHaveBeenCalledWith("mailbox");
|
||||
});
|
||||
it("closes overflow menu on outside click", async () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} showSkillsTab experimentalFeatures={{ insights: true }} />);
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
expect(screen.getByTestId("view-overflow-insights")).toBeDefined();
|
||||
// Simulate click outside
|
||||
fireEvent.mouseDown(document.body);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("view-overflow-insights")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("marks mailbox view button as active when view is 'mailbox'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="mailbox" onChangeView={onChangeView} />);
|
||||
const mailboxBtn = screen.getByTitle("Mailbox view");
|
||||
expect(mailboxBtn.className).toContain("active");
|
||||
expect(mailboxBtn.getAttribute("aria-pressed")).toBe("true");
|
||||
});
|
||||
it("does not render skills, roadmaps, insights as inline toggle buttons", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} showSkillsTab experimentalFeatures={{ insights: true, roadmap: true }} />);
|
||||
expect(screen.queryByTitle("Skills view")).toBeNull();
|
||||
expect(screen.queryByTitle("Roadmaps view")).toBeNull();
|
||||
expect(screen.queryByTitle("Insights view")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not mark mailbox view button as active when view is 'board'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} />);
|
||||
const mailboxBtn = screen.getByTitle("Mailbox view");
|
||||
expect(mailboxBtn.className).not.toContain("active");
|
||||
expect(mailboxBtn.getAttribute("aria-pressed")).toBe("false");
|
||||
});
|
||||
it("does not render Insights overflow item when experimentalFeatures.insights is false", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ insights: false }} />);
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
expect(screen.queryByTestId("view-overflow-insights")).toBeNull();
|
||||
});
|
||||
|
||||
// ── Roadmaps View Toggle ───────────────────────────────────────
|
||||
it("does not render Roadmaps overflow item when experimentalFeatures.roadmap is false", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ roadmap: false }} />);
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
expect(screen.queryByTestId("view-overflow-roadmaps")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders roadmaps view button in view toggle when experimentalFeatures.roadmap is true", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ roadmap: true }} />);
|
||||
const roadmapsBtn = screen.getByTitle("Roadmaps view");
|
||||
expect(roadmapsBtn).toBeDefined();
|
||||
});
|
||||
|
||||
it("calls onChangeView with 'roadmaps' when roadmaps view button is clicked", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ roadmap: true }} />);
|
||||
const roadmapsBtn = screen.getByTitle("Roadmaps view");
|
||||
fireEvent.click(roadmapsBtn);
|
||||
expect(onChangeView).toHaveBeenCalledWith("roadmaps");
|
||||
});
|
||||
|
||||
it("marks roadmaps view button as active when view is 'roadmaps'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="roadmaps" onChangeView={onChangeView} experimentalFeatures={{ roadmap: true }} />);
|
||||
const roadmapsBtn = screen.getByTitle("Roadmaps view");
|
||||
expect(roadmapsBtn.className).toContain("active");
|
||||
expect(roadmapsBtn.getAttribute("aria-pressed")).toBe("true");
|
||||
});
|
||||
|
||||
it("does not mark roadmaps view button as active when view is 'board'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ roadmap: true }} />);
|
||||
const roadmapsBtn = screen.getByTitle("Roadmaps view");
|
||||
expect(roadmapsBtn.className).not.toContain("active");
|
||||
expect(roadmapsBtn.getAttribute("aria-pressed")).toBe("false");
|
||||
});
|
||||
|
||||
it("does not render roadmaps view button when experimentalFeatures.roadmap is false", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ roadmap: false }} />);
|
||||
expect(screen.queryByTitle("Roadmaps view")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render roadmaps view button when experimentalFeatures is not provided", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} />);
|
||||
expect(screen.queryByTitle("Roadmaps view")).toBeNull();
|
||||
});
|
||||
|
||||
// ── Insights View Toggle ─────────────────────────────────────────
|
||||
|
||||
it("renders insights view button in view toggle when experimentalFeatures.insights is true", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ insights: true }} />);
|
||||
const insightsBtn = screen.getByTitle("Insights view");
|
||||
expect(insightsBtn).toBeDefined();
|
||||
});
|
||||
|
||||
it("calls onChangeView with 'insights' when insights view button is clicked", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ insights: true }} />);
|
||||
const insightsBtn = screen.getByTitle("Insights view");
|
||||
fireEvent.click(insightsBtn);
|
||||
expect(onChangeView).toHaveBeenCalledWith("insights");
|
||||
});
|
||||
|
||||
it("marks insights view button as active when view is 'insights'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="insights" onChangeView={onChangeView} experimentalFeatures={{ insights: true }} />);
|
||||
const insightsBtn = screen.getByTitle("Insights view");
|
||||
expect(insightsBtn.className).toContain("active");
|
||||
expect(insightsBtn.getAttribute("aria-pressed")).toBe("true");
|
||||
});
|
||||
|
||||
it("does not mark insights view button as active when view is 'board'", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ insights: true }} />);
|
||||
const insightsBtn = screen.getByTitle("Insights view");
|
||||
expect(insightsBtn.className).not.toContain("active");
|
||||
expect(insightsBtn.getAttribute("aria-pressed")).toBe("false");
|
||||
});
|
||||
|
||||
it("does not render insights view button when experimentalFeatures.insights is false", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} experimentalFeatures={{ insights: false }} />);
|
||||
expect(screen.queryByTitle("Insights view")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render insights view button when experimentalFeatures is not provided", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} />);
|
||||
expect(screen.queryByTitle("Insights view")).toBeNull();
|
||||
it("does not render Skills overflow item when showSkillsTab is false", () => {
|
||||
const onChangeView = vi.fn();
|
||||
render(<Header view="board" onChangeView={onChangeView} showSkillsTab={false} />);
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
expect(screen.queryByTestId("view-overflow-skills")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Search Visibility by View ─────────────────────────────────────
|
||||
|
||||
@@ -858,6 +858,7 @@ body {
|
||||
border-radius: var(--radius);
|
||||
padding: 2px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.view-toggle-btn {
|
||||
@@ -893,6 +894,42 @@ body {
|
||||
color: var(--bg);
|
||||
}
|
||||
|
||||
/* === View Toggle Overflow Dropdown === */
|
||||
.view-toggle-overflow-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: var(--space-xs);
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-lg);
|
||||
min-width: 140px;
|
||||
z-index: 200;
|
||||
padding: var(--space-xs) 0;
|
||||
}
|
||||
.view-toggle-overflow-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
width: 100%;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
.view-toggle-overflow-item:hover {
|
||||
background: var(--card-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
.view-toggle-overflow-item.active {
|
||||
color: var(--todo);
|
||||
}
|
||||
|
||||
/* Header Search */
|
||||
.header-search {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user