Skip to content

Commit 866e3bb

Browse files
committed
modal upgrade
1 parent 81ff8ea commit 866e3bb

9 files changed

+125
-31
lines changed

components/Providers.tsx

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

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

78
interface ModalContent {
89
data?: any;
@@ -33,5 +34,9 @@ export default function Providers({ children }) {
3334
showModal,
3435
};
3536

36-
return <ModalContext.Provider value={modalContextValue}>{children}</ModalContext.Provider>;
37+
return <ModalContext.Provider value={modalContextValue}>
38+
<ModalProvider>
39+
{children}
40+
</ModalProvider>
41+
</ModalContext.Provider>;
3742
}

demos/modals/ModalHamburgerMenu.tsx

+15-4
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,23 @@ import * as React from 'react';
55
import { H4 } from '@system/typography';
66

77
import Link from 'next/link';
8+
import OutsideElementEvent from '@root/system/detectors/OutsideElementEvent';
9+
import { useModalV2 } from '@root/system/modals/GlobalModalManagerV2';
810

9-
export default function ModalHamburgerMenu(props) {
11+
export interface ModalHamburgerMenuProps {
12+
content: {
13+
data: {
14+
navItems: { name: string, link: string }[],
15+
},
16+
};
17+
}
18+
19+
export default function ModalHamburgerMenu(props: ModalHamburgerMenuProps) {
1020
const navItems = props.content.data.navItems;
11-
21+
const modal = useModalV2(ModalHamburgerMenu);
22+
1223
return (
13-
<div className={`${styles.hamburgerModal} ${styles.slideIn}`}>
24+
<OutsideElementEvent className={`${styles.hamburgerModal} ${styles.slideIn}`} onOutsideEvent={() => modal.close()}>
1425
{navItems?.map((item) => (
1526
<div key={item.name} className={styles.menuContent}>
1627
{item.link ? (
@@ -22,6 +33,6 @@ export default function ModalHamburgerMenu(props) {
2233
)}
2334
</div>
2435
))}
25-
</div>
36+
</OutsideElementEvent>
2637
);
2738
}

next-env.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
/// <reference types="next/navigation-types/compat/navigation" />
44

55
// NOTE: This file should not be edited
6-
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
6+
// see https://nextjs.org/docs/basic-features/typescript for more information.

pages/examples/components/modals-hamburger-menu.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import GlobalModalManager from '@system/modals/GlobalModalManager';
44
import HamburgerMenuButton from '@system/HamburgerMenuButton';
55
import Navigation from '@system/Navigation';
66
import Page from '@components/Page';
7+
import { ModalsV2 } from '@root/system/modals/GlobalModalManagerV2';
78

89
function ExampleModalsHamburgerMenu(props) {
910
// (@xBalbinus): This populates the navigation in the hamburger menu.
@@ -20,7 +21,7 @@ function ExampleModalsHamburgerMenu(props) {
2021
url="https://wireframes.internet.dev/examples/components/modals-hamburger-menu"
2122
>
2223
<Navigation />
23-
<GlobalModalManager />
24+
<ModalsV2 />
2425
<HamburgerMenuButton navItems={NAV_CONTENT} />
2526
</Page>
2627
);

system/HamburgerMenuButton.tsx

+13-19
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,31 @@ import styles from '@system/HamburgerMenuButton.module.scss';
33
import * as React from 'react';
44
import * as Utilities from '@common/utilities';
55

6-
import { useModal } from '@system/providers/ModalContextProvider';
6+
import ModalHamburgerMenu from '@root/demos/modals/ModalHamburgerMenu';
7+
8+
import { useModalV2 } from './modals/GlobalModalManagerV2';
79

810
export default function HamburgerMenuButton(props) {
9-
const { modalContent, showModal } = useModal();
11+
const modal = useModalV2(ModalHamburgerMenu);
1012

11-
const [menuActive, setMenuActive] = React.useState(false);
1213
const [isAnimating, setIsAnimating] = React.useState(false);
1314

1415
const hamburgerRef = React.useRef<HTMLDivElement>(null);
1516

16-
// (@xBalbinus) The hamburger menu is a modal, and can get closed by other modals
17-
// Handle the case where the hamburger menu is closed by another modal or gets clicked outside of.
18-
React.useEffect(() => {
19-
if (modalContent?.name === 'HAMBURGER_MENU') {
20-
setMenuActive(true);
21-
} else if (modalContent?.name !== 'HAMBURGER_MENU') {
22-
setMenuActive(false);
23-
}
24-
}, [modalContent]);
25-
2617
// (@xBalbinus): The hamburger menu gets closed on click to any outside HTML element if we only use
2718
// onOutsideEvent, we need to check if the click is coming from the hamburger button specifically.
2819
function toggleModal() {
2920
if (isAnimating) return;
3021

3122
setIsAnimating(true);
32-
setMenuActive((prev) => !prev);
33-
showModal({
34-
name: 'HAMBURGER_MENU',
35-
data: { navItems: props.navItems, isActive: menuActive, triggerElement: hamburgerRef.current },
36-
});
23+
24+
if (!modal.isActive) {
25+
modal.open({
26+
content: { data: { navItems: props.navItems } },
27+
});
28+
} else {
29+
modal.close();
30+
}
3731
setTimeout(() => {
3832
setIsAnimating(false);
3933
}, 300);
@@ -43,7 +37,7 @@ export default function HamburgerMenuButton(props) {
4337
<div className={styles.body}>
4438
<div
4539
ref={hamburgerRef}
46-
className={Utilities.classNames(styles.hamburger, { [styles.active]: menuActive })}
40+
className={Utilities.classNames(styles.hamburger, { [styles.active]: modal.isActive })}
4741
onMouseDown={toggleModal}
4842
>
4943
<span className={styles.bar}></span>

system/detectors/OutsideElementEvent.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,15 @@ interface OutsideElementEventProps {
1010
const OutsideElementEvent: React.FC<OutsideElementEventProps> = ({ className, children, onOutsideEvent, style }) => {
1111
const ref = useRef<HTMLDivElement>(null);
1212

13+
// Don't call on the same frame as first render
14+
const [isReady, setIsReady] = React.useState(false);
15+
React.useEffect(() => {
16+
setIsReady(true);
17+
}, []);
18+
1319
const handleOutsideEvent = (event) => {
20+
if (!isReady) return;
21+
1422
if (event.target.hasAttribute('data-detector-ignore-navigation')) {
1523
return;
1624
}
@@ -29,7 +37,7 @@ const OutsideElementEvent: React.FC<OutsideElementEventProps> = ({ className, ch
2937
document.removeEventListener('mousedown', handleOutsideEvent);
3038
document.removeEventListener('touchstart', handleOutsideEvent);
3139
};
32-
}, []);
40+
}, [isReady]);
3341

3442
return (
3543
<div ref={ref} className={className} style={style}>

system/modals/GlobalModalManager.tsx

-4
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,6 @@ export default function GlobalModalManager(props) {
4040
nextModal = <ModalError content={modalContent} onShowModal={showModal} viewer={props.viewer} />;
4141
}
4242

43-
if (modalContent && modalContent.name === 'HAMBURGER_MENU') {
44-
nextModal = <ModalHamburgerMenu content={modalContent} onShowModal={showModal} viewer={props.viewer} />;
45-
}
46-
4743
if (modalContent && modalContent.name === 'INDEX') {
4844
nextModal = <ModalIndex content={modalContent} onShowModal={showModal} />;
4945
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.modalBackground {
2+
position: absolute;
3+
top: 0;
4+
right: 0;
5+
bottom: 0;
6+
left: 0;
7+
background: 1px solid var(--theme-background);
8+
}
+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import styles from './GlobalModalManagerV2.module.scss';
2+
3+
import * as React from 'react';
4+
5+
export interface ModalProviderProps {
6+
children?: React.ReactNode;
7+
}
8+
9+
export interface ModalState {
10+
component: React.FC;
11+
props: object;
12+
}
13+
14+
export interface ModalContextTypeV2 {
15+
activeModal: ModalState | null;
16+
showModal: <T>(component: React.FC<T> | null, props?: T) => void;
17+
}
18+
19+
const defaultModalContext: ModalContextTypeV2 = {
20+
activeModal: null,
21+
showModal: () => {},
22+
};
23+
export const ModalContextV2 = React.createContext(defaultModalContext);
24+
25+
function newModalState(): ModalState | null {
26+
return null;
27+
}
28+
29+
export function ModalProvider({ children }: ModalProviderProps) {
30+
const [activeModal, setActiveModal] = React.useState(newModalState);
31+
32+
const showModal = (component, props) => {
33+
setActiveModal({
34+
component,
35+
props: props || {},
36+
});
37+
};
38+
39+
return <ModalContextV2.Provider value={{ activeModal, showModal }}>{children}</ModalContextV2.Provider>;
40+
}
41+
42+
export function ModalsV2() {
43+
const { activeModal } = React.useContext(ModalContextV2);
44+
45+
const Component = activeModal?.component;
46+
const props = activeModal?.props || {};
47+
48+
return (
49+
Component && (
50+
<div className={styles.modalBackground}>
51+
<Component {...props} />
52+
</div>
53+
)
54+
);
55+
}
56+
57+
export interface ModalHandleV2<T> {
58+
isActive: boolean;
59+
open: (props?: T) => void;
60+
close: () => void;
61+
}
62+
63+
export function useModalV2<T>(component: React.FC<T>): ModalHandleV2<T> {
64+
const { showModal, activeModal } = React.useContext(ModalContextV2);
65+
66+
return {
67+
isActive: component === activeModal?.component,
68+
open: (props) => showModal(component, props),
69+
close: () => showModal(null),
70+
};
71+
}

0 commit comments

Comments
 (0)