Skip to content

Commit 7c8856a

Browse files
Migrate UI to React 19 ref-as-prop (remove forwardRef) (#308)
* ref(ui): migrate components to React 19 ref-as-prop (remove forwardRef, displayName); update all elements * fix(ui): replace deprecated React.ElementRef with ComponentRef and run prettier
1 parent 13e5e9e commit 7c8856a

29 files changed

+745
-859
lines changed

packages/ui/src/elements/accordion.tsx

Lines changed: 35 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,56 +7,53 @@ import { cn } from '@o2s/ui/lib/utils';
77

88
const Accordion = AccordionPrimitive.Root;
99

10-
const AccordionItem = React.forwardRef<
11-
React.ElementRef<typeof AccordionPrimitive.Item>,
12-
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
13-
>(({ className, ...props }, ref) => (
10+
type AccordionItemProps = React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item> & {
11+
ref?: React.Ref<React.ComponentRef<typeof AccordionPrimitive.Item>>;
12+
};
13+
const AccordionItem = ({ className, ref, ...props }: AccordionItemProps) => (
1414
<AccordionPrimitive.Item ref={ref} className={cn('border-b', className)} {...props} />
15-
));
16-
AccordionItem.displayName = 'AccordionItem';
15+
);
1716

1817
export interface AccordionTriggerProps extends React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> {
1918
tag?: keyof JSX.IntrinsicElements;
2019
}
2120

22-
const AccordionTrigger = React.forwardRef<React.ElementRef<typeof AccordionPrimitive.Trigger>, AccordionTriggerProps>(
23-
({ className, tag = 'h3', children, ...props }, ref) => {
24-
const Comp = tag;
25-
26-
return (
27-
<AccordionPrimitive.Header className="flex" asChild>
28-
<Comp>
29-
<AccordionPrimitive.Trigger
30-
ref={ref}
31-
className={cn(
32-
'flex flex-1 gap-2 items-center justify-between py-4 font-medium text-left transition-all underline-offset-4 hover:underline [&[data-state=open]>svg]:rotate-180',
33-
className,
34-
)}
35-
{...props}
36-
>
37-
{children}
38-
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
39-
</AccordionPrimitive.Trigger>
40-
</Comp>
41-
</AccordionPrimitive.Header>
42-
);
43-
},
44-
);
45-
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
46-
47-
const AccordionContent = React.forwardRef<
48-
React.ElementRef<typeof AccordionPrimitive.Content>,
49-
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
50-
>(({ className, children, ...props }, ref) => (
21+
type AccordionTriggerOwnProps = AccordionTriggerProps & {
22+
ref?: React.Ref<React.ComponentRef<typeof AccordionPrimitive.Trigger>>;
23+
};
24+
const AccordionTrigger = ({ className, tag = 'h3', children, ref, ...props }: AccordionTriggerOwnProps) => {
25+
const Comp = tag;
26+
27+
return (
28+
<AccordionPrimitive.Header className="flex" asChild>
29+
<Comp>
30+
<AccordionPrimitive.Trigger
31+
ref={ref}
32+
className={cn(
33+
'flex flex-1 gap-2 items-center justify-between py-4 font-medium text-left transition-all underline-offset-4 hover:underline [&[data-state=open]>svg]:rotate-180',
34+
className,
35+
)}
36+
{...props}
37+
>
38+
{children}
39+
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
40+
</AccordionPrimitive.Trigger>
41+
</Comp>
42+
</AccordionPrimitive.Header>
43+
);
44+
};
45+
46+
type AccordionContentProps = React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content> & {
47+
ref?: React.Ref<React.ComponentRef<typeof AccordionPrimitive.Content>>;
48+
};
49+
const AccordionContent = ({ className, children, ref, ...props }: AccordionContentProps) => (
5150
<AccordionPrimitive.Content
5251
ref={ref}
5352
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
5453
{...props}
5554
>
5655
<div className={cn('pb-4 pt-0', className)}>{children}</div>
5756
</AccordionPrimitive.Content>
58-
));
59-
60-
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
57+
);
6158

6259
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

packages/ui/src/elements/alert.tsx

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,22 @@ const alertVariants = cva(
1818
},
1919
);
2020

