diff --git a/src/components/InternationalPhoneCodeSelect.tsx b/src/components/InternationalPhoneCodeSelect.tsx new file mode 100644 index 0000000000..77f3ffe0ae --- /dev/null +++ b/src/components/InternationalPhoneCodeSelect.tsx @@ -0,0 +1,71 @@ +import {Fragment, useMemo} from 'react' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {getCountriesWithTelephoneCodes} from '#/lib/international-telephone-codes' +import {useGeolocationStatus} from '#/state/geolocation' +import {atoms as a} from '#/alf' +import * as Select from '#/components/Select' + +/** + * Country picker for a phone number input + * + * Pro tip: you can use `location?.countryCode` from `useGeolocationStatus()` + * to set a default value. + */ +export function InternationalPhoneCodeSelect({ + value, + onChange, +}: { + value?: string + onChange: (value: string) => void +}) { + const {_, i18n} = useLingui() + const {location} = useGeolocationStatus() + const items = useMemo(() => { + const telCountryMap = getCountriesWithTelephoneCodes(i18n) + + return ( + Object.entries(telCountryMap) + .map(([value, {name, code}]) => ({ + value, + name, + code, + label: `${name} ${code}`, + })) + // boost the default value to the top + .sort((a, b) => + a.value === location.countryCode + ? -1 + : b.value === location.countryCode + ? 1 + : 0, + ) + ) + }, [i18n, location]) + + return ( + + + + + + ( + + + + {item.name} + + {item.code} + + + {item.value === location?.countryCode && } + + )} + /> + + ) +} diff --git a/src/components/Select/index.tsx b/src/components/Select/index.tsx index a723e52370..0794c9f586 100644 --- a/src/components/Select/index.tsx +++ b/src/components/Select/index.tsx @@ -7,7 +7,7 @@ import { useState, } from 'react' import {View} from 'react-native' -import {msg, Trans} from '@lingui/macro' +import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useTheme} from '#/alf' @@ -15,9 +15,9 @@ 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 {BaseRadio} from '../forms/Toggle' import { type ContentProps, type IconProps, @@ -162,15 +162,14 @@ export function Content({ } function ContentInner({ + label, items, renderItem, valueExtractor, ...context }: ContentProps & ContextType) { - const control = Dialog.useDialogContext() - const {_} = useLingui() - const [headerHeight, setHeaderHeight] = useState(50) + const [headerHeight, setHeaderHeight] = useState(61) const render = useCallback( ({item, index}: {item: T; index: number}) => { @@ -179,33 +178,26 @@ function ContentInner({ [renderItem, context.value], ) - const doneButton = useCallback( - () => ( - - ), - [control, _], - ) - return ( setHeaderHeight(evt.nativeEvent.layout.height)} - style={[a.absolute, a.top_0, a.left_0, a.right_0, a.z_10]}> - - Select an option + style={[ + a.absolute, + a.top_0, + a.left_0, + a.right_0, + a.z_10, + a.pt_3xl, + a.pb_sm, + a.border_b_0, + ]}> + + {label ?? _(msg`Select an option`)} + {children} @@ -273,20 +266,46 @@ export function Item({children, value, label, style}: ItemProps) { ) } -export function ItemText({children}: ItemTextProps) { +export function ItemText({children, style}: ItemTextProps) { const {selected} = useItemContext() - const t = useTheme() - // eslint-disable-next-line bsky-internal/avoid-unwrapped-text return ( - - {children} - + + {children} + ) } -export function ItemIndicator({icon: Icon = CheckIcon}: ItemIndicatorProps) { - const {selected} = useItemContext() +export function ItemIndicator({icon: Icon}: ItemIndicatorProps) { + const {selected, focused, hovered} = useItemContext() - return {selected && } + if (Icon) { + return {selected && } + } + + return ( + + ) +} + +export function Separator() { + const t = useTheme() + + return ( + + ) } diff --git a/src/components/Select/index.web.tsx b/src/components/Select/index.web.tsx index 995f9d412e..71d0ae7013 100644 --- a/src/components/Select/index.web.tsx +++ b/src/components/Select/index.web.tsx @@ -1,4 +1,11 @@ -import {createContext, forwardRef, Fragment, useContext, useMemo} from 'react' +import { + createContext, + type CSSProperties, + forwardRef, + Fragment, + useContext, + useMemo, +} from 'react' import {View} from 'react-native' import {Select as RadixSelect} from 'radix-ui' @@ -16,6 +23,7 @@ import { type IconProps, type ItemIndicatorProps, type ItemProps, + type ItemTextProps, type RadixPassThroughTriggerProps, type RootProps, type TriggerProps, @@ -278,7 +286,13 @@ export function Item({ref, value, style, children}: ItemProps) { ) } -export const ItemText = RadixSelect.ItemText +export const ItemText = function ItemText({children, style}: ItemTextProps) { + return ( + + {children} + + ) +} export function ItemIndicator({icon: Icon = CheckIcon}: ItemIndicatorProps) { return ( @@ -294,3 +308,20 @@ export function ItemIndicator({icon: Icon = CheckIcon}: ItemIndicatorProps) { ) } + +export function Separator() { + const t = useTheme() + + return ( + + ) +} diff --git a/src/components/Select/types.ts b/src/components/Select/types.ts index 0e58c197be..33a91bbedb 100644 --- a/src/components/Select/types.ts +++ b/src/components/Select/types.ts @@ -137,6 +137,12 @@ export type ValueProps = { export type IconProps = TextStyleProp export type ContentProps = { + /** + * Label at the top of the sheet on native. + * + * @default "Select an option" + */ + label?: string /** * 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 @@ -180,6 +186,7 @@ export type ItemProps = { export type ItemTextProps = { children: React.ReactNode + style?: StyleProp } export type ItemIndicatorProps = { diff --git a/src/components/forms/Toggle/index.tsx b/src/components/forms/Toggle/index.tsx index 60fa50478a..819008f34f 100644 --- a/src/components/forms/Toggle/index.tsx +++ b/src/components/forms/Toggle/index.tsx @@ -503,9 +503,23 @@ export function Switch() { } export function Radio() { + const props = useContext(ItemContext) + + return +} + +export function BaseRadio({ + hovered, + focused, + selected, + disabled, + isInvalid, +}: Pick< + ItemState, + 'hovered' | 'focused' | 'selected' | 'disabled' | 'isInvalid' +>) { const t = useTheme() - const {selected, hovered, focused, disabled, isInvalid} = - useContext(ItemContext) + const {baseStyles, baseHoverStyles, indicatorStyles} = createSharedToggleStyles({ theme: t, @@ -515,6 +529,7 @@ export function Radio() { disabled, isInvalid, }) + return (