@gjsify/adwaita-web 0.36.0 → 0.38.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 (151) hide show
  1. package/dist/adwaita-web.css +504 -34
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/accent.d.ts +28 -0
  4. package/lib/types/adw-accent.spec.d.ts +2 -0
  5. package/lib/types/adw-shortcut-label.spec.d.ts +2 -0
  6. package/lib/types/breakpoints.d.ts +10 -12
  7. package/lib/types/elements/adw-about-dialog.d.ts +19 -36
  8. package/lib/types/elements/adw-action-row.d.ts +9 -12
  9. package/lib/types/elements/adw-alert-dialog.d.ts +0 -12
  10. package/lib/types/elements/adw-avatar.d.ts +1 -0
  11. package/lib/types/elements/adw-bottom-sheet.d.ts +7 -12
  12. package/lib/types/elements/adw-button-content.d.ts +3 -4
  13. package/lib/types/elements/adw-button-row.d.ts +1 -1
  14. package/lib/types/elements/adw-carousel.d.ts +28 -49
  15. package/lib/types/elements/adw-checks.d.ts +2 -8
  16. package/lib/types/elements/adw-combo-row.d.ts +6 -6
  17. package/lib/types/elements/adw-data-grid.d.ts +5 -10
  18. package/lib/types/elements/adw-drop-down.d.ts +3 -11
  19. package/lib/types/elements/adw-entry-row.d.ts +20 -23
  20. package/lib/types/elements/adw-icon.d.ts +2 -4
  21. package/lib/types/elements/adw-inline-view-switcher.d.ts +18 -31
  22. package/lib/types/elements/adw-navigation-split-view.d.ts +7 -12
  23. package/lib/types/elements/adw-navigation-view.d.ts +2 -10
  24. package/lib/types/elements/adw-overlay-split-view.d.ts +28 -52
  25. package/lib/types/elements/adw-password-entry-row.d.ts +4 -5
  26. package/lib/types/elements/adw-popover.d.ts +2 -7
  27. package/lib/types/elements/adw-preferences-dialog.d.ts +21 -34
  28. package/lib/types/elements/adw-preferences-group.d.ts +12 -14
  29. package/lib/types/elements/adw-progress-bar.d.ts +3 -4
  30. package/lib/types/elements/adw-shortcut-label.d.ts +20 -0
  31. package/lib/types/elements/adw-sidebar.d.ts +5 -10
  32. package/lib/types/elements/adw-spinner.d.ts +3 -5
  33. package/lib/types/elements/adw-split-button.d.ts +6 -16
  34. package/lib/types/elements/adw-switch-row.d.ts +3 -3
  35. package/lib/types/elements/adw-tab-view.d.ts +16 -26
  36. package/lib/types/elements/adw-toast-overlay.d.ts +8 -13
  37. package/lib/types/elements/adw-view-stack.d.ts +7 -17
  38. package/lib/types/elements/adw-view-switcher-bar.d.ts +4 -8
  39. package/lib/types/elements/adw-view-switcher.d.ts +18 -38
  40. package/lib/types/elements/adw-wrap-box.d.ts +7 -17
  41. package/lib/types/elements/view-switcher-dom.d.ts +10 -13
  42. package/lib/types/index.d.ts +3 -0
  43. package/lib/types/source-view/hex.d.ts +7 -13
  44. package/lib/types/style-classes.spec.d.ts +11 -0
  45. package/lib/types/styles.generated.d.ts +1 -1
  46. package/package.json +18 -7
  47. package/scss/_about_dialog.scss +3 -2
  48. package/scss/_action_row.scss +33 -0
  49. package/scss/_avatar.scss +12 -0
  50. package/scss/_button.scss +44 -22
  51. package/scss/_card.scss +11 -8
  52. package/scss/_checks.scss +2 -4
  53. package/scss/_dialog.scss +3 -2
  54. package/scss/_headerbar.scss +23 -0
  55. package/scss/_popover.scss +8 -4
  56. package/scss/_preferences.scss +7 -7
  57. package/scss/_progress_bar.scss +9 -6
  58. package/scss/_reset.scss +1 -1
  59. package/scss/_shortcut_label.scss +80 -0
  60. package/scss/_sidebar.scss +57 -0
  61. package/scss/_status_page.scss +20 -0
  62. package/scss/_style_classes.scss +407 -0
  63. package/scss/_switch.scss +4 -3
  64. package/scss/_theme.scss +20 -2
  65. package/scss/_variables.scss +34 -1
  66. package/scss/adwaita-skin.scss +12 -0
  67. package/src/accent.ts +74 -0
  68. package/src/adw-about-dialog.spec.ts +34 -49
  69. package/src/adw-accent.spec.ts +105 -0
  70. package/src/adw-action-rows.spec.ts +25 -33
  71. package/src/adw-avatar.spec.ts +51 -20
  72. package/src/adw-banner.spec.ts +5 -8
  73. package/src/adw-button-content.spec.ts +9 -12
  74. package/src/adw-button-row.spec.ts +2 -4
  75. package/src/adw-carousel.spec.ts +40 -53
  76. package/src/adw-checks.spec.ts +36 -47
  77. package/src/adw-drop-down.spec.ts +25 -32
  78. package/src/adw-header-bar.spec.ts +5 -7
  79. package/src/adw-icon.spec.ts +23 -32
  80. package/src/adw-progress-bar.spec.ts +10 -12
  81. package/src/adw-row-state.spec.ts +9 -10
  82. package/src/adw-shortcut-label.spec.ts +112 -0
  83. package/src/adw-sidebar.spec.ts +17 -24
  84. package/src/adw-switch.spec.ts +11 -17
  85. package/src/adw-tab-view.spec.ts +7 -10
  86. package/src/adw-toast-overlay.spec.ts +7 -11
  87. package/src/adw-view-switcher-bar.spec.ts +10 -14
  88. package/src/adw-wrap-box.spec.ts +25 -36
  89. package/src/bottom-sheet.spec.ts +30 -36
  90. package/src/breakpoints.ts +15 -18
  91. package/src/chrome.spec.ts +13 -21
  92. package/src/elements/adw-about-dialog.ts +71 -114
  93. package/src/elements/adw-action-row.ts +22 -37
  94. package/src/elements/adw-alert-dialog.ts +25 -50
  95. package/src/elements/adw-avatar.ts +40 -15
  96. package/src/elements/adw-banner.ts +14 -18
  97. package/src/elements/adw-bottom-sheet.ts +31 -52
  98. package/src/elements/adw-button-content.ts +30 -37
  99. package/src/elements/adw-button-row.ts +14 -24
  100. package/src/elements/adw-carousel.ts +87 -148
  101. package/src/elements/adw-checks.ts +43 -56
  102. package/src/elements/adw-clamp.ts +8 -9
  103. package/src/elements/adw-combo-row.ts +17 -25
  104. package/src/elements/adw-data-grid.ts +26 -38
  105. package/src/elements/adw-dialog.ts +25 -56
  106. package/src/elements/adw-drop-down.ts +31 -51
  107. package/src/elements/adw-entry-row.ts +39 -46
  108. package/src/elements/adw-expander-row.ts +30 -53
  109. package/src/elements/adw-icon.ts +30 -53
  110. package/src/elements/adw-inline-view-switcher.ts +38 -67
  111. package/src/elements/adw-menu-button.ts +28 -58
  112. package/src/elements/adw-navigation-split-view.ts +35 -47
  113. package/src/elements/adw-navigation-view.ts +12 -28
  114. package/src/elements/adw-overlay-split-view.ts +100 -175
  115. package/src/elements/adw-password-entry-row.ts +18 -28
  116. package/src/elements/adw-popover.ts +28 -44
  117. package/src/elements/adw-preferences-dialog.ts +41 -66
  118. package/src/elements/adw-preferences-group.ts +33 -46
  119. package/src/elements/adw-progress-bar.ts +27 -47
  120. package/src/elements/adw-shortcut-label.ts +138 -0
  121. package/src/elements/adw-sidebar.ts +31 -39
  122. package/src/elements/adw-spin-row.ts +7 -14
  123. package/src/elements/adw-spinner.ts +31 -43
  124. package/src/elements/adw-split-button.ts +45 -75
  125. package/src/elements/adw-switch-row.ts +29 -46
  126. package/src/elements/adw-switch.ts +28 -47
  127. package/src/elements/adw-tab-view.ts +62 -103
  128. package/src/elements/adw-toast-overlay.ts +28 -35
  129. package/src/elements/adw-toggle-group.ts +9 -16
  130. package/src/elements/adw-view-stack.ts +32 -51
  131. package/src/elements/adw-view-switcher-bar.ts +26 -50
  132. package/src/elements/adw-view-switcher.ts +60 -110
  133. package/src/elements/adw-window-title.ts +4 -4
  134. package/src/elements/adw-wrap-box.ts +33 -59
  135. package/src/elements/view-switcher-dom.ts +15 -22
  136. package/src/entry-rows.spec.ts +18 -24
  137. package/src/index.ts +24 -15
  138. package/src/navigation-view.spec.ts +7 -10
  139. package/src/preferences-search.ts +11 -15
  140. package/src/preferences.spec.ts +13 -15
  141. package/src/source-view/adw-source-view.ts +6 -19
  142. package/src/source-view/hex.ts +7 -13
  143. package/src/split-button.spec.ts +32 -44
  144. package/src/split-views.spec.ts +83 -149
  145. package/src/style-classes.spec.ts +181 -0
  146. package/src/style-isolation.spec.ts +3 -6
  147. package/src/styles.generated.ts +1 -1
  148. package/src/tab-view.spec.ts +26 -35
  149. package/src/test.browser.mts +6 -0
  150. package/src/view-stack.spec.ts +23 -34
  151. package/src/view-switcher.spec.ts +56 -84