21-
const Alert = React.forwardRef<
22-
HTMLDivElement,
23-
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
24-
>(({ className, variant, ...props }, ref) => (
21+
type AlertProps = React.HTMLAttributes<HTMLDivElement> &
22+
VariantProps<typeof alertVariants> & {
23+
ref?: React.Ref<HTMLDivElement>;
24+
};
25+
const Alert = ({ className, variant, ref, ...props }: AlertProps) => (
2526
<div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
26-
));
27-
Alert.displayName = 'Alert';
27+
);
2828

29-
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
30-
({ className, ...props }, ref) => (
31-
<h5 ref={ref} className={cn('mb-1 font-medium leading-none tracking-tight', className)} {...props} />
32-
),
29+
type AlertTitleProps = React.HTMLAttributes<HTMLHeadingElement> & { ref?: React.Ref<HTMLHeadingElement> };
30+
const AlertTitle = ({ className, ref, ...props }: AlertTitleProps) => (
31+
<h5 ref={ref} className={cn('mb-1 font-medium leading-none tracking-tight', className)} {...props} />
3332
);
34-
AlertTitle.displayName = 'AlertTitle';
3533

36-
const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
37-
({ className, ...props }, ref) => (
38-
<div ref={ref} className={cn('text-sm [&_p]:leading-relaxed', className)} {...props} />
39-
),
34+
type AlertDescriptionProps = React.HTMLAttributes<HTMLParagraphElement> & { ref?: React.Ref<HTMLDivElement> };
35+
const AlertDescription = ({ className, ref, ...props }: AlertDescriptionProps) => (
36+
<div ref={ref} className={cn('text-sm [&_p]:leading-relaxed', className)} {...props} />
4037
);
41-
AlertDescription.displayName = 'AlertDescription';
4238

4339
export { Alert, AlertTitle, AlertDescription };

packages/ui/src/elements/avatar.tsx

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -23,50 +23,47 @@ type AvatarProps = {
2323
email?: string;
2424
} & React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>;
2525

26-
const Avatar = React.forwardRef<React.ElementRef<typeof AvatarPrimitive.Root>, AvatarProps>(
27-
({ name, email, className, ...props }, ref) => (
28-
<div className="flex items-center gap-2">
29-
<AvatarPrimitive.Root
30-
ref={ref}
31-
className={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className)}
32-
{...props}
33-
/>
34-
{name && <AvatarUser name={name} email={email} />}
35-
</div>
36-
),
26+
type AvatarOwnProps = AvatarProps & { ref?: React.Ref<React.ComponentRef<typeof AvatarPrimitive.Root>> };
27+
const Avatar = ({ name, email, className, ref, ...props }: AvatarOwnProps) => (
28+
<div className="flex items-center gap-2">
29+
<AvatarPrimitive.Root
30+
ref={ref}
31+
className={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className)}
32+
{...props}
33+
/>
34+
{name && <AvatarUser name={name} email={email} />}
35+
</div>
3736
);
38-
Avatar.displayName = AvatarPrimitive.Root.displayName;
3937

40-
const AvatarImage = React.forwardRef<
41-
React.ElementRef<typeof AvatarPrimitive.Image>,
42-
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
43-
>(({ className, alt = '', ...props }, ref) => (
38+
type AvatarImageProps = React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> & {
39+
ref?: React.Ref<React.ComponentRef<typeof AvatarPrimitive.Image>>;
40+
};
41+
const AvatarImage = ({ className, alt = '', ref, ...props }: AvatarImageProps) => (
4442
<AvatarPrimitive.Image ref={ref} className={cn('aspect-square h-full w-full', className)} alt={alt} {...props} />
45-
));
46-
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
43+
);
4744

4845
export interface AvatarFallbackProps
4946
extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>,
5047
VariantProps<typeof avatarVariants> {
5148
name: string;
5249
}
5350

