@gjsify/adwaita-web 0.30.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adwaita-web.css +786 -272
- 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-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 +37 -11
- package/lib/types/elements/adw-menu-button.d.ts +6 -6
- package/lib/types/elements/adw-navigation-split-view.d.ts +6 -0
- package/lib/types/elements/adw-navigation-view.d.ts +63 -20
- package/lib/types/elements/adw-overlay-split-view.d.ts +38 -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-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 +27 -19
- package/lib/types/elements/adw-view-switcher-bar.d.ts +23 -7
- package/lib/types/elements/adw-view-switcher.d.ts +22 -11
- package/lib/types/elements/adw-window-title.d.ts +3 -1
- 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/_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/_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 +7 -3
- 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 +64 -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 +56 -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 +139 -0
- package/src/adw-banner.spec.ts +152 -0
- package/src/adw-button-content.spec.ts +183 -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-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 +194 -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 +301 -0
- package/src/elements/adw-about-dialog.ts +223 -163
- package/src/elements/adw-action-row.ts +120 -10
- 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 +15 -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 +240 -129
- package/src/elements/adw-menu-button.ts +93 -70
- package/src/elements/adw-navigation-split-view.ts +48 -7
- package/src/elements/adw-navigation-view.ts +286 -148
- package/src/elements/adw-overlay-split-view.ts +164 -14
- 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 +23 -8
- 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 +682 -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 +162 -85
- package/src/elements/adw-view-switcher-bar.ts +163 -81
- package/src/elements/adw-view-switcher.ts +231 -112
- package/src/elements/adw-window-title.ts +44 -11
- package/src/elements/adw-wrap-box.ts +131 -35
- 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 +309 -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 +446 -0
|
@@ -12,6 +12,21 @@
|
|
|
12
12
|
// Escape and supports arrow-key navigation, Home/End, Enter to pick, and
|
|
13
13
|
// type-ahead (typing jumps to the next matching option).
|
|
14
14
|
//
|
|
15
|
+
// The popover is `<adw-popover>` and the dismissal/keyboard machine is
|
|
16
|
+
// `@gjsify/adwaita-core`'s (ADR 0004). So is the SELECTION: the options list,
|
|
17
|
+
// the index↔value mapping, the bounds guards and the
|
|
18
|
+
// programmatic-vs-interactive split are {@link ComboState}, the same state
|
|
19
|
+
// machine `<adw-combo-row>` composes — this element imported NOTHING from core
|
|
20
|
+
// and re-derived all of it, which is how the two selectors ended up accepting
|
|
21
|
+
// different option vocabularies (this one took `{value,label}` descriptors, the
|
|
22
|
+
// row only bare strings). What stays here is what is genuinely a DROP-DOWN: the
|
|
23
|
+
// search filter and the type-ahead buffer.
|
|
24
|
+
//
|
|
25
|
+
// The arrow/Home/End wrap arithmetic left earlier, with the popover lift — it
|
|
26
|
+
// was a second copy of `<adw-menu-button>`'s, and both had the same bug (ArrowUp
|
|
27
|
+
// from an unfocused list landed on the SECOND-TO-LAST option, which
|
|
28
|
+
// `enable-search` hit on every single open because it focuses the entry).
|
|
29
|
+
//
|
|
15
30
|
// Attributes:
|
|
16
31
|
// options / items — JSON array: `["a","b"]` or `[{"value":"a","label":"A"}]`.
|
|
17
32
|
// selected — the selected index (number).
|
|
@@ -28,72 +43,78 @@
|
|
|
28
43
|
// `notify::selected` (CustomEvent, bubbles, detail = { selected }) — the index
|
|
29
44
|
// changed (mirrors the Gtk.DropDown `notify::selected` GObject signal).
|
|
30
45
|
// `change` (CustomEvent, bubbles, detail = { index, value, label }) —
|
|
31
|
-
// a convenience event carrying the full selection.
|
|
46
|
+
// a convenience event carrying the full selection. Fires ONLY on a user
|
|
47
|
+
// pick — `ComboState`'s `interactive` flag is what tells the two apart.
|
|
32
48
|
// Reference: refs/libadwaita/src/stylesheet/widgets/_menus.scss (popover surface)
|
|
33
49
|
// Reference: refs/adwaita-web (adw-combo-row dropdown-arrow mask technique)
|
|
34
50
|
// Copyright (c) GNOME contributors (libadwaita) / the GTK team. LGPLv2.1+.
|
|
35
51
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
36
52
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
// SIDE-EFFECT import, deliberately separate from the type import below: it is
|
|
54
|
+
// what guarantees `adw-popover` is defined before this module's own
|
|
55
|
+
// `customElements.define` can upgrade a server-rendered `<adw-drop-down>` and
|
|
56
|
+
// build one. A combined `import { AdwPopover }` would NOT do it — the binding is
|
|
57
|
+
// only ever used in type position here, and this package compiles without
|
|
58
|
+
// `verbatimModuleSyntax`, so TypeScript would elide the whole statement and take
|
|
59
|
+
// the registration with it.
|
|
60
|
+
import './adw-popover.js';
|
|
61
|
+
import type { AdwPopover } from './adw-popover.js';
|
|
62
|
+
|
|
63
|
+
import { ComboState, normalizeComboOptions } from '@gjsify/adwaita-core';
|
|
64
|
+
import type { AdwComboOption, AdwComboOptionInput } from '@gjsify/adwaita-core';
|
|
65
|
+
|
|
66
|
+
import { createAdwIcon } from './adw-icon.js';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A dropdown option — `value` is the stable id, `label` the shown text.
|
|
70
|
+
*
|
|
71
|
+
* An alias of core's {@link AdwComboOption}: the drop-down and the combo row are
|
|
72
|
+
* the same selection model behind two chromes, and they had two names for one
|
|
73
|
+
* descriptor. Kept exported so the published type name does not break.
|
|
74
|
+
*/
|
|
75
|
+
export type AdwDropDownOption = AdwComboOption;
|
|
44
76
|
|
|
45
77
|
export class AdwDropDown extends HTMLElement {
|
|
46
78
|
private _buttonEl!: HTMLButtonElement;
|
|
47
79
|
private _labelEl!: HTMLSpanElement;
|
|
48
|
-
private _popoverEl!:
|
|
80
|
+
private _popoverEl!: AdwPopover;
|
|
49
81
|
private _listEl!: HTMLDivElement;
|
|
50
82
|
private _searchEl: HTMLInputElement | null = null;
|
|
51
|
-
private _options: AdwDropDownOption[] = [];
|
|
52
83
|
private _itemButtons: HTMLButtonElement[] = [];
|
|
53
|
-
|
|
54
|
-
private
|
|
84
|
+
/** The headless options list + selectedIndex↔selectedValue machine (ADR 0004). */
|
|
85
|
+
private readonly _state = new ComboState();
|
|
55
86
|
private _initialized = false;
|
|
56
87
|
private _typeAheadBuffer = '';
|
|
57
88
|
private _typeAheadTimer: ReturnType<typeof setTimeout> | null = null;
|
|
58
89
|
|
|
59
|
-
private _onDocumentPointerDown = (event: Event): void => {
|
|
60
|
-
if (!this.contains(event.target as Node)) this._setActive(false);
|
|
61
|
-
};
|
|
62
|
-
private _onDocumentKeyDown = (event: KeyboardEvent): void => {
|
|
63
|
-
if (event.key === 'Escape') {
|
|
64
|
-
event.stopPropagation();
|
|
65
|
-
this._setActive(false);
|
|
66
|
-
this._buttonEl.focus();
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
|
|
70
90
|
static get observedAttributes() {
|
|
71
91
|
return ['options', 'items', 'selected', 'enable-search', 'disabled'];
|
|
72
92
|
}
|
|
73
93
|
|
|
74
94
|
/** The options list. Setting it rebuilds the popover and clamps the selection. */
|
|
75
95
|
get options(): AdwDropDownOption[] {
|
|
76
|
-
return this.
|
|
96
|
+
return this._state.options;
|
|
77
97
|
}
|
|
78
98
|
|
|
79
|
-
set options(value: ReadonlyArray<
|
|
80
|
-
|
|
81
|
-
|
|
99
|
+
set options(value: ReadonlyArray<AdwComboOptionInput>) {
|
|
100
|
+
// `setOptions` resets the index to 0 when it no longer fits — the same
|
|
101
|
+
// clamp this element used to spell out.
|
|
102
|
+
this._state.setOptions(normalizeComboOptions(value));
|
|
82
103
|
if (this._initialized) this._renderAll();
|
|
83
104
|
}
|
|
84
105
|
|
|
85
106
|
/** Alias of `options` (Gtk.DropDown's model is often called "items"). */
|
|
86
107
|
get items(): AdwDropDownOption[] {
|
|
87
|
-
return this.
|
|
108
|
+
return this._state.options;
|
|
88
109
|
}
|
|
89
110
|
|
|
90
|
-
set items(value: ReadonlyArray<
|
|
111
|
+
set items(value: ReadonlyArray<AdwComboOptionInput>) {
|
|
91
112
|
this.options = value;
|
|
92
113
|
}
|
|
93
114
|
|
|
94
115
|
/** The selected index. Out-of-range values are ignored (Gtk.DropDown:selected). */
|
|
95
116
|
get selected(): number {
|
|
96
|
-
return this.
|
|
117
|
+
return this._state.selectedIndex;
|
|
97
118
|
}
|
|
98
119
|
|
|
99
120
|
set selected(value: number) {
|
|
@@ -102,17 +123,17 @@ export class AdwDropDown extends HTMLElement {
|
|
|
102
123
|
|
|
103
124
|
/** The selected option's value, or '' when empty (Gtk.DropDown:selected-item). */
|
|
104
125
|
get selectedValue(): string {
|
|
105
|
-
return this.
|
|
126
|
+
return this._state.selectedValue;
|
|
106
127
|
}
|
|
107
128
|
|
|
108
129
|
set selectedValue(value: string) {
|
|
109
|
-
const index = this.
|
|
130
|
+
const index = this._state.options.findIndex((o) => o.value === value);
|
|
110
131
|
if (index >= 0) this._selectIndex(index);
|
|
111
132
|
}
|
|
112
133
|
|
|
113
134
|
/** The selected option descriptor, or null when empty. */
|
|
114
135
|
get selectedItem(): AdwDropDownOption | null {
|
|
115
|
-
return this.
|
|
136
|
+
return this._state.options[this._state.selectedIndex] ?? null;
|
|
116
137
|
}
|
|
117
138
|
|
|
118
139
|
/** Whether the search entry is shown in the popover. */
|
|
@@ -127,7 +148,7 @@ export class AdwDropDown extends HTMLElement {
|
|
|
127
148
|
|
|
128
149
|
/** Whether the popover is currently open. */
|
|
129
150
|
get active(): boolean {
|
|
130
|
-
return this.
|
|
151
|
+
return this._popoverEl?.open ?? false;
|
|
131
152
|
}
|
|
132
153
|
|
|
133
154
|
connectedCallback() {
|
|
@@ -145,48 +166,48 @@ export class AdwDropDown extends HTMLElement {
|
|
|
145
166
|
this._labelEl = document.createElement('span');
|
|
146
167
|
this._labelEl.className = 'adw-drop-down-label';
|
|
147
168
|
|
|
148
|
-
|
|
149
|
-
arrow.className = 'adw-icon adw-icon--go-down adw-drop-down-arrow';
|
|
150
|
-
arrow.setAttribute('aria-hidden', 'true');
|
|
151
|
-
|
|
152
|
-
this._buttonEl.append(this._labelEl, arrow);
|
|
169
|
+
this._buttonEl.append(this._labelEl, createAdwIcon('go-down', 'adw-drop-down-arrow'));
|
|
153
170
|
this._buttonEl.addEventListener('click', () => {
|
|
154
171
|
if (this.hasAttribute('disabled')) return;
|
|
155
|
-
|
|
172
|
+
if (!this._popoverEl.open && this._state.count === 0) return;
|
|
173
|
+
this._popoverEl.open = !this._popoverEl.open;
|
|
156
174
|
});
|
|
157
175
|
this._buttonEl.addEventListener('keydown', (event) => this._onButtonKeyDown(event));
|
|
158
176
|
|
|
159
|
-
this._popoverEl = document.createElement('
|
|
160
|
-
this._popoverEl.
|
|
177
|
+
this._popoverEl = document.createElement('adw-popover') as AdwPopover;
|
|
178
|
+
this._popoverEl.classList.add('adw-drop-down-popover');
|
|
161
179
|
this._popoverEl.setAttribute('role', 'listbox');
|
|
162
|
-
|
|
163
|
-
|
|
180
|
+
// A dropdown's popover is a `popover.menu` too — `dropdown { popover.menu
|
|
181
|
+
// { … } }` (_dropdowns.scss:22) — even though its rows are ARIA `option`s
|
|
182
|
+
// rather than `menuitem`s. The style class and the role answer different
|
|
183
|
+
// questions, so neither is derived from the other.
|
|
184
|
+
this._popoverEl.setAttribute('menu', '');
|
|
164
185
|
|
|
165
186
|
this._listEl = document.createElement('div');
|
|
166
187
|
this._listEl.className = 'adw-drop-down-list';
|
|
167
188
|
this._popoverEl.appendChild(this._listEl);
|
|
168
189
|
|
|
169
|
-
// Seed options from the property (already set) or the JSON attribute
|
|
170
|
-
|
|
190
|
+
// Seed options from the property (already set) or the JSON attribute,
|
|
191
|
+
// BEFORE anything subscribes, so building the initial DOM below is not
|
|
192
|
+
// driven by a change notification.
|
|
193
|
+
if (this._state.count === 0) this._state.setOptions(this._parseOptionsAttr());
|
|
171
194
|
const attrSelected = Number.parseInt(this.getAttribute('selected') ?? '', 10);
|
|
172
|
-
if (
|
|
173
|
-
this._selected = attrSelected;
|
|
174
|
-
}
|
|
195
|
+
if (this._state.hasIndex(attrSelected)) this._state.setSelectedIndex(attrSelected);
|
|
175
196
|
|
|
176
197
|
this.replaceChildren(this._buttonEl, this._popoverEl);
|
|
198
|
+
this._popoverEl.anchor = this._buttonEl;
|
|
199
|
+
this._popoverEl.subscribe((open) => this._onPopoverToggled(open));
|
|
200
|
+
// Type-ahead is the ONE key class the shared popover deliberately leaves
|
|
201
|
+
// to the renderer (`resolvePopoverKey` returns 'none' for printables), so
|
|
202
|
+
// it needs a listener of its own alongside the popover's.
|
|
203
|
+
this._popoverEl.addEventListener('keydown', (event) => this._onPopoverTypeAhead(event));
|
|
177
204
|
this._renderAll();
|
|
178
205
|
}
|
|
179
206
|
|
|
180
|
-
disconnectedCallback() {
|
|
181
|
-
document.removeEventListener('pointerdown', this._onDocumentPointerDown);
|
|
182
|
-
document.removeEventListener('keydown', this._onDocumentKeyDown, true);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
207
|
attributeChangedCallback(name: string, _old: string | null, value: string | null) {
|
|
186
208
|
if (!this._initialized || !this._buttonEl) return;
|
|
187
209
|
if (name === 'options' || name === 'items') {
|
|
188
|
-
this.
|
|
189
|
-
if (this._selected >= this._options.length) this._selected = 0;
|
|
210
|
+
this._state.setOptions(this._parseOptionsAttr());
|
|
190
211
|
this._renderAll();
|
|
191
212
|
} else if (name === 'selected') {
|
|
192
213
|
const index = Number.parseInt(value ?? '', 10);
|
|
@@ -195,7 +216,7 @@ export class AdwDropDown extends HTMLElement {
|
|
|
195
216
|
this._renderPopover();
|
|
196
217
|
} else if (name === 'disabled') {
|
|
197
218
|
this._buttonEl.disabled = this.hasAttribute('disabled');
|
|
198
|
-
if (this.hasAttribute('disabled')) this.
|
|
219
|
+
if (this.hasAttribute('disabled')) this._popoverEl.popdown();
|
|
199
220
|
}
|
|
200
221
|
}
|
|
201
222
|
|
|
@@ -205,17 +226,24 @@ export class AdwDropDown extends HTMLElement {
|
|
|
205
226
|
const raw = this.getAttribute('options') ?? this.getAttribute('items');
|
|
206
227
|
if (!raw) return [];
|
|
207
228
|
try {
|
|
208
|
-
|
|
209
|
-
|
|
229
|
+
// `normalizeComboOptions` already guards a non-array, so a JSON
|
|
230
|
+
// object or scalar yields the empty model rather than a throw.
|
|
231
|
+
return normalizeComboOptions(JSON.parse(raw) as AdwComboOptionInput[]);
|
|
210
232
|
} catch {
|
|
211
233
|
return [];
|
|
212
234
|
}
|
|
213
235
|
}
|
|
214
236
|
|
|
215
237
|
private _selectIndex(index: number, opts: { fromAttr?: boolean; fromUser?: boolean } = {}): void {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
238
|
+
// The bounds arithmetic is `ComboState`'s; the POLICY of rejecting an
|
|
239
|
+
// out-of-range set is this element's published `selected` contract, and
|
|
240
|
+
// `<adw-combo-row>` deliberately answers it the other way — see
|
|
241
|
+
// `ComboState.hasIndex`.
|
|
242
|
+
if (!this._state.hasIndex(index)) return;
|
|
243
|
+
// Both core setters return "did it change", which is what gates the
|
|
244
|
+
// notify — no second copy of the current index is kept here to compare
|
|
245
|
+
// against, and `setOptions`' own label re-sync cannot masquerade as one.
|
|
246
|
+
const changed = opts.fromUser ? this._state.select(index) : this._state.setSelectedIndex(index);
|
|
219
247
|
this._updateLabel();
|
|
220
248
|
this._updateSelectedStates();
|
|
221
249
|
// Keep the attribute in sync without re-entering the guarded callback.
|
|
@@ -223,7 +251,7 @@ export class AdwDropDown extends HTMLElement {
|
|
|
223
251
|
this.setAttribute('selected', String(index));
|
|
224
252
|
}
|
|
225
253
|
if (changed) {
|
|
226
|
-
const option = this.
|
|
254
|
+
const option = this._state.options[index];
|
|
227
255
|
// `notify::selected` mirrors GObject property-notify: it fires on EVERY change,
|
|
228
256
|
// programmatic included.
|
|
229
257
|
this.dispatchEvent(new CustomEvent('notify::selected', { bubbles: true, detail: { selected: index } }));
|
|
@@ -242,27 +270,17 @@ export class AdwDropDown extends HTMLElement {
|
|
|
242
270
|
}
|
|
243
271
|
}
|
|
244
272
|
|
|
245
|
-
private
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
this.
|
|
250
|
-
this.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
this._filter('');
|
|
254
|
-
document.addEventListener('pointerdown', this._onDocumentPointerDown);
|
|
255
|
-
// Capture Escape before it reaches the button/popover handlers.
|
|
256
|
-
document.addEventListener('keydown', this._onDocumentKeyDown, true);
|
|
257
|
-
if (this._searchEl) {
|
|
258
|
-
this._searchEl.value = '';
|
|
259
|
-
this._searchEl.focus();
|
|
260
|
-
} else {
|
|
261
|
-
(this._visibleItems()[this._selected] ?? this._visibleItems()[0])?.focus();
|
|
262
|
-
}
|
|
273
|
+
private _onPopoverToggled(open: boolean): void {
|
|
274
|
+
this.classList.toggle('active', open);
|
|
275
|
+
this._buttonEl.setAttribute('aria-expanded', String(open));
|
|
276
|
+
if (!open) return;
|
|
277
|
+
this._filter('');
|
|
278
|
+
if (this._searchEl) {
|
|
279
|
+
this._searchEl.value = '';
|
|
280
|
+
this._searchEl.focus();
|
|
263
281
|
} else {
|
|
264
|
-
|
|
265
|
-
|
|
282
|
+
const visible = this._visibleItems();
|
|
283
|
+
(visible[this._state.selectedIndex] ?? visible[0])?.focus();
|
|
266
284
|
}
|
|
267
285
|
}
|
|
268
286
|
|
|
@@ -270,35 +288,18 @@ export class AdwDropDown extends HTMLElement {
|
|
|
270
288
|
if (this.hasAttribute('disabled')) return;
|
|
271
289
|
if (event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Enter' || event.key === ' ') {
|
|
272
290
|
event.preventDefault();
|
|
273
|
-
this.
|
|
274
|
-
} else if (event.key.length === 1 && !this.
|
|
291
|
+
if (this._state.count > 0) this._popoverEl.popup();
|
|
292
|
+
} else if (event.key.length === 1 && !this._popoverEl.open) {
|
|
275
293
|
// Type-ahead directly on the closed button (Gtk.DropDown behaviour).
|
|
276
294
|
this._typeAhead(event.key);
|
|
277
295
|
}
|
|
278
296
|
}
|
|
279
297
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
event.preventDefault();
|
|
286
|
-
items[(current + 1 + items.length) % items.length]?.focus();
|
|
287
|
-
} else if (event.key === 'ArrowUp') {
|
|
288
|
-
event.preventDefault();
|
|
289
|
-
items[(current - 1 + items.length) % items.length]?.focus();
|
|
290
|
-
} else if (event.key === 'Home') {
|
|
291
|
-
event.preventDefault();
|
|
292
|
-
items[0]?.focus();
|
|
293
|
-
} else if (event.key === 'End') {
|
|
294
|
-
event.preventDefault();
|
|
295
|
-
items[items.length - 1]?.focus();
|
|
296
|
-
} else if ((event.key === 'Enter' || event.key === ' ') && current >= 0) {
|
|
297
|
-
event.preventDefault();
|
|
298
|
-
items[current]?.click();
|
|
299
|
-
} else if (!this._searchEl && event.key.length === 1) {
|
|
300
|
-
this._typeAhead(event.key);
|
|
301
|
-
}
|
|
298
|
+
/** Printable keys inside an OPEN popover, when there is no search entry to type into. */
|
|
299
|
+
private _onPopoverTypeAhead(event: KeyboardEvent): void {
|
|
300
|
+
if (this._searchEl) return;
|
|
301
|
+
if (event.key.length !== 1) return;
|
|
302
|
+
this._typeAhead(event.key);
|
|
302
303
|
}
|
|
303
304
|
|
|
304
305
|
/** Type-ahead: accumulate keys, focus/select the next matching option. */
|
|
@@ -306,9 +307,9 @@ export class AdwDropDown extends HTMLElement {
|
|
|
306
307
|
this._typeAheadBuffer += char.toLowerCase();
|
|
307
308
|
if (this._typeAheadTimer) clearTimeout(this._typeAheadTimer);
|
|
308
309
|
this._typeAheadTimer = setTimeout(() => (this._typeAheadBuffer = ''), 800);
|
|
309
|
-
const match = this.
|
|
310
|
+
const match = this._state.options.findIndex((o) => o.label.toLowerCase().startsWith(this._typeAheadBuffer));
|
|
310
311
|
if (match < 0) return;
|
|
311
|
-
if (this.
|
|
312
|
+
if (this._popoverEl.open) this._itemButtons[match]?.focus();
|
|
312
313
|
else this._selectIndex(match, { fromUser: true });
|
|
313
314
|
}
|
|
314
315
|
|
|
@@ -316,7 +317,7 @@ export class AdwDropDown extends HTMLElement {
|
|
|
316
317
|
private _filter(query: string): void {
|
|
317
318
|
const q = query.trim().toLowerCase();
|
|
318
319
|
this._itemButtons.forEach((button, index) => {
|
|
319
|
-
const match = q.length === 0 || (this.
|
|
320
|
+
const match = q.length === 0 || (this._state.options[index]?.label.toLowerCase().includes(q) ?? false);
|
|
320
321
|
button.hidden = !match;
|
|
321
322
|
});
|
|
322
323
|
}
|
|
@@ -328,7 +329,6 @@ export class AdwDropDown extends HTMLElement {
|
|
|
328
329
|
private _renderAll(): void {
|
|
329
330
|
this._buttonEl.disabled = this.hasAttribute('disabled');
|
|
330
331
|
this.classList.toggle('disabled', this.hasAttribute('disabled'));
|
|
331
|
-
if (this._selected >= this._options.length) this._selected = 0;
|
|
332
332
|
this._renderPopover();
|
|
333
333
|
this._updateLabel();
|
|
334
334
|
}
|
|
@@ -337,12 +337,12 @@ export class AdwDropDown extends HTMLElement {
|
|
|
337
337
|
// The setters (`options`/`selected`) may run before connectedCallback
|
|
338
338
|
// builds the DOM — stay a no-op until the label element exists.
|
|
339
339
|
if (!this._labelEl) return;
|
|
340
|
-
this._labelEl.textContent = this.
|
|
340
|
+
this._labelEl.textContent = this._state.selectedLabel;
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
private _updateSelectedStates(): void {
|
|
344
344
|
this._itemButtons.forEach((button, index) => {
|
|
345
|
-
const isSelected = index === this.
|
|
345
|
+
const isSelected = index === this._state.selectedIndex;
|
|
346
346
|
button.classList.toggle('selected', isSelected);
|
|
347
347
|
button.setAttribute('aria-selected', String(isSelected));
|
|
348
348
|
});
|
|
@@ -367,10 +367,10 @@ export class AdwDropDown extends HTMLElement {
|
|
|
367
367
|
if (existing) existing.remove();
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
-
for (const [index, option] of this.
|
|
370
|
+
for (const [index, option] of this._state.options.entries()) {
|
|
371
371
|
const item = document.createElement('button');
|
|
372
372
|
item.type = 'button';
|
|
373
|
-
item.className = 'adw-drop-down-item';
|
|
373
|
+
item.className = 'adw-popover-item adw-drop-down-item';
|
|
374
374
|
item.setAttribute('role', 'option');
|
|
375
375
|
item.tabIndex = -1;
|
|
376
376
|
|
|
@@ -386,7 +386,7 @@ export class AdwDropDown extends HTMLElement {
|
|
|
386
386
|
item.append(label, check);
|
|
387
387
|
item.addEventListener('click', () => {
|
|
388
388
|
this._selectIndex(index, { fromUser: true });
|
|
389
|
-
this.
|
|
389
|
+
this._popoverEl.popdown();
|
|
390
390
|
this._buttonEl.focus();
|
|
391
391
|
});
|
|
392
392
|
this._listEl.appendChild(item);
|
|
@@ -394,19 +394,8 @@ export class AdwDropDown extends HTMLElement {
|
|
|
394
394
|
}
|
|
395
395
|
|
|
396
396
|
this._updateSelectedStates();
|
|
397
|
-
if (this.
|
|
397
|
+
if (this._state.count === 0) this._popoverEl.popdown();
|
|
398
398
|
}
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
-
/** Normalise raw options (strings or `{value,label}`) to a stable descriptor list. */
|
|
402
|
-
function normalizeOptions(raw: ReadonlyArray<RawOption>): AdwDropDownOption[] {
|
|
403
|
-
if (!Array.isArray(raw)) return [];
|
|
404
|
-
return raw.map((entry) => {
|
|
405
|
-
if (typeof entry === 'string') return { value: entry, label: entry };
|
|
406
|
-
const value = entry.value !== undefined ? String(entry.value) : String(entry.label ?? '');
|
|
407
|
-
const label = entry.label !== undefined ? String(entry.label) : value;
|
|
408
|
-
return { value, label };
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
|
|
412
401
|
customElements.define('adw-drop-down', AdwDropDown);
|