fix(web): refine memo menu interactions

This commit is contained in:
tianma-if committed 2026-08-12 18:21:43 +08:00
1 parent e96cf62d68
commit 1e36175f29
8 files changed
+70 -16

No files matched your search

+1 -1
View File
@@ -3814,7 +3814,7 @@ const RichEditorPane = ({
onValueChange={(value) => handleNotebookChange(value)}
>
<SelectTrigger className="h-8 min-w-0 border-transparent bg-transparent px-2 text-sm font-medium text-slate-600 hover:border-slate-200 hover:bg-slate-50 hover:text-slate-900 whitespace-nowrap">
<SelectValue placeholder={t("editor.notebookPlaceholder")} />
<SelectValue placeholder={t("editor.notebookPlaceholder")}>{currentNotebookLabel}</SelectValue>
</SelectTrigger>
<SelectContent className="max-h-60 bg-white border border-slate-200 rounded-md py-1 shadow-md">
{notebookOptions.map((notebook) => (
+1
View File
@@ -413,6 +413,7 @@ export const MemoCard = ({
title={t("memoCard.moreActions")}
aria-label={t("memoCard.moreActions")}
aria-haspopup="menu"
data-memo-actions-trigger
onClick={(event) => {
event.stopPropagation();
onOpenKeyboardContextMenu(event.currentTarget);
+14 -6
View File
@@ -9,6 +9,7 @@ import {
type KeyboardEvent as ReactKeyboardEvent,
type ReactNode,
} from "react";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
import * as m from "motion/react-m";
import {
@@ -140,9 +141,13 @@ export const MemoSelectionActionBar = ({
onMoveTargetChange: (notebookId: string) => void;
}) => {
const { t } = useTranslation();
const selectedMoveNotebookName = moveNotebookOptions.find((item) => item.id === moveTargetNotebookId)?.name;
return (
<div className="hidden h-full min-h-0 flex-1 items-center justify-start bg-white px-16 py-10 lg:flex lg:pl-44 xl:px-24 xl:pl-44">
<div
className="hidden h-full min-h-0 flex-1 items-start justify-start bg-white px-6 py-6 lg:flex lg:px-8 lg:py-8 xl:px-10"
data-memo-selection-action-bar
>
<m.div className="w-72 overflow-hidden rounded-md border border-slate-200 bg-white py-1 shadow-lg" {...paneEnterMotion}>
<div className="flex h-9 items-center gap-2 px-3 text-xs font-semibold text-slate-400">
<CheckSquare className="h-4 w-4" />
@@ -153,7 +158,7 @@ export const MemoSelectionActionBar = ({
<div className="flex items-center gap-2">
<Select value={moveTargetNotebookId} disabled={isMoving} onValueChange={onMoveTargetChange}>
<SelectTrigger className="h-8 min-w-0 flex-1 text-xs text-slate-700 border-slate-200">
<SelectValue placeholder={t("memoList.chooseNotebook")} />
<SelectValue placeholder={t("memoList.chooseNotebook")}>{selectedMoveNotebookName}</SelectValue>
</SelectTrigger>
<SelectContent className="max-h-60 bg-white border border-slate-200 rounded-md py-1 shadow-md">
{moveNotebookOptions.map((item) => (
@@ -1426,8 +1431,9 @@ export const MemoListPane = ({
)}
</div>
{/* Controlled Right Click context menu for single note on Desktop using absolute placement */}
{memoContextMenu && (
{/* Keep the virtual trigger in the document viewport so fixed coordinates
are not offset by the memo pane's backdrop-filter containing block. */}
{memoContextMenu && typeof document !== "undefined" ? createPortal(
<div style={{ position: "fixed", left: memoContextMenu.x, top: memoContextMenu.y, zIndex: 100 }}>
<DropdownMenu open={true} onOpenChange={(open) => { if (!open) setMemoContextMenu(null); }}>
<DropdownMenuTrigger asChild>
@@ -1436,6 +1442,7 @@ export const MemoListPane = ({
<DropdownMenuContent
align="start"
className="max-h-[calc(100dvh-1.5rem)] w-56 max-w-[calc(100vw-1.5rem)] overflow-y-auto bg-white border border-slate-200 rounded-md py-1 shadow-md"
data-memo-actions-menu
>
<DropdownMenuItem
className="flex h-9 w-full items-center gap-2 px-3 text-left text-sm text-slate-700 hover:bg-slate-50 cursor-pointer outline-none"
@@ -1596,8 +1603,9 @@ export const MemoListPane = ({
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</div>,
document.body
) : null}
{memoIdCopyNotice && (
<div
+4 -4
View File
@@ -19,7 +19,7 @@ const ContextMenuSubTrigger = React.forwardRef<
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-slate-100 focus:text-slate-900 data-[state=open]:bg-slate-100 data-[state=open]:text-slate-900",
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-[hsl(var(--accent))] focus:text-[hsl(var(--accent-foreground))] data-[state=open]:bg-[hsl(var(--accent))] data-[state=open]:text-[hsl(var(--accent-foreground))]",
inset && "pl-8",
className
)}
@@ -72,7 +72,7 @@ const ContextMenuItem = React.forwardRef<
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-[hsl(var(--accent))] focus:text-[hsl(var(--accent-foreground))] data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
@@ -88,7 +88,7 @@ const ContextMenuCheckboxItem = React.forwardRef<
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-[hsl(var(--accent))] focus:text-[hsl(var(--accent-foreground))] data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
@@ -112,7 +112,7 @@ const ContextMenuRadioItem = React.forwardRef<
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-[hsl(var(--accent))] focus:text-[hsl(var(--accent-foreground))] data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
+4 -4
View File
@@ -19,7 +19,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-slate-100 data-[state=open]:bg-slate-100",
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-[hsl(var(--accent))] focus:text-[hsl(var(--accent-foreground))] data-[state=open]:bg-[hsl(var(--accent))] data-[state=open]:text-[hsl(var(--accent-foreground))]",
inset && "pl-8",
className
)}
@@ -75,7 +75,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-[hsl(var(--accent))] focus:text-[hsl(var(--accent-foreground))] data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
@@ -91,7 +91,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-[hsl(var(--accent))] focus:text-[hsl(var(--accent-foreground))] data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
@@ -115,7 +115,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-[hsl(var(--accent))] focus:text-[hsl(var(--accent-foreground))] data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
+1 -1
View File
@@ -113,7 +113,7 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-[hsl(var(--accent))] focus:text-[hsl(var(--accent-foreground))] data-[highlighted]:bg-[hsl(var(--accent))] data-[highlighted]:text-[hsl(var(--accent-foreground))] data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
+24
View File
@@ -96,6 +96,30 @@ test.describe("dark mode visual contracts", () => {
expect(await auditVisibleTextContrast(page)).toEqual([]);
});
test("notebook move picker keeps highlighted options dark and selected labels clean", async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: "选择笔记", exact: true }).click();
const actionBar = page.locator("[data-memo-selection-action-bar]");
const actionCard = actionBar.locator(":scope > div");
await expect(actionBar).toBeVisible();
await expect.poll(async () => (await actionCard.boundingBox())?.y ?? Number.POSITIVE_INFINITY).toBeLessThan(120);
const notebookSelect = actionBar.getByRole("combobox");
await expect(notebookSelect).toContainText("功能演示");
await expect(notebookSelect).not.toContainText("└");
await notebookSelect.click();
const nestedNotebook = page.getByRole("option", { name: /功能演示/ });
await nestedNotebook.hover();
await expect.poll(() => nestedNotebook.evaluate((element) => getComputedStyle(element).backgroundColor))
.not.toBe("rgb(241, 245, 249)");
expect(await auditVisibleTextContrast(page)).toEqual([]);
await nestedNotebook.click();
await expect(notebookSelect).toHaveText("功能演示");
});
test("public share content remains readable in dark mode", async ({ page }) => {
const memoId = "memo_demo_overview";
const response = await page.request.post(`/api/v1/memos/${memoId}/share`);
+21
View File
@@ -0,0 +1,21 @@
import { expect, test } from "@playwright/test";
test("anchors a memo's more-actions menu to its visible trigger", async ({ page }) => {
await page.goto("/");
const memoCard = page.locator('[data-memo-id="memo_demo_overview"]');
const trigger = memoCard.locator("[data-memo-actions-trigger]");
await memoCard.hover();
await trigger.click();
const menu = page.locator("[data-memo-actions-menu]");
await expect(menu).toBeVisible();
const triggerBox = await trigger.boundingBox();
const menuBox = await menu.boundingBox();
expect(triggerBox).not.toBeNull();
expect(menuBox).not.toBeNull();
expect(Math.abs(menuBox!.x - (triggerBox!.x + triggerBox!.width))).toBeLessThan(24);
expect(Math.abs(menuBox!.y - (triggerBox!.y + triggerBox!.height))).toBeLessThan(24);
});