@gradio/tabs 0.3.0 → 0.3.2

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.2
4
+
5
+ ### Fixes
6
+
7
+ - [#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!
8
+ - [#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!
9
+
10
+ ## 0.3.1
11
+
12
+ ### Fixes
13
+
14
+ - [#9728](https://github.com/gradio-app/gradio/pull/9728) [`d0b2ce8`](https://github.com/gradio-app/gradio/commit/d0b2ce8c0f150f0b636ad7d2226f7c8c61401996) - Ensure tabs render in SSR mode and reduce time it takes for them to render. Thanks @pngwn!
15
+
3
16
  ## 0.3.0
4
17
 
5
18
  ### Features
package/Index.svelte CHANGED
@@ -1,11 +1,11 @@
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
 
@@ -13,10 +13,13 @@
13
13
  export let elem_id = "";
14
14
  export let elem_classes: string[] = [];
15
15
  export let selected: number | string;
16
- export let gradio: Gradio<{
17
- change: never;
18
- select: SelectData;
19
- }>;
16
+ export let initial_tabs: Tab[] = [];
17
+ export let gradio:
18
+ | Gradio<{
19
+ change: never;
20
+ select: SelectData;
21
+ }>
22
+ | undefined;
20
23
 
21
24
  $: dispatch("prop_change", { selected });
22
25
  </script>
@@ -26,8 +29,9 @@
26
29
  {elem_id}
27
30
  {elem_classes}
28
31
  bind:selected
29
- on:change={() => gradio.dispatch("change")}
30
- on:select={(e) => gradio.dispatch("select", e.detail)}
32
+ on:change={() => gradio?.dispatch("change")}
33
+ on:select={(e) => gradio?.dispatch("select", e.detail)}
34
+ {initial_tabs}
31
35
  >
32
36
  <slot />
33
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,12 +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 initial_tabs = [];
11
12
  export let gradio;
12
13
  $:
13
14
  dispatch("prop_change", { selected });
@@ -18,8 +19,9 @@ $:
18
19
  {elem_id}
19
20
  {elem_classes}
20
21
  bind:selected
21
- on:change={() => gradio.dispatch("change")}
22
- on:select={(e) => gradio.dispatch("select", e.detail)}
22
+ on:change={() => gradio?.dispatch("change")}
23
+ on:select={(e) => gradio?.dispatch("select", e.detail)}
24
+ {initial_tabs}
23
25
  >
24
26
  <slot />
25
27
  </Tabs>
@@ -1,16 +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;
11
+ initial_tabs?: Tab[] | undefined;
10
12
  gradio: Gradio<{
11
13
  change: never;
12
14
  select: SelectData;
13
- }>;
15
+ }> | undefined;
14
16
  };
15
17
  events: {
16
18
  prop_change: CustomEvent<any>;
@@ -1,31 +1,38 @@
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
- } from "svelte";
4
+ <script>import { setContext, createEventDispatcher, tick, onMount } from "svelte";
11
5
  import OverflowIcon from "./OverflowIcon.svelte";
12
6
  import { writable } from "svelte/store";
13
7
  export let visible = true;
14
8
  export let elem_id = "";
15
9
  export let elem_classes = [];
16
10
  export let selected;
17
- let tabs = [];
11
+ export let initial_tabs;
12
+ let tabs = [...initial_tabs];
13
+ let visible_tabs = [...initial_tabs];
14
+ let overflow_tabs = [];
18
15
  let overflow_menu_open = false;
19
16
  let overflow_menu;
20
17
  $:
21
18
  has_tabs = tabs.length > 0;
22
19
  let tab_nav_el;
23
- let overflow_nav;
24
- const selected_tab = writable(false);
25
- const selected_tab_index = writable(0);
20
+ const selected_tab = writable(
21
+ selected || tabs[0]?.id || false
22
+ );
23
+ const selected_tab_index = writable(
24
+ tabs.findIndex((t) => t.id === selected) || 0
25
+ );
26
26
  const dispatch = createEventDispatcher();
27
27
  let is_overflowing = false;
28
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
+ });
29
36
  setContext(TABS, {
30
37
  register_tab: (tab) => {
31
38
  let index = tabs.findIndex((t) => t.id === tab.id);
@@ -54,89 +61,102 @@ setContext(TABS, {
54
61
  });
55
62
  function change_tab(id) {
56
63
  const tab_to_activate = tabs.find((t) => t.id === id);
57
- if (tab_to_activate && tab_to_activate.interactive && tab_to_activate.visible) {
64
+ if (tab_to_activate && tab_to_activate.interactive && tab_to_activate.visible && $selected_tab !== tab_to_activate.id) {
58
65
  selected = id;
59
66
  $selected_tab = id;
60
67
  $selected_tab_index = tabs.findIndex((t) => t.id === id);
61
68
  dispatch("change");
62
69
  overflow_menu_open = false;
63
- } else {
64
- console.warn("Attempted to select a non-interactive or hidden tab.");
65
70
  }
66
71
  }
67
72
  $:
68
73
  tabs, selected !== null && change_tab(selected);
69
- onMount(() => {
70
- handle_menu_overflow();
71
- window.addEventListener("resize", handle_menu_overflow);
72
- window.addEventListener("click", handle_outside_click);
73
- });
74
- onDestroy(() => {
75
- if (typeof window === "undefined")
76
- return;
77
- window.removeEventListener("resize", handle_menu_overflow);
78
- window.removeEventListener("click", handle_outside_click);
79
- });
74
+ $:
75
+ tabs, tab_nav_el, tab_els, handle_menu_overflow();
80
76
  function handle_outside_click(event) {
81
77
  if (overflow_menu_open && overflow_menu && !overflow_menu.contains(event.target)) {
82
78
  overflow_menu_open = false;
83
79
  }
84
80
  }
85
- function handle_menu_overflow() {
86
- if (!tab_nav_el) {
87
- console.error("Menu elements not found");
81
+ async function handle_menu_overflow() {
82
+ if (!tab_nav_el)
88
83
  return;
89
- }
90
- let all_items = [];
91
- [tab_nav_el, overflow_nav].forEach((menu) => {
92
- Array.from(menu.querySelectorAll("button")).forEach(
93
- (item) => all_items.push(item)
94
- );
95
- });
96
- all_items.forEach((item) => tab_nav_el.appendChild(item));
97
- const nav_items = [];
98
- const overflow_items = [];
99
- Array.from(tab_nav_el.querySelectorAll("button")).forEach((item) => {
100
- const tab_rect = item.getBoundingClientRect();
101
- const tab_menu_rect = tab_nav_el.getBoundingClientRect();
102
- is_overflowing = tab_rect.right > tab_menu_rect.right || tab_rect.left < tab_menu_rect.left;
103
- if (is_overflowing) {
104
- overflow_items.push(item);
105
- } else {
106
- 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;
107
98
  }
99
+ }
100
+ overflow_tabs = tabs.slice(last_visible_index + 1);
101
+ visible_tabs = tabs.slice(0, last_visible_index + 1);
102
+ overflow_has_selected_tab = handle_overflow_has_selected_tab($selected_tab);
103
+ is_overflowing = overflow_tabs.length > 0;
104
+ }
105
+ $:
106
+ overflow_has_selected_tab = handle_overflow_has_selected_tab($selected_tab);
107
+ function handle_overflow_has_selected_tab(selected_tab2) {
108
+ if (selected_tab2 === false)
109
+ return false;
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();
108
116
  });
109
- nav_items.forEach((item) => tab_nav_el.appendChild(item));
110
- overflow_items.forEach((item) => overflow_nav.appendChild(item));
111
- overflow_has_selected_tab = tabs.some(
112
- (t) => t.id === $selected_tab && overflow_nav.contains(document.querySelector(`[data-tab-id="${t.id}"]`))
113
- );
117
+ return tab_sizes;
114
118
  }
115
119
  </script>
116
120
 
121
+ <svelte:window
122
+ on:resize={handle_menu_overflow}
123
+ on:click={handle_outside_click}
124
+ />
125
+
117
126
  {#if has_tabs}
118
127
  <div class="tabs {elem_classes.join(' ')}" class:hide={!visible} id={elem_id}>
119
128
  <div class="tab-wrapper">
120
- <div class="tab-container" bind:this={tab_nav_el} role="tablist">
129
+ <div class="tab-container visually-hidden" aria-hidden="true">
121
130
  {#each tabs as t, i (t.id)}
122
- <button
123
- role="tab"
124
- class:selected={t.id === $selected_tab}
125
- aria-selected={t.id === $selected_tab}
126
- aria-controls={t.elem_id}
127
- disabled={!t.interactive}
128
- aria-disabled={!t.interactive}
129
- id={t.elem_id ? t.elem_id + "-button" : null}
130
- data-tab-id={t.id}
131
- on:click={() => {
132
- if (t.id !== $selected_tab) {
133
- change_tab(t.id);
134
- dispatch("select", { value: t.name, index: i });
135
- }
136
- }}
137
- >
138
- {t.name}
139
- </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}
140
160
  {/each}
141
161
  </div>
142
162
  <span
@@ -151,11 +171,16 @@ function handle_menu_overflow() {
151
171
  >
152
172
  <OverflowIcon />
153
173
  </button>
154
- <div
155
- class="overflow-dropdown"
156
- bind:this={overflow_nav}
157
- class:hide={!overflow_menu_open}
158
- />
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>
159
184
  </span>
160
185
  </div>
161
186
  </div>
@@ -213,7 +238,7 @@ function handle_menu_overflow() {
213
238
  color: var(--body-text-color);
214
239
  font-weight: var(--section-header-text-weight);
215
240
  font-size: var(--section-header-text-size);
216
- transition: all 0.2s ease-out;
241
+ transition: background-color color 0.2s ease-out;
217
242
  background-color: transparent;
218
243
  height: 100%;
219
244
  display: flex;
@@ -308,4 +333,16 @@ function handle_menu_overflow() {
308
333
  .overflow-item-selected :global(svg) {
309
334
  color: var(--color-accent);
310
335
  }
336
+
337
+ .visually-hidden {
338
+ position: absolute;
339
+ width: 1px;
340
+ height: 1px;
341
+ padding: 0;
342
+ margin: -1px;
343
+ overflow: hidden;
344
+ clip: rect(0, 0, 0, 0);
345
+ white-space: nowrap;
346
+ border: 0;
347
+ }
311
348
  </style>
@@ -1,12 +1,20 @@
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: {
6
13
  visible?: boolean | undefined;
7
14
  elem_id?: string | undefined;
8
15
  elem_classes?: string[] | undefined;
9
- selected: number | string | object;
16
+ selected: number | string;
17
+ initial_tabs: Tab[];
10
18
  };
11
19
  events: {
12
20
  change: CustomEvent<undefined>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/tabs",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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.12.1"
24
24
  },
25
25
  "peerDependencies": {
26
26
  "svelte": "^4.0.0"
@@ -1,42 +1,43 @@
1
- <script context="module">
1
+ <script context="module" lang="ts">
2
2
  export const TABS = {};
3
+
4
+ export interface Tab {
5
+ label: string;
6
+ id: string | number;
7
+ elem_id: string | undefined;
8
+ visible: boolean;
9
+ interactive: boolean;
10
+ }
3
11
  </script>
4
12
 
5
13
  <script lang="ts">
6
- import {
7
- setContext,
8
- createEventDispatcher,
9
- onMount,
10
- onDestroy
11
- } from "svelte";
14
+ import { setContext, createEventDispatcher, tick, onMount } from "svelte";
12
15
  import OverflowIcon from "./OverflowIcon.svelte";
13
16
  import { writable } from "svelte/store";
14
17
  import type { SelectData } from "@gradio/utils";
15
18
 
16
- interface Tab {
17
- name: string;
18
- id: object;
19
- elem_id: string | undefined;
20
- visible: boolean;
21
- interactive: boolean;
22
- }
23
-
24
19
  export let visible = true;
25
20
  export let elem_id = "";
26
21
  export let elem_classes: string[] = [];
27
- export let selected: number | string | object;
22
+ export let selected: number | string;
23
+ export let initial_tabs: Tab[];
28
24
 
29
- let tabs: Tab[] = [];
25
+ let tabs: Tab[] = [...initial_tabs];
26
+ let visible_tabs: Tab[] = [...initial_tabs];
27
+ let overflow_tabs: Tab[] = [];
30
28
  let overflow_menu_open = false;
31
29
  let overflow_menu: HTMLElement;
32
30
 
33
31
  $: has_tabs = tabs.length > 0;
34
32
 
35
- let tab_nav_el: HTMLElement;
36
- let overflow_nav: HTMLElement;
33
+ let tab_nav_el: HTMLDivElement;
37
34
 
38
- const selected_tab = writable<false | object | number | string>(false);
39
- const selected_tab_index = writable<number>(0);
35
+ const selected_tab = writable<false | number | string>(
36
+ selected || tabs[0]?.id || false
37
+ );
38
+ const selected_tab_index = writable<number>(
39
+ tabs.findIndex((t) => t.id === selected) || 0
40
+ );
40
41
  const dispatch = createEventDispatcher<{
41
42
  change: undefined;
42
43
  select: SelectData;
@@ -44,6 +45,14 @@
44
45
 
45
46
  let is_overflowing = false;
46
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
+ });
47
56
 
48
57
  setContext(TABS, {
49
58
  register_tab: (tab: Tab) => {
@@ -72,37 +81,24 @@
72
81
  selected_tab_index
73
82
  });
74
83
 
75
- function change_tab(id: object | string | number): void {
84
+ function change_tab(id: string | number): void {
76
85
  const tab_to_activate = tabs.find((t) => t.id === id);
77
86
  if (
78
87
  tab_to_activate &&
79
88
  tab_to_activate.interactive &&
80
- tab_to_activate.visible
89
+ tab_to_activate.visible &&
90
+ $selected_tab !== tab_to_activate.id
81
91
  ) {
82
92
  selected = id;
83
93
  $selected_tab = id;
84
94
  $selected_tab_index = tabs.findIndex((t) => t.id === id);
85
95
  dispatch("change");
86
96
  overflow_menu_open = false;
87
- } else {
88
- console.warn("Attempted to select a non-interactive or hidden tab.");
89
97
  }
90
98
  }
91
99
 
92
100
  $: tabs, selected !== null && change_tab(selected);
93
-
94
- onMount(() => {
95
- handle_menu_overflow();
96
-
97
- window.addEventListener("resize", handle_menu_overflow);
98
- window.addEventListener("click", handle_outside_click);
99
- });
100
-
101
- onDestroy(() => {
102
- if (typeof window === "undefined") return;
103
- window.removeEventListener("resize", handle_menu_overflow);
104
- window.removeEventListener("click", handle_outside_click);
105
- });
101
+ $: tabs, tab_nav_el, tab_els, handle_menu_overflow();
106
102
 
107
103
  function handle_outside_click(event: MouseEvent): void {
108
104
  if (
@@ -114,73 +110,95 @@
114
110
  }
115
111
  }
116
112
 
117
- function handle_menu_overflow(): void {
118
- if (!tab_nav_el) {
119
- console.error("Menu elements not found");
120
- return;
121
- }
113
+ async function handle_menu_overflow(): Promise<void> {
114
+ if (!tab_nav_el) return;
122
115
 
123
- let all_items: HTMLElement[] = [];
116
+ await tick();
117
+ const tab_nav_size = tab_nav_el.getBoundingClientRect();
124
118
 
125
- [tab_nav_el, overflow_nav].forEach((menu) => {
126
- Array.from(menu.querySelectorAll("button")).forEach((item) =>
127
- all_items.push(item as HTMLElement)
128
- );
129
- });
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;
130
123
 
131
- all_items.forEach((item) => tab_nav_el.appendChild(item));
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;
131
+ }
132
+ }
132
133
 
133
- const nav_items: HTMLElement[] = [];
134
- const overflow_items: HTMLElement[] = [];
134
+ overflow_tabs = tabs.slice(last_visible_index + 1);
135
+ visible_tabs = tabs.slice(0, last_visible_index + 1);
135
136
 
136
- Array.from(tab_nav_el.querySelectorAll("button")).forEach((item) => {
137
- const tab_rect = item.getBoundingClientRect();
138
- const tab_menu_rect = tab_nav_el.getBoundingClientRect();
139
- is_overflowing =
140
- tab_rect.right > tab_menu_rect.right ||
141
- tab_rect.left < tab_menu_rect.left;
137
+ overflow_has_selected_tab = handle_overflow_has_selected_tab($selected_tab);
138
+ is_overflowing = overflow_tabs.length > 0;
139
+ }
142
140
 
143
- if (is_overflowing) {
144
- overflow_items.push(item as HTMLElement);
145
- } else {
146
- nav_items.push(item as HTMLElement);
147
- }
148
- });
141
+ $: overflow_has_selected_tab =
142
+ handle_overflow_has_selected_tab($selected_tab);
149
143
 
150
- nav_items.forEach((item) => tab_nav_el.appendChild(item));
151
- overflow_items.forEach((item) => overflow_nav.appendChild(item));
144
+ function handle_overflow_has_selected_tab(
145
+ selected_tab: number | string | false
146
+ ): boolean {
147
+ if (selected_tab === false) return false;
148
+ return overflow_tabs.some((t) => t.id === selected_tab);
149
+ }
152
150
 
153
- overflow_has_selected_tab = tabs.some(
154
- (t) =>
155
- t.id === $selected_tab &&
156
- overflow_nav.contains(document.querySelector(`[data-tab-id="${t.id}"]`))
157
- );
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;
158
160
  }
159
161
  </script>
160
162
 
163
+ <svelte:window
164
+ on:resize={handle_menu_overflow}
165
+ on:click={handle_outside_click}
166
+ />
167
+
161
168
  {#if has_tabs}
162
169
  <div class="tabs {elem_classes.join(' ')}" class:hide={!visible} id={elem_id}>
163
170
  <div class="tab-wrapper">
164
- <div class="tab-container" bind:this={tab_nav_el} role="tablist">
171
+ <div class="tab-container visually-hidden" aria-hidden="true">
165
172
  {#each tabs as t, i (t.id)}
166
- <button
167
- role="tab"
168
- class:selected={t.id === $selected_tab}
169
- aria-selected={t.id === $selected_tab}
170
- aria-controls={t.elem_id}
171
- disabled={!t.interactive}
172
- aria-disabled={!t.interactive}
173
- id={t.elem_id ? t.elem_id + "-button" : null}
174
- data-tab-id={t.id}
175
- on:click={() => {
176
- if (t.id !== $selected_tab) {
177
- change_tab(t.id);
178
- dispatch("select", { value: t.name, index: i });
179
- }
180
- }}
181
- >
182
- {t.name}
183
- </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}
184
202
  {/each}
185
203
  </div>
186
204
  <span
@@ -195,11 +213,16 @@
195
213
  >
196
214
  <OverflowIcon />
197
215
  </button>
198
- <div
199
- class="overflow-dropdown"
200
- bind:this={overflow_nav}
201
- class:hide={!overflow_menu_open}
202
- />
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>
203
226
  </span>
204
227
  </div>
205
228
  </div>
@@ -257,7 +280,7 @@
257
280
  color: var(--body-text-color);
258
281
  font-weight: var(--section-header-text-weight);
259
282
  font-size: var(--section-header-text-size);
260
- transition: all 0.2s ease-out;
283
+ transition: background-color color 0.2s ease-out;
261
284
  background-color: transparent;
262
285
  height: 100%;
263
286
  display: flex;
@@ -352,4 +375,16 @@
352
375
  .overflow-item-selected :global(svg) {
353
376
  color: var(--color-accent);
354
377
  }
378
+
379
+ .visually-hidden {
380
+ position: absolute;
381
+ width: 1px;
382
+ height: 1px;
383
+ padding: 0;
384
+ margin: -1px;
385
+ overflow: hidden;
386
+ clip: rect(0, 0, 0, 0);
387
+ white-space: nowrap;
388
+ border: 0;
389
+ }
355
390
  </style>