New Select component (#8323)

* radix select component on web

* native implementation (wip)

* fix sheet height/padding

* tone down web styles

* react 19 cleanup

* replace primary language select

* change style on native

* get auto placeholder working

* more style tweaks

* replace app language dropdown

* replace rnpickerselect with native select

* rm react-native-picker-select dependency

* rm placeholder, since a value is always selected

* docblock for renderItem

* add more docblocks

* add style prop to item

* pass selectedValue through renderItem

* fix context

* Style overflow buttons

---------

Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
Samuel Newman
2025-05-06 20:27:05 +03:00
committed by GitHub
parent 25f8506c41
commit 973538d246
18 changed files with 899 additions and 300 deletions
+47 -40
View File
@@ -1,17 +1,19 @@
import React from 'react'
import {View} from 'react-native'
import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
import {APP_LANGUAGES} from '#/locale/languages'
import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
import {resetPostsFeedQueries} from '#/state/queries/post-feed'
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
import {atoms as a, platform, useTheme} from '#/alf'
import * as Select from '#/components/Select'
import {Button} from './Button'
export function AppLanguageDropdown(_props: ViewStyleProp) {
export function AppLanguageDropdown() {
const t = useTheme()
const {_} = useLingui()
const queryClient = useQueryClient()
const langPrefs = useLanguagePrefs()
@@ -19,7 +21,7 @@ export function AppLanguageDropdown(_props: ViewStyleProp) {
const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage)
const onChangeAppLanguage = React.useCallback(
(value: Parameters<PickerSelectProps['onValueChange']>[0]) => {
(value: string) => {
if (!value) return
if (sanitizedLang !== value) {
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
@@ -32,43 +34,48 @@ export function AppLanguageDropdown(_props: ViewStyleProp) {
)
return (
<View style={a.relative}>
<RNPickerSelect
darkTheme={t.scheme === 'dark'}
placeholder={{}}
value={sanitizedLang}
onValueChange={onChangeAppLanguage}
items={APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({
<Select.Root
value={sanitizeAppLanguageSetting(langPrefs.appLanguage)}
onValueChange={onChangeAppLanguage}>
<Select.Trigger label={_(msg`Change app language`)}>
{({props}) => (
<Button
{...props}
label={props.accessibilityLabel}
size={platform({
web: 'tiny',
native: 'small',
})}
variant="ghost"
color="secondary"
style={[
a.pr_xs,
a.pl_sm,
platform({
web: [{alignSelf: 'flex-start'}, a.gap_sm],
native: [a.gap_xs],
}),
]}>
<Select.ValueText
placeholder={_(msg`Select an app language`)}
style={[t.atoms.text_contrast_medium]}
/>
<Select.Icon style={[t.atoms.text_contrast_medium]} />
</Button>
)}
</Select.Trigger>
<Select.Content
renderItem={({label, value}) => (
<Select.Item value={value} label={label}>
<Select.ItemIndicator />
<Select.ItemText>{label}</Select.ItemText>
</Select.Item>
)}
items={APP_LANGUAGES.map(l => ({
label: l.name,
value: l.code2,
key: l.code2,
}))}
useNativeAndroidPickerStyle={false}
style={{
inputAndroid: {
color: t.atoms.text_contrast_medium.color,
fontSize: 16,
paddingRight: 12 + 4,
},
inputIOS: {
color: t.atoms.text.color,
fontSize: 16,
paddingRight: 12 + 4,
},
}}
/>
<View
style={[
a.absolute,
a.inset_0,
{left: 'auto'},
{pointerEvents: 'none'},
a.align_center,
a.justify_center,
]}>
<ChevronDown fill={t.atoms.text.color} size="xs" />
</View>
</View>
</Select.Root>
)
}
@@ -1,83 +0,0 @@
import React from 'react'
import {View} from 'react-native'
import {useQueryClient} from '@tanstack/react-query'
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
import {APP_LANGUAGES} from '#/locale/languages'
import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
import {resetPostsFeedQueries} from '#/state/queries/post-feed'
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
import {Text} from '#/components/Typography'
export function AppLanguageDropdown({style}: ViewStyleProp) {
const t = useTheme()
const queryClient = useQueryClient()
const langPrefs = useLanguagePrefs()
const setLangPrefs = useLanguagePrefsApi()
const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage)
const onChangeAppLanguage = React.useCallback(
(ev: React.ChangeEvent<HTMLSelectElement>) => {
const value = ev.target.value
if (!value) return
if (sanitizedLang !== value) {
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
}
// reset feeds to refetch content
resetPostsFeedQueries(queryClient)
},
[sanitizedLang, setLangPrefs, queryClient],
)
return (
<View
style={[
// We don't have hitSlop here to increase the tap region,
// alternative is negative margins.
{height: 32, marginVertical: -((32 - 14) / 2)},
style,
]}>
<View
style={[
a.flex_row,
a.gap_sm,
a.align_center,
a.flex_shrink,
a.h_full,
t.atoms.bg,
]}>
<Text aria-hidden={true} style={t.atoms.text_contrast_medium}>
{APP_LANGUAGES.find(l => l.code2 === sanitizedLang)?.name}
</Text>
<ChevronDown fill={t.atoms.text.color} size="xs" style={a.flex_0} />
</View>
<select
value={sanitizedLang}
onChange={onChangeAppLanguage}
style={{
fontSize: a.text_sm.fontSize,
letterSpacing: a.text_sm.letterSpacing,
cursor: 'pointer',
position: 'absolute',
inset: 0,
opacity: 0,
color: t.atoms.text.color,
background: t.atoms.bg.backgroundColor,
padding: 4,
maxWidth: '100%',
}}>
{APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => (
<option key={l.code2} value={l.code2}>
{l.name}
</option>
))}
</select>
</View>
)
}
+10 -1
View File
@@ -1,5 +1,11 @@
import React from 'react'
import {StyleProp, TextStyle, View, ViewStyle} from 'react-native'
import {
LayoutChangeEvent,
StyleProp,
TextStyle,
View,
ViewStyle,
} from 'react-native'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
@@ -9,15 +15,18 @@ export function Header({
renderRight,
children,
style,
onLayout,
}: {
renderLeft?: () => React.ReactNode
renderRight?: () => React.ReactNode
children?: React.ReactNode
style?: StyleProp<ViewStyle>
onLayout?: (event: LayoutChangeEvent) => void
}) {
const t = useTheme()
return (
<View
onLayout={onLayout}
style={[
a.relative,
a.w_full,
+289
View File
@@ -0,0 +1,289 @@
import {
createContext,
useCallback,
useContext,
useLayoutEffect,
useMemo,
useState,
} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
import {ChevronTopBottom_Stroke2_Corner0_Rounded as ChevronUpDownIcon} from '#/components/icons/Chevron'
import {Text} from '#/components/Typography'
import {
type ContentProps,
type IconProps,
type ItemIndicatorProps,
type ItemProps,
type ItemTextProps,
type RootProps,
type TriggerProps,
type ValueProps,
} from './types'
type ContextType = {
control: Dialog.DialogControlProps
} & Pick<RootProps, 'value' | 'onValueChange' | 'disabled'>
const Context = createContext<ContextType | null>(null)
const ValueTextContext = createContext<
[any, React.Dispatch<React.SetStateAction<any>>]
>([undefined, () => {}])
function useSelectContext() {
const ctx = useContext(Context)
if (!ctx) {
throw new Error('Select components must must be used within a Select.Root')
}
return ctx
}
export function Root({children, value, onValueChange, disabled}: RootProps) {
const control = Dialog.useDialogControl()
const valueTextCtx = useState<any>()
const ctx = useMemo(
() => ({
control,
value,
onValueChange,
disabled,
}),
[control, value, onValueChange, disabled],
)
return (
<Context.Provider value={ctx}>
<ValueTextContext.Provider value={valueTextCtx}>
{children}
</ValueTextContext.Provider>
</Context.Provider>
)
}
export function Trigger({children, label}: TriggerProps) {
const {control} = useSelectContext()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const {
state: pressed,
onIn: onPressIn,
onOut: onPressOut,
} = useInteractionState()
if (typeof children === 'function') {
return children({
isNative: true,
control,
state: {
hovered: false,
focused,
pressed,
},
props: {
onPress: control.open,
onFocus,
onBlur,
onPressIn,
onPressOut,
accessibilityLabel: label,
},
})
} else {
return (
<Button
label={label}
onPress={control.open}
style={[a.flex_1, a.justify_between]}
color="secondary"
size="small"
variant="solid">
<>{children}</>
</Button>
)
}
}
export function ValueText({
placeholder,
children = value => value.label,
style,
}: ValueProps) {
const [value] = useContext(ValueTextContext)
const t = useTheme()
let text = value && children(value)
if (typeof text !== 'string') text = placeholder
return (
<ButtonText style={[t.atoms.text, a.font_normal, style]}>{text}</ButtonText>
)
}
export function Icon({}: IconProps) {
return <ButtonIcon icon={ChevronUpDownIcon} />
}
export function Content<T>({
items,
valueExtractor = defaultItemValueExtractor,
...props
}: ContentProps<T>) {
const {control, ...context} = useSelectContext()
const [, setValue] = useContext(ValueTextContext)
useLayoutEffect(() => {
const item = items.find(item => valueExtractor(item) === context.value)
if (item) {
setValue(item)
}
}, [items, context.value, valueExtractor, setValue])
return (
<Dialog.Outer control={control}>
<ContentInner
control={control}
items={items}
valueExtractor={valueExtractor}
{...props}
{...context}
/>
</Dialog.Outer>
)
}
function ContentInner<T>({
items,
renderItem,
valueExtractor,
...context
}: ContentProps<T> & ContextType) {
const control = Dialog.useDialogContext()
const {_} = useLingui()
const [headerHeight, setHeaderHeight] = useState(50)
const render = useCallback(
({item, index}: {item: T; index: number}) => {
return renderItem(item, index, context.value)
},
[renderItem, context.value],
)
const doneButton = useCallback(
() => (
<Button
label={_(msg`Done`)}
onPress={() => control.close()}
size="small"
color="primary"
variant="ghost"
style={[a.rounded_full]}>
<ButtonText style={[a.text_md]}>
<Trans>Done</Trans>
</ButtonText>
</Button>
),
[control, _],
)
return (
<Context.Provider value={context}>
<Dialog.Header
renderRight={doneButton}
onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}
style={[a.absolute, a.top_0, a.left_0, a.right_0, a.z_10]}>
<Dialog.HeaderText>
<Trans>Select an option</Trans>
</Dialog.HeaderText>
</Dialog.Header>
<Dialog.InnerFlatList
headerOffset={headerHeight}
data={items}
renderItem={render}
keyExtractor={valueExtractor}
/>
</Context.Provider>
)
}
function defaultItemValueExtractor(item: any) {
return item.value
}
const ItemContext = createContext<{
selected: boolean
hovered: boolean
focused: boolean
pressed: boolean
}>({
selected: false,
hovered: false,
focused: false,
pressed: false,
})
export function useItemContext() {
return useContext(ItemContext)
}
export function Item({children, value, label, style}: ItemProps) {
const t = useTheme()
const control = Dialog.useDialogContext()
const {value: selected, onValueChange} = useSelectContext()
return (
<Button
role="listitem"
label={label}
style={[a.flex_1]}
onPress={() => {
control.close(() => {
onValueChange?.(value)
})
}}>
{({hovered, focused, pressed}) => (
<ItemContext.Provider
value={{selected: value === selected, hovered, focused, pressed}}>
<View
style={[
a.flex_1,
a.pl_md,
(focused || pressed) && t.atoms.bg_contrast_25,
a.flex_row,
a.align_center,
a.gap_sm,
style,
]}>
{children}
</View>
</ItemContext.Provider>
)}
</Button>
)
}
export function ItemText({children}: ItemTextProps) {
const {selected} = useItemContext()
const t = useTheme()
// eslint-disable-next-line bsky-internal/avoid-unwrapped-text
return (
<View style={[a.flex_1, a.py_md, a.border_b, t.atoms.border_contrast_low]}>
<Text style={[a.text_md, selected && a.font_bold]}>{children}</Text>
</View>
)
}
export function ItemIndicator({icon: Icon = CheckIcon}: ItemIndicatorProps) {
const {selected} = useItemContext()
return <View style={{width: 24}}>{selected && <Icon size="md" />}</View>
}
+280
View File
@@ -0,0 +1,280 @@
import {createContext, forwardRef, useContext, useMemo} from 'react'
import {View} from 'react-native'
import {Select as RadixSelect} from 'radix-ui'
import {flatten, useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
import {
ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon,
ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon,
} from '#/components/icons/Chevron'
import {Text} from '#/components/Typography'
import {
type ContentProps,
type IconProps,
type ItemIndicatorProps,
type ItemProps,
type RadixPassThroughTriggerProps,
type RootProps,
type TriggerProps,
type ValueProps,
} from './types'
const SelectedValueContext = createContext<string | undefined | null>(null)
export function Root(props: RootProps) {
return (
<SelectedValueContext.Provider value={props.value}>
<RadixSelect.Root {...props} />
</SelectedValueContext.Provider>
)
}
const RadixTriggerPassThrough = forwardRef(
(
props: {
children: (
props: RadixPassThroughTriggerProps & {
ref: React.Ref<any>
},
) => React.ReactNode
},
ref,
) => {
// @ts-expect-error Radix provides no types of this stuff
return props.children?.({...props, ref})
},
)
RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'
export function Trigger({children, label}: TriggerProps) {
const t = useTheme()
const {
state: hovered,
onIn: onMouseEnter,
onOut: onMouseLeave,
} = useInteractionState()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
if (typeof children === 'function') {
return (
<RadixSelect.Trigger asChild>
<RadixTriggerPassThrough>
{props =>
children({
isNative: false,
state: {
hovered,
focused,
pressed: false,
},
props: {
...props,
onFocus: onFocus,
onBlur: onBlur,
onMouseEnter,
onMouseLeave,
accessibilityLabel: label,
},
})
}
</RadixTriggerPassThrough>
</RadixSelect.Trigger>
)
} else {
return (
<RadixSelect.Trigger
onFocus={onFocus}
onBlur={onBlur}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
style={flatten([
a.flex,
a.relative,
t.atoms.bg_contrast_25,
a.rounded_sm,
a.w_full,
{maxWidth: 400},
a.align_center,
a.gap_sm,
a.justify_between,
a.py_sm,
a.px_md,
{
outline: 0,
borderWidth: 2,
borderStyle: 'solid',
borderColor: focused
? t.palette.primary_500
: hovered
? t.palette.contrast_100
: t.palette.contrast_25,
},
])}>
{children}
</RadixSelect.Trigger>
)
}
}
export function ValueText({children: _, style, ...props}: ValueProps) {
return (
<Text style={style}>
<RadixSelect.Value {...props} />
</Text>
)
}
export function Icon({style}: IconProps) {
const t = useTheme()
return (
<RadixSelect.Icon>
<ChevronDownIcon style={[t.atoms.text, style]} size="xs" />
</RadixSelect.Icon>
)
}
export function Content<T>({items, renderItem}: ContentProps<T>) {
const t = useTheme()
const selectedValue = useContext(SelectedValueContext)
const scrollBtnStyles: React.CSSProperties[] = [
a.absolute,
a.flex,
a.align_center,
a.justify_center,
a.rounded_sm,
a.z_10,
]
const up: React.CSSProperties[] = [
...scrollBtnStyles,
a.pt_sm,
a.pb_lg,
{
top: 0,
left: 0,
right: 0,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
background: `linear-gradient(to bottom, ${t.atoms.bg.backgroundColor} 0%, transparent 100%)`,
},
]
const down: React.CSSProperties[] = [
...scrollBtnStyles,
a.pt_lg,
a.pb_sm,
{
bottom: 0,
left: 0,
right: 0,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
background: `linear-gradient(to top, ${t.atoms.bg.backgroundColor} 0%, transparent 100%)`,
},
]
return (
<RadixSelect.Portal>
<RadixSelect.Content
style={flatten([t.atoms.bg, a.rounded_sm, a.overflow_hidden])}
position="popper"
sideOffset={5}
className="radix-select-content">
<View
style={[
a.flex_1,
a.border,
t.atoms.border_contrast_low,
a.rounded_sm,
]}>
<RadixSelect.ScrollUpButton style={flatten(up)}>
<ChevronUpIcon style={[t.atoms.text]} size="xs" />
</RadixSelect.ScrollUpButton>
<RadixSelect.Viewport style={flatten([a.p_xs])}>
{items.map((item, index) => renderItem(item, index, selectedValue))}
</RadixSelect.Viewport>
<RadixSelect.ScrollDownButton style={flatten(down)}>
<ChevronDownIcon style={[t.atoms.text]} size="xs" />
</RadixSelect.ScrollDownButton>
</View>
</RadixSelect.Content>
</RadixSelect.Portal>
)
}
const ItemContext = createContext<{
hovered: boolean
focused: boolean
pressed: boolean
selected: boolean
}>({
hovered: false,
focused: false,
pressed: false,
selected: false,
})
export function useItemContext() {
return useContext(ItemContext)
}
export function Item({ref, value, style, children}: ItemProps) {
const t = useTheme()
const {
state: hovered,
onIn: onMouseEnter,
onOut: onMouseLeave,
} = useInteractionState()
const selected = useContext(SelectedValueContext) === value
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const ctx = useMemo(
() => ({hovered, focused, pressed: false, selected}),
[hovered, focused, selected],
)
return (
<RadixSelect.Item
ref={ref}
value={value}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onFocus={onFocus}
onBlur={onBlur}
style={flatten([
a.relative,
a.flex,
{minHeight: 25, paddingLeft: 30, paddingRight: 35},
a.user_select_none,
a.align_center,
a.rounded_xs,
a.py_2xs,
a.text_sm,
{outline: 0},
(hovered || focused) && {backgroundColor: t.palette.primary_50},
selected && [a.font_bold],
a.transition_color,
style,
])}>
<ItemContext.Provider value={ctx}>{children}</ItemContext.Provider>
</RadixSelect.Item>
)
}
export const ItemText = RadixSelect.ItemText
export function ItemIndicator({icon: Icon = CheckIcon}: ItemIndicatorProps) {
return (
<RadixSelect.ItemIndicator
style={flatten([
a.absolute,
{left: 0, width: 30},
a.flex,
a.align_center,
a.justify_center,
])}>
<Icon size="sm" />
</RadixSelect.ItemIndicator>
)
}
+185
View File
@@ -0,0 +1,185 @@
import {
type AccessibilityProps,
type StyleProp,
type TextStyle,
type ViewStyle,
} from 'react-native'
import {type TextStyleProp} from '#/alf'
import {type DialogControlProps} from '#/components/Dialog'
import {type Props as SVGIconProps} from '#/components/icons/common'
export type RootProps = {
children?: React.ReactNode
value?: string
onValueChange?: (value: string) => void
disabled?: boolean
/**
* @platform web
*/
defaultValue?: string
/**
* @platform web
*/
open?: boolean
/**
* @platform web
*/
defaultOpen?: boolean
/**
* @platform web
*/
onOpenChange?(open: boolean): void
/**
* @platform web
*/
name?: string
/**
* @platform web
*/
autoComplete?: string
/**
* @platform web
*/
required?: boolean
}
export type RadixPassThroughTriggerProps = {
id: string
type: 'button'
disabled: boolean
['data-disabled']: boolean
['data-state']: string
['aria-controls']?: string
['aria-haspopup']?: boolean
['aria-expanded']?: AccessibilityProps['aria-expanded']
onPress: () => void
}
export type TriggerProps = {
children: React.ReactNode | ((props: TriggerChildProps) => React.ReactNode)
label: string
}
export type TriggerChildProps =
| {
isNative: true
control: DialogControlProps
state: {
/**
* Web only, `false` on native
*/
hovered: false
focused: boolean
pressed: boolean
}
/**
* We don't necessarily know what these will be spread on to, so we
* should add props one-by-one.
*
* On web, these properties are applied to a parent `Pressable`, so this
* object is empty.
*/
props: {
onPress: () => void
onFocus: () => void
onBlur: () => void
onPressIn: () => void
onPressOut: () => void
accessibilityLabel: string
}
}
| {
isNative: false
state: {
hovered: boolean
focused: boolean
/**
* Native only, `false` on web
*/
pressed: false
}
props: RadixPassThroughTriggerProps & {
onPress: () => void
onFocus: () => void
onBlur: () => void
onMouseEnter: () => void
onMouseLeave: () => void
accessibilityLabel: string
}
}
/*
* For use within the `Select.Trigger` component.
* Shows the currently selected value. You can also
* provide a placeholder to show when no value is selected.
*
* If you're passing items of a different shape than {value: string, label: string},
* you'll need to pass a function to `children` that extracts the label from an item.
*/
export type ValueProps = {
/**
* Only needed for native. Extracts the label from an item. Defaults to `item => item.label`
*/
children?: (value: any) => string
placeholder?: string
style?: StyleProp<TextStyle>
}
/*
* Icon for use within the `Select.Trigger` component.
* Changes based on platform - chevron down on web, up/down chevrons on native
*
* `style` prop is web only
*/
export type IconProps = TextStyleProp
export type ContentProps<T> = {
/**
* Items to render. Recommended to be in the form {value: string, label: string} - if not,
* you need to provide a `valueExtractor` function to extract the value from an item and
* customise the `Select.ValueText` component.
*/
items: T[]
/**
* Renders an item. You should probably use the `Select.Item` component.
*
* @example
* ```tsx
* renderItem={({label, value}) => (
* <Select.Item value={value} label={label}>
* <Select.ItemIndicator />
* <Select.ItemText>{label}</Select.ItemText>
* </Select.Item>
* )}
* ```
*/
renderItem: (
item: T,
index: number,
selectedValue?: string | null,
) => React.ReactElement
/*
* Extracts the value from an item. Defaults to `item => item.value`
*/
valueExtractor?: (item: T) => string
}
/*
* An item within the select dropdown
*/
export type ItemProps = {
ref?: React.Ref<HTMLDivElement>
value: string
label: string
children: React.ReactNode
style?: StyleProp<ViewStyle>
}
export type ItemTextProps = {
children: React.ReactNode
}
export type ItemIndicatorProps = {
icon?: React.ComponentType<SVGIconProps>
}