@gjsify/adwaita-web 0.36.0 → 0.37.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 (146) hide show
  1. package/dist/adwaita-web.css +433 -33
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/adw-shortcut-label.spec.d.ts +2 -0
  4. package/lib/types/breakpoints.d.ts +10 -12
  5. package/lib/types/elements/adw-about-dialog.d.ts +19 -36
  6. package/lib/types/elements/adw-action-row.d.ts +9 -12
  7. package/lib/types/elements/adw-alert-dialog.d.ts +0 -12
  8. package/lib/types/elements/adw-avatar.d.ts +1 -0
  9. package/lib/types/elements/adw-bottom-sheet.d.ts +7 -12
  10. package/lib/types/elements/adw-button-content.d.ts +3 -4
  11. package/lib/types/elements/adw-button-row.d.ts +1 -1
  12. package/lib/types/elements/adw-carousel.d.ts +28 -49
  13. package/lib/types/elements/adw-checks.d.ts +2 -8
  14. package/lib/types/elements/adw-combo-row.d.ts +6 -6
  15. package/lib/types/elements/adw-data-grid.d.ts +5 -10
  16. package/lib/types/elements/adw-drop-down.d.ts +3 -11
  17. package/lib/types/elements/adw-entry-row.d.ts +20 -23
  18. package/lib/types/elements/adw-icon.d.ts +2 -4
  19. package/lib/types/elements/adw-inline-view-switcher.d.ts +18 -31
  20. package/lib/types/elements/adw-navigation-split-view.d.ts +7 -12
  21. package/lib/types/elements/adw-navigation-view.d.ts +2 -10
  22. package/lib/types/elements/adw-overlay-split-view.d.ts +28 -52
  23. package/lib/types/elements/adw-password-entry-row.d.ts +4 -5
  24. package/lib/types/elements/adw-popover.d.ts +2 -7
  25. package/lib/types/elements/adw-preferences-dialog.d.ts +21 -34
  26. package/lib/types/elements/adw-preferences-group.d.ts +12 -14
  27. package/lib/types/elements/adw-progress-bar.d.ts +3 -4
  28. package/lib/types/elements/adw-shortcut-label.d.ts +20 -0
  29. package/lib/types/elements/adw-sidebar.d.ts +5 -10
  30. package/lib/types/elements/adw-spinner.d.ts +3 -5
  31. package/lib/types/elements/adw-split-button.d.ts +6 -16
  32. package/lib/types/elements/adw-switch-row.d.ts +3 -3
  33. package/lib/types/elements/adw-tab-view.d.ts +16 -26
  34. package/lib/types/elements/adw-toast-overlay.d.ts +8 -13
  35. package/lib/types/elements/adw-view-stack.d.ts +7 -17
  36. package/lib/types/elements/adw-view-switcher-bar.d.ts +4 -8
  37. package/lib/types/elements/adw-view-switcher.d.ts +18 -38
  38. package/lib/types/elements/adw-wrap-box.d.ts +7 -17
  39. package/lib/types/elements/view-switcher-dom.d.ts +10 -13
  40. package/lib/types/index.d.ts +1 -0
  41. package/lib/types/source-view/hex.d.ts +7 -13
  42. package/lib/types/style-classes.spec.d.ts +11 -0
  43. package/lib/types/styles.generated.d.ts +1 -1
  44. package/package.json +6 -6
  45. package/scss/_about_dialog.scss +3 -2
  46. package/scss/_action_row.scss +33 -0
  47. package/scss/_avatar.scss +12 -0
  48. package/scss/_button.scss +44 -22
  49. package/scss/_card.scss +11 -8
  50. package/scss/_checks.scss +2 -4
  51. package/scss/_dialog.scss +3 -2
  52. package/scss/_headerbar.scss +23 -0
  53. package/scss/_popover.scss +8 -4
  54. package/scss/_preferences.scss +7 -7
  55. package/scss/_progress_bar.scss +9 -6
  56. package/scss/_reset.scss +1 -1
  57. package/scss/_shortcut_label.scss +80 -0
  58. package/scss/_sidebar.scss +57 -0
  59. package/scss/_status_page.scss +20 -0
  60. package/scss/_style_classes.scss +407 -0
  61. package/scss/_switch.scss +4 -3
  62. package/scss/_variables.scss +25 -0
  63. package/scss/adwaita-skin.scss +12 -0
  64. package/src/adw-about-dialog.spec.ts +34 -49
  65. package/src/adw-action-rows.spec.ts +25 -33
  66. package/src/adw-avatar.spec.ts +51 -20
  67. package/src/adw-banner.spec.ts +5 -8
  68. package/src/adw-button-content.spec.ts +9 -12
  69. package/src/adw-button-row.spec.ts +2 -4
  70. package/src/adw-carousel.spec.ts +40 -53
  71. package/src/adw-checks.spec.ts +36 -47
  72. package/src/adw-drop-down.spec.ts +25 -32
  73. package/src/adw-header-bar.spec.ts +5 -7
  74. package/src/adw-icon.spec.ts +23 -32
  75. package/src/adw-progress-bar.spec.ts +10 -12
  76. package/src/adw-row-state.spec.ts +9 -10
  77. package/src/adw-shortcut-label.spec.ts +112 -0
  78. package/src/adw-sidebar.spec.ts +17 -24
  79. package/src/adw-switch.spec.ts +11 -17
  80. package/src/adw-tab-view.spec.ts +7 -10
  81. package/src/adw-toast-overlay.spec.ts +7 -11
  82. package/src/adw-view-switcher-bar.spec.ts +10 -14
  83. package/src/adw-wrap-box.spec.ts +25 -36
  84. package/src/bottom-sheet.spec.ts +30 -36
  85. package/src/breakpoints.ts +15 -18
  86. package/src/chrome.spec.ts +13 -21
  87. package/src/elements/adw-about-dialog.ts +71 -114
  88. package/src/elements/adw-action-row.ts +22 -37
  89. package/src/elements/adw-alert-dialog.ts +25 -50
  90. package/src/elements/adw-avatar.ts +40 -15
  91. package/src/elements/adw-banner.ts +14 -18
  92. package/src/elements/adw-bottom-sheet.ts +31 -52
  93. package/src/elements/adw-button-content.ts +30 -37
  94. package/src/elements/adw-button-row.ts +14 -24
  95. package/src/elements/adw-carousel.ts +87 -148
  96. package/src/elements/adw-checks.ts +43 -56
  97. package/src/elements/adw-clamp.ts +8 -9
  98. package/src/elements/adw-combo-row.ts +17 -25
  99. package/src/elements/adw-data-grid.ts +26 -38
  100. package/src/elements/adw-dialog.ts +25 -56
  101. package/src/elements/adw-drop-down.ts +31 -51
  102. package/src/elements/adw-entry-row.ts +39 -46
  103. package/src/elements/adw-expander-row.ts +30 -53
  104. package/src/elements/adw-icon.ts +30 -53
  105. package/src/elements/adw-inline-view-switcher.ts +38 -67
  106. package/src/elements/adw-menu-button.ts +28 -58
  107. package/src/elements/adw-navigation-split-view.ts +35 -47
  108. package/src/elements/adw-navigation-view.ts +12 -28
  109. package/src/elements/adw-overlay-split-view.ts +100 -175
  110. package/src/elements/adw-password-entry-row.ts +18 -28
  111. package/src/elements/adw-popover.ts +28 -44
  112. package/src/elements/adw-preferences-dialog.ts +41 -66
  113. package/src/elements/adw-preferences-group.ts +33 -46
  114. package/src/elements/adw-progress-bar.ts +27 -47
  115. package/src/elements/adw-shortcut-label.ts +138 -0
  116. package/src/elements/adw-sidebar.ts +31 -39
  117. package/src/elements/adw-spin-row.ts +7 -14
  118. package/src/elements/adw-spinner.ts +31 -43
  119. package/src/elements/adw-split-button.ts +45 -75
  120. package/src/elements/adw-switch-row.ts +29 -46
  121. package/src/elements/adw-switch.ts +28 -47
  122. package/src/elements/adw-tab-view.ts +62 -103
  123. package/src/elements/adw-toast-overlay.ts +28 -35
  124. package/src/elements/adw-toggle-group.ts +9 -16
  125. package/src/elements/adw-view-stack.ts +32 -51
  126. package/src/elements/adw-view-switcher-bar.ts +26 -50
  127. package/src/elements/adw-view-switcher.ts +60 -110
  128. package/src/elements/adw-window-title.ts +4 -4
  129. package/src/elements/adw-wrap-box.ts +33 -59
  130. package/src/elements/view-switcher-dom.ts +15 -22
  131. package/src/entry-rows.spec.ts +18 -24
  132. package/src/index.ts +13 -15
  133. package/src/navigation-view.spec.ts +7 -10
  134. package/src/preferences-search.ts +11 -15
  135. package/src/preferences.spec.ts +13 -15
  136. package/src/source-view/adw-source-view.ts +6 -19
  137. package/src/source-view/hex.ts +7 -13
  138. package/src/split-button.spec.ts +32 -44
  139. package/src/split-views.spec.ts +83 -149
  140. package/src/style-classes.spec.ts +181 -0
  141. package/src/style-isolation.spec.ts +3 -6
  142. package/src/styles.generated.ts +1 -1
  143. package/src/tab-view.spec.ts +26 -35
  144. package/src/test.browser.mts +4 -0
  145. package/src/view-stack.spec.ts +23 -34
  146. package/src/view-switcher.spec.ts +56 -84
