mirror of
https://github.com/gradio-app/gradio.git
synced 2025-02-17 11:29:58 +08:00
Ensures tabs with visible set to false are not visible. (#9653)
* * fix tab visibility * add story * add changeset * stuff * fix * more fix * fix undefined tab labels * fix tabs again * add changeset * format * format * fix type * add changeset * fix all things * format * add changeset * notebooks * visible tabs --------- Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com> Co-authored-by: Abubakar Abid <abubakar@huggingface.co> Co-authored-by: pngwn <hello@pngwn.io> Co-authored-by: freddyaboulton <alfonsoboulton@gmail.com>
This commit is contained in:
parent
cfd60b0279
commit
61cd768490
12
.changeset/open-geckos-flash.md
Normal file
12
.changeset/open-geckos-flash.md
Normal file
@ -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.
|
@ -15,7 +15,7 @@ const base = defineConfig({
|
||||
}
|
||||
},
|
||||
expect: { timeout: 10000 },
|
||||
timeout: 10000,
|
||||
timeout: 30000,
|
||||
testMatch: /.*\.spec\.ts/,
|
||||
testDir: "..",
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
|
@ -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}
|
||||
{"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}
|
@ -8,8 +8,10 @@ css = """
|
||||
}
|
||||
|
||||
.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 @@ css = """
|
||||
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()
|
||||
|
1
demo/tabs_visibility/run.ipynb
Normal file
1
demo/tabs_visibility/run.ipynb
Normal file
@ -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}
|
36
demo/tabs_visibility/run.py
Normal file
36
demo/tabs_visibility/run.py
Normal file
@ -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()
|
@ -540,7 +540,7 @@
|
||||
class="mt-1 flex-1 flex flex-col relative overflow-scroll code-scroll"
|
||||
>
|
||||
<Tabs
|
||||
inital_tabs={TABS}
|
||||
initial_tabs={TABS}
|
||||
selected={selected_tab}
|
||||
elem_classes={["editor-tabs"]}
|
||||
>
|
||||
|
@ -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<typeof create_intersection_store> = {
|
||||
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;
|
||||
|
@ -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,
|
||||
|
@ -43,6 +43,8 @@
|
||||
|
||||
let _components: Record<string, ComponentType<SvelteComponent>> = {};
|
||||
|
||||
const is_browser = typeof window !== "undefined";
|
||||
|
||||
async function update_components(): Promise<void> {
|
||||
_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}
|
||||
|
@ -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<number>();
|
||||
export function create_components(initial_layout: ComponentMeta | undefined): {
|
||||
layout: Writable<ComponentMeta>;
|
||||
targets: Writable<TargetMap>;
|
||||
@ -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;
|
||||
|
@ -30,7 +30,8 @@
|
||||
"development": "./server.ts",
|
||||
"default": "./dist/server.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"scripts": {
|
||||
"package": "svelte-package --input=. --cwd=../../.config/"
|
||||
|
@ -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"]);
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
</script>
|
||||
@ -20,11 +22,11 @@
|
||||
<TabItem
|
||||
{elem_id}
|
||||
{elem_classes}
|
||||
name={label}
|
||||
{label}
|
||||
{visible}
|
||||
{interactive}
|
||||
{id}
|
||||
on:select={({ detail }) => gradio.dispatch("select", detail)}
|
||||
on:select={({ detail }) => gradio?.dispatch("select", detail)}
|
||||
>
|
||||
<slot />
|
||||
</TabItem>
|
||||
|
@ -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 }));
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -13,11 +13,13 @@
|
||||
export let elem_id = "";
|
||||
export let elem_classes: string[] = [];
|
||||
export let selected: number | string;
|
||||
export let inital_tabs: Tab[] = [];
|
||||
export let gradio: Gradio<{
|
||||
change: never;
|
||||
select: SelectData;
|
||||
}>;
|
||||
export let initial_tabs: Tab[] = [];
|
||||
export let gradio:
|
||||
| Gradio<{
|
||||
change: never;
|
||||
select: SelectData;
|
||||
}>
|
||||
| undefined;
|
||||
|
||||
$: dispatch("prop_change", { selected });
|
||||
</script>
|
||||
@ -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}
|
||||
>
|
||||
<slot />
|
||||
</Tabs>
|
||||
|
48
js/tabs/Tabs.stories.svelte
Normal file
48
js/tabs/Tabs.stories.svelte
Normal file
@ -0,0 +1,48 @@
|
||||
<script>
|
||||
import { Meta, Template, Story } from "@storybook/addon-svelte-csf";
|
||||
import Tabs from "./Index.svelte";
|
||||
import TabItem from "../tabitem/Index.svelte";
|
||||
</script>
|
||||
|
||||
<Meta title="Components/Tabs" component={Tabs} />
|
||||
|
||||
<Template let:args>
|
||||
<Tabs {...args}>
|
||||
<TabItem
|
||||
id="tab-1"
|
||||
label="Image Tab"
|
||||
gradio={undefined}
|
||||
visible
|
||||
interactive
|
||||
elem_classes={["editor-tabitem"]}
|
||||
>
|
||||
<img
|
||||
style="width: 200px;"
|
||||
alt="Cheetah"
|
||||
src="https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg"
|
||||
/>
|
||||
</TabItem>
|
||||
<TabItem
|
||||
id="tab-2"
|
||||
label="Hidden Tab"
|
||||
gradio={undefined}
|
||||
visible={false}
|
||||
interactive
|
||||
elem_classes={["editor-tabitem"]}
|
||||
>
|
||||
Secret Tab
|
||||
</TabItem>
|
||||
<TabItem
|
||||
id="tab-3"
|
||||
label="Visible Tab"
|
||||
gradio={undefined}
|
||||
visible
|
||||
interactive
|
||||
elem_classes={["editor-tabitem"]}
|
||||
>
|
||||
Visible Tab
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</Template>
|
||||
|
||||
<Story name="Tabs" args={{}} />
|
@ -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 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
setContext,
|
||||
createEventDispatcher,
|
||||
onMount,
|
||||
onDestroy
|
||||
} from "svelte";
|
||||
import { setContext, createEventDispatcher, tick, onMount } from "svelte";
|
||||
import OverflowIcon from "./OverflowIcon.svelte";
|
||||
import { writable } from "svelte/store";
|
||||
import type { SelectData } from "@gradio/utils";
|
||||
@ -25,16 +20,17 @@
|
||||
export let elem_id = "";
|
||||
export let elem_classes: string[] = [];
|
||||
export let selected: number | string;
|
||||
export let inital_tabs: Tab[] = [];
|
||||
export let initial_tabs: Tab[];
|
||||
|
||||
let tabs: Tab[] = inital_tabs;
|
||||
let tabs: Tab[] = [...initial_tabs];
|
||||
let visible_tabs: Tab[] = [...initial_tabs];
|
||||
let overflow_tabs: Tab[] = [];
|
||||
let overflow_menu_open = false;
|
||||
let overflow_menu: HTMLElement;
|
||||
|
||||
$: has_tabs = tabs.length > 0;
|
||||
|
||||
let tab_nav_el: HTMLElement;
|
||||
let overflow_nav: HTMLElement;
|
||||
let tab_nav_el: HTMLDivElement;
|
||||
|
||||
const selected_tab = writable<false | number | string>(
|
||||
selected || tabs[0]?.id || false
|
||||
@ -49,6 +45,14 @@
|
||||
|
||||
let is_overflowing = false;
|
||||
let overflow_has_selected_tab = false;
|
||||
let tab_els: Record<string | number, HTMLElement> = {};
|
||||
|
||||
onMount(() => {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
handle_menu_overflow();
|
||||
});
|
||||
observer.observe(tab_nav_el);
|
||||
});
|
||||
|
||||
setContext(TABS, {
|
||||
register_tab: (tab: Tab) => {
|
||||
@ -94,19 +98,7 @@
|
||||
}
|
||||
|
||||
$: tabs, selected !== null && change_tab(selected);
|
||||
|
||||
onMount(() => {
|
||||
handle_menu_overflow();
|
||||
|
||||
window.addEventListener("resize", handle_menu_overflow);
|
||||
window.addEventListener("click", handle_outside_click);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
window.removeEventListener("resize", handle_menu_overflow);
|
||||
window.removeEventListener("click", handle_outside_click);
|
||||
});
|
||||
$: tabs, tab_nav_el, tab_els, handle_menu_overflow();
|
||||
|
||||
function handle_outside_click(event: MouseEvent): void {
|
||||
if (
|
||||
@ -118,42 +110,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handle_menu_overflow(): void {
|
||||
if (!tab_nav_el) {
|
||||
console.error("Menu elements not found");
|
||||
return;
|
||||
async function handle_menu_overflow(): Promise<void> {
|
||||
if (!tab_nav_el) return;
|
||||
|
||||
await tick();
|
||||
const tab_nav_size = tab_nav_el.getBoundingClientRect();
|
||||
|
||||
let max_width = tab_nav_size.width;
|
||||
const tab_sizes = get_tab_sizes(tabs, tab_els);
|
||||
let last_visible_index = 0;
|
||||
const offset = tab_nav_size.left;
|
||||
|
||||
for (let i = tabs.length - 1; i >= 0; i--) {
|
||||
const tab = tabs[i];
|
||||
const tab_rect = tab_sizes[tab.id];
|
||||
if (!tab_rect) continue;
|
||||
if (tab_rect.right - offset < max_width) {
|
||||
last_visible_index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let all_items: HTMLElement[] = [];
|
||||
overflow_tabs = tabs.slice(last_visible_index + 1);
|
||||
visible_tabs = tabs.slice(0, last_visible_index + 1);
|
||||
|
||||
[tab_nav_el, overflow_nav].forEach((menu) => {
|
||||
Array.from(menu.querySelectorAll("button")).forEach((item) =>
|
||||
all_items.push(item as HTMLElement)
|
||||
);
|
||||
});
|
||||
|
||||
all_items.forEach((item) => tab_nav_el.appendChild(item));
|
||||
|
||||
const nav_items: HTMLElement[] = [];
|
||||
const overflow_items: HTMLElement[] = [];
|
||||
|
||||
Array.from(tab_nav_el.querySelectorAll("button")).forEach((item) => {
|
||||
const tab_rect = item.getBoundingClientRect();
|
||||
const tab_menu_rect = tab_nav_el.getBoundingClientRect();
|
||||
is_overflowing =
|
||||
tab_rect.right > tab_menu_rect.right ||
|
||||
tab_rect.left < tab_menu_rect.left;
|
||||
|
||||
if (is_overflowing) {
|
||||
overflow_items.push(item as HTMLElement);
|
||||
} else {
|
||||
nav_items.push(item as HTMLElement);
|
||||
}
|
||||
});
|
||||
|
||||
nav_items.forEach((item) => tab_nav_el.appendChild(item));
|
||||
overflow_items.forEach((item) => overflow_nav.appendChild(item));
|
||||
overflow_has_selected_tab = handle_overflow_has_selected_tab($selected_tab);
|
||||
is_overflowing = overflow_tabs.length > 0;
|
||||
}
|
||||
|
||||
$: overflow_has_selected_tab =
|
||||
@ -162,38 +144,61 @@
|
||||
function handle_overflow_has_selected_tab(
|
||||
selected_tab: number | string | false
|
||||
): boolean {
|
||||
if (selected_tab === false || !overflow_nav) return false;
|
||||
return tabs.some(
|
||||
(t) =>
|
||||
t.id === selected_tab &&
|
||||
overflow_nav.contains(document.querySelector(`[data-tab-id="${t.id}"]`))
|
||||
);
|
||||
if (selected_tab === false) return false;
|
||||
return overflow_tabs.some((t) => t.id === selected_tab);
|
||||
}
|
||||
|
||||
function get_tab_sizes(
|
||||
tabs: Tab[],
|
||||
tab_els: Record<string | number, HTMLElement>
|
||||
): Record<string | number, DOMRect> {
|
||||
const tab_sizes: Record<string | number, DOMRect> = {};
|
||||
tabs.forEach((tab) => {
|
||||
tab_sizes[tab.id] = tab_els[tab.id]?.getBoundingClientRect();
|
||||
});
|
||||
return tab_sizes;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:resize={handle_menu_overflow}
|
||||
on:click={handle_outside_click}
|
||||
/>
|
||||
|
||||
{#if has_tabs}
|
||||
<div class="tabs {elem_classes.join(' ')}" class:hide={!visible} id={elem_id}>
|
||||
<div class="tab-wrapper">
|
||||
<div class="tab-container" bind:this={tab_nav_el} role="tablist">
|
||||
<div class="tab-container visually-hidden" aria-hidden="true">
|
||||
{#each tabs as t, i (t.id)}
|
||||
<button
|
||||
role="tab"
|
||||
class:selected={t.id === $selected_tab}
|
||||
aria-selected={t.id === $selected_tab}
|
||||
aria-controls={t.elem_id}
|
||||
disabled={!t.interactive}
|
||||
aria-disabled={!t.interactive}
|
||||
id={t.elem_id ? t.elem_id + "-button" : null}
|
||||
data-tab-id={t.id}
|
||||
on:click={() => {
|
||||
if (t.id !== $selected_tab) {
|
||||
change_tab(t.id);
|
||||
dispatch("select", { value: t.name, index: i });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t.name}
|
||||
</button>
|
||||
{#if t.visible}
|
||||
<button bind:this={tab_els[t.id]}>
|
||||
{t.label}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tab-container" bind:this={tab_nav_el} role="tablist">
|
||||
{#each visible_tabs as t, i (t.id)}
|
||||
{#if t.visible}
|
||||
<button
|
||||
role="tab"
|
||||
class:selected={t.id === $selected_tab}
|
||||
aria-selected={t.id === $selected_tab}
|
||||
aria-controls={t.elem_id}
|
||||
disabled={!t.interactive}
|
||||
aria-disabled={!t.interactive}
|
||||
id={t.elem_id ? t.elem_id + "-button" : null}
|
||||
data-tab-id={t.id}
|
||||
on:click={() => {
|
||||
if (t.id !== $selected_tab) {
|
||||
change_tab(t.id);
|
||||
dispatch("select", { value: t.label, index: i });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<span
|
||||
@ -208,11 +213,16 @@
|
||||
>
|
||||
<OverflowIcon />
|
||||
</button>
|
||||
<div
|
||||
class="overflow-dropdown"
|
||||
bind:this={overflow_nav}
|
||||
class:hide={!overflow_menu_open}
|
||||
/>
|
||||
<div class="overflow-dropdown" class:hide={!overflow_menu_open}>
|
||||
{#each overflow_tabs as t}
|
||||
<button
|
||||
on:click={() => change_tab(t.id)}
|
||||
class:selected={t.id === $selected_tab}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -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;
|
||||
}
|
||||
</style>
|
||||
|
Loading…
Reference in New Issue
Block a user