1
+ import { ForwardedRef , forwardRef , ReactNode , useMemo } from "react" ;
2
+ import { Listbox } from "@headlessui/react" ;
3
+ import { twMerge } from "tailwind-merge" ;
4
+ import { cva } from "class-variance-authority" ;
5
+ import { Control , Controller , FieldPath , FieldValues , PathValue , UseControllerProps } from "react-hook-form" ;
6
+ import { AnimatePresence , motion , Variants } from "framer-motion" ;
7
+
8
+ import { style as inputStyle , VariantProps as InputVariantProps } from "./Input" ;
9
+ import { mergeRefs } from "@/utils/react" ;
10
+ import ChevronDownIcon from "@heroicons/react/24/solid/ChevronDownIcon" ;
11
+
12
+ export const style = inputStyle ;
13
+ export type VariantProps = InputVariantProps ;
14
+
15
+ export const containerStyle = cva ( "absolute border-2 bg-white left-1/2 -translate-x-1/2 top-full rounded-lg min-w-fit w-full overflow-hidden translate-y-1 z-50" , {
16
+ variants : {
17
+ color : {
18
+ primary : "border-primary" ,
19
+ secondary : "border-secondary"
20
+ }
21
+ }
22
+ } ) ;
23
+ export const itemStyle = cva ( "px-4 py-2" , {
24
+ variants : {
25
+ color : {
26
+ primary : "ui-active:bg-primary/20" ,
27
+ secondary : "ui-active:bg-secondary/20"
28
+ }
29
+ }
30
+ } ) ;
31
+
32
+ export type BaseProps < TOption extends any , TOptions extends readonly TOption [ ] = readonly TOption [ ] > = {
33
+ name ?: string ;
34
+ className ?: string ;
35
+ containerClassName ?: string ;
36
+ itemClassName ?: string ;
37
+ value : TOption ;
38
+ getDisplayName : ( option : TOption ) => ReactNode ;
39
+ options : TOptions ;
40
+ onChange : ( option : TOption ) => void ;
41
+ onBlur ?: ( ) => void ;
42
+ } ;
43
+
44
+ export type SelectProps < TOption extends any > = BaseProps < TOption > & VariantProps ;
45
+
46
+ const optionsAnim = {
47
+ in : {
48
+ height : 0 ,
49
+ paddingBlock : 0 ,
50
+ } ,
51
+ anim : {
52
+ height : "auto" ,
53
+ paddingBlock : "0.5rem" ,
54
+ transition : {
55
+ duration : 0.15
56
+ }
57
+ } ,
58
+ exit : {
59
+ height : 0 ,
60
+ paddingBlock : 0 ,
61
+ transition : {
62
+ duration : 0.15
63
+ }
64
+ }
65
+ } as Variants ;
66
+
67
+ const Select = forwardRef ( < TOption extends any > (
68
+ {
69
+ name,
70
+ className,
71
+ containerClassName,
72
+ itemClassName,
73
+ value,
74
+ color,
75
+ options,
76
+ getDisplayName,
77
+ onChange,
78
+ onBlur
79
+ } : SelectProps < TOption > ,
80
+ ref : ForwardedRef < HTMLButtonElement >
81
+ ) => {
82
+
83
+ const computedClassName = useMemo ( ( ) => twMerge (
84
+ "relative flex items-center" ,
85
+ style ( { color } ) ,
86
+ className
87
+ ) , [ color , className ] ) ;
88
+
89
+ const computedContainerClassName = useMemo ( ( ) => twMerge (
90
+ containerStyle ( { color } ) ,
91
+ containerClassName
92
+ ) , [ color , containerClassName ] ) ;
93
+
94
+ const computedItemClassName = useMemo ( ( ) => twMerge (
95
+ itemStyle ( { color } ) ,
96
+ itemClassName
97
+ ) , [ color , itemClassName ] ) ;
98
+
99
+ return (
100
+
101
+ < Listbox name = { name } value = { value } onChange = { onChange } >
102
+ < Listbox . Button
103
+ ref = { ref }
104
+ onBlur = { onBlur }
105
+ className = { computedClassName }
106
+ >
107
+ { ( { open } ) => (
108
+ < >
109
+ { getDisplayName ( value ) }
110
+ < ChevronDownIcon className = "inline w-4 h-4 ml-2 motion-safe:transition-transform ui-open:rotate-180" />
111
+ < AnimatePresence >
112
+ { open && < motion . div
113
+ key = "options"
114
+ className = { computedContainerClassName }
115
+ variants = { optionsAnim }
116
+ initial = "in"
117
+ animate = "anim"
118
+ exit = "exit"
119
+ >
120
+ < Listbox . Options static >
121
+ { options . map ( ( option , i ) => (
122
+ < Listbox . Option key = { i } value = { option } className = { computedItemClassName } >
123
+ { getDisplayName ( option ) }
124
+ </ Listbox . Option >
125
+ ) ) }
126
+ </ Listbox . Options >
127
+ </ motion . div > }
128
+ </ AnimatePresence >
129
+ </ >
130
+ ) }
131
+ </ Listbox . Button >
132
+ </ Listbox >
133
+ ) ;
134
+ } ) ;
135
+
136
+ export type ControlledSelectProps < Values extends FieldValues , TOption extends any , Name extends FieldPath < Values > = FieldPath < Values > > = {
137
+ name : Name ,
138
+ defaultValue ?: PathValue < Values , Name > ;
139
+ control : Control < Values > ;
140
+ }
141
+ & Omit < SelectProps < TOption > , "value" | "onChange" >
142
+ & Pick < UseControllerProps < Values > , "rules" | "shouldUnregister" > ; ;
143
+
144
+ export const ControlledSelect = forwardRef ( < Values extends FieldValues , TOption extends any > (
145
+ {
146
+ name,
147
+ control,
148
+ defaultValue,
149
+ rules,
150
+ shouldUnregister,
151
+ ...props
152
+ } : ControlledSelectProps < Values , TOption > ,
153
+ ref : ForwardedRef < HTMLButtonElement >
154
+ ) => (
155
+ < Controller
156
+ name = { name }
157
+ control = { control }
158
+ rules = { rules }
159
+ defaultValue = { defaultValue }
160
+ shouldUnregister = { shouldUnregister }
161
+ render = { ( { field : { ref : controllerRef , value, ...controlledProps } } ) => (
162
+ < Select
163
+ ref = { mergeRefs ( ref , controllerRef ) }
164
+ value = { value }
165
+ { ...props }
166
+ { ...controlledProps }
167
+ />
168
+ ) }
169
+ />
170
+ ) ) ;
171
+
172
+
173
+ export default Select ;
0 commit comments