@gjsify/adwaita-web 0.36.0 → 0.38.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.
- package/dist/adwaita-web.css +504 -34
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/accent.d.ts +28 -0
- package/lib/types/adw-accent.spec.d.ts +2 -0
- package/lib/types/adw-shortcut-label.spec.d.ts +2 -0
- package/lib/types/breakpoints.d.ts +10 -12
- package/lib/types/elements/adw-about-dialog.d.ts +19 -36
- package/lib/types/elements/adw-action-row.d.ts +9 -12
- package/lib/types/elements/adw-alert-dialog.d.ts +0 -12
- package/lib/types/elements/adw-avatar.d.ts +1 -0
- package/lib/types/elements/adw-bottom-sheet.d.ts +7 -12
- package/lib/types/elements/adw-button-content.d.ts +3 -4
- package/lib/types/elements/adw-button-row.d.ts +1 -1
- package/lib/types/elements/adw-carousel.d.ts +28 -49
- package/lib/types/elements/adw-checks.d.ts +2 -8
- package/lib/types/elements/adw-combo-row.d.ts +6 -6
- package/lib/types/elements/adw-data-grid.d.ts +5 -10
- package/lib/types/elements/adw-drop-down.d.ts +3 -11
- package/lib/types/elements/adw-entry-row.d.ts +20 -23
- package/lib/types/elements/adw-icon.d.ts +2 -4
- package/lib/types/elements/adw-inline-view-switcher.d.ts +18 -31
- package/lib/types/elements/adw-navigation-split-view.d.ts +7 -12
- package/lib/types/elements/adw-navigation-view.d.ts +2 -10
- package/lib/types/elements/adw-overlay-split-view.d.ts +28 -52
- package/lib/types/elements/adw-password-entry-row.d.ts +4 -5
- package/lib/types/elements/adw-popover.d.ts +2 -7
- package/lib/types/elements/adw-preferences-dialog.d.ts +21 -34
- package/lib/types/elements/adw-preferences-group.d.ts +12 -14
- package/lib/types/elements/adw-progress-bar.d.ts +3 -4
- package/lib/types/elements/adw-shortcut-label.d.ts +20 -0
- package/lib/types/elements/adw-sidebar.d.ts +5 -10
- package/lib/types/elements/adw-spinner.d.ts +3 -5
- package/lib/types/elements/adw-split-button.d.ts +6 -16
- package/lib/types/elements/adw-switch-row.d.ts +3 -3
- package/lib/types/elements/adw-tab-view.d.ts +16 -26
- package/lib/types/elements/adw-toast-overlay.d.ts +8 -13
- package/lib/types/elements/adw-view-stack.d.ts +7 -17
- package/lib/types/elements/adw-view-switcher-bar.d.ts +4 -8
- package/lib/types/elements/adw-view-switcher.d.ts +18 -38
- package/lib/types/elements/adw-wrap-box.d.ts +7 -17
- package/lib/types/elements/view-switcher-dom.d.ts +10 -13
- package/lib/types/index.d.ts +3 -0
- package/lib/types/source-view/hex.d.ts +7 -13
- package/lib/types/style-classes.spec.d.ts +11 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/package.json +18 -7
- package/scss/_about_dialog.scss +3 -2
- package/scss/_action_row.scss +33 -0
- package/scss/_avatar.scss +12 -0
- package/scss/_button.scss +44 -22
- package/scss/_card.scss +11 -8
- package/scss/_checks.scss +2 -4
- package/scss/_dialog.scss +3 -2
- package/scss/_headerbar.scss +23 -0
- package/scss/_popover.scss +8 -4
- package/scss/_preferences.scss +7 -7
- package/scss/_progress_bar.scss +9 -6
- package/scss/_reset.scss +1 -1
- package/scss/_shortcut_label.scss +80 -0
- package/scss/_sidebar.scss +57 -0
- package/scss/_status_page.scss +20 -0
- package/scss/_style_classes.scss +407 -0
- package/scss/_switch.scss +4 -3
- package/scss/_theme.scss +20 -2
- package/scss/_variables.scss +34 -1
- package/scss/adwaita-skin.scss +12 -0
- package/src/accent.ts +74 -0
- package/src/adw-about-dialog.spec.ts +34 -49
- package/src/adw-accent.spec.ts +105 -0
- package/src/adw-action-rows.spec.ts +25 -33
- package/src/adw-avatar.spec.ts +51 -20
- package/src/adw-banner.spec.ts +5 -8
- package/src/adw-button-content.spec.ts +9 -12
- package/src/adw-button-row.spec.ts +2 -4
- package/src/adw-carousel.spec.ts +40 -53
- package/src/adw-checks.spec.ts +36 -47
- package/src/adw-drop-down.spec.ts +25 -32
- package/src/adw-header-bar.spec.ts +5 -7
- package/src/adw-icon.spec.ts +23 -32
- package/src/adw-progress-bar.spec.ts +10 -12
- package/src/adw-row-state.spec.ts +9 -10
- package/src/adw-shortcut-label.spec.ts +112 -0
- package/src/adw-sidebar.spec.ts +17 -24
- package/src/adw-switch.spec.ts +11 -17
- package/src/adw-tab-view.spec.ts +7 -10
- package/src/adw-toast-overlay.spec.ts +7 -11
- package/src/adw-view-switcher-bar.spec.ts +10 -14
- package/src/adw-wrap-box.spec.ts +25 -36
- package/src/bottom-sheet.spec.ts +30 -36
- package/src/breakpoints.ts +15 -18
- package/src/chrome.spec.ts +13 -21
- package/src/elements/adw-about-dialog.ts +71 -114
- package/src/elements/adw-action-row.ts +22 -37
- package/src/elements/adw-alert-dialog.ts +25 -50
- package/src/elements/adw-avatar.ts +40 -15
- package/src/elements/adw-banner.ts +14 -18
- package/src/elements/adw-bottom-sheet.ts +31 -52
- package/src/elements/adw-button-content.ts +30 -37
- package/src/elements/adw-button-row.ts +14 -24
- package/src/elements/adw-carousel.ts +87 -148
- package/src/elements/adw-checks.ts +43 -56
- package/src/elements/adw-clamp.ts +8 -9
- package/src/elements/adw-combo-row.ts +17 -25
- package/src/elements/adw-data-grid.ts +26 -38
- package/src/elements/adw-dialog.ts +25 -56
- package/src/elements/adw-drop-down.ts +31 -51
- package/src/elements/adw-entry-row.ts +39 -46
- package/src/elements/adw-expander-row.ts +30 -53
- package/src/elements/adw-icon.ts +30 -53
- package/src/elements/adw-inline-view-switcher.ts +38 -67
- package/src/elements/adw-menu-button.ts +28 -58
- package/src/elements/adw-navigation-split-view.ts +35 -47
- package/src/elements/adw-navigation-view.ts +12 -28
- package/src/elements/adw-overlay-split-view.ts +100 -175
- package/src/elements/adw-password-entry-row.ts +18 -28
- package/src/elements/adw-popover.ts +28 -44
- package/src/elements/adw-preferences-dialog.ts +41 -66
- package/src/elements/adw-preferences-group.ts +33 -46
- package/src/elements/adw-progress-bar.ts +27 -47
- package/src/elements/adw-shortcut-label.ts +138 -0
- package/src/elements/adw-sidebar.ts +31 -39
- package/src/elements/adw-spin-row.ts +7 -14
- package/src/elements/adw-spinner.ts +31 -43
- package/src/elements/adw-split-button.ts +45 -75
- package/src/elements/adw-switch-row.ts +29 -46
- package/src/elements/adw-switch.ts +28 -47
- package/src/elements/adw-tab-view.ts +62 -103
- package/src/elements/adw-toast-overlay.ts +28 -35
- package/src/elements/adw-toggle-group.ts +9 -16
- package/src/elements/adw-view-stack.ts +32 -51
- package/src/elements/adw-view-switcher-bar.ts +26 -50
- package/src/elements/adw-view-switcher.ts +60 -110
- package/src/elements/adw-window-title.ts +4 -4
- package/src/elements/adw-wrap-box.ts +33 -59
- package/src/elements/view-switcher-dom.ts +15 -22
- package/src/entry-rows.spec.ts +18 -24
- package/src/index.ts +24 -15
- package/src/navigation-view.spec.ts +7 -10
- package/src/preferences-search.ts +11 -15
- package/src/preferences.spec.ts +13 -15
- package/src/source-view/adw-source-view.ts +6 -19
- package/src/source-view/hex.ts +7 -13
- package/src/split-button.spec.ts +32 -44
- package/src/split-views.spec.ts +83 -149
- package/src/style-classes.spec.ts +181 -0
- package/src/style-isolation.spec.ts +3 -6
- package/src/styles.generated.ts +1 -1
- package/src/tab-view.spec.ts +26 -35
- package/src/test.browser.mts +6 -0
- package/src/view-stack.spec.ts +23 -34
- package/src/view-switcher.spec.ts +56 -84
|
@@ -20,26 +20,14 @@
|
|
|
20
20
|
// in libadwaita's emission order.
|
|
21
21
|
// `menu-activated` — a menu entry was chosen (detail.label / .action / .index).
|
|
22
22
|
//
|
|
23
|
-
// All of the
|
|
24
|
-
// {@link SplitButtonState}; this element only paints it and translates DOM
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// The last piece of that copy to go was the direction→arrow table, which
|
|
33
|
-
// outlived the lift and drew the `open-menu` hamburger for `direction="none"`.
|
|
34
|
-
// Only the glyph→mask-class mapping is a renderer concern; see ARROW_MASK_CLASSES.
|
|
35
|
-
//
|
|
36
|
-
// THE POPOVER WENT THE SAME WAY, and it was the worst of the three copies: a
|
|
37
|
-
// 9px (`--button-radius`) surface where libadwaita's is 15px, under a two-layer
|
|
38
|
-
// shadow nothing in the vendored stylesheet has, with NO Escape dismissal and NO
|
|
39
|
-
// arrow-key navigation at all — a menu you could open with the keyboard and then
|
|
40
|
-
// only leave with the mouse. It is now `<adw-popover>`, so those two behaviours
|
|
41
|
-
// arrive with the surface rather than having to be written a third time; the
|
|
42
|
-
// spec cases for exactly them are in `src/split-button.spec.ts`.
|
|
23
|
+
// All of the behaviour above lives in `@gjsify/adwaita-core` (ADR 0004) as
|
|
24
|
+
// {@link SplitButtonState}; this element only paints it and translates DOM events
|
|
25
|
+
// into state calls, pinned to the C source by the shared vectors in
|
|
26
|
+
// `@gjsify/adwaita-core/conformance` (see `src/split-button.spec.ts`). Of the
|
|
27
|
+
// element's own former copies only the glyph→mask-class mapping is a renderer
|
|
28
|
+
// concern — see ARROW_MASK_CLASSES. The menu surface is `<adw-popover>`, which is
|
|
29
|
+
// what brings the 15px radius, Escape dismissal and arrow-key navigation; do not
|
|
30
|
+
// hand-roll a surface here instead.
|
|
43
31
|
//
|
|
44
32
|
// Reference: refs/libadwaita/src/adw-split-button.c (AdwSplitButton)
|
|
45
33
|
// Reference: refs/adwaita-web/adwaita-web/scss/_split_button.scss
|
|
@@ -78,19 +66,17 @@ const VARIANT_CLASSES: Record<string, string> = {
|
|
|
78
66
|
|
|
79
67
|
/**
|
|
80
68
|
* libadwaita's arrow GLYPH NAMES mapped onto the mask classes
|
|
81
|
-
* `_icons.generated.scss` actually ships. Renderer-specific by nature: the icon
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* owns that, and this table is keyed by its output rather than by direction. That
|
|
86
|
-
* split is the point: keying by direction is how this element came to draw the
|
|
87
|
-
* `open-menu` hamburger for `direction="none"`, where a split button draws the
|
|
88
|
-
* down caret (`splitbutton > menubutton > button > arrow.none`,
|
|
89
|
-
* _buttons.scss:621-623). The mask mapping is a fact about our asset set; the
|
|
90
|
-
* glyph choice is a fact about libadwaita, and only one of them lives here.
|
|
69
|
+
* `_icons.generated.scss` actually ships. Renderer-specific by nature: the icon set
|
|
70
|
+
* here is the `go-*` family, so no `pan-*` mask exists to map onto, and
|
|
71
|
+
* `pan-up-symbolic` reuses `go-down` flipped by the caller rather than drawing a blank
|
|
72
|
+
* arrow.
|
|
91
73
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
74
|
+
* WHICH glyph a direction gets is NOT decided here — {@link splitButtonArrowIcon} owns
|
|
75
|
+
* that, and this table is keyed by its OUTPUT rather than by direction. Keying by
|
|
76
|
+
* direction is how this element came to draw the `open-menu` hamburger for
|
|
77
|
+
* `direction="none"`, where a split button draws the down caret
|
|
78
|
+
* (`splitbutton > menubutton > button > arrow.none`). The mask mapping is a fact about
|
|
79
|
+
* our asset set, the glyph choice a fact about libadwaita.
|
|
94
80
|
*/
|
|
95
81
|
const ARROW_MASK_CLASSES: Readonly<Record<AdwArrowIcon, string>> = {
|
|
96
82
|
'open-menu-symbolic': 'open-menu',
|
|
@@ -100,11 +86,9 @@ const ARROW_MASK_CLASSES: Readonly<Record<AdwArrowIcon, string>> = {
|
|
|
100
86
|
'pan-end-symbolic': 'go-next',
|
|
101
87
|
};
|
|
102
88
|
|
|
103
|
-
// The `-symbolic` strip
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
// same class without the guard; keeping the correct one private is what let
|
|
107
|
-
// them stay wrong.
|
|
89
|
+
// The `-symbolic` strip and the single-CSS-token guard belong to `normalizeIconName`,
|
|
90
|
+
// applied by <adw-icon>: never rebuild the mask class by hand, which is how five sites
|
|
91
|
+
// in this package shipped without the guard.
|
|
108
92
|
|
|
109
93
|
/**
|
|
110
94
|
* Direction → the CSS axis `<adw-popover>` places the surface on. WHICH
|
|
@@ -147,7 +131,6 @@ export class AdwSplitButton extends HTMLElement {
|
|
|
147
131
|
];
|
|
148
132
|
}
|
|
149
133
|
|
|
150
|
-
/** Whether the dropdown menu is currently open. */
|
|
151
134
|
get active(): boolean {
|
|
152
135
|
return this._state.open;
|
|
153
136
|
}
|
|
@@ -164,8 +147,8 @@ export class AdwSplitButton extends HTMLElement {
|
|
|
164
147
|
|
|
165
148
|
set label(value: string) {
|
|
166
149
|
this.setAttribute('label', value ?? '');
|
|
167
|
-
// Setting the label clears the icon
|
|
168
|
-
//
|
|
150
|
+
// Setting the label clears the icon; mirror that in the DOM so the
|
|
151
|
+
// attributes never contradict the state.
|
|
169
152
|
this.removeAttribute('icon-name');
|
|
170
153
|
}
|
|
171
154
|
|
|
@@ -179,7 +162,6 @@ export class AdwSplitButton extends HTMLElement {
|
|
|
179
162
|
this.removeAttribute('label');
|
|
180
163
|
}
|
|
181
164
|
|
|
182
|
-
/** The dropdown menu entries. */
|
|
183
165
|
get menuItems(): readonly AdwMenuEntry[] {
|
|
184
166
|
return this._state.menuModel ?? [];
|
|
185
167
|
}
|
|
@@ -198,7 +180,6 @@ export class AdwSplitButton extends HTMLElement {
|
|
|
198
180
|
this.setAttribute('dropdown-tooltip', value ?? '');
|
|
199
181
|
}
|
|
200
182
|
|
|
201
|
-
/** The direction the popup opens in, and the arrow points. */
|
|
202
183
|
get direction(): SplitButtonDirection {
|
|
203
184
|
return this._state.direction;
|
|
204
185
|
}
|
|
@@ -207,7 +188,6 @@ export class AdwSplitButton extends HTMLElement {
|
|
|
207
188
|
this.setAttribute('direction', value);
|
|
208
189
|
}
|
|
209
190
|
|
|
210
|
-
/** Whether an underline in the label marks a mnemonic. */
|
|
211
191
|
get useUnderline(): boolean {
|
|
212
192
|
return this._state.useUnderline;
|
|
213
193
|
}
|
|
@@ -252,8 +232,8 @@ export class AdwSplitButton extends HTMLElement {
|
|
|
252
232
|
this._menuEl = document.createElement('adw-popover') as AdwPopover;
|
|
253
233
|
this._menuEl.classList.add('adw-split-button-menu');
|
|
254
234
|
this._menuEl.setAttribute('role', 'menu');
|
|
255
|
-
// The dropdown half IS a GtkMenuButton (
|
|
256
|
-
//
|
|
235
|
+
// The dropdown half IS a GtkMenuButton (the C passes straight through to
|
|
236
|
+
// one), so its popover is a `popover.menu`.
|
|
257
237
|
this._menuEl.setAttribute('menu', '');
|
|
258
238
|
this._menuEl.setAttribute('align', 'end');
|
|
259
239
|
|
|
@@ -315,12 +295,11 @@ export class AdwSplitButton extends HTMLElement {
|
|
|
315
295
|
this._render();
|
|
316
296
|
}
|
|
317
297
|
|
|
318
|
-
/** Seed label/icon/menu/direction/tooltip from the attributes present at connect time. */
|
|
319
298
|
private _syncStateFromAttributes(): void {
|
|
320
299
|
const label = this.getAttribute('label') ?? (this._inlineLabel.length > 0 ? this._inlineLabel : null);
|
|
321
300
|
const iconName = this.getAttribute('icon-name');
|
|
322
301
|
// With both present the icon wins — exactly where `set_label();
|
|
323
|
-
// set_icon_name();` leaves the widget
|
|
302
|
+
// set_icon_name();` leaves the widget.
|
|
324
303
|
if (label !== null) this._state.setLabel(label);
|
|
325
304
|
if (iconName !== null) this._state.setIconName(iconName);
|
|
326
305
|
|
|
@@ -334,14 +313,12 @@ export class AdwSplitButton extends HTMLElement {
|
|
|
334
313
|
}
|
|
335
314
|
|
|
336
315
|
/**
|
|
337
|
-
* Route a `label` / `icon-name` attribute mutation into the content machine.
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
* to be a silent no-op, so the storybook's icon-only mode rendered the icon
|
|
344
|
-
* next to a stale "Save".
|
|
316
|
+
* Route a `label` / `icon-name` attribute mutation into the content machine. C has no
|
|
317
|
+
* independent "unset" for either slot — clearing one is a side effect of setting
|
|
318
|
+
* another — so a REMOVED attribute falls back to whichever sibling is still present,
|
|
319
|
+
* and to the empty child otherwise. Without that fallback
|
|
320
|
+
* `removeAttribute('label')` is a silent no-op and an icon-only button keeps a stale
|
|
321
|
+
* label beside its icon.
|
|
345
322
|
*/
|
|
346
323
|
private _applyContentAttribute(name: 'label' | 'icon-name', value: string | null): void {
|
|
347
324
|
if (value !== null) {
|
|
@@ -366,8 +343,7 @@ export class AdwSplitButton extends HTMLElement {
|
|
|
366
343
|
}
|
|
367
344
|
this._render();
|
|
368
345
|
// Focus lands in the menu on open, AFTER _render has built the rows —
|
|
369
|
-
//
|
|
370
|
-
// to move from, which is half of why this element had none.
|
|
346
|
+
// otherwise the popover's arrow keys have nothing to move from.
|
|
371
347
|
if (opened) this._menuEl.items[0]?.focus();
|
|
372
348
|
}
|
|
373
349
|
|
|
@@ -375,9 +351,8 @@ export class AdwSplitButton extends HTMLElement {
|
|
|
375
351
|
const disabled = this.hasAttribute('disabled');
|
|
376
352
|
this.classList.toggle('disabled', disabled);
|
|
377
353
|
this._actionEl.disabled = disabled;
|
|
378
|
-
// With neither a menu model nor a popover the dropdown is INSENSITIVE
|
|
379
|
-
//
|
|
380
|
-
// popover, which is what the old unguarded click handler did.
|
|
354
|
+
// With neither a menu model nor a popover the dropdown is INSENSITIVE: it must
|
|
355
|
+
// not open an empty popover.
|
|
381
356
|
this._dropdownEl.disabled = disabled || !this._state.dropdownEnabled;
|
|
382
357
|
|
|
383
358
|
this.classList.remove('flat', 'suggested-action', 'destructive-action');
|
|
@@ -390,12 +365,10 @@ export class AdwSplitButton extends HTMLElement {
|
|
|
390
365
|
this._renderMenu();
|
|
391
366
|
}
|
|
392
367
|
|
|
393
|
-
/** The action half + the root style classes libadwaita derives from it. */
|
|
394
368
|
private _renderContent(): void {
|
|
395
369
|
const { mode, label, iconName, styleClasses } = this._state;
|
|
396
370
|
|
|
397
|
-
// `splitbutton[.image-button][.text-button]` — the documented CSS node
|
|
398
|
-
// contract (adw-split-button.c:32-50, :145-165).
|
|
371
|
+
// `splitbutton[.image-button][.text-button]` — the documented CSS node contract.
|
|
399
372
|
this.classList.toggle('image-button', styleClasses.includes('image-button'));
|
|
400
373
|
this.classList.toggle('text-button', styleClasses.includes('text-button'));
|
|
401
374
|
|
|
@@ -417,7 +390,6 @@ export class AdwSplitButton extends HTMLElement {
|
|
|
417
390
|
else this._actionEl.removeAttribute('aria-label');
|
|
418
391
|
}
|
|
419
392
|
|
|
420
|
-
/** The dropdown half: arrow glyph, tooltip/accessible name, expanded state. */
|
|
421
393
|
private _renderDropdown(): void {
|
|
422
394
|
const { direction, open } = this._state;
|
|
423
395
|
const glyph = splitButtonArrowIcon(direction);
|
|
@@ -425,18 +397,18 @@ export class AdwSplitButton extends HTMLElement {
|
|
|
425
397
|
// No pan-up mask ships yet, so it is the down arrow turned over. Keyed off
|
|
426
398
|
// the GLYPH, not the direction: the flip belongs to the mask substitution.
|
|
427
399
|
this._arrowEl.style.transform = glyph === 'pan-up-symbolic' ? 'rotate(180deg)' : '';
|
|
428
|
-
// Where the popup goes is core's call (`none` → `down`); only the
|
|
429
|
-
//
|
|
400
|
+
// Where the popup goes is core's call (`none` → `down`); only the direction →
|
|
401
|
+
// CSS-axis mapping is ours.
|
|
430
402
|
this._menuEl.position = POPOVER_POSITIONS[splitButtonPopupDirection(direction)];
|
|
431
403
|
|
|
432
|
-
// An empty value RESTORES the translated default
|
|
433
|
-
//
|
|
404
|
+
// An empty value RESTORES the translated default instead of leaving the button
|
|
405
|
+
// without an accessible name.
|
|
434
406
|
const { text } = resolveDropdownTooltip(this._state.dropdownTooltip);
|
|
435
407
|
this._dropdownEl.title = text;
|
|
436
408
|
this._dropdownEl.setAttribute('aria-label', text);
|
|
437
409
|
this._dropdownEl.setAttribute('aria-expanded', String(open));
|
|
438
410
|
|
|
439
|
-
// update_state folds both halves onto the root
|
|
411
|
+
// update_state folds both halves onto the root.
|
|
440
412
|
// The browser gives the ACTIVE half of that fold for free — CSS `:active`
|
|
441
413
|
// matches the ancestors of the pressed element — so only CHECKED, which
|
|
442
414
|
// the arrow half carries while the menu is open, has to be applied here.
|
|
@@ -450,7 +422,6 @@ export class AdwSplitButton extends HTMLElement {
|
|
|
450
422
|
this.classList.toggle('active', open);
|
|
451
423
|
}
|
|
452
424
|
|
|
453
|
-
/** The popover contents, one button per menu entry, dispatched BY POSITION. */
|
|
454
425
|
private _renderMenu(): void {
|
|
455
426
|
this._menuEl.replaceChildren();
|
|
456
427
|
this._menuEl.open = this._state.open;
|
|
@@ -458,16 +429,15 @@ export class AdwSplitButton extends HTMLElement {
|
|
|
458
429
|
for (const [index, entry] of (this._state.menuModel ?? []).entries()) {
|
|
459
430
|
const item = document.createElement('button');
|
|
460
431
|
item.type = 'button';
|
|
461
|
-
// `.adw-popover-item` is what makes the row navigable: it is the
|
|
462
|
-
//
|
|
463
|
-
// element never had.
|
|
432
|
+
// `.adw-popover-item` is what makes the row navigable: it is the selector
|
|
433
|
+
// `<adw-popover>` walks for arrow/Home/End/Enter.
|
|
464
434
|
item.className = 'adw-popover-item adw-split-button-menu-item';
|
|
465
435
|
item.setAttribute('role', 'menuitem');
|
|
466
436
|
item.tabIndex = -1;
|
|
467
437
|
item.textContent = entry.label;
|
|
468
438
|
item.addEventListener('click', () => {
|
|
469
|
-
// By position, never by label: two entries called "Copy" are
|
|
470
|
-
//
|
|
439
|
+
// By position, never by label: two entries called "Copy" are legal
|
|
440
|
+
// and must each dispatch their own action.
|
|
471
441
|
const activated = this._state.activateMenuEntry(index);
|
|
472
442
|
if (activated === null) return;
|
|
473
443
|
this.dispatchEvent(
|
|
@@ -1,26 +1,14 @@
|
|
|
1
|
-
// <adw-switch-row> —
|
|
2
|
-
// Attributes: title, subtitle, active (boolean)
|
|
3
|
-
// Events: notify::active (CustomEvent, mirrors GJS GObject signal naming)
|
|
1
|
+
// <adw-switch-row> — row with a title/subtitle and a trailing `<adw-switch>`.
|
|
4
2
|
//
|
|
5
|
-
// The ACTIVE flag, its notify rule and the toggle-on-row-activation are HEADLESS
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// fire on a programmatic write, notified. libadwaita has exactly one notify
|
|
15
|
-
// path (`slider_notify_active_cb`, C:66-77) and it cannot see the origin.
|
|
16
|
-
// - clicking the ROW toggles. `adw_switch_row_init` points the
|
|
17
|
-
// activatable-widget at the slider (C:160-162), so the title is part of the
|
|
18
|
-
// control; here only the handle itself worked.
|
|
19
|
-
//
|
|
20
|
-
// The TOGGLE itself is `<adw-switch>`. It used to be built here by hand —
|
|
21
|
-
// `label.adw-switch > input[type=checkbox] + span.adw-switch-slider` — and
|
|
22
|
-
// `<adw-expander-row>` built the identical thing, over a stylesheet block that
|
|
23
|
-
// was a character-for-character copy of this row's.
|
|
3
|
+
// The ACTIVE flag, its notify rule and the toggle-on-row-activation are HEADLESS and live
|
|
4
|
+
// in `@gjsify/adwaita-core` (ADR 0004) as {@link SwitchRowState}; this element keeps only
|
|
5
|
+
// the DOM half — the label column, the switch and the `notify::active` event. Two rules
|
|
6
|
+
// from adw-switch-row.c that a DOM-first implementation gets wrong:
|
|
7
|
+
// - a PROGRAMMATIC `row.active = true` MUST notify. The DOM fires no `change` for
|
|
8
|
+
// `.checked =`, so an event keyed off the checkbox leaves the row silent; libadwaita
|
|
9
|
+
// has exactly one notify path (`slider_notify_active_cb`) and it cannot see the origin.
|
|
10
|
+
// - clicking the ROW toggles: `adw_switch_row_init` points the activatable-widget at the
|
|
11
|
+
// slider, so the title is part of the control, not just the handle.
|
|
24
12
|
//
|
|
25
13
|
// Adapted from Adwaita Web UI Framework (https://github.com/mclellac/adwaita-web).
|
|
26
14
|
// Copyright (c) 2025 csm. MIT License.
|
|
@@ -30,13 +18,12 @@
|
|
|
30
18
|
|
|
31
19
|
import { SwitchRowState, deriveRowLabels } from '@gjsify/adwaita-core';
|
|
32
20
|
|
|
33
|
-
// SIDE-EFFECT import, deliberately separate from the type import below: it
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
// the registration with it.
|
|
21
|
+
// SIDE-EFFECT import, deliberately separate from the type import below: it guarantees
|
|
22
|
+
// `adw-switch` is defined before this module's `customElements.define` can upgrade a
|
|
23
|
+
// server-rendered `<adw-switch-row>` and build one. A combined `import { AdwSwitch }`
|
|
24
|
+
// would NOT do it — the binding is only used in type position, and this package compiles
|
|
25
|
+
// without `verbatimModuleSyntax`, so TypeScript would elide the statement and take the
|
|
26
|
+
// registration with it.
|
|
40
27
|
import './adw-switch.js';
|
|
41
28
|
import type { AdwSwitch } from './adw-switch.js';
|
|
42
29
|
|
|
@@ -80,23 +67,20 @@ export class AdwSwitchRow extends HTMLElement {
|
|
|
80
67
|
|
|
81
68
|
this._state.setActive(this.hasAttribute('active'));
|
|
82
69
|
this._switchEl.active = this._state.active;
|
|
83
|
-
// `
|
|
84
|
-
// plus the initial CHECKED state (adw-switch-row.c:147, :155-158).
|
|
70
|
+
// C's `GTK_ACCESSIBLE_ROLE_SWITCH` plus the initial CHECKED state.
|
|
85
71
|
this.setAttribute('role', 'switch');
|
|
86
72
|
this.setAttribute('aria-checked', String(this._state.active));
|
|
87
73
|
|
|
88
74
|
this._switchEl.addEventListener('notify::active', (event) => {
|
|
89
|
-
// The ROW is the public event surface
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
// would make every listener on the row see each toggle twice.
|
|
75
|
+
// The ROW is the public event surface, and libadwaita has exactly one notify
|
|
76
|
+
// path for the property. Letting the composed switch's identically-named event
|
|
77
|
+
// escape would make every listener on the row see each toggle twice.
|
|
93
78
|
event.stopPropagation();
|
|
94
79
|
this._apply(this._state.setActive(this._switchEl.active));
|
|
95
80
|
});
|
|
96
81
|
|
|
97
|
-
// Activating the row inverts the state
|
|
98
|
-
//
|
|
99
|
-
// toggling again here would immediately undo it.
|
|
82
|
+
// Activating the row inverts the state. A click that landed on the switch itself is
|
|
83
|
+
// already handled there; toggling again here would undo it.
|
|
100
84
|
this.addEventListener('click', (event) => {
|
|
101
85
|
if (event.target instanceof Node && this._switchEl.contains(event.target)) return;
|
|
102
86
|
this._apply(this._state.activate());
|
|
@@ -112,17 +96,17 @@ export class AdwSwitchRow extends HTMLElement {
|
|
|
112
96
|
}
|
|
113
97
|
|
|
114
98
|
/**
|
|
115
|
-
* Push a state transition out to the DOM and emit `notify::active
|
|
116
|
-
*
|
|
117
|
-
*
|
|
99
|
+
* Push a state transition out to the DOM and emit `notify::active`, but only when
|
|
100
|
+
* something actually changed — the `g_object_notify` gate libadwaita puts on the same
|
|
101
|
+
* transition.
|
|
118
102
|
*/
|
|
119
103
|
private _apply(changed: boolean) {
|
|
120
104
|
if (!changed) return;
|
|
121
|
-
// Idempotent on the switch's side too, so this write does not bounce
|
|
122
|
-
//
|
|
105
|
+
// Idempotent on the switch's side too, so this write does not bounce back through
|
|
106
|
+
// the `notify::active` listener as a second transition.
|
|
123
107
|
this._switchEl.active = this._state.active;
|
|
124
108
|
this.toggleAttribute('active', this._state.active);
|
|
125
|
-
// The a11y state libadwaita updates alongside the notify
|
|
109
|
+
// The a11y state libadwaita updates alongside the notify.
|
|
126
110
|
this.setAttribute('aria-checked', String(this._state.active));
|
|
127
111
|
this.dispatchEvent(
|
|
128
112
|
new CustomEvent('notify::active', {
|
|
@@ -138,8 +122,7 @@ export class AdwSwitchRow extends HTMLElement {
|
|
|
138
122
|
subtitle: this.getAttribute('subtitle'),
|
|
139
123
|
});
|
|
140
124
|
this._titleEl.textContent = title;
|
|
141
|
-
// `string_is_not_empty` applies to the TITLE too
|
|
142
|
-
// inherited — AdwSwitchRow derives from AdwActionRow, adw-switch-row.c:50).
|
|
125
|
+
// `string_is_not_empty` applies to the TITLE too, inherited from AdwActionRow.
|
|
143
126
|
this._titleEl.hidden = !titleVisible;
|
|
144
127
|
this._subtitleEl.textContent = subtitle;
|
|
145
128
|
this._subtitleEl.hidden = !subtitleVisible;
|
|
@@ -1,43 +1,27 @@
|
|
|
1
1
|
// <adw-switch> — the Adwaita toggle, as an element. A 44×24 track with a hidden
|
|
2
2
|
// checkbox behind it and a knob that slides on `active`.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// `_expander_row.scss:73-121` was a character-for-character copy of
|
|
7
|
-
// `_switch_row.scss:15-63`. The cause was structural — `_switch_row.scss:9`
|
|
8
|
-
// opened `adw-switch-row {` and nested everything inside it, so `.adw-switch` did
|
|
9
|
-
// not exist outside a switch row and the expander could not reuse it. The styles
|
|
10
|
-
// are now the unscoped `scss/_switch.scss` and the markup is here.
|
|
4
|
+
// The styles are the UNSCOPED `scss/_switch.scss`, so `.adw-switch` exists outside a
|
|
5
|
+
// switch row and `<adw-expander-row>` can reuse it instead of copying the block.
|
|
11
6
|
//
|
|
12
|
-
// NO CORE STATE MACHINE. The state is one boolean with no derivation, no
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// (`SwitchRowState`), which is where the notify rule that IS derived from C
|
|
21
|
-
// lives.
|
|
7
|
+
// NO CORE STATE MACHINE. The state is one boolean with no derivation, no ordering and no
|
|
8
|
+
// notify subtlety beyond "on change" — ADR 0004 is explicit that trivial behaviour gets
|
|
9
|
+
// no core class, and the `active` ATTRIBUTE is the state (`toggleAttribute` is idempotent,
|
|
10
|
+
// so "notify only on a real change" needs no guard). `GtkSwitch` does carry a two-phase
|
|
11
|
+
// `active`/`state` pair for async toggles, but `refs/gtk` is an uninitialized submodule
|
|
12
|
+
// here so that is unverifiable, and neither this port nor
|
|
13
|
+
// `@gjsify/adwaita-nativescript` models it. The notify rule that IS derived from C lives
|
|
14
|
+
// in the ROW's `SwitchRowState`.
|
|
22
15
|
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// active — the toggled state (get/set).
|
|
28
|
-
// disabled — whether the control is inert (get/set).
|
|
29
|
-
// Events:
|
|
30
|
-
// `notify::active` (CustomEvent, bubbles, detail = { active }) — mirrors the
|
|
31
|
-
// GObject property-notify; fires on every change, programmatic included.
|
|
32
|
-
// A widget that COMPOSES this one and publishes its own `notify::active`
|
|
33
|
-
// (`<adw-switch-row>`) stops this one at the switch, so the row stays the
|
|
34
|
-
// single public event surface.
|
|
16
|
+
// `notify::active` (CustomEvent, bubbles, detail `{ active }`) fires on every change,
|
|
17
|
+
// programmatic included. A widget that COMPOSES this one and publishes its own
|
|
18
|
+
// `notify::active` (`<adw-switch-row>`) stops this one at the switch, so the row stays
|
|
19
|
+
// the single public event surface.
|
|
35
20
|
//
|
|
36
|
-
// A11Y: the checkbox is left bare — no `role="switch"` — because the widgets
|
|
37
|
-
//
|
|
38
|
-
// `GTK_ACCESSIBLE_ROLE_SWITCH` on the ROW
|
|
39
|
-
//
|
|
40
|
-
// shipped inside the two rows before the extraction.
|
|
21
|
+
// A11Y: the checkbox is left bare — no `role="switch"` — because the widgets that host
|
|
22
|
+
// one claim that role themselves (`adw_switch_row_init` sets
|
|
23
|
+
// `GTK_ACCESSIBLE_ROLE_SWITCH` on the ROW) and a nested second switch role would be
|
|
24
|
+
// announced twice.
|
|
41
25
|
//
|
|
42
26
|
// Reference: refs/adwaita-web/adwaita-web/scss/_switch.scss
|
|
43
27
|
// Reference: refs/libadwaita/src/stylesheet/widgets/_switch.scss
|
|
@@ -60,11 +44,10 @@ export class AdwSwitch extends HTMLElement {
|
|
|
60
44
|
}
|
|
61
45
|
|
|
62
46
|
set active(value: boolean) {
|
|
63
|
-
// `toggleAttribute` with a force flag is a no-op when the attribute
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
// holding.
|
|
47
|
+
// `toggleAttribute` with a force flag is a no-op when the attribute already holds
|
|
48
|
+
// the wanted state, so re-setting the current value never runs
|
|
49
|
+
// `attributeChangedCallback` and never notifies. That idempotence is what a state
|
|
50
|
+
// class would otherwise hold.
|
|
68
51
|
this.toggleAttribute('active', !!value);
|
|
69
52
|
}
|
|
70
53
|
|
|
@@ -89,18 +72,16 @@ export class AdwSwitch extends HTMLElement {
|
|
|
89
72
|
this._slider.className = 'adw-switch-slider';
|
|
90
73
|
this.replaceChildren(this._input, this._slider);
|
|
91
74
|
|
|
92
|
-
// Keyboard (Space on the focused checkbox) and a
|
|
93
|
-
//
|
|
94
|
-
//
|
|
75
|
+
// Keyboard (Space on the focused checkbox) and a programmatic `input.click()` both
|
|
76
|
+
// arrive here; the attribute is the state, so this writes it and lets
|
|
77
|
+
// attributeChangedCallback do the rest.
|
|
95
78
|
this._input.addEventListener('change', () => {
|
|
96
79
|
this.active = this._input.checked;
|
|
97
80
|
});
|
|
98
81
|
|
|
99
|
-
// The slider covers the whole track (`inset: 0`), so every pointer click
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
// checkbox is already handled by the listener above; toggling again here
|
|
103
|
-
// would immediately undo it.
|
|
82
|
+
// The slider covers the whole track (`inset: 0`), so every pointer click lands on
|
|
83
|
+
// it rather than on the 0×0 checkbox. A click that DID reach the checkbox is
|
|
84
|
+
// already handled by the listener above; toggling again here would undo it.
|
|
104
85
|
this.addEventListener('click', (event) => {
|
|
105
86
|
if (this.disabled) return;
|
|
106
87
|
if (event.target === this._input) return;
|