@gjsify/adwaita-web 0.30.0 → 0.32.0

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 (176) hide show
  1. package/dist/adwaita-web.css +786 -272
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/adw-about-dialog.spec.d.ts +1 -0
  4. package/lib/types/adw-action-rows.spec.d.ts +1 -0
  5. package/lib/types/adw-avatar.spec.d.ts +1 -0
  6. package/lib/types/adw-banner.spec.d.ts +1 -0
  7. package/lib/types/adw-button-content.spec.d.ts +1 -0
  8. package/lib/types/adw-carousel.spec.d.ts +1 -0
  9. package/lib/types/adw-checks.spec.d.ts +1 -0
  10. package/lib/types/adw-header-bar.spec.d.ts +1 -0
  11. package/lib/types/adw-icon.spec.d.ts +1 -0
  12. package/lib/types/adw-progress-bar.spec.d.ts +1 -0
  13. package/lib/types/adw-sidebar.spec.d.ts +1 -0
  14. package/lib/types/adw-switch.spec.d.ts +1 -0
  15. package/lib/types/adw-wrap-box.spec.d.ts +1 -0
  16. package/lib/types/bottom-sheet.spec.d.ts +1 -0
  17. package/lib/types/breakpoints.d.ts +22 -0
  18. package/lib/types/breakpoints.spec.d.ts +1 -0
  19. package/lib/types/chrome.spec.d.ts +1 -0
  20. package/lib/types/elements/adw-about-dialog.d.ts +47 -8
  21. package/lib/types/elements/adw-action-row.d.ts +35 -0
  22. package/lib/types/elements/adw-avatar.d.ts +11 -0
  23. package/lib/types/elements/adw-bottom-sheet.d.ts +33 -4
  24. package/lib/types/elements/adw-button-content.d.ts +12 -0
  25. package/lib/types/elements/adw-button-row.d.ts +10 -1
  26. package/lib/types/elements/adw-carousel.d.ts +111 -8
  27. package/lib/types/elements/adw-checks.d.ts +57 -0
  28. package/lib/types/elements/adw-clamp.d.ts +4 -1
  29. package/lib/types/elements/adw-data-grid.d.ts +11 -31
  30. package/lib/types/elements/adw-drop-down.d.ts +17 -20
  31. package/lib/types/elements/adw-entry-row.d.ts +93 -3
  32. package/lib/types/elements/adw-expander-row.d.ts +7 -1
  33. package/lib/types/elements/adw-header-bar.d.ts +15 -0
  34. package/lib/types/elements/adw-icon.d.ts +25 -0
  35. package/lib/types/elements/adw-inline-view-switcher.d.ts +37 -11
  36. package/lib/types/elements/adw-menu-button.d.ts +6 -6
  37. package/lib/types/elements/adw-navigation-split-view.d.ts +6 -0
  38. package/lib/types/elements/adw-navigation-view.d.ts +63 -20
  39. package/lib/types/elements/adw-overlay-split-view.d.ts +38 -0
  40. package/lib/types/elements/adw-password-entry-row.d.ts +19 -10
  41. package/lib/types/elements/adw-popover.d.ts +58 -0
  42. package/lib/types/elements/adw-preferences-dialog.d.ts +57 -2
  43. package/lib/types/elements/adw-preferences-group.d.ts +31 -0
  44. package/lib/types/elements/adw-progress-bar.d.ts +38 -0
  45. package/lib/types/elements/adw-sidebar.d.ts +37 -5
  46. package/lib/types/elements/adw-split-button.d.ts +53 -7
  47. package/lib/types/elements/adw-switch-row.d.ts +11 -2
  48. package/lib/types/elements/adw-switch.d.ts +15 -0
  49. package/lib/types/elements/adw-tab-view.d.ts +129 -12
  50. package/lib/types/elements/adw-toolbar-view.d.ts +5 -0
  51. package/lib/types/elements/adw-view-stack.d.ts +27 -19
  52. package/lib/types/elements/adw-view-switcher-bar.d.ts +23 -7
  53. package/lib/types/elements/adw-view-switcher.d.ts +22 -11
  54. package/lib/types/elements/adw-window-title.d.ts +3 -1
  55. package/lib/types/elements/view-switcher-dom.d.ts +44 -0
  56. package/lib/types/entry-rows.spec.d.ts +1 -0
  57. package/lib/types/index.d.ts +7 -0
  58. package/lib/types/navigation-view.spec.d.ts +1 -0
  59. package/lib/types/preferences-search.d.ts +20 -0
  60. package/lib/types/preferences.spec.d.ts +1 -0
  61. package/lib/types/split-button.spec.d.ts +1 -0
  62. package/lib/types/split-views.spec.d.ts +1 -0
  63. package/lib/types/styles.generated.d.ts +1 -1
  64. package/lib/types/tab-view.spec.d.ts +1 -0
  65. package/lib/types/view-stack.spec.d.ts +1 -0
  66. package/lib/types/view-switcher.spec.d.ts +1 -0
  67. package/package.json +6 -6
  68. package/scss/_banner.scss +13 -0
  69. package/scss/_bottom_sheet.scss +5 -6
  70. package/scss/_button.scss +3 -26
  71. package/scss/_button_content.scss +28 -0
  72. package/scss/_checks.scss +228 -0
  73. package/scss/_clamp.scss +9 -3
  74. package/scss/_data_grid.scss +15 -7
  75. package/scss/_drop_down.scss +12 -55
  76. package/scss/_expander_row.scss +5 -50
  77. package/scss/_icon.scss +51 -0
  78. package/scss/_icons.generated.scss +6 -0
  79. package/scss/_inline_view_switcher.scss +34 -0
  80. package/scss/_labels.scss +135 -0
  81. package/scss/_menu_button.scss +10 -70
  82. package/scss/_popover.scss +186 -0
  83. package/scss/_preferences.scss +29 -0
  84. package/scss/_preferences_dialog.scss +17 -0
  85. package/scss/_progress_bar.scss +161 -0
  86. package/scss/_reset.scss +18 -7
  87. package/scss/_spinner.scss +7 -3
  88. package/scss/_split_button.scss +6 -50
  89. package/scss/_switch.scss +90 -0
  90. package/scss/_switch_row.scss +6 -52
  91. package/scss/_tab_view.scss +64 -0
  92. package/scss/_theme.scss +10 -0
  93. package/scss/_toolbar_view.scss +74 -0
  94. package/scss/_variables.scss +34 -0
  95. package/scss/_view_switcher.scss +40 -0
  96. package/scss/_view_switcher_bar.scss +37 -0
  97. package/scss/_wrap_box.scss +56 -6
  98. package/scss/adwaita-skin.scss +23 -0
  99. package/src/adw-about-dialog.spec.ts +350 -0
  100. package/src/adw-action-rows.spec.ts +342 -0
  101. package/src/adw-avatar.spec.ts +139 -0
  102. package/src/adw-banner.spec.ts +152 -0
  103. package/src/adw-button-content.spec.ts +183 -0
  104. package/src/adw-button-row.spec.ts +12 -6
  105. package/src/adw-carousel.spec.ts +389 -0
  106. package/src/adw-checks.spec.ts +368 -0
  107. package/src/adw-drop-down.spec.ts +123 -0
  108. package/src/adw-header-bar.spec.ts +83 -0
  109. package/src/adw-icon.spec.ts +167 -0
  110. package/src/adw-progress-bar.spec.ts +173 -0
  111. package/src/adw-sidebar.spec.ts +332 -0
  112. package/src/adw-switch.spec.ts +242 -0
  113. package/src/adw-tab-view.spec.ts +12 -3
  114. package/src/adw-view-switcher-bar.spec.ts +61 -13
  115. package/src/adw-wrap-box.spec.ts +194 -0
  116. package/src/bottom-sheet.spec.ts +325 -0
  117. package/src/breakpoints.spec.ts +147 -0
  118. package/src/breakpoints.ts +67 -0
  119. package/src/chrome.spec.ts +301 -0
  120. package/src/elements/adw-about-dialog.ts +223 -163
  121. package/src/elements/adw-action-row.ts +120 -10
  122. package/src/elements/adw-avatar.ts +48 -54
  123. package/src/elements/adw-banner.ts +46 -10
  124. package/src/elements/adw-bottom-sheet.ts +145 -60
  125. package/src/elements/adw-button-content.ts +107 -33
  126. package/src/elements/adw-button-row.ts +61 -37
  127. package/src/elements/adw-button.ts +5 -7
  128. package/src/elements/adw-carousel.ts +467 -105
  129. package/src/elements/adw-checks.ts +299 -0
  130. package/src/elements/adw-clamp.ts +80 -10
  131. package/src/elements/adw-combo-row.ts +15 -7
  132. package/src/elements/adw-data-grid.ts +43 -91
  133. package/src/elements/adw-drop-down.ts +117 -128
  134. package/src/elements/adw-entry-row.ts +404 -36
  135. package/src/elements/adw-expander-row.ts +61 -29
  136. package/src/elements/adw-header-bar.ts +31 -6
  137. package/src/elements/adw-icon.ts +148 -0
  138. package/src/elements/adw-inline-view-switcher.ts +240 -129
  139. package/src/elements/adw-menu-button.ts +93 -70
  140. package/src/elements/adw-navigation-split-view.ts +48 -7
  141. package/src/elements/adw-navigation-view.ts +286 -148
  142. package/src/elements/adw-overlay-split-view.ts +164 -14
  143. package/src/elements/adw-password-entry-row.ts +111 -89
  144. package/src/elements/adw-popover.ts +283 -0
  145. package/src/elements/adw-preferences-dialog.ts +120 -21
  146. package/src/elements/adw-preferences-group.ts +128 -22
  147. package/src/elements/adw-progress-bar.ts +189 -0
  148. package/src/elements/adw-sidebar.ts +301 -142
  149. package/src/elements/adw-spin-row.ts +11 -5
  150. package/src/elements/adw-spinner.ts +23 -8
  151. package/src/elements/adw-split-button.ts +371 -92
  152. package/src/elements/adw-status-page.ts +9 -9
  153. package/src/elements/adw-switch-row.ts +100 -30
  154. package/src/elements/adw-switch.ts +129 -0
  155. package/src/elements/adw-tab-view.ts +682 -129
  156. package/src/elements/adw-toggle-group.ts +5 -7
  157. package/src/elements/adw-toolbar-view.ts +84 -21
  158. package/src/elements/adw-view-stack.ts +162 -85
  159. package/src/elements/adw-view-switcher-bar.ts +163 -81
  160. package/src/elements/adw-view-switcher.ts +231 -112
  161. package/src/elements/adw-window-title.ts +44 -11
  162. package/src/elements/adw-wrap-box.ts +131 -35
  163. package/src/elements/view-switcher-dom.ts +108 -0
  164. package/src/entry-rows.spec.ts +391 -0
  165. package/src/index.ts +23 -0
  166. package/src/navigation-view.spec.ts +435 -0
  167. package/src/preferences-search.ts +135 -0
  168. package/src/preferences.spec.ts +250 -0
  169. package/src/source-view/adw-source-view.ts +2 -5
  170. package/src/split-button.spec.ts +470 -0
  171. package/src/split-views.spec.ts +309 -0
  172. package/src/styles.generated.ts +1 -1
  173. package/src/tab-view.spec.ts +451 -0
  174. package/src/test.browser.mts +49 -0
  175. package/src/view-stack.spec.ts +272 -0
  176. package/src/view-switcher.spec.ts +446 -0
