From ccc70a1a8516347b87c59b4786de05c374e075c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fred=20Lef=C3=A9v=C3=A8re-Laoide?= Date: Tue, 9 Jul 2024 10:48:35 +0200 Subject: [PATCH] hidden properties password reveal icon pass type resolves #1395 --- .../taipy-gui/src/components/Taipy/Input.tsx | 155 +++++++++++------- .../taipy-gui/src/components/Taipy/Login.tsx | 77 +++++---- taipy/gui/_renderers/builder.py | 6 +- taipy/gui/viselements.json | 6 + tools/gui/generate_pyi.py | 16 +- 5 files changed, 162 insertions(+), 98 deletions(-) diff --git a/frontend/taipy-gui/src/components/Taipy/Input.tsx b/frontend/taipy-gui/src/components/Taipy/Input.tsx index 39c8ad67bf..87718106a1 100644 --- a/frontend/taipy-gui/src/components/Taipy/Input.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Input.tsx @@ -11,10 +11,13 @@ * specific language governing permissions and limitations under the License. */ -import React, { useState, useEffect, useCallback, useRef, KeyboardEvent, useMemo } from "react"; +import React, { useState, useEffect, useCallback, useRef, KeyboardEvent, useMemo, CSSProperties } from "react"; +import IconButton from "@mui/material/IconButton"; +import InputAdornment from "@mui/material/InputAdornment"; import TextField from "@mui/material/TextField"; import Tooltip from "@mui/material/Tooltip"; -import IconButton from "@mui/material/IconButton"; +import Visibility from "@mui/icons-material/Visibility"; +import VisibilityOff from "@mui/icons-material/VisibilityOff"; import ArrowDropUpIcon from "@mui/icons-material/ArrowDropUp"; import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; @@ -36,6 +39,20 @@ const getActionKeys = (keys?: string): string[] => { return ak.length > 0 ? ak : [AUTHORIZED_KEYS[0]]; }; +const numberSx = { + "& input[type=number]::-webkit-outer-spin-button, & input[type=number]::-webkit-inner-spin-button": { + display: "none", + }, + "& input[type=number]": { + MozAppearance: "textfield", + }, +}; +const verticalDivStyle: CSSProperties = { + display: "flex", + flexDirection: "column", + gap: 0, +}; + const Input = (props: TaipyInputProps) => { const { type, @@ -48,6 +65,7 @@ const Input = (props: TaipyInputProps) => { multiline = false, linesShown = 5, } = props; + const [value, setValue] = useState(defaultValue); const dispatch = useDispatch(); const delayCall = useRef(-1); @@ -81,7 +99,7 @@ const Input = (props: TaipyInputProps) => { }, changeDelay); } }, - [updateVarName, dispatch, propagate, onChange, changeDelay, module], + [updateVarName, dispatch, propagate, onChange, changeDelay, module] ); const handleAction = useCallback( @@ -134,7 +152,7 @@ const Input = (props: TaipyInputProps) => { updateVarName, onChange, propagate, - ], + ] ); const roundBasedOnStep = useMemo(() => { @@ -160,7 +178,7 @@ const Input = (props: TaipyInputProps) => { step || 1, stepMultiplier || 10, event.shiftKey, - increment, + increment ); if (min !== undefined && Number(newValue) < min) { return min.toString(); @@ -171,21 +189,89 @@ const Input = (props: TaipyInputProps) => { return newValue; }); }, - [min, max, step, stepMultiplier, calculateNewValue], + [min, max, step, stepMultiplier, calculateNewValue] ); const handleUpStepperMouseDown = useCallback( (event: React.MouseEvent) => { handleStepperMouseDown(event, true); }, - [handleStepperMouseDown], + [handleStepperMouseDown] ); const handleDownStepperMouseDown = useCallback( (event: React.MouseEvent) => { handleStepperMouseDown(event, false); }, - [handleStepperMouseDown], + [handleStepperMouseDown] + ); + + // password + const [showPassword, setShowPassword] = useState(false); + const handleClickShowPassword = useCallback(() => setShowPassword((show) => !show), []); + const handleMouseDownPassword = useCallback( + (event: React.MouseEvent) => event.preventDefault(), + [] + ); + const muiInputProps = useMemo( + () => + type == "password" + ? { + endAdornment: ( + + + {showPassword ? : } + + + ), + } + : type == "number" + ? { + endAdornment: ( +
+ + + + + + +
+ ), + } + : undefined, + [ + type, + showPassword, + handleClickShowPassword, + handleMouseDownPassword, + handleUpStepperMouseDown, + handleDownStepperMouseDown, + ] + ); + + const inputProps = useMemo( + () => + type == "number" + ? { + step: step ? step : 1, + min: min, + max: max, + } + : undefined, + [type, step, min, max] ); useEffect(() => { @@ -197,60 +283,15 @@ const Input = (props: TaipyInputProps) => { return ( - - - - - - - - ), - } - : {} - } + inputProps={inputProps} + InputProps={muiInputProps} label={props.label} onChange={handleInput} disabled={!active} diff --git a/frontend/taipy-gui/src/components/Taipy/Login.tsx b/frontend/taipy-gui/src/components/Taipy/Login.tsx index 80c3ef5b84..c2101f8479 100644 --- a/frontend/taipy-gui/src/components/Taipy/Login.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Login.tsx @@ -11,7 +11,7 @@ * specific language governing permissions and limitations under the License. */ -import React, { ChangeEvent, KeyboardEvent, MouseEvent, useCallback, useEffect, useState } from "react"; +import React, { ChangeEvent, KeyboardEvent, MouseEvent, useCallback, useEffect, useMemo, useState } from "react"; import Button from "@mui/material/Button"; import CircularProgress from "@mui/material/CircularProgress"; import DialogTitle from "@mui/material/DialogTitle"; @@ -19,10 +19,7 @@ import Dialog from "@mui/material/Dialog"; import DialogActions from "@mui/material/DialogActions"; import DialogContent from "@mui/material/DialogContent"; import DialogContentText from "@mui/material/DialogContentText"; -import FormControl from "@mui/material/FormControl"; import InputAdornment from "@mui/material/InputAdornment"; -import InputLabel from "@mui/material/InputLabel"; -import OutlinedInput from "@mui/material/OutlinedInput"; import TextField from "@mui/material/TextField"; import IconButton from "@mui/material/IconButton"; import CloseIcon from "@mui/icons-material/Close"; @@ -51,6 +48,8 @@ const closeSx: SxProps = { alignSelf: "start", }; const titleSx = { m: 0, p: 2, display: "flex", paddingRight: "0.1em" }; +const userProps = { autocomplete: "username" }; +const pwdProps = { autocomplete: "current-password" }; const Login = (props: LoginProps) => { const { id, title = "Log-in", onAction = "on_login", message, defaultMessage } = props; @@ -81,13 +80,6 @@ const Login = (props: LoginProps) => { input == "user" ? setUser(evt.currentTarget.value) : setPassword(evt.currentTarget.value); }, []); - const handleClickShowPassword = useCallback(() => setShowPassword((show) => !show), []); - - const handleMouseDownPassword = useCallback( - (event: React.MouseEvent) => event.preventDefault(), - [] - ); - const handleEnter = useCallback( (evt: KeyboardEvent) => { if (!evt.shiftKey && !evt.ctrlKey && !evt.altKey && evt.key == "Enter") { @@ -98,6 +90,30 @@ const Login = (props: LoginProps) => { [handleAction] ); + // password + const handleClickShowPassword = useCallback(() => setShowPassword((show) => !show), []); + const handleMouseDownPassword = useCallback( + (event: React.MouseEvent) => event.preventDefault(), + [] + ); + const passwordProps = useMemo( + () => ({ + endAdornment: ( + + + {showPassword ? : } + + + ), + }), + [showPassword, handleClickShowPassword, handleMouseDownPassword] + ); + useEffect(() => { nbLogins++; if (nbLogins === 1) { @@ -129,30 +145,23 @@ const Login = (props: LoginProps) => { onChange={changeInput} data-input="user" onKeyDown={handleEnter} + inputProps={userProps} > - - Password - - - {showPassword ? : } - - - } - label="Password" - onKeyDown={handleEnter} - /> - + {message || defaultMessage} diff --git a/taipy/gui/_renderers/builder.py b/taipy/gui/_renderers/builder.py index 3758524216..70dede6c2e 100644 --- a/taipy/gui/_renderers/builder.py +++ b/taipy/gui/_renderers/builder.py @@ -159,7 +159,7 @@ def _get_variable_hash_names( if hashname is None: if callable(v): if v.__name__ == "": - hashname = _get_expr_var_name(v.__code__) + hashname = f"__lambda_{id(v)}" gui._bind_var_val(hashname, v) else: hashname = _get_expr_var_name(v.__name__) @@ -404,7 +404,7 @@ def _get_lov_adapter(self, var_name: str, property_name: t.Optional[str] = None, adapter = self.__gui._get_adapter_for_type(var_type) elif var_type == str.__name__ and callable(adapter): var_type += ( - _get_expr_var_name(str(adapter.__code__)) + f"__lambda_{id(adapter)}" if adapter.__name__ == "" else _get_expr_var_name(adapter.__name__) ) @@ -888,7 +888,7 @@ def _set_table_pagesize_options(self, default_size=None): def _set_input_type(self, type_name: str, allow_password=False): if allow_password and self.__get_boolean_attribute("password", False): return self.set_attribute("type", "password") - return self.set_attribute("type", type_name) + return self.set_attribute("type", self.__attributes.get("type", type_name)) def _set_kind(self): if self.__attributes.get("theme", False): diff --git a/taipy/gui/viselements.json b/taipy/gui/viselements.json index c7238b77af..0e04b42f2d 100644 --- a/taipy/gui/viselements.json +++ b/taipy/gui/viselements.json @@ -109,6 +109,12 @@ "type": "int", "default_value": "5", "doc": "The height of the displayed element if multiline is True." + }, + { + "name": "type", + "type": "str", + "default_value": "text", + "doc": "The type of input: text, tel, email ..." } ] } diff --git a/tools/gui/generate_pyi.py b/tools/gui/generate_pyi.py index c4eba01e71..f2f4e464d0 100644 --- a/tools/gui/generate_pyi.py +++ b/tools/gui/generate_pyi.py @@ -110,7 +110,7 @@ def build_doc(name: str, element: t.Dict[str, t.Any]): r"\1\3", doc, ) - doc = markdownify(doc, strip=['br']) + doc = markdownify(doc, strip=["br"]) return f"{element['name']} ({element['type']}): {doc} {'(default: '+markdownify(element['default_value']) + ')' if 'default_value' in element else ''}" # noqa: E501 @@ -118,16 +118,24 @@ def build_doc(name: str, element: t.Dict[str, t.Any]): name = control_element[0] property_list: t.List[t.Dict[str, t.Any]] = [] property_names: t.List[str] = [] + hidden_properties: t.List[str] = [] for property in get_properties(control_element[1], viselements): - if property["name"] not in property_names and "[" not in property["name"]: + if "hide" in property and property["hide"] is True: + hidden_properties.append(property["name"]) + continue + if ( + property["name"] not in property_names + and "[" not in property["name"] + and property["name"] not in hidden_properties + ): if "default_property" in property and property["default_property"] is True: property_list.insert(0, property) property_names.insert(0, property["name"]) continue property_list.append(property) property_names.append(property["name"]) - properties = ", ".join([f"{p} = ..." for p in property_names]) - doc_arguments = "\n".join([build_doc(name, p) for p in property_list]) + properties = ", ".join([f"{p} = ..." for p in property_names if p not in hidden_properties]) + doc_arguments = "\n".join([build_doc(name, p) for p in property_list if p["name"] not in hidden_properties]) # append properties to __init__.pyi with open(builder_pyi_file, "a") as file: file.write(