Browse Source

Merge pull request #1676 from Avaiga/1626-add-dynamic-label-to-progress-visual-element

New Feature: Enhanced Progress Control
Nam Nguyen 8 months ago
parent
commit
7d09861eab

+ 126 - 3
frontend/taipy-gui/src/components/Taipy/Progress.spec.tsx

@@ -12,10 +12,8 @@
  */
 
 import React from "react";
-
 import { render } from "@testing-library/react";
 import "@testing-library/jest-dom";
-
 import Progress from "./Progress";
 
 describe("Progress component", () => {
@@ -24,11 +22,13 @@ describe("Progress component", () => {
         const elt = getByRole("progressbar");
         expect(elt).toHaveClass("MuiCircularProgress-root");
     });
+
     it("uses the class", async () => {
         const { getByRole } = render(<Progress className="taipy-progress" />);
         const elt = getByRole("progressbar");
         expect(elt).toHaveClass("taipy-progress");
     });
+
     it("renders circular progress with value (determinate)", () => {
         const { getByRole, getByText } = render(<Progress showValue value={50} />);
         const elt = getByRole("progressbar");
@@ -36,11 +36,13 @@ describe("Progress component", () => {
         expect(elt).toHaveClass("MuiCircularProgress-root");
         expect(valueText).toBeInTheDocument();
     });
-    it("renders linear progress without value (inderminate)", () => {
+
+    it("renders linear progress without value (indeterminate)", () => {
         const { getByRole } = render(<Progress linear />);
         const elt = getByRole("progressbar");
         expect(elt).toHaveClass("MuiLinearProgress-root");
     });
+
     it("renders linear progress with value (determinate)", () => {
         const { getByRole, getByText } = render(<Progress linear showValue value={50} />);
         const elt = getByRole("progressbar");
@@ -48,4 +50,125 @@ describe("Progress component", () => {
         expect(elt).toHaveClass("MuiLinearProgress-root");
         expect(valueText).toBeInTheDocument();
     });
+
+    it("does not render when render prop is false", async () => {
+        const { container } = render(<Progress render={false} />);
+        expect(container.firstChild).toBeNull();
+    });
+
+    it("should render the title when title is defined", () => {
+        const { getByText } = render(<Progress title="Title" />);
+        const title = getByText("Title");
+        expect(title).toBeInTheDocument();
+    });
+
+    it("renders Typography with correct sx and variant", () => {
+        const { getByText } = render(<Progress title="Title" />);
+        const typographyElement = getByText("Title");
+        expect(typographyElement).toBeInTheDocument();
+        expect(typographyElement).toHaveStyle("margin: 8px");
+        expect(typographyElement.tagName).toBe("SPAN");
+    });
+
+    it("renders determinate progress correctly", () => {
+        const { getByRole } = render(<Progress value={50} />);
+        const progressBar = getByRole("progressbar");
+        expect(progressBar).toBeInTheDocument();
+        expect(progressBar).toHaveAttribute("aria-valuenow", "50");
+    });
+
+    it("renders determinate progress with linear progress bar", () => {
+        const { getByRole } = render(<Progress value={50} linear />);
+        const progressBar = getByRole("progressbar");
+        expect(progressBar).toBeInTheDocument();
+        expect(progressBar).toHaveAttribute("aria-valuenow", "50");
+    });
+
+    it("renders title and linear progress bar correctly", () => {
+        const { getByText, getByRole } = render(<Progress title="Title" value={50} linear showValue={true} />);
+        const title = getByText("Title");
+        const progressBar = getByRole("progressbar");
+        expect(title).toBeInTheDocument();
+        expect(progressBar).toBeInTheDocument();
+    });
+
+    it("renders title and linear progress bar without showing value", () => {
+        const { getByText, queryByText } = render(<Progress title="Title" value={50} linear />);
+        const title = getByText("Title");
+        const value = queryByText("50%");
+        expect(title).toBeInTheDocument();
+        expect(value).toBeNull();
+    });
+
+    it("renders title and circular progress bar correctly", () => {
+        const { getByText, getByRole } = render(<Progress title="Title" value={50} showValue={true} />);
+        const title = getByText("Title");
+        const progressBar = getByRole("progressbar");
+        expect(title).toBeInTheDocument();
+        expect(progressBar).toBeInTheDocument();
+    });
+
+    it("displays title above progress", () => {
+        const { container } = render(<Progress titleAnchor="top" />);
+        const box = container.querySelector(".MuiBox-root");
+        expect(box).toHaveStyle("flex-direction: column");
+    });
+
+    it("displays title to the left of progress", () => {
+        const { container } = render(<Progress titleAnchor="left" />);
+        const box = container.querySelector(".MuiBox-root");
+        expect(box).toHaveStyle("flex-direction: row");
+    });
+
+    it("displays title to the right of progress", () => {
+        const { container } = render(<Progress titleAnchor="right" />);
+        const box = container.querySelector(".MuiBox-root");
+        expect(box).toHaveStyle("flex-direction: row-reverse");
+    });
+
+    it("displays title at the bottom of progress", () => {
+        const { container } = render(<Progress titleAnchor="bottom" />);
+        const box = container.querySelector(".MuiBox-root");
+        expect(box).toHaveStyle("flex-direction: column-reverse");
+    });
+
+    it("displays the title at the bottom of the progress bar when the title anchor is undefined", () => {
+        const { container } = render(<Progress />);
+        const box = container.querySelector(".MuiBox-root");
+        expect(box).toHaveStyle("flex-direction: column-reverse");
+    });
+
+    it("applies color to linear progress when color is defined", () => {
+        const { container } = render(<Progress linear value={50} color="red" />);
+        const linearProgressBar = container.querySelector(".MuiLinearProgress-bar");
+        expect(linearProgressBar).toHaveStyle("background: red");
+    });
+
+    it("does not apply color to linear progress when color is undefined", () => {
+        const { container } = render(<Progress linear value={50} />);
+        const linearProgressBar = container.querySelector(".MuiLinearProgress-bar");
+        expect(linearProgressBar).not.toHaveStyle("background: red");
+    });
+
+    it("applies color to circular progress when color is defined", () => {
+        const { container } = render(<Progress linear={false} value={50} color="blue" />);
+        const circularProgressCircle = container.querySelector(".MuiCircularProgress-circle");
+        expect(circularProgressCircle).toHaveStyle("color: blue");
+    });
+
+    it("does not apply color to circular progress when color is undefined", () => {
+        const { container } = render(<Progress linear={false} value={50} />);
+        const circularProgressCircle = container.querySelector(".MuiCircularProgress-circle");
+        expect(circularProgressCircle).not.toHaveStyle("color: blue");
+    });
+});
+
+describe("Progress functions", () => {
+    it("renders title and linear progress bar correctly", () => {
+        const { getByText, getByRole } = render(<Progress title="Title" value={50} linear showValue={true} />);
+        const title = getByText("Title");
+        const progressBar = getByRole("progressbar");
+        expect(title).toBeInTheDocument();
+        expect(progressBar).toBeInTheDocument();
+    });
 });

+ 113 - 31
frontend/taipy-gui/src/components/Taipy/Progress.tsx

@@ -11,8 +11,7 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React from "react";
-
+import React, { useMemo } from "react";
 import Box from "@mui/material/Box";
 import CircularProgress from "@mui/material/CircularProgress";
 import LinearProgress from "@mui/material/LinearProgress";
@@ -22,16 +21,21 @@ import { useClassNames, useDynamicProperty } from "../../utils/hooks";
 import { TaipyBaseProps } from "./utils";
 
 interface ProgressBarProps extends TaipyBaseProps {
-    linear?: boolean; //by default - false
-    showValue?: boolean; //by default - false
-    value?: number; //progress value
-    defaultValue?: number; //default progress value
+    color?: string;
+    linear?: boolean;
+    showValue?: boolean;
+    value?: number;
+    defaultValue?: number;
     render?: boolean;
     defaultRender?: boolean;
+    title?: string;
+    defaultTitle?: string;
+    titleAnchor?: "top" | "bottom" | "left" | "right" | "none";
 }
 
-const linearSx = { display: "flex", alignItems: "center" };
+const linearSx = { display: "flex", alignItems: "center", width: "100%" };
 const linearPrgSx = { width: "100%", mr: 1 };
+const titleSx = { margin: 1 };
 const linearTxtSx = { minWidth: 35 };
 const circularSx = { position: "relative", display: "inline-flex" };
 const circularPrgSx = {
@@ -45,12 +49,60 @@ const circularPrgSx = {
     justifyContent: "center",
 };
 
+const getFlexDirection = (titleAnchor: string) => {
+    switch (titleAnchor) {
+        case "top":
+            return "column";
+        case "left":
+            return "row";
+        case "right":
+            return "row-reverse";
+        case "bottom":
+        default:
+            return "column-reverse";
+    }
+};
+
 const Progress = (props: ProgressBarProps) => {
-    const { linear = false, showValue = false } = props;
+    const { linear = false, showValue = false, titleAnchor = "bottom" } = props;
 
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
     const value = useDynamicProperty(props.value, props.defaultValue, undefined, "number", true);
     const render = useDynamicProperty(props.render, props.defaultRender, true);
+    const title = useDynamicProperty(props.title, props.defaultTitle, undefined);
+
+    const memoizedValues = useMemo(() => {
+        return {
+            boxWithFlexDirectionSx: {
+                ...linearSx,
+                flexDirection: getFlexDirection(titleAnchor),
+            },
+            circularBoxSx: {
+                ...circularSx,
+                flexDirection: getFlexDirection(titleAnchor),
+                alignItems: title && titleAnchor ? "center" : "",
+            },
+            linearProgressSx: {
+                "& .MuiLinearProgress-bar": {
+                    background: props.color ? props.color : undefined,
+                },
+            },
+            circularProgressSx: {
+                "& .MuiCircularProgress-circle": {
+                    color: props.color ? props.color : undefined,
+                },
+            },
+            linearProgressFullWidthSx: {
+                width: "100%",
+                "& .MuiLinearProgress-bar": {
+                    background: props.color ? props.color : undefined,
+                },
+            },
+        };
+    }, [props.color, title, titleAnchor]);
+
+    const { boxWithFlexDirectionSx, circularBoxSx, linearProgressSx, circularProgressSx, linearProgressFullWidthSx } =
+        memoizedValues;
 
     if (!render) {
         return null;
@@ -58,38 +110,68 @@ const Progress = (props: ProgressBarProps) => {
 
     return showValue && value !== undefined ? (
         linear ? (
-            <Box sx={linearSx} className={className} id={props.id}>
-                <Box sx={linearPrgSx}>
-                    <LinearProgress variant="determinate" value={value} />
-                </Box>
-                <Box sx={linearTxtSx}>
-                    <Typography variant="body2" color="text.secondary">{`${Math.round(value)}%`}</Typography>
+            <Box sx={boxWithFlexDirectionSx}>
+                {title && titleAnchor !== "none" ? (
+                    <Typography sx={titleSx} variant="caption">
+                        {title}
+                    </Typography>
+                ) : null}
+                <Box sx={linearSx} className={className} id={props.id}>
+                    <Box sx={linearPrgSx}>
+                        <LinearProgress sx={linearProgressSx} variant="determinate" value={value} />
+                    </Box>
+                    <Box sx={linearTxtSx}>
+                        <Typography variant="body2" color="text.secondary">{`${Math.round(value)}%`}</Typography>
+                    </Box>
                 </Box>
             </Box>
         ) : (
-            <Box sx={circularSx} className={className} id={props.id}>
-                <CircularProgress variant="determinate" value={value} />
-                <Box sx={circularPrgSx}>
-                    <Typography variant="caption" component="div" color="text.secondary">
-                        {`${Math.round(value)}%`}
+            <Box sx={circularBoxSx}>
+                {title && titleAnchor !== "none" ? (
+                    <Typography sx={titleSx} variant="caption">
+                        {title}
                     </Typography>
+                ) : null}
+                <Box sx={circularSx} className={className} id={props.id}>
+                    <CircularProgress sx={circularProgressSx} variant="determinate" value={value} />
+                    <Box sx={circularPrgSx}>
+                        <Typography variant="caption" component="div" color="text.secondary">
+                            {`${Math.round(value)}%`}
+                        </Typography>
+                    </Box>
                 </Box>
             </Box>
         )
     ) : linear ? (
-        <LinearProgress
-            id={props.id}
-            variant={value === undefined ? "indeterminate" : "determinate"}
-            value={value}
-            className={className}
-        />
+        <Box sx={boxWithFlexDirectionSx}>
+            {title && titleAnchor !== "none" ? (
+                <Typography sx={titleSx} variant="caption">
+                    {title}
+                </Typography>
+            ) : null}
+            <LinearProgress
+                id={props.id}
+                sx={linearProgressFullWidthSx}
+                variant={value === undefined ? "indeterminate" : "determinate"}
+                value={value}
+                className={className}
+            />
+        </Box>
     ) : (
-        <CircularProgress
-            id={props.id}
-            variant={value === undefined ? "indeterminate" : "determinate"}
-            value={value}
-            className={className}
-        />
+        <Box sx={circularBoxSx}>
+            {title && titleAnchor !== "none" ? (
+                <Typography sx={titleSx} variant="caption">
+                    {title}
+                </Typography>
+            ) : null}
+            <CircularProgress
+                id={props.id}
+                sx={circularProgressSx}
+                variant={value === undefined ? "indeterminate" : "determinate"}
+                value={value}
+                className={className}
+            />
+        </Box>
     );
 };
 

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

@@ -608,8 +608,11 @@ class _Factory:
         .set_value_and_default(var_type=PropertyType.dynamic_number, native_type=True)
         .set_attributes(
             [
+                ("color", PropertyType.string),
                 ("linear", PropertyType.boolean, False),
                 ("show_value", PropertyType.boolean, False),
+                ("title", PropertyType.dynamic_string),
+                ("title_anchor", PropertyType.string, "bottom"),
                 ("render", PropertyType.dynamic_boolean, True),
             ]
         )

+ 16 - 0
taipy/gui/viselements.json

@@ -1353,6 +1353,22 @@
                         "default_value": "False",
                         "doc": "If set to True, the progress value is shown."
                     },
+                    {
+                        "name": "title",
+                        "type": "dynamic(str)",
+                        "doc": "The title of the progress indicator."
+                    },
+                                        {
+                        "name": "title_anchor",
+                        "type": "str",
+                        "default_value": "\"bottom\"",
+                        "doc": "The anchor of the title."
+                    },
+                    {
+                        "name": "color",
+                        "type": "str",
+                        "doc": "The color of the progress indicator."
+                    },
                     {
                         "name": "render",
                         "type": "dynamic(bool)",