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

* add tabs params and visible logic

* add disabled logic

* add tabbed_interface logic

* add tab accessibility improvements

* Add aria-disabled attribute to tab buttons

* add e2e test

* add changeset

* add changeset

* add tab e2e test

* formatting

* run generate_notebooks.py

* lint

* ensure tabs values update

* remove tabbedinterface logic

* Remove unused parameters from TabbedInterface constructor

* remove test

* add test

* add changeset

* formatting

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
This commit is contained in:
Hannah 2024-01-17 14:39:10 +01:00 committed by GitHub
parent 13dc8b0f38
commit ec28b4e7c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 149 additions and 29 deletions

View File

@ -0,0 +1,8 @@
---
"@gradio/dataframe": minor
"@gradio/tabitem": minor
"@gradio/tabs": minor
"gradio": minor
---
feat:Add `visible` and `interactive` params to `gr.Tab()`

View File

@ -1 +1 @@
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: blocks_flashcards"]}, {"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 random\n", "\n", "import gradio as gr\n", "\n", "demo = gr.Blocks()\n", "\n", "with demo:\n", " gr.Markdown(\n", " \"Load the flashcards in the table below, then use the Practice tab to practice.\"\n", " )\n", "\n", " with gr.Tab(\"Word Bank\"):\n", " flashcards_table = gr.Dataframe(headers=[\"front\", \"back\"], type=\"array\")\n", " with gr.Tab(\"Practice\"):\n", " with gr.Row():\n", " with gr.Column():\n", " front = gr.Textbox(label=\"Prompt\")\n", " with gr.Row():\n", " new_btn = gr.Button(\"New Card\")\n", " flip_btn = gr.Button(\"Flip Card\")\n", " with gr.Column(visible=False) as answer_col:\n", " back = gr.Textbox(label=\"Answer\")\n", " selected_card = gr.State()\n", " with gr.Row():\n", " correct_btn = gr.Button(\"Correct\")\n", " incorrect_btn = gr.Button(\"Incorrect\")\n", "\n", " with gr.Tab(\"Results\"):\n", " results = gr.State(value={})\n", " correct_field = gr.Markdown(\"# Correct: 0\")\n", " incorrect_field = gr.Markdown(\"# Incorrect: 0\")\n", " gr.Markdown(\"Card Statistics: \")\n", " results_table = gr.Dataframe(headers=[\"Card\", \"Correct\", \"Incorrect\"])\n", "\n", " def load_new_card(flashcards):\n", " card = random.choice(flashcards)\n", " return (\n", " card,\n", " card[0],\n", " gr.Column(visible=False),\n", " )\n", "\n", " new_btn.click(\n", " load_new_card,\n", " [flashcards_table],\n", " [selected_card, front, answer_col],\n", " )\n", "\n", " def flip_card(card):\n", " return card[1], gr.Column(visible=True)\n", "\n", " flip_btn.click(flip_card, [selected_card], [back, answer_col])\n", "\n", " def mark_correct(card, results):\n", " if card[0] not in results:\n", " results[card[0]] = [0, 0]\n", " results[card[0]][0] += 1\n", " correct_count = sum(result[0] for result in results.values())\n", " return (\n", " results,\n", " f\"# Correct: {correct_count}\",\n", " [[front, scores[0], scores[1]] for front, scores in results.items()],\n", " )\n", "\n", " def mark_incorrect(card, results):\n", " if card[0] not in results:\n", " results[card[0]] = [0, 0]\n", " results[card[0]][1] += 1\n", " incorrect_count = sum(result[1] for result in results.values())\n", " return (\n", " results,\n", " f\"# Inorrect: {incorrect_count}\",\n", " [[front, scores[0], scores[1]] for front, scores in results.items()],\n", " )\n", "\n", " correct_btn.click(\n", " mark_correct,\n", " [selected_card, results],\n", " [results, correct_field, results_table],\n", " )\n", "\n", " incorrect_btn.click(\n", " mark_incorrect,\n", " [selected_card, results],\n", " [results, incorrect_field, results_table],\n", " )\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: blocks_flashcards"]}, {"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 random\n", "\n", "import gradio as gr\n", "\n", "demo = gr.Blocks()\n", "\n", "with demo:\n", " gr.Markdown(\n", " \"Load the flashcards in the table below, then use the Practice tab to practice.\"\n", " )\n", "\n", " with gr.Tab(\"Word Bank\"):\n", " flashcards_table = gr.Dataframe(headers=[\"front\", \"back\"], type=\"array\")\n", " with gr.Tab(\"Practice\"):\n", " with gr.Row():\n", " with gr.Column():\n", " front = gr.Textbox(label=\"Prompt\")\n", " with gr.Row():\n", " new_btn = gr.Button(\"New Card\")\n", " flip_btn = gr.Button(\"Flip Card\")\n", " with gr.Column(visible=False) as answer_col:\n", " back = gr.Textbox(label=\"Answer\")\n", " selected_card = gr.State()\n", " with gr.Row():\n", " correct_btn = gr.Button(\"Correct\")\n", " incorrect_btn = gr.Button(\"Incorrect\")\n", "\n", " with gr.Tab(\"Results\", visible=False) as results_tab:\n", " results = gr.State(value={})\n", " correct_field = gr.Markdown(\"# Correct: 0\")\n", " incorrect_field = gr.Markdown(\"# Incorrect: 0\")\n", " gr.Markdown(\"Card Statistics: \")\n", " results_table = gr.Dataframe(headers=[\"Card\", \"Correct\", \"Incorrect\"])\n", "\n", " def load_new_card(flashcards):\n", " card = random.choice(flashcards)\n", " return (\n", " card,\n", " card[0],\n", " gr.Column(visible=False),\n", " )\n", "\n", " new_btn.click(\n", " load_new_card,\n", " [flashcards_table],\n", " [selected_card, front, answer_col],\n", " )\n", "\n", " def flip_card(card):\n", " return card[1], gr.Column(visible=True)\n", "\n", " flip_btn.click(flip_card, [selected_card], [back, answer_col])\n", "\n", " def mark_correct(card, results):\n", " if card[0] not in results:\n", " results[card[0]] = [0, 0]\n", " results[card[0]][0] += 1\n", " correct_count = sum(result[0] for result in results.values())\n", " return (\n", " results,\n", " f\"# Correct: {correct_count}\",\n", " [[front, scores[0], scores[1]] for front, scores in results.items()],\n", " )\n", "\n", " def mark_incorrect(card, results):\n", " if card[0] not in results:\n", " results[card[0]] = [\n", " 0, 0]\n", " results[card[0]][1] += 1\n", " incorrect_count = sum(result[1] for result in results.values())\n", " return (\n", " results,\n", " f\"# Inorrect: {incorrect_count}\",\n", " [[front, scores[0], scores[1]] for front, scores in results.items()],\n", " )\n", "\n", " def toggle_results_tab():\n", " return gr.Tab(\"Results\", visible=True)\n", "\n", " correct_btn.click(\n", " mark_correct,\n", " [selected_card, results],\n", " [results, correct_field, results_table],\n", " )\n", "\n", " incorrect_btn.click(mark_incorrect, [selected_card, results], [results, incorrect_field, results_table])\n", "\n", " # set results tab to visible when correct or incorrect button is clicked\n", " correct_btn.click(fn=toggle_results_tab, outputs=[results_tab])\n", " incorrect_btn.click(fn=toggle_results_tab, outputs=[results_tab])\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}

View File

@ -25,7 +25,7 @@ with demo:
correct_btn = gr.Button("Correct")
incorrect_btn = gr.Button("Incorrect")
with gr.Tab("Results"):
with gr.Tab("Results", visible=False) as results_tab:
results = gr.State(value={})
correct_field = gr.Markdown("# Correct: 0")
incorrect_field = gr.Markdown("# Incorrect: 0")
@ -64,7 +64,8 @@ with demo:
def mark_incorrect(card, results):
if card[0] not in results:
results[card[0]] = [0, 0]
results[card[0]] = [
0, 0]
results[card[0]][1] += 1
incorrect_count = sum(result[1] for result in results.values())
return (
@ -73,17 +74,20 @@ with demo:
[[front, scores[0], scores[1]] for front, scores in results.items()],
)
def toggle_results_tab():
return gr.Tab("Results", visible=True)
correct_btn.click(
mark_correct,
[selected_card, results],
[results, correct_field, results_table],
)
incorrect_btn.click(
mark_incorrect,
[selected_card, results],
[results, incorrect_field, results_table],
)
incorrect_btn.click(mark_incorrect, [selected_card, results], [results, incorrect_field, results_table])
# set results tab to visible when correct or incorrect button is clicked
correct_btn.click(fn=toggle_results_tab, outputs=[results_tab])
incorrect_btn.click(fn=toggle_results_tab, outputs=[results_tab])
if __name__ == "__main__":
demo.launch()

View File

@ -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,
@ -75,6 +77,9 @@ class Tab(BlockContext, metaclass=ComponentMeta):
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,
@ -84,6 +89,8 @@ class Tab(BlockContext, metaclass=ComponentMeta):
)
self.label = label
self.id = id
self.visible = visible
self.interactive = interactive
def get_expected_parent(self) -> type[Tabs]:
return Tabs

