From 5574c2a609a7955c528c9696b50e93f69475a51f Mon Sep 17 00:00:00 2001 From: Wouter Date: Fri, 15 Dec 2023 17:49:05 +0100 Subject: [PATCH] feat: add option to disable swipe gestures (#446) * feat(swipeable): option to disable swipe gestures * test: update tests for swipeable option * docs: update api * ci: run quality checks & tests on push --------- Co-authored-by: Calin Tamas --- .github/workflows/quality.yml | 8 ++++++- .github/workflows/tests.yml | 8 ++++++- docs/api.md | 1 + src/ToastUI.tsx | 3 ++- src/__tests__/useToast.test.ts | 1 + src/components/AnimatedContainer.tsx | 7 ++++-- .../__tests__/AnimatedContainer.test.tsx | 1 + src/hooks/__tests__/usePanResponder.test.ts | 22 +++++++++++++++++-- src/hooks/usePanResponder.ts | 16 +++++++++++--- src/types/index.ts | 5 +++++ src/useToast.ts | 3 +++ 11 files changed, 65 insertions(+), 10 deletions(-) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 45a1321f7..537805a6e 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -1,6 +1,12 @@ name: Run code quality checks -on: pull_request +on: + push: + branches: + - main + pull_request: + branches: + - main jobs: quality: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ea7e90be0..b9930534f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,6 +1,12 @@ name: Run tests -on: pull_request +on: + push: + branches: + - main + pull_request: + branches: + - main jobs: test: diff --git a/docs/api.md b/docs/api.md index 37ad1098f..28be2460f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -59,6 +59,7 @@ The following set of `props` can be passed to the `Toast` component instance to | `position` | Default Toast position | `top` or `bottom` | `top` | | `visibilityTime` | Number of milliseconds after which Toast automatically hides. Has effect only in conjunction with `autoHide` prop set to `true` | `number` | `4000` | | `autoHide` | When `true`, the visible Toast automatically hides after a certain number of milliseconds, specified by the `visibilityTime` prop | `boolean` | `true` | +| `swipeable` | When `true`, the Toast can be swiped to dismiss | `boolean` | `true` | | `topOffset` | Offset from the top of the screen (in px). Has effect only when `position` is `top` | `number` | `40` | | `bottomOffset` | Offset from the bottom of the screen (in px). Has effect only when `position` is `bottom` | `number` | `40` | | `keyboardOffset` | Offset from the Keyboard (in px). Has effect only when `position` is `bottom` and Keyboard is visible (iOS only) | `number` | `10` | diff --git a/src/ToastUI.tsx b/src/ToastUI.tsx index 033f77ad4..ad00a261b 100644 --- a/src/ToastUI.tsx +++ b/src/ToastUI.tsx @@ -67,7 +67,7 @@ function renderComponent({ export function ToastUI(props: ToastUIProps) { const { isVisible, options, hide } = props; - const { position, topOffset, bottomOffset, keyboardOffset } = options; + const { position, topOffset, bottomOffset, keyboardOffset, swipeable } = options; return ( {renderComponent(props)} diff --git a/src/__tests__/useToast.test.ts b/src/__tests__/useToast.test.ts index 098e481dc..daf1b8099 100644 --- a/src/__tests__/useToast.test.ts +++ b/src/__tests__/useToast.test.ts @@ -119,6 +119,7 @@ describe('test useToast hook', () => { const options: ToastOptions = { type: 'info', position: 'bottom', + swipeable: true, text1Style: null, text2Style: null, autoHide: false, diff --git a/src/components/AnimatedContainer.tsx b/src/components/AnimatedContainer.tsx index 1bcc0056e..e1f09feec 100644 --- a/src/components/AnimatedContainer.tsx +++ b/src/components/AnimatedContainer.tsx @@ -18,6 +18,7 @@ export type AnimatedContainerProps = { isVisible: boolean; position: ToastPosition; topOffset: number; + swipeable: boolean; bottomOffset: number; keyboardOffset: number; onHide: () => void; @@ -74,7 +75,8 @@ export function AnimatedContainer({ bottomOffset, keyboardOffset, onHide, - onRestorePosition = noop + onRestorePosition = noop, + swipeable }: AnimatedContainerProps) { const { log } = useLogger(); @@ -112,7 +114,8 @@ export function AnimatedContainer({ animatedValue, computeNewAnimatedValueForGesture, onDismiss, - onRestore + onRestore, + disable: !swipeable }); React.useLayoutEffect(() => { diff --git a/src/components/__tests__/AnimatedContainer.test.tsx b/src/components/__tests__/AnimatedContainer.test.tsx index 28fea4cd6..5bca48dc1 100644 --- a/src/components/__tests__/AnimatedContainer.test.tsx +++ b/src/components/__tests__/AnimatedContainer.test.tsx @@ -26,6 +26,7 @@ const setup = (props?: Omit, 'children'>) => { const defaultProps: Omit = { isVisible: false, position: 'top', + swipeable: true, topOffset: 40, bottomOffset: 40, keyboardOffset: 10, diff --git a/src/hooks/__tests__/usePanResponder.test.ts b/src/hooks/__tests__/usePanResponder.test.ts index d44c9bd74..6ed6ddddc 100644 --- a/src/hooks/__tests__/usePanResponder.test.ts +++ b/src/hooks/__tests__/usePanResponder.test.ts @@ -7,7 +7,7 @@ import { mockGestureValues } from '../../__helpers__/PanResponder'; import { usePanResponder } from '../usePanResponder'; import { shouldSetPanResponder } from '..'; -const setup = ({ newAnimatedValueForGesture = 0 } = {}) => { +const setup = ({ newAnimatedValueForGesture = 0, disable = false } = {}) => { const animatedValue = { current: new Animated.Value(0) }; @@ -22,7 +22,8 @@ const setup = ({ newAnimatedValueForGesture = 0 } = {}) => { animatedValue, computeNewAnimatedValueForGesture, onDismiss, - onRestore + onRestore, + disable }) ); return { @@ -58,6 +59,23 @@ describe('test usePanResponder hook', () => { expect(computeNewAnimatedValueForGesture).toBeCalledWith(mockGestureValues); }); + it('should NOT compute new animated value on move/release, if disable: true', () => { + const { result, computeNewAnimatedValueForGesture } = setup({ + newAnimatedValueForGesture: 1, + disable: true + }); + + result.current.onMove({} as GestureResponderEvent, mockGestureValues); + expect(computeNewAnimatedValueForGesture).not.toBeCalledWith( + mockGestureValues + ); + + result.current.onRelease({} as GestureResponderEvent, mockGestureValues); + expect(computeNewAnimatedValueForGesture).not.toBeCalledWith( + mockGestureValues + ); + }); + it('calls onDismiss when swipe gesture value is below dismiss threshold', () => { const { result, computeNewAnimatedValueForGesture, onDismiss } = setup({ newAnimatedValueForGesture: 0.65 diff --git a/src/hooks/usePanResponder.ts b/src/hooks/usePanResponder.ts index df93e984d..3e0260a14 100644 --- a/src/hooks/usePanResponder.ts +++ b/src/hooks/usePanResponder.ts @@ -36,24 +36,34 @@ export type UsePanResponderParams = { ) => number; onDismiss: () => void; onRestore: () => void; + disable?: boolean; }; export function usePanResponder({ animatedValue, computeNewAnimatedValueForGesture, onDismiss, - onRestore + onRestore, + disable }: UsePanResponderParams) { const onMove = React.useCallback( (_event: GestureResponderEvent, gesture: PanResponderGestureState) => { + if (disable) { + return; + } + const newAnimatedValue = computeNewAnimatedValueForGesture(gesture); animatedValue.current?.setValue(newAnimatedValue); }, - [animatedValue, computeNewAnimatedValueForGesture] + [animatedValue, computeNewAnimatedValueForGesture, disable] ); const onRelease = React.useCallback( (_event: GestureResponderEvent, gesture: PanResponderGestureState) => { + if (disable) { + return; + } + const newAnimatedValue = computeNewAnimatedValueForGesture(gesture); if (shouldDismissView(newAnimatedValue, gesture)) { onDismiss(); @@ -61,7 +71,7 @@ export function usePanResponder({ onRestore(); } }, - [computeNewAnimatedValueForGesture, onDismiss, onRestore] + [computeNewAnimatedValueForGesture, onDismiss, onRestore, disable] ); const panResponder = React.useMemo( diff --git a/src/types/index.ts b/src/types/index.ts index 748463e5c..66c1c0b87 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -44,6 +44,11 @@ export type ToastOptions = { * Default value: `4000` */ visibilityTime?: number; + /** + * When `true`, the Toast can be dismissed by a swipe gesture, specified by the `swipeable` prop. + * Default value: `true` + */ + swipeable?: boolean; /** * Offset from the top of the screen (in px). * Has effect only when `position` is `top` diff --git a/src/useToast.ts b/src/useToast.ts index 0e782a200..11b459749 100644 --- a/src/useToast.ts +++ b/src/useToast.ts @@ -17,6 +17,7 @@ export const DEFAULT_OPTIONS: Required = { text2Style: null, position: 'top', autoHide: true, + swipeable: true, visibilityTime: 4000, topOffset: 40, bottomOffset: 40, @@ -79,6 +80,7 @@ export function useToast({ defaultOptions }: UseToastParams) { onShow = initialOptions.onShow, onHide = initialOptions.onHide, onPress = initialOptions.onPress, + swipeable = initialOptions.swipeable, props = initialOptions.props } = params; setData({ @@ -99,6 +101,7 @@ export function useToast({ defaultOptions }: UseToastParams) { onShow, onHide, onPress, + swipeable, props }) as Required );