Skip to content

feat(number-input): add isRealTimeFormat prop#5926

Closed
hasegawa-101 wants to merge 5 commits into
heroui-inc:canaryfrom
hasegawa-101:feat/number-input-real-time-format
Closed

feat(number-input): add isRealTimeFormat prop#5926
hasegawa-101 wants to merge 5 commits into
heroui-inc:canaryfrom
hasegawa-101:feat/number-input-real-time-format

Conversation

@hasegawa-101
Copy link
Copy Markdown

@hasegawa-101 hasegawa-101 commented Nov 22, 2025

Closes #5136

📝 Description

Add isRealTimeFormat prop to enable real-time number formatting while typing.

⛳️ Current behavior (updates)

NumberInput only formats numbers on blur. Users cannot type more than 4 digits with useGrouping: true because the formatted comma interferes with input.

🚀 New behavior

  • Add isRealTimeFormat prop (default: false)
  • When enabled: formats numbers as you type (12345 → 12,345)

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

No - Default behavior unchanged. Opt-in only.

📝 Additional Information

Summary by CodeRabbit

  • New Features

    • Real-time number formatting as you type via a new isRealTimeFormat option with locale-aware formatting, grouping control, IME/composition support, cursor preservation, and a demo story showcasing the behavior.
  • Chores

    • Added runtime dependency "@internationalized/number" (v3.6.5) to support parsing and formatting.
  • Tests

    • New test suite covering real-time formatting, grouping, paste/cut and composition handling, input validation, and cursor restoration.

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Nov 22, 2025

⚠️ No Changeset found

Latest commit: c3e41ed

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@vercel
Copy link
Copy Markdown

vercel Bot commented Nov 22, 2025

@hasegawa-101 is attempting to deploy a commit to the HeroUI Inc Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Nov 22, 2025

Walkthrough

Adds an opt-in real-time number-formatting feature to NumberInput: new isRealTimeFormat prop, a useRealTimeInputFormatting hook, wiring of formatting handlers into the input lifecycle, a story and tests, and a runtime dependency @internationalized/number@3.6.5.

Changes

Cohort / File(s) Summary
Dependency Addition
\packages/components/number-input/package.json``
Added runtime dependency @internationalized/number @ 3.6.5.
Core Real-Time Formatting & API
\packages/components/number-input/src/use-number-input.ts``
Added public prop isRealTimeFormat?: boolean; memoized numberParser/numberFormatter based on locale/formatOptions; integrated real-time formatting flow; conditionally attached beforeInput/input/composition/paste/cut handlers; updated onChange/onValue flows to use parser/formatter.
Real-Time Hook
\packages/components/number-input/src/use-real-time-formatting.ts``
New hook useRealTimeInputFormatting exposing shouldFormat, composition/input/paste/cut handlers, cursor-restoration logic, DOM interactions, and integration with NumberFieldState, numberParser, and numberFormatter.
Stories
\packages/components/number-input/stories/number-input.stories.tsx``
Added WithRealTimeFormat story demonstrating isRealTimeFormat: true with grouping and decimal style.
Tests
\packages/components/number-input/tests/number-input.test.tsx``
Added tests covering real-time formatting, grouping, beforeInput blocking, paste/cut handling, and extensive cursor-restoration cases (diff contains duplicated test blocks).

Sequence Diagram

sequenceDiagram
    participant User
    participant Input as Input Element
    participant Hook as useRealTimeInputFormatting
    participant Parser as NumberParser (`@internationalized/number`)
    participant Formatter as Intl.NumberFormat
    participant State as NumberFieldState

    User->>Input: type / paste / cut / composition
    Input->>Hook: onBeforeInput / onInput / onPaste / onCut / composition events
    Hook->>Parser: validate / parse partial input
    alt valid partial or number
        Parser-->>Hook: parsed value
        Hook->>Formatter: format using locale & formatOptions
        Formatter-->>Hook: formatted string
        Hook->>Input: set formatted display & restore cursor
        Hook->>State: update numeric value and emit onChange/onValueChange
    else invalid
        Hook->>Input: preventDefault / ignore
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Focus areas:
    • Cursor restoration and selection math in use-real-time-formatting and integration points in use-number-input.
    • IME/composition handling and correct use of beforeInput vs input events.
    • Memoization dependencies and lifecycle/cleanup of handlers.
    • Duplicate test blocks in __tests__/number-input.test.tsx that likely need deduplication.

