Skip to content

Conversation

@BarriosXJavier
Copy link
Contributor

@BarriosXJavier BarriosXJavier commented Sep 27, 2025

This implementation allows numbers to be formatted according to different regional conventions.
The NumberField.Root component accepts a locale prop that uses the Intl.NumberFormat API to format the displayed value based on the provided locale.

Summary by CodeRabbit

  • New Features

    • Locale-aware NumberField: new locale option, inputs display formatted values and expose focus/blur callbacks.
    • Input type changed to text and Enter blurs the field to support localized decimals.
  • Bug Fixes

    • Improved parsing for locale decimal separators, negatives, and empty input.
    • More robust min/max clamping and refined step/large-step behavior while typing.
  • Documentation

    • Added DecimalValues and WithLocale stories; expanded controlled and form demos.

@changeset-bot
Copy link

changeset-bot bot commented Sep 27, 2025

⚠️ No Changeset found

Latest commit: 4a53d56

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 27, 2025

Walkthrough

NumberField was refactored to use locale-aware string handling: context now provides formattedValue and optional locale, handleOnChange accepts a string, input switched to text, root adds parsing/formatting/clamping and focus/blur handling, and stories updated with locale and new examples.

Changes

Cohort / File(s) Summary
NumberField Context
src/components/ui/NumberField/contexts/NumberFieldContext.tsx
Extends NumberFieldContextType with formattedValue: string, optional locale?: string, onFocus/onBlur handlers; changes handleOnChange signature to (input: string) => void.
NumberField Input (render & events)
src/components/ui/NumberField/fragments/NumberFieldInput.tsx
Switches input type from number to text; binds value to formattedValue; onChange forwards raw string; adds Enter-to-blur, preserves arrow-step key handling and className composition; wires onFocus/onBlur.
NumberField Root (logic, parsing, formatting)
src/components/ui/NumberField/fragments/NumberFieldRoot.tsx
Adds locale?: string prop; introduces stringValue and isTyping state, locale-aware decimal separator detection, parse/normalize from string→number, clamp to min/max, format via Intl.NumberFormat, updated applyStep, and focus/blur handlers; exposes formattedValue and locale via context.
Stories (examples, locale demos)
src/components/ui/NumberField/stories/NumberField.stories.tsx
Rewrites stories formatting; adds DecimalValues and WithLocale exports; updates Basic, Controlled, and FormExample to use string/number controlled state, submit/display behavior, input width styling, and demonstrates locale usage (e.g., en-GB).

Sequence Diagram(s)

sequenceDiagram
  participant U as User
  participant NI as NumberFieldInput
  participant NC as NumberFieldContext
  participant NR as NumberFieldRoot
  participant NF as Intl.NumberFormat

  U->>NI: Type/edit text (keys / Enter / arrows)
  NI->>NC: handleOnChange(rawString)
  NC->>NR: onChange(rawString)
  rect rgba(200,230,255,0.18)
    note right of NR: Locale-aware parsing & validation
    NR->>NR: detect decimal separator (locale)
    NR->>NR: normalize string → numeric or empty
    NR->>NR: clamp to min/max if numeric
    NR->>NF: format numeric value (locale)
    NF-->>NR: formatted string
  end
  NR-->>NC: update { inputValue, formattedValue, locale }
  NC-->>NI: rerender with formattedValue
  NI->>NC: onFocus / onBlur events
  NC->>NR: propagate focus/blur callbacks
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

automerge

Suggested reviewers

  • kotAPI

Poem

I nibble commas, hop on keys,
From text to number with gentle ease.
Locale twirls the dotted line,
Focus, blur — the digits align.
Bunny formats carrots just fine. 🐰✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly and accurately reflects the main change—adding locale-based number formatting support to the NumberField component—and aligns with the pull request’s objectives and implementation details.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6159427 and 4a53d56.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • src/components/ui/NumberField/contexts/NumberFieldContext.tsx (1 hunks)
  • src/components/ui/NumberField/fragments/NumberFieldInput.tsx (3 hunks)
  • src/components/ui/NumberField/fragments/NumberFieldRoot.tsx (4 hunks)
  • src/components/ui/NumberField/stories/NumberField.stories.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/ui/NumberField/contexts/NumberFieldContext.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/ui/NumberField/fragments/NumberFieldRoot.tsx (1)
