Skip to content

Commit

Permalink
feat!: implement modal actions callback 🌺
Browse files Browse the repository at this point in the history
BREAKING CHANGE: A new `callback` argument is provided to the `animateOut` function in the modal `options` and *must* be called whenever the animation is `finished`. If only `animationOutConfig` is used, calling `callback` is not required.
  • Loading branch information
CharlesMangwa committed Jan 24, 2022
1 parent ce6040f commit e802dc1
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 40 deletions.
2 changes: 1 addition & 1 deletion lib/ModalProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const ModalProvider = ({ children, stack }: Props) => {
)
}

ModalState.openModal(modalName, params)
ModalState.openModal(modalName, params, false, callback)
}

const getParam: SharedProps<any>['getParam'] = (
Expand Down
15 changes: 7 additions & 8 deletions lib/ModalStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,14 @@ import { getStackItemOptions, sh } from '../utils'

type Props<P> = SharedProps<P>

type State<P> = {
backdropClosedItems: string[]
openedItemsArray: ModalStackItem<P>[]
}

const ModalStack = <P extends ModalfyParams>(props: Props<P>) => {
const { stack } = props

const [hasChangedBackdropColor, setBackdropColorStatus] = useState(false)

const [backdropClosedItems, setBackdropClosedItems] = useState<string[]>([])

const [openedItemsArray, setOpenedItemsArray] = useState<
State<P>['openedItemsArray']
>([...stack.openedItems])
const [openActionCallbacks, setOpenActionCallbacks] = useState<string[]>([])

const { opacity, translateY } = useMemo(
() => ({
Expand Down Expand Up @@ -99,6 +92,12 @@ const ModalStack = <P extends ModalfyParams>(props: Props<P>) => {
key={index}
zIndex={index + 1}
position={position}
openModal={(...args) => {
// @ts-ignore
props.openModal(...args)
setOpenActionCallbacks((state) => [...state, stackItem.hash])
}}
wasOpenCallbackCalled={openActionCallbacks.includes(stackItem.hash)}
wasClosedByBackdropPress={backdropClosedItems.includes(stackItem.hash)}
pendingClosingAction={
hasPendingClosingAction ? pendingClosingAction : undefined
Expand Down
24 changes: 14 additions & 10 deletions lib/ModalState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ const createModalState = (): ModalStateType<any> => {
modalName: Exclude<keyof P, symbol | number>,
params?: P,
isCalledOutsideOfContext?: boolean,
callback?: () => void,
) => {
const {
stack: { content, names },
Expand Down Expand Up @@ -284,6 +285,7 @@ const createModalState = (): ModalStateType<any> => {
pendingClosingActions: currentState.stack.pendingClosingActions.add({
hash,
action,
callback,
modalName,
currentModalHash: [...currentState.stack.openedItems].slice(-1)[0]
.hash,
Expand Down Expand Up @@ -357,12 +359,12 @@ export const modalfy = <
/**
* This function closes every open modal.
*
* @example modalfy().closeAllModals()
* @example modalfy().closeAllModals(() => console.log('All modals closed'))
*
* @see https://colorfy-software.gitbook.io/react-native-modalfy/api/types/modalprop#closeallmodals
*/
closeAllModals: () => {
ModalState.queueClosingAction({ action: 'closeAllModals' })
closeAllModals: (callback?: () => void) => {
ModalState.queueClosingAction({ action: 'closeAllModals', callback })
},
/**
* This function closes the currently displayed modal by default.
Expand All @@ -371,14 +373,15 @@ export const modalfy = <
* than the latest opened. This will only close the latest instance of that modal,
* see `closeModals()` if you want to close all instances.
*
* @example modalfy().closeModal()
* @example modalfy().closeModal('ExampleModal', () => console.log('Current modal closed'))
*
* @see https://colorfy-software.gitbook.io/react-native-modalfy/api/types/modalprop#closemodal
*/
closeModal: (modalName?: M) => {
closeModal: (modalName?: M, callback?: () => void) => {
ModalState.queueClosingAction({
action: 'closeModal',
modalName,
callback,
})
},
/**
Expand All @@ -387,17 +390,18 @@ export const modalfy = <
* You can use it whenever you have the same modal opened
* several times, to close all of them at once.
*
* @example modalfy().closeModals('ExampleModal')
* @example modalfy().closeModals('ExampleModal', () => console.log('All ExampleModal modals closed'))
*
* @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) => {
closeModals: (modalName: M, callback?: () => void) => {
ModalState.queueClosingAction({
action: 'closeModals',
modalName,
callback,
})
},
/**
Expand All @@ -416,12 +420,12 @@ export const modalfy = <
* Alternatively, you can also provide some `params` that will be
* accessible to that component.
*
* @example modalfy().openModal('PokédexEntryModal', { id: 619, name: 'Lin-Fu' })
* @example modalfy().openModal('PokédexEntryModal', { id: 619, name: 'Lin-Fu' }, () => console.log('PokédexEntryModal modal opened'))
*
* @see https://colorfy-software.gitbook.io/react-native-modalfy/api/types/modalprop#openmodal
*/
openModal: (modalName: M, params?: P[M]) =>
ModalState.openModal(modalName, params, true),
openModal: (modalName: M, params?: P[M], callback?: () => void) =>
ModalState.openModal(modalName, params, true, callback),
})

export default ModalState
36 changes: 26 additions & 10 deletions lib/StackItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@ import {
import { getStackItemOptions, vh } from '../utils'

type Props<P> = SharedProps<P> & {
wasOpenCallbackCalled: boolean
wasClosedByBackdropPress: boolean
pendingClosingAction?: ModalPendingClosingAction
position: number
zIndex: number
}

const StackItem = <P extends ModalfyParams>({
removeClosingAction,
pendingClosingAction,
wasOpenCallbackCalled,
wasClosedByBackdropPress,
registerListener,
clearListeners,
Expand Down Expand Up @@ -110,13 +113,15 @@ const StackItem = <P extends ModalfyParams>({
(
toValue: number,
closeModalCallback?: (closingElement: ModalStackItem<P>) => void,
modalStackItemCallback?: () => void,
) => {
if (!shouldAnimateOut) closeModalCallback?.(stackItem)
if (!closeModalCallback && animationIn) {
animationIn(animatedValue, toValue)
animationIn(animatedValue, toValue, modalStackItemCallback)
} else if (closeModalCallback && animationOut) {
animationOut(animatedValue, toValue, () => {
closeModalCallback(stackItem)
modalStackItemCallback?.()
})
} else {
Animated.timing(animatedValue, {
Expand All @@ -126,53 +131,57 @@ const StackItem = <P extends ModalfyParams>({
}).start(({ finished }) => {
if (finished) {
closeModalCallback?.(stackItem)
clearTimeout(timeout)
modalStackItemCallback?.()
}
})
}
},
[
stackItem,
animationIn,
animateInConfig,
animationOut,
animateOutConfig,
animatedValue,
shouldAnimateOut,
stackItem,
animateInConfig,
animateOutConfig,
],
)

const closeStackItem = useCallback(
(modalName) => {
(modalName, callback?: () => void) => {
if (!modalName || modalName === currentModal) {
updateAnimatedValue(position - 1, () => {
closeModal(modalName)
callback?.()
})
} else {
closeModal(modalName)
callback?.()
}
},
[closeModal, currentModal, position, updateAnimatedValue],
)

const closeStackItems = useCallback(
(closingElement) => {
(closingElement, callback?: () => void) => {
if (closingElement === currentModal && position === 1) {
return updateAnimatedValue(position - 1, () => {
const output = closeModals(closingElement)
callback?.()
return output
})
}
const output = closeModals(closingElement)
callback?.()
return output
},
[closeModals, currentModal, position, updateAnimatedValue],
)

const closeAllStackItems = useCallback(
() =>
(callback?: () => void) =>
updateAnimatedValue(position - 1, () => {
closeAllModals()
callback?.()
}),
[closeAllModals, position, updateAnimatedValue],
)
Expand Down Expand Up @@ -264,7 +273,11 @@ const StackItem = <P extends ModalfyParams>({
}, [verticalPosition])

useEffect(() => {
updateAnimatedValue(position)
updateAnimatedValue(
position,
undefined,
wasOpenCallbackCalled ? undefined : stackItem.callback,
)
if (wasClosedByBackdropPress) {
if (backBehavior === 'clear') closeAllStackItems()
else closeStackItem(undefined)
Expand All @@ -274,8 +287,10 @@ const StackItem = <P extends ModalfyParams>({
closeAllStackItems,
closeStackItem,
position,
stackItem.callback,
updateAnimatedValue,
wasClosedByBackdropPress,
wasOpenCallbackCalled,
])

useEffect(() => {
Expand Down Expand Up @@ -327,5 +342,6 @@ export default memo(
prevProps.position === nextProps.position &&
prevProps.stackItem.hash === nextProps.stackItem.hash &&
prevProps.pendingClosingAction === nextProps.pendingClosingAction &&
prevProps.wasOpenCallbackCalled === nextProps.wasOpenCallbackCalled &&
prevProps.wasClosedByBackdropPress === nextProps.wasClosedByBackdropPress,
)
8 changes: 4 additions & 4 deletions lib/useModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default function <P extends ModalfyParams>(): UsableModalProp<P> {
/**
* This function closes every open modal.
*
* @example modal.closeAllModals()
* @example modal.closeAllModals(() => console.log('All modals closed'))
*
* @see https://colorfy-software.gitbook.io/react-native-modalfy/api/types/modalprop#closeallmodals
*/
Expand All @@ -31,7 +31,7 @@ export default function <P extends ModalfyParams>(): UsableModalProp<P> {
* than the latest opened. This will only close the latest instance of that modal,
* see `closeModals()` if you want to close all instances.
*
* @example modal.closeModal()
* @example modal.closeModal('Example', () => console.log('Current modal closed'))
*
* @see https://colorfy-software.gitbook.io/react-native-modalfy/api/types/modalprop#closemodal
*/
Expand All @@ -42,7 +42,7 @@ export default function <P extends ModalfyParams>(): UsableModalProp<P> {
* You can use it whenever you have the same modal opened
* several times, to close all of them at once.
*
* @example modal.closeModals('ErrorModal')
* @example modal.closeModals('ExampleModal', () => console.log('All ExampleModal modals closed'))
*
* @returns { boolean } Whether or not Modalfy found any open modal
* corresponding to `modalName` (and then closed them).
Expand All @@ -66,7 +66,7 @@ export default function <P extends ModalfyParams>(): UsableModalProp<P> {
* Alternatively, you can also provide some `params` that will be
* accessible to that component.
*
* @example openModal('PokedexEntryModal', { id: 619, name: 'Lin-Fu' })
* @example openModal('PokedexEntryModal', { id: 619, name: 'Lin-Fu' }, () => console.log('PokedexEntryModal modal opened'))
*
* @see https://colorfy-software.gitbook.io/react-native-modalfy/api/types/modalprop#openmodal
*/
Expand Down
24 changes: 17 additions & 7 deletions types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ComponentType } from 'react'
import { ComponentType, Dispatch } from 'react'
import { Animated, ViewStyle } from 'react-native'

/* ======================== ========================
Expand Down Expand Up @@ -100,15 +100,19 @@ export interface ModalContextProvider<
>
> {
currentModal: M | null
closeAllModals: () => void
closeModal: (stackItem?: M | ModalStackItem<P>) => void
closeModals: (modalName: M) => boolean
closeAllModals: (callback?: () => void) => void
closeModal: (stackItem?: M | ModalStackItem<P>, callback?: () => void) => void
closeModals: (modalName: M, callback?: () => void) => boolean
getParam: <N extends keyof P[M], D extends P[M][N]>(
hash: ModalStackItem<P>['hash'],
paramName: N,
defaultValue?: D,
) => D extends P[M][N] ? P[M][N] : undefined
openModal: <N extends M>(modalName: N, params?: P[N]) => void
openModal: <N extends M>(
modalName: N,
params?: P[N],
callback?: () => void,
) => void
stack: ModalStack<P>
}

Expand Down Expand Up @@ -154,6 +158,7 @@ export type ModalState<P> = Omit<
modalName: N,
params?: P[N],
isCalledOutsideOfContext?: boolean,
callback?: () => void,
) => void
handleBackPress(): boolean
init: <P>(newState: {
Expand Down Expand Up @@ -193,15 +198,18 @@ export type UsableModalProp<P extends ModalfyParams> = Pick<
ModalContextProvider<P>,
'closeAllModals' | 'closeModals' | 'currentModal' | 'openModal'
> & {
closeModal: (modalName?: Exclude<keyof P, symbol | number>) => void
closeModal: (
modalName?: Exclude<keyof P, symbol | number>,
callback?: () => void,
) => void
}

export interface UsableModalComponentProp<
P extends ModalfyParams,
M extends keyof P
> extends Omit<ModalContextProvider<P>, 'closeModal' | 'stack' | 'getParam'> {
addListener: ModalListener
closeModal: (modalName?: M) => void
closeModal: (modalName?: M, callback?: () => void) => void
getParam: <N extends keyof P[M], D extends P[M][N]>(
paramName: N,
defaultValue?: D,
Expand Down Expand Up @@ -273,6 +281,7 @@ export interface ModalOptions {
animationIn?: (
animatedValue: Animated.Value,
toValue: number,
callback?: () => void,
) => Animated.CompositeAnimation | void
/**
* Animation configuration used to animate a modal out (underneath other modals or when closing the last one).
Expand Down Expand Up @@ -313,6 +322,7 @@ export interface ModalOptions {
animationOut?: (
animatedValue: Animated.Value,
toValue: number,
callback?: () => void,
) => Animated.CompositeAnimation | void
/**
* How you want the modal stack to behave when users press the backdrop, but also when the physical back button is pressed on Android.
Expand Down

0 comments on commit e802dc1

Please sign in to comment.