diff --git a/TRACKING.md b/TRACKING.md index 6f5024a9..05e2854f 100644 --- a/TRACKING.md +++ b/TRACKING.md @@ -98,3 +98,4 @@ - 2026-06-14 组件复用修正:编辑器 `EditorIconButton` 改为委托 `PlatformIconButton` 的薄包装,保留编辑器局部 class 与调用方式,但不再维护重复的原生图标按钮可访问性和基础 chrome;验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorPrimitives.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/common/PlatformIconButton.test.tsx`、`npm run typecheck`。 - 2026-06-14 组件复用修正:编辑器生成输入框的提交按钮改为复用 `PlatformActionButton`,仅保留生成器局部尺寸和 Lovart 式浅灰覆盖,不再直接维护原生 submit 按钮基础 chrome;验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/common/PlatformActionButton.test.tsx`、`npm run typecheck`。 - 2026-06-14 组件复用修正:编辑器生成输入框的“参考图”按钮改为复用 `PlatformIconButton variant="surfaceFloating"`,用 children 承载短标签,仅保留生成器局部尺寸和浅灰覆盖;验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/common/PlatformIconButton.test.tsx`、`npm run typecheck`。 +- 2026-06-14 组件复用修正:编辑器生成图右上角元数据角标改为复用 `PlatformIconButton asChild="spanButton"`,保留嵌套在图层按钮内的合法 DOM 结构,同时把 Enter / Space 键盘触发和 `darkMini` chrome 收口到共享组件;验证命令:`npm run test -- src/components/common/PlatformIconButton.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`。 diff --git a/src/components/common/PlatformIconButton.test.tsx b/src/components/common/PlatformIconButton.test.tsx index 1533c47b..29038101 100644 --- a/src/components/common/PlatformIconButton.test.tsx +++ b/src/components/common/PlatformIconButton.test.tsx @@ -1,7 +1,7 @@ /* @vitest-environment jsdom */ -import { render, screen } from '@testing-library/react'; -import { expect, test } from 'vitest'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { expect, test, vi } from 'vitest'; import { PlatformIconButton } from './PlatformIconButton'; @@ -95,6 +95,34 @@ test('supports dark mini icon action chrome', () => { expect(button.textContent).toBe('?'); }); +test('supports span button child chrome for nested action surfaces', () => { + const onClick = vi.fn(); + render( + , + ); + + const action = screen.getByRole('button', { name: '查看元数据' }); + + expect(action.tagName).toBe('SPAN'); + expect(action.className).toContain('bg-black/55'); + expect(action.className).toContain('absolute'); + expect(action.getAttribute('tabindex')).toBe('0'); + + fireEvent.keyDown(action, { key: 'Enter' }); + fireEvent.keyDown(action, { key: ' ' }); + + expect(onClick).toHaveBeenCalledTimes(2); +}); + test('supports label child chrome for icon upload controls', () => { const { container } = render( <> diff --git a/src/components/common/PlatformIconButton.tsx b/src/components/common/PlatformIconButton.tsx index b15a3a83..409f69c3 100644 --- a/src/components/common/PlatformIconButton.tsx +++ b/src/components/common/PlatformIconButton.tsx @@ -1,5 +1,7 @@ import type { ButtonHTMLAttributes, + HTMLAttributes, + KeyboardEvent, LabelHTMLAttributes, ReactNode, } from 'react'; @@ -27,9 +29,19 @@ type PlatformIconButtonLabelProps = Omit< asChild: 'label'; }; +type PlatformIconButtonSpanButtonProps = Omit< + HTMLAttributes, + 'aria-label' | 'children' | 'role' +> & + PlatformIconButtonBaseProps & { + asChild: 'spanButton'; + disabled?: boolean; + }; + type PlatformIconButtonProps = | PlatformIconButtonButtonProps - | PlatformIconButtonLabelProps; + | PlatformIconButtonLabelProps + | PlatformIconButtonSpanButtonProps; /** * 平台通用图标动作按钮。 @@ -71,6 +83,45 @@ export function PlatformIconButton({ ); } + if (asChild === 'spanButton') { + const { + disabled, + onClick, + onKeyDown, + tabIndex, + ...spanProps + } = actionProps as HTMLAttributes & { + disabled?: boolean; + }; + const handleKeyDown = (event: KeyboardEvent) => { + onKeyDown?.(event); + if (event.defaultPrevented || disabled) { + return; + } + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + event.currentTarget.click(); + } + }; + + return ( + + {icon} + {children} + + ); + } + const { type = 'button', ...buttonProps } = actionProps as ButtonHTMLAttributes; diff --git a/src/components/image-editor/ImageCanvasEditorView.test.tsx b/src/components/image-editor/ImageCanvasEditorView.test.tsx index 75f13f85..7238b5ef 100644 --- a/src/components/image-editor/ImageCanvasEditorView.test.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.test.tsx @@ -1064,6 +1064,10 @@ describe('ImageCanvasEditorView', () => { if (!metadataCornerButton) { throw new Error('metadata corner button should exist'); } + expect(metadataCornerButton.className).toContain('bg-black/55'); + expect(metadataCornerButton.className).toContain( + 'image-canvas-editor__metadata-corner', + ); fireEvent.click(metadataCornerButton); const metadataDialog = screen.getByRole('dialog', { name: /生成图片 .*元数据/ }); diff --git a/src/components/image-editor/ImageCanvasEditorView.tsx b/src/components/image-editor/ImageCanvasEditorView.tsx index 610a8aed..ced09770 100644 --- a/src/components/image-editor/ImageCanvasEditorView.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.tsx @@ -2550,28 +2550,19 @@ export function ImageCanvasEditorView() { > {`画布图片:${layer.title}`} {isGeneratedLayer(layer) ? ( - } onClick={(event) => { event.stopPropagation(); setMetadataLayer(layer); selectSingleLayer(layer.id); }} onPointerDown={(event) => event.stopPropagation()} - onKeyDown={(event) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - event.stopPropagation(); - setMetadataLayer(layer); - selectSingleLayer(layer.id); - } - }} - > - - + /> ) : null} {isHovered ? (