Skip to content

Commit cc82607

Browse files
refactor(ui): update components for React 19 compatibility
- Refactor Label, Input, Textarea, Select, DropdownMenu components to accept `ref` as a standard prop, eliminating the need for forwardRef. - Enhance documentation with examples for each component, showcasing the new ref usage. - Update component display names for consistency and clarity.
1 parent 53bc91b commit cc82607

File tree

5 files changed

+392
-269
lines changed

5 files changed

+392
-269
lines changed

components/ui/dropdown-menu.tsx

Lines changed: 178 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
/**
2-
* Dropdown Menu Component
2+
* Dropdown Menu Component (React 19)
33
*
44
* A dropdown menu component built on Radix UI DropdownMenu primitive.
55
* - Accessible menus with keyboard navigation and focus management
66
* - Supports menu items, checkboxes, radio groups, separators, and submenus
7+
* - All components accept `ref` as a standard prop (no forwardRef needed in React 19+)
78
*/
89

910
'use client'
@@ -26,154 +27,203 @@ const DropdownMenuSub = DropdownMenuPrimitive.Sub
2627

2728
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
2829

29-
const DropdownMenuSubTrigger = React.forwardRef<
30-
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
31-
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
32-
inset?: boolean
33-
}
34-
>(({ className, inset, children, ...props }, ref) => (
35-
<DropdownMenuPrimitive.SubTrigger
36-
ref={ref}
37-
className={cn(
38-
'flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
39-
inset && 'pl-8',
40-
className,
41-
)}
42-
{...props}
43-
>
44-
{children}
45-
<ChevronRight className="ml-auto" />
46-
</DropdownMenuPrimitive.SubTrigger>
47-
))
30+
type DropdownMenuSubTriggerProps = React.ComponentPropsWithoutRef<
31+
typeof DropdownMenuPrimitive.SubTrigger
32+
> & {
33+
inset?: boolean
34+
ref?: React.Ref<React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>>
35+
}
36+
37+
const DropdownMenuSubTrigger = React.memo(
38+
({
39+
className,
40+
inset,
41+
children,
42+
ref,
43+
...props
44+
}: DropdownMenuSubTriggerProps) => (
45+
<DropdownMenuPrimitive.SubTrigger
46+
ref={ref}
47+
className={cn(
48+
'flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
49+
inset && 'pl-8',
50+
className,
51+
)}
52+
{...props}
53+
>
54+
{children}
55+
<ChevronRight className="ml-auto" />
56+
</DropdownMenuPrimitive.SubTrigger>
57+
),
58+
)
4859
DropdownMenuSubTrigger.displayName =
4960
DropdownMenuPrimitive.SubTrigger.displayName
5061

51-
const DropdownMenuSubContent = React.forwardRef<
52-
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
53-
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
54-
>(({ className, ...props }, ref) => (
55-
<DropdownMenuPrimitive.SubContent
56-
ref={ref}
57-
className={cn(
58-
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]',
59-
className,
60-
)}
61-
{...props}
62-
/>
63-
))
64-
DropdownMenuSubContent.displayName =
65-
DropdownMenuPrimitive.SubContent.displayName
62+
type DropdownMenuSubContentProps = React.ComponentPropsWithoutRef<
63+
typeof DropdownMenuPrimitive.SubContent
64+
> & {
65+
ref?: React.Ref<React.ElementRef<typeof DropdownMenuPrimitive.SubContent>>
66+
}
6667

67-
const DropdownMenuContent = React.forwardRef<
68-
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
69-
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
70-
>(({ className, sideOffset = 4, ...props }, ref) => (
71-
<DropdownMenuPrimitive.Portal>
72-
<DropdownMenuPrimitive.Content
68+
const DropdownMenuSubContent = React.memo(
69+
({ className, ref, ...props }: DropdownMenuSubContentProps) => (
70+
<DropdownMenuPrimitive.SubContent
7371
ref={ref}
74-
sideOffset={sideOffset}
7572
className={cn(
76-
'z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]',
73+
'z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]',
7774
className,
7875
)}
7976
{...props}
8077
/>
81-
</DropdownMenuPrimitive.Portal>
82-
))
78+
),
79+
)
80+
DropdownMenuSubContent.displayName =
81+
DropdownMenuPrimitive.SubContent.displayName
82+
83+
type DropdownMenuContentProps = React.ComponentPropsWithoutRef<
84+
typeof DropdownMenuPrimitive.Content
85+
> & {
86+
ref?: React.Ref<React.ElementRef<typeof DropdownMenuPrimitive.Content>>
87+
}
88+
89+
const DropdownMenuContent = React.memo(
90+
({ className, sideOffset = 4, ref, ...props }: DropdownMenuContentProps) => (
91+
<DropdownMenuPrimitive.Portal>
92+
<DropdownMenuPrimitive.Content
93+
ref={ref}
94+
sideOffset={sideOffset}
95+
className={cn(
96+
'z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-32 overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]',
97+
className,
98+
)}
99+
{...props}
100+
/>
101+
</DropdownMenuPrimitive.Portal>
102+
),
103+
)
83104
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
84105

