Skip to content

fix(VirtualInput): onBlur not triggered on Android when long-pressing native input #7033

Open
shuningYanger wants to merge 10 commits into
ant-design:masterfrom
shuningYanger:fix/input-blur
Open

fix(VirtualInput): onBlur not triggered on Android when long-pressing native input #7033
shuningYanger wants to merge 10 commits into
ant-design:masterfrom
shuningYanger:fix/input-blur

Conversation

@shuningYanger
Copy link
Copy Markdown
Contributor

@shuningYanger shuningYanger commented Mar 17, 2026

Description

🐛 Bug Description

When VirtualInput is focused (custom keyboard visible) on Android devices, if
the user long-presses a native element elsewhere on the page, the
system keyboard appears and allows input, but VirtualInput's onBlur callback is
never triggered.

This causes the custom keyboard to remain visible while the system keyboard is
also shown, creating a poor UX and potential layout conflicts.

🔍 Root Cause

The blur detection mechanism in useClickOutside only listens to click events:

document.addEventListener('click', handleClick, hasKeyboardProps);

On Android, long-press gestures do NOT fire click events:

  1. User long-presses a native input
  2. Touch sequence: touchstart → (hold threshold) → system takes over
  3. System keyboard appears, native input gains focus
  4. click event is cancelled by the system ❌
  5. useClickOutside never detects the interaction
  6. onBlur callback never fires

📱 Reproduction Steps

Environment:

  • Device: Android phone (any version)
  • Browser: Chrome/WebView

Steps:

  1. Open a page with VirtualInput (with custom keyboard) and a native
    field
  2. Tap VirtualInput to focus it → custom keyboard appears
  3. Long-press the native field (hold for ~500ms)
  4. System keyboard appears, you can type in the native input
  5. Bug: VirtualInput remains focused, custom keyboard still visible, onBlur
    never called

Expected: VirtualInput should blur and hide its custom keyboard when the native
input gains focus.

✅ Solution

Listen to focusin events in addition to click events. The focusin event:

  • Bubbles to document when any element gains focus
  • IS fired when Android system keyboard appears and focuses a native input
  • Provides the same event.target for contains() checking

Summary by CodeRabbit

发布说明

  • Bug Fixes

    • 改进了虚拟输入框的焦点管理与失焦处理,提升在外部元素获得焦点时的表现
    • 增强了 Android 长按场景下的焦点交互稳定性,避免重复触发聚焦行为
  • Tests

    • 添加了全面的自动化测试,覆盖点击、focusin、外部聚焦与动态处理器切换等交互场景,以保证行为一致性

@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Mar 17, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Prepare preview

@gemini-code-assist
Copy link
Copy Markdown
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 resolves a critical user experience bug on Android devices where the VirtualInput component's custom keyboard would remain visible even after a native input element gained focus via a long-press gesture. By extending the useClickOutside hook to monitor focusin events, the system now correctly detects when focus shifts away from the VirtualInput, ensuring the custom keyboard dismisses as expected and preventing UI conflicts.

Highlights

  • Bug Fix: Addressed an issue where VirtualInput's onBlur callback failed to trigger on Android when a native input was long-pressed, causing the custom keyboard to persist.
  • Root Cause Identified: Determined that Android long-press gestures do not fire click events, which the useClickOutside hook previously relied upon for blur detection.
  • Enhanced Blur Detection: Implemented focusin event listening within the useClickOutside hook to correctly detect when an external native input gains focus, thereby triggering the onBlur callback.
  • Comprehensive Testing: Introduced new test cases to validate the focusin event handling and ensure the onBlur callback functions as expected in various scenarios, including handler updates and internal/external focus changes.

🧠 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/components/virtual-input/tests/virtual-input.test.tsx
    • Added new test suites for useClickOutside to cover focusin event handling, ensuring onBlur is triggered correctly when external elements gain focus and not triggered when internal elements gain focus.
    • Included tests for handler updates and multiple clicks to ensure robust behavior.
  • src/components/virtual-input/use-click-outside.tsx
    • Modified the useClickOutside hook to subscribe to focusin events on the document, in addition to click events, to address blur detection issues on Android.
    • Integrated the useEvent hook to ensure a stable handler reference within the useEffect dependency array, preventing unnecessary re-renders and potential stale closures.
