Skip to content

feat(swipe-action): 优化滑动操作组件#7051

Open
Passing-of-A-Dream wants to merge 4 commits into
ant-design:masterfrom
Passing-of-A-Dream:fix-swipe-action-a11y
Open

feat(swipe-action): 优化滑动操作组件#7051
Passing-of-A-Dream wants to merge 4 commits into
ant-design:masterfrom
Passing-of-A-Dream:fix-swipe-action-a11y

Conversation

@Passing-of-A-Dream
Copy link
Copy Markdown
Contributor

@Passing-of-A-Dream Passing-of-A-Dream commented May 22, 2026

close #6602 修复无障碍聚焦时的不正常打开导致无法关闭的问题

Summary by CodeRabbit

  • 功能改进

    • 增加可按侧打开的可编程接口,展开时触发回调,提升控制灵活性。
    • 优化外部交互关闭逻辑,新增触摸与焦点移出监听以提高关闭一致性。
    • 左/右操作项获得焦点时会自动展开对应侧栏,改善键盘导航与可访问性体验。
  • 测试

    • 新增聚焦展开与焦点移出关闭的交互用例,覆盖对应行为验证。

Review Change Stack

- 提取打开侧边栏逻辑到独立函数
- 增加焦点事件处理外部点击关闭
- 添加焦点时自动展开侧边栏
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. feature labels May 22, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

📝 Walkthrough

Walkthrough

为 SwipeAction 组件新增内部 open(side) 统一展开逻辑,重构外部关闭监听以同时处理 touchstart 与 focusout,并在左右 actions 上添加 onFocus 以支持键盘/无障碍触发展开;补充对应测试。

变更

SwipeAction 无障碍和交互改进

Layer / File(s) Summary
打开方法和命令式 API 统一
src/components/swipe-action/swipe-action.tsx
新增 open(side),根据侧别启动动画到左侧 getLeftWidth() 或右侧 -getRightWidth() 并触发 onActionsRevealuseImperativeHandleshow 改为调用 open(side)
外部交互关闭逻辑重构
src/components/swipe-action/swipe-action.tsx
closeOnTouchOutside 由单一 touchstart 监听改为同时监听 documenttouchstart 与组件根节点的 focusout;当位置非 0 且事件/相关焦点目标不在组件内时调用 close(),清理时移除两类监听器。
焦点驱动的侧滑打开
src/components/swipe-action/swipe-action.tsx
左右 actions 容器新增 onFocus:当焦点进入且当前 x 未达到对应侧完全打开位时,分别调用 open('left') / open('right')
测试:焦点展开与 focusout 收起
src/components/swipe-action/tests/swipe-action.test.tsx
调整导入以包含 act,新增用例验证对右侧动作按钮执行 focus 会触发展开并调用 onActionsReveal('right'),对根节点触发 focusOut 后收起。

估计审查工作量

🎯 3 (Moderate) | ⏱️ ~20 分钟

建议标签

lgtm

建议审查者

  • zombieJ

🐰 轻跳指尖去探寻,侧滑门扉悄然开,
焦点如灯摇步进,键盘也能来相陪,
触屏与焦点两相顾,关合有序不紊乱,
小兔庆祝新逻辑,交互更亲切可爱。

🚥 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 PR标题清晰准确地反映了主要改动——优化滑动操作组件,特别是针对无障碍功能的改进。
Linked Issues check ✅ Passed 代码改动完整实现了issue #6602的需求:通过焦点触发打开SwipeAction,以及通过focusout事件实现关闭功能,确保无障碍交互正常工作。
Out of Scope Changes check ✅ Passed 所有改动都与无障碍焦点交互相关,包括open()方法、focusout监听、onFocus事件处理及对应测试用例,完全在issue #6602的范围内

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot requested a review from zombieJ May 22, 2026 01:30
Copy link
Copy Markdown
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 refactors the SwipeAction component by introducing a centralized open function and enhancing focus management. Key changes include adding a focusout listener to close the actions when focus leaves the component and implementing onFocus handlers on action containers to ensure they remain open when interacted with. Review feedback focused on improving the robustness of these changes by suggesting the use of x.goal instead of x.get() to avoid redundant animation calls and identifying a missing dependency (props.onClose) in a useEffect hook.

