@gjsify/adwaita-web 0.43.0 → 0.45.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-entry-row.d.ts +5 -7
- package/lib/types/elements/adw-header-bar.d.ts +19 -4
- 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/slotted-children.d.ts +65 -0
- package/lib/types/slotted-children.spec.d.ts +1 -0
- package/package.json +9 -6
- package/src/adw-alert-dialog.spec.ts +146 -0
- package/src/adw-header-bar.spec.ts +13 -6
- package/src/adw-tab-view.spec.ts +71 -0
- package/src/elements/adw-action-row.ts +8 -6
- package/src/elements/adw-alert-dialog.ts +99 -23
- 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 +67 -27
- package/src/elements/adw-navigation-split-view.ts +31 -8
- package/src/elements/adw-navigation-view.ts +23 -3
- 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-tab-view.ts +25 -3
- package/src/elements/adw-toast-overlay.ts +6 -4
- package/src/elements/adw-toolbar-view.ts +16 -11
- 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
|
|
@@ -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.
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
// <adw-header-bar> — Adwaita header bar with centered title and start/end button slots.
|
|
2
2
|
//
|
|
3
|
-
// The derived centre is an `<adw-window-title
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// The derived centre is an `<adw-window-title>`. That is a DIVERGENCE, and the comment
|
|
4
|
+
// here used to claim the opposite — that `Adw.HeaderBar` puts one there. Measured
|
|
5
|
+
// against the source: `construct_title_label` (adw-header-bar.c:512) builds a plain
|
|
6
|
+
// `gtk_label_new (NULL)` with the `title` class, `single-line-mode` and
|
|
7
|
+
// `ellipsize=END`, and `Adw.HeaderBar` has NO `title` property at all — `update_title`
|
|
8
|
+
// (:475) resolves one from the navigation page, then the dialog, then the root window,
|
|
9
|
+
// then `g_get_application_name`, then `g_get_prgname`. An app that wants a subtitle
|
|
10
|
+
// there sets an `AdwWindowTitle` as its title-widget.
|
|
11
|
+
//
|
|
12
|
+
// The divergence is kept: a declarative surface wants `title=` / `subtitle=` attributes
|
|
13
|
+
// rather than a widget handoff, and both renderers already use them. It is recorded as
|
|
14
|
+
// `HeaderBarRenderState.derivedSubtitle` in `@gjsify/adwaita-core`, so it is a named
|
|
15
|
+
// difference instead of a mistaken claim of fidelity.
|
|
16
|
+
//
|
|
17
|
+
// It used to be a bare span with
|
|
6
18
|
// `textContent = title ?? ''`, and that span carried none of the three rules the
|
|
7
19
|
// window title already held in `@gjsify/adwaita-core`: an EMPTY title still
|
|
8
20
|
// reserved a blank line, re-setting the same value repainted, and there was no
|
|
@@ -16,6 +28,8 @@
|
|
|
16
28
|
// Copyright (c) 2025 csm. MIT License.
|
|
17
29
|
// Modifications: Reimplemented as Web Component for @gjsify/adwaita-web.
|
|
18
30
|
|
|
31
|
+
import { bindSlottedChildren } from '../slotted-children.js';
|
|
32
|
+
|
|
19
33
|
// Registers <adw-window-title>: the derived centre is one, so importing the bar
|
|
20
34
|
// alone must still define it.
|
|
21
35
|
import './adw-window-title.js';
|
|
@@ -27,8 +41,10 @@ export class AdwHeaderBar extends HTMLElement {
|
|
|
27
41
|
private _endEl: HTMLDivElement | null = null;
|
|
28
42
|
/**
|
|
29
43
|
* The derived `<adw-window-title>`, or `null` when a `slot="center"`
|
|
30
|
-
* title-widget took the centre. `Adw.HeaderBar` has the same either/or
|
|
31
|
-
*
|
|
44
|
+
* title-widget took the centre. `Adw.HeaderBar` has the same either/or —
|
|
45
|
+
* `adw_header_bar_set_title_widget` empties the centre bin before installing
|
|
46
|
+
* (adw-header-bar.c:1201) — over a derived `GtkLabel`, not an `AdwWindowTitle`;
|
|
47
|
+
* see the divergence at the top of this file.
|
|
32
48
|
*/
|
|
33
49
|
private _titleEl: HTMLElement | null = null;
|
|
34
50
|
|
|
@@ -56,48 +72,72 @@ export class AdwHeaderBar extends HTMLElement {
|
|
|
56
72
|
if (this._initialized) return;
|
|
57
73
|
this._initialized = true;
|
|
58
74
|
|
|
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
75
|
this._startEl = document.createElement('div');
|
|
66
76
|
this._startEl.className = 'adw-header-bar-start';
|
|
67
|
-
for (const child of startChildren) this._startEl.appendChild(child);
|
|
68
77
|
|
|
69
|
-
// Center section — a `slot="center"` widget (title-widget) wins over the
|
|
70
|
-
// plain `title` text.
|
|
71
78
|
this._centerEl = document.createElement('div');
|
|
72
79
|
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
80
|
|
|
81
|
-
// End section
|
|
82
81
|
this._endEl = document.createElement('div');
|
|
83
82
|
this._endEl.className = 'adw-header-bar-end';
|
|
84
|
-
for (const child of endChildren) this._endEl.appendChild(child);
|
|
85
83
|
|
|
86
|
-
//
|
|
87
|
-
|
|
84
|
+
// All three slots stay LIVE: a button appended with `slot="end"` after connect used
|
|
85
|
+
// to sit outside the end section, and `adw-header-bar.spec.ts` carried the
|
|
86
|
+
// workaround ("appended LAST") as a house rule. `src/slotted-children.ts` has the
|
|
87
|
+
// incident. The `onAdopt` hook keeps the title-widget either/or true afterwards —
|
|
88
|
+
// a centre widget that arrives late replaces the derived title, as setting
|
|
89
|
+
// `Adw.HeaderBar:title-widget` at any point does.
|
|
90
|
+
bindSlottedChildren(
|
|
91
|
+
this,
|
|
92
|
+
[
|
|
93
|
+
{ name: 'start', into: this._startEl },
|
|
94
|
+
{ name: 'center', into: this._centerEl },
|
|
95
|
+
{ name: 'end', into: this._endEl },
|
|
96
|
+
],
|
|
97
|
+
(_node, slot) => {
|
|
98
|
+
if (slot.name === 'center') this._dropDerivedTitle();
|
|
99
|
+
},
|
|
100
|
+
).install(this._startEl, this._centerEl, this._endEl);
|
|
101
|
+
|
|
102
|
+
// Derived only when the centre is still free — the same either/or as
|
|
103
|
+
// `Adw.HeaderBar`, which runs `construct_title_label` exactly while no
|
|
104
|
+
// title-widget holds the centre (adw-header-bar.c:1211). What it builds there is
|
|
105
|
+
// a `GtkLabel`; this is the documented `<adw-window-title>` divergence.
|
|
106
|
+
if (this._centerEl.childElementCount === 0) {
|
|
107
|
+
this._titleEl = document.createElement('adw-window-title');
|
|
108
|
+
this._titleEl.className = 'adw-header-bar-title';
|
|
109
|
+
this._centerEl.appendChild(this._titleEl);
|
|
110
|
+
}
|
|
88
111
|
this._renderTitle();
|
|
89
112
|
}
|
|
90
113
|
|
|
91
114
|
/**
|
|
92
115
|
* `title` used to be read ONCE, in `connectedCallback`, so every later write
|
|
93
116
|
* was a silent no-op — a header bar whose title tracked the open document
|
|
94
|
-
* kept whatever it was created with. `Adw.HeaderBar`
|
|
95
|
-
*
|
|
117
|
+
* kept whatever it was created with. `Adw.HeaderBar` never reads its title once
|
|
118
|
+
* either: it has no `title` property at all, and re-runs `update_title`
|
|
119
|
+
* (adw-header-bar.c:475) on every ancestor change that could move the answer.
|
|
96
120
|
*/
|
|
97
121
|
attributeChangedCallback() {
|
|
98
122
|
if (this._initialized) this._renderTitle();
|
|
99
123
|
}
|
|
100
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Give the centre up to a title-widget. `adw_header_bar_set_title_widget` empties the
|
|
127
|
+
* centre bin and drops its derived label (adw-header-bar.c:1201, :1209) rather than
|
|
128
|
+
* stacking the two, so the late case has to remove ours too — a bar showing both would
|
|
129
|
+
* be a shape neither GTK nor the declared markup can produce.
|
|
130
|
+
*
|
|
131
|
+
* NOT SYMMETRIC HERE, AND KNOWN: C rebuilds the label the moment the title-widget goes
|
|
132
|
+
* away (:1211); nothing in this element brings the derived centre back, so a bar that
|
|
133
|
+
* gave it up has given it up for good. `HeaderBarState` in `@gjsify/adwaita-core` holds
|
|
134
|
+
* the rebuild, and this element adopting it is the rewiring tracked in #1343.
|
|
135
|
+
*/
|
|
136
|
+
private _dropDerivedTitle() {
|
|
137
|
+
this._titleEl?.remove();
|
|
138
|
+
this._titleEl = null;
|
|
139
|
+
}
|
|
140
|
+
|
|
101
141
|
private _renderTitle() {
|
|
102
142
|
// A `slot="center"` widget replaced the derived title, so there is
|
|
103
143
|
// nothing for the attribute to write to — the same either/or as
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
} from '@gjsify/adwaita-core';
|
|
37
37
|
|
|
38
38
|
import { bindBreakpointSetter } from '../breakpoints.js';
|
|
39
|
+
import { bindSlottedChildren } from '../slotted-children.js';
|
|
39
40
|
|
|
40
41
|
/** Detail of the `navigation-push` / `navigation-pop` delegation event. */
|
|
41
42
|
export interface NavigationDelegateDetail {
|
|
@@ -142,18 +143,24 @@ export class AdwNavigationSplitView extends HTMLElement {
|
|
|
142
143
|
if (this._initialized) return;
|
|
143
144
|
this._initialized = true;
|
|
144
145
|
|
|
145
|
-
const sidebarChildren = Array.from(this.querySelectorAll(':scope > [slot="sidebar"]'));
|
|
146
|
-
const contentChildren = Array.from(this.querySelectorAll(':scope > [slot="content"]'));
|
|
147
|
-
|
|
148
146
|
this._sidebarEl = document.createElement('div');
|
|
149
147
|
this._sidebarEl.className = 'adw-nsv-sidebar';
|
|
150
|
-
for (const child of sidebarChildren) this._sidebarEl.appendChild(child);
|
|
151
148
|
|
|
152
149
|
this._contentEl = document.createElement('div');
|
|
153
150
|
this._contentEl.className = 'adw-nsv-content';
|
|
154
|
-
for (const child of contentChildren) this._contentEl.appendChild(child);
|
|
155
151
|
|
|
156
|
-
|
|
152
|
+
// Both panes stay LIVE. `adw_navigation_split_view_set_sidebar` is a property
|
|
153
|
+
// setter, callable whenever, so a pane appended after connect has to reach the
|
|
154
|
+
// pane box AND the state that decides the stack — which is what `_mountPane` does
|
|
155
|
+
// on the way. `src/slotted-children.ts` has the incident.
|
|
156
|
+
bindSlottedChildren(
|
|
157
|
+
this,
|
|
158
|
+
[
|
|
159
|
+
{ name: 'sidebar', into: this._sidebarEl },
|
|
160
|
+
{ name: 'content', into: this._contentEl },
|
|
161
|
+
],
|
|
162
|
+
(_node, slot) => this._mountPane(slot.name === 'sidebar' ? 'sidebar' : 'content'),
|
|
163
|
+
).install(this._sidebarEl, this._contentEl);
|
|
157
164
|
|
|
158
165
|
// Parse-time attributes are the CONSTRUCTION properties, as in the overlay view:
|
|
159
166
|
// applying them as sequential setters emits a transition for a state the markup
|
|
@@ -175,14 +182,30 @@ export class AdwNavigationSplitView extends HTMLElement {
|
|
|
175
182
|
// Which children EXIST decides the stack, so the panes are mounted into the state
|
|
176
183
|
// and not merely into the DOM — a lone child stays visible whatever
|
|
177
184
|
// `show-content` says, which two CSS classes cannot express.
|
|
178
|
-
this.
|
|
179
|
-
this.
|
|
185
|
+
this._mountPane('sidebar');
|
|
186
|
+
this._mountPane('content');
|
|
180
187
|
this._state.subscribe(() => {
|
|
181
188
|
this._reflectShowContent();
|
|
182
189
|
this._syncClasses();
|
|
183
190
|
});
|
|
184
191
|
}
|
|
185
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Mount whatever a pane box currently HOLDS into the state — `null` while it is empty.
|
|
195
|
+
*
|
|
196
|
+
* Re-mounting an unchanged pane re-emits the plan, because the state compares the page
|
|
197
|
+
* ref by identity. Both subscribers are idempotent reflections (an attribute and a
|
|
198
|
+
* class list), so a second child in the same pane costs a repaint decision and never a
|
|
199
|
+
* navigation event.
|
|
200
|
+
*/
|
|
201
|
+
private _mountPane(pane: 'sidebar' | 'content'): void {
|
|
202
|
+
const box = pane === 'sidebar' ? this._sidebarEl : this._contentEl;
|
|
203
|
+
const children = Array.from(box.children);
|
|
204
|
+
const page = children.length > 0 ? { tag: this._tagOf(children) } : null;
|
|
205
|
+
if (pane === 'sidebar') this._state.setSidebar(page);
|
|
206
|
+
else this._state.setContent(page);
|
|
207
|
+
}
|
|
208
|
+
|
|
186
209
|
/** `Adw.NavigationPage:tag` off the slotted pane, or `null` when untagged. */
|
|
187
210
|
private _tagOf(children: readonly Element[]): string | null {
|
|
188
211
|
for (const child of children) {
|