From 1d729721e553848037700688e2f1ccde333a8c84 Mon Sep 17 00:00:00 2001
From: Eric Bailey <git@esb.lol>
Date: Fri, 16 Feb 2024 13:25:07 -0600
Subject: Link updates (#2890)

* Link updates, add atoms

* Update comments

* Support download

* Don't open new window for download
---
 src/components/Link.tsx | 85 ++++++++++++++++++++++++++++++-------------------
 1 file changed, 53 insertions(+), 32 deletions(-)

(limited to 'src/components/Link.tsx')

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>
-- 
cgit 1.4.1

