Skip to content

fix: resolve react-number-format compatibility by refining compositionEnd handling#147

Open
oraziospada17 wants to merge 1 commit into
react-component:masterfrom
oraziospada17:fix/react-number-format-compat
Open

fix: resolve react-number-format compatibility by refining compositionEnd handling#147
oraziospada17 wants to merge 1 commit into
react-component:masterfrom
oraziospada17:fix/react-number-format-compat

Conversation

@oraziospada17
Copy link
Copy Markdown

@oraziospada17 oraziospada17 commented Nov 22, 2025

Fixes ant-design/ant-design#46999

Problem

The fix for #46587 (preventing double onChange firing with Chinese input) introduced a regression where compositionEnd events were completely skipped. This broke compatibility with libraries like react-number-format which rely on receiving these events to function correctly when wrapping the Input component.

Solution

Instead of early returning when source === 'compositionEnd', this PR modifies triggerChange to:

  1. Always update the internal state (setValue).
  2. Only trigger the external onChange callback if the value has actually changed.

This approach solves both issues:

  • Prevents double-firing of onChange (solving #46587) because the value won't change on the second trigger.
  • Allows react-number-format to work (solving #46999) because it now receives the necessary events.

Summary by CodeRabbit

发布说明

  • Bug Fixes

    • 修复输入组件在中文输入结束时的状态更新和事件触发,避免多余的 onChange 事件,提升光标与输入一致性。
    • 修正清除操作与非文件输入的值更新行为,确保界面显示与事件目标保持同步。
  • Tests

    • 新增针对 onChange 事件目标为已挂载输入元素的测试,覆盖清除、输入及格式化场景。

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 22, 2025

Warning

Rate limit exceeded

@afc163 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 29 minutes and 36 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4911d51d-75e5-4100-a00b-2f18de6ed94d

📥 Commits

Reviewing files that changed from the base of the PR and between 9904c82 and 428c817.

📒 Files selected for processing (3)
  • src/Input.tsx
  • src/utils/commonUtils.ts
  • tests/index.test.tsx

Walkthrough

修正 compositionEnd 分支以先更新内部值再返回;用 createNormalizedEvent 替换克隆事件以确保发出的 onChange.target/currentTarget 指向真实挂载的 input;新增测试验证事件目标为真实元素。

变更

Input 事件处理与规范化

Layer / File(s) Summary
compositionEnd 状态更新
src/Input.tsx
在 compositionEnd 情况下先调用 setValue(cutValue),然后直接返回,避免在 compositionEnd 期间触发 onChange
合成事件规范化实现
src/utils/commonUtils.ts
新增 createNormalizedEvent,在真实输入元素上设置 value 并返回以该元素为 target/currentTarget 的合成事件;resolveOnChange 在清除或强制 targetValue 情况下使用该函数发出事件。
事件目标测试
tests/index.test.tsx
新增 onChange event target 测试,断言 clear、常规输入及 count.exceedFormatter 场景中 onChangetarget/currentTarget 为已挂载的 <input>,并验证裁剪后的值。

序列图

sequenceDiagram
  participant User
  participant InputComponent
  participant createNormalizedEvent
  participant RealInputElement
  participant onChange

  User->>InputComponent: 点击清除 / 输入 / compositionEnd
  InputComponent->>createNormalizedEvent: 需要发出标准化事件 (origEvent, RealInputElement, targetValue)
  createNormalizedEvent->>RealInputElement: 设置 value 为 targetValue
  createNormalizedEvent-->>InputComponent: 返回 normalizedEvent (target/currentTarget -> RealInputElement)
  InputComponent->>onChange: 调用 onChange(normalizedEvent)
Loading

代码审查工作量估计

🎯 3 (Moderate) | ⏱️ ~20 minutes

可能相关的 PRs

  • react-component/input#81: 修改了 src/Input.tsx 的 compositionEnd 处理,与本次 compositionEnd 路径调整存在代码层面的重叠。

建议评审者

  • afc163
  • zombieJ

诗歌

🐰 我是读码的兔子,草地上轻跳跃
键入与合成终相和,事件指向真元素 🎯
compositionEnd 不再错乱,值先落后再查验
测试为证,心安如山,输入之路又平稳 ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 The title accurately describes the main change: fixing compositionEnd handling to resolve react-number-format compatibility issues, which is the primary focus of the PR.
Linked Issues check ✅ Passed The PR successfully addresses issue #46999 by restoring compositionEnd event delivery while preventing double onChange firing from #46587, enabling react-number-format compatibility.
Out of Scope Changes check ✅ Passed All changes directly support the PR objectives: Input.tsx updates compositionEnd handling, commonUtils.ts replaces cloneEvent with createNormalizedEvent for proper event targeting, and tests verify correct target behavior.

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

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

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


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.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @oraziospada17, 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 regression where compositionEnd events were being skipped, which broke compatibility with libraries like react-number-format. The solution refines the triggerChange mechanism to always update the internal input state, but critically, only triggers the external onChange callback if the input's value has actually changed. This approach effectively addresses both the react-number-format compatibility issue and the original problem of double onChange firing with complex input methods.

Highlights

  • Refined compositionEnd Handling: The early return for compositionEnd events within the triggerChange function has been removed, allowing the input processing to continue for these events.
  • Conditional onChange Triggering: The onChange callback is now only invoked if the input's value has genuinely changed (cutValue !== formatValue), preventing redundant calls and addressing the double-firing issue.
  • Compatibility Restoration: This change restores compatibility with external libraries like react-number-format by ensuring compositionEnd events are properly processed, while simultaneously maintaining the fix for double onChange firing with Chinese input.
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.

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 aims to fix a regression with react-number-format by ensuring compositionEnd events are processed, while still preventing the double onChange firing for IME input. The approach is to always update the internal state but only trigger onChange if the value has changed.

However, the implementation of the value check (cutValue !== formatValue) has a potential flaw. Due to React's event batching, formatValue may be stale across both compositionend and change event handlers, leading to onChange being fired twice. This could affect both uncontrolled and controlled components, re-introducing the bug the change is meant to solve. I've left a critical comment with a more detailed explanation.

Comment thread src/Input.tsx Outdated
Comment on lines 128 to 130
if (inputRef.current && cutValue !== formatValue) {
resolveOnChange(inputRef.current, e, onChange, cutValue);
}
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.

critical

This condition may not reliably prevent double onChange events during IME composition. Both the compositionend event and the subsequent change event can trigger triggerChange before the component re-renders. In this scenario, formatValue will hold the stale value from the previous render for both calls, causing this condition to be true twice and onChange to be fired twice.

This could re-introduce the issue this PR is trying to solve for compositionend scenarios (#46587). A useRef to track the last reported value within the event cycle might be a more robust solution to prevent the double-firing.

@svetlanapuro
Copy link
Copy Markdown

Hello,

Are there any updates on this PR? Will it be merged? 🙏

I really need this change to fix antd input and react-number-format. There aren't any other masking libraries that work well with antd input.

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.

Caution

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

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

104-127: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

compositionEnd 在 exceedFormatter 命中时仍会触发 onChange

当前 else if 结构会导致 info.source === 'compositionEnd' 且超长裁剪命中时跳过早返回,随后在 Line 132 继续触发 resolveOnChange,会把双触发问题带回这个分支。

建议修复
-    if (
+    if (
       !compositionRef.current &&
       countConfig.exceedFormatter &&
       countConfig.max &&
       countConfig.strategy(currentValue) > countConfig.max
     ) {
       cutValue = countConfig.exceedFormatter(currentValue, {
         max: countConfig.max,
       });

       if (currentValue !== cutValue) {
         setSelection([
           inputRef.current?.selectionStart || 0,
           inputRef.current?.selectionEnd || 0,
         ]);
       }
-    } else if (info.source === 'compositionEnd') {
+    }
+
+    if (info.source === 'compositionEnd') {
       // Always update internal state on compositionEnd, but skip onChange.
       // The browser fires an input/change event after compositionEnd, which
       // will call onInternalChange → triggerChange again and trigger onChange.
       // Skipping here prevents double-firing (`#46587`).
       setValue(cutValue);
       return;
     }
🤖 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/Input.tsx` around lines 104 - 127, The compositionEnd branch still allows
resolveOnChange to fire when exceedFormatter trimmed the value; update the logic
so that when countConfig.exceedFormatter runs and info.source ===
'compositionEnd' you apply the trimmed value and return early to avoid later
resolveOnChange calling onChange. Concretely, in the block that runs when
!compositionRef.current && countConfig.exceedFormatter … (where cutValue is
computed and setSelection is called), detect info.source === 'compositionEnd',
call setValue(cutValue) and return (same as the existing compositionEnd branch)
so resolveOnChange is skipped; keep references to compositionRef,
countConfig.exceedFormatter, cutValue, setSelection, setValue, and info.source
to locate the code.
🧹 Nitpick comments (1)
tests/index.test.tsx (1)

401-458: ⚡ Quick win

建议补一条 compositionEnd + exceedFormatter 的回归用例

现有新测试覆盖了 target 归一化,但还缺少 compositionEndexceedFormatter 同时发生时“只更新内部值、不额外触发 onChange”的断言。这个用例能直接兜住本 PR 的关键契约。

可参考的测试补充
 describe('onChange event target', () => {
+  it('should not emit extra onChange on compositionEnd when exceedFormatter is active', () => {
+    const onChange = jest.fn();
+    const { container } = render(
+      <Input
+        count={{ max: 3, exceedFormatter: (val) => val.slice(0, 3) }}
+        onChange={onChange}
+      />,
+    );
+    const input = container.querySelector('input')!;
+
+    fireEvent.compositionStart(input);
+    fireEvent.compositionEnd(input, { data: 'abcdef' });
+    fireEvent.change(input, { target: { value: 'abcdef' } });
+
+    expect(onChange).toHaveBeenCalledTimes(1);
+    expect(onChange.mock.calls[0][0].target.value).toBe('abc');
+  });
🤖 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 `@tests/index.test.tsx` around lines 401 - 458, Add a regression test that
verifies compositionEnd combined with exceedFormatter only updates internal
value and does not invoke onChange: in tests/index.test.tsx add an it block
(e.g., "compositionEnd with exceedFormatter should not trigger onChange") that
renders <Input count={{ max: 3, exceedFormatter: val => val.slice(0,3) }}
onChange={jest.fn()} />, selects the input element, simulate compositionstart
(optional), set the input value to a long string via fireEvent.change or by
setting value then fireEvent.compositionEnd, then assert onChange was not called
and the input.value is trimmed to 'abc' (i.e., internal value updated but no
onChange fired) to cover the compositionEnd + exceedFormatter contract for the
Input component.
🤖 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.

Outside diff comments:
In `@src/Input.tsx`:
- Around line 104-127: The compositionEnd branch still allows resolveOnChange to
fire when exceedFormatter trimmed the value; update the logic so that when
countConfig.exceedFormatter runs and info.source === 'compositionEnd' you apply
the trimmed value and return early to avoid later resolveOnChange calling
onChange. Concretely, in the block that runs when !compositionRef.current &&
countConfig.exceedFormatter … (where cutValue is computed and setSelection is
called), detect info.source === 'compositionEnd', call setValue(cutValue) and
return (same as the existing compositionEnd branch) so resolveOnChange is
skipped; keep references to compositionRef, countConfig.exceedFormatter,
cutValue, setSelection, setValue, and info.source to locate the code.

---

Nitpick comments:
In `@tests/index.test.tsx`:
- Around line 401-458: Add a regression test that verifies compositionEnd
combined with exceedFormatter only updates internal value and does not invoke
onChange: in tests/index.test.tsx add an it block (e.g., "compositionEnd with
exceedFormatter should not trigger onChange") that renders <Input count={{ max:
3, exceedFormatter: val => val.slice(0,3) }} onChange={jest.fn()} />, selects
the input element, simulate compositionstart (optional), set the input value to
a long string via fireEvent.change or by setting value then
fireEvent.compositionEnd, then assert onChange was not called and the
input.value is trimmed to 'abc' (i.e., internal value updated but no onChange
fired) to cover the compositionEnd + exceedFormatter contract for the Input
component.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d58026a4-3124-4a34-9fb3-993957f028f3

📥 Commits

Reviewing files that changed from the base of the PR and between 8b74fd6 and 9904c82.

📒 Files selected for processing (3)
  • src/Input.tsx
  • src/utils/commonUtils.ts
  • tests/index.test.tsx

@afc163 afc163 force-pushed the fix/react-number-format-compat branch from 9904c82 to 96eb6b8 Compare May 13, 2026 11:45
…ouble-firing (#46587)

- Replace cloneEvent (which uses cloneNode creating detached DOM nodes)
  with createNormalizedEvent that sets target.value directly and points
  target/currentTarget at the real mounted input element. This fixes
  document.contains(e.target) returning false for third-party wrappers
  like react-number-format.

- In triggerChange, use compositionEnd early-return with setValue to
  prevent double-firing of onChange. The browser's subsequent input
  event will handle the onChange call naturally.

- Add regression tests for onChange event target being the real mounted
  element across clear, normal typing, and exceedFormatter scenarios.
@afc163 afc163 force-pushed the fix/react-number-format-compat branch from 96eb6b8 to 428c817 Compare May 13, 2026 11:55
@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.30%. Comparing base (0215da5) to head (428c817).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #147      +/-   ##
==========================================
- Coverage   98.34%   98.30%   -0.05%     
==========================================
  Files          11       11              
  Lines         424      413      -11     
  Branches      137      134       -3     
==========================================
- Hits          417      406      -11     
  Misses          7        7              

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

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.

Antd Input: To use with react-number-format customInput feature.

2 participants