Skip to content

Commit

Permalink
Merge pull request #1676 from Avaiga/1626-add-dynamic-label-to-progre…
Browse files Browse the repository at this point in the history
…ss-visual-element

New Feature: Enhanced Progress Control
  • Loading branch information
namnguyen20999 committed Sep 17, 2024
2 parents 825c843 + fcbedd2 commit 7d09861
Show file tree
Hide file tree
Showing 4 changed files with 258 additions and 34 deletions.
129 changes: 126 additions & 3 deletions frontend/taipy-gui/src/components/Taipy/Progress.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -24,28 +22,153 @@ 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");
const valueText = getByText("50%");
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");
const valueText = getByText("50%");
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();
});
});
144 changes: 113 additions & 31 deletions frontend/taipy-gui/src/components/Taipy/Progress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 = {
Expand All @@ -45,51 +49,129 @@ 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;
}

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>
);
};

Expand Down
3 changes: 3 additions & 0 deletions taipy/gui/_renderers/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
]
)
Expand Down
16 changes: 16 additions & 0 deletions taipy/gui/viselements.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down

0 comments on commit 7d09861

Please sign in to comment.