From cb847616888206d7f7b099e818b5a0d2bb8c0d2f Mon Sep 17 00:00:00 2001 From: Allan Galdino Date: Thu, 23 Nov 2023 15:23:59 -0300 Subject: [PATCH 1/7] feat: add inModal state on useModal --- src/components/Modal/Modal.stories.tsx | 19 +++++++++++++++++ src/components/Modal/useModal.test.tsx | 29 ++++++++++++++++++++++++++ src/components/Modal/useModal.tsx | 4 +++- 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/components/Modal/Modal.stories.tsx b/src/components/Modal/Modal.stories.tsx index 9812f8543..4e0b9b10f 100644 --- a/src/components/Modal/Modal.stories.tsx +++ b/src/components/Modal/Modal.stories.tsx @@ -8,6 +8,7 @@ import { TestModalFilterTable, VirtualizedTable, } from "src/components/Modal/TestModalContent"; +import { Css } from "src/Css"; import { FormStateApp } from "src/forms/FormStateApp"; import { noop } from "src/utils/index"; import { withBeamDecorator, withDimensions } from "src/utils/sb"; @@ -111,6 +112,24 @@ export function ModalForm() { ); } +export function InModalState() { + const { openModal, inModal } = useModal(); + const open = () => + openModal({ + content: , + size: { width: "md", height: 600 }, + }); + + return ( +
+
+
+ Open: {inModal() ? "YES" : "NO"} +
+ ); +} + interface ModalExampleProps extends Pick, TestModalContentProps {} diff --git a/src/components/Modal/useModal.test.tsx b/src/components/Modal/useModal.test.tsx index 8ad60c07b..6af4f1a97 100644 --- a/src/components/Modal/useModal.test.tsx +++ b/src/components/Modal/useModal.test.tsx @@ -95,4 +95,33 @@ describe("useModal", () => { // And the BeamContext has been cleared expect(beamContext!.modalCanCloseChecks.current).toEqual([]); }); + + it("can identify when modal is open/closed", async () => { + // Given a ModalContext + let context: UseModalHook; + const modalProps = { title: "Test", content:
Test
}; + + function TestApp() { + context = useModal(); + return
; + } + + // When rendering a component without a modal defined + await render(); + + const { openModal, closeModal, inModal } = context!; + + // Then expect inModal returns false + expect(inModal()).toBeFalsy(); + + // When opening the modal via the context method + act(() => openModal(modalProps)); + // Then expect inModal returns true + expect(inModal()).toBeTruthy(); + + // When closing the modal via the context method + act(() => closeModal()); + // Then expect inModal returns false + expect(inModal()).toBeFalsy(); + }); }); diff --git a/src/components/Modal/useModal.tsx b/src/components/Modal/useModal.tsx index 2a7a43eba..d9d2563f4 100644 --- a/src/components/Modal/useModal.tsx +++ b/src/components/Modal/useModal.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useRef } from "react"; import { useBeamContext } from "src/components/BeamContext"; import { CheckFn } from "src/types"; -import { maybeCall } from "src/utils"; +import { isDefined, maybeCall } from "src/utils"; import { ModalApi, ModalProps } from "./Modal"; export interface UseModalHook { @@ -9,6 +9,7 @@ export interface UseModalHook { closeModal: VoidFunction; addCanClose: (canClose: CheckFn) => void; setSize: (size: ModalProps["size"]) => void; + inModal: () => boolean; } export function useModal(): UseModalHook { @@ -51,6 +52,7 @@ export function useModal(): UseModalHook { modalState.current.api.current.setSize(size); } }, + inModal: () => isDefined(modalState.current), }), [modalState, modalCanCloseChecks], ); From 3afcc4525f72511238009e9fbb7ed4faa07bac14 Mon Sep 17 00:00:00 2001 From: Allan Galdino Date: Wed, 29 Nov 2023 12:17:35 -0300 Subject: [PATCH 2/7] create modal context and wrap modal component --- src/components/BeamContext.tsx | 5 ++-- src/components/Modal/Modal.stories.tsx | 2 +- src/components/Modal/ModalContext.tsx | 37 ++++++++++++++++++++++++++ src/components/Modal/useModal.tsx | 9 ++++--- 4 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 src/components/Modal/ModalContext.tsx diff --git a/src/components/BeamContext.tsx b/src/components/BeamContext.tsx index b45111dad..850acdc29 100644 --- a/src/components/BeamContext.tsx +++ b/src/components/BeamContext.tsx @@ -7,9 +7,10 @@ import { SnackbarProvider } from "src/components/Snackbar/SnackbarContext"; import { SuperDrawer } from "src/components/SuperDrawer/SuperDrawer"; import { ContentStack } from "src/components/SuperDrawer/useSuperDrawer"; import { CanCloseCheck, CheckFn } from "src/types"; -import { EmptyRef } from "src/utils/index"; +import { EmptyRef, isDefined } from "src/utils/index"; import { RightPaneProvider } from "./Layout"; import { ToastProvider } from "./Toast/ToastContext"; +import { ModalContext, ModalProvider } from "./Modal/ModalContext"; /** The internal state of our Beam context; see useModal and useSuperDrawer for the public APIs. */ export interface BeamContextState { @@ -100,7 +101,7 @@ export function BeamProvider({ children, ...presentationProps }: BeamProviderPro {children} - {modalRef.current && } + {modalRef.current && } diff --git a/src/components/Modal/Modal.stories.tsx b/src/components/Modal/Modal.stories.tsx index 4e0b9b10f..ee2afbe6a 100644 --- a/src/components/Modal/Modal.stories.tsx +++ b/src/components/Modal/Modal.stories.tsx @@ -125,7 +125,7 @@ export function InModalState() {
- Open: {inModal() ? "YES" : "NO"} + Open: {inModal ? "YES" : "NO"}
); } diff --git a/src/components/Modal/ModalContext.tsx b/src/components/Modal/ModalContext.tsx new file mode 100644 index 000000000..1dff40e92 --- /dev/null +++ b/src/components/Modal/ModalContext.tsx @@ -0,0 +1,37 @@ +// ModalContext.tsx + +import React, { createContext, useContext, useState } from "react"; +import { Modal, ModalProps } from "./Modal"; + +// Define the type for the modal context +interface ModalContextState { + inModal: boolean; +} + +// Create a context for the modal state +export const ModalContext = createContext({ inModal: false }); + +// Create a provider component to wrap your app and provide the modal state +interface ModalProviderProps { + modalProps: ModalProps; +} + +export function ModalProvider({ modalProps }: ModalProviderProps) { + const [inModal, setInModal] = useState(true); + return ( + + { + setInModal(false); + modalProps.onClose?.(); + }} + /> + + ); +} + +// Create a custom hook to conveniently access the modal context +export function useModalContext(): ModalContextState { + return useContext(ModalContext); +} diff --git a/src/components/Modal/useModal.tsx b/src/components/Modal/useModal.tsx index d9d2563f4..7f4bb41e0 100644 --- a/src/components/Modal/useModal.tsx +++ b/src/components/Modal/useModal.tsx @@ -3,17 +3,20 @@ import { useBeamContext } from "src/components/BeamContext"; import { CheckFn } from "src/types"; import { isDefined, maybeCall } from "src/utils"; import { ModalApi, ModalProps } from "./Modal"; +import { useModalContext } from "./ModalContext"; export interface UseModalHook { openModal: (props: ModalProps) => void; closeModal: VoidFunction; addCanClose: (canClose: CheckFn) => void; setSize: (size: ModalProps["size"]) => void; - inModal: () => boolean; + inModal: boolean; } export function useModal(): UseModalHook { const { modalState, modalCanCloseChecks } = useBeamContext(); + const { inModal } = useModalContext(); + console.log("useModal", { inModal }); const lastCanClose = useRef(); const api = useRef(); useEffect(() => { @@ -52,8 +55,8 @@ export function useModal(): UseModalHook { modalState.current.api.current.setSize(size); } }, - inModal: () => isDefined(modalState.current), + inModal, }), - [modalState, modalCanCloseChecks], + [inModal, modalState, modalCanCloseChecks], ); } From e3bd209425ad07a78ca82e9af0ddcb1a45e08ce8 Mon Sep 17 00:00:00 2001 From: Brandon Dow Date: Fri, 1 Dec 2023 09:24:29 -0500 Subject: [PATCH 3/7] Small refactor --- src/components/BeamContext.tsx | 35 ++++++++++++--------------- src/components/Modal/ModalContext.tsx | 14 +++-------- src/components/Modal/useModal.tsx | 3 +-- 3 files changed, 20 insertions(+), 32 deletions(-) diff --git a/src/components/BeamContext.tsx b/src/components/BeamContext.tsx index 850acdc29..2121ed6e8 100644 --- a/src/components/BeamContext.tsx +++ b/src/components/BeamContext.tsx @@ -70,26 +70,21 @@ export function BeamProvider({ children, ...presentationProps }: BeamProviderPro // We essentially expose the refs, but with our own getters/setters so that we can // have the setters call `tick` to re-render this Provider - const context = useMemo( - () => { - return { - // These two keys need to trigger re-renders on change - modalState: new PretendRefThatTicks(modalRef, tick), - drawerContentStack: new PretendRefThatTicks(drawerContentStackRef, tick), - // The rest we don't need to re-render when these are mutated, so just expose as-is - modalCanCloseChecks: modalCanCloseChecksRef, - modalHeaderDiv, - modalBodyDiv, - modalFooterDiv, - drawerCanCloseChecks, - drawerCanCloseDetailsChecks, - sdHeaderDiv, - }; - }, - // TODO: validate this eslint-disable. It was automatically ignored as part of https://app.shortcut.com/homebound-team/story/40033/enable-react-hooks-exhaustive-deps-for-react-projects - // eslint-disable-next-line react-hooks/exhaustive-deps - [modalBodyDiv, modalFooterDiv], - ); + const context = useMemo(() => { + return { + // These two keys need to trigger re-renders on change + modalState: new PretendRefThatTicks(modalRef, tick), + drawerContentStack: new PretendRefThatTicks(drawerContentStackRef, tick), + // The rest we don't need to re-render when these are mutated, so just expose as-is + modalCanCloseChecks: modalCanCloseChecksRef, + modalHeaderDiv, + modalBodyDiv, + modalFooterDiv, + drawerCanCloseChecks, + drawerCanCloseDetailsChecks, + sdHeaderDiv, + }; + }, [modalBodyDiv, modalFooterDiv, modalHeaderDiv, sdHeaderDiv]); return ( diff --git a/src/components/Modal/ModalContext.tsx b/src/components/Modal/ModalContext.tsx index 1dff40e92..e96da1da4 100644 --- a/src/components/Modal/ModalContext.tsx +++ b/src/components/Modal/ModalContext.tsx @@ -1,6 +1,6 @@ // ModalContext.tsx -import React, { createContext, useContext, useState } from "react"; +import React, { createContext, useContext, useMemo, useState } from "react"; import { Modal, ModalProps } from "./Modal"; // Define the type for the modal context @@ -17,16 +17,10 @@ interface ModalProviderProps { } export function ModalProvider({ modalProps }: ModalProviderProps) { - const [inModal, setInModal] = useState(true); + const value = useMemo(() => ({ inModal: true }), []); return ( - - { - setInModal(false); - modalProps.onClose?.(); - }} - /> + + ); } diff --git a/src/components/Modal/useModal.tsx b/src/components/Modal/useModal.tsx index 7f4bb41e0..a43161417 100644 --- a/src/components/Modal/useModal.tsx +++ b/src/components/Modal/useModal.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useRef } from "react"; import { useBeamContext } from "src/components/BeamContext"; import { CheckFn } from "src/types"; -import { isDefined, maybeCall } from "src/utils"; +import { maybeCall } from "src/utils"; import { ModalApi, ModalProps } from "./Modal"; import { useModalContext } from "./ModalContext"; @@ -16,7 +16,6 @@ export interface UseModalHook { export function useModal(): UseModalHook { const { modalState, modalCanCloseChecks } = useBeamContext(); const { inModal } = useModalContext(); - console.log("useModal", { inModal }); const lastCanClose = useRef(); const api = useRef(); useEffect(() => { From 1432327218adb5fa3baf77557e3f4f3dc452e872 Mon Sep 17 00:00:00 2001 From: Allan Galdino Date: Fri, 1 Dec 2023 12:08:27 -0300 Subject: [PATCH 4/7] revert BeamContext changes + refact ModalProvider to wrap Modal content --- src/components/BeamContext.tsx | 2 +- src/components/Modal/Modal.stories.tsx | 2 +- src/components/Modal/Modal.tsx | 83 ++++++++++++----------- src/components/Modal/ModalContext.tsx | 19 ++---- src/components/Modal/TestModalContent.tsx | 3 +- 5 files changed, 51 insertions(+), 58 deletions(-) diff --git a/src/components/BeamContext.tsx b/src/components/BeamContext.tsx index 2121ed6e8..f0b791999 100644 --- a/src/components/BeamContext.tsx +++ b/src/components/BeamContext.tsx @@ -96,7 +96,7 @@ export function BeamProvider({ children, ...presentationProps }: BeamProviderPro {children} - {modalRef.current && } + {modalRef.current && } diff --git a/src/components/Modal/Modal.stories.tsx b/src/components/Modal/Modal.stories.tsx index ee2afbe6a..c736e2564 100644 --- a/src/components/Modal/Modal.stories.tsx +++ b/src/components/Modal/Modal.stories.tsx @@ -125,7 +125,7 @@ export function InModalState() {
- Open: {inModal ? "YES" : "NO"} + In Modal: {inModal ? "YES" : "NO"} ); } diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index f44ebfc76..55f57318f 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -8,6 +8,7 @@ import { IconButton } from "src/components/IconButton"; import { useModal as ourUseModal } from "src/components/Modal/useModal"; import { Css, Only, Xss } from "src/Css"; import { useTestIds } from "src/utils"; +import { ModalProvider } from "./ModalContext"; export type ModalSize = "sm" | "md" | "lg" | "xl" | "xxl"; @@ -103,47 +104,49 @@ export function Modal(props: ModalProps) { ); return ( - - -
- -
- {/* Setup three children (header, content, footer), and flex grow the content. */} -
-

- - - -

-
+ + +
+ +
- {/* We'll include content here, but we expect ModalBody and ModalFooter to use their respective portals. */} - {content} -
-
-
-
-
-
-
-
-
+ {/* Setup three children (header, content, footer), and flex grow the content. */} +
+

+ + + +

+
+ {/* We'll include content here, but we expect ModalBody and ModalFooter to use their respective portals. */} + {content} +
+
+
+
+ + + + + + ); } diff --git a/src/components/Modal/ModalContext.tsx b/src/components/Modal/ModalContext.tsx index e96da1da4..a6ae167e1 100644 --- a/src/components/Modal/ModalContext.tsx +++ b/src/components/Modal/ModalContext.tsx @@ -1,31 +1,20 @@ -// ModalContext.tsx +import { ReactNode, createContext, useContext, useMemo } from "react"; -import React, { createContext, useContext, useMemo, useState } from "react"; -import { Modal, ModalProps } from "./Modal"; - -// Define the type for the modal context interface ModalContextState { inModal: boolean; } -// Create a context for the modal state export const ModalContext = createContext({ inModal: false }); -// Create a provider component to wrap your app and provide the modal state interface ModalProviderProps { - modalProps: ModalProps; + children: ReactNode; } -export function ModalProvider({ modalProps }: ModalProviderProps) { +export function ModalProvider({ children }: ModalProviderProps) { const value = useMemo(() => ({ inModal: true }), []); - return ( - - - - ); + return {children}; } -// Create a custom hook to conveniently access the modal context export function useModalContext(): ModalContextState { return useContext(ModalContext); } diff --git a/src/components/Modal/TestModalContent.tsx b/src/components/Modal/TestModalContent.tsx index 97fb4f1d1..e817f4fd2 100644 --- a/src/components/Modal/TestModalContent.tsx +++ b/src/components/Modal/TestModalContent.tsx @@ -22,7 +22,7 @@ export interface TestModalContentProps { /** A fake modal content component that we share across the modal and superdrawer stories. */ export function TestModalContent(props: TestModalContentProps) { - const { closeModal } = useModal(); + const { closeModal, inModal } = useModal(); const { initNumSentences = 1, showLeftAction, withDateField } = props; const [numSentences, setNumSentences] = useState(initNumSentences); const [primaryDisabled, setPrimaryDisabled] = useState(false); @@ -66,6 +66,7 @@ export function TestModalContent(props: TestModalContentProps) { )}

