Bluesky app fork with some witchin' additions 馃挮
at jean/pds-label 314 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/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}