@gjsify/adwaita-web 0.64.0 → 0.64.1-next.241

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/adwaita-web",
3
- "version": "0.64.0",
3
+ "version": "0.64.1-next.241",
4
4
  "description": "Adwaita/Libadwaita web components for browser targets",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -58,14 +58,14 @@
58
58
  "@codemirror/language": "^6.12.4",
59
59
  "@codemirror/state": "^6.7.0",
60
60
  "@codemirror/view": "^6.43.4",
61
- "@gjsify/adwaita-core": "^0.64.0",
62
- "@gjsify/adwaita-fonts": "^0.64.0",
63
- "@gjsify/adwaita-icons": "^0.64.0",
64
- "@gjsify/gtksource-core": "^0.64.0",
61
+ "@gjsify/adwaita-core": "0.64.1-next.241",
62
+ "@gjsify/adwaita-fonts": "0.64.1-next.241",
63
+ "@gjsify/adwaita-icons": "0.64.1-next.241",
64
+ "@gjsify/gtksource-core": "0.64.1-next.241",
65
65
  "@lezer/highlight": "^1.2.3"
66
66
  },
67
67
  "peerDependencies": {
68
- "@gjsify/app-settings": "^0.64.0"
68
+ "@gjsify/app-settings": "0.64.1-next.241"
69
69
  },
70
70
  "peerDependenciesMeta": {
71
71
  "@gjsify/app-settings": {
@@ -73,11 +73,11 @@
73
73
  }
74
74
  },
75
75
  "devDependencies": {
76
- "@gjsify/app-settings": "^0.64.0",
77
- "@gjsify/blueprint": "^0.64.0",
78
- "@gjsify/cli": "^0.64.0",
79
- "@gjsify/unit": "^0.64.0",
80
- "@gjsify/vite-plugin-blueprint": "^0.64.0",
76
+ "@gjsify/app-settings": "0.64.1-next.241",
77
+ "@gjsify/blueprint": "0.64.1-next.241",
78
+ "@gjsify/cli": "0.64.1-next.241",
79
+ "@gjsify/unit": "0.64.1-next.241",
80
+ "@gjsify/vite-plugin-blueprint": "0.64.1-next.241",
81
81
  "sass": "^1.101.0",
82
82
  "typescript": "^6.0.3"
83
83
  },
@@ -114,5 +114,6 @@
114
114
  "bugs": {
115
115
  "url": "https://github.com/gjsify/gjsify/issues"
116
116
  },
117
- "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/web/adwaita-web#readme"
117
+ "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/web/adwaita-web#readme",
118
+ "gitHead": "9937e8d27d18d159e35471b6669bdca6dff0cd05"
118
119
  }
package/scss/_window.scss CHANGED
@@ -26,6 +26,13 @@ adw-application-window {
26
26
  outline: 1px solid rgb(255 255 255 / 7%);
27
27
  outline-offset: -1px;
28
28
 
29
+ // No authored size (the element sets an inline one from `width`/`default-width`): a page
30
+ // has no window manager to size a toplevel, so it fills the viewport. The 360×200
31
+ // minimum (Adw.Window's size request) is the inline min-size the element sets.
32
+ width: 100%;
33
+ height: 100dvh;
34
+ box-sizing: border-box;
35
+
29
36
  // `window.maximized` / `window.fullscreen` zero the radius and drop the shadow
30
37
  // (_window.scss:66-72). A page-sized window IS the maximised one here: the frame
31
38
  // is in-page, so there is nothing outside it to cast a shadow on.
@@ -674,4 +674,24 @@ export const AdwActionRowsTest = async () => {
674
674
  host.remove();
675
675
  });
676
676
  });
677
+
678
+ await describe('declared GTK signals reach connect()', async () => {
679
+ await it('<adw-action-row> connects `activated`', () => {
680
+ const { el, host } = mount<AdwActionRow>('adw-action-row');
681
+ let seen = 0;
682
+ (el as unknown as { connect(s: string, h: () => void): number }).connect('activated', () => seen++);
683
+ el.dispatchEvent(new CustomEvent('activated'));
684
+ expect(seen).toBe(1);
685
+ host.remove();
686
+ });
687
+
688
+ await it('<adw-switch-row> connects `notify::active`', () => {
689
+ const { el, host } = mount<AdwSwitchRow>('adw-switch-row');
690
+ let seen = 0;
691
+ (el as unknown as { connect(s: string, h: () => void): number }).connect('notify::active', () => seen++);
692
+ el.setAttribute('active', '');
693
+ expect(seen).toBe(1);
694
+ host.remove();
695
+ });
696
+ });
677
697
  };
