Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nested Text with onPress / TouchableOpacity Bug #27549

Open
zackify opened this issue Dec 17, 2019 · 43 comments
Open

Nested Text with onPress / TouchableOpacity Bug #27549

zackify opened this issue Dec 17, 2019 · 43 comments
Labels
Bug Component: TouchableOpacity Never gets stale Prevent those issues and PRs from getting stale

Comments

@zackify
Copy link

zackify commented Dec 17, 2019

Hey there. I found an issue when rendering nested Text elements. It's almost the exact same as this ticket: #1030

I was able to get it to sort of work. I had to add an onPress to the Text component.

Problems:

  • TypeScript says there isn't an onPress on Text elements. But it does in fact work. This should probably be fixed in the type definitons.
  • Not possible to use TouchableOpacity, so it doesn't feel good when pressing on these items.

When using TouchableOpacity like this:

<Text>
  <Text>first part</Text>
  <TouchableOpacity><Text>Second part</Text></TouchableOpacity>
</Text>

Second part doesn't get rendered at all.

Before you suggest using a <View> around the <Text> instead, please look at the referenced issue. When you do that, the text runs off screen, or wraps weirdly.

TLDR; I need to add a touchable opacity inside a nested Text component. Our api returns text in blocks, the RN app needs to parse it and render an array of text elements together with different styling.

React Native version:

0.61.4

@zackify zackify added the Bug label Dec 17, 2019
@zackify zackify changed the title Nested Text with onPress Nested Text with onPress / TouchableOpacity Bug Dec 17, 2019
@stale
Copy link

stale bot commented Mar 16, 2020

Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. You may also label this issue as a "Discussion" or add it to the "Backlog" and I will leave it open. Thank you for your contributions.

@stale stale bot added the Stale There has been a lack of activity on this issue and it may be closed soon. label Mar 16, 2020
@zackify
Copy link
Author

zackify commented Mar 17, 2020

Still a problem

@stale stale bot removed the Stale There has been a lack of activity on this issue and it may be closed soon. label Mar 17, 2020
@darrylyoung
Copy link

Yeah, it'd be great if this was possible as I just came across the same issue when trying to nest links inside a string of translated text.

Right now, I'm having to use onPress on my Text component and by using that instead of TouchableOpacity, I get no control over hitSlop, activeOpacity, etc. It just gives me an ugly grey background to the text when I press it, which I can only turn off with suppressHighlighting.

@backmeupplz
Copy link

Same issue here. I have the following setup:

<TouchableOpacity onLongPress={() => console.log('1')}>
  <Text>
    <Text>Some text </Text>
    <Text onPress={() => console.log('2')}>clickable text</Text>
    <Text> another text</Text>
  </Text>
</TouchableOpacity>

I understand that we need to use onPress in the nested Text, my example works on Android (both 1 and 2 can be logged). But it seems that on iOS TouchableOpacity hijacks all touch events and doesn't propagate events down to onPress of the Text element. Any chance we can fix it or there are workarounds? Simply using View with flexDirection: row doesn't wrap text intelligently.

@backmeupplz
Copy link

FYI: when nesting just Text tags on iOS the gestures get propagated :) So this works:

<Text onLongPress={() => console.log('1')}>
  <Text>
    <Text>Some text </Text>
    <Text onPress={() => console.log('2')}>clickable text</Text>
    <Text> another text</Text>
  </Text>
</Text>

@backmeupplz
Copy link

Any activity on it? 🤔

@vinaysharma14
Copy link

Hey @zackify, this should help you out :)

<View style={{ flexDirection: 'row' }}>
    <Text>first part </Text> // notice the empty space space after part

    <TouchableOpacity>
        <Text>second part</Text>
    </TouchableOpacity>
</View>

@backmeupplz
Copy link

backmeupplz commented Aug 18, 2020

