Skip to content

Commit e7b13c2

Browse files
committed
Support ref prop on interactive components
1 parent be42245 commit e7b13c2

12 files changed

Lines changed: 483 additions & 368 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
- [2025-12-13] [Support ref prop on interactive components](https://github.com/RubricLab/ui/commit/12d61d1abe2a73b45aa1d9bb04e2a43f1bd186b6)
12
- [2025-12-13] [Add missing package repo field](https://github.com/RubricLab/ui/commit/1455da66d5986829514e65ee905bff45d50ee1c4)
23
- [2025-12-13] [Standardize publish file name](https://github.com/RubricLab/ui/commit/1b256d2160f52441a43fe45f9280d20dec684a5d)
34
- [2025-12-13] [Bump package version](https://github.com/RubricLab/ui/commit/aa3cf682f1fbf580cc41eea60cee41f33ed94a0c)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,5 @@
5757
"post-commit": "bun x @rubriclab/package post-commit"
5858
},
5959
"types": "./src/index.ts",
60-
"version": "5.1.52"
60+
"version": "5.1.53"
6161
}

src/components/alert-dialog.tsx

Lines changed: 59 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,36 @@
11
'use client'
22

33
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
4-
import type * as React from 'react'
4+
import * as React from 'react'
55
import { cn } from '../utils'
66
import { buttonVariants } from './button'
77

88
const AlertDialog = ({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) => {
99
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
1010
}
1111

12-
const AlertDialogTrigger = ({
13-
...props
14-
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) => {
15-
return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
16-
}
12+
const AlertDialogTrigger = React.forwardRef<
13+
React.ElementRef<typeof AlertDialogPrimitive.Trigger>,
14+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Trigger>
15+
>(({ ...props }, ref) => {
16+
return <AlertDialogPrimitive.Trigger ref={ref} data-slot="alert-dialog-trigger" {...props} />
17+
})
18+
19+
AlertDialogTrigger.displayName = AlertDialogPrimitive.Trigger.displayName
1720

1821
const AlertDialogPortal = ({
1922
...props
2023
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) => {
2124
return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
2225
}
2326

24-
const AlertDialogOverlay = ({
25-
className,
26-
...props
27-
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) => {
27+
const AlertDialogOverlay = React.forwardRef<
28+
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
29+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
30+
>(({ className, ...props }, ref) => {
2831
return (
2932
<AlertDialogPrimitive.Overlay
33+
ref={ref}
3034
data-slot="alert-dialog-overlay"
3135
className={cn(
3236
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=open]:animate-in',
@@ -35,16 +39,19 @@ const AlertDialogOverlay = ({
3539
{...props}
3640
/>
3741
)
38-
}
42+
})
3943

40-
const AlertDialogContent = ({
41-
className,
42-
...props
43-
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) => {
44+
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
45+
46+
const AlertDialogContent = React.forwardRef<
47+
React.ElementRef<typeof AlertDialogPrimitive.Content>,
48+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
49+
>(({ className, ...props }, ref) => {
4450
return (
4551
<AlertDialogPortal>
4652
<AlertDialogOverlay />
4753
<AlertDialogPrimitive.Content
54+
ref={ref}
4855
data-slot="alert-dialog-content"
4956
className={cn(
5057
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-default border bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in sm:max-w-lg',
@@ -54,7 +61,9 @@ const AlertDialogContent = ({
5461
/>
5562
</AlertDialogPortal>
5663
)
57-
}
64+
})
65+
66+
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
5867

5968
const AlertDialogHeader = ({ className, ...props }: React.ComponentProps<'div'>) => {
6069
return (
@@ -76,50 +85,63 @@ const AlertDialogFooter = ({ className, ...props }: React.ComponentProps<'div'>)
7685
)
7786
}
7887

79-
const AlertDialogTitle = ({
80-
className,
81-
...props
82-
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) => {
88+
const AlertDialogTitle = React.forwardRef<
89+
React.ElementRef<typeof AlertDialogPrimitive.Title>,
90+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
91+
>(({ className, ...props }, ref) => {
8392
return (
8493
<AlertDialogPrimitive.Title
94+
ref={ref}
8595
data-slot="alert-dialog-title"
8696
className={cn('font-semibold text-lg', className)}
8797
{...props}
8898
/>
8999
)
90-
}
100+
})
91101

92-
const AlertDialogDescription = ({
93-
className,
94-
...props
95-
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) => {
102+
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
103+
104+
const AlertDialogDescription = React.forwardRef<
105+
React.ElementRef<typeof AlertDialogPrimitive.Description>,
106+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
107+
>(({ className, ...props }, ref) => {
96108
return (
97109
<AlertDialogPrimitive.Description
110+
ref={ref}
98111
data-slot="alert-dialog-description"
99112
className={cn('text-muted-foreground text-sm', className)}
100113
{...props}
101114
/>
102115
)
103-
}
116+
})
104117

105-
const AlertDialogAction = ({
106-
className,
107-
...props
108-
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) => {
109-
return <AlertDialogPrimitive.Action className={cn(buttonVariants(), className)} {...props} />
110-
}
118+
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
111119

112-
const AlertDialogCancel = ({
113-
className,
114-
...props
115-
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) => {
120+
const AlertDialogAction = React.forwardRef<
121+
React.ElementRef<typeof AlertDialogPrimitive.Action>,
122+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
123+
>(({ className, ...props }, ref) => {
124+
return (
125+
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
126+
)
127+
})
128+
129+
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
130+
131+
const AlertDialogCancel = React.forwardRef<
132+
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
133+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
134+
>(({ className, ...props }, ref) => {
116135
return (
117136
<AlertDialogPrimitive.Cancel
137+
ref={ref}
118138
className={cn('focus:outline-none', buttonVariants({ variant: 'ghost' }), className)}
119139
{...props}
120140
/>
121141
)
122-
}
142+
})
143+
144+
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
123145

124146
export {
125147
AlertDialog,

src/components/button.tsx

Lines changed: 68 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { cva } from 'class-variance-authority'
2-
import type * as React from 'react'
2+
import * as React from 'react'
33
import { widthClasses } from '../styles'
44
import type { ButtonProps, IconNames } from '../types'
55
import { camelToPascal, cn } from '../utils'
@@ -89,69 +89,78 @@ const ButtonContent = ({ arrangement, iconName, children }: ButtonContentProps)
8989
</>
9090
)
9191

92-
const Button: React.FC<ButtonProps> = ({
93-
type = 'button',
94-
onClick,
95-
disabled = false,
96-
label,
97-
href,
98-
variant = 'secondary',
99-
size = 'md',
100-
width = 'fit',
101-
arrangement = 'leadingLabel',
102-
icon,
103-
onMouseEnter,
104-
onMouseLeave,
105-
className
106-
}) => {
107-
const iconName = icon ? (camelToPascal(icon) as IconNames) : null
92+
type ButtonRef = HTMLButtonElement | HTMLAnchorElement
10893

109-
const buttonContent = (
110-
<ButtonContent arrangement={arrangement} iconName={iconName}>
111-
{label}
112-
</ButtonContent>
113-
)
94+
const Button = React.forwardRef<ButtonRef, ButtonProps>(
95+
(
96+
{
97+
type = 'button',
98+
onClick,
99+
disabled = false,
100+
label,
101+
href,
102+
variant = 'secondary',
103+
size = 'md',
104+
width = 'fit',
105+
arrangement = 'leadingLabel',
106+
icon,
107+
onMouseEnter,
108+
onMouseLeave,
109+
className
110+
},
111+
ref
112+
) => {
113+
const iconName = icon ? (camelToPascal(icon) as IconNames) : null
114114

115-
const buttonElement = href ? (
116-
<a
117-
href={href}
118-
className={cn(
119-
'no-underline',
120-
buttonVariants({ arrangement, size, variant }),
121-
arrangement !== 'hiddenLabel' && widthClasses[width], // TODO: clean up
122-
className
123-
)}
124-
>
125-
{buttonContent}
126-
</a>
127-
) : (
128-
<button
129-
disabled={disabled}
130-
onClick={onClick}
131-
type={type}
132-
className={cn(
133-
buttonVariants({ arrangement, size, variant }),
134-
arrangement !== 'hiddenLabel' && widthClasses[width], // TODO: clean up
135-
className
136-
)}
137-
onMouseEnter={onMouseEnter}
138-
onMouseLeave={onMouseLeave}
139-
>
140-
{buttonContent}
141-
</button>
142-
)
115+
const buttonContent = (
116+
<ButtonContent arrangement={arrangement} iconName={iconName}>
117+
{label}
118+
</ButtonContent>
119+
)
143120

144-
if (arrangement === 'hiddenLabel') {
145-
return (
146-
<Tooltip>
147-
<TooltipTrigger asChild>{buttonElement}</TooltipTrigger>
148-
<TooltipContent>{label}</TooltipContent>
149-
</Tooltip>
121+
const buttonElement = href ? (
122+
<a
123+
ref={ref as React.Ref<HTMLAnchorElement>}
124+
href={href}
125+
className={cn(
126+
'no-underline',
127+
buttonVariants({ arrangement, size, variant }),
128+
arrangement !== 'hiddenLabel' && widthClasses[width], // TODO: clean up
129+
className
130+
)}
131+
>
132+
{buttonContent}
133+
</a>
134+
) : (
135+
<button
136+
ref={ref as React.Ref<HTMLButtonElement>}
137+
disabled={disabled}
138+
onClick={onClick}
139+
type={type}
140+
className={cn(
141+
buttonVariants({ arrangement, size, variant }),
142+
arrangement !== 'hiddenLabel' && widthClasses[width], // TODO: clean up
143+
className
144+
)}
145+
onMouseEnter={onMouseEnter}
146+
onMouseLeave={onMouseLeave}
147+
>
148+
{buttonContent}
149+
</button>
150150
)
151-
}
152151

153-
return buttonElement
154-
}
152+
if (arrangement === 'hiddenLabel') {
153+
return (
154+
<Tooltip>
155+
<TooltipTrigger asChild>{buttonElement}</TooltipTrigger>
156+
<TooltipContent>{label}</TooltipContent>
157+
</Tooltip>
158+
)
159+
}
160+
161+
return buttonElement
162+
}
163+
)
155164

156165
Button.displayName = 'Button'
157166

0 commit comments

Comments
 (0)