mui / material-ui

Material UI: Comprehensive React component library that implements Google's Material Design. Free forever.
https://mui.com/material-ui/
MIT License
93.94k stars 32.27k forks source link

[material-ui] Add Number Input component #19154

Open m4theushw opened 4 years ago

m4theushw commented 4 years ago

I noticed in the Material-UI's roadmap a Numeric Input component to be built. If nobody has took the lead then I can help. Recently I had to develop a component like that for a personal project (first screenshot below).

Summary 💡

I didn't look deeply but I think about a <NumericField /> wrapping a <TextField />. This text field component would render the step buttons.

Examples 🌈

Capture d’écran 2020-09-04 à 13 45 42 Capture d’écran 2020-09-04 à 13 46 23

Motivation 🔦

Sometimes we want to force an input to only accept numbers, but the support for type="number" has limitations:

TODO

Benchmarks

https://mui-org.notion.site/Input-Number-component-364825a7bec94381809ac11ff05b4cc0

dcworldwide commented 4 years ago

It would need to support floating point numbers in addition to integers. With the ability to set precision ranges.

My project's current solution was to wrap TextField.

Is adding generic mask property for TextField sufficient? That is more generic....as it ensures userland can define the content for their needs...phone numbers, bank account numbers and of course floats, integers. As these are common...const regex patterns provide by mui would be nice.

m4theushw commented 4 years ago

It would need to support floating point numbers in addition to integers.

@dcworldwide I don't know, step buttons are not very useful with continuous quantities.

Is adding generic mask property for TextField sufficient?

For phone numbers, bank account numbers or currency, masks solve the problem. But to enforce a minimum/maximum value or a custom step (0, 2, 4, 6) regex patterns become huge.

Maybe I need redefine what kind of numeric input I am talking. I see an opportunity for an integer field and a floating-point field.

mbrookes commented 4 years ago

https://elemefe.github.io/element-react/#/en-US/input-number

m4theushw commented 4 years ago

I'm thinking about starting a draft for NumericField. To support currency values should we (1) use Intl.NumberFormat, (2) extend the Material-UI's localization API or (3) delegate all formating to user?

yehonadav commented 4 years ago

i would vote to start with (3)

msalahz commented 4 years ago

https://stackoverflow.com/questions/47798104/set-min-max-on-textfield-type-number

GaddMaster commented 3 years ago

Hey Why when I copy and paste exactly this from the TextField doc it does not work ? <TextField inputProps={{ inputMode: 'numeric', pattern: '[0-9]*' }} /> Daniel

GaddMaster commented 3 years ago

@sscotth Your code sandbox also does not work Chrome Firefox

gearcoded commented 2 years ago

This is a little bit offtopic, but the documentation leads to this page. So, this information might be helpful for someone.

From the examples with the slider, I found the following way to add the Input element with the Number type:

import MuiInput from '@mui/material/Input';

function NumberComponent(props){
  // if you want to use state, uncomment this:
  // let { numberValue } = props || 1;

  // if you want to use state, uncomment this:
  // const [numberValue, handleDurationChange] = useState(numberProperty);

  return (
    <MuiInput
      value={numberValue}
      size="small"
      inputProps={{
        step: 1,
        min: 0,
        max: 99999,
        type: 'number',
      }}
    />
  )
}
aurorapete commented 2 years ago

I wanted to chime and say that, from a product designer's perspective, an incrementer would be very useful to have in Material UI. The software I use on a daily basis (Figma, Adobe products, Cinema 4D) uses this kind of field all the time, and because I work on apps at my company which involve similar visualization and manipulation of values, I want to be able to provide this kind of a control to our users. Pasting a small mockup of the sorts of things we would want it do in our case.

image

So, I'm mainly just chiming in here to say this would be a really valuable to provide in Material UI, and it could help product designers like me advocate more strongly for Material UI in our various spheres of influence. Cheers.

violabg commented 2 years ago

any update on this component?

Benzer1406 commented 2 years ago

Any update here?

AndyFang36 commented 2 years ago

Great, thank you!

mbrookes commented 2 years ago

@siriwatknp Could you link to the source for yours?

siriwatknp commented 2 years ago

Demo: https://next.mui-treasury.com/?path=/story/component-numberinput--default Component src: https://github.com/siriwatknp/mui-treasury/blob/next/packages/component-numberinput/src/NumberInput.tsx useNumberInput hook: https://github.com/siriwatknp/mui-treasury/blob/next/packages/use-number-input/src/useNumberInput.ts

mbrookes commented 2 years ago

Ah, that's why I couldn't find it :)

Thanks!

TheRealCuran commented 2 years ago

