@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
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;
|
|
@@ -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
|
|
@@ -6,8 +6,10 @@ export declare class AdwHeaderBar extends HTMLElement {
|
|
|
6
6
|
private _endEl;
|
|
7
7
|
/**
|
|
8
8
|
* The derived `<adw-window-title>`, or `null` when a `slot="center"`
|
|
9
|
-
* title-widget took the centre. `Adw.HeaderBar` has the same either/or
|
|
10
|
-
*
|
|
9
|
+
* title-widget took the centre. `Adw.HeaderBar` has the same either/or —
|
|
10
|
+
* `adw_header_bar_set_title_widget` empties the centre bin before installing
|
|
11
|
+
* (adw-header-bar.c:1201) — over a derived `GtkLabel`, not an `AdwWindowTitle`;
|
|
12
|
+
* see the divergence at the top of this file.
|
|
11
13
|
*/
|
|
12
14
|
private _titleEl;
|
|
13
15
|
static get observedAttributes(): string[];
|
|
@@ -22,9 +24,22 @@ export declare class AdwHeaderBar extends HTMLElement {
|
|
|
22
24
|
/**
|
|
23
25
|
* `title` used to be read ONCE, in `connectedCallback`, so every later write
|
|
24
26
|
* was a silent no-op — a header bar whose title tracked the open document
|
|
25
|
-
* kept whatever it was created with. `Adw.HeaderBar`
|
|
26
|
-
*
|
|
27
|
+
* kept whatever it was created with. `Adw.HeaderBar` never reads its title once
|
|
28
|
+
* either: it has no `title` property at all, and re-runs `update_title`
|
|
29
|
+
* (adw-header-bar.c:475) on every ancestor change that could move the answer.
|
|
27
30
|
*/
|
|
28
31
|
attributeChangedCallback(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Give the centre up to a title-widget. `adw_header_bar_set_title_widget` empties the
|
|
34
|
+
* centre bin and drops its derived label (adw-header-bar.c:1201, :1209) rather than
|
|
35
|
+
* stacking the two, so the late case has to remove ours too — a bar showing both would
|
|
36
|
+
* be a shape neither GTK nor the declared markup can produce.
|
|
37
|
+
*
|
|
38
|
+
* NOT SYMMETRIC HERE, AND KNOWN: C rebuilds the label the moment the title-widget goes
|
|
39
|
+
* away (:1211); nothing in this element brings the derived centre back, so a bar that
|
|
40
|
+
* gave it up has given it up for good. `HeaderBarState` in `@gjsify/adwaita-core` holds
|
|
41
|
+
* the rebuild, and this element adopting it is the rewiring tracked in #1343.
|
|
42
|
+
*/
|
|
43
|
+
private _dropDerivedTitle;
|
|
29
44
|
private _renderTitle;
|
|
30
45
|
}
|
|
@@ -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,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.45.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.45.0",
|
|
49
|
+
"@gjsify/adwaita-fonts": "^0.45.0",
|
|
50
|
+
"@gjsify/adwaita-icons": "^0.45.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.45.0",
|
|
55
|
+
"@gjsify/unit": "^0.45.0",
|
|
56
56
|
"sass": "^1.101.0",
|
|
57
57
|
"typescript": "^6.0.3"
|
|
58
58
|
},
|
|
@@ -75,6 +75,9 @@
|
|
|
75
75
|
"navigator",
|
|
76
76
|
"process"
|
|
77
77
|
]
|
|
78
|
+
},
|
|
79
|
+
"widgetVocabulary": {
|
|
80
|
+
"role": "renderer"
|
|
78
81
|
}
|
|
79
82
|
},
|
|
80
83
|
"license": "MIT",
|
|
@@ -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
|
+
};
|
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
// the open document keeps its construction-time title otherwise, and nothing notices
|
|
5
5
|
// because the attribute is still there and A title still renders.
|
|
6
6
|
//
|
|
7
|
-
// `
|
|
8
|
-
// `
|
|
9
|
-
//
|
|
7
|
+
// The either/or the `slot="center"` case below asserts IS `Adw.HeaderBar`'s: setting
|
|
8
|
+
// `title-widget` empties the centre bin (adw-header-bar.c:1201), and clearing it builds the
|
|
9
|
+
// derived centre again (:1211). What C builds there is a plain `gtk_label_new (NULL)`
|
|
10
|
+
// (`construct_title_label`, :512) with no subtitle, and no `title` property behind it —
|
|
11
|
+
// `update_title` (:475) resolves one from the ancestry. The `<adw-window-title>` centre and
|
|
12
|
+
// the title/subtitle PAIR are this port's DIVERGENCE, named in `elements/adw-header-bar.ts`
|
|
13
|
+
// and recorded as `HeaderBarRenderState.derivedSubtitle` in `@gjsify/adwaita-core`.
|
|
10
14
|
|
|
11
15
|
import { describe, expect, it } from '@gjsify/unit';
|
|
12
16
|
|
|
@@ -68,8 +72,10 @@ function mountSizedHeaderBar(width: number): HTMLElement {
|
|
|
68
72
|
bar.appendChild(button);
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
|
-
//
|
|
72
|
-
//
|
|
75
|
+
// The bar enters the document last, so this geometry is measured on the PARSE-TIME
|
|
76
|
+
// path. It used to be the only path that worked — the sections were built in
|
|
77
|
+
// `connectedCallback` from a one-shot snapshot — and that is no longer a constraint:
|
|
78
|
+
// `slotted-children.spec.ts` requires the appended path to place a child identically.
|
|
73
79
|
host.appendChild(bar);
|
|
74
80
|
return bar;
|
|
75
81
|
}
|
|
@@ -120,7 +126,8 @@ export const AdwHeaderBarTest = async () => {
|
|
|
120
126
|
});
|
|
121
127
|
});
|
|
122
128
|
|
|
123
|
-
// The centre is
|
|
129
|
+
// The centre is an `<adw-window-title>` — this port's divergence, not the bare
|
|
130
|
+
// `GtkLabel` `construct_title_label` builds (adw-header-bar.c:512) — so the three
|
|
124
131
|
// rules that element holds through @gjsify/adwaita-core reach the header bar too.
|
|
125
132
|
// None of them did while the centre was a bare span.
|
|
126
133
|
await describe('<adw-header-bar> derives an <adw-window-title>', async () => {
|
package/src/adw-tab-view.spec.ts
CHANGED
|
@@ -75,6 +75,77 @@ export const AdwTabViewTest = async () => {
|
|
|
75
75
|
});
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
// ONE HALF of the upgrade-order defect; the other half is the define order at the
|
|
79
|
+
// foot of `elements/adw-tab-view.ts`, which no test in this realm can observe —
|
|
80
|
+
// both names are already registered by the time a spec runs, and `define` is what
|
|
81
|
+
// upgrades. What IS reproducible here is the STATE that order produced: a real
|
|
82
|
+
// `<adw-tab-page>` next to an `<adw-tab-view>` that is still an ordinary
|
|
83
|
+
// HTMLElement. `document.implementation.createHTMLDocument()` has no browsing
|
|
84
|
+
// context, so nothing it creates is ever upgraded; adopting the element carries it
|
|
85
|
+
// into this document without upgrading it, which INSERTING it would do.
|
|
86
|
+
//
|
|
87
|
+
// Constructing both with `document.createElement` instead would pass either way —
|
|
88
|
+
// both are defined, so both come back upgraded and the ancestor always has the
|
|
89
|
+
// method. The parse-then-define order the site hits is covered in
|
|
90
|
+
// `tests/browser/specs/adwaita-upgrade-order.spec.ts`.
|
|
91
|
+
await describe('adw-tab-page beside an un-upgraded view', async () => {
|
|
92
|
+
await it('reports nothing when the view has none of its methods yet', async () => {
|
|
93
|
+
const view = document.adoptNode(
|
|
94
|
+
document.implementation.createHTMLDocument('').createElement('adw-tab-view'),
|
|
95
|
+
);
|
|
96
|
+
expect('syncDeclaredPage' in view).toBe(false);
|
|
97
|
+
const page = document.createElement('adw-tab-page');
|
|
98
|
+
view.appendChild(page);
|
|
99
|
+
|
|
100
|
+
// A custom-element reaction never throws INTO its caller: the exception is
|
|
101
|
+
// REPORTED, so `setAttribute` returns normally whether or not the callback
|
|
102
|
+
// blew up and only a listener sees the difference. `preventDefault` keeps a
|
|
103
|
+
// red run from also logging it at the console.
|
|
104
|
+
const reported: string[] = [];
|
|
105
|
+
const onError = (event: ErrorEvent) => {
|
|
106
|
+
reported.push(event.message);
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
};
|
|
109
|
+
window.addEventListener('error', onError);
|
|
110
|
+
page.setAttribute('title', 'One');
|
|
111
|
+
window.removeEventListener('error', onError);
|
|
112
|
+
|
|
113
|
+
expect(reported).toStrictEqual([]);
|
|
114
|
+
expect(page.getAttribute('title')).toBe('One');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await it('leaves a page appended after connect unadopted rather than throwing', async () => {
|
|
118
|
+
// The imperative surface is `appendPage`; a bare DOM append is out of
|
|
119
|
+
// contract, and the point here is only that it fails QUIETLY — the page
|
|
120
|
+
// carries no `data-page-id`, so `syncDeclaredPage` has no record to touch.
|
|
121
|
+
//
|
|
122
|
+
// "rather than throwing" is ASSERTED and not assumed. Without the listener
|
|
123
|
+
// this test measured only the two counts, and both hold whether or not the
|
|
124
|
+
// callback blew up: a custom-element reaction is REPORTED, never rethrown
|
|
125
|
+
// into `setAttribute`, so the model is untouched either way. Measured — with
|
|
126
|
+
// `syncDeclaredPage` mutated to throw on exactly this path, the whole
|
|
127
|
+
// in-bundle suite of 4631 stayed green and only an unrelated spec's global
|
|
128
|
+
// error sweep noticed.
|
|
129
|
+
const view = makeTabView();
|
|
130
|
+
const late = document.createElement('adw-tab-page');
|
|
131
|
+
view.appendChild(late);
|
|
132
|
+
|
|
133
|
+
const reported: string[] = [];
|
|
134
|
+
const onError = (event: ErrorEvent) => {
|
|
135
|
+
reported.push(event.message);
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
};
|
|
138
|
+
window.addEventListener('error', onError);
|
|
139
|
+
late.setAttribute('title', 'Late');
|
|
140
|
+
window.removeEventListener('error', onError);
|
|
141
|
+
|
|
142
|
+
expect(reported).toStrictEqual([]);
|
|
143
|
+
expect(view.nPages).toBe(3);
|
|
144
|
+
expect(chips(view).length).toBe(3);
|
|
145
|
+
view.parentElement?.remove();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
78
149
|
await describe('adw-tab-view expand-tabs / no-close', async () => {
|
|
79
150
|
await it('no-close hides every close affordance', async () => {
|
|
80
151
|
const view = makeTabView('no-close');
|
|
@@ -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
|