Possibly related PRs

Suggested Reviewers

  • wingkwong
  • jrgarciadev

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
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 (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main feature addition: adding the isRealTimeFormat prop to the number-input component, which is the primary focus of this PR.
Description check ✅ Passed The description follows the template structure with all required sections completed: issue reference, description of the feature, current vs. new behavior, breaking change status, and context.
Linked Issues check ✅ Passed The PR successfully addresses issue #5136 by implementing real-time number formatting through the isRealTimeFormat prop, enabling digits to be entered without artificial limits while formatting occurs during typing.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the isRealTimeFormat feature: dependency addition, hook implementation, prop exposure, stories, and comprehensive tests. No unrelated modifications detected.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent 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 acc74d1 and c3e41ed.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • packages/components/number-input/__tests__/number-input.test.tsx (1 hunks)
  • packages/components/number-input/package.json (1 hunks)
  • packages/components/number-input/src/use-number-input.ts (6 hunks)
  • packages/components/number-input/src/use-real-time-formatting.ts (1 hunks)
  • packages/components/number-input/stories/number-input.stories.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-27T21:48:35.308Z
Learnt from: adbjo
Repo: heroui-inc/heroui PR: 5846
File: packages/components/tabs/src/tabs.tsx:76-101
Timestamp: 2025-10-27T21:48:35.308Z
Learning: In packages/components/tabs/src/tabs.tsx, the updateCursorPosition useCallback dependency array intentionally includes `cursorRef.current` to handle the case where the cursor span element is unmounted and remounted (e.g., when `disableAnimation` or `disableCursorAnimation` toggles). This ensures the callback is recreated when the ref points to a new element, triggering a dependency chain that re-establishes the ResizeObserver and initializes the new cursor element with the data-initialized attribute.
</learning]

Applied to files:

  • packages/components/number-input/__tests__/number-input.test.tsx
  • packages/components/number-input/src/use-number-input.ts
  • packages/components/number-input/src/use-real-time-formatting.ts
🧬 Code graph analysis (2)
packages/components/number-input/__tests__/number-input.test.tsx (3)
packages/core/theme/src/components/user.ts (1)
  • user (33-33)
packages/components/number-input/src/index.ts (1)
  • NumberInput (10-10)
packages/core/theme/src/components/input.ts (1)
  • input (926-926)
packages/components/number-input/src/use-number-input.ts (1)
packages/components/number-input/src/use-real-time-formatting.ts (1)
  • useRealTimeInputFormatting (15-246)
🔇 Additional comments (13)
packages/components/number-input/stories/number-input.stories.tsx (1)

541-554: LGTM!

The story clearly demonstrates the real-time formatting feature with appropriate configuration.

packages/components/number-input/src/use-number-input.ts (4)

85-90: LGTM!

The isRealTimeFormat prop is well-documented and defaults to false, making this a non-breaking, opt-in feature.


265-289: LGTM!

The formatter and parser are correctly memoized, and the real-time formatting hook is properly integrated with all necessary dependencies.


291-362: Backspace handling logic is appropriate.

The handleKeyDown function correctly handles arrow keys for increment/decrement and preserves the existing backspace logic for grouping separator navigation. The async cursor restoration using setTimeout is consistent with the real-time formatting approach.


447-483: LGTM!

The conditional handler overrides are correctly applied only when shouldFormat is true, preserving React Aria's default behavior when real-time formatting is disabled.

packages/components/number-input/src/use-real-time-formatting.ts (7)

6-23: LGTM!

The interface is well-defined, and IME composition tracking is correctly implemented using a ref to avoid unnecessary re-renders.


25-43: LGTM!

The cursor restoration logic correctly handles formatted strings by counting digits rather than characters, which works with currency symbols and grouping separators.


45-81: LGTM!

The handleInput implementation follows a sound validation and formatting flow, correctly updating state and restoring the cursor position based on digit count.


83-89: LGTM!

The composition end handler correctly resets the IME flag and reuses the handleInput logic to process the composed text.


91-132: LGTM!

