@gjsify/adwaita-web 0.36.0 → 0.38.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 +504 -34
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/accent.d.ts +28 -0
- package/lib/types/adw-accent.spec.d.ts +2 -0
- package/lib/types/adw-shortcut-label.spec.d.ts +2 -0
- package/lib/types/breakpoints.d.ts +10 -12
- package/lib/types/elements/adw-about-dialog.d.ts +19 -36
- package/lib/types/elements/adw-action-row.d.ts +9 -12
- package/lib/types/elements/adw-alert-dialog.d.ts +0 -12
- package/lib/types/elements/adw-avatar.d.ts +1 -0
- package/lib/types/elements/adw-bottom-sheet.d.ts +7 -12
- package/lib/types/elements/adw-button-content.d.ts +3 -4
- package/lib/types/elements/adw-button-row.d.ts +1 -1
- package/lib/types/elements/adw-carousel.d.ts +28 -49
- package/lib/types/elements/adw-checks.d.ts +2 -8
- package/lib/types/elements/adw-combo-row.d.ts +6 -6
- package/lib/types/elements/adw-data-grid.d.ts +5 -10
- package/lib/types/elements/adw-drop-down.d.ts +3 -11
- package/lib/types/elements/adw-entry-row.d.ts +20 -23
- package/lib/types/elements/adw-icon.d.ts +2 -4
- package/lib/types/elements/adw-inline-view-switcher.d.ts +18 -31
- package/lib/types/elements/adw-navigation-split-view.d.ts +7 -12
- package/lib/types/elements/adw-navigation-view.d.ts +2 -10
- package/lib/types/elements/adw-overlay-split-view.d.ts +28 -52
- package/lib/types/elements/adw-password-entry-row.d.ts +4 -5
- package/lib/types/elements/adw-popover.d.ts +2 -7
- package/lib/types/elements/adw-preferences-dialog.d.ts +21 -34
- package/lib/types/elements/adw-preferences-group.d.ts +12 -14
- package/lib/types/elements/adw-progress-bar.d.ts +3 -4
- package/lib/types/elements/adw-shortcut-label.d.ts +20 -0
- package/lib/types/elements/adw-sidebar.d.ts +5 -10
- package/lib/types/elements/adw-spinner.d.ts +3 -5
- package/lib/types/elements/adw-split-button.d.ts +6 -16
- package/lib/types/elements/adw-switch-row.d.ts +3 -3
- package/lib/types/elements/adw-tab-view.d.ts +16 -26
- package/lib/types/elements/adw-toast-overlay.d.ts +8 -13
- package/lib/types/elements/adw-view-stack.d.ts +7 -17
- package/lib/types/elements/adw-view-switcher-bar.d.ts +4 -8
- package/lib/types/elements/adw-view-switcher.d.ts +18 -38
- package/lib/types/elements/adw-wrap-box.d.ts +7 -17
- package/lib/types/elements/view-switcher-dom.d.ts +10 -13
- package/lib/types/index.d.ts +3 -0
- package/lib/types/source-view/hex.d.ts +7 -13
- package/lib/types/style-classes.spec.d.ts +11 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/package.json +18 -7
- package/scss/_about_dialog.scss +3 -2
- package/scss/_action_row.scss +33 -0
- package/scss/_avatar.scss +12 -0
- package/scss/_button.scss +44 -22
- package/scss/_card.scss +11 -8
- package/scss/_checks.scss +2 -4
- package/scss/_dialog.scss +3 -2
- package/scss/_headerbar.scss +23 -0
- package/scss/_popover.scss +8 -4
- package/scss/_preferences.scss +7 -7
- package/scss/_progress_bar.scss +9 -6
- package/scss/_reset.scss +1 -1
- package/scss/_shortcut_label.scss +80 -0
- package/scss/_sidebar.scss +57 -0
- package/scss/_status_page.scss +20 -0
- package/scss/_style_classes.scss +407 -0
- package/scss/_switch.scss +4 -3
- package/scss/_theme.scss +20 -2
- package/scss/_variables.scss +34 -1
- package/scss/adwaita-skin.scss +12 -0
- package/src/accent.ts +74 -0
- package/src/adw-about-dialog.spec.ts +34 -49
- package/src/adw-accent.spec.ts +105 -0
- package/src/adw-action-rows.spec.ts +25 -33
- package/src/adw-avatar.spec.ts +51 -20
- package/src/adw-banner.spec.ts +5 -8
- package/src/adw-button-content.spec.ts +9 -12
- package/src/adw-button-row.spec.ts +2 -4
- package/src/adw-carousel.spec.ts +40 -53
- package/src/adw-checks.spec.ts +36 -47
- package/src/adw-drop-down.spec.ts +25 -32
- package/src/adw-header-bar.spec.ts +5 -7
- package/src/adw-icon.spec.ts +23 -32
- package/src/adw-progress-bar.spec.ts +10 -12
- package/src/adw-row-state.spec.ts +9 -10
- package/src/adw-shortcut-label.spec.ts +112 -0
- package/src/adw-sidebar.spec.ts +17 -24
- package/src/adw-switch.spec.ts +11 -17
- package/src/adw-tab-view.spec.ts +7 -10
- package/src/adw-toast-overlay.spec.ts +7 -11
- package/src/adw-view-switcher-bar.spec.ts +10 -14
- package/src/adw-wrap-box.spec.ts +25 -36
- package/src/bottom-sheet.spec.ts +30 -36
- package/src/breakpoints.ts +15 -18
- package/src/chrome.spec.ts +13 -21
- package/src/elements/adw-about-dialog.ts +71 -114
- package/src/elements/adw-action-row.ts +22 -37
- package/src/elements/adw-alert-dialog.ts +25 -50
- package/src/elements/adw-avatar.ts +40 -15
- package/src/elements/adw-banner.ts +14 -18
- package/src/elements/adw-bottom-sheet.ts +31 -52
- package/src/elements/adw-button-content.ts +30 -37
- package/src/elements/adw-button-row.ts +14 -24
- package/src/elements/adw-carousel.ts +87 -148
- package/src/elements/adw-checks.ts +43 -56
- package/src/elements/adw-clamp.ts +8 -9
- package/src/elements/adw-combo-row.ts +17 -25
- package/src/elements/adw-data-grid.ts +26 -38
- package/src/elements/adw-dialog.ts +25 -56
- package/src/elements/adw-drop-down.ts +31 -51
- package/src/elements/adw-entry-row.ts +39 -46
- package/src/elements/adw-expander-row.ts +30 -53
- package/src/elements/adw-icon.ts +30 -53
- package/src/elements/adw-inline-view-switcher.ts +38 -67
- package/src/elements/adw-menu-button.ts +28 -58
- package/src/elements/adw-navigation-split-view.ts +35 -47
- package/src/elements/adw-navigation-view.ts +12 -28
- package/src/elements/adw-overlay-split-view.ts +100 -175
- package/src/elements/adw-password-entry-row.ts +18 -28
- package/src/elements/adw-popover.ts +28 -44
- package/src/elements/adw-preferences-dialog.ts +41 -66
- package/src/elements/adw-preferences-group.ts +33 -46
- package/src/elements/adw-progress-bar.ts +27 -47
- package/src/elements/adw-shortcut-label.ts +138 -0
- package/src/elements/adw-sidebar.ts +31 -39
- package/src/elements/adw-spin-row.ts +7 -14
- package/src/elements/adw-spinner.ts +31 -43
- package/src/elements/adw-split-button.ts +45 -75
- package/src/elements/adw-switch-row.ts +29 -46
- package/src/elements/adw-switch.ts +28 -47
- package/src/elements/adw-tab-view.ts +62 -103
- package/src/elements/adw-toast-overlay.ts +28 -35
- package/src/elements/adw-toggle-group.ts +9 -16
- package/src/elements/adw-view-stack.ts +32 -51
- package/src/elements/adw-view-switcher-bar.ts +26 -50
- package/src/elements/adw-view-switcher.ts +60 -110
- package/src/elements/adw-window-title.ts +4 -4
- package/src/elements/adw-wrap-box.ts +33 -59
- package/src/elements/view-switcher-dom.ts +15 -22
- package/src/entry-rows.spec.ts +18 -24
- package/src/index.ts +24 -15
- package/src/navigation-view.spec.ts +7 -10
- package/src/preferences-search.ts +11 -15
- package/src/preferences.spec.ts +13 -15
- package/src/source-view/adw-source-view.ts +6 -19
- package/src/source-view/hex.ts +7 -13
- package/src/split-button.spec.ts +32 -44
- package/src/split-views.spec.ts +83 -149
- package/src/style-classes.spec.ts +181 -0
- package/src/style-isolation.spec.ts +3 -6
- package/src/styles.generated.ts +1 -1
- package/src/tab-view.spec.ts +26 -35
- package/src/test.browser.mts +6 -0
- package/src/view-stack.spec.ts +23 -34
- package/src/view-switcher.spec.ts +56 -84
package/src/tab-view.spec.ts
CHANGED
|
@@ -2,16 +2,12 @@
|
|
|
2
2
|
// core suite and the NativeScript renderer assert against
|
|
3
3
|
// (`@gjsify/adwaita-core/conformance`).
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// close successor, the reorder clamps and the keyboard model existed in neither.
|
|
12
|
-
// This suite is what makes the element a thin adapter over `TabViewState` instead
|
|
13
|
-
// of a second implementation: the element is handed STRAIGHT to the shared vector
|
|
14
|
-
// driver, so a method it re-implements fails here.
|
|
5
|
+
// The element is a thin adapter over `TabViewState`, not a second implementation: it is
|
|
6
|
+
// handed STRAIGHT to the shared vector driver, so a method it re-implements fails here.
|
|
7
|
+
// Neither renderer had the model at all — `close-page` meant three different things,
|
|
8
|
+
// out-of-range selection was clamped in one and ignored in the other, and the pinned
|
|
9
|
+
// partition, the parent-aware close successor, the reorder clamps and the keyboard model
|
|
10
|
+
// existed in neither.
|
|
15
11
|
import { describe, expect, it } from '@gjsify/unit';
|
|
16
12
|
|
|
17
13
|
import {
|
|
@@ -38,12 +34,10 @@ interface MountedTabView {
|
|
|
38
34
|
}
|
|
39
35
|
|
|
40
36
|
/**
|
|
41
|
-
* Mount an EMPTY view and record its events.
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* is the vector target" literal. The declarative path gets its own describe
|
|
46
|
-
* block below.
|
|
37
|
+
* Mount an EMPTY view and record its events. Empty rather than markup-seeded: the
|
|
38
|
+
* vectors seed through the same `seedTabViewPages` the other two suites use, which is
|
|
39
|
+
* what makes "the element is the vector target" literal. The declarative path has its own
|
|
40
|
+
* describe block below.
|
|
47
41
|
*/
|
|
48
42
|
function mountTabView(handler: 'default' | 'defer'): MountedTabView {
|
|
49
43
|
const host = document.createElement('div');
|
|
@@ -126,11 +120,9 @@ export const AdwTabViewConformanceTest = async () => {
|
|
|
126
120
|
if (vector.closing) expect(tabViewClosing(view)).toStrictEqual([...vector.closing]);
|
|
127
121
|
if (vector.diagnostics) expect(view.diagnostics).toStrictEqual([...vector.diagnostics]);
|
|
128
122
|
|
|
129
|
-
// The DOM must agree with the model: one chip per page in model
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
// connect and never touched it again, so a closed page kept its
|
|
133
|
-
// chip forever — this is the assertion that would have caught it.
|
|
123
|
+
// The DOM must agree with the model: one chip per page in model order,
|
|
124
|
+
// exactly the selected panel shown, and nothing when the view empties. A
|
|
125
|
+
// bar built once at connect keeps a closed page's chip forever.
|
|
134
126
|
expect(tabChips(view).map((tab) => tab.dataset.pageId)).toStrictEqual([...vector.order]);
|
|
135
127
|
const expectedShown = vector.order.map(() => false);
|
|
136
128
|
if (vector.selectedIndex >= 0) expectedShown[vector.selectedIndex] = true;
|
|
@@ -175,15 +167,15 @@ export const AdwTabViewConformanceTest = async () => {
|
|
|
175
167
|
expect(view.pages[0]!.title).toBe('Renamed');
|
|
176
168
|
expect(tabChips(view)[0]!.querySelector('.adw-tab-title')!.textContent).toBe('Renamed');
|
|
177
169
|
|
|
178
|
-
// `null` coerces to ''
|
|
170
|
+
// `null` coerces to '', not to 'null'.
|
|
179
171
|
page.removeAttribute('title');
|
|
180
172
|
expect(view.pages[0]!.title).toBe('');
|
|
181
173
|
host.remove();
|
|
182
174
|
});
|
|
183
175
|
|
|
184
176
|
await it('generates a distinct page id per declared page, and an explicit page-id wins', () => {
|
|
185
|
-
// An id stands in for the AdwTabPage POINTER, so two pages must never
|
|
186
|
-
//
|
|
177
|
+
// An id stands in for the AdwTabPage POINTER, so two pages must never share
|
|
178
|
+
// one — a declared page without `page-id` still needs its own.
|
|
187
179
|
const host = document.createElement('div');
|
|
188
180
|
host.innerHTML = `<adw-tab-view>
|
|
189
181
|
<adw-tab-page title="One"></adw-tab-page>
|
|
@@ -221,15 +213,15 @@ export const AdwTabViewConformanceTest = async () => {
|
|
|
221
213
|
});
|
|
222
214
|
|
|
223
215
|
await it('IGNORES an out-of-range or unparseable index instead of clamping it', () => {
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
216
|
+
// A clamping `Number.isNaN(raw) ? 0 : Math.min(Math.max(raw, 0), max)` selects
|
|
217
|
+
// the last page for `99` and the first for `-1`/`oops`; libadwaita refuses all
|
|
218
|
+
// three.
|
|
227
219
|
const { view, host } = mountThree();
|
|
228
220
|
for (const value of ['99', '-1', 'oops']) {
|
|
229
221
|
view.setAttribute('selected', value);
|
|
230
222
|
expect(view.selectedIndex).toBe(0);
|
|
231
223
|
}
|
|
232
|
-
|
|
224
|
+
//...and the attribute is put back in sync rather than left lying.
|
|
233
225
|
view.setAttribute('selected', '1');
|
|
234
226
|
expect(view.selectedIndex).toBe(1);
|
|
235
227
|
expect(view.getAttribute('selected')).toBe('1');
|
|
@@ -237,8 +229,8 @@ export const AdwTabViewConformanceTest = async () => {
|
|
|
237
229
|
});
|
|
238
230
|
|
|
239
231
|
await it('notifies on a PROGRAMMATIC selection, not only on a click', () => {
|
|
240
|
-
// `view.selected = 2`
|
|
241
|
-
//
|
|
232
|
+
// C notifies on EVERY path: a programmatic `view.selected = 2` must notify as
|
|
233
|
+
// a tab click does.
|
|
242
234
|
const { view, host } = mountThree();
|
|
243
235
|
const seen: number[] = [];
|
|
244
236
|
view.addEventListener('notify::selected-page', (event) =>
|
|
@@ -359,8 +351,8 @@ export const AdwTabViewConformanceTest = async () => {
|
|
|
359
351
|
|
|
360
352
|
for (const vector of TAB_CLOSE_VISIBLE_VECTORS) {
|
|
361
353
|
await it(`close visibility: ${vector.rule}`, () => {
|
|
362
|
-
// The renderer's gate IS the core predicate;
|
|
363
|
-
//
|
|
354
|
+
// The renderer's gate IS the core predicate; the table here keeps a future
|
|
355
|
+
// "simplify to `selected`" from passing.
|
|
364
356
|
expect(
|
|
365
357
|
tabCloseVisible({
|
|
366
358
|
hovering: vector.hovering,
|
|
@@ -416,9 +408,8 @@ export const AdwTabViewConformanceTest = async () => {
|
|
|
416
408
|
});
|
|
417
409
|
|
|
418
410
|
await it('moves the roving tabindex with the selection, so inactive tabs stay reachable', () => {
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
// keyboard-unreachable.
|
|
411
|
+
// `tab.tabIndex = isActive ? 0 : -1` under role=tablist needs a key handler,
|
|
412
|
+
// or every inactive tab is keyboard-unreachable.
|
|
422
413
|
const { view, host } = mountFour();
|
|
423
414
|
press(view, { key: 'ArrowRight' });
|
|
424
415
|
expect(view.selectedId).toBe('b');
|
package/src/test.browser.mts
CHANGED
|
@@ -40,8 +40,14 @@ import { AdwSwitchTest } from './adw-switch.spec.js';
|
|
|
40
40
|
import { AdwChecksTest } from './adw-checks.spec.js';
|
|
41
41
|
import { AdwProgressBarTest } from './adw-progress-bar.spec.js';
|
|
42
42
|
import { AdwAboutDialogTest } from './adw-about-dialog.spec.js';
|
|
43
|
+
import { AdwStyleClassesTest } from './style-classes.spec.js';
|
|
44
|
+
import { AdwAccentTest } from './adw-accent.spec.js';
|
|
45
|
+
import { AdwShortcutLabelTest } from './adw-shortcut-label.spec.js';
|
|
43
46
|
|
|
44
47
|
run({
|
|
48
|
+
AdwStyleClassesTest,
|
|
49
|
+
AdwAccentTest,
|
|
50
|
+
AdwShortcutLabelTest,
|
|
45
51
|
AdwAboutDialogTest,
|
|
46
52
|
AdwBannerTest,
|
|
47
53
|
AdwButtonContentTest,
|
package/src/view-stack.spec.ts
CHANGED
|
@@ -2,16 +2,11 @@
|
|
|
2
2
|
// the core suite and the NativeScript renderer assert against
|
|
3
3
|
// (`@gjsify/adwaita-core/conformance`).
|
|
4
4
|
//
|
|
5
|
-
// The
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// took the first page ADDED instead of the first VISIBLE one, an empty
|
|
11
|
-
// `visible-child-name` was treated as "no name" by the declarative path but as a
|
|
12
|
-
// legal lookup by the property path, and the auto-pick notified nothing so a
|
|
13
|
-
// bound switcher needed a manual `refresh()`. Nothing failed, because nothing
|
|
14
|
-
// compared them. This suite is that comparison.
|
|
5
|
+
// The rules it pins, each of which both renderers had drifted from while carrying
|
|
6
|
+
// independent copies of the selection logic: a non-integer index is REFUSED, not merely
|
|
7
|
+
// finite-checked; the auto-pick takes the first VISIBLE page, not the first added; an
|
|
8
|
+
// empty `visible-child-name` means the same thing to the declarative and the property
|
|
9
|
+
// path; and the auto-pick NOTIFIES, so a bound switcher needs no manual `refresh()`.
|
|
15
10
|
import { describe, expect, it } from '@gjsify/unit';
|
|
16
11
|
|
|
17
12
|
import type { ViewStackVectorChange, ViewStackVectorOp, ViewStackVectorPage } from '@gjsify/adwaita-core/conformance';
|
|
@@ -33,12 +28,10 @@ interface MountedStack {
|
|
|
33
28
|
/**
|
|
34
29
|
* Mount a stack whose pages are DECLARED as markup, driving the attribute path.
|
|
35
30
|
*
|
|
36
|
-
* Attributes
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* connected so the auto-pick notification is observable, which is the whole
|
|
41
|
-
* point of the fix that made it fire at all.
|
|
31
|
+
* Attributes go through `setAttribute` rather than an HTML string: several vectors hinge
|
|
32
|
+
* on an exact name — the empty string, the NFD spelling of `Übersicht` — which parsed
|
|
33
|
+
* markup is not a reliable carrier for. The change listener is attached BEFORE connect,
|
|
34
|
+
* so the auto-pick notification is observable at all.
|
|
42
35
|
*/
|
|
43
36
|
function mountStack(pages: readonly ViewStackVectorPage[], visibleChildName?: string): MountedStack {
|
|
44
37
|
const host = document.createElement('div');
|
|
@@ -110,10 +103,9 @@ export const AdwViewStackTest = async () => {
|
|
|
110
103
|
expect(stack.pages.length).toBe(vector.count);
|
|
111
104
|
expect(stack.pages[stack.visibleChildIndex]?.title ?? '').toBe(vector.visibleTitle);
|
|
112
105
|
|
|
113
|
-
// The DOM must agree with the model: exactly the selected page is
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
// still reporting a selection.
|
|
106
|
+
// The DOM must agree with the model: exactly the selected page is shown,
|
|
107
|
+
// and nothing at all when the selection is -1. A stack storing an
|
|
108
|
+
// unreachable index shows NOTHING while still reporting a selection.
|
|
117
109
|
const expectedShown = vector.pages.map(() => false).slice(0, vector.count);
|
|
118
110
|
if (vector.visibleIndex >= 0) expectedShown[vector.visibleIndex] = true;
|
|
119
111
|
expect(shownPages(stack)).toStrictEqual(expectedShown);
|
|
@@ -160,11 +152,10 @@ export const AdwViewStackTest = async () => {
|
|
|
160
152
|
|
|
161
153
|
await describe('adw-view-stack imperative pages (Adw.ViewStack.add)', async () => {
|
|
162
154
|
await it('keeps pages added BEFORE the stack enters the document', () => {
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
// libadwaita has no parse-once phase (adw-view-stack.c:2160-2251).
|
|
155
|
+
// connectedCallback must not overwrite the page list from whatever
|
|
156
|
+
// <adw-view-stack-page> children it finds — none, for imperative use — and
|
|
157
|
+
// then replaceChildren() the subtree away, destroying the caller's element.
|
|
158
|
+
// libadwaita has no parse-once phase.
|
|
168
159
|
const stack = document.createElement('adw-view-stack') as AdwViewStack;
|
|
169
160
|
const content = document.createElement('div');
|
|
170
161
|
content.textContent = 'learn body';
|
|
@@ -221,10 +212,9 @@ export const AdwViewStackTest = async () => {
|
|
|
221
212
|
});
|
|
222
213
|
|
|
223
214
|
await it('treats an EMPTY declared name as a legal lookup, not as "no name"', () => {
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
// g_strcmp0("", "") is 0 (adw-view-stack.c:901).
|
|
215
|
+
// A declarative `initialName ? … : -1` sends an explicit empty name to page 0
|
|
216
|
+
// while the PROPERTY path matches the page named '' — two answers, one input.
|
|
217
|
+
// g_strcmp0("", "") is 0.
|
|
228
218
|
const { stack, host } = mountStack([{ name: 'a' }, { name: '' }], '');
|
|
229
219
|
expect(stack.visibleChildIndex).toBe(1);
|
|
230
220
|
expect(shownPages(stack)).toStrictEqual([false, true]);
|
|
@@ -235,8 +225,7 @@ export const AdwViewStackTest = async () => {
|
|
|
235
225
|
const { stack, host } = mountStack([{ name: 'a' }, { name: '' }]);
|
|
236
226
|
expect(stack.getAttribute('visible-child-name')).toBe('a');
|
|
237
227
|
stack.visibleChildIndex = 1;
|
|
238
|
-
//
|
|
239
|
-
// nameless pages never reflected its selection at all.
|
|
228
|
+
// Reflection guarded by `if (current && …)` never fires for nameless pages.
|
|
240
229
|
expect(stack.getAttribute('visible-child-name')).toBe('');
|
|
241
230
|
host.remove();
|
|
242
231
|
});
|
|
@@ -259,9 +248,9 @@ export const AdwViewStackTest = async () => {
|
|
|
259
248
|
});
|
|
260
249
|
|
|
261
250
|
await it('does not bounce the selection back when a duplicate name is reflected', () => {
|
|
262
|
-
// Reflection writes the attribute
|
|
263
|
-
//
|
|
264
|
-
//
|
|
251
|
+
// Reflection writes the attribute and re-enters attributeChangedCallback;
|
|
252
|
+
// with duplicate names a naive re-lookup resolves to the FIRST match and
|
|
253
|
+
// undoes the selection.
|
|
265
254
|
const { stack, host } = mountStack([{ name: 'dup' }, { name: 'dup' }]);
|
|
266
255
|
stack.visibleChildIndex = 1;
|
|
267
256
|
expect(stack.visibleChildIndex).toBe(1);
|
|
@@ -3,15 +3,9 @@
|
|
|
3
3
|
// vectors the core suite and the NativeScript suite assert against
|
|
4
4
|
// (`@gjsify/adwaita-core/conformance`).
|
|
5
5
|
//
|
|
6
|
-
// The
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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.
|
|
6
|
+
// The suite exists to keep the two renderers from drifting: they used to carry
|
|
7
|
+
// independent copies of every switcher derivation, and five libadwaita rules lived in
|
|
8
|
+
// neither because nothing compared them.
|
|
15
9
|
import { describe, expect, it } from '@gjsify/unit';
|
|
16
10
|
|
|
17
11
|
import {
|
|
@@ -44,8 +38,8 @@ import type { AdwViewStack } from './elements/adw-view-stack.js';
|
|
|
44
38
|
function declarePage(tag: string, page: ViewSwitcherVectorPage): HTMLElement {
|
|
45
39
|
const element = document.createElement(tag);
|
|
46
40
|
element.setAttribute('name', page.name);
|
|
47
|
-
// `getAttribute` returns null for an absent attribute, which IS C's NULL
|
|
48
|
-
//
|
|
41
|
+
// `getAttribute` returns null for an absent attribute, which IS C's NULL, so an
|
|
42
|
+
// absent or explicitly-null title must not become `title=""`.
|
|
49
43
|
if (typeof page.title === 'string') element.setAttribute('title', page.title);
|
|
50
44
|
if (typeof page.iconName === 'string') element.setAttribute('icon-name', page.iconName);
|
|
51
45
|
if (page.visible === false) element.setAttribute('hidden', '');
|
|
@@ -77,8 +71,7 @@ function mountSwitcher(pages: readonly ViewSwitcherVectorPage[], policy?: string
|
|
|
77
71
|
for (const page of pages) element.appendChild(declarePage('adw-view-switcher-page', page));
|
|
78
72
|
|
|
79
73
|
const changes: ViewSwitcherVectorChange[] = [];
|
|
80
|
-
//
|
|
81
|
-
// notification is observable — the switcher used to be silent there.
|
|
74
|
+
// Attached BEFORE connect, so the auto-pick notification is observable.
|
|
82
75
|
element.addEventListener('notify::visible-child', (event) => {
|
|
83
76
|
const detail = (event as CustomEvent<{ index: number; name: string; title: string; interactive: boolean }>)
|
|
84
77
|
.detail;
|
|
@@ -142,16 +135,14 @@ function partText(button: HTMLElement, selector: string): string | null {
|
|
|
142
135
|
export const AdwViewSwitcherTest = async () => {
|
|
143
136
|
await describe('adw-view-switcher buttons (libadwaita conformance vectors)', async () => {
|
|
144
137
|
for (const vector of VIEW_SWITCHER_BUTTON_VECTORS) {
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
// drives that row directly.
|
|
138
|
+
// `selected: -1` cannot be staged here: this element bundles its own stack,
|
|
139
|
+
// whose auto-pick takes the first visible page, and reaching "nothing
|
|
140
|
+
// selected" needs every page hidden — which changes the `visible` flags the
|
|
141
|
+
// row asserts. The core suite drives that row.
|
|
150
142
|
if (vector.selected < 0) continue;
|
|
151
143
|
await it(vector.rule, () => {
|
|
152
144
|
const { element, host } = mountSwitcher(vector.pages, vector.policy);
|
|
153
|
-
//
|
|
154
|
-
// element rather than assuming the auto-pick landed there.
|
|
145
|
+
// Drive the vector's selection rather than assume the auto-pick.
|
|
155
146
|
element.active = vector.selected;
|
|
156
147
|
|
|
157
148
|
const buttons = buttonsOf(element, '.adw-view-switcher-button');
|
|
@@ -159,11 +150,11 @@ export const AdwViewSwitcherTest = async () => {
|
|
|
159
150
|
|
|
160
151
|
vector.buttons.forEach((model, index) => {
|
|
161
152
|
const button = buttons[index]!;
|
|
162
|
-
//
|
|
153
|
+
// `visible && (title != NULL || icon-name != NULL)`.
|
|
163
154
|
expect(button.hidden).toBe(!model.visible);
|
|
164
155
|
expect(button.getAttribute('aria-selected')).toBe(String(model.selected));
|
|
165
156
|
expect(partText(button, '.adw-view-switcher-label')).toBe(model.label);
|
|
166
|
-
// The icon node always exists and always names an icon
|
|
157
|
+
// The icon node always exists and always names an icon:
|
|
167
158
|
// `image-missing` when the page has none.
|
|
168
159
|
const icon = button.querySelector('.adw-icon') as HTMLElement;
|
|
169
160
|
expect(icon.classList.contains(`adw-icon--${normalizeIconName(model.iconName)}`)).toBe(true);
|
|
@@ -171,16 +162,14 @@ export const AdwViewSwitcherTest = async () => {
|
|
|
171
162
|
expect(button.getAttribute('aria-description')).toBe(model.description || null);
|
|
172
163
|
});
|
|
173
164
|
|
|
174
|
-
// The policy style class lives on the single `viewswitcher` node.
|
|
175
165
|
expect(element.classList.contains(vector.policy)).toBe(true);
|
|
176
166
|
host.remove();
|
|
177
167
|
});
|
|
178
168
|
}
|
|
179
169
|
|
|
180
170
|
await it('hides a titleless, iconless button in the LAYOUT, not just visually', () => {
|
|
181
|
-
// The `[hidden]` UA rule loses to the component's own author
|
|
182
|
-
//
|
|
183
|
-
// same cascade trap the switcher BAR was fixed for.
|
|
171
|
+
// The `[hidden]` UA rule loses to the component's own author `display:
|
|
172
|
+
// inline-flex` unless the stylesheet re-asserts it.
|
|
184
173
|
const { element, host } = mountSwitcher([{ name: 'a', title: 'A' }, { name: 'b' }]);
|
|
185
174
|
const buttons = buttonsOf(element, '.adw-view-switcher-button');
|
|
186
175
|
expect(getComputedStyle(buttons[1]!).display).toBe('none');
|
|
@@ -202,16 +191,16 @@ export const AdwViewSwitcherTest = async () => {
|
|
|
202
191
|
expect(element.active).toBe(vector.selected);
|
|
203
192
|
expect(element.visibleChildName).toBe(vector.selectedName);
|
|
204
193
|
|
|
205
|
-
//
|
|
206
|
-
//
|
|
194
|
+
// Exactly the selected page's body is shown, none when the selection
|
|
195
|
+
// is -1.
|
|
207
196
|
const bodies = buttonsOf(element, '.adw-view-switcher-page');
|
|
208
197
|
const shown = bodies.map((body) => !(body as HTMLElement).hidden);
|
|
209
198
|
const expectedShown = vector.pages.map((_page, index) => index === vector.selected);
|
|
210
199
|
expect(shown).toStrictEqual(expectedShown);
|
|
211
200
|
|
|
212
|
-
// The reflected attribute must not claim a selection
|
|
213
|
-
//
|
|
214
|
-
//
|
|
201
|
+
// The reflected attribute must not claim a REFUSED selection: libadwaita
|
|
202
|
+
// refuses an out-of-range index, where clamping makes `active="99"` the
|
|
203
|
+
// last page silently.
|
|
215
204
|
expect(element.getAttribute('active')).toBe(String(vector.selected));
|
|
216
205
|
host.remove();
|
|
217
206
|
});
|
|
@@ -237,8 +226,8 @@ export const AdwViewSwitcherTest = async () => {
|
|
|
237
226
|
});
|
|
238
227
|
|
|
239
228
|
await it('scopes its page query, so a nested switcher keeps its own pages', () => {
|
|
240
|
-
// Both switchers use the same tag for their pages
|
|
241
|
-
// querySelectorAll
|
|
229
|
+
// Both switchers use the same tag for their pages, so an unscoped
|
|
230
|
+
// querySelectorAll lets the outer one adopt the inner one's.
|
|
242
231
|
const host = document.createElement('div');
|
|
243
232
|
document.body.appendChild(host);
|
|
244
233
|
|
|
@@ -286,8 +275,7 @@ export const AdwViewSwitcherTest = async () => {
|
|
|
286
275
|
const { element, host } = mountInline(vector.pages, vector.displayMode);
|
|
287
276
|
|
|
288
277
|
const toggles = buttonsOf(element, '.adw-inline-view-switcher-toggle');
|
|
289
|
-
// A hidden page produces NO toggle — the
|
|
290
|
-
// index spaces exist. The old element made one per declared page.
|
|
278
|
+
// A hidden page produces NO toggle — why the two index spaces exist.
|
|
291
279
|
expect(toggles).toHaveLength(vector.toggles.length);
|
|
292
280
|
|
|
293
281
|
vector.toggles.forEach((model, index) => {
|
|
@@ -307,15 +295,15 @@ export const AdwViewSwitcherTest = async () => {
|
|
|
307
295
|
expect(toggle.getAttribute('aria-description')).toBe(model.description || null);
|
|
308
296
|
});
|
|
309
297
|
|
|
310
|
-
// Every panel still exists
|
|
298
|
+
// Every panel still exists; only the toggles are compacted.
|
|
311
299
|
expect(buttonsOf(element, '.adw-inline-view-switcher-page')).toHaveLength(vector.pages.length);
|
|
312
300
|
host.remove();
|
|
313
301
|
});
|
|
314
302
|
}
|
|
315
303
|
|
|
316
304
|
for (const vector of INLINE_TOOLTIP_VECTORS) {
|
|
317
|
-
// A NULL title has no attribute spelling
|
|
318
|
-
//
|
|
305
|
+
// A NULL title has no attribute spelling distinct from an absent one; the
|
|
306
|
+
// toggle vectors above cover both.
|
|
319
307
|
if (vector.title === null) continue;
|
|
320
308
|
await it(`tooltip in ${vector.displayMode} — ${vector.rule}`, () => {
|
|
321
309
|
const { element, host } = mountInline(
|
|
@@ -329,8 +317,6 @@ export const AdwViewSwitcherTest = async () => {
|
|
|
329
317
|
}
|
|
330
318
|
|
|
331
319
|
await it("defaults to 'labels' and REFUSES an unknown display mode", () => {
|
|
332
|
-
// The old element defaulted to 'both' and replaced the current mode
|
|
333
|
-
// with 'both' for any unrecognised value.
|
|
334
320
|
const { element, host } = mountInline([
|
|
335
321
|
{ name: 'a', title: 'A', iconName: 'go-home-symbolic' },
|
|
336
322
|
{ name: 'b', title: 'B' },
|
|
@@ -360,8 +346,8 @@ export const AdwViewSwitcherTest = async () => {
|
|
|
360
346
|
changes.length = 0;
|
|
361
347
|
|
|
362
348
|
const toggles = buttonsOf(element, '.adw-inline-view-switcher-toggle');
|
|
363
|
-
// Toggle 1 is page 2
|
|
364
|
-
//
|
|
349
|
+
// Toggle 1 is page 2: a click selects the PAGE, not the toggle index, which is
|
|
350
|
+
// what a missing child-index mapping would select.
|
|
365
351
|
toggles[1]!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
366
352
|
expect(element.active).toBe(2);
|
|
367
353
|
expect(element.activeToggle).toBe(1);
|
|
@@ -374,9 +360,8 @@ export const AdwViewSwitcherTest = async () => {
|
|
|
374
360
|
{ name: 'a', title: 'A' },
|
|
375
361
|
{ name: 'b', title: 'B' },
|
|
376
362
|
]);
|
|
377
|
-
// Hiding the page
|
|
378
|
-
//
|
|
379
|
-
// by hiding every OTHER page first.
|
|
363
|
+
// Hiding the shown page normally moves the selection, so reach the state the C
|
|
364
|
+
// sentinel exists for by hiding every OTHER page first.
|
|
380
365
|
element.setPageVisible('a', false);
|
|
381
366
|
expect(element.active).toBe(1);
|
|
382
367
|
element.setPageVisible('b', false);
|
|
@@ -410,8 +395,8 @@ export const AdwViewSwitcherTest = async () => {
|
|
|
410
395
|
|
|
411
396
|
bar.reveal = vector.reveal;
|
|
412
397
|
expect(bar.reveal).toBe(vector.reveal);
|
|
413
|
-
//
|
|
414
|
-
//
|
|
398
|
+
// `reveal && more-than-one-visible-page`: a one-page stack keeps the bar
|
|
399
|
+
// collapsed however loudly the layout asks.
|
|
415
400
|
expect(bar.revealed).toBe(vector.revealed);
|
|
416
401
|
expect(bar.hidden).toBe(!vector.revealed);
|
|
417
402
|
expect(getComputedStyle(bar).display).toBe(vector.revealed ? 'block' : 'none');
|
|
@@ -420,9 +405,8 @@ export const AdwViewSwitcherTest = async () => {
|
|
|
420
405
|
}
|
|
421
406
|
|
|
422
407
|
await it('keeps tracking its stack after being detached and re-inserted', () => {
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
// on a re-attach, so the bar stopped tracking for good.
|
|
408
|
+
// Re-adding the listener only when `ref && !this._stack` is never true on a
|
|
409
|
+
// re-attach, and the bar goes deaf.
|
|
426
410
|
const host = document.createElement('div');
|
|
427
411
|
document.body.appendChild(host);
|
|
428
412
|
|
|
@@ -460,9 +444,6 @@ export const AdwViewSwitcherTest = async () => {
|
|
|
460
444
|
scheduler: { schedule(cb: () => void, ms: number): unknown; cancel(handle: unknown): void };
|
|
461
445
|
};
|
|
462
446
|
const clock = createViewSwitcherClock();
|
|
463
|
-
// The seam that did not exist: the elements built their state inline
|
|
464
|
-
// with the DOM scheduler, so VIEW_SWITCHER_DRAG_VECTORS was driven by
|
|
465
|
-
// the core suite alone while a header claimed all three drove it.
|
|
466
447
|
switcher.scheduler = clock;
|
|
467
448
|
for (const page of pages) switcher.appendChild(declarePage('adw-view-switcher-page', page));
|
|
468
449
|
switcher.setAttribute('active', String(initial));
|
|
@@ -480,9 +461,8 @@ export const AdwViewSwitcherTest = async () => {
|
|
|
480
461
|
continue;
|
|
481
462
|
}
|
|
482
463
|
const button = buttons[step.index];
|
|
483
|
-
// `relatedTarget` OUTSIDE the button
|
|
484
|
-
//
|
|
485
|
-
// separate test below.
|
|
464
|
+
// `relatedTarget` OUTSIDE the button — a real cross-button move; the
|
|
465
|
+
// internal case is tested below.
|
|
486
466
|
button?.dispatchEvent(
|
|
487
467
|
new DragEvent(step.kind === 'enter' ? 'dragenter' : 'dragleave', {
|
|
488
468
|
bubbles: true,
|
|
@@ -496,11 +476,10 @@ export const AdwViewSwitcherTest = async () => {
|
|
|
496
476
|
}
|
|
497
477
|
|
|
498
478
|
await it('a drag sliding from the icon to the LABEL does not restart the dwell', () => {
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
// the button never switched at all.
|
|
479
|
+
// A button CONTAINS an icon and a label, so crossing between them fires
|
|
480
|
+
// leave + enter; without the relatedTarget check that cleared the timer and
|
|
481
|
+
// re-armed a fresh 500 ms, so a drag across the button never switched. The
|
|
482
|
+
// vectors cannot see this.
|
|
504
483
|
const pages: ViewSwitcherVectorPage[] = [
|
|
505
484
|
{ name: 'one', title: 'One' },
|
|
506
485
|
{ name: 'two', title: 'Two' },
|
|
@@ -524,7 +503,7 @@ export const AdwViewSwitcherTest = async () => {
|
|
|
524
503
|
|
|
525
504
|
await it('a page attribute changed AFTER connect reaches the button', async () => {
|
|
526
505
|
// `observedAttributes` without an `attributeChangedCallback` is a dead
|
|
527
|
-
// declaration
|
|
506
|
+
// declaration, where C rebinds on every page notify.
|
|
528
507
|
const host = document.createElement('div');
|
|
529
508
|
document.body.appendChild(host);
|
|
530
509
|
const switcher = document.createElement('adw-view-switcher') as AdwViewSwitcher;
|
|
@@ -537,10 +516,8 @@ export const AdwViewSwitcherTest = async () => {
|
|
|
537
516
|
(switcher.querySelector('.adw-view-switcher-label') as HTMLElement | null)?.textContent;
|
|
538
517
|
expect(labelOf()).toBe('One');
|
|
539
518
|
page.setAttribute('title', 'Renamed');
|
|
540
|
-
// A MutationObserver delivers on a MICROTASK
|
|
541
|
-
//
|
|
542
|
-
// the DOM offers for watching a detached node; nothing here depends
|
|
543
|
-
// on the repaint happening inside the setter.
|
|
519
|
+
// A MutationObserver delivers on a MICROTASK where GTK's `notify` is sync;
|
|
520
|
+
// nothing depends on the repaint happening inside the setter.
|
|
544
521
|
await Promise.resolve();
|
|
545
522
|
expect(labelOf()).toBe('Renamed');
|
|
546
523
|
host.remove();
|
|
@@ -559,8 +536,8 @@ export const AdwViewSwitcherTest = async () => {
|
|
|
559
536
|
|
|
560
537
|
const indicator = switcher.querySelector('.adw-view-switcher-indicator') as HTMLElement;
|
|
561
538
|
expect(indicator.textContent).toBe(vector.badgeLabel);
|
|
562
|
-
// The DESCRIPTION is what a screen reader announces
|
|
563
|
-
//
|
|
539
|
+
// The DESCRIPTION is what a screen reader announces: a badge with no
|
|
540
|
+
// accessible text is a dot nobody hears.
|
|
564
541
|
const button = switcher.querySelector('.adw-view-switcher-button') as HTMLElement;
|
|
565
542
|
expect(button.getAttribute('aria-description') ?? '').toBe(vector.description);
|
|
566
543
|
host.remove();
|
|
@@ -568,11 +545,10 @@ export const AdwViewSwitcherTest = async () => {
|
|
|
568
545
|
}
|
|
569
546
|
|
|
570
547
|
await it('names the three tables that stay the core suite’s', () => {
|
|
571
|
-
// MNEMONIC and ICON are
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
//
|
|
575
|
-
// so the omission is a decision rather than a gap.
|
|
548
|
+
// MNEMONIC and ICON are string derivations with no DOM surface beyond the
|
|
549
|
+
// label VIEW_SWITCHER_BUTTON_VECTORS already asserts; BUTTON_VISIBILITY and
|
|
550
|
+
// REBUILD are asserted through that table's rendered button set. Named so
|
|
551
|
+
// the omission is a decision rather than a gap.
|
|
576
552
|
expect(VIEW_SWITCHER_MNEMONIC_VECTORS.length).toBeGreaterThan(0);
|
|
577
553
|
expect(VIEW_SWITCHER_ICON_VECTORS.length).toBeGreaterThan(0);
|
|
578
554
|
expect(VIEW_SWITCHER_BUTTON_VISIBILITY_VECTORS.length).toBeGreaterThan(0);
|
|
@@ -582,10 +558,9 @@ export const AdwViewSwitcherTest = async () => {
|
|
|
582
558
|
|
|
583
559
|
await describe('<adw-view-switcher-bar> reveal follows the pages model, not just the selection', async () => {
|
|
584
560
|
await it('retracts when a page is REMOVED without moving the selection', () => {
|
|
585
|
-
// `update_bar_revealed` re-runs on the pages model's `items-changed
|
|
586
|
-
//
|
|
587
|
-
// `
|
|
588
|
-
// `refresh()` was the documented workaround.
|
|
561
|
+
// `update_bar_revealed` re-runs on the pages model's `items-changed`.
|
|
562
|
+
// Listening only on `notify::visible-child` leaves the bar stale, with a
|
|
563
|
+
// manual `refresh()` as the workaround.
|
|
589
564
|
const host = document.createElement('div');
|
|
590
565
|
document.body.appendChild(host);
|
|
591
566
|
const stack = document.createElement('adw-view-stack') as AdwViewStack;
|
|
@@ -627,13 +602,10 @@ export const AdwViewSwitcherTest = async () => {
|
|
|
627
602
|
});
|
|
628
603
|
});
|
|
629
604
|
|
|
630
|
-
//
|
|
631
|
-
//
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
// been moved between parents (a slideshow slide, a client-side route change)
|
|
635
|
-
// stopped tracking its pages permanently, and silently. Same shape as the
|
|
636
|
-
// overlay split view's ResizeObserver, and found with it.
|
|
605
|
+
// `disconnectedCallback` drops the page observer, and `connectedCallback` returned
|
|
606
|
+
// early once the element was built — so a switcher merely MOVED between parents (a
|
|
607
|
+
// slideshow slide, a client-side route change) stopped tracking its pages
|
|
608
|
+
// permanently and silently. Same shape as the overlay split view's ResizeObserver.
|
|
637
609
|
await describe('adw-view-switcher — a moved switcher keeps watching its pages', async () => {
|
|
638
610
|
await it('still notices a page attribute change after being re-parented', async () => {
|
|
639
611
|
const from = document.createElement('div');
|