@blotch/dolan 22.2.3 → 22.2.4
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/commands/add.js +9 -1
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/add.test.js +36 -3
- package/dist/commands/add.test.js.map +1 -1
- package/dist/commands/doctor.js +187 -7
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +280 -7
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.js +38 -18
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/init.test.js +107 -1
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/theme.js +1 -1
- package/dist/commands/theme.js.map +1 -1
- package/dist/commands/update.d.ts +5 -0
- package/dist/commands/update.js +126 -45
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +314 -2
- package/dist/commands/update.test.js.map +1 -1
- package/dist/lockfile.d.ts +7 -0
- package/dist/lockfile.js +36 -0
- package/dist/lockfile.js.map +1 -1
- package/dist/lockfile.test.d.ts +1 -0
- package/dist/lockfile.test.js +25 -0
- package/dist/lockfile.test.js.map +1 -0
- package/dist/registry.d.ts +6 -0
- package/dist/registry.js +48 -2
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +45 -3
- package/dist/registry.test.js.map +1 -1
- package/dist/test-utils.d.ts +5 -0
- package/dist/test-utils.js +6 -1
- package/dist/test-utils.js.map +1 -1
- package/dist/testing/fire-lynx-event.d.ts +14 -0
- package/dist/testing/fire-lynx-event.js +19 -0
- package/dist/testing/fire-lynx-event.js.map +1 -0
- package/dist/testing/fire-lynx-event.test.d.ts +1 -0
- package/dist/testing/fire-lynx-event.test.js +44 -0
- package/dist/testing/fire-lynx-event.test.js.map +1 -0
- package/dist/testing/lynx-main-thread.d.ts +22 -0
- package/dist/testing/lynx-main-thread.js +11 -0
- package/dist/testing/lynx-main-thread.js.map +1 -0
- package/dist/testing/lynx-main-thread.test.d.ts +1 -0
- package/dist/testing/lynx-main-thread.test.js +24 -0
- package/dist/testing/lynx-main-thread.test.js.map +1 -0
- package/dist/testing/set-input-signal.d.ts +5 -4
- package/dist/testing/set-input-signal.js +5 -4
- package/dist/testing/set-input-signal.js.map +1 -1
- package/dist/ui/components/accordion/accordion.test.ts +158 -144
- package/dist/ui/components/accordion/accordion.ts +19 -14
- package/dist/ui/components/action-sheet/action-sheet.test.ts +272 -187
- package/dist/ui/components/action-sheet/action-sheet.ts +40 -34
- package/dist/ui/components/alert/alert.test.ts +67 -19
- package/dist/ui/components/alert/alert.ts +14 -16
- package/dist/ui/components/alert-dialog/alert-dialog.test.ts +206 -139
- package/dist/ui/components/alert-dialog/alert-dialog.ts +32 -26
- package/dist/ui/components/area-chart/area-chart.test.ts +81 -1
- package/dist/ui/components/avatar/avatar.test.ts +89 -45
- package/dist/ui/components/badge/badge.test.ts +57 -23
- package/dist/ui/components/badge/badge.ts +12 -14
- package/dist/ui/components/bar-chart/bar-chart.test.ts +82 -1
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +373 -297
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +34 -33
- package/dist/ui/components/button/button.test.ts +611 -2
- package/dist/ui/components/button/button.ts +431 -62
- package/dist/ui/components/button/index.ts +2 -0
- package/dist/ui/components/button-group/button-group.test.ts +484 -2
- package/dist/ui/components/button-group/button-group.ts +380 -68
- package/dist/ui/components/button-group/index.ts +3 -0
- package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +82 -1
- package/dist/ui/components/card/card.test.ts +82 -62
- package/dist/ui/components/chart/chart-presets.test.ts +197 -1
- package/dist/ui/components/chart/chart-presets.ts +8 -1
- package/dist/ui/components/chart/chart-theme.ts +12 -0
- package/dist/ui/components/chart/chart.test.ts +1017 -0
- package/dist/ui/components/chart/chart.ts +56 -43
- package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
- package/dist/ui/components/checkbox/checkbox.ts +23 -23
- package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
- package/dist/ui/components/collapsible/collapsible.ts +19 -14
- package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
- package/dist/ui/components/context-menu/context-menu.ts +110 -0
- package/dist/ui/components/context-menu/index.ts +1 -0
- package/dist/ui/components/dialog/dialog.test.ts +210 -152
- package/dist/ui/components/dialog/dialog.ts +29 -23
- package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
- package/dist/ui/components/dock/dock-scroll.ts +133 -0
- package/dist/ui/components/dock/dock.test.ts +749 -0
- package/dist/ui/components/dock/dock.ts +960 -0
- package/dist/ui/components/dock/index.ts +2 -0
- package/dist/ui/components/empty-state/empty-state.test.ts +28 -39
- package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
- package/dist/ui/components/glass/concentric-radius.ts +17 -0
- package/dist/ui/components/glass/glass-design.test.ts +78 -0
- package/dist/ui/components/glass/glass-design.ts +119 -0
- package/dist/ui/components/glass/glass.test.ts +338 -0
- package/dist/ui/components/glass/glass.ts +360 -0
- package/dist/ui/components/glass/index.ts +18 -0
- package/dist/ui/components/icon/icon-theme.test.ts +36 -0
- package/dist/ui/components/icon/icon-theme.ts +36 -13
- package/dist/ui/components/icon/icon.test.ts +52 -41
- package/dist/ui/components/input/input.test.ts +178 -101
- package/dist/ui/components/input/input.ts +15 -12
- package/dist/ui/components/label/label.test.ts +11 -24
- package/dist/ui/components/line-chart/line-chart.test.ts +76 -1
- package/dist/ui/components/list/list.test.ts +80 -49
- package/dist/ui/components/menu/index.ts +25 -0
- package/dist/ui/components/menu/menu-placement.test.ts +153 -0
- package/dist/ui/components/menu/menu-placement.ts +159 -0
- package/dist/ui/components/menu/menu-popup.test.ts +632 -0
- package/dist/ui/components/menu/menu-popup.ts +544 -0
- package/dist/ui/components/menu/menu.test.ts +429 -0
- package/dist/ui/components/menu/menu.ts +415 -0
- package/dist/ui/components/nav-drawer/nav-drawer.test.ts +425 -260
- package/dist/ui/components/nav-drawer/nav-drawer.ts +72 -56
- package/dist/ui/components/pagination/pagination.test.ts +136 -86
- package/dist/ui/components/progress/progress.test.ts +102 -46
- package/dist/ui/components/progress/progress.ts +52 -54
- package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
- package/dist/ui/components/radio-group/radio-group.ts +24 -24
- package/dist/ui/components/scatter-chart/scatter-chart.test.ts +74 -1
- package/dist/ui/components/scroll-area/scroll-area.test.ts +72 -36
- package/dist/ui/components/select/select.test.ts +138 -78
- package/dist/ui/components/select/select.ts +17 -11
- package/dist/ui/components/separator/separator.test.ts +18 -17
- package/dist/ui/components/sheet/sheet.test.ts +64 -11
- package/dist/ui/components/sheet/sheet.ts +9 -1
- package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
- package/dist/ui/components/skeleton/skeleton.ts +10 -16
- package/dist/ui/components/spinner/spinner.test.ts +64 -46
- package/dist/ui/components/spinner/spinner.ts +16 -19
- package/dist/ui/components/switch/switch.test.ts +116 -46
- package/dist/ui/components/switch/switch.ts +8 -13
- package/dist/ui/components/tab-bar/index.ts +1 -0
- package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
- package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
- package/dist/ui/components/tabs/tabs.test.ts +215 -157
- package/dist/ui/components/tabs/tabs.ts +110 -118
- package/dist/ui/components/textarea/textarea.test.ts +206 -100
- package/dist/ui/components/textarea/textarea.ts +14 -12
- package/dist/ui/components/toast/toast.test.ts +434 -350
- package/dist/ui/components/toast/toast.ts +42 -51
- package/dist/ui/components/toggle/toggle.test.ts +247 -140
- package/dist/ui/theme/chart-colors.test.ts +8 -3
- package/dist/ui/theme/dark.css +38 -2
- package/dist/ui/theme/default.css +55 -7
- package/dist/ui/theme/icon-colors.test.ts +8 -3
- package/dist/ui/theme/rgb-channels.test.ts +84 -0
- package/dist/ui/theme/tailwind-build.test.ts +114 -0
- package/dist/ui/theme/tailwind-css.test.ts +83 -0
- package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
- package/dist/ui/theme/tailwind-plugin.ts +93 -29
- package/dist/ui/theme/tailwind.css +113 -0
- package/dist/ui/utils/animate.test.ts +13 -1
- package/dist/utils/analyze.d.ts +14 -0
- package/dist/utils/analyze.js +15 -0
- package/dist/utils/analyze.js.map +1 -1
- package/dist/utils/analyze.test.js +14 -1
- package/dist/utils/analyze.test.js.map +1 -1
- package/dist/utils/installed.d.ts +12 -0
- package/dist/utils/installed.js +16 -0
- package/dist/utils/installed.js.map +1 -1
- package/dist/utils/installed.test.js +17 -1
- package/dist/utils/installed.test.js.map +1 -1
- package/dist/utils/resolve-paths.d.ts +4 -4
- package/dist/utils/resolve-paths.js +5 -7
- package/dist/utils/resolve-paths.js.map +1 -1
- package/dist/utils/resolve-paths.test.js +9 -5
- package/dist/utils/resolve-paths.test.js.map +1 -1
- package/dist/utils/tailwind.d.ts +25 -0
- package/dist/utils/tailwind.js +51 -0
- package/dist/utils/tailwind.js.map +1 -0
- package/dist/utils/tailwind.test.d.ts +1 -0
- package/dist/utils/tailwind.test.js +71 -0
- package/dist/utils/tailwind.test.js.map +1 -0
- package/package.json +9 -1
- package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -63
- package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -58
- package/dist/ui/components/button/button-render.test.ts +0 -132
- package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
- package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -59
- package/dist/ui/components/chart/chart-render.test.ts +0 -730
- package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -83
- package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -64
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApplicationRef,
|
|
3
|
+
Component,
|
|
4
|
+
type ElementRef,
|
|
5
|
+
type Provider,
|
|
6
|
+
signal,
|
|
7
|
+
viewChild,
|
|
8
|
+
} from '@angular/core';
|
|
9
|
+
import {
|
|
10
|
+
fireEvent,
|
|
11
|
+
render,
|
|
12
|
+
waitForUpdate,
|
|
13
|
+
} from '@blotch/angular-lynx-testing-library';
|
|
14
|
+
import { LYNX_ELEMENTS, LynxAccessibilitySettings } from '@blotch/angular-lynx';
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
16
|
+
import { fireLynxEvent } from '../../../test-utils/fire-lynx-event';
|
|
17
|
+
import { provideIconTheme } from '../icon/icon-theme';
|
|
18
|
+
import { type MenuElement, type MenuKind, UiMenuPopup } from './menu-popup';
|
|
19
|
+
import { type MenuRect } from './menu-placement';
|
|
20
|
+
|
|
21
|
+
@Component({
|
|
22
|
+
imports: [LYNX_ELEMENTS, UiMenuPopup],
|
|
23
|
+
template: `
|
|
24
|
+
<view
|
|
25
|
+
#home
|
|
26
|
+
class="home"
|
|
27
|
+
[style.width.px]="popup.placeholder()?.width"
|
|
28
|
+
[style.height.px]="popup.placeholder()?.height"
|
|
29
|
+
>
|
|
30
|
+
<view #anchor class="anchor"><text>Kyoto</text></view>
|
|
31
|
+
</view>
|
|
32
|
+
<ui-menu-popup
|
|
33
|
+
#popup
|
|
34
|
+
[open]="open()"
|
|
35
|
+
[kind]="kind()"
|
|
36
|
+
[anchor]="anchorView().nativeElement"
|
|
37
|
+
[anchorHome]="lifting() ? homeView().nativeElement : null"
|
|
38
|
+
(dismissed)="dismissed()"
|
|
39
|
+
(closed)="closed()"
|
|
40
|
+
>
|
|
41
|
+
<view class="item"><text>Share</text></view>
|
|
42
|
+
</ui-menu-popup>
|
|
43
|
+
`,
|
|
44
|
+
})
|
|
45
|
+
class PopupHost {
|
|
46
|
+
readonly open = signal(false);
|
|
47
|
+
readonly kind = signal<MenuKind>('pull-down');
|
|
48
|
+
readonly lifting = signal(false);
|
|
49
|
+
readonly anchorView = viewChild.required<ElementRef<MenuElement>>('anchor');
|
|
50
|
+
readonly homeView = viewChild.required<ElementRef<MenuElement>>('home');
|
|
51
|
+
readonly dismissed = vi.fn();
|
|
52
|
+
readonly closed = vi.fn();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The Lynx function that runs an element's UI methods, such as measuring it
|
|
57
|
+
* (`boundingClientRect`). The testing library defines it on the global
|
|
58
|
+
* object, and answers without a box.
|
|
59
|
+
*/
|
|
60
|
+
type InvokeUIMethod = {
|
|
61
|
+
__InvokeUIMethod: (
|
|
62
|
+
element: Element,
|
|
63
|
+
method: string,
|
|
64
|
+
params: Record<string, unknown>,
|
|
65
|
+
callback: (result: { code: number; data?: unknown }) => void,
|
|
66
|
+
) => void;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** The safe area of a phone, on screen. */
|
|
70
|
+
const AREA: MenuRect = { left: 0, top: 62, width: 402, height: 778 };
|
|
71
|
+
/** The menu's content, at rest. */
|
|
72
|
+
const CONTENT_HEIGHT = 120;
|
|
73
|
+
|
|
74
|
+
/** Accessibility settings a test can change, as the host app would. */
|
|
75
|
+
const settings = {
|
|
76
|
+
reduceMotion: signal(false),
|
|
77
|
+
reduceTransparency: signal(false),
|
|
78
|
+
increaseContrast: signal(false),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const IOS_26 = { platform: 'iOS', osVersion: '26.0' };
|
|
82
|
+
const IOS_18 = { platform: 'iOS', osVersion: '18.5' };
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Renders the host on a platform, with the elements answering
|
|
86
|
+
* `boundingClientRect` as on a phone: the anchor at `anchorRect`, the safe
|
|
87
|
+
* area at AREA, and the menu CONTENT_HEIGHT tall. `held` keeps the answers
|
|
88
|
+
* for the elements it picks until `release()`.
|
|
89
|
+
*/
|
|
90
|
+
const renderPopup = async ({
|
|
91
|
+
platform = IOS_26,
|
|
92
|
+
anchorRect = { left: 16, top: 162, width: 370, height: 96 },
|
|
93
|
+
area = AREA,
|
|
94
|
+
contentHeight = CONTENT_HEIGHT,
|
|
95
|
+
providers = [],
|
|
96
|
+
}: {
|
|
97
|
+
area?: MenuRect;
|
|
98
|
+
contentHeight?: number;
|
|
99
|
+
/** `SystemInfo`, or null for none, as in a test, where design is Material. */
|
|
100
|
+
platform?: { platform: string; osVersion: string } | null;
|
|
101
|
+
anchorRect?: MenuRect;
|
|
102
|
+
providers?: Provider[];
|
|
103
|
+
} = {}) => {
|
|
104
|
+
vi.stubGlobal('SystemInfo', platform ?? undefined);
|
|
105
|
+
const pending: (() => void)[] = [];
|
|
106
|
+
const held = { anchor: false, menu: false, measure: true };
|
|
107
|
+
const invoke = vi
|
|
108
|
+
.spyOn(globalThis as unknown as InvokeUIMethod, '__InvokeUIMethod')
|
|
109
|
+
.mockImplementation((element, method, _params, callback) => {
|
|
110
|
+
if (method !== 'boundingClientRect') {
|
|
111
|
+
callback({ code: 0 });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const isAnchor = element.classList.contains('anchor');
|
|
115
|
+
const box = !held.measure
|
|
116
|
+
? undefined
|
|
117
|
+
: isAnchor
|
|
118
|
+
? anchorRect
|
|
119
|
+
: element.parentElement?.tagName.toLowerCase() === 'scroll-view'
|
|
120
|
+
? { left: 0, top: 0, width: 250, height: contentHeight }
|
|
121
|
+
: area;
|
|
122
|
+
const answer = () => callback({ code: 0, data: box });
|
|
123
|
+
if (isAnchor ? held.anchor : held.menu) pending.push(answer);
|
|
124
|
+
else answer();
|
|
125
|
+
});
|
|
126
|
+
const result = await render(PopupHost, {
|
|
127
|
+
providers: [
|
|
128
|
+
{ provide: LynxAccessibilitySettings, useValue: settings },
|
|
129
|
+
...providers,
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
const host = result.componentRef.instance as PopupHost;
|
|
133
|
+
const appRef = result.componentRef.injector.get(ApplicationRef);
|
|
134
|
+
const query = (selector: string) =>
|
|
135
|
+
result.container.querySelector(selector) as HTMLElement | null;
|
|
136
|
+
return {
|
|
137
|
+
...result,
|
|
138
|
+
host,
|
|
139
|
+
invoke,
|
|
140
|
+
held,
|
|
141
|
+
/** Answers the measurements `held` kept back. */
|
|
142
|
+
release: () => {
|
|
143
|
+
for (const answer of pending.splice(0)) answer();
|
|
144
|
+
},
|
|
145
|
+
/** Runs change detection and effects now. */
|
|
146
|
+
tick: () => appRef.tick(),
|
|
147
|
+
overlay: () => query('overlay')!,
|
|
148
|
+
backdrop: () => query('overlay > view')!,
|
|
149
|
+
panel: () => query('overlay blur-view[ui-glass]')!,
|
|
150
|
+
scroller: () => query('overlay scroll-view')!,
|
|
151
|
+
slot: () =>
|
|
152
|
+
query('overlay blur-view[ui-glass]')!
|
|
153
|
+
.previousElementSibling as HTMLElement,
|
|
154
|
+
anchor: () => query('.anchor')!,
|
|
155
|
+
home: () => query('.home')!,
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
type Rendered = Awaited<ReturnType<typeof renderPopup>>;
|
|
160
|
+
|
|
161
|
+
const style = (element: Element, name: string) =>
|
|
162
|
+
(element as HTMLElement).style.getPropertyValue(name);
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The box an element's inline style puts it in.
|
|
166
|
+
*/
|
|
167
|
+
const box = (element: Element) => ({
|
|
168
|
+
left: parseFloat(style(element, 'left')),
|
|
169
|
+
top: parseFloat(style(element, 'top')),
|
|
170
|
+
width: parseFloat(style(element, 'width')),
|
|
171
|
+
height: parseFloat(style(element, 'height')),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Opens the menu and waits until it has grown to its frame.
|
|
176
|
+
*/
|
|
177
|
+
const openMenu = async (rendered: Rendered) => {
|
|
178
|
+
rendered.host.open.set(true);
|
|
179
|
+
await vi.waitFor(() =>
|
|
180
|
+
expect(rendered.panel().getAttribute('accessibility-exclusive-focus')).toBe(
|
|
181
|
+
'true',
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Closes the menu and waits for its close animation to finish.
|
|
188
|
+
*/
|
|
189
|
+
const closeMenu = async (rendered: Rendered) => {
|
|
190
|
+
rendered.host.open.set(false);
|
|
191
|
+
await vi.waitFor(() => expect(rendered.host.closed).toHaveBeenCalled());
|
|
192
|
+
await waitForUpdate();
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Lets every pending promise callback run.
|
|
197
|
+
*/
|
|
198
|
+
const settle = async () => {
|
|
199
|
+
for (let i = 0; i < 10; i++) await Promise.resolve();
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// The renderer warns that <ui-menu-popup> isn't a Lynx element.
|
|
203
|
+
beforeEach(() => {
|
|
204
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
afterEach(() => {
|
|
208
|
+
vi.restoreAllMocks();
|
|
209
|
+
vi.unstubAllGlobals();
|
|
210
|
+
settings.reduceMotion.set(false);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('UiMenuPopup', () => {
|
|
214
|
+
it('is hidden until opened', async () => {
|
|
215
|
+
const { overlay, invoke } = await renderPopup();
|
|
216
|
+
|
|
217
|
+
expect(overlay().getAttribute('visible')).toBe('false');
|
|
218
|
+
expect(style(overlay(), 'display')).toBe('none');
|
|
219
|
+
expect(invoke).not.toHaveBeenCalled();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('grows a pull-down menu out of its anchor, as one glass shape', async () => {
|
|
223
|
+
const rendered = await renderPopup();
|
|
224
|
+
const { host, overlay, panel, scroller } = rendered;
|
|
225
|
+
|
|
226
|
+
rendered.held.menu = true;
|
|
227
|
+
host.open.set(true);
|
|
228
|
+
// Measured off screen at its natural size first.
|
|
229
|
+
await vi.waitFor(() =>
|
|
230
|
+
expect(overlay().getAttribute('visible')).toBe('true'),
|
|
231
|
+
);
|
|
232
|
+
expect(style(panel(), 'left')).toBe('-10000px');
|
|
233
|
+
expect(style(panel(), 'width')).toBe('');
|
|
234
|
+
expect(style(panel(), 'transition')).toBe('none');
|
|
235
|
+
rendered.release();
|
|
236
|
+
|
|
237
|
+
// ...and grows to the menu, with a spring, its items fading in.
|
|
238
|
+
await openMenu(rendered);
|
|
239
|
+
expect(box(panel())).toEqual({
|
|
240
|
+
left: 16,
|
|
241
|
+
top: 100,
|
|
242
|
+
width: 250,
|
|
243
|
+
height: 120,
|
|
244
|
+
});
|
|
245
|
+
expect(style(panel(), 'transition')).toContain('width 350ms');
|
|
246
|
+
expect(style(scroller(), 'opacity')).toBe('1');
|
|
247
|
+
expect(style(scroller(), 'height')).toBe('120px');
|
|
248
|
+
expect(style(panel(), 'opacity')).toBe('1');
|
|
249
|
+
expect(style(panel(), 'justify-content')).toBe('flex-start');
|
|
250
|
+
expect(style(panel(), 'align-items')).toBe('flex-start');
|
|
251
|
+
expect(panel().getAttribute('blur-effect')).toBe('glass');
|
|
252
|
+
expect(panel().getAttribute('accessibility-exclusive-focus')).toBe('true');
|
|
253
|
+
// A pull-down menu doesn't dim the screen or move its anchor.
|
|
254
|
+
expect(overlay().querySelector('.bg-glass-dim')).toBeNull();
|
|
255
|
+
expect(rendered.home().contains(rendered.anchor())).toBe(true);
|
|
256
|
+
|
|
257
|
+
await closeMenu(rendered);
|
|
258
|
+
expect(overlay().getAttribute('visible')).toBe('false');
|
|
259
|
+
expect(host.closed).toHaveBeenCalledTimes(1);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('moves a screen reader into the menu once it opens', async () => {
|
|
263
|
+
const rendered = await renderPopup();
|
|
264
|
+
|
|
265
|
+
await openMenu(rendered);
|
|
266
|
+
|
|
267
|
+
expect(rendered.invoke).toHaveBeenCalledWith(
|
|
268
|
+
rendered.panel(),
|
|
269
|
+
'requestAccessibilityFocus',
|
|
270
|
+
expect.anything(),
|
|
271
|
+
expect.any(Function),
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('shrinks back into its anchor as it closes', async () => {
|
|
276
|
+
const rendered = await renderPopup();
|
|
277
|
+
await openMenu(rendered);
|
|
278
|
+
|
|
279
|
+
rendered.host.open.set(false);
|
|
280
|
+
await waitForUpdate();
|
|
281
|
+
|
|
282
|
+
expect(box(rendered.panel())).toEqual({
|
|
283
|
+
left: 16,
|
|
284
|
+
top: 100,
|
|
285
|
+
width: 370,
|
|
286
|
+
height: 96,
|
|
287
|
+
});
|
|
288
|
+
expect(style(rendered.panel(), 'transition')).toContain('width 250ms');
|
|
289
|
+
expect(style(rendered.scroller(), 'opacity')).toBe('0');
|
|
290
|
+
expect(rendered.host.closed).not.toHaveBeenCalled();
|
|
291
|
+
await closeMenu(rendered);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('opens toward the middle of the screen from an anchor in a corner', async () => {
|
|
295
|
+
const rendered = await renderPopup({
|
|
296
|
+
anchorRect: { left: 340, top: 790, width: 44, height: 44 },
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
await openMenu(rendered);
|
|
300
|
+
|
|
301
|
+
// Lined up with the anchor's right edge, and standing on its bottom edge,
|
|
302
|
+
// inside the margin at the bottom of the safe area.
|
|
303
|
+
expect(box(rendered.panel())).toEqual({
|
|
304
|
+
left: 384 - 250,
|
|
305
|
+
top: 778 - 8 - 120,
|
|
306
|
+
width: 250,
|
|
307
|
+
height: 120,
|
|
308
|
+
});
|
|
309
|
+
expect(style(rendered.panel(), 'justify-content')).toBe('flex-end');
|
|
310
|
+
expect(style(rendered.panel(), 'align-items')).toBe('flex-end');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('reports a tap outside the menu, and Android back, as dismissing it', async () => {
|
|
314
|
+
const rendered = await renderPopup();
|
|
315
|
+
await openMenu(rendered);
|
|
316
|
+
|
|
317
|
+
fireEvent.tap(rendered.panel(), { eventType: 'catchEvent' });
|
|
318
|
+
expect(rendered.host.dismissed).not.toHaveBeenCalled();
|
|
319
|
+
|
|
320
|
+
fireEvent.tap(rendered.backdrop());
|
|
321
|
+
fireLynxEvent(rendered.overlay(), 'requestclose');
|
|
322
|
+
expect(rendered.host.dismissed).toHaveBeenCalledTimes(2);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("measures in the app's pixels where the app is drawn scaled", async () => {
|
|
326
|
+
/**
|
|
327
|
+
* A box drawn at half its size, as a docs preview might draw the app:
|
|
328
|
+
* boundingClientRect gives boxes like that, while the safe area's layout
|
|
329
|
+
* gives its real width.
|
|
330
|
+
*/
|
|
331
|
+
const half = (box: MenuRect): MenuRect => ({
|
|
332
|
+
left: box.left / 2,
|
|
333
|
+
top: box.top / 2,
|
|
334
|
+
width: box.width / 2,
|
|
335
|
+
height: box.height / 2,
|
|
336
|
+
});
|
|
337
|
+
const rendered = await renderPopup({
|
|
338
|
+
anchorRect: half({ left: 16, top: 162, width: 370, height: 96 }),
|
|
339
|
+
area: half(AREA),
|
|
340
|
+
contentHeight: CONTENT_HEIGHT / 2,
|
|
341
|
+
});
|
|
342
|
+
const area = rendered.panel().parentElement!;
|
|
343
|
+
fireLynxEvent(area, 'layoutchange', { detail: { width: AREA.width } });
|
|
344
|
+
|
|
345
|
+
await openMenu(rendered);
|
|
346
|
+
|
|
347
|
+
expect(box(rendered.panel())).toEqual({
|
|
348
|
+
left: 16,
|
|
349
|
+
top: 100,
|
|
350
|
+
width: 250,
|
|
351
|
+
height: 120,
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('takes a layout without a size as unknown', async () => {
|
|
356
|
+
const rendered = await renderPopup();
|
|
357
|
+
fireLynxEvent(rendered.panel().parentElement!, 'layoutchange');
|
|
358
|
+
|
|
359
|
+
await openMenu(rendered);
|
|
360
|
+
|
|
361
|
+
expect(box(rendered.panel()).width).toBe(250);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('opens even where the elements answer without a box', async () => {
|
|
365
|
+
const rendered = await renderPopup();
|
|
366
|
+
rendered.held.measure = false;
|
|
367
|
+
|
|
368
|
+
await openMenu(rendered);
|
|
369
|
+
|
|
370
|
+
// Nothing to place it by: it opens at the area's top-left corner.
|
|
371
|
+
expect(box(rendered.panel())).toEqual({
|
|
372
|
+
left: 8,
|
|
373
|
+
top: 8,
|
|
374
|
+
width: 0,
|
|
375
|
+
height: 0,
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe('as a context menu', () => {
|
|
380
|
+
const renderContext = async (
|
|
381
|
+
options: Parameters<typeof renderPopup>[0] = {},
|
|
382
|
+
) => {
|
|
383
|
+
const rendered = await renderPopup(options);
|
|
384
|
+
rendered.host.kind.set('context');
|
|
385
|
+
rendered.host.lifting.set(true);
|
|
386
|
+
await waitForUpdate();
|
|
387
|
+
return rendered;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
it('lifts the element above a dimming layer and opens beside it', async () => {
|
|
391
|
+
const rendered = await renderContext();
|
|
392
|
+
const { host, overlay, panel, slot } = rendered;
|
|
393
|
+
|
|
394
|
+
rendered.held.menu = true;
|
|
395
|
+
host.open.set(true);
|
|
396
|
+
await vi.waitFor(() =>
|
|
397
|
+
expect(overlay().getAttribute('visible')).toBe('true'),
|
|
398
|
+
);
|
|
399
|
+
expect(slot().children).toHaveLength(0);
|
|
400
|
+
rendered.release();
|
|
401
|
+
await settle();
|
|
402
|
+
rendered.tick();
|
|
403
|
+
// The element moves into the overlay where it was, and its home keeps
|
|
404
|
+
// its size; the menu starts small, in the corner nearest it.
|
|
405
|
+
expect(slot().contains(rendered.anchor())).toBe(true);
|
|
406
|
+
expect(box(slot())).toEqual({
|
|
407
|
+
left: 16,
|
|
408
|
+
top: 100,
|
|
409
|
+
width: 370,
|
|
410
|
+
height: NaN,
|
|
411
|
+
});
|
|
412
|
+
expect(style(rendered.home(), 'width')).toBe('370px');
|
|
413
|
+
expect(style(rendered.home(), 'height')).toBe('96px');
|
|
414
|
+
expect(box(panel())).toEqual({
|
|
415
|
+
left: 16,
|
|
416
|
+
top: 204,
|
|
417
|
+
width: 75,
|
|
418
|
+
height: 36,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
await openMenu(rendered);
|
|
422
|
+
expect(box(panel())).toEqual({
|
|
423
|
+
left: 16,
|
|
424
|
+
top: 204,
|
|
425
|
+
width: 250,
|
|
426
|
+
height: 120,
|
|
427
|
+
});
|
|
428
|
+
const dim = overlay().querySelector('.bg-glass-dim')!;
|
|
429
|
+
expect(style(dim, 'opacity')).toBe('1');
|
|
430
|
+
expect(style(dim, 'transition')).toContain('opacity 250ms');
|
|
431
|
+
expect(style(slot(), 'transform')).toBe('scale(1.03)');
|
|
432
|
+
expect(style(slot(), 'transition')).toContain('transform 350ms');
|
|
433
|
+
// iOS 26 dims; it doesn't blur.
|
|
434
|
+
expect(overlay().querySelector('blur-view:not([ui-glass])')).toBeNull();
|
|
435
|
+
|
|
436
|
+
await closeMenu(rendered);
|
|
437
|
+
// The element is back where it was, and its home sizes to it again.
|
|
438
|
+
expect(rendered.home().querySelector('.anchor')?.textContent).toBe(
|
|
439
|
+
'Kyoto',
|
|
440
|
+
);
|
|
441
|
+
expect(slot().children).toHaveLength(0);
|
|
442
|
+
expect(style(rendered.home(), 'height')).toBe('');
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('moves the lifted element up when the menu fits on neither side', async () => {
|
|
446
|
+
const rendered = await renderContext({
|
|
447
|
+
anchorRect: { left: 16, top: 162, width: 370, height: 600 },
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
await openMenu(rendered);
|
|
451
|
+
|
|
452
|
+
// 778 - 8 - 120 - 8 - 600: the element rises to make room below it.
|
|
453
|
+
expect(style(rendered.slot(), 'top')).toBe('42px');
|
|
454
|
+
expect(box(rendered.panel()).top).toBe(42 + 600 + 8);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it('blurs the screen behind it before iOS 26, in the app color scheme', async () => {
|
|
458
|
+
const light = await renderContext({ platform: IOS_18 });
|
|
459
|
+
await openMenu(light);
|
|
460
|
+
|
|
461
|
+
const blur = light.overlay().querySelector('blur-view:not([ui-glass])')!;
|
|
462
|
+
expect(blur.getAttribute('blur-effect')).toBe('light');
|
|
463
|
+
expect(style(blur, 'opacity')).toBe('1');
|
|
464
|
+
light.unmount();
|
|
465
|
+
|
|
466
|
+
const dark = await renderContext({
|
|
467
|
+
platform: IOS_18,
|
|
468
|
+
providers: [provideIconTheme({ colorScheme: 'dark' })],
|
|
469
|
+
});
|
|
470
|
+
expect(
|
|
471
|
+
dark
|
|
472
|
+
.overlay()
|
|
473
|
+
.querySelector('blur-view:not([ui-glass])')!
|
|
474
|
+
.getAttribute('blur-effect'),
|
|
475
|
+
).toBe('dark');
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('opens a Material menu beside the element, without lifting or dimming it', async () => {
|
|
479
|
+
const rendered = await renderContext({ platform: null });
|
|
480
|
+
const { host, overlay, panel } = rendered;
|
|
481
|
+
|
|
482
|
+
// It unfolds and fades in...
|
|
483
|
+
await openMenu(rendered);
|
|
484
|
+
expect(box(panel())).toEqual({
|
|
485
|
+
left: 16,
|
|
486
|
+
top: 204,
|
|
487
|
+
width: 224,
|
|
488
|
+
height: 120,
|
|
489
|
+
});
|
|
490
|
+
expect(style(panel(), 'opacity')).toBe('1');
|
|
491
|
+
|
|
492
|
+
// ...and folds back into a sliver along the edge nearest the element,
|
|
493
|
+
// fading out.
|
|
494
|
+
host.open.set(false);
|
|
495
|
+
await waitForUpdate();
|
|
496
|
+
expect(box(panel())).toEqual({
|
|
497
|
+
left: 16,
|
|
498
|
+
top: 204,
|
|
499
|
+
width: 224,
|
|
500
|
+
height: 0,
|
|
501
|
+
});
|
|
502
|
+
expect(style(panel(), 'opacity')).toBe('0');
|
|
503
|
+
expect(overlay().querySelector('.bg-glass-dim')).toBeNull();
|
|
504
|
+
expect(rendered.home().contains(rendered.anchor())).toBe(true);
|
|
505
|
+
expect(style(rendered.home(), 'height')).toBe('');
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('follows Reduce Motion: nothing grows, moves or lifts, and only fades play', async () => {
|
|
509
|
+
settings.reduceMotion.set(true);
|
|
510
|
+
const rendered = await renderContext({
|
|
511
|
+
anchorRect: { left: 16, top: 162, width: 370, height: 600 },
|
|
512
|
+
});
|
|
513
|
+
const { host, panel, slot } = rendered;
|
|
514
|
+
|
|
515
|
+
host.open.set(true);
|
|
516
|
+
await vi.waitFor(() => expect(style(panel(), 'width')).toBe('250px'));
|
|
517
|
+
// At its frame from the start, and the element already where it goes.
|
|
518
|
+
expect(box(panel()).height).toBe(120);
|
|
519
|
+
expect(style(slot(), 'top')).toBe('42px');
|
|
520
|
+
|
|
521
|
+
await openMenu(rendered);
|
|
522
|
+
expect(style(slot(), 'transform')).toBe('scale(1)');
|
|
523
|
+
expect(style(slot(), 'transition')).toBe('none');
|
|
524
|
+
expect(style(panel(), 'transition')).toBe(
|
|
525
|
+
'opacity 350ms cubic-bezier(0.22, 1.2, 0.36, 1)',
|
|
526
|
+
);
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
describe('when it closes before it has opened', () => {
|
|
531
|
+
it('stops before the anchor is measured', async () => {
|
|
532
|
+
const rendered = await renderPopup();
|
|
533
|
+
rendered.held.anchor = true;
|
|
534
|
+
|
|
535
|
+
rendered.host.open.set(true);
|
|
536
|
+
await waitForUpdate();
|
|
537
|
+
rendered.host.open.set(false);
|
|
538
|
+
await waitForUpdate();
|
|
539
|
+
rendered.release();
|
|
540
|
+
await settle();
|
|
541
|
+
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
542
|
+
|
|
543
|
+
expect(rendered.overlay().getAttribute('visible')).toBe('false');
|
|
544
|
+
expect(rendered.host.closed).not.toHaveBeenCalled();
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('stops while the overlay lays out, and reports it closed', async () => {
|
|
548
|
+
const rendered = await renderPopup();
|
|
549
|
+
rendered.held.anchor = true;
|
|
550
|
+
|
|
551
|
+
rendered.host.open.set(true);
|
|
552
|
+
await waitForUpdate();
|
|
553
|
+
rendered.release();
|
|
554
|
+
await settle();
|
|
555
|
+
// The overlay shows, to lay the menu out; closing now hides it again.
|
|
556
|
+
rendered.tick();
|
|
557
|
+
expect(rendered.overlay().getAttribute('visible')).toBe('true');
|
|
558
|
+
rendered.host.open.set(false);
|
|
559
|
+
rendered.tick();
|
|
560
|
+
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
561
|
+
|
|
562
|
+
expect(rendered.overlay().getAttribute('visible')).toBe('false');
|
|
563
|
+
expect(rendered.host.closed).toHaveBeenCalledTimes(1);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('stops while the menu is measured', async () => {
|
|
567
|
+
const rendered = await renderPopup();
|
|
568
|
+
rendered.held.menu = true;
|
|
569
|
+
|
|
570
|
+
rendered.host.open.set(true);
|
|
571
|
+
await vi.waitFor(() =>
|
|
572
|
+
expect(rendered.overlay().getAttribute('visible')).toBe('true'),
|
|
573
|
+
);
|
|
574
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
575
|
+
rendered.host.open.set(false);
|
|
576
|
+
rendered.tick();
|
|
577
|
+
rendered.release();
|
|
578
|
+
await settle();
|
|
579
|
+
await waitForUpdate();
|
|
580
|
+
|
|
581
|
+
expect(rendered.overlay().getAttribute('visible')).toBe('false');
|
|
582
|
+
expect(style(rendered.panel(), 'left')).toBe('-10000px');
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('closes from where it started growing', async () => {
|
|
586
|
+
const rendered = await renderPopup();
|
|
587
|
+
rendered.held.menu = true;
|
|
588
|
+
|
|
589
|
+
rendered.host.open.set(true);
|
|
590
|
+
await vi.waitFor(() =>
|
|
591
|
+
expect(rendered.overlay().getAttribute('visible')).toBe('true'),
|
|
592
|
+
);
|
|
593
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
594
|
+
rendered.release();
|
|
595
|
+
await settle();
|
|
596
|
+
// At its start frame now; closing doesn't let it finish growing.
|
|
597
|
+
rendered.host.open.set(false);
|
|
598
|
+
rendered.tick();
|
|
599
|
+
|
|
600
|
+
await vi.waitFor(() => expect(rendered.host.closed).toHaveBeenCalled());
|
|
601
|
+
expect(
|
|
602
|
+
rendered.panel().getAttribute('accessibility-exclusive-focus'),
|
|
603
|
+
).toBe('false');
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('opens again at once when reopened while closing', async () => {
|
|
608
|
+
const rendered = await renderPopup();
|
|
609
|
+
await openMenu(rendered);
|
|
610
|
+
|
|
611
|
+
rendered.host.open.set(false);
|
|
612
|
+
await waitForUpdate();
|
|
613
|
+
await openMenu(rendered);
|
|
614
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
615
|
+
|
|
616
|
+
// The interrupted close never finished, so never reported.
|
|
617
|
+
expect(rendered.host.closed).not.toHaveBeenCalled();
|
|
618
|
+
expect(rendered.overlay().getAttribute('visible')).toBe('true');
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it('stops its timers when destroyed while closing', async () => {
|
|
622
|
+
const rendered = await renderPopup();
|
|
623
|
+
await openMenu(rendered);
|
|
624
|
+
|
|
625
|
+
rendered.host.open.set(false);
|
|
626
|
+
await waitForUpdate();
|
|
627
|
+
rendered.unmount();
|
|
628
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
629
|
+
|
|
630
|
+
expect(rendered.host.closed).not.toHaveBeenCalled();
|
|
631
|
+
});
|
|
632
|
+
});
|