瀏覽代碼

display toggle as switch if lov is not declared (#737)

* display toggle as switch if lov is not declared

* default value

* switch mode

* fix test

* fix test

---------

Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
Fred Lefévère-Laoide 1 年之前
父節點
當前提交
f8e90ad322

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

@@ -20,7 +20,7 @@ import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
 import WbSunny from "@mui/icons-material/WbSunny";
 import WbSunny from "@mui/icons-material/WbSunny";
 import Brightness3 from "@mui/icons-material/Brightness3";
 import Brightness3 from "@mui/icons-material/Brightness3";
 
 
-import { TaipyActiveProps } from "./utils";
+import { TaipyActiveProps, emptyStyle } from "./utils";
 import { TaipyContext } from "../../context/taipyContext";
 import { TaipyContext } from "../../context/taipyContext";
 import { createThemeAction } from "../../context/taipyReducers";
 import { createThemeAction } from "../../context/taipyReducers";
 import { useClassNames } from "../../utils/hooks";
 import { useClassNames } from "../../utils/hooks";
@@ -46,7 +46,7 @@ const boxSx = {
 const groupSx = { verticalAlign: "middle" };
 const groupSx = { verticalAlign: "middle" };
 
 
 const ThemeToggle = (props: ThemeToggleProps) => {
 const ThemeToggle = (props: ThemeToggleProps) => {
-    const { id, label = "Mode", style = {}, active = true } = props;
+    const { id, label = "Mode", style = emptyStyle, active = true } = props;
     const { state, dispatch } = useContext(TaipyContext);
     const { state, dispatch } = useContext(TaipyContext);
 
 
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);

+ 56 - 0
frontend/taipy-gui/src/components/Taipy/Toggle.spec.tsx

@@ -143,4 +143,60 @@ describe("Toggle Component", () => {
         await userEvent.click(elt);
         await userEvent.click(elt);
         expect(dispatch).not.toHaveBeenCalled();
         expect(dispatch).not.toHaveBeenCalled();
     });
     });
+
+    describe("As Switch", () => {
+        it("renders", async () => {
+            const { getByText } = render(<Toggle isSwitch={true} label="switch" />);
+            const elt = getByText("switch");
+            expect(elt.tagName).toBe("SPAN");
+        });
+        it("uses the class", async () => {
+            const { getByText } = render(<Toggle isSwitch={true}  label="switch" className="taipy-toggle" />);
+            const elt = getByText("switch");
+            expect(elt.parentElement).toHaveClass("taipy-toggle-switch");
+        });
+        it("shows a selection at start", async () => {
+            const { getByText } = render(<Toggle isSwitch={true} defaultValue={true as unknown as string} label="switch" />);
+            const elt = getByText("switch");
+            expect(elt.parentElement?.querySelector(".MuiSwitch-switchBase")).toHaveClass("Mui-checked");
+        });
+        it("shows a selection at start through value", async () => {
+            const { getByText } = render(<Toggle isSwitch={true} value={true as unknown as string} defaultValue={false as unknown as string} label="switch" />);
+            const elt = getByText("switch");
+            expect(elt.parentElement?.querySelector(".MuiSwitch-switchBase")).toHaveClass("Mui-checked");
+        });
+        it("is disabled", async () => {
+            const { getByText } = render(<Toggle isSwitch={true} defaultValue={false as unknown as string} label="switch" active={false} />);
+            const elt = getByText("switch");
+            expect(elt.parentElement?.querySelector("input")).toBeDisabled();
+        });
+        it("is enabled by default", async () => {
+            const { getByText } = render(<Toggle isSwitch={true} defaultValue={false as unknown as string} label="switch" />);
+            const elt = getByText("switch");
+            expect(elt.parentElement?.querySelector("input")).not.toBeDisabled();
+        });
+        it("is enabled by active", async () => {
+            const { getByText } = render(<Toggle isSwitch={true} defaultValue={false as unknown as string} label="switch" active={true} />);
+            const elt = getByText("switch");
+            expect(elt.parentElement?.querySelector("input")).not.toBeDisabled();
+        });
+        it("dispatch a well formed message", async () => {
+            const dispatch = jest.fn();
+            const state: TaipyState = INITIAL_STATE;
+            const { getByText } = render(
+                <TaipyContext.Provider value={{ state, dispatch }}>
+                    <Toggle isSwitch={true} updateVarName="varname" defaultValue={false as unknown as string} label="switch" />
+                </TaipyContext.Provider>
+            );
+            const elt = getByText("switch");
+            await userEvent.click(elt);
+            expect(dispatch).toHaveBeenCalledWith({
+                name: "varname",
+                payload: { value: true },
+                propagate: true,
+                type: "SEND_UPDATE_ACTION",
+            });
+        });
+
+    });
 });
 });

