about summary refs log tree commit diff
path: root/src/components/Link.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/Link.tsx')
-rw-r--r--src/components/Link.tsx185
1 files changed, 103 insertions, 82 deletions
diff --git a/src/components/Link.tsx b/src/components/Link.tsx
index 763f07ca9..7d0e83332 100644
--- a/src/components/Link.tsx
+++ b/src/components/Link.tsx
@@ -1,21 +1,13 @@
 import React from 'react'
-import {
-  GestureResponderEvent,
-  Linking,
-  TouchableWithoutFeedback,
-} from 'react-native'
-import {
-  useLinkProps,
-  useNavigation,
-  StackActions,
-} from '@react-navigation/native'
+import {GestureResponderEvent} from 'react-native'
+import {useLinkProps, StackActions} from '@react-navigation/native'
 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 {AllNavigatorParams} from '#/lib/routes/types'
 import {
   convertBskyAppUrlIfNeeded,
   isExternalUrl,
@@ -23,7 +15,9 @@ import {
 } from '#/lib/strings/url-helpers'
 import {useModalControls} from '#/state/modals'
 import {router} from '#/routes'
-import {Text} from '#/components/Typography'
+import {Text, TextProps} from '#/components/Typography'
+import {useOpenLink} from 'state/preferences/in-app-browser'
+import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
 
 /**
  * Only available within a `Link`, since that inherits from `Button`.
@@ -35,6 +29,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.
    */
@@ -45,29 +46,48 @@ type BaseLinkProps = Pick<
    *
    * Note: atm this only works for `InlineLink`s with a string child.
    */
-  warnOnMismatchingTextChild?: boolean
+  disableMismatchWarning?: boolean
+
+  /**
+   * Callback for when the link is pressed. Prevent default and return `false`
+   * to exit early and prevent navigation.
+   *
+   * DO NOT use this for navigation, that's what the `to` prop is for.
+   */
+  onPress?: (e: GestureResponderEvent) => void | false
+
+  /**
+   * Web-only attribute. Sets `download` attr on web.
+   */
+  download?: string
 }
 
 export function useLink({
   to,
   displayText,
   action = 'push',
-  warnOnMismatchingTextChild,
+  disableMismatchWarning,
+  onPress: outerOnPress,
 }: BaseLinkProps & {
   displayText: string
 }) {
-  const navigation = useNavigation<NavigationProp>()
+  const navigation = useNavigationDeduped()
   const {href} = useLinkProps<AllNavigatorParams>({
     to:
       typeof to === 'string' ? convertBskyAppUrlIfNeeded(sanitizeUrl(to)) : to,
   })
   const isExternal = isExternalUrl(href)
   const {openModal, closeModal} = useModalControls()
+  const openLink = useOpenLink()
 
   const onPress = React.useCallback(
     (e: GestureResponderEvent) => {
+      const exitEarlyIfFalse = outerOnPress?.(e)
+
+      if (exitEarlyIfFalse === false) return
+
       const requiresWarning = Boolean(
-        warnOnMismatchingTextChild &&
+        !disableMismatchWarning &&
           displayText &&
           isExternal &&
           linkRequiresWarning(href, displayText),
@@ -85,7 +105,7 @@ export function useLink({
         e.preventDefault()
 
         if (isExternal) {
-          Linking.openURL(href)
+          openLink(href)
         } else {
           /**
            * A `GestureResponderEvent`, but cast to `any` to avoid using a bunch
@@ -103,7 +123,7 @@ export function useLink({
             href.startsWith('http') ||
             href.startsWith('mailto')
           ) {
-            Linking.openURL(href)
+            openLink(href)
           } else {
             closeModal() // close any active modals
 
@@ -124,14 +144,16 @@ export function useLink({
       }
     },
     [
-      href,
-      isExternal,
-      warnOnMismatchingTextChild,
-      navigation,
-      action,
+      outerOnPress,
+      disableMismatchWarning,
       displayText,
-      closeModal,
+      isExternal,
+      href,
       openModal,
+      openLink,
+      closeModal,
+      action,
+      navigation,
     ],
   )
 
@@ -142,17 +164,8 @@ 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
-  }
+export type LinkProps = Omit<BaseLinkProps, 'disableMismatchWarning'> &
+  Omit<ButtonProps, 'onPress' | 'disabled' | 'label'>
 
 /**
  * A interactive element that renders as a `<a>` tag on the web. On mobile it
@@ -166,6 +179,7 @@ export function Link({
   children,
   to,
   action = 'push',
+  onPress: outerOnPress,
   download,
   ...rest
 }: LinkProps) {
@@ -173,24 +187,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,21 +216,19 @@ export function Link({
 }
 
 export type InlineLinkProps = React.PropsWithChildren<
-  BaseLinkProps &
-    TextStyleProp & {
-      /**
-       * Label for a11y. Defaults to the href.
-       */
-      label?: string
-    }
+  BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'>
 >
 
 export function InlineLink({
   children,
   to,
   action = 'push',
-  warnOnMismatchingTextChild,
+  disableMismatchWarning,
   style,
+  onPress: outerOnPress,
+  download,
+  selectable,
+  label,
   ...rest
 }: InlineLinkProps) {
   const t = useTheme()
@@ -223,52 +237,59 @@ export function InlineLink({
     to,
     displayText: stringChildren ? children : '',
     action,
-    warnOnMismatchingTextChild,
+    disableMismatchWarning,
+    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}
+    <Text
+      selectable={selectable}
+      accessibilityHint=""
+      accessibilityLabel={label || href}
+      {...rest}
+      style={[
+        {color: t.palette.primary_500},
+        (hovered || focused || pressed) && {
+          ...web({outline: 0}),
+          textDecorationLine: 'underline',
+          textDecorationColor: flattenedStyle.color ?? t.palette.primary_500,
+        },
+        flattenedStyle,
+      ]}
+      role="link"
+      onPress={download ? undefined : onPress}
       onPressIn={onPressIn}
       onPressOut={onPressOut}
       onFocus={onFocus}
-      onBlur={onBlur}>
-      <Text
-        label={href}
-        {...rest}
-        style={[
-          {color: t.palette.primary_500},
-          (focused || pressed) && {
-            outline: 0,
-            textDecorationLine: 'underline',
-            textDecorationColor: t.palette.primary_500,
-          },
-          flatten(style),
-        ]}
-        role="link"
-        accessibilityRole="link"
-        href={href}
-        {...web({
-          hrefAttrs: {
-            target: isExternal ? 'blank' : undefined,
-            rel: isExternal ? 'noopener noreferrer' : undefined,
-          },
-          dataSet: stringChildren
-            ? {}
-            : {
-                // default to no underline, apply this ourselves
-                noUnderline: '1',
-              },
-        })}>
-        {children}
-      </Text>
-    </TouchableWithoutFeedback>
+      onBlur={onBlur}
+      onMouseEnter={onHoverIn}
+      onMouseLeave={onHoverOut}
+      accessibilityRole="link"
+      href={href}
+      {...web({
+        hrefAttrs: {
+          target: download ? undefined : isExternal ? 'blank' : undefined,
+          rel: isExternal ? 'noopener noreferrer' : undefined,
+          download,
+        },
+        dataSet: {
+          // default to no underline, apply this ourselves
+          noUnderline: '1',
+        },
+      })}>
+      {children}
+    </Text>
   )
 }