@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
|
@@ -21,10 +21,13 @@
|
|
|
21
21
|
// Reference: refs/adwaita-web/adwaita-web/scss/_toggle_group.scss
|
|
22
22
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
23
23
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the
|
|
24
|
-
// selection state machine composed from @gjsify/adwaita-core
|
|
24
|
+
// selection state machine composed from @gjsify/adwaita-core, the icon node
|
|
25
|
+
// from <adw-icon>.
|
|
25
26
|
|
|
26
27
|
import { ToggleGroupState } from '@gjsify/adwaita-core';
|
|
27
28
|
|
|
29
|
+
import { createAdwIcon } from './adw-icon.js';
|
|
30
|
+
|
|
28
31
|
/** A single toggle. Children of <adw-toggle-group>; consumed at connect time. */
|
|
29
32
|
export class AdwToggle extends HTMLElement {
|
|
30
33
|
static get observedAttributes() {
|
|
@@ -70,12 +73,7 @@ export class AdwToggleGroup extends HTMLElement {
|
|
|
70
73
|
|
|
71
74
|
const label = toggle.getAttribute('label') ?? '';
|
|
72
75
|
const icon = toggle.getAttribute('icon-name') ?? '';
|
|
73
|
-
if (icon)
|
|
74
|
-
const span = document.createElement('span');
|
|
75
|
-
span.className = `adw-icon adw-icon--${icon}`;
|
|
76
|
-
span.setAttribute('aria-hidden', 'true'); // decorative (mask-image only)
|
|
77
|
-
btn.appendChild(span);
|
|
78
|
-
}
|
|
76
|
+
if (icon) btn.appendChild(createAdwIcon(icon));
|
|
79
77
|
if (label) btn.appendChild(document.createTextNode(label));
|
|
80
78
|
|
|
81
79
|
// An icon-only toggle has no text — give it an accessible name.
|
|
@@ -1,17 +1,46 @@
|
|
|
1
|
-
// <adw-toolbar-view> — A content area framed by
|
|
1
|
+
// <adw-toolbar-view> — A content area framed by top and/or bottom bars
|
|
2
2
|
// (typically header bars). Mirrors Adw.ToolbarView.
|
|
3
3
|
// Slots: slot="top" (top bars, stacked), slot="bottom" (bottom bars), and any
|
|
4
4
|
// remaining children become the scrollable content.
|
|
5
|
-
//
|
|
5
|
+
// Attributes: top-bar-style / bottom-bar-style (flat | raised | raised-border),
|
|
6
|
+
// extend-content-to-top-edge, extend-content-to-bottom-edge (boolean).
|
|
7
|
+
//
|
|
8
|
+
// The four style classes libadwaita derives — `raised` and `border` on a bar box,
|
|
9
|
+
// `undershoot-top` / `undershoot-bottom` on the view — come from
|
|
10
|
+
// `@gjsify/adwaita-core`'s `toolbarViewClasses`. Neither renderer had any of them,
|
|
11
|
+
// so a toolbar view looked identical in all three styles and the scroll fade under
|
|
12
|
+
// a flat bar was missing entirely.
|
|
13
|
+
//
|
|
14
|
+
// The undershoot decision reads the ALLOCATED bar heights, exactly as
|
|
15
|
+
// `update_undershoots` reads `gtk_widget_get_height (self->top_bar)` — so the
|
|
16
|
+
// classes are recomputed whenever a bar box resizes, not once at connect time.
|
|
17
|
+
// The allocation itself stays with the browser: flexbox is the layout manager
|
|
18
|
+
// here, and `toolbarViewAllocate` is the specification it is measured against
|
|
19
|
+
// (see `@gjsify/adwaita-core/conformance`), not a second layout pass fighting it.
|
|
20
|
+
//
|
|
21
|
+
// Reference: refs/libadwaita/src/adw-toolbar-view.c (update_undershoots, style setters)
|
|
6
22
|
// Reference: refs/libadwaita/src/stylesheet/widgets/_toolbars.scss
|
|
23
|
+
// Reference: refs/adwaita-web/adwaita-web/docs/widgets/toolbarview.md
|
|
7
24
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
8
25
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
9
26
|
|
|
27
|
+
import {
|
|
28
|
+
ADW_TOOLBAR_BAR_CLASSES,
|
|
29
|
+
ADW_TOOLBAR_VIEW_CLASSES,
|
|
30
|
+
parseToolbarStyle,
|
|
31
|
+
toolbarViewClasses,
|
|
32
|
+
} from '@gjsify/adwaita-core';
|
|
33
|
+
|
|
10
34
|
export class AdwToolbarView extends HTMLElement {
|
|
11
35
|
private _topEl!: HTMLDivElement;
|
|
12
36
|
private _contentEl!: HTMLDivElement;
|
|
13
37
|
private _bottomEl!: HTMLDivElement;
|
|
14
38
|
private _initialized = false;
|
|
39
|
+
private _resize: ResizeObserver | null = null;
|
|
40
|
+
|
|
41
|
+
static get observedAttributes() {
|
|
42
|
+
return ['top-bar-style', 'bottom-bar-style', 'extend-content-to-top-edge', 'extend-content-to-bottom-edge'];
|
|
43
|
+
}
|
|
15
44
|
|
|
16
45
|
/** The top-bar container — append header bars here imperatively. */
|
|
17
46
|
get topBar(): HTMLDivElement {
|
|
@@ -29,31 +58,65 @@ export class AdwToolbarView extends HTMLElement {
|
|
|
29
58
|
}
|
|
30
59
|
|
|
31
60
|
connectedCallback() {
|
|
32
|
-
if (this._initialized)
|
|
33
|
-
|
|
61
|
+
if (!this._initialized) {
|
|
62
|
+
this._initialized = true;
|
|
63
|
+
|
|
64
|
+
const topChildren = Array.from(this.querySelectorAll(':scope > [slot="top"]'));
|
|
65
|
+
const bottomChildren = Array.from(this.querySelectorAll(':scope > [slot="bottom"]'));
|
|
66
|
+
const contentChildren = Array.from(this.childNodes).filter(
|
|
67
|
+
(n) => !topChildren.includes(n as Element) && !bottomChildren.includes(n as Element),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
this._topEl = document.createElement('div');
|
|
71
|
+
this._topEl.className = 'adw-toolbar-view-top';
|
|
72
|
+
for (const child of topChildren) this._topEl.appendChild(child);
|
|
34
73
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
(n) => !topChildren.includes(n as Element) && !bottomChildren.includes(n as Element),
|
|
39
|
-
);
|
|
74
|
+
this._contentEl = document.createElement('div');
|
|
75
|
+
this._contentEl.className = 'adw-toolbar-view-content';
|
|
76
|
+
for (const child of contentChildren) this._contentEl.appendChild(child);
|
|
40
77
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
78
|
+
this._bottomEl = document.createElement('div');
|
|
79
|
+
this._bottomEl.className = 'adw-toolbar-view-bottom';
|
|
80
|
+
for (const child of bottomChildren) this._bottomEl.appendChild(child);
|
|
44
81
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
for (const child of contentChildren) this._contentEl.appendChild(child);
|
|
82
|
+
this._topEl.hidden = this._topEl.childElementCount === 0;
|
|
83
|
+
this._bottomEl.hidden = this._bottomEl.childElementCount === 0;
|
|
48
84
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
85
|
+
this.replaceChildren(this._topEl, this._contentEl, this._bottomEl);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// `update_undershoots` runs from size_allocate, so the classes have to
|
|
89
|
+
// follow the bars' real heights rather than a one-shot read.
|
|
90
|
+
this._resize = new ResizeObserver(() => this._syncClasses());
|
|
91
|
+
this._resize.observe(this._topEl);
|
|
92
|
+
this._resize.observe(this._bottomEl);
|
|
93
|
+
this._syncClasses();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
disconnectedCallback() {
|
|
97
|
+
this._resize?.disconnect();
|
|
98
|
+
this._resize = null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
attributeChangedCallback() {
|
|
102
|
+
if (this._initialized) this._syncClasses();
|
|
103
|
+
}
|
|
52
104
|
|
|
53
|
-
|
|
54
|
-
|
|
105
|
+
private _syncClasses(): void {
|
|
106
|
+
const { view, topBar, bottomBar } = toolbarViewClasses({
|
|
107
|
+
topBarStyle: parseToolbarStyle(this.getAttribute('top-bar-style')),
|
|
108
|
+
bottomBarStyle: parseToolbarStyle(this.getAttribute('bottom-bar-style')),
|
|
109
|
+
extendContentToTopEdge: this.hasAttribute('extend-content-to-top-edge'),
|
|
110
|
+
extendContentToBottomEdge: this.hasAttribute('extend-content-to-bottom-edge'),
|
|
111
|
+
topBarHeight: this._topEl.offsetHeight,
|
|
112
|
+
bottomBarHeight: this._bottomEl.offsetHeight,
|
|
113
|
+
});
|
|
55
114
|
|
|
56
|
-
|
|
115
|
+
for (const cls of ADW_TOOLBAR_VIEW_CLASSES) this.classList.toggle(cls, view.includes(cls));
|
|
116
|
+
for (const cls of ADW_TOOLBAR_BAR_CLASSES) {
|
|
117
|
+
this._topEl.classList.toggle(cls, topBar.includes(cls));
|
|
118
|
+
this._bottomEl.classList.toggle(cls, bottomBar.includes(cls));
|
|
119
|
+
}
|
|
57
120
|
}
|
|
58
121
|
}
|
|
59
122
|
|
|
@@ -6,139 +6,168 @@
|
|
|
6
6
|
// their own pages (<adw-view-switcher>, <adw-inline-view-switcher>) but no
|
|
7
7
|
// standalone stack a separate switcher can drive.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
9
|
+
// The SELECTION itself — the name lookup, the guards on an index change, the
|
|
10
|
+
// first-visible-page auto-pick and which paths notify — is HEADLESS and lives in
|
|
11
|
+
// `@gjsify/adwaita-core` (ADR 0004) as {@link ViewStackState}, shared with the
|
|
12
|
+
// NativeScript twin and specced by the conformance vectors in
|
|
13
|
+
// `@gjsify/adwaita-core/conformance`. This element is the DOM half only: it
|
|
14
|
+
// turns declared markup into pages, toggles their visibility, reflects the
|
|
15
|
+
// selection to an attribute, and re-emits the change as a DOM event.
|
|
16
|
+
//
|
|
17
|
+
// Pages are declared as <adw-view-stack-page> children with `name`, `title`,
|
|
18
|
+
// `icon-name` and `hidden` attributes (their own children become the page body),
|
|
19
|
+
// OR added imperatively with `add(content, name, title?, icon?)`. The first
|
|
20
|
+
// VISIBLE page is shown automatically.
|
|
13
21
|
//
|
|
14
22
|
// Attributes:
|
|
15
23
|
// visible-child-name — the name of the page to show. Reading/writing it stays
|
|
16
24
|
// in sync with the `visibleChildName` property so an adaptive layout can
|
|
17
25
|
// drive the stack declaratively (mirrors the Adw.ViewStack GObject prop).
|
|
26
|
+
// An EMPTY value is a legal name lookup, not "no name".
|
|
18
27
|
// Properties (mirroring Adw.ViewStack):
|
|
19
|
-
// pages — readonly page descriptors ({ name, title, icon,
|
|
20
|
-
// visibleChildName — name of the visible page ('' when
|
|
21
|
-
// visibleChildIndex — zero-based index of the visible page (
|
|
28
|
+
// pages — readonly page descriptors ({ name, title, icon, content, visible }).
|
|
29
|
+
// visibleChildName — name of the visible page ('' when nothing is selected).
|
|
30
|
+
// visibleChildIndex — zero-based index of the visible page, -1 for none (guarded).
|
|
22
31
|
// visibleChild — the visible page's content element (or null).
|
|
23
32
|
// Methods:
|
|
24
33
|
// add(content, name, title?, icon?) — append a page; returns its descriptor.
|
|
25
34
|
// addTitled(content, name, title) — Adw.ViewStack.add_titled convenience.
|
|
35
|
+
// removePage(name) — Adw.ViewStack.remove; clears the selection
|
|
36
|
+
// when the removed page was the visible one.
|
|
37
|
+
// setPageVisible(name, visible) — AdwViewStackPage:visible; hiding the visible
|
|
38
|
+
// page falls back to the next visible one.
|
|
26
39
|
// Events:
|
|
27
|
-
// `notify::visible-child` (CustomEvent, bubbles,
|
|
28
|
-
//
|
|
29
|
-
//
|
|
40
|
+
// `notify::visible-child` (CustomEvent, bubbles,
|
|
41
|
+
// detail = { index, name, title, interactive }) whenever the visible page
|
|
42
|
+
// changes — including the auto-shown first page, exactly as
|
|
43
|
+
// `set_visible_child` notifies there (adw-view-stack.c:1038-1039). It used to
|
|
44
|
+
// stay silent on that path, which is why a bound switcher needed a manual
|
|
45
|
+
// `refresh()`. `interactive` is false for an auto-pick.
|
|
30
46
|
// Reference: refs/libadwaita/src/adw-view-stack.c (AdwViewStack behaviour)
|
|
31
47
|
// Reference: packages/nativescript-bridge/adwaita/src/widgets/adw-view-stack.ts (NS twin)
|
|
32
48
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
33
49
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
34
50
|
|
|
51
|
+
import { ViewStackState } from '@gjsify/adwaita-core';
|
|
52
|
+
import type { AdwViewStackPageInfo as CorePageInfo, ViewStackStateChange } from '@gjsify/adwaita-core';
|
|
53
|
+
|
|
35
54
|
/** A page descriptor exposed via `pages` (consumed by a bound switcher bar). */
|
|
36
|
-
export
|
|
37
|
-
/** Stable id used by visibleChildName + switcher binding. */
|
|
38
|
-
name: string;
|
|
39
|
-
/** Human-facing title shown by a bound switcher. */
|
|
40
|
-
title: string;
|
|
41
|
-
/** Symbolic icon name (no `-symbolic` suffix), or '' for none. */
|
|
42
|
-
icon: string;
|
|
43
|
-
/** The page's content container (the visibility-toggled node). */
|
|
44
|
-
element: HTMLElement;
|
|
45
|
-
}
|
|
55
|
+
export type AdwViewStackPageInfo = CorePageInfo<HTMLElement>;
|
|
46
56
|
|
|
47
57
|
export class AdwViewStack extends HTMLElement {
|
|
48
|
-
private
|
|
49
|
-
private _visibleIndex = 0;
|
|
58
|
+
private readonly _state = new ViewStackState<HTMLElement>();
|
|
50
59
|
private _initialized = false;
|
|
51
60
|
|
|
52
61
|
static get observedAttributes() {
|
|
53
62
|
return ['visible-child-name'];
|
|
54
63
|
}
|
|
55
64
|
|
|
65
|
+
constructor() {
|
|
66
|
+
super();
|
|
67
|
+
// Subscribing touches no attributes and no children, so it is legal in a
|
|
68
|
+
// custom element constructor — unlike anything DOM-shaped.
|
|
69
|
+
this._state.subscribe((change) => this._onStateChange(change));
|
|
70
|
+
}
|
|
71
|
+
|
|
56
72
|
/** All pages in declaration/add order. A bound switcher reads this. */
|
|
57
73
|
get pages(): readonly AdwViewStackPageInfo[] {
|
|
58
|
-
return this.
|
|
74
|
+
return this._state.pages;
|
|
59
75
|
}
|
|
60
76
|
|
|
61
|
-
/** Zero-based index of the visible page
|
|
77
|
+
/** Zero-based index of the visible page, `-1` when nothing is selected. */
|
|
62
78
|
get visibleChildIndex(): number {
|
|
63
|
-
return this.
|
|
79
|
+
return this._state.visibleIndex;
|
|
64
80
|
}
|
|
65
81
|
|
|
66
82
|
set visibleChildIndex(value: number) {
|
|
67
|
-
|
|
68
|
-
if (value === this._visibleIndex) return;
|
|
69
|
-
this._visibleIndex = value;
|
|
70
|
-
this._apply(true);
|
|
83
|
+
this._state.setVisibleIndex(value);
|
|
71
84
|
}
|
|
72
85
|
|
|
73
|
-
/** Name of the visible page, or '' when
|
|
86
|
+
/** Name of the visible page, or '' when nothing is selected. */
|
|
74
87
|
get visibleChildName(): string {
|
|
75
|
-
return this.
|
|
88
|
+
return this._state.visibleName;
|
|
76
89
|
}
|
|
77
90
|
|
|
78
|
-
set visibleChildName(name: string) {
|
|
79
|
-
|
|
80
|
-
if (index >= 0) this.visibleChildIndex = index;
|
|
91
|
+
set visibleChildName(name: string | null | undefined) {
|
|
92
|
+
this._state.setVisibleName(name);
|
|
81
93
|
}
|
|
82
94
|
|
|
83
|
-
/** The visible page's content element, or null when
|
|
95
|
+
/** The visible page's content element, or null when nothing is selected. */
|
|
84
96
|
get visibleChild(): HTMLElement | null {
|
|
85
|
-
return this.
|
|
97
|
+
return this._state.visiblePage?.content ?? null;
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
connectedCallback() {
|
|
89
101
|
if (this._initialized) return;
|
|
90
102
|
this._initialized = true;
|
|
91
103
|
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
// Read the declared selection BEFORE adopting any page: the auto-pick
|
|
105
|
+
// reflects its own name back into this very attribute, which would
|
|
106
|
+
// otherwise overwrite the author's value with page 0's name.
|
|
107
|
+
const initialName = this.getAttribute('visible-child-name');
|
|
108
|
+
|
|
109
|
+
// Adopt the declared <adw-view-stack-page> children by APPENDING them —
|
|
110
|
+
// never by replacing the page list. The old code assigned the parsed
|
|
111
|
+
// wrappers wholesale and then `replaceChildren()`d the subtree away, so a
|
|
112
|
+
// stack built imperatively before it entered the document
|
|
113
|
+
// (`createElement` → `add()` → `body.append()`) lost every page it had
|
|
114
|
+
// been given. libadwaita has no parse-once phase that can discard
|
|
115
|
+
// children: the buildable adds them one at a time
|
|
116
|
+
// (adw_view_stack_buildable_add_child, adw-view-stack.c:1666-1680) and
|
|
117
|
+
// `adw_view_stack_add` works at any point in the widget's life.
|
|
118
|
+
for (const pageEl of Array.from(this.querySelectorAll(':scope > adw-view-stack-page')) as HTMLElement[]) {
|
|
119
|
+
// A declared page with no `name` is the page named '' — C stores the
|
|
120
|
+
// name verbatim (adw-view-stack.c:1170), and an invented `page-<index>`
|
|
121
|
+
// is addressable by a name the author never wrote.
|
|
122
|
+
const name = pageEl.getAttribute('name') ?? '';
|
|
123
|
+
// Wrapping each page's content lets us toggle exactly one.
|
|
100
124
|
const element = document.createElement('div');
|
|
101
125
|
element.className = 'adw-view-stack-child';
|
|
102
126
|
element.dataset.name = name;
|
|
103
127
|
for (const child of Array.from(pageEl.childNodes)) element.appendChild(child);
|
|
104
|
-
return { name, title, icon, element };
|
|
105
|
-
});
|
|
106
128
|
|
|
107
|
-
|
|
108
|
-
|
|
129
|
+
this._state.addPage({
|
|
130
|
+
name,
|
|
131
|
+
title: pageEl.getAttribute('title'),
|
|
132
|
+
icon: pageEl.getAttribute('icon-name'),
|
|
133
|
+
content: element,
|
|
134
|
+
// `hidden` is the DOM spelling of AdwViewStackPage:visible, which
|
|
135
|
+
// gates the auto-pick and refuses selection (adw-view-stack.c:1149-1151, :2415).
|
|
136
|
+
visible: !pageEl.hasAttribute('hidden'),
|
|
137
|
+
});
|
|
138
|
+
this.replaceChild(element, pageEl);
|
|
139
|
+
}
|
|
109
140
|
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
this.
|
|
141
|
+
// An explicit `visible-child-name` selects the initial page. Only a
|
|
142
|
+
// MISSING attribute falls through to the auto-pick — an empty value is a
|
|
143
|
+
// legal lookup for a page named '' (g_strcmp0, adw-view-stack.c:901),
|
|
144
|
+
// which the old `initialName ? … : -1` falsy check silently dropped.
|
|
145
|
+
if (initialName !== null) this._state.setVisibleName(initialName, false);
|
|
146
|
+
this._applyVisibility();
|
|
147
|
+
this._reflectName();
|
|
116
148
|
}
|
|
117
149
|
|
|
118
150
|
attributeChangedCallback(name: string, _old: string | null, value: string | null) {
|
|
119
151
|
if (!this._initialized) return;
|
|
120
|
-
|
|
121
|
-
|
|
152
|
+
// Only a REMOVAL is ignored (there is no name to select). An empty value
|
|
153
|
+
// goes through the same lookup as the property setter — the declarative
|
|
154
|
+
// and imperative paths must not disagree on the same input.
|
|
155
|
+
if (name === 'visible-child-name' && value !== null && value !== this._state.visibleName) {
|
|
156
|
+
this._state.setVisibleName(value);
|
|
122
157
|
}
|
|
123
158
|
}
|
|
124
159
|
|
|
125
160
|
/**
|
|
126
|
-
* Append a page.
|
|
127
|
-
*
|
|
161
|
+
* Append a page. It becomes visible when nothing is selected yet, matching
|
|
162
|
+
* `Adw.ViewStack.add()` — and, unlike before, that auto-pick NOTIFIES.
|
|
128
163
|
*/
|
|
129
164
|
add(content: HTMLElement, name: string, title?: string, icon?: string): AdwViewStackPageInfo {
|
|
130
165
|
content.classList.add('adw-view-stack-child');
|
|
131
166
|
content.dataset.name = name;
|
|
132
|
-
const page
|
|
133
|
-
name,
|
|
134
|
-
title: title ?? name,
|
|
135
|
-
icon: (icon ?? '').replace(/-symbolic$/, ''),
|
|
136
|
-
element: content,
|
|
137
|
-
};
|
|
138
|
-
this._pages.push(page);
|
|
167
|
+
const page = this._state.addPage({ name, title, icon, content });
|
|
139
168
|
this.appendChild(content);
|
|
140
|
-
|
|
141
|
-
this.
|
|
169
|
+
this._applyVisibility();
|
|
170
|
+
this._reflectName();
|
|
142
171
|
return page;
|
|
143
172
|
}
|
|
144
173
|
|
|
@@ -147,26 +176,74 @@ export class AdwViewStack extends HTMLElement {
|
|
|
147
176
|
return this.add(content, name, title);
|
|
148
177
|
}
|
|
149
178
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
179
|
+
/**
|
|
180
|
+
* Remove the first page named `name` and detach its content. Returns whether
|
|
181
|
+
* anything was removed. When the removed page was the visible one the stack
|
|
182
|
+
* ends up showing NOTHING and emits no event — `stack_remove` clears
|
|
183
|
+
* `visible_child` without re-picking (adw-view-stack.c:1202-1203).
|
|
184
|
+
*/
|
|
185
|
+
removePage(name: string): boolean {
|
|
186
|
+
const content = this._state.pages[this._state.indexOfName(name)]?.content;
|
|
187
|
+
if (!this._state.removePage(name)) return false;
|
|
188
|
+
content?.remove();
|
|
189
|
+
this._applyVisibility();
|
|
190
|
+
this._reflectName();
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Show or hide a page (`AdwViewStackPage:visible`). Returns whether the
|
|
196
|
+
* SELECTION moved: hiding the visible page falls back to the next visible
|
|
197
|
+
* one, and making a page visible while nothing is selected selects it
|
|
198
|
+
* (`update_child_visible`, adw-view-stack.c:1061-1082).
|
|
199
|
+
*/
|
|
200
|
+
setPageVisible(name: string, visible: boolean): boolean {
|
|
201
|
+
const moved = this._state.setPageVisible(name, visible);
|
|
202
|
+
this._applyVisibility();
|
|
203
|
+
return moved;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private _onStateChange(change: ViewStackStateChange<HTMLElement>): void {
|
|
207
|
+
this._applyVisibility();
|
|
208
|
+
this._reflectName();
|
|
209
|
+
this.dispatchEvent(
|
|
210
|
+
new CustomEvent('notify::visible-child', {
|
|
211
|
+
bubbles: true,
|
|
212
|
+
detail: {
|
|
213
|
+
index: change.index,
|
|
214
|
+
name: change.name,
|
|
215
|
+
title: change.title,
|
|
216
|
+
interactive: change.interactive,
|
|
217
|
+
},
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private _applyVisibility(): void {
|
|
223
|
+
const selected = this._state.visibleIndex;
|
|
224
|
+
this._state.pages.forEach((page, index) => {
|
|
225
|
+
const element = page.content;
|
|
226
|
+
if (!element) return;
|
|
227
|
+
const isVisible = index === selected;
|
|
228
|
+
element.classList.toggle('active-view', isVisible);
|
|
229
|
+
element.hidden = !isVisible;
|
|
155
230
|
});
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private _reflectName(): void {
|
|
234
|
+
// With nothing selected there is no name to mirror — C's
|
|
235
|
+
// `adw_view_stack_get_visible_child_name` returns NULL there
|
|
236
|
+
// (adw-view-stack.c:2384), and a missing attribute is the DOM spelling of
|
|
237
|
+
// that. An EMPTY name is still a name and IS reflected: the old
|
|
238
|
+
// `if (current && …)` guard meant a stack of nameless pages never
|
|
239
|
+
// reflected its selection at all.
|
|
240
|
+
if (this._state.visibleIndex < 0) {
|
|
241
|
+
this.removeAttribute('visible-child-name');
|
|
242
|
+
return;
|
|
161
243
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
this.
|
|
165
|
-
new CustomEvent('notify::visible-child', {
|
|
166
|
-
bubbles: true,
|
|
167
|
-
detail: { index: this._visibleIndex, name: page?.name ?? '', title: page?.title ?? '' },
|
|
168
|
-
}),
|
|
169
|
-
);
|
|
244
|
+
const current = this._state.visibleName;
|
|
245
|
+
if (this.getAttribute('visible-child-name') !== current) {
|
|
246
|
+
this.setAttribute('visible-child-name', current);
|
|
170
247
|
}
|
|
171
248
|
}
|
|
172
249
|
}
|