@gjsify/adwaita-web 0.31.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adwaita-web.css +835 -299
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/adw-about-dialog.spec.d.ts +1 -0
- package/lib/types/adw-action-rows.spec.d.ts +1 -0
- package/lib/types/adw-avatar.spec.d.ts +1 -0
- package/lib/types/adw-banner.spec.d.ts +1 -0
- package/lib/types/adw-button-content.spec.d.ts +1 -0
- package/lib/types/adw-carousel.spec.d.ts +1 -0
- package/lib/types/adw-checks.spec.d.ts +1 -0
- package/lib/types/adw-header-bar.spec.d.ts +1 -0
- package/lib/types/adw-icon.spec.d.ts +1 -0
- package/lib/types/adw-progress-bar.spec.d.ts +1 -0
- package/lib/types/adw-sidebar.spec.d.ts +1 -0
- package/lib/types/adw-switch.spec.d.ts +1 -0
- package/lib/types/adw-wrap-box.spec.d.ts +1 -0
- package/lib/types/bottom-sheet.spec.d.ts +1 -0
- package/lib/types/breakpoints.d.ts +22 -0
- package/lib/types/breakpoints.spec.d.ts +1 -0
- package/lib/types/chrome.spec.d.ts +1 -0
- package/lib/types/elements/adw-about-dialog.d.ts +47 -8
- package/lib/types/elements/adw-action-row.d.ts +35 -0
- package/lib/types/elements/adw-avatar.d.ts +11 -0
- package/lib/types/elements/adw-bottom-sheet.d.ts +33 -4
- package/lib/types/elements/adw-button-content.d.ts +12 -0
- package/lib/types/elements/adw-button-row.d.ts +10 -1
- package/lib/types/elements/adw-carousel.d.ts +111 -8
- package/lib/types/elements/adw-checks.d.ts +57 -0
- package/lib/types/elements/adw-clamp.d.ts +4 -1
- package/lib/types/elements/adw-combo-row.d.ts +10 -0
- package/lib/types/elements/adw-data-grid.d.ts +11 -31
- package/lib/types/elements/adw-drop-down.d.ts +17 -20
- package/lib/types/elements/adw-entry-row.d.ts +93 -3
- package/lib/types/elements/adw-expander-row.d.ts +7 -1
- package/lib/types/elements/adw-header-bar.d.ts +15 -0
- package/lib/types/elements/adw-icon.d.ts +25 -0
- package/lib/types/elements/adw-inline-view-switcher.d.ts +73 -11
- package/lib/types/elements/adw-menu-button.d.ts +6 -6
- package/lib/types/elements/adw-navigation-split-view.d.ts +48 -0
- package/lib/types/elements/adw-navigation-view.d.ts +63 -20
- package/lib/types/elements/adw-overlay-split-view.d.ts +94 -0
- package/lib/types/elements/adw-password-entry-row.d.ts +19 -10
- package/lib/types/elements/adw-popover.d.ts +58 -0
- package/lib/types/elements/adw-preferences-dialog.d.ts +57 -2
- package/lib/types/elements/adw-preferences-group.d.ts +31 -0
- package/lib/types/elements/adw-progress-bar.d.ts +38 -0
- package/lib/types/elements/adw-sidebar.d.ts +37 -5
- package/lib/types/elements/adw-spinner.d.ts +23 -0
- package/lib/types/elements/adw-split-button.d.ts +53 -7
- package/lib/types/elements/adw-switch-row.d.ts +11 -2
- package/lib/types/elements/adw-switch.d.ts +15 -0
- package/lib/types/elements/adw-tab-view.d.ts +129 -12
- package/lib/types/elements/adw-toolbar-view.d.ts +5 -0
- package/lib/types/elements/adw-view-stack.d.ts +39 -19
- package/lib/types/elements/adw-view-switcher-bar.d.ts +25 -7
- package/lib/types/elements/adw-view-switcher.d.ts +76 -13
- package/lib/types/elements/adw-window-title.d.ts +3 -1
- package/lib/types/elements/adw-wrap-box.d.ts +58 -3
- package/lib/types/elements/view-switcher-dom.d.ts +44 -0
- package/lib/types/entry-rows.spec.d.ts +1 -0
- package/lib/types/index.d.ts +7 -0
- package/lib/types/navigation-view.spec.d.ts +1 -0
- package/lib/types/preferences-search.d.ts +20 -0
- package/lib/types/preferences.spec.d.ts +1 -0
- package/lib/types/split-button.spec.d.ts +1 -0
- package/lib/types/split-views.spec.d.ts +1 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/lib/types/tab-view.spec.d.ts +1 -0
- package/lib/types/view-stack.spec.d.ts +1 -0
- package/lib/types/view-switcher.spec.d.ts +1 -0
- package/package.json +6 -6
- package/scss/_banner.scss +13 -0
- package/scss/_bottom_sheet.scss +5 -6
- package/scss/_button.scss +3 -26
- package/scss/_button_content.scss +28 -0
- package/scss/_checks.scss +228 -0
- package/scss/_clamp.scss +9 -3
- package/scss/_combo_row.scss +19 -0
- package/scss/_data_grid.scss +15 -7
- package/scss/_drop_down.scss +12 -55
- package/scss/_expander_row.scss +5 -50
- package/scss/_icon.scss +51 -0
- package/scss/_icons.generated.scss +6 -0
- package/scss/_inline_view_switcher.scss +34 -0
- package/scss/_labels.scss +135 -0
- package/scss/_menu_button.scss +10 -70
- package/scss/_navigation_split_view.scss +47 -8
- package/scss/_overlay_split_view.scss +117 -32
- package/scss/_popover.scss +186 -0
- package/scss/_preferences.scss +29 -0
- package/scss/_preferences_dialog.scss +17 -0
- package/scss/_progress_bar.scss +161 -0
- package/scss/_reset.scss +18 -7
- package/scss/_spinner.scss +37 -19
- package/scss/_split_button.scss +6 -50
- package/scss/_switch.scss +90 -0
- package/scss/_switch_row.scss +6 -52
- package/scss/_tab_view.scss +63 -0
- package/scss/_theme.scss +10 -0
- package/scss/_toolbar_view.scss +74 -0
- package/scss/_variables.scss +34 -0
- package/scss/_view_switcher.scss +40 -0
- package/scss/_view_switcher_bar.scss +37 -0
- package/scss/_wrap_box.scss +65 -6
- package/scss/adwaita-skin.scss +23 -0
- package/src/adw-about-dialog.spec.ts +350 -0
- package/src/adw-action-rows.spec.ts +342 -0
- package/src/adw-avatar.spec.ts +176 -0
- package/src/adw-banner.spec.ts +152 -0
- package/src/adw-button-content.spec.ts +207 -0
- package/src/adw-button-row.spec.ts +12 -6
- package/src/adw-carousel.spec.ts +389 -0
- package/src/adw-checks.spec.ts +368 -0
- package/src/adw-drop-down.spec.ts +123 -0
- package/src/adw-header-bar.spec.ts +83 -0
- package/src/adw-icon.spec.ts +167 -0
- package/src/adw-progress-bar.spec.ts +173 -0
- package/src/adw-row-state.spec.ts +49 -0
- package/src/adw-sidebar.spec.ts +332 -0
- package/src/adw-switch.spec.ts +242 -0
- package/src/adw-tab-view.spec.ts +12 -3
- package/src/adw-view-switcher-bar.spec.ts +61 -13
- package/src/adw-wrap-box.spec.ts +341 -0
- package/src/bottom-sheet.spec.ts +325 -0
- package/src/breakpoints.spec.ts +147 -0
- package/src/breakpoints.ts +67 -0
- package/src/chrome.spec.ts +400 -0
- package/src/elements/adw-about-dialog.ts +223 -163
- package/src/elements/adw-action-row.ts +131 -11
- package/src/elements/adw-avatar.ts +48 -54
- package/src/elements/adw-banner.ts +46 -10
- package/src/elements/adw-bottom-sheet.ts +145 -60
- package/src/elements/adw-button-content.ts +107 -33
- package/src/elements/adw-button-row.ts +61 -37
- package/src/elements/adw-button.ts +5 -7
- package/src/elements/adw-carousel.ts +467 -105
- package/src/elements/adw-checks.ts +299 -0
- package/src/elements/adw-clamp.ts +80 -10
- package/src/elements/adw-combo-row.ts +32 -7
- package/src/elements/adw-data-grid.ts +43 -91
- package/src/elements/adw-drop-down.ts +117 -128
- package/src/elements/adw-entry-row.ts +404 -36
- package/src/elements/adw-expander-row.ts +61 -29
- package/src/elements/adw-header-bar.ts +31 -6
- package/src/elements/adw-icon.ts +148 -0
- package/src/elements/adw-inline-view-switcher.ts +325 -131
- package/src/elements/adw-menu-button.ts +93 -70
- package/src/elements/adw-navigation-split-view.ts +220 -14
- package/src/elements/adw-navigation-view.ts +286 -148
- package/src/elements/adw-overlay-split-view.ts +536 -28
- package/src/elements/adw-password-entry-row.ts +111 -89
- package/src/elements/adw-popover.ts +283 -0
- package/src/elements/adw-preferences-dialog.ts +120 -21
- package/src/elements/adw-preferences-group.ts +128 -22
- package/src/elements/adw-progress-bar.ts +189 -0
- package/src/elements/adw-sidebar.ts +301 -142
- package/src/elements/adw-spin-row.ts +11 -5
- package/src/elements/adw-spinner.ts +168 -9
- package/src/elements/adw-split-button.ts +371 -92
- package/src/elements/adw-status-page.ts +9 -9
- package/src/elements/adw-switch-row.ts +100 -30
- package/src/elements/adw-switch.ts +129 -0
- package/src/elements/adw-tab-view.ts +700 -129
- package/src/elements/adw-toggle-group.ts +5 -7
- package/src/elements/adw-toolbar-view.ts +84 -21
- package/src/elements/adw-view-stack.ts +183 -85
- package/src/elements/adw-view-switcher-bar.ts +171 -81
- package/src/elements/adw-view-switcher.ts +328 -117
- package/src/elements/adw-window-title.ts +44 -11
- package/src/elements/adw-wrap-box.ts +329 -78
- package/src/elements/view-switcher-dom.ts +108 -0
- package/src/entry-rows.spec.ts +391 -0
- package/src/index.ts +23 -0
- package/src/navigation-view.spec.ts +435 -0
- package/src/preferences-search.ts +135 -0
- package/src/preferences.spec.ts +250 -0
- package/src/source-view/adw-source-view.ts +2 -5
- package/src/split-button.spec.ts +470 -0
- package/src/split-views.spec.ts +893 -0
- package/src/styles.generated.ts +1 -1
- package/src/tab-view.spec.ts +451 -0
- package/src/test.browser.mts +49 -0
- package/src/view-stack.spec.ts +272 -0
- package/src/view-switcher.spec.ts +665 -0
|
@@ -1,90 +1,210 @@
|
|
|
1
1
|
// <adw-view-switcher> — An adaptive, tab-style switcher driving a set of named
|
|
2
|
-
// pages
|
|
3
|
-
// Pages are declared as <adw-view-switcher-page>
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
2
|
+
// pages: the web counterpart of Adw.ViewSwitcher, bundled with the Adw.ViewStack
|
|
3
|
+
// libadwaita keeps separate. Pages are declared as <adw-view-switcher-page>
|
|
4
|
+
// children; their own children become the page body.
|
|
5
|
+
//
|
|
6
|
+
// Everything that decides WHAT to show is HEADLESS and lives in
|
|
7
|
+
// `@gjsify/adwaita-core` (ADR 0004): the button-visibility rule, the
|
|
8
|
+
// `image-missing` icon fallback, the mnemonic-stripped label, the badge label
|
|
9
|
+
// and its screen-reader description, and the SELECTION — which is
|
|
10
|
+
// `ViewSwitcherState` over the wave-1 `ViewStackState`, so this element and the
|
|
11
|
+
// NativeScript twin share one set of guards and one set of conformance vectors
|
|
12
|
+
// (`@gjsify/adwaita-core/conformance`). This file is the DOM half only.
|
|
13
|
+
//
|
|
14
|
+
// What the lift fixed here, all of it C-derived:
|
|
15
|
+
// - a page with neither title nor icon rendered an empty, clickable,
|
|
16
|
+
// space-consuming tab; libadwaita hides the button outright
|
|
17
|
+
// (adw-view-switcher.c:178). An EMPTY title is NOT that case — it keeps its
|
|
18
|
+
// button, which is why `getAttribute` is no longer flattened with `?? ''`.
|
|
19
|
+
// - a page with no icon rendered no icon at all; libadwaita substitutes
|
|
20
|
+
// `image-missing` (adw-view-switcher-button.c:399-405).
|
|
21
|
+
// - `active="-1"` / `active="99"` were CLAMPED into range, so an out-of-range
|
|
22
|
+
// request silently switched pages; `adw_view_stack_pages_select_item`
|
|
23
|
+
// refuses (adw-view-stack.c:682-684).
|
|
24
|
+
// - `notify::visible-child` fired only for a click, never for a property or
|
|
25
|
+
// attribute set; C notifies on every path (adw-view-stack.c:1038-1039).
|
|
26
|
+
// - `notify::policy` was documented in this header and never dispatched.
|
|
27
|
+
// - the declared pages were harvested with an UNSCOPED querySelectorAll, so a
|
|
28
|
+
// nested switcher inside a page had its pages stolen by the outer one.
|
|
29
|
+
// - the 500 ms drag-hover auto-switch did not exist
|
|
30
|
+
// (adw-view-switcher-button.c:14, :58-96).
|
|
31
|
+
//
|
|
10
32
|
// Attributes:
|
|
11
|
-
// policy — 'wide' | 'narrow' (default 'narrow'
|
|
12
|
-
//
|
|
33
|
+
// policy — 'wide' | 'narrow' (default 'narrow'). An unrecognised value is
|
|
34
|
+
// REJECTED and leaves the current policy alone, as `g_return_if_fail` does;
|
|
35
|
+
// removing the attribute restores the default.
|
|
36
|
+
// active — zero-based index of the selected PAGE. Out-of-range, negative and
|
|
37
|
+
// fractional values are refused and the attribute is reflected back to the
|
|
38
|
+
// selection that actually holds.
|
|
39
|
+
// Page attributes: name, title, icon-name, hidden (= AdwViewStackPage:visible),
|
|
40
|
+
// use-underline, badge-number, needs-attention.
|
|
13
41
|
// Events:
|
|
14
|
-
// `notify::visible-child` (CustomEvent, bubbles,
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
42
|
+
// `notify::visible-child` (CustomEvent, bubbles,
|
|
43
|
+
// detail = { index, name, title, interactive }) on every selection change,
|
|
44
|
+
// including the auto-pick; `interactive` is false for a model-driven one.
|
|
45
|
+
// `notify::policy` (CustomEvent, bubbles, detail = { policy }).
|
|
18
46
|
// Reference: refs/libadwaita/src/adw-view-switcher.c (AdwViewSwitcher behaviour)
|
|
47
|
+
// Reference: refs/libadwaita/src/adw-view-switcher-button.c (AdwViewSwitcherButton)
|
|
19
48
|
// Reference: refs/libadwaita/src/stylesheet/widgets/_view-switcher.scss
|
|
20
|
-
// Reference:
|
|
49
|
+
// Reference: packages/nativescript-bridge/adwaita/src/widgets/view-switcher-model.ts (NS twin)
|
|
21
50
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
22
51
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
23
52
|
|
|
24
|
-
|
|
53
|
+
import {
|
|
54
|
+
VIEW_SWITCHER_POLICIES,
|
|
55
|
+
ViewSwitcherState,
|
|
56
|
+
buildViewSwitcherButtons,
|
|
57
|
+
isViewSwitcherPolicy,
|
|
58
|
+
} from '@gjsify/adwaita-core';
|
|
59
|
+
import type { ViewSwitcherScheduler } from '@gjsify/adwaita-core';
|
|
60
|
+
// Aliased because `AdwViewSwitcherPage` is already this module's page ELEMENT —
|
|
61
|
+
// the custom element the package has always exported under that name.
|
|
62
|
+
import type {
|
|
63
|
+
AdwViewSwitcherPage as AdwViewSwitcherPageInfo,
|
|
64
|
+
AdwViewSwitcherPolicy,
|
|
65
|
+
ViewSwitcherStateChange,
|
|
66
|
+
} from '@gjsify/adwaita-core';
|
|
25
67
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
68
|
+
import {
|
|
69
|
+
applyDescription,
|
|
70
|
+
applyIndicator,
|
|
71
|
+
applySwitcherIcon,
|
|
72
|
+
domViewSwitcherScheduler,
|
|
73
|
+
readSwitcherPage,
|
|
74
|
+
} from './view-switcher-dom.js';
|
|
75
|
+
import { type AdwIcon, createAdwIcon } from './adw-icon.js';
|
|
76
|
+
|
|
77
|
+
/** The DOM nodes one page owns — the switcher paints the derived model onto them. */
|
|
78
|
+
interface PageNodes {
|
|
31
79
|
button: HTMLButtonElement;
|
|
80
|
+
icon: AdwIcon;
|
|
81
|
+
label: HTMLSpanElement;
|
|
82
|
+
indicator: HTMLSpanElement;
|
|
83
|
+
body: HTMLDivElement;
|
|
32
84
|
}
|
|
33
85
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
86
|
+
/**
|
|
87
|
+
* A single page. Children of `<adw-view-switcher>`; consumed at connect time.
|
|
88
|
+
*
|
|
89
|
+
* Deliberately carries NO `observedAttributes`. It used to declare six of them
|
|
90
|
+
* and no `attributeChangedCallback`, which is a dead declaration — the browser
|
|
91
|
+
* computes the list and has nowhere to deliver to — so changing `badge-number`,
|
|
92
|
+
* `title`, `icon-name` or `needs-attention` after connect did nothing at all,
|
|
93
|
+
* where C rebinds on every page `notify` (adw-view-switcher.c:184-193, :223).
|
|
94
|
+
*
|
|
95
|
+
* A callback could not fix it either: the switcher consumes these elements at
|
|
96
|
+
* connect and `replaceChildren`s them away, so a detached page has no `closest`
|
|
97
|
+
* to reach its owner through. The switcher watches them with a `MutationObserver`
|
|
98
|
+
* instead, which keeps working after the detach and needs nothing from this class.
|
|
99
|
+
*/
|
|
100
|
+
export class AdwViewSwitcherPage extends HTMLElement {}
|
|
40
101
|
|
|
41
102
|
export class AdwViewSwitcher extends HTMLElement {
|
|
103
|
+
/**
|
|
104
|
+
* The dwell timer, injectable.
|
|
105
|
+
*
|
|
106
|
+
* It used to be `new ViewSwitcherState({ scheduler: domViewSwitcherScheduler })`
|
|
107
|
+
* inline, which left NOWHERE to hand a clock in — so
|
|
108
|
+
* `VIEW_SWITCHER_DRAG_VECTORS` was driven by the core suite alone, and the
|
|
109
|
+
* conformance header claimed all three suites drove it. `scheduler` is a
|
|
110
|
+
* property so a test can replace it before the element connects; the DOM one
|
|
111
|
+
* is the default, exactly as before.
|
|
112
|
+
*/
|
|
113
|
+
scheduler: ViewSwitcherScheduler = domViewSwitcherScheduler;
|
|
114
|
+
private _stateInstance: ViewSwitcherState | null = null;
|
|
115
|
+
|
|
116
|
+
/** Built on first access, so a `scheduler` set before connect is the one used. */
|
|
117
|
+
private get _state(): ViewSwitcherState {
|
|
118
|
+
if (!this._stateInstance) {
|
|
119
|
+
this._stateInstance = new ViewSwitcherState({ scheduler: this.scheduler });
|
|
120
|
+
this._stateInstance.subscribe((change) => this._onStateChange(change));
|
|
121
|
+
}
|
|
122
|
+
return this._stateInstance;
|
|
123
|
+
}
|
|
42
124
|
private _barEl!: HTMLDivElement;
|
|
43
125
|
private _contentEl!: HTMLDivElement;
|
|
44
|
-
private
|
|
45
|
-
|
|
46
|
-
private
|
|
126
|
+
private _nodes: PageNodes[] = [];
|
|
127
|
+
/** The page ELEMENTS, kept because the switcher detaches them from the tree. */
|
|
128
|
+
private _declared: HTMLElement[] = [];
|
|
129
|
+
private _pageObserver: MutationObserver | undefined;
|
|
130
|
+
private _policy: AdwViewSwitcherPolicy = 'narrow';
|
|
47
131
|
private _initialized = false;
|
|
48
132
|
|
|
49
133
|
static get observedAttributes() {
|
|
50
134
|
return ['policy', 'active'];
|
|
51
135
|
}
|
|
52
136
|
|
|
137
|
+
constructor() {
|
|
138
|
+
super();
|
|
139
|
+
// Subscribing touches no attributes and no children, so it is legal in a
|
|
140
|
+
// custom element constructor — unlike anything DOM-shaped.
|
|
141
|
+
}
|
|
142
|
+
|
|
53
143
|
/** The policy controlling the button layout ('wide' | 'narrow'). */
|
|
54
|
-
get policy():
|
|
144
|
+
get policy(): AdwViewSwitcherPolicy {
|
|
55
145
|
return this._policy;
|
|
56
146
|
}
|
|
57
147
|
|
|
58
|
-
set policy(value:
|
|
148
|
+
set policy(value: AdwViewSwitcherPolicy) {
|
|
59
149
|
this.setAttribute('policy', value);
|
|
60
150
|
}
|
|
61
151
|
|
|
62
|
-
/** Zero-based index of the
|
|
152
|
+
/** Zero-based index of the selected page, `-1` when nothing is selected. */
|
|
63
153
|
get active(): number {
|
|
64
|
-
return this.
|
|
154
|
+
return this._state.selected;
|
|
65
155
|
}
|
|
66
156
|
|
|
67
157
|
set active(value: number) {
|
|
68
|
-
this.
|
|
158
|
+
this._state.setSelected(value);
|
|
159
|
+
this._reflectActive();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Name of the selected page, `''` when nothing is selected. */
|
|
163
|
+
get visibleChildName(): string {
|
|
164
|
+
return this._state.selectedName;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
set visibleChildName(name: string | null | undefined) {
|
|
168
|
+
this._state.selectName(name);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The resolved page records a bound layout can read. */
|
|
172
|
+
get pages(): readonly AdwViewSwitcherPageInfo[] {
|
|
173
|
+
return this._state.pages;
|
|
69
174
|
}
|
|
70
175
|
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Show or hide a page (`AdwViewStackPage:visible`). Returns whether the
|
|
178
|
+
* SELECTION moved — hiding the selected page falls back to the first still
|
|
179
|
+
* visible one (adw-view-stack.c:1061-1082).
|
|
180
|
+
*/
|
|
181
|
+
setPageVisible(name: string, visible: boolean): boolean {
|
|
182
|
+
const moved = this._state.setPageVisible(name, visible);
|
|
183
|
+
this._render();
|
|
184
|
+
return moved;
|
|
74
185
|
}
|
|
75
186
|
|
|
76
187
|
connectedCallback() {
|
|
77
|
-
if (this._initialized)
|
|
78
|
-
|
|
188
|
+
if (this._initialized) {
|
|
189
|
+
// Re-entering a document. `disconnectedCallback` drops the page
|
|
190
|
+
// observer on the way out, and this early return used to skip
|
|
191
|
+
// rebuilding it — so a switcher that had merely been MOVED (a
|
|
192
|
+
// slideshow slide, a client-side route change) stopped noticing
|
|
193
|
+
// page attribute changes for good, and did it silently.
|
|
194
|
+
this._watchPages();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
79
197
|
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
const
|
|
198
|
+
// `:scope >` rather than a subtree scan: an unscoped query let an outer
|
|
199
|
+
// switcher adopt the pages of a nested one, since both use the same tag.
|
|
200
|
+
// Pages come from the stack's OWN children in C (adw-view-switcher.c:203).
|
|
201
|
+
const declared = Array.from(this.querySelectorAll(':scope > adw-view-switcher-page')) as HTMLElement[];
|
|
202
|
+
// Read the declared selection BEFORE adopting any page: the auto-pick
|
|
203
|
+
// reflects its own index back into this very attribute.
|
|
204
|
+
const declaredActive = this.getAttribute('active');
|
|
84
205
|
|
|
85
|
-
// The single `viewswitcher` node holds the segmented button bar; a
|
|
86
|
-
// content area shows the
|
|
87
|
-
// Adw.ViewStack the switcher would otherwise drive).
|
|
206
|
+
// The single `viewswitcher` node holds the segmented button bar; a
|
|
207
|
+
// sibling content area shows the selected page.
|
|
88
208
|
this._barEl = document.createElement('div');
|
|
89
209
|
this._barEl.className = 'adw-view-switcher-bar';
|
|
90
210
|
this._barEl.setAttribute('role', 'tablist');
|
|
@@ -92,107 +212,198 @@ export class AdwViewSwitcher extends HTMLElement {
|
|
|
92
212
|
this._contentEl = document.createElement('div');
|
|
93
213
|
this._contentEl.className = 'adw-view-switcher-content';
|
|
94
214
|
|
|
95
|
-
this.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
button.type = 'button';
|
|
102
|
-
button.className = 'adw-view-switcher-button';
|
|
103
|
-
button.setAttribute('role', 'tab');
|
|
104
|
-
|
|
105
|
-
const iconEl = document.createElement('span');
|
|
106
|
-
iconEl.className = `adw-icon${icon ? ` adw-icon--${icon}` : ''}`;
|
|
107
|
-
iconEl.setAttribute('aria-hidden', 'true'); // decorative (mask-image only)
|
|
108
|
-
iconEl.hidden = icon.length === 0;
|
|
109
|
-
|
|
110
|
-
const labelEl = document.createElement('span');
|
|
111
|
-
labelEl.className = 'adw-view-switcher-label';
|
|
112
|
-
labelEl.textContent = title;
|
|
113
|
-
labelEl.hidden = title.length === 0;
|
|
114
|
-
|
|
115
|
-
button.append(iconEl, labelEl);
|
|
116
|
-
// An icon-only tab has no visible text — give it an accessible name.
|
|
117
|
-
if (icon && !title) button.setAttribute('aria-label', icon);
|
|
118
|
-
|
|
119
|
-
button.addEventListener('click', () => this._selectIndex(index));
|
|
215
|
+
this._declared = declared;
|
|
216
|
+
this._watchPages();
|
|
217
|
+
this._nodes = declared.map((pageEl, index) => this._buildPage(pageEl, index));
|
|
218
|
+
this._barEl.append(...this._nodes.map((nodes) => nodes.button));
|
|
219
|
+
this._contentEl.append(...this._nodes.map((nodes) => nodes.body));
|
|
220
|
+
this.replaceChildren(this._barEl, this._contentEl);
|
|
120
221
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
body.dataset.name = name;
|
|
124
|
-
for (const child of Array.from(pageEl.childNodes)) body.appendChild(child);
|
|
222
|
+
this._policy = this._readPolicyAttr();
|
|
223
|
+
this._initialized = true;
|
|
125
224
|
|
|
126
|
-
|
|
127
|
-
|
|
225
|
+
this._state.setPages(declared.map((pageEl) => readSwitcherPage(pageEl)));
|
|
226
|
+
if (declaredActive !== null) {
|
|
227
|
+
const index = Number.parseInt(declaredActive, 10);
|
|
228
|
+
// A declared selection is not a user activation.
|
|
229
|
+
if (!Number.isNaN(index)) this._state.setSelected(index, false);
|
|
230
|
+
}
|
|
231
|
+
this._render();
|
|
232
|
+
}
|
|
128
233
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
234
|
+
disconnectedCallback() {
|
|
235
|
+
// A drag that leaves with the element would otherwise switch pages after
|
|
236
|
+
// the widget is gone; C drops the source in dispose the same way.
|
|
237
|
+
this._state.cancelDrag();
|
|
238
|
+
this._pageObserver?.disconnect();
|
|
239
|
+
this._pageObserver = undefined;
|
|
240
|
+
}
|
|
132
241
|
|
|
133
|
-
|
|
134
|
-
|
|
242
|
+
/**
|
|
243
|
+
* Re-read every declared page and push it into the state.
|
|
244
|
+
*
|
|
245
|
+
* Called by `AdwViewSwitcherPage.attributeChangedCallback`, which is what
|
|
246
|
+
* makes a runtime `badge-number` change reach the button. `setPages` keeps
|
|
247
|
+
* the selection where it can, so this is a repaint and not a reset.
|
|
248
|
+
*/
|
|
249
|
+
refreshPages(): void {
|
|
250
|
+
if (!this._initialized) return;
|
|
251
|
+
this._state.setPages(this._declared.map((pageEl) => readSwitcherPage(pageEl)));
|
|
135
252
|
this._render();
|
|
136
253
|
}
|
|
137
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Watch the declared pages for attribute changes.
|
|
257
|
+
*
|
|
258
|
+
* A `MutationObserver` rather than an `attributeChangedCallback` on the page:
|
|
259
|
+
* the switcher consumes those elements at connect and `replaceChildren`s them
|
|
260
|
+
* out of the tree, so a detached page cannot find its owner — but an observer
|
|
261
|
+
* bound to it keeps firing. This is the DOM's `notify::` on
|
|
262
|
+
* `AdwViewStackPage`, which the C's `update_button` handler binds per page
|
|
263
|
+
* (adw-view-switcher.c:184-193).
|
|
264
|
+
*
|
|
265
|
+
* It delivers on a MICROTASK where GTK's notify is synchronous. That is the
|
|
266
|
+
* DOM's contract for watching a node, it is invisible to a user, and nothing
|
|
267
|
+
* in this element reads the button back inside the setter.
|
|
268
|
+
*/
|
|
269
|
+
private _watchPages(): void {
|
|
270
|
+
this._pageObserver?.disconnect();
|
|
271
|
+
this._pageObserver = new MutationObserver(() => this.refreshPages());
|
|
272
|
+
for (const pageEl of this._declared) this._pageObserver.observe(pageEl, { attributes: true });
|
|
273
|
+
}
|
|
274
|
+
|
|
138
275
|
attributeChangedCallback(name: string) {
|
|
139
276
|
if (!this._initialized) return;
|
|
140
277
|
if (name === 'policy') {
|
|
141
278
|
const next = this._readPolicyAttr();
|
|
142
|
-
if (next
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
279
|
+
if (next === this._policy) return;
|
|
280
|
+
this._policy = next;
|
|
281
|
+
this._render();
|
|
282
|
+
this.dispatchEvent(new CustomEvent('notify::policy', { bubbles: true, detail: { policy: next } }));
|
|
146
283
|
return;
|
|
147
284
|
}
|
|
148
285
|
if (name === 'active') {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
286
|
+
const raw = this.getAttribute('active');
|
|
287
|
+
const index = raw === null ? Number.NaN : Number.parseInt(raw, 10);
|
|
288
|
+
if (!Number.isNaN(index)) this._state.setSelected(index);
|
|
289
|
+
// A refused request must not leave the attribute claiming something
|
|
290
|
+
// the widget is not showing; re-reflecting is what makes "refused,
|
|
291
|
+
// not clamped" observable in the DOM.
|
|
292
|
+
this._reflectActive();
|
|
154
293
|
}
|
|
155
294
|
}
|
|
156
295
|
|
|
157
|
-
private
|
|
158
|
-
|
|
296
|
+
private _buildPage(pageEl: HTMLElement, index: number): PageNodes {
|
|
297
|
+
const button = document.createElement('button');
|
|
298
|
+
button.type = 'button';
|
|
299
|
+
button.className = 'adw-view-switcher-button';
|
|
300
|
+
button.setAttribute('role', 'tab');
|
|
301
|
+
|
|
302
|
+
// Both children always exist, exactly as AdwViewSwitcherButton's template
|
|
303
|
+
// does — the icon carries the `image-missing` fallback rather than
|
|
304
|
+
// disappearing (adw-view-switcher-button.ui).
|
|
305
|
+
const icon = createAdwIcon(null, 'adw-view-switcher-icon');
|
|
306
|
+
|
|
307
|
+
const label = document.createElement('span');
|
|
308
|
+
label.className = 'adw-view-switcher-label';
|
|
309
|
+
|
|
310
|
+
const indicator = document.createElement('span');
|
|
311
|
+
indicator.className = 'adw-view-switcher-indicator';
|
|
312
|
+
indicator.setAttribute('aria-hidden', 'true'); // announced via aria-description
|
|
313
|
+
|
|
314
|
+
button.append(icon, label, indicator);
|
|
315
|
+
button.addEventListener('click', () => {
|
|
316
|
+
this._state.setSelected(index);
|
|
317
|
+
});
|
|
318
|
+
// GtkDropControllerMotion's enter/leave, which drive TIMEOUT_EXPAND.
|
|
319
|
+
//
|
|
320
|
+
// The `relatedTarget` test is the whole point: this button contains an
|
|
321
|
+
// icon and a label, and the DOM fires `dragleave` + `dragenter` when the
|
|
322
|
+
// pointer crosses BETWEEN them. `ViewSwitcherDragSwitch.leave` clears the
|
|
323
|
+
// dwell timer and `enter` re-arms a fresh 500ms, so a drag that merely
|
|
324
|
+
// slid from the icon to the label restarted the countdown and the page
|
|
325
|
+
// never switched. C has one `GtkDropControllerMotion` per BUTTON
|
|
326
|
+
// (adw-view-switcher-button.c:79-96), which has no such internal edges —
|
|
327
|
+
// `relatedTarget` containment is that same "did we really leave" test.
|
|
328
|
+
button.addEventListener('dragenter', (event) => {
|
|
329
|
+
if (button.contains((event as DragEvent).relatedTarget as Node | null)) return;
|
|
330
|
+
this._state.dragEnter(index);
|
|
331
|
+
});
|
|
332
|
+
button.addEventListener('dragleave', (event) => {
|
|
333
|
+
if (button.contains((event as DragEvent).relatedTarget as Node | null)) return;
|
|
334
|
+
this._state.dragLeave(index);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const body = document.createElement('div');
|
|
338
|
+
body.className = 'adw-view-switcher-page';
|
|
339
|
+
body.dataset.name = pageEl.getAttribute('name') ?? '';
|
|
340
|
+
for (const child of Array.from(pageEl.childNodes)) body.appendChild(child);
|
|
341
|
+
|
|
342
|
+
return { button, icon, label, indicator, body };
|
|
159
343
|
}
|
|
160
344
|
|
|
161
|
-
private
|
|
162
|
-
const raw =
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
345
|
+
private _readPolicyAttr(): AdwViewSwitcherPolicy {
|
|
346
|
+
const raw = this.getAttribute('policy');
|
|
347
|
+
// A removed attribute falls back to the C default; an unrecognised value
|
|
348
|
+
// is refused and keeps the current policy (adw_view_switcher_set_policy
|
|
349
|
+
// is reached only through the enum, which rejects garbage).
|
|
350
|
+
if (raw === null) return 'narrow';
|
|
351
|
+
return isViewSwitcherPolicy(raw) ? raw : this._policy;
|
|
166
352
|
}
|
|
167
353
|
|
|
168
|
-
private
|
|
169
|
-
if (index === this._active) return;
|
|
170
|
-
this._active = index;
|
|
171
|
-
// Keep the attribute in sync without re-entering the guarded callback
|
|
172
|
-
// (value already matches, so attributeChangedCallback no-ops).
|
|
173
|
-
this.setAttribute('active', String(index));
|
|
354
|
+
private _onStateChange(change: ViewSwitcherStateChange): void {
|
|
174
355
|
this._render();
|
|
175
|
-
const page = this._pages[index];
|
|
176
356
|
this.dispatchEvent(
|
|
177
357
|
new CustomEvent('notify::visible-child', {
|
|
178
358
|
bubbles: true,
|
|
179
|
-
detail: {
|
|
359
|
+
detail: {
|
|
360
|
+
index: change.selected,
|
|
361
|
+
name: change.name,
|
|
362
|
+
title: change.title,
|
|
363
|
+
interactive: change.interactive,
|
|
364
|
+
},
|
|
180
365
|
}),
|
|
181
366
|
);
|
|
182
367
|
}
|
|
183
368
|
|
|
184
369
|
private _render(): void {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
this.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
370
|
+
if (!this._initialized) return;
|
|
371
|
+
|
|
372
|
+
// The single `viewswitcher` node carries the policy style class
|
|
373
|
+
// (adw-view-switcher.c:539-545).
|
|
374
|
+
for (const policy of VIEW_SWITCHER_POLICIES) this.classList.toggle(policy, policy === this._policy);
|
|
375
|
+
|
|
376
|
+
const models = buildViewSwitcherButtons(this._state.pages, this._state.selected, this._policy);
|
|
377
|
+
models.forEach((model, index) => {
|
|
378
|
+
const nodes = this._nodes[index];
|
|
379
|
+
if (!nodes) return;
|
|
380
|
+
|
|
381
|
+
nodes.button.hidden = !model.visible;
|
|
382
|
+
nodes.button.classList.toggle('active', model.selected);
|
|
383
|
+
nodes.button.setAttribute('aria-selected', String(model.selected));
|
|
384
|
+
nodes.button.tabIndex = model.selected ? 0 : -1;
|
|
385
|
+
|
|
386
|
+
applySwitcherIcon(nodes.icon, model.iconName);
|
|
387
|
+
nodes.label.textContent = model.label;
|
|
388
|
+
nodes.label.hidden = model.label.length === 0;
|
|
389
|
+
applyIndicator(nodes.indicator, model.badgeLabel, model.needsAttention);
|
|
390
|
+
applyDescription(nodes.button, model.description);
|
|
391
|
+
|
|
392
|
+
// A tab with no visible text still needs an accessible name; the
|
|
393
|
+
// icon name is the only thing left to build one from.
|
|
394
|
+
if (model.label) nodes.button.removeAttribute('aria-label');
|
|
395
|
+
else nodes.button.setAttribute('aria-label', model.iconName);
|
|
396
|
+
|
|
397
|
+
nodes.body.classList.toggle('active-view', model.selected);
|
|
398
|
+
nodes.body.hidden = !model.selected;
|
|
195
399
|
});
|
|
400
|
+
|
|
401
|
+
this._reflectActive();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private _reflectActive(): void {
|
|
405
|
+
const current = String(this._state.selected);
|
|
406
|
+
if (this.getAttribute('active') !== current) this.setAttribute('active', current);
|
|
196
407
|
}
|
|
197
408
|
}
|
|
198
409
|
|
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
// <adw-window-title> — A composite title + subtitle for a header bar.
|
|
2
2
|
// Attributes: title, subtitle.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
3
|
+
// Events: notify::title / notify::subtitle (CustomEvent, mirroring the GObject
|
|
4
|
+
// notify names Adw.WindowTitle emits).
|
|
5
|
+
//
|
|
6
|
+
// The two labels, their visibility and the change-detection are HEADLESS and
|
|
7
|
+
// live in `@gjsify/adwaita-core` (ADR 0004) as {@link WindowTitleState}; this
|
|
8
|
+
// element composes it and keeps only the DOM render half.
|
|
9
|
+
// `@gjsify/adwaita-nativescript` composes the same state, so both ports share one
|
|
10
|
+
// behaviour. Three rules from adw-window-title.c that neither renderer had:
|
|
11
|
+
// - the TITLE label hides when the title is empty (C:207-208; the template even
|
|
12
|
+
// starts it `visible=False`, adw-window-title.ui:15). Only the subtitle was
|
|
13
|
+
// ever hidden here, so a header bar with a subtitle and no title reserved a
|
|
14
|
+
// blank title line above it.
|
|
15
|
+
// - setting the value it already has returns early (C:203-204, :244-245).
|
|
16
|
+
// - a real change notifies (C:210, :251).
|
|
17
|
+
//
|
|
5
18
|
// Reference: refs/adwaita-web/adwaita-web/docs/widgets/windowtitle.md
|
|
6
|
-
// Reference: refs/libadwaita
|
|
19
|
+
// Reference: refs/libadwaita/src/adw-window-title.c, adw-window-title.ui
|
|
7
20
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
8
|
-
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web
|
|
21
|
+
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the
|
|
22
|
+
// title/subtitle state composed from @gjsify/adwaita-core.
|
|
23
|
+
|
|
24
|
+
import { WindowTitleState } from '@gjsify/adwaita-core';
|
|
9
25
|
|
|
10
26
|
export class AdwWindowTitle extends HTMLElement {
|
|
11
27
|
private _titleEl!: HTMLSpanElement;
|
|
12
28
|
private _subtitleEl!: HTMLSpanElement;
|
|
29
|
+
/** The headless title/subtitle pair + change detection (ADR 0004). */
|
|
30
|
+
private readonly _state = new WindowTitleState();
|
|
13
31
|
private _initialized = false;
|
|
14
32
|
|
|
15
33
|
static get observedAttributes() {
|
|
@@ -27,21 +45,36 @@ export class AdwWindowTitle extends HTMLElement {
|
|
|
27
45
|
this._subtitleEl.className = 'adw-window-title-subtitle';
|
|
28
46
|
|
|
29
47
|
this.replaceChildren(this._titleEl, this._subtitleEl);
|
|
48
|
+
// Adopt the declared attributes without notifying: this is the initial
|
|
49
|
+
// value, not a change (`adw_window_title_new` sets both at construction).
|
|
50
|
+
this._state.setTitle(this.getAttribute('title'));
|
|
51
|
+
this._state.setSubtitle(this.getAttribute('subtitle'));
|
|
30
52
|
this._render();
|
|
31
53
|
}
|
|
32
54
|
|
|
33
|
-
attributeChangedCallback() {
|
|
34
|
-
if (this._initialized)
|
|
55
|
+
attributeChangedCallback(name: string, _old: string | null, value: string | null) {
|
|
56
|
+
if (!this._initialized) return;
|
|
57
|
+
const changed = name === 'title' ? this._state.setTitle(value) : this._state.setSubtitle(value);
|
|
58
|
+
// The early-return is the whole point: re-setting the same value paints
|
|
59
|
+
// nothing and notifies nothing (C:203-204, :244-245).
|
|
60
|
+
if (!changed) return;
|
|
61
|
+
this._render();
|
|
62
|
+
this.dispatchEvent(
|
|
63
|
+
new CustomEvent(`notify::${name}`, {
|
|
64
|
+
bubbles: true,
|
|
65
|
+
detail: { [name]: name === 'title' ? this._state.title : this._state.subtitle },
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
35
68
|
}
|
|
36
69
|
|
|
37
70
|
private _render() {
|
|
38
|
-
const title = this.
|
|
39
|
-
const subtitle = this.getAttribute('subtitle') ?? '';
|
|
71
|
+
const { title, titleVisible, subtitle, subtitleVisible } = this._state.state;
|
|
40
72
|
this._titleEl.textContent = title;
|
|
73
|
+
this._titleEl.hidden = !titleVisible;
|
|
41
74
|
this._subtitleEl.textContent = subtitle;
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
this._subtitleEl.hidden =
|
|
75
|
+
// Hiding the empty label entirely keeps the other one vertically
|
|
76
|
+
// centered, which is what the GTK box does when a child is invisible.
|
|
77
|
+
this._subtitleEl.hidden = !subtitleVisible;
|
|
45
78
|
}
|
|
46
79
|
}
|
|
47
80
|
|