Repository: gradio-app/gradio

Issue:
Textbox update failed sometime
### Describe the bug

When you set the textbox's type with `text`, `gr.update(type="password")` will fail. The textbox won't mask.
But when you set the textbox's type with `password`, `gr.update(type="text")` will work.

### Have you searched existing issues?  🔎

- [x] I have searched and found no existing issues

### Reproduction

```python
import gradio as gr

# Define a function that takes a name and returns a greeting.
def greet(name):
    return "Hello " + name + "!"

# Define a function that handles the selection change in the dropdown.
def handle_selection(selection):
    if selection in ['2', '3']:
        return gr.update(type="password")
    else:
        return gr.update(type="text")

# Create a Gradio interface with a textbox, a dropdown, and a button.
with gr.Blocks() as demo:
    name = gr.Textbox(label="Name")
    output = gr.Textbox(label="Output Box", type="text")
    greet_btn = gr.Button("Greet")
    select_box = gr.Dropdown(
        label="Service",
        choices=['1', '2', '3'],
        value="1",
    )
    
    # Add a click event to the greet button to call the greet function.
    greet_btn.click(fn=greet, inputs=name, outputs=output, api_name="greet")
    
    # Add a select event to the dropdown to call the handle_selection function.
    select_box.select(fn=handle_selection, inputs=select_box, outputs=output)

if __name__ == "__main__":
    demo.launch(show_error=True)
```
When I select different options in the dropdown, nothing changes.
```Python
import gradio as gr

# Define a function that takes a name and returns a greeting.
def greet(name):
    return "Hello " + name + "!"

# Define a function that handles the selection change in the dropdown.
def handle_selection(selection):
    if selection in ['2', '3']:
        return gr.update(type="password")
    else:
        return gr.update(type="text")

# Create a Gradio interface with a textbox, a dropdown, and a button.
with gr.Blocks() as demo:
    name = gr.Textbox(label="Name")
    output = gr.Textbox(label="Output Box", type="password")
    greet_btn = gr.Button("Greet")
    select_box = gr.Dropdown(
        label="Service",
        choices=['1', '2', '3'],
        value="1",
    )
    
    # Add a click event to the greet button to call the greet function.
    greet_btn.click(fn=greet, inputs=name, outputs=output, api_name="greet")
    
    # Add a select event to the dropdown to call the handle_selection function.
    select_box.select(fn=handle_selection, inputs=select_box, outputs=output)

if __name__ == "__main__":
    demo.launch(show_error=True)
```
With this code, textbox will mask when I select '2' or '3' in dropdown.

### Screenshot

_No response_

### Logs

```shell

```

### System Info

```shell
I run the code in [gradio playgroud](https://www.gradio.app/playground).
```

### Severity

I can work around it

Hints from issue comments:
@abidlabs: Hi @hellofinch please set `max_lines=1` in `gr.Textbox`, as this is necessary when `type="password"`. We'll add a warning to make this clearer for developers. You can verify with this example in the [Playground](https://www.gradio.app/playground?demo=Hello_World&code=aW1wb3J0IGdyYWRpbyBhcyBncgoKd2l0aCBnci5CbG9ja3MoKSBhcyBkZW1vOgogIGQgPSBnci5Ecm9wZG93bihbInRleHQiLCAicGFzc3dvcmQiXSkKICB0ID0gZ3IuVGV4dGJveChpbnRlcmFjdGl2ZT1UcnVlLCB0eXBlPSJ0ZXh0IiwgbWF4X2xpbmVzPTEpCgogIGQuY2hhbmdlKGxhbWJkYSB4OiBnci51cGRhdGUodHlwZT14KSwgZCwgdCkKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICBkZW1vLmxhdW5jaCgpCg%3D%3D&reqs=):

