Skip to content

Commit

Permalink
feat(input-component): support icons left/right
Browse files Browse the repository at this point in the history
  • Loading branch information
IndyVC committed Oct 16, 2024
1 parent 1297abc commit 1581369
Show file tree
Hide file tree
Showing 11 changed files with 302 additions and 20 deletions.
22 changes: 22 additions & 0 deletions apps/www/__registry__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1281,6 +1281,17 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"input-with-icons": {
name: "input-with-icons",
type: "registry:example",
registryDependencies: ["input"],
files: ["registry/new-york/example/input-with-icons.tsx"],
component: React.lazy(() => import("@/registry/new-york/example/input-with-icons.tsx")),
source: "",
category: "undefined",
subcategory: "undefined",
chunks: []
},
"input-otp-demo": {
name: "input-otp-demo",
type: "registry:example",
Expand Down Expand Up @@ -4637,6 +4648,17 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"input-with-icons": {
name: "input-with-icons",
type: "registry:example",
registryDependencies: ["input"],
files: ["registry/default/example/input-with-icons.tsx"],
component: React.lazy(() => import("@/registry/default/example/input-with-icons.tsx")),
source: "",
category: "undefined",
subcategory: "undefined",
chunks: []
},
"input-otp-demo": {
name: "input-otp-demo",
type: "registry:example",
Expand Down
7 changes: 7 additions & 0 deletions apps/www/content/docs/components/input.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,10 @@ import { Input } from "@/components/ui/input"
### Form

<ComponentPreview name="input-form" />

### With Icons

<ComponentPreview
name="input-with-icons"
description="An input with icons on both sides"
/>
2 changes: 1 addition & 1 deletion apps/www/public/r/styles/default/input.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"files": [
{
"path": "ui/input.tsx",
"content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface InputProps\n extends React.InputHTMLAttributes<HTMLInputElement> {}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n ({ className, type, ...props }, ref) => {\n return (\n <input\n type={type}\n className={cn(\n \"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n ref={ref}\n {...props}\n />\n )\n }\n)\nInput.displayName = \"Input\"\n\nexport { Input }\n",
"content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useComposition } from \"@/registry/default/hooks/use-composition\"\n\ninterface InputComposition {\n Icon: typeof InputIcon\n}\n\nconst iconVariants = cva(\"absolute top-3\", {\n variants: {\n size: {\n default: \"h-4 w-4\",\n },\n side: {\n left: \"left-3\",\n right: \"right-3\",\n },\n },\n defaultVariants: {\n size: \"default\",\n side: \"left\",\n },\n})\n\nexport interface InputIconProps\n extends React.HTMLAttributes<HTMLOrSVGElement>,\n VariantProps<typeof iconVariants> {}\n\nconst InputIcon = React.forwardRef<HTMLSlotElement, InputIconProps>(\n ({ children, className, size, side }, ref) => {\n return (\n <Slot\n data-icon\n ref={ref}\n className={cn(iconVariants({ size, side }), className)}\n >\n {children}\n </Slot>\n )\n }\n)\nInputIcon.displayName = \"InputIcon\"\n\nconst inputVariants = cva(\n \"flex h-10 w-full rounded-md border border-input bg-background py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n {\n variants: {\n composition: {\n true: \"px-10\",\n false: \"px-3\",\n },\n },\n defaultVariants: {\n composition: false,\n },\n }\n)\nexport interface InputProps\n extends React.InputHTMLAttributes<HTMLInputElement> {}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n ({ className, type, ...props }, ref) => {\n return <input type={type} className={className} ref={ref} {...props} />\n }\n)\nInput.displayName = \"Input\"\n\nconst Root = React.forwardRef<HTMLInputElement, InputProps>(\n ({ children, className, ...props }, ref) => {\n const IconLeft = useComposition(children, \"InputIconLeft\")\n const IconRight = useComposition(children, \"InputIconRight\")\n\n if (IconLeft || IconRight) {\n return (\n <div className=\"relative\">\n {IconLeft}\n <Input\n ref={ref}\n className={cn(\n inputVariants({ composition: !!IconLeft || !!IconRight }),\n className\n )}\n {...props}\n />\n {IconRight}\n </div>\n )\n }\n return (\n <Input\n ref={ref}\n className={cn(\n inputVariants({ composition: !!IconLeft || !!IconRight }),\n className\n )}\n {...props}\n />\n )\n }\n) as React.ForwardRefExoticComponent<\n InputProps & React.RefAttributes<HTMLInputElement>\n> &\n InputComposition\n\nRoot.displayName = \"Input\"\nRoot.Icon = InputIcon\n\nexport { Root as Input }\n",
"type": "registry:ui",
"target": ""
}
Expand Down
2 changes: 1 addition & 1 deletion apps/www/public/r/styles/new-york/input.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"files": [
{
"path": "ui/input.tsx",
"content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface InputProps\n extends React.InputHTMLAttributes<HTMLInputElement> {}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n ({ className, type, ...props }, ref) => {\n return (\n <input\n type={type}\n className={cn(\n \"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n ref={ref}\n {...props}\n />\n )\n }\n)\nInput.displayName = \"Input\"\n\nexport { Input }\n",
"content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useComposition } from \"@/registry/default/hooks/use-composition\"\n\ninterface InputComposition {\n IconLeft: typeof InputIconLeft\n IconRight: typeof InputIconRight\n}\n\nconst iconVariants = cva(\"absolute left-3 top-3\", {\n variants: {\n size: {\n default: \"h-3 w-3\",\n },\n },\n defaultVariants: {\n size: \"default\",\n },\n})\n\nexport interface InputIconProps\n extends React.HTMLAttributes<HTMLOrSVGElement>,\n VariantProps<typeof iconVariants> {}\n\nconst InputIconLeft = React.forwardRef<HTMLSlotElement, InputIconProps>(\n ({ children, className, size }, ref) => {\n return (\n <Slot ref={ref} className={cn(iconVariants({ size }), className)}>\n {children}\n </Slot>\n )\n }\n)\nInputIconLeft.displayName = \"InputIconLeft\"\n\nconst InputIconRight = React.forwardRef<HTMLSlotElement, InputIconProps>(\n ({ children, className, size }, ref) => {\n return (\n <Slot ref={ref} className={cn(iconVariants({ size }), className)}>\n {children}\n </Slot>\n )\n }\n)\nInputIconRight.displayName = \"InputIconRight\"\n\nconst inputVariants = cva(\n \"flex h-9 w-full rounded-md border border-input bg-transparent py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50\",\n {\n variants: {\n composition: {\n true: \"px-9\",\n false: \"px-3\",\n },\n },\n defaultVariants: {\n composition: false,\n },\n }\n)\nexport interface InputProps\n extends React.InputHTMLAttributes<HTMLInputElement> {}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n ({ className, type, ...props }, ref) => {\n return <input type={type} className={className} ref={ref} {...props} />\n }\n)\nInput.displayName = \"Input\"\n\nconst Root = React.forwardRef<HTMLInputElement, InputProps>(\n ({ children, className, ...props }, ref) => {\n const IconLeft = useComposition(children, \"InputIconLeft\")\n const IconRight = useComposition(children, \"InputIconRight\")\n\n if (IconLeft || IconRight) {\n return (\n <div className=\"relative\">\n {IconLeft}\n <Input\n ref={ref}\n className={cn(\n inputVariants({ composition: !!IconLeft || !!IconRight }),\n className\n )}\n {...props}\n />\n {IconRight}\n </div>\n )\n }\n return (\n <Input\n ref={ref}\n className={cn(\n inputVariants({ composition: !!IconLeft || !!IconRight }),\n className\n )}\n {...props}\n />\n )\n }\n) as React.ForwardRefExoticComponent<\n InputProps & React.RefAttributes<HTMLInputElement>\n> &\n InputComposition\n\nRoot.displayName = \"Input\"\nRoot.IconLeft = InputIconLeft\nRoot.IconRight = InputIconRight\n\nexport { Root as Input }\n",
"type": "registry:ui",
"target": ""
}
Expand Down
18 changes: 18 additions & 0 deletions apps/www/registry/default/example/input-with-icons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MapIcon, MapPin } from "lucide-react"

import { Input } from "@/registry/default/ui/input"

export default function InputWithIcons() {
return (
<div>
<Input>
<Input.Icon side="left">
<MapIcon />
</Input.Icon>
<Input.Icon side="right">
<MapPin />
</Input.Icon>
</Input>
</div>
)
}
29 changes: 29 additions & 0 deletions apps/www/registry/default/hooks/use-composition.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from "react"

type ReactElementWithDisplayName = React.ReactElement & {
type: { displayName: string }
}

const isElement = (
element: unknown | undefined
): element is ReactElementWithDisplayName => {
return (
(element as ReactElementWithDisplayName)?.type?.displayName !== undefined
)
}

export function useComposition(
children: React.ReactNode,
component: string | undefined
) {
const Children = React.useMemo(
() =>
React.Children.toArray(children).filter((child) => {
if (isElement(child)) return child.type.displayName === component
return false
}),
[children, component]
)

return Children
}
96 changes: 87 additions & 9 deletions apps/www/registry/default/ui/input.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,103 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"
import { useComposition } from "@/registry/default/hooks/use-composition"

interface InputComposition {
Icon: typeof InputIcon
}

const iconVariants = cva("absolute top-3", {
variants: {
size: {
default: "h-4 w-4",
},
side: {
left: "left-3",
right: "right-3",
},
},
defaultVariants: {
size: "default",
side: "left",
},
})

export interface InputIconProps
extends React.HTMLAttributes<HTMLOrSVGElement>,
VariantProps<typeof iconVariants> {}

const InputIcon = React.forwardRef<HTMLSlotElement, InputIconProps>(
({ children, className, size, side }, ref) => {
return (
<Slot
data-icon
ref={ref}
className={cn(iconVariants({ size, side }), className)}
>
{children}
</Slot>
)
}
)
InputIcon.displayName = "InputIcon"

const inputVariants = cva(
"flex h-10 w-full rounded-md border border-input bg-background py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
{
variants: {
composition: {
true: "px-10",
false: "px-3",
},
},
defaultVariants: {
composition: false,
},
}
)
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return <input type={type} className={className} ref={ref} {...props} />
}
)
Input.displayName = "Input"

const Root = React.forwardRef<HTMLInputElement, InputProps>(
({ children, className, ...props }, ref) => {
const Icons = useComposition(children, InputIcon.displayName)

if (Icons.length > 0) {
return (
<div className="relative">
{Icons}
<Input
ref={ref}
className={cn(inputVariants({ composition: true }), className)}
{...props}
/>
</div>
)
}
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
<Input
ref={ref}
className={cn(inputVariants({ composition: false }), className)}
{...props}
/>
)
}
)
Input.displayName = "Input"
) as React.ForwardRefExoticComponent<
InputProps & React.RefAttributes<HTMLInputElement>
> &
InputComposition

