Skip to content

feat: improve Image preview accessibility#496

Open
claytonlin1110 wants to merge 6 commits intoreact-component:masterfrom
claytonlin1110:feat/image-preview-a11y
Open

feat: improve Image preview accessibility#496
claytonlin1110 wants to merge 6 commits intoreact-component:masterfrom
claytonlin1110:feat/image-preview-a11y

Conversation

@claytonlin1110
Copy link

@claytonlin1110 claytonlin1110 commented Mar 13, 2026

Summary

  • Make image thumbnails keyboard-activatable when preview is enabled (role="button", tab focus, Enter/Space to open).
  • Treat the preview overlay as a proper dialog with role="dialog", aria-modal="true", and managed focus.
  • Render preview toolbar and navigation controls as real <button> elements to support keyboard focus and interaction.
  • Add tests to verify keyboard behavior and dialog semantics.

Motivation

This PR aligns the Image preview behavior with WCAG requirements by ensuring keyboard users can:

  • Focus the image and open the preview with the keyboard.
  • Land focus inside the preview dialog when it opens and have it restored on close.
  • Tab to and activate all preview controls (Close, Next/Prev, Flip, Rotate, Zoom, etc.).

It also serves as the underlying change required for Ant Design’s Image component accessibility improvement requested in ant-design/ant-design#57254.

Breaking Changes

  • No breaking API changes are expected. DOM structure of preview controls changes from div to button but classNames are preserved, so styles should remain compatible.

Related

Closes ant-design/ant-design#57254

Summary by CodeRabbit

发布说明

  • 新功能

    • 添加键盘预览支持:可通过 Enter / Space 触发图片预览并阻止默认行为
  • 无障碍改进

    • 图片在可预览时具有按钮语义、可聚焦并提供 aria-label
    • 预览以对话框呈现,设置 role="dialog"、aria-modal,并在打开/关闭时管理焦点恢复
    • 预览操作与翻页控件使用语义化按钮,支持 disabled 与 aria-label
  • 样式

    • 重置切换与操作按钮的边框、内边距与字体继承
  • 测试

    • 新增键盘交互与无障碍相关测试用例

@vercel
Copy link

vercel bot commented Mar 13, 2026

@claytonlin1110 is attempting to deploy a commit to the React Component Team on Vercel.

A member of the Team first needs to authorize it.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the accessibility of the Image preview component, aligning its behavior with WCAG requirements. It ensures that keyboard users can seamlessly interact with image previews, from activating thumbnails to navigating and controlling the preview dialog, by implementing proper ARIA roles, focus management, and native button elements.

Highlights

  • Keyboard Activation for Image Thumbnails: Image thumbnails are now keyboard-activatable when preview is enabled, allowing users to open the preview using Enter or Space keys.
  • Enhanced Preview Overlay Accessibility: The preview overlay now functions as a proper dialog with "role="dialog"", "aria-modal="true"", and managed focus for improved accessibility.
  • Interactive Preview Controls: Preview toolbar and navigation controls have been converted to native "" elements, enabling keyboard focus and interaction.
  • Comprehensive Accessibility Tests: New tests were added to verify the correct keyboard behavior and dialog semantics of the image preview component.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/Image.tsx
    • Implemented onPreviewKeyDown handler to enable keyboard activation of the image preview.
    • Added role="button", tabIndex="0", and aria-label attributes to the image wrapper when preview is enabled.
    • Attached the onPreviewKeyDown handler to the image wrapper.
  • src/Preview/Footer.tsx
    • Refactored preview operation div elements into semantic button elements.
    • Added type="button", disabled, and aria-label attributes to the new button elements.
  • src/Preview/PrevNext.tsx
    • Converted previous and next navigation div elements into semantic button elements.
    • Added type="button", disabled, and aria-label attributes to the navigation buttons.
  • src/Preview/index.tsx
    • Introduced wrapperRef and lastActiveRef for managing focus within the preview dialog.
    • Implemented a useEffect hook to manage focus: setting focus to the preview wrapper when opened and restoring focus to the last active element when closed.
    • Applied role="dialog", aria-modal="true", aria-label, and tabIndex="-1" to the main preview div element.
  • tests/preview.test.tsx
    • Added a test to confirm the image wrapper has role="button" and tabIndex="0" when preview is enabled.
    • Included a test to verify that pressing Enter on the image wrapper opens the preview.
    • Added a test to assert that the preview dialog has the correct ARIA attributes (role="dialog", aria-modal="true", aria-label) and receives focus upon opening.
Activity
  • No human activity has been recorded on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link

coderabbitai bot commented Mar 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

为 Image/Preview 增加键盘触发与对话框无障碍语义、将若干控件改为原生按钮并在打开/关闭预览时管理焦点;新增对应无障碍测试。(不超过 50 字)

Changes

Cohort / File(s) Summary
Image 键盘与无障碍
src/Image.tsx
新增 onPreviewKeyDown:按 Enter/Space 触发预览并 preventDefault(受 canPreview 约束);计算触发点中心以作为预览来源;在可预览时为包裹器设置 role="button"、默认 tabIndex=0(若未显式提供)、aria-label,并将 onKeyDown 与原有处理器合并;保留 click 行为。
Preview 操作语义化
src/Preview/Footer.tsx, src/Preview/PrevNext.tsx
将操作控件与前/后切换由 div 替换为原生 button,添加 type="button"disabledaria-label,保留原有点击逻辑与图标渲染;同时更新 RenderOperationParams.onClick 类型为 React.MouseEventHandler<HTMLButtonElement>
Preview 对话框焦点管理
src/Preview/index.tsx
新增 wrapperReflastActiveRef;打开时保存当前焦点并聚焦容器,关闭时恢复焦点并清理;为预览容器添加 role="dialog"aria-modal="true"aria-label(回退到 "Image preview")与 tabIndex={-1}
样式重置以支持按钮语义
assets/preview.less
为切换与操作按钮添加样式重置(border:0; padding:0; background:transparent; font:inherit;),确保原生按钮视觉与交互一致。
可访问性测试
tests/preview.test.tsx
新增测试:图片包装器在可预览时为可聚焦(role="button"/tabindex=0);按 Enter/Space 打开预览;预览对话框具有 role="dialog"aria-modal="true" 与正确 aria-label