@@ -1,15 +1,9 @@
1
1
  // DOM-level conformance tests for <adw-carousel>, driven by the SAME vectors the
2
2
  // NativeScript renderer asserts against (`@gjsify/adwaita-core/conformance`).
3
3
  //
4
- // The two renderers used to carry independent, differently-wrong copies of the
5
- // carousel's arithmetic, and nothing compared them: this element defaulted
6
- // `allow-scroll-wheel` to FALSE where libadwaita defaults it TRUE (so a plain
7
- // <adw-carousel> ignored the wheel while its own header claimed the opposite),
8
- // resolved a half-way position to the LATER page where every libadwaita lookup
9
- // takes the earlier one, dropped `<adw-carousel position="2">` on the floor,
10
- // dropped the horizontal-delta fallback its comment described, and emitted
11
- // `page-changed` only when the index changed — which made the documented -1
12
- // unreachable. This suite is that comparison.
4
+ // The suite exists because the two renderers used to carry independent,
5
+ // differently-wrong copies of the carousel's arithmetic with nothing comparing them;
6
+ // each rule that drifted is pinned by a test below.
13
7
  //
14
8
  // Scrolls are made instant by overriding the track's `scroll-behavior`; the
15
9
  // element sets it as an inline style precisely so it stays overridable.
@@ -44,10 +38,9 @@ function card(label: string): HTMLElement {
44
38
  }
45
39
 
46
40
  /**
47
- * Mount a carousel with the given page labels.
48
- *
49
- * The host gets an explicit size because the page pitch is MEASURED — a carousel
50
- * with no width has no distance, and every scroll would be a no-op.
41
+ * Mount a carousel with the given page labels. The host gets an explicit size because
42
+ * the page pitch is MEASURED: a carousel with no width has no distance, so every scroll
43
+ * would be a no-op.
51
44
  */
