Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
at main 315 lines 7.3 kB view raw
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}