Skip to content

fix: use committed count value during IME composition#179

Closed
ZQDesigned wants to merge 2 commits into
react-component:masterfrom
ZQDesigned:fix/ime-show-count
Closed

fix: use committed count value during IME composition#179
ZQDesigned wants to merge 2 commits into
react-component:masterfrom
ZQDesigned:fix/ime-show-count

Conversation

@ZQDesigned

@ZQDesigned ZQDesigned commented Jun 10, 2026

Copy link
Copy Markdown

Summary

This change makes showCount derive from a committed count value during IME composition instead of the intermediate phonetic text.

Related Issue

Root Cause

Input and TextArea updated their count display from the live input value on every change event. During CJK IME composition, that meant showCount counted intermediate pinyin text like zhu instead of the committed character like .

Changes

  • add a committed count value for Input
  • add a committed count value for TextArea
  • keep showCount and count-based out-of-range state aligned with the committed value during IME composition
  • add regression tests for Input and TextArea

Verification

  • pnpm test -- --runInBand tests/count.test.tsx tests/TextArea.count.test.tsx
  • pnpm test -- --runInBand

Summary by CodeRabbit

发布说明

  • Bug Fixes

    • 修复输入框和文本域在输入法组合输入期间的计数显示不准确问题,计数现在基于已提交的值而非进行中的中间文本
  • Tests

    • 新增输入法输入期间计数显示的验证测试用例

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

概览

该 PR 为 Input 和 TextArea 组件的计数显示(showCount)添加了 IME(输入法编辑器)感知能力。引入独立的 countValue 状态跟踪已提交值,在输入法合成期间将计数显示与中间文本隔离,同时通过新增测试验证 IME 组合流程中的计数行为正确性。

更改

IME 感知的计数值状态管理

Layer / File(s) 说明
Input 组件计数状态与同步逻辑
src/Input.tsx
新增 countValue 状态,useCountDisplay 改为基于 countValue 计算数值;添加 useEffectformatValue 变更且非 composition 状态时同步更新 countValuetriggerChange 中于非 composition 时更新 countValueonCompositionStart 中将 countValue 固定为 formatValue
TextArea 组件计数状态与同步逻辑
src/TextArea.tsx
与 Input 组件采用相同的计数值管理策略,通过独立状态在 IME 组合期间保护计数显示,避免中间合成文本干扰。
IME 组合输入计数行为测试
tests/TextArea.count.test.tsx, tests/count.test.tsx
为 Input 和 TextArea 分别新增测试用例,验证在 compositionStart/change/compositionEnd 全流程中,计数正确基于已提交值计算。

🎯 3 (Moderate) | ⏱️ ~25 minutes

建议审阅人

  • zombieJ

🐰 妙哉 IME 之妙,
中文输入莫烦恼,
计数值稳如磐石,
已提交值永为矣,
合成中间不作妖! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Pull request title clearly summarizes the main change: fixing showCount to use committed value during IME composition rather than intermediate text.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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.

@ZQDesigned ZQDesigned marked this pull request as ready for review June 10, 2026 18:00
Copilot AI review requested due to automatic review settings June 10, 2026 18:00

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

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.

Code Review

This pull request updates the Input and TextArea components to ensure that the character count display reflects the committed value during IME composition instead of intermediate phonetic text, and adds corresponding unit tests. The reviewer suggests a refactoring to simplify state management: instead of maintaining a separate countValue state synced via useEffect (which can cause unnecessary double-renders), the components should track only the value at the start of composition (compositionStartValue) and derive the display count value directly during render.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/Input.tsx
Comment on lines +63 to +65
// Count-related state should reflect the committed value during IME composition
// instead of the intermediate phonetic text.
const [countValue, setCountValue] = useState(formatValue);

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

Instead of maintaining a separate countValue state and syncing it with formatValue via useEffect, we can derive countValue directly during render. By tracking only the compositionStartValue (the value when IME composition starts), we can simplify the state logic, eliminate the useEffect entirely, and avoid potential double-renders or out-of-sync issues.