The paste handler correctly prevents default behavior, validates and formats the pasted content, and restores the cursor position accounting for both existing and pasted digits.


134-181: LGTM!

The cut handler correctly manages clipboard data, validates the resulting value, and handles the edge case where cutting all digits results in an empty input.


183-235: LGTM!

The handleBeforeInput implementation correctly handles IME composition with both ref and native event checks, validates input, prevents invalid characters, and manages cursor restoration for formatted values.

packages/components/number-input/package.json (1)

51-51: Verify the dependency version and security advisories.

Ensure that version 3.6.5 of @internationalized/number is current and free from known vulnerabilities.

#!/bin/bash
# Check npm registry for latest version
curl -s https://registry.npmjs.org/@internationalized/number | jq '.["dist-tags"].latest, .versions | keys | .[-5:]'

# Check for security advisories
gh api graphql -f query='
{
  securityVulnerabilities(first: 5, ecosystem: NPM, package: "@internationalized/number") {
    nodes {
      advisory {
        summary
        severity
        publishedAt
      }
      vulnerableVersionRange
      firstPatchedVersion {
        identifier
      }
    }
  }
}'

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 Nov 22, 2025

Open in StackBlitz

@heroui/accordion

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

@heroui/alert

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

@heroui/autocomplete

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

@heroui/avatar

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

@heroui/badge

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

@heroui/breadcrumbs

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

@heroui/button

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

@heroui/calendar

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

@heroui/card

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

@heroui/checkbox

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

@heroui/chip

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

@heroui/code

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

@heroui/date-input

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

@heroui/date-picker

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

@heroui/divider

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

@heroui/drawer

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

@heroui/dropdown

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

@heroui/form

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

@heroui/image

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

@heroui/input

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

@heroui/input-otp

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

@heroui/kbd

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

@heroui/link

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

@heroui/listbox

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

@heroui/menu

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

@heroui/modal

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

@heroui/navbar

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

@heroui/number-input

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

@heroui/pagination

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

@heroui/popover

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

@heroui/progress

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

@heroui/radio

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

@heroui/ripple

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

@heroui/scroll-shadow

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

@heroui/select

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

@heroui/skeleton

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

@heroui/slider

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

@heroui/snippet

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

@heroui/spacer

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

@heroui/spinner

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

@heroui/switch

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

@heroui/table

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

@heroui/tabs

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

@heroui/toast

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

@heroui/tooltip

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

@heroui/user

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

@heroui/react

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

@heroui/system

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

@heroui/system-rsc

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

@heroui/theme

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

@heroui/use-aria-accordion

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

@heroui/use-aria-accordion-item

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

@heroui/use-aria-button

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

@heroui/use-aria-link

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

@heroui/use-aria-modal-overlay

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

@heroui/use-aria-multiselect

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

@heroui/use-aria-overlay

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

@heroui/use-callback-ref

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

@heroui/use-clipboard

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

@heroui/use-data-scroll-overflow

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

@heroui/use-disclosure

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

@heroui/use-draggable

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

@heroui/use-form-reset

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

@heroui/use-image

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

@heroui/use-infinite-scroll

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

@heroui/use-intersection-observer

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

@heroui/use-is-mobile

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

@heroui/use-is-mounted

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

@heroui/use-measure

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

@heroui/use-pagination

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

@heroui/use-real-shape

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

@heroui/use-ref-state

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

@heroui/use-resize

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

@heroui/use-safe-layout-effect

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

@heroui/use-scroll-position

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

@heroui/use-ssr

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

@heroui/use-theme

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

@heroui/use-update-effect

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

@heroui/use-viewport-size

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

@heroui/aria-utils

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

@heroui/dom-animation

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

@heroui/framer-utils

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

@heroui/react-rsc-utils

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

@heroui/react-utils

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

@heroui/shared-icons

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

@heroui/shared-utils

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

@heroui/stories-utils

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

@heroui/test-utils

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

commit: c3e41ed

@hasegawa-101 hasegawa-101 marked this pull request as ready for review November 22, 2025 13:48
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: 3

📜 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 5d9a05b and 4a6a03f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • packages/components/number-input/package.json (1 hunks)
  • packages/components/number-input/src/use-number-input.ts (5 hunks)
  • packages/components/number-input/stories/number-input.stories.tsx (1 hunks)
