Skip to content

Validate the text of the CreatableSelect number input - #4073

Open
sholzer wants to merge 16 commits into
2026.2from
1954-improve-input-number-validation
Open

Validate the text of the CreatableSelect number input#4073
sholzer wants to merge 16 commits into
2026.2from
1954-improve-input-number-validation

Conversation

@sholzer

@sholzer sholzer commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Changes in this pull request

Resolves #1954

InputNumber is a text input and only reports parseable numbers through onChange, so the number
branch never saw what was typed: 12aaa kept the last valid 12, deleting the digits re-validated
nothing, and a below-min entry was silently rewritten to the boundary value on blur or Enter.

The raw text is now tracked via onInput, and validity requires the parsed text to equal the
committed value — so what the field shows is what gets validated and added.

Additional info

  • The raw text is deliberately not the value prop — feeding back a non-numeric string makes
    AntD's controlled-value effect wipe the display mid-typing.
  • Enter stops propagating: AntD's own onKeyDown flushes and re-aligns into [min, max], with no
    prop to disable it.
  • min/max now constrain created options and not just the steppers, without callers repeating the
    bounds in validate. A parser and an onInput passed through numberInputProps are both
    honoured — the latter had to be forwarded explicitly, since the component is SDK-exported.

Two other behaviour changes worth a look during review:

  • Invalid text stays visible on blur with "Invalid option" instead of being silently discarded.
  • "Option already exists" is suppressed while the input is invalid. It was keyed to the committed
    value, so typing 12a with option 12 present showed it alongside "Invalid option" while
    describing a value no longer in the field. This also affects the string input, where both
    messages could previously appear together.

No new translation keys, and no consumer changes — the fix is entirely in the shared component.

Verification

creatable-select.test.tsx adds 18 tests: the reported symptoms, min on the steppers and on
created options, stale committed values, duplicate submission, message precedence, a custom
parser, onInput forwarding, both locale separator conventions, and the string input. Each was
confirmed to fail against the component before its respective fix. Suite is 432/432; eslint and
tsc --noEmit clean. Also confirmed by hand in the Application Logger's refresh-interval selector.

sholzer and others added 2 commits September 2, 2026 15:57
InputNumber renders a text input and only reports parseable numbers
through onChange, so the number branch never saw what the user actually
typed: "12aaa" left the last valid 12 in state while the field read
"12aaa", deleting the leading digits re-validated nothing, and a
below-min entry was silently rewritten to the boundary value on blur or
Enter.

Track the raw text through onInput alongside the committed value from
onChange, and gate the Add button, the error message and handleAddOption
on both. Passing the raw text as InputNumber's value is not an option --
feeding back a non-numeric string makes the controlled-value effect wipe
the display -- so the committed value stays the value prop.

changeOnBlur is disabled and the Enter key no longer propagates to
InputNumber's own handler, because both flush the input and re-align it
into [min, max]. min stays fully supported: it still bounds the steppers
and sets aria-valuemin, but an out-of-range entry is now reported as
invalid instead of being coerced.

onChange also mirrors the committed value into the tracked text, since
the steppers change the value without firing onInput.

Fixes #1954

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sholzer
sholzer requested a balanced review from Copilot September 2, 2026 14:10
@sholzer sholzer added this to the 2026.2.9 milestone Sep 2, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Numeric bounds, custom parsers, and duplicate-entry resets are not handled correctly.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Improves CreatableSelect number-input validation by tracking and validating raw user text.

Changes:

  • Tracks raw numeric input independently from committed values.
  • Prevents blur/Enter coercion and surfaces invalid-input errors.
  • Adds locale-aware decimal validation.
File summaries
File Description
assets/js/src/core/components/creatable-select/creatable-select.tsx Adds raw-text tracking and numeric validation.
Review details

Suppressed comments (2)