Comment on lines 178 to 200
useEffect(() => {
if (!props.closeOnTouchOutside) return
function handle(e: Event) {
if (x.get() === 0) {
return
const root = rootRef.current
if (!root) return
function onTouchOutside(e: Event) {
if (x.get() === 0) return
if (!root.contains(e.target as Node)) {
close()
}
const root = rootRef.current
if (root && !root.contains(e.target as Node)) {
}
function onFocusOutside(e: FocusEvent) {
if (x.get() === 0) return
if (!root.contains(e.relatedTarget as Node)) {
close()
}
}
document.addEventListener('touchstart', handle)
document.addEventListener('touchstart', onTouchOutside)
root.addEventListener('focusout', onFocusOutside)
return () => {
document.removeEventListener('touchstart', handle)
document.removeEventListener('touchstart', onTouchOutside)
root.removeEventListener('focusout', onFocusOutside)
}
}, [props.closeOnTouchOutside])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

useEffect 钩子在依赖数组中缺少 props.onClose。由于事件监听器调用了 close(),而 close() 内部使用了 props.onClose,如果 onClose 发生变化,监听器可能会捕获到旧的回调版本。此外,建议在监听器中使用 x.goal 代替 x.get()x.goal 代表动画的目标值,这可以避免在动画过程中触发冗余的调用,使逻辑更加健壮。

    useEffect(() => {
      if (!props.closeOnTouchOutside) return
      const root = rootRef.current
      if (!root) return
      function onTouchOutside(e: Event) {
        if (x.goal === 0) return
        if (!root.contains(e.target as Node)) {
          close()
        }
      }
      function onFocusOutside(e: FocusEvent) {
        if (x.goal === 0) return
        if (!root.contains(e.relatedTarget as Node)) {
          close()
        }
      }
      document.addEventListener('touchstart', onTouchOutside)
      root.addEventListener('focusout', onFocusOutside)
      return () => {
        document.removeEventListener('touchstart', onTouchOutside)
        root.removeEventListener('focusout', onFocusOutside)
      }
    }, [props.closeOnTouchOutside, props.onClose])

Comment on lines +243 to +245
onFocus={() => {
if (x.get() !== getLeftWidth()) open('left')
}}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

建议使用 x.goal 代替 x.get() 来判断组件是否已经处于打开或正在打开的状态。这可以避免在动画执行过程中重复触发 open 调用以及 onActionsReveal 回调。

Suggested change
onFocus={() => {
if (x.get() !== getLeftWidth()) open('left')
}}
onFocus={() => {
if (x.goal !== getLeftWidth()) open('left')
}}

Comment on lines +275 to +277
onFocus={() => {
if (x.get() !== -getRightWidth()) open('right')
}}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

建议使用 x.goal 代替 x.get()。这可以确保一致性,并避免在同一个操作容器内的元素之间移动焦点时触发不必要的动画启动。

Suggested change
onFocus={() => {
if (x.get() !== -getRightWidth()) open('right')
}}
onFocus={() => {
if (x.goal !== -getRightWidth()) open('right')
}}

Copy link
Copy Markdown

@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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/swipe-action/swipe-action.tsx`:
- Around line 180-199: The callbacks onTouchOutside and onFocusOutside capture
rootRef.current which TypeScript still treats as possibly null causing TS2531;
fix by reading and asserting the non-null root once before registering handlers
(e.g., const root = rootRef.current! after the early null-return) and use that
local non-null root inside onTouchOutside/onFocusOutside and when removing
listeners, ensuring the same root reference is used for
addEventListener/removeEventListener on document/root and avoiding nullable
access in those closures (referencing rootRef, root, onTouchOutside,
onFocusOutside, x.get(), close).
🪄 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: e8c12453-6c17-4f54-90cf-9f2e1a0bb6d4

📥 Commits

Reviewing files that changed from the base of the PR and between 09966e7 and 6654b1e.

📒 Files selected for processing (1)
  • src/components/swipe-action/swipe-action.tsx

Comment thread src/components/swipe-action/swipe-action.tsx
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 22, 2026

npm i https://pkg.pr.new/antd-mobile@7051

commit: 853e4b7

@codecov
Copy link
Copy Markdown

codecov Bot commented May 22, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.97%. Comparing base (09966e7) to head (853e4b7).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #7051      +/-   ##
==========================================
+ Coverage   92.94%   92.97%   +0.02%     
==========================================
  Files         337      337              
  Lines        7386     7398      +12     
  Branches     1868     1885      +17     
==========================================
+ Hits         6865     6878      +13     
+ Misses        485      484       -1     
  Partials       36       36              

☔ 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.

- 将 `x.get()` 替换为 `x.goal` 进行状态判断
- 修复焦点和触摸外部事件的条件检查
@coderabbitai coderabbitai Bot added the lgtm This PR has been approved by a maintainer label May 22, 2026
- 测试焦点在操作按钮上时打开滑动动作
- 测试焦点移出时关闭滑动动作
Copy link
Copy Markdown

@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/components/swipe-action/tests/swipe-action.test.tsx (1)

299-312: 💤 Low value

可选:为测试失败时提供更清晰的错误信息。

当前代码使用类型断言(line 301 as HTMLElement)和非空断言(line 306、312 的 !),如果元素未找到,测试失败信息可能不够明确。可以考虑添加显式的存在性断言以便更快定位问题:

const rightButton = container.querySelector(
  `.${classPrefix}-actions-right button`
)
expect(rightButton).not.toBeNull()

act(() => {
  rightButton!.focus()
})

const track = container.querySelector(`.${classPrefix}-track`)
expect(track).not.toBeNull()

不过鉴于这是测试代码且元素结构由组件保证,当前写法也是可接受的实践。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/swipe-action/tests/swipe-action.test.tsx` around lines 299 -
312, The test currently uses type/non-null assertions on DOM queries
(rightButton, track, root) which can yield unclear failures; add explicit
existence assertions before using them: after querying `rightButton`, `track`,
and `root` (via container.querySelector with `.${classPrefix}-actions-right
button`, `.${classPrefix}-track`, and `.${classPrefix}`) call
expect(...).not.toBeNull() (or expect(...).toBeInTheDocument()) and then safely
cast/use them (e.g., call focus or check styles) so failures clearly indicate
which element was missing; keep the existing assertions for `onActionsReveal`
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/components/swipe-action/tests/swipe-action.test.tsx`:
- Around line 299-312: The test currently uses type/non-null assertions on DOM
queries (rightButton, track, root) which can yield unclear failures; add
explicit existence assertions before using them: after querying `rightButton`,
`track`, and `root` (via container.querySelector with
`.${classPrefix}-actions-right button`, `.${classPrefix}-track`, and
`.${classPrefix}`) call expect(...).not.toBeNull() (or
expect(...).toBeInTheDocument()) and then safely cast/use them (e.g., call focus
or check styles) so failures clearly indicate which element was missing; keep
the existing assertions for `onActionsReveal` unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 724bb398-4afd-418d-bf64-7a5941e91a75

📥 Commits

Reviewing files that changed from the base of the PR and between 6b474e6 and 853e4b7.

📒 Files selected for processing (1)
  • src/components/swipe-action/tests/swipe-action.test.tsx

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

Labels

feature lgtm This PR has been approved by a maintainer size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

无障碍Tab按钮触发SwipeAction侧滑后,无法收回

1 participant