🔇 Additional comments (8)
packages/components/number-input/stories/number-input.stories.tsx (1)

534-547: LGTM! Good demonstration of the new feature.

The story effectively demonstrates real-time formatting with appropriate format options and uses a controlled component to showcase the value updates.

packages/components/number-input/src/use-number-input.ts (6)

17-17: LGTM! Appropriate import for number parsing.

The NumberParser from @internationalized/number is correctly imported to support locale-aware number parsing.


83-88: LGTM! Clear prop definition with good documentation.

The isRealTimeFormat prop is well-documented and defaults to false, maintaining backward compatibility with existing behavior.


314-320: LGTM! Proper memoization of formatters.

The numberFormatter and numberParser are correctly memoized with appropriate dependencies to avoid unnecessary recreations while ensuring they update when locale or format options change.


461-462: Good conditional attachment pattern.

The conditional attachment of onBeforeInput only when shouldFormat is true is a good practice. However, this depends on fixing the shouldFormat logic flagged earlier.


469-484: LGTM! Dependency array correctly updated.

The dependency array properly includes shouldFormat and handleBeforeInput, ensuring the props getter updates when these values change.


332-376: Review comment is factually incorrect on both concerns.

  1. Deletion handling is implemented: handleKeyDown explicitly handles Backspace (lines 271–303) with e.preventDefault() and custom logic for grouping separators. Deletion operations are not ignored.

  2. Validation input type is correct: React Aria's useNumberFieldState.validate expects a string input, so calling state.validate(formattedValue) with the formatted string is the correct approach, not an error.

The code follows React Aria's design patterns appropriately.

Likely an incorrect or invalid review comment.

packages/components/number-input/package.json (1)

51-51: No issues found — dependency version is current and secure.

Verification confirms that @internationalized/number@3.6.5 is the latest stable version with no known security vulnerabilities.

Comment thread packages/components/number-input/src/use-number-input.ts Outdated
Comment thread packages/components/number-input/src/use-number-input.ts Outdated
@hasegawa-101
Copy link
Copy Markdown
Author

@wingkwong I would appreciate it if you could review the implementation and let me know if the direction is correct 🙏

@wingkwong wingkwong self-assigned this Dec 12, 2025
@vercel
Copy link
Copy Markdown

vercel Bot commented Dec 17, 2025

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

Project Deployment Review Updated (UTC)
heroui Error Error Dec 17, 2025 5:40am
heroui-sb Ready Ready Preview, Comment Dec 17, 2025 5:40am

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

🧹 Nitpick comments (3)
packages/components/number-input/__tests__/number-input.test.tsx (1)

751-768: Consider making locale explicit in currency formatting test.

The test at line 766 uses a regex /\$1234(\.00)?/ that assumes en-US locale formatting. While the test environment likely defaults to en-US, making the locale explicit would improve test reliability across different environments.

Consider wrapping the test with an explicit locale or making the assertion more flexible:

   it("should format even if useGrouping is false when isRealTimeFormat is true", async () => {
     const {container} = render(
+      <HeroUIProvider locale="en-US">
         <NumberInput
           isRealTimeFormat
           formatOptions={{style: "currency", currency: "USD", useGrouping: false}}
           label="Price"
         />
+      </HeroUIProvider>,
     );
     const input = container.querySelector("input") as HTMLInputElement;
 
-    // Type 1234. Should be formatted as $1234 (no comma)
-    // Note: Currency symbol depends on locale. Default en-US -> $
     await user.type(input, "1234");
-    // Standard currency formatting often adds decimals and currency symbol
-    // We check that it has some currency formatting but NO commas for thousands
     expect(input.value).toMatch(/\$1234(\.00)?/);
     expect(input.value).not.toContain(",");
   });
packages/components/number-input/src/use-number-input.ts (1)

352-361: Remove unnecessary numberParser from handleKeyDown dependency array.

The handleKeyDown callback includes numberParser in its dependency array (line 359), but numberParser is never used within the callback. Only numberFormatter.formatToParts is used at line 318 to extract the grouping character. The backspace logic manually parses with parseFloat (line 329) rather than using the parser.

