fix(VirtualInput): onBlur not triggered on Android when long-pressing native input #7033
fix(VirtualInput): onBlur not triggered on Android when long-pressing native input #7033shuningYanger wants to merge 10 commits into
Conversation
Summary of ChangesHello, 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
🧠 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
Activity
Using Gemini Code AssistThe 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
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 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
|
📝 WalkthroughWalkthrough为 VirtualInput 添加了全面的单元测试以覆盖点击外部与焦点行为,并在 Changes
Estimated code review effort🎯 3 (中等) | ⏱️ ~25 分钟 诗篇
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Important Merge conflicts detected
✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
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. Comment Tip Migrating from UI to YAML configuration.Use the |
There was a problem hiding this comment.
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.
| 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 | ||
| } |
There was a problem hiding this comment.
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
}
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
src/components/virtual-input/tests/virtual-input.test.tsxsrc/components/virtual-input/use-click-outside.tsx
| 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 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 验证 effect 内是否使用 hasKeyboardProps 且依赖数组未包含它
rg -n "addEventListener\('click'.*hasKeyboardProps|removeEventListener\('click'.*hasKeyboardProps|\}, \[ref\]" src/components/virtual-input/use-click-outside.tsxRepository: ant-design/ant-design-mobile
Length of output: 260
🏁 Script executed:
cat -n src/components/virtual-input/use-click-outside.tsx | head -60Repository: 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.
| 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.
There was a problem hiding this comment.
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
📒 Files selected for processing (1)
src/components/virtual-input/use-click-outside.tsx
| handler: (event: MouseEvent) => void, | ||
| ref: React.RefObject<HTMLElement>, |
There was a problem hiding this comment.
🧩 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 -C2Repository: ant-design/ant-design-mobile
Length of output: 1080
🏁 Script executed:
cat -n src/components/virtual-input/use-click-outside.tsxRepository: ant-design/ant-design-mobile
Length of output: 2090
🏁 Script executed:
sed -n '145,155p' src/components/virtual-input/virtual-input.tsxRepository: ant-design/ant-design-mobile
Length of output: 220
🏁 Script executed:
rg "useClickOutside" src/components --type ts --type tsxRepository: ant-design/ant-design-mobile
Length of output: 99
🏁 Script executed:
rg "useClickOutside" src/componentsRepository: 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 -50Repository: 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.
| 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.

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:
📱 Reproduction Steps
Environment:
Steps:
field
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:
Summary by CodeRabbit
发布说明
Bug Fixes
Tests