Bluesky app fork with some witchin' additions 💫

✨ `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

authored by samuel.fm and committed by GitHub 92926a24 7a8ab551

Changed files
+445 -139
src
+1
.eslintrc.js
··· 43 43 suggestedTextWrappers: { 44 44 Button: 'ButtonText', 45 45 'ToggleButton.Button': 'ToggleButton.ButtonText', 46 + 'SegmentedControl.Item': 'SegmentedControl.ItemText', 46 47 }, 47 48 }, 48 49 ],
+18 -17
src/components/dialogs/Embed.tsx
··· 10 10 import {atoms as a, useTheme} from '#/alf' 11 11 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 12 12 import * as Dialog from '#/components/Dialog' 13 + import * as SegmentedControl from '#/components/forms/SegmentedControl' 13 14 import * as TextField from '#/components/forms/TextField' 14 - import * as ToggleButton from '#/components/forms/ToggleButton' 15 15 import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' 16 16 import { 17 17 ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottomIcon, ··· 150 150 <Text style={[t.atoms.text_contrast_medium, a.font_semi_bold]}> 151 151 <Trans>Color theme</Trans> 152 152 </Text> 153 - <ToggleButton.Group 153 + <SegmentedControl.Root 154 154 label={_(msg`Color mode`)} 155 - values={[colorMode]} 156 - onChange={([value]) => setColorMode(value as ColorModeValues)}> 157 - <ToggleButton.Button name="system" label={_(msg`System`)}> 158 - <ToggleButton.ButtonText> 155 + type="radio" 156 + value={colorMode} 157 + onChange={setColorMode}> 158 + <SegmentedControl.Item value="system" label={_(msg`System`)}> 159 + <SegmentedControl.ItemText> 159 160 <Trans>System</Trans> 160 - </ToggleButton.ButtonText> 161 - </ToggleButton.Button> 162 - <ToggleButton.Button name="light" label={_(msg`Light`)}> 163 - <ToggleButton.ButtonText> 161 + </SegmentedControl.ItemText> 162 + </SegmentedControl.Item> 163 + <SegmentedControl.Item value="light" label={_(msg`Light`)}> 164 + <SegmentedControl.ItemText> 164 165 <Trans>Light</Trans> 165 - </ToggleButton.ButtonText> 166 - </ToggleButton.Button> 167 - <ToggleButton.Button name="dark" label={_(msg`Dark`)}> 168 - <ToggleButton.ButtonText> 166 + </SegmentedControl.ItemText> 167 + </SegmentedControl.Item> 168 + <SegmentedControl.Item value="dark" label={_(msg`Dark`)}> 169 + <SegmentedControl.ItemText> 169 170 <Trans>Dark</Trans> 170 - </ToggleButton.ButtonText> 171 - </ToggleButton.Button> 172 - </ToggleButton.Group> 171 + </SegmentedControl.ItemText> 172 + </SegmentedControl.Item> 173 + </SegmentedControl.Root> 173 174 </View> 174 175 )} 175 176 </View>
+1 -1
src/components/forms/HostingProvider.tsx
··· 4 4 import {useLingui} from '@lingui/react' 5 5 6 6 import {toNiceDomain} from '#/lib/strings/url-helpers' 7 - import {ServerInputDialog} from '#/view/com/auth/server-input' 8 7 import {atoms as a, tokens, useTheme} from '#/alf' 9 8 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 10 9 import {useDialogControl} from '#/components/Dialog' 10 + import {ServerInputDialog} from '#/components/dialogs/ServerInput' 11 11 import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe' 12 12 import {PencilLine_Stroke2_Corner0_Rounded as PencilIcon} from '#/components/icons/Pencil' 13 13 import {Text} from '#/components/Typography'
+284
src/components/forms/SegmentedControl.tsx
··· 1 + import { 2 + createContext, 3 + useCallback, 4 + useContext, 5 + useLayoutEffect, 6 + useMemo, 7 + useState, 8 + } from 'react' 9 + import {type StyleProp, View, type ViewStyle} from 'react-native' 10 + import Animated, {Easing, LinearTransition} from 'react-native-reanimated' 11 + 12 + import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 13 + import {atoms as a, native, platform, useTheme} from '#/alf' 14 + import { 15 + Button, 16 + type ButtonProps, 17 + ButtonText, 18 + type ButtonTextProps, 19 + } from '../Button' 20 + 21 + const InternalContext = createContext<{ 22 + type: 'tabs' | 'radio' 23 + size: 'small' | 'large' 24 + selectedValue: string 25 + selectedPosition: {width: number; x: number} | null 26 + onSelectValue: ( 27 + value: string, 28 + position: {width: number; x: number} | null, 29 + ) => void 30 + updatePosition: (position: {width: number; x: number}) => void 31 + } | null>(null) 32 + 33 + /** 34 + * Segmented control component. 35 + * 36 + * @example 37 + * ```tsx 38 + * <SegmentedControl.Root value={value} onChange={setValue}> 39 + * <SegmentedControl.Item value="one"> 40 + * <SegmentedControl.ItemText value="one"> 41 + * One 42 + * </SegmentedControl.ItemText> 43 + * </SegmentedControl.Item> 44 + * <SegmentedControl.Item value="two"> 45 + * <SegmentedControl.ItemText value="two"> 46 + * Two 47 + * </SegmentedControl.ItemText> 48 + * </SegmentedControl.Item> 49 + * </SegmentedControl.Root> 50 + * ``` 51 + */ 52 + export function Root<T extends string>({ 53 + label, 54 + type = 'radio', 55 + size = 'large', 56 + value, 57 + onChange, 58 + children, 59 + style, 60 + accessibilityHint, 61 + }: { 62 + label: string 63 + type: 'tabs' | 'radio' 64 + size?: 'small' | 'large' 65 + value: T 66 + onChange: (value: T) => void 67 + children: React.ReactNode 68 + style?: StyleProp<ViewStyle> 69 + accessibilityHint?: string 70 + }) { 71 + const t = useTheme() 72 + const [selectedPosition, setSelectedPosition] = useState<{ 73 + width: number 74 + x: number 75 + } | null>(null) 76 + 77 + const contextValue = useMemo(() => { 78 + return { 79 + type, 80 + size, 81 + selectedValue: value, 82 + selectedPosition, 83 + onSelectValue: ( 84 + val: string, 85 + position: {width: number; x: number} | null, 86 + ) => { 87 + onChange(val as T) 88 + if (position) setSelectedPosition(position) 89 + }, 90 + updatePosition: (position: {width: number; x: number}) => { 91 + setSelectedPosition(currPos => { 92 + if ( 93 + currPos && 94 + currPos.width === position.width && 95 + currPos.x === position.x 96 + ) { 97 + return currPos 98 + } 99 + return position 100 + }) 101 + }, 102 + } 103 + }, [value, selectedPosition, setSelectedPosition, onChange, type, size]) 104 + 105 + return ( 106 + <View 107 + accessibilityLabel={label} 108 + accessibilityHint={accessibilityHint ?? ''} 109 + style={[ 110 + a.w_full, 111 + a.flex_1, 112 + a.relative, 113 + a.flex_row, 114 + t.atoms.bg_contrast_50, 115 + {borderRadius: 14}, 116 + a.curve_continuous, 117 + a.p_xs, 118 + style, 119 + ]} 120 + role={type === 'tabs' ? 'tablist' : 'radiogroup'}> 121 + {selectedPosition !== null && ( 122 + <Slider x={selectedPosition.x} width={selectedPosition.width} /> 123 + )} 124 + <InternalContext.Provider value={contextValue}> 125 + {children} 126 + </InternalContext.Provider> 127 + </View> 128 + ) 129 + } 130 + 131 + const InternalItemContext = createContext<{ 132 + active: boolean 133 + pressed: boolean 134 + hovered: boolean 135 + focused: boolean 136 + } | null>(null) 137 + 138 + export function Item({ 139 + value, 140 + style, 141 + children, 142 + onPress: onPressProp, 143 + ...props 144 + }: {value: string; children: React.ReactNode} & Omit<ButtonProps, 'children'>) { 145 + const [position, setPosition] = useState<{x: number; width: number} | null>( 146 + null, 147 + ) 148 + 149 + const ctx = useContext(InternalContext) 150 + if (!ctx) 151 + throw new Error( 152 + 'SegmentedControl.Item must be used within a SegmentedControl.Root', 153 + ) 154 + 155 + const active = ctx.selectedValue === value 156 + 157 + // update position if change was external, and not due to onPress 158 + const needsUpdate = 159 + active && 160 + position && 161 + (ctx.selectedPosition?.x !== position.x || 162 + ctx.selectedPosition?.width !== position.width) 163 + 164 + // can't wait for `useEffectEvent` 165 + const update = useNonReactiveCallback(() => { 166 + if (position) ctx.updatePosition(position) 167 + }) 168 + 169 + useLayoutEffect(() => { 170 + if (needsUpdate) { 171 + update() 172 + } 173 + }, [needsUpdate, update]) 174 + 175 + const onPress = useCallback( 176 + (evt: any) => { 177 + ctx.onSelectValue(value, position) 178 + onPressProp?.(evt) 179 + }, 180 + [ctx, value, position, onPressProp], 181 + ) 182 + 183 + return ( 184 + <View 185 + style={[a.flex_1, a.flex_row]} 186 + onLayout={evt => { 187 + const measuredPosition = { 188 + x: evt.nativeEvent.layout.x, 189 + width: evt.nativeEvent.layout.width, 190 + } 191 + if (!ctx.selectedPosition && active) { 192 + ctx.onSelectValue(value, measuredPosition) 193 + } 194 + setPosition(measuredPosition) 195 + }}> 196 + <Button 197 + {...props} 198 + onPress={onPress} 199 + role={ctx.type === 'tabs' ? 'tab' : 'radio'} 200 + accessibilityState={{selected: active}} 201 + style={[ 202 + a.flex_1, 203 + a.bg_transparent, 204 + a.px_sm, 205 + a.py_xs, 206 + {minHeight: ctx.size === 'large' ? 40 : 32}, 207 + style, 208 + ]}> 209 + {({pressed, hovered, focused}) => ( 210 + <InternalItemContext.Provider 211 + value={{active, pressed, hovered, focused}}> 212 + {children} 213 + </InternalItemContext.Provider> 214 + )} 215 + </Button> 216 + </View> 217 + ) 218 + } 219 + 220 + export function ItemText({style, ...props}: ButtonTextProps) { 221 + const t = useTheme() 222 + const ctx = useContext(InternalItemContext) 223 + if (!ctx) 224 + throw new Error( 225 + 'SegmentedControl.ItemText must be used within a SegmentedControl.Item', 226 + ) 227 + return ( 228 + <ButtonText 229 + {...props} 230 + style={[ 231 + a.text_center, 232 + a.text_md, 233 + a.font_medium, 234 + a.px_xs, 235 + ctx.active 236 + ? t.atoms.text 237 + : ctx.focused || ctx.hovered || ctx.pressed 238 + ? t.atoms.text_contrast_medium 239 + : t.atoms.text_contrast_low, 240 + style, 241 + ]} 242 + /> 243 + ) 244 + } 245 + 246 + function Slider({x, width}: {x: number; width: number}) { 247 + const t = useTheme() 248 + 249 + return ( 250 + <Animated.View 251 + layout={native(LinearTransition.easing(Easing.out(Easing.exp)))} 252 + style={[ 253 + a.absolute, 254 + a.curve_continuous, 255 + t.atoms.bg, 256 + { 257 + top: 4, 258 + bottom: 4, 259 + left: 0, 260 + width, 261 + borderRadius: 10, 262 + }, 263 + // TODO: new arch supports boxShadow on native 264 + // in the meantime this is an attempt to get close 265 + platform({ 266 + web: { 267 + boxShadow: '0px 2px 4px 0px #0000000D', 268 + }, 269 + ios: { 270 + shadowColor: '#000', 271 + shadowOffset: {width: 0, height: 2}, 272 + shadowOpacity: 0x0d / 0xff, 273 + shadowRadius: 4, 274 + }, 275 + android: {elevation: 0.25}, 276 + }), 277 + platform({ 278 + native: [{left: x}], 279 + web: [{transform: [{translateX: x}]}, a.transition_transform], 280 + }), 281 + ]} 282 + /> 283 + ) 284 + }
+12 -3
src/components/forms/ToggleButton.tsx
··· 1 - import React from 'react' 1 + import {useMemo} from 'react' 2 2 import { 3 3 type AccessibilityProps, 4 4 type TextStyle, ··· 20 20 multiple?: boolean 21 21 } 22 22 23 + /** 24 + * @deprecated - use SegmentedControl 25 + */ 23 26 export function Group({children, multiple, ...props}: GroupProps) { 24 27 const t = useTheme() 25 28 return ( ··· 39 42 ) 40 43 } 41 44 45 + /** 46 + * @deprecated - use SegmentedControl 47 + */ 42 48 export function Button({children, ...props}: ItemProps) { 43 49 return ( 44 50 <Toggle.Item {...props} style={[a.flex_grow, a.flex_1]}> ··· 51 57 const t = useTheme() 52 58 const state = Toggle.useItemContext() 53 59 54 - const {baseStyles, hoverStyles, activeStyles} = React.useMemo(() => { 60 + const {baseStyles, hoverStyles, activeStyles} = useMemo(() => { 55 61 const base: ViewStyle[] = [] 56 62 const hover: ViewStyle[] = [] 57 63 const active: ViewStyle[] = [] ··· 112 118 ) 113 119 } 114 120 121 + /** 122 + * @deprecated - use SegmentedControl 123 + */ 115 124 export function ButtonText({children}: {children: React.ReactNode}) { 116 125 const t = useTheme() 117 126 const state = Toggle.useItemContext() 118 127 119 - const textStyles = React.useMemo(() => { 128 + const textStyles = useMemo(() => { 120 129 const text: TextStyle[] = [] 121 130 if (state.selected) { 122 131 text.push(t.atoms.text_inverted)
+33 -40
src/screens/Settings/AppearanceSettings.tsx
··· 15 15 import {isNative} from '#/platform/detection' 16 16 import {useSetThemePrefs, useThemePrefs} from '#/state/shell' 17 17 import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem' 18 - import {atoms as a, native, useAlf, useTheme} from '#/alf' 19 - import * as ToggleButton from '#/components/forms/ToggleButton' 18 + import {type Alf, atoms as a, native, useAlf, useTheme} from '#/alf' 19 + import * as SegmentedControl from '#/components/forms/SegmentedControl' 20 20 import {type Props as SVGIconProps} from '#/components/icons/common' 21 21 import {Moon_Stroke2_Corner0_Rounded as MoonIcon} from '#/components/icons/Moon' 22 22 import {Phone_Stroke2_Corner0_Rounded as PhoneIcon} from '#/components/icons/Phone' ··· 36 36 const {setColorMode, setDarkTheme} = useSetThemePrefs() 37 37 38 38 const onChangeAppearance = useCallback( 39 - (keys: string[]) => { 40 - const appearance = keys.find(key => key !== colorMode) as 41 - | 'system' 42 - | 'light' 43 - | 'dark' 44 - | undefined 45 - if (!appearance) return 46 - setColorMode(appearance) 39 + (value: 'light' | 'system' | 'dark') => { 40 + setColorMode(value) 47 41 }, 48 - [setColorMode, colorMode], 42 + [setColorMode], 49 43 ) 50 44 51 45 const onChangeDarkTheme = useCallback( 52 - (keys: string[]) => { 53 - const theme = keys.find(key => key !== darkTheme) as 54 - | 'dim' 55 - | 'dark' 56 - | undefined 57 - if (!theme) return 58 - setDarkTheme(theme) 46 + (value: 'dim' | 'dark') => { 47 + setDarkTheme(value) 59 48 }, 60 - [setDarkTheme, darkTheme], 49 + [setDarkTheme], 61 50 ) 62 51 63 52 const onChangeFontFamily = useCallback( 64 - (values: string[]) => { 65 - const next = values[0] === 'system' ? 'system' : 'theme' 66 - fonts.setFontFamily(next) 53 + (value: 'system' | 'theme') => { 54 + fonts.setFontFamily(value) 67 55 }, 68 56 [fonts], 69 57 ) 70 58 71 59 const onChangeFontScale = useCallback( 72 - (values: string[]) => { 73 - const next = values[0] || ('0' as any) 74 - fonts.setFontScale(next) 60 + (value: Alf['fonts']['scale']) => { 61 + fonts.setFontScale(value) 75 62 }, 76 63 [fonts], 77 64 ) ··· 107 94 name: 'dark', 108 95 }, 109 96 ]} 110 - values={[colorMode]} 97 + value={colorMode} 111 98 onChange={onChangeAppearance} 112 99 /> 113 100 ··· 128 115 name: 'dark', 129 116 }, 130 117 ]} 131 - values={[darkTheme ?? 'dim']} 118 + value={darkTheme ?? 'dim'} 132 119 onChange={onChangeDarkTheme} 133 120 /> 134 121 </Animated.View> ··· 153 140 name: 'theme', 154 141 }, 155 142 ]} 156 - values={[fonts.family]} 143 + value={fonts.family} 157 144 onChange={onChangeFontFamily} 158 145 /> 159 146 ··· 174 161 name: '1', 175 162 }, 176 163 ]} 177 - values={[fonts.scale]} 164 + value={fonts.scale} 178 165 onChange={onChangeFontScale} 179 166 /> 180 167 ··· 192 179 ) 193 180 } 194 181 195 - export function AppearanceToggleButtonGroup({ 182 + export function AppearanceToggleButtonGroup<T extends string>({ 196 183 title, 197 184 description, 198 185 icon: Icon, 199 186 items, 200 - values, 187 + value, 201 188 onChange, 202 189 }: { 203 190 title: string ··· 205 192 icon: React.ComponentType<SVGIconProps> 206 193 items: { 207 194 label: string 208 - name: string 195 + name: T 209 196 }[] 210 - values: string[] 211 - onChange: (values: string[]) => void 197 + value: T 198 + onChange: (value: T) => void 212 199 }) { 213 200 const t = useTheme() 214 201 return ( ··· 227 214 {description} 228 215 </Text> 229 216 )} 230 - <ToggleButton.Group label={title} values={values} onChange={onChange}> 217 + <SegmentedControl.Root 218 + type="radio" 219 + label={title} 220 + value={value} 221 + onChange={onChange}> 231 222 {items.map(item => ( 232 - <ToggleButton.Button 223 + <SegmentedControl.Item 233 224 key={item.name} 234 225 label={item.label} 235 - name={item.name}> 236 - <ToggleButton.ButtonText>{item.label}</ToggleButton.ButtonText> 237 - </ToggleButton.Button> 226 + value={item.name}> 227 + <SegmentedControl.ItemText> 228 + {item.label} 229 + </SegmentedControl.ItemText> 230 + </SegmentedControl.Item> 238 231 ))} 239 - </ToggleButton.Group> 232 + </SegmentedControl.Root> 240 233 </SettingsList.Group> 241 234 </> 242 235 )
+14 -13
src/screens/Settings/components/ChangeHandleDialog.tsx
··· 29 29 import {Admonition} from '#/components/Admonition' 30 30 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 31 31 import * as Dialog from '#/components/Dialog' 32 + import * as SegmentedControl from '#/components/forms/SegmentedControl' 32 33 import * as TextField from '#/components/forms/TextField' 33 - import * as ToggleButton from '#/components/forms/ToggleButton' 34 34 import { 35 35 ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon, 36 36 ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon, ··· 395 395 /> 396 396 </TextField.Root> 397 397 </View> 398 - <ToggleButton.Group 398 + <SegmentedControl.Root 399 399 label={_(msg`Choose domain verification method`)} 400 - values={[dnsPanel ? 'dns' : 'file']} 401 - onChange={values => setDNSPanel(values[0] === 'dns')}> 402 - <ToggleButton.Button name="dns" label={_(msg`DNS Panel`)}> 403 - <ToggleButton.ButtonText> 400 + type="tabs" 401 + value={dnsPanel ? 'dns' : 'file'} 402 + onChange={values => setDNSPanel(values === 'dns')}> 403 + <SegmentedControl.Item value="dns" label={_(msg`DNS Panel`)}> 404 + <SegmentedControl.ItemText> 404 405 <Trans>DNS Panel</Trans> 405 - </ToggleButton.ButtonText> 406 - </ToggleButton.Button> 407 - <ToggleButton.Button name="file" label={_(msg`No DNS Panel`)}> 408 - <ToggleButton.ButtonText> 406 + </SegmentedControl.ItemText> 407 + </SegmentedControl.Item> 408 + <SegmentedControl.Item value="file" label={_(msg`No DNS Panel`)}> 409 + <SegmentedControl.ItemText> 409 410 <Trans>No DNS Panel</Trans> 410 - </ToggleButton.ButtonText> 411 - </ToggleButton.Button> 412 - </ToggleButton.Group> 411 + </SegmentedControl.ItemText> 412 + </SegmentedControl.Item> 413 + </SegmentedControl.Root> 413 414 {dnsPanel ? ( 414 415 <> 415 416 <Text>
+59 -49
src/view/com/auth/server-input/index.tsx src/components/dialogs/ServerInput.tsx
··· 5 5 import {useLingui} from '@lingui/react' 6 6 7 7 import {BSKY_SERVICE} from '#/lib/constants' 8 - import {logEvent} from '#/lib/statsig/statsig' 8 + import {logger} from '#/logger' 9 9 import * as persisted from '#/state/persisted' 10 10 import {useSession} from '#/state/session' 11 - import {atoms as a, useBreakpoints, useTheme} from '#/alf' 11 + import {atoms as a, platform, useBreakpoints, useTheme, web} from '#/alf' 12 12 import {Admonition} from '#/components/Admonition' 13 13 import {Button, ButtonText} from '#/components/Button' 14 14 import * as Dialog from '#/components/Dialog' 15 + import * as SegmentedControl from '#/components/forms/SegmentedControl' 15 16 import * as TextField from '#/components/forms/TextField' 16 - import * as ToggleButton from '#/components/forms/ToggleButton' 17 17 import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' 18 18 import {InlineLinkText} from '#/components/Link' 19 - import {P, Text} from '#/components/Typography' 19 + import {Text} from '#/components/Typography' 20 + 21 + type SegmentedControlOptions = typeof BSKY_SERVICE | 'custom' 20 22 21 23 export function ServerInputDialog({ 22 24 control, ··· 29 31 const formRef = useRef<DialogInnerRef>(null) 30 32 31 33 // persist these options between dialog open/close 32 - const [fixedOption, setFixedOption] = useState(BSKY_SERVICE) 34 + const [fixedOption, setFixedOption] = 35 + useState<SegmentedControlOptions>(BSKY_SERVICE) 33 36 const [previousCustomAddress, setPreviousCustomAddress] = useState('') 34 37 35 38 const onClose = useCallback(() => { ··· 40 43 setPreviousCustomAddress(result) 41 44 } 42 45 } 43 - logEvent('signin:hostingProviderPressed', { 46 + logger.metric('signin:hostingProviderPressed', { 44 47 hostingProviderDidChange: fixedOption !== BSKY_SERVICE, 45 48 }) 46 49 }, [onSelect, fixedOption]) ··· 49 52 <Dialog.Outer 50 53 control={control} 51 54 onClose={onClose} 52 - nativeOptions={{minHeight: height / 2}}> 55 + nativeOptions={platform({ 56 + android: {minHeight: height / 2}, 57 + ios: {preventExpansion: true}, 58 + })}> 53 59 <Dialog.Handle /> 54 60 <DialogInner 55 61 formRef={formRef} ··· 70 76 initialCustomAddress, 71 77 }: { 72 78 formRef: React.Ref<DialogInnerRef> 73 - fixedOption: string 74 - setFixedOption: (opt: string) => void 79 + fixedOption: SegmentedControlOptions 80 + setFixedOption: (opt: SegmentedControlOptions) => void 75 81 initialCustomAddress: string 76 82 }) { 77 83 const control = Dialog.useDialogContext() ··· 124 130 return ( 125 131 <Dialog.ScrollableInner 126 132 accessibilityDescribedBy="dialog-description" 127 - accessibilityLabelledBy="dialog-title"> 133 + accessibilityLabelledBy="dialog-title" 134 + style={web({maxWidth: 500})}> 128 135 <View style={[a.relative, a.gap_md, a.w_full]}> 129 - <Text nativeID="dialog-title" style={[a.text_2xl, a.font_semi_bold]}> 136 + <Text nativeID="dialog-title" style={[a.text_2xl, a.font_bold]}> 130 137 <Trans>Choose your account provider</Trans> 131 138 </Text> 132 - <ToggleButton.Group 133 - label="Preferences" 134 - values={[fixedOption]} 135 - onChange={values => setFixedOption(values[0])}> 136 - <ToggleButton.Button name={BSKY_SERVICE} label={_(msg`Bluesky`)}> 137 - <ToggleButton.ButtonText>{_(msg`Bluesky`)}</ToggleButton.ButtonText> 138 - </ToggleButton.Button> 139 - <ToggleButton.Button 139 + <SegmentedControl.Root 140 + type="tabs" 141 + label={_(msg`Account provider`)} 142 + value={fixedOption} 143 + onChange={setFixedOption}> 144 + <SegmentedControl.Item 145 + testID="bskyServiceSelectBtn" 146 + value={BSKY_SERVICE} 147 + label={_(msg`Bluesky`)}> 148 + <SegmentedControl.ItemText> 149 + {_(msg`Bluesky`)} 150 + </SegmentedControl.ItemText> 151 + </SegmentedControl.Item> 152 + <SegmentedControl.Item 140 153 testID="customSelectBtn" 141 - name="custom" 154 + value="custom" 142 155 label={_(msg`Custom`)}> 143 - <ToggleButton.ButtonText>{_(msg`Custom`)}</ToggleButton.ButtonText> 144 - </ToggleButton.Button> 145 - </ToggleButton.Group> 156 + <SegmentedControl.ItemText> 157 + {_(msg`Custom`)} 158 + </SegmentedControl.ItemText> 159 + </SegmentedControl.Item> 160 + </SegmentedControl.Root> 146 161 147 162 {fixedOption === BSKY_SERVICE && isFirstTimeUser && ( 148 - <Admonition type="tip"> 149 - <Trans> 150 - Bluesky is an open network where you can choose your own provider. 151 - If you're new here, we recommend sticking with the default Bluesky 152 - Social option. 153 - </Trans> 154 - </Admonition> 163 + <View role="tabpanel"> 164 + <Admonition type="tip"> 165 + <Trans> 166 + Bluesky is an open network where you can choose your own 167 + provider. If you're new here, we recommend sticking with the 168 + default Bluesky Social option. 169 + </Trans> 170 + </Admonition> 171 + </View> 155 172 )} 156 173 157 174 {fixedOption === 'custom' && ( 158 - <View 159 - style={[ 160 - a.border, 161 - t.atoms.border_contrast_low, 162 - a.rounded_sm, 163 - a.px_md, 164 - a.py_md, 165 - ]}> 175 + <View role="tabpanel"> 166 176 <TextField.LabelText nativeID="address-input-label"> 167 177 <Trans>Server address</Trans> 168 178 </TextField.LabelText> ··· 197 207 )} 198 208 199 209 <View style={[a.py_xs]}> 200 - <P 201 - style={[ 202 - t.atoms.text_contrast_medium, 203 - a.text_sm, 204 - a.leading_snug, 205 - a.flex_1, 206 - ]}> 210 + <Text 211 + style={[t.atoms.text_contrast_medium, a.text_sm, a.leading_snug]}> 207 212 {isFirstTimeUser ? ( 208 213 <Trans> 209 214 If you're a developer, you can host your own server. ··· 219 224 to="https://atproto.com/guides/self-hosting"> 220 225 <Trans>Learn more.</Trans> 221 226 </InlineLinkText> 222 - </P> 227 + </Text> 223 228 </View> 224 229 225 230 <View style={gtMobile && [a.flex_row, a.justify_end]}> 226 231 <Button 227 232 testID="doneBtn" 228 - variant="outline" 233 + variant="solid" 229 234 color="primary" 230 - size="small" 235 + size={platform({ 236 + native: 'large', 237 + web: 'small', 238 + })} 231 239 onPress={() => control.close()} 232 240 label={_(msg`Done`)}> 233 - <ButtonText>{_(msg`Done`)}</ButtonText> 241 + <ButtonText> 242 + <Trans>Done</Trans> 243 + </ButtonText> 234 244 </Button> 235 245 </View> 236 246 </View>
+23 -16
src/view/screens/Storybook/Forms.tsx
··· 4 4 import {atoms as a} from '#/alf' 5 5 import {Button, ButtonText} from '#/components/Button' 6 6 import {DateField, LabelText} from '#/components/forms/DateField' 7 + import * as SegmentedControl from '#/components/forms/SegmentedControl' 7 8 import * as TextField from '#/components/forms/TextField' 8 9 import * as Toggle from '#/components/forms/Toggle' 9 10 import * as ToggleButton from '#/components/forms/ToggleButton' ··· 15 16 const [toggleGroupBValues, setToggleGroupBValues] = React.useState(['a', 'b']) 16 17 const [toggleGroupCValues, setToggleGroupCValues] = React.useState(['a', 'b']) 17 18 const [toggleGroupDValues, setToggleGroupDValues] = React.useState(['warn']) 19 + const [segmentedControlValue, setSegmentedControlValue] = React.useState< 20 + 'hide' | 'warn' | 'show' 21 + >('warn') 18 22 19 23 const [value, setValue] = React.useState('') 20 24 const [date, setDate] = React.useState('2001-01-01') ··· 254 258 <ToggleButton.ButtonText>Show</ToggleButton.ButtonText> 255 259 </ToggleButton.Button> 256 260 </ToggleButton.Group> 261 + </View> 257 262 258 - <View> 259 - <ToggleButton.Group 260 - label="Preferences" 261 - values={toggleGroupDValues} 262 - onChange={setToggleGroupDValues}> 263 - <ToggleButton.Button name="hide" label="Hide"> 264 - <ToggleButton.ButtonText>Hide</ToggleButton.ButtonText> 265 - </ToggleButton.Button> 266 - <ToggleButton.Button name="warn" label="Warn"> 267 - <ToggleButton.ButtonText>Warn</ToggleButton.ButtonText> 268 - </ToggleButton.Button> 269 - <ToggleButton.Button name="show" label="Show"> 270 - <ToggleButton.ButtonText>Show</ToggleButton.ButtonText> 271 - </ToggleButton.Button> 272 - </ToggleButton.Group> 273 - </View> 263 + <View style={[a.gap_md, a.align_start, a.w_full]}> 264 + <H3>SegmentedControl</H3> 265 + 266 + <SegmentedControl.Root 267 + label="Preferences" 268 + type="radio" 269 + value={segmentedControlValue} 270 + onChange={setSegmentedControlValue}> 271 + <SegmentedControl.Item value="hide" label="Hide"> 272 + <SegmentedControl.ItemText>Hide</SegmentedControl.ItemText> 273 + </SegmentedControl.Item> 274 + <SegmentedControl.Item value="warn" label="Warn"> 275 + <SegmentedControl.ItemText>Warn</SegmentedControl.ItemText> 276 + </SegmentedControl.Item> 277 + <SegmentedControl.Item value="show" label="Show"> 278 + <SegmentedControl.ItemText>Show</SegmentedControl.ItemText> 279 + </SegmentedControl.Item> 280 + </SegmentedControl.Root> 274 281 </View> 275 282 </View> 276 283 )