@gradio/tabs 0.2.14 → 0.3.0-beta.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 +20 -0
- package/dist/shared/OverflowIcon.svelte +11 -0
- package/dist/shared/OverflowIcon.svelte.d.ts +23 -0
- package/dist/shared/Tabs.svelte +222 -67
- package/package.json +2 -2
- package/shared/OverflowIcon.svelte +11 -0
- package/shared/Tabs.svelte +244 -69
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @gradio/tabs
|
|
2
2
|
|
|
3
|
+
## 0.3.0-beta.2
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- [#9339](https://github.com/gradio-app/gradio/pull/9339) [`4c8c6f2`](https://github.com/gradio-app/gradio/commit/4c8c6f2fe603081941c5fdc43f48a0632b9f31ad) - Ssr part 2. Thanks @pngwn!
|
|
8
|
+
|
|
9
|
+
### Dependency updates
|
|
10
|
+
|
|
11
|
+
- @gradio/utils@0.7.0-beta.2
|
|
12
|
+
|
|
13
|
+
## 0.3.0-beta.1
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
- [#9199](https://github.com/gradio-app/gradio/pull/9199) [`3175c7a`](https://github.com/gradio-app/gradio/commit/3175c7aebc6ad2466d31d6949580f5a3cb4cd698) - Redesign `gr.Tabs()`. Thanks @hannahblair!
|
|
18
|
+
|
|
19
|
+
### Dependency updates
|
|
20
|
+
|
|
21
|
+
- @gradio/utils@0.7.0-beta.1
|
|
22
|
+
|
|
3
23
|
## 0.2.14
|
|
4
24
|
|
|
5
25
|
### Fixes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg
|
|
2
|
+
width="16"
|
|
3
|
+
height="16"
|
|
4
|
+
viewBox="0 0 16 16"
|
|
5
|
+
fill="none"
|
|
6
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
7
|
+
>
|
|
8
|
+
<circle cx="2.5" cy="8" r="1.5" fill="currentColor" />
|
|
9
|
+
<circle cx="8" cy="8" r="1.5" fill="currentColor" />
|
|
10
|
+
<circle cx="13.5" cy="8" r="1.5" fill="currentColor" />
|
|
11
|
+
</svg>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** @typedef {typeof __propDef.props} OverflowIconProps */
|
|
2
|
+
/** @typedef {typeof __propDef.events} OverflowIconEvents */
|
|
3
|
+
/** @typedef {typeof __propDef.slots} OverflowIconSlots */
|
|
4
|
+
export default class OverflowIcon extends SvelteComponent<{
|
|
5
|
+
[x: string]: never;
|
|
6
|
+
}, {
|
|
7
|
+
[evt: string]: CustomEvent<any>;
|
|
8
|
+
}, {}> {
|
|
9
|
+
}
|
|
10
|
+
export type OverflowIconProps = typeof __propDef.props;
|
|
11
|
+
export type OverflowIconEvents = typeof __propDef.events;
|
|
12
|
+
export type OverflowIconSlots = typeof __propDef.slots;
|
|
13
|
+
import { SvelteComponent } from "svelte";
|
|
14
|
+
declare const __propDef: {
|
|
15
|
+
props: {
|
|
16
|
+
[x: string]: never;
|
|
17
|
+
};
|
|
18
|
+
events: {
|
|
19
|
+
[evt: string]: CustomEvent<any>;
|
|
20
|
+
};
|
|
21
|
+
slots: {};
|
|
22
|
+
};
|
|
23
|
+
export {};
|
package/dist/shared/Tabs.svelte
CHANGED
|
@@ -2,49 +2,52 @@
|
|
|
2
2
|
export const TABS = {};
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
|
-
<script>import {
|
|
5
|
+
<script>import {
|
|
6
|
+
setContext,
|
|
7
|
+
createEventDispatcher,
|
|
8
|
+
onMount,
|
|
9
|
+
onDestroy
|
|
10
|
+
} from "svelte";
|
|
11
|
+
import OverflowIcon from "./OverflowIcon.svelte";
|
|
6
12
|
import { writable } from "svelte/store";
|
|
7
13
|
export let visible = true;
|
|
8
14
|
export let elem_id = "id";
|
|
9
15
|
export let elem_classes = [];
|
|
10
16
|
export let selected;
|
|
11
17
|
let tabs = [];
|
|
18
|
+
let overflow_menu_open = false;
|
|
19
|
+
let overflow_menu;
|
|
20
|
+
$:
|
|
21
|
+
has_tabs = tabs.length > 0;
|
|
22
|
+
let tab_nav_el;
|
|
23
|
+
let overflow_nav;
|
|
12
24
|
const selected_tab = writable(false);
|
|
13
25
|
const selected_tab_index = writable(0);
|
|
14
26
|
const dispatch = createEventDispatcher();
|
|
27
|
+
let is_overflowing = false;
|
|
28
|
+
let overflow_has_selected_tab = false;
|
|
15
29
|
setContext(TABS, {
|
|
16
30
|
register_tab: (tab) => {
|
|
17
|
-
let index;
|
|
18
|
-
|
|
19
|
-
if (existingTab) {
|
|
20
|
-
index = tabs.findIndex((t) => t.id === tab.id);
|
|
31
|
+
let index = tabs.findIndex((t) => t.id === tab.id);
|
|
32
|
+
if (index !== -1) {
|
|
21
33
|
tabs[index] = { ...tabs[index], ...tab };
|
|
22
34
|
} else {
|
|
23
|
-
tabs
|
|
24
|
-
name: tab.name,
|
|
25
|
-
id: tab.id,
|
|
26
|
-
elem_id: tab.elem_id,
|
|
27
|
-
visible: tab.visible,
|
|
28
|
-
interactive: tab.interactive
|
|
29
|
-
});
|
|
35
|
+
tabs = [...tabs, tab];
|
|
30
36
|
index = tabs.length - 1;
|
|
31
37
|
}
|
|
32
|
-
selected_tab.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
let nextTab = tabs.find((t) => t.visible && t.interactive);
|
|
37
|
-
return nextTab ? nextTab.id : current;
|
|
38
|
-
});
|
|
39
|
-
tabs = tabs;
|
|
38
|
+
if ($selected_tab === false && tab.visible && tab.interactive) {
|
|
39
|
+
$selected_tab = tab.id;
|
|
40
|
+
}
|
|
40
41
|
return index;
|
|
41
42
|
},
|
|
42
43
|
unregister_tab: (tab) => {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
(
|
|
47
|
-
|
|
44
|
+
const index = tabs.findIndex((t) => t.id === tab.id);
|
|
45
|
+
if (index !== -1) {
|
|
46
|
+
tabs = tabs.filter((t) => t.id !== tab.id);
|
|
47
|
+
if ($selected_tab === tab.id) {
|
|
48
|
+
$selected_tab = tabs[0]?.id || false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
48
51
|
},
|
|
49
52
|
selected_tab,
|
|
50
53
|
selected_tab_index
|
|
@@ -56,47 +59,109 @@ function change_tab(id) {
|
|
|
56
59
|
$selected_tab = id;
|
|
57
60
|
$selected_tab_index = tabs.findIndex((t) => t.id === id);
|
|
58
61
|
dispatch("change");
|
|
62
|
+
overflow_menu_open = false;
|
|
59
63
|
} else {
|
|
60
64
|
console.warn("Attempted to select a non-interactive or hidden tab.");
|
|
61
65
|
}
|
|
62
66
|
}
|
|
63
67
|
$:
|
|
64
68
|
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
|
+
});
|
|
80
|
+
function handle_outside_click(event) {
|
|
81
|
+
if (overflow_menu_open && overflow_menu && !overflow_menu.contains(event.target)) {
|
|
82
|
+
overflow_menu_open = false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function handle_menu_overflow() {
|
|
86
|
+
if (!tab_nav_el) {
|
|
87
|
+
console.error("Menu elements not found");
|
|
88
|
+
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);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
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
|
+
);
|
|
114
|
+
}
|
|
65
115
|
</script>
|
|
66
116
|
|
|
67
|
-
|
|
68
|
-
<div class="
|
|
69
|
-
|
|
70
|
-
{
|
|
71
|
-
{#
|
|
117
|
+
{#if has_tabs}
|
|
118
|
+
<div class="tabs {elem_classes.join(' ')}" class:hide={!visible} id={elem_id}>
|
|
119
|
+
<div class="tab-wrapper">
|
|
120
|
+
<div class="tab-container" bind:this={tab_nav_el} role="tablist">
|
|
121
|
+
{#each tabs as t, i (t.id)}
|
|
72
122
|
<button
|
|
73
123
|
role="tab"
|
|
74
|
-
class=
|
|
75
|
-
aria-selected={
|
|
76
|
-
aria-controls={t.elem_id}
|
|
77
|
-
id={t.elem_id ? t.elem_id + "-button" : null}
|
|
78
|
-
>
|
|
79
|
-
{t.name}
|
|
80
|
-
</button>
|
|
81
|
-
{:else}
|
|
82
|
-
<button
|
|
83
|
-
role="tab"
|
|
84
|
-
aria-selected={false}
|
|
124
|
+
class:selected={t.id === $selected_tab}
|
|
125
|
+
aria-selected={t.id === $selected_tab}
|
|
85
126
|
aria-controls={t.elem_id}
|
|
86
127
|
disabled={!t.interactive}
|
|
87
128
|
aria-disabled={!t.interactive}
|
|
88
129
|
id={t.elem_id ? t.elem_id + "-button" : null}
|
|
130
|
+
data-tab-id={t.id}
|
|
89
131
|
on:click={() => {
|
|
90
|
-
|
|
91
|
-
|
|
132
|
+
if (t.id !== $selected_tab) {
|
|
133
|
+
change_tab(t.id);
|
|
134
|
+
dispatch("select", { value: t.name, index: i });
|
|
135
|
+
}
|
|
92
136
|
}}
|
|
93
137
|
>
|
|
94
138
|
{t.name}
|
|
95
139
|
</button>
|
|
96
|
-
{/
|
|
97
|
-
|
|
98
|
-
|
|
140
|
+
{/each}
|
|
141
|
+
</div>
|
|
142
|
+
<span
|
|
143
|
+
class="overflow-menu"
|
|
144
|
+
class:hide={!is_overflowing}
|
|
145
|
+
bind:this={overflow_menu}
|
|
146
|
+
>
|
|
147
|
+
<button
|
|
148
|
+
on:click|stopPropagation={() =>
|
|
149
|
+
(overflow_menu_open = !overflow_menu_open)}
|
|
150
|
+
class:overflow-item-selected={overflow_has_selected_tab}
|
|
151
|
+
>
|
|
152
|
+
<OverflowIcon />
|
|
153
|
+
</button>
|
|
154
|
+
<div
|
|
155
|
+
class="overflow-dropdown"
|
|
156
|
+
bind:this={overflow_nav}
|
|
157
|
+
class:hide={!overflow_menu_open}
|
|
158
|
+
/>
|
|
159
|
+
</span>
|
|
160
|
+
</div>
|
|
99
161
|
</div>
|
|
162
|
+
{/if}
|
|
163
|
+
|
|
164
|
+
<div>
|
|
100
165
|
<slot />
|
|
101
166
|
</div>
|
|
102
167
|
|
|
@@ -109,50 +174,140 @@ $:
|
|
|
109
174
|
display: none;
|
|
110
175
|
}
|
|
111
176
|
|
|
112
|
-
.tab-
|
|
177
|
+
.tab-wrapper {
|
|
113
178
|
display: flex;
|
|
179
|
+
align-items: center;
|
|
180
|
+
justify-content: space-between;
|
|
114
181
|
position: relative;
|
|
115
|
-
|
|
116
|
-
|
|
182
|
+
height: var(--size-8);
|
|
183
|
+
padding-bottom: var(--size-2);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.tab-container {
|
|
187
|
+
display: flex;
|
|
188
|
+
align-items: center;
|
|
189
|
+
width: 100%;
|
|
190
|
+
position: relative;
|
|
191
|
+
overflow: hidden;
|
|
192
|
+
height: var(--size-8);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.tab-container::after {
|
|
196
|
+
content: "";
|
|
197
|
+
position: absolute;
|
|
198
|
+
bottom: 0;
|
|
199
|
+
left: 0;
|
|
200
|
+
right: 0;
|
|
201
|
+
height: 1px;
|
|
202
|
+
background-color: var(--border-color-primary);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.overflow-menu {
|
|
206
|
+
flex-shrink: 0;
|
|
207
|
+
margin-left: var(--size-2);
|
|
117
208
|
}
|
|
118
209
|
|
|
119
210
|
button {
|
|
120
|
-
margin-bottom:
|
|
121
|
-
border:
|
|
122
|
-
border-
|
|
123
|
-
|
|
124
|
-
border-top-right-radius: var(--container-radius);
|
|
125
|
-
border-top-left-radius: var(--container-radius);
|
|
126
|
-
padding: var(--size-1) var(--size-4);
|
|
211
|
+
margin-bottom: 0;
|
|
212
|
+
border: none;
|
|
213
|
+
border-radius: 0;
|
|
214
|
+
padding: 0 var(--size-4);
|
|
127
215
|
color: var(--body-text-color-subdued);
|
|
128
216
|
font-weight: var(--section-header-text-weight);
|
|
129
217
|
font-size: var(--section-header-text-size);
|
|
218
|
+
transition: all 0.2s ease-out;
|
|
219
|
+
background-color: transparent;
|
|
220
|
+
height: 100%;
|
|
221
|
+
display: flex;
|
|
222
|
+
align-items: center;
|
|
223
|
+
white-space: nowrap;
|
|
224
|
+
position: relative;
|
|
130
225
|
}
|
|
131
226
|
|
|
132
227
|
button:disabled {
|
|
133
|
-
color: var(--body-text-color-subdued);
|
|
134
228
|
opacity: 0.5;
|
|
135
229
|
cursor: not-allowed;
|
|
136
230
|
}
|
|
137
231
|
|
|
138
|
-
button:hover {
|
|
232
|
+
button:hover:not(:disabled):not(.selected) {
|
|
233
|
+
background-color: var(--background-fill-secondary);
|
|
139
234
|
color: var(--body-text-color);
|
|
140
235
|
}
|
|
236
|
+
|
|
141
237
|
.selected {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
238
|
+
background-color: transparent;
|
|
239
|
+
color: var(--color-accent);
|
|
240
|
+
position: relative;
|
|
145
241
|
}
|
|
146
242
|
|
|
147
|
-
.
|
|
148
|
-
|
|
243
|
+
.selected::after {
|
|
244
|
+
content: "";
|
|
149
245
|
position: absolute;
|
|
150
|
-
bottom:
|
|
246
|
+
bottom: 0;
|
|
151
247
|
left: 0;
|
|
152
|
-
z-index: 999;
|
|
153
|
-
background: var(--background-fill-primary);
|
|
154
248
|
width: 100%;
|
|
155
249
|
height: 2px;
|
|
156
|
-
|
|
250
|
+
background-color: var(--color-accent);
|
|
251
|
+
animation: fade-grow 0.2s ease-out forwards;
|
|
252
|
+
transform-origin: center;
|
|
253
|
+
z-index: 1;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@keyframes fade-grow {
|
|
257
|
+
from {
|
|
258
|
+
opacity: 0;
|
|
259
|
+
transform: scaleX(0.8);
|
|
260
|
+
}
|
|
261
|
+
to {
|
|
262
|
+
opacity: 1;
|
|
263
|
+
transform: scaleX(1);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.overflow-dropdown {
|
|
268
|
+
position: absolute;
|
|
269
|
+
top: calc(100% + var(--size-2));
|
|
270
|
+
right: 0;
|
|
271
|
+
background-color: var(--background-fill-primary);
|
|
272
|
+
border: 1px solid var(--border-color-primary);
|
|
273
|
+
border-radius: var(--radius-sm);
|
|
274
|
+
z-index: var(--layer-5);
|
|
275
|
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
|
276
|
+
padding: var(--size-2);
|
|
277
|
+
min-width: 150px;
|
|
278
|
+
width: max-content;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.overflow-dropdown button {
|
|
282
|
+
display: block;
|
|
283
|
+
width: 100%;
|
|
284
|
+
text-align: left;
|
|
285
|
+
padding: var(--size-2);
|
|
286
|
+
white-space: nowrap;
|
|
287
|
+
overflow: hidden;
|
|
288
|
+
text-overflow: ellipsis;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.overflow-menu > button {
|
|
292
|
+
padding: var(--size-1) var(--size-2);
|
|
293
|
+
min-width: auto;
|
|
294
|
+
border: 1px solid var(--border-color-primary);
|
|
295
|
+
border-radius: var(--radius-sm);
|
|
296
|
+
display: flex;
|
|
297
|
+
align-items: center;
|
|
298
|
+
justify-content: center;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.overflow-menu > button:hover {
|
|
302
|
+
background-color: var(--background-fill-secondary);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.overflow-menu :global(svg) {
|
|
306
|
+
width: 16px;
|
|
307
|
+
height: 16px;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.overflow-item-selected :global(svg) {
|
|
311
|
+
color: var(--color-accent);
|
|
157
312
|
}
|
|
158
313
|
</style>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gradio/tabs",
|
|
3
|
-
"version": "0.2
|
|
3
|
+
"version": "0.3.0-beta.2",
|
|
4
4
|
"description": "Gradio UI packages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"./package.json": "./package.json"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@gradio/utils": "^0.
|
|
20
|
+
"@gradio/utils": "^0.7.0-beta.2"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@gradio/preview": "^0.11.1"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg
|
|
2
|
+
width="16"
|
|
3
|
+
height="16"
|
|
4
|
+
viewBox="0 0 16 16"
|
|
5
|
+
fill="none"
|
|
6
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
7
|
+
>
|
|
8
|
+
<circle cx="2.5" cy="8" r="1.5" fill="currentColor" />
|
|
9
|
+
<circle cx="8" cy="8" r="1.5" fill="currentColor" />
|
|
10
|
+
<circle cx="13.5" cy="8" r="1.5" fill="currentColor" />
|
|
11
|
+
</svg>
|
package/shared/Tabs.svelte
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<script lang="ts">
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
setContext,
|
|
8
|
+
createEventDispatcher,
|
|
9
|
+
onMount,
|
|
10
|
+
onDestroy
|
|
11
|
+
} from "svelte";
|
|
12
|
+
import OverflowIcon from "./OverflowIcon.svelte";
|
|
7
13
|
import { writable } from "svelte/store";
|
|
8
14
|
import type { SelectData } from "@gradio/utils";
|
|
9
15
|
|
|
@@ -21,6 +27,13 @@
|
|
|
21
27
|
export let selected: number | string | object;
|
|
22
28
|
|
|
23
29
|
let tabs: Tab[] = [];
|
|
30
|
+
let overflow_menu_open = false;
|
|
31
|
+
let overflow_menu: HTMLElement;
|
|
32
|
+
|
|
33
|
+
$: has_tabs = tabs.length > 0;
|
|
34
|
+
|
|
35
|
+
let tab_nav_el: HTMLElement;
|
|
36
|
+
let overflow_nav: HTMLElement;
|
|
24
37
|
|
|
25
38
|
const selected_tab = writable<false | object | number | string>(false);
|
|
26
39
|
const selected_tab_index = writable<number>(0);
|
|
@@ -29,41 +42,31 @@
|
|
|
29
42
|
select: SelectData;
|
|
30
43
|
}>();
|
|
31
44
|
|
|
45
|
+
let is_overflowing = false;
|
|
46
|
+
let overflow_has_selected_tab = false;
|
|
47
|
+
|
|
32
48
|
setContext(TABS, {
|
|
33
49
|
register_tab: (tab: Tab) => {
|
|
34
|
-
let index
|
|
35
|
-
|
|
36
|
-
if (existingTab) {
|
|
37
|
-
// update existing tab with newer values
|
|
38
|
-
index = tabs.findIndex((t) => t.id === tab.id);
|
|
50
|
+
let index = tabs.findIndex((t) => t.id === tab.id);
|
|
51
|
+
if (index !== -1) {
|
|
39
52
|
tabs[index] = { ...tabs[index], ...tab };
|
|
40
53
|
} else {
|
|
41
|
-
tabs
|
|
42
|
-
name: tab.name,
|
|
43
|
-
id: tab.id,
|
|
44
|
-
elem_id: tab.elem_id,
|
|
45
|
-
visible: tab.visible,
|
|
46
|
-
interactive: tab.interactive
|
|
47
|
-
});
|
|
54
|
+
tabs = [...tabs, tab];
|
|
48
55
|
index = tabs.length - 1;
|
|
49
56
|
}
|
|
50
|
-
selected_tab.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
let nextTab = tabs.find((t) => t.visible && t.interactive);
|
|
56
|
-
return nextTab ? nextTab.id : current;
|
|
57
|
-
});
|
|
58
|
-
tabs = tabs;
|
|
57
|
+
if ($selected_tab === false && tab.visible && tab.interactive) {
|
|
58
|
+
$selected_tab = tab.id;
|
|
59
|
+
}
|
|
59
60
|
return index;
|
|
60
61
|
},
|
|
61
62
|
unregister_tab: (tab: Tab) => {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
const index = tabs.findIndex((t) => t.id === tab.id);
|
|
64
|
+
if (index !== -1) {
|
|
65
|
+
tabs = tabs.filter((t) => t.id !== tab.id);
|
|
66
|
+
if ($selected_tab === tab.id) {
|
|
67
|
+
$selected_tab = tabs[0]?.id || false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
67
70
|
},
|
|
68
71
|
selected_tab,
|
|
69
72
|
selected_tab_index
|
|
@@ -80,47 +83,129 @@
|
|
|
80
83
|
$selected_tab = id;
|
|
81
84
|
$selected_tab_index = tabs.findIndex((t) => t.id === id);
|
|
82
85
|
dispatch("change");
|
|
86
|
+
overflow_menu_open = false;
|
|
83
87
|
} else {
|
|
84
88
|
console.warn("Attempted to select a non-interactive or hidden tab.");
|
|
85
89
|
}
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
$: 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
|
+
});
|
|
106
|
+
|
|
107
|
+
function handle_outside_click(event: MouseEvent): void {
|
|
108
|
+
if (
|
|
109
|
+
overflow_menu_open &&
|
|
110
|
+
overflow_menu &&
|
|
111
|
+
!overflow_menu.contains(event.target as Node)
|
|
112
|
+
) {
|
|
113
|
+
overflow_menu_open = false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function handle_menu_overflow(): void {
|
|
118
|
+
if (!tab_nav_el) {
|
|
119
|
+
console.error("Menu elements not found");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let all_items: HTMLElement[] = [];
|
|
124
|
+
|
|
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
|
+
});
|
|
130
|
+
|
|
131
|
+
all_items.forEach((item) => tab_nav_el.appendChild(item));
|
|
132
|
+
|
|
133
|
+
const nav_items: HTMLElement[] = [];
|
|
134
|
+
const overflow_items: HTMLElement[] = [];
|
|
135
|
+
|
|
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;
|
|
142
|
+
|
|
143
|
+
if (is_overflowing) {
|
|
144
|
+
overflow_items.push(item as HTMLElement);
|
|
145
|
+
} else {
|
|
146
|
+
nav_items.push(item as HTMLElement);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
nav_items.forEach((item) => tab_nav_el.appendChild(item));
|
|
151
|
+
overflow_items.forEach((item) => overflow_nav.appendChild(item));
|
|
152
|
+
|
|
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
|
+
);
|
|
158
|
+
}
|
|
89
159
|
</script>
|
|
90
160
|
|
|
91
|
-
|
|
92
|
-
<div class="
|
|
93
|
-
|
|
94
|
-
{
|
|
95
|
-
{#
|
|
96
|
-
<button
|
|
97
|
-
role="tab"
|
|
98
|
-
class="selected"
|
|
99
|
-
aria-selected={true}
|
|
100
|
-
aria-controls={t.elem_id}
|
|
101
|
-
id={t.elem_id ? t.elem_id + "-button" : null}
|
|
102
|
-
>
|
|
103
|
-
{t.name}
|
|
104
|
-
</button>
|
|
105
|
-
{:else}
|
|
161
|
+
{#if has_tabs}
|
|
162
|
+
<div class="tabs {elem_classes.join(' ')}" class:hide={!visible} id={elem_id}>
|
|
163
|
+
<div class="tab-wrapper">
|
|
164
|
+
<div class="tab-container" bind:this={tab_nav_el} role="tablist">
|
|
165
|
+
{#each tabs as t, i (t.id)}
|
|
106
166
|
<button
|
|
107
167
|
role="tab"
|
|
108
|
-
|
|
168
|
+
class:selected={t.id === $selected_tab}
|
|
169
|
+
aria-selected={t.id === $selected_tab}
|
|
109
170
|
aria-controls={t.elem_id}
|
|
110
171
|
disabled={!t.interactive}
|
|
111
172
|
aria-disabled={!t.interactive}
|
|
112
173
|
id={t.elem_id ? t.elem_id + "-button" : null}
|
|
174
|
+
data-tab-id={t.id}
|
|
113
175
|
on:click={() => {
|
|
114
|
-
|
|
115
|
-
|
|
176
|
+
if (t.id !== $selected_tab) {
|
|
177
|
+
change_tab(t.id);
|
|
178
|
+
dispatch("select", { value: t.name, index: i });
|
|
179
|
+
}
|
|
116
180
|
}}
|
|
117
181
|
>
|
|
118
182
|
{t.name}
|
|
119
183
|
</button>
|
|
120
|
-
{/
|
|
121
|
-
|
|
122
|
-
|
|
184
|
+
{/each}
|
|
185
|
+
</div>
|
|
186
|
+
<span
|
|
187
|
+
class="overflow-menu"
|
|
188
|
+
class:hide={!is_overflowing}
|
|
189
|
+
bind:this={overflow_menu}
|
|
190
|
+
>
|
|
191
|
+
<button
|
|
192
|
+
on:click|stopPropagation={() =>
|
|
193
|
+
(overflow_menu_open = !overflow_menu_open)}
|
|
194
|
+
class:overflow-item-selected={overflow_has_selected_tab}
|
|
195
|
+
>
|
|
196
|
+
<OverflowIcon />
|
|
197
|
+
</button>
|
|
198
|
+
<div
|
|
199
|
+
class="overflow-dropdown"
|
|
200
|
+
bind:this={overflow_nav}
|
|
201
|
+
class:hide={!overflow_menu_open}
|
|
202
|
+
/>
|
|
203
|
+
</span>
|
|
204
|
+
</div>
|
|
123
205
|
</div>
|
|
206
|
+
{/if}
|
|
207
|
+
|
|
208
|
+
<div>
|
|
124
209
|
<slot />
|
|
125
210
|
</div>
|
|
126
211
|
|
|
@@ -133,50 +218,140 @@
|
|
|
133
218
|
display: none;
|
|
134
219
|
}
|
|
135
220
|
|
|
136
|
-
.tab-
|
|
221
|
+
.tab-wrapper {
|
|
137
222
|
display: flex;
|
|
223
|
+
align-items: center;
|
|
224
|
+
justify-content: space-between;
|
|
138
225
|
position: relative;
|
|
139
|
-
|
|
140
|
-
|
|
226
|
+
height: var(--size-8);
|
|
227
|
+
padding-bottom: var(--size-2);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.tab-container {
|
|
231
|
+
display: flex;
|
|
232
|
+
align-items: center;
|
|
233
|
+
width: 100%;
|
|
234
|
+
position: relative;
|
|
235
|
+
overflow: hidden;
|
|
236
|
+
height: var(--size-8);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.tab-container::after {
|
|
240
|
+
content: "";
|
|
241
|
+
position: absolute;
|
|
242
|
+
bottom: 0;
|
|
243
|
+
left: 0;
|
|
244
|
+
right: 0;
|
|
245
|
+
height: 1px;
|
|
246
|
+
background-color: var(--border-color-primary);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.overflow-menu {
|
|
250
|
+
flex-shrink: 0;
|
|
251
|
+
margin-left: var(--size-2);
|
|
141
252
|
}
|
|
142
253
|
|
|
143
254
|
button {
|
|
144
|
-
margin-bottom:
|
|
145
|
-
border:
|
|
146
|
-
border-
|
|
147
|
-
|
|
148
|
-
border-top-right-radius: var(--container-radius);
|
|
149
|
-
border-top-left-radius: var(--container-radius);
|
|
150
|
-
padding: var(--size-1) var(--size-4);
|
|
255
|
+
margin-bottom: 0;
|
|
256
|
+
border: none;
|
|
257
|
+
border-radius: 0;
|
|
258
|
+
padding: 0 var(--size-4);
|
|
151
259
|
color: var(--body-text-color-subdued);
|
|
152
260
|
font-weight: var(--section-header-text-weight);
|
|
153
261
|
font-size: var(--section-header-text-size);
|
|
262
|
+
transition: all 0.2s ease-out;
|
|
263
|
+
background-color: transparent;
|
|
264
|
+
height: 100%;
|
|
265
|
+
display: flex;
|
|
266
|
+
align-items: center;
|
|
267
|
+
white-space: nowrap;
|
|
268
|
+
position: relative;
|
|
154
269
|
}
|
|
155
270
|
|
|
156
271
|
button:disabled {
|
|
157
|
-
color: var(--body-text-color-subdued);
|
|
158
272
|
opacity: 0.5;
|
|
159
273
|
cursor: not-allowed;
|
|
160
274
|
}
|
|
161
275
|
|
|
162
|
-
button:hover {
|
|
276
|
+
button:hover:not(:disabled):not(.selected) {
|
|
277
|
+
background-color: var(--background-fill-secondary);
|
|
163
278
|
color: var(--body-text-color);
|
|
164
279
|
}
|
|
280
|
+
|
|
165
281
|
.selected {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
282
|
+
background-color: transparent;
|
|
283
|
+
color: var(--color-accent);
|
|
284
|
+
position: relative;
|
|
169
285
|
}
|
|
170
286
|
|
|
171
|
-
.
|
|
172
|
-
|
|
287
|
+
.selected::after {
|
|
288
|
+
content: "";
|
|
173
289
|
position: absolute;
|
|
174
|
-
bottom:
|
|
290
|
+
bottom: 0;
|
|
175
291
|
left: 0;
|
|
176
|
-
z-index: 999;
|
|
177
|
-
background: var(--background-fill-primary);
|
|
178
292
|
width: 100%;
|
|
179
293
|
height: 2px;
|
|
180
|
-
|
|
294
|
+
background-color: var(--color-accent);
|
|
295
|
+
animation: fade-grow 0.2s ease-out forwards;
|
|
296
|
+
transform-origin: center;
|
|
297
|
+
z-index: 1;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
@keyframes fade-grow {
|
|
301
|
+
from {
|
|
302
|
+
opacity: 0;
|
|
303
|
+
transform: scaleX(0.8);
|
|
304
|
+
}
|
|
305
|
+
to {
|
|
306
|
+
opacity: 1;
|
|
307
|
+
transform: scaleX(1);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.overflow-dropdown {
|
|
312
|
+
position: absolute;
|
|
313
|
+
top: calc(100% + var(--size-2));
|
|
314
|
+
right: 0;
|
|
315
|
+
background-color: var(--background-fill-primary);
|
|
316
|
+
border: 1px solid var(--border-color-primary);
|
|
317
|
+
border-radius: var(--radius-sm);
|
|
318
|
+
z-index: var(--layer-5);
|
|
319
|
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
|
320
|
+
padding: var(--size-2);
|
|
321
|
+
min-width: 150px;
|
|
322
|
+
width: max-content;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.overflow-dropdown button {
|
|
326
|
+
display: block;
|
|
327
|
+
width: 100%;
|
|
328
|
+
text-align: left;
|
|
329
|
+
padding: var(--size-2);
|
|
330
|
+
white-space: nowrap;
|
|
331
|
+
overflow: hidden;
|
|
332
|
+
text-overflow: ellipsis;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.overflow-menu > button {
|
|
336
|
+
padding: var(--size-1) var(--size-2);
|
|
337
|
+
min-width: auto;
|
|
338
|
+
border: 1px solid var(--border-color-primary);
|
|
339
|
+
border-radius: var(--radius-sm);
|
|
340
|
+
display: flex;
|
|
341
|
+
align-items: center;
|
|
342
|
+
justify-content: center;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.overflow-menu > button:hover {
|
|
346
|
+
background-color: var(--background-fill-secondary);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.overflow-menu :global(svg) {
|
|
350
|
+
width: 16px;
|
|
351
|
+
height: 16px;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.overflow-item-selected :global(svg) {
|
|
355
|
+
color: var(--color-accent);
|
|
181
356
|
}
|
|
182
357
|
</style>
|