Skip to content

[number-input] Allow invalid prop to override isOutOfRange error state #2943

@isBatak

Description

@isBatak

🚀 Feature request

The invalid prop on number-input should take precedence over the internal isOutOfRange computation, allowing consumers to fully control the error state.

🧱 Problem Statement / Justification

Currently, the error state is derived like this:

const invalid = computed("isOutOfRange") || !!prop("invalid")

This means that isOutOfRange always forces the component into an invalid state, even when the consumer explicitly provides the invalid prop.

This makes it difficult to control validation externally (e.g. with form libraries), because:
• Custom validation errors cannot override range-based validation
• Error state becomes coupled to internal logic instead of consumer intent

It would be more flexible if the invalid prop had higher priority and only fell back to isOutOfRange when invalid is not provided.

Relevant code:

const invalid = computed("isOutOfRange") || !!prop("invalid")
const isIncrementDisabled = disabled || !computed("canIncrement") || readOnly
const isDecrementDisabled = disabled || !computed("canDecrement") || readOnly
const translations = prop("translations")
return {
focused: focused,

✅ Proposed solution or API

Change the precedence logic so that:
• If invalid prop is defined → use it
• If invalid prop is undefined → fallback to isOutOfRange

For example:

const invalid =
  prop("invalid") !== undefined
    ? prop("invalid")
    : computed("isOutOfRange")

Another option would be to introduce a dedicated computed isInvalid state that encapsulates the precedence logic, instead of inlining it in connect

Using react-hook-form, validation is handled externally and passed down via invalid:

<Field
  label={t('createTickerDialog.form.tickSizeLabel')}
  invalid={!!errors.tickSize}
  errorText={errors.tickSize?.message}
>
  <Controller
    name="tickSize"
    control={control}
    rules={{
      validate: {
        min: (val) =>
          !val || +val >= 10 ** -8 || t('formValidation.min', { value: 10 ** -8 }),
        max: (val) =>
          !val || +val <= 1 || t('formValidation.max', { value: 1 }),
      },
    }}
    render={({ field }) => (
      <NumberInputRoot
        size="sm"
        disabled={field.disabled}
        name={field.name}
        value={field.value}
        onValueChange={({ value }) => {
          field.onChange(value)
        }}
        min={10 ** -8}
        max={1}
        step={10 ** -8}
        formatOptions={{
          maximumFractionDigits: 8,
        }}
      >
        <NumberInputField onBlur={field.onBlur} />
      </NumberInputRoot>
    )}
  />
</Field>

In this scenario, the consumer expects invalid to fully control the error state, but isOutOfRange currently overrides it.

↩️ Alternatives

What alternative solutions have you considered before making this request?

📝 Additional Information

What resources (links, screenshots, etc.) do you have to assist this effort?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions