浏览代码

popupdialog associated with an id (query selector) (#2339)

* popupdialog associated with an id (query selector)
resolves #2293
- also fix a possible bug in chart

* with test

---------

Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
Fred Lefévère-Laoide 5 月之前
父节点
当前提交
0ce3c0dfd9

+ 2 - 2
frontend/taipy-gui/src/components/Taipy/Chart.tsx

@@ -280,8 +280,8 @@ const updateArrays = (sel: number[][], val: number[], idx: number) => {
     return sel;
 };
 
-const getDataKey = (columns: Record<string, ColumnDesc>, decimators?: string[]): [string[], string] => {
-    const backCols = Object.values(columns).map((col) => col.dfid);
+const getDataKey = (columns?: Record<string, ColumnDesc>, decimators?: string[]): [string[], string] => {
+    const backCols = columns ? Object.values(columns).map((col) => col.dfid) : [];
     return [backCols, backCols.join("-") + (decimators ? `--${decimators.join("")}` : "")];
 };
 

+ 31 - 16
frontend/taipy-gui/src/components/Taipy/Dialog.spec.tsx

@@ -44,7 +44,7 @@ describe("Dialog Component", () => {
         const { getByText } = render(
             <HelmetProvider>
                 <Dialog title="Dialog-Test-Title" page="page" open={true} />
-            </HelmetProvider>,
+            </HelmetProvider>
         );
         const elt = getByText("Dialog-Test-Title");
         expect(elt.tagName).toBe("H2");
@@ -54,7 +54,7 @@ describe("Dialog Component", () => {
         const { queryAllByText } = render(
             <HelmetProvider>
                 <Dialog title="Dialog-Test-Title" page="page" open={false} />
-            </HelmetProvider>,
+            </HelmetProvider>
         );
         expect(queryAllByText("Dialog-Test-Title")).toHaveLength(0);
         const divs = document.getElementsByTagName("div");
@@ -65,7 +65,7 @@ describe("Dialog Component", () => {
         const wrapper = render(
             <HelmetProvider>
                 <Dialog title="Dialog-Test-Title" page="page" open={true} className="taipy-dialog" />
-            </HelmetProvider>,
+            </HelmetProvider>
         );
         const elt = document.querySelector(".MuiDialog-root");
         expect(elt).toHaveClass("taipy-dialog");
@@ -79,7 +79,7 @@ describe("Dialog Component", () => {
                     defaultOpen="true"
                     open={undefined as unknown as boolean}
                 />
-            </HelmetProvider>,
+            </HelmetProvider>
         );
         getByText("Dialog-Test-Title");
     });
@@ -92,7 +92,7 @@ describe("Dialog Component", () => {
                     defaultOpen="true"
                     open={undefined as unknown as boolean}
                 />
-            </HelmetProvider>,
+            </HelmetProvider>
         );
         expect(getAllByRole("button")).toHaveLength(1);
     });
@@ -106,7 +106,7 @@ describe("Dialog Component", () => {
                     open={undefined as unknown as boolean}
                     labels={JSON.stringify(["toto"])}
                 />
-            </HelmetProvider>,
+            </HelmetProvider>
         );
         expect(getAllByRole("button")).toHaveLength(2);
     });
@@ -120,7 +120,7 @@ describe("Dialog Component", () => {
                     open={undefined as unknown as boolean}
                     labels={JSON.stringify(["toto", "titi", "toto"])}
                 />
-            </HelmetProvider>,
+            </HelmetProvider>
         );
         expect(getAllByRole("button")).toHaveLength(4);
     });
@@ -134,7 +134,7 @@ describe("Dialog Component", () => {
                     active={false}
                     labels={JSON.stringify(["testValidate", "testCancel"])}
                 />
-            </HelmetProvider>,
+            </HelmetProvider>
         );
         expect(getByText("testValidate")).toBeDisabled();
         expect(getByText("testCancel")).toBeDisabled();
@@ -148,7 +148,7 @@ describe("Dialog Component", () => {
                     open={true}
                     labels={JSON.stringify(["testValidate", "testCancel"])}
                 />
-            </HelmetProvider>,
+            </HelmetProvider>
         );
         expect(getByText("testValidate")).not.toBeDisabled();
         expect(getByText("testCancel")).not.toBeDisabled();
@@ -163,7 +163,7 @@ describe("Dialog Component", () => {
                     active={true}
                     labels={JSON.stringify(["testValidate", "testCancel"])}
                 />
-            </HelmetProvider>,
+            </HelmetProvider>
         );
         expect(getByText("testValidate")).not.toBeDisabled();
         expect(getByText("testCancel")).not.toBeDisabled();
@@ -183,7 +183,7 @@ describe("Dialog Component", () => {
                         onAction="testValidateAction"
                     />
                 </HelmetProvider>
-            </TaipyContext.Provider>,
+            </TaipyContext.Provider>
         );
         await userEvent.click(getByTitle("Close"));
         expect(dispatch).toHaveBeenLastCalledWith({
@@ -208,7 +208,7 @@ describe("Dialog Component", () => {
                         onAction="testValidateAction"
                     />
                 </HelmetProvider>
-            </TaipyContext.Provider>,
+            </TaipyContext.Provider>
         );
         await userEvent.click(getByText("testValidate"));
         expect(dispatch).toHaveBeenLastCalledWith({
@@ -233,7 +233,7 @@ describe("Dialog Component", () => {
                         onAction="testValidateAction"
                     />
                 </HelmetProvider>
-            </TaipyContext.Provider>,
+            </TaipyContext.Provider>
         );
         await userEvent.click(getByText("Another One"));
         expect(dispatch).toHaveBeenLastCalledWith({
@@ -259,13 +259,28 @@ describe("Dialog Component", () => {
         expect(computedStyles.width).not.toBe("500px");
         expect(computedStyles.height).not.toBe("300px");
     });
-    it("calls localAction prop when handleAction is triggered", () => {
+    it("calls localAction prop when handleAction is triggered", async () => {
         const localActionMock = jest.fn();
         const { getByLabelText } = render(
-            <Dialog id="test-dialog" title="Test Dialog" localAction={localActionMock} open={true} />,
+            <Dialog id="test-dialog" title="Test Dialog" localAction={localActionMock} open={true} />
         );
         const closeButton = getByLabelText("close");
-        fireEvent.click(closeButton);
+        await fireEvent.click(closeButton);
+        expect(localActionMock).toHaveBeenCalledWith(-1);
+    });
+    it("shows a popup", async () => {
+        const localActionMock = jest.fn();
+        const { getByText } = render(
+            <>
+                <Dialog id="test-dialog" title="" open={true} popup={true} localAction={localActionMock}>
+                    Hello
+                </Dialog>
+                <div>Outside</div>
+            </>
+        );
+        const Hello = getByText("Hello");
+        const Outside = getByText("Outside");
+        await userEvent.keyboard("{Escape}")
         expect(localActionMock).toHaveBeenCalledWith(-1);
     });
 });

+ 49 - 5
frontend/taipy-gui/src/components/Taipy/Dialog.tsx

@@ -17,10 +17,11 @@ import DialogTitle from "@mui/material/DialogTitle";
 import MuiDialog from "@mui/material/Dialog";
 import DialogActions from "@mui/material/DialogActions";
 import DialogContent from "@mui/material/DialogContent";
-import Tooltip from "@mui/material/Tooltip";
 import IconButton from "@mui/material/IconButton";
-import CloseIcon from "@mui/icons-material/Close";
+import Popover, { PopoverOrigin } from "@mui/material/Popover";
+import Tooltip from "@mui/material/Tooltip";
 import { SxProps, Theme } from "@mui/system";
+import CloseIcon from "@mui/icons-material/Close";
 
 import { createSendActionNameAction } from "../../context/taipyReducers";
 import TaipyRendered from "../pages/TaipyRendered";
@@ -41,6 +42,9 @@ interface DialogProps extends TaipyActiveProps {
     height?: string | number;
     width?: string | number;
     localAction?: (idx: number) => void;
+    refId?: string;
+    defaultRefId?: string;
+    popup?: boolean;
 }
 
 const closeSx: SxProps<Theme> = {
@@ -51,6 +55,29 @@ const closeSx: SxProps<Theme> = {
 };
 const titleSx = { m: 0, p: 2, display: "flex", paddingRight: "0.1em" };
 
+const virtualElt = {
+    nodeType: 1,
+    getBoundingClientRect: () => {
+        const x = (document.body.offsetWidth - document.body.offsetLeft) / 2;
+        const y = (document.body.offsetHeight - document.body.offsetTop) / 2;
+        return {
+            x,
+            y,
+            width: 0,
+            height: 0,
+            top: y,
+            left: x,
+            bottom: y,
+            right: x,
+        };
+    },
+} as Element;
+
+const popoverAnchor: PopoverOrigin = {
+    vertical: "center",
+    horizontal: "center",
+};
+
 const Dialog = (props: DialogProps) => {
     const {
         id,
@@ -64,6 +91,7 @@ const Dialog = (props: DialogProps) => {
         partial,
         width,
         height,
+        popup = false,
     } = props;
     const dispatch = useDispatch();
     const module = useModule();
@@ -71,6 +99,7 @@ const Dialog = (props: DialogProps) => {
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
     const active = useDynamicProperty(props.active, props.defaultActive, true);
     const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
+    const refId = useDynamicProperty(props.refId, props.defaultRefId, undefined);
 
     const handleAction = useCallback(
         (evt: MouseEvent<HTMLElement>) => {
@@ -110,7 +139,22 @@ const Dialog = (props: DialogProps) => {
         return {};
     }, [width, height]);
 
-    return (
+    const getAnchorEl = useCallback(() => (refId && document.querySelector(refId)) || virtualElt, [refId]);
+
+    return popup ? (
+        <Popover
+            id={id}
+            onClose={handleAction}
+            open={open === undefined ? defaultOpen === "true" || defaultOpen === true : !!open}
+            className={`${className} ${getComponentClassName(props.children)}`}
+            sx={paperProps.sx}
+            anchorEl={getAnchorEl}
+            anchorOrigin={popoverAnchor}
+        >
+            {page ? <TaipyRendered path={"/" + page} partial={partial} fromBlock={true} /> : null}
+            {props.children}
+        </Popover>
+    ) : (
         <MuiDialog
             id={id}
             onClose={handleAction}
@@ -133,9 +177,9 @@ const Dialog = (props: DialogProps) => {
             </DialogContent>
             {labels.length ? (
                 <DialogActions>
-                    {labels.map((l, i) => (
+                    {labels.map((label, i) => (
                         <Button onClick={handleAction} disabled={!active} key={"label" + i} data-idx={i}>
-                            {l}
+                            {label}
                         </Button>
                     ))}
                 </DialogActions>

+ 2 - 0
taipy/gui/_renderers/factory.py

@@ -215,6 +215,8 @@ class _Factory:
                 ("width", PropertyType.string_or_number),
                 ("height", PropertyType.string_or_number),
                 ("hover_text", PropertyType.dynamic_string),
+                ("ref_id", PropertyType.dynamic_string),
+                ("popup", PropertyType.boolean),
             ]
         )
         ._set_propagate(),

+ 2 - 1
taipy/gui/viselements.json

@@ -2018,7 +2018,8 @@
                         "name": "height",
                         "type": "Union[str,int,float]",
                         "doc": "The height of the dialog, in CSS units."
-                    }
+                    },
+                    {"name": "ref_id", "type": "dynamic(str)", "doc": "TODO an id or a query selector that allows to identify an HTML component that would be the anchor for the dialog."}
                 ]
             }
         ],