收口元数据角标图标按钮

PlatformIconButton 增加 spanButton 形态,支持嵌套动作面合法复用图标按钮 chrome。

编辑器生成图元数据角标改为复用 PlatformIconButton,移除局部键盘处理。

补充测试覆盖 spanButton 键盘触发和元数据角标平台样式,并更新 TRACKING。
This commit is contained in:
2026-06-14 14:44:04 +08:00
parent 80d3a06e29
commit 451fca4a56
5 changed files with 93 additions and 18 deletions

View File

@@ -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 组件复用修正:编辑器 `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 组件复用修正:编辑器生成输入框的提交按钮改为复用 `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 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`

View File

@@ -1,7 +1,7 @@
/* @vitest-environment jsdom */ /* @vitest-environment jsdom */
import { render, screen } from '@testing-library/react'; import { fireEvent, render, screen } from '@testing-library/react';
import { expect, test } from 'vitest'; import { expect, test, vi } from 'vitest';
import { PlatformIconButton } from './PlatformIconButton'; import { PlatformIconButton } from './PlatformIconButton';
@@ -95,6 +95,34 @@ test('supports dark mini icon action chrome', () => {
expect(button.textContent).toBe('?'); expect(button.textContent).toBe('?');
}); });
test('supports span button child chrome for nested action surfaces', () => {
const onClick = vi.fn();
render(
<button type="button">
<PlatformIconButton
asChild="spanButton"
label="查看元数据"
variant="darkMini"
icon={<span aria-hidden="true">{'{}'}</span>}
className="absolute"
onClick={onClick}
/>
</button>,
);
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', () => { test('supports label child chrome for icon upload controls', () => {
const { container } = render( const { container } = render(
<> <>

View File

@@ -1,5 +1,7 @@
import type { import type {
ButtonHTMLAttributes, ButtonHTMLAttributes,
HTMLAttributes,
KeyboardEvent,
LabelHTMLAttributes, LabelHTMLAttributes,
ReactNode, ReactNode,
} from 'react'; } from 'react';
@@ -27,9 +29,19 @@ type PlatformIconButtonLabelProps = Omit<
asChild: 'label'; asChild: 'label';
}; };
type PlatformIconButtonSpanButtonProps = Omit<
HTMLAttributes<HTMLSpanElement>,
'aria-label' | 'children' | 'role'
> &
PlatformIconButtonBaseProps & {
asChild: 'spanButton';
disabled?: boolean;
};
type PlatformIconButtonProps = type PlatformIconButtonProps =
| PlatformIconButtonButtonProps | PlatformIconButtonButtonProps
| PlatformIconButtonLabelProps; | PlatformIconButtonLabelProps
| PlatformIconButtonSpanButtonProps;
/** /**
* 平台通用图标动作按钮。 * 平台通用图标动作按钮。
@@ -71,6 +83,45 @@ export function PlatformIconButton({
); );
} }
if (asChild === 'spanButton') {
const {
disabled,
onClick,
onKeyDown,
tabIndex,
...spanProps
} = actionProps as HTMLAttributes<HTMLSpanElement> & {
disabled?: boolean;
};
const handleKeyDown = (event: KeyboardEvent<HTMLSpanElement>) => {
onKeyDown?.(event);
if (event.defaultPrevented || disabled) {
return;
}
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
event.currentTarget.click();
}
};
return (
<span
{...spanProps}
aria-disabled={disabled}
aria-label={label}
className={actionClassName}
onClick={disabled ? undefined : onClick}
onKeyDown={handleKeyDown}
role="button"
tabIndex={disabled ? -1 : (tabIndex ?? 0)}
title={title}
>
{icon}
{children}
</span>
);
}
const { type = 'button', ...buttonProps } = const { type = 'button', ...buttonProps } =
actionProps as ButtonHTMLAttributes<HTMLButtonElement>; actionProps as ButtonHTMLAttributes<HTMLButtonElement>;

View File

@@ -1064,6 +1064,10 @@ describe('ImageCanvasEditorView', () => {
if (!metadataCornerButton) { if (!metadataCornerButton) {
throw new Error('metadata corner button should exist'); 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); fireEvent.click(metadataCornerButton);
const metadataDialog = screen.getByRole('dialog', { name: /生成图片 .*元数据/ }); const metadataDialog = screen.getByRole('dialog', { name: /生成图片 .*元数据/ });

View File

@@ -2550,28 +2550,19 @@ export function ImageCanvasEditorView() {
> >
<img src={layer.src} alt={`画布图片:${layer.title}`} /> <img src={layer.src} alt={`画布图片:${layer.title}`} />
{isGeneratedLayer(layer) ? ( {isGeneratedLayer(layer) ? (
<span <PlatformIconButton
asChild="spanButton"
variant="darkMini"
className="image-canvas-editor__metadata-corner" className="image-canvas-editor__metadata-corner"
role="button" label={`查看${layer.title}元数据`}
tabIndex={0} icon={<Braces className="h-3 w-3" />}
aria-label={`查看${layer.title}元数据`}
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
setMetadataLayer(layer); setMetadataLayer(layer);
selectSingleLayer(layer.id); selectSingleLayer(layer.id);
}} }}
onPointerDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}
onKeyDown={(event) => { />
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
event.stopPropagation();
setMetadataLayer(layer);
selectSingleLayer(layer.id);
}
}}
>
<Braces className="h-3 w-3" />
</span>
) : null} ) : null}
{isHovered ? ( {isHovered ? (
<span className="image-canvas-editor__size-badge"> <span className="image-canvas-editor__size-badge">