@gradio/tabs 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @gradio/tabs
2
2
 
3
+ ## 0.3.3
4
+
5
+ ### Fixes
6
+
7
+ - [#9836](https://github.com/gradio-app/gradio/pull/9836) [`a4e70f3`](https://github.com/gradio-app/gradio/commit/a4e70f3c428d7a43e31b63d296e9c4c73b09eda8) - Fix Tabs in Rows. Thanks @aliabid94!
8
+
9
+ ## 0.3.2
10
+
11
+ ### Fixes
12
+
13
+ - [#9653](https://github.com/gradio-app/gradio/pull/9653) [`61cd768`](https://github.com/gradio-app/gradio/commit/61cd768490a12f5d63101d5434092bcd1cfc43a8) - Ensures tabs with visible set to false are not visible. Thanks @hannahblair!
14
+ - [#9738](https://github.com/gradio-app/gradio/pull/9738) [`2ade59b`](https://github.com/gradio-app/gradio/commit/2ade59b95d4c3610a1a461cc95f020fbf9627305) - Export `Tabs` type from `@gradio/tabs` and fix the Playground to be compatible with the new Tabs API. Thanks @whitphx!
15
+
3
16
  ## 0.3.1
4
17
 
5
18
  ### Fixes
package/Index.svelte CHANGED
@@ -1,31 +1,25 @@
1
1
  <script context="module" lang="ts">
2
- export { default as BaseTabs, TABS } from "./shared/Tabs.svelte";
2
+ export { default as BaseTabs, TABS, type Tab } from "./shared/Tabs.svelte";
3
3
  </script>
4
4
 
5
5
  <script lang="ts">
6
6
  import type { Gradio, SelectData } from "@gradio/utils";
7
7
  import { createEventDispatcher } from "svelte";
8
- import Tabs from "./shared/Tabs.svelte";
8
+ import Tabs, { type Tab } from "./shared/Tabs.svelte";
9
9
 
10
10
  const dispatch = createEventDispatcher();
11
11
 
12
- interface Tab {
13
- name: string;
14
- id: string | number;
15
- elem_id: string | undefined;
16
- visible: boolean;
17
- interactive: boolean;
18
- }
19
-
20
12
  export let visible = true;
21
13
  export let elem_id = "";
22
14
  export let elem_classes: string[] = [];
23
15
  export let selected: number | string;
24
- export let inital_tabs: Tab[];
25
- export let gradio: Gradio<{
26
- change: never;
27
- select: SelectData;
28
- }>;
16
+ export let initial_tabs: Tab[] = [];
17
+ export let gradio:
18
+ | Gradio<{
19
+ change: never;
20
+ select: SelectData;
21
+ }>
22
+ | undefined;
29
23
 
30
24
  $: dispatch("prop_change", { selected });
31
25
  </script>
@@ -35,9 +29,9 @@
35
29
  {elem_id}
36
30
  {elem_classes}
37
31
  bind:selected
38
- on:change={() => gradio.dispatch("change")}
39
- on:select={(e) => gradio.dispatch("select", e.detail)}
40
- {inital_tabs}
32
+ on:change={() => gradio?.dispatch("change")}
33
+ on:select={(e) => gradio?.dispatch("select", e.detail)}
34
+ {initial_tabs}
41
35
  >
42
36
  <slot />
43
37
  </Tabs>
@@ -0,0 +1,48 @@
1
+ <script>
2
+ import { Meta, Template, Story } from "@storybook/addon-svelte-csf";
3
+ import Tabs from "./Index.svelte";
4
+ import TabItem from "../tabitem/Index.svelte";
5
+ </script>
6
+
7
+ <Meta title="Components/Tabs" component={Tabs} />
8
+
9
+ <Template let:args>
10
+ <Tabs {...args}>
11
+ <TabItem
12
+ id="tab-1"
13
+ label="Image Tab"
14
+ gradio={undefined}
15
+ visible
16
+ interactive
17
+ elem_classes={["editor-tabitem"]}
18
+ >
19
+ <img
20
+ style="width: 200px;"
21
+ alt="Cheetah"
22
+ src="https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg"
23
+ />
24
+ </TabItem>
25
+ <TabItem
26
+ id="tab-2"
27
+ label="Hidden Tab"
28
+ gradio={undefined}
29
+ visible={false}
30
+ interactive
31
+ elem_classes={["editor-tabitem"]}
32
+ >
33
+ Secret Tab
34
+ </TabItem>
35
+ <TabItem
36
+ id="tab-3"
37
+ label="Visible Tab"
38
+ gradio={undefined}
39
+ visible
40
+ interactive
41
+ elem_classes={["editor-tabitem"]}
42
+ >
43
+ Visible Tab
44
+ </TabItem>
45
+ </Tabs>
46
+ </Template>
47
+
48
+ <Story name="Tabs" args={{}} />
package/dist/Index.svelte CHANGED
@@ -2,13 +2,13 @@
2
2
  </script>
3
3
 
4
4
  <script>import { createEventDispatcher } from "svelte";
5
- import Tabs from "./shared/Tabs.svelte";
5
+ import Tabs, {} from "./shared/Tabs.svelte";
6
6
  const dispatch = createEventDispatcher();
7
7
  export let visible = true;
8
8
  export let elem_id = "";
9
9
  export let elem_classes = [];
10
10
  export let selected;
11
- export let inital_tabs;
11
+ export let initial_tabs = [];
12
12
  export let gradio;
13
13
  $:
14
14
  dispatch("prop_change", { selected });
@@ -19,9 +19,9 @@ $:
19
19
  {elem_id}
20
20
  {elem_classes}
21
21
  bind:selected
22
- on:change={() => gradio.dispatch("change")}
23
- on:select={(e) => gradio.dispatch("select", e.detail)}
24
- {inital_tabs}
22
+ on:change={() => gradio?.dispatch("change")}
23
+ on:select={(e) => gradio?.dispatch("select", e.detail)}
24
+ {initial_tabs}
25
25
  >
26
26
  <slot />
27
27
  </Tabs>
@@ -1,23 +1,18 @@
1
1
  import { SvelteComponent } from "svelte";
2
- export { default as BaseTabs, TABS } from "./shared/Tabs.svelte";
2
+ export { default as BaseTabs, TABS, type Tab } from "./shared/Tabs.svelte";
3
3
  import type { Gradio, SelectData } from "@gradio/utils";
4
+ import { type Tab } from "./shared/Tabs.svelte";
4
5
  declare const __propDef: {
5
6
  props: {
6
7
  visible?: boolean | undefined;
7
8
  elem_id?: string | undefined;
8
9
  elem_classes?: string[] | undefined;
9
10
  selected: number | string;
10
- inital_tabs: {
11
- name: string;
12
- id: string | number;
13
- elem_id: string | undefined;
14
- visible: boolean;
15
- interactive: boolean;
16
- }[];
11
+ initial_tabs?: Tab[] | undefined;
17
12
  gradio: Gradio<{
18
13
  change: never;
19
14
  select: SelectData;
20
- }>;
15
+ }> | undefined;
21
16
  };
22
17
  events: {
23
18
  prop_change: CustomEvent<any>;
@@ -1,28 +1,22 @@
1
- <script context="module">
2
- export const TABS = {};
1
+ <script context="module">export const TABS = {};
3
2
  </script>
4
3
 
5
- <script>import {
6
- setContext,
7
- createEventDispatcher,
8
- onMount,
9
- onDestroy,
10
- tick
11
- } from "svelte";
4
+ <script>import { setContext, createEventDispatcher, tick, onMount } from "svelte";
12
5
  import OverflowIcon from "./OverflowIcon.svelte";
13
6
  import { writable } from "svelte/store";
14
7
  export let visible = true;
15
8
  export let elem_id = "";
16
9
  export let elem_classes = [];
17
10
  export let selected;
18
- export let inital_tabs;
19
- let tabs = inital_tabs;
11
+ export let initial_tabs;
12
+ let tabs = [...initial_tabs];
13
+ let visible_tabs = [...initial_tabs];
14
+ let overflow_tabs = [];
20
15
  let overflow_menu_open = false;
21
16
  let overflow_menu;
22
17
  $:
23
18
  has_tabs = tabs.length > 0;
24
19
  let tab_nav_el;
25
- let overflow_nav;
26
20
  const selected_tab = writable(
27
21
  selected || tabs[0]?.id || false
28
22
  );
@@ -32,6 +26,13 @@ const selected_tab_index = writable(
32
26
  const dispatch = createEventDispatcher();
33
27
  let is_overflowing = false;
34
28
  let overflow_has_selected_tab = false;
29
+ let tab_els = {};
30
+ onMount(() => {
31
+ const observer = new IntersectionObserver((entries) => {
32
+ handle_menu_overflow();
33
+ });
34
+ observer.observe(tab_nav_el);
35
+ });
35
36
  setContext(TABS, {
36
37
  register_tab: (tab) => {
37
38
  let index = tabs.findIndex((t) => t.id === tab.id);
@@ -70,84 +71,92 @@ function change_tab(id) {
70
71
  }
71
72
  $:
72
73
  tabs, selected !== null && change_tab(selected);
73
- onMount(() => {
74
- handle_menu_overflow();
75
- window.addEventListener("resize", handle_menu_overflow);
76
- window.addEventListener("click", handle_outside_click);
77
- });
78
- onDestroy(() => {
79
- if (typeof window === "undefined")
80
- return;
81
- window.removeEventListener("resize", handle_menu_overflow);
82
- window.removeEventListener("click", handle_outside_click);
83
- });
74
+ $:
75
+ tabs, tab_nav_el, tab_els, handle_menu_overflow();
84
76
  function handle_outside_click(event) {
85
77
  if (overflow_menu_open && overflow_menu && !overflow_menu.contains(event.target)) {
86
78
  overflow_menu_open = false;
87
79
  }
88
80
  }
89
- function handle_menu_overflow() {
90
- if (!tab_nav_el) {
91
- console.error("Menu elements not found");
81
+ async function handle_menu_overflow() {
82
+ if (!tab_nav_el)
92
83
  return;
93
- }
94
- let all_items = [];
95
- [tab_nav_el, overflow_nav].forEach((menu) => {
96
- Array.from(menu.querySelectorAll("button")).forEach(
97
- (item) => all_items.push(item)
98
- );
99
- });
100
- all_items.forEach((item) => tab_nav_el.appendChild(item));
101
- const nav_items = [];
102
- const overflow_items = [];
103
- Array.from(tab_nav_el.querySelectorAll("button")).forEach((item) => {
104
- const tab_rect = item.getBoundingClientRect();
105
- const tab_menu_rect = tab_nav_el.getBoundingClientRect();
106
- is_overflowing = tab_rect.right > tab_menu_rect.right || tab_rect.left < tab_menu_rect.left;
107
- if (is_overflowing) {
108
- overflow_items.push(item);
109
- } else {
110
- nav_items.push(item);
84
+ await tick();
85
+ const tab_nav_size = tab_nav_el.getBoundingClientRect();
86
+ let max_width = tab_nav_size.width;
87
+ const tab_sizes = get_tab_sizes(tabs, tab_els);
88
+ let last_visible_index = 0;
89
+ const offset = tab_nav_size.left;
90
+ for (let i = tabs.length - 1; i >= 0; i--) {
91
+ const tab = tabs[i];
92
+ const tab_rect = tab_sizes[tab.id];
93
+ if (!tab_rect)
94
+ continue;
95
+ if (tab_rect.right - offset < max_width) {
96
+ last_visible_index = i;
97
+ break;
111
98
  }
112
- });
113
- nav_items.forEach((item) => tab_nav_el.appendChild(item));
114
- overflow_items.forEach((item) => overflow_nav.appendChild(item));
99
+ }
100
+ overflow_tabs = tabs.slice(last_visible_index + 1);
101
+ visible_tabs = tabs.slice(0, last_visible_index + 1);
115
102
  overflow_has_selected_tab = handle_overflow_has_selected_tab($selected_tab);
103
+ is_overflowing = overflow_tabs.length > 0;
116
104
  }
117
105
  $:
118
106
  overflow_has_selected_tab = handle_overflow_has_selected_tab($selected_tab);
119
107
  function handle_overflow_has_selected_tab(selected_tab2) {
120
- if (selected_tab2 === false || !overflow_nav)
108
+ if (selected_tab2 === false)
121
109
  return false;
122
- return tabs.some(
123
- (t) => t.id === selected_tab2 && overflow_nav.contains(document.querySelector(`[data-tab-id="${t.id}"]`))
124
- );
110
+ return overflow_tabs.some((t) => t.id === selected_tab2);
111
+ }
112
+ function get_tab_sizes(tabs2, tab_els2) {
113
+ const tab_sizes = {};
114
+ tabs2.forEach((tab) => {
115
+ tab_sizes[tab.id] = tab_els2[tab.id]?.getBoundingClientRect();
116
+ });
117
+ return tab_sizes;
125
118
  }
126
119
  </script>
127
120
 
128
- {#if has_tabs}
129
- <div class="tabs {elem_classes.join(' ')}" class:hide={!visible} id={elem_id}>
121
+ <svelte:window
122
+ on:resize={handle_menu_overflow}
123
+ on:click={handle_outside_click}
124
+ />
125
+
126
+ <div class="tabs {elem_classes.join(' ')}" class:hide={!visible} id={elem_id}>
127
+ {#if has_tabs}
130
128
  <div class="tab-wrapper">
131
- <div class="tab-container" bind:this={tab_nav_el} role="tablist">
129
+ <div class="tab-container visually-hidden" aria-hidden="true">
132
130
  {#each tabs as t, i (t.id)}
133
- <button
134
- role="tab"
135
- class:selected={t.id === $selected_tab}
136
- aria-selected={t.id === $selected_tab}
137
- aria-controls={t.elem_id}
138
- disabled={!t.interactive}
139
- aria-disabled={!t.interactive}
140
- id={t.elem_id ? t.elem_id + "-button" : null}
141
- data-tab-id={t.id}
142
- on:click={() => {
143
- if (t.id !== $selected_tab) {
144
- change_tab(t.id);
145
- dispatch("select", { value: t.name, index: i });
146
- }
147
- }}
148
- >
149
- {t.name}
150
- </button>
131
+ {#if t.visible}
132
+ <button bind:this={tab_els[t.id]}>
133
+ {t.label}
134
+ </button>
135
+ {/if}
136
+ {/each}
137
+ </div>
138
+ <div class="tab-container" bind:this={tab_nav_el} role="tablist">
139
+ {#each visible_tabs as t, i (t.id)}
140
+ {#if t.visible}
141
+ <button
142
+ role="tab"
143
+ class:selected={t.id === $selected_tab}
144
+ aria-selected={t.id === $selected_tab}
145
+ aria-controls={t.elem_id}
146
+ disabled={!t.interactive}
147
+ aria-disabled={!t.interactive}
148
+ id={t.elem_id ? t.elem_id + "-button" : null}
149
+ data-tab-id={t.id}
150
+ on:click={() => {
151
+ if (t.id !== $selected_tab) {
152
+ change_tab(t.id);
153
+ dispatch("select", { value: t.label, index: i });
154
+ }
155
+ }}
156
+ >
157
+ {t.label}
158
+ </button>
159
+ {/if}
151
160
  {/each}
152
161
  </div>
153
162
  <span
@@ -162,21 +171,28 @@ function handle_overflow_has_selected_tab(selected_tab2) {
162
171
  >
163
172
  <OverflowIcon />
164
173
  </button>
165
- <div
166
- class="overflow-dropdown"
167
- bind:this={overflow_nav}
168
- class:hide={!overflow_menu_open}
169
- />
174
+ <div class="overflow-dropdown" class:hide={!overflow_menu_open}>
175
+ {#each overflow_tabs as t}
176
+ <button
177
+ on:click={() => change_tab(t.id)}
178
+ class:selected={t.id === $selected_tab}
179
+ >
180
+ {t.label}
181
+ </button>
182
+ {/each}
183
+ </div>
170
184
  </span>
171
185
  </div>
172
- </div>
173
- {/if}
174
-
175
- <slot />
186
+ {/if}
187
+ <slot />
188
+ </div>
176
189
 
177
190
  <style>
178
191
  .tabs {
179
192
  position: relative;
193
+ display: flex;
194
+ flex-direction: column;
195
+ gap: var(--layout-gap);
180
196
  }
181
197
 
182
198
  .hide {
@@ -224,7 +240,7 @@ function handle_overflow_has_selected_tab(selected_tab2) {
224
240
  color: var(--body-text-color);
225
241
  font-weight: var(--section-header-text-weight);
226
242
  font-size: var(--section-header-text-size);
227
- transition: all 0.2s ease-out;
243
+ transition: background-color color 0.2s ease-out;
228
244
  background-color: transparent;
229
245
  height: 100%;
230
246
  display: flex;
@@ -319,4 +335,16 @@ function handle_overflow_has_selected_tab(selected_tab2) {
319
335
  .overflow-item-selected :global(svg) {
320
336
  color: var(--color-accent);
321
337
  }
338
+
339
+ .visually-hidden {
340
+ position: absolute;
341
+ width: 1px;
342
+ height: 1px;
343
+ padding: 0;
344
+ margin: -1px;
345
+ overflow: hidden;
346
+ clip: rect(0, 0, 0, 0);
347
+ white-space: nowrap;
348
+ border: 0;
349
+ }
322
350
  </style>
@@ -1,5 +1,12 @@
1
1
  import { SvelteComponent } from "svelte";
2
2
  export declare const TABS: {};
3
+ export interface Tab {
4
+ label: string;
5
+ id: string | number;
6
+ elem_id: string | undefined;
7
+ visible: boolean;
8
+ interactive: boolean;
9
+ }
3
10
  import type { SelectData } from "@gradio/utils";
4
11
  declare const __propDef: {
5
12
  props: {
@@ -7,13 +14,7 @@ declare const __propDef: {
7
14
  elem_id?: string | undefined;
8
15
  elem_classes?: string[] | undefined;
9
16
  selected: number | string;
10
- inital_tabs: {
11
- name: string;
12
- id: string | number;
13
- elem_id: string | undefined;
14
- visible: boolean;
15
- interactive: boolean;
16
- }[];
17
+ initial_tabs: Tab[];
17
18
  };
18
19
  events: {
19
20
  change: CustomEvent<undefined>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/tabs",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -20,7 +20,7 @@
20
20
  "@gradio/utils": "^0.7.0"
21
21
  },
22
22
  "devDependencies": {
23
- "@gradio/preview": "^0.12.0"
23
+ "@gradio/preview": "^0.13.0"
24
24
  },
25
25
  "peerDependencies": {
26
26
  "svelte": "^4.0.0"
@@ -1,41 +1,36 @@
1
- <script context="module">
1
+ <script context="module" lang="ts">
2
2
  export const TABS = {};
3
- </script>
4
-
5
- <script lang="ts">
6
- import {
7
- setContext,
8
- createEventDispatcher,
9
- onMount,
10
- onDestroy,
11
- tick
12
- } from "svelte";
13
- import OverflowIcon from "./OverflowIcon.svelte";
14
- import { writable } from "svelte/store";
15
- import type { SelectData } from "@gradio/utils";
16
3
 
17
- interface Tab {
18
- name: string;
4
+ export interface Tab {
5
+ label: string;
19
6
  id: string | number;
20
7
  elem_id: string | undefined;
21
8
  visible: boolean;
22
9
  interactive: boolean;
23
10
  }
11
+ </script>
12
+
13
+ <script lang="ts">
14
+ import { setContext, createEventDispatcher, tick, onMount } from "svelte";
15
+ import OverflowIcon from "./OverflowIcon.svelte";
16
+ import { writable } from "svelte/store";
17
+ import type { SelectData } from "@gradio/utils";
24
18
 
25
19
  export let visible = true;
26
20
  export let elem_id = "";
27
21
  export let elem_classes: string[] = [];
28
22
  export let selected: number | string;
29
- export let inital_tabs: Tab[];
23
+ export let initial_tabs: Tab[];
30
24
 
31
- let tabs: Tab[] = inital_tabs;
25
+ let tabs: Tab[] = [...initial_tabs];
26
+ let visible_tabs: Tab[] = [...initial_tabs];
27
+ let overflow_tabs: Tab[] = [];
32
28
  let overflow_menu_open = false;
33
29
  let overflow_menu: HTMLElement;
34
30
 
35
31
  $: has_tabs = tabs.length > 0;
36
32
 
37
- let tab_nav_el: HTMLElement;
38
- let overflow_nav: HTMLElement;
33
+ let tab_nav_el: HTMLDivElement;
39
34
 
40
35
  const selected_tab = writable<false | number | string>(
41
36
  selected || tabs[0]?.id || false
@@ -50,6 +45,14 @@
50
45
 
51
46
  let is_overflowing = false;
52
47
  let overflow_has_selected_tab = false;
48
+ let tab_els: Record<string | number, HTMLElement> = {};
49
+
50
+ onMount(() => {
51
+ const observer = new IntersectionObserver((entries) => {
52
+ handle_menu_overflow();
53
+ });
54
+ observer.observe(tab_nav_el);
55
+ });
53
56
 
54
57
  setContext(TABS, {
55
58
  register_tab: (tab: Tab) => {
@@ -95,19 +98,7 @@
95
98
  }
96
99
 
97
100
  $: tabs, selected !== null && change_tab(selected);
98
-
99
- onMount(() => {
100
- handle_menu_overflow();
101
-
102
- window.addEventListener("resize", handle_menu_overflow);
103
- window.addEventListener("click", handle_outside_click);
104
- });
105
-
106
- onDestroy(() => {
107
- if (typeof window === "undefined") return;
108
- window.removeEventListener("resize", handle_menu_overflow);
109
- window.removeEventListener("click", handle_outside_click);
110
- });
101
+ $: tabs, tab_nav_el, tab_els, handle_menu_overflow();
111
102
 
112
103
  function handle_outside_click(event: MouseEvent): void {
113
104
  if (
@@ -119,42 +110,32 @@
119
110
  }
120
111
  }
121
112
 
122
- function handle_menu_overflow(): void {
123
- if (!tab_nav_el) {
124
- console.error("Menu elements not found");
125
- return;
126
- }
113
+ async function handle_menu_overflow(): Promise<void> {
114
+ if (!tab_nav_el) return;
127
115
 
128
- let all_items: HTMLElement[] = [];
116
+ await tick();
117
+ const tab_nav_size = tab_nav_el.getBoundingClientRect();
129
118
 
130
- [tab_nav_el, overflow_nav].forEach((menu) => {
131
- Array.from(menu.querySelectorAll("button")).forEach((item) =>
132
- all_items.push(item as HTMLElement)
133
- );
134
- });
119
+ let max_width = tab_nav_size.width;
120
+ const tab_sizes = get_tab_sizes(tabs, tab_els);
121
+ let last_visible_index = 0;
122
+ const offset = tab_nav_size.left;
135
123
 
136
- all_items.forEach((item) => tab_nav_el.appendChild(item));
137
-
138
- const nav_items: HTMLElement[] = [];
139
- const overflow_items: HTMLElement[] = [];
140
-
141
- Array.from(tab_nav_el.querySelectorAll("button")).forEach((item) => {
142
- const tab_rect = item.getBoundingClientRect();
143
- const tab_menu_rect = tab_nav_el.getBoundingClientRect();
144
- is_overflowing =
145
- tab_rect.right > tab_menu_rect.right ||
146
- tab_rect.left < tab_menu_rect.left;
147
-
148
- if (is_overflowing) {
149
- overflow_items.push(item as HTMLElement);
150
- } else {
151
- nav_items.push(item as HTMLElement);
124
+ for (let i = tabs.length - 1; i >= 0; i--) {
125
+ const tab = tabs[i];
126
+ const tab_rect = tab_sizes[tab.id];
127
+ if (!tab_rect) continue;
128
+ if (tab_rect.right - offset < max_width) {
129
+ last_visible_index = i;
130
+ break;
152
131
  }
153
- });
132
+ }
133
+
134
+ overflow_tabs = tabs.slice(last_visible_index + 1);
135
+ visible_tabs = tabs.slice(0, last_visible_index + 1);
154
136
 
155
- nav_items.forEach((item) => tab_nav_el.appendChild(item));
156
- overflow_items.forEach((item) => overflow_nav.appendChild(item));
157
137
  overflow_has_selected_tab = handle_overflow_has_selected_tab($selected_tab);
138
+ is_overflowing = overflow_tabs.length > 0;
158
139
  }
159
140
 
160
141
  $: overflow_has_selected_tab =
@@ -163,38 +144,61 @@
163
144
  function handle_overflow_has_selected_tab(
164
145
  selected_tab: number | string | false
165
146
  ): boolean {
166
- if (selected_tab === false || !overflow_nav) return false;
167
- return tabs.some(
168
- (t) =>
169
- t.id === selected_tab &&
170
- overflow_nav.contains(document.querySelector(`[data-tab-id="${t.id}"]`))
171
- );
147
+ if (selected_tab === false) return false;
148
+ return overflow_tabs.some((t) => t.id === selected_tab);
149
+ }
150
+
151
+ function get_tab_sizes(
152
+ tabs: Tab[],
153
+ tab_els: Record<string | number, HTMLElement>
154
+ ): Record<string | number, DOMRect> {
155
+ const tab_sizes: Record<string | number, DOMRect> = {};
156
+ tabs.forEach((tab) => {
157
+ tab_sizes[tab.id] = tab_els[tab.id]?.getBoundingClientRect();
158
+ });
159
+ return tab_sizes;
172
160
  }
173
161
  </script>
174
162
 
175
- {#if has_tabs}
176
- <div class="tabs {elem_classes.join(' ')}" class:hide={!visible} id={elem_id}>
163
+ <svelte:window
164
+ on:resize={handle_menu_overflow}
165
+ on:click={handle_outside_click}
166
+ />
167
+
168
+ <div class="tabs {elem_classes.join(' ')}" class:hide={!visible} id={elem_id}>
169
+ {#if has_tabs}
177
170
  <div class="tab-wrapper">
178
- <div class="tab-container" bind:this={tab_nav_el} role="tablist">
171
+ <div class="tab-container visually-hidden" aria-hidden="true">
179
172
  {#each tabs as t, i (t.id)}
180
- <button
181
- role="tab"
182
- class:selected={t.id === $selected_tab}
183
- aria-selected={t.id === $selected_tab}
184
- aria-controls={t.elem_id}
185
- disabled={!t.interactive}
186
- aria-disabled={!t.interactive}
187
- id={t.elem_id ? t.elem_id + "-button" : null}
188
- data-tab-id={t.id}
189
- on:click={() => {
190
- if (t.id !== $selected_tab) {
191
- change_tab(t.id);
192
- dispatch("select", { value: t.name, index: i });
193
- }
194
- }}
195
- >
196
- {t.name}
197
- </button>
173
+ {#if t.visible}
174
+ <button bind:this={tab_els[t.id]}>
175
+ {t.label}
176
+ </button>
177
+ {/if}
178
+ {/each}
179
+ </div>
180
+ <div class="tab-container" bind:this={tab_nav_el} role="tablist">
181
+ {#each visible_tabs as t, i (t.id)}
182
+ {#if t.visible}
183
+ <button
184
+ role="tab"
185
+ class:selected={t.id === $selected_tab}
186
+ aria-selected={t.id === $selected_tab}
187
+ aria-controls={t.elem_id}
188
+ disabled={!t.interactive}
189
+ aria-disabled={!t.interactive}
190
+ id={t.elem_id ? t.elem_id + "-button" : null}
191
+ data-tab-id={t.id}
192
+ on:click={() => {
193
+ if (t.id !== $selected_tab) {
194
+ change_tab(t.id);
195
+ dispatch("select", { value: t.label, index: i });
196
+ }
197
+ }}
198
+ >
199
+ {t.label}
200
+ </button>
201
+ {/if}
198
202
  {/each}
199
203
  </div>
200
204
  <span
@@ -209,21 +213,28 @@
209
213
  >
210
214
  <OverflowIcon />
211
215
  </button>
212
- <div
213
- class="overflow-dropdown"
214
- bind:this={overflow_nav}
215
- class:hide={!overflow_menu_open}
216
- />
216
+ <div class="overflow-dropdown" class:hide={!overflow_menu_open}>
217
+ {#each overflow_tabs as t}
218
+ <button
219
+ on:click={() => change_tab(t.id)}
220
+ class:selected={t.id === $selected_tab}
221
+ >
222
+ {t.label}
223
+ </button>
224
+ {/each}
225
+ </div>
217
226
  </span>
218
227
  </div>
219
- </div>
220
- {/if}
221
-
222
- <slot />
228
+ {/if}
229
+ <slot />
230
+ </div>
223
231
 
224
232
  <style>
225
233
  .tabs {
226
234
  position: relative;
235
+ display: flex;
236
+ flex-direction: column;
237
+ gap: var(--layout-gap);
227
238
  }
228
239
 
229
240
  .hide {
@@ -271,7 +282,7 @@
271
282
  color: var(--body-text-color);
272
283
  font-weight: var(--section-header-text-weight);
273
284
  font-size: var(--section-header-text-size);
274
- transition: all 0.2s ease-out;
285
+ transition: background-color color 0.2s ease-out;
275
286
  background-color: transparent;
276
287
  height: 100%;
277
288
  display: flex;
@@ -366,4 +377,16 @@
366
377
  .overflow-item-selected :global(svg) {
367
378
  color: var(--color-accent);
368
379
  }
380
+
381
+ .visually-hidden {
382
+ position: absolute;
383
+ width: 1px;
384
+ height: 1px;
385
+ padding: 0;
386
+ margin: -1px;
387
+ overflow: hidden;
388
+ clip: rect(0, 0, 0, 0);
389
+ white-space: nowrap;
390
+ border: 0;
391
+ }
369
392
  </style>