Skip to content

Commit

Permalink
Add Text component (#3202)
Browse files Browse the repository at this point in the history
## Description

This PR adds `Text` component to **Gesture Handler**. 

Upon investigating #3159 we decided that it will be better to add our own `Text` component, instead of forcing users to create their own version of `Text` with `NativeViewGestureHandler`.

## Test plan

<details>
<summary>New example:</summary>

```jsx
import { useState } from 'react';
import { StyleSheet } from 'react-native';
import {
  Text,
  GestureHandlerRootView,
  TouchableOpacity,
} from 'react-native-gesture-handler';

export default function NestedText() {
  const [counter, setCounter] = useState(0);

  return (
    <GestureHandlerRootView style={styles.container}>
      <Text style={{ fontSize: 30 }}>{`Counter: ${counter}`}</Text>

      <TouchableOpacity
        onPress={() => {
          console.log('Touchable');
          setCounter((prev) => prev + 1);
        }}>
        <Text
          style={[styles.textCommon, styles.outerText]}
          onPress={() => {
            console.log('Outer text');
            setCounter((prev) => prev + 1);
          }}>
          {'Outer Text '}
          <Text
            style={[styles.textCommon, styles.innerText]}
            onPress={() => {
              console.log('Nested text');
              setCounter((prev) => prev + 1);
            }}>
            {'Nested Text'}
          </Text>
        </Text>
      </TouchableOpacity>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',

    gap: 20,
  },

  textCommon: {
    padding: 10,
    color: 'white',
  },

  outerText: {
    fontSize: 30,
    borderWidth: 2,
    backgroundColor: '#131313',
  },

  innerText: {
    fontSize: 25,
    backgroundColor: '#F06312',
  },
});


```

</details>
  • Loading branch information
m-bert authored Dec 12, 2024
1 parent 466d4e5 commit 6a7a128
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.view.ViewGroup
import android.widget.ScrollView
import com.facebook.react.views.scroll.ReactScrollView
import com.facebook.react.views.swiperefresh.ReactSwipeRefreshLayout
import com.facebook.react.views.text.ReactTextView
import com.facebook.react.views.textinput.ReactEditText
import com.facebook.react.views.view.ReactViewGroup
import com.swmansion.gesturehandler.react.RNGestureHandlerButtonViewManager
Expand Down Expand Up @@ -45,7 +46,11 @@ class NativeViewGestureHandler : GestureHandler<NativeViewGestureHandler>() {

override fun shouldRecognizeSimultaneously(handler: GestureHandler<*>): Boolean {
// if the gesture is marked by user as simultaneous with other or the hook return true
if (super.shouldRecognizeSimultaneously(handler) || hook.shouldRecognizeSimultaneously(handler)) {
hook.shouldRecognizeSimultaneously(handler)?.let {
return@shouldRecognizeSimultaneously it
}

if (super.shouldRecognizeSimultaneously(handler)) {
return true
}

Expand Down Expand Up @@ -80,6 +85,7 @@ class NativeViewGestureHandler : GestureHandler<NativeViewGestureHandler>() {
is ReactEditText -> this.hook = EditTextHook(this, view)
is ReactSwipeRefreshLayout -> this.hook = SwipeRefreshLayoutHook(this, view)
is ReactScrollView -> this.hook = ScrollViewHook()
is ReactTextView -> this.hook = TextViewHook()
is ReactViewGroup -> this.hook = ReactViewGroupHook()
}
}
Expand All @@ -102,7 +108,8 @@ class NativeViewGestureHandler : GestureHandler<NativeViewGestureHandler>() {
cancel()
} else {
hook.sendTouchEvent(view, event)
if ((state == STATE_UNDETERMINED || state == STATE_BEGAN) && view.isPressed) {

if ((state == STATE_UNDETERMINED || state == STATE_BEGAN) && hook.canActivate(view)) {
activate()
}

Expand Down Expand Up @@ -172,6 +179,11 @@ class NativeViewGestureHandler : GestureHandler<NativeViewGestureHandler>() {
*/
fun canBegin(event: MotionEvent) = true

/**
* Checks whether handler can activate. Used by TextViewHook.
*/
fun canActivate(view: View) = view.isPressed

/**
* Called after the gesture transitions to the END state.
*/
Expand All @@ -181,7 +193,7 @@ class NativeViewGestureHandler : GestureHandler<NativeViewGestureHandler>() {
* @return Boolean value signalling whether the gesture can be recognized simultaneously with
* other (handler). Returning false doesn't necessarily prevent it from happening.
*/
fun shouldRecognizeSimultaneously(handler: GestureHandler<*>) = false
fun shouldRecognizeSimultaneously(handler: GestureHandler<*>): Boolean? = null

/**
* shouldActivateOnStart and tryIntercept have priority over this method
Expand All @@ -208,6 +220,14 @@ class NativeViewGestureHandler : GestureHandler<NativeViewGestureHandler>() {
fun sendTouchEvent(view: View?, event: MotionEvent) = view?.onTouchEvent(event)
}

private class TextViewHook() : NativeViewGestureHandlerHook {
override fun shouldRecognizeSimultaneously(handler: GestureHandler<*>) = false

// We have to explicitly check for ReactTextView, since its `isPressed` flag is not set to `true`,
// in contrast to e.g. Touchable
override fun canActivate(view: View) = view is ReactTextView
}

private class EditTextHook(
private val handler: NativeViewGestureHandler,
private val editText: ReactEditText
Expand Down
6 changes: 6 additions & 0 deletions example/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import PointerType from './src/release_tests/pointerType';
import SwipeableReanimation from './src/release_tests/swipeableReanimation';
import NestedGestureHandlerRootViewWithModal from './src/release_tests/nestedGHRootViewWithModal';
import TwoFingerPan from './src/release_tests/twoFingerPan';
import NestedText from './src/release_tests/nestedText';
import { PinchableBox } from './src/recipes/scaleAndRotate';
import PanAndScroll from './src/recipes/panAndScroll';
import { BottomSheet } from './src/showcase/bottomSheet';
Expand Down Expand Up @@ -217,6 +218,11 @@ const EXAMPLES: ExamplesSection[] = [
component: TwoFingerPan,
unsupportedPlatforms: new Set(['android', 'macos']),
},
{
name: 'Nested Text',
component: NestedText,
unsupportedPlatforms: new Set(['macos']),
},
],
},
{
Expand Down
66 changes: 66 additions & 0 deletions example/src/release_tests/nestedText/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useState } from 'react';
import { StyleSheet } from 'react-native';
import {
Text,
GestureHandlerRootView,
TouchableOpacity,
} from 'react-native-gesture-handler';

export default function NestedText() {
const [counter, setCounter] = useState(0);

return (
<GestureHandlerRootView style={styles.container}>
<Text style={{ fontSize: 30 }}>{`Counter: ${counter}`}</Text>

<TouchableOpacity
onPress={() => {
console.log('Touchable');
setCounter((prev) => prev + 1);
}}>
<Text
style={[styles.textCommon, styles.outerText]}
onPress={() => {
console.log('Outer text');
setCounter((prev) => prev + 1);
}}>
{'Outer Text '}
<Text
style={[styles.textCommon, styles.innerText]}
onPress={() => {
console.log('Nested text');
setCounter((prev) => prev + 1);
}}>
{'Nested Text'}
</Text>
</Text>
</TouchableOpacity>
</GestureHandlerRootView>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',

gap: 20,
},

textCommon: {
padding: 10,
color: 'white',
},

outerText: {
fontSize: 30,
borderWidth: 2,
backgroundColor: '#131313',
},

innerText: {
fontSize: 25,
backgroundColor: '#F06312',
},
});
60 changes: 60 additions & 0 deletions src/components/Text.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, {
ForwardedRef,
forwardRef,
RefObject,
useEffect,
useRef,
} from 'react';
import {
Platform,
Text as RNText,
TextProps as RNTextProps,
} from 'react-native';

import { Gesture, GestureDetector } from '../';

export const Text = forwardRef(
(props: RNTextProps, ref: ForwardedRef<RNText>) => {
const { onPress, ...rest } = props;
const textRef = useRef<RNText | null>(null);
const native = Gesture.Native().runOnJS(true);

const refHandler = (node: any) => {
textRef.current = node;

if (ref === null) {
return;
}

if (typeof ref === 'function') {
ref(node);
} else {
ref.current = node;
}
};

useEffect(() => {
if (Platform.OS !== 'web') {
return;
}

const textElement = ref
? (ref as RefObject<RNText>).current
: textRef.current;

// At this point we are sure that textElement is div in HTML tree
(textElement as unknown as HTMLDivElement)?.setAttribute(
'rnghtext',
'true'
);
}, []);

return (
<GestureDetector gesture={native}>
<RNText onPress={onPress} ref={refHandler} {...rest} />
</GestureDetector>
);
}
);
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type Text = typeof Text & RNText;
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export {
FlatList,
RefreshControl,
} from './components/GestureComponents';
export { Text } from './components/Text';
export { HoverEffect } from './handlers/gestures/hoverGesture';
export type {
// Events
Expand Down
6 changes: 5 additions & 1 deletion src/web/handlers/NativeViewGestureHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ export default class NativeViewGestureHandler extends GestureHandler {
}

this.begin();
if (this.buttonRole) {

const view = this.delegate.getView() as HTMLElement;
const isRNGHText = view.hasAttribute('rnghtext');

if (this.buttonRole || isRNGHText) {
this.activate();
}
}
Expand Down

0 comments on commit 6a7a128

Please sign in to comment.