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
+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>