@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
@@ -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>
@@ -1,13 +1,11 @@
1
1
  // DOM-level tests for <adw-progress-bar>.
2
2
  //
3
- // Three things here are worth more than the attribute round-trips: the CLAMP is
4
- // GLib's (`glibClamp`, shared from `@gjsify/adwaita-core`, not a local
5
- // `Math.min/max`), the ZERO fraction has to make the indicator VANISH rather
6
- // than draw a rounded stub (`> trough.empty > progress { all: unset }`,
7
- // _progress-bar.scss:85), and the text node has to actually PICK UP `.dimmed` +
8
- // `.numeric` from `_labels.scss` — that composition is the reason those classes
9
- // exist, and a `@extend` that silently reached nothing would look identical in
10
- // the source.
3
+ // Three things matter more than the attribute round-trips: the CLAMP is GLib's
4
+ // (`glibClamp` from `@gjsify/adwaita-core`, not a local `Math.min/max`); a ZERO fraction
5
+ // makes the indicator VANISH rather than draw a rounded stub
6
+ // (`> trough.empty > progress { all: unset }` in `_progress-bar.scss`); and the text node
7
+ // really PICKS UP `.dimmed` + `.numeric` from `_labels.scss` — a `@extend` that reached
8
+ // nothing would look identical in the source.
11
9
 
12
10
  import { describe, expect, it } from '@gjsify/unit';
13
11
 
@@ -124,8 +122,8 @@ export const AdwProgressBarTest = async () => {
124
122
  });
125
123
 
