mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import {createContext, forwardRef, useContext, useMemo} from 'react'
2import {View} from 'react-native'
3import {Select as RadixSelect} from 'radix-ui'
4
5import {flatten, useTheme} from '#/alf'
6import {atoms as a} from '#/alf'
7import {useInteractionState} from '#/components/hooks/useInteractionState'
8import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
9import {
10 ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon,
11 ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon,
12} from '#/components/icons/Chevron'
13import {Text} from '#/components/Typography'
14import {
15 type ContentProps,
16 type IconProps,
17 type ItemIndicatorProps,
18 type ItemProps,
19 type RadixPassThroughTriggerProps,
20 type RootProps,
21 type TriggerProps,
22 type ValueProps,
23} from './types'
24
25const SelectedValueContext = createContext<string | undefined | null>(null)
26SelectedValueContext.displayName = 'SelectSelectedValueContext'
27
28export function Root(props: RootProps) {
29 return (
30 <SelectedValueContext.Provider value={props.value}>
31 <RadixSelect.Root {...props} />
32 </SelectedValueContext.Provider>
33 )
34}
35
36const RadixTriggerPassThrough = forwardRef(
37 (
38 props: {
39 children: (
40 props: RadixPassThroughTriggerProps & {
41 ref: React.Ref<any>
42 },
43 ) => React.ReactNode
44 },
45 ref,
46 ) => {
47 // @ts-expect-error Radix provides no types of this stuff
48
49 return props.children?.({...props, ref})
50 },
51)
52RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'
53
54export function Trigger({children, label}: TriggerProps) {
55 const t = useTheme()
56 const {
57 state: hovered,
58 onIn: onMouseEnter,
59 onOut: onMouseLeave,
60 } = useInteractionState()
61 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
62
63 if (typeof children === 'function') {
64 return (
65 <RadixSelect.Trigger asChild>
66 <RadixTriggerPassThrough>
67 {props =>
68 children({
69 isNative: false,
70 state: {
71 hovered,
72 focused,
73 pressed: false,
74 },
75 props: {
76 ...props,
77 onPress: props.onClick,
78 onFocus: onFocus,
79 onBlur: onBlur,
80 onMouseEnter,
81 onMouseLeave,
82 accessibilityLabel: label,
83 },
84 })
85 }
86 </RadixTriggerPassThrough>
87 </RadixSelect.Trigger>
88 )
89 } else {
90 return (
91 <RadixSelect.Trigger
92 onFocus={onFocus}
93 onBlur={onBlur}
94 onMouseEnter={onMouseEnter}
95 onMouseLeave={onMouseLeave}
96 style={flatten([
97 a.flex,
98 a.relative,
99 t.atoms.bg_contrast_25,
100 a.rounded_sm,
101 a.w_full,
102 a.align_center,
103 a.gap_sm,
104 a.justify_between,
105 a.py_sm,
106 a.px_md,
107 a.pointer,
108 {
109 maxWidth: 400,
110 outline: 0,
111 borderWidth: 2,
112 borderStyle: 'solid',
113 borderColor: focused
114 ? t.palette.primary_500
115 : hovered
116 ? t.palette.contrast_100
117 : t.palette.contrast_25,
118 },
119 ])}>
120 {children}
121 </RadixSelect.Trigger>
122 )
123 }
124}
125
126export function ValueText({children: _, style, ...props}: ValueProps) {
127 return (
128 <Text style={style}>
129 <RadixSelect.Value {...props} />
130 </Text>
131 )
132}
133
134export function Icon({style}: IconProps) {
135 const t = useTheme()
136 return (
137 <RadixSelect.Icon>
138 <ChevronDownIcon style={[t.atoms.text, style]} size="xs" />
139 </RadixSelect.Icon>
140 )
141}
142
143export function Content<T>({items, renderItem}: ContentProps<T>) {
144 const t = useTheme()
145 const selectedValue = useContext(SelectedValueContext)
146
147 const scrollBtnStyles: React.CSSProperties[] = [
148 a.absolute,
149 a.flex,
150 a.align_center,
151 a.justify_center,
152 a.rounded_sm,
153 a.z_10,
154 ]
155 const up: React.CSSProperties[] = [
156 ...scrollBtnStyles,
157 a.pt_sm,
158 a.pb_lg,
159 {
160 top: 0,
161 left: 0,
162 right: 0,
163 borderBottomLeftRadius: 0,
164 borderBottomRightRadius: 0,
165 background: `linear-gradient(to bottom, ${t.atoms.bg.backgroundColor} 0%, transparent 100%)`,
166 },
167 ]
168 const down: React.CSSProperties[] = [
169 ...scrollBtnStyles,
170 a.pt_lg,
171 a.pb_sm,
172 {
173 bottom: 0,
174 left: 0,
175 right: 0,
176 borderBottomLeftRadius: 0,
177 borderBottomRightRadius: 0,
178 background: `linear-gradient(to top, ${t.atoms.bg.backgroundColor} 0%, transparent 100%)`,
179 },
180 ]
181
182 return (
183 <RadixSelect.Portal>
184 <RadixSelect.Content
185 style={flatten([t.atoms.bg, a.rounded_sm, a.overflow_hidden])}
186 position="popper"
187 sideOffset={5}
188 className="radix-select-content">
189 <View
190 style={[
191 a.flex_1,
192 a.border,
193 t.atoms.border_contrast_low,
194 a.rounded_sm,
195 a.overflow_hidden,
196 ]}>
197 <RadixSelect.ScrollUpButton style={flatten(up)}>
198 <ChevronUpIcon style={[t.atoms.text]} size="xs" />
199 </RadixSelect.ScrollUpButton>
200 <RadixSelect.Viewport style={flatten([a.p_xs])}>
201 {items.map((item, index) => renderItem(item, index, selectedValue))}
202 </RadixSelect.Viewport>
203 <RadixSelect.ScrollDownButton style={flatten(down)}>
204 <ChevronDownIcon style={[t.atoms.text]} size="xs" />
205 </RadixSelect.ScrollDownButton>
206 </View>
207 </RadixSelect.Content>
208 </RadixSelect.Portal>
209 )
210}
211
212const ItemContext = createContext<{
213 hovered: boolean
214 focused: boolean
215 pressed: boolean
216 selected: boolean
217}>({
218 hovered: false,
219 focused: false,
220 pressed: false,
221 selected: false,
222})
223ItemContext.displayName = 'SelectItemContext'
224
225export function useItemContext() {
226 return useContext(ItemContext)
227}
228
229export function Item({ref, value, style, children}: ItemProps) {
230 const t = useTheme()
231 const {
232 state: hovered,
233 onIn: onMouseEnter,
234 onOut: onMouseLeave,
235 } = useInteractionState()
236 const selected = useContext(SelectedValueContext) === value
237 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
238 const ctx = useMemo(
239 () => ({hovered, focused, pressed: false, selected}),
240 [hovered, focused, selected],
241 )
242 return (
243 <RadixSelect.Item
244 ref={ref}
245 value={value}
246 onMouseEnter={onMouseEnter}
247 onMouseLeave={onMouseLeave}
248 onFocus={onFocus}
249 onBlur={onBlur}
250 style={flatten([
251 t.atoms.text,
252 a.relative,
253 a.flex,
254 {minHeight: 25, paddingLeft: 30, paddingRight: 35},
255 a.user_select_none,
256 a.align_center,
257 a.rounded_xs,
258 a.py_2xs,
259 a.text_sm,
260 {outline: 0},
261 (hovered || focused) && {backgroundColor: t.palette.primary_50},
262 selected && [a.font_semi_bold],
263 a.transition_color,
264 style,
265 ])}>
266 <ItemContext.Provider value={ctx}>{children}</ItemContext.Provider>
267 </RadixSelect.Item>
268 )
269}
270
271export const ItemText = RadixSelect.ItemText
272
273export function ItemIndicator({icon: Icon = CheckIcon}: ItemIndicatorProps) {
274 return (
275 <RadixSelect.ItemIndicator
276 style={flatten([
277 a.absolute,
278 {left: 0, width: 30},
279 a.flex,
280 a.align_center,
281 a.justify_center,
282 ])}>
283 <Icon size="sm" />
284 </RadixSelect.ItemIndicator>
285 )
286}