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 {useA11y} from '#/state/a11y'
6import {flatten, useTheme, web} from '#/alf'
7import {atoms as a} from '#/alf'
8import {useInteractionState} from '#/components/hooks/useInteractionState'
9import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
10import {
11 ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon,
12 ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon,
13} from '#/components/icons/Chevron'
14import {Text} from '#/components/Typography'
15import {
16 type ContentProps,
17 type IconProps,
18 type ItemIndicatorProps,
19 type ItemProps,
20 type ItemTextProps,
21 type RadixPassThroughTriggerProps,
22 type RootProps,
23 type TriggerProps,
24 type ValueProps,
25} from './types'
26
27const SelectedValueContext = createContext<string | undefined | null>(null)
28SelectedValueContext.displayName = 'SelectSelectedValueContext'
29
30export function Root(props: RootProps) {
31 return (
32 <SelectedValueContext.Provider value={props.value}>
33 <RadixSelect.Root {...props} />
34 </SelectedValueContext.Provider>
35 )
36}
37
38const RadixTriggerPassThrough = forwardRef(
39 (
40 props: {
41 children: (
42 props: RadixPassThroughTriggerProps & {
43 ref: React.Ref<any>
44 },
45 ) => React.ReactNode
46 },
47 ref,
48 ) => {
49 // @ts-expect-error Radix provides no types of this stuff
50
51 return props.children?.({...props, ref})
52 },
53)
54RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'
55
56export function Trigger({children, label}: TriggerProps) {
57 const t = useTheme()
58 const {
59 state: hovered,
60 onIn: onMouseEnter,
61 onOut: onMouseLeave,
62 } = useInteractionState()
63 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
64
65 if (typeof children === 'function') {
66 return (
67 <RadixSelect.Trigger asChild>
68 <RadixTriggerPassThrough>
69 {props =>
70 children({
71 IS_NATIVE: false,
72 state: {
73 hovered,
74 focused,
75 pressed: false,
76 },
77 props: {
78 ...props,
79 onPress: props.onClick,
80 onFocus: onFocus,
81 onBlur: onBlur,
82 onMouseEnter,
83 onMouseLeave,
84 accessibilityLabel: label,
85 },
86 })
87 }
88 </RadixTriggerPassThrough>
89 </RadixSelect.Trigger>
90 )
91 } else {
92 return (
93 <RadixSelect.Trigger
94 onFocus={onFocus}
95 onBlur={onBlur}
96 onMouseEnter={onMouseEnter}
97 onMouseLeave={onMouseLeave}
98 style={flatten([
99 a.flex,
100 a.relative,
101 t.atoms.bg_contrast_50,
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 borderRadius: 10,
110 maxWidth: 400,
111 outline: 0,
112 borderWidth: 2,
113 borderStyle: 'solid',
114 borderColor: focused
115 ? t.palette.primary_500
116 : t.palette.contrast_50,
117 },
118 ])}>
119 {children}
120 </RadixSelect.Trigger>
121 )
122 }
123}
124
125export function ValueText({
126 children,
127 webOverrideValue,
128 style,
129 ...props
130}: ValueProps) {
131 let content
132
133 if (webOverrideValue && children) {
134 content = children(webOverrideValue)
135 }
136
137 return (
138 <Text style={style}>
139 <RadixSelect.Value {...props}>{content}</RadixSelect.Value>
140 </Text>
141 )
142}
143
144export function Icon({style}: IconProps) {
145 const t = useTheme()
146 return (
147 <RadixSelect.Icon>
148 <ChevronDownIcon style={[t.atoms.text, style]} size="xs" />
149 </RadixSelect.Icon>
150 )
151}
152
153export function Content<T>({
154 items,
155 renderItem,
156 valueExtractor = defaultItemValueExtractor,
157}: ContentProps<T>) {
158 const t = useTheme()
159 const selectedValue = useContext(SelectedValueContext)
160 const {reduceMotionEnabled} = useA11y()
161
162 const scrollBtnStyles: React.CSSProperties[] = [
163 a.absolute,
164 a.flex,
165 a.align_center,
166 a.justify_center,
167 a.rounded_sm,
168 a.z_10,
169 ]
170 const up: React.CSSProperties[] = [
171 ...scrollBtnStyles,
172 a.pt_sm,
173 a.pb_lg,
174 {
175 top: 0,
176 left: 0,
177 right: 0,
178 borderBottomLeftRadius: 0,
179 borderBottomRightRadius: 0,
180 background: `linear-gradient(to bottom, ${t.atoms.bg.backgroundColor} 0%, transparent 100%)`,
181 },
182 ]
183 const down: React.CSSProperties[] = [
184 ...scrollBtnStyles,
185 a.pt_lg,
186 a.pb_sm,
187 {
188 bottom: 0,
189 left: 0,
190 right: 0,
191 borderBottomLeftRadius: 0,
192 borderBottomRightRadius: 0,
193 background: `linear-gradient(to top, ${t.atoms.bg.backgroundColor} 0%, transparent 100%)`,
194 },
195 ]
196
197 return (
198 <RadixSelect.Portal>
199 <RadixSelect.Content
200 style={flatten([t.atoms.bg, a.rounded_sm, a.overflow_hidden])}
201 position="popper"
202 align="center"
203 sideOffset={5}
204 className="radix-select-content"
205 // prevent the keyboard shortcut for opening the composer
206 onKeyDown={evt => evt.stopPropagation()}>
207 <View
208 style={[
209 a.flex_1,
210 a.border,
211 t.atoms.border_contrast_low,
212 a.rounded_sm,
213 a.overflow_hidden,
214 !reduceMotionEnabled && a.zoom_fade_in,
215 ]}>
216 <RadixSelect.ScrollUpButton style={flatten(up)}>
217 <ChevronUpIcon style={[t.atoms.text]} size="xs" />
218 </RadixSelect.ScrollUpButton>
219 <RadixSelect.Viewport style={flatten([a.p_xs])}>
220 {items.map((item, index) => (
221 <Fragment key={valueExtractor(item)}>
222 {renderItem(item, index, selectedValue)}
223 </Fragment>
224 ))}
225 </RadixSelect.Viewport>
226 <RadixSelect.ScrollDownButton style={flatten(down)}>
227 <ChevronDownIcon style={[t.atoms.text]} size="xs" />
228 </RadixSelect.ScrollDownButton>
229 </View>
230 </RadixSelect.Content>
231 </RadixSelect.Portal>
232 )
233}
234
235function defaultItemValueExtractor(item: any) {
236 return item.value
237}
238
239const ItemContext = createContext<{
240 hovered: boolean
241 focused: boolean
242 pressed: boolean
243 selected: boolean
244}>({
245 hovered: false,
246 focused: false,
247 pressed: false,
248 selected: false,
249})
250ItemContext.displayName = 'SelectItemContext'
251
252export function useItemContext() {
253 return useContext(ItemContext)
254}
255
256export function Item({ref, value, style, children}: ItemProps) {
257 const t = useTheme()
258 const {
259 state: hovered,
260 onIn: onMouseEnter,
261 onOut: onMouseLeave,
262 } = useInteractionState()
263 const selected = useContext(SelectedValueContext) === value
264 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
265 const ctx = useMemo(
266 () => ({hovered, focused, pressed: false, selected}),
267 [hovered, focused, selected],
268 )
269 return (
270 <RadixSelect.Item
271 ref={ref}
272 value={value}
273 onMouseEnter={onMouseEnter}
274 onMouseLeave={onMouseLeave}
275 onFocus={onFocus}
276 onBlur={onBlur}
277 style={flatten([
278 t.atoms.text,
279 a.relative,
280 a.flex,
281 {minHeight: 25, paddingLeft: 30, paddingRight: 8},
282 a.user_select_none,
283 a.align_center,
284 a.rounded_xs,
285 a.py_2xs,
286 a.text_sm,
287 {outline: 0},
288 (hovered || focused) && {backgroundColor: t.palette.primary_50},
289 selected && [a.font_semi_bold],
290 a.transition_color,
291 style,
292 ])}>
293 <ItemContext.Provider value={ctx}>{children}</ItemContext.Provider>
294 </RadixSelect.Item>
295 )
296}
297
298export const ItemText = function ItemText({children, style}: ItemTextProps) {
299 return (
300 <RadixSelect.ItemText asChild>
301 <Text style={flatten([style, web({pointerEvents: 'inherit'})])}>
302 {children}
303 </Text>
304 </RadixSelect.ItemText>
305 )
306}
307
308export function ItemIndicator({icon: Icon = CheckIcon}: ItemIndicatorProps) {
309 return (
310 <RadixSelect.ItemIndicator
311 style={flatten([
312 a.absolute,
313 {left: 0, width: 30},
314 a.flex,
315 a.align_center,
316 a.justify_center,
317 ])}>
318 <Icon size="sm" />
319 </RadixSelect.ItemIndicator>
320 )
321}
322
323export function Separator() {
324 const t = useTheme()
325
326 return (
327 <RadixSelect.Separator
328 style={flatten([
329 {
330 height: 1,
331 backgroundColor: t.atoms.border_contrast_low.borderColor,
332 },
333 a.my_xs,
334 a.w_full,
335 ])}
336 />
337 )
338}