54-
const AvatarFallback = React.forwardRef<React.ElementRef<typeof AvatarPrimitive.Fallback>, AvatarFallbackProps>(
55-
({ variant, className, name, ...props }, ref) => {
56-
const initials = name
57-
.split(' ')
58-
.map((name) => name[0])
59-
.join('')
60-
.toUpperCase();
51+
type AvatarFallbackOwnProps = AvatarFallbackProps & {
52+
ref?: React.Ref<React.ComponentRef<typeof AvatarPrimitive.Fallback>>;
53+
};
54+
const AvatarFallback = ({ variant, className, name, ref, ...props }: AvatarFallbackOwnProps) => {
55+
const initials = name
56+
.split(' ')
57+
.map((name) => name[0])
58+
.join('')
59+
.toUpperCase();
6160

62-
return (
63-
<AvatarPrimitive.Fallback ref={ref} className={cn(avatarVariants({ variant, className }))} {...props}>
64-
{initials}
65-
</AvatarPrimitive.Fallback>
66-
);
67-
},
68-
);
69-
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
61+
return (
62+
<AvatarPrimitive.Fallback ref={ref} className={cn(avatarVariants({ variant, className }))} {...props}>
63+
{initials}
64+
</AvatarPrimitive.Fallback>
65+
);
66+
};
7067