85-
const DropdownMenuItem = React.forwardRef<
86-
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
87-
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
88-
inset?: boolean
89-
}
90-
>(({ className, inset, ...props }, ref) => (
91-
<DropdownMenuPrimitive.Item
92-
ref={ref}
93-
className={cn(
94-
'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
95-
inset && 'pl-8',
96-
className,
97-
)}
98-
{...props}
99-
/>
100-
))
106+
type DropdownMenuItemProps = React.ComponentPropsWithoutRef<
107+
typeof DropdownMenuPrimitive.Item
108+
> & {
109+
inset?: boolean
110+
ref?: React.Ref<React.ElementRef<typeof DropdownMenuPrimitive.Item>>
111+
}
112+
113+
const DropdownMenuItem = React.memo(
114+
({ className, inset, ref, ...props }: DropdownMenuItemProps) => (
115+
<DropdownMenuPrimitive.Item
116+
ref={ref}
117+
className={cn(
118+
'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
119+
inset && 'pl-8',
120+
className,
121+
)}
122+
{...props}
123+
/>
124+
),
125+
)
101126
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
102127

103-
const DropdownMenuCheckboxItem = React.forwardRef<
104-
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
105-
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
106-
>(({ className, children, checked, ...props }, ref) => (
107-
<DropdownMenuPrimitive.CheckboxItem
108-
ref={ref}
109-
className={cn(
110-
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
111-
className,
112-
)}
113-
checked={checked}
114-
{...props}
115-
>
116-
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
117-
<DropdownMenuPrimitive.ItemIndicator>
118-
<Check className="h-4 w-4" />
119-
</DropdownMenuPrimitive.ItemIndicator>
120-
</span>
121-
{children}
122-
</DropdownMenuPrimitive.CheckboxItem>
123-
))
128+
type DropdownMenuCheckboxItemProps = React.ComponentPropsWithoutRef<
129+
typeof DropdownMenuPrimitive.CheckboxItem
130+
> & {
131+
ref?: React.Ref<React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>>
132+
}
133+
134+
const DropdownMenuCheckboxItem = React.memo(
135+
({
136+
className,
137+
children,
138+
checked,
139+
ref,
140+
...props
141+
}: DropdownMenuCheckboxItemProps) => (
142+
<DropdownMenuPrimitive.CheckboxItem
143+
ref={ref}
144+
className={cn(
145+
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
146+
className,
147+
)}
148+
checked={checked}
149+
{...props}
150+
>
151+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
152+
<DropdownMenuPrimitive.ItemIndicator>
153+
<Check className="h-4 w-4" />
154+
</DropdownMenuPrimitive.ItemIndicator>
155+
</span>
156+
{children}
157+
</DropdownMenuPrimitive.CheckboxItem>
158+
),
159+
)
124160
DropdownMenuCheckboxItem.displayName =
125161
DropdownMenuPrimitive.CheckboxItem.displayName
126162

127-
const DropdownMenuRadioItem = React.forwardRef<
128-
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
129-
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
130-
>(({ className, children, ...props }, ref) => (
131-
<DropdownMenuPrimitive.RadioItem
132-
ref={ref}
133-
className={cn(
134-
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
135-
className,
136-
)}
137-
{...props}
138-
>
139-
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
140-
<DropdownMenuPrimitive.ItemIndicator>
141-
<Circle className="h-2 w-2 fill-current" />
142-
</DropdownMenuPrimitive.ItemIndicator>
143-
</span>
144-
{children}
145-
</DropdownMenuPrimitive.RadioItem>
146-
))
163+
type DropdownMenuRadioItemProps = React.ComponentPropsWithoutRef<
164+
typeof DropdownMenuPrimitive.RadioItem
165+
> & {
166+
ref?: React.Ref<React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>>
167+
}
168+
169+
const DropdownMenuRadioItem = React.memo(
170+
({ className, children, ref, ...props }: DropdownMenuRadioItemProps) => (
171+
<DropdownMenuPrimitive.RadioItem
172+
ref={ref}
173+
className={cn(
174+
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
175+
className,
176+
)}
177+
{...props}
178+
>
179+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
180+
<DropdownMenuPrimitive.ItemIndicator>
181+
<Circle className="h-2 w-2 fill-current" />
182+
</DropdownMenuPrimitive.ItemIndicator>
183+
</span>
184+
{children}
185+
</DropdownMenuPrimitive.RadioItem>
186+
),
187+
)
147188
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
148189

