@gradio/tabs 0.5.10 → 0.6.0
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 +12 -0
- package/Index.svelte +9 -7
- package/Tabs.test.ts +269 -0
- package/WithChild.svelte +8 -0
- package/dist/Index.svelte +9 -7
- package/dist/WithChild.svelte +8 -0
- package/dist/WithChild.svelte.d.ts +3 -0
- package/dist/shared/Tabs.svelte +12 -6
- package/package.json +1 -1
- package/shared/Tabs.svelte +12 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @gradio/tabs
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- [#13467](https://github.com/gradio-app/gradio/pull/13467) [`feba2e1`](https://github.com/gradio-app/gradio/commit/feba2e1e41c68f4bbdacf4c963d8d9689d4b3346) - `Tab`, `TabItem`, `Plot` Unit tests. Thanks @dawoodkhan82!
|
|
8
|
+
|
|
9
|
+
## 0.5.11
|
|
10
|
+
|
|
11
|
+
### Fixes
|
|
12
|
+
|
|
13
|
+
- [#13240](https://github.com/gradio-app/gradio/pull/13240) [`0d670ad`](https://github.com/gradio-app/gradio/commit/0d670adf41a0b510f7fd745495dce1664d38f0e5) - Fix browser freeze when a dataframe's value is set (e.g. via a tab select event), and only dispatch the tabs select event when the selected tab actually changes. Thanks @freddyaboulton!
|
|
14
|
+
|
|
3
15
|
## 0.5.10
|
|
4
16
|
|
|
5
17
|
### Fixes
|
package/Index.svelte
CHANGED
|
@@ -7,24 +7,26 @@
|
|
|
7
7
|
import Tabs from "./shared/Tabs.svelte";
|
|
8
8
|
import Walkthrough from "./shared/Walkthrough.svelte";
|
|
9
9
|
import type { TabsProps, TabsEvents } from "./types";
|
|
10
|
-
import { untrack } from "svelte";
|
|
11
10
|
|
|
12
11
|
let props = $props();
|
|
13
12
|
const gradio = new Gradio<TabsEvents, TabsProps>(props);
|
|
14
13
|
|
|
14
|
+
let old_selected = gradio.props.selected;
|
|
15
|
+
|
|
15
16
|
$effect(() => {
|
|
16
|
-
if (gradio.props.selected) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
if (old_selected !== gradio.props.selected) {
|
|
18
|
+
const i = gradio.props.initial_tabs.findIndex(
|
|
19
|
+
(t) => t.id === gradio.props.selected
|
|
20
|
+
);
|
|
21
|
+
if (i >= 0) {
|
|
21
22
|
gradio.dispatch("gradio_tab_select", {
|
|
22
23
|
value: gradio.props.initial_tabs[i].label,
|
|
23
24
|
index: i,
|
|
24
25
|
id: gradio.props.initial_tabs[i].id,
|
|
25
26
|
component_id: gradio.props.initial_tabs[i].component_id
|
|
26
27
|
});
|
|
27
|
-
}
|
|
28
|
+
}
|
|
29
|
+
old_selected = gradio.props.selected;
|
|
28
30
|
}
|
|
29
31
|
});
|
|
30
32
|
</script>
|
package/Tabs.test.ts
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { test, describe, afterEach, expect } from "vitest";
|
|
2
|
+
import { cleanup, render, fireEvent } from "@self/tootils/render";
|
|
3
|
+
import { run_shared_prop_tests } from "@self/tootils/shared-prop-tests";
|
|
4
|
+
import type { Tab } from "./shared/Tabs.svelte";
|
|
5
|
+
|
|
6
|
+
import Tabs from "./Index.svelte";
|
|
7
|
+
import TabsWithChild from "./WithChild.svelte";
|
|
8
|
+
|
|
9
|
+
const make_tab = (over: Partial<Tab>): Tab => ({
|
|
10
|
+
label: "Tab",
|
|
11
|
+
id: "t1",
|
|
12
|
+
elem_id: undefined,
|
|
13
|
+
visible: true,
|
|
14
|
+
interactive: true,
|
|
15
|
+
scale: null,
|
|
16
|
+
component_id: 1,
|
|
17
|
+
...over
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const tabs: Tab[] = [
|
|
21
|
+
make_tab({ label: "First", id: "t1", component_id: 1 }),
|
|
22
|
+
make_tab({ label: "Second", id: "t2", component_id: 2 }),
|
|
23
|
+
make_tab({ label: "Third", id: "t3", component_id: 3 })
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const default_props = {
|
|
27
|
+
initial_tabs: tabs,
|
|
28
|
+
selected: "t1",
|
|
29
|
+
name: "tabs" as const,
|
|
30
|
+
visible: true
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
run_shared_prop_tests({
|
|
34
|
+
component: Tabs,
|
|
35
|
+
name: "Tabs",
|
|
36
|
+
base_props: { initial_tabs: tabs, selected: "t1", name: "tabs" },
|
|
37
|
+
has_label: false,
|
|
38
|
+
has_validation_error: false,
|
|
39
|
+
has_block_wrapper: false,
|
|
40
|
+
visible_false_hides: true
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("Tabs", () => {
|
|
44
|
+
afterEach(() => cleanup());
|
|
45
|
+
|
|
46
|
+
test("renders a tab button for each visible tab", async () => {
|
|
47
|
+
const { getByRole } = await render(Tabs, default_props);
|
|
48
|
+
|
|
49
|
+
expect(getByRole("tab", { name: "First" })).toBeInTheDocument();
|
|
50
|
+
expect(getByRole("tab", { name: "Second" })).toBeInTheDocument();
|
|
51
|
+
expect(getByRole("tab", { name: "Third" })).toBeInTheDocument();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("does not render a button for a tab with visible: false", async () => {
|
|
55
|
+
const { queryByRole } = await render(Tabs, {
|
|
56
|
+
...default_props,
|
|
57
|
+
initial_tabs: [
|
|
58
|
+
make_tab({ label: "Shown", id: "t1", component_id: 1 }),
|
|
59
|
+
make_tab({ label: "Gone", id: "t2", visible: false, component_id: 2 })
|
|
60
|
+
]
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(queryByRole("tab", { name: "Gone" })).toBeNull();
|
|
64
|
+
expect(queryByRole("tab", { name: "Shown" })).toBeInTheDocument();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("the selected tab is marked aria-selected", async () => {
|
|
68
|
+
const { getByRole } = await render(Tabs, {
|
|
69
|
+
...default_props,
|
|
70
|
+
selected: "t2"
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(getByRole("tab", { name: "Second" })).toHaveAttribute(
|
|
74
|
+
"aria-selected",
|
|
75
|
+
"true"
|
|
76
|
+
);
|
|
77
|
+
expect(getByRole("tab", { name: "First" })).toHaveAttribute(
|
|
78
|
+
"aria-selected",
|
|
79
|
+
"false"
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("Props: interactive", () => {
|
|
85
|
+
afterEach(() => cleanup());
|
|
86
|
+
|
|
87
|
+
test("a non-interactive tab is disabled", async () => {
|
|
88
|
+
const { getByRole } = await render(Tabs, {
|
|
89
|
+
...default_props,
|
|
90
|
+
initial_tabs: [
|
|
91
|
+
make_tab({ label: "Active", id: "t1", component_id: 1 }),
|
|
92
|
+
make_tab({
|
|
93
|
+
label: "Locked",
|
|
94
|
+
id: "t2",
|
|
95
|
+
interactive: false,
|
|
96
|
+
component_id: 2
|
|
97
|
+
})
|
|
98
|
+
]
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(getByRole("tab", { name: "Locked" })).toBeDisabled();
|
|
102
|
+
expect(getByRole("tab", { name: "Active" })).toBeEnabled();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("Events: select", () => {
|
|
107
|
+
afterEach(() => cleanup());
|
|
108
|
+
|
|
109
|
+
test("clicking a tab dispatches select with its details", async () => {
|
|
110
|
+
const { listen, getByRole } = await render(Tabs, default_props);
|
|
111
|
+
|
|
112
|
+
const select = listen("select");
|
|
113
|
+
|
|
114
|
+
await fireEvent.click(getByRole("tab", { name: "Second" }));
|
|
115
|
+
|
|
116
|
+
expect(select).toHaveBeenCalledTimes(1);
|
|
117
|
+
expect(select).toHaveBeenCalledWith({
|
|
118
|
+
value: "Second",
|
|
119
|
+
index: 1,
|
|
120
|
+
id: "t2",
|
|
121
|
+
component_id: 2
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("clicking the already-selected tab does not dispatch select", async () => {
|
|
126
|
+
const { listen, getByRole } = await render(Tabs, default_props);
|
|
127
|
+
|
|
128
|
+
const select = listen("select");
|
|
129
|
+
|
|
130
|
+
await fireEvent.click(getByRole("tab", { name: "First" }));
|
|
131
|
+
|
|
132
|
+
expect(select).not.toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("clicking a disabled tab does not make it the selected tab", async () => {
|
|
136
|
+
const { listen, get_data, getByRole } = await render(Tabs, {
|
|
137
|
+
...default_props,
|
|
138
|
+
selected: "t1",
|
|
139
|
+
initial_tabs: [
|
|
140
|
+
make_tab({ label: "Active", id: "t1", component_id: 1 }),
|
|
141
|
+
make_tab({
|
|
142
|
+
label: "Locked",
|
|
143
|
+
id: "t2",
|
|
144
|
+
interactive: false,
|
|
145
|
+
component_id: 2
|
|
146
|
+
})
|
|
147
|
+
]
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const change = listen("change");
|
|
151
|
+
|
|
152
|
+
await fireEvent.click(getByRole("tab", { name: "Locked" }));
|
|
153
|
+
|
|
154
|
+
expect(change).not.toHaveBeenCalled();
|
|
155
|
+
expect(getByRole("tab", { name: "Locked" })).toHaveAttribute(
|
|
156
|
+
"aria-selected",
|
|
157
|
+
"false"
|
|
158
|
+
);
|
|
159
|
+
expect((await get_data()).selected).toBe("t1");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("Events: change", () => {
|
|
164
|
+
afterEach(() => cleanup());
|
|
165
|
+
|
|
166
|
+
test("clicking a different tab dispatches change once", async () => {
|
|
167
|
+
const { listen, getByRole } = await render(Tabs, default_props);
|
|
168
|
+
|
|
169
|
+
const change = listen("change");
|
|
170
|
+
|
|
171
|
+
await fireEvent.click(getByRole("tab", { name: "Third" }));
|
|
172
|
+
|
|
173
|
+
expect(change).toHaveBeenCalledTimes(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("clicking the already-selected tab does not dispatch change", async () => {
|
|
177
|
+
const { listen, getByRole } = await render(Tabs, default_props);
|
|
178
|
+
|
|
179
|
+
const change = listen("change");
|
|
180
|
+
|
|
181
|
+
await fireEvent.click(getByRole("tab", { name: "First" }));
|
|
182
|
+
|
|
183
|
+
expect(change).not.toHaveBeenCalled();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("Events: gradio_tab_select", () => {
|
|
188
|
+
afterEach(() => cleanup());
|
|
189
|
+
|
|
190
|
+
test("fires when the selected tab changes via set_data", async () => {
|
|
191
|
+
const { listen, set_data } = await render(Tabs, default_props);
|
|
192
|
+
|
|
193
|
+
const gradio_tab_select = listen("gradio_tab_select");
|
|
194
|
+
|
|
195
|
+
await set_data({ selected: "t3" });
|
|
196
|
+
|
|
197
|
+
expect(gradio_tab_select).toHaveBeenCalledTimes(1);
|
|
198
|
+
expect(gradio_tab_select).toHaveBeenCalledWith({
|
|
199
|
+
value: "Third",
|
|
200
|
+
index: 2,
|
|
201
|
+
id: "t3",
|
|
202
|
+
component_id: 3
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("get_data / set_data", () => {
|
|
208
|
+
afterEach(() => cleanup());
|
|
209
|
+
|
|
210
|
+
test("get_data returns the initial selection", async () => {
|
|
211
|
+
const { get_data } = await render(Tabs, default_props);
|
|
212
|
+
|
|
213
|
+
const data = await get_data();
|
|
214
|
+
expect(data.selected).toBe("t1");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("clicking a tab updates the selection returned by get_data", async () => {
|
|
218
|
+
const { get_data, getByRole } = await render(Tabs, default_props);
|
|
219
|
+
|
|
220
|
+
await fireEvent.click(getByRole("tab", { name: "Second" }));
|
|
221
|
+
|
|
222
|
+
const data = await get_data();
|
|
223
|
+
expect(data.selected).toBe("t2");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("set_data updates which tab is marked aria-selected", async () => {
|
|
227
|
+
const { set_data, getByRole } = await render(Tabs, default_props);
|
|
228
|
+
|
|
229
|
+
await set_data({ selected: "t3" });
|
|
230
|
+
|
|
231
|
+
expect(getByRole("tab", { name: "Third" })).toHaveAttribute(
|
|
232
|
+
"aria-selected",
|
|
233
|
+
"true"
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("set_data then get_data round-trips the selection", async () => {
|
|
238
|
+
const { set_data, get_data } = await render(Tabs, default_props);
|
|
239
|
+
|
|
240
|
+
await set_data({ selected: "t2" });
|
|
241
|
+
|
|
242
|
+
const data = await get_data();
|
|
243
|
+
expect(data.selected).toBe("t2");
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe("Children / slot", () => {
|
|
248
|
+
afterEach(() => cleanup());
|
|
249
|
+
|
|
250
|
+
test("renders slot children inside the tabs container", async () => {
|
|
251
|
+
const { getByTestId } = await render(TabsWithChild, default_props);
|
|
252
|
+
|
|
253
|
+
expect(getByTestId("slot-content")).toBeVisible();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("Edge cases", () => {
|
|
258
|
+
afterEach(() => cleanup());
|
|
259
|
+
|
|
260
|
+
test("no select or change events fire on initial mount", async () => {
|
|
261
|
+
const { listen } = await render(Tabs, default_props);
|
|
262
|
+
|
|
263
|
+
const select = listen("select", { retrospective: true });
|
|
264
|
+
const change = listen("change", { retrospective: true });
|
|
265
|
+
|
|
266
|
+
expect(select).not.toHaveBeenCalled();
|
|
267
|
+
expect(change).not.toHaveBeenCalled();
|
|
268
|
+
});
|
|
269
|
+
});
|
package/WithChild.svelte
ADDED
package/dist/Index.svelte
CHANGED
|
@@ -7,24 +7,26 @@
|
|
|
7
7
|
import Tabs from "./shared/Tabs.svelte";
|
|
8
8
|
import Walkthrough from "./shared/Walkthrough.svelte";
|
|
9
9
|
import type { TabsProps, TabsEvents } from "./types";
|
|
10
|
-
import { untrack } from "svelte";
|
|
11
10
|
|
|
12
11
|
let props = $props();
|
|
13
12
|
const gradio = new Gradio<TabsEvents, TabsProps>(props);
|
|
14
13
|
|
|
14
|
+
let old_selected = gradio.props.selected;
|
|
15
|
+
|
|
15
16
|
$effect(() => {
|
|
16
|
-
if (gradio.props.selected) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
if (old_selected !== gradio.props.selected) {
|
|
18
|
+
const i = gradio.props.initial_tabs.findIndex(
|
|
19
|
+
(t) => t.id === gradio.props.selected
|
|
20
|
+
);
|
|
21
|
+
if (i >= 0) {
|
|
21
22
|
gradio.dispatch("gradio_tab_select", {
|
|
22
23
|
value: gradio.props.initial_tabs[i].label,
|
|
23
24
|
index: i,
|
|
24
25
|
id: gradio.props.initial_tabs[i].id,
|
|
25
26
|
component_id: gradio.props.initial_tabs[i].component_id
|
|
26
27
|
});
|
|
27
|
-
}
|
|
28
|
+
}
|
|
29
|
+
old_selected = gradio.props.selected;
|
|
28
30
|
}
|
|
29
31
|
});
|
|
30
32
|
</script>
|
package/dist/shared/Tabs.svelte
CHANGED
|
@@ -37,16 +37,20 @@
|
|
|
37
37
|
|
|
38
38
|
// When initial_tabs changes (e.g. a non-mounted tab's props were updated),
|
|
39
39
|
// sync the internal tabs array so the tab buttons reflect the new state.
|
|
40
|
-
//
|
|
41
|
-
//
|
|
40
|
+
// The tabs mutation is deferred via tick() because in Svelte 5 legacy mode
|
|
41
|
+
// $: effects track all reads inside called functions — writing tabs[i]
|
|
42
|
+
// through the $state proxy would track `tabs` as a dependency, creating
|
|
43
|
+
// an infinite self-triggering loop.
|
|
42
44
|
$: _sync_tabs(initial_tabs);
|
|
43
45
|
|
|
44
46
|
function _sync_tabs(new_tabs: Tab[]): void {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
tick().then(() => {
|
|
48
|
+
for (let i = 0; i < new_tabs.length; i++) {
|
|
49
|
+
if (new_tabs[i] && !mounted_tab_orders.has(i)) {
|
|
50
|
+
tabs[i] = new_tabs[i];
|
|
51
|
+
}
|
|
48
52
|
}
|
|
49
|
-
}
|
|
53
|
+
});
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
$: has_tabs = tabs.length > 0;
|
|
@@ -138,6 +142,8 @@
|
|
|
138
142
|
await tick();
|
|
139
143
|
await new Promise((r) => requestAnimationFrame(r));
|
|
140
144
|
|
|
145
|
+
if (!tab_nav_el) return;
|
|
146
|
+
|
|
141
147
|
const available = tab_nav_el.clientWidth;
|
|
142
148
|
|
|
143
149
|
let cumulative = 0;
|
package/package.json
CHANGED
package/shared/Tabs.svelte
CHANGED
|
@@ -37,16 +37,20 @@
|
|
|
37
37
|
|
|
38
38
|
// When initial_tabs changes (e.g. a non-mounted tab's props were updated),
|
|
39
39
|
// sync the internal tabs array so the tab buttons reflect the new state.
|
|
40
|
-
//
|
|
41
|
-
//
|
|
40
|
+
// The tabs mutation is deferred via tick() because in Svelte 5 legacy mode
|
|
41
|
+
// $: effects track all reads inside called functions — writing tabs[i]
|
|
42
|
+
// through the $state proxy would track `tabs` as a dependency, creating
|
|
43
|
+
// an infinite self-triggering loop.
|
|
42
44
|
$: _sync_tabs(initial_tabs);
|
|
43
45
|
|
|
44
46
|
function _sync_tabs(new_tabs: Tab[]): void {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
tick().then(() => {
|
|
48
|
+
for (let i = 0; i < new_tabs.length; i++) {
|
|
49
|
+
if (new_tabs[i] && !mounted_tab_orders.has(i)) {
|
|
50
|
+
tabs[i] = new_tabs[i];
|
|
51
|
+
}
|
|
48
52
|
}
|
|
49
|
-
}
|
|
53
|
+
});
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
$: has_tabs = tabs.length > 0;
|
|
@@ -138,6 +142,8 @@
|
|
|
138
142
|
await tick();
|
|
139
143
|
await new Promise((r) => requestAnimationFrame(r));
|
|
140
144
|
|
|
145
|
+
if (!tab_nav_el) return;
|
|
146
|
+
|
|
141
147
|
const available = tab_nav_el.clientWidth;
|
|
142
148
|
|
|
143
149
|
let cumulative = 0;
|