@vinaysharma14 looks like the wrapping is broken when you do it like this :(

yasminnvaz added a commit to natura-cosmeticos/natds-rn that referenced this issue Oct 28, 2020
[DSY-851] we have a bug when we try to use this feature within another text tag. The open issue:
facebook/react-native#27549
@stale
Copy link

stale bot commented Dec 25, 2020

Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. You may also label this issue as a "Discussion" or add it to the "Backlog" and I will leave it open. Thank you for your contributions.

@stale stale bot added the Stale There has been a lack of activity on this issue and it may be closed soon. label Dec 25, 2020
@backmeupplz
Copy link

Not fixed yet!

@stale stale bot removed the Stale There has been a lack of activity on this issue and it may be closed soon. label Dec 25, 2020
@Stevemoretz
Copy link

Great we can't do it yet :)

@vinaysharma14
Copy link

@vinaysharma14 looks like the wrapping is broken when you do it like this :(

Hi @zackify @backmeupplz @Stevemoretz I guess this should work fine :)

<Text>
  <Text>
    I believe @backmeupplz had said that sentence wrapping would break{' '}
  </Text>

  <TouchableOpacity>
    <Text>here</Text>
  </TouchableOpacity>

  <Text>
    {' '}but it didn’t it!
  </Text>
</Text>

@backmeupplz
Copy link

backmeupplz commented Jun 11, 2021

@vinaysharma14 does this work on iOS though?

<TouchableOpacity onLongPress={() => console.log('1')}>
  <Text>
    <Text>Some text </Text>
    <Text onPress={() => console.log('2')}>clickable text</Text>
    <Text> another text</Text>
  </Text>
</TouchableOpacity>

@vinaysharma14
Copy link

@backmeupplz I didn’t try but it's not OP's requirement.

@backmeupplz
Copy link

@vinaysharma14 sure, I can create a separate issue for this

@vinaysharma14
Copy link

@backmeupplz may I know what functionality are you trying to achieve with this code snippet? I'm unable to understand the usecase of adding touchable on entire text and an onPress on a word.

@backmeupplz
Copy link

@vinaysharma14 sure. It's a text of a todo in a list of todos. Todo text has links in it. Tapping the text in general copies the whole text. Taping the links opens links.

@vinaysharma14
Copy link

@backmeupplz I visualised the code you wrote into a wireframe. Can you confirm if this is the desired behaviour?

Screenshot 2021-06-13 at 1 47 58 AM

@backmeupplz
Copy link

@vinaysharma14 this looks correct, yes. I can't remember if I had a long tap on the outer side or just a single tap, it was a while ago — but general idea is correct

@vinaysharma14
Copy link

@backmeupplz thanks for confirming that. Either it's a long or single tap, TouchableOpacity is sufficient. So, if we are able to get it working, both taps would work. If you are seeking a solution to this, you can give a thumbs up and I can check if we can implement it in React Native.

@vinaysharma14
Copy link

Hi @backmeupplz, you may check the implementation below.

GIF Demo

Untitled

Code

import React, { useCallback, Children } from 'react';

import {
  View,
  Linking,
  TextInput,
  StyleSheet,
  SafeAreaView,
  Text as RNText,
  TouchableOpacity,
} from 'react-native';

import RNClipboard from '@react-native-clipboard/clipboard';

// =================== Reusable Components =================== //

const Text = ({ children, style = {} }) => (
  <RNText style={[styles.text, style]}>{children}</RNText>
);

const Link = ({ text, link }) => {
  const openLink = useCallback(() => {
    Linking.openURL(link);
  }, [link]);

  return (
    <TouchableOpacity style={styles.touchable} onPress={openLink}>
      <Text style={styles.red}>{text}</Text>
    </TouchableOpacity>
  );
};

const Clipboard = ({ text, children }) => {
  const copyToClipboard = useCallback(() => {
    RNClipboard.setString(text);
  }, [text]);

  return (
    <TouchableOpacity onPress={copyToClipboard}>{children}</TouchableOpacity>
  );
};

// ===================== Util Function ===================== //

const isString = value => typeof value === 'string';

// ===================== Mock Links ===================== //

const { name, rapidReact, mmt, react, reactNative, node } = {
  name: {
    text: 'Vinay Sharma',
    link: 'https://www.linkedin.com/in/vinaysharma-/',
  },
  rapidReact: {
    text: 'Rapid React',
    link: 'https://www.npmjs.com/package/rapid-react',
  },
  mmt: {
    text: 'MakeMyTrip',
    link: 'https://www.makemytrip.com/',
  },
  react: {
    text: 'React',
    link: 'https://reactjs.org/',
  },
  reactNative: {
    text: 'React Native',
    link: 'https://reactnative.dev/',
  },
  node: {
    text: 'Node',
    link: 'https://nodejs.org/en/',
  },
};

// ================== Mock Message ================== //

const mockMsg = [
  'Hi, my name is ',
  name,
  '. I am the author of ',
  rapidReact,
  ' and a SDE at ',
  mmt,
  '.\n\n',
  'I love developing Full Stack applications with ',
  react,
  ', ',
  reactNative,
  ', ',
  node,
  ' and much more!',
];

const stringifiedMockMsg = mockMsg
  .map(msg => (isString(msg) ? msg : msg.text))
  .join('');

// ===================== App ===================== //

const App = () => {
  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.subContainer}>
        <Clipboard text={stringifiedMockMsg}>
          <Text>
            {Children.toArray(
              mockMsg.map(msg =>
                isString(msg) ? <Text>{msg}</Text> : <Link {...msg} />,
              ),
            )}
          </Text>
        </Clipboard>
      </View>

      <TextInput
        multiline
        placeholder="Paste here"
        placeholderTextColor="#999"
        style={[styles.subContainer, styles.input]}
      />
    </SafeAreaView>
  );
};

