移除画风菜单说明文案并按需加载预览图

删除画风菜单默认操作说明与规则解释文案
画风预览图改为悬停或键盘聚焦时挂载单张图片
补充指针与键盘预览回归测试
This commit is contained in:
2026-08-05 14:37:58 +08:00
parent 6fcf909095
commit 0a703ac236
3 changed files with 126 additions and 27 deletions
@@ -0,0 +1,76 @@
/* @vitest-environment jsdom */
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useState } from 'react';
import { describe, expect, it } from 'vitest';
import type { GenerateDialogState } from './ImageCanvasEditorTypes';
import { ImageCanvasSceneStyleControl } from './ImageCanvasSceneStyleControl';
function SceneStyleControlHarness() {
const [dialog, setDialog] = useState<GenerateDialogState | null>({
mode: 'scene',
prompt: '',
status: 'idle',
sceneStylePreset: 'anime',
});
return dialog ? (
<ImageCanvasSceneStyleControl
dialog={dialog}
setGenerateDialog={setDialog}
renderEditorPortal={(node) => node}
buildPortalMenuStyle={() => ({ position: 'fixed', left: 0, top: 0 })}
/>
) : null;
}
describe('ImageCanvasSceneStyleControl', () => {
it('仅在指针停留于对应预览按钮时挂载一张预览图', async () => {
const user = userEvent.setup();
render(<SceneStyleControlHarness />);
await user.click(screen.getByRole('button', { name: '画风预设 日系动画' }));
expect(screen.queryByRole('img')).toBeNull();
const animePreviewButton = screen.getByRole('button', {
name: '预览日系动画画风',
});
await user.hover(animePreviewButton);
expect(
screen.getByRole('img', { name: '日系动画固定画风预览' }),
).toBeTruthy();
expect(screen.getAllByRole('img')).toHaveLength(1);
await user.unhover(animePreviewButton);
expect(screen.queryByRole('img')).toBeNull();
});
it('键盘聚焦预览按钮时挂载图片,焦点离开后卸载', async () => {
const user = userEvent.setup();
render(<SceneStyleControlHarness />);
await user.click(screen.getByRole('button', { name: '画风预设 日系动画' }));
await user.tab();
await user.tab();
const animePreviewButton = screen.getByRole('button', {
name: '预览日系动画画风',
});
expect(document.activeElement).toBe(animePreviewButton);
expect(
screen.getByRole('img', { name: '日系动画固定画风预览' }),
).toBeTruthy();
await user.tab();
expect(document.activeElement).toBe(
screen.getByRole('menuitemradio', { name: //u }),
);
expect(screen.queryByRole('img')).toBeNull();
});
});
@@ -49,11 +49,20 @@ export function ImageCanvasSceneStyleControl({
buildPortalMenuStyle,
}: ImageCanvasSceneStyleControlProps) {
const [isOpen, setIsOpen] = useState(false);
const [hoveredPreview, setHoveredPreview] =
useState<EditorSceneStylePreset | null>(null);
const [focusedPreview, setFocusedPreview] =
useState<EditorSceneStylePreset | null>(null);
const triggerRef = useRef<HTMLButtonElement | null>(null);
const dismiss = useCallback(() => setIsOpen(false), []);
const dismiss = useCallback(() => {
setIsOpen(false);
setHoveredPreview(null);
setFocusedPreview(null);
}, []);
const selectedValue =
dialog.sceneStylePreset ?? DEFAULT_EDITOR_SCENE_STYLE_PRESET;
const selectedLabel = getEditorSceneStylePresetLabel(selectedValue);
const activePreview = hoveredPreview ?? focusedPreview;
useImageCanvasFloatingOptionDismiss({
isOpen,
@@ -76,7 +85,7 @@ export function ImageCanvasSceneStyleControl({
}
: currentDialog,
);
setIsOpen(false);
dismiss();
};
return (
@@ -88,7 +97,13 @@ export function ImageCanvasSceneStyleControl({
aria-expanded={isOpen}
disabled={dialog.status === 'generating'}
trailingIcon={<ChevronDown className="h-3 w-3" />}
onClick={() => setIsOpen((current) => !current)}
onClick={() => {
if (isOpen) {
dismiss();
return;
}
setIsOpen(true);
}}
>
<span className="image-canvas-editor__scene-style-trigger-label">
<Palette className="h-3.5 w-3.5" aria-hidden="true" />
@@ -106,11 +121,11 @@ export function ImageCanvasSceneStyleControl({
>
<div className="image-canvas-editor__scene-style-menu-heading">
<strong></strong>
<span></span>
</div>
{EDITOR_SCENE_STYLE_PRESET_OPTIONS.map((option) => {
const selected = option.value === selectedValue;
const preview = SCENE_STYLE_PREVIEWS[option.value];
const previewActive = activePreview === option.value;
return (
<div
key={option.value}
@@ -135,27 +150,47 @@ export function ImageCanvasSceneStyleControl({
/>
</button>
{preview ? (
<span className="image-canvas-editor__scene-style-preview-wrap">
<span
className="image-canvas-editor__scene-style-preview-wrap"
onPointerEnter={() => setHoveredPreview(option.value)}
onPointerLeave={() =>
setHoveredPreview((current) =>
current === option.value ? null : current,
)
}
onFocus={() => setFocusedPreview(option.value)}
onBlur={(event) => {
if (
!event.currentTarget.contains(event.relatedTarget)
) {
setFocusedPreview((current) =>
current === option.value ? null : current,
);
}
}}
>
<button
type="button"
className="image-canvas-editor__scene-style-preview-button"
aria-label={`预览${option.label}画风`}
aria-expanded={previewActive}
onPointerDown={(event) => event.preventDefault()}
onClick={(event) => event.stopPropagation()}
>
<Eye className="h-3.5 w-3.5" aria-hidden="true" />
<span></span>
</button>
<figure className="image-canvas-editor__scene-style-preview-card">
<img
src={preview}
alt={`${option.label}固定画风预览`}
/>
<figcaption>
<strong>{option.label}</strong>
<span> · </span>
</figcaption>
</figure>
{previewActive ? (
<figure className="image-canvas-editor__scene-style-preview-card">
<img
src={preview}
alt={`${option.label}固定画风预览`}
/>
<figcaption>
<strong>{option.label}</strong>
</figcaption>
</figure>
) : null}
</span>
) : null}
</div>
-12
View File
@@ -16879,12 +16879,6 @@ button {
font-weight: 850;
}
.image-canvas-editor__scene-style-menu-heading span {
color: #94a3b8;
font-size: 0.67rem;
font-weight: 700;
}
.image-canvas-editor__scene-style-row {
position: relative;
display: grid;
@@ -17026,12 +17020,6 @@ button {
font-weight: 850;
}
.image-canvas-editor__scene-style-preview-card figcaption span {
color: #94a3b8;
font-size: 0.64rem;
font-weight: 700;
}
@media (max-width: 760px) {
.image-canvas-editor__scene-style-anchor {
grid-column: 1 / -1;