Repository: gradio-app/gradio
Pull request: allow users to update the current tab from python SDK
Closes #1652 and implement steps noted in the issue.

To test run `demo/blocks_plug`.




Problem statement:
Allow currently selected `TabItem` to be set + updated
Currently there is no way to initialise or update the `Tabs` component to show a specific `TabItem` it always defaults to the first.

We need to implement the following:

- Add `selected` kwarg to `Tabs`
- Add `id` kwargs to `TabItem`
- Add an `update` method to `Tabs` that accepts a `selected` kwarg.
- Use the provided `TabItem` `id` as the actual id in the frontend
- Use the provided `selected` `id` passed to `Tabs` as the selected tab.

Hints from issue discussion:

Commit message: update test again

Patch to review:
diff --git a/demo/blocks_plug/run.py b/demo/blocks_plug/run.py
index 0447e67de5..97684fa61b 100644
--- a/demo/blocks_plug/run.py
+++ b/demo/blocks_plug/run.py
@@ -1,9 +1,14 @@
 import gradio as gr
 
+
+def change_tab():
+    return gr.Tabs.update(selected=2)
+
+
 identity_demo, input_demo, output_demo = gr.Blocks(), gr.Blocks(), gr.Blocks()
 
 with identity_demo:
-    gr.Interface(lambda x:x, "text", "text")
+    gr.Interface(lambda x: x, "text", "text")
 
 with input_demo:
     t = gr.Textbox(label="Enter your text here")
@@ -17,14 +22,15 @@
 
 with gr.Blocks() as demo:
     gr.Markdown("Three demos in one!")
-    with gr.Tabs():
-        with gr.TabItem("Text Identity"):
+    with gr.Tabs(selected=1) as tabs:
+        with gr.TabItem("Text Identity", id=0):
             identity_demo.render()
-        with gr.TabItem("Text Input"):
+        with gr.TabItem("Text Input", id=1):
             input_demo.render()
-        with gr.TabItem("Text Static"):
+        with gr.TabItem("Text Static", id=2):
             output_demo.render()
-    
+    btn = gr.Button("Change tab")
+    btn.click(inputs=None, outputs=tabs, fn=change_tab)
 
 if __name__ == "__main__":
     demo.launch()
diff --git a/demo/blocks_xray/config.json b/demo/blocks_xray/config.json
index 36b6c037dc..1674d01da0 100644
--- a/demo/blocks_xray/config.json
+++ b/demo/blocks_xray/config.json
@@ -1,5 +1,5 @@
 {
-  "version": "3.0.6",
+  "version": "3.0.24\n",
   "mode": "blocks",
   "dev_mode": true,
   "components": [
@@ -34,6 +34,7 @@
       "id": 3,
       "type": "tabs",
       "props": {
+        "selected": false,
         "visible": true,
         "style": {}
       }
@@ -43,6 +44,7 @@
       "type": "tabitem",
       "props": {
         "label": "X-ray",
+        "id": false,
         "visible": true,
         "style": {}
       }
@@ -106,6 +108,7 @@
       "type": "tabitem",
       "props": {
         "label": "CT Scan",
+        "id": false,
         "visible": true,
         "style": {}
       }
@@ -271,7 +274,19 @@
       "backend_fn": true,
       "js": null,
       "status_tracker": 9,
-      "queue": null
+      "queue": null,
+      "api_name": "xray_model",
+      "scroll_to_output": false,
+      "show_progress": true,
+      "documentation": [
+        [
+          "(List[str]): list of selected choices",
+          "(str | dict): base64 url data, or (if tool == \"sketch) a dict of image and mask base64 url data"
+        ],
+        [
+          "(Dict | List): JSON output"
+        ]
+      ]
     },
     {
       "targets": [
@@ -288,7 +303,19 @@
       "backend_fn": true,
       "js": null,
       "status_tracker": 15,
-      "queue": null
+      "queue": null,
+      "api_name": "ct_model",
+      "scroll_to_output": false,
+      "show_progress": true,
+      "documentation": [
+        [
+          "(List[str]): list of selected choices",
+          "(str | dict): base64 url data, or (if tool == \"sketch) a dict of image and mask base64 url data"
+        ],
+        [
+          "(Dict | List): JSON output"
+        ]
+      ]
     },
     {
       "targets": [
@@ -303,7 +330,10 @@
       "backend_fn": true,
       "js": null,
       "status_tracker": 17,
-      "queue": null
+      "queue": null,
+      "api_name": null,
+      "scroll_to_output": false,
+      "show_progress": true
     }
   ]
 }
\ No newline at end of file
diff --git a/gradio/layouts.py b/gradio/layouts.py
index f386ff0b7d..5dcd4297f7 100644
--- a/gradio/layouts.py
+++ b/gradio/layouts.py
@@ -96,6 +96,21 @@ class Tabs(BlockContext):
     Tabs context.
     """
 
+    def __init__(self, selected=False, **kwargs):
+        super().__init__(**kwargs)
+        self.selected = selected
+
+    def get_config(self):
+        return {"selected": self.selected, **super().get_config()}
+
+    def update(
+        selected: Optional[int | str] = None,
+    ):
+        return {
+            "selected": selected,
+            "__type__": "update",
+        }
+
     def change(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
         """
         Parameters:
@@ -113,12 +128,13 @@ class TabItem(BlockContext):
     components defined within the TabItem will be rendered within a tab.
     """
 
-    def __init__(self, label, **kwargs):
+    def __init__(self, label, id=False, **kwargs):
         super().__init__(**kwargs)
         self.label = label
+        self.id = id
 
     def get_config(self):
-        return {"label": self.label, **super().get_config()}
+        return {"label": self.label, "id": self.id, **super().get_config()}
 
     def select(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
         """
diff --git a/gradio/test_data/blocks_configs.py b/gradio/test_data/blocks_configs.py
index 2ecfb8afaa..df5d94bf9e 100644
--- a/gradio/test_data/blocks_configs.py
+++ b/gradio/test_data/blocks_configs.py
@@ -27,11 +27,20 @@
                 "style": {},
             },
         },