@@ -5,6 +5,20 @@
5
5
  // real Adwaita-styled popover positioned under the button; the popover is
6
6
  // dismissed on an outside click or Escape and supports arrow-key navigation.
7
7
  //
8
+ // The popover is `<adw-popover>` and the dismissal/keyboard machine is
9
+ // `@gjsify/adwaita-core`'s (ADR 0004). This element used to build both by hand,
10
+ // as did `<adw-drop-down>` and `<adw-split-button>` — three surfaces that
11
+ // disagreed with libadwaita and with each other on radius, shadow and padding,
12
+ // and two copies of the same `(current ± 1 + n) % n` arithmetic. This element
13
+ // keeps only what is a menu button: the icon, the item model, the title.
14
+ //
15
+ // THE ICON WENT THE SAME WAY, and it is where the drift actually bit: this
16
+ // element interpolated `class="adw-icon adw-menu-button-icon adw-icon--<name>"`
17
+ // while `<adw-split-button>` — alone among six copies — first checked the name
18
+ // was one CSS token. So `icon-name="a b"` shipped a stray `b` class here, and so
19
+ // did every JSON menu entry's `icon`. Both are `<adw-icon>` now, and the guard
20
+ // is `normalizeIconName`'s.
21
+ //
8
22
  // Attributes:
