fix(web): refine memo menu interactions
This commit is contained in:
1 parent
e96cf62d68
commit
1e36175f29
8 files changed
+70
-16
No files matched your search
@@ -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) => (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in new issue
Block a user