assets/js/src/core/components/creatable-select/creatable-select.tsx:57

  • This validity check ignores numberInputProps.min and max. Since blur coercion is now disabled and Enter flushing is suppressed, a consumer using only numberInputProps={{ min: 1 }} can type 0 and add it; the bounds affect the steppers but no longer constrain created options. Compare the parsed value against both bounds here rather than requiring callers to duplicate them in validate.
    if (inputType === 'number' && !Number.isFinite(Number(numberInputText.trim().replace(decimalSeparator, '.')))) {

assets/js/src/core/components/creatable-select/creatable-select.tsx:131

  • The new raw-text state is only reset after a successful add. If an existing numeric option is submitted with Enter, the duplicate branch clears newOptionText and returns while numberInputText remains stale; without a custom validator this leaves Add enabled for a blank input, and with one it can show “Invalid option” under the blank field. Reset both states in the duplicate branch.
    setNumberInputText('')
  • Files reviewed: 1/2 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +55 to +62
const isInputTextValid = useCallback((): boolean => {
// Grouping separators are rejected on purpose — "1,000" cannot be read unambiguously across locales.
if (inputType === 'number' && !Number.isFinite(Number(numberInputText.trim().replace(decimalSeparator, '.')))) {
return false
}

return validate === undefined || validate(newOptionText.trim())
}, [inputType, numberInputText, decimalSeparator, newOptionText, validate])
sholzer and others added 2 commits September 3, 2026 08:05
Review follow-up to the raw-text validation, which had three gaps.

min and max only constrained the steppers. InputNumber refuses to commit
an out-of-range value, so requiring a committed value in the validity
check enforces the bounds as well, rather than asking callers to repeat
them in validate. Before this, a below-min entry left the Add button
enabled while clicking it added nothing.

Submitting an existing option with Enter cleared the committed value but
left the tracked text behind, which showed "Invalid option" under a
blank field. The duplicate branch now resets both.

A parser supplied through numberInputProps was ignored, because the
check applied Number to the displayed text. A consumer formatting the
input would have had every entry reported invalid.

Adds creatable-select.test.tsx covering the original issue and these
three cases. Every one of the ten tests was confirmed to fail against
the component before its respective fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Stale committed values can still be submitted, duplicate handling can discard invalid text, and consumer onInput callbacks are suppressed.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

assets/js/src/core/components/creatable-select/creatable-select.tsx:123

  • Validate the raw numeric text before handling duplicates. If option 12 already exists, typing 12aaa leaves newOptionText as 12; pressing Enter therefore enters this branch and clears the invalid text before isInputTextValid() runs, contrary to the new validation behavior. Preserve the existing string-input ordering while rejecting invalid numeric text first.
    if (optionExists && !allowDuplicates) {
      setNewOptionText('')
      setNumberInputText('')
      return
  • Files reviewed: 2/3 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread assets/js/src/core/components/creatable-select/creatable-select.tsx
Comment thread assets/js/src/core/components/creatable-select/creatable-select.tsx Outdated
sholzer and others added 3 commits September 3, 2026 08:50
The separator is read exclusively by the number branch of the validity
check, but it was computed on every render of every instance. Resolving
it builds an Intl.NumberFormat and calls formatToParts, so string-mode
consumers -- the select document editable among them, which can appear
many times in one editor -- paid for work they never use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review follow-up. Requiring a committed value was not enough, because
that value can be left over from earlier keystrokes: entering 12 and
then replacing it with a below-min 0 makes InputNumber withhold
onChange, so the field read 0 while 12 was still committed -- and Add
submitted 12. The parsed text must equal the committed number.

handleAddOption checked for duplicates before validity, so submitting
invalid text that shares a prefix with an existing option cleared the
field instead of reporting the problem. Number inputs are now validated
first; the string input keeps its previous ordering.

An onInput handler passed through numberInputProps was dropped, since
the explicit handler replaces it rather than sitting behind the spread.
The component is exported from the SDK, so a consumer's callback simply
stopped running. It is now invoked after the internal state update.

The duplicate message was keyed to the committed value, so typing 12a
with option 12 present showed both "already exists" -- describing a
value no longer in the field -- and "invalid option". It is now gated on
validity, which makes the two mutually exclusive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Locale ambiguity and invalid duplicate string submission remain incorrectly handled.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

assets/js/src/core/components/creatable-select/creatable-select.tsx:125

  • Restricting this early validity check to number inputs leaves string inputs inconsistent with the new message precedence. If an invalid string also matches an existing option, pressing Enter reaches the duplicate branch and clears the field even though the UI labels it “Invalid option.” Validate both input types before handling duplicates; valid duplicates will still be cleared as before.
    if (inputType === 'number' && !isInputTextValid()) {
      return
    }
  • Files reviewed: 2/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread assets/js/src/core/components/creatable-select/creatable-select.tsx Outdated
The validity check mirrored InputNumber's parsing, which reads "." as a
decimal point in every locale. In de that made "1.000" commit 1, so the
check agreed with it and Add created 1 from text denoting one thousand.
It now rejects the separator the active locale does not use, so both
"1.000" in de and "1,000" in en are reported instead of guessed. The
separator itself still comes from the SDK's getDecimalSeparator.

The two keydown handlers collapse into one. The number variant only
existed to stop propagation before delegating, which meant testing for
Enter twice across two callbacks.

The test now renders the real InputNumber wrapper rather than raw antd.
The wrapper supplies the locale decimalSeparator, without which the
de cases pass or fail for reasons the component never sees.

Comments trimmed to the decisions that are not evident from the code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Number coercion breaks supported arbitrary-precision stringMode values and can accept stale committed values.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 2/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread assets/js/src/core/components/creatable-select/creatable-select.tsx Outdated
sholzer and others added 2 commits September 3, 2026 10:52
Reported by SonarQube on #4073.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Pressing Enter still clears invalid string input when it matches an existing option.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 2/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread assets/js/src/core/components/creatable-select/creatable-select.tsx Outdated
The guard was scoped to number inputs, so a string that was both invalid
and equal to an existing option reached the duplicate branch on Enter
and had its text cleared before validation ran -- silently discarding
input the UI had just marked invalid. The ordering predates this branch,
but suppressing the duplicate message while the input is invalid made
the inconsistency visible.

Validity is now checked once, ahead of the duplicate branch, for both
types. For string inputs the check reduces to the caller's validate, so
callers that pass none see no change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sholzer
sholzer requested a balanced review from Copilot September 3, 2026 09:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Newly added nullish checks violate the project’s mandatory lodash type-check convention.

Review details

Suppressed comments (3)

Previously missed (3) — in code that hasn't changed since the last review.

assets/js/src/core/components/creatable-select/creatable-select.test.tsx:62

  • These helpers use direct null comparisons, contrary to the project’s required lodash type-check pattern. Import isNull and use it for both query results (negating it for the positive checks).
    assets/js/src/core/components/creatable-select/creatable-select.tsx:62
  • The project’s type-safe-check guideline requires lodash nullish helpers instead of direct undefined comparisons. Import and use isUndefined(numberInputProps.parser) here before invoking the optional parser.
    assets/js/src/core/components/creatable-select/creatable-select.tsx:71
  • This direct undefined comparison also violates the project’s required lodash type-check pattern. Import isUndefined and use isUndefined(validate) for this optional callback check.
  • Files reviewed: 2/3 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

The contributor guidelines ask for isNull and isUndefined over direct
comparisons. Applied to the four checks this branch introduced; the
pre-existing ones in the same file are left alone to keep the diff on
topic. No behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Numeric coercion can accept empty parser results and collapse distinct high-precision values.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

assets/js/src/core/components/creatable-select/creatable-select.tsx:66

  • Converting both sides to Number defeats InputNumber's stringMode precision guarantee: for example, 9007199254740993 and 9007199254740992 compare equal here. With a bound retaining the latter as the committed value, the former displayed text can therefore validate and submit the stale value. Compare canonical decimal representations with a precision-preserving mechanism instead.
      if (committedText === '' || !Number.isFinite(parsedNumber) || parsedNumber !== Number(committedText)) {
  • Files reviewed: 2/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread assets/js/src/core/components/creatable-select/creatable-select.tsx
@sonarqubecloud

sonarqubecloud Bot commented Sep 3, 2026

Copy link
Copy Markdown

@sholzer
sholzer marked this pull request as ready for review September 3, 2026 09:42
@sholzer
sholzer requested a review from xIrusux September 3, 2026 09:42
@sholzer sholzer linked an issue Sep 3, 2026 that may be closed by this pull request

@xIrusux xIrusux left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM :) just added a suggestion as we try to use - const { t } = useTranslation()

...selectProps
}: CreatableSelectProps): React.JSX.Element => {
const { t } = useTranslation()
const { t, i18n } = useTranslation()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
const { t, i18n } = useTranslation()
const { t } = useTranslation()

const [pendingSelection, setPendingSelection] = useState<SelectOptionType | null>(null)
const allOptions = [...options, ...customOptions]
const decimalSeparator = inputType === 'number'
? numberInputProps.decimalSeparator ?? getDecimalSeparator(i18n?.language)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
? numberInputProps.decimalSeparator ?? getDecimalSeparator(i18n?.language)
? numberInputProps.decimalSeparator ?? getDecimalSeparator()

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.

Improve inputNumber input

3 participants