126
124
  await it('the text node composes .dimmed and .numeric from _labels.scss', () => {
127
- // `> text { @extend .dimmed; @extend .numeric; }`
128
- // (_progress-bar.scss:17-21) — this asserts the extends actually
125
+ // `> text { @extend.dimmed; @extend.numeric; }`
126
+ // (_progress-bar.scss) — this asserts the extends actually
129
127
  // REACHED the utility classes, which a source read cannot tell.
130
128
  const { el, host } = mount();
131
129
  el.showText = true;
@@ -138,8 +136,8 @@ export const AdwProgressBarTest = async () => {
138
136
 
139
137
  await describe('<adw-progress-bar> variants', async () => {
140
138
  await it('is an 8px trough by default and a 2px hairline with osd', () => {
141
- // The 8px is libadwaita's (_progress-bar.scss:6) and the 2px is its
142
- // `.osd` variant (:59-83) — NOT the vendored web port's 6px, which
139
+ // The 8px is libadwaita's (_progress-bar.scss) and the 2px is its
140
+ // `.osd` variant — NOT the vendored web port's 6px, which
143
141
  // has no OSD variant at all.
144
142
  //
145
143
  // TWO bars rather than one flipped in place, on purpose: the trough
@@ -85,15 +85,14 @@ export const AdwRowStateTest = async () => {
85
85
  const value = row.querySelector('.adw-row-value') as HTMLSpanElement;
86
86
  const select = row.querySelector('select') as HTMLSelectElement;
87
87
 
88
- // The ARROW: `.adw-row-value::after` is masked in, and the
89
- // `[data-presents-chooser='false']` block removes it. Asserted
90
- // as "not none" rather than against the authored `inline-block`,
91
- // because `.adw-row-value` is a flex container and blockifies
92
- // its children's display.
88
+ // The ARROW: `.adw-row-value::after` is masked in and the
89
+ // `[data-presents-chooser='false']` block removes it. Asserted as "not
90
+ // none" rather than against the authored `inline-block`, because
91
+ // `.adw-row-value` is a flex container and blockifies its children.
93
92
  expect(getComputedStyle(value, '::after').display === 'none').toBe(!presentsChooser);
94
- // ...and the ROW being activatable, which on this port is the
95
- // overlaid <select> taking clicks at all. Hiding the arrow
96
- // without this leaves a row that still opens a one-entry popup.
93
+ // …and the ROW being activatable, which here is the overlaid <select>
94
+ // taking clicks at all: hiding the arrow alone leaves a row that still
95
+ // opens a one-entry popup.
97
96
  expect(select.disabled).toBe(!presentsChooser);
98
97
 
99
98
  host.remove();
@@ -108,8 +107,8 @@ export const AdwRowStateTest = async () => {
108
107
  const select = row.querySelector('select') as HTMLSelectElement;
109
108
  expect(select.disabled).toBe(true);
110
109
 
111
- // `items` is read at connect only, so the model is replaced through
112
- // the state the element composes — the same path a consumer has.
110
+ // `items` is read at connect only, so the model is replaced through the state
111
+ // the element composes — the same path a consumer has.
113
112
  (
114
113
  row as unknown as { _state: { setOptions(o: { label: string; value: string }[]): void } }
115
114
  )._state.setOptions([
@@ -0,0 +1,112 @@
1
+ // <adw-shortcut-label> against `SHORTCUT_LABEL_VECTORS` — the same table the
2
+ // core suite drives, so a divergence between the parse and what this element
3
+ // actually renders fails a test naming the accelerator.
4
+ //
5
+ // The serialiser below walks the REAL DOM and produces the table's compact form.
6
+ // That is the whole point of the shared format: if this file built its own
7
+ // expectation strings instead, the two suites could agree with each other while
8
+ // both disagreeing with libadwaita.
9
+ import { describe, expect, it } from '@gjsify/unit';
10
+ import { SHORTCUT_LABEL_VECTORS } from '@gjsify/adwaita-core/conformance';
11
+
12
+ import '@gjsify/adwaita-web';
13
+
14
+ /** The rendered element in the table's compact form (see conformance/shortcut-label.ts). */
15
+ const serialize = (host: HTMLElement): string =>
16
+ [...host.children]
17
+ .map((child) => {
18
+ if (child.classList.contains('adw-shortcut-label-disabled'))
19
+ return `(disabled: ${child.textContent ?? ''})`;
20
+ if (child.classList.contains('dimmed')) return child.textContent ?? '';
21
+
22
+ return [...child.children]
23
+ .map((cap) => {
24
+ const side = cap.querySelector('.adw-shortcut-label-side');
25
+ // The marker rides INSIDE the keycap, so its text has to come
26
+ // back out of the label before the two are compared.
27
+ const label = (cap.textContent ?? '').slice(
28
+ 0,
29
+ (cap.textContent ?? '').length - (side?.textContent?.length ?? 0),
30
+ );
31
+ return `[${side ? `${label} (${side.textContent})` : label}]`;
32
+ })
33
+ .join('');
34
+ })
35
+ .join(' ')
36
+ .trim();
37
+
38
+ export const AdwShortcutLabelTest = async () => {
39
+ await describe('<adw-shortcut-label> (conformance vectors)', async () => {
40
+ for (const vector of SHORTCUT_LABEL_VECTORS) {
41
+ // The Apple glyph set is a build constant in the C and an option in
42
+ // core; the ELEMENT reads its platform from nothing, so the two
43
+ // Apple rows have no element to drive. They stay core-only.
44
+ if (vector.platform === 'apple') continue;
45
+
46
+ await it(`${vector.accelerator || '(empty)'} — ${vector.rule}`, async () => {
47
+ const host = document.createElement('adw-shortcut-label');
48
+ host.setAttribute('accelerator', vector.accelerator);
49
+ if (vector.disabledText !== undefined) host.setAttribute('disabled-text', vector.disabledText);
50
+ // RTL comes from the rendered direction, so the vector's
51
+ // direction has to be a real attribute on a CONNECTED element.
52
+ if (vector.direction === 'rtl') host.setAttribute('dir', 'rtl');
53
+ document.body.appendChild(host);
54
+
55
+ expect(serialize(host)).toBe(vector.expected);
56
+ if (vector.accessibleLabel !== undefined) {
57
+ expect(host.getAttribute('aria-label')).toBe(vector.accessibleLabel);
58
+ }
59
+
60
+ host.remove();
61
+ });
62
+ }
63
+
64
+ await it('re-renders when the accelerator changes', async () => {
65
+ const host = document.createElement('adw-shortcut-label');
66
+ host.setAttribute('accelerator', '<Control>C');
67
+ document.body.appendChild(host);
68
+
69
+ let notified = 0;
70
+ host.addEventListener('notify::accelerator', () => notified++);
71
+ host.setAttribute('accelerator', '<Control>V');
72
+
73
+ expect(serialize(host)).toBe('[Ctrl][V]');
74
+ expect(notified).toBe(1);
75
+
76
+ host.remove();
77
+ });
78
+
79
+ await it('draws the keycaps of one combination left-to-right inside an RTL context', async () => {
80
+ // `gtk_widget_set_direction (box, GTK_TEXT_DIR_LTR)` (:380): the
81
+ // modifier order belongs to the shortcut, not to the surrounding
82
+ // text — only the SEQUENCE arrow flips.
83
+ const host = document.createElement('adw-shortcut-label');
84
+ host.setAttribute('dir', 'rtl');
85
+ host.setAttribute('accelerator', '<Control>C+<Control>X');
86
+ document.body.appendChild(host);
87
+
88
+ const box = host.querySelector<HTMLElement>('.adw-shortcut-label-keys');
89
+ expect(box?.getAttribute('dir')).toBe('ltr');
90
+ expect(getComputedStyle(box as HTMLElement).direction).toBe('ltr');
91
+ expect(host.querySelector('.dimmed')?.textContent).toBe('←');
92
+
93
+ host.remove();
94
+ });
95
+
96
+ await it('gives its keycaps the styling libadwaita gives them', async () => {
97
+ // The rule exists, and it reaches the node the element builds — the
98
+ // half a class-name assertion cannot see (`_shortcuts-dialog.scss:42-53`).
99
+ const host = document.createElement('adw-shortcut-label');
100
+ host.setAttribute('accelerator', '<Control>C');
101
+ document.body.appendChild(host);
102
+
103
+ const cap = host.querySelector<HTMLElement>('.keycap');
104
+ const style = getComputedStyle(cap as HTMLElement);
105
+ expect(style.borderTopLeftRadius).toBe('6px');
106
+ expect(style.paddingTop).toBe('6px');
107
+ expect(Number.parseFloat(style.minWidth)).toBe(20);
108
+
109
+ host.remove();
110
+ });
111
+ });
112
+ };