From 6ee1aedf802ac0d05014bd910a986f58d12b1346 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 9 Jan 2024 14:20:06 -0600 Subject: [PATCH 01/68] Install on native as well --- src/App.native.tsx | 43 ++++++++++++++++++++++++------------------- src/App.web.tsx | 2 +- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/App.native.tsx b/src/App.native.tsx index 6402b4a898..a9dfafe36f 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -13,6 +13,8 @@ import { import 'view/icons' +import {ThemeProvider as Alf} from '#/alf' +import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {init as initPersistedState} from '#/state/persisted' import {listenSessionDropped} from './state/events' import {useColorMode} from 'state/shell' @@ -46,6 +48,7 @@ function InnerApp() { const colorMode = useColorMode() const {isInitialLoad, currentAccount} = useSession() const {resumeSession} = useSessionApi() + const theme = useColorModeTheme(colorMode) // init useEffect(() => { @@ -60,25 +63,27 @@ function InnerApp() { return ( - - - - - - {/* All components should be within this provider */} - - - - - - - - - - - + + + + + + + {/* All components should be within this provider */} + + + + + + + + + + + + ) } diff --git a/src/App.web.tsx b/src/App.web.tsx index 1bdb3c208c..9ee9a4fd71 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -8,6 +8,7 @@ import {RootSiblingParent} from 'react-native-root-siblings' import 'view/icons' import {ThemeProvider as Alf} from '#/alf' +import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {init as initPersistedState} from '#/state/persisted' import {useColorMode} from 'state/shell' import {Shell} from 'view/shell/index' @@ -29,7 +30,6 @@ import { } from 'state/session' import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' import * as persisted from '#/state/persisted' -import {useColorModeTheme} from '#/alf/util/useColorModeTheme' function InnerApp() { const {isInitialLoad, currentAccount} = useSession() From 3e055025c762d08722d5269b4bea780c8d1d7ad4 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 9 Jan 2024 14:23:18 -0600 Subject: [PATCH 02/68] Add button and link components --- src/view/com/Button.tsx | 239 ++++++++++++++++++++++------------ src/view/com/Link.tsx | 162 +++++++++++++++++++++++ src/view/screens/DebugNew.tsx | 115 ++++++++++------ 3 files changed, 391 insertions(+), 125 deletions(-) create mode 100644 src/view/com/Link.tsx diff --git a/src/view/com/Button.tsx b/src/view/com/Button.tsx index d1f70d4aeb..259ae59771 100644 --- a/src/view/com/Button.tsx +++ b/src/view/com/Button.tsx @@ -1,86 +1,47 @@ import React from 'react' -import {Pressable, Text, PressableProps, TextProps} from 'react-native' -import * as tokens from '#/alf/tokens' -import {atoms} from '#/alf' - -export type ButtonType = - | 'primary' - | 'secondary' - | 'tertiary' - | 'positive' - | 'negative' +import { + Pressable, + Text, + PressableProps, + TextProps, + ViewStyle, +} from 'react-native' + +import {atoms, tokens, web, native} from '#/alf' + +export type ButtonType = 'primary' | 'secondary' | 'negative' export type ButtonSize = 'small' | 'large' export type VariantProps = { type?: ButtonType size?: ButtonSize } -type ButtonState = { - pressed: boolean - hovered: boolean - focused: boolean -} export type ButtonProps = Omit & VariantProps & { children: | ((props: { - state: ButtonState - type?: ButtonType - size?: ButtonSize + state: { + pressed: boolean + hovered: boolean + focused: boolean + } + props: VariantProps & { + disabled?: boolean + } }) => React.ReactNode) | React.ReactNode | string } -export type ButtonTextProps = TextProps & VariantProps - -export function Button({children, style, type, size, ...rest}: ButtonProps) { - const {baseStyles, hoverStyles} = React.useMemo(() => { - const baseStyles = [] - const hoverStyles = [] - - switch (type) { - case 'primary': - baseStyles.push({ - backgroundColor: tokens.color.blue_500, - }) - break - case 'secondary': - baseStyles.push({ - backgroundColor: tokens.color.gray_200, - }) - hoverStyles.push({ - backgroundColor: tokens.color.gray_100, - }) - break - default: - } - - switch (size) { - case 'large': - baseStyles.push( - atoms.py_md, - atoms.px_xl, - atoms.rounded_md, - atoms.gap_sm, - ) - break - case 'small': - baseStyles.push( - atoms.py_sm, - atoms.px_md, - atoms.rounded_sm, - atoms.gap_xs, - ) - break - default: - } - - return { - baseStyles, - hoverStyles, - } - }, [type, size]) +export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean} +export function Button({ + children, + style, + type, + size, + disabled = false, + ...rest +}: ButtonProps) { const [state, setState] = React.useState({ pressed: false, hovered: false, @@ -124,10 +85,99 @@ export function Button({children, style, type, size, ...rest}: ButtonProps) { })) }, [setState]) + const {baseStyles, hoverStyles} = React.useMemo(() => { + const baseStyles: ViewStyle[] = [] + const hoverStyles: ViewStyle[] = [] + + switch (type) { + case 'primary': { + if (disabled) { + baseStyles.push({ + backgroundColor: tokens.color.blue_300, + }) + } else { + baseStyles.push({ + backgroundColor: tokens.color.blue_500, + }) + } + break + } + case 'secondary': { + if (disabled) { + baseStyles.push({ + backgroundColor: tokens.color.gray_100, + }) + } else { + baseStyles.push({ + backgroundColor: tokens.color.gray_200, + }) + } + break + } + case 'negative': { + if (disabled) { + baseStyles.push({ + backgroundColor: tokens.color.red_400, + }) + } else { + baseStyles.push({ + backgroundColor: tokens.color.red_500, + }) + } + break + } + default: + } + + switch (size) { + case 'large': { + baseStyles.push( + atoms.py_md, + atoms.px_xl, + atoms.rounded_md, + atoms.gap_sm, + ) + break + } + case 'small': { + baseStyles.push( + atoms.py_sm, + atoms.px_md, + atoms.rounded_sm, + atoms.gap_xs, + ) + break + } + default: + } + + return { + baseStyles, + hoverStyles, + } + }, [type, size, disabled]) + + const childProps = React.useMemo( + () => ({ + state, + props: { + type, + size, + disabled: disabled || false, + }, + }), + [state, type, size, disabled], + ) + return ( [ + disabled={disabled || false} + accessibilityState={{ + disabled: disabled || false, + }} + style={[ atoms.flex_row, atoms.align_center, ...baseStyles, @@ -141,11 +191,11 @@ export function Button({children, style, type, size, ...rest}: ButtonProps) { onFocus={onFocus} onBlur={onBlur}> {typeof children === 'string' ? ( - + {children} ) : typeof children === 'function' ? ( - children({state, type, size}) + children(childProps) ) : ( children )} @@ -158,35 +208,60 @@ export function ButtonText({ style, type, size, + disabled, ...rest }: ButtonTextProps) { const textStyles = React.useMemo(() => { - const base = [] + const baseStyles = [] switch (type) { - case 'primary': - base.push({color: tokens.color.white}) + case 'primary': { + baseStyles.push({color: tokens.color.white}) + break + } + case 'secondary': { + if (disabled) { + baseStyles.push({ + color: tokens.color.gray_500, + }) + } else { + baseStyles.push({ + color: tokens.color.gray_700, + }) + } break - case 'secondary': - base.push({ - color: tokens.color.gray_700, + } + case 'negative': { + baseStyles.push({ + color: tokens.color.white, }) break + } default: } switch (size) { - case 'small': - base.push(atoms.text_sm, {paddingBottom: 1}) + case 'small': { + baseStyles.push( + atoms.text_sm, + web({paddingBottom: 1}), + native({marginTop: 2}), + ) break - case 'large': - base.push(atoms.text_md, {paddingBottom: 1}) + } + case 'large': { + baseStyles.push( + atoms.text_md, + web({paddingBottom: 1}), + native({marginTop: 2}), + ) break + } default: } - return base - }, [type, size]) + return baseStyles + }, [type, size, disabled]) return ( & { + style?: StyleProp // only accept text styles on element itself + warnOnMismatchingLabel?: boolean + action?: 'push' | 'replace' | 'navigate' +} & Pick>[0], 'to'> + +/** + * A interactive element that renders as a `` tag on the web. On mobile it + * will translate the `href` to navigator screens and params and dispatch a + * navigation action. + * + * Intended to behave as a web anchor tag. For more complex routing, use a + * `Button`. + */ +export function Link({ + children, + to, + style, + action = 'push', + warnOnMismatchingLabel, + ...rest +}: LinkProps) { + const t = useTheme() + const navigation = useNavigation() + const {href, accessibilityRole} = useLinkProps({ + to: + typeof to === 'string' ? convertBskyAppUrlIfNeeded(sanitizeUrl(to)) : to, + }) + const isExternal = isExternalUrl(href) + const {openModal, closeModal} = useModalControls() + const onPress = React.useCallback( + (e: GestureResponderEvent) => { + const label = typeof children === 'string' ? children : '' + const requiresWarning = Boolean( + warnOnMismatchingLabel && + label && + isExternal && + linkRequiresWarning(href, label), + ) + + if (requiresWarning) { + e.preventDefault() + + openModal({ + name: 'link-warning', + text: label, + href: href, + }) + } else { + e.preventDefault() + + if (isExternal) { + Linking.openURL(href) + } else { + /** + * A `GestureResponderEvent`, but cast to `any` to avoid using a bunch + * of @ts-ignore below. + */ + const event = e as any + const isMiddleClick = isWeb && event.button === 1 + const isMetaKey = + isWeb && + (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) + const shouldOpenInNewTab = isMetaKey || isMiddleClick + + if ( + shouldOpenInNewTab || + href.startsWith('http') || + href.startsWith('mailto') + ) { + Linking.openURL(href) + } else { + closeModal() // close any active modals + + if (action === 'push') { + navigation.dispatch(StackActions.push(...router.matchPath(href))) + } else if (action === 'replace') { + navigation.dispatch( + StackActions.replace(...router.matchPath(href)), + ) + } else if (action === 'navigate') { + // @ts-ignore + navigation.navigate(...router.matchPath(href)) + } else { + throw Error('Unsupported navigator action.') + } + } + } + } + }, + [ + href, + isExternal, + warnOnMismatchingLabel, + navigation, + action, + children, + closeModal, + openModal, + ], + ) + + return ( + + ) +} diff --git a/src/view/screens/DebugNew.tsx b/src/view/screens/DebugNew.tsx index 0b7c5f03bc..b687a0999a 100644 --- a/src/view/screens/DebugNew.tsx +++ b/src/view/screens/DebugNew.tsx @@ -7,6 +7,7 @@ import {useSetColorMode} from '#/state/shell' import * as tokens from '#/alf/tokens' import {atoms as a, useTheme, useBreakpoints, ThemeProvider as Alf} from '#/alf' import {Button, ButtonText} from '#/view/com/Button' +import {Link} from '#/view/com/Link' import {Text, H1, H2, H3, H4, H5, H6} from '#/view/com/Typography' function ThemeSelector() { @@ -142,6 +143,75 @@ function ThemedSection() { ) } +export function Buttons() { + const t = useTheme() + + return ( + + + + + + + + + + + + + + + + + External + + +

External with custom children

+ + + https://blueskyweb.xyz + + + Internal + + + + {({props}) => Link as a button} + +
+ ) +} + export function DebugScreen() { const t = useTheme() @@ -151,6 +221,8 @@ export function DebugScreen() { + + @@ -173,49 +245,6 @@ export function DebugScreen() { atoms.text_xs atoms.text_xxs - - - - - - - - - - - - - Date: Tue, 9 Jan 2024 14:27:17 -0600 Subject: [PATCH 03/68] Comments --- src/view/com/Button.tsx | 8 +++++++- src/view/com/Link.tsx | 20 +++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/view/com/Button.tsx b/src/view/com/Button.tsx index 259ae59771..8aa17ec5a9 100644 --- a/src/view/com/Button.tsx +++ b/src/view/com/Button.tsx @@ -11,11 +11,17 @@ import {atoms, tokens, web, native} from '#/alf' export type ButtonType = 'primary' | 'secondary' | 'negative' export type ButtonSize = 'small' | 'large' - export type VariantProps = { + /** + * The presentation styles of the button + */ type?: ButtonType + /** + * The size of the button + */ size?: ButtonSize } + export type ButtonProps = Omit & VariantProps & { children: diff --git a/src/view/com/Link.tsx b/src/view/com/Link.tsx index f139e2d45c..8ff28f5fb3 100644 --- a/src/view/com/Link.tsx +++ b/src/view/com/Link.tsx @@ -26,9 +26,19 @@ import {useModalControls} from '#/state/modals' import {router} from '#/routes' export type LinkProps = Omit & { - style?: StyleProp // only accept text styles on element itself - warnOnMismatchingLabel?: boolean + /** + * `TextStyle` to apply to the anchor element itself. Does not apply to any children. + */ + style?: StyleProp + /** + * The React Navigation `StackAction` to perform when the link is pressed. + */ action?: 'push' | 'replace' | 'navigate' + /** + * If true, will warn the user if the link text does not match the href. Only + * works for Links with children that are strings i.e. text links. + */ + warnOnMismatchingTextChild?: boolean } & Pick>[0], 'to'> /** @@ -44,7 +54,7 @@ export function Link({ to, style, action = 'push', - warnOnMismatchingLabel, + warnOnMismatchingTextChild, ...rest }: LinkProps) { const t = useTheme() @@ -59,7 +69,7 @@ export function Link({ (e: GestureResponderEvent) => { const label = typeof children === 'string' ? children : '' const requiresWarning = Boolean( - warnOnMismatchingLabel && + warnOnMismatchingTextChild && label && isExternal && linkRequiresWarning(href, label), @@ -118,7 +128,7 @@ export function Link({ [ href, isExternal, - warnOnMismatchingLabel, + warnOnMismatchingTextChild, navigation, action, children, From b5474eb61e4338235c68aa097616c63eabbdc45b Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 9 Jan 2024 14:29:39 -0600 Subject: [PATCH 04/68] Use new prop --- src/view/screens/DebugNew.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/view/screens/DebugNew.tsx b/src/view/screens/DebugNew.tsx index b687a0999a..e95991c8fa 100644 --- a/src/view/screens/DebugNew.tsx +++ b/src/view/screens/DebugNew.tsx @@ -185,7 +185,7 @@ export function Buttons() { External @@ -194,13 +194,13 @@ export function Buttons() { https://blueskyweb.xyz Internal From 83876b62e190ffed10eb23c6f999aa919fea00df Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 9 Jan 2024 17:37:43 -0600 Subject: [PATCH 05/68] Add some form elements --- src/alf/themes.ts | 12 ++- src/view/com/Button.tsx | 7 +- src/view/com/forms/InputGroup.tsx | 37 +++++++++ src/view/com/forms/InputText.tsx | 125 ++++++++++++++++++++++++++++++ src/view/icons/Logo.tsx | 6 +- src/view/screens/DebugNew.tsx | 13 ++++ 6 files changed, 189 insertions(+), 11 deletions(-) create mode 100644 src/view/com/forms/InputGroup.tsx create mode 100644 src/view/com/forms/InputText.tsx diff --git a/src/alf/themes.ts b/src/alf/themes.ts index aae5c58931..14e1de7ff6 100644 --- a/src/alf/themes.ts +++ b/src/alf/themes.ts @@ -4,14 +4,10 @@ import type {Mutable} from '#/alf/types' export type ThemeName = 'light' | 'dark' export type ReadonlyTheme = typeof light export type Theme = Mutable +export type ReadonlyPalette = typeof lightPalette +export type Palette = Mutable -export type Palette = { - primary: string - positive: string - negative: string -} - -export const lightPalette: Palette = { +export const lightPalette = { primary: tokens.color.blue_500, positive: tokens.color.green_500, negative: tokens.color.red_500, @@ -24,6 +20,7 @@ export const darkPalette: Palette = { } as const export const light = { + name: 'light', palette: lightPalette, atoms: { text: { @@ -66,6 +63,7 @@ export const light = { } export const dark: Theme = { + name: 'dark', palette: darkPalette, atoms: { text: { diff --git a/src/view/com/Button.tsx b/src/view/com/Button.tsx index 8aa17ec5a9..1d858a8eea 100644 --- a/src/view/com/Button.tsx +++ b/src/view/com/Button.tsx @@ -7,7 +7,7 @@ import { ViewStyle, } from 'react-native' -import {atoms, tokens, web, native} from '#/alf' +import {useTheme, atoms, tokens, web, native} from '#/alf' export type ButtonType = 'primary' | 'secondary' | 'negative' export type ButtonSize = 'small' | 'large' @@ -217,6 +217,8 @@ export function ButtonText({ disabled, ...rest }: ButtonTextProps) { + const t = useTheme() + const textStyles = React.useMemo(() => { const baseStyles = [] @@ -244,6 +246,7 @@ export function ButtonText({ break } default: + baseStyles.push(t.atoms.text) } switch (size) { @@ -267,7 +270,7 @@ export function ButtonText({ } return baseStyles - }, [type, size, disabled]) + }, [t, type, size, disabled]) return ( ) { + const children = React.Children.toArray(props.children) + const total = children.length + return ( + + {children.map((child, i) => { + return React.isValidElement(child) ? ( + + {React.cloneElement(child, { + // @ts-ignore + style: [ + ...(Array.isArray(child.props?.style) + ? child.props.style + : [child.props.style || {}]), + { + borderTopLeftRadius: i > 0 ? 0 : undefined, + borderTopRightRadius: i > 0 ? 0 : undefined, + borderBottomLeftRadius: i < total - 1 ? 0 : undefined, + borderBottomRightRadius: i < total - 1 ? 0 : undefined, + borderBottomWidth: i < total - 1 ? 0 : undefined, + }, + ], + })} + + ) : null + })} + + ) +} diff --git a/src/view/com/forms/InputText.tsx b/src/view/com/forms/InputText.tsx new file mode 100644 index 0000000000..4f136a3241 --- /dev/null +++ b/src/view/com/forms/InputText.tsx @@ -0,0 +1,125 @@ +import React from 'react' +import {View, TextInput, TextInputProps, TextStyle} from 'react-native' + +import {useTheme, atoms, web, tokens} from '#/alf' + +type Props = Omit & { + placeholder: string + hasError?: boolean + icon?: React.FunctionComponent +} + +export function InputText({hasError, icon: Icon, ...props}: Props) { + const t = useTheme() + const [state, setState] = React.useState({ + hovered: false, + focused: false, + }) + + const onHoverIn = React.useCallback(() => { + setState(s => ({ + ...s, + hovered: true, + })) + }, [setState]) + const onHoverOut = React.useCallback(() => { + setState(s => ({ + ...s, + hovered: false, + })) + }, [setState]) + const onFocus = React.useCallback(() => { + setState(s => ({ + ...s, + focused: true, + })) + }, [setState]) + const onBlur = React.useCallback(() => { + setState(s => ({ + ...s, + focused: false, + })) + }, [setState]) + + const {inputStyles, iconStyles} = React.useMemo(() => { + const input: TextStyle[] = [] + const icon: TextStyle[] = [] + + if (Icon) { + input.push({ + paddingLeft: 40, + }) + } + + if (hasError) { + input.push({ + borderColor: tokens.color.red_200, + }) + icon.push({ + color: tokens.color.red_400, + }) + } + + if (state.hovered || state.focused) { + input.push({ + borderColor: t.atoms.border_contrast_500.borderColor, + }) + + if (hasError) { + input.push({ + borderColor: tokens.color.red_500, + }) + } + } + + return {inputStyles: input, iconStyles: icon} + }, [t, state, hasError, Icon]) + + return ( + + + + {Icon && ( + + )} + + ) +} diff --git a/src/view/icons/Logo.tsx b/src/view/icons/Logo.tsx index 15ab5a11c0..9f9f57fc85 100644 --- a/src/view/icons/Logo.tsx +++ b/src/view/icons/Logo.tsx @@ -1,4 +1,5 @@ import React from 'react' +import {StyleSheet} from 'react-native' import Svg, { Path, Defs, @@ -19,7 +20,8 @@ type Props = { export const Logo = React.forwardRef(function LogoImpl(props: Props, ref) { const {fill, ...rest} = props const gradient = fill === 'sky' - const _fill = gradient ? 'url(#sky)' : fill || colors.blue3 + const styles = StyleSheet.flatten(props.style) + const _fill = gradient ? 'url(#sky)' : fill || styles?.color || colors.blue3 // @ts-ignore it's fiiiiine const size = parseInt(rest.width || 32) return ( @@ -29,7 +31,7 @@ export const Logo = React.forwardRef(function LogoImpl(props: Props, ref) { ref={ref} viewBox="0 0 64 57" {...rest} - style={{width: size, height: size * ratio}}> + style={[{width: size, height: size * ratio}, styles]}> {gradient && ( diff --git a/src/view/screens/DebugNew.tsx b/src/view/screens/DebugNew.tsx index e95991c8fa..de08ab8228 100644 --- a/src/view/screens/DebugNew.tsx +++ b/src/view/screens/DebugNew.tsx @@ -9,6 +9,8 @@ import {atoms as a, useTheme, useBreakpoints, ThemeProvider as Alf} from '#/alf' import {Button, ButtonText} from '#/view/com/Button' import {Link} from '#/view/com/Link' import {Text, H1, H2, H3, H4, H5, H6} from '#/view/com/Typography' +import {InputText} from '#/view/com/forms/InputText' +import {Logo} from '#/view/icons/Logo' function ThemeSelector() { const setColorMode = useSetColorMode() @@ -212,6 +214,16 @@ export function Buttons() { ) } +function Forms() { + return ( + + + + + + ) +} + export function DebugScreen() { const t = useTheme() @@ -221,6 +233,7 @@ export function DebugScreen() { + From 40446a13044b5d45a920c3c9cf3cea8837255385 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 9 Jan 2024 17:58:47 -0600 Subject: [PATCH 06/68] Add labels to input --- src/view/com/forms/InputText.tsx | 23 +++++++++++++++++++++-- src/view/screens/DebugNew.tsx | 2 +- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/view/com/forms/InputText.tsx b/src/view/com/forms/InputText.tsx index 4f136a3241..55506b84d7 100644 --- a/src/view/com/forms/InputText.tsx +++ b/src/view/com/forms/InputText.tsx @@ -2,14 +2,17 @@ import React from 'react' import {View, TextInput, TextInputProps, TextStyle} from 'react-native' import {useTheme, atoms, web, tokens} from '#/alf' +import {Text} from '#/view/com/Typography' type Props = Omit & { + label?: string placeholder: string hasError?: boolean icon?: React.FunctionComponent } -export function InputText({hasError, icon: Icon, ...props}: Props) { +export function InputText({label, hasError, icon: Icon, ...props}: Props) { + const labelId = React.useId() const t = useTheme() const [state, setState] = React.useState({ hovered: false, @@ -77,7 +80,23 @@ export function InputText({hasError, icon: Icon, ...props}: Props) { return ( + {label && ( + + {label} + + )} + - + From 214254b410687eb12b48a7ec527f43275cd35d32 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 10 Jan 2024 10:45:55 -0600 Subject: [PATCH 07/68] Fix line height, add suffix --- src/view/com/forms/InputText.tsx | 71 +++++++++++++++++++++++++------- src/view/screens/DebugNew.tsx | 6 +++ 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/src/view/com/forms/InputText.tsx b/src/view/com/forms/InputText.tsx index 55506b84d7..eb77c6242f 100644 --- a/src/view/com/forms/InputText.tsx +++ b/src/view/com/forms/InputText.tsx @@ -1,5 +1,11 @@ import React from 'react' -import {View, TextInput, TextInputProps, TextStyle} from 'react-native' +import { + View, + TextInput, + TextInputProps, + TextStyle, + LayoutChangeEvent, +} from 'react-native' import {useTheme, atoms, web, tokens} from '#/alf' import {Text} from '#/view/com/Typography' @@ -9,15 +15,23 @@ type Props = Omit & { placeholder: string hasError?: boolean icon?: React.FunctionComponent + suffix?: React.FunctionComponent } -export function InputText({label, hasError, icon: Icon, ...props}: Props) { +export function InputText({ + label, + hasError, + icon: Icon, + suffix: Suffix, + ...props +}: Props) { const labelId = React.useId() const t = useTheme() const [state, setState] = React.useState({ hovered: false, focused: false, }) + const [suffixPadding, setSuffixPadding] = React.useState(0) const onHoverIn = React.useCallback(() => { setState(s => ({ @@ -43,6 +57,12 @@ export function InputText({label, hasError, icon: Icon, ...props}: Props) { focused: false, })) }, [setState]) + const handleSuffixLayout = React.useCallback( + (e: LayoutChangeEvent) => { + setSuffixPadding(e.nativeEvent.layout.width + 16) + }, + [setSuffixPadding], + ) const {inputStyles, iconStyles} = React.useMemo(() => { const input: TextStyle[] = [] @@ -113,31 +133,52 @@ export function InputText({label, hasError, icon: Icon, ...props}: Props) { atoms.text_md, t.atoms.border, t.atoms.text, - {borderWidth: 2}, web({ paddingTop: atoms.pt_md.paddingTop - 1, }), + {paddingRight: suffixPadding}, + {borderWidth: 2, lineHeight: atoms.text_md.lineHeight * 1.1875}, ...inputStyles, ...(Array.isArray(props.style) ? props.style : [props.style]), ]} /> {Icon && ( - + + + )} + + {Suffix && ( + + atoms.align_center, + atoms.justify_center, + atoms.pr_lg, + {left: 'auto'}, + ]}> + + )} ) diff --git a/src/view/screens/DebugNew.tsx b/src/view/screens/DebugNew.tsx index 369b57caff..d52e7a6e00 100644 --- a/src/view/screens/DebugNew.tsx +++ b/src/view/screens/DebugNew.tsx @@ -220,6 +220,12 @@ function Forms() { + + .bksy.social} + /> ) } From 1ea20b3a053a22ea007d7305888e3ffd3168aec0 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 10 Jan 2024 14:33:11 -0600 Subject: [PATCH 08/68] Date inputs --- bskyweb/templates/base.html | 4 + .../com/forms/InputDate/index.android.tsx | 162 ++++++++++++++++++ src/view/com/forms/InputDate/index.tsx | 77 +++++++++ src/view/com/forms/InputDate/index.web.tsx | 154 +++++++++++++++++ src/view/com/forms/InputDate/utils.ts | 16 ++ src/view/com/forms/types.ts | 22 +++ .../com/util/hooks/useInteractionState.ts | 21 +++ src/view/screens/DebugNew.tsx | 16 ++ web/index.html | 4 + 9 files changed, 476 insertions(+) create mode 100644 src/view/com/forms/InputDate/index.android.tsx create mode 100644 src/view/com/forms/InputDate/index.tsx create mode 100644 src/view/com/forms/InputDate/index.web.tsx create mode 100644 src/view/com/forms/InputDate/utils.ts create mode 100644 src/view/com/forms/types.ts create mode 100644 src/view/com/util/hooks/useInteractionState.ts diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html index 55c7c9fd86..ffc2adf0f8 100644 --- a/bskyweb/templates/base.html +++ b/bskyweb/templates/base.html @@ -39,6 +39,10 @@ height: calc(100% + env(safe-area-inset-top)); } + input::-webkit-date-and-time-value { + text-align: left; + } + /* Color theming */ :root { --text: black; diff --git a/src/view/com/forms/InputDate/index.android.tsx b/src/view/com/forms/InputDate/index.android.tsx new file mode 100644 index 0000000000..76ffa2e56e --- /dev/null +++ b/src/view/com/forms/InputDate/index.android.tsx @@ -0,0 +1,162 @@ +import React from 'react' +import {View, TextInputProps, TextStyle, Pressable} from 'react-native' +import DateTimePicker, { + BaseProps as DateTimePickerProps, +} from '@react-native-community/datetimepicker' + +import {Logo} from '#/view/icons/Logo' +import {useTheme, atoms, tokens} from '#/alf' +import {Text} from '#/view/com/Typography' +import {useInteractionState} from '#/view/com/util/hooks/useInteractionState' +import {BaseProps} from '#/view/com/forms/types' +import { + localizeDate, + toSimpleDateString, +} from '#/view/com/forms/InputDate/utils' + +type Props = Omit & BaseProps + +export * as utils from '#/view/com/forms/InputDate/utils' + +export function InputDate({ + value: initialValue, + onChange, + testID, + label, + hasError, + accessibilityLabel, + accessibilityHint, + ...props +}: Props) { + const labelId = React.useId() + const t = useTheme() + const [open, setOpen] = React.useState(false) + const [value, setValue] = React.useState(initialValue) + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + + const {inputStyles, iconStyles} = React.useMemo(() => { + const input: TextStyle[] = [ + { + paddingLeft: 40, + }, + ] + const icon: TextStyle[] = [] + + if (hasError) { + input.push({ + borderColor: tokens.color.red_200, + }) + icon.push({ + color: tokens.color.red_400, + }) + } + + if (focused) { + input.push({ + borderColor: t.atoms.border_contrast_500.borderColor, + }) + + if (hasError) { + input.push({ + borderColor: tokens.color.red_500, + }) + } + } + + return {inputStyles: input, iconStyles: icon} + }, [t, focused, hasError]) + + const onChangeInternal = React.useCallback< + Required['onChange'] + >( + (_event, date) => { + setOpen(false) + + if (date) { + const formatted = toSimpleDateString(date) + onChange(formatted) + setValue(formatted) + } + }, + [onChange, setOpen, setValue], + ) + + return ( + + {label && ( + + {label} + + )} + + setOpen(true)} + onFocus={onFocus} + onBlur={onBlur} + style={[ + { + paddingTop: atoms.pt_md.paddingTop + 2, + }, + atoms.w_full, + atoms.px_lg, + atoms.pb_md, + atoms.rounded_sm, + t.atoms.bg_contrast_100, + ...inputStyles, + ]}> + {localizeDate(value)} + + + + + + + {open && ( + + )} + + ) +} diff --git a/src/view/com/forms/InputDate/index.tsx b/src/view/com/forms/InputDate/index.tsx new file mode 100644 index 0000000000..6b3c8b40b9 --- /dev/null +++ b/src/view/com/forms/InputDate/index.tsx @@ -0,0 +1,77 @@ +import React from 'react' +import {View, TextInputProps} from 'react-native' +import DateTimePicker, { + DateTimePickerEvent, +} from '@react-native-community/datetimepicker' + +import {useTheme, atoms} from '#/alf' +import {Text} from '#/view/com/Typography' +import {BaseProps} from '#/view/com/forms/types' +import {toSimpleDateString} from '#/view/com/forms/InputDate/utils' + +type Props = Omit & BaseProps + +export * as utils from '#/view/com/forms/InputDate/utils' + +/** + * Date-only input. Accepts a date in the format YYYY-MM-DD, and reports date + * changes in the same format. + * + * For dates of unknown format, convert with the + * `utils.toSimpleDateString(Date)` export of this file. + */ +export function InputDate({ + value: initialValue, + onChange, + testID, + label, + accessibilityLabel, + accessibilityHint, +}: Props) { + const labelId = React.useId() + const t = useTheme() + const [value, setValue] = React.useState(initialValue) + + const onChangeInternal = React.useCallback( + (event: DateTimePickerEvent, date: Date | undefined) => { + if (date) { + const formatted = toSimpleDateString(date) + onChange(formatted) + setValue(formatted) + } + }, + [onChange], + ) + + return ( + + {label && ( + + {label} + + )} + + + + ) +} diff --git a/src/view/com/forms/InputDate/index.web.tsx b/src/view/com/forms/InputDate/index.web.tsx new file mode 100644 index 0000000000..994a7f509f --- /dev/null +++ b/src/view/com/forms/InputDate/index.web.tsx @@ -0,0 +1,154 @@ +import React from 'react' +import {View, TextStyle} from 'react-native' +// @ts-ignore +import {unstable_createElement} from 'react-native-web' + +import {Logo} from '#/view/icons/Logo' +import {useTheme, atoms, tokens} from '#/alf' +import {Text} from '#/view/com/Typography' +import {useInteractionState} from '#/view/com/util/hooks/useInteractionState' + +import {BaseProps} from '#/view/com/forms/types' +import {toSimpleDateString} from '#/view/com/forms/InputDate/utils' + +type Props = BaseProps + +export * as utils from '#/view/com/forms/InputDate/utils' + +export function InputDate({ + label, + hasError, + testID, + value: initialValue, + onChange, + accessibilityLabel, + accessibilityHint, + ...props +}: Props) { + const labelId = React.useId() + const t = useTheme() + const [value, setValue] = React.useState(initialValue) + const { + state: hovered, + onIn: onHoverIn, + onOut: onHoverOut, + } = useInteractionState() + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + + const {inputStyles, iconStyles} = React.useMemo(() => { + const input: TextStyle[] = [ + { + paddingLeft: 40, + }, + ] + const icon: TextStyle[] = [] + + if (hasError) { + input.push({ + borderColor: tokens.color.red_200, + }) + icon.push({ + color: tokens.color.red_400, + }) + } + + if (hovered || focused) { + input.push({ + borderColor: t.atoms.border_contrast_500.borderColor, + }) + + if (hasError) { + input.push({ + borderColor: tokens.color.red_500, + }) + } + } + + return {inputStyles: input, iconStyles: icon} + }, [t, hovered, focused, hasError]) + + const handleOnChange = React.useCallback( + (e: any) => { + const date = e.currentTarget.valueAsDate + + if (date) { + const formatted = toSimpleDateString(date) + onChange(formatted) + setValue(formatted) + } + }, + [onChange, setValue], + ) + + return ( + + {label && ( + + {label} + + )} + + {unstable_createElement('input', { + ...props, + testID: `${testID}-datepicker`, + 'aria-labelledby': labelId, + 'aria-label': label, + accessibilityLabel: accessibilityLabel, + accessibilityHint: accessibilityHint, + type: 'date', + value: value, + onFocus: onFocus, + onBlur: onBlur, + onChange: handleOnChange, + onMouseEnter: onHoverIn, + onMouseLeave: onHoverOut, + style: [ + { + outline: 0, + border: 0, + appearance: 'none', + boxSizing: 'border-box', + lineHeight: atoms.text_md.lineHeight * 1.1875, + paddingTop: atoms.pt_md.paddingTop - 1, + }, + atoms.w_full, + atoms.px_lg, + atoms.pb_md, + atoms.rounded_sm, + atoms.text_md, + t.atoms.bg_contrast_100, + t.atoms.text, + ...inputStyles, + ], + })} + + + + + + ) +} diff --git a/src/view/com/forms/InputDate/utils.ts b/src/view/com/forms/InputDate/utils.ts new file mode 100644 index 0000000000..c787272fe8 --- /dev/null +++ b/src/view/com/forms/InputDate/utils.ts @@ -0,0 +1,16 @@ +import {getLocales} from 'expo-localization' + +const LOCALE = getLocales()[0] + +// we need the date in the form yyyy-MM-dd to pass to the input +export function toSimpleDateString(date: Date | string): string { + const _date = typeof date === 'string' ? new Date(date) : date + return _date.toISOString().split('T')[0] +} + +export function localizeDate(date: Date | string): string { + const _date = typeof date === 'string' ? new Date(date) : date + return new Intl.DateTimeFormat(LOCALE.languageTag, { + timeZone: 'UTC', + }).format(_date) +} diff --git a/src/view/com/forms/types.ts b/src/view/com/forms/types.ts new file mode 100644 index 0000000000..9a4217579b --- /dev/null +++ b/src/view/com/forms/types.ts @@ -0,0 +1,22 @@ +import {AccessibilityProps, TextInputProps} from 'react-native' + +export type RequiredAccessibilityProps = Required + +export type BaseProps = Omit< + AccessibilityProps, + 'accessibilityLabel' | 'accessibilityHint' +> & + Pick< + RequiredAccessibilityProps, + 'accessibilityLabel' | 'accessibilityHint' + > & { + value: T + onChange: (value: T) => void + testID: string + label?: string + hasError?: boolean + /** + * **NOTE:** Available only on web + */ + autoFocus?: TextInputProps['autoFocus'] + } diff --git a/src/view/com/util/hooks/useInteractionState.ts b/src/view/com/util/hooks/useInteractionState.ts new file mode 100644 index 0000000000..653b1c10e6 --- /dev/null +++ b/src/view/com/util/hooks/useInteractionState.ts @@ -0,0 +1,21 @@ +import React from 'react' + +export function useInteractionState() { + const [state, setState] = React.useState(false) + + const onIn = React.useCallback(() => { + setState(true) + }, [setState]) + const onOut = React.useCallback(() => { + setState(false) + }, [setState]) + + return React.useMemo( + () => ({ + state, + onIn, + onOut, + }), + [state, onIn, onOut], + ) +} diff --git a/src/view/screens/DebugNew.tsx b/src/view/screens/DebugNew.tsx index d52e7a6e00..35f0e415d2 100644 --- a/src/view/screens/DebugNew.tsx +++ b/src/view/screens/DebugNew.tsx @@ -10,6 +10,7 @@ import {Button, ButtonText} from '#/view/com/Button' import {Link} from '#/view/com/Link' import {Text, H1, H2, H3, H4, H5, H6} from '#/view/com/Typography' import {InputText} from '#/view/com/forms/InputText' +import {InputDate, utils} from '#/view/com/forms/InputDate' import {Logo} from '#/view/icons/Logo' function ThemeSelector() { @@ -226,6 +227,21 @@ function Forms() { icon={Logo} suffix={() => .bksy.social} /> + + console.log(date)} + accessibilityLabel="Date" + accessibilityHint="Enter a date" + /> + console.log(date)} + accessibilityLabel="Date" + accessibilityHint="Enter a date" + /> ) } diff --git a/web/index.html b/web/index.html index 18b985bff5..70072f2ae2 100644 --- a/web/index.html +++ b/web/index.html @@ -43,6 +43,10 @@ height: calc(100% + env(safe-area-inset-top)); } + input::-webkit-date-and-time-value { + text-align: left; + } + /* Color theming */ :root { --text: black; From 0c8c37b2212f4694b85a7162fdb5ff91ece1d9c6 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 10 Jan 2024 14:36:47 -0600 Subject: [PATCH 09/68] Autofill styles --- bskyweb/templates/base.html | 15 +++++++++++++++ web/index.html | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html index ffc2adf0f8..089bdcfa0f 100644 --- a/bskyweb/templates/base.html +++ b/bskyweb/templates/base.html @@ -39,6 +39,21 @@ height: calc(100% + env(safe-area-inset-top)); } + /* Remove autofill styles on Webkit */ + input:-webkit-autofill, + input:-webkit-autofill:hover, + input:-webkit-autofill:focus, + textarea:-webkit-autofill, + textarea:-webkit-autofill:hover, + textarea:-webkit-autofill:focus, + select:-webkit-autofill, + select:-webkit-autofill:hover, + select:-webkit-autofill:focus { + border: 0; + -webkit-text-fill-color: transparent; + -webkit-box-shadow: none; + } + /* Force left-align date/time inputs on iOS mobile */ input::-webkit-date-and-time-value { text-align: left; } diff --git a/web/index.html b/web/index.html index 70072f2ae2..585d573536 100644 --- a/web/index.html +++ b/web/index.html @@ -43,6 +43,21 @@ height: calc(100% + env(safe-area-inset-top)); } + /* Remove autofill styles on Webkit */ + input:-webkit-autofill, + input:-webkit-autofill:hover, + input:-webkit-autofill:focus, + textarea:-webkit-autofill, + textarea:-webkit-autofill:hover, + textarea:-webkit-autofill:focus, + select:-webkit-autofill, + select:-webkit-autofill:hover, + select:-webkit-autofill:focus { + border: 0; + -webkit-text-fill-color: transparent; + -webkit-box-shadow: none; + } + /* Force left-align date/time inputs on iOS mobile */ input::-webkit-date-and-time-value { text-align: left; } From 24074dea50fad2dd0e0aa7c1cd09b1b008919381 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 10 Jan 2024 14:44:14 -0600 Subject: [PATCH 10/68] Clean up InputDate types --- src/view/com/forms/InputDate/index.android.tsx | 9 ++++----- src/view/com/forms/InputDate/index.tsx | 8 +++----- src/view/com/forms/InputDate/index.web.tsx | 6 ++---- src/view/com/forms/types.ts | 6 +----- 4 files changed, 10 insertions(+), 19 deletions(-) diff --git a/src/view/com/forms/InputDate/index.android.tsx b/src/view/com/forms/InputDate/index.android.tsx index 76ffa2e56e..a492ad7da9 100644 --- a/src/view/com/forms/InputDate/index.android.tsx +++ b/src/view/com/forms/InputDate/index.android.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {View, TextInputProps, TextStyle, Pressable} from 'react-native' +import {View, TextStyle, Pressable} from 'react-native' import DateTimePicker, { BaseProps as DateTimePickerProps, } from '@react-native-community/datetimepicker' @@ -8,14 +8,13 @@ import {Logo} from '#/view/icons/Logo' import {useTheme, atoms, tokens} from '#/alf' import {Text} from '#/view/com/Typography' import {useInteractionState} from '#/view/com/util/hooks/useInteractionState' -import {BaseProps} from '#/view/com/forms/types' + +import {InputDateProps} from '#/view/com/forms/InputDate/types' import { localizeDate, toSimpleDateString, } from '#/view/com/forms/InputDate/utils' -type Props = Omit & BaseProps - export * as utils from '#/view/com/forms/InputDate/utils' export function InputDate({ @@ -27,7 +26,7 @@ export function InputDate({ accessibilityLabel, accessibilityHint, ...props -}: Props) { +}: InputDateProps) { const labelId = React.useId() const t = useTheme() const [open, setOpen] = React.useState(false) diff --git a/src/view/com/forms/InputDate/index.tsx b/src/view/com/forms/InputDate/index.tsx index 6b3c8b40b9..eb642c3a08 100644 --- a/src/view/com/forms/InputDate/index.tsx +++ b/src/view/com/forms/InputDate/index.tsx @@ -1,16 +1,14 @@ import React from 'react' -import {View, TextInputProps} from 'react-native' +import {View} from 'react-native' import DateTimePicker, { DateTimePickerEvent, } from '@react-native-community/datetimepicker' import {useTheme, atoms} from '#/alf' import {Text} from '#/view/com/Typography' -import {BaseProps} from '#/view/com/forms/types' import {toSimpleDateString} from '#/view/com/forms/InputDate/utils' -type Props = Omit & BaseProps - +import {InputDateProps} from '#/view/com/forms/InputDate/types' export * as utils from '#/view/com/forms/InputDate/utils' /** @@ -27,7 +25,7 @@ export function InputDate({ label, accessibilityLabel, accessibilityHint, -}: Props) { +}: InputDateProps) { const labelId = React.useId() const t = useTheme() const [value, setValue] = React.useState(initialValue) diff --git a/src/view/com/forms/InputDate/index.web.tsx b/src/view/com/forms/InputDate/index.web.tsx index 994a7f509f..33a7473850 100644 --- a/src/view/com/forms/InputDate/index.web.tsx +++ b/src/view/com/forms/InputDate/index.web.tsx @@ -8,11 +8,9 @@ import {useTheme, atoms, tokens} from '#/alf' import {Text} from '#/view/com/Typography' import {useInteractionState} from '#/view/com/util/hooks/useInteractionState' -import {BaseProps} from '#/view/com/forms/types' +import {InputDateProps} from '#/view/com/forms/InputDate/types' import {toSimpleDateString} from '#/view/com/forms/InputDate/utils' -type Props = BaseProps - export * as utils from '#/view/com/forms/InputDate/utils' export function InputDate({ @@ -24,7 +22,7 @@ export function InputDate({ accessibilityLabel, accessibilityHint, ...props -}: Props) { +}: InputDateProps) { const labelId = React.useId() const t = useTheme() const [value, setValue] = React.useState(initialValue) diff --git a/src/view/com/forms/types.ts b/src/view/com/forms/types.ts index 9a4217579b..c43b760b0b 100644 --- a/src/view/com/forms/types.ts +++ b/src/view/com/forms/types.ts @@ -1,4 +1,4 @@ -import {AccessibilityProps, TextInputProps} from 'react-native' +import {AccessibilityProps} from 'react-native' export type RequiredAccessibilityProps = Required @@ -15,8 +15,4 @@ export type BaseProps = Omit< testID: string label?: string hasError?: boolean - /** - * **NOTE:** Available only on web - */ - autoFocus?: TextInputProps['autoFocus'] } From 9bd19a676274c8bcd3ff1f7330b168fc27dbb6ac Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 10 Jan 2024 14:54:02 -0600 Subject: [PATCH 11/68] Improve types for InputText, value handling --- src/view/com/forms/InputDate/types.ts | 10 ++++ src/view/com/forms/InputText.tsx | 76 +++++++++++++-------------- src/view/screens/DebugNew.tsx | 35 ++++++++++-- 3 files changed, 79 insertions(+), 42 deletions(-) create mode 100644 src/view/com/forms/InputDate/types.ts diff --git a/src/view/com/forms/InputDate/types.ts b/src/view/com/forms/InputDate/types.ts new file mode 100644 index 0000000000..8a8d8cd1db --- /dev/null +++ b/src/view/com/forms/InputDate/types.ts @@ -0,0 +1,10 @@ +import {TextInputProps} from 'react-native' + +import {BaseProps} from '#/view/com/forms/types' + +export type InputDateProps = BaseProps & { + /** + * **NOTE:** Available only on web + */ + autoFocus?: TextInputProps['autoFocus'] +} diff --git a/src/view/com/forms/InputText.tsx b/src/view/com/forms/InputText.tsx index eb77c6242f..c573a3c650 100644 --- a/src/view/com/forms/InputText.tsx +++ b/src/view/com/forms/InputText.tsx @@ -9,16 +9,23 @@ import { import {useTheme, atoms, web, tokens} from '#/alf' import {Text} from '#/view/com/Typography' +import {useInteractionState} from '#/view/com/util/hooks/useInteractionState' -type Props = Omit & { - label?: string - placeholder: string - hasError?: boolean - icon?: React.FunctionComponent - suffix?: React.FunctionComponent -} +import {BaseProps} from '#/view/com/forms/types' + +type Props = BaseProps & + Omit & { + placeholder: Required['placeholder'] + icon?: React.FunctionComponent + suffix?: React.FunctionComponent + } export function InputText({ + value: initialValue, + onChange, + testID, + accessibilityLabel, + accessibilityHint, label, hasError, icon: Icon, @@ -27,36 +34,15 @@ export function InputText({ }: Props) { const labelId = React.useId() const t = useTheme() - const [state, setState] = React.useState({ - hovered: false, - focused: false, - }) + const [value, setValue] = React.useState(initialValue) const [suffixPadding, setSuffixPadding] = React.useState(0) + const { + state: hovered, + onIn: onHoverIn, + onOut: onHoverOut, + } = useInteractionState() + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() - const onHoverIn = React.useCallback(() => { - setState(s => ({ - ...s, - hovered: true, - })) - }, [setState]) - const onHoverOut = React.useCallback(() => { - setState(s => ({ - ...s, - hovered: false, - })) - }, [setState]) - const onFocus = React.useCallback(() => { - setState(s => ({ - ...s, - focused: true, - })) - }, [setState]) - const onBlur = React.useCallback(() => { - setState(s => ({ - ...s, - focused: false, - })) - }, [setState]) const handleSuffixLayout = React.useCallback( (e: LayoutChangeEvent) => { setSuffixPadding(e.nativeEvent.layout.width + 16) @@ -83,7 +69,7 @@ export function InputText({ }) } - if (state.hovered || state.focused) { + if (hovered || focused) { input.push({ borderColor: t.atoms.border_contrast_500.borderColor, }) @@ -96,7 +82,16 @@ export function InputText({ } return {inputStyles: input, iconStyles: icon} - }, [t, state, hasError, Icon]) + }, [t, hovered, focused, hasError, Icon]) + + const handleOnChange = React.useCallback( + (e: any) => { + const value = e.currentTarget.value + onChange(value) + setValue(value) + }, + [onChange, setValue], + ) return ( @@ -114,12 +109,17 @@ export function InputText({ )} - - - - console.log(text)} + /> + console.log(text)} + /> + console.log(text)} + icon={Logo} + /> + console.log(text)} icon={Logo} suffix={() => .bksy.social} /> From 22f0c16cf1fe956ffbde331dad37eaa5270161de Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 10 Jan 2024 15:02:07 -0600 Subject: [PATCH 12/68] Enforce a11y props on buttons --- src/view/com/Button.tsx | 15 +++++++-- src/view/com/Link.tsx | 6 ++-- src/view/screens/DebugNew.tsx | 61 +++++++++++++++++++++++++++++------ 3 files changed, 67 insertions(+), 15 deletions(-) diff --git a/src/view/com/Button.tsx b/src/view/com/Button.tsx index 1d858a8eea..7b3beb7dca 100644 --- a/src/view/com/Button.tsx +++ b/src/view/com/Button.tsx @@ -5,6 +5,7 @@ import { PressableProps, TextProps, ViewStyle, + AccessibilityProps, } from 'react-native' import {useTheme, atoms, tokens, web, native} from '#/alf' @@ -22,7 +23,10 @@ export type VariantProps = { size?: ButtonSize } -export type ButtonProps = Omit & +export type ButtonProps = Omit< + PressableProps, + 'children' | 'style' | 'accessibilityLabel' | 'accessibilityHint' +> & VariantProps & { children: | ((props: { @@ -37,14 +41,17 @@ export type ButtonProps = Omit & }) => React.ReactNode) | React.ReactNode | string + accessibilityLabel: Required['accessibilityLabel'] + accessibilityHint: Required['accessibilityHint'] } export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean} export function Button({ children, - style, type, size, + accessibilityLabel, + accessibilityHint, disabled = false, ...rest }: ButtonProps) { @@ -179,6 +186,9 @@ export function Button({ () - const {href, accessibilityRole} = useLinkProps({ + const {href} = useLinkProps({ to: typeof to === 'string' ? convertBskyAppUrlIfNeeded(sanitizeUrl(to)) : to, }) @@ -139,9 +139,9 @@ export function Link({ return ( + - - - - - - External - +

External with custom children

https://blueskyweb.xyz Internal - + {({props}) => Link as a button}
From 2bac965da4b7d6f03e025b3175e6acdec823423a Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 10 Jan 2024 16:13:02 -0600 Subject: [PATCH 13/68] Add Dialog, Portal --- package.json | 1 + src/App.native.tsx | 6 +- src/App.web.tsx | 6 +- src/view/com/Dialog/index.tsx | 7 +++ src/view/com/Dialog/index.web.tsx | 96 +++++++++++++++++++++++++++++++ src/view/com/Dialog/types.ts | 4 ++ src/view/com/Portal.tsx | 58 +++++++++++++++++++ src/view/screens/DebugNew.tsx | 25 ++++++++ yarn.lock | 25 ++++++++ 9 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 src/view/com/Dialog/index.tsx create mode 100644 src/view/com/Dialog/index.web.tsx create mode 100644 src/view/com/Dialog/types.ts create mode 100644 src/view/com/Portal.tsx diff --git a/package.json b/package.json index 7e63ad9a62..01d90da90f 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@segment/analytics-react-native": "^2.10.1", "@segment/sovran-react-native": "^0.4.5", "@sentry/react-native": "5.5.0", + "@tamagui/focus-scope": "^1.84.1", "@tanstack/react-query": "^5.8.1", "@tiptap/core": "^2.0.0-beta.220", "@tiptap/extension-document": "^2.0.0-beta.220", diff --git a/src/App.native.tsx b/src/App.native.tsx index a9dfafe36f..39485ee24f 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -41,6 +41,7 @@ import { import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' import * as persisted from '#/state/persisted' import {Splash} from '#/Splash' +import {Provider as PortalProvider, Outlet as PortalOutlet} from '#/view/com/Portal' SplashScreen.preventAutoHideAsync() @@ -76,6 +77,7 @@ function InnerApp() { + @@ -113,7 +115,9 @@ function App() { - + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index 9ee9a4fd71..5760b0d4a2 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -30,6 +30,7 @@ import { } from 'state/session' import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' import * as persisted from '#/state/persisted' +import {Provider as PortalProvider, Outlet as PortalOutlet} from '#/view/com/Portal' function InnerApp() { const {isInitialLoad, currentAccount} = useSession() @@ -58,6 +59,7 @@ function InnerApp() { + @@ -94,7 +96,9 @@ function App() { - + + + diff --git a/src/view/com/Dialog/index.tsx b/src/view/com/Dialog/index.tsx new file mode 100644 index 0000000000..fc3f784704 --- /dev/null +++ b/src/view/com/Dialog/index.tsx @@ -0,0 +1,7 @@ +import React from 'react' + +import {DialogProps} from '#/view/com/Dialog/types' + +export function Dialog(props: React.PropsWithChildren) { + return null +} diff --git a/src/view/com/Dialog/index.web.tsx b/src/view/com/Dialog/index.web.tsx new file mode 100644 index 0000000000..9c6f09f4f3 --- /dev/null +++ b/src/view/com/Dialog/index.web.tsx @@ -0,0 +1,96 @@ +import React from 'react' +import {View, TouchableWithoutFeedback, DimensionValue} from 'react-native' +import {FocusScope} from '@tamagui/focus-scope' +import Animated, {FadeInDown, FadeIn, FadeOut} from 'react-native-reanimated' + +import {useTheme, atoms as a, useBreakpoints} from '#/alf' +import {Portal} from '#/view/com/Portal' +import {DialogProps} from '#/view/com/Dialog/types' + +const Context = React.createContext<{ + dismiss: () => void +}>({ + dismiss: () => {}, +}) + +export function useDialog() { + return React.useContext(Context) +} + +export function Dialog({ + isOpen, + onDismiss, + children, +}: React.PropsWithChildren) { + const t = useTheme() + const {gtMobile} = useBreakpoints() + + const dismiss = React.useCallback(() => { + onDismiss() + }, [onDismiss]) + + React.useEffect(() => { + function handler(e: KeyboardEvent) { + if (e.key === 'Escape') dismiss() + } + + document.addEventListener('keydown', handler) + + return () => document.removeEventListener('keydown', handler) + }, [dismiss]) + + const context = React.useMemo(() => ({dismiss}), [dismiss]) + + return ( + <> + {isOpen && ( + + + + + + + + + + {children} + + + + + + + + )} + + ) +} diff --git a/src/view/com/Dialog/types.ts b/src/view/com/Dialog/types.ts new file mode 100644 index 0000000000..eb9dc56865 --- /dev/null +++ b/src/view/com/Dialog/types.ts @@ -0,0 +1,4 @@ +export type DialogProps = { + isOpen: boolean + onDismiss: () => void +} diff --git a/src/view/com/Portal.tsx b/src/view/com/Portal.tsx new file mode 100644 index 0000000000..47a216bd80 --- /dev/null +++ b/src/view/com/Portal.tsx @@ -0,0 +1,58 @@ +import React from 'react' + +type Component = React.ReactElement + +type ContextType = { + outlet: Component | null + append(id: string, component: Component): void + remove(id: string): void +} + +type ComponentMap = { + [id: string]: Component +} + +export const Context = React.createContext({ + outlet: null, + append: () => {}, + remove: () => {}, +}) + +export function Provider(props: React.PropsWithChildren<{}>) { + const map = React.useRef({}) + const [outlet, setOutlet] = React.useState(null) + + const append = React.useCallback((id, component) => { + if (map.current[id]) return + map.current[id] = {component} + setOutlet(<>{Object.values(map.current)}) + }, []) + + const remove = React.useCallback(id => { + delete map.current[id] + setOutlet(<>{Object.values(map.current)}) + }, []) + + console.log(outlet) + + return ( + + {props.children} + + ) +} + +export function Outlet() { + const ctx = React.useContext(Context) + return ctx.outlet +} + +export function Portal({children}: React.PropsWithChildren<{}>) { + const {append, remove} = React.useContext(Context) + const id = React.useId() + React.useEffect(() => { + append(id, children as Component) + return () => remove(id) + }, [id, children, append, remove]) + return null +} diff --git a/src/view/screens/DebugNew.tsx b/src/view/screens/DebugNew.tsx index 444fe4f29a..ce7f5c20a8 100644 --- a/src/view/screens/DebugNew.tsx +++ b/src/view/screens/DebugNew.tsx @@ -12,6 +12,7 @@ import {Text, H1, H2, H3, H4, H5, H6} from '#/view/com/Typography' import {InputText} from '#/view/com/forms/InputText' import {InputDate, utils} from '#/view/com/forms/InputDate' import {Logo} from '#/view/icons/Logo' +import {Dialog} from '#/view/com/Dialog' function ThemeSelector() { const setColorMode = useSetColorMode() @@ -316,6 +317,28 @@ function Forms() { ) } +function Dialogs() { + const [isOpen, setIsOpen] = React.useState(false) + + console.log({isOpen}) + + return ( + <> + + setIsOpen(false)}> + + + + + ) +} + export function DebugScreen() { const t = useTheme() @@ -325,6 +348,8 @@ export function DebugScreen() { + + diff --git a/yarn.lock b/yarn.lock index c1db264bc6..7d0ceecd38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6921,6 +6921,31 @@ "@svgr/plugin-svgo" "^5.5.0" loader-utils "^2.0.0" +"@tamagui/compose-refs@1.84.1": + version "1.84.1" + resolved "https://registry.yarnpkg.com/@tamagui/compose-refs/-/compose-refs-1.84.1.tgz#244735edc3ac2e617389297f005d5bc25872465f" + integrity sha512-oZ0rUmQABlGm/QKQITxAW9WLV3qjyq1ehgoWcZVmtc1Kc/hkFQe2J+wRQV726CmTAnuUgUXi3eoNMwBVoZksfQ== + +"@tamagui/constants@1.84.1": + version "1.84.1" + resolved "https://registry.yarnpkg.com/@tamagui/constants/-/constants-1.84.1.tgz#62e41837dbe844d14e255f3eea9c2583044d2509" + integrity sha512-QmvyCqtEIugqXutQI35GJQ1hlpSapYCdOHx9QlgsOWjAY34pu55MaY/tDrQeQ0AUmI/qx30vy7TsCJxB4QFEoQ== + +"@tamagui/focus-scope@^1.84.1": + version "1.84.1" + resolved "https://registry.yarnpkg.com/@tamagui/focus-scope/-/focus-scope-1.84.1.tgz#e9f061184048c75f87da023f54b9c5abccdd460d" + integrity sha512-0E1Wc3jmKhafETfH1dUuJYmGK1bDNA/9TySbOeTjTToxUoL3V0G2W5JSwSMCDqR1Bl+xrGlGwzXTUhouw8qSog== + dependencies: + "@tamagui/compose-refs" "1.84.1" + "@tamagui/use-event" "1.84.1" + +"@tamagui/use-event@1.84.1": + version "1.84.1" + resolved "https://registry.yarnpkg.com/@tamagui/use-event/-/use-event-1.84.1.tgz#a095a1bde9c40c4a397226c57c3fa32f6018f504" + integrity sha512-U88WCxvMz7ZSfMFMJEFbG3tJjK/Lf+PHlmtYvlx1V+YiqRBoj5+milzoM8PclENn5vZMiJW0ozYRgzI/cdE7Eg== + dependencies: + "@tamagui/constants" "1.84.1" + "@tanstack/query-core@5.8.1": version "5.8.1" resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.8.1.tgz#5215a028370d9b2f32e83787a0ea119e2f977996" From 78a0f652347d262cfd910ad5dcf063a8f88df290 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 10 Jan 2024 16:45:42 -0600 Subject: [PATCH 14/68] Dialog contents --- src/view/com/Dialog/index.tsx | 14 +++++- src/view/com/Dialog/index.web.tsx | 71 +++++++++++++++++++++++++++++-- src/view/screens/DebugNew.tsx | 25 ++++++----- 3 files changed, 94 insertions(+), 16 deletions(-) diff --git a/src/view/com/Dialog/index.tsx b/src/view/com/Dialog/index.tsx index fc3f784704..2cf78022d2 100644 --- a/src/view/com/Dialog/index.tsx +++ b/src/view/com/Dialog/index.tsx @@ -2,6 +2,18 @@ import React from 'react' import {DialogProps} from '#/view/com/Dialog/types' -export function Dialog(props: React.PropsWithChildren) { +export function Outer(props: React.PropsWithChildren) { + return null +} + +export function Inner(props: React.PropsWithChildren<{}>) { + return null +} + +export function Header(props: React.PropsWithChildren<{ title: string }>) { + return null +} + +export function Close() { return null } diff --git a/src/view/com/Dialog/index.web.tsx b/src/view/com/Dialog/index.web.tsx index 9c6f09f4f3..a972a8f0d7 100644 --- a/src/view/com/Dialog/index.web.tsx +++ b/src/view/com/Dialog/index.web.tsx @@ -4,8 +4,11 @@ import {FocusScope} from '@tamagui/focus-scope' import Animated, {FadeInDown, FadeIn, FadeOut} from 'react-native-reanimated' import {useTheme, atoms as a, useBreakpoints} from '#/alf' +import {EventStopper} from '#/view/com/util/EventStopper' +import {H3, Text} from '#/view/com/Typography' import {Portal} from '#/view/com/Portal' import {DialogProps} from '#/view/com/Dialog/types' +import {Button} from '#/view/com/Button' const Context = React.createContext<{ dismiss: () => void @@ -17,7 +20,7 @@ export function useDialog() { return React.useContext(Context) } -export function Dialog({ +export function Outer({ isOpen, onDismiss, children, @@ -63,7 +66,7 @@ export function Dialog({ style={[ a.absolute, a.inset_0, - t.atoms.bg_contrast_100, + t.atoms.bg_contrast_200, {opacity: 0.8}, ]} /> @@ -81,8 +84,11 @@ export function Dialog({ - {children} + aria-role="dialog" + > + + {children} + @@ -94,3 +100,60 @@ export function Dialog({ ) } + +export function Inner(props: React.PropsWithChildren<{}>) { + const t = useTheme() + return ( + + {props.children} + + ) +} + +export function Header({ children, title }: React.PropsWithChildren<{ title: string }>) { + const t = useTheme() + return ( + +

{title}

+ {children} +
+ ) +} + +export function Close() { + const t = useTheme() + const {dismiss} = useDialog() + return ( + + + + ) +} diff --git a/src/view/screens/DebugNew.tsx b/src/view/screens/DebugNew.tsx index ce7f5c20a8..b72407d0d9 100644 --- a/src/view/screens/DebugNew.tsx +++ b/src/view/screens/DebugNew.tsx @@ -12,7 +12,7 @@ import {Text, H1, H2, H3, H4, H5, H6} from '#/view/com/Typography' import {InputText} from '#/view/com/forms/InputText' import {InputDate, utils} from '#/view/com/forms/InputDate' import {Logo} from '#/view/icons/Logo' -import {Dialog} from '#/view/com/Dialog' +import * as Dialog from '#/view/com/Dialog' function ThemeSelector() { const setColorMode = useSetColorMode() @@ -320,21 +320,24 @@ function Forms() { function Dialogs() { const [isOpen, setIsOpen] = React.useState(false) - console.log({isOpen}) - return ( <> - setIsOpen(false)}> - - - + setIsOpen(false)}> + + + + + + + + ) } From ad6d9c4d9fcf9aa6847cfc93a31f4fdc7a6b75b8 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 10 Jan 2024 19:19:35 -0600 Subject: [PATCH 15/68] Native dialog --- src/view/com/Dialog/index.tsx | 120 ++++++++++++++++++++++++++-- src/view/com/Dialog/index.web.tsx | 126 +++++++++++++++++------------- src/view/com/Dialog/types.ts | 11 ++- src/view/screens/DebugNew.tsx | 25 +++--- 4 files changed, 209 insertions(+), 73 deletions(-) diff --git a/src/view/com/Dialog/index.tsx b/src/view/com/Dialog/index.tsx index 2cf78022d2..229cd7b867 100644 --- a/src/view/com/Dialog/index.tsx +++ b/src/view/com/Dialog/index.tsx @@ -1,17 +1,123 @@ -import React from 'react' +import React, {useImperativeHandle} from 'react' +import {View, Dimensions} from 'react-native' +import BottomSheet, {BottomSheetBackdrop} from '@gorhom/bottom-sheet' -import {DialogProps} from '#/view/com/Dialog/types' +import {useTheme, atoms as a} from '#/alf' +import {Portal} from '#/view/com/Portal' -export function Outer(props: React.PropsWithChildren) { - return null +import {DialogProps, DialogControl} from '#/view/com/Dialog/types' + +const Context = React.createContext<{ + close: () => void +}>({ + close: () => {}, +}) + +export function useDialogControl() { + const control = React.useRef({ + open: () => {}, + close: () => {}, + }) + + return control +} + +export function useDialog() { + return React.useContext(Context) +} + +export function Outer({ + control, + onClose, + children, +}: React.PropsWithChildren) { + const t = useTheme() + const sheet = React.useRef(null) + + const open = React.useCallback((i = 0) => { + sheet.current?.snapToIndex(i) + }, []) + + const close = React.useCallback(() => { + sheet.current?.close() + onClose?.() + }, [onClose]) + + useImperativeHandle( + control, + () => ({ + open, + close, + }), + [open, close], + ) + + return ( + + ( + + )} + handleIndicatorStyle={{backgroundColor: t.palette.primary}} + handleStyle={{display: 'none'}} + onChange={() => {}} + onClose={onClose}> + {children} + + + ) } export function Inner(props: React.PropsWithChildren<{}>) { - return null + const t = useTheme() + return ( + + {props.children} + + ) } -export function Header(props: React.PropsWithChildren<{ title: string }>) { - return null +export function Handle() { + const t = useTheme() + return ( + + ) } export function Close() { diff --git a/src/view/com/Dialog/index.web.tsx b/src/view/com/Dialog/index.web.tsx index a972a8f0d7..b27c757029 100644 --- a/src/view/com/Dialog/index.web.tsx +++ b/src/view/com/Dialog/index.web.tsx @@ -1,48 +1,73 @@ -import React from 'react' +import React, {useImperativeHandle} from 'react' import {View, TouchableWithoutFeedback, DimensionValue} from 'react-native' import {FocusScope} from '@tamagui/focus-scope' import Animated, {FadeInDown, FadeIn, FadeOut} from 'react-native-reanimated' import {useTheme, atoms as a, useBreakpoints} from '#/alf' import {EventStopper} from '#/view/com/util/EventStopper' -import {H3, Text} from '#/view/com/Typography' +import {Text} from '#/view/com/Typography' import {Portal} from '#/view/com/Portal' -import {DialogProps} from '#/view/com/Dialog/types' import {Button} from '#/view/com/Button' +import {DialogProps, DialogControl} from '#/view/com/Dialog/types' + const Context = React.createContext<{ - dismiss: () => void + close: () => void }>({ - dismiss: () => {}, + close: () => {}, }) +export function useDialogControl() { + const control = React.useRef({ + open: () => {}, + close: () => {}, + }) + + return control +} + export function useDialog() { return React.useContext(Context) } export function Outer({ - isOpen, - onDismiss, + control, + onClose, children, }: React.PropsWithChildren) { const t = useTheme() const {gtMobile} = useBreakpoints() + const [isOpen, setIsOpen] = React.useState(false) + + const open = React.useCallback(() => { + setIsOpen(true) + }, [setIsOpen]) - const dismiss = React.useCallback(() => { - onDismiss() - }, [onDismiss]) + const close = React.useCallback(() => { + setIsOpen(false) + onClose?.() + }, [onClose, setIsOpen]) + + useImperativeHandle( + control, + () => ({ + open, + close, + }), + [open, close], + ) React.useEffect(() => { function handler(e: KeyboardEvent) { - if (e.key === 'Escape') dismiss() + if (e.key === 'Escape') close() } document.addEventListener('keydown', handler) return () => document.removeEventListener('keydown', handler) - }, [dismiss]) + }, [close]) - const context = React.useMemo(() => ({dismiss}), [dismiss]) + const context = React.useMemo(() => ({close}), [close]) return ( <> @@ -51,7 +76,7 @@ export function Outer({ + onPress={close}> - - {children} - + aria-role="dialog"> + {children} @@ -104,52 +126,46 @@ export function Outer({ export function Inner(props: React.PropsWithChildren<{}>) { const t = useTheme() return ( - + {props.children} ) } -export function Header({ children, title }: React.PropsWithChildren<{ title: string }>) { - const t = useTheme() - return ( - -

{title}

- {children} -
- ) +export function Handle() { + return null } export function Close() { const t = useTheme() - const {dismiss} = useDialog() + const {close} = useDialog() return ( - - - setIsOpen(false)}> + + + - - - - From 1da54e53641d4a14a7ac29654ed2be4c55c65579 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 10 Jan 2024 19:33:27 -0600 Subject: [PATCH 16/68] Clean up --- src/view/com/Dialog/index.web.tsx | 29 +++++++++++++++++++---------- src/view/com/Portal.tsx | 2 -- src/view/screens/DebugNew.tsx | 27 ++++++++++++++++++--------- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/view/com/Dialog/index.web.tsx b/src/view/com/Dialog/index.web.tsx index b27c757029..f1576d6f33 100644 --- a/src/view/com/Dialog/index.web.tsx +++ b/src/view/com/Dialog/index.web.tsx @@ -4,13 +4,14 @@ import {FocusScope} from '@tamagui/focus-scope' import Animated, {FadeInDown, FadeIn, FadeOut} from 'react-native-reanimated' import {useTheme, atoms as a, useBreakpoints} from '#/alf' -import {EventStopper} from '#/view/com/util/EventStopper' import {Text} from '#/view/com/Typography' import {Portal} from '#/view/com/Portal' import {Button} from '#/view/com/Button' import {DialogProps, DialogControl} from '#/view/com/Dialog/types' +const stopPropagation = (e: any) => e.stopPropagation() + const Context = React.createContext<{ close: () => void }>({ @@ -58,6 +59,8 @@ export function Outer({ ) React.useEffect(() => { + if (!isOpen) return + function handler(e: KeyboardEvent) { if (e.key === 'Escape') close() } @@ -65,7 +68,7 @@ export function Outer({ document.addEventListener('keydown', handler) return () => document.removeEventListener('keydown', handler) - }, [close]) + }, [isOpen, close]) const context = React.useMemo(() => ({close}), [close]) @@ -105,14 +108,20 @@ export function Outer({ paddingTop: gtMobile ? ('10vh' as DimensionValue) : 0, }, ]}> - - - {children} - - + true} + onTouchEnd={stopPropagation}> + + + {children} + + +
diff --git a/src/view/com/Portal.tsx b/src/view/com/Portal.tsx index 47a216bd80..1813d9e05e 100644 --- a/src/view/com/Portal.tsx +++ b/src/view/com/Portal.tsx @@ -33,8 +33,6 @@ export function Provider(props: React.PropsWithChildren<{}>) { setOutlet(<>{Object.values(map.current)}) }, []) - console.log(outlet) - return ( {props.children} diff --git a/src/view/screens/DebugNew.tsx b/src/view/screens/DebugNew.tsx index b6598cf669..24538bcb33 100644 --- a/src/view/screens/DebugNew.tsx +++ b/src/view/screens/DebugNew.tsx @@ -334,15 +334,24 @@ function Dialogs() { - - + + + + From c100f04a68b458647548f29e74d4bc1e118d62a3 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 11 Jan 2024 13:05:32 -0600 Subject: [PATCH 17/68] Fix animations --- src/view/com/Dialog/index.web.tsx | 45 +++++++++++++++++++------------ 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/src/view/com/Dialog/index.web.tsx b/src/view/com/Dialog/index.web.tsx index f1576d6f33..3d1a267ceb 100644 --- a/src/view/com/Dialog/index.web.tsx +++ b/src/view/com/Dialog/index.web.tsx @@ -39,13 +39,17 @@ export function Outer({ const t = useTheme() const {gtMobile} = useBreakpoints() const [isOpen, setIsOpen] = React.useState(false) + const [isVisible, setIsVisible] = React.useState(true) const open = React.useCallback(() => { setIsOpen(true) }, [setIsOpen]) - const close = React.useCallback(() => { + const close = React.useCallback(async () => { + setIsVisible(false) + await new Promise(resolve => setTimeout(resolve, 150)) setIsOpen(false) + setIsVisible(true) onClose?.() }, [onClose, setIsOpen]) @@ -88,16 +92,18 @@ export function Outer({ a.flex_row, a.justify_center, ]}> - + {isVisible && ( + + )} true} onTouchEnd={stopPropagation}> - - {children} - + {isVisible ? ( + + {children} + + ) : ( + + )}
From bf1641d03aa91037f0b197e9685fbcc195857196 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 11 Jan 2024 14:46:03 -0600 Subject: [PATCH 18/68] Improvements to web modal, exiting still broken --- src/alf/atoms.ts | 3 ++ src/view/com/Dialog/index.web.tsx | 74 +++++++++++++++++-------------- src/view/screens/DebugNew.tsx | 1 + 3 files changed, 44 insertions(+), 34 deletions(-) diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index c142f5f716..15972ce67d 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -4,6 +4,9 @@ export const atoms = { /* * Positioning */ + fixed: { + position: 'fixed', + }, absolute: { position: 'absolute', }, diff --git a/src/view/com/Dialog/index.web.tsx b/src/view/com/Dialog/index.web.tsx index 3d1a267ceb..ea5e309da0 100644 --- a/src/view/com/Dialog/index.web.tsx +++ b/src/view/com/Dialog/index.web.tsx @@ -1,9 +1,9 @@ import React, {useImperativeHandle} from 'react' -import {View, TouchableWithoutFeedback, DimensionValue} from 'react-native' +import {View, TouchableWithoutFeedback, ViewStyle} from 'react-native' import {FocusScope} from '@tamagui/focus-scope' -import Animated, {FadeInDown, FadeIn, FadeOut} from 'react-native-reanimated' +import Animated, {FadeInDown, FadeIn} from 'react-native-reanimated' -import {useTheme, atoms as a, useBreakpoints} from '#/alf' +import {useTheme, atoms as a, useBreakpoints, web} from '#/alf' import {Text} from '#/view/com/Typography' import {Portal} from '#/view/com/Portal' import {Button} from '#/view/com/Button' @@ -86,20 +86,21 @@ export function Outer({ onPress={close}> {isVisible && ( @@ -109,30 +110,13 @@ export function Outer({ style={[ a.w_full, a.z_20, + a.justify_center, + a.align_center, { - maxWidth: 600, - paddingTop: gtMobile ? ('10vh' as DimensionValue) : 0, + minHeight: web('calc(90vh - 36px)') || undefined, }, ]}> - true} - onTouchEnd={stopPropagation}> - - {isVisible ? ( - - {children} - - ) : ( - - )} - - + {isVisible ? children : null}
@@ -143,12 +127,34 @@ export function Outer({ ) } -export function Inner(props: React.PropsWithChildren<{}>) { +export function Inner({ + children, + style, +}: React.PropsWithChildren<{style?: ViewStyle}>) { const t = useTheme() + const {gtMobile} = useBreakpoints() return ( - - {props.children} - + + true} + onTouchEnd={stopPropagation} + entering={FadeInDown.duration(100)} + // exiting={FadeOut.duration(100)} + style={[ + a.relative, + a.rounded_md, + a.w_full, + gtMobile ? a.p_xl : a.p_lg, + t.atoms.bg, + {maxWidth: 600}, + ...(Array.isArray(style) ? style : [style || {}]), + ]}> + {children} + + ) } diff --git a/src/view/screens/DebugNew.tsx b/src/view/screens/DebugNew.tsx index 24538bcb33..b6bd1f2c89 100644 --- a/src/view/screens/DebugNew.tsx +++ b/src/view/screens/DebugNew.tsx @@ -343,6 +343,7 @@ function Dialogs() { accessibilityHint="Open basic dialog"> Close basic dialog + - + + ) +} + +export function Action({ + children, + onPress, +}: React.PropsWithChildren<{onPress?: () => void}>) { + const {close} = Dialog.useDialogContext() + const handleOnPress = React.useCallback(() => { + close() + onPress?.() + }, [close, onPress]) + return ( + + ) +} diff --git a/src/view/com/Typography.tsx b/src/view/com/Typography.tsx index 6579c2e511..029fc2e24f 100644 --- a/src/view/com/Typography.tsx +++ b/src/view/com/Typography.tsx @@ -1,6 +1,7 @@ import React from 'react' import {Text as RNText, TextProps} from 'react-native' -import {useTheme, atoms, web} from '#/alf' + +import {useTheme, atoms, web, flatten} from '#/alf' export function Text({style, ...rest}: TextProps) { const t = useTheme() @@ -18,7 +19,7 @@ export function H1({style, ...rest}: TextProps) { ) } @@ -34,7 +35,7 @@ export function H2({style, ...rest}: TextProps) { ) } @@ -50,7 +51,7 @@ export function H3({style, ...rest}: TextProps) { ) } @@ -66,7 +67,7 @@ export function H4({style, ...rest}: TextProps) { ) } @@ -82,7 +83,7 @@ export function H5({style, ...rest}: TextProps) { ) } @@ -98,7 +99,26 @@ export function H6({style, ...rest}: TextProps) { + ) +} + +export function P({style, ...rest}: TextProps) { + const t = useTheme() + const attr = + web({ + role: 'paragraph', + }) || {} + const _style = flatten(style) + const lineHeight = + (_style?.lineHeight || atoms.text_md.lineHeight) * + atoms.leading_normal.lineHeight + return ( + ) } diff --git a/src/view/screens/DebugNew.tsx b/src/view/screens/DebugNew.tsx index 6abcc438b7..27b993c95e 100644 --- a/src/view/screens/DebugNew.tsx +++ b/src/view/screens/DebugNew.tsx @@ -8,32 +8,39 @@ import * as tokens from '#/alf/tokens' import {atoms as a, useTheme, useBreakpoints, ThemeProvider as Alf} from '#/alf' import {Button, ButtonText} from '#/view/com/Button' import {Link} from '#/view/com/Link' -import {Text, H1, H2, H3, H4, H5, H6} from '#/view/com/Typography' +import {Text, H1, H2, H3, H4, H5, H6, P} from '#/view/com/Typography' import {InputText} from '#/view/com/forms/InputText' import {InputDate, utils} from '#/view/com/forms/InputDate' import {Logo} from '#/view/icons/Logo' import * as Dialog from '#/view/com/Dialog' +import * as Prompt from '#/view/com/Prompt' function ThemeSelector() { const setColorMode = useSetColorMode() return ( - + @@ -319,39 +326,58 @@ function Forms() { function Dialogs() { const control = Dialog.useDialogControl() + const prompt = Prompt.usePromptControl() return ( <> + + + + Are you sure? + + This action cannot be undone. This action cannot be undone. This + action cannot be undone. + + + Cancel + Confirm + + + - + - - - +

Dialog

+

Description

+ + +
From e85df5415605886cd49ef7b56a9ddfab83f433ec Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 12 Jan 2024 11:22:29 -0600 Subject: [PATCH 21/68] Integrate new design tokens, reorg storybook --- src/Navigation.tsx | 4 +- src/alf/atoms.ts | 312 ++++++-- src/alf/themes.ts | 86 +- src/alf/tokens.ts | 112 +-- src/view/com/Button.tsx | 2 +- src/view/com/Dialog/index.tsx | 2 +- src/view/com/Prompt.tsx | 4 +- src/view/com/Typography.tsx | 12 +- .../com/forms/InputDate/index.android.tsx | 4 +- src/view/com/forms/InputDate/index.tsx | 4 +- src/view/com/forms/InputDate/index.web.tsx | 4 +- src/view/com/forms/InputText.tsx | 4 +- src/view/icons/Logo.tsx | 5 +- src/view/screens/Debug.tsx | 520 ------------ src/view/screens/DebugNew.tsx | 746 ------------------ src/view/screens/Storybook/Breakpoints.tsx | 25 + src/view/screens/Storybook/Buttons.tsx | 120 +++ src/view/screens/Storybook/Dialogs.tsx | 69 ++ src/view/screens/Storybook/Forms.tsx | 66 ++ src/view/screens/Storybook/Palette.tsx | 279 +++++++ src/view/screens/Storybook/Spacing.tsx | 64 ++ src/view/screens/Storybook/Theming.tsx | 56 ++ src/view/screens/Storybook/Typography.tsx | 29 + src/view/screens/Storybook/index.tsx | 69 ++ 24 files changed, 1137 insertions(+), 1461 deletions(-) delete mode 100644 src/view/screens/Debug.tsx delete mode 100644 src/view/screens/DebugNew.tsx create mode 100644 src/view/screens/Storybook/Breakpoints.tsx create mode 100644 src/view/screens/Storybook/Buttons.tsx create mode 100644 src/view/screens/Storybook/Dialogs.tsx create mode 100644 src/view/screens/Storybook/Forms.tsx create mode 100644 src/view/screens/Storybook/Palette.tsx create mode 100644 src/view/screens/Storybook/Spacing.tsx create mode 100644 src/view/screens/Storybook/Theming.tsx create mode 100644 src/view/screens/Storybook/Typography.tsx create mode 100644 src/view/screens/Storybook/index.tsx diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 76a893c68c..534ba28d5b 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -61,7 +61,7 @@ import {ProfileListScreen} from './view/screens/ProfileList' import {PostThreadScreen} from './view/screens/PostThread' import {PostLikedByScreen} from './view/screens/PostLikedBy' import {PostRepostedByScreen} from './view/screens/PostRepostedBy' -import {DebugScreen} from './view/screens/DebugNew' +import {Storybook} from './view/screens/Storybook' import {LogScreen} from './view/screens/Log' import {SupportScreen} from './view/screens/Support' import {PrivacyPolicyScreen} from './view/screens/PrivacyPolicy' @@ -191,7 +191,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { /> DebugScreen} + getComponent={() => Storybook} options={{title: title('Debug'), requireAuth: true}} /> + style={[atoms.font_bold, atoms.text_center, ...textStyles, style]}> {children}
) diff --git a/src/view/com/Dialog/index.tsx b/src/view/com/Dialog/index.tsx index 7f7a630a14..5a832e6fe4 100644 --- a/src/view/com/Dialog/index.tsx +++ b/src/view/com/Dialog/index.tsx @@ -91,7 +91,7 @@ export function Inner(props: DialogInnerProps) { ) { return (

+ style={[a.font_bold, t.atoms.text_contrast_600, a.pb_sm]}> {children}

) @@ -82,7 +82,7 @@ export function Description({children}: React.PropsWithChildren<{}>) { const t = useTheme() const {descriptionId} = React.useContext(Context) return ( -

+

{children}

) diff --git a/src/view/com/Typography.tsx b/src/view/com/Typography.tsx index 029fc2e24f..66cf0720de 100644 --- a/src/view/com/Typography.tsx +++ b/src/view/com/Typography.tsx @@ -19,7 +19,7 @@ export function H1({style, ...rest}: TextProps) { ) } @@ -35,7 +35,7 @@ export function H2({style, ...rest}: TextProps) { ) } @@ -51,7 +51,7 @@ export function H3({style, ...rest}: TextProps) { ) } @@ -67,7 +67,7 @@ export function H4({style, ...rest}: TextProps) { ) } @@ -83,7 +83,7 @@ export function H5({style, ...rest}: TextProps) { ) } @@ -99,7 +99,7 @@ export function H6({style, ...rest}: TextProps) { ) } diff --git a/src/view/com/forms/InputDate/index.android.tsx b/src/view/com/forms/InputDate/index.android.tsx index a492ad7da9..13b1ac25d2 100644 --- a/src/view/com/forms/InputDate/index.android.tsx +++ b/src/view/com/forms/InputDate/index.android.tsx @@ -87,8 +87,8 @@ export function InputDate({ nativeID={labelId} style={[ atoms.text_sm, - atoms.font_semibold, - t.atoms.text_contrast_700, + atoms.font_bold, + t.atoms.text_contrast_600, atoms.mb_sm, ]}> {label} diff --git a/src/view/com/forms/InputDate/index.tsx b/src/view/com/forms/InputDate/index.tsx index eb642c3a08..89a19560b7 100644 --- a/src/view/com/forms/InputDate/index.tsx +++ b/src/view/com/forms/InputDate/index.tsx @@ -48,8 +48,8 @@ export function InputDate({ nativeID={labelId} style={[ atoms.text_sm, - atoms.font_semibold, - t.atoms.text_contrast_700, + atoms.font_bold, + t.atoms.text_contrast_600, atoms.mb_sm, ]}> {label} diff --git a/src/view/com/forms/InputDate/index.web.tsx b/src/view/com/forms/InputDate/index.web.tsx index 33a7473850..87e41e8e65 100644 --- a/src/view/com/forms/InputDate/index.web.tsx +++ b/src/view/com/forms/InputDate/index.web.tsx @@ -85,8 +85,8 @@ export function InputDate({ nativeID={labelId} style={[ atoms.text_sm, - atoms.font_semibold, - t.atoms.text_contrast_700, + atoms.font_bold, + t.atoms.text_contrast_600, atoms.mb_sm, ]}> {label} diff --git a/src/view/com/forms/InputText.tsx b/src/view/com/forms/InputText.tsx index c573a3c650..f66f817422 100644 --- a/src/view/com/forms/InputText.tsx +++ b/src/view/com/forms/InputText.tsx @@ -100,8 +100,8 @@ export function InputText({ nativeID={labelId} style={[ atoms.text_sm, - atoms.font_semibold, - t.atoms.text_contrast_700, + atoms.font_bold, + t.atoms.text_contrast_600, atoms.mb_sm, ]}> {label} diff --git a/src/view/icons/Logo.tsx b/src/view/icons/Logo.tsx index 9f9f57fc85..9212381a9e 100644 --- a/src/view/icons/Logo.tsx +++ b/src/view/icons/Logo.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {StyleSheet} from 'react-native' +import {StyleSheet, TextProps} from 'react-native' import Svg, { Path, Defs, @@ -15,7 +15,8 @@ const ratio = 57 / 64 type Props = { fill?: PathProps['fill'] -} & SvgProps + style?: TextProps['style'] +} & Omit export const Logo = React.forwardRef(function LogoImpl(props: Props, ref) { const {fill, ...rest} = props diff --git a/src/view/screens/Debug.tsx b/src/view/screens/Debug.tsx deleted file mode 100644 index 0e0464200e..0000000000 --- a/src/view/screens/Debug.tsx +++ /dev/null @@ -1,520 +0,0 @@ -import React from 'react' -import {ScrollView, View} from 'react-native' -import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' -import {ViewHeader} from '../com/util/ViewHeader' -import {ThemeProvider, PaletteColorName} from 'lib/ThemeContext' -import {usePalette} from 'lib/hooks/usePalette' -import {s} from 'lib/styles' -import * as Toast from 'view/com/util/Toast' -import {Text} from '../com/util/text/Text' -import {ViewSelector} from '../com/util/ViewSelector' -import {EmptyState} from '../com/util/EmptyState' -import * as LoadingPlaceholder from '../com/util/LoadingPlaceholder' -import {Button, ButtonType} from '../com/util/forms/Button' -import {DropdownButton, DropdownItem} from '../com/util/forms/DropdownButton' -import {ToggleButton} from '../com/util/forms/ToggleButton' -import {RadioGroup} from '../com/util/forms/RadioGroup' -import {ErrorScreen} from '../com/util/error/ErrorScreen' -import {ErrorMessage} from '../com/util/error/ErrorMessage' - -const MAIN_VIEWS = ['Base', 'Controls', 'Error', 'Notifs'] - -export const DebugScreen = ({}: NativeStackScreenProps< - CommonNavigatorParams, - 'Debug' ->) => { - const [colorScheme, setColorScheme] = React.useState<'light' | 'dark'>( - 'light', - ) - const onToggleColorScheme = () => { - setColorScheme(colorScheme === 'light' ? 'dark' : 'light') - } - return ( - - - - ) -} - -function DebugInner({ - colorScheme, - onToggleColorScheme, -}: { - colorScheme: 'light' | 'dark' - onToggleColorScheme: () => void -}) { - const [currentView, setCurrentView] = React.useState(0) - const pal = usePalette('default') - - const renderItem = (item: any) => { - return ( - - - - - {item.currentView === 3 ? ( - - ) : item.currentView === 2 ? ( - - ) : item.currentView === 1 ? ( - - ) : ( - - )} - - ) - } - - const items = [{currentView}] - - return ( - - - - - ) -} - -function Heading({label}: {label: string}) { - const pal = usePalette('default') - return ( - - - {label} - - - ) -} - -function BaseView() { - return ( - - - - - - - - - - - - - - - - ) -} - -function ControlsView() { - return ( - - - - - - - - - - - - ) -} - -function ErrorView() { - return ( - - - {}} - /> - - - - - - - - - {}} - /> - - - {}} - numberOfLines={1} - /> - - - ) -} - -function NotifsView() { - const triggerPush = () => { - // TODO: implement local notification for testing - } - const triggerToast = () => { - Toast.show('The task has been completed') - } - const triggerToast2 = () => { - Toast.show('The task has been completed successfully and with no problems') - } - return ( - - - - - - - ) -} - -function BreakpointDebugger() { - const t = useTheme() - const breakpoints = useBreakpoints() - - return ( - -

Breakpoint Debugger

- - Current breakpoint: {!breakpoints.gtMobile && mobile} - {breakpoints.gtMobile && !breakpoints.gtTablet && tablet} - {breakpoints.gtTablet && desktop} - - - {JSON.stringify(breakpoints, null, 2)} - -
- ) -} - -function ThemedSection() { - const t = useTheme() - - return ( - -

theme.atoms.text

- -

- theme.atoms.text_contrast_700 -

- -

- theme.atoms.text_contrast_500 -

- - - - - theme.bg - - - theme.bg_contrast_100 - - - - - theme.bg_contrast_200 - - - theme.bg_contrast_300 - - - - - theme.bg_positive - - - theme.bg_negative - - - - ) -} - -export function Buttons() { - const t = useTheme() - - return ( - - - - - - - - - - - - - - - - - External - - -

External with custom children

- - - https://blueskyweb.xyz - - - Internal - - - - {({props}) => Link as a button} - -
- ) -} - -function Forms() { - return ( - - console.log(text)} - /> - console.log(text)} - /> - console.log(text)} - icon={Logo} - /> - console.log(text)} - icon={Logo} - suffix={() => .bksy.social} - /> - - console.log(date)} - accessibilityLabel="Date" - accessibilityHint="Enter a date" - /> - console.log(date)} - accessibilityLabel="Date" - accessibilityHint="Enter a date" - /> - - ) -} - -function Dialogs() { - const control = Dialog.useDialogControl() - const prompt = Prompt.usePromptControl() - - return ( - <> - - - - - - Are you sure? - - This action cannot be undone. This action cannot be undone. This - action cannot be undone. - - - Cancel - Confirm - - - - - - - -

Dialog

-

Description

- - - -
-
-
- - ) -} - -export function DebugScreen() { - const t = useTheme() - - return ( - - - - - - - - - - - - - - - - - -

Heading 1

-

Heading 2

-

Heading 3

-

Heading 4

-
Heading 5
-
Heading 6
- - atoms.text_xxl - atoms.text_xl - atoms.text_lg - atoms.text_md - atoms.text_sm - atoms.text_xs - atoms.text_xxs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Spacing

- - - - xxs (2px) - - - - - xs (4px) - - - - - sm (8px) - - - - - md (12px) - - - - - lg (18px) - - - - - xl (24px) - - - - - xxl (32px) - - - - - - - -
-
- ) -} diff --git a/src/view/screens/Storybook/Breakpoints.tsx b/src/view/screens/Storybook/Breakpoints.tsx new file mode 100644 index 0000000000..34a9dbfd6e --- /dev/null +++ b/src/view/screens/Storybook/Breakpoints.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a, useTheme, useBreakpoints} from '#/alf' +import {Text, H3} from '#/view/com/Typography' + +export function Breakpoints() { + const t = useTheme() + const breakpoints = useBreakpoints() + + return ( + +

Breakpoint Debugger

+ + Current breakpoint: {!breakpoints.gtMobile && mobile} + {breakpoints.gtMobile && !breakpoints.gtTablet && tablet} + {breakpoints.gtTablet && desktop} + + + {JSON.stringify(breakpoints, null, 2)} + +
+ ) +} diff --git a/src/view/screens/Storybook/Buttons.tsx b/src/view/screens/Storybook/Buttons.tsx new file mode 100644 index 0000000000..41993cc29b --- /dev/null +++ b/src/view/screens/Storybook/Buttons.tsx @@ -0,0 +1,120 @@ +import React from 'react' +import {View} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' + +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonText} from '#/view/com/Button' +import {Link} from '#/view/com/Link' +import {Text, H3} from '#/view/com/Typography' + +export function Buttons() { + const t = useTheme() + + return ( + + + + + + + + + + + + + + + + + External + + +

External with custom children

+ + + https://blueskyweb.xyz + + + Internal + + + + {({props}) => Link as a button} + +
+ ) +} diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx new file mode 100644 index 0000000000..e0fcf0f811 --- /dev/null +++ b/src/view/screens/Storybook/Dialogs.tsx @@ -0,0 +1,69 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a} from '#/alf' +import {Button} from '#/view/com/Button' +import {H3, P} from '#/view/com/Typography' +import * as Dialog from '#/view/com/Dialog' +import * as Prompt from '#/view/com/Prompt' + +export function Dialogs() { + const control = Dialog.useDialogControl() + const prompt = Prompt.usePromptControl() + + return ( + <> + + + + + + Are you sure? + + This action cannot be undone. This action cannot be undone. This + action cannot be undone. + + + Cancel + Confirm + + + + + + + +

Dialog

+

Description

+ + + +
+
+
+ + ) +} diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx new file mode 100644 index 0000000000..198b0582ec --- /dev/null +++ b/src/view/screens/Storybook/Forms.tsx @@ -0,0 +1,66 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a} from '#/alf' +import {Text} from '#/view/com/Typography' +import {InputText} from '#/view/com/forms/InputText' +import {InputDate, utils} from '#/view/com/forms/InputDate' +import {Logo} from '#/view/icons/Logo' + +export function Forms() { + return ( + + console.log(text)} + /> + console.log(text)} + /> + console.log(text)} + icon={Logo} + /> + console.log(text)} + icon={Logo} + suffix={() => .bksy.social} + /> + + console.log(date)} + accessibilityLabel="Date" + accessibilityHint="Enter a date" + /> + console.log(date)} + accessibilityLabel="Date" + accessibilityHint="Enter a date" + /> + + ) +} diff --git a/src/view/screens/Storybook/Palette.tsx b/src/view/screens/Storybook/Palette.tsx new file mode 100644 index 0000000000..1844f8d900 --- /dev/null +++ b/src/view/screens/Storybook/Palette.tsx @@ -0,0 +1,279 @@ +import React from 'react' +import {View} from 'react-native' + +import * as tokens from '#/alf/tokens' +import {atoms as a} from '#/alf' + +export function Palette() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/src/view/screens/Storybook/Spacing.tsx b/src/view/screens/Storybook/Spacing.tsx new file mode 100644 index 0000000000..c162779340 --- /dev/null +++ b/src/view/screens/Storybook/Spacing.tsx @@ -0,0 +1,64 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' +import {Text} from '#/view/com/Typography' + +export function Spacing() { + const t = useTheme() + return ( + + + + 2xs (2px) + + + + + xs (4px) + + + + + sm (8px) + + + + + md (12px) + + + + + lg (16px) + + + + + xl (20px) + + + + + 2xl (24px) + + + + + 3xl (28px) + + + + + 4xl (32px) + + + + + 5xl (40px) + + + + + ) +} diff --git a/src/view/screens/Storybook/Theming.tsx b/src/view/screens/Storybook/Theming.tsx new file mode 100644 index 0000000000..68a402683d --- /dev/null +++ b/src/view/screens/Storybook/Theming.tsx @@ -0,0 +1,56 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' +import {Text} from '#/view/com/Typography' +import {Palette} from './Palette' + +export function Theming() { + const t = useTheme() + + return ( + + + + theme.atoms.text + + + + theme.atoms.text_contrast_600 + + + + + theme.atoms.text_contrast_500 + + + + + theme.atoms.text_contrast_400 + + + + + + + theme.atoms.bg + + + theme.atoms.bg_contrast_25 + + + theme.atoms.bg_contrast_50 + + + theme.atoms.bg_contrast_100 + + + theme.atoms.bg_contrast_200 + + + theme.atoms.bg_contrast_300 + + + + ) +} diff --git a/src/view/screens/Storybook/Typography.tsx b/src/view/screens/Storybook/Typography.tsx new file mode 100644 index 0000000000..470d17f913 --- /dev/null +++ b/src/view/screens/Storybook/Typography.tsx @@ -0,0 +1,29 @@ +import React from 'react' + +import {atoms as a} from '#/alf' +import {Text, H1, H2, H3, H4, H5, H6, P} from '#/view/com/Typography' + +export function Typography() { + return ( + <> +

H1 Heading

+

H2 Heading

+

H3 Heading

+

H4 Heading

+
H5 Heading
+
H6 Heading
+

P Paragraph

+ + atoms.text_5xl + atoms.text_4xl + atoms.text_3xl + atoms.text_2xl + atoms.text_xl + atoms.text_lg + atoms.text_md + atoms.text_sm + atoms.text_xs + atoms.text_2xs + + ) +} diff --git a/src/view/screens/Storybook/index.tsx b/src/view/screens/Storybook/index.tsx new file mode 100644 index 0000000000..642eae9f7f --- /dev/null +++ b/src/view/screens/Storybook/index.tsx @@ -0,0 +1,69 @@ +import React from 'react' +import {View} from 'react-native' +import {CenteredView, ScrollView} from '#/view/com/util/Views' + +import {atoms as a, useTheme, ThemeProvider} from '#/alf' +import {useSetColorMode} from '#/state/shell' +import {Button} from '#/view/com/Button' + +import {Theming} from './Theming' +import {Typography} from './Typography' +import {Spacing} from './Spacing' +import {Buttons} from './Buttons' +import {Forms} from './Forms' +import {Dialogs} from './Dialogs' +import {Breakpoints} from './Breakpoints' + +export function Storybook() { + const t = useTheme() + const setColorMode = useSetColorMode() + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} From a33fa45b7483b5e11f8c74d8663c3b0453307cb4 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 12 Jan 2024 11:35:47 -0600 Subject: [PATCH 22/68] Button colors --- src/alf/themes.ts | 8 ++++++-- src/alf/tokens.ts | 1 + src/view/com/Button.tsx | 26 ++++++++++++++++---------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/alf/themes.ts b/src/alf/themes.ts index 4be901c647..614bab66f1 100644 --- a/src/alf/themes.ts +++ b/src/alf/themes.ts @@ -8,6 +8,8 @@ export type ReadonlyPalette = typeof lightPalette export type Palette = Mutable export const lightPalette = { + white: tokens.color.white, + black: tokens.color.black, primary: tokens.color.blue_500, positive: tokens.color.green_500, negative: tokens.color.red_500, @@ -26,9 +28,11 @@ export const lightPalette = { } as const export const darkPalette: Palette = { + white: tokens.color.white, + black: tokens.color.black, primary: tokens.color.blue_500, - positive: tokens.color.green_400, - negative: tokens.color.red_400, + positive: tokens.color.green_500, + negative: tokens.color.red_500, contrast_25: tokens.color.gray_900, contrast_50: tokens.color.gray_800, diff --git a/src/alf/tokens.ts b/src/alf/tokens.ts index 564c7fc8e1..34bef53bd7 100644 --- a/src/alf/tokens.ts +++ b/src/alf/tokens.ts @@ -1,5 +1,6 @@ export const color = { white: '#FFFFFF', + black: '#000000', gray_25: `#FCFCFD`, gray_50: `#F9FAFB`, diff --git a/src/view/com/Button.tsx b/src/view/com/Button.tsx index be3c0d67ab..62227b5830 100644 --- a/src/view/com/Button.tsx +++ b/src/view/com/Button.tsx @@ -106,23 +106,29 @@ export function Button({ case 'primary': { if (disabled) { baseStyles.push({ - backgroundColor: tokens.color.blue_300, + backgroundColor: tokens.color.blue_200, }) } else { baseStyles.push({ backgroundColor: tokens.color.blue_500, }) + hoverStyles.push({ + backgroundColor: tokens.color.blue_600, + }) } break } case 'secondary': { if (disabled) { baseStyles.push({ - backgroundColor: tokens.color.gray_100, + backgroundColor: tokens.color.gray_200, }) } else { baseStyles.push({ - backgroundColor: tokens.color.gray_200, + backgroundColor: tokens.color.gray_300, + }) + hoverStyles.push({ + backgroundColor: tokens.color.gray_400, }) } break @@ -145,16 +151,16 @@ export function Button({ switch (size) { case 'large': { baseStyles.push( - atoms.py_md, - atoms.px_xl, - atoms.rounded_md, + {paddingVertical: 15}, + atoms.px_2xl, + atoms.rounded_sm, atoms.gap_sm, ) break } case 'small': { baseStyles.push( - atoms.py_sm, + {paddingVertical: 9}, atoms.px_md, atoms.rounded_sm, atoms.gap_xs, @@ -239,11 +245,11 @@ export function ButtonText({ case 'secondary': { if (disabled) { baseStyles.push({ - color: tokens.color.gray_500, + color: tokens.color.gray_400, }) } else { baseStyles.push({ - color: tokens.color.gray_700, + color: tokens.color.gray_800, }) } break @@ -261,7 +267,7 @@ export function ButtonText({ switch (size) { case 'small': { baseStyles.push( - atoms.text_sm, + atoms.text_md, web({paddingBottom: 1}), native({marginTop: 2}), ) From 45c807b83b4849b51f72ab10d93c1c4ef406f7f2 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 12 Jan 2024 13:38:44 -0600 Subject: [PATCH 23/68] Dim mode --- src/alf/themes.ts | 76 +++++++++++++++---- src/alf/tokens.ts | 3 - .../com/forms/InputDate/index.android.tsx | 4 +- src/view/com/forms/InputDate/index.web.tsx | 4 +- src/view/com/forms/InputText.tsx | 4 +- src/view/screens/Storybook/Theming.tsx | 2 +- src/view/screens/Storybook/index.tsx | 3 + 7 files changed, 71 insertions(+), 25 deletions(-) diff --git a/src/alf/themes.ts b/src/alf/themes.ts index 614bab66f1..a4fe02fc1c 100644 --- a/src/alf/themes.ts +++ b/src/alf/themes.ts @@ -1,15 +1,15 @@ import * as tokens from '#/alf/tokens' import type {Mutable} from '#/alf/types' -export type ThemeName = 'light' | 'dark' +export type ThemeName = 'light' | 'dim' | 'dark' export type ReadonlyTheme = typeof light export type Theme = Mutable export type ReadonlyPalette = typeof lightPalette export type Palette = Mutable export const lightPalette = { - white: tokens.color.white, - black: tokens.color.black, + white: '#FFFFFF', + black: '#080B12', primary: tokens.color.blue_500, positive: tokens.color.green_500, negative: tokens.color.red_500, @@ -28,8 +28,8 @@ export const lightPalette = { } as const export const darkPalette: Palette = { - white: tokens.color.white, - black: tokens.color.black, + white: '#FFFFFF', + black: '#080B12', primary: tokens.color.blue_500, positive: tokens.color.green_500, negative: tokens.color.red_500, @@ -64,10 +64,10 @@ export const light = { color: lightPalette.contrast_400, }, text_inverted: { - color: tokens.color.white, + color: lightPalette.white, }, bg: { - backgroundColor: tokens.color.white, + backgroundColor: lightPalette.white, }, bg_contrast_25: { backgroundColor: lightPalette.contrast_25, @@ -85,10 +85,56 @@ export const light = { backgroundColor: lightPalette.contrast_300, }, border: { - borderColor: tokens.color.gray_200, + borderColor: lightPalette.contrast_100, }, - border_contrast_500: { - borderColor: tokens.color.gray_500, + border_contrast: { + borderColor: lightPalette.contrast_400, + }, + }, +} + +export const dim: Theme = { + name: 'dim', + palette: darkPalette, + atoms: { + text: { + color: darkPalette.white, + }, + text_contrast_600: { + color: darkPalette.contrast_600, + }, + text_contrast_500: { + color: darkPalette.contrast_500, + }, + text_contrast_400: { + color: darkPalette.contrast_400, + }, + text_inverted: { + color: darkPalette.contrast_900, + }, + bg: { + backgroundColor: darkPalette.contrast_25, + }, + bg_contrast_25: { + backgroundColor: darkPalette.contrast_50, + }, + bg_contrast_50: { + backgroundColor: darkPalette.contrast_100, + }, + bg_contrast_100: { + backgroundColor: darkPalette.contrast_200, + }, + bg_contrast_200: { + backgroundColor: darkPalette.contrast_300, + }, + bg_contrast_300: { + backgroundColor: darkPalette.contrast_400, + }, + border: { + borderColor: darkPalette.contrast_50, + }, + border_contrast: { + borderColor: darkPalette.contrast_300, }, }, } @@ -98,7 +144,7 @@ export const dark: Theme = { palette: darkPalette, atoms: { text: { - color: tokens.color.white, + color: darkPalette.white, }, text_contrast_600: { color: darkPalette.contrast_600, @@ -113,7 +159,7 @@ export const dark: Theme = { color: tokens.color.gray_900, }, bg: { - backgroundColor: tokens.color.gray_900, + backgroundColor: darkPalette.black, }, bg_contrast_25: { backgroundColor: darkPalette.contrast_25, @@ -131,10 +177,10 @@ export const dark: Theme = { backgroundColor: darkPalette.contrast_300, }, border: { - borderColor: tokens.color.gray_800, + borderColor: darkPalette.contrast_50, }, - border_contrast_500: { - borderColor: tokens.color.gray_500, + border_contrast: { + borderColor: darkPalette.contrast_300, }, }, } diff --git a/src/alf/tokens.ts b/src/alf/tokens.ts index 34bef53bd7..35189e7b88 100644 --- a/src/alf/tokens.ts +++ b/src/alf/tokens.ts @@ -1,7 +1,4 @@ export const color = { - white: '#FFFFFF', - black: '#000000', - gray_25: `#FCFCFD`, gray_50: `#F9FAFB`, gray_100: `#F3F4F6`, diff --git a/src/view/com/forms/InputDate/index.android.tsx b/src/view/com/forms/InputDate/index.android.tsx index 13b1ac25d2..9d80ba38a5 100644 --- a/src/view/com/forms/InputDate/index.android.tsx +++ b/src/view/com/forms/InputDate/index.android.tsx @@ -52,7 +52,7 @@ export function InputDate({ if (focused) { input.push({ - borderColor: t.atoms.border_contrast_500.borderColor, + borderColor: t.atoms.border_contrast.borderColor, }) if (hasError) { @@ -130,7 +130,7 @@ export function InputDate({ ]}> - + diff --git a/src/view/screens/Storybook/index.tsx b/src/view/screens/Storybook/index.tsx index 642eae9f7f..3724154c00 100644 --- a/src/view/screens/Storybook/index.tsx +++ b/src/view/screens/Storybook/index.tsx @@ -52,6 +52,9 @@ export function Storybook() { + + + From ceef55041a0517b09cb05ddb0a5a5c57ea32135c Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Sat, 13 Jan 2024 12:11:25 -0600 Subject: [PATCH 24/68] Reorg --- src/App.native.tsx | 5 +- src/App.web.tsx | 5 +- src/alf/themes.ts | 92 +++- src/alf/tokens.ts | 3 + src/components/Button.tsx | 434 ++++++++++++++++++ .../com => components}/Dialog/context.ts | 2 +- src/{view/com => components}/Dialog/index.tsx | 12 +- .../com => components}/Dialog/index.web.tsx | 14 +- src/{view/com => components}/Dialog/types.ts | 0 src/{view/com => components}/Link.tsx | 6 +- src/{view/com => components}/Portal.tsx | 0 src/{view/com => components}/Prompt.tsx | 14 +- src/{view/com => components}/Typography.tsx | 0 .../forms/InputDate/index.android.tsx | 10 +- .../forms/InputDate/index.tsx | 8 +- .../forms/InputDate/index.web.tsx | 10 +- .../forms/InputDate/types.ts | 2 +- .../forms/InputDate/utils.ts | 0 .../com => components}/forms/InputGroup.tsx | 0 .../com => components}/forms/InputText.tsx | 6 +- src/{view/com => components}/forms/types.ts | 0 .../hooks/useInteractionState.ts | 0 src/view/com/Button.tsx | 297 ------------ src/view/screens/Storybook/Breakpoints.tsx | 2 +- src/view/screens/Storybook/Buttons.tsx | 142 ++---- src/view/screens/Storybook/Dialogs.tsx | 17 +- src/view/screens/Storybook/Forms.tsx | 6 +- src/view/screens/Storybook/Links.tsx | 59 +++ src/view/screens/Storybook/Spacing.tsx | 86 ++-- src/view/screens/Storybook/Theming.tsx | 2 +- src/view/screens/Storybook/Typography.tsx | 7 +- src/view/screens/Storybook/index.tsx | 15 +- 32 files changed, 730 insertions(+), 526 deletions(-) create mode 100644 src/components/Button.tsx rename src/{view/com => components}/Dialog/context.ts (84%) rename src/{view/com => components}/Dialog/index.tsx (91%) rename src/{view/com => components}/Dialog/index.web.tsx (92%) rename src/{view/com => components}/Dialog/types.ts (100%) rename src/{view/com => components}/Link.tsx (96%) rename src/{view/com => components}/Portal.tsx (100%) rename src/{view/com => components}/Prompt.tsx (91%) rename src/{view/com => components}/Typography.tsx (100%) rename src/{view/com => components}/forms/InputDate/index.android.tsx (92%) rename src/{view/com => components}/forms/InputDate/index.tsx (88%) rename src/{view/com => components}/forms/InputDate/index.web.tsx (91%) rename src/{view/com => components}/forms/InputDate/types.ts (78%) rename src/{view/com => components}/forms/InputDate/utils.ts (100%) rename src/{view/com => components}/forms/InputGroup.tsx (100%) rename src/{view/com => components}/forms/InputText.tsx (96%) rename src/{view/com => components}/forms/types.ts (100%) rename src/{view/com/util => components}/hooks/useInteractionState.ts (100%) delete mode 100644 src/view/com/Button.tsx create mode 100644 src/view/screens/Storybook/Links.tsx diff --git a/src/App.native.tsx b/src/App.native.tsx index 39485ee24f..7042c6e098 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -41,7 +41,10 @@ import { import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' import * as persisted from '#/state/persisted' import {Splash} from '#/Splash' -import {Provider as PortalProvider, Outlet as PortalOutlet} from '#/view/com/Portal' +import { + Provider as PortalProvider, + Outlet as PortalOutlet, +} from '#/components/Portal' SplashScreen.preventAutoHideAsync() diff --git a/src/App.web.tsx b/src/App.web.tsx index 5760b0d4a2..546f5614df 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -30,7 +30,10 @@ import { } from 'state/session' import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' import * as persisted from '#/state/persisted' -import {Provider as PortalProvider, Outlet as PortalOutlet} from '#/view/com/Portal' +import { + Provider as PortalProvider, + Outlet as PortalOutlet, +} from '#/components/Portal' function InnerApp() { const {isInitialLoad, currentAccount} = useSession() diff --git a/src/alf/themes.ts b/src/alf/themes.ts index a4fe02fc1c..103693b0b3 100644 --- a/src/alf/themes.ts +++ b/src/alf/themes.ts @@ -8,11 +8,8 @@ export type ReadonlyPalette = typeof lightPalette export type Palette = Mutable export const lightPalette = { - white: '#FFFFFF', - black: '#080B12', - primary: tokens.color.blue_500, - positive: tokens.color.green_500, - negative: tokens.color.red_500, + white: tokens.color.white, + black: tokens.color.black, contrast_25: tokens.color.gray_25, contrast_50: tokens.color.gray_50, @@ -25,14 +22,47 @@ export const lightPalette = { contrast_700: tokens.color.gray_700, contrast_800: tokens.color.gray_800, contrast_900: tokens.color.gray_900, + + primary_25: tokens.color.blue_25, + primary_50: tokens.color.blue_50, + primary_100: tokens.color.blue_100, + primary_200: tokens.color.blue_200, + primary_300: tokens.color.blue_300, + primary_400: tokens.color.blue_400, + primary_500: tokens.color.blue_500, + primary_600: tokens.color.blue_600, + primary_700: tokens.color.blue_700, + primary_800: tokens.color.blue_800, + primary_900: tokens.color.blue_900, + + positive_25: tokens.color.green_25, + positive_50: tokens.color.green_50, + positive_100: tokens.color.green_100, + positive_200: tokens.color.green_200, + positive_300: tokens.color.green_300, + positive_400: tokens.color.green_400, + positive_500: tokens.color.green_500, + positive_600: tokens.color.green_600, + positive_700: tokens.color.green_700, + positive_800: tokens.color.green_800, + positive_900: tokens.color.green_900, + + negative_25: tokens.color.red_25, + negative_50: tokens.color.red_50, + negative_100: tokens.color.red_100, + negative_200: tokens.color.red_200, + negative_300: tokens.color.red_300, + negative_400: tokens.color.red_400, + negative_500: tokens.color.red_500, + negative_600: tokens.color.red_600, + negative_700: tokens.color.red_700, + negative_800: tokens.color.red_800, + negative_900: tokens.color.red_900, } as const export const darkPalette: Palette = { - white: '#FFFFFF', - black: '#080B12', - primary: tokens.color.blue_500, - positive: tokens.color.green_500, - negative: tokens.color.red_500, + white: tokens.color.white, + black: tokens.color.black, contrast_25: tokens.color.gray_900, contrast_50: tokens.color.gray_800, @@ -45,6 +75,42 @@ export const darkPalette: Palette = { contrast_700: tokens.color.gray_100, contrast_800: tokens.color.gray_50, contrast_900: tokens.color.gray_25, + + primary_25: tokens.color.blue_25, + primary_50: tokens.color.blue_50, + primary_100: tokens.color.blue_100, + primary_200: tokens.color.blue_200, + primary_300: tokens.color.blue_300, + primary_400: tokens.color.blue_400, + primary_500: tokens.color.blue_500, + primary_600: tokens.color.blue_600, + primary_700: tokens.color.blue_700, + primary_800: tokens.color.blue_800, + primary_900: tokens.color.blue_900, + + positive_25: tokens.color.green_25, + positive_50: tokens.color.green_50, + positive_100: tokens.color.green_100, + positive_200: tokens.color.green_200, + positive_300: tokens.color.green_300, + positive_400: tokens.color.green_400, + positive_500: tokens.color.green_500, + positive_600: tokens.color.green_600, + positive_700: tokens.color.green_700, + positive_800: tokens.color.green_800, + positive_900: tokens.color.green_900, + + negative_25: tokens.color.red_25, + negative_50: tokens.color.red_50, + negative_100: tokens.color.red_100, + negative_200: tokens.color.red_200, + negative_300: tokens.color.red_300, + negative_400: tokens.color.red_400, + negative_500: tokens.color.red_500, + negative_600: tokens.color.red_600, + negative_700: tokens.color.red_700, + negative_800: tokens.color.red_800, + negative_900: tokens.color.red_900, } as const export const light = { @@ -52,7 +118,7 @@ export const light = { palette: lightPalette, atoms: { text: { - color: tokens.color.gray_900, + color: lightPalette.black, }, text_contrast_600: { color: lightPalette.contrast_600, @@ -110,7 +176,7 @@ export const dim: Theme = { color: darkPalette.contrast_400, }, text_inverted: { - color: darkPalette.contrast_900, + color: darkPalette.black, }, bg: { backgroundColor: darkPalette.contrast_25, @@ -156,7 +222,7 @@ export const dark: Theme = { color: darkPalette.contrast_400, }, text_inverted: { - color: tokens.color.gray_900, + color: darkPalette.black, }, bg: { backgroundColor: darkPalette.black, diff --git a/src/alf/tokens.ts b/src/alf/tokens.ts index 35189e7b88..db402840f8 100644 --- a/src/alf/tokens.ts +++ b/src/alf/tokens.ts @@ -1,4 +1,7 @@ export const color = { + white: '#FFFFFF', + black: '#080B12', + gray_25: `#FCFCFD`, gray_50: `#F9FAFB`, gray_100: `#F3F4F6`, diff --git a/src/components/Button.tsx b/src/components/Button.tsx new file mode 100644 index 0000000000..9d36892a55 --- /dev/null +++ b/src/components/Button.tsx @@ -0,0 +1,434 @@ +import React from 'react' +import { + Pressable, + Text, + PressableProps, + TextProps, + ViewStyle, + AccessibilityProps, +} from 'react-native' + +import {useTheme, atoms, tokens, web, native} from '#/alf' + +export type ButtonVariant = 'solid' | 'outline' | 'ghost' +export type ButtonColor = 'primary' | 'secondary' | 'negative' +export type ButtonSize = 'small' | 'large' +export type VariantProps = { + /** + * The style variation of the button + */ + variant?: ButtonVariant + /** + * The color of the button + */ + color?: ButtonColor + /** + * The size of the button + */ + size?: ButtonSize +} + +export type ButtonProps = Omit< + PressableProps, + 'children' | 'style' | 'accessibilityLabel' | 'accessibilityHint' +> & + VariantProps & { + children: + | ((props: { + state: { + pressed: boolean + hovered: boolean + focused: boolean + } + props: VariantProps & { + disabled?: boolean + } + }) => React.ReactNode) + | React.ReactNode + | string + accessibilityLabel: Required['accessibilityLabel'] + accessibilityHint: Required['accessibilityHint'] + } +export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean} + +export function Button({ + children, + variant, + color, + size, + accessibilityLabel, + accessibilityHint, + disabled = false, + ...rest +}: ButtonProps) { + const t = useTheme() + const [state, setState] = React.useState({ + pressed: false, + hovered: false, + focused: false, + }) + + const onPressIn = React.useCallback(() => { + setState(s => ({ + ...s, + pressed: true, + })) + }, [setState]) + const onPressOut = React.useCallback(() => { + setState(s => ({ + ...s, + pressed: false, + })) + }, [setState]) + const onHoverIn = React.useCallback(() => { + setState(s => ({ + ...s, + hovered: true, + })) + }, [setState]) + const onHoverOut = React.useCallback(() => { + setState(s => ({ + ...s, + hovered: false, + })) + }, [setState]) + const onFocus = React.useCallback(() => { + setState(s => ({ + ...s, + focused: true, + })) + }, [setState]) + const onBlur = React.useCallback(() => { + setState(s => ({ + ...s, + focused: false, + })) + }, [setState]) + + const {baseStyles, hoverStyles} = React.useMemo(() => { + const baseStyles: ViewStyle[] = [] + const hoverStyles: ViewStyle[] = [] + const light = t.name === 'light' + + if (color === 'primary') { + if (variant === 'solid') { + if (!disabled) { + baseStyles.push({ + backgroundColor: t.palette.primary_500, + }) + hoverStyles.push({ + backgroundColor: t.palette.primary_600, + }) + } else { + baseStyles.push({ + backgroundColor: t.palette.primary_700, + }) + } + } else if (variant === 'outline') { + baseStyles.push(atoms.border, t.atoms.bg, { + borderWidth: 1, + }) + + if (!disabled) { + baseStyles.push(atoms.border, { + borderColor: tokens.color.blue_500, + }) + hoverStyles.push(atoms.border, { + backgroundColor: light + ? t.palette.primary_100 + : t.palette.primary_900, + }) + } else { + baseStyles.push(atoms.border, { + borderColor: light ? tokens.color.blue_200 : tokens.color.blue_900, + }) + } + } else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push(t.atoms.bg) + hoverStyles.push({ + backgroundColor: light + ? t.palette.primary_100 + : t.palette.primary_900, + }) + } + } + } else if (color === 'secondary') { + if (variant === 'solid') { + if (!disabled) { + baseStyles.push({ + backgroundColor: light + ? tokens.color.gray_200 + : tokens.color.gray_800, + }) + hoverStyles.push({ + backgroundColor: light + ? tokens.color.gray_300 + : tokens.color.gray_900, + }) + } else { + baseStyles.push({ + backgroundColor: light + ? tokens.color.gray_300 + : tokens.color.gray_900, + }) + } + } else if (variant === 'outline') { + baseStyles.push(atoms.border, t.atoms.bg, { + borderWidth: 1, + }) + + if (!disabled) { + baseStyles.push(atoms.border, { + borderColor: light ? tokens.color.gray_500 : tokens.color.gray_500, + }) + hoverStyles.push(atoms.border, t.atoms.bg_contrast_50) + } else { + baseStyles.push(atoms.border, { + borderColor: light ? tokens.color.gray_200 : tokens.color.gray_800, + }) + } + } else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push(t.atoms.bg) + hoverStyles.push({ + backgroundColor: light + ? tokens.color.gray_100 + : tokens.color.gray_800, + }) + } + } + } else if (color === 'negative') { + if (variant === 'solid') { + if (!disabled) { + baseStyles.push({ + backgroundColor: t.palette.negative_500, + }) + hoverStyles.push({ + backgroundColor: t.palette.negative_600, + }) + } else { + baseStyles.push({ + backgroundColor: t.palette.negative_700, + }) + } + } else if (variant === 'outline') { + baseStyles.push(atoms.border, t.atoms.bg, { + borderWidth: 1, + }) + + if (!disabled) { + baseStyles.push(atoms.border, { + borderColor: t.palette.negative_600, + }) + hoverStyles.push(atoms.border, { + backgroundColor: light ? t.palette.negative_50 : '#2D0614', // darker red + }) + } else { + baseStyles.push(atoms.border, { + borderColor: light + ? t.palette.negative_200 + : t.palette.negative_900, + }) + } + } else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push(t.atoms.bg) + hoverStyles.push({ + backgroundColor: light ? t.palette.negative_50 : '#2D0614', // darker red + }) + } + } + } + + if (size === 'large') { + baseStyles.push( + {paddingVertical: 15}, + atoms.px_2xl, + atoms.rounded_sm, + atoms.gap_sm, + ) + } else if (size === 'small') { + baseStyles.push( + {paddingVertical: 9}, + atoms.px_md, + atoms.rounded_sm, + atoms.gap_xs, + ) + } + + return { + baseStyles, + hoverStyles, + } + }, [t, variant, color, size, disabled]) + + const childProps = React.useMemo( + () => ({ + state, + props: { + variant, + color, + size, + disabled: disabled || false, + }, + }), + [state, variant, color, size, disabled], + ) + + return ( + + {typeof children === 'string' ? ( + + {children} + + ) : typeof children === 'function' ? ( + children(childProps) + ) : ( + children + )} + + ) +} + +export function ButtonText({ + children, + style, + variant, + color, + size, + disabled, + ...rest +}: ButtonTextProps) { + const t = useTheme() + + const textStyles = React.useMemo(() => { + const baseStyles = [] + const light = t.name === 'light' + + if (color === 'primary') { + if (variant === 'solid') { + if (!disabled) { + baseStyles.push({color: t.palette.white}) + } else { + baseStyles.push({color: t.palette.white, opacity: 0.5}) + } + } else if (variant === 'outline') { + if (!disabled) { + baseStyles.push({ + color: light ? t.palette.primary_600 : t.palette.primary_500, + }) + } else { + baseStyles.push({color: t.palette.primary_600, opacity: 0.5}) + } + } else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push({color: t.palette.primary_600}) + } else { + baseStyles.push({color: t.palette.primary_600, opacity: 0.5}) + } + } + } else if (color === 'secondary') { + if (variant === 'solid') { + if (!disabled) { + baseStyles.push({ + color: light ? tokens.color.gray_700 : tokens.color.gray_100, + }) + } else { + baseStyles.push({ + color: light ? tokens.color.gray_400 : tokens.color.gray_700, + }) + } + } else if (variant === 'outline') { + if (!disabled) { + baseStyles.push({ + color: light ? tokens.color.gray_600 : tokens.color.gray_300, + }) + } else { + baseStyles.push({ + color: light ? tokens.color.gray_400 : tokens.color.gray_700, + }) + } + } else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push({ + color: light ? tokens.color.gray_600 : tokens.color.gray_300, + }) + } else { + baseStyles.push({ + color: light ? tokens.color.gray_400 : tokens.color.gray_600, + }) + } + } + } else if (color === 'negative') { + if (variant === 'solid') { + if (!disabled) { + baseStyles.push({color: t.palette.white}) + } else { + baseStyles.push({color: t.palette.white, opacity: 0.5}) + } + } else if (variant === 'outline') { + if (!disabled) { + baseStyles.push({color: t.palette.negative_500}) + } else { + baseStyles.push({color: t.palette.negative_500, opacity: 0.5}) + } + } else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push({color: t.palette.negative_500}) + } else { + baseStyles.push({color: t.palette.negative_500, opacity: 0.5}) + } + } + } + + if (size === 'large') { + baseStyles.push( + atoms.text_md, + web({paddingBottom: 1}), + native({marginTop: 2}), + ) + } else { + baseStyles.push( + atoms.text_md, + web({paddingBottom: 1}), + native({marginTop: 2}), + ) + } + + return baseStyles + }, [t, variant, color, size, disabled]) + + return ( + + {children} + + ) +} diff --git a/src/view/com/Dialog/context.ts b/src/components/Dialog/context.ts similarity index 84% rename from src/view/com/Dialog/context.ts rename to src/components/Dialog/context.ts index 2330969933..76473aa243 100644 --- a/src/view/com/Dialog/context.ts +++ b/src/components/Dialog/context.ts @@ -1,5 +1,5 @@ import React from 'react' -import {DialogContextProps, DialogControlProps} from '#/view/com/Dialog/types' +import {DialogContextProps, DialogControlProps} from '#/components/Dialog/types' export const Context = React.createContext({ close: () => {}, diff --git a/src/view/com/Dialog/index.tsx b/src/components/Dialog/index.tsx similarity index 91% rename from src/view/com/Dialog/index.tsx rename to src/components/Dialog/index.tsx index 5a832e6fe4..4bc1338e0b 100644 --- a/src/view/com/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -3,17 +3,17 @@ import {View, Dimensions} from 'react-native' import BottomSheet, {BottomSheetBackdrop} from '@gorhom/bottom-sheet' import {useTheme, atoms as a} from '#/alf' -import {Portal} from '#/view/com/Portal' +import {Portal} from '#/components/Portal' import { DialogOuterProps, DialogControlProps, DialogInnerProps, -} from '#/view/com/Dialog/types' -import {Context} from '#/view/com/Dialog/context' +} from '#/components/Dialog/types' +import {Context} from '#/components/Dialog/context' -export {useDialogControl, useDialogContext} from '#/view/com/Dialog/context' -export * from '#/view/com/Dialog/types' +export {useDialogControl, useDialogContext} from '#/components/Dialog/context' +export * from '#/components/Dialog/types' export function Outer({ children, @@ -62,7 +62,7 @@ export function Outer({ {...props} /> )} - handleIndicatorStyle={{backgroundColor: t.palette.primary}} + handleIndicatorStyle={{backgroundColor: t.palette.primary_500}} handleStyle={{display: 'none'}} onClose={onClose}> diff --git a/src/view/com/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx similarity index 92% rename from src/view/com/Dialog/index.web.tsx rename to src/components/Dialog/index.web.tsx index 37d4cebc85..8efccd55d7 100644 --- a/src/view/com/Dialog/index.web.tsx +++ b/src/components/Dialog/index.web.tsx @@ -4,15 +4,15 @@ import {FocusScope} from '@tamagui/focus-scope' import Animated, {FadeInDown, FadeIn} from 'react-native-reanimated' import {useTheme, atoms as a, useBreakpoints, web} from '#/alf' -import {Text} from '#/view/com/Typography' -import {Portal} from '#/view/com/Portal' -import {Button} from '#/view/com/Button' +import {Text} from '#/components/Typography' +import {Portal} from '#/components/Portal' +import {Button} from '#/components/Button' -import {DialogOuterProps, DialogInnerProps} from '#/view/com/Dialog/types' -import {Context, useDialogContext} from '#/view/com/Dialog/context' +import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types' +import {Context, useDialogContext} from '#/components/Dialog/context' -export {useDialogControl, useDialogContext} from '#/view/com/Dialog/context' -export * from '#/view/com/Dialog/types' +export {useDialogControl, useDialogContext} from '#/components/Dialog/context' +export * from '#/components/Dialog/types' const stopPropagation = (e: any) => e.stopPropagation() diff --git a/src/view/com/Dialog/types.ts b/src/components/Dialog/types.ts similarity index 100% rename from src/view/com/Dialog/types.ts rename to src/components/Dialog/types.ts diff --git a/src/view/com/Link.tsx b/src/components/Link.tsx similarity index 96% rename from src/view/com/Link.tsx rename to src/components/Link.tsx index fd6bf5ca3c..8a2e825479 100644 --- a/src/view/com/Link.tsx +++ b/src/components/Link.tsx @@ -15,7 +15,7 @@ import {sanitizeUrl} from '@braintree/sanitize-url' import {isWeb} from '#/platform/detection' import {useTheme, web} from '#/alf' -import {Button, ButtonProps} from '#/view/com/Button' +import {Button, ButtonProps} from '#/components/Button' import {AllNavigatorParams, NavigationProp} from '#/lib/routes/types' import { convertBskyAppUrlIfNeeded, @@ -157,10 +157,10 @@ export function Link({ {children as string} diff --git a/src/view/com/Portal.tsx b/src/components/Portal.tsx similarity index 100% rename from src/view/com/Portal.tsx rename to src/components/Portal.tsx diff --git a/src/view/com/Prompt.tsx b/src/components/Prompt.tsx similarity index 91% rename from src/view/com/Prompt.tsx rename to src/components/Prompt.tsx index baf7625828..b5841e7bb9 100644 --- a/src/view/com/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -3,12 +3,12 @@ import {View, PressableProps, LayoutChangeEvent} from 'react-native' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useTheme, atoms as a} from '#/alf' -import {H2, P} from '#/view/com/Typography' -import {Button} from '#/view/com/Button' +import {H2, P} from '#/components/Typography' +import {Button} from '#/components/Button' -import * as Dialog from '#/view/com/Dialog' +import * as Dialog from '#/components/Dialog' -export {useDialogControl as usePromptControl} from '#/view/com/Dialog' +export {useDialogControl as usePromptControl} from '#/components/Dialog' const Context = React.createContext<{ titleId: string @@ -102,7 +102,8 @@ export function Cancel({ const {close} = Dialog.useDialogContext() return ( - - + + + ))} - )} - - - - - - - - - - - - - - External - - -

External with custom children

- - - https://blueskyweb.xyz - - - Internal - - - - {({props}) => Link as a button} - + ))} +
) } diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx index e0fcf0f811..b4a779e9d0 100644 --- a/src/view/screens/Storybook/Dialogs.tsx +++ b/src/view/screens/Storybook/Dialogs.tsx @@ -2,10 +2,10 @@ import React from 'react' import {View} from 'react-native' import {atoms as a} from '#/alf' -import {Button} from '#/view/com/Button' -import {H3, P} from '#/view/com/Typography' -import * as Dialog from '#/view/com/Dialog' -import * as Prompt from '#/view/com/Prompt' +import {Button} from '#/components/Button' +import {H3, P} from '#/components/Typography' +import * as Dialog from '#/components/Dialog' +import * as Prompt from '#/components/Prompt' export function Dialogs() { const control = Dialog.useDialogControl() @@ -14,7 +14,8 @@ export function Dialogs() { return ( <>
+ @@ -68,7 +69,6 @@ export function Storybook() { -
From 048ce0244f3dbf005f332e37e75054f26ba95a7b Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 15 Jan 2024 15:58:57 -0600 Subject: [PATCH 30/68] Scrollable dialogs --- src/App.native.tsx | 6 +- src/App.web.tsx | 6 +- src/components/Dialog/index.tsx | 80 +++++++++++++++----------- src/components/Dialog/index.web.tsx | 7 ++- src/components/Prompt.tsx | 3 +- src/view/screens/Storybook/Dialogs.tsx | 13 +++-- src/view/shell/index.tsx | 2 + src/view/shell/index.web.tsx | 2 + 8 files changed, 67 insertions(+), 52 deletions(-) diff --git a/src/App.native.tsx b/src/App.native.tsx index 7042c6e098..b0aa4b182a 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -41,10 +41,7 @@ import { import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' import * as persisted from '#/state/persisted' import {Splash} from '#/Splash' -import { - Provider as PortalProvider, - Outlet as PortalOutlet, -} from '#/components/Portal' +import {Provider as PortalProvider} from '#/components/Portal' SplashScreen.preventAutoHideAsync() @@ -80,7 +77,6 @@ function InnerApp() { - diff --git a/src/App.web.tsx b/src/App.web.tsx index 546f5614df..756d4954eb 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -30,10 +30,7 @@ import { } from 'state/session' import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' import * as persisted from '#/state/persisted' -import { - Provider as PortalProvider, - Outlet as PortalOutlet, -} from '#/components/Portal' +import {Provider as PortalProvider} from '#/components/Portal' function InnerApp() { const {isInitialLoad, currentAccount} = useSession() @@ -62,7 +59,6 @@ function InnerApp() { - diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index de9d69edb2..89af99d66a 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -2,6 +2,7 @@ import React, {useImperativeHandle} from 'react' import {View, Dimensions} from 'react-native' import BottomSheet, { BottomSheetBackdrop, + BottomSheetScrollView, BottomSheetView, } from '@gorhom/bottom-sheet' import {useSafeAreaInsets} from 'react-native-safe-area-context' @@ -16,13 +17,6 @@ import { } from '#/components/Dialog/types' import {Context} from '#/components/Dialog/context' -/** - * Exports - */ -export { - BottomSheetScrollView as ScrollView, - BottomSheetTextInput as TextInput, -} from '@gorhom/bottom-sheet' export {useDialogControl, useDialogContext} from '#/components/Dialog/context' export * from '#/components/Dialog/types' @@ -34,6 +28,8 @@ export function Outer({ }: React.PropsWithChildren) { const t = useTheme() const sheet = React.useRef(null) + const sheetOptions = nativeOptions?.sheet || {} + const hasSnapPoints = !!sheetOptions.snapPoints const open = React.useCallback((i = 0) => { sheet.current?.snapToIndex(i) @@ -58,15 +54,15 @@ export function Outer({ return ( ( - - - - {children} - - + + + {children} + ) @@ -103,8 +97,27 @@ export function Outer({ export function Inner(props: DialogInnerProps) { const insets = useSafeAreaInsets() return ( - + {props.children} + + ) +} + +export function ScrollableInner(props: DialogInnerProps) { + const insets = useSafeAreaInsets() + return ( + {props.children} - + ) } @@ -126,12 +139,13 @@ export function Handle() { a.absolute, a.rounded_sm, a.z_10, - t.atoms.bg_contrast_200, { - top: 12, - width: 50, - height: 6, + top: a.pt_lg.paddingTop, + width: 35, + height: 4, alignSelf: 'center', + backgroundColor: t.palette.contrast_900, + opacity: 0.5, }, ]} /> diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx index 88b29e13a8..b98ddae5e8 100644 --- a/src/components/Dialog/index.web.tsx +++ b/src/components/Dialog/index.web.tsx @@ -11,7 +11,6 @@ import {Button} from '#/components/Button' import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types' import {Context, useDialogContext} from '#/components/Dialog/context' -export {ScrollView, TextInput} from 'react-native' export {useDialogControl, useDialogContext} from '#/components/Dialog/context' export * from '#/components/Dialog/types' @@ -91,7 +90,7 @@ export function Outer({ style={[ web(a.fixed), a.inset_0, - t.atoms.bg_contrast_300, + t.atoms.bg_contrast_100, {opacity: 0.8}, ]} /> @@ -145,7 +144,7 @@ export function Inner({ a.border, gtMobile ? a.p_xl : a.p_lg, t.atoms.bg, - {maxWidth: 600, borderColor: t.palette.contrast_300}, + {maxWidth: 600, borderColor: t.palette.contrast_200}, ...(Array.isArray(style) ? style : [style || {}]), ]}> {children} @@ -154,6 +153,8 @@ export function Inner({ ) } +export const ScrollableInner = Inner + export function Handle() { return null } diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx index 4617943829..5e1f88453f 100644 --- a/src/components/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -34,11 +34,12 @@ export function Outer({ return ( + + - {children} diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx index f34fca8b85..9dd76be40a 100644 --- a/src/view/screens/Storybook/Dialogs.tsx +++ b/src/view/screens/Storybook/Dialogs.tsx @@ -45,15 +45,18 @@ export function Dialogs() { - - + + + - -

Dialog

Description

+ - @@ -66,9 +66,11 @@ export function Storybook() { + + From e7d9ce69f722c5d267efde28bad57a865aee78ca Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 15 Jan 2024 19:13:10 -0600 Subject: [PATCH 36/68] Button focus states --- src/components/Button.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 9d36892a55..558ec32d2b 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -105,7 +105,7 @@ export function Button({ })) }, [setState]) - const {baseStyles, hoverStyles} = React.useMemo(() => { + const {baseStyles, hoverStyles, focusStyles} = React.useMemo(() => { const baseStyles: ViewStyle[] = [] const hoverStyles: ViewStyle[] = [] const light = t.name === 'light' @@ -260,6 +260,12 @@ export function Button({ return { baseStyles, hoverStyles, + focusStyles: [ + ...hoverStyles, + { + outline: 0, + }, + ], } }, [t, variant, color, size, disabled]) @@ -292,6 +298,7 @@ export function Button({ atoms.align_center, ...baseStyles, ...(state.hovered ? hoverStyles : []), + ...(state.focused ? focusStyles : []), ]} onPressIn={onPressIn} onPressOut={onPressOut} From 4bf30a1ed332be45635ac8ddf916ed073a0b806c Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 15 Jan 2024 20:15:50 -0600 Subject: [PATCH 37/68] Button pressed states --- src/components/Button.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 558ec32d2b..7896fdc878 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -264,7 +264,7 @@ export function Button({ ...hoverStyles, { outline: 0, - }, + } as ViewStyle, ], } }, [t, variant, color, size, disabled]) @@ -297,7 +297,7 @@ export function Button({ atoms.flex_row, atoms.align_center, ...baseStyles, - ...(state.hovered ? hoverStyles : []), + ...(state.hovered || state.pressed ? hoverStyles : []), ...(state.focused ? focusStyles : []), ]} onPressIn={onPressIn} From 56a139ed4be0da7cff74f834eaf7428010a503f2 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 15 Jan 2024 21:10:02 -0600 Subject: [PATCH 38/68] Gradient poc --- src/alf/atoms.ts | 4 ++++ src/components/Button.tsx | 22 ++++++++++++++++++---- src/view/screens/Storybook/Buttons.tsx | 9 +++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index c0d20e9c77..203c2f282a 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -35,6 +35,10 @@ export const atoms = { zIndex: 50, }, + overflow_hidden: { + overflow: 'hidden', + }, + /* * Width */ diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 7896fdc878..b44fb05cac 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -7,10 +7,11 @@ import { ViewStyle, AccessibilityProps, } from 'react-native' +import LinearGradient from 'react-native-linear-gradient' import {useTheme, atoms, tokens, web, native} from '#/alf' -export type ButtonVariant = 'solid' | 'outline' | 'ghost' +export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient' export type ButtonColor = 'primary' | 'secondary' | 'negative' export type ButtonSize = 'small' | 'large' export type VariantProps = { @@ -296,6 +297,7 @@ export function Button({ style={[ atoms.flex_row, atoms.align_center, + atoms.overflow_hidden, ...baseStyles, ...(state.hovered || state.pressed ? hoverStyles : []), ...(state.focused ? focusStyles : []), @@ -306,6 +308,18 @@ export function Button({ onHoverOut={onHoverOut} onFocus={onFocus} onBlur={onBlur}> + {variant === 'gradient' && ( + + )} {typeof children === 'string' ? ( ))} + +
) From ce049f1e1bd035def57a465fdc13dd2b5b259fd4 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 16 Jan 2024 10:41:35 -0600 Subject: [PATCH 39/68] Gradient colors and hovers --- src/alf/tokens.ts | 50 +++++++++++++++++++++ src/components/Button.tsx | 56 ++++++++++++++++++++--- src/view/screens/Storybook/Buttons.tsx | 62 ++++++++++++++++++++++---- 3 files changed, 154 insertions(+), 14 deletions(-) diff --git a/src/alf/tokens.ts b/src/alf/tokens.ts index 0bcffaebef..36f6f3992a 100644 --- a/src/alf/tokens.ts +++ b/src/alf/tokens.ts @@ -98,6 +98,56 @@ export const fontWeight = { bold: '900', } as const +export const gradients = { + sky: { + values: [ + [0, '#0A7AFF'], + [1, '#59B9FF'], + ], + hover_value: '#0A7AFF', + }, + midnight: { + values: [ + [0, '#022C5E'], + [1, '#4079BC'], + ], + hover_value: '#022C5E', + }, + sunrise: { + values: [ + [0, '#4E90AE'], + [0.4, '#AEA3AB'], + [0.8, '#E6A98F'], + [1, '#F3A84C'], + ], + hover_value: '#AEA3AB', + }, + sunset: { + values: [ + [0, '#6772AF'], + [0.6, '#B88BB6'], + [1, '#FFA6AC'], + ], + hover_value: '#B88BB6', + }, + nordic: { + values: [ + [0, '#083367'], + [1, '#9EE8C1'], + ], + hover_value: '#3A7085', + }, + bonfire: { + values: [ + [0, '#203E4E'], + [0.4, '#755B62'], + [0.8, '#CD7765'], + [1, '#EF956E'], + ], + hover_value: '#755B62', + }, +} as const + export type Color = keyof typeof color export type Space = keyof typeof space export type FontSize = keyof typeof fontSize diff --git a/src/components/Button.tsx b/src/components/Button.tsx index b44fb05cac..b76839de49 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -12,7 +12,16 @@ import LinearGradient from 'react-native-linear-gradient' import {useTheme, atoms, tokens, web, native} from '#/alf' export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient' -export type ButtonColor = 'primary' | 'secondary' | 'negative' +export type ButtonColor = + | 'primary' + | 'secondary' + | 'negative' + | 'gradient_sky' + | 'gradient_midnight' + | 'gradient_sunrise' + | 'gradient_sunset' + | 'gradient_nordic' + | 'gradient_bonfire' export type ButtonSize = 'small' | 'large' export type VariantProps = { /** @@ -270,6 +279,36 @@ export function Button({ } }, [t, variant, color, size, disabled]) + const {gradientColors, gradientHoverColors, gradientLocations} = + React.useMemo(() => { + const colors: string[] = [] + const hoverColors: string[] = [] + const locations: number[] = [] + const gradient = { + primary: tokens.gradients.sky, + secondary: tokens.gradients.sky, + negative: tokens.gradients.sky, + gradient_sky: tokens.gradients.sky, + gradient_midnight: tokens.gradients.midnight, + gradient_sunrise: tokens.gradients.sunrise, + gradient_sunset: tokens.gradients.sunset, + gradient_nordic: tokens.gradients.nordic, + gradient_bonfire: tokens.gradients.bonfire, + }[color || 'primary'] + + if (variant === 'gradient') { + colors.push(...gradient.values.map(([_, color]) => color)) + hoverColors.push(...gradient.values.map(_ => gradient.hover_value)) + locations.push(...gradient.values.map(([location, _]) => location)) + } + + return { + gradientColors: colors, + gradientHoverColors: hoverColors, + gradientLocations: locations, + } + }, [variant, color]) + const childProps = React.useMemo( () => ({ state, @@ -311,10 +350,11 @@ export function Button({ {variant === 'gradient' && (

Buttons

- + {['primary', 'secondary', 'negative'].map(color => ( {['solid', 'outline', 'ghost'].map(variant => ( @@ -37,14 +37,58 @@ export function Buttons() { ))} - + + + {['gradient_sky', 'gradient_midnight', 'gradient_sunrise'].map( + name => ( + + + + + ), + )} + + + {['gradient_sunset', 'gradient_nordic', 'gradient_bonfire'].map( + name => ( + + + + + ), + )} + + ) From 1e02cde581a7245776ff278feee78b6dab155bef Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 16 Jan 2024 10:43:31 -0600 Subject: [PATCH 40/68] Add hrefAttrs to Link --- src/components/Link.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 8a2e825479..2eb7b6bffd 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -145,8 +145,10 @@ export function Link({ href={href} onPress={onPress} {...web({ - target: isExternal ? '_blank' : undefined, - rel: isExternal ? 'noopener noreferrer' : undefined, + hrefAttrs: { + target: isExternal ? 'blank' : undefined, + rel: isExternal ? 'noopener noreferrer' : undefined, + }, dataSet: { // default to no underline, apply this ourselves noUnderline: '1', From 7315b0d7145f6df0c6a4e2818b7d4f9a01d79687 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 16 Jan 2024 11:04:54 -0600 Subject: [PATCH 41/68] Some more a11y --- src/components/Button.tsx | 1 + src/components/Dialog/index.web.tsx | 5 ++++- src/components/Dialog/types.ts | 18 ++++++++++++++---- src/components/forms/InputText.tsx | 2 ++ src/components/forms/Toggle.tsx | 3 +++ src/view/screens/Storybook/Dialogs.tsx | 4 ++-- 6 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/components/Button.tsx b/src/components/Button.tsx index b76839de49..7c032dbad8 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -327,6 +327,7 @@ export function Button({ role="button" {...rest} aria-label={accessibilityLabel} + aria-pressed={state.pressed} accessibilityLabel={accessibilityLabel} accessibilityHint={accessibilityHint} disabled={disabled || false} diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx index 6ddc1b0b4a..ad205d3648 100644 --- a/src/components/Dialog/index.web.tsx +++ b/src/components/Dialog/index.web.tsx @@ -120,6 +120,7 @@ export function Outer({ export function Inner({ children, style, + label, accessibilityLabelledBy, accessibilityDescribedBy, }: DialogInnerProps) { @@ -128,9 +129,11 @@ export function Inner({ return ( true} diff --git a/src/components/Dialog/types.ts b/src/components/Dialog/types.ts index cdcad90da6..d36784183c 100644 --- a/src/components/Dialog/types.ts +++ b/src/components/Dialog/types.ts @@ -26,8 +26,18 @@ export type DialogOuterProps = { webOptions?: {} } -export type DialogInnerProps = React.PropsWithChildren<{ +type DialogInnerPropsBase = React.PropsWithChildren<{ style?: ViewStyle - accessibilityLabelledBy: A11yProps['aria-labelledby'] - accessibilityDescribedBy: string -}> +}> & + T +export type DialogInnerProps = + | DialogInnerPropsBase<{ + label?: undefined + accessibilityLabelledBy: A11yProps['aria-labelledby'] + accessibilityDescribedBy: string + }> + | DialogInnerPropsBase<{ + label: string + accessibilityLabelledBy?: undefined + accessibilityDescribedBy?: undefined + }> diff --git a/src/components/forms/InputText.tsx b/src/components/forms/InputText.tsx index 5a6b60d468..8933b1d085 100644 --- a/src/components/forms/InputText.tsx +++ b/src/components/forms/InputText.tsx @@ -166,6 +166,8 @@ export function createTextInput(Input: typeof TextInput) { testID={testID} aria-labelledby={labelId} aria-label={label} + aria-invalid={hasError} + aria-placeholder={props.placeholder} accessibilityLabel={accessibilityLabel} accessibilityHint={accessibilityHint} placeholderTextColor={t.atoms.text_contrast_400.color} diff --git a/src/components/forms/Toggle.tsx b/src/components/forms/Toggle.tsx index 2bcf40836f..8a9c53a608 100644 --- a/src/components/forms/Toggle.tsx +++ b/src/components/forms/Toggle.tsx @@ -41,6 +41,7 @@ type ItemProps = Omit & { name: string value?: boolean onChange?: ({name, value}: {name: string; value: boolean}) => void + hasError?: boolean style?: (state: ItemState) => ViewStyle children: ((props: ItemState) => React.ReactNode) | React.ReactNode } @@ -51,6 +52,7 @@ function Item({ value = false, disabled, onChange, + hasError, style, role, }: ItemProps) { @@ -92,6 +94,7 @@ function Item({ aria-disabled={disabled ?? false} aria-checked={value} aria-labelledby={labelId} + aria-invalid={hasError} role={role} accessibilityRole={role as AccessibilityRole} accessibilityState={{ diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx index 5ad6764b0d..5fd8856e55 100644 --- a/src/view/screens/Storybook/Dialogs.tsx +++ b/src/view/screens/Storybook/Dialogs.tsx @@ -51,8 +51,8 @@ export function Dialogs() { + accessibilityDescribedBy="dialog-description" + accessibilityLabelledBy="dialog-title">

Dialog

Description

From 4c76e38cc3b2bdc293640a3388ce36890bad393d Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 16 Jan 2024 11:32:39 -0600 Subject: [PATCH 42/68] Toggle invalid states --- src/components/forms/Toggle.tsx | 159 ++++++++++++++++++--------- src/view/screens/Storybook/Forms.tsx | 12 ++ 2 files changed, 117 insertions(+), 54 deletions(-) diff --git a/src/components/forms/Toggle.tsx b/src/components/forms/Toggle.tsx index 8a9c53a608..875ccbbed5 100644 --- a/src/components/forms/Toggle.tsx +++ b/src/components/forms/Toggle.tsx @@ -12,6 +12,7 @@ type ItemState = { name: string value: boolean disabled: boolean + hasError: boolean hovered: boolean pressed: boolean focused: boolean @@ -22,6 +23,7 @@ const ItemContext = React.createContext({ name: '', value: false, disabled: false, + hasError: false, hovered: false, pressed: false, focused: false, @@ -80,11 +82,12 @@ function Item({ name, value, disabled: disabled ?? false, + hasError: hasError ?? false, hovered, pressed, focused, }), - [labelId, name, value, disabled, hovered, pressed, focused], + [labelId, name, value, disabled, hovered, pressed, focused, hasError], ) return ( @@ -235,47 +238,87 @@ function createSharedToggleStyles({ focused, value, disabled, + hasError, }: { theme: ReturnType value: boolean hovered: boolean focused: boolean disabled: boolean + hasError: boolean }) { - return [ - hovered || focused - ? { - backgroundColor: t.palette.contrast_50, - borderColor: t.palette.contrast_500, - } - : {}, - value - ? { - backgroundColor: - t.name === 'light' ? t.palette.primary_25 : t.palette.primary_900, - borderColor: t.palette.primary_500, - } - : {}, - value && (hovered || focused) - ? { - backgroundColor: - t.name === 'light' ? t.palette.primary_100 : t.palette.primary_800, - borderColor: - t.name === 'light' ? t.palette.primary_600 : t.palette.primary_400, - } - : {}, - disabled - ? { - backgroundColor: t.palette.contrast_200, - borderColor: t.palette.contrast_300, - } - : {}, - ] + const base: ViewStyle[] = [] + const baseHover: ViewStyle[] = [] + const indicator: ViewStyle[] = [] + + if (value) { + base.push({ + backgroundColor: + t.name === 'light' ? t.palette.primary_25 : t.palette.primary_900, + borderColor: t.palette.primary_500, + }) + + if (hovered || focused) { + baseHover.push({ + backgroundColor: + t.name === 'light' ? t.palette.primary_100 : t.palette.primary_800, + borderColor: + t.name === 'light' ? t.palette.primary_600 : t.palette.primary_400, + }) + } + } else { + if (hovered || focused) { + baseHover.push({ + backgroundColor: t.palette.contrast_50, + borderColor: t.palette.contrast_500, + }) + } + } + + if (hasError) { + base.push({ + backgroundColor: + t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900, + borderColor: + t.name === 'light' ? t.palette.negative_300 : t.palette.negative_800, + }) + + if (hovered || focused) { + baseHover.push({ + backgroundColor: + t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900, + borderColor: t.palette.negative_500, + }) + } + } + + if (disabled) { + base.push({ + backgroundColor: t.palette.contrast_200, + borderColor: t.palette.contrast_300, + }) + } + + return { + baseStyles: base, + baseHoverStyles: baseHover, + indicatorStyles: indicator, + } } function Checkbox() { const t = useTheme() - const {value, hovered, focused, disabled} = React.useContext(ItemContext) + const {value, hovered, focused, disabled, hasError} = + React.useContext(ItemContext) + const {baseStyles, baseHoverStyles, indicatorStyles} = + createSharedToggleStyles({ + theme: t, + hovered, + focused, + value, + disabled, + hasError, + }) return ( {value ? ( ) : null} @@ -318,7 +357,17 @@ function Checkbox() { function Switch() { const t = useTheme() - const {value, hovered, focused, disabled} = React.useContext(ItemContext) + const {value, hovered, focused, disabled, hasError} = + React.useContext(ItemContext) + const {baseStyles, baseHoverStyles, indicatorStyles} = + createSharedToggleStyles({ + theme: t, + hovered, + focused, + value, + disabled, + hasError, + }) return ( @@ -364,7 +409,17 @@ function Switch() { function Radio() { const t = useTheme() - const {value, hovered, focused, disabled} = React.useContext(ItemContext) + const {value, hovered, focused, disabled, hasError} = + React.useContext(ItemContext) + const {baseStyles, baseHoverStyles, indicatorStyles} = + createSharedToggleStyles({ + theme: t, + hovered, + focused, + value, + disabled, + hasError, + }) return ( {value ? ( ) : null} diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx index a6513641be..bd15e7fec4 100644 --- a/src/view/screens/Storybook/Forms.tsx +++ b/src/view/screens/Storybook/Forms.tsx @@ -146,6 +146,10 @@ export function Forms() { Click me + + + Click me + Click me + + + Click me + Click me + + + Click me + From 7c2b586467869947cb2ef32ef7a97b3c0ac52548 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 16 Jan 2024 14:03:08 -0600 Subject: [PATCH 43/68] Update dialog descriptions for demo --- src/view/screens/Storybook/Dialogs.tsx | 14 ++++++++------ src/view/screens/Storybook/index.tsx | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx index 5fd8856e55..56492be325 100644 --- a/src/view/screens/Storybook/Dialogs.tsx +++ b/src/view/screens/Storybook/Dialogs.tsx @@ -12,7 +12,7 @@ export function Dialogs() { const prompt = Prompt.usePromptControl() return ( - <> + + @@ -70,7 +71,6 @@ export function Storybook() { - From 2d80844b4ea68c5f9cd7c8c431198832b8a29beb Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 16 Jan 2024 15:45:43 -0600 Subject: [PATCH 44/68] Icons --- .../arrowTopRight_stoke2_corner0_rounded.svg | 1 + .../icons/globe_stroke2_corner0_rounded.svg | 1 + src/components/icons/ArrowTopRight.tsx | 27 ++++++++++++++++ src/components/icons/Globe.tsx | 27 ++++++++++++++++ src/components/icons/TEMPLATE.tsx | 27 ++++++++++++++++ src/components/icons/common.ts | 32 +++++++++++++++++++ src/view/screens/Storybook/Icons.tsx | 32 +++++++++++++++++++ src/view/screens/Storybook/index.tsx | 4 ++- 8 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 assets/icons/arrowTopRight_stoke2_corner0_rounded.svg create mode 100644 assets/icons/globe_stroke2_corner0_rounded.svg create mode 100644 src/components/icons/ArrowTopRight.tsx create mode 100644 src/components/icons/Globe.tsx create mode 100644 src/components/icons/TEMPLATE.tsx create mode 100644 src/components/icons/common.ts create mode 100644 src/view/screens/Storybook/Icons.tsx diff --git a/assets/icons/arrowTopRight_stoke2_corner0_rounded.svg b/assets/icons/arrowTopRight_stoke2_corner0_rounded.svg new file mode 100644 index 0000000000..554a7374ec --- /dev/null +++ b/assets/icons/arrowTopRight_stoke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/globe_stroke2_corner0_rounded.svg b/assets/icons/globe_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..83cb88d136 --- /dev/null +++ b/assets/icons/globe_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/src/components/icons/ArrowTopRight.tsx b/src/components/icons/ArrowTopRight.tsx new file mode 100644 index 0000000000..ca29730203 --- /dev/null +++ b/src/components/icons/ArrowTopRight.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import Svg, {Path} from 'react-native-svg' + +import {useCommonSVGProps, Props} from '#/components/icons/common' + +export const ArrowTopRight_Stroke2_Corner0_Rounded = React.forwardRef( + function LogoImpl(props: Props, ref) { + const {fill, size, style, ...rest} = useCommonSVGProps(props) + + return ( + + + + ) + }, +) diff --git a/src/components/icons/Globe.tsx b/src/components/icons/Globe.tsx new file mode 100644 index 0000000000..3801607ea3 --- /dev/null +++ b/src/components/icons/Globe.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import Svg, {Path} from 'react-native-svg' + +import {useCommonSVGProps, Props} from '#/components/icons/common' + +export const Globe_Stroke2_Corner0_Rounded = React.forwardRef(function LogoImpl( + props: Props, + ref, +) { + const {fill, size, style, ...rest} = useCommonSVGProps(props) + + return ( + + + + ) +}) diff --git a/src/components/icons/TEMPLATE.tsx b/src/components/icons/TEMPLATE.tsx new file mode 100644 index 0000000000..c3e53336bd --- /dev/null +++ b/src/components/icons/TEMPLATE.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import Svg, {Path} from 'react-native-svg' + +import {useCommonSVGProps, Props} from '#/components/icons/common' + +export const IconTemplate_Stroke2_Corner0_Rounded = React.forwardRef( + function LogoImpl(props: Props, ref) { + const {fill, size, style, ...rest} = useCommonSVGProps(props) + + return ( + + + + ) + }, +) diff --git a/src/components/icons/common.ts b/src/components/icons/common.ts new file mode 100644 index 0000000000..2eaafb2cd1 --- /dev/null +++ b/src/components/icons/common.ts @@ -0,0 +1,32 @@ +import {StyleSheet, TextProps} from 'react-native' +import type {SvgProps, PathProps} from 'react-native-svg' + +import {tokens} from '#/alf' + +export type Props = { + fill?: PathProps['fill'] + style?: TextProps['style'] + size?: keyof typeof sizes +} & Omit + +export const sizes = { + xs: 12, + sm: 16, + md: 20, + lg: 24, + xl: 28, +} + +export function useCommonSVGProps(props: Props) { + const {fill, size, ...rest} = props + const style = StyleSheet.flatten(rest.style) + const _fill = fill || style?.color || tokens.color.blue_500 + const _size = Number(size ? sizes[size] : rest.width || sizes.md) + + return { + fill: _fill, + size: _size, + style, + ...rest, + } +} diff --git a/src/view/screens/Storybook/Icons.tsx b/src/view/screens/Storybook/Icons.tsx new file mode 100644 index 0000000000..c3c41ce7e9 --- /dev/null +++ b/src/view/screens/Storybook/Icons.tsx @@ -0,0 +1,32 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' +import {H1} from '#/components/Typography' +import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' +import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight' + +export function Icons() { + const t = useTheme() + return ( + +

Icons

+ + + + + + + + + + + + + + + + +
+ ) +} diff --git a/src/view/screens/Storybook/index.tsx b/src/view/screens/Storybook/index.tsx index fa07cd9d79..9f627ad2c2 100644 --- a/src/view/screens/Storybook/index.tsx +++ b/src/view/screens/Storybook/index.tsx @@ -15,6 +15,7 @@ import {Forms} from './Forms' import {Dialogs} from './Dialogs' import {Breakpoints} from './Breakpoints' import {Shadows} from './Shadows' +import {Icons} from './Icons' export function Storybook() { const t = useTheme() @@ -53,7 +54,6 @@ export function Storybook() { Dark
- @@ -69,8 +69,10 @@ export function Storybook() { + +
From 0fc575084b970f2b2b2a86d4da53d51aac6a4de7 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 17 Jan 2024 11:18:55 -0600 Subject: [PATCH 45/68] WIP Toggle cleanup --- src/alf/themes.ts | 2 +- src/components/forms/Toggle.tsx | 297 +++++++++++++++------------ src/view/screens/Storybook/Forms.tsx | 59 ++++-- 3 files changed, 213 insertions(+), 145 deletions(-) diff --git a/src/alf/themes.ts b/src/alf/themes.ts index dc45944563..08aee75512 100644 --- a/src/alf/themes.ts +++ b/src/alf/themes.ts @@ -152,7 +152,7 @@ export const light = { backgroundColor: lightPalette.contrast_300, }, border: { - borderColor: lightPalette.contrast_100, + borderColor: lightPalette.contrast_200, }, border_contrast: { borderColor: lightPalette.contrast_400, diff --git a/src/components/forms/Toggle.tsx b/src/components/forms/Toggle.tsx index 875ccbbed5..0b07c88987 100644 --- a/src/components/forms/Toggle.tsx +++ b/src/components/forms/Toggle.tsx @@ -5,12 +5,10 @@ import {useTheme, atoms as a, web} from '#/alf' import {Text} from '#/components/Typography' import {useInteractionState} from '#/components/hooks/useInteractionState' import {StyleProp} from 'react-native' -import {AccessibilityRole} from 'react-native' type ItemState = { - id: string name: string - value: boolean + selected: boolean disabled: boolean hasError: boolean hovered: boolean @@ -19,9 +17,8 @@ type ItemState = { } const ItemContext = React.createContext({ - id: '', name: '', - value: false, + selected: false, disabled: false, hasError: false, hovered: false, @@ -32,15 +29,32 @@ const ItemContext = React.createContext({ const GroupContext = React.createContext<{ values: string[] disabled: boolean - role?: 'radio' | 'checkbox' + type: 'radio' | 'checkbox' + setFieldValue: (props: {name: string; value: boolean}) => void }>({ + type: 'checkbox', values: [], disabled: false, - role: 'checkbox', + setFieldValue: () => {}, }) -type ItemProps = Omit & { +export type GroupProps = React.PropsWithChildren<{ + type?: 'radio' | 'checkbox' + values: string[] + maxSelections?: number + disabled?: boolean + onChange: (value: string[]) => void + label: string + style?: StyleProp +}> + +export type ItemProps = Omit< + PressableProps, + 'children' | 'style' | 'onPress' | 'role' +> & { + type?: 'radio' | 'checkbox' name: string + label: string value?: boolean onChange?: ({name, value}: {name: string; value: boolean}) => void hasError?: boolean @@ -48,109 +62,31 @@ type ItemProps = Omit & { children: ((props: ItemState) => React.ReactNode) | React.ReactNode } -function Item({ - children, - name, - value = false, - disabled, - onChange, - hasError, - style, - role, -}: ItemProps) { - const labelId = React.useId() - const { - state: hovered, - onIn: onHoverIn, - onOut: onHoverOut, - } = useInteractionState() - const { - state: pressed, - onIn: onPressIn, - onOut: onPressOut, - } = useInteractionState() - const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() - - const onPress = React.useCallback(() => { - const next = !value - onChange?.({name, value: next}) - }, [name, value, onChange]) - - const state = React.useMemo( - () => ({ - id: labelId, - name, - value, - disabled: disabled ?? false, - hasError: hasError ?? false, - hovered, - pressed, - focused, - }), - [labelId, name, value, disabled, hovered, pressed, focused, hasError], - ) - - return ( - - - {typeof children === 'function' ? children(state) : children} - - - ) -} - function Group({ children, values: initialValues, onChange, - disabled, - role = 'checkbox', + disabled = false, + type = 'checkbox', maxSelections, style, -}: React.PropsWithChildren<{ - values: string[] - onChange: (value: string[]) => void - disabled?: boolean - role?: 'radio' | 'checkbox' - maxSelections?: number - style?: StyleProp -}>) { - const _disabled = disabled ?? false + label, +}: GroupProps) { + if (!initialValues) { + throw new Error(`Don't forget to pass in 'values' to your Toggle.Group`) + } + + const groupRole = type === 'radio' ? 'radiogroup' : undefined const [values, setValues] = React.useState( - role === 'radio' ? initialValues.slice(0, 1) : initialValues, + type === 'radio' ? initialValues.slice(0, 1) : initialValues, ) const [maxReached, setMaxReached] = React.useState(false) - const itemOnChange = React.useCallback< + const setFieldValue = React.useCallback< Exclude >( ({name, value}) => { - if (role === 'checkbox') { + if (type === 'checkbox') { setValues(s => { const state = s.filter(v => v !== name) return value ? state.concat(name) : state @@ -159,7 +95,7 @@ function Group({ setValues([name]) } }, - [role, setValues], + [type, setValues], ) React.useEffect(() => { @@ -167,7 +103,7 @@ function Group({ }, [values, onChange]) React.useEffect(() => { - if (role === 'checkbox') { + if (type === 'checkbox') { if ( maxSelections && values.length >= maxSelections && @@ -182,16 +118,35 @@ function Group({ setMaxReached(false) } } - }, [role, values.length, maxSelections, maxReached, setMaxReached]) + }, [type, values.length, maxSelections, maxReached, setMaxReached]) + + const context = React.useMemo( + () => ({ + values, + type, + disabled, + setFieldValue, + }), + [values, disabled, type, setFieldValue], + ) return ( - - + + {React.Children.map(children, child => { if (!React.isValidElement(child)) return null const isSelected = values.includes(child.props.name) - let isDisabled = _disabled || child.props.disabled + let isDisabled = disabled || child.props.disabled if (maxReached && !isSelected) { isDisabled = true @@ -202,9 +157,9 @@ function Group({ {React.cloneElement(child, { // @ts-ignore TODO figure out children types disabled: isDisabled, - role: role === 'radio' ? 'radio' : 'checkbox', + type: type === 'radio' ? 'radio' : 'checkbox', value: isSelected, - onChange: itemOnChange, + onChange: setFieldValue, })} ) : null @@ -214,12 +169,102 @@ function Group({ ) } +function Item({ + children, + name, + value = false, + disabled: itemDisabled = false, + onChange, + hasError, + style, + type = 'checkbox', + label, + ...rest +}: ItemProps) { + // context can be empty if used outside a Group + const { + values: selectedValues, + type: groupType, + disabled: groupDisabled, + setFieldValue, + } = React.useContext(GroupContext) + const { + state: hovered, + onIn: onHoverIn, + onOut: onHoverOut, + } = useInteractionState() + const { + state: pressed, + onIn: onPressIn, + onOut: onPressOut, + } = useInteractionState() + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + + const role = groupType === 'radio' ? 'radio' : type + const selected = selectedValues.includes(name) || !!value + const disabled = groupDisabled || itemDisabled + + const onPress = React.useCallback(() => { + const next = !value + setFieldValue({name, value: next}) + onChange?.({name, value: next}) // TODO don't use confusing method + }, [name, value, onChange, setFieldValue]) + + const state = React.useMemo( + () => ({ + name, + selected, + disabled: disabled ?? false, + hasError: hasError ?? false, + hovered, + pressed, + focused, + }), + [name, selected, disabled, hovered, pressed, focused, hasError], + ) + + return ( + + + {typeof children === 'function' ? children(state) : children} + + + ) +} + function Label({children}: React.PropsWithChildren<{}>) { const t = useTheme() - const {id, disabled} = React.useContext(ItemContext) + const {disabled} = React.useContext(ItemContext) return ( - value: boolean + selected: boolean hovered: boolean focused: boolean disabled: boolean @@ -251,7 +296,7 @@ function createSharedToggleStyles({ const baseHover: ViewStyle[] = [] const indicator: ViewStyle[] = [] - if (value) { + if (selected) { base.push({ backgroundColor: t.name === 'light' ? t.palette.primary_25 : t.palette.primary_900, @@ -308,14 +353,14 @@ function createSharedToggleStyles({ function Checkbox() { const t = useTheme() - const {value, hovered, focused, disabled, hasError} = + const {selected, hovered, focused, disabled, hasError} = React.useContext(ItemContext) const {baseStyles, baseHoverStyles, indicatorStyles} = createSharedToggleStyles({ theme: t, hovered, focused, - value, + selected, disabled, hasError, }) @@ -330,19 +375,19 @@ function Checkbox() { { height: 20, width: 20, - backgroundColor: value ? t.palette.primary_500 : undefined, - borderColor: value ? t.palette.primary_500 : undefined, + backgroundColor: selected ? t.palette.primary_500 : undefined, + borderColor: selected ? t.palette.primary_500 : undefined, }, baseStyles, hovered || focused ? baseHoverStyles : {}, ]}> - {value ? ( + {selected ? ( - {value ? ( + {selected ? ( Toggles console.log(e)} style={[a.gap_sm]}> - + Click me - + Click me - + Click me - + Click me - + Click me console.log(e)} style={[a.gap_sm]}> - + Click me - + Click me - + Click me - + Click me - + Click me console.log(e)} style={[a.gap_sm]}> - + Click me - + Click me - + Click me - + Click me - + Click me + + +

ToggleButton

+ + console.log(e)}> + + Hide + + + Warn + + + Show + + +
) } From f520214738bc7caae5b9d71266ba3fa20dafd2b6 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 17 Jan 2024 11:31:46 -0600 Subject: [PATCH 46/68] Refactor toggle to not rely on immediate children --- src/components/forms/Toggle.tsx | 43 +++++++++------------------- src/view/screens/Storybook/Forms.tsx | 5 ++++ 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/src/components/forms/Toggle.tsx b/src/components/forms/Toggle.tsx index 0b07c88987..0e913854a6 100644 --- a/src/components/forms/Toggle.tsx +++ b/src/components/forms/Toggle.tsx @@ -30,11 +30,13 @@ const GroupContext = React.createContext<{ values: string[] disabled: boolean type: 'radio' | 'checkbox' + maxSelectionsReached: boolean setFieldValue: (props: {name: string; value: boolean}) => void }>({ type: 'checkbox', values: [], disabled: false, + maxSelectionsReached: false, setFieldValue: () => {}, }) @@ -56,7 +58,7 @@ export type ItemProps = Omit< name: string label: string value?: boolean - onChange?: ({name, value}: {name: string; value: boolean}) => void + onChange?: (selected: boolean) => void hasError?: boolean style?: (state: ItemState) => ViewStyle children: ((props: ItemState) => React.ReactNode) | React.ReactNode @@ -83,7 +85,7 @@ function Group({ const [maxReached, setMaxReached] = React.useState(false) const setFieldValue = React.useCallback< - Exclude + (props: {name: string; value: boolean}) => void >( ({name, value}) => { if (type === 'checkbox') { @@ -125,9 +127,10 @@ function Group({ values, type, disabled, + maxSelectionsReached: maxReached, setFieldValue, }), - [values, disabled, type, setFieldValue], + [values, disabled, type, maxReached, setFieldValue], ) return ( @@ -142,28 +145,7 @@ function Group({ accessibilityRole: groupRole, } : {})}> - {React.Children.map(children, child => { - if (!React.isValidElement(child)) return null - - const isSelected = values.includes(child.props.name) - let isDisabled = disabled || child.props.disabled - - if (maxReached && !isSelected) { - isDisabled = true - } - - return React.isValidElement(child) ? ( - - {React.cloneElement(child, { - // @ts-ignore TODO figure out children types - disabled: isDisabled, - type: type === 'radio' ? 'radio' : 'checkbox', - value: isSelected, - onChange: setFieldValue, - })} - - ) : null - })} + {children}
) @@ -181,12 +163,12 @@ function Item({ label, ...rest }: ItemProps) { - // context can be empty if used outside a Group const { values: selectedValues, type: groupType, disabled: groupDisabled, setFieldValue, + maxSelectionsReached, } = React.useContext(GroupContext) const { state: hovered, @@ -202,13 +184,14 @@ function Item({ const role = groupType === 'radio' ? 'radio' : type const selected = selectedValues.includes(name) || !!value - const disabled = groupDisabled || itemDisabled + const disabled = + groupDisabled || itemDisabled || (!selected && maxSelectionsReached) const onPress = React.useCallback(() => { - const next = !value + const next = !selected setFieldValue({name, value: next}) - onChange?.({name, value: next}) // TODO don't use confusing method - }, [name, value, onChange, setFieldValue]) + onChange?.(next) + }, [name, selected, onChange, setFieldValue]) const state = React.useMemo( () => ({ diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx index fd0b1ca706..b76509c3a6 100644 --- a/src/view/screens/Storybook/Forms.tsx +++ b/src/view/screens/Storybook/Forms.tsx @@ -125,6 +125,11 @@ export function Forms() {

Toggles

+ + + Uncontrolled toggle + + Date: Wed, 17 Jan 2024 14:03:00 -0600 Subject: [PATCH 47/68] Make Toggle controlled --- src/components/forms/Toggle.tsx | 29 ++--- src/components/forms/ToggleButton.tsx | 78 ++++++++++++++ src/view/screens/Storybook/Forms.tsx | 150 ++++++++++++++------------ 3 files changed, 168 insertions(+), 89 deletions(-) create mode 100644 src/components/forms/ToggleButton.tsx diff --git a/src/components/forms/Toggle.tsx b/src/components/forms/Toggle.tsx index 0e913854a6..4145895d81 100644 --- a/src/components/forms/Toggle.tsx +++ b/src/components/forms/Toggle.tsx @@ -4,7 +4,6 @@ import {Pressable, PressableProps, View, ViewStyle} from 'react-native' import {useTheme, atoms as a, web} from '#/alf' import {Text} from '#/components/Typography' import {useInteractionState} from '#/components/hooks/useInteractionState' -import {StyleProp} from 'react-native' type ItemState = { name: string @@ -47,7 +46,6 @@ export type GroupProps = React.PropsWithChildren<{ disabled?: boolean onChange: (value: string[]) => void label: string - style?: StyleProp }> export type ItemProps = Omit< @@ -66,22 +64,15 @@ export type ItemProps = Omit< function Group({ children, - values: initialValues, + values: providedValues, onChange, disabled = false, type = 'checkbox', maxSelections, - style, label, }: GroupProps) { - if (!initialValues) { - throw new Error(`Don't forget to pass in 'values' to your Toggle.Group`) - } - const groupRole = type === 'radio' ? 'radiogroup' : undefined - const [values, setValues] = React.useState( - type === 'radio' ? initialValues.slice(0, 1) : initialValues, - ) + const values = type === 'radio' ? providedValues.slice(0, 1) : providedValues const [maxReached, setMaxReached] = React.useState(false) const setFieldValue = React.useCallback< @@ -89,21 +80,16 @@ function Group({ >( ({name, value}) => { if (type === 'checkbox') { - setValues(s => { - const state = s.filter(v => v !== name) - return value ? state.concat(name) : state - }) + const pruned = values.filter(v => v !== name) + const next = value ? pruned.concat(name) : pruned + onChange(next) } else { - setValues([name]) + onChange([name]) } }, - [type, setValues], + [type, onChange, values], ) - React.useEffect(() => { - onChange(values) - }, [values, onChange]) - React.useEffect(() => { if (type === 'checkbox') { if ( @@ -137,7 +123,6 @@ function Group({ & + AccessibilityProps & + React.PropsWithChildren<{}> + +export type GroupProps = Omit & { + multiple?: boolean +} + +export function Group({children, multiple, ...props}: GroupProps) { + const t = useTheme() + return ( + + + {children} + + + ) +} + +export function Button({children, ...props}: ItemProps) { + const t = useTheme() + return ( + + {state => ( + + {typeof children === 'string' ? ( + + {children} + + ) : ( + children + )} + + )} + + ) +} + +export default { + Group, + Button, +} diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx index b76509c3a6..1c2a9f4c46 100644 --- a/src/view/screens/Storybook/Forms.tsx +++ b/src/view/screens/Storybook/Forms.tsx @@ -9,8 +9,11 @@ import {InputGroup} from '#/components/forms/InputGroup' import {Logo} from '#/view/icons/Logo' import Toggle from '#/components/forms/Toggle' import ToggleButton from '#/components/forms/ToggleButton' +import {Button} from '#/components/Button' export function Forms() { + const [toggleGroupValues, setToggleGroupValues] = React.useState(['a']) + return (

Forms

@@ -135,85 +138,98 @@ export function Forms() { type="checkbox" maxSelections={2} values={['a', 'b']} - onChange={e => console.log(e)} - style={[a.gap_sm]}> - - - Click me - - - - Click me - - - - Click me - - - - Click me - - - - Click me - + onChange={e => console.log(e)}> + + + + Click me + + + + Click me + + + + Click me + + + + Click me + + + + Click me + +
console.log(e)} - style={[a.gap_sm]}> - - - Click me - - - - Click me - - - - Click me - - - - Click me - - - - Click me - + values={toggleGroupValues} + onChange={setToggleGroupValues}> + + + + Click me + + + + Click me + + + + Click me + + + + Click me + + + + Click me + + + + console.log(e)} - style={[a.gap_sm]}> - - - Click me - - - - Click me - - - - Click me - - - - Click me - - - - Click me - + onChange={e => console.log(e)}> + + + + Click me + + + + Click me + + + + Click me + + + + Click me + + + + Click me + +
From d3d329dc500be1f025fed2ded79cd9bbd58c9afe Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 17 Jan 2024 14:09:59 -0600 Subject: [PATCH 48/68] Clean up Toggles storybook --- src/components/forms/Toggle.tsx | 10 ++--- src/view/screens/Storybook/Forms.tsx | 56 ++++++++++++++-------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/components/forms/Toggle.tsx b/src/components/forms/Toggle.tsx index 4145895d81..7f506f862a 100644 --- a/src/components/forms/Toggle.tsx +++ b/src/components/forms/Toggle.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {Pressable, PressableProps, View, ViewStyle} from 'react-native' +import {Pressable, View, ViewStyle} from 'react-native' import {useTheme, atoms as a, web} from '#/alf' import {Text} from '#/components/Typography' @@ -48,14 +48,12 @@ export type GroupProps = React.PropsWithChildren<{ label: string }> -export type ItemProps = Omit< - PressableProps, - 'children' | 'style' | 'onPress' | 'role' -> & { +export type ItemProps = { type?: 'radio' | 'checkbox' name: string label: string value?: boolean + disabled?: boolean onChange?: (selected: boolean) => void hasError?: boolean style?: (state: ItemState) => ViewStyle @@ -314,7 +312,7 @@ function createSharedToggleStyles({ return { baseStyles: base, - baseHoverStyles: baseHover, + baseHoverStyles: disabled ? [] : baseHover, indicatorStyles: indicator, } } diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx index 1c2a9f4c46..d02a4bfe7a 100644 --- a/src/view/screens/Storybook/Forms.tsx +++ b/src/view/screens/Storybook/Forms.tsx @@ -12,7 +12,9 @@ import ToggleButton from '#/components/forms/ToggleButton' import {Button} from '#/components/Button' export function Forms() { - const [toggleGroupValues, setToggleGroupValues] = React.useState(['a']) + const [toggleGroupAValues, setToggleGroupAValues] = React.useState(['a']) + const [toggleGroupBValues, setToggleGroupBValues] = React.useState(['a', 'b']) + const [toggleGroupCValues, setToggleGroupCValues] = React.useState(['a', 'b']) return ( @@ -137,29 +139,39 @@ export function Forms() { label="Toggle" type="checkbox" maxSelections={2} - values={['a', 'b']} - onChange={e => console.log(e)}> + values={toggleGroupAValues} + onChange={setToggleGroupAValues}> - + Click me - + Click me - + Click me - + Click me - + Click me + + @@ -167,47 +179,37 @@ export function Forms() { label="Toggle" type="checkbox" maxSelections={2} - values={toggleGroupValues} - onChange={setToggleGroupValues}> + values={toggleGroupBValues} + onChange={setToggleGroupBValues}> - + Click me - + Click me - + Click me - + Click me - + Click me - - console.log(e)}> + values={toggleGroupCValues} + onChange={setToggleGroupCValues}> From 623047977beacd1b0aace7380a5ff1e41c888911 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 17 Jan 2024 14:53:51 -0600 Subject: [PATCH 49/68] ToggleButton styles --- src/components/forms/Toggle.tsx | 12 +-- src/components/forms/ToggleButton.tsx | 108 +++++++++++++++++++------- src/view/screens/Storybook/Forms.tsx | 5 +- 3 files changed, 89 insertions(+), 36 deletions(-) diff --git a/src/components/forms/Toggle.tsx b/src/components/forms/Toggle.tsx index 7f506f862a..ed678e7fd6 100644 --- a/src/components/forms/Toggle.tsx +++ b/src/components/forms/Toggle.tsx @@ -60,6 +60,10 @@ export type ItemProps = { children: ((props: ItemState) => React.ReactNode) | React.ReactNode } +export function useItemContext() { + return React.useContext(ItemContext) +} + function Group({ children, values: providedValues, @@ -228,7 +232,7 @@ function Item({ function Label({children}: React.PropsWithChildren<{}>) { const t = useTheme() - const {disabled} = React.useContext(ItemContext) + const {disabled} = useItemContext() return ( & @@ -36,39 +37,88 @@ export function Group({children, multiple, ...props}: GroupProps) { } export function Button({children, ...props}: ItemProps) { - const t = useTheme() return ( - {state => ( - {children} + + ) +} + +function ButtonInner({children}: React.PropsWithChildren<{}>) { + const t = useTheme() + const state = useItemContext() + + const {baseStyles, hoverStyles, activeStyles, textStyles} = + React.useMemo(() => { + const base: ViewStyle[] = [] + const hover: ViewStyle[] = [] + const active: ViewStyle[] = [] + const text: TextStyle[] = [] + + hover.push(t.atoms.bg_contrast_100) + + if (state.selected) { + active.push({ + backgroundColor: t.palette.contrast_800, + }) + text.push(t.atoms.text_inverted) + hover.push({ + backgroundColor: t.palette.contrast_800, + }) + + if (state.disabled) { + active.push({ + backgroundColor: t.palette.contrast_500, + }) + } + } + + if (state.disabled) { + base.push({ + backgroundColor: t.palette.contrast_100, + }) + text.push({ + opacity: 0.5, + }) + } + + return { + baseStyles: base, + hoverStyles: hover, + activeStyles: active, + textStyles: text, + } + }, [t, state]) + + return ( + + {typeof children === 'string' ? ( + - {typeof children === 'string' ? ( - - {children} - - ) : ( - children - )} - + {children} + + ) : ( + children )} - + ) } diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx index d02a4bfe7a..c7dd998b6e 100644 --- a/src/view/screens/Storybook/Forms.tsx +++ b/src/view/screens/Storybook/Forms.tsx @@ -15,6 +15,7 @@ export function Forms() { const [toggleGroupAValues, setToggleGroupAValues] = React.useState(['a']) const [toggleGroupBValues, setToggleGroupBValues] = React.useState(['a', 'b']) const [toggleGroupCValues, setToggleGroupCValues] = React.useState(['a', 'b']) + const [toggleGroupDValues, setToggleGroupDValues] = React.useState(['warn']) return ( @@ -240,8 +241,8 @@ export function Forms() { console.log(e)}> + values={toggleGroupDValues} + onChange={setToggleGroupDValues}> Hide From 449c29b94b91b1ab2aac5224dae54b280c053af8 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 17 Jan 2024 15:22:28 -0600 Subject: [PATCH 50/68] Improve a11y labels --- src/components/Button.tsx | 18 +++++++----------- src/components/Dialog/index.web.tsx | 5 +++-- src/components/Prompt.tsx | 6 ++---- src/components/forms/Toggle.tsx | 2 +- src/view/screens/Storybook/Buttons.tsx | 18 ++++++------------ src/view/screens/Storybook/Forms.tsx | 3 +-- src/view/screens/Storybook/Links.tsx | 15 +++++---------- 7 files changed, 25 insertions(+), 42 deletions(-) diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 7c032dbad8..06226ad1e0 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -38,10 +38,8 @@ export type VariantProps = { size?: ButtonSize } -export type ButtonProps = Omit< - PressableProps, - 'children' | 'style' | 'accessibilityLabel' | 'accessibilityHint' -> & +export type ButtonProps = Pick & + AccessibilityProps & VariantProps & { children: | ((props: { @@ -56,8 +54,7 @@ export type ButtonProps = Omit< }) => React.ReactNode) | React.ReactNode | string - accessibilityLabel: Required['accessibilityLabel'] - accessibilityHint: Required['accessibilityHint'] + label: string } export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean} @@ -66,8 +63,7 @@ export function Button({ variant, color, size, - accessibilityLabel, - accessibilityHint, + label, disabled = false, ...rest }: ButtonProps) { @@ -325,11 +321,11 @@ export function Button({ return ( {children} @@ -108,8 +107,7 @@ export function Action({ variant="solid" color="primary" size="small" - accessibilityLabel="Confirm" - accessibilityHint="Confirm this action" + label="Confirm" onPress={handleOnPress}> {children} diff --git a/src/components/forms/Toggle.tsx b/src/components/forms/Toggle.tsx index ed678e7fd6..8fe43f2f41 100644 --- a/src/components/forms/Toggle.tsx +++ b/src/components/forms/Toggle.tsx @@ -196,6 +196,7 @@ function Item({ return ( + label="Click here"> Button @@ -46,8 +44,7 @@ export function Buttons() { variant="gradient" color={name as ButtonColor} size="large" - accessibilityLabel="Click here" - accessibilityHint="Opens something"> + label="Click here"> Button @@ -71,8 +67,7 @@ export function Buttons() { variant="gradient" color={name as ButtonColor} size="large" - accessibilityLabel="Click here" - accessibilityHint="Opens something"> + label="Click here"> Button diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx index c7dd998b6e..66268cb802 100644 --- a/src/view/screens/Storybook/Forms.tsx +++ b/src/view/screens/Storybook/Forms.tsx @@ -168,8 +168,7 @@ export function Forms() { variant="solid" color="primary" size="small" - accessibilityLabel="Reset" - accessibilityHint="Reset" + label="Reset" onPress={() => setToggleGroupAValues(['a'])}> Reset diff --git a/src/view/screens/Storybook/Links.tsx b/src/view/screens/Storybook/Links.tsx index b5c183425a..2b9da2a9de 100644 --- a/src/view/screens/Storybook/Links.tsx +++ b/src/view/screens/Storybook/Links.tsx @@ -13,31 +13,27 @@ export function Links() { External

External with custom children

https://blueskyweb.xyz @@ -45,8 +41,7 @@ export function Links() { Date: Wed, 17 Jan 2024 15:23:52 -0600 Subject: [PATCH 51/68] ToggleButton hover darkmode --- src/components/forms/ToggleButton.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/forms/ToggleButton.tsx b/src/components/forms/ToggleButton.tsx index 01edb45ad1..f6578c90e2 100644 --- a/src/components/forms/ToggleButton.tsx +++ b/src/components/forms/ToggleButton.tsx @@ -55,7 +55,9 @@ function ButtonInner({children}: React.PropsWithChildren<{}>) { const active: ViewStyle[] = [] const text: TextStyle[] = [] - hover.push(t.atoms.bg_contrast_100) + hover.push( + t.name === 'light' ? t.atoms.bg_contrast_100 : t.atoms.bg_contrast_25, + ) if (state.selected) { active.push({ From 13409f889c6114b98d5f40aa26b7f7f12e741dd2 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 17 Jan 2024 15:30:52 -0600 Subject: [PATCH 52/68] Some i18n --- src/components/Dialog/index.web.tsx | 11 ++++++----- src/components/Prompt.tsx | 8 ++++++-- src/view/screens/Storybook/Forms.tsx | 22 +++++++++++++--------- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx index 88ebbbc1d2..bb7b6d33df 100644 --- a/src/components/Dialog/index.web.tsx +++ b/src/components/Dialog/index.web.tsx @@ -2,6 +2,8 @@ import React, {useImperativeHandle} from 'react' import {View, TouchableWithoutFeedback} from 'react-native' import {FocusScope} from '@tamagui/focus-scope' import Animated, {FadeInDown, FadeIn} from 'react-native-reanimated' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {useTheme, atoms as a, useBreakpoints, web} from '#/alf' import {Text} from '#/components/Typography' @@ -22,6 +24,7 @@ export function Outer({ onClose, children, }: React.PropsWithChildren) { + const {_} = useLingui() const t = useTheme() const {gtMobile} = useBreakpoints() const [isOpen, setIsOpen] = React.useState(false) @@ -74,7 +77,7 @@ export function Outer({ - @@ -97,6 +100,7 @@ export function Action({ children, onPress, }: React.PropsWithChildren<{onPress?: () => void}>) { + const {_} = useLingui() const {close} = Dialog.useDialogContext() const handleOnPress = React.useCallback(() => { close() @@ -107,7 +111,7 @@ export function Action({ variant="solid" color="primary" size="small" - label="Confirm" + label={_(msg`Confirm`)} onPress={handleOnPress}> {children} diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx index 66268cb802..cee24af288 100644 --- a/src/view/screens/Storybook/Forms.tsx +++ b/src/view/screens/Storybook/Forms.tsx @@ -163,15 +163,6 @@ export function Forms() { Click me - -
@@ -235,6 +226,19 @@ export function Forms() {
+ +

ToggleButton

From 1e5f213f12837ddecab69424c84891237ea7be8c Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 18 Jan 2024 11:06:54 -0600 Subject: [PATCH 53/68] Refactor input --- src/components/forms/InputText.tsx | 52 ++-- src/components/forms/TextField.tsx | 316 +++++++++++++++++++++++++ src/components/forms/types.ts | 21 +- src/components/icons/ArrowTopRight.tsx | 8 +- src/components/icons/Globe.tsx | 7 +- src/components/icons/TEMPLATE.tsx | 4 +- src/view/screens/Storybook/Forms.tsx | 100 +++----- 7 files changed, 394 insertions(+), 114 deletions(-) create mode 100644 src/components/forms/TextField.tsx diff --git a/src/components/forms/InputText.tsx b/src/components/forms/InputText.tsx index 8933b1d085..67b50176f8 100644 --- a/src/components/forms/InputText.tsx +++ b/src/components/forms/InputText.tsx @@ -7,7 +7,7 @@ import { LayoutChangeEvent, } from 'react-native' -import {useTheme, atoms, web, tokens} from '#/alf' +import {useTheme, atoms as a, web, tokens} from '#/alf' import {Text} from '#/components/Typography' import {useInteractionState} from '#/components/hooks/useInteractionState' @@ -15,7 +15,7 @@ import {BaseProps} from '#/components/forms/types' type Props = BaseProps & Omit & { - placeholder: Required['placeholder'] + placeholder?: Required['placeholder'] icon?: React.FunctionComponent suffix?: React.FunctionComponent } @@ -25,8 +25,6 @@ export function createTextInput(Input: typeof TextInput) { value: initialValue, onChange, testID, - accessibilityLabel, - accessibilityHint, label, hasError, icon: Icon, @@ -146,21 +144,22 @@ export function createTextInput(Input: typeof TextInput) { ) return ( - + {label && ( {label} )} diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx new file mode 100644 index 0000000000..0568caaf61 --- /dev/null +++ b/src/components/forms/TextField.tsx @@ -0,0 +1,316 @@ +import React from 'react' +import { + View, + TextInput, + TextInputProps, + TextStyle, + ViewStyle, + Pressable, + StyleSheet, + AccessibilityProps, +} from 'react-native' + +import {isWeb} from '#/platform/detection' +import {useTheme, atoms as a, web, tokens} from '#/alf' +import {Text} from '#/components/Typography' +import {useInteractionState} from '#/components/hooks/useInteractionState' +import {Props as SVGIconProps} from '#/components/icons/common' + +const Context = React.createContext<{ + inputRef: React.RefObject | null + isInvalid: boolean + hovered: boolean + onHoverIn: () => void + onHoverOut: () => void + focused: boolean + onFocus: () => void + onBlur: () => void +}>({ + inputRef: null, + isInvalid: false, + hovered: false, + onHoverIn: () => {}, + onHoverOut: () => {}, + focused: false, + onFocus: () => {}, + onBlur: () => {}, +}) + +export type RootProps = React.PropsWithChildren<{isInvalid?: boolean}> + +function Root({children, isInvalid = false}: RootProps) { + const inputRef = React.useRef(null) + const rootRef = React.useRef(null) + const { + state: hovered, + onIn: onHoverIn, + onOut: onHoverOut, + } = useInteractionState() + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + + const context = React.useMemo( + () => ({ + inputRef, + hovered, + onHoverIn, + onHoverOut, + focused, + onFocus, + onBlur, + isInvalid, + }), + [ + inputRef, + hovered, + onHoverIn, + onHoverOut, + focused, + onFocus, + onBlur, + isInvalid, + ], + ) + + React.useLayoutEffect(() => { + const root = rootRef.current + if (!root || !isWeb) return + // @ts-ignore web only + root.tabIndex = -1 + }, []) + + return ( + + inputRef.current?.focus()} + onHoverIn={onHoverIn} + onHoverOut={onHoverOut}> + {children} + + + ) +} + +function useSharedInputStyles() { + const t = useTheme() + return React.useMemo(() => { + const hover: ViewStyle[] = [ + { + borderColor: t.palette.contrast_100, + }, + ] + const focus: ViewStyle[] = [ + { + backgroundColor: t.palette.contrast_50, + borderColor: t.palette.primary_500, + }, + ] + const error: ViewStyle[] = [ + { + backgroundColor: + t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900, + borderColor: + t.name === 'light' ? t.palette.negative_300 : t.palette.negative_800, + }, + ] + const errorHover: ViewStyle[] = [ + { + backgroundColor: + t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900, + borderColor: tokens.color.red_500, + }, + ] + + return { + chromeHover: StyleSheet.flatten(hover), + chromeFocus: StyleSheet.flatten(focus), + chromeError: StyleSheet.flatten(error), + chromeErrorHover: StyleSheet.flatten(errorHover), + } + }, [t]) +} + +export type InputProps = Omit & { + label: string + value: string + onChangeText: (value: string) => void + isInvalid?: boolean +} + +function Input({ + label, + placeholder, + value, + onChangeText, + isInvalid, + ...rest +}: InputProps) { + const t = useTheme() + const ctx = React.useContext(Context) + const withinRoot = Boolean(ctx.inputRef) + + const {chromeHover, chromeFocus, chromeError, chromeErrorHover} = + useSharedInputStyles() + + if (!withinRoot) { + return ( + + + + ) + } + + return ( + <> + + + + + ) +} + +function Icon({icon: Comp}: {icon: React.ComponentType}) { + const t = useTheme() + const ctx = React.useContext(Context) + const {hover, focus, errorHover, errorFocus} = React.useMemo(() => { + const hover: TextStyle[] = [ + { + color: t.palette.contrast_800, + }, + ] + const focus: TextStyle[] = [ + { + color: t.palette.primary_500, + }, + ] + const errorHover: TextStyle[] = [ + { + color: t.palette.negative_500, + }, + ] + const errorFocus: TextStyle[] = [ + { + color: t.palette.negative_500, + }, + ] + + return { + hover, + focus, + errorHover, + errorFocus, + } + }, [t]) + + return ( + + + + ) +} + +function Suffix({ + children, + label, + accessibilityHint, +}: React.PropsWithChildren<{ + label: string + accessibilityHint?: AccessibilityProps['accessibilityHint'] +}>) { + const t = useTheme() + const ctx = React.useContext(Context) + return ( + + {children} + + ) +} + +export default { + Root, + Input, + Icon, + Suffix, +} diff --git a/src/components/forms/types.ts b/src/components/forms/types.ts index c43b760b0b..8d94bf0d1c 100644 --- a/src/components/forms/types.ts +++ b/src/components/forms/types.ts @@ -2,17 +2,10 @@ import {AccessibilityProps} from 'react-native' export type RequiredAccessibilityProps = Required -export type BaseProps = Omit< - AccessibilityProps, - 'accessibilityLabel' | 'accessibilityHint' -> & - Pick< - RequiredAccessibilityProps, - 'accessibilityLabel' | 'accessibilityHint' - > & { - value: T - onChange: (value: T) => void - testID: string - label?: string - hasError?: boolean - } +export type BaseProps = AccessibilityProps & { + value: T + onChange: (value: T) => void + testID?: string + label: string + hasError?: boolean +} diff --git a/src/components/icons/ArrowTopRight.tsx b/src/components/icons/ArrowTopRight.tsx index ca29730203..368c45308d 100644 --- a/src/components/icons/ArrowTopRight.tsx +++ b/src/components/icons/ArrowTopRight.tsx @@ -14,11 +14,13 @@ export const ArrowTopRight_Stroke2_Corner0_Rounded = React.forwardRef( // @ts-ignore it's fiiiiine ref={ref} viewBox="0 0 24 24" - style={[{width: size}, style]}> + width={size} + height={size} + style={[style]}> diff --git a/src/components/icons/Globe.tsx b/src/components/icons/Globe.tsx index 3801607ea3..28187266ac 100644 --- a/src/components/icons/Globe.tsx +++ b/src/components/icons/Globe.tsx @@ -16,10 +16,13 @@ export const Globe_Stroke2_Corner0_Rounded = React.forwardRef(function LogoImpl( // @ts-ignore it's fiiiiine ref={ref} viewBox="0 0 24 24" - style={[{width: size}, style]}> + width={size} + height={size} + style={[style]}> diff --git a/src/components/icons/TEMPLATE.tsx b/src/components/icons/TEMPLATE.tsx index c3e53336bd..c47849aca4 100644 --- a/src/components/icons/TEMPLATE.tsx +++ b/src/components/icons/TEMPLATE.tsx @@ -14,7 +14,9 @@ export const IconTemplate_Stroke2_Corner0_Rounded = React.forwardRef( // @ts-ignore it's fiiiiine ref={ref} viewBox="0 0 24 24" - style={[{width: size}, style]}> + width={size} + height={size} + style={[style]}>

Forms

@@ -24,77 +27,43 @@ export function Forms() {

InputText

- console.log(text)} - /> - console.log(text)} - /> - console.log(text)} - icon={Logo} - /> - console.log(text)} - icon={Logo} - /> - console.log(text)} - icon={Logo} - suffix={() => .bksy.social} - /> - console.log(text)} + + + + + + + + + + @gmail.com + +

InputDate

console.log(date)} - accessibilityLabel="Date" - accessibilityHint="Enter a date" + label="Input" /> console.log(date)} - accessibilityLabel="Date" - accessibilityHint="Enter a date" + label="Input" />
@@ -103,24 +72,21 @@ export function Forms() { console.log(text)} /> console.log(text)} /> console.log(text)} From 43dc77491ee33c7eda55efa4b5ec290078c41b70 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 18 Jan 2024 11:17:27 -0600 Subject: [PATCH 54/68] Allow extension of input --- src/components/Dialog/index.tsx | 4 +- src/components/forms/TextField.tsx | 132 +++++++++++++------------ src/view/screens/Storybook/Dialogs.tsx | 18 +--- 3 files changed, 75 insertions(+), 79 deletions(-) diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index 87d0a08372..44e4dc8a70 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -10,7 +10,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useTheme, atoms as a} from '#/alf' import {Portal} from '#/components/Portal' -import {createTextInput} from '#/components/forms/InputText' +import {createInput} from '#/components/forms/TextField' import { DialogOuterProps, @@ -22,7 +22,7 @@ import {Context} from '#/components/Dialog/context' export {useDialogControl, useDialogContext} from '#/components/Dialog/context' export * from '#/components/Dialog/types' // @ts-ignore -export const InputText = createTextInput(BottomSheetTextInput) +export const Input = createInput(BottomSheetTextInput) export function Outer({ children, diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx index 0568caaf61..bcf7dafe0d 100644 --- a/src/components/forms/TextField.tsx +++ b/src/components/forms/TextField.tsx @@ -149,79 +149,85 @@ export type InputProps = Omit & { isInvalid?: boolean } -function Input({ - label, - placeholder, - value, - onChangeText, - isInvalid, - ...rest -}: InputProps) { - const t = useTheme() - const ctx = React.useContext(Context) - const withinRoot = Boolean(ctx.inputRef) +export function createInput(Component: typeof TextInput) { + return function Input({ + label, + placeholder, + value, + onChangeText, + isInvalid, + ...rest + }: InputProps) { + const t = useTheme() + const ctx = React.useContext(Context) + const withinRoot = Boolean(ctx.inputRef) - const {chromeHover, chromeFocus, chromeError, chromeErrorHover} = - useSharedInputStyles() + const {chromeHover, chromeFocus, chromeError, chromeErrorHover} = + useSharedInputStyles() + + if (!withinRoot) { + return ( + + + + ) + } - if (!withinRoot) { return ( - - + + + - + ) } - - return ( - <> - - - - - ) } +const Input = createInput(TextInput) + function Icon({icon: Comp}: {icon: React.ComponentType}) { const t = useTheme() const ctx = React.useContext(Context) diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx index 56492be325..d4dcc91938 100644 --- a/src/view/screens/Storybook/Dialogs.tsx +++ b/src/view/screens/Storybook/Dialogs.tsx @@ -18,8 +18,7 @@ export function Dialogs() { color="secondary" size="small" onPress={() => control.open()} - accessibilityLabel="Open basic dialog" - accessibilityHint="Open basic dialog"> + label="Open basic dialog"> Open basic dialog @@ -28,8 +27,7 @@ export function Dialogs() { color="primary" size="small" onPress={() => prompt.open()} - accessibilityLabel="Open prompt" - accessibilityHint="Open prompt"> + label="Open prompt"> Open prompt @@ -58,14 +56,7 @@ export function Dialogs() {

A scrollable dialog with an input within it.

- {}} - placeholder="Type here" - accessibilityLabel="Type" - accessibilityHint="Type" - /> + {}} label="Type here" /> From 7faaf1996a8f6edf50098b5852a51f57ef422d57 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 18 Jan 2024 11:19:23 -0600 Subject: [PATCH 55/68] Remove old input --- src/components/forms/InputText.tsx | 248 --------------------------- src/view/screens/Storybook/Forms.tsx | 29 ---- 2 files changed, 277 deletions(-) delete mode 100644 src/components/forms/InputText.tsx diff --git a/src/components/forms/InputText.tsx b/src/components/forms/InputText.tsx deleted file mode 100644 index 67b50176f8..0000000000 --- a/src/components/forms/InputText.tsx +++ /dev/null @@ -1,248 +0,0 @@ -import React from 'react' -import { - View, - TextInput, - TextInputProps, - TextStyle, - LayoutChangeEvent, -} from 'react-native' - -import {useTheme, atoms as a, web, tokens} from '#/alf' -import {Text} from '#/components/Typography' -import {useInteractionState} from '#/components/hooks/useInteractionState' - -import {BaseProps} from '#/components/forms/types' - -type Props = BaseProps & - Omit & { - placeholder?: Required['placeholder'] - icon?: React.FunctionComponent - suffix?: React.FunctionComponent - } - -export function createTextInput(Input: typeof TextInput) { - return function InputText({ - value: initialValue, - onChange, - testID, - label, - hasError, - icon: Icon, - suffix: Suffix, - ...props - }: Props) { - const labelId = React.useId() - const t = useTheme() - const [value, setValue] = React.useState(initialValue) - const [suffixPadding, setSuffixPadding] = React.useState(0) - const { - state: hovered, - onIn: onHoverIn, - onOut: onHoverOut, - } = useInteractionState() - const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() - const hasIcon = !!Icon - - const handleSuffixLayout = React.useCallback( - (e: LayoutChangeEvent) => { - setSuffixPadding(e.nativeEvent.layout.width + 16) - }, - [setSuffixPadding], - ) - - const {inputBaseStyles, inputHoverStyles, inputFocusStyles} = - React.useMemo(() => { - const base: TextStyle[] = [] - const hover: TextStyle[] = [ - { - borderColor: t.palette.contrast_300, - }, - ] - const focus: TextStyle[] = [ - { - backgroundColor: t.palette.contrast_50, - borderColor: t.palette.primary_500, - }, - ] - - if (hasIcon) { - base.push({ - paddingLeft: 40, - }) - } - - if (hasError) { - base.push({ - backgroundColor: - t.name === 'light' - ? t.palette.negative_25 - : t.palette.negative_900, - borderColor: - t.name === 'light' - ? t.palette.negative_300 - : t.palette.negative_800, - }) - hover.push({ - borderColor: tokens.color.red_500, - }) - focus.push({ - backgroundColor: - t.name === 'light' - ? t.palette.negative_25 - : t.palette.negative_900, - borderColor: tokens.color.red_500, - }) - } - - return { - inputBaseStyles: base, - inputHoverStyles: hover, - inputFocusStyles: focus, - } - }, [t, hasError, hasIcon]) - - const {iconBaseStyles, iconHoverStyles, iconFocusStyles} = - React.useMemo(() => { - const base: TextStyle[] = [] - const hover: TextStyle[] = [ - { - color: t.palette.contrast_500, - }, - ] - const focus: TextStyle[] = [ - { - color: t.palette.primary_500, - }, - ] - - if (hasError) { - base.push({ - color: t.palette.negative_400, - }) - hover.push({ - color: t.palette.negative_500, - }) - focus.push({ - color: t.palette.negative_500, - }) - } - - return { - iconBaseStyles: base, - iconHoverStyles: hover, - iconFocusStyles: focus, - } - }, [t, hasError]) - - const handleOnChange = React.useCallback( - (e: any) => { - const value = e.currentTarget.value - onChange(value) - setValue(value) - }, - [onChange, setValue], - ) - - return ( - - {label && ( - - {label} - - )} - - - - {Icon && ( - - - - )} - - {Suffix && ( - - - - )} - - ) - } -} - -export const InputText = createTextInput(TextInput) diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx index 9e50a5abc7..68fd1190f9 100644 --- a/src/view/screens/Storybook/Forms.tsx +++ b/src/view/screens/Storybook/Forms.tsx @@ -3,10 +3,8 @@ import {View} from 'react-native' import {atoms as a} from '#/alf' import {H1, H3} from '#/components/Typography' -import {InputText} from '#/components/forms/InputText' import TextField from '#/components/forms/TextField' import {InputDate, utils} from '#/components/forms/InputDate' -import {InputGroup} from '#/components/forms/InputGroup' import Toggle from '#/components/forms/Toggle' import ToggleButton from '#/components/forms/ToggleButton' import {Button} from '#/components/Button' @@ -67,33 +65,6 @@ export function Forms() { /> - -

InputGroup (WIP)

- - console.log(text)} - /> - console.log(text)} - /> - console.log(text)} - /> - -
-

Toggles

From fb2e366b0e9dd8874045bb6a02c7898ddcb61bd6 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 18 Jan 2024 11:33:01 -0600 Subject: [PATCH 56/68] Improve icons, add CalendarDays --- .../calendarDays_stroke2_corner0_rounded.svg | 1 + src/components/Dialog/index.web.tsx | 3 +- src/components/icons/ArrowTopRight.tsx | 32 +++---------------- src/components/icons/CalendarDays.tsx | 5 +++ src/components/icons/Globe.tsx | 31 ++---------------- src/components/icons/TEMPLATE.tsx | 23 +++++++++++-- src/components/icons/common.ts | 2 +- src/view/screens/Storybook/Icons.tsx | 9 ++++++ 8 files changed, 46 insertions(+), 60 deletions(-) create mode 100644 assets/icons/calendarDays_stroke2_corner0_rounded.svg create mode 100644 src/components/icons/CalendarDays.tsx diff --git a/assets/icons/calendarDays_stroke2_corner0_rounded.svg b/assets/icons/calendarDays_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..09d9c0f490 --- /dev/null +++ b/assets/icons/calendarDays_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx index bb7b6d33df..3350785ec2 100644 --- a/src/components/Dialog/index.web.tsx +++ b/src/components/Dialog/index.web.tsx @@ -12,10 +12,11 @@ import {Button} from '#/components/Button' import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types' import {Context, useDialogContext} from '#/components/Dialog/context' +import TextField from '#/components/forms/TextField' export {useDialogControl, useDialogContext} from '#/components/Dialog/context' export * from '#/components/Dialog/types' -export {InputText} from '#/components/forms/InputText' +export const Input = TextField.Input const stopPropagation = (e: any) => e.stopPropagation() diff --git a/src/components/icons/ArrowTopRight.tsx b/src/components/icons/ArrowTopRight.tsx index 368c45308d..92ad30a129 100644 --- a/src/components/icons/ArrowTopRight.tsx +++ b/src/components/icons/ArrowTopRight.tsx @@ -1,29 +1,5 @@ -import React from 'react' -import Svg, {Path} from 'react-native-svg' +import {createSinglePathSVG} from './TEMPLATE' -import {useCommonSVGProps, Props} from '#/components/icons/common' - -export const ArrowTopRight_Stroke2_Corner0_Rounded = React.forwardRef( - function LogoImpl(props: Props, ref) { - const {fill, size, style, ...rest} = useCommonSVGProps(props) - - return ( - - - - ) - }, -) +export const ArrowTopRight_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M8 6a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v9a1 1 0 1 1-2 0V8.414l-9.793 9.793a1 1 0 0 1-1.414-1.414L15.586 7H9a1 1 0 0 1-1-1Z', +}) diff --git a/src/components/icons/CalendarDays.tsx b/src/components/icons/CalendarDays.tsx new file mode 100644 index 0000000000..72cc48e261 --- /dev/null +++ b/src/components/icons/CalendarDays.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const CalendarDays_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Zm1 16V9h14v10H5ZM5 7h14V5H5v2Zm3 10.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5ZM17.25 12a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0ZM12 13.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5ZM9.25 12a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0ZM12 17.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z', +}) diff --git a/src/components/icons/Globe.tsx b/src/components/icons/Globe.tsx index 28187266ac..f81b3ff7a0 100644 --- a/src/components/icons/Globe.tsx +++ b/src/components/icons/Globe.tsx @@ -1,30 +1,5 @@ -import React from 'react' -import Svg, {Path} from 'react-native-svg' +import {createSinglePathSVG} from './TEMPLATE' -import {useCommonSVGProps, Props} from '#/components/icons/common' - -export const Globe_Stroke2_Corner0_Rounded = React.forwardRef(function LogoImpl( - props: Props, - ref, -) { - const {fill, size, style, ...rest} = useCommonSVGProps(props) - - return ( - - - - ) +export const Globe_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M4.062 11h2.961c.103-2.204.545-4.218 1.235-5.77.06-.136.123-.269.188-.399A8.007 8.007 0 0 0 4.062 11ZM12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm0 2c-.227 0-.518.1-.868.432-.354.337-.719.872-1.047 1.61-.561 1.263-.958 2.991-1.06 4.958h5.95c-.102-1.967-.499-3.695-1.06-4.958-.328-.738-.693-1.273-1.047-1.61C12.518 4.099 12.227 4 12 4Zm4.977 7c-.103-2.204-.545-4.218-1.235-5.77a9.78 9.78 0 0 0-.188-.399A8.006 8.006 0 0 1 19.938 11h-2.961Zm-2.003 2H9.026c.101 1.966.498 3.695 1.06 4.958.327.738.692 1.273 1.046 1.61.35.333.641.432.868.432.227 0 .518-.1.868-.432.354-.337.719-.872 1.047-1.61.561-1.263.958-2.991 1.06-4.958Zm.58 6.169c.065-.13.128-.263.188-.399.69-1.552 1.132-3.566 1.235-5.77h2.961a8.006 8.006 0 0 1-4.384 6.169Zm-7.108 0a9.877 9.877 0 0 1-.188-.399c-.69-1.552-1.132-3.566-1.235-5.77H4.062a8.006 8.006 0 0 0 4.384 6.169Z', }) diff --git a/src/components/icons/TEMPLATE.tsx b/src/components/icons/TEMPLATE.tsx index c47849aca4..9fc1470375 100644 --- a/src/components/icons/TEMPLATE.tsx +++ b/src/components/icons/TEMPLATE.tsx @@ -19,11 +19,30 @@ export const IconTemplate_Stroke2_Corner0_Rounded = React.forwardRef( style={[style]}> ) }, ) + +export function createSinglePathSVG({path}: {path: string}) { + return React.forwardRef(function LogoImpl(props, ref) { + const {fill, size, style, ...rest} = useCommonSVGProps(props) + + return ( + + + + ) + }) +} diff --git a/src/components/icons/common.ts b/src/components/icons/common.ts index 2eaafb2cd1..9e9f15c4dd 100644 --- a/src/components/icons/common.ts +++ b/src/components/icons/common.ts @@ -7,7 +7,7 @@ export type Props = { fill?: PathProps['fill'] style?: TextProps['style'] size?: keyof typeof sizes -} & Omit +} & Omit export const sizes = { xs: 12, diff --git a/src/view/screens/Storybook/Icons.tsx b/src/view/screens/Storybook/Icons.tsx index c3c41ce7e9..73466e077f 100644 --- a/src/view/screens/Storybook/Icons.tsx +++ b/src/view/screens/Storybook/Icons.tsx @@ -5,6 +5,7 @@ import {atoms as a, useTheme} from '#/alf' import {H1} from '#/components/Typography' import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight' +import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays' export function Icons() { const t = useTheme() @@ -27,6 +28,14 @@ export function Icons() {
+ + + + + + + +
) } From ead02d766c4db751612bf78d35347f533072070e Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 18 Jan 2024 13:14:18 -0600 Subject: [PATCH 57/68] Refactor DateField, web done --- .../forms/DateField/index.android.tsx | 161 ++++++++++++++++++ src/components/forms/DateField/index.tsx | 63 +++++++ src/components/forms/DateField/index.web.tsx | 71 ++++++++ src/components/forms/DateField/types.ts | 10 ++ src/components/forms/DateField/utils.ts | 16 ++ src/components/forms/TextField.tsx | 16 +- src/view/screens/Storybook/Forms.tsx | 18 +- 7 files changed, 342 insertions(+), 13 deletions(-) create mode 100644 src/components/forms/DateField/index.android.tsx create mode 100644 src/components/forms/DateField/index.tsx create mode 100644 src/components/forms/DateField/index.web.tsx create mode 100644 src/components/forms/DateField/types.ts create mode 100644 src/components/forms/DateField/utils.ts diff --git a/src/components/forms/DateField/index.android.tsx b/src/components/forms/DateField/index.android.tsx new file mode 100644 index 0000000000..3547557841 --- /dev/null +++ b/src/components/forms/DateField/index.android.tsx @@ -0,0 +1,161 @@ +import React from 'react' +import {View, TextStyle, Pressable} from 'react-native' +import DateTimePicker, { + BaseProps as DateTimePickerProps, +} from '@react-native-community/datetimepicker' + +import {Logo} from '#/view/icons/Logo' +import {useTheme, atoms, tokens} from '#/alf' +import {Text} from '#/components/Typography' +import {useInteractionState} from '#/components/hooks/useInteractionState' + +import {InputDateProps} from '#/components/forms/InputDate/types' +import { + localizeDate, + toSimpleDateString, +} from '#/components/forms/InputDate/utils' + +export * as utils from '#/components/forms/InputDate/utils' + +export function InputDate({ + value: initialValue, + onChange, + testID, + label, + hasError, + accessibilityLabel, + accessibilityHint, + ...props +}: InputDateProps) { + const labelId = React.useId() + const t = useTheme() + const [open, setOpen] = React.useState(false) + const [value, setValue] = React.useState(initialValue) + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + + const {inputStyles, iconStyles} = React.useMemo(() => { + const input: TextStyle[] = [ + { + paddingLeft: 40, + }, + ] + const icon: TextStyle[] = [] + + if (hasError) { + input.push({ + borderColor: tokens.color.red_200, + }) + icon.push({ + color: tokens.color.red_400, + }) + } + + if (focused) { + input.push({ + borderColor: t.atoms.border_contrast.borderColor, + }) + + if (hasError) { + input.push({ + borderColor: tokens.color.red_500, + }) + } + } + + return {inputStyles: input, iconStyles: icon} + }, [t, focused, hasError]) + + const onChangeInternal = React.useCallback< + Required['onChange'] + >( + (_event, date) => { + setOpen(false) + + if (date) { + const formatted = toSimpleDateString(date) + onChange(formatted) + setValue(formatted) + } + }, + [onChange, setOpen, setValue], + ) + + return ( + + {label && ( + + {label} + + )} + + setOpen(true)} + onFocus={onFocus} + onBlur={onBlur} + style={[ + { + paddingTop: atoms.pt_md.paddingTop + 2, + }, + atoms.w_full, + atoms.px_lg, + atoms.pb_md, + atoms.rounded_sm, + t.atoms.bg_contrast_100, + ...inputStyles, + ]}> + {localizeDate(value)} + + + + + + + {open && ( + + )} + + ) +} diff --git a/src/components/forms/DateField/index.tsx b/src/components/forms/DateField/index.tsx new file mode 100644 index 0000000000..dcf509b7d8 --- /dev/null +++ b/src/components/forms/DateField/index.tsx @@ -0,0 +1,63 @@ +import React from 'react' +import {View} from 'react-native' +import DateTimePicker, { + DateTimePickerEvent, +} from '@react-native-community/datetimepicker' + +import {useTheme, atoms} from '#/alf' +import {toSimpleDateString} from '#/components/forms/InputDate/utils' +import {InputDateProps} from '#/components/forms/InputDate/types' +import TextField from '#/components/forms/TextField' + +export * as utils from '#/components/forms/InputDate/utils' +export const Label = TextField.Label + +/** + * Date-only input. Accepts a date in the format YYYY-MM-DD, and reports date + * changes in the same format. + * + * For dates of unknown format, convert with the + * `utils.toSimpleDateString(Date)` export of this file. + */ +export function DateField({ + value: initialValue, + onChange, + testID, + label, + accessibilityLabel, + accessibilityHint, +}: InputDateProps) { + const labelId = React.useId() + const t = useTheme() + const [value, setValue] = React.useState(initialValue) + + const onChangeInternal = React.useCallback( + (event: DateTimePickerEvent, date: Date | undefined) => { + if (date) { + const formatted = toSimpleDateString(date) + onChange(formatted) + setValue(formatted) + } + }, + [onChange], + ) + + return ( + + + + ) +} diff --git a/src/components/forms/DateField/index.web.tsx b/src/components/forms/DateField/index.web.tsx new file mode 100644 index 0000000000..f8809cb7ca --- /dev/null +++ b/src/components/forms/DateField/index.web.tsx @@ -0,0 +1,71 @@ +import React from 'react' +import {TextInput, TextInputProps, StyleSheet} from 'react-native' +// @ts-ignore +import {unstable_createElement} from 'react-native-web' + +import TextField, {createInput} from '#/components/forms/TextField' + +import {toSimpleDateString} from '#/components/forms/InputDate/utils' + +export * as utils from '#/components/forms/InputDate/utils' +export const Label = TextField.Label + +const InputBase = React.forwardRef( + ({style, ...props}, ref) => { + return unstable_createElement('input', { + ...props, + ref, + type: 'date', + style: [ + StyleSheet.flatten(style), + { + background: 'transparent', + border: 0, + }, + ], + }) + }, +) + +InputBase.displayName = 'InputBase' + +const Input = createInput(InputBase as unknown as typeof TextInput) + +export function DateField({ + value, + onChange, + label, + isInvalid, + testID, +}: { + value: string + onChange: (value: string) => void + label: string + isInvalid?: boolean + testID?: string +}) { + const handleOnChange = React.useCallback( + (e: any) => { + const date = e.target.valueAsDate || e.target.value + + if (date) { + const formatted = toSimpleDateString(date) + onChange(formatted) + } + }, + [onChange], + ) + + return ( + + {}} + isInvalid={isInvalid} + testID={testID} + /> + + ) +} diff --git a/src/components/forms/DateField/types.ts b/src/components/forms/DateField/types.ts new file mode 100644 index 0000000000..2ef0a4b3ba --- /dev/null +++ b/src/components/forms/DateField/types.ts @@ -0,0 +1,10 @@ +import {TextInputProps} from 'react-native' + +import {BaseProps} from '#/components/forms/types' + +export type InputDateProps = BaseProps & { + /** + * **NOTE:** Available only on web + */ + autoFocus?: TextInputProps['autoFocus'] +} diff --git a/src/components/forms/DateField/utils.ts b/src/components/forms/DateField/utils.ts new file mode 100644 index 0000000000..c787272fe8 --- /dev/null +++ b/src/components/forms/DateField/utils.ts @@ -0,0 +1,16 @@ +import {getLocales} from 'expo-localization' + +const LOCALE = getLocales()[0] + +// we need the date in the form yyyy-MM-dd to pass to the input +export function toSimpleDateString(date: Date | string): string { + const _date = typeof date === 'string' ? new Date(date) : date + return _date.toISOString().split('T')[0] +} + +export function localizeDate(date: Date | string): string { + const _date = typeof date === 'string' ? new Date(date) : date + return new Intl.DateTimeFormat(LOCALE.languageTag, { + timeZone: 'UTC', + }).format(_date) +} diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx index bcf7dafe0d..cc3aae2e59 100644 --- a/src/components/forms/TextField.tsx +++ b/src/components/forms/TextField.tsx @@ -94,7 +94,7 @@ function Root({children, isInvalid = false}: RootProps) { paddingVertical: 14, }, ]} - onPressIn={() => inputRef.current?.focus()} + onPress={() => inputRef.current?.focus()} onHoverIn={onHoverIn} onHoverOut={onHoverOut}> {children} @@ -103,7 +103,7 @@ function Root({children, isInvalid = false}: RootProps) { ) } -function useSharedInputStyles() { +export function useSharedInputStyles() { const t = useTheme() return React.useMemo(() => { const hover: ViewStyle[] = [ @@ -228,6 +228,15 @@ export function createInput(Component: typeof TextInput) { const Input = createInput(TextInput) +function Label({children}: React.PropsWithChildren<{}>) { + const t = useTheme() + return ( + + {children} + + ) +} + function Icon({icon: Comp}: {icon: React.ComponentType}) { const t = useTheme() const ctx = React.useContext(Context) @@ -266,7 +275,7 @@ function Icon({icon: Comp}: {icon: React.ComponentType}) { @@ -51,16 +52,13 @@ export function Forms() {

InputDate

- console.log(date)} - label="Input" - /> - console.log(date)} + value={date} + onChange={date => { + console.log(date) + setDate(date) + }} label="Input" />
From 8a5e1885bae89107c8475010b6c3ff2e9b0e8891 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 18 Jan 2024 13:20:12 -0600 Subject: [PATCH 58/68] Add label example --- src/view/screens/Storybook/Forms.tsx | 47 ++++++++++++++++------------ 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx index 28a908c414..4339f389d3 100644 --- a/src/view/screens/Storybook/Forms.tsx +++ b/src/view/screens/Storybook/Forms.tsx @@ -4,7 +4,7 @@ import {View} from 'react-native' import {atoms as a} from '#/alf' import {H1, H3} from '#/components/Typography' import TextField from '#/components/forms/TextField' -import {DateField} from '#/components/forms/DateField' +import {DateField, Label} from '#/components/forms/DateField' import Toggle from '#/components/forms/Toggle' import ToggleButton from '#/components/forms/ToggleButton' import {Button} from '#/components/Button' @@ -41,26 +41,33 @@ export function Forms() { /> - - - + Text field + + + + @gmail.com + +
+ +

DateField

+ + + + { + console.log(date) + setDate(date) + }} + label="Input" /> - @gmail.com - - -

InputDate

- { - console.log(date) - setDate(date) - }} - label="Input" - /> +
From 175466ade6d67a0ec86989b7f97fcf62d5493d64 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 18 Jan 2024 14:02:39 -0600 Subject: [PATCH 59/68] Clean up old InputDate, DateField android, text area example --- .../forms/DateField/index.android.tsx | 144 +++++----------- src/components/forms/DateField/index.tsx | 29 ++-- src/components/forms/DateField/index.web.tsx | 23 +-- src/components/forms/DateField/types.ts | 15 +- .../forms/InputDate/index.android.tsx | 161 ------------------ src/components/forms/InputDate/index.tsx | 75 -------- src/components/forms/InputDate/index.web.tsx | 152 ----------------- src/components/forms/InputDate/types.ts | 10 -- src/components/forms/InputDate/utils.ts | 16 -- src/components/forms/TextField.tsx | 4 +- src/view/screens/Storybook/Forms.tsx | 18 +- 11 files changed, 87 insertions(+), 560 deletions(-) delete mode 100644 src/components/forms/InputDate/index.android.tsx delete mode 100644 src/components/forms/InputDate/index.tsx delete mode 100644 src/components/forms/InputDate/index.web.tsx delete mode 100644 src/components/forms/InputDate/types.ts delete mode 100644 src/components/forms/InputDate/utils.ts diff --git a/src/components/forms/DateField/index.android.tsx b/src/components/forms/DateField/index.android.tsx index 3547557841..b1116411cc 100644 --- a/src/components/forms/DateField/index.android.tsx +++ b/src/components/forms/DateField/index.android.tsx @@ -1,69 +1,41 @@ import React from 'react' -import {View, TextStyle, Pressable} from 'react-native' +import {View, Pressable} from 'react-native' import DateTimePicker, { BaseProps as DateTimePickerProps, } from '@react-native-community/datetimepicker' -import {Logo} from '#/view/icons/Logo' -import {useTheme, atoms, tokens} from '#/alf' +import {useTheme, atoms} from '#/alf' import {Text} from '#/components/Typography' import {useInteractionState} from '#/components/hooks/useInteractionState' +import TextField, {useSharedInputStyles} from '#/components/forms/TextField' +import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays' -import {InputDateProps} from '#/components/forms/InputDate/types' +import {DateFieldProps} from '#/components/forms/DateField/types' import { localizeDate, toSimpleDateString, -} from '#/components/forms/InputDate/utils' +} from '#/components/forms/DateField/utils' -export * as utils from '#/components/forms/InputDate/utils' +export * as utils from '#/components/forms/DateField/utils' +export const Label = TextField.Label -export function InputDate({ - value: initialValue, - onChange, - testID, +export function DateField({ + value, + onChangeDate, label, - hasError, - accessibilityLabel, - accessibilityHint, - ...props -}: InputDateProps) { - const labelId = React.useId() + isInvalid, + testID, +}: DateFieldProps) { const t = useTheme() const [open, setOpen] = React.useState(false) - const [value, setValue] = React.useState(initialValue) + const { + state: pressed, + onIn: onPressIn, + onOut: onPressOut, + } = useInteractionState() const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() - const {inputStyles, iconStyles} = React.useMemo(() => { - const input: TextStyle[] = [ - { - paddingLeft: 40, - }, - ] - const icon: TextStyle[] = [] - - if (hasError) { - input.push({ - borderColor: tokens.color.red_200, - }) - icon.push({ - color: tokens.color.red_400, - }) - } - - if (focused) { - input.push({ - borderColor: t.atoms.border_contrast.borderColor, - }) - - if (hasError) { - input.push({ - borderColor: tokens.color.red_500, - }) - } - } - - return {inputStyles: input, iconStyles: icon} - }, [t, focused, hasError]) + const {chromeFocus, chromeError, chromeErrorHover} = useSharedInputStyles() const onChangeInternal = React.useCallback< Required['onChange'] @@ -73,75 +45,53 @@ export function InputDate({ if (date) { const formatted = toSimpleDateString(date) - onChange(formatted) - setValue(formatted) + onChangeDate(formatted) } }, - [onChange, setOpen, setValue], + [onChangeDate, setOpen], ) return ( - {label && ( - - {label} - - )} - setOpen(true)} + onPressIn={onPressIn} + onPressOut={onPressOut} onFocus={onFocus} onBlur={onBlur} style={[ { - paddingTop: atoms.pt_md.paddingTop + 2, + paddingTop: 16, + paddingBottom: 16, + borderColor: 'transparent', + borderWidth: 2, }, + atoms.flex_row, + atoms.flex_1, atoms.w_full, atoms.px_lg, - atoms.pb_md, atoms.rounded_sm, - t.atoms.bg_contrast_100, - ...inputStyles, + t.atoms.bg_contrast_50, + focused || pressed ? chromeFocus : {}, + isInvalid ? chromeError : {}, + isInvalid && (focused || pressed) ? chromeErrorHover : {}, ]}> - {localizeDate(value)} - + - - - + + {localizeDate(value)} + +
{open && ( )} diff --git a/src/components/forms/DateField/index.tsx b/src/components/forms/DateField/index.tsx index dcf509b7d8..8a8db876b2 100644 --- a/src/components/forms/DateField/index.tsx +++ b/src/components/forms/DateField/index.tsx @@ -5,11 +5,11 @@ import DateTimePicker, { } from '@react-native-community/datetimepicker' import {useTheme, atoms} from '#/alf' -import {toSimpleDateString} from '#/components/forms/InputDate/utils' -import {InputDateProps} from '#/components/forms/InputDate/types' import TextField from '#/components/forms/TextField' +import {toSimpleDateString} from '#/components/forms/DateField/utils' +import {DateFieldProps} from '#/components/forms/DateField/types' -export * as utils from '#/components/forms/InputDate/utils' +export * as utils from '#/components/forms/DateField/utils' export const Label = TextField.Label /** @@ -20,43 +20,36 @@ export const Label = TextField.Label * `utils.toSimpleDateString(Date)` export of this file. */ export function DateField({ - value: initialValue, - onChange, + value, + onChangeDate, testID, label, - accessibilityLabel, - accessibilityHint, -}: InputDateProps) { - const labelId = React.useId() +}: DateFieldProps) { const t = useTheme() - const [value, setValue] = React.useState(initialValue) const onChangeInternal = React.useCallback( (event: DateTimePickerEvent, date: Date | undefined) => { if (date) { const formatted = toSimpleDateString(date) - onChange(formatted) - setValue(formatted) + onChangeDate(formatted) } }, - [onChange], + [onChangeDate], ) return ( ) diff --git a/src/components/forms/DateField/index.web.tsx b/src/components/forms/DateField/index.web.tsx index f8809cb7ca..4dd1980393 100644 --- a/src/components/forms/DateField/index.web.tsx +++ b/src/components/forms/DateField/index.web.tsx @@ -4,10 +4,10 @@ import {TextInput, TextInputProps, StyleSheet} from 'react-native' import {unstable_createElement} from 'react-native-web' import TextField, {createInput} from '#/components/forms/TextField' +import {toSimpleDateString} from '#/components/forms/DateField/utils' +import {DateFieldProps} from '#/components/forms/DateField/types' -import {toSimpleDateString} from '#/components/forms/InputDate/utils' - -export * as utils from '#/components/forms/InputDate/utils' +export * as utils from '#/components/forms/DateField/utils' export const Label = TextField.Label const InputBase = React.forwardRef( @@ -33,37 +33,30 @@ const Input = createInput(InputBase as unknown as typeof TextInput) export function DateField({ value, - onChange, + onChangeDate, label, isInvalid, testID, -}: { - value: string - onChange: (value: string) => void - label: string - isInvalid?: boolean - testID?: string -}) { +}: DateFieldProps) { const handleOnChange = React.useCallback( (e: any) => { const date = e.target.valueAsDate || e.target.value if (date) { const formatted = toSimpleDateString(date) - onChange(formatted) + onChangeDate(formatted) } }, - [onChange], + [onChangeDate], ) return ( - + {}} - isInvalid={isInvalid} testID={testID} /> diff --git a/src/components/forms/DateField/types.ts b/src/components/forms/DateField/types.ts index 2ef0a4b3ba..129f5672d4 100644 --- a/src/components/forms/DateField/types.ts +++ b/src/components/forms/DateField/types.ts @@ -1,10 +1,7 @@ -import {TextInputProps} from 'react-native' - -import {BaseProps} from '#/components/forms/types' - -export type InputDateProps = BaseProps & { - /** - * **NOTE:** Available only on web - */ - autoFocus?: TextInputProps['autoFocus'] +export type DateFieldProps = { + value: string + onChangeDate: (date: string) => void + label: string + isInvalid?: boolean + testID?: string } diff --git a/src/components/forms/InputDate/index.android.tsx b/src/components/forms/InputDate/index.android.tsx deleted file mode 100644 index 3547557841..0000000000 --- a/src/components/forms/InputDate/index.android.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import React from 'react' -import {View, TextStyle, Pressable} from 'react-native' -import DateTimePicker, { - BaseProps as DateTimePickerProps, -} from '@react-native-community/datetimepicker' - -import {Logo} from '#/view/icons/Logo' -import {useTheme, atoms, tokens} from '#/alf' -import {Text} from '#/components/Typography' -import {useInteractionState} from '#/components/hooks/useInteractionState' - -import {InputDateProps} from '#/components/forms/InputDate/types' -import { - localizeDate, - toSimpleDateString, -} from '#/components/forms/InputDate/utils' - -export * as utils from '#/components/forms/InputDate/utils' - -export function InputDate({ - value: initialValue, - onChange, - testID, - label, - hasError, - accessibilityLabel, - accessibilityHint, - ...props -}: InputDateProps) { - const labelId = React.useId() - const t = useTheme() - const [open, setOpen] = React.useState(false) - const [value, setValue] = React.useState(initialValue) - const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() - - const {inputStyles, iconStyles} = React.useMemo(() => { - const input: TextStyle[] = [ - { - paddingLeft: 40, - }, - ] - const icon: TextStyle[] = [] - - if (hasError) { - input.push({ - borderColor: tokens.color.red_200, - }) - icon.push({ - color: tokens.color.red_400, - }) - } - - if (focused) { - input.push({ - borderColor: t.atoms.border_contrast.borderColor, - }) - - if (hasError) { - input.push({ - borderColor: tokens.color.red_500, - }) - } - } - - return {inputStyles: input, iconStyles: icon} - }, [t, focused, hasError]) - - const onChangeInternal = React.useCallback< - Required['onChange'] - >( - (_event, date) => { - setOpen(false) - - if (date) { - const formatted = toSimpleDateString(date) - onChange(formatted) - setValue(formatted) - } - }, - [onChange, setOpen, setValue], - ) - - return ( - - {label && ( - - {label} - - )} - - setOpen(true)} - onFocus={onFocus} - onBlur={onBlur} - style={[ - { - paddingTop: atoms.pt_md.paddingTop + 2, - }, - atoms.w_full, - atoms.px_lg, - atoms.pb_md, - atoms.rounded_sm, - t.atoms.bg_contrast_100, - ...inputStyles, - ]}> - {localizeDate(value)} - - - - - - - {open && ( - - )} - - ) -} diff --git a/src/components/forms/InputDate/index.tsx b/src/components/forms/InputDate/index.tsx deleted file mode 100644 index 727dcd6736..0000000000 --- a/src/components/forms/InputDate/index.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react' -import {View} from 'react-native' -import DateTimePicker, { - DateTimePickerEvent, -} from '@react-native-community/datetimepicker' - -import {useTheme, atoms} from '#/alf' -import {Text} from '#/components/Typography' -import {toSimpleDateString} from '#/components/forms/InputDate/utils' - -import {InputDateProps} from '#/components/forms/InputDate/types' -export * as utils from '#/components/forms/InputDate/utils' - -/** - * Date-only input. Accepts a date in the format YYYY-MM-DD, and reports date - * changes in the same format. - * - * For dates of unknown format, convert with the - * `utils.toSimpleDateString(Date)` export of this file. - */ -export function InputDate({ - value: initialValue, - onChange, - testID, - label, - accessibilityLabel, - accessibilityHint, -}: InputDateProps) { - const labelId = React.useId() - const t = useTheme() - const [value, setValue] = React.useState(initialValue) - - const onChangeInternal = React.useCallback( - (event: DateTimePickerEvent, date: Date | undefined) => { - if (date) { - const formatted = toSimpleDateString(date) - onChange(formatted) - setValue(formatted) - } - }, - [onChange], - ) - - return ( - - {label && ( - - {label} - - )} - - - - ) -} diff --git a/src/components/forms/InputDate/index.web.tsx b/src/components/forms/InputDate/index.web.tsx deleted file mode 100644 index 95a4d9e1b4..0000000000 --- a/src/components/forms/InputDate/index.web.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import React from 'react' -import {View, TextStyle} from 'react-native' -// @ts-ignore -import {unstable_createElement} from 'react-native-web' - -import {Logo} from '#/view/icons/Logo' -import {useTheme, atoms, tokens} from '#/alf' -import {Text} from '#/components/Typography' -import {useInteractionState} from '#/components/hooks/useInteractionState' - -import {InputDateProps} from '#/components/forms/InputDate/types' -import {toSimpleDateString} from '#/components/forms/InputDate/utils' - -export * as utils from '#/components/forms/InputDate/utils' - -export function InputDate({ - label, - hasError, - testID, - value: initialValue, - onChange, - accessibilityLabel, - accessibilityHint, - ...props -}: InputDateProps) { - const labelId = React.useId() - const t = useTheme() - const [value, setValue] = React.useState(initialValue) - const { - state: hovered, - onIn: onHoverIn, - onOut: onHoverOut, - } = useInteractionState() - const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() - - const {inputStyles, iconStyles} = React.useMemo(() => { - const input: TextStyle[] = [ - { - paddingLeft: 40, - }, - ] - const icon: TextStyle[] = [] - - if (hasError) { - input.push({ - borderColor: tokens.color.red_200, - }) - icon.push({ - color: tokens.color.red_400, - }) - } - - if (hovered || focused) { - input.push({ - borderColor: t.atoms.border_contrast.borderColor, - }) - - if (hasError) { - input.push({ - borderColor: tokens.color.red_500, - }) - } - } - - return {inputStyles: input, iconStyles: icon} - }, [t, hovered, focused, hasError]) - - const handleOnChange = React.useCallback( - (e: any) => { - const date = e.currentTarget.valueAsDate - - if (date) { - const formatted = toSimpleDateString(date) - onChange(formatted) - setValue(formatted) - } - }, - [onChange, setValue], - ) - - return ( - - {label && ( - - {label} - - )} - - {unstable_createElement('input', { - ...props, - testID: `${testID}-datepicker`, - 'aria-labelledby': labelId, - 'aria-label': label, - accessibilityLabel: accessibilityLabel, - accessibilityHint: accessibilityHint, - type: 'date', - value: value, - onFocus: onFocus, - onBlur: onBlur, - onChange: handleOnChange, - onMouseEnter: onHoverIn, - onMouseLeave: onHoverOut, - style: [ - { - outline: 0, - border: 0, - appearance: 'none', - boxSizing: 'border-box', - lineHeight: atoms.text_md.lineHeight * 1.1875, - paddingTop: atoms.pt_md.paddingTop - 1, - }, - atoms.w_full, - atoms.px_lg, - atoms.pb_md, - atoms.rounded_sm, - atoms.text_md, - t.atoms.bg_contrast_100, - t.atoms.text, - ...inputStyles, - ], - })} - - - - - - ) -} diff --git a/src/components/forms/InputDate/types.ts b/src/components/forms/InputDate/types.ts deleted file mode 100644 index 2ef0a4b3ba..0000000000 --- a/src/components/forms/InputDate/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {TextInputProps} from 'react-native' - -import {BaseProps} from '#/components/forms/types' - -export type InputDateProps = BaseProps & { - /** - * **NOTE:** Available only on web - */ - autoFocus?: TextInputProps['autoFocus'] -} diff --git a/src/components/forms/InputDate/utils.ts b/src/components/forms/InputDate/utils.ts deleted file mode 100644 index c787272fe8..0000000000 --- a/src/components/forms/InputDate/utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {getLocales} from 'expo-localization' - -const LOCALE = getLocales()[0] - -// we need the date in the form yyyy-MM-dd to pass to the input -export function toSimpleDateString(date: Date | string): string { - const _date = typeof date === 'string' ? new Date(date) : date - return _date.toISOString().split('T')[0] -} - -export function localizeDate(date: Date | string): string { - const _date = typeof date === 'string' ? new Date(date) : date - return new Intl.DateTimeFormat(LOCALE.languageTag, { - timeZone: 'UTC', - }).format(_date) -} diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx index cc3aae2e59..941c16bd30 100644 --- a/src/components/forms/TextField.tsx +++ b/src/components/forms/TextField.tsx @@ -215,8 +215,8 @@ export function createInput(Component: typeof TextInput) { {borderColor: 'transparent', borderWidth: 2}, ctx.hovered ? chromeHover : {}, ctx.focused ? chromeFocus : {}, - ctx.isInvalid ? chromeError : {}, - ctx.isInvalid && (ctx.hovered || ctx.focused) + ctx.isInvalid || isInvalid ? chromeError : {}, + (ctx.isInvalid || isInvalid) && (ctx.hovered || ctx.focused) ? chromeErrorHover : {}, ]} diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx index 4339f389d3..0e9bf54b01 100644 --- a/src/view/screens/Storybook/Forms.tsx +++ b/src/view/screens/Storybook/Forms.tsx @@ -41,7 +41,7 @@ export function Forms() { /> - + Text field @@ -54,14 +54,26 @@ export function Forms() { + + Textarea + + +

DateField

- + { + onChangeDate={date => { console.log(date) setDate(date) }} From 1514ffd6e965a3955b6905f916f2e5bb15741659 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 18 Jan 2024 14:10:48 -0600 Subject: [PATCH 60/68] Consistent imports --- src/components/Dialog/index.web.tsx | 3 +- .../forms/DateField/index.android.tsx | 5 +- src/components/forms/DateField/index.tsx | 2 +- src/components/forms/DateField/index.web.tsx | 4 +- src/components/forms/TextField.tsx | 18 ++---- src/components/forms/Toggle.tsx | 57 ++++++++----------- src/components/forms/ToggleButton.tsx | 17 ++---- src/components/forms/types.ts | 11 ---- src/view/screens/Storybook/Forms.tsx | 12 ++-- src/view/screens/Storybook/index.tsx | 9 +-- 10 files changed, 49 insertions(+), 89 deletions(-) delete mode 100644 src/components/forms/types.ts diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx index 3350785ec2..6099efdd86 100644 --- a/src/components/Dialog/index.web.tsx +++ b/src/components/Dialog/index.web.tsx @@ -12,11 +12,10 @@ import {Button} from '#/components/Button' import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types' import {Context, useDialogContext} from '#/components/Dialog/context' -import TextField from '#/components/forms/TextField' export {useDialogControl, useDialogContext} from '#/components/Dialog/context' export * from '#/components/Dialog/types' -export const Input = TextField.Input +export {Input} from '#/components/forms/TextField' const stopPropagation = (e: any) => e.stopPropagation() diff --git a/src/components/forms/DateField/index.android.tsx b/src/components/forms/DateField/index.android.tsx index b1116411cc..83fa285f5f 100644 --- a/src/components/forms/DateField/index.android.tsx +++ b/src/components/forms/DateField/index.android.tsx @@ -7,7 +7,7 @@ import DateTimePicker, { import {useTheme, atoms} from '#/alf' import {Text} from '#/components/Typography' import {useInteractionState} from '#/components/hooks/useInteractionState' -import TextField, {useSharedInputStyles} from '#/components/forms/TextField' +import * as TextField from '#/components/forms/TextField' import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays' import {DateFieldProps} from '#/components/forms/DateField/types' @@ -35,7 +35,8 @@ export function DateField({ } = useInteractionState() const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() - const {chromeFocus, chromeError, chromeErrorHover} = useSharedInputStyles() + const {chromeFocus, chromeError, chromeErrorHover} = + TextField.useSharedInputStyles() const onChangeInternal = React.useCallback< Required['onChange'] diff --git a/src/components/forms/DateField/index.tsx b/src/components/forms/DateField/index.tsx index 8a8db876b2..c359a9d460 100644 --- a/src/components/forms/DateField/index.tsx +++ b/src/components/forms/DateField/index.tsx @@ -5,7 +5,7 @@ import DateTimePicker, { } from '@react-native-community/datetimepicker' import {useTheme, atoms} from '#/alf' -import TextField from '#/components/forms/TextField' +import * as TextField from '#/components/forms/TextField' import {toSimpleDateString} from '#/components/forms/DateField/utils' import {DateFieldProps} from '#/components/forms/DateField/types' diff --git a/src/components/forms/DateField/index.web.tsx b/src/components/forms/DateField/index.web.tsx index 4dd1980393..32f38a5d16 100644 --- a/src/components/forms/DateField/index.web.tsx +++ b/src/components/forms/DateField/index.web.tsx @@ -3,7 +3,7 @@ import {TextInput, TextInputProps, StyleSheet} from 'react-native' // @ts-ignore import {unstable_createElement} from 'react-native-web' -import TextField, {createInput} from '#/components/forms/TextField' +import * as TextField from '#/components/forms/TextField' import {toSimpleDateString} from '#/components/forms/DateField/utils' import {DateFieldProps} from '#/components/forms/DateField/types' @@ -29,7 +29,7 @@ const InputBase = React.forwardRef( InputBase.displayName = 'InputBase' -const Input = createInput(InputBase as unknown as typeof TextInput) +const Input = TextField.createInput(InputBase as unknown as typeof TextInput) export function DateField({ value, diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx index 941c16bd30..133ca09cc4 100644 --- a/src/components/forms/TextField.tsx +++ b/src/components/forms/TextField.tsx @@ -38,7 +38,7 @@ const Context = React.createContext<{ export type RootProps = React.PropsWithChildren<{isInvalid?: boolean}> -function Root({children, isInvalid = false}: RootProps) { +export function Root({children, isInvalid = false}: RootProps) { const inputRef = React.useRef(null) const rootRef = React.useRef(null) const { @@ -226,9 +226,9 @@ export function createInput(Component: typeof TextInput) { } } -const Input = createInput(TextInput) +export const Input = createInput(TextInput) -function Label({children}: React.PropsWithChildren<{}>) { +export function Label({children}: React.PropsWithChildren<{}>) { const t = useTheme() return ( @@ -237,7 +237,7 @@ function Label({children}: React.PropsWithChildren<{}>) { ) } -function Icon({icon: Comp}: {icon: React.ComponentType}) { +export function Icon({icon: Comp}: {icon: React.ComponentType}) { const t = useTheme() const ctx = React.useContext(Context) const {hover, focus, errorHover, errorFocus} = React.useMemo(() => { @@ -286,7 +286,7 @@ function Icon({icon: Comp}: {icon: React.ComponentType}) { ) } -function Suffix({ +export function Suffix({ children, label, accessibilityHint, @@ -322,11 +322,3 @@ function Suffix({ ) } - -export default { - Root, - Input, - Label, - Icon, - Suffix, -} diff --git a/src/components/forms/Toggle.tsx b/src/components/forms/Toggle.tsx index 8fe43f2f41..6fdbaa2eed 100644 --- a/src/components/forms/Toggle.tsx +++ b/src/components/forms/Toggle.tsx @@ -5,11 +5,11 @@ import {useTheme, atoms as a, web} from '#/alf' import {Text} from '#/components/Typography' import {useInteractionState} from '#/components/hooks/useInteractionState' -type ItemState = { +export type ItemState = { name: string selected: boolean disabled: boolean - hasError: boolean + isInvalid: boolean hovered: boolean pressed: boolean focused: boolean @@ -19,7 +19,7 @@ const ItemContext = React.createContext({ name: '', selected: false, disabled: false, - hasError: false, + isInvalid: false, hovered: false, pressed: false, focused: false, @@ -55,7 +55,7 @@ export type ItemProps = { value?: boolean disabled?: boolean onChange?: (selected: boolean) => void - hasError?: boolean + isInvalid?: boolean style?: (state: ItemState) => ViewStyle children: ((props: ItemState) => React.ReactNode) | React.ReactNode } @@ -64,7 +64,7 @@ export function useItemContext() { return React.useContext(ItemContext) } -function Group({ +export function Group({ children, values: providedValues, onChange, @@ -138,13 +138,13 @@ function Group({ ) } -function Item({ +export function Item({ children, name, value = false, disabled: itemDisabled = false, onChange, - hasError, + isInvalid, style, type = 'checkbox', label, @@ -185,12 +185,12 @@ function Item({ name, selected, disabled: disabled ?? false, - hasError: hasError ?? false, + isInvalid: isInvalid ?? false, hovered, pressed, focused, }), - [name, selected, disabled, hovered, pressed, focused, hasError], + [name, selected, disabled, hovered, pressed, focused, isInvalid], ) return ( @@ -201,7 +201,7 @@ function Item({ disabled={disabled} aria-disabled={disabled ?? false} aria-checked={selected} - aria-invalid={hasError} + aria-invalid={isInvalid} aria-label={label} role={role} accessibilityRole={role} @@ -230,7 +230,7 @@ function Item({ ) } -function Label({children}: React.PropsWithChildren<{}>) { +export function Label({children}: React.PropsWithChildren<{}>) { const t = useTheme() const {disabled} = useItemContext() return ( @@ -247,20 +247,20 @@ function Label({children}: React.PropsWithChildren<{}>) { ) } -function createSharedToggleStyles({ +export function createSharedToggleStyles({ theme: t, hovered, focused, selected, disabled, - hasError, + isInvalid, }: { theme: ReturnType selected: boolean hovered: boolean focused: boolean disabled: boolean - hasError: boolean + isInvalid: boolean }) { const base: ViewStyle[] = [] const baseHover: ViewStyle[] = [] @@ -290,7 +290,7 @@ function createSharedToggleStyles({ } } - if (hasError) { + if (isInvalid) { base.push({ backgroundColor: t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900, @@ -321,9 +321,9 @@ function createSharedToggleStyles({ } } -function Checkbox() { +export function Checkbox() { const t = useTheme() - const {selected, hovered, focused, disabled, hasError} = useItemContext() + const {selected, hovered, focused, disabled, isInvalid} = useItemContext() const {baseStyles, baseHoverStyles, indicatorStyles} = createSharedToggleStyles({ theme: t, @@ -331,7 +331,7 @@ function Checkbox() { focused, selected, disabled, - hasError, + isInvalid, }) return ( ) } - -export default { - Item, - Checkbox, - Label, - Switch, - Radio, - Group, -} diff --git a/src/components/forms/ToggleButton.tsx b/src/components/forms/ToggleButton.tsx index f6578c90e2..2694dd021c 100644 --- a/src/components/forms/ToggleButton.tsx +++ b/src/components/forms/ToggleButton.tsx @@ -4,17 +4,13 @@ import {View, AccessibilityProps, TextStyle, ViewStyle} from 'react-native' import {atoms as a, useTheme} from '#/alf' import {Text} from '#/components/Typography' -import Toggle, { - GroupProps as BaseGroupProps, - ItemProps as BaseItemProps, - useItemContext, -} from '#/components/forms/Toggle' +import * as Toggle from '#/components/forms/Toggle' -export type ItemProps = Omit & +export type ItemProps = Omit & AccessibilityProps & React.PropsWithChildren<{}> -export type GroupProps = Omit & { +export type GroupProps = Omit & { multiple?: boolean } @@ -46,7 +42,7 @@ export function Button({children, ...props}: ItemProps) { function ButtonInner({children}: React.PropsWithChildren<{}>) { const t = useTheme() - const state = useItemContext() + const state = Toggle.useItemContext() const {baseStyles, hoverStyles, activeStyles, textStyles} = React.useMemo(() => { @@ -123,8 +119,3 @@ function ButtonInner({children}: React.PropsWithChildren<{}>) { ) } - -export default { - Group, - Button, -} diff --git a/src/components/forms/types.ts b/src/components/forms/types.ts deleted file mode 100644 index 8d94bf0d1c..0000000000 --- a/src/components/forms/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {AccessibilityProps} from 'react-native' - -export type RequiredAccessibilityProps = Required - -export type BaseProps = AccessibilityProps & { - value: T - onChange: (value: T) => void - testID?: string - label: string - hasError?: boolean -} diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx index 0e9bf54b01..6862e3fef9 100644 --- a/src/view/screens/Storybook/Forms.tsx +++ b/src/view/screens/Storybook/Forms.tsx @@ -3,10 +3,10 @@ import {View} from 'react-native' import {atoms as a} from '#/alf' import {H1, H3} from '#/components/Typography' -import TextField from '#/components/forms/TextField' +import * as TextField from '#/components/forms/TextField' import {DateField, Label} from '#/components/forms/DateField' -import Toggle from '#/components/forms/Toggle' -import ToggleButton from '#/components/forms/ToggleButton' +import * as Toggle from '#/components/forms/Toggle' +import * as ToggleButton from '#/components/forms/ToggleButton' import {Button} from '#/components/Button' import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' @@ -113,7 +113,7 @@ export function Forms() { Click me - + Click me @@ -143,7 +143,7 @@ export function Forms() { Click me - + Click me @@ -172,7 +172,7 @@ export function Forms() { Click me - + Click me diff --git a/src/view/screens/Storybook/index.tsx b/src/view/screens/Storybook/index.tsx index 9f627ad2c2..d8898f20e8 100644 --- a/src/view/screens/Storybook/index.tsx +++ b/src/view/screens/Storybook/index.tsx @@ -30,8 +30,7 @@ export function Storybook() { variant="outline" color="primary" size="small" - accessibilityLabel='Set theme to "system"' - accessibilityHint="Set theme to system default" + label='Set theme to "system"' onPress={() => setColorMode('system')}> System @@ -39,8 +38,7 @@ export function Storybook() { variant="solid" color="secondary" size="small" - accessibilityLabel='Set theme to "system"' - accessibilityHint="Set theme to system default" + label='Set theme to "system"' onPress={() => setColorMode('light')}> Light @@ -48,8 +46,7 @@ export function Storybook() { variant="solid" color="secondary" size="small" - accessibilityLabel='Set theme to "system"' - accessibilityHint="Set theme to system default" + label='Set theme to "system"' onPress={() => setColorMode('dark')}> Dark From d6925a00d7da42895cafb8f68c98492a964f1e71 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 18 Jan 2024 15:12:42 -0600 Subject: [PATCH 61/68] Button context, icons --- src/components/Button.tsx | 175 +++++++++++++------------ src/components/Link.tsx | 65 +++++---- src/view/screens/Storybook/Buttons.tsx | 37 +++++- src/view/screens/Storybook/Links.tsx | 14 +- 4 files changed, 171 insertions(+), 120 deletions(-) diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 06226ad1e0..a37017decc 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -6,10 +6,14 @@ import { TextProps, ViewStyle, AccessibilityProps, + View, + TextStyle, + StyleSheet, } from 'react-native' import LinearGradient from 'react-native-linear-gradient' -import {useTheme, atoms, tokens, web, native} from '#/alf' +import {useTheme, atoms as a, tokens, web, native} from '#/alf' +import {Props as SVGIconProps} from '#/components/icons/common' export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient' export type ButtonColor = @@ -38,25 +42,32 @@ export type VariantProps = { size?: ButtonSize } -export type ButtonProps = Pick & - AccessibilityProps & +export type ButtonProps = React.PropsWithChildren< + Pick & + AccessibilityProps & + VariantProps & { + label: string + } +> +export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean} + +const Context = React.createContext< VariantProps & { - children: - | ((props: { - state: { - pressed: boolean - hovered: boolean - focused: boolean - } - props: VariantProps & { - disabled?: boolean - } - }) => React.ReactNode) - | React.ReactNode - | string - label: string + hovered: boolean + focused: boolean + pressed: boolean + disabled: boolean } -export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean} +>({ + hovered: false, + focused: false, + pressed: false, + disabled: false, +}) + +export function useButtonContext() { + return React.useContext(Context) +} export function Button({ children, @@ -131,21 +142,21 @@ export function Button({ }) } } else if (variant === 'outline') { - baseStyles.push(atoms.border, t.atoms.bg, { + baseStyles.push(a.border, t.atoms.bg, { borderWidth: 1, }) if (!disabled) { - baseStyles.push(atoms.border, { + baseStyles.push(a.border, { borderColor: tokens.color.blue_500, }) - hoverStyles.push(atoms.border, { + hoverStyles.push(a.border, { backgroundColor: light ? t.palette.primary_100 : t.palette.primary_900, }) } else { - baseStyles.push(atoms.border, { + baseStyles.push(a.border, { borderColor: light ? tokens.color.blue_200 : tokens.color.blue_900, }) } @@ -180,17 +191,17 @@ export function Button({ }) } } else if (variant === 'outline') { - baseStyles.push(atoms.border, t.atoms.bg, { + baseStyles.push(a.border, t.atoms.bg, { borderWidth: 1, }) if (!disabled) { - baseStyles.push(atoms.border, { + baseStyles.push(a.border, { borderColor: light ? tokens.color.gray_500 : tokens.color.gray_500, }) - hoverStyles.push(atoms.border, t.atoms.bg_contrast_50) + hoverStyles.push(a.border, t.atoms.bg_contrast_50) } else { - baseStyles.push(atoms.border, { + baseStyles.push(a.border, { borderColor: light ? tokens.color.gray_200 : tokens.color.gray_800, }) } @@ -219,19 +230,19 @@ export function Button({ }) } } else if (variant === 'outline') { - baseStyles.push(atoms.border, t.atoms.bg, { + baseStyles.push(a.border, t.atoms.bg, { borderWidth: 1, }) if (!disabled) { - baseStyles.push(atoms.border, { + baseStyles.push(a.border, { borderColor: t.palette.negative_600, }) - hoverStyles.push(atoms.border, { + hoverStyles.push(a.border, { backgroundColor: light ? t.palette.negative_50 : '#2D0614', // darker red }) } else { - baseStyles.push(atoms.border, { + baseStyles.push(a.border, { borderColor: light ? t.palette.negative_200 : t.palette.negative_900, @@ -248,19 +259,9 @@ export function Button({ } if (size === 'large') { - baseStyles.push( - {paddingVertical: 15}, - atoms.px_2xl, - atoms.rounded_sm, - atoms.gap_sm, - ) + baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_sm) } else if (size === 'small') { - baseStyles.push( - {paddingVertical: 9}, - atoms.px_md, - atoms.rounded_sm, - atoms.gap_xs, - ) + baseStyles.push({paddingVertical: 9}, a.px_md, a.rounded_sm, a.gap_sm) } return { @@ -305,15 +306,13 @@ export function Button({ } }, [variant, color]) - const childProps = React.useMemo( + const context = React.useMemo( () => ({ - state, - props: { - variant, - color, - size, - disabled: disabled || false, - }, + ...state, + variant, + color, + size, + disabled: disabled || false, }), [state, variant, color, size, disabled], ) @@ -331,9 +330,9 @@ export function Button({ disabled: disabled || false, }} style={[ - atoms.flex_row, - atoms.align_center, - atoms.overflow_hidden, + a.flex_row, + a.align_center, + a.overflow_hidden, ...baseStyles, ...(state.hovered || state.pressed ? hoverStyles : []), ...(state.focused ? focusStyles : []), @@ -354,39 +353,25 @@ export function Button({ locations={gradientLocations} start={{x: 0, y: 0}} end={{x: 1, y: 1}} - style={[atoms.absolute, atoms.inset_0]} + style={[a.absolute, a.inset_0]} /> )} - {typeof children === 'string' ? ( - - {children} - - ) : typeof children === 'function' ? ( - children(childProps) - ) : ( - children - )} + + {typeof children === 'string' ? ( + {children} + ) : ( + children + )} + ) } -export function ButtonText({ - children, - style, - variant, - color, - size, - disabled, - ...rest -}: ButtonTextProps) { +export function useSharedButtonTextStyles() { const t = useTheme() - - const textStyles = React.useMemo(() => { - const baseStyles = [] + const {color, variant, disabled, size} = useButtonContext() + return React.useMemo(() => { + const baseStyles: TextStyle[] = [] const light = t.name === 'light' if (color === 'primary') { @@ -473,26 +458,46 @@ export function ButtonText({ if (size === 'large') { baseStyles.push( - atoms.text_md, + a.text_md, web({paddingBottom: 1}), native({marginTop: 2}), ) } else { baseStyles.push( - atoms.text_md, + a.text_md, web({paddingBottom: 1}), native({marginTop: 2}), ) } - return baseStyles + return StyleSheet.flatten(baseStyles) }, [t, variant, color, size, disabled]) +} + +export function ButtonText({children, style, ...rest}: ButtonTextProps) { + const textStyles = useSharedButtonTextStyles() return ( - + {children} ) } + +export function ButtonIcon({ + icon: Comp, +}: { + icon: React.ComponentType +}) { + const {size} = useButtonContext() + const textStyles = useSharedButtonTextStyles() + + return ( + + + + ) +} diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 2eb7b6bffd..8f686f3c4c 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -14,8 +14,8 @@ import { import {sanitizeUrl} from '@braintree/sanitize-url' import {isWeb} from '#/platform/detection' -import {useTheme, web} from '#/alf' -import {Button, ButtonProps} from '#/components/Button' +import {useTheme, web, flatten} from '#/alf' +import {Button, ButtonProps, useButtonContext} from '#/components/Button' import {AllNavigatorParams, NavigationProp} from '#/lib/routes/types' import { convertBskyAppUrlIfNeeded, @@ -25,7 +25,10 @@ import { import {useModalControls} from '#/state/modals' import {router} from '#/routes' -export type LinkProps = Omit & { +export type LinkProps = Omit< + ButtonProps, + 'style' | 'onPress' | 'disabled' | 'label' +> & { /** * `TextStyle` to apply to the anchor element itself. Does not apply to any children. */ @@ -39,6 +42,7 @@ export type LinkProps = Omit & { * works for Links with children that are strings i.e. text links. */ warnOnMismatchingTextChild?: boolean + label?: ButtonProps['label'] } & Pick>[0], 'to'> /** @@ -52,12 +56,11 @@ export type LinkProps = Omit & { export function Link({ children, to, - style, action = 'push', warnOnMismatchingTextChild, + style, ...rest }: LinkProps) { - const t = useTheme() const navigation = useNavigation() const {href} = useLinkProps({ to: @@ -67,12 +70,12 @@ export function Link({ const {openModal, closeModal} = useModalControls() const onPress = React.useCallback( (e: GestureResponderEvent) => { - const label = typeof children === 'string' ? children : '' + const stringChildren = typeof children === 'string' ? children : '' const requiresWarning = Boolean( warnOnMismatchingTextChild && - label && + stringChildren && isExternal && - linkRequiresWarning(href, label), + linkRequiresWarning(href, stringChildren), ) if (requiresWarning) { @@ -80,7 +83,7 @@ export function Link({ openModal({ name: 'link-warning', - text: label, + text: stringChildren, href: href, }) } else { @@ -139,6 +142,7 @@ export function Link({ return ( ) } + +function LinkText({ + children, + style, +}: React.PropsWithChildren<{ + style?: StyleProp +}>) { + const t = useTheme() + const {hovered} = useButtonContext() + return ( + + {children as string} + + ) +} diff --git a/src/view/screens/Storybook/Buttons.tsx b/src/view/screens/Storybook/Buttons.tsx index 168d088408..fbdc84eb4b 100644 --- a/src/view/screens/Storybook/Buttons.tsx +++ b/src/view/screens/Storybook/Buttons.tsx @@ -2,8 +2,16 @@ import React from 'react' import {View} from 'react-native' import {atoms as a} from '#/alf' -import {Button, ButtonVariant, ButtonColor} from '#/components/Button' +import { + Button, + ButtonVariant, + ButtonColor, + ButtonIcon, + ButtonText, +} from '#/components/Button' import {H1} from '#/components/Typography' +import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight' +import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' export function Buttons() { return ( @@ -83,6 +91,33 @@ export function Buttons() { )} + + + + + +
) diff --git a/src/view/screens/Storybook/Links.tsx b/src/view/screens/Storybook/Links.tsx index 2b9da2a9de..c3b1c0e0f3 100644 --- a/src/view/screens/Storybook/Links.tsx +++ b/src/view/screens/Storybook/Links.tsx @@ -13,27 +13,21 @@ export function Links() { External - +

External with custom children

+ style={[a.text_lg]}> https://blueskyweb.xyz @@ -41,12 +35,12 @@ export function Links() { - {({props}) => Link as a button} + Link as a button
From 6d3f19c695ae667163788b7eabb0ff1c45bf60c3 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 18 Jan 2024 15:14:42 -0600 Subject: [PATCH 62/68] Add todo --- src/components/forms/Toggle.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/forms/Toggle.tsx b/src/components/forms/Toggle.tsx index 6fdbaa2eed..7c77207749 100644 --- a/src/components/forms/Toggle.tsx +++ b/src/components/forms/Toggle.tsx @@ -247,6 +247,7 @@ export function Label({children}: React.PropsWithChildren<{}>) { ) } +// TODO(eric) refactor to memoize styles without knowledge of state export function createSharedToggleStyles({ theme: t, hovered, From 83c2b20c393dec76f1b46645927817ab0acf2f6a Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 18 Jan 2024 15:45:37 -0600 Subject: [PATCH 63/68] Add closeAllDialogs control --- src/App.native.tsx | 17 ++++++---- src/App.web.tsx | 17 ++++++---- src/components/Dialog/context.ts | 12 +++++++ src/state/dialogs/index.tsx | 44 ++++++++++++++++++++++++++ src/view/screens/Storybook/Dialogs.tsx | 16 +++++++++- src/view/screens/Storybook/Forms.tsx | 1 - 6 files changed, 91 insertions(+), 16 deletions(-) create mode 100644 src/state/dialogs/index.tsx diff --git a/src/App.native.tsx b/src/App.native.tsx index 1c3a3f3e74..41b78fc98a 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -27,6 +27,7 @@ import {queryClient} from 'lib/react-query' import {TestCtrls} from 'view/com/testing/TestCtrls' import {Provider as ShellStateProvider} from 'state/shell' import {Provider as ModalStateProvider} from 'state/modals' +import {Provider as DialogStateProvider} from 'state/dialogs' import {Provider as LightboxStateProvider} from 'state/lightbox' import {Provider as MutedThreadsProvider} from 'state/muted-threads' import {Provider as InvitesStateProvider} from 'state/invites' @@ -115,13 +116,15 @@ function App() { - - - - - - - + + + + + + + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index 756d4954eb..1efa0567c7 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -17,6 +17,7 @@ import {ThemeProvider} from 'lib/ThemeContext' import {queryClient} from 'lib/react-query' import {Provider as ShellStateProvider} from 'state/shell' import {Provider as ModalStateProvider} from 'state/modals' +import {Provider as DialogStateProvider} from 'state/dialogs' import {Provider as LightboxStateProvider} from 'state/lightbox' import {Provider as MutedThreadsProvider} from 'state/muted-threads' import {Provider as InvitesStateProvider} from 'state/invites' @@ -93,13 +94,15 @@ function App() { - - - - - - - + + + + + + + + + diff --git a/src/components/Dialog/context.ts b/src/components/Dialog/context.ts index 76473aa243..b28b9f5a25 100644 --- a/src/components/Dialog/context.ts +++ b/src/components/Dialog/context.ts @@ -1,4 +1,6 @@ import React from 'react' + +import {useDialogStateContext} from '#/state/dialogs' import {DialogContextProps, DialogControlProps} from '#/components/Dialog/types' export const Context = React.createContext({ @@ -10,10 +12,20 @@ export function useDialogContext() { } export function useDialogControl() { + const id = React.useId() const control = React.useRef({ open: () => {}, close: () => {}, }) + const {activeDialogs} = useDialogStateContext() + + React.useEffect(() => { + activeDialogs.current.set(id, control) + return () => { + // eslint-disable-next-line react-hooks/exhaustive-deps + activeDialogs.current.delete(id) + } + }, [id, activeDialogs]) return { ref: control, diff --git a/src/state/dialogs/index.tsx b/src/state/dialogs/index.tsx new file mode 100644 index 0000000000..4cafaa0861 --- /dev/null +++ b/src/state/dialogs/index.tsx @@ -0,0 +1,44 @@ +import React from 'react' +import {DialogControlProps} from '#/components/Dialog' + +const DialogContext = React.createContext<{ + activeDialogs: React.MutableRefObject< + Map> + > +}>({ + activeDialogs: { + current: new Map(), + }, +}) + +const DialogControlContext = React.createContext<{ + closeAllDialogs(): void +}>({ + closeAllDialogs: () => {}, +}) + +export function useDialogStateContext() { + return React.useContext(DialogContext) +} + +export function useDialogStateControlContext() { + return React.useContext(DialogControlContext) +} + +export function Provider({children}: React.PropsWithChildren<{}>) { + const activeDialogs = React.useRef< + Map> + >(new Map()) + const closeAllDialogs = React.useCallback(() => { + activeDialogs.current.forEach(dialog => dialog.current.close()) + }, []) + const context = React.useMemo(() => ({activeDialogs}), []) + const controls = React.useMemo(() => ({closeAllDialogs}), [closeAllDialogs]) + return ( + + + {children} + + + ) +} diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx index d4dcc91938..db568c6bd5 100644 --- a/src/view/screens/Storybook/Dialogs.tsx +++ b/src/view/screens/Storybook/Dialogs.tsx @@ -6,10 +6,12 @@ import {Button} from '#/components/Button' import {H3, P} from '#/components/Typography' import * as Dialog from '#/components/Dialog' import * as Prompt from '#/components/Prompt' +import {useDialogStateControlContext} from '#/state/dialogs' export function Dialogs() { const control = Dialog.useDialogControl() const prompt = Prompt.usePromptControl() + const {closeAllDialogs} = useDialogStateControlContext() return ( @@ -17,7 +19,10 @@ export function Dialogs() { variant="outline" color="secondary" size="small" - onPress={() => control.open()} + onPress={() => { + control.open() + prompt.open() + }} label="Open basic dialog"> Open basic dialog @@ -57,6 +62,15 @@ export function Dialogs() { A scrollable dialog with an input within it.

{}} label="Type here" /> + + - - ) -} +/** + * TODO(eric) unused rn + */ +// export function Close() { +// const {_} = useLingui() +// const t = useTheme() +// const {close} = useDialogContext() +// return ( +// +// +// +// ) +// }