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

Add visible and interactive params to gr.Tab() #7018

Merged
merged 25 commits into from Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
740c08f
add tabs params and visible logic
hannahblair Jan 12, 2024
e5e7af4
add disabled logic
hannahblair Jan 12, 2024
ddd3cfc
add tabbed_interface logic
hannahblair Jan 12, 2024
f69640c
add tab accessibility improvements
hannahblair Jan 12, 2024
84237bb
Add aria-disabled attribute to tab buttons
hannahblair Jan 12, 2024
2010ae4
add e2e test
hannahblair Jan 12, 2024
e255a28
add changeset
gradio-pr-bot Jan 12, 2024
5d5a79c
add changeset
gradio-pr-bot Jan 12, 2024
9497632
add tab e2e test
hannahblair Jan 12, 2024
820258b
Merge branch 'tabs-visible-interactive-params' of https://github.com/…
hannahblair Jan 12, 2024
28b0e76
Merge branch 'main' into tabs-visible-interactive-params
hannahblair Jan 12, 2024
01568c4
formatting
hannahblair Jan 12, 2024
b9eeeb8
Merge branch 'tabs-visible-interactive-params' of https://github.com/…
hannahblair Jan 12, 2024
6c6edc0
run generate_notebooks.py
hannahblair Jan 12, 2024
2b1b77b
lint
hannahblair Jan 12, 2024
ea7497e
ensure tabs values update
hannahblair Jan 16, 2024
76ed21b
Merge branch 'main' into tabs-visible-interactive-params
hannahblair Jan 16, 2024
67e535c
remove tabbedinterface logic
hannahblair Jan 16, 2024
f207000
Merge branch 'tabs-visible-interactive-params' of https://github.com/…
hannahblair Jan 16, 2024
5546d87
Remove unused parameters from TabbedInterface constructor
hannahblair Jan 16, 2024
ba116ce
remove test
hannahblair Jan 16, 2024
b841ba2
add test
hannahblair Jan 16, 2024
eeb97f5
add changeset
gradio-pr-bot Jan 16, 2024
68ab8f9
formatting
hannahblair Jan 17, 2024
40a232c
Merge branch 'tabs-visible-interactive-params' of https://github.com/…
hannahblair Jan 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/slow-ends-double.md
@@ -0,0 +1,7 @@
---
"@gradio/tabitem": minor
"@gradio/tabs": minor
"gradio": minor
---