Just to make sure: please don't overlook a step parameter in the implementation. And please make it possible to define a set of valid fractions (static and by callback). I have number fields implemented in a tool we use internally, where only a defined set of fractions are OK. Say you press the "up" button and you would only want to got from x.33 to x.5, then there should be some kind of option to define those valid steps.

In case it helps, my Order ID is: :credit_card: 47016

mbrookes commented 2 years ago

Not sure why Sam is asking for interviews to ask what components users would like in v6 when we already have a concrete list based on issue upvotes; and even better, an existing implementation of the most popular request. Nor does it need to wait for v6 - an addition isn't a breaking change. Lab/refine/publish...

afilp commented 2 years ago

If possible, I would "urge" this to happen soon. Why?

Well, I have found myself several times accidentally changing the focused number input while scrolling through the touchpad towards the end of the page to hit the SAVE button. Due to the stepper...

See the result here, this was entered as 24500, I then scrolled down to hit Save and I saved the form, see what happened to the input (I re-entered to the form afterwards):

image

So, this can be done by many users too and they will not even notice! (I noticed this accidentally too)

Thanks!

MVR5991 commented 2 years ago

If possible, I would "urge" this to happen soon. Why?

Well, I have found myself several times accidentally changing the focused number input while scrolling through the touchpad towards the end of the page to hit the SAVE button. Due to the stepper...

We recently had the same bug in our application which took a while to get noticed since it is very subtle. Our workaround for now is something like this:

export const blurTarget = (event: SyntheticEvent) => {
  event.target instanceof HTMLElement && event.target.blur()
}

 <TextField
      size="small"
      type="number"
     onWheel={blurTarget}

My company would also like to see this implemented as soon as possible :)

Thanks

jayarjo commented 1 year ago

Any progress on this?

mnajdova commented 1 year ago

Any progress on this?

We plan to start working on this in this quarter.

oliviertassinari commented 1 year ago

@mj12albert I think that it could be great to spend your first couple of weeks on smaller changes than this issue. There are probably two different NumberInput components to build here. My idea is that with smaller changes, you could maybe learn the codebase, workflows, and build confidence faster.

thiagorochatr commented 1 year ago

Hey guys!

I don't know if it has much to do with the topic of discussion, but I would like to ask a question! If I can't send it here, let me know and I'll remove it! And if possible, point me to the correct place to take this doubt.

In a system I'm building, I ask the user for the value in Brazilian Reais (R$ 00,00 format). This week, I was told that the input is not working on Samsung and OnePlus phones (I suspect it is on any phone with a Chromium-based browser). On my MacBook and Iphone, the input works perfectly.

Can anyone help me, please? Links: https://github.com/thiagorochatr/teste-input-value/blob/main/components/original/InputPriceOG.tsx https://teste-input-value.vercel.app/original

thiagorochatr commented 1 year ago

Hey guys!

I don't know if it has much to do with the topic of discussion, but I would like to ask a question! If I can't send it here, let me know and I'll remove it! And if possible, point me to the correct place to take this doubt.

In a system I'm building, I ask the user for the value in Brazilian Reais (R$ 00,00 format). This week, I was told that the input is not working on Samsung and OnePlus phones (I suspect it is on any phone with a Chromium-based browser). On my MacBook and Iphone, the input works perfectly.

Can anyone help me, please? Links: https://github.com/thiagorochatr/teste-input-value/blob/main/components/original/InputPriceOG.tsx https://teste-input-value.vercel.app/original

Update: fixed!

JahnoelRondon commented 1 year ago

Demo: https://next.mui-treasury.com/?path=/story/component-numberinput--default Component src: https://github.com/siriwatknp/mui-treasury/blob/next/packages/component-numberinput/src/NumberInput.tsx useNumberInput hook: https://github.com/siriwatknp/mui-treasury/blob/next/packages/use-number-input/src/useNumberInput.ts

You can still use + and other math operators

JahnoelRondon commented 1 year ago

Current example solution With Keydown preventing changes based on criteria.

// Solution with keydown
    <TextField
      value={state.value}
      type="number"
      onKeyDown={(e) => {
        if (e.key === "e" || e.key === "E" || e.key === "-" || e.key === "+") {
          e.preventDefault()
        }
      }}
      onChange={(e) => {
        handleChange();
      }}
    />

found this stack over flow helpful https://stackoverflow.com/questions/31706611/why-does-the-html-input-with-type-number-allow-the-letter-e-to-be-entered-in

mj12albert commented 1 year ago

✨ We've released a first iteration on the Number Input for Base UI in v5.14.4! The docs: https://mui.com/base-ui/react-number-input/.

jo-chemla commented 1 year ago

✨ We've released a first iteration on the Number Input for Base UI in v5.14.4!

Great to hear! Will this be ported to Material UI core itself at some point? Thanks!

mbrookes commented 1 year ago

