Skip to content
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ node_modules
.idea
coverage
lib
.yarn
.yarnrc.yml
6 changes: 4 additions & 2 deletions src/Toast.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';

import { LoggerProvider } from './contexts';
import { LoggerProvider, GestureProvider } from './contexts';
import { ToastUI } from './ToastUI';
import {
ToastHideParams,
Expand Down Expand Up @@ -88,7 +88,9 @@ export function Toast(props: ToastProps) {

return (
<LoggerProvider enableLogs={false}>
<ToastRoot ref={setRef} {...props} />
<GestureProvider>
<ToastRoot ref={setRef} {...props} />
</GestureProvider>
</LoggerProvider>
);
}
Expand Down
4 changes: 4 additions & 0 deletions src/__helpers__/PanResponder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions src/__tests__/Toast.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => <ActualModal {...props} />;
return (props: any) => <ActualModal {...props} />;
});

jest.mock('react-native/Libraries/LogBox/LogBox');
Expand Down Expand Up @@ -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();
});
Expand All @@ -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();
});
Expand Down
40 changes: 36 additions & 4 deletions src/__tests__/useToast.test.ts → src/__tests__/useToast.test.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => (
<GestureProvider panning={panning}>{children}</GestureProvider>
);
};

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();
Expand Down Expand Up @@ -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();

Expand Down
21 changes: 18 additions & 3 deletions src/components/AnimatedContainer.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -81,6 +81,7 @@ export function AnimatedContainer({
swipeable
}: AnimatedContainerProps) {
const { log } = useLogger();
const { panning } = useGesture();

const { computeViewDimensions, height } = useViewDimensions();

Expand All @@ -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);
Expand All @@ -118,7 +131,9 @@ export function AnimatedContainer({
computeNewAnimatedValueForGesture,
onDismiss,
onRestore,
disable: !swipeable
onStart,
onEnd,
disable,
});

React.useLayoutEffect(() => {
Expand All @@ -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}
</Animated.View>
Expand Down
3 changes: 3 additions & 0 deletions src/components/__tests__/AnimatedContainer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const setup = (props?: Omit<Partial<AnimatedContainerProps>, 'children'>) => {
topOffset: 40,
bottomOffset: 40,
keyboardOffset: 10,
avoidKeyboard: true,
onHide
};

Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down
31 changes: 31 additions & 0 deletions src/contexts/GestureContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';

import { ReactChildren } from '../types';

export type GestureContextType = {
panning: React.MutableRefObject<boolean>;
};

export type GestureProviderProps = {
children: ReactChildren;
panning?: boolean;
};

const GestureContext = React.createContext<GestureContextType>({
panning: { current: false }
});

function GestureProvider({ children, panning = false }: GestureProviderProps) {
const panningRef = React.useRef(panning);
const value = { panning: panningRef };
return (
<GestureContext.Provider value={value}>{children}</GestureContext.Provider>
);
}

function useGesture() {
const ctx = React.useContext(GestureContext);
return ctx;
}

export { GestureProvider, useGesture };
32 changes: 32 additions & 0 deletions src/contexts/__tests__/GestureContext.test.tsx
Original file line number Diff line number Diff line change
@@ -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<GestureProviderProps, 'children'>) => {
const wrapper = ({ children }: { children: ReactChildren }) => (
<GestureProvider {...props}>{children}</GestureProvider>
);
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);
});
});
8 changes: 2 additions & 6 deletions src/contexts/__tests__/LoggerContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,8 @@ const setup = (props?: Omit<LoggerProviderProps, 'children'>) => {
const wrapper = ({ children }: { children: ReactChildren }) => (
<LoggerProvider {...props}>{children}</LoggerProvider>
);
const utils = renderHook(useLogger, {
wrapper
});
return {
...utils
};
const utils = renderHook(useLogger, { wrapper });
return { ...utils };
};

describe('test Logger context', () => {
Expand Down
1 change: 1 addition & 0 deletions src/contexts/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './LoggerContext';
export * from './GestureContext';
Loading
Loading