@gjsify/adwaita-web 0.31.0 → 0.33.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.
Files changed (183) hide show
  1. package/dist/adwaita-web.css +835 -299
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/adw-about-dialog.spec.d.ts +1 -0
  4. package/lib/types/adw-action-rows.spec.d.ts +1 -0
  5. package/lib/types/adw-avatar.spec.d.ts +1 -0
  6. package/lib/types/adw-banner.spec.d.ts +1 -0
  7. package/lib/types/adw-button-content.spec.d.ts +1 -0
  8. package/lib/types/adw-carousel.spec.d.ts +1 -0
  9. package/lib/types/adw-checks.spec.d.ts +1 -0
  10. package/lib/types/adw-header-bar.spec.d.ts +1 -0
  11. package/lib/types/adw-icon.spec.d.ts +1 -0
  12. package/lib/types/adw-progress-bar.spec.d.ts +1 -0
  13. package/lib/types/adw-sidebar.spec.d.ts +1 -0
  14. package/lib/types/adw-switch.spec.d.ts +1 -0
  15. package/lib/types/adw-wrap-box.spec.d.ts +1 -0
  16. package/lib/types/bottom-sheet.spec.d.ts +1 -0
  17. package/lib/types/breakpoints.d.ts +22 -0
  18. package/lib/types/breakpoints.spec.d.ts +1 -0
  19. package/lib/types/chrome.spec.d.ts +1 -0
  20. package/lib/types/elements/adw-about-dialog.d.ts +47 -8
  21. package/lib/types/elements/adw-action-row.d.ts +35 -0
  22. package/lib/types/elements/adw-avatar.d.ts +11 -0
  23. package/lib/types/elements/adw-bottom-sheet.d.ts +33 -4
  24. package/lib/types/elements/adw-button-content.d.ts +12 -0
  25. package/lib/types/elements/adw-button-row.d.ts +10 -1
  26. package/lib/types/elements/adw-carousel.d.ts +111 -8
  27. package/lib/types/elements/adw-checks.d.ts +57 -0
  28. package/lib/types/elements/adw-clamp.d.ts +4 -1
  29. package/lib/types/elements/adw-combo-row.d.ts +10 -0
  30. package/lib/types/elements/adw-data-grid.d.ts +11 -31
  31. package/lib/types/elements/adw-drop-down.d.ts +17 -20
  32. package/lib/types/elements/adw-entry-row.d.ts +93 -3
  33. package/lib/types/elements/adw-expander-row.d.ts +7 -1
  34. package/lib/types/elements/adw-header-bar.d.ts +15 -0
  35. package/lib/types/elements/adw-icon.d.ts +25 -0
  36. package/lib/types/elements/adw-inline-view-switcher.d.ts +73 -11
  37. package/lib/types/elements/adw-menu-button.d.ts +6 -6
  38. package/lib/types/elements/adw-navigation-split-view.d.ts +48 -0
  39. package/lib/types/elements/adw-navigation-view.d.ts +63 -20
  40. package/lib/types/elements/adw-overlay-split-view.d.ts +94 -0
  41. package/lib/types/elements/adw-password-entry-row.d.ts +19 -10
  42. package/lib/types/elements/adw-popover.d.ts +58 -0
  43. package/lib/types/elements/adw-preferences-dialog.d.ts +57 -2
  44. package/lib/types/elements/adw-preferences-group.d.ts +31 -0
  45. package/lib/types/elements/adw-progress-bar.d.ts +38 -0
  46. package/lib/types/elements/adw-sidebar.d.ts +37 -5
  47. package/lib/types/elements/adw-spinner.d.ts +23 -0
  48. package/lib/types/elements/adw-split-button.d.ts +53 -7
  49. package/lib/types/elements/adw-switch-row.d.ts +11 -2
  50. package/lib/types/elements/adw-switch.d.ts +15 -0
  51. package/lib/types/elements/adw-tab-view.d.ts +129 -12
  52. package/lib/types/elements/adw-toolbar-view.d.ts +5 -0
  53. package/lib/types/elements/adw-view-stack.d.ts +39 -19
  54. package/lib/types/elements/adw-view-switcher-bar.d.ts +25 -7
  55. package/lib/types/elements/adw-view-switcher.d.ts +76 -13
  56. package/lib/types/elements/adw-window-title.d.ts +3 -1
  57. package/lib/types/elements/adw-wrap-box.d.ts +58 -3
  58. package/lib/types/elements/view-switcher-dom.d.ts +44 -0
  59. package/lib/types/entry-rows.spec.d.ts +1 -0
  60. package/lib/types/index.d.ts +7 -0
  61. package/lib/types/navigation-view.spec.d.ts +1 -0
  62. package/lib/types/preferences-search.d.ts +20 -0
  63. package/lib/types/preferences.spec.d.ts +1 -0
  64. package/lib/types/split-button.spec.d.ts +1 -0
  65. package/lib/types/split-views.spec.d.ts +1 -0
  66. package/lib/types/styles.generated.d.ts +1 -1
  67. package/lib/types/tab-view.spec.d.ts +1 -0
  68. package/lib/types/view-stack.spec.d.ts +1 -0
  69. package/lib/types/view-switcher.spec.d.ts +1 -0
  70. package/package.json +6 -6
  71. package/scss/_banner.scss +13 -0
  72. package/scss/_bottom_sheet.scss +5 -6
  73. package/scss/_button.scss +3 -26
  74. package/scss/_button_content.scss +28 -0
  75. package/scss/_checks.scss +228 -0
  76. package/scss/_clamp.scss +9 -3
  77. package/scss/_combo_row.scss +19 -0
  78. package/scss/_data_grid.scss +15 -7
  79. package/scss/_drop_down.scss +12 -55
  80. package/scss/_expander_row.scss +5 -50
  81. package/scss/_icon.scss +51 -0
  82. package/scss/_icons.generated.scss +6 -0
  83. package/scss/_inline_view_switcher.scss +34 -0
  84. package/scss/_labels.scss +135 -0
  85. package/scss/_menu_button.scss +10 -70
  86. package/scss/_navigation_split_view.scss +47 -8
  87. package/scss/_overlay_split_view.scss +117 -32
  88. package/scss/_popover.scss +186 -0
  89. package/scss/_preferences.scss +29 -0
  90. package/scss/_preferences_dialog.scss +17 -0
  91. package/scss/_progress_bar.scss +161 -0
  92. package/scss/_reset.scss +18 -7
  93. package/scss/_spinner.scss +37 -19
  94. package/scss/_split_button.scss +6 -50
  95. package/scss/_switch.scss +90 -0
  96. package/scss/_switch_row.scss +6 -52
  97. package/scss/_tab_view.scss +63 -0
  98. package/scss/_theme.scss +10 -0
  99. package/scss/_toolbar_view.scss +74 -0
  100. package/scss/_variables.scss +34 -0
  101. package/scss/_view_switcher.scss +40 -0
  102. package/scss/_view_switcher_bar.scss +37 -0
  103. package/scss/_wrap_box.scss +65 -6
  104. package/scss/adwaita-skin.scss +23 -0
  105. package/src/adw-about-dialog.spec.ts +350 -0
  106. package/src/adw-action-rows.spec.ts +342 -0
  107. package/src/adw-avatar.spec.ts +176 -0
  108. package/src/adw-banner.spec.ts +152 -0
  109. package/src/adw-button-content.spec.ts +207 -0
  110. package/src/adw-button-row.spec.ts +12 -6
  111. package/src/adw-carousel.spec.ts +389 -0
  112. package/src/adw-checks.spec.ts +368 -0
  113. package/src/adw-drop-down.spec.ts +123 -0
  114. package/src/adw-header-bar.spec.ts +83 -0
  115. package/src/adw-icon.spec.ts +167 -0
  116. package/src/adw-progress-bar.spec.ts +173 -0
  117. package/src/adw-row-state.spec.ts +49 -0
  118. package/src/adw-sidebar.spec.ts +332 -0
  119. package/src/adw-switch.spec.ts +242 -0
  120. package/src/adw-tab-view.spec.ts +12 -3
  121. package/src/adw-view-switcher-bar.spec.ts +61 -13
  122. package/src/adw-wrap-box.spec.ts +341 -0
  123. package/src/bottom-sheet.spec.ts +325 -0
  124. package/src/breakpoints.spec.ts +147 -0
  125. package/src/breakpoints.ts +67 -0
  126. package/src/chrome.spec.ts +400 -0
  127. package/src/elements/adw-about-dialog.ts +223 -163
  128. package/src/elements/adw-action-row.ts +131 -11
  129. package/src/elements/adw-avatar.ts +48 -54
  130. package/src/elements/adw-banner.ts +46 -10
  131. package/src/elements/adw-bottom-sheet.ts +145 -60
  132. package/src/elements/adw-button-content.ts +107 -33
  133. package/src/elements/adw-button-row.ts +61 -37
  134. package/src/elements/adw-button.ts +5 -7
  135. package/src/elements/adw-carousel.ts +467 -105
  136. package/src/elements/adw-checks.ts +299 -0
  137. package/src/elements/adw-clamp.ts +80 -10
  138. package/src/elements/adw-combo-row.ts +32 -7
  139. package/src/elements/adw-data-grid.ts +43 -91
  140. package/src/elements/adw-drop-down.ts +117 -128
  141. package/src/elements/adw-entry-row.ts +404 -36
  142. package/src/elements/adw-expander-row.ts +61 -29
  143. package/src/elements/adw-header-bar.ts +31 -6
  144. package/src/elements/adw-icon.ts +148 -0
  145. package/src/elements/adw-inline-view-switcher.ts +325 -131
  146. package/src/elements/adw-menu-button.ts +93 -70
  147. package/src/elements/adw-navigation-split-view.ts +220 -14
  148. package/src/elements/adw-navigation-view.ts +286 -148
  149. package/src/elements/adw-overlay-split-view.ts +536 -28
  150. package/src/elements/adw-password-entry-row.ts +111 -89
  151. package/src/elements/adw-popover.ts +283 -0
  152. package/src/elements/adw-preferences-dialog.ts +120 -21
  153. package/src/elements/adw-preferences-group.ts +128 -22
  154. package/src/elements/adw-progress-bar.ts +189 -0
  155. package/src/elements/adw-sidebar.ts +301 -142
  156. package/src/elements/adw-spin-row.ts +11 -5
  157. package/src/elements/adw-spinner.ts +168 -9
  158. package/src/elements/adw-split-button.ts +371 -92
  159. package/src/elements/adw-status-page.ts +9 -9
  160. package/src/elements/adw-switch-row.ts +100 -30
  161. package/src/elements/adw-switch.ts +129 -0
  162. package/src/elements/adw-tab-view.ts +700 -129
  163. package/src/elements/adw-toggle-group.ts +5 -7
  164. package/src/elements/adw-toolbar-view.ts +84 -21
  165. package/src/elements/adw-view-stack.ts +183 -85
  166. package/src/elements/adw-view-switcher-bar.ts +171 -81
  167. package/src/elements/adw-view-switcher.ts +328 -117
  168. package/src/elements/adw-window-title.ts +44 -11
  169. package/src/elements/adw-wrap-box.ts +329 -78
  170. package/src/elements/view-switcher-dom.ts +108 -0
  171. package/src/entry-rows.spec.ts +391 -0
  172. package/src/index.ts +23 -0
  173. package/src/navigation-view.spec.ts +435 -0
  174. package/src/preferences-search.ts +135 -0
  175. package/src/preferences.spec.ts +250 -0
  176. package/src/source-view/adw-source-view.ts +2 -5
  177. package/src/split-button.spec.ts +470 -0
  178. package/src/split-views.spec.ts +893 -0
  179. package/src/styles.generated.ts +1 -1
  180. package/src/tab-view.spec.ts +451 -0
  181. package/src/test.browser.mts +49 -0
  182. package/src/view-stack.spec.ts +272 -0
  183. package/src/view-switcher.spec.ts +665 -0
