@gjsify/adwaita-web 0.64.12 → 0.64.13
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/dist/adwaita-web.css +31 -0
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/elements/adw-bin.d.ts +13 -0
- package/lib/types/elements/adw-expander-row.d.ts +3 -0
- package/lib/types/elements/gtk-label.d.ts +64 -0
- package/lib/types/label-markup.d.ts +30 -0
- package/lib/types/pango-markup.d.ts +17 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/lib/types/widget-element.d.ts +8 -2
- package/package.json +11 -11
- package/scss/_labels.scss +19 -0
- package/scss/_link.scss +73 -7
- package/src/adw-action-rows.spec.ts +19 -0
- package/src/adw-bin.spec.ts +20 -0
- package/src/adw-row-state.spec.ts +80 -0
- package/src/elements/adw-bin.ts +20 -1
- package/src/elements/adw-expander-row.ts +46 -16
- package/src/elements/gtk-label.ts +206 -16
- package/src/gtk-label.spec.ts +431 -11
- package/src/gtk-switch.spec.ts +8 -0
- package/src/label-markup.ts +193 -0
- package/src/pango-markup.spec.ts +11 -0
- package/src/pango-markup.ts +14 -5
- package/src/split-button.spec.ts +7 -4
- package/src/styles.generated.ts +1 -1
- package/src/widget-element.ts +28 -7
- package/src/widget-members.spec.ts +42 -3
|
@@ -9,7 +9,13 @@ export declare class WidgetElement extends HTMLElement {
|
|
|
9
9
|
get_visible(): boolean;
|
|
10
10
|
/** `gtk_widget_set_visible`. */
|
|
11
11
|
set_visible(visible: boolean): void;
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* `gtk_widget_get_first_child`: the first child WIDGET, `null` for a leaf.
|
|
14
|
+
*
|
|
15
|
+
* The five walkers skip the elements a port builds for its own insides — `isWidgetNode`
|
|
16
|
+
* in `@gjsify/adwaita-core` carries the rule and the incident behind it, and the
|
|
17
|
+
* registered-class copy in `widgetTreeMembers()` answers from the same function.
|
|
18
|
+
*/
|
|
13
19
|
get_first_child(): Element | null;
|
|
14
20
|
/** `gtk_widget_get_last_child`. */
|
|
15
21
|
get_last_child(): Element | null;
|
|
@@ -17,7 +23,7 @@ export declare class WidgetElement extends HTMLElement {
|
|
|
17
23
|
get_next_sibling(): Element | null;
|
|
18
24
|
/** `gtk_widget_get_prev_sibling`. */
|
|
19
25
|
get_prev_sibling(): Element | null;
|
|
20
|
-
/** `gtk_widget_get_parent
|
|
26
|
+
/** `gtk_widget_get_parent`: the nearest ancestor that is a widget, not a port's own wrapper. */
|
|
21
27
|
get_parent(): Element | null;
|
|
22
28
|
/** `gtk_widget_get_width`: the layout box in CSS px; 0 until the widget is rendered. */
|
|
23
29
|
get_width(): number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/adwaita-web",
|
|
3
|
-
"version": "0.64.
|
|
3
|
+
"version": "0.64.13",
|
|
4
4
|
"description": "Adwaita/Libadwaita web components for browser targets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -58,14 +58,14 @@
|
|
|
58
58
|
"@codemirror/language": "^6.12.4",
|
|
59
59
|
"@codemirror/state": "^6.7.0",
|
|
60
60
|
"@codemirror/view": "^6.43.4",
|
|
61
|
-
"@gjsify/adwaita-core": "^0.64.
|
|
62
|
-
"@gjsify/adwaita-fonts": "^0.64.
|
|
63
|
-
"@gjsify/adwaita-icons": "^0.64.
|
|
64
|
-
"@gjsify/gtksource-core": "^0.64.
|
|
61
|
+
"@gjsify/adwaita-core": "^0.64.13",
|
|
62
|
+
"@gjsify/adwaita-fonts": "^0.64.13",
|
|
63
|
+
"@gjsify/adwaita-icons": "^0.64.13",
|
|
64
|
+
"@gjsify/gtksource-core": "^0.64.13",
|
|
65
65
|
"@lezer/highlight": "^1.2.3"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
|
-
"@gjsify/app-settings": "^0.64.
|
|
68
|
+
"@gjsify/app-settings": "^0.64.13"
|
|
69
69
|
},
|
|
70
70
|
"peerDependenciesMeta": {
|
|
71
71
|
"@gjsify/app-settings": {
|
|
@@ -73,11 +73,11 @@
|
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@gjsify/app-settings": "^0.64.
|
|
77
|
-
"@gjsify/blueprint": "^0.64.
|
|
78
|
-
"@gjsify/cli": "^0.64.
|
|
79
|
-
"@gjsify/unit": "^0.64.
|
|
80
|
-
"@gjsify/vite-plugin-blueprint": "^0.64.
|
|
76
|
+
"@gjsify/app-settings": "^0.64.13",
|
|
77
|
+
"@gjsify/blueprint": "^0.64.13",
|
|
78
|
+
"@gjsify/cli": "^0.64.13",
|
|
79
|
+
"@gjsify/unit": "^0.64.13",
|
|
80
|
+
"@gjsify/vite-plugin-blueprint": "^0.64.13",
|
|
81
81
|
"sass": "^1.101.0",
|
|
82
82
|
"typescript": "^6.0.3"
|
|
83
83
|
},
|
package/scss/_labels.scss
CHANGED
|
@@ -109,6 +109,25 @@ gtk-label {
|
|
|
109
109
|
max-width: 100%;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
// `gtk-label.ts`'s run-render path, SINGLE-LINE: the markup's runs move into this span
|
|
113
|
+
// because this element is a flex container (the two spacers above), and a node directly
|
|
114
|
+
// under one is a FLEX ITEM, not text — `vertical-align` does not apply to a flex item,
|
|
115
|
+
// so `<sup>`/`<sub>` stop being raised, and `align-items` centres each run on its own
|
|
116
|
+
// instead of the line sharing one baseline. Plain text never needed a box: a bare text
|
|
117
|
+
// node is ONE anonymous item, where a run list is one item per run.
|
|
118
|
+
//
|
|
119
|
+
// NO `max-width` HERE, unlike `.adw-label-text`: this box only ever holds ONE line (the
|
|
120
|
+
// host's `white-space: pre`, and this path is the not-wrapping, not-ellipsizing case),
|
|
121
|
+
// so there is no shrink-to-fit collapse to measure around — a single-line label as wide
|
|
122
|
+
// as its text, overflowing a narrower container, is what GtkLabel does and what
|
|
123
|
+
// `gtk-label.spec.ts` already pins for the plain-text path. The box's own size is its
|
|
124
|
+
// text's: `flex-basis: auto` over `width: auto` is the item's max-content width, which
|
|
125
|
+
// leaves the two spacers the free space `xalign` splits.
|
|
126
|
+
.adw-label-runs {
|
|
127
|
+
// Blockified by spec as a flex item; spelled out as `.adw-label-text` spells it out.
|
|
128
|
+
display: block;
|
|
129
|
+
}
|
|
130
|
+
|
|
112
131
|
// `wrap-mode` — Pango's break-opportunity choice — is a real `word-break`/
|
|
113
132
|
// `overflow-wrap` mapping, not a declared gap: CSS's `break-all` is Pango's CHAR (break
|
|
114
133
|
// anywhere), and `overflow-wrap: anywhere` is Pango's WORD_CHAR (word boundaries, falling
|
package/scss/_link.scss
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
// _link.scss — `link` and `button.link`, libadwaita's link treatment.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// `button.link` (`_links.scss:1-2`)
|
|
5
|
-
// `<
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
3
|
+
// TWO NODES, NEVER A BARE `.link`. Upstream's rule names the `link` NODE and
|
|
4
|
+
// `button.link` (`_links.scss:1-2`). `button.link` is `<gtk-link-button>`'s inner
|
|
5
|
+
// `<button class="adw-button link">`, which the first selector below reads. The `link`
|
|
6
|
+
// node is the inner node of a `Gtk.Label`'s `<a>` markup, and `<gtk-label>` now DRAWS
|
|
7
|
+
// that — an `<a href>` per link in the markup (`label-markup.ts`) — so the second
|
|
8
|
+
// selector is upstream's `link` rule, ported onto the element that finally exists.
|
|
9
|
+
//
|
|
10
|
+
// The `.link` style CLASS on anything but those two is unstyled upstream: `AdwLinkRow`
|
|
11
|
+
// adds it to its row (adw-link-row.c:377), and no rule matches `row.link`, so its title
|
|
12
|
+
// stays in the row's own colour. A bare `.link` here painted the About dialog's link rows
|
|
13
|
+
// blue and underlined. It would now also hit `<gtk-label>` ITSELF, which gains the class
|
|
14
|
+
// whenever its markup has a link (gtklabel.c:3896) — the label is not a link, its anchors
|
|
15
|
+
// are. So both selectors name a node, and neither matches a class alone.
|
|
16
|
+
//
|
|
17
|
+
// THE LABEL'S ANCHORS ARE NOT STYLED THROUGH `:visited`, the CSS pseudo-class, even
|
|
18
|
+
// though they are real `<a href>` elements that would match it. A browser's `:visited` is
|
|
19
|
+
// the user's BROWSING HISTORY: a link followed in another tab, or last year, would come
|
|
20
|
+
// up visited in a label that has never been clicked, which `Gtk.Label` never does. Its
|
|
21
|
+
// `visited` is per-label state carried by URI across a re-parse
|
|
22
|
+
// (`labelLinksInheritVisited`), painted as `.visited` by `_paintVisited` — the same class
|
|
23
|
+
// `<gtk-link-button>` writes for the same reason one paragraph down.
|
|
10
24
|
//
|
|
11
25
|
// THE TWO COLOURS ARE THE PALETTE'S, not new tokens: `$link_color: var(--accent-color)`
|
|
12
26
|
// and `$link_visited_color: color-mix(in srgb, var(--accent-color) 80%, var(--view-fg-color))`
|
|
@@ -36,6 +50,7 @@
|
|
|
36
50
|
$link-color: var(--accent-color);
|
|
37
51
|
$link-visited-color: color-mix(in srgb, var(--accent-color) 80%, var(--view-fg-color));
|
|
38
52
|
$osd-link-color: color-mix(in srgb, var(--accent-bg-color) 50%, white);
|
|
53
|
+
$osd-link-visited-color: color-mix(in srgb, var(--accent-bg-color) 75%, white);
|
|
39
54
|
|
|
40
55
|
.adw-button.link {
|
|
41
56
|
color: $link-color;
|
|
@@ -82,4 +97,55 @@ $osd-link-color: color-mix(in srgb, var(--accent-bg-color) 50%, white);
|
|
|
82
97
|
&.checked {
|
|
83
98
|
background-color: transparent;
|
|
84
99
|
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Upstream's bare `link` node — a `Gtk.Label`'s `<a href>` markup, which `<gtk-label>`
|
|
103
|
+
// draws as a real anchor per link (`label-markup.ts`, `buildAnchor`).
|
|
104
|
+
//
|
|
105
|
+
// THE SELECTOR IS THE ATTRIBUTE, not the class. `data-gtk-link` is written by the renderer
|
|
106
|
+
// and by nothing else, so it names the node the way upstream's `link` names a GTK node;
|
|
107
|
+
// the `.link` class on the same anchor is GTK's own style class, which a consumer may also
|
|
108
|
+
// put on anything (and `<gtk-label>` ITSELF carries it whenever its markup has a link —
|
|
109
|
+
// see the header). Scoped under `gtk-label` so an `<a>` a consumer put in a label's slot
|
|
110
|
+
// for its own reasons is untouched.
|
|
111
|
+
gtk-label a[data-gtk-link] {
|
|
112
|
+
color: $link-color;
|
|
113
|
+
text-decoration: underline;
|
|
114
|
+
font-weight: inherit;
|
|
115
|
+
|
|
116
|
+
&:hover {
|
|
117
|
+
color: hsl(from $link-color h calc(s * 1.1) calc(l * 1.1));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
&:active {
|
|
121
|
+
color: $link-color;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// `.visited` and NOT `:visited` — the pseudo-class is the user's browsing history, where
|
|
125
|
+
// `Gtk.Label`'s `visited` is per-label state carried by URI (`_paintVisited`, and the
|
|
126
|
+
// paragraph in the header).
|
|
127
|
+
&.visited {
|
|
128
|
+
color: $link-visited-color;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.osd & {
|
|
132
|
+
color: $osd-link-color;
|
|
133
|
+
|
|
134
|
+
&.visited {
|
|
135
|
+
color: $osd-link-visited-color;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
&:active {
|
|
139
|
+
color: $osd-link-color;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// `link { @include focus-ring() }` (_links.scss:33-35) — the mixin is a 2px outline at
|
|
144
|
+
// offset -2px (`_drawing.scss`), spelled out as `_expander.scss` and `_checks.scss`
|
|
145
|
+
// spell it out. `:focus-visible` rather than `:focus`: GTK draws the ring on keyboard
|
|
146
|
+
// focus, and a click on a link must not leave one behind.
|
|
147
|
+
&:focus-visible {
|
|
148
|
+
outline: 2px solid var(--accent-color);
|
|
149
|
+
outline-offset: -2px;
|
|
150
|
+
}
|
|
85
151
|
}
|
|
@@ -19,6 +19,7 @@ import { describe, expect, it } from '@gjsify/unit';
|
|
|
19
19
|
|
|
20
20
|
import {
|
|
21
21
|
ACTION_ROW_ACTIVATION_VECTORS,
|
|
22
|
+
ACTION_ROW_SUBTITLE_VECTORS,
|
|
22
23
|
BUTTON_ROW_ACTIVATABLE_VECTORS,
|
|
23
24
|
BUTTON_ROW_ICON_VECTORS,
|
|
24
25
|
LABEL_VISIBILITY_VECTORS,
|
|
@@ -98,6 +99,24 @@ export const AdwActionRowsTest = async () => {
|
|
|
98
99
|
});
|
|
99
100
|
});
|
|
100
101
|
|
|
102
|
+
await describe('<adw-action-row> subtitle (libadwaita conformance vectors)', async () => {
|
|
103
|
+
// The element is attribute-driven and publishes no `notify::subtitle`, so only the
|
|
104
|
+
// value the vectors hold afterwards is observable; `null` is an absent attribute.
|
|
105
|
+
for (const vector of ACTION_ROW_SUBTITLE_VECTORS) {
|
|
106
|
+
await it(`${vector.name} — ${vector.rule}`, () => {
|
|
107
|
+
const { el: row, host } = mount<AdwActionRow>('adw-action-row');
|
|
108
|
+
for (const step of vector.steps) {
|
|
109
|
+
if (step.write === null) row.removeAttribute('subtitle');
|
|
110
|
+
else row.setAttribute('subtitle', step.write);
|
|
111
|
+
const label = row.querySelector('.adw-row-subtitle') as HTMLElement;
|
|
112
|
+
expect(label.textContent).toBe(step.subtitle);
|
|
113
|
+
expect(label.hidden).toBe(step.subtitle === '');
|
|
114
|
+
}
|
|
115
|
+
host.remove();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
101
120
|
await describe('<adw-action-row> activatable-widget (libadwaita conformance vectors)', async () => {
|
|
102
121
|
for (const vector of ACTION_ROW_ACTIVATION_VECTORS) {
|
|
103
122
|
await it(`${vector.name} — ${vector.rule}`, async () => {
|
package/src/adw-bin.spec.ts
CHANGED
|
@@ -61,6 +61,26 @@ export const AdwBinTest = async () => {
|
|
|
61
61
|
other.remove();
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
await it('answers `get_child()` / `set_child()` too, the spelling GI-built code uses', () => {
|
|
65
|
+
// `Adw.Bin:child` has two doors, and markup only takes one of them: code written
|
|
66
|
+
// against GI calls the methods, which is what a `.blp`-driven widget shared with
|
|
67
|
+
// GJS and NativeScript does. Both of those renderers answer them; the web did not,
|
|
68
|
+
// and shared code died here with `get_child is not a function`.
|
|
69
|
+
const { el, host } = mount();
|
|
70
|
+
expect(el.get_child()).toBeNull();
|
|
71
|
+
const child = document.createElement('div');
|
|
72
|
+
el.set_child(child);
|
|
73
|
+
expect(el.get_child()).toBe(child);
|
|
74
|
+
expect(el.child).toBe(child);
|
|
75
|
+
const next = document.createElement('span');
|
|
76
|
+
el.set_child(next);
|
|
77
|
+
expect(el.get_child()).toBe(next);
|
|
78
|
+
expect(el.childElementCount).toBe(1);
|
|
79
|
+
el.set_child(null);
|
|
80
|
+
expect(el.get_child()).toBeNull();
|
|
81
|
+
host.remove();
|
|
82
|
+
});
|
|
83
|
+
|
|
64
84
|
await it('a bin is a column flex box, and its only child fills it', () => {
|
|
65
85
|
const { el, host } = mount([document.createElement('div')]);
|
|
66
86
|
// A bin takes the size it is GIVEN and hands all of it to the child, which
|
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
COMBO_CHOOSER_VECTORS,
|
|
16
16
|
COMBO_ITEMS_CHANGED_VECTORS,
|
|
17
17
|
COMBO_SELECTION_VECTORS,
|
|
18
|
+
EXPANDER_ROW_CONSTRUCT_VECTORS,
|
|
19
|
+
EXPANDER_ROW_INITIAL,
|
|
20
|
+
EXPANDER_ROW_VECTORS,
|
|
18
21
|
LIST_ITEMS_CHANGED_VECTORS,
|
|
19
22
|
LIST_MODEL_OWNERSHIP_VECTORS,
|
|
20
23
|
LIST_NORMALIZE_VECTORS,
|
|
@@ -779,6 +782,83 @@ export const AdwRowStateTest = async () => {
|
|
|
779
782
|
host.remove();
|
|
780
783
|
});
|
|
781
784
|
|
|
785
|
+
for (const vector of EXPANDER_ROW_VECTORS) {
|
|
786
|
+
await it(`holds ${vector.name} (shared conformance vectors)`, async () => {
|
|
787
|
+
const { el: row, host } = parse<AdwExpanderRow>(
|
|
788
|
+
'<adw-expander-row title="More"><adw-action-row title="Nested"></adw-action-row></adw-expander-row>',
|
|
789
|
+
'adw-expander-row',
|
|
790
|
+
);
|
|
791
|
+
expect(row.expanded).toBe(EXPANDER_ROW_INITIAL.expanded);
|
|
792
|
+
expect(row.enableExpansion).toBe(EXPANDER_ROW_INITIAL.enableExpansion);
|
|
793
|
+
for (const step of vector.steps) {
|
|
794
|
+
if (step.op === 'expanded') row.expanded = step.value;
|
|
795
|
+
else row.enableExpansion = step.value;
|
|
796
|
+
// A DOM element publishes only the user's notifications, so `notified` is
|
|
797
|
+
// not observable here — the held values and what they reflect to are.
|
|
798
|
+
expect(row.expanded).toBe(step.expanded);
|
|
799
|
+
expect(row.enableExpansion).toBe(step.enableExpansion);
|
|
800
|
+
expect(row.hasAttribute('expanded')).toBe(step.expanded);
|
|
801
|
+
expect(row.classList.contains('expanded')).toBe(step.expanded);
|
|
802
|
+
}
|
|
803
|
+
host.remove();
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
for (const vector of EXPANDER_ROW_CONSTRUCT_VECTORS) {
|
|
808
|
+
await it(`parses expanded=${vector.expanded}, enable-expansion=${vector.enableExpansion} — ${vector.rule}`, async () => {
|
|
809
|
+
const enable = `enable-expansion="${vector.enableExpansion}"`;
|
|
810
|
+
const expanded = vector.expanded ? 'expanded' : '';
|
|
811
|
+
// Both attribute orders: the parser delivers them in source order.
|
|
812
|
+
for (const attributes of [`${enable} ${expanded}`, `${expanded} ${enable}`]) {
|
|
813
|
+
const { el: row, host } = parse<AdwExpanderRow>(
|
|
814
|
+
`<adw-expander-row title="x" ${attributes}></adw-expander-row>`,
|
|
815
|
+
'adw-expander-row',
|
|
816
|
+
);
|
|
817
|
+
expect(row.enableExpansion).toBe(vector.enableExpansion);
|
|
818
|
+
expect(row.expanded).toBe(vector.resultExpanded);
|
|
819
|
+
expect(row.hasAttribute('expanded')).toBe(vector.resultExpanded);
|
|
820
|
+
host.remove();
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
await it('does not open on a header click while expansion is disabled', async () => {
|
|
826
|
+
const { el: row, host } = parse<AdwExpanderRow>(
|
|
827
|
+
'<adw-expander-row title="More" enable-expansion="false"></adw-expander-row>',
|
|
828
|
+
'adw-expander-row',
|
|
829
|
+
);
|
|
830
|
+
const header = row.querySelector('.adw-expander-row-header') as HTMLDivElement;
|
|
831
|
+
const events = record(row, 'notify::expanded');
|
|
832
|
+
header.click();
|
|
833
|
+
expect(row.expanded).toBe(false);
|
|
834
|
+
expect(events.details.length).toBe(0);
|
|
835
|
+
events.stop();
|
|
836
|
+
host.remove();
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
await it('the enable switch publishes expanded BEFORE enable-expansion, as libadwaita emits them', async () => {
|
|
840
|
+
const { el: row, host } = parse<AdwExpanderRow>(
|
|
841
|
+
'<adw-expander-row title="More" show-enable-switch enable-expansion="false"></adw-expander-row>',
|
|
842
|
+
'adw-expander-row',
|
|
843
|
+
);
|
|
844
|
+
const seen: string[] = [];
|
|
845
|
+
row.addEventListener('notify::expanded', () => seen.push('expanded'));
|
|
846
|
+
row.addEventListener('notify::enable-expansion', () => seen.push('enable-expansion'));
|
|
847
|
+
const toggle = row.querySelector('.adw-expander-row-enable-switch') as HTMLElement & { active: boolean };
|
|
848
|
+
|
|
849
|
+
toggle.active = true;
|
|
850
|
+
expect(seen.join()).toBe('expanded,enable-expansion');
|
|
851
|
+
expect(row.expanded).toBe(true);
|
|
852
|
+
expect(row.hasAttribute('expanded')).toBe(true);
|
|
853
|
+
|
|
854
|
+
seen.length = 0;
|
|
855
|
+
toggle.active = false;
|
|
856
|
+
expect(seen.join()).toBe('expanded,enable-expansion');
|
|
857
|
+
expect(row.expanded).toBe(false);
|
|
858
|
+
expect(row.hasAttribute('expanded')).toBe(false);
|
|
859
|
+
host.remove();
|
|
860
|
+
});
|
|
861
|
+
|
|
782
862
|
await it('adopts a declarative expanded attribute', async () => {
|
|
783
863
|
const { el: row, host } = parse<AdwExpanderRow>(
|
|
784
864
|
'<adw-expander-row title="Open" expanded></adw-expander-row>',
|
package/src/elements/adw-bin.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// default `GENERIC` and a bin contributes nothing to the accessibility tree of its own.
|
|
25
25
|
//
|
|
26
26
|
// KNOWN_GAPS: none — `AdwBin`'s only property is `child`, a widget, which is a SLOT on
|
|
27
|
-
// every renderer
|
|
27
|
+
// every renderer, and it is reachable BOTH ways: `child` and `get_child()`/`set_child()`.
|
|
28
28
|
//
|
|
29
29
|
// Reference: refs/libadwaita/src/adw-bin.c (adw_bin_set_child, class_init)
|
|
30
30
|
// Copyright (c) 2021 Purism SPC / GNOME contributors (libadwaita). LGPLv2.1+.
|
|
@@ -46,6 +46,25 @@ export class AdwBin extends WidgetElement {
|
|
|
46
46
|
this.setChild(value);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* `adw_bin_get_child` / `adw_bin_set_child` — the METHOD spelling of {@link child}.
|
|
51
|
+
*
|
|
52
|
+
* The property alone is not the whole door. `Adw.Bin:child` is reached two ways and only
|
|
53
|
+
* one of them is a property: markup writes `child:`, but code built against GI calls
|
|
54
|
+
* `bin.get_child()` / `bin.set_child(w)`, which is the spelling a `.blp`-driven widget
|
|
55
|
+
* shared across renderers uses — GJS and the NativeScript bridge both answer it
|
|
56
|
+
* (`AdwSingleChildBase`), and here it was missing, so the shared code died with
|
|
57
|
+
* `get_child is not a function` on the web and nowhere else.
|
|
58
|
+
*/
|
|
59
|
+
get_child(): Element | null {
|
|
60
|
+
return this.child;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `adw_bin_set_child`. Refuses a child that already has a parent, as {@link setChild} does. */
|
|
64
|
+
set_child(child: Element | null): void {
|
|
65
|
+
this.setChild(child);
|
|
66
|
+
}
|
|
67
|
+
|
|
49
68
|
connectedCallback() {
|
|
50
69
|
// The NAME only. Every child of a bin already IS its content — there is no
|
|
51
70
|
// internal structure to route into — so `.install()` is deliberately not called
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
// listbox. The `notify::expanded` / `notify::enable-expansion` events are named after
|
|
14
14
|
// Adw.ExpanderRow's GObject signals so one code path drives the web and GTK renderers.
|
|
15
15
|
//
|
|
16
|
-
// The DISCLOSURE state (the expanded flag
|
|
17
|
-
// transitions) is HEADLESS and lives in `@gjsify/adwaita-core`
|
|
18
|
-
// {@link ExpanderState}; this element keeps only the DOM half — header/content
|
|
19
|
-
// the `expanded` attribute reflection and the
|
|
16
|
+
// The DISCLOSURE state (the expanded flag, the `enable-expansion` gate over it and their
|
|
17
|
+
// idempotent, notify-on-change transitions) is HEADLESS and lives in `@gjsify/adwaita-core`
|
|
18
|
+
// (ADR 0004) as {@link ExpanderState}; this element keeps only the DOM half — header/content
|
|
19
|
+
// markup, the `expanded` / `enable-expansion` attribute reflection and the events.
|
|
20
20
|
//
|
|
21
21
|
// Reference: refs/adwaita-web/adwaita-web/scss/_expander_row.scss
|
|
22
22
|
// Reference: refs/libadwaita/src/stylesheet/widgets/_lists.scss (AdwExpanderRow)
|
|
@@ -70,8 +70,7 @@ export class AdwExpanderRow extends TooltipElement {
|
|
|
70
70
|
// custom-element constructor rules allow it: nothing touches the DOM until the
|
|
71
71
|
// state actually changes.
|
|
72
72
|
this._state.subscribe((expanded) => {
|
|
73
|
-
|
|
74
|
-
else this.removeAttribute('expanded');
|
|
73
|
+
this._reflectExpanded(expanded);
|
|
75
74
|
if (this._initialized) this._render();
|
|
76
75
|
});
|
|
77
76
|
}
|
|
@@ -98,7 +97,7 @@ export class AdwExpanderRow extends TooltipElement {
|
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
get enableExpansion(): boolean {
|
|
101
|
-
return
|
|
100
|
+
return this._state.enableExpansion;
|
|
102
101
|
}
|
|
103
102
|
|
|
104
103
|
set enableExpansion(value: boolean) {
|
|
@@ -138,7 +137,7 @@ export class AdwExpanderRow extends TooltipElement {
|
|
|
138
137
|
// Clicking the enable switch does NOT toggle the disclosure.
|
|
139
138
|
this._enableSwitch = document.createElement('gtk-switch') as GtkSwitch;
|
|
140
139
|
this._enableSwitch.classList.add('adw-expander-row-enable-switch');
|
|
141
|
-
this._enableSwitch.active = this.enableExpansion;
|
|
140
|
+
this._enableSwitch.active = this._state.enableExpansion;
|
|
142
141
|
|
|
143
142
|
this._chevronEl = createGtkImage('go-down', 'adw-expander-row-chevron');
|
|
144
143
|
|
|
@@ -197,27 +196,58 @@ export class AdwExpanderRow extends TooltipElement {
|
|
|
197
196
|
event.stopPropagation();
|
|
198
197
|
if (this._reflectingEnable) return;
|
|
199
198
|
const enabled = this._enableSwitch.active;
|
|
199
|
+
// The state drives `expanded` BEFORE it notifies `enable-expansion`, and reports
|
|
200
|
+
// both in that order, so the events leave in the order libadwaita emits them.
|
|
201
|
+
const notified = this._state.setEnableExpansion(enabled);
|
|
200
202
|
this.setAttribute('enable-expansion', enabled ? 'true' : 'false');
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
203
|
+
for (const property of notified) {
|
|
204
|
+
this.dispatchEvent(
|
|
205
|
+
new CustomEvent(`notify::${property}`, {
|
|
206
|
+
bubbles: true,
|
|
207
|
+
detail: property === 'expanded' ? { expanded: this.expanded } : { enableExpansion: enabled },
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
207
211
|
});
|
|
208
212
|
|
|
209
213
|
// Seed the disclosure from the parsed markup: attributeChangedCallback is guarded
|
|
210
214
|
// until the element is initialized, so a declarative `expanded` lands here.
|
|
215
|
+
// `enable-expansion` is already in the state (see attributeChangedCallback), so a
|
|
216
|
+
// markup `expanded` on a disabled row is refused as libadwaita refuses it.
|
|
211
217
|
this._state.setExpanded(this.hasAttribute('expanded'));
|
|
218
|
+
this._reflectExpanded(this._state.expanded);
|
|
212
219
|
this._render();
|
|
213
220
|
}
|
|
214
221
|
|
|
215
222
|
attributeChangedCallback(name: string) {
|
|
223
|
+
if (name === 'enable-expansion') {
|
|
224
|
+
// Taken before the init guard so `enableExpansion` answers a parsed attribute
|
|
225
|
+
// before the element connects. A programmatic write: the state drives `expanded`
|
|
226
|
+
// with it and the subscriber reflects that, silently. Only the switch publishes
|
|
227
|
+
// `notify::enable-expansion`.
|
|
228
|
+
this._state.setEnableExpansion(this._enableAttribute());
|
|
229
|
+
}
|
|
216
230
|
if (!this._initialized) return;
|
|
217
|
-
if (name === 'expanded')
|
|
231
|
+
if (name === 'expanded') {
|
|
232
|
+
this._state.setExpanded(this.hasAttribute('expanded'));
|
|
233
|
+
// A refusal (disabled row) changes nothing, so no subscriber would take the
|
|
234
|
+
// attribute back down: the state is the truth, the attribute its reflection.
|
|
235
|
+
this._reflectExpanded(this._state.expanded);
|
|
236
|
+
}
|
|
218
237
|
this._render();
|
|
219
238
|
}
|
|
220
239
|
|
|
240
|
+
/** The `enable-expansion` attribute as a flag: on unless it says `false`. */
|
|
241
|
+
private _enableAttribute(): boolean {
|
|
242
|
+
return this.getAttribute('enable-expansion') !== 'false';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private _reflectExpanded(expanded: boolean): void {
|
|
246
|
+
if (this.hasAttribute('expanded') === expanded) return;
|
|
247
|
+
if (expanded) this.setAttribute('expanded', '');
|
|
248
|
+
else this.removeAttribute('expanded');
|
|
249
|
+
}
|
|
250
|
+
|
|
221
251
|
private _render() {
|
|
222
252
|
// The `string_is_not_empty` label rule, from core — it hides the TITLE too, not
|
|
223
253
|
// just the subtitle.
|
|
@@ -234,7 +264,7 @@ export class AdwExpanderRow extends TooltipElement {
|
|
|
234
264
|
this._enableSwitch.hidden = !showSwitch;
|
|
235
265
|
// OUR write, not the user's — see `_reflectingEnable`.
|
|
236
266
|
this._reflectingEnable = true;
|
|
237
|
-
this._enableSwitch.active = this.enableExpansion;
|
|
267
|
+
this._enableSwitch.active = this._state.enableExpansion;
|
|
238
268
|
this._reflectingEnable = false;
|
|
239
269
|
|
|
240
270
|
this.classList.toggle('expanded', this.expanded);
|