@gjsify/adwaita-web 0.31.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
  // layer covers the content and dismisses the sheet on click (unless `can-close`
6
6
  // is false, in which case a close attempt is signalled instead).
7
7
  //
8
+ // The open/closed state and the dismissal gate are NOT implemented here: they
9
+ // live in `@gjsify/adwaita-core` as `BottomSheetPresentation` +
10
+ // `resolveBottomSheetClose`, ported from the C source and shared with
11
+ // `@gjsify/adwaita-nativescript` (ADR 0004). This element owns only the DOM.
12
+ // Three things changed when it moved:
13
+ // - `open="false"` now means CLOSED. The old getter was `hasAttribute('open')`
14
+ // while every other boolean used `!== 'false'`, so one element carried two
15
+ // incompatible conventions and `<adw-bottom-sheet open="false">` rendered
16
+ // open (adw-bottom-sheet.c:1670 — `open = !!open` on a strict gboolean).
17
+ // - `notify::open` follows the STATE, not the attribute VALUE. Rewriting the
18
+ // attribute to another truthy spelling no longer fires a notification
19
+ // carrying an unchanged payload (adw-bottom-sheet.c:1672-1682).
20
+ // - the drag handle is decorative, not a close button — see below.
21
+ //
8
22
  // Children are declared as <adw-bottom-sheet-content> (the persistent content)
9
23
  // and <adw-bottom-sheet-sheet> (the slide-up sheet); both are consumed at
10
24
  // connect time. Unslotted children fall back to the content, mirroring the
@@ -13,22 +27,30 @@
13
27
  // Attributes:
14
28
  // open (boolean — whether the sheet is revealed; default closed)
15
29
  // modal (boolean — dim + block the content while open; default on)
16
- // can-close (boolean — user can dismiss via the handle / dimming / Esc;
17
- // default on. When off, a dismissal raises `close-attempt`
18
- // instead of closing — mirrors Adw.BottomSheet:can-close)
30
+ // can-close (boolean — user can dismiss via the dimming / Esc / the
31
+ // sheet.close action; default on. When off, a dismissal
32
+ // raises `close-attempt` instead of closing — mirrors
33
+ // Adw.BottomSheet:can-close)
19
34
  // show-drag-handle (boolean — overlay the drag handle on the sheet; default on)
20
35
  //
21
36
  // Events:
22
37
  // `notify::open` (CustomEvent, bubbles, `detail = { open }`) when the revealed
23
38
  // state changes — mirrors the Adw.BottomSheet `open` GObject property.
24
- // `close-attempt` (CustomEvent, bubbles) when a dismissal is attempted while
25
- // `can-close` is false — mirrors the Adw.BottomSheet `close-attempt` signal.
39
+ // `close-attempt` (CustomEvent, bubbles) when a dismissal is attempted that the
40
+ // sheet refuses — mirrors the Adw.BottomSheet `close-attempt` signal.
41
+ // `sheet.close` (CustomEvent, bubbles) when the sheet.close action is used on
42
+ // an ALREADY-closed sheet, so an enclosing sheet/dialog can handle it — the
43
+ // DOM equivalent of `gtk_widget_activate_action (parent, "sheet.close")`
44
+ // (adw-bottom-sheet.c:382-385).
26
45
  //
27
46
  // Reference: refs/libadwaita/src/adw-bottom-sheet.c (AdwBottomSheet behaviour)
28
47
  // Reference: refs/libadwaita/src/stylesheet/widgets/_bottom-sheet.scss
29
48
  // Copyright (c) 2023-2024 GNOME Foundation Inc. (libadwaita). LGPLv2.1+.
30
49
  // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
31
50
 
51
+ import { BottomSheetPresentation } from '@gjsify/adwaita-core';
52
+ import type { BottomSheetCloseOutcome, BottomSheetCloseSource } from '@gjsify/adwaita-core';
53
+
32
54
  /** The persistent content. Child of <adw-bottom-sheet>; consumed at connect time. */
33
55
  export class AdwBottomSheetContent extends HTMLElement {}
34
56
 
