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 Nov 8, 2024
1 parent 4a0d4cf commit ed32659
Show file tree
Hide file tree
Showing 15 changed files with 360 additions and 26 deletions.
32 changes: 32 additions & 0 deletions apps/www/__registry__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1885,6 +1885,22 @@ export const Index: Record<string, any> = {
subcategory: "",
chunks: []
},
"input-with-icons": {
name: "input-with-icons",
description: "",
type: "registry:example",
registryDependencies: ["input"],
files: [{
path: "registry/new-york/example/input-with-icons.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(() => import("@/registry/new-york/example/input-with-icons.tsx")),
source: "",
category: "",
subcategory: "",
chunks: []
},
"input-otp-demo": {
name: "input-otp-demo",
description: "",
Expand Down Expand Up @@ -7378,6 +7394,22 @@ export const Index: Record<string, any> = {
subcategory: "",
chunks: []
},
"input-with-icons": {
name: "input-with-icons",
description: "",
type: "registry:example",
registryDependencies: ["input"],
files: [{
path: "registry/default/example/input-with-icons.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(() => import("@/registry/default/example/input-with-icons.tsx")),
source: "",
category: "",
subcategory: "",
chunks: []
},
"input-otp-demo": {
name: "input-otp-demo",
description: "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,16 @@ export default function Component() {

const filteredData = chartData.filter((item) => {
const date = new Date(item.date)
const now = new Date()
const referenceDate = new Date("2024-06-30")
let daysToSubtract = 90
if (timeRange === "30d") {
daysToSubtract = 30
} else if (timeRange === "7d") {
daysToSubtract = 7
}
now.setDate(now.getDate() - daysToSubtract)
return date >= now
const startDate = new Date(referenceDate)
startDate.setDate(startDate.getDate() - daysToSubtract)
return date >= startDate
})

return (
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"
/>
15 changes: 15 additions & 0 deletions apps/www/public/r/styles/default/input-with-icons.json
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": ""
}
]
}
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\nconst Input = React.forwardRef<\n HTMLInputElement,\n React.ComponentProps<\"input\">\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-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\",\n className\n )}\n ref={ref}\n {...props}\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

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions apps/www/public/r/styles/new-york/input-with-icons.json
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": ""
}
]
}
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\nconst Input = React.forwardRef<\n HTMLInputElement,\n React.ComponentProps<\"input\">\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-base 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 md:text-sm\",\n className\n )}\n ref={ref}\n {...props}\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
}
101 changes: 91 additions & 10 deletions apps/www/registry/default/ui/input.tsx
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 }
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
}
Loading

0 comments on commit ed32659

Please sign in to comment.