// ===================== Styles ===================== //

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    backgroundColor: '#fff',
  },
  subContainer: {
    padding: 20,
    borderWidth: 1,
    borderRadius: 5,
    borderColor: '#000',
    marginHorizontal: 50,
  },
  input: {
    height: 180,
    marginTop: 50,
    paddingTop: 20,
  },
  touchable: {
    marginBottom: -3,
  },
  text: {
    fontSize: 15,
  },
  red: {
    color: 'red',
  },
});

export default App;

Package.json

{
  "name": "foo",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "start": "react-native start",
    "test": "jest",
    "lint": "eslint ."
  },
  "dependencies": {
    "@react-native-clipboard/clipboard": "^1.8.1",
    "@react-native-community/clipboard": "^1.5.1",
    "react": "17.0.1",
    "react-native": "0.64.2"
  },
  "devDependencies": {
    "@babel/core": "^7.12.9",
    "@babel/runtime": "^7.12.5",
    "@react-native-community/eslint-config": "^2.0.0",
    "@types/react-native": "^0.64.10",
    "babel-jest": "^26.6.3",
    "eslint": "7.14.0",
    "jest": "^26.6.3",
    "metro-react-native-babel-preset": "^0.64.0",
    "react-test-renderer": "17.0.1"
  },
  "jest": {
    "preset": "react-native"
  }
}

@backmeupplz
Copy link

@vinaysharma14 thank you for such a thorough investigation and for the example! It looks like this works :) Cheers!

@devendra-learngram-ai
Copy link

devendra-learngram-ai commented Jul 30, 2021

Recently I encountered the same issue, as I had to render urls differently than a simple text in chat. I tried 3 different Approach, out of which the last approach worked pretty neatly and does what I want.

What I want:

  1. It should render url and plain text differently
  2. It should have access to all touch events same as any Touchable components, i.e onPressIn, onLongPress, onPressOut, and onPress
  3. It should be able to wrap multiline messages
const MessageBox = (message) => {
  const URL_REGEX =
    /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;

  return (
    <Text>
      {message.split(" ").map((word) =>
        URL_REGEX.test(part) ? (
          <Text
            onResponderGrant={(event) =>
              console.log(
                "this is the time to highlight and show the user what is happening"
              )
            }
            onLongPress={() => console.log("use this to copy message")}
            onResponderRelease={(event) =>
              console.log("this is the time to open link in the browser")
            }
          >
            {url}
          </Text>
        ) : (
          <Text>{word}</Text>
        )
      )}
    </Text>
  );
};

@cakasuma
Copy link

@devendra-learngram-ai is there any example of how to use the onResponderGrand and onResponderRelease to imitate touchableopacity animation on text onPress?