src/core/hooks/useControllableState/index.tsx (1)
  • useControllableState (15-78)
⏰ 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). (1)
  • GitHub Check: coverage

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.

Copy link
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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5446ae5 and 2933cf7.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • src/components/ui/NumberField/contexts/NumberFieldContext.tsx (2 hunks)
  • src/components/ui/NumberField/fragments/NumberFieldInput.tsx (3 hunks)
  • src/components/ui/NumberField/fragments/NumberFieldRoot.tsx (1 hunks)
  • src/components/ui/NumberField/stories/NumberField.stories.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/ui/NumberField/fragments/NumberFieldRoot.tsx (1)
src/core/hooks/useControllableState/index.tsx (1)
  • useControllableState (15-78)
⏰ 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). (2)
  • GitHub Check: Build
  • GitHub Check: coverage

Copy link
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: 4

🧹 Nitpick comments (7)
src/components/ui/NumberField/fragments/NumberFieldInput.tsx (1)

15-26: Restore mobile keypad + improve a11y for text input spinbutton

Switching to type="text" drops numeric keypad hints and built‑in semantics. Add inputMode/pattern for better virtual keyboards and expose ARIA spinbutton attributes. Requires min/max to be provided by context (see Root comment).

   const {
-        formattedValue,
+        formattedValue,
         handleOnChange,
         handleStep,
         id,
         name,
         disabled,
         readOnly,
         required,
-        rootClass,
+        rootClass,
         onFocus,
         onBlur,
+        // optional; add in Root context to enable these aria props
+        min,
+        max,
     } = context;
@@
-        <input
+        <input
             ref={ref}
             type="text"
             onKeyDown={handleKeyDown}
             value={formattedValue}
             onChange={(e) => handleOnChange(e.target.value)}
             onFocus={onFocus}
             onBlur={onBlur}
+            inputMode="decimal"
+            pattern="[0-9]+([.,][0-9]+)?"
+            role="spinbutton"
+            aria-valuemin={typeof min === 'number' ? min : undefined}
+            aria-valuemax={typeof max === 'number' ? max : undefined}
+            aria-valuenow={
+              formattedValue === '' || readOnly ? undefined : (context.inputValue as number | undefined)
+            }
             id={id}
             name={name}
             disabled={disabled}
             readOnly={readOnly}
             required={required}

Also applies to: 50-58

src/components/ui/NumberField/contexts/NumberFieldContext.tsx (1)

3-17: Context shape looks good; consider naming clarity for handler

The additions make sense. Minor: handleOnChange now carries a string; renaming to onInputStringChange would reduce ambiguity with onValueChange at the Root level. Optional.

src/components/ui/NumberField/stories/NumberField.stories.tsx (1)

107-120: Add a locale with non‑Latin digits to validate parsing

Great to see en‑GB. Please add an Arabic or Persian example (e.g., ar‑EG) and a negative value to exercise minus sign and localized numerals.

 export const WithLocale = () => (
   <SandboxEditor>
     <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
       <div>
         <p>British (en-GB)</p>
         <NumberField.Root defaultValue={1234567.89} locale="en-GB" step={1} largeStep={100}>
           <NumberField.Decrement>-</NumberField.Decrement>
           <NumberField.Input style={{ width: '250px' }} />
           <NumberField.Increment>+</NumberField.Increment>
         </NumberField.Root>
       </div>
+      <div>
+        <p>Arabic (ar-EG) with negative</p>
+        <NumberField.Root defaultValue={-1234.56} locale="ar-EG" step={0.1} largeStep={10}>
+          <NumberField.Decrement>-</NumberField.Decrement>
+          <NumberField.Input style={{ width: '250px' }} />
+          <NumberField.Increment>+</NumberField.Increment>
+        </NumberField.Root>
+      </div>
     </div>
   </SandboxEditor>
 );
src/components/ui/NumberField/fragments/NumberFieldRoot.tsx (4)

35-41: Formatting effect handles empty state correctly; cache formatter for perf

Nice fix for empty string. Consider memoizing the formatter to avoid re-creating Intl.NumberFormat on every inputValue/typing toggle.

