fix(ui): refine compact navigation and memo actions

This commit is contained in:
johnnyjoygh 2026-09-02 22:22:13 +08:00
parent 5a65350400
commit ce0dd23d2f
10 changed files with 194 additions and 31 deletions

View file

@ -437,7 +437,7 @@ const GlobalNavigation = () => {
return ( return (
<TooltipProvider> <TooltipProvider>
<nav className={cn("flex h-9 items-center gap-1", SIDEBAR_RAIL_CLASSES)} aria-label="Primary"> <nav className={cn("flex h-8 items-center gap-1", SIDEBAR_RAIL_CLASSES)} aria-label="Primary">
{currentUser && ( {currentUser && (
<DropdownMenu <DropdownMenu
onOpenChange={(open, eventDetails) => { onOpenChange={(open, eventDetails) => {
@ -505,7 +505,7 @@ const GlobalNavigation = () => {
<span <span
data-sidebar-trailing data-sidebar-trailing
className={cn( className={cn(
"absolute -end-0.5 -top-0.5 flex min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[9px] font-semibold leading-4 text-primary-foreground transition-[opacity,scale] duration-200 ease-out motion-reduce:transition-none", "absolute -end-0.5 top-0 flex min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[9px] font-semibold leading-4 text-primary-foreground transition-[opacity,scale] duration-200 ease-out motion-reduce:transition-none",
item.count > 0 ? "scale-100 opacity-100" : "scale-50 opacity-0", item.count > 0 ? "scale-100 opacity-100" : "scale-50 opacity-0",
)} )}
> >

View file

@ -1,4 +1,4 @@
import { CheckIcon, ChevronDownIcon, LoaderCircleIcon, type LucideIcon, PlusIcon } from "lucide-react"; import { CheckIcon, ChevronsUpDownIcon, LoaderCircleIcon, type LucideIcon, PlusIcon } from "lucide-react";
import { type ReactNode, useRef, useState } from "react"; import { type ReactNode, useRef, useState } from "react";
import CreateSpaceDialog from "@/components/CreateSpaceDialog"; import CreateSpaceDialog from "@/components/CreateSpaceDialog";
import MemosLogo from "@/components/MemosLogo"; import MemosLogo from "@/components/MemosLogo";
@ -92,7 +92,7 @@ function SpaceSwitcher({ className, size = "md" }: { className?: string; size?:
aria-label={`${t("space.switch")}: ${currentContextLabel}`} aria-label={`${t("space.switch")}: ${currentContextLabel}`}
title={currentContextLabel} title={currentContextLabel}
className={cn( className={cn(
"group text-start transition-colors hover:bg-sidebar-accent/65 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/40", "text-start transition-colors hover:bg-sidebar-accent/65 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/40",
size === "header" ? sidebarSurfaceVariants({ role: "headerBrand" }) : sidebarSurfaceVariants({ role: "mobileBrand" }), size === "header" ? sidebarSurfaceVariants({ role: "headerBrand" }) : sidebarSurfaceVariants({ role: "mobileBrand" }),
className, className,
)} )}
@ -127,10 +127,7 @@ function SpaceSwitcher({ className, size = "md" }: { className?: string; size?:
<MemosLogo compact size={brandSize} /> <MemosLogo compact size={brandSize} />
)} )}
</span> </span>
<ChevronDownIcon <ChevronsUpDownIcon aria-hidden="true" className="size-3.5 shrink-0 text-muted-foreground/70" strokeWidth={1.8} />
className="size-3.5 shrink-0 text-muted-foreground/70 transition-transform duration-150 group-data-[popup-open]:rotate-180 motion-reduce:transition-none"
strokeWidth={1.8}
/>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
size="sm" size="sm"

View file

@ -25,6 +25,7 @@ import {
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import { State } from "@/types/proto/api/v1/common_pb"; import { State } from "@/types/proto/api/v1/common_pb";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
import { useMemoActionHandlers } from "./hooks"; import { useMemoActionHandlers } from "./hooks";
@ -63,8 +64,20 @@ const MemoActionMenu = (props: MemoActionMenuProps) => {
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger render={<Button variant="ghost" size="icon" className="size-4" />}> <DropdownMenuTrigger
<MoreVerticalIcon className="text-muted-foreground" /> render={
<Button
variant="ghost"
size="icon-sm"
aria-label={t("common.more")}
className={cn(
"text-muted-foreground/70 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/50 data-popup-open:bg-accent data-popup-open:text-accent-foreground",
props.className,
)}
/>
}
>
<MoreVerticalIcon className="size-4" strokeWidth={1.8} />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={2}> <DropdownMenuContent align="end" sideOffset={2}>
{/* Edit actions (non-readonly, non-archived) */} {/* Edit actions (non-readonly, non-archived) */}

View file

@ -4,6 +4,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
import { useInstance } from "@/contexts/InstanceContext"; import { useInstance } from "@/contexts/InstanceContext";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { Memo } from "@/types/proto/api/v1/memo_service_pb"; import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
import { useTranslate } from "@/utils/i18n";
import { useReactionActions } from "./hooks"; import { useReactionActions } from "./hooks";
interface Props { interface Props {
@ -14,6 +15,7 @@ interface Props {
const ReactionSelector = (props: Props) => { const ReactionSelector = (props: Props) => {
const { memo, className, onOpenChange } = props; const { memo, className, onOpenChange } = props;
const t = useTranslate();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { memoRelatedSetting } = useInstance(); const { memoRelatedSetting } = useInstance();
@ -30,17 +32,18 @@ const ReactionSelector = (props: Props) => {
return ( return (
<Popover open={open} onOpenChange={handleOpenChange}> <Popover open={open} onOpenChange={handleOpenChange}>
<PopoverTrigger <PopoverTrigger
nativeButton={false}
render={ render={
<span <button
type="button"
aria-label={t("setting.memo.add-reaction")}
className={cn( className={cn(
"h-7 w-7 flex justify-center items-center rounded-full border cursor-pointer transition-all hover:opacity-80", "flex size-7 cursor-pointer items-center justify-center rounded-full border text-muted-foreground transition-all hover:opacity-80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
className, className,
)} )}
/> />
} }
> >
<SmilePlusIcon className="w-4 h-4 mx-auto text-muted-foreground" /> <SmilePlusIcon className="mx-auto size-4" />
</PopoverTrigger> </PopoverTrigger>
<PopoverContent align="center" className="max-w-[90vw] sm:max-w-md"> <PopoverContent align="center" className="max-w-[90vw] sm:max-w-md">
<div className="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-1 max-h-64 overflow-y-auto"> <div className="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-1 max-h-64 overflow-y-auto">

View file

@ -2,6 +2,7 @@ import { BookmarkIcon } from "lucide-react";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import RelativeTime from "@/components/RelativeTime"; import RelativeTime from "@/components/RelativeTime";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { useNewMemo } from "@/contexts/NewMemoContext"; import { useNewMemo } from "@/contexts/NewMemoContext";
import useNavigateTo from "@/hooks/useNavigateTo"; import useNavigateTo from "@/hooks/useNavigateTo";
@ -21,6 +22,9 @@ import { createMemoNavigationState } from "../navigation";
import type { MemoHeaderProps } from "../types"; import type { MemoHeaderProps } from "../types";
import MemoSpaceBadge from "./MemoSpaceBadge"; import MemoSpaceBadge from "./MemoSpaceBadge";
const MEMO_HEADER_ACTION_CLASSES =
"size-6 shrink-0 rounded-md border-none bg-transparent text-muted-foreground transition-colors hover:bg-accent hover:text-foreground hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/50 data-popup-open:bg-accent data-popup-open:text-foreground";
const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, showPinned, showSpace }) => { const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, showPinned, showSpace }) => {
const t = useTranslate(); const t = useTranslate();
const [reactionSelectorOpen, setReactionSelectorOpen] = useState(false); const [reactionSelectorOpen, setReactionSelectorOpen] = useState(false);
@ -82,10 +86,14 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, sh
)} )}
</div> </div>
<div className="flex flex-row justify-end items-center select-none shrink-0 gap-2"> <div data-slot="memo-header-actions" className="flex shrink-0 select-none flex-row items-center justify-end gap-1">
{currentUser && !isArchived && ( {currentUser && !isArchived && (
<ReactionSelector <ReactionSelector
className={cn("border-none w-auto h-auto", reactionSelectorOpen && "block!", "block sm:hidden sm:group-hover:block")} className={cn(
MEMO_HEADER_ACTION_CLASSES,
reactionSelectorOpen && "sm:flex!",
"flex sm:hidden sm:group-hover:flex sm:group-focus-within:flex",
)}
memo={memo} memo={memo}
onOpenChange={setReactionSelectorOpen} onOpenChange={setReactionSelectorOpen}
/> />
@ -93,10 +101,8 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, sh
{showVisibility && memo.visibility !== Visibility.PRIVATE && ( {showVisibility && memo.visibility !== Visibility.PRIVATE && (
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger aria-label={visibilityOption && t(visibilityOption.labelKey)} className={MEMO_HEADER_ACTION_CLASSES}>
<span className="flex justify-center items-center rounded-md hover:opacity-80"> <VisibilityIcon visibility={memo.visibility} className="text-current" />
<VisibilityIcon visibility={memo.visibility} />
</span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>{visibilityOption && t(visibilityOption.labelKey)}</TooltipContent> <TooltipContent>{visibilityOption && t(visibilityOption.labelKey)}</TooltipContent>
</Tooltip> </Tooltip>
@ -105,8 +111,18 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, sh
{showPinned && memo.pinned && ( {showPinned && memo.pinned && (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger render={<span className="cursor-pointer" />}> <TooltipTrigger
<BookmarkIcon className="w-4 h-auto text-primary" onClick={unpinMemo} /> render={
<Button
variant="ghost"
size="icon-sm"
aria-label={t("common.unpin")}
className={cn(MEMO_HEADER_ACTION_CLASSES, "text-primary hover:text-primary data-popup-open:text-primary")}
onClick={unpinMemo}
/>
}
>
<BookmarkIcon className="size-4" strokeWidth={1.8} />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>{t("common.unpin")}</p> <p>{t("common.unpin")}</p>
@ -115,7 +131,13 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, sh
</TooltipProvider> </TooltipProvider>
)} )}
<MemoActionMenu memo={memo} parentScope={parentScope} readonly={readonly} onEdit={openEditor} /> <MemoActionMenu
memo={memo}
parentScope={parentScope}
readonly={readonly}
className={MEMO_HEADER_ACTION_CLASSES}
onEdit={openEditor}
/>
</div> </div>
</div> </div>
); );

View file

@ -222,7 +222,8 @@ describe("App sidebar logo", () => {
expect(switcher).not.toHaveClass("px-1"); expect(switcher).not.toHaveClass("px-1");
expect(switcher.firstElementChild).not.toHaveClass("flex-1"); expect(switcher.firstElementChild).not.toHaveClass("flex-1");
expect(within(switcher).getByText("Memos logo")).toHaveAttribute("data-logo-size", "header"); expect(within(switcher).getByText("Memos logo")).toHaveAttribute("data-logo-size", "header");
expect(switcher.querySelector(".lucide-chevron-down")).not.toBeNull(); expect(switcher.querySelector(".lucide-chevrons-up-down")).not.toBeNull();
expect(switcher.querySelector(".lucide-chevron-down")).toBeNull();
expect(compose).toHaveClass("size-7", "rounded-md", "border", "bg-background", "shadow-xs"); expect(compose).toHaveClass("size-7", "rounded-md", "border", "bg-background", "shadow-xs");
expect(compose).not.toHaveClass("rounded-full"); expect(compose).not.toHaveClass("rounded-full");
@ -398,7 +399,7 @@ describe("App sidebar logo", () => {
expect(screen.getByRole("link", { name: "Memos logo" })).toHaveAttribute("href", "/explore"); expect(screen.getByRole("link", { name: "Memos logo" })).toHaveAttribute("href", "/explore");
expect(screen.queryByRole("button", { name: /^space\.switch:/ })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: /^space\.switch:/ })).not.toBeInTheDocument();
const primaryNavigation = screen.getByRole("navigation", { name: "Primary" }); const primaryNavigation = screen.getByRole("navigation", { name: "Primary" });
expect(primaryNavigation).toHaveClass("h-9", "items-center", "gap-1", "px-3"); expect(primaryNavigation).toHaveClass("h-8", "items-center", "gap-1", "px-3");
expect(primaryNavigation).not.toHaveClass("flex-col"); expect(primaryNavigation).not.toHaveClass("flex-col");
const navigation = within(primaryNavigation); const navigation = within(primaryNavigation);
expectActiveNavPill(navigation.getByRole("link", { name: "common.explore" }), "common.explore"); expectActiveNavPill(navigation.getByRole("link", { name: "common.explore" }), "common.explore");
@ -523,6 +524,7 @@ describe("App sidebar logo", () => {
const scopeTrigger = screen.getByRole("button", { name: "common.home" }); const scopeTrigger = screen.getByRole("button", { name: "common.home" });
expectActiveNavPill(scopeTrigger, "common.home"); expectActiveNavPill(scopeTrigger, "common.home");
expect(scopeTrigger.querySelector(".lucide-chevron-down")).toBeInTheDocument(); expect(scopeTrigger.querySelector(".lucide-chevron-down")).toBeInTheDocument();
expect(scopeTrigger.querySelector(".lucide-chevrons-up-down")).not.toBeInTheDocument();
expectCollapsedNavPill(screen.getByRole("link", { name: "common.attachments" }), "common.attachments"); expectCollapsedNavPill(screen.getByRole("link", { name: "common.attachments" }), "common.attachments");
fireEvent.click(scopeTrigger); fireEvent.click(scopeTrigger);
expect(await screen.findByRole("menuitem", { name: "common.home" })).toBeInTheDocument(); expect(await screen.findByRole("menuitem", { name: "common.home" })).toBeInTheDocument();

View file

@ -0,0 +1,49 @@
import { create } from "@bufbuild/protobuf";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import MemoActionMenu from "@/components/MemoActionMenu";
import { State } from "@/types/proto/api/v1/common_pb";
import { MemoSchema } from "@/types/proto/api/v1/memo_service_pb";
const handlers = vi.hoisted(() => ({
handleTogglePinMemoBtnClick: vi.fn(),
handleEditMemoClick: vi.fn(),
handleToggleMemoStatusClick: vi.fn(),
handleCopyLink: vi.fn(),
handleCopyContent: vi.fn(),
handleCheckAllTaskListItemsClick: vi.fn(),
handleUncheckAllTaskListItemsClick: vi.fn(),
handleDeleteMemoClick: vi.fn(),
confirmDeleteMemo: vi.fn(),
}));
vi.mock("@/components/ConfirmDialog", () => ({
default: () => null,
}));
vi.mock("@/components/MemoActionMenu/hooks", () => ({
useMemoActionHandlers: () => handlers,
}));
vi.mock("@/utils/i18n", () => ({
useTranslate: () => (key: string) => key,
}));
describe("MemoActionMenu", () => {
it("uses the standard compact action surface and preserves its open state", async () => {
const memo = create(MemoSchema, { name: "memos/1", state: State.NORMAL, pinned: false });
render(<MemoActionMenu memo={memo} parentScope="preserve" />);
const trigger = screen.getByRole("button", { name: "common.more" });
expect(trigger).toHaveClass("size-6", "rounded-md", "hover:bg-accent", "focus-visible:ring-2", "data-popup-open:bg-accent");
expect(trigger).toHaveAttribute("aria-haspopup", "menu");
expect(trigger).toHaveAttribute("aria-expanded", "false");
expect(trigger.querySelector(".lucide-ellipsis-vertical")).toHaveClass("size-4");
fireEvent.click(trigger);
await waitFor(() => expect(trigger).toHaveAttribute("aria-expanded", "true"));
expect(trigger).toHaveAttribute("data-popup-open");
expect(await screen.findByRole("menuitem", { name: "common.pin" })).toBeInTheDocument();
});
});

View file

@ -5,6 +5,7 @@ import MemoHeader from "@/components/MemoView/components/MemoHeader";
const state = vi.hoisted(() => ({ const state = vi.hoisted(() => ({
creator: undefined as { username: string; displayName: string; avatarUrl: string } | undefined, creator: undefined as { username: string; displayName: string; avatarUrl: string } | undefined,
currentUser: undefined as { name: string } | undefined,
})); }));
vi.mock("@/components/RelativeTime", () => ({ vi.mock("@/components/RelativeTime", () => ({
@ -12,11 +13,19 @@ vi.mock("@/components/RelativeTime", () => ({
})); }));
vi.mock("@/components/MemoActionMenu", () => ({ vi.mock("@/components/MemoActionMenu", () => ({
default: () => null, default: ({ className }: { className?: string }) => (
<button type="button" aria-label="memo-actions" className={className}>
Memo actions
</button>
),
})); }));
vi.mock("@/components/MemoReactionListView", () => ({ vi.mock("@/components/MemoReactionListView", () => ({
ReactionSelector: () => null, ReactionSelector: ({ className }: { className?: string }) => (
<button type="button" aria-label="add-reaction" className={className}>
Add reaction
</button>
),
})); }));
vi.mock("@/components/UserAvatar", () => ({ vi.mock("@/components/UserAvatar", () => ({
@ -50,7 +59,7 @@ vi.mock("@/components/MemoView/MemoViewContext", () => ({
useMemoViewContext: () => ({ useMemoViewContext: () => ({
memo: { name: "memos/123", visibility: 1, pinned: false, space: "spaces/product" }, memo: { name: "memos/123", visibility: 1, pinned: false, space: "spaces/product" },
creator: state.creator, creator: state.creator,
currentUser: undefined, currentUser: state.currentUser,
parentPage: "/explore?filter=tagSearch%3Awork", parentPage: "/explore?filter=tagSearch%3Awork",
parentScope: "preserve", parentScope: "preserve",
isArchived: false, isArchived: false,
@ -82,6 +91,34 @@ const LocationProbe = () => {
describe("MemoHeader navigation", () => { describe("MemoHeader navigation", () => {
beforeEach(() => { beforeEach(() => {
state.creator = undefined; state.creator = undefined;
state.currentUser = undefined;
});
it("uses one compact interaction surface for memo header actions", () => {
state.currentUser = { name: "users/alice" };
render(
<MemoryRouter>
<MemoHeader />
</MemoryRouter>,
);
const reaction = screen.getByRole("button", { name: "add-reaction" });
const actions = screen.getByRole("button", { name: "memo-actions" });
const actionRail = actions.closest('[data-slot="memo-header-actions"]');
expect(actionRail).toHaveClass("items-center", "gap-1");
expect(reaction).toHaveClass("sm:group-focus-within:flex");
for (const action of [reaction, actions]) {
expect(action).toHaveClass(
"size-6",
"rounded-md",
"hover:bg-accent",
"hover:text-foreground",
"focus-visible:ring-2",
"data-popup-open:bg-accent",
);
}
}); });
it.each([false, true])("uses a keyboard-operable timestamp and preserves origin when showCreator=%s", (showCreator) => { it.each([false, true])("uses a keyboard-operable timestamp and preserves origin when showCreator=%s", (showCreator) => {

View file

@ -0,0 +1,36 @@
import { create } from "@bufbuild/protobuf";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import ReactionSelector from "@/components/MemoReactionListView/ReactionSelector";
import { MemoSchema } from "@/types/proto/api/v1/memo_service_pb";
vi.mock("@/components/MemoReactionListView/hooks", () => ({
useReactionActions: () => ({
hasReacted: () => false,
handleReactionClick: vi.fn(),
}),
}));
vi.mock("@/contexts/InstanceContext", () => ({
useInstance: () => ({ memoRelatedSetting: { reactions: ["👍"] } }),
}));
vi.mock("@/utils/i18n", () => ({
useTranslate: () => (key: string) => key,
}));
describe("ReactionSelector", () => {
it("uses a named native trigger with a visible focus state", async () => {
const memo = create(MemoSchema, { name: "memos/1", reactions: [] });
render(<ReactionSelector memo={memo} />);
const trigger = screen.getByRole("button", { name: "setting.memo.add-reaction" });
expect(trigger).toHaveClass("size-7", "rounded-full", "focus-visible:ring-2");
expect(trigger).toHaveAttribute("aria-expanded", "false");
fireEvent.click(trigger);
await waitFor(() => expect(trigger).toHaveAttribute("aria-expanded", "true"));
expect(screen.getByRole("button", { name: "👍" })).toBeInTheDocument();
});
});

View file

@ -64,7 +64,11 @@ describe("SpaceSwitcher", () => {
it("lists Memos, every available Space, and the create entry", async () => { it("lists Memos, every available Space, and the create entry", async () => {
render(<SpaceSwitcher />); render(<SpaceSwitcher />);
fireEvent.click(screen.getByRole("button", { name: "space.switch: common.memos" })); const trigger = screen.getByRole("button", { name: "space.switch: common.memos" });
expect(trigger).toHaveAttribute("aria-haspopup", "menu");
expect(trigger).toHaveAttribute("aria-expanded", "false");
fireEvent.click(trigger);
expect(trigger).toHaveAttribute("aria-expanded", "true");
expect(await screen.findByRole("menuitemradio", { name: "Memos" })).toHaveAttribute("aria-checked", "true"); expect(await screen.findByRole("menuitemradio", { name: "Memos" })).toHaveAttribute("aria-checked", "true");
const productRow = screen.getByRole("menuitemradio", { name: "Product" }); const productRow = screen.getByRole("menuitemradio", { name: "Product" });
@ -157,8 +161,8 @@ describe("SpaceSwitcher", () => {
expect(trigger).not.toHaveClass("px-1"); expect(trigger).not.toHaveClass("px-1");
expect(title).toHaveClass("text-[14px]", "font-semibold", "leading-5"); expect(title).toHaveClass("text-[14px]", "font-semibold", "leading-5");
expect(mark).toHaveClass("size-5", "rounded-[5px]"); expect(mark).toHaveClass("size-5", "rounded-[5px]");
expect(trigger.querySelector(".lucide-chevron-down")).not.toBeNull(); expect(trigger.querySelector(".lucide-chevrons-up-down")).not.toBeNull();
expect(trigger.querySelector(".lucide-chevrons-up-down")).toBeNull(); expect(trigger.querySelector(".lucide-chevron-down")).toBeNull();
}); });
it("keeps duplicate identity in the header label but out of its geometry", () => { it("keeps duplicate identity in the header label but out of its geometry", () => {