@aprilmintacpineda
Copy link

It's 2022 and we still can't make a proper clickable text that's inside a paragraph.

<Text
  style={{
    marginTop: 45,
    width: 167,
    textAlign: 'center'
  }}
>
  Don’t have an account yet?{' '}
  <TouchableOpacity
    onPress={() => {
      console.log('fuck');
    }}
  >
    <Text style={{ color: 'red' }}>Create account</Text>
  </TouchableOpacity>{' '}
  now!
</Text>

The code above, you be able to render a paragraph, but the touchable text is not properly aligned. It will be pushed up by a few pixels and I can't find a workaround to fix it.

We have self driving cars but can't do a proper touchable text, ironic.

@Stevemoretz
Copy link

Stevemoretz commented Feb 13, 2022

Don't use touchable opacity use a Text also it has an onPress prop, you can also modify the style for that.

@aprilmintacpineda
Copy link

Don't use touchable opacity use a Text also it has an onPress prop, you can also modify the style for that.

I know, but that won't have a proper response, I tried making my own with Animated.Text but the opacity won't work unless I wrap it inside View but that produces the same problem -- the text will be misaligned.

@Stevemoretz
Copy link

@devendra-learngram-ai is there any example of how to use the onResponderGrand and onResponderRelease to imitate touchableopacity animation on text onPress?

You could try with reanimated2 Animated.Text that might actually work without any problems. Haven't tested it myself yet though it should be pretty easy to test.

@Stevemoretz
Copy link

Don't use touchable opacity use a Text also it has an onPress prop, you can also modify the style for that.

I know, but that won't have a proper response, I tried making my own with Animated.Text but the opacity won't work unless I wrap it inside View but that produces the same problem -- the text will be misaligned.

Yeah I meant only it works without the animation, but if you need animation let me try with reanimated 2 and report the results here.

@aprilmintacpineda
Copy link

I'm using "react-native-reanimated": "^2.4.1" it didn't work, the opacity won't work until you wrap the Animated.Text inside the View. I deleted the whole code for it out of frustration, because I've been stuck here for about 1.5h searching on google for answers, I tried adding marginTop: -3 and it worked for ios (the text was aligned) but not for android. I'll just stick to unresponsive touchable text I suppose. It's a shame but it is what it is.

@aprilmintacpineda
Copy link

Thanks for responding btw, I really appreciate it.

@Stevemoretz
Copy link

Stevemoretz commented Feb 13, 2022

Sure! No problem.
Yeah I just remembered gestureHandler only works if there is an Animated.View nested in it, so that's not really an option here.

We at least need to have two events "on text tap" and "on text release" natively added, that way we can easily fix the issue with reanimated (if the animation also works lol) , right now it's impossible, (unless those events exist already that I'm not aware of)

@devendra-learngram-ai
Copy link

@devendra-learngram-ai is there any example of how to use the onResponderGrand and onResponderRelease to imitate touchableopacity animation on text onPress?

@cakasuma I didn't want to go through extra trouble after making the Text click work, so I took another path and highlighted text on click to let user know about the click. So I set the state inside onResponderGrant, and based on that state I applied the background color in the Text. And on ResponderRelease I set the state back to its initial value, which will remove the background color from Text.

@Stevemoretz
Copy link

@devendra-learngram-ai is there any example of how to use the onResponderGrand and onResponderRelease to imitate touchableopacity animation on text onPress?

@cakasuma I didn't want to go through extra trouble after making the Text click work, so I took another path and highlighted text on click to let user know about the click. So I set the state inside onResponderGrant, and based on that state I applied the background color in the Text. And on ResponderRelease I set the state back to its initial value, which will remove the background color from Text.

just testing:

                <Text
                    onResponderGrant={() => {
                        console.log("s");
                    }}
                >
                    Hello
                </Text>

Doesn't work!
Also the prop doesn't exist in the types.

@Stevemoretz
Copy link

Tried with native animated api by react-native:

