@gradio/tabs 0.3.1 → 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 +7 -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 +102 -76
- package/dist/shared/Tabs.svelte.d.ts +8 -7
- package/package.json +2 -2
- package/shared/Tabs.svelte +115 -94
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
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
|
+
|
|
3
10
|
## 0.3.1
|
|
4
11
|
|
|
5
12
|
### 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
|
|
|
121
|
+
<svelte:window
|
|
122
|
+
on:resize={handle_menu_overflow}
|
|
123
|
+
on:click={handle_outside_click}
|
|
124
|
+
/>
|
|
125
|
+
|
|
128
126
|
{#if has_tabs}
|
|
129
127
|
<div class="tabs {elem_classes.join(' ')}" class:hide={!visible} id={elem_id}>
|
|
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,11 +171,16 @@ 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
186
|
</div>
|
|
@@ -224,7 +238,7 @@ function handle_overflow_has_selected_tab(selected_tab2) {
|
|
|
224
238
|
color: var(--body-text-color);
|
|
225
239
|
font-weight: var(--section-header-text-weight);
|
|
226
240
|
font-size: var(--section-header-text-size);
|
|
227
|
-
transition:
|
|
241
|
+
transition: background-color color 0.2s ease-out;
|
|
228
242
|
background-color: transparent;
|
|
229
243
|
height: 100%;
|
|
230
244
|
display: flex;
|
|
@@ -319,4 +333,16 @@ function handle_overflow_has_selected_tab(selected_tab2) {
|
|
|
319
333
|
.overflow-item-selected :global(svg) {
|
|
320
334
|
color: var(--color-accent);
|
|
321
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
|
+
}
|
|
322
348
|
</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.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,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
|
-
});
|
|
135
|
-
|
|
136
|
-
all_items.forEach((item) => tab_nav_el.appendChild(item));
|
|
137
|
-
|
|
138
|
-
const nav_items: HTMLElement[] = [];
|
|
139
|
-
const overflow_items: HTMLElement[] = [];
|
|
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;
|
|
140
123
|
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
|
163
|
+
<svelte:window
|
|
164
|
+
on:resize={handle_menu_overflow}
|
|
165
|
+
on:click={handle_outside_click}
|
|
166
|
+
/>
|
|
167
|
+
|
|
175
168
|
{#if has_tabs}
|
|
176
169
|
<div class="tabs {elem_classes.join(' ')}" class:hide={!visible} id={elem_id}>
|
|
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,11 +213,16 @@
|
|
|
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
228
|
</div>
|
|
@@ -271,7 +280,7 @@
|
|
|
271
280
|
color: var(--body-text-color);
|
|
272
281
|
font-weight: var(--section-header-text-weight);
|
|
273
282
|
font-size: var(--section-header-text-size);
|
|
274
|
-
transition:
|
|
283
|
+
transition: background-color color 0.2s ease-out;
|
|
275
284
|
background-color: transparent;
|
|
276
285
|
height: 100%;
|
|
277
286
|
display: flex;
|
|
@@ -366,4 +375,16 @@
|
|
|
366
375
|
.overflow-item-selected :global(svg) {
|
|
367
376
|
color: var(--color-accent);
|
|
368
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
|
+
}
|
|
369
390
|
</style>
|