```py
import gradio as gr

with gr.Blocks() as demo:
  d = gr.Dropdown(["text", "password"])
  t = gr.Textbox(interactive=True, type="text", max_lines=1)

  d.change(lambda x: gr.update(type=x), d, t)

if __name__ == "__main__":
    demo.launch()

```@hellofinch: Thanks! I have another question. If the textbox is multiline, is the mask meaningful? I'm not sure.@abidlabs: No, it has no effect. In the PR to close this issue, I've now added a warning if a user tries to set lines or max_lines to be greater than 1.

Solution patch:
diff --git a/.changeset/giant-donkeys-pump.md b/.changeset/giant-donkeys-pump.md
new file mode 100644
index 000000000..528f6c89c
--- /dev/null
+++ b/.changeset/giant-donkeys-pump.md
@@ -0,0 +1,6 @@
+---
+"@gradio/textbox": patch
+"gradio": patch
+---
+
+fix:Move `gr.Textbox` lines logic to frontend
diff --git a/gradio/components/textbox.py b/gradio/components/textbox.py
index b864003fa..3f51270c0 100644
--- a/gradio/components/textbox.py
+++ b/gradio/components/textbox.py
@@ -2,6 +2,7 @@
 
 from __future__ import annotations
 
+import warnings
 from collections.abc import Callable, Sequence
 from typing import TYPE_CHECKING, Any, Literal
 
@@ -38,8 +39,9 @@ class Textbox(FormComponent):
         self,
         value: str | Callable | None = None,
         *,
+        type: Literal["text", "password", "email"] = "text",
         lines: int = 1,
-        max_lines: int = 20,
+        max_lines: int | None = None,
         placeholder: str | None = None,
         label: str | None = None,
         info: str | None = None,
@@ -57,7 +59,6 @@ class Textbox(FormComponent):
         elem_classes: list[str] | str | None = None,
         render: bool = True,
         key: int | str | None = None,
-        type: Literal["text", "password", "email"] = "text",
         text_align: Literal["left", "right"] | None = None,
         rtl: bool = False,
         show_copy_button: bool = False,
@@ -68,8 +69,9 @@ class Textbox(FormComponent):
         """
         Parameters:
             value: text to show in textbox. If a function is provided, the function will be called each time the app loads to set the initial value of this component.
+            type: The type of textbox. One of: 'text' (which allows users to enter any text), 'password' (which masks text entered by the user), 'email' (which suggests email input to the browser). For "password" and "email" types, `lines` must be 1 and `max_lines` must be None or 1.
             lines: minimum number of line rows to provide in textarea.
-            max_lines: maximum number of line rows to provide in textarea.
+            max_lines: maximum number of line rows to provide in textarea. Must be at least `lines`. If not provided, the maximum number of lines is max(lines, 20) for "text" type, and 1 for "password" and "email" types.
             placeholder: placeholder hint to provide behind textarea.
             label: the label for this component, displayed above the component if `show_label` is `True` and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component corresponds to.
             info: additional component description, appears below the label in smaller font. Supports markdown / HTML syntax.
@@ -86,7 +88,6 @@ class Textbox(FormComponent):
             elem_classes: An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.
             render: If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.
             key: if assigned, will be used to assume identity across a re-render. Components that have the same key across a re-render will have their value preserved.
-            type: The type of textbox. One of: 'text', 'password', 'email', Default is 'text'.
             text_align: How to align the text in the textbox, can be: "left", "right", or None (default). If None, the alignment is left if `rtl` is False, or right if `rtl` is True. Can only be changed if `type` is "text".
             rtl: If True and `type` is "text", sets the direction of the text to right-to-left (cursor appears on the left of the text). Default is False, which renders cursor on the right.
             show_copy_button: If True, includes a copy button to copy the text in the textbox. Only applies if show_label is True.
@@ -96,12 +97,20 @@ class Textbox(FormComponent):
         """
         if type not in ["text", "password", "email"]:
             raise ValueError('`type` must be one of "text", "password", or "email".')
