about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2024-02-19 10:08:21 -0600
committerGitHub <noreply@github.com>2024-02-19 08:08:21 -0800
commit943acd16aac187ed4eb5f9cea67d6c507aab1ab5 (patch)
tree8a9960f70d317e5fb564f52b3f8bcb443b4f3280 /src
parent7390863a1005eeadbb6dbdcbc47f9cc13298e101 (diff)
downloadvoidsky-943acd16aac187ed4eb5f9cea67d6c507aab1ab5.tar.zst
Add `selectable` to new text components (#2899)
* Make new text selectable (broken)

* Fixes

* Fix bad conflict resolution

* Remove console
Diffstat (limited to 'src')
-rw-r--r--src/components/Link.tsx77
-rw-r--r--src/components/RichText.tsx23
-rw-r--r--src/components/Typography.tsx38
-rw-r--r--src/view/screens/Storybook/Typography.tsx5
4 files changed, 74 insertions, 69 deletions
diff --git a/src/components/Link.tsx b/src/components/Link.tsx
index afd30b5ee..85c13270a 100644
--- a/src/components/Link.tsx
+++ b/src/components/Link.tsx
@@ -1,9 +1,5 @@
 import React from 'react'
-import {
-  GestureResponderEvent,
-  Linking,
-  TouchableWithoutFeedback,
-} from 'react-native'
+import {GestureResponderEvent, Linking} from 'react-native'
 import {
   useLinkProps,
   useNavigation,
@@ -23,7 +19,7 @@ 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'
 
 /**
  * Only available within a `Link`, since that inherits from `Button`.
@@ -217,7 +213,7 @@ export function Link({
 }
 
 export type InlineLinkProps = React.PropsWithChildren<
-  BaseLinkProps & TextStyleProp
+  BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'>
 >
 
 export function InlineLink({
@@ -228,6 +224,7 @@ export function InlineLink({
   style,
   onPress: outerOnPress,
   download,
+  selectable,
   ...rest
 }: InlineLinkProps) {
   const t = useTheme()
@@ -253,43 +250,41 @@ export function InlineLink({
   const flattenedStyle = flatten(style)
 
   return (
-    <TouchableWithoutFeedback
-      accessibilityRole="button"
+    <Text
+      selectable={selectable}
+      label={href}
+      {...rest}
+      style={[
+        {color: t.palette.primary_500},
+        (hovered || focused || pressed) && {
+          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},
-          (hovered || focused || pressed) && {
-            outline: 0,
-            textDecorationLine: 'underline',
-            textDecorationColor: flattenedStyle.color ?? t.palette.primary_500,
-          },
-          flattenedStyle,
-        ]}
-        role="link"
-        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>
-    </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>
   )
 }
diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx
index 068ee99e0..8aeef9ea1 100644
--- a/src/components/RichText.tsx
+++ b/src/components/RichText.tsx
@@ -3,7 +3,7 @@ import {RichText as RichTextAPI, AppBskyRichtextFacet} from '@atproto/api'
 
 import {atoms as a, TextStyleProp} from '#/alf'
 import {InlineLink} from '#/components/Link'
-import {Text} from '#/components/Typography'
+import {Text, TextProps} from '#/components/Typography'
 import {toShortUrl} from 'lib/strings/url-helpers'
 import {getAgent} from '#/state/session'
 
@@ -16,13 +16,15 @@ export function RichText({
   numberOfLines,
   disableLinks,
   resolveFacets = false,
-}: TextStyleProp & {
-  value: RichTextAPI | string
-  testID?: string
-  numberOfLines?: number
-  disableLinks?: boolean
-  resolveFacets?: boolean
-}) {
+  selectable,
+}: TextStyleProp &
+  Pick<TextProps, 'selectable'> & {
+    value: RichTextAPI | string
+    testID?: string
+    numberOfLines?: number
+    disableLinks?: boolean
+    resolveFacets?: boolean
+  }) {
   const detected = React.useRef(false)
   const [richText, setRichText] = React.useState<RichTextAPI>(() =>
     value instanceof RichTextAPI ? value : new RichTextAPI({text: value}),
@@ -50,6 +52,7 @@ export function RichText({
     if (text.length <= 5 && /^\p{Extended_Pictographic}+$/u.test(text)) {
       return (
         <Text
+          selectable={selectable}
           testID={testID}
           style={[
             {
@@ -65,6 +68,7 @@ export function RichText({
     }
     return (
       <Text
+        selectable={selectable}
         testID={testID}
         style={styles}
         numberOfLines={numberOfLines}
@@ -88,6 +92,7 @@ export function RichText({
     ) {
       els.push(
         <InlineLink
+          selectable={selectable}
           key={key}
           to={`/profile/${mention.did}`}
           style={[...styles, {pointerEvents: 'auto'}]}
@@ -102,6 +107,7 @@ export function RichText({
       } else {
         els.push(
           <InlineLink
+            selectable={selectable}
             key={key}
             to={link.uri}
             style={[...styles, {pointerEvents: 'auto'}]}
@@ -120,6 +126,7 @@ export function RichText({
 
   return (
     <Text
+      selectable={selectable}
       testID={testID}
       style={styles}
       numberOfLines={numberOfLines}
diff --git a/src/components/Typography.tsx b/src/components/Typography.tsx
index b34f51018..c9ab7a8a1 100644
--- a/src/components/Typography.tsx
+++ b/src/components/Typography.tsx
@@ -1,7 +1,16 @@
 import React from 'react'
-import {Text as RNText, TextStyle, TextProps} from 'react-native'
+import {Text as RNText, TextStyle, TextProps as RNTextProps} from 'react-native'
+import {UITextView} from 'react-native-ui-text-view'
 
 import {useTheme, atoms, web, flatten} from '#/alf'
+import {isIOS} from '#/platform/detection'
+
+export type TextProps = RNTextProps & {
+  /**
+   * Lets the user select text, to use the native copy and paste functionality.
+   */
+  selectable?: boolean
+}
 
 /**
  * Util to calculate lineHeight from a text size atom and a leading atom
@@ -44,27 +53,24 @@ function normalizeTextStyles(styles: TextStyle[]) {
 /**
  * Our main text component. Use this most of the time.
  */
-export function Text({style, ...rest}: TextProps) {
+export function Text({style, selectable, ...rest}: TextProps) {
   const t = useTheme()
   const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)])
