import { useCallback } from 'use-memo-one' import React, { ReactNode, useEffect, useRef, useState } from 'react' import { BackHandler, NativeEventSubscription, Platform } from 'react-native' import type { SharedProps, ModalStateListener, ModalEventListeners, ModalContextProvider, ModalStateSubscription, ModalStack as ModalStackType, } from '../types' import ModalStack from './ModalStack' import ModalState from './ModalState' import ModalContext from './ModalContext' import { invariant, validateListener } from '../utils' interface Props { children: ReactNode stack: ModalStackType } /** * `` is the component you're going to use to wrap your whole application, * so it'll be able to display your modals on top of everything else, using React Context API. * * @prop { ModalStackType } `stack` - Modal stack object generated by `createModalStack()` * * @see https://colorfy-software.gitbook.io/react-native-modalfy/guides/stack#provider */ const ModalProvider = ({ children, stack }: Props) => { const backHandlerSubscription = useRef() const modalStateSubscription = useRef>() const modalEventListeners = useRef(new Set()).current const openModal: SharedProps['openModal'] = (modalName, params, callback) => { const { currentModal } = ModalState.getState() if (!currentModal) { backHandlerSubscription.current = BackHandler.addEventListener('hardwareBackPress', ModalState.handleBackPress) } ModalState.openModal({ modalName, params, callback, isCalledOutsideOfContext: true }) } const getParam: SharedProps['getParam'] = (hash, paramName, defaultValue) => ModalState.getParam(hash, paramName, defaultValue) const closeModal: SharedProps['closeModal'] = stackItem => ModalState.closeModal(stackItem) const closeModals: SharedProps['closeModals'] = modalName => ModalState.closeModals(modalName) const closeAllModals: SharedProps['closeAllModals'] = () => ModalState.closeAllModals() const [contextValue, setContextValue] = useState>({ stack, getParam, openModal, closeModal, closeModals, closeAllModals, currentModal: null, }) const registerListener: SharedProps['registerListener'] = useCallback( (hash, eventName, handler) => { validateListener('add', { eventName, handler }) const newListener = { event: `${hash}_${eventName}`, handler, } modalEventListeners.add(newListener) return { remove: () => modalEventListeners.delete(newListener), } }, [modalEventListeners], ) const clearListeners: SharedProps['clearListeners'] = useCallback( hash => { modalEventListeners.forEach(item => { if (item.event.includes(hash)) modalEventListeners.delete(item) }) }, [modalEventListeners], ) const listener: ModalStateListener = (modalState, error) => { if (modalState) { setContextValue({ ...contextValue, currentModal: modalState.currentModal, stack: modalState.stack, }) } else console.warn('Modalfy', error) } useEffect(() => { invariant(stack, 'You need to provide a `stack` prop to ') ModalState.init(() => ({ currentModal: null, stack, })) modalStateSubscription.current = ModalState.subscribe(listener) return () => { backHandlerSubscription.current?.remove?.() modalStateSubscription.current?.unsubscribe?.() } }, []) useEffect(() => { // NOTE: Used to prevent scrolling on Web when the modal stack is opened. if (Platform.OS === 'web' && contextValue.stack.openedItems.size) { document.body.style.overflow = 'hidden' document.body.style.touchAction = 'none' document.body.style.overscrollBehavior = 'none' } return () => { if (Platform.OS === 'web') { document.body.style.overflow = 'auto' document.body.style.touchAction = 'auto' document.body.style.overscrollBehavior = 'auto' } } }, [contextValue.stack.openedItems.size]) return ( <> {children} ) } export default ModalProvider