Bluesky app fork with some witchin' additions 馃挮
at readme-update 338 lines 8.6 kB view raw
1import {createContext, forwardRef, Fragment, useContext, useMemo} from 'react' 2import {View} from 'react-native' 3import {Select as RadixSelect} from 'radix-ui' 4 5import {useA11y} from '#/state/a11y' 6import {flatten, useTheme, web} from '#/alf' 7import {atoms as a} from '#/alf' 8import {useInteractionState} from '#/components/hooks/useInteractionState' 9import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' 10import { 11 ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon, 12 ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon, 13} from '#/components/icons/Chevron' 14import {Text} from '#/components/Typography' 15import { 16 type ContentProps, 17 type IconProps, 18 type ItemIndicatorProps, 19 type ItemProps, 20 type ItemTextProps, 21 type RadixPassThroughTriggerProps, 22 type RootProps, 23 type TriggerProps, 24 type ValueProps, 25} from './types' 26 27const SelectedValueContext = createContext<string | undefined | null>(null) 28SelectedValueContext.displayName = 'SelectSelectedValueContext' 29 30export function Root(props: RootProps) { 31 return ( 32 <SelectedValueContext.Provider value={props.value}> 33 <RadixSelect.Root {...props} /> 34 </SelectedValueContext.Provider> 35 ) 36} 37 38const RadixTriggerPassThrough = forwardRef( 39 ( 40 props: { 41 children: ( 42 props: RadixPassThroughTriggerProps & { 43 ref: React.Ref<any> 44 }, 45 ) => React.ReactNode 46 }, 47 ref, 48 ) => { 49 // @ts-expect-error Radix provides no types of this stuff 50 51 return props.children?.({...props, ref}) 52 }, 53) 54RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough' 55 56export function Trigger({children, label}: TriggerProps) { 57 const t = useTheme() 58 const { 59 state: hovered, 60 onIn: onMouseEnter, 61 onOut: onMouseLeave, 62 } = useInteractionState() 63 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() 64 65 if (typeof children === 'function') { 66 return ( 67 <RadixSelect.Trigger asChild> 68 <RadixTriggerPassThrough> 69 {props => 70 children({ 71 IS_NATIVE: false, 72 state: { 73 hovered, 74 focused, 75 pressed: false, 76 }, 77 props: { 78 ...props, 79 onPress: props.onClick, 80 onFocus: onFocus, 81 onBlur: onBlur, 82 onMouseEnter, 83 onMouseLeave, 84 accessibilityLabel: label, 85 }, 86 }) 87 } 88 </RadixTriggerPassThrough> 89 </RadixSelect.Trigger> 90 ) 91 } else { 92 return ( 93 <RadixSelect.Trigger 94 onFocus={onFocus} 95 onBlur={onBlur} 96 onMouseEnter={onMouseEnter} 97 onMouseLeave={onMouseLeave} 98 style={flatten([ 99 a.flex, 100 a.relative, 101 t.atoms.bg_contrast_50, 102 a.align_center, 103 a.gap_sm, 104 a.justify_between, 105 a.py_sm, 106 a.px_md, 107 a.pointer, 108 { 109 borderRadius: 10, 110 maxWidth: 400, 111 outline: 0, 112 borderWidth: 2, 113 borderStyle: 'solid', 114 borderColor: focused 115 ? t.palette.primary_500 116 : t.palette.contrast_50, 117 }, 118 ])}> 119 {children} 120 </RadixSelect.Trigger> 121 ) 122 } 123} 124 125export function ValueText({ 126 children, 127 webOverrideValue, 128 style, 129 ...props 130}: ValueProps) { 131 let content 132 133 if (webOverrideValue && children) { 134 content = children(webOverrideValue) 135 } 136 137 return ( 138 <Text style={style}> 139 <RadixSelect.Value {...props}>{content}</RadixSelect.Value> 140 </Text> 141 ) 142} 143 144export function Icon({style}: IconProps) { 145 const t = useTheme() 146 return ( 147 <RadixSelect.Icon> 148 <ChevronDownIcon style={[t.atoms.text, style]} size="xs" /> 149 </RadixSelect.Icon> 150 ) 151} 152 153export function Content<T>({ 154 items, 155 renderItem, 156 valueExtractor = defaultItemValueExtractor, 157}: ContentProps<T>) { 158 const t = useTheme() 159 const selectedValue = useContext(SelectedValueContext) 160 const {reduceMotionEnabled} = useA11y() 161 162 const scrollBtnStyles: React.CSSProperties[] = [ 163 a.absolute, 164 a.flex, 165 a.align_center, 166 a.justify_center, 167 a.rounded_sm, 168 a.z_10, 169 ] 170 const up: React.CSSProperties[] = [ 171 ...scrollBtnStyles, 172 a.pt_sm, 173 a.pb_lg, 174 { 175 top: 0, 176 left: 0, 177 right: 0, 178 borderBottomLeftRadius: 0, 179 borderBottomRightRadius: 0, 180 background: `linear-gradient(to bottom, ${t.atoms.bg.backgroundColor} 0%, transparent 100%)`, 181 }, 182 ] 183 const down: React.CSSProperties[] = [ 184 ...scrollBtnStyles, 185 a.pt_lg, 186 a.pb_sm, 187 { 188 bottom: 0, 189 left: 0, 190 right: 0, 191 borderBottomLeftRadius: 0, 192 borderBottomRightRadius: 0, 193 background: `linear-gradient(to top, ${t.atoms.bg.backgroundColor} 0%, transparent 100%)`, 194 }, 195 ] 196 197 return ( 198 <RadixSelect.Portal> 199 <RadixSelect.Content 200 style={flatten([t.atoms.bg, a.rounded_sm, a.overflow_hidden])} 201 position="popper" 202 align="center" 203 sideOffset={5} 204 className="radix-select-content" 205 // prevent the keyboard shortcut for opening the composer 206 onKeyDown={evt => evt.stopPropagation()}> 207 <View 208 style={[ 209 a.flex_1, 210 a.border, 211 t.atoms.border_contrast_low, 212 a.rounded_sm, 213 a.overflow_hidden, 214 !reduceMotionEnabled && a.zoom_fade_in, 215 ]}> 216 <RadixSelect.ScrollUpButton style={flatten(up)}> 217 <ChevronUpIcon style={[t.atoms.text]} size="xs" /> 218 </RadixSelect.ScrollUpButton> 219 <RadixSelect.Viewport style={flatten([a.p_xs])}> 220 {items.map((item, index) => ( 221 <Fragment key={valueExtractor(item)}> 222 {renderItem(item, index, selectedValue)} 223 </Fragment> 224 ))} 225 </RadixSelect.Viewport> 226 <RadixSelect.ScrollDownButton style={flatten(down)}> 227 <ChevronDownIcon style={[t.atoms.text]} size="xs" /> 228 </RadixSelect.ScrollDownButton> 229 </View> 230 </RadixSelect.Content> 231 </RadixSelect.Portal> 232 ) 233} 234 235function defaultItemValueExtractor(item: any) { 236 return item.value 237} 238 239const ItemContext = createContext<{ 240 hovered: boolean 241 focused: boolean 242 pressed: boolean 243 selected: boolean 244}>({ 245 hovered: false, 246 focused: false, 247 pressed: false, 248 selected: false, 249}) 250ItemContext.displayName = 'SelectItemContext' 251 252export function useItemContext() { 253 return useContext(ItemContext) 254} 255 256export function Item({ref, value, style, children}: ItemProps) { 257 const t = useTheme() 258 const { 259 state: hovered, 260 onIn: onMouseEnter, 261 onOut: onMouseLeave, 262 } = useInteractionState() 263 const selected = useContext(SelectedValueContext) === value 264 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() 265 const ctx = useMemo( 266 () => ({hovered, focused, pressed: false, selected}), 267 [hovered, focused, selected], 268 ) 269 return ( 270 <RadixSelect.Item 271 ref={ref} 272 value={value} 273 onMouseEnter={onMouseEnter} 274 onMouseLeave={onMouseLeave} 275 onFocus={onFocus} 276 onBlur={onBlur} 277 style={flatten([ 278 t.atoms.text, 279 a.relative, 280 a.flex, 281 {minHeight: 25, paddingLeft: 30, paddingRight: 8}, 282 a.user_select_none, 283 a.align_center, 284 a.rounded_xs, 285 a.py_2xs, 286 a.text_sm, 287 {outline: 0}, 288 (hovered || focused) && {backgroundColor: t.palette.primary_50}, 289 selected && [a.font_semi_bold], 290 a.transition_color, 291 style, 292 ])}> 293 <ItemContext.Provider value={ctx}>{children}</ItemContext.Provider> 294 </RadixSelect.Item> 295 ) 296} 297 298export const ItemText = function ItemText({children, style}: ItemTextProps) { 299 return ( 300 <RadixSelect.ItemText asChild> 301 <Text style={flatten([style, web({pointerEvents: 'inherit'})])}> 302 {children} 303 </Text> 304 </RadixSelect.ItemText> 305 ) 306} 307 308export function ItemIndicator({icon: Icon = CheckIcon}: ItemIndicatorProps) { 309 return ( 310 <RadixSelect.ItemIndicator 311 style={flatten([ 312 a.absolute, 313 {left: 0, width: 30}, 314 a.flex, 315 a.align_center, 316 a.justify_center, 317 ])}> 318 <Icon size="sm" /> 319 </RadixSelect.ItemIndicator> 320 ) 321} 322 323export function Separator() { 324 const t = useTheme() 325 326 return ( 327 <RadixSelect.Separator 328 style={flatten([ 329 { 330 height: 1, 331 backgroundColor: t.atoms.border_contrast_low.borderColor, 332 }, 333 a.my_xs, 334 a.w_full, 335 ])} 336 /> 337 ) 338}