@gjsify/adwaita-web 0.55.0 → 0.56.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 +8 -0
- package/dist/adwaita-web.css +129 -27
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/adw-status-page.spec.d.ts +1 -0
- package/lib/types/attributes.d.ts +19 -0
- package/lib/types/elements/adw-action-row.d.ts +4 -0
- package/lib/types/elements/adw-combo-row.d.ts +4 -0
- package/lib/types/elements/adw-entry-row.d.ts +6 -0
- package/lib/types/elements/adw-expander-row.d.ts +2 -0
- package/lib/types/elements/adw-header-bar.d.ts +6 -0
- package/lib/types/elements/adw-overlay-split-view.d.ts +13 -0
- package/lib/types/elements/adw-sidebar.d.ts +3 -2
- package/lib/types/elements/adw-spin-row.d.ts +6 -0
- package/lib/types/elements/adw-switch-row.d.ts +4 -0
- package/lib/types/elements/adw-toggle-group.d.ts +17 -0
- package/lib/types/elements/gtk-scrolled-window.d.ts +10 -2
- package/lib/types/elements/gtk-window.d.ts +8 -2
- package/lib/types/index.d.ts +1 -1
- package/lib/types/row-line-clamp.d.ts +20 -0
- package/lib/types/row-tooltip.d.ts +8 -0
- package/lib/types/row-tooltip.spec.d.ts +1 -0
- package/lib/types/shared-tree-builder.d.ts +49 -19
- package/lib/types/shared-tree-mount.spec.d.ts +1 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/lib/types/widget-size.spec.d.ts +1 -0
- package/package.json +8 -8
- package/scss/_action_row.scss +39 -15
- package/scss/_button_row.scss +5 -0
- package/scss/_expander_row.scss +36 -3
- package/scss/_headerbar.scss +2 -1
- package/scss/_preferences_row.scss +9 -0
- package/scss/_row.scss +16 -0
- package/scss/_status_page.scss +1 -1
- package/scss/_toggle_group.scss +28 -0
- package/scss/_widget.scss +14 -3
- package/scss/_window.scss +13 -1
- package/src/adw-action-rows.spec.ts +255 -0
- package/src/adw-header-bar.spec.ts +18 -0
- package/src/adw-status-page.spec.ts +124 -0
- package/src/attributes.ts +52 -0
- package/src/capabilities.mjs +3 -0
- package/src/construct-vectors.spec.ts +1 -1
- package/src/elements/adw-action-row.ts +31 -2
- package/src/elements/adw-alert-dialog.ts +4 -1
- package/src/elements/adw-button-row.ts +7 -1
- package/src/elements/adw-combo-row.ts +26 -1
- package/src/elements/adw-entry-row.ts +22 -12
- package/src/elements/adw-expander-row.ts +34 -1
- package/src/elements/adw-header-bar.ts +18 -4
- package/src/elements/adw-overlay-split-view.ts +29 -6
- package/src/elements/adw-preferences-row.ts +5 -2
- package/src/elements/adw-sidebar.ts +33 -5
- package/src/elements/adw-spin-row.ts +37 -2
- package/src/elements/adw-status-page.ts +10 -2
- package/src/elements/adw-switch-row.ts +25 -1
- package/src/elements/adw-toggle-group.ts +119 -10
- package/src/elements/adw-window.ts +9 -4
- package/src/elements/gtk-scrolled-window.ts +18 -7
- package/src/elements/gtk-window.ts +9 -3
- package/src/gtk-window.spec.ts +12 -0
- package/src/index.ts +1 -1
- package/src/keyboard-operable.spec.ts +47 -11
- package/src/row-line-clamp.ts +74 -0
- package/src/row-tooltip.spec.ts +313 -0
- package/src/row-tooltip.ts +52 -0
- package/src/shared-tree-builder.ts +88 -35
- package/src/shared-tree-mount.spec.ts +130 -0
- package/src/shared-trees.spec.ts +15 -0
- package/src/styles.generated.ts +1 -1
- package/src/test.browser.mts +8 -0
- package/src/widget-size.spec.ts +117 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// <adw-status-page> compact — the documented smaller status page for a narrow surface.
|
|
2
|
+
//
|
|
3
|
+
// libadwaita ships ONE style class on this widget, `.compact`
|
|
4
|
+
// (style-classes.md "Compact Status Page"), and `_misc.scss` gives it four differences from the
|
|
5
|
+
// full page: margin 24px 12px not 36px 12px, icon 96px not 128px, 12px not 24px under the icon,
|
|
6
|
+
// `.title-2` not `.title-1`. The child keeps libadwaita's 24px spacing, of which this renderer's
|
|
7
|
+
// flex `gap` supplies 12 — hence `margin-top: 12px`.
|
|
8
|
+
//
|
|
9
|
+
// Each is asserted as a COMPUTED size on the node the rule selects, not as the class alone: the
|
|
10
|
+
// class would pass against the pre-#1826 element whenever an author wrote `class="compact"` by
|
|
11
|
+
// hand, so a class-only suite would have left the variant unmeasured.
|
|
12
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
13
|
+
|
|
14
|
+
import type { AdwStatusPage } from './elements/adw-status-page.js';
|
|
15
|
+
|
|
16
|
+
/** Mount a status page wide enough for the full variant to differ from the compact one. */
|
|
17
|
+
function mount(
|
|
18
|
+
attributes: Record<string, string> = {},
|
|
19
|
+
width = 400,
|
|
20
|
+
): {
|
|
21
|
+
el: AdwStatusPage;
|
|
22
|
+
host: HTMLElement;
|
|
23
|
+
} {
|
|
24
|
+
const host = document.createElement('div');
|
|
25
|
+
host.style.width = `${width}px`;
|
|
26
|
+
document.body.appendChild(host);
|
|
27
|
+
const el = document.createElement('adw-status-page') as AdwStatusPage;
|
|
28
|
+
el.setAttribute('icon', 'dialog-error-symbolic');
|
|
29
|
+
el.setAttribute('title', 'Could not connect');
|
|
30
|
+
el.setAttribute('description', 'Check your network connection and try again.');
|
|
31
|
+
for (const [name, value] of Object.entries(attributes)) el.setAttribute(name, value);
|
|
32
|
+
host.appendChild(el);
|
|
33
|
+
return { el, host };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Tolerance for a computed ratio: Firefox rounds a percentage-derived px size. */
|
|
37
|
+
const near = (actual: number, expected: number, tolerance = 0.005): boolean => Math.abs(actual - expected) <= tolerance;
|
|
38
|
+
|
|
39
|
+
const iconOf = (el: AdwStatusPage): HTMLElement => el.querySelector('.adw-status-page-icon') as HTMLElement;
|
|
40
|
+
const titleOf = (el: AdwStatusPage): HTMLElement => el.querySelector('.adw-status-page-title') as HTMLElement;
|
|
41
|
+
const childOf = (el: AdwStatusPage): HTMLElement => el.querySelector('.adw-status-page-child') as HTMLElement;
|
|
42
|
+
|
|
43
|
+
export const AdwStatusPageTest = async () => {
|
|
44
|
+
await describe('adw-status-page compact (style-classes.md "Compact Status Page")', async () => {
|
|
45
|
+
await it('the compact attribute puts the documented style class on the host', async () => {
|
|
46
|
+
const { el, host } = mount({ compact: '' });
|
|
47
|
+
// The selector is `adw-status-page.compact` — the HOST, not an inner node, which
|
|
48
|
+
// is why this element writes the class itself instead of leaving it to a wrapper.
|
|
49
|
+
expect(el.classList.contains('compact')).toBe(true);
|
|
50
|
+
host.remove();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await it('a full status page carries no compact class and keeps the 128px icon', async () => {
|
|
54
|
+
const { el, host } = mount();
|
|
55
|
+
expect(el.classList.contains('compact')).toBe(false);
|
|
56
|
+
expect(getComputedStyle(iconOf(el)).width).toBe('128px');
|
|
57
|
+
expect(getComputedStyle(el).paddingTop).toBe('36px');
|
|
58
|
+
host.remove();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await it('compact shrinks the icon to 96px and the margin to 24px (_misc.scss, statuspage.compact)', async () => {
|
|
62
|
+
const { el, host } = mount({ compact: '' });
|
|
63
|
+
const icon = getComputedStyle(iconOf(el));
|
|
64
|
+
expect(icon.width).toBe('96px');
|
|
65
|
+
expect(icon.height).toBe('96px');
|
|
66
|
+
expect(icon.marginBottom).toBe('12px');
|
|
67
|
+
expect(getComputedStyle(el).paddingTop).toBe('24px');
|
|
68
|
+
expect(getComputedStyle(el).paddingBottom).toBe('24px');
|
|
69
|
+
host.remove();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await it('compact sets .title-2 (136%), the full page .title-1 (181%) (_labels.scss)', async () => {
|
|
73
|
+
const compact = mount({ compact: '' });
|
|
74
|
+
const full = mount();
|
|
75
|
+
try {
|
|
76
|
+
// The RATIO, not a pixel size: `_misc.scss` picks between `.title-1` and
|
|
77
|
+
// `.title-2`, which are 181% and 136% of the SAME inherited size — the
|
|
78
|
+
// boundary reset re-roots it (`_reset.scss`, ADR 0010), and a px assertion
|
|
79
|
+
// would pin this suite to one font instead of to the ratio the class encodes.
|
|
80
|
+
const compactTitle = parseFloat(getComputedStyle(titleOf(compact.el)).fontSize);
|
|
81
|
+
const fullTitle = parseFloat(getComputedStyle(titleOf(full.el)).fontSize);
|
|
82
|
+
expect(compactTitle < fullTitle).toBe(true);
|
|
83
|
+
expect(near(compactTitle / fullTitle, 136 / 181)).toBe(true);
|
|
84
|
+
// …and the weights both `.title-1`/`.title-2` carry, so this is the
|
|
85
|
+
// heading scale rather than a body label that happens to be smaller.
|
|
86
|
+
expect(getComputedStyle(titleOf(compact.el)).fontWeight).toBe('800');
|
|
87
|
+
} finally {
|
|
88
|
+
compact.host.remove();
|
|
89
|
+
full.host.remove();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await it('compact leaves 12px to the child beyond the 12px flex gap (_misc.scss, 24px spacing)', async () => {
|
|
94
|
+
const { el, host } = mount({ compact: '' });
|
|
95
|
+
const button = document.createElement('button');
|
|
96
|
+
button.className = 'adw-button suggested-action';
|
|
97
|
+
button.textContent = 'Try Again';
|
|
98
|
+
el.appendChild(button);
|
|
99
|
+
|
|
100
|
+
expect(getComputedStyle(childOf(el)).marginTop).toBe('12px');
|
|
101
|
+
// The full page's own 24px, so the pair cannot both be the compact rule.
|
|
102
|
+
const full = mount();
|
|
103
|
+
try {
|
|
104
|
+
expect(getComputedStyle(childOf(full.el)).marginTop).toBe('24px');
|
|
105
|
+
} finally {
|
|
106
|
+
host.remove();
|
|
107
|
+
full.host.remove();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await it('a .blp\'s styles ["compact"] survives a later attribute change', async () => {
|
|
112
|
+
const { el, host } = mount({ compact: '' });
|
|
113
|
+
el.classList.add('compact');
|
|
114
|
+
el.removeAttribute('compact');
|
|
115
|
+
// `title` is the attribute every real page sets, and setting it re-renders. A
|
|
116
|
+
// `toggle` here would strip the class the shared-tree builder had put there —
|
|
117
|
+
// `shared-tree-builder.ts:296` writes a `.blp`'s style classes onto this host.
|
|
118
|
+
el.setAttribute('title', 'Still compact');
|
|
119
|
+
expect(el.classList.contains('compact')).toBe(true);
|
|
120
|
+
expect(getComputedStyle(iconOf(el)).width).toBe('96px');
|
|
121
|
+
host.remove();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// The one reading of an attribute that spells a GObject BOOLEAN property.
|
|
2
|
+
//
|
|
3
|
+
// An HTML boolean attribute can only spell TRUE by PRESENCE, and many GTK properties
|
|
4
|
+
// default to TRUE — `GtkWidget:sensitive`, `AdwToggle:enabled`, `GtkEditable:editable`.
|
|
5
|
+
// Reading `sensitive` by presence would leave every `<adw-toggle-group>` on a page
|
|
6
|
+
// insensitive. So a TRUE-default property is read by VALUE (`sensitive="false"` is the
|
|
7
|
+
// only way to say it), and the DEFAULT is passed in at the call site rather than assumed.
|
|
8
|
+
// The default varies by widget and property: `show-apply-button` defaults FALSE,
|
|
9
|
+
// `AdwBanner:revealed` defaults FALSE, but `GtkActionBar:revealed` defaults TRUE.
|
|
10
|
+
// The per-widget map in `@gjsify/adwaita-core/tags` ({@link isValueBasedBooleanAttr})
|
|
11
|
+
// records which (widget, property) pairs are value-based.
|
|
12
|
+
//
|
|
13
|
+
// TWO CATEGORIES, NOT THREE CONVENTIONS (ADR 0049, ADR 0034):
|
|
14
|
+
// - STYLE CLASS → presence-based: `flat`, `round`, `compact` — the attribute's PRESENCE
|
|
15
|
+
// toggles the CSS class. `booleanAttribute` is NOT used for these.
|
|
16
|
+
// - GTK PROPERTY → value-based per (widget, property): the attribute's VALUE
|
|
17
|
+
// (`"false"` vs anything else) sets the property. `booleanAttribute` IS used for these.
|
|
18
|
+
// A declarative `false` MUST write `attr="false"`, not remove the attribute.
|
|
19
|
+
//
|
|
20
|
+
// It was a private helper in `elements/adw-entry-row.ts` until a second caller needed it.
|
|
21
|
+
|
|
22
|
+
import { VALUE_BASED_BOOLEAN_ATTRS } from '@gjsify/adwaita-core/tags';
|
|
23
|
+
|
|
24
|
+
/** Re-export for consumers that need the list (e.g. `shared-tree-builder.ts`). */
|
|
25
|
+
export { VALUE_BASED_BOOLEAN_ATTRS };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether an attribute spells a boolean property, defaulting to `whenAbsent`.
|
|
29
|
+
*
|
|
30
|
+
* Absent is the property's default; present, the literal `"false"` is FALSE and anything
|
|
31
|
+
* else is TRUE.
|
|
32
|
+
*/
|
|
33
|
+
export function booleanAttribute(value: string | null, whenAbsent: boolean): boolean {
|
|
34
|
+
if (value === null) return whenAbsent;
|
|
35
|
+
return value !== 'false';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Write a value-based boolean attribute (GTK property defaulting to TRUE).
|
|
40
|
+
*
|
|
41
|
+
* `true` → sets `attr="true"` (present, not "false", reads as true)
|
|
42
|
+
* `false` → sets `attr="false"` (present, reads as false)
|
|
43
|
+
* This avoids `toggleAttribute` which removes the attribute for `false`,
|
|
44
|
+
* causing value-based readers to fall back to the default (true).
|
|
45
|
+
*/
|
|
46
|
+
export function writeBooleanAttribute(el: HTMLElement, attr: string, value: boolean): void {
|
|
47
|
+
if (value) {
|
|
48
|
+
el.setAttribute(attr, 'true');
|
|
49
|
+
} else {
|
|
50
|
+
el.setAttribute(attr, 'false');
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/capabilities.mjs
CHANGED
|
@@ -23,7 +23,7 @@ customElements.define('corpus-extern', CorpusExtern);
|
|
|
23
23
|
registerTemplateClass(EXTERN_VECTOR_CLASS, 'corpus-extern');
|
|
24
24
|
|
|
25
25
|
function observe(vector: ConstructVector): unknown {
|
|
26
|
-
if (vector.kind === 'layout') {
|
|
26
|
+
if (vector.kind === 'layout' || vector.kind === 'sibling-object') {
|
|
27
27
|
buildSharedTree(vector.tree);
|
|
28
28
|
return undefined;
|
|
29
29
|
}
|
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
// in `@gjsify/adwaita-core` (ADR 0004) as {@link ActionRowState}; this element keeps
|
|
8
8
|
// only the DOM render half.
|
|
9
9
|
//
|
|
10
|
+
// ATTRIBUTES:
|
|
11
|
+
// title, subtitle — the text column
|
|
12
|
+
// activatable — whether the row activates on click (boolean presence)
|
|
13
|
+
// tooltip-text — `Gtk.Widget:tooltip-text` (`src/row-tooltip.ts` carries why it lands
|
|
14
|
+
// on the row's parts and not on the host)
|
|
15
|
+
// title-lines — clamp title to N lines (0 = unlimited, default)
|
|
16
|
+
// subtitle-lines — clamp subtitle to N lines (0 = unlimited, default)
|
|
17
|
+
//
|
|
10
18
|
// Reference: refs/adwaita-web/adwaita-web/docs/widgets/actionrow.md
|
|
11
19
|
// Reference: refs/libadwaita/src/adw-action-row.c, adw-action-row.ui
|
|
12
20
|
// Reference: refs/libadwaita/src/stylesheet/widgets/_lists.scss (.row styling)
|
|
@@ -17,8 +25,10 @@
|
|
|
17
25
|
import { ActionRowState } from '@gjsify/adwaita-core';
|
|
18
26
|
|
|
19
27
|
import { bindEmptySections } from '../empty-sections.js';
|
|
28
|
+
import { applyRowTooltip } from '../row-tooltip.js';
|
|
20
29
|
import { bindSlottedChildren } from '../slotted-children.js';
|
|
21
30
|
import { type AdwRowActivation, attachRowActivation } from './row-activation.js';
|
|
31
|
+
import { applyRowLineClamp, parseRowLinesAttribute } from '../row-line-clamp.js';
|
|
22
32
|
|
|
23
33
|
/**
|
|
24
34
|
* Attributes whose change means the activatable widget's sensitivity may have
|
|
@@ -38,6 +48,8 @@ export class AdwActionRow extends HTMLElement {
|
|
|
38
48
|
private _subtitleEl!: HTMLSpanElement;
|
|
39
49
|
private _suffixEl!: HTMLDivElement;
|
|
40
50
|
private _prefixEl!: HTMLDivElement;
|
|
51
|
+
/** The label column — kept for `tooltip-text`, which the browser reads off a PART. */
|
|
52
|
+
private _textEl!: HTMLDivElement;
|
|
41
53
|
/** The headless labels + activatable-widget coupling (ADR 0004). */
|
|
42
54
|
private readonly _state = new ActionRowState<HTMLElement>();
|
|
43
55
|
/** Watches the activatable widget's `disabled`/`aria-disabled` — the live binding. */
|
|
@@ -47,7 +59,7 @@ export class AdwActionRow extends HTMLElement {
|
|
|
47
59
|
private _activation?: AdwRowActivation;
|
|
48
60
|
|
|
49
61
|
static get observedAttributes() {
|
|
50
|
-
return ['title', 'subtitle', 'activatable'];
|
|
62
|
+
return ['title', 'subtitle', 'activatable', 'tooltip-text', 'title-lines', 'subtitle-lines'];
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
/** The end (suffix) section — append controls here imperatively. */
|
|
@@ -95,7 +107,11 @@ export class AdwActionRow extends HTMLElement {
|
|
|
95
107
|
this._prefixEl.className = 'adw-action-row-prefix';
|
|
96
108
|
|
|
97
109
|
const textEl = document.createElement('div');
|
|
98
|
-
|
|
110
|
+
// `.adw-row-text`, the ONE spelling every row type's text column carries —
|
|
111
|
+
// switch, combo, spin and expander all build it under that name, and so do the
|
|
112
|
+
// shared `.adw-row-clamp` rules in `_action_row.scss`/`_expander_row.scss`.
|
|
113
|
+
textEl.className = 'adw-row-text';
|
|
114
|
+
this._textEl = textEl;
|
|
99
115
|
this._titleEl = document.createElement('span');
|
|
100
116
|
this._titleEl.className = 'adw-row-title';
|
|
101
117
|
this._subtitleEl = document.createElement('span');
|
|
@@ -133,6 +149,7 @@ export class AdwActionRow extends HTMLElement {
|
|
|
133
149
|
this._state.setTitle(this.getAttribute('title'));
|
|
134
150
|
this._state.setSubtitle(this.getAttribute('subtitle'));
|
|
135
151
|
this._state.setActivatable(this.hasAttribute('activatable'));
|
|
152
|
+
this._applyLineClamping();
|
|
136
153
|
}
|
|
137
154
|
|
|
138
155
|
disconnectedCallback() {
|
|
@@ -194,8 +211,20 @@ export class AdwActionRow extends HTMLElement {
|
|
|
194
211
|
this._subtitleEl.textContent = subtitle;
|
|
195
212
|
this._subtitleEl.hidden = !subtitleVisible;
|
|
196
213
|
this.classList.toggle('activatable', this._state.activatable);
|
|
214
|
+
applyRowTooltip(this, [this._prefixEl, this._textEl, this._suffixEl]);
|
|
197
215
|
this._activation?.sync();
|
|
198
216
|
}
|
|
217
|
+
|
|
218
|
+
/** Apply CSS line-clamping based on title-lines/subtitle-lines attributes. */
|
|
219
|
+
private _applyLineClamping(): void {
|
|
220
|
+
applyRowLineClamp({
|
|
221
|
+
titleEl: this._titleEl,
|
|
222
|
+
subtitleEl: this._subtitleEl,
|
|
223
|
+
textContainerEl: this._titleEl.parentElement!,
|
|
224
|
+
getTitleLines: () => parseRowLinesAttribute(this.getAttribute('title-lines')),
|
|
225
|
+
getSubtitleLines: () => parseRowLinesAttribute(this.getAttribute('subtitle-lines')),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
199
228
|
}
|
|
200
229
|
|
|
201
230
|
customElements.define('adw-action-row', AdwActionRow);
|
|
@@ -59,6 +59,7 @@ import type { AdwResponseAppearance } from '@gjsify/adwaita-core';
|
|
|
59
59
|
import { bindEmptySections } from '../empty-sections.js';
|
|
60
60
|
import { bindSlottedChildren } from '../slotted-children.js';
|
|
61
61
|
import { AdwModalSurface } from './modal-surface.js';
|
|
62
|
+
import { booleanAttribute } from '../attributes.js';
|
|
62
63
|
|
|
63
64
|
/** Response button appearance — mirrors Adw.ResponseAppearance. */
|
|
64
65
|
export type { AdwResponseAppearance } from '@gjsify/adwaita-core';
|
|
@@ -269,7 +270,9 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
269
270
|
const appearance = (el.getAttribute('appearance') ?? 'default') as AdwResponseAppearance;
|
|
270
271
|
this.addResponse(id, (el.textContent ?? '').trim());
|
|
271
272
|
if (appearance !== 'default') this.setResponseAppearance(id, appearance);
|
|
272
|
-
|
|
273
|
+
// `enabled` is a value-based boolean attribute (GTK property defaulting to TRUE).
|
|
274
|
+
// Use the shared reader instead of a hand-rolled `=== 'false'` comparison.
|
|
275
|
+
if (!booleanAttribute(el.getAttribute('enabled'), true)) this.setResponseEnabled(id, false);
|
|
273
276
|
}
|
|
274
277
|
|
|
275
278
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
// The label/icon visibility rules are HEADLESS and live in `@gjsify/adwaita-core`
|
|
8
8
|
// (ADR 0004) as {@link ButtonRowState}; this element keeps only the DOM render half.
|
|
9
9
|
//
|
|
10
|
+
// Attributes: title, start-icon-name, end-icon-name, tooltip-text
|
|
11
|
+
// (`Gtk.Widget:tooltip-text`).
|
|
12
|
+
//
|
|
10
13
|
// THERE IS NO `activatable` OPT-OUT, and inventing one would be wrong twice over: the
|
|
11
14
|
// upstream template hardcodes `activatable=True` and the class docs say "AdwButtonRow is
|
|
12
15
|
// always activatable", while `<adw-action-row>` in this same package reads `activatable`
|
|
@@ -21,6 +24,7 @@
|
|
|
21
24
|
|
|
22
25
|
import { BUTTON_ROW_ACTIVATABLE, ButtonRowState } from '@gjsify/adwaita-core';
|
|
23
26
|
|
|
27
|
+
import { applyRowTooltip } from '../row-tooltip.js';
|
|
24
28
|
import { type GtkImage, createGtkImage } from './gtk-image.js';
|
|
25
29
|
import { attachRowActivation } from './row-activation.js';
|
|
26
30
|
|
|
@@ -34,7 +38,7 @@ export class AdwButtonRow extends HTMLElement {
|
|
|
34
38
|
private _initialized = false;
|
|
35
39
|
|
|
36
40
|
static get observedAttributes() {
|
|
37
|
-
return ['title', 'start-icon-name', 'end-icon-name'];
|
|
41
|
+
return ['title', 'start-icon-name', 'end-icon-name', 'tooltip-text'];
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
connectedCallback() {
|
|
@@ -81,6 +85,8 @@ export class AdwButtonRow extends HTMLElement {
|
|
|
81
85
|
|
|
82
86
|
// Unconditional: an AdwButtonRow has no way to not be activatable.
|
|
83
87
|
this.classList.toggle('activatable', BUTTON_ROW_ACTIVATABLE);
|
|
88
|
+
// The contents box is the whole row here, so it carries the tooltip for all of it.
|
|
89
|
+
applyRowTooltip(this, [this._contentsEl]);
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
/**
|
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
// title / subtitle — the text column.
|
|
30
30
|
// model — JSON array: `["a","b"]` or `[{"value":"a","label":"A"}]`.
|
|
31
31
|
// selected — the selected index (number).
|
|
32
|
+
// tooltip-text — `Gtk.Widget:tooltip-text` (`src/row-tooltip.ts`).
|
|
33
|
+
// title-lines — clamp title to N lines (0 = unlimited, default)
|
|
34
|
+
// subtitle-lines — clamp subtitle to N lines (0 = unlimited, default)
|
|
32
35
|
// Properties — the `<gtk-drop-down>` set minus `enableSearch` and `active`, which are that
|
|
33
36
|
// element's own popover chrome and have no counterpart on a row that opens a native
|
|
34
37
|
// <select>:
|
|
@@ -58,12 +61,16 @@
|
|
|
58
61
|
import { ComboState, deriveRowLabels, normalizeComboOptions, parseListModel } from '@gjsify/adwaita-core';
|
|
59
62
|
import type { AdwComboOption, AdwListItemsChanged, AdwListModelInput } from '@gjsify/adwaita-core';
|
|
60
63
|
|
|
64
|
+
import { applyRowTooltip } from '../row-tooltip.js';
|
|
61
65
|
import { bindSlottedChildren } from '../slotted-children.js';
|
|
62
66
|
import { stringListSlot } from '../string-list-slot.js';
|
|
67
|
+
import { applyRowLineClamp, parseRowLinesAttribute } from '../row-line-clamp.js';
|
|
63
68
|
|
|
64
69
|
export class AdwComboRow extends HTMLElement {
|
|
65
70
|
private _select!: HTMLSelectElement;
|
|
66
71
|
private _valueEl!: HTMLSpanElement;
|
|
72
|
+
/** The label column — kept for `tooltip-text`, which the browser reads off a PART. */
|
|
73
|
+
private _textEl!: HTMLDivElement;
|
|
67
74
|
private _titleEl!: HTMLSpanElement;
|
|
68
75
|
private _subtitleEl!: HTMLSpanElement;
|
|
69
76
|
/** The headless options list + selectedIndex↔selectedValue state machine (ADR 0004). */
|
|
@@ -71,7 +78,7 @@ export class AdwComboRow extends HTMLElement {
|
|
|
71
78
|
private _initialized = false;
|
|
72
79
|
|
|
73
80
|
static get observedAttributes() {
|
|
74
|
-
return ['title', 'subtitle', 'model', 'selected'];
|
|
81
|
+
return ['title', 'subtitle', 'model', 'selected', 'tooltip-text', 'title-lines', 'subtitle-lines'];
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
/** The list model (`Adw.ComboRow:model`). Setting it splices the <select> and clamps the selection. */
|
|
@@ -141,6 +148,7 @@ export class AdwComboRow extends HTMLElement {
|
|
|
141
148
|
// survive being attached, and an absent attribute must not blank it.
|
|
142
149
|
const text = document.createElement('div');
|
|
143
150
|
text.className = 'adw-row-text';
|
|
151
|
+
this._textEl = text;
|
|
144
152
|
this._titleEl = document.createElement('span');
|
|
145
153
|
this._titleEl.className = 'adw-row-title';
|
|
146
154
|
this._subtitleEl = document.createElement('span');
|
|
@@ -283,6 +291,23 @@ export class AdwComboRow extends HTMLElement {
|
|
|
283
291
|
this._titleEl.hidden = !labels.titleVisible;
|
|
284
292
|
this._subtitleEl.textContent = labels.subtitle;
|
|
285
293
|
this._subtitleEl.hidden = !labels.subtitleVisible;
|
|
294
|
+
// The <select> is stretched over the whole row (`position: absolute; inset: 0`,
|
|
295
|
+
// `_combo_row.scss`), so it is what the pointer actually lands on — the label column
|
|
296
|
+
// has `pointer-events: none`. A tooltip on the label parts alone would therefore
|
|
297
|
+
// never be reached over the title half of the row.
|
|
298
|
+
applyRowTooltip(this, [this._textEl, this._valueEl, this._select]);
|
|
299
|
+
this._applyLineClamping();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Apply CSS line-clamping based on title-lines/subtitle-lines attributes. */
|
|
303
|
+
private _applyLineClamping(): void {
|
|
304
|
+
applyRowLineClamp({
|
|
305
|
+
titleEl: this._titleEl,
|
|
306
|
+
subtitleEl: this._subtitleEl,
|
|
307
|
+
textContainerEl: this._titleEl.parentElement!,
|
|
308
|
+
getTitleLines: () => parseRowLinesAttribute(this.getAttribute('title-lines')),
|
|
309
|
+
getSubtitleLines: () => parseRowLinesAttribute(this.getAttribute('subtitle-lines')),
|
|
310
|
+
});
|
|
286
311
|
}
|
|
287
312
|
}
|
|
288
313
|
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
// held to one table (`@gjsify/adwaita-core/conformance`).
|
|
11
11
|
//
|
|
12
12
|
// Attributes: title, text, editable, max-length, show-apply-button,
|
|
13
|
-
// activates-default. `text-length` is
|
|
14
|
-
// `Adw.EntryRow:text-length`).
|
|
13
|
+
// activates-default, tooltip-text (`Gtk.Widget:tooltip-text`). `text-length` is
|
|
14
|
+
// REFLECTED (read-only, mirrors `Adw.EntryRow:text-length`).
|
|
15
15
|
// Properties: text, editable, maxLength, textLength, showApplyButton,
|
|
16
16
|
// activatesDefault, editing.
|
|
17
17
|
// Events:
|
|
@@ -37,6 +37,8 @@ import {
|
|
|
37
37
|
type EntryRowRenderState,
|
|
38
38
|
} from '@gjsify/adwaita-core';
|
|
39
39
|
|
|
40
|
+
import { booleanAttribute } from '../attributes.js';
|
|
41
|
+
import { applyRowTooltip } from '../row-tooltip.js';
|
|
40
42
|
import { bindSlottedChildren, type AdwSlottedChildren } from '../slotted-children.js';
|
|
41
43
|
import { type GtkImage, createGtkImage } from './gtk-image.js';
|
|
42
44
|
|
|
@@ -51,11 +53,6 @@ function setPartVisible(part: HTMLElement, visible: boolean): void {
|
|
|
51
53
|
part.style.display = visible ? '' : 'none';
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
/** Read a boolean attribute the HTML way, with an explicit `="false"` opt-out. */
|
|
55
|
-
function boolAttribute(value: string | null): boolean {
|
|
56
|
-
return value !== null && value !== 'false';
|
|
57
|
-
}
|
|
58
|
-
|
|
59
56
|
export class AdwEntryRow extends HTMLElement {
|
|
60
57
|
/** The headless `update_empty` derivation + apply latch (ADR 0004). */
|
|
61
58
|
protected readonly _state = new EntryRowState();
|
|
@@ -81,7 +78,7 @@ export class AdwEntryRow extends HTMLElement {
|
|
|
81
78
|
private _lastLength = 0;
|
|
82
79
|
|
|
83
80
|
static get observedAttributes(): string[] {
|
|
84
|
-
return ['title', 'text', 'editable', 'max-length', 'show-apply-button', 'activates-default'];
|
|
81
|
+
return ['title', 'text', 'editable', 'max-length', 'show-apply-button', 'activates-default', 'tooltip-text'];
|
|
85
82
|
}
|
|
86
83
|
|
|
87
84
|
/**
|
|
@@ -158,8 +155,8 @@ export class AdwEntryRow extends HTMLElement {
|
|
|
158
155
|
// before the element was connected survives.
|
|
159
156
|
this._state.setMaxLength(Number.parseInt(this.getAttribute('max-length') ?? '', 10));
|
|
160
157
|
this._state.setEditable(this.getAttribute('editable') !== 'false');
|
|
161
|
-
this._state.setShowApplyButton(
|
|
162
|
-
this._state.setActivatesDefault(
|
|
158
|
+
this._state.setShowApplyButton(booleanAttribute(this.getAttribute('show-apply-button'), false));
|
|
159
|
+
this._state.setActivatesDefault(booleanAttribute(this.getAttribute('activates-default'), false));
|
|
163
160
|
if (this.hasAttribute('text')) this._state.setText(this.getAttribute('text') ?? '');
|
|
164
161
|
this._renderTitle();
|
|
165
162
|
|
|
@@ -181,6 +178,7 @@ export class AdwEntryRow extends HTMLElement {
|
|
|
181
178
|
this._lastText = this._state.text;
|
|
182
179
|
this._lastLength = this._state.textLength;
|
|
183
180
|
this._render(this._state.state);
|
|
181
|
+
applyRowTooltip(this, this._tooltipParts());
|
|
184
182
|
this._state.subscribe((state) => this._onStateChanged(state));
|
|
185
183
|
}
|
|
186
184
|
|
|
@@ -200,14 +198,26 @@ export class AdwEntryRow extends HTMLElement {
|
|
|
200
198
|
this._state.setMaxLength(Number.parseInt(value ?? '', 10));
|
|
201
199
|
break;
|
|
202
200
|
case 'show-apply-button':
|
|
203
|
-
this._state.setShowApplyButton(
|
|
201
|
+
this._state.setShowApplyButton(booleanAttribute(value, false));
|
|
204
202
|
break;
|
|
205
203
|
case 'activates-default':
|
|
206
|
-
this._state.setActivatesDefault(
|
|
204
|
+
this._state.setActivatesDefault(booleanAttribute(value, false));
|
|
205
|
+
break;
|
|
206
|
+
case 'tooltip-text':
|
|
207
|
+
applyRowTooltip(this, this._tooltipParts());
|
|
207
208
|
break;
|
|
208
209
|
}
|
|
209
210
|
}
|
|
210
211
|
|
|
212
|
+
/**
|
|
213
|
+
* The parts `tooltip-text` lands on: the two slots and the editable area — NOT the
|
|
214
|
+
* apply button or the indicator, which carry tooltips of their own that the consumer
|
|
215
|
+
* reads for what they are.
|
|
216
|
+
*/
|
|
217
|
+
private _tooltipParts(): readonly Element[] {
|
|
218
|
+
return [this._prefixes, this._area, this._suffixes];
|
|
219
|
+
}
|
|
220
|
+
|
|
211
221
|
/**
|
|
212
222
|
* Add a widget after the editable area — `adw_entry_row_add_suffix`, which APPENDS
|
|
213
223
|
* into a box, so several suffixes coexist and the password row's own peek toggle
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
// <adw-expander-row> — a boxed-list row that discloses nested rows when expanded,
|
|
2
2
|
// with an optional enable switch. Mirrors Adw.ExpanderRow.
|
|
3
3
|
//
|
|
4
|
+
// ATTRIBUTES: `title`, `subtitle`, `expanded`, `enable-expansion`, `show-enable-switch`,
|
|
5
|
+
// and `title-lines` / `subtitle-lines` — clamp either label to N lines, `0` = unlimited,
|
|
6
|
+
// the AdwActionRow pspec's sentinel (`@girs/adw-1`, min 0, default 0).
|
|
7
|
+
// `tooltip-text` (`Gtk.Widget:tooltip-text`) — on the HEADER row alone, which is the
|
|
8
|
+
// part that is the row libadwaita draws.
|
|
9
|
+
//
|
|
4
10
|
// Slots: `slot="prefix"` / `slot="suffix"` children sit in the HEADER row beside the
|
|
5
11
|
// title (before the enable switch and disclosure chevron), like Adw.ExpanderRow's
|
|
6
12
|
// add_prefix/add_suffix; every other child is moved into the disclosed content
|
|
@@ -22,7 +28,9 @@
|
|
|
22
28
|
import { ExpanderState, deriveRowLabels } from '@gjsify/adwaita-core';
|
|
23
29
|
|
|
24
30
|
import { bindEmptySections } from '../empty-sections.js';
|
|
31
|
+
import { applyRowTooltip } from '../row-tooltip.js';
|
|
25
32
|
import { bindSlottedChildren } from '../slotted-children.js';
|
|
33
|
+
import { applyRowLineClamp, parseRowLinesAttribute } from '../row-line-clamp.js';
|
|
26
34
|
|
|
27
35
|
// SIDE-EFFECT import, deliberately separate from the type import below: it guarantees
|
|
28
36
|
// `gtk-switch` is defined before this module's `customElements.define` can upgrade a
|
|
@@ -68,7 +76,16 @@ export class AdwExpanderRow extends HTMLElement {
|
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
static get observedAttributes() {
|
|
71
|
-
return [
|
|
79
|
+
return [
|
|
80
|
+
'title',
|
|
81
|
+
'subtitle',
|
|
82
|
+
'expanded',
|
|
83
|
+
'enable-expansion',
|
|
84
|
+
'show-enable-switch',
|
|
85
|
+
'tooltip-text',
|
|
86
|
+
'title-lines',
|
|
87
|
+
'subtitle-lines',
|
|
88
|
+
];
|
|
72
89
|
}
|
|
73
90
|
|
|
74
91
|
get expanded(): boolean {
|
|
@@ -224,6 +241,22 @@ export class AdwExpanderRow extends HTMLElement {
|
|
|
224
241
|
// step with the flag; `aria-expanded` is the same statement to the same reader.
|
|
225
242
|
this._headerEl.setAttribute('aria-expanded', String(this.expanded));
|
|
226
243
|
this._contentEl.classList.toggle('expanded', this.expanded);
|
|
244
|
+
// The HEADER only, not the disclosure: libadwaita's tooltip belongs to the row, and
|
|
245
|
+
// the content box is where the disclosed rows live — a child row's own tooltip is
|
|
246
|
+
// what the browser should offer there.
|
|
247
|
+
applyRowTooltip(this, [this._headerEl]);
|
|
248
|
+
this._applyLineClamping();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Apply CSS line-clamping based on title-lines/subtitle-lines attributes. */
|
|
252
|
+
private _applyLineClamping(): void {
|
|
253
|
+
applyRowLineClamp({
|
|
254
|
+
titleEl: this._titleEl,
|
|
255
|
+
subtitleEl: this._subtitleEl,
|
|
256
|
+
textContainerEl: this._titleEl.parentElement!,
|
|
257
|
+
getTitleLines: () => parseRowLinesAttribute(this.getAttribute('title-lines')),
|
|
258
|
+
getSubtitleLines: () => parseRowLinesAttribute(this.getAttribute('subtitle-lines')),
|
|
259
|
+
});
|
|
227
260
|
}
|
|
228
261
|
}
|
|
229
262
|
|
|
@@ -53,6 +53,12 @@ export class AdwHeaderBar extends HTMLElement {
|
|
|
53
53
|
* bare `Adw.HeaderBar {}` in a `.blp` page shows "Mailboxes" with no title of its own.
|
|
54
54
|
*/
|
|
55
55
|
private _page: Element | null = null;
|
|
56
|
+
/**
|
|
57
|
+
* The window the bar sits in: `update_title`'s last stop before the application name
|
|
58
|
+
* (`gtk_window_get_title` of the root), so a bar with no title of its own, under no titled
|
|
59
|
+
* page, shows the window's.
|
|
60
|
+
*/
|
|
61
|
+
private _window: Element | null = null;
|
|
56
62
|
private _pageObserver: MutationObserver | null = null;
|
|
57
63
|
|
|
58
64
|
static get observedAttributes() {
|
|
@@ -136,6 +142,7 @@ export class AdwHeaderBar extends HTMLElement {
|
|
|
136
142
|
this._pageObserver?.disconnect();
|
|
137
143
|
this._pageObserver = null;
|
|
138
144
|
this._page = null;
|
|
145
|
+
this._window = null;
|
|
139
146
|
}
|
|
140
147
|
|
|
141
148
|
/**
|
|
@@ -146,9 +153,12 @@ export class AdwHeaderBar extends HTMLElement {
|
|
|
146
153
|
*/
|
|
147
154
|
private _followPage() {
|
|
148
155
|
this._page = this.closest('adw-navigation-page');
|
|
149
|
-
|
|
156
|
+
this._window = this.closest('adw-window, adw-application-window');
|
|
150
157
|
this._pageObserver ??= new MutationObserver(() => this._renderTitle());
|
|
151
|
-
|
|
158
|
+
for (const ancestor of [this._page, this._window]) {
|
|
159
|
+
if (ancestor !== null)
|
|
160
|
+
this._pageObserver.observe(ancestor, { attributes: true, attributeFilter: ['title'] });
|
|
161
|
+
}
|
|
152
162
|
}
|
|
153
163
|
|
|
154
164
|
/**
|
|
@@ -188,9 +198,13 @@ export class AdwHeaderBar extends HTMLElement {
|
|
|
188
198
|
// keeps "unset" distinguishable from "set to empty" on the child.
|
|
189
199
|
for (const name of ['title', 'subtitle']) {
|
|
190
200
|
// The bar's own `title` is this port's declarative divergence (see the header) and
|
|
191
|
-
// wins; without one, the page's title is the first answer `update_title` has
|
|
201
|
+
// wins; without one, the page's title is the first answer `update_title` has, then
|
|
202
|
+
// the window's.
|
|
192
203
|
const value =
|
|
193
|
-
this.getAttribute(name) ??
|
|
204
|
+
this.getAttribute(name) ??
|
|
205
|
+
(name === 'title'
|
|
206
|
+
? (this._page?.getAttribute('title') ?? this._window?.getAttribute('title') ?? null)
|
|
207
|
+
: null);
|
|
194
208
|
if (value === null) this._titleEl.removeAttribute(name);
|
|
195
209
|
else this._titleEl.setAttribute(name, value);
|
|
196
210
|
}
|