forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {createContext, forwardRef, Fragment, 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_50,
100 a.w_full,
101 a.align_center,
102 a.gap_sm,
103 a.justify_between,
104 a.py_sm,
105 a.px_md,
106 a.pointer,
107 {
108 borderRadius: 10,
109 maxWidth: 400,
110 outline: 0,
111 borderWidth: 2,
112 borderStyle: 'solid',
113 borderColor: focused
114 ? t.palette.primary_500
115 : t.palette.contrast_50,
116 },
117 ])}>
118 {children}
119 </RadixSelect.Trigger>
120 )
121 }
122}
123
124export function ValueText({children: _, style, ...props}: ValueProps) {
125 return (
126 <Text style={style}>
127 <RadixSelect.Value {...props} />
128 </Text>
129 )
130}
131
132export function Icon({style}: IconProps) {
133 const t = useTheme()
134 return (
135 <RadixSelect.Icon>
136 <ChevronDownIcon style={[t.atoms.text, style]} size="xs" />
137 </RadixSelect.Icon>
138 )
139}
140
141export function Content<T>({
142 items,
143 renderItem,
144 valueExtractor = defaultItemValueExtractor,
145}: ContentProps<T>) {
146 const t = useTheme()
147 const selectedValue = useContext(SelectedValueContext)
148
149 const scrollBtnStyles: React.CSSProperties[] = [
150 a.absolute,
151 a.flex,
152 a.align_center,
153 a.justify_center,
154 a.rounded_sm,
155 a.z_10,
156 ]
157 const up: React.CSSProperties[] = [
158 ...scrollBtnStyles,
159 a.pt_sm,
160 a.pb_lg,
161 {
162 top: 0,
163 left: 0,
164 right: 0,
165 borderBottomLeftRadius: 0,
166 borderBottomRightRadius: 0,
167 background: `linear-gradient(to bottom, ${t.atoms.bg.backgroundColor} 0%, transparent 100%)`,
168 },
169 ]
170 const down: React.CSSProperties[] = [
171 ...scrollBtnStyles,
172 a.pt_lg,
173 a.pb_sm,
174 {
175 bottom: 0,
176 left: 0,
177 right: 0,
178 borderBottomLeftRadius: 0,
179 borderBottomRightRadius: 0,
180 background: `linear-gradient(to top, ${t.atoms.bg.backgroundColor} 0%, transparent 100%)`,
181 },
182 ]
183
184 return (
185 <RadixSelect.Portal>
186 <RadixSelect.Content
187 style={flatten([t.atoms.bg, a.rounded_sm, a.overflow_hidden])}
188 position="popper"
189 sideOffset={5}
190 className="radix-select-content">
191 <View
192 style={[
193 a.flex_1,
194 a.border,
195 t.atoms.border_contrast_low,
196 a.rounded_sm,
197 a.overflow_hidden,
198 ]}>
199 <RadixSelect.ScrollUpButton style={flatten(up)}>
200 <ChevronUpIcon style={[t.atoms.text]} size="xs" />
201 </RadixSelect.ScrollUpButton>
202 <RadixSelect.Viewport style={flatten([a.p_xs])}>
203 {items.map((item, index) => (
204 <Fragment key={valueExtractor(item)}>
205 {renderItem(item, index, selectedValue)}
206 </Fragment>
207 ))}
208 </RadixSelect.Viewport>
209 <RadixSelect.ScrollDownButton style={flatten(down)}>
210 <ChevronDownIcon style={[t.atoms.text]} size="xs" />
211 </RadixSelect.ScrollDownButton>
212 </View>
213 </RadixSelect.Content>
214 </RadixSelect.Portal>
215 )
216}
217
218function defaultItemValueExtractor(item: any) {
219 return item.value
220}
221
222const ItemContext = createContext<{
223 hovered: boolean
224 focused: boolean
225 pressed: boolean
226 selected: boolean
227}>({
228 hovered: false,
229 focused: false,
230 pressed: false,
231 selected: false,
232})
233ItemContext.displayName = 'SelectItemContext'
234
235export function useItemContext() {
236 return useContext(ItemContext)
237}
238
239export function Item({ref, value, style, children}: ItemProps) {
240 const t = useTheme()
241 const {
242 state: hovered,
243 onIn: onMouseEnter,
244 onOut: onMouseLeave,
245 } = useInteractionState()
246 const selected = useContext(SelectedValueContext) === value
247 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
248 const ctx = useMemo(
249 () => ({hovered, focused, pressed: false, selected}),
250 [hovered, focused, selected],
251 )
252 return (
253 <RadixSelect.Item
254 ref={ref}
255 value={value}
256 onMouseEnter={onMouseEnter}
257 onMouseLeave={onMouseLeave}
258 onFocus={onFocus}
259 onBlur={onBlur}
260 style={flatten([
261 t.atoms.text,
262 a.relative,
263 a.flex,
264 {minHeight: 25, paddingLeft: 30, paddingRight: 35},
265 a.user_select_none,
266 a.align_center,
267 a.rounded_xs,
268 a.py_2xs,
269 a.text_sm,
270 {outline: 0},
271 (hovered || focused) && {backgroundColor: t.palette.primary_50},
272 selected && [a.font_semi_bold],
273 a.transition_color,
274 style,
275 ])}>
276 <ItemContext.Provider value={ctx}>{children}</ItemContext.Provider>
277 </RadixSelect.Item>
278 )
279}
280
281export const ItemText = RadixSelect.ItemText
282
283export function ItemIndicator({icon: Icon = CheckIcon}: ItemIndicatorProps) {
284 return (
285 <RadixSelect.ItemIndicator
286 style={flatten([
287 a.absolute,
288 {left: 0, width: 30},
289 a.flex,
290 a.align_center,
291 a.justify_center,
292 ])}>
293 <Icon size="sm" />
294 </RadixSelect.ItemIndicator>
295 )
296}