7168
type AvatarUserProps = {
7269
name: string;

packages/ui/src/elements/breadcrumb.tsx

Lines changed: 34 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,60 +4,49 @@ import * as React from 'react';
44

55
import { cn } from '@o2s/ui/lib/utils';
66

7-
const Breadcrumb = React.forwardRef<
8-
HTMLElement,
9-
React.ComponentPropsWithoutRef<'nav'> & {
10-
separator?: React.ReactNode;
11-
}
12-
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
13-
Breadcrumb.displayName = 'Breadcrumb';
7+
type BreadcrumbProps = React.ComponentPropsWithoutRef<'nav'> & {
8+
separator?: React.ReactNode;
9+
ref?: React.Ref<HTMLElement>;
10+
};
11+
const Breadcrumb = ({ ref, ...props }: BreadcrumbProps) => <nav ref={ref} aria-label="breadcrumb" {...props} />;
1412

15-
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<'ol'>>(
16-
({ className, ...props }, ref) => (
17-
<ol
18-
ref={ref}
19-
className={cn(
20-
'flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5',
21-
className,
22-
)}
23-
{...props}
24-
/>
25-
),
13+
type BreadcrumbListProps = React.ComponentPropsWithoutRef<'ol'> & { ref?: React.Ref<HTMLOListElement> };
14+
const BreadcrumbList = ({ className, ref, ...props }: BreadcrumbListProps) => (
15+
<ol
16+
ref={ref}
17+
className={cn(
18+
'flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5',
19+
className,
20+
)}
21+
{...props}
22+
/>
2623
);
27-
BreadcrumbList.displayName = 'BreadcrumbList';
2824

29-
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<'li'>>(
30-
({ className, ...props }, ref) => (
31-
<li ref={ref} className={cn('inline-flex items-center gap-1.5', className)} {...props} />
32-
),
25+
type BreadcrumbItemProps = React.ComponentPropsWithoutRef<'li'> & { ref?: React.Ref<HTMLLIElement> };
26+
const BreadcrumbItem = ({ className, ref, ...props }: BreadcrumbItemProps) => (
27+
<li ref={ref} className={cn('inline-flex items-center gap-1.5', className)} {...props} />
3328
);
34-
BreadcrumbItem.displayName = 'BreadcrumbItem';
3529

36-
const BreadcrumbLink = React.forwardRef<
37-
HTMLAnchorElement,
38-
React.ComponentPropsWithoutRef<'a'> & {
39-
asChild?: boolean;
40-
}
41-
>(({ asChild, className, ...props }, ref) => {
30+
type BreadcrumbLinkProps = React.ComponentPropsWithoutRef<'a'> & {
31+
asChild?: boolean;
32+
ref?: React.Ref<HTMLAnchorElement>;
33+
};
34+
const BreadcrumbLink = ({ asChild, className, ref, ...props }: BreadcrumbLinkProps) => {
4235
const Comp = asChild ? Slot : 'a';
43-
4436
return <Comp ref={ref} className={cn('transition-colors hover:text-foreground', className)} {...props} />;
45-
});
46-
BreadcrumbLink.displayName = 'BreadcrumbLink';
37+
};
4738

48-
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<'span'>>(
49-
({ className, ...props }, ref) => (
50-
<span
51-
ref={ref}
52-
role="link"
53-
aria-disabled="true"
54-
aria-current="page"
55-
className={cn('font-normal text-foreground', className)}
56-
{...props}
57-
/>
58-
),
39+
type BreadcrumbPageProps = React.ComponentPropsWithoutRef<'span'> & { ref?: React.Ref<HTMLSpanElement> };
40+
const BreadcrumbPage = ({ className, ref, ...props }: BreadcrumbPageProps) => (
41+
<span
42+
ref={ref}
43+
role="link"
44+
aria-disabled="true"
45+
aria-current="page"
46+
className={cn('font-normal text-foreground', className)}
47+
{...props}
48+
/>
5949
);
60-
BreadcrumbPage.displayName = 'BreadcrumbPage';
6150

6251
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<'li'>) => (
6352
<li role="presentation" aria-hidden="true" className={cn('[&>svg]:w-3.5 [&>svg]:h-3.5', className)} {...props}>

packages/ui/src/elements/button.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,12 @@ import { baseVariant } from '@o2s/ui/lib/utils';
88

99
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof baseVariant> {
1010
asChild?: boolean;
11+
ref?: React.Ref<HTMLButtonElement>;
1112
}
1213

13-
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
14-
({ className, variant = 'primary', size = 'default', asChild = false, ...props }, ref) => {
15-
const Comp = asChild ? Slot : 'button';
16-
return <Comp className={cn(baseVariant({ variant, size }), buttonVariants, className)} ref={ref} {...props} />;
17-
},
18-
);
19-
Button.displayName = 'Button';
14+
const Button = ({ className, variant = 'primary', size = 'default', asChild = false, ref, ...props }: ButtonProps) => {
15+
const Comp = asChild ? Slot : 'button';
16+
return <Comp className={cn(baseVariant({ variant, size }), buttonVariants, className)} ref={ref} {...props} />;
17+
};
2018

2119
export { Button, buttonVariants };

packages/ui/src/elements/card.tsx

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,30 @@ import * as React from 'react';
22

33
import { cn } from '@o2s/ui/lib/utils';
44

5-
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
5+
type CardProps = React.HTMLAttributes<HTMLDivElement> & { ref?: React.Ref<HTMLDivElement> };
6+
const Card = ({ className, ref, ...props }: CardProps) => (
67
<div ref={ref} className={cn('rounded-lg border bg-card text-card-foreground shadow-xs', className)} {...props} />
7-
));
8-
Card.displayName = 'Card';
8+
);
99

10-
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
11-
({ className, ...props }, ref) => (
12-
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6 pb-0', className)} {...props} />
13-
),
10+
type CardSectionProps = React.HTMLAttributes<HTMLDivElement> & { ref?: React.Ref<HTMLDivElement> };
11+
const CardHeader = ({ className, ref, ...props }: CardSectionProps) => (
12+
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6 pb-0', className)} {...props} />
1413
);
15-
CardHeader.displayName = 'CardHeader';
1614

17-
const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
18-
({ className, ...props }, ref) => (
19-
<div ref={ref} className={cn('text-2xl font-semibold leading-none tracking-tight', className)} {...props} />
20-
),
15+
const CardTitle = ({ className, ref, ...props }: CardSectionProps) => (
16+
<div ref={ref} className={cn('text-2xl font-semibold leading-none tracking-tight', className)} {...props} />
2117
);
22-
CardTitle.displayName = 'CardTitle';
2318

24-
const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
25-
({ className, ...props }, ref) => (
26-
<div ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
27-
),
19+
const CardDescription = ({ className, ref, ...props }: CardSectionProps) => (
20+
<div ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
2821
);
29-
CardDescription.displayName = 'CardDescription';
3022

31-
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
32-
({ className, ...props }, ref) => <div ref={ref} className={cn('p-6', className)} {...props} />,
23+
const CardContent = ({ className, ref, ...props }: CardSectionProps) => (
24+
<div ref={ref} className={cn('p-6', className)} {...props} />
3325
);
34-
CardContent.displayName = 'CardContent';
3526

36-
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
37-
({ className, ...props }, ref) => (
38-
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
39-
),
27+
const CardFooter = ({ className, ref, ...props }: CardSectionProps) => (
28+
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
4029
);
41-
CardFooter.displayName = 'CardFooter';
4230

4331
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };

0 commit comments

Comments
 (0)