@gjsify/adwaita-web 0.40.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 +142 -63
- 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-carousel.d.ts +48 -0
- 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-tab-view.d.ts +23 -0
- package/lib/types/elements/adw-toggle-group.d.ts +2 -0
- package/lib/types/elements/adw-toolbar-view.d.ts +8 -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 +3 -1
- package/lib/types/keyboard-operable.spec.d.ts +1 -0
- package/lib/types/scroll-shading.d.ts +56 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/package.json +10 -6
- package/scss/_banner.scss +1 -1
- package/scss/_carousel.scss +15 -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/_headerbar.scss +35 -6
- package/scss/_icon.scss +47 -0
- package/scss/_icons.generated.scss +36 -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/_scrolling.scss +107 -0
- package/scss/_status_page.scss +1 -1
- package/scss/_style_classes.scss +21 -8
- package/scss/_tab_view.scss +1 -1
- package/scss/_theme.scss +1 -0
- package/scss/_toast.scss +6 -24
- package/scss/_toolbar_view.scss +52 -22
- package/scss/_variables.scss +30 -3
- package/scss/_view_switcher.scss +80 -20
- package/scss/adwaita-skin.scss +1 -0
- package/src/adw-action-rows.spec.ts +21 -9
- package/src/adw-button-content.spec.ts +11 -6
- package/src/adw-carousel.spec.ts +141 -1
- package/src/adw-fonts.spec.ts +188 -0
- package/src/adw-header-bar.spec.ts +92 -0
- package/src/adw-icon.spec.ts +17 -8
- package/src/adw-row-state.spec.ts +11 -1
- package/src/adw-tab-view.spec.ts +151 -0
- package/src/bottom-sheet.spec.ts +87 -0
- package/src/breakpoints.ts +18 -4
- package/src/chrome.spec.ts +233 -0
- 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-carousel.ts +121 -4
- 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 +70 -7
- 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 +28 -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 +30 -1
- package/src/keyboard-operable.spec.ts +755 -0
- package/src/scroll-shading.ts +255 -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
- package/src/view-switcher.spec.ts +149 -0
|
@@ -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';
|
|
@@ -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
|
}
|