@gjsify/adwaita-web 0.55.0 → 0.57.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 (127) hide show
  1. package/README.md +8 -0
  2. package/dist/adwaita-web.css +137 -29
  3. package/dist/adwaita-web.css.map +1 -1
  4. package/lib/types/adw-status-page.spec.d.ts +1 -0
  5. package/lib/types/application.d.ts +24 -0
  6. package/lib/types/application.spec.d.ts +1 -0
  7. package/lib/types/attributes.d.ts +19 -0
  8. package/lib/types/elements/adw-action-row.d.ts +4 -0
  9. package/lib/types/elements/adw-application-window.d.ts +2 -0
  10. package/lib/types/elements/adw-combo-row.d.ts +4 -0
  11. package/lib/types/elements/adw-entry-row.d.ts +6 -0
  12. package/lib/types/elements/adw-expander-row.d.ts +2 -0
  13. package/lib/types/elements/adw-header-bar.d.ts +6 -0
  14. package/lib/types/elements/adw-overlay-split-view.d.ts +13 -0
  15. package/lib/types/elements/adw-sidebar.d.ts +3 -2
  16. package/lib/types/elements/adw-spin-row.d.ts +6 -0
  17. package/lib/types/elements/adw-switch-row.d.ts +4 -0
  18. package/lib/types/elements/adw-toggle-group.d.ts +17 -0
  19. package/lib/types/elements/adw-view-stack.d.ts +3 -0
  20. package/lib/types/elements/checks.d.ts +6 -0
  21. package/lib/types/elements/gtk-application-window.d.ts +2 -0
  22. package/lib/types/elements/gtk-drop-down.d.ts +3 -0
  23. package/lib/types/elements/gtk-entry.d.ts +10 -0
  24. package/lib/types/elements/gtk-menu-button.d.ts +2 -0
  25. package/lib/types/elements/gtk-scrolled-window.d.ts +26 -2
  26. package/lib/types/elements/gtk-stack.d.ts +3 -0
  27. package/lib/types/elements/gtk-switch.d.ts +3 -0
  28. package/lib/types/elements/gtk-window.d.ts +8 -2
  29. package/lib/types/glib.spec.d.ts +1 -0
  30. package/lib/types/gobject-door.d.ts +4 -0
  31. package/lib/types/gobject-door.spec.d.ts +1 -0
  32. package/lib/types/gobject-elements.d.ts +17 -0
  33. package/lib/types/gtksource/gtk-source-view.d.ts +77 -0
  34. package/lib/types/gtksource/gtk-source-view.spec.d.ts +2 -0
  35. package/lib/types/gtksource/web-editor-driver.d.ts +44 -0
  36. package/lib/types/index.d.ts +4 -1
  37. package/lib/types/menu-actions.spec.d.ts +1 -0
  38. package/lib/types/namespace/adw.d.ts +1 -0
  39. package/lib/types/namespace/gio.d.ts +2 -0
  40. package/lib/types/namespace/glib.d.ts +10 -0
  41. package/lib/types/namespace/gobject.d.ts +27 -0
  42. package/lib/types/namespace/gtk-enums.d.ts +22 -0
  43. package/lib/types/namespace/gtk.d.ts +2 -0
  44. package/lib/types/namespace/gtksource.d.ts +2 -0
  45. package/lib/types/row-line-clamp.d.ts +20 -0
  46. package/lib/types/row-tooltip.d.ts +8 -0
  47. package/lib/types/row-tooltip.spec.d.ts +1 -0
  48. package/lib/types/shared-tree-builder.d.ts +69 -20
  49. package/lib/types/shared-tree-mount.spec.d.ts +1 -0
  50. package/lib/types/styles.generated.d.ts +1 -1
  51. package/lib/types/widget-size.spec.d.ts +1 -0
  52. package/package.json +11 -9
  53. package/scss/_action_row.scss +39 -15
  54. package/scss/_button_row.scss +5 -0
  55. package/scss/_expander_row.scss +36 -3
  56. package/scss/_headerbar.scss +2 -1
  57. package/scss/_preferences_row.scss +9 -0
  58. package/scss/_reset.scss +1 -1
  59. package/scss/_row.scss +16 -0
  60. package/scss/_status_page.scss +1 -1
  61. package/scss/_toggle_group.scss +28 -0
  62. package/scss/_widget.scss +14 -3
  63. package/scss/_window.scss +13 -1
  64. package/src/adw-action-rows.spec.ts +255 -0
  65. package/src/adw-header-bar.spec.ts +18 -0
  66. package/src/adw-status-page.spec.ts +124 -0
  67. package/src/application.spec.ts +75 -0
  68. package/src/application.ts +92 -0
  69. package/src/attributes.ts +52 -0
  70. package/src/capabilities.mjs +3 -0
  71. package/src/construct-vectors.spec.ts +156 -1
  72. package/src/elements/adw-action-row.ts +31 -2
  73. package/src/elements/adw-alert-dialog.ts +4 -1
  74. package/src/elements/adw-application-window.ts +5 -0
  75. package/src/elements/adw-button-row.ts +7 -1
  76. package/src/elements/adw-combo-row.ts +26 -1
  77. package/src/elements/adw-entry-row.ts +22 -12
  78. package/src/elements/adw-expander-row.ts +34 -1
  79. package/src/elements/adw-header-bar.ts +18 -4
  80. package/src/elements/adw-overlay-split-view.ts +29 -6
  81. package/src/elements/adw-preferences-row.ts +5 -2
  82. package/src/elements/adw-sidebar.ts +33 -5
  83. package/src/elements/adw-spin-row.ts +37 -2
  84. package/src/elements/adw-split-button.ts +5 -1
  85. package/src/elements/adw-status-page.ts +10 -2
  86. package/src/elements/adw-switch-row.ts +25 -1
  87. package/src/elements/adw-toggle-group.ts +119 -10
  88. package/src/elements/adw-view-stack.ts +14 -0
  89. package/src/elements/adw-window.ts +9 -4
  90. package/src/elements/checks.ts +25 -1
  91. package/src/elements/gtk-application-window.ts +5 -0
  92. package/src/elements/gtk-drop-down.ts +16 -0
  93. package/src/elements/gtk-entry.ts +45 -7
  94. package/src/elements/gtk-menu-button.ts +12 -1
  95. package/src/elements/gtk-popover-menu.ts +7 -2
  96. package/src/elements/gtk-scrolled-window.ts +76 -7
  97. package/src/elements/gtk-stack.ts +8 -0
  98. package/src/elements/gtk-switch.ts +8 -0
  99. package/src/elements/gtk-window.ts +9 -3
  100. package/src/glib.spec.ts +34 -0
  101. package/src/gobject-door.spec.ts +119 -0
  102. package/src/gobject-door.ts +137 -0
  103. package/src/gobject-elements.ts +172 -0
  104. package/src/gtk-window.spec.ts +12 -0
  105. package/src/gtksource/gtk-source-view.spec.ts +69 -0
  106. package/src/gtksource/gtk-source-view.ts +379 -0
  107. package/src/gtksource/web-editor-driver.ts +260 -0
  108. package/src/index.ts +11 -1
  109. package/src/keyboard-operable.spec.ts +47 -11
  110. package/src/menu-actions.spec.ts +47 -0
  111. package/src/namespace/adw.ts +1 -0
  112. package/src/namespace/gio.ts +2 -0
  113. package/src/namespace/glib.ts +22 -0
  114. package/src/namespace/gobject.ts +37 -0
  115. package/src/namespace/gtk-enums.ts +15 -0
  116. package/src/namespace/gtk.ts +2 -0
  117. package/src/namespace/gtksource.ts +9 -0
  118. package/src/row-line-clamp.ts +74 -0
  119. package/src/row-tooltip.spec.ts +313 -0
  120. package/src/row-tooltip.ts +52 -0
  121. package/src/shared-tree-builder.ts +230 -42
  122. package/src/shared-tree-mount.spec.ts +130 -0
  123. package/src/shared-trees.spec.ts +15 -0
  124. package/src/split-button.spec.ts +2 -0
  125. package/src/styles.generated.ts +1 -1
  126. package/src/test.browser.mts +18 -0
  127. package/src/widget-size.spec.ts +117 -0