Adding a reminder to update: https://mui.com/material-ui/react-text-field/#type-quot-number-quot when this is released.

nameer commented 1 year ago

Here is my implementation of NumberInput with support for decimal numbers: https://gist.github.com/nameer/664abb9d4e261a210de1f1ba814a0ad2

To use it for integers, simply set decimalScale to 0.

secretwpn commented 1 year ago

One thing about existing number inputs I've always found annoying is the half-sized up/down arrows - they are often too small to be usable and I personally would much prefer something like 123 + - - normal text field followed by icon buttons.

This way the buttons have enough vertical space to get to usable size

nameer commented 1 year ago

Here is my implementation of NumberInput with support for decimal numbers: https://gist.github.com/nameer/664abb9d4e261a210de1f1ba814a0ad2

To use it for integers, simply set decimalScale to 0.

Sample

A live implementation is here: https://codesandbox.io/p/sandbox/numberinput-7fxlym

This component supports setting minimum, maximum, and step values, and also handles decimal numbers. It allows you to provide an initial number, and you can monitor changes via the onChange prop. onChange event will only be triggered if the value is a valid number (typing the dot of "5." will not trigger it).

MiLandry commented 10 months ago

Is there going to be any support for adding labels to number input fields?

Catlike14 commented 10 months ago

It seems to not work with react-hook-form. https://codesandbox.io/p/sandbox/epic-monad-jrtnc2 Am I missing something?

simonetavoletta commented 7 months ago

Hello, is there any news about this new component?

Thank you 😄

xav1639 commented 6 months ago

Super duper excited for this component by the way can't wait 😉

ClementDreptin commented 3 months ago

Hi everyone,

I've been working on such a component for a little while now and I thought the MUI users could benefit from it. It is inspired from the integration with 3rd party input libraries section of the docs. It uses react-number-format under the hood and supports decimal numbers, increment and decrement with buttons or ArrowUp/ArrowDown, custom step, thousand and decimal separators based on the current locale.

That's how it looks: number input

And here is the code:

import * as React from "react";
import { NumericFormat, type NumericFormatProps } from "react-number-format";
import { useLocale, useTranslations } from "next-intl";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import {
  IconButton,
  InputAdornment,
  Stack,
  TextField,
  type IconButtonProps,
  type TextFieldProps,
} from "@mui/material";

const NumericFormatCustom = React.forwardRef<
  HTMLInputElement,
  NumericFormatProps
>(function NumericFormatCustom(props, ref) {
  const { decimalSeparator, thousandSeparator, value, ...rest } = props;
  const locale = useLocale();

  const defaultThousandSeparator = React.useMemo(
    () => getThousandSeparator(locale),
    [locale],
  );

  const defaultDecimalSeparator = React.useMemo(
    () => getDecimalSeparator(locale),
    [locale],
  );

  return (
    <NumericFormat
      {...rest}
      value={value ?? ""} // We can't ever pass null to value because it breaks the shrink state of the label, so we pass empty string instead
      getInputRef={ref}
      thousandSeparator={thousandSeparator ?? defaultThousandSeparator}
      decimalSeparator={decimalSeparator ?? defaultDecimalSeparator}
    />
  );
});

type NumberInputProps = Omit<TextFieldProps, "onChange"> & {
  hideActionButtons?: boolean;
  max?: number;
  min?: number;
  numericFormatProps?: NumericFormatProps;
  onChange?: (value: number | null) => void;
  step?: number;
  value?: number | null;
};