+ 65 - 30
frontend/taipy-gui/src/components/Taipy/Toggle.tsx

@@ -11,8 +11,9 @@
  * specific language governing permissions and limitations under the License.
  * specific language governing permissions and limitations under the License.
  */
  */
 
 
-import React, { CSSProperties, MouseEvent, useCallback, useEffect, useState } from "react";
+import React, { CSSProperties, MouseEvent, SyntheticEvent, useCallback, useEffect, useState } from "react";
 import Box from "@mui/material/Box";
 import Box from "@mui/material/Box";
+import Switch from "@mui/material/Switch";
 import Typography from "@mui/material/Typography";
 import Typography from "@mui/material/Typography";
 import ToggleButton from "@mui/material/ToggleButton";
 import ToggleButton from "@mui/material/ToggleButton";
 import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
 import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
@@ -22,24 +23,25 @@ import { createSendUpdateAction } from "../../context/taipyReducers";
 import ThemeToggle from "./ThemeToggle";
 import ThemeToggle from "./ThemeToggle";
 import { LovProps, useLovListMemo } from "./lovUtils";
 import { LovProps, useLovListMemo } from "./lovUtils";
 import { useClassNames, useDispatch, useDynamicProperty, useModule } from "../../utils/hooks";
 import { useClassNames, useDispatch, useDynamicProperty, useModule } from "../../utils/hooks";
-import { getUpdateVar } from "./utils";
+import { emptyStyle, getSuffixedClassNames, getUpdateVar } from "./utils";
 import { Icon, IconAvatar } from "../../utils/icon";
 import { Icon, IconAvatar } from "../../utils/icon";