@@ -0,0 +1,313 @@
1
+ // `Gtk.Widget:tooltip-text` on the boxed-list rows, and the two sensitivity doors
2
+ // `<adw-toggle-group>` gained with it.
3
+ //
4
+ // THE COLLISION THESE TESTS EXIST FOR: a row's `title` attribute is its LABEL
5
+ // (`Adw.PreferencesRow:title`), and `HTMLElement.title` reflects the same attribute — so
6
+ // the browser's own tooltip channel cannot be written on the host without blanking the
7
+ // label. `src/row-tooltip.ts` puts it on the row's PARTS instead, and Firefox resolves a
8
+ // tooltip by walking up from the hovered node until an element carries a non-empty
9
+ // `title`. What the tests pin is that consequence: the parts carry the text, the host's
10
+ // label survives, and a part the element gave a tooltip of its own keeps it.
11
+ import { describe, expect, it } from '@gjsify/unit';
12
+
13
+ import type { AdwToggleGroup } from './elements/adw-toggle-group.js';
14
+
15
+ /** Connect, run, disconnect — the same helper `keyboard-operable.spec.ts` drives with. */
16
+ function withWidget(make: () => HTMLElement, body: (el: HTMLElement) => void): void {
17
+ const el = make();
18
+ document.body.appendChild(el);
19
+ try {
20
+ body(el);
21
+ } finally {
22
+ el.remove();
23
+ }
24
+ }
25
+
26
+ /** The parts `applyRowTooltip` is expected to have written `tooltip-text` onto. */
27
+ const TOOLTIPPED_PARTS: Record<string, string[]> = {
28
+ 'adw-action-row': ['.adw-action-row-prefix', '.adw-row-text', '.adw-action-row-suffix'],
29
+ 'adw-button-row': ['.adw-button-row-contents'],
30
+ 'adw-combo-row': ['.adw-row-text', '.adw-row-value', 'select'],
31
+ 'adw-expander-row': ['.adw-expander-row-header'],
32
+ 'adw-preferences-row': ['.adw-row-title', '.adw-preferences-row-content'],
33
+ 'adw-spin-row': ['.adw-row-text', '.adw-spin-control'],
34
+ 'adw-switch-row': ['.adw-switch-row-prefix', '.adw-row-text'],
35
+ };
36
+
37
+ /** Every `title` attribute inside `el`, as `selector → value`, absent ones as `null`. */
38
+ function partTitles(el: Element): Record<string, string | null> {
39
+ const titles: Record<string, string | null> = {};
40
+ for (const selector of TOOLTIPPED_PARTS[el.localName]) {
41
+ titles[selector] = el.querySelector(selector)?.getAttribute('title') ?? null;
42
+ }
43
+ return titles;
44
+ }
45
+
46
+ /**
47
+ * Measure the vertical coverage of tooltip-marked parts against the row's bounding box.
48
+ * Returns the ratio of covered height to row height (1.0 = full coverage).
49
+ */
50
+ function tooltipCoverageRatio(el: HTMLElement): number {
51
+ const rowRect = el.getBoundingClientRect();
52
+ const parts = TOOLTIPPED_PARTS[el.localName] ?? [];
53
+ if (parts.length === 0) return 0;
54
+
55
+ // Union of all tooltip parts' bounding boxes
56
+ let top = Infinity;
57
+ let bottom = -Infinity;
58
+ for (const selector of parts) {
59
+ const part = el.querySelector(selector) as HTMLElement | null;
60
+ if (!part) continue;
61
+ const rect = part.getBoundingClientRect();
62
+ top = Math.min(top, rect.top);
63
+ bottom = Math.max(bottom, rect.bottom);
64
+ }
65
+ if (!isFinite(top) || !isFinite(bottom)) return 0;
66
+ const covered = bottom - top;
67
+ return covered / rowRect.height;
68
+ }
69
+
70
+ export const AdwRowTooltipTest = async () => {
71
+ await describe('row tooltip-text (Gtk.Widget:tooltip-text)', async () => {
72
+ // One case per row family, and the assertion is the SAME for all of them: the
73
+ // parts carry the text and the host keeps its label. A row that wrote the tooltip
74
+ // on the host instead would blank `title` here, which is the whole failure mode.
75
+ for (const [tag, selectors] of Object.entries(TOOLTIPPED_PARTS)) {
76
+ await it(`${tag} puts tooltip-text on its parts and keeps its title label`, () => {
77
+ withWidget(
78
+ () => {
79
+ const el = document.createElement(tag);
80
+ el.setAttribute('title', 'Wi-Fi');
81
+ el.setAttribute('tooltip-text', 'Connects to the office network');
82
+ return el;
83
+ },
84
+ (el) => {
85
+ expect(el.getAttribute('title')).toBe('Wi-Fi');
86
+ expect((el as HTMLElement & { title: string }).title).toBe('Wi-Fi');
87
+ for (const selector of selectors) {
88
+ expect(el.querySelector(selector)?.getAttribute('title')).toBe(
89
+ 'Connects to the office network',
90
+ );
91
+ }
92
+ },
93
+ );
94
+ });
95
+
96
+ await it(`${tag} clears the tooltip when the attribute goes`, () => {
97
+ withWidget(
98
+ () => {
99
+ const el = document.createElement(tag);
100
+ el.setAttribute('title', 'Wi-Fi');
101
+ el.setAttribute('tooltip-text', 'Gone soon');
102
+ return el;
103
+ },
104
+ (el) => {
105
+ expect(partTitles(el)).toStrictEqual(
106
+ Object.fromEntries(TOOLTIPPED_PARTS[tag].map((selector) => [selector, 'Gone soon'])),
107
+ );
108
+
109
+ el.removeAttribute('tooltip-text');
110
+ for (const selector of selectors) {
111
+ expect(el.querySelector(selector)?.hasAttribute('title')).toBe(false);
112
+ }
113
+ // The LABEL is untouched either way.
114
+ expect(el.getAttribute('title')).toBe('Wi-Fi');
115
+ },
116
+ );
117
+ });
118
+ }
119
+
120
+ await it('adw-entry-row leaves the apply button its own tooltip', () => {
121
+ withWidget(
122
+ () => {
123
+ const el = document.createElement('adw-entry-row');
124
+ el.setAttribute('text', 'query');
125
+ el.setAttribute('show-apply-button', '');
126
+ el.setAttribute('tooltip-text', 'The search term');
127
+ return el;
128
+ },
129
+ (el) => {
130
+ const apply = el.querySelector('button') as HTMLButtonElement;
131
+ expect(apply.title).not.toBe('The search term');
132
+ expect(apply.title.length).toBeGreaterThan(0);
133
+ // The editable area is the part that takes the row's tooltip.
134
+ expect(el.querySelector('.adw-entry-row-text')?.getAttribute('title')).toBe('The search term');
135
+ },
136
+ );
137
+ });
138
+
139
+ await it('adw-password-entry-row inherits the entry row tooltip', () => {
140
+ withWidget(
141
+ () => {
142
+ const el = document.createElement('adw-password-entry-row');
143
+ el.setAttribute('title', 'Password');
144
+ el.setAttribute('tooltip-text', 'At least 12 characters');
145
+ return el;
146
+ },
147
+ (el) => {
148
+ expect(el.getAttribute('title')).toBe('Password');
149
+ expect(el.querySelector('.adw-password-entry-row-text')?.getAttribute('title')).toBe(
150
+ 'At least 12 characters',
151
+ );
152
+ },
153
+ );
154
+ });
155
+
156
+ // BLOCKER 1: the tooltip parts must cover the row's full hit area (height).
157
+ // GTK draws the tooltip over the whole widget; the port was only covering the
158
+ // label text's own height. The `[data-row-tooltip]` marker + `align-self: stretch`
159
+ // in `_row.scss` stretches the parts, and this assertion prevents regression.
160
+ await it("tooltip parts cover the row's full height (>= 0.95 of row height)", () => {
161
+ for (const [tag] of Object.entries(TOOLTIPPED_PARTS)) {
162
+ withWidget(
163
+ () => {
164
+ const el = document.createElement(tag);
165
+ el.setAttribute('title', 'Wi-Fi');
166
+ el.setAttribute('tooltip-text', 'Connects to the office network');
167
+ return el;
168
+ },
169
+ (el) => {
170
+ // Force layout so getBoundingClientRect is accurate
171
+ el.style.position = 'absolute';
172
+ el.style.top = '-9999px';
173
+ document.body.appendChild(el);
174
+ try {
175
+ const ratio = tooltipCoverageRatio(el);
176
+ expect(ratio).toBeGreaterThanOrEqual(0.95);
177
+ } finally {
178
+ el.remove();
179
+ }
180
+ },
181
+ );
182
+ }
183
+ });
184
+ });
185
+
186
+ await describe('adw-toggle-group sensitivity', async () => {
187
+ const group = (attrs: Record<string, string>, toggles: string[]) => () => {
188
+ const el = document.createElement('adw-toggle-group');
189
+ for (const [name, value] of Object.entries(attrs)) el.setAttribute(name, value);
190
+ el.innerHTML = toggles.join('');
191
+ return el;
192
+ };
193
+ const buttons = (el: Element) => Array.from(el.querySelectorAll<HTMLButtonElement>('button.adw-toggle'));
194
+
195
+ await it('defaults to sensitive, so a bare group is operable', () => {
196
+ withWidget(group({}, ['<adw-toggle label="One"></adw-toggle>']), (el) => {
197
+ const group = el as unknown as AdwToggleGroup;
198
+ expect(group.sensitive).toBe(true);
199
+ expect(el.hasAttribute('disabled')).toBe(false);
200
+ expect(buttons(el)[0].disabled).toBe(false);
201
+ });
202
+ });
203
+
204
+ await it('reads `sensitive` by VALUE, not by presence', () => {
205
+ // `GtkWidget:sensitive` defaults to TRUE. Read by presence, the bare element
206
+ // would be insensitive and every group on a page would be dimmed and dead.
207
+ withWidget(group({ sensitive: 'false' }, ['<adw-toggle label="One"></adw-toggle>']), (el) => {
208
+ const group = el as unknown as AdwToggleGroup;
209
+ expect(group.sensitive).toBe(false);
210
+ expect(el.getAttribute('disabled')).toBe('');
211
+ expect(buttons(el)[0].disabled).toBe(true);
212
+ });
213
+ });
214
+
215
+ await it('an explicit `sensitive="true"` is sensitive', () => {
216
+ withWidget(group({ sensitive: 'true' }, ['<adw-toggle label="One"></adw-toggle>']), (el) => {
217
+ expect((el as unknown as AdwToggleGroup).sensitive).toBe(true);
218
+ expect(el.hasAttribute('disabled')).toBe(false);
219
+ });
220
+ });
221
+
222
+ await it('sensitive is reachable as a property and takes effect live', () => {
223
+ withWidget(
224
+ group({}, ['<adw-toggle label="One"></adw-toggle>', '<adw-toggle label="Two"></adw-toggle>']),
225
+ (el) => {
226
+ const group = el as unknown as AdwToggleGroup;
227
+ expect(buttons(el).map((btn) => btn.disabled)).toStrictEqual([false, false]);
228
+
229
+ group.sensitive = false;
230
+ expect(buttons(el).map((btn) => btn.disabled)).toStrictEqual([true, true]);
231
+ expect(el.getAttribute('disabled')).toBe('');
232
+
233
+ group.sensitive = true;
234
+ expect(buttons(el).map((btn) => btn.disabled)).toStrictEqual([false, false]);
235
+ expect(el.hasAttribute('disabled')).toBe(false);
236
+ },
237
+ );
238
+ });
239
+
240
+ await it('one toggle takes `enabled="false"` and only that one', () => {
241
+ // `AdwToggle:enabled` — `add_toggle` spends it on the button it builds
242
+ // (adw-toggle-group.c:871). `boolAttribute` is not its reading: the property
243
+ // defaults TRUE, so a bare `<adw-toggle>` is enabled.
244
+ withWidget(
245
+ group({}, [
246
+ '<adw-toggle label="One"></adw-toggle>',
247
+ '<adw-toggle label="Two" enabled="false"></adw-toggle>',
248
+ ]),
249
+ (el) => {
250
+ expect(buttons(el).map((btn) => btn.disabled)).toStrictEqual([false, true]);
251
+ expect((el as unknown as AdwToggleGroup).isToggleEnabled(0)).toBe(true);
252
+ expect((el as unknown as AdwToggleGroup).isToggleEnabled(1)).toBe(false);
253
+ },
254
+ );
255
+ });
256
+
257
+ await it('an insensitive toggle cannot be selected', () => {
258
+ // `set_active_toggle` refuses a toggle that is not enabled (:720), so a
259
+ // disabled button and an unreachable selection cannot disagree.
260
+ withWidget(
261
+ group({}, [
262
+ '<adw-toggle label="One"></adw-toggle>',
263
+ '<adw-toggle label="Two" enabled="false"></adw-toggle>',
264
+ ]),
265
+ (el) => {
266
+ buttons(el)[1].click();
267
+ expect((el as unknown as AdwToggleGroup).active).toBe(0);
268
+ expect(buttons(el).map((btn) => btn.classList.contains('active'))).toStrictEqual([true, false]);
269
+ // The reflected `active` is written only on a real CHANGE
270
+ // (`_selectIndex`), and this press was not one — so it stays absent,
271
+ // the way a group nobody has touched reads.
272
+ expect(el.hasAttribute('active')).toBe(false);
273
+ },
274
+ );
275
+ });
276
+
277
+ await it('an active-but-insensitive toggle hands the selection to an enabled one', () => {
278
+ // `active="1"` beside `<adw-toggle enabled="false">` is a pair upstream cannot
279
+ // express — `adw_toggle_set_enabled` clears the selection instead (:1659). Left
280
+ // alone it would be a checked radio nothing can focus or click.
281
+ withWidget(
282
+ group({ active: '1' }, [
283
+ '<adw-toggle label="One"></adw-toggle>',
284
+ '<adw-toggle label="Two" enabled="false"></adw-toggle>',
285
+ ]),
286
+ (el) => {
287
+ expect((el as unknown as AdwToggleGroup).active).toBe(0);
288
+ expect(buttons(el).map((btn) => btn.classList.contains('active'))).toStrictEqual([true, false]);
289
+ // And the group's ONE tab stop is on the toggle that can take it.
290
+ expect(buttons(el).map((btn) => btn.tabIndex)).toStrictEqual([0, -1]);
291
+ },
292
+ );
293
+ });
294
+
295
+ await it('a group with no enabled toggle leaves the selection alone', () => {
296
+ withWidget(
297
+ group({}, [
298
+ '<adw-toggle label="One" enabled="false"></adw-toggle>',
299
+ '<adw-toggle label="Two" enabled="false"></adw-toggle>',
300
+ ]),
301
+ (el) => {
302
+ // Nothing to move it to. The selection stays where the core put it
303
+ // (index 0), so the first button gets tabIndex 0 (the group's one
304
+ // tab stop) even though it is disabled — the roving filter will
305
+ // skip it, but the attribute reflects the active index.
306
+ expect((el as unknown as AdwToggleGroup).active).toBe(0);
307
+ expect(buttons(el).every((btn) => btn.disabled)).toBe(true);
308
+ expect(buttons(el).map((btn) => btn.tabIndex)).toStrictEqual([0, -1]);
309
+ },
310
+ );
311
+ });
312
+ });
313
+ };
@@ -0,0 +1,52 @@
1
+ // `Gtk.Widget:tooltip-text` on a boxed-list row.
2
+ //
3
+ // A BROWSER'S TOOLTIP IS THE `title` CONTENT ATTRIBUTE, and on a row that attribute is
4
+ // already spoken for: `Adw.PreferencesRow:title` is the row's LABEL and is authored as
5
+ // `title=`, which is the same attribute `HTMLElement.title` reflects. Writing the tooltip
6
+ // to the host would blank the label, so it goes to the row's own parts instead — and the
7
+ // browser then finds it FIRST. Firefox resolves a tooltip by walking UP from the hovered
8
+ // node until an element carries a non-empty `title`, so one `title` per part covers
9
+ // everything inside it. Two consequences, both wanted:
10
+ //
11
+ // - a SLOTTED control that carries a tooltip of its own still wins, which is GTK's
12
+ // precedence too — `gtk_widget_set_tooltip_text` on a child replaces the parent's;
13
+ // - a part the element owns for its OWN tooltip keeps it, which is why the entry row
14
+ // hands over its prefixes / area / suffixes and not its apply button.
15
+ //
16
+ // A PART ONLY COUNTS IF IT COVERS THE ROW. Measured on the shipped row before the
17
+ // `data-row-tooltip` marker existed: a 884x54 `adw-action-row` offered the LABEL over 36 of
18
+ // its 54 rows of pixels and across its 24px of inline padding, and the tooltip only over the
19
+ // 18px the label text happens to occupy — the label column is content-height in a 54px row,
20
+ // so two thirds of the widget's own hit area fell through to the host and found the label.
21
+ // The tooltip has to reach the whole widget the way GTK's does, so the marked part STRETCHES
22
+ // (`scss/_row.scss`) and the row's inline padding is what is left; `row-tooltip.spec.ts`
23
+ // measures the covered region against the row's box so this cannot come back.
24
+ //
25
+ // Upstream has no stylesheet rule for this: GTK draws the tooltip surface itself
26
+ // (`refs/libadwaita/src/stylesheet/widgets/_tooltip.scss`), and `refs/adwaita-web` has no
27
+ // `tooltip` anywhere. The browser's own tooltip IS the port.
28
+
29
+ /**
30
+ * Apply `row`'s `tooltip-text` to `parts`, or clear it when the attribute is absent.
31
+ *
32
+ * `parts` are the row's OWN sections, never a slotted consumer widget — see the header.
33
+ * Each one is marked `data-row-tooltip`, which is the hook `scss/_row.scss` stretches so
34
+ * the tooltip covers the row rather than the label text inside it.
35
+ */
36
+ export function applyRowTooltip(row: HTMLElement, parts: readonly Element[]): void {
37
+ const tooltip = row.getAttribute('tooltip-text') ?? '';
38
+ for (const part of parts) {
39
+ if (tooltip) {
40
+ // Only on a real change: `_render` runs on every unrelated attribute write, and
41
+ // `setAttribute` fires a mutation record even for the value already there.
42
+ if (part.getAttribute('title') !== tooltip) part.setAttribute('title', tooltip);
43
+ if (!part.hasAttribute(TOOLTIP_PART_ATTR)) part.setAttribute(TOOLTIP_PART_ATTR, '');
44
+ } else {
45
+ part.removeAttribute('title');
46
+ part.removeAttribute(TOOLTIP_PART_ATTR);
47
+ }
48
+ }
49
+ }
50
+
51
+ /** The hook `applyRowTooltip` marks its parts with, and `scss/_row.scss` selects on. */
52
+ const TOOLTIP_PART_ATTR = 'data-row-tooltip';