@codegouvfr/react-dsfr
Version:
French State Design System React integration library
139 lines (104 loc) • 4.61 kB
text/typescript
"use client";
import { useEffect, useState } from "react";
import { assert } from "tsafe/assert";
import { symToStr } from "tsafe/symToStr";
import { useConstCallback } from "../tools/powerhooks/useConstCallback";
export function useIsModalOpen(
modal: { isOpenedByDefault: boolean; id: string },
callbacks?: {
onConceal?: () => void;
onDisclose?: () => void;
}
): boolean {
const { id, isOpenedByDefault } = modal;
const [isModalOpen, setIsModalOpen] = useState(isOpenedByDefault);
const getCurrentCallbacks = useConstCallback(() => callbacks);
useEffect(() => {
const cleanups: (() => void)[] = [];
const observeDialogHtmlElement = (element: HTMLElement) => {
const onConceal = () => {
setIsModalOpen(false);
setTimeout(() => {
getCurrentCallbacks()?.onConceal?.();
}, 0);
};
const onDisclose = () => {
setIsModalOpen(true);
getCurrentCallbacks()?.onDisclose?.();
};
element.addEventListener("dsfr.conceal", onConceal);
element.addEventListener("dsfr.disclose", onDisclose);
const removeEventListeners = () => {
element.removeEventListener("dsfr.conceal", onConceal);
element.removeEventListener("dsfr.disclose", onDisclose);
};
cleanups.push(removeEventListeners);
const observer = new MutationObserver(mutationsList => {
const isDetached =
mutationsList.find(
mutation => Array.from(mutation.removedNodes).indexOf(element) !== -1
) !== undefined;
if (!isDetached) {
return;
}
cleanups.splice(cleanups.indexOf(removeEventListeners), 1);
observer.disconnect();
cleanups.splice(cleanups.indexOf(disconnectObserver), 1);
setIsModalOpen(false);
observeMountAndObserveDialogHtmlElement({ "isInitialCall": false });
});
observer.observe(document, { childList: true, subtree: true });
const disconnectObserver = () => observer.disconnect();
cleanups.push(disconnectObserver);
};
const observeMountAndObserveDialogHtmlElement = (params: { isInitialCall: boolean }) => {
const { isInitialCall } = params;
if (isInitialCall) {
assert(
!isOpenedByDefault,
[
`The ${id} modal isn't initially mounted,`,
`it's ok but in this case ${symToStr({
isOpenedByDefault
})} must be set to false.`,
"This limitation is to prevent inconsistent state in SSR setups."
].join(" ")
);
}
const observer = new MutationObserver((mutationsList, observer) => {
// Filter the mutations list for nodes that match our criteria
const matchedNode = mutationsList
.map(mutation => Array.from(mutation.addedNodes))
.reduce((acc, curr) => acc.concat(curr), [])
.find(
node =>
node.nodeType === Node.ELEMENT_NODE &&
node.nodeName === "DIALOG" &&
(node as HTMLElement).id === id
);
if (matchedNode === undefined) {
return;
}
observer.disconnect();
cleanups.splice(cleanups.indexOf(disconnectObserver), 1);
observeDialogHtmlElement(matchedNode as HTMLElement);
if (isOpenedByDefault) {
setIsModalOpen(true);
}
});
observer.observe(document, { "childList": true, "subtree": true });
const disconnectObserver = () => observer.disconnect();
cleanups.push(disconnectObserver);
};
const element = document.getElementById(id);
if (element !== null) {
observeDialogHtmlElement(element);
} else {
observeMountAndObserveDialogHtmlElement({ "isInitialCall": true });
}
return () => {
cleanups.forEach(cleanup => cleanup());
};
}, [id]);
return isModalOpen;
}