+import { FormControlLabel } from "@mui/material";
 
 
-const groupSx = {verticalAlign: "middle"};
+const groupSx = { verticalAlign: "middle" };
 
 
 interface ToggleProps extends LovProps<string> {
 interface ToggleProps extends LovProps<string> {
     style?: CSSProperties;
     style?: CSSProperties;
     label?: string;
     label?: string;
-    kind?: string;
     unselectedValue?: string;
     unselectedValue?: string;
     allowUnselect?: boolean;
     allowUnselect?: boolean;
+    mode?: string;
+    isSwitch? : boolean;
 }
 }
 
 
 const Toggle = (props: ToggleProps) => {
 const Toggle = (props: ToggleProps) => {
     const {
     const {
         id,
         id,
-        style = {},
-        kind,
+        style = emptyStyle,
         label,
         label,
         updateVarName = "",
         updateVarName = "",
         propagate = true,
         propagate = true,
@@ -48,9 +50,18 @@ const Toggle = (props: ToggleProps) => {
         unselectedValue = "",
         unselectedValue = "",
         updateVars = "",
         updateVars = "",
         valueById,
         valueById,
+        mode = "",
+        isSwitch = false,
     } = props;
     } = props;
     const dispatch = useDispatch();
     const dispatch = useDispatch();
     const [value, setValue] = useState(props.defaultValue);
     const [value, setValue] = useState(props.defaultValue);
+    const [bVal, setBVal] = useState(() =>
+        typeof props.defaultValue === "boolean"
+            ? props.defaultValue
+            : typeof props.value === "boolean"
+            ? props.value
+            : false
+    );
     const module = useModule();
     const module = useModule();
 
 
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
@@ -61,7 +72,7 @@ const Toggle = (props: ToggleProps) => {
 
 
     const changeValue = useCallback(
     const changeValue = useCallback(
         (evt: MouseEvent, val: string) => {
         (evt: MouseEvent, val: string) => {
-            if (!props.allowUnselect && val === null ) {
+            if (!props.allowUnselect && val === null) {
                 return;
                 return;
             }
             }
             dispatch(
             dispatch(
@@ -73,36 +84,60 @@ const Toggle = (props: ToggleProps) => {
                     propagate,
                     propagate,
                     valueById ? undefined : getUpdateVar(updateVars, "lov")
                     valueById ? undefined : getUpdateVar(updateVars, "lov")
                 )
                 )
-            )},
-        [unselectedValue, updateVarName, propagate, dispatch, updateVars, valueById, props.onChange, props.allowUnselect, module]
+            );
+        },
+        [
+            unselectedValue,
+            updateVarName,
+            propagate,
+            dispatch,
+            updateVars,
+            valueById,
+            props.onChange,
+            props.allowUnselect,
+            module,
+        ]
+    );
+
+    const changeSwitchValue = useCallback(
+        (evt: SyntheticEvent, checked: boolean) =>
+            dispatch(createSendUpdateAction(updateVarName, checked, module, props.onChange, propagate)),
+        [updateVarName, dispatch, props.onChange, propagate, module]
     );
     );
 
 
-    useEffect(() => {props.value !== undefined && setValue(props.value)}, [props.value]);
+    useEffect(() => {
+        typeof props.value === "boolean" ? setBVal(props.value) : props.value !== undefined && setValue(props.value);
+    }, [props.value]);
 
 
-    return kind === "theme" ? (
+    return mode.toLowerCase() === "theme" ? (
         <ThemeToggle {...props} />
         <ThemeToggle {...props} />
     ) : (
     ) : (
         <Box id={id} sx={style} className={className}>
         <Box id={id} sx={style} className={className}>
-            {label ? <Typography>{label}</Typography> : null}
+            {label && !isSwitch ? <Typography>{label}</Typography> : null}
             <Tooltip title={hover || ""}>
             <Tooltip title={hover || ""}>
-                <ToggleButtonGroup
-                    value={value}
-                    exclusive
-                    onChange={changeValue}
-                    disabled={!active}
-                    sx={groupSx}
-                >
-                    {lovList &&
-                        lovList.map((v) => (
-                            <ToggleButton value={v.id} key={v.id}>
-                                {typeof v.item === "string" ? (
-                                    <Typography>{v.item}</Typography>
-                                ) : (
-                                    <IconAvatar id={v.id} img={v.item as Icon} />
-                                )}
-                            </ToggleButton>
-                        ))}
-                </ToggleButtonGroup>
+                {isSwitch ? (
+                    <FormControlLabel
+                        control={<Switch />}
+                        checked={bVal}
+                        onChange={changeSwitchValue}
+                        disabled={!active}
+                        label={label}
+                        className={getSuffixedClassNames(className, "-switch")}
+                    />
+                ) : (
+                    <ToggleButtonGroup value={value} exclusive onChange={changeValue} disabled={!active} sx={groupSx}>
+                        {lovList &&
+                            lovList.map((v) => (
+                                <ToggleButton value={v.id} key={v.id}>
+                                    {typeof v.item === "string" ? (
+                                        <Typography>{v.item}</Typography>
+                                    ) : (
+                                        <IconAvatar id={v.id} img={v.item as Icon} />
+                                    )}
+                                </ToggleButton>
+                            ))}
+                    </ToggleButtonGroup>
+                )}
             </Tooltip>
             </Tooltip>
         </Box>
         </Box>
     );
     );

+ 3 - 1
frontend/taipy-gui/src/components/Taipy/utils.ts

@@ -11,7 +11,7 @@
  * specific language governing permissions and limitations under the License.
  * specific language governing permissions and limitations under the License.
  */
  */
 
 
-import { MouseEvent } from "react";
+import { CSSProperties, MouseEvent } from "react";
 
 
 export interface TaipyActiveProps extends TaipyDynamicProps, TaipyHoverProps {
 export interface TaipyActiveProps extends TaipyDynamicProps, TaipyHoverProps {
     defaultActive?: boolean;
     defaultActive?: boolean;
@@ -109,3 +109,5 @@ export const getSuffixedClassNames = (names: string | undefined, suffix: string)
         .split(/\s+/)
         .split(/\s+/)
         .map((n) => n + suffix)
         .map((n) => n + suffix)
         .join(" ");
         .join(" ");
+
+export const emptyStyle = {} as CSSProperties;

+ 6 - 2
taipy/gui/_renderers/builder.py

@@ -739,10 +739,14 @@ class _Builder:
             default_val (optional(Any)): the default value.
             default_val (optional(Any)): the default value.
         """
         """
         var_name = self.__default_property_name if var_name is None else var_name
         var_name = self.__default_property_name if var_name is None else var_name
-        if var_type == PropertyType.slider_value:
+        if var_type == PropertyType.slider_value or var_type == PropertyType.toggle_value:
             if self.__attributes.get("lov"):
             if self.__attributes.get("lov"):
                 var_type = PropertyType.lov_value
                 var_type = PropertyType.lov_value
                 native_type = False
                 native_type = False
+            elif var_type == PropertyType.toggle_value:
+                self.__set_react_attribute(_to_camel_case("is_switch"), True)
+                var_type = PropertyType.dynamic_boolean
+                native_type = True
             else:
             else:
                 var_type = (
                 var_type = (
                     PropertyType.dynamic_lo_numbers
                     PropertyType.dynamic_lo_numbers
@@ -832,7 +836,7 @@ class _Builder:
 
 
     def _set_kind(self):
     def _set_kind(self):
         if self.__attributes.get("theme", False):
         if self.__attributes.get("theme", False):
-            self.set_attribute("kind", "theme")
+            self.set_attribute("mode", "theme")
         return self
         return self
 
 
     def __get_typed_hash_name(self, hash_name: str, var_type: t.Optional[PropertyType]) -> str:
     def __get_typed_hash_name(self, hash_name: str, var_type: t.Optional[PropertyType]) -> str:

+ 4 - 7
taipy/gui/_renderers/factory.py

@@ -81,11 +81,7 @@ class _Factory:
             ]
             ]
         ),
         ),
         "chart": lambda gui, control_type, attrs: _Builder(
         "chart": lambda gui, control_type, attrs: _Builder(
-            gui=gui,
-            control_type=control_type,
-            element_name="Chart",
-            attributes=attrs,
-            default_value=None
+            gui=gui, control_type=control_type, element_name="Chart", attributes=attrs, default_value=None
         )
         )
         .set_value_and_default(with_default=False, var_type=PropertyType.data)
         .set_value_and_default(with_default=False, var_type=PropertyType.data)
         .set_attributes(
         .set_attributes(
@@ -104,7 +100,7 @@ class _Factory:
                 ("template", PropertyType.dict),
                 ("template", PropertyType.dict),
                 ("template[dark]", PropertyType.dict, gui._get_config("chart_dark_template", None)),
                 ("template[dark]", PropertyType.dict, gui._get_config("chart_dark_template", None)),
                 ("template[light]", PropertyType.dict),
                 ("template[light]", PropertyType.dict),
-                ("figure", PropertyType.to_json)
+                ("figure", PropertyType.to_json),
             ]
             ]
         )
         )
         ._get_chart_config("scatter", "lines+markers")
         ._get_chart_config("scatter", "lines+markers")
@@ -506,7 +502,7 @@ class _Factory:
         "toggle": lambda gui, control_type, attrs: _Builder(
         "toggle": lambda gui, control_type, attrs: _Builder(
             gui=gui, control_type=control_type, element_name="Toggle", attributes=attrs, default_value=None
             gui=gui, control_type=control_type, element_name="Toggle", attributes=attrs, default_value=None
         )
         )
-        .set_value_and_default(with_default=False, var_type=PropertyType.lov_value)
+        .set_value_and_default(with_default=False, var_type=PropertyType.toggle_value)
         ._get_adapter("lov", multi_selection=False)  # need to be called before set_lov
         ._get_adapter("lov", multi_selection=False)  # need to be called before set_lov
         ._set_lov()
         ._set_lov()
         .set_attributes(
         .set_attributes(
@@ -519,6 +515,7 @@ class _Factory:
                 ("unselected_value", PropertyType.string, ""),
                 ("unselected_value", PropertyType.string, ""),
                 ("allow_unselect", PropertyType.boolean),
                 ("allow_unselect", PropertyType.boolean),
                 ("on_change", PropertyType.function),
                 ("on_change", PropertyType.function),
+                ("mode",),
             ]
             ]
         )
         )
         ._set_kind()
         ._set_kind()

