@gjsify/adwaita-web 0.31.0 → 0.32.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 +786 -272
- 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-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 +37 -11
- package/lib/types/elements/adw-menu-button.d.ts +6 -6
- package/lib/types/elements/adw-navigation-split-view.d.ts +6 -0
- package/lib/types/elements/adw-navigation-view.d.ts +63 -20
- package/lib/types/elements/adw-overlay-split-view.d.ts +38 -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-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 +27 -19
- package/lib/types/elements/adw-view-switcher-bar.d.ts +23 -7
- package/lib/types/elements/adw-view-switcher.d.ts +22 -11
- package/lib/types/elements/adw-window-title.d.ts +3 -1
- 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/_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/_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 +7 -3
- 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 +64 -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 +56 -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 +139 -0
- package/src/adw-banner.spec.ts +152 -0
- package/src/adw-button-content.spec.ts +183 -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-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 +194 -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 +301 -0
- package/src/elements/adw-about-dialog.ts +223 -163
- package/src/elements/adw-action-row.ts +120 -10
- 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 +15 -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 +240 -129
- package/src/elements/adw-menu-button.ts +93 -70
- package/src/elements/adw-navigation-split-view.ts +48 -7
- package/src/elements/adw-navigation-view.ts +286 -148
- package/src/elements/adw-overlay-split-view.ts +164 -14
- 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 +23 -8
- 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 +682 -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 +162 -85
- package/src/elements/adw-view-switcher-bar.ts +163 -81
- package/src/elements/adw-view-switcher.ts +231 -112
- package/src/elements/adw-window-title.ts +44 -11
- package/src/elements/adw-wrap-box.ts +131 -35
- 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 +309 -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 +446 -0
|
@@ -1,90 +1,170 @@
|
|
|
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
|
+
// Aliased because `AdwViewSwitcherPage` is already this module's page ELEMENT —
|
|
60
|
+
// the custom element the package has always exported under that name.
|
|
61
|
+
import type {
|
|
62
|
+
AdwViewSwitcherPage as AdwViewSwitcherPageInfo,
|
|
63
|
+
AdwViewSwitcherPolicy,
|
|
64
|
+
ViewSwitcherStateChange,
|
|
65
|
+
} from '@gjsify/adwaita-core';
|
|
25
66
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
67
|
+
import {
|
|
68
|
+
applyDescription,
|
|
69
|
+
applyIndicator,
|
|
70
|
+
applySwitcherIcon,
|
|
71
|
+
domViewSwitcherScheduler,
|
|
72
|
+
readSwitcherPage,
|
|
73
|
+
} from './view-switcher-dom.js';
|
|
74
|
+
import { type AdwIcon, createAdwIcon } from './adw-icon.js';
|
|
75
|
+
|
|
76
|
+
/** The DOM nodes one page owns — the switcher paints the derived model onto them. */
|
|
77
|
+
interface PageNodes {
|
|
31
78
|
button: HTMLButtonElement;
|
|
79
|
+
icon: AdwIcon;
|
|
80
|
+
label: HTMLSpanElement;
|
|
81
|
+
indicator: HTMLSpanElement;
|
|
82
|
+
body: HTMLDivElement;
|
|
32
83
|
}
|
|
33
84
|
|
|
34
85
|
/** A single page. Children of <adw-view-switcher>; consumed at connect time. */
|
|
35
86
|
export class AdwViewSwitcherPage extends HTMLElement {
|
|
36
87
|
static get observedAttributes() {
|
|
37
|
-
return ['name', 'title', 'icon-name'];
|
|
88
|
+
return ['name', 'title', 'icon-name', 'badge-number', 'needs-attention', 'use-underline'];
|
|
38
89
|
}
|
|
39
90
|
}
|
|
40
91
|
|
|
41
92
|
export class AdwViewSwitcher extends HTMLElement {
|
|
93
|
+
private readonly _state = new ViewSwitcherState({ scheduler: domViewSwitcherScheduler });
|
|
42
94
|
private _barEl!: HTMLDivElement;
|
|
43
95
|
private _contentEl!: HTMLDivElement;
|
|
44
|
-
private
|
|
45
|
-
private
|
|
46
|
-
private _policy: Policy = 'narrow';
|
|
96
|
+
private _nodes: PageNodes[] = [];
|
|
97
|
+
private _policy: AdwViewSwitcherPolicy = 'narrow';
|
|
47
98
|
private _initialized = false;
|
|
48
99
|
|
|
49
100
|
static get observedAttributes() {
|
|
50
101
|
return ['policy', 'active'];
|
|
51
102
|
}
|
|
52
103
|
|
|
104
|
+
constructor() {
|
|
105
|
+
super();
|
|
106
|
+
// Subscribing touches no attributes and no children, so it is legal in a
|
|
107
|
+
// custom element constructor — unlike anything DOM-shaped.
|
|
108
|
+
this._state.subscribe((change) => this._onStateChange(change));
|
|
109
|
+
}
|
|
110
|
+
|
|
53
111
|
/** The policy controlling the button layout ('wide' | 'narrow'). */
|
|
54
|
-
get policy():
|
|
112
|
+
get policy(): AdwViewSwitcherPolicy {
|
|
55
113
|
return this._policy;
|
|
56
114
|
}
|
|
57
115
|
|
|
58
|
-
set policy(value:
|
|
116
|
+
set policy(value: AdwViewSwitcherPolicy) {
|
|
59
117
|
this.setAttribute('policy', value);
|
|
60
118
|
}
|
|
61
119
|
|
|
62
|
-
/** Zero-based index of the
|
|
120
|
+
/** Zero-based index of the selected page, `-1` when nothing is selected. */
|
|
63
121
|
get active(): number {
|
|
64
|
-
return this.
|
|
122
|
+
return this._state.selected;
|
|
65
123
|
}
|
|
66
124
|
|
|
67
125
|
set active(value: number) {
|
|
68
|
-
this.
|
|
126
|
+
this._state.setSelected(value);
|
|
127
|
+
this._reflectActive();
|
|
69
128
|
}
|
|
70
129
|
|
|
71
|
-
/**
|
|
72
|
-
get visibleChildName(): string
|
|
73
|
-
return this.
|
|
130
|
+
/** Name of the selected page, `''` when nothing is selected. */
|
|
131
|
+
get visibleChildName(): string {
|
|
132
|
+
return this._state.selectedName;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
set visibleChildName(name: string | null | undefined) {
|
|
136
|
+
this._state.selectName(name);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The resolved page records a bound layout can read. */
|
|
140
|
+
get pages(): readonly AdwViewSwitcherPageInfo[] {
|
|
141
|
+
return this._state.pages;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Show or hide a page (`AdwViewStackPage:visible`). Returns whether the
|
|
146
|
+
* SELECTION moved — hiding the selected page falls back to the first still
|
|
147
|
+
* visible one (adw-view-stack.c:1061-1082).
|
|
148
|
+
*/
|
|
149
|
+
setPageVisible(name: string, visible: boolean): boolean {
|
|
150
|
+
const moved = this._state.setPageVisible(name, visible);
|
|
151
|
+
this._render();
|
|
152
|
+
return moved;
|
|
74
153
|
}
|
|
75
154
|
|
|
76
155
|
connectedCallback() {
|
|
77
156
|
if (this._initialized) return;
|
|
78
|
-
this._initialized = true;
|
|
79
157
|
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
const
|
|
158
|
+
// `:scope >` rather than a subtree scan: an unscoped query let an outer
|
|
159
|
+
// switcher adopt the pages of a nested one, since both use the same tag.
|
|
160
|
+
// Pages come from the stack's OWN children in C (adw-view-switcher.c:203).
|
|
161
|
+
const declared = Array.from(this.querySelectorAll(':scope > adw-view-switcher-page')) as HTMLElement[];
|
|
162
|
+
// Read the declared selection BEFORE adopting any page: the auto-pick
|
|
163
|
+
// reflects its own index back into this very attribute.
|
|
164
|
+
const declaredActive = this.getAttribute('active');
|
|
84
165
|
|
|
85
|
-
// The single `viewswitcher` node holds the segmented button bar; a
|
|
86
|
-
// content area shows the
|
|
87
|
-
// Adw.ViewStack the switcher would otherwise drive).
|
|
166
|
+
// The single `viewswitcher` node holds the segmented button bar; a
|
|
167
|
+
// sibling content area shows the selected page.
|
|
88
168
|
this._barEl = document.createElement('div');
|
|
89
169
|
this._barEl.className = 'adw-view-switcher-bar';
|
|
90
170
|
this._barEl.setAttribute('role', 'tablist');
|
|
@@ -92,107 +172,146 @@ export class AdwViewSwitcher extends HTMLElement {
|
|
|
92
172
|
this._contentEl = document.createElement('div');
|
|
93
173
|
this._contentEl.className = 'adw-view-switcher-content';
|
|
94
174
|
|
|
95
|
-
this.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const icon = (pageEl.getAttribute('icon-name') ?? '').replace(/-symbolic$/, '');
|
|
99
|
-
|
|
100
|
-
const button = document.createElement('button');
|
|
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));
|
|
120
|
-
|
|
121
|
-
const body = document.createElement('div');
|
|
122
|
-
body.className = 'adw-view-switcher-page';
|
|
123
|
-
body.dataset.name = name;
|
|
124
|
-
for (const child of Array.from(pageEl.childNodes)) body.appendChild(child);
|
|
125
|
-
|
|
126
|
-
return { name, title, icon, body, button };
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
this._barEl.append(...this._pages.map((p) => p.button));
|
|
130
|
-
this._contentEl.append(...this._pages.map((p) => p.body));
|
|
175
|
+
this._nodes = declared.map((pageEl, index) => this._buildPage(pageEl, index));
|
|
176
|
+
this._barEl.append(...this._nodes.map((nodes) => nodes.button));
|
|
177
|
+
this._contentEl.append(...this._nodes.map((nodes) => nodes.body));
|
|
131
178
|
this.replaceChildren(this._barEl, this._contentEl);
|
|
132
179
|
|
|
133
180
|
this._policy = this._readPolicyAttr();
|
|
134
|
-
this.
|
|
181
|
+
this._initialized = true;
|
|
182
|
+
|
|
183
|
+
this._state.setPages(declared.map((pageEl) => readSwitcherPage(pageEl)));
|
|
184
|
+
if (declaredActive !== null) {
|
|
185
|
+
const index = Number.parseInt(declaredActive, 10);
|
|
186
|
+
// A declared selection is not a user activation.
|
|
187
|
+
if (!Number.isNaN(index)) this._state.setSelected(index, false);
|
|
188
|
+
}
|
|
135
189
|
this._render();
|
|
136
190
|
}
|
|
137
191
|
|
|
192
|
+
disconnectedCallback() {
|
|
193
|
+
// A drag that leaves with the element would otherwise switch pages after
|
|
194
|
+
// the widget is gone; C drops the source in dispose the same way.
|
|
195
|
+
this._state.cancelDrag();
|
|
196
|
+
}
|
|
197
|
+
|
|
138
198
|
attributeChangedCallback(name: string) {
|
|
139
199
|
if (!this._initialized) return;
|
|
140
200
|
if (name === 'policy') {
|
|
141
201
|
const next = this._readPolicyAttr();
|
|
142
|
-
if (next
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
202
|
+
if (next === this._policy) return;
|
|
203
|
+
this._policy = next;
|
|
204
|
+
this._render();
|
|
205
|
+
this.dispatchEvent(new CustomEvent('notify::policy', { bubbles: true, detail: { policy: next } }));
|
|
146
206
|
return;
|
|
147
207
|
}
|
|
148
208
|
if (name === 'active') {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
209
|
+
const raw = this.getAttribute('active');
|
|
210
|
+
const index = raw === null ? Number.NaN : Number.parseInt(raw, 10);
|
|
211
|
+
if (!Number.isNaN(index)) this._state.setSelected(index);
|
|
212
|
+
// A refused request must not leave the attribute claiming something
|
|
213
|
+
// the widget is not showing; re-reflecting is what makes "refused,
|
|
214
|
+
// not clamped" observable in the DOM.
|
|
215
|
+
this._reflectActive();
|
|
154
216
|
}
|
|
155
217
|
}
|
|
156
218
|
|
|
157
|
-
private
|
|
158
|
-
|
|
219
|
+
private _buildPage(pageEl: HTMLElement, index: number): PageNodes {
|
|
220
|
+
const button = document.createElement('button');
|
|
221
|
+
button.type = 'button';
|
|
222
|
+
button.className = 'adw-view-switcher-button';
|
|
223
|
+
button.setAttribute('role', 'tab');
|
|
224
|
+
|
|
225
|
+
// Both children always exist, exactly as AdwViewSwitcherButton's template
|
|
226
|
+
// does — the icon carries the `image-missing` fallback rather than
|
|
227
|
+
// disappearing (adw-view-switcher-button.ui).
|
|
228
|
+
const icon = createAdwIcon(null, 'adw-view-switcher-icon');
|
|
229
|
+
|
|
230
|
+
const label = document.createElement('span');
|
|
231
|
+
label.className = 'adw-view-switcher-label';
|
|
232
|
+
|
|
233
|
+
const indicator = document.createElement('span');
|
|
234
|
+
indicator.className = 'adw-view-switcher-indicator';
|
|
235
|
+
indicator.setAttribute('aria-hidden', 'true'); // announced via aria-description
|
|
236
|
+
|
|
237
|
+
button.append(icon, label, indicator);
|
|
238
|
+
button.addEventListener('click', () => {
|
|
239
|
+
this._state.setSelected(index);
|
|
240
|
+
});
|
|
241
|
+
// GtkDropControllerMotion's enter/leave, which drive TIMEOUT_EXPAND.
|
|
242
|
+
button.addEventListener('dragenter', () => this._state.dragEnter(index));
|
|
243
|
+
button.addEventListener('dragleave', () => this._state.dragLeave(index));
|
|
244
|
+
|
|
245
|
+
const body = document.createElement('div');
|
|
246
|
+
body.className = 'adw-view-switcher-page';
|
|
247
|
+
body.dataset.name = pageEl.getAttribute('name') ?? '';
|
|
248
|
+
for (const child of Array.from(pageEl.childNodes)) body.appendChild(child);
|
|
249
|
+
|
|
250
|
+
return { button, icon, label, indicator, body };
|
|
159
251
|
}
|
|
160
252
|
|
|
161
|
-
private
|
|
162
|
-
const raw =
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
253
|
+
private _readPolicyAttr(): AdwViewSwitcherPolicy {
|
|
254
|
+
const raw = this.getAttribute('policy');
|
|
255
|
+
// A removed attribute falls back to the C default; an unrecognised value
|
|
256
|
+
// is refused and keeps the current policy (adw_view_switcher_set_policy
|
|
257
|
+
// is reached only through the enum, which rejects garbage).
|
|
258
|
+
if (raw === null) return 'narrow';
|
|
259
|
+
return isViewSwitcherPolicy(raw) ? raw : this._policy;
|
|
166
260
|
}
|
|
167
261
|
|
|
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));
|
|
262
|
+
private _onStateChange(change: ViewSwitcherStateChange): void {
|
|
174
263
|
this._render();
|
|
175
|
-
const page = this._pages[index];
|
|
176
264
|
this.dispatchEvent(
|
|
177
265
|
new CustomEvent('notify::visible-child', {
|
|
178
266
|
bubbles: true,
|
|
179
|
-
detail: {
|
|
267
|
+
detail: {
|
|
268
|
+
index: change.selected,
|
|
269
|
+
name: change.name,
|
|
270
|
+
title: change.title,
|
|
271
|
+
interactive: change.interactive,
|
|
272
|
+
},
|
|
180
273
|
}),
|
|
181
274
|
);
|
|
182
275
|
}
|
|
183
276
|
|
|
184
277
|
private _render(): void {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
this.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
278
|
+
if (!this._initialized) return;
|
|
279
|
+
|
|
280
|
+
// The single `viewswitcher` node carries the policy style class
|
|
281
|
+
// (adw-view-switcher.c:539-545).
|
|
282
|
+
for (const policy of VIEW_SWITCHER_POLICIES) this.classList.toggle(policy, policy === this._policy);
|
|
283
|
+
|
|
284
|
+
const models = buildViewSwitcherButtons(this._state.pages, this._state.selected, this._policy);
|
|
285
|
+
models.forEach((model, index) => {
|
|
286
|
+
const nodes = this._nodes[index];
|
|
287
|
+
if (!nodes) return;
|
|
288
|
+
|
|
289
|
+
nodes.button.hidden = !model.visible;
|
|
290
|
+
nodes.button.classList.toggle('active', model.selected);
|
|
291
|
+
nodes.button.setAttribute('aria-selected', String(model.selected));
|
|
292
|
+
nodes.button.tabIndex = model.selected ? 0 : -1;
|
|
293
|
+
|
|
294
|
+
applySwitcherIcon(nodes.icon, model.iconName);
|
|
295
|
+
nodes.label.textContent = model.label;
|
|
296
|
+
nodes.label.hidden = model.label.length === 0;
|
|
297
|
+
applyIndicator(nodes.indicator, model.badgeLabel, model.needsAttention);
|
|
298
|
+
applyDescription(nodes.button, model.description);
|
|
299
|
+
|
|
300
|
+
// A tab with no visible text still needs an accessible name; the
|
|
301
|
+
// icon name is the only thing left to build one from.
|
|
302
|
+
if (model.label) nodes.button.removeAttribute('aria-label');
|
|
303
|
+
else nodes.button.setAttribute('aria-label', model.iconName);
|
|
304
|
+
|
|
305
|
+
nodes.body.classList.toggle('active-view', model.selected);
|
|
306
|
+
nodes.body.hidden = !model.selected;
|
|
195
307
|
});
|
|
308
|
+
|
|
309
|
+
this._reflectActive();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private _reflectActive(): void {
|
|
313
|
+
const current = String(this._state.selected);
|
|
314
|
+
if (this.getAttribute('active') !== current) this.setAttribute('active', current);
|
|
196
315
|
}
|
|
197
316
|
}
|
|
198
317
|
|
|
@@ -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
|
|