import React, { useEffect, useState } from 'react'
import { useCallback, useMemo } from 'use-memo-one'
import { Easing, Animated, StyleSheet, TouchableWithoutFeedback, Platform } from 'react-native'
import type {
SharedProps,
ModalOptions,
ModalfyParams,
ModalStackItem,
ModalStackOptions,
ModalPendingClosingAction,
ModalStackSavedStackItemsOptions,
} from '../types'
import StackItem from './StackItem'
import { computeUpdatedModalOptions, defaultOptions, getStackItemOptions, queueMacroTask, sh } from '../utils'
type Props
= SharedProps
const ModalStack =
(props: Props
) => {
const { stack } = props
const [hasChangedBackdropColor, setBackdropColorStatus] = useState(false)
const [backdropClosedItems, setBackdropClosedItems] = useState([])
const [openActionCallbacks, setOpenActionCallbacks] = useState([])
const [stackStatus, setStackStatus] = useState<'idle' | 'shown' | 'hiding' | 'hidden'>('idle')
const [savedStackItemsOptions, setSavedStackItemsOptions] = useState>({})
const [
{ backBehavior, backdropColor, backdropOpacity, backdropPosition, stackContainerStyle, backdropAnimationDuration },
setModalStackOptions,
] = useState(getStackItemOptions(Array.from(stack.openedItems).pop(), stack))
const canShowStack = stackStatus === 'hiding' || stackStatus === 'shown'
const { opacity, translateY } = useMemo(
() => ({
opacity: new Animated.Value(0),
translateY: new Animated.Value(sh(100)),
}),
[],
)
const getStackContainerStyle = () => {
if (typeof stackContainerStyle === 'object') return stackContainerStyle
else if (typeof stackContainerStyle === 'function') return stackContainerStyle(opacity)
return {}
}
const hideBackdrop = useCallback(() => {
if (stackStatus === 'shown') {
setStackStatus('hiding')
Animated.timing(opacity, {
toValue: 0,
easing: Easing.inOut(Easing.ease),
duration: backdropAnimationDuration,
useNativeDriver: true,
}).start(() => {
queueMacroTask(() => {
setStackStatus('hidden')
setBackdropClosedItems([])
translateY.setValue(sh(100))
})
})
}
}, [backdropAnimationDuration, opacity, translateY, stackStatus])
const onSetModalStackOptions = useCallback((newModalOptions: ModalOptions) => {
queueMacroTask(() => {
setModalStackOptions(
computeUpdatedModalOptions(
'modalStack',
newModalOptions,
getStackItemOptions(Array.from(stack.openedItems).pop(), stack),
),
)
})
}, [])
const resetModalStackOptions = useCallback(() => {
queueMacroTask(() => {
setModalStackOptions(getStackItemOptions(Array.from(stack.openedItems).pop(), stack))
})
}, [])
const renderStackItem = (stackItem: ModalStackItem, index: number) => {
const position = stack.openedItems.size - index
const isBackdropBelowLatestModal = backdropPosition === 'belowLatest'
const isNotLatestOpenedModal = position >= 2
const isFirstVisibleModal = position === stack.openedItems.size
const isLastOpenedModal = position === 1 && stack.openedItems.size === 1
const pendingClosingAction: ModalPendingClosingAction | undefined = stack.pendingClosingActions
.values()
.next().value
const hasPendingClosingAction = position === 1 && pendingClosingAction?.currentModalHash === stackItem.hash
return (
{
// @ts-ignore
props.openModal(...args)
setOpenActionCallbacks(state => [...state, stackItem.hash])
}}
isLastOpenedModal={isLastOpenedModal}
isFirstVisibleModal={isFirstVisibleModal}
setModalStackOptions={onSetModalStackOptions}
savedStackItemsOptions={savedStackItemsOptions}
setSavedStackItemsOptions={setSavedStackItemsOptions}
wasOpenCallbackCalled={openActionCallbacks.includes(stackItem.hash)}
wasClosedByBackdropPress={backdropClosedItems.includes(stackItem.hash)}
zIndex={isBackdropBelowLatestModal && isNotLatestOpenedModal ? -1 : index + 1}
pendingClosingAction={hasPendingClosingAction ? pendingClosingAction : undefined}
/>
)
}
const renderStack = () => {
if (!stack.openedItems.size) return null
return [...stack.openedItems].map(renderStackItem)
}
const onBackdropPress = () => {
if (backBehavior === 'none') return
const currentItem = [...stack.openedItems].slice(-1)[0]
setBackdropClosedItems([...backdropClosedItems, currentItem?.hash])
}
const renderBackdrop = () => {
const onPress = () => onBackdropPress()
const isBackdropBelowLatestModal = backdropPosition === 'belowLatest'
const backgroundColor =
stack.openedItems.size && backdropColor ? backdropColor : hasChangedBackdropColor ? 'transparent' : 'black'
return (
)
}
useEffect(() => {
resetModalStackOptions()
}, [stack.openedItems.size])
useEffect(() => {
if (stack.openedItems.size && backdropColor && backdropColor !== 'black' && !hasChangedBackdropColor) {
setBackdropColorStatus(true)
}
}, [backdropColor, hasChangedBackdropColor, stack.openedItems.size])
useEffect(() => {
const scrollY = Platform.OS === 'web' ? window.scrollY ?? document.documentElement.scrollTop : 0
if (stack.openedItems.size) {
setStackStatus('shown')
translateY.setValue(scrollY)
Animated.timing(opacity, {
toValue: 1,
easing: Easing.in(Easing.ease),
duration: backdropAnimationDuration,
useNativeDriver: true,
}).start()
} else hideBackdrop()
}, [backdropAnimationDuration, opacity, stack.openedItems.size, translateY])
return canShowStack ? (
{renderBackdrop()}
{renderStack()}
) : null
}
const styles = StyleSheet.create({
container: {
...Platform.select({
android: {
...StyleSheet.absoluteFillObject,
zIndex: 0,
},
ios: {
...StyleSheet.absoluteFillObject,
zIndex: 0,
},
}),
},
containerWeb: {
...StyleSheet.absoluteFillObject,
overflow: 'hidden',
height: '100vh',
width: '100vw',
zIndex: 0,
// mobile viewport units bug fix
maxHeight: '-webkit-fill-available',
maxWidth: '-webkit-fill-available',
},
backdrop: {
...StyleSheet.absoluteFillObject,
},
})
export default ModalStack