+ 1 - 0
taipy/gui/gui_types.py

@@ -132,6 +132,7 @@ class PropertyType(Enum):
     """
     """
     boolean_or_list = "boolean|list"
     boolean_or_list = "boolean|list"
     slider_value = "number|number[]|lovValue"
     slider_value = "number|number[]|lovValue"
+    toggle_value = "boolean|lovValue"
     string_list = "stringlist"
     string_list = "stringlist"
     decimator = Decimator
     decimator = Decimator
     """
     """

+ 5 - 0
taipy/gui/viselements.json

@@ -215,6 +215,11 @@
             "type": "bool",
             "type": "bool",
             "default_value": "False",
             "default_value": "False",
             "doc": "If set, this allows de-selection and the value is set to unselected_value."
             "doc": "If set, this allows de-selection and the value is set to unselected_value."
+          },
+          {
+            "name": "mode",
+            "type": "str",
+            "doc": "Define the way the toggle is displayed:<ul><li>&quote;theme&quote;: synonym for setting the *theme* property to True</li></ul>"
           }
           }
         ]
         ]
       }
       }

+ 1 - 1
tests/gui/builder/control/test_toggle.py

@@ -16,7 +16,7 @@ from taipy.gui import Gui
 def test_toggle_builder(gui: Gui, helpers):
 def test_toggle_builder(gui: Gui, helpers):
     with tgb.Page(frame=None) as page:
     with tgb.Page(frame=None) as page:
         tgb.toggle(theme=True)  # type: ignore[attr-defined]
         tgb.toggle(theme=True)  # type: ignore[attr-defined]