Activity
  • Implemented a fix for the VirtualInput blur issue on Android devices.
  • Added comprehensive unit tests for the useClickOutside hook, covering click and focusin event scenarios.
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
Copy Markdown

coderabbitai Bot commented Mar 17, 2026

📝 Walkthrough

Walkthrough

为 VirtualInput 添加了全面的单元测试以覆盖点击外部与焦点行为,并在 use-click-outside 钩子中引入稳定处理器包装与 focusin 监听器,调整事件绑定与依赖以改进外部点击/焦点失去的处理逻辑。

Changes

Cohort / File(s) Summary
VirtualInput 测试套件
src/components/virtual-input/tests/virtual-input.test.tsx
新增 252 行测试,覆盖 VirtualInput 与 useClickOutside 的各种场景:初次点击聚焦不触发 blur、外部点击触发 blur、处理器在聚焦期间切换时使用最新 handler、重复点击不重复触发聚焦、外部 focusin 导致 blur、内部元素 focusin 不触发 blur 等。
useClickOutside 实现优化
src/components/virtual-input/use-click-outside.tsx
引入 rc-util 的稳定处理器(stableHandler/useEvent)以确保处理器引用稳定;新增 focusin 事件处理以兼容(例如 Android 长按)场景;改为无条件附加/移除相关事件监听器;将 effect 依赖数组从包含 handler 调整为仅依赖 ref

Estimated code review effort

🎯 3 (中等) | ⏱️ ~25 分钟

诗篇

我是小兔,窜过输入园,
焦点一触,键盘跳然。
事件稳如树,外点亦归根,
代码小修补,欢跃又安然。 🐰✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确反映了拉取请求的主要目标——修复Android长按原生输入框时VirtualInput的onBlur事件未被触发的问题,与提供的文件改动和PR目标完全一致。

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

Important

Merge conflicts detected

  • Resolve merge conflict in branch fix/input-blur
✨ 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.

Tip

Migrating from UI to YAML configuration.

Use the @coderabbitai configuration command in a PR comment to get a dump of all your UI settings in YAML format. You can then edit this YAML file and upload it to the root of your repository to configure CodeRabbit programmatically.

@shuningYanger shuningYanger changed the title Fix/input blur fix(VirtualInput): onBlur not triggered on Android when long-pressing native input Mar 17, 2026
@dosubot dosubot Bot added the bug label Mar 17, 2026
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 effectively addresses a bug on Android devices where the VirtualInput fails to blur. The solution, which involves listening for focusin events in the useClickOutside hook, is appropriate and well-implemented. The accompanying tests are thorough and cover the new logic effectively. I have one suggestion to improve the type safety within the use-click-outside.tsx file.

