Skip to content

Commit a549d6a

Browse files
committed
Input: add support for start and end decorations
1 parent ed573df commit a549d6a

2 files changed

Lines changed: 93 additions & 4 deletions

File tree

src/components/ui/input.tsx

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,61 @@
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+
debug?: boolean
20+
}
21+
22+
function Input({
23+
className,
24+
type,
25+
beforeStartDecoration,
26+
startDecoration,
27+
endDecoration,
28+
afterEndDecoration,
29+
debug,
30+
...props
31+
}: React.ComponentProps<'input'> & OwnProps) {
32+
const startDecorationRef = useRef<HTMLSpanElement>(null)
33+
const [startDecorationWidth, setStartDecorationWidth] = useState<number>(0)
34+
const endDecorationRef = useRef<HTMLSpanElement>(null)
35+
const [endDecorationWidth, setEndDecorationWidth] = useState<number>(0)
36+
37+
useEffect(() => {
38+
if (!!startDecoration && !!startDecorationRef.current) {
39+
const rect = startDecorationRef.current.getBoundingClientRect()
40+
setStartDecorationWidth(rect.width)
41+
} else {
42+
setStartDecorationWidth(0)
43+
}
44+
}, [startDecoration, startDecorationRef.current])
45+
46+
useEffect(() => {
47+
if (!!endDecoration && !!endDecorationRef.current) {
48+
const rect = endDecorationRef.current.getBoundingClientRect()
49+
setEndDecorationWidth(rect.width)
50+
} else {
51+
setEndDecorationWidth(0)
52+
}
53+
}, [endDecoration, endDecorationRef.current]) // Re-run if content changes
54+
55+
const startStyle = !!startDecorationWidth ? { paddingLeft: `${16 + startDecorationWidth}px` } : {}
56+
const endStyle = !!endDecorationWidth ? { paddingRight: `${16 + endDecorationWidth}px` } : {}
57+
58+
const input = (
759
<input
860
type={type}
961
data-slot="input"
@@ -13,9 +65,32 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
1365
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
1466
className
1567
)}
68+
style={{ ...startStyle, ...endStyle }}
1669
{...props}
1770
/>
1871
)
72+
73+
// const hasInsideDecorations = !!startDecoration || !!endDecoration
74+
// const hasOutsideDecorations = !!beforeStartDecoration || !!afterEndDecoration
75+
return (
76+
<>
77+
{beforeStartDecoration}
78+
<div className={'relative'}>
79+
{!!startDecoration && (
80+
<span className={'absolute left-2.5 top-2.5'} ref={startDecorationRef}>
81+
{startDecoration}
82+
</span>
83+
)}
84+
{input}
85+
{!!endDecoration && (
86+
<span className={'absolute right-2.5 top-2.5'} ref={endDecorationRef}>
87+
{endDecoration}
88+
</span>
89+
)}
90+
</div>
91+
{afterEndDecoration}
92+
</>
93+
)
1994
}
2095

2196
export { Input }

src/stories/Input/Input.stories.tsx

Lines changed: 16 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,20 @@ 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+
debug={true}
67+
/>
68+
</div>
69+
),
70+
}
71+
5872
export const Invalid: Story = {
5973
render: () => (
6074
<div className="grid w-full max-w-sm items-center gap-1.5">

0 commit comments

Comments
 (0)