+        if type in ["password", "email"]:
+            if lines != 1:
+                warnings.warn(
+                    "The `lines` parameter must be 1 for `type` of 'password' or 'email'. Setting `lines` to 1."
+                )
+                lines = 1
+            if max_lines not in [None, 1]:
+                warnings.warn(
+                    "The `max_lines` parameter must be None or 1 for `type` of 'password' or 'email'. Setting `max_lines` to 1."
+                )
+                max_lines = 1
 
         self.lines = lines
-        if type == "text":
-            self.max_lines = max(lines, max_lines)
-        else:
-            self.max_lines = 1
+        self.max_lines = max_lines
         self.placeholder = placeholder
         self.show_copy_button = show_copy_button
         self.submit_btn = submit_btn
diff --git a/js/textbox/Index.svelte b/js/textbox/Index.svelte
index d35f388ea..f9909d140 100644
--- a/js/textbox/Index.svelte
+++ b/js/textbox/Index.svelte
@@ -32,7 +32,7 @@
 	export let lines: number;
 	export let placeholder = "";
 	export let show_label: boolean;
-	export let max_lines: number;
+	export let max_lines: number | undefined = undefined;
 	export let type: "text" | "password" | "email" = "text";
 	export let container = true;
 	export let scale: number | null = null;
@@ -80,7 +80,7 @@
 		{type}
 		{rtl}
 		{text_align}
-		max_lines={!max_lines ? lines + 1 : max_lines}
+		{max_lines}
 		{placeholder}
 		{submit_btn}
 		{stop_btn}
diff --git a/js/textbox/shared/Textbox.svelte b/js/textbox/shared/Textbox.svelte
index 4a59eceb9..5622bf9f4 100644
--- a/js/textbox/shared/Textbox.svelte
+++ b/js/textbox/shared/Textbox.svelte
@@ -19,7 +19,7 @@
 	export let disabled = false;
 	export let show_label = true;
 	export let container = true;
-	export let max_lines: number;
+	export let max_lines: number | undefined = undefined;
 	export let type: "text" | "password" | "email" = "text";
 	export let show_copy_button = false;
 	export let submit_btn: string | boolean | null = null;
@@ -37,10 +37,21 @@
 	let can_scroll: boolean;
 	let previous_scroll_top = 0;
 	let user_has_scrolled_up = false;
+	let _max_lines: number;
 
 	const show_textbox_border = !submit_btn;
 
-	$: value, el && lines !== max_lines && resize({ target: el });
+	$: if (max_lines === undefined) {
+		if (type === "text") {
+			_max_lines = Math.max(lines, 20);
+		} else {
+			_max_lines = 1;
+		}
+	} else {
+		_max_lines = Math.max(max_lines, lines);
+	}
+
+	$: value, el && lines !== _max_lines && resize({ target: el });
 
 	$: if (value === null) value = "";
 
@@ -119,7 +130,7 @@
 			e.key === "Enter" &&
 			!e.shiftKey &&
 			lines === 1 &&