Root.displayName = "Input"
Root.Icon = InputIcon

export { Input }
export { Root as Input }
18 changes: 18 additions & 0 deletions apps/www/registry/new-york/example/input-with-icons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MapIcon, MapPin } from "lucide-react"

import { Input } from "@/registry/new-york/ui/input"

export default function InputWithIcons() {
return (
<div>
<Input>
<Input.Icon side="left">
<MapIcon />
</Input.Icon>
<Input.Icon side="right">
<MapPin />
</Input.Icon>
</Input>
</div>
)
}
26 changes: 26 additions & 0 deletions apps/www/registry/new-york/hooks/use-composition.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as React from "react"

type ReactElementWithDisplayName = React.ReactElement & {
type: { displayName: string }
}

const isElement = (
element: unknown | undefined
): element is ReactElementWithDisplayName => {
return (
(element as ReactElementWithDisplayName)?.type?.displayName !== undefined
)
}

export function useComposition(children: React.ReactNode, component: string) {
const Child = React.useMemo(
() =>
React.Children.toArray(children).find((child) => {
if (isElement(child)) return child.type.displayName === component
return false
}),
[children, component]
)

return Child ?? null
}
96 changes: 87 additions & 9 deletions apps/www/registry/new-york/ui/input.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,103 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"
import { useComposition } from "@/registry/default/hooks/use-composition"

