diff --git a/.changeset/calm-columns-appear.md b/.changeset/calm-columns-appear.md new file mode 100644 index 00000000000..fb97fe664e6 --- /dev/null +++ b/.changeset/calm-columns-appear.md @@ -0,0 +1,12 @@ +--- +"@gradio/column": patch +"@gradio/core": patch +"@gradio/dataset": patch +"@gradio/html": patch +"@gradio/json": patch +"@gradio/plot": patch +"@gradio/tabitem": patch +"@gradio/textbox": patch +--- + +fix: restore shared props lost during frontend prop partitioning diff --git a/demo/shared_props/run.py b/demo/shared_props/run.py new file mode 100644 index 00000000000..d12e9dc1792 --- /dev/null +++ b/demo/shared_props/run.py @@ -0,0 +1,70 @@ +import matplotlib.pyplot as plt + +import gradio as gr +from gradio.media import get_image + + +def validate_text(value: str): + return gr.validate(bool(value.strip()), "Text is required") + + +def echo(value: str): + return value + + +figure, axis = plt.subplots() +axis.plot([0, 1, 2], [0, 1, 0]) + + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(variant="panel", elem_id="panel-column"): + gr.Markdown("Panel column") + with gr.Column(variant="compact", elem_id="compact-column"): + gr.Button("Compact one") + gr.Button("Compact two") + + with gr.Row(): + gr.HTML( + "Padded HTML", + container=True, + padding=True, + elem_id="padded-html", + ) + gr.HTML( + "Unpadded HTML", + container=True, + padding=False, + elem_id="unpadded-html", + ) + + gr.JSON({"shared": "theme"}, elem_id="shared-json") + gr.Plot(figure, elem_id="shared-plot") + + with gr.Tabs(): + with gr.Tab("Scaled tab", scale=2, elem_id="scaled-tab"): + gr.Markdown("Scaled tab content") + with gr.Tab("Other tab"): + gr.Markdown("Other tab content") + + image_component = gr.Image(visible=False) + gr.Dataset( + components=[image_component], + samples=[[get_image("cheetah1.jpg")]], + label="Image examples", + elem_id="shared-dataset", + ) + + validated_text = gr.Textbox(label="Validated text") + validate_button = gr.Button("Validate text") + validated_output = gr.Textbox(label="Validated output", interactive=False) + validate_button.click( + echo, + validated_text, + validated_output, + validator=validate_text, + ) + + +if __name__ == "__main__": + demo.launch() diff --git a/js/column/Index.svelte b/js/column/Index.svelte index 65b6fbf4148..5cf14ab0b7b 100644 --- a/js/column/Index.svelte +++ b/js/column/Index.svelte @@ -13,7 +13,7 @@ ); - + diff --git a/js/core/src/Blocks.svelte b/js/core/src/Blocks.svelte index 4fc4faf66b0..0cdd18225de 100644 --- a/js/core/src/Blocks.svelte +++ b/js/core/src/Blocks.svelte @@ -163,6 +163,7 @@ { root, theme: theme_mode, + theme_mode, version, api_prefix, max_file_size, @@ -244,6 +245,7 @@ app_tree.reload(components, layout, dependencies, { root, theme: theme_mode, + theme_mode, version, api_prefix, max_file_size, diff --git a/js/core/src/types.ts b/js/core/src/types.ts index ac6137c141e..e578327b155 100644 --- a/js/core/src/types.ts +++ b/js/core/src/types.ts @@ -136,6 +136,7 @@ export type LoadingComponent = Promise<{ export interface AppConfig { root: string; theme: string; + theme_mode: ThemeMode; version: string; max_file_size?: number; autoscroll: boolean; diff --git a/js/dataset/Dataset.test.ts b/js/dataset/Dataset.test.ts new file mode 100644 index 00000000000..452d609298f --- /dev/null +++ b/js/dataset/Dataset.test.ts @@ -0,0 +1,35 @@ +import { afterEach, describe, expect, test } from "vitest"; +import { cleanup, render, waitFor } from "@self/tootils/render"; + +import Dataset from "./Index.svelte"; +import DatasetRootExample from "./DatasetRootExample.svelte"; + +describe("Dataset", () => { + afterEach(() => cleanup()); + + test("forwards the shared root to example components", async () => { + const result = await render(Dataset, { + components: [{ name: "textbox", class_id: "textbox" }], + component_props: [{}], + headers: ["Example"], + samples: [["sample"]], + sample_labels: null, + value: null, + root: "/gradio-root", + proxy_url: null, + samples_per_page: 10, + layout: "gallery", + show_label: false, + load_component: () => ({ + component: Promise.resolve({ default: DatasetRootExample }), + runtime: false + }) + }); + + await waitFor(() => { + expect(result.getByTestId("dataset-root")).toHaveTextContent( + "/gradio-root" + ); + }); + }); +}); diff --git a/js/dataset/DatasetRootExample.svelte b/js/dataset/DatasetRootExample.svelte new file mode 100644 index 00000000000..46468140252 --- /dev/null +++ b/js/dataset/DatasetRootExample.svelte @@ -0,0 +1,5 @@ + + +{root ?? "missing"} diff --git a/js/dataset/Index.svelte b/js/dataset/Index.svelte index 0f44649a18a..2b79fc2382f 100644 --- a/js/dataset/Index.svelte +++ b/js/dataset/Index.svelte @@ -54,6 +54,7 @@ onselect={(data) => gradio.dispatch("select", data)} load_component={gradio.shared.load_component} {...gradio.props} + root={gradio.shared.root} {samples} /> diff --git a/js/dataset/types.ts b/js/dataset/types.ts index 78e50a66537..686bf51f64f 100644 --- a/js/dataset/types.ts +++ b/js/dataset/types.ts @@ -7,7 +7,6 @@ export interface DatasetProps { samples: any[][] | null; sample_labels: string[] | null; value: number | null; - root: string; proxy_url: null | string; samples_per_page: number; diff --git a/js/html/Index.svelte b/js/html/Index.svelte index c246aafa825..096b6ce597d 100644 --- a/js/html/Index.svelte +++ b/js/html/Index.svelte @@ -58,7 +58,7 @@ elem_id={gradio.shared.elem_id} elem_classes={gradio.shared.elem_classes} container={gradio.shared.container} - padding={gradio.props.padding !== false} + padding={gradio.shared.padding !== false} overflow_behavior="visible" > {#if gradio.shared.show_label && gradio.props.buttons && gradio.props.buttons.length > 0} diff --git a/js/html/types.ts b/js/html/types.ts index f8497bb8ed0..974b40ecea6 100644 --- a/js/html/types.ts +++ b/js/html/types.ts @@ -13,7 +13,6 @@ export interface HTMLProps { head: string | null; component_class_name: string; buttons: (string | CustomButton)[] | null; - padding: boolean; } export interface HTMLEvents { diff --git a/js/json/Index.svelte b/js/json/Index.svelte index 19d94401ae7..e6c5ecf2f31 100644 --- a/js/json/Index.svelte +++ b/js/json/Index.svelte @@ -64,7 +64,7 @@ { + await page.emulateMedia({ colorScheme: "dark" }); + await page.reload(); + + await expect(page.locator("#panel-column")).toHaveClass(/\bpanel\b/); + await expect(page.locator("#compact-column")).toHaveClass(/\bcompact\b/); + + await expect(page.locator("#padded-html")).toHaveClass(/\bpadded\b/); + await expect(page.locator("#unpadded-html")).not.toHaveClass(/\bpadded\b/); + + await expect(page.locator("#shared-json .json-node.root")).toHaveClass( + /\bdark-mode\b/ + ); + + const plot = page.locator("#shared-plot [data-testid='matplotlib'] img"); + await expect(plot).toBeVisible(); + await expect + .poll(() => plot.evaluate((image: HTMLImageElement) => image.naturalWidth)) + .toBeGreaterThan(0); + + const scaled_tab = page.getByRole("tabpanel").filter({ + has: page.getByText("Scaled tab content") + }); + await expect(scaled_tab).toHaveCSS("flex-grow", "2"); + + const dataset_image = page.locator("#shared-dataset img"); + await expect(dataset_image).toBeVisible(); + await expect + .poll(() => + dataset_image.evaluate((image: HTMLImageElement) => image.naturalWidth) + ) + .toBeGreaterThan(0); + + await page.getByRole("button", { name: "Validate text" }).click(); + await expect(page.getByText("Text is required")).toBeVisible(); + + await page.getByLabel("Validated text").fill("valid value"); + await expect(page.getByText("Text is required")).toBeHidden(); + + await page.getByRole("button", { name: "Validate text" }).click(); + await expect(page.getByLabel("Validated output")).toHaveValue("valid value"); +}); diff --git a/js/tabitem/Index.svelte b/js/tabitem/Index.svelte index 8b9c32383e3..18c26367eaa 100644 --- a/js/tabitem/Index.svelte +++ b/js/tabitem/Index.svelte @@ -20,7 +20,7 @@ interactive={gradio.shared.interactive} id={gradio.props.id} order={gradio.props.order} - scale={gradio.props.scale} + scale={gradio.shared.scale} component_id={gradio.props.component_id} onselect={(data) => gradio.dispatch("select", data)} > diff --git a/js/tabitem/types.ts b/js/tabitem/types.ts index 5138e9d1d7f..7468d4c771f 100644 --- a/js/tabitem/types.ts +++ b/js/tabitem/types.ts @@ -8,7 +8,6 @@ export interface TabItemProps { visible: boolean | "hidden"; interactive: boolean; order: number; - scale: number; component_id: number; } diff --git a/js/textbox/Index.svelte b/js/textbox/Index.svelte index bba29a87db2..a1d20ec22fa 100644 --- a/js/textbox/Index.svelte +++ b/js/textbox/Index.svelte @@ -32,7 +32,7 @@ async function handle_input(value: string): Promise { if (!gradio.shared || !gradio.props) return; - gradio.props.validation_error = null; + clear_validation_error(); gradio.props.value = value; await tick(); gradio.dispatch("input"); @@ -44,9 +44,16 @@ function handle_change(value: string): void { if (!gradio.shared || !gradio.props) return; - gradio.props.validation_error = null; + clear_validation_error(); gradio.props.value = value; } + + function clear_validation_error(): void { + gradio.shared.validation_error = null; + if (gradio.shared.loading_status) { + gradio.shared.loading_status.validation_error = null; + } + } { - gradio.shared.validation_error = null; + clear_validation_error(); gradio.dispatch("submit"); }} onblur={() => gradio.dispatch("blur")} diff --git a/js/textbox/Textbox.test.ts b/js/textbox/Textbox.test.ts index 7071c73ef5a..b1fd3982f91 100644 --- a/js/textbox/Textbox.test.ts +++ b/js/textbox/Textbox.test.ts @@ -53,6 +53,26 @@ describe("Textbox", () => { expect(item.value).toBe("hi some text"); }); + + test.each(["input", "change"] as const)( + "%s clears the shared validation error", + async (event_name) => { + const result = await render(Textbox, { + ...default_props, + value: "", + validation_error: "Shared error", + loading_status: { validation_error: "Required" } + }); + const textbox = result.getByRole("textbox"); + + expect(result.getByText("Required")).toBeVisible(); + await fireEvent[event_name](textbox, { target: { value: "valid" } }); + + await waitFor(() => { + expect(result.queryByText("Required")).not.toBeInTheDocument(); + }); + } + ); }); describe("Props: type", () => { diff --git a/js/textbox/types.ts b/js/textbox/types.ts index 8d9ad66b4e1..addd2b0602c 100644 --- a/js/textbox/types.ts +++ b/js/textbox/types.ts @@ -34,7 +34,6 @@ export interface TextboxProps { autoscroll: boolean; max_length: number; html_attributes: InputHTMLAttributes; - validation_error: string | null; } type FullAutoFill = diff --git a/js/utils/src/utils.svelte.ts b/js/utils/src/utils.svelte.ts index c1a602f3eeb..d2119856423 100644 --- a/js/utils/src/utils.svelte.ts +++ b/js/utils/src/utils.svelte.ts @@ -35,7 +35,7 @@ export interface SharedProps { client: Client; scale: number; min_width: number; - padding: number; + padding: boolean; load_component: load_component; loading_status?: any; label: string;