diff --git a/.gitignore b/.gitignore
index 453d0e1d0..2a4cd9736 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,5 @@ node_modules
.idea
coverage
lib
+.yarn
+.yarnrc.yml
diff --git a/src/Toast.tsx b/src/Toast.tsx
index a151a550d..38fa98c2e 100644
--- a/src/Toast.tsx
+++ b/src/Toast.tsx
@@ -1,6 +1,6 @@
import React from 'react';
-import { LoggerProvider } from './contexts';
+import { LoggerProvider, GestureProvider } from './contexts';
import { ToastUI } from './ToastUI';
import {
ToastHideParams,
@@ -88,7 +88,9 @@ export function Toast(props: ToastProps) {
return (
-
+
+
+
);
}
diff --git a/src/__helpers__/PanResponder.ts b/src/__helpers__/PanResponder.ts
index fcb6b68d1..a9b7d918b 100644
--- a/src/__helpers__/PanResponder.ts
+++ b/src/__helpers__/PanResponder.ts
@@ -24,12 +24,16 @@ export function mockPanResponder() {
.spyOn(PanResponder, 'create')
.mockImplementation(
({
+ onStartShouldSetPanResponder,
+ onPanResponderGrant,
onMoveShouldSetPanResponder,
onMoveShouldSetPanResponderCapture,
onPanResponderMove,
onPanResponderRelease
}: PanResponderCallbacks) => ({
panHandlers: {
+ onStartShouldSetResponder: onStartShouldSetPanResponder,
+ onResponderGrant: onPanResponderGrant,
onMoveShouldSetResponder: onMoveShouldSetPanResponder,
onMoveShouldSetResponderCapture: onMoveShouldSetPanResponderCapture,
onResponderMove: onPanResponderMove,
diff --git a/src/__tests__/Toast.test.tsx b/src/__tests__/Toast.test.tsx
index f91b1dd62..9113a105d 100644
--- a/src/__tests__/Toast.test.tsx
+++ b/src/__tests__/Toast.test.tsx
@@ -7,14 +7,14 @@ import { Button, Modal, Text } from 'react-native';
import { Toast } from '../Toast';
/*
- The Modal component is automatically mocked by RN and apparently contains a bug which makes the Modal
+ The Modal component is automatically mocked by RN and apparently contains a bug which makes the Modal
(and its children) to always be visible in the test tree.
This fixes the issue:
*/
jest.mock('react-native/Libraries/Modal/Modal', () => {
const ActualModal = jest.requireActual('react-native/Libraries/Modal/Modal');
- return (props) => ;
+ return (props: any) => ;
});
jest.mock('react-native/Libraries/LogBox/LogBox');
@@ -84,7 +84,7 @@ describe('test Toast component', () => {
// Show the Modal
const showModalButton = utils.queryByText('Show modal');
expect(showModalButton).toBeTruthy();
- fireEvent.press(showModalButton);
+ fireEvent.press(showModalButton as any);
await waitFor(() => {
expect(utils.queryByText('Inside modal')).toBeTruthy();
});
@@ -104,7 +104,7 @@ describe('test Toast component', () => {
// Hide modal
const hideModalButton = utils.queryByText('Hide modal');
expect(hideModalButton).toBeTruthy();
- fireEvent.press(hideModalButton);
+ fireEvent.press(hideModalButton as any);
await waitFor(() => {
expect(utils.queryByText('Inside modal')).toBeFalsy();
});
diff --git a/src/__tests__/useToast.test.ts b/src/__tests__/useToast.test.tsx
similarity index 81%
rename from src/__tests__/useToast.test.ts
rename to src/__tests__/useToast.test.tsx
index 4227189b3..5f47270af 100644
--- a/src/__tests__/useToast.test.ts
+++ b/src/__tests__/useToast.test.tsx
@@ -1,21 +1,30 @@
/* eslint-env jest */
import { act, renderHook } from '@testing-library/react-hooks';
+import React from 'react';
import { ToastOptions } from '../types';
import { DEFAULT_DATA, DEFAULT_OPTIONS, useToast } from '../useToast';
+import { GestureProvider } from '../contexts';
-const setup = () => {
+const setupGestureWrapper = (panning: boolean) => {
+ return ({ children }: { children: React.ReactNode }) => (
+ {children}
+ );
+};
+
+const setup = (panning = false) => {
+ const wrapper = setupGestureWrapper(panning)
const utils = renderHook(() =>
- useToast({
- defaultOptions: DEFAULT_OPTIONS
- })
+ useToast({ defaultOptions: DEFAULT_OPTIONS }),
+ { wrapper }
);
return {
...utils
};
};
+
describe('test useToast hook', () => {
it('returns defaults', () => {
const { result } = setup();
@@ -167,6 +176,29 @@ describe('test useToast hook', () => {
expect(onHide).toHaveBeenCalled();
});
+ it('does not hide when autoHide is true but user is panning', () => {
+ jest.useFakeTimers();
+ const { result } = setup(true);
+ const onHide = jest.fn();
+ act(() => {
+ result.current.show({
+ text1: 'test',
+ autoHide: true,
+ onHide
+ });
+ });
+
+ expect(result.current.isVisible).toBe(true);
+
+ act(() => {
+ jest.runAllTimers();
+ });
+
+ expect(result.current.isVisible).toBe(true);
+ expect(onHide).not.toHaveBeenCalled();
+ });
+
+
it('shows using only text2', () => {
const { result } = setup();
diff --git a/src/components/AnimatedContainer.tsx b/src/components/AnimatedContainer.tsx
index e12f2c415..3f7bac4a0 100644
--- a/src/components/AnimatedContainer.tsx
+++ b/src/components/AnimatedContainer.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import { Animated, Dimensions, PanResponderGestureState } from 'react-native';
-import { useLogger } from '../contexts';
+import { useLogger, useGesture } from '../contexts';
import {
usePanResponder,
useSlideAnimation,
@@ -81,6 +81,7 @@ export function AnimatedContainer({
swipeable
}: AnimatedContainerProps) {
const { log } = useLogger();
+ const { panning } = useGesture();
const { computeViewDimensions, height } = useViewDimensions();
@@ -93,6 +94,18 @@ export function AnimatedContainer({
avoidKeyboard
});
+ const disable = !swipeable || !isVisible;
+
+ const onStart = React.useCallback(() => {
+ log('Swipe, pan start');
+ panning.current = true;
+ }, [log, panning]);
+
+ const onEnd = React.useCallback(() => {
+ log('Swipe, pan end');
+ panning.current = false;
+ }, [log, panning]);
+
const onDismiss = React.useCallback(() => {
log('Swipe, dismissing');
animate(0);
@@ -118,7 +131,9 @@ export function AnimatedContainer({
computeNewAnimatedValueForGesture,
onDismiss,
onRestore,
- disable: !swipeable
+ onStart,
+ onEnd,
+ disable,
});
React.useLayoutEffect(() => {
@@ -133,7 +148,7 @@ export function AnimatedContainer({
style={[styles.base, styles[position], animationStyles]}
// This container View is never the target of touch events but its subviews can be.
// By doing this, tapping buttons behind the Toast is allowed
- pointerEvents={isVisible ? 'box-none' : 'none'}
+ pointerEvents='box-none'
{...panResponder.panHandlers}>
{children}
diff --git a/src/components/__tests__/AnimatedContainer.test.tsx b/src/components/__tests__/AnimatedContainer.test.tsx
index 5bca48dc1..e956682ee 100644
--- a/src/components/__tests__/AnimatedContainer.test.tsx
+++ b/src/components/__tests__/AnimatedContainer.test.tsx
@@ -30,6 +30,7 @@ const setup = (props?: Omit, 'children'>) => {
topOffset: 40,
bottomOffset: 40,
keyboardOffset: 10,
+ avoidKeyboard: true,
onHide
};
@@ -95,6 +96,7 @@ describe('test AnimatedContainer component', () => {
moveY: 100,
dy: 10
};
+ panHandler?.props.onResponderGrant();
panHandler?.props.onResponderMove(undefined, gesture);
panHandler?.props.onResponderRelease(undefined, gesture);
expect(onRestorePosition).toHaveBeenCalled();
@@ -119,6 +121,7 @@ describe('test AnimatedContainer component', () => {
moveY: 5,
dy: -78
};
+ panHandler?.props.onResponderGrant();
panHandler?.props.onResponderMove(undefined, gesture);
panHandler?.props.onResponderRelease(undefined, gesture);
expect(onHide).toHaveBeenCalled();
diff --git a/src/contexts/GestureContext.tsx b/src/contexts/GestureContext.tsx
new file mode 100644
index 000000000..0172dad81
--- /dev/null
+++ b/src/contexts/GestureContext.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+
+import { ReactChildren } from '../types';
+
+export type GestureContextType = {
+ panning: React.MutableRefObject;
+};
+
+export type GestureProviderProps = {
+ children: ReactChildren;
+ panning?: boolean;
+};
+
+const GestureContext = React.createContext({
+ panning: { current: false }
+});
+
+function GestureProvider({ children, panning = false }: GestureProviderProps) {
+ const panningRef = React.useRef(panning);
+ const value = { panning: panningRef };
+ return (
+ {children}
+ );
+}
+
+function useGesture() {
+ const ctx = React.useContext(GestureContext);
+ return ctx;
+}
+
+export { GestureProvider, useGesture };
diff --git a/src/contexts/__tests__/GestureContext.test.tsx b/src/contexts/__tests__/GestureContext.test.tsx
new file mode 100644
index 000000000..5d9630749
--- /dev/null
+++ b/src/contexts/__tests__/GestureContext.test.tsx
@@ -0,0 +1,32 @@
+/* eslint-env jest */
+
+import { renderHook, act } from '@testing-library/react-hooks';
+import React from 'react';
+
+import { ReactChildren } from '../../types';
+import { GestureProvider, useGesture } from '../GestureContext';
+import { GestureProviderProps } from '..';
+
+const setup = (props?: Omit) => {
+ const wrapper = ({ children }: { children: ReactChildren }) => (
+ {children}
+ );
+ const utils = renderHook(() => useGesture(), { wrapper });
+ return { ...utils };
+};
+
+describe('GestureContext', () => {
+ it('provides a panning ref with current defaulting to false', () => {
+ const { result } = setup();
+ expect(result.current.panning).toBeDefined();
+ expect(result.current.panning.current).toBe(false);
+ });
+
+ it('allows updating the panning ref value', () => {
+ const { result } = setup();
+ act(() => {
+ result.current.panning.current = true
+ });
+ expect(result.current.panning.current).toBe(true);
+ });
+});
diff --git a/src/contexts/__tests__/LoggerContext.test.tsx b/src/contexts/__tests__/LoggerContext.test.tsx
index 5170f9c65..4cef88b71 100644
--- a/src/contexts/__tests__/LoggerContext.test.tsx
+++ b/src/contexts/__tests__/LoggerContext.test.tsx
@@ -11,12 +11,8 @@ const setup = (props?: Omit) => {
const wrapper = ({ children }: { children: ReactChildren }) => (
{children}
);
- const utils = renderHook(useLogger, {
- wrapper
- });
- return {
- ...utils
- };
+ const utils = renderHook(useLogger, { wrapper });
+ return { ...utils };
};
describe('test Logger context', () => {
diff --git a/src/contexts/index.ts b/src/contexts/index.ts
index 0b66bf77c..115567318 100644
--- a/src/contexts/index.ts
+++ b/src/contexts/index.ts
@@ -1 +1,2 @@
export * from './LoggerContext';
+export * from './GestureContext';
diff --git a/src/hooks/__tests__/usePanResponder.test.ts b/src/hooks/__tests__/usePanResponder.test.ts
index 6ed6ddddc..169651d46 100644
--- a/src/hooks/__tests__/usePanResponder.test.ts
+++ b/src/hooks/__tests__/usePanResponder.test.ts
@@ -5,7 +5,7 @@ import { Animated, GestureResponderEvent } from 'react-native';
import { mockGestureValues } from '../../__helpers__/PanResponder';
import { usePanResponder } from '../usePanResponder';
-import { shouldSetPanResponder } from '..';
+import { moveShouldSetPanResponder, startShouldSetPanResponder } from '..';
const setup = ({ newAnimatedValueForGesture = 0, disable = false } = {}) => {
const animatedValue = {
@@ -16,6 +16,8 @@ const setup = ({ newAnimatedValueForGesture = 0, disable = false } = {}) => {
);
const onDismiss = jest.fn();
const onRestore = jest.fn();
+ const onStart = jest.fn();
+ const onEnd = jest.fn();
const utils = renderHook(() =>
usePanResponder({
@@ -23,6 +25,8 @@ const setup = ({ newAnimatedValueForGesture = 0, disable = false } = {}) => {
computeNewAnimatedValueForGesture,
onDismiss,
onRestore,
+ onStart,
+ onEnd,
disable
})
);
@@ -39,6 +43,7 @@ describe('test usePanResponder hook', () => {
it('returns defaults', () => {
const { result } = setup();
expect(result.current.panResponder.panHandlers).toBeDefined();
+ expect(result.current.onGrant).toBeDefined();
expect(result.current.onMove).toBeDefined();
expect(result.current.onRelease).toBeDefined();
});
@@ -47,6 +52,7 @@ describe('test usePanResponder hook', () => {
const { result, computeNewAnimatedValueForGesture } = setup({
newAnimatedValueForGesture: 1
});
+ result.current.onGrant();
result.current.onMove({} as GestureResponderEvent, mockGestureValues);
expect(computeNewAnimatedValueForGesture).toBeCalledWith(mockGestureValues);
});
@@ -65,6 +71,7 @@ describe('test usePanResponder hook', () => {
disable: true
});
+ result.current.onGrant();
result.current.onMove({} as GestureResponderEvent, mockGestureValues);
expect(computeNewAnimatedValueForGesture).not.toBeCalledWith(
mockGestureValues
@@ -110,13 +117,17 @@ describe('test usePanResponder hook', () => {
});
describe('test shouldSetPanResponder function', () => {
+ it('is set pan start always true', () => {
+ expect(startShouldSetPanResponder()).toBe(true);
+ });
+
it('is set when dx > offset', () => {
const gesture = {
...mockGestureValues,
dx: 2.1,
dy: 0
};
- expect(shouldSetPanResponder({} as GestureResponderEvent, gesture)).toBe(
+ expect(moveShouldSetPanResponder({} as GestureResponderEvent, gesture)).toBe(
true
);
});
@@ -127,7 +138,7 @@ describe('test shouldSetPanResponder function', () => {
dx: 0,
dy: 2.1
};
- expect(shouldSetPanResponder({} as GestureResponderEvent, gesture)).toBe(
+ expect(moveShouldSetPanResponder({} as GestureResponderEvent, gesture)).toBe(
true
);
});
@@ -138,7 +149,7 @@ describe('test shouldSetPanResponder function', () => {
dx: 2,
dy: 0
};
- expect(shouldSetPanResponder({} as GestureResponderEvent, gesture)).toBe(
+ expect(moveShouldSetPanResponder({} as GestureResponderEvent, gesture)).toBe(
false
);
});
@@ -149,7 +160,7 @@ describe('test shouldSetPanResponder function', () => {
dx: 0,
dy: 2
};
- expect(shouldSetPanResponder({} as GestureResponderEvent, gesture)).toBe(
+ expect(moveShouldSetPanResponder({} as GestureResponderEvent, gesture)).toBe(
false
);
});
diff --git a/src/hooks/__tests__/useViewDimensions.test.ts b/src/hooks/__tests__/useViewDimensions.test.ts
index b08ba3afb..953164ce8 100644
--- a/src/hooks/__tests__/useViewDimensions.test.ts
+++ b/src/hooks/__tests__/useViewDimensions.test.ts
@@ -7,7 +7,7 @@ import { act } from 'react-test-renderer';
import { useViewDimensions } from '../useViewDimensions';
import { UseViewDimensionsParams } from '..';
-const setup = (offsets: UseViewDimensionsParams) => {
+const setup = (offsets?: UseViewDimensionsParams) => {
const layoutChangeEventMock = {
nativeEvent: {
layout: {
diff --git a/src/hooks/usePanResponder.ts b/src/hooks/usePanResponder.ts
index 3e0260a14..7fdc11838 100644
--- a/src/hooks/usePanResponder.ts
+++ b/src/hooks/usePanResponder.ts
@@ -6,7 +6,11 @@ import {
PanResponderGestureState
} from 'react-native';
-export function shouldSetPanResponder(
+export function startShouldSetPanResponder() {
+ return true;
+}
+
+export function moveShouldSetPanResponder(
_event: GestureResponderEvent,
gesture: PanResponderGestureState
) {
@@ -36,6 +40,8 @@ export type UsePanResponderParams = {
) => number;
onDismiss: () => void;
onRestore: () => void;
+ onStart: () => void;
+ onEnd: () => void;
disable?: boolean;
};
@@ -44,15 +50,23 @@ export function usePanResponder({
computeNewAnimatedValueForGesture,
onDismiss,
onRestore,
+ onStart,
+ onEnd,
disable
}: UsePanResponderParams) {
+ const onGrant = React.useCallback(() => {
+ if (disable) return;
+ onStart();
+ },
+ [onStart, disable]
+ );
+
const onMove = React.useCallback(
(_event: GestureResponderEvent, gesture: PanResponderGestureState) => {
- if (disable) {
- return;
- }
+ if (disable) return;
const newAnimatedValue = computeNewAnimatedValueForGesture(gesture);
+
animatedValue.current?.setValue(newAnimatedValue);
},
[animatedValue, computeNewAnimatedValueForGesture, disable]
@@ -60,34 +74,36 @@ export function usePanResponder({
const onRelease = React.useCallback(
(_event: GestureResponderEvent, gesture: PanResponderGestureState) => {
- if (disable) {
- return;
- }
+ if (disable) return;
const newAnimatedValue = computeNewAnimatedValueForGesture(gesture);
+ onEnd();
if (shouldDismissView(newAnimatedValue, gesture)) {
onDismiss();
} else {
onRestore();
}
},
- [computeNewAnimatedValueForGesture, onDismiss, onRestore, disable]
+ [computeNewAnimatedValueForGesture, onEnd, onDismiss, onRestore, disable]
);
const panResponder = React.useMemo(
() =>
PanResponder.create({
- onMoveShouldSetPanResponder: shouldSetPanResponder,
- onMoveShouldSetPanResponderCapture: shouldSetPanResponder,
+ onStartShouldSetPanResponder: startShouldSetPanResponder,
+ onPanResponderGrant: onGrant,
+ onMoveShouldSetPanResponder: moveShouldSetPanResponder,
+ onMoveShouldSetPanResponderCapture: moveShouldSetPanResponder,
onPanResponderMove: onMove,
onPanResponderRelease: onRelease
}),
- [onMove, onRelease]
+ [onMove, onRelease, onGrant]
);
return {
panResponder,
+ onGrant,
onMove,
- onRelease
+ onRelease,
};
}
diff --git a/src/useToast.ts b/src/useToast.ts
index f738a33c8..99a239f68 100644
--- a/src/useToast.ts
+++ b/src/useToast.ts
@@ -1,6 +1,6 @@
import React from 'react';
-import { useLogger } from './contexts';
+import { useLogger, useGesture } from './contexts';
import { useTimeout } from './hooks';
import { ToastData, ToastOptions, ToastProps, ToastShowParams } from './types';
import { noop } from './utils/func';
@@ -35,6 +35,7 @@ export type UseToastParams = {
export function useToast({ defaultOptions }: UseToastParams) {
const { log } = useLogger();
+ const { panning } = useGesture();
const [isVisible, setIsVisible] = React.useState(false);
const [data, setData] = React.useState(DEFAULT_DATA);
@@ -47,10 +48,14 @@ export function useToast({ defaultOptions }: UseToastParams) {
React.useState>(initialOptions);
const onAutoHide = React.useCallback(() => {
- log('Auto hiding');
- setIsVisible(false);
- options.onHide();
- }, [log, options]);
+ if (panning.current) {
+ log('Auto hiding was blocked due to panning');
+ } else {
+ log('Auto hiding');
+ setIsVisible(false);
+ options.onHide();
+ }
+ }, [log, options, panning]);
const { startTimer, clearTimer } = useTimeout(
onAutoHide,
options.visibilityTime