Skip to content

Commit 615af0e

Browse files
committed
feat(Slot): Add custom Slot component
1 parent ba03c1f commit 615af0e

9 files changed

Lines changed: 102 additions & 12 deletions

File tree

bun.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
"@radix-ui/react-dialog": "1.1.15",
2727
"@radix-ui/react-popover": "1.1.15",
2828
"@radix-ui/react-select": "2.2.6",
29-
"@radix-ui/react-slot": "1.2.4",
3029
"@radix-ui/react-switch": "1.2.6",
3130
"@radix-ui/react-tooltip": "1.2.8",
3231
"@types/react": "19.2.8",

src/components/ui/auto-complete/auto-complete.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@ import {
88
useState,
99
} from "react"
1010

11-
import { Slot } from "@radix-ui/react-slot"
12-
1311
import { Button } from "components/ui/button"
1412
import { Portal } from "components/utility/portal"
13+
import { Slot } from "components/utility/slot"
1514
import { useDropdownNavigation } from "hooks/use-dropdown-navigation"
1615
import { useFocus } from "hooks/use-focus"
1716
import { cn } from "utils/cn"

src/components/ui/button/button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import {
55
PropsWithChildren,
66
} from "react"
77

8-
import { Slot } from "@radix-ui/react-slot"
98
import { cva, type VariantProps } from "class-variance-authority"
109
import { Link } from "wouter"
1110

11+
import { Slot } from "components/utility/slot"
1212
import {
1313
AsChildProp,
1414
ClassNameProp,

src/components/ui/input-label.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { PropsWithChildren, useId, ReactNode, isValidElement } from "react"
22

3-
import { Slot } from "@radix-ui/react-slot"
4-
3+
import { Slot } from "components/utility/slot"
54
import { ClassNameProp } from "types/base-props"
65
import { cn } from "utils/cn"
76
import { vstack } from "utils/styles"

src/components/utility/auto-animate-height.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { PropsWithChildren, useEffect, useRef, useState } from "react"
22

3-
import { Slot } from "@radix-ui/react-slot"
43
import { motion } from "motion/react"
54

5+
import { Slot } from "components/utility/slot"
66
import { ClassNameProp } from "types/base-props"
77
import { cn } from "utils/cn"
88

src/components/utility/scroll-area.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { PropsWithChildren } from "react"
22

3-
import { Slot } from "@radix-ui/react-slot"
4-
3+
import { Slot } from "components/utility/slot"
54
import {
65
AsChildProp,
76
ClassNameProp,

src/components/utility/slot.tsx

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import {
2+
type CSSProperties,
3+
type HTMLAttributes,
4+
type PropsWithChildren,
5+
type Ref,
6+
type RefCallback,
7+
cloneElement,
8+
isValidElement,
9+
} from "react"
10+
11+
const isFunction = (value: unknown): value is (...args: unknown[]) => unknown =>
12+
typeof value === "function"
13+
14+
const isObject = (value: unknown): value is Record<string, unknown> =>
15+
typeof value === "object" && value !== null
16+
17+
const mergeRefs = <T,>(...refs: unknown[]): RefCallback<T> | undefined => {
18+
if (refs.every(ref => ref == null)) {
19+
return
20+
}
21+
22+
return (value: T | null) => {
23+
refs.forEach(ref => {
24+
if (!ref) return
25+
26+
if (isFunction(ref)) {
27+
ref(value)
28+
} else if (isObject(ref) && "current" in ref) {
29+
ref["current"] = value
30+
}
31+
})
32+
}
33+
}
34+
35+
const mergeFunctions =
36+
(slotFn: unknown, childFn: unknown) =>
37+
(...args: unknown[]) => {
38+
if (isFunction(slotFn)) slotFn(...args)
39+
if (isFunction(childFn)) childFn(...args)
40+
}
41+
42+
const mergeClassNames = (slot: unknown, child: unknown) =>
43+
[child, slot]
44+
.filter(className => className && typeof className === "string")
45+
.join(" ")
46+
47+
const mergeStyles = (slot: CSSProperties, child: CSSProperties) => ({
48+
...slot,
49+
...child,
50+
})
51+
52+
const mergeProps = (
53+
slotProps: Record<string, unknown>,
54+
childProps: Record<string, unknown>
55+
) => {
56+
const result: Record<string, unknown> = { ...childProps }
57+
58+
for (const key in slotProps) {
59+
const slotValue = slotProps[key]
60+
const childValue = childProps[key]
61+
62+
if (key === "ref") {
63+
result[key] = mergeRefs(slotValue, childValue)
64+
} else if (key === "className") {
65+
result[key] = mergeClassNames(slotValue, childValue)
66+
} else if (key === "style" && isObject(slotValue) && isObject(childValue)) {
67+
result[key] = mergeStyles(slotValue, childValue)
68+
} else if (/^on[A-Z]/.test(key)) {
69+
result[key] = mergeFunctions(slotValue, childValue)
70+
} else {
71+
result[key] = childValue ?? slotValue
72+
}
73+
}
74+
75+
return result
76+
}
77+
78+
type SlotProps = PropsWithChildren<
79+
HTMLAttributes<HTMLElement> & {
80+
ref?: Ref<HTMLElement>
81+
}
82+
>
83+
84+
export const Slot = ({ children, ...slotProps }: SlotProps) => {
85+
if (!isValidElement(children)) {
86+
throw new Error("Slot requires a single React element as child.")
87+
}
88+
89+
return cloneElement(
90+
children,
91+
mergeProps(
92+
slotProps as Record<string, unknown>,
93+
children.props as Record<string, unknown>
94+
)
95+
)
96+
}

src/components/utility/tab-pattern.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ import {
1111
useState,
1212
} from "react"
1313

14-
import { Slot } from "@radix-ui/react-slot"
15-
14+
import { Slot } from "components/utility/slot"
1615
import { AsChildProp, ClassNameProp } from "types/base-props"
1716
import { createContext } from "utils/create-context"
1817

0 commit comments

Comments
 (0)