Comment on lines 5 to 43
function useClickOutside(
handler: (event: MouseEvent) => void,
ref: React.RefObject<HTMLElement>,
hasKeyboardProps: boolean = false
hasKeyboardProps = false
) {
const stableHandler = useEvent(handler)

useEffect(() => {
function handleClick(event: MouseEvent) {
if (!ref.current || ref.current.contains(event.target as Node)) {
return
}
handler(event)
stableHandler(event) // 使用 ref 中的 handler
}

// 向前兼容逻辑:
// 1. 对于有键盘属性的 VirtualInput,在捕获阶段监听:
// 这是为了确保在事件被阻止传播之前触发。比如输入框中的单个数字 click 事件会 stopPropagation, 但这里依然能捕获到
// 2. 对于无键盘属性的 VirtualInput 组件,在冒泡阶段监听:
// 这种情况通常是 VirtualInput + NumberKeyboard 为兄弟关系,在以前版本中点击 NumberKeyboard **不会**触发 VirtualInput 的 blur 事件
// 原先原理:通过 NumberKeyboard 内部 onMouseDown 时 preventDefault 阻止的 VirtualInput 内原生的 blur 事件
// 新的原理:NumberKeyboard 的 Popup 默认会 stopPropagation click, 这里在冒泡阶段监听不到,不会调用 VirtualInput 的 onBlur 回调(非原生事件)。
document.addEventListener(
'click',
handleClick,
hasKeyboardProps ? true : false
)

// 安卓长按 native input 时不会触发 click 事件,通过监听 focusin 补充处理:
// 长按后系统键盘弹出,native input 获得焦点并冒泡 focusin 到 document,以此触发 blur
function handleFocusIn(event: FocusEvent) {
if (!ref.current || ref.current.contains(event.target as Node)) {
return
}
stableHandler(event as unknown as MouseEvent)
}

document.addEventListener('click', handleClick, hasKeyboardProps)
document.addEventListener('focusin', handleFocusIn)
return () => {
document.removeEventListener(
'click',
handleClick,
hasKeyboardProps ? true : false
)
document.removeEventListener('click', handleClick, hasKeyboardProps)
document.removeEventListener('focusin', handleFocusIn)
}
}, [handler, ref])
}, [ref]) // 只依赖 ref,不依赖 handler
}
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

The type cast event as unknown as MouseEvent for a FocusEvent is unsafe and suggests a type mismatch. Since the handler's implementation in virtual-input.tsx doesn't utilize the event object, it would be safer and clearer to modify the hook's signature to accept a handler with no arguments, such as handler: () => void. This change would eliminate the need for the unsafe type cast and make the hook's intended use more explicit.

function useClickOutside(
  handler: () => void,
  ref: React.RefObject<HTMLElement>,
  hasKeyboardProps = false
) {
  const stableHandler = useEvent(handler)

  useEffect(() => {
    function handleClick(event: MouseEvent) {
      if (!ref.current || ref.current.contains(event.target as Node)) {
        return
      }
      stableHandler() // 使用 ref 中的 handler
    }
    // 向前兼容逻辑:
    // 1. 对于有键盘属性的 VirtualInput,在捕获阶段监听:
    //      这是为了确保在事件被阻止传播之前触发。比如输入框中的单个数字 click 事件会 stopPropagation, 但这里依然能捕获到
    // 2. 对于无键盘属性的 VirtualInput 组件,在冒泡阶段监听:
    //      这种情况通常是 VirtualInput + NumberKeyboard 为兄弟关系,在以前版本中点击 NumberKeyboard **不会**触发 VirtualInput 的 blur 事件
    //      原先原理:通过 NumberKeyboard 内部 onMouseDown 时 preventDefault 阻止的 VirtualInput 内原生的 blur 事件
    //      新的原理:NumberKeyboard 的 Popup 默认会 stopPropagation click, 这里在冒泡阶段监听不到,不会调用 VirtualInput 的 onBlur 回调(非原生事件)。

    // 安卓长按 native input 时不会触发 click 事件,通过监听 focusin 补充处理:
    // 长按后系统键盘弹出,native input 获得焦点并冒泡 focusin 到 document,以此触发 blur
    function handleFocusIn(event: FocusEvent) {
      if (!ref.current || ref.current.contains(event.target as Node)) {
        return
      }
      stableHandler()
    }

    document.addEventListener('click', handleClick, hasKeyboardProps)
    document.addEventListener('focusin', handleFocusIn)
    return () => {
      document.removeEventListener('click', handleClick, hasKeyboardProps)
      document.removeEventListener('focusin', handleFocusIn)
    }
  }, [ref]) // 只依赖 ref,不依赖 handler
}

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

🧹 Nitpick comments (1)
src/components/virtual-input/use-click-outside.tsx (1)

6-7: 统一事件类型契约,移除不必要的类型强转。

Line 33 的 as unknown as MouseEvent 强转会掩盖真实的事件类型。虽然当前 handler 不访问事件属性,但为保证类型安全,应将 handler 类型改为联合类型并直接传参,避免后续维护时误用鼠标专有字段。