@@ -0,0 +1,451 @@
1
+ // DOM-level conformance tests for <adw-tab-view>, driven by the SAME vectors the
2
+ // core suite and the NativeScript renderer assert against
3
+ // (`@gjsify/adwaita-core/conformance`).
4
+ //
5
+ // This family is the one where the two renderers did not merely drift — they had
6
+ // never implemented the model at all. `close-page` meant three different things
7
+ // (libadwaita removes a non-pinned page through a two-phase confirm; this element
8
+ // removed nothing, EVER, so every close was permanently denied; NS removed
9
+ // immediately with no signal and refused on the last tab), out-of-range selection
10
+ // was clamped here and ignored there, and the pinned partition, the parent-aware
11
+ // close successor, the reorder clamps and the keyboard model existed in neither.
12
+ // This suite is what makes the element a thin adapter over `TabViewState` instead
13
+ // of a second implementation: the element is handed STRAIGHT to the shared vector
14
+ // driver, so a method it re-implements fails here.
15
+ import { describe, expect, it } from '@gjsify/unit';
16
+
17
+ import {
18
+ TAB_CLOSE_VISIBLE_VECTORS,
19
+ TAB_VIEW_VECTORS,
20
+ TABS_REVEALED_VECTORS,
21
+ applyTabViewOp,
22
+ seedTabViewPages,
23
+ tabViewClosing,
24
+ tabViewOrder,
25
+ } from '@gjsify/adwaita-core/conformance';
26
+ import type { TabViewVectorPagesChange, TabViewVectorSelection } from '@gjsify/adwaita-core/conformance';
27
+ import { tabCloseVisible, tabsRevealed } from '@gjsify/adwaita-core';
28
+
29
+ import type { AdwTabView } from './elements/adw-tab-view.js';
30
+
31
+ /** A mounted view plus every event it emitted. */
32
+ interface MountedTabView {
33
+ view: AdwTabView;
34
+ host: HTMLElement;
35
+ selections: TabViewVectorSelection[];
36
+ pagesChanges: TabViewVectorPagesChange[];
37
+ closeAttempts: string[];
38
+ }
39
+
40
+ /**
41
+ * Mount an EMPTY view and record its events.
42
+ *
43
+ * Empty rather than markup-seeded: the vectors seed through the same
44
+ * `seedTabViewPages` the other two suites use, which is what makes "the element
45
+ * is the vector target" literal. The declarative path gets its own describe
46
+ * block below.
47
+ */
48
+ function mountTabView(handler: 'default' | 'defer'): MountedTabView {
49
+ const host = document.createElement('div');
50
+ document.body.appendChild(host);
51
+
52
+ const view = document.createElement('adw-tab-view') as AdwTabView;
53
+ const selections: TabViewVectorSelection[] = [];
54
+ const pagesChanges: TabViewVectorPagesChange[] = [];
55
+ const closeAttempts: string[] = [];
56
+
57
+ view.addEventListener('notify::selected-page', (event) => {
58
+ const detail = (event as CustomEvent).detail;
59
+ selections.push({
60
+ selectedId: detail.selectedId,
61
+ selectedIndex: detail.selected,
62
+ previousId: detail.previousId,
63
+ interactive: detail.interactive,
64
+ });
65
+ });
66
+ for (const kind of ['attached', 'detached', 'reordered', 'pinned', 'updated'] as const) {
67
+ view.addEventListener(`page-${kind}`, (event) => {
68
+ const detail = (event as CustomEvent).detail;
69
+ pagesChanges.push({
70
+ kind,
71
+ id: detail.id,
72
+ position: detail.position,
73
+ previousPosition: detail.previousPosition,
74
+ });
75
+ });
76
+ }
77
+ // Every close ATTEMPT dispatches this event, so the log IS the attempt log —
78
+ // and `preventDefault()` is the DOM spelling of the handler's 'defer'.
79
+ view.addEventListener('close-page', (event) => {
80
+ closeAttempts.push((event as CustomEvent).detail.id);
81
+ if (handler === 'defer') event.preventDefault();
82
+ });
83
+
84
+ host.appendChild(view);
85
+ return { view, host, selections, pagesChanges, closeAttempts };
86
+ }
87
+
88
+ /** Which page panels are actually SHOWN — what the user sees, not what the model says. */
89
+ function shownPanels(view: AdwTabView): boolean[] {
90
+ const panels = Array.from(view.querySelectorAll('.adw-tab-view-pages > .adw-tab-page')) as HTMLElement[];
91
+ return panels.map((panel) => !panel.hidden);
92
+ }
93
+
94
+ /** The tab chips, in bar order. */
95
+ function tabChips(view: AdwTabView): HTMLButtonElement[] {
96
+ return Array.from(view.querySelectorAll('.adw-tab-box > .adw-tab')) as HTMLButtonElement[];
97
+ }
98
+
99
+ export const AdwTabViewConformanceTest = async () => {
100
+ await describe('adw-tab-view model (libadwaita conformance vectors)', async () => {
101
+ for (const vector of TAB_VIEW_VECTORS) {
102
+ await it(vector.rule, () => {
103
+ const mounted = mountTabView(vector.handler ?? 'default');
104
+ const { view } = mounted;
105
+
106
+ // The element IS the vector target: no adapter, no glue. A method
107
+ // it re-implements instead of delegating fails right here.
108
+ seedTabViewPages(view, vector.pages);
109
+ expect(tabViewOrder(view)).toStrictEqual(vector.pages.map((page) => page.id));
110
+ expect(mounted.selections).toStrictEqual([...vector.setupChanges]);
111
+
112
+ mounted.selections.length = 0;
113
+ mounted.pagesChanges.length = 0;
114
+ mounted.closeAttempts.length = 0;
115
+ const results = vector.ops.map((op) => applyTabViewOp(view, op));
116
+
117
+ expect(results).toStrictEqual([...vector.opResults]);
118
+ expect(mounted.selections).toStrictEqual([...vector.changes]);
119
+ if (vector.pagesChanges) expect(mounted.pagesChanges).toStrictEqual([...vector.pagesChanges]);
120
+ if (vector.closeAttempts) expect(mounted.closeAttempts).toStrictEqual([...vector.closeAttempts]);
121
+
122
+ expect(tabViewOrder(view)).toStrictEqual([...vector.order]);
123
+ expect(view.nPinnedPages).toBe(vector.nPinnedPages);
124
+ expect(view.selectedId).toBe(vector.selectedId);
125
+ expect(view.selectedIndex).toBe(vector.selectedIndex);
126
+ if (vector.closing) expect(tabViewClosing(view)).toStrictEqual([...vector.closing]);
127
+ if (vector.diagnostics) expect(view.diagnostics).toStrictEqual([...vector.diagnostics]);
128
+
129
+ // The DOM must agree with the model: one chip per page in model
130
+ // order, exactly the selected panel shown, and nothing at all
131
+ // when the view emptied. The old element built the bar once at
132
+ // connect and never touched it again, so a closed page kept its
133
+ // chip forever — this is the assertion that would have caught it.
134
+ expect(tabChips(view).map((tab) => tab.dataset.pageId)).toStrictEqual([...vector.order]);
135
+ const expectedShown = vector.order.map(() => false);
136
+ if (vector.selectedIndex >= 0) expectedShown[vector.selectedIndex] = true;
137
+ expect(shownPanels(view)).toStrictEqual(expectedShown);
138
+ expect(tabChips(view).map((tab) => tab.getAttribute('aria-selected'))).toStrictEqual(
139
+ expectedShown.map((shown) => String(shown)),
140
+ );
141
+
142
+ mounted.host.remove();
143
+ });
144
+ }
145
+ });
146
+
147
+ await describe('adw-tab-view declared markup', async () => {
148
+ await it('adopts <adw-tab-page> children as the page panels and shows the first', () => {
149
+ const host = document.createElement('div');
150
+ host.innerHTML = `<adw-tab-view>
151
+ <adw-tab-page page-id="one" title="One"><p>first</p></adw-tab-page>
152
+ <adw-tab-page page-id="two" title="Two"><p>second</p></adw-tab-page>
153
+ </adw-tab-view>`;
154
+ document.body.appendChild(host);
155
+ const view = host.querySelector('adw-tab-view') as AdwTabView;
156
+
157
+ expect(tabViewOrder(view)).toStrictEqual(['one', 'two']);
158
+ expect(view.pages.map((page) => page.title)).toStrictEqual(['One', 'Two']);
159
+ expect(shownPanels(view)).toStrictEqual([true, false]);
160
+ // The declared element itself is the panel, so its markup survives.
161
+ expect(view.pages[0]!.content!.querySelector('p')!.textContent).toBe('first');
162
+ host.remove();
163
+ });
164
+
165
+ await it('keeps a title LIVE — the observedAttributes declaration used to have nothing behind it', () => {
166
+ const host = document.createElement('div');
167
+ host.innerHTML = `<adw-tab-view>
168
+ <adw-tab-page page-id="one" title="One"></adw-tab-page>
169
+ </adw-tab-view>`;
170
+ document.body.appendChild(host);
171
+ const view = host.querySelector('adw-tab-view') as AdwTabView;
172
+ const page = view.querySelector('adw-tab-page') as HTMLElement;
173
+
174
+ page.setAttribute('title', 'Renamed');
175
+ expect(view.pages[0]!.title).toBe('Renamed');
176
+ expect(tabChips(view)[0]!.querySelector('.adw-tab-title')!.textContent).toBe('Renamed');
177
+
178
+ // `null` coerces to '' (adw-tab-view.c:3021), not to 'null'.
179
+ page.removeAttribute('title');
180
+ expect(view.pages[0]!.title).toBe('');
181
+ host.remove();
182
+ });
183
+
184
+ await it('generates a distinct page id per declared page, and an explicit page-id wins', () => {
185
+ // An id stands in for the AdwTabPage POINTER, so two pages must never
186
+ // share one — a declared page without `page-id` still needs its own.
187
+ const host = document.createElement('div');
188
+ host.innerHTML = `<adw-tab-view>
189
+ <adw-tab-page title="One"></adw-tab-page>
190
+ <adw-tab-page title="Two"></adw-tab-page>
191
+ <adw-tab-page page-id="named" title="Three"></adw-tab-page>
192
+ </adw-tab-view>`;
193
+ document.body.appendChild(host);
194
+ const view = host.querySelector('adw-tab-view') as AdwTabView;
195
+
196
+ const ids = tabViewOrder(view);
197
+ expect(new Set(ids).size).toBe(3);
198
+ expect(ids[2]).toBe('named');
199
+ expect(view.setSelectedPage('named')).toBe(true);
200
+ host.remove();
201
+ });
202
+ });
203
+
204
+ await describe('adw-tab-view selected attribute', async () => {
205
+ function mountThree(attrs = ''): { view: AdwTabView; host: HTMLElement } {
206
+ const host = document.createElement('div');
207
+ host.innerHTML = `<adw-tab-view ${attrs}>
208
+ <adw-tab-page page-id="a" title="One"></adw-tab-page>
209
+ <adw-tab-page page-id="b" title="Two"></adw-tab-page>
210
+ <adw-tab-page page-id="c" title="Three"></adw-tab-page>
211
+ </adw-tab-view>`;
212
+ document.body.appendChild(host);
213
+ return { view: host.querySelector('adw-tab-view') as AdwTabView, host };
214
+ }
215
+
216
+ await it('selects the declared index instead of the auto-pick', () => {
217
+ const { view, host } = mountThree('selected="2"');
218
+ expect(view.selectedIndex).toBe(2);
219
+ expect(shownPanels(view)).toStrictEqual([false, false, true]);
220
+ host.remove();
221
+ });
222
+
223
+ await it('IGNORES an out-of-range or unparseable index instead of clamping it', () => {
224
+ // The old `Number.isNaN(raw) ? 0 : Math.min(Math.max(raw, 0), max)`
225
+ // silently selected the last page for `99` and the first for `-1`
226
+ // and `oops`; libadwaita refuses all three (adw-tab-view.c:2145, :4133).
227
+ const { view, host } = mountThree();
228
+ for (const value of ['99', '-1', 'oops']) {
229
+ view.setAttribute('selected', value);
230
+ expect(view.selectedIndex).toBe(0);
231
+ }
232
+ // ...and the attribute is put back in sync rather than left lying.
233
+ view.setAttribute('selected', '1');
234
+ expect(view.selectedIndex).toBe(1);
235
+ expect(view.getAttribute('selected')).toBe('1');
236
+ host.remove();
237
+ });
238
+
239
+ await it('notifies on a PROGRAMMATIC selection, not only on a click', () => {
240
+ // `view.selected = 2` used to change the visible page and fire
241
+ // nothing, while a tab click fired; C notifies on every path (:1854).
242
+ const { view, host } = mountThree();
243
+ const seen: number[] = [];
244
+ view.addEventListener('notify::selected-page', (event) =>
245
+ seen.push((event as CustomEvent).detail.selected),
246
+ );
247
+ view.selected = 2;
248
+ expect(seen).toStrictEqual([2]);
249
+ host.remove();
250
+ });
251
+
252
+ await it('reflects the selection back and drops the attribute when the view empties', () => {
253
+ const { view, host } = mountThree();
254
+ const chips = tabChips(view);
255
+ chips[1]!.click();
256
+ expect(view.getAttribute('selected')).toBe('1');
257
+
258
+ for (const id of ['a', 'b', 'c']) view.closePage(id);
259
+ expect(view.nPages).toBe(0);
260
+ expect(view.hasAttribute('selected')).toBe(false);
261
+ expect(view.selectedIndex).toBe(-1);
262
+ host.remove();
263
+ });
264
+ });
265
+
266
+ await describe('adw-tab-view close protocol', async () => {
267
+ await it('CLOSES a non-pinned page by default — the element used to remove nothing, ever', () => {
268
+ const mounted = mountTabView('default');
269
+ seedTabViewPages(mounted.view, [{ id: 'a' }, { id: 'b' }, { id: 'c' }]);
270
+ mounted.view.closePage('b');
271
+ expect(tabViewOrder(mounted.view)).toStrictEqual(['a', 'c']);
272
+ expect(tabChips(mounted.view)).toHaveLength(2);
273
+ mounted.host.remove();
274
+ });
275
+
276
+ await it('denies a PINNED page by default and leaves it in place', () => {
277
+ const mounted = mountTabView('default');
278
+ mounted.view.appendPinnedPage({ id: 'p' });
279
+ mounted.view.appendPage({ id: 'a' });
280
+ expect(mounted.view.closePage('p')).toBe(true);
281
+ expect(tabViewOrder(mounted.view)).toStrictEqual(['p', 'a']);
282
+ expect(mounted.view.isClosing('p')).toBe(false);
283
+ mounted.host.remove();
284
+ });
285
+
286
+ await it('preventDefault() defers the close until closePageFinish — the "save before closing?" seam', () => {
287
+ const mounted = mountTabView('defer');
288
+ seedTabViewPages(mounted.view, [{ id: 'a' }, { id: 'b' }]);
289
+
290
+ expect(mounted.view.closePage('b')).toBe(true);
291
+ expect(mounted.view.isClosing('b')).toBe(true);
292
+ expect(tabViewOrder(mounted.view)).toStrictEqual(['a', 'b']);
293
+
294
+ // A second request while one is pending is ignored, handler and all.
295
+ expect(mounted.view.closePage('b')).toBe(false);
296
+ expect(mounted.closeAttempts).toStrictEqual(['b']);
297
+
298
+ expect(mounted.view.closePageFinish('b', true)).toBe(true);
299
+ expect(tabViewOrder(mounted.view)).toStrictEqual(['a']);
300
+ mounted.host.remove();
301
+ });
302
+
303
+ await it('keeps `detail.index` pointing at the page while it is still there', () => {
304
+ const mounted = mountTabView('default');
305
+ seedTabViewPages(mounted.view, [{ id: 'a' }, { id: 'b' }, { id: 'c' }]);
306
+ let index = -1;
307
+ mounted.view.addEventListener('close-page', (event) => {
308
+ index = (event as CustomEvent).detail.index;
309
+ });
310
+ (mounted.view.querySelectorAll('.adw-tab-close')[2] as HTMLButtonElement).click();
311
+ expect(index).toBe(2);
312
+ mounted.host.remove();
313
+ });
314
+ });
315
+
316
+ await describe('adw-tab-view tab bar chrome', async () => {
317
+ await it('reveals the bar per tabsRevealed, including for a single PINNED tab', () => {
318
+ const host = document.createElement('div');
319
+ document.body.appendChild(host);
320
+ const view = document.createElement('adw-tab-view') as AdwTabView;
321
+ view.autohide = true;
322
+ host.appendChild(view);
323
+ const bar = view.querySelector('.adw-tab-bar') as HTMLElement;
324
+
325
+ view.appendPage({ id: 'a' });
326
+ expect(bar.hidden).toBe(
327
+ !tabsRevealed({ autohide: true, nPages: 1, nPinnedPages: 0, isTransferringPage: false }),
328
+ );
329
+ expect(bar.hidden).toBe(true);
330
+
331
+ // The clause the old `pages.length <= 1` check could not express.
332
+ view.setPagePinned('a', true);
333
+ expect(bar.hidden).toBe(false);
334
+ host.remove();
335
+ });
336
+
337
+ await it('gates the close button on tabCloseVisible rather than showing it always', () => {
338
+ const mounted = mountTabView('default');
339
+ seedTabViewPages(mounted.view, [{ id: 'a' }, { id: 'b' }]);
340
+ mounted.view.appendPinnedPage({ id: 'p' });
341
+ const closes = Array.from(mounted.view.querySelectorAll('.adw-tab-close')) as HTMLElement[];
342
+ const ids = tabViewOrder(mounted.view);
343
+
344
+ // p, a, b — 'a' is selected (it was the first page added).
345
+ expect(ids).toStrictEqual(['p', 'a', 'b']);
346
+ expect(closes.map((close) => !close.hidden)).toStrictEqual(
347
+ ids.map((id) =>
348
+ tabCloseVisible({
349
+ hovering: false,
350
+ fullyVisible: true,
351
+ selected: mounted.view.selectedId === id,
352
+ dragging: false,
353
+ pinned: mounted.view.pages.find((page) => page.id === id)!.pinned,
354
+ }),
355
+ ),
356
+ );
357
+ mounted.host.remove();
358
+ });
359
+
360
+ for (const vector of TAB_CLOSE_VISIBLE_VECTORS) {
361
+ await it(`close visibility: ${vector.rule}`, () => {
362
+ // The renderer's gate IS the core predicate; asserting the table
363
+ // here keeps a future "simplify to `selected`" from passing.
364
+ expect(
365
+ tabCloseVisible({
366
+ hovering: vector.hovering,
367
+ fullyVisible: vector.fullyVisible,
368
+ selected: vector.selected,
369
+ dragging: vector.dragging,
370
+ pinned: vector.pinned,
371
+ }),
372
+ ).toBe(vector.visible);
373
+ });
374
+ }
375
+
376
+ for (const vector of TABS_REVEALED_VECTORS) {
377
+ await it(`bar visibility: ${vector.rule}`, () => {
378
+ expect(
379
+ tabsRevealed({
380
+ autohide: vector.autohide,
381
+ nPages: vector.nPages,
382
+ nPinnedPages: vector.nPinnedPages,
383
+ isTransferringPage: vector.isTransferringPage,
384
+ }),
385
+ ).toBe(vector.revealed);
386
+ });
387
+ }
388
+ });
389
+
390
+ await describe('adw-tab-view keyboard model', async () => {
391
+ function mountFour(): { view: AdwTabView; host: HTMLElement } {
392
+ const host = document.createElement('div');
393
+ document.body.appendChild(host);
394
+ const view = document.createElement('adw-tab-view') as AdwTabView;
395
+ host.appendChild(view);
396
+ for (const id of ['a', 'b', 'c', 'd']) view.appendPage({ id, title: id });
397
+ return { view, host };
398
+ }
399
+
400
+ function press(view: AdwTabView, init: KeyboardEventInit & { key: string }): void {
401
+ const chips = tabChips(view);
402
+ const target = chips.find((chip) => chip.classList.contains('active')) ?? view;
403
+ target.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, cancelable: true, ...init }));
404
+ }
405
+
406
+ await it('Ctrl+Tab WRAPS where ArrowRight stops', () => {
407
+ const { view, host } = mountFour();
408
+ view.setSelectedPage('d');
409
+ press(view, { key: 'ArrowRight' });
410
+ expect(view.selectedId).toBe('d');
411
+ press(view, { key: 'Tab', ctrlKey: true });
412
+ expect(view.selectedId).toBe('a');
413
+ press(view, { key: 'Tab', ctrlKey: true, shiftKey: true });
414
+ expect(view.selectedId).toBe('d');
415
+ host.remove();
416
+ });
417
+
418
+ await it('moves the roving tabindex with the selection, so inactive tabs stay reachable', () => {
419
+ // The old element set `tab.tabIndex = isActive ? 0 : -1` under
420
+ // role=tablist with NO key handler, so every inactive tab was
421
+ // keyboard-unreachable.
422
+ const { view, host } = mountFour();
423
+ press(view, { key: 'ArrowRight' });
424
+ expect(view.selectedId).toBe('b');
425
+ expect(tabChips(view).map((chip) => chip.tabIndex)).toStrictEqual([-1, 0, -1, -1]);
426
+ host.remove();
427
+ });
428
+
429
+ await it('Alt+digit selects by index, with Alt+0 meaning page 9', () => {
430
+ const { view, host } = mountFour();
431
+ press(view, { key: '3', altKey: true });
432
+ expect(view.selectedId).toBe('c');
433
+ press(view, { key: '0', altKey: true });
434
+ expect(view.selectedId).toBe('c'); // index 9 does not exist
435
+ host.remove();
436
+ });
437
+
438
+ await it('Ctrl+Shift+Page-Up/Down REORDERS the selected page inside its partition', () => {
439
+ const { view, host } = mountFour();
440
+ view.setPagePinned('a', true);
441
+ view.setSelectedPage('b');
442
+ press(view, { key: 'PageDown', ctrlKey: true, shiftKey: true });
443
+ expect(tabViewOrder(view)).toStrictEqual(['a', 'c', 'b', 'd']);
444
+ // 'b' is now back at the partition's first slot; it cannot cross it.
445
+ press(view, { key: 'PageUp', ctrlKey: true, shiftKey: true });
446
+ press(view, { key: 'PageUp', ctrlKey: true, shiftKey: true });
447
+ expect(tabViewOrder(view)).toStrictEqual(['a', 'b', 'c', 'd']);
448
+ host.remove();
449
+ });
450
+ });
451
+ };
@@ -7,7 +7,22 @@ import { run } from '@gjsify/unit';
7
7
  // Registers the custom elements (side-effect) + injects the compiled CSS.
