@gjsify/adwaita-web 0.31.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adwaita-web.css +786 -272
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/adw-about-dialog.spec.d.ts +1 -0
- package/lib/types/adw-action-rows.spec.d.ts +1 -0
- package/lib/types/adw-avatar.spec.d.ts +1 -0
- package/lib/types/adw-banner.spec.d.ts +1 -0
- package/lib/types/adw-button-content.spec.d.ts +1 -0
- package/lib/types/adw-carousel.spec.d.ts +1 -0
- package/lib/types/adw-checks.spec.d.ts +1 -0
- package/lib/types/adw-header-bar.spec.d.ts +1 -0
- package/lib/types/adw-icon.spec.d.ts +1 -0
- package/lib/types/adw-progress-bar.spec.d.ts +1 -0
- package/lib/types/adw-sidebar.spec.d.ts +1 -0
- package/lib/types/adw-switch.spec.d.ts +1 -0
- package/lib/types/adw-wrap-box.spec.d.ts +1 -0
- package/lib/types/bottom-sheet.spec.d.ts +1 -0
- package/lib/types/breakpoints.d.ts +22 -0
- package/lib/types/breakpoints.spec.d.ts +1 -0
- package/lib/types/chrome.spec.d.ts +1 -0
- package/lib/types/elements/adw-about-dialog.d.ts +47 -8
- package/lib/types/elements/adw-action-row.d.ts +35 -0
- package/lib/types/elements/adw-avatar.d.ts +11 -0
- package/lib/types/elements/adw-bottom-sheet.d.ts +33 -4
- package/lib/types/elements/adw-button-content.d.ts +12 -0
- package/lib/types/elements/adw-button-row.d.ts +10 -1
- package/lib/types/elements/adw-carousel.d.ts +111 -8
- package/lib/types/elements/adw-checks.d.ts +57 -0
- package/lib/types/elements/adw-clamp.d.ts +4 -1
- package/lib/types/elements/adw-data-grid.d.ts +11 -31
- package/lib/types/elements/adw-drop-down.d.ts +17 -20
- package/lib/types/elements/adw-entry-row.d.ts +93 -3
- package/lib/types/elements/adw-expander-row.d.ts +7 -1
- package/lib/types/elements/adw-header-bar.d.ts +15 -0
- package/lib/types/elements/adw-icon.d.ts +25 -0
- package/lib/types/elements/adw-inline-view-switcher.d.ts +37 -11
- package/lib/types/elements/adw-menu-button.d.ts +6 -6
- package/lib/types/elements/adw-navigation-split-view.d.ts +6 -0
- package/lib/types/elements/adw-navigation-view.d.ts +63 -20
- package/lib/types/elements/adw-overlay-split-view.d.ts +38 -0
- package/lib/types/elements/adw-password-entry-row.d.ts +19 -10
- package/lib/types/elements/adw-popover.d.ts +58 -0
- package/lib/types/elements/adw-preferences-dialog.d.ts +57 -2
- package/lib/types/elements/adw-preferences-group.d.ts +31 -0
- package/lib/types/elements/adw-progress-bar.d.ts +38 -0
- package/lib/types/elements/adw-sidebar.d.ts +37 -5
- package/lib/types/elements/adw-split-button.d.ts +53 -7
- package/lib/types/elements/adw-switch-row.d.ts +11 -2
- package/lib/types/elements/adw-switch.d.ts +15 -0
- package/lib/types/elements/adw-tab-view.d.ts +129 -12
- package/lib/types/elements/adw-toolbar-view.d.ts +5 -0
- package/lib/types/elements/adw-view-stack.d.ts +27 -19
- package/lib/types/elements/adw-view-switcher-bar.d.ts +23 -7
- package/lib/types/elements/adw-view-switcher.d.ts +22 -11
- package/lib/types/elements/adw-window-title.d.ts +3 -1
- package/lib/types/elements/view-switcher-dom.d.ts +44 -0
- package/lib/types/entry-rows.spec.d.ts +1 -0
- package/lib/types/index.d.ts +7 -0
- package/lib/types/navigation-view.spec.d.ts +1 -0
- package/lib/types/preferences-search.d.ts +20 -0
- package/lib/types/preferences.spec.d.ts +1 -0
- package/lib/types/split-button.spec.d.ts +1 -0
- package/lib/types/split-views.spec.d.ts +1 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/lib/types/tab-view.spec.d.ts +1 -0
- package/lib/types/view-stack.spec.d.ts +1 -0
- package/lib/types/view-switcher.spec.d.ts +1 -0
- package/package.json +6 -6
- package/scss/_banner.scss +13 -0
- package/scss/_bottom_sheet.scss +5 -6
- package/scss/_button.scss +3 -26
- package/scss/_button_content.scss +28 -0
- package/scss/_checks.scss +228 -0
- package/scss/_clamp.scss +9 -3
- package/scss/_data_grid.scss +15 -7
- package/scss/_drop_down.scss +12 -55
- package/scss/_expander_row.scss +5 -50
- package/scss/_icon.scss +51 -0
- package/scss/_icons.generated.scss +6 -0
- package/scss/_inline_view_switcher.scss +34 -0
- package/scss/_labels.scss +135 -0
- package/scss/_menu_button.scss +10 -70
- package/scss/_popover.scss +186 -0
- package/scss/_preferences.scss +29 -0
- package/scss/_preferences_dialog.scss +17 -0
- package/scss/_progress_bar.scss +161 -0
- package/scss/_reset.scss +18 -7
- package/scss/_spinner.scss +7 -3
- package/scss/_split_button.scss +6 -50
- package/scss/_switch.scss +90 -0
- package/scss/_switch_row.scss +6 -52
- package/scss/_tab_view.scss +64 -0
- package/scss/_theme.scss +10 -0
- package/scss/_toolbar_view.scss +74 -0
- package/scss/_variables.scss +34 -0
- package/scss/_view_switcher.scss +40 -0
- package/scss/_view_switcher_bar.scss +37 -0
- package/scss/_wrap_box.scss +56 -6
- package/scss/adwaita-skin.scss +23 -0
- package/src/adw-about-dialog.spec.ts +350 -0
- package/src/adw-action-rows.spec.ts +342 -0
- package/src/adw-avatar.spec.ts +139 -0
- package/src/adw-banner.spec.ts +152 -0
- package/src/adw-button-content.spec.ts +183 -0
- package/src/adw-button-row.spec.ts +12 -6
- package/src/adw-carousel.spec.ts +389 -0
- package/src/adw-checks.spec.ts +368 -0
- package/src/adw-drop-down.spec.ts +123 -0
- package/src/adw-header-bar.spec.ts +83 -0
- package/src/adw-icon.spec.ts +167 -0
- package/src/adw-progress-bar.spec.ts +173 -0
- package/src/adw-sidebar.spec.ts +332 -0
- package/src/adw-switch.spec.ts +242 -0
- package/src/adw-tab-view.spec.ts +12 -3
- package/src/adw-view-switcher-bar.spec.ts +61 -13
- package/src/adw-wrap-box.spec.ts +194 -0
- package/src/bottom-sheet.spec.ts +325 -0
- package/src/breakpoints.spec.ts +147 -0
- package/src/breakpoints.ts +67 -0
- package/src/chrome.spec.ts +301 -0
- package/src/elements/adw-about-dialog.ts +223 -163
- package/src/elements/adw-action-row.ts +120 -10
- package/src/elements/adw-avatar.ts +48 -54
- package/src/elements/adw-banner.ts +46 -10
- package/src/elements/adw-bottom-sheet.ts +145 -60
- package/src/elements/adw-button-content.ts +107 -33
- package/src/elements/adw-button-row.ts +61 -37
- package/src/elements/adw-button.ts +5 -7
- package/src/elements/adw-carousel.ts +467 -105
- package/src/elements/adw-checks.ts +299 -0
- package/src/elements/adw-clamp.ts +80 -10
- package/src/elements/adw-combo-row.ts +15 -7
- package/src/elements/adw-data-grid.ts +43 -91
- package/src/elements/adw-drop-down.ts +117 -128
- package/src/elements/adw-entry-row.ts +404 -36
- package/src/elements/adw-expander-row.ts +61 -29
- package/src/elements/adw-header-bar.ts +31 -6
- package/src/elements/adw-icon.ts +148 -0
- package/src/elements/adw-inline-view-switcher.ts +240 -129
- package/src/elements/adw-menu-button.ts +93 -70
- package/src/elements/adw-navigation-split-view.ts +48 -7
- package/src/elements/adw-navigation-view.ts +286 -148
- package/src/elements/adw-overlay-split-view.ts +164 -14
- package/src/elements/adw-password-entry-row.ts +111 -89
- package/src/elements/adw-popover.ts +283 -0
- package/src/elements/adw-preferences-dialog.ts +120 -21
- package/src/elements/adw-preferences-group.ts +128 -22
- package/src/elements/adw-progress-bar.ts +189 -0
- package/src/elements/adw-sidebar.ts +301 -142
- package/src/elements/adw-spin-row.ts +11 -5
- package/src/elements/adw-spinner.ts +23 -8
- package/src/elements/adw-split-button.ts +371 -92
- package/src/elements/adw-status-page.ts +9 -9
- package/src/elements/adw-switch-row.ts +100 -30
- package/src/elements/adw-switch.ts +129 -0
- package/src/elements/adw-tab-view.ts +682 -129
- package/src/elements/adw-toggle-group.ts +5 -7
- package/src/elements/adw-toolbar-view.ts +84 -21
- package/src/elements/adw-view-stack.ts +162 -85
- package/src/elements/adw-view-switcher-bar.ts +163 -81
- package/src/elements/adw-view-switcher.ts +231 -112
- package/src/elements/adw-window-title.ts +44 -11
- package/src/elements/adw-wrap-box.ts +131 -35
- package/src/elements/view-switcher-dom.ts +108 -0
- package/src/entry-rows.spec.ts +391 -0
- package/src/index.ts +23 -0
- package/src/navigation-view.spec.ts +435 -0
- package/src/preferences-search.ts +135 -0
- package/src/preferences.spec.ts +250 -0
- package/src/source-view/adw-source-view.ts +2 -5
- package/src/split-button.spec.ts +470 -0
- package/src/split-views.spec.ts +309 -0
- package/src/styles.generated.ts +1 -1
- package/src/tab-view.spec.ts +451 -0
- package/src/test.browser.mts +49 -0
- package/src/view-stack.spec.ts +272 -0
- package/src/view-switcher.spec.ts +446 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
// DOM-level conformance tests for <adw-navigation-view>, driven by the SAME
|
|
2
|
+
// vectors the NativeScript renderer and the headless core assert against
|
|
3
|
+
// (`@gjsify/adwaita-core/conformance`).
|
|
4
|
+
//
|
|
5
|
+
// The two renderers used to carry independent copies of the whole stack machine,
|
|
6
|
+
// and both had drifted away from libadwaita in different directions. Four rows in
|
|
7
|
+
// the shared table fail against the pre-lift code here: `pop()` refused a
|
|
8
|
+
// `can-pop="false"` page (the C documents the opposite), `replace(['some-tag'])`
|
|
9
|
+
// blanked the view when the tag belonged to a dynamically-pushed page,
|
|
10
|
+
// `popToTag()` stopped early at a `can-pop="false"` page and emitted N separate
|
|
11
|
+
// transitions instead of one, and a duplicate `tag` was accepted silently.
|
|
12
|
+
// Nothing failed, because nothing compared them. This suite is that comparison.
|
|
13
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
NAVIGATION_VIEW_VECTORS,
|
|
17
|
+
collectNavigationState,
|
|
18
|
+
navigationEventLog,
|
|
19
|
+
runNavigationSteps,
|
|
20
|
+
} from '@gjsify/adwaita-core/conformance';
|
|
21
|
+
import type {
|
|
22
|
+
NavigationEventRecord,
|
|
23
|
+
NavigationPageId,
|
|
24
|
+
NavigationVectorAdapter,
|
|
25
|
+
} from '@gjsify/adwaita-core/conformance';
|
|
26
|
+
import type { AdwNavigationPageProps } from '@gjsify/adwaita-core';
|
|
27
|
+
|
|
28
|
+
import type { AdwNavigationPage, AdwNavigationView } from './elements/adw-navigation-view.js';
|
|
29
|
+
|
|
30
|
+
/** Write vector props onto the element — the attributes ARE the authoring surface. */
|
|
31
|
+
function applyProps(page: AdwNavigationPage, props?: AdwNavigationPageProps): void {
|
|
32
|
+
if (props === undefined) return;
|
|
33
|
+
if ('tag' in props) page.tag = props.tag ?? null;
|
|
34
|
+
if (props.title !== undefined) page.title = props.title;
|
|
35
|
+
if (props.canPop !== undefined) page.canPop = props.canPop;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Mount an empty view and adapt it to the vector driver. */
|
|
39
|
+
function mountView() {
|
|
40
|
+
const host = document.createElement('div');
|
|
41
|
+
document.body.appendChild(host);
|
|
42
|
+
const view = document.createElement('adw-navigation-view') as AdwNavigationView;
|
|
43
|
+
host.appendChild(view);
|
|
44
|
+
|
|
45
|
+
const handles = new Map<NavigationPageId, AdwNavigationPage>();
|
|
46
|
+
const ids = new Map<AdwNavigationPage, NavigationPageId>();
|
|
47
|
+
const page = (id: NavigationPageId): AdwNavigationPage => {
|
|
48
|
+
let element = handles.get(id);
|
|
49
|
+
if (element === undefined) {
|
|
50
|
+
element = document.createElement('adw-navigation-page') as AdwNavigationPage;
|
|
51
|
+
handles.set(id, element);
|
|
52
|
+
ids.set(element, id);
|
|
53
|
+
}
|
|
54
|
+
return element;
|
|
55
|
+
};
|
|
56
|
+
const idOf = (element: AdwNavigationPage | null | undefined): NavigationPageId | null =>
|
|
57
|
+
element === null || element === undefined ? null : (ids.get(element) ?? null);
|
|
58
|
+
|
|
59
|
+
const events: NavigationEventRecord[] = [];
|
|
60
|
+
for (const type of ['pushed', 'popped', 'replaced', 'notify::visible-page'] as const) {
|
|
61
|
+
view.addEventListener(type, (event) => {
|
|
62
|
+
const detail = (event as CustomEvent<{ page: AdwNavigationPage | null }>).detail;
|
|
63
|
+
events.push({ type, page: idOf(detail?.page ?? null) });
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const adapter: NavigationVectorAdapter<AdwNavigationPage> = {
|
|
68
|
+
page,
|
|
69
|
+
idOf,
|
|
70
|
+
// The slide is a CSS animation on the incoming page, so the element settles
|
|
71
|
+
// the deferred destroy inside its own change handler.
|
|
72
|
+
defersTransition: false,
|
|
73
|
+
add: (p, props) => {
|
|
74
|
+
applyProps(p, props);
|
|
75
|
+
return view.addPage(p);
|
|
76
|
+
},
|
|
77
|
+
remove: (p) => view.removePage(p),
|
|
78
|
+
push: (p, props) => {
|
|
79
|
+
applyProps(p, props);
|
|
80
|
+
return view.push(p);
|
|
81
|
+
},
|
|
82
|
+
pushByTag: (tag) => view.pushByTag(tag),
|
|
83
|
+
pop: () => view.pop(),
|
|
84
|
+
popToPage: (p) => view.popToPage(p),
|
|
85
|
+
popToTag: (tag) => view.popToTag(tag),
|
|
86
|
+
replace: (pages, props) => {
|
|
87
|
+
if (props !== undefined) {
|
|
88
|
+
for (const p of pages) {
|
|
89
|
+
if (p !== null) applyProps(p, props(p));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
view.replace([...pages]);
|
|
93
|
+
},
|
|
94
|
+
replaceWithTags: (tags) => view.replaceWithTags(tags),
|
|
95
|
+
// The property setters go through `setAttribute`, so these rows also
|
|
96
|
+
// exercise the attribute → core sync that keeps the back button live.
|
|
97
|
+
// A rejected rename is rolled back, which is what makes the boolean right.
|
|
98
|
+
setTag: (p, tag) => {
|
|
99
|
+
const before = p.tag;
|
|
100
|
+
p.tag = tag;
|
|
101
|
+
return p.tag !== before;
|
|
102
|
+
},
|
|
103
|
+
setTitle: (p, title) => {
|
|
104
|
+
const before = p.title;
|
|
105
|
+
p.title = title;
|
|
106
|
+
return p.title !== before;
|
|
107
|
+
},
|
|
108
|
+
setCanPop: (p, canPop) => {
|
|
109
|
+
const before = p.canPop;
|
|
110
|
+
p.canPop = canPop;
|
|
111
|
+
return p.canPop !== before;
|
|
112
|
+
},
|
|
113
|
+
setAnimateTransitions: (value) => {
|
|
114
|
+
view.animateTransitions = value;
|
|
115
|
+
},
|
|
116
|
+
setPopOnEscape: (value) => {
|
|
117
|
+
view.popOnEscape = value;
|
|
118
|
+
},
|
|
119
|
+
popFromShortcut: () => (dispatchKey(view, 'ArrowLeft', true) ? 'stop' : 'propagate'),
|
|
120
|
+
popFromEscape: () => (dispatchKey(view, 'Escape', false) ? 'stop' : 'propagate'),
|
|
121
|
+
finishTransition: () => [],
|
|
122
|
+
stack: () => view.navigationStack,
|
|
123
|
+
pages: () => view.pages,
|
|
124
|
+
visiblePage: () => view.visiblePage,
|
|
125
|
+
visiblePageTag: () => view.visiblePageTag,
|
|
126
|
+
depth: () => view.depth,
|
|
127
|
+
animateTransitions: () => view.animateTransitions,
|
|
128
|
+
popOnEscape: () => view.popOnEscape,
|
|
129
|
+
canGoBack: () => view.canGoBack,
|
|
130
|
+
backButtonTooltip: () => view.backButtonTooltip,
|
|
131
|
+
findPage: (tag) => view.findPage(tag),
|
|
132
|
+
getPreviousPage: (p) => view.getPreviousPage(p),
|
|
133
|
+
// `removeOnPop` is core-only; its DOM-visible consequence is the page
|
|
134
|
+
// leaving `pages` (and the document), which every row still asserts.
|
|
135
|
+
pageState: (p) => ({
|
|
136
|
+
registered: view.pages.includes(p),
|
|
137
|
+
tag: p.tag,
|
|
138
|
+
title: p.title,
|
|
139
|
+
canPop: p.canPop,
|
|
140
|
+
}),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return { adapter, view, host, events, page, idOf };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Send a real key event and report whether the view consumed it. The keyboard is
|
|
148
|
+
* how `pop_shortcut_cb` / `escape_shortcut_cb` are reachable from the DOM, so the
|
|
149
|
+
* shortcut vectors drive the actual listener rather than a method behind it.
|
|
150
|
+
*/
|
|
151
|
+
function dispatchKey(view: AdwNavigationView, key: string, altKey: boolean): boolean {
|
|
152
|
+
const event = new KeyboardEvent('keydown', { key, altKey, bubbles: true, cancelable: true });
|
|
153
|
+
view.dispatchEvent(event);
|
|
154
|
+
return event.defaultPrevented;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Which page element is currently shown. */
|
|
158
|
+
function renderedPage(view: AdwNavigationView): AdwNavigationPage | null {
|
|
159
|
+
const shown = Array.from(view.querySelectorAll('adw-navigation-page')).filter(
|
|
160
|
+
(page) => !(page as HTMLElement).hidden,
|
|
161
|
+
);
|
|
162
|
+
expect(shown.length <= 1).toBe(true);
|
|
163
|
+
return (shown[0] as AdwNavigationPage | undefined) ?? null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Build a page carrying a header bar, so the automatic back button has somewhere to go. */
|
|
167
|
+
function pageWithHeaderBar(title: string, tag?: string): AdwNavigationPage {
|
|
168
|
+
const page = document.createElement('adw-navigation-page') as AdwNavigationPage;
|
|
169
|
+
page.setAttribute('title', title);
|
|
170
|
+
if (tag !== undefined) page.setAttribute('tag', tag);
|
|
171
|
+
page.appendChild(document.createElement('adw-header-bar'));
|
|
172
|
+
return page;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The injected back button on the visible page, if any. */
|
|
176
|
+
function backButton(view: AdwNavigationView): HTMLElement | null {
|
|
177
|
+
return view.querySelector('.adw-navigation-back-button') as HTMLElement | null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export const AdwNavigationViewTest = async () => {
|
|
181
|
+
await describe('adw-navigation-view stack (libadwaita conformance vectors)', async () => {
|
|
182
|
+
for (const vector of NAVIGATION_VIEW_VECTORS) {
|
|
183
|
+
await it(`${vector.name} — ${vector.rule}`, () => {
|
|
184
|
+
const { adapter, host, events } = mountView();
|
|
185
|
+
|
|
186
|
+
for (const outcome of runNavigationSteps(vector.steps, adapter)) {
|
|
187
|
+
if (outcome.expected === undefined) continue;
|
|
188
|
+
expect({ at: outcome.index, result: outcome.result }).toStrictEqual({
|
|
189
|
+
at: outcome.index,
|
|
190
|
+
result: outcome.expected,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const check of collectNavigationState(vector.expect, adapter)) {
|
|
195
|
+
expect({ [check.label]: check.actual }).toStrictEqual({ [check.label]: check.expected });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (vector.expect.changes !== undefined) {
|
|
199
|
+
expect(events).toStrictEqual(navigationEventLog(vector.expect.changes));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
host.remove();
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await describe('adw-navigation-view declared markup', async () => {
|
|
208
|
+
await it('auto-pushes the first declared page and announces it', () => {
|
|
209
|
+
const host = document.createElement('div');
|
|
210
|
+
const view = document.createElement('adw-navigation-view') as AdwNavigationView;
|
|
211
|
+
view.append(pageWithHeaderBar('Home', 'root'), pageWithHeaderBar('Detail', 'detail'));
|
|
212
|
+
|
|
213
|
+
// Subscribe BEFORE connecting: the auto-push happens at connect time and
|
|
214
|
+
// used to emit nothing at all, so a listener never saw the view fill.
|
|
215
|
+
const seen: string[] = [];
|
|
216
|
+
view.addEventListener('pushed', () => seen.push('pushed'));
|
|
217
|
+
view.addEventListener('notify::visible-page', (event) => {
|
|
218
|
+
seen.push(`visible:${(event as CustomEvent<{ tag: string | null }>).detail.tag}`);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
host.appendChild(view);
|
|
222
|
+
document.body.appendChild(host);
|
|
223
|
+
|
|
224
|
+
expect(seen).toStrictEqual(['pushed', 'visible:root']);
|
|
225
|
+
expect(view.visiblePageTag).toBe('root');
|
|
226
|
+
expect(view.depth).toBe(1);
|
|
227
|
+
expect(view.pages).toHaveLength(2);
|
|
228
|
+
expect(renderedPage(view)).toBe(view.findPage('root'));
|
|
229
|
+
host.remove();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
await it('shows only the top of the stack', () => {
|
|
233
|
+
const host = document.createElement('div');
|
|
234
|
+
const view = document.createElement('adw-navigation-view') as AdwNavigationView;
|
|
235
|
+
view.append(pageWithHeaderBar('Home', 'root'), pageWithHeaderBar('Detail', 'detail'));
|
|
236
|
+
host.appendChild(view);
|
|
237
|
+
document.body.appendChild(host);
|
|
238
|
+
|
|
239
|
+
view.pushByTag('detail');
|
|
240
|
+
expect(renderedPage(view)).toBe(view.findPage('detail'));
|
|
241
|
+
view.pop();
|
|
242
|
+
expect(renderedPage(view)).toBe(view.findPage('root'));
|
|
243
|
+
host.remove();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
await it('drops a declared page whose tag collides', () => {
|
|
247
|
+
const host = document.createElement('div');
|
|
248
|
+
const view = document.createElement('adw-navigation-view') as AdwNavigationView;
|
|
249
|
+
const duplicate = pageWithHeaderBar('Other', 'root');
|
|
250
|
+
view.append(pageWithHeaderBar('Home', 'root'), duplicate);
|
|
251
|
+
host.appendChild(view);
|
|
252
|
+
document.body.appendChild(host);
|
|
253
|
+
|
|
254
|
+
expect(view.pages).toHaveLength(1);
|
|
255
|
+
expect(duplicate.isConnected).toBe(false);
|
|
256
|
+
expect(renderedPage(view)).toBe(view.findPage('root'));
|
|
257
|
+
host.remove();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
await describe('adw-navigation-view back button', async () => {
|
|
262
|
+
await it('appears above the root and carries the PREVIOUS page title', () => {
|
|
263
|
+
const host = document.createElement('div');
|
|
264
|
+
const view = document.createElement('adw-navigation-view') as AdwNavigationView;
|
|
265
|
+
view.append(pageWithHeaderBar('Home', 'root'), pageWithHeaderBar('Detail', 'detail'));
|
|
266
|
+
host.appendChild(view);
|
|
267
|
+
document.body.appendChild(host);
|
|
268
|
+
|
|
269
|
+
expect(backButton(view)).toBe(null);
|
|
270
|
+
view.pushByTag('detail');
|
|
271
|
+
// The old element hardcoded "Back" here, where the C uses the title of
|
|
272
|
+
// the page the button reveals (adw-back-button.c query_tooltip:411-417).
|
|
273
|
+
expect(backButton(view)?.getAttribute('tooltip')).toBe('Home');
|
|
274
|
+
|
|
275
|
+
view.pop();
|
|
276
|
+
expect(backButton(view)).toBe(null);
|
|
277
|
+
host.remove();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
await it('falls back to "Back" when the previous page has no title', () => {
|
|
281
|
+
const host = document.createElement('div');
|
|
282
|
+
const view = document.createElement('adw-navigation-view') as AdwNavigationView;
|
|
283
|
+
const root = pageWithHeaderBar('', 'root');
|
|
284
|
+
view.append(root, pageWithHeaderBar('Detail', 'detail'));
|
|
285
|
+
host.appendChild(view);
|
|
286
|
+
document.body.appendChild(host);
|
|
287
|
+
|
|
288
|
+
view.pushByTag('detail');
|
|
289
|
+
expect(backButton(view)?.getAttribute('tooltip')).toBe('Back');
|
|
290
|
+
host.remove();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
await it('re-syncs when can-pop changes at runtime', () => {
|
|
294
|
+
// The regression: `can-pop` was in observedAttributes but nothing acted
|
|
295
|
+
// on it, so the button only reflected the last stack mutation's value.
|
|
296
|
+
const host = document.createElement('div');
|
|
297
|
+
const view = document.createElement('adw-navigation-view') as AdwNavigationView;
|
|
298
|
+
const detail = pageWithHeaderBar('Detail', 'detail');
|
|
299
|
+
view.append(pageWithHeaderBar('Home', 'root'), detail);
|
|
300
|
+
host.appendChild(view);
|
|
301
|
+
document.body.appendChild(host);
|
|
302
|
+
|
|
303
|
+
view.pushByTag('detail');
|
|
304
|
+
expect(backButton(view)).not.toBe(null);
|
|
305
|
+
|
|
306
|
+
detail.canPop = false;
|
|
307
|
+
expect(backButton(view)).toBe(null);
|
|
308
|
+
expect(view.canGoBack).toBe(false);
|
|
309
|
+
|
|
310
|
+
detail.canPop = true;
|
|
311
|
+
expect(backButton(view)).not.toBe(null);
|
|
312
|
+
host.remove();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await it('follows a runtime title change on the previous page', () => {
|
|
316
|
+
const host = document.createElement('div');
|
|
317
|
+
const view = document.createElement('adw-navigation-view') as AdwNavigationView;
|
|
318
|
+
const root = pageWithHeaderBar('Home', 'root');
|
|
319
|
+
view.append(root, pageWithHeaderBar('Detail', 'detail'));
|
|
320
|
+
host.appendChild(view);
|
|
321
|
+
document.body.appendChild(host);
|
|
322
|
+
|
|
323
|
+
view.pushByTag('detail');
|
|
324
|
+
root.title = 'Start';
|
|
325
|
+
expect(backButton(view)?.getAttribute('tooltip')).toBe('Start');
|
|
326
|
+
host.remove();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
await it('is suppressed by no-back-button without disabling the pop', () => {
|
|
330
|
+
const host = document.createElement('div');
|
|
331
|
+
const view = document.createElement('adw-navigation-view') as AdwNavigationView;
|
|
332
|
+
const detail = pageWithHeaderBar('Detail', 'detail');
|
|
333
|
+
detail.setAttribute('no-back-button', '');
|
|
334
|
+
view.append(pageWithHeaderBar('Home', 'root'), detail);
|
|
335
|
+
host.appendChild(view);
|
|
336
|
+
document.body.appendChild(host);
|
|
337
|
+
|
|
338
|
+
view.pushByTag('detail');
|
|
339
|
+
expect(backButton(view)).toBe(null);
|
|
340
|
+
// AdwHeaderBar:show-back-button removes the BUTTON, not the shortcut.
|
|
341
|
+
expect(view.canGoBack).toBe(true);
|
|
342
|
+
expect(view.pop()).toBe(true);
|
|
343
|
+
host.remove();
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
await describe('adw-navigation-view DOM lifecycle', async () => {
|
|
348
|
+
await it('takes a dynamically-pushed page out of the document when it is popped', () => {
|
|
349
|
+
const { adapter, view, host, page } = mountView();
|
|
350
|
+
adapter.add(page('root'));
|
|
351
|
+
adapter.push(page('sheet'));
|
|
352
|
+
expect(page('sheet').isConnected).toBe(true);
|
|
353
|
+
|
|
354
|
+
expect(view.pop()).toBe(true);
|
|
355
|
+
expect(page('sheet').isConnected).toBe(false);
|
|
356
|
+
expect(view.pages).toHaveLength(1);
|
|
357
|
+
host.remove();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
await it('keeps a statically-added page in the document across push and pop', () => {
|
|
361
|
+
const { adapter, view, host, page } = mountView();
|
|
362
|
+
adapter.add(page('root'));
|
|
363
|
+
adapter.add(page('detail'));
|
|
364
|
+
adapter.push(page('detail'));
|
|
365
|
+
expect(view.pop()).toBe(true);
|
|
366
|
+
expect(page('detail').isConnected).toBe(true);
|
|
367
|
+
expect((page('detail') as HTMLElement).hidden).toBe(true);
|
|
368
|
+
host.remove();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
await it('mirrors animate-transitions onto the animated class and the core', () => {
|
|
372
|
+
const { adapter, view, host, page } = mountView();
|
|
373
|
+
adapter.add(page('root'));
|
|
374
|
+
expect(view.classList.contains('animated')).toBe(true);
|
|
375
|
+
|
|
376
|
+
view.animateTransitions = false;
|
|
377
|
+
expect(view.classList.contains('animated')).toBe(false);
|
|
378
|
+
expect(view.animateTransitions).toBe(false);
|
|
379
|
+
host.remove();
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
await describe('adw-navigation-view keyboard shortcuts', async () => {
|
|
384
|
+
await it('pops on Escape and stops the event', () => {
|
|
385
|
+
const { adapter, view, host, page } = mountView();
|
|
386
|
+
adapter.add(page('root'));
|
|
387
|
+
adapter.add(page('detail'));
|
|
388
|
+
adapter.push(page('detail'));
|
|
389
|
+
|
|
390
|
+
expect(dispatchKey(view, 'Escape', false)).toBe(true);
|
|
391
|
+
expect(view.depth).toBe(1);
|
|
392
|
+
host.remove();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
await it('leaves Escape alone when pop-on-escape is off', () => {
|
|
396
|
+
const { adapter, view, host, page } = mountView();
|
|
397
|
+
adapter.add(page('root'));
|
|
398
|
+
adapter.add(page('detail'));
|
|
399
|
+
adapter.push(page('detail'));
|
|
400
|
+
view.popOnEscape = false;
|
|
401
|
+
|
|
402
|
+
expect(dispatchKey(view, 'Escape', false)).toBe(false);
|
|
403
|
+
expect(view.depth).toBe(2);
|
|
404
|
+
host.remove();
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
await it('stops Alt+Left WITHOUT popping a can-pop="false" page', () => {
|
|
408
|
+
// GDK_EVENT_STOP so the key never reaches an enclosing navigation view
|
|
409
|
+
// (pop_shortcut_cb:1150-1152).
|
|
410
|
+
const { adapter, view, host, page } = mountView();
|
|
411
|
+
adapter.add(page('root'));
|
|
412
|
+
adapter.add(page('detail'));
|
|
413
|
+
adapter.push(page('detail'));
|
|
414
|
+
page('detail').canPop = false;
|
|
415
|
+
|
|
416
|
+
expect(dispatchKey(view, 'ArrowLeft', true)).toBe(true);
|
|
417
|
+
expect(view.depth).toBe(2);
|
|
418
|
+
// …while a manual pop() still works, which is the whole point of can-pop.
|
|
419
|
+
expect(view.pop()).toBe(true);
|
|
420
|
+
expect(view.depth).toBe(1);
|
|
421
|
+
host.remove();
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
await it('ignores an unrelated key', () => {
|
|
425
|
+
const { adapter, view, host, page } = mountView();
|
|
426
|
+
adapter.add(page('root'));
|
|
427
|
+
adapter.add(page('detail'));
|
|
428
|
+
adapter.push(page('detail'));
|
|
429
|
+
|
|
430
|
+
expect(dispatchKey(view, 'ArrowLeft', false)).toBe(false);
|
|
431
|
+
expect(view.depth).toBe(2);
|
|
432
|
+
host.remove();
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Preferences search for the browser renderer — the DOM half only.
|
|
2
|
+
//
|
|
3
|
+
// The pipeline itself (case fold, markup parse, the three-filter corpus, the
|
|
4
|
+
// `page → group` subtitle) is headless and lives in `@gjsify/adwaita-core`;
|
|
5
|
+
// what belongs here is the one genuinely renderer-specific step: reading a
|
|
6
|
+
// `<adw-preferences-page>` subtree and describing it as the renderer-free tree
|
|
7
|
+
// the core reasons over.
|
|
8
|
+
//
|
|
9
|
+
// Reference: refs/libadwaita/src/adw-preferences-dialog.c
|
|
10
|
+
// (adw_preferences_dialog_init's page/group/row model, :617-641)
|
|
11
|
+
// Reference: refs/libadwaita/src/adw-action-row.c, adw-entry-row.c,
|
|
12
|
+
// adw-expander-row.c, adw-button-row.c (which rows ARE action rows)
|
|
13
|
+
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
searchPreferences,
|
|
17
|
+
type PreferencesSearchGroup,
|
|
18
|
+
type PreferencesSearchPage,
|
|
19
|
+
type PreferencesSearchResult,
|
|
20
|
+
type PreferencesSearchRow,
|
|
21
|
+
type SearchPreferencesOptions,
|
|
22
|
+
} from '@gjsify/adwaita-core';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The rows that derive from `AdwActionRow` and therefore have their SUBTITLE
|
|
26
|
+
* searched (`filter_search_results`, adw-preferences-dialog.c:140-146).
|
|
27
|
+
*
|
|
28
|
+
* A tag-name set rather than an `instanceof` chain, because the browser
|
|
29
|
+
* elements are all direct `HTMLElement` subclasses — the GObject hierarchy is
|
|
30
|
+
* not reproduced in the DOM, so the distinction has to be stated. It is not
|
|
31
|
+
* "has a subtitle": `adw-entry-row`, `adw-expander-row` and `adw-button-row`
|
|
32
|
+
* all derive from `AdwPreferencesRow` in C (adw-entry-row.c:90,
|
|
33
|
+
* adw-expander-row.c:72, adw-button-row.c:74), so the text typed into an entry
|
|
34
|
+
* row is deliberately NOT searchable.
|
|
35
|
+
*/
|
|
36
|
+
const ACTION_ROW_TAGS: ReadonlySet<string> = new Set([
|
|
37
|
+
'adw-action-row',
|
|
38
|
+
'adw-switch-row',
|
|
39
|
+
'adw-combo-row',
|
|
40
|
+
'adw-spin-row',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* `ADW_IS_PREFERENCES_ROW` for the browser elements — the first clause of
|
|
45
|
+
* `row_has_title` (adw-preferences-group.c:158-173).
|
|
46
|
+
*
|
|
47
|
+
* Spelled out rather than inferred from the `adw-` prefix, because a group's
|
|
48
|
+
* boxed list legitimately holds other things: a bare `<adw-button>` dropped in
|
|
49
|
+
* as a footer is not a preferences row and must not be searchable.
|
|
50
|
+
*/
|
|
51
|
+
const PREFERENCES_ROW_TAGS: ReadonlySet<string> = new Set([
|
|
52
|
+
...ACTION_ROW_TAGS,
|
|
53
|
+
'adw-entry-row',
|
|
54
|
+
'adw-password-entry-row',
|
|
55
|
+
'adw-expander-row',
|
|
56
|
+
'adw-button-row',
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
/** `gtk_widget_get_visible` for a DOM element. */
|
|
60
|
+
function isVisible(element: Element): boolean {
|
|
61
|
+
return !(element instanceof HTMLElement && element.hidden);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A group element's rows, whether or not it has upgraded into its listbox yet. */
|
|
65
|
+
function groupRows(group: Element): Element[] {
|
|
66
|
+
const listbox = group.querySelector(':scope > .adw-preferences-group-listbox');
|
|
67
|
+
return Array.from((listbox ?? group).children).filter((child) =>
|
|
68
|
+
PREFERENCES_ROW_TAGS.has(child.tagName.toLowerCase()),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Describe one `<adw-*-row>` element for the search corpus. */
|
|
73
|
+
function describeRow(row: Element): PreferencesSearchRow<Element> {
|
|
74
|
+
const subtitle = row.getAttribute('subtitle');
|
|
75
|
+
return {
|
|
76
|
+
title: row.getAttribute('title') ?? '',
|
|
77
|
+
subtitle,
|
|
78
|
+
visible: isVisible(row),
|
|
79
|
+
isActionRow: ACTION_ROW_TAGS.has(row.tagName.toLowerCase()),
|
|
80
|
+
// The browser rows paint their title as TEXT, not as Pango markup, so
|
|
81
|
+
// the string compared is the string shown. Rendering markup is a
|
|
82
|
+
// separate, still-open gap; closing it flips this one flag.
|
|
83
|
+
useMarkup: false,
|
|
84
|
+
useUnderline: false,
|
|
85
|
+
ref: row,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Describe one `<adw-preferences-group>` element. */
|
|
90
|
+
function describeGroup(group: Element): PreferencesSearchGroup<Element> {
|
|
91
|
+
return {
|
|
92
|
+
title: group.getAttribute('title') ?? '',
|
|
93
|
+
visible: isVisible(group),
|
|
94
|
+
rows: groupRows(group).map(describeRow),
|
|
95
|
+
ref: group,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Describe every `<adw-preferences-page>` under `root` as the tree
|
|
101
|
+
* `searchPreferences` indexes.
|
|
102
|
+
*
|
|
103
|
+
* Page identity is read off the element itself — which is why the dialog keeps
|
|
104
|
+
* the page elements in its subtree instead of hoisting their children out. A
|
|
105
|
+
* flattened dialog has no page titles, and `create_search_row_subtitle` needs
|
|
106
|
+
* them the moment a second page exists.
|
|
107
|
+
*/
|
|
108
|
+
export function describePreferencesPages(root: ParentNode): PreferencesSearchPage<Element>[] {
|
|
109
|
+
return Array.from(root.querySelectorAll('adw-preferences-page')).map((page) => ({
|
|
110
|
+
name: page.getAttribute('name'),
|
|
111
|
+
title: page.getAttribute('title') ?? '',
|
|
112
|
+
iconName: page.getAttribute('icon-name'),
|
|
113
|
+
useUnderline: page.hasAttribute('use-underline'),
|
|
114
|
+
visible: isVisible(page),
|
|
115
|
+
description: page.getAttribute('description') ?? '',
|
|
116
|
+
groups: Array.from(page.querySelectorAll('adw-preferences-group')).map(describeGroup),
|
|
117
|
+
ref: page,
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Search the preferences pages under `root`, returning the result rows a
|
|
123
|
+
* `search_results` list box would be bound to.
|
|
124
|
+
*
|
|
125
|
+
* Each result carries the `<adw-preferences-page>` and the row element, so a
|
|
126
|
+
* caller can reproduce `search_result_activated_cb`: reveal the page, focus
|
|
127
|
+
* the row.
|
|
128
|
+
*/
|
|
129
|
+
export function searchPreferencesDom(
|
|
130
|
+
root: ParentNode,
|
|
131
|
+
query: string,
|
|
132
|
+
options?: SearchPreferencesOptions,
|
|
133
|
+
): PreferencesSearchResult<Element>[] {
|
|
134
|
+
return searchPreferences(describePreferencesPages(root), query, options);
|
|
135
|
+
}
|