Suggested change
// Count-related state should reflect the committed value during IME composition
// instead of the intermediate phonetic text.
const [countValue, setCountValue] = useState(formatValue);
// Track the value at the start of IME composition to reflect the committed value
// during composition instead of the intermediate phonetic text.
const [compositionStartValue, setCompositionStartValue] = useState<string | null>(null);
const countValue = compositionStartValue !== null ? compositionStartValue : formatValue;

Comment thread src/Input.tsx
Comment on lines +105 to +109
useEffect(() => {
if (!compositionRef.current) {
setCountValue(formatValue);
}
}, [formatValue]);

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

With the derived countValue approach, this useEffect is no longer needed, which prevents unnecessary double-renders when formatValue changes.

Comment thread src/Input.tsx
Comment on lines 123 to +128
return;
}
setValue(cutValue);
if (!compositionRef.current) {
setCountValue(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.

medium

Update triggerChange to reset compositionStartValue to null when composition ends. Note that we also reset it before the early return to ensure the count display is correctly updated even if the change event is skipped to avoid duplicate triggers.

Suggested change
return;
}
setValue(cutValue);
if (!compositionRef.current) {
setCountValue(cutValue);
}
setCompositionStartValue(null);
return;
}
setValue(cutValue);
if (!compositionRef.current) {
setCompositionStartValue(null);
}

Comment thread src/Input.tsx
Comment on lines 240 to 244
onCompositionStart={(e) => {
compositionRef.current = true;
setCountValue(formatValue);
onCompositionStart?.(e);
}}

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

Set compositionStartValue to the current formatValue when composition starts.

Suggested change
onCompositionStart={(e) => {
compositionRef.current = true;
setCountValue(formatValue);
onCompositionStart?.(e);
}}
onCompositionStart={(e) => {
compositionRef.current = true;
setCompositionStartValue(formatValue);
onCompositionStart?.(e);
}}

Comment thread src/TextArea.tsx
Comment on lines +63 to +65
// Count-related state should reflect the committed value during IME composition
// instead of the intermediate phonetic text.
const [countValue, setCountValue] = React.useState(formatValue);

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

Similarly to Input.tsx, we can derive countValue directly during render by tracking only the compositionStartValue (the value when IME composition starts). This simplifies the state logic, eliminates the useEffect entirely, and avoids potential double-renders or out-of-sync issues.

Suggested change
// Count-related state should reflect the committed value during IME composition
// instead of the intermediate phonetic text.
const [countValue, setCountValue] = React.useState(formatValue);
// Track the value at the start of IME composition to reflect the committed value
// during composition instead of the intermediate phonetic text.
const [compositionStartValue, setCompositionStartValue] = React.useState<string | null>(null);
const countValue = compositionStartValue !== null ? compositionStartValue : formatValue;

Comment thread src/TextArea.tsx
Comment on lines +94 to +98
useEffect(() => {
if (!compositionRef.current) {
setCountValue(formatValue);
}
}, [formatValue]);

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

With the derived countValue approach, this useEffect is no longer needed, which prevents unnecessary double-renders when formatValue changes.

