mirror of
https://github.com/gradio-app/gradio.git
synced 2025-04-12 12:40:29 +08:00
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:
parent
13dc8b0f38
commit
ec28b4e7c4
8
.changeset/slow-ends-double.md
Normal file
8
.changeset/slow-ends-double.md
Normal 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()`
|
@ -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}
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
44
js/app/test/blocks_flashcards.spec.ts
Normal file
44
js/app/test/blocks_flashcards.spec.ts
Normal 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();
|
||||
});
|
@ -54,6 +54,7 @@
|
||||
|
||||
{#if edit}
|
||||
<input
|
||||
role="textbox"
|
||||
bind:this={el}
|
||||
bind:value={_value}
|
||||
class:header
|
||||
|
@ -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)}
|
||||
>
|
||||
|
@ -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 />
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user