From 158136965055e7b56bfbd792c3cbb98ee5cdea75 Mon Sep 17 00:00:00 2001 From: Indy Van Canegem Date: Wed, 16 Oct 2024 21:00:24 +0200 Subject: [PATCH] feat(input-component): support icons left/right --- apps/www/__registry__/index.tsx | 22 +++++ apps/www/content/docs/components/input.mdx | 7 ++ apps/www/public/r/styles/default/input.json | 2 +- apps/www/public/r/styles/new-york/input.json | 2 +- .../default/example/input-with-icons.tsx | 18 ++++ .../default/hooks/use-composition.tsx | 29 ++++++ apps/www/registry/default/ui/input.tsx | 96 +++++++++++++++++-- .../new-york/example/input-with-icons.tsx | 18 ++++ .../new-york/hooks/use-composition.tsx | 26 +++++ apps/www/registry/new-york/ui/input.tsx | 96 +++++++++++++++++-- apps/www/registry/registry-examples.ts | 6 ++ 11 files changed, 302 insertions(+), 20 deletions(-) create mode 100644 apps/www/registry/default/example/input-with-icons.tsx create mode 100644 apps/www/registry/default/hooks/use-composition.tsx create mode 100644 apps/www/registry/new-york/example/input-with-icons.tsx create mode 100644 apps/www/registry/new-york/hooks/use-composition.tsx diff --git a/apps/www/__registry__/index.tsx b/apps/www/__registry__/index.tsx index 79e4865a846..80ebe65aa82 100644 --- a/apps/www/__registry__/index.tsx +++ b/apps/www/__registry__/index.tsx @@ -1281,6 +1281,17 @@ export const Index: Record = { 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", @@ -4637,6 +4648,17 @@ export const Index: Record = { 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", diff --git a/apps/www/content/docs/components/input.mdx b/apps/www/content/docs/components/input.mdx index 49d6ddf71ca..bae6c6a49f6 100644 --- a/apps/www/content/docs/components/input.mdx +++ b/apps/www/content/docs/components/input.mdx @@ -97,3 +97,10 @@ import { Input } from "@/components/ui/input" ### Form + +### With Icons + + diff --git a/apps/www/public/r/styles/default/input.json b/apps/www/public/r/styles/default/input.json index 24dcd50e3c1..356a9b77071 100644 --- a/apps/www/public/r/styles/default/input.json +++ b/apps/www/public/r/styles/default/input.json @@ -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 {}\n\nconst Input = React.forwardRef(\n ({ className, type, ...props }, ref) => {\n return (\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,\n VariantProps {}\n\nconst InputIcon = React.forwardRef(\n ({ children, className, size, side }, ref) => {\n return (\n \n {children}\n \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 {}\n\nconst Input = React.forwardRef(\n ({ className, type, ...props }, ref) => {\n return \n }\n)\nInput.displayName = \"Input\"\n\nconst Root = React.forwardRef(\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
\n {IconLeft}\n \n {IconRight}\n
\n )\n }\n return (\n \n )\n }\n) as React.ForwardRefExoticComponent<\n InputProps & React.RefAttributes\n> &\n InputComposition\n\nRoot.displayName = \"Input\"\nRoot.Icon = InputIcon\n\nexport { Root as Input }\n", "type": "registry:ui", "target": "" } diff --git a/apps/www/public/r/styles/new-york/input.json b/apps/www/public/r/styles/new-york/input.json index 20ca4956eb6..7fa97e08f51 100644 --- a/apps/www/public/r/styles/new-york/input.json +++ b/apps/www/public/r/styles/new-york/input.json @@ -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 {}\n\nconst Input = React.forwardRef(\n ({ className, type, ...props }, ref) => {\n return (\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,\n VariantProps {}\n\nconst InputIconLeft = React.forwardRef(\n ({ children, className, size }, ref) => {\n return (\n \n {children}\n \n )\n }\n)\nInputIconLeft.displayName = \"InputIconLeft\"\n\nconst InputIconRight = React.forwardRef(\n ({ children, className, size }, ref) => {\n return (\n \n {children}\n \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 {}\n\nconst Input = React.forwardRef(\n ({ className, type, ...props }, ref) => {\n return \n }\n)\nInput.displayName = \"Input\"\n\nconst Root = React.forwardRef(\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
\n {IconLeft}\n \n {IconRight}\n
\n )\n }\n return (\n \n )\n }\n) as React.ForwardRefExoticComponent<\n InputProps & React.RefAttributes\n> &\n InputComposition\n\nRoot.displayName = \"Input\"\nRoot.IconLeft = InputIconLeft\nRoot.IconRight = InputIconRight\n\nexport { Root as Input }\n", "type": "registry:ui", "target": "" } diff --git a/apps/www/registry/default/example/input-with-icons.tsx b/apps/www/registry/default/example/input-with-icons.tsx new file mode 100644 index 00000000000..8f160e79941 --- /dev/null +++ b/apps/www/registry/default/example/input-with-icons.tsx @@ -0,0 +1,18 @@ +import { MapIcon, MapPin } from "lucide-react" + +import { Input } from "@/registry/default/ui/input" + +export default function InputWithIcons() { + return ( +
+ + + + + + + + +
+ ) +} diff --git a/apps/www/registry/default/hooks/use-composition.tsx b/apps/www/registry/default/hooks/use-composition.tsx new file mode 100644 index 00000000000..e391091443a --- /dev/null +++ b/apps/www/registry/default/hooks/use-composition.tsx @@ -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 +} diff --git a/apps/www/registry/default/ui/input.tsx b/apps/www/registry/default/ui/input.tsx index a921025cebb..d480e3bc879 100644 --- a/apps/www/registry/default/ui/input.tsx +++ b/apps/www/registry/default/ui/input.tsx @@ -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, + VariantProps {} + +const InputIcon = React.forwardRef( + ({ children, className, size, side }, ref) => { + return ( + + {children} + + ) + } +) +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 {} const Input = React.forwardRef( ({ className, type, ...props }, ref) => { + return + } +) +Input.displayName = "Input" + +const Root = React.forwardRef( + ({ children, className, ...props }, ref) => { + const Icons = useComposition(children, InputIcon.displayName) + + if (Icons.length > 0) { + return ( +
+ {Icons} + +
+ ) + } return ( - ) } -) -Input.displayName = "Input" +) as React.ForwardRefExoticComponent< + InputProps & React.RefAttributes +> & + InputComposition + +Root.displayName = "Input" +Root.Icon = InputIcon -export { Input } +export { Root as Input } diff --git a/apps/www/registry/new-york/example/input-with-icons.tsx b/apps/www/registry/new-york/example/input-with-icons.tsx new file mode 100644 index 00000000000..049488760d9 --- /dev/null +++ b/apps/www/registry/new-york/example/input-with-icons.tsx @@ -0,0 +1,18 @@ +import { MapIcon, MapPin } from "lucide-react" + +import { Input } from "@/registry/new-york/ui/input" + +export default function InputWithIcons() { + return ( +
+ + + + + + + + +
+ ) +} diff --git a/apps/www/registry/new-york/hooks/use-composition.tsx b/apps/www/registry/new-york/hooks/use-composition.tsx new file mode 100644 index 00000000000..5e43f537606 --- /dev/null +++ b/apps/www/registry/new-york/hooks/use-composition.tsx @@ -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 +} diff --git a/apps/www/registry/new-york/ui/input.tsx b/apps/www/registry/new-york/ui/input.tsx index 5af26b2c1a9..2a463198595 100644 --- a/apps/www/registry/new-york/ui/input.tsx +++ b/apps/www/registry/new-york/ui/input.tsx @@ -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, + VariantProps {} + +const InputIcon = React.forwardRef( + ({ children, className, size, side }, ref) => { + return ( + + {children} + + ) + } +) +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 {} const Input = React.forwardRef( ({ className, type, ...props }, ref) => { + return + } +) +Input.displayName = "Input" + +const Root = React.forwardRef( + ({ children, className, ...props }, ref) => { + const Icons = useComposition(children, InputIcon.displayName) + + if (Icons.length > 0) { + return ( +
+ {Icons} + +
+ ) + } return ( - ) } -) -Input.displayName = "Input" +) as React.ForwardRefExoticComponent< + InputProps & React.RefAttributes +> & + InputComposition + +Root.displayName = "Input" +Root.Icon = InputIcon -export { Input } +export { Root as Input } diff --git a/apps/www/registry/registry-examples.ts b/apps/www/registry/registry-examples.ts index 4fe1f370f91..1d580361543 100644 --- a/apps/www/registry/registry-examples.ts +++ b/apps/www/registry/registry-examples.ts @@ -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",