@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
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// <adw-menu-button> — the web counterpart of
|
|
2
|
-
// default the flat `open-menu-symbolic` app-menu button used
|
|
3
|
-
// bar. Clicking it toggles an Adwaita-styled popover under the
|
|
4
|
-
// an outside click or Escape, with arrow-key navigation.
|
|
1
|
+
// <adw-menu-button> — the web counterpart of Gtk.MenuButton, which libadwaita styles
|
|
2
|
+
// but never subclassed: by default the flat `open-menu-symbolic` app-menu button used
|
|
3
|
+
// at the end of a header bar. Clicking it toggles an Adwaita-styled popover under the
|
|
4
|
+
// button, dismissed on an outside click or Escape, with arrow-key navigation.
|
|
5
5
|
//
|
|
6
6
|
// The popover is `<adw-popover>` and the dismissal/keyboard machine is
|
|
7
7
|
// `@gjsify/adwaita-core`'s (ADR 0004); this element keeps only what is a menu button:
|
|
@@ -14,13 +14,19 @@
|
|
|
14
14
|
// detail `{ id, label, index }`) with `id` falling back to `label`, matching the NS
|
|
15
15
|
// twin.
|
|
16
16
|
//
|
|
17
|
-
// Reference: refs/
|
|
17
|
+
// Reference: refs/gtk/gtk/gtkmenubutton.c (GtkMenuButton)
|
|
18
|
+
// Reference: refs/libadwaita/src/stylesheet/widgets/_buttons.scss (menubutton)
|
|
18
19
|
// Reference: refs/libadwaita/src/stylesheet/widgets/_menus.scss (popover.menu / modelbutton)
|
|
19
20
|
// Reference: packages/nativescript-bridge/adwaita/src/widgets/adw-menu-button.ts (NS twin)
|
|
20
21
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
21
22
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
22
23
|
|
|
23
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
isSplitButtonDirection,
|
|
26
|
+
menuButtonPopupDirection,
|
|
27
|
+
parseMenuEntries,
|
|
28
|
+
stringIsNotEmpty,
|
|
29
|
+
} from '@gjsify/adwaita-core';
|
|
24
30
|
import type { AdwMenuEntry, SplitButtonDirection } from '@gjsify/adwaita-core';
|
|
25
31
|
|
|
26
32
|
// SIDE-EFFECT import, deliberately separate from the type import below: it guarantees
|
|
@@ -210,11 +216,12 @@ export class AdwMenuButton extends HTMLElement {
|
|
|
210
216
|
item.setAttribute('role', 'menuitem');
|
|
211
217
|
item.tabIndex = -1;
|
|
212
218
|
|
|
213
|
-
// An entry
|
|
214
|
-
//
|
|
215
|
-
//
|
|
219
|
+
// An entry that asked for NO icon gets no icon node — the emptiness of the
|
|
220
|
+
// declared name, not whether it resolved. Testing `resolvedIconName` dropped
|
|
221
|
+
// the node for a name that was given and merely undrawable, which is the one
|
|
222
|
+
// case a reader needs to SEE; `<adw-icon>` draws `image-missing` there.
|
|
216
223
|
const entryIcon = createAdwIcon(entry.icon ?? null, 'adw-menu-button-item-icon');
|
|
217
|
-
if (
|
|
224
|
+
if (stringIsNotEmpty(entry.icon)) item.appendChild(entryIcon);
|
|
218
225
|
const labelEl = document.createElement('span');
|
|
219
226
|
labelEl.className = 'adw-menu-button-item-label';
|
|
220
227
|
labelEl.textContent = entry.label;
|
|
@@ -50,6 +50,12 @@ export class AdwNavigationSplitView extends HTMLElement {
|
|
|
50
50
|
private _contentEl!: HTMLDivElement;
|
|
51
51
|
private _initialized = false;
|
|
52
52
|
private _disposeBreakpoint: (() => void) | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* What the breakpoint last SET, carried across every rebind. Not read off the
|
|
55
|
+
* attribute: a `collapsed` the markup declared was never this breakpoint's doing,
|
|
56
|
+
* and unapplying it at connect would undo the author.
|
|
57
|
+
*/
|
|
58
|
+
private _breakpointApplied = false;
|
|
53
59
|
/** The ordering table, the tag guard and the action routing (ADR 0004). */
|
|
54
60
|
private _state = new NavigationSplitViewState();
|
|
55
61
|
/** Re-entrancy guard for the `show-content` reflection. */
|
|
@@ -118,6 +124,21 @@ export class AdwNavigationSplitView extends HTMLElement {
|
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
connectedCallback() {
|
|
127
|
+
this._buildOnce();
|
|
128
|
+
// `_syncBreakpoint` is the one that HAS to run on EVERY connect: it re-establishes
|
|
129
|
+
// the ResizeObserver `disconnectedCallback` disposed, and without it a view MOVED
|
|
130
|
+
// between parents (a slideshow slide, a client-side route change) stops tracking
|
|
131
|
+
// its condition for good — `<adw-navigation-split-view breakpoint="max-width: 720px">`
|
|
132
|
+
// never collapses again however narrow the page gets. The three above it are the
|
|
133
|
+
// post-build derivations, idempotent on a later connect and left in the order
|
|
134
|
+
// `<adw-overlay-split-view>` runs them so the two views read alike.
|
|
135
|
+
this._reflectShowContent();
|
|
136
|
+
this._syncWidth();
|
|
137
|
+
this._syncClasses();
|
|
138
|
+
this._syncBreakpoint();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private _buildOnce() {
|
|
121
142
|
if (this._initialized) return;
|
|
122
143
|
this._initialized = true;
|
|
123
144
|
|
|
@@ -160,10 +181,6 @@ export class AdwNavigationSplitView extends HTMLElement {
|
|
|
160
181
|
this._reflectShowContent();
|
|
161
182
|
this._syncClasses();
|
|
162
183
|
});
|
|
163
|
-
|
|
164
|
-
this._syncWidth();
|
|
165
|
-
this._syncClasses();
|
|
166
|
-
this._syncBreakpoint();
|
|
167
184
|
}
|
|
168
185
|
|
|
169
186
|
/** `Adw.NavigationPage:tag` off the slotted pane, or `null` when untagged. */
|
|
@@ -284,9 +301,15 @@ export class AdwNavigationSplitView extends HTMLElement {
|
|
|
284
301
|
|
|
285
302
|
private _syncBreakpoint() {
|
|
286
303
|
this._disposeBreakpoint?.();
|
|
287
|
-
this._disposeBreakpoint = bindBreakpointSetter(
|
|
288
|
-
this
|
|
289
|
-
|
|
304
|
+
this._disposeBreakpoint = bindBreakpointSetter(
|
|
305
|
+
this,
|
|
306
|
+
this.getAttribute('breakpoint'),
|
|
307
|
+
(active) => {
|
|
308
|
+
this._breakpointApplied = active;
|
|
309
|
+
this.collapsed = active;
|
|
310
|
+
},
|
|
311
|
+
this._breakpointApplied,
|
|
312
|
+
);
|
|
290
313
|
}
|
|
291
314
|
}
|
|
292
315
|
|
|
@@ -84,6 +84,12 @@ export class AdwOverlaySplitView extends HTMLElement {
|
|
|
84
84
|
private _contentEl!: HTMLDivElement;
|
|
85
85
|
private _backdropEl!: HTMLDivElement;
|
|
86
86
|
private _disposeBreakpoint: (() => void) | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* What the breakpoint last SET, carried across every rebind. Not read off the
|
|
89
|
+
* attribute: a `collapsed` the markup declared was never this breakpoint's doing,
|
|
90
|
+
* and unapplying it at connect would undo the author.
|
|
91
|
+
*/
|
|
92
|
+
private _breakpointApplied = false;
|
|
87
93
|
/**
|
|
88
94
|
* The property interplay, shared with the NativeScript renderer (ADR 0004): the
|
|
89
95
|
* element owns the attributes and the painting, core owns what `collapsed` +
|
|
@@ -335,9 +341,15 @@ export class AdwOverlaySplitView extends HTMLElement {
|
|
|
335
341
|
|
|
336
342
|
private _syncBreakpoint() {
|
|
337
343
|
this._disposeBreakpoint?.();
|
|
338
|
-
this._disposeBreakpoint = bindBreakpointSetter(
|
|
339
|
-
this
|
|
340
|
-
|
|
344
|
+
this._disposeBreakpoint = bindBreakpointSetter(
|
|
345
|
+
this,
|
|
346
|
+
this.getAttribute('breakpoint'),
|
|
347
|
+
(active) => {
|
|
348
|
+
this._breakpointApplied = active;
|
|
349
|
+
this.collapsed = active;
|
|
350
|
+
},
|
|
351
|
+
this._breakpointApplied,
|
|
352
|
+
);
|
|
341
353
|
}
|
|
342
354
|
|
|
343
355
|
openSidebar() {
|
|
@@ -160,6 +160,17 @@ export class AdwPopover extends HTMLElement {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
connectedCallback() {
|
|
163
|
+
this._buildOnce();
|
|
164
|
+
// EVERY connect, not only the first. The document listeners are bound from
|
|
165
|
+
// `_onStateChange` and released by `disconnectedCallback`, so a popover that was
|
|
166
|
+
// OPEN when it moved — a slideshow slide, a client-side route change — comes back
|
|
167
|
+
// visible with neither outside-click nor Escape dismissal, and no state change
|
|
168
|
+
// left to re-arm them: the only ways out are the two that just died. Idempotent
|
|
169
|
+
// by construction, the same function references and capture flag the unbind uses.
|
|
170
|
+
if (this._state.open) this._bindDocument();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private _buildOnce() {
|
|
163
174
|
if (this._initialized) return;
|
|
164
175
|
this._initialized = true;
|
|
165
176
|
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
|
|
51
51
|
import type { PreferencesSearchResult, SearchPreferencesOptions } from '@gjsify/adwaita-core';
|
|
52
52
|
import { searchPreferencesDom } from '../preferences-search.js';
|
|
53
|
+
import { AdwModalSurface } from './modal-surface.js';
|
|
53
54
|
|
|
54
55
|
const DEFAULT_CONTENT_WIDTH = 640;
|
|
55
56
|
|
|
@@ -106,6 +107,7 @@ export class AdwPreferencesDialog extends HTMLElement {
|
|
|
106
107
|
private _bodyEl!: HTMLDivElement;
|
|
107
108
|
private _pagesEl!: HTMLDivElement;
|
|
108
109
|
private _closeBtn!: HTMLButtonElement;
|
|
110
|
+
private _modal!: AdwModalSurface;
|
|
109
111
|
|
|
110
112
|
static get observedAttributes() {
|
|
111
113
|
return ['title', 'open', 'can-close', 'content-width'];
|
|
@@ -218,8 +220,6 @@ export class AdwPreferencesDialog extends HTMLElement {
|
|
|
218
220
|
|
|
219
221
|
this._dialogEl = document.createElement('div');
|
|
220
222
|
this._dialogEl.className = 'adw-preferences-dialog-box';
|
|
221
|
-
this._dialogEl.setAttribute('role', 'dialog');
|
|
222
|
-
this._dialogEl.setAttribute('aria-modal', 'true');
|
|
223
223
|
// Clicks inside the dialog must not bubble to the scrim's dismiss handler.
|
|
224
224
|
this._dialogEl.addEventListener('click', (event) => event.stopPropagation());
|
|
225
225
|
|
|
@@ -241,8 +241,8 @@ export class AdwPreferencesDialog extends HTMLElement {
|
|
|
241
241
|
this._closeBtn.type = 'button';
|
|
242
242
|
this._closeBtn.className = 'adw-preferences-dialog-close';
|
|
243
243
|
this._closeBtn.setAttribute('aria-label', 'Close');
|
|
244
|
-
// The "×" is a CSS glyph
|
|
245
|
-
//
|
|
244
|
+
// The "×" is a CSS glyph, for the sizing reason adw-tab-view's close affordance
|
|
245
|
+
// records — not for want of a mask class, which `window-close` has.
|
|
246
246
|
this._closeBtn.addEventListener('click', () => this._attemptClose());
|
|
247
247
|
trailing.appendChild(this._closeBtn);
|
|
248
248
|
|
|
@@ -260,14 +260,14 @@ export class AdwPreferencesDialog extends HTMLElement {
|
|
|
260
260
|
|
|
261
261
|
this.replaceChildren(this._scrimEl, this._dialogEl);
|
|
262
262
|
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
263
|
+
// The role, `aria-modal`, Escape (Adw.Dialog's shortcut → `maybe_close_cb`), the
|
|
264
|
+
// Tab trap and the return-focus.
|
|
265
|
+
this._modal = new AdwModalSurface({
|
|
266
|
+
host: this,
|
|
267
|
+
surface: this._dialogEl,
|
|
268
|
+
role: 'dialog',
|
|
269
|
+
isOpen: () => this.open,
|
|
270
|
+
onEscape: () => this._attemptClose(),
|
|
271
271
|
});
|
|
272
272
|
|
|
273
273
|
this._render();
|
|
@@ -279,7 +279,12 @@ export class AdwPreferencesDialog extends HTMLElement {
|
|
|
279
279
|
this._render();
|
|
280
280
|
if (name === 'open') {
|
|
281
281
|
this.dispatchEvent(new CustomEvent('notify::open', { bubbles: true, detail: { open: this.open } }));
|
|
282
|
-
if (
|
|
282
|
+
if (this.open) {
|
|
283
|
+
this._modal.present();
|
|
284
|
+
} else {
|
|
285
|
+
this.dispatchEvent(new CustomEvent('closed', { bubbles: true }));
|
|
286
|
+
this._modal.dismiss();
|
|
287
|
+
}
|
|
283
288
|
}
|
|
284
289
|
}
|
|
285
290
|
|
|
@@ -304,9 +309,6 @@ export class AdwPreferencesDialog extends HTMLElement {
|
|
|
304
309
|
const locked = !this.canClose;
|
|
305
310
|
this._closeBtn.disabled = locked;
|
|
306
311
|
this._closeBtn.classList.toggle('locked', locked);
|
|
307
|
-
|
|
308
|
-
// Move focus into the dialog on present so Escape works immediately.
|
|
309
|
-
if (this.open) this.focus();
|
|
310
312
|
}
|
|
311
313
|
}
|
|
312
314
|
|
|
@@ -115,6 +115,10 @@ export class AdwPreferencesGroup extends HTMLElement {
|
|
|
115
115
|
for (const child of rowChildren) this._listboxEl.appendChild(child);
|
|
116
116
|
|
|
117
117
|
this.replaceChildren(this._headerEl, this._listboxEl);
|
|
118
|
+
// adw-preferences-group.c:319 — GTK_ACCESSIBLE_ROLE_GROUP. A GROUP, not a list:
|
|
119
|
+
// that is why the rows inside carry no `listitem` role, which outside a list
|
|
120
|
+
// would be worse than none.
|
|
121
|
+
this.setAttribute('role', 'group');
|
|
118
122
|
|
|
119
123
|
this._renderHeader();
|
|
120
124
|
this._observeHost();
|
|
@@ -158,6 +162,9 @@ export class AdwPreferencesGroup extends HTMLElement {
|
|
|
158
162
|
});
|
|
159
163
|
this._observer.observe(this, { childList: true });
|
|
160
164
|
this._observer.observe(this._listboxEl, { childList: true });
|
|
165
|
+
// The suffix too: `hasHeaderSuffix` is an INPUT to the header derivation, so a
|
|
166
|
+
// suffix appended after connect left the header hidden on a group with no title.
|
|
167
|
+
this._observer.observe(this._suffixEl, { childList: true });
|
|
161
168
|
}
|
|
162
169
|
|
|
163
170
|
private _renderHeader() {
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
} from '@gjsify/adwaita-core';
|
|
58
58
|
|
|
59
59
|
import { createAdwIcon } from './adw-icon.js';
|
|
60
|
+
import { attachRovingFocus } from './roving-focus.js';
|
|
60
61
|
|
|
61
62
|
// The sidebar consumes its declared children and drops them from the tree, so a later
|
|
62
63
|
// `setAttribute` on one cannot find its sidebar with `closest()`. These keep the link —
|
|
@@ -161,6 +162,20 @@ export class AdwSidebar extends HTMLElement {
|
|
|
161
162
|
this._listEl = document.createElement('div');
|
|
162
163
|
this._listEl.className = 'adw-sidebar-list';
|
|
163
164
|
this._listEl.setAttribute('role', 'listbox');
|
|
165
|
+
// What makes the roving tabindex in `_applySelection` navigable: without it the
|
|
166
|
+
// unselected rows are reachable by no key at all. `setSelected` and NOT
|
|
167
|
+
// `_activate`: an arrow key is `GtkListBox`'s `row-selected`, and firing
|
|
168
|
+
// `activated` on every press would navigate a split view on each keystroke.
|
|
169
|
+
// Enter and Space still activate — the row IS a <button>.
|
|
170
|
+
attachRovingFocus({
|
|
171
|
+
host: this,
|
|
172
|
+
orientation: 'vertical',
|
|
173
|
+
items: () => this._rows.filter((row) => !row.el.hidden && !row.el.disabled).map((row) => row.el),
|
|
174
|
+
select: (item) => {
|
|
175
|
+
const row = this._rows.find((candidate) => candidate.el === item);
|
|
176
|
+
if (row !== undefined) this._state.setSelected(row.index);
|
|
177
|
+
},
|
|
178
|
+
});
|
|
164
179
|
|
|
165
180
|
// Seed the model BEFORE subscribing: connecting is not a change anyone
|
|
166
181
|
// asked to hear about, and `setSections` runs the 0 → n auto-select.
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
// icon, a title, a description and an optional action (slotted child).
|
|
3
3
|
// Attributes: icon (symbolic name, with or without -symbolic), title, description.
|
|
4
4
|
// Reference: refs/adwaita-web/adwaita-web/scss/_status_page.scss
|
|
5
|
-
// Reference: refs/libadwaita/src/stylesheet/widgets/
|
|
5
|
+
// Reference: refs/libadwaita/src/stylesheet/widgets/_misc.scss (AdwStatusPage)
|
|
6
6
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
7
7
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the
|
|
8
8
|
// icon node is <adw-icon>.
|
|
9
9
|
|
|
10
10
|
import { stringIsNotEmpty } from '@gjsify/adwaita-core';
|
|
11
11
|
|
|
12
|
+
import { bindEmptySections } from '../empty-sections.js';
|
|
12
13
|
import { type AdwIcon, createAdwIcon } from './adw-icon.js';
|
|
13
14
|
|
|
14
15
|
export class AdwStatusPage extends HTMLElement {
|
|
@@ -41,6 +42,9 @@ export class AdwStatusPage extends HTMLElement {
|
|
|
41
42
|
for (const child of children) this._childEl.appendChild(child);
|
|
42
43
|
|
|
43
44
|
this.replaceChildren(this._iconEl, this._titleEl, this._descEl, this._childEl);
|
|
45
|
+
// `_render` only ever runs from `attributeChangedCallback`, which an action
|
|
46
|
+
// appended into the child slot after connect does not trigger.
|
|
47
|
+
bindEmptySections(this._childEl);
|
|
44
48
|
this._render();
|
|
45
49
|
}
|
|
46
50
|
|
|
@@ -51,7 +55,13 @@ export class AdwStatusPage extends HTMLElement {
|
|
|
51
55
|
private _render() {
|
|
52
56
|
// The element swaps the mask class and keeps the size class.
|
|
53
57
|
this._iconEl.iconName = this.getAttribute('icon');
|
|
54
|
-
|
|
58
|
+
// `has_image`: `paintable || (icon_name && icon_name[0])` — a name that was GIVEN
|
|
59
|
+
// shows the image whether or not the theme can resolve it, and GTK draws
|
|
60
|
+
// `image-missing` in it. Reading `resolvedIconName` instead hid the slot for every
|
|
61
|
+
// name that is not one CSS token, a branch the C does not have.
|
|
62
|
+
// Reference: refs/libadwaita/src/adw-status-page.c:88 (has_image), bound from
|
|
63
|
+
// `<binding name="visible">` in adw-status-page.ui:27-31.
|
|
64
|
+
this._iconEl.hidden = !stringIsNotEmpty(this.getAttribute('icon'));
|
|
55
65
|
|
|
56
66
|
// `string_is_not_empty` from the core, not a local `.length === 0`: the same
|
|
57
67
|
// closure the NativeScript port binds its labels to, and the reason a title of
|
|
@@ -63,8 +73,6 @@ export class AdwStatusPage extends HTMLElement {
|
|
|
63
73
|
const description = this.getAttribute('description') ?? '';
|
|
64
74
|
this._descEl.textContent = description;
|
|
65
75
|
this._descEl.hidden = !stringIsNotEmpty(description);
|
|
66
|
-
|
|
67
|
-
this._childEl.hidden = this._childEl.childElementCount === 0;
|
|
68
76
|
}
|
|
69
77
|
}
|
|
70
78
|
|
|
@@ -25,6 +25,7 @@ import { SwitchRowState, deriveRowLabels } from '@gjsify/adwaita-core';
|
|
|
25
25
|
// without `verbatimModuleSyntax`, so TypeScript would elide the statement and take the
|
|
26
26
|
// registration with it.
|
|
27
27
|
import './adw-switch.js';
|
|
28
|
+
import { attachRowActivation } from './row-activation.js';
|
|
28
29
|
import type { AdwSwitch } from './adw-switch.js';
|
|
29
30
|
|
|
30
31
|
export class AdwSwitchRow extends HTMLElement {
|
|
@@ -62,6 +63,8 @@ export class AdwSwitchRow extends HTMLElement {
|
|
|
62
63
|
text.append(this._titleEl, this._subtitleEl);
|
|
63
64
|
|
|
64
65
|
this._switchEl = document.createElement('adw-switch') as AdwSwitch;
|
|
66
|
+
// adw-switch-row.c:159 — the slider is not a focus target; the ROW is.
|
|
67
|
+
this._switchEl.unfocusable = true;
|
|
65
68
|
|
|
66
69
|
this.replaceChildren(text, this._switchEl);
|
|
67
70
|
|
|
@@ -86,6 +89,10 @@ export class AdwSwitchRow extends HTMLElement {
|
|
|
86
89
|
this._apply(this._state.activate());
|
|
87
90
|
});
|
|
88
91
|
|
|
92
|
+
// adw-switch-row.c:160 makes the row itself activatable. Enter and Space run the
|
|
93
|
+
// same transition a click does, through the row's own click handler.
|
|
94
|
+
attachRowActivation({ row: this, activatable: () => true });
|
|
95
|
+
|
|
89
96
|
this._renderText();
|
|
90
97
|
}
|
|
91
98
|
|
|
@@ -35,7 +35,23 @@ export class AdwSwitch extends HTMLElement {
|
|
|
35
35
|
private _initialized = false;
|
|
36
36
|
|
|
37
37
|
static get observedAttributes() {
|
|
38
|
-
return ['active', 'disabled'];
|
|
38
|
+
return ['active', 'disabled', 'unfocusable'];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* `Gtk.Widget:can-focus` on the slider, inverted so the default stays "yes".
|
|
43
|
+
*
|
|
44
|
+
* `adw_switch_row_init` does `gtk_widget_set_can_focus (self->slider, FALSE)`
|
|
45
|
+
* (adw-switch-row.c:159) and makes the ROW activatable instead, so a switch row is ONE
|
|
46
|
+
* tab stop announced as its own title — not a bare checkbox followed by a label that
|
|
47
|
+
* cannot be reached. A standalone `<adw-switch>` keeps its checkbox focusable.
|
|
48
|
+
*/
|
|
49
|
+
get unfocusable(): boolean {
|
|
50
|
+
return this.hasAttribute('unfocusable');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
set unfocusable(value: boolean) {
|
|
54
|
+
this.toggleAttribute('unfocusable', !!value);
|
|
39
55
|
}
|
|
40
56
|
|
|
41
57
|
/** Whether the switch is on. */
|
|
@@ -103,6 +119,8 @@ export class AdwSwitch extends HTMLElement {
|
|
|
103
119
|
const disabled = this.disabled;
|
|
104
120
|
this._input.checked = this.active;
|
|
105
121
|
this._input.disabled = disabled;
|
|
122
|
+
if (this.unfocusable) this._input.tabIndex = -1;
|
|
123
|
+
else this._input.removeAttribute('tabindex');
|
|
106
124
|
this.classList.toggle('disabled', disabled);
|
|
107
125
|
}
|
|
108
126
|
}
|
|
@@ -500,10 +500,13 @@ export class AdwTabView extends HTMLElement {
|
|
|
500
500
|
|
|
501
501
|
tab.appendChild(createAdwIcon(null, 'adw-tab-indicator'));
|
|
502
502
|
|
|
503
|
-
// Close affordance — a small flat button drawn with a CSS "×" glyph
|
|
504
|
-
// `window-close`
|
|
505
|
-
//
|
|
506
|
-
//
|
|
503
|
+
// Close affordance — a small flat button drawn with a CSS "×" glyph. NOT because
|
|
504
|
+
// the glyph is unavailable: `window-close` is in the ICONS map and has a mask class
|
|
505
|
+
// (the comment that used to stand here said otherwise, and was read by two more
|
|
506
|
+
// widgets). It is a SIZING difference — upstream's close button is 24px around a
|
|
507
|
+
// 16px symbolic (adw-tab.ui:77), this strip's chip budgets 18px, and a 16px-grid
|
|
508
|
+
// symbolic scaled into it is a design change, not a rename.
|
|
509
|
+
// `can-focus=False` in C, so it stays out of the tab order here too.
|
|
507
510
|
const close = document.createElement('button');
|
|
508
511
|
close.type = 'button';
|
|
509
512
|
close.className = 'adw-tab-close';
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
// queue/auto-dismiss state machine composed from @gjsify/adwaita-core.
|
|
19
19
|
|
|
20
20
|
import { AdwToast, AdwToastQueue } from '@gjsify/adwaita-core';
|
|
21
|
+
|
|
22
|
+
import { createAdwIcon } from './adw-icon.js';
|
|
21
23
|
import type { ToastScheduler, ToastTimerHandle } from '@gjsify/adwaita-core';
|
|
22
24
|
|
|
23
25
|
// The timing seam is re-exported so a consumer can type a custom scheduler for
|
|
@@ -183,11 +185,16 @@ export class AdwToastOverlay extends HTMLElement {
|
|
|
183
185
|
el.appendChild(button);
|
|
184
186
|
}
|
|
185
187
|
|
|
186
|
-
// Close affordance
|
|
188
|
+
// Close affordance — the real `window-close` glyph, which is what upstream puts
|
|
189
|
+
// here (adw-toast-widget.ui:64) and what this port drew with two rotated CSS bars
|
|
190
|
+
// under a comment claiming no such icon shipped. It does, and the toast is the one
|
|
191
|
+
// close button in the package already sized the way upstream sizes it: a 16px
|
|
192
|
+
// symbolic in a 24px circle, so the swap is the name and nothing else.
|
|
187
193
|
const close = document.createElement('button');
|
|
188
194
|
close.type = 'button';
|
|
189
195
|
close.className = 'adw-toast-close-button';
|
|
190
196
|
close.setAttribute('aria-label', 'Close');
|
|
197
|
+
close.appendChild(createAdwIcon('window-close'));
|
|
191
198
|
close.addEventListener('click', () => this._queue.dismiss());
|
|
192
199
|
el.appendChild(close);
|
|
193
200
|
|
|
@@ -7,7 +7,44 @@
|
|
|
7
7
|
// The SELECTION state machine (the segment list plus the guarded, no-op-on-same active
|
|
8
8
|
// index) is HEADLESS and lives in `@gjsify/adwaita-core` (ADR 0004) as
|
|
9
9
|
// {@link ToggleGroupState}; this element keeps only the DOM half — the buttons, the active
|
|
10
|
-
// pill /
|
|
10
|
+
// pill / checked reflection, the keyboard and the event.
|
|
11
|
+
//
|
|
12
|
+
// THE ROLE IS A RADIO GROUP, not a tab list, and the element had neither. Upstream
|
|
13
|
+
// declares `GTK_ACCESSIBLE_ROLE_RADIO_GROUP` (adw-toggle-group.c:1191) and reads the
|
|
14
|
+
// group's CURRENT role when it builds each toggle: a group already declared a tab list
|
|
15
|
+
// gets `GTK_ACCESSIBLE_ROLE_TAB` children, everything else `GTK_ACCESSIBLE_ROLE_RADIO`
|
|
16
|
+
// (:857-865, under the comment "Special case for AdwInlineViewSwitcher" — that switcher
|
|
17
|
+
// builds exactly this widget with `TAB_LIST`, adw-inline-view-switcher.c:702). Both
|
|
18
|
+
// branches are ported, in the same order, because a consumer that declares the tab list
|
|
19
|
+
// upstream sanctions would otherwise get radios inside it.
|
|
20
|
+
//
|
|
21
|
+
// `aria-pressed` was the state before, and it belongs to NEITHER role — it is the
|
|
22
|
+
// toolbar toggle-button pattern, where each button is independent. The state of one of
|
|
23
|
+
// N mutually exclusive choices is `aria-checked` under `radio` and `aria-selected` under
|
|
24
|
+
// `tab`, so a screen reader was told these were three separate on/off buttons.
|
|
25
|
+
//
|
|
26
|
+
// THE KEYBOARD is one tab stop and arrows inside, which is the roving tabindex
|
|
27
|
+
// `elements/roving-focus.ts` implements — and `adw_toggle_group_focus`
|
|
28
|
+
// (adw-toggle-group.c:1045) is the citation that module is built on. Tab in either
|
|
29
|
+
// direction is PROPAGATED (:1059-1060), i.e. focus leaves the group; every other
|
|
30
|
+
// direction goes to `adw_widget_focus_child` (:1062), i.e. moves within it. Which button
|
|
31
|
+
// Tab enters on is `adw_toggle_group_grab_focus` (:1066): the ACTIVE toggle's button,
|
|
32
|
+
// never the first. The web element had three plain tab stops and no arrow keys at all,
|
|
33
|
+
// so it stayed operable — this change turns those three stops into one, which is visible
|
|
34
|
+
// to anything that tabs through a toggle group.
|
|
35
|
+
//
|
|
36
|
+
// HORIZONTAL ONLY, and the reason is not the missing `orientation` property. The C looks
|
|
37
|
+
// orientation-blind — `adw_toggle_group_focus` filters the two Tab directions and hands
|
|
38
|
+
// every other one on without consulting `self->orientation` — but the axis is decided
|
|
39
|
+
// one level down, GEOMETRICALLY. `adw_widget_focus_child` is `focus_move`
|
|
40
|
+
// (adw-widget-utils.c:428), which sorts through `focus_sort` (:388), which dispatches
|
|
41
|
+
// UP/DOWN into `focus_sort_up_down` (:298). That sorter DELETES every sibling with no
|
|
42
|
+
// horizontal overlap with the focused child (:339-342) — which, in a row of toggles, is
|
|
43
|
+
// all of them. The list is left holding only the focused button, `gtk_widget_child_focus`
|
|
44
|
+
// on a leaf that already has focus returns FALSE, and the press PROPAGATES: upstream,
|
|
45
|
+
// ArrowDown in a horizontal toggle group leaves the group. So `'horizontal'` is what the
|
|
46
|
+
// C does for the only layout this element has, it matches the three sibling tab lists,
|
|
47
|
+
// and it leaves ArrowUp/ArrowDown to the page instead of swallowing a scroll.
|
|
11
48
|
//
|
|
12
49
|
// Reference: refs/libadwaita/src/adw-toggle-group.c (AdwToggleGroup behaviour)
|
|
13
50
|
// Reference: refs/libadwaita/src/stylesheet/widgets/_toggle-group.scss
|
|
@@ -20,6 +57,7 @@
|
|
|
20
57
|
import { ToggleGroupState } from '@gjsify/adwaita-core';
|
|
21
58
|
|
|
22
59
|
import { createAdwIcon } from './adw-icon.js';
|
|
60
|
+
import { attachRovingFocus } from './roving-focus.js';
|
|
23
61
|
|
|
24
62
|
/** A single toggle. Children of <adw-toggle-group>; consumed at connect time. */
|
|
25
63
|
export class AdwToggle extends HTMLElement {
|
|
@@ -34,6 +72,8 @@ export class AdwToggleGroup extends HTMLElement {
|
|
|
34
72
|
/** The headless segment list + guarded selection state machine (ADR 0004). */
|
|
35
73
|
private readonly _state = new ToggleGroupState();
|
|
36
74
|
private _initialized = false;
|
|
75
|
+
/** `aria-checked` under `radio`, `aria-selected` under `tab` — decided at connect. */
|
|
76
|
+
private _stateAttr: 'aria-checked' | 'aria-selected' = 'aria-checked';
|
|
37
77
|
|
|
38
78
|
static get observedAttributes() {
|
|
39
79
|
return ['active', 'flat', 'round'];
|
|
@@ -56,13 +96,48 @@ export class AdwToggleGroup extends HTMLElement {
|
|
|
56
96
|
// subtree — their label / icon-name become the rendered buttons.
|
|
57
97
|
const toggles = Array.from(this.querySelectorAll('adw-toggle')) as AdwToggle[];
|
|
58
98
|
|
|
99
|
+
// `gtk_widget_class_set_accessible_role` sets a CLASS DEFAULT that an instance's
|
|
100
|
+
// construct-time `accessible-role` overrides, and `add_toggle` then reads
|
|
101
|
+
// whatever the instance actually has. So a declared role is kept — only an
|
|
102
|
+
// undeclared one is filled in — and `tablist` is the single branch, exactly as
|
|
103
|
+
// in the C: any other role still gets radio children.
|
|
104
|
+
// `|| null`, not `?? null`: `role=""` is not a declared role, and treating it as
|
|
105
|
+
// one left the group with no role at all while its children were radios.
|
|
106
|
+
const declared = this.getAttribute('role')?.trim() || null;
|
|
107
|
+
if (declared === null) this.setAttribute('role', 'radiogroup');
|
|
108
|
+
const isTabList = declared === 'tablist';
|
|
109
|
+
const toggleRole = isTabList ? 'tab' : 'radio';
|
|
110
|
+
this._stateAttr = isTabList ? 'aria-selected' : 'aria-checked';
|
|
111
|
+
// Read ONCE, which is what upstream allows: `GtkAccessible:accessible-role` is
|
|
112
|
+
// writable but documented "The accessible role cannot be changed once set", and
|
|
113
|
+
// the only public setter is the CLASS-level `gtk_widget_class_set_accessible_role`
|
|
114
|
+
// — there is no instance setter at all. So a later `role=` change has no upstream
|
|
115
|
+
// meaning, and re-labelling the toggles on it would be behaviour this port
|
|
116
|
+
// invented. Declare it in markup, or before the element is connected.
|
|
117
|
+
//
|
|
118
|
+
// (It is NOT construct-only, which an earlier draft of this comment claimed:
|
|
119
|
+
// `Gtk-4.0.gir` marks the property `writable` with no construct flag at all. The
|
|
120
|
+
// rule is the documented one above, which is the stronger citation anyway.)
|
|
121
|
+
|
|
59
122
|
this._innerEl = document.createElement('div');
|
|
60
123
|
this._innerEl.className = 'adw-toggle-group-inner';
|
|
124
|
+
// The toggles are this element's own children upstream; the wrapper is a web-only
|
|
125
|
+
// flex box with no counterpart in the C at all. Without a role it is a `generic`
|
|
126
|
+
// sitting between a radio group and its radios, so it declares itself away.
|
|
127
|
+
//
|
|
128
|
+
// NO upstream precedent is claimed for this, and an earlier draft of this comment
|
|
129
|
+
// invented one: every `GTK_ACCESSIBLE_ROLE_PRESENTATION` in libadwaita is a leaf
|
|
130
|
+
// decoration (an icon, a gizmo), never a layout container between a role-bearing
|
|
131
|
+
// parent and role-bearing children — because GTK has no such container here to
|
|
132
|
+
// annotate. That is exactly why the attribute is needed on this side and nowhere
|
|
133
|
+
// in the C.
|
|
134
|
+
this._innerEl.setAttribute('role', 'none');
|
|
61
135
|
|
|
62
136
|
this._buttons = toggles.map((toggle, index) => {
|
|
63
137
|
const btn = document.createElement('button');
|
|
64
138
|
btn.type = 'button';
|
|
65
139
|
btn.className = 'adw-toggle';
|
|
140
|
+
btn.setAttribute('role', toggleRole);
|
|
66
141
|
|
|
67
142
|
const label = toggle.getAttribute('label') ?? '';
|
|
68
143
|
const icon = toggle.getAttribute('icon-name') ?? '';
|
|
@@ -87,6 +162,20 @@ export class AdwToggleGroup extends HTMLElement {
|
|
|
87
162
|
this._state.subscribe(() => this._render());
|
|
88
163
|
this._state.setSelected(this._readActiveAttr());
|
|
89
164
|
this._render();
|
|
165
|
+
|
|
166
|
+
// No `disabled`/`hidden` filter, because no `<adw-toggle>` attribute can produce
|
|
167
|
+
// either — upstream's per-toggle `enabled` (adw-toggle-group.c:871) has no web
|
|
168
|
+
// counterpart yet, and a filter for a state this element cannot reach would be
|
|
169
|
+
// untestable. `AdwToggle.observedAttributes` is pinned in
|
|
170
|
+
// `keyboard-operable.spec.ts` so the day it grows, this decision has to be
|
|
171
|
+
// revisited instead of silently becoming a focus trap.
|
|
172
|
+
attachRovingFocus({
|
|
173
|
+
host: this,
|
|
174
|
+
orientation: 'horizontal',
|
|
175
|
+
items: () => this._buttons,
|
|
176
|
+
// Same path a click takes, so an arrow key cannot drift from a press.
|
|
177
|
+
select: (item) => this._selectIndex(this._buttons.findIndex((btn) => btn === item)),
|
|
178
|
+
});
|
|
90
179
|
}
|
|
91
180
|
|
|
92
181
|
attributeChangedCallback(name: string) {
|
|
@@ -124,7 +213,10 @@ export class AdwToggleGroup extends HTMLElement {
|
|
|
124
213
|
this._buttons.forEach((btn, index) => {
|
|
125
214
|
const isActive = index === active;
|
|
126
215
|
btn.classList.toggle('active', isActive);
|
|
127
|
-
btn.setAttribute(
|
|
216
|
+
btn.setAttribute(this._stateAttr, String(isActive));
|
|
217
|
+
// The roving tabindex: Tab enters on the ACTIVE toggle, the way
|
|
218
|
+
// `adw_toggle_group_grab_focus` grabs it, and leaves the group from there.
|
|
219
|
+
btn.tabIndex = isActive ? 0 : -1;
|
|
128
220
|
});
|
|
129
221
|
}
|
|
130
222
|
}
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
parseToolbarStyle,
|
|
31
31
|
toolbarViewClasses,
|
|
32
32
|
} from '@gjsify/adwaita-core';
|
|
33
|
+
import { bindEmptySections } from '../empty-sections.js';
|
|
33
34
|
import { AdwScrollShading } from '../scroll-shading.js';
|
|
34
35
|
|
|
35
36
|
export class AdwToolbarView extends HTMLElement {
|
|
@@ -88,8 +89,11 @@ export class AdwToolbarView extends HTMLElement {
|
|
|
88
89
|
this._bottomEl.className = 'adw-toolbar-view-bottom';
|
|
89
90
|
for (const child of bottomChildren) this._bottomEl.appendChild(child);
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
// A bar appended imperatively — which `topBar` invites — is a childList
|
|
93
|
+
// change, not an attribute one, so the empty/non-empty decision cannot be a
|
|
94
|
+
// one-shot read here. The ResizeObserver below then sees the bar go 0 → 48
|
|
95
|
+
// and the undershoot classes follow on their own.
|
|
96
|
+
bindEmptySections(this._topEl, this._bottomEl);
|
|
93
97
|
|
|
94
98
|
this.replaceChildren(this._topEl, this._contentEl, this._bottomEl);
|
|
95
99
|
}
|