@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
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// Light-DOM slot routing that stays LIVE — "where a `[slot=]` child lands", kept true
|
|
2
|
+
// after connect instead of only at parse time.
|
|
3
|
+
//
|
|
4
|
+
// THE INCIDENT
|
|
5
|
+
//
|
|
6
|
+
// 42 of the element files under `src/elements/` install their own subtree with
|
|
7
|
+
// `this.replaceChildren(…)`, and the ones with slots snapshot the author's children in
|
|
8
|
+
// the line before it — ONCE, inside the `_initialized` guard. Every one of them is
|
|
9
|
+
// therefore a parse-time-only slot. `row.append(icon)` with `slot="prefix"` after connect
|
|
10
|
+
// leaves `icon` as a sibling of the three internal sections rather than inside
|
|
11
|
+
// `prefixSection`, where the byte-identical DECLARED child lands; nothing throws and
|
|
12
|
+
// nothing logs. `adw-header-bar.spec.ts` had the workaround written down as a test
|
|
13
|
+
// convention — "Appended LAST: the sections are built in `connectedCallback`, so the
|
|
14
|
+
// slotted children have to be in place before the bar enters the document" — which is
|
|
15
|
+
// this bug recorded as a house rule.
|
|
16
|
+
//
|
|
17
|
+
// It is wrong for hand-written HTML that appends after load, and it is what ADR 0027 § 9
|
|
18
|
+
// names as the obstacle to one widget vocabulary across GTK, Blueprint, JSX, Vue and this
|
|
19
|
+
// pillar: a renderer mutates its tree after mount by definition, so the same authored
|
|
20
|
+
// markup cannot drive both surfaces while a slot is a one-shot read.
|
|
21
|
+
//
|
|
22
|
+
// WHY A MutationObserver, and not either alternative
|
|
23
|
+
//
|
|
24
|
+
// - **native `<slot>`** needs a shadow root, and ADR 0010 keeps this package light-DOM
|
|
25
|
+
// ON PURPOSE: a shadow boundary is symmetric, so it would block the host page's CSS
|
|
26
|
+
// from overriding IN, which is the overridability the `--adw-*` token contract sells.
|
|
27
|
+
// That ADR lists native `<slot>` as part of the deferred shadow path and gates it
|
|
28
|
+
// behind an ADR of its own — so it is not an option available to this fix. No element
|
|
29
|
+
// in the package calls `attachShadow` today.
|
|
30
|
+
// - **re-homing from the CHILD's `connectedCallback`** only reaches children that are
|
|
31
|
+
// themselves custom elements: a plain `<button slot="suffix">` and a bare text node
|
|
32
|
+
// have no callback, and both are slotted here today. It also inverts ownership —
|
|
33
|
+
// every child would have to know its parent's internal structure — so it cannot live
|
|
34
|
+
// in ONE place, which is the whole point of this module. And `connectedCallback`
|
|
35
|
+
// fires on every re-parent of the subtree, so it would re-home on a move that changed
|
|
36
|
+
// nothing.
|
|
37
|
+
//
|
|
38
|
+
// The observer is also the technique this package already reached for twice for the same
|
|
39
|
+
// "evaluated once at connect time" class: `<adw-clamp>` for its child width, and
|
|
40
|
+
// `src/empty-sections.ts` for the `hidden` derivation.
|
|
41
|
+
//
|
|
42
|
+
// WHAT THIS DELIBERATELY DOES NOT DO: reorder.
|
|
43
|
+
//
|
|
44
|
+
// A late child is APPENDED to its destination, which is where a parse-time child in the
|
|
45
|
+
// same position lands. Nothing finer is expressible: `host.insertBefore(child, ref)` with
|
|
46
|
+
// an already-adopted `ref` throws `NotFoundError` in the DOM before this module is
|
|
47
|
+
// reached, because `ref` is no longer a child of the host. Order fidelity under reordering
|
|
48
|
+
// is a property of native `<slot>`, i.e. of the shadow path ADR 0010 § 3 defers — until
|
|
49
|
+
// then a renderer that reorders must reorder inside the destination it was given, and
|
|
50
|
+
// that divergence is written down here rather than absorbed.
|
|
51
|
+
//
|
|
52
|
+
// NOTHING TO TEAR DOWN, deliberately. The observed node is the HOST, so the observer is
|
|
53
|
+
// unreachable the moment the element is and is collected with it — the same reasoning
|
|
54
|
+
// `src/empty-sections.ts` writes above its own. Only a binding that reaches OUTSIDE
|
|
55
|
+
// (document, window, a media query) has to be released on disconnect and re-established
|
|
56
|
+
// on every connect, which is what `scripts/check-adwaita-connect-rebind.mjs` polices.
|
|
57
|
+
// Staying bound while detached is also the better behaviour: a child appended to a parked
|
|
58
|
+
// widget is already in the right place when it comes back.
|
|
59
|
+
|
|
60
|
+
export interface AdwSlotClaim {
|
|
61
|
+
/** The `slot="…"` value claimed. Omit for the DEFAULT slot — everything else. */
|
|
62
|
+
readonly name?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Claim by NODE as well as by slot name, for the typed-child spelling
|
|
65
|
+
* (`<adw-alert-response>` IS the slot) and for a default that takes only some node
|
|
66
|
+
* kinds (`<adw-entry-row>` adopts elements and drops stray whitespace, as GtkBuildable
|
|
67
|
+
* does). Tried after an explicit `slot=` name and before the default, so a claim can
|
|
68
|
+
* never swallow a child that named a different slot.
|
|
69
|
+
*/
|
|
70
|
+
readonly claims?: (node: Node) => boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A destination that RE-HOMES what it claims — the ordinary slot. */
|
|
74
|
+
export interface AdwSlotInto extends AdwSlotClaim {
|
|
75
|
+
readonly into: HTMLElement;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A slot whose child is consumed by a CALL rather than by a move: either data —
|
|
80
|
+
* `<adw-alert-response id="ok">` is read for its id, label and appearance and then gone,
|
|
81
|
+
* exactly as GtkBuilder leaves nothing of a `<responses>` entry in the widget tree — or a
|
|
82
|
+
* widget whose insertion rule is the element's own (`addPrefix` PREPENDS, mirroring
|
|
83
|
+
* `gtk_box_prepend`). A node the call did not move out of the host is dropped, so a
|
|
84
|
+
* data-only child cannot linger as a stray sibling of the internal structure.
|
|
85
|
+
*/
|
|
86
|
+
export interface AdwSlotConsume extends AdwSlotClaim {
|
|
87
|
+
readonly consume: (node: Node) => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type AdwSlot = AdwSlotInto | AdwSlotConsume;
|
|
91
|
+
|
|
92
|
+
export interface AdwSlottedChildren {
|
|
93
|
+
/** The declared slots, so a driver can probe them without a hand-written ledger. */
|
|
94
|
+
readonly slots: readonly AdwSlot[];
|
|
95
|
+
/**
|
|
96
|
+
* Route the host's CURRENT children into their slots, then make `structure` the
|
|
97
|
+
* host's own children, then stay live.
|
|
98
|
+
*
|
|
99
|
+
* The three steps are one call because their ORDER is the invariant: the author's
|
|
100
|
+
* children have to leave the host before it is emptied, or `replaceChildren` destroys
|
|
101
|
+
* them — the bug `<adw-entry-row>` records above its own `_authored` snapshot. Passing
|
|
102
|
+
* `structure` here also means the set this module must never route is DERIVED from the
|
|
103
|
+
* call that installs it, rather than a second list to keep in sync.
|
|
104
|
+
*
|
|
105
|
+
* Call once per built subtree; a second call replaces the observer rather than
|
|
106
|
+
* stacking one.
|
|
107
|
+
*/
|
|
108
|
+
install(...structure: readonly Node[]): void;
|
|
109
|
+
/** The slot `node` belongs to, or `null` when none claims it. */
|
|
110
|
+
slotFor(node: Node): AdwSlot | null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Where a bound host's slots can be read back from — see {@link slottedChildrenOf}. */
|
|
114
|
+
const BOUND = new WeakMap<Element, AdwSlottedChildren>();
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The slot binding `host` installed, if it has one.
|
|
118
|
+
*
|
|
119
|
+
* This is how `slotted-children.spec.ts` drives EVERY registered element rather than a
|
|
120
|
+
* list of the ones someone remembered: an element that binds a slot enrols itself in the
|
|
121
|
+
* placement driver, and one that stops binding drops out of it visibly.
|
|
122
|
+
*/
|
|
123
|
+
export function slottedChildrenOf(host: Element): AdwSlottedChildren | undefined {
|
|
124
|
+
return BOUND.get(host);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Bind `host`'s light-DOM children to `slots`.
|
|
129
|
+
*
|
|
130
|
+
* `onAdopt` fires for a child adopted AFTER {@link AdwSlottedChildren.install} and not
|
|
131
|
+
* during it: the install pass IS the element's own build, which seeds its state on the
|
|
132
|
+
* next line with everything that landed, so firing there would make every element's build
|
|
133
|
+
* order depend on how many children the author wrote.
|
|
134
|
+
*/
|
|
135
|
+
export function bindSlottedChildren(
|
|
136
|
+
host: HTMLElement,
|
|
137
|
+
slots: readonly AdwSlot[],
|
|
138
|
+
onAdopt?: (node: Node, slot: AdwSlot) => void,
|
|
139
|
+
): AdwSlottedChildren {
|
|
140
|
+
const named = new Map<string, AdwSlot>();
|
|
141
|
+
const typed: Array<[(node: Node) => boolean, AdwSlot]> = [];
|
|
142
|
+
let fallback: AdwSlot | null = null;
|
|
143
|
+
for (const slot of slots) {
|
|
144
|
+
const claims = slot.claims;
|
|
145
|
+
if (slot.name !== undefined) named.set(slot.name, slot);
|
|
146
|
+
if (claims !== undefined) typed.push([claims, slot]);
|
|
147
|
+
if (slot.name === undefined && claims === undefined) fallback = slot;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The host's OWN children, installed by this module — never routed back into a slot. */
|
|
151
|
+
let structure: ReadonlySet<Node> = new Set();
|
|
152
|
+
let observer: MutationObserver | null = null;
|
|
153
|
+
|
|
154
|
+
const slotFor = (node: Node): AdwSlot | null => {
|
|
155
|
+
if (structure.has(node)) return null;
|
|
156
|
+
// An explicit `slot=` NAME is answered first and answered alone: `slot=""` is the
|
|
157
|
+
// default slot in the native assignment algorithm, and an unmatched name is
|
|
158
|
+
// assigned nowhere at all. Both are copied deliberately — a `slot="sufix"` typo
|
|
159
|
+
// that silently became default content would render in the wrong place and look
|
|
160
|
+
// intentional, where a child that visibly stays put does not.
|
|
161
|
+
const name = node instanceof Element ? (node.getAttribute('slot') ?? '') : '';
|
|
162
|
+
if (name !== '') return named.get(name) ?? null;
|
|
163
|
+
// Then the typed claims (`<adw-alert-response>` IS the slot), then the default.
|
|
164
|
+
for (const [claims, slot] of typed) if (claims(node)) return slot;
|
|
165
|
+
return fallback;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const route = (node: Node): AdwSlot | null => {
|
|
169
|
+
const slot = slotFor(node);
|
|
170
|
+
if (slot === null) return null;
|
|
171
|
+
if ('into' in slot) {
|
|
172
|
+
slot.into.appendChild(node);
|
|
173
|
+
return slot;
|
|
174
|
+
}
|
|
175
|
+
slot.consume(node);
|
|
176
|
+
// Data, not content: the call read what it needed and left the node where it was,
|
|
177
|
+
// so it has to go. A `consume` that MOVED the node (`addPrefix`) has already
|
|
178
|
+
// changed the parent and this is a no-op.
|
|
179
|
+
if (node.parentNode === host) host.removeChild(node);
|
|
180
|
+
return slot;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const binding: AdwSlottedChildren = {
|
|
184
|
+
slots,
|
|
185
|
+
slotFor,
|
|
186
|
+
install(...nodes: readonly Node[]) {
|
|
187
|
+
for (const node of Array.from(host.childNodes)) route(node);
|
|
188
|
+
structure = new Set(nodes);
|
|
189
|
+
host.replaceChildren(...nodes);
|
|
190
|
+
// Armed AFTER `replaceChildren`, so its own records are never delivered: an
|
|
191
|
+
// observer armed first would see the whole of `structure` as added nodes and
|
|
192
|
+
// spend a microtask rejecting them.
|
|
193
|
+
observer?.disconnect();
|
|
194
|
+
observer = new MutationObserver((records) => {
|
|
195
|
+
for (const record of records) {
|
|
196
|
+
// Only the HOST's own child list. A destination's records are the
|
|
197
|
+
// CONSEQUENCE of a move, and re-routing them appends each child a
|
|
198
|
+
// second time — the reason `<adw-preferences-group>` filtered on
|
|
199
|
+
// `record.target` back when it was the only element doing this.
|
|
200
|
+
if (record.target !== host) continue;
|
|
201
|
+
for (const node of record.addedNodes) {
|
|
202
|
+
const slot = route(node);
|
|
203
|
+
if (slot !== null) onAdopt?.(node, slot);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
observer.observe(host, { childList: true });
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
BOUND.set(host, binding);
|
|
211
|
+
return binding;
|
|
212
|
+
}
|
package/src/test.browser.mts
CHANGED
|
@@ -25,6 +25,7 @@ import { AdwActionRowsTest } from './adw-action-rows.spec.js';
|
|
|
25
25
|
import { AdwBreakpointsTest } from './breakpoints.spec.js';
|
|
26
26
|
import { AdwDataGridTest } from './adw-data-grid.spec.js';
|
|
27
27
|
import { AdwDialogTest } from './adw-dialog.spec.js';
|
|
28
|
+
import { AdwAlertDialogTest } from './adw-alert-dialog.spec.js';
|
|
28
29
|
import { AdwDropDownTest } from './adw-drop-down.spec.js';
|
|
29
30
|
import { AdwRowStateTest } from './adw-row-state.spec.js';
|
|
30
31
|
import { AdwTabViewTest } from './adw-tab-view.spec.js';
|
|
@@ -49,6 +50,7 @@ import { AdwAccentTest } from './adw-accent.spec.js';
|
|
|
49
50
|
import { AdwShortcutLabelTest } from './adw-shortcut-label.spec.js';
|
|
50
51
|
import { AdwConnectLifecycleTest } from './connect-lifecycle.spec.js';
|
|
51
52
|
import { AdwEmptySectionsTest } from './empty-sections.spec.js';
|
|
53
|
+
import { AdwSlottedChildrenTest } from './slotted-children.spec.js';
|
|
52
54
|
import { AdwKeyboardOperableTest } from './keyboard-operable.spec.js';
|
|
53
55
|
import { AdwFontsTest } from './adw-fonts.spec.js';
|
|
54
56
|
|
|
@@ -56,10 +58,12 @@ run({
|
|
|
56
58
|
AdwKeyboardOperableTest,
|
|
57
59
|
AdwConnectLifecycleTest,
|
|
58
60
|
AdwEmptySectionsTest,
|
|
61
|
+
AdwSlottedChildrenTest,
|
|
59
62
|
AdwStyleClassesTest,
|
|
60
63
|
AdwAccentTest,
|
|
61
64
|
AdwShortcutLabelTest,
|
|
62
65
|
AdwAboutDialogTest,
|
|
66
|
+
AdwAlertDialogTest,
|
|
63
67
|
AdwBannerTest,
|
|
64
68
|
AdwButtonContentTest,
|
|
65
69
|
AdwIconTest,
|