SegmentedControl component (#8606)

* new segmented control

* fix type error

* convert server input, use CSS for web

* add segmented control to storybook

* use segmented control in embed dialog

* add to suggested text wrappers

* update change handle dialog

* update styles since button changes

* fix atom

* style updates to segmented control, add size prop

* update state in layout effect rather than in render

* set type = 'radio' as default

* prevent expansion in server dialog on iOS

* use non reactive callback in needsUpdate effect
This commit is contained in:
Samuel Newman
2025-11-14 21:43:28 +02:00
committed by GitHub
parent 7a8ab551e3
commit 92926a2417
9 changed files with 445 additions and 139 deletions
+1
View File
@@ -43,6 +43,7 @@ module.exports = {
suggestedTextWrappers: {
Button: 'ButtonText',
'ToggleButton.Button': 'ToggleButton.ButtonText',
'SegmentedControl.Item': 'SegmentedControl.ItemText',
},
},
],
+18 -17
View File
@@ -10,8 +10,8 @@ import {toShareUrl} from '#/lib/strings/url-helpers'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import * as SegmentedControl from '#/components/forms/SegmentedControl'
import * as TextField from '#/components/forms/TextField'
import * as ToggleButton from '#/components/forms/ToggleButton'
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
import {
ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottomIcon,
@@ -150,26 +150,27 @@ function EmbedDialogInner({
<Text style={[t.atoms.text_contrast_medium, a.font_semi_bold]}>
<Trans>Color theme</Trans>
</Text>
<ToggleButton.Group
<SegmentedControl.Root
label={_(msg`Color mode`)}
values={[colorMode]}
onChange={([value]) => setColorMode(value as ColorModeValues)}>
<ToggleButton.Button name="system" label={_(msg`System`)}>
<ToggleButton.ButtonText>
type="radio"
value={colorMode}
onChange={setColorMode}>
<SegmentedControl.Item value="system" label={_(msg`System`)}>
<SegmentedControl.ItemText>
<Trans>System</Trans>
</ToggleButton.ButtonText>
</ToggleButton.Button>
<ToggleButton.Button name="light" label={_(msg`Light`)}>
<ToggleButton.ButtonText>
</SegmentedControl.ItemText>
</SegmentedControl.Item>
<SegmentedControl.Item value="light" label={_(msg`Light`)}>
<SegmentedControl.ItemText>
<Trans>Light</Trans>
</ToggleButton.ButtonText>
</ToggleButton.Button>
<ToggleButton.Button name="dark" label={_(msg`Dark`)}>
<ToggleButton.ButtonText>
</SegmentedControl.ItemText>
</SegmentedControl.Item>
<SegmentedControl.Item value="dark" label={_(msg`Dark`)}>
<SegmentedControl.ItemText>
<Trans>Dark</Trans>
</ToggleButton.ButtonText>
</ToggleButton.Button>
</ToggleButton.Group>
</SegmentedControl.ItemText>
</SegmentedControl.Item>
</SegmentedControl.Root>
</View>
)}
</View>
@@ -5,18 +5,20 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {BSKY_SERVICE} from '#/lib/constants'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import * as persisted from '#/state/persisted'
import {useSession} from '#/state/session'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {atoms as a, platform, useBreakpoints, useTheme, web} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import * as SegmentedControl from '#/components/forms/SegmentedControl'
import * as TextField from '#/components/forms/TextField'
import * as ToggleButton from '#/components/forms/ToggleButton'
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
import {InlineLinkText} from '#/components/Link'
import {P, Text} from '#/components/Typography'
import {Text} from '#/components/Typography'
type SegmentedControlOptions = typeof BSKY_SERVICE | 'custom'
export function ServerInputDialog({
control,
@@ -29,7 +31,8 @@ export function ServerInputDialog({
const formRef = useRef<DialogInnerRef>(null)
// persist these options between dialog open/close
const [fixedOption, setFixedOption] = useState(BSKY_SERVICE)
const [fixedOption, setFixedOption] =
useState<SegmentedControlOptions>(BSKY_SERVICE)
const [previousCustomAddress, setPreviousCustomAddress] = useState('')
const onClose = useCallback(() => {
@@ -40,7 +43,7 @@ export function ServerInputDialog({
setPreviousCustomAddress(result)
}
}
logEvent('signin:hostingProviderPressed', {
logger.metric('signin:hostingProviderPressed', {
hostingProviderDidChange: fixedOption !== BSKY_SERVICE,
})
}, [onSelect, fixedOption])
@@ -49,7 +52,10 @@ export function ServerInputDialog({
<Dialog.Outer
control={control}
onClose={onClose}
nativeOptions={{minHeight: height / 2}}>
nativeOptions={platform({
android: {minHeight: height / 2},
ios: {preventExpansion: true},
})}>
<Dialog.Handle />
<DialogInner
formRef={formRef}
@@ -70,8 +76,8 @@ function DialogInner({
initialCustomAddress,
}: {
formRef: React.Ref<DialogInnerRef>
fixedOption: string
setFixedOption: (opt: string) => void
fixedOption: SegmentedControlOptions
setFixedOption: (opt: SegmentedControlOptions) => void
initialCustomAddress: string
}) {
const control = Dialog.useDialogContext()
@@ -124,45 +130,49 @@ function DialogInner({
return (
<Dialog.ScrollableInner
accessibilityDescribedBy="dialog-description"
accessibilityLabelledBy="dialog-title">
accessibilityLabelledBy="dialog-title"
style={web({maxWidth: 500})}>
<View style={[a.relative, a.gap_md, a.w_full]}>
<Text nativeID="dialog-title" style={[a.text_2xl, a.font_semi_bold]}>
<Text nativeID="dialog-title" style={[a.text_2xl, a.font_bold]}>
<Trans>Choose your account provider</Trans>
</Text>
<ToggleButton.Group
label="Preferences"
values={[fixedOption]}
onChange={values => setFixedOption(values[0])}>
<ToggleButton.Button name={BSKY_SERVICE} label={_(msg`Bluesky`)}>
<ToggleButton.ButtonText>{_(msg`Bluesky`)}</ToggleButton.ButtonText>
</ToggleButton.Button>
<ToggleButton.Button
<SegmentedControl.Root
type="tabs"
label={_(msg`Account provider`)}
value={fixedOption}
onChange={setFixedOption}>
<SegmentedControl.Item
testID="bskyServiceSelectBtn"
value={BSKY_SERVICE}
label={_(msg`Bluesky`)}>
<SegmentedControl.ItemText>
{_(msg`Bluesky`)}
</SegmentedControl.ItemText>
</SegmentedControl.Item>
<SegmentedControl.Item
testID="customSelectBtn"
name="custom"
value="custom"
label={_(msg`Custom`)}>
<ToggleButton.ButtonText>{_(msg`Custom`)}</ToggleButton.ButtonText>
</ToggleButton.Button>
</ToggleButton.Group>
<SegmentedControl.ItemText>
{_(msg`Custom`)}
</SegmentedControl.ItemText>
</SegmentedControl.Item>
</SegmentedControl.Root>
{fixedOption === BSKY_SERVICE && isFirstTimeUser && (
<View role="tabpanel">
<Admonition type="tip">
<Trans>
Bluesky is an open network where you can choose your own provider.
If you're new here, we recommend sticking with the default Bluesky
Social option.
Bluesky is an open network where you can choose your own
provider. If you're new here, we recommend sticking with the
default Bluesky Social option.
</Trans>
</Admonition>
</View>
)}
{fixedOption === 'custom' && (
<View
style={[
a.border,
t.atoms.border_contrast_low,
a.rounded_sm,
a.px_md,
a.py_md,
]}>
<View role="tabpanel">
<TextField.LabelText nativeID="address-input-label">
<Trans>Server address</Trans>
</TextField.LabelText>
@@ -197,13 +207,8 @@ function DialogInner({
)}
<View style={[a.py_xs]}>
<P
style={[
t.atoms.text_contrast_medium,
a.text_sm,
a.leading_snug,
a.flex_1,
]}>
<Text
style={[t.atoms.text_contrast_medium, a.text_sm, a.leading_snug]}>
{isFirstTimeUser ? (
<Trans>
If you're a developer, you can host your own server.
@@ -219,18 +224,23 @@ function DialogInner({
to="https://atproto.com/guides/self-hosting">
<Trans>Learn more.</Trans>
</InlineLinkText>
</P>
</Text>
</View>
<View style={gtMobile && [a.flex_row, a.justify_end]}>
<Button
testID="doneBtn"
variant="outline"
variant="solid"
color="primary"
size="small"
size={platform({
native: 'large',
web: 'small',
})}
onPress={() => control.close()}
label={_(msg`Done`)}>
<ButtonText>{_(msg`Done`)}</ButtonText>
<ButtonText>
<Trans>Done</Trans>
</ButtonText>
</Button>
</View>
</View>
+1 -1
View File
@@ -4,10 +4,10 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {toNiceDomain} from '#/lib/strings/url-helpers'
import {ServerInputDialog} from '#/view/com/auth/server-input'
import {atoms as a, tokens, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {ServerInputDialog} from '#/components/dialogs/ServerInput'
import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe'
import {PencilLine_Stroke2_Corner0_Rounded as PencilIcon} from '#/components/icons/Pencil'
import {Text} from '#/components/Typography'
+284
View File
@@ -0,0 +1,284 @@
import {
createContext,
useCallback,
useContext,
useLayoutEffect,
useMemo,
useState,
} from 'react'
import {type StyleProp, View, type ViewStyle} from 'react-native'
import Animated, {Easing, LinearTransition} from 'react-native-reanimated'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {atoms as a, native, platform, useTheme} from '#/alf'
import {
Button,
type ButtonProps,
ButtonText,
type ButtonTextProps,
} from '../Button'
const InternalContext = createContext<{
type: 'tabs' | 'radio'
size: 'small' | 'large'
selectedValue: string
selectedPosition: {width: number; x: number} | null
onSelectValue: (
value: string,
position: {width: number; x: number} | null,
) => void
updatePosition: (position: {width: number; x: number}) => void
} | null>(null)
/**
* Segmented control component.
*
* @example
* ```tsx
* <SegmentedControl.Root value={value} onChange={setValue}>
* <SegmentedControl.Item value="one">
* <SegmentedControl.ItemText value="one">
* One
* </SegmentedControl.ItemText>
* </SegmentedControl.Item>
* <SegmentedControl.Item value="two">
* <SegmentedControl.ItemText value="two">
* Two
* </SegmentedControl.ItemText>
* </SegmentedControl.Item>
* </SegmentedControl.Root>
* ```
*/
export function Root<T extends string>({
label,
type = 'radio',
size = 'large',
value,
onChange,
children,
style,
accessibilityHint,
}: {
label: string
type: 'tabs' | 'radio'
size?: 'small' | 'large'
value: T
onChange: (value: T) => void
children: React.ReactNode
style?: StyleProp<ViewStyle>
accessibilityHint?: string
}) {
const t = useTheme()
const [selectedPosition, setSelectedPosition] = useState<{
width: number
x: number
} | null>(null)
const contextValue = useMemo(() => {
return {
type,
size,
selectedValue: value,
selectedPosition,
onSelectValue: (
val: string,
position: {width: number; x: number} | null,
) => {
onChange(val as T)
if (position) setSelectedPosition(position)
},
updatePosition: (position: {width: number; x: number}) => {
setSelectedPosition(currPos => {
if (
currPos &&
currPos.width === position.width &&
currPos.x === position.x
) {
return currPos
}
return position
})
},
}
}, [value, selectedPosition, setSelectedPosition, onChange, type, size])
return (
<View
accessibilityLabel={label}
accessibilityHint={accessibilityHint ?? ''}
style={[
a.w_full,
a.flex_1,
a.relative,
a.flex_row,
t.atoms.bg_contrast_50,
{borderRadius: 14},
a.curve_continuous,
a.p_xs,
style,
]}
role={type === 'tabs' ? 'tablist' : 'radiogroup'}>
{selectedPosition !== null && (
<Slider x={selectedPosition.x} width={selectedPosition.width} />
)}
<InternalContext.Provider value={contextValue}>
{children}
</InternalContext.Provider>
</View>
)
}
const InternalItemContext = createContext<{
active: boolean
pressed: boolean
hovered: boolean
focused: boolean
} | null>(null)
export function Item({
value,
style,
children,
onPress: onPressProp,
...props
}: {value: string; children: React.ReactNode} & Omit<ButtonProps, 'children'>) {
const [position, setPosition] = useState<{x: number; width: number} | null>(
null,
)
const ctx = useContext(InternalContext)
if (!ctx)
throw new Error(
'SegmentedControl.Item must be used within a SegmentedControl.Root',
)
const active = ctx.selectedValue === value
// update position if change was external, and not due to onPress
const needsUpdate =
active &&
position &&
(ctx.selectedPosition?.x !== position.x ||
ctx.selectedPosition?.width !== position.width)
// can't wait for `useEffectEvent`
const update = useNonReactiveCallback(() => {
if (position) ctx.updatePosition(position)
})
useLayoutEffect(() => {
if (needsUpdate) {
update()
}
}, [needsUpdate, update])
const onPress = useCallback(
(evt: any) => {
ctx.onSelectValue(value, position)
onPressProp?.(evt)
},
[ctx, value, position, onPressProp],
)
return (
<View
style={[a.flex_1, a.flex_row]}
onLayout={evt => {
const measuredPosition = {
x: evt.nativeEvent.layout.x,
width: evt.nativeEvent.layout.width,
}
if (!ctx.selectedPosition && active) {
ctx.onSelectValue(value, measuredPosition)
}
setPosition(measuredPosition)
}}>
<Button
{...props}
onPress={onPress}
role={ctx.type === 'tabs' ? 'tab' : 'radio'}
accessibilityState={{selected: active}}
style={[
a.flex_1,
a.bg_transparent,
a.px_sm,
a.py_xs,
{minHeight: ctx.size === 'large' ? 40 : 32},
style,
]}>
{({pressed, hovered, focused}) => (
<InternalItemContext.Provider
value={{active, pressed, hovered, focused}}>
{children}
</InternalItemContext.Provider>
)}
</Button>
</View>
)
}
export function ItemText({style, ...props}: ButtonTextProps) {
const t = useTheme()
const ctx = useContext(InternalItemContext)
if (!ctx)
throw new Error(
'SegmentedControl.ItemText must be used within a SegmentedControl.Item',
)
return (
<ButtonText
{...props}
style={[
a.text_center,
a.text_md,
a.font_medium,
a.px_xs,
ctx.active
? t.atoms.text
: ctx.focused || ctx.hovered || ctx.pressed
? t.atoms.text_contrast_medium
: t.atoms.text_contrast_low,
style,
]}
/>
)
}
function Slider({x, width}: {x: number; width: number}) {
const t = useTheme()
return (
<Animated.View
layout={native(LinearTransition.easing(Easing.out(Easing.exp)))}
style={[
a.absolute,
a.curve_continuous,
t.atoms.bg,
{
top: 4,
bottom: 4,
left: 0,
width,
borderRadius: 10,
},
// TODO: new arch supports boxShadow on native
// in the meantime this is an attempt to get close
platform({
web: {
boxShadow: '0px 2px 4px 0px #0000000D',
},
ios: {
shadowColor: '#000',
shadowOffset: {width: 0, height: 2},
shadowOpacity: 0x0d / 0xff,
shadowRadius: 4,
},
android: {elevation: 0.25},
}),
platform({
native: [{left: x}],
web: [{transform: [{translateX: x}]}, a.transition_transform],
}),
]}
/>
)
}
+12 -3
View File
@@ -1,4 +1,4 @@
import React from 'react'
import {useMemo} from 'react'
import {
type AccessibilityProps,
type TextStyle,
@@ -20,6 +20,9 @@ export type GroupProps = Omit<Toggle.GroupProps, 'style' | 'type'> & {
multiple?: boolean
}
/**
* @deprecated - use SegmentedControl
*/
export function Group({children, multiple, ...props}: GroupProps) {
const t = useTheme()
return (
@@ -39,6 +42,9 @@ export function Group({children, multiple, ...props}: GroupProps) {
)
}
/**
* @deprecated - use SegmentedControl
*/
export function Button({children, ...props}: ItemProps) {
return (
<Toggle.Item {...props} style={[a.flex_grow, a.flex_1]}>
@@ -51,7 +57,7 @@ function ButtonInner({children}: React.PropsWithChildren<{}>) {
const t = useTheme()
const state = Toggle.useItemContext()
const {baseStyles, hoverStyles, activeStyles} = React.useMemo(() => {
const {baseStyles, hoverStyles, activeStyles} = useMemo(() => {
const base: ViewStyle[] = []
const hover: ViewStyle[] = []
const active: ViewStyle[] = []
@@ -112,11 +118,14 @@ function ButtonInner({children}: React.PropsWithChildren<{}>) {
)
}
/**
* @deprecated - use SegmentedControl
*/
export function ButtonText({children}: {children: React.ReactNode}) {
const t = useTheme()
const state = Toggle.useItemContext()
const textStyles = React.useMemo(() => {
const textStyles = useMemo(() => {
const text: TextStyle[] = []
if (state.selected) {
text.push(t.atoms.text_inverted)
+33 -40
View File
@@ -15,8 +15,8 @@ import {
import {isNative} from '#/platform/detection'
import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem'
import {atoms as a, native, useAlf, useTheme} from '#/alf'
import * as ToggleButton from '#/components/forms/ToggleButton'
import {type Alf, atoms as a, native, useAlf, useTheme} from '#/alf'
import * as SegmentedControl from '#/components/forms/SegmentedControl'
import {type Props as SVGIconProps} from '#/components/icons/common'
import {Moon_Stroke2_Corner0_Rounded as MoonIcon} from '#/components/icons/Moon'
import {Phone_Stroke2_Corner0_Rounded as PhoneIcon} from '#/components/icons/Phone'
@@ -36,42 +36,29 @@ export function AppearanceSettingsScreen({}: Props) {
const {setColorMode, setDarkTheme} = useSetThemePrefs()
const onChangeAppearance = useCallback(
(keys: string[]) => {
const appearance = keys.find(key => key !== colorMode) as
| 'system'
| 'light'
| 'dark'
| undefined
if (!appearance) return
setColorMode(appearance)
(value: 'light' | 'system' | 'dark') => {
setColorMode(value)
},
[setColorMode, colorMode],
[setColorMode],
)
const onChangeDarkTheme = useCallback(
(keys: string[]) => {
const theme = keys.find(key => key !== darkTheme) as
| 'dim'
| 'dark'
| undefined
if (!theme) return
setDarkTheme(theme)
(value: 'dim' | 'dark') => {
setDarkTheme(value)
},
[setDarkTheme, darkTheme],
[setDarkTheme],
)
const onChangeFontFamily = useCallback(
(values: string[]) => {
const next = values[0] === 'system' ? 'system' : 'theme'
fonts.setFontFamily(next)
(value: 'system' | 'theme') => {
fonts.setFontFamily(value)
},
[fonts],
)
const onChangeFontScale = useCallback(
(values: string[]) => {
const next = values[0] || ('0' as any)
fonts.setFontScale(next)
(value: Alf['fonts']['scale']) => {
fonts.setFontScale(value)
},
[fonts],
)
@@ -107,7 +94,7 @@ export function AppearanceSettingsScreen({}: Props) {
name: 'dark',
},
]}
values={[colorMode]}
value={colorMode}
onChange={onChangeAppearance}
/>
@@ -128,7 +115,7 @@ export function AppearanceSettingsScreen({}: Props) {
name: 'dark',
},
]}
values={[darkTheme ?? 'dim']}
value={darkTheme ?? 'dim'}
onChange={onChangeDarkTheme}
/>
</Animated.View>
@@ -153,7 +140,7 @@ export function AppearanceSettingsScreen({}: Props) {
name: 'theme',
},
]}
values={[fonts.family]}
value={fonts.family}
onChange={onChangeFontFamily}
/>
@@ -174,7 +161,7 @@ export function AppearanceSettingsScreen({}: Props) {
name: '1',
},
]}
values={[fonts.scale]}
value={fonts.scale}
onChange={onChangeFontScale}
/>
@@ -192,12 +179,12 @@ export function AppearanceSettingsScreen({}: Props) {
)
}
export function AppearanceToggleButtonGroup({
export function AppearanceToggleButtonGroup<T extends string>({
title,
description,
icon: Icon,
items,
values,
value,
onChange,
}: {
title: string
@@ -205,10 +192,10 @@ export function AppearanceToggleButtonGroup({
icon: React.ComponentType<SVGIconProps>
items: {
label: string
name: string
name: T
}[]
values: string[]
onChange: (values: string[]) => void
value: T
onChange: (value: T) => void
}) {
const t = useTheme()
return (
@@ -227,16 +214,22 @@ export function AppearanceToggleButtonGroup({
{description}
</Text>
)}
<ToggleButton.Group label={title} values={values} onChange={onChange}>
<SegmentedControl.Root
type="radio"
label={title}
value={value}
onChange={onChange}>
{items.map(item => (
<ToggleButton.Button
<SegmentedControl.Item
key={item.name}
label={item.label}
name={item.name}>
<ToggleButton.ButtonText>{item.label}</ToggleButton.ButtonText>
</ToggleButton.Button>
value={item.name}>
<SegmentedControl.ItemText>
{item.label}
</SegmentedControl.ItemText>
</SegmentedControl.Item>
))}
</ToggleButton.Group>
</SegmentedControl.Root>
</SettingsList.Group>
</>
)
@@ -29,8 +29,8 @@ import {atoms as a, native, useBreakpoints, useTheme} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import * as SegmentedControl from '#/components/forms/SegmentedControl'
import * as TextField from '#/components/forms/TextField'
import * as ToggleButton from '#/components/forms/ToggleButton'
import {
ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon,
ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon,
@@ -395,21 +395,22 @@ function OwnHandlePage({goToServiceHandle}: {goToServiceHandle: () => void}) {
/>
</TextField.Root>
</View>
<ToggleButton.Group
<SegmentedControl.Root
label={_(msg`Choose domain verification method`)}
values={[dnsPanel ? 'dns' : 'file']}
onChange={values => setDNSPanel(values[0] === 'dns')}>
<ToggleButton.Button name="dns" label={_(msg`DNS Panel`)}>
<ToggleButton.ButtonText>
type="tabs"
value={dnsPanel ? 'dns' : 'file'}
onChange={values => setDNSPanel(values === 'dns')}>
<SegmentedControl.Item value="dns" label={_(msg`DNS Panel`)}>
<SegmentedControl.ItemText>
<Trans>DNS Panel</Trans>
</ToggleButton.ButtonText>
</ToggleButton.Button>
<ToggleButton.Button name="file" label={_(msg`No DNS Panel`)}>
<ToggleButton.ButtonText>
</SegmentedControl.ItemText>
</SegmentedControl.Item>
<SegmentedControl.Item value="file" label={_(msg`No DNS Panel`)}>
<SegmentedControl.ItemText>
<Trans>No DNS Panel</Trans>
</ToggleButton.ButtonText>
</ToggleButton.Button>
</ToggleButton.Group>
</SegmentedControl.ItemText>
</SegmentedControl.Item>
</SegmentedControl.Root>
{dnsPanel ? (
<>
<Text>
+23 -16
View File
@@ -4,6 +4,7 @@ import {type TextInput, View} from 'react-native'
import {atoms as a} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {DateField, LabelText} from '#/components/forms/DateField'
import * as SegmentedControl from '#/components/forms/SegmentedControl'
import * as TextField from '#/components/forms/TextField'
import * as Toggle from '#/components/forms/Toggle'
import * as ToggleButton from '#/components/forms/ToggleButton'
@@ -15,6 +16,9 @@ export function Forms() {
const [toggleGroupBValues, setToggleGroupBValues] = React.useState(['a', 'b'])
const [toggleGroupCValues, setToggleGroupCValues] = React.useState(['a', 'b'])
const [toggleGroupDValues, setToggleGroupDValues] = React.useState(['warn'])
const [segmentedControlValue, setSegmentedControlValue] = React.useState<
'hide' | 'warn' | 'show'
>('warn')
const [value, setValue] = React.useState('')
const [date, setDate] = React.useState('2001-01-01')
@@ -254,23 +258,26 @@ export function Forms() {
<ToggleButton.ButtonText>Show</ToggleButton.ButtonText>
</ToggleButton.Button>
</ToggleButton.Group>
<View>
<ToggleButton.Group
label="Preferences"
values={toggleGroupDValues}
onChange={setToggleGroupDValues}>
<ToggleButton.Button name="hide" label="Hide">
<ToggleButton.ButtonText>Hide</ToggleButton.ButtonText>
</ToggleButton.Button>
<ToggleButton.Button name="warn" label="Warn">
<ToggleButton.ButtonText>Warn</ToggleButton.ButtonText>
</ToggleButton.Button>
<ToggleButton.Button name="show" label="Show">
<ToggleButton.ButtonText>Show</ToggleButton.ButtonText>
</ToggleButton.Button>
</ToggleButton.Group>
</View>
<View style={[a.gap_md, a.align_start, a.w_full]}>
<H3>SegmentedControl</H3>
<SegmentedControl.Root
label="Preferences"
type="radio"
value={segmentedControlValue}
onChange={setSegmentedControlValue}>
<SegmentedControl.Item value="hide" label="Hide">
<SegmentedControl.ItemText>Hide</SegmentedControl.ItemText>
</SegmentedControl.Item>
<SegmentedControl.Item value="warn" label="Warn">
<SegmentedControl.ItemText>Warn</SegmentedControl.ItemText>
</SegmentedControl.Item>
<SegmentedControl.Item value="show" label="Show">
<SegmentedControl.ItemText>Show</SegmentedControl.ItemText>
</SegmentedControl.Item>
</SegmentedControl.Root>
</View>
</View>
)