@gjsify/adwaita-web 0.62.0 → 0.63.1-next.235
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 +31 -0
- package/dist/adwaita-web.css +4 -0
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/canvas2d/canvas2d-bridge.d.ts +26 -5
- package/lib/types/elements/adw-application-window.d.ts +26 -7
- package/lib/types/elements/adw-bottom-sheet.d.ts +3 -0
- package/lib/types/elements/adw-data-grid.d.ts +4 -0
- package/lib/types/elements/adw-window-features.d.ts +39 -0
- package/lib/types/elements/adw-window.d.ts +11 -24
- package/lib/types/elements/gtk-application-window.d.ts +12 -4
- package/lib/types/elements/gtk-window.d.ts +44 -0
- package/lib/types/gtk-window.spec.d.ts +6 -0
- package/lib/types/pango-markup.d.ts +10 -0
- package/lib/types/pango-markup.spec.d.ts +1 -0
- package/lib/types/source-view/copy-glyph.d.ts +6 -0
- package/lib/types/source-view/copy-glyph.spec.d.ts +1 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/package.json +13 -12
- package/scss/_window.scss +7 -2
- package/src/adw-application-window.spec.ts +8 -0
- package/src/adw-data-grid.spec.ts +11 -0
- package/src/adw-window.spec.ts +8 -0
- package/src/application.spec.ts +34 -1
- package/src/application.ts +16 -0
- package/src/bottom-sheet.spec.ts +9 -0
- package/src/canvas2d/canvas2d-bridge.ts +24 -8
- package/src/elements/adw-alert-dialog.ts +8 -5
- package/src/elements/adw-application-window.ts +90 -20
- package/src/elements/adw-banner.ts +14 -9
- package/src/elements/adw-bottom-sheet.ts +9 -0
- package/src/elements/adw-data-grid.ts +18 -0
- package/src/elements/adw-window-features.ts +128 -0
- package/src/elements/adw-window.ts +45 -108
- package/src/elements/gtk-application-window.ts +17 -6
- package/src/elements/gtk-window.ts +170 -18
- package/src/gtk-window.spec.ts +133 -0
- package/src/pango-markup.spec.ts +257 -0
- package/src/pango-markup.ts +266 -0
- package/src/source-view/adw-source-view.ts +2 -8
- package/src/source-view/copy-glyph.spec.ts +46 -0
- package/src/source-view/copy-glyph.ts +52 -0
- package/src/styles.generated.ts +1 -1
- package/src/test.browser.mts +4 -0
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
// difference a reader of the DOM can see.
|
|
48
48
|
//
|
|
49
49
|
// NOT PORTED, and why — these are the KNOWN_GAPS of this element:
|
|
50
|
-
// `application`, `child`, `default-widget`, `display
|
|
50
|
+
// `application`, `child`, `default-widget`, `display` — WIDGET- or
|
|
51
51
|
// DISPLAY-typed, so a slot or an id here rather than an attribute; `child` and
|
|
52
52
|
// `titlebar` ARE slots and `transient-for` only has to be PRESENT.
|
|
53
53
|
// `destroy-with-parent` — a DOM node has no destroy, and a page has no parent to destroy.
|
|
@@ -59,15 +59,32 @@
|
|
|
59
59
|
// business and needs the `Gio.MenuModel` it is built from (:1150).
|
|
60
60
|
// `startup-id` — write-only, and written by the launcher that started the application (:883).
|
|
61
61
|
//
|
|
62
|
+
// THE STATE MEMBERS, and what each is in a page (the shared rules — notify only on change, the
|
|
63
|
+
// close sequence — live in `@gjsify/adwaita-core`'s `window-members.ts`):
|
|
64
|
+
// `is-active` — the DOCUMENT has focus and this frame is connected and shown. A page has one
|
|
65
|
+
// focus for all its windows, so every shown frame is active together; that is
|
|
66
|
+
// the honest limit of a frame (`document.hasFocus()`, window focus/blur and
|
|
67
|
+
// `visibilitychange` feed it). Read-only, as in GTK.
|
|
68
|
+
// `focus-widget` — the element inside the frame that holds `document.activeElement`, live.
|
|
69
|
+
// `set_focus(el)` calls `el.focus()` and refuses an element outside the frame;
|
|
70
|
+
// `set_focus(null)` blurs. `focusin`/`focusout` notify; focus that merely
|
|
71
|
+
// leaves with the window (blur) keeps its widget, as GTK keeps it.
|
|
72
|
+
// `maximized` — the FRAME's state (square corners), not the browser window's: a page cannot
|
|
73
|
+
// maximize its browser window. `is_maximized()` reads it.
|
|
74
|
+
// `present()` — un-hides the frame; a page cannot raise or focus a window it does not own.
|
|
75
|
+
//
|
|
62
76
|
// Events: `notify::<prop>` (CustomEvent, bubbles, detail `{ <prop>: value }`) on every real
|
|
63
|
-
// change, and `close-request` (CustomEvent, bubbles,
|
|
64
|
-
// `Gtk.Window::close-request
|
|
77
|
+
// change, and `close-request` (CustomEvent, bubbles, CANCELLABLE, no detail), which is
|
|
78
|
+
// `Gtk.Window::close-request`: a listener that calls `preventDefault()` is a GTK handler that
|
|
79
|
+
// returns true. `window.close()` runs it through `CloseRequestState`.
|
|
65
80
|
//
|
|
66
81
|
// Reference: refs/gtk/gtk/gtkwindow.c
|
|
67
82
|
// Reference: refs/libadwaita/src/stylesheet/widgets/_window.scss (GtkWindow)
|
|
68
83
|
// Copyright (c) The GTK Team. LGPLv2.1+.
|
|
69
84
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
70
85
|
|
|
86
|
+
import { CloseRequestState, WindowMemberState, type WindowMemberProperty } from '@gjsify/adwaita-core';
|
|
87
|
+
|
|
71
88
|
import { bindSlottedChildren, type AdwSlot } from '../slotted-children.js';
|
|
72
89
|
|
|
73
90
|
// Registers <gtk-window-controls>: a frame with no titlebar of its own draws the two of them
|
|
@@ -88,6 +105,20 @@ export class GtkWindow extends HTMLElement {
|
|
|
88
105
|
private _titleLabel: HTMLElement | null = null;
|
|
89
106
|
/** Watches the strip for an author's titlebar arriving after the build. */
|
|
90
107
|
private _titlebarObserver: MutationObserver | null = null;
|
|
108
|
+
/** `is-active` and `focus-widget`; `maximized` stays the attribute above. */
|
|
109
|
+
private readonly _members = new WindowMemberState<Element>((property) => this._notifyMember(property));
|
|
110
|
+
private readonly _closing = new CloseRequestState();
|
|
111
|
+
private readonly _syncActive = (): void => void this._members.reportActive(this._documentIsActive());
|
|
112
|
+
private readonly _onFocusIn = (event: FocusEvent): void => {
|
|
113
|
+
if (event.target instanceof Element) this._members.reportFocus(event.target);
|
|
114
|
+
};
|
|
115
|
+
private readonly _onFocusOut = (event: FocusEvent): void => {
|
|
116
|
+
const next = event.relatedTarget;
|
|
117
|
+
if (next instanceof Node && this.contains(next)) return;
|
|
118
|
+
// Focus leaving WITH the window (tab switch, another app) keeps the widget; GTK does too.
|
|
119
|
+
if (next === null && !document.hasFocus()) return;
|
|
120
|
+
this._members.reportFocus(null);
|
|
121
|
+
};
|
|
91
122
|
|
|
92
123
|
static get observedAttributes() {
|
|
93
124
|
return [
|
|
@@ -103,6 +134,8 @@ export class GtkWindow extends HTMLElement {
|
|
|
103
134
|
'resizable',
|
|
104
135
|
'title',
|
|
105
136
|
'transient-for',
|
|
137
|
+
// Not a window property: `is-active` depends on it, so it is watched but never notified.
|
|
138
|
+
'hidden',
|
|
106
139
|
];
|
|
107
140
|
}
|
|
108
141
|
|
|
@@ -186,11 +219,13 @@ export class GtkWindow extends HTMLElement {
|
|
|
186
219
|
|
|
187
220
|
connectedCallback() {
|
|
188
221
|
if (this._initialized) {
|
|
222
|
+
this._watchActivity();
|
|
189
223
|
this._watchTitlebar();
|
|
190
224
|
this._render();
|
|
191
225
|
return;
|
|
192
226
|
}
|
|
193
227
|
this._initialized = true;
|
|
228
|
+
this._watchActivity();
|
|
194
229
|
|
|
195
230
|
this._titlebar = document.createElement('div');
|
|
196
231
|
this._titlebar.className = 'adw-gtk-window-titlebar';
|
|
@@ -198,7 +233,7 @@ export class GtkWindow extends HTMLElement {
|
|
|
198
233
|
this._content.className = 'adw-gtk-window-content';
|
|
199
234
|
|
|
200
235
|
bindSlottedChildren(this, [
|
|
201
|
-
{ name: 'titlebar', into: this._titlebar },
|
|
236
|
+
...(this.hasTitlebar() ? [{ name: 'titlebar', into: this._titlebar }] : []),
|
|
202
237
|
...this.extraSlots(),
|
|
203
238
|
{ into: this._content },
|
|
204
239
|
]).install(...this.strips());
|
|
@@ -216,6 +251,37 @@ export class GtkWindow extends HTMLElement {
|
|
|
216
251
|
disconnectedCallback() {
|
|
217
252
|
this._titlebarObserver?.disconnect();
|
|
218
253
|
this._titlebarObserver = null;
|
|
254
|
+
window.removeEventListener('focus', this._syncActive);
|
|
255
|
+
window.removeEventListener('blur', this._syncActive);
|
|
256
|
+
document.removeEventListener('visibilitychange', this._syncActive);
|
|
257
|
+
this.removeEventListener('focusin', this._onFocusIn);
|
|
258
|
+
this.removeEventListener('focusout', this._onFocusOut);
|
|
259
|
+
// An unmapped window is neither active nor holding focus.
|
|
260
|
+
this._members.reportActive(false);
|
|
261
|
+
this._members.reportFocus(null);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private _watchActivity(): void {
|
|
265
|
+
window.addEventListener('focus', this._syncActive);
|
|
266
|
+
window.addEventListener('blur', this._syncActive);
|
|
267
|
+
document.addEventListener('visibilitychange', this._syncActive);
|
|
268
|
+
this.addEventListener('focusin', this._onFocusIn);
|
|
269
|
+
this.addEventListener('focusout', this._onFocusOut);
|
|
270
|
+
this._syncActive();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private _documentIsActive(): boolean {
|
|
274
|
+
return this.isConnected && !this.hidden && document.visibilityState !== 'hidden' && document.hasFocus();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private _notifyMember(property: WindowMemberProperty): void {
|
|
278
|
+
const detail =
|
|
279
|
+
property === 'is-active'
|
|
280
|
+
? this._members.active
|
|
281
|
+
: property === 'focus-widget'
|
|
282
|
+
? this._members.focus
|
|
283
|
+
: this.maximized;
|
|
284
|
+
this.dispatchEvent(new CustomEvent(`notify::${property}`, { bubbles: true, detail: { [property]: detail } }));
|
|
219
285
|
}
|
|
220
286
|
|
|
221
287
|
/**
|
|
@@ -242,14 +308,28 @@ export class GtkWindow extends HTMLElement {
|
|
|
242
308
|
return [this._titlebar, this._content];
|
|
243
309
|
}
|
|
244
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Whether this window has a titlebar strip at all. `Adw.Window` has none (it styles nothing
|
|
313
|
+
* itself, you pair it with a header bar), so a subclass answers `false` and neither the
|
|
314
|
+
* `titlebar` slot nor the server-side decoration is built.
|
|
315
|
+
*/
|
|
316
|
+
protected hasTitlebar(): boolean {
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
|
|
245
320
|
/** The slots a subclass adds beside `titlebar`; none by default. */
|
|
246
321
|
protected extraSlots(): AdwSlot[] {
|
|
247
322
|
return [];
|
|
248
323
|
}
|
|
249
324
|
|
|
250
325
|
attributeChangedCallback(name: string) {
|
|
251
|
-
if (
|
|
252
|
-
|
|
326
|
+
if (name === 'hidden') {
|
|
327
|
+
this._syncActive();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
// Before the first connect there is nothing to draw, but the property still moved: GTK
|
|
331
|
+
// notifies a window that was never mapped, and a listener on a detached frame hears it.
|
|
332
|
+
if (this._initialized) this._render();
|
|
253
333
|
this.dispatchEvent(
|
|
254
334
|
new CustomEvent(`notify::${name}`, { bubbles: true, detail: { [name]: this._property(name) } }),
|
|
255
335
|
);
|
|
@@ -274,6 +354,19 @@ export class GtkWindow extends HTMLElement {
|
|
|
274
354
|
this.classList.toggle('maximized', this.decorated && this.maximized);
|
|
275
355
|
this.classList.toggle('fullscreen', this.decorated && this._offByDefault('fullscreened'));
|
|
276
356
|
|
|
357
|
+
this.renderFrameSize();
|
|
358
|
+
|
|
359
|
+
const title = this.getAttribute('title');
|
|
360
|
+
if (title === null) this.removeAttribute('aria-label');
|
|
361
|
+
else this.setAttribute('aria-label', title);
|
|
362
|
+
this._renderTitlebar();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* The frame's box from `default-width` / `default-height`; an absent one clears it. A subclass
|
|
367
|
+
* with a size request of its own (`Adw.Window`'s 360×200) replaces this.
|
|
368
|
+
*/
|
|
369
|
+
protected renderFrameSize(): void {
|
|
277
370
|
const width = Number.parseFloat(this.getAttribute('default-width') ?? '');
|
|
278
371
|
const height = Number.parseFloat(this.getAttribute('default-height') ?? '');
|
|
279
372
|
for (const [axis, value] of [
|
|
@@ -283,11 +376,6 @@ export class GtkWindow extends HTMLElement {
|
|
|
283
376
|
if (Number.isFinite(value) && value > 0) this.style.setProperty(axis, `${value}px`);
|
|
284
377
|
else this.style.removeProperty(axis);
|
|
285
378
|
}
|
|
286
|
-
|
|
287
|
-
const title = this.getAttribute('title');
|
|
288
|
-
if (title === null) this.removeAttribute('aria-label');
|
|
289
|
-
else this.setAttribute('aria-label', title);
|
|
290
|
-
this._renderTitlebar();
|
|
291
379
|
}
|
|
292
380
|
|
|
293
381
|
/**
|
|
@@ -313,7 +401,7 @@ export class GtkWindow extends HTMLElement {
|
|
|
313
401
|
this._titleLabel = null;
|
|
314
402
|
return;
|
|
315
403
|
}
|
|
316
|
-
if (!this.decorated) return;
|
|
404
|
+
if (!this.decorated || !this.hasTitlebar()) return;
|
|
317
405
|
|
|
318
406
|
if (this._startControls === null) {
|
|
319
407
|
this._startControls = document.createElement('gtk-window-controls');
|
|
@@ -344,14 +432,78 @@ export class GtkWindow extends HTMLElement {
|
|
|
344
432
|
* it, hide the window when `hide-on-close` is set and DESTROY it otherwise. "Destroy" is
|
|
345
433
|
* the one word this port cannot mean — a page has no unref — so the default detaches the
|
|
346
434
|
* frame, which is the same observable outcome for everything above it.
|
|
435
|
+
*
|
|
436
|
+
* A frame that is not in the page is not realized, so there is nothing to close; a listener
|
|
437
|
+
* that calls `close()` again is ignored, as GTK ignores it. Answers whether the window went
|
|
438
|
+
* away (hidden or detached), `false` when a listener took the request over or it was ignored.
|
|
347
439
|
*/
|
|
348
440
|
close(): boolean {
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
441
|
+
const outcome = this._closing.close({
|
|
442
|
+
realized: this.isConnected,
|
|
443
|
+
hideOnClose: this._offByDefault('hide-on-close'),
|
|
444
|
+
emit: () => {
|
|
445
|
+
const request = new CustomEvent('close-request', { bubbles: true, cancelable: true });
|
|
446
|
+
this.dispatchEvent(request);
|
|
447
|
+
return request.defaultPrevented;
|
|
448
|
+
},
|
|
449
|
+
hide: () => void (this.hidden = true),
|
|
450
|
+
destroy: () => this.remove(),
|
|
451
|
+
});
|
|
452
|
+
return outcome === 'hidden' || outcome === 'destroyed';
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* `gtk_window_present`: show the frame. GTK also raises and focuses the toplevel, which is
|
|
457
|
+
* the window manager's to grant; a page owns no such surface, so this is only the first half.
|
|
458
|
+
*/
|
|
459
|
+
present(): void {
|
|
460
|
+
this.hidden = false;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** `Gtk.Window:is-active` — see the header: the document's focus, while this frame is shown. */
|
|
464
|
+
get isActive(): boolean {
|
|
465
|
+
return this._members.active;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** `gtk_window_is_active`. */
|
|
469
|
+
is_active(): boolean {
|
|
470
|
+
return this._members.active;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** `Gtk.Window:focus-widget`, read live from the document. */
|
|
474
|
+
get focusWidget(): Element | null {
|
|
475
|
+
return this.get_focus();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** `gtk_window_get_focus`: the element inside the frame that has focus, or `null`. */
|
|
479
|
+
get_focus(): Element | null {
|
|
480
|
+
const active = document.activeElement;
|
|
481
|
+
return active !== null && active !== this && this.contains(active) ? active : null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* `gtk_window_set_focus`. `null` clears the focus. An element outside the frame is refused:
|
|
486
|
+
* GTK's window only focuses its own descendants.
|
|
487
|
+
*/
|
|
488
|
+
set_focus(widget: Element | null): void {
|
|
489
|
+
if (widget === null) {
|
|
490
|
+
const current = this.get_focus();
|
|
491
|
+
if (current instanceof HTMLElement || current instanceof SVGElement) current.blur();
|
|
492
|
+
this._members.reportFocus(null);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (widget === this || !this.contains(widget)) {
|
|
496
|
+
throw new Error('<gtk-window>.set_focus: the widget is not inside this window');
|
|
497
|
+
}
|
|
498
|
+
if (widget instanceof HTMLElement || widget instanceof SVGElement) widget.focus();
|
|
499
|
+
// `focus()` fires no focusin while the document itself is unfocused, so report what the
|
|
500
|
+
// document now says; an equal write is dropped by the state.
|
|
501
|
+
this._members.reportFocus(this.get_focus());
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** `gtk_window_is_maximized`: the FRAME's state, not the browser window's. */
|
|
505
|
+
is_maximized(): boolean {
|
|
506
|
+
return this.maximized;
|
|
355
507
|
}
|
|
356
508
|
|
|
357
509
|
/**
|
package/src/gtk-window.spec.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// (gtkwindow.c:1274-1287).
|
|
6
6
|
import { describe, expect, it } from '@gjsify/unit';
|
|
7
7
|
|
|
8
|
+
import { driveWindowVectors, WINDOW_VECTORS, type WindowSubject } from '@gjsify/adwaita-core/conformance';
|
|
9
|
+
|
|
8
10
|
import type { GtkWindow } from './elements/gtk-window.js';
|
|
9
11
|
|
|
10
12
|
function mount(attrs: Record<string, string> = {}): { el: GtkWindow; host: HTMLElement } {
|
|
@@ -24,6 +26,58 @@ function titlebar(el: GtkWindow): HTMLElement {
|
|
|
24
26
|
return el.querySelector('.adw-gtk-window-titlebar') as HTMLElement;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
/** The `Gtk.Window` conformance subject for any window element (`gtk-window`, `adw-window`, …). */
|
|
30
|
+
export function webWindowSubject(tag: string): { subject: WindowSubject; dispose: () => void } {
|
|
31
|
+
const windows = new Map<object, HTMLElement>();
|
|
32
|
+
const subject: WindowSubject = {
|
|
33
|
+
name: `<${tag}>`,
|
|
34
|
+
isOracle: false,
|
|
35
|
+
capabilities: { maximizable: true, observesFocus: true, hideOnClose: true },
|
|
36
|
+
create({ hideOnClose }) {
|
|
37
|
+
// Never in a page: not realized, which is GTK's state for a window before `present()`.
|
|
38
|
+
const el = document.createElement(tag) as GtkWindow;
|
|
39
|
+
if (hideOnClose) el.setAttribute('hide-on-close', 'true');
|
|
40
|
+
el.append(document.createElement('button'), document.createElement('button'));
|
|
41
|
+
return el;
|
|
42
|
+
},
|
|
43
|
+
show(w) {
|
|
44
|
+
const host = document.createElement('div');
|
|
45
|
+
document.body.appendChild(host);
|
|
46
|
+
host.appendChild(w as GtkWindow);
|
|
47
|
+
windows.set(w, host);
|
|
48
|
+
(w as GtkWindow).present();
|
|
49
|
+
},
|
|
50
|
+
connect(w, signal, handler) {
|
|
51
|
+
(w as GtkWindow).addEventListener(signal, (event) => {
|
|
52
|
+
if (signal === 'close-request') {
|
|
53
|
+
if (event.defaultPrevented) return;
|
|
54
|
+
if (handler()) {
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
event.stopImmediatePropagation();
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
handler();
|
|
61
|
+
});
|
|
62
|
+
return 1;
|
|
63
|
+
},
|
|
64
|
+
close: (w) => void (w as GtkWindow).close(),
|
|
65
|
+
maximize: (w) => (w as GtkWindow).maximize(),
|
|
66
|
+
unmaximize: (w) => (w as GtkWindow).unmaximize(),
|
|
67
|
+
isMaximized: (w) => (w as GtkWindow).is_maximized(),
|
|
68
|
+
isVisible: (w) => (w as GtkWindow).isConnected && !(w as GtkWindow).hidden,
|
|
69
|
+
focusables: (w) => [...(w as GtkWindow).querySelectorAll('button')] as unknown as [Element, Element],
|
|
70
|
+
setFocus: (w, widget) => (w as GtkWindow).set_focus(widget as Element | null),
|
|
71
|
+
getFocus: (w) => (w as GtkWindow).get_focus(),
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
subject,
|
|
75
|
+
dispose: () => {
|
|
76
|
+
for (const host of windows.values()) host.remove();
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
27
81
|
export const GtkWindowTest = async () => {
|
|
28
82
|
await describe('<gtk-window> structure', async () => {
|
|
29
83
|
await it('is a titlebar strip over a content box', async () => {
|
|
@@ -199,4 +253,83 @@ export const GtkWindowTest = async () => {
|
|
|
199
253
|
host.remove();
|
|
200
254
|
});
|
|
201
255
|
});
|
|
256
|
+
|
|
257
|
+
// The element's native close contract is `preventDefault()`; a GTK handler "returns true". The
|
|
258
|
+
// adapter below is that mapping, and it also stops later listeners, which is what the handled
|
|
259
|
+
// accumulator does. So the "third never runs" row exercises the adapter plus the element's
|
|
260
|
+
// sequence, not DOM dispatch alone.
|
|
261
|
+
const { subject: webSubject, dispose } = webWindowSubject('gtk-window');
|
|
262
|
+
await driveWindowVectors(webSubject, { describe, it, expect }, WINDOW_VECTORS);
|
|
263
|
+
dispose();
|
|
264
|
+
|
|
265
|
+
await describe('<gtk-window> state members', async () => {
|
|
266
|
+
await it('a frame that was never connected is not realized: close() does nothing', async () => {
|
|
267
|
+
const el = document.createElement('gtk-window') as GtkWindow;
|
|
268
|
+
let seen = 0;
|
|
269
|
+
el.addEventListener('close-request', () => void seen++);
|
|
270
|
+
expect(el.close()).toBe(false);
|
|
271
|
+
expect(seen).toBe(0);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
await it('is-active follows the document focus and notifies only on change', async () => {
|
|
275
|
+
const { el, host } = mount();
|
|
276
|
+
const seen: boolean[] = [];
|
|
277
|
+
el.addEventListener('notify::is-active', (e) => seen.push((e as CustomEvent).detail['is-active']));
|
|
278
|
+
const before = el.isActive;
|
|
279
|
+
expect(before).toBe(document.hasFocus());
|
|
280
|
+
el.hidden = true;
|
|
281
|
+
expect(el.is_active()).toBe(false);
|
|
282
|
+
el.present();
|
|
283
|
+
expect(el.isActive).toBe(document.hasFocus());
|
|
284
|
+
// Every transition above is one notification at most, and none repeats a value.
|
|
285
|
+
for (let i = 1; i < seen.length; i++) expect(seen[i]).not.toBe(seen[i - 1]);
|
|
286
|
+
host.remove();
|
|
287
|
+
expect(el.isActive).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
await it('is-active has no setter: GTK declares it read-only', async () => {
|
|
291
|
+
const { el, host } = mount();
|
|
292
|
+
const descriptor =
|
|
293
|
+
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el.constructor.prototype), 'isActive') ??
|
|
294
|
+
Object.getOwnPropertyDescriptor(el.constructor.prototype, 'isActive');
|
|
295
|
+
expect(descriptor?.set).toBe(undefined);
|
|
296
|
+
host.remove();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
await it('set_focus refuses an element outside the frame, by name', async () => {
|
|
300
|
+
const { el, host } = mount();
|
|
301
|
+
const outside = document.createElement('button');
|
|
302
|
+
document.body.append(outside);
|
|
303
|
+
let message = '';
|
|
304
|
+
try {
|
|
305
|
+
el.set_focus(outside);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
message = (error as Error).message;
|
|
308
|
+
}
|
|
309
|
+
expect(message.includes('set_focus')).toBe(true);
|
|
310
|
+
outside.remove();
|
|
311
|
+
host.remove();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
await it('focus-widget notifies with the widget in its detail', async () => {
|
|
315
|
+
const { el, host } = mount();
|
|
316
|
+
const button = document.createElement('button');
|
|
317
|
+
el.append(button);
|
|
318
|
+
await Promise.resolve();
|
|
319
|
+
let detail: unknown;
|
|
320
|
+
el.addEventListener('notify::focus-widget', (e) => (detail = (e as CustomEvent).detail['focus-widget']));
|
|
321
|
+
el.set_focus(button);
|
|
322
|
+
expect(detail).toBe(button);
|
|
323
|
+
expect(el.focusWidget).toBe(button);
|
|
324
|
+
host.remove();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
await it('is_maximized() reads the frame state', async () => {
|
|
328
|
+
const { el, host } = mount();
|
|
329
|
+
expect(el.is_maximized()).toBe(false);
|
|
330
|
+
el.maximize();
|
|
331
|
+
expect(el.is_maximized()).toBe(true);
|
|
332
|
+
host.remove();
|
|
333
|
+
});
|
|
334
|
+
});
|
|
202
335
|
};
|