52
45
  function mount(labels: readonly string[], attributes: Record<string, string> = {}) {
53
46
  const host = document.createElement('div');
@@ -61,10 +54,9 @@ function mount(labels: readonly string[], attributes: Record<string, string> = {
61
54
  host.appendChild(carousel);
62
55
 
63
56
  const track = carousel.querySelector('.adw-carousel-track') as HTMLElement;
64
- // Instant scrolls: the assertions are about WHERE it lands, never about the
65
- // animation getting there. Snapping is off for the same reason — `mandatory`
66
- // re-snaps a programmatic `scrollLeft` to the nearest page, which is exactly
67
- // the fractional position several of these tests are about.
57
+ // Instant scrolls: the assertions are about WHERE it lands, never the animation.
58
+ // Snapping is off for the same reason — `mandatory` re-snaps a programmatic
59
+ // `scrollLeft` to the nearest page, the very fractional position under test.
68
60
  track.style.setProperty('scroll-behavior', 'auto');
69
61
  track.style.setProperty('scroll-snap-type', 'none');
70
62
  return { carousel, host, track };
@@ -131,16 +123,15 @@ function applyPageOp(carousel: CarouselElement, op: CarouselPageOp, pages: Map<s
131
123
  case 'scrollTo':
132
124
  case 'setPosition':
133
125
  case 'settle':
134
- // Position ops need a settled scroll, so they are driven separately
135
- // by the scroll tests below rather than through this replay.
126
+ // Position ops need a settled scroll and are driven by the scroll tests
127
+ // below, not through this replay.
136
128
  return true;
137
129
  }
138
130
  }
139
131
 
140
132
  export const AdwCarouselTest = async () => {
141
133
  await describe('adw-carousel page list (libadwaita conformance vectors)', async () => {
142
- // Only the structural rows: the element's page methods are what the DOM
143
- // exposes, while position ops need a real scroll and are covered below.
134
+ // Only the structural rows: the page methods are what the DOM exposes.
144
135
  const structural = CAROUSEL_PAGE_LIST_VECTORS.filter((vector) =>
145
136
  vector.ops.every((op) => op.kind === 'insert' || op.kind === 'remove' || op.kind === 'reorder'),
146
137
  );
@@ -157,11 +148,11 @@ export const AdwCarouselTest = async () => {
157
148
 
158
149
  expect(results).toStrictEqual([...vector.opResults]);
159
150
  expect(carousel.nPages).toBe(vector.expected.nPages);
160
- // The DOM order IS the model order — a reorder that only moved
161
- // the model would leave the pages painted where they were.
151
+ // The DOM order IS the model order: a reorder that moved only the model
152
+ // would leave the pages painted where they were.
162
153
  expect(domOrder(carousel)).toStrictEqual([...vector.expected.ids]);
163
- // And the position compensation reached the scroll container: a
164
- // page revealed before the current one must not push it aside.
154
+ // The position compensation reaches the scroll container: a page revealed
155
+ // before the current one must not push it aside.
165
156
  expect(carousel.position).toBe(vector.expected.position);
166
157
  host.remove();
167
158
  });
@@ -170,17 +161,15 @@ export const AdwCarouselTest = async () => {
170
161
 
171
162
  await describe('adw-carousel page lookup (libadwaita conformance vectors)', async () => {
172
163
  for (const vector of CAROUSEL_PAGE_AT_POSITION_VECTORS) {
173
- // Only the rows whose snap points are a settled page list are
174
- // reachable through the DOM; the mid-reveal geometry rows need a
175
- // reveal ramp this port does not have.
164
+ // Only rows whose snap points are a settled page list are reachable through
165
+ // the DOM; mid-reveal geometry rows need a reveal ramp this port lacks.
176
166
  const settled = vector.snapPoints.length > 0 && vector.snapPoints.every((point, i) => point === i);
177
167
  if (!settled) continue;
178
168
 
179
169
  await it(`${vector.position} → page ${vector.page} — ${vector.rule}`, async () => {
180
- // Driven through the `position` property rather than a real
181
- // scroll: a scroll offset is quantised to device pixels, so the
182
- // ±1e-7 rows around the tie are not expressible as a drag. The
183
- // tie ITSELF is, and gets its own drag test below.
170
+ // Driven through the `position` property, not a real scroll: a scroll
171
+ // offset is quantised to device pixels, so the ±1e-7 rows around the tie
172
+ // are not expressible as a drag. The tie itself is, below.
184
173
  const { carousel, host } = mount(vector.snapPoints.map((_point, index) => `p${index}`));
185
174
  (carousel as unknown as { position: number }).position = vector.position;
186
175
  await flush();
@@ -192,8 +181,8 @@ export const AdwCarouselTest = async () => {
192
181
 
193
182
  await describe('adw-carousel scroll wheel (scroll_cb)', async () => {
194
183
  await it('pages on a mouse wheel WITHOUT being asked to — the default is TRUE', async () => {
195
- // The regression pin: this element used to require an explicit
196
- // `allow-scroll-wheel` attribute, inverting adw-carousel.c:1103-1106.
184
+ // This element used to require an explicit `allow-scroll-wheel` attribute,
185
+ // inverting the libadwaita default.
197
186
  const { carousel, host } = mount(['a', 'b', 'c']);
198
187
  expect(carousel.allowScrollWheel).toBe(true);
199
188
  expect(wheel(carousel, { deltaY: 120 })).toBe(true);
@@ -212,8 +201,8 @@ export const AdwCarouselTest = async () => {
212
201
  });
213
202
 
214
203
  await it('pages on a horizontal delta — the fallback the port dropped', async () => {
215
- // adw-carousel.c:554-559. The old code returned early whenever
216
- // |deltaY| <= |deltaX|, so a dx-only event did nothing at all.
204
+ // The old code returned early whenever |deltaY| <= |deltaX|, so a dx-only
205
+ // event did nothing at all.
217
206
  const { carousel, host } = mount(['a', 'b', 'c']);
218
207
  expect(wheel(carousel, { deltaX: 120 })).toBe(true);
219
208
  await flush();
@@ -222,8 +211,8 @@ export const AdwCarouselTest = async () => {
222
211
  });
223
212
 
224
213
  await it('locks out a second notch for 150 ms', async () => {
225
- // adw-carousel.c:22, :526. The old accumulator had no time component
226
- // at all, so a fast wheel paged as many times as it had deltas.
214
+ // The old accumulator had no time component, so a fast wheel paged as many
215
+ // times as it had deltas.
227
216
  const { carousel, host } = mount(['a', 'b', 'c']);
228
217
  expect(wheel(carousel, { deltaY: 120 })).toBe(true);
229
218
  expect(wheel(carousel, { deltaY: 120 })).toBe(false);
@@ -233,8 +222,8 @@ export const AdwCarouselTest = async () => {
233
222
  });
234
223
 
235
224
  await it('leaves a touchpad gesture to the native scroll-snap', async () => {
236
- // A touchpad is ignored outright (:537-538) because its own kinetic
237
- // scroll already drives the strip — here, the track's scroll-snap.
225
+ // A touchpad is ignored outright: its own kinetic scroll already drives the
226
+ // strip, here the track's scroll-snap.
238
227
  const { carousel, host } = mount(['a', 'b', 'c']);
239
228
  expect(wheel(carousel, { deltaX: 12, deltaY: 7 })).toBe(false);
240
229
  await flush();
@@ -243,8 +232,8 @@ export const AdwCarouselTest = async () => {
243
232
  });
244
233
 
245
234
  await it('does not consume a wheel event it did not act on', async () => {
246
- // GDK_EVENT_STOP only when it paged (:561-562 vs :576). The old code
247
- // called preventDefault BEFORE its threshold test.
235
+ // GDK_EVENT_STOP only when it paged; the old code called preventDefault()
236
+ // BEFORE its threshold test.
248
237
  const { carousel, host } = mount(['a', 'b', 'c'], { interactive: 'false' });
249
238
  expect(wheel(carousel, { deltaY: 120 })).toBe(false);
250
239
  host.remove();
@@ -253,8 +242,8 @@ export const AdwCarouselTest = async () => {
253
242
 
254
243
  await describe('adw-carousel page-changed (scroll_animation_done_cb)', async () => {
255
244
  await it('reports every completed scroll, including back onto the same page', async () => {
256
- // The old emission was gated on the ROUNDED index changing, so a
257
- // settle back onto the page it started from said nothing.
245
+ // Gating the emission on the ROUNDED index means a settle back onto the page
246
+ // it started from says nothing.
258
247
  const { carousel, host } = mount(['a', 'b', 'c']);
259
248
  const seen: number[] = [];
260
249
  carousel.addEventListener('page-changed', (event) => {
@@ -271,8 +260,7 @@ export const AdwCarouselTest = async () => {
271
260
  await it('reports the LOWER page for a half-way settle', async () => {
272
261
  const { carousel, host } = mount(['a', 'b']);
273
262
  await dragTo(carousel, 0.5);
274
- // Math.round would have said page 1 here; every libadwaita lookup
275
- // resolves the tie downwards (adw-carousel.c:198-201).
263
+ // Math.round would say page 1; every libadwaita lookup resolves the tie down.
276
264
  expect(carousel.currentPage).toBe(0);
277
265
  host.remove();
278
266
  });
@@ -280,8 +268,8 @@ export const AdwCarouselTest = async () => {
280
268
 
281
269
  await describe('adw-carousel declarative position', async () => {
282
270
  await it('honours <adw-carousel position="2"> at upgrade time', async () => {
283
- // Attribute changes are delivered BEFORE connectedCallback, and the
284
- // old guard dropped them, so a declared position never applied.
271
+ // Attribute changes are delivered BEFORE connectedCallback, so a guard that
272
+ // drops them drops a declared position.
285
273
  const { carousel, host } = mount(['a', 'b', 'c'], { position: '2' });
286
274
  await flush();
287
275
  expect(carousel.currentPage).toBe(2);
@@ -360,9 +348,8 @@ export const AdwCarouselTest = async () => {
360
348
 
361
349
  await it('turns allow-long-swipes into scroll-snap-stop', async () => {
362
350
  // The property used to be inert: both branches of its ternary were
363
- // `current + step`. FALSE means "each swipe can only move to the
364
- // adjacent pages" (adw-carousel.c:1111-1113), which is what
365
- // scroll-snap-stop: always enforces.
351
+ // `current + step`. FALSE means "each swipe moves only to an adjacent
352
+ // page", which `scroll-snap-stop: always` enforces.
366
353
  const { carousel, host } = mount(['a', 'b', 'c']);
367
354
  const slot = carousel.querySelector('.adw-carousel-page') as HTMLElement;
368
355
  expect(carousel.allowLongSwipes).toBe(false);
@@ -374,8 +361,8 @@ export const AdwCarouselTest = async () => {
374
361
  });
375
362
 
376
363
  await it('spaces the pages by the spacing property', async () => {
377
- // `distance = size + spacing` (adw-carousel.c:767); with no gap in the
378
- // track the property would have nowhere to show up.
364
+ // `distance = size + spacing`; with no gap in the track the property would
365
+ // have nowhere to show up.
379
366
  const { carousel, host, track } = mount(['a', 'b', 'c'], { spacing: '16' });
380
367
  expect(track.style.columnGap).toBe('16px');
381
368
  // The pitch a drag is measured in must include the gap; if it did not,
@@ -1,17 +1,15 @@
1
1
  // DOM-level tests for <adw-checkbox> and <adw-radio>, plus the browser side of
2
2
  // the `RADIO_GROUP_VECTORS` conformance table.
3
3
  //
4
- // THE ONE CLAIM WORTH THE MOST HERE: the browser's native radio exclusivity is
5
- // NOT enough. `<input type="radio" name="g">` unchecks its sibling INPUT and
6
- // stops there — the sibling `<adw-radio>` HOST keeps its `checked` attribute,
7
- // which is the element's published state AND the selector `_checks.scss` paints
8
- // from, so a group left to the browser draws two selected radios. Every vector
9
- // below reads back the HOST attribute of every member for exactly that reason.
4
+ // The browser's native radio exclusivity is NOT enough: `<input type="radio" name="g">`
5
+ // unchecks its sibling INPUT and stops there, while the sibling `<adw-radio>` HOST keeps
6
+ // its `checked` attribute — the element's published state and the selector
7
+ // `_checks.scss` paints from — so a group left to the browser draws two selected radios.
8
+ // Every vector below reads back the HOST attribute of every member for that reason.
10
9
  //
11
- // The vectors run under a per-scenario group-name prefix: `RadioGroupState` is
12
- // document-global by design (as a bare `<input type=radio name=x>` outside a
13
- // form is), so two scenarios sharing the literal name `colour` would share a
14
- // selection. The prefix is the isolation, not a change to what is being tested.
10
+ // The vectors run under a per-scenario group-name prefix because `RadioGroupState` is
11
+ // document-global by design (as a bare `<input type=radio name=x>` outside a form is),
12
+ // so two scenarios sharing the literal name `colour` would share a selection.
15
13
 
16
14
  import { describe, expect, it } from '@gjsify/unit';
17
15
  import { RADIO_GROUP_VECTORS } from '@gjsify/adwaita-core/conformance';
@@ -60,12 +58,11 @@ export const AdwChecksTest = async () => {
60
58
  expect(input).toBeTruthy();
61
59
  expect(input.type).toBe('checkbox');
62
60
  expect(indicator).toBeTruthy();
63
- // The focus ring is `input:focus-visible ~ .adw-check-indicator`, so
64
- // the input MUST precede the indicator.
61
+ // The focus ring is `input:focus-visible ~ .adw-check-indicator`, so the
62
+ // input MUST precede the indicator.
65
63
  expect(input.nextElementSibling).toBe(indicator);
66
64
  expect(el.checked).toBe(false);
67
65
  expect(input.getAttribute('aria-checked')).toBe('false');
68
- // An empty label takes no space.
69
66
  expect(label.hidden).toBe(true);
70
67
  parent.remove();
71
68
  });
@@ -110,9 +107,8 @@ export const AdwChecksTest = async () => {
110
107
  });
111
108
 
112
109
  await it('a click on the hidden input toggles exactly once', () => {
113
- // The input is 0x0 so a pointer never reaches it, but keyboard
114
- // activation and `input.click()` do — and the host listener must not
115
- // toggle a second time on the way out.
110
+ // The input is 0x0 so a pointer never reaches it, but keyboard activation and
111
+ // `input.click()` do — and the host listener must not toggle a second time.
116
112
  const { el, parent } = mountCheckbox();
117
113
  const events = record(el, 'notify::checked');
118
114
  parts(el).input.click();
@@ -143,18 +139,17 @@ export const AdwChecksTest = async () => {
143
139
  el.checked = true;
144
140
  el.indeterminate = true;
145
141
  const { input } = parts(el);
146
- // HTML has a native mixed CHECKBOX, and an explicit aria-checked so
147
- // the radio (which has no native mixed) reads the same way.
142
+ // HTML has a native mixed CHECKBOX; the radio has none, so an explicit
143
+ // aria-checked makes it read the same way.
148
144
  expect(input.indeterminate).toBe(true);
149
145
  expect(input.getAttribute('aria-checked')).toBe('mixed');
150
146
  parent.remove();
151
147
  });
152
148
 
153
149
  await it('draws the dash even when also checked', () => {
154
- // `check, radio { &:indeterminate { … dash.svg } }`
155
- // (_checks.scss:79-81) is re-declared AFTER both `:checked` rules at
156
- // equal specificity, so the dash wins. Comparing the three states
157
- // against each other proves the cascade, not just that a mask exists.
150
+ // In `_checks.scss` the `:indeterminate` glyph is re-declared AFTER both
151
+ // `:checked` rules at equal specificity, so the dash wins. Comparing all
152
+ // three states proves the cascade, not just that a mask exists.
158
153
  const parent = host();
159
154
  const make = (attrs: string[]) => {
160
155
  const el = document.createElement('adw-checkbox') as AdwCheckbox;
@@ -173,7 +168,7 @@ export const AdwChecksTest = async () => {
173
168
 
174
169
  await it('activating an indeterminate checkbox clears it and checks', () => {
175
170
  // HTML's pre-click activation steps, not a reading of
176
- // GtkCheckButton:inconsistent — GTK's is not verifiable in this tree.
171
+ // GtkCheckButton:inconsistent, which is not verifiable in this tree.
177
172
  const { el, parent } = mountCheckbox();
178
173
  el.indeterminate = true;
179
174
  el.click();
@@ -192,33 +187,31 @@ export const AdwChecksTest = async () => {
192
187
 
193
188
  for (const el of [check, radio]) {
194
189
  const style = getComputedStyle(parts(el).indicator);
195
- // GTK sizes the CONTENT box at 14px and grows the drawn control
196
- // with `padding: 3px` (_checks.scss:22-26) — content-box, not
197
- // the ADR-0010 descendant border-box pin.
190
+ // GTK sizes the CONTENT box at 14px and grows the drawn control with
191
+ // `padding: 3px` — content-box, not the ADR-0010 border-box pin.
198
192
  expect(style.width).toBe('14px');
199
193
  expect(style.height).toBe('14px');
200
194
  expect(style.paddingTop).toBe('3px');
201
195
  }
202
196
 
203
- // `check { border-radius: 6px }` (:67-68) vs
204
- // `radio { border-radius: 100% }` (:73-74).
197
+ // `check { border-radius: 6px }` vs
198
+ // `radio { border-radius: 100% }`.
205
199
  expect(getComputedStyle(parts(check).indicator).borderTopLeftRadius).toBe('6px');
206
200
  expect(getComputedStyle(parts(radio).indicator).borderTopLeftRadius).toContain('%');
207
201
  parent.remove();
208
202
  });
209
203
 
210
204
  await it('a checked radio FILLS, with the SAME accent background as a checked check', () => {
211
- // refs/adwaita-web's `_radio.scss:22-33` colours only the border and
212
- // the dot, leaving the indicator on the view background; libadwaita
213
- // fills the whole control from the block it SHARES with the checkbox
214
- // (:52-60). libadwaita wins — so the two fills must be identical.
205
+ // refs/adwaita-web's `_radio.scss` colours only the border and the dot,
206
+ // leaving the indicator on the view background; libadwaita fills the whole
207
+ // control from the block it SHARES with the checkbox, and libadwaita wins —
208
+ // so the two fills must be identical.
215
209
  //
216
- // THREE elements rather than one flipped in place: the indicator
217
- // TRANSITIONS `background`, so a computed read taken right after
218
- // `checked = true` returns the INTERPOLATED colour — still the old
219
- // one. Declaring the state before insertion leaves no transition to
220
- // be part-way through. The names are distinct so seeding one group
221
- // cannot reach the reference radio.
210
+ // THREE elements rather than one flipped in place: the indicator TRANSITIONS
211
+ // `background`, so a computed read right after `checked = true` returns the
212
+ // INTERPOLATED colour. Declaring the state before insertion leaves no
213
+ // transition part-way through, and distinct names keep seeding one group from
214
+ // reaching the reference radio.
222
215
  const parent = host();
223
216
  const plain = document.createElement('adw-radio') as AdwRadio;
224
217
  plain.setAttribute('name', 'fill-plain');
@@ -243,8 +236,6 @@ export const AdwChecksTest = async () => {
243
236
  for (const [index, vector] of RADIO_GROUP_VECTORS.entries()) {
244
237
  await it(`${vector.name} — ${vector.rule}`, () => {
245
238
  const parent = host();
246
- // Per-scenario prefix: the registry is document-global, so two
247
- // scenarios sharing a literal group name would share a selection.
248
239
  const scope = (name: string) => `v${index}-${name}`;
249
240
 
250
241
  const members = vector.members.map(([name, value]) => {
@@ -272,9 +263,8 @@ export const AdwChecksTest = async () => {
272
263
  expect(parts(el).input.checked).toBe(shouldHold);
273
264
  }
274
265
 
275
- // Every emitted change repaints the winner, and the loser too
276
- // when there was one — that second repaint is the whole reason
277
- // the group state exists.
266
+ // Every emitted change repaints the winner AND the loser when there was
267
+ // one; that second repaint is why the group state exists.
278
268
  const expectedEvents = vector.emitted.reduce(
279
269
  (total, change) => total + (change.deselected === null ? 1 : 2),
280
270
  0,
@@ -327,10 +317,9 @@ export const AdwChecksTest = async () => {
327
317
  });
328
318
 
329
319
  await it('a NAMELESS radio is not in a group', () => {
330
- // HTML forms a radio button group only from a non-empty `name`.
331
- // Without that rule every nameless radio in the document would land
332
- // in one `''` group and fight over it — which is exactly what a
333
- // registry keyed by name does if nothing guards the empty key.
320
+ // HTML forms a radio group only from a non-empty `name`; without that rule
321
+ // every nameless radio lands in one `''` group and fights over it, which is
322
+ // what a registry keyed by name does unless it guards the empty key.
334
323
  const parent = host();
335
324
  const first = document.createElement('adw-radio') as AdwRadio;
336
325
  const second = document.createElement('adw-radio') as AdwRadio;
@@ -1,17 +1,14 @@
1
- // DOM-level behaviour tests for <adw-drop-down>. Runs in a real browser via the
2
- // @gjsify/adwaita-web browser test axis. Asserts option parsing, selected↔value
3
- // sync, notify-on-change semantics, popover open/close, item selection, search
4
- // filtering and the disabled guard.
1
+ // DOM-level behaviour tests for <adw-drop-down>, in a real browser via the
2
+ // @gjsify/adwaita-web browser test axis.
5
3
  //
6
- // The SELECTION half is driven by the SAME table the NativeScript renderer
7
- // asserts against (`COMBO_SELECTION_VECTORS`, `@gjsify/adwaita-core/conformance`)
8
- // — replayed against the real element through the API a consumer would use, and
9
- // read back as what a user would see: the button label, `selected`/`selectedValue`
10
- // and the `change` stream. `<adw-combo-row>` composes the same `ComboState` but
11
- // cannot drive the table, because two of its four step ops have no DOM spelling:
12
- // its options come from the `items` attribute at connect time only (`items` is not
13
- // even observed), and it publishes no select-by-value setter. So this element is
14
- // the browser's driver for both.
4
+ // The SELECTION half is driven by the SAME table the NativeScript renderer asserts
5
+ // against (`COMBO_SELECTION_VECTORS`, `@gjsify/adwaita-core/conformance`), replayed
6
+ // through the API a consumer would use and read back as what a user would see: the
7
+ // button label, `selected`/`selectedValue` and the `change` stream. `<adw-combo-row>`
8
+ // composes the same `ComboState` but cannot drive the table — two of its four step ops
9
+ // have no DOM spelling, since its options come from the `items` attribute at connect
10
+ // time only (`items` is not observed) and it publishes no select-by-value setter. So
11
+ // this element is the browser's driver for both.
15
12
  import { describe, it, expect } from '@gjsify/unit';
16
13
 
17
14
  import { COMBO_SELECTION_VECTORS } from '@gjsify/adwaita-core/conformance';
@@ -53,9 +50,8 @@ function applyStep(dd: AdwDropDown, step: ComboSelectionStep): void {
53
50
  case 'select':
54
51
  openChooser(dd);
55
52
  if (step.index < 0) {
56
- // The dismissed chooser: the popover closes with nothing picked,
57
- // which here is an outside pointerdown rather than a cancelled
58
- // native sheet.
53
+ // The dismissed chooser: the popover closes with nothing picked — here an
54
+ // outside pointerdown rather than a cancelled native sheet.
59
55
  document.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }));
60
56
  return;
61
57
  }
@@ -65,13 +61,12 @@ function applyStep(dd: AdwDropDown, step: ComboSelectionStep): void {
65
61
  }
66
62
 
67
63
  /**
68
- * The ONE row this element answers differently, and does so on purpose: its
69
- * published `selected` contract REJECTS an out-of-range set (`_selectIndex` gates
70
- * on `ComboState.hasIndex`), where the core state accepts it and reports an empty
71
- * value — the split `ComboState.hasIndex`'s doc comment records and
72
- * `<adw-combo-row>` takes the other side of. The row is still driven below,
73
- * against the element's answer rather than the state's, so neither policy can
74
- * change silently.
64
+ * The ONE row this element answers differently, on purpose: its published `selected`
65
+ * contract REJECTS an out-of-range set (`_selectIndex` gates on `ComboState.hasIndex`)
66
+ * where the core state accepts it and reports an empty value — the split
67
+ * `ComboState.hasIndex`'s doc records and `<adw-combo-row>` takes the other side of. The
68
+ * row is still driven below, against the element's answer, so neither policy can change
69
+ * silently.
75
70
  */
76
71
  const DROP_DOWN_REJECTS = 'an index past the end';
77
72
 
@@ -102,10 +97,9 @@ export const AdwDropDownTest = async () => {
102
97
  expect(dd.selected).toBe(vector.selected);
103
98
  expect(dd.selectedValue).toBe(vector.value);
104
99
  expect(labelText(dd)).toBe(vector.label);
105
- // `change` is this element's DOM spelling of the `interactive`
106
- // flag: it fires for a user pick and never for a programmatic
107
- // set, so the interactive frames of the emission stream ARE the
108
- // events a listener sees.
100
+ // `change` is this element's DOM spelling of the `interactive` flag: it
101
+ // fires for a user pick and never for a programmatic set, so the
102
+ // interactive frames of the stream ARE the events a listener sees.
109
103
  expect(changes).toStrictEqual(
110
104
  vector.emitted
111
105
  .filter((change) => change.interactive)
@@ -119,11 +113,10 @@ export const AdwDropDownTest = async () => {
119
113
  const vector = vectorNamed(DROP_DOWN_REJECTS);
120
114
  const { dd, changes } = mountRecording();
121
115
 
122
- // Everything up to the out-of-range set…
123
116
  for (const step of vector.steps.slice(0, -1)) applyStep(dd, step);
124
117
  const before = { selected: dd.selected, value: dd.selectedValue, label: labelText(dd) };
125
- // …and then the set itself, which the core state would accept and
126
- // report as nothing-to-draw (`value`/`label` both empty below).
118
+ // The out-of-range set itself, which the core state would accept and report
119
+ // as nothing-to-draw (`value`/`label` both empty below).
127
120
  expect(vector.value).toBe('');
128
121
  expect(vector.label).toBe('');
129
122
  applyStep(dd, vector.steps[vector.steps.length - 1]);
@@ -210,8 +203,8 @@ export const AdwDropDownTest = async () => {
210
203
  });
211
204
 
212
205
  await it('setting options + selected before connect does not crash + applies on connect', async () => {
213
- // Regression: the property setters ran before connectedCallback built
214
- // the DOM, so _updateLabel touched an undefined label element.
206
+ // The property setters can run BEFORE connectedCallback builds the DOM, where
207
+ // _updateLabel would touch an undefined label element.
215
208
  const dd = document.createElement('adw-drop-down') as AdwDropDown;
216
209
  dd.options = ['a', 'b', 'c'];
217
210
  dd.selected = 2;
@@ -1,14 +1,12 @@
1
1
  // DOM-level tests for <adw-header-bar>'s title.
2
2
  //
3
- // `title` was read exactly once, in `connectedCallback`, and the element declared
4
- // no `observedAttributes` at all — so every later write was a silent no-op. A
5
- // header bar whose title tracks the open document kept whatever it was created
6
- // with, and nothing in the build was in a position to notice: the attribute was
7
- // still THERE, and the element still rendered A title.
3
+ // `title` must be OBSERVED, not read once at `connectedCallback`: a header bar tracking
4
+ // the open document keeps its construction-time title otherwise, and nothing notices
5
+ // because the attribute is still there and A title still renders.
8
6
  //
9
7
  // `Adw.HeaderBar` builds its centre from `title`/`subtitle` through a derived
10
- // `AdwWindowTitle` bound to the properties, unless a `title-widget` replaces it
11
- // — which is the either/or the `slot="center"` case below asserts.
8
+ // `AdwWindowTitle` bound to the properties, unless a `title-widget` replaces it — the
9
+ // either/or the `slot="center"` case below asserts.
12
10
 
13
11
  import { describe, expect, it } from '@gjsify/unit';
14
12
 
@@ -1,16 +1,11 @@
1
- // DOM-level tests for <adw-icon>, driven by the SAME `normalizeIconName` vectors
2
- // the core suite asserts against (`@gjsify/adwaita-core/conformance`), plus the
3
- // REGRESSION PROOF for the drift the element was extracted to close.
1
+ // DOM-level tests for <adw-icon>, driven by the SAME `normalizeIconName` vectors the
2
+ // core suite asserts against (`@gjsify/adwaita-core/conformance`).
4
3
  //
5
- // The drift: `<span class="adw-icon adw-icon--<name>" aria-hidden="true">` was
6
- // built by hand at twenty-three sites across seventeen files, nine of them
7
- // re-deriving `name.replace(/-symbolic$/, '')` on the spot. Exactly ONE site —
8
- // `<adw-split-button>` — also checked the result was a single CSS token. So
9
- // `icon-name="a b"` interpolated into `class="adw-icon adw-icon--a b"` and shipped
10
- // a stray `b` class through `<adw-menu-button>`, for its own icon AND for every
11
- // JSON menu entry's, while the one element that guarded kept passing its spec.
12
- // The vectors below now run against the ELEMENT, so a renderer that starts
13
- // building the class by hand again fails naming the input.
4
+ // The icon-name class must never be built by hand: a name is only usable once it is
5
+ // known to be a SINGLE CSS token, and hand-built spans skipped that check, so
6
+ // `icon-name="a b"` interpolated a stray second class and shipped it through
7
+ // `<adw-menu-button>` for its own icon and for every JSON menu entry's. The vectors run
8
+ // against the ELEMENT, so hand-building the class again fails naming the input.
14
9
  import { describe, expect, it } from '@gjsify/unit';
15
10
 
16
11
  import { VIEW_STACK_ICON_NAME_VECTORS } from '@gjsify/adwaita-core/conformance';
@@ -39,13 +34,11 @@ export const AdwIconTest = async () => {
39
34
  el.iconName = icon ?? null;
40
35
 
41
36
  expect(el.resolvedIconName).toBe(normalized);
42
- // The base class is unconditional — a nameless icon is still an
43
- // icon box, which is what `<adw-status-page>` hides rather than
44
- // omits.
37
+ // The base class is unconditional: a nameless icon is still an icon box,
38
+ // which `<adw-status-page>` hides rather than omits.
45
39
  expect(el.classList.contains('adw-icon')).toBe(true);
46
- // …and NOTHING else is on the node. This is the assertion the
47
- // hand-rolled copies could not make: `icon-name="a b"` used to
48
- // leave a stray `b` here.
40
+ // …and NOTHING else is on the node — the assertion a hand-rolled copy
41
+ // cannot make, where `icon-name="a b"` leaves a stray `b`.
49
42
  expect(extraClasses(el)).toStrictEqual(normalized === '' ? [] : [`adw-icon--${normalized}`]);
50
43
  host.remove();
51
44
  });
@@ -66,8 +59,8 @@ export const AdwIconTest = async () => {
66
59
  expect(extraClasses(el)).toStrictEqual(['adw-drop-down-arrow', 'adw-icon--go-down', 'start']);
67
60
 
68
61
  el.iconName = 'go-next';
69
- // Exactly one mask class at a time, and the caller's are untouched —
70
- // a `className =` assignment would have taken them with it.
62
+ // Exactly one mask class at a time, and the caller's are untouched: a
63
+ // `className =` assignment would take them with it.
71
64
  expect(extraClasses(el)).toStrictEqual(['adw-drop-down-arrow', 'adw-icon--go-next', 'start']);
72
65
 
73
66
  el.iconName = null;
@@ -86,10 +79,9 @@ export const AdwIconTest = async () => {
86
79
  const styled = getComputedStyle(el);
87
80
  expect(styled.width).toBe('32px');
88
81
  expect(styled.height).toBe('32px');
89
- // Sizing only the box would leave a 16px glyph floating in it. Only
90
- // the axis we set is asserted: `mask-size: 32px` means "32px wide,
91
- // height from the aspect ratio", and the CSSOM re-serializes the
92
- // omitted half (Firefox reports `32px auto`, in the inline style too).
82
+ // Sizing only the box leaves a 16px glyph floating in it. Only the axis set is
83
+ // asserted: `mask-size: 32px` means "32px wide, height from the aspect ratio",
84
+ // and the CSSOM re-serializes the omitted half (Firefox reports `32px auto`).
93
85
  expect(styled.maskSize.split(' ')[0]).toBe('32px');
94
86
  expect(styled.webkitMaskSize.split(' ')[0]).toBe('32px');
95
87
 
@@ -99,10 +91,10 @@ export const AdwIconTest = async () => {
99
91
  });
100
92
 
101
93
  await it('paints in the INHERITED colour, not the boundary reset colour', () => {
102
- // `.adw-icon` masks with `currentColor`, and <adw-icon> is a custom
103
- // element — so the ADR-0010 `$adw-components` reset re-roots `color`
104
- // on it. Without `color: inherit` in `_icon.scss` every icon would
105
- // repaint in `--window-fg-color` regardless of the button it sits in.
94
+ // `.adw-icon` masks with `currentColor` and <adw-icon> is a custom element, so
95
+ // the ADR-0010 `$adw-components` reset re-roots `color` on it: without
96
+ // `color: inherit` in `_icon.scss` every icon repaints in `--window-fg-color`
97
+ // regardless of the button it sits in.
106
98
  const { el: box, host } = mount<HTMLElement>('div');
107
99
  box.style.color = 'rgb(255, 0, 0)';
108
100
  const icon = document.createElement('adw-icon') as AdwIcon;
@@ -142,10 +134,9 @@ export const AdwIconTest = async () => {
142
134
  });
143
135
 
144
136
  await it('every icon in the package is an <adw-icon>', () => {
145
- // The point of the extraction, asserted structurally: build a page of
146
- // the widgets that draw icons and check no bare span carries the
147
- // base class. A new hand-rolled `<span class="adw-icon …">` fails
148
- // here even if it happens to guard its own name correctly.
137
+ // Asserted structurally: a page of every icon-drawing widget, and no bare span
138
+ // carrying the base class — so a new hand-rolled `<span class="adw-icon …">`
139
+ // fails here even when it guards its own name correctly.
149
140
  const { el: page, host } = mount<HTMLElement>('div');
150
141
  page.innerHTML = `
151
142
  <adw-button icon="go-next" label="Next"></adw-button>