Sequence Diagram(s)

sequenceDiagram
    participant User as 用户
    participant Image as Image 组件
    participant Preview as Preview 对话框
    participant DOM as 浏览器焦点

    User->>Image: 按 Enter/Space 或 点击
    Image->>Image: 检查 canPreview 并 调用 onPreviewKeyDown/onClick
    Image->>Preview: 打开预览(传入 origin/位置)
    Image->>DOM: 保存当前聚焦元素到 lastActiveRef
    Preview->>DOM: 聚焦 wrapperRef (tabIndex=-1)
    Note right of Preview: role="dialog", aria-modal="true", aria-label
    User->>Preview: 与控件交互(按钮为原生 button)
    Preview->>DOM: 关闭时恢复 lastActiveRef 的焦点
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • zombieJ

Poem

🐰 轻敲回车与空格键,
我把焦点藏进画中间,
按钮换上了语义衣,
对话框有名也有面,
键盘游客来赏不难。

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题清晰地总结了主要变更——改进图片预览的可访问性,与代码更改保持一致。
Linked Issues check ✅ Passed 代码更改完整地满足了链接问题#57254的所有编码要求:为图片添加keyboard激活、role="button"和tabIndex;为预览对话框添加role="dialog"和焦点管理;将按钮转换为语义化元素。
Out of Scope Changes check ✅ Passed 所有代码更改都与链接问题#57254的可访问性目标相关,包括键盘支持、焦点管理和按钮样式重置。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claytonlin1110
Copy link
Author

@yoyo837 @afc163 Please review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request significantly improves the accessibility of the Image component by making the preview feature fully keyboard-operable and adhering to ARIA standards for dialogs. The changes are well-executed, including converting interactive divs to buttons and properly managing focus. I've identified one high-severity issue related to type safety when triggering the preview via the keyboard, which could lead to runtime errors. Addressing this will make the implementation more robust.