feat:Add `visible` and `interactive` params to `gr.Tab()`
2 changes: 1 addition & 1 deletion demo/tabbed_interface_lite/run.ipynb
@@ -1 +1 @@
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: tabbed_interface_lite"]}, {"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", "hello_world = gr.Interface(lambda name: \"Hello \" + name, \"text\", \"text\")\n", "bye_world = gr.Interface(lambda name: \"Bye \" + name, \"text\", \"text\")\n", "\n", "demo = gr.TabbedInterface([hello_world, bye_world], [\"Hello World\", \"Bye World\"])\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: tabbed_interface_lite"]}, {"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", "hello_world = gr.Interface(lambda name: \"Hello \" + name, \"text\", \"text\")\n", "bye_world = gr.Interface(lambda name: \"Bye \" + name, \"text\", \"text\")\n", "hidden_tab = gr.Interface(lambda name: \"Hidden \" + name, \"text\", \"text\")\n", "secret_tab = gr.Interface(lambda name: \"Secret \" + name, \"text\", \"text\")\n", "\n", "demo = gr.TabbedInterface([secret_tab, hello_world, bye_world, hidden_tab], [\"Secret Tab\", \"Hello World\", \"Bye World\", \"Hidden Tab\"], visible_tabs=[0,1,2], interactive_tabs=[1,2])\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
4 changes: 3 additions & 1 deletion demo/tabbed_interface_lite/run.py
Expand Up @@ -2,8 +2,10 @@

hello_world = gr.Interface(lambda name: "Hello " + name, "text", "text")
bye_world = gr.Interface(lambda name: "Bye " + name, "text", "text")
hidden_tab = gr.Interface(lambda name: "Hidden " + name, "text", "text")
secret_tab = gr.Interface(lambda name: "Secret " + name, "text", "text")

demo = gr.TabbedInterface([hello_world, bye_world], ["Hello World", "Bye World"])
demo = gr.TabbedInterface([secret_tab, hello_world, bye_world, hidden_tab], ["Secret Tab", "Hello World", "Bye World", "Hidden Tab"], visible_tabs=[0,1,2], interactive_tabs=[1,2])

if __name__ == "__main__":
demo.launch()
15 changes: 14 additions & 1 deletion gradio/interface.py
Expand Up @@ -819,6 +819,8 @@ def __init__(
theme: Theme | None = None,
analytics_enabled: bool | None = None,
css: str | None = None,
visible_tabs: list[int] | None = None,
interactive_tabs: list[int] | None = None,
):
"""
Parameters:
Expand All @@ -827,6 +829,8 @@ def __init__(
title: a title for the interface; if provided, appears above the input and output components in large font. Also used as the tab title when opened in a browser window.
analytics_enabled: whether to allow basic telemetry. If None, will use GRADIO_ANALYTICS_ENABLED environment variable or default to True.
css: custom css or path to custom css file to apply to entire Blocks
visible_tabs: a list of indices of tabs that should be visible. If None, all tabs are visible.
interactive_tabs: a list of indices of tabs that should be interactive. If None, all tabs are interactive.
Returns:
a Gradio Tabbed Interface for the given interfaces
"""
Expand All @@ -839,14 +843,23 @@ def __init__(
)
if tab_names is None:
tab_names = [f"Tab {i}" for i in range(len(interface_list))]

if visible_tabs is None:
visible_tabs = list(range(len(interface_list)))

if interactive_tabs is None:
interactive_tabs = list(range(len(interface_list)))
with self:
if title:
Markdown(
f"<h1 style='text-align: center; margin-bottom: 1rem'>{title}</h1>"
)
with Tabs():
for interface, tab_name in zip(interface_list, tab_names):
with Tab(label=tab_name):
interactive = interface_list.index(interface) in interactive_tabs
visible = interface_list.index(interface) in visible_tabs

with Tab(label=tab_name, visible=visible, interactive=interactive):
interface.render()


Expand Down
7 changes: 7 additions & 0 deletions gradio/layouts/tabs.py
Expand Up @@ -63,6 +63,8 @@ class Tab(BlockContext, metaclass=ComponentMeta):
def __init__(
self,
label: str | None = None,
visible: bool = True,
interactive: bool = True,
*,
id: int | str | None = None,
elem_id: str | None = None,
Expand All @@ -75,6 +77,9 @@ def __init__(
id: An optional identifier for the tab, required if you wish to control the selected tab from a predict function.
elem_id: An optional string that is assigned as the id of the <div> containing the contents of the Tab layout. The same string followed by "-button" is attached to the Tab button. Can be used for targeting CSS styles.
elem_classes: An optional string or list of strings that are assigned as the class of this component in the HTML DOM. Can be used for targeting CSS styles.
render: If False, this layout will not be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.
visible: If False, Tab will be hidden.
interactive: If False, Tab will not be clickable.
"""
BlockContext.__init__(
self,
Expand All @@ -84,6 +89,8 @@ def __init__(
)
self.label = label
self.id = id
self.visible = visible
self.interactive = interactive

def get_expected_parent(self) -> type[Tabs]:
return Tabs
Expand Down
10 changes: 8 additions & 2 deletions js/app/test/audio_debugger.spec.ts
@@ -1,11 +1,17 @@
import { test } from "@gradio/tootils";
import { test, expect } from "@gradio/tootils";

// we cannot currently test the waveform canvas with playwright (https://github.com/microsoft/playwright/issues/23964)
// so this test covers the interactive elements around the waveform canvas

test("audio waveform", async ({ page }) => {
await page.getByRole("button", { name: "Interface" }).click();
await expect(page.getByRole("tab", { name: "Audio" })).toHaveAttribute(
"aria-selected",
"true"
);
await page.getByRole("tab", { name: "Interface" }).click();
await page.getByRole("tab", { name: "Interface" }).click();
await page.getByRole("button", { name: "cantina.wav" }).click();

await page
.getByTestId("waveform-x")
.getByLabel("Adjust playback speed to 1.5x")
Expand Down
27 changes: 27 additions & 0 deletions js/app/test/tabbed_interface_lite.spec.ts
@@ -0,0 +1,27 @@
import { test, expect } from "@gradio/tootils";

test("correct tabs are visible", async ({ page }) => {
const tabs = await page.getByRole("tab");
expect(tabs).toHaveCount(3);

await expect(tabs.nth(0)).toContainText("Secret Tab");
await expect(tabs.nth(1)).toContainText("Hello World");
await expect(tabs.nth(2)).toContainText("Bye World");

let hidden_tab_locator = await page.getByText("Hidden Tab");
await expect(hidden_tab_locator).not.toBeAttached();
});

test("correct tab is selected", async ({ page }) => {
const tabs = await page.getByRole("tab");
await expect(tabs.nth(0)).toHaveAttribute("aria-selected", "false");
await expect(tabs.nth(1)).toHaveAttribute("aria-selected", "true");
await expect(tabs.nth(2)).toHaveAttribute("aria-selected", "false");
});

test("correct tabs are disabled", async ({ page }) => {
const tabs = await page.getByRole("tab");
await expect(tabs.nth(0)).toHaveAttribute("disabled");
await expect(tabs.nth(1)).not.toHaveAttribute("disabled");
await expect(tabs.nth(1)).not.toHaveAttribute("disabled");
});
4 changes: 4 additions & 0 deletions js/tabitem/Index.svelte
Expand Up @@ -9,12 +9,16 @@
export let gradio: Gradio<{
select: SelectData;
}>;
export let visible = true;
export let interactive = true;
</script>

<TabItem
{elem_id}
{elem_classes}
name={label}
{visible}
{interactive}
{id}
on:select={({ detail }) => gradio.dispatch("select", detail)}
>
Expand Down
5 changes: 4 additions & 1 deletion js/tabitem/shared/TabItem.svelte
Expand Up @@ -8,13 +8,15 @@
export let elem_classes: string[] = [];
export let name: string;
export let id: string | number | object = {};
export let visible: boolean;
export let interactive: boolean;

const dispatch = createEventDispatcher<{ select: SelectData }>();

const { register_tab, unregister_tab, selected_tab, selected_tab_index } =
getContext(TABS) as any;

let tab_index = register_tab({ name, id, elem_id });
let tab_index = register_tab({ name, id, elem_id, visible, interactive });

onMount(() => {
return (): void => unregister_tab({ name, id, elem_id });
Expand All @@ -28,6 +30,7 @@
id={elem_id}
class="tabitem {elem_classes.join(' ')}"
style:display={$selected_tab === id ? "block" : "none"}
role="tabpanel"
>
<Column>
<slot />
Expand Down
68 changes: 51 additions & 17 deletions js/tabs/shared/Tabs.svelte
Expand Up @@ -11,6 +11,8 @@
name: string;
id: object;
elem_id: string | undefined;
visible: boolean;
interactive: boolean;
}

export let visible = true;
Expand All @@ -29,8 +31,21 @@

setContext(TABS, {
register_tab: (tab: Tab) => {
tabs.push({ name: tab.name, id: tab.id, elem_id: tab.elem_id });
selected_tab.update((current) => current ?? tab.id);
tabs.push({
name: tab.name,
id: tab.id,
elem_id: tab.elem_id,
visible: tab.visible,
interactive: tab.interactive
});
selected_tab.update((current) => {
if (current === false && tab.visible && tab.interactive) {
return tab.id;
}

let nextTab = tabs.find((t) => t.visible && t.interactive);
return nextTab ? nextTab.id : current;
});
tabs = tabs;
return tabs.length - 1;
},
Expand All @@ -56,22 +71,35 @@
</script>

<div class="tabs {elem_classes.join(' ')}" class:hide={!visible} id={elem_id}>
<div class="tab-nav scroll-hide">
<div class="tab-nav scroll-hide" role="tablist">
{#each tabs as t, i (t.id)}
{#if t.id === $selected_tab}
<button class="selected" id={t.elem_id ? t.elem_id + "-button" : null}>
{t.name}
</button>
{:else}
<button
id={t.elem_id ? t.elem_id + "-button" : null}
on:click={() => {
change_tab(t.id);
dispatch("select", { value: t.name, index: i });
}}
>
{t.name}
</button>
{#if t.visible}
{#if t.id === $selected_tab}
<button
role="tab"
class="selected"
aria-selected={true}
aria-controls={t.elem_id}
id={t.elem_id ? t.elem_id + "-button" : null}
>
{t.name}
</button>
{:else}
<button
role="tab"
aria-selected={false}
aria-controls={t.elem_id}
disabled={!t.interactive}
aria-disabled={!t.interactive}
id={t.elem_id ? t.elem_id + "-button" : null}
on:click={() => {
change_tab(t.id);
dispatch("select", { value: t.name, index: i });
}}
>
{t.name}
</button>
{/if}
{/if}
{/each}
</div>
Expand Down Expand Up @@ -107,6 +135,12 @@
font-size: var(--section-header-text-size);
}

button:disabled {
color: var(--body-text-color-subdued);
opacity: 0.5;
cursor: not-allowed;
}

button:hover {
color: var(--body-text-color);
}
Expand Down