@@ -2,16 +2,12 @@
2
2
  // core suite and the NativeScript renderer assert against
3
3
  // (`@gjsify/adwaita-core/conformance`).
4
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.
5
+ // The element is a thin adapter over `TabViewState`, not a second implementation: it is
6
+ // handed STRAIGHT to the shared vector driver, so a method it re-implements fails here.
7
+ // Neither renderer had the model at all — `close-page` meant three different things,
8
+ // out-of-range selection was clamped in one and ignored in the other, and the pinned
9
+ // partition, the parent-aware close successor, the reorder clamps and the keyboard model
10
+ // existed in neither.
15
11
  import { describe, expect, it } from '@gjsify/unit';
16
12
 
17
13
  import {
@@ -38,12 +34,10 @@ interface MountedTabView {
38
34
  }
39
35
 
40
36
  /**
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.
37
+ * Mount an EMPTY view and record its events. Empty rather than markup-seeded: the
38
+ * vectors seed through the same `seedTabViewPages` the other two suites use, which is
39
+ * what makes "the element is the vector target" literal. The declarative path has its own
40
+ * describe block below.
47
41
  */
48
42
  function mountTabView(handler: 'default' | 'defer'): MountedTabView {
49
43
  const host = document.createElement('div');
@@ -126,11 +120,9 @@ export const AdwTabViewConformanceTest = async () => {
126
120
  if (vector.closing) expect(tabViewClosing(view)).toStrictEqual([...vector.closing]);
127
121
  if (vector.diagnostics) expect(view.diagnostics).toStrictEqual([...vector.diagnostics]);
128
122
 
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.
123
+ // The DOM must agree with the model: one chip per page in model order,
124
+ // exactly the selected panel shown, and nothing when the view empties. A
125
+ // bar built once at connect keeps a closed page's chip forever.
134
126
  expect(tabChips(view).map((tab) => tab.dataset.pageId)).toStrictEqual([...vector.order]);
135
127
  const expectedShown = vector.order.map(() => false);
136
128
  if (vector.selectedIndex >= 0) expectedShown[vector.selectedIndex] = true;
@@ -175,15 +167,15 @@ export const AdwTabViewConformanceTest = async () => {
175
167
  expect(view.pages[0]!.title).toBe('Renamed');
176
168
  expect(tabChips(view)[0]!.querySelector('.adw-tab-title')!.textContent).toBe('Renamed');
177
169
 
178
- // `null` coerces to '' (adw-tab-view.c:3021), not to 'null'.
170
+ // `null` coerces to '', not to 'null'.
179
171
  page.removeAttribute('title');
180
172
  expect(view.pages[0]!.title).toBe('');
181
173
  host.remove();
182
174
  });
183
175
 
184
176
  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.
177
+ // An id stands in for the AdwTabPage POINTER, so two pages must never share
178
+ // one — a declared page without `page-id` still needs its own.
187
179
  const host = document.createElement('div');
188
180
  host.innerHTML = `<adw-tab-view>
189
181
  <adw-tab-page title="One"></adw-tab-page>
@@ -221,15 +213,15 @@ export const AdwTabViewConformanceTest = async () => {
221
213
  });
222
214
 
223
215
  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).
216
+ // A clamping `Number.isNaN(raw) ? 0 : Math.min(Math.max(raw, 0), max)` selects
217
+ // the last page for `99` and the first for `-1`/`oops`; libadwaita refuses all
218
+ // three.
227
219
  const { view, host } = mountThree();
228
220
  for (const value of ['99', '-1', 'oops']) {
229
221
  view.setAttribute('selected', value);
230
222
  expect(view.selectedIndex).toBe(0);
231
223
  }
232
- // ...and the attribute is put back in sync rather than left lying.
224
+ //...and the attribute is put back in sync rather than left lying.
233
225
  view.setAttribute('selected', '1');
234
226
  expect(view.selectedIndex).toBe(1);
235
227
  expect(view.getAttribute('selected')).toBe('1');
@@ -237,8 +229,8 @@ export const AdwTabViewConformanceTest = async () => {
237
229
  });
238
230
 
239
231
  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).
232
+ // C notifies on EVERY path: a programmatic `view.selected = 2` must notify as
233
+ // a tab click does.
242
234
  const { view, host } = mountThree();
243
235
  const seen: number[] = [];
244
236
  view.addEventListener('notify::selected-page', (event) =>
@@ -359,8 +351,8 @@ export const AdwTabViewConformanceTest = async () => {
359
351
 
360
352
  for (const vector of TAB_CLOSE_VISIBLE_VECTORS) {
361
353
  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.
354
+ // The renderer's gate IS the core predicate; the table here keeps a future
355
+ // "simplify to `selected`" from passing.
364
356
  expect(
365
357
  tabCloseVisible({
366
358
  hovering: vector.hovering,
@@ -416,9 +408,8 @@ export const AdwTabViewConformanceTest = async () => {
416
408
  });
417
409
 
418
410
  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.
411
+ // `tab.tabIndex = isActive ? 0 : -1` under role=tablist needs a key handler,
412
+ // or every inactive tab is keyboard-unreachable.
422
413
  const { view, host } = mountFour();
423
414
  press(view, { key: 'ArrowRight' });
424
415
  expect(view.selectedId).toBe('b');
@@ -40,8 +40,12 @@ import { AdwSwitchTest } from './adw-switch.spec.js';
40
40
  import { AdwChecksTest } from './adw-checks.spec.js';
41
41
  import { AdwProgressBarTest } from './adw-progress-bar.spec.js';
42
42
  import { AdwAboutDialogTest } from './adw-about-dialog.spec.js';
43
+ import { AdwStyleClassesTest } from './style-classes.spec.js';
44
+ import { AdwShortcutLabelTest } from './adw-shortcut-label.spec.js';
43
45
 
44
46
  run({
47
+ AdwStyleClassesTest,
48
+ AdwShortcutLabelTest,
45
49
  AdwAboutDialogTest,
46
50
  AdwBannerTest,
47
51
  AdwButtonContentTest,
@@ -2,16 +2,11 @@
2
2
  // the core suite and the NativeScript renderer assert against
3
3
  // (`@gjsify/adwaita-core/conformance`).
4
4
  //
5
- // The two renderers used to carry independent copies of the selection logic —
6
- // the same four-clause index guard, the same `findIndex` by name, the same
7
- // `?? ''` fallbacks — and both had drifted away from libadwaita in the same
8
- // ways: `visibleChildIndex = 1.5` passed the `Number.isFinite` guard and blanked
9
- // the entire stack, the per-page `visible` flag did not exist so the auto-pick
10
- // took the first page ADDED instead of the first VISIBLE one, an empty
11
- // `visible-child-name` was treated as "no name" by the declarative path but as a
12
- // legal lookup by the property path, and the auto-pick notified nothing so a
13
- // bound switcher needed a manual `refresh()`. Nothing failed, because nothing
14
- // compared them. This suite is that comparison.
5
+ // The rules it pins, each of which both renderers had drifted from while carrying
6
+ // independent copies of the selection logic: a non-integer index is REFUSED, not merely
7
+ // finite-checked; the auto-pick takes the first VISIBLE page, not the first added; an
8
+ // empty `visible-child-name` means the same thing to the declarative and the property
9
+ // path; and the auto-pick NOTIFIES, so a bound switcher needs no manual `refresh()`.
15
10
  import { describe, expect, it } from '@gjsify/unit';
16
11
 
17
12
  import type { ViewStackVectorChange, ViewStackVectorOp, ViewStackVectorPage } from '@gjsify/adwaita-core/conformance';
@@ -33,12 +28,10 @@ interface MountedStack {
33
28
  /**
34
29
  * Mount a stack whose pages are DECLARED as markup, driving the attribute path.
35
30
  *
36
- * Attributes are set through `setAttribute` rather than an HTML string on
37
- * purpose: several vectors hinge on an exact name — the empty string, the NFD
38
- * spelling of `Übersicht` — which an attribute value in parsed markup is not a
39
- * reliable carrier for. The change listener is attached BEFORE the element is
40
- * connected so the auto-pick notification is observable, which is the whole
41
- * point of the fix that made it fire at all.
31
+ * Attributes go through `setAttribute` rather than an HTML string: several vectors hinge
32
+ * on an exact name — the empty string, the NFD spelling of `Übersicht` — which parsed
33
+ * markup is not a reliable carrier for. The change listener is attached BEFORE connect,
34
+ * so the auto-pick notification is observable at all.
42
35
  */
43
36
  function mountStack(pages: readonly ViewStackVectorPage[], visibleChildName?: string): MountedStack {
44
37
  const host = document.createElement('div');
@@ -110,10 +103,9 @@ export const AdwViewStackTest = async () => {
110
103
  expect(stack.pages.length).toBe(vector.count);
111
104
  expect(stack.pages[stack.visibleChildIndex]?.title ?? '').toBe(vector.visibleTitle);
112
105
 
113
- // The DOM must agree with the model: exactly the selected page is
114
- // shown, and nothing at all when the selection is -1. A stack that
115
- // stored an unreachable index (the 1.5 bug) showed NOTHING while
116
- // still reporting a selection.
106
+ // The DOM must agree with the model: exactly the selected page is shown,
107
+ // and nothing at all when the selection is -1. A stack storing an
108
+ // unreachable index shows NOTHING while still reporting a selection.
117
109
  const expectedShown = vector.pages.map(() => false).slice(0, vector.count);
118
110
  if (vector.visibleIndex >= 0) expectedShown[vector.visibleIndex] = true;
119
111
  expect(shownPages(stack)).toStrictEqual(expectedShown);
@@ -160,11 +152,10 @@ export const AdwViewStackTest = async () => {
160
152
 
161
153
  await describe('adw-view-stack imperative pages (Adw.ViewStack.add)', async () => {
162
154
  await it('keeps pages added BEFORE the stack enters the document', () => {
163
- // The regression: connectedCallback used to overwrite the page list
164
- // with whatever <adw-view-stack-page> children it found — none, for
165
- // imperative use — and then replaceChildren() the subtree away, so
166
- // this exact order silently destroyed the caller's element.
167
- // libadwaita has no parse-once phase (adw-view-stack.c:2160-2251).
155
+ // connectedCallback must not overwrite the page list from whatever
156
+ // <adw-view-stack-page> children it finds — none, for imperative use — and
157
+ // then replaceChildren() the subtree away, destroying the caller's element.
158
+ // libadwaita has no parse-once phase.
168
159
  const stack = document.createElement('adw-view-stack') as AdwViewStack;
169
160
  const content = document.createElement('div');
170
161
  content.textContent = 'learn body';
@@ -221,10 +212,9 @@ export const AdwViewStackTest = async () => {
221
212
  });
222
213
 
223
214
  await it('treats an EMPTY declared name as a legal lookup, not as "no name"', () => {
224
- // The declarative path used `initialName ? … : -1`, so an explicit
225
- // empty name fell through to page 0 while the PROPERTY path matched
226
- // the page named '' — one port, two answers for one input.
227
- // g_strcmp0("", "") is 0 (adw-view-stack.c:901).
215
+ // A declarative `initialName ? … : -1` sends an explicit empty name to page 0
216
+ // while the PROPERTY path matches the page named '' — two answers, one input.
217
+ // g_strcmp0("", "") is 0.
228
218
  const { stack, host } = mountStack([{ name: 'a' }, { name: '' }], '');
229
219
  expect(stack.visibleChildIndex).toBe(1);
230
220
  expect(shownPages(stack)).toStrictEqual([false, true]);
@@ -235,8 +225,7 @@ export const AdwViewStackTest = async () => {
235
225
  const { stack, host } = mountStack([{ name: 'a' }, { name: '' }]);
236
226
  expect(stack.getAttribute('visible-child-name')).toBe('a');
237
227
  stack.visibleChildIndex = 1;
238
- // The old reflection was guarded by `if (current && …)`, so a stack of
239
- // nameless pages never reflected its selection at all.
228
+ // Reflection guarded by `if (current && …)` never fires for nameless pages.
240
229
  expect(stack.getAttribute('visible-child-name')).toBe('');
241
230
  host.remove();
242
231
  });
@@ -259,9 +248,9 @@ export const AdwViewStackTest = async () => {
259
248
  });
260
249
 
261
250
  await it('does not bounce the selection back when a duplicate name is reflected', () => {
262
- // Reflection writes the attribute, which re-enters
263
- // attributeChangedCallback; with duplicate names a naive re-lookup
264
- // would resolve to the FIRST match and undo the selection.
251
+ // Reflection writes the attribute and re-enters attributeChangedCallback;
252
+ // with duplicate names a naive re-lookup resolves to the FIRST match and
253
+ // undoes the selection.
265
254
  const { stack, host } = mountStack([{ name: 'dup' }, { name: 'dup' }]);
266
255
  stack.visibleChildIndex = 1;
267
256
  expect(stack.visibleChildIndex).toBe(1);
@@ -3,15 +3,9 @@
3
3
  // vectors the core suite and the NativeScript suite assert against
4
4
  // (`@gjsify/adwaita-core/conformance`).
5
5
  //
6
- // The two renderers used to carry independent copies of everything a switcher
7
- // derives, and FIVE libadwaita rules lived in neither of them: the
8
- // button-visibility predicate `visible && (title != NULL || icon-name != NULL)`,
9
- // the `image-missing` icon fallback, the bar's `reveal && more-than-one-visible-
10
- // page` gate, the inline switcher's two index spaces, and the badge /
11
- // needs-attention label with the screen-reader description AdwIndicatorBin
12
- // derives from them. On top of that the browser switcher CLAMPED an out-of-range
13
- // index into range where libadwaita refuses. Nothing failed, because nothing
14
- // compared them. This suite is that comparison.
6
+ // The suite exists to keep the two renderers from drifting: they used to carry
7
+ // independent copies of every switcher derivation, and five libadwaita rules lived in
8
+ // neither because nothing compared them.
15
9
  import { describe, expect, it } from '@gjsify/unit';
16
10
 
17
11
  import {
@@ -44,8 +38,8 @@ import type { AdwViewStack } from './elements/adw-view-stack.js';
44
38
  function declarePage(tag: string, page: ViewSwitcherVectorPage): HTMLElement {
45
39
  const element = document.createElement(tag);
46
40
  element.setAttribute('name', page.name);
47
- // `getAttribute` returns null for an absent attribute, which IS C's NULL —
48
- // so an absent or explicitly-null title must not become `title=""`.
41
+ // `getAttribute` returns null for an absent attribute, which IS C's NULL, so an
42
+ // absent or explicitly-null title must not become `title=""`.
49
43
  if (typeof page.title === 'string') element.setAttribute('title', page.title);
50
44
  if (typeof page.iconName === 'string') element.setAttribute('icon-name', page.iconName);
51
45
  if (page.visible === false) element.setAttribute('hidden', '');
@@ -77,8 +71,7 @@ function mountSwitcher(pages: readonly ViewSwitcherVectorPage[], policy?: string
77
71
  for (const page of pages) element.appendChild(declarePage('adw-view-switcher-page', page));
78
72
 
79
73
  const changes: ViewSwitcherVectorChange[] = [];
80
- // The listener is attached BEFORE the element connects so the auto-pick
81
- // notification is observable — the switcher used to be silent there.
74
+ // Attached BEFORE connect, so the auto-pick notification is observable.
82
75
  element.addEventListener('notify::visible-child', (event) => {
83
76
  const detail = (event as CustomEvent<{ index: number; name: string; title: string; interactive: boolean }>)
84
77
  .detail;
@@ -142,16 +135,14 @@ function partText(button: HTMLElement, selector: string): string | null {
142
135
  export const AdwViewSwitcherTest = async () => {
143
136
  await describe('adw-view-switcher buttons (libadwaita conformance vectors)', async () => {
144
137
  for (const vector of VIEW_SWITCHER_BUTTON_VECTORS) {
145
- // A `selected: -1` row cannot be staged here: this element bundles its
146
- // own stack, whose auto-pick takes the first visible page, and the only
147
- // way to reach "nothing selected" is to hide every page — which would
148
- // change the very `visible` flags the row is asserting. The core suite
149
- // drives that row directly.
138
+ // `selected: -1` cannot be staged here: this element bundles its own stack,
139
+ // whose auto-pick takes the first visible page, and reaching "nothing
140
+ // selected" needs every page hidden — which changes the `visible` flags the
141
+ // row asserts. The core suite drives that row.
150
142
  if (vector.selected < 0) continue;
151
143
  await it(vector.rule, () => {
152
144
  const { element, host } = mountSwitcher(vector.pages, vector.policy);
153
- // The vectors carry an explicit selection; drive it through the
154
- // element rather than assuming the auto-pick landed there.
145
+ // Drive the vector's selection rather than assume the auto-pick.
155
146
  element.active = vector.selected;
156
147
 
157
148
  const buttons = buttonsOf(element, '.adw-view-switcher-button');
@@ -159,11 +150,11 @@ export const AdwViewSwitcherTest = async () => {
159
150
 
160
151
  vector.buttons.forEach((model, index) => {
161
152
  const button = buttons[index]!;
162
- // The button-visibility rule, which BOTH ports lacked.
153
+ // `visible && (title != NULL || icon-name != NULL)`.
163
154
  expect(button.hidden).toBe(!model.visible);
164
155
  expect(button.getAttribute('aria-selected')).toBe(String(model.selected));
165
156
  expect(partText(button, '.adw-view-switcher-label')).toBe(model.label);
166
- // The icon node always exists and always names an icon —
157
+ // The icon node always exists and always names an icon:
167
158
  // `image-missing` when the page has none.
168
159
  const icon = button.querySelector('.adw-icon') as HTMLElement;
169
160
  expect(icon.classList.contains(`adw-icon--${normalizeIconName(model.iconName)}`)).toBe(true);
@@ -171,16 +162,14 @@ export const AdwViewSwitcherTest = async () => {
171
162
  expect(button.getAttribute('aria-description')).toBe(model.description || null);
172
163
  });
173
164
 
174
- // The policy style class lives on the single `viewswitcher` node.
175
165
  expect(element.classList.contains(vector.policy)).toBe(true);
176
166
  host.remove();
177
167
  });
178
168
  }
179
169
 
180
170
  await it('hides a titleless, iconless button in the LAYOUT, not just visually', () => {
181
- // The `[hidden]` UA rule loses to the component's own author
182
- // `display: inline-flex` unless the stylesheet re-asserts it — the
183
- // same cascade trap the switcher BAR was fixed for.
171
+ // The `[hidden]` UA rule loses to the component's own author `display:
172
+ // inline-flex` unless the stylesheet re-asserts it.
184
173
  const { element, host } = mountSwitcher([{ name: 'a', title: 'A' }, { name: 'b' }]);
185
174
  const buttons = buttonsOf(element, '.adw-view-switcher-button');
186
175
  expect(getComputedStyle(buttons[1]!).display).toBe('none');
@@ -202,16 +191,16 @@ export const AdwViewSwitcherTest = async () => {
202
191
  expect(element.active).toBe(vector.selected);
203
192
  expect(element.visibleChildName).toBe(vector.selectedName);
204
193
 
205
- // The DOM must agree with the model: exactly the selected page's
206
- // body is shown, and none at all when the selection is -1.
194
+ // Exactly the selected page's body is shown, none when the selection
195
+ // is -1.
207
196
  const bodies = buttonsOf(element, '.adw-view-switcher-page');
208
197
  const shown = bodies.map((body) => !(body as HTMLElement).hidden);
209
198
  const expectedShown = vector.pages.map((_page, index) => index === vector.selected);
210
199
  expect(shown).toStrictEqual(expectedShown);
211
200
 
212
- // The reflected attribute must not claim a selection that was
213
- // refused — the old element clamped instead, so `active="99"`
214
- // silently became the last page.
201
+ // The reflected attribute must not claim a REFUSED selection: libadwaita
202
+ // refuses an out-of-range index, where clamping makes `active="99"` the
203
+ // last page silently.
215
204
  expect(element.getAttribute('active')).toBe(String(vector.selected));
216
205
  host.remove();
217
206
  });
@@ -237,8 +226,8 @@ export const AdwViewSwitcherTest = async () => {
237
226
  });
238
227
 
239
228
  await it('scopes its page query, so a nested switcher keeps its own pages', () => {
240
- // Both switchers use the same tag for their pages; an unscoped
241
- // querySelectorAll let the outer one adopt the inner one's.
229
+ // Both switchers use the same tag for their pages, so an unscoped
230
+ // querySelectorAll lets the outer one adopt the inner one's.
242
231
  const host = document.createElement('div');
243
232
  document.body.appendChild(host);
244
233
 
@@ -286,8 +275,7 @@ export const AdwViewSwitcherTest = async () => {
286
275
  const { element, host } = mountInline(vector.pages, vector.displayMode);
287
276
 
288
277
  const toggles = buttonsOf(element, '.adw-inline-view-switcher-toggle');
289
- // A hidden page produces NO toggle — the whole reason the two
290
- // index spaces exist. The old element made one per declared page.
278
+ // A hidden page produces NO toggle — why the two index spaces exist.
291
279
  expect(toggles).toHaveLength(vector.toggles.length);
292
280
 
293
281
  vector.toggles.forEach((model, index) => {
@@ -307,15 +295,15 @@ export const AdwViewSwitcherTest = async () => {
307
295
  expect(toggle.getAttribute('aria-description')).toBe(model.description || null);
308
296
  });
309
297
 
310
- // Every panel still exists — only the toggles are compacted.
298
+ // Every panel still exists; only the toggles are compacted.
311
299
  expect(buttonsOf(element, '.adw-inline-view-switcher-page')).toHaveLength(vector.pages.length);
312
300
  host.remove();
313
301
  });
314
302
  }
315
303
 
316
304
  for (const vector of INLINE_TOOLTIP_VECTORS) {
317
- // A NULL title has no attribute spelling that differs from an absent
318
- // one, and both are covered by the toggle vectors above.
305
+ // A NULL title has no attribute spelling distinct from an absent one; the
306
+ // toggle vectors above cover both.
319
307
  if (vector.title === null) continue;
320
308
  await it(`tooltip in ${vector.displayMode} — ${vector.rule}`, () => {
321
309
  const { element, host } = mountInline(
@@ -329,8 +317,6 @@ export const AdwViewSwitcherTest = async () => {
329
317
  }
330
318
 
331
319
  await it("defaults to 'labels' and REFUSES an unknown display mode", () => {
332
- // The old element defaulted to 'both' and replaced the current mode
333
- // with 'both' for any unrecognised value.
334
320
  const { element, host } = mountInline([
335
321
  { name: 'a', title: 'A', iconName: 'go-home-symbolic' },
336
322
  { name: 'b', title: 'B' },
@@ -360,8 +346,8 @@ export const AdwViewSwitcherTest = async () => {
360
346
  changes.length = 0;
361
347
 
362
348
  const toggles = buttonsOf(element, '.adw-inline-view-switcher-toggle');
363
- // Toggle 1 is page 2 — clicking it must select the PAGE, not the
364
- // toggle index, which is what the missing child-index mapping did.
349
+ // Toggle 1 is page 2: a click selects the PAGE, not the toggle index, which is
350
+ // what a missing child-index mapping would select.
365
351
  toggles[1]!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
366
352
  expect(element.active).toBe(2);
367
353
  expect(element.activeToggle).toBe(1);
@@ -374,9 +360,8 @@ export const AdwViewSwitcherTest = async () => {
374
360
  { name: 'a', title: 'A' },
375
361
  { name: 'b', title: 'B' },
376
362
  ]);
377
- // Hiding the page a switcher is showing normally moves the selection;
378
- // force the "selected page is hidden" state the C sentinel exists for
379
- // by hiding every OTHER page first.
363
+ // Hiding the shown page normally moves the selection, so reach the state the C
364
+ // sentinel exists for by hiding every OTHER page first.
380
365
  element.setPageVisible('a', false);
381
366
  expect(element.active).toBe(1);
382
367
  element.setPageVisible('b', false);
@@ -410,8 +395,8 @@ export const AdwViewSwitcherTest = async () => {
410
395
 
411
396
  bar.reveal = vector.reveal;
412
397
  expect(bar.reveal).toBe(vector.reveal);
413
- // The derivation both ports skipped: a one-page stack keeps the
414
- // bar collapsed however loudly the layout asks.
398
+ // `reveal && more-than-one-visible-page`: a one-page stack keeps the bar
399
+ // collapsed however loudly the layout asks.
415
400
  expect(bar.revealed).toBe(vector.revealed);
416
401
  expect(bar.hidden).toBe(!vector.revealed);
417
402
  expect(getComputedStyle(bar).display).toBe(vector.revealed ? 'block' : 'none');
@@ -420,9 +405,8 @@ export const AdwViewSwitcherTest = async () => {
420
405
  }
421
406
 
422
407
  await it('keeps tracking its stack after being detached and re-inserted', () => {
423
- // `disconnectedCallback` dropped the listener and `connectedCallback`
424
- // only re-added it when `ref && !this._stack` — which is never true
425
- // on a re-attach, so the bar stopped tracking for good.
408
+ // Re-adding the listener only when `ref && !this._stack` is never true on a
409
+ // re-attach, and the bar goes deaf.
426
410
  const host = document.createElement('div');
427
411
  document.body.appendChild(host);
428
412
 
@@ -460,9 +444,6 @@ export const AdwViewSwitcherTest = async () => {
460
444
  scheduler: { schedule(cb: () => void, ms: number): unknown; cancel(handle: unknown): void };
461
445
  };
462
446
  const clock = createViewSwitcherClock();
463
- // The seam that did not exist: the elements built their state inline
464
- // with the DOM scheduler, so VIEW_SWITCHER_DRAG_VECTORS was driven by
465
- // the core suite alone while a header claimed all three drove it.
466
447
  switcher.scheduler = clock;
467
448
  for (const page of pages) switcher.appendChild(declarePage('adw-view-switcher-page', page));
468
449
  switcher.setAttribute('active', String(initial));
@@ -480,9 +461,8 @@ export const AdwViewSwitcherTest = async () => {
480
461
  continue;
481
462
  }
482
463
  const button = buttons[step.index];
483
- // `relatedTarget` OUTSIDE the button, which is what a real
484
- // cross-button move looks like — the internal case is the
485
- // separate test below.
464
+ // `relatedTarget` OUTSIDE the button — a real cross-button move; the
465
+ // internal case is tested below.
486
466
  button?.dispatchEvent(
487
467
  new DragEvent(step.kind === 'enter' ? 'dragenter' : 'dragleave', {
488
468
  bubbles: true,
@@ -496,11 +476,10 @@ export const AdwViewSwitcherTest = async () => {
496
476
  }
497
477
 
498
478
  await it('a drag sliding from the icon to the LABEL does not restart the dwell', () => {
499
- // The bug the vectors could not see: the element bound enter/leave on
500
- // a button that CONTAINS an icon and a label, so crossing between them
501
- // fired leave + enter, `ViewSwitcherDragSwitch.leave` cleared the timer
502
- // and `enter` re-armed a fresh 500 ms. A drag that merely slid across
503
- // the button never switched at all.
479
+ // A button CONTAINS an icon and a label, so crossing between them fires
480
+ // leave + enter; without the relatedTarget check that cleared the timer and
481
+ // re-armed a fresh 500 ms, so a drag across the button never switched. The
482
+ // vectors cannot see this.
504
483
  const pages: ViewSwitcherVectorPage[] = [
505
484
  { name: 'one', title: 'One' },
506
485
  { name: 'two', title: 'Two' },
@@ -524,7 +503,7 @@ export const AdwViewSwitcherTest = async () => {
524
503
 
525
504
  await it('a page attribute changed AFTER connect reaches the button', async () => {
526
505
  // `observedAttributes` without an `attributeChangedCallback` is a dead
527
- // declaration; C rebinds on every page notify (adw-view-switcher.c:184-193).
506
+ // declaration, where C rebinds on every page notify.
528
507
  const host = document.createElement('div');
529
508
  document.body.appendChild(host);
530
509
  const switcher = document.createElement('adw-view-switcher') as AdwViewSwitcher;
@@ -537,10 +516,8 @@ export const AdwViewSwitcherTest = async () => {
537
516
  (switcher.querySelector('.adw-view-switcher-label') as HTMLElement | null)?.textContent;
538
517
  expect(labelOf()).toBe('One');
539
518
  page.setAttribute('title', 'Renamed');
540
- // A MutationObserver delivers on a MICROTASK, where GTK's `notify`
541
- // is synchronous. One microtask is invisible to a user and is what
542
- // the DOM offers for watching a detached node; nothing here depends
543
- // on the repaint happening inside the setter.
519
+ // A MutationObserver delivers on a MICROTASK where GTK's `notify` is sync;
520
+ // nothing depends on the repaint happening inside the setter.
544
521
  await Promise.resolve();
545
522
  expect(labelOf()).toBe('Renamed');
546
523
  host.remove();
@@ -559,8 +536,8 @@ export const AdwViewSwitcherTest = async () => {
559
536
 
560
537
  const indicator = switcher.querySelector('.adw-view-switcher-indicator') as HTMLElement;
561
538
  expect(indicator.textContent).toBe(vector.badgeLabel);
562
- // The DESCRIPTION is what a screen reader announces; a badge with
563
- // no accessible text is a dot nobody hears.
539
+ // The DESCRIPTION is what a screen reader announces: a badge with no
540
+ // accessible text is a dot nobody hears.
564
541
  const button = switcher.querySelector('.adw-view-switcher-button') as HTMLElement;
565
542
  expect(button.getAttribute('aria-description') ?? '').toBe(vector.description);
566
543
  host.remove();
@@ -568,11 +545,10 @@ export const AdwViewSwitcherTest = async () => {
568
545
  }
569
546
 
570
547
  await it('names the three tables that stay the core suite’s', () => {
571
- // MNEMONIC and ICON are pure string derivations with no DOM surface
572
- // beyond the label this suite already asserts through
573
- // VIEW_SWITCHER_BUTTON_VECTORS; BUTTON_VISIBILITY and REBUILD are
574
- // asserted through the rendered button set by the same table. Named
575
- // so the omission is a decision rather than a gap.
548
+ // MNEMONIC and ICON are string derivations with no DOM surface beyond the
549
+ // label VIEW_SWITCHER_BUTTON_VECTORS already asserts; BUTTON_VISIBILITY and
550
+ // REBUILD are asserted through that table's rendered button set. Named so
551
+ // the omission is a decision rather than a gap.
576
552
  expect(VIEW_SWITCHER_MNEMONIC_VECTORS.length).toBeGreaterThan(0);
577
553
  expect(VIEW_SWITCHER_ICON_VECTORS.length).toBeGreaterThan(0);
578
554
  expect(VIEW_SWITCHER_BUTTON_VISIBILITY_VECTORS.length).toBeGreaterThan(0);
@@ -582,10 +558,9 @@ export const AdwViewSwitcherTest = async () => {
582
558
 
583
559
  await describe('<adw-view-switcher-bar> reveal follows the pages model, not just the selection', async () => {
584
560
  await it('retracts when a page is REMOVED without moving the selection', () => {
585
- // `update_bar_revealed` re-runs on the pages model's `items-changed`
586
- // (adw-view-switcher-bar.c:340). Both ports listened only on
587
- // `notify::visible-child`, so the bar went stale and a manual
588
- // `refresh()` was the documented workaround.
561
+ // `update_bar_revealed` re-runs on the pages model's `items-changed`.
562
+ // Listening only on `notify::visible-child` leaves the bar stale, with a
563
+ // manual `refresh()` as the workaround.
589
564
  const host = document.createElement('div');
590
565
  document.body.appendChild(host);
591
566
  const stack = document.createElement('adw-view-stack') as AdwViewStack;
@@ -627,13 +602,10 @@ export const AdwViewSwitcherTest = async () => {
627
602
  });
628
603
  });
629
604
 
630
- // MOVING A SWITCHER MUST NOT MAKE IT DEAF.
631
- //
632
- // `disconnectedCallback` drops the page observer, and `connectedCallback`
633
- // returned early once the element was built — so a switcher that had merely
634
- // been moved between parents (a slideshow slide, a client-side route change)
635
- // stopped tracking its pages permanently, and silently. Same shape as the
636
- // overlay split view's ResizeObserver, and found with it.
605
+ // `disconnectedCallback` drops the page observer, and `connectedCallback` returned
606
+ // early once the element was built — so a switcher merely MOVED between parents (a
607
+ // slideshow slide, a client-side route change) stopped tracking its pages
608
+ // permanently and silently. Same shape as the overlay split view's ResizeObserver.
637
609
  await describe('adw-view-switcher — a moved switcher keeps watching its pages', async () => {
638
610
  await it('still notices a page attribute change after being re-parented', async () => {
639
611
  const from = document.createElement('div');