@gjsify/adwaita-web 0.64.1 → 0.64.2-next.252
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 +12 -8
- package/dist/adwaita-web.css +39 -9
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/application.d.ts +1 -1
- package/lib/types/child-slot.spec.d.ts +1 -0
- package/lib/types/container-fill.spec.d.ts +1 -0
- package/lib/types/elements/adw-alert-dialog.d.ts +77 -17
- package/lib/types/elements/adw-toast-overlay.d.ts +8 -29
- package/lib/types/elements/adw-window-features.d.ts +8 -0
- package/lib/types/elements/gtk-application-window.d.ts +1 -4
- package/lib/types/elements/gtk-emoji-chooser.d.ts +2 -0
- package/lib/types/elements/gtk-popover-menu.d.ts +2 -0
- package/lib/types/elements/gtk-popover.d.ts +5 -0
- package/lib/types/elements/gtk-stack-page.d.ts +28 -0
- package/lib/types/elements/gtk-stack.d.ts +11 -0
- package/lib/types/elements/gtk-window.d.ts +16 -1
- package/lib/types/gobject-elements.d.ts +9 -0
- package/lib/types/index.d.ts +2 -2
- package/lib/types/learn6502-window.spec.d.ts +1 -0
- package/lib/types/namespace/adw.d.ts +1 -1
- package/lib/types/namespace/gtk.d.ts +2 -0
- package/lib/types/shared-tree-builder.d.ts +31 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/lib/types/widget-visible.spec.d.ts +1 -0
- package/lib/types/window-maximize.spec.d.ts +1 -0
- package/package.json +13 -12
- package/scss/_bin.scss +6 -3
- package/scss/_overlay.scss +11 -1
- package/scss/_reset.scss +1 -1
- package/scss/_stack.scss +8 -2
- package/scss/_toast.scss +13 -3
- package/scss/_window.scss +16 -0
- package/src/adw-alert-dialog.spec.ts +71 -5
- package/src/adw-bin.spec.ts +4 -3
- package/src/adw-header-bar.spec.ts +18 -0
- package/src/adw-toast-overlay.spec.ts +68 -24
- package/src/application.spec.ts +44 -0
- package/src/application.ts +13 -18
- package/src/capabilities.mjs +1 -3
- package/src/child-slot.spec.ts +71 -0
- package/src/construct-vectors.spec.ts +51 -6
- package/src/container-fill.spec.ts +70 -0
- package/src/elements/adw-alert-dialog.ts +174 -44
- package/src/elements/adw-application-window.ts +0 -3
- package/src/elements/adw-breakpoint-bin.ts +3 -0
- package/src/elements/adw-dialog.ts +3 -1
- package/src/elements/adw-header-bar.ts +4 -0
- package/src/elements/adw-toast-overlay.ts +35 -69
- package/src/elements/adw-window-features.ts +14 -2
- package/src/elements/gtk-application-window.ts +3 -8
- package/src/elements/gtk-drag-icon.ts +4 -0
- package/src/elements/gtk-emoji-chooser.ts +5 -0
- package/src/elements/gtk-flow-box-child.ts +3 -1
- package/src/elements/gtk-graphics-offload.ts +3 -1
- package/src/elements/gtk-list-box-row.ts +3 -1
- package/src/elements/gtk-overlay.ts +5 -4
- package/src/elements/gtk-paned.ts +7 -1
- package/src/elements/gtk-popover-bin.ts +3 -0
- package/src/elements/gtk-popover-menu.ts +5 -0
- package/src/elements/gtk-popover.ts +12 -0
- package/src/elements/gtk-search-bar.ts +5 -5
- package/src/elements/gtk-stack-page.ts +111 -0
- package/src/elements/gtk-stack.ts +50 -5
- package/src/elements/gtk-tree-expander.ts +3 -0
- package/src/elements/gtk-viewport.ts +4 -0
- package/src/elements/gtk-window-handle.ts +4 -0
- package/src/elements/gtk-window.ts +55 -6
- package/src/gobject-elements.ts +28 -0
- package/src/gtk-stack.spec.ts +170 -0
- package/src/index.ts +2 -2
- package/src/keyboard-operable.spec.ts +3 -3
- package/src/learn6502-window.spec.ts +113 -0
- package/src/namespace/adw.ts +1 -1
- package/src/namespace/gtk.ts +2 -0
- package/src/shared-tree-builder.ts +105 -10
- package/src/shared-trees.spec.ts +15 -1
- package/src/slotted-children.spec.ts +24 -0
- package/src/slotted-children.ts +18 -2
- package/src/styles.generated.ts +1 -1
- package/src/test.browser.mts +10 -0
- package/src/value-lists.spec.ts +3 -3
- package/src/widget-visible.spec.ts +104 -0
- package/src/window-maximize.spec.ts +93 -0
|
@@ -92,6 +92,7 @@
|
|
|
92
92
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
93
93
|
|
|
94
94
|
import type { DispatchedSignals } from '../signals.js';
|
|
95
|
+
import { STACK_PAGE_ATTRIBUTES, pageChildOf } from './gtk-stack-page.js';
|
|
95
96
|
|
|
96
97
|
/** `GtkStackTransitionType`, spelled as the GIR's enum nicks (gtkstack.c:95-121). */
|
|
97
98
|
export type StackTransitionType =
|
|
@@ -416,7 +417,7 @@ export class GtkStack extends HTMLElement {
|
|
|
416
417
|
}
|
|
417
418
|
this._initialized = true;
|
|
418
419
|
this.setAttribute('role', 'group');
|
|
419
|
-
for (const child of this._children()) this._initChild(child);
|
|
420
|
+
for (const child of this._children()) this._initChild(this._adopt(child));
|
|
420
421
|
// `add_page` connects the stack to each page's `notify::visible`
|
|
421
422
|
// (gtkstack.c:1771), which is the browser's mutation observer.
|
|
422
423
|
this._observer = new MutationObserver((records) => this._onChildMutations(records));
|
|
@@ -531,12 +532,44 @@ export class GtkStack extends HTMLElement {
|
|
|
531
532
|
return this._children().find((child) => this._page(child).visible) ?? null;
|
|
532
533
|
}
|
|
533
534
|
|
|
535
|
+
/**
|
|
536
|
+
* A `<gtk-stack-page>` record, adopted: its child takes the record's place and its properties
|
|
537
|
+
* as the attributes this element reads, the way `<adw-view-stack>` adopts a declared page.
|
|
538
|
+
* Any other child is already the page and is handed back as it is.
|
|
539
|
+
*/
|
|
540
|
+
private _adopt(child: HTMLElement): HTMLElement {
|
|
541
|
+
if (child.localName !== 'gtk-stack-page') return child;
|
|
542
|
+
const content = pageChildOf(child);
|
|
543
|
+
if (content === null) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`<gtk-stack-page name="${child.getAttribute('name') ?? ''}"> has no child, so the stack would ` +
|
|
546
|
+
'hold a page with nothing to show.',
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
for (const name of STACK_PAGE_ATTRIBUTES) {
|
|
550
|
+
const value = child.getAttribute(name);
|
|
551
|
+
if (value !== null) content.setAttribute(name, value);
|
|
552
|
+
}
|
|
553
|
+
if (child.hasAttribute('hidden')) content.setAttribute('hidden', '');
|
|
554
|
+
content.removeAttribute('slot');
|
|
555
|
+
this.replaceChild(content, child);
|
|
556
|
+
return content;
|
|
557
|
+
}
|
|
558
|
+
|
|
534
559
|
private _initChild(child: HTMLElement): void {
|
|
535
560
|
if (this._initializedChildren.has(child)) return;
|
|
536
561
|
this._initializedChildren.add(child);
|
|
537
562
|
child.classList.add('adw-stack-page');
|
|
538
563
|
}
|
|
539
564
|
|
|
565
|
+
/**
|
|
566
|
+
* The observer watches the subtree, so an attribute record can come from anything below a
|
|
567
|
+
* page; only a direct child is a page of THIS stack (a nested stack's pages are its own).
|
|
568
|
+
*/
|
|
569
|
+
private _isPage(node: Node): boolean {
|
|
570
|
+
return node.parentNode === this;
|
|
571
|
+
}
|
|
572
|
+
|
|
540
573
|
private _onChildMutations(records: MutationRecord[]): void {
|
|
541
574
|
let added = 0;
|
|
542
575
|
let removed = 0;
|
|
@@ -544,13 +577,24 @@ export class GtkStack extends HTMLElement {
|
|
|
544
577
|
let pageUpdated = false;
|
|
545
578
|
for (const record of records) {
|
|
546
579
|
if (record.type === 'childList') {
|
|
580
|
+
// The observer watches the subtree for page attributes; a child list changing
|
|
581
|
+
// below a page (an adopted record handing over its child) is not a page change.
|
|
582
|
+
if (record.target !== this) continue;
|
|
547
583
|
for (const node of Array.from(record.removedNodes)) {
|
|
584
|
+
// A record leaving as its child takes its place was never a page here.
|
|
585
|
+
if (node instanceof HTMLElement && node.localName === 'gtk-stack-page') continue;
|
|
548
586
|
if (node === this._visible) this._visible = null;
|
|
549
587
|
if (node === this._lastVisible) this._lastVisible = null;
|
|
550
588
|
removed += 1;
|
|
551
589
|
}
|
|
552
590
|
for (const node of Array.from(record.addedNodes)) {
|
|
553
591
|
if (!(node instanceof HTMLElement)) continue;
|
|
592
|
+
// An adopted record is replaced by its child, and that replacement is the
|
|
593
|
+
// childList record the page is counted in.
|
|
594
|
+
if (node.localName === 'gtk-stack-page') {
|
|
595
|
+
if (node.parentElement === this) this._adopt(node);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
554
598
|
this._initChild(node);
|
|
555
599
|
added += 1;
|
|
556
600
|
if (position === -1) position = this._children().indexOf(node);
|
|
@@ -558,13 +602,14 @@ export class GtkStack extends HTMLElement {
|
|
|
558
602
|
continue;
|
|
559
603
|
}
|
|
560
604
|
const name = record.attributeName;
|
|
561
|
-
if (!name) continue;
|
|
605
|
+
if (!name || !this._isPage(record.target)) continue;
|
|
562
606
|
if (name === 'hidden') {
|
|
563
607
|
// `stack_child_visibility_notify_cb` (gtkstack.c:1617-1633) re-runs
|
|
564
608
|
// `update_child_visible`: hiding the visible page falls back to the first
|
|
565
609
|
// visible one (which is what the NULL means, :1437-1450), and showing one
|
|
566
|
-
// while none is visible picks it up (:3147-3149).
|
|
567
|
-
|
|
610
|
+
// while none is visible picks it up (:3147-3149). Any other change leaves
|
|
611
|
+
// the selection where it is.
|
|
612
|
+
if (!this._visible || !this._page(this._visible).visible) this._show(this._firstVisible());
|
|
568
613
|
continue;
|
|
569
614
|
}
|
|
570
615
|
if (PAGE_ATTRIBUTES.includes(name)) pageUpdated = true;
|
|
@@ -579,7 +624,7 @@ export class GtkStack extends HTMLElement {
|
|
|
579
624
|
}
|
|
580
625
|
if (pageUpdated) {
|
|
581
626
|
for (const record of records) {
|
|
582
|
-
if (record.type !== 'attributes') continue;
|
|
627
|
+
if (record.type !== 'attributes' || !this._isPage(record.target)) continue;
|
|
583
628
|
const target = record.target as HTMLElement;
|
|
584
629
|
this.dispatchEvent(new CustomEvent('page-updated', { bubbles: true, detail: { child: target } }));
|
|
585
630
|
// `gtk_stack_page_set_name` re-notifies `visible-child-name` when the
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
} from '@gjsify/adwaita-core';
|
|
60
60
|
|
|
61
61
|
import { createGtkImage } from './gtk-image.js';
|
|
62
|
+
import { bindSlottedChildren } from '../slotted-children.js';
|
|
62
63
|
|
|
63
64
|
/** `TIMEOUT_EXPAND` — how long a drag hovers before the row opens (gtktreeexpander.c:717). */
|
|
64
65
|
const TIMEOUT_EXPAND = 500;
|
|
@@ -142,6 +143,8 @@ export class GtkTreeExpander extends HTMLElement {
|
|
|
142
143
|
if (!this.hasAttribute('tabindex')) this.tabIndex = 0;
|
|
143
144
|
this._wire();
|
|
144
145
|
}
|
|
146
|
+
// The NAME only, for the reason `adw-bin.ts` gives: the child already IS the content.
|
|
147
|
+
bindSlottedChildren(this, [{ name: 'child', into: this }]);
|
|
145
148
|
this.setAttribute('role', 'button');
|
|
146
149
|
this._render();
|
|
147
150
|
}
|
|
@@ -65,6 +65,8 @@
|
|
|
65
65
|
|
|
66
66
|
import { clampAdjustmentValue, normalizeAdjustment, type AdwAdjustment } from '@gjsify/adwaita-core';
|
|
67
67
|
|
|
68
|
+
import { bindSlottedChildren } from '../slotted-children.js';
|
|
69
|
+
|
|
68
70
|
/** `viewport_set_adjustment_values`'s two fractions of the visible size. */
|
|
69
71
|
const STEP_FRACTION = 0.1;
|
|
70
72
|
const PAGE_FRACTION = 0.9;
|
|
@@ -130,6 +132,8 @@ export class GtkViewport extends HTMLElement {
|
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
connectedCallback() {
|
|
135
|
+
// The NAME only, for the reason `adw-bin.ts` gives: the child already IS the content.
|
|
136
|
+
bindSlottedChildren(this, [{ name: 'child', into: this }]);
|
|
133
137
|
this.setAttribute('role', 'generic');
|
|
134
138
|
// `scroll` does not bubble, so the platform's own is heard here, on the node that
|
|
135
139
|
// raised it, and re-announced as GTK's `GtkAdjustment::value-changed`.
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
// Copyright (c) The GTK Team. LGPLv2.1+.
|
|
47
47
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
48
48
|
|
|
49
|
+
import { bindSlottedChildren } from '../slotted-children.js';
|
|
50
|
+
|
|
49
51
|
/** `GtkTitlebarGesture`'s three gestures, as `perform_titlebar_action` decides them. */
|
|
50
52
|
export type GtkTitlebarGesture = 'double-click' | 'middle-click' | 'right-click';
|
|
51
53
|
|
|
@@ -146,6 +148,8 @@ export class GtkWindowHandle extends HTMLElement {
|
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
connectedCallback() {
|
|
151
|
+
// The NAME only, for the reason `adw-bin.ts` gives: the child already IS the content.
|
|
152
|
+
bindSlottedChildren(this, [{ name: 'child', into: this }]);
|
|
149
153
|
this.setAttribute('role', 'generic');
|
|
150
154
|
this.addEventListener('click', this._onClick);
|
|
151
155
|
this.addEventListener('pointerdown', this._onPointerDown);
|
|
@@ -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
|
-
// `
|
|
50
|
+
// `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.
|
|
@@ -69,8 +69,14 @@
|
|
|
69
69
|
// `set_focus(el)` calls `el.focus()` and refuses an element outside the frame;
|
|
70
70
|
// `set_focus(null)` blurs. `focusin`/`focusout` notify; focus that merely
|
|
71
71
|
// leaves with the window (blur) keeps its widget, as GTK keeps it.
|
|
72
|
-
// `maximized` — the FRAME's state
|
|
73
|
-
//
|
|
72
|
+
// `maximized` — the FRAME's state, not the browser window's: a page cannot maximize its
|
|
73
|
+
// browser window. Square corners, and — as in GTK, where a maximized or
|
|
74
|
+
// fullscreen window ignores its default size and fills the work area — the frame
|
|
75
|
+
// fills its CONTAINING BLOCK (the viewport for a frame in a full-height body)
|
|
76
|
+
// until it is unmaximized, which restores `default-width`/`default-height`.
|
|
77
|
+
// `fullscreened` does the same. `maximize()`, `unmaximize()`, `fullscreen()`,
|
|
78
|
+
// `unfullscreen()` and `toggle-maximized` all write the attribute. `is_maximized()`
|
|
79
|
+
// reads it.
|
|
74
80
|
// `present()` — un-hides the frame; a page cannot raise or focus a window it does not own.
|
|
75
81
|
//
|
|
76
82
|
// THE GOBJECT DOOR (ADR 0096 § 3, ADR 0098 Amendment 3): `connect` / `disconnect` / `emit` /
|
|
@@ -92,6 +98,7 @@
|
|
|
92
98
|
|
|
93
99
|
import { CloseRequestState, WindowMemberState, type WindowMemberProperty } from '@gjsify/adwaita-core';
|
|
94
100
|
|
|
101
|
+
import { defineWindowApplication, type GtkApplication } from '../application.js';
|
|
95
102
|
import { installElementApi } from '../gobject-elements.js';
|
|
96
103
|
import type { DispatchedSignals } from '../signals.js';
|
|
97
104
|
import { bindSlottedChildren, type AdwSlot } from '../slotted-children.js';
|
|
@@ -99,6 +106,7 @@ import { bindSlottedChildren, type AdwSlot } from '../slotted-children.js';
|
|
|
99
106
|
// Registers <gtk-window-controls>: a frame with no titlebar of its own draws the two of them
|
|
100
107
|
// a server-side decoration would have, so importing the window alone must define them.
|
|
101
108
|
import './gtk-window-controls.js';
|
|
109
|
+
import { fillsContainingBlock } from './adw-window-features.js';
|
|
102
110
|
import type { WindowAction } from './gtk-window-controls.js';
|
|
103
111
|
|
|
104
112
|
export class GtkWindow extends HTMLElement {
|
|
@@ -138,6 +146,7 @@ export class GtkWindow extends HTMLElement {
|
|
|
138
146
|
'close-request': 'close-request',
|
|
139
147
|
...Object.fromEntries(
|
|
140
148
|
[
|
|
149
|
+
'application',
|
|
141
150
|
'decorated',
|
|
142
151
|
'default-height',
|
|
143
152
|
'default-width',
|
|
@@ -156,6 +165,11 @@ export class GtkWindow extends HTMLElement {
|
|
|
156
165
|
),
|
|
157
166
|
};
|
|
158
167
|
|
|
168
|
+
/** `Gtk.Window:application`: setting it is `add_window` (ADR 0098), set by `defineWindowApplication`. */
|
|
169
|
+
declare application: GtkApplication | null;
|
|
170
|
+
declare get_application: () => GtkApplication | null;
|
|
171
|
+
declare set_application: (application: GtkApplication | null) => void;
|
|
172
|
+
|
|
159
173
|
/** `GObject.Object.connect`: installed per instance by `installElementApi`; refuses an undeclared signal. */
|
|
160
174
|
declare connect: (signal: string, handler: (emitter: this, ...args: any[]) => unknown) => number;
|
|
161
175
|
/** `GObject.Object.disconnect`: throws for an id that is not connected. */
|
|
@@ -239,6 +253,15 @@ export class GtkWindow extends HTMLElement {
|
|
|
239
253
|
this._write('maximized', String(value));
|
|
240
254
|
}
|
|
241
255
|
|
|
256
|
+
/** `Gtk.Window:fullscreened` — square corners, and the frame fills its containing block. */
|
|
257
|
+
get fullscreened(): boolean {
|
|
258
|
+
return this._offByDefault('fullscreened');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
set fullscreened(value: boolean) {
|
|
262
|
+
this._write('fullscreened', String(value));
|
|
263
|
+
}
|
|
264
|
+
|
|
242
265
|
/** `Gtk.Window:resizable` — whether the maximize button may appear. */
|
|
243
266
|
get resizable(): boolean {
|
|
244
267
|
return this._onByDefault('resizable');
|
|
@@ -300,7 +323,14 @@ export class GtkWindow extends HTMLElement {
|
|
|
300
323
|
this._content.className = 'adw-gtk-window-content';
|
|
301
324
|
|
|
302
325
|
bindSlottedChildren(this, [
|
|
303
|
-
|
|
326
|
+
// `child` only beside a titlebar: AdwWindow and AdwApplicationWindow inherit
|
|
327
|
+
// `Gtk.Window:child` and abort on it (adw-window.c:180), `content` is theirs.
|
|
328
|
+
...(this.hasTitlebar()
|
|
329
|
+
? [
|
|
330
|
+
{ name: 'titlebar', into: this._titlebar },
|
|
331
|
+
{ name: 'child', into: this._content },
|
|
332
|
+
]
|
|
333
|
+
: []),
|
|
304
334
|
...this.extraSlots(),
|
|
305
335
|
{ into: this._content },
|
|
306
336
|
]).install(...this.strips());
|
|
@@ -431,16 +461,18 @@ export class GtkWindow extends HTMLElement {
|
|
|
431
461
|
|
|
432
462
|
/**
|
|
433
463
|
* The frame's box from `default-width` / `default-height`; an absent one clears it. A subclass
|
|
434
|
-
* with a size request of its own (`Adw.Window`'s 360×200) replaces this.
|
|
464
|
+
* with a size request of its own (`Adw.Window`'s 360×200) replaces this. A maximized or
|
|
465
|
+
* fullscreened frame carries no inline size: the skin fills its containing block.
|
|
435
466
|
*/
|
|
436
467
|
protected renderFrameSize(): void {
|
|
468
|
+
const fills = fillsContainingBlock(this);
|
|
437
469
|
const width = Number.parseFloat(this.getAttribute('default-width') ?? '');
|
|
438
470
|
const height = Number.parseFloat(this.getAttribute('default-height') ?? '');
|
|
439
471
|
for (const [axis, value] of [
|
|
440
472
|
['width', width],
|
|
441
473
|
['height', height],
|
|
442
474
|
] as const) {
|
|
443
|
-
if (Number.isFinite(value) && value > 0) this.style.setProperty(axis, `${value}px`);
|
|
475
|
+
if (!fills && Number.isFinite(value) && value > 0) this.style.setProperty(axis, `${value}px`);
|
|
444
476
|
else this.style.removeProperty(axis);
|
|
445
477
|
}
|
|
446
478
|
}
|
|
@@ -598,6 +630,23 @@ export class GtkWindow extends HTMLElement {
|
|
|
598
630
|
unmaximize(): void {
|
|
599
631
|
this._write('maximized', 'false');
|
|
600
632
|
}
|
|
633
|
+
|
|
634
|
+
/** `gtk_window_is_fullscreen`: the FRAME's state, like {@link is_maximized}. */
|
|
635
|
+
is_fullscreen(): boolean {
|
|
636
|
+
return this.fullscreened;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** `gtk_window_fullscreen`; asynchronous in GTK, so the property is written directly. */
|
|
640
|
+
fullscreen(): void {
|
|
641
|
+
this._write('fullscreened', 'true');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** `gtk_window_unfullscreen`, with the same caveat as {@link fullscreen}. */
|
|
645
|
+
unfullscreen(): void {
|
|
646
|
+
this._write('fullscreened', 'false');
|
|
647
|
+
}
|
|
601
648
|
}
|
|
602
649
|
|
|
603
650
|
customElements.define('gtk-window', GtkWindow);
|
|
651
|
+
|
|
652
|
+
defineWindowApplication(GtkWindow.prototype);
|
package/src/gobject-elements.ts
CHANGED
|
@@ -154,6 +154,34 @@ export function installSensitive(el: HTMLElement): void {
|
|
|
154
154
|
});
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
const VISIBLE_INSTALLED = new WeakSet<Element>();
|
|
158
|
+
|
|
159
|
+
/** Whether `installVisible` gave `el` its `visible` (an element with its own keeps its own). */
|
|
160
|
+
export function hasInstalledVisible(el: Element): boolean {
|
|
161
|
+
return VISIBLE_INSTALLED.has(el);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* `Gtk.Widget:visible` for an element that has no member of that name: `gtk_widget_get_visible`
|
|
166
|
+
* and `gtk_widget_set_visible` over the DOM's `hidden` attribute, TRUE until hidden as GtkWidget's
|
|
167
|
+
* default is. It notifies only where the element declares `notify::visible`. An element that already declares `visible` (`<gtk-stack-page>`, `<adw-sidebar>`)
|
|
168
|
+
* keeps its own, and so does a registered class whose prototype has the port's members.
|
|
169
|
+
*/
|
|
170
|
+
export function installVisible(el: HTMLElement): void {
|
|
171
|
+
if ('visible' in el) return;
|
|
172
|
+
VISIBLE_INSTALLED.add(el);
|
|
173
|
+
const define = (name: string, descriptor: PropertyDescriptor) =>
|
|
174
|
+
Object.defineProperty(el, name, { configurable: true, enumerable: false, ...descriptor });
|
|
175
|
+
const set = (value: unknown): void => {
|
|
176
|
+
const before = !el.hidden;
|
|
177
|
+
el.toggleAttribute('hidden', !value);
|
|
178
|
+
if (!el.hidden !== before && 'notify::visible' in dispatchedSignalsOf(el)) notifyElement(el, 'visible');
|
|
179
|
+
};
|
|
180
|
+
define('visible', { get: () => !el.hidden, set });
|
|
181
|
+
if (!('get_visible' in el)) define('get_visible', { value: () => !el.hidden, writable: true });
|
|
182
|
+
if (!('set_visible' in el)) define('set_visible', { value: set, writable: true });
|
|
183
|
+
}
|
|
184
|
+
|
|
157
185
|
/** Gives `el` the instance API of a registered class, over the events it declares. */
|
|
158
186
|
export function installElementApi(el: HTMLElement): void {
|
|
159
187
|
// A registered class already has the core's four methods on its prototype.
|
package/src/gtk-stack.spec.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import { describe, expect, it } from '@gjsify/unit';
|
|
8
8
|
|
|
9
9
|
import type { GtkStack } from './elements/gtk-stack.js';
|
|
10
|
+
import { pageChildOf } from './elements/gtk-stack-page.js';
|
|
11
|
+
import { mountSharedTree } from './shared-tree-builder.js';
|
|
10
12
|
|
|
11
13
|
interface Page {
|
|
12
14
|
name: string;
|
|
@@ -144,6 +146,66 @@ export const GtkStackTest = async () => {
|
|
|
144
146
|
host.remove();
|
|
145
147
|
});
|
|
146
148
|
|
|
149
|
+
await it('a descendant toggling hidden keeps the selection', async () => {
|
|
150
|
+
// gtkstack.c:1617-1633 listens to the PAGE's own visibility, never to what is
|
|
151
|
+
// nested inside it (an `adw-view-stack` showing and hiding its own pages).
|
|
152
|
+
const { el, host } = mount([
|
|
153
|
+
{ name: 'single', title: 'Single' },
|
|
154
|
+
{ name: 'three', title: 'Three' },
|
|
155
|
+
]);
|
|
156
|
+
const nested = document.createElement('span');
|
|
157
|
+
el.children[0]!.appendChild(nested);
|
|
158
|
+
el.setVisibleChildName('three');
|
|
159
|
+
nested.setAttribute('hidden', '');
|
|
160
|
+
await settle();
|
|
161
|
+
nested.removeAttribute('hidden');
|
|
162
|
+
await settle();
|
|
163
|
+
expect(el.visibleChildName).toBe('three');
|
|
164
|
+
expect(marks(el)).toStrictEqual(['single', 'three:visible']);
|
|
165
|
+
host.remove();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
await it('hiding a page that is not visible keeps the selection', async () => {
|
|
169
|
+
const { el, host } = mount([
|
|
170
|
+
{ name: 'a', title: 'Alpha' },
|
|
171
|
+
{ name: 'b', title: 'Beta' },
|
|
172
|
+
{ name: 'c', title: 'Gamma' },
|
|
173
|
+
]);
|
|
174
|
+
el.setVisibleChildName('c');
|
|
175
|
+
(el.children[1] as HTMLElement).setAttribute('hidden', '');
|
|
176
|
+
await settle();
|
|
177
|
+
expect(el.visibleChildName).toBe('c');
|
|
178
|
+
host.remove();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await it('hiding the visible page after a selection falls back to the first visible', async () => {
|
|
182
|
+
const { el, host } = mount([
|
|
183
|
+
{ name: 'a', title: 'Alpha' },
|
|
184
|
+
{ name: 'b', title: 'Beta' },
|
|
185
|
+
{ name: 'c', title: 'Gamma' },
|
|
186
|
+
]);
|
|
187
|
+
el.setVisibleChildName('c');
|
|
188
|
+
(el.children[0] as HTMLElement).setAttribute('hidden', '');
|
|
189
|
+
(el.children[2] as HTMLElement).setAttribute('hidden', '');
|
|
190
|
+
await settle();
|
|
191
|
+
expect(el.visibleChildName).toBe('b');
|
|
192
|
+
host.remove();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await it('a descendant name change dispatches no page-updated', async () => {
|
|
196
|
+
const { el, host } = mount([{ name: 'a', title: 'Alpha' }]);
|
|
197
|
+
const nested = document.createElement('span');
|
|
198
|
+
el.children[0]!.appendChild(nested);
|
|
199
|
+
await settle();
|
|
200
|
+
const events: string[] = [];
|
|
201
|
+
el.addEventListener('page-updated', () => events.push('updated'));
|
|
202
|
+
nested.setAttribute('name', 'inner');
|
|
203
|
+
await settle();
|
|
204
|
+
expect(events).toStrictEqual([]);
|
|
205
|
+
expect(el.visibleChildName).toBe('a');
|
|
206
|
+
host.remove();
|
|
207
|
+
});
|
|
208
|
+
|
|
147
209
|
await it('re-selecting the visible page is not a change', () => {
|
|
148
210
|
// gtkstack.c:1452-1453 — no notify, no transition.
|
|
149
211
|
const { el, host } = mount([
|
|
@@ -429,4 +491,112 @@ export const GtkStackTest = async () => {
|
|
|
429
491
|
host.remove();
|
|
430
492
|
});
|
|
431
493
|
});
|
|
494
|
+
|
|
495
|
+
await describe('<gtk-stack> adopts a <gtk-stack-page> record', async () => {
|
|
496
|
+
const record = (attrs: Record<string, string>, body = 'page'): HTMLElement => {
|
|
497
|
+
const page = document.createElement('gtk-stack-page');
|
|
498
|
+
for (const [name, value] of Object.entries(attrs)) page.setAttribute(name, value);
|
|
499
|
+
const child = document.createElement('div');
|
|
500
|
+
child.setAttribute('slot', 'child');
|
|
501
|
+
child.textContent = body;
|
|
502
|
+
page.appendChild(child);
|
|
503
|
+
return page;
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
await it('puts the child where the record was, with its properties as attributes', () => {
|
|
507
|
+
const host = document.createElement('div');
|
|
508
|
+
document.body.appendChild(host);
|
|
509
|
+
const el = document.createElement('gtk-stack') as GtkStack;
|
|
510
|
+
el.appendChild(
|
|
511
|
+
record({
|
|
512
|
+
name: 'a',
|
|
513
|
+
title: 'Alpha',
|
|
514
|
+
'icon-name': 'go-home',
|
|
515
|
+
'needs-attention': '',
|
|
516
|
+
'use-underline': '',
|
|
517
|
+
}),
|
|
518
|
+
);
|
|
519
|
+
el.appendChild(record({ name: 'b' }, 'beta'));
|
|
520
|
+
host.appendChild(el);
|
|
521
|
+
expect(el.querySelector('gtk-stack-page')).toBe(null);
|
|
522
|
+
expect(marks(el)).toStrictEqual(['a:visible', 'b']);
|
|
523
|
+
const [page] = el.pages;
|
|
524
|
+
expect(page!.title).toBe('Alpha');
|
|
525
|
+
expect(page!.iconName).toBe('go-home');
|
|
526
|
+
expect(page!.needsAttention).toBe(true);
|
|
527
|
+
expect(page!.useUnderline).toBe(true);
|
|
528
|
+
expect(page!.child.hasAttribute('slot')).toBe(false);
|
|
529
|
+
expect(el.getChildByName('b')!.textContent).toBe('beta');
|
|
530
|
+
host.remove();
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
await it('reads a hidden record as GtkStackPage:visible = FALSE', () => {
|
|
534
|
+
const host = document.createElement('div');
|
|
535
|
+
document.body.appendChild(host);
|
|
536
|
+
const el = document.createElement('gtk-stack') as GtkStack;
|
|
537
|
+
const hidden = record({ name: 'a' });
|
|
538
|
+
(hidden as unknown as { visible: boolean }).visible = false;
|
|
539
|
+
el.appendChild(hidden);
|
|
540
|
+
el.appendChild(record({ name: 'b' }));
|
|
541
|
+
host.appendChild(el);
|
|
542
|
+
expect(el.pages.map((page) => page.visible)).toStrictEqual([false, true]);
|
|
543
|
+
expect(el.visibleChildName).toBe('b');
|
|
544
|
+
host.remove();
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
await it('adopts a record appended after connect, counted once', async () => {
|
|
548
|
+
const { el, host } = mount([{ name: 'a' }]);
|
|
549
|
+
const events: unknown[] = [];
|
|
550
|
+
el.addEventListener('items-changed', (e) => events.push((e as CustomEvent).detail));
|
|
551
|
+
el.appendChild(record({ name: 'b' }));
|
|
552
|
+
await settle();
|
|
553
|
+
await settle();
|
|
554
|
+
expect(marks(el)).toStrictEqual(['a:visible', 'b']);
|
|
555
|
+
expect(events).toStrictEqual([{ position: 1, removed: 0, added: 1 }]);
|
|
556
|
+
host.remove();
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
await it('takes a bare child as the page, and refuses a record with two', () => {
|
|
560
|
+
const bare = document.createElement('gtk-stack-page');
|
|
561
|
+
const child = document.createElement('div');
|
|
562
|
+
bare.appendChild(child);
|
|
563
|
+
expect(pageChildOf(bare)).toBe(child);
|
|
564
|
+
bare.appendChild(document.createElement('div'));
|
|
565
|
+
expect(() => pageChildOf(bare)).toThrow('has one `child`');
|
|
566
|
+
expect(pageChildOf(document.createElement('gtk-stack-page'))).toBe(null);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
await it('builds `Stack { StackPage { name; child: … } }` from a shared tree', () => {
|
|
570
|
+
const { root, unmount } = mountSharedTree({
|
|
571
|
+
tag: 'GtkStack',
|
|
572
|
+
id: 'layoutHost',
|
|
573
|
+
children: [
|
|
574
|
+
{
|
|
575
|
+
tag: 'GtkStackPage',
|
|
576
|
+
id: 'singlePage',
|
|
577
|
+
props: { name: 'single' },
|
|
578
|
+
children: [{ tag: 'AdwBin', slot: 'child', id: 'singleBin' }],
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
tag: 'GtkStackPage',
|
|
582
|
+
id: 'threePage',
|
|
583
|
+
props: { name: 'three', title: 'Three' },
|
|
584
|
+
children: [{ tag: 'AdwBin', slot: 'child', id: 'threeBin' }],
|
|
585
|
+
},
|
|
586
|
+
],
|
|
587
|
+
});
|
|
588
|
+
try {
|
|
589
|
+
const stack = root as GtkStack;
|
|
590
|
+
expect(stack.pages.map((page) => page.child.id)).toStrictEqual(['singleBin', 'threeBin']);
|
|
591
|
+
// The first page added is the visible one (gtkstack.c:1773-1775); a breakpoint
|
|
592
|
+
// setter selects another by the name its record carried.
|
|
593
|
+
expect(stack.visibleChild!.id).toBe('singleBin');
|
|
594
|
+
expect(stack.setVisibleChildName('three')).toBe(true);
|
|
595
|
+
expect(stack.visibleChild!.id).toBe('threeBin');
|
|
596
|
+
expect(stack.getPage(stack.visibleChild!)!.title).toBe('Three');
|
|
597
|
+
} finally {
|
|
598
|
+
unmount();
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
});
|
|
432
602
|
};
|
package/src/index.ts
CHANGED
|
@@ -80,8 +80,8 @@ export type { AdwUndershootEdges } from './scroll-shading.js';
|
|
|
80
80
|
// replaying an authored tree — a storybook fixture, a devtools probe, the shared-trees
|
|
81
81
|
// spec itself — has a route to it that is not a `.spec.ts` import; see that file's header
|
|
82
82
|
// for why `mountSharedTree` and not `buildSharedTree` alone is the instantiation half.
|
|
83
|
-
export { buildSharedTree, mountSharedTree } from './shared-tree-builder.js';
|
|
84
|
-
export type { MountOptions, MountedSharedTree, SizeSource } from './shared-tree-builder.js';
|
|
83
|
+
export { buildSharedTree, buildWithSiblings, mountSharedTree } from './shared-tree-builder.js';
|
|
84
|
+
export type { BuildOptions, BuiltTree, MountOptions, MountedSharedTree, SizeSource } from './shared-tree-builder.js';
|
|
85
85
|
export { registerTemplateClass } from './template-classes.js';
|
|
86
86
|
|
|
87
87
|
// The stylesheet compiles a chosen SUBSET of `@gjsify/adwaita-icons` (the whole set is
|
|
@@ -314,8 +314,8 @@ export const AdwKeyboardOperableTest = async () => {
|
|
|
314
314
|
'<adw-alert-response id="ok">OK</adw-alert-response>' +
|
|
315
315
|
'<adw-alert-response id="cancel">Cancel</adw-alert-response>',
|
|
316
316
|
);
|
|
317
|
-
el.
|
|
318
|
-
el.
|
|
317
|
+
el.set_default_response('ok');
|
|
318
|
+
el.set_response_enabled('ok', false);
|
|
319
319
|
opener.focus();
|
|
320
320
|
el.setAttribute('open', '');
|
|
321
321
|
|
|
@@ -344,7 +344,7 @@ export const AdwKeyboardOperableTest = async () => {
|
|
|
344
344
|
'<adw-alert-response id="ok">OK</adw-alert-response>' +
|
|
345
345
|
'<adw-alert-response id="cancel">Cancel</adw-alert-response>',
|
|
346
346
|
);
|
|
347
|
-
el.
|
|
347
|
+
el.set_default_response('ok');
|
|
348
348
|
el.setAttribute('open', '');
|
|
349
349
|
|
|
350
350
|
// `adw_alert_dialog_grab_focus` tries the CONTENT first (adw-alert-dialog.c:397)
|