bd17f04810
* Add storybook shortcut in dev * Migrate to alg pkg * Migrate font_bold to new font_semi_bold * Migrate font_heavy to font_bold * Fix old theme refs * Remove leading util * Bump * Revert "Add storybook shortcut in dev" This reverts commit 7a86195c77fb5d449c7782e64ebabb6addfab919. * Remove alf script * Adjust font scale * Bump * Bump pkg * Migrate values from conflicts * Create default themes outside react * Use built-in alf utils * Migrate old font styles * Remove unused tokens * Move theme creation to more appropriate file * Update button colors * Adjust TextField colors, line heights
287 lines
7.3 KiB
TypeScript
287 lines
7.3 KiB
TypeScript
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)
|
|
SelectedValueContext.displayName = 'SelectSelectedValueContext'
|
|
|
|
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,
|
|
onPress: props.onClick,
|
|
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,
|
|
a.align_center,
|
|
a.gap_sm,
|
|
a.justify_between,
|
|
a.py_sm,
|
|
a.px_md,
|
|
a.pointer,
|
|
{
|
|
maxWidth: 400,
|
|
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,
|
|
a.overflow_hidden,
|
|
]}>
|
|
<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,
|
|
})
|
|
ItemContext.displayName = 'SelectItemContext'
|
|
|
|
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([
|
|
t.atoms.text,
|
|
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_semi_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>
|
|
)
|
|
}
|