Skip to content

fix(number-input): backspace behavior with formatted numbers#5719

Merged
wingkwong merged 3 commits into
canaryfrom
fix/eng-2800
Oct 4, 2025
Merged

fix(number-input): backspace behavior with formatted numbers#5719
wingkwong merged 3 commits into
canaryfrom
fix/eng-2800

Conversation

@wingkwong
Copy link
Copy Markdown
Member

@wingkwong wingkwong commented Sep 18, 2025

Closes #5712

📝 Description

The problem occurred when the cursor was positioned between the first digit and the first comma in a formatted number (e.g., between "1" and "," in "1,234"). Pressing backspace in this position would do nothing.

This PR is to add the handling for such case.

⛳️ Current behavior (updates)

🚀 New behavior

💣 Is this a breaking change (Yes/No):

📝 Additional Information

Summary by CodeRabbit

  • Bug Fixes

    • Improved backspace behavior in Number Input when using formatted numbers with thousands separators, ensuring intuitive deletion near separators and correct value updates.
  • Tests

    • Added tests validating backspace handling with grouped numbers, confirming correct cursor placement and resulting values in common formatting scenarios.
  • Chores

    • Added a patch release entry for the Number Input package documenting the backspace behavior fix.

@wingkwong wingkwong added this to the v2.8.5 milestone Sep 18, 2025
@linear
Copy link
Copy Markdown

linear Bot commented Sep 18, 2025

@vercel
Copy link
Copy Markdown

vercel Bot commented Sep 18, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
heroui Ready Ready Preview Comment Sep 23, 2025 10:37am
heroui-sb Ready Ready Preview Comment Sep 23, 2025 10:37am

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Sep 18, 2025

🦋 Changeset detected

Latest commit: f937dc9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@heroui/number-input Patch
@heroui/react Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 18, 2025

Walkthrough

Adds a patch changeset for @heroui/number-input, implements locale-aware Backspace handling when the caret sits between a digit and the first grouping separator, and adds tests verifying backspace behavior with grouped/formatted numbers. No public APIs changed.

Changes

