@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,446 @@
|
|
|
1
|
+
// DOM-level conformance tests for <adw-view-switcher>,
|
|
2
|
+
// <adw-inline-view-switcher> and <adw-view-switcher-bar>, driven by the SAME
|
|
3
|
+
// vectors the core suite and the NativeScript suite assert against
|
|
4
|
+
// (`@gjsify/adwaita-core/conformance`).
|
|
5
|
+
//
|
|
6
|
+
// The two renderers used to carry independent copies of everything a switcher
|
|
7
|
+
// derives, and FIVE libadwaita rules lived in neither of them: the
|
|
8
|
+
// button-visibility predicate `visible && (title != NULL || icon-name != NULL)`,
|
|
9
|
+
// the `image-missing` icon fallback, the bar's `reveal && more-than-one-visible-
|
|
10
|
+
// page` gate, the inline switcher's two index spaces, and the badge /
|
|
11
|
+
// needs-attention label with the screen-reader description AdwIndicatorBin
|
|
12
|
+
// derives from them. On top of that the browser switcher CLAMPED an out-of-range
|
|
13
|
+
// index into range where libadwaita refuses. Nothing failed, because nothing
|
|
14
|
+
// compared them. This suite is that comparison.
|
|
15
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
INLINE_TOGGLE_VECTORS,
|
|
19
|
+
INLINE_TOOLTIP_VECTORS,
|
|
20
|
+
VIEW_SWITCHER_BAR_REVEAL_VECTORS,
|
|
21
|
+
VIEW_SWITCHER_BUTTON_VECTORS,
|
|
22
|
+
VIEW_SWITCHER_SELECTION_VECTORS,
|
|
23
|
+
} from '@gjsify/adwaita-core/conformance';
|
|
24
|
+
import type {
|
|
25
|
+
ViewSwitcherVectorChange,
|
|
26
|
+
ViewSwitcherVectorOp,
|
|
27
|
+
ViewSwitcherVectorPage,
|
|
28
|
+
} from '@gjsify/adwaita-core/conformance';
|
|
29
|
+
import { VIEW_SWITCHER_DRAG_SWITCH_DELAY, normalizeIconName } from '@gjsify/adwaita-core';
|
|
30
|
+
|
|
31
|
+
import type { AdwViewSwitcher } from './elements/adw-view-switcher.js';
|
|
32
|
+
import type { AdwInlineViewSwitcher } from './elements/adw-inline-view-switcher.js';
|
|
33
|
+
import type { AdwViewSwitcherBar } from './elements/adw-view-switcher-bar.js';
|
|
34
|
+
import type { AdwViewStack } from './elements/adw-view-stack.js';
|
|
35
|
+
|
|
36
|
+
/** Declare one vector page as markup, keeping "absent" and "empty" distinct. */
|
|
37
|
+
function declarePage(tag: string, page: ViewSwitcherVectorPage): HTMLElement {
|
|
38
|
+
const element = document.createElement(tag);
|
|
39
|
+
element.setAttribute('name', page.name);
|
|
40
|
+
// `getAttribute` returns null for an absent attribute, which IS C's NULL —
|
|
41
|
+
// so an absent or explicitly-null title must not become `title=""`.
|
|
42
|
+
if (typeof page.title === 'string') element.setAttribute('title', page.title);
|
|
43
|
+
if (typeof page.iconName === 'string') element.setAttribute('icon-name', page.iconName);
|
|
44
|
+
if (page.visible === false) element.setAttribute('hidden', '');
|
|
45
|
+
if (page.useUnderline) element.setAttribute('use-underline', '');
|
|
46
|
+
if (page.badgeNumber) element.setAttribute('badge-number', String(page.badgeNumber));
|
|
47
|
+
if (page.needsAttention) element.setAttribute('needs-attention', '');
|
|
48
|
+
|
|
49
|
+
const body = document.createElement('p');
|
|
50
|
+
body.className = 'page-body';
|
|
51
|
+
body.textContent = page.name;
|
|
52
|
+
element.appendChild(body);
|
|
53
|
+
return element;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A mounted switcher, with every selection change recorded in vector shape. */
|
|
57
|
+
interface Mounted<T extends HTMLElement> {
|
|
58
|
+
element: T;
|
|
59
|
+
host: HTMLElement;
|
|
60
|
+
changes: ViewSwitcherVectorChange[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Mount an <adw-view-switcher> whose pages are DECLARED as markup. */
|
|
64
|
+
function mountSwitcher(pages: readonly ViewSwitcherVectorPage[], policy?: string): Mounted<AdwViewSwitcher> {
|
|
65
|
+
const host = document.createElement('div');
|
|
66
|
+
document.body.appendChild(host);
|
|
67
|
+
|
|
68
|
+
const element = document.createElement('adw-view-switcher') as AdwViewSwitcher;
|
|
69
|
+
if (policy !== undefined) element.setAttribute('policy', policy);
|
|
70
|
+
for (const page of pages) element.appendChild(declarePage('adw-view-switcher-page', page));
|
|
71
|
+
|
|
72
|
+
const changes: ViewSwitcherVectorChange[] = [];
|
|
73
|
+
// The listener is attached BEFORE the element connects so the auto-pick
|
|
74
|
+
// notification is observable — the switcher used to be silent there.
|
|
75
|
+
element.addEventListener('notify::visible-child', (event) => {
|
|
76
|
+
const detail = (event as CustomEvent<{ index: number; name: string; title: string; interactive: boolean }>)
|
|
77
|
+
.detail;
|
|
78
|
+
changes.push({
|
|
79
|
+
selected: detail.index,
|
|
80
|
+
name: detail.name,
|
|
81
|
+
title: detail.title,
|
|
82
|
+
interactive: detail.interactive,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
host.appendChild(element);
|
|
87
|
+
return { element, host, changes };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Mount an <adw-inline-view-switcher> whose pages are DECLARED as markup. */
|
|
91
|
+
function mountInline(pages: readonly ViewSwitcherVectorPage[], displayMode?: string): Mounted<AdwInlineViewSwitcher> {
|
|
92
|
+
const host = document.createElement('div');
|
|
93
|
+
document.body.appendChild(host);
|
|
94
|
+
|
|
95
|
+
const element = document.createElement('adw-inline-view-switcher') as AdwInlineViewSwitcher;
|
|
96
|
+
if (displayMode !== undefined) element.setAttribute('display-mode', displayMode);
|
|
97
|
+
for (const page of pages) element.appendChild(declarePage('adw-view-stack-page', page));
|
|
98
|
+
|
|
99
|
+
const changes: ViewSwitcherVectorChange[] = [];
|
|
100
|
+
element.addEventListener('notify::active', (event) => {
|
|
101
|
+
const detail = (event as CustomEvent<{ active: number; name: string; interactive: boolean }>).detail;
|
|
102
|
+
changes.push({ selected: detail.active, name: detail.name, title: '', interactive: detail.interactive });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
host.appendChild(element);
|
|
106
|
+
return { element, host, changes };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Apply one vector op through the element's own public surface. */
|
|
110
|
+
function applyOp(element: AdwViewSwitcher, op: ViewSwitcherVectorOp): void {
|
|
111
|
+
switch (op.kind) {
|
|
112
|
+
case 'selectIndex':
|
|
113
|
+
element.active = op.index;
|
|
114
|
+
break;
|
|
115
|
+
case 'selectName':
|
|
116
|
+
element.visibleChildName = op.name;
|
|
117
|
+
break;
|
|
118
|
+
case 'setPageVisible':
|
|
119
|
+
element.setPageVisible(op.name, op.visible);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The rendered buttons of a switcher, in page order. */
|
|
125
|
+
function buttonsOf(element: HTMLElement, selector: string): HTMLElement[] {
|
|
126
|
+
return Array.from(element.querySelectorAll(selector)) as HTMLElement[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The text of a button's parts, `null` when the part is absent. */
|
|
130
|
+
function partText(button: HTMLElement, selector: string): string | null {
|
|
131
|
+
const part = button.querySelector(selector) as HTMLElement | null;
|
|
132
|
+
return part ? (part.textContent ?? '') : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const AdwViewSwitcherTest = async () => {
|
|
136
|
+
await describe('adw-view-switcher buttons (libadwaita conformance vectors)', async () => {
|
|
137
|
+
for (const vector of VIEW_SWITCHER_BUTTON_VECTORS) {
|
|
138
|
+
// A `selected: -1` row cannot be staged here: this element bundles its
|
|
139
|
+
// own stack, whose auto-pick takes the first visible page, and the only
|
|
140
|
+
// way to reach "nothing selected" is to hide every page — which would
|
|
141
|
+
// change the very `visible` flags the row is asserting. The core suite
|
|
142
|
+
// drives that row directly.
|
|
143
|
+
if (vector.selected < 0) continue;
|
|
144
|
+
await it(vector.rule, () => {
|
|
145
|
+
const { element, host } = mountSwitcher(vector.pages, vector.policy);
|
|
146
|
+
// The vectors carry an explicit selection; drive it through the
|
|
147
|
+
// element rather than assuming the auto-pick landed there.
|
|
148
|
+
element.active = vector.selected;
|
|
149
|
+
|
|
150
|
+
const buttons = buttonsOf(element, '.adw-view-switcher-button');
|
|
151
|
+
expect(buttons).toHaveLength(vector.buttons.length);
|
|
152
|
+
|
|
153
|
+
vector.buttons.forEach((model, index) => {
|
|
154
|
+
const button = buttons[index]!;
|
|
155
|
+
// The button-visibility rule, which BOTH ports lacked.
|
|
156
|
+
expect(button.hidden).toBe(!model.visible);
|
|
157
|
+
expect(button.getAttribute('aria-selected')).toBe(String(model.selected));
|
|
158
|
+
expect(partText(button, '.adw-view-switcher-label')).toBe(model.label);
|
|
159
|
+
// The icon node always exists and always names an icon —
|
|
160
|
+
// `image-missing` when the page has none.
|
|
161
|
+
const icon = button.querySelector('.adw-icon') as HTMLElement;
|
|
162
|
+
expect(icon.classList.contains(`adw-icon--${normalizeIconName(model.iconName)}`)).toBe(true);
|
|
163
|
+
expect(partText(button, '.adw-view-switcher-indicator')).toBe(model.badgeLabel);
|
|
164
|
+
expect(button.getAttribute('aria-description')).toBe(model.description || null);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// The policy style class lives on the single `viewswitcher` node.
|
|
168
|
+
expect(element.classList.contains(vector.policy)).toBe(true);
|
|
169
|
+
host.remove();
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await it('hides a titleless, iconless button in the LAYOUT, not just visually', () => {
|
|
174
|
+
// The `[hidden]` UA rule loses to the component's own author
|
|
175
|
+
// `display: inline-flex` unless the stylesheet re-asserts it — the
|
|
176
|
+
// same cascade trap the switcher BAR was fixed for.
|
|
177
|
+
const { element, host } = mountSwitcher([{ name: 'a', title: 'A' }, { name: 'b' }]);
|
|
178
|
+
const buttons = buttonsOf(element, '.adw-view-switcher-button');
|
|
179
|
+
expect(getComputedStyle(buttons[1]!).display).toBe('none');
|
|
180
|
+
expect(getComputedStyle(buttons[0]!).display).not.toBe('none');
|
|
181
|
+
host.remove();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await describe('adw-view-switcher selection (libadwaita conformance vectors)', async () => {
|
|
186
|
+
for (const vector of VIEW_SWITCHER_SELECTION_VECTORS) {
|
|
187
|
+
await it(vector.rule, () => {
|
|
188
|
+
const { element, host, changes } = mountSwitcher(vector.pages);
|
|
189
|
+
expect(changes).toStrictEqual([...vector.setupChanges]);
|
|
190
|
+
|
|
191
|
+
changes.length = 0;
|
|
192
|
+
for (const op of vector.ops) applyOp(element, op);
|
|
193
|
+
|
|
194
|
+
expect(changes).toStrictEqual([...vector.changes]);
|
|
195
|
+
expect(element.active).toBe(vector.selected);
|
|
196
|
+
expect(element.visibleChildName).toBe(vector.selectedName);
|
|
197
|
+
|
|
198
|
+
// The DOM must agree with the model: exactly the selected page's
|
|
199
|
+
// body is shown, and none at all when the selection is -1.
|
|
200
|
+
const bodies = buttonsOf(element, '.adw-view-switcher-page');
|
|
201
|
+
const shown = bodies.map((body) => !(body as HTMLElement).hidden);
|
|
202
|
+
const expectedShown = vector.pages.map((_page, index) => index === vector.selected);
|
|
203
|
+
expect(shown).toStrictEqual(expectedShown);
|
|
204
|
+
|
|
205
|
+
// The reflected attribute must not claim a selection that was
|
|
206
|
+
// refused — the old element clamped instead, so `active="99"`
|
|
207
|
+
// silently became the last page.
|
|
208
|
+
expect(element.getAttribute('active')).toBe(String(vector.selected));
|
|
209
|
+
host.remove();
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
await it('dispatches notify::policy, which the header only ever promised', () => {
|
|
214
|
+
const { element, host } = mountSwitcher([{ name: 'a', title: 'A' }]);
|
|
215
|
+
const policies: string[] = [];
|
|
216
|
+
element.addEventListener('notify::policy', (event) => {
|
|
217
|
+
policies.push((event as CustomEvent<{ policy: string }>).detail.policy);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
element.policy = 'wide';
|
|
221
|
+
expect(element.policy).toBe('wide');
|
|
222
|
+
element.policy = 'wide'; // idempotent — no second event
|
|
223
|
+
expect(policies).toStrictEqual(['wide']);
|
|
224
|
+
|
|
225
|
+
// An unrecognised value is REFUSED and keeps the current policy.
|
|
226
|
+
element.setAttribute('policy', 'bogus');
|
|
227
|
+
expect(element.policy).toBe('wide');
|
|
228
|
+
expect(policies).toStrictEqual(['wide']);
|
|
229
|
+
host.remove();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
await it('scopes its page query, so a nested switcher keeps its own pages', () => {
|
|
233
|
+
// Both switchers use the same tag for their pages; an unscoped
|
|
234
|
+
// querySelectorAll let the outer one adopt the inner one's.
|
|
235
|
+
const host = document.createElement('div');
|
|
236
|
+
document.body.appendChild(host);
|
|
237
|
+
|
|
238
|
+
const outer = document.createElement('adw-view-switcher') as AdwViewSwitcher;
|
|
239
|
+
const outerPage = declarePage('adw-view-switcher-page', { name: 'outer', title: 'Outer' });
|
|
240
|
+
const inner = document.createElement('adw-view-switcher') as AdwViewSwitcher;
|
|
241
|
+
inner.appendChild(declarePage('adw-view-switcher-page', { name: 'inner-a', title: 'A' }));
|
|
242
|
+
inner.appendChild(declarePage('adw-view-switcher-page', { name: 'inner-b', title: 'B' }));
|
|
243
|
+
outerPage.appendChild(inner);
|
|
244
|
+
outer.appendChild(outerPage);
|
|
245
|
+
host.appendChild(outer);
|
|
246
|
+
|
|
247
|
+
expect(outer.pages.map((page) => page.name)).toStrictEqual(['outer']);
|
|
248
|
+
expect(inner.pages.map((page) => page.name)).toStrictEqual(['inner-a', 'inner-b']);
|
|
249
|
+
host.remove();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
await it('switches pages after a 500 ms drag hover, and not before', async () => {
|
|
253
|
+
const { element, host } = mountSwitcher([
|
|
254
|
+
{ name: 'a', title: 'A' },
|
|
255
|
+
{ name: 'b', title: 'B' },
|
|
256
|
+
{ name: 'c', title: 'C' },
|
|
257
|
+
]);
|
|
258
|
+
const buttons = buttonsOf(element, '.adw-view-switcher-button');
|
|
259
|
+
|
|
260
|
+
buttons[2]!.dispatchEvent(new Event('dragenter'));
|
|
261
|
+
await new Promise((resolve) => setTimeout(resolve, VIEW_SWITCHER_DRAG_SWITCH_DELAY / 2));
|
|
262
|
+
expect(element.active).toBe(0);
|
|
263
|
+
|
|
264
|
+
await new Promise((resolve) => setTimeout(resolve, VIEW_SWITCHER_DRAG_SWITCH_DELAY));
|
|
265
|
+
expect(element.active).toBe(2);
|
|
266
|
+
|
|
267
|
+
// Leaving before the dwell elapses cancels it.
|
|
268
|
+
buttons[0]!.dispatchEvent(new Event('dragenter'));
|
|
269
|
+
buttons[0]!.dispatchEvent(new Event('dragleave'));
|
|
270
|
+
await new Promise((resolve) => setTimeout(resolve, VIEW_SWITCHER_DRAG_SWITCH_DELAY * 1.5));
|
|
271
|
+
expect(element.active).toBe(2);
|
|
272
|
+
host.remove();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await describe('adw-inline-view-switcher toggles (libadwaita conformance vectors)', async () => {
|
|
277
|
+
for (const vector of INLINE_TOGGLE_VECTORS) {
|
|
278
|
+
await it(vector.rule, () => {
|
|
279
|
+
const { element, host } = mountInline(vector.pages, vector.displayMode);
|
|
280
|
+
|
|
281
|
+
const toggles = buttonsOf(element, '.adw-inline-view-switcher-toggle');
|
|
282
|
+
// A hidden page produces NO toggle — the whole reason the two
|
|
283
|
+
// index spaces exist. The old element made one per declared page.
|
|
284
|
+
expect(toggles).toHaveLength(vector.toggles.length);
|
|
285
|
+
|
|
286
|
+
vector.toggles.forEach((model, index) => {
|
|
287
|
+
const toggle = toggles[index]!;
|
|
288
|
+
expect(toggle.dataset.toggleIndex).toBe(String(model.toggleIndex));
|
|
289
|
+
expect(toggle.dataset.pageIndex).toBe(String(model.pageIndex));
|
|
290
|
+
expect(partText(toggle, '.adw-inline-view-switcher-label')).toBe(
|
|
291
|
+
model.showLabel ? model.label : null,
|
|
292
|
+
);
|
|
293
|
+
const icon = toggle.querySelector('.adw-icon') as HTMLElement | null;
|
|
294
|
+
expect(icon !== null).toBe(model.showIcon);
|
|
295
|
+
if (icon) {
|
|
296
|
+
expect(icon.classList.contains(`adw-icon--${normalizeIconName(model.iconName)}`)).toBe(true);
|
|
297
|
+
}
|
|
298
|
+
expect(toggle.getAttribute('title')).toBe(model.tooltip || null);
|
|
299
|
+
expect(partText(toggle, '.adw-inline-view-switcher-indicator')).toBe(model.badgeLabel);
|
|
300
|
+
expect(toggle.getAttribute('aria-description')).toBe(model.description || null);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Every panel still exists — only the toggles are compacted.
|
|
304
|
+
expect(buttonsOf(element, '.adw-inline-view-switcher-page')).toHaveLength(vector.pages.length);
|
|
305
|
+
host.remove();
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
for (const vector of INLINE_TOOLTIP_VECTORS) {
|
|
310
|
+
// A NULL title has no attribute spelling that differs from an absent
|
|
311
|
+
// one, and both are covered by the toggle vectors above.
|
|
312
|
+
if (vector.title === null) continue;
|
|
313
|
+
await it(`tooltip in ${vector.displayMode} — ${vector.rule}`, () => {
|
|
314
|
+
const { element, host } = mountInline(
|
|
315
|
+
[{ name: 'a', title: vector.title, useUnderline: vector.useUnderline }],
|
|
316
|
+
vector.displayMode,
|
|
317
|
+
);
|
|
318
|
+
const toggle = buttonsOf(element, '.adw-inline-view-switcher-toggle')[0]!;
|
|
319
|
+
expect(toggle.getAttribute('title')).toBe(vector.tooltip || null);
|
|
320
|
+
host.remove();
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
await it("defaults to 'labels' and REFUSES an unknown display mode", () => {
|
|
325
|
+
// The old element defaulted to 'both' and replaced the current mode
|
|
326
|
+
// with 'both' for any unrecognised value.
|
|
327
|
+
const { element, host } = mountInline([
|
|
328
|
+
{ name: 'a', title: 'A', iconName: 'go-home-symbolic' },
|
|
329
|
+
{ name: 'b', title: 'B' },
|
|
330
|
+
]);
|
|
331
|
+
expect(element.displayMode).toBe('labels');
|
|
332
|
+
expect(element.querySelector('.adw-inline-view-switcher-toggle .adw-icon')).toBe(null);
|
|
333
|
+
|
|
334
|
+
const modes: string[] = [];
|
|
335
|
+
element.addEventListener('notify::display-mode', (event) => {
|
|
336
|
+
modes.push((event as CustomEvent<{ displayMode: string }>).detail.displayMode);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
element.setAttribute('display-mode', 'icons');
|
|
340
|
+
expect(element.displayMode).toBe('icons');
|
|
341
|
+
element.setAttribute('display-mode', 'bogus');
|
|
342
|
+
expect(element.displayMode).toBe('icons');
|
|
343
|
+
expect(modes).toStrictEqual(['icons']);
|
|
344
|
+
host.remove();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
await it('maps a toggle back to its PAGE when hidden pages shift the indices', () => {
|
|
348
|
+
const { element, host, changes } = mountInline([
|
|
349
|
+
{ name: 'a', title: 'A' },
|
|
350
|
+
{ name: 'b', title: 'B', visible: false },
|
|
351
|
+
{ name: 'c', title: 'C' },
|
|
352
|
+
]);
|
|
353
|
+
changes.length = 0;
|
|
354
|
+
|
|
355
|
+
const toggles = buttonsOf(element, '.adw-inline-view-switcher-toggle');
|
|
356
|
+
// Toggle 1 is page 2 — clicking it must select the PAGE, not the
|
|
357
|
+
// toggle index, which is what the missing child-index mapping did.
|
|
358
|
+
toggles[1]!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
359
|
+
expect(element.active).toBe(2);
|
|
360
|
+
expect(element.activeToggle).toBe(1);
|
|
361
|
+
expect(changes).toStrictEqual([{ selected: 2, name: 'c', title: '', interactive: true }]);
|
|
362
|
+
host.remove();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
await it('reports -1 when the visible page has no toggle', () => {
|
|
366
|
+
const { element, host } = mountInline([
|
|
367
|
+
{ name: 'a', title: 'A' },
|
|
368
|
+
{ name: 'b', title: 'B' },
|
|
369
|
+
]);
|
|
370
|
+
// Hiding the page a switcher is showing normally moves the selection;
|
|
371
|
+
// force the "selected page is hidden" state the C sentinel exists for
|
|
372
|
+
// by hiding every OTHER page first.
|
|
373
|
+
element.setPageVisible('a', false);
|
|
374
|
+
expect(element.active).toBe(1);
|
|
375
|
+
element.setPageVisible('b', false);
|
|
376
|
+
expect(element.active).toBe(-1);
|
|
377
|
+
expect(element.activeToggle).toBe(-1);
|
|
378
|
+
expect(buttonsOf(element, '.adw-inline-view-switcher-toggle')).toHaveLength(0);
|
|
379
|
+
host.remove();
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
await describe('adw-view-switcher-bar reveal (libadwaita conformance vectors)', async () => {
|
|
384
|
+
for (const vector of VIEW_SWITCHER_BAR_REVEAL_VECTORS) {
|
|
385
|
+
await it(vector.rule, () => {
|
|
386
|
+
const host = document.createElement('div');
|
|
387
|
+
document.body.appendChild(host);
|
|
388
|
+
|
|
389
|
+
const stack = document.createElement('adw-view-stack') as AdwViewStack;
|
|
390
|
+
stack.id = `bar-stack-${Math.random().toString(36).slice(2)}`;
|
|
391
|
+
vector.pages.forEach((page, index) => {
|
|
392
|
+
const pageEl = document.createElement('adw-view-stack-page');
|
|
393
|
+
pageEl.setAttribute('name', `p${index}`);
|
|
394
|
+
pageEl.setAttribute('title', `P${index}`);
|
|
395
|
+
if (!page.visible) pageEl.setAttribute('hidden', '');
|
|
396
|
+
stack.appendChild(pageEl);
|
|
397
|
+
});
|
|
398
|
+
host.appendChild(stack);
|
|
399
|
+
|
|
400
|
+
const bar = document.createElement('adw-view-switcher-bar') as AdwViewSwitcherBar;
|
|
401
|
+
bar.setAttribute('stack', stack.id);
|
|
402
|
+
host.appendChild(bar);
|
|
403
|
+
|
|
404
|
+
bar.reveal = vector.reveal;
|
|
405
|
+
expect(bar.reveal).toBe(vector.reveal);
|
|
406
|
+
// The derivation both ports skipped: a one-page stack keeps the
|
|
407
|
+
// bar collapsed however loudly the layout asks.
|
|
408
|
+
expect(bar.revealed).toBe(vector.revealed);
|
|
409
|
+
expect(bar.hidden).toBe(!vector.revealed);
|
|
410
|
+
expect(getComputedStyle(bar).display).toBe(vector.revealed ? 'block' : 'none');
|
|
411
|
+
host.remove();
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
await it('keeps tracking its stack after being detached and re-inserted', () => {
|
|
416
|
+
// `disconnectedCallback` dropped the listener and `connectedCallback`
|
|
417
|
+
// only re-added it when `ref && !this._stack` — which is never true
|
|
418
|
+
// on a re-attach, so the bar stopped tracking for good.
|
|
419
|
+
const host = document.createElement('div');
|
|
420
|
+
document.body.appendChild(host);
|
|
421
|
+
|
|
422
|
+
const stack = document.createElement('adw-view-stack') as AdwViewStack;
|
|
423
|
+
for (const name of ['a', 'b']) {
|
|
424
|
+
const pageEl = document.createElement('adw-view-stack-page');
|
|
425
|
+
pageEl.setAttribute('name', name);
|
|
426
|
+
pageEl.setAttribute('title', name.toUpperCase());
|
|
427
|
+
stack.appendChild(pageEl);
|
|
428
|
+
}
|
|
429
|
+
host.appendChild(stack);
|
|
430
|
+
|
|
431
|
+
const bar = document.createElement('adw-view-switcher-bar') as AdwViewSwitcherBar;
|
|
432
|
+
host.appendChild(bar);
|
|
433
|
+
bar.setStack(stack);
|
|
434
|
+
bar.reveal = true;
|
|
435
|
+
|
|
436
|
+
bar.remove();
|
|
437
|
+
host.appendChild(bar);
|
|
438
|
+
|
|
439
|
+
stack.visibleChildName = 'b';
|
|
440
|
+
const buttons = buttonsOf(bar, '.adw-view-switcher-bar-button');
|
|
441
|
+
expect(buttons[1]!.getAttribute('aria-selected')).toBe('true');
|
|
442
|
+
expect(buttons[0]!.getAttribute('aria-selected')).toBe('false');
|
|
443
|
+
host.remove();
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
};
|