@@ -41,6 +63,10 @@ export class AdwBottomSheet extends HTMLElement {
41
63
  private _dimmingEl!: HTMLDivElement;
42
64
  private _sheetEl!: HTMLDivElement;
43
65
  private _dragHandleEl!: HTMLDivElement;
66
+ /** The shared open/can-close model — the single source of truth for both. */
67
+ private readonly _state = new BottomSheetPresentation();
68
+ /** Set while the element writes `open` back, so the write does not re-enter. */
69
+ private _reflecting = false;
44
70
 
45
71
  static get observedAttributes() {
46
72
  return ['open', 'modal', 'can-close', 'show-drag-handle'];
@@ -48,26 +74,25 @@ export class AdwBottomSheet extends HTMLElement {
48
74
 
49
75
  /** Whether the sheet is revealed. */
50
76
  get open(): boolean {
51
- return this.hasAttribute('open');
77
+ return this._state.open;
52
78
  }
53
79
 
54
80
  set open(value: boolean) {
55
- if (value) this.setAttribute('open', '');
56
- else this.removeAttribute('open');
81
+ this._setOpen(!!value);
57
82
  }
58
83
 
59
84
  /** Whether the content is dimmed + blocked while the sheet is open. */
60
85
  get modal(): boolean {
61
- return this._boolAttr('modal');
86
+ return this._boolAttr('modal', true);
62
87
  }
63
88
 
64
89
  set modal(value: boolean) {
65
90
  this._setBoolAttr('modal', value);
66
91
  }
67
92
 
68
- /** Whether the user can dismiss the sheet (handle / dimming / Escape). */
93
+ /** Whether the user can dismiss the sheet (dimming / Escape / sheet.close). */
69
94
  get canClose(): boolean {
70
- return this._boolAttr('can-close');
95
+ return this._state.canClose;
71
96
  }
72
97
 
73
98
  set canClose(value: boolean) {
@@ -76,13 +101,33 @@ export class AdwBottomSheet extends HTMLElement {
76
101
 
77
102
  /** Whether the overlaid drag handle is shown. */
78
103
  get showDragHandle(): boolean {
79
- return this._boolAttr('show-drag-handle');
104
+ return this._boolAttr('show-drag-handle', true);
80
105
  }
81
106
 
82
107
  set showDragHandle(value: boolean) {
83
108
  this._setBoolAttr('show-drag-handle', value);
84
109
  }
85
110
 
111
+ /**
112
+ * Route a dismissal affordance through the shared gate and act on the
113
+ * verdict: close, raise `close-attempt`, forward `sheet.close` to an
114
+ * ancestor, or do nothing. The element calls this for the dimming layer and
115
+ * Escape; a close button inside the sheet calls it with `'close-button'`.
116
+ *
117
+ * The verdict differs per source — a locked sheet signals a scrim click but
118
+ * swallows a swipe, and the drag handle never closes anything. See
119
+ * `resolveBottomSheetClose` in `@gjsify/adwaita-core`.
120
+ */
121
+ requestClose(source: BottomSheetCloseSource): BottomSheetCloseOutcome {
122
+ const outcome = this._state.requestClose(source);
123
+ if (outcome === 'close-attempt') {
124
+ this.dispatchEvent(new CustomEvent('close-attempt', { bubbles: true }));
125
+ } else if (outcome === 'delegate') {
126
+ this.dispatchEvent(new CustomEvent('sheet.close', { bubbles: true }));
127
+ }
128
+ return outcome;
129
+ }
130
+
86
131
  connectedCallback() {
87
132
  if (this._initialized) return;
88
133
  this._initialized = true;
@@ -90,9 +135,9 @@ export class AdwBottomSheet extends HTMLElement {
90
135
  // Snapshot declared children before taking over the subtree. The sheet
91
136
  // and content are placed by slot; anything unslotted falls back to the
92
137
  // content (the AdwBottomSheet GtkBuildable default).
93
- const sheetChildren = this._collectSlot('adw-bottom-sheet-sheet', 'sheet');
94
- const contentChildren = this._collectSlot('adw-bottom-sheet-content', 'content');
95
- const claimed = new Set<Node>([...sheetChildren, ...contentChildren]);
138
+ const claimed = new Set<Node>();
139
+ const sheetChildren = this._collectSlot('adw-bottom-sheet-sheet', 'sheet', claimed);
140
+ const contentChildren = this._collectSlot('adw-bottom-sheet-content', 'content', claimed);
96
141
  const unslotted = Array.from(this.childNodes).filter((n) => !claimed.has(n));
97
142
 
98
143
  // Persistent content layer.
@@ -105,25 +150,22 @@ export class AdwBottomSheet extends HTMLElement {
105
150
  // Clicking it dismisses the sheet (or raises close-attempt when locked).
106
151
  this._dimmingEl = document.createElement('div');
107
152
  this._dimmingEl.className = 'adw-bottom-sheet-dimming';
108
- this._dimmingEl.addEventListener('click', () => this._attemptClose());
153
+ this._dimmingEl.addEventListener('click', () => this.requestClose('dimming'));
109
154
 
110
155
  // The slide-up sheet with its overlaid drag handle.
111
156
  this._sheetEl = document.createElement('div');
112
157
  this._sheetEl.className = 'adw-bottom-sheet-sheet';
113
158
 
159
+ // libadwaita builds the handle with can_focus = FALSE and
160
+ // can_target = FALSE (adw-bottom-sheet.c:1197-1198): it is a decorative
161
+ // pill that cannot be clicked or focused at all. Its only behavioural
162
+ // role is elsewhere — `allow_mouse_drag = show_drag_handle || bottom_bar`
163
+ // (adw-bottom-sheet.c:455-457). This element used to give it
164
+ // role="button", tabindex=0 and a click-to-close handler, which is the
165
+ // one behaviour it provably does not have.
114
166
  this._dragHandleEl = document.createElement('div');
115
167
  this._dragHandleEl.className = 'adw-bottom-sheet-drag-handle';
116
- this._dragHandleEl.setAttribute('role', 'button');
117
- this._dragHandleEl.setAttribute('aria-label', 'Drag handle');
118
- this._dragHandleEl.tabIndex = 0;
119
- // The handle is the primary pointer affordance for closing the sheet.
120
- this._dragHandleEl.addEventListener('click', () => this._attemptClose());
121
- this._dragHandleEl.addEventListener('keydown', (event) => {
122
- if (event.key === 'Enter' || event.key === ' ') {
123
- event.preventDefault();
124
- this._attemptClose();
125
- }
126
- });
168
+ this._dragHandleEl.setAttribute('aria-hidden', 'true');
127
169
 
128
170
  const sheetBody = document.createElement('div');
129
171
  sheetBody.className = 'adw-bottom-sheet-sheet-body';
@@ -133,56 +175,83 @@ export class AdwBottomSheet extends HTMLElement {
133
175
 
134
176
  this.replaceChildren(this._contentEl, this._dimmingEl, this._sheetEl);
135
177
 
136
- // Escape closes the sheet when open (mirrors AdwBottomSheet's Escape
137
- // shortcut → maybe_close_cb).
178
+ // Escape → maybe_close_cb (adw-bottom-sheet.c:389-400). In GTK the
179
+ // shortcut sits on the sheet itself, so it fires exactly while the sheet
180
+ // has focus — INCLUDING while the sheet is closed, which is the corner
181
+ // that still emits `close-attempt`. This port has no focus transfer into
182
+ // the sheet, so an open sheet counts as the focused surface and a closed
183
+ // one only when focus is still inside its (off-screen) body.
138
184
  this.addEventListener('keydown', (event) => {
139
- if (event.key === 'Escape' && this.open) {
140
- event.stopPropagation();
141
- this._attemptClose();
142
- }
185
+ if (event.key !== 'Escape') return;
186
+ if (!this.open && !this._sheetEl.contains(event.target as Node)) return;
187
+ event.stopPropagation();
188
+ this.requestClose('escape');
189
+ });
190
+
191
+ // Adopt whatever the author declared, then start mirroring state changes
192
+ // back out. Seeding before the subscription keeps the initial value
193
+ // silent, as an attribute set during upgrade has always been.
194
+ this._state.setCanClose(this._boolAttr('can-close', true));
195
+ this._state.setOpen(this._boolAttr('open', false));
196
+ this._state.subscribe((open) => {
197
+ this._render();
198
+ this.dispatchEvent(new CustomEvent('notify::open', { bubbles: true, detail: { open } }));
143
199
  });
144
200
 
145
201
  this._render();
146
202
  }
147
203
 
148
204
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
149
- if (!this._initialized) return;
205
+ // Our own write-back of `open`; the state already holds this value.
206
+ if (this._reflecting) return;
150
207
  if (oldValue === newValue) return;
151
- this._render();
208
+
209
+ // The state is authoritative even before the DOM exists, so an attribute
210
+ // set during upgrade is not lost. Only the rendering half waits.
152
211
  if (name === 'open') {
153
- this.dispatchEvent(new CustomEvent('notify::open', { bubbles: true, detail: { open: this.open } }));
212
+ // A no-op change (one truthy spelling for another) leaves the state
213
+ // alone, so nothing renders and nothing notifies.
214
+ this._state.setOpen(this._boolAttr('open', false));
215
+ return;
154
216
  }
217
+ if (name === 'can-close') this._state.setCanClose(this._boolAttr('can-close', true));
218
+ if (this._initialized) this._render();
155
219
  }
156
220
 
157
- private _attemptClose(): void {
158
- if (!this.open) return;
159
- if (!this.canClose) {
160
- this.dispatchEvent(new CustomEvent('close-attempt', { bubbles: true }));
161
- return;
162
- }
163
- // Keep the attribute in sync without re-entering the guarded callback
164
- // (it no-ops on an unchanged value, but the notify::open fires from the
165
- // attribute write below).
166
- this.open = false;
221
+ /** The programmatic open/close — `AdwBottomSheet:open`, ungated by `can-close`. */
222
+ private _setOpen(open: boolean): void {
223
+ if (!this._state.setOpen(open)) return;
224
+ // Once connected the subscription renders (which reflects the attribute)
225
+ // and notifies; before that there is no DOM, so only the attribute has
226
+ // to be kept in step.
227
+ if (!this._initialized) this._reflectOpen(open);
167
228
  }
168
229
 
169
230
  private _render(): void {
170
- this.classList.toggle('open', this.open);
231
+ const open = this._state.open;
232
+ this._reflectOpen(open);
233
+ this.classList.toggle('open', open);
171
234
  this.classList.toggle('modal', this.modal);
172
235
  this.classList.toggle('has-drag-handle', this.showDragHandle);
173
236
 
174
237
  // The dimming layer is only meaningful while modal; it fades in with the
175
238
  // sheet and is non-interactive when closed.
176
- this._dimmingEl.classList.toggle('visible', this.open && this.modal);
239
+ this._dimmingEl.classList.toggle('visible', open && this.modal);
177
240
  this._dragHandleEl.hidden = !this.showDragHandle;
178
241
  }
179
242
 
180
- /** Read a boolean attribute that defaults to `true` when absent. */
181
- private _boolAttr(name: string): boolean {
243
+ /** Mirror the state onto the `open` attribute (the CSS + markup hook). */
244
+ private _reflectOpen(open: boolean): void {
245
+ this._reflecting = true;
246
+ if (open) this.setAttribute('open', '');
247
+ else this.removeAttribute('open');
248
+ this._reflecting = false;
249
+ }
250
+
251
+ /** Read a boolean attribute: absent → `fallback`, explicit `="false"` → off. */
252
+ private _boolAttr(name: string, fallback: boolean): boolean {
182
253
  const value = this.getAttribute(name);
183
- // Absent attribute → default on; explicit `="false"` → off.
184
- if (value === null) return name === 'modal' || name === 'can-close' || name === 'show-drag-handle';
185
- return value !== 'false';
254
+ return value === null ? fallback : value !== 'false';
186
255
  }
187
256
 
188
257
  private _setBoolAttr(name: string, value: boolean): void {
@@ -190,13 +259,29 @@ export class AdwBottomSheet extends HTMLElement {
190
259
  else this.setAttribute(name, 'false');
191
260
  }
192
261
 
193
- /** Move declared slot children out of the light-DOM tree into a list. */
194
- private _collectSlot(tag: string, slot: string): Node[] {
195
- const byElement = Array.from(this.querySelectorAll(`:scope > ${tag}`)).flatMap((el) =>
196
- Array.from(el.childNodes),
197
- );
198
- const bySlotAttr = Array.from(this.querySelectorAll(`:scope > [slot="${slot}"]`));
199
- return [...byElement, ...bySlotAttr];
262
+ /**
263
+ * Collect the nodes declared for one slot, in DOCUMENT order, marking every
264
+ * consumed child in `claimed`.
265
+ *
266
+ * The `<adw-bottom-sheet-sheet>` / `<adw-bottom-sheet-content>` wrappers are
267
+ * markup, not widgets — GtkBuilder's `<child type="sheet">` leaves nothing in
268
+ * the tree (adw-bottom-sheet.c:1278-1293) — so the wrapper itself is claimed
269
+ * too. It used to fall through to `unslotted` and be appended to the content
270
+ * layer, leaving an empty custom element behind after its children were
271
+ * pulled out.
272
+ */
273
+ private _collectSlot(tag: string, slot: string, claimed: Set<Node>): Node[] {
274
+ const nodes: Node[] = [];
275
+ for (const child of Array.from(this.children)) {
276
+ if (child.tagName.toLowerCase() === tag) {
277
+ claimed.add(child);
278
+ nodes.push(...Array.from(child.childNodes));
279
+ } else if (child.getAttribute('slot') === slot) {
280
+ claimed.add(child);
281
+ nodes.push(child);
282
+ }
283
+ }
284
+ return nodes;
200
285
  }
201
286
  }
202
287
 
@@ -1,67 +1,141 @@
1
1
  // <adw-button-content> — a box-like child pairing a symbolic icon with a label,
2
- // intended as the child of an <adw-button> (or Gtk.Button / MenuButton /
3
- // SplitButton in libadwaita) when it needs both an icon and a label. With
4
- // `can-shrink` the label ellipsizes instead of forcing the button wider.
5
- // Attributes: icon-name (symbolic name, e.g. "folder-download"; empty hides the
6
- // icon), label (empty hides the label), can-shrink (boolean — label
7
- // ellipsizes when set).
8
- // Events: `notify::icon-name`, `notify::label`, `notify::can-shrink`
9
- // (CustomEvent, bubbles) mirroring the AdwButtonContent GObject notify signals
10
- // the native story observes.
2
+ // intended as the child of a button (or Gtk.Button / MenuButton / SplitButton in
3
+ // libadwaita) when it needs both an icon and a label.
4
+ // Attributes: icon-name (symbolic name, e.g. "folder-download"; EMPTY shows the
5
+ // `image-missing` fallback, it does not hide the icon), label (empty hides the
6
+ // label), use-underline (boolean — `_` marks a mnemonic), can-shrink (boolean —
7
+ // label ellipsizes when set).
8
+ // Events: `notify::icon-name`, `notify::label`, `notify::use-underline`,
9
+ // `notify::can-shrink` (CustomEvent, bubbles) mirroring the AdwButtonContent
10
+ // GObject notify signals the native story observes.
11
+ //
12
+ // The derivations come from `@gjsify/adwaita-core` (ADR 0004). THE STYLE CLASS IS
13
+ // WHY THIS WAS TOUCHED: `AdwButtonContent` puts `image-text-button` on the button
14
+ // hosting it (adw-button-content.c:115) and removes it on unroot (:126), and the
15
+ // stylesheet gives that class `padding-left/right: 9px` where a plain text button
16
+ // has 17px (_buttons.scss:77-80 against :72-75). `grep -rn "image-text-button"`
17
+ // over this package used to return NOTHING, so every icon+label button here was
18
+ // drawn with a plain button's padding.
19
+ //
11
20
  // Reference: refs/libadwaita/src/adw-button-content.c (AdwButtonContent)
12
- // Reference: refs/libadwaita/src/stylesheet/widgets/_buttons.scss (buttoncontent)
21
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_buttons.scss
22
+ // (image-text-button :77-91 · splitbutton override :499-507 ·
23
+ // buttoncontent :626-645)
13
24
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
14
- // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
25
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the
26
+ // icon node is <adw-icon>.
27
+
28
+ import { BUTTON_CONTENT_STYLE_CLASS, buttonContentIconName, buttonContentLabelText } from '@gjsify/adwaita-core';
29
+
30
+ import { type AdwIcon, createAdwIcon } from './adw-icon.js';
31
+
32
+ /**
33
+ * What `gtk_widget_get_ancestor (…, GTK_TYPE_BUTTON)` (:95) looks like in the
34
+ * DOM: a native `<button>`, or anything wearing the Adwaita button class (the
35
+ * `<adw-button>` element renders an inner `<button class="adw-button">`, and the
36
+ * stories style a plain one with the same class set).
37
+ */
38
+ const BUTTON_SELECTOR = 'button, .adw-button';
39
+
40
+ /** The split-button node the class is RETARGETED to (:112-113). */
41
+ const SPLIT_BUTTON_SELECTOR = 'adw-split-button, .adw-split-button';
15
42
 
16
43
  export class AdwButtonContent extends HTMLElement {
17
44
  private _boxEl!: HTMLDivElement;
18
- private _iconEl!: HTMLSpanElement;
45
+ private _iconEl!: AdwIcon;
19
46
  private _labelEl!: HTMLSpanElement;
20
47
  private _initialized = false;
48
+ /** The node currently carrying `image-text-button`, so unroot can find it. */
49
+ private _styledHost: HTMLElement | null = null;
21
50
 
22
51
  static get observedAttributes() {
23
- return ['icon-name', 'label', 'can-shrink'];
52
+ return ['icon-name', 'label', 'use-underline', 'can-shrink'];
24
53
  }
25
54
 
26
55
  connectedCallback() {
27
- if (this._initialized) return;
28
- this._initialized = true;
56
+ if (!this._initialized) {
57
+ this._initialized = true;
29
58
 
30
- // CSS node mirrors libadwaita's `buttoncontent > box > {image, label}`.
31
- this._boxEl = document.createElement('div');
32
- this._boxEl.className = 'adw-button-content-box';
59
+ // CSS node mirrors libadwaita's `buttoncontent > box > {image, label}`.
60
+ this._boxEl = document.createElement('div');
61
+ this._boxEl.className = 'adw-button-content-box';
33
62
 
34
- this._iconEl = document.createElement('span');
35
- // The image node is GTK_ACCESSIBLE_ROLE_PRESENTATION in libadwaita —
36
- // it is decorative, the label carries the accessible name.
37
- this._iconEl.setAttribute('aria-hidden', 'true');
63
+ // The image node is GTK_ACCESSIBLE_ROLE_PRESENTATION in libadwaita —
64
+ // it is decorative, the label carries the accessible name, which is
65
+ // what <adw-icon>'s own `aria-hidden` says.
66
+ this._iconEl = createAdwIcon(null);
38
67
 
39
- this._labelEl = document.createElement('span');
40
- this._labelEl.className = 'adw-button-content-label';
68
+ this._labelEl = document.createElement('span');
69
+ this._labelEl.className = 'adw-button-content-label';
41
70
 
42
- this._boxEl.append(this._iconEl, this._labelEl);
43
- this.replaceChildren(this._boxEl);
44
- this._render();
71
+ this._boxEl.append(this._iconEl, this._labelEl);
72
+ this.replaceChildren(this._boxEl);
73
+ this._render();
74
+ }
75
+ // `adw_button_content_root` runs on every rooting, not just the first
76
+ // (:99-116), so a re-parented content re-stamps its new host.
77
+ this._root();
78
+ }
79
+
80
+ disconnectedCallback() {
81
+ this._unroot();
45
82
  }
46
83
 
47
84
  attributeChangedCallback(name: string) {
48
85
  if (!this._initialized) return;
49
86
  this._render();
50
87
  // Mirror the AdwButtonContent notify::<prop> signals the native story
51
- // listens to (icon-name / label / can-shrink).
88
+ // listens to.
52
89
  this.dispatchEvent(new CustomEvent(`notify::${name}`, { bubbles: true }));
53
90
  }
54
91
 
92
+ /**
93
+ * `adw_button_content_root` (:108-116): the nearest button ancestor, unless
94
+ * that button's DIRECT parent is a split button — then the split button
95
+ * takes the class, because `splitbutton.image-text-button > button` is the
96
+ * rule that styles it (_buttons.scss:499-507).
97
+ */
98
+ private _hostButton(): HTMLElement | null {
99
+ const button = this.parentElement?.closest<HTMLElement>(BUTTON_SELECTOR) ?? null;
100
+ if (!button) return null;
101
+ const parent = button.parentElement;
102
+ return parent?.matches(SPLIT_BUTTON_SELECTOR) ? parent : button;
103
+ }
104
+
105
+ private _root(): void {
106
+ const host = this._hostButton();
107
+ if (host === this._styledHost) return;
108
+ this._unroot();
109
+ // A content with no button ancestor styles nothing. The C would pass
110
+ // NULL to `gtk_widget_get_parent`/`add_css_class` here (:112-115) — two
111
+ // CRITICALs — so there is nothing to reproduce.
112
+ if (!host) return;
113
+ host.classList.add(BUTTON_CONTENT_STYLE_CLASS);
114
+ this._styledHost = host;
115
+ }
116
+
117
+ private _unroot(): void {
118
+ // `adw_button_content_unroot` (:126) — a button that loses its content
119
+ // goes back to plain-button padding.
120
+ this._styledHost?.classList.remove(BUTTON_CONTENT_STYLE_CLASS);
121
+ this._styledHost = null;
122
+ }
123
+
55
124
  private _render() {
56
- const icon = this.getAttribute('icon-name') ?? '';
57
- this._iconEl.className = icon ? `adw-icon adw-icon--${icon}` : '';
58
- this._iconEl.hidden = icon.length === 0;
125
+ // An empty `icon-name` resolves to `image-missing` and the image node
126
+ // stays visible (:355-356). The docs at :228/:343 say the icon is not
127
+ // shown; the code never hides it, and this follows the code — see the
128
+ // `BUTTON_CONTENT_ICON_VECTORS` rule string.
129
+ this._iconEl.iconName = buttonContentIconName(this.getAttribute('icon-name') ?? '');
130
+ this._iconEl.hidden = false;
59
131
 
60
132
  const label = this.getAttribute('label') ?? '';
61
- this._labelEl.textContent = label;
133
+ this._labelEl.textContent = buttonContentLabelText(label, this.hasAttribute('use-underline'));
134
+ // `gtk_widget_set_visible (self->label, label[0])` (:398) — the RAW
135
+ // label decides, not the mnemonic-resolved text.
62
136
  this._labelEl.hidden = label.length === 0;
63
137
 
64
- // can-shrink → ellipsize the label (Gtk.Label PANGO_ELLIPSIZE_END).
138
+ // can-shrink → ellipsize the label (Gtk.Label PANGO_ELLIPSIZE_END, :490).
65
139
  this.classList.toggle('can-shrink', this.hasAttribute('can-shrink'));
66
140
  }
67
141
  }
@@ -1,24 +1,49 @@
1
1
  // <adw-button-row> — A boxed-list row that behaves like a button: a centered
2
- // title with an optional start icon, spanning the full width of the list. The
3
- // `suggested-action` / `destructive-action` style classes recolor it like the
4
- // matching Adwaita button variants.
5
- // Attributes: title, start-icon-name (symbolic name, e.g. "list-add"),
6
- // activatable (default on — matches Adw.ButtonRow).
2
+ // title between an optional start icon and an optional end icon, spanning the
3
+ // full width of the list. The `suggested-action` / `destructive-action` style
4
+ // classes recolor it like the matching Adwaita button variants.
5
+ // Attributes: title, start-icon-name, end-icon-name (symbolic names, e.g.
6
+ // "list-add" / "go-next").
7
7
  // Events: `activated` (CustomEvent, bubbles) when the row is clicked — mirrors
8
8
  // the Adw.ButtonRow `activated` GObject signal.
9
- // Reference: refs/libadwaita/src/adw-button-row.ui (AdwButtonRow template)
9
+ //
10
+ // The label/icon visibility rules are HEADLESS and live in
11
+ // `@gjsify/adwaita-core` (ADR 0004) as {@link ButtonRowState}; this element
12
+ // composes it and keeps only the DOM render half.
13
+ // `@gjsify/adwaita-nativescript` composes the same state, so both ports share one
14
+ // behaviour. Two things this fixes, both from adw-button-row.c:
15
+ // - `end-icon-name` (C:213-223, bound at adw-button-row.ui:52-65) exists since
16
+ // libadwaita 1.6 and neither renderer had it, so the trailing-chevron shape
17
+ // the property exists for could not be expressed.
18
+ // - THERE IS NO `activatable` OPT-OUT. `<property name="activatable">True
19
+ // </property>` is in the template (adw-button-row.ui:5), the class docs say
20
+ // "AdwButtonRow is always activatable." (C:31), and the whole public surface
21
+ // (C:270-352) is `new` plus the two icon-name pairs. This element used to
22
+ // honour an invented `activatable="false"`, which also gave the same markup
23
+ // two opposite meanings inside one package — `<adw-action-row>` reads
24
+ // `activatable` by PRESENCE, so `activatable="false"` there meant TRUE.
25
+ //
26
+ // Reference: refs/libadwaita/src/adw-button-row.c, adw-button-row.ui
10
27
  // Reference: refs/libadwaita/src/stylesheet/widgets/_lists.scss (row.button)
11
28
  // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
12
- // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
29
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the
30
+ // label/icon state composed from @gjsify/adwaita-core.
31
+
32
+ import { BUTTON_ROW_ACTIVATABLE, ButtonRowState } from '@gjsify/adwaita-core';
33
+
34
+ import { type AdwIcon, createAdwIcon } from './adw-icon.js';
13
35
 
14
36
  export class AdwButtonRow extends HTMLElement {
15
37
  private _contentsEl!: HTMLDivElement;
16
- private _startIconEl!: HTMLSpanElement;
38
+ private _startIconEl!: AdwIcon;
39
+ private _endIconEl!: AdwIcon;
17
40
  private _titleEl!: HTMLSpanElement;
41
+ /** The headless title + start/end icon state (ADR 0004). */
42
+ private readonly _state = new ButtonRowState();
18
43
  private _initialized = false;
19
44
 
20
45
  static get observedAttributes() {
21
- return ['title', 'start-icon-name', 'activatable'];
46
+ return ['title', 'start-icon-name', 'end-icon-name'];
22
47
  }
23
48
 
24
49
  connectedCallback() {
@@ -27,33 +52,21 @@ export class AdwButtonRow extends HTMLElement {
27
52
  this._contentsEl = document.createElement('div');
28
53
  this._contentsEl.className = 'adw-button-row-contents';
29
54
 
30
- this._startIconEl = document.createElement('span');
31
- this._startIconEl.setAttribute('aria-hidden', 'true');
55
+ this._startIconEl = createAdwIcon(null, 'start');
32
56
 
33
57
  this._titleEl = document.createElement('span');
34
58
  this._titleEl.className = 'adw-button-row-title';
35
59
 
36
- this._contentsEl.append(this._startIconEl, this._titleEl);
37
- this.replaceChildren(this._contentsEl);
60
+ this._endIconEl = createAdwIcon(null, 'end');
38
61
 
39
- // Adw.ButtonRow is activatable by default; only an explicit
40
- // activatable="false" turns it off. Set this while still uninitialized
41
- // so the re-entrant attributeChangedCallback is a no-op — the explicit
42
- // _render() below does the first render, once the elements above exist.
43
- // (Setting _initialized before the elements were built made this
44
- // setAttribute re-enter _render() and crash on a declaratively-parsed
45
- // row, where connectedCallback runs with attributes already present.)
46
- if (!this.hasAttribute('activatable')) {
47
- this.setAttribute('activatable', '');
48
- }
62
+ this._contentsEl.append(this._startIconEl, this._titleEl, this._endIconEl);
63
+ this.replaceChildren(this._contentsEl);
49
64
 
50
65
  this._initialized = true;
51
66
  this._render();
52
67
 
53
68
  this.addEventListener('click', () => {
54
- if (this._isActivatable()) {
55
- this.dispatchEvent(new CustomEvent('activated', { bubbles: true }));
56
- }
69
+ this.dispatchEvent(new CustomEvent('activated', { bubbles: true }));
57
70
  });
58
71
  }
59
72
 
@@ -61,20 +74,31 @@ export class AdwButtonRow extends HTMLElement {
61
74
  if (this._initialized) this._render();
62
75
  }
63
76
 
64
- private _isActivatable(): boolean {
65
- return this.getAttribute('activatable') !== 'false';
66
- }
67
-
68
77
  private _render() {
69
- const title = this.getAttribute('title') ?? '';
70
- this._titleEl.textContent = title;
71
- this._titleEl.hidden = title.length === 0;
78
+ this._state.setTitle(this.getAttribute('title'));
79
+ this._state.setStartIconName(this.getAttribute('start-icon-name'));
80
+ this._state.setEndIconName(this.getAttribute('end-icon-name'));
81
+ const state = this._state.state;
82
+
83
+ this._titleEl.textContent = state.title;
84
+ this._titleEl.hidden = !state.titleVisible;
72
85
 
73
- const icon = this.getAttribute('start-icon-name') ?? '';
74
- this._startIconEl.className = icon ? `adw-icon adw-icon--${icon}` : '';
75
- this._startIconEl.hidden = icon.length === 0;
86
+ this._paintIcon(this._startIconEl, state.startIconName, state.startIconVisible);
87
+ this._paintIcon(this._endIconEl, state.endIconName, state.endIconVisible);
88
+
89
+ // Unconditional: an AdwButtonRow has no way to not be activatable.
90
+ this.classList.toggle('activatable', BUTTON_ROW_ACTIVATABLE);
91
+ }
76
92
 
77
- this.classList.toggle('activatable', this._isActivatable());
93
+ /**
94
+ * Paint one of the two `image.icon.{start,end}` nodes (adw-button-row.c:39-40).
95
+ *
96
+ * The `start` / `end` position class is set once at construction — it is
97
+ * where the node sits, not what it draws — so only the name changes here.
98
+ */
99
+ private _paintIcon(el: AdwIcon, iconName: string, visible: boolean) {
100
+ el.iconName = visible ? iconName : null;
101
+ el.hidden = !visible;
78
102
  }
79
103
  }
80
104