Skip to content

Commit 80d6401

Browse files
committed
Add a hook for observing if a modal is open
1 parent e006c55 commit 80d6401

File tree

6 files changed

+65
-14
lines changed

6 files changed

+65
-14
lines changed

src/Modal.tsx renamed to src/Modal/Modal.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import React, { memo, forwardRef, type CSSProperties, type ReactNode } from "react";
2-
import { fr } from "./fr";
3-
import { cx } from "./tools/cx";
2+
import { fr } from "../fr";
3+
import { cx } from "../tools/cx";
44
import { assert } from "tsafe/assert";
55
import { symToStr } from "tsafe/symToStr";
66
import type { Equals } from "tsafe";
7-
import { createComponentI18nApi } from "./i18n";
8-
import type { FrIconClassName, RiIconClassName } from "./fr/generatedFromCss/classNames";
9-
import Button, { ButtonProps } from "./Button";
7+
import { createComponentI18nApi } from "../i18n";
8+
import type { FrIconClassName, RiIconClassName } from "../fr/generatedFromCss/classNames";
9+
import Button, { ButtonProps } from "../Button";
1010
import { typeGuard } from "tsafe/typeGuard";
1111
import { overwriteReadonlyProp } from "tsafe/lab/overwriteReadonlyProp";
1212

@@ -24,6 +24,7 @@ export type ModalProps = {
2424
| [ModalProps.ActionAreaButtonProps, ...ModalProps.ActionAreaButtonProps[]]
2525
| ModalProps.ActionAreaButtonProps;
2626
style?: CSSProperties;
27+
onClose?: () => void;
2728
};
2829

2930
export namespace ModalProps {
@@ -53,6 +54,7 @@ const Modal = memo(
5354
buttons: buttons_props,
5455
size = "medium",
5556
style,
57+
onClose,
5658
...rest
5759
} = props;
5860

@@ -76,6 +78,7 @@ const Modal = memo(
7678
style={style}
7779
ref={ref}
7880
data-fr-concealing-backdrop={concealingBackdrop}
81+
onClose={onClose}
7982
>
8083
<div className={fr.cx("fr-container", "fr-container--fluid", "fr-container-md")}>
8184
<div className={fr.cx("fr-grid-row", "fr-grid-row--center")}>
@@ -205,6 +208,8 @@ export function createModal(params: { isOpenedByDefault: boolean; id: string }):
205208
Component: (props: ModalProps) => JSX.Element;
206209
close: () => void;
207210
open: () => void;
211+
isOpenedByDefault: boolean;
212+
id: string;
208213
} {
209214
const { isOpenedByDefault, id } = params;
210215

@@ -266,6 +271,8 @@ export function createModal(params: { isOpenedByDefault: boolean; id: string }):
266271
Component,
267272
buttonProps,
268273
open,
269-
close
274+
close,
275+
isOpenedByDefault,
276+
id
270277
};
271278
}

src/Modal/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./Modal";

src/Modal/useIsModalOpen.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { assert } from "tsafe/assert";
5+
6+
export function useIsModalOpen(modal: { isOpenedByDefault: boolean; id: string }): boolean {
7+
const { id, isOpenedByDefault } = modal;
8+
9+
const [isModalOpen, setIsModalOpen] = useState(isOpenedByDefault);
10+
11+
useEffect(() => {
12+
const element = document.getElementById(id);
13+
14+
assert(element !== null, `The ${id} modal isn't mounted`);
15+
16+
const onConceal = () => setIsModalOpen(false);
17+
18+
element.addEventListener("dsfr.conceal", onConceal);
19+
20+
const onDisclose = () => setIsModalOpen(true);
21+
22+
element.addEventListener("dsfr.disclose", onDisclose);
23+
24+
return () => {
25+
element.removeEventListener("dsfr.conceal", onConceal);
26+
element.removeEventListener("dsfr.disclose", onDisclose);
27+
};
28+
}, [id]);
29+
30+
return isModalOpen;
31+
}

src/useIsDark/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ function getCurrentIsDarkFromHtmlAttribute(): boolean | undefined {
9191
return true;
9292
}
9393

94-
assert(false);
94+
assert(false, `Unrecognized ${data_fr_theme} attribute value: ${colorSchemeFromHtmlAttribute}`);
9595
}
9696

9797
export function startClientSideIsDarkLogic(params: {

stories/Modal.stories.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@ const { meta, getStory } = getStoryFactory({
1414
- [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/modale)
1515
- [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Modal.tsx)
1616
17-
**Eg.:**
18-
1917
\`\`\`tsx
18+
"use client";
19+
// NOTE for Next App Router: As long as you avoid using the useIsModalOpen hook and use
20+
// modal.buttonProps instead of modal.open() the Modal component can be used as a
21+
// server component (you can remove "use client";)
22+
2023
import { createModal } from "@codegouvfr/react-dsfr/Modal";
24+
import { useIsModalOpen } from "@codegouvfr/react-dsfr/Modal/useIsModalOpen";
2125
import { Button } from "@codegouvfr/react-dsfr/Button";
2226
2327
const modal = createModal({
@@ -26,23 +30,26 @@ const modal = createModal({
2630
});
2731
2832
function Home(){
33+
34+
const isOpen = useIsModalOpen(modal);
35+
36+
console.log(\`Modal is currently: \${isOpen ? "open" : "closed"}\`);
37+
2938
return (
3039
<>
3140
{/* ... */}
3241
<modal.Component title="foo modal title">
3342
<h1>Foo modal content</h1>
3443
</modal.Component>
35-
<Button nativeButtonProps={fooModal.buttonProps}>Open foo modal</Button> {/* Use this if you are in a Server component (Next AppDir) and you don't want to add "use client"; just because of the the Modal */}
36-
<Button onClick={fooModal.open}>Open foo modal</Button> {/* ...otherwise this works just as well and is more versatile */}
37-
<Button onClick={fooModal.close}>Close foo modal</Button>
44+
<Button nativeButtonProps={modal.buttonProps}>Open foo modal</Button> {/* Use this if you are in Next App Dir and you don't want to label the "use client"; the component hosting the Modal. */}
45+
<Button onClick={()=> modal.open()}>Open foo modal</Button> {/* ...otherwise modal.open() works just as well and is more versatile */}
46+
<Button onClick={()=> modal.close()}>Close foo modal</Button>
3847
</>
3948
);
4049
);
4150
4251
\`\`\`
4352
44-
## The Modal component
45-
4653
`,
4754
"argTypes": {
4855
"title": {

test/integration/next-appdir/ui/ClientComponent.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@ import Stack from '@mui/material/Stack';
55
import Button from '@mui/material/Button';
66
import { useIsDark } from "@codegouvfr/react-dsfr/useIsDark";
77
import { createModal } from "@codegouvfr/react-dsfr/Modal";
8+
import { useIsModalOpen } from "@codegouvfr/react-dsfr/Modal/useIsModalOpen";
89

910
export function ClientComponent() {
1011

1112
const { isDark } = useIsDark();
1213

14+
const isModalOpen = useIsModalOpen(modal);
15+
16+
console.log(`Modal ${modal.id} is currently: ${isModalOpen ? "open" : "closed"}`);
17+
1318
return (
1419
<>
1520
<Stack spacing={2} direction="row" sx={{ mt: 5 }}>

0 commit comments

Comments
 (0)