@gjsify/adwaita-web 0.41.0 → 0.42.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/README.md +98 -3
- package/dist/adwaita-web.css +30 -23
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/adw-fonts.spec.d.ts +2 -0
- package/lib/types/breakpoints.d.ts +10 -1
- package/lib/types/connect-lifecycle.spec.d.ts +1 -0
- package/lib/types/elements/adw-about-dialog.d.ts +1 -0
- package/lib/types/elements/adw-action-row.d.ts +2 -0
- package/lib/types/elements/adw-alert-dialog.d.ts +14 -2
- package/lib/types/elements/adw-dialog.d.ts +2 -7
- package/lib/types/elements/adw-icon.d.ts +17 -1
- package/lib/types/elements/adw-navigation-split-view.d.ts +7 -0
- package/lib/types/elements/adw-overlay-split-view.d.ts +6 -0
- package/lib/types/elements/adw-popover.d.ts +1 -0
- package/lib/types/elements/adw-preferences-dialog.d.ts +1 -0
- package/lib/types/elements/adw-switch.d.ts +10 -0
- package/lib/types/elements/adw-toggle-group.d.ts +2 -0
- package/lib/types/elements/modal-surface.d.ts +59 -0
- package/lib/types/elements/roving-focus.d.ts +26 -0
- package/lib/types/elements/row-activation.d.ts +26 -0
- package/lib/types/empty-sections.d.ts +15 -0
- package/lib/types/empty-sections.spec.d.ts +1 -0
- package/lib/types/fonts.d.ts +1 -0
- package/lib/types/icon-registry.d.ts +35 -0
- package/lib/types/icon-registry.spec.d.ts +18 -0
- package/lib/types/index.d.ts +1 -1
- package/lib/types/keyboard-operable.spec.d.ts +1 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/package.json +10 -6
- package/scss/_banner.scss +1 -1
- package/scss/_checks.scss +8 -8
- package/scss/_combo_row.scss +1 -1
- package/scss/_drop_down.scss +1 -1
- package/scss/_expander_row.scss +1 -1
- package/scss/_icon.scss +47 -0
- package/scss/_icons.generated.scss +18 -6
- package/scss/_password_entry_row.scss +1 -1
- package/scss/_preferences_dialog.scss +3 -2
- package/scss/_progress_bar.scss +1 -1
- package/scss/_row.scss +1 -1
- package/scss/_status_page.scss +1 -1
- package/scss/_tab_view.scss +1 -1
- package/scss/_toast.scss +6 -24
- package/scss/_variables.scss +23 -3
- package/src/adw-action-rows.spec.ts +21 -9
- package/src/adw-button-content.spec.ts +11 -6
- package/src/adw-fonts.spec.ts +188 -0
- package/src/adw-icon.spec.ts +17 -8
- package/src/adw-row-state.spec.ts +11 -1
- package/src/breakpoints.ts +18 -4
- package/src/connect-lifecycle.spec.ts +351 -0
- package/src/elements/adw-about-dialog.ts +24 -18
- package/src/elements/adw-action-row.ts +14 -2
- package/src/elements/adw-alert-dialog.ts +39 -20
- package/src/elements/adw-button-row.ts +3 -0
- package/src/elements/adw-clamp.ts +3 -2
- package/src/elements/adw-data-grid.ts +5 -2
- package/src/elements/adw-dialog.ts +16 -63
- package/src/elements/adw-expander-row.ts +18 -4
- package/src/elements/adw-header-bar.ts +2 -0
- package/src/elements/adw-icon.ts +32 -6
- package/src/elements/adw-inline-view-switcher.ts +12 -0
- package/src/elements/adw-menu-button.ts +17 -10
- package/src/elements/adw-navigation-split-view.ts +30 -7
- package/src/elements/adw-overlay-split-view.ts +15 -3
- package/src/elements/adw-popover.ts +11 -0
- package/src/elements/adw-preferences-dialog.ts +18 -16
- package/src/elements/adw-preferences-group.ts +7 -0
- package/src/elements/adw-sidebar.ts +15 -0
- package/src/elements/adw-status-page.ts +12 -4
- package/src/elements/adw-switch-row.ts +7 -0
- package/src/elements/adw-switch.ts +19 -1
- package/src/elements/adw-tab-view.ts +7 -4
- package/src/elements/adw-toast-overlay.ts +8 -1
- package/src/elements/adw-toggle-group.ts +94 -2
- package/src/elements/adw-toolbar-view.ts +6 -2
- package/src/elements/adw-view-switcher-bar.ts +18 -0
- package/src/elements/adw-view-switcher.ts +14 -0
- package/src/elements/modal-surface.ts +180 -0
- package/src/elements/roving-focus.ts +154 -0
- package/src/elements/row-activation.ts +92 -0
- package/src/empty-sections.spec.ts +81 -0
- package/src/empty-sections.ts +54 -0
- package/src/fonts.ts +25 -0
- package/src/icon-registry.spec.ts +240 -0
- package/src/icon-registry.ts +157 -0
- package/src/index.ts +24 -1
- package/src/keyboard-operable.spec.ts +755 -0
- package/src/source-view/theme.ts +15 -3
- package/src/split-button.spec.ts +6 -2
- package/src/style-classes.spec.ts +6 -0
- package/src/styles.generated.ts +1 -1
- package/src/test.browser.mts +14 -0
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
} from '@gjsify/adwaita-core';
|
|
69
69
|
|
|
70
70
|
import { createAdwIcon } from './adw-icon.js';
|
|
71
|
+
import { AdwModalSurface } from './modal-surface.js';
|
|
71
72
|
|
|
72
73
|
/** A template label with its mnemonic marker removed — this renderer has no accelerator layer. */
|
|
73
74
|
function rowLabel(key: keyof typeof ADW_ABOUT_DIALOG_LABELS): string {
|
|
@@ -80,6 +81,7 @@ export class AdwAboutDialog extends HTMLElement {
|
|
|
80
81
|
// Layout nodes (rebuilt from attributes on each render).
|
|
81
82
|
private _scrimEl!: HTMLDivElement;
|
|
82
83
|
private _sheetEl!: HTMLDivElement;
|
|
84
|
+
private _modal!: AdwModalSurface;
|
|
83
85
|
private _navView!: HTMLElement;
|
|
84
86
|
private _scrollEl!: HTMLDivElement;
|
|
85
87
|
private _mainToolbarEl!: HTMLDivElement;
|
|
@@ -260,8 +262,6 @@ export class AdwAboutDialog extends HTMLElement {
|
|
|
260
262
|
|
|
261
263
|
this._sheetEl = document.createElement('div');
|
|
262
264
|
this._sheetEl.className = 'adw-about-dialog-sheet';
|
|
263
|
-
this._sheetEl.setAttribute('role', 'dialog');
|
|
264
|
-
this._sheetEl.setAttribute('aria-modal', 'true');
|
|
265
265
|
// AdwDialog:title, whose template default is the bare word. The application
|
|
266
266
|
// name is announced by the title-1 label inside, exactly as the header
|
|
267
267
|
// revealer shows it separately in GTK.
|
|
@@ -269,12 +269,15 @@ export class AdwAboutDialog extends HTMLElement {
|
|
|
269
269
|
|
|
270
270
|
this.replaceChildren(this._scrimEl, this._sheetEl);
|
|
271
271
|
|
|
272
|
-
// Escape
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
272
|
+
// The role, `aria-modal`, Escape, the Tab trap and the return-focus. No
|
|
273
|
+
// `initialFocus`: the sheet itself is the fallback, which is what this element
|
|
274
|
+
// used to focus by hand, and the navigation view's first control is better.
|
|
275
|
+
this._modal = new AdwModalSurface({
|
|
276
|
+
host: this,
|
|
277
|
+
surface: this._sheetEl,
|
|
278
|
+
role: 'dialog',
|
|
279
|
+
isOpen: () => this.open,
|
|
280
|
+
onEscape: () => this.close(),
|
|
278
281
|
});
|
|
279
282
|
|
|
280
283
|
this._render();
|
|
@@ -301,8 +304,9 @@ export class AdwAboutDialog extends HTMLElement {
|
|
|
301
304
|
nav.replace?.(['main']);
|
|
302
305
|
this._scrollEl.scrollTop = 0;
|
|
303
306
|
this._updateHeaderTitle();
|
|
304
|
-
|
|
305
|
-
|
|
307
|
+
this._modal.present();
|
|
308
|
+
} else {
|
|
309
|
+
this._modal.dismiss();
|
|
306
310
|
}
|
|
307
311
|
}
|
|
308
312
|
|
|
@@ -352,7 +356,6 @@ export class AdwAboutDialog extends HTMLElement {
|
|
|
352
356
|
if (visibility.legalRow) this._navView.append(this._buildLegalPage());
|
|
353
357
|
|
|
354
358
|
this._sheetEl.replaceChildren(this._navView);
|
|
355
|
-
this._sheetEl.tabIndex = -1;
|
|
356
359
|
|
|
357
360
|
this.classList.toggle('open', this.open);
|
|
358
361
|
}
|
|
@@ -382,10 +385,11 @@ export class AdwAboutDialog extends HTMLElement {
|
|
|
382
385
|
header.setAttribute('title', headerTitle);
|
|
383
386
|
|
|
384
387
|
if (showClose) {
|
|
385
|
-
// The close button — drawn with a CSS "×" glyph, the same approach
|
|
386
|
-
// adw-tab-view's close affordance: `window-close`
|
|
387
|
-
//
|
|
388
|
-
//
|
|
388
|
+
// The close button — drawn with a CSS "×" glyph, the same approach and the
|
|
389
|
+
// same reason as adw-tab-view's close affordance: `window-close` IS compiled
|
|
390
|
+
// and does have a mask class; what differs from upstream's
|
|
391
|
+
// `window-close-symbolic` (adw-sheet-controls.c:118) is the button size this
|
|
392
|
+
// header budgets for it.
|
|
389
393
|
const close = document.createElement('button');
|
|
390
394
|
close.type = 'button';
|
|
391
395
|
close.className = 'adw-button flat circular adw-about-dialog-close';
|
|
@@ -430,9 +434,11 @@ export class AdwAboutDialog extends HTMLElement {
|
|
|
430
434
|
|
|
431
435
|
// Only when one was given: GTK hides the image outright for an empty icon
|
|
432
436
|
// name, and a generic glyph would show an icon for an application that asked
|
|
433
|
-
// for none. A reverse-DNS application id (`org.gnome.Builder`)
|
|
434
|
-
//
|
|
435
|
-
//
|
|
437
|
+
// for none. A reverse-DNS application id (`org.gnome.Builder`) — what
|
|
438
|
+
// `adw_about_dialog_new_from_appdata` hands to `set_application_icon` in
|
|
439
|
+
// refs/libadwaita/src/adw-about-dialog.c — is not one CSS token, so it draws
|
|
440
|
+
// `image-missing` rather than three stray classes; that is also what GTK draws for
|
|
441
|
+
// it on a machine without the app installed.
|
|
436
442
|
if (visibility.appIcon) {
|
|
437
443
|
body.appendChild(createAdwIcon(this.applicationIcon, 'adw-about-dialog-icon'));
|
|
438
444
|
}
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
|
|
17
17
|
import { ActionRowState } from '@gjsify/adwaita-core';
|
|
18
18
|
|
|
19
|
+
import { bindEmptySections } from '../empty-sections.js';
|
|
20
|
+
import { type AdwRowActivation, attachRowActivation } from './row-activation.js';
|
|
21
|
+
|
|
19
22
|
/**
|
|
20
23
|
* Attributes whose change means the activatable widget's sensitivity may have
|
|
21
24
|
* changed. `disabled` is the DOM's `GtkWidget:sensitive` for form controls;
|
|
@@ -39,6 +42,8 @@ export class AdwActionRow extends HTMLElement {
|
|
|
39
42
|
/** Watches the activatable widget's `disabled`/`aria-disabled` — the live binding. */
|
|
40
43
|
private _sensitivityObserver: MutationObserver | null = null;
|
|
41
44
|
private _initialized = false;
|
|
45
|
+
/** Tab stop + Enter/Space, re-derived whenever `activatable` moves. */
|
|
46
|
+
private _activation?: AdwRowActivation;
|
|
42
47
|
|
|
43
48
|
static get observedAttributes() {
|
|
44
49
|
return ['title', 'subtitle', 'activatable'];
|
|
@@ -105,12 +110,20 @@ export class AdwActionRow extends HTMLElement {
|
|
|
105
110
|
for (const child of suffixChildren) this._suffixEl.appendChild(child);
|
|
106
111
|
|
|
107
112
|
this.replaceChildren(this._prefixEl, textEl, this._suffixEl);
|
|
113
|
+
// `_render` runs from `attributeChangedCallback`, which a control appended into
|
|
114
|
+
// `suffixSection` never triggers — the section stayed `hidden` and the control
|
|
115
|
+
// measured 0x0. The observer is what keeps the two sections in step with what
|
|
116
|
+
// they actually hold.
|
|
117
|
+
bindEmptySections(this._prefixEl, this._suffixEl);
|
|
108
118
|
// `attributeChangedCallback` was guarded until now, so a parsed row would
|
|
109
119
|
// otherwise start with a stale state.
|
|
110
120
|
this._adoptAttributes();
|
|
111
121
|
this._render();
|
|
112
122
|
|
|
113
123
|
this.addEventListener('click', (event) => this._onClick(event));
|
|
124
|
+
// Only while `activatable` — a plain action row is a label, and a label that
|
|
125
|
+
// takes Tab makes every static row in a group a stop.
|
|
126
|
+
this._activation = attachRowActivation({ row: this, activatable: () => this._state.activatable });
|
|
114
127
|
}
|
|
115
128
|
|
|
116
129
|
/** Copy the declarative attributes into the headless state. */
|
|
@@ -178,9 +191,8 @@ export class AdwActionRow extends HTMLElement {
|
|
|
178
191
|
this._titleEl.hidden = !titleVisible;
|
|
179
192
|
this._subtitleEl.textContent = subtitle;
|
|
180
193
|
this._subtitleEl.hidden = !subtitleVisible;
|
|
181
|
-
this._prefixEl.hidden = this._prefixEl.childElementCount === 0;
|
|
182
|
-
this._suffixEl.hidden = this._suffixEl.childElementCount === 0;
|
|
183
194
|
this.classList.toggle('activatable', this._state.activatable);
|
|
195
|
+
this._activation?.sync();
|
|
184
196
|
}
|
|
185
197
|
}
|
|
186
198
|
|
|
@@ -44,6 +44,9 @@
|
|
|
44
44
|
import { AdwAlertResponses } from '@gjsify/adwaita-core';
|
|
45
45
|
import type { AdwResponseAppearance } from '@gjsify/adwaita-core';
|
|
46
46
|
|
|
47
|
+
import { bindEmptySections } from '../empty-sections.js';
|
|
48
|
+
import { AdwModalSurface } from './modal-surface.js';
|
|
49
|
+
|
|
47
50
|
/** Response button appearance — mirrors Adw.ResponseAppearance. */
|
|
48
51
|
export type { AdwResponseAppearance } from '@gjsify/adwaita-core';
|
|
49
52
|
|
|
@@ -67,6 +70,7 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
67
70
|
private _bodyEl!: HTMLParagraphElement;
|
|
68
71
|
private _childEl!: HTMLDivElement;
|
|
69
72
|
private _responseAreaEl!: HTMLDivElement;
|
|
73
|
+
private _modal!: AdwModalSurface;
|
|
70
74
|
|
|
71
75
|
/** The headless response registry + default/close semantics + resolution (ADR 0004). */
|
|
72
76
|
private _model = new AdwAlertResponses();
|
|
@@ -149,10 +153,17 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
149
153
|
|
|
150
154
|
this._dialogEl = document.createElement('div');
|
|
151
155
|
this._dialogEl.className = 'adw-alert-dialog-box';
|
|
152
|
-
this._dialogEl.setAttribute('role', 'alertdialog');
|
|
153
|
-
this._dialogEl.setAttribute('aria-modal', 'true');
|
|
154
156
|
// Clicks inside the box must not bubble to the scrim's dismiss handler.
|
|
155
157
|
this._dialogEl.addEventListener('click', (event) => event.stopPropagation());
|
|
158
|
+
// The role, `aria-modal`, Escape, the Tab trap and the return-focus.
|
|
159
|
+
this._modal = new AdwModalSurface({
|
|
160
|
+
host: this,
|
|
161
|
+
surface: this._dialogEl,
|
|
162
|
+
role: 'alertdialog',
|
|
163
|
+
isOpen: () => this.open,
|
|
164
|
+
onEscape: () => this._dismiss(),
|
|
165
|
+
initialFocus: (focusables) => this._initialFocusTarget(focusables),
|
|
166
|
+
});
|
|
156
167
|
|
|
157
168
|
const messageArea = document.createElement('div');
|
|
158
169
|
messageArea.className = 'adw-alert-dialog-message';
|
|
@@ -168,6 +179,9 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
168
179
|
for (const child of extraChildren) this._childEl.appendChild(child);
|
|
169
180
|
|
|
170
181
|
messageArea.append(this._headingEl, this._bodyEl, this._childEl);
|
|
182
|
+
// `_render` runs from `attributeChangedCallback` and from the response API, and
|
|
183
|
+
// an extra child appended after connect triggers neither.
|
|
184
|
+
bindEmptySections(this._childEl);
|
|
171
185
|
|
|
172
186
|
this._responseAreaEl = document.createElement('div');
|
|
173
187
|
this._responseAreaEl.className = 'adw-alert-dialog-responses';
|
|
@@ -175,15 +189,6 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
175
189
|
this._dialogEl.append(messageArea, this._responseAreaEl);
|
|
176
190
|
this.replaceChildren(this._scrimEl, this._dialogEl);
|
|
177
191
|
|
|
178
|
-
// Escape dismisses the dialog when open (the AdwDialog Escape shortcut →
|
|
179
|
-
// close-response).
|
|
180
|
-
this.addEventListener('keydown', (event) => {
|
|
181
|
-
if (event.key === 'Escape' && this.open) {
|
|
182
|
-
event.stopPropagation();
|
|
183
|
-
this._dismiss();
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
|
|
187
192
|
for (const el of declared) {
|
|
188
193
|
const id = el.getAttribute('id');
|
|
189
194
|
if (!id) continue;
|
|
@@ -217,7 +222,8 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
217
222
|
this.dispatchEvent(new CustomEvent('notify::body', { bubbles: true, detail: { body: this.body } }));
|
|
218
223
|
} else if (name === 'open') {
|
|
219
224
|
this.dispatchEvent(new CustomEvent('notify::open', { bubbles: true, detail: { open: this.open } }));
|
|
220
|
-
if (this.open) this.
|
|
225
|
+
if (this.open) this._modal.present();
|
|
226
|
+
else this._modal.dismiss();
|
|
221
227
|
}
|
|
222
228
|
}
|
|
223
229
|
|
|
@@ -351,14 +357,29 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
351
357
|
this.dispatchEvent(new CustomEvent('response', { bubbles: true, detail: { response } }));
|
|
352
358
|
}
|
|
353
359
|
|
|
354
|
-
/**
|
|
355
|
-
|
|
360
|
+
/**
|
|
361
|
+
* Where a presented alert puts focus, in the three steps `adw_alert_dialog_grab_focus`
|
|
362
|
+
* takes (refs/libadwaita/src/adw-alert-dialog.c:382): the CONTENT first
|
|
363
|
+
* (`adw_widget_grab_focus_child (priv->scrolled_window)`, :397), then the default
|
|
364
|
+
* widget (:406), then the first response whose `enabled` is set (:413). The default
|
|
365
|
+
* response is rarely the first button — a destructive alert opens on Cancel — and
|
|
366
|
+
* content beats it: an alert carrying an entry is answered by typing into it.
|
|
367
|
+
*
|
|
368
|
+
* `focusables` is the surface's own list, so what "focusable" means is decided once.
|
|
369
|
+
*/
|
|
370
|
+
private _initialFocusTarget(focusables: readonly HTMLElement[]): HTMLElement | undefined {
|
|
371
|
+
return focusables.find((el) => this._childEl.contains(el)) ?? this._defaultResponseButton();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** The default response button, or the first enabled one. */
|
|
375
|
+
private _defaultResponseButton(): HTMLElement | undefined {
|
|
356
376
|
const defaultId = this._model.defaultResponse;
|
|
357
377
|
const firstEnabledId = this._model.responses.find((response) => response.enabled)?.id;
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
378
|
+
// The enabled check covers the DEFAULT too: `focus()` on a disabled button is a
|
|
379
|
+
// no-op, so handing one back left the dialog open with focus outside itself.
|
|
380
|
+
const defaultButton =
|
|
381
|
+
defaultId !== null && this._model.getResponseEnabled(defaultId) ? this._buttons.get(defaultId) : undefined;
|
|
382
|
+
return defaultButton ?? (firstEnabledId ? this._buttons.get(firstEnabledId) : undefined);
|
|
362
383
|
}
|
|
363
384
|
|
|
364
385
|
private _render(): void {
|
|
@@ -372,8 +393,6 @@ export class AdwAlertDialog extends HTMLElement {
|
|
|
372
393
|
this._bodyEl.textContent = body;
|
|
373
394
|
this._bodyEl.hidden = body.length === 0;
|
|
374
395
|
|
|
375
|
-
this._childEl.hidden = this._childEl.childElementCount === 0;
|
|
376
|
-
|
|
377
396
|
// Horizontal only when prefer-wide-layout is set AND there are at most two
|
|
378
397
|
// responses — the AdwAlertDialog heuristic for keeping wide buttons readable.
|
|
379
398
|
const horizontal = this.preferWideLayout && this._model.responses.length <= 2;
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { BUTTON_ROW_ACTIVATABLE, ButtonRowState } from '@gjsify/adwaita-core';
|
|
23
23
|
|
|
24
24
|
import { type AdwIcon, createAdwIcon } from './adw-icon.js';
|
|
25
|
+
import { attachRowActivation } from './row-activation.js';
|
|
25
26
|
|
|
26
27
|
export class AdwButtonRow extends HTMLElement {
|
|
27
28
|
private _contentsEl!: HTMLDivElement;
|
|
@@ -58,6 +59,8 @@ export class AdwButtonRow extends HTMLElement {
|
|
|
58
59
|
this.addEventListener('click', () => {
|
|
59
60
|
this.dispatchEvent(new CustomEvent('activated', { bubbles: true }));
|
|
60
61
|
});
|
|
62
|
+
// `BUTTON_ROW_ACTIVATABLE`, not an attribute read: there is no opt-out to read.
|
|
63
|
+
attachRowActivation({ row: this, activatable: () => BUTTON_ROW_ACTIVATABLE });
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
attributeChangedCallback() {
|
|
@@ -40,8 +40,9 @@ export class AdwClamp extends HTMLElement {
|
|
|
40
40
|
this._resize = new ResizeObserver(() => this._allocate());
|
|
41
41
|
this._resize.observe(this);
|
|
42
42
|
// Children appended AFTER connect are clamped too. Without this they
|
|
43
|
-
// would keep whatever width they came with — the
|
|
44
|
-
// connect time" bug
|
|
43
|
+
// would keep whatever width they came with — the "evaluated once at
|
|
44
|
+
// connect time" bug that cost six container getters their layout;
|
|
45
|
+
// `src/empty-sections.ts` is the same technique for the `hidden` half.
|
|
45
46
|
this._mutations = new MutationObserver(() => this._allocate());
|
|
46
47
|
this._mutations.observe(this, { childList: true });
|
|
47
48
|
this._allocate();
|
|
@@ -52,8 +52,11 @@
|
|
|
52
52
|
// column tracks, the cell classes, the two normalisers, the cell text and the activation
|
|
53
53
|
// rule. `@gjsify/adwaita-nativescript` renders the same widget over `GridLayout`
|
|
54
54
|
// `ItemSpec`s — NativeScript has no subgrid — so the TRACK rule is a renderer-neutral
|
|
55
|
-
// descriptor and only the mapping onto `grid-template-columns` stays here.
|
|
56
|
-
//
|
|
55
|
+
// descriptor and only the mapping onto `grid-template-columns` stays here. The NativeScript
|
|
56
|
+
// suite is held to DATA_GRID_TRACK_VECTORS, DATA_GRID_COLUMN_CLASS_VECTORS,
|
|
57
|
+
// DATA_GRID_VARIANT_VECTORS, DATA_GRID_CELL_TEXT_VECTORS and DATA_GRID_INTERACTIVE_VECTORS.
|
|
58
|
+
// This element is held to none of them — the browser half of the shared derivation is
|
|
59
|
+
// asserted only against itself, which is the gap, not the arrangement.
|
|
57
60
|
// Reference: Gtk.Grid usage in the Buchhaltung BWA view (native financial grid).
|
|
58
61
|
// Reference: refs/libadwaita/src/stylesheet/_colors.scss (separator / card tokens).
|
|
59
62
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
@@ -24,21 +24,14 @@
|
|
|
24
24
|
// Copyright (c) 2023-2024 GNOME Foundation Inc. (libadwaita). LGPLv2.1+.
|
|
25
25
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
26
26
|
|
|
27
|
+
import { AdwModalSurface } from './modal-surface.js';
|
|
28
|
+
|
|
27
29
|
/** Adaptive presentation of the dialog — mirrors Adw.DialogPresentationMode. */
|
|
28
30
|
export type AdwDialogPresentationMode = 'auto' | 'floating' | 'bottom-sheet';
|
|
29
31
|
|
|
30
32
|
const DEFAULT_CONTENT_WIDTH = 360;
|
|
31
33
|
const PRESENTATION_MODES: readonly AdwDialogPresentationMode[] = ['auto', 'floating', 'bottom-sheet'];
|
|
32
34
|
|
|
33
|
-
const FOCUSABLE_SELECTOR = [
|
|
34
|
-
'a[href]',
|
|
35
|
-
'button:not([disabled])',
|
|
36
|
-
'input:not([disabled])',
|
|
37
|
-
'select:not([disabled])',
|
|
38
|
-
'textarea:not([disabled])',
|
|
39
|
-
'[tabindex]:not([tabindex="-1"])',
|
|
40
|
-
].join(',');
|
|
41
|
-
|
|
42
35
|
export class AdwDialog extends HTMLElement {
|
|
43
36
|
private _initialized = false;
|
|
44
37
|
private _scrimEl!: HTMLDivElement;
|
|
@@ -47,8 +40,7 @@ export class AdwDialog extends HTMLElement {
|
|
|
47
40
|
private _titleEl!: HTMLSpanElement;
|
|
48
41
|
private _closeBtn!: HTMLButtonElement;
|
|
49
42
|
private _contentEl!: HTMLDivElement;
|
|
50
|
-
|
|
51
|
-
private _previouslyFocused: HTMLElement | null = null;
|
|
43
|
+
private _modal!: AdwModalSurface;
|
|
52
44
|
|
|
53
45
|
static get observedAttributes() {
|
|
54
46
|
return ['title', 'open', 'can-close', 'content-width', 'content-height', 'presentation-mode', 'show-header'];
|
|
@@ -157,15 +149,19 @@ export class AdwDialog extends HTMLElement {
|
|
|
157
149
|
this._scrimEl.className = 'adw-dialog-scrim';
|
|
158
150
|
this._scrimEl.addEventListener('click', () => this._attemptClose());
|
|
159
151
|
|
|
160
|
-
//
|
|
161
|
-
//
|
|
152
|
+
// Clicks inside must not reach the scrim's dismiss handler. The role, `aria-modal`
|
|
153
|
+
// and the -1 tabindex that lets the box hold focus when its content has none all
|
|
154
|
+
// come from the modal surface below.
|
|
162
155
|
this._boxEl = document.createElement('div');
|
|
163
156
|
this._boxEl.className = 'adw-dialog-box';
|
|
164
|
-
this._boxEl.setAttribute('role', 'dialog');
|
|
165
|
-
this._boxEl.setAttribute('aria-modal', 'true');
|
|
166
|
-
this._boxEl.tabIndex = -1;
|
|
167
157
|
this._boxEl.addEventListener('click', (event) => event.stopPropagation());
|
|
168
|
-
this.
|
|
158
|
+
this._modal = new AdwModalSurface({
|
|
159
|
+
host: this,
|
|
160
|
+
surface: this._boxEl,
|
|
161
|
+
role: 'dialog',
|
|
162
|
+
isOpen: () => this.open,
|
|
163
|
+
onEscape: () => this._attemptClose(),
|
|
164
|
+
});
|
|
169
165
|
|
|
170
166
|
this._headerEl = document.createElement('div');
|
|
171
167
|
this._headerEl.className = 'adw-dialog-header';
|
|
@@ -206,7 +202,7 @@ export class AdwDialog extends HTMLElement {
|
|
|
206
202
|
this._render();
|
|
207
203
|
if (name === 'open') {
|
|
208
204
|
this.dispatchEvent(new CustomEvent('notify::open', { bubbles: true, detail: { open: this.open } }));
|
|
209
|
-
if (this.open) this.
|
|
205
|
+
if (this.open) this._modal.present();
|
|
210
206
|
else this._onClosed();
|
|
211
207
|
}
|
|
212
208
|
}
|
|
@@ -222,53 +218,10 @@ export class AdwDialog extends HTMLElement {
|
|
|
222
218
|
this.open = false;
|
|
223
219
|
}
|
|
224
220
|
|
|
225
|
-
/**
|
|
226
|
-
private _onBoxKeyDown(event: KeyboardEvent): void {
|
|
227
|
-
if (event.key === 'Escape' && this.open) {
|
|
228
|
-
event.stopPropagation();
|
|
229
|
-
this._attemptClose();
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
if (event.key !== 'Tab') return;
|
|
233
|
-
const focusables = this._focusables();
|
|
234
|
-
if (focusables.length === 0) {
|
|
235
|
-
// Nothing tabbable inside — keep focus on the box.
|
|
236
|
-
event.preventDefault();
|
|
237
|
-
this._boxEl.focus();
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
const first = focusables[0];
|
|
241
|
-
const last = focusables[focusables.length - 1];
|
|
242
|
-
const active = document.activeElement;
|
|
243
|
-
if (event.shiftKey && (active === first || active === this._boxEl)) {
|
|
244
|
-
event.preventDefault();
|
|
245
|
-
last.focus();
|
|
246
|
-
} else if (!event.shiftKey && active === last) {
|
|
247
|
-
event.preventDefault();
|
|
248
|
-
first.focus();
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
private _focusables(): HTMLElement[] {
|
|
253
|
-
return Array.from(this._boxEl.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)).filter(
|
|
254
|
-
(el) => !el.hasAttribute('disabled') && !el.hidden,
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/** On present: remember focus, then move it into the dialog. */
|
|
259
|
-
private _onPresented(): void {
|
|
260
|
-
const active = document.activeElement;
|
|
261
|
-
this._previouslyFocused = active instanceof HTMLElement && active !== this ? active : null;
|
|
262
|
-
const target = this._focusables()[0] ?? this._boxEl;
|
|
263
|
-
target.focus();
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/** On close: emit `closed` and return focus to where it was before present. */
|
|
221
|
+
/** On close: emit `closed`, then let the surface return focus to where it was. */
|
|
267
222
|
private _onClosed(): void {
|
|
268
223
|
this.dispatchEvent(new CustomEvent('closed', { bubbles: true }));
|
|
269
|
-
|
|
270
|
-
this._previouslyFocused = null;
|
|
271
|
-
if (restore && restore.isConnected) restore.focus();
|
|
224
|
+
this._modal.dismiss();
|
|
272
225
|
}
|
|
273
226
|
|
|
274
227
|
private _render(): void {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// the `expanded` attribute reflection and the event.
|
|
14
14
|
//
|
|
15
15
|
// Reference: refs/adwaita-web/adwaita-web/scss/_expander_row.scss
|
|
16
|
-
// Reference: refs/libadwaita/src/stylesheet/widgets/
|
|
16
|
+
// Reference: refs/libadwaita/src/stylesheet/widgets/_lists.scss (AdwExpanderRow)
|
|
17
17
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
18
18
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the
|
|
19
19
|
// disclosure state machine composed from @gjsify/adwaita-core, the enable
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
|
|
22
22
|
import { ExpanderState, deriveRowLabels } from '@gjsify/adwaita-core';
|
|
23
23
|
|
|
24
|
+
import { bindEmptySections } from '../empty-sections.js';
|
|
25
|
+
|
|
24
26
|
// SIDE-EFFECT import, deliberately separate from the type import below: it guarantees
|
|
25
27
|
// `adw-switch` is defined before this module's `customElements.define` can upgrade a
|
|
26
28
|
// server-rendered `<adw-expander-row>` and build one. A combined
|
|
@@ -30,6 +32,7 @@ import { ExpanderState, deriveRowLabels } from '@gjsify/adwaita-core';
|
|
|
30
32
|
import './adw-switch.js';
|
|
31
33
|
import type { AdwSwitch } from './adw-switch.js';
|
|
32
34
|
import { type AdwIcon, createAdwIcon } from './adw-icon.js';
|
|
35
|
+
import { attachRowActivation } from './row-activation.js';
|
|
33
36
|
|
|
34
37
|
export class AdwExpanderRow extends HTMLElement {
|
|
35
38
|
private _headerEl!: HTMLDivElement;
|
|
@@ -133,6 +136,12 @@ export class AdwExpanderRow extends HTMLElement {
|
|
|
133
136
|
this._suffixEl.className = 'adw-expander-row-suffix';
|
|
134
137
|
for (const child of suffixChildren) this._suffixEl.appendChild(child);
|
|
135
138
|
|
|
139
|
+
// Same reason as the action row's: `_render` is attribute-driven, and a widget
|
|
140
|
+
// appended into `prefixSection` after connect is a childList change nothing else
|
|
141
|
+
// hears. `contentSection` is NOT in here — an empty disclosure is still the
|
|
142
|
+
// disclosure, and `.expanded` alone decides whether it shows.
|
|
143
|
+
bindEmptySections(this._prefixEl, this._suffixEl);
|
|
144
|
+
|
|
136
145
|
this._headerEl.append(this._prefixEl, textEl, this._suffixEl, this._enableSwitch, this._chevronEl);
|
|
137
146
|
|
|
138
147
|
this._contentEl = document.createElement('div');
|
|
@@ -154,6 +163,11 @@ export class AdwExpanderRow extends HTMLElement {
|
|
|
154
163
|
}
|
|
155
164
|
});
|
|
156
165
|
|
|
166
|
+
// adw-expander-row.c:123 — `grab_focus` forwards to the header row, so the header
|
|
167
|
+
// is both the focus target and the disclosure control. Enter and Space go through
|
|
168
|
+
// its click handler, which is what a pointer already uses.
|
|
169
|
+
attachRowActivation({ row: this._headerEl, activatable: () => true });
|
|
170
|
+
|
|
157
171
|
this._enableSwitch.addEventListener('notify::active', (event) => {
|
|
158
172
|
// The switch's own notify stops at the row: the row publishes
|
|
159
173
|
// `notify::enable-expansion`, and letting `notify::active` past would give
|
|
@@ -201,10 +215,10 @@ export class AdwExpanderRow extends HTMLElement {
|
|
|
201
215
|
this._enableSwitch.active = this.enableExpansion;
|
|
202
216
|
this._reflectingEnable = false;
|
|
203
217
|
|
|
204
|
-
this._prefixEl.hidden = this._prefixEl.childElementCount === 0;
|
|
205
|
-
this._suffixEl.hidden = this._suffixEl.childElementCount === 0;
|
|
206
|
-
|
|
207
218
|
this.classList.toggle('expanded', this.expanded);
|
|
219
|
+
// adw-expander-row.c:657 keeps GTK_ACCESSIBLE_STATE_EXPANDED on the header row in
|
|
220
|
+
// step with the flag; `aria-expanded` is the same statement to the same reader.
|
|
221
|
+
this._headerEl.setAttribute('aria-expanded', String(this.expanded));
|
|
208
222
|
this._contentEl.classList.toggle('expanded', this.expanded);
|
|
209
223
|
}
|
|
210
224
|
}
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// subtitle at all. So a header bar could not show what its own NativeScript twin
|
|
10
10
|
// could, and the fix is delegation rather than a fourth copy of the derivation.
|
|
11
11
|
//
|
|
12
|
+
// CORE-VIA: ./adw-window-title.js — the derived centre IS that element, so the three rules run in its WindowTitleState.
|
|
13
|
+
//
|
|
12
14
|
// Reference: refs/libadwaita/src/adw-header-bar.c
|
|
13
15
|
// Adapted from Adwaita Web UI Framework (https://github.com/mclellac/adwaita-web).
|
|
14
16
|
// Copyright (c) 2025 csm. MIT License.
|
package/src/elements/adw-icon.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
// Attributes:
|
|
8
8
|
// icon-name — symbolic name, with or without the `-symbolic` suffix. A name that is
|
|
9
9
|
// not a single CSS token (a space, a quote, a reverse-DNS application
|
|
10
|
-
// id)
|
|
10
|
+
// id) never reaches a class — it draws {@link MISSING_ICON_NAME}, which
|
|
11
|
+
// is what GTK draws for it, rather than injecting markup.
|
|
11
12
|
// size — rendered edge length in px. Absent leaves the stylesheet's 16px in
|
|
12
13
|
// charge; a context that sizes its own icons through CSS
|
|
13
14
|
// (`.adw-about-dialog-icon`, the split button's 14px arrows) keeps doing
|
|
@@ -29,10 +30,21 @@
|
|
|
29
30
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the
|
|
30
31
|
// name derivation composed from @gjsify/adwaita-core.
|
|
31
32
|
|
|
32
|
-
import { normalizeIconName } from '@gjsify/adwaita-core';
|
|
33
|
+
import { normalizeIconName, stringIsNotEmpty } from '@gjsify/adwaita-core';
|
|
33
34
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* The generated mask-class prefix — the one place the `--` spelling appears. `icon-registry`
|
|
37
|
+
* builds the same class for a runtime registration and imports it from here rather than
|
|
38
|
+
* spelling it twice.
|
|
39
|
+
*/
|
|
40
|
+
export const MASK_CLASS_PREFIX = 'adw-icon--';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The glyph an icon falls back to — the same name `_icon.scss` masks with, the same one
|
|
44
|
+
* libadwaita's C substitutes for a NULL icon-name, and the same one GTK's icon theme
|
|
45
|
+
* returns for a name it cannot find.
|
|
46
|
+
*/
|
|
47
|
+
export const MISSING_ICON_NAME = 'image-missing';
|
|
36
48
|
|
|
37
49
|
export class AdwIcon extends HTMLElement {
|
|
38
50
|
static get observedAttributes() {
|
|
@@ -52,7 +64,11 @@ export class AdwIcon extends HTMLElement {
|
|
|
52
64
|
/**
|
|
53
65
|
* The name that actually reached the mask class — `''` when the icon-name is
|
|
54
66
|
* absent, empty, or not usable as one CSS token. Consumers that need to know
|
|
55
|
-
* whether
|
|
67
|
+
* whether the DECLARED name is drawable ask this, not the raw attribute.
|
|
68
|
+
*
|
|
69
|
+
* `''` does NOT mean the box draws nothing: an unusable name that was given still
|
|
70
|
+
* draws {@link MISSING_ICON_NAME}. Whether an icon was ASKED FOR is the raw
|
|
71
|
+
* attribute's emptiness, which is the predicate a host's visibility follows.
|
|
56
72
|
*/
|
|
57
73
|
get resolvedIconName(): string {
|
|
58
74
|
return normalizeIconName(this.getAttribute('icon-name'));
|
|
@@ -87,7 +103,17 @@ export class AdwIcon extends HTMLElement {
|
|
|
87
103
|
const stale = [...this.classList].filter((existing) => existing.startsWith(MASK_CLASS_PREFIX));
|
|
88
104
|
this.classList.remove(...stale);
|
|
89
105
|
const name = this.resolvedIconName;
|
|
90
|
-
if (name !== '')
|
|
106
|
+
if (name !== '') {
|
|
107
|
+
this.classList.add(`${MASK_CLASS_PREFIX}${name}`);
|
|
108
|
+
} else if (stringIsNotEmpty(this.getAttribute('icon-name'))) {
|
|
109
|
+
// A name was GIVEN and cannot be one CSS token (a space, a quote, a reverse-DNS
|
|
110
|
+
// application id). GTK has no such state: `gtk_icon_theme_lookup_icon` never
|
|
111
|
+
// returns NULL, it returns the always-available `image-missing`
|
|
112
|
+
// (refs/gtk/gtk/gtkicontheme.c — `gtk_icon_paintable_get_icon_name`'s own docs
|
|
113
|
+
// say so). So the same glyph is drawn here rather than an invisible 16px hole,
|
|
114
|
+
// which is what `<adw-button-row start-icon-name="a b">` reserved and showed.
|
|
115
|
+
this.classList.add(`${MASK_CLASS_PREFIX}${MISSING_ICON_NAME}`);
|
|
116
|
+
}
|
|
91
117
|
|
|
92
118
|
// A masked box has no text, so assistive tech has nothing to announce — the
|
|
93
119
|
// accessible name belongs to the control that HOSTS the icon.
|
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
domViewSwitcherScheduler,
|
|
56
56
|
readSwitcherPage,
|
|
57
57
|
} from './view-switcher-dom.js';
|
|
58
|
+
import { attachRovingFocus } from './roving-focus.js';
|
|
58
59
|
|
|
59
60
|
/** A single page. Children of <adw-inline-view-switcher>; consumed at connect time. */
|
|
60
61
|
export class AdwViewStackPage extends HTMLElement {
|
|
@@ -166,6 +167,17 @@ export class AdwInlineViewSwitcher extends HTMLElement {
|
|
|
166
167
|
this._groupEl = document.createElement('div');
|
|
167
168
|
this._groupEl.className = 'adw-inline-view-switcher-group';
|
|
168
169
|
this._groupEl.setAttribute('role', 'tablist');
|
|
170
|
+
// What makes the roving tabindex in `_render` navigable: without it the
|
|
171
|
+
// unselected toggles are reachable by no key at all. `_toggles` is already the
|
|
172
|
+
// COMPACTED list — a hidden page produces no toggle — so it needs no filter.
|
|
173
|
+
attachRovingFocus({
|
|
174
|
+
host: this,
|
|
175
|
+
orientation: 'horizontal',
|
|
176
|
+
items: () => this._toggles,
|
|
177
|
+
select: (item) => {
|
|
178
|
+
this._state.setSelected(Number(item.dataset.pageIndex));
|
|
179
|
+
},
|
|
180
|
+
});
|
|
169
181
|
|
|
170
182
|
this._pagesEl = document.createElement('div');
|
|
171
183
|
this._pagesEl.className = 'adw-inline-view-switcher-pages';
|