Bluesky app fork with some witchin' additions 馃挮
at readme-update 315 lines 7.2 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/macro' 11import {useLingui} from '@lingui/react' 12 13import {useTheme} from '#/alf' 14import {atoms as a} from '#/alf' 15import {Button, ButtonIcon, ButtonText} from '#/components/Button' 16import * as Dialog from '#/components/Dialog' 17import {useInteractionState} from '#/components/hooks/useInteractionState' 18import {ChevronTopBottom_Stroke2_Corner0_Rounded as ChevronUpDownIcon} from '#/components/icons/Chevron' 19import {Text} from '#/components/Typography' 20import {BaseRadio} from '../forms/Toggle' 21import { 22 type ContentProps, 23 type IconProps, 24 type ItemIndicatorProps, 25 type ItemProps, 26 type ItemTextProps, 27 type RootProps, 28 type TriggerProps, 29 type ValueProps, 30} from './types' 31 32type ContextType = { 33 control: Dialog.DialogControlProps 34} & Pick<RootProps, 'value' | 'onValueChange' | 'disabled'> 35 36const Context = createContext<ContextType | null>(null) 37Context.displayName = 'SelectContext' 38 39const ValueTextContext = createContext< 40 [any, React.Dispatch<React.SetStateAction<any>>] 41>([undefined, () => {}]) 42ValueTextContext.displayName = 'ValueTextContext' 43 44function useSelectContext() { 45 const ctx = useContext(Context) 46 if (!ctx) { 47 throw new Error('Select components must must be used within a Select.Root') 48 } 49 return ctx 50} 51 52export function Root({children, value, onValueChange, disabled}: RootProps) { 53 const control = Dialog.useDialogControl() 54 const valueTextCtx = useState<any>() 55 56 const ctx = useMemo( 57 () => ({ 58 control, 59 value, 60 onValueChange, 61 disabled, 62 }), 63 [control, value, onValueChange, disabled], 64 ) 65 return ( 66 <Context.Provider value={ctx}> 67 <ValueTextContext.Provider value={valueTextCtx}> 68 {children} 69 </ValueTextContext.Provider> 70 </Context.Provider> 71 ) 72} 73 74export function Trigger({children, label}: TriggerProps) { 75 const {control} = useSelectContext() 76 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() 77 const { 78 state: pressed, 79 onIn: onPressIn, 80 onOut: onPressOut, 81 } = useInteractionState() 82 83 if (typeof children === 'function') { 84 return children({ 85 IS_NATIVE: true, 86 control, 87 state: { 88 hovered: false, 89 focused, 90 pressed, 91 }, 92 props: { 93 onPress: control.open, 94 onFocus, 95 onBlur, 96 onPressIn, 97 onPressOut, 98 accessibilityLabel: label, 99 }, 100 }) 101 } else { 102 return ( 103 <Button 104 label={label} 105 onPress={control.open} 106 style={[a.flex_1, a.justify_between]} 107 color="secondary" 108 size="small" 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}