Skip to content

Commit 3946c36

Browse files
committed
add modal unmount delay
1 parent 866e3bb commit 3946c36

File tree

7 files changed

+113
-51
lines changed

7 files changed

+113
-51
lines changed

.prettierrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"requirePragma": false,
1212
"insertPragma": false,
1313
"proseWrap": "preserve",
14-
"parser": "babel",
14+
"parser": "babel-ts",
1515
"overrides": [
1616
{
1717
"files": "*.js",

components/Providers.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import * as React from 'react';
44

55
import { ModalContext } from '@system/providers/ModalContextProvider';
6-
import { ModalProvider } from '@root/system/modals/GlobalModalManagerV2';
6+
import { ModalProviderV2 } from '@root/system/modals/GlobalModalManagerV2';
77

88
interface ModalContent {
99
data?: any;
@@ -35,8 +35,8 @@ export default function Providers({ children }) {
3535
};
3636

3737
return <ModalContext.Provider value={modalContextValue}>
38-
<ModalProvider>
38+
<ModalProviderV2>
3939
{children}
40-
</ModalProvider>
40+
</ModalProviderV2>
4141
</ModalContext.Provider>;
4242
}

demos/modals/ModalHamburgerMenu.tsx

+13-9
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,26 @@ import styles from '@demos/modals/Modals.module.scss';
22

33
import * as React from 'react';
44

5-
import { H4 } from '@system/typography';
6-
75
import Link from 'next/link';
86
import OutsideElementEvent from '@root/system/detectors/OutsideElementEvent';
9-
import { useModalV2 } from '@root/system/modals/GlobalModalManagerV2';
107

11-
export interface ModalHamburgerMenuProps {
8+
import { H4 } from '@system/typography';
9+
import { CommonModalProps, ModalComponentV2, useModalV2 } from '@root/system/modals/GlobalModalManagerV2';
10+
11+
export interface ModalHamburgerMenuProps extends CommonModalProps {
1212
content: {
1313
data: {
14-
navItems: { name: string, link: string }[],
15-
},
14+
navItems: { name: string; link: string }[];
15+
};
1616
};
1717
}
1818

19-
export default function ModalHamburgerMenu(props: ModalHamburgerMenuProps) {
19+
const ModalHamburgerMenu: ModalComponentV2<ModalHamburgerMenuProps> = (props) => {
2020
const navItems = props.content.data.navItems;
2121
const modal = useModalV2(ModalHamburgerMenu);
2222

2323
return (
24-
<OutsideElementEvent className={`${styles.hamburgerModal} ${styles.slideIn}`} onOutsideEvent={() => modal.close()}>
24+
<OutsideElementEvent className={styles.hamburgerModal} onOutsideEvent={() => modal.close()} style={{ animationDirection: modal.isActive ? 'normal' : 'reverse' }}>
2525
{navItems?.map((item) => (
2626
<div key={item.name} className={styles.menuContent}>
2727
{item.link ? (
@@ -35,4 +35,8 @@ export default function ModalHamburgerMenu(props: ModalHamburgerMenuProps) {
3535
))}
3636
</OutsideElementEvent>
3737
);
38-
}
38+
};
39+
40+
ModalHamburgerMenu.unmountDelayMS = 300;
41+
42+
export default ModalHamburgerMenu;

demos/modals/Modals.module.scss

+6-16
Original file line numberDiff line numberDiff line change
@@ -18,35 +18,25 @@
1818
border-right: 1px solid var(--theme-border);
1919
box-sizing: content-box;
2020
transition: left 0.3s;
21+
animation: slideInAnimation 0.3s;
22+
animation-fill-mode: forwards;
2123

2224
@media (max-width: 960px) {
2325
width: 100%;
2426
}
2527
}
2628

27-
.slideIn {
28-
animation: slideInAnimation 0.3s forwards;
29-
}
30-
31-
.slideOut {
32-
animation: slideOutAnimation 0.3s forwards;
29+
.hamburgerModalClosing {
30+
animation-direction: alternate;
3331
}
3432

3533
@keyframes slideInAnimation {
3634
from {
3735
transform: translateX(-100%);
3836
}
39-
to {
40-
transform: translateX(0);
41-
}
42-
}
4337

44-
@keyframes slideOutAnimation {
45-
from {
46-
transform: translateX(0);
47-
}
4838
to {
49-
transform: translateX(-100%);
39+
transform: translateX(0);
5040
}
5141
}
5242

@@ -174,4 +164,4 @@
174164
.linkStyle {
175165
text-decoration: none;
176166
color: inherit;
177-
}
167+
}

system/HamburgerMenuButton.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ export default function HamburgerMenuButton(props) {
2525
modal.open({
2626
content: { data: { navItems: props.navItems } },
2727
});
28+
console.log('opening');
2829
} else {
2930
modal.close();
31+
console.log('closing');
3032
}
3133
setTimeout(() => {
3234
setIsAnimating(false);

system/detectors/OutsideElementEvent.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ interface OutsideElementEventProps {
44
className?: string;
55
children: React.ReactNode;
66
onOutsideEvent: (event: MouseEvent | TouchEvent) => void;
7-
style?: Record<string, any>;
7+
style?: React.CSSProperties;
88
}
99

1010
const OutsideElementEvent: React.FC<OutsideElementEventProps> = ({ className, children, onOutsideEvent, style }) => {

system/modals/GlobalModalManagerV2.tsx

+87-21
Original file line numberDiff line numberDiff line change
@@ -6,51 +6,111 @@ export interface ModalProviderProps {
66
children?: React.ReactNode;
77
}
88

9+
export interface ModalComponentV2<P extends CommonModalProps = CommonModalProps> extends React.FC<P> {
10+
unmountDelayMS?: number;
11+
}
12+
913
export interface ModalState {
10-
component: React.FC;
14+
key: string;
15+
component: ModalComponentV2;
1116
props: object;
1217
}
1318

19+
export interface CommonModalProps {
20+
isClosing?: boolean;
21+
}
22+
1423
export interface ModalContextTypeV2 {
1524
activeModal: ModalState | null;
16-
showModal: <T>(component: React.FC<T> | null, props?: T) => void;
25+
closingModals: { [key: string]: ModalState };
26+
showModal: <P extends CommonModalProps>(key: string, component: ModalComponentV2<P> | null, props?: P) => void;
27+
hideCurrentModal: () => void;
28+
hideModal: (key: string) => void;
1729
}
1830

1931
const defaultModalContext: ModalContextTypeV2 = {
2032
activeModal: null,
33+
closingModals: {},
2134
showModal: () => {},
35+
hideCurrentModal: () => {},
36+
hideModal: () => {},
2237
};
2338
export const ModalContextV2 = React.createContext(defaultModalContext);
2439

2540
function newModalState(): ModalState | null {
2641
return null;
2742
}
2843

29-
export function ModalProvider({ children }: ModalProviderProps) {
44+
/** Provides a context that allows any of its descendants to control modals via
45+
* `useModalV2()`. Must be paired with `<ModalsV2 />` in order for modals to
46+
* show up. */
47+
export function ModalProviderV2({ children }: ModalProviderProps) {
3048
const [activeModal, setActiveModal] = React.useState(newModalState);
49+
const [closingModals, setClosingModals] = React.useState<{ [key: string]: ModalState }>({});
50+
51+
const showModal = (key, component, props) => {
52+
// If a modal was previously active, remove it now, or set a timeout for it
53+
// if there is an unmount delay.
54+
hideCurrentModal();
55+
56+
if (component) {
57+
setActiveModal({
58+
key,
59+
component,
60+
props: props || {},
61+
});
62+
} else {
63+
setActiveModal(null);
64+
}
65+
};
66+
67+
const hideCurrentModal = () => {
68+
if (activeModal) {
69+
setActiveModal(null);
70+
setClosingModals((prev) => ({ ...prev, [activeModal.key]: activeModal }));
3171

32-
const showModal = (component, props) => {
33-
setActiveModal({
34-
component,
35-
props: props || {},
36-
});
72+
const timeout = activeModal.component.unmountDelayMS || 0;
73+
const callback = () => {
74+
setClosingModals((prev) => {
75+
const { [activeModal.key]: _, ...filtered } = prev;
76+
return filtered;
77+
});
78+
};
79+
80+
if (timeout) {
81+
setTimeout(callback, timeout);
82+
} else {
83+
callback();
84+
}
85+
}
86+
};
87+
88+
const hideModal = (key) => {
89+
if (activeModal && activeModal.key === key) {
90+
hideCurrentModal();
91+
}
3792
};
3893

39-
return <ModalContextV2.Provider value={{ activeModal, showModal }}>{children}</ModalContextV2.Provider>;
94+
return <ModalContextV2.Provider value={{ activeModal, closingModals, showModal, hideCurrentModal, hideModal }}>{children}</ModalContextV2.Provider>;
4095
}
4196

97+
/** Displays the active modal. Without this component, the modal context does
98+
* nothing. */
4299
export function ModalsV2() {
43-
const { activeModal } = React.useContext(ModalContextV2);
100+
const { activeModal, closingModals } = React.useContext(ModalContextV2);
101+
102+
const renderModal = (state: ModalState, isClosing: boolean) => {
103+
const Component: React.FC<CommonModalProps> = state?.component;
104+
const props = state?.props || {};
44105

45-
const Component = activeModal?.component;
46-
const props = activeModal?.props || {};
106+
return <Component key={state.key} isClosing={isClosing} {...props} />;
107+
};
47108

48109
return (
49-
Component && (
50-
<div className={styles.modalBackground}>
51-
<Component {...props} />
52-
</div>
53-
)
110+
<div className={styles.modalBackground}>
111+
{activeModal && renderModal(activeModal, false)}
112+
{Object.values(closingModals).map((closingModal) => renderModal(closingModal, true))}
113+
</div>
54114
);
55115
}
56116

@@ -60,12 +120,18 @@ export interface ModalHandleV2<T> {
60120
close: () => void;
61121
}
62122

63-
export function useModalV2<T>(component: React.FC<T>): ModalHandleV2<T> {
64-
const { showModal, activeModal } = React.useContext(ModalContextV2);
123+
export function useModalV2<T extends CommonModalProps>(component: React.FC<T>): ModalHandleV2<T> {
124+
const { showModal, hideModal, hideCurrentModal, activeModal } = React.useContext(ModalContextV2);
125+
126+
const key = React.useMemo(() => uniqueModalKey(), []);
65127

66128
return {
67129
isActive: component === activeModal?.component,
68-
open: (props) => showModal(component, props),
69-
close: () => showModal(null),
130+
open: (props) => showModal(key, component, props),
131+
close: () => hideModal(key),
70132
};
71133
}
134+
135+
function uniqueModalKey(): string {
136+
return `modal-${Math.floor(Math.random() * 999999999)}`;
137+
}

0 commit comments

Comments
 (0)