-			max_lines >= 1
+			_max_lines >= 1
 		) {
 			e.preventDefault();
 			dispatch("submit");
@@ -153,7 +164,7 @@
 		event: Event | { target: HTMLTextAreaElement | HTMLInputElement }
 	): Promise<void> {
 		await tick();
-		if (lines === max_lines) return;
+		if (lines === _max_lines) return;
 
 		const target = event.target as HTMLTextAreaElement;
 		const computed_styles = window.getComputedStyle(target);
@@ -162,9 +173,9 @@
 		const line_height = parseFloat(computed_styles.lineHeight);
 
 		let max =
-			max_lines === undefined
+			_max_lines === undefined
 				? false
-				: padding_top + padding_bottom + line_height * max_lines;
+				: padding_top + padding_bottom + line_height * _max_lines;
 		let min = padding_top + padding_bottom + lines * line_height;
 
 		target.style.height = "1px";
@@ -185,7 +196,7 @@
 		_el: HTMLTextAreaElement,
 		_value: string
 	): any | undefined {
-		if (lines === max_lines) return;
+		if (lines === _max_lines) return;
 		_el.style.overflowY = "scroll";
 		_el.addEventListener("input", resize);
 
@@ -220,7 +231,7 @@
 	<BlockTitle {root} {show_label} {info}>{label}</BlockTitle>
 
 	<div class="input-container">
-		{#if lines === 1 && max_lines === 1}
+		{#if lines === 1 && _max_lines === 1}
 			{#if type === "text"}
 				<input
 					data-testid="textbox"


Test patch:
diff --git a/js/spa/test/todo_list.spec.ts b/js/spa/test/todo_list.spec.ts
index f6bd7792c..0c96d71f8 100644
--- a/js/spa/test/todo_list.spec.ts
+++ b/js/spa/test/todo_list.spec.ts
@@ -12,23 +12,21 @@ test("clicking through tabs shows correct content", async ({ page }) => {
 	await expect(page.locator("body")).not.toContainText("Incomplete Tasks (0)");
 	await expect(page.locator("body")).toContainText("Incomplete Tasks (1)");
 	await expect(page.locator("body")).toContainText("Complete Tasks (0)");
-	await expect(page.locator("textarea").nth(1)).toHaveValue("eat");
+	await expect(page.locator("input").nth(0)).toHaveValue("eat");
 
 	await input_text.fill("pray");
 	await input_text.press("Enter");
 
 	await expect(page.locator("body")).toContainText("Incomplete Tasks (2)");
 	await expect(page.locator("body")).toContainText("Complete Tasks (0)");
-	await expect(page.locator("textarea").nth(2)).toHaveValue("pray");
+	await expect(page.locator("input").nth(1)).toHaveValue("pray");
 
 	await input_text.fill("love");
 	await input_text.press("Enter");
 
 	await expect(page.locator("body")).toContainText("Incomplete Tasks (3)");
 	await expect(page.locator("body")).toContainText("Complete Tasks (0)");
-	await expect(page.locator("textarea").nth(1)).toHaveValue("eat");
-	await expect(page.locator("textarea").nth(2)).toHaveValue("pray");
-	await expect(page.locator("textarea").nth(3)).toHaveValue("love");
+	await expect(page.locator("input").nth(2)).toHaveValue("love");
 
 	const done_btn_for_eat = page
 		.locator("button")
diff --git a/test/components/test_textbox.py b/test/components/test_textbox.py
index e9fb5cac8..edb5345a7 100644
--- a/test/components/test_textbox.py
+++ b/test/components/test_textbox.py
@@ -17,7 +17,7 @@ class TestTextbox:
         assert text_input.postprocess(2.14) == "2.14"  # type: ignore
         assert text_input.get_config() == {
             "lines": 1,
-            "max_lines": 20,
+            "max_lines": None,
             "placeholder": None,
             "value": None,
             "name": "textbox",
@@ -86,9 +86,3 @@ class TestTextbox:
             ValueError, match='`type` must be one of "text", "password", or "email".'
         ):
             gr.Textbox(type="boo")  # type: ignore
-
-    def test_max_lines(self):
-        assert gr.Textbox(type="password").get_config().get("max_lines") == 1
-        assert gr.Textbox(type="email").get_config().get("max_lines") == 1
-        assert gr.Textbox(type="text").get_config().get("max_lines") == 20
-        assert gr.Textbox().get_config().get("max_lines") == 20
diff --git a/test/test_helpers.py b/test/test_helpers.py
index e811e1165..28f45f12b 100644
--- a/test/test_helpers.py
+++ b/test/test_helpers.py
@@ -556,7 +556,6 @@ class TestProcessExamples:
         assert response.json()["data"] == [
             {
                 "lines": 1,
-                "max_lines": 20,
                 "show_label": True,
                 "container": True,
                 "min_width": 160,
@@ -578,7 +577,6 @@ class TestProcessExamples:
         assert response.json()["data"] == [
             {
                 "lines": 1,
-                "max_lines": 20,
                 "show_label": True,
                 "container": True,
                 "min_width": 160,