Comment thread src/TextArea.tsx
Comment on lines +110 to +112
if (!compositionRef.current) {
setCountValue(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.

medium

Update triggerChange to reset compositionStartValue to null when composition ends.

Suggested change
if (!compositionRef.current) {
setCountValue(cutValue);
}
if (!compositionRef.current) {
setCompositionStartValue(null);
}

Comment thread src/TextArea.tsx
Comment on lines 121 to 123
compositionRef.current = true;
setCountValue(formatValue);
onCompositionStart?.(e);

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

Set compositionStartValue to the current formatValue when composition starts.

Suggested change
compositionRef.current = true;
setCountValue(formatValue);
onCompositionStart?.(e);
compositionRef.current = true;
setCompositionStartValue(formatValue);
onCompositionStart?.(e);

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR updates Input and TextArea character counting so the displayed count reflects the last committed value during IME composition (instead of counting the intermediate phonetic composition text), and adds regression tests for the behavior.

Changes:

  • Add a dedicated countValue state for count calculations during composition in Input and TextArea.
  • Update count display hooks to use countValue rather than the live formatted value.
  • Add IME composition tests for both Input and TextArea.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
tests/count.test.tsx Adds regression test ensuring Input count is based on committed value during IME composition.
tests/TextArea.count.test.tsx Adds regression test ensuring TextArea count is based on committed value during IME composition.
src/TextArea.tsx Introduces countValue and updates count calculation to ignore intermediate IME composition text.
src/Input.tsx Introduces countValue and updates count calculation to ignore intermediate IME composition text.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/count.test.tsx Outdated
Comment on lines +167 to +168
fireEvent.compositionEnd(input, { currentTarget: { value: '竹' } });
fireEvent.input(input, { target: { value: '竹' } });
Comment thread tests/TextArea.count.test.tsx Outdated
Comment on lines +147 to +148
fireEvent.compositionEnd(textarea, { currentTarget: { value: '竹' } });
fireEvent.input(textarea, { target: { value: '竹' } });
Comment thread src/TextArea.tsx
Comment on lines +94 to +98
useEffect(() => {
if (!compositionRef.current) {
setCountValue(formatValue);
}
}, [formatValue]);

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
src/Input.tsx (2)

105-109: ⚡ Quick win

建议为 useEffect 添加注释说明其用途。

此 effect 的作用是在非组合输入期间将 countValue 同步到 formatValue。建议添加简短注释说明为何需要检查 compositionRef.current,以便后续维护者理解同步时机。

另外,注意到 triggerChange 中已经在非组合状态时设置了 countValue(第 127 行),此 effect 会在 formatValue 更新后再次设置相同的值。虽然 React 会批处理并跳过不必要的重渲染,这种冗余的 setState 在功能上无害,但如果未来考虑优化性能,可以考虑去除这种重复。

💡 可选的注释示例
+ // Sync countValue with formatValue when not composing.
+ // During composition, countValue remains frozen at the pre-composition value.
  useEffect(() => {
    if (!compositionRef.current) {
      setCountValue(formatValue);
    }
  }, [formatValue]);
🤖 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 105 - 109, Add a short inline comment above the
useEffect that synchronizes countValue to formatValue explaining that this
effect ensures countValue follows formatValue when the input is not in
composition mode (check compositionRef.current) and therefore avoids clobbering
in-progress IME compositions; reference the useEffect block,
compositionRef.current, setCountValue, and formatValue so reviewers can find it
easily, and optionally note that triggerChange already updates countValue in
non-composition cases (see triggerChange) so the effect is mainly a safety sync
and could be removed if you want to avoid redundant setState in future
optimizations.

63-109: ⚖️ Poor tradeoff

可选重构建议:考虑提取共享的计数值管理逻辑。

InputTextArea 中的 countValue 状态管理逻辑几乎完全相同(状态声明、useEffect 同步、triggerChange 中的条件更新、onCompositionStart 中的冻结)。如果未来需要修改此逻辑(例如添加新的边缘情况处理),需要在两个地方同步修改。

建议将此逻辑提取为自定义 Hook(例如 useCommittedCountValue),接受 formatValuecompositionRef 作为参数,返回 countValuesetCountValue。这样可以:

  • 消除代码重复
  • 确保两个组件的行为保持一致
  • 简化未来的维护和测试

不过,这个重构可以在后续 PR 中进行,当前 PR 的实现在功能上是正确的。

🤖 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 63 - 109, The countValue state management is
duplicated between Input and TextArea; extract the shared logic into a custom
hook (e.g., useCommittedCountValue) that accepts formatValue and compositionRef
and returns [countValue, setCountValue]; move the useEffect that syncs
countValue when compositionRef.current is false, and any updates currently done
in triggerChange/onCompositionStart into this hook so both Input and TextArea
call useCommittedCountValue(formatValue, compositionRef) to eliminate
duplication and keep behavior consistent.
src/TextArea.tsx (1)

94-98: ⚡ Quick win

建议为 useEffect 添加注释说明其用途。

Input.tsx 中的相同 effect(第 105-109 行)一样,建议添加注释说明此 effect 的同步逻辑以及为何需要检查组合状态。同样存在 triggerChange 与 effect 之间的冗余 setState 调用,虽然无害但可作为未来优化点。

💡 可选的注释示例
+ // Sync countValue with formatValue when not composing.
+ // During composition, countValue remains frozen at the pre-composition value.
  useEffect(() => {
    if (!compositionRef.current) {
      setCountValue(formatValue);
    }
  }, [formatValue]);
🤖 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/TextArea.tsx` around lines 94 - 98, 在位于 TextArea.tsx 中的 useEffect(引用
useEffect、compositionRef.current、setCountValue、formatValue)上方添加一行注释,说明该 effect
的目的:在非组合输入期间将受控的格式化值同步到计数/显示状态(即为什么要检查 compositionRef.current),并指出它与 Input.tsx
中相同逻辑的对应关系;同时在注释中标注 triggerChange 与该 effect 之间存在冗余的 setCountValue 调用并添加一个
TODO/优化提示,方便未来合并或去重该冗余逻辑。
🤖 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/Input.tsx`:
- Around line 105-109: Add a short inline comment above the useEffect that
synchronizes countValue to formatValue explaining that this effect ensures
countValue follows formatValue when the input is not in composition mode (check
compositionRef.current) and therefore avoids clobbering in-progress IME
compositions; reference the useEffect block, compositionRef.current,
setCountValue, and formatValue so reviewers can find it easily, and optionally
note that triggerChange already updates countValue in non-composition cases (see
triggerChange) so the effect is mainly a safety sync and could be removed if you
want to avoid redundant setState in future optimizations.
- Around line 63-109: The countValue state management is duplicated between
Input and TextArea; extract the shared logic into a custom hook (e.g.,
useCommittedCountValue) that accepts formatValue and compositionRef and returns
[countValue, setCountValue]; move the useEffect that syncs countValue when
compositionRef.current is false, and any updates currently done in
triggerChange/onCompositionStart into this hook so both Input and TextArea call
useCommittedCountValue(formatValue, compositionRef) to eliminate duplication and
keep behavior consistent.

In `@src/TextArea.tsx`:
- Around line 94-98: 在位于 TextArea.tsx 中的 useEffect(引用
useEffect、compositionRef.current、setCountValue、formatValue)上方添加一行注释,说明该 effect
的目的:在非组合输入期间将受控的格式化值同步到计数/显示状态(即为什么要检查 compositionRef.current),并指出它与 Input.tsx
中相同逻辑的对应关系;同时在注释中标注 triggerChange 与该 effect 之间存在冗余的 setCountValue 调用并添加一个
TODO/优化提示,方便未来合并或去重该冗余逻辑。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8d4dca2f-acd0-45ff-943a-097eddd7a014

📥 Commits

Reviewing files that changed from the base of the PR and between cd850ac and 54e5fdf.

📒 Files selected for processing (4)
  • src/Input.tsx
  • src/TextArea.tsx
  • tests/TextArea.count.test.tsx
  • tests/count.test.tsx

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.39%. Comparing base (cd850ac) to head (8f9a2fd).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #179      +/-   ##
==========================================
+ Coverage   98.34%   98.39%   +0.05%     
==========================================
  Files          11       11              
  Lines         423      437      +14     
  Branches      136      135       -1     
==========================================
+ Hits          416      430      +14     
  Misses          7        7              

☔ View full report in Codecov by Harness.
📢 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.

Comment thread src/Input.tsx
);
// Count-related state should reflect the committed value during IME composition
// instead of the intermediate phonetic text.
const [countValue, setCountValue] = useState(formatValue);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

看了一下原 issue,感觉这并不是 bug。IME 状态下也是要统计字数的,原生的 validity.tooLong 也是按照输入长度来的。

我把结论 ref 过去,这个不用修。

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@ZQDesigned ZQDesigned closed this Jun 11, 2026
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.

3 participants