-    useEffect(() => {
+    const numberFormatter = React.useMemo(
+      () => new Intl.NumberFormat(locale || 'en-US', { maximumFractionDigits: 20 }),
+      [locale]
+    );
+    useEffect(() => {
         if (!isTyping) {
-            const formatted = inputValue === '' ? '' : new Intl.NumberFormat(locale ? locale : 'en-US', { maximumFractionDigits: 20 }).format(inputValue);
+            const formatted = inputValue === '' ? '' : numberFormatter.format(inputValue);
             setStringValue(formatted);
         }
-    }, [inputValue, isTyping, locale]);
+    }, [inputValue, isTyping, numberFormatter]);

42-45: Expose all separators and minus sign once

You’ll need group and minusSign for robust parsing. Compute them once.

-    const getDecimalSeparator = (locale: string) => {
-        const parts = new Intl.NumberFormat(locale).formatToParts(1234.5);
-        return parts.find(part => part.type === 'decimal')?.value || '.';
-    }
+    const localeTokens = React.useMemo(() => {
+        const nf = new Intl.NumberFormat(locale || 'en-US');
+        const parts = nf.formatToParts(1234567.89);
+        const decimal = parts.find(p => p.type === 'decimal')?.value || '.';
+        const group = parts.find(p => p.type === 'group')?.value || ',';
+        const minusSign = new Intl.NumberFormat(locale || 'en-US')
+          .formatToParts(-1)
+          .find(p => p.type === 'minusSign')?.value || '-';
+        return { decimal, group, minusSign };
+    }, [locale]);

92-97: On focus, show locale digits without grouping for a smoother edit experience

toString() yields ASCII digits and ignores numbering systems. Use Intl.NumberFormat with useGrouping: false so users in locales like ar‑EG see expected numerals while editing.

-    const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
-        setIsTyping(true);
-        const currentStringValue = inputValue === '' ? '' : inputValue.toString().replace('.', getDecimalSeparator(locale || 'en-US'));
-        setStringValue(currentStringValue);
+    const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
+        setIsTyping(true);
+        const nfNoGroup = new Intl.NumberFormat(locale || 'en-US', { useGrouping: false, maximumFractionDigits: 20 });
+        const currentStringValue = inputValue === '' ? '' : nfNoGroup.format(inputValue as number);
+        setStringValue(currentStringValue);
         onFocus?.(e);
     };

150-164: Expose min/max in context to unlock ARIA on the Input

Propagate min and max so NumberFieldInput can set aria‑valuemin/max and improve accessibility.

     const contextValues = {
         inputValue,
         formattedValue: stringValue,
         locale,
         handleOnChange,
         handleStep,
         id,
         name,
+        min,
+        max,
         disabled,
         readOnly,
         required,
         rootClass,
         onFocus: handleFocus,
         onBlur: handleBlur,
     };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2933cf7 and 6159427.

📒 Files selected for processing (4)
  • src/components/ui/NumberField/contexts/NumberFieldContext.tsx (1 hunks)
  • src/components/ui/NumberField/fragments/NumberFieldInput.tsx (3 hunks)
  • src/components/ui/NumberField/fragments/NumberFieldRoot.tsx (4 hunks)
  • src/components/ui/NumberField/stories/NumberField.stories.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/ui/NumberField/fragments/NumberFieldRoot.tsx (1)
src/core/hooks/useControllableState/index.tsx (1)
  • useControllableState (15-78)
⏰ 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). (2)
  • GitHub Check: Build
  • GitHub Check: coverage
🔇 Additional comments (2)
src/components/ui/NumberField/fragments/NumberFieldRoot.tsx (2)

47-86: Note: minus sign handling improved; empty->"" fix looks good

Leading '-' is preserved during typing and empty string no longer formats to 0. Remaining gaps covered above.

Also applies to: 105-127


47-86: Locale digits are not parsed; Arabic/Indic numerals break input and step

Sanitization only keeps [0-9] and the decimal separator. This rejects valid numerals in locales that use non‑Latin digits and breaks typing/stepping in those locales. Also only ASCII '-' is recognized.