const fadeAnim = useRef(new Animated.Value(0)).current; // Initial value for opacity: 0

    useEffect(() => {
        Animated.timing(fadeAnim, {
            toValue: 1,
            duration: 10000,
            easing: Easing.linear,
        }).start();
    }, [fadeAnim]);

    return (
        <View style={{marginTop: 200}}>
            <Animated.Text>
                Hi
                <Animated.Text
                    style={{
                        opacity: fadeAnim,
                    }}
                >
                    Hello
                </Animated.Text>
            </Animated.Text>
        </View>
    );

This also works only on the top level text not the nested, must be related to :
#15934 (comment)

It's basically impossible right now to implement even a workaround for the animation part.

@Hostname47
Copy link

With a small addition to @aprilmintacpineda answer, you'll be able to have an aligned TouchableOpacity aligned with a text

I'm using "react-native-reanimated": "^2.4.1" it didn't work, the opacity won't work until you wrap the Animated.Text inside the View. I deleted the whole code for it out of frustration, because I've been stuck here for about 1.5h searching on google for answers, I tried adding marginTop: -3 and it worked for ios (the text was aligned) but not for android. I'll just stick to unresponsive touchable text I suppose. It's a shame but it is what it is.

After hours of war, I found a workaround to fix the pixels that RN add to TouchableOpacity element when you wrap it within a Text. The key is to add a View element within your TouchableOpacity and wrap all the element within that View and add marginBottom to negative value to align it with text as shown in the following example:

<Text style={styles.modalText}>
  You can suggest some questions along with answers by send it to us in
  <TouchableOpacity onPress={() => console.log("go to contact")}>
    <View
      style={{
        flexDirection: "row",
        alignItems: "center",
        marginHorizontal: 6,
        marginBottom: -4, // <-- This one will take care of the pixels that RN add to TouchableOpacity when you wrap it within Text
        }}
      >
        <ContactIcon width={12} height={12} fill="#08A3FD" />
        <Text
          numberOfLines={1}
          style={{
          fontWeight: "bold",
          marginLeft: 4,
          fontSize: 14,
          color: "#08A3FD",
          }}
        >
          contact
        </Text>
      </View>
    </TouchableOpacity>
    section
</Text>

Output :
312442230_386446743607075_1017777519541700690_n

good luck <3

@github-actions
Copy link

This issue is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 7 days.

@github-actions github-actions bot added the Stale There has been a lack of activity on this issue and it may be closed soon. label Apr 22, 2023
@whaddafish
Copy link

Issue still exists

@github-actions github-actions bot removed the Stale There has been a lack of activity on this issue and it may be closed soon. label Apr 23, 2023
@pfcodes
Copy link

pfcodes commented Apr 27, 2023

Anybody figure out a good workaround?

@cortinico cortinico added the Never gets stale Prevent those issues and PRs from getting stale label Apr 27, 2023
@ba9nist
Copy link

ba9nist commented Jun 1, 2023

just import TouchableHighlight from 'react-native' package and not from 'react-native-gesture-handler'

@alainib
Copy link

alainib commented Aug 7, 2023

still bugging in 2023

@maulik54e
Copy link

maulik54e commented Dec 12, 2023

Still an issue if used with in parent Text numberOfLines & adjustsFontSizeToFit

@Hemistone
Copy link

How about using <Pressable/>, instead?

I had similar situation in iOS for making 'KeyboardDismissView' which enables to dismiss keyboard when user touches outer area of Input, but I've solved it by implementing <Pressable/>, not <TouchableWithoutFeedback/>

// Use <Pressable/> instead of <Touchable~~~/> Components
export default function KeyboardDismissView(props: KeyboardDismissViewProps) {
  const { children, style } = props;
  return (
    <Pressable onPress={Keyboard.dismiss} accessible={false} style={{ ...style, flexGrow: 1 }}>
      {children}
    </Pressable>
  );
};

export default function InputWithButtonScreen () {
  return(
    <KeyboardDismissView>
      <TextInput/>
      <Button onPress={()=>{console.log('1')}}>Hello</Button> // <-- Now Press Event gets in this Button, after using <Pressable/>
    </KeyboardDismissView>
  );
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug Component: TouchableOpacity Never gets stale Prevent those issues and PRs from getting stale
Projects
None yet
Development

No branches or pull requests