@gradio/tabs 0.5.8 → 0.5.10

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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # @gradio/tabs
2
2
 
3
+ ## 0.5.10
4
+
5
+ ### Fixes
6
+
7
+ - [#13303](https://github.com/gradio-app/gradio/pull/13303) [`79c5776`](https://github.com/gradio-app/gradio/commit/79c577680978adfb5eeaa01944635e14dc67e5f1) - Rework `Dataframe` wrapping, truncation, and column width sizing. Thanks @pngwn!
8
+
9
+ ## 0.5.9
10
+
11
+ ### Fixes
12
+
13
+ - [#13048](https://github.com/gradio-app/gradio/pull/13048) [`a5d4096`](https://github.com/gradio-app/gradio/commit/a5d40965bba21a832da522127048926b71c1a6dd) - Fix Tab Interactive Bug. Thanks @freddyaboulton!
14
+
15
+ ### Dependency updates
16
+
17
+ - @gradio/utils@0.12.2
18
+
19
+ ## 0.5.8
20
+
21
+ ### Dependency updates
22
+
23
+ - @gradio/utils@0.12.1
24
+
3
25
  ## 0.5.8
4
26
 
5
27
  ### Dependency updates
@@ -30,6 +30,25 @@
30
30
  let overflow_menu_open = false;
31
31
  let overflow_menu: HTMLElement;
32
32
 
33
+ // Track which tab orders have been registered by mounted TabItem components.
34
+ // Once a TabItem mounts and calls register_tab, it manages its own tab entry
35
+ // via _set_data -> register_tab, so _sync_tabs should not overwrite it.
36
+ let mounted_tab_orders: Set<number> = new Set();
37
+
38
+ // When initial_tabs changes (e.g. a non-mounted tab's props were updated),
39
+ // sync the internal tabs array so the tab buttons reflect the new state.
40
+ // Using a function call so the $: dependency is only on initial_tabs,
41
+ // not on tabs (which would cause a loop with register_tab).
42
+ $: _sync_tabs(initial_tabs);
43
+
44
+ function _sync_tabs(new_tabs: Tab[]): void {
45
+ for (let i = 0; i < new_tabs.length; i++) {
46
+ if (new_tabs[i] && !mounted_tab_orders.has(i)) {
47
+ tabs[i] = new_tabs[i];
48
+ }
49
+ }
50
+ }
51
+
33
52
  $: has_tabs = tabs.length > 0;
34
53
 
35
54
  let tab_nav_el: HTMLDivElement;
@@ -51,14 +70,16 @@
51
70
 
52
71
  onMount(() => {
53
72
  if (!tab_nav_el) return;
54
- const observer = new IntersectionObserver((entries) => {
73
+ const ro = new ResizeObserver(() => {
55
74
  handle_menu_overflow();
56
75
  });
57
- observer.observe(tab_nav_el);
76
+ ro.observe(tab_nav_el);
77
+ return () => ro.disconnect();
58
78
  });
59
79
 
60
80
  setContext(TABS, {
61
81
  register_tab: (tab: Tab, order: number) => {
82
+ mounted_tab_orders.add(order);
62
83
  tabs[order] = tab;
63
84
 
64
85
  if ($selected_tab === false && tab.visible !== false && tab.interactive) {
@@ -68,6 +89,7 @@
68
89
  return order;
69
90
  },
70
91
  unregister_tab: (tab: Tab, order: number) => {
92
+ mounted_tab_orders.delete(order);
71
93
  if ($selected_tab === tab.id) {
72
94
  $selected_tab = tabs[0]?.id || false;
73
95
  }
@@ -107,33 +129,42 @@
107
129
  }
108
130
  }
109
131
 
132
+ // approximate width of the "..." overflow button including margin
133
+ const OVERFLOW_BTN_RESERVE = 48;
134
+
110
135
  async function handle_menu_overflow(): Promise<void> {
111
136
  if (!tab_nav_el) return;
112
137
 
113
138
  await tick();
114
- const tab_nav_size = tab_nav_el.getBoundingClientRect();
139
+ await new Promise((r) => requestAnimationFrame(r));
115
140
 
116
- let max_width = tab_nav_size.width;
117
- const tab_sizes = get_tab_sizes(tabs, tab_els);
118
- let last_visible_index = 0;
119
- const offset = tab_nav_size.left;
141
+ const available = tab_nav_el.clientWidth;
120
142
 
121
- for (let i = tabs.length - 1; i >= 0; i--) {
143
+ let cumulative = 0;
144
+ let split_index = tabs.length;
145
+
146
+ for (let i = 0; i < tabs.length; i++) {
122
147
  const tab = tabs[i];
123
- if (!tab) continue;
124
- const tab_rect = tab_sizes[tab.id];
125
- if (!tab_rect) continue;
126
- if (tab_rect.right - offset < max_width) {
127
- last_visible_index = i;
148
+ if (!tab || tab.visible === false || tab.visible === "hidden") continue;
149
+ const el = tab_els[tab.id];
150
+ if (!el) continue;
151
+ cumulative += el.getBoundingClientRect().width;
152
+ const has_more = tabs
153
+ .slice(i + 1)
154
+ .some((t) => t && t.visible !== false && t.visible !== "hidden");
155
+ const limit = has_more ? available - OVERFLOW_BTN_RESERVE : available;
156
+ if (cumulative > limit) {
157
+ split_index = i;
128
158
  break;
129
159
  }
130
160
  }
131
161
 
132
- overflow_tabs = tabs.slice(last_visible_index + 1);
133
- visible_tabs = tabs.slice(0, last_visible_index + 1);
162
+ visible_tabs = tabs.slice(0, split_index);
163
+ overflow_tabs = tabs.slice(split_index);
134
164
 
135
165
  overflow_has_selected_tab = handle_overflow_has_selected_tab($selected_tab);
136
- is_overflowing = overflow_tabs.length > 0;
166
+ is_overflowing =
167
+ overflow_tabs.filter((t) => t && t.visible !== false).length > 0;
137
168
  }
138
169
 
139
170
  $: overflow_has_selected_tab =
@@ -146,18 +177,6 @@
146
177
  return overflow_tabs.some((t) => t?.id === selected_tab);
147
178
  }
148
179
 
149
- function get_tab_sizes(
150
- tabs: (Tab | null)[],
151
- tab_els: Record<string | number, HTMLElement>
152
- ): Record<string | number, DOMRect> {
153
- const tab_sizes: Record<string | number, DOMRect> = {};
154
- tabs.forEach((tab) => {
155
- if (!tab) return;
156
- tab_sizes[tab.id] = tab_els[tab.id]?.getBoundingClientRect();
157
- });
158
- return tab_sizes;
159
- }
160
-
161
180
  $: tab_scale =
162
181
  tabs[$selected_tab_index >= 0 ? $selected_tab_index : 0]?.scale;
163
182
  </script>
@@ -256,9 +275,7 @@
256
275
  <style>
257
276
  .tabs {
258
277
  position: relative;
259
- display: flex;
260
278
  flex-direction: column;
261
- gap: var(--layout-gap);
262
279
  }
263
280
 
264
281
  .hide {
@@ -276,6 +293,7 @@
276
293
  position: relative;
277
294
  height: var(--size-8);
278
295
  padding-bottom: var(--size-2);
296
+ margin-bottom: var(--layout-gap);
279
297
  }
280
298
 
281
299
  .tab-container {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/tabs",
3
- "version": "0.5.8",
3
+ "version": "0.5.10",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -17,10 +17,10 @@
17
17
  "./package.json": "./package.json"
18
18
  },
19
19
  "dependencies": {
20
- "@gradio/utils": "^0.12.0"
20
+ "@gradio/utils": "^0.12.2"
21
21
  },
22
22
  "devDependencies": {
23
- "@gradio/preview": "^0.16.0"
23
+ "@gradio/preview": "^0.16.2"
24
24
  },
25
25
  "peerDependencies": {
26
26
  "svelte": "^5.48.0"
@@ -30,6 +30,25 @@
30
30
  let overflow_menu_open = false;
31
31
  let overflow_menu: HTMLElement;
32
32
 
33
+ // Track which tab orders have been registered by mounted TabItem components.
34
+ // Once a TabItem mounts and calls register_tab, it manages its own tab entry
35
+ // via _set_data -> register_tab, so _sync_tabs should not overwrite it.
36
+ let mounted_tab_orders: Set<number> = new Set();
37
+
38
+ // When initial_tabs changes (e.g. a non-mounted tab's props were updated),
39
+ // sync the internal tabs array so the tab buttons reflect the new state.
40
+ // Using a function call so the $: dependency is only on initial_tabs,
41
+ // not on tabs (which would cause a loop with register_tab).
42
+ $: _sync_tabs(initial_tabs);
43
+
44
+ function _sync_tabs(new_tabs: Tab[]): void {
45
+ for (let i = 0; i < new_tabs.length; i++) {
46
+ if (new_tabs[i] && !mounted_tab_orders.has(i)) {
47
+ tabs[i] = new_tabs[i];
48
+ }
49
+ }
50
+ }
51
+
33
52
  $: has_tabs = tabs.length > 0;
34
53
 
35
54
  let tab_nav_el: HTMLDivElement;
@@ -51,14 +70,16 @@
51
70
 
52
71
  onMount(() => {
53
72
  if (!tab_nav_el) return;
54
- const observer = new IntersectionObserver((entries) => {
73
+ const ro = new ResizeObserver(() => {
55
74
  handle_menu_overflow();
56
75
  });
57
- observer.observe(tab_nav_el);
76
+ ro.observe(tab_nav_el);
77
+ return () => ro.disconnect();
58
78
  });
59
79
 
60
80
  setContext(TABS, {
61
81
  register_tab: (tab: Tab, order: number) => {
82
+ mounted_tab_orders.add(order);
62
83
  tabs[order] = tab;
63
84
 
64
85
  if ($selected_tab === false && tab.visible !== false && tab.interactive) {
@@ -68,6 +89,7 @@
68
89
  return order;
69
90
  },
70
91
  unregister_tab: (tab: Tab, order: number) => {
92
+ mounted_tab_orders.delete(order);
71
93
  if ($selected_tab === tab.id) {
72
94
  $selected_tab = tabs[0]?.id || false;
73
95
  }
@@ -107,33 +129,42 @@
107
129
  }
108
130
  }
109
131
 
132
+ // approximate width of the "..." overflow button including margin
133
+ const OVERFLOW_BTN_RESERVE = 48;
134
+
110
135
  async function handle_menu_overflow(): Promise<void> {
111
136
  if (!tab_nav_el) return;
112
137
 
113
138
  await tick();
114
- const tab_nav_size = tab_nav_el.getBoundingClientRect();
139
+ await new Promise((r) => requestAnimationFrame(r));
115
140
 
116
- let max_width = tab_nav_size.width;
117
- const tab_sizes = get_tab_sizes(tabs, tab_els);
118
- let last_visible_index = 0;
119
- const offset = tab_nav_size.left;
141
+ const available = tab_nav_el.clientWidth;
120
142
 
121
- for (let i = tabs.length - 1; i >= 0; i--) {
143
+ let cumulative = 0;
144
+ let split_index = tabs.length;
145
+
146
+ for (let i = 0; i < tabs.length; i++) {
122
147
  const tab = tabs[i];
123
- if (!tab) continue;
124
- const tab_rect = tab_sizes[tab.id];
125
- if (!tab_rect) continue;
126
- if (tab_rect.right - offset < max_width) {
127
- last_visible_index = i;
148
+ if (!tab || tab.visible === false || tab.visible === "hidden") continue;
149
+ const el = tab_els[tab.id];
150
+ if (!el) continue;
151
+ cumulative += el.getBoundingClientRect().width;
152
+ const has_more = tabs
153
+ .slice(i + 1)
154
+ .some((t) => t && t.visible !== false && t.visible !== "hidden");
155
+ const limit = has_more ? available - OVERFLOW_BTN_RESERVE : available;
156
+ if (cumulative > limit) {
157
+ split_index = i;
128
158
  break;
129
159
  }
130
160
  }
131
161
 
132
- overflow_tabs = tabs.slice(last_visible_index + 1);
133
- visible_tabs = tabs.slice(0, last_visible_index + 1);
162
+ visible_tabs = tabs.slice(0, split_index);
163
+ overflow_tabs = tabs.slice(split_index);
134
164
 
135
165
  overflow_has_selected_tab = handle_overflow_has_selected_tab($selected_tab);
136
- is_overflowing = overflow_tabs.length > 0;
166
+ is_overflowing =
167
+ overflow_tabs.filter((t) => t && t.visible !== false).length > 0;
137
168
  }
138
169
 
139
170
  $: overflow_has_selected_tab =
@@ -146,18 +177,6 @@
146
177
  return overflow_tabs.some((t) => t?.id === selected_tab);
147
178
  }
148
179
 
149
- function get_tab_sizes(
150
- tabs: (Tab | null)[],
151
- tab_els: Record<string | number, HTMLElement>
152
- ): Record<string | number, DOMRect> {
153
- const tab_sizes: Record<string | number, DOMRect> = {};
154
- tabs.forEach((tab) => {
155
- if (!tab) return;
156
- tab_sizes[tab.id] = tab_els[tab.id]?.getBoundingClientRect();
157
- });
158
- return tab_sizes;
159
- }
160
-
161
180
  $: tab_scale =
162
181
  tabs[$selected_tab_index >= 0 ? $selected_tab_index : 0]?.scale;
163
182
  </script>
@@ -256,9 +275,7 @@
256
275
  <style>
257
276
  .tabs {
258
277
  position: relative;
259
- display: flex;
260
278
  flex-direction: column;
261
- gap: var(--layout-gap);
262
279
  }
263
280
 
264
281
  .hide {
@@ -276,6 +293,7 @@
276
293
  position: relative;
277
294
  height: var(--size-8);
278
295
  padding-bottom: var(--size-2);
296
+ margin-bottom: var(--layout-gap);
279
297
  }
280
298
 
281
299
  .tab-container {