UNPKG

react-native-ui-lib

Version:

[![SWUbanner](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct.svg)](https://stand-with-ukraine.pp.ua)

218 lines (210 loc) • 7.57 kB
import React, { useMemo, useCallback, useImperativeHandle, forwardRef, useEffect, useState } from 'react'; import { StyleSheet } from 'react-native'; import hoistStatics from 'hoist-non-react-statics'; import { Extrapolation, interpolate, runOnJS, useAnimatedStyle, useSharedValue, withSpring, withTiming } from 'react-native-reanimated'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import { Spacings, Colors, BorderRadiuses } from "../../style"; import { useDidUpdate } from "../../hooks"; import { asBaseComponent, Constants } from "../../commons/new"; import View from "../../components/view"; import Modal from "../../components/modal"; import { extractAlignmentsValues } from "../../commons/modifiers"; import useHiddenLocation from "../hooks/useHiddenLocation"; import DialogHeader from "./DialogHeader"; import useDialogContent from "./useDialogContent"; import { DialogProps, DialogDirections, DialogDirectionsEnum, DialogHeaderProps, DialogMigrationProps } from "./types"; export { DialogProps, DialogDirections, DialogDirectionsEnum, DialogHeaderProps, DialogMigrationProps }; const THRESHOLD_VELOCITY = 750; const Dialog = (props, ref) => { const { visible = false, headerProps, showCloseButton, closeButtonProps, containerStyle: propsContainerStyle, containerProps: propsContainerProps, width, height, onDismiss, direction = DialogDirectionsEnum.DOWN, ignoreBackgroundPress, modalProps = {}, useSafeArea, testID, children } = props; const { overlayBackgroundColor = Colors.rgba(Colors.grey10, 0.65), ...otherModalProps } = modalProps; const visibility = useSharedValue(0); // value between 0 (closed) and 1 (open) const initialTranslation = useSharedValue(0); const [modalVisibility, setModalVisibility] = useState(visible); // unfortunately this is needed when changing visibility by the user (clicking on an option etc) const { setRef, onLayout, hiddenLocation: _hiddenLocation } = useHiddenLocation(); const hiddenLocation = _hiddenLocation[direction]; const wasMeasured = _hiddenLocation.wasMeasured; const isVertical = useMemo(() => { 'worklet'; return direction === DialogDirectionsEnum.DOWN || direction === DialogDirectionsEnum.UP; }, [direction]); const getTranslationInterpolation = useCallback(value => { 'worklet'; return interpolate(value, [0, 1], [hiddenLocation, 0], Extrapolation.CLAMP); }, [hiddenLocation]); const getTranslationReverseInterpolation = useCallback(value => { 'worklet'; return interpolate(value, [hiddenLocation, 0], [0, 1]); }, [hiddenLocation]); const _onDismiss = useCallback(() => { 'worklet'; runOnJS(setModalVisibility)(false); }, []); const close = useCallback(() => { 'worklet'; visibility.value = withTiming(0, undefined, _onDismiss); // eslint-disable-next-line react-hooks/exhaustive-deps }, [_onDismiss]); const open = useCallback(() => { 'worklet'; visibility.value = withSpring(1); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { if (visible) { setModalVisibility(true); } else if (wasMeasured && modalVisibility) { // Close when sending visible = false close(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [visible, wasMeasured]); useDidUpdate(() => { if (wasMeasured) { if (modalVisibility) { open(); } else if (Constants.isAndroid) { onDismiss?.(); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [modalVisibility, wasMeasured]); const alignmentStyle = useMemo(() => { return { flex: 1, alignItems: 'center', ...extractAlignmentsValues(props) }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const { renderDialogContent, containerProps, containerStyle } = useDialogContent({ showCloseButton, close, closeButtonProps, containerStyle: propsContainerStyle, containerProps: propsContainerProps, headerProps, children }); const animatedStyle = useAnimatedStyle(() => { if (isVertical) { return { transform: [{ translateY: getTranslationInterpolation(visibility.value) }] }; } else { return { transform: [{ translateX: getTranslationInterpolation(visibility.value) }] }; } }); const style = useMemo(() => { return [styles.defaultDialogStyle, { backgroundColor: Colors.$backgroundDefault }, containerStyle, animatedStyle, width ? { width } : undefined, height ? { height } : undefined]; // eslint-disable-next-line react-hooks/exhaustive-deps }, [containerStyle, width, height]); const shouldClose = event => { 'worklet'; const wasPannedOverThreshold = Math.abs(getTranslationInterpolation(visibility.value)) >= Math.abs(hiddenLocation / 3); let velocity; switch (direction) { case DialogDirectionsEnum.DOWN: default: velocity = event.velocityY; break; case DialogDirectionsEnum.UP: velocity = -event.velocityY; break; case DialogDirectionsEnum.LEFT: velocity = -event.velocityX; break; case DialogDirectionsEnum.RIGHT: velocity = event.velocityX; break; } const wasFlung = velocity >= THRESHOLD_VELOCITY; return wasPannedOverThreshold || wasFlung; }; const panGesture = Gesture.Pan().onStart(event => { initialTranslation.value = getTranslationReverseInterpolation(isVertical ? event.translationY : event.translationX) - visibility.value; }).onUpdate(event => { visibility.value = getTranslationReverseInterpolation(isVertical ? event.translationY : event.translationX) - initialTranslation.value; }).onEnd(event => { if (shouldClose(event)) { close(); } else { open(); } }); useImperativeHandle(ref, () => ({ dismiss: close })); const renderDialog = () => <GestureDetector gesture={panGesture}> <View {...containerProps} reanimated={!Constants.accessibility.isReduceMotionEnabled} style={style} onLayout={onLayout} ref={setRef} testID={testID}> {renderDialogContent()} </View> </GestureDetector>; const overlayStyle = useAnimatedStyle(() => { return { opacity: visibility.value, backgroundColor: overlayBackgroundColor }; }, [overlayBackgroundColor]); const renderOverlayView = () => <View testID={`${testID}.overlayFadingBackground`} absF reanimated style={overlayStyle} pointerEvents="none" />; return <Modal transparent animationType={'none'} {...otherModalProps} testID={`${testID}.modal`} useGestureHandlerRootView visible={modalVisibility} onBackgroundPress={ignoreBackgroundPress ? undefined : close} onRequestClose={ignoreBackgroundPress ? undefined : close} onDismiss={onDismiss}> {renderOverlayView()} <View useSafeArea={useSafeArea} pointerEvents={'box-none'} style={alignmentStyle}> {renderDialog()} </View> </Modal>; }; Dialog.displayName = 'Incubator.Dialog'; Dialog.directions = DialogDirectionsEnum; Dialog.Header = DialogHeader; const _Dialog = forwardRef(Dialog); hoistStatics(_Dialog, Dialog); export default asBaseComponent(_Dialog); const styles = StyleSheet.create({ defaultDialogStyle: { marginBottom: Spacings.s5, maxHeight: '60%', width: 250, borderRadius: BorderRadiuses.br20, overflow: 'hidden' } });