@gjsify/adwaita-web 0.31.0 → 0.33.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 +835 -299
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/adw-about-dialog.spec.d.ts +1 -0
- package/lib/types/adw-action-rows.spec.d.ts +1 -0
- package/lib/types/adw-avatar.spec.d.ts +1 -0
- package/lib/types/adw-banner.spec.d.ts +1 -0
- package/lib/types/adw-button-content.spec.d.ts +1 -0
- package/lib/types/adw-carousel.spec.d.ts +1 -0
- package/lib/types/adw-checks.spec.d.ts +1 -0
- package/lib/types/adw-header-bar.spec.d.ts +1 -0
- package/lib/types/adw-icon.spec.d.ts +1 -0
- package/lib/types/adw-progress-bar.spec.d.ts +1 -0
- package/lib/types/adw-sidebar.spec.d.ts +1 -0
- package/lib/types/adw-switch.spec.d.ts +1 -0
- package/lib/types/adw-wrap-box.spec.d.ts +1 -0
- package/lib/types/bottom-sheet.spec.d.ts +1 -0
- package/lib/types/breakpoints.d.ts +22 -0
- package/lib/types/breakpoints.spec.d.ts +1 -0
- package/lib/types/chrome.spec.d.ts +1 -0
- package/lib/types/elements/adw-about-dialog.d.ts +47 -8
- package/lib/types/elements/adw-action-row.d.ts +35 -0
- package/lib/types/elements/adw-avatar.d.ts +11 -0
- package/lib/types/elements/adw-bottom-sheet.d.ts +33 -4
- package/lib/types/elements/adw-button-content.d.ts +12 -0
- package/lib/types/elements/adw-button-row.d.ts +10 -1
- package/lib/types/elements/adw-carousel.d.ts +111 -8
- package/lib/types/elements/adw-checks.d.ts +57 -0
- package/lib/types/elements/adw-clamp.d.ts +4 -1
- package/lib/types/elements/adw-combo-row.d.ts +10 -0
- package/lib/types/elements/adw-data-grid.d.ts +11 -31
- package/lib/types/elements/adw-drop-down.d.ts +17 -20
- package/lib/types/elements/adw-entry-row.d.ts +93 -3
- package/lib/types/elements/adw-expander-row.d.ts +7 -1
- package/lib/types/elements/adw-header-bar.d.ts +15 -0
- package/lib/types/elements/adw-icon.d.ts +25 -0
- package/lib/types/elements/adw-inline-view-switcher.d.ts +73 -11
- package/lib/types/elements/adw-menu-button.d.ts +6 -6
- package/lib/types/elements/adw-navigation-split-view.d.ts +48 -0
- package/lib/types/elements/adw-navigation-view.d.ts +63 -20
- package/lib/types/elements/adw-overlay-split-view.d.ts +94 -0
- package/lib/types/elements/adw-password-entry-row.d.ts +19 -10
- package/lib/types/elements/adw-popover.d.ts +58 -0
- package/lib/types/elements/adw-preferences-dialog.d.ts +57 -2
- package/lib/types/elements/adw-preferences-group.d.ts +31 -0
- package/lib/types/elements/adw-progress-bar.d.ts +38 -0
- package/lib/types/elements/adw-sidebar.d.ts +37 -5
- package/lib/types/elements/adw-spinner.d.ts +23 -0
- package/lib/types/elements/adw-split-button.d.ts +53 -7
- package/lib/types/elements/adw-switch-row.d.ts +11 -2
- package/lib/types/elements/adw-switch.d.ts +15 -0
- package/lib/types/elements/adw-tab-view.d.ts +129 -12
- package/lib/types/elements/adw-toolbar-view.d.ts +5 -0
- package/lib/types/elements/adw-view-stack.d.ts +39 -19
- package/lib/types/elements/adw-view-switcher-bar.d.ts +25 -7
- package/lib/types/elements/adw-view-switcher.d.ts +76 -13
- package/lib/types/elements/adw-window-title.d.ts +3 -1
- package/lib/types/elements/adw-wrap-box.d.ts +58 -3
- package/lib/types/elements/view-switcher-dom.d.ts +44 -0
- package/lib/types/entry-rows.spec.d.ts +1 -0
- package/lib/types/index.d.ts +7 -0
- package/lib/types/navigation-view.spec.d.ts +1 -0
- package/lib/types/preferences-search.d.ts +20 -0
- package/lib/types/preferences.spec.d.ts +1 -0
- package/lib/types/split-button.spec.d.ts +1 -0
- package/lib/types/split-views.spec.d.ts +1 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/lib/types/tab-view.spec.d.ts +1 -0
- package/lib/types/view-stack.spec.d.ts +1 -0
- package/lib/types/view-switcher.spec.d.ts +1 -0
- package/package.json +6 -6
- package/scss/_banner.scss +13 -0
- package/scss/_bottom_sheet.scss +5 -6
- package/scss/_button.scss +3 -26
- package/scss/_button_content.scss +28 -0
- package/scss/_checks.scss +228 -0
- package/scss/_clamp.scss +9 -3
- package/scss/_combo_row.scss +19 -0
- package/scss/_data_grid.scss +15 -7
- package/scss/_drop_down.scss +12 -55
- package/scss/_expander_row.scss +5 -50
- package/scss/_icon.scss +51 -0
- package/scss/_icons.generated.scss +6 -0
- package/scss/_inline_view_switcher.scss +34 -0
- package/scss/_labels.scss +135 -0
- package/scss/_menu_button.scss +10 -70
- package/scss/_navigation_split_view.scss +47 -8
- package/scss/_overlay_split_view.scss +117 -32
- package/scss/_popover.scss +186 -0
- package/scss/_preferences.scss +29 -0
- package/scss/_preferences_dialog.scss +17 -0
- package/scss/_progress_bar.scss +161 -0
- package/scss/_reset.scss +18 -7
- package/scss/_spinner.scss +37 -19
- package/scss/_split_button.scss +6 -50
- package/scss/_switch.scss +90 -0
- package/scss/_switch_row.scss +6 -52
- package/scss/_tab_view.scss +63 -0
- package/scss/_theme.scss +10 -0
- package/scss/_toolbar_view.scss +74 -0
- package/scss/_variables.scss +34 -0
- package/scss/_view_switcher.scss +40 -0
- package/scss/_view_switcher_bar.scss +37 -0
- package/scss/_wrap_box.scss +65 -6
- package/scss/adwaita-skin.scss +23 -0
- package/src/adw-about-dialog.spec.ts +350 -0
- package/src/adw-action-rows.spec.ts +342 -0
- package/src/adw-avatar.spec.ts +176 -0
- package/src/adw-banner.spec.ts +152 -0
- package/src/adw-button-content.spec.ts +207 -0
- package/src/adw-button-row.spec.ts +12 -6
- package/src/adw-carousel.spec.ts +389 -0
- package/src/adw-checks.spec.ts +368 -0
- package/src/adw-drop-down.spec.ts +123 -0
- package/src/adw-header-bar.spec.ts +83 -0
- package/src/adw-icon.spec.ts +167 -0
- package/src/adw-progress-bar.spec.ts +173 -0
- package/src/adw-row-state.spec.ts +49 -0
- package/src/adw-sidebar.spec.ts +332 -0
- package/src/adw-switch.spec.ts +242 -0
- package/src/adw-tab-view.spec.ts +12 -3
- package/src/adw-view-switcher-bar.spec.ts +61 -13
- package/src/adw-wrap-box.spec.ts +341 -0
- package/src/bottom-sheet.spec.ts +325 -0
- package/src/breakpoints.spec.ts +147 -0
- package/src/breakpoints.ts +67 -0
- package/src/chrome.spec.ts +400 -0
- package/src/elements/adw-about-dialog.ts +223 -163
- package/src/elements/adw-action-row.ts +131 -11
- package/src/elements/adw-avatar.ts +48 -54
- package/src/elements/adw-banner.ts +46 -10
- package/src/elements/adw-bottom-sheet.ts +145 -60
- package/src/elements/adw-button-content.ts +107 -33
- package/src/elements/adw-button-row.ts +61 -37
- package/src/elements/adw-button.ts +5 -7
- package/src/elements/adw-carousel.ts +467 -105
- package/src/elements/adw-checks.ts +299 -0
- package/src/elements/adw-clamp.ts +80 -10
- package/src/elements/adw-combo-row.ts +32 -7
- package/src/elements/adw-data-grid.ts +43 -91
- package/src/elements/adw-drop-down.ts +117 -128
- package/src/elements/adw-entry-row.ts +404 -36
- package/src/elements/adw-expander-row.ts +61 -29
- package/src/elements/adw-header-bar.ts +31 -6
- package/src/elements/adw-icon.ts +148 -0
- package/src/elements/adw-inline-view-switcher.ts +325 -131
- package/src/elements/adw-menu-button.ts +93 -70
- package/src/elements/adw-navigation-split-view.ts +220 -14
- package/src/elements/adw-navigation-view.ts +286 -148
- package/src/elements/adw-overlay-split-view.ts +536 -28
- package/src/elements/adw-password-entry-row.ts +111 -89
- package/src/elements/adw-popover.ts +283 -0
- package/src/elements/adw-preferences-dialog.ts +120 -21
- package/src/elements/adw-preferences-group.ts +128 -22
- package/src/elements/adw-progress-bar.ts +189 -0
- package/src/elements/adw-sidebar.ts +301 -142
- package/src/elements/adw-spin-row.ts +11 -5
- package/src/elements/adw-spinner.ts +168 -9
- package/src/elements/adw-split-button.ts +371 -92
- package/src/elements/adw-status-page.ts +9 -9
- package/src/elements/adw-switch-row.ts +100 -30
- package/src/elements/adw-switch.ts +129 -0
- package/src/elements/adw-tab-view.ts +700 -129
- package/src/elements/adw-toggle-group.ts +5 -7
- package/src/elements/adw-toolbar-view.ts +84 -21
- package/src/elements/adw-view-stack.ts +183 -85
- package/src/elements/adw-view-switcher-bar.ts +171 -81
- package/src/elements/adw-view-switcher.ts +328 -117
- package/src/elements/adw-window-title.ts +44 -11
- package/src/elements/adw-wrap-box.ts +329 -78
- package/src/elements/view-switcher-dom.ts +108 -0
- package/src/entry-rows.spec.ts +391 -0
- package/src/index.ts +23 -0
- package/src/navigation-view.spec.ts +435 -0
- package/src/preferences-search.ts +135 -0
- package/src/preferences.spec.ts +250 -0
- package/src/source-view/adw-source-view.ts +2 -5
- package/src/split-button.spec.ts +470 -0
- package/src/split-views.spec.ts +893 -0
- package/src/styles.generated.ts +1 -1
- package/src/tab-view.spec.ts +451 -0
- package/src/test.browser.mts +49 -0
- package/src/view-stack.spec.ts +272 -0
- package/src/view-switcher.spec.ts +665 -0
|
@@ -3,50 +3,142 @@
|
|
|
3
3
|
// halves are separated by a thin divider; the .suggested-action /
|
|
4
4
|
// .destructive-action / .flat style classes recolor the whole control like the
|
|
5
5
|
// matching Adwaita button variants.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
6
|
+
//
|
|
7
|
+
// Attributes:
|
|
8
|
+
// label — the action label. MUTUALLY EXCLUSIVE with icon-name.
|
|
9
|
+
// icon-name — symbolic name (with or without the `-symbolic` suffix).
|
|
10
|
+
// use-underline — boolean; an underline in the label marks a mnemonic.
|
|
11
|
+
// tooltip — tooltip/accessible name of the action half.
|
|
12
|
+
// dropdown-tooltip — tooltip of the dropdown half; empty restores "More Options".
|
|
13
|
+
// direction — none | up | down | left | right (default down).
|
|
14
|
+
// menu — JSON array of entries, each `{ "label", "action"? }`.
|
|
15
|
+
// disabled, flat, suggested, destructive — boolean flags.
|
|
12
16
|
// Events (all CustomEvent, bubbles — mirror the Adw.SplitButton GObject signals):
|
|
13
|
-
// `clicked`
|
|
14
|
-
// `notify::active`
|
|
15
|
-
// `
|
|
17
|
+
// `clicked` — the primary action half was activated.
|
|
18
|
+
// `notify::active` — the dropdown menu was opened/closed (detail.active).
|
|
19
|
+
// `notify::<prop>` — one per GObject property the state machine notified,
|
|
20
|
+
// in libadwaita's emission order.
|
|
21
|
+
// `menu-activated` — a menu entry was chosen (detail.label / .action / .index).
|
|
22
|
+
//
|
|
23
|
+
// All of the behavior above lives in `@gjsify/adwaita-core` (ADR 0004) as
|
|
24
|
+
// {@link SplitButtonState}; this element only paints it and translates DOM
|
|
25
|
+
// events into state calls. It used to carry its own copy, and that copy rendered
|
|
26
|
+
// the label AND the icon together (libadwaita clears whichever slot it is not
|
|
27
|
+
// using), could not clear the label at all, hardcoded "Menu" as the dropdown's
|
|
28
|
+
// accessible name, and opened an empty popover when there was no menu. The
|
|
29
|
+
// shared vectors in `@gjsify/adwaita-core/conformance` now pin this element to
|
|
30
|
+
// the C source — see `src/split-button.spec.ts`.
|
|
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`.
|
|
43
|
+
//
|
|
16
44
|
// Reference: refs/libadwaita/src/adw-split-button.c (AdwSplitButton)
|
|
17
45
|
// Reference: refs/adwaita-web/adwaita-web/scss/_split_button.scss
|
|
18
46
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
19
47
|
// Copyright (c) 2025 csm (adwaita-web). MIT License.
|
|
20
48
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
21
49
|
|
|
50
|
+
import {
|
|
51
|
+
SplitButtonState,
|
|
52
|
+
isSplitButtonDirection,
|
|
53
|
+
parseMenuEntries,
|
|
54
|
+
resolveDropdownTooltip,
|
|
55
|
+
splitButtonArrowIcon,
|
|
56
|
+
splitButtonPopupDirection,
|
|
57
|
+
splitButtonRootState,
|
|
58
|
+
} from '@gjsify/adwaita-core';
|
|
59
|
+
import type { AdwArrowIcon, AdwMenuEntry, SplitButtonChange, SplitButtonDirection } from '@gjsify/adwaita-core';
|
|
60
|
+
|
|
61
|
+
// SIDE-EFFECT import, deliberately separate from the type import below: it is
|
|
62
|
+
// what guarantees `adw-popover` is defined before this module's own
|
|
63
|
+
// `customElements.define` can upgrade a server-rendered `<adw-split-button>` and
|
|
64
|
+
// build one. A combined `import { AdwPopover }` would NOT do it — the binding is
|
|
65
|
+
// only ever used in type position here, and this package compiles without
|
|
66
|
+
// `verbatimModuleSyntax`, so TypeScript would elide the whole statement and take
|
|
67
|
+
// the registration with it.
|
|
68
|
+
import './adw-popover.js';
|
|
69
|
+
import type { AdwPopover } from './adw-popover.js';
|
|
70
|
+
|
|
71
|
+
import { type AdwIcon, createAdwIcon } from './adw-icon.js';
|
|
72
|
+
|
|
22
73
|
const VARIANT_CLASSES: Record<string, string> = {
|
|
23
74
|
flat: 'flat',
|
|
24
75
|
suggested: 'suggested-action',
|
|
25
76
|
destructive: 'destructive-action',
|
|
26
77
|
};
|
|
27
78
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
79
|
+
/**
|
|
80
|
+
* libadwaita's arrow GLYPH NAMES mapped onto the mask classes
|
|
81
|
+
* `_icons.generated.scss` actually ships. Renderer-specific by nature: the icon
|
|
82
|
+
* set here is the `go-*` family, so no `pan-*` mask exists to map onto.
|
|
83
|
+
*
|
|
84
|
+
* WHICH glyph a direction gets is NOT decided here — {@link splitButtonArrowIcon}
|
|
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.
|
|
91
|
+
*
|
|
92
|
+
* `pan-up-symbolic` has no mask at all, so it reuses `go-down` flipped by the
|
|
93
|
+
* caller — the alternative is a blank arrow.
|
|
94
|
+
*/
|
|
95
|
+
const ARROW_MASK_CLASSES: Readonly<Record<AdwArrowIcon, string>> = {
|
|
96
|
+
'open-menu-symbolic': 'open-menu',
|
|
97
|
+
'pan-down-symbolic': 'go-down',
|
|
98
|
+
'pan-up-symbolic': 'go-down',
|
|
99
|
+
'pan-start-symbolic': 'go-previous',
|
|
100
|
+
'pan-end-symbolic': 'go-next',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// The `-symbolic` strip AND the token guard that used to live here — as
|
|
104
|
+
// `iconClass`, the ONLY copy of the guard in the package — are
|
|
105
|
+
// `normalizeIconName`'s now, applied by <adw-icon>. Five other places built the
|
|
106
|
+
// same class without the guard; keeping the correct one private is what let
|
|
107
|
+
// them stay wrong.
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Direction → the CSS axis `<adw-popover>` places the surface on. WHICH
|
|
111
|
+
* direction applies is not decided here — {@link splitButtonPopupDirection}
|
|
112
|
+
* folds `none` onto `down` (GtkMenuButton's rule, not the split button's), and
|
|
113
|
+
* this table is keyed by its output for the same reason ARROW_MASK_CLASSES is.
|
|
114
|
+
*/
|
|
115
|
+
const POPOVER_POSITIONS = {
|
|
116
|
+
down: 'bottom',
|
|
117
|
+
up: 'top',
|
|
118
|
+
left: 'start',
|
|
119
|
+
right: 'end',
|
|
120
|
+
} as const;
|
|
32
121
|
|
|
33
122
|
export class AdwSplitButton extends HTMLElement {
|
|
123
|
+
private readonly _state = new SplitButtonState();
|
|
34
124
|
private _actionEl!: HTMLButtonElement;
|
|
35
125
|
private _dropdownEl!: HTMLButtonElement;
|
|
36
|
-
private
|
|
37
|
-
private
|
|
38
|
-
|
|
126
|
+
private _arrowEl!: AdwIcon;
|
|
127
|
+
private _menuEl!: AdwPopover;
|
|
128
|
+
/** Inline text content, captured before we take over the subtree. */
|
|
129
|
+
private _inlineLabel = '';
|
|
130
|
+
/** Last `open` value reflected as `notify::active`, so the event fires once per flip. */
|
|
131
|
+
private _openReflected = false;
|
|
39
132
|
private _initialized = false;
|
|
40
|
-
private _onDocumentPointerDown = (event: Event): void => {
|
|
41
|
-
if (!this.contains(event.target as Node)) this._setActive(false);
|
|
42
|
-
};
|
|
43
133
|
|
|
44
134
|
static get observedAttributes() {
|
|
45
135
|
return [
|
|
46
136
|
'label',
|
|
47
137
|
'icon-name',
|
|
138
|
+
'use-underline',
|
|
48
139
|
'tooltip',
|
|
49
140
|
'dropdown-tooltip',
|
|
141
|
+
'direction',
|
|
50
142
|
'menu',
|
|
51
143
|
'disabled',
|
|
52
144
|
'flat',
|
|
@@ -57,15 +149,92 @@ export class AdwSplitButton extends HTMLElement {
|
|
|
57
149
|
|
|
58
150
|
/** Whether the dropdown menu is currently open. */
|
|
59
151
|
get active(): boolean {
|
|
60
|
-
return this.
|
|
152
|
+
return this._state.open;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Whether the dropdown half is live (there is a menu to show). */
|
|
156
|
+
get dropdownEnabled(): boolean {
|
|
157
|
+
return this._state.dropdownEnabled;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The action label, or `null` when the action half shows an icon or nothing. */
|
|
161
|
+
get label(): string | null {
|
|
162
|
+
return this._state.label;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
set label(value: string) {
|
|
166
|
+
this.setAttribute('label', value ?? '');
|
|
167
|
+
// Setting the label clears the icon (adw-split-button.c:658-659); mirror
|
|
168
|
+
// that in the DOM so the attributes never contradict the state.
|
|
169
|
+
this.removeAttribute('icon-name');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The symbolic icon name, or `null` when the action half shows a label or nothing. */
|
|
173
|
+
get iconName(): string | null {
|
|
174
|
+
return this._state.iconName;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
set iconName(value: string) {
|
|
178
|
+
this.setAttribute('icon-name', value ?? '');
|
|
179
|
+
this.removeAttribute('label');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The dropdown menu entries. */
|
|
183
|
+
get menuItems(): readonly AdwMenuEntry[] {
|
|
184
|
+
return this._state.menuModel ?? [];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
set menuItems(value: readonly AdwMenuEntry[]) {
|
|
188
|
+
this._state.setMenuModel(Array.isArray(value) ? value.map((entry) => ({ ...entry })) : null);
|
|
189
|
+
if (this._initialized) this._render();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The dropdown tooltip as set — `''` while unset. */
|
|
193
|
+
get dropdownTooltip(): string {
|
|
194
|
+
return this._state.dropdownTooltip;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
set dropdownTooltip(value: string) {
|
|
198
|
+
this.setAttribute('dropdown-tooltip', value ?? '');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The direction the popup opens in, and the arrow points. */
|
|
202
|
+
get direction(): SplitButtonDirection {
|
|
203
|
+
return this._state.direction;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
set direction(value: SplitButtonDirection) {
|
|
207
|
+
this.setAttribute('direction', value);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Whether an underline in the label marks a mnemonic. */
|
|
211
|
+
get useUnderline(): boolean {
|
|
212
|
+
return this._state.useUnderline;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
set useUnderline(value: boolean) {
|
|
216
|
+
if (value) this.setAttribute('use-underline', '');
|
|
217
|
+
else this.removeAttribute('use-underline');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Pop the menu up — `adw_split_button_popup()`. */
|
|
221
|
+
popup(): void {
|
|
222
|
+
this._state.openMenu();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Dismiss the menu — `adw_split_button_popdown()`. */
|
|
226
|
+
popdown(): void {
|
|
227
|
+
this._state.closeMenu();
|
|
61
228
|
}
|
|
62
229
|
|
|
63
230
|
connectedCallback() {
|
|
64
231
|
if (this._initialized) return;
|
|
65
232
|
this._initialized = true;
|
|
66
233
|
|
|
67
|
-
//
|
|
68
|
-
|
|
234
|
+
// Inline text is the label when no `label` attribute is given. This seed
|
|
235
|
+
// IS trimmed — markup indentation is not part of the value — while an
|
|
236
|
+
// explicit attribute never is, because C keys `text-button` off label[0].
|
|
237
|
+
this._inlineLabel = (this.textContent ?? '').trim();
|
|
69
238
|
|
|
70
239
|
this.classList.add('adw-split-button');
|
|
71
240
|
|
|
@@ -76,16 +245,31 @@ export class AdwSplitButton extends HTMLElement {
|
|
|
76
245
|
this._dropdownEl = document.createElement('button');
|
|
77
246
|
this._dropdownEl.type = 'button';
|
|
78
247
|
this._dropdownEl.className = 'adw-split-button-dropdown';
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
this._dropdownEl.appendChild(arrow);
|
|
248
|
+
this._dropdownEl.setAttribute('aria-haspopup', 'menu');
|
|
249
|
+
this._arrowEl = createAdwIcon(null);
|
|
250
|
+
this._dropdownEl.appendChild(this._arrowEl);
|
|
83
251
|
|
|
84
|
-
this._menuEl = document.createElement('
|
|
85
|
-
this._menuEl.
|
|
86
|
-
this._menuEl.
|
|
252
|
+
this._menuEl = document.createElement('adw-popover') as AdwPopover;
|
|
253
|
+
this._menuEl.classList.add('adw-split-button-menu');
|
|
254
|
+
this._menuEl.setAttribute('role', 'menu');
|
|
255
|
+
// The dropdown half IS a GtkMenuButton (adw-split-button.c:997 passes
|
|
256
|
+
// straight through to it), so its popover is a `popover.menu`.
|
|
257
|
+
this._menuEl.setAttribute('menu', '');
|
|
258
|
+
this._menuEl.setAttribute('align', 'end');
|
|
87
259
|
|
|
88
260
|
this.replaceChildren(this._actionEl, this._dropdownEl, this._menuEl);
|
|
261
|
+
// The DROPDOWN half is the anchor, not the whole control: the popover
|
|
262
|
+
// must not treat the half that toggles it as "outside" (it would close
|
|
263
|
+
// and immediately reopen), and Escape returns focus to the half that
|
|
264
|
+
// opened the menu.
|
|
265
|
+
this._menuEl.anchor = this._dropdownEl;
|
|
266
|
+
// The popover is a MIRROR of `SplitButtonState.open`, which stays the
|
|
267
|
+
// single source of truth (`dropdownEnabled`, the root `checked` fold and
|
|
268
|
+
// `notify::active` all read it). A dismissal the popover owns — outside
|
|
269
|
+
// click, Escape — is fed back in here rather than diverging.
|
|
270
|
+
this._menuEl.subscribe((open) => {
|
|
271
|
+
if (!open) this._state.closeMenu();
|
|
272
|
+
});
|
|
89
273
|
|
|
90
274
|
this._actionEl.addEventListener('click', () => {
|
|
91
275
|
if (this.hasAttribute('disabled')) return;
|
|
@@ -93,113 +277,208 @@ export class AdwSplitButton extends HTMLElement {
|
|
|
93
277
|
});
|
|
94
278
|
this._dropdownEl.addEventListener('click', () => {
|
|
95
279
|
if (this.hasAttribute('disabled')) return;
|
|
96
|
-
this.
|
|
280
|
+
this._state.toggleMenu();
|
|
97
281
|
});
|
|
98
282
|
|
|
283
|
+
// Seed the state from the attributes BEFORE subscribing, so connecting an
|
|
284
|
+
// element does not fire a burst of notify::* for properties that were
|
|
285
|
+
// never "changed" from an author's point of view.
|
|
286
|
+
this._syncStateFromAttributes();
|
|
287
|
+
this._state.subscribe((change) => this._onStateChange(change));
|
|
99
288
|
this._render();
|
|
100
289
|
}
|
|
101
290
|
|
|
102
|
-
|
|
103
|
-
|
|
291
|
+
attributeChangedCallback(name: string, _previous: string | null, value: string | null) {
|
|
292
|
+
if (!this._initialized) return;
|
|
293
|
+
switch (name) {
|
|
294
|
+
case 'label':
|
|
295
|
+
case 'icon-name':
|
|
296
|
+
this._applyContentAttribute(name, value);
|
|
297
|
+
break;
|
|
298
|
+
case 'use-underline':
|
|
299
|
+
this._state.setUseUnderline(value !== null);
|
|
300
|
+
break;
|
|
301
|
+
case 'direction':
|
|
302
|
+
this._state.setDirection(isSplitButtonDirection(value) ? value : 'down');
|
|
303
|
+
break;
|
|
304
|
+
case 'dropdown-tooltip':
|
|
305
|
+
this._state.setDropdownTooltip(value ?? '');
|
|
306
|
+
break;
|
|
307
|
+
case 'menu':
|
|
308
|
+
this._state.setMenuModel(parseMenuEntries(value));
|
|
309
|
+
break;
|
|
310
|
+
default:
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
// A change that the state machine swallowed as a no-op still has to reach
|
|
314
|
+
// the DOM (`tooltip`, `disabled`, the variant flags have no state).
|
|
315
|
+
this._render();
|
|
104
316
|
}
|
|
105
317
|
|
|
106
|
-
|
|
107
|
-
|
|
318
|
+
/** Seed label/icon/menu/direction/tooltip from the attributes present at connect time. */
|
|
319
|
+
private _syncStateFromAttributes(): void {
|
|
320
|
+
const label = this.getAttribute('label') ?? (this._inlineLabel.length > 0 ? this._inlineLabel : null);
|
|
321
|
+
const iconName = this.getAttribute('icon-name');
|
|
322
|
+
// With both present the icon wins — exactly where `set_label();
|
|
323
|
+
// set_icon_name();` leaves the widget (adw-split-button.c:749-771).
|
|
324
|
+
if (label !== null) this._state.setLabel(label);
|
|
325
|
+
if (iconName !== null) this._state.setIconName(iconName);
|
|
326
|
+
|
|
327
|
+
this._state.setUseUnderline(this.hasAttribute('use-underline'));
|
|
328
|
+
const direction = this.getAttribute('direction');
|
|
329
|
+
if (isSplitButtonDirection(direction)) this._state.setDirection(direction);
|
|
330
|
+
this._state.setDropdownTooltip(this.getAttribute('dropdown-tooltip') ?? '');
|
|
331
|
+
// The `menuItems` property may have been set before the element upgraded;
|
|
332
|
+
// the attribute only seeds an otherwise empty menu.
|
|
333
|
+
if (this._state.menuModel === null) this._state.setMenuModel(parseMenuEntries(this.getAttribute('menu')));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Route a `label` / `icon-name` attribute mutation into the content machine.
|
|
338
|
+
*
|
|
339
|
+
* C has no independent "unset" for either slot — clearing one is a side
|
|
340
|
+
* effect of setting another — so a REMOVED attribute falls back to whichever
|
|
341
|
+
* sibling is still present, and to the empty child otherwise. That fallback
|
|
342
|
+
* is what makes `removeAttribute('label')` actually drop the label; it used
|
|
343
|
+
* to be a silent no-op, so the storybook's icon-only mode rendered the icon
|
|
344
|
+
* next to a stale "Save".
|
|
345
|
+
*/
|
|
346
|
+
private _applyContentAttribute(name: 'label' | 'icon-name', value: string | null): void {
|
|
347
|
+
if (value !== null) {
|
|
348
|
+
if (name === 'label') this._state.setLabel(value);
|
|
349
|
+
else this._state.setIconName(value);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const sibling = this.getAttribute(name === 'label' ? 'icon-name' : 'label');
|
|
353
|
+
if (sibling === null) this._state.setChild(null);
|
|
354
|
+
else if (name === 'label') this._state.setIconName(sibling);
|
|
355
|
+
else this._state.setLabel(sibling);
|
|
108
356
|
}
|
|
109
357
|
|
|
110
|
-
private
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
this._menuEl.hidden = !active;
|
|
114
|
-
this.classList.toggle('active', active);
|
|
115
|
-
this._dropdownEl.setAttribute('aria-expanded', String(active));
|
|
116
|
-
if (active) {
|
|
117
|
-
document.addEventListener('pointerdown', this._onDocumentPointerDown);
|
|
118
|
-
} else {
|
|
119
|
-
document.removeEventListener('pointerdown', this._onDocumentPointerDown);
|
|
358
|
+
private _onStateChange(change: SplitButtonChange): void {
|
|
359
|
+
for (const property of change.notified) {
|
|
360
|
+
this.dispatchEvent(new CustomEvent(`notify::${property}`, { bubbles: true }));
|
|
120
361
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const raw = this.getAttribute('menu');
|
|
126
|
-
if (!raw) return [];
|
|
127
|
-
try {
|
|
128
|
-
const parsed: unknown = JSON.parse(raw);
|
|
129
|
-
if (!Array.isArray(parsed)) return [];
|
|
130
|
-
return parsed
|
|
131
|
-
.filter((entry): entry is MenuEntry => typeof entry === 'object' && entry !== null && 'label' in entry)
|
|
132
|
-
.map((entry) => ({ label: String(entry.label), action: entry.action }));
|
|
133
|
-
} catch {
|
|
134
|
-
return [];
|
|
362
|
+
const opened = change.open && !this._openReflected;
|
|
363
|
+
if (change.open !== this._openReflected) {
|
|
364
|
+
this._openReflected = change.open;
|
|
365
|
+
this.dispatchEvent(new CustomEvent('notify::active', { bubbles: true, detail: { active: change.open } }));
|
|
135
366
|
}
|
|
367
|
+
this._render();
|
|
368
|
+
// Focus lands in the menu on open, AFTER _render has built the rows —
|
|
369
|
+
// without it the arrow keys the popover now provides would have nothing
|
|
370
|
+
// to move from, which is half of why this element had none.
|
|
371
|
+
if (opened) this._menuEl.items[0]?.focus();
|
|
136
372
|
}
|
|
137
373
|
|
|
138
|
-
private _render() {
|
|
374
|
+
private _render(): void {
|
|
139
375
|
const disabled = this.hasAttribute('disabled');
|
|
140
376
|
this.classList.toggle('disabled', disabled);
|
|
141
377
|
this._actionEl.disabled = disabled;
|
|
142
|
-
|
|
378
|
+
// With neither a menu model nor a popover the dropdown is INSENSITIVE
|
|
379
|
+
// (adw-split-button.c:376-378, :394-396) — it must not open an empty
|
|
380
|
+
// popover, which is what the old unguarded click handler did.
|
|
381
|
+
this._dropdownEl.disabled = disabled || !this._state.dropdownEnabled;
|
|
143
382
|
|
|
144
383
|
this.classList.remove('flat', 'suggested-action', 'destructive-action');
|
|
145
|
-
for (const [
|
|
146
|
-
if (this.hasAttribute(
|
|
384
|
+
for (const [attribute, cls] of Object.entries(VARIANT_CLASSES)) {
|
|
385
|
+
if (this.hasAttribute(attribute)) this.classList.add(cls);
|
|
147
386
|
}
|
|
148
387
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
388
|
+
this._renderContent();
|
|
389
|
+
this._renderDropdown();
|
|
390
|
+
this._renderMenu();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** The action half + the root style classes libadwaita derives from it. */
|
|
394
|
+
private _renderContent(): void {
|
|
395
|
+
const { mode, label, iconName, styleClasses } = this._state;
|
|
396
|
+
|
|
397
|
+
// `splitbutton[.image-button][.text-button]` — the documented CSS node
|
|
398
|
+
// contract (adw-split-button.c:32-50, :145-165).
|
|
399
|
+
this.classList.toggle('image-button', styleClasses.includes('image-button'));
|
|
400
|
+
this.classList.toggle('text-button', styleClasses.includes('text-button'));
|
|
153
401
|
|
|
154
402
|
this._actionEl.replaceChildren();
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
403
|
+
this._actionEl.classList.toggle('icon-only', mode === 'icon');
|
|
404
|
+
|
|
405
|
+
if (mode === 'icon' && iconName !== null) {
|
|
406
|
+
this._actionEl.appendChild(createAdwIcon(iconName));
|
|
407
|
+
} else if (mode === 'label' && label !== null) {
|
|
408
|
+
// Never trimmed: two spaces are a valid label and render as one.
|
|
409
|
+
this._actionEl.appendChild(document.createTextNode(label));
|
|
160
410
|
}
|
|
161
|
-
if (label) this._actionEl.appendChild(document.createTextNode(label));
|
|
162
411
|
|
|
163
412
|
const tooltip = this.getAttribute('tooltip');
|
|
164
413
|
this._actionEl.title = tooltip ?? '';
|
|
165
414
|
// An icon-only action half has no text, so give it an accessible name —
|
|
166
415
|
// prefer the tooltip, fall back to the symbolic icon name (WCAG 4.1.2).
|
|
167
|
-
if (icon
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
this._actionEl.removeAttribute('aria-label');
|
|
171
|
-
}
|
|
416
|
+
if (mode === 'icon') this._actionEl.setAttribute('aria-label', tooltip ?? (iconName || 'Action'));
|
|
417
|
+
else this._actionEl.removeAttribute('aria-label');
|
|
418
|
+
}
|
|
172
419
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
this.
|
|
178
|
-
|
|
420
|
+
/** The dropdown half: arrow glyph, tooltip/accessible name, expanded state. */
|
|
421
|
+
private _renderDropdown(): void {
|
|
422
|
+
const { direction, open } = this._state;
|
|
423
|
+
const glyph = splitButtonArrowIcon(direction);
|
|
424
|
+
this._arrowEl.iconName = ARROW_MASK_CLASSES[glyph];
|
|
425
|
+
// No pan-up mask ships yet, so it is the down arrow turned over. Keyed off
|
|
426
|
+
// the GLYPH, not the direction: the flip belongs to the mask substitution.
|
|
427
|
+
this._arrowEl.style.transform = glyph === 'pan-up-symbolic' ? 'rotate(180deg)' : '';
|
|
428
|
+
// Where the popup goes is core's call (`none` → `down`); only the
|
|
429
|
+
// direction → CSS-axis mapping is ours.
|
|
430
|
+
this._menuEl.position = POPOVER_POSITIONS[splitButtonPopupDirection(direction)];
|
|
179
431
|
|
|
180
|
-
//
|
|
181
|
-
|
|
432
|
+
// An empty value RESTORES the translated default (adw-split-button.c:1044-1051)
|
|
433
|
+
// instead of leaving the button without an accessible name.
|
|
434
|
+
const { text } = resolveDropdownTooltip(this._state.dropdownTooltip);
|
|
435
|
+
this._dropdownEl.title = text;
|
|
436
|
+
this._dropdownEl.setAttribute('aria-label', text);
|
|
437
|
+
this._dropdownEl.setAttribute('aria-expanded', String(open));
|
|
438
|
+
|
|
439
|
+
// update_state folds both halves onto the root (adw-split-button.c:118-143).
|
|
440
|
+
// The browser gives the ACTIVE half of that fold for free — CSS `:active`
|
|
441
|
+
// matches the ancestors of the pressed element — so only CHECKED, which
|
|
442
|
+
// the arrow half carries while the menu is open, has to be applied here.
|
|
443
|
+
const { checked } = splitButtonRootState(
|
|
444
|
+
{ active: false, checked: false, keyboardActivating: false },
|
|
445
|
+
{ active: false, checked: open, keyboardActivating: false },
|
|
446
|
+
);
|
|
447
|
+
this.classList.toggle('checked', checked);
|
|
448
|
+
// `.active` predates the lift and stays the open-state hook; libadwaita
|
|
449
|
+
// spells the same state `checked`.
|
|
450
|
+
this.classList.toggle('active', open);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** The popover contents, one button per menu entry, dispatched BY POSITION. */
|
|
454
|
+
private _renderMenu(): void {
|
|
182
455
|
this._menuEl.replaceChildren();
|
|
183
|
-
this._menuEl.
|
|
184
|
-
|
|
456
|
+
this._menuEl.open = this._state.open;
|
|
457
|
+
|
|
458
|
+
for (const [index, entry] of (this._state.menuModel ?? []).entries()) {
|
|
185
459
|
const item = document.createElement('button');
|
|
186
460
|
item.type = 'button';
|
|
187
|
-
|
|
461
|
+
// `.adw-popover-item` is what makes the row navigable: it is the
|
|
462
|
+
// selector `<adw-popover>` walks for arrow/Home/End/Enter, which this
|
|
463
|
+
// element never had.
|
|
464
|
+
item.className = 'adw-popover-item adw-split-button-menu-item';
|
|
188
465
|
item.setAttribute('role', 'menuitem');
|
|
466
|
+
item.tabIndex = -1;
|
|
189
467
|
item.textContent = entry.label;
|
|
190
468
|
item.addEventListener('click', () => {
|
|
191
|
-
|
|
469
|
+
// By position, never by label: two entries called "Copy" are
|
|
470
|
+
// legal and must dispatch their own action (c:385-388).
|
|
471
|
+
const activated = this._state.activateMenuEntry(index);
|
|
472
|
+
if (activated === null) return;
|
|
192
473
|
this.dispatchEvent(
|
|
193
474
|
new CustomEvent('menu-activated', {
|
|
194
475
|
bubbles: true,
|
|
195
|
-
detail: { label:
|
|
476
|
+
detail: { label: activated.label, action: activated.action, index },
|
|
196
477
|
}),
|
|
197
478
|
);
|
|
198
479
|
});
|
|
199
480
|
this._menuEl.appendChild(item);
|
|
200
481
|
}
|
|
201
|
-
// A split button with no menu entries hides its (empty) popover entirely.
|
|
202
|
-
if (entries.length === 0) this._setActive(false);
|
|
203
482
|
}
|
|
204
483
|
}
|
|
205
484
|
|
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
// Reference: refs/adwaita-web/adwaita-web/scss/_status_page.scss
|
|
5
5
|
// Reference: refs/libadwaita/src/stylesheet/widgets/_status-page.scss
|
|
6
6
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
7
|
-
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web
|
|
7
|
+
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the
|
|
8
|
+
// icon node is <adw-icon>.
|
|
9
|
+
|
|
10
|
+
import { type AdwIcon, createAdwIcon } from './adw-icon.js';
|
|
8
11
|
|
|
9
12
|
export class AdwStatusPage extends HTMLElement {
|
|
10
|
-
private _iconEl!:
|
|
13
|
+
private _iconEl!: AdwIcon;
|
|
11
14
|
private _titleEl!: HTMLSpanElement;
|
|
12
15
|
private _descEl!: HTMLSpanElement;
|
|
13
16
|
private _childEl!: HTMLDivElement;
|
|
@@ -23,9 +26,7 @@ export class AdwStatusPage extends HTMLElement {
|
|
|
23
26
|
|
|
24
27
|
const children = Array.from(this.childNodes);
|
|
25
28
|
|
|
26
|
-
this._iconEl =
|
|
27
|
-
this._iconEl.className = 'adw-status-page-icon adw-icon';
|
|
28
|
-
this._iconEl.setAttribute('aria-hidden', 'true');
|
|
29
|
+
this._iconEl = createAdwIcon(null, 'adw-status-page-icon');
|
|
29
30
|
|
|
30
31
|
this._titleEl = document.createElement('span');
|
|
31
32
|
this._titleEl.className = 'adw-status-page-title';
|
|
@@ -46,10 +47,9 @@ export class AdwStatusPage extends HTMLElement {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
private _render() {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
this._iconEl.
|
|
52
|
-
this._iconEl.hidden = icon.length === 0;
|
|
50
|
+
// The element swaps the mask class and keeps the size class.
|
|
51
|
+
this._iconEl.iconName = this.getAttribute('icon');
|
|
52
|
+
this._iconEl.hidden = this._iconEl.resolvedIconName === '';
|
|
53
53
|
|
|
54
54
|
const title = this.getAttribute('title') ?? '';
|
|
55
55
|
this._titleEl.textContent = title;
|