Skip to content

Commit 1b0bd77

Browse files
committed
Input: add support for start and end decorations
1 parent 7f49814 commit 1b0bd77

2 files changed

Lines changed: 89 additions & 4 deletions

File tree

src/components/ui/input.tsx

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,58 @@
11
import * as React from 'react'
22

33
import { cn } from '../../lib/utils'
4+
import { useEffect, useRef, useState } from 'react'
45

5-
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
6-
return (
6+
interface OwnProps {
7+
// Something that needs to appear before the start, outside the field
8+
beforeStartDecoration?: React.ReactNode
9+
10+
// Something that needs to appear at the start, inside the field
11+
startDecoration?: React.ReactNode
12+
13+
// Something that needs to appear at the end, inside the field
14+
endDecoration?: React.ReactNode
15+
16+
// Something that needs to appear after the end, outside the field
17+
afterEndDecoration?: React.ReactNode
18+
}
19+
20+
function Input({
21+
className,
22+
type,
23+
beforeStartDecoration,
24+
startDecoration,
25+
endDecoration,
26+
afterEndDecoration,
27+
...props
28+
}: React.ComponentProps<'input'> & OwnProps) {
29+
const startDecorationRef = useRef<HTMLSpanElement>(null)
30+
const [startDecorationWidth, setStartDecorationWidth] = useState<number>(0)
31+
const endDecorationRef = useRef<HTMLSpanElement>(null)
32+
const [endDecorationWidth, setEndDecorationWidth] = useState<number>(0)
33+
34+
useEffect(() => {
35+
if (!!startDecoration && !!startDecorationRef.current) {
36+
const rect = startDecorationRef.current.getBoundingClientRect()
37+
setStartDecorationWidth(rect.width)
38+
} else {
39+
setStartDecorationWidth(0)
40+
}
41+
}, [startDecoration, startDecorationRef.current])
42+
43+
useEffect(() => {
44+
if (!!endDecoration && !!endDecorationRef.current) {
45+
const rect = endDecorationRef.current.getBoundingClientRect()
46+
setEndDecorationWidth(rect.width)
47+
} else {
48+
setEndDecorationWidth(0)
49+
}
50+
}, [endDecoration, endDecorationRef.current]) // Re-run if content changes
51+
52+
const startStyle = !!startDecorationWidth ? { paddingLeft: `${16 + startDecorationWidth}px` } : {}
53+
const endStyle = !!endDecorationWidth ? { paddingRight: `${16 + endDecorationWidth}px` } : {}
54+
55+
const input = (
756
<input
857
type={type}
958
data-slot="input"
@@ -13,9 +62,32 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
1362
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
1463
className
1564
)}
65+
style={{ ...startStyle, ...endStyle }}
1666
{...props}
1767
/>
1868
)
69+
70+
// const hasInsideDecorations = !!startDecoration || !!endDecoration
71+
// const hasOutsideDecorations = !!beforeStartDecoration || !!afterEndDecoration
72+
return (
73+
<>
74+
{beforeStartDecoration}
75+
<div className={'relative'}>
76+
{!!startDecoration && (
77+
<span className={'absolute left-2.5 top-2.5'} ref={startDecorationRef}>
78+
{startDecoration}
79+
</span>
80+
)}
81+
{input}
82+
{!!endDecoration && (
83+
<span className={'absolute right-2.5 top-2.5'} ref={endDecorationRef}>
84+
{endDecoration}
85+
</span>
86+
)}
87+
</div>
88+
{afterEndDecoration}
89+
</>
90+
)
1991
}
2092

2193
export { Input }

src/stories/Input/Input.stories.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
22
import { Input } from '../../components/ui/input.tsx'
33
import { Label } from '../../components/ui/label.tsx'
44
import { expect, within, userEvent } from 'storybook/test'
5-
import { SearchIcon } from 'lucide-react'
5+
import { SearchIcon, X as CloseIcon } from 'lucide-react'
66

77
const meta: Meta<typeof Input> = {
88
title: 'Components/Input',
@@ -46,7 +46,7 @@ export const Default: Story = {
4646
},
4747
}
4848

49-
export const WithIcon: Story = {
49+
export const WithManualIcon: Story = {
5050
render: () => (
5151
<div className="relative w-[300px]">
5252
<SearchIcon className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
@@ -55,6 +55,19 @@ export const WithIcon: Story = {
5555
),
5656
}
5757

58+
export const WithStartAndEndDecoration: Story = {
59+
render: () => (
60+
<div className="w-[300px]">
61+
<Input
62+
type="search"
63+
placeholder="Search..."
64+
startDecoration={<SearchIcon className="h-4 w-4 text-muted-foreground" />}
65+
endDecoration={<CloseIcon className="h-4 w-4 text-muted-foreground" />}
66+
/>
67+
</div>
68+
),
69+
}
70+
5871
export const Invalid: Story = {
5972
render: () => (
6073
<div className="grid w-full max-w-sm items-center gap-1.5">

0 commit comments

Comments
 (0)