Cohort / File(s) Summary
NumberInput behavior
packages/components/number-input/src/use-number-input.ts
Adds locale-aware Backspace branch in handleKeyDown to detect grouping separator, prevent default, remove the preceding digit, clean/parse the value, update state, and restore cursor; changes handleKeyDown dependency to state.
NumberInput tests
packages/components/number-input/__tests__/number-input.test.tsx
Adds tests under "Backspace behavior with formatted numbers" verifying deletion of the digit before the first grouping separator for 4- and 7-digit formatted values using React Hook Form.
Changeset metadata
.changeset/gentle-owls-arrive.md
Adds patch changeset: "@heroui/number-input" — "fix backspace behavior with formatted numbers" (references #5712).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant NI as NumberInput (DOM)
  participant H as useNumberInput.handleKeyDown
  participant S as Internal State

  U->>NI: Press Backspace
  NI->>H: handleKeyDown(event)
  rect rgb(230,245,255)
  note over H: Caret between digit and first grouping separator
  H-->>NI: event.preventDefault()
  H->>H: compute newValue (remove preceding digit)
  H->>H: clean/parse numeric string
  H->>S: setNumberValue / setInputValue
  H-->>NI: setTimeout(0) to restore caret position
  end
  NI-->>U: Updated displayed value and caret position
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • jrgarciadev
  • winchesHe

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The PR includes "Closes #5712" and a brief description of the problem and intent, but the repository's template sections "⛳️ Current behavior", "🚀 New behavior", and "💣 Is this a breaking change" are left empty, so the required template structure is not fully followed and reviewers lack explicit current vs new behavior and breaking-change information. Please populate the templated sections: describe the observed "Current behavior" with reproduction steps, specify the "New behavior" and exactly how the user-visible behavior changes after this fix, and explicitly state "Is this a breaking change (Yes/No)" with migration notes if applicable; optionally add testing notes and links to the new tests.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title "fix(number-input): backspace behavior with formatted numbers" is concise, follows conventional commit style, and accurately summarizes the primary change (a bugfix to NumberInput backspace handling for formatted/ grouped numbers), so a reviewer scanning history will understand the main intent.
Linked Issues Check ✅ Passed The code changes add locale-aware handling for Backspace when the caret is between a digit and the first grouping separator and include tests for the 4- and 7-digit formatted-number cases; these changes directly implement the objective described in issue #5712 and the PR closes that issue.
Out of Scope Changes Check ✅ Passed All modifications are limited to the NumberInput package (use-number-input logic, unit tests) and the repository changeset file; there are no unrelated file changes or functionality outside the scope of fixing the backspace behavior.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/eng-2800

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.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Sep 18, 2025

Open in StackBlitz

@heroui/accordion

npm i https://pkg.pr.new/@heroui/accordion@5719

@heroui/alert

npm i https://pkg.pr.new/@heroui/alert@5719

@heroui/autocomplete

npm i https://pkg.pr.new/@heroui/autocomplete@5719

@heroui/avatar

npm i https://pkg.pr.new/@heroui/avatar@5719

@heroui/badge

npm i https://pkg.pr.new/@heroui/badge@5719

@heroui/breadcrumbs

npm i https://pkg.pr.new/@heroui/breadcrumbs@5719

@heroui/button

npm i https://pkg.pr.new/@heroui/button@5719

@heroui/calendar

npm i https://pkg.pr.new/@heroui/calendar@5719

@heroui/card

npm i https://pkg.pr.new/@heroui/card@5719

@heroui/checkbox

npm i https://pkg.pr.new/@heroui/checkbox@5719

@heroui/chip

npm i https://pkg.pr.new/@heroui/chip@5719

@heroui/code

npm i https://pkg.pr.new/@heroui/code@5719

@heroui/date-input

npm i https://pkg.pr.new/@heroui/date-input@5719

@heroui/date-picker

npm i https://pkg.pr.new/@heroui/date-picker@5719

@heroui/divider

npm i https://pkg.pr.new/@heroui/divider@5719

@heroui/drawer

npm i https://pkg.pr.new/@heroui/drawer@5719

@heroui/dropdown

npm i https://pkg.pr.new/@heroui/dropdown@5719

@heroui/form

npm i https://pkg.pr.new/@heroui/form@5719

@heroui/image

npm i https://pkg.pr.new/@heroui/image@5719

@heroui/input

npm i https://pkg.pr.new/@heroui/input@5719

@heroui/input-otp

npm i https://pkg.pr.new/@heroui/input-otp@5719

@heroui/kbd

npm i https://pkg.pr.new/@heroui/kbd@5719

@heroui/link

npm i https://pkg.pr.new/@heroui/link@5719

@heroui/listbox

npm i https://pkg.pr.new/@heroui/listbox@5719

@heroui/menu

npm i https://pkg.pr.new/@heroui/menu@5719

@heroui/modal

npm i https://pkg.pr.new/@heroui/modal@5719

@heroui/navbar

npm i https://pkg.pr.new/@heroui/navbar@5719

@heroui/number-input

npm i https://pkg.pr.new/@heroui/number-input@5719

@heroui/pagination

npm i https://pkg.pr.new/@heroui/pagination@5719

@heroui/popover

npm i https://pkg.pr.new/@heroui/popover@5719

@heroui/progress

npm i https://pkg.pr.new/@heroui/progress@5719

@heroui/radio

npm i https://pkg.pr.new/@heroui/radio@5719

@heroui/ripple

npm i https://pkg.pr.new/@heroui/ripple@5719

@heroui/scroll-shadow

npm i https://pkg.pr.new/@heroui/scroll-shadow@5719

@heroui/select

npm i https://pkg.pr.new/@heroui/select@5719

@heroui/skeleton

npm i https://pkg.pr.new/@heroui/skeleton@5719

@heroui/slider

npm i https://pkg.pr.new/@heroui/slider@5719

@heroui/snippet

npm i https://pkg.pr.new/@heroui/snippet@5719

@heroui/spacer

npm i https://pkg.pr.new/@heroui/spacer@5719

@heroui/spinner

npm i https://pkg.pr.new/@heroui/spinner@5719

@heroui/switch

npm i https://pkg.pr.new/@heroui/switch@5719

@heroui/table

npm i https://pkg.pr.new/@heroui/table@5719

@heroui/tabs

npm i https://pkg.pr.new/@heroui/tabs@5719

@heroui/toast

npm i https://pkg.pr.new/@heroui/toast@5719

@heroui/tooltip

npm i https://pkg.pr.new/@heroui/tooltip@5719

@heroui/user

npm i https://pkg.pr.new/@heroui/user@5719

@heroui/react

npm i https://pkg.pr.new/@heroui/react@5719

@heroui/system

npm i https://pkg.pr.new/@heroui/system@5719

@heroui/system-rsc

npm i https://pkg.pr.new/@heroui/system-rsc@5719

@heroui/theme

npm i https://pkg.pr.new/@heroui/theme@5719

@heroui/use-aria-accordion

npm i https://pkg.pr.new/@heroui/use-aria-accordion@5719

@heroui/use-aria-accordion-item

npm i https://pkg.pr.new/@heroui/use-aria-accordion-item@5719

@heroui/use-aria-button

npm i https://pkg.pr.new/@heroui/use-aria-button@5719

@heroui/use-aria-link

npm i https://pkg.pr.new/@heroui/use-aria-link@5719

@heroui/use-aria-modal-overlay

npm i https://pkg.pr.new/@heroui/use-aria-modal-overlay@5719

@heroui/use-aria-multiselect

npm i https://pkg.pr.new/@heroui/use-aria-multiselect@5719

@heroui/use-aria-overlay

npm i https://pkg.pr.new/@heroui/use-aria-overlay@5719

@heroui/use-callback-ref

npm i https://pkg.pr.new/@heroui/use-callback-ref@5719

@heroui/use-clipboard

npm i https://pkg.pr.new/@heroui/use-clipboard@5719

@heroui/use-data-scroll-overflow

npm i https://pkg.pr.new/@heroui/use-data-scroll-overflow@5719

@heroui/use-disclosure

npm i https://pkg.pr.new/@heroui/use-disclosure@5719

@heroui/use-draggable

npm i https://pkg.pr.new/@heroui/use-draggable@5719

@heroui/use-form-reset

npm i https://pkg.pr.new/@heroui/use-form-reset@5719

@heroui/use-image

npm i https://pkg.pr.new/@heroui/use-image@5719

@heroui/use-infinite-scroll

npm i https://pkg.pr.new/@heroui/use-infinite-scroll@5719

@heroui/use-intersection-observer

npm i https://pkg.pr.new/@heroui/use-intersection-observer@5719

@heroui/use-is-mobile

npm i https://pkg.pr.new/@heroui/use-is-mobile@5719

@heroui/use-is-mounted

npm i https://pkg.pr.new/@heroui/use-is-mounted@5719

@heroui/use-measure

npm i https://pkg.pr.new/@heroui/use-measure@5719

@heroui/use-pagination

npm i https://pkg.pr.new/@heroui/use-pagination@5719

@heroui/use-real-shape

npm i https://pkg.pr.new/@heroui/use-real-shape@5719

@heroui/use-ref-state

npm i https://pkg.pr.new/@heroui/use-ref-state@5719

@heroui/use-resize

npm i https://pkg.pr.new/@heroui/use-resize@5719

@heroui/use-safe-layout-effect

npm i https://pkg.pr.new/@heroui/use-safe-layout-effect@5719

@heroui/use-scroll-position

npm i https://pkg.pr.new/@heroui/use-scroll-position@5719

@heroui/use-ssr

npm i https://pkg.pr.new/@heroui/use-ssr@5719

@heroui/use-theme

npm i https://pkg.pr.new/@heroui/use-theme@5719

@heroui/use-update-effect

npm i https://pkg.pr.new/@heroui/use-update-effect@5719

@heroui/use-viewport-size

npm i https://pkg.pr.new/@heroui/use-viewport-size@5719

@heroui/aria-utils

npm i https://pkg.pr.new/@heroui/aria-utils@5719

@heroui/dom-animation

npm i https://pkg.pr.new/@heroui/dom-animation@5719

@heroui/framer-utils

npm i https://pkg.pr.new/@heroui/framer-utils@5719

@heroui/react-rsc-utils

npm i https://pkg.pr.new/@heroui/react-rsc-utils@5719

@heroui/react-utils

npm i https://pkg.pr.new/@heroui/react-utils@5719

@heroui/shared-icons

npm i https://pkg.pr.new/@heroui/shared-icons@5719

@heroui/shared-utils

npm i https://pkg.pr.new/@heroui/shared-utils@5719

@heroui/stories-utils

npm i https://pkg.pr.new/@heroui/stories-utils@5719

@heroui/test-utils

npm i https://pkg.pr.new/@heroui/test-utils@5719

commit: f937dc9

Copy link
Copy Markdown
Contributor

@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: 2

Caution

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

⚠️ Outside diff range comments (1)
packages/components/number-input/src/use-number-input.ts (1)

245-292: Make number-input locale-aware (RTL & non‑Latin digits)

Confirmed: packages/components/number-input/src/use-number-input.ts (~lines 245–292) uses ASCII-only cleanup (checks for ','; value.replace(/[^\d.-]/g, '') and parseFloat(cleanValue)) which strips Arabic‑Indic/Eastern digits and mishandles locale group/decimal separators. Action: normalize input in a locale-aware way — get locale decimal/group chars via Intl.NumberFormat, map Unicode digits (e.g. \p{Nd} ranges) to ASCII before parsing, and adjust cursor-position logic to compute the new caret after normalization. Note: apps/docs/utils/number.ts also uses parseFloat (docs only — review if UI-facing); packages/components/toast/src/use-toast.ts uses parseFloat for CSS margins (acceptable).

🧹 Nitpick comments (4)
packages/components/number-input/src/use-number-input.ts (3)

275-281: Cursor restore timing

setTimeout(0) works but can be racy under concurrent renders. Consider requestAnimationFrame or queueMicrotask for tighter ordering.

Example:

-        setTimeout(() => {
+        queueMicrotask(() => {
           // set the new cursor position
           const pos = Math.max(0, selectionStart - 1);
           inputElement.setSelectionRange(pos, pos);
-        }, 0);
+        });

291-292: Dependency array should include locale

handleKeyDown now depends on locale; add it to avoid stale grouping char.

Apply this diff:

-    [inputValue, state, onClear, isClearable, originalProps.isReadOnly],
+    [inputValue, state, onClear, isClearable, originalProps.isReadOnly, locale],

245-292: Optional: simplify the condition

The guard value[selectionStart - 1] !== groupChar is unnecessary given real formats never have consecutive group chars. You can drop it to reduce branching.

packages/components/number-input/__tests__/number-input.test.tsx (1)

596-652: Add i18n/readOnly/decimal coverage

Great regression tests. Please add:

  • A locale with non-comma grouping (e.g., de-DE or fr-FR narrow NBSP).
  • readOnly scenario to ensure value doesn’t change on Backspace.
  • A case with decimals and a negative number.
  • (Optional) Verify Delete key remains default (no special handling).

Example (sketch):

import {I18nProvider} from "@react-aria/i18n";

it("handles backspace with de-DE grouping", () => {
  const {container} = render(
    <I18nProvider locale="de-DE">
      <NumberInput defaultValue={1234} formatOptions={{style: "decimal", useGrouping: true}} />
    </I18nProvider>,
  );
  const input = container.querySelector("input[type='text']") as HTMLInputElement;
  expect(input.value).toBe("1.234");
  act(() => {
    input.focus();
    input.setSelectionRange(1, 1); // 1|.234
    fireEvent.keyDown(input, {key: "Backspace"});
  });
  expect(input.value).toBe("234");
});

it("does not modify value on Backspace when readOnly", () => {
  const {container} = render(
    <NumberInput isReadOnly defaultValue={1234} formatOptions={{style: "decimal", useGrouping: true}} />
  );
  const input = container.querySelector("input[type='text']") as HTMLInputElement;
  act(() => {
    input.focus();
    input.setSelectionRange(1, 1);
    fireEvent.keyDown(input, {key: "Backspace"});
  });
  expect(input.value).toBe("1,234");
});
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 27c5129 and 2a3e477.

📒 Files selected for processing (3)
  • .changeset/gentle-owls-arrive.md (1 hunks)
  • packages/components/number-input/__tests__/number-input.test.tsx (1 hunks)
  • packages/components/number-input/src/use-number-input.ts (2 hunks)
🔇 Additional comments (1)
.changeset/gentle-owls-arrive.md (1)

1-6: Changeset looks good

Patch scope and message are appropriate and reference the bug ID. No action needed.

Comment thread packages/components/number-input/src/use-number-input.ts
Comment thread packages/components/number-input/src/use-number-input.ts
@wingkwong wingkwong marked this pull request as draft September 18, 2025 17:20
Copy link
Copy Markdown
Contributor

@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: 0

🧹 Nitpick comments (3)
packages/components/number-input/src/use-number-input.ts (3)

296-297: Include missing dependencies in useCallback

locale and originalProps.isDisabled are captured but not in deps, leading to stale behavior when they change.

Apply this diff:

-    [inputValue, state, onClear, isClearable, originalProps.isReadOnly],
+    [inputValue, state, onClear, isClearable, locale, originalProps.isReadOnly, originalProps.isDisabled],

251-253: Fix comment typo and scope

Comment ends with a stray “if (” and says “first” though condition applies to any group separator.

Apply this diff:

-      // handle backspace when cursor is between a digit and the first group separator
-      // e.g. 1|,234 (en-US) or 1|.234 (de-DE) -> backspace removes the preceding digit if (
+      // Handle Backspace when the cursor is between a digit and a group separator.
+      // e.g., 1|,234 (en-US) or 1|.234 (de-DE) -> Backspace removes the preceding digit.

248-250: Optional: memoize the grouping character

Avoid recomputing per keydown and shrink the handler. Compute once per locale.

You can hoist and memoize:

const groupChar = useMemo(() => {
  const nf = new Intl.NumberFormat(locale, {useGrouping: true});
  return nf.formatToParts(1000).find((p) => p.type === "group")?.value ?? ",";
}, [locale]);

Then remove the per-call computation inside handleKeyDown.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2a3e477 and f937dc9.

📒 Files selected for processing (1)
  • packages/components/number-input/src/use-number-input.ts (2 hunks)
🔇 Additional comments (2)
packages/components/number-input/src/use-number-input.ts (2)

248-250: Locale-aware grouping separator detection looks good

Good use of Intl.NumberFormat + formatToParts with a safe fallback.


268-278: Locale parsing bug: replace/parseFloat breaks decimal-comma and non-ASCII digits

Regex cleanup + parseFloat is not locale-safe (e.g., de-DE: ".234,56" → 0.234). Delegate parsing/formatting to the NumberField state by setting the localized string directly.

Apply this diff:

-        // e.g. ,234 -> 234
-        const cleanValue = newValue.replace(/[^\d.-]/g, "");
-
-        if (cleanValue === "" || cleanValue === "-") {
-          state.setInputValue("");
-        } else {
-          const numberValue = parseFloat(cleanValue);
-
-          if (!isNaN(numberValue)) {
-            state.setNumberValue(numberValue);
-          }
-        }
+        // Normalize leading group char so the localized parser can handle it.
+        // Examples: ",234" -> "234", "-,234" -> "-234"
+        let normalized = newValue;
+        if (normalized.startsWith(groupChar)) {
+          normalized = normalized.slice(groupChar.length);
+        } else if (normalized.startsWith("-" + groupChar)) {
+          normalized = "-" + normalized.slice(1 + groupChar.length);
+        }
+        state.setInputValue(normalized);

@wingkwong wingkwong merged commit 736293b into canary Oct 4, 2025
10 checks passed
@wingkwong wingkwong deleted the fix/eng-2800 branch October 4, 2025 02:16
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.

[BUG] - NumberInput Won't Backspace in specific case

1 participant