From 086f946865a382472439e1a18efdfc7b778b90a6 Mon Sep 17 00:00:00 2001 From: Fabien Lelaquais Date: Mon, 2 Dec 2024 09:35:01 +0100 Subject: [PATCH] Added the action_on_blur property to 'input' and 'number' --- .../taipy-gui/src/components/Taipy/Input.tsx | 126 ++++++++++-------- .../taipy-gui/src/components/Taipy/utils.ts | 1 + taipy/gui/_renderers/factory.py | 2 + taipy/gui/gui.py | 6 +- taipy/gui/viselements.json | 124 +++++++++-------- taipy/gui_core/viselements.json | 2 +- tests/gui/e2e/with_action/test_input.py | 77 +++++++++++ 7 files changed, 219 insertions(+), 119 deletions(-) create mode 100644 tests/gui/e2e/with_action/test_input.py diff --git a/frontend/taipy-gui/src/components/Taipy/Input.tsx b/frontend/taipy-gui/src/components/Taipy/Input.tsx index 1160a8a99c..174cf8ee1f 100644 --- a/frontend/taipy-gui/src/components/Taipy/Input.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Input.tsx @@ -31,9 +31,9 @@ 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]]; @@ -63,6 +63,7 @@ const Input = (props: TaipyInputProps) => { onAction, onChange, multiline = false, + actionOnBlur = false, linesShown = 5, } = props; @@ -85,9 +86,9 @@ const Input = (props: TaipyInputProps) => { () => props.width ? { - ...numberSx, - maxWidth: getCssSize(props.width), - } + ...numberSx, + maxWidth: getCssSize(props.width), + } : numberSx, [props.width] ); @@ -139,19 +140,26 @@ const Input = (props: TaipyInputProps) => { ); const handleBlur = useCallback( - (e: React.FocusEvent) => { - const val = e.target.value; - if (changeDelay > 0 && delayCall.current > 0) { - clearTimeout(delayCall.current); - delayCall.current = -1; - dispatch(createSendUpdateAction(updateVarName, val, module, onChange, propagate)); - } else if (changeDelay === -1) { + (evt: React.FocusEvent) => { + let val = (type === "number") + ? Number(evt.currentTarget.querySelector("input")?.value) + : (multiline + ? evt.currentTarget.querySelector("textarea")?.value + : evt.currentTarget.querySelector("input")?.value) + ; + if (delayCall.current > 0) { + if (changeDelay > 0) { + clearTimeout(delayCall.current); + delayCall.current = -1; + } dispatch(createSendUpdateAction(updateVarName, val, module, onChange, propagate)); } + onAction && dispatch(createSendActionNameAction(id, module, onAction, "Tab", updateVarName, val)); + evt.preventDefault(); }, [dispatch, updateVarName, module, onChange, propagate, changeDelay] ); - + const handleAction = useCallback( (evt: KeyboardEvent) => { if (evt.shiftKey && type === "number") { @@ -279,51 +287,51 @@ const Input = (props: TaipyInputProps) => { () => type == "number" ? { - htmlInput: { - step: step ? step : 1, - min: min, - max: max, - }, - input: { - endAdornment: ( -
- - - - - - -
- ), - }, - } + htmlInput: { + step: step ? step : 1, + min: min, + max: max, + }, + input: { + endAdornment: ( +
+ + + + + + +
+ ), + }, + } : type == "password" - ? { - htmlInput: { autoComplete: "current-password" }, - input: { - endAdornment: ( - - {showPassword ? : } - - ), - }, - } - : undefined, + ? { + htmlInput: { autoComplete: "current-password" }, + input: { + endAdornment: ( + + {showPassword ? : } + + ), + }, + } + : undefined, [ active, type, @@ -357,8 +365,8 @@ const Input = (props: TaipyInputProps) => { id={id} slotProps={inputProps} label={props.label} - onBlur={handleBlur} onChange={handleInput} + onBlur={actionOnBlur ? handleBlur : undefined} disabled={!active} onKeyDown={handleAction} multiline={multiline} diff --git a/frontend/taipy-gui/src/components/Taipy/utils.ts b/frontend/taipy-gui/src/components/Taipy/utils.ts index 7d5f79e455..a714cbbea7 100644 --- a/frontend/taipy-gui/src/components/Taipy/utils.ts +++ b/frontend/taipy-gui/src/components/Taipy/utils.ts @@ -62,6 +62,7 @@ export interface TaipyInputProps extends TaipyActiveProps, TaipyChangeProps, Tai changeDelay?: number; onAction?: string; actionKeys?: string; + actionOnBlur?: boolean; multiline?: boolean; linesShown?: number; width?: string | number; diff --git a/taipy/gui/_renderers/factory.py b/taipy/gui/_renderers/factory.py index 400b3586fc..4945d5fb09 100644 --- a/taipy/gui/_renderers/factory.py +++ b/taipy/gui/_renderers/factory.py @@ -326,6 +326,7 @@ class _Factory: ("action_keys",), ("label",), ("change_delay", PropertyType.number, gui._get_config("change_delay", None)), + ("action_on_blur", PropertyType.boolean, False), ("multiline", PropertyType.boolean, False), ("lines_shown", PropertyType.number, 5), ("width", PropertyType.string_or_number), @@ -434,6 +435,7 @@ class _Factory: ("on_action", PropertyType.function), ("label",), ("change_delay", PropertyType.number, gui._get_config("change_delay", None)), + ("action_on_blur", PropertyType.boolean, False), ("width", PropertyType.string_or_number), ] ), diff --git a/taipy/gui/gui.py b/taipy/gui/gui.py index e5257a62df..e38ba4a603 100644 --- a/taipy/gui/gui.py +++ b/taipy/gui/gui.py @@ -1155,9 +1155,9 @@ def set_unsupported_data_converter(converter: t.Optional[t.Callable[[t.Any], t.A Arguments: converter: A function that converts a value with an unsupported data type (the only - parameter to the function) into data with a supported data type (the returned value - from the function).
- If set to `None`, it removes any existing converter. + parameter to the function) into data with a supported data type (the returned value + from the function).
+ If set to `None`, it removes any existing converter. """ Gui.__unsupported_data_converter = converter diff --git a/taipy/gui/viselements.json b/taipy/gui/viselements.json index 709a5451fa..f87c8bb11c 100644 --- a/taipy/gui/viselements.json +++ b/taipy/gui/viselements.json @@ -23,7 +23,7 @@ { "name": "mode", "type": "str", - "doc": "Define the way the text is processed:\n
  • "raw": synonym for setting the raw property to True
  • "pre": keeps spaces and new lines
  • "markdown" or "md": basic support for Markdown
  • "latex": LaTeχ support
  • " + "doc": "Define the way the text is processed:\n
    • "raw": synonym for setting the raw property to True
    • "pre": keeps spaces and new lines
    • "markdown" or "md": basic support for Markdown
    • "latex": LaTeχ support
    " }, { "name": "format", @@ -34,7 +34,7 @@ "name": "width", "type": "Union[str,int]", "default_value": "None", - "doc": "The width of the element." + "doc": "The width of the text element, in CSS units." } ] } @@ -114,31 +114,37 @@ "name": "password", "type": "bool", "default_value": "False", - "doc": "If True, the text is obscured: all input characters are displayed as an asterisk ('*')." + "doc": "If True, the text is obscured, and all characters are displayed as asterisks ('*').
    This can be useful for sensitive information such as passwords." }, { "name": "label", "type": "str", "default_value": "None", - "doc": "The label associated with the input." + "doc": "The label associated with the input field.
    This provides context to the user and improves accessibility." }, { "name": "multiline", "type": "bool", "default_value": "False", - "doc": "If True, the text is presented as a multi line input." + "doc": "If True, the input is rendered as a multi-line text area
    The default behavior is a single-line input." }, { "name": "lines_shown", "type": "int", "default_value": "5", - "doc": "The number of lines shown in the input control, when multiline is True." + "doc": "The number of lines displayed in the input control when multiline is True." }, { "name": "type", "type": "str", "default_value": "\"text\"", - "doc": "The type of generated input HTML element, as defined in [HTML input types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types).
    This value forces certain values to be entered and can be set to \"text\", \"tel\", \"email\", \"url\"..., among other choices." + "doc": "The type of input element, as per HTML input types.
    This property enforces specific input formats where applicable. Supported values include \"text\", \"tel\", \"email\", \"url\", etc." + }, + { + "name": "action_on_blur", + "type": "bool", + "default_value": "False", + "doc": "If True, the on_action callback is triggered when the input control looses keyboard focus (e.g., when the user presses the Tab key). When this happens, the key name for the event (set in the args property of the payload parameter to the callback function) is set to \"Tab\"." } ] } @@ -162,29 +168,35 @@ "name": "label", "type": "str", "default_value": "None", - "doc": "The label associated with the input." + "doc": "The label associated with the number field.
    This provides context to the user and improves accessibility." }, { "name": "step", "type": "dynamic(Union[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." + "doc": "The increment or decrement applied to the value when the user clicks the arrow buttons." }, { "name": "step_multiplier", "type": "dynamic(Union[int,float])", "default_value": "10", - "doc": "A factor that multiplies step when the user presses the Shift key while clicking one of the arrow buttons." + "doc": "The factor by which the step value is multiplied when the user holds the Shift key while clicking the arrow buttons." }, { "name": "min", "type": "dynamic(Union[int,float])", - "doc": "The minimum value to accept for this input." + "doc": "The minimum acceptable value.
    Values below this threshold are invalid." }, { "name": "max", "type": "dynamic(Union[int,float])", - "doc": "The maximum value to accept for this input." + "doc": "The maximum acceptable value.
    Values above this threshold are invalid." + }, + { + "name": "action_on_blur", + "type": "bool", + "default_value": "False", + "doc": "If True, the on_action callback is triggered when the number control looses keyboard focus (e.g., when the user presses the Tab key). When this happens, the key name for the event (set in the args property of the payload parameter to the callback function) is set to \"Tab\"." } ] } @@ -236,30 +248,30 @@ "name": "continuous", "type": "bool", "default_value": "True", - "doc": "If set to False, the control emits an on_change notification only when the mouse button is released, otherwise notifications are emitted during the cursor movements.
    If lov is defined, the default value is False." + "doc": "If set to False, the control emits an on_change notification only when the mouse button is released, otherwise notifications are emitted during the cursor movements.
    If lov is defined, the default value is False." }, { "name": "change_delay", "type": "int", "default_value": "App config", - "doc": "Minimum time between triggering two on_change callbacks.
    The default value is defined at the application configuration level by the change_delay configuration option. if None or 0, there's no delay." + "doc": "Minimum time between triggering two on_change callbacks.
    The default value is defined at the application configuration level by the change_delay configuration option. if None or 0, there's no delay." }, { "name": "width", "type": "str", "default_value": "\"300px\"", - "doc": "The width of this slider, in CSS units." + "doc": "The width of the slider, in CSS units." }, { "name": "height", "type": "str", - "doc": "The height of this slider, in CSS units.
    It defaults to the value of width when using the vertical orientation." + "doc": "The height of the slider, in CSS units.
    It defaults to the value of width when using the vertical orientation." }, { "name": "orientation", "type": "str", "default_value": "\"horizontal\"", - "doc": "The orientation of this slider.
    Valid values are \"horizontal\" or \"vertical\"." + "doc": "The orientation of the slider.
    Valid values are \"horizontal\" or \"vertical\"." } ] } @@ -588,7 +600,7 @@ { "name": "title", "type": "str", - "doc": "The title of this chart control." + "doc": "The title of the chart control." }, { "name": "render", @@ -659,7 +671,7 @@ { "name": "selected_marker", "type": "indexed(dict[str, Any])", - "doc": "The type of markers used for selected points in the indicated trace.
    See selected marker for more details." + "doc": "The type of markers used for selected points in the indicated trace.
    See
    selected marker for more details." }, { "name": "layout", @@ -700,12 +712,12 @@ "name": "width", "type": "Union[str,int,float]", "default_value": "\"100%\"", - "doc": "The width of this chart, in CSS units." + "doc": "The width of the chart, in CSS units." }, { "name": "height", "type": "Union[str,int,float]", - "doc": "The height of this chart, in CSS units." + "doc": "The height of the chart, in CSS units." }, { "name": "template", @@ -895,13 +907,13 @@ "name": "width", "type": "str", "default_value": "\"100%\"", - "doc": "The width of this table control, in CSS units." + "doc": "The width of the table control, in CSS units." }, { "name": "height", "type": "str", "default_value": "\"80vh\"", - "doc": "The height of this table control, in CSS units." + "doc": "The height of the table control, in CSS units." }, { "name": "filter", @@ -943,7 +955,7 @@ "name": "on_edit", "type": "Union[bool, Callable]", "default_value": "default implementation", - "doc": "A function or the name of a function triggered when an edited cell is validated.
    This function is invoked with the following parameters:
    • state (State^): the state instance.
    • var_name (str): the name of the tabular data variable.
    • payload (dict): a dictionary containing details about the callback invocation, with the following keys:
      • index (int): the row index.
      • col (str): the column name.
      • value (Any): the new cell value, cast to the column's data type.
      • user_value (str): the new cell value, as entered by the user.
      • tz (str): the timezone, if the column type is date.
    If this property is set to False, the table does not provide the cell editing functionality.
    If this property is not set, the table will use the default implementation for editing cells.", + "doc": "A function or the name of a function triggered when an edited cell is validated.
    This function is invoked with the following parameters:
    • state (State^): the state instance.
    • var_name (str): the name of the tabular data variable.
    • payload (dict): a dictionary containing details about the callback invocation, with the following keys:
      • index (int): the row index.
      • col (str): the column name.
      • value (Any): the new cell value, cast to the column's data type.
      • user_value (str): the new cell value, as entered by the user.
      • tz (str): the timezone, if the column type is date.
    If this property is set to False, the table does not provide the cell editing functionality.
    If this property is not set, the table will use the default implementation for editing cells.", "signature": [ [ "state", @@ -1085,7 +1097,7 @@ { "name": "mode", "type": "str", - "doc": "Define the way the selector is displayed:\n
    • "radio": as a list of radio buttons
    • "check": as a list of check boxes
    • any other value: a plain list." + "doc": "Define the way the selector is displayed:\n
      • "radio": as a list of radio buttons
      • "check": as a list of check boxes
      • any other value: a plain list.
      " }, { "name": "dropdown", @@ -1109,12 +1121,12 @@ "name": "width", "type": "Union[str,int]", "default_value": "\"360px\"", - "doc": "The width of this selector, in CSS units." + "doc": "The width of the selector, in CSS units." }, { "name": "height", "type": "Union[str,int]", - "doc": "The height of this selector, in CSS units." + "doc": "The height of the selector, in CSS units." } ] } @@ -1308,12 +1320,12 @@ "name": "width", "type": "Union[str,int,float]", "default_value": "\"300px\"", - "doc": "The width of this image control, in CSS units." + "doc": "The width of the image control, in CSS units." }, { "name": "height", "type": "Union[str,int,float]", - "doc": "The height of this image control, in CSS units." + "doc": "The height of the image control, in CSS units." } ] } @@ -1341,13 +1353,13 @@ "name": "min", "type": "Union[int,float]", "default_value": "0", - "doc": "The minimum value of this metric control's gauge." + "doc": "The minimum value of the metric control's gauge." }, { "name": "max", "type": "Union[int,float]", "default_value": "100", - "doc": "The maximum value of this metric control's gauge." + "doc": "The maximum value of the metric control's gauge." }, { "name": "delta", @@ -1480,7 +1492,7 @@ "name": "width", "type": "Union[str,int]", "default_value": "None", - "doc": "The width of this progress indicator, in CSS units." + "doc": "The width of the progress indicator, in CSS units." } ] } @@ -1525,7 +1537,7 @@ "name": "orientation", "type": "str", "default_value": "\"horizontal\"", - "doc": "The orientation of this slider." + "doc": "The orientation of the indicator." }, { "name": "width", @@ -1596,7 +1608,7 @@ { "name": "adapter", "type": "Union[str, Callable]", - "default_value": "lambda x: str(x)", + "default_value": "lambda x: str(x)", "doc": "A function or the name of the function that transforms an element of lov into a tuple(id:str, label:Union[str,Icon]).
      The default value is a function that returns the string representation of the lov element." }, { @@ -1801,7 +1813,7 @@ { "name": "height", "type": "Union[str,int,float]", - "doc": "The maximum height of this chat control, in CSS units." + "doc": "The maximum height of the chat control, in CSS units." }, { "name": "show_sender", @@ -1858,7 +1870,7 @@ { "name": "row_height", "type": "str", - "doc": "The height of each row of this tree, in CSS units." + "doc": "The height of each row of the tree, in CSS units." }, { "name": "mode", @@ -1901,7 +1913,7 @@ { "name": "height", "type": "dynamic(str)", - "doc": "The height, in CSS units, of this block." + "doc": "The height of the part, in CSS units." }, { "name": "content", @@ -1924,7 +1936,7 @@ "name": "title", "default_property": true, "type": "dynamic(str)", - "doc": "Title of this block element." + "doc": "Title of the expandable block." }, { "name": "expanded", @@ -1974,22 +1986,22 @@ "name": "close_label", "type": "str", "default_value": "\"Close\"", - "doc": "The tooltip of the top-right close icon button. In the on_action callback, args will be set to -1." + "doc": "The tooltip of the top-right close icon button. In the on_action callback, args will be set to -1." }, { "name": "labels", "type": "Union[str,list[str]]", - "doc": "A list of labels to show in a row of buttons at the bottom of the dialog. The index of the button in the list is reported as args in the on_action callback (that index is -1 for the close icon)." + "doc": "A list of labels to show in a row of buttons at the bottom of the dialog. The index of the button in the list is reported as args in the on_action callback (that index is -1 for the close icon)." }, { "name": "width", "type": "Union[str,int,float]", - "doc": "The width of this dialog, in CSS units." + "doc": "The width of the dialog, in CSS units." }, { "name": "height", "type": "Union[str,int,float]", - "doc": "The height of this dialog, in CSS units." + "doc": "The height of the dialog, in CSS units." } ] } @@ -2071,13 +2083,13 @@ "name": "width", "type": "str", "default_value": "\"30vw\"", - "doc": "Width, in CSS units, of this pane.
      This is used only if anchor is \"left\" or \"right\"." + "doc": "Width of the pane, in CSS units.
      This is used only if anchor is \"left\" or \"right\"." }, { "name": "height", "type": "str", "default_value": "\"30vh\"", - "doc": "Height, in CSS units, of this pane.
      This is used only if anchor is \"top\" or \"bottom\"." + "doc": "Height of this pane, in CSS units.
      This is used only if anchor is \"top\" or \"bottom\"." }, { "name": "show_button", @@ -2098,7 +2110,7 @@ "name": "active", "type": "dynamic(bool)", "default_value": "True", - "doc": "Indicates if this component is active.
      An inactive component allows no user interaction." + "doc": "Indicates if this element is active.
      If False, the element is disabled, and user interaction is not allowed." } ] } @@ -2124,7 +2136,7 @@ { "name": "adapter", "type": "Union[str, Callable]", - "default_value": "lambda x: str(x)", + "default_value": "lambda x: str(x)", "doc": "A function or the name of the function that transforms an element of lov into a tuple(id:str, label:Union[str,Icon]).
      The default value is a function that returns the string representation of the lov element." }, { @@ -2149,7 +2161,7 @@ { "name": "on_change", "type": "Union[str, Callable]", - "doc": "A function or the name of a function that is triggered when the value is updated.
      This function is invoked with the following parameters:
        \n
      • state (State^): the state instance.
      • var_name (str): the variable name.
      • value (Any): the new value.
      ", + "doc": "A function or the name of a function that is triggered when the value changes.
      The callback function receives the following parameters:
        \n
      • state (State^): the state instance.
      • var_name (str): the bound variable name.
      • value (Any): the updated value.
      ", "signature": [ [ "state", @@ -2197,7 +2209,7 @@ "name": "propagate", "type": "bool", "default_value": "App config", - "doc": "Allows the control's main value to be automatically propagated.
      The default value is defined at the application configuration level by the propagate configuration option.
      If True, any change to the control's value is immediately reflected in the bound application variable." + "doc": "Determines whether the control's value is automatically reflected in the bound application variable.
      The default value is defined at the application configuration level by the propagate configuration option.
      If True, any change to the control's value is immediately reflected in the variable." } ] } @@ -2210,12 +2222,12 @@ "name": "change_delay", "type": "int", "default_value": "App config", - "doc": "Minimum interval between two consecutive calls to the on_change callback.
      The default value is defined at the application configuration level by the change_delay configuration option.
      if None, the delay is set to 300 ms.
      If set to -1, the input change is triggered only when the user presses the Enter key." + "doc": "The minimum interval (in milliseconds) between two consecutive calls to the on_change callback.
      The default value is defined at the application configuration level by the change_delay configuration option.
      if None, the delay is set to 300 ms.
      If set to -1, the callback is triggered only when the user presses the Enter key." }, { "name": "on_action", "type": "Union[str, Callable]", - "doc": "A function or the name of a function that is triggered when a specific key is pressed.
      This function is invoked with the following parameters:
        \n
      • state (State^): the state instance.
      • id (str): the identifier of the control if it has one.
      • payload (dict): the details on this callback's invocation.
        \nThis dictionary has the following keys:\n
          \n
        • action: the name of the action that triggered this callback.
        • args (list):\n
          • key name
          • variable name
          • current value
      ", + "doc": "A function or the name of a function that is triggered when a specific key is pressed.
      The callback function is invoked with the following parameters:
        \n
      • state (State^): the state instance.
      • id (str): the identifier of the control if it has one.
      • payload (dict): the callback details
        \nThis dictionary has the following keys:\n
          \n
        • action: the name of the action that triggered this callback.
        • args (list):\n
          • The key name pressed.
          • The variable name.
          • The current value of the variable.
      ", "signature": [ [ "state", @@ -2235,13 +2247,13 @@ "name": "action_keys", "type": "str", "default_value": "\"Enter\"", - "doc": "Semicolon (';')-separated list of supported key names.
      Authorized values are Enter, Escape, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12." + "doc": "A semicolon-separated list of keys that can trigger the on_action callback.
      Authorized values are Enter, Escape, and function keys F1 to F12." }, { "name": "width", "type": "Union[str,int]", "default_value": "None", - "doc": "The width of the element." + "doc": "The width of the element, in CSS units." } ] } @@ -2253,22 +2265,22 @@ { "name": "id", "type": "str", - "doc": "The identifier that is assigned to the rendered HTML component." + "doc": "The identifier assigned to the rendered HTML component.
      This can be used in callbacks or to target the element for styling." }, { "name": "properties", "type": "dict[str, Any]", - "doc": "Bound to a dictionary that contains additional properties for this element." + "doc": "A dictionary of additional properties that can be set to the element." }, { "name": "class_name", "type": "dynamic(str)", - "doc": "The list of CSS class names that are associated with the generated HTML Element.
      These class names are added to the default taipy-[element_type] class name." + "doc": "A space-separated list of CSS class names to be applied to the generated HTML element.
      These classes are added to the default taipy-[element_type] class." }, { "name": "hover_text", "type": "dynamic(str)", - "doc": "The information that is displayed when the user hovers over this element." + "doc": "The text that is displayed when the user hovers over the element." } ] } diff --git a/taipy/gui_core/viselements.json b/taipy/gui_core/viselements.json index deeb270e2d..2dc0394d06 100644 --- a/taipy/gui_core/viselements.json +++ b/taipy/gui_core/viselements.json @@ -208,7 +208,7 @@ { "name": "on_submission_change", "type": "Union[str, Callable]", - "doc": "A function or the name of a function that is triggered when a submission status is changed.

      All the parameters of that function are optional:\n
        \n
      • state (State^): the state instance.
      • \n
      • submission (Submission): the submission entity containing submission information.
      • \n
      • details (dict): the details on this callback's invocation.
        \nThis dictionary has the following keys:\n
          \n
        • submission_status (str): the new status of the submission (possible values are: \"SUBMITTED\", \"COMPLETED\", \"CANCELED\", \"FAILED\", \"BLOCKED\", \"WAITING\", or \"RUNNING\").
        • \n
        • job: the Job (if any) that is at the origin of the submission status change.
        • \n
        • submittable_entity (Submittable): the entity (usually a Scenario) that was submitted.
        • \n
        ", + "doc": "A function or the name of a function that is triggered when a submission status is changed.

        All the parameters of that function are optional:\n
          \n
        • state (State^): the state instance.
        • \n
        • submission (Submission): the submission entity containing submission information.
        • \n
        • details (dict): the details on this callback's invocation.
          \nThis dictionary has the following keys:\n
            \n
          • submission_status (str): the new status of the submission (possible values are: \"SUBMITTED\", \"COMPLETED\", \"CANCELED\", \"FAILED\", \"BLOCKED\", \"WAITING\", or \"RUNNING\").
          • \n
          • job: the Job (if any) that is at the origin of the submission status change.
          • \n
          • submittable_entity (Submittable): the entity (usually a Scenario) that was submitted.
        ", "signature": [ [ "state", diff --git a/tests/gui/e2e/with_action/test_input.py b/tests/gui/e2e/with_action/test_input.py new file mode 100644 index 0000000000..0951e2ac7f --- /dev/null +++ b/tests/gui/e2e/with_action/test_input.py @@ -0,0 +1,77 @@ +# 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. + +import inspect +import logging +from importlib import util + +import pytest + +if util.find_spec("playwright"): + from playwright._impl._page import Page + +from taipy.gui import Gui + + +@pytest.mark.teste2e +def test_input_action(page: "Page", gui: Gui, helpers): + page_md = """ +<|{input1_value}|input|on_action=input_action|id=input1|> +<|{input2_value}|input|on_action=input_action|id=input2|action_on_blur|> +<|X|button|id=button1|on_action=button_action|> +<|{input1_action_tracker}|id=input1_tracker|> +<|{input2_action_tracker}|id=input2_tracker|> +<|{button_action_tracker}|id=button_tracker|> +""" + input1_value = "init" # noqa: F841 + input2_value = "init" # noqa: F841 + input1_action_tracker = 0 # noqa: F841 + input2_action_tracker = 0 # noqa: F841 + button_action_tracker = 0 # noqa: F841 + + def input_action(state, id): + if id == "input1": + state.input1_action_tracker = state.input1_action_tracker + 1 + elif id == "input2": + state.input2_action_tracker = state.input2_action_tracker + 1 + + def button_action(state, id): + state.button_action_tracker = state.button_action_tracker + 1 + + gui._set_frame(inspect.currentframe()) + gui.add_page(name="test", page=page_md) + helpers.run_e2e(gui) + page.goto("./test") + page.expect_websocket() + page.wait_for_selector("#input1_tracker") + assert page.query_selector("#input1").input_value() == "init" + page.click("#button1") + try: + page.wait_for_function("document.querySelector('#button_tracker').innerText !== '0'") + except Exception as e: + logging.getLogger().debug(f"Function evaluation timeout.\n{e}") + assert page.query_selector("#button_tracker").inner_text() == "1" + page.click("#input1") + page.fill("#input1", "step2") + page.click("#button1") + assert ( + page.query_selector("#input1_tracker").inner_text() == "0" + ), "Action should not have been invoked (no action_on_blur)" + assert page.query_selector("#button_tracker").inner_text() == "2", "Button action should have been invoked" + page.click("#input2") + page.fill("#input2", "step2") + page.click("#button1") + assert page.query_selector("#button_tracker").inner_text() == "3", "Button action should have been invoked" + page.click("#button1") + assert ( + page.query_selector("#input2_tracker").inner_text() == "1" + ), "Action should have been invoked (action_on_blur)" + assert page.query_selector("#button_tracker").inner_text() == "4"