Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Create a new component NumberField #4864

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions apps/www/__registry__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,17 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"number-field": {
name: "number-field",
type: "registry:ui",
registryDependencies: ["button","input"],
files: ["registry/new-york/ui/number-field.tsx"],
component: React.lazy(() => import("@/registry/new-york/ui/number-field.tsx")),
source: "",
category: "undefined",
subcategory: "undefined",
chunks: []
},
"pagination": {
name: "pagination",
type: "registry:ui",
Expand Down Expand Up @@ -1369,6 +1380,72 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"number-field-demo": {
name: "number-field-demo",
type: "registry:example",
registryDependencies: ["number-field"],
files: ["registry/new-york/example/number-field-demo.tsx"],
component: React.lazy(() => import("@/registry/new-york/example/number-field-demo.tsx")),
source: "",
category: "undefined",
subcategory: "undefined",
chunks: []
},
"number-field-disabled": {
name: "number-field-disabled",
type: "registry:example",
registryDependencies: ["number-field"],
files: ["registry/new-york/example/number-field-disabled.tsx"],
component: React.lazy(() => import("@/registry/new-york/example/number-field-disabled.tsx")),
source: "",
category: "undefined",
subcategory: "undefined",
chunks: []
},
"number-field-form": {
name: "number-field-form",
type: "registry:example",
registryDependencies: ["number-field"],
files: ["registry/new-york/example/number-field-form.tsx"],
component: React.lazy(() => import("@/registry/new-york/example/number-field-form.tsx")),
source: "",
category: "undefined",
subcategory: "undefined",
chunks: []
},
"number-field-btn-position": {
name: "number-field-btn-position",
type: "registry:example",
registryDependencies: ["number-field"],
files: ["registry/new-york/example/number-field-btn-position.tsx"],
component: React.lazy(() => import("@/registry/new-york/example/number-field-btn-position.tsx")),
source: "",
category: "undefined",
subcategory: "undefined",
chunks: []
},
"number-field-label-position": {
name: "number-field-label-position",
type: "registry:example",
registryDependencies: ["number-field"],
files: ["registry/new-york/example/number-field-label-position.tsx"],
component: React.lazy(() => import("@/registry/new-york/example/number-field-label-position.tsx")),
source: "",
category: "undefined",
subcategory: "undefined",
chunks: []
},
"number-field-number-formatting": {
name: "number-field-number-formatting",
type: "registry:example",
registryDependencies: ["number-field"],
files: ["registry/new-york/example/number-field-number-formatting.tsx"],
component: React.lazy(() => import("@/registry/new-york/example/number-field-number-formatting.tsx")),
source: "",
category: "undefined",
subcategory: "undefined",
chunks: []
},
"pagination-demo": {
name: "pagination-demo",
type: "registry:example",
Expand Down Expand Up @@ -3647,6 +3724,17 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"number-field": {
name: "number-field",
type: "registry:ui",
registryDependencies: ["button","input"],
files: ["registry/default/ui/number-field.tsx"],
component: React.lazy(() => import("@/registry/default/ui/number-field.tsx")),
source: "",
category: "undefined",
subcategory: "undefined",
chunks: []
},
"pagination": {
name: "pagination",
type: "registry:ui",
Expand Down Expand Up @@ -4725,6 +4813,72 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"number-field-demo": {
name: "number-field-demo",
type: "registry:example",
registryDependencies: ["number-field"],
files: ["registry/default/example/number-field-demo.tsx"],
component: React.lazy(() => import("@/registry/default/example/number-field-demo.tsx")),
source: "",
category: "undefined",
subcategory: "undefined",
chunks: []
},
"number-field-disabled": {
name: "number-field-disabled",
type: "registry:example",
registryDependencies: ["number-field"],
files: ["registry/default/example/number-field-disabled.tsx"],
component: React.lazy(() => import("@/registry/default/example/number-field-disabled.tsx")),
source: "",
category: "undefined",
subcategory: "undefined",
chunks: []
},
"number-field-form": {
name: "number-field-form",
type: "registry:example",
registryDependencies: ["number-field"],
files: ["registry/default/example/number-field-form.tsx"],
component: React.lazy(() => import("@/registry/default/example/number-field-form.tsx")),
source: "",
category: "undefined",
subcategory: "undefined",
chunks: []
},
"number-field-btn-position": {
name: "number-field-btn-position",
type: "registry:example",
registryDependencies: ["number-field"],
files: ["registry/default/example/number-field-btn-position.tsx"],
component: React.lazy(() => import("@/registry/default/example/number-field-btn-position.tsx")),
source: "",
category: "undefined",
subcategory: "undefined",
chunks: []
},
"number-field-label-position": {
name: "number-field-label-position",
type: "registry:example",
registryDependencies: ["number-field"],
files: ["registry/default/example/number-field-label-position.tsx"],
component: React.lazy(() => import("@/registry/default/example/number-field-label-position.tsx")),
source: "",
category: "undefined",
subcategory: "undefined",
chunks: []
},
"number-field-number-formatting": {
name: "number-field-number-formatting",
type: "registry:example",
registryDependencies: ["number-field"],
files: ["registry/default/example/number-field-number-formatting.tsx"],
component: React.lazy(() => import("@/registry/default/example/number-field-number-formatting.tsx")),
source: "",
category: "undefined",
subcategory: "undefined",
chunks: []
},
"pagination-demo": {
name: "pagination-demo",
type: "registry:example",
Expand Down
5 changes: 5 additions & 0 deletions apps/www/config/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,11 @@ export const docsConfig: DocsConfig = {
href: "/docs/components/navigation-menu",
items: [],
},
{
title: "Number Field",
href: "/docs/components/number-field",
items: [],
},
{
title: "Pagination",
href: "/docs/components/pagination",
Expand Down
119 changes: 119 additions & 0 deletions apps/www/content/docs/components/number-field.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
---
title: Number Field
description: A component used to create a number input field.
component: true
links:
doc: https://github.com/thought7878/andi-ui
api: https://github.com/thought7878/andi-ui?tab=readme-ov-file#api-reference
---

<ComponentPreview
name="number-field-demo"
description="A number field component."
/>

## Installation

<Tabs defaultValue="cli">

<TabsList>
<TabsTrigger value="cli">CLI</TabsTrigger>
<TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">

```bash
npx shadcn@latest add number-field
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Install the following dependencies:</Step>

```bash
npm install react-stately react-aria @react-types/shared
```

<Step>Copy and paste the following code into your project.</Step>

<ComponentSource name="number-field" />

<Step>Update the import paths to match your project setup.</Step>

</Steps>

</TabsContent>

</Tabs>

## Usage

```tsx
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
NumberFieldLabel,
} from "@/components/ui/number-field"
```

```tsx
<NumberField>
<NumberFieldLabel>Amount</NumberFieldLabel>
<NumberFieldGroup>
<NumberFieldIncrement>{custom icon}</NumberFieldIncrement>
<NumberFieldInput />
<NumberFieldDecrement>{custom icon}</NumberFieldDecrement>
</NumberFieldGroup>
</NumberField>
```

## Examples

### Default

<ComponentPreview
name="number-field-demo"
description="A number field component."
/>

### Form

<ComponentPreview
name="number-field-form"
description="A number field component with form controls."
/>

### Disabled

<ComponentPreview
name="number-field-disabled"
description="A number field component with disabled state."
/>

### Button position

<ComponentPreview
name="number-field-btn-position"
description="A number field component with button position."
/>

### Label position

<ComponentPreview
name="number-field-label-position"
description="A number field component with label position."
/>

### Number formatting

<ComponentPreview
name="number-field-number-formatting"
description="A number field component with number formatting."
/>
3 changes: 3 additions & 0 deletions apps/www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-toggle-group": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.6",
"@react-types/shared": "^3.24.1",
"@tanstack/react-table": "^8.9.1",
"@vercel/analytics": "^1.2.2",
"@vercel/og": "^0.0.21",
Expand All @@ -72,10 +73,12 @@
"next-contentlayer2": "^0.4.6",
"next-themes": "^0.2.1",
"react": "^18.2.0",
"react-aria": "^3.34.3",
"react-day-picker": "^8.7.1",
"react-dom": "^18.2.0",
"react-hook-form": "^7.44.2",
"react-resizable-panels": "^2.0.22",
"react-stately": "^3.32.2",
"react-wrap-balancer": "^0.4.1",
"recharts": "2.12.7",
"sharp": "^0.31.3",
Expand Down
19 changes: 19 additions & 0 deletions apps/www/public/r/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,25 @@
}
]
},
{
"name": "number-field",
"type": "registry:ui",
"dependencies": [
"react-aria",
"react-stately",
"@react-types/shared"
],
"registryDependencies": [
"button",
"input"
],
"files": [
{
"path": "ui/number-field.tsx",
"type": "registry:ui"
}
]
},
{
"name": "pagination",
"type": "registry:ui",
Expand Down
21 changes: 21 additions & 0 deletions apps/www/public/r/styles/default/number-field.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "number-field",
"type": "registry:ui",
"dependencies": [
"react-aria",
"react-stately",
"@react-types/shared"
],
"registryDependencies": [
"button",
"input"
],
"files": [
{
"path": "ui/number-field.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { type ValidationResult } from \"@react-types/shared\"\nimport clsx from \"clsx\"\nimport { ChevronDownIcon, ChevronUpIcon } from \"lucide-react\"\nimport {\n AriaNumberFieldProps,\n useButton,\n useLocale,\n useNumberField,\n type NumberFieldAria,\n} from \"react-aria\"\nimport {\n NumberFieldState,\n NumberFieldStateOptions,\n useNumberFieldState,\n} from \"react-stately\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/registry/default/ui/button\"\nimport { Input } from \"@/registry/default/ui/input\"\n\ninterface NumberFieldContextValue {\n numberFieldProps: NumberFieldAria\n inputRef?: React.RefObject<HTMLInputElement>\n btnPosition?: \"inside\" | \"outside\"\n labelPosition?: \"left\" | \"top\"\n errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)\n}\nconst NumberFieldContext = React.createContext<NumberFieldContextValue>(\n {} as NumberFieldContextValue\n)\n\nconst useNumberFieldContext = () => {\n const numberFieldContext = React.useContext(NumberFieldContext)\n if (!numberFieldContext) {\n throw new Error(\"useNumberFieldContext should be used within <NumberField>\")\n }\n return numberFieldContext\n}\n\ntype NumberFieldRef = Partial<HTMLDivElement> & {\n state: NumberFieldState\n numberFieldProps: NumberFieldAria\n}\ntype NumberFieldProps = React.PropsWithChildren<\n Partial<AriaNumberFieldProps> & {\n name?: string\n className?: string\n btnPosition?: \"inside\" | \"outside\"\n labelPosition?: \"left\" | \"top\"\n } & Partial<Pick<NumberFieldStateOptions, \"locale\">>\n>\nconst NumberField = React.forwardRef<NumberFieldRef, NumberFieldProps>(\n (\n {\n children,\n className,\n btnPosition = \"inside\",\n labelPosition = \"top\",\n locale: customLocale,\n errorMessage,\n validationBehavior = \"native\",\n ...props\n },\n ref\n ) => {\n const hookLocale = useLocale().locale\n const locale = customLocale || hookLocale\n props.label = props.label || props.name || \"label\"\n\n const state = useNumberFieldState({\n ...props,\n locale,\n errorMessage,\n validationBehavior,\n })\n\n const inputRef = React.useRef<HTMLInputElement>(null)\n const numberFieldProps = useNumberField(\n { ...props, errorMessage, validationBehavior },\n state,\n inputRef\n )\n\n numberFieldProps.inputProps.name = props.name\n\n return (\n <NumberFieldContext.Provider\n value={{\n numberFieldProps,\n inputRef,\n btnPosition,\n labelPosition,\n errorMessage,\n }}\n >\n <div\n ref={ref as React.ForwardedRef<HTMLDivElement>}\n {...numberFieldProps.groupProps}\n className={cn(\n \"grid\",\n labelPosition === \"left\"\n ? \"grid-cols-[auto_1fr] grid-rows-[1fr_auto] gap-x-1\"\n : \" grid-cols-1 grid-rows-[auto_1fr_auto]\",\n className\n )}\n >\n {children}\n </div>\n </NumberFieldContext.Provider>\n )\n }\n)\nNumberField.displayName = \"NumberField\"\n\ntype NumberFieldGroupProps = {\n className?: string\n children: React.ReactNode\n}\nconst NumberFieldGroup = React.forwardRef<\n HTMLDivElement,\n NumberFieldGroupProps\n>(({ className, children }, ref) => {\n const {\n numberFieldProps: { groupProps },\n } = useNumberFieldContext()\n return (\n <div\n ref={ref}\n className={cn(\"relative flex gap-1\", className)}\n {...groupProps}\n >\n {children}\n </div>\n )\n})\nNumberFieldGroup.displayName = \"NumberFieldGroup\"\n\ntype NumberFieldIncrementProps = {\n className?: string\n children?: React.ReactNode\n}\nconst NumberFieldIncrement = React.forwardRef<\n HTMLButtonElement,\n NumberFieldIncrementProps\n>(({ className, children }, ref) => {\n const {\n numberFieldProps: { incrementButtonProps },\n btnPosition,\n } = useNumberFieldContext()\n\n const { buttonProps } = useButton(\n incrementButtonProps,\n ref as React.RefObject<HTMLButtonElement | null>\n )\n\n return (\n <Button\n className={clsx(\n \"focus-visible:ring-0 focus-visible:ring-offset-0\",\n btnPosition === \"outside\"\n ? \"px-3 py-2\"\n : \"absolute right-0 top-0 z-10 flex h-1/2 w-6 items-center justify-center rounded-b-none p-0 focus-visible:outline-none\",\n className\n )}\n variant=\"outline\"\n {...buttonProps}\n ref={ref}\n >\n {children || <ChevronUpIcon className=\"h-4 w-4\" />}\n </Button>\n )\n})\nNumberFieldIncrement.displayName = \"NumberFieldIncrement\"\n\ntype NumberFieldDecrementProps = {\n children?: React.ReactNode\n className?: string\n}\nconst NumberFieldDecrement = React.forwardRef<\n HTMLButtonElement,\n NumberFieldDecrementProps\n>(({ className, children }, ref) => {\n const {\n numberFieldProps: { decrementButtonProps },\n btnPosition,\n } = useNumberFieldContext()\n\n const { buttonProps } = useButton(\n decrementButtonProps,\n ref as React.RefObject<HTMLButtonElement | null>\n )\n\n return (\n <Button\n className={clsx(\n \"focus-visible:ring-0 focus-visible:ring-offset-0\",\n btnPosition === \"outside\"\n ? \"px-3 py-2\"\n : \"absolute bottom-0 right-0 z-10 flex h-1/2 w-6 items-center justify-center rounded-t-none p-0 focus-visible:outline-none\",\n className\n )}\n variant=\"outline\"\n {...buttonProps}\n ref={ref}\n >\n {children || <ChevronDownIcon className=\"h-4 w-4\" />}\n </Button>\n )\n})\nNumberFieldDecrement.displayName = \"NumberFieldDecrement\"\n\ntype NumberFieldInputProps = { className?: string }\nconst NumberFieldInput = React.forwardRef<\n HTMLInputElement,\n NumberFieldInputProps\n>(({ className }, ref) => {\n const {\n numberFieldProps: { inputProps, isInvalid },\n inputRef,\n } = useNumberFieldContext()\n\n React.useEffect(() => {\n if (ref && \"current\" in ref && inputRef?.current) {\n ref.current = inputRef?.current\n }\n }, [inputRef, ref])\n\n return (\n <Input\n ref={inputRef}\n type=\"number\"\n className={clsx(\n { \"focus-visible:ring-destructive\": isInvalid },\n className\n )}\n {...inputProps}\n />\n )\n})\nNumberFieldInput.displayName = \"NumberFieldInput\"\n\ntype NumberFieldLabelProps = {\n className?: string\n children: React.ReactNode\n}\nconst NumberFieldLabel = React.forwardRef<\n HTMLLabelElement,\n NumberFieldLabelProps\n>(({ className, children }, ref) => {\n const {\n numberFieldProps: { labelProps },\n labelPosition,\n } = useNumberFieldContext()\n\n return (\n <label\n ref={ref}\n {...labelProps}\n className={cn(\n labelPosition === \"left\" && \"flex items-center justify-center\",\n className\n )}\n >\n {children}\n </label>\n )\n})\nNumberFieldLabel.displayName = \"NumberFieldLabel\"\n\ntype NumberFieldErrorProps = {\n className?: string\n // children?: React.ReactNode;\n}\nconst NumberFieldError = React.forwardRef<\n HTMLDivElement,\n NumberFieldErrorProps\n>(({ className }, ref) => {\n const {\n numberFieldProps: {\n errorMessageProps,\n isInvalid,\n validationErrors,\n validationDetails,\n },\n errorMessage,\n labelPosition,\n } = useNumberFieldContext()\n\n let errorMessageString: React.ReactNode = null\n if (typeof errorMessage === \"function\") {\n errorMessageString =\n isInvalid && validationErrors != null && validationDetails != null\n ? errorMessage({\n isInvalid,\n validationErrors,\n validationDetails,\n })\n : null\n } else if (errorMessage) {\n errorMessageString = errorMessage\n } else {\n errorMessageString = validationErrors\n }\n\n return (\n <div\n ref={ref}\n {...errorMessageProps}\n className={cn(\n \"text-destructive\",\n labelPosition === \"left\" && \"col-start-2\",\n className\n )}\n >\n {isInvalid && errorMessageString}\n </div>\n )\n})\nNumberFieldError.displayName = \"NumberFieldError\"\n\nexport {\n NumberField,\n NumberFieldDecrement,\n NumberFieldGroup,\n NumberFieldIncrement,\n NumberFieldInput,\n NumberFieldLabel,\n NumberFieldError,\n}\n\nexport type {\n NumberFieldRef,\n NumberFieldProps,\n NumberFieldDecrementProps,\n NumberFieldGroupProps,\n NumberFieldIncrementProps,\n NumberFieldInputProps,\n NumberFieldLabelProps,\n NumberFieldErrorProps,\n}\n",
"type": "registry:ui",
"target": ""
}
]
}
Loading