-        {"id": 3, "type": "tabs", "props": {"visible": True, "style": {}}},
+        {
+            "id": 3,
+            "type": "tabs",
+            "props": {"visible": True, "style": {}, "selected": False},
+        },
         {
             "id": 4,
             "type": "tabitem",
-            "props": {"label": "X-ray", "visible": True, "style": {}},
+            "props": {
+                "label": "X-ray",
+                "visible": True,
+                "style": {},
+                "id": False,
+            },
         },
         {
             "id": 5,
@@ -71,7 +80,12 @@
         {
             "id": 9,
             "type": "tabitem",
-            "props": {"label": "CT Scan", "visible": True, "style": {}},
+            "props": {
+                "label": "CT Scan",
+                "visible": True,
+                "style": {},
+                "id": False,
+            },
         },
         {
             "id": 10,
@@ -213,11 +227,20 @@
                 "style": {},
             },
         },
-        {"id": 3, "type": "tabs", "props": {"visible": True, "style": {}}},
+        {
+            "id": 3,
+            "type": "tabs",
+            "props": {"visible": True, "style": {}, "selected": False},
+        },
         {
             "id": 444,
             "type": "tabitem",
-            "props": {"label": "X-ray", "visible": True, "style": {}},
+            "props": {
+                "label": "X-ray",
+                "visible": True,
+                "style": {},
+                "id": False,
+            },
         },
         {
             "id": 5,
@@ -262,7 +285,12 @@
         {
             "id": 9,
             "type": "tabitem",
-            "props": {"label": "CT Scan", "visible": True, "style": {}},
+            "props": {
+                "label": "CT Scan",
+                "visible": True,
+                "style": {},
+                "id": False,
+            },
         },
         {
             "id": 10,
@@ -410,7 +438,7 @@
         {
             "id": 3,
             "type": "tabs",
-            "props": {"style": {}, "value": True},
+            "props": {"style": {}, "value": True, "selected": False},
         },
         {
             "id": 4,
@@ -419,6 +447,7 @@
                 "label": "X-ray",
                 "style": {},
                 "value": True,
+                "id": False,
             },
         },
         {
@@ -465,6 +494,7 @@
                 "label": "CT Scan",
                 "style": {},
                 "value": True,
+                "id": False,
             },
         },
         {
diff --git a/ui/packages/app/src/Blocks.svelte b/ui/packages/app/src/Blocks.svelte
index 0243e15481..e3fbbb8d3c 100644
--- a/ui/packages/app/src/Blocks.svelte
+++ b/ui/packages/app/src/Blocks.svelte
@@ -299,7 +299,7 @@
 									if (
 										typeof value === "object" &&
 										value !== null &&
-										value.__type__ == "update"
+										value.__type__ === "update"
 									) {
 										for (const [update_key, update_value] of Object.entries(
 											value
diff --git a/ui/packages/app/src/components/TabItem/Tabs.svelte b/ui/packages/app/src/components/TabItem/Tabs.svelte
index b52f5119f2..4364e1e6fc 100644
--- a/ui/packages/app/src/components/TabItem/Tabs.svelte
+++ b/ui/packages/app/src/components/TabItem/Tabs.svelte
@@ -4,8 +4,9 @@
 	export let elem_id: string = "";
 	export let visible: boolean = true;
 	export let label: string;
+	export let id: string | number;
 </script>
 
-<TabItem {elem_id} {visible} name={label} on:select>
+<TabItem {elem_id} {visible} name={label} {id} on:select>
 	<slot />
 </TabItem>
diff --git a/ui/packages/app/src/components/Tabs/Tabs.svelte b/ui/packages/app/src/components/Tabs/Tabs.svelte
index 27414ec95b..0a09d6ceb3 100644
--- a/ui/packages/app/src/components/Tabs/Tabs.svelte
+++ b/ui/packages/app/src/components/Tabs/Tabs.svelte
@@ -3,8 +3,9 @@
 
 	export let elem_id: string = "";
 	export let visible: boolean = true;
+	export let selected: number | string;
 </script>
 
-<Tabs {elem_id} {visible} on:change>
+<Tabs {elem_id} {visible} bind:selected on:change>
 	<slot />
 </Tabs>
diff --git a/ui/packages/tabs/src/TabItem.svelte b/ui/packages/tabs/src/TabItem.svelte
index 90af0fdfd3..e9443b91f3 100644
--- a/ui/packages/tabs/src/TabItem.svelte
+++ b/ui/packages/tabs/src/TabItem.svelte
@@ -5,12 +5,14 @@
 	export let elem_id: string = "";
 	export let visible: boolean = true;
 	export let name: string;
+	export let id: string | number | object | false = {};
 
 	const dispatch = createEventDispatcher<{ select: undefined }>();
-	const id = {};
 
 	const { register_tab, unregister_tab, selected_tab } = getContext(TABS);
 
+	id = id === false ? {} : id;
+
 	register_tab({ name, id });
 
 	onMount(() => {
diff --git a/ui/packages/tabs/src/Tabs.svelte b/ui/packages/tabs/src/Tabs.svelte
index 13a912ab30..a3bde9b158 100644
--- a/ui/packages/tabs/src/Tabs.svelte
+++ b/ui/packages/tabs/src/Tabs.svelte
@@ -3,7 +3,7 @@
 </script>
 
 <script lang="ts">
-	import { setContext, createEventDispatcher } from "svelte";
+	import { setContext, createEventDispatcher, onMount, tick } from "svelte";
 	import { writable } from "svelte/store";
 
 	interface Tab {
@@ -13,16 +13,18 @@
 
 	export let elem_id: string;
 	export let visible: boolean = true;
+	export let selected: number | string | false | object;
 
-	const tabs: Array<Tab> = [];
+	let tabs: Array<Tab> = [];
 
-	const selected_tab = writable<false | object>(false);
+	const selected_tab = writable<false | object | number | string>(false);
 	const dispatch = createEventDispatcher<{ change: undefined }>();
 
 	setContext(TABS, {
 		register_tab: (tab: Tab) => {
 			tabs.push({ name: tab.name, id: tab.id });
-			selected_tab.update((current) => current || tab.id);
+			selected_tab.update((current) => (current !== false ? current : tab.id));
+			tabs = tabs;
 		},
 		unregister_tab: (tab: Tab) => {
 			const i = tabs.findIndex((t) => t.id === tab.id);
@@ -35,15 +37,17 @@
 		selected_tab
 	});
 
-	function handle_click(id: object) {
+	function change_tab(id: object | string | number) {
 		$selected_tab = id;
 		dispatch("change");
 	}
+
+	$: selected !== false && change_tab(selected);
 </script>
 
 <div class="tabs flex flex-col my-4" id={elem_id}>
 	<div class="flex border-b-2 dark:border-gray-700">
-		{#each tabs as t, i}
+		{#each tabs as t (t.id)}
 			{#if t.id === $selected_tab}
 				<button
 					class="bg-white px-4 pb-2 pt-1.5 rounded-t-lg border-gray-200 -mb-[2px] border-2 border-b-0"
@@ -53,7 +57,7 @@
 			{:else}
 				<button
 					class="px-4 pb-2 pt-1.5 border-transparent text-gray-400 hover:text-gray-700 -mb-[2px] border-2 border-b-0"
-					on:click={() => handle_click(t.id)}
+					on:click={() => (selected = t.id)}
 				>
 					{t.name}
 				</button>


Code review comments:
diff_hunk: @@ -96,6 +96,21 @@ class Tabs(BlockContext):
     Tabs context.
     """
 
+    def __init__(self, selected=False, **kwargs):
path: gradio/layouts.py
text: @user1:
Doesn't seem pythonic to have selected default to `False`. Can we have it default to `None` instead?

```suggestion
    def __init__(self, selected: Optional[int | str] = None, **kwargs):
```

@user1:
The `selected` parameter needs documentation

diff_hunk: @@ -5,12 +5,14 @@
 	export let elem_id: string = "";
 	export let visible: boolean = true;
 	export let name: string;
+	export let id: string | number | object | false = {};
path: ui/packages/tabs/src/TabItem.svelte
text: @user1:
Presumably this would no longer include "false" as an acceptable type if we make the changes above.

diff_hunk: @@ -113,12 +128,13 @@ class TabItem(BlockContext):
     components defined within the TabItem will be rendered within a tab.
     """
 
-    def __init__(self, label, **kwargs):
+    def __init__(self, label, id=False, **kwargs):
path: gradio/layouts.py
text: @user1:
Let's use `None` instead of `False` 

```suggestion
    def __init__(self, label: str, id: Optional[int | str] = None, **kwargs):
```

@user1:
Let's document these parameters