149-
const DropdownMenuLabel = React.forwardRef<
150-
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
151-
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
152-
inset?: boolean
153-
}
154-
>(({ className, inset, ...props }, ref) => (
155-
<DropdownMenuPrimitive.Label
156-
ref={ref}
157-
className={cn(
158-
'px-2 py-1.5 text-sm font-semibold',
159-
inset && 'pl-8',
160-
className,
161-
)}
162-
{...props}
163-
/>
164-
))
190+
type DropdownMenuLabelProps = React.ComponentPropsWithoutRef<
191+
typeof DropdownMenuPrimitive.Label
192+
> & {
193+
inset?: boolean
194+
ref?: React.Ref<React.ElementRef<typeof DropdownMenuPrimitive.Label>>
195+
}
196+
197+
const DropdownMenuLabel = React.memo(
198+
({ className, inset, ref, ...props }: DropdownMenuLabelProps) => (
199+
<DropdownMenuPrimitive.Label
200+
ref={ref}
201+
className={cn(
202+
'px-2 py-1.5 text-sm font-semibold',
203+
inset && 'pl-8',
204+
className,
205+
)}
206+
{...props}
207+
/>
208+
),
209+
)
165210
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
166211

167-
const DropdownMenuSeparator = React.forwardRef<
168-
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
169-
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
170-
>(({ className, ...props }, ref) => (
171-
<DropdownMenuPrimitive.Separator
172-
ref={ref}
173-
className={cn('-mx-1 my-1 h-px bg-muted', className)}
174-
{...props}
175-
/>
176-
))
212+
type DropdownMenuSeparatorProps = React.ComponentPropsWithoutRef<
213+
typeof DropdownMenuPrimitive.Separator
214+
> & {
215+
ref?: React.Ref<React.ElementRef<typeof DropdownMenuPrimitive.Separator>>
216+
}
217+
218+
const DropdownMenuSeparator = React.memo(
219+
({ className, ref, ...props }: DropdownMenuSeparatorProps) => (
220+
<DropdownMenuPrimitive.Separator
221+
ref={ref}
222+
className={cn('-mx-1 my-1 h-px bg-muted', className)}
223+
{...props}
224+
/>
225+
),
226+
)
177227
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
178228

179229
const DropdownMenuShortcut = React.memo(function DropdownMenuShortcut({

components/ui/input.tsx

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,40 @@
11
/**
2-
* Input Component
2+
* Input Component (React 19)
33
*
44
* A styled input component for text input fields.
55
* - Supports all standard HTML input attributes and types
66
* - Proper focus states, disabled states, and placeholder styling
7+
* - Accepts `ref` as a standard prop (no forwardRef needed in React 19+)
78
*/
89

910
import * as React from 'react'
1011

1112
import { cn } from '@/lib/utils'
1213

13-
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
14-
({ className, type, ...props }, ref) => {
15-
return (
16-
<input
17-
type={type}
18-
className={cn(
19-
'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',
20-
className,
21-
)}
22-
ref={ref}
23-
{...props}
24-
/>
25-
)
26-
},
27-
)
14+
type InputProps = React.ComponentProps<'input'> & {
15+
ref?: React.Ref<HTMLInputElement>
16+
}
17+
18+
/**
19+
* Input element with consistent styling
20+
*
21+
* @example
22+
* const ref = useRef<HTMLInputElement>(null)
23+
* <Input ref={ref} type="email" placeholder="Enter email" />
24+
*/
25+
const Input = React.memo(({ className, type, ref, ...props }: InputProps) => {
26+
return (
27+
<input
28+
type={type}
29+
className={cn(
30+
'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',
31+
className,
32+
)}
33+
ref={ref}
34+
{...props}
35+
/>
36+
)
37+
})
2838
Input.displayName = 'Input'
2939

3040
export { Input }

0 commit comments

Comments
 (0)