interface InputComposition {
Icon: typeof InputIcon
}

const iconVariants = cva("absolute top-2.5", {
variants: {
size: {
default: "h-4 w-4",
},
side: {
left: "left-3",
right: "right-3",
},
},
defaultVariants: {
size: "default",
side: "left",
},
})

export interface InputIconProps
extends React.HTMLAttributes<HTMLOrSVGElement>,
VariantProps<typeof iconVariants> {}

const InputIcon = React.forwardRef<HTMLSlotElement, InputIconProps>(
({ children, className, size, side }, ref) => {
return (
<Slot
data-icon
ref={ref}
className={cn(iconVariants({ size, side }), className)}
>
{children}
</Slot>
)
}
)
InputIcon.displayName = "InputIcon"

const inputVariants = cva(
"flex h-9 w-full rounded-md border border-input bg-transparent py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
{
variants: {
composition: {
true: "px-9",
false: "px-3",
},
},
defaultVariants: {
composition: false,
},
}
)
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return <input type={type} className={className} ref={ref} {...props} />
}
)
Input.displayName = "Input"

const Root = React.forwardRef<HTMLInputElement, InputProps>(
({ children, className, ...props }, ref) => {
const Icons = useComposition(children, InputIcon.displayName)

if (Icons.length > 0) {
return (
<div className="relative">
{Icons}
<Input
ref={ref}
className={cn(inputVariants({ composition: true }), className)}
{...props}
/>
</div>
)
}
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
<Input
ref={ref}
className={cn(inputVariants({ composition: false }), className)}
{...props}
/>
)
}
)
Input.displayName = "Input"
) as React.ForwardRefExoticComponent<
InputProps & React.RefAttributes<HTMLInputElement>
> &
InputComposition

Root.displayName = "Input"
Root.Icon = InputIcon

export { Input }
export { Root as Input }
6 changes: 6 additions & 0 deletions apps/www/registry/registry-examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,12 @@ export const examples: Registry = [
registryDependencies: ["input", "button", "label"],
files: ["example/input-with-text.tsx"],
},
{
name: "input-with-icons",
type: "registry:example",
registryDependencies: ["input"],
files: ["example/input-with-icons.tsx"],
},
{
name: "input-otp-demo",
type: "registry:example",
Expand Down

0 comments on commit 1581369

Please sign in to comment.