@gjsify/adwaita-web 0.30.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
|
@@ -3,12 +3,28 @@
|
|
|
3
3
|
// a navigation stack controlled with push()/pop()/replace(); the visible page
|
|
4
4
|
// fills the view. Only <adw-navigation-page> children are managed.
|
|
5
5
|
//
|
|
6
|
+
// The stack machine itself is NOT here: it lives in `@gjsify/adwaita-core`'s
|
|
7
|
+
// `NavigationViewState` (ADR 0004), ported from the C and shared with the
|
|
8
|
+
// NativeScript renderer, which used to carry its own near-identical copy. This
|
|
9
|
+
// element is the DOM half — mount/unmount pages, toggle `hidden`, grow the
|
|
10
|
+
// automatic back button, translate stack changes into CustomEvents, and route
|
|
11
|
+
// Escape / Alt+Left into the core's shortcut handlers.
|
|
12
|
+
//
|
|
13
|
+
// The copy this replaces had drifted from libadwaita in ways nothing compared:
|
|
14
|
+
// `pop()` refused to pop a `can-pop="false"` page (the C documents the exact
|
|
15
|
+
// opposite at adw-navigation-view.c:2559-2561), `replace()` purged dynamic pages
|
|
16
|
+
// BEFORE resolving string entries so `replace(['tag-of-a-pushed-page'])` blanked
|
|
17
|
+
// the view outright, `popToTag()` was a loop of single pops that stopped early at
|
|
18
|
+
// a `can-pop="false"` page, and the back-button tooltip was hardcoded to "Back"
|
|
19
|
+
// where the C uses the previous page's title. See `navigation-view.spec.ts` for
|
|
20
|
+
// the vectors this element is now held to.
|
|
21
|
+
//
|
|
6
22
|
// Pages are declared as <adw-navigation-page> children (each with a `title` and
|
|
7
23
|
// optional `tag` attribute). The view snapshots them at connect time. Like
|
|
8
24
|
// Adw.NavigationView, a statically-added page is kept around but not shown until
|
|
9
|
-
// pushed
|
|
10
|
-
// element OR an existing page's tag (push-by-tag); a
|
|
11
|
-
// removed from the DOM again once it is popped.
|
|
25
|
+
// pushed, and adding a page while the stack is EMPTY pushes it automatically.
|
|
26
|
+
// push() accepts a page element OR an existing page's tag (push-by-tag); a
|
|
27
|
+
// dynamically-pushed page is removed from the DOM again once it is popped.
|
|
12
28
|
//
|
|
13
29
|
// Header Bar integration: mirroring libadwaita, when the visible page is above
|
|
14
30
|
// the root the view surfaces an automatic back button at the start of that
|
|
@@ -19,28 +35,42 @@
|
|
|
19
35
|
// Attributes (on <adw-navigation-view>):
|
|
20
36
|
// animate-transitions (boolean, default present — slide animation on push/pop;
|
|
21
37
|
// mirrors Adw.NavigationView:animate-transitions)
|
|
38
|
+
// pop-on-escape ("false" disables Escape-to-pop; mirrors
|
|
39
|
+
// Adw.NavigationView:pop-on-escape, which defaults to TRUE)
|
|
22
40
|
// Attributes (on <adw-navigation-page>):
|
|
23
|
-
// title (the page title — shown in the header bar's window title
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
41
|
+
// title (the page title — shown in the header bar's window title, and used as
|
|
42
|
+
// the NEXT page's back-button tooltip)
|
|
43
|
+
// tag (a unique identifier for push-by-tag; a duplicate is rejected)
|
|
44
|
+
// can-pop ("false" disables the back button and the keyboard shortcuts;
|
|
45
|
+
// mirrors AdwNavigationPage:can-pop — it does NOT disable pop())
|
|
27
46
|
// no-back-button (boolean — suppresses the automatic back button only)
|
|
28
|
-
// Events (CustomEvent, bubbles):
|
|
29
|
-
// `pushed`
|
|
30
|
-
//
|
|
31
|
-
// `popped` (detail = { tag }) after a page is popped — mirrors
|
|
47
|
+
// Events (CustomEvent, bubbles, detail = { page, tag }):
|
|
48
|
+
// `pushed` after a page is pushed — mirrors AdwNavigationView::pushed.
|
|
49
|
+
// `popped` after a page is popped, once per popped page, top-first — mirrors
|
|
32
50
|
// AdwNavigationView::popped.
|
|
33
|
-
// `replaced` after the whole stack is replaced —
|
|
34
|
-
// AdwNavigationView::replaced.
|
|
35
|
-
// `notify::visible-page`
|
|
36
|
-
//
|
|
51
|
+
// `replaced` (detail page/tag null) after the whole stack is replaced —
|
|
52
|
+
// mirrors AdwNavigationView::replaced.
|
|
53
|
+
// `notify::visible-page` whenever the visible page changes — mirrors the
|
|
54
|
+
// Adw.NavigationView:visible-page property.
|
|
37
55
|
// Reference: refs/libadwaita/src/adw-navigation-view.c (AdwNavigationView behaviour)
|
|
56
|
+
// Reference: refs/libadwaita/src/adw-back-button.c (the back button derivation)
|
|
38
57
|
// Reference: refs/adwaita-web/adwaita-web/docs/widgets/navigationview.md
|
|
39
58
|
// Copyright (c) 2022-2023 Purism SPC / GNOME contributors (libadwaita). LGPLv2.1+.
|
|
40
59
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
41
60
|
|
|
61
|
+
import { BACK_BUTTON_FALLBACK_TOOLTIP, NavigationViewState, describeNavigationDiagnostic } from '@gjsify/adwaita-core';
|
|
62
|
+
import type { AdwNavigationPageProps, NavigationDiagnostic, NavigationStackChange } from '@gjsify/adwaita-core';
|
|
63
|
+
|
|
64
|
+
/** The page properties the core owns, read off the element's attributes. */
|
|
65
|
+
function readPageProps(page: AdwNavigationPage): AdwNavigationPageProps {
|
|
66
|
+
return { tag: page.tag, title: page.title, canPop: page.canPop };
|
|
67
|
+
}
|
|
68
|
+
|
|
42
69
|
/** A single navigation page. Children of <adw-navigation-view>. */
|
|
43
70
|
export class AdwNavigationPage extends HTMLElement {
|
|
71
|
+
/** Set while the owning view writes an attribute back, so the sync cannot recurse. */
|
|
72
|
+
private _syncing = false;
|
|
73
|
+
|
|
44
74
|
static get observedAttributes() {
|
|
45
75
|
return ['title', 'tag', 'can-pop', 'no-back-button'];
|
|
46
76
|
}
|
|
@@ -62,7 +92,7 @@ export class AdwNavigationPage extends HTMLElement {
|
|
|
62
92
|
else this.setAttribute('tag', value);
|
|
63
93
|
}
|
|
64
94
|
|
|
65
|
-
/** Whether the page can be popped
|
|
95
|
+
/** Whether the page can be popped by a shortcut or the back button. */
|
|
66
96
|
get canPop(): boolean {
|
|
67
97
|
return this.getAttribute('can-pop') !== 'false';
|
|
68
98
|
}
|
|
@@ -70,22 +100,42 @@ export class AdwNavigationPage extends HTMLElement {
|
|
|
70
100
|
set canPop(value: boolean) {
|
|
71
101
|
this.setAttribute('can-pop', value ? 'true' : 'false');
|
|
72
102
|
}
|
|
103
|
+
|
|
104
|
+
attributeChangedCallback(name: string, previous: string | null): void {
|
|
105
|
+
if (this._syncing) return;
|
|
106
|
+
// The attributes are the authoring surface, but the view's tag index and
|
|
107
|
+
// back-button derivation read the CORE's copy — so a runtime change has to
|
|
108
|
+
// be pushed into it. Without this the back button only ever reflected the
|
|
109
|
+
// `can-pop` value it had at the last stack mutation.
|
|
110
|
+
const view = this.closest('adw-navigation-view') as AdwNavigationView | null;
|
|
111
|
+
view?.syncPageProperty(this, name, previous);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Put an attribute back the way it was, without re-entering the sync. */
|
|
115
|
+
revertAttribute(name: string, value: string | null): void {
|
|
116
|
+
this._syncing = true;
|
|
117
|
+
if (value === null) this.removeAttribute(name);
|
|
118
|
+
else this.setAttribute(name, value);
|
|
119
|
+
this._syncing = false;
|
|
120
|
+
}
|
|
73
121
|
}
|
|
74
122
|
|
|
75
123
|
export class AdwNavigationView extends HTMLElement {
|
|
76
124
|
private _pagesEl!: HTMLDivElement;
|
|
77
|
-
// Every page known to the view (static + dynamically pushed), in DOM order.
|
|
78
|
-
private _pages: AdwNavigationPage[] = [];
|
|
79
|
-
// The navigation stack — references into _pages, bottom-first.
|
|
80
|
-
private _stack: AdwNavigationPage[] = [];
|
|
81
|
-
// Pages added dynamically via push() (destroyed when popped off).
|
|
82
|
-
private _dynamic = new Set<AdwNavigationPage>();
|
|
83
125
|
// The back button injected into each visible page (so we can remove it).
|
|
84
|
-
private _backButtons = new WeakMap<AdwNavigationPage, HTMLElement>();
|
|
126
|
+
private readonly _backButtons = new WeakMap<AdwNavigationPage, HTMLElement>();
|
|
85
127
|
private _initialized = false;
|
|
86
128
|
|
|
129
|
+
private readonly _diagnose = (diagnostic: NavigationDiagnostic): void => {
|
|
130
|
+
console.warn(`[adw-navigation-view] ${describeNavigationDiagnostic(diagnostic)}`);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// The whole stack machine. Declared AFTER `_diagnose` — class fields run in
|
|
134
|
+
// declaration order, so the sink has to exist first.
|
|
135
|
+
private readonly _state = new NavigationViewState<AdwNavigationPage>({ onDiagnostic: this._diagnose });
|
|
136
|
+
|
|
87
137
|
static get observedAttributes() {
|
|
88
|
-
return ['animate-transitions'];
|
|
138
|
+
return ['animate-transitions', 'pop-on-escape'];
|
|
89
139
|
}
|
|
90
140
|
|
|
91
141
|
/** Whether push/pop transitions are animated. */
|
|
@@ -98,14 +148,48 @@ export class AdwNavigationView extends HTMLElement {
|
|
|
98
148
|
this.setAttribute('animate-transitions', value ? 'true' : 'false');
|
|
99
149
|
}
|
|
100
150
|
|
|
151
|
+
/** Whether pressing Escape pops the visible page (Adw.NavigationView:pop-on-escape). */
|
|
152
|
+
get popOnEscape(): boolean {
|
|
153
|
+
return this.getAttribute('pop-on-escape') !== 'false';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
set popOnEscape(value: boolean) {
|
|
157
|
+
this.setAttribute('pop-on-escape', value ? 'true' : 'false');
|
|
158
|
+
}
|
|
159
|
+
|
|
101
160
|
/** The currently-visible page (top of the stack), or null when empty. */
|
|
102
161
|
get visiblePage(): AdwNavigationPage | null {
|
|
103
|
-
return this.
|
|
162
|
+
return this._state.visiblePage;
|
|
104
163
|
}
|
|
105
164
|
|
|
106
165
|
/** The tag of the visible page, or null. */
|
|
107
166
|
get visiblePageTag(): string | null {
|
|
108
|
-
return this.
|
|
167
|
+
return this._state.visiblePageTag;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The number of pages on the navigation stack. */
|
|
171
|
+
get depth(): number {
|
|
172
|
+
return this._state.depth;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The navigation stack, bottom-first (Adw.NavigationView:navigation-stack). */
|
|
176
|
+
get navigationStack(): readonly AdwNavigationPage[] {
|
|
177
|
+
return this._state.stack;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Every page this view knows — static and dynamically pushed. */
|
|
181
|
+
get pages(): readonly AdwNavigationPage[] {
|
|
182
|
+
return this._state.pages;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Whether the automatic back button is shown (AdwBackButton's visibility rule). */
|
|
186
|
+
get canGoBack(): boolean {
|
|
187
|
+
return this._state.canGoBack();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The back button's tooltip — the previous page's title, or null when there is no button. */
|
|
191
|
+
get backButtonTooltip(): string | null {
|
|
192
|
+
return this._state.backButtonTooltip();
|
|
109
193
|
}
|
|
110
194
|
|
|
111
195
|
connectedCallback() {
|
|
@@ -113,187 +197,223 @@ export class AdwNavigationView extends HTMLElement {
|
|
|
113
197
|
this._initialized = true;
|
|
114
198
|
|
|
115
199
|
// Snapshot the declared <adw-navigation-page> children — they become the
|
|
116
|
-
// statically-added pages.
|
|
117
|
-
|
|
200
|
+
// statically-added pages. Moving them into the stack wrapper BEFORE they
|
|
201
|
+
// are registered keeps their DOM order and avoids a detach/attach flash.
|
|
202
|
+
const declared = Array.from(this.querySelectorAll(':scope > adw-navigation-page')) as AdwNavigationPage[];
|
|
118
203
|
|
|
119
204
|
this._pagesEl = document.createElement('div');
|
|
120
205
|
this._pagesEl.className = 'adw-navigation-view-pages';
|
|
121
|
-
|
|
122
|
-
this._pages = [];
|
|
123
|
-
this._stack = [];
|
|
124
|
-
|
|
125
|
-
for (const page of pages) {
|
|
206
|
+
for (const page of declared) {
|
|
126
207
|
page.classList.add('adw-navigation-page');
|
|
127
208
|
this._pagesEl.appendChild(page);
|
|
128
|
-
this._pages.push(page);
|
|
129
209
|
}
|
|
130
|
-
|
|
131
210
|
this.replaceChildren(this._pagesEl);
|
|
132
211
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
212
|
+
this._state.subscribe((change) => this._onChange(change));
|
|
213
|
+
this._state.setAnimateTransitions(this.animateTransitions);
|
|
214
|
+
this._state.setPopOnEscape(this.popOnEscape);
|
|
215
|
+
this.addEventListener('keydown', this._onKeyDown);
|
|
216
|
+
|
|
217
|
+
// add_page:1284 auto-pushes into an EMPTY stack, so the first declared page
|
|
218
|
+
// becomes visible — and it does so through push_to_stack, which is why the
|
|
219
|
+
// `pushed` / `notify::visible-page` events fire for it.
|
|
220
|
+
for (const page of declared) this._state.add(page, readPageProps(page));
|
|
221
|
+
// A declared page the core REJECTED (a duplicate tag) is not one of ours:
|
|
222
|
+
// it must leave the DOM, or it would sit in the stack wrapper unmanaged and
|
|
223
|
+
// therefore permanently visible.
|
|
224
|
+
for (const page of declared) {
|
|
225
|
+
if (!this._state.isRegistered(page)) this._detach(page);
|
|
136
226
|
}
|
|
137
227
|
|
|
138
228
|
this._syncClasses();
|
|
139
229
|
this._render();
|
|
140
230
|
}
|
|
141
231
|
|
|
142
|
-
attributeChangedCallback() {
|
|
232
|
+
attributeChangedCallback(name: string) {
|
|
143
233
|
if (!this._initialized) return;
|
|
144
|
-
|
|
145
|
-
|
|
234
|
+
if (name === 'pop-on-escape') {
|
|
235
|
+
this._state.setPopOnEscape(this.popOnEscape);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this._state.setAnimateTransitions(this.animateTransitions);
|
|
146
239
|
this._syncClasses();
|
|
147
240
|
}
|
|
148
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Push a page property change into the core. Called by
|
|
244
|
+
* {@link AdwNavigationPage.attributeChangedCallback}; a rename the core rejects
|
|
245
|
+
* (the tag is taken) is rolled back, because the attribute must not disagree
|
|
246
|
+
* with the tag index `push-by-tag` resolves against.
|
|
247
|
+
*/
|
|
248
|
+
syncPageProperty(page: AdwNavigationPage, name: string, previous: string | null): void {
|
|
249
|
+
if (!this._state.isRegistered(page)) return;
|
|
250
|
+
if (name === 'tag') {
|
|
251
|
+
this._state.setTag(page, page.tag);
|
|
252
|
+
if (this._state.tagOf(page) !== page.tag) page.revertAttribute('tag', previous);
|
|
253
|
+
} else if (name === 'title') {
|
|
254
|
+
this._state.setTitle(page, page.title);
|
|
255
|
+
} else if (name === 'can-pop') {
|
|
256
|
+
this._state.setCanPop(page, page.canPop);
|
|
257
|
+
}
|
|
258
|
+
this._render();
|
|
259
|
+
}
|
|
260
|
+
|
|
149
261
|
/**
|
|
150
262
|
* Add a static page without pushing it (mirrors AdwNavigationView.add). The
|
|
151
263
|
* view keeps a reference; the page becomes reachable via push-by-tag. Named
|
|
152
264
|
* `addPage` rather than `add` to avoid clashing with any DOM method.
|
|
153
265
|
*/
|
|
154
|
-
addPage(page: AdwNavigationPage):
|
|
155
|
-
|
|
156
|
-
page.classList.add('adw-navigation-page');
|
|
157
|
-
this._pagesEl.appendChild(page);
|
|
158
|
-
this._pages.push(page);
|
|
266
|
+
addPage(page: AdwNavigationPage): boolean {
|
|
267
|
+
const added = this._state.add(page, readPageProps(page));
|
|
159
268
|
this._render();
|
|
269
|
+
return added;
|
|
160
270
|
}
|
|
161
271
|
|
|
162
272
|
/**
|
|
163
|
-
* Remove a
|
|
164
|
-
*
|
|
273
|
+
* Remove a page (mirrors AdwNavigationView.remove). A page that is on the
|
|
274
|
+
* stack is removed once it is POPPED, not now. Named `removePage` to avoid
|
|
275
|
+
* overriding HTMLElement.remove() (which takes no arguments).
|
|
165
276
|
*/
|
|
166
|
-
removePage(page: AdwNavigationPage):
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (index === -1) return;
|
|
170
|
-
this._pages.splice(index, 1);
|
|
171
|
-
this._dynamic.delete(page);
|
|
172
|
-
if (page.parentNode === this._pagesEl) this._pagesEl.removeChild(page);
|
|
277
|
+
removePage(page: AdwNavigationPage): boolean {
|
|
278
|
+
const removed = this._state.remove(page);
|
|
279
|
+
if (removed && !this._state.isRegistered(page)) this._detach(page);
|
|
173
280
|
this._render();
|
|
281
|
+
return removed;
|
|
174
282
|
}
|
|
175
283
|
|
|
176
284
|
/**
|
|
177
285
|
* Push a page onto the navigation stack (mirrors AdwNavigationView.push).
|
|
178
286
|
* Accepts a page element or, when given a string, the tag of a known page
|
|
179
|
-
* (push-by-tag
|
|
180
|
-
*
|
|
287
|
+
* (push-by-tag). A page that is not already known is added dynamically and
|
|
288
|
+
* destroyed when it is popped.
|
|
181
289
|
*/
|
|
182
|
-
push(pageOrTag: AdwNavigationPage | string):
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
console.warn(`[adw-navigation-view] No page found for "${String(pageOrTag)}"`);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
if (this._stack.includes(page)) {
|
|
189
|
-
console.warn('[adw-navigation-view] Page is already in the navigation stack');
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
if (!this._pages.includes(page)) {
|
|
193
|
-
// A brand-new dynamic page — keep a reference + mark for cleanup.
|
|
194
|
-
page.classList.add('adw-navigation-page');
|
|
195
|
-
this._pagesEl.appendChild(page);
|
|
196
|
-
this._pages.push(page);
|
|
197
|
-
this._dynamic.add(page);
|
|
198
|
-
}
|
|
199
|
-
this._stack.push(page);
|
|
290
|
+
push(pageOrTag: AdwNavigationPage | string): boolean {
|
|
291
|
+
if (typeof pageOrTag === 'string') return this.pushByTag(pageOrTag);
|
|
292
|
+
const pushed = this._state.push(pageOrTag, readPageProps(pageOrTag));
|
|
200
293
|
this._render();
|
|
201
|
-
|
|
202
|
-
|
|
294
|
+
return pushed;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Push the page carrying `tag` (mirrors AdwNavigationView.push_by_tag). */
|
|
298
|
+
pushByTag(tag: string): boolean {
|
|
299
|
+
const pushed = this._state.pushByTag(tag);
|
|
300
|
+
this._render();
|
|
301
|
+
return pushed;
|
|
203
302
|
}
|
|
204
303
|
|
|
205
304
|
/**
|
|
206
305
|
* Pop the visible page off the stack (mirrors AdwNavigationView.pop).
|
|
207
|
-
* Returns true when a page was popped.
|
|
208
|
-
*
|
|
306
|
+
* Returns true when a page was popped. The root page cannot be popped.
|
|
307
|
+
* `can-pop` does NOT gate this — it gates the shortcuts and the back button.
|
|
209
308
|
*/
|
|
210
309
|
pop(): boolean {
|
|
211
|
-
|
|
212
|
-
const visible = this.visiblePage;
|
|
213
|
-
if (visible && !visible.canPop) return false;
|
|
214
|
-
|
|
215
|
-
const popped = this._stack.pop() as AdwNavigationPage;
|
|
216
|
-
// Destroy dynamically-pushed pages once they leave the stack.
|
|
217
|
-
if (this._dynamic.has(popped)) {
|
|
218
|
-
this._dynamic.delete(popped);
|
|
219
|
-
const index = this._pages.indexOf(popped);
|
|
220
|
-
if (index !== -1) this._pages.splice(index, 1);
|
|
221
|
-
if (popped.parentNode === this._pagesEl) this._pagesEl.removeChild(popped);
|
|
222
|
-
}
|
|
310
|
+
const popped = this._state.pop();
|
|
223
311
|
this._render();
|
|
224
|
-
|
|
225
|
-
const next = this.visiblePage;
|
|
226
|
-
this.dispatchEvent(
|
|
227
|
-
new CustomEvent('notify::visible-page', { bubbles: true, detail: { tag: next?.tag ?? null } }),
|
|
228
|
-
);
|
|
229
|
-
return true;
|
|
312
|
+
return popped;
|
|
230
313
|
}
|
|
231
314
|
|
|
232
|
-
/** Pop
|
|
315
|
+
/** Pop until `page` is visible, in ONE transition (mirrors AdwNavigationView.pop_to_page). */
|
|
316
|
+
popToPage(page: AdwNavigationPage): boolean {
|
|
317
|
+
const popped = this._state.popToPage(page);
|
|
318
|
+
this._render();
|
|
319
|
+
return popped;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Pop until the page carrying `tag` is visible (mirrors AdwNavigationView.pop_to_tag). */
|
|
233
323
|
popToTag(tag: string): boolean {
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
const index = this._stack.indexOf(target);
|
|
237
|
-
if (index === -1 || index === this._stack.length - 1) return false;
|
|
238
|
-
let popped = false;
|
|
239
|
-
while (this._stack.length - 1 > index) {
|
|
240
|
-
if (!this.pop()) break;
|
|
241
|
-
popped = true;
|
|
242
|
-
}
|
|
324
|
+
const popped = this._state.popToTag(tag);
|
|
325
|
+
this._render();
|
|
243
326
|
return popped;
|
|
244
327
|
}
|
|
245
328
|
|
|
246
329
|
/**
|
|
247
|
-
* Replace the whole navigation stack
|
|
248
|
-
*
|
|
330
|
+
* Replace the whole navigation stack (mirrors AdwNavigationView.replace). The
|
|
331
|
+
* last page becomes visible; the transition is never animated.
|
|
332
|
+
*
|
|
333
|
+
* String entries are resolved BEFORE anything is mutated — the C does the same
|
|
334
|
+
* in replace_with_tags:3136-3147, and resolving late is what used to lose a
|
|
335
|
+
* dynamically-pushed page and blank the view.
|
|
249
336
|
*/
|
|
250
|
-
replace(pages:
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (this.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
this._stack = [];
|
|
262
|
-
|
|
263
|
-
for (const entry of pages) {
|
|
264
|
-
const page = this._resolvePage(entry);
|
|
265
|
-
if (!page) continue;
|
|
266
|
-
if (!this._pages.includes(page)) {
|
|
267
|
-
page.classList.add('adw-navigation-page');
|
|
268
|
-
this._pagesEl.appendChild(page);
|
|
269
|
-
this._pages.push(page);
|
|
270
|
-
this._dynamic.add(page);
|
|
271
|
-
}
|
|
272
|
-
this._stack.push(page);
|
|
273
|
-
}
|
|
337
|
+
replace(pages: ReadonlyArray<AdwNavigationPage | string | null>): void {
|
|
338
|
+
const resolved = pages.map((entry) => {
|
|
339
|
+
if (typeof entry !== 'string') return entry ?? null;
|
|
340
|
+
const page = this._state.findPage(entry);
|
|
341
|
+
if (page === null) this._diagnose({ code: 'tag-not-found', tag: entry });
|
|
342
|
+
return page;
|
|
343
|
+
});
|
|
344
|
+
this._state.replace(resolved, readPageProps);
|
|
345
|
+
this._render();
|
|
346
|
+
}
|
|
274
347
|
|
|
348
|
+
/** Replace the stack with the pages carrying `tags` (mirrors AdwNavigationView.replace_with_tags). */
|
|
349
|
+
replaceWithTags(tags: readonly string[]): void {
|
|
350
|
+
this._state.replaceWithTags(tags);
|
|
275
351
|
this._render();
|
|
276
|
-
this.dispatchEvent(new CustomEvent('replaced', { bubbles: true }));
|
|
277
|
-
this.dispatchEvent(
|
|
278
|
-
new CustomEvent('notify::visible-page', { bubbles: true, detail: { tag: this.visiblePageTag } }),
|
|
279
|
-
);
|
|
280
352
|
}
|
|
281
353
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
354
|
+
/** The page with this tag, or null (mirrors AdwNavigationView.find_page). */
|
|
355
|
+
findPage(tag: string): AdwNavigationPage | null {
|
|
356
|
+
return this._state.findPage(tag);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** The page popping `page` would reveal (mirrors AdwNavigationView.get_previous_page). */
|
|
360
|
+
getPreviousPage(page: AdwNavigationPage): AdwNavigationPage | null {
|
|
361
|
+
return this._state.getPreviousPage(page);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private _onChange(change: NavigationStackChange<AdwNavigationPage>): void {
|
|
365
|
+
for (const page of change.removed) this._detach(page);
|
|
366
|
+
// The slide is a CSS animation on the incoming page, so there is no JS
|
|
367
|
+
// transition to wait for: settle the deferred destroy at once, exactly as
|
|
368
|
+
// adw_animation_skip does in the C when `animate` is FALSE.
|
|
369
|
+
for (const page of this._state.finishTransition()) this._detach(page);
|
|
370
|
+
this._render();
|
|
371
|
+
|
|
372
|
+
if (change.reason === 'add' || change.reason === 'push') {
|
|
373
|
+
this._dispatch('pushed', change.visiblePage);
|
|
374
|
+
} else if (change.reason === 'pop') {
|
|
375
|
+
for (const page of change.popped) this._dispatch('popped', page);
|
|
376
|
+
} else {
|
|
377
|
+
this._dispatch('replaced', null);
|
|
378
|
+
}
|
|
379
|
+
if (change.previousVisiblePage !== change.visiblePage) {
|
|
380
|
+
this._dispatch('notify::visible-page', change.visiblePage);
|
|
285
381
|
}
|
|
286
|
-
return pageOrTag;
|
|
287
382
|
}
|
|
288
383
|
|
|
384
|
+
private _dispatch(type: string, page: AdwNavigationPage | null): void {
|
|
385
|
+
this.dispatchEvent(new CustomEvent(type, { bubbles: true, detail: { page, tag: page?.tag ?? null } }));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private readonly _onKeyDown = (event: KeyboardEvent): void => {
|
|
389
|
+
if (event.defaultPrevented) return;
|
|
390
|
+
let result: 'stop' | 'propagate';
|
|
391
|
+
if (event.key === 'Escape') {
|
|
392
|
+
result = this._state.popFromEscape();
|
|
393
|
+
} else if (event.altKey && (event.key === 'ArrowLeft' || event.key === 'ArrowRight')) {
|
|
394
|
+
// back_forward_shortcut_cb:1192-1193 swaps the arrows under RTL; only
|
|
395
|
+
// the BACK direction is handled, since forward needs ::get-next-page.
|
|
396
|
+
const back = getComputedStyle(this).direction === 'rtl' ? 'ArrowRight' : 'ArrowLeft';
|
|
397
|
+
if (event.key !== back) return;
|
|
398
|
+
result = this._state.popFromShortcut();
|
|
399
|
+
} else {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (result === 'stop') {
|
|
403
|
+
event.preventDefault();
|
|
404
|
+
event.stopPropagation();
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
|
|
289
408
|
private _syncClasses(): void {
|
|
290
409
|
this.classList.toggle('animated', this.animateTransitions);
|
|
291
410
|
}
|
|
292
411
|
|
|
293
412
|
private _render(): void {
|
|
294
|
-
const visible = this.visiblePage;
|
|
413
|
+
const visible = this._state.visiblePage;
|
|
295
414
|
|
|
296
|
-
for (const page of this.
|
|
415
|
+
for (const page of this._state.pages) {
|
|
416
|
+
this._attach(page);
|
|
297
417
|
const isVisible = page === visible;
|
|
298
418
|
page.classList.toggle('visible-page', isVisible);
|
|
299
419
|
page.hidden = !isVisible;
|
|
@@ -305,15 +425,33 @@ export class AdwNavigationView extends HTMLElement {
|
|
|
305
425
|
if (visible) this._syncBackButton(visible);
|
|
306
426
|
}
|
|
307
427
|
|
|
428
|
+
private _attach(page: AdwNavigationPage): void {
|
|
429
|
+
if (page.parentNode === this._pagesEl) return;
|
|
430
|
+
page.classList.add('adw-navigation-page');
|
|
431
|
+
this._pagesEl.appendChild(page);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private _detach(page: AdwNavigationPage): void {
|
|
435
|
+
this._removeBackButton(page);
|
|
436
|
+
page.classList.remove('visible-page');
|
|
437
|
+
if (page.parentNode === this._pagesEl) this._pagesEl.removeChild(page);
|
|
438
|
+
}
|
|
439
|
+
|
|
308
440
|
/** Inject (or remove) the automatic back button on the visible page. */
|
|
309
441
|
private _syncBackButton(page: AdwNavigationPage): void {
|
|
310
|
-
|
|
311
|
-
const poppable = this._stack.indexOf(page) > 0 && page.canPop && !page.hasAttribute('no-back-button');
|
|
312
|
-
if (!poppable) {
|
|
442
|
+
if (!this._state.canGoBack() || page.hasAttribute('no-back-button')) {
|
|
313
443
|
this._removeBackButton(page);
|
|
314
444
|
return;
|
|
315
445
|
}
|
|
316
|
-
|
|
446
|
+
|
|
447
|
+
// The tooltip is the title of the page the button REVEALS, so it can change
|
|
448
|
+
// while the button itself stays put (adw-back-button.c query_tooltip:411-417).
|
|
449
|
+
const tooltip = this._state.backButtonTooltip() ?? BACK_BUTTON_FALLBACK_TOOLTIP;
|
|
450
|
+
const existing = this._backButtons.get(page);
|
|
451
|
+
if (existing) {
|
|
452
|
+
existing.setAttribute('tooltip', tooltip);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
317
455
|
|
|
318
456
|
// Find the page's header bar's start section. We surface the button into
|
|
319
457
|
// the first <adw-header-bar> inside the page content, the way
|
|
@@ -327,7 +465,7 @@ export class AdwNavigationView extends HTMLElement {
|
|
|
327
465
|
back.classList.add('adw-navigation-back-button');
|
|
328
466
|
back.setAttribute('icon', 'go-previous');
|
|
329
467
|
back.setAttribute('flat', '');
|
|
330
|
-
back.setAttribute('tooltip',
|
|
468
|
+
back.setAttribute('tooltip', tooltip);
|
|
331
469
|
back.addEventListener('click', () => this.pop());
|
|
332
470
|
// Place the back button at the very start of the header.
|
|
333
471
|
start.insertBefore(back, start.firstChild);
|