@gjsify/adwaita-web 0.64.13-next.283 → 0.64.14

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.
Files changed (44) hide show
  1. package/lib/types/application.d.ts +3 -3
  2. package/lib/types/elements/adw-application-window.d.ts +4 -4
  3. package/lib/types/elements/adw-bin.d.ts +13 -0
  4. package/lib/types/elements/adw-split-button.d.ts +19 -28
  5. package/lib/types/elements/adw-tab-overview.d.ts +4 -4
  6. package/lib/types/elements/gtk-menu-button.d.ts +30 -42
  7. package/lib/types/elements/gtk-popover-bin.d.ts +18 -11
  8. package/lib/types/elements/gtk-popover-menu-bar.d.ts +15 -8
  9. package/lib/types/elements/gtk-popover-menu.d.ts +13 -15
  10. package/lib/types/elements/menu-slot.d.ts +49 -0
  11. package/lib/types/elements/popover-menu.d.ts +11 -7
  12. package/lib/types/index.d.ts +2 -1
  13. package/lib/types/namespace/gio.d.ts +1 -1
  14. package/lib/types/widget-element.d.ts +8 -2
  15. package/package.json +12 -13
  16. package/src/adw-application-window.spec.ts +5 -2
  17. package/src/adw-bin.spec.ts +20 -0
  18. package/src/application.ts +4 -4
  19. package/src/elements/adw-application-window.ts +17 -21
  20. package/src/elements/adw-bin.ts +20 -1
  21. package/src/elements/adw-split-button.ts +35 -59
  22. package/src/elements/adw-tab-overview.ts +18 -28
  23. package/src/elements/gtk-menu-button.ts +60 -82
  24. package/src/elements/gtk-popover-bin.ts +32 -38
  25. package/src/elements/gtk-popover-menu-bar.ts +41 -46
  26. package/src/elements/gtk-popover-menu.ts +35 -54
  27. package/src/elements/menu-slot.ts +139 -0
  28. package/src/elements/popover-menu.ts +26 -16
  29. package/src/gio-menu.spec.ts +173 -34
  30. package/src/gtk-image.spec.ts +3 -2
  31. package/src/gtk-menu-button.spec.ts +19 -16
  32. package/src/gtk-popover-bin.spec.ts +6 -5
  33. package/src/gtk-popover-menu-bar.spec.ts +11 -6
  34. package/src/gtk-popover-menu.spec.ts +22 -22
  35. package/src/index.ts +10 -2
  36. package/src/keyboard-operable.spec.ts +6 -5
  37. package/src/menu-actions.spec.ts +6 -1
  38. package/src/namespace/gio.ts +19 -2
  39. package/src/pango-markup.spec.ts +11 -0
  40. package/src/pango-markup.ts +5 -2
  41. package/src/shared-tree-builder.ts +6 -1
  42. package/src/split-button.spec.ts +25 -8
  43. package/src/widget-element.ts +28 -7
  44. package/src/widget-members.spec.ts +42 -3
@@ -61,6 +61,26 @@ export const AdwBinTest = async () => {
61
61
  other.remove();
62
62
  });
63
63
 
