@blotch/dolan 22.2.2 → 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.d.ts +6 -2
- package/dist/commands/add.js +41 -11
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/add.test.js +162 -9
- package/dist/commands/add.test.js.map +1 -1
- package/dist/commands/diff.d.ts +2 -1
- package/dist/commands/diff.js +38 -25
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/diff.test.js +72 -11
- package/dist/commands/diff.test.js.map +1 -1
- package/dist/commands/doctor.js +228 -34
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +367 -10
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/eject.js +7 -0
- package/dist/commands/eject.js.map +1 -1
- package/dist/commands/eject.test.js +39 -0
- package/dist/commands/eject.test.js.map +1 -1
- package/dist/commands/info.js +3 -2
- package/dist/commands/info.js.map +1 -1
- package/dist/commands/init.d.ts +37 -1
- package/dist/commands/init.js +169 -40
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/init.test.js +392 -27
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/install-packages.d.ts +6 -0
- package/dist/commands/install-packages.js +20 -0
- package/dist/commands/install-packages.js.map +1 -0
- package/dist/commands/install-packages.test.d.ts +1 -0
- package/dist/commands/install-packages.test.js +55 -0
- package/dist/commands/install-packages.test.js.map +1 -0
- package/dist/commands/list.js +21 -14
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/list.test.js +62 -5
- package/dist/commands/list.test.js.map +1 -1
- package/dist/commands/outdated.js +11 -10
- package/dist/commands/outdated.js.map +1 -1
- package/dist/commands/outdated.test.js +20 -5
- package/dist/commands/outdated.test.js.map +1 -1
- package/dist/commands/remove.d.ts +4 -0
- package/dist/commands/remove.js +17 -4
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/remove.test.js +53 -2
- package/dist/commands/remove.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 +11 -0
- package/dist/commands/update.js +174 -58
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +587 -33
- package/dist/commands/update.test.js.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.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 +14 -0
- package/dist/registry.js +78 -24
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +119 -38
- 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 +14 -0
- package/dist/testing/set-input-signal.js +21 -0
- package/dist/testing/set-input-signal.js.map +1 -0
- package/dist/testing/set-input-signal.test.d.ts +1 -0
- package/dist/testing/set-input-signal.test.js +20 -0
- package/dist/testing/set-input-signal.test.js.map +1 -0
- 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 +283 -151
- package/dist/ui/components/action-sheet/action-sheet.ts +77 -43
- 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 +208 -94
- package/dist/ui/components/alert-dialog/alert-dialog.ts +75 -38
- package/dist/ui/components/area-chart/area-chart.test.ts +134 -119
- package/dist/ui/components/area-chart/area-chart.ts +100 -315
- package/dist/ui/components/area-chart/index.ts +2 -3
- 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 +172 -45
- package/dist/ui/components/bar-chart/bar-chart.ts +140 -294
- package/dist/ui/components/bar-chart/index.ts +3 -3
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +386 -128
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +170 -53
- package/dist/ui/components/button/button.test.ts +611 -2
- package/dist/ui/components/button/button.ts +434 -49
- 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 +183 -58
- package/dist/ui/components/candlestick-chart/candlestick-chart.ts +150 -323
- package/dist/ui/components/candlestick-chart/index.ts +3 -4
- package/dist/ui/components/card/card.test.ts +82 -62
- package/dist/ui/components/chart/chart-focus.test.ts +132 -0
- package/dist/ui/components/chart/chart-focus.ts +115 -0
- package/dist/ui/components/chart/chart-presets.test.ts +335 -0
- package/dist/ui/components/chart/chart-presets.ts +278 -0
- package/dist/ui/components/chart/chart-scene.test.ts +589 -0
- package/dist/ui/components/chart/chart-scene.ts +483 -0
- package/dist/ui/components/chart/chart-theme.test.ts +85 -0
- package/dist/ui/components/chart/chart-theme.ts +210 -0
- package/dist/ui/components/chart/chart-zoom.test.ts +159 -0
- package/dist/ui/components/chart/chart-zoom.ts +162 -0
- package/dist/ui/components/chart/chart.test.ts +1017 -0
- package/dist/ui/components/chart/chart.ts +934 -0
- package/dist/ui/components/chart/index.ts +24 -0
- 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 +213 -108
- package/dist/ui/components/dialog/dialog.ts +72 -35
- 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 +169 -0
- package/dist/ui/components/icon/icon-theme.ts +227 -0
- package/dist/ui/components/icon/icon.test.ts +85 -25
- package/dist/ui/components/icon/icon.ts +38 -21
- 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/index.ts +2 -3
- package/dist/ui/components/line-chart/line-chart.test.ts +149 -56
- package/dist/ui/components/line-chart/line-chart.ts +83 -241
- package/dist/ui/components/list/list.test.ts +119 -21
- package/dist/ui/components/list/list.ts +21 -15
- 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 +435 -191
- package/dist/ui/components/nav-drawer/nav-drawer.ts +127 -56
- package/dist/ui/components/pagination/pagination.test.ts +177 -65
- package/dist/ui/components/pagination/pagination.ts +55 -8
- 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/index.ts +2 -3
- package/dist/ui/components/scatter-chart/scatter-chart.test.ts +131 -26
- package/dist/ui/components/scatter-chart/scatter-chart.ts +104 -250
- package/dist/ui/components/scroll-area/scroll-area.test.ts +95 -24
- package/dist/ui/components/scroll-area/scroll-area.ts +8 -8
- 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 +67 -22
- package/dist/ui/components/spinner/spinner.ts +48 -41
- 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 +436 -315
- package/dist/ui/components/toast/toast.ts +62 -55
- package/dist/ui/components/toggle/toggle.test.ts +247 -140
- package/dist/ui/theme/chart-colors.test.ts +35 -0
- package/dist/ui/theme/dark.css +46 -2
- package/dist/ui/theme/default.css +63 -7
- package/dist/ui/theme/icon-colors.test.ts +35 -0
- 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 +98 -25
- 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 +29 -0
- package/dist/utils/installed.js +43 -0
- package/dist/utils/installed.js.map +1 -0
- package/dist/utils/installed.test.d.ts +1 -0
- package/dist/utils/installed.test.js +57 -0
- package/dist/utils/installed.test.js.map +1 -0
- package/dist/utils/interactive.d.ts +9 -0
- package/dist/utils/interactive.js +10 -0
- package/dist/utils/interactive.js.map +1 -0
- package/dist/utils/interactive.test.d.ts +1 -0
- package/dist/utils/interactive.test.js +24 -0
- package/dist/utils/interactive.test.js.map +1 -0
- package/dist/utils/packages.d.ts +36 -0
- package/dist/utils/packages.js +70 -0
- package/dist/utils/packages.js.map +1 -0
- package/dist/utils/packages.test.d.ts +1 -0
- package/dist/utils/packages.test.js +89 -0
- package/dist/utils/packages.test.js.map +1 -0
- package/dist/utils/resolve-paths.d.ts +17 -1
- package/dist/utils/resolve-paths.js +21 -3
- package/dist/utils/resolve-paths.js.map +1 -1
- package/dist/utils/resolve-paths.test.js +38 -2
- 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 +13 -1
- package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -121
- package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -174
- package/dist/ui/components/button/button-render.test.ts +0 -113
- 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 -174
- package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +0 -188
- package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +0 -525
- package/dist/ui/components/cartesian-chart/cartesian-chart.ts +0 -1140
- package/dist/ui/components/cartesian-chart/index.ts +0 -18
- package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -121
- package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -157
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
fireEvent,
|
|
4
|
+
render,
|
|
5
|
+
waitForUpdate,
|
|
6
|
+
within,
|
|
7
|
+
} from '@blotch/angular-lynx-testing-library';
|
|
8
|
+
import { LYNX_ELEMENTS, LynxAccessibilitySettings } from '@blotch/angular-lynx';
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import { UiMenuItem, UiMenuSeparator } from '../menu/menu';
|
|
11
|
+
import { UiContextMenu } from './context-menu';
|
|
12
|
+
|
|
13
|
+
@Component({
|
|
14
|
+
imports: [LYNX_ELEMENTS, UiContextMenu, UiMenuItem, UiMenuSeparator],
|
|
15
|
+
template: `
|
|
16
|
+
<ui-context-menu
|
|
17
|
+
[open]="open()"
|
|
18
|
+
(openChange)="open.set($event)"
|
|
19
|
+
[disabled]="disabled()"
|
|
20
|
+
(closed)="closed()"
|
|
21
|
+
>
|
|
22
|
+
<view class="card" (bindtap)="opened()">
|
|
23
|
+
<text>Kyoto</text>
|
|
24
|
+
</view>
|
|
25
|
+
<ui-menu-item label="Share" icon="upload" (pressed)="shared()" />
|
|
26
|
+
<ui-menu-separator />
|
|
27
|
+
<ui-menu-item
|
|
28
|
+
label="Delete"
|
|
29
|
+
icon="trash"
|
|
30
|
+
variant="destructive"
|
|
31
|
+
(pressed)="deleted()"
|
|
32
|
+
/>
|
|
33
|
+
</ui-context-menu>
|
|
34
|
+
`,
|
|
35
|
+
})
|
|
36
|
+
class ContextMenuHost {
|
|
37
|
+
readonly open = signal(false);
|
|
38
|
+
readonly disabled = signal(false);
|
|
39
|
+
readonly closed = vi.fn();
|
|
40
|
+
readonly opened = vi.fn();
|
|
41
|
+
readonly shared = vi.fn();
|
|
42
|
+
readonly deleted = vi.fn();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The Lynx function that runs an element's UI methods, such as measuring it
|
|
47
|
+
* (`boundingClientRect`). The testing library defines it on the global
|
|
48
|
+
* object.
|
|
49
|
+
*/
|
|
50
|
+
type InvokeUIMethod = {
|
|
51
|
+
__InvokeUIMethod: (
|
|
52
|
+
element: Element,
|
|
53
|
+
method: string,
|
|
54
|
+
params: Record<string, unknown>,
|
|
55
|
+
callback: (result: { code: number; data?: unknown }) => void,
|
|
56
|
+
) => void;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Accessibility settings a test can change, as the host app would. */
|
|
60
|
+
const settings = {
|
|
61
|
+
reduceMotion: signal(false),
|
|
62
|
+
reduceTransparency: signal(false),
|
|
63
|
+
increaseContrast: signal(false),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const IOS_26 = { platform: 'iOS', osVersion: '26.0' };
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Renders the context menu on a platform (none for Material), measuring
|
|
70
|
+
* elements as `boundingClientRect` would on a phone: the safe area (the
|
|
71
|
+
* element that holds the menu) a phone's screen, the menu's content 120pt
|
|
72
|
+
* tall, and anything else, the pressed element, a card near the top.
|
|
73
|
+
*/
|
|
74
|
+
const renderContextMenu = async (
|
|
75
|
+
platform: { platform: string; osVersion: string } | null = IOS_26,
|
|
76
|
+
) => {
|
|
77
|
+
vi.stubGlobal('SystemInfo', platform ?? undefined);
|
|
78
|
+
vi.spyOn(
|
|
79
|
+
globalThis as unknown as InvokeUIMethod,
|
|
80
|
+
'__InvokeUIMethod',
|
|
81
|
+
).mockImplementation((element, method, _params, callback) => {
|
|
82
|
+
if (method !== 'boundingClientRect') {
|
|
83
|
+
callback({ code: 0 });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const data = element.querySelector(':scope > blur-view[ui-glass]')
|
|
87
|
+
? { left: 0, top: 62, width: 402, height: 778 }
|
|
88
|
+
: element.parentElement?.tagName.toLowerCase() === 'scroll-view'
|
|
89
|
+
? { left: 0, top: 0, width: 250, height: 120 }
|
|
90
|
+
: { left: 16, top: 162, width: 370, height: 96 };
|
|
91
|
+
callback({ code: 0, data });
|
|
92
|
+
});
|
|
93
|
+
const result = await render(ContextMenuHost, {
|
|
94
|
+
providers: [{ provide: LynxAccessibilitySettings, useValue: settings }],
|
|
95
|
+
});
|
|
96
|
+
const host = result.componentRef.instance as ContextMenuHost;
|
|
97
|
+
const overlay = result.container.querySelector('overlay')!;
|
|
98
|
+
return {
|
|
99
|
+
...result,
|
|
100
|
+
host,
|
|
101
|
+
overlay,
|
|
102
|
+
menu: within(overlay as HTMLElement),
|
|
103
|
+
card: () => result.getByText('Kyoto').parentElement!,
|
|
104
|
+
/** The element the card is in: its home, or the overlay's slot. */
|
|
105
|
+
holder: () =>
|
|
106
|
+
result.getByText('Kyoto').parentElement!.parentElement!.parentElement!,
|
|
107
|
+
panel: () => overlay.querySelector('blur-view[ui-glass]')!,
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
type Rendered = Awaited<ReturnType<typeof renderContextMenu>>;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Waits until the menu has opened.
|
|
115
|
+
*/
|
|
116
|
+
const waitForOpen = (rendered: Rendered) =>
|
|
117
|
+
vi.waitFor(() =>
|
|
118
|
+
expect(rendered.panel().getAttribute('accessibility-exclusive-focus')).toBe(
|
|
119
|
+
'true',
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Waits until the menu has closed and its animation has finished.
|
|
125
|
+
*/
|
|
126
|
+
const waitForClose = async (rendered: Rendered) => {
|
|
127
|
+
await vi.waitFor(() => expect(rendered.host.closed).toHaveBeenCalled());
|
|
128
|
+
await waitForUpdate();
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Long-presses an element. The press bubbles, as Lynx's touch events do, to
|
|
133
|
+
* the context menu around the element.
|
|
134
|
+
*/
|
|
135
|
+
const longPress = (element: Element) =>
|
|
136
|
+
fireEvent.longpress(element, { bubbles: true });
|
|
137
|
+
|
|
138
|
+
const style = (element: Element, name: string) =>
|
|
139
|
+
(element as HTMLElement).style.getPropertyValue(name);
|
|
140
|
+
|
|
141
|
+
// The renderer warns that the components' selectors aren't Lynx elements.
|
|
142
|
+
beforeEach(() => {
|
|
143
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
afterEach(() => {
|
|
147
|
+
vi.restoreAllMocks();
|
|
148
|
+
vi.unstubAllGlobals();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('UiContextMenu', () => {
|
|
152
|
+
it('opens on a long press, lifting the element above the dimmed screen', async () => {
|
|
153
|
+
const rendered = await renderContextMenu();
|
|
154
|
+
const home = rendered.holder();
|
|
155
|
+
|
|
156
|
+
longPress(rendered.card());
|
|
157
|
+
await waitForOpen(rendered);
|
|
158
|
+
|
|
159
|
+
expect(rendered.host.open()).toBe(true);
|
|
160
|
+
// The element itself is in the menu's overlay now, lifted, and the spot
|
|
161
|
+
// it left keeps its size.
|
|
162
|
+
expect(rendered.overlay.contains(rendered.card())).toBe(true);
|
|
163
|
+
expect(style(rendered.holder(), 'transform')).toBe('scale(1.03)');
|
|
164
|
+
expect(style(home, 'width')).toBe('370px');
|
|
165
|
+
expect(style(home, 'height')).toBe('96px');
|
|
166
|
+
expect(rendered.overlay.querySelector('.bg-glass-dim')).not.toBeNull();
|
|
167
|
+
// The menu opens below it.
|
|
168
|
+
expect(style(rendered.panel(), 'top')).toBe(`${100 + 96 + 8}px`);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('runs the chosen item and closes, putting the element back', async () => {
|
|
172
|
+
const rendered = await renderContextMenu();
|
|
173
|
+
longPress(rendered.card());
|
|
174
|
+
await waitForOpen(rendered);
|
|
175
|
+
|
|
176
|
+
fireEvent.tap(
|
|
177
|
+
rendered.menu.getByRole('button', { name: 'Delete, destructive' }),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
expect(rendered.host.deleted).toHaveBeenCalledTimes(1);
|
|
181
|
+
expect(rendered.host.open()).toBe(false);
|
|
182
|
+
await waitForClose(rendered);
|
|
183
|
+
expect(rendered.overlay.contains(rendered.card())).toBe(false);
|
|
184
|
+
expect(style(rendered.holder(), 'height')).toBe('');
|
|
185
|
+
// A tap on the element still reaches it.
|
|
186
|
+
fireEvent.tap(rendered.card());
|
|
187
|
+
expect(rendered.host.opened).toHaveBeenCalledTimes(1);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('closes on a tap outside the menu', async () => {
|
|
191
|
+
const rendered = await renderContextMenu();
|
|
192
|
+
rendered.host.open.set(true);
|
|
193
|
+
await waitForOpen(rendered);
|
|
194
|
+
|
|
195
|
+
fireEvent.tap(rendered.overlay.firstElementChild!);
|
|
196
|
+
|
|
197
|
+
expect(rendered.host.open()).toBe(false);
|
|
198
|
+
await waitForClose(rendered);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("doesn't open on a long press when disabled", async () => {
|
|
202
|
+
const rendered = await renderContextMenu();
|
|
203
|
+
rendered.host.disabled.set(true);
|
|
204
|
+
await waitForUpdate();
|
|
205
|
+
|
|
206
|
+
longPress(rendered.card());
|
|
207
|
+
await waitForUpdate();
|
|
208
|
+
|
|
209
|
+
expect(rendered.host.open()).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('opens a Material menu beside the element on Android, leaving it in place', async () => {
|
|
213
|
+
const rendered = await renderContextMenu(null);
|
|
214
|
+
const home = rendered.holder();
|
|
215
|
+
|
|
216
|
+
longPress(rendered.card());
|
|
217
|
+
await waitForOpen(rendered);
|
|
218
|
+
|
|
219
|
+
expect(home.contains(rendered.card())).toBe(true);
|
|
220
|
+
expect(style(home, 'height')).toBe('');
|
|
221
|
+
expect(rendered.overlay.querySelector('.bg-glass-dim')).toBeNull();
|
|
222
|
+
expect(rendered.menu.getByRole('button', { name: 'Share' })).toBeTruthy();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
type ElementRef,
|
|
4
|
+
ViewEncapsulation,
|
|
5
|
+
booleanAttribute,
|
|
6
|
+
inject,
|
|
7
|
+
input,
|
|
8
|
+
model,
|
|
9
|
+
output,
|
|
10
|
+
viewChild,
|
|
11
|
+
} from '@angular/core';
|
|
12
|
+
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
|
|
13
|
+
|
|
14
|
+
import { type MenuElement, UiMenuPopup } from '../menu/menu-popup';
|
|
15
|
+
import { UI_MENU, type UiMenuHost } from '../menu/menu';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The `ui-context-menu` an item is in, for `UI_MENU`. A named function rather
|
|
19
|
+
* than an inline factory: the component's metadata keeps its own copy of an
|
|
20
|
+
* inline one, which nothing calls, so an app's coverage report would show it
|
|
21
|
+
* as uncovered.
|
|
22
|
+
*/
|
|
23
|
+
const contextMenu = (): UiMenuHost => inject(UiContextMenu);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A context menu: a long press on an element lifts it and opens a menu of
|
|
27
|
+
* actions beside it, while the rest of the screen dims.
|
|
28
|
+
*
|
|
29
|
+
* ```html
|
|
30
|
+
* <ui-context-menu>
|
|
31
|
+
* <view class="rounded-xl bg-card p-4">
|
|
32
|
+
* <text class="text-foreground">Trip to Kyoto</text>
|
|
33
|
+
* </view>
|
|
34
|
+
* <ui-menu-item label="Share" icon="upload" (pressed)="share()" />
|
|
35
|
+
* <ui-menu-item label="Favorite" icon="heart" (pressed)="favorite()" />
|
|
36
|
+
* <ui-menu-separator />
|
|
37
|
+
* <ui-menu-item label="Delete" icon="trash" variant="destructive" (pressed)="remove()" />
|
|
38
|
+
* </ui-context-menu>
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* The items (`ui-menu-item`, `ui-menu-section`, `ui-menu-separator`) are the
|
|
42
|
+
* menu, and everything else is the element it opens from. Taps on the
|
|
43
|
+
* element still reach it: Lynx tells a tap from a long press. A tap outside
|
|
44
|
+
* the menu, choosing an item, or Android's back button closes it, and
|
|
45
|
+
* `[(open)]` opens and closes it from code.
|
|
46
|
+
*
|
|
47
|
+
* On iOS the element lifts above a dimming layer (a blur before iOS 26) and
|
|
48
|
+
* the menu, Liquid Glass from iOS 26, opens below it, or above it near the
|
|
49
|
+
* bottom of the screen; when it fits on neither side, the element moves up
|
|
50
|
+
* to make room. The element itself moves into the menu's overlay, so it
|
|
51
|
+
* stays live and looks exactly as it did. On Android it's a Material 3 menu
|
|
52
|
+
* beside the element, without the lift or the dimming, unless the app opts
|
|
53
|
+
* into glass with `provideGlass({ android: 'glass' })`.
|
|
54
|
+
*
|
|
55
|
+
* Apple asks for a context menu's first actions to match the swipe actions
|
|
56
|
+
* of the row it's on, when there are any, so the two ways to reach an
|
|
57
|
+
* action agree.
|
|
58
|
+
*/
|
|
59
|
+
@Component({
|
|
60
|
+
selector: 'ui-context-menu',
|
|
61
|
+
imports: [LYNX_ELEMENTS, UiMenuPopup],
|
|
62
|
+
encapsulation: ViewEncapsulation.None,
|
|
63
|
+
providers: [{ provide: UI_MENU, useFactory: contextMenu }],
|
|
64
|
+
template: `
|
|
65
|
+
<!-- Keeps the element's size while it's lifted into the overlay, so the
|
|
66
|
+
page doesn't move under the menu. -->
|
|
67
|
+
<view
|
|
68
|
+
#home
|
|
69
|
+
class="flex-col flex"
|
|
70
|
+
[style.width.px]="popup.placeholder()?.width"
|
|
71
|
+
[style.height.px]="popup.placeholder()?.height"
|
|
72
|
+
>
|
|
73
|
+
<view #element class="flex-col flex" (bindlongpress)="onLongPress()">
|
|
74
|
+
<ng-content />
|
|
75
|
+
</view>
|
|
76
|
+
</view>
|
|
77
|
+
<ui-menu-popup
|
|
78
|
+
#popup
|
|
79
|
+
kind="context"
|
|
80
|
+
[open]="open()"
|
|
81
|
+
[anchor]="elementView().nativeElement"
|
|
82
|
+
[anchorHome]="homeView().nativeElement"
|
|
83
|
+
(dismissed)="close()"
|
|
84
|
+
(closed)="closed.emit()"
|
|
85
|
+
>
|
|
86
|
+
<ng-content select="ui-menu-item, ui-menu-section, ui-menu-separator" />
|
|
87
|
+
</ui-menu-popup>
|
|
88
|
+
`,
|
|
89
|
+
})
|
|
90
|
+
export class UiContextMenu implements UiMenuHost {
|
|
91
|
+
/** Whether the menu is open. */
|
|
92
|
+
readonly open = model(false);
|
|
93
|
+
/** Turns the long press off. */
|
|
94
|
+
readonly disabled = input(false, { transform: booleanAttribute });
|
|
95
|
+
|
|
96
|
+
/** The menu has closed and its close animation has finished. */
|
|
97
|
+
readonly closed = output<void>();
|
|
98
|
+
|
|
99
|
+
// Required: both are always in the template.
|
|
100
|
+
readonly elementView = viewChild.required<ElementRef<MenuElement>>('element');
|
|
101
|
+
readonly homeView = viewChild.required<ElementRef<MenuElement>>('home');
|
|
102
|
+
|
|
103
|
+
protected onLongPress(): void {
|
|
104
|
+
if (!this.disabled()) this.open.set(true);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
close(): void {
|
|
108
|
+
this.open.set(false);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UiContextMenu } from './context-menu';
|