ui icon indicating copy to clipboard operation
ui copied to clipboard

feat(input-number): update docs

Open dinogit opened this issue 1 year ago • 6 comments

Input Number component wit min, max and steps

dinogit avatar Oct 23 '23 22:10 dinogit

@dinogit is attempting to deploy a commit to the shadcn-pro Team on Vercel.

A member of the Team first needs to authorize it.

vercel[bot] avatar Oct 23 '23 22:10 vercel[bot]

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
ui ✅ Ready (Inspect) Visit Preview 💬 Add feedback Oct 24, 2023 11:24am

vercel[bot] avatar Oct 24 '23 11:10 vercel[bot]

This looks great. Thank you.

Some quick notes: I wonder if we can make the component composable. Something like:

<NumberField>
  <NumberFieldInput />
  <NumberFieldIncrement />
  <NumberFieldDecrement />
</NumberField>

shadcn avatar Oct 24 '23 11:10 shadcn

sure, will give it a try

dinogit avatar Oct 24 '23 12:10 dinogit

Hey guys! I've recently been working on a <NumberInput /> component based on react-aria and following the “shadcn/ui” philosophy. react-aria provides incredible hooks for these kinds of components.

https://github.com/shadcn-ui/ui/assets/69400730/8022052c-ec21-497d-b3ef-8d0d4f2de9a2

I’m sharing the full source code here in case it could be helpful in any way.

Usage:

<NumberInput
  label="% Example"
  defaultValue={0.1}
  minValue={0}
  formatOptions={{
    style: "percent",
    notation: "compact",
  }}
/>

Source:

"use client";
import * as React from "react";
import { useNumberFieldState } from "react-stately";
import {
  type AriaNumberFieldProps,
  useLocale,
  useNumberField,
  useButton,
  type AriaButtonOptions,
} from "react-aria";
import { ChevronUp, ChevronDown } from "lucide-react";

import { Input } from "./input";
import { Label } from "./label";
import { cn } from "./utils/cn";

export const NumberInput = ({
  className,
  ...props
}: {
  className?: string;
} & AriaNumberFieldProps) => {
  const { locale } = useLocale();
  const state = useNumberFieldState({ ...props, locale });
  const inputRef = React.useRef(null);
  const {
    labelProps,
    groupProps,
    inputProps,
    incrementButtonProps,
    decrementButtonProps,
  } = useNumberField(props, state, inputRef);

  return (
    <div className={className}>
      <Label {...labelProps}>{props.label}</Label>
      <div className="grid h-10 grid-cols-[auto_2.3rem]" {...groupProps}>
        <Input
          {...inputProps}
          className={cn(
            inputProps.className,
            "row-span-2 h-full rounded-r-none border-r-0",
          )}
          ref={inputRef}
        />
        <AriaButton
          className="rounded-tr-md border px-2 hover:bg-border"
          {...incrementButtonProps}
        >
          <ChevronUp className="mx-auto" size="1em" />
        </AriaButton>
        <AriaButton
          className="rounded-br-md border-x border-b px-2 hover:bg-border"
          {...decrementButtonProps}
        >
          <ChevronDown className="mx-auto" size="1em" />
        </AriaButton>
      </div>
    </div>
  );
};

const AriaButton = ({
  className,
  children,
  ...props
}: {
  className?: string;
  children: React.ReactNode;
} & AriaButtonOptions<"button">) => {
  const ref = React.useRef(null);
  const { buttonProps } = useButton(props, ref);
  return (
    <button
      {...buttonProps}
      className={cn(buttonProps.className, className)}
      ref={ref}
    >
      {children}
    </button>
  );
};

simonecervini avatar Nov 14 '23 13:11 simonecervini

Hey everyone! I've been working on a solution for a personal project and thought I'd share.

Source:

'use client'

import * as React from 'react'
import { useLocale } from 'react-aria'
import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'
import { type NumberFieldState, useNumberFieldState, NumberFieldStateOptions } from 'react-stately'
import { Input, InputProps } from '@/src/components/ui/input'
import { Button, ButtonProps } from '@/src/components/ui/button'
import { cn } from '@/src/lib/utils'
import { ControllerRenderProps } from 'react-hook-form'

type NumberFieldContextValue = NumberFieldState

const NumberFieldContext = React.createContext<NumberFieldContextValue>(
  {} as NumberFieldContextValue
)

const useNumberField = () => {
  const numberFieldContext = React.useContext(NumberFieldContext)

  if (!numberFieldContext) {
    throw new Error('useNumberField should be used within <NumberField>')
  }

  return numberFieldContext
}

type NumberFieldProps = Partial<NumberFieldStateOptions> & ControllerRenderProps
const NumberField = React.forwardRef<HTMLDivElement, React.PropsWithChildren<NumberFieldProps>>(
  ({ children, ...props }, ref) => {
    const { locale } = useLocale()
    const state = useNumberFieldState({ ...props, locale })

    return (
      <NumberFieldContext.Provider value={state}>
        <div ref={ref} className={cn('flex rounded-md gap-1')}>
          {children}
        </div>
      </NumberFieldContext.Provider>
    )
  }
)
NumberField.displayName = 'NumberField'

const NumberFieldIncrement = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, ...props }, ref) => {
    const state = useNumberField()

    return (
      <Button
        variant={'outline'}
        size={'icon'}
        type="button"
        className={cn('aspect-square', className)}
        onClick={state.increment}
        ref={ref}
        {...props}
      >
        <ChevronUpIcon />
      </Button>
    )
  }
)
NumberFieldIncrement.displayName = 'NumberFieldIncrement'

const NumberFieldDecrement = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, ...props }, ref) => {
    const state = useNumberField()

    return (
      <Button
        variant={'outline'}
        size={'icon'}
        type="button"
        className={cn('aspect-square', className)}
        onClick={state.decrement}
        ref={ref}
        {...props}
      >
        <ChevronDownIcon />
      </Button>
    )
  }
)
NumberFieldDecrement.displayName = 'NumberFieldDecrement'

const NumberFieldInput = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, ...props }, ref) => {
    const state = useNumberField()

    return (
      <Input
        ref={ref}
        type="number"
        className={cn(
          '[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none',
          className
        )}
        value={state.inputValue}
        onChange={(e) => state.setInputValue(e.target.value)}
        {...props}
      />
    )
  }
)
NumberFieldInput.displayName = 'NumberFieldInput'

export { NumberField, NumberFieldInput, NumberFieldDecrement, NumberFieldIncrement }

Usage:

// ...
<FormField
  control={form.control}
  name="exampleNumberField"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Example number Field</FormLabel>
      <FormControl>
        <NumberField {...field}>
          <NumberFieldInput />
          <NumberFieldDecrement />
          <NumberFieldIncrement />
        </NumberField>
      </FormControl>
      <FormMessage />
    </FormItem>
  )}
/>
// ...

Example:

https://github.com/shadcn-ui/ui/assets/149010961/cbd9f7b1-4d4c-4315-bd58-646810165fb4

landry-fairwinds avatar Jan 17 '24 01:01 landry-fairwinds