64
+ await it('answers `get_child()` / `set_child()` too, the spelling GI-built code uses', () => {
65
+ // `Adw.Bin:child` has two doors, and markup only takes one of them: code written
66
+ // against GI calls the methods, which is what a `.blp`-driven widget shared with
67
+ // GJS and NativeScript does. Both of those renderers answer them; the web did not,
68
+ // and shared code died here with `get_child is not a function`.
69
+ const { el, host } = mount();
70
+ expect(el.get_child()).toBeNull();
71
+ const child = document.createElement('div');
72
+ el.set_child(child);
73
+ expect(el.get_child()).toBe(child);
74
+ expect(el.child).toBe(child);
75
+ const next = document.createElement('span');
76
+ el.set_child(next);
77
+ expect(el.get_child()).toBe(next);
78
+ expect(el.childElementCount).toBe(1);
79
+ el.set_child(null);
80
+ expect(el.get_child()).toBeNull();
81
+ host.remove();
82
+ });
83
+
64
84
  await it('a bin is a column flex box, and its only child fills it', () => {
65
85
  const { el, host } = mount([document.createElement('div')]);
66
86
  // A bin takes the size it is GIVEN and hands all of it to the child, which
@@ -15,7 +15,7 @@ import {
15
15
  menuActionsFor,
16
16
  type AdwMenuItem,
17
17
  type AdwMenuActions,
18
- type AdwMenuModel,
18
+ type AdwMenuTree,
19
19
  type ApplicationHost,
20
20
  } from '@gjsify/adwaita-core';
21
21
  import { IconTheme } from './namespace/gtk-platform.js';
@@ -63,9 +63,9 @@ export function activateWidgetAction(widget: Element, fullName: string, paramete
63
63
  return activateIn(widget, fullName, parentOfElement, parameter);
64
64
  }
65
65
 
66
- /** What the items of `model` read from the actions registered above `el` (ADR 0097 § 3). */
67
- export function menuActionsOfElement(el: Element, model: AdwMenuModel): AdwMenuActions {
68
- return menuActionsFor(model, el, parentOfElement);
66
+ /** What the items of `tree` read from the actions registered above `el` (ADR 0097 § 3). */
67
+ export function menuActionsOfElement(el: Element, tree: AdwMenuTree): AdwMenuActions {
68
+ return menuActionsFor(tree, el, parentOfElement);
69
69
  }
70
70
 
71
71
  /** What choosing `item` in a menu owned by `el` does: the action it names fires. */
@@ -9,9 +9,9 @@
9
9
  // is on, and no menubar at all when it is off.
10
10
  //
11
11
  // A BROWSER HAS NO GtkApplication, so the model is handed to the window instead of to
12
- // an application that does not exist here. `menu-model` is the same portable menu value
13
- // ADR 0042 gave `<gtk-menu-button>` and `<adw-split-button>` (a JSON array), and it is
14
- // read through the same core parser, so one menu is written once. A menubar is a row of
12
+ // an application that does not exist here. `menu-model` is the same real `Gio.MenuModel`
13
+ // ADR 0128 gave `<gtk-menu-button>` and `<adw-split-button>`, taken through the same
14
+ // {@link MenuSlot} door, so one menu is written once. A menubar is a row of
15
15
  // MENU LINKS, which is what `Adw.Application`'s menubar is documented to be, so a
16
16
  // section is inlined and a bare item becomes a plain button — `flattenMenu` is the
17
17
  // walk that decides both.
@@ -20,8 +20,9 @@
20
20
  // show-menubar (boolean — Gtk.ApplicationWindow:show-menubar; the bar is drawn only
21
21
  // while this is set AND the model has something in it, exactly as GTK draws no bar
22
22
  // for an application that installed none)
23
- // menu-model (the portable menu model, JSON — the stand-in for the model a
24
- // GtkApplication would own; the same value `<gtk-menu-button menu-model>` takes)
23
+ // menu-model (the menu's JSON, parsed into the model this window owns — the
24
+ // stand-in for the model a GtkApplication would own; `<gtk-menu-button menu-model>`'s
25
+ // own spelling, and a mutation of a held model redraws the bar)
25
26
  //
26
27
  // Extends <gtk-application-window>, as Adw.ApplicationWindow extends Gtk.ApplicationWindow,
27
28
  // so the Gtk.Window state members come with it. The content property, the size request, the
@@ -33,22 +34,17 @@
33
34
  // Copyright (c) 2021-2023 Alice Mikhaylenko / Purism SPC (libadwaita). LGPLv2.1+
34
35
  // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
35
36
 
36
- import {
37
- flattenMenu,
38
- normalizeMenuModel,
39
- parseMenuModel,
40
- type AdwMenuInput,
41
- type AdwMenuModel,
42
- } from '@gjsify/adwaita-core';
37
+ import { buildMenu, flattenMenu, type AdwMenuModel } from '@gjsify/adwaita-core';
43
38
 
44
39
  import type { AdwBreakpoint } from '@gjsify/adwaita-core';
45
40
  import type { AdwSlot } from '../slotted-children.js';
46
41
  import { AdwWindowFeatures } from './adw-window-features.js';
42
+ import { MenuSlot } from './menu-slot.js';
47
43
  import { GtkApplicationWindow } from './gtk-application-window.js';
48
44
 
49
45
  export class AdwApplicationWindow extends GtkApplicationWindow {
50
46
  private readonly _features = new AdwWindowFeatures(this);
51
- private _model: AdwMenuModel = [];
47
+ private readonly _slot = new MenuSlot(() => this._renderMenubar());
52
48
  private _menubarEl: HTMLDivElement | null = null;
53
49
 
54
50
  // The base's list BY NAME, not `super.observedAttributes`: `AdwPasswordEntryRow`
@@ -110,12 +106,12 @@ export class AdwApplicationWindow extends GtkApplicationWindow {
110
106
  }
111
107
 
112
108
  /** The menubar's model — what a GtkApplication would own here. */
113
- get menuModel(): AdwMenuModel {
114
- return this._model;
109
+ get menuModel(): AdwMenuModel | null {
110
+ return this._slot.model;
115
111
  }
116
112
 
117
- set menuModel(value: AdwMenuInput) {
118
- this._model = normalizeMenuModel(value);
113
+ set menuModel(value: AdwMenuModel | string | null) {
114
+ this._slot.write(value);
119
115
  this._renderMenubar();
120
116
  }
121
117
 
@@ -145,7 +141,7 @@ export class AdwApplicationWindow extends GtkApplicationWindow {
145
141
  // The attribute is the declarative form and it wins when it changes — the same
146
142
  // precedence `<gtk-menu-button>` gives `menu-model`, through the same parser.
147
143
  if (name === 'menu-model') {
148
- this._model = parseMenuModel(this.getAttribute('menu-model'));
144
+ this._slot.writeMarkup(this.getAttribute('menu-model'));
149
145
  this._renderMenubar();
150
146
  }
151
147
  }
@@ -197,16 +193,16 @@ export class AdwApplicationWindow extends GtkApplicationWindow {
197
193
  bar.replaceChildren();
198
194
  // GTK draws no menubar for an application that installed none, so neither does
199
195
  // this: both the flag and a model with rows in it are required.
200
- bar.hidden = !this.showMenubar || this._model.length === 0;
196
+ bar.hidden = !this.showMenubar || this._slot.isEmpty;
201
197
 
202
- for (const row of flattenMenu(this._model)) {
198
+ for (const row of flattenMenu(this._slot.tree)) {
203
199
  const node = row.node;
204
200
  if (node.kind === 'submenu') {
205
201
  // A link is a menu the user OPENS — a button with that menu on it,
206
202
  // which is the whole of what a Gtk.PopoverMenuBar row is.
207
203
  const button = document.createElement('gtk-menu-button');
208
204
  button.setAttribute('menu-title', node.label);
209
- (button as unknown as { menuModel: AdwMenuInput }).menuModel = node.items;
205
+ (button as unknown as { menuModel: AdwMenuModel | null }).menuModel = buildMenu(node.items);
210
206
  bar.appendChild(button);
211
207
  continue;
212
208
  }
@@ -24,7 +24,7 @@
24
24
  // default `GENERIC` and a bin contributes nothing to the accessibility tree of its own.
25
25
  //
26
26
  // KNOWN_GAPS: none — `AdwBin`'s only property is `child`, a widget, which is a SLOT on
27
- // every renderer.
27
+ // every renderer, and it is reachable BOTH ways: `child` and `get_child()`/`set_child()`.
28
28
  //
29
29
  // Reference: refs/libadwaita/src/adw-bin.c (adw_bin_set_child, class_init)
30
30
  // Copyright (c) 2021 Purism SPC / GNOME contributors (libadwaita). LGPLv2.1+.
@@ -46,6 +46,25 @@ export class AdwBin extends WidgetElement {
46
46
  this.setChild(value);
47
47
  }
48
48
 
49
+ /**
50
+ * `adw_bin_get_child` / `adw_bin_set_child` — the METHOD spelling of {@link child}.
51
+ *
52
+ * The property alone is not the whole door. `Adw.Bin:child` is reached two ways and only
53
+ * one of them is a property: markup writes `child:`, but code built against GI calls
54
+ * `bin.get_child()` / `bin.set_child(w)`, which is the spelling a `.blp`-driven widget
55
+ * shared across renderers uses — GJS and the NativeScript bridge both answer it
56
+ * (`AdwSingleChildBase`), and here it was missing, so the shared code died with
57
+ * `get_child is not a function` on the web and nowhere else.
58
+ */
59
+ get_child(): Element | null {
60
+ return this.child;
61
+ }
62
+
63
+ /** `adw_bin_set_child`. Refuses a child that already has a parent, as {@link setChild} does. */
64
+ set_child(child: Element | null): void {
65
+ this.setChild(child);
66
+ }
67
+
49
68
  connectedCallback() {
50
69
  // The NAME only. Every child of a bin already IS its content — there is no
51
70
  // internal structure to route into — so `.install()` is deliberately not called
@@ -37,13 +37,8 @@
37
37
  // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
38
38
 
39
39
  import {
40
- ADW_MENU_SURFACE_WEB,
41
40
  SplitButtonState,
42
- assertMenuRenderable,
43
41
  isSplitButtonDirection,
44
- menuRefusals,
45
- normalizeMenuModel,
46
- parseMenuModel,
47
42
  resolveDropdownTooltip,
48
43
  splitButtonArrowIcon,
49
44
  splitButtonPopupDirection,
@@ -52,13 +47,13 @@ import {
52
47
  import type {
53
48
  AdwArrowIcon,
54
49
  AdwMenuActions,
55
- AdwMenuInput,
56
50
  AdwMenuModel,
57
51
  SplitButtonChange,
58
52
  SplitButtonDirection,
59
53
  } from '@gjsify/adwaita-core';
60
54
  import { activateElementMenuItem, menuActionsOfElement } from '../application.js';
61
55
 
56
+ import { MenuSlot } from './menu-slot.js';
62
57
  import { PopoverMenuView } from './popover-menu.js';
63
58
 
64
59
  // SIDE-EFFECT import, deliberately separate from the type import below: it is
@@ -121,6 +116,15 @@ const POPOVER_POSITIONS = {
121
116
 
122
117
  export class AdwSplitButton extends WidgetElement {
123
118
  private readonly _state = new SplitButtonState();
119
+ /**
120
+ * The model's door, tree snapshot and `items-changed` subscription (ADR 0128). The
121
+ * STATE still owns the menu as a property — `dropdownEnabled` is derived from it and
122
+ * the notify order is libadwaita's — so every write here goes through the slot FIRST
123
+ * (which is what refuses it and snapshots it) and is then handed to the state.
124
+ */
125
+ private readonly _slot = new MenuSlot(() => {
126
+ if (this._initialized) this._render();
127
+ });
124
128
  private _actionEl!: HTMLButtonElement;
125
129
  private _dropdownEl!: HTMLButtonElement;
126
130
  private _arrowEl!: GtkImage;
@@ -180,60 +184,29 @@ export class AdwSplitButton extends WidgetElement {
180
184
  this.removeAttribute('label');
181
185
  }
182
186
 
183
- /** The menu, normalised (ADR 0042). Empty when the dropdown has none. */
184
- get menuModel(): AdwMenuModel {
185
- return this._state.menuModel ?? [];
186
- }
187
-
188
187
  /**
189
- * Set the menu from anything the portable model accepts — a bare `string[]`, item
190
- * descriptors, sections, submenus.
188
+ * The menu the dropdown pops up, by identity, or `null` when it has none (ADR 0128).
191
189
  *
192
- * The REFUSAL is here rather than at render time: a `custom` item names an
193
- * application widget this surface cannot host, and discovering that as a blank row
194
- * three interactions later is what {@link assertMenuRenderable} exists to prevent.
190
+ * `null` is the ONLY spelling of "no menu" now: an empty model is a menu, so an empty
191
+ * one leaves the dropdown enabled exactly as it does in GTK, where a non-null model
192
+ * still builds a popover (measured, `MENU_MODEL_DOOR_VECTORS`).
195
193
  */
196
- set menuModel(value: AdwMenuInput) {
197
- // Refused BEFORE it is stored, so a throw leaves the widget showing the menu it
198
- // had rather than one it cannot draw.
199
- this._state.setMenuModel(this._acceptMenu(value));
200
- if (this._initialized) this._render();
194
+ get menuModel(): AdwMenuModel | null {
195
+ return this._state.menuModel;
201
196
  }
202
197
 
203
198
  /**
204
- * Normalise, REFUSE, store — the one door every menu comes through.
205
- *
206
- * The property setter is not the only way a menu arrives: an attribute is, and the
207
- * first cut refused only the setter, so a `custom` item written in markup drew
208
- * exactly the row `assertMenuRenderable` exists to prevent — one that reads like a
209
- * command and does nothing.
210
- *
211
- * THE TWO DOORS REFUSE DIFFERENTLY, and the difference is which side can hear it.
212
- * A property assignment is a CALL, so it throws and the caller can catch. An
213
- * attribute is MARKUP, parsed by the browser: a throw from `connectedCallback` is
214
- * not delivered to whoever appended the element, it is reported as an uncaught
215
- * page error — measured, it broke `adwaita-upgrade-order.spec.ts`, which counts
216
- * exactly those. Nobody can handle it and everybody else pays for it. So the
217
- * attribute path REFUSES THE MENU (no menu, and a menu-less dropdown is
218
- * insensitive, which is visible) and says why on `console.error`.
199
+ * Take a real model, a JSON string in the portable spelling, or `null`.
219
200
  *
220
- * A REFUSAL AND A TYPO ARE STILL DIFFERENT THINGS. `parseMenuModel` is total
221
- * because malformed JSON is an author slip and must not stop the element
222
- * upgrading; `custom` is well-formed, deliberate, and unhonourable here.
201
+ * The REFUSAL is at the assignment rather than at render time: a `custom` item names
202
+ * an application widget this surface cannot host, and discovering that as a blank row
203
+ * three interactions later is what the refusal exists to prevent. {@link MenuSlot}
204
+ * owns the door; the state is handed only a menu this surface agreed to draw.
223
205
  */
224
- private _acceptMenu(input: AdwMenuInput, strict = true): AdwMenuModel {
225
- const model = normalizeMenuModel(input);
226
- if (strict) {
227
- assertMenuRenderable(model, ADW_MENU_SURFACE_WEB);
228
- return model;
229
- }
230
- const refusals = menuRefusals(model, ADW_MENU_SURFACE_WEB);
231
- if (refusals.length === 0) return model;
232
- // `console.error` is the browser's own channel for "this is wrong, the page
233
- // continues" — and the alternative here is a row that lies.
234
- console.error(`${ADW_MENU_SURFACE_WEB.name} cannot render this menu, so it has none:`);
235
- for (const refusal of refusals) console.error(` [${refusal.path.join('.')}] ${refusal.message}`);
236
- return [];
206
+ set menuModel(value: AdwMenuModel | string | null) {
207
+ this._slot.write(value);
208
+ this._state.setMenuModel(this._slot.model);
209
+ if (this._initialized) this._render();
237
210
  }
238
211
 
239
212
  /**
@@ -387,7 +360,8 @@ export class AdwSplitButton extends WidgetElement {
387
360
  this._state.setDropdownTooltip(value ?? '');
388
361
  break;
389
362
  case 'menu-model':
390
- this._state.setMenuModel(this._acceptMenu(parseMenuModel(value), false));
363
+ this._slot.writeMarkup(value);
364
+ this._state.setMenuModel(this._slot.model);
391
365
  break;
392
366
  default:
393
367
  break;
@@ -409,10 +383,12 @@ export class AdwSplitButton extends WidgetElement {
409
383
  const direction = this.getAttribute('direction');
410
384
  if (isSplitButtonDirection(direction)) this._state.setDirection(direction);
411
385
  this._state.setDropdownTooltip(this.getAttribute('dropdown-tooltip') ?? '');
412
- // The `menuModel` property may have been set before the element upgraded;
413
- // the attribute only seeds an otherwise empty menu.
386
+ // The `menuModel` property may have been set before the element upgraded; the
387
+ // attribute only seeds a widget that has NO menu — which since ADR 0128 means a
388
+ // null one, an empty model being a menu an author may have meant.
414
389
  if (this._state.menuModel === null) {
415
- this._state.setMenuModel(this._acceptMenu(parseMenuModel(this.getAttribute('menu-model')), false));
390
+ this._slot.writeMarkup(this.getAttribute('menu-model'));
391
+ this._state.setMenuModel(this._slot.model);
416
392
  }
417
393
  }
418
394
 
@@ -528,9 +504,9 @@ export class AdwSplitButton extends WidgetElement {
528
504
 
529
505
  private _renderMenu(): void {
530
506
  this._menuEl.open = this._state.open;
531
- const model = this._state.menuModel ?? [];
532
- this._menuView.setMenuModel(model);
533
- this._menuView.setActions(this._actions ?? menuActionsOfElement(this, model));
507
+ const tree = this._slot.tree;
508
+ this._menuView.setMenuTree(tree);
509
+ this._menuView.setActions(this._actions ?? menuActionsOfElement(this, tree));
534
510
  this._menuView.render();
535
511
  }
536
512
  }
@@ -46,8 +46,9 @@
46
46
  //
47
47
  // `view` is a widget REFERENCE (`Adw.TabView`), so the attribute is the element id
48
48
  // `<adw-tab-button view>` also takes, while `view`/`setView` take the element.
49
- // `secondary-menu` is a `GMenuModel`: the property takes the portable model and the
50
- // attribute takes its JSON, which is `<gtk-menu-button menu-model>`'s own spelling.
49
+ // `secondary-menu` is a `GMenuModel` and takes a real one (ADR 0128): the property takes
50
+ // the model, the attribute takes its JSON, which is `<gtk-menu-button menu-model>`'s own
51
+ // spelling, and {@link MenuSlot} is the door both go through.
51
52
  //
52
53
  // A11Y: the overview is a `role="grid"` of `role="gridcell"` thumbnails under a
53
54
  // header bar, and the pinned grid is a second grid — C's `AdwTabGrid` is a
@@ -68,16 +69,9 @@
68
69
  // Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the icon,
69
70
  // indicator and portrait nodes are <gtk-image> and the grids are <div role="grid">.
70
71
 
71
- import {
72
- gridColumns,
73
- gridNavigate,
74
- tabSearchMatches,
75
- tabTooltip,
76
- type AdwMenuInput,
77
- type AdwMenuModel,
78
- } from '@gjsify/adwaita-core';
79
- import { normalizeMenuModel } from '@gjsify/adwaita-core';
80
- import { assertMenuRenderable, ADW_MENU_SURFACE_WEB } from '@gjsify/adwaita-core';
72
+ import { gridColumns, gridNavigate, tabSearchMatches, tabTooltip, type AdwMenuModel } from '@gjsify/adwaita-core';
73
+
74
+ import { MenuSlot } from './menu-slot.js';
81
75
 
82
76
  import type { AdwTabView, AdwTabViewPage } from './adw-tab-view.js';
83
77
  import { createGtkImage, type GtkImage } from './gtk-image.js';
@@ -119,7 +113,7 @@ export class AdwTabOverview extends WidgetElement {
119
113
  private readonly _thumbs = new Map<string, Thumbnail>();
120
114
 
121
115
  private _view: AdwTabView | null = null;
122
- private _model: AdwMenuModel = [];
116
+ private readonly _slot = new MenuSlot(() => this._applyMenu());
123
117
  private _searchActive = false;
124
118
  private _initialized = false;
125
119
  /** The last visibility actually painted, so `notify::open` fires on a real change. */
@@ -225,16 +219,14 @@ export class AdwTabOverview extends WidgetElement {
225
219
  }
226
220
 
227
221
  /** `Adw.TabOverview:secondary-menu` — the portal menu of extra actions. */
228
- get secondaryMenu(): AdwMenuModel {
229
- return this._model;
222
+ get secondaryMenu(): AdwMenuModel | null {
223
+ return this._slot.model;
230
224
  }
231
225
 
232
- set secondaryMenu(value: AdwMenuInput) {
233
- const model = normalizeMenuModel(value);
226
+ set secondaryMenu(value: AdwMenuModel | string | null) {
234
227
  // The property setter is a CALL, so a `custom` item throws here where the caller
235
228
  // can catch it — `<gtk-menu-button>`'s own rule, which keeps the two doors honest.
236
- assertMenuRenderable(model, ADW_MENU_SURFACE_WEB);
237
- this._model = model;
229
+ this._slot.write(value);
238
230
  this._applyMenu();
239
231
  }
240
232
 
@@ -398,7 +390,8 @@ export class AdwTabOverview extends WidgetElement {
398
390
 
399
391
  const declaredMenu = this.getAttribute('secondary-menu');
400
392
  if (declaredMenu !== null) {
401
- this.secondaryMenu = JSON.parse(declaredMenu) as AdwMenuInput;
393
+ this._slot.writeMarkup(declaredMenu);
394
+ this._applyMenu();
402
395
  this.removeAttribute('secondary-menu');
403
396
  }
404
397
 
@@ -423,11 +416,8 @@ export class AdwTabOverview extends WidgetElement {
423
416
  if (value === null) return;
424
417
  // An attribute is MARKUP: a `custom` item here is reported, not thrown
425
418
  // (adw-sidebar/`gtk-menu-button`'s rule for the same two doors).
426
- try {
427
- this.secondaryMenu = JSON.parse(value) as AdwMenuInput;
428
- } catch (error) {
429
- console.error(`<adw-tab-overview secondary-menu> is not valid JSON: ${String(error)}`);
430
- }
419
+ this._slot.writeMarkup(value);
420
+ this._applyMenu();
431
421
  return;
432
422
  }
433
423
  case 'enable-new-tab':
@@ -557,7 +547,7 @@ export class AdwTabOverview extends WidgetElement {
557
547
  * slot is where an application puts its own controls.
558
548
  */
559
549
  private _applyHeaderBar(): void {
560
- const hasMenu = this._model.length > 0;
550
+ const hasMenu = !this._slot.isEmpty;
561
551
  this._headerEl.hidden = !(
562
552
  this.enableSearch ||
563
553
  hasMenu ||
@@ -572,8 +562,8 @@ export class AdwTabOverview extends WidgetElement {
572
562
  }
573
563
 
574
564
  private _applyMenu(): void {
575
- const view = this._menuBtn as HTMLElement & { menuModel?: AdwMenuModel };
576
- if (this._model.length > 0) view.menuModel = this._model;
565
+ const view = this._menuBtn as HTMLElement & { menuModel?: AdwMenuModel | null };
566
+ if (this._slot.model !== null) view.menuModel = this._slot.model;
577
567
  this._applyHeaderBar();
578
568
  }
579
569