@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,79 +1,188 @@
|
|
|
1
1
|
// <adw-inline-view-switcher> — A compact, toolbar-friendly view switcher (the
|
|
2
|
-
// web counterpart of Adw.InlineViewSwitcher).
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
2
|
+
// web counterpart of Adw.InlineViewSwitcher). Each declared <adw-view-stack-page>
|
|
3
|
+
// child becomes a toggle in a toggle-group-style bar and a panel in the stack,
|
|
4
|
+
// and exactly one page is visible at a time.
|
|
5
|
+
//
|
|
6
|
+
// Everything that decides WHAT to show is HEADLESS and lives in
|
|
7
|
+
// `@gjsify/adwaita-core` (ADR 0004) — including the TWO INDEX SPACES this widget
|
|
8
|
+
// is built on: a hidden page produces NO toggle, so the compacted toggle index
|
|
9
|
+
// and the page index diverge, and libadwaita reconciles them through the
|
|
10
|
+
// `child-index` it stashes on every toggle (adw-inline-view-switcher.c:346,
|
|
11
|
+
// :114-129, :434-453). `buildInlineToggles` carries both indices, so this file
|
|
12
|
+
// never has to reconcile them itself.
|
|
13
|
+
//
|
|
14
|
+
// What the lift fixed here, all of it C-derived:
|
|
15
|
+
// - hidden pages were not filtered at all, so the two index spaces collapsed
|
|
16
|
+
// into one and `AdwViewStackPage:visible` had no spelling (:370-378).
|
|
17
|
+
// - the display-mode default was 'both'; libadwaita's is 'labels' (:657, :715).
|
|
18
|
+
// - an unrecognised display-mode value REPLACED the current mode with 'both';
|
|
19
|
+
// `g_return_if_fail (mode <= …_BOTH)` rejects it and keeps the current one
|
|
20
|
+
// (:819).
|
|
21
|
+
// - the icons-mode tooltip used the RAW title, so `title="_Files"` showed the
|
|
22
|
+
// underscore; C strips the mnemonic when use-underline is set (:180-188).
|
|
23
|
+
// - a page with no icon rendered no icon; C substitutes `image-missing`
|
|
24
|
+
// (:137-142) in every mode that builds an image at all.
|
|
25
|
+
// - `active` was CLAMPED into range instead of refused, `notify::display-mode`
|
|
26
|
+
// was documented but never dispatched, the page query was unscoped (so a
|
|
27
|
+
// nested switcher lost its pages to the outer one), and the 500 ms
|
|
28
|
+
// drag-hover auto-switch did not exist (:80, :194-236).
|
|
29
|
+
//
|
|
8
30
|
// Attributes:
|
|
9
|
-
// active
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
31
|
+
// active — zero-based index of the visible PAGE (not of the toggle). Refused,
|
|
32
|
+
// never clamped, and reflected back to what actually holds.
|
|
33
|
+
// display-mode — labels | icons | both (default 'labels'). An unrecognised
|
|
34
|
+
// value is rejected; removing the attribute restores the default.
|
|
35
|
+
// flat / round — the Adw.ToggleGroup style classes.
|
|
36
|
+
// Page attributes: name, title, icon-name, hidden (= AdwViewStackPage:visible),
|
|
37
|
+
// use-underline, badge-number, needs-attention.
|
|
13
38
|
// Events:
|
|
14
|
-
// `notify::active` (CustomEvent, bubbles,
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// Reference: refs/libadwaita/src/
|
|
20
|
-
// Reference: refs/libadwaita/src/stylesheet/widgets/_toggle-group.scss (inline-view-switcher > toggle-group)
|
|
21
|
-
// Reference: refs/adwaita-web/adwaita-web/scss/_viewswitcher.scss
|
|
39
|
+
// `notify::active` (CustomEvent, bubbles,
|
|
40
|
+
// detail = { active, name, toggleIndex, interactive }) on every selection
|
|
41
|
+
// change, including the auto-pick.
|
|
42
|
+
// `notify::display-mode` (CustomEvent, bubbles, detail = { displayMode }).
|
|
43
|
+
// Reference: refs/libadwaita/src/adw-inline-view-switcher.c (AdwInlineViewSwitcher)
|
|
44
|
+
// Reference: refs/libadwaita/src/stylesheet/widgets/_toggle-group.scss
|
|
22
45
|
// Copyright (c) 2023 Maximiliano Sandoval / 2024 GNOME Foundation Inc. (libadwaita). LGPLv2.1+.
|
|
23
46
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
24
47
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
48
|
+
import {
|
|
49
|
+
INLINE_VIEW_SWITCHER_DISPLAY_MODES,
|
|
50
|
+
VIEW_SWITCHER_NO_SELECTION,
|
|
51
|
+
ViewSwitcherState,
|
|
52
|
+
buildInlineToggles,
|
|
53
|
+
isInlineViewSwitcherDisplayMode,
|
|
54
|
+
toggleIndexForPage,
|
|
55
|
+
} from '@gjsify/adwaita-core';
|
|
56
|
+
import type { ViewSwitcherScheduler } from '@gjsify/adwaita-core';
|
|
57
|
+
import type {
|
|
58
|
+
AdwInlineViewSwitcherDisplayMode,
|
|
59
|
+
AdwViewSwitcherPage,
|
|
60
|
+
ViewSwitcherStateChange,
|
|
61
|
+
} from '@gjsify/adwaita-core';
|
|
62
|
+
|
|
63
|
+
import {
|
|
64
|
+
applyDescription,
|
|
65
|
+
applyIndicator,
|
|
66
|
+
createSwitcherIcon,
|
|
67
|
+
domViewSwitcherScheduler,
|
|
68
|
+
readSwitcherPage,
|
|
69
|
+
} from './view-switcher-dom.js';
|
|
28
70
|
|
|
29
71
|
/** A single page. Children of <adw-inline-view-switcher>; consumed at connect time. */
|
|
30
72
|
export class AdwViewStackPage extends HTMLElement {
|
|
31
73
|
static get observedAttributes() {
|
|
32
|
-
return ['title', 'icon-name'];
|
|
74
|
+
return ['name', 'title', 'icon-name', 'badge-number', 'needs-attention', 'use-underline'];
|
|
33
75
|
}
|
|
34
76
|
}
|
|
35
77
|
|
|
36
78
|
export class AdwInlineViewSwitcher extends HTMLElement {
|
|
79
|
+
/**
|
|
80
|
+
* The dwell timer, injectable.
|
|
81
|
+
*
|
|
82
|
+
* It used to be `new ViewSwitcherState({ scheduler: domViewSwitcherScheduler })`
|
|
83
|
+
* inline, which left NOWHERE to hand a clock in — so
|
|
84
|
+
* `VIEW_SWITCHER_DRAG_VECTORS` was driven by the core suite alone, and the
|
|
85
|
+
* conformance header claimed all three suites drove it. `scheduler` is a
|
|
86
|
+
* property so a test can replace it before the element connects; the DOM one
|
|
87
|
+
* is the default, exactly as before.
|
|
88
|
+
*/
|
|
89
|
+
scheduler: ViewSwitcherScheduler = domViewSwitcherScheduler;
|
|
90
|
+
private _stateInstance: ViewSwitcherState | null = null;
|
|
91
|
+
|
|
92
|
+
/** Built on first access, so a `scheduler` set before connect is the one used. */
|
|
93
|
+
private get _state(): ViewSwitcherState {
|
|
94
|
+
if (!this._stateInstance) {
|
|
95
|
+
this._stateInstance = new ViewSwitcherState({ scheduler: this.scheduler });
|
|
96
|
+
this._stateInstance.subscribe((change) => this._onStateChange(change));
|
|
97
|
+
}
|
|
98
|
+
return this._stateInstance;
|
|
99
|
+
}
|
|
37
100
|
private _groupEl!: HTMLDivElement;
|
|
38
101
|
private _pagesEl!: HTMLDivElement;
|
|
102
|
+
private _panels: HTMLDivElement[] = [];
|
|
39
103
|
private _toggles: HTMLButtonElement[] = [];
|
|
40
|
-
private
|
|
41
|
-
private _active = 0;
|
|
42
|
-
private _displayMode: DisplayMode = 'both';
|
|
104
|
+
private _displayMode: AdwInlineViewSwitcherDisplayMode = 'labels';
|
|
43
105
|
private _initialized = false;
|
|
106
|
+
/** The page ELEMENTS, kept because this element detaches them from the tree. */
|
|
107
|
+
private _declared: HTMLElement[] = [];
|
|
108
|
+
private _pageObserver: MutationObserver | undefined;
|
|
44
109
|
|
|
45
110
|
static get observedAttributes() {
|
|
46
|
-
return ['active', 'display-mode', 'flat', 'round'];
|
|
111
|
+
return ['active', 'display-mode', 'flat', 'round', 'osd'];
|
|
47
112
|
}
|
|
48
113
|
|
|
49
|
-
|
|
114
|
+
constructor() {
|
|
115
|
+
super();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Zero-based index of the visible PAGE, `-1` when nothing is selected. */
|
|
50
119
|
get active(): number {
|
|
51
|
-
return this.
|
|
120
|
+
return this._state.selected;
|
|
52
121
|
}
|
|
53
122
|
|
|
54
123
|
set active(value: number) {
|
|
55
|
-
this.
|
|
124
|
+
this._state.setSelected(value);
|
|
125
|
+
this._reflectActive();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Index of the ACTIVE TOGGLE, `-1` when the visible page has none because it
|
|
130
|
+
* is hidden — the port-safe `GTK_INVALID_LIST_POSITION`
|
|
131
|
+
* (adw-inline-view-switcher.c:441-446).
|
|
132
|
+
*/
|
|
133
|
+
get activeToggle(): number {
|
|
134
|
+
return toggleIndexForPage(this._state.pages, this._state.selected);
|
|
56
135
|
}
|
|
57
136
|
|
|
58
137
|
/** What the toggles display: 'labels', 'icons' or 'both'. */
|
|
59
|
-
get displayMode():
|
|
138
|
+
get displayMode(): AdwInlineViewSwitcherDisplayMode {
|
|
60
139
|
return this._displayMode;
|
|
61
140
|
}
|
|
62
141
|
|
|
63
|
-
set displayMode(value:
|
|
142
|
+
set displayMode(value: AdwInlineViewSwitcherDisplayMode) {
|
|
64
143
|
this.setAttribute('display-mode', value);
|
|
65
144
|
}
|
|
66
145
|
|
|
146
|
+
/** Name of the visible page, `''` when nothing is selected. */
|
|
147
|
+
get visibleChildName(): string {
|
|
148
|
+
return this._state.selectedName;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
set visibleChildName(name: string | null | undefined) {
|
|
152
|
+
this._state.selectName(name);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The resolved page records. */
|
|
156
|
+
get pages(): readonly AdwViewSwitcherPage[] {
|
|
157
|
+
return this._state.pages;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Show or hide a page. Returns whether the SELECTION moved. Rebuilds the
|
|
162
|
+
* toggle row, because a hidden page has no toggle at all — C reacts to a
|
|
163
|
+
* page's `notify::visible` with `recreate_toggles`
|
|
164
|
+
* (adw-inline-view-switcher.c:425, :471-477).
|
|
165
|
+
*/
|
|
166
|
+
setPageVisible(name: string, visible: boolean): boolean {
|
|
167
|
+
const moved = this._state.setPageVisible(name, visible);
|
|
168
|
+
this._rebuildToggles();
|
|
169
|
+
return moved;
|
|
170
|
+
}
|
|
171
|
+
|
|
67
172
|
connectedCallback() {
|
|
68
|
-
if (this._initialized)
|
|
69
|
-
|
|
173
|
+
if (this._initialized) {
|
|
174
|
+
// Re-entering a document — same reason as `adw-view-switcher.ts`:
|
|
175
|
+
// the page observer is dropped on disconnect, so it has to be
|
|
176
|
+
// rebuilt here or a moved switcher goes deaf permanently.
|
|
177
|
+
this._watchPages();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
70
180
|
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
const
|
|
181
|
+
// `:scope >` rather than a subtree scan — `<adw-view-stack>` uses the
|
|
182
|
+
// same tag for its pages, so an unscoped query adopted a nested stack's.
|
|
183
|
+
const declared = Array.from(this.querySelectorAll(':scope > adw-view-stack-page')) as HTMLElement[];
|
|
184
|
+
const declaredActive = this.getAttribute('active');
|
|
75
185
|
|
|
76
|
-
// The switcher bar — a toggle-group of one toggle per page.
|
|
77
186
|
this._groupEl = document.createElement('div');
|
|
78
187
|
this._groupEl.className = 'adw-inline-view-switcher-group';
|
|
79
188
|
this._groupEl.setAttribute('role', 'tablist');
|
|
@@ -81,140 +190,225 @@ export class AdwInlineViewSwitcher extends HTMLElement {
|
|
|
81
190
|
this._pagesEl = document.createElement('div');
|
|
82
191
|
this._pagesEl.className = 'adw-inline-view-switcher-pages';
|
|
83
192
|
|
|
84
|
-
this.
|
|
85
|
-
this._pages = [];
|
|
86
|
-
|
|
87
|
-
pages.forEach((page, index) => {
|
|
88
|
-
const title = page.getAttribute('title') ?? '';
|
|
89
|
-
const icon = (page.getAttribute('icon-name') ?? '').replace(/-symbolic$/, '');
|
|
90
|
-
|
|
91
|
-
const toggle = document.createElement('button');
|
|
92
|
-
toggle.type = 'button';
|
|
93
|
-
toggle.className = 'adw-inline-view-switcher-toggle';
|
|
94
|
-
toggle.setAttribute('role', 'tab');
|
|
95
|
-
// The toggle stores both title + icon; _render() shows the parts the
|
|
96
|
-
// current display mode calls for (mirrors libadwaita rebuilding the
|
|
97
|
-
// toggle child on a display-mode change).
|
|
98
|
-
toggle.dataset.title = title;
|
|
99
|
-
toggle.dataset.icon = icon;
|
|
100
|
-
|
|
101
|
-
const iconEl = document.createElement('span');
|
|
102
|
-
iconEl.className = `adw-icon${icon ? ` adw-icon--${icon}` : ''} adw-inline-view-switcher-icon`;
|
|
103
|
-
iconEl.setAttribute('aria-hidden', 'true'); // decorative (mask-image only)
|
|
104
|
-
toggle.appendChild(iconEl);
|
|
105
|
-
|
|
106
|
-
const labelEl = document.createElement('span');
|
|
107
|
-
labelEl.className = 'adw-inline-view-switcher-label';
|
|
108
|
-
labelEl.textContent = title;
|
|
109
|
-
toggle.appendChild(labelEl);
|
|
110
|
-
|
|
111
|
-
toggle.addEventListener('click', () => this._selectIndex(index));
|
|
112
|
-
this._groupEl.appendChild(toggle);
|
|
113
|
-
this._toggles.push(toggle);
|
|
114
|
-
|
|
115
|
-
// The page content area — the declared element's children are moved
|
|
116
|
-
// into a panel that the switcher toggles visible.
|
|
193
|
+
this._panels = declared.map((pageEl) => {
|
|
117
194
|
const panel = document.createElement('div');
|
|
118
195
|
panel.className = 'adw-inline-view-switcher-page';
|
|
119
196
|
panel.setAttribute('role', 'tabpanel');
|
|
120
|
-
|
|
197
|
+
panel.dataset.name = pageEl.getAttribute('name') ?? '';
|
|
198
|
+
for (const child of Array.from(pageEl.childNodes)) panel.appendChild(child);
|
|
121
199
|
this._pagesEl.appendChild(panel);
|
|
122
|
-
|
|
200
|
+
return panel;
|
|
123
201
|
});
|
|
124
202
|
|
|
125
203
|
this.replaceChildren(this._groupEl, this._pagesEl);
|
|
126
204
|
|
|
127
|
-
this._active = this._readActiveAttr();
|
|
128
205
|
this._displayMode = this._readDisplayModeAttr();
|
|
129
|
-
this.
|
|
206
|
+
this._initialized = true;
|
|
207
|
+
|
|
208
|
+
this._declared = declared;
|
|
209
|
+
this._watchPages();
|
|
210
|
+
this._state.setPages(declared.map((pageEl) => readSwitcherPage(pageEl)));
|
|
211
|
+
if (declaredActive !== null) {
|
|
212
|
+
const index = Number.parseInt(declaredActive, 10);
|
|
213
|
+
if (!Number.isNaN(index)) this._state.setSelected(index, false);
|
|
214
|
+
}
|
|
215
|
+
this._rebuildToggles();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
disconnectedCallback() {
|
|
219
|
+
this._state.cancelDrag();
|
|
220
|
+
this._pageObserver?.disconnect();
|
|
221
|
+
this._pageObserver = undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Re-read every declared page and push it into the state.
|
|
226
|
+
*
|
|
227
|
+
* Driven by {@link _watchPages}, which is what makes a runtime
|
|
228
|
+
* `badge-number` or `icon-name` change reach the toggle — C rebinds on every
|
|
229
|
+
* page `notify` (adw-view-switcher.c:184-193) and this element did not.
|
|
230
|
+
*/
|
|
231
|
+
refreshPages(): void {
|
|
232
|
+
if (!this._initialized) return;
|
|
233
|
+
this._state.setPages(this._declared.map((pageEl) => readSwitcherPage(pageEl)));
|
|
234
|
+
this._rebuildToggles();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Watch the declared pages for attribute changes — the DOM's `notify::` on
|
|
239
|
+
* `AdwViewStackPage`, which C binds per page (adw-view-switcher.c:184-193).
|
|
240
|
+
*
|
|
241
|
+
* A `MutationObserver` rather than an `attributeChangedCallback`: this
|
|
242
|
+
* element consumes the page elements at connect and `replaceChildren`s them
|
|
243
|
+
* out of the tree, so a detached page cannot find its owner. It delivers on
|
|
244
|
+
* a MICROTASK where GTK's notify is synchronous — invisible to a user, and
|
|
245
|
+
* nothing here reads the toggle back inside the setter.
|
|
246
|
+
*/
|
|
247
|
+
private _watchPages(): void {
|
|
248
|
+
this._pageObserver?.disconnect();
|
|
249
|
+
this._pageObserver = new MutationObserver(() => this.refreshPages());
|
|
250
|
+
for (const pageEl of this._declared) this._pageObserver.observe(pageEl, { attributes: true });
|
|
130
251
|
}
|
|
131
252
|
|
|
132
253
|
attributeChangedCallback(name: string) {
|
|
133
254
|
if (!this._initialized) return;
|
|
134
255
|
if (name === 'active') {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
256
|
+
const raw = this.getAttribute('active');
|
|
257
|
+
const index = raw === null ? Number.NaN : Number.parseInt(raw, 10);
|
|
258
|
+
if (!Number.isNaN(index)) this._state.setSelected(index);
|
|
259
|
+
this._reflectActive();
|
|
140
260
|
return;
|
|
141
261
|
}
|
|
142
262
|
if (name === 'display-mode') {
|
|
143
263
|
const next = this._readDisplayModeAttr();
|
|
144
|
-
if (next
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
264
|
+
if (next === this._displayMode) return;
|
|
265
|
+
this._displayMode = next;
|
|
266
|
+
// C rebuilds every toggle's child on a mode change rather than
|
|
267
|
+
// toggling visibility on the existing ones (:852-854).
|
|
268
|
+
this._rebuildToggles();
|
|
269
|
+
this.dispatchEvent(
|
|
270
|
+
new CustomEvent('notify::display-mode', { bubbles: true, detail: { displayMode: next } }),
|
|
271
|
+
);
|
|
148
272
|
return;
|
|
149
273
|
}
|
|
150
274
|
// flat / round are styling-only.
|
|
151
|
-
this.
|
|
275
|
+
this._applyStyleClasses();
|
|
152
276
|
}
|
|
153
277
|
|
|
154
|
-
private
|
|
155
|
-
const raw =
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
return Math.min(Math.max(raw, 0), max);
|
|
278
|
+
private _readDisplayModeAttr(): AdwInlineViewSwitcherDisplayMode {
|
|
279
|
+
const raw = this.getAttribute('display-mode');
|
|
280
|
+
if (raw === null) return 'labels';
|
|
281
|
+
return isInlineViewSwitcherDisplayMode(raw) ? raw : this._displayMode;
|
|
159
282
|
}
|
|
160
283
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
284
|
+
/**
|
|
285
|
+
* Rebuild the toggle row from the derived models. One node per VISIBLE page,
|
|
286
|
+
* in compacted order — the structural half of `recreate_toggles`. Selection
|
|
287
|
+
* alone never lands here: `selection_changed_cb` only sets the active toggle
|
|
288
|
+
* (adw-inline-view-switcher.c:434-453), and rebuilding mid-drag would drop
|
|
289
|
+
* the dwell the drag has already armed.
|
|
290
|
+
*/
|
|
291
|
+
private _rebuildToggles(): void {
|
|
292
|
+
if (!this._initialized) return;
|
|
293
|
+
|
|
294
|
+
const models = buildInlineToggles(this._state.pages, this._displayMode);
|
|
295
|
+
this._groupEl.replaceChildren();
|
|
296
|
+
this._toggles = models.map((model) => {
|
|
297
|
+
const toggle = document.createElement('button');
|
|
298
|
+
toggle.type = 'button';
|
|
299
|
+
toggle.className = 'adw-inline-view-switcher-toggle';
|
|
300
|
+
toggle.setAttribute('role', 'tab');
|
|
301
|
+
// Both index spaces, so a consumer never has to recompute either.
|
|
302
|
+
toggle.dataset.pageIndex = String(model.pageIndex);
|
|
303
|
+
toggle.dataset.toggleIndex = String(model.toggleIndex);
|
|
304
|
+
|
|
305
|
+
if (model.showIcon) {
|
|
306
|
+
toggle.appendChild(createSwitcherIcon(model.iconName, 'adw-inline-view-switcher-icon'));
|
|
307
|
+
}
|
|
308
|
+
if (model.showLabel) {
|
|
309
|
+
const label = document.createElement('span');
|
|
310
|
+
label.className = 'adw-inline-view-switcher-label';
|
|
311
|
+
label.textContent = model.label;
|
|
312
|
+
toggle.appendChild(label);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const indicator = document.createElement('span');
|
|
316
|
+
indicator.className = 'adw-inline-view-switcher-indicator';
|
|
317
|
+
indicator.setAttribute('aria-hidden', 'true');
|
|
318
|
+
applyIndicator(indicator, model.badgeLabel, model.needsAttention);
|
|
319
|
+
toggle.appendChild(indicator);
|
|
320
|
+
|
|
321
|
+
if (model.tooltip) toggle.title = model.tooltip;
|
|
322
|
+
// An icon-only toggle has no visible text; the tooltip is also its
|
|
323
|
+
// accessible name, which is exactly what C's toggle does.
|
|
324
|
+
if (!model.showLabel) toggle.setAttribute('aria-label', model.tooltip || model.iconName);
|
|
325
|
+
applyDescription(toggle, model.description);
|
|
326
|
+
|
|
327
|
+
toggle.addEventListener('click', () => {
|
|
328
|
+
this._state.setSelected(model.pageIndex);
|
|
329
|
+
});
|
|
330
|
+
// The `relatedTarget` test is load-bearing: a toggle holds an icon,
|
|
331
|
+
// a label and an indicator, and the DOM fires `dragleave` +
|
|
332
|
+
// `dragenter` when the pointer crosses BETWEEN them — which cleared
|
|
333
|
+
// the dwell timer and re-armed a fresh 500ms, so a drag that merely
|
|
334
|
+
// slid across the toggle never switched the page. C has one
|
|
335
|
+
// `GtkDropControllerMotion` per BUTTON widget
|
|
336
|
+
// (adw-view-switcher-button.c:79-96), which has no internal edges.
|
|
337
|
+
toggle.addEventListener('dragenter', (event) => {
|
|
338
|
+
if (toggle.contains((event as DragEvent).relatedTarget as Node | null)) return;
|
|
339
|
+
this._state.dragEnter(model.pageIndex);
|
|
340
|
+
});
|
|
341
|
+
toggle.addEventListener('dragleave', (event) => {
|
|
342
|
+
if (toggle.contains((event as DragEvent).relatedTarget as Node | null)) return;
|
|
343
|
+
this._state.dragLeave(model.pageIndex);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
this._groupEl.appendChild(toggle);
|
|
347
|
+
return toggle;
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
this._render();
|
|
164
351
|
}
|
|
165
352
|
|
|
166
|
-
private
|
|
167
|
-
if (index === this._active) return;
|
|
168
|
-
this._active = index;
|
|
169
|
-
// Keep the attribute in sync without re-entering via the guarded
|
|
170
|
-
// attributeChangedCallback (value already matches, so it no-ops).
|
|
171
|
-
this.setAttribute('active', String(index));
|
|
353
|
+
private _onStateChange(change: ViewSwitcherStateChange): void {
|
|
172
354
|
this._render();
|
|
173
|
-
this.dispatchEvent(
|
|
355
|
+
this.dispatchEvent(
|
|
356
|
+
new CustomEvent('notify::active', {
|
|
357
|
+
bubbles: true,
|
|
358
|
+
detail: {
|
|
359
|
+
active: change.selected,
|
|
360
|
+
name: change.name,
|
|
361
|
+
toggleIndex: this.activeToggle,
|
|
362
|
+
interactive: change.interactive,
|
|
363
|
+
},
|
|
364
|
+
}),
|
|
365
|
+
);
|
|
174
366
|
}
|
|
175
367
|
|
|
176
368
|
private _render(): void {
|
|
177
|
-
|
|
178
|
-
this.classList.toggle('round', this.hasAttribute('round'));
|
|
179
|
-
|
|
180
|
-
// The bar carries the active display mode as a class — mirrors
|
|
181
|
-
// libadwaita's inline-view-switcher > toggle-group.{labels,icons,both}.
|
|
182
|
-
for (const mode of DISPLAY_MODES) this._groupEl.classList.toggle(mode, mode === this._displayMode);
|
|
369
|
+
if (!this._initialized) return;
|
|
183
370
|
|
|
184
|
-
|
|
185
|
-
const showLabel = this._displayMode !== 'icons';
|
|
371
|
+
this._applyStyleClasses();
|
|
186
372
|
|
|
187
|
-
|
|
188
|
-
|
|
373
|
+
const activeToggle = this.activeToggle;
|
|
374
|
+
this._toggles.forEach((toggle, toggleIndex) => {
|
|
375
|
+
const isActive = toggleIndex === activeToggle && activeToggle !== VIEW_SWITCHER_NO_SELECTION;
|
|
189
376
|
toggle.classList.toggle('active', isActive);
|
|
190
377
|
toggle.setAttribute('aria-selected', String(isActive));
|
|
191
378
|
toggle.tabIndex = isActive ? 0 : -1;
|
|
192
|
-
|
|
193
|
-
const iconEl = toggle.querySelector('.adw-inline-view-switcher-icon') as HTMLElement | null;
|
|
194
|
-
const labelEl = toggle.querySelector('.adw-inline-view-switcher-label') as HTMLElement | null;
|
|
195
|
-
const hasIcon = (toggle.dataset.icon ?? '').length > 0;
|
|
196
|
-
const title = toggle.dataset.title ?? '';
|
|
197
|
-
|
|
198
|
-
if (iconEl) iconEl.hidden = !showIcon || !hasIcon;
|
|
199
|
-
if (labelEl) labelEl.hidden = !showLabel || title.length === 0;
|
|
200
|
-
|
|
201
|
-
// In icons-only mode the toggle has no visible text — surface the
|
|
202
|
-
// title as an accessible name + a hover tooltip (the native widget
|
|
203
|
-
// sets the toggle tooltip to the page title in ICONS mode).
|
|
204
|
-
if (!showLabel && title) {
|
|
205
|
-
toggle.setAttribute('aria-label', title);
|
|
206
|
-
toggle.title = title;
|
|
207
|
-
} else {
|
|
208
|
-
toggle.removeAttribute('aria-label');
|
|
209
|
-
toggle.removeAttribute('title');
|
|
210
|
-
}
|
|
211
379
|
});
|
|
212
380
|
|
|
213
|
-
this.
|
|
214
|
-
|
|
381
|
+
const selected = this._state.selected;
|
|
382
|
+
this._panels.forEach((panel, index) => {
|
|
383
|
+
const isActive = index === selected;
|
|
215
384
|
panel.classList.toggle('active-view', isActive);
|
|
216
385
|
panel.hidden = !isActive;
|
|
217
386
|
});
|
|
387
|
+
|
|
388
|
+
this._reflectActive();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private _applyStyleClasses(): void {
|
|
392
|
+
// `css_classes_changed_cb` (adw-inline-view-switcher.c:528-545) forwards
|
|
393
|
+
// THREE classes onto the toggle group, not two: `flat`, `round` and
|
|
394
|
+
// `osd`. This element carried the first two and only onto the host, so an
|
|
395
|
+
// `osd` inline switcher — the one in a floating overlay, where the class
|
|
396
|
+
// exists to make it legible — styled nothing at all.
|
|
397
|
+
for (const name of ['flat', 'round', 'osd'] as const) {
|
|
398
|
+
const present = this.hasAttribute(name);
|
|
399
|
+
this.classList.toggle(name, present);
|
|
400
|
+
this._groupEl?.classList.toggle(name, present);
|
|
401
|
+
}
|
|
402
|
+
// The group carries the display-mode class, mirroring libadwaita's
|
|
403
|
+
// inline-view-switcher > toggle-group.{labels,icons,both} (:826-850).
|
|
404
|
+
for (const mode of INLINE_VIEW_SWITCHER_DISPLAY_MODES) {
|
|
405
|
+
this._groupEl.classList.toggle(mode, mode === this._displayMode);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private _reflectActive(): void {
|
|
410
|
+
const current = String(this._state.selected);
|
|
411
|
+
if (this.getAttribute('active') !== current) this.setAttribute('active', current);
|
|
218
412
|
}
|
|
219
413
|
}
|
|
220
414
|
|