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() {
>
{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 ? (