@gjsify/adwaita-web 0.64.1-next.240 → 0.64.1-next.243
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 +42 -4
- package/dist/adwaita-web.css +12 -1
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/canvas2d/canvas2d-bridge.d.ts +18 -0
- package/lib/types/canvas2d-bridge.spec.d.ts +1 -0
- package/lib/types/elements/adw-action-row.d.ts +3 -0
- package/lib/types/elements/adw-bottom-sheet.d.ts +3 -0
- package/lib/types/elements/adw-navigation-view.d.ts +7 -0
- package/lib/types/elements/adw-switch-row.d.ts +3 -0
- package/lib/types/elements/adw-window-features.d.ts +6 -1
- package/lib/types/elements/gtk-application-window.d.ts +2 -0
- package/lib/types/elements/gtk-image.d.ts +12 -3
- package/lib/types/elements/gtk-scrolled-window.d.ts +10 -0
- package/lib/types/elements/gtk-window.d.ts +29 -3
- package/lib/types/gobject-elements.d.ts +7 -0
- package/lib/types/icon-registry.d.ts +3 -1
- package/lib/types/icon-theme.d.ts +53 -0
- package/lib/types/icon-theme.spec.d.ts +1 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/package.json +12 -12
- package/scss/_about_dialog.scss +2 -2
- package/scss/_icon.scss +18 -0
- package/scss/_window.scss +7 -0
- package/src/adw-action-rows.spec.ts +20 -0
- package/src/adw-alert-dialog.spec.ts +15 -0
- package/src/adw-application-window.spec.ts +2 -1
- package/src/adw-window.spec.ts +22 -3
- package/src/bottom-sheet.spec.ts +11 -0
- package/src/canvas2d/canvas2d-bridge.ts +51 -1
- package/src/canvas2d-bridge.spec.ts +47 -0
- package/src/elements/adw-about-dialog.ts +4 -2
- package/src/elements/adw-action-row.ts +4 -0
- package/src/elements/adw-alert-dialog.ts +6 -0
- package/src/elements/adw-application-window.ts +9 -2
- package/src/elements/adw-bottom-sheet.ts +4 -0
- package/src/elements/adw-navigation-view.ts +19 -0
- package/src/elements/adw-switch-row.ts +4 -0
- package/src/elements/adw-window-features.ts +19 -4
- package/src/elements/adw-window.ts +2 -2
- package/src/elements/gtk-application-window.ts +6 -0
- package/src/elements/gtk-image.ts +55 -24
- package/src/elements/gtk-scrolled-window.ts +37 -1
- package/src/elements/gtk-window.ts +82 -3
- package/src/gobject-door.spec.ts +44 -0
- package/src/gobject-elements.ts +43 -2
- package/src/gtk-scrolled-window.spec.ts +74 -0
- package/src/gtk-window.spec.ts +97 -1
- package/src/icon-registry.ts +13 -7
- package/src/icon-theme.spec.ts +182 -0
- package/src/icon-theme.ts +178 -0
- package/src/index.ts +3 -0
- package/src/navigation-view.spec.ts +26 -0
- package/src/shared-tree-builder.ts +2 -1
- package/src/styles.generated.ts +1 -1
- package/src/test.browser.mts +5 -0
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
// <gtk-image> — the
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// <gtk-image> — the icon node as an element. A symbolic icon is a CSS-masked box whose
|
|
2
|
+
// glyph comes from a generated `.adw-icon--<name>` class and whose colour is
|
|
3
|
+
// `currentColor`, so it themes with whatever contains it. A full-colour icon registered
|
|
4
|
+
// through `registerColorIcon` is the same box with the image as its background and no
|
|
5
|
+
// mask. Which of the two a name reaches is decided once, in `../icon-theme.ts`, in the
|
|
6
|
+
// order GTK's own theme tries them; the symbolic name derivation and its guard live once
|
|
7
|
+
// in `@gjsify/adwaita-core`'s {@link normalizeIconName}, the markup once here.
|
|
6
8
|
//
|
|
7
9
|
// Attributes:
|
|
8
|
-
// icon-name —
|
|
9
|
-
//
|
|
10
|
-
// id) never reaches a class — it
|
|
11
|
-
// is what GTK draws for it, rather
|
|
10
|
+
// icon-name — icon name, `-symbolic` suffix optional. A name that is neither a
|
|
11
|
+
// registered colour icon nor a single CSS token (a space, a quote, an
|
|
12
|
+
// unregistered reverse-DNS application id) never reaches a class — it
|
|
13
|
+
// draws {@link MISSING_ICON_NAME}, which is what GTK draws for it, rather
|
|
14
|
+
// than injecting markup.
|
|
12
15
|
// size — rendered edge length in px. Absent leaves the stylesheet's 16px in
|
|
13
16
|
// charge; a context that sizes its own icons through CSS
|
|
14
17
|
// (`.adw-about-dialog-icon`, the split button's 14px arrows) keeps doing
|
|
@@ -30,7 +33,9 @@
|
|
|
30
33
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the
|
|
31
34
|
// name derivation composed from @gjsify/adwaita-core.
|
|
32
35
|
|
|
33
|
-
import { normalizeIconName
|
|
36
|
+
import { normalizeIconName } from '@gjsify/adwaita-core';
|
|
37
|
+
|
|
38
|
+
import { lookupIcon, onIconThemeChanged } from '../icon-theme.js';
|
|
34
39
|
|
|
35
40
|
/**
|
|
36
41
|
* The generated mask-class prefix — the one place the `--` spelling appears. `icon-registry`
|
|
@@ -46,6 +51,13 @@ export const MASK_CLASS_PREFIX = 'adw-icon--';
|
|
|
46
51
|
*/
|
|
47
52
|
export const MISSING_ICON_NAME = 'image-missing';
|
|
48
53
|
|
|
54
|
+
/**
|
|
55
|
+
* The modifier a colour icon carries. Deliberately NOT under {@link MASK_CLASS_PREFIX}: the
|
|
56
|
+
* stale-class sweep below and `.adw-icon:not([class*='adw-icon--'])` both read that prefix
|
|
57
|
+
* as "a mask is applied", and a colour icon has none.
|
|
58
|
+
*/
|
|
59
|
+
export const COLOR_ICON_CLASS = 'adw-icon-color';
|
|
60
|
+
|
|
49
61
|
export class GtkImage extends HTMLElement {
|
|
50
62
|
static get observedAttributes() {
|
|
51
63
|
return ['icon-name', 'size'];
|
|
@@ -62,9 +74,10 @@ export class GtkImage extends HTMLElement {
|
|
|
62
74
|
}
|
|
63
75
|
|
|
64
76
|
/**
|
|
65
|
-
* The name
|
|
66
|
-
*
|
|
67
|
-
*
|
|
77
|
+
* The name the SYMBOLIC lookup works with — `''` when the icon-name is absent, empty,
|
|
78
|
+
* or not usable as one CSS token. A registered colour icon can still draw for a name
|
|
79
|
+
* that answers `''` here (`org.gnome.Builder`); whether a name draws anything but
|
|
80
|
+
* `image-missing` is `isIconAvailable`'s question.
|
|
68
81
|
*
|
|
69
82
|
* `''` does NOT mean the box draws nothing: an unusable name that was given still
|
|
70
83
|
* draws {@link MISSING_ICON_NAME}. Whether an icon was ASKED FOR is the raw
|
|
@@ -85,10 +98,18 @@ export class GtkImage extends HTMLElement {
|
|
|
85
98
|
else this.setAttribute('size', String(value));
|
|
86
99
|
}
|
|
87
100
|
|
|
101
|
+
private _unsubscribe: (() => void) | null = null;
|
|
102
|
+
|
|
88
103
|
connectedCallback() {
|
|
104
|
+
this._unsubscribe ??= onIconThemeChanged(() => this._render());
|
|
89
105
|
this._render();
|
|
90
106
|
}
|
|
91
107
|
|
|
108
|
+
disconnectedCallback() {
|
|
109
|
+
this._unsubscribe?.();
|
|
110
|
+
this._unsubscribe = null;
|
|
111
|
+
}
|
|
112
|
+
|
|
92
113
|
attributeChangedCallback() {
|
|
93
114
|
this._render();
|
|
94
115
|
}
|
|
@@ -102,20 +123,30 @@ export class GtkImage extends HTMLElement {
|
|
|
102
123
|
// would skip the entry after each hit.
|
|
103
124
|
const stale = [...this.classList].filter((existing) => existing.startsWith(MASK_CLASS_PREFIX));
|
|
104
125
|
this.classList.remove(...stale);
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
126
|
+
const found = lookupIcon(this.getAttribute('icon-name'));
|
|
127
|
+
this.classList.toggle(COLOR_ICON_CLASS, found?.kind === 'color');
|
|
128
|
+
if (found?.kind === 'color') {
|
|
129
|
+
// Drawn as-is: `_icon.scss` drops the mask and the `currentColor` fill for this
|
|
130
|
+
// modifier, so only the image paints. `url` is a data-URI or a resolved href;
|
|
131
|
+
// the quote and backslash escapes keep it one CSS string either way.
|
|
132
|
+
this.style.backgroundImage = `url("${found.url.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}")`;
|
|
133
|
+
} else {
|
|
134
|
+
this.style.removeProperty('background-image');
|
|
135
|
+
}
|
|
136
|
+
if (found?.kind === 'symbolic') {
|
|
137
|
+
this.classList.add(`${MASK_CLASS_PREFIX}${found.name}`);
|
|
138
|
+
} else if (found?.kind === 'missing') {
|
|
139
|
+
// A name was GIVEN and nothing answers it — not a colour icon, and not one CSS
|
|
140
|
+
// token (a space, a quote, an unregistered reverse-DNS application id). GTK has
|
|
141
|
+
// no such state: `gtk_icon_theme_lookup_icon` never returns NULL, it returns the
|
|
142
|
+
// always-available `image-missing` (refs/gtk/gtk/gtkicontheme.c —
|
|
143
|
+
// `gtk_icon_paintable_get_icon_name`'s own docs say so). So the same glyph is
|
|
144
|
+
// drawn here rather than an invisible 16px hole, which is what
|
|
145
|
+
// `<adw-button-row start-icon-name="a b">` reserved and showed.
|
|
115
146
|
this.classList.add(`${MASK_CLASS_PREFIX}${MISSING_ICON_NAME}`);
|
|
116
147
|
}
|
|
117
148
|
|
|
118
|
-
//
|
|
149
|
+
// An icon box has no text, so assistive tech has nothing to announce — the
|
|
119
150
|
// accessible name belongs to the control that HOSTS the icon.
|
|
120
151
|
this.setAttribute('aria-hidden', 'true');
|
|
121
152
|
|
|
@@ -394,6 +394,36 @@ export class GtkScrolledWindow extends HTMLElement {
|
|
|
394
394
|
return this._bars?.vertical ?? null;
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
+
/** `Gtk.ScrolledWindow:child` — the widget inside the viewport, or the one waiting to be adopted. */
|
|
398
|
+
get child(): Element | null {
|
|
399
|
+
return this._built ? this._viewport.firstElementChild : this.firstElementChild;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
set child(value: Element | null) {
|
|
403
|
+
this.set_child(value);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
get_child(): Element | null {
|
|
407
|
+
return this.child;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* `gtk_scrolled_window_set_child`: replace the child, which sits in the viewport. Before the
|
|
412
|
+
* first connect there is no viewport yet, so the child waits in the light DOM where
|
|
413
|
+
* `connectedCallback` adopts it the way it adopts a declared one.
|
|
414
|
+
*/
|
|
415
|
+
set_child(child: Element | null): void {
|
|
416
|
+
const current = this.child;
|
|
417
|
+
if (child === current) return;
|
|
418
|
+
if (this._built) {
|
|
419
|
+
this._viewport.replaceChildren(...(child === null ? [] : [child]));
|
|
420
|
+
} else {
|
|
421
|
+
current?.remove();
|
|
422
|
+
if (child !== null) this.appendChild(child);
|
|
423
|
+
}
|
|
424
|
+
this.dispatchEvent(new CustomEvent('notify::child', { bubbles: true, detail: { child } }));
|
|
425
|
+
}
|
|
426
|
+
|
|
397
427
|
/** The `<gtk-viewport>` the child sits in — the one GTK's `set_child` builds for itself. */
|
|
398
428
|
get viewport(): GtkViewport {
|
|
399
429
|
return this._viewport;
|
|
@@ -408,7 +438,13 @@ export class GtkScrolledWindow extends HTMLElement {
|
|
|
408
438
|
horizontal.setAttribute('orientation', 'horizontal');
|
|
409
439
|
vertical.setAttribute('orientation', 'vertical');
|
|
410
440
|
this._bars = { horizontal, vertical };
|
|
411
|
-
|
|
441
|
+
// `Gtk.ScrolledWindow:child` is a widget PROPERTY, so an authored `child: …` names the
|
|
442
|
+
// slot by that name; the bare child is the default. Both land in the viewport.
|
|
443
|
+
bindSlottedChildren(this, [{ name: 'child', into: this._viewport }, { into: this._viewport }]).install(
|
|
444
|
+
this._viewport,
|
|
445
|
+
vertical,
|
|
446
|
+
horizontal,
|
|
447
|
+
);
|
|
412
448
|
this.setAttribute('role', 'generic');
|
|
413
449
|
// A bar is a VIEW of the viewport's adjustment, so the viewport's
|
|
414
450
|
// `value-changed` is what moves the content and a bar's is what moves the
|
|
@@ -73,6 +73,13 @@
|
|
|
73
73
|
// maximize its browser window. `is_maximized()` reads it.
|
|
74
74
|
// `present()` — un-hides the frame; a page cannot raise or focus a window it does not own.
|
|
75
75
|
//
|
|
76
|
+
// THE GOBJECT DOOR (ADR 0096 § 3, ADR 0098 Amendment 3): `connect` / `disconnect` / `emit` /
|
|
77
|
+
// `notify` / `get_property` / `set_property` come from `installElementApi`, over the events in
|
|
78
|
+
// `static signals`; an undeclared signal throws. A `close-request` handler that returns true is
|
|
79
|
+
// `preventDefault()` and stops later listeners. `emit('close-request')` throws: the signal is
|
|
80
|
+
// `close()`'s, which also runs the default handler. `is_active` and `focus_widget` are PROPERTIES,
|
|
81
|
+
// as in GJS and `@girs/gtk-4.0` (GTK 4 has no `gtk_window_is_active()`); `is_active()` is gone.
|
|
82
|
+
//
|
|
76
83
|
// Events: `notify::<prop>` (CustomEvent, bubbles, detail `{ <prop>: value }`) on every real
|
|
77
84
|
// change, and `close-request` (CustomEvent, bubbles, CANCELLABLE, no detail), which is
|
|
78
85
|
// `Gtk.Window::close-request`: a listener that calls `preventDefault()` is a GTK handler that
|
|
@@ -85,6 +92,8 @@
|
|
|
85
92
|
|
|
86
93
|
import { CloseRequestState, WindowMemberState, type WindowMemberProperty } from '@gjsify/adwaita-core';
|
|
87
94
|
|
|
95
|
+
import { installElementApi } from '../gobject-elements.js';
|
|
96
|
+
import type { DispatchedSignals } from '../signals.js';
|
|
88
97
|
import { bindSlottedChildren, type AdwSlot } from '../slotted-children.js';
|
|
89
98
|
|
|
90
99
|
// Registers <gtk-window-controls>: a frame with no titlebar of its own draws the two of them
|
|
@@ -120,6 +129,64 @@ export class GtkWindow extends HTMLElement {
|
|
|
120
129
|
this._members.reportFocus(null);
|
|
121
130
|
};
|
|
122
131
|
|
|
132
|
+
/**
|
|
133
|
+
* The GObject door (ADR 0096 § 3): `connect` / `disconnect` / `emit` / `notify` / `get_property` /
|
|
134
|
+
* `set_property` over these events, the same ones every element declares in `static signals`.
|
|
135
|
+
* A `close-request` handler that returns true is `preventDefault()`.
|
|
136
|
+
*/
|
|
137
|
+
static readonly signals: DispatchedSignals = {
|
|
138
|
+
'close-request': 'close-request',
|
|
139
|
+
...Object.fromEntries(
|
|
140
|
+
[
|
|
141
|
+
'decorated',
|
|
142
|
+
'default-height',
|
|
143
|
+
'default-width',
|
|
144
|
+
'deletable',
|
|
145
|
+
'focus-widget',
|
|
146
|
+
'fullscreened',
|
|
147
|
+
'hide-on-close',
|
|
148
|
+
'icon-name',
|
|
149
|
+
'is-active',
|
|
150
|
+
'maximized',
|
|
151
|
+
'modal',
|
|
152
|
+
'resizable',
|
|
153
|
+
'title',
|
|
154
|
+
'transient-for',
|
|
155
|
+
].map((name) => [`notify::${name}`, `notify::${name}`]),
|
|
156
|
+
),
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/** `GObject.Object.connect`: installed per instance by `installElementApi`; refuses an undeclared signal. */
|
|
160
|
+
declare connect: (signal: string, handler: (emitter: this, ...args: any[]) => unknown) => number;
|
|
161
|
+
/** `GObject.Object.disconnect`: throws for an id that is not connected. */
|
|
162
|
+
declare disconnect: (id: number) => void;
|
|
163
|
+
/** `GObject.Object.emit`; `close-request` is refused, call `close()`. */
|
|
164
|
+
declare emit: (signal: string, ...args: unknown[]) => void;
|
|
165
|
+
/** `GObject.Object.notify`. */
|
|
166
|
+
declare notify: (name: string) => void;
|
|
167
|
+
/** `GObject.Object.get_property`, by GTK's dashed name. */
|
|
168
|
+
declare get_property: (name: string) => unknown;
|
|
169
|
+
/** `GObject.Object.set_property`; a read-only property throws. */
|
|
170
|
+
declare set_property: (name: string, value: unknown) => void;
|
|
171
|
+
|
|
172
|
+
constructor() {
|
|
173
|
+
super();
|
|
174
|
+
installElementApi(this);
|
|
175
|
+
// `Gtk.Window::close-request` is emitted by `close()`, which also runs the default handler
|
|
176
|
+
// (hide or destroy). Emitting it bare would be a different thing under the same name.
|
|
177
|
+
const emit = (this as unknown as { emit: (signal: string, ...args: unknown[]) => void }).emit;
|
|
178
|
+
Object.defineProperty(this, 'emit', {
|
|
179
|
+
configurable: true,
|
|
180
|
+
writable: true,
|
|
181
|
+
value: (signal: string, ...args: unknown[]): void => {
|
|
182
|
+
if (signal === 'close-request') {
|
|
183
|
+
throw new Error("<gtk-window>.emit('close-request') is not supported; call close()");
|
|
184
|
+
}
|
|
185
|
+
emit(signal, ...args);
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
123
190
|
static get observedAttributes() {
|
|
124
191
|
return [
|
|
125
192
|
'decorated',
|
|
@@ -460,13 +527,16 @@ export class GtkWindow extends HTMLElement {
|
|
|
460
527
|
this.hidden = false;
|
|
461
528
|
}
|
|
462
529
|
|
|
463
|
-
/**
|
|
530
|
+
/**
|
|
531
|
+
* `Gtk.Window:is-active` — see the header: the document's focus, while this frame is shown.
|
|
532
|
+
* A PROPERTY, as in GJS and `@girs/gtk-4.0`: GTK 4 has no `gtk_window_is_active`, so
|
|
533
|
+
* `window.is_active` is the value and `window.is_active()` does not exist.
|
|
534
|
+
*/
|
|
464
535
|
get isActive(): boolean {
|
|
465
536
|
return this._members.active;
|
|
466
537
|
}
|
|
467
538
|
|
|
468
|
-
|
|
469
|
-
is_active(): boolean {
|
|
539
|
+
get is_active(): boolean {
|
|
470
540
|
return this._members.active;
|
|
471
541
|
}
|
|
472
542
|
|
|
@@ -475,6 +545,15 @@ export class GtkWindow extends HTMLElement {
|
|
|
475
545
|
return this.get_focus();
|
|
476
546
|
}
|
|
477
547
|
|
|
548
|
+
/** `Gtk.Window:focus-widget` under its GJS spelling; writing it is `set_focus`. */
|
|
549
|
+
get focus_widget(): Element | null {
|
|
550
|
+
return this.get_focus();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
set focus_widget(widget: Element | null) {
|
|
554
|
+
this.set_focus(widget);
|
|
555
|
+
}
|
|
556
|
+
|
|
478
557
|
/** `gtk_window_get_focus`: the element inside the frame that has focus, or `null`. */
|
|
479
558
|
get_focus(): Element | null {
|
|
480
559
|
const active = document.activeElement;
|
package/src/gobject-door.spec.ts
CHANGED
|
@@ -62,6 +62,34 @@ GObject.registerClass(
|
|
|
62
62
|
GoWebHexdump as never,
|
|
63
63
|
);
|
|
64
64
|
|
|
65
|
+
const SensitiveTemplate = tree(`template $GoWebSensitive : Adw.Bin {
|
|
66
|
+
child: Gtk.Box {
|
|
67
|
+
Gtk.Button btn { sensitive: bind template.enabled; }
|
|
68
|
+
Gtk.Switch sw { sensitive: bind template.enabled inverted; }
|
|
69
|
+
Gtk.Label lbl { sensitive: bind template.enabled; }
|
|
70
|
+
};
|
|
71
|
+
}`);
|
|
72
|
+
|
|
73
|
+
class GoWebSensitive extends Adw.Bin {
|
|
74
|
+
declare _btn: HTMLButtonElement;
|
|
75
|
+
declare _sw: HTMLElement & { disabled: boolean };
|
|
76
|
+
declare _lbl: HTMLElement;
|
|
77
|
+
declare enabled: boolean;
|
|
78
|
+
constructor(params?: Record<string, unknown>) {
|
|
79
|
+
// @ts-expect-error the element's own constructor takes none; the registered class's takes the params
|
|
80
|
+
super(params);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
GObject.registerClass(
|
|
84
|
+
{
|
|
85
|
+
GTypeName: 'GoWebSensitive',
|
|
86
|
+
Template: SensitiveTemplate,
|
|
87
|
+
InternalChildren: ['btn', 'sw', 'lbl'],
|
|
88
|
+
Properties: { enabled: GObject.ParamSpec.boolean('enabled', '', '', GObject.ParamFlags.READWRITE, true) },
|
|
89
|
+
},
|
|
90
|
+
GoWebSensitive as never,
|
|
91
|
+
);
|
|
92
|
+
|
|
65
93
|
export const GObjectDoorTest = async () => {
|
|
66
94
|
await driveGObjectVectors(subject, { describe, it, expect }, GOBJECT_VECTORS);
|
|
67
95
|
await describe('adwaita-web: GObject.registerClass end to end (ADR 0096)', async () => {
|
|
@@ -116,4 +144,20 @@ export const GObjectDoorTest = async () => {
|
|
|
116
144
|
});
|
|
117
145
|
}
|
|
118
146
|
});
|
|
147
|
+
await describe('adwaita-web: Gtk.Widget:sensitive in a template bind', async () => {
|
|
148
|
+
await it('binds sensitive to a template property, following it both ways of the flag', () => {
|
|
149
|
+
const el = new GoWebSensitive();
|
|
150
|
+
expect(el._btn.hasAttribute('disabled')).toBe(false);
|
|
151
|
+
expect(el._lbl.getAttribute('aria-disabled')).toBe(null);
|
|
152
|
+
el.enabled = false;
|
|
153
|
+
expect(el._btn.hasAttribute('disabled')).toBe(true);
|
|
154
|
+
expect((el._btn as unknown as { sensitive: boolean }).sensitive).toBe(false);
|
|
155
|
+
expect(el._lbl.getAttribute('aria-disabled')).toBe('true');
|
|
156
|
+
expect(el._sw.hasAttribute('disabled')).toBe(false);
|
|
157
|
+
el.enabled = true;
|
|
158
|
+
expect(el._btn.hasAttribute('disabled')).toBe(false);
|
|
159
|
+
expect(el._lbl.getAttribute('aria-disabled')).toBe(null);
|
|
160
|
+
expect(el._sw.hasAttribute('disabled')).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
119
163
|
};
|
package/src/gobject-elements.ts
CHANGED
|
@@ -113,7 +113,13 @@ export function listenElement(el: HTMLElement, signal: string, handler: Handler)
|
|
|
113
113
|
const listener = (domEvent: Event) => {
|
|
114
114
|
// A notification bubbles, and a parent must not hear its child's property as its own.
|
|
115
115
|
if (notification && domEvent.target !== el) return;
|
|
116
|
-
handler(el, ...(EMITTED.has(domEvent) ? ((domEvent as CustomEvent).detail as unknown[]) : []));
|
|
116
|
+
const handled = handler(el, ...(EMITTED.has(domEvent) ? ((domEvent as CustomEvent).detail as unknown[]) : []));
|
|
117
|
+
// A GTK handler of a boolean-handled signal (`close-request`) returns true to take it over,
|
|
118
|
+
// which on a cancelable DOM event is `preventDefault()`; the handlers after it do not run.
|
|
119
|
+
if (handled === true && domEvent.cancelable) {
|
|
120
|
+
domEvent.preventDefault();
|
|
121
|
+
domEvent.stopImmediatePropagation();
|
|
122
|
+
}
|
|
117
123
|
};
|
|
118
124
|
el.addEventListener(event, listener);
|
|
119
125
|
return () => el.removeEventListener(event, listener);
|
|
@@ -126,6 +132,28 @@ export function dispatchElement(el: HTMLElement, signal: string, args: readonly
|
|
|
126
132
|
el.dispatchEvent(domEvent);
|
|
127
133
|
}
|
|
128
134
|
|
|
135
|
+
/**
|
|
136
|
+
* `Gtk.Widget:sensitive` for an element that has no member of that name. The DOM spells it
|
|
137
|
+
* `disabled` on a form control and `aria-disabled` on anything else (`adw-action-row.ts` reads
|
|
138
|
+
* both), so the accessor writes the spelling the element understands. An element that already
|
|
139
|
+
* declares `sensitive` (`<adw-toggle-group>`) keeps its own.
|
|
140
|
+
*/
|
|
141
|
+
export function installSensitive(el: HTMLElement): void {
|
|
142
|
+
if ('sensitive' in el) return;
|
|
143
|
+
const observed = (el.constructor as { observedAttributes?: string[] }).observedAttributes ?? [];
|
|
144
|
+
const form = 'disabled' in el || observed.includes('disabled');
|
|
145
|
+
Object.defineProperty(el, 'sensitive', {
|
|
146
|
+
configurable: true,
|
|
147
|
+
enumerable: false,
|
|
148
|
+
get: () => (form ? !el.hasAttribute('disabled') : el.getAttribute('aria-disabled') !== 'true'),
|
|
149
|
+
set: (value: boolean) => {
|
|
150
|
+
if (form) el.toggleAttribute('disabled', !value);
|
|
151
|
+
else if (value) el.removeAttribute('aria-disabled');
|
|
152
|
+
else el.setAttribute('aria-disabled', 'true');
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
129
157
|
/** Gives `el` the instance API of a registered class, over the events it declares. */
|
|
130
158
|
export function installElementApi(el: HTMLElement): void {
|
|
131
159
|
// A registered class already has the core's four methods on its prototype.
|
|
@@ -148,13 +176,26 @@ export function installElementApi(el: HTMLElement): void {
|
|
|
148
176
|
});
|
|
149
177
|
define('emit', (signal: string, ...args: unknown[]): void => dispatchElement(el, signal, args));
|
|
150
178
|
define('notify', (name: string): void => notifyElement(el, name));
|
|
179
|
+
define('get_property', (name: string): unknown => {
|
|
180
|
+
eventOf(el, `notify::${name}`);
|
|
181
|
+
return (el as unknown as Record<string, unknown>)[propertyOf(name)];
|
|
182
|
+
});
|
|
183
|
+
define('set_property', (name: string, value: unknown): void => {
|
|
184
|
+
eventOf(el, `notify::${name}`);
|
|
185
|
+
const member = propertyOf(name);
|
|
186
|
+
const descriptor = inheritedDescriptor(el, member);
|
|
187
|
+
if (descriptor !== undefined && descriptor.get !== undefined && descriptor.set === undefined) {
|
|
188
|
+
throw new Error(`Property '${name}' of <${el.localName}> is read-only`);
|
|
189
|
+
}
|
|
190
|
+
(el as unknown as Record<string, unknown>)[member] = value;
|
|
191
|
+
});
|
|
151
192
|
// The `Gtk.Widget` CSS-class calls a template's TypeScript makes on its children.
|
|
152
193
|
define('add_css_class', (name: string): void => el.classList.add(name));
|
|
153
194
|
define('remove_css_class', (name: string): void => el.classList.remove(name));
|
|
154
195
|
define('has_css_class', (name: string): boolean => el.classList.contains(name));
|
|
155
196
|
}
|
|
156
197
|
|
|
157
|
-
const API_MEMBERS = new Set(['connect', 'disconnect', 'emit', 'notify']);
|
|
198
|
+
const API_MEMBERS = new Set(['connect', 'disconnect', 'emit', 'notify', 'get_property', 'set_property']);
|
|
158
199
|
|
|
159
200
|
/**
|
|
160
201
|
* The end of a bind the core's engine drives. An element speaks camelCase members where a `.blp`
|
|
@@ -91,6 +91,80 @@ export const GtkScrolledWindowTest = async () => {
|
|
|
91
91
|
});
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
await describe('<gtk-scrolled-window> its child, before and after connect', async () => {
|
|
95
|
+
const detached = (): { el: GtkScrolledWindow; host: HTMLElement } => {
|
|
96
|
+
const host = document.createElement('div');
|
|
97
|
+
host.style.height = '100px';
|
|
98
|
+
const el = document.createElement('gtk-scrolled-window') as GtkScrolledWindow;
|
|
99
|
+
host.appendChild(el);
|
|
100
|
+
return { el, host };
|
|
101
|
+
};
|
|
102
|
+
const label = (slot?: string): HTMLElement => {
|
|
103
|
+
const child = document.createElement('gtk-label');
|
|
104
|
+
if (slot !== undefined) child.setAttribute('slot', slot);
|
|
105
|
+
return child;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
await it('adopts a slot="child" child appended BEFORE the element connects', async () => {
|
|
109
|
+
const host = document.createElement('div');
|
|
110
|
+
const el = document.createElement('gtk-scrolled-window') as GtkScrolledWindow;
|
|
111
|
+
const child = label('child');
|
|
112
|
+
el.appendChild(child);
|
|
113
|
+
host.appendChild(el);
|
|
114
|
+
document.body.appendChild(host);
|
|
115
|
+
await settle();
|
|
116
|
+
expect(el.viewport.contains(child)).toBe(true);
|
|
117
|
+
host.remove();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await it('adopts a slot="child" child appended AFTER the element connects', async () => {
|
|
121
|
+
const { el, host } = mount();
|
|
122
|
+
const child = label('child');
|
|
123
|
+
el.appendChild(child);
|
|
124
|
+
await settle();
|
|
125
|
+
expect(el.viewport.contains(child)).toBe(true);
|
|
126
|
+
host.remove();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await it('adopts a default-slot child appended before connect', async () => {
|
|
130
|
+
const host = document.createElement('div');
|
|
131
|
+
const el = document.createElement('gtk-scrolled-window') as GtkScrolledWindow;
|
|
132
|
+
const child = label();
|
|
133
|
+
el.appendChild(child);
|
|
134
|
+
host.appendChild(el);
|
|
135
|
+
document.body.appendChild(host);
|
|
136
|
+
await settle();
|
|
137
|
+
expect(el.viewport.contains(child)).toBe(true);
|
|
138
|
+
host.remove();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await it('set_child before connect is adopted on connect', async () => {
|
|
142
|
+
const { el, host } = detached();
|
|
143
|
+
const child = label();
|
|
144
|
+
el.set_child(child);
|
|
145
|
+
expect(el.get_child()).toBe(child);
|
|
146
|
+
document.body.appendChild(host);
|
|
147
|
+
await settle();
|
|
148
|
+
expect(el.viewport.contains(child)).toBe(true);
|
|
149
|
+
expect(el.child).toBe(child);
|
|
150
|
+
host.remove();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await it('set_child after connect replaces the child inside the viewport', async () => {
|
|
154
|
+
const { el, host } = mount();
|
|
155
|
+
const first = label();
|
|
156
|
+
const second = label();
|
|
157
|
+
el.set_child(first);
|
|
158
|
+
expect(el.viewport.contains(first)).toBe(true);
|
|
159
|
+
el.set_child(second);
|
|
160
|
+
expect(el.viewport.contains(first)).toBe(false);
|
|
161
|
+
expect(el.viewport.contains(second)).toBe(true);
|
|
162
|
+
el.set_child(null);
|
|
163
|
+
expect(el.child).toBe(null);
|
|
164
|
+
host.remove();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
94
168
|
await describe('<gtk-scrolled-window> the policies', async () => {
|
|
95
169
|
await it('shows an automatic bar only while the content is longer than the window', async () => {
|
|
96
170
|
const { el, host } = mount();
|
package/src/gtk-window.spec.ts
CHANGED
|
@@ -262,6 +262,102 @@ export const GtkWindowTest = async () => {
|
|
|
262
262
|
await driveWindowVectors(webSubject, { describe, it, expect }, WINDOW_VECTORS);
|
|
263
263
|
dispose();
|
|
264
264
|
|
|
265
|
+
await describe('<gtk-window> GObject door', async () => {
|
|
266
|
+
await it('a close-request handler returning true is preventDefault', async () => {
|
|
267
|
+
const { el, host } = mount();
|
|
268
|
+
let later = 0;
|
|
269
|
+
el.connect('close-request', () => true);
|
|
270
|
+
el.addEventListener('close-request', () => void later++);
|
|
271
|
+
expect(el.close()).toBe(false);
|
|
272
|
+
expect(el.isConnected).toBe(true);
|
|
273
|
+
expect(later).toBe(0);
|
|
274
|
+
host.remove();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
await it('a handler that returns nothing lets the default run', async () => {
|
|
278
|
+
const { el, host } = mount();
|
|
279
|
+
let seen = 0;
|
|
280
|
+
el.connect('close-request', () => void seen++);
|
|
281
|
+
expect(el.close()).toBe(true);
|
|
282
|
+
expect(seen).toBe(1);
|
|
283
|
+
expect(el.isConnected).toBe(false);
|
|
284
|
+
host.remove();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
await it('connect hands the window to the handler and disconnect removes it', async () => {
|
|
288
|
+
const { el, host } = mount();
|
|
289
|
+
let seen: unknown[] = [];
|
|
290
|
+
const id = el.connect('notify::title', (emitter) => void seen.push(emitter));
|
|
291
|
+
el.title = 'a';
|
|
292
|
+
expect(seen).toStrictEqual([el]);
|
|
293
|
+
el.disconnect(id);
|
|
294
|
+
el.title = 'b';
|
|
295
|
+
expect(seen.length).toBe(1);
|
|
296
|
+
host.remove();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
await it('notify::is-active and notify::maximized are connectable', async () => {
|
|
300
|
+
const { el, host } = mount();
|
|
301
|
+
let maximized = 0;
|
|
302
|
+
el.connect('notify::is-active', () => {});
|
|
303
|
+
el.connect('notify::maximized', () => void maximized++);
|
|
304
|
+
el.maximize();
|
|
305
|
+
expect(maximized).toBe(1);
|
|
306
|
+
host.remove();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
await it('an undeclared signal is refused by name', async () => {
|
|
310
|
+
const { el, host } = mount();
|
|
311
|
+
let message = '';
|
|
312
|
+
try {
|
|
313
|
+
el.connect('map', () => {});
|
|
314
|
+
} catch (error) {
|
|
315
|
+
message = (error as Error).message;
|
|
316
|
+
}
|
|
317
|
+
expect(message.includes("'map'")).toBe(true);
|
|
318
|
+
host.remove();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await it("emit('close-request') is refused, close() is the way", async () => {
|
|
322
|
+
const { el, host } = mount();
|
|
323
|
+
let message = '';
|
|
324
|
+
try {
|
|
325
|
+
el.emit('close-request');
|
|
326
|
+
} catch (error) {
|
|
327
|
+
message = (error as Error).message;
|
|
328
|
+
}
|
|
329
|
+
expect(message.includes('close()')).toBe(true);
|
|
330
|
+
host.remove();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
await it('is_active and focus_widget are properties, as in GJS', async () => {
|
|
334
|
+
const { el, host } = mount();
|
|
335
|
+
const button = document.createElement('button');
|
|
336
|
+
el.append(button);
|
|
337
|
+
await Promise.resolve();
|
|
338
|
+
expect(typeof el.is_active).toBe('boolean');
|
|
339
|
+
expect(el.get_property('is-active')).toBe(el.is_active);
|
|
340
|
+
el.focus_widget = button;
|
|
341
|
+
expect(el.focus_widget).toBe(button);
|
|
342
|
+
expect(el.get_property('focus-widget')).toBe(button);
|
|
343
|
+
el.set_property('title', 'x');
|
|
344
|
+
expect(el.title).toBe('x');
|
|
345
|
+
host.remove();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
await it('is-active is read-only through set_property', async () => {
|
|
349
|
+
const { el, host } = mount();
|
|
350
|
+
let message = '';
|
|
351
|
+
try {
|
|
352
|
+
el.set_property('is-active', true);
|
|
353
|
+
} catch (error) {
|
|
354
|
+
message = (error as Error).message;
|
|
355
|
+
}
|
|
356
|
+
expect(message.includes('read-only')).toBe(true);
|
|
357
|
+
host.remove();
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
265
361
|
await describe('<gtk-window> state members', async () => {
|
|
266
362
|
await it('a frame that was never connected is not realized: close() does nothing', async () => {
|
|
267
363
|
const el = document.createElement('gtk-window') as GtkWindow;
|
|
@@ -278,7 +374,7 @@ export const GtkWindowTest = async () => {
|
|
|
278
374
|
const before = el.isActive;
|
|
279
375
|
expect(before).toBe(document.hasFocus());
|
|
280
376
|
el.hidden = true;
|
|
281
|
-
expect(el.is_active
|
|
377
|
+
expect(el.is_active).toBe(false);
|
|
282
378
|
el.present();
|
|
283
379
|
expect(el.isActive).toBe(document.hasFocus());
|
|
284
380
|
// Every transition above is one notification at most, and none repeats a value.
|