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