forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {
2 createContext,
3 useCallback,
4 useContext,
5 useLayoutEffect,
6 useMemo,
7 useState,
8} from 'react'
9import {View} from 'react-native'
10import {msg} from '@lingui/core/macro'
11import {useLingui} from '@lingui/react'
12
13import {atoms as a, useTheme} from '#/alf'
14import {Button, ButtonIcon, ButtonText} from '#/components/Button'
15import * as Dialog from '#/components/Dialog'
16import {useInteractionState} from '#/components/hooks/useInteractionState'
17import {ChevronTopBottom_Stroke2_Corner0_Rounded as ChevronUpDownIcon} from '#/components/icons/Chevron'
18import {Text} from '#/components/Typography'
19import {BaseRadio} from '../forms/Toggle'
20import {
21 type ContentProps,
22 type IconProps,
23 type ItemIndicatorProps,
24 type ItemProps,
25 type ItemTextProps,
26 type RootProps,
27 type TriggerProps,
28 type ValueProps,
29} from './types'
30
31type ContextType = {
32 control: Dialog.DialogControlProps
33} & Pick<RootProps, 'value' | 'onValueChange' | 'disabled'>
34
35const Context = createContext<ContextType | null>(null)
36Context.displayName = 'SelectContext'
37
38const ValueTextContext = createContext<
39 [any, React.Dispatch<React.SetStateAction<any>>]
40>([undefined, () => {}])
41ValueTextContext.displayName = 'ValueTextContext'
42
43function useSelectContext() {
44 const ctx = useContext(Context)
45 if (!ctx) {
46 throw new Error('Select components must must be used within a Select.Root')
47 }
48 return ctx
49}
50
51export function Root({children, value, onValueChange, disabled}: RootProps) {
52 const control = Dialog.useDialogControl()
53 const valueTextCtx = useState<any>()
54
55 const ctx = useMemo(
56 () => ({
57 control,
58 value,
59 onValueChange,
60 disabled,
61 }),
62 [control, value, onValueChange, disabled],
63 )
64 return (
65 <Context.Provider value={ctx}>
66 <ValueTextContext.Provider value={valueTextCtx}>
67 {children}
68 </ValueTextContext.Provider>
69 </Context.Provider>
70 )
71}
72
73export function Trigger({children, label}: TriggerProps) {
74 const {control} = useSelectContext()
75 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
76 const {
77 state: pressed,
78 onIn: onPressIn,
79 onOut: onPressOut,
80 } = useInteractionState()
81
82 if (typeof children === 'function') {
83 return children({
84 IS_NATIVE: true,
85 control,
86 state: {
87 hovered: false,
88 focused,
89 pressed,
90 },
91 props: {
92 onPress: control.open,
93 onFocus,
94 onBlur,
95 onPressIn,
96 onPressOut,
97 accessibilityLabel: label,
98 },
99 })
100 } else {
101 return (
102 <Button
103 label={label}
104 onPress={control.open}
105 style={[a.flex_1, a.justify_between, a.pl_lg, a.pr_md]}
106 color="secondary"
107 size="large"
108 shape="rectangular">
109 <>{children}</>
110 </Button>
111 )
112 }
113}
114
115export function ValueText({
116 placeholder,
117 children = value => value.label,
118 style,
119}: ValueProps) {
120 const [value] = useContext(ValueTextContext)
121 const t = useTheme()
122
123 let text = value && children(value)
124 if (!text) text = placeholder
125
126 return (
127 <ButtonText style={[t.atoms.text, a.font_normal, style]} emoji>
128 {text}
129 </ButtonText>
130 )
131}
132
133export function Icon({}: IconProps) {
134 return <ButtonIcon icon={ChevronUpDownIcon} />
135}
136
137export function Content<T>({
138 items,
139 valueExtractor = defaultItemValueExtractor,
140 ...props
141}: ContentProps<T>) {
142 const {control, ...context} = useSelectContext()
143 const [, setValue] = useContext(ValueTextContext)
144
145 useLayoutEffect(() => {
146 const item = items.find(item => valueExtractor(item) === context.value)
147 if (item) {
148 setValue(item)
149 }
150 }, [items, context.value, valueExtractor, setValue])
151
152 return (
153 <Dialog.Outer control={control}>
154 <ContentInner
155 control={control}
156 items={items}
157 valueExtractor={valueExtractor}
158 {...props}
159 {...context}
160 />
161 </Dialog.Outer>
162 )
163}
164
165function ContentInner<T>({
166 label,
167 items,
168 renderItem,
169 valueExtractor,
170 ...context
171}: ContentProps<T> & ContextType) {
172 const {_} = useLingui()
173 const [headerHeight, setHeaderHeight] = useState(61)
174
175 const render = useCallback(
176 ({item, index}: {item: T; index: number}) => {
177 return renderItem(item, index, context.value)
178 },
179 [renderItem, context.value],
180 )
181
182 return (
183 <Context.Provider value={context}>
184 <Dialog.Header
185 onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}
186 style={[
187 a.absolute,
188 a.top_0,
189 a.left_0,
190 a.right_0,
191 a.z_10,
192 a.pt_3xl,
193 a.pb_sm,
194 a.border_b_0,
195 ]}>
196 <Dialog.HeaderText
197 style={[a.flex_1, a.px_xl, a.text_left, a.font_bold, a.text_2xl]}>
198 {label ?? _(msg`Select an option`)}
199 </Dialog.HeaderText>
200 </Dialog.Header>
201 <Dialog.Handle />
202 <Dialog.InnerFlatList
203 headerOffset={headerHeight}
204 data={items}
205 renderItem={render}
206 keyExtractor={valueExtractor}
207 />
208 </Context.Provider>
209 )
210}
211
212function defaultItemValueExtractor(item: any) {
213 return item.value
214}
215
216const ItemContext = createContext<{
217 selected: boolean
218 hovered: boolean
219 focused: boolean
220 pressed: boolean
221}>({
222 selected: false,
223 hovered: false,
224 focused: false,
225 pressed: false,
226})
227ItemContext.displayName = 'SelectItemContext'
228
229export function useItemContext() {
230 return useContext(ItemContext)
231}
232
233export function Item({children, value, label, style}: ItemProps) {
234 const t = useTheme()
235 const control = Dialog.useDialogContext()
236 const {value: selected, onValueChange} = useSelectContext()
237
238 return (
239 <Button
240 role="listitem"
241 label={label}
242 style={[a.flex_1]}
243 onPress={() => {
244 control.close(() => {
245 onValueChange?.(value)
246 })
247 }}>
248 {({hovered, focused, pressed}) => (
249 <ItemContext.Provider
250 value={{selected: value === selected, hovered, focused, pressed}}>
251 <View
252 style={[
253 a.flex_1,
254 a.px_xl,
255 (focused || pressed) && t.atoms.bg_contrast_25,
256 a.flex_row,
257 a.align_center,
258 a.gap_sm,
259 a.py_md,
260 style,
261 ]}>
262 {children}
263 </View>
264 </ItemContext.Provider>
265 )}
266 </Button>
267 )
268}
269
270export function ItemText({children, style, emoji}: ItemTextProps) {
271 const {selected} = useItemContext()
272
273 return (
274 <Text
275 style={[a.text_md, selected && a.font_semi_bold, style]}
276 emoji={emoji}>
277 {children}
278 </Text>
279 )
280}
281
282export function ItemIndicator({icon: Icon}: ItemIndicatorProps) {
283 const {selected, focused, hovered} = useItemContext()
284
285 if (Icon) {
286 return <View style={{width: 24}}>{selected && <Icon size="md" />}</View>
287 }
288
289 return (
290 <BaseRadio
291 selected={selected}
292 focused={focused}
293 hovered={hovered}
294 isInvalid={false}
295 disabled={false}
296 />
297 )
298}
299
300export function Separator() {
301 const t = useTheme()
302
303 return (
304 <View
305 style={[
306 a.flex_1,
307 a.border_b,
308 t.atoms.border_contrast_low,
309 a.mx_xl,
310 a.my_xs,
311 ]}
312 />
313 )
314}