Comment on lines +207 to +216
const onPreviewKeyDown: React.KeyboardEventHandler<HTMLDivElement> = event => {
if (!canPreview) {
return;
}

if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
onPreview(event as any);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The use of event as any when calling onPreview from this keyboard handler conceals a type mismatch that could lead to runtime errors. The onPreview function is typed as a React.MouseEventHandler and is designed to handle mouse events. It also calls the component's onClick prop, which explicitly expects a React.MouseEvent.

Passing a KeyboardEvent from onPreviewKeyDown violates this contract. If a consumer of the Image component provides an onClick handler that relies on mouse-specific properties (e.g., clientX, button), their code will fail when the preview is triggered by a key press.

To fix this and maintain type safety, the logic for opening the preview should be handled within this function, ensuring that the onClick prop is not called for keyboard events. While this introduces some code duplication from onPreview, it correctly separates the concerns of click and keyboard interactions and prevents the bug.

  const onPreviewKeyDown: React.KeyboardEventHandler<HTMLDivElement> = event => {
    if (!canPreview) {
      return;
    }

    if (event.key === 'Enter' || event.key === ' ') {
      event.preventDefault();

      const rect = (event.target as HTMLDivElement).getBoundingClientRect();
      const left = rect.x + rect.width / 2;
      const top = rect.y + rect.height / 2;

      if (groupContext) {
        groupContext.onPreview(imageId, src, left, top);
      } else {
        setMousePosition({
          x: left,
          y: top,
        });
        triggerPreviewOpen(true);
      }
    }
  };

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Preview/Footer.tsx (1)

23-23: ⚠️ Potential issue | 🟡 Minor

onClick 类型与实际元素不匹配

RenderOperationParams 接口中 onClick 的类型定义为 React.MouseEvent<HTMLDivElement>, 但现在元素已改为 <button>,应更新为 HTMLButtonElement

🔧 建议修复
 interface RenderOperationParams {
   icon: React.ReactNode;
   type: OperationType;
   disabled?: boolean;
-  onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
+  onClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Preview/Footer.tsx` at line 23, RenderOperationParams 的 onClick 类型仍声明为
React.MouseEvent<HTMLDivElement, MouseEvent>,但该回调现在绑定到 <button> 元素;请将该属性类型改为
React.MouseEvent<HTMLButtonElement, MouseEvent>(或简写为
React.MouseEvent<HTMLButtonElement>),并更新相关使用处/测试中对 RenderOperationParams.onClick
的类型假设以匹配新元素类型。
🧹 Nitpick comments (4)
tests/preview.test.tsx (1)

1151-1163: 建议补充 Space 键测试

src/Image.tsx 中的 onPreviewKeyDown 同时支持 Enter 和 Space 键打开预览,但测试只覆盖了 Enter 键。建议补充 Space 键的测试以确保完整覆盖。

🧪 建议添加测试
   it('Pressing Enter on image wrapper should open preview', () => {
     const { container } = render(<Image src="src" alt="keyboard open" />);

     const wrapper = container.querySelector('.rc-image') as HTMLElement;
     wrapper.focus();
     fireEvent.keyDown(wrapper, { key: 'Enter' });

     act(() => {
       jest.runAllTimers();
     });

     expect(document.querySelector('.rc-image-preview')).toBeTruthy();
   });
+
+  it('Pressing Space on image wrapper should open preview', () => {
+    const { container } = render(<Image src="src" alt="keyboard space" />);
+
+    const wrapper = container.querySelector('.rc-image') as HTMLElement;
+    wrapper.focus();
+    fireEvent.keyDown(wrapper, { key: ' ' });
+
+    act(() => {
+      jest.runAllTimers();
+    });
+
+    expect(document.querySelector('.rc-image-preview')).toBeTruthy();
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/preview.test.tsx` around lines 1151 - 1163, Add a new test alongside
the existing Enter-key case in tests/preview.test.tsx that verifies Space opens
the preview: locate the Image render and the wrapper element query ('.rc-image')
used in the current test, then simulate focus and fireEvent.keyDown(wrapper, {
key: ' ' }) or { key: 'Space' } to trigger the component's onPreviewKeyDown
handler, run timers with act(() => jest.runAllTimers()), and assert that
document.querySelector('.rc-image-preview') is truthy; mirror the existing test
structure so both Enter and Space key paths in Image.onPreviewKeyDown are
covered.
src/Preview/Footer.tsx (1)

98-109: aria-label 值对屏幕阅读器用户不够友好

当前 aria-label={type} 会输出技术术语如 "flipY""rotateLeft",对于屏幕阅读器用户来说不够直观。建议使用更具描述性的标签。

♻️ 建议改进
+const operationLabels: Record<OperationType, string> = {
+  prev: 'Previous image',
+  next: 'Next image',
+  flipY: 'Flip vertically',
+  flipX: 'Flip horizontally',
+  rotateLeft: 'Rotate left',
+  rotateRight: 'Rotate right',
+  zoomOut: 'Zoom out',
+  zoomIn: 'Zoom in',
+};
+
 const renderOperation = ({ type, disabled, onClick, icon }: RenderOperationParams) => {
   return (
     <button
       type="button"
       key={type}
       className={clsx(actionCls, `${actionCls}-${type}`, {
         [`${actionCls}-disabled`]: !!disabled,
       })}
       onClick={onClick}
       disabled={!!disabled}
-      aria-label={type}
+      aria-label={operationLabels[type]}
     >
       {icon}
     </button>
   );
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Preview/Footer.tsx` around lines 98 - 109, The aria-label currently uses
the raw `type` (e.g., "flipY", "rotateLeft") which is not friendly for screen
readers; update the `Footer` button rendering to map `type` to a human-readable
label (e.g., via a lookup object like actionLabelMap keyed by `type`) and use
that mapped string in the `aria-label` instead of `type`; keep using the
existing props/variables (`type`, `icon`, `onClick`, `actionCls`, `disabled`)
and ensure a sensible default label (e.g., the `type` transformed or `"Action"`)
if the map has no entry.
src/Preview/index.tsx (1)

244-256: 焦点管理时机可能与动画冲突

当前焦点管理在 open 状态变化时立即执行,但 CSSMotion 可能仍在执行进入/离开动画。这可能导致:

  1. 打开时:焦点可能在元素完全可见前就被设置
  2. 关闭时:焦点在对话框消失前就被恢复

考虑将焦点恢复逻辑移至 onVisibleChanged 回调中,以确保动画完成后再处理焦点:

♻️ 建议改进
   // =========================== Focus ============================
   useEffect(() => {
     if (open) {
       lastActiveRef.current = (document.activeElement as HTMLElement) || null;

       if (wrapperRef.current) {
         wrapperRef.current.focus();
       }
-    } else if (!open && lastActiveRef.current) {
-      lastActiveRef.current.focus();
-      lastActiveRef.current = null;
     }
   }, [open]);

   // ... existing code ...

   const onVisibleChanged = (nextVisible: boolean) => {
     if (!nextVisible) {
       setLockScroll(false);
+      // Restore focus after animation completes
+      if (lastActiveRef.current) {
+        lastActiveRef.current.focus();
+        lastActiveRef.current = null;
+      }
     }
     afterOpenChange?.(nextVisible);
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Preview/index.tsx` around lines 244 - 256, The focus handling currently
in the useEffect tied to open fires immediately and can clash with CSSMotion
animations; move the restore-and-set-focus logic out of that useEffect and into
the component's onVisibleChanged callback so focus is applied only after
enter/leave animations finish: keep lastActiveRef and wrapperRef usage but
trigger wrapperRef.current.focus() when onVisibleChanged(true) reports visible,
and restore lastActiveRef.focus() when onVisibleChanged(false) reports not
visible, removing the immediate focus/restore from the useEffect that watches
open.
src/Image.tsx (1)

206-216: 键盘事件类型转换需要注意

Line 214 将 KeyboardEvent 强制转换为 any 传递给 onPreview,而 onPreview 期望接收 MouseEvent<HTMLDivElement>。虽然两种事件的 target 属性都指向同一元素且 getBoundingClientRect() 可正常工作,但这种类型转换可能隐藏潜在问题。

建议考虑提取公共逻辑或创建单独的键盘处理函数:

♻️ 可选的重构方案
+  const triggerPreview = (target: HTMLElement) => {
+    const rect = target.getBoundingClientRect();
+    const left = rect.x + rect.width / 2;
+    const top = rect.y + rect.height / 2;
+
+    if (groupContext) {
+      groupContext.onPreview(imageId, src, left, top);
+    } else {
+      setMousePosition({
+        x: left,
+        y: top,
+      });
+      triggerPreviewOpen(true);
+    }
+  };
+
   const onPreview: React.MouseEventHandler<HTMLDivElement> = e => {
-    const rect = (e.target as HTMLDivElement).getBoundingClientRect();
-    const left = rect.x + rect.width / 2;
-    const top = rect.y + rect.height / 2;
-
-    if (groupContext) {
-      groupContext.onPreview(imageId, src, left, top);
-    } else {
-      setMousePosition({
-        x: left,
-        y: top,
-      });
-      triggerPreviewOpen(true);
-    }
-
+    triggerPreview(e.currentTarget);
     onClick?.(e);
   };

   const onPreviewKeyDown: React.KeyboardEventHandler<HTMLDivElement> = event => {
     if (!canPreview) {
       return;
     }

     if (event.key === 'Enter' || event.key === ' ') {
       event.preventDefault();
-      onPreview(event as any);
+      triggerPreview(event.currentTarget);
     }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Image.tsx` around lines 206 - 216, The keyboard handler onPreviewKeyDown
currently casts the KeyboardEvent to any and passes it to onPreview (which
expects MouseEvent<HTMLDivElement>); instead extract the shared preview behavior
into a new helper (e.g., triggerPreviewFromElement or handlePreviewAction) that
accepts the target element (HTMLDivElement) or its bounding rect, then have
onPreview call that helper using its MouseEvent.currentTarget and have
onPreviewKeyDown call the helper using event.currentTarget (or
event.currentTarget as HTMLDivElement) when canPreview is true and Enter/Space
is pressed; update onPreviewKeyDown to remove the unsafe (event as any) cast and
reference canPreview, onPreview and the new helper accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/preview.test.tsx`:
- Around line 1165-1173: The focus assertion in the test "Preview dialog should
have role dialog and receive focus" can race with the Preview component's
useEffect focus logic; update the test in tests/preview.test.tsx to advance
timers before asserting focus (or use an async waitFor) so the focus set inside
src/Preview/index.tsx's useEffect has run—specifically, after rendering the
<Image ... preview={{ open: true }} /> call, invoke jest.runAllTimers() (or
await waitFor(() => expect(document.activeElement).toBe(preview))) before the
expect(document.activeElement).toBe(preview) assertion to ensure deterministic
focus timing.

---

Outside diff comments:
In `@src/Preview/Footer.tsx`:
- Line 23: RenderOperationParams 的 onClick 类型仍声明为
React.MouseEvent<HTMLDivElement, MouseEvent>,但该回调现在绑定到 <button> 元素;请将该属性类型改为
React.MouseEvent<HTMLButtonElement, MouseEvent>(或简写为
React.MouseEvent<HTMLButtonElement>),并更新相关使用处/测试中对 RenderOperationParams.onClick
的类型假设以匹配新元素类型。

---

Nitpick comments:
In `@src/Image.tsx`:
- Around line 206-216: The keyboard handler onPreviewKeyDown currently casts the
KeyboardEvent to any and passes it to onPreview (which expects
MouseEvent<HTMLDivElement>); instead extract the shared preview behavior into a
new helper (e.g., triggerPreviewFromElement or handlePreviewAction) that accepts
the target element (HTMLDivElement) or its bounding rect, then have onPreview
call that helper using its MouseEvent.currentTarget and have onPreviewKeyDown
call the helper using event.currentTarget (or event.currentTarget as
HTMLDivElement) when canPreview is true and Enter/Space is pressed; update
onPreviewKeyDown to remove the unsafe (event as any) cast and reference
canPreview, onPreview and the new helper accordingly.

In `@src/Preview/Footer.tsx`:
- Around line 98-109: The aria-label currently uses the raw `type` (e.g.,
"flipY", "rotateLeft") which is not friendly for screen readers; update the
`Footer` button rendering to map `type` to a human-readable label (e.g., via a
lookup object like actionLabelMap keyed by `type`) and use that mapped string in
the `aria-label` instead of `type`; keep using the existing props/variables
(`type`, `icon`, `onClick`, `actionCls`, `disabled`) and ensure a sensible
default label (e.g., the `type` transformed or `"Action"`) if the map has no
entry.

In `@src/Preview/index.tsx`:
- Around line 244-256: The focus handling currently in the useEffect tied to
open fires immediately and can clash with CSSMotion animations; move the
restore-and-set-focus logic out of that useEffect and into the component's
onVisibleChanged callback so focus is applied only after enter/leave animations
finish: keep lastActiveRef and wrapperRef usage but trigger
wrapperRef.current.focus() when onVisibleChanged(true) reports visible, and
restore lastActiveRef.focus() when onVisibleChanged(false) reports not visible,
removing the immediate focus/restore from the useEffect that watches open.

In `@tests/preview.test.tsx`:
- Around line 1151-1163: Add a new test alongside the existing Enter-key case in
tests/preview.test.tsx that verifies Space opens the preview: locate the Image
render and the wrapper element query ('.rc-image') used in the current test,
then simulate focus and fireEvent.keyDown(wrapper, { key: ' ' }) or { key:
'Space' } to trigger the component's onPreviewKeyDown handler, run timers with
act(() => jest.runAllTimers()), and assert that
document.querySelector('.rc-image-preview') is truthy; mirror the existing test
structure so both Enter and Space key paths in Image.onPreviewKeyDown are
covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 31280583-fa4b-4e1b-bb85-8ed8fbb64df3

📥 Commits

Reviewing files that changed from the base of the PR and between 2825512 and 8c398fc.

📒 Files selected for processing (5)
  • src/Image.tsx
  • src/Preview/Footer.tsx
  • src/Preview/PrevNext.tsx
  • src/Preview/index.tsx
  • tests/preview.test.tsx

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
tests/preview.test.tsx (1)

1165-1172: ⚠️ Potential issue | 🟠 Major

测试名与断言不一致:缺少“receive focus”校验

Line 1165 的用例名声明“receive focus”,但 Line 1168-1172 只校验了 ARIA 属性,没有校验焦点是否落入预览对话框,核心行为未被测试保护。

建议修改
   it('Preview dialog should have role dialog and receive focus', () => {
     render(<Image src="src" alt="dialog a11y" preview={{ open: true }} />);

+    act(() => {
+      jest.runAllTimers();
+    });
+
     const preview = document.querySelector('.rc-image-preview') as HTMLElement;
     expect(preview).toHaveAttribute('role', 'dialog');
     expect(preview).toHaveAttribute('aria-modal', 'true');
     expect(preview).toHaveAttribute('aria-label', 'dialog a11y');
+    expect(document.activeElement).toBe(preview);
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/preview.test.tsx` around lines 1165 - 1172, The test name promises to
verify the preview "receive focus" but only asserts ARIA attributes; update the
case that renders Image (render(<Image src="src" alt="dialog a11y" preview={{
open: true }} />)) to also assert focus on the preview element (the queried
.rc-image-preview stored in preview) — e.g. ensure the preview is the active
element by using expect(preview).toHaveFocus() or assert document.activeElement
=== preview, and if needed waitFor or ensure the preview is focusable (tabindex)
before asserting.
🧹 Nitpick comments (1)
tests/preview.test.tsx (1)

1151-1163: 建议补充 Space 键触发预览的测试

Line 1151-1163 目前只覆盖 Enter,未覆盖需求中的 Space 键路径,建议补一条用例避免回归。

建议修改
   it('Pressing Enter on image wrapper should open preview', () => {
     const { container } = render(<Image src="src" alt="keyboard open" />);

     const wrapper = container.querySelector('.rc-image') as HTMLElement;
     wrapper.focus();
     fireEvent.keyDown(wrapper, { key: 'Enter' });

     act(() => {
       jest.runAllTimers();
     });

     expect(document.querySelector('.rc-image-preview')).toBeTruthy();
   });
+
+  it('Pressing Space on image wrapper should open preview', () => {
+    const { container } = render(<Image src="src" alt="keyboard open by space" />);
+
+    const wrapper = container.querySelector('.rc-image') as HTMLElement;
+    wrapper.focus();
+    fireEvent.keyDown(wrapper, { key: ' ' });
+
+    act(() => {
+      jest.runAllTimers();
+    });
+
+    expect(document.querySelector('.rc-image-preview')).toBeTruthy();
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/preview.test.tsx` around lines 1151 - 1163, Add a new test case
alongside the existing "Pressing Enter on image wrapper should open preview"
that verifies pressing the Space key also opens the preview: render the Image
component (Image), query and focus the wrapper element ('.rc-image'), dispatch a
Space keydown via fireEvent.keyDown on the wrapper (use the same pattern as the
Enter test), run timers with jest.runAllTimers() inside act(), and assert the
preview appears via document.querySelector('.rc-image-preview'); name the test
something like "Pressing Space on image wrapper should open preview" to prevent
regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@tests/preview.test.tsx`:
- Around line 1165-1172: The test name promises to verify the preview "receive
focus" but only asserts ARIA attributes; update the case that renders Image
(render(<Image src="src" alt="dialog a11y" preview={{ open: true }} />)) to also
assert focus on the preview element (the queried .rc-image-preview stored in
preview) — e.g. ensure the preview is the active element by using
expect(preview).toHaveFocus() or assert document.activeElement === preview, and
if needed waitFor or ensure the preview is focusable (tabindex) before
asserting.

---

Nitpick comments:
In `@tests/preview.test.tsx`:
- Around line 1151-1163: Add a new test case alongside the existing "Pressing
Enter on image wrapper should open preview" that verifies pressing the Space key
also opens the preview: render the Image component (Image), query and focus the
wrapper element ('.rc-image'), dispatch a Space keydown via fireEvent.keyDown on
the wrapper (use the same pattern as the Enter test), run timers with
jest.runAllTimers() inside act(), and assert the preview appears via
document.querySelector('.rc-image-preview'); name the test something like
"Pressing Space on image wrapper should open preview" to prevent regressions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 79ba0b31-aa32-406a-ae02-400ec9811c3e

📥 Commits

Reviewing files that changed from the base of the PR and between 8c398fc and 92eff10.

⛔ Files ignored due to path filters (1)
  • tests/__snapshots__/basic.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (1)
  • tests/preview.test.tsx

@claytonlin1110
Copy link
Author

@afc163 Please review when you have a chance. Thanks.

@claytonlin1110
Copy link
Author

cc @meet-student

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Preview/Footer.tsx`:
- Line 106: 当前 Footer.tsx 中将 aria-label 直接设为内部标识 aria-label={type},会把诸如
"flipY"/"zoomOut" 读作不可理解的技术词,需改为人类可读文本并与 PrevNext.tsx 的 “Previous image / Next
image” 风格一致。修改方案:在 Footer 组件中为传入的 type(或 action)建立一个映射函数/对象(例如
getAriaLabelForAction 或 actionAriaLabels)将每个内部标识(flipY, zoomOut,
等)映射到清晰的可读字符串(可本地化),然后把 aria-label 设置为该映射结果;保持未识别值的回退字符串并更新相应测试/注释以反映可读标签。
ensure references: aria-label prop in Footer.tsx and the type variable, and
align wording with PrevNext.tsx labels.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 04fdde69-f225-49c2-a515-7f9e378eb841

📥 Commits

Reviewing files that changed from the base of the PR and between 92eff10 and df15f86.

📒 Files selected for processing (1)
  • src/Preview/Footer.tsx

@claytonlin1110
Copy link
Author

@afc163 Would you please review this? Thanks.

@claytonlin1110
Copy link
Author

@yoyo837 Any feedback for this feature PR?

@yoyo837
Copy link
Member

yoyo837 commented Mar 17, 2026

CI failed

@yoyo837
Copy link
Member

yoyo837 commented Mar 17, 2026

Great accessibility direction overall — keyboard activation for preview, dialog semantics, and switching interactive controls to semantic <button> are all meaningful improvements.

Non-CI review feedback:

  1. Please preserve consumer-provided aria-label on the trigger wrapper
  • Current code in src/Image.tsx uses:
    aria-label={canPreview ? (alt || 'Preview image') : otherProps['aria-label']}
  • When preview is enabled, this overrides an explicitly provided aria-label from consumers.
  • Suggested behavior: prefer user-provided label first, then fallback to alt, then default text.
    Example:
    aria-label={canPreview ? (otherProps['aria-label'] ?? alt ?? 'Preview image') : otherProps['aria-label']}
  1. div -> button migration likely needs explicit button reset styles
  • In Footer.tsx and PrevNext.tsx, changing to <button> is correct for a11y.
  • But without resetting native button styles, browsers may introduce default border/padding/background and cause visual regressions.
  • Please add a reset for .rc-image-preview-actions-action / .rc-image-preview-switch (e.g. border: 0; background: transparent; padding: 0; font: inherit;) to keep rendering consistent.
  1. Optional: include a Space-key test on trigger
  • Enter is covered, but adding a Space activation test would better lock keyboard behavior and align with button-like semantics.

Thanks again — this is a solid accessibility improvement overall.

src/Image.tsx Outdated
onClick={canPreview ? onPreview : onClick}
role={canPreview ? 'button' : otherProps.role}
tabIndex={canPreview && otherProps.tabIndex == null ? 0 : otherProps.tabIndex}
aria-label={canPreview ? (alt || 'Preview image') : otherProps['aria-label']}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: keep consumer-provided aria-label highest priority when preview is enabled.

Suggested change
aria-label={canPreview ? (alt || 'Preview image') : otherProps['aria-label']}
aria-label={canPreview ? (otherProps['aria-label'] ?? alt ?? 'Preview image') : otherProps['aria-label']}

src/Image.tsx Outdated
role={canPreview ? 'button' : otherProps.role}
tabIndex={canPreview && otherProps.tabIndex == null ? 0 : otherProps.tabIndex}
aria-label={canPreview ? (alt || 'Preview image') : otherProps['aria-label']}
onKeyDown={onPreviewKeyDown}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: preserve external onKeyDown handlers to avoid behavior regressions for consumers attaching keyboard logic on the wrapper.

Suggested change
onKeyDown={onPreviewKeyDown}
onKeyDown={event => {
onPreviewKeyDown(event);
otherProps.onKeyDown?.(event);
}}

@claytonlin1110
Copy link
Author

@yoyo837 Thanks for your feedback, I just updated and pushed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/Image.tsx (1)

206-229: 建议提取重复的预览触发逻辑

onPreviewKeyDown 中的位置计算和预览触发逻辑(lines 215-227)与 onPreview 中的逻辑(lines 189-200)高度重复。可以考虑提取一个共享的辅助函数来减少冗余。

♻️ 可选重构建议
+  const triggerPreview = (target: HTMLElement) => {
+    const rect = target.getBoundingClientRect();
+    const left = rect.x + rect.width / 2;
+    const top = rect.y + rect.height / 2;
+
+    if (groupContext) {
+      groupContext.onPreview(imageId, src, left, top);
+    } else {
+      setMousePosition({ x: left, y: top });
+      triggerPreviewOpen(true);
+    }
+  };
+
   const onPreview: React.MouseEventHandler<HTMLDivElement> = e => {
-    const rect = (e.target as HTMLDivElement).getBoundingClientRect();
-    const left = rect.x + rect.width / 2;
-    const top = rect.y + rect.height / 2;
-
-    if (groupContext) {
-      groupContext.onPreview(imageId, src, left, top);
-    } else {
-      setMousePosition({
-        x: left,
-        y: top,
-      });
-      triggerPreviewOpen(true);
-    }
-
+    triggerPreview(e.target as HTMLDivElement);
     onClick?.(e);
   };

   const onPreviewKeyDown: React.KeyboardEventHandler<HTMLDivElement> = event => {
     if (!canPreview) {
       return;
     }

     if (event.key === 'Enter' || event.key === ' ') {
       event.preventDefault();
-
-      const rect = (event.target as HTMLDivElement).getBoundingClientRect();
-      const left = rect.x + rect.width / 2;
-      const top = rect.y + rect.height / 2;
-
-      if (groupContext) {
-        groupContext.onPreview(imageId, src, left, top);
-      } else {
-        setMousePosition({
-          x: left,
-          y: top,
-        });
-        triggerPreviewOpen(true);
-      }
+      triggerPreview(event.target as HTMLDivElement);
     }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Image.tsx` around lines 206 - 229, The keydown handler duplicates the
center-position calculation and preview-trigger logic found in onPreview;
extract a shared helper (e.g., computeCenterAndTriggerPreview) that accepts an
element or rect plus imageId/src and performs the rect center calc and the two
trigger paths (calling groupContext.onPreview(...) when groupContext exists,
otherwise calling setMousePosition(...) and triggerPreviewOpen(true)); replace
the duplicated blocks in onPreviewKeyDown and onPreview to call that helper so
the logic is centralized and DRY while still using existing symbols
(onPreviewKeyDown, onPreview, groupContext, groupContext.onPreview,
setMousePosition, triggerPreviewOpen, imageId, src).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/Image.tsx`:
- Around line 206-229: The keydown handler duplicates the center-position
calculation and preview-trigger logic found in onPreview; extract a shared
helper (e.g., computeCenterAndTriggerPreview) that accepts an element or rect
plus imageId/src and performs the rect center calc and the two trigger paths
(calling groupContext.onPreview(...) when groupContext exists, otherwise calling
setMousePosition(...) and triggerPreviewOpen(true)); replace the duplicated
blocks in onPreviewKeyDown and onPreview to call that helper so the logic is
centralized and DRY while still using existing symbols (onPreviewKeyDown,
onPreview, groupContext, groupContext.onPreview, setMousePosition,
triggerPreviewOpen, imageId, src).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 41dbc003-8d4e-4a54-9361-c5caed9c83e5

📥 Commits

Reviewing files that changed from the base of the PR and between df15f86 and 824ecce.

📒 Files selected for processing (3)
  • assets/preview.less
  • src/Image.tsx
  • tests/preview.test.tsx

@yoyo837
Copy link
Member

yoyo837 commented Mar 17, 2026

@coderabbitai Review it again please.

@claytonlin1110
Copy link
Author

@yoyo837 About test issue, i could reproduce the same issue on master branch as well.

@coderabbitai
Copy link

coderabbitai bot commented Mar 17, 2026

@yoyo837 Sure! I'll re-review the latest changes right away.

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
tests/preview.test.tsx (1)

1179-1186: ⚠️ Potential issue | 🟡 Minor

用例名与断言不一致:未验证“receive focus”。

当前只校验了 role/aria,没有校验焦点是否落入对话框。建议要么重命名用例,要么在 Line 1182 之后补充对 document.activeElement 的断言(必要时先推进 timers)。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/preview.test.tsx` around lines 1179 - 1186, The test "Preview dialog
should have role dialog and receive focus" only asserts role/aria attributes on
the rendered Image preview (variable preview) but doesn't verify focus; either
rename the test to remove "receive focus" or add an assertion that
document.activeElement equals the preview element (or that
preview.contains(document.activeElement)) after render; if focus is set
asynchronously, run pending timers (e.g., via jest.runAllTimers or
jest.advanceTimersByTime) before checking document.activeElement to ensure the
focus change has occurred.
src/Preview/Footer.tsx (1)

98-107: ⚠️ Potential issue | 🟠 Major

aria-label 不应直接使用内部操作码。

Line 106 现在会读出 flipY/zoomOut 这类技术词,不利于读屏理解。建议映射为可读文本,并保持与 PrevNext 文案风格一致。

建议修复
+  const operationAriaLabelMap: Record<OperationType, string> = {
+    prev: 'Previous image',
+    next: 'Next image',
+    flipY: 'Flip vertically',
+    flipX: 'Flip horizontally',
+    rotateLeft: 'Rotate left',
+    rotateRight: 'Rotate right',
+    zoomOut: 'Zoom out',
+    zoomIn: 'Zoom in',
+  };
+
   const renderOperation = ({ type, disabled, onClick, icon }: RenderOperationParams) => {
     return (
       <button
@@
-        aria-label={type}
+        aria-label={operationAriaLabelMap[type] ?? 'Image action'}
       >
         {icon}
       </button>
     );
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Preview/Footer.tsx` around lines 98 - 107, The aria-label currently uses
the internal action code variable type (e.g., "flipY" or "zoomOut") which is
unreadable to screen readers; change the button's aria-label to a human-readable
mapping instead (e.g., map "flipY" -> "Flip vertically", "zoomOut" -> "Zoom
out") by adding a small mapping object or helper (e.g., ARIA_LABELS or
getAriaLabel) and using ARIA_LABELS[type] || type for the aria-label in the
button inside Footer.tsx so the label matches the PrevNext copy style while
preserving existing props like onClick, disabled, and className (actionCls and
`${actionCls}-${type}`).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Preview/index.tsx`:
- Around line 245-257: Current open/close effect (useEffect watching open) only
moves focus in/out via lastActiveRef and wrapperRef but does not trap Tab, so
keyboard users can tab out of the modal; update the Preview component to add a
keydown handler attached to wrapperRef when open that intercepts Tab/Shift+Tab,
queries focusable elements within wrapperRef (use standard focusable selector:
a, button, input, select, textarea, [tabindex]:not([tabindex="-1"]) and
:not([disabled])), and when Tab would move past the last element move focus to
the first (and when Shift+Tab would move before the first move focus to the
last); ensure the handler is registered on open and removed on close and that
wrapperRef.current is focused as before (keep lastActiveRef behavior intact).

---

Duplicate comments:
In `@src/Preview/Footer.tsx`:
- Around line 98-107: The aria-label currently uses the internal action code
variable type (e.g., "flipY" or "zoomOut") which is unreadable to screen
readers; change the button's aria-label to a human-readable mapping instead
(e.g., map "flipY" -> "Flip vertically", "zoomOut" -> "Zoom out") by adding a
small mapping object or helper (e.g., ARIA_LABELS or getAriaLabel) and using
ARIA_LABELS[type] || type for the aria-label in the button inside Footer.tsx so
the label matches the PrevNext copy style while preserving existing props like
onClick, disabled, and className (actionCls and `${actionCls}-${type}`).

In `@tests/preview.test.tsx`:
- Around line 1179-1186: The test "Preview dialog should have role dialog and
receive focus" only asserts role/aria attributes on the rendered Image preview
(variable preview) but doesn't verify focus; either rename the test to remove
"receive focus" or add an assertion that document.activeElement equals the
preview element (or that preview.contains(document.activeElement)) after render;
if focus is set asynchronously, run pending timers (e.g., via jest.runAllTimers
or jest.advanceTimersByTime) before checking document.activeElement to ensure
the focus change has occurred.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6b582e2a-92dd-4fe8-8e89-d02994682b40

📥 Commits

Reviewing files that changed from the base of the PR and between 2825512 and 824ecce.

⛔ Files ignored due to path filters (1)
  • tests/__snapshots__/basic.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (6)
  • assets/preview.less
  • src/Image.tsx
  • src/Preview/Footer.tsx
  • src/Preview/PrevNext.tsx
  • src/Preview/index.tsx
  • tests/preview.test.tsx

Comment on lines +245 to +257
useEffect(() => {
if (open) {
lastActiveRef.current = (document.activeElement as HTMLElement) || null;

if (wrapperRef.current) {
wrapperRef.current.focus();
}
} else if (!open && lastActiveRef.current) {
lastActiveRef.current.focus();
lastActiveRef.current = null;
}
}, [open]);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

对话框缺少焦点陷阱,Tab 仍可逃逸到弹层外。

目前实现了“打开聚焦 + 关闭恢复”,但没有 Tab 循环约束;这会让键盘用户离开 modal 语境。建议在预览容器上补充 Tab/Shift+Tab 循环处理。

建议方向(最小实现)
+  const onDialogKeyDown: React.KeyboardEventHandler<HTMLDivElement> = event => {
+    if (event.key !== 'Tab' || !wrapperRef.current) return;
+    const focusables = wrapperRef.current.querySelectorAll<HTMLElement>(
+      'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
+    );
+    if (!focusables.length) return;
+    const first = focusables[0];
+    const last = focusables[focusables.length - 1];
+    const active = document.activeElement as HTMLElement | null;
+    if (!event.shiftKey && active === last) {
+      event.preventDefault();
+      first.focus();
+    } else if (event.shiftKey && active === first) {
+      event.preventDefault();
+      last.focus();
+    }
+  };
@@
-            <div
+            <div
               ref={wrapperRef}
@@
               tabIndex={-1}
+              onKeyDown={onDialogKeyDown}
             >

Also applies to: 442-446

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Preview/index.tsx` around lines 245 - 257, Current open/close effect
(useEffect watching open) only moves focus in/out via lastActiveRef and
wrapperRef but does not trap Tab, so keyboard users can tab out of the modal;
update the Preview component to add a keydown handler attached to wrapperRef
when open that intercepts Tab/Shift+Tab, queries focusable elements within
wrapperRef (use standard focusable selector: a, button, input, select, textarea,
[tabindex]:not([tabindex="-1"]) and :not([disabled])), and when Tab would move
past the last element move focus to the first (and when Shift+Tab would move
before the first move focus to the last); ensure the handler is registered on
open and removed on close and that wrapperRef.current is focused as before (keep
lastActiveRef behavior intact).

@claytonlin1110 claytonlin1110 requested a review from yoyo837 March 17, 2026 07:04
@claytonlin1110
Copy link
Author

@yoyo837 Please review

@yoyo837
Copy link
Member

yoyo837 commented Mar 17, 2026

@yoyo837 About test issue, i could reproduce the same issue on master branch as well.

Let me fix that on master before merge this.

@yoyo837 yoyo837 requested a review from zombieJ March 17, 2026 08:55
@yoyo837
Copy link
Member

yoyo837 commented Mar 17, 2026

@aojunhao123 Please help to check this.

@claytonlin1110
Copy link
Author

@zombieJ Please check

@yoyo837
Copy link
Member

yoyo837 commented Mar 18, 2026

/rebase

@claytonlin1110 claytonlin1110 force-pushed the feat/image-preview-a11y branch from 824ecce to 46c5259 Compare March 18, 2026 02:46
@claytonlin1110
Copy link
Author

@yoyo837 rebased

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/Preview/Footer.tsx (1)

96-107: ⚠️ Potential issue | 🟠 Major

aria-label 不应直接使用内部标识。

Line 106 当前会读出 flipY/zoomOut 这类技术标识,读屏可理解性较差,建议映射为可读文案并保留兜底。

建议修复
+const operationAriaLabelMap: Record<OperationType, string> = {
+  prev: 'Previous image',
+  next: 'Next image',
+  flipY: 'Flip vertically',
+  flipX: 'Flip horizontally',
+  rotateLeft: 'Rotate left',
+  rotateRight: 'Rotate right',
+  zoomOut: 'Zoom out',
+  zoomIn: 'Zoom in',
+};
+
   const renderOperation = ({ type, disabled, onClick, icon }: RenderOperationParams) => {
     return (
       <button
         type="button"
         key={type}
@@
-        aria-label={type}
+        aria-label={operationAriaLabelMap[type] ?? 'Image action'}
       >
         {icon}
       </button>
     );
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Preview/Footer.tsx` around lines 96 - 107, The aria-label in
renderOperation currently uses the raw internal operation type (e.g., "flipY",
"zoomOut"), which is unfriendly for screen readers; change renderOperation to
map the incoming type (RenderOperationParams.type) to a human-readable label via
a lookup (e.g., an operationLabelMap or a getOperationLabel function) and use
that string for aria-label, keeping a safe fallback to the raw type when no
mapping exists; update any tests or usages that assume the old value and ensure
the mapping covers all known operation types.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/Preview/Footer.tsx`:
- Around line 96-107: The aria-label in renderOperation currently uses the raw
internal operation type (e.g., "flipY", "zoomOut"), which is unfriendly for
screen readers; change renderOperation to map the incoming type
(RenderOperationParams.type) to a human-readable label via a lookup (e.g., an
operationLabelMap or a getOperationLabel function) and use that string for
aria-label, keeping a safe fallback to the raw type when no mapping exists;
update any tests or usages that assume the old value and ensure the mapping
covers all known operation types.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 55ffa002-6c6f-4310-a5cc-c3f653ebd48a

📥 Commits

Reviewing files that changed from the base of the PR and between 824ecce and 46c5259.

⛔ Files ignored due to path filters (1)
  • tests/__snapshots__/basic.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (6)
  • assets/preview.less
  • src/Image.tsx
  • src/Preview/Footer.tsx
  • src/Preview/PrevNext.tsx
  • src/Preview/index.tsx
  • tests/preview.test.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/Preview/PrevNext.tsx
  • tests/preview.test.tsx
  • src/Preview/index.tsx

@codecov
Copy link

codecov bot commented Mar 18, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.44%. Comparing base (718a756) to head (64b4872).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #496      +/-   ##
==========================================
+ Coverage   99.41%   99.44%   +0.03%     
==========================================
  Files          17       17              
  Lines         511      541      +30     
  Branches      153      165      +12     
==========================================
+ Hits          508      538      +30     
  Misses          3        3              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@claytonlin1110
Copy link
Author

@yoyo837 please re-run CI

@claytonlin1110
Copy link
Author

@yoyo837 please run CI again

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Image: Improve accessibility

2 participants