about summary refs log tree commit diff
path: root/src/components
diff options
context:
space:
mode:
authorMinseo Lee <itoupluk427@gmail.com>2024-02-19 14:17:59 +0900
committerGitHub <noreply@github.com>2024-02-19 14:17:59 +0900
commit6d422bb583bf8946d92fe270b1fe5c760251f0cc (patch)
tree11871ebc34d48c5afcac1aa56464f8bffdc9f83a /src/components
parentb3e6f0f29deae515d232d14f099e8c144d870f48 (diff)
parent7390863a1005eeadbb6dbdcbc47f9cc13298e101 (diff)
downloadvoidsky-6d422bb583bf8946d92fe270b1fe5c760251f0cc.tar.zst
Merge branch 'main' into patch-3
Diffstat (limited to 'src/components')
-rw-r--r--src/components/Button.tsx100
-rw-r--r--src/components/Link.tsx85
-rw-r--r--src/components/Loader.tsx13
3 files changed, 130 insertions, 68 deletions
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index 68cee4374..e401bda2a 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -27,7 +27,7 @@ export type ButtonColor =
   | 'gradient_sunset'
   | 'gradient_nordic'
   | 'gradient_bonfire'
-export type ButtonSize = 'small' | 'large'
+export type ButtonSize = 'tiny' | 'small' | 'large'
 export type ButtonShape = 'round' | 'square' | 'default'
 export type VariantProps = {
   /**
@@ -48,25 +48,32 @@ export type VariantProps = {
   shape?: ButtonShape
 }
 
-export type ButtonProps = React.PropsWithChildren<
-  Pick<PressableProps, 'disabled' | 'onPress'> &
-    AccessibilityProps &
-    VariantProps & {
-      testID?: string
-      label: string
-      style?: StyleProp<ViewStyle>
-    }
->
-export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
+export type ButtonState = {
+  hovered: boolean
+  focused: boolean
+  pressed: boolean
+  disabled: boolean
+}
+
+export type ButtonContext = VariantProps & ButtonState
 
-const Context = React.createContext<
+export type ButtonProps = Pick<
+  PressableProps,
+  'disabled' | 'onPress' | 'testID'
+> &
+  AccessibilityProps &
   VariantProps & {
-    hovered: boolean
-    focused: boolean
-    pressed: boolean
-    disabled: boolean
+    testID?: string
+    label: string
+    style?: StyleProp<ViewStyle>
+    children:
+      | React.ReactNode
+      | string
+      | ((context: ButtonContext) => React.ReactNode | string)
   }
->({
+export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
+
+const Context = React.createContext<VariantProps & ButtonState>({
   hovered: false,
   focused: false,
   pressed: false,
@@ -277,6 +284,8 @@ export function Button({
         baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md)
       } else if (size === 'small') {
         baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm)
+      } else if (size === 'tiny') {
+        baseStyles.push({paddingVertical: 4}, a.px_sm, a.rounded_xs, a.gap_xs)
       }
     } else if (shape === 'round' || shape === 'square') {
       if (size === 'large') {
@@ -287,12 +296,18 @@ export function Button({
         }
       } else if (size === 'small') {
         baseStyles.push({height: 40, width: 40})
+      } else if (size === 'tiny') {
+        baseStyles.push({height: 20, width: 20})
       }
 
       if (shape === 'round') {
         baseStyles.push(a.rounded_full)
       } else if (shape === 'square') {
-        baseStyles.push(a.rounded_sm)
+        if (size === 'tiny') {
+          baseStyles.push(a.rounded_xs)
+        } else {
+          baseStyles.push(a.rounded_sm)
+        }
       }
     }
 
@@ -338,7 +353,7 @@ export function Button({
       }
     }, [variant, color])
 
-  const context = React.useMemo(
+  const context = React.useMemo<ButtonContext>(
     () => ({
       ...state,
       variant,
@@ -349,6 +364,8 @@ export function Button({
     [state, variant, color, size, disabled],
   )
 
+  const flattenedBaseStyles = flatten(baseStyles)
+
   return (
     <Pressable
       role="button"
@@ -362,15 +379,14 @@ export function Button({
         disabled: disabled || false,
       }}
       style={[
-        flatten(style),
         a.flex_row,
         a.align_center,
         a.justify_center,
-        a.overflow_hidden,
         a.justify_center,
-        ...baseStyles,
+        flattenedBaseStyles,
         ...(state.hovered || state.pressed ? hoverStyles : []),
         ...(state.focused ? focusStyles : []),
+        flatten(style),
       ]}
       onPressIn={onPressIn}
       onPressOut={onPressOut}
@@ -379,21 +395,31 @@ export function Button({
       onFocus={onFocus}
       onBlur={onBlur}>
       {variant === 'gradient' && (
-        <LinearGradient
-          colors={
-            state.hovered || state.pressed || state.focused
-              ? gradientHoverColors
-              : gradientColors
-          }
-          locations={gradientLocations}
-          start={{x: 0, y: 0}}
-          end={{x: 1, y: 1}}
-          style={[a.absolute, a.inset_0]}
-        />
+        <View
+          style={[
+            a.absolute,
+            a.inset_0,
+            a.overflow_hidden,
+            {borderRadius: flattenedBaseStyles.borderRadius},
+          ]}>
+          <LinearGradient
+            colors={
+              state.hovered || state.pressed || state.focused
+                ? gradientHoverColors
+                : gradientColors
+            }
+            locations={gradientLocations}
+            start={{x: 0, y: 0}}
+            end={{x: 1, y: 1}}
+            style={[a.absolute, a.inset_0]}
+          />
+        </View>
       )}
       <Context.Provider value={context}>
         {typeof children === 'string' ? (
           <ButtonText>{children}</ButtonText>
+        ) : typeof children === 'function' ? (
+          children(context)
         ) : (
           children
         )}
@@ -493,6 +519,8 @@ export function useSharedButtonTextStyles() {
 
     if (size === 'large') {
       baseStyles.push(a.text_md, android({paddingBottom: 1}))
+    } else if (size === 'tiny') {
+      baseStyles.push(a.text_xs, android({paddingBottom: 1}))
     } else {
       baseStyles.push(a.text_sm, android({paddingBottom: 1}))
     }
@@ -514,9 +542,11 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
 export function ButtonIcon({
   icon: Comp,
   position,
+  size: iconSize,
 }: {
   icon: React.ComponentType<SVGIconProps>
   position?: 'left' | 'right'
+  size?: SVGIconProps['size']
 }) {
   const {size, disabled} = useButtonContext()
   const textStyles = useSharedButtonTextStyles()
@@ -532,7 +562,9 @@ export function ButtonIcon({
         },
       ]}>
       <Comp
-        size={size === 'large' ? 'md' : 'sm'}
+        size={
+          iconSize ?? (size === 'large' ? 'md' : size === 'tiny' ? 'xs' : 'sm')
+        }
         style={[{color: textStyles.color, pointerEvents: 'none'}]}
       />
     </View>
diff --git a/src/components/Link.tsx b/src/components/Link.tsx
index 763f07ca9..afd30b5ee 100644
--- a/src/components/Link.tsx
+++ b/src/components/Link.tsx
@@ -13,7 +13,7 @@ import {sanitizeUrl} from '@braintree/sanitize-url'
 
 import {useInteractionState} from '#/components/hooks/useInteractionState'
 import {isWeb} from '#/platform/detection'
-import {useTheme, web, flatten, TextStyleProp} from '#/alf'
+import {useTheme, web, flatten, TextStyleProp, atoms as a} from '#/alf'
 import {Button, ButtonProps} from '#/components/Button'
 import {AllNavigatorParams, NavigationProp} from '#/lib/routes/types'
 import {
@@ -35,6 +35,13 @@ type BaseLinkProps = Pick<
   Parameters<typeof useLinkProps<AllNavigatorParams>>[0],
   'to'
 > & {
+  testID?: string
+
+  /**
+   * Label for a11y. Defaults to the href.
+   */
+  label?: string
+
   /**
    * The React Navigation `StackAction` to perform when the link is pressed.
    */
@@ -46,6 +53,18 @@ type BaseLinkProps = Pick<
    * Note: atm this only works for `InlineLink`s with a string child.
    */
   warnOnMismatchingTextChild?: boolean
+
+  /**
+   * Callback for when the link is pressed.
+   *
+   * DO NOT use this for navigation, that's what the `to` prop is for.
+   */
+  onPress?: (e: GestureResponderEvent) => void
+
+  /**
+   * Web-only attribute. Sets `download` attr on web.
+   */
+  download?: string
 }
 
 export function useLink({
@@ -53,6 +72,7 @@ export function useLink({
   displayText,
   action = 'push',
   warnOnMismatchingTextChild,
+  onPress: outerOnPress,
 }: BaseLinkProps & {
   displayText: string
 }) {
@@ -66,6 +86,8 @@ export function useLink({
 
   const onPress = React.useCallback(
     (e: GestureResponderEvent) => {
+      outerOnPress?.(e)
+
       const requiresWarning = Boolean(
         warnOnMismatchingTextChild &&
           displayText &&
@@ -132,6 +154,7 @@ export function useLink({
       displayText,
       closeModal,
       openModal,
+      outerOnPress,
     ],
   )
 
@@ -143,16 +166,7 @@ export function useLink({
 }
 
 export type LinkProps = Omit<BaseLinkProps, 'warnOnMismatchingTextChild'> &
-  Omit<ButtonProps, 'style' | 'onPress' | 'disabled' | 'label'> & {
-    /**
-     * Label for a11y. Defaults to the href.
-     */
-    label?: string
-    /**
-     * Web-only attribute. Sets `download` attr on web.
-     */
-    download?: string
-  }
+  Omit<ButtonProps, 'onPress' | 'disabled' | 'label'>
 
 /**
  * A interactive element that renders as a `<a>` tag on the web. On mobile it
@@ -166,6 +180,7 @@ export function Link({
   children,
   to,
   action = 'push',
+  onPress: outerOnPress,
   download,
   ...rest
 }: LinkProps) {
@@ -173,24 +188,26 @@ export function Link({
     to,
     displayText: typeof children === 'string' ? children : '',
     action,
+    onPress: outerOnPress,
   })
 
   return (
     <Button
       label={href}
       {...rest}
+      style={[a.justify_start, flatten(rest.style)]}
       role="link"
       accessibilityRole="link"
       href={href}
-      onPress={onPress}
+      onPress={download ? undefined : onPress}
       {...web({
         hrefAttrs: {
-          target: isExternal ? 'blank' : undefined,
+          target: download ? undefined : isExternal ? 'blank' : undefined,
           rel: isExternal ? 'noopener noreferrer' : undefined,
           download,
         },
         dataSet: {
-          // default to no underline, apply this ourselves
+          // no underline, only `InlineLink` has underlines
           noUnderline: '1',
         },
       })}>
@@ -200,13 +217,7 @@ export function Link({
 }
 
 export type InlineLinkProps = React.PropsWithChildren<
-  BaseLinkProps &
-    TextStyleProp & {
-      /**
-       * Label for a11y. Defaults to the href.
-       */
-      label?: string
-    }
+  BaseLinkProps & TextStyleProp
 >
 
 export function InlineLink({
@@ -215,6 +226,8 @@ export function InlineLink({
   action = 'push',
   warnOnMismatchingTextChild,
   style,
+  onPress: outerOnPress,
+  download,
   ...rest
 }: InlineLinkProps) {
   const t = useTheme()
@@ -224,18 +237,25 @@ export function InlineLink({
     displayText: stringChildren ? children : '',
     action,
     warnOnMismatchingTextChild,
+    onPress: outerOnPress,
   })
+  const {
+    state: hovered,
+    onIn: onHoverIn,
+    onOut: onHoverOut,
+  } = useInteractionState()
   const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
   const {
     state: pressed,
     onIn: onPressIn,
     onOut: onPressOut,
   } = useInteractionState()
+  const flattenedStyle = flatten(style)
 
   return (
     <TouchableWithoutFeedback
       accessibilityRole="button"
-      onPress={onPress}
+      onPress={download ? undefined : onPress}
       onPressIn={onPressIn}
       onPressOut={onPressOut}
       onFocus={onFocus}
@@ -245,27 +265,28 @@ export function InlineLink({
         {...rest}
         style={[
           {color: t.palette.primary_500},
-          (focused || pressed) && {
+          (hovered || focused || pressed) && {
             outline: 0,
             textDecorationLine: 'underline',
-            textDecorationColor: t.palette.primary_500,
+            textDecorationColor: flattenedStyle.color ?? t.palette.primary_500,
           },
-          flatten(style),
+          flattenedStyle,
         ]}
         role="link"
+        onMouseEnter={onHoverIn}
+        onMouseLeave={onHoverOut}
         accessibilityRole="link"
         href={href}
         {...web({
           hrefAttrs: {
-            target: isExternal ? 'blank' : undefined,
+            target: download ? undefined : isExternal ? 'blank' : undefined,
             rel: isExternal ? 'noopener noreferrer' : undefined,
+            download,
+          },
+          dataSet: {
+            // default to no underline, apply this ourselves
+            noUnderline: '1',
           },
-          dataSet: stringChildren
-            ? {}
-            : {
-                // default to no underline, apply this ourselves
-                noUnderline: '1',
-              },
         })}>
         {children}
       </Text>
diff --git a/src/components/Loader.tsx b/src/components/Loader.tsx
index bbe4e2f75..b9f399f95 100644
--- a/src/components/Loader.tsx
+++ b/src/components/Loader.tsx
@@ -7,11 +7,12 @@ import Animated, {
   withTiming,
 } from 'react-native-reanimated'
 
-import {atoms as a} from '#/alf'
+import {atoms as a, useTheme, flatten} from '#/alf'
 import {Props, useCommonSVGProps} from '#/components/icons/common'
 import {Loader_Stroke2_Corner0_Rounded as Icon} from '#/components/icons/Loader'
 
 export function Loader(props: Props) {
+  const t = useTheme()
   const common = useCommonSVGProps(props)
   const rotation = useSharedValue(0)
 
@@ -35,7 +36,15 @@ export function Loader(props: Props) {
         {width: common.size, height: common.size},
         animatedStyles,
       ]}>
-      <Icon {...props} style={[a.absolute, a.inset_0, props.style]} />
+      <Icon
+        {...props}
+        style={[
+          a.absolute,
+          a.inset_0,
+          t.atoms.text_contrast_high,
+          flatten(props.style),
+        ]}
+      />
     </Animated.View>
   )
 }