Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Feature: Enhanced Progress Control #1676

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6b30fb9
Progress title + color attribute
namnguyen20999 Aug 17, 2024
58890d4
Add title anchor as none
namnguyen20999 Aug 17, 2024
f417fd8
Adding color property to Progress
namnguyen20999 Aug 21, 2024
57dba8e
Adding style apply to circular progress
namnguyen20999 Aug 21, 2024
49f3168
Merge branch 'develop' into 1626-add-dynamic-label-to-progress-visual…
namnguyen20999 Aug 21, 2024
f5b5a35
Merge branch 'develop' into 1626-add-dynamic-label-to-progress-visual…
namnguyen20999 Sep 4, 2024
9386508
remove sx from components
namnguyen20999 Sep 9, 2024
e06aed4
Merge branch 'develop' into 1626-add-dynamic-label-to-progress-visual…
namnguyen20999 Sep 9, 2024
7901097
update file to return CR
namnguyen20999 Sep 9, 2024
ccdf2b6
Merge branch '1626-add-dynamic-label-to-progress-visual-element' of g…
namnguyen20999 Sep 9, 2024
ba04767
Merge branch 'develop' into 1626-add-dynamic-label-to-progress-visual…
namnguyen20999 Sep 10, 2024
fbf6d70
Merge branch 'develop' into 1626-add-dynamic-label-to-progress-visual…
namnguyen20999 Sep 13, 2024
77e93cd
Merge branch 'develop' into 1626-add-dynamic-label-to-progress-visual…
namnguyen20999 Sep 16, 2024
bcd05ba
per Fab
namnguyen20999 Sep 16, 2024
0a114f4
update per Fred
namnguyen20999 Sep 16, 2024
a4f037e
Merge branch '1626-add-dynamic-label-to-progress-visual-element' of g…
namnguyen20999 Sep 16, 2024
3fb77d4
Merge branch 'develop' into 1626-add-dynamic-label-to-progress-visual…
namnguyen20999 Sep 16, 2024
270ed24
update per Fred & add more test cases
namnguyen20999 Sep 16, 2024
26c25ca
Merge branch '1626-add-dynamic-label-to-progress-visual-element' of g…
namnguyen20999 Sep 16, 2024
27dc808
Merge branch 'develop' into 1626-add-dynamic-label-to-progress-visual…
namnguyen20999 Sep 16, 2024
2cee9cb
typo
namnguyen20999 Sep 16, 2024
fcbedd2
Merge remote-tracking branch 'origin/1626-add-dynamic-label-to-progre…
namnguyen20999 Sep 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
namnguyen20999 marked this conversation as resolved.
Show resolved Hide resolved
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."
namnguyen20999 marked this conversation as resolved.
Show resolved Hide resolved
},
{
"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
Loading