From 6974f322e929cd89183cf73d6e147a9d90c3f804 Mon Sep 17 00:00:00 2001 From: Thomas Churchman Date: Thu, 18 Jan 2024 15:31:02 +0100 Subject: [PATCH] feat: add a modal confirm dialog hook --- .../src/Components/ModalDialog.tsx | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/astroplant-frontend/src/Components/ModalDialog.tsx b/astroplant-frontend/src/Components/ModalDialog.tsx index c60bef9..0744c7d 100644 --- a/astroplant-frontend/src/Components/ModalDialog.tsx +++ b/astroplant-frontend/src/Components/ModalDialog.tsx @@ -2,7 +2,9 @@ import React, { PropsWithChildren, useCallback, useEffect, + useMemo, useRef, + useState, } from "react"; import style from "./ModalDialog.module.css"; @@ -123,3 +125,95 @@ export function ModalConfirmDialog({ ); } + +export type UseModalConfirmDialogValue = { + element: React.ReactNode; + confirmed: boolean; + trigger: ({ + header, + body, + }: { + header?: string; + body?: React.ReactNode; + }) => Promise; + isOpen: boolean; +}; + +/** + * A hook to imperatively trigger a modal confirm dialog. + * + * Example: + * + * ```tsx + * const { element, trigger } = useModalConfirmDialog(); + * const someCallback = (async () => { + * const confirmed = await trigger({header: "Are you sure?", body: <>Please confirm.}); + * if (confirmed) { + * // do something + * } + * }); + * return <>{element}{otherChildren} + * ``` + */ +export function useModalConfirmDialog(): UseModalConfirmDialogValue { + const [confirmed, setConfirmed] = useState(false); + const [open, setOpen] = useState(false); + const [resolve, setResolve] = useState< + ((value: boolean | PromiseLike) => void) | null + >(null); + + const [content, setContent] = useState<{ + header?: string; + body?: React.ReactNode; + }>({}); + + const trigger = useMemo(() => { + return (content: { header?: string; body?: React.ReactNode }) => { + setConfirmed(false); + setOpen(true); + const p = new Promise((resolve_, _reject) => { + // Use a lambda here, because `setResolve` accepts either new state or + // a function taking state and returning new state. If passed a + // function, `setResolve` calls it. + setResolve(() => resolve_); + }); + setContent(content); + return p; + }; + }, []); + + useEffect(() => { + return () => { + if (resolve !== null) { + // Cancel outstanding request. This is a no-op if resolve was already called. + resolve(false); + } + }; + }, [resolve]); + + const element = ( + { + if (resolve) { + resolve(true); + } + setConfirmed(true); + setOpen(false); + setResolve(null); + }} + onCancel={() => { + if (resolve) { + resolve(false); + } + setOpen(false); + setResolve(null); + }} + header={content.header} + > + {content.body} + + ); + + return { element, confirmed, trigger, isOpen: open }; +}