View File

@ -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")

View File

@ -0,0 +1,44 @@
import { test } from "@gradio/tootils";
test("shows the results tab when results > 0", async ({ page }) => {
await page
.getByRole("button", { name: "front back" })
.getByRole("button")
.nth(2)
.dblclick();
await page
.getByRole("button", { name: "front back" })
.locator("tbody")
.getByRole("textbox")
.fill("dog");
await page
.getByRole("button", { name: "front back" })
.locator("tbody")
.getByRole("textbox")
.press("Enter");
await page
.getByRole("button", { name: "front back" })
.getByRole("button")
.nth(3)
.dblclick();
await page
.getByRole("button", { name: "front back" })
.locator("tbody")
.getByRole("textbox")
.fill("cat");
await page
.getByRole("button", { name: "front back" })
.locator("tbody")
.getByRole("textbox")
.press("Enter");
await page.getByText("New row").click();
await page.getByRole("tab", { name: "Practice" }).click();
await page.getByRole("button", { name: "New Card" }).click();
await page.waitForTimeout(1000);
await page.getByRole("button", { name: "Flip Card" }).click();
await page.getByRole("button", { name: "Correct", exact: true }).click();
await page.getByRole("tab", { name: "Results" }).click();
});

View File

@ -54,6 +54,7 @@
{#if edit}
<input
role="textbox"
bind:this={el}
bind:value={_value}
class:header

View File

@ -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)}
>

View File

@ -8,13 +8,17 @@
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: number;
$: tab_index = register_tab({ name, id, elem_id, visible, interactive });
onMount(() => {
return (): void => unregister_tab({ name, id, elem_id });
@ -28,6 +32,7 @@
id={elem_id}
class="tabitem {elem_classes.join(' ')}"
style:display={$selected_tab === id ? "block" : "none"}
role="tabpanel"
>
<Column>
<slot />

View File

@ -3,7 +3,7 @@
</script>
<script lang="ts">
import { setContext, createEventDispatcher, tick } from "svelte";
import { setContext, createEventDispatcher } from "svelte";
import { writable } from "svelte/store";
import type { SelectData } from "@gradio/utils";
@ -11,6 +11,8 @@
name: string;
id: object;
elem_id: string | undefined;
visible: boolean;
interactive: boolean;
}
export let visible = true;
@ -29,8 +31,28 @@
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);
let existingTab = tabs.find((t) => t.id === tab.id);
if (existingTab) {
// update existing tab with newer values
let i = tabs.findIndex((t) => t.id === tab.id);
tabs[i] = { ...tabs[i], ...tab };
} else {
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;
},
@ -56,22 +78,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>
@ -107,6 +142,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);
}