-    const parseAndSetValue = (val: string) => {
+    // Map localized digits to ASCII and normalize separators before parsing
+    const buildDigitMap = React.useCallback((loc: string) => {
+        const map = new Map<string, string>();
+        for (let d = 0; d <= 9; d++) {
+            map.set(d.toLocaleString(loc), String(d));
+        }
+        return map;
+    }, []);
+    const digitMap = React.useMemo(() => buildDigitMap(locale || 'en-US'), [buildDigitMap, locale]);
+    const mapDigitsToAscii = (s: string) => {
+        let out = '';
+        for (const ch of s) out += digitMap.get(ch) ?? ch;
+        return out;
+    };
+
+    const parseAndSetValue = (val: string) => {
         if (val === '') {
             setInputValue('');
             return;
         }
-
-        const decimal = getDecimalSeparator(locale || 'en-US');
-        let isNegative = false;
-        let numberString = val;
-
-        if (val.startsWith('-')) {
-            isNegative = true;
-            numberString = val.substring(1);
-        }
-
-        const regex = new RegExp(`[^0-9${decimal}]`, 'g');
-        const cleaned = numberString.replace(regex, '');
-        const normalized = cleaned.replace(decimal, '.');
-        let numericValue = parseFloat(normalized);
+        const { decimal, group, minusSign } = localeTokens;
+        const trimmed = val.trim();
+        const hasMinus = trimmed.startsWith('-') || trimmed.startsWith(minusSign);
+        const withoutSign = hasMinus
+          ? trimmed.slice(trimmed.startsWith(minusSign) ? minusSign.length : 1)
+          : trimmed;
+        // Convert localized digits to ASCII, strip grouping, keep only first decimal
+        const ascii = mapDigitsToAscii(withoutSign).split(group).join('');
+        const parts = ascii.split(decimal);
+        const normalized = parts[0] + (parts.length > 1 ? '.' + parts.slice(1).join('') : '');
+        let numericValue = parseFloat(normalized);
 
         if (isNaN(numericValue)) {
             // If parsing fails (e.g., user just typed "-"), do nothing with the numeric value
             // but keep the string value for the input.
             return;
         }
 
-        if (isNegative) {
+        if (hasMinus) {
             numericValue *= -1;
         }
 
         if (max !== undefined && numericValue > max) {
             numericValue = max;
         }
 
         if (min !== undefined && numericValue < min) {
             numericValue = min;
         }
 
         setInputValue(numericValue);
     }

Comment on lines +45 to +47
if (event.key === 'Enter') {
(event.target as HTMLInputElement).blur();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Prevent unintended form submission on Enter

Pressing Enter blurs the input but still allows default form submission. Prevent it to avoid accidental submits while committing the value.

-        if (event.key === 'Enter') {
-            (event.target as HTMLInputElement).blur();
-        }
+        if (event.key === 'Enter') {
+            event.preventDefault();
+            (event.target as HTMLInputElement).blur();
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (event.key === 'Enter') {
(event.target as HTMLInputElement).blur();
}
if (event.key === 'Enter') {
event.preventDefault();
(event.target as HTMLInputElement).blur();
}
🤖 Prompt for AI Agents
In src/components/ui/NumberField/fragments/NumberFieldInput.tsx around lines 45
to 47, the Enter key handler currently blurs the input but does not prevent the
default form submission; update the handler so when event.key === 'Enter' you
call event.preventDefault() (and optionally event.stopPropagation()) before
blurring to stop the browser submitting the form, and ensure the event is typed
as a KeyboardEvent<HTMLInputElement> so preventDefault is available.

Comment on lines 105 to 127
const applyStep = (amount: number) => {
setInputValue((prev) => {
let temp = prev;
if (temp === '') {
if (min !== undefined) {
temp = min;
} else {
temp = -1;
}
}
const nextValue = temp + amount;

if (max !== undefined && nextValue > max) {
return max;
}

if (min !== undefined && nextValue < min) {
return min;
}

return nextValue;
});
const valueToStep = isTyping ? parseFloat(stringValue.replace(getDecimalSeparator(locale || 'en-US'), '.')) : (inputValue || 0);

let startingValue = valueToStep;
if(isNaN(valueToStep)) {
startingValue = 0;
}

let nextValue = startingValue + amount;

if (max !== undefined && nextValue > max) {
nextValue = max;
}

if (min !== undefined && nextValue < min) {
nextValue = min;
}

setInputValue(nextValue);
const nextStringValue = nextValue.toString().replace('.', getDecimalSeparator(locale || 'en-US'));
setStringValue(nextStringValue);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Stepping while typing fails for localized digits and can expose float artifacts

  • parseFloat on the raw string ignores localized numerals.
  • Using JS float math can surface 0.30000000000004, etc.

Normalize input using the same mapping as parseAndSetValue and round to the precision of the step amount.

-    const applyStep = (amount: number) => {
-        const valueToStep = isTyping ? parseFloat(stringValue.replace(getDecimalSeparator(locale || 'en-US'), '.')) : (inputValue || 0);
-        
-        let startingValue = valueToStep;
-        if(isNaN(valueToStep)) {
-            startingValue = 0;
-        }
-
-        let nextValue = startingValue + amount;
+    const decimalsOf = (n: number) => {
+        const s = String(n);
+        return s.includes('.') ? s.split('.')[1].length : 0;
+    };
+    const applyStep = (amount: number) => {
+        const { decimal, group } = localeTokens;
+        let startingValue: number;
+        if (isTyping) {
+            // normalize localized input while typing
+            const ascii = mapDigitsToAscii(stringValue).split(group).join('');
+            const parts = ascii.split(decimal);
+            const normalized = parts[0] + (parts.length > 1 ? '.' + parts.slice(1).join('') : '');
+            const parsed = parseFloat(normalized);
+            startingValue = isNaN(parsed) ? 0 : parsed;
+        } else {
+            startingValue = (inputValue as number) || 0;
+        }
+        // round to precision of the step to avoid float artifacts
+        let nextValue = Number((startingValue + amount).toFixed(decimalsOf(amount)));
 
         if (max !== undefined && nextValue > max) {
             nextValue = max;
         }
 
         if (min !== undefined && nextValue < min) {
             nextValue = min;
         }
 
         setInputValue(nextValue);
-        
-        const nextStringValue = nextValue.toString().replace('.', getDecimalSeparator(locale || 'en-US'));
-        setStringValue(nextStringValue);
+        const nfNoGroup = new Intl.NumberFormat(locale || 'en-US', { useGrouping: false, maximumFractionDigits: 20 });
+        setStringValue(nfNoGroup.format(nextValue));
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const applyStep = (amount: number) => {
setInputValue((prev) => {
let temp = prev;
if (temp === '') {
if (min !== undefined) {
temp = min;
} else {
temp = -1;
}
}
const nextValue = temp + amount;
if (max !== undefined && nextValue > max) {
return max;
}
if (min !== undefined && nextValue < min) {
return min;
}
return nextValue;
});
const valueToStep = isTyping ? parseFloat(stringValue.replace(getDecimalSeparator(locale || 'en-US'), '.')) : (inputValue || 0);
let startingValue = valueToStep;
if(isNaN(valueToStep)) {
startingValue = 0;
}
let nextValue = startingValue + amount;
if (max !== undefined && nextValue > max) {
nextValue = max;
}
if (min !== undefined && nextValue < min) {
nextValue = min;
}
setInputValue(nextValue);
const nextStringValue = nextValue.toString().replace('.', getDecimalSeparator(locale || 'en-US'));
setStringValue(nextStringValue);
};
// Helper to count decimal places in the step amount
const decimalsOf = (n: number) => {
const s = String(n);
return s.includes('.') ? s.split('.')[1].length : 0;
};
const applyStep = (amount: number) => {
const { decimal, group } = localeTokens;
let startingValue: number;
if (isTyping) {
// Normalize localized input while typing
// 1) Map locale digits to ASCII
// 2) Strip grouping separators
// 3) Replace decimal separator with '.'
const ascii = mapDigitsToAscii(stringValue).split(group).join('');
const parts = ascii.split(decimal);
const normalized = parts[0] + (parts.length > 1 ? '.' + parts.slice(1).join('') : '');
const parsed = parseFloat(normalized);
startingValue = isNaN(parsed) ? 0 : parsed;
} else {
startingValue = (inputValue as number) || 0;
}
// Round to the precision of the step to avoid float artifacts
let nextValue = Number((startingValue + amount).toFixed(decimalsOf(amount)));
if (max !== undefined && nextValue > max) {
nextValue = max;
}
if (min !== undefined && nextValue < min) {
nextValue = min;
}
setInputValue(nextValue);
// Format without grouping, preserving locale-specific decimal separator
const nfNoGroup = new Intl.NumberFormat(locale || 'en-US', {
useGrouping: false,
maximumFractionDigits: 20,
});
setStringValue(nfNoGroup.format(nextValue));
};
🤖 Prompt for AI Agents
In src/components/ui/NumberField/fragments/NumberFieldRoot.tsx around lines 105
to 127, the applyStep function currently uses parseFloat on the raw string
(which ignores localized numerals) and plain JS float arithmetic (which can
expose artifacts like 0.30000000000004); change it to normalize the input using
the same parsing/mapping logic as parseAndSetValue (convert localized digits and
decimal separator into a canonical number), derive the precision from the step
amount (count decimal places of step), perform the arithmetic on integers or use
rounding to that precision after adding the step, then enforce min/max and
setInputValue/setStringValue using the localized formatting (replace '.' with
locale decimal separator and format to the determined precision) so stepping
works with localized digits and avoids float artifacts.

Comment on lines 20 to +68
export const Controlled = () => {
const [value, setValue] = React.useState<number | ''>(3);
return (
<SandboxEditor>
<NumberField.Root value={value} onValueChange={setValue} defaultValue={3} step={1} min={0} max={10} largeStep={5}>
<NumberField.Decrement>-</NumberField.Decrement>
<NumberField.Input />
<NumberField.Increment>+</NumberField.Increment>
</NumberField.Root>
<div style={{ marginTop: 8 }}>Current value: {value}</div>
</SandboxEditor>
);
const [value, setValue] = React.useState<number | "">(3);
return (
<SandboxEditor>
<NumberField.Root
value={value}
onValueChange={setValue}
defaultValue={3}
step={1}
min={0}
max={10}
largeStep={5}
>
<NumberField.Decrement>-</NumberField.Decrement>
<NumberField.Input style={{ width: '250px' }} />
<NumberField.Increment>+</NumberField.Increment>
</NumberField.Root>
<div style={{ marginTop: 8 }}>Current value: {value}</div>
</SandboxEditor>
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid passing both value and defaultValue in controlled story

This triggers the dev warning in useControllableState and can confuse consumers. Drop defaultValue when the component is controlled.

   <NumberField.Root
     value={value}
     onValueChange={setValue}
-    defaultValue={3}
     step={1}
     min={0}
     max={10}
     largeStep={5}
   >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const Controlled = () => {
const [value, setValue] = React.useState<number | ''>(3);
return (
<SandboxEditor>
<NumberField.Root value={value} onValueChange={setValue} defaultValue={3} step={1} min={0} max={10} largeStep={5}>
<NumberField.Decrement>-</NumberField.Decrement>
<NumberField.Input />
<NumberField.Increment>+</NumberField.Increment>
</NumberField.Root>
<div style={{ marginTop: 8 }}>Current value: {value}</div>
</SandboxEditor>
);
const [value, setValue] = React.useState<number | "">(3);
return (
<SandboxEditor>
<NumberField.Root
value={value}
onValueChange={setValue}
defaultValue={3}
step={1}
min={0}
max={10}
largeStep={5}
>
<NumberField.Decrement>-</NumberField.Decrement>
<NumberField.Input style={{ width: '250px' }} />
<NumberField.Increment>+</NumberField.Increment>
</NumberField.Root>
<div style={{ marginTop: 8 }}>Current value: {value}</div>
</SandboxEditor>
);
export const Controlled = () => {
const [value, setValue] = React.useState<number | "">(3);
return (
<SandboxEditor>
<NumberField.Root
value={value}
onValueChange={setValue}
step={1}
min={0}
max={10}
largeStep={5}
>
<NumberField.Decrement>-</NumberField.Decrement>
<NumberField.Input style={{ width: '250px' }} />
<NumberField.Increment>+</NumberField.Increment>
</NumberField.Root>
<div style={{ marginTop: 8 }}>Current value: {value}</div>
</SandboxEditor>
);
};
🤖 Prompt for AI Agents
In src/components/ui/NumberField/stories/NumberField.stories.tsx around lines 49
to 68, the Controlled story passes both value and defaultValue which triggers a
dev warning and confuses consumers; remove the defaultValue prop from the
NumberField.Root when using controlled value (keep value, onValueChange, step,
min, max, largeStep) so the component is purely controlled and retain the local
React state initialization as needed.

Comment on lines 34 to +105
export const FormExample = () => {
const [fieldValue, setFieldValue] = React.useState<number | ''>(2);
const [submitted, setSubmitted] = React.useState<number | null>(null);
return (
<SandboxEditor>
<form
onSubmit={e => {
e.preventDefault();
setSubmitted(fieldValue === '' ? null : fieldValue);
}}
>
<NumberField.Root name="quantity" value={fieldValue} onValueChange={setFieldValue} defaultValue={2} step={1} min={0} max={10} largeStep={5}>
const [fieldValue, setFieldValue] = React.useState<number | "">(2);
const [submitted, setSubmitted] = React.useState<number | null>(null);
return (
<SandboxEditor>
<form
onSubmit={(e) => {
e.preventDefault();
setSubmitted(fieldValue === "" ? null : fieldValue);
}}
>
<NumberField.Root
name="quantity"
value={fieldValue}
onValueChange={setFieldValue}
defaultValue={2}
step={1}
min={0}
max={10}
largeStep={5}
>
<NumberField.Decrement>-</NumberField.Decrement>
<NumberField.Input style={{ width: '250px' }} />
<NumberField.Increment>+</NumberField.Increment>
</NumberField.Root>
<button type="submit" style={{ marginTop: 8 }}>
Submit
</button>
</form>
{submitted !== null && (
<div style={{ marginTop: 8 }}>Submitted value: {submitted}</div>
)}
</SandboxEditor>
);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Same issue in FormExample: remove defaultValue when controlled

The form passes value/onValueChange and defaultValue simultaneously.

   <NumberField.Root
     name="quantity"
     value={fieldValue}
     onValueChange={setFieldValue}
-    defaultValue={2}
     step={1}
     min={0}
     max={10}
     largeStep={5}
   >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const FormExample = () => {
const [fieldValue, setFieldValue] = React.useState<number | ''>(2);
const [submitted, setSubmitted] = React.useState<number | null>(null);
return (
<SandboxEditor>
<form
onSubmit={e => {
e.preventDefault();
setSubmitted(fieldValue === '' ? null : fieldValue);
}}
>
<NumberField.Root name="quantity" value={fieldValue} onValueChange={setFieldValue} defaultValue={2} step={1} min={0} max={10} largeStep={5}>
const [fieldValue, setFieldValue] = React.useState<number | "">(2);
const [submitted, setSubmitted] = React.useState<number | null>(null);
return (
<SandboxEditor>
<form
onSubmit={(e) => {
e.preventDefault();
setSubmitted(fieldValue === "" ? null : fieldValue);
}}
>
<NumberField.Root
name="quantity"
value={fieldValue}
onValueChange={setFieldValue}
defaultValue={2}
step={1}
min={0}
max={10}
largeStep={5}
>
<NumberField.Decrement>-</NumberField.Decrement>
<NumberField.Input style={{ width: '250px' }} />
<NumberField.Increment>+</NumberField.Increment>
</NumberField.Root>
<button type="submit" style={{ marginTop: 8 }}>
Submit
</button>
</form>
{submitted !== null && (
<div style={{ marginTop: 8 }}>Submitted value: {submitted}</div>
)}
</SandboxEditor>
);
};
<NumberField.Root
name="quantity"
value={fieldValue}
onValueChange={setFieldValue}
step={1}
min={0}
max={10}
largeStep={5}
>
🤖 Prompt for AI Agents
In src/components/ui/NumberField/stories/NumberField.stories.tsx around lines 71
to 105, the NumberField.Root is being used as a controlled component (it
receives value and onValueChange) but also provides defaultValue; remove the
defaultValue prop to avoid mixing controlled and uncontrolled behavior and rely
on the value/onValueChange pair to control the component (or alternatively make
it uncontrolled by removing value and onValueChange if that was intended).

@BarriosXJavier BarriosXJavier force-pushed the barrios/add-api-support-numberfield-locale-1601 branch from 6159427 to 4a53d56 Compare October 2, 2025 06:29
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.

1 participant