chore(sidebar): move tag layout toggle into a menu
This commit is contained in:
parent
bf1f742321
commit
5ad4aea565
5 changed files with 103 additions and 52 deletions
|
|
@ -1,6 +1,15 @@
|
|||
import { HashIcon, ListIcon, ListTreeIcon } from "lucide-react";
|
||||
import { HashIcon, ListTreeIcon, MoreHorizontalIcon } from "lucide-react";
|
||||
import { forwardRef, useMemo } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useMemoFilterContext } from "@/contexts/MemoFilterContext";
|
||||
import { useLocalStorage, useOverflowTitle } from "@/hooks";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
|
@ -13,11 +22,7 @@ import {
|
|||
sidebarRowStateAttributes,
|
||||
sidebarRowStateClasses,
|
||||
} from "./SidebarRow";
|
||||
import SidebarSection, {
|
||||
SIDEBAR_SECTION_ACTION_ACTIVE_CLASSES,
|
||||
SIDEBAR_SECTION_ACTION_BUTTON_CLASSES,
|
||||
SIDEBAR_SECTION_ACTION_ICON_CLASSES,
|
||||
} from "./SidebarSection";
|
||||
import SidebarSection, { SIDEBAR_SECTION_ACTION_BUTTON_CLASSES, SIDEBAR_SECTION_ACTION_ICON_CLASSES } from "./SidebarSection";
|
||||
|
||||
interface Props {
|
||||
tagCount: Record<string, number>;
|
||||
|
|
@ -100,28 +105,42 @@ const TagsSection = ({ tagCount, onSelect, scope }: Props) => {
|
|||
<SidebarSection
|
||||
label={t("common.tags")}
|
||||
action={
|
||||
<div className="flex items-center gap-0.5" role="group" aria-label={t("common.tags")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label={`${t("common.tags")}: ${t("memo.layout-list")}`}
|
||||
aria-pressed={!treeMode}
|
||||
className={cn(SIDEBAR_SECTION_ACTION_BUTTON_CLASSES, !treeMode && SIDEBAR_SECTION_ACTION_ACTIVE_CLASSES)}
|
||||
onClick={() => setTreeMode(false)}
|
||||
>
|
||||
<ListIcon className={SIDEBAR_SECTION_ACTION_ICON_CLASSES} strokeWidth={1.8} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label={`${t("common.tags")}: ${t("common.tree-mode")}`}
|
||||
aria-pressed={treeMode}
|
||||
className={cn(SIDEBAR_SECTION_ACTION_BUTTON_CLASSES, treeMode && SIDEBAR_SECTION_ACTION_ACTIVE_CLASSES)}
|
||||
onClick={() => setTreeMode(true)}
|
||||
>
|
||||
<ListTreeIcon className={SIDEBAR_SECTION_ACTION_ICON_CLASSES} strokeWidth={1.8} />
|
||||
</Button>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span className="inline-flex" />}>
|
||||
<DropdownMenuTrigger
|
||||
aria-label={`${t("common.tags")}: ${t("common.more")}`}
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className={cn(
|
||||
SIDEBAR_SECTION_ACTION_BUTTON_CLASSES,
|
||||
"data-popup-open:bg-sidebar-accent data-popup-open:text-foreground",
|
||||
)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MoreHorizontalIcon className={SIDEBAR_SECTION_ACTION_ICON_CLASSES} strokeWidth={1.8} />
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">{t("common.more")}</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent align="end" sideOffset={4} size="sm" className="w-44">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="text-muted-foreground">{t("common.tags")}</DropdownMenuLabel>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={treeMode}
|
||||
onCheckedChange={setTreeMode}
|
||||
closeOnClick
|
||||
className="ps-2 pe-7 [&>span]:start-auto [&>span]:end-2"
|
||||
>
|
||||
<ListTreeIcon className="text-muted-foreground" strokeWidth={1.8} />
|
||||
{t("common.tree-mode")}
|
||||
</DropdownMenuCheckboxItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
}
|
||||
>
|
||||
{treeMode ? (
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@
|
|||
"tags": "Tags",
|
||||
"title": "Title",
|
||||
"today": "Today",
|
||||
"tree-mode": "Tree mode",
|
||||
"tree-mode": "Show as tree",
|
||||
"type": "Type",
|
||||
"unpin": "Unpin",
|
||||
"update": "Update",
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@
|
|||
"tasks": "Tasks",
|
||||
"title": "Title",
|
||||
"today": "Today",
|
||||
"tree-mode": "Tree mode",
|
||||
"tree-mode": "Show as tree",
|
||||
"type": "Type",
|
||||
"unlink": "Unlink",
|
||||
"unpin": "Unpin",
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@
|
|||
"tasks": "任务",
|
||||
"title": "标题",
|
||||
"today": "今天",
|
||||
"tree-mode": "树模式",
|
||||
"tree-mode": "树形显示",
|
||||
"type": "类型",
|
||||
"unlink": "解绑",
|
||||
"unpin": "取消置顶",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { SIDEBAR_ROW_BOX_CLASSES, SIDEBAR_ROW_COUNT_RAIL_CLASSES, SIDEBAR_ROW_SLOT_CLASSES } from "@/components/AppSidebar/SidebarRow";
|
||||
import {
|
||||
SIDEBAR_SECTION_ACTION_ACTIVE_CLASSES,
|
||||
SIDEBAR_SECTION_ACTION_BUTTON_CLASSES,
|
||||
SIDEBAR_SECTION_ACTION_ICON_CLASSES,
|
||||
} from "@/components/AppSidebar/SidebarSection";
|
||||
import { SIDEBAR_SECTION_ACTION_BUTTON_CLASSES, SIDEBAR_SECTION_ACTION_ICON_CLASSES } from "@/components/AppSidebar/SidebarSection";
|
||||
import TagsSection from "@/components/AppSidebar/TagsSection";
|
||||
import { MemoFilterProvider } from "@/contexts/MemoFilterContext";
|
||||
|
||||
|
|
@ -29,22 +25,58 @@ describe("TagsSection", () => {
|
|||
const heading = screen.getByRole("heading", { name: "common.tags", level: 2 });
|
||||
expect(heading.parentElement).toHaveTextContent(/^common.tags$/);
|
||||
|
||||
const listButton = screen.getByRole("button", { name: "common.tags: memo.layout-list" });
|
||||
const treeButton = screen.getByRole("button", { name: "common.tags: common.tree-mode" });
|
||||
const stableActionClasses = SIDEBAR_SECTION_ACTION_BUTTON_CLASSES.split(" ").filter(
|
||||
(className) => className !== "text-muted-foreground/65",
|
||||
);
|
||||
for (const button of [listButton, treeButton]) {
|
||||
expect(button).toHaveClass(...stableActionClasses);
|
||||
expect(button.querySelector("svg")).toHaveClass(SIDEBAR_SECTION_ACTION_ICON_CLASSES);
|
||||
}
|
||||
expect(listButton).toHaveClass(...SIDEBAR_SECTION_ACTION_ACTIVE_CLASSES.split(" "));
|
||||
expect(treeButton).toHaveClass("text-muted-foreground/65");
|
||||
const trigger = screen.getByRole("button", { name: "common.tags: common.more" });
|
||||
expect(trigger).toHaveClass(...SIDEBAR_SECTION_ACTION_BUTTON_CLASSES.split(" "));
|
||||
expect(trigger.querySelector("svg")).toHaveClass(SIDEBAR_SECTION_ACTION_ICON_CLASSES);
|
||||
expect(screen.queryByRole("button", { name: "common.tags: memo.layout-list" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("menuitemcheckbox")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(treeButton);
|
||||
expect(treeButton).toHaveClass(...SIDEBAR_SECTION_ACTION_ACTIVE_CLASSES.split(" "));
|
||||
expect(listButton).not.toHaveClass(...SIDEBAR_SECTION_ACTION_ACTIVE_CLASSES.split(" "));
|
||||
expect(listButton).toHaveClass("text-muted-foreground/65");
|
||||
it("switches layouts from the menu, closes it, and persists the preference", async () => {
|
||||
const onSelect = vi.fn();
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MemoFilterProvider>
|
||||
<TagsSection tagCount={{ a: 2, "a/b": 1 }} scope="home" onSelect={onSelect} />
|
||||
</MemoFilterProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
const trigger = screen.getByRole("button", { name: "common.tags: common.more" });
|
||||
fireEvent.click(trigger);
|
||||
const treeMode = await screen.findByRole("menuitemcheckbox", { name: "common.tree-mode" });
|
||||
expect(treeMode).toHaveAttribute("aria-checked", "false");
|
||||
fireEvent.click(treeMode);
|
||||
|
||||
expect(screen.getByRole("tree")).toBeInTheDocument();
|
||||
expect(localStorage.getItem("tag-view-as-tree")).toBe("true");
|
||||
await waitFor(() => expect(screen.queryByRole("menuitemcheckbox")).not.toBeInTheDocument());
|
||||
await waitFor(() => expect(trigger).toHaveFocus());
|
||||
|
||||
fireEvent.click(trigger);
|
||||
const checkedTreeMode = await screen.findByRole("menuitemcheckbox", { name: "common.tree-mode" });
|
||||
expect(checkedTreeMode).toHaveAttribute("aria-checked", "true");
|
||||
fireEvent.click(checkedTreeMode);
|
||||
|
||||
expect(screen.queryByRole("tree")).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "#a/b, setting.tags.used-count" })).toBeInTheDocument();
|
||||
expect(localStorage.getItem("tag-view-as-tree")).toBe("false");
|
||||
expect(onSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reflects an existing tree preference when opening the menu", async () => {
|
||||
localStorage.setItem("tag-view-as-tree", "true");
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MemoFilterProvider>
|
||||
<TagsSection tagCount={{ a: 2, "a/b": 1 }} scope="home" />
|
||||
</MemoFilterProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("tree")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.tags: common.more" }));
|
||||
expect(await screen.findByRole("menuitemcheckbox", { name: "common.tree-mode" })).toHaveAttribute("aria-checked", "true");
|
||||
});
|
||||
|
||||
it("keeps flat rows on the shared row grammar with a trailing count rail", () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue