-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(input-component): support icons left/right
- Loading branch information
Showing
15 changed files
with
360 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"name": "input-with-icons", | ||
"type": "registry:example", | ||
"registryDependencies": [ | ||
"input" | ||
], | ||
"files": [ | ||
{ | ||
"path": "example/input-with-icons.tsx", | ||
"content": "import { MapIcon, MapPin } from \"lucide-react\"\n\nimport { Input } from \"@/registry/default/ui/input\"\n\nexport default function InputWithIcons() {\n return (\n <div>\n <Input>\n <Input.Icon side=\"left\">\n <MapIcon />\n </Input.Icon>\n <Input.Icon side=\"right\">\n <MapPin />\n </Input.Icon>\n </Input>\n </div>\n )\n}\n", | ||
"type": "registry:example", | ||
"target": "" | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"name": "input-with-icons", | ||
"type": "registry:example", | ||
"registryDependencies": [ | ||
"input" | ||
], | ||
"files": [ | ||
{ | ||
"path": "example/input-with-icons.tsx", | ||
"content": "import { MapIcon, MapPin } from \"lucide-react\"\n\nimport { Input } from \"@/registry/new-york/ui/input\"\n\nexport default function InputWithIcons() {\n return (\n <div>\n <Input>\n <Input.Icon side=\"left\">\n <MapIcon />\n </Input.Icon>\n <Input.Icon side=\"right\">\n <MapPin />\n </Input.Icon>\n </Input>\n </div>\n )\n}\n", | ||
"type": "registry:example", | ||
"target": "" | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +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" | ||
|
||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>( | ||
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 px-3 py-2 text-base 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 md:text-sm", | ||
{ | ||
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-base 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 md:text-sm", | ||
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 } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.