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

Number visual element #1442

25 changes: 25 additions & 0 deletions doc/gui/examples/controls/number-step.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2021-2024 Avaiga Private Limited
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.
# -----------------------------------------------------------------------------------------
# To execute this script, make sure that the taipy-gui package is installed in your
# Python environment and run:
# python <script>
# -----------------------------------------------------------------------------------------
from taipy.gui import Gui

value = 50

page = """
<|{value}|number|step=2|>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

"""

Gui(page).run()

87 changes: 78 additions & 9 deletions frontend/taipy-gui/src/components/Taipy/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,36 @@
* specific language governing permissions and limitations under the License.
*/

import React, { useState, useEffect, useCallback, useRef, KeyboardEvent } from "react";
import React, {useState, useEffect, useCallback, useRef, KeyboardEvent, useMemo} from "react";
import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip";
import {styled} from '@mui/material/styles';
import IconButton from "@mui/material/IconButton";
import ArrowDropUpIcon from '@mui/icons-material/ArrowDropUp';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';

import { createSendActionNameAction, createSendUpdateAction } from "../../context/taipyReducers";
import { TaipyInputProps } from "./utils";
import { useClassNames, useDispatch, useDynamicProperty, useModule } from "../../utils/hooks";
import {createSendActionNameAction, createSendUpdateAction} from "../../context/taipyReducers";
import {TaipyInputProps} from "./utils";
import {useClassNames, useDispatch, useDynamicProperty, useModule} from "../../utils/hooks";

