抽取项目页浮层菜单组件
新增 PlatformFloatingMenu 与菜单项测试 项目卡片更多菜单复用平台浮层菜单 更新 TRACKING 记录组件收口验证
This commit is contained in:
31
src/components/common/PlatformFloatingMenu.test.tsx
Normal file
31
src/components/common/PlatformFloatingMenu.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
54
src/components/common/PlatformFloatingMenu.tsx
Normal file
54
src/components/common/PlatformFloatingMenu.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user