@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 +13 -0
- package/Index.svelte +12 -8
- package/Tabs.stories.svelte +48 -0
- package/dist/Index.svelte +5 -3
- package/dist/Index.svelte.d.ts +4 -2
- package/dist/shared/Tabs.svelte +114 -77
- package/dist/shared/Tabs.svelte.d.ts +9 -1
- package/package.json +2 -2
- package/shared/Tabs.svelte +131 -96
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
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
30
|
-
on:select={(e) => gradio
|
|
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
|
|
22
|
-
on:select={(e) => gradio
|
|
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>
|
package/dist/Index.svelte.d.ts
CHANGED
|
@@ -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>;
|
package/dist/shared/Tabs.svelte
CHANGED
|
@@ -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
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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"
|
|
129
|
+
<div class="tab-container visually-hidden" aria-hidden="true">
|
|
121
130
|
{#each tabs as t, i (t.id)}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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:
|
|
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
|
|
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.
|
|
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.
|
|
23
|
+
"@gradio/preview": "^0.12.1"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"svelte": "^4.0.0"
|
package/shared/Tabs.svelte
CHANGED
|
@@ -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
|
|
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:
|
|
36
|
-
let overflow_nav: HTMLElement;
|
|
33
|
+
let tab_nav_el: HTMLDivElement;
|
|
37
34
|
|
|
38
|
-
const selected_tab = writable<false |
|
|
39
|
-
|
|
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:
|
|
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
|
-
|
|
116
|
+
await tick();
|
|
117
|
+
const tab_nav_size = tab_nav_el.getBoundingClientRect();
|
|
124
118
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
134
|
+
overflow_tabs = tabs.slice(last_visible_index + 1);
|
|
135
|
+
visible_tabs = tabs.slice(0, last_visible_index + 1);
|
|
135
136
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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"
|
|
171
|
+
<div class="tab-container visually-hidden" aria-hidden="true">
|
|
165
172
|
{#each tabs as t, i (t.id)}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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:
|
|
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>
|