Skip to content

Commit 470550e

Browse files
Add code
1 parent 735a6a6 commit 470550e

File tree

5 files changed

+102
-6
lines changed

5 files changed

+102
-6
lines changed

demo/tabs/run.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: tabs"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "with gr.Blocks() as demo:\n", " with gr.Tabs():\n", " with gr.Tab(\"Set 1\"):\n", " with gr.Tabs(selected=\"a3\") as tabs_1:\n", " tabset_1 = []\n", " textset_1 = []\n", " for i in range(10):\n", " with gr.Tab(f\"Tab {i+1}\", id=f\"a{i+1}\") as tab:\n", " gr.Markdown(f\"Text {i+1}!\")\n", " textbox = gr.Textbox(label=f\"Input {i+1}\")\n", " tabset_1.append(tab)\n", " textset_1.append(textbox)\n", " with gr.Tab(\"Set 2\"):\n", " tabset_2 = []\n", " textset_2 = []\n", " for i in range(10):\n", " with gr.Tab(f\"Tab {i+11}\") as tab:\n", " gr.Markdown(f\"Text {i+11}!\")\n", " textbox = gr.Textbox(label=f\"Input {i+11}\")\n", " tabset_2.append(tab)\n", " textset_2.append(textbox)\n", "\n", " for text1, text2 in zip(textset_1, textset_2):\n", " text1.submit(lambda x: x, text1, text2)\n", "\n", " selected = gr.Textbox(label=\"Selected Tab\")\n", " with gr.Row():\n", " hide_odd_btn = gr.Button(\"Hide Odd Tabs\")\n", " show_all_btn = gr.Button(\"Show All Tabs\")\n", " make_even_uninteractive_btn = gr.Button(\"Make Even Tabs Uninteractive\")\n", " make_all_interactive_btn = gr.Button(\"Make All Tabs Interactive\")\n", "\n", " select_tab_num = gr.Number(label=\"Select Tab #\", value=1)\n", "\n", " hide_odd_btn.click(lambda: [gr.Tab(visible=i % 2 == 1) for i, _ in enumerate(tabset_1 + tabset_2)], outputs=(tabset_1 + tabset_2))\n", " show_all_btn.click(lambda: [gr.Tab(visible=True) for tab in tabset_1 + tabset_2], outputs=(tabset_1 + tabset_2))\n", " make_even_uninteractive_btn.click(lambda: [gr.Tab(interactive=i % 2 == 0) for i, _ in enumerate(tabset_1 + tabset_2)], outputs=(tabset_1 + tabset_2))\n", " make_all_interactive_btn.click(lambda: [gr.Tab(interactive=True) for tab in tabset_1 + tabset_2], outputs=(tabset_1 + tabset_2))\n", " select_tab_num.submit(lambda x: gr.Tabs(selected=f\"a{x}\"), inputs=select_tab_num, outputs=tabs_1)\n", "\n", " def get_selected_index(evt: gr.SelectData):\n", " return evt.value\n", " gr.on([tab.select for tab in tabset_1 + tabset_2], get_selected_index, outputs=selected)\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
1+
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: tabs"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "with gr.Blocks() as demo:\n", " with gr.Tabs():\n", " with gr.Tab(\"Set 1\"):\n", " with gr.Tabs(selected=\"a3\") as tabs_1:\n", " tabset_1 = []\n", " textset_1 = []\n", " for i in range(10):\n", " with gr.Tab(f\"Tab {i+1}\", id=f\"a{i+1}\") as tab:\n", " gr.Markdown(f\"Text {i+1}!\")\n", " textbox = gr.Textbox(label=f\"Input {i+1}\")\n", " tabset_1.append(tab)\n", " textset_1.append(textbox)\n", " with gr.Tab(\"Set 2\"):\n", " tabset_2 = []\n", " textset_2 = []\n", " for i in range(10):\n", " with gr.Tab(f\"Tab {i+11}\") as tab:\n", " gr.Markdown(f\"Text {i+11}!\")\n", " textbox = gr.Textbox(label=f\"Input {i+11}\")\n", " tabset_2.append(tab)\n", " textset_2.append(textbox)\n", "\n", " for text1, text2 in zip(textset_1, textset_2):\n", " text1.submit(lambda x: x, text1, text2)\n", "\n", " with gr.Tab(\"Locked Tab\", id=\"locked\", interactive=False) as locked_tab:\n", " gr.Markdown(\"This tab was unlocked!\")\n", "\n", " selected = gr.Textbox(label=\"Selected Tab\")\n", " with gr.Row():\n", " hide_odd_btn = gr.Button(\"Hide Odd Tabs\")\n", " show_all_btn = gr.Button(\"Show All Tabs\")\n", " make_even_uninteractive_btn = gr.Button(\"Make Even Tabs Uninteractive\")\n", " make_all_interactive_btn = gr.Button(\"Make All Tabs Interactive\")\n", " unlock_btn = gr.Button(\"Unlock Tab\")\n", "\n", " select_tab_num = gr.Number(label=\"Select Tab #\", value=1)\n", "\n", " hide_odd_btn.click(lambda: [gr.Tab(visible=i % 2 == 1) for i, _ in enumerate(tabset_1 + tabset_2)], outputs=(tabset_1 + tabset_2))\n", " show_all_btn.click(lambda: [gr.Tab(visible=True) for tab in tabset_1 + tabset_2], outputs=(tabset_1 + tabset_2))\n", " make_even_uninteractive_btn.click(lambda: [gr.Tab(interactive=i % 2 == 0) for i, _ in enumerate(tabset_1 + tabset_2)], outputs=(tabset_1 + tabset_2))\n", " make_all_interactive_btn.click(lambda: [gr.Tab(interactive=True) for tab in tabset_1 + tabset_2], outputs=(tabset_1 + tabset_2))\n", " unlock_btn.click(lambda: gr.Tab(interactive=True), outputs=locked_tab)\n", " select_tab_num.submit(lambda x: gr.Tabs(selected=f\"a{x}\"), inputs=select_tab_num, outputs=tabs_1)\n", "\n", " def get_selected_index(evt: gr.SelectData):\n", " return evt.value\n", " gr.on([tab.select for tab in tabset_1 + tabset_2], get_selected_index, outputs=selected)\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}

demo/tabs/run.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,24 @@
2525
for text1, text2 in zip(textset_1, textset_2):
2626
text1.submit(lambda x: x, text1, text2)
2727

28+
with gr.Tab("Locked Tab", id="locked", interactive=False) as locked_tab:
29+
gr.Markdown("This tab was unlocked!")
30+
2831
selected = gr.Textbox(label="Selected Tab")
2932
with gr.Row():
3033
hide_odd_btn = gr.Button("Hide Odd Tabs")
3134
show_all_btn = gr.Button("Show All Tabs")
3235
make_even_uninteractive_btn = gr.Button("Make Even Tabs Uninteractive")
3336
make_all_interactive_btn = gr.Button("Make All Tabs Interactive")
37+
unlock_btn = gr.Button("Unlock Tab")
3438

3539
select_tab_num = gr.Number(label="Select Tab #", value=1)
3640

3741
hide_odd_btn.click(lambda: [gr.Tab(visible=i % 2 == 1) for i, _ in enumerate(tabset_1 + tabset_2)], outputs=(tabset_1 + tabset_2))
3842
show_all_btn.click(lambda: [gr.Tab(visible=True) for tab in tabset_1 + tabset_2], outputs=(tabset_1 + tabset_2))
3943
make_even_uninteractive_btn.click(lambda: [gr.Tab(interactive=i % 2 == 0) for i, _ in enumerate(tabset_1 + tabset_2)], outputs=(tabset_1 + tabset_2))
4044
make_all_interactive_btn.click(lambda: [gr.Tab(interactive=True) for tab in tabset_1 + tabset_2], outputs=(tabset_1 + tabset_2))
45+
unlock_btn.click(lambda: gr.Tab(interactive=True), outputs=locked_tab)
4146
select_tab_num.submit(lambda x: gr.Tabs(selected=f"a{x}"), inputs=select_tab_num, outputs=tabs_1)
4247

4348
def get_selected_index(evt: gr.SelectData):

js/core/src/init.svelte.ts

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -421,11 +421,6 @@ export class AppTree {
421421
new_state: Partial<SharedProps> & Record<string, unknown>,
422422
check_visibility: boolean = true
423423
) {
424-
// Visibility is tricky 😅
425-
// If the component is not visible, it has not been rendered
426-
// and so it has no _set_data callback
427-
// Therefore, we need to traverse the tree and set the visible prop to true
428-
// and then render it and its children. After that, we can call the _set_data callback
429424
const node = find_node_by_id(this.root!, id);
430425
let already_updated_visibility = false;
431426
if (check_visibility && !node?.component) {
@@ -465,6 +460,12 @@ export class AppTree {
465460
if ("value" in new_state && !dequal(old_value, new_state.value)) {
466461
this.#event_dispatcher(id, "change", null);
467462
}
463+
464+
// If this is a non-mounted tabitem, update the parent Tabs'
465+
// initial_tabs so the tab button reflects the new state.
466+
if (node?.type === "tabitem") {
467+
this.#update_parent_tabs_initial_tab(id, node);
468+
}
468469
} else if (_set_data) {
469470
_set_data(new_state);
470471
}
@@ -492,6 +493,55 @@ export class AppTree {
492493
});
493494
}
494495

496+
/**
497+
* Updates the parent Tabs component's initial_tabs when a non-mounted
498+
* tabitem's props change. This ensures the tab button (rendered by
499+
* the Tabs component) reflects the updated state even though the
500+
* TabItem component itself is not mounted.
501+
*/
502+
#update_parent_tabs_initial_tab(
503+
id: number,
504+
node: ProcessedComponentMeta
505+
): void {
506+
const parent = find_parent(this.root!, id);
507+
if (!parent || parent.type !== "tabs") return;
508+
509+
const initial_tabs = parent.props.props.initial_tabs as Tab[];
510+
if (!initial_tabs) return;
511+
512+
const tab_index = initial_tabs.findIndex((t) => t.component_id === node.id);
513+
if (tab_index === -1) return;
514+
515+
const i18n = node.props.props.i18n as ((str: string) => string) | undefined;
516+
const raw_label = node.props.shared_props.label as string;
517+
// Use original_visibility since the node's visible may have been
518+
// set to false by the startup optimization for non-selected tabs.
519+
const visible =
520+
"original_visibility" in node
521+
? (node.original_visibility as boolean)
522+
: (node.props.shared_props.visible as boolean);
523+
initial_tabs[tab_index] = {
524+
label: i18n ? i18n(raw_label) : raw_label,
525+
id: node.props.props.id as string,
526+
elem_id: node.props.shared_props.elem_id,
527+
visible,
528+
interactive: node.props.shared_props.interactive,
529+
scale: node.props.shared_props.scale || null,
530+
component_id: node.id
531+
};
532+
533+
// Trigger reactivity by replacing the array
534+
parent.props.props.initial_tabs = [...initial_tabs];
535+
536+
// Also update via _set_data if the Tabs component is mounted
537+
const parent_set_data = this.#set_callbacks.get(parent.id);
538+
if (parent_set_data) {
539+
parent_set_data({
540+
initial_tabs: parent.props.props.initial_tabs
541+
});
542+
}
543+
}
544+
495545
/**
496546
* Gets the current state of a component by its ID
497547
* @param id the ID of the component to get the state of

js/spa/test/tabs.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,26 @@ test("programmatic selection works", async ({ page }) => {
8484
await expect(page.getByText("Text 6!")).toBeVisible();
8585
});
8686

87+
test("tab initialized with interactive=False can be unlocked", async ({
88+
page
89+
}) => {
90+
await page.waitForTimeout(1000);
91+
92+
// The "Locked Tab" should be visible but disabled
93+
const locked_tab = page.getByRole("tab", { name: "Locked Tab" });
94+
await expect(locked_tab).toBeVisible();
95+
await expect(locked_tab).toBeDisabled();
96+
97+
// Click unlock button
98+
await page.getByRole("button", { name: "Unlock Tab" }).click();
99+
await page.waitForTimeout(1000);
100+
101+
// Tab should now be enabled and clickable
102+
await expect(locked_tab).toBeEnabled();
103+
await locked_tab.click();
104+
await expect(page.getByText("This tab was unlocked!")).toBeVisible();
105+
});
106+
87107
test("lazy load subtabs and accordion components", async ({ page }) => {
88108
await page.waitForTimeout(1000);
89109
// Main tab components visible and rendered

js/tabs/shared/Tabs.svelte

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,25 @@
3030
let overflow_menu_open = false;
3131
let overflow_menu: HTMLElement;
3232
33+
// Track which tab orders have been registered by mounted TabItem components.
34+
// Once a TabItem mounts and calls register_tab, it manages its own tab entry
35+
// via _set_data -> register_tab, so _sync_tabs should not overwrite it.
36+
let mounted_tab_orders: Set<number> = new Set();
37+
38+
// When initial_tabs changes (e.g. a non-mounted tab's props were updated),
39+
// sync the internal tabs array so the tab buttons reflect the new state.
40+
// Using a function call so the $: dependency is only on initial_tabs,
41+
// not on tabs (which would cause a loop with register_tab).
42+
$: _sync_tabs(initial_tabs);
43+
44+
function _sync_tabs(new_tabs: Tab[]): void {
45+
for (let i = 0; i < new_tabs.length; i++) {
46+
if (new_tabs[i] && !mounted_tab_orders.has(i)) {
47+
tabs[i] = new_tabs[i];
48+
}
49+
}
50+
}
51+
3352
$: has_tabs = tabs.length > 0;
3453
3554
let tab_nav_el: HTMLDivElement;
@@ -59,6 +78,7 @@
5978
6079
setContext(TABS, {
6180
register_tab: (tab: Tab, order: number) => {
81+
mounted_tab_orders.add(order);
6282
tabs[order] = tab;
6383
6484
if ($selected_tab === false && tab.visible !== false && tab.interactive) {
@@ -68,6 +88,7 @@
6888
return order;
6989
},
7090
unregister_tab: (tab: Tab, order: number) => {
91+
mounted_tab_orders.delete(order);
7192
if ($selected_tab === tab.id) {
7293
$selected_tab = tabs[0]?.id || false;
7394
}

0 commit comments

Comments
 (0)