@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
package/README.md
CHANGED
|
@@ -53,6 +53,33 @@ Vite preset (or any Vite/bundler setup). Type declarations are shipped
|
|
|
53
53
|
pre-built at `lib/types/index.d.ts`, so `gjsify tsc` consumers resolve the
|
|
54
54
|
package's types without compiling its source.
|
|
55
55
|
|
|
56
|
+
## Slots are live
|
|
57
|
+
|
|
58
|
+
These are light-DOM custom elements, so `slot="…"` is emulated rather than native
|
|
59
|
+
(see [ADR 0010](../../../docs/adr/0010-adwaita-web-style-isolation.md) for why the
|
|
60
|
+
package stays out of the shadow DOM). The emulation is **live**: a child appended
|
|
61
|
+
after the element is in the document lands in the same internal box as the
|
|
62
|
+
identical child written in the markup.
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
const row = document.querySelector('adw-action-row')!;
|
|
66
|
+
const toggle = document.createElement('adw-switch');
|
|
67
|
+
toggle.setAttribute('slot', 'suffix');
|
|
68
|
+
row.append(toggle); // lands in the suffix section, exactly as a declared one does
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Two limits are worth knowing:
|
|
72
|
+
|
|
73
|
+
- **Append, not reorder.** A late child goes to the end of its slot's box. Once a
|
|
74
|
+
child has been adopted it is no longer a child of the host, so
|
|
75
|
+
`host.insertBefore(next, adopted)` throws in the DOM before this package sees
|
|
76
|
+
it — reorder inside the box you were handed (`row.prefixSection`,
|
|
77
|
+
`view.topBar`, …). Native `<slot>` is what would lift this, and it is part of
|
|
78
|
+
the shadow-DOM path ADR 0010 defers.
|
|
79
|
+
- **An unknown slot name is assigned nowhere**, as in the native algorithm: a
|
|
80
|
+
`slot="sufix"` typo stays where you put it instead of quietly becoming default
|
|
81
|
+
content in the wrong box.
|
|
82
|
+
|
|
56
83
|
## Shared behaviour (`@gjsify/adwaita-core`)
|
|
57
84
|
|
|
58
85
|
The widget *behaviour* — as opposed to the DOM rendering — comes from
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const AdwAlertDialogTest: () => Promise<void>;
|
|
@@ -31,6 +31,12 @@ export declare class AdwAlertDialog extends HTMLElement {
|
|
|
31
31
|
set body(value: string);
|
|
32
32
|
get open(): boolean;
|
|
33
33
|
set open(value: boolean);
|
|
34
|
+
/** Whether {@link heading} is read as markup rather than plain text. */
|
|
35
|
+
get headingUseMarkup(): boolean;
|
|
36
|
+
set headingUseMarkup(value: boolean);
|
|
37
|
+
/** Whether {@link body} is read as markup rather than plain text. */
|
|
38
|
+
get bodyUseMarkup(): boolean;
|
|
39
|
+
set bodyUseMarkup(value: boolean);
|
|
34
40
|
/** Whether to prefer horizontal button layout when the buttons fit. */
|
|
35
41
|
get preferWideLayout(): boolean;
|
|
36
42
|
set preferWideLayout(value: boolean);
|
|
@@ -41,6 +47,12 @@ export declare class AdwAlertDialog extends HTMLElement {
|
|
|
41
47
|
get closeResponse(): string;
|
|
42
48
|
set closeResponse(value: string);
|
|
43
49
|
connectedCallback(): void;
|
|
50
|
+
/**
|
|
51
|
+
* Take one declared `<adw-alert-response>` as a response. An entry with no `id` is
|
|
52
|
+
* skipped rather than given a generated one: the id is what `response` events carry, so
|
|
53
|
+
* inventing one would hand the consumer a name it never wrote.
|
|
54
|
+
*/
|
|
55
|
+
private _adoptResponse;
|
|
44
56
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
45
57
|
/** Present the dialog. `parent` is accepted for API parity; unused. */
|
|
46
58
|
present(_parent?: unknown): void;
|
|
@@ -34,6 +34,8 @@ export declare class AdwCarousel extends HTMLElement implements CarouselHost {
|
|
|
34
34
|
* index while the target page stays the target.
|
|
35
35
|
*/
|
|
36
36
|
private _scrollTargetId;
|
|
37
|
+
/** The pointer-drag gesture, asked whether it owns the strip. See `_onWheel`. */
|
|
38
|
+
private _drag;
|
|
37
39
|
static get observedAttributes(): string[];
|
|
38
40
|
constructor();
|
|
39
41
|
get nPages(): number;
|
|
@@ -47,6 +49,12 @@ export declare class AdwCarousel extends HTMLElement implements CarouselHost {
|
|
|
47
49
|
*/
|
|
48
50
|
set position(value: number);
|
|
49
51
|
get currentPage(): number;
|
|
52
|
+
/**
|
|
53
|
+
* `AdwCarousel:allow-mouse-drag` — whether the strip can be dragged with the pointer.
|
|
54
|
+
* Defaults to TRUE, as in libadwaita, where FALSE leaves dragging to touch only.
|
|
55
|
+
*/
|
|
56
|
+
get allowMouseDrag(): boolean;
|
|
57
|
+
set allowMouseDrag(value: boolean);
|
|
50
58
|
/** Whether the scroll wheel navigates between pages. Defaults to TRUE, as in libadwaita. */
|
|
51
59
|
get allowScrollWheel(): boolean;
|
|
52
60
|
set allowScrollWheel(value: boolean);
|
|
@@ -16,8 +16,11 @@ export declare class AdwEntryRow extends HTMLElement {
|
|
|
16
16
|
protected _prefixes: HTMLDivElement;
|
|
17
17
|
protected _suffixes: HTMLDivElement;
|
|
18
18
|
protected _initialized: boolean;
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* The two slots, installed after `_onConnected` rather than inside `_build`: the order
|
|
21
|
+
* IS the contract — see the `install` call in `connectedCallback`.
|
|
22
|
+
*/
|
|
23
|
+
private _slots;
|
|
21
24
|
private _lastText;
|
|
22
25
|
private _lastLength;
|
|
23
26
|
static get observedAttributes(): string[];
|
|
@@ -76,11 +79,6 @@ export declare class AdwEntryRow extends HTMLElement {
|
|
|
76
79
|
/** Subclass hook: runs after the DOM exists and the state is seeded, before the first render. */
|
|
77
80
|
protected _onConnected(): void;
|
|
78
81
|
private _build;
|
|
79
|
-
/**
|
|
80
|
-
* Re-home the children the author wrote. libadwaita's buildable maps an untyped
|
|
81
|
-
* `<child>` to a suffix; `slot="prefix"` is the web spelling of `type="prefix"`.
|
|
82
|
-
*/
|
|
83
|
-
private _adoptAuthored;
|
|
84
82
|
/**
|
|
85
83
|
* `pressed_cb`: a click landing on the row, the editable area, the indicator or
|
|
86
84
|
* either affordance box grabs focus; anything else (a suffix control, the apply
|
|
@@ -26,5 +26,12 @@ export declare class AdwHeaderBar extends HTMLElement {
|
|
|
26
26
|
* is bound to the property and re-renders on every change.
|
|
27
27
|
*/
|
|
28
28
|
attributeChangedCallback(): void;
|
|
29
|
+
/**
|
|
30
|
+
* Give the centre up to a title-widget. `adw_header_bar_set_title_widget` destroys the
|
|
31
|
+
* derived `AdwWindowTitle` rather than stacking the two, so the late case has to remove
|
|
32
|
+
* it too — a bar showing both would be a shape neither GTK nor the declared markup can
|
|
33
|
+
* produce.
|
|
34
|
+
*/
|
|
35
|
+
private _dropDerivedTitle;
|
|
29
36
|
private _renderTitle;
|
|
30
37
|
}
|
|
@@ -45,6 +45,15 @@ export declare class AdwNavigationSplitView extends HTMLElement {
|
|
|
45
45
|
pop(): NavigationActionResult;
|
|
46
46
|
connectedCallback(): void;
|
|
47
47
|
private _buildOnce;
|
|
48
|
+
/**
|
|
49
|
+
* Mount whatever a pane box currently HOLDS into the state — `null` while it is empty.
|
|
50
|
+
*
|
|
51
|
+
* Re-mounting an unchanged pane re-emits the plan, because the state compares the page
|
|
52
|
+
* ref by identity. Both subscribers are idempotent reflections (an attribute and a
|
|
53
|
+
* class list), so a second child in the same pane costs a repaint decision and never a
|
|
54
|
+
* navigation event.
|
|
55
|
+
*/
|
|
56
|
+
private _mountPane;
|
|
48
57
|
/** `Adw.NavigationPage:tag` off the slotted pane, or `null` when untagged. */
|
|
49
58
|
private _tagOf;
|
|
50
59
|
/**
|
|
@@ -5,6 +5,12 @@ export declare class AdwPreferencesGroup extends HTMLElement {
|
|
|
5
5
|
private _suffixEl;
|
|
6
6
|
private _listboxEl;
|
|
7
7
|
private _initialized;
|
|
8
|
+
/**
|
|
9
|
+
* Watches the two BOXES, not the host: the routing of a host child is
|
|
10
|
+
* `src/slotted-children.ts`'s job now, and what is left here is the header derivation,
|
|
11
|
+
* whose inputs are the row count and whether a suffix exists. A removal straight out of
|
|
12
|
+
* the listbox changes both and reaches no other callback.
|
|
13
|
+
*/
|
|
8
14
|
private _observer;
|
|
9
15
|
static get observedAttributes(): string[];
|
|
10
16
|
get description(): string;
|
|
@@ -13,10 +19,11 @@ export declare class AdwPreferencesGroup extends HTMLElement {
|
|
|
13
19
|
* `adw_preferences_group_add`: a row goes into the boxed list, a `header-suffix`
|
|
14
20
|
* child into the header.
|
|
15
21
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
22
|
+
* Kept as a method as well as a slot, because both spellings occur —
|
|
23
|
+
* `group.addRow(row)` in code, `group.append(row)` from a framework's rendering — and a
|
|
24
|
+
* bare child of the host would sit outside the card, invisible to the row count that
|
|
25
|
+
* decides whether the card is painted at all. The `append` spelling is routed by
|
|
26
|
+
* `src/slotted-children.ts`, which every other slotted element now shares.
|
|
20
27
|
*/
|
|
21
28
|
addRow(child: Node): void;
|
|
22
29
|
/**
|
|
@@ -29,13 +36,15 @@ export declare class AdwPreferencesGroup extends HTMLElement {
|
|
|
29
36
|
disconnectedCallback(): void;
|
|
30
37
|
attributeChangedCallback(): void;
|
|
31
38
|
/**
|
|
32
|
-
*
|
|
33
|
-
* `group.append(row)` behaves like `adw_preferences_group_add`.
|
|
39
|
+
* Re-derive the header whenever the two boxes change, in EITHER direction.
|
|
34
40
|
*
|
|
35
41
|
* C gets this for free: `gtk_widget_observe_children` on the listbox drives
|
|
36
|
-
* `update_listbox_visibility` through `items-changed`. A DOM element needs the
|
|
37
|
-
*
|
|
42
|
+
* `update_listbox_visibility` through `items-changed`. A DOM element needs the observer
|
|
43
|
+
* for the same rule to hold over time rather than only at connect. Adoption is not what
|
|
44
|
+
* this watches — a row appended to the host reaches the listbox through the shared slot
|
|
45
|
+
* routing, and lands here as the listbox record it causes. A row REMOVED straight out
|
|
46
|
+
* of the listbox reaches no other callback at all, and it still changes the row count.
|
|
38
47
|
*/
|
|
39
|
-
private
|
|
48
|
+
private _observeBoxes;
|
|
40
49
|
private _renderHeader;
|
|
41
50
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Which way the widget pages, and therefore which axis claims a drag. */
|
|
2
|
+
export type AdwSwipeOrientation = 'horizontal' | 'vertical';
|
|
3
|
+
export interface AdwSwipeDragInit {
|
|
4
|
+
/** The element the listeners sit on. Items rebuilt under it stay covered. */
|
|
5
|
+
host: HTMLElement;
|
|
6
|
+
orientation: AdwSwipeOrientation;
|
|
7
|
+
/** Re-read on every press: may a mouse drag start right now? */
|
|
8
|
+
enabled: () => boolean;
|
|
9
|
+
/** Page pitch in px. The raw pixel delta is divided by it to reach progress. */
|
|
10
|
+
pitch: () => number;
|
|
11
|
+
/** Snap points, ascending, as `adw_swipeable_get_snap_points` returns them. */
|
|
12
|
+
points: () => readonly number[];
|
|
13
|
+
/** Progress now — the gesture is seeded from it and bounded by it. */
|
|
14
|
+
progress: () => number;
|
|
15
|
+
/** `AdwCarousel:allow-long-swipes`. */
|
|
16
|
+
allowLongSwipes: () => boolean;
|
|
17
|
+
/**
|
|
18
|
+
* `adw_swipeable_get_cancel_progress` — where a CANCELLED gesture goes.
|
|
19
|
+
*
|
|
20
|
+
* Not optional by accident: the tracker's own fallback is where the gesture began,
|
|
21
|
+
* and `AdwCarousel`'s answer is `get_closest_snap_point` — the point nearest where
|
|
22
|
+
* the finger is NOW (adw-carousel.c:1294-1299). The two differ for exactly the
|
|
23
|
+
* gesture that matters, one cancelled past the halfway mark: upstream commits to the
|
|
24
|
+
* page under the cursor, the fallback yanks it back.
|
|
25
|
+
*/
|
|
26
|
+
cancelProgress: () => number;
|
|
27
|
+
/** Show this progress. Called on every move that the gesture owns. */
|
|
28
|
+
onUpdate: (progress: number) => void;
|
|
29
|
+
/**
|
|
30
|
+
* Whether a gesture may be claimed at all right now, on top of the refusals this
|
|
31
|
+
* module makes itself. `AdwCarousel` needs none — its `interactive` and
|
|
32
|
+
* `allow-mouse-drag` are {@link enabled} — but `is_in_swipe_area` lives here for the
|
|
33
|
+
* widgets that implement `get_swipe_area`, which the carousel does not.
|
|
34
|
+
*/
|
|
35
|
+
claimable?: (start: {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
}) => boolean;
|
|
39
|
+
/** Settle on this progress. `velocity` is raw px/ms, for an animation duration. */
|
|
40
|
+
onEnd: (progress: number, velocity: number) => void;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Make `host` draggable with a mouse, deciding where it lands the way `AdwSwipeTracker`
|
|
44
|
+
* does.
|
|
45
|
+
*
|
|
46
|
+
* The listener is the element's own and is bound once, without teardown: it survives a
|
|
47
|
+
* re-parent and is collected with the element.
|
|
48
|
+
*/
|
|
49
|
+
/** What a caller gets back, so it can tell whether a gesture owns the widget. */
|
|
50
|
+
export interface AdwSwipeDrag {
|
|
51
|
+
/**
|
|
52
|
+
* Whether a gesture is currently claimed.
|
|
53
|
+
*
|
|
54
|
+
* A widget needs this to keep its OTHER inputs out of the offset while a drag owns
|
|
55
|
+
* it. Measured on `<adw-carousel>`: one wheel notch during a claimed drag ran the
|
|
56
|
+
* whole wheel path and jumped `scrollLeft` 80 → 440 in a frame, which the next move
|
|
57
|
+
* then yanked back. Upstream's `scroll_cb` is ungated too, but there the external
|
|
58
|
+
* request is a spring the next frame quietly overrides — here `claim()` sets
|
|
59
|
+
* `scroll-behavior: auto`, which turns it into an instant jump.
|
|
60
|
+
*/
|
|
61
|
+
readonly dragging: boolean;
|
|
62
|
+
}
|
|
63
|
+
export declare function attachSwipeDrag(init: AdwSwipeDragInit): AdwSwipeDrag;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export interface AdwSlotClaim {
|
|
2
|
+
/** The `slot="…"` value claimed. Omit for the DEFAULT slot — everything else. */
|
|
3
|
+
readonly name?: string;
|
|
4
|
+
/**
|
|
5
|
+
* Claim by NODE as well as by slot name, for the typed-child spelling
|
|
6
|
+
* (`<adw-alert-response>` IS the slot) and for a default that takes only some node
|
|
7
|
+
* kinds (`<adw-entry-row>` adopts elements and drops stray whitespace, as GtkBuildable
|
|
8
|
+
* does). Tried after an explicit `slot=` name and before the default, so a claim can
|
|
9
|
+
* never swallow a child that named a different slot.
|
|
10
|
+
*/
|
|
11
|
+
readonly claims?: (node: Node) => boolean;
|
|
12
|
+
}
|
|
13
|
+
/** A destination that RE-HOMES what it claims — the ordinary slot. */
|
|
14
|
+
export interface AdwSlotInto extends AdwSlotClaim {
|
|
15
|
+
readonly into: HTMLElement;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A slot whose child is consumed by a CALL rather than by a move: either data —
|
|
19
|
+
* `<adw-alert-response id="ok">` is read for its id, label and appearance and then gone,
|
|
20
|
+
* exactly as GtkBuilder leaves nothing of a `<responses>` entry in the widget tree — or a
|
|
21
|
+
* widget whose insertion rule is the element's own (`addPrefix` PREPENDS, mirroring
|
|
22
|
+
* `gtk_box_prepend`). A node the call did not move out of the host is dropped, so a
|
|
23
|
+
* data-only child cannot linger as a stray sibling of the internal structure.
|
|
24
|
+
*/
|
|
25
|
+
export interface AdwSlotConsume extends AdwSlotClaim {
|
|
26
|
+
readonly consume: (node: Node) => void;
|
|
27
|
+
}
|
|
28
|
+
export type AdwSlot = AdwSlotInto | AdwSlotConsume;
|
|
29
|
+
export interface AdwSlottedChildren {
|
|
30
|
+
/** The declared slots, so a driver can probe them without a hand-written ledger. */
|
|
31
|
+
readonly slots: readonly AdwSlot[];
|
|
32
|
+
/**
|
|
33
|
+
* Route the host's CURRENT children into their slots, then make `structure` the
|
|
34
|
+
* host's own children, then stay live.
|
|
35
|
+
*
|
|
36
|
+
* The three steps are one call because their ORDER is the invariant: the author's
|
|
37
|
+
* children have to leave the host before it is emptied, or `replaceChildren` destroys
|
|
38
|
+
* them — the bug `<adw-entry-row>` records above its own `_authored` snapshot. Passing
|
|
39
|
+
* `structure` here also means the set this module must never route is DERIVED from the
|
|
40
|
+
* call that installs it, rather than a second list to keep in sync.
|
|
41
|
+
*
|
|
42
|
+
* Call once per built subtree; a second call replaces the observer rather than
|
|
43
|
+
* stacking one.
|
|
44
|
+
*/
|
|
45
|
+
install(...structure: readonly Node[]): void;
|
|
46
|
+
/** The slot `node` belongs to, or `null` when none claims it. */
|
|
47
|
+
slotFor(node: Node): AdwSlot | null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The slot binding `host` installed, if it has one.
|
|
51
|
+
*
|
|
52
|
+
* This is how `slotted-children.spec.ts` drives EVERY registered element rather than a
|
|
53
|
+
* list of the ones someone remembered: an element that binds a slot enrols itself in the
|
|
54
|
+
* placement driver, and one that stops binding drops out of it visibly.
|
|
55
|
+
*/
|
|
56
|
+
export declare function slottedChildrenOf(host: Element): AdwSlottedChildren | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Bind `host`'s light-DOM children to `slots`.
|
|
59
|
+
*
|
|
60
|
+
* `onAdopt` fires for a child adopted AFTER {@link AdwSlottedChildren.install} and not
|
|
61
|
+
* during it: the install pass IS the element's own build, which seeds its state on the
|
|
62
|
+
* next line with everything that landed, so firing there would make every element's build
|
|
63
|
+
* order depend on how many children the author wrote.
|
|
64
|
+
*/
|
|
65
|
+
export declare function bindSlottedChildren(host: HTMLElement, slots: readonly AdwSlot[], onAdopt?: (node: Node, slot: AdwSlot) => void): AdwSlottedChildren;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const AdwSlottedChildrenTest: () => Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/adwaita-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"description": "Adwaita/Libadwaita web components for browser targets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -45,14 +45,14 @@
|
|
|
45
45
|
"@codemirror/language": "^6.12.4",
|
|
46
46
|
"@codemirror/state": "^6.7.0",
|
|
47
47
|
"@codemirror/view": "^6.43.4",
|
|
48
|
-
"@gjsify/adwaita-core": "^0.
|
|
49
|
-
"@gjsify/adwaita-fonts": "^0.
|
|
50
|
-
"@gjsify/adwaita-icons": "^0.
|
|
48
|
+
"@gjsify/adwaita-core": "^0.44.0",
|
|
49
|
+
"@gjsify/adwaita-fonts": "^0.44.0",
|
|
50
|
+
"@gjsify/adwaita-icons": "^0.44.0",
|
|
51
51
|
"@lezer/highlight": "^1.2.3"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@gjsify/cli": "^0.
|
|
55
|
-
"@gjsify/unit": "^0.
|
|
54
|
+
"@gjsify/cli": "^0.44.0",
|
|
55
|
+
"@gjsify/unit": "^0.44.0",
|
|
56
56
|
"sass": "^1.101.0",
|
|
57
57
|
"typescript": "^6.0.3"
|
|
58
58
|
},
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// <adw-alert-dialog> against `Adw.AlertDialog`'s own property list.
|
|
2
|
+
//
|
|
3
|
+
// This element shipped with FOUR of its eight GIR properties reachable from markup —
|
|
4
|
+
// `heading`, `body`, `open` (a web convention with no GIR counterpart) and
|
|
5
|
+
// `prefer-wide-layout`. `close-response` and `default-response` existed as JS
|
|
6
|
+
// properties but no attribute fed them, and `heading-use-markup`/`body-use-markup`
|
|
7
|
+
// were absent entirely. The website's generated widget table read
|
|
8
|
+
// `observedAttributes` and truthfully reported "takes 4 attributes", which is how the
|
|
9
|
+
// gap became visible: the DOC was right and the ELEMENT was short.
|
|
10
|
+
//
|
|
11
|
+
// The markup pair defaults to FALSE here and in libadwaita alike — unlike
|
|
12
|
+
// `Adw.Banner:use-markup`, whose C default is TRUE and which `adw-banner.ts` therefore
|
|
13
|
+
// departs from on purpose. So these vectors pin a MATCH, not a departure, and the
|
|
14
|
+
// negative case is the one that matters: tags must show literally until asked for.
|
|
15
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
16
|
+
|
|
17
|
+
import type { AdwAlertDialog } from './elements/adw-alert-dialog.js';
|
|
18
|
+
|
|
19
|
+
/** Mount via `setAttribute` so an attribute value's exact text survives. */
|
|
20
|
+
function mount(attributes: Record<string, string> = {}): { el: AdwAlertDialog; host: HTMLElement } {
|
|
21
|
+
const host = document.createElement('div');
|
|
22
|
+
document.body.appendChild(host);
|
|
23
|
+
const el = document.createElement('adw-alert-dialog') as AdwAlertDialog;
|
|
24
|
+
for (const [name, value] of Object.entries(attributes)) el.setAttribute(name, value);
|
|
25
|
+
host.appendChild(el);
|
|
26
|
+
return { el, host };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const headingEl = (el: AdwAlertDialog): HTMLElement => el.querySelector('.adw-alert-dialog-heading') as HTMLElement;
|
|
30
|
+
const bodyEl = (el: AdwAlertDialog): HTMLElement => el.querySelector('.adw-alert-dialog-body') as HTMLElement;
|
|
31
|
+
|
|
32
|
+
export const AdwAlertDialogTest = async () => {
|
|
33
|
+
await describe('adw-alert-dialog observes every Adw.AlertDialog property it can render', async () => {
|
|
34
|
+
// Named rather than counted: a count passes again the moment someone swaps one
|
|
35
|
+
// attribute for another, which is the drift this file exists to catch.
|
|
36
|
+
for (const name of [
|
|
37
|
+
'heading',
|
|
38
|
+
'body',
|
|
39
|
+
'heading-use-markup',
|
|
40
|
+
'body-use-markup',
|
|
41
|
+
'close-response',
|
|
42
|
+
'default-response',
|
|
43
|
+
'prefer-wide-layout',
|
|
44
|
+
]) {
|
|
45
|
+
await it(`observes \`${name}\``, () => {
|
|
46
|
+
expect(
|
|
47
|
+
(
|
|
48
|
+
customElements.get('adw-alert-dialog') as typeof HTMLElement & {
|
|
49
|
+
observedAttributes: string[];
|
|
50
|
+
}
|
|
51
|
+
).observedAttributes,
|
|
52
|
+
).toContain(name);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await describe('adw-alert-dialog markup is opt-in, matching the libadwaita default', async () => {
|
|
58
|
+
await it('a heading with tags shows them literally by default', () => {
|
|
59
|
+
const { el, host } = mount({ heading: '<b>Delete</b> file?' });
|
|
60
|
+
expect(headingEl(el).textContent).toBe('<b>Delete</b> file?');
|
|
61
|
+
expect(headingEl(el).querySelector('b')).toBe(null);
|
|
62
|
+
host.remove();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await it('heading-use-markup renders it, which is the author asserting it is trusted', () => {
|
|
66
|
+
const { el, host } = mount({ heading: '<b>Delete</b> file?', 'heading-use-markup': '' });
|
|
67
|
+
expect(headingEl(el).querySelector('b')?.textContent).toBe('Delete');
|
|
68
|
+
host.remove();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await it('a body with tags shows them literally by default', () => {
|
|
72
|
+
const { el, host } = mount({ body: 'Really delete <i>notes.txt</i>?' });
|
|
73
|
+
expect(bodyEl(el).textContent).toBe('Really delete <i>notes.txt</i>?');
|
|
74
|
+
expect(bodyEl(el).querySelector('i')).toBe(null);
|
|
75
|
+
host.remove();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await it('body-use-markup renders it', () => {
|
|
79
|
+
const { el, host } = mount({ body: 'Really delete <i>notes.txt</i>?', 'body-use-markup': '' });
|
|
80
|
+
expect(bodyEl(el).querySelector('i')?.textContent).toBe('notes.txt');
|
|
81
|
+
host.remove();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await it('toggling the attribute off returns to text', () => {
|
|
85
|
+
const { el, host } = mount({ heading: '<b>Delete</b>', 'heading-use-markup': '' });
|
|
86
|
+
el.headingUseMarkup = false;
|
|
87
|
+
expect(headingEl(el).textContent).toBe('<b>Delete</b>');
|
|
88
|
+
expect(headingEl(el).querySelector('b')).toBe(null);
|
|
89
|
+
host.remove();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await describe('adw-alert-dialog response IDs are reachable from markup', async () => {
|
|
94
|
+
await it('close-response defaults to `close`, as in Adw.AlertDialog', () => {
|
|
95
|
+
const { el, host } = mount();
|
|
96
|
+
expect(el.closeResponse).toBe('close');
|
|
97
|
+
host.remove();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await it('the attribute feeds the headless model', () => {
|
|
101
|
+
const { el, host } = mount({ 'close-response': 'cancel' });
|
|
102
|
+
expect(el.closeResponse).toBe('cancel');
|
|
103
|
+
host.remove();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await it('default-response defaults to null and the attribute sets it', () => {
|
|
107
|
+
const { el, host } = mount();
|
|
108
|
+
expect(el.defaultResponse).toBe(null);
|
|
109
|
+
el.setAttribute('default-response', 'delete');
|
|
110
|
+
expect(el.defaultResponse).toBe('delete');
|
|
111
|
+
host.remove();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await it('removing default-response clears it', () => {
|
|
115
|
+
const { el, host } = mount({ 'default-response': 'delete' });
|
|
116
|
+
el.removeAttribute('default-response');
|
|
117
|
+
expect(el.defaultResponse).toBe(null);
|
|
118
|
+
host.remove();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await it('a dismissal emits the close response the attribute named', () => {
|
|
122
|
+
// The scrim click, not `open = false`. `_dismiss()` SETS `open = false` and
|
|
123
|
+
// then emits, so the property is the lower-level state change and driving it
|
|
124
|
+
// directly would assert a contract this element does not have.
|
|
125
|
+
const { el, host } = mount({ 'close-response': 'cancel', open: '' });
|
|
126
|
+
let seen: string | null = null;
|
|
127
|
+
el.addEventListener('response', (event) => {
|
|
128
|
+
seen = (event as CustomEvent<{ response: string }>).detail.response;
|
|
129
|
+
});
|
|
130
|
+
(el.querySelector('.adw-alert-dialog-scrim') as HTMLElement).click();
|
|
131
|
+
expect(seen).toBe('cancel');
|
|
132
|
+
expect(el.open).toBe(false);
|
|
133
|
+
host.remove();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await it('removing close-response keeps the model value, unlike default-response', () => {
|
|
137
|
+
// An ASYMMETRY worth pinning rather than discovering: `close-response` has a
|
|
138
|
+
// non-null GIR default (`close`), so an absent attribute means "leave the
|
|
139
|
+
// model alone", while `default-response` defaults to null and clears.
|
|
140
|
+
const { el, host } = mount({ 'close-response': 'cancel' });
|
|
141
|
+
el.removeAttribute('close-response');
|
|
142
|
+
expect(el.closeResponse).toBe('cancel');
|
|
143
|
+
host.remove();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
};
|
|
@@ -68,8 +68,10 @@ function mountSizedHeaderBar(width: number): HTMLElement {
|
|
|
68
68
|
bar.appendChild(button);
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
-
//
|
|
72
|
-
//
|
|
71
|
+
// The bar enters the document last, so this geometry is measured on the PARSE-TIME
|
|
72
|
+
// path. It used to be the only path that worked — the sections were built in
|
|
73
|
+
// `connectedCallback` from a one-shot snapshot — and that is no longer a constraint:
|
|
74
|
+
// `slotted-children.spec.ts` requires the appended path to place a child identically.
|
|
73
75
|
host.appendChild(bar);
|
|
74
76
|
return bar;
|
|
75
77
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { ActionRowState } from '@gjsify/adwaita-core';
|
|
18
18
|
|
|
19
19
|
import { bindEmptySections } from '../empty-sections.js';
|
|
20
|
+
import { bindSlottedChildren } from '../slotted-children.js';
|
|
20
21
|
import { type AdwRowActivation, attachRowActivation } from './row-activation.js';
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -90,12 +91,8 @@ export class AdwActionRow extends HTMLElement {
|
|
|
90
91
|
}
|
|
91
92
|
this._initialized = true;
|
|
92
93
|
|
|
93
|
-
const prefixChildren = Array.from(this.querySelectorAll(':scope > [slot="prefix"]'));
|
|
94
|
-
const suffixChildren = Array.from(this.querySelectorAll(':scope > [slot="suffix"]'));
|
|
95
|
-
|
|
96
94
|
this._prefixEl = document.createElement('div');
|
|
97
95
|
this._prefixEl.className = 'adw-action-row-prefix';
|
|
98
|
-
for (const child of prefixChildren) this._prefixEl.appendChild(child);
|
|
99
96
|
|
|
100
97
|
const textEl = document.createElement('div');
|
|
101
98
|
textEl.className = 'adw-action-row-text';
|
|
@@ -107,9 +104,14 @@ export class AdwActionRow extends HTMLElement {
|
|
|
107
104
|
|
|
108
105
|
this._suffixEl = document.createElement('div');
|
|
109
106
|
this._suffixEl.className = 'adw-action-row-suffix';
|
|
110
|
-
for (const child of suffixChildren) this._suffixEl.appendChild(child);
|
|
111
107
|
|
|
112
|
-
|
|
108
|
+
// The two slots stay LIVE — a control appended with `slot="suffix"` after connect
|
|
109
|
+
// has to land in the same section the declared one does, which a snapshot taken
|
|
110
|
+
// here cannot do. `src/slotted-children.ts` carries the incident.
|
|
111
|
+
bindSlottedChildren(this, [
|
|
112
|
+
{ name: 'prefix', into: this._prefixEl },
|
|
113
|
+
{ name: 'suffix', into: this._suffixEl },
|
|
114
|
+
]).install(this._prefixEl, textEl, this._suffixEl);
|
|
113
115
|
// `_render` runs from `attributeChangedCallback`, which a control appended into
|
|
114
116
|
// `suffixSection` never triggers — the section stayed `hidden` and the control
|
|
115
117
|
// measured 0x0. The observer is what keeps the two sections in step with what
|