{"The body content of the modal. This content can be anything!".repeat(numSentences)}

+

In Modal? {inModal ? "YES" : "NO"}

{withDateField && } From d6679131d154353ac3854be450bfbb95c9cd20dc Mon Sep 17 00:00:00 2001 From: Allan Galdino Date: Fri, 1 Dec 2023 12:27:02 -0300 Subject: [PATCH 5/7] update test --- src/components/Modal/useModal.test.tsx | 40 +++++++++++--------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/src/components/Modal/useModal.test.tsx b/src/components/Modal/useModal.test.tsx index 6af4f1a97..53e382dad 100644 --- a/src/components/Modal/useModal.test.tsx +++ b/src/components/Modal/useModal.test.tsx @@ -96,32 +96,26 @@ describe("useModal", () => { expect(beamContext!.modalCanCloseChecks.current).toEqual([]); }); - it("can identify when modal is open/closed", async () => { - // Given a ModalContext - let context: UseModalHook; - const modalProps = { title: "Test", content:
Test
}; - - function TestApp() { - context = useModal(); - return
; + it("can identify when component is In Modal", async () => { + // Given a test app that opens a modal with content that checks if it is in a modal + function TestApp(props: ModalProps) { + const { openModal, inModal } = useModal(); + useEffect(() => openModal(props), [openModal, props]); + return
Behind Modal: InModal? {String(inModal)}
; } - // When rendering a component without a modal defined - await render(); - - const { openModal, closeModal, inModal } = context!; - - // Then expect inModal returns false - expect(inModal()).toBeFalsy(); + // And a modal content that checks if it is in a modal also + function TestModalContent() { + const { inModal } = useModal(); + return
Modal Content: InModal? {String(inModal)}
; + } - // When opening the modal via the context method - act(() => openModal(modalProps)); - // Then expect inModal returns true - expect(inModal()).toBeTruthy(); + // When rendering the test app + const r = await render(} />); - // When closing the modal via the context method - act(() => closeModal()); - // Then expect inModal returns false - expect(inModal()).toBeFalsy(); + // Then the test app should not be in a modal + expect(r.testApp).toHaveTextContent("Behind Modal: InModal? false"); + // And the modal content should be in a modal + expect(r.modalContent).toHaveTextContent("Modal Content: InModal? true"); }); }); From 61c2ba2b181650f9949f3476f156a9baf79abc45 Mon Sep 17 00:00:00 2001 From: Allan Galdino Date: Fri, 1 Dec 2023 14:45:32 -0300 Subject: [PATCH 6/7] feedback PR: clean up --- src/components/BeamContext.tsx | 3 +-- src/components/Modal/Modal.stories.tsx | 19 ------------------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/src/components/BeamContext.tsx b/src/components/BeamContext.tsx index f0b791999..0679e89cc 100644 --- a/src/components/BeamContext.tsx +++ b/src/components/BeamContext.tsx @@ -7,10 +7,9 @@ import { SnackbarProvider } from "src/components/Snackbar/SnackbarContext"; import { SuperDrawer } from "src/components/SuperDrawer/SuperDrawer"; import { ContentStack } from "src/components/SuperDrawer/useSuperDrawer"; import { CanCloseCheck, CheckFn } from "src/types"; -import { EmptyRef, isDefined } from "src/utils/index"; +import { EmptyRef } from "src/utils/index"; import { RightPaneProvider } from "./Layout"; import { ToastProvider } from "./Toast/ToastContext"; -import { ModalContext, ModalProvider } from "./Modal/ModalContext"; /** The internal state of our Beam context; see useModal and useSuperDrawer for the public APIs. */ export interface BeamContextState { diff --git a/src/components/Modal/Modal.stories.tsx b/src/components/Modal/Modal.stories.tsx index c736e2564..9812f8543 100644 --- a/src/components/Modal/Modal.stories.tsx +++ b/src/components/Modal/Modal.stories.tsx @@ -8,7 +8,6 @@ import { TestModalFilterTable, VirtualizedTable, } from "src/components/Modal/TestModalContent"; -import { Css } from "src/Css"; import { FormStateApp } from "src/forms/FormStateApp"; import { noop } from "src/utils/index"; import { withBeamDecorator, withDimensions } from "src/utils/sb"; @@ -112,24 +111,6 @@ export function ModalForm() { ); } -export function InModalState() { - const { openModal, inModal } = useModal(); - const open = () => - openModal({ - content: , - size: { width: "md", height: 600 }, - }); - - return ( -
-
-
- In Modal: {inModal ? "YES" : "NO"} -
- ); -} - interface ModalExampleProps extends Pick, TestModalContentProps {} From 9018a0dd2ab15401f9a5c0148f45d1753c17a045 Mon Sep 17 00:00:00 2001 From: Allan Galdino Date: Fri, 1 Dec 2023 14:51:01 -0300 Subject: [PATCH 7/7] feedback PR: clean up test modal component --- src/components/Modal/TestModalContent.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Modal/TestModalContent.tsx b/src/components/Modal/TestModalContent.tsx index e817f4fd2..97fb4f1d1 100644 --- a/src/components/Modal/TestModalContent.tsx +++ b/src/components/Modal/TestModalContent.tsx @@ -22,7 +22,7 @@ export interface TestModalContentProps { /** A fake modal content component that we share across the modal and superdrawer stories. */ export function TestModalContent(props: TestModalContentProps) { - const { closeModal, inModal } = useModal(); + const { closeModal } = useModal(); const { initNumSentences = 1, showLeftAction, withDateField } = props; const [numSentences, setNumSentences] = useState(initNumSentences); const [primaryDisabled, setPrimaryDisabled] = useState(false); @@ -66,7 +66,6 @@ export function TestModalContent(props: TestModalContentProps) { )}

{"The body content of the modal. This content can be anything!".repeat(numSentences)}

-

In Modal? {inModal ? "YES" : "NO"}

{withDateField && }