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 && (
<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.
</Trans>
</Admonition>
<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.
</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>
<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>
)