9
23
  // icon-name — symbolic icon (no `-symbolic` suffix), default `open-menu`.
10
24
  // menu-title — optional heading shown atop the popover.
@@ -12,6 +26,10 @@
12
26
  // disabled — boolean; a disabled button does not open the menu.
13
27
  // flat — boolean style class (default: flat, like the header app menu).
14
28
  // circular — boolean style class (round 34×34 button).
29
+ // direction — none | up | down | left | right (default down); where the
30
+ // popover opens. `none` behaves as `down`, which is
31
+ // Gtk.MenuButton's own rule, not ours — see
32
+ // {@link menuButtonPopupDirection}.
15
33
  // Properties (mirroring Adw.MenuButton / the NS twin):
16
34
  // menuItems — the menu entries ({ id?, label, icon? }[]) (get/set).
17
35
  // menuTitle — the popover heading (get/set).
@@ -26,6 +44,21 @@
26
44
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
27
45
  // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
28
46
 
47
+ import { isSplitButtonDirection, menuButtonPopupDirection } from '@gjsify/adwaita-core';
48
+ import type { SplitButtonDirection } from '@gjsify/adwaita-core';
49
+
50
+ // SIDE-EFFECT import, deliberately separate from the type import below: it is
51
+ // what guarantees `adw-popover` is defined before this module's own
52
+ // `customElements.define` can upgrade a server-rendered `<adw-menu-button>` and
53
+ // build one. A combined `import { AdwPopover }` would NOT do it — the binding is
54
+ // only ever used in type position here, and this package compiles without
55
+ // `verbatimModuleSyntax`, so TypeScript would elide the whole statement and take
56
+ // the registration with it.
57
+ import './adw-popover.js';
58
+ import type { AdwPopover } from './adw-popover.js';
59
+
60
+ import { createAdwIcon } from './adw-icon.js';
61
+
29
62
  /** A menu entry. `id` defaults to `label` on activation; `icon` is optional. */
