diff --git a/.changeset/open-geckos-flash.md b/.changeset/open-geckos-flash.md new file mode 100644 index 0000000000000..67dc3a5fcaf55 --- /dev/null +++ b/.changeset/open-geckos-flash.md @@ -0,0 +1,12 @@ +--- +"@gradio/chatbot": patch +"@gradio/core": patch +"@gradio/sanitize": patch +"@gradio/tabitem": patch +"@gradio/tabs": patch +"@self/app": patch +"gradio": patch +"website": patch +--- + +fix:Ensures tabs with visible set to false are not visible. diff --git a/.config/playwright.config.js b/.config/playwright.config.js index 01d9b1ce1215f..d7eb6f9f4cd08 100644 --- a/.config/playwright.config.js +++ b/.config/playwright.config.js @@ -15,7 +15,7 @@ const base = defineConfig({ } }, expect: { timeout: 10000 }, - timeout: 10000, + timeout: 30000, testMatch: /.*\.spec\.ts/, testDir: "..", workers: process.env.CI ? 1 : undefined, diff --git a/demo/custom_css/run.ipynb b/demo/custom_css/run.ipynb index 04098fc284d09..3dce1d93515cb 100644 --- a/demo/custom_css/run.ipynb +++ b/demo/custom_css/run.ipynb @@ -1 +1 @@ -{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: custom_css"]}, {"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", "css = \"\"\"\n", "/* CSSKeyframesRule for animation */\n", "@keyframes animation {\n", " from {background-color: red;}\n", " to {background-color: blue;}\n", "}\n", "\n", ".cool-col {\n", " animation-name: animation;\n", " animation-duration: 4s;\n", " animation-iteration-count: infinite;\n", " border-radius: 10px;\n", " padding: 20px;\n", "}\n", "\n", "/* CSSStyleRule */\n", ".markdown {\n", " background-color: lightblue;\n", " padding: 20px;\n", "}\n", "\n", ".markdown p {\n", " color: royalblue;\n", "}\n", "\n", "/* CSSMediaRule */\n", "@media screen and (max-width: 600px) {\n", " .markdown {\n", " background: blue;\n", " }\n", " .markdown p {\n", " color: lightblue;\n", " }\n", "}\n", "\n", ".dark .markdown {\n", " background: pink;\n", "}\n", "\n", ".darktest h3 {\n", " color: black;\n", "}\n", "\n", ".dark .darktest h3 {\n", " color: yellow;\n", "}\n", "\n", "/* CSSFontFaceRule */\n", "@font-face {\n", " font-family: \"test-font\";\n", " src: url(\"https://mdn.github.io/css-examples/web-fonts/VeraSeBd.ttf\") format(\"truetype\");\n", "}\n", "\n", ".cool-col {\n", " font-family: \"test-font\";\n", "}\n", "\n", "/* CSSImportRule */\n", "@import url(\"https://fonts.googleapis.com/css2?family=Protest+Riot&display=swap\");\n", "\n", ".markdown {\n", " font-family: \"Protest Riot\", sans-serif;\n", "}\n", "\"\"\"\n", "\n", "with gr.Blocks(css=css) as demo:\n", " with gr.Column(elem_classes=\"cool-col\"):\n", " gr.Markdown(\"### Gradio Demo with Custom CSS\", elem_classes=\"darktest\")\n", " gr.Markdown(elem_classes=\"markdown\", value=\"Resize the browser window to see the CSS media query in action.\")\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file +{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: custom_css"]}, {"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", "css = \"\"\"\n", "/* CSSKeyframesRule for animation */\n", "@keyframes animation {\n", " from {background-color: red;}\n", " to {background-color: blue;}\n", "}\n", "\n", ".cool-col {\n", " background-color: red;\n", " animation-name: animation;\n", " animation-duration: 4s;\n", " animation-delay: 2s;\n", " animation-iteration-count: infinite;\n", " border-radius: 10px;\n", " padding: 20px;\n", "}\n", "\n", "/* CSSStyleRule */\n", ".markdown {\n", " background-color: lightblue;\n", " padding: 20px;\n", "}\n", "\n", ".markdown p {\n", " color: royalblue;\n", "}\n", "\n", "/* CSSMediaRule */\n", "@media screen and (max-width: 600px) {\n", " .markdown {\n", " background: blue;\n", " }\n", " .markdown p {\n", " color: lightblue;\n", " }\n", "}\n", "\n", ".dark .markdown {\n", " background: pink;\n", "}\n", "\n", ".darktest h3 {\n", " color: black;\n", "}\n", "\n", ".dark .darktest h3 {\n", " color: yellow;\n", "}\n", "\n", "/* CSSFontFaceRule */\n", "@font-face {\n", " font-family: \"test-font\";\n", " src: url(\"https://mdn.github.io/css-examples/web-fonts/VeraSeBd.ttf\") format(\"truetype\");\n", "}\n", "\n", ".cool-col {\n", " font-family: \"test-font\";\n", "}\n", "\n", "/* CSSImportRule */\n", "@import url(\"https://fonts.googleapis.com/css2?family=Protest+Riot&display=swap\");\n", "\n", ".markdown {\n", " font-family: \"Protest Riot\", sans-serif;\n", "}\n", "\"\"\"\n", "\n", "with gr.Blocks(css=css) as demo:\n", " with gr.Column(elem_classes=\"cool-col\"):\n", " gr.Markdown(\"### Gradio Demo with Custom CSS\", elem_classes=\"darktest\")\n", " gr.Markdown(\n", " elem_classes=\"markdown\",\n", " value=\"Resize the browser window to see the CSS media query in action.\",\n", " )\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file diff --git a/demo/custom_css/run.py b/demo/custom_css/run.py index 38512e33d69f5..240045f21a67b 100644 --- a/demo/custom_css/run.py +++ b/demo/custom_css/run.py @@ -8,8 +8,10 @@ } .cool-col { + background-color: red; animation-name: animation; animation-duration: 4s; + animation-delay: 2s; animation-iteration-count: infinite; border-radius: 10px; padding: 20px; @@ -68,7 +70,10 @@ with gr.Blocks(css=css) as demo: with gr.Column(elem_classes="cool-col"): gr.Markdown("### Gradio Demo with Custom CSS", elem_classes="darktest") - gr.Markdown(elem_classes="markdown", value="Resize the browser window to see the CSS media query in action.") + gr.Markdown( + elem_classes="markdown", + value="Resize the browser window to see the CSS media query in action.", + ) if __name__ == "__main__": demo.launch() diff --git a/demo/tabs_visibility/run.ipynb b/demo/tabs_visibility/run.ipynb new file mode 100644 index 0000000000000..214774c2b83d1 --- /dev/null +++ b/demo/tabs_visibility/run.ipynb @@ -0,0 +1 @@ +{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: tabs_visibility"]}, {"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.Tab(\"abc\"):\n", " gr.Textbox(label=\"abc\")\n", " with gr.Tab(\"def\", visible=False) as t:\n", " gr.Textbox(label=\"def\")\n", " with gr.Tab(\"ghi\"):\n", " gr.Textbox(label=\"ghi\")\n", " with gr.Tab(\"jkl\", visible=False) as t2:\n", " gr.Textbox(label=\"jkl\")\n", " with gr.Tab(\"mno\"):\n", " gr.Textbox(label=\"mno\")\n", " with gr.Tab(\"pqr\", visible=False) as t3:\n", " gr.Textbox(label=\"pqr\")\n", " with gr.Tab(\"stu\"):\n", " gr.Textbox(label=\"stu\")\n", " with gr.Tab(\"vwx\", visible=False) as t4:\n", " gr.Textbox(label=\"vwx\")\n", " with gr.Tab(\"yz\"):\n", " gr.Textbox(label=\"yz\")\n", " b = gr.Button(\"Make visible\")\n", "\n", " b.click(\n", " lambda: [\n", " gr.Tab(visible=True),\n", " gr.Tab(visible=True),\n", " gr.Tab(visible=True),\n", " gr.Tab(visible=True),\n", " ],\n", " inputs=None,\n", " outputs=[t, t2, t3, t4],\n", " )\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file diff --git a/demo/tabs_visibility/run.py b/demo/tabs_visibility/run.py new file mode 100644 index 0000000000000..4b6c026ac1c8e --- /dev/null +++ b/demo/tabs_visibility/run.py @@ -0,0 +1,36 @@ +import gradio as gr + +with gr.Blocks() as demo: + with gr.Tab("abc"): + gr.Textbox(label="abc") + with gr.Tab("def", visible=False) as t: + gr.Textbox(label="def") + with gr.Tab("ghi"): + gr.Textbox(label="ghi") + with gr.Tab("jkl", visible=False) as t2: + gr.Textbox(label="jkl") + with gr.Tab("mno"): + gr.Textbox(label="mno") + with gr.Tab("pqr", visible=False) as t3: + gr.Textbox(label="pqr") + with gr.Tab("stu"): + gr.Textbox(label="stu") + with gr.Tab("vwx", visible=False) as t4: + gr.Textbox(label="vwx") + with gr.Tab("yz"): + gr.Textbox(label="yz") + b = gr.Button("Make visible") + + b.click( + lambda: [ + gr.Tab(visible=True), + gr.Tab(visible=True), + gr.Tab(visible=True), + gr.Tab(visible=True), + ], + inputs=None, + outputs=[t, t2, t3, t4], + ) + +if __name__ == "__main__": + demo.launch() diff --git a/js/_website/src/lib/components/DemosLite.svelte b/js/_website/src/lib/components/DemosLite.svelte index bf482f21689aa..41ceb97b12b6a 100644 --- a/js/_website/src/lib/components/DemosLite.svelte +++ b/js/_website/src/lib/components/DemosLite.svelte @@ -540,7 +540,7 @@ class="mt-1 flex-1 flex flex-col relative overflow-scroll code-scroll" > diff --git a/js/app/src/routes/[...catchall]/+page.svelte b/js/app/src/routes/[...catchall]/+page.svelte index c83be0b09389c..1906be7eca606 100644 --- a/js/app/src/routes/[...catchall]/+page.svelte +++ b/js/app/src/routes/[...catchall]/+page.svelte @@ -94,6 +94,59 @@ export let container: boolean; let stream: EventSource; + function handle_theme_mode(target: HTMLElement): "light" | "dark" { + let new_theme_mode: ThemeMode; + + const url = new URL(window.location.toString()); + const url_color_mode: ThemeMode | null = url.searchParams.get( + "__theme" + ) as ThemeMode | null; + new_theme_mode = theme_mode || url_color_mode || "system"; + + if (new_theme_mode === "dark" || new_theme_mode === "light") { + apply_theme(target, new_theme_mode); + } else { + new_theme_mode = sync_system_theme(target); + } + return new_theme_mode; + } + + function sync_system_theme(target: HTMLElement): "light" | "dark" { + const theme = update_scheme(); + window + ?.matchMedia("(prefers-color-scheme: dark)") + ?.addEventListener("change", update_scheme); + + function update_scheme(): "light" | "dark" { + let _theme: "light" | "dark" = window?.matchMedia?.( + "(prefers-color-scheme: dark)" + ).matches + ? "dark" + : "light"; + + apply_theme(target, _theme); + return _theme; + } + return theme; + } + + function apply_theme(target: HTMLElement, theme: "dark" | "light"): void { + const dark_class_element = is_embed ? target.parentElement! : document.body; + const bg_element = is_embed ? target : target.parentElement!; + bg_element.style.background = "var(--body-background-fill)"; + if (theme === "dark") { + dark_class_element.classList.add("dark"); + } else { + dark_class_element.classList.remove("dark"); + } + } + + let active_theme_mode: ThemeMode; + + if (browser) { + active_theme_mode = handle_theme_mode(document.body); + } + // These utilities are exported to be injectable for the Wasm version. // export let Client: typeof ClientType; @@ -112,7 +165,7 @@ let render_complete = false; $: config = data.config; let loading_text = $_("common.loading") + "..."; - let active_theme_mode: ThemeMode; + let intersecting: ReturnType = { register: () => {}, subscribe: writable({}).subscribe @@ -139,8 +192,6 @@ let gradio_dev_mode = ""; onMount(async () => { - // active_theme_mode = handle_theme_mode(wrapper); - //@ts-ignore config = data.config; window.gradio_config = config; diff --git a/js/app/src/routes/[...catchall]/+page.ts b/js/app/src/routes/[...catchall]/+page.ts index 9d003b12bcefb..4c65f4ce6817c 100644 --- a/js/app/src/routes/[...catchall]/+page.ts +++ b/js/app/src/routes/[...catchall]/+page.ts @@ -31,7 +31,7 @@ export async function load({ throw new Error("No config found"); } - const { create_layout, layout } = create_components(); + const { create_layout, layout } = create_components(undefined); await create_layout({ app, diff --git a/js/chatbot/shared/ChatBot.svelte b/js/chatbot/shared/ChatBot.svelte index 9fc0bc024f195..cd45ab0c0fa56 100644 --- a/js/chatbot/shared/ChatBot.svelte +++ b/js/chatbot/shared/ChatBot.svelte @@ -43,6 +43,8 @@ let _components: Record> = {}; + const is_browser = typeof window !== "undefined"; + async function update_components(): Promise { _components = await load_components( get_components_from_messages(value), @@ -323,7 +325,7 @@ show_undo={_undoable && is_last_bot_message(messages, value)} {show_copy_button} handle_action={(selected) => handle_like(i, messages[0], selected)} - {scroll} + scroll={is_browser ? scroll : () => {}} /> {/each} {#if pending_message} diff --git a/js/core/src/init.ts b/js/core/src/init.ts index 7882e0769e11a..89bad0fc9410a 100644 --- a/js/core/src/init.ts +++ b/js/core/src/init.ts @@ -1,4 +1,5 @@ import { writable, type Writable, get } from "svelte/store"; + import type { ComponentMeta, Dependency, @@ -27,6 +28,7 @@ const raf = is_browser * Create a store with the layout and a map of targets * @returns A store with the layout and a map of targets */ +let has_run = new Set(); export function create_components(initial_layout: ComponentMeta | undefined): { layout: Writable; targets: Writable; @@ -290,19 +292,26 @@ export function create_components(initial_layout: ComponentMeta | undefined): { ); } - if (instance.type === "tabs") { - instance.children = - instance?.children?.map((c) => ({ - ...c, - props: { - ...c.props, - id: c.props.id || c.id - } - })) || []; - const child_tab_items = instance.children?.filter( + if (instance.type === "tabs" && !instance.props.initial_tabs) { + const tab_items_props = + node.children?.map((c) => { + const instance = instance_map[c.id]; + // console.log("tabs", JSON.stringify(instance.props, null, 2)); + instance.props.id ??= c.id; + return { + type: instance.type, + props: { + ...(instance.props as any), + id: instance.props.id + } + }; + }) || []; + + const child_tab_items = tab_items_props.filter( (child) => child.type === "tabitem" ); - instance.props.inital_tabs = child_tab_items?.map((child) => ({ + + instance.props.initial_tabs = child_tab_items?.map((child) => ({ label: child.props.label, id: child.props.id, visible: child.props.visible, @@ -337,7 +346,7 @@ export function create_components(initial_layout: ComponentMeta | undefined): { else if (update.value instanceof Set) new_value = new Set(update.value); else if (Array.isArray(update.value)) new_value = [...update.value]; - else if (update.value === null) new_value = null; + else if (update.value == null) new_value = null; else if (typeof update.value === "object") new_value = { ...update.value }; else new_value = update.value; diff --git a/js/sanitize/package.json b/js/sanitize/package.json index 49c6d442cc966..67a919db2f549 100644 --- a/js/sanitize/package.json +++ b/js/sanitize/package.json @@ -30,7 +30,8 @@ "development": "./server.ts", "default": "./dist/server.js" } - } + }, + "./package.json": "./package.json" }, "scripts": { "package": "svelte-package --input=. --cwd=../../.config/" diff --git a/js/spa/test/audio_debugger.spec.ts b/js/spa/test/audio_debugger.spec.ts index f367980e3420a..d901e0f80b74a 100644 --- a/js/spa/test/audio_debugger.spec.ts +++ b/js/spa/test/audio_debugger.spec.ts @@ -71,7 +71,7 @@ test("recording audio", async ({ page }) => { permissions: ["microphone"] }); - await page.getByText("Interface").click(); + await page.getByRole("tab", { name: "Interface" }).click(); await page.getByLabel("Record audio").click(); context.grantPermissions(["microphone"]); diff --git a/js/spa/test/blocks_xray.spec.ts b/js/spa/test/blocks_xray.spec.ts index 433588894dde0..3dc02a80c6924 100644 --- a/js/spa/test/blocks_xray.spec.ts +++ b/js/spa/test/blocks_xray.spec.ts @@ -7,7 +7,8 @@ test("renders the correct elements", async ({ page }) => { const checkboxes = await page.getByTestId("checkbox-group"); await expect(checkboxes).toContainText("Covid Malaria Lung Cancer"); - const tabs = await page.locator("button", { hasText: /X-ray|CT Scan/ }); + // const tabs = await page.locator("button", { hasText: /X-ray|CT Scan/ }); + const tabs = await page.getByRole("tab", { name: /X-ray|CT Scan/ }); await expect(tabs).toHaveCount(2); }); diff --git a/js/tabitem/Index.svelte b/js/tabitem/Index.svelte index ff8e842b65483..8a378d85a076f 100644 --- a/js/tabitem/Index.svelte +++ b/js/tabitem/Index.svelte @@ -10,9 +10,11 @@ export let elem_classes: string[] = []; export let label: string; export let id: string | number; - export let gradio: Gradio<{ - select: SelectData; - }>; + export let gradio: + | Gradio<{ + select: SelectData; + }> + | undefined; export let visible = true; export let interactive = true; @@ -20,11 +22,11 @@ gradio.dispatch("select", detail)} + on:select={({ detail }) => gradio?.dispatch("select", detail)} > diff --git a/js/tabitem/shared/TabItem.svelte b/js/tabitem/shared/TabItem.svelte index 16b7fd9639814..1a9a9dc89b30b 100644 --- a/js/tabitem/shared/TabItem.svelte +++ b/js/tabitem/shared/TabItem.svelte @@ -6,7 +6,7 @@ export let elem_id = ""; export let elem_classes: string[] = []; - export let name: string; + export let label: string; export let id: string | number | object = {}; export let visible: boolean; export let interactive: boolean; @@ -18,14 +18,14 @@ let tab_index: number; - $: tab_index = register_tab({ name, id, elem_id, visible, interactive }); + $: tab_index = register_tab({ label, id, elem_id, visible, interactive }); onMount(() => { - return (): void => unregister_tab({ name, id, elem_id }); + return (): void => unregister_tab({ label, id, elem_id }); }); $: $selected_tab_index === tab_index && - tick().then(() => dispatch("select", { value: name, index: tab_index })); + tick().then(() => dispatch("select", { value: label, index: tab_index }));
; + export let initial_tabs: Tab[] = []; + export let gradio: + | Gradio<{ + change: never; + select: SelectData; + }> + | undefined; $: dispatch("prop_change", { selected }); @@ -27,9 +29,9 @@ {elem_id} {elem_classes} bind:selected - on:change={() => gradio.dispatch("change")} - on:select={(e) => gradio.dispatch("select", e.detail)} - {inital_tabs} + on:change={() => gradio?.dispatch("change")} + on:select={(e) => gradio?.dispatch("select", e.detail)} + {initial_tabs} > diff --git a/js/tabs/Tabs.stories.svelte b/js/tabs/Tabs.stories.svelte new file mode 100644 index 0000000000000..2f68316c93f3f --- /dev/null +++ b/js/tabs/Tabs.stories.svelte @@ -0,0 +1,48 @@ + + + + + + + diff --git a/js/tabs/shared/Tabs.svelte b/js/tabs/shared/Tabs.svelte index ac7aaa3862741..848290ab5ca2e 100644 --- a/js/tabs/shared/Tabs.svelte +++ b/js/tabs/shared/Tabs.svelte @@ -2,7 +2,7 @@ export const TABS = {}; export interface Tab { - name: string; + label: string; id: string | number; elem_id: string | undefined; visible: boolean; @@ -11,12 +11,7 @@ + + {#if has_tabs}
-
+ +
+ {#each visible_tabs as t, i (t.id)} + {#if t.visible} + + {/if} {/each}
-
+
+ {#each overflow_tabs as t} + + {/each} +
@@ -270,7 +280,7 @@ color: var(--body-text-color); font-weight: var(--section-header-text-weight); font-size: var(--section-header-text-size); - transition: all 0.2s ease-out; + transition: background-color color 0.2s ease-out; background-color: transparent; height: 100%; display: flex; @@ -365,4 +375,16 @@ .overflow-item-selected :global(svg) { color: var(--color-accent); } + + .visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + }