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:
Hannah 2024-10-22 00:46:01 +01:00 committed by GitHub
parent cfd60b0279
commit 61cd768490
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 320 additions and 128 deletions

View 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.

View File

@ -15,7 +15,7 @@ const base = defineConfig({
}
},
expect: { timeout: 10000 },
timeout: 10000,
timeout: 30000,
testMatch: /.*\.spec\.ts/,
testDir: "..",
workers: process.env.CI ? 1 : undefined,

View File

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

View File

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

View 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}

View 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()

View File

@ -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"]}
>

View File

@ -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;

View File

@ -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,

View File

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

View File

@ -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;

View File

@ -30,7 +30,8 @@
"development": "./server.ts",
"default": "./dist/server.js"
}
}
},
"./package.json": "./package.json"
},
"scripts": {
"package": "svelte-package --input=. --cwd=../../.config/"

View File

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

View File

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

View File

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

View File

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

View File

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

View 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={{}} />

View File

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