-    expected_list = ["<Toggle", 'kind="theme"', 'unselectedValue=""']
+    expected_list = ["<Toggle", 'mode="theme"', 'unselectedValue=""']
     helpers.test_control_builder(gui, page, expected_list)
     helpers.test_control_builder(gui, page, expected_list)
 
 
 
 

+ 18 - 2
tests/gui/control/test_toggle.py

@@ -14,7 +14,7 @@ from taipy.gui import Gui
 
 
 def test_toggle_md(gui: Gui, helpers):
 def test_toggle_md(gui: Gui, helpers):
     md_string = "<|toggle|theme|>"
     md_string = "<|toggle|theme|>"
-    expected_list = ["<Toggle", 'kind="theme"', 'unselectedValue=""']
+    expected_list = ["<Toggle", 'mode="theme"', 'unselectedValue=""']
     helpers.test_control_md(gui, md_string, expected_list)
     helpers.test_control_md(gui, md_string, expected_list)
 
 
 
 
@@ -44,7 +44,7 @@ def test_toggle_lov_md(gui: Gui, test_client, helpers):
 
 
 def test_toggle_html_1(gui: Gui, helpers):
 def test_toggle_html_1(gui: Gui, helpers):
     html_string = '<taipy:toggle theme="True" />'
     html_string = '<taipy:toggle theme="True" />'
-    expected_list = ["<Toggle", 'kind="theme"', 'unselectedValue=""']
+    expected_list = ["<Toggle", 'mode="theme"', 'unselectedValue=""']
     helpers.test_control_html(gui, html_string, expected_list)
     helpers.test_control_html(gui, html_string, expected_list)
 
 
 
 
@@ -64,3 +64,19 @@ def test_toggle_html_2(gui: Gui, test_client, helpers):
         "value={_TpLv_tpec_TpExPr_x_TPMDL_0}",
         "value={_TpLv_tpec_TpExPr_x_TPMDL_0}",
     ]
     ]
     helpers.test_control_html(gui, html_string, expected_list)
     helpers.test_control_html(gui, html_string, expected_list)
+
+def test_toggle_switch_md(gui: Gui, test_client, helpers):
+    gui._bind_var_val("x", True)
+    md_string = "<|{x}|toggle|label=Label|>"
+    expected_list = [
+        "<Toggle",
+        'isSwitch={true}',
+        'defaultValue={true}',
+        'libClassName="taipy-toggle"',
+        'updateVarName="_TpB_tpec_TpExPr_x_TPMDL_0"',
+        'value={_TpB_tpec_TpExPr_x_TPMDL_0}',
+        'label="Label"',
+    ]
+    helpers.test_control_md(gui, md_string, expected_list)
+
+