Bluesky app fork with some witchin' additions 馃挮
witchsky.app
bluesky
fork
client
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, hitSlop, 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 hitSlop={hitSlop}
104 label={label}
105 onPress={control.open}
106 style={[a.flex_1, a.justify_between, a.pl_lg, a.pr_md]}
107 color="secondary"
108 size="large"
109 shape="rectangular">
110 <>{children}</>
111 </Button>
112 )
113 }
114}
115
116export function ValueText({
117 placeholder,
118 children = value => value.label,
119 style,
120}: ValueProps) {
121 const [value] = useContext(ValueTextContext)
122 const t = useTheme()
123
124 let text = value && children(value)
125 if (!text) text = placeholder
126
127 return (
128 <ButtonText style={[t.atoms.text, a.font_normal, style]} emoji>
129 {text}
130 </ButtonText>
131 )
132}
133
134export function Icon({}: IconProps) {
135 return <ButtonIcon icon={ChevronUpDownIcon} />
136}
137
138export function Content<T>({
139 items,
140 valueExtractor = defaultItemValueExtractor,
141 ...props
142}: ContentProps<T>) {
143 const {control, ...context} = useSelectContext()
144 const [, setValue] = useContext(ValueTextContext)
145
146 useLayoutEffect(() => {
147 const item = items.find(item => valueExtractor(item) === context.value)
148 if (item) {
149 setValue(item)
150 }
151 }, [items, context.value, valueExtractor, setValue])
152
153 return (
154 <Dialog.Outer control={control}>
155 <ContentInner
156 control={control}
157 items={items}
158 valueExtractor={valueExtractor}
159 {...props}
160 {...context}
161 />
162 </Dialog.Outer>
163 )
164}
165
166function ContentInner<T>({
167 label,
168 items,
169 renderItem,
170 valueExtractor,
171 ...context
172}: ContentProps<T> & ContextType) {
173 const {_} = useLingui()
174 const [headerHeight, setHeaderHeight] = useState(61)
175
176 const render = useCallback(
177 ({item, index}: {item: T; index: number}) => {
178 return renderItem(item, index, context.value)
179 },
180 [renderItem, context.value],
181 )
182
183 return (
184 <Context.Provider value={context}>
185 <Dialog.Header
186 onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}
187 style={[
188 a.absolute,
189 a.top_0,
190 a.left_0,
191 a.right_0,
192 a.z_10,
193 a.pt_3xl,
194 a.pb_sm,
195 a.border_b_0,
196 ]}>
197 <Dialog.HeaderText
198 style={[a.flex_1, a.px_xl, a.text_left, a.font_bold, a.text_2xl]}>
199 {label ?? _(msg`Select an option`)}
200 </Dialog.HeaderText>
201 </Dialog.Header>
202 <Dialog.Handle />
203 <Dialog.InnerFlatList
204 headerOffset={headerHeight}
205 data={items}
206 renderItem={render}
207 keyExtractor={valueExtractor}
208 />
209 </Context.Provider>
210 )
211}
212
213function defaultItemValueExtractor(item: any) {
214 return item.value
215}
216
217const ItemContext = createContext<{
218 selected: boolean
219 hovered: boolean
220 focused: boolean
221 pressed: boolean
222}>({
223 selected: false,
224 hovered: false,
225 focused: false,
226 pressed: false,
227})
228ItemContext.displayName = 'SelectItemContext'
229
230export function useItemContext() {
231 return useContext(ItemContext)
232}
233
234export function Item({children, value, label, style}: ItemProps) {
235 const t = useTheme()
236 const control = Dialog.useDialogContext()
237 const {value: selected, onValueChange} = useSelectContext()
238
239 return (
240 <Button
241 role="listitem"
242 label={label}
243 style={[a.flex_1]}
244 onPress={() => {
245 control.close(() => {
246 onValueChange?.(value)
247 })
248 }}>
249 {({hovered, focused, pressed}) => (
250 <ItemContext.Provider
251 value={{selected: value === selected, hovered, focused, pressed}}>
252 <View
253 style={[
254 a.flex_1,
255 a.px_xl,
256 (focused || pressed) && t.atoms.bg_contrast_25,
257 a.flex_row,
258 a.align_center,
259 a.gap_sm,
260 a.py_md,
261 style,
262 ]}>
263 {children}
264 </View>
265 </ItemContext.Provider>
266 )}
267 </Button>
268 )
269}
270
271export function ItemText({children, style, emoji}: ItemTextProps) {
272 const {selected} = useItemContext()
273
274 return (
275 <Text
276 style={[a.text_md, selected && a.font_semi_bold, style]}
277 emoji={emoji}>
278 {children}
279 </Text>
280 )
281}
282
283export function ItemIndicator({icon: Icon}: ItemIndicatorProps) {
284 const {selected, focused, hovered} = useItemContext()
285
286 if (Icon) {
287 return <View style={{width: 24}}>{selected && <Icon size="md" />}</View>
288 }
289
290 return (
291 <BaseRadio
292 selected={selected}
293 focused={focused}
294 hovered={hovered}
295 isInvalid={false}
296 disabled={false}
297 />
298 )
299}
300
301export function Separator() {
302 const t = useTheme()
303
304 return (
305 <View
306 style={[
307 a.flex_1,
308 a.border_b,
309 t.atoms.border_contrast_low,
310 a.mx_xl,
311 a.my_xs,
312 ]}
313 />
314 )
315}