Skip to content

Commit

Permalink
fix: animate modals out when closed via hooks/HOC/vanilla 🌬
Browse files Browse the repository at this point in the history
  • Loading branch information
CharlesMangwa committed Jan 24, 2022
1 parent 091c9af commit ce6040f
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 90 deletions.
13 changes: 7 additions & 6 deletions lib/ModalProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,10 @@ const ModalProvider = ({ children, stack }: Props) => {
useEffect(() => {
invariant(stack, 'You need to provide a `stack` prop to <ModalProvider>')

ModalState.init<any>({
ModalState.init<any>(() => ({
currentModal: null,
stack,
})
}))

modalStateSubscription.current = ModalState.subscribe(listener)

Expand All @@ -130,19 +130,20 @@ const ModalProvider = ({ children, stack }: Props) => {
modalStateSubscription.current?.unsubscribe()
}

// Should only be triggered on initial mount and return when unmounted
// NOTE: Should only be triggered on initial mount and return when unmounted
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

return (
<ModalContext.Provider value={contextValue}>
<>
<>{children}</>
{children}
<ModalStack
{...contextValue}
eventListeners={modalEventListeners}
registerListener={registerListener}
clearListeners={clearListeners}
registerListener={registerListener}
eventListeners={modalEventListeners}
removeClosingAction={ModalState.removeClosingAction}
/>
</>
</ModalContext.Provider>
Expand Down
54 changes: 33 additions & 21 deletions lib/ModalStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import {
} from 'react-native'
import { useMemo } from 'use-memo-one'

import { ModalfyParams, ModalStackItem, SharedProps } from '../types'
import {
SharedProps,
ModalfyParams,
ModalStackItem,
ModalPendingClosingAction,
} from '../types'

import StackItem from './StackItem'

Expand Down Expand Up @@ -79,27 +84,32 @@ const ModalStack = <P extends ModalfyParams>(props: Props<P>) => {
}
}, [openedItemsArray.length, stack.openedItems.size, translateY, opacity])

useEffect(() => {
if (stack.openedItemsSize !== openedItemsArray.length) {
setOpenedItemsArray([...stack.openedItems])
}
}, [openedItemsArray.length, stack.openedItems, stack.openedItemsSize])

const renderStackItem = (stackItem: ModalStackItem<P>, index: number) => (
<StackItem
{...props}
// @ts-ignore
stackItem={stackItem}
key={index}
zIndex={index + 1}
position={openedItemsArray.length - index}
wasClosedByBackdropPress={backdropClosedItems.includes(stackItem.hash)}
/>
)
const renderStackItem = (stackItem: ModalStackItem<P>, index: number) => {
const position = stack.openedItemsSize - index
const pendingClosingAction: ModalPendingClosingAction | undefined =
stack.pendingClosingActions.values().next().value
const hasPendingClosingAction =
position === 1 &&
pendingClosingAction?.currentModalHash === stackItem.hash
return (
<StackItem
{...props}
// @ts-ignore
stackItem={stackItem}
key={index}
zIndex={index + 1}
position={position}
wasClosedByBackdropPress={backdropClosedItems.includes(stackItem.hash)}
pendingClosingAction={
hasPendingClosingAction ? pendingClosingAction : undefined
}
/>
)
}

const renderStack = () => {
if (!openedItemsArray.length) return null
return openedItemsArray.map(renderStackItem)
if (!stack.openedItemsSize) return null
return [...stack.openedItems].map(renderStackItem)
}

const onBackdropPress = () => {
Expand Down Expand Up @@ -176,5 +186,7 @@ const styles = StyleSheet.create({
export default memo(
ModalStack,
(prevProps, nextProps) =>
prevProps.stack.openedItemsSize === nextProps.stack.openedItemsSize,
prevProps.stack.openedItemsSize === nextProps.stack.openedItemsSize &&
prevProps.stack.pendingClosingActionsSize ===
nextProps.stack.pendingClosingActionsSize,
)
139 changes: 111 additions & 28 deletions lib/ModalState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,16 @@ const createModalState = (): ModalStateType<any> => {
let initialState: State<any>
let state: State<any>

const setState = <P>(newState: State<P>): State<P> => {
const setState = <P>(
updater: (currentState: ModalInternalState<P>) => ModalInternalState<P>,
) => {
const newState = updater(state)
state = {
...newState,
stack: {
...newState.stack,
openedItemsSize: newState.stack.openedItems.size,
pendingClosingActionsSize: newState.stack.pendingClosingActions.size,
},
}
listeners.forEach((listener) => listener())
Expand Down Expand Up @@ -108,18 +112,19 @@ const createModalState = (): ModalStateType<any> => {
BackHandler.addEventListener('hardwareBackPress', handleBackPress)
}

setState<P>({
setState<P>((currentState) => ({
currentModal: modalName,
stack: {
...state.stack,
...currentState.stack,
openedItems: state.stack.openedItems.add(
Object.assign({}, stackItem, {
hash,
callback,
...(params && { params }),
}),
),
} as ModalContextProvider<P>['stack'],
})
}))
}

const getParam = <
Expand Down Expand Up @@ -183,10 +188,10 @@ const createModalState = (): ModalStateType<any> => {

const openedItemsArray = Array.from(openedItems)

setState({
setState((currentState) => ({
currentModal: openedItemsArray?.[openedItemsArray?.length - 1]?.name,
stack: { ...state.stack, openedItems },
})
stack: { ...currentState.stack, openedItems },
}))
}

const closeModals = <P>(modalName: Exclude<keyof P, symbol>): boolean => {
Expand All @@ -210,10 +215,10 @@ const createModalState = (): ModalStateType<any> => {

if (newOpenedItems.size !== oldOpenedItems.size) {
const openedItemsArray = Array.from(newOpenedItems)
setState({
setState((currentState) => ({
currentModal: openedItemsArray?.[openedItemsArray?.length - 1]?.name,
stack: { ...state.stack, openedItems: newOpenedItems },
})
stack: { ...currentState.stack, openedItems: newOpenedItems },
}))
return true
}

Expand All @@ -225,10 +230,10 @@ const createModalState = (): ModalStateType<any> => {

openedItems.clear()

setState({
setState((currentState) => ({
currentModal: null,
stack: { ...state.stack, openedItems },
})
stack: { ...currentState.stack, openedItems },
}))
}

const handleBackPress = (): boolean => {
Expand All @@ -239,28 +244,94 @@ const createModalState = (): ModalStateType<any> => {
if (currentModal) {
if (backBehavior === 'none') return true
else if (backBehavior === 'clear') {
setState(initialState)
queueClosingAction({ action: 'closeAllModals' })
return true
} else if (backBehavior === 'pop') {
closeModal(currentModalStackItem)
queueClosingAction({ action: 'closeModal', modalName: currentModal })
return true
}
}

return false
}

const queueClosingAction = <P>({
action,
callback,
modalName,
}: ModalStateType<P>['queueClosingAction']['arguments']): ModalStateType<P>['queueClosingAction']['arguments'] => {
const {
stack: { names },
} = state

if (action !== 'closeAllModals' && modalName) {
invariant(
names.some((name) => name === modalName),
`'${modalName}' is not a valid modal name. Did you mean any of these: ${names.map(
(validName) => `\n• ${validName}`,
)}`,
)
}

const hash = `${
modalName ? `${modalName}_${action}` : action
}_${Math.random().toString(36).substring(2, 11)}`

const { pendingClosingActions } = setState((currentState) => ({
...currentState,
stack: {
...currentState.stack,
pendingClosingActions: currentState.stack.pendingClosingActions.add({
hash,
action,
modalName,
currentModalHash: [...currentState.stack.openedItems].slice(-1)[0]
.hash,
}),
},
})).stack

return [...pendingClosingActions].slice(-1)[0]
}

const removeClosingAction = (action: ModalPendingClosingAction): boolean => {
const {
stack: { pendingClosingActions: oldPendingClosingActions },
} = state

const newPendingClosingActions = new Set(oldPendingClosingActions)

if (newPendingClosingActions.has(action)) {
newPendingClosingActions.delete(action)
}

if (newPendingClosingActions.size !== oldPendingClosingActions.size) {
setState((currentState) => ({
...currentState,
stack: {
...currentState.stack,
pendingClosingActions: newPendingClosingActions,
},
}))
return true
}

return false
}

return {
handleBackPress,
closeAllModals,
closeModals,
closeModal,
subscribe,
openModal,
getParam,
getState,
setState,
init,
setState,
getState,
getParam,
openModal,
subscribe,
closeModal,
closeModals,
closeAllModals,
handleBackPress,
queueClosingAction,
removeClosingAction,
}
}

Expand Down Expand Up @@ -290,7 +361,9 @@ export const modalfy = <
*
* @see https://colorfy-software.gitbook.io/react-native-modalfy/api/types/modalprop#closeallmodals
*/
closeAllModals: ModalState.closeAllModals,
closeAllModals: () => {
ModalState.queueClosingAction({ action: 'closeAllModals' })
},
/**
* This function closes the currently displayed modal by default.
*
Expand All @@ -302,21 +375,31 @@ export const modalfy = <
*
* @see https://colorfy-software.gitbook.io/react-native-modalfy/api/types/modalprop#closemodal
*/
closeModal: (modalName?: M) => ModalState.closeModal(modalName),
closeModal: (modalName?: M) => {
ModalState.queueClosingAction({
action: 'closeModal',
modalName,
})
},
/**
* This function closes all the instances of a given modal.
*
* You can use it whenever you have the same modal opened
* several times, to close all of them at once.
*
* @example modalfy().closeModals('ErrorModal')
* @example modalfy().closeModals('ExampleModal')
*
* @returns { boolean } Whether or not Modalfy found any open modal
* corresponding to `modalName` (and then closed them).
*
* @see https://colorfy-software.gitbook.io/react-native-modalfy/api/types/modalprop#closemodals
*/
closeModals: (modalName: M) => ModalState.closeModals(modalName),
closeModals: (modalName: M) => {
ModalState.queueClosingAction({
action: 'closeModals',
modalName,
})
},
/**
* This value returns the current open modal (`null` if none).
*
Expand Down

0 comments on commit ce6040f

Please sign in to comment.