@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 +13 -0
- package/Index.svelte +12 -18
- package/Tabs.stories.svelte +48 -0
- package/dist/Index.svelte +5 -5
- package/dist/Index.svelte.d.ts +4 -9
- package/dist/shared/Tabs.svelte +110 -82
- package/dist/shared/Tabs.svelte.d.ts +8 -7
- package/package.json +2 -2
- package/shared/Tabs.svelte +123 -100
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
|
|
25
|
-
export let gradio:
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
39
|
-
on:select={(e) => gradio
|
|
40
|
-
{
|
|
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
|
|
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
|
|
23
|
-
on:select={(e) => gradio
|
|
24
|
-
{
|
|
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>
|
package/dist/Index.svelte.d.ts
CHANGED
|
@@ -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
|
-
|
|
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>;
|
package/dist/shared/Tabs.svelte
CHANGED
|
@@ -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
|
|
19
|
-
let 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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
|
108
|
+
if (selected_tab2 === false)
|
|
121
109
|
return false;
|
|
122
|
-
return
|
|
123
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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"
|
|
129
|
+
<div class="tab-container visually-hidden" aria-hidden="true">
|
|
132
130
|
{#each tabs as t, i (t.id)}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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.
|
|
23
|
+
"@gradio/preview": "^0.13.0"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"svelte": "^4.0.0"
|
package/shared/Tabs.svelte
CHANGED
|
@@ -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
|
-
|
|
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
|
|
23
|
+
export let initial_tabs: Tab[];
|
|
30
24
|
|
|
31
|
-
let tabs: Tab[] =
|
|
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:
|
|
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
|
-
|
|
116
|
+
await tick();
|
|
117
|
+
const tab_nav_size = tab_nav_el.getBoundingClientRect();
|
|
129
118
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
167
|
-
return
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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"
|
|
171
|
+
<div class="tab-container visually-hidden" aria-hidden="true">
|
|
179
172
|
{#each tabs as t, i (t.id)}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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:
|
|
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>
|