8
8
  import '@gjsify/adwaita-web';
9
9
 
10
+ import { AdwViewStackTest } from './view-stack.spec.js';
11
+ import { AdwNavigationViewTest } from './navigation-view.spec.js';
12
+ import { AdwSidebarTest } from './adw-sidebar.spec.js';
13
+ import { AdwEntryRowsTest } from './entry-rows.spec.js';
14
+ import { AdwSplitButtonTest } from './split-button.spec.js';
15
+ import { AdwCarouselTest } from './adw-carousel.spec.js';
16
+ import { AdwBottomSheetTest } from './bottom-sheet.spec.js';
17
+ import { AdwChromeTest } from './chrome.spec.js';
18
+ import { AdwPreferencesTest } from './preferences.spec.js';
19
+ import { AdwTabViewConformanceTest } from './tab-view.spec.js';
20
+ import { AdwViewSwitcherTest } from './view-switcher.spec.js';
21
+ import { AdwAvatarTest } from './adw-avatar.spec.js';
22
+ import { AdwSplitViewsTest } from './split-views.spec.js';
10
23
  import { AdwButtonRowTest } from './adw-button-row.spec.js';
24
+ import { AdwActionRowsTest } from './adw-action-rows.spec.js';
25
+ import { AdwBreakpointsTest } from './breakpoints.spec.js';
11
26
  import { AdwDataGridTest } from './adw-data-grid.spec.js';