const AUTHORIZED_KEYS = ["Enter", "Escape", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"];

const StyledTextField = styled(TextField)({
'& input[type=number]::-webkit-outer-spin-button, & input[type=number]::-webkit-inner-spin-button': {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure we want to use WebKit or mozilla specific css...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do if this is mandatory for specific browsers.
I don't like it either though.

display: 'none',
},
'& input[type=number]': {
'MozAppearance': 'textfield',
},
});

const getActionKeys = (keys?: string): string[] => {
const ak = (
keys
? keys
.split(";")
.map((v) => v.trim().toLowerCase())
.filter((v) => AUTHORIZED_KEYS.some((k) => k.toLowerCase() === v))
.split(";")
.map((v) => v.trim().toLowerCase())
.filter((v) => AUTHORIZED_KEYS.some((k) => k.toLowerCase() === v))
: []
).map((v) => AUTHORIZED_KEYS.find((k) => k.toLowerCase() == v) as string);
return ak.length > 0 ? ak : [AUTHORIZED_KEYS[0]];
Expand Down Expand Up @@ -55,6 +68,8 @@ const Input = (props: TaipyInputProps) => {
const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
const active = useDynamicProperty(props.active, props.defaultActive, true);
const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
const step = useDynamicProperty(props.step, props.defaultStep, 1);
const stepMultiplier = useDynamicProperty(props.stepMultiplier, props.defaultStepMultiplier, 10);

const handleInput = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
Expand All @@ -79,6 +94,12 @@ const Input = (props: TaipyInputProps) => {

const handleAction = useCallback(
(evt: KeyboardEvent<HTMLDivElement>) => {
if (evt.shiftKey && evt.key === 'ArrowUp') {
setValue(((Number(evt.currentTarget.querySelector("input")?.value || 0) + (step || 1) * (stepMultiplier || 10) - (step || 1)).toString()));
}
if (evt.shiftKey && evt.key === 'ArrowDown') {
namnguyen20999 marked this conversation as resolved.
Show resolved Hide resolved
setValue(((Number(evt.currentTarget.querySelector("input")?.value || 0) - (step || 1) * (stepMultiplier || 10) + (step || 1)).toString()));
}
if (!evt.shiftKey && !evt.ctrlKey && !evt.altKey && actionKeys.includes(evt.key)) {
namnguyen20999 marked this conversation as resolved.
Show resolved Hide resolved
const val = evt.currentTarget.querySelector("input")?.value;
if (changeDelay > 0 && delayCall.current > 0) {
Expand All @@ -92,9 +113,36 @@ const Input = (props: TaipyInputProps) => {
evt.preventDefault();
}
},
[actionKeys, updateVarName, onAction, id, dispatch, onChange, changeDelay, propagate, module]
[actionKeys, step, stepMultiplier, changeDelay, onAction, dispatch, id, module, updateVarName, onChange, propagate]
);

const roundBasedOnStep = useMemo(() => {
const stepString = (step || 1).toString();
const decimalPlaces = stepString.includes('.') ? stepString.split('.')[1].length : 0;
const multiplier = Math.pow(10, decimalPlaces);
return (value: number) => Math.round(value * multiplier) / multiplier;
}, [step]);

const calculateNewValue = useMemo(() => {
return (prevValue: string, step: number, stepMultiplier: number, shiftKey: boolean, increment: boolean) => {
const multiplier = shiftKey ? stepMultiplier : 1;
const change = step * multiplier * (increment ? 1 : -1);
return roundBasedOnStep(Number(prevValue) + change).toString();
};
}, [roundBasedOnStep]);

const handleStepperMouseDown = useCallback((event: React.MouseEvent<HTMLButtonElement>, increment: boolean) => {
setValue(prevValue => calculateNewValue(prevValue, step || 1, stepMultiplier || 10, event.shiftKey, increment));
}, [step, stepMultiplier, calculateNewValue]);

const handleUpStepperMouseDown = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
handleStepperMouseDown(event, true);
}, [handleStepperMouseDown]);

const handleDownStepperMouseDown = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
handleStepperMouseDown(event, false);
}, [handleStepperMouseDown]);

useEffect(() => {
if (props.value !== undefined) {
setValue(props.value);
Expand All @@ -103,13 +151,34 @@ const Input = (props: TaipyInputProps) => {

return (
<Tooltip title={hover || ""}>
<TextField
<StyledTextField
margin="dense"
hiddenLabel
value={value ?? ""}
className={className}
type={type}
id={id}
inputProps={{
step: step ? step : 1,
}}
InputProps={{
FredLL-Avaiga marked this conversation as resolved.
Show resolved Hide resolved
endAdornment: (
<>
<IconButton
size="small"
onMouseDown={handleUpStepperMouseDown}
>
<ArrowDropUpIcon />
</IconButton>
<IconButton
size="small"
onMouseDown={handleDownStepperMouseDown}
>
<ArrowDropDownIcon />
</IconButton>
</>
),
}}
label={props.label}
onChange={handleInput}
disabled={!active}
Expand Down
4 changes: 4 additions & 0 deletions frontend/taipy-gui/src/components/Taipy/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ export interface TaipyInputProps extends TaipyActiveProps, TaipyChangeProps, Tai
type: string;
value: string;
defaultValue?: string;
step?: number;
defaultStep?: number;
stepMultiplier?: number;
defaultStepMultiplier?: number;
changeDelay?: number;
onAction?: string;
actionKeys?: string;
Expand Down
2 changes: 2 additions & 0 deletions taipy/gui/_renderers/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,8 @@ class _Factory:
.set_attributes(
[
("active", PropertyType.dynamic_boolean, True),
("step", PropertyType.dynamic_number, 1),
("step_multiplier", PropertyType.dynamic_number, 10),
("hover_text", PropertyType.dynamic_string),
("on_change", PropertyType.function),
("on_action", PropertyType.function),
Expand Down
12 changes: 12 additions & 0 deletions taipy/gui/viselements.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,18 @@
"type": "str",
"default_value": "None",
"doc": "The label associated with the input."
},
{
"name": "step",
"type": "dynamic(int|float)",
"default_value": "1",
"doc": "The amount by which the value is incremented or decremented when the user clicks one of the arrow buttons."
},
{
"name": "step_multiplier",
"type": "dynamic(int|float)",
"default_value": "10",
"doc": "A factor that multiplies <i>step</i> when the user presses the Shift key while clicking one of the arrow buttons."
}
]
}
Expand Down
Loading