建议修改
 function useClickOutside(
-  handler: (event: MouseEvent) => void,
+  handler: (event: MouseEvent | FocusEvent) => void,
   ref: React.RefObject<HTMLElement>,
   hasKeyboardProps = false
 ) {
   const stableHandler = useEvent(handler)
   
   useEffect(() => {
     function handleFocusIn(event: FocusEvent) {
       if (!ref.current || ref.current.contains(event.target as Node)) {
         return
       }
-      stableHandler(event as unknown as MouseEvent)
+      stableHandler(event)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/virtual-input/use-click-outside.tsx` around lines 6 - 7, The
handler parameter is currently typed as (event: MouseEvent) => void and callers
cast events with `as unknown as MouseEvent`; change the handler signature to
accept a union of possible DOM event types (e.g. MouseEvent | TouchEvent |
PointerEvent) in the use-click-outside hook, update the internal listener to
accept Event (or Event | TouchEvent | PointerEvent) and pass the event directly
to handler without the `as unknown as MouseEvent` cast, and keep the ref type as
React.RefObject<HTMLElement>—this removes the unsafe cast and preserves proper
type coverage for the handler and internal listener.
🤖 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/components/virtual-input/use-click-outside.tsx`:
- Around line 36-42: Effect currently binds and removes the click listener using
hasKeyboardProps but does not list hasKeyboardProps in the useEffect dependency
array, so when hasKeyboardProps changes the listener's capture/bubble phase
isn't updated; update the dependency array to include hasKeyboardProps
(alongside ref) so the effect re-runs and rebinds/remove listeners with the
correct phase, ensuring the addEventListener/removeEventListener calls that
reference handleClick and hasKeyboardProps are always paired using the same
hasKeyboardProps value.

---

Nitpick comments:
In `@src/components/virtual-input/use-click-outside.tsx`:
- Around line 6-7: The handler parameter is currently typed as (event:
MouseEvent) => void and callers cast events with `as unknown as MouseEvent`;
change the handler signature to accept a union of possible DOM event types (e.g.
MouseEvent | TouchEvent | PointerEvent) in the use-click-outside hook, update
the internal listener to accept Event (or Event | TouchEvent | PointerEvent) and
pass the event directly to handler without the `as unknown as MouseEvent` cast,
and keep the ref type as React.RefObject<HTMLElement>—this removes the unsafe
cast and preserves proper type coverage for the handler and internal listener.
🪄 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: 911fa522-cb5c-4bb2-9ac5-d5877c8038df

📥 Commits

Reviewing files that changed from the base of the PR and between efdd755 and ad7ac6a.

📒 Files selected for processing (2)
  • src/components/virtual-input/tests/virtual-input.test.tsx
  • src/components/virtual-input/use-click-outside.tsx

Comment on lines +36 to +42
document.addEventListener('click', handleClick, hasKeyboardProps)
document.addEventListener('focusin', handleFocusIn)
return () => {
document.removeEventListener(
'click',
handleClick,
hasKeyboardProps ? true : false
)
document.removeEventListener('click', handleClick, hasKeyboardProps)
document.removeEventListener('focusin', handleFocusIn)
}
}, [handler, ref])
}, [ref]) // 只依赖 ref,不依赖 handler
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 验证 effect 内是否使用 hasKeyboardProps 且依赖数组未包含它
rg -n "addEventListener\('click'.*hasKeyboardProps|removeEventListener\('click'.*hasKeyboardProps|\}, \[ref\]" src/components/virtual-input/use-click-outside.tsx

Repository: ant-design/ant-design-mobile

Length of output: 260


🏁 Script executed:

cat -n src/components/virtual-input/use-click-outside.tsx | head -60

Repository: ant-design/ant-design-mobile

Length of output: 2054


hasKeyboardProps 添加到 effect 依赖数组,避免监听阶段失效。

第 36 行与第 39 行使用 hasKeyboardProps 控制事件监听的阶段(捕获阶段或冒泡阶段),但第 42 行的依赖数组中未包含它。当 hasKeyboardProps 在组件生命周期内变化时,监听器不会按新的阶段重绑,导致监听行为与期望不符。