@@ -143,4 +143,19 @@ export const AdwAlertDialogTest = async () => {
143
143
  host.remove();
144
144
  });
145
145
  });
146
+
147
+ await describe('adw-alert-dialog responses before the first connect', async () => {
148
+ await it('buttons added while detached appear once connected', () => {
149
+ const el = document.createElement('adw-alert-dialog') as AdwAlertDialog;
150
+ el.addResponse('cancel', 'Cancel');
151
+ el.addResponse('ok', 'OK');
152
+ expect(el.getResponseLabel('ok')).toBe('OK');
153
+ const host = document.createElement('div');
154
+ document.body.appendChild(host);
155
+ host.appendChild(el);
156
+ const labels = [...el.querySelectorAll('.adw-alert-dialog-response')].map((b) => b.textContent);
157
+ expect(labels.join(',')).toBe('Cancel,OK');
158
+ host.remove();
159
+ });
160
+ });
146
161
  };
@@ -47,7 +47,8 @@ export const AdwApplicationWindowTest = async () => {
47
47
 
48
48
  await it('keeps the inherited size request', async () => {
49
49
  const win = makeWindow();
50
- expect(win.style.width).toBe('360px');
50
+ expect(win.style.minWidth).toBe('360px');
51
+ expect(win.style.width).toBe('');
51
52
  win.setAttribute('width', '520');
52
53
  expect(win.style.width).toBe('520px');
53
54
  win.remove();
@@ -53,13 +53,32 @@ export const AdwWindowTest = async () => {
53
53
  win.remove();
54
54
  });
55
55
 
56
- await it('applies the default 360×200 size', async () => {
56
+ await it('keeps 360×200 as a minimum and fills the viewport without an authored size', async () => {
57
57
  const win = makeWindow();
58
- expect(win.style.width).toBe('360px');
59
- expect(win.style.height).toBe('200px');
58
+ expect(win.style.minWidth).toBe('360px');
59
+ expect(win.style.minHeight).toBe('200px');
60
+ expect(win.style.width).toBe('');
61
+ expect(win.style.height).toBe('');
62
+ expect(Math.round(win.getBoundingClientRect().width)).toBe(document.documentElement.clientWidth);
63
+ expect(Math.round(win.getBoundingClientRect().height)).toBe(window.innerHeight);
60
64
  win.remove();
61
65
  });
62
66
 
67
+ await it('width-request/height-request override the minimum; gtk-window has none', async () => {
68
+ const win = document.createElement('adw-window') as AdwWindow;
69
+ win.setAttribute('width-request', '100');
70
+ win.setAttribute('height-request', '50');
71
+ document.body.appendChild(win);
72
+ expect(win.style.minWidth).toBe('100px');
73
+ expect(win.style.minHeight).toBe('50px');
74
+ win.remove();
75
+ const plain = document.createElement('gtk-window');
76
+ document.body.appendChild(plain);
77
+ expect(plain.style.minWidth).toBe('');
78
+ expect(plain.style.minHeight).toBe('');
79
+ plain.remove();
80
+ });
81
+
63
82
  await it('width/height attributes override the default size', async () => {
64
83
  const win = document.createElement('adw-window') as AdwWindow;
65
84
  win.setAttribute('width', '500');
@@ -577,4 +577,15 @@ export const AdwBottomSheetTest = async () => {
577
577
  sheet.remove();
578
578
  });
579
579
  });
580
+
581
+ await describe('<adw-bottom-sheet> declared signals', async () => {
582
+ await it('connects `notify::open`', () => {
583
+ const sheet = mountSheet();
584
+ let seen = 0;
585
+ (sheet as unknown as { connect(s: string, h: () => void): number }).connect('notify::open', () => seen++);
586
+ sheet.open = true;
587
+ expect(seen).toBe(1);
588
+ sheet.remove();
589
+ });
590
+ });
580
591
  };
@@ -27,6 +27,7 @@ import { ActionRowState } from '@gjsify/adwaita-core';
27
27
  import { bindEmptySections } from '../empty-sections.js';
28
28
  import { applyRowTooltip } from '../row-tooltip.js';
29
29
  import { bindSlottedChildren } from '../slotted-children.js';
30
+ import type { DispatchedSignals } from '../signals.js';
30
31
  import { type AdwRowActivation, attachRowActivation } from './row-activation.js';
31
32
  import { applyRowLineClamp, parseRowLinesAttribute } from '../row-line-clamp.js';
32
33
 
@@ -44,6 +45,9 @@ function isSensitive(widget: HTMLElement): boolean {
44
45
  }
45
46
 
46
47
  export class AdwActionRow extends HTMLElement {
48
+ /** The GTK signals this element dispatches, each with the DOM event it arrives as (ADR 0093). */
49
+ static readonly signals: DispatchedSignals = { activated: 'activated' };
50
+
47
51
  private _titleEl!: HTMLSpanElement;
48
52
  private _subtitleEl!: HTMLSpanElement;
49
53
  private _suffixEl!: HTMLDivElement;
@@ -222,6 +222,10 @@ export class AdwAlertDialog extends HTMLElement {
222
222
  this._responseAreaEl = document.createElement('div');
223
223
  this._responseAreaEl.className = 'adw-alert-dialog-responses';
224
224
 
225
+ // Responses added before the first connect already own a button (`addResponse` is
226
+ // callable at any point in the dialog's life); only the area to hang them in was missing.
227
+ this._responseAreaEl.append(...this._buttons.values());
228
+
225
229
  this._dialogEl.append(messageArea, this._responseAreaEl);
226
230
 
227
231
  // Two slots, both LIVE. An `<adw-alert-response>` is DATA — read for its id, label
@@ -463,6 +467,8 @@ export class AdwAlertDialog extends HTMLElement {
463
467
  }
464
468
 
465
469
  private _render(): void {
470
+ // Before the first connect there is no DOM to render into; `connectedCallback` renders.
471
+ if (!this._initialized) return;
466
472
  this.classList.toggle('open', this.open);
467
473
 
468
474
  const heading = this.heading;
@@ -57,7 +57,14 @@ export class AdwApplicationWindow extends GtkApplicationWindow {
57
57
  // resolves a `[...Base.observedAttributes]` spread across files, and reports a
58
58
  // `super` one as unreadable rather than as empty).
59
59
  static get observedAttributes(): string[] {
60
- return [...GtkApplicationWindow.observedAttributes, 'width', 'height', 'menu-model'];
60
+ return [
61
+ ...GtkApplicationWindow.observedAttributes,
62
+ 'width',
63
+ 'height',
64
+ 'width-request',
65
+ 'height-request',
66
+ 'menu-model',
67
+ ];
61
68
  }
62
69
 
63
70
  /** The content area — the Adw.Window:content property. */
@@ -131,7 +138,7 @@ export class AdwApplicationWindow extends GtkApplicationWindow {
131
138
  }
132
139
 
133
140
  attributeChangedCallback(name: string) {
134
- if (name === 'width' || name === 'height') {
141
+ if (name === 'width' || name === 'height' || name === 'width-request' || name === 'height-request') {
135
142
  this.renderFrameSize();
136
143
  return;
137
144
  }
@@ -64,6 +64,7 @@ import type {
64
64
  } from '@gjsify/adwaita-core';
65
65
 
66
66
  import { bindSlottedChildren, type AdwSlot } from '../slotted-children.js';
67
+ import type { DispatchedSignals } from '../signals.js';
67
68
 
68
69
  /** The persistent content. Child of <adw-bottom-sheet>; consumed at connect time. */
69
70
  export class AdwBottomSheetContent extends HTMLElement {}
@@ -75,6 +76,9 @@ export class AdwBottomSheetSheet extends HTMLElement {}
75
76
  export class AdwBottomSheetBottomBar extends HTMLElement {}
76
77
 
77
78
  export class AdwBottomSheet extends HTMLElement {
79
+ /** The GTK signals this element dispatches, each with the DOM event it arrives as (ADR 0093). */
80
+ static readonly signals: DispatchedSignals = { 'notify::open': 'notify::open' };
81
+
78
82
  private _initialized = false;
79
83
  private _contentEl!: HTMLDivElement;
80
84
  private _dimmingEl!: HTMLDivElement;
@@ -55,6 +55,7 @@ import { BACK_BUTTON_FALLBACK_TOOLTIP, NavigationViewState, describeNavigationDi
55
55
  import type { AdwNavigationPageProps, NavigationDiagnostic, NavigationStackChange } from '@gjsify/adwaita-core';
56
56
 
57
57
  import { bindSlottedChildren } from '../slotted-children.js';
58
+ import type { DispatchedSignals } from '../signals.js';
58
59
 
59
60
  /** The page properties the core owns, read off the element's attributes. */
60
61
  function readPageProps(page: AdwNavigationPage): AdwNavigationPageProps {
@@ -135,6 +136,14 @@ export class AdwNavigationPage extends HTMLElement {
135
136
  }
136
137
 
137
138
  export class AdwNavigationView extends HTMLElement {
139
+ /** The GTK signals this element dispatches, each with the DOM event it arrives as (ADR 0093). */
140
+ static readonly signals: DispatchedSignals = {
141
+ pushed: 'pushed',
142
+ popped: 'popped',
143
+ replaced: 'replaced',
144
+ 'notify::visible-page': 'notify::visible-page',
145
+ };
146
+
138
147
  private _pagesEl!: HTMLDivElement;
139
148
  // The back button injected into each visible page (so we can remove it).
140
149
  private readonly _backButtons = new WeakMap<AdwNavigationPage, HTMLElement>();
@@ -178,6 +187,11 @@ export class AdwNavigationView extends HTMLElement {
178
187
  return this._state.visiblePageTag;
179
188
  }
180
189
 
190
+ /** `adw_navigation_view_get_visible_page_tag`. */
191
+ get_visible_page_tag(): string | null {
192
+ return this.visiblePageTag;
193
+ }
194
+
181
195
  get depth(): number {
182
196
  return this._state.depth;
183
197
  }
@@ -303,6 +317,11 @@ export class AdwNavigationView extends HTMLElement {
303
317
  return pushed;
304
318
  }
305
319
 
320
+ /** `adw_navigation_view_push_by_tag`, under the name a GJS caller uses. */
321
+ push_by_tag(tag: string): boolean {
322
+ return this.pushByTag(tag);
323
+ }
324
+
306
325
  /** Push the page carrying `tag` (mirrors AdwNavigationView.push_by_tag). */
307
326
  pushByTag(tag: string): boolean {
308
327
  const pushed = this._state.pushByTag(tag);
@@ -29,6 +29,7 @@ import { SwitchRowState, deriveRowLabels } from '@gjsify/adwaita-core';
29
29
  import { bindEmptySections } from '../empty-sections.js';
30
30
  import { applyRowTooltip } from '../row-tooltip.js';
31
31
  import { bindSlottedChildren } from '../slotted-children.js';
32
+ import type { DispatchedSignals } from '../signals.js';
32
33
  import { applyRowLineClamp, parseRowLinesAttribute } from '../row-line-clamp.js';
33
34
 
34
35
  // SIDE-EFFECT import, deliberately separate from the type import below: it guarantees
@@ -42,6 +43,9 @@ import { attachRowActivation } from './row-activation.js';
42
43
  import type { GtkSwitch } from './gtk-switch.js';
43
44
 
44
45
  export class AdwSwitchRow extends HTMLElement {
46
+ /** The GTK signals this element dispatches, each with the DOM event it arrives as (ADR 0093). */
47
+ static readonly signals: DispatchedSignals = { 'notify::active': 'notify::active' };
48
+
45
49
  private _switchEl!: GtkSwitch;
46
50
  private _prefixEl!: HTMLDivElement;
47
51
  /** The label column — kept for `tooltip-text`, which the browser reads off a PART. */
@@ -13,10 +13,16 @@
13
13
  import { AdwBreakpoint } from '@gjsify/adwaita-core';
14
14
  import { addBreakpoints } from '../breakpoints.js';
15
15
 
16
- /** The default minimum size of an AdwWindow, from adw_window_init (C:344). */
16
+ /** `adw_window_init`'s size request: a MINIMUM, overridable by `width-request`/`height-request`. */
17
17
  const DEFAULT_WIDTH = 360;
18
18
  const DEFAULT_HEIGHT = 200;
19
19
 
20
+ /** A `width-request`/`height-request` attribute as px; absent, unparsable or -1 means unset. */
21
+ function requested(host: HTMLElement, name: string, fallback: number): number {
22
+ const value = Number.parseFloat(host.getAttribute(name) ?? '');
23
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
24
+ }
25
+
20
26
  export class AdwWindowFeatures {
21
27
  private _contentEl: HTMLDivElement | null = null;
22
28
  private _breakpoints: AdwBreakpoint[] = [];
@@ -117,12 +123,21 @@ export class AdwWindowFeatures {
117
123
  this._disposeBreakpoints = null;
118
124
  }
119
125
 
120
- /** The window's size request — GTK's, not CSS's, so an absent attribute means the default. */
126
+ /**
127
+ * The window's size. `Adw.Window` defaults to a 360×200 MINIMUM (a size request, not the
128
+ * size; `width-request`/`height-request` override it). `default-width/height` is only
129
+ * a default: an authored size is the box, and an absent one leaves the box to the
130
+ * stylesheet, which fills the viewport — a page has no window manager to pick a size.
131
+ */
121
132
  applySizeRequest(): void {
122
133
  const host = this._host;
134
+ host.style.minWidth = `${requested(host, 'width-request', DEFAULT_WIDTH)}px`;
135
+ host.style.minHeight = `${requested(host, 'height-request', DEFAULT_HEIGHT)}px`;
123
136
  const width = host.getAttribute('default-width') ?? host.getAttribute('width');
124
137
  const height = host.getAttribute('default-height') ?? host.getAttribute('height');
125
- host.style.width = width ? `${width}px` : `${DEFAULT_WIDTH}px`;
126
- host.style.height = height ? `${height}px` : `${DEFAULT_HEIGHT}px`;
138
+ if (width) host.style.width = `${width}px`;
139
+ else host.style.removeProperty('width');
140
+ if (height) host.style.height = `${height}px`;
141
+ else host.style.removeProperty('height');
127
142
  }
128
143
  }
@@ -48,7 +48,7 @@ export class AdwWindow extends GtkWindow {
48
48
  private readonly _features = new AdwWindowFeatures(this);
49
49
 
50
50
  static get observedAttributes() {
51
- return [...GtkWindow.observedAttributes, 'width', 'height'];
51
+ return [...GtkWindow.observedAttributes, 'width', 'height', 'width-request', 'height-request'];
52
52
  }
53
53
 
54
54
  /** The content area — the Adw.Window:content property. */
@@ -105,7 +105,7 @@ export class AdwWindow extends GtkWindow {
105
105
  attributeChangedCallback(name: string) {
106
106
  // The size request is read from the ATTRIBUTE, so it is applied on every change —
107
107
  // including one that arrived before the element was connected.
108
- if (name === 'width' || name === 'height') {
108
+ if (name === 'width' || name === 'height' || name === 'width-request' || name === 'height-request') {
109
109
  this.renderFrameSize();
110
110
  return;
111
111
  }
@@ -28,6 +28,7 @@
28
28
 
29
29
  import type { ActionLike } from '@gjsify/adwaita-core';
30
30
  import type { GtkApplication } from '../application.js';
31
+ import type { DispatchedSignals } from '../signals.js';
31
32
  import type { AdwSlot } from '../slotted-children.js';
32
33
  import { GtkWindow } from './gtk-window.js';
33
34
  import { defineWindowApplication } from '../application.js';
@@ -45,6 +46,11 @@ export class GtkApplicationWindow extends GtkWindow {
45
46
  private _menubar: HTMLDivElement | null = null;
46
47
  private _menubarObserver: MutationObserver | null = null;
47
48
 
49
+ static override readonly signals: DispatchedSignals = {
50
+ ...GtkWindow.signals,
51
+ 'notify::show-menubar': 'notify::show-menubar',
52
+ };
53
+
48
54
  static get observedAttributes(): string[] {
49
55
  return [...GtkWindow.observedAttributes, 'show-menubar'];
50
56
  }
@@ -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
- /** `Gtk.Window:is-active` — see the header: the document's focus, while this frame is shown. */
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
- /** `gtk_window_is_active`. */
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;
@@ -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);
@@ -148,13 +154,26 @@ export function installElementApi(el: HTMLElement): void {
148
154
  });
149
155
  define('emit', (signal: string, ...args: unknown[]): void => dispatchElement(el, signal, args));
150
156
  define('notify', (name: string): void => notifyElement(el, name));
157
+ define('get_property', (name: string): unknown => {
158
+ eventOf(el, `notify::${name}`);
159
+ return (el as unknown as Record<string, unknown>)[propertyOf(name)];
160
+ });
161
+ define('set_property', (name: string, value: unknown): void => {
162
+ eventOf(el, `notify::${name}`);
163
+ const member = propertyOf(name);
164
+ const descriptor = inheritedDescriptor(el, member);
165
+ if (descriptor !== undefined && descriptor.get !== undefined && descriptor.set === undefined) {
166
+ throw new Error(`Property '${name}' of <${el.localName}> is read-only`);
167
+ }
168
+ (el as unknown as Record<string, unknown>)[member] = value;
169
+ });
151
170
  // The `Gtk.Widget` CSS-class calls a template's TypeScript makes on its children.
152
171
  define('add_css_class', (name: string): void => el.classList.add(name));
153
172
  define('remove_css_class', (name: string): void => el.classList.remove(name));
154
173
  define('has_css_class', (name: string): boolean => el.classList.contains(name));
155
174
  }
156
175
 
157
- const API_MEMBERS = new Set(['connect', 'disconnect', 'emit', 'notify']);
176
+ const API_MEMBERS = new Set(['connect', 'disconnect', 'emit', 'notify', 'get_property', 'set_property']);
158
177
 
159
178
  /**
160
179
  * The end of a bind the core's engine drives. An element speaks camelCase members where a `.blp`