Browse Source

Overlay nested touchables (#6234)

* initial commit

* Fix nested touchables in Overlay

When nesting Touchable components in an Overlay, under certain conditions, the outer most Touchable component consumed all touch events.

Fixes #5184

Co-authored-by: Yogev Ben David <yogev132@gmail.com>
Guy Carmeli 4 years ago
parent
commit
851703c0ca
No account linked to committer's email address

+ 11
- 0
e2e/Overlay.test.js View File

@@ -35,6 +35,17 @@ describe('Overlay', () => {
35 35
     await expect(elementById(TestIDs.OVERLAY_ALERT_HEADER)).toBeVisible();
36 36
   });
37 37
 
38
+  fit('nested touchables work as expected', async () => {
39
+    await elementById(TestIDs.TOAST_BTN).tap();
40
+    await elementById(TestIDs.TOAST_OK_BTN_INNER).tap();
41
+    await expect(elementByLabel('Inner button clicked')).toBeVisible();
42
+    await elementById(TestIDs.OK_BUTTON).tap();
43
+
44
+    await elementById(TestIDs.TOAST_BTN).tap();
45
+    await elementById(TestIDs.TOAST_OK_BTN_OUTER).tap();
46
+    await expect(elementByLabel('Outer button clicked')).toBeVisible();
47
+  });
48
+
38 49
   xtest('overlay pass touches - false', async () => {
39 50
     await elementById(TestIDs.SHOW_OVERLAY_BUTTON).tap();
40 51
     await expect(elementById(TestIDs.SHOW_OVERLAY_BUTTON)).toBeVisible();

+ 2
- 2
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/IReactView.java View File

@@ -1,7 +1,7 @@
1 1
 package com.reactnativenavigation.viewcontrollers;
2 2
 
3 3
 import android.view.MotionEvent;
4
-import android.view.View;
4
+import android.view.ViewGroup;
5 5
 
6 6
 import com.reactnativenavigation.interfaces.ScrollEventListener;
7 7
 
@@ -9,7 +9,7 @@ public interface IReactView extends Destroyable {
9 9
 
10 10
     boolean isReady();
11 11
 
12
-    View asView();
12
+    ViewGroup asView();
13 13
 
14 14
     void sendOnNavigationButtonPressed(String buttonId);
15 15
 

+ 7
- 3
lib/android/app/src/main/java/com/reactnativenavigation/views/ComponentLayout.java View File

@@ -3,7 +3,7 @@ package com.reactnativenavigation.views;
3 3
 import android.annotation.SuppressLint;
4 4
 import android.content.Context;
5 5
 import android.view.MotionEvent;
6
-import android.view.View;
6
+import android.view.ViewGroup;
7 7
 
8 8
 import com.reactnativenavigation.interfaces.ScrollEventListener;
9 9
 import com.reactnativenavigation.parse.Options;
@@ -27,7 +27,7 @@ public class ComponentLayout extends CoordinatorLayout implements ReactComponent
27 27
 		super(context);
28 28
 		this.reactView = reactView;
29 29
         addView(reactView.asView(), matchParentLP());
30
-        touchDelegate = new OverlayTouchDelegate(reactView);
30
+        touchDelegate = new OverlayTouchDelegate(this, reactView);
31 31
     }
32 32
 
33 33
     @Override
@@ -36,7 +36,7 @@ public class ComponentLayout extends CoordinatorLayout implements ReactComponent
36 36
     }
37 37
 
38 38
     @Override
39
-    public View asView() {
39
+    public ViewGroup asView() {
40 40
         return this;
41 41
     }
42 42
 
@@ -94,4 +94,8 @@ public class ComponentLayout extends CoordinatorLayout implements ReactComponent
94 94
     public boolean onInterceptTouchEvent(MotionEvent ev) {
95 95
         return touchDelegate.onInterceptTouchEvent(ev);
96 96
     }
97
+
98
+    public boolean superOnInterceptTouchEvent(MotionEvent event) {
99
+        return super.onInterceptTouchEvent(event);
100
+    }
97 101
 }

+ 17
- 27
lib/android/app/src/main/java/com/reactnativenavigation/views/touch/OverlayTouchDelegate.java View File

@@ -1,59 +1,49 @@
1 1
 package com.reactnativenavigation.views.touch;
2 2
 
3 3
 import android.graphics.Rect;
4
-import androidx.annotation.VisibleForTesting;
5 4
 import android.view.MotionEvent;
6 5
 import android.view.View;
7
-import android.view.ViewGroup;
8 6
 
9 7
 import com.reactnativenavigation.parse.params.Bool;
10 8
 import com.reactnativenavigation.parse.params.NullBool;
11 9
 import com.reactnativenavigation.viewcontrollers.IReactView;
10
+import com.reactnativenavigation.views.ComponentLayout;
11
+
12
+import androidx.annotation.VisibleForTesting;
12 13
 
13 14
 public class OverlayTouchDelegate {
14
-    private enum TouchLocation {Outside, Inside}
15 15
     private final Rect hitRect = new Rect();
16
-    private IReactView reactView;
17 16
     private Bool interceptTouchOutside = new NullBool();
17
+    private ComponentLayout component;
18
+    private IReactView reactView;
18 19
 
19 20
     public void setInterceptTouchOutside(Bool interceptTouchOutside) {
20 21
         this.interceptTouchOutside = interceptTouchOutside;
21 22
     }
22 23
 
23
-    public OverlayTouchDelegate(IReactView reactView) {
24
+    public OverlayTouchDelegate(ComponentLayout component, IReactView reactView) {
25
+        this.component = component;
24 26
         this.reactView = reactView;
25 27
     }
26 28
 
27 29
     public boolean onInterceptTouchEvent(MotionEvent event) {
28
-        if (interceptTouchOutside instanceof NullBool) return false;
29
-        switch (event.getActionMasked()) {
30
-            case MotionEvent.ACTION_DOWN:
31
-                return handleDown(event);
32
-            default:
33
-                reactView.dispatchTouchEventToJs(event);
34
-                return false;
35
-
36
-        }
30
+        return interceptTouchOutside.hasValue() && event.getActionMasked() == MotionEvent.ACTION_DOWN ?
31
+                handleDown(event) :
32
+                component.superOnInterceptTouchEvent(event);
37 33
     }
38 34
 
39 35
     @VisibleForTesting
40 36
     public boolean handleDown(MotionEvent event) {
41
-        TouchLocation location = getTouchLocation(event);
42
-        if (location == TouchLocation.Inside) {
43
-            reactView.dispatchTouchEventToJs(event);
44
-            return false;
45
-        }
46
-        return interceptTouchOutside.isFalseOrUndefined();
37
+        if (isTouchInsideOverlay(event)) return component.superOnInterceptTouchEvent(event);
38
+        return interceptTouchOutside.isFalse();
47 39
     }
48 40
 
49
-    private TouchLocation getTouchLocation(MotionEvent ev) {
50
-        getView((ViewGroup) reactView.asView()).getHitRect(hitRect);
51
-        return hitRect.contains((int) ev.getRawX(), (int) ev.getRawY()) ?
52
-                TouchLocation.Inside :
53
-                TouchLocation.Outside;
41
+    private boolean isTouchInsideOverlay(MotionEvent ev) {
42
+        getOverlayView().getHitRect(hitRect);
43
+        return hitRect.contains((int) ev.getRawX(), (int) ev.getRawY());
54 44
     }
55 45
 
56
-    private View getView(ViewGroup view) {
57
-        return view.getChildCount() > 0 ? view.getChildAt(0) : view;
46
+    private View getOverlayView() {
47
+        return reactView.asView().getChildCount() > 0 ? reactView.asView().getChildAt(0) : reactView.asView();
58 48
     }
59 49
 }

+ 2
- 2
lib/android/app/src/test/java/com/reactnativenavigation/mocks/SimpleOverlay.java View File

@@ -2,7 +2,7 @@ package com.reactnativenavigation.mocks;
2 2
 
3 3
 import android.content.Context;
4 4
 import android.view.MotionEvent;
5
-import android.view.View;
5
+import android.view.ViewGroup;
6 6
 import android.widget.FrameLayout;
7 7
 import android.widget.RelativeLayout;
8 8
 
@@ -20,7 +20,7 @@ public class SimpleOverlay extends RelativeLayout implements IReactView {
20 20
     }
21 21
 
22 22
     @Override
23
-    public View asView() {
23
+    public ViewGroup asView() {
24 24
         FrameLayout root = new FrameLayout(getContext());
25 25
         FrameLayout overlay = new FrameLayout(getContext());
26 26
         root.addView(overlay);

+ 2
- 2
lib/android/app/src/test/java/com/reactnativenavigation/mocks/TestComponentLayout.java View File

@@ -2,7 +2,7 @@ package com.reactnativenavigation.mocks;
2 2
 
3 3
 import android.content.Context;
4 4
 import android.view.MotionEvent;
5
-import android.view.View;
5
+import android.view.ViewGroup;
6 6
 
7 7
 import com.reactnativenavigation.interfaces.ScrollEventListener;
8 8
 import com.reactnativenavigation.parse.Options;
@@ -26,7 +26,7 @@ public class TestComponentLayout extends ComponentLayout implements TitleBarButt
26 26
     }
27 27
 
28 28
     @Override
29
-    public View asView() {
29
+    public ViewGroup asView() {
30 30
         return this;
31 31
     }
32 32
 

+ 6
- 3
lib/android/app/src/test/java/com/reactnativenavigation/views/TouchDelegateTest.java View File

@@ -8,6 +8,7 @@ import com.reactnativenavigation.parse.params.Bool;
8 8
 import com.reactnativenavigation.views.touch.OverlayTouchDelegate;
9 9
 
10 10
 import org.junit.Test;
11
+import org.mockito.Mockito;
11 12
 
12 13
 import static org.assertj.core.api.Java6Assertions.assertThat;
13 14
 import static org.mockito.Mockito.spy;
@@ -21,12 +22,14 @@ public class TouchDelegateTest extends BaseTest {
21 22
     private final MotionEvent downEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, x, y, 0);
22 23
     private final MotionEvent upEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, x, y, 0);
23 24
     private SimpleOverlay reactView;
25
+    private ComponentLayout component;
24 26
 
25 27
     @Override
26 28
     public void beforeEach() {
27 29
         super.beforeEach();
28 30
         reactView = spy(new SimpleOverlay(newActivity()));
29
-        uut = spy(new OverlayTouchDelegate(reactView));
31
+        component = Mockito.mock(ComponentLayout.class);
32
+        uut = spy(new OverlayTouchDelegate(component, reactView));
30 33
     }
31 34
 
32 35
     @Test
@@ -50,9 +53,9 @@ public class TouchDelegateTest extends BaseTest {
50 53
     }
51 54
 
52 55
     @Test
53
-    public void nonDownEventsDispatchTouchEventsToJs() {
56
+    public void nonDownEventsInvokeSuperImplementation() {
54 57
         uut.setInterceptTouchOutside(new Bool(true));
55 58
         uut.onInterceptTouchEvent(upEvent);
56
-        verify(reactView, times(1)).dispatchTouchEventToJs(upEvent);
59
+        verify(component).superOnInterceptTouchEvent(upEvent);
57 60
     }
58 61
 }

+ 0
- 1
playground/src/screens/OptionsScreen.js View File

@@ -1,6 +1,5 @@
1 1
 const React = require('react');
2 2
 const {Component} = require('react');
3
-import {Platform} from 'react-native';
4 3
 const Root = require('../components/Root');
5 4
 const Button = require('../components/Button')
6 5
 const Navigation = require('../services/Navigation');

+ 5
- 1
playground/src/screens/OverlayScreen.js View File

@@ -7,7 +7,8 @@ const {
7 7
   SHOW_OVERLAY_BTN,
8 8
   SHOW_TOUCH_THROUGH_OVERLAY_BTN,
9 9
   ALERT_BUTTON,
10
-  SET_ROOT_BTN
10
+  SET_ROOT_BTN,
11
+  TOAST_BTN
11 12
 } = require('../testIDs');
12 13
 const Screens = require('./Screens');
13 14
 
@@ -25,6 +26,7 @@ class OverlayScreen extends React.Component {
25 26
   render() {
26 27
     return (
27 28
       <Root componentId={this.props.componentId}>
29
+        <Button label='Toast' testID={TOAST_BTN} onPress={this.toast} />
28 30
         <Button label='Alert' testID={ALERT_BUTTON} onPress={() => alert('Alert displayed')} />
29 31
         <Button label='Show overlay' testID={SHOW_OVERLAY_BTN} onPress={() => this.showOverlay(true)} />
30 32
         <Button label='Show touch through overlay' testID={SHOW_TOUCH_THROUGH_OVERLAY_BTN} onPress={() => this.showOverlay(false)} />
@@ -34,6 +36,8 @@ class OverlayScreen extends React.Component {
34 36
     );
35 37
   }
36 38
 
39
+  toast = () => Navigation.showOverlay(Screens.Toast);
40
+
37 41
   showOverlay = (interceptTouchOutside) => Navigation.showOverlay(Screens.OverlayAlert, {
38 42
     layout: { componentBackgroundColor: 'transparent' },
39 43
     overlay: { interceptTouchOutside }

+ 9
- 2
playground/src/screens/ScrollViewOverlay.js View File

@@ -10,7 +10,14 @@ const colors = [
10 10
   '#D9644A',
11 11
   '#CF262F',
12 12
   '#8B1079',
13
-  '#48217B'
13
+  '#48217B',
14
+  '#8B1079',
15
+  '#CF262F',
16
+  '#D9644A',
17
+  '#E2902B',
18
+  '#00A65F',
19
+  '#00AAAF',
20
+  '#3182C8',
14 21
 ];
15 22
 
16 23
 class ScrollViewOverlay extends PureComponent {
@@ -27,7 +34,7 @@ class ScrollViewOverlay extends PureComponent {
27 34
     );
28 35
   }
29 36
 
30
-  renderRow = (color) => <Text key={color} style={[styles.row, { backgroundColor: color }]}>{color}</Text>
37
+  renderRow = (color) => <Text key={color + Math.random()} style={[styles.row, { backgroundColor: color }]}>{color}</Text>
31 38
 }
32 39
 
33 40
 const styles = StyleSheet.create({

+ 17
- 7
playground/src/screens/Toast.js View File

@@ -2,16 +2,27 @@ const React = require('react');
2 2
 const { View, Text, StyleSheet, TouchableOpacity } = require('react-native');
3 3
 const Colors = require('../commons/Colors');
4 4
 const Navigation = require('../services/Navigation');
5
+const {
6
+  TOAST_OK_BTN_INNER,
7
+  TOAST_OK_BTN_OUTER
8
+} = require('../testIDs');
5 9
 
6 10
 const Toast = function ({componentId}) {
11
+  const dismiss = (txt) => {
12
+    alert(txt)
13
+    Navigation.dismissOverlay(componentId);
14
+  }
15
+
7 16
   return (
8 17
     <View style={styles.root}>
9
-      <View style={styles.toast}>
10
-        <Text style={styles.text}>This a very important message!</Text>
11
-        <TouchableOpacity style={styles.button} onPress={() => Navigation.dismissOverlay(componentId)}>
12
-          <Text style={styles.buttonText}>OK</Text>
13
-        </TouchableOpacity>
14
-      </View>
18
+      <TouchableOpacity testID={TOAST_OK_BTN_OUTER} onPress={() => dismiss('Outer button clicked')}>
19
+        <View style={styles.toast}>
20
+          <Text style={styles.text}>This a very important message!</Text>
21
+          <TouchableOpacity testID={TOAST_OK_BTN_INNER} style={styles.button} onPress={() => dismiss('Inner button clicked')}>
22
+            <Text style={styles.buttonText}>OK</Text>
23
+          </TouchableOpacity>
24
+        </View>
25
+      </TouchableOpacity>
15 26
     </View>
16 27
   )
17 28
 }
@@ -21,7 +32,6 @@ const styles = StyleSheet.create({
21 32
     flex: 1,
22 33
     flexDirection: 'column-reverse',
23 34
     backgroundColor: 0x3e434aa1
24
-    // backgroundColor: 'red'
25 35
   },
26 36
   toast: {
27 37
     elevation: 2,

+ 3
- 0
playground/src/testIDs.js View File

@@ -98,6 +98,9 @@ module.exports = {
98 98
   ADD_BUTTON: 'ADD_BUTTON',
99 99
   TAB_BASED_APP_BUTTON: `TAB_BASED_APP_BUTTON`,
100 100
   TAB_BASED_APP_SIDE_BUTTON: `TAB_BASED_APP_SIDE_BUTTON`,
101
+  TOAST_BTN: 'TOAST_BTN',
102
+  TOAST_OK_BTN_OUTER: 'TOAST_OK_BTN_OUTER',
103
+  TOAST_OK_BTN_INNER: 'TOAST_OK_BTN_INNER',
101 104
   PUSH_STATIC_LIFECYCLE_BUTTON: `PUSH_STATIC_LIFECYCLE_BUTTON`,
102 105
   PUSH_CONTEXT_SCREEN_BUTTON: `PUSH_CONTEXT_SCREEN_BUTTON`,
103 106
   PUSH_OPTIONS_BUTTON: `PUSH_OPTIONS_BUTTON`,