-  return <RNText style={s} {...rest} />
+  return selectable && isIOS ? (
+    <UITextView style={s} {...rest} />
+  ) : (
+    <RNText selectable={selectable} style={s} {...rest} />
+  )
 }
 
 export function createHeadingElement({level}: {level: number}) {
   return function HeadingElement({style, ...rest}: TextProps) {
-    const t = useTheme()
     const attr =
       web({
         role: 'heading',
         'aria-level': level,
       }) || {}
-    return (
-      <RNText
-        {...attr}
-        {...rest}
-        style={normalizeTextStyles([t.atoms.text, flatten(style)])}
-      />
-    )
+    return <Text {...attr} {...rest} style={style} />
   }
 }
 
@@ -78,21 +84,15 @@ export const H4 = createHeadingElement({level: 4})
 export const H5 = createHeadingElement({level: 5})
 export const H6 = createHeadingElement({level: 6})
 export function P({style, ...rest}: TextProps) {
-  const t = useTheme()
   const attr =
     web({
       role: 'paragraph',
     }) || {}
   return (
-    <RNText
+    <Text
       {...attr}
       {...rest}
-      style={normalizeTextStyles([
-        atoms.text_md,
-        atoms.leading_normal,
-        t.atoms.text,
-        flatten(style),
-      ])}
+      style={[atoms.text_md, atoms.leading_normal, flatten(style)]}
     />
   )
 }
diff --git a/src/view/screens/Storybook/Typography.tsx b/src/view/screens/Storybook/Typography.tsx
index 5d3a96f4d..8ee4270b2 100644
--- a/src/view/screens/Storybook/Typography.tsx
+++ b/src/view/screens/Storybook/Typography.tsx
@@ -8,7 +8,9 @@ import {RichText} from '#/components/RichText'
 export function Typography() {
   return (
     <View style={[a.gap_md]}>
-      <Text style={[a.text_5xl]}>atoms.text_5xl</Text>
+      <Text selectable style={[a.text_5xl]}>
+        atoms.text_5xl
+      </Text>
       <Text style={[a.text_4xl]}>atoms.text_4xl</Text>
       <Text style={[a.text_3xl]}>atoms.text_3xl</Text>
       <Text style={[a.text_2xl]}>atoms.text_2xl</Text>
@@ -24,6 +26,7 @@ export function Typography() {
         value={`This is rich text. It can have mentions like @bsky.app or links like https://bsky.social`}
       />
       <RichText
+        selectable
         resolveFacets
         value={`This is rich text. It can have mentions like @bsky.app or links like https://bsky.social`}
         style={[a.text_xl]}