Remove the unused dependency:

   },
   [
     inputValue,
     state,
     onClear,
     isClearable,
     originalProps.isReadOnly,
     originalProps.isRealTimeFormat,
-    numberParser,
     numberFormatter,
   ],
 );
packages/components/number-input/src/use-real-time-formatting.ts (1)

68-73: Consider safer typing for synthetic onChange events.

The handlers cast plain objects to React.ChangeEvent<HTMLInputElement> at lines 68-73, 132-137, 194-199, and 262-267. This type assertion is not fully safe—real ChangeEvent objects include properties like nativeEvent, preventDefault, stopPropagation, etc., which are not present in the synthetic objects. Consumer code accessing these properties may encounter runtime errors.

Consider one of these approaches:

Option 1: Define a minimal event type:

type MinimalChangeEvent = {
  target: { value: string };
  currentTarget: { value: string };
};

// Then pass without casting:
if (onChange) {
  onChange({
    target: { value: formattedValue },
    currentTarget: { value: formattedValue },
  } as any);
}

Option 2: Document the limitation in the prop type:

 export interface UseRealTimeInputFormattingProps {
   // ...
+  /**
+   * Optional change handler. Note: receives a synthetic event with only
+   * target.value and currentTarget.value populated.
+   */
   onChange?: React.ChangeEventHandler<HTMLInputElement>;
 }

Also applies to: 132-137, 194-199, 262-267

📜 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 0e56c42 and 3862606.

📒 Files selected for processing (3)
  • packages/components/number-input/__tests__/number-input.test.tsx (1 hunks)
  • packages/components/number-input/src/use-number-input.ts (6 hunks)
  • packages/components/number-input/src/use-real-time-formatting.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-27T21:48:35.308Z
Learnt from: adbjo
Repo: heroui-inc/heroui PR: 5846
File: packages/components/tabs/src/tabs.tsx:76-101
Timestamp: 2025-10-27T21:48:35.308Z
Learning: In packages/components/tabs/src/tabs.tsx, the updateCursorPosition useCallback dependency array intentionally includes `cursorRef.current` to handle the case where the cursor span element is unmounted and remounted (e.g., when `disableAnimation` or `disableCursorAnimation` toggles). This ensures the callback is recreated when the ref points to a new element, triggering a dependency chain that re-establishes the ResizeObserver and initializes the new cursor element with the data-initialized attribute.
</learning]

Applied to files:

  • packages/components/number-input/src/use-number-input.ts
🧬 Code graph analysis (2)
packages/components/number-input/src/use-number-input.ts (1)
packages/components/number-input/src/use-real-time-formatting.ts (1)
  • useRealTimeInputFormatting (15-281)
packages/components/number-input/__tests__/number-input.test.tsx (3)
packages/core/theme/src/components/user.ts (1)
  • user (33-33)
packages/components/number-input/src/index.ts (1)
  • NumberInput (10-10)
packages/core/theme/src/components/input.ts (1)
  • input (926-926)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: ESLint
  • GitHub Check: Tests
  • GitHub Check: Build
  • GitHub Check: Continuous Release
  • GitHub Check: TypeScript
🔇 Additional comments (2)
packages/components/number-input/src/use-number-input.ts (1)

265-289: LGTM: Clean integration of real-time formatting.

The memoization of numberFormatter and numberParser is appropriate to avoid unnecessary recreations. The integration with useRealTimeInputFormatting correctly passes all required dependencies, and the conditional handler attachment at lines 447-457 ensures the feature is truly opt-in.

packages/components/number-input/src/use-real-time-formatting.ts (1)

204-270: LGTM: Comprehensive beforeInput handling with IME support.

The handleBeforeInput implementation correctly addresses previous review concerns:

  • Checks for IME composition at line 206 (both via ref and native event)
  • Calls preventDefault() for invalid input (lines 218, 226)
  • Always prevents default at line 231 to take full control of formatting
  • Manages cursor position at lines 239-260

The double-check for composition state provides robust IME support.

Comment thread packages/components/number-input/__tests__/number-input.test.tsx
Comment thread packages/components/number-input/src/use-real-time-formatting.ts
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-real-time-formatting.ts (3)

19-21: Remove unnecessary memoization.

