@gjsify/adwaita-web 0.31.0 → 0.33.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 +835 -299
- 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-combo-row.d.ts +10 -0
- 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 +73 -11
- package/lib/types/elements/adw-menu-button.d.ts +6 -6
- package/lib/types/elements/adw-navigation-split-view.d.ts +48 -0
- package/lib/types/elements/adw-navigation-view.d.ts +63 -20
- package/lib/types/elements/adw-overlay-split-view.d.ts +94 -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-spinner.d.ts +23 -0
- 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 +39 -19
- package/lib/types/elements/adw-view-switcher-bar.d.ts +25 -7
- package/lib/types/elements/adw-view-switcher.d.ts +76 -13
- package/lib/types/elements/adw-window-title.d.ts +3 -1
- package/lib/types/elements/adw-wrap-box.d.ts +58 -3
- 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/_combo_row.scss +19 -0
- 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/_navigation_split_view.scss +47 -8
- package/scss/_overlay_split_view.scss +117 -32
- 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 +37 -19
- 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 +63 -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 +65 -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 +176 -0
- package/src/adw-banner.spec.ts +152 -0
- package/src/adw-button-content.spec.ts +207 -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-row-state.spec.ts +49 -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 +341 -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 +400 -0
- package/src/elements/adw-about-dialog.ts +223 -163
- package/src/elements/adw-action-row.ts +131 -11
- 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 +32 -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 +325 -131
- package/src/elements/adw-menu-button.ts +93 -70
- package/src/elements/adw-navigation-split-view.ts +220 -14
- package/src/elements/adw-navigation-view.ts +286 -148
- package/src/elements/adw-overlay-split-view.ts +536 -28
- 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 +168 -9
- 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 +700 -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 +183 -85
- package/src/elements/adw-view-switcher-bar.ts +171 -81
- package/src/elements/adw-view-switcher.ts +328 -117
- package/src/elements/adw-window-title.ts +44 -11
- package/src/elements/adw-wrap-box.ts +329 -78
- 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 +893 -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 +665 -0
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
// The two split views against the shared conformance vectors.
|
|
2
|
+
//
|
|
3
|
+
// The element now delegates the property interplay to
|
|
4
|
+
// `OverlaySplitViewState`, so this suite asserts the half a DOM renderer owns:
|
|
5
|
+
// that the attributes reach the state, and that what the state derives actually
|
|
6
|
+
// reaches the DOM.
|
|
7
|
+
//
|
|
8
|
+
// The rule it pins down first is the one the element got wrong for its whole
|
|
9
|
+
// life: `Adw.OverlaySplitView:show-sidebar` defaults to TRUE
|
|
10
|
+
// (adw-overlay-split-view.c:974-976), and an HTML boolean attribute is
|
|
11
|
+
// presence-only, which can only express a FALSE default. Bare markup showed no
|
|
12
|
+
// sidebar where GTK shows one.
|
|
13
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
14
|
+
|
|
15
|
+
import type { OverlaySplitViewState } from '@gjsify/adwaita-core';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
OVERLAY_COLLAPSE_VECTORS,
|
|
19
|
+
OVERLAY_SWIPE_CANCEL_VECTORS,
|
|
20
|
+
OVERLAY_SWIPE_RELEASE_VECTORS,
|
|
21
|
+
OVERLAY_SWIPE_SNAP_POINT_VECTORS,
|
|
22
|
+
OVERLAY_SWIPE_START_VECTORS,
|
|
23
|
+
NAVIGATION_ACTION_VECTORS,
|
|
24
|
+
OVERLAY_SWIPE_AREA_VECTORS,
|
|
25
|
+
OVERLAY_SPLIT_VIEW_LAYOUT_VECTORS,
|
|
26
|
+
NAVIGATION_STACK_VECTORS,
|
|
27
|
+
SIDEBAR_BOUNDS_VECTORS,
|
|
28
|
+
} from '@gjsify/adwaita-core/conformance';
|
|
29
|
+
|
|
30
|
+
import type { AdwNavigationSplitView } from './elements/adw-navigation-split-view.js';
|
|
31
|
+
import type { AdwOverlaySplitView } from './elements/adw-overlay-split-view.js';
|
|
32
|
+
|
|
33
|
+
/** Wait for a ResizeObserver delivery (it runs after layout, before paint). */
|
|
34
|
+
function settle(): Promise<void> {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Mount a split view from markup and return it. */
|
|
41
|
+
function mount(attrs = ''): { view: AdwOverlaySplitView; host: HTMLElement } {
|
|
42
|
+
const host = document.createElement('div');
|
|
43
|
+
document.body.appendChild(host);
|
|
44
|
+
host.innerHTML =
|
|
45
|
+
`<adw-overlay-split-view ${attrs}>` +
|
|
46
|
+
'<div slot="sidebar">s</div><div slot="content">c</div>' +
|
|
47
|
+
'</adw-overlay-split-view>';
|
|
48
|
+
return { view: host.querySelector('adw-overlay-split-view') as AdwOverlaySplitView, host };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The parts of `OverlaySplitViewSnapshot` a DOM renderer actually publishes. */
|
|
52
|
+
interface DomSnapshot {
|
|
53
|
+
showSidebar: boolean;
|
|
54
|
+
collapsed: boolean;
|
|
55
|
+
shieldVisible: boolean;
|
|
56
|
+
sidebarFocusable: boolean;
|
|
57
|
+
contentFocusable: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Read the whole derived state back OUT of the DOM.
|
|
62
|
+
*
|
|
63
|
+
* Asserting only `showSidebar`/`collapsed` is what let `pin-sidebar` fall out of
|
|
64
|
+
* `observedAttributes` unnoticed: both of those still agreed, because the vectors
|
|
65
|
+
* only ever set the attribute in markup, where `connectedCallback` reads it
|
|
66
|
+
* directly. Every field the element publishes is checked here instead.
|
|
67
|
+
*
|
|
68
|
+
* `showProgress` is the one snapshot field with no DOM surface — the element
|
|
69
|
+
* paints the reveal from the `collapsed`/`show-sidebar` classes rather than a
|
|
70
|
+
* numeric progress (a continuous reveal is not implemented). `shieldVisible` is
|
|
71
|
+
* derived from it (`collapsed && showProgress > 0`), so a progress bug still
|
|
72
|
+
* shows up here.
|
|
73
|
+
*/
|
|
74
|
+
function domSnapshot(view: AdwOverlaySplitView): DomSnapshot {
|
|
75
|
+
const backdrop = view.querySelector('.adw-osv-backdrop') as HTMLElement;
|
|
76
|
+
const sidebar = view.querySelector('.adw-osv-sidebar') as HTMLElement;
|
|
77
|
+
const content = view.querySelector('.adw-osv-content') as HTMLElement;
|
|
78
|
+
return {
|
|
79
|
+
showSidebar: view.classList.contains('show-sidebar'),
|
|
80
|
+
collapsed: view.classList.contains('collapsed'),
|
|
81
|
+
shieldVisible: !backdrop.hidden,
|
|
82
|
+
sidebarFocusable: sidebar.getAttribute('aria-hidden') === 'false',
|
|
83
|
+
contentFocusable: content.getAttribute('aria-hidden') === 'false',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The same five fields, taken from a conformance snapshot. */
|
|
88
|
+
function expectedSnapshot(snapshot: {
|
|
89
|
+
showSidebar: boolean;
|
|
90
|
+
collapsed: boolean;
|
|
91
|
+
shieldVisible: boolean;
|
|
92
|
+
sidebarFocusable: boolean;
|
|
93
|
+
contentFocusable: boolean;
|
|
94
|
+
}): DomSnapshot {
|
|
95
|
+
return {
|
|
96
|
+
showSidebar: snapshot.showSidebar,
|
|
97
|
+
collapsed: snapshot.collapsed,
|
|
98
|
+
shieldVisible: snapshot.shieldVisible,
|
|
99
|
+
sidebarFocusable: snapshot.sidebarFocusable,
|
|
100
|
+
contentFocusable: snapshot.contentFocusable,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const AdwSplitViewsTest = async () => {
|
|
105
|
+
await describe('adw-overlay-split-view defaults (Adw.OverlaySplitView properties)', async () => {
|
|
106
|
+
await it('shows the sidebar with no attributes at all, like GTK does', () => {
|
|
107
|
+
const { view, host } = mount();
|
|
108
|
+
expect(view.showSidebar).toBe(true);
|
|
109
|
+
expect(view.classList.contains('show-sidebar')).toBe(true);
|
|
110
|
+
host.remove();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await it('takes show-sidebar="false" as the explicit off switch', () => {
|
|
114
|
+
const { view, host } = mount('show-sidebar="false"');
|
|
115
|
+
expect(view.showSidebar).toBe(false);
|
|
116
|
+
expect(view.classList.contains('show-sidebar')).toBe(false);
|
|
117
|
+
host.remove();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await it('still treats a bare show-sidebar as true', () => {
|
|
121
|
+
const { view, host } = mount('show-sidebar');
|
|
122
|
+
expect(view.showSidebar).toBe(true);
|
|
123
|
+
host.remove();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await it('round-trips the property setter through the attribute', () => {
|
|
127
|
+
const { view, host } = mount();
|
|
128
|
+
view.showSidebar = false;
|
|
129
|
+
expect(view.showSidebar).toBe(false);
|
|
130
|
+
view.showSidebar = true;
|
|
131
|
+
expect(view.showSidebar).toBe(true);
|
|
132
|
+
host.remove();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await describe('adw-overlay-split-view collapse (shared conformance vectors)', async () => {
|
|
137
|
+
for (const vector of OVERLAY_COLLAPSE_VECTORS) {
|
|
138
|
+
const { initial, before, setCollapsed, after, rule } = vector;
|
|
139
|
+
// Only the rows a DOM element can express: `pinSidebar` and the
|
|
140
|
+
// starting `collapsed`/`showSidebar` all have attributes.
|
|
141
|
+
const attrs = [
|
|
142
|
+
initial.collapsed ? 'collapsed' : '',
|
|
143
|
+
initial.showSidebar === false ? 'show-sidebar="false"' : '',
|
|
144
|
+
initial.pinSidebar ? 'pin-sidebar' : '',
|
|
145
|
+
]
|
|
146
|
+
.filter(Boolean)
|
|
147
|
+
.join(' ');
|
|
148
|
+
|
|
149
|
+
await it(`${JSON.stringify(initial)} → collapsed=${setCollapsed} — ${rule}`, () => {
|
|
150
|
+
const { view, host } = mount(attrs);
|
|
151
|
+
expect(view.showSidebar).toBe(before.showSidebar);
|
|
152
|
+
expect(view.collapsed).toBe(before.collapsed);
|
|
153
|
+
expect(domSnapshot(view)).toStrictEqual(expectedSnapshot(before));
|
|
154
|
+
|
|
155
|
+
view.collapsed = setCollapsed;
|
|
156
|
+
expect(view.showSidebar).toBe(after.showSidebar);
|
|
157
|
+
expect(view.collapsed).toBe(after.collapsed);
|
|
158
|
+
expect(domSnapshot(view)).toStrictEqual(expectedSnapshot(after));
|
|
159
|
+
host.remove();
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
await it('honours pin-sidebar set AFTER construction, not only in markup', () => {
|
|
164
|
+
// The regression the vector loop above cannot see: it only ever puts
|
|
165
|
+
// `pin-sidebar` in the markup, which `connectedCallback` reads directly.
|
|
166
|
+
// With the attribute missing from `observedAttributes` the
|
|
167
|
+
// `_readAttribute` branch was dead, so pinning a live element silently
|
|
168
|
+
// did nothing and the next collapse still hid the sidebar.
|
|
169
|
+
const { view, host } = mount();
|
|
170
|
+
view.setAttribute('pin-sidebar', '');
|
|
171
|
+
|
|
172
|
+
view.collapsed = true;
|
|
173
|
+
expect(domSnapshot(view)).toStrictEqual({
|
|
174
|
+
showSidebar: true,
|
|
175
|
+
collapsed: true,
|
|
176
|
+
shieldVisible: true,
|
|
177
|
+
sidebarFocusable: true,
|
|
178
|
+
contentFocusable: false,
|
|
179
|
+
});
|
|
180
|
+
host.remove();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await it('unpins again when the attribute is removed', () => {
|
|
184
|
+
const { view, host } = mount('pin-sidebar');
|
|
185
|
+
view.removeAttribute('pin-sidebar');
|
|
186
|
+
|
|
187
|
+
view.collapsed = true;
|
|
188
|
+
expect(domSnapshot(view)).toStrictEqual({
|
|
189
|
+
showSidebar: false,
|
|
190
|
+
collapsed: true,
|
|
191
|
+
shieldVisible: false,
|
|
192
|
+
sidebarFocusable: false,
|
|
193
|
+
contentFocusable: true,
|
|
194
|
+
});
|
|
195
|
+
host.remove();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await describe('adw-overlay-split-view derived state reaches the DOM', async () => {
|
|
200
|
+
await it('hides the shield unless collapsed AND revealed', () => {
|
|
201
|
+
const { view, host } = mount();
|
|
202
|
+
const backdrop = view.querySelector('.adw-osv-backdrop') as HTMLElement;
|
|
203
|
+
// Expanded: no shield, whatever the sidebar is doing.
|
|
204
|
+
expect(backdrop.hidden).toBe(true);
|
|
205
|
+
host.remove();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
await it('marks the off-screen pane aria-hidden once collapsing hides it', () => {
|
|
209
|
+
// Collapsing is a TRANSITION: it auto-hides an unpinned sidebar, so
|
|
210
|
+
// content becomes the reachable pane. Constructing collapsed does
|
|
211
|
+
// NOT — `<adw-overlay-split-view collapsed>` keeps the sidebar shown,
|
|
212
|
+
// the same as building the GTK widget with both properties set.
|
|
213
|
+
const { view, host } = mount();
|
|
214
|
+
expect((view.querySelector('.adw-osv-sidebar') as HTMLElement).getAttribute('aria-hidden')).toBe('false');
|
|
215
|
+
|
|
216
|
+
view.collapsed = true;
|
|
217
|
+
expect((view.querySelector('.adw-osv-sidebar') as HTMLElement).getAttribute('aria-hidden')).toBe('true');
|
|
218
|
+
expect((view.querySelector('.adw-osv-content') as HTMLElement).getAttribute('aria-hidden')).toBe('false');
|
|
219
|
+
host.remove();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await it('constructing collapsed keeps the sidebar shown, unlike collapsing later', () => {
|
|
223
|
+
const { view, host } = mount('collapsed');
|
|
224
|
+
expect(view.showSidebar).toBe(true);
|
|
225
|
+
host.remove();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
await it('leaves both panes reachable while expanded', () => {
|
|
229
|
+
const { view, host } = mount();
|
|
230
|
+
expect((view.querySelector('.adw-osv-sidebar') as HTMLElement).getAttribute('aria-hidden')).toBe('false');
|
|
231
|
+
expect((view.querySelector('.adw-osv-content') as HTMLElement).getAttribute('aria-hidden')).toBe('false');
|
|
232
|
+
host.remove();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
await describe('adw-overlay-split-view sidebar width (Adw.OverlaySplitView properties)', async () => {
|
|
236
|
+
await it('defaults to the WIDGET values, not to an app preference', () => {
|
|
237
|
+
// 180 / 280 / 0.25 — adw-overlay-split-view.c:1036-1075. These read
|
|
238
|
+
// 280 / 400 / 0.30 before, which are the values the three.js and
|
|
239
|
+
// canvas2d showcases set explicitly in their .blp, so the storybook's
|
|
240
|
+
// GTK and browser sides showed different sidebars.
|
|
241
|
+
const { view, host } = mount();
|
|
242
|
+
expect(view.minSidebarWidth).toBe(180);
|
|
243
|
+
expect(view.maxSidebarWidth).toBe(280);
|
|
244
|
+
expect(view.sidebarWidthFraction).toBe(0.25);
|
|
245
|
+
host.remove();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
await it('writes the normalised bounds, not the raw attributes', () => {
|
|
249
|
+
const { view, host } = mount('min-sidebar-width="300" max-sidebar-width="200"');
|
|
250
|
+
const sidebar = view.querySelector('.adw-osv-sidebar') as HTMLElement;
|
|
251
|
+
// libadwaita never lets max fall below min, so both land at 300.
|
|
252
|
+
// CSS on its own resolves the conflict the other way (min-width wins
|
|
253
|
+
// over max-width), which would render a different widget.
|
|
254
|
+
expect(sidebar.style.minWidth).toBe('300px');
|
|
255
|
+
expect(sidebar.style.maxWidth).toBe('300px');
|
|
256
|
+
host.remove();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
for (const vector of SIDEBAR_BOUNDS_VECTORS) {
|
|
260
|
+
// Only the rows expressible in CSS pixels: the element has no dpi or
|
|
261
|
+
// length-unit attribute, so `sp`-at-another-dpi rows stay in core.
|
|
262
|
+
if (vector.spec.sidebarWidthUnit !== undefined || vector.spec.dpi !== undefined) continue;
|
|
263
|
+
if (vector.sidebarChildMin !== 0 || !vector.ceil) continue;
|
|
264
|
+
|
|
265
|
+
const attrs = [
|
|
266
|
+
vector.spec.minSidebarWidth !== undefined ? `min-sidebar-width="${vector.spec.minSidebarWidth}"` : '',
|
|
267
|
+
vector.spec.maxSidebarWidth !== undefined ? `max-sidebar-width="${vector.spec.maxSidebarWidth}"` : '',
|
|
268
|
+
]
|
|
269
|
+
.filter(Boolean)
|
|
270
|
+
.join(' ');
|
|
271
|
+
|
|
272
|
+
await it(`bounds ${JSON.stringify(vector.spec)} → ${vector.min}/${vector.max} — ${vector.rule}`, () => {
|
|
273
|
+
const { view, host } = mount(attrs);
|
|
274
|
+
const sidebar = view.querySelector('.adw-osv-sidebar') as HTMLElement;
|
|
275
|
+
expect(sidebar.style.minWidth).toBe(`${vector.min}px`);
|
|
276
|
+
expect(sidebar.style.maxWidth).toBe(`${vector.max}px`);
|
|
277
|
+
host.remove();
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
await describe('adw-navigation-split-view sidebar width (Adw.NavigationSplitView)', async () => {
|
|
282
|
+
const mountNav = (attrs = '') => {
|
|
283
|
+
const host = document.createElement('div');
|
|
284
|
+
document.body.appendChild(host);
|
|
285
|
+
host.innerHTML =
|
|
286
|
+
`<adw-navigation-split-view ${attrs}>` +
|
|
287
|
+
'<div slot="sidebar">s</div><div slot="content">c</div>' +
|
|
288
|
+
'</adw-navigation-split-view>';
|
|
289
|
+
return {
|
|
290
|
+
view: host.querySelector('adw-navigation-split-view') as AdwNavigationSplitView,
|
|
291
|
+
host,
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
await it('bounds the sidebar even with no attributes', () => {
|
|
296
|
+
// An absent attribute used to mean "no bound at all"; the widget
|
|
297
|
+
// always has 180 / 280.
|
|
298
|
+
const { view, host } = mountNav();
|
|
299
|
+
const sidebar = view.querySelector('.adw-nsv-sidebar') as HTMLElement;
|
|
300
|
+
expect(sidebar.style.minWidth).toBe('180px');
|
|
301
|
+
expect(sidebar.style.maxWidth).toBe('280px');
|
|
302
|
+
host.remove();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
await it('normalises an inverted pair the way GLib CLAMP does', () => {
|
|
306
|
+
const { view, host } = mountNav('min-sidebar-width="320" max-sidebar-width="200"');
|
|
307
|
+
const sidebar = view.querySelector('.adw-nsv-sidebar') as HTMLElement;
|
|
308
|
+
expect(sidebar.style.minWidth).toBe('320px');
|
|
309
|
+
expect(sidebar.style.maxWidth).toBe('320px');
|
|
310
|
+
host.remove();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
await it('drops the caps while collapsed, so the pane fills the view', () => {
|
|
314
|
+
const { view, host } = mountNav('collapsed');
|
|
315
|
+
const sidebar = view.querySelector('.adw-nsv-sidebar') as HTMLElement;
|
|
316
|
+
expect(sidebar.style.minWidth).toBe('');
|
|
317
|
+
expect(sidebar.style.maxWidth).toBe('');
|
|
318
|
+
host.remove();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await it('falls back to the default when the attribute is not a number', () => {
|
|
322
|
+
const { view, host } = mountNav('min-sidebar-width="wide"');
|
|
323
|
+
const sidebar = view.querySelector('.adw-nsv-sidebar') as HTMLElement;
|
|
324
|
+
expect(sidebar.style.minWidth).toBe('180px');
|
|
325
|
+
host.remove();
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
await describe('<adw-overlay-split-view> reveal is a CONTINUUM, not two states', async () => {
|
|
330
|
+
/** Mount a view of a known width so the geometry is predictable. */
|
|
331
|
+
function mountSizedView(width: number, attrs = ''): { view: AdwOverlaySplitView; host: HTMLElement } {
|
|
332
|
+
const { view, host } = mount(attrs);
|
|
333
|
+
host.style.width = `${width}px`;
|
|
334
|
+
host.style.height = '400px';
|
|
335
|
+
(view as unknown as HTMLElement).style.height = '400px';
|
|
336
|
+
return { view, host };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** The state the element composes — the same object the vectors describe. */
|
|
340
|
+
const stateOf = (view: AdwOverlaySplitView) => (view as unknown as { _state: OverlaySplitViewState })._state;
|
|
341
|
+
|
|
342
|
+
await it('drives show-progress through the whole range, not 0 and 1', async () => {
|
|
343
|
+
const { view, host } = mountSizedView(800);
|
|
344
|
+
const state = stateOf(view);
|
|
345
|
+
const sidebar = view.querySelector('.adw-osv-sidebar') as HTMLElement;
|
|
346
|
+
await settle();
|
|
347
|
+
|
|
348
|
+
const offsets = new Set<string>();
|
|
349
|
+
for (const progress of [0, 0.25, 0.5, 0.75, 1]) {
|
|
350
|
+
state.setShowProgress(progress);
|
|
351
|
+
offsets.add(sidebar.style.marginLeft || sidebar.style.left || '');
|
|
352
|
+
}
|
|
353
|
+
// Two end states would give at most two distinct offsets; the five
|
|
354
|
+
// OVERLAY_SWIPE_* tables need every value in between to mean something.
|
|
355
|
+
expect(offsets.size).toBeGreaterThan(2);
|
|
356
|
+
host.remove();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
for (const vector of OVERLAY_SWIPE_SNAP_POINT_VECTORS) {
|
|
360
|
+
await it(`snap points [${vector.snapPoints.join(', ')}] — ${vector.rule}`, async () => {
|
|
361
|
+
const attrs =
|
|
362
|
+
(vector.enableShowGesture ? '' : 'enable-show-gesture="false" ') +
|
|
363
|
+
(vector.enableHideGesture ? '' : 'enable-hide-gesture="false"');
|
|
364
|
+
const { view, host } = mountSizedView(800, attrs);
|
|
365
|
+
const state = stateOf(view);
|
|
366
|
+
await settle();
|
|
367
|
+
state.setShowProgress(vector.showProgress);
|
|
368
|
+
if (vector.swipeActive) state.beginSwipe();
|
|
369
|
+
expect(state.snapPoints).toStrictEqual([...vector.snapPoints]);
|
|
370
|
+
host.remove();
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
await it('grabs only where the swipe area is — the ADW_SWIPE_BORDER strip when closed', async () => {
|
|
375
|
+
// The row this probes: closed, start edge, LTR — the grabbable strip
|
|
376
|
+
// is `ADW_SWIPE_BORDER` wide and nothing outside it starts a gesture.
|
|
377
|
+
// What a DOM renderer owns is that second half, which no unit test of
|
|
378
|
+
// `resolveSwipeArea` can show.
|
|
379
|
+
const closedAtStart = OVERLAY_SWIPE_AREA_VECTORS.find(
|
|
380
|
+
(vector) =>
|
|
381
|
+
vector.isDrag &&
|
|
382
|
+
vector.showProgress === 0 &&
|
|
383
|
+
vector.sidebarPosition === 'start' &&
|
|
384
|
+
vector.direction === 'ltr',
|
|
385
|
+
);
|
|
386
|
+
if (!closedAtStart) throw new Error('OVERLAY_SWIPE_AREA_VECTORS lost its closed/start/ltr row');
|
|
387
|
+
const strip = closedAtStart.area.width;
|
|
388
|
+
|
|
389
|
+
const { view, host } = mountSizedView(800, 'collapsed show-sidebar="false"');
|
|
390
|
+
const state = stateOf(view);
|
|
391
|
+
await settle();
|
|
392
|
+
const rect = view.getBoundingClientRect();
|
|
393
|
+
|
|
394
|
+
const down = (offsetX: number) =>
|
|
395
|
+
view.dispatchEvent(
|
|
396
|
+
new PointerEvent('pointerdown', {
|
|
397
|
+
bubbles: true,
|
|
398
|
+
pointerId: 1,
|
|
399
|
+
button: 0,
|
|
400
|
+
clientX: rect.left + offsetX,
|
|
401
|
+
clientY: rect.top + 100,
|
|
402
|
+
}),
|
|
403
|
+
);
|
|
404
|
+
const move = (offsetX: number) =>
|
|
405
|
+
view.dispatchEvent(
|
|
406
|
+
new PointerEvent('pointermove', {
|
|
407
|
+
bubbles: true,
|
|
408
|
+
pointerId: 1,
|
|
409
|
+
clientX: rect.left + offsetX,
|
|
410
|
+
clientY: rect.top + 100,
|
|
411
|
+
}),
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
// Just OUTSIDE the strip, so nothing begins.
|
|
415
|
+
down(strip + 4);
|
|
416
|
+
move(strip + 120);
|
|
417
|
+
expect(state.swipeActive).toBe(false);
|
|
418
|
+
|
|
419
|
+
// Just inside it: the gesture takes over and the progress follows.
|
|
420
|
+
down(Math.max(strip - 4, 0));
|
|
421
|
+
move(strip + 120);
|
|
422
|
+
expect(state.swipeActive).toBe(true);
|
|
423
|
+
expect(state.showProgress).toBeGreaterThan(0);
|
|
424
|
+
host.remove();
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
for (const vector of OVERLAY_SWIPE_RELEASE_VECTORS) {
|
|
428
|
+
await it(`release at ${vector.to} — ${vector.rule}`, async () => {
|
|
429
|
+
const { view, host } = mountSizedView(800, 'collapsed');
|
|
430
|
+
const state = stateOf(view);
|
|
431
|
+
await settle();
|
|
432
|
+
// Seed `show-sidebar` without animating, then take the gesture.
|
|
433
|
+
state.setShowSidebar(vector.showSidebar, { animate: false });
|
|
434
|
+
state.beginSwipe();
|
|
435
|
+
const plan = state.endSwipe(vector.to);
|
|
436
|
+
expect(plan.kind).toBe(vector.kind);
|
|
437
|
+
// A `set-show-sidebar` release is the one that notifies, so the
|
|
438
|
+
// property has to have MOVED, not just the animation settled.
|
|
439
|
+
if (plan.kind === 'set-show-sidebar') expect(state.showSidebar).toBe(vector.showSidebarAfter);
|
|
440
|
+
host.remove();
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
await it('a cancelled pointer snaps back where get_cancel_progress says', async () => {
|
|
445
|
+
const { view, host } = mountSizedView(800, 'collapsed show-sidebar="false"');
|
|
446
|
+
const state = stateOf(view);
|
|
447
|
+
await settle();
|
|
448
|
+
const rect = view.getBoundingClientRect();
|
|
449
|
+
view.dispatchEvent(
|
|
450
|
+
new PointerEvent('pointerdown', {
|
|
451
|
+
bubbles: true,
|
|
452
|
+
pointerId: 2,
|
|
453
|
+
button: 0,
|
|
454
|
+
clientX: rect.left + 8,
|
|
455
|
+
clientY: rect.top + 100,
|
|
456
|
+
}),
|
|
457
|
+
);
|
|
458
|
+
view.dispatchEvent(
|
|
459
|
+
new PointerEvent('pointermove', {
|
|
460
|
+
bubbles: true,
|
|
461
|
+
pointerId: 2,
|
|
462
|
+
clientX: rect.left + 60,
|
|
463
|
+
clientY: rect.top + 100,
|
|
464
|
+
}),
|
|
465
|
+
);
|
|
466
|
+
expect(state.swipeActive).toBe(true);
|
|
467
|
+
view.dispatchEvent(new PointerEvent('pointercancel', { bubbles: true, pointerId: 2 }));
|
|
468
|
+
expect(state.swipeActive).toBe(false);
|
|
469
|
+
// A short drag from closed rounds back to closed.
|
|
470
|
+
expect(state.showSidebar).toBe(false);
|
|
471
|
+
host.remove();
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
await it('Escape closes a collapsed revealed sidebar', async () => {
|
|
475
|
+
// `escape_shortcut_cb` (adw-overlay-split-view.c:705-716) was absent
|
|
476
|
+
// from both ports, which made `OverlaySplitViewState.escape()` dead
|
|
477
|
+
// code.
|
|
478
|
+
const { view, host } = mountSizedView(800, 'collapsed');
|
|
479
|
+
await settle();
|
|
480
|
+
// Parse-time attributes are CONSTRUCTION options, not sequential
|
|
481
|
+
// setters, so `collapsed` in the markup does not fire the auto-hide
|
|
482
|
+
// — the element documents that and OVERLAY_COLLAPSE_VECTORS covers
|
|
483
|
+
// the setter path. So the sidebar is already revealed here.
|
|
484
|
+
expect(view.showSidebar).toBe(true);
|
|
485
|
+
|
|
486
|
+
const event = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true });
|
|
487
|
+
view.dispatchEvent(event);
|
|
488
|
+
expect(event.defaultPrevented).toBe(true);
|
|
489
|
+
expect(view.showSidebar).toBe(false);
|
|
490
|
+
host.remove();
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
await it('...and PROPAGATES from an uncollapsed one, so an enclosing dialog still closes', async () => {
|
|
494
|
+
const { view, host } = mountSizedView(800);
|
|
495
|
+
await settle();
|
|
496
|
+
expect(view.collapsed).toBe(false);
|
|
497
|
+
expect(view.showSidebar).toBe(true);
|
|
498
|
+
|
|
499
|
+
const event = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true });
|
|
500
|
+
view.dispatchEvent(event);
|
|
501
|
+
// A docked sidebar is not something Escape closes, however revealed
|
|
502
|
+
// it is — the gate is `!collapsed`, checked before the progress.
|
|
503
|
+
expect(event.defaultPrevented).toBe(false);
|
|
504
|
+
expect(view.showSidebar).toBe(true);
|
|
505
|
+
host.remove();
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
await it('is gated on the PROGRESS, so it still consumes mid-close', async () => {
|
|
509
|
+
// Worth pinning because it looks like a bug: `escape_shortcut_cb`
|
|
510
|
+
// tests `show_progress`, not `show_sidebar`, so a second Escape
|
|
511
|
+
// during the closing animation is consumed too and the set inside is
|
|
512
|
+
// a no-op. Reading the gate as "is the sidebar shown" would make the
|
|
513
|
+
// key propagate for ~250ms after the first press and close a dialog
|
|
514
|
+
// behind the view.
|
|
515
|
+
const { view, host } = mountSizedView(800, 'collapsed');
|
|
516
|
+
await settle();
|
|
517
|
+
expect(view.showSidebar).toBe(true);
|
|
518
|
+
|
|
519
|
+
const escape = () => {
|
|
520
|
+
const event = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true });
|
|
521
|
+
view.dispatchEvent(event);
|
|
522
|
+
return event.defaultPrevented;
|
|
523
|
+
};
|
|
524
|
+
expect(escape()).toBe(true);
|
|
525
|
+
expect(view.showSidebar).toBe(false);
|
|
526
|
+
// The reveal is still animating out, so the progress is above zero.
|
|
527
|
+
expect(escape()).toBe(true);
|
|
528
|
+
host.remove();
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
await it('slides off the edge the sidebar is ON, in both positions', async () => {
|
|
532
|
+
// `marginRight: -offsetWidth` regardless of `sidebarPosition` is what
|
|
533
|
+
// this used to do, so an `end` sidebar slid the wrong way.
|
|
534
|
+
const start = mountSizedView(800, 'show-sidebar="false"');
|
|
535
|
+
await settle();
|
|
536
|
+
const startBar = start.view.querySelector('.adw-osv-sidebar') as HTMLElement;
|
|
537
|
+
expect(startBar.style.marginLeft.startsWith('-')).toBe(true);
|
|
538
|
+
expect(startBar.style.marginRight).toBe('');
|
|
539
|
+
start.host.remove();
|
|
540
|
+
|
|
541
|
+
const end = mountSizedView(800, 'sidebar-position="end" show-sidebar="false"');
|
|
542
|
+
await settle();
|
|
543
|
+
const endBar = end.view.querySelector('.adw-osv-sidebar') as HTMLElement;
|
|
544
|
+
expect(endBar.style.marginRight.startsWith('-')).toBe(true);
|
|
545
|
+
expect(endBar.style.marginLeft).toBe('');
|
|
546
|
+
end.host.remove();
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
await it('mirrors under RTL — a start sidebar is drawn on the RIGHT', async () => {
|
|
550
|
+
const { view, host } = mountSizedView(800, 'show-sidebar="false"');
|
|
551
|
+
host.style.direction = 'rtl';
|
|
552
|
+
await settle();
|
|
553
|
+
// Re-run the geometry now the direction is readable.
|
|
554
|
+
view.setAttribute('sidebar-position', 'start');
|
|
555
|
+
await settle();
|
|
556
|
+
expect(view.classList.contains('sidebar-at-visual-start')).toBe(false);
|
|
557
|
+
const sidebar = view.querySelector('.adw-osv-sidebar') as HTMLElement;
|
|
558
|
+
expect(sidebar.style.marginRight.startsWith('-')).toBe(true);
|
|
559
|
+
host.remove();
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
await it('does not write a CSS PERCENTAGE width — (int) truncates, a percentage is fractional', async () => {
|
|
563
|
+
const { view, host } = mountSizedView(800);
|
|
564
|
+
await settle();
|
|
565
|
+
const sidebar = view.querySelector('.adw-osv-sidebar') as HTMLElement;
|
|
566
|
+
expect(sidebar.style.width.endsWith('%')).toBe(false);
|
|
567
|
+
expect(sidebar.style.width.endsWith('px')).toBe(true);
|
|
568
|
+
// 800 * 0.25 = 200, inside [180, 280], so it is the fraction exactly.
|
|
569
|
+
expect(sidebar.style.width).toBe('200px');
|
|
570
|
+
host.remove();
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
await it('keeps the content pane from being squeezed to nothing', async () => {
|
|
574
|
+
// `measure_uncollapsed` (:558-563) as CSS spells it: the pane claims
|
|
575
|
+
// its own min-content, where `min-width: 0` let the sidebar take
|
|
576
|
+
// everything.
|
|
577
|
+
const { view, host } = mountSizedView(800);
|
|
578
|
+
await settle();
|
|
579
|
+
const content = view.querySelector('.adw-osv-content') as HTMLElement;
|
|
580
|
+
expect(getComputedStyle(content).minWidth).not.toBe('0px');
|
|
581
|
+
host.remove();
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
for (const vector of OVERLAY_SPLIT_VIEW_LAYOUT_VECTORS) {
|
|
585
|
+
// Its exact analogue NAVIGATION_SPLIT_VIEW_LAYOUT_VECTORS was driven
|
|
586
|
+
// from the NativeScript suite while this one was driven by nobody —
|
|
587
|
+
// two sibling tables in the same file, with no reason for the
|
|
588
|
+
// difference. The element writes the pane rect from
|
|
589
|
+
// `layoutOverlaySplitView` now, so the rects are readable.
|
|
590
|
+
const label = `${vector.collapsed ? 'collapsed' : 'docked'} ${vector.sidebarPosition}/${vector.direction} @${vector.showProgress}`;
|
|
591
|
+
await it(`${label} — ${vector.rule}`, async () => {
|
|
592
|
+
const attrs =
|
|
593
|
+
(vector.collapsed ? 'collapsed ' : '') +
|
|
594
|
+
(vector.sidebarPosition === 'end' ? 'sidebar-position="end" ' : '') +
|
|
595
|
+
'pin-sidebar';
|
|
596
|
+
const { view, host } = mountSizedView(vector.totalWidth, attrs);
|
|
597
|
+
if (vector.direction === 'rtl') host.style.direction = 'rtl';
|
|
598
|
+
await settle();
|
|
599
|
+
const state = stateOf(view);
|
|
600
|
+
state.setShowProgress(vector.showProgress);
|
|
601
|
+
|
|
602
|
+
// The SHIELD and the paint gate are the element's to publish;
|
|
603
|
+
// the pixel rects depend on the measured sidebar width, which a
|
|
604
|
+
// real layout decides, so the shared half is what is compared.
|
|
605
|
+
const backdrop = view.querySelector('.adw-osv-backdrop') as HTMLElement;
|
|
606
|
+
expect(backdrop.hidden).toBe(!vector.layout.shieldVisible);
|
|
607
|
+
const sidebar = view.querySelector('.adw-osv-sidebar') as HTMLElement;
|
|
608
|
+
expect(sidebar.style.pointerEvents === 'none').toBe(!vector.layout.sidebarPainted);
|
|
609
|
+
host.remove();
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
await it('names the cancel rows the core owns, so this suite is not read as covering them', () => {
|
|
614
|
+
// `swipeCancelProgress`'s half-away-from-zero rounding is arithmetic
|
|
615
|
+
// with no DOM surface — a pointercancel can only show the rounded
|
|
616
|
+
// OUTCOME, not the rule. The core suite drives every row.
|
|
617
|
+
expect(OVERLAY_SWIPE_CANCEL_VECTORS.length).toBeGreaterThan(0);
|
|
618
|
+
expect(OVERLAY_SWIPE_START_VECTORS.length).toBeGreaterThan(0);
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
await describe('<adw-navigation-split-view> stack, tags and navigation.* actions', async () => {
|
|
623
|
+
/** Mount a navigation split view with the given panes and attributes. */
|
|
624
|
+
function mountNav(attrs = '', panes = { sidebar: true, content: true, sidebarTag: '', contentTag: '' }) {
|
|
625
|
+
const host = document.createElement('div');
|
|
626
|
+
host.style.width = '800px';
|
|
627
|
+
host.style.height = '400px';
|
|
628
|
+
document.body.appendChild(host);
|
|
629
|
+
const sidebar = panes.sidebar
|
|
630
|
+
? `<div slot="sidebar"${panes.sidebarTag ? ` tag="${panes.sidebarTag}"` : ''}>s</div>`
|
|
631
|
+
: '';
|
|
632
|
+
const content = panes.content
|
|
633
|
+
? `<div slot="content"${panes.contentTag ? ` tag="${panes.contentTag}"` : ''}>c</div>`
|
|
634
|
+
: '';
|
|
635
|
+
host.innerHTML = `<adw-navigation-split-view ${attrs}>${sidebar}${content}</adw-navigation-split-view>`;
|
|
636
|
+
return {
|
|
637
|
+
view: host.querySelector('adw-navigation-split-view') as AdwNavigationSplitView,
|
|
638
|
+
host,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** Which pane the element says is on top, read off the DOM. */
|
|
643
|
+
const visiblePane = (view: AdwNavigationSplitView) => {
|
|
644
|
+
const sidebar = view.querySelector('.adw-nsv-sidebar') as HTMLElement;
|
|
645
|
+
const content = view.querySelector('.adw-nsv-content') as HTMLElement;
|
|
646
|
+
return {
|
|
647
|
+
sidebar: sidebar?.dataset.paneVisible === 'true',
|
|
648
|
+
content: content?.dataset.paneVisible === 'true',
|
|
649
|
+
};
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
for (const vector of NAVIGATION_STACK_VECTORS) {
|
|
653
|
+
// Only the STATIC rows: the animated branch reaches the same stack by
|
|
654
|
+
// a push or a pop, and a DOM renderer that has no transition cannot
|
|
655
|
+
// tell them apart. The core suite drives the direction.
|
|
656
|
+
if (vector.changingPage) continue;
|
|
657
|
+
const label =
|
|
658
|
+
`${vector.hasSidebar ? 'sidebar' : '-'}/${vector.hasContent ? 'content' : '-'} ` +
|
|
659
|
+
`${vector.sidebarPosition} show-content=${vector.showContent}`;
|
|
660
|
+
await it(`${label} → [${vector.plan.stack.join(', ')}] — ${vector.rule}`, async () => {
|
|
661
|
+
const attrs =
|
|
662
|
+
'collapsed ' +
|
|
663
|
+
(vector.showContent ? 'show-content ' : '') +
|
|
664
|
+
(vector.sidebarPosition === 'end' ? 'sidebar-position="end"' : '');
|
|
665
|
+
const { view, host } = mountNav(attrs, {
|
|
666
|
+
sidebar: vector.hasSidebar,
|
|
667
|
+
content: vector.hasContent,
|
|
668
|
+
sidebarTag: '',
|
|
669
|
+
contentTag: '',
|
|
670
|
+
});
|
|
671
|
+
await settle();
|
|
672
|
+
const top = vector.plan.stack[vector.plan.stack.length - 1] ?? null;
|
|
673
|
+
const panes = visiblePane(view);
|
|
674
|
+
// A LONE child stays visible whatever `show-content` says — the
|
|
675
|
+
// rule two CSS classes could not express, and the reason a
|
|
676
|
+
// collapsed sidebar-only view used to render blank.
|
|
677
|
+
if (vector.hasSidebar) expect(panes.sidebar).toBe(top === 'sidebar');
|
|
678
|
+
if (vector.hasContent) expect(panes.content).toBe(top === 'content');
|
|
679
|
+
host.remove();
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
for (const vector of NAVIGATION_ACTION_VECTORS) {
|
|
684
|
+
const label =
|
|
685
|
+
vector.action === 'push'
|
|
686
|
+
? `push "${vector.tag}" (sidebar=${vector.sidebarTag ?? 'none'}, content=${vector.contentTag ?? 'none'})`
|
|
687
|
+
: `pop (sidebar=${vector.hasSidebar}, content=${vector.hasContent})`;
|
|
688
|
+
// A row whose two panes carry the SAME tag describes a state the
|
|
689
|
+
// duplicate-tag guard makes unreachable through a real widget: the
|
|
690
|
+
// second assignment is refused (:1195-1201), so the element can never
|
|
691
|
+
// hold both. Asserted below instead of driven here.
|
|
692
|
+
const collides =
|
|
693
|
+
vector.action === 'push' && vector.sidebarTag != null && vector.sidebarTag === vector.contentTag;
|
|
694
|
+
if (collides) continue;
|
|
695
|
+
|
|
696
|
+
await it(`${label} → ${vector.result.kind} — ${vector.rule}`, async () => {
|
|
697
|
+
const attrs = (vector.collapsed ? 'collapsed ' : '') + (vector.showContent ? 'show-content' : '');
|
|
698
|
+
const { view, host } = mountNav(attrs, {
|
|
699
|
+
sidebar: vector.action === 'pop' ? (vector.hasSidebar ?? false) : true,
|
|
700
|
+
content: vector.action === 'pop' ? (vector.hasContent ?? false) : true,
|
|
701
|
+
sidebarTag: vector.sidebarTag ?? '',
|
|
702
|
+
contentTag: vector.contentTag ?? '',
|
|
703
|
+
});
|
|
704
|
+
// `delegate` is the ROUTING's answer; whether it survives depends
|
|
705
|
+
// on the ancestor. An ancestor that claims the tag is the case the
|
|
706
|
+
// table describes — the unclaimed one becomes a critical, which
|
|
707
|
+
// is the state's own step and has its own test below.
|
|
708
|
+
host.addEventListener('navigation-push', (event) => {
|
|
709
|
+
(event as CustomEvent<{ handled: boolean }>).detail.handled = true;
|
|
710
|
+
});
|
|
711
|
+
host.addEventListener('navigation-pop', (event) => {
|
|
712
|
+
(event as CustomEvent<{ handled: boolean }>).detail.handled = true;
|
|
713
|
+
});
|
|
714
|
+
await settle();
|
|
715
|
+
const result = vector.action === 'push' ? view.push(vector.tag as string) : view.pop();
|
|
716
|
+
expect(result.kind).toBe(vector.result.kind);
|
|
717
|
+
if (result.kind === 'set-show-content' && vector.result.kind === 'set-show-content') {
|
|
718
|
+
expect(view.showContent).toBe(vector.result.showContent);
|
|
719
|
+
// The DOM has to agree, or a consumer reading the attribute
|
|
720
|
+
// back after a push sees the pre-push answer.
|
|
721
|
+
expect(view.hasAttribute('show-content')).toBe(vector.result.showContent);
|
|
722
|
+
}
|
|
723
|
+
host.remove();
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
await it('leaves out only the rows the tag guard makes unreachable', () => {
|
|
728
|
+
const collidingRows = NAVIGATION_ACTION_VECTORS.filter(
|
|
729
|
+
(v) => v.action === 'push' && v.sidebarTag != null && v.sidebarTag === v.contentTag,
|
|
730
|
+
);
|
|
731
|
+
// Exactly the shared-tag pushes. Their `rule` even says so — the C's
|
|
732
|
+
// routing tests the content tag first, which only matters in a
|
|
733
|
+
// collision the setters refuse to create.
|
|
734
|
+
expect(collidingRows.length).toBeGreaterThan(0);
|
|
735
|
+
for (const row of collidingRows) expect(row.contentTag).toBe(row.sidebarTag);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
await it('an UNCLAIMED push becomes a critical, not a silent no-op', async () => {
|
|
739
|
+
// `navigation_push_cb` :683 — the parent gets first refusal, and only
|
|
740
|
+
// then is it an error. A pop walks off the end silently instead.
|
|
741
|
+
const { view, host } = mountNav('collapsed', {
|
|
742
|
+
sidebar: true,
|
|
743
|
+
content: true,
|
|
744
|
+
sidebarTag: 'list',
|
|
745
|
+
contentTag: 'detail',
|
|
746
|
+
});
|
|
747
|
+
await settle();
|
|
748
|
+
const pushed = view.push('nowhere');
|
|
749
|
+
expect(pushed.kind).toBe('not-found');
|
|
750
|
+
expect(view.showContent).toBe(false);
|
|
751
|
+
host.remove();
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
await it('delegates an unmatched push to an ancestor, which is how nesting forwards it', async () => {
|
|
755
|
+
const { view, host } = mountNav('collapsed', {
|
|
756
|
+
sidebar: true,
|
|
757
|
+
content: true,
|
|
758
|
+
sidebarTag: 'list',
|
|
759
|
+
contentTag: 'detail',
|
|
760
|
+
});
|
|
761
|
+
await settle();
|
|
762
|
+
const seen: string[] = [];
|
|
763
|
+
host.addEventListener('navigation-push', (event) => {
|
|
764
|
+
const detail = (event as CustomEvent<{ tag?: string; handled: boolean }>).detail;
|
|
765
|
+
seen.push(detail.tag ?? '');
|
|
766
|
+
// An ancestor that OWNS the tag says so, and the critical is
|
|
767
|
+
// never reached — `return TRUE` in the C's routing.
|
|
768
|
+
detail.handled = true;
|
|
769
|
+
});
|
|
770
|
+
const result = view.push('somewhere-else');
|
|
771
|
+
expect(seen).toStrictEqual(['somewhere-else']);
|
|
772
|
+
expect(result.kind).toBe('delegate');
|
|
773
|
+
host.remove();
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
await it('REFUSES a pane whose tag the other one already carries', async () => {
|
|
777
|
+
// `adw_navigation_split_view_set_content` :1195-1201 — the pane keeps
|
|
778
|
+
// what it had; the assignment does not happen. Neither port had a
|
|
779
|
+
// duplicate-tag guard at all.
|
|
780
|
+
const { view, host } = mountNav('', {
|
|
781
|
+
sidebar: true,
|
|
782
|
+
content: true,
|
|
783
|
+
sidebarTag: 'same',
|
|
784
|
+
contentTag: 'same',
|
|
785
|
+
});
|
|
786
|
+
await settle();
|
|
787
|
+
expect(view.sidebarTag).toBe('same');
|
|
788
|
+
// The content was refused, so it carries no tag of record.
|
|
789
|
+
expect(view.contentTag).toBe(null);
|
|
790
|
+
host.remove();
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
await it('puts the divider on the side the sidebar is DRAWN on', async () => {
|
|
794
|
+
const start = mountNav();
|
|
795
|
+
await settle();
|
|
796
|
+
expect(start.view.classList.contains('sidebar-at-visual-start')).toBe(true);
|
|
797
|
+
start.host.remove();
|
|
798
|
+
|
|
799
|
+
const end = mountNav('sidebar-position="end"');
|
|
800
|
+
await settle();
|
|
801
|
+
expect(end.view.classList.contains('sidebar-at-visual-start')).toBe(false);
|
|
802
|
+
end.host.remove();
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
// A VIEW THAT WAS NEVER MEASURED, AND A VIEW THAT WAS MOVED.
|
|
807
|
+
//
|
|
808
|
+
// Both shipped broken in v0.32.0 and were visible on the docs site: its
|
|
809
|
+
// slideshow builds each slide inside `display: none` and moves it in, so the
|
|
810
|
+
// controls pane had no measurement when it first rendered and no live
|
|
811
|
+
// observer afterwards. A hidden sidebar was painted over the content at the
|
|
812
|
+
// wrong edge — the state was right the whole time, only the geometry never
|
|
813
|
+
// reached the DOM.
|
|
814
|
+
//
|
|
815
|
+
// These are placement assertions on purpose. Every other test here reads the
|
|
816
|
+
// state or a class, and every one of them PASSED while the widget was
|
|
817
|
+
// visibly wrong: nothing in the suite looked at where the pane actually
|
|
818
|
+
// ended up. That is the gap, not the individual bug.
|
|
819
|
+
await describe('AdwOverlaySplitView — geometry survives being unmeasured and moved', async () => {
|
|
820
|
+
/** Does the pane cover the view's own box? */
|
|
821
|
+
const covers = (view: AdwOverlaySplitView): boolean => {
|
|
822
|
+
const pane = view.querySelector('.adw-osv-sidebar') as HTMLElement;
|
|
823
|
+
const a = view.getBoundingClientRect();
|
|
824
|
+
const b = pane.getBoundingClientRect();
|
|
825
|
+
return b.width > 0 && b.right > a.left + 1 && b.left < a.right - 1;
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
await it('keeps a hidden sidebar off-screen when it is first laid out inside display:none', async () => {
|
|
829
|
+
const slide = document.createElement('div');
|
|
830
|
+
slide.style.display = 'none';
|
|
831
|
+
document.body.appendChild(slide);
|
|
832
|
+
slide.innerHTML =
|
|
833
|
+
'<div style="width:900px;height:300px;display:flex">' +
|
|
834
|
+
'<adw-overlay-split-view collapsed show-sidebar="false" sidebar-position="end">' +
|
|
835
|
+
'<div slot="content">c</div><div slot="sidebar">s</div>' +
|
|
836
|
+
'</adw-overlay-split-view></div>';
|
|
837
|
+
const view = slide.querySelector('adw-overlay-split-view') as AdwOverlaySplitView;
|
|
838
|
+
await settle();
|
|
839
|
+
|
|
840
|
+
// The slide becomes the visible one.
|
|
841
|
+
slide.style.display = '';
|
|
842
|
+
await settle();
|
|
843
|
+
expect(view.showSidebar).toBe(false);
|
|
844
|
+
expect(covers(view)).toBe(false);
|
|
845
|
+
slide.remove();
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
await it('re-measures after being moved to another parent', async () => {
|
|
849
|
+
const from = document.createElement('div');
|
|
850
|
+
from.style.display = 'none';
|
|
851
|
+
const to = document.createElement('div');
|
|
852
|
+
to.setAttribute('style', 'width:900px;height:300px;display:flex');
|
|
853
|
+
document.body.append(from, to);
|
|
854
|
+
from.innerHTML =
|
|
855
|
+
'<adw-overlay-split-view collapsed show-sidebar="false" sidebar-position="end">' +
|
|
856
|
+
'<div slot="content">c</div><div slot="sidebar">s</div>' +
|
|
857
|
+
'</adw-overlay-split-view>';
|
|
858
|
+
const view = from.querySelector('adw-overlay-split-view') as AdwOverlaySplitView;
|
|
859
|
+
await settle();
|
|
860
|
+
|
|
861
|
+
// Moving a node is a disconnect followed by a connect. The observer
|
|
862
|
+
// torn down by the first has to be rebuilt by the second.
|
|
863
|
+
to.appendChild(view);
|
|
864
|
+
await settle();
|
|
865
|
+
expect(covers(view)).toBe(false);
|
|
866
|
+
|
|
867
|
+
// And it must keep tracking: a resize after the move has to land.
|
|
868
|
+
to.style.width = '600px';
|
|
869
|
+
await settle();
|
|
870
|
+
const pane = view.querySelector('.adw-osv-sidebar') as HTMLElement;
|
|
871
|
+
expect(pane.style.left).toBe('600px');
|
|
872
|
+
from.remove();
|
|
873
|
+
to.remove();
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
await it('hands placement back to the stylesheet rather than stranding a stale offset', async () => {
|
|
877
|
+
const { view, host } = mount('collapsed show-sidebar="false" sidebar-position="end"');
|
|
878
|
+
host.setAttribute('style', 'width:900px;height:300px;display:flex');
|
|
879
|
+
await settle();
|
|
880
|
+
const pane = view.querySelector('.adw-osv-sidebar') as HTMLElement;
|
|
881
|
+
expect(pane.style.left).toBe('900px');
|
|
882
|
+
|
|
883
|
+
// Unmeasurable again: the inline offset must be CLEARED, because an
|
|
884
|
+
// absolutely-positioned box with `left` + `width` ignores `right`,
|
|
885
|
+
// so a leftover value would outrank the resting rule it falls back
|
|
886
|
+
// to and pin the pane wherever it last happened to be.
|
|
887
|
+
host.style.display = 'none';
|
|
888
|
+
await settle();
|
|
889
|
+
expect(pane.style.left).toBe('');
|
|
890
|
+
host.remove();
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
};
|