@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
|
@@ -51,46 +51,36 @@
|
|
|
51
51
|
// Responsive: the grid scrolls horizontally within its OWN container on narrow
|
|
52
52
|
// viewports (it never overflows the page); it sits inside an <adw-card>/boxed
|
|
53
53
|
// surface and follows light + dark via the adwaita-web CSS variables.
|
|
54
|
+
// The derivations are HEADLESS and live in `@gjsify/adwaita-core` (ADR 0004):
|
|
55
|
+
// the column tracks, the cell classes, the two normalisers, the cell text and
|
|
56
|
+
// the activation rule. `@gjsify/adwaita-nativescript` renders the same widget
|
|
57
|
+
// over `GridLayout` `ItemSpec`s — NativeScript has no subgrid — so the TRACK
|
|
58
|
+
// rule is emitted as a renderer-neutral descriptor and only the mapping onto
|
|
59
|
+
// `grid-template-columns` stays here. `@gjsify/adwaita-core/conformance`'s
|
|
60
|
+
// `DATA_GRID_*_VECTORS` is the table both ports are held to.
|
|
54
61
|
// Reference: Gtk.Grid usage in the Buchhaltung BWA view (native financial grid).
|
|
55
62
|
// Reference: refs/libadwaita/src/stylesheet/_colors.scss (separator / card tokens).
|
|
56
63
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
57
64
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
/** Render cells in a monospace family (tabular figures apply regardless). */
|
|
78
|
-
monospace?: boolean;
|
|
79
|
-
/** Convenience: right-align + tabular-nums (numeric columns of figures). */
|
|
80
|
-
numeric?: boolean;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** A row: cell values keyed by column key + optional accounting metadata. */
|
|
84
|
-
export interface AdwDataGridRow {
|
|
85
|
-
/** Accounting emphasis. Default 'normal'. */
|
|
86
|
-
variant?: AdwDataGridRowVariant;
|
|
87
|
-
/** Make just this row clickable (overrides the element-level flag). */
|
|
88
|
-
interactive?: boolean;
|
|
89
|
-
/** Cell values keyed by column key (pre-formatted strings, or numbers). */
|
|
90
|
-
[key: string]: string | number | boolean | AdwDataGridRowVariant | undefined;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const VARIANTS: ReadonlySet<string> = new Set(['normal', 'section', 'subtotal', 'total']);
|
|
66
|
+
import {
|
|
67
|
+
dataGridCellText,
|
|
68
|
+
dataGridColumnClasses,
|
|
69
|
+
dataGridRowInteractive,
|
|
70
|
+
dataGridTrackTemplate,
|
|
71
|
+
dataGridTracks,
|
|
72
|
+
normalizeDataGridColumns,
|
|
73
|
+
normalizeDataGridVariant,
|
|
74
|
+
} from '@gjsify/adwaita-core';
|
|
75
|
+
import type { AdwDataGridColumn, AdwDataGridRow } from '@gjsify/adwaita-core';
|
|
76
|
+
|
|
77
|
+
export type {
|
|
78
|
+
AdwDataGridAlign,
|
|
79
|
+
AdwDataGridCellValue,
|
|
80
|
+
AdwDataGridColumn,
|
|
81
|
+
AdwDataGridRow,
|
|
82
|
+
AdwDataGridRowVariant,
|
|
83
|
+
} from '@gjsify/adwaita-core';
|
|
94
84
|
|
|
95
85
|
export class AdwDataGrid extends HTMLElement {
|
|
96
86
|
private _captionEl!: HTMLDivElement;
|
|
@@ -110,7 +100,7 @@ export class AdwDataGrid extends HTMLElement {
|
|
|
110
100
|
}
|
|
111
101
|
|
|
112
102
|
set columns(value: ReadonlyArray<AdwDataGridColumn>) {
|
|
113
|
-
this._columns =
|
|
103
|
+
this._columns = normalizeDataGridColumns(value);
|
|
114
104
|
if (this._initialized) this._render();
|
|
115
105
|
}
|
|
116
106
|
|
|
@@ -163,7 +153,7 @@ export class AdwDataGrid extends HTMLElement {
|
|
|
163
153
|
this._scrollEl.appendChild(this._tableEl);
|
|
164
154
|
|
|
165
155
|
// Seed from JSON attributes only when the properties were not set.
|
|
166
|
-
if (this._columns.length === 0) this._columns = this._parseJsonAttr('columns',
|
|
156
|
+
if (this._columns.length === 0) this._columns = this._parseJsonAttr('columns', normalizeDataGridColumns);
|
|
167
157
|
if (this._rows.length === 0) this._rows = this._parseJsonAttr('rows', (v) => (Array.isArray(v) ? v : []));
|
|
168
158
|
|
|
169
159
|
this.replaceChildren(this._captionEl, this._scrollEl);
|
|
@@ -173,7 +163,7 @@ export class AdwDataGrid extends HTMLElement {
|
|
|
173
163
|
attributeChangedCallback(name: string, _old: string | null, value: string | null) {
|
|
174
164
|
if (!this._initialized) return;
|
|
175
165
|
if (name === 'columns') {
|
|
176
|
-
this._columns = this._parseJsonAttr('columns',
|
|
166
|
+
this._columns = this._parseJsonAttr('columns', normalizeDataGridColumns);
|
|
177
167
|
this._render();
|
|
178
168
|
} else if (name === 'rows') {
|
|
179
169
|
this._rows = this._parseJsonAttr('rows', (v) => (Array.isArray(v) ? (v as AdwDataGridRow[]) : []));
|
|
@@ -204,28 +194,21 @@ export class AdwDataGrid extends HTMLElement {
|
|
|
204
194
|
this._captionEl.hidden = caption.length === 0;
|
|
205
195
|
}
|
|
206
196
|
|
|
207
|
-
/**
|
|
197
|
+
/**
|
|
198
|
+
* The CSS `grid-template-columns` value derived from the column descriptors.
|
|
199
|
+
*
|
|
200
|
+
* The DERIVATION is core's (`dataGridTracks`), because NativeScript needs the
|
|
201
|
+
* same one for its `ItemSpec`s and can do nothing with a CSS string; the
|
|
202
|
+
* mapping onto CSS text — including the `minmax(0px, 1fr)` slack track whose
|
|
203
|
+
* `0px` must keep its unit to round-trip through the CSSOM — is
|
|
204
|
+
* `dataGridTrackTemplate`.
|
|
205
|
+
*/
|
|
208
206
|
private _trackTemplate(): string {
|
|
209
|
-
|
|
210
|
-
return this._columns
|
|
211
|
-
.map((column, index) => {
|
|
212
|
-
if (column.width !== undefined) return column.width;
|
|
213
|
-
if (column.flex !== undefined) return `${column.flex}fr`;
|
|
214
|
-
// With no explicit sizing anywhere, the first (label) column
|
|
215
|
-
// absorbs slack so trailing numeric columns hug the right edge.
|
|
216
|
-
// `0px` (not `0`) is the CSSOM-canonical form so it round-trips.
|
|
217
|
-
if (!anyExplicit && index === 0) return 'minmax(0px, 1fr)';
|
|
218
|
-
return 'auto';
|
|
219
|
-
})
|
|
220
|
-
.join(' ');
|
|
207
|
+
return dataGridTrackTemplate(dataGridTracks(this._columns));
|
|
221
208
|
}
|
|
222
209
|
|
|
223
210
|
private _columnClasses(column: AdwDataGridColumn): string {
|
|
224
|
-
|
|
225
|
-
const classes = ['adw-data-grid-cell', `align-${align}`];
|
|
226
|
-
if (column.numeric) classes.push('numeric');
|
|
227
|
-
if (column.monospace) classes.push('mono');
|
|
228
|
-
return classes.join(' ');
|
|
211
|
+
return dataGridColumnClasses(column).join(' ');
|
|
229
212
|
}
|
|
230
213
|
|
|
231
214
|
private _render(): void {
|
|
@@ -253,7 +236,7 @@ export class AdwDataGrid extends HTMLElement {
|
|
|
253
236
|
const elementInteractive = this.hasAttribute('interactive');
|
|
254
237
|
|
|
255
238
|
this._rows.forEach((rowData, index) => {
|
|
256
|
-
const variant =
|
|
239
|
+
const variant = normalizeDataGridVariant(rowData.variant);
|
|
257
240
|
const row = document.createElement('div');
|
|
258
241
|
row.className = `adw-data-grid-row variant-${variant}`;
|
|
259
242
|
row.setAttribute('role', 'row');
|
|
@@ -266,25 +249,21 @@ export class AdwDataGrid extends HTMLElement {
|
|
|
266
249
|
cell.className = 'adw-data-grid-cell align-start section-cell';
|
|
267
250
|
cell.setAttribute('role', 'cell');
|
|
268
251
|
cell.style.gridColumn = '1 / -1';
|
|
269
|
-
cell.textContent =
|
|
252
|
+
cell.textContent = dataGridCellText(rowData[firstKey]);
|
|
270
253
|
row.appendChild(cell);
|
|
271
254
|
} else {
|
|
272
255
|
for (const column of this._columns) {
|
|
273
256
|
const cell = document.createElement('div');
|
|
274
257
|
cell.className = this._columnClasses(column);
|
|
275
258
|
cell.setAttribute('role', 'cell');
|
|
276
|
-
cell.textContent =
|
|
259
|
+
cell.textContent = dataGridCellText(rowData[column.key]);
|
|
277
260
|
row.appendChild(cell);
|
|
278
261
|
}
|
|
279
262
|
}
|
|
280
263
|
|
|
281
264
|
// A row is clickable only when it is a plain data row AND either the
|
|
282
265
|
// element opts everything in or the row opts itself in.
|
|
283
|
-
|
|
284
|
-
variant === 'normal' &&
|
|
285
|
-
rowData.interactive !== false &&
|
|
286
|
-
(rowData.interactive === true || elementInteractive);
|
|
287
|
-
if (rowInteractive) {
|
|
266
|
+
if (dataGridRowInteractive(variant, rowData.interactive, elementInteractive)) {
|
|
288
267
|
row.classList.add('interactive');
|
|
289
268
|
row.tabIndex = 0;
|
|
290
269
|
row.addEventListener('click', () => this._activate(index, rowData));
|
|
@@ -305,31 +284,4 @@ export class AdwDataGrid extends HTMLElement {
|
|
|
305
284
|
}
|
|
306
285
|
}
|
|
307
286
|
|
|
308
|
-
/** Coerce a cell value to display text — pre-formatted strings pass through. */
|
|
309
|
-
function cellText(value: string | number | boolean | AdwDataGridRowVariant | undefined): string {
|
|
310
|
-
if (value === undefined || value === null) return '';
|
|
311
|
-
return String(value);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/** Clamp an arbitrary variant to the known set (unknown → 'normal'). */
|
|
315
|
-
function normalizeVariant(variant: unknown): AdwDataGridRowVariant {
|
|
316
|
-
return typeof variant === 'string' && VARIANTS.has(variant) ? (variant as AdwDataGridRowVariant) : 'normal';
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/** Normalise raw column descriptors to a stable list (drops malformed entries). */
|
|
320
|
-
function normalizeColumns(raw: unknown): AdwDataGridColumn[] {
|
|
321
|
-
if (!Array.isArray(raw)) return [];
|
|
322
|
-
return raw
|
|
323
|
-
.filter((c): c is AdwDataGridColumn => typeof c === 'object' && c !== null && 'key' in c)
|
|
324
|
-
.map((c) => ({
|
|
325
|
-
key: String(c.key),
|
|
326
|
-
label: c.label !== undefined ? String(c.label) : undefined,
|
|
327
|
-
align: c.align,
|
|
328
|
-
width: c.width !== undefined ? String(c.width) : undefined,
|
|
329
|
-
flex: typeof c.flex === 'number' ? c.flex : undefined,
|
|
330
|
-
monospace: c.monospace === true,
|
|
331
|
-
numeric: c.numeric === true,
|
|
332
|
-
}));
|
|
333
|
-
}
|
|
334
|
-
|
|
335
287
|
customElements.define('adw-data-grid', AdwDataGrid);
|
|
@@ -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);
|