12
27
  import { AdwDialogTest } from './adw-dialog.spec.js';
13
28
  import { AdwDropDownTest } from './adw-drop-down.spec.js';
@@ -16,9 +31,36 @@ import { AdwTabViewTest } from './adw-tab-view.spec.js';
16
31
  import { AdwToastOverlayTest } from './adw-toast-overlay.spec.js';
17
32
  import { AdwViewSwitcherBarTest } from './adw-view-switcher-bar.spec.js';
18
33
  import { AdwStyleIsolationTest } from './style-isolation.spec.js';
34
+ import { AdwWrapBoxTest } from './adw-wrap-box.spec.js';
35
+ import { AdwHeaderBarTest } from './adw-header-bar.spec.js';
36
+ import { AdwBannerTest } from './adw-banner.spec.js';
37
+ import { AdwButtonContentTest } from './adw-button-content.spec.js';
38
+ import { AdwIconTest } from './adw-icon.spec.js';
39
+ import { AdwSwitchTest } from './adw-switch.spec.js';
40
+ import { AdwChecksTest } from './adw-checks.spec.js';
41
+ import { AdwProgressBarTest } from './adw-progress-bar.spec.js';
42
+ import { AdwAboutDialogTest } from './adw-about-dialog.spec.js';
19
43
 