The shouldFormat computation is just Boolean(isRealTimeFormat), which is trivial. The useMemo overhead exceeds any benefit here.

Apply this diff:

-  const shouldFormat = useMemo(() => {
-    return Boolean(isRealTimeFormat);
-  }, [isRealTimeFormat]);
+  const shouldFormat = Boolean(isRealTimeFormat);

185-188: Use proper type assertion instead of any.

Accessing isComposing through as any bypasses type safety. The nativeEvent is an InputEvent which has the isComposing property.

Apply this diff:

   const handleBeforeInput = useCallback(
     (e: React.FormEvent<HTMLInputElement> & {data: string | null}) => {
-      if (isComposingRef.current || (e.nativeEvent as any).isComposing) return;
+      if (isComposingRef.current || (e.nativeEvent as InputEvent)?.isComposing) return;
       if (!e.data) return;

76-80: Consider using a more complete synthetic event or documenting the limitation.

The synthetic ChangeEvent created at lines 76–80 includes only target and currentTarget with value properties. While most onChange handlers access only e.target.value, consumers could theoretically rely on other event properties (e.g., type, timeStamp, nativeEvent). The current type assertion masks this incompleteness. If you want to improve robustness, either construct a more complete event object or document that only the value property is guaranteed to be available.

📜 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 3862606 and 371b253.

📒 Files selected for processing (2)
  • packages/components/number-input/__tests__/number-input.test.tsx (1 hunks)
  • packages/components/number-input/src/use-real-time-formatting.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-27T21:48:35.308Z
Learnt from: adbjo
Repo: heroui-inc/heroui PR: 5846
File: packages/components/tabs/src/tabs.tsx:76-101
Timestamp: 2025-10-27T21:48:35.308Z
Learning: In packages/components/tabs/src/tabs.tsx, the updateCursorPosition useCallback dependency array intentionally includes `cursorRef.current` to handle the case where the cursor span element is unmounted and remounted (e.g., when `disableAnimation` or `disableCursorAnimation` toggles). This ensures the callback is recreated when the ref points to a new element, triggering a dependency chain that re-establishes the ResizeObserver and initializes the new cursor element with the data-initialized attribute.
</learning]

Applied to files:

  • packages/components/number-input/__tests__/number-input.test.tsx
  • packages/components/number-input/src/use-real-time-formatting.ts
🧬 Code graph analysis (1)
packages/components/number-input/__tests__/number-input.test.tsx (3)
packages/core/theme/src/components/user.ts (1)
  • user (33-33)
packages/components/number-input/src/index.ts (1)
  • NumberInput (10-10)
packages/core/theme/src/components/input.ts (1)
  • input (926-926)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: TypeScript
  • GitHub Check: Tests
  • GitHub Check: Prettier
  • GitHub Check: Build
  • GitHub Check: Continuous Release
  • GitHub Check: ESLint
🔇 Additional comments (6)
packages/components/number-input/__tests__/number-input.test.tsx (1)

815-822: Timer mocking correctly implemented.

The fake timer setup and teardown have been properly added to support the cursor restoration tests that call jest.runAllTimers(). This addresses the previous review feedback.

packages/components/number-input/src/use-real-time-formatting.ts (5)

27-45: Well-structured cursor restoration helper.

The extraction of this helper function addresses the previous review feedback about code duplication. The logic correctly counts digits to restore cursor position after formatting.


47-83: LGTM - well-structured input handler.

The handler correctly:

  • Skips formatting during composition
  • Validates partial numbers before parsing
  • Checks state constraints before applying formatted values
  • Preserves cursor position
  • Emits onChange events

93-134: LGTM - paste handler correctly implemented.

The paste handler properly:

  • Prevents default behavior to maintain control
  • Validates the resulting value before applying
  • Counts digits accurately for cursor restoration
  • Updates both input and numeric state

136-183: LGTM - cut handler with appropriate NaN handling.

The cut handler correctly:

  • Manages clipboard data manually for control
  • Handles the edge case where cutting results in NaN by clearing the input
  • Validates and formats the remaining value
  • Restores cursor position appropriately

185-237: LGTM - beforeInput handler with proper validation.

The beforeInput handler effectively:

  • Blocks invalid input via preventDefault before it reaches the DOM
  • Validates the resulting value would be parseable
  • Formats immediately for real-time feedback
  • Correctly calculates cursor position including the new input

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 (1)
packages/components/number-input/src/use-real-time-formatting.ts (1)

91-132: Add shouldFormat guard for consistency with handleInput.

handleInput (line 47) returns early when !shouldFormat, but handlePaste, handleCut, and handleBeforeInput lack this guard. If these handlers are ever attached when formatting is disabled, they would still execute formatting logic.

For defensive consistency:

   const handlePaste = useCallback(
     (e: React.ClipboardEvent<HTMLInputElement>) => {
+      if (!shouldFormat) return;
       e.preventDefault();
       const clipboardData = e.clipboardData.getData("text/plain");

Apply the same pattern to handleCut (line 134) and handleBeforeInput (line 183).

📜 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 371b253 and c3e41ed.

📒 Files selected for processing (2)
  • packages/components/number-input/__tests__/number-input.test.tsx (1 hunks)
  • packages/components/number-input/src/use-real-time-formatting.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-27T21:48:35.308Z
Learnt from: adbjo
Repo: heroui-inc/heroui PR: 5846
File: packages/components/tabs/src/tabs.tsx:76-101
Timestamp: 2025-10-27T21:48:35.308Z
Learning: In packages/components/tabs/src/tabs.tsx, the updateCursorPosition useCallback dependency array intentionally includes `cursorRef.current` to handle the case where the cursor span element is unmounted and remounted (e.g., when `disableAnimation` or `disableCursorAnimation` toggles). This ensures the callback is recreated when the ref points to a new element, triggering a dependency chain that re-establishes the ResizeObserver and initializes the new cursor element with the data-initialized attribute.
</learning]

Applied to files:

  • packages/components/number-input/src/use-real-time-formatting.ts
  • packages/components/number-input/__tests__/number-input.test.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Tests
  • GitHub Check: Continuous Release
  • GitHub Check: ESLint
  • GitHub Check: TypeScript
  • GitHub Check: Build
  • GitHub Check: Prettier
🔇 Additional comments (7)
packages/components/number-input/__tests__/number-input.test.tsx (3)

811-821: Timer mocking no longer required with current approach.

The cursor restoration tests now use real timers with await new Promise((resolve) => setTimeout(resolve, 0)) wrapped in act() instead of jest.runAllTimers(). This approach works correctly since it allows the production code's setTimeout(0) to execute before assertions.

This is a valid alternative to the fake timer approach mentioned in the past review.


737-749: LGTM!

The core real-time formatting test correctly validates that typing "1234" results in "1,234" when isRealTimeFormat is enabled with grouping.


770-781: Verify beforeInput event behavior in JSDOM.

This test assumes userEvent.type fires beforeInput events that can be prevented. JSDOM's support for beforeInput events may vary. If this test passes in CI, it confirms the implementation works correctly.

packages/components/number-input/src/use-real-time-formatting.ts (4)

25-43: Cursor restoration helper correctly extracted.

The restoreCursorPosition helper addresses the previous review feedback about duplicated cursor restoration logic. The implementation correctly counts digits and positions the cursor after formatting.


183-214: handleBeforeInput correctly intercepts and formats valid input.

The design of always calling e.preventDefault() at line 210 for valid input is intentional and correct. This allows the hook to take full control of formatting by:

  1. Preventing default input behavior
  2. Manually formatting and setting the value
  3. Restoring cursor position

This approach ensures consistent formatting without race conditions between default behavior and state updates.


155-160: Good edge case handling for cutting all content.

Correctly clears the input and sets numberValue to NaN when the cut operation would result in an invalid/empty number.


73-78: Synthetic event is a minimal mock.

The synthetic ChangeEvent only includes target.value and currentTarget.value. This is sufficient for the common use case of accessing the formatted value, but consumers relying on other event properties (e.g., nativeEvent, bubbles) would receive undefined. This is an acceptable trade-off for this feature scope.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 17, 2025

✅ Actions performed

Full review triggered.

@hasegawa-101
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 17, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@wingkwong wingkwong closed this Dec 19, 2025
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 doesn't apply changes on real time

2 participants