Number Input

PreviousNext

A number input component with various features and customization options.

Docs
mui-treasuryitem

Preview

Loading preview…
components/number-input/number-input.tsx
/* eslint-disable @typescript-eslint/no-unused-vars */
import React from "react";
import { unstable_useId as useId } from "@mui/material/utils";
import Box from "@mui/material/Box";
import ButtonBase from "@mui/material/ButtonBase";
import OutlinedInput, { OutlinedInputProps } from "@mui/material/OutlinedInput";
import SvgIcon from "@mui/material/SvgIcon";
import FormControl from "@mui/material/FormControl";
import FormHelperText from "@mui/material/FormHelperText";
import InputLabel from "@mui/material/InputLabel";
import {
  useNumberInput,
  UseNumberInputOptions,
} from "@/registry/hooks/use-number-input";

export type {
  NumberInputError,
  NumberInputEventType,
} from "@/registry/hooks/use-number-input";

export type NumberInputProps = UseNumberInputOptions & {
  inputElement?: React.ReactElement;
  incrementIcon?: React.ReactNode;
  decrementIcon?: React.ReactNode;
} & Omit<OutlinedInputProps, "onChange">;

export const NumberInput = React.forwardRef<HTMLDivElement, NumberInputProps>(
  function NumberInput(props, ref) {
    const {
      endAdornment = null,
      defaultValue,
      allowMouseWheel,
      keepWithinRange,
      clampValueOnBlur,
      focusInputOnChange,
      formatter,
      parser,
      onChange,
      ...other
    } = props;

    const { inputRef, getInputProps, getIncrementProps, getDecrementProps } =
      useNumberInput(props);

    return (
      <OutlinedInput
        ref={ref}
        {...other}
        inputRef={inputRef}
        inputProps={getInputProps()}
        endAdornment={
          <>
            {endAdornment}
            <Box
              sx={(theme) => ({
                display: "flex",
                flexDirection: "column",
                alignSelf: "stretch",
                marginRight: "-14px",
                marginLeft: "0.5rem",
                borderLeft: `1px solid ${
                  (theme.vars || theme).palette.divider
                }`,
                "& > button": {
                  display: "flex",
                  justifyContent: "center",
                  alignItems: "center",
                  position: "relative",
                  flex: 1,
                  width: 28,
                  "&:hover": {
                    color: theme.palette.text.primary,
                    backgroundColor: (theme.vars || theme).palette.action.hover,
                  },
                  "&.Mui-disabled": {
                    opacity: 0.5,
                    cursor: "not-allowed",
                  },
                  "&:first-child": {
                    borderTopRightRadius: (theme.vars || theme).shape
                      .borderRadius,
                  },
                  "&:last-child": {
                    borderBottomRightRadius: (theme.vars || theme).shape
                      .borderRadius,
                  },
                  "& svg": {
                    fontSize: "1rem",
                    mr: "2px",
                  },
                },
              })}
            >
              <ButtonBase {...getIncrementProps()}>
                <SvgIcon sx={{ bottom: "-4px" }}>
                  <svg
                    xmlns="http://www.w3.org/2000/svg"
                    width="24"
                    height="24"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth="2"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  >
                    <path d="m18 15-6-6-6 6" />
                  </svg>
                </SvgIcon>
              </ButtonBase>
              <Box sx={{ width: "100%", height: "1px", bgcolor: "divider" }} />
              <ButtonBase {...getDecrementProps()}>
                <SvgIcon sx={{ top: "-4px" }}>
                  <svg
                    xmlns="http://www.w3.org/2000/svg"
                    width="24"
                    height="24"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth="2"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  >
                    <path d="m6 9 6 6 6-6" />
                  </svg>
                </SvgIcon>
              </ButtonBase>
            </Box>
          </>
        }
      />
    );
  }
);

export const NumberInputField = React.forwardRef<
  HTMLDivElement,
  NumberInputProps & {
    label?: React.ReactNode;
    helperText?: React.ReactNode;
  }
>(function NumberInputField(
  { id: idOverride, helperText, label, error, fullWidth, ...props },
  ref
) {
  const id = useId(idOverride);
  const helperTextId = helperText && id ? `${id}-helper-text` : undefined;
  const inputLabelId = label && id ? `${id}-label` : undefined;
  return (
    <FormControl error={error} ref={ref} fullWidth={fullWidth}>
      <InputLabel id={inputLabelId} htmlFor={id}>
        {label}
      </InputLabel>
      <NumberInput id={id} {...props} />
      {helperText && (
        <FormHelperText id={helperTextId}>{helperText}</FormHelperText>
      )}
    </FormControl>
  );
});

Installation

npx shadcn@latest add @mui-treasury/number-input

Usage

import { NumberInput } from "@/components/number-input"
<NumberInput />