20
44
  run({
45
+ AdwAboutDialogTest,
46
+ AdwBannerTest,
47
+ AdwButtonContentTest,
48
+ AdwIconTest,
49
+ AdwSwitchTest,
50
+ AdwChecksTest,
51
+ AdwProgressBarTest,
52
+ AdwAvatarTest,
53
+ AdwCarouselTest,
54
+ AdwBottomSheetTest,
55
+ AdwChromeTest,
56
+ AdwPreferencesTest,
57
+ AdwTabViewConformanceTest,
58
+ AdwViewSwitcherTest,
59
+
60
+ AdwSplitViewsTest,
21
61
  AdwButtonRowTest,
62
+ AdwActionRowsTest,
63
+ AdwBreakpointsTest,
22
64
  AdwDataGridTest,
23
65
  AdwDialogTest,
24
66
  AdwDropDownTest,
@@ -27,4 +69,11 @@ run({
27
69
  AdwToastOverlayTest,
28
70
  AdwViewSwitcherBarTest,
29
71
  AdwStyleIsolationTest,
72
+ AdwWrapBoxTest,
73
+ AdwHeaderBarTest,
74
+ AdwViewStackTest,
75
+ AdwNavigationViewTest,
76
+ AdwSidebarTest,
77
+ AdwEntryRowsTest,
78
+ AdwSplitButtonTest,
30
79
  });