30
63
  export interface AdwMenuItem {
31
64
  id?: string;
@@ -33,31 +66,38 @@ export interface AdwMenuItem {
33
66
  icon?: string;
34
67
  }
35
68
 
69
+ /**
70
+ * Where the popover sits, per `GtkArrowType`. `menuButtonPopupDirection` folds
71
+ * `none` onto `down` (adw-split-button.c:415 — a pass-through to
72
+ * `gtk_menu_button_set_direction`, so it is GtkMenuButton's rule); this table is
73
+ * only the direction→CSS-axis mapping, which is a renderer fact.
74
+ *
75
+ * `left`/`right` become `start`/`end` because the surface is placed with
76
+ * logical properties, so it follows the writing direction the way GTK's
77
+ * `:dir(rtl)` popovers do.
78
+ */
79
+ const POPOVER_POSITIONS = {
80
+ down: 'bottom',
81
+ up: 'top',
82
+ left: 'start',
83
+ right: 'end',
84
+ } as const;
85
+
36
86
  export class AdwMenuButton extends HTMLElement {
37
87
  private _buttonEl!: HTMLButtonElement;
38
- private _popoverEl!: HTMLDivElement;
88
+ private _popoverEl!: AdwPopover;
39
89
  private _items: AdwMenuItem[] = [];
40
90
  private _menuTitle = '';
41
- private _active = false;
42
91
  private _initialized = false;
43
92
  private _itemButtons: HTMLButtonElement[] = [];
44
- private _onDocumentPointerDown = (event: Event): void => {
45
- if (!this.contains(event.target as Node)) this._setActive(false);
46
- };
47
- private _onDocumentKeyDown = (event: KeyboardEvent): void => {
48
- if (event.key === 'Escape') {
49
- this._setActive(false);
50
- this._buttonEl.focus();
51
- }
52
- };
53
93
 
54
94
  static get observedAttributes() {
55
- return ['icon-name', 'menu-title', 'menu', 'disabled', 'flat', 'circular'];
95
+ return ['icon-name', 'menu-title', 'menu', 'disabled', 'flat', 'circular', 'direction'];
56
96
  }
57
97
 
58
98
  /** Whether the popover menu is currently open. */
59
99
  get active(): boolean {
60
- return this._active;
100
+ return this._popoverEl?.open ?? false;
61
101
  }
62
102
 
63
103
  /** The menu entries. Setting rebuilds the popover. */
@@ -80,6 +120,16 @@ export class AdwMenuButton extends HTMLElement {
80
120
  this.setAttribute('menu-title', this._menuTitle);
81
121
  }
82
122
 
123
+ /** Where the popover opens, and (via core) which way `none` folds. */
124
+ get direction(): SplitButtonDirection {
125
+ const value = this.getAttribute('direction');
126
+ return isSplitButtonDirection(value) ? value : 'down';
127
+ }
128
+
129
+ set direction(value: SplitButtonDirection) {
130
+ this.setAttribute('direction', value);
131
+ }
132
+
83
133
  connectedCallback() {
84
134
  if (this._initialized) return;
85
135
  this._initialized = true;
@@ -92,17 +142,23 @@ export class AdwMenuButton extends HTMLElement {
92
142
  this._buttonEl.setAttribute('aria-haspopup', 'menu');
93
143
  this._buttonEl.setAttribute('aria-expanded', 'false');
94
144
 
95
- this._popoverEl = document.createElement('div');
96
- this._popoverEl.className = 'adw-menu-button-popover';
145
+ this._popoverEl = document.createElement('adw-popover') as AdwPopover;
146
+ this._popoverEl.classList.add('adw-menu-button-popover');
97
147
  this._popoverEl.setAttribute('role', 'menu');
98
- this._popoverEl.hidden = true;
99
- this._popoverEl.addEventListener('keydown', (e) => this._onMenuKeyDown(e));
148
+ // `gtk_menu_button_set_menu_model` builds a GtkPopoverMenu, whose CSS
149
+ // node is `popover.menu` — the surface _menus.scss styles throughout.
150
+ this._popoverEl.setAttribute('menu', '');
151
+ this._popoverEl.setAttribute('align', 'end'); // the header-end app menu idiom
100
152
 
101
153
  this.replaceChildren(this._buttonEl, this._popoverEl);
154
+ this._popoverEl.anchor = this._buttonEl;
155
+ this._popoverEl.subscribe((open) => this._onPopoverToggled(open));
102
156
 
103
157
  this._buttonEl.addEventListener('click', () => {
104
158
  if (this.hasAttribute('disabled')) return;
105
- this._setActive(!this._active);
159
+ // Nothing to show is not a menu — never open an empty popover.
160
+ if (!this._popoverEl.open && this._items.length === 0) return;
161
+ this._popoverEl.open = !this._popoverEl.open;
106
162
  });
107
163
 
108
164
  // Seed items from the `menu` attribute if the property was not set.
@@ -114,11 +170,6 @@ export class AdwMenuButton extends HTMLElement {
114
170
  this._render();
115
171
  }
116
172
 
117
- disconnectedCallback() {
118
- document.removeEventListener('pointerdown', this._onDocumentPointerDown);
119
- document.removeEventListener('keydown', this._onDocumentKeyDown);
120
- }
121
-
122
173
  attributeChangedCallback(name: string) {
123
174
  if (!this._initialized || !this._buttonEl) return;
124
175
  if (name === 'menu') {
@@ -143,52 +194,26 @@ export class AdwMenuButton extends HTMLElement {
143
194
  }
144
195
  }
145
196
 
146
- private _setActive(active: boolean): void {
147
- if (this._active === active) return;
148
- // Only open when there is something to show.
149
- if (active && this._items.length === 0) return;
150
- this._active = active;
151
- this._popoverEl.hidden = !active;
152
- this.classList.toggle('active', active);
153
- this._buttonEl.setAttribute('aria-expanded', String(active));
154
- if (active) {
155
- document.addEventListener('pointerdown', this._onDocumentPointerDown);
156
- document.addEventListener('keydown', this._onDocumentKeyDown);
157
- // Move focus into the menu for keyboard navigation.
158
- this._itemButtons[0]?.focus();
159
- } else {
160
- document.removeEventListener('pointerdown', this._onDocumentPointerDown);
161
- document.removeEventListener('keydown', this._onDocumentKeyDown);
162
- }
163
- }
164
-
165
- private _onMenuKeyDown(event: KeyboardEvent): void {
166
- const buttons = this._itemButtons;
167
- if (buttons.length === 0) return;
168
- const current = buttons.indexOf(document.activeElement as HTMLButtonElement);
169
- let next = -1;
170
- if (event.key === 'ArrowDown') next = (current + 1) % buttons.length;
171
- else if (event.key === 'ArrowUp') next = (current - 1 + buttons.length) % buttons.length;
172
- else if (event.key === 'Home') next = 0;
173
- else if (event.key === 'End') next = buttons.length - 1;
174
- else return;
175
- event.preventDefault();
176
- buttons[next]?.focus();
197
+ private _onPopoverToggled(open: boolean): void {
198
+ this.classList.toggle('active', open);
199
+ this._buttonEl.setAttribute('aria-expanded', String(open));
200
+ // Move focus into the menu for keyboard navigation.
201
+ if (open) this._itemButtons[0]?.focus();
177
202
  }
178
203
 
179
204
  private _render(): void {
180
205
  const disabled = this.hasAttribute('disabled');
181
206
  this.classList.toggle('disabled', disabled);
182
207
  this._buttonEl.disabled = disabled;
208
+ if (disabled) this._popoverEl.popdown();
183
209
  this.classList.toggle('flat', this.hasAttribute('flat'));
184
210
  this.classList.toggle('circular', this.hasAttribute('circular'));
211
+ this._popoverEl.position = POPOVER_POSITIONS[menuButtonPopupDirection(this.direction)];
185
212
 
186
- const icon = (this.getAttribute('icon-name') ?? 'open-menu').replace(/-symbolic$/, '');
187
213
  this._buttonEl.replaceChildren();
188
- const iconEl = document.createElement('span');
189
- iconEl.className = `adw-icon adw-menu-button-icon${icon ? ` adw-icon--${icon}` : ''}`;
190
- iconEl.setAttribute('aria-hidden', 'true'); // decorative (mask-image only)
191
- this._buttonEl.appendChild(iconEl);
214
+ this._buttonEl.appendChild(
215
+ createAdwIcon(this.getAttribute('icon-name') ?? 'open-menu', 'adw-menu-button-icon'),
216
+ );
192
217
  this._buttonEl.setAttribute('aria-label', this.getAttribute('menu-title') || 'Menu');
193
218
 
194
219
  this._renderMenu();
@@ -201,7 +226,7 @@ export class AdwMenuButton extends HTMLElement {
201
226
  const title = this.getAttribute('menu-title') ?? this._menuTitle;
202
227
  if (title) {
203
228
  const titleEl = document.createElement('div');
204
- titleEl.className = 'adw-menu-button-title';
229
+ titleEl.className = 'adw-popover-title adw-menu-button-title';
205
230
  titleEl.textContent = title;
206
231
  this._popoverEl.appendChild(titleEl);
207
232
  }
@@ -209,24 +234,22 @@ export class AdwMenuButton extends HTMLElement {
209
234
  for (const [index, entry] of this._items.entries()) {
210
235
  const item = document.createElement('button');
211
236
  item.type = 'button';
212
- item.className = 'adw-menu-button-item';
237
+ item.className = 'adw-popover-item adw-menu-button-item';
213
238
  item.setAttribute('role', 'menuitem');
214
239
  item.tabIndex = -1;
215
240
 
216
- const iconName = (entry.icon ?? '').replace(/-symbolic$/, '');
217
- if (iconName) {
218
- const iconEl = document.createElement('span');
219
- iconEl.className = `adw-icon adw-menu-button-item-icon adw-icon--${iconName}`;
220
- iconEl.setAttribute('aria-hidden', 'true');
221
- item.appendChild(iconEl);
222
- }
241
+ // An entry with no USABLE icon gets no icon node at all, rather than
242
+ // an empty 16px box — the element itself answers whether the name
243
+ // resolved, so the `-symbolic`-only and bad-token cases agree.
244
+ const entryIcon = createAdwIcon(entry.icon ?? null, 'adw-menu-button-item-icon');
245
+ if (entryIcon.resolvedIconName !== '') item.appendChild(entryIcon);
223
246
  const labelEl = document.createElement('span');
224
247
  labelEl.className = 'adw-menu-button-item-label';
225
248
  labelEl.textContent = entry.label;
226
249
  item.appendChild(labelEl);
227
250
 
228
251
  item.addEventListener('click', () => {
229
- this._setActive(false);
252
+ this._popoverEl.popdown();
230
253
  this._buttonEl.focus();
231
254
  this.dispatchEvent(
232
255
  new CustomEvent('menu-item-activated', {
@@ -240,7 +263,7 @@ export class AdwMenuButton extends HTMLElement {
240
263
  }
241
264
 
242
265
  // A menu-button with no entries can never open.
243
- if (this._items.length === 0) this._setActive(false);
266
+ if (this._items.length === 0) this._popoverEl.popdown();
244
267
  }
245
268
  }
246
269
 
@@ -2,20 +2,27 @@
2
2
  // visible when expanded; when `collapsed` it shows one pane at a time
3
3
  // (`show-content` decides which), mirroring Adw.NavigationSplitView's
4
4
  // push-navigation on narrow widths.
5
- // Attributes: collapsed, show-content, min-sidebar-width, max-sidebar-width.
5
+ // Attributes: collapsed, show-content, min-sidebar-width, max-sidebar-width,
6
+ // breakpoint (an Adwaita condition, e.g. "max-width: 720px", that collapses
7
+ // the view by itself — the counterpart to an Adw.Breakpoint add_setter()).
6
8
  // Slots: slot="sidebar", slot="content".
7
9
  // Reference: refs/adwaita-web/adwaita-web/docs/widgets/navigationsplitview.md
8
10
  // Reference: refs/libadwaita/src/stylesheet/widgets/_sidebars.scss
9
11
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
10
12
  // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
11
13
 
14
+ import { DEFAULT_MAX_SIDEBAR_WIDTH, DEFAULT_MIN_SIDEBAR_WIDTH, resolveSidebarBounds } from '@gjsify/adwaita-core';
15
+
16
+ import { bindBreakpointSetter } from '../breakpoints.js';
17
+
12
18
  export class AdwNavigationSplitView extends HTMLElement {
13
19
  private _sidebarEl!: HTMLDivElement;
14
20
  private _contentEl!: HTMLDivElement;
15
21
  private _initialized = false;
22
+ private _disposeBreakpoint: (() => void) | undefined;
16
23
 
17
24
  static get observedAttributes() {
18
- return ['collapsed', 'show-content', 'min-sidebar-width', 'max-sidebar-width'];
25
+ return ['collapsed', 'show-content', 'min-sidebar-width', 'max-sidebar-width', 'breakpoint'];
19
26
  }
20
27
 
21
28
  get collapsed(): boolean {
@@ -53,11 +60,18 @@ export class AdwNavigationSplitView extends HTMLElement {
53
60
  this.replaceChildren(this._sidebarEl, this._contentEl);
54
61
  this._syncWidth();
55
62
  this._syncClasses();
63
+ this._syncBreakpoint();
64
+ }
65
+
66
+ disconnectedCallback() {
67
+ this._disposeBreakpoint?.();
68
+ this._disposeBreakpoint = undefined;
56
69
  }
57
70
 
58
71
  attributeChangedCallback(name: string) {
59
72
  if (!this._initialized) return;
60
- if (name === 'min-sidebar-width' || name === 'max-sidebar-width') this._syncWidth();
73
+ if (name === 'breakpoint') this._syncBreakpoint();
74
+ else if (name === 'min-sidebar-width' || name === 'max-sidebar-width') this._syncWidth();
61
75
  else {
62
76
  this._syncClasses();
63
77
  // Collapsing flips whether the sidebar is width-capped, so re-apply.
@@ -74,16 +88,43 @@ export class AdwNavigationSplitView extends HTMLElement {
74
88
  this._sidebarEl.style.maxWidth = '';
75
89
  return;
76
90
  }
77
- const min = this.getAttribute('min-sidebar-width');
78
- const max = this.getAttribute('max-sidebar-width');
79
- this._sidebarEl.style.minWidth = min ? `${parseFloat(min)}px` : '';
80
- this._sidebarEl.style.maxWidth = max ? `${parseFloat(max)}px` : '';
91
+ // An ABSENT attribute used to mean "no bound at all", where
92
+ // Adw.NavigationSplitView always has 180 / 280
93
+ // (adw-navigation-split-view.c min-sidebar-width / max-sidebar-width).
94
+ // The core also normalises the pair — libadwaita never lets max fall
95
+ // below min, and CSS resolves that conflict the other way round.
96
+ const bounds = resolveSidebarBounds(
97
+ {
98
+ minSidebarWidth: this._widthAttr('min-sidebar-width', DEFAULT_MIN_SIDEBAR_WIDTH),
99
+ maxSidebarWidth: this._widthAttr('max-sidebar-width', DEFAULT_MAX_SIDEBAR_WIDTH),
100
+ },
101
+ 0,
102
+ { ceil: true },
103
+ );
104
+ this._sidebarEl.style.minWidth = `${bounds.min}px`;
105
+ this._sidebarEl.style.maxWidth = `${bounds.max}px`;
106
+ }
107
+
108
+ /** A pixel attribute, falling back to the widget's own default. */
109
+ private _widthAttr(name: string, fallback: number): number {
110
+ const raw = this.getAttribute(name);
111
+ if (raw === null) return fallback;
112
+ const value = parseFloat(raw);
113
+ return Number.isFinite(value) ? value : fallback;
81
114
  }
82
115
 
83
116
  private _syncClasses() {
84
117
  this.classList.toggle('collapsed', this.collapsed);
85
118
  this.classList.toggle('show-content', this.showContent);
86
119
  }
120
+
121
+ /** (Re)bind the `breakpoint` condition to `collapsed`. */
122
+ private _syncBreakpoint() {
123
+ this._disposeBreakpoint?.();
124
+ this._disposeBreakpoint = bindBreakpointSetter(this, this.getAttribute('breakpoint'), (active) => {
125
+ this.collapsed = active;
126
+ });
127
+ }
87
128
  }
88
129
 
89
130
  customElements.define('adw-navigation-split-view', AdwNavigationSplitView);