const NumberInput = React.forwardRef<HTMLDivElement, NumberInputProps>(
  function NumberInput(props, ref) {
    const t = useTranslations("NumberInput");
    const {
      disabled = false,
      hideActionButtons = false,
      max = Infinity,
      min = -Infinity,
      numericFormatProps: numericFormatPropsProp,
      onChange,
      size,
      slotProps,
      step = 1,
      value: valueProp,
      ...rest
    } = props;
    const isControlled = valueProp !== undefined && onChange !== undefined;

    // We use an internal state when the component is uncontrolled
    const [fallbackValue, setFallbackValue] = React.useState(valueProp);
    const value = isControlled ? valueProp : fallbackValue;
    const setValue = isControlled ? onChange : setFallbackValue;

    const increment = () => {
      // If we increment when the input is empty, we consider the previous value to be 0
      const newValue =
        (value != null && !Number.isNaN(value) ? value : 0) + step;

      if (newValue > max) {
        return;
      }

      setValue(newValue);
    };

    const decrement = () => {
      // If we decrement when the input is empty, we consider the previous value to be 0
      const newValue =
        (value != null && !Number.isNaN(value) ? value : 0) - step;

      if (newValue < min) {
        return;
      }

      setValue(newValue);
    };

    const numericFormatProps: NumericFormatProps = {
      // We set a default to avoid displaying floating point errors when using a decimal step
      decimalScale: 5,

      // react-number-format doesn't provide min or max props so we do the checking here instead
      isAllowed: ({ floatValue }) => {
        if (floatValue == null) {
          return true;
        }

        return floatValue >= min && floatValue <= max;
      },

      // Only add the min, max and step html attributes when the value isn't the default one
      max: max !== Infinity ? max : undefined,
      min: min !== -Infinity ? min : undefined,
      step: step !== 1 ? step : undefined,

      // Allow to increment with ArrowUp and decrement with ArrowDown
      onKeyDown: (event) => {
        if (event.key === "ArrowUp") {
          increment();
        } else if (event.key === "ArrowDown") {
          decrement();
        }
      },

      onValueChange: ({ floatValue }) => {
        // When incrementing or decrementing, the value prop is already up to date
        // so we make sure the value needs to be updated to prevent an unnecessary re-render
        if (floatValue === value) {
          return;
        }

        setValue(floatValue ?? null);
      },
      value,

      ...numericFormatPropsProp,
    };

    const commonAdornmentButtonProps: IconButtonProps = {
      edge: "end",
      sx: { p: size !== "small" ? "1px" : 0 },
    };

    return (
      <TextField
        {...rest}
        ref={ref}
        value={value ?? ""} // We can't ever pass null to value because it breaks the shrink state of the label, so we pass empty string instead
        disabled={disabled}
        size={size}
        slotProps={{
          ...slotProps,
          input: {
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            inputComponent: NumericFormatCustom as any,
            endAdornment: !hideActionButtons && (
              <InputAdornment position="end">
                <Stack>
                  <IconButton
                    aria-label={t("incrementAriaLabel")}
                    disabled={disabled || (value ?? 0) + step > max}
                    onClick={increment}
                    {...commonAdornmentButtonProps}
                  >
                    <KeyboardArrowUpIcon fontSize={size} />
                  </IconButton>
                  <IconButton
                    aria-label={t("decrementAriaLabel")}
                    disabled={disabled || (value ?? 0) - step < min}
                    onClick={decrement}
                    {...commonAdornmentButtonProps}
                  >
                    <KeyboardArrowDownIcon fontSize={size} />
                  </IconButton>
                </Stack>
              </InputAdornment>
            ),
            ...slotProps?.input,
          },
          // @ts-expect-error The type should be React.ComponentProps<typeof inputComponent> but instead
          // it is hard-coded to InputBaseComponentProps
          htmlInput: { ...slotProps?.htmlInput, ...numericFormatProps },
        }}
      />
    );
  },
);

// Taken from here:
// https://stackoverflow.com/questions/32054025/how-to-determine-thousands-separator-in-javascript#comments-77517574

function getThousandSeparator(locale: string) {
  return (
    new Intl.NumberFormat(locale)
      .formatToParts(1234.5678)
      .find((part) => part.type === "group")?.value ?? ""
  );
}

function getDecimalSeparator(locale: string) {
  return (
    new Intl.NumberFormat(locale)
      .formatToParts(1234.5678)
      .find((part) => part.type === "decimal")?.value ?? "."
  );
}

export default NumberInput;

You can hard code the aria-labels and just pass undefined to getThousandSeparator and getDecimalSeparator if your app doesn't support internationalization.

Hope it helps!

Edit: Replaced InputProps with slotProps.input and inputProps with slotProps.htmlInput as they became deprecated in MUI v6.

solovevayaroslavna commented 2 months ago

Hello, is there any news about porting Number Input from Base IU to material UI? Thank you 🙇

sm-rrapatwar commented 1 month ago

I would love to see Number Input component implemented in MUI similar to Ant Design and BlueprintJs. It should support +, - in the input, and floating point number. The step size should also support floating point number. There are few other properties that are missing. A combination of below would be awesome.

https://ant.design/components/input-number

https://blueprintjs.com/docs/#core/components/numeric-input

timrutter commented 3 weeks ago

For me this is completely fundamental and I'm not sure i understand why it doesnt exist. the number of bugs we get due to numbers being strings due to being forced to use a textfield for entering numbers...

rnnyrk commented 2 weeks ago

@ClementDreptin Thanks a lot, was struggling implementing this, by MUI recommended, NumberInput with React Hook Form from https://mui.com/base-ui/react-number-input/#hook

No success, yours works instantly and perfectly.

siriwatknp commented 2 weeks ago

I would love to see Number Input component implemented in MUI similar to Ant Design and BlueprintJs. It should support +, - in the input, and floating point number. The step size should also support floating point number. There are few other properties that are missing. A combination of below would be awesome.

You can get the components from MUI Treasury:

https://github.com/user-attachments/assets/0f911c9e-670a-48a5-96b2-28aa72ed731e