建议修改
-  }, [ref]) // 只依赖 ref,不依赖 handler
+  }, [ref, hasKeyboardProps]) // 当 hasKeyboardProps 变化时重新绑定监听
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
document.addEventListener('click', handleClick, hasKeyboardProps)
document.addEventListener('focusin', handleFocusIn)
return () => {
document.removeEventListener(
'click',
handleClick,
hasKeyboardProps ? true : false
)
document.removeEventListener('click', handleClick, hasKeyboardProps)
document.removeEventListener('focusin', handleFocusIn)
}
}, [handler, ref])
}, [ref]) // 只依赖 ref,不依赖 handler
document.addEventListener('click', handleClick, hasKeyboardProps)
document.addEventListener('focusin', handleFocusIn)
return () => {
document.removeEventListener('click', handleClick, hasKeyboardProps)
document.removeEventListener('focusin', handleFocusIn)
}
}, [ref, hasKeyboardProps]) // 当 hasKeyboardProps 变化时重新绑定监听
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/virtual-input/use-click-outside.tsx` around lines 36 - 42,
Effect currently binds and removes the click listener using hasKeyboardProps but
does not list hasKeyboardProps in the useEffect dependency array, so when
hasKeyboardProps changes the listener's capture/bubble phase isn't updated;
update the dependency array to include hasKeyboardProps (alongside ref) so the
effect re-runs and rebinds/remove listeners with the correct phase, ensuring the
addEventListener/removeEventListener calls that reference handleClick and
hasKeyboardProps are always paired using the same hasKeyboardProps value.

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

♻️ Duplicate comments (1)
src/components/virtual-input/use-click-outside.tsx (1)

36-42: ⚠️ Potential issue | 🟠 Major

hasKeyboardProps 变化时监听阶段不会更新(重复问题,仍未修复)。

Line [36]-[40]hasKeyboardProps 决定监听阶段,但 Line [42] 依赖仅 [ref],当该值变化时 effect 不会重绑监听。

建议修改
-  }, [ref]) // 只依赖 ref,不依赖 handler
+  }, [ref, hasKeyboardProps]) // hasKeyboardProps 变化时重绑监听
#!/bin/bash
set -euo pipefail

# 验证监听选项使用了 hasKeyboardProps,但 effect 依赖未包含它
rg -n "addEventListener\\('click'.*hasKeyboardProps|addEventListener\\('focusin'.*hasKeyboardProps|removeEventListener\\('click'.*hasKeyboardProps|removeEventListener\\('focusin'.*hasKeyboardProps|\\}, \\[ref\\]\\)" src/components/virtual-input/use-click-outside.tsx

期望结果:能同时看到 hasKeyboardProps 被用于事件绑定/解绑,以及依赖数组仍是 [ref]

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

In `@src/components/virtual-input/use-click-outside.tsx` around lines 36 - 42, The
effect adds/removes document listeners using the hasKeyboardProps option but the
dependency array only lists [ref], so changes to hasKeyboardProps won't rebind
listeners; update the useEffect dependency list to include hasKeyboardProps (and
any handlers used like handleClick and handleFocusIn if they are not stable) so
that when hasKeyboardProps changes the effect will remove and re-add listeners
with the correct capture option; specifically adjust the effect surrounding
document.addEventListener('click', handleClick, hasKeyboardProps) and
document.addEventListener('focusin', handleFocusIn, hasKeyboardProps) to depend
on hasKeyboardProps (and stable handler references) to ensure correct
unbind/rebind behavior.
🤖 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/components/virtual-input/use-click-outside.tsx`:
- Around line 6-7: The handler passed into useClickOutside is currently typed as
(event: MouseEvent) => void while handleFocusIn receives a FocusEvent and is
being force-cast (event as unknown as MouseEvent); change the handler signature
to (event: MouseEvent | FocusEvent) => void in useClickOutside and update any
call sites (e.g., where handleFocusIn is attached) to accept the union type,
then remove the unsafe double-cast in handleFocusIn so you pass the actual
FocusEvent through without coercion.

