@gjsify/adwaita-web 0.42.0 → 0.44.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.
- package/README.md +27 -0
- package/lib/types/adw-alert-dialog.spec.d.ts +1 -0
- package/lib/types/elements/adw-alert-dialog.d.ts +12 -0
- package/lib/types/elements/adw-carousel.d.ts +8 -0
- package/lib/types/elements/adw-entry-row.d.ts +5 -7
- package/lib/types/elements/adw-header-bar.d.ts +7 -0
- package/lib/types/elements/adw-navigation-split-view.d.ts +9 -0
- package/lib/types/elements/adw-preferences-group.d.ts +18 -9
- package/lib/types/elements/swipe-drag.d.ts +63 -0
- package/lib/types/slotted-children.d.ts +65 -0
- package/lib/types/slotted-children.spec.d.ts +1 -0
- package/package.json +6 -6
- package/src/adw-alert-dialog.spec.ts +146 -0
- package/src/adw-header-bar.spec.ts +4 -2
- package/src/elements/adw-action-row.ts +8 -6
- package/src/elements/adw-alert-dialog.ts +99 -23
- package/src/elements/adw-carousel.ts +86 -5
- package/src/elements/adw-dialog.ts +5 -6
- package/src/elements/adw-entry-row.ts +33 -34
- package/src/elements/adw-expander-row.ts +19 -15
- package/src/elements/adw-header-bar.ts +39 -20
- package/src/elements/adw-navigation-split-view.ts +31 -8
- package/src/elements/adw-overlay-split-view.ts +11 -12
- package/src/elements/adw-preferences-dialog.ts +7 -8
- package/src/elements/adw-preferences-group.ts +37 -36
- package/src/elements/adw-status-page.ts +10 -4
- package/src/elements/adw-toast-overlay.ts +6 -4
- package/src/elements/adw-toolbar-view.ts +16 -11
- package/src/elements/swipe-drag.ts +450 -0
- package/src/slotted-children.spec.ts +227 -0
- package/src/slotted-children.ts +212 -0
- package/src/test.browser.mts +4 -0
|
@@ -14,9 +14,21 @@
|
|
|
14
14
|
// Attributes:
|
|
15
15
|
// heading (the bold heading text)
|
|
16
16
|
// body (the body text below the heading)
|
|
17
|
+
// heading-use-markup (boolean — read `heading` as markup rather than plain text)
|
|
18
|
+
// body-use-markup (boolean — read `body` as markup rather than plain text)
|
|
19
|
+
// close-response (the response ID emitted on dismissal; default `close`)
|
|
20
|
+
// default-response (the response ID whose button is highlighted and focused)
|
|
17
21
|
// open (boolean — whether the dialog is shown; default closed)
|
|
18
22
|
// prefer-wide-layout (boolean — lay responses out horizontally when they fit)
|
|
19
23
|
//
|
|
24
|
+
// MARKUP IS OPT-IN, and here that MATCHES libadwaita rather than departing from it:
|
|
25
|
+
// `Adw.AlertDialog:heading-use-markup` and `:body-use-markup` both default to FALSE
|
|
26
|
+
// (`Adw.Banner:use-markup` is the one that defaults to TRUE, which is why
|
|
27
|
+
// `adw-banner.ts` documents a deliberate departure and this file does not need to).
|
|
28
|
+
// Opting in renders through `innerHTML`, which is the author asserting the string is
|
|
29
|
+
// trusted — Pango markup has a fixed tag set and no scripting, HTML has neither
|
|
30
|
+
// property.
|
|
31
|
+
//
|
|
20
32
|
// Buttons are stacked vertically by default (the Adw.AlertDialog default at medium
|
|
21
33
|
// sizes) and laid out horizontally when prefer-wide-layout is set and there are no more
|
|
22
34
|
// than two of them — the libadwaita measure/allocate heuristic in spirit, not to the px.
|
|
@@ -45,6 +57,7 @@ import { AdwAlertResponses } from '@gjsify/adwaita-core';
|
|
|
45
57
|
import type { AdwResponseAppearance } from '@gjsify/adwaita-core';
|
|
46
58
|
|
|
47
59
|
import { bindEmptySections } from '../empty-sections.js';
|
|
60
|
+
import { bindSlottedChildren } from '../slotted-children.js';
|
|
48
61
|
import { AdwModalSurface } from './modal-surface.js';
|
|
49
62
|
|
|
50
63
|
/** Response button appearance — mirrors Adw.ResponseAppearance. */
|
|
@@ -81,7 +94,16 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
81
94
|
private _blockCloseResponse = false;
|
|
82
95
|
|
|
83
96
|
static get observedAttributes() {
|
|
84
|
-
return [
|
|
97
|
+
return [
|
|
98
|
+
'heading',
|
|
99
|
+
'body',
|
|
100
|
+
'heading-use-markup',
|
|
101
|
+
'body-use-markup',
|
|
102
|
+
'close-response',
|
|
103
|
+
'default-response',
|
|
104
|
+
'open',
|
|
105
|
+
'prefer-wide-layout',
|
|
106
|
+
];
|
|
85
107
|
}
|
|
86
108
|
|
|
87
109
|
get heading(): string {
|
|
@@ -109,6 +131,26 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
109
131
|
else this.removeAttribute('open');
|
|
110
132
|
}
|
|
111
133
|
|
|
134
|
+
/** Whether {@link heading} is read as markup rather than plain text. */
|
|
135
|
+
get headingUseMarkup(): boolean {
|
|
136
|
+
return this.hasAttribute('heading-use-markup');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
set headingUseMarkup(value: boolean) {
|
|
140
|
+
if (value) this.setAttribute('heading-use-markup', '');
|
|
141
|
+
else this.removeAttribute('heading-use-markup');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Whether {@link body} is read as markup rather than plain text. */
|
|
145
|
+
get bodyUseMarkup(): boolean {
|
|
146
|
+
return this.hasAttribute('body-use-markup');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
set bodyUseMarkup(value: boolean) {
|
|
150
|
+
if (value) this.setAttribute('body-use-markup', '');
|
|
151
|
+
else this.removeAttribute('body-use-markup');
|
|
152
|
+
}
|
|
153
|
+
|
|
112
154
|
/** Whether to prefer horizontal button layout when the buttons fit. */
|
|
113
155
|
get preferWideLayout(): boolean {
|
|
114
156
|
return this.hasAttribute('prefer-wide-layout');
|
|
@@ -141,12 +183,6 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
141
183
|
if (this._initialized) return;
|
|
142
184
|
this._initialized = true;
|
|
143
185
|
|
|
144
|
-
// Declared <adw-alert-response> children are snapshotted before the element takes
|
|
145
|
-
// over the subtree; everything else becomes the extra child.
|
|
146
|
-
const declared = Array.from(this.querySelectorAll(':scope > adw-alert-response')) as AdwAlertResponse[];
|
|
147
|
-
const declaredSet = new Set<Node>(declared);
|
|
148
|
-
const extraChildren = Array.from(this.childNodes).filter((n) => !declaredSet.has(n));
|
|
149
|
-
|
|
150
186
|
this._scrimEl = document.createElement('div');
|
|
151
187
|
this._scrimEl.className = 'adw-alert-dialog-scrim';
|
|
152
188
|
this._scrimEl.addEventListener('click', () => this._dismiss());
|
|
@@ -176,36 +212,66 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
176
212
|
|
|
177
213
|
this._childEl = document.createElement('div');
|
|
178
214
|
this._childEl.className = 'adw-alert-dialog-child';
|
|
179
|
-
for (const child of extraChildren) this._childEl.appendChild(child);
|
|
180
215
|
|
|
181
216
|
messageArea.append(this._headingEl, this._bodyEl, this._childEl);
|
|
182
|
-
// `_render` runs from `attributeChangedCallback` and from the response API, and
|
|
183
|
-
// an extra child appended after connect triggers neither.
|
|
184
|
-
bindEmptySections(this._childEl);
|
|
185
217
|
|
|
186
218
|
this._responseAreaEl = document.createElement('div');
|
|
187
219
|
this._responseAreaEl.className = 'adw-alert-dialog-responses';
|
|
188
220
|
|
|
189
221
|
this._dialogEl.append(messageArea, this._responseAreaEl);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
222
|
+
|
|
223
|
+
// Two slots, both LIVE. An `<adw-alert-response>` is DATA — read for its id, label
|
|
224
|
+
// and appearance and then gone, as GtkBuilder leaves nothing of a `<responses>`
|
|
225
|
+
// entry in the widget tree — so it is consumed rather than re-homed, and
|
|
226
|
+
// `adw_alert_dialog_add_response` is callable at any point in the dialog's life.
|
|
227
|
+
// Everything else is the extra child. `src/slotted-children.ts` has the incident.
|
|
228
|
+
// Installed AFTER the response area exists, because consuming a response fills it.
|
|
229
|
+
bindSlottedChildren(this, [
|
|
230
|
+
{
|
|
231
|
+
claims: (node) => node instanceof Element && node.localName === 'adw-alert-response',
|
|
232
|
+
consume: (node) => this._adoptResponse(node as AdwAlertResponse),
|
|
233
|
+
},
|
|
234
|
+
{ into: this._childEl },
|
|
235
|
+
]).install(this._scrimEl, this._dialogEl);
|
|
236
|
+
|
|
237
|
+
// `_render` runs from `attributeChangedCallback` and from the response API, and an
|
|
238
|
+
// extra child appended after connect triggers neither. AFTER the routing: this
|
|
239
|
+
// derives once synchronously, and a `hidden` child area is not focusable, so
|
|
240
|
+
// deriving it on the not-yet-filled box sent the initial focus to the default
|
|
241
|
+
// response instead of into the content.
|
|
242
|
+
bindEmptySections(this._childEl);
|
|
200
243
|
|
|
201
244
|
// Seed the headless model's text from the parsed markup; the attributes stay the
|
|
202
245
|
// source of truth for what is rendered.
|
|
203
246
|
this._model.heading = this.heading;
|
|
204
247
|
this._model.body = this.body;
|
|
205
248
|
|
|
249
|
+
// And the two response IDs. `attributeChangedCallback` returns early until
|
|
250
|
+
// `_initialized`, so an attribute present in the MARKUP — the declarative case
|
|
251
|
+
// these attributes exist for — is never delivered to it. `heading` and `body`
|
|
252
|
+
// survive that only because `_render()` re-reads them from the attribute; these
|
|
253
|
+
// two live in the model and would simply be dropped.
|
|
254
|
+
const closeResponse = this.getAttribute('close-response');
|
|
255
|
+
if (closeResponse !== null) this._model.closeResponse = closeResponse;
|
|
256
|
+
this._model.defaultResponse = this.getAttribute('default-response');
|
|
257
|
+
|
|
206
258
|
this._render();
|
|
207
259
|
}
|
|
208
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Take one declared `<adw-alert-response>` as a response. An entry with no `id` is
|
|
263
|
+
* skipped rather than given a generated one: the id is what `response` events carry, so
|
|
264
|
+
* inventing one would hand the consumer a name it never wrote.
|
|
265
|
+
*/
|
|
266
|
+
private _adoptResponse(el: AdwAlertResponse): void {
|
|
267
|
+
const id = el.getAttribute('id');
|
|
268
|
+
if (!id) return;
|
|
269
|
+
const appearance = (el.getAttribute('appearance') ?? 'default') as AdwResponseAppearance;
|
|
270
|
+
this.addResponse(id, (el.textContent ?? '').trim());
|
|
271
|
+
if (appearance !== 'default') this.setResponseAppearance(id, appearance);
|
|
272
|
+
if (el.getAttribute('enabled') === 'false') this.setResponseEnabled(id, false);
|
|
273
|
+
}
|
|
274
|
+
|
|
209
275
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
|
|
210
276
|
if (!this._initialized) return;
|
|
211
277
|
if (oldValue === newValue) return;
|
|
@@ -220,6 +286,14 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
220
286
|
} else if (name === 'body') {
|
|
221
287
|
this._model.body = this.body;
|
|
222
288
|
this.dispatchEvent(new CustomEvent('notify::body', { bubbles: true, detail: { body: this.body } }));
|
|
289
|
+
} else if (name === 'close-response') {
|
|
290
|
+
// The MODEL stays the source of truth for response semantics (see the
|
|
291
|
+
// header) — the attribute is a markup entry point that feeds it, so the
|
|
292
|
+
// setter is not mirrored back and there is no reflection loop. An absent
|
|
293
|
+
// attribute means "leave the model's default", not "reset to `close`".
|
|
294
|
+
if (newValue !== null) this.setCloseResponse(newValue);
|
|
295
|
+
} else if (name === 'default-response') {
|
|
296
|
+
this.setDefaultResponse(newValue);
|
|
223
297
|
} else if (name === 'open') {
|
|
224
298
|
this.dispatchEvent(new CustomEvent('notify::open', { bubbles: true, detail: { open: this.open } }));
|
|
225
299
|
if (this.open) this._modal.present();
|
|
@@ -386,11 +460,13 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
386
460
|
this.classList.toggle('open', this.open);
|
|
387
461
|
|
|
388
462
|
const heading = this.heading;
|
|
389
|
-
this._headingEl.
|
|
463
|
+
if (this.headingUseMarkup) this._headingEl.innerHTML = heading;
|
|
464
|
+
else this._headingEl.textContent = heading;
|
|
390
465
|
this._headingEl.hidden = heading.length === 0;
|
|
391
466
|
|
|
392
467
|
const body = this.body;
|
|
393
|
-
this._bodyEl.
|
|
468
|
+
if (this.bodyUseMarkup) this._bodyEl.innerHTML = body;
|
|
469
|
+
else this._bodyEl.textContent = body;
|
|
394
470
|
this._bodyEl.hidden = body.length === 0;
|
|
395
471
|
|
|
396
472
|
// Horizontal only when prefer-wide-layout is set AND there are at most two
|
|
@@ -9,9 +9,14 @@
|
|
|
9
9
|
// position compensation. This element is the DOM half only — scroll-snap strip,
|
|
10
10
|
// offset↔position, DOM events.
|
|
11
11
|
//
|
|
12
|
-
// The
|
|
13
|
-
// `interactive` default TRUE, so `="false"` is the off switch;
|
|
14
|
-
// is an ordinary presence attribute.
|
|
12
|
+
// The four attribute defaults HTML cannot spell: `allow-scroll-wheel`,
|
|
13
|
+
// `allow-mouse-drag` and `interactive` default TRUE, so `="false"` is the off switch;
|
|
14
|
+
// `allow-long-swipes` is an ordinary presence attribute.
|
|
15
|
+
//
|
|
16
|
+
// A MOUSE DRAG is `elements/swipe-drag.ts`, and it is the one gesture a browser gives
|
|
17
|
+
// nothing for: touch and touchpad scroll this strip natively, a held mouse button does
|
|
18
|
+
// not. Upstream turns it on in `init` (:1209) and exposes `allow-mouse-drag`, default
|
|
19
|
+
// TRUE (:1091). Measured before: a 300 px drag moved `position` 0 → 0. `position` is the INITIAL page only —
|
|
15
20
|
// write-only, with the live value on the `position` property and `notify::position`.
|
|
16
21
|
//
|
|
17
22
|
// `page-changed` (`detail = { index }`) fires on every settle, including back
|
|
@@ -27,9 +32,11 @@
|
|
|
27
32
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
28
33
|
// Modifications: Implemented as Web Components for @gjsify/adwaita-web.
|
|
29
34
|
|
|
30
|
-
import { CarouselState, carouselPageAllocation } from '@gjsify/adwaita-core';
|
|
35
|
+
import { CarouselState, carouselClosestSnapPoint, carouselPageAllocation } from '@gjsify/adwaita-core';
|
|
31
36
|
import type { CarouselScrollRequest, CarouselScrollSource, CarouselStateChange } from '@gjsify/adwaita-core';
|
|
32
37
|
|
|
38
|
+
import { attachSwipeDrag, type AdwSwipeDrag } from './swipe-drag.js';
|
|
39
|
+
|
|
33
40
|
/** Common surface an indicator binds to. The concrete element is AdwCarousel. */
|
|
34
41
|
interface CarouselHost extends HTMLElement {
|
|
35
42
|
readonly nPages: number;
|
|
@@ -94,9 +101,11 @@ export class AdwCarousel extends HTMLElement implements CarouselHost {
|
|
|
94
101
|
* index while the target page stays the target.
|
|
95
102
|
*/
|
|
96
103
|
private _scrollTargetId: string | null = null;
|
|
104
|
+
/** The pointer-drag gesture, asked whether it owns the strip. See `_onWheel`. */
|
|
105
|
+
private _drag: AdwSwipeDrag | undefined;
|
|
97
106
|
|
|
98
107
|
static get observedAttributes() {
|
|
99
|
-
return ['allow-scroll-wheel', 'allow-long-swipes', 'interactive', 'spacing', 'position'];
|
|
108
|
+
return ['allow-scroll-wheel', 'allow-mouse-drag', 'allow-long-swipes', 'interactive', 'spacing', 'position'];
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
constructor() {
|
|
@@ -132,6 +141,18 @@ export class AdwCarousel extends HTMLElement implements CarouselHost {
|
|
|
132
141
|
return this._state.pageAt(this._state.position);
|
|
133
142
|
}
|
|
134
143
|
|
|
144
|
+
/**
|
|
145
|
+
* `AdwCarousel:allow-mouse-drag` — whether the strip can be dragged with the pointer.
|
|
146
|
+
* Defaults to TRUE, as in libadwaita, where FALSE leaves dragging to touch only.
|
|
147
|
+
*/
|
|
148
|
+
get allowMouseDrag(): boolean {
|
|
149
|
+
return this._state.allowMouseDrag;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
set allowMouseDrag(value: boolean) {
|
|
153
|
+
this.setAttribute('allow-mouse-drag', value ? 'true' : 'false');
|
|
154
|
+
}
|
|
155
|
+
|
|
135
156
|
/** Whether the scroll wheel navigates between pages. Defaults to TRUE, as in libadwaita. */
|
|
136
157
|
get allowScrollWheel(): boolean {
|
|
137
158
|
return this._state.allowScrollWheel;
|
|
@@ -187,6 +208,7 @@ export class AdwCarousel extends HTMLElement implements CarouselHost {
|
|
|
187
208
|
// Declared properties BEFORE children: `spacing` is part of the page pitch,
|
|
188
209
|
// so a page added first would be measured against the wrong distance.
|
|
189
210
|
this._applyAttribute('allow-scroll-wheel', this.getAttribute('allow-scroll-wheel'));
|
|
211
|
+
this._applyAttribute('allow-mouse-drag', this.getAttribute('allow-mouse-drag'));
|
|
190
212
|
this._applyAttribute('allow-long-swipes', this.getAttribute('allow-long-swipes'));
|
|
191
213
|
this._applyAttribute('interactive', this.getAttribute('interactive'));
|
|
192
214
|
this._applyAttribute('spacing', this.getAttribute('spacing'));
|
|
@@ -200,6 +222,52 @@ export class AdwCarousel extends HTMLElement implements CarouselHost {
|
|
|
200
222
|
this._trackEl.addEventListener('scroll', this._onScroll, { passive: true });
|
|
201
223
|
this._trackEl.addEventListener('wheel', this._onWheel, { passive: false });
|
|
202
224
|
|
|
225
|
+
this._drag = attachSwipeDrag({
|
|
226
|
+
// On the TRACK, not on the host: the track is the scroll container the drag
|
|
227
|
+
// writes `scrollLeft` on, and it is what the pages live in.
|
|
228
|
+
host: this._trackEl,
|
|
229
|
+
orientation: 'horizontal',
|
|
230
|
+
// `interactive` gates it because that is exactly what it does upstream —
|
|
231
|
+
// `adw_carousel_set_interactive` is `adw_swipe_tracker_set_enabled` (:1705).
|
|
232
|
+
enabled: () => this._state.interactive && this._state.allowMouseDrag,
|
|
233
|
+
pitch: () => this._distance(),
|
|
234
|
+
points: () => this._state.snapPoints,
|
|
235
|
+
progress: () => this._state.position,
|
|
236
|
+
allowLongSwipes: () => this._state.allowLongSwipes,
|
|
237
|
+
// `adw_carousel_get_cancel_progress` is `get_closest_snap_point`
|
|
238
|
+
// (adw-carousel.c:1294-1299) — the page nearest where the gesture was
|
|
239
|
+
// interrupted, NOT the one it started from.
|
|
240
|
+
//
|
|
241
|
+
// `carouselClosestSnapPoint` and NOT `_state.pageAt`, for two reasons that
|
|
242
|
+
// both make the answer a different page. `pageAt` searches the VISIBLE snap
|
|
243
|
+
// points (it filters out children being removed) and returns an index into
|
|
244
|
+
// that filtered array, so subscripting the unfiltered list with it shifts by
|
|
245
|
+
// one for every removing child ahead of the target. And it is the
|
|
246
|
+
// `count_removing = FALSE` variant, while `get_closest_snap_point` passes
|
|
247
|
+
// TRUE (adw-carousel.c:1298) — this is the one call site that counts them.
|
|
248
|
+
//
|
|
249
|
+
// The POINT, not an index: a page mid-reveal has a fractional size, so
|
|
250
|
+
// `carouselSnapPoints` (cumulative size − 1) hands back non-integers.
|
|
251
|
+
cancelProgress: () => {
|
|
252
|
+
const points = this._state.snapPoints;
|
|
253
|
+
return points[carouselClosestSnapPoint(this._state.position, points)] ?? this._state.position;
|
|
254
|
+
},
|
|
255
|
+
// The DRAG writes the offset directly and the existing scroll handler reads
|
|
256
|
+
// the position back off it — the same path a touch scroll takes, so there is
|
|
257
|
+
// no second route into `position` to drift from this one.
|
|
258
|
+
onUpdate: (progress) => {
|
|
259
|
+
const distance = this._distance();
|
|
260
|
+
if (distance > 0) this._trackEl.scrollLeft = progress * distance;
|
|
261
|
+
},
|
|
262
|
+
// `scroll_to` with the swipe's velocity is what `end_swipe_cb` does (:434).
|
|
263
|
+
// Here the settle is the browser's smooth scroll, so the velocity is unused
|
|
264
|
+
// — snap is already restored, and `to` is a snap point by construction.
|
|
265
|
+
onEnd: (progress) => {
|
|
266
|
+
const distance = this._distance();
|
|
267
|
+
if (distance > 0) this._trackEl.scrollTo({ left: progress * distance });
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
|
|
203
271
|
// Applied last, once the pages it names exist. Custom-element upgrade
|
|
204
272
|
// delivers attributes BEFORE `connectedCallback`, so reading it here is the
|
|
205
273
|
// only way `<adw-carousel position="2">` works at all.
|
|
@@ -246,6 +314,10 @@ export class AdwCarousel extends HTMLElement implements CarouselHost {
|
|
|
246
314
|
// usual "presence = on" spelling would invert it.
|
|
247
315
|
this._state.setAllowScrollWheel(value !== 'false');
|
|
248
316
|
return;
|
|
317
|
+
case 'allow-mouse-drag':
|
|
318
|
+
// Absent means TRUE, like `allow-scroll-wheel` and for the same reason.
|
|
319
|
+
this._state.setAllowMouseDrag(value !== 'false');
|
|
320
|
+
return;
|
|
249
321
|
case 'allow-long-swipes':
|
|
250
322
|
this._state.setAllowLongSwipes(value !== null && value !== 'false');
|
|
251
323
|
this._applySnapStop();
|
|
@@ -546,6 +618,15 @@ export class AdwCarousel extends HTMLElement implements CarouselHost {
|
|
|
546
618
|
};
|
|
547
619
|
|
|
548
620
|
private _onWheel = (event: WheelEvent): void => {
|
|
621
|
+
// A drag owns the offset while it is claimed. Without this a single wheel notch
|
|
622
|
+
// mid-drag ran the whole paging path and moved `scrollLeft` a full page in one
|
|
623
|
+
// frame — measured 80 → 440 — which the next pointer move then yanked back, and
|
|
624
|
+
// it left `_scrollTargetId` pointing at a page the drag overrode. `GDK_EVENT_STOP`
|
|
625
|
+
// rather than propagate, because the gesture IS handling this widget's scrolling.
|
|
626
|
+
if (this._drag?.dragging === true) {
|
|
627
|
+
event.preventDefault();
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
549
630
|
const step = this._state.handleWheel({
|
|
550
631
|
deltaX: event.deltaX,
|
|
551
632
|
deltaY: event.deltaY,
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
// Copyright (c) 2023-2024 GNOME Foundation Inc. (libadwaita). LGPLv2.1+.
|
|
25
25
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
26
26
|
|
|
27
|
+
import { bindSlottedChildren } from '../slotted-children.js';
|
|
27
28
|
import { AdwModalSurface } from './modal-surface.js';
|
|
28
29
|
|
|
29
30
|
/** Adaptive presentation of the dialog — mirrors Adw.DialogPresentationMode. */
|
|
@@ -141,10 +142,6 @@ export class AdwDialog extends HTMLElement {
|
|
|
141
142
|
|
|
142
143
|
this.classList.add('adw-dialog');
|
|
143
144
|
|
|
144
|
-
// Snapshot before the subtree is replaced: everything the author placed inside
|
|
145
|
-
// becomes the dialog content.
|
|
146
|
-
const contentChildren = Array.from(this.childNodes);
|
|
147
|
-
|
|
148
145
|
this._scrimEl = document.createElement('div');
|
|
149
146
|
this._scrimEl.className = 'adw-dialog-scrim';
|
|
150
147
|
this._scrimEl.addEventListener('click', () => this._attemptClose());
|
|
@@ -187,11 +184,13 @@ export class AdwDialog extends HTMLElement {
|
|
|
187
184
|
|
|
188
185
|
this._contentEl = document.createElement('div');
|
|
189
186
|
this._contentEl.className = 'adw-dialog-content';
|
|
190
|
-
for (const child of contentChildren) this._contentEl.appendChild(child);
|
|
191
187
|
|
|
192
188
|
this._boxEl.append(this._headerEl, this._contentEl);
|
|
193
189
|
this._scrimEl.appendChild(this._boxEl);
|
|
194
|
-
|
|
190
|
+
// Everything the author places inside is the dialog content — LIVE, because
|
|
191
|
+
// `Adw.Dialog:child` is a property and a dialog whose body is filled in after it
|
|
192
|
+
// was created is the ordinary case. `src/slotted-children.ts` has the incident.
|
|
193
|
+
bindSlottedChildren(this, [{ into: this._contentEl }]).install(this._scrimEl);
|
|
195
194
|
|
|
196
195
|
this._render();
|
|
197
196
|
}
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
type EntryRowRenderState,
|
|
38
38
|
} from '@gjsify/adwaita-core';
|
|
39
39
|
|
|
40
|
+
import { bindSlottedChildren, type AdwSlottedChildren } from '../slotted-children.js';
|
|
40
41
|
import { type AdwIcon, createAdwIcon } from './adw-icon.js';
|
|
41
42
|
|
|
42
43
|
/**
|
|
@@ -71,8 +72,11 @@ export class AdwEntryRow extends HTMLElement {
|
|
|
71
72
|
protected _prefixes!: HTMLDivElement;
|
|
72
73
|
protected _suffixes!: HTMLDivElement;
|
|
73
74
|
protected _initialized = false;
|
|
74
|
-
/**
|
|
75
|
-
|
|
75
|
+
/**
|
|
76
|
+
* The two slots, installed after `_onConnected` rather than inside `_build`: the order
|
|
77
|
+
* IS the contract — see the `install` call in `connectedCallback`.
|
|
78
|
+
*/
|
|
79
|
+
private _slots!: AdwSlottedChildren;
|
|
76
80
|
private _lastText = '';
|
|
77
81
|
private _lastLength = 0;
|
|
78
82
|
|
|
@@ -159,11 +163,20 @@ export class AdwEntryRow extends HTMLElement {
|
|
|
159
163
|
if (this.hasAttribute('text')) this._state.setText(this.getAttribute('text') ?? '');
|
|
160
164
|
this._renderTitle();
|
|
161
165
|
|
|
162
|
-
// Subclass parts first, THEN the author's children: `AdwPasswordEntryRow`
|
|
163
|
-
//
|
|
164
|
-
//
|
|
166
|
+
// Subclass parts first, THEN the author's children: `AdwPasswordEntryRow` installs
|
|
167
|
+
// its peek toggle from its own `init`, so a consumer suffix lands after it and both
|
|
168
|
+
// remain. That ordering is why the structure is installed HERE and not at the end of
|
|
169
|
+
// `_build` — `install` routes the author's children on its way, and doing it a phase
|
|
170
|
+
// earlier would put every declared suffix in front of the password row's toggle.
|
|
165
171
|
this._onConnected();
|
|
166
|
-
this.
|
|
172
|
+
this._slots.install(
|
|
173
|
+
this._prefixes,
|
|
174
|
+
this._area,
|
|
175
|
+
this._indicator,
|
|
176
|
+
this._applyButton,
|
|
177
|
+
this._editIcon,
|
|
178
|
+
this._suffixes,
|
|
179
|
+
);
|
|
167
180
|
|
|
168
181
|
this._lastText = this._state.text;
|
|
169
182
|
this._lastLength = this._state.textLength;
|
|
@@ -254,12 +267,6 @@ export class AdwEntryRow extends HTMLElement {
|
|
|
254
267
|
private _build(): void {
|
|
255
268
|
const prefix = this._classPrefix;
|
|
256
269
|
|
|
257
|
-
// Author-supplied light-DOM children are KEPT: `replaceChildren` used to
|
|
258
|
-
// discard them silently, so `<adw-entry-row><adw-button>Go</adw-button>`
|
|
259
|
-
// rendered without its button. Elements only — GtkBuildable also only
|
|
260
|
-
// ever adopts widgets, and keeping stray whitespace would make an empty
|
|
261
|
-
// affordance box measure as non-empty.
|
|
262
|
-
this._authored = [...this.children];
|
|
263
270
|
this._prefixes = document.createElement('div');
|
|
264
271
|
this._prefixes.className = `${prefix}-prefixes`;
|
|
265
272
|
this._suffixes = document.createElement('div');
|
|
@@ -322,34 +329,26 @@ export class AdwEntryRow extends HTMLElement {
|
|
|
322
329
|
this._editIcon = createAdwIcon('document-edit', 'adw-row-edit', `${prefix}-edit`);
|
|
323
330
|
this._editIcon.dataset.iconName = ENTRY_ROW_EDIT_ICON_NAME;
|
|
324
331
|
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
332
|
+
// Author-supplied light-DOM children are KEPT and stay LIVE: `replaceChildren` used
|
|
333
|
+
// to discard them silently, so `<adw-entry-row><adw-button>Go</adw-button>` rendered
|
|
334
|
+
// without its button, and the snapshot that fixed that was still a one-shot read — a
|
|
335
|
+
// suffix appended after connect sat outside the affordance box. ELEMENTS only, on
|
|
336
|
+
// both paths: GtkBuildable also only ever adopts widgets, and keeping stray
|
|
337
|
+
// whitespace would make an empty affordance box measure as non-empty. Consumed
|
|
338
|
+
// through the two `add*` methods, because their insertion rules differ —
|
|
339
|
+
// `addPrefix` PREPENDS, mirroring `gtk_box_prepend`. Structure order:
|
|
340
|
+
// indicator → apply → edit icon inside the editable area, prefixes/suffixes outside.
|
|
341
|
+
// `src/slotted-children.ts` has the incident.
|
|
342
|
+
this._slots = bindSlottedChildren(this, [
|
|
343
|
+
{ name: 'prefix', consume: (node) => this.addPrefix(node) },
|
|
344
|
+
{ name: 'suffix', claims: (node) => node instanceof Element, consume: (node) => this.addSuffix(node) },
|
|
345
|
+
]);
|
|
335
346
|
setPartVisible(this._prefixes, false);
|
|
336
347
|
setPartVisible(this._suffixes, false);
|
|
337
348
|
|
|
338
349
|
this.addEventListener('click', (event) => this._maybeFocus(event));
|
|
339
350
|
}
|
|
340
351
|
|
|
341
|
-
/**
|
|
342
|
-
* Re-home the children the author wrote. libadwaita's buildable maps an untyped
|
|
343
|
-
* `<child>` to a suffix; `slot="prefix"` is the web spelling of `type="prefix"`.
|
|
344
|
-
*/
|
|
345
|
-
private _adoptAuthored(): void {
|
|
346
|
-
for (const node of this._authored) {
|
|
347
|
-
if (node.getAttribute('slot') === 'prefix') this.addPrefix(node);
|
|
348
|
-
else this.addSuffix(node);
|
|
349
|
-
}
|
|
350
|
-
this._authored = [];
|
|
351
|
-
}
|
|
352
|
-
|
|
353
352
|
/**
|
|
354
353
|
* `pressed_cb`: a click landing on the row, the editable area, the indicator or
|
|
355
354
|
* either affordance box grabs focus; anything else (a suffix control, the apply
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { ExpanderState, deriveRowLabels } from '@gjsify/adwaita-core';
|
|
23
23
|
|
|
24
24
|
import { bindEmptySections } from '../empty-sections.js';
|
|
25
|
+
import { bindSlottedChildren } from '../slotted-children.js';
|
|
25
26
|
|
|
26
27
|
// SIDE-EFFECT import, deliberately separate from the type import below: it guarantees
|
|
27
28
|
// `adw-switch` is defined before this module's `customElements.define` can upgrade a
|
|
@@ -105,11 +106,6 @@ export class AdwExpanderRow extends HTMLElement {
|
|
|
105
106
|
if (this._initialized) return;
|
|
106
107
|
this._initialized = true;
|
|
107
108
|
|
|
108
|
-
const prefixChildren = Array.from(this.querySelectorAll(':scope > [slot="prefix"]'));
|
|
109
|
-
const suffixChildren = Array.from(this.querySelectorAll(':scope > [slot="suffix"]'));
|
|
110
|
-
const claimed = new Set<Node>([...prefixChildren, ...suffixChildren]);
|
|
111
|
-
const rows = Array.from(this.childNodes).filter((node) => !claimed.has(node));
|
|
112
|
-
|
|
113
109
|
this._headerEl = document.createElement('div');
|
|
114
110
|
this._headerEl.className = 'adw-expander-row-header';
|
|
115
111
|
|
|
@@ -130,25 +126,33 @@ export class AdwExpanderRow extends HTMLElement {
|
|
|
130
126
|
|
|
131
127
|
this._prefixEl = document.createElement('div');
|
|
132
128
|
this._prefixEl.className = 'adw-expander-row-prefix';
|
|
133
|
-
for (const child of prefixChildren) this._prefixEl.appendChild(child);
|
|
134
129
|
|
|
135
130
|
this._suffixEl = document.createElement('div');
|
|
136
131
|
this._suffixEl.className = 'adw-expander-row-suffix';
|
|
137
|
-
for (const child of suffixChildren) this._suffixEl.appendChild(child);
|
|
138
|
-
|
|
139
|
-
// Same reason as the action row's: `_render` is attribute-driven, and a widget
|
|
140
|
-
// appended into `prefixSection` after connect is a childList change nothing else
|
|
141
|
-
// hears. `contentSection` is NOT in here — an empty disclosure is still the
|
|
142
|
-
// disclosure, and `.expanded` alone decides whether it shows.
|
|
143
|
-
bindEmptySections(this._prefixEl, this._suffixEl);
|
|
144
132
|
|
|
145
133
|
this._headerEl.append(this._prefixEl, textEl, this._suffixEl, this._enableSwitch, this._chevronEl);
|
|
146
134
|
|
|
147
135
|
this._contentEl = document.createElement('div');
|
|
148
136
|
this._contentEl.className = 'adw-expander-row-content';
|
|
149
|
-
for (const row of rows) this._contentEl.appendChild(row);
|
|
150
137
|
|
|
151
|
-
|
|
138
|
+
// Three LIVE slots: the two header sections by name, and the disclosure rows as the
|
|
139
|
+
// unnamed default — `adw_expander_row_add_row` works at any point in the widget's
|
|
140
|
+
// life, so a row appended after connect must reach the content box rather than sit
|
|
141
|
+
// beside it. `src/slotted-children.ts` has the incident.
|
|
142
|
+
bindSlottedChildren(this, [
|
|
143
|
+
{ name: 'prefix', into: this._prefixEl },
|
|
144
|
+
{ name: 'suffix', into: this._suffixEl },
|
|
145
|
+
{ into: this._contentEl },
|
|
146
|
+
]).install(this._headerEl, this._contentEl);
|
|
147
|
+
|
|
148
|
+
// Same reason as the action row's: `_render` is attribute-driven, and a widget
|
|
149
|
+
// appended into `prefixSection` after connect is a childList change nothing else
|
|
150
|
+
// hears. `contentSection` is NOT in here — an empty disclosure is still the
|
|
151
|
+
// disclosure, and `.expanded` alone decides whether it shows.
|
|
152
|
+
//
|
|
153
|
+
// AFTER the routing: this derives once synchronously, so on a box the routing has
|
|
154
|
+
// not filled yet it hides a section that a declared child had already earned.
|
|
155
|
+
bindEmptySections(this._prefixEl, this._suffixEl);
|
|
152
156
|
|
|
153
157
|
this._headerEl.addEventListener('click', (event) => {
|
|
154
158
|
// Clicks on the enable switch toggle expansion-enable, not disclosure.
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
// Copyright (c) 2025 csm. MIT License.
|
|
17
17
|
// Modifications: Reimplemented as Web Component for @gjsify/adwaita-web.
|
|
18
18
|
|
|
19
|
+
import { bindSlottedChildren } from '../slotted-children.js';
|
|
20
|
+
|
|
19
21
|
// Registers <adw-window-title>: the derived centre is one, so importing the bar
|
|
20
22
|
// alone must still define it.
|
|
21
23
|
import './adw-window-title.js';
|
|
@@ -56,35 +58,41 @@ export class AdwHeaderBar extends HTMLElement {
|
|
|
56
58
|
if (this._initialized) return;
|
|
57
59
|
this._initialized = true;
|
|
58
60
|
|
|
59
|
-
// Capture any pre-existing slotted children before clearing
|
|
60
|
-
const startChildren = Array.from(this.querySelectorAll(':scope > [slot="start"]'));
|
|
61
|
-
const centerChildren = Array.from(this.querySelectorAll(':scope > [slot="center"]'));
|
|
62
|
-
const endChildren = Array.from(this.querySelectorAll(':scope > [slot="end"]'));
|
|
63
|
-
|
|
64
|
-
// Start section
|
|
65
61
|
this._startEl = document.createElement('div');
|
|
66
62
|
this._startEl.className = 'adw-header-bar-start';
|
|
67
|
-
for (const child of startChildren) this._startEl.appendChild(child);
|
|
68
63
|
|
|
69
|
-
// Center section — a `slot="center"` widget (title-widget) wins over the
|
|
70
|
-
// plain `title` text.
|
|
71
64
|
this._centerEl = document.createElement('div');
|
|
72
65
|
this._centerEl.className = 'adw-header-bar-center';
|
|
73
|
-
if (centerChildren.length > 0) {
|
|
74
|
-
for (const child of centerChildren) this._centerEl.appendChild(child);
|
|
75
|
-
} else {
|
|
76
|
-
this._titleEl = document.createElement('adw-window-title');
|
|
77
|
-
this._titleEl.className = 'adw-header-bar-title';
|
|
78
|
-
this._centerEl.appendChild(this._titleEl);
|
|
79
|
-
}
|
|
80
66
|
|
|
81
|
-
// End section
|
|
82
67
|
this._endEl = document.createElement('div');
|
|
83
68
|
this._endEl.className = 'adw-header-bar-end';
|
|
84
|
-
for (const child of endChildren) this._endEl.appendChild(child);
|
|
85
69
|
|
|
86
|
-
//
|
|
87
|
-
|
|
70
|
+
// All three slots stay LIVE: a button appended with `slot="end"` after connect used
|
|
71
|
+
// to sit outside the end section, and `adw-header-bar.spec.ts` carried the
|
|
72
|
+
// workaround ("appended LAST") as a house rule. `src/slotted-children.ts` has the
|
|
73
|
+
// incident. The `onAdopt` hook keeps the title-widget either/or true afterwards —
|
|
74
|
+
// a centre widget that arrives late replaces the derived title, as setting
|
|
75
|
+
// `Adw.HeaderBar:title-widget` at any point does.
|
|
76
|
+
bindSlottedChildren(
|
|
77
|
+
this,
|
|
78
|
+
[
|
|
79
|
+
{ name: 'start', into: this._startEl },
|
|
80
|
+
{ name: 'center', into: this._centerEl },
|
|
81
|
+
{ name: 'end', into: this._endEl },
|
|
82
|
+
],
|
|
83
|
+
(_node, slot) => {
|
|
84
|
+
if (slot.name === 'center') this._dropDerivedTitle();
|
|
85
|
+
},
|
|
86
|
+
).install(this._startEl, this._centerEl, this._endEl);
|
|
87
|
+
|
|
88
|
+
// Derived only when the centre is still free — the same either/or as
|
|
89
|
+
// `Adw.HeaderBar`, which builds an `AdwWindowTitle` exactly while no title-widget
|
|
90
|
+
// was given.
|
|
91
|
+
if (this._centerEl.childElementCount === 0) {
|
|
92
|
+
this._titleEl = document.createElement('adw-window-title');
|
|
93
|
+
this._titleEl.className = 'adw-header-bar-title';
|
|
94
|
+
this._centerEl.appendChild(this._titleEl);
|
|
95
|
+
}
|
|
88
96
|
this._renderTitle();
|
|
89
97
|
}
|
|
90
98
|
|
|
@@ -98,6 +106,17 @@ export class AdwHeaderBar extends HTMLElement {
|
|
|
98
106
|
if (this._initialized) this._renderTitle();
|
|
99
107
|
}
|
|
100
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Give the centre up to a title-widget. `adw_header_bar_set_title_widget` destroys the
|
|
111
|
+
* derived `AdwWindowTitle` rather than stacking the two, so the late case has to remove
|
|
112
|
+
* it too — a bar showing both would be a shape neither GTK nor the declared markup can
|
|
113
|
+
* produce.
|
|
114
|
+
*/
|
|
115
|
+
private _dropDerivedTitle() {
|
|
116
|
+
this._titleEl?.remove();
|
|
117
|
+
this._titleEl = null;
|
|
118
|
+
}
|
|
119
|
+
|
|
101
120
|
private _renderTitle() {
|
|
102
121
|
// A `slot="center"` widget replaced the derived title, so there is
|
|
103
122
|
// nothing for the attribute to write to — the same either/or as
|