抽取项目页浮层菜单组件

新增 PlatformFloatingMenu 与菜单项测试

项目卡片更多菜单复用平台浮层菜单

更新 TRACKING 记录组件收口验证
This commit is contained in:
2026-06-14 00:50:05 +08:00
parent 8b4175dc7d
commit 304d6806f0
5 changed files with 103 additions and 17 deletions

View File

@@ -0,0 +1,31 @@
/* @vitest-environment jsdom */
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';
import {
PlatformFloatingMenu,
PlatformFloatingMenuItem,
} from './PlatformFloatingMenu';
describe('PlatformFloatingMenu', () => {
it('renders menu items with accessible menu semantics', async () => {
const onRename = vi.fn();
const user = userEvent.setup();
render(
<PlatformFloatingMenu>
<PlatformFloatingMenuItem onClick={onRename}>
</PlatformFloatingMenuItem>
</PlatformFloatingMenu>,
);
expect(screen.getByRole('menu')).toBeTruthy();
await user.click(screen.getByRole('menuitem', { name: '重命名' }));
expect(onRename).toHaveBeenCalledOnce();
});
});

View File

@@ -0,0 +1,54 @@
import type { ButtonHTMLAttributes, ReactNode } from 'react';
type PlatformFloatingMenuProps = {
children: ReactNode;
className?: string;
};
type PlatformFloatingMenuItemProps = Omit<
ButtonHTMLAttributes<HTMLButtonElement>,
'children'
> & {
icon?: ReactNode;
children: ReactNode;
};
/**
* 平台浮层菜单原语。
* 用于卡片角标、画布局部菜单等轻量动作集合,统一 role 与菜单项 chrome。
*/
export function PlatformFloatingMenu({
children,
className,
}: PlatformFloatingMenuProps) {
return (
<div
className={['platform-floating-menu', className].filter(Boolean).join(' ')}
role="menu"
>
{children}
</div>
);
}
export function PlatformFloatingMenuItem({
icon,
children,
className,
type = 'button',
...buttonProps
}: PlatformFloatingMenuItemProps) {
return (
<button
{...buttonProps}
type={type}
className={['platform-floating-menu__item', className]
.filter(Boolean)
.join(' ')}
role="menuitem"
>
{icon}
<span>{children}</span>
</button>
);
}

View File

@@ -18,6 +18,10 @@ import {
} from '../../services/image-editor/editorProjectClient';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformEmptyState } from '../common/PlatformEmptyState';
import {
PlatformFloatingMenu,
PlatformFloatingMenuItem,
} from '../common/PlatformFloatingMenu';
import { PlatformIconButton } from '../common/PlatformIconButton';
import { PlatformTextField } from '../common/PlatformTextField';
import { UnifiedModal } from '../common/UnifiedModal';
@@ -242,10 +246,9 @@ export function ProjectGalleryView({ onOpenProject }: ProjectGalleryViewProps) {
}}
/>
{activeMenuProjectId === project.projectId ? (
<div className="project-gallery__card-menu" role="menu">
<button
type="button"
role="menuitem"
<PlatformFloatingMenu>
<PlatformFloatingMenuItem
icon={<Pencil className="h-4 w-4" />}
onClick={() =>
setRenameDraft({
projectId: project.projectId,
@@ -253,18 +256,15 @@ export function ProjectGalleryView({ onOpenProject }: ProjectGalleryViewProps) {
})
}
>
<Pencil className="h-4 w-4" />
<span></span>
</button>
<button
type="button"
role="menuitem"
</PlatformFloatingMenuItem>
<PlatformFloatingMenuItem
icon={<Trash2 className="h-4 w-4" />}
onClick={() => deleteProjects([project.projectId])}
>
<Trash2 className="h-4 w-4" />
<span></span>
</button>
</div>
</PlatformFloatingMenuItem>
</PlatformFloatingMenu>
) : null}
</div>
) : null}