---

Duplicate comments:
In `@src/components/virtual-input/use-click-outside.tsx`:
- Around line 36-42: The effect adds/removes document listeners using the
hasKeyboardProps option but the dependency array only lists [ref], so changes to
hasKeyboardProps won't rebind listeners; update the useEffect dependency list to
include hasKeyboardProps (and any handlers used like handleClick and
handleFocusIn if they are not stable) so that when hasKeyboardProps changes the
effect will remove and re-add listeners with the correct capture option;
specifically adjust the effect surrounding document.addEventListener('click',
handleClick, hasKeyboardProps) and document.addEventListener('focusin',
handleFocusIn, hasKeyboardProps) to depend on hasKeyboardProps (and stable
handler references) to ensure correct unbind/rebind behavior.
🪄 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: 5eac428b-e645-49c5-9771-ae16ae6bd85e

📥 Commits

Reviewing files that changed from the base of the PR and between ad7ac6a and 8b1c7fa.

📒 Files selected for processing (1)
  • src/components/virtual-input/use-click-outside.tsx

Comment on lines 6 to 7
handler: (event: MouseEvent) => void,
ref: React.RefObject<HTMLElement>,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 验证 useClickOutside 的调用点是否依赖 MouseEvent 专有字段
rg -nP "useClickOutside\\s*\\(" src/components -C3
rg -nP "\\bevent\\.(button|clientX|clientY|pageX|pageY|screenX|screenY)\\b" src/components/virtual-input -C2

Repository: ant-design/ant-design-mobile

Length of output: 1080


🏁 Script executed:

cat -n src/components/virtual-input/use-click-outside.tsx

Repository: ant-design/ant-design-mobile

Length of output: 2090


🏁 Script executed:

sed -n '145,155p' src/components/virtual-input/virtual-input.tsx

Repository: ant-design/ant-design-mobile

Length of output: 220


🏁 Script executed:

rg "useClickOutside" src/components --type ts --type tsx

Repository: ant-design/ant-design-mobile

Length of output: 99


🏁 Script executed:

rg "useClickOutside" src/components

Repository: ant-design/ant-design-mobile

Length of output: 489


🏁 Script executed:

sed -n '/describe.*useClickOutside/,/^describe/p' src/components/virtual-input/tests/virtual-input.test.tsx | head -50

Repository: ant-design/ant-design-mobile

Length of output: 1342


改进 useClickOutside 的事件类型处理,消除类型不安全的强制转换。

第 33 行的双重断言 event as unknown as MouseEvent 存在类型安全风险。handleFocusIn 接收 FocusEvent 但被强制转为 MouseEvent,会掩盖实际的事件类型。建议将 handler 的类型改为联合类型 (event: MouseEvent | FocusEvent) => void,去掉强制转换。

建议修改
 function useClickOutside(
-  handler: (event: MouseEvent) => void,
+  handler: (event: MouseEvent | FocusEvent) => void,
   ref: React.RefObject<HTMLElement>,
   hasKeyboardProps = false
 ) {
     function handleFocusIn(event: FocusEvent) {
       if (!ref.current || ref.current.contains(event.target as Node)) {
         return
       }
-      stableHandler(event as unknown as MouseEvent)
+      stableHandler(event)
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
handler: (event: MouseEvent) => void,
ref: React.RefObject<HTMLElement>,
handler: (event: MouseEvent | FocusEvent) => void,
ref: React.RefObject<HTMLElement>,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/virtual-input/use-click-outside.tsx` around lines 6 - 7, The
handler passed into useClickOutside is currently typed as (event: MouseEvent) =>
void while handleFocusIn receives a FocusEvent and is being force-cast (event as
unknown as MouseEvent); change the handler signature to (event: MouseEvent |
FocusEvent) => void in useClickOutside and update any call sites (e.g., where
handleFocusIn is attached) to accept the union type, then remove the unsafe
double-cast in handleFocusIn so you pass the actual FocusEvent through without
coercion.

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

Labels

bug size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant