@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.
Files changed (259) hide show
  1. package/dist/commands/add.d.ts +6 -2
  2. package/dist/commands/add.js +41 -11
  3. package/dist/commands/add.js.map +1 -1
  4. package/dist/commands/add.test.js +162 -9
  5. package/dist/commands/add.test.js.map +1 -1
  6. package/dist/commands/diff.d.ts +2 -1
  7. package/dist/commands/diff.js +38 -25
  8. package/dist/commands/diff.js.map +1 -1
  9. package/dist/commands/diff.test.js +72 -11
  10. package/dist/commands/diff.test.js.map +1 -1
  11. package/dist/commands/doctor.js +228 -34
  12. package/dist/commands/doctor.js.map +1 -1
  13. package/dist/commands/doctor.test.js +367 -10
  14. package/dist/commands/doctor.test.js.map +1 -1
  15. package/dist/commands/eject.js +7 -0
  16. package/dist/commands/eject.js.map +1 -1
  17. package/dist/commands/eject.test.js +39 -0
  18. package/dist/commands/eject.test.js.map +1 -1
  19. package/dist/commands/info.js +3 -2
  20. package/dist/commands/info.js.map +1 -1
  21. package/dist/commands/init.d.ts +37 -1
  22. package/dist/commands/init.js +169 -40
  23. package/dist/commands/init.js.map +1 -1
  24. package/dist/commands/init.test.js +392 -27
  25. package/dist/commands/init.test.js.map +1 -1
  26. package/dist/commands/install-packages.d.ts +6 -0
  27. package/dist/commands/install-packages.js +20 -0
  28. package/dist/commands/install-packages.js.map +1 -0
  29. package/dist/commands/install-packages.test.d.ts +1 -0
  30. package/dist/commands/install-packages.test.js +55 -0
  31. package/dist/commands/install-packages.test.js.map +1 -0
  32. package/dist/commands/list.js +21 -14
  33. package/dist/commands/list.js.map +1 -1
  34. package/dist/commands/list.test.js +62 -5
  35. package/dist/commands/list.test.js.map +1 -1
  36. package/dist/commands/outdated.js +11 -10
  37. package/dist/commands/outdated.js.map +1 -1
  38. package/dist/commands/outdated.test.js +20 -5
  39. package/dist/commands/outdated.test.js.map +1 -1
  40. package/dist/commands/remove.d.ts +4 -0
  41. package/dist/commands/remove.js +17 -4
  42. package/dist/commands/remove.js.map +1 -1
  43. package/dist/commands/remove.test.js +53 -2
  44. package/dist/commands/remove.test.js.map +1 -1
  45. package/dist/commands/theme.js +1 -1
  46. package/dist/commands/theme.js.map +1 -1
  47. package/dist/commands/update.d.ts +11 -0
  48. package/dist/commands/update.js +174 -58
  49. package/dist/commands/update.js.map +1 -1
  50. package/dist/commands/update.test.js +587 -33
  51. package/dist/commands/update.test.js.map +1 -1
  52. package/dist/index.js +7 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/lockfile.d.ts +7 -0
  55. package/dist/lockfile.js +36 -0
  56. package/dist/lockfile.js.map +1 -1
  57. package/dist/lockfile.test.d.ts +1 -0
  58. package/dist/lockfile.test.js +25 -0
  59. package/dist/lockfile.test.js.map +1 -0
  60. package/dist/registry.d.ts +14 -0
  61. package/dist/registry.js +78 -24
  62. package/dist/registry.js.map +1 -1
  63. package/dist/registry.test.js +119 -38
  64. package/dist/registry.test.js.map +1 -1
  65. package/dist/test-utils.d.ts +5 -0
  66. package/dist/test-utils.js +6 -1
  67. package/dist/test-utils.js.map +1 -1
  68. package/dist/testing/fire-lynx-event.d.ts +14 -0
  69. package/dist/testing/fire-lynx-event.js +19 -0
  70. package/dist/testing/fire-lynx-event.js.map +1 -0
  71. package/dist/testing/fire-lynx-event.test.d.ts +1 -0
  72. package/dist/testing/fire-lynx-event.test.js +44 -0
  73. package/dist/testing/fire-lynx-event.test.js.map +1 -0
  74. package/dist/testing/lynx-main-thread.d.ts +22 -0
  75. package/dist/testing/lynx-main-thread.js +11 -0
  76. package/dist/testing/lynx-main-thread.js.map +1 -0
  77. package/dist/testing/lynx-main-thread.test.d.ts +1 -0
  78. package/dist/testing/lynx-main-thread.test.js +24 -0
  79. package/dist/testing/lynx-main-thread.test.js.map +1 -0
  80. package/dist/testing/set-input-signal.d.ts +14 -0
  81. package/dist/testing/set-input-signal.js +21 -0
  82. package/dist/testing/set-input-signal.js.map +1 -0
  83. package/dist/testing/set-input-signal.test.d.ts +1 -0
  84. package/dist/testing/set-input-signal.test.js +20 -0
  85. package/dist/testing/set-input-signal.test.js.map +1 -0
  86. package/dist/ui/components/accordion/accordion.test.ts +158 -144
  87. package/dist/ui/components/accordion/accordion.ts +19 -14
  88. package/dist/ui/components/action-sheet/action-sheet.test.ts +283 -151
  89. package/dist/ui/components/action-sheet/action-sheet.ts +77 -43
  90. package/dist/ui/components/alert/alert.test.ts +67 -19
  91. package/dist/ui/components/alert/alert.ts +14 -16
  92. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +208 -94
  93. package/dist/ui/components/alert-dialog/alert-dialog.ts +75 -38
  94. package/dist/ui/components/area-chart/area-chart.test.ts +134 -119
  95. package/dist/ui/components/area-chart/area-chart.ts +100 -315
  96. package/dist/ui/components/area-chart/index.ts +2 -3
  97. package/dist/ui/components/avatar/avatar.test.ts +89 -45
  98. package/dist/ui/components/badge/badge.test.ts +57 -23
  99. package/dist/ui/components/badge/badge.ts +12 -14
  100. package/dist/ui/components/bar-chart/bar-chart.test.ts +172 -45
  101. package/dist/ui/components/bar-chart/bar-chart.ts +140 -294
  102. package/dist/ui/components/bar-chart/index.ts +3 -3
  103. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +386 -128
  104. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +170 -53
  105. package/dist/ui/components/button/button.test.ts +611 -2
  106. package/dist/ui/components/button/button.ts +434 -49
  107. package/dist/ui/components/button/index.ts +2 -0
  108. package/dist/ui/components/button-group/button-group.test.ts +484 -2
  109. package/dist/ui/components/button-group/button-group.ts +380 -68
  110. package/dist/ui/components/button-group/index.ts +3 -0
  111. package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +183 -58
  112. package/dist/ui/components/candlestick-chart/candlestick-chart.ts +150 -323
  113. package/dist/ui/components/candlestick-chart/index.ts +3 -4
  114. package/dist/ui/components/card/card.test.ts +82 -62
  115. package/dist/ui/components/chart/chart-focus.test.ts +132 -0
  116. package/dist/ui/components/chart/chart-focus.ts +115 -0
  117. package/dist/ui/components/chart/chart-presets.test.ts +335 -0
  118. package/dist/ui/components/chart/chart-presets.ts +278 -0
  119. package/dist/ui/components/chart/chart-scene.test.ts +589 -0
  120. package/dist/ui/components/chart/chart-scene.ts +483 -0
  121. package/dist/ui/components/chart/chart-theme.test.ts +85 -0
  122. package/dist/ui/components/chart/chart-theme.ts +210 -0
  123. package/dist/ui/components/chart/chart-zoom.test.ts +159 -0
  124. package/dist/ui/components/chart/chart-zoom.ts +162 -0
  125. package/dist/ui/components/chart/chart.test.ts +1017 -0
  126. package/dist/ui/components/chart/chart.ts +934 -0
  127. package/dist/ui/components/chart/index.ts +24 -0
  128. package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
  129. package/dist/ui/components/checkbox/checkbox.ts +23 -23
  130. package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
  131. package/dist/ui/components/collapsible/collapsible.ts +19 -14
  132. package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
  133. package/dist/ui/components/context-menu/context-menu.ts +110 -0
  134. package/dist/ui/components/context-menu/index.ts +1 -0
  135. package/dist/ui/components/dialog/dialog.test.ts +213 -108
  136. package/dist/ui/components/dialog/dialog.ts +72 -35
  137. package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
  138. package/dist/ui/components/dock/dock-scroll.ts +133 -0
  139. package/dist/ui/components/dock/dock.test.ts +749 -0
  140. package/dist/ui/components/dock/dock.ts +960 -0
  141. package/dist/ui/components/dock/index.ts +2 -0
  142. package/dist/ui/components/empty-state/empty-state.test.ts +28 -39
  143. package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
  144. package/dist/ui/components/glass/concentric-radius.ts +17 -0
  145. package/dist/ui/components/glass/glass-design.test.ts +78 -0
  146. package/dist/ui/components/glass/glass-design.ts +119 -0
  147. package/dist/ui/components/glass/glass.test.ts +338 -0
  148. package/dist/ui/components/glass/glass.ts +360 -0
  149. package/dist/ui/components/glass/index.ts +18 -0
  150. package/dist/ui/components/icon/icon-theme.test.ts +169 -0
  151. package/dist/ui/components/icon/icon-theme.ts +227 -0
  152. package/dist/ui/components/icon/icon.test.ts +85 -25
  153. package/dist/ui/components/icon/icon.ts +38 -21
  154. package/dist/ui/components/input/input.test.ts +178 -101
  155. package/dist/ui/components/input/input.ts +15 -12
  156. package/dist/ui/components/label/label.test.ts +11 -24
  157. package/dist/ui/components/line-chart/index.ts +2 -3
  158. package/dist/ui/components/line-chart/line-chart.test.ts +149 -56
  159. package/dist/ui/components/line-chart/line-chart.ts +83 -241
  160. package/dist/ui/components/list/list.test.ts +119 -21
  161. package/dist/ui/components/list/list.ts +21 -15
  162. package/dist/ui/components/menu/index.ts +25 -0
  163. package/dist/ui/components/menu/menu-placement.test.ts +153 -0
  164. package/dist/ui/components/menu/menu-placement.ts +159 -0
  165. package/dist/ui/components/menu/menu-popup.test.ts +632 -0
  166. package/dist/ui/components/menu/menu-popup.ts +544 -0
  167. package/dist/ui/components/menu/menu.test.ts +429 -0
  168. package/dist/ui/components/menu/menu.ts +415 -0
  169. package/dist/ui/components/nav-drawer/nav-drawer.test.ts +435 -191
  170. package/dist/ui/components/nav-drawer/nav-drawer.ts +127 -56
  171. package/dist/ui/components/pagination/pagination.test.ts +177 -65
  172. package/dist/ui/components/pagination/pagination.ts +55 -8
  173. package/dist/ui/components/progress/progress.test.ts +102 -46
  174. package/dist/ui/components/progress/progress.ts +52 -54
  175. package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
  176. package/dist/ui/components/radio-group/radio-group.ts +24 -24
  177. package/dist/ui/components/scatter-chart/index.ts +2 -3
  178. package/dist/ui/components/scatter-chart/scatter-chart.test.ts +131 -26
  179. package/dist/ui/components/scatter-chart/scatter-chart.ts +104 -250
  180. package/dist/ui/components/scroll-area/scroll-area.test.ts +95 -24
  181. package/dist/ui/components/scroll-area/scroll-area.ts +8 -8
  182. package/dist/ui/components/select/select.test.ts +138 -78
  183. package/dist/ui/components/select/select.ts +17 -11
  184. package/dist/ui/components/separator/separator.test.ts +18 -17
  185. package/dist/ui/components/sheet/sheet.test.ts +64 -11
  186. package/dist/ui/components/sheet/sheet.ts +9 -1
  187. package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
  188. package/dist/ui/components/skeleton/skeleton.ts +10 -16
  189. package/dist/ui/components/spinner/spinner.test.ts +67 -22
  190. package/dist/ui/components/spinner/spinner.ts +48 -41
  191. package/dist/ui/components/switch/switch.test.ts +116 -46
  192. package/dist/ui/components/switch/switch.ts +8 -13
  193. package/dist/ui/components/tab-bar/index.ts +1 -0
  194. package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
  195. package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
  196. package/dist/ui/components/tabs/tabs.test.ts +215 -157
  197. package/dist/ui/components/tabs/tabs.ts +110 -118
  198. package/dist/ui/components/textarea/textarea.test.ts +206 -100
  199. package/dist/ui/components/textarea/textarea.ts +14 -12
  200. package/dist/ui/components/toast/toast.test.ts +436 -315
  201. package/dist/ui/components/toast/toast.ts +62 -55
  202. package/dist/ui/components/toggle/toggle.test.ts +247 -140
  203. package/dist/ui/theme/chart-colors.test.ts +35 -0
  204. package/dist/ui/theme/dark.css +46 -2
  205. package/dist/ui/theme/default.css +63 -7
  206. package/dist/ui/theme/icon-colors.test.ts +35 -0
  207. package/dist/ui/theme/rgb-channels.test.ts +84 -0
  208. package/dist/ui/theme/tailwind-build.test.ts +114 -0
  209. package/dist/ui/theme/tailwind-css.test.ts +83 -0
  210. package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
  211. package/dist/ui/theme/tailwind-plugin.ts +98 -25
  212. package/dist/ui/theme/tailwind.css +113 -0
  213. package/dist/ui/utils/animate.test.ts +13 -1
  214. package/dist/utils/analyze.d.ts +14 -0
  215. package/dist/utils/analyze.js +15 -0
  216. package/dist/utils/analyze.js.map +1 -1
  217. package/dist/utils/analyze.test.js +14 -1
  218. package/dist/utils/analyze.test.js.map +1 -1
  219. package/dist/utils/installed.d.ts +29 -0
  220. package/dist/utils/installed.js +43 -0
  221. package/dist/utils/installed.js.map +1 -0
  222. package/dist/utils/installed.test.d.ts +1 -0
  223. package/dist/utils/installed.test.js +57 -0
  224. package/dist/utils/installed.test.js.map +1 -0
  225. package/dist/utils/interactive.d.ts +9 -0
  226. package/dist/utils/interactive.js +10 -0
  227. package/dist/utils/interactive.js.map +1 -0
  228. package/dist/utils/interactive.test.d.ts +1 -0
  229. package/dist/utils/interactive.test.js +24 -0
  230. package/dist/utils/interactive.test.js.map +1 -0
  231. package/dist/utils/packages.d.ts +36 -0
  232. package/dist/utils/packages.js +70 -0
  233. package/dist/utils/packages.js.map +1 -0
  234. package/dist/utils/packages.test.d.ts +1 -0
  235. package/dist/utils/packages.test.js +89 -0
  236. package/dist/utils/packages.test.js.map +1 -0
  237. package/dist/utils/resolve-paths.d.ts +17 -1
  238. package/dist/utils/resolve-paths.js +21 -3
  239. package/dist/utils/resolve-paths.js.map +1 -1
  240. package/dist/utils/resolve-paths.test.js +38 -2
  241. package/dist/utils/resolve-paths.test.js.map +1 -1
  242. package/dist/utils/tailwind.d.ts +25 -0
  243. package/dist/utils/tailwind.js +51 -0
  244. package/dist/utils/tailwind.js.map +1 -0
  245. package/dist/utils/tailwind.test.d.ts +1 -0
  246. package/dist/utils/tailwind.test.js +71 -0
  247. package/dist/utils/tailwind.test.js.map +1 -0
  248. package/package.json +13 -1
  249. package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -121
  250. package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -174
  251. package/dist/ui/components/button/button-render.test.ts +0 -113
  252. package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
  253. package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -174
  254. package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +0 -188
  255. package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +0 -525
  256. package/dist/ui/components/cartesian-chart/cartesian-chart.ts +0 -1140
  257. package/dist/ui/components/cartesian-chart/index.ts +0 -18
  258. package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -121
  259. package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -157
@@ -1,163 +1,421 @@
1
1
  import { Component, signal } from '@angular/core';
2
- import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
2
+ import {
3
+ fireEvent,
4
+ render,
5
+ waitForUpdate,
6
+ } from '@blotch/angular-lynx-testing-library';
3
7
  import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
4
- import { describe, expect, it } from 'vitest';
5
- import { setInputSignal } from '../../../test-utils/set-input-signal';
8
+ import {
9
+ type MockInstance,
10
+ afterEach,
11
+ beforeEach,
12
+ describe,
13
+ expect,
14
+ it,
15
+ vi,
16
+ } from 'vitest';
17
+ import { DURATION } from '../../utils/animate';
18
+ import {
19
+ type LynxMainThread,
20
+ lynxMainThread,
21
+ } from '../../../test-utils/lynx-main-thread';
6
22
  import { UiBottomSheet } from './bottom-sheet';
7
23
 
8
24
  @Component({
9
- standalone: true,
10
25
  imports: [UiBottomSheet, LYNX_ELEMENTS],
11
- template: `<ui-bottom-sheet [(open)]="open"
12
- ><text>Panel</text></ui-bottom-sheet
13
- >`,
26
+ template: `
27
+ <ui-bottom-sheet [(open)]="open" class="max-h-96" (closed)="closed()">
28
+ <text>Panel</text>
29
+ </ui-bottom-sheet>
30
+ `,
14
31
  })
15
32
  class BottomSheetHost {
16
- open = true;
33
+ readonly open = signal(false);
34
+ readonly closed = vi.fn();
17
35
  }
18
36
 
19
- describe('UiBottomSheet', () => {
20
- const patchRefs = (inst: UiBottomSheet, view: Element) => {
21
- (inst as unknown as { backdropRef: unknown }).backdropRef = signal({
22
- nativeElement: view,
23
- });
24
- (inst as unknown as { panelRef: unknown }).panelRef = signal({
25
- nativeElement: view,
26
- });
37
+ /**
38
+ * The Lynx function an element's `invoke()` calls to run one of its UI
39
+ * methods, such as moving a screen reader's focus. The testing library
40
+ * defines it on the global object.
41
+ */
42
+ type InvokeUIMethod = {
43
+ __InvokeUIMethod: (
44
+ element: Element,
45
+ method: string,
46
+ params: Record<string, unknown>,
47
+ callback: (result: { code: number }) => void,
48
+ ) => void;
49
+ };
50
+
51
+ /** `__ElementAnimate`'s first argument for starting an animation. */
52
+ const START = 0;
53
+ /** `__ElementAnimate`'s first argument for canceling an animation. */
54
+ const CANCEL = 3;
55
+
56
+ const renderSheet = async () => {
57
+ const result = await render(BottomSheetHost);
58
+ const { container } = result;
59
+ return {
60
+ ...result,
61
+ host: result.componentRef.instance as BottomSheetHost,
62
+ overlay: container.querySelector('overlay')!,
63
+ backdrop: container.querySelector('overlay > view')!,
64
+ panel: container.querySelector('.rounded-t-2xl')!,
65
+ // The strip around the grabber pill, which takes the drag.
66
+ handle: container.querySelector('.bg-muted')!.parentElement!,
27
67
  };
68
+ };
28
69
 
29
- it('renders the panel content when open', async () => {
30
- const { getByText } = await render(BottomSheetHost);
31
- expect(getByText('Panel')).toBeTruthy();
32
- });
70
+ /**
71
+ * Opens the sheet and waits out its open sequence: the overlay shows on one
72
+ * tick, and the panel slides in on the next.
73
+ */
74
+ const openSheet = async (host: BottomSheetHost) => {
75
+ host.open.set(true);
76
+ for (let i = 0; i < 3; i++) await waitForUpdate();
77
+ };
33
78
 
34
- it('backdrop tap closes the sheet', async () => {
35
- const { componentRef } = await render(UiBottomSheet);
36
- const inst = componentRef.instance as UiBottomSheet;
37
- inst.open.set(true);
38
- await waitForUpdate();
39
- (inst as unknown as { onBackdropTap: () => void }).onBackdropTap();
40
- expect(inst.open()).toBe(false);
41
- });
79
+ /**
80
+ * Waits for the close sequence to finish: the panel slides out, then the
81
+ * overlay hides and `closed` fires. Polled rather than timed, since the
82
+ * sequence only starts on the update after `open` changes.
83
+ */
84
+ const waitForClose = async (host: BottomSheetHost) => {
85
+ await vi.waitFor(() => expect(host.closed).toHaveBeenCalled());
86
+ await waitForUpdate();
87
+ };
88
+
89
+ /**
90
+ * The panel's live drag offset, from its inline transform.
91
+ */
92
+ const transformOf = (panel: Element) =>
93
+ (panel as HTMLElement).style.getPropertyValue('transform');
94
+
95
+ /**
96
+ * The touch fields a handle's touch event carries.
97
+ */
98
+ const touchAt = (clientY: number) => ({
99
+ eventType: 'catchEvent',
100
+ touches: [{ clientY }],
101
+ });
42
102
 
43
- it('panel tap is a no-op', async () => {
44
- const { componentRef } = await render(UiBottomSheet);
45
- const inst = componentRef.instance as UiBottomSheet;
46
- expect(() =>
47
- (inst as unknown as { onPanelTap: () => void }).onPanelTap(),
48
- ).not.toThrow();
103
+ describe('UiBottomSheet', () => {
104
+ // Watched per test: the testing library resets the main thread after each.
105
+ let animate: MockInstance<LynxMainThread['__ElementAnimate']>;
106
+ beforeEach(() => {
107
+ animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
49
108
  });
109
+ afterEach(() => animate.mockRestore());
50
110
 
51
- it('open effect runs the animate-in and animate-out lifecycles', async () => {
52
- const { componentRef, container } = await render(UiBottomSheet);
53
- const inst = componentRef.instance as UiBottomSheet;
54
- const view = container.querySelector('view') as Element;
55
- patchRefs(inst, view);
111
+ it('stays hidden until opened, then shows the panel and its content', async () => {
112
+ const { host, overlay, getByText } = await renderSheet();
113
+ expect(overlay.getAttribute('style')).toContain('display: none');
114
+ expect(overlay.getAttribute('visible')).toBe('false');
56
115
 
57
- inst.open.set(true);
58
- await waitForUpdate();
59
- await waitForUpdate();
60
- await waitForUpdate();
116
+ await openSheet(host);
117
+ expect(overlay.getAttribute('style')).not.toContain('display: none');
118
+ expect(overlay.getAttribute('visible')).toBe('true');
119
+ expect(getByText('Panel')).toBeTruthy();
120
+ });
61
121
 
62
- inst.open.set(false);
63
- await new Promise((r) => setTimeout(r, 320));
64
- await waitForUpdate();
65
- expect(inst.open()).toBe(false);
122
+ it("adds the class to the panel's own", async () => {
123
+ const { panel } = await renderSheet();
124
+ expect(panel.classList).toContain('max-h-96');
125
+ expect(panel.classList).toContain('bg-background');
66
126
  });
67
127
 
68
- it('open effect bails when backdrop/panel refs are undefined', async () => {
69
- const { componentRef } = await render(UiBottomSheet);
70
- const inst = componentRef.instance as UiBottomSheet;
71
- inst.open.set(true);
72
- await waitForUpdate();
73
- await waitForUpdate();
74
- inst.open.set(false);
75
- await waitForUpdate();
76
- expect(inst.open()).toBe(false);
128
+ it('fades the backdrop in and slides the panel up when opened', async () => {
129
+ const { host, backdrop, panel } = await renderSheet();
130
+ await openSheet(host);
131
+ expect(animate).toHaveBeenCalledWith(backdrop, [
132
+ START,
133
+ expect.any(String),
134
+ [{ opacity: 0 }, { opacity: 1 }],
135
+ expect.objectContaining({ duration: DURATION.normal }),
136
+ ]);
137
+ expect(animate).toHaveBeenCalledWith(panel, [
138
+ START,
139
+ expect.any(String),
140
+ [
141
+ { transform: 'translateY(100%)', opacity: 0.8 },
142
+ { transform: 'translateY(0%)', opacity: 1 },
143
+ ],
144
+ expect.objectContaining({ duration: DURATION.slow }),
145
+ ]);
77
146
  });
78
147
 
79
- it('drag beyond the dismiss threshold closes the sheet', async () => {
80
- const { componentRef, container } = await render(UiBottomSheet);
81
- const inst = componentRef.instance as UiBottomSheet;
82
- const view = container.querySelector('view') as Element;
83
- patchRefs(inst, view);
84
- inst.open.set(true);
85
- await waitForUpdate();
148
+ it('moves a screen reader into the panel and keeps it there while open', async () => {
149
+ const invoke = vi.spyOn(
150
+ globalThis as unknown as InvokeUIMethod,
151
+ '__InvokeUIMethod',
152
+ );
153
+ const { host, panel } = await renderSheet();
154
+ expect(panel.getAttribute('accessibility-exclusive-focus')).toBe('false');
86
155
 
87
- const cast = inst as unknown as {
88
- onHandleTouchStart: (e: { touches: { clientY: number }[] }) => void;
89
- onHandleTouchMove: (e: { touches: { clientY: number }[] }) => void;
90
- onHandleTouchEnd: () => void;
91
- };
92
- cast.onHandleTouchStart({ touches: [{ clientY: 100 }] });
93
- cast.onHandleTouchMove({ touches: [{ clientY: 300 }] });
94
- cast.onHandleTouchEnd();
95
- await waitForUpdate();
96
- expect(inst.open()).toBe(false);
156
+ await openSheet(host);
157
+ expect(invoke).toHaveBeenCalledWith(
158
+ panel,
159
+ 'requestAccessibilityFocus',
160
+ expect.anything(),
161
+ expect.any(Function),
162
+ );
163
+ expect(panel.getAttribute('accessibility-exclusive-focus')).toBe('true');
164
+ invoke.mockRestore();
97
165
  });
98
166
 
99
- it('drag below the dismiss threshold snaps back to open', async () => {
100
- const { componentRef, container } = await render(UiBottomSheet);
101
- const inst = componentRef.instance as UiBottomSheet;
102
- const view = container.querySelector('view') as Element;
103
- patchRefs(inst, view);
104
- inst.open.set(true);
105
- await waitForUpdate();
167
+ it('closes when the backdrop is tapped, sliding the panel down', async () => {
168
+ const { host, overlay, backdrop, panel } = await renderSheet();
169
+ await openSheet(host);
106
170
 
107
- const cast = inst as unknown as {
108
- onHandleTouchStart: (e: { touches: { clientY: number }[] }) => void;
109
- onHandleTouchMove: (e: { touches: { clientY: number }[] }) => void;
110
- onHandleTouchEnd: () => void;
111
- };
112
- cast.onHandleTouchStart({ touches: [{ clientY: 100 }] });
113
- cast.onHandleTouchMove({ touches: [{ clientY: 130 }] });
114
- cast.onHandleTouchEnd();
115
- await waitForUpdate();
116
- expect(inst.open()).toBe(true);
171
+ fireEvent.tap(backdrop);
172
+ expect(host.open()).toBe(false);
173
+ await waitForClose(host);
174
+ expect(animate).toHaveBeenCalledWith(backdrop, [
175
+ START,
176
+ expect.any(String),
177
+ [{ opacity: 1 }, { opacity: 0 }],
178
+ expect.objectContaining({ duration: DURATION.normal }),
179
+ ]);
180
+ expect(animate).toHaveBeenCalledWith(panel, [
181
+ START,
182
+ expect.any(String),
183
+ [
184
+ { transform: 'translateY(0%)', opacity: 1 },
185
+ { transform: 'translateY(100%)', opacity: 0.8 },
186
+ ],
187
+ expect.objectContaining({ duration: DURATION.normal }),
188
+ ]);
189
+ // Once the slide is over, the overlay hides and `closed` fires, only for
190
+ // this close: a sheet that starts closed hasn't closed before.
191
+ expect(overlay.getAttribute('style')).toContain('display: none');
192
+ expect(host.closed).toHaveBeenCalledOnce();
117
193
  });
118
194
 
119
- it('touch handlers guard against missing touches and non-active drags', async () => {
120
- const { componentRef } = await render(UiBottomSheet);
121
- const inst = componentRef.instance as UiBottomSheet;
122
- const cast = inst as unknown as {
123
- onHandleTouchStart: (e: { touches?: { clientY: number }[] }) => void;
124
- onHandleTouchMove: (e: { touches?: { clientY: number }[] }) => void;
125
- onHandleTouchEnd: () => void;
126
- };
127
- // touchmove without prior touchstart is a no-op (guards `#isDragging`).
128
- cast.onHandleTouchMove({ touches: [{ clientY: 200 }] });
129
- // touchstart / touchmove without touches entries is a no-op.
130
- cast.onHandleTouchStart({ touches: [] });
131
- cast.onHandleTouchStart({ touches: [{ clientY: 100 }] });
132
- cast.onHandleTouchMove({ touches: [] });
133
- // touchend without an active drag is also a no-op.
134
- cast.onHandleTouchEnd();
135
- cast.onHandleTouchEnd();
195
+ it('stays open when the panel is tapped', async () => {
196
+ const { host, panel } = await renderSheet();
197
+ await openSheet(host);
198
+ // The panel catches its taps, so they never reach the backdrop.
199
+ fireEvent.tap(panel, { eventType: 'catchEvent' });
200
+ expect(host.open()).toBe(true);
136
201
  });
137
202
 
138
- it('applies a user class on the panel', async () => {
139
- const { componentRef, container } = await render(UiBottomSheet);
140
- const inst = componentRef.instance as UiBottomSheet;
141
- setInputSignal(inst.userClass, 'custom-panel');
203
+ it('stops its close timers when it is destroyed while closing', async () => {
204
+ const warn = vi.spyOn(console, 'warn');
205
+ const { host, unmount } = await renderSheet();
206
+ await openSheet(host);
207
+ host.open.set(false);
142
208
  await waitForUpdate();
143
- expect(
144
- Array.from(container.querySelectorAll('view')).some((el) =>
145
- el.getAttribute('class')?.includes('custom-panel'),
146
- ),
147
- ).toBe(true);
209
+ // Destroyed during the close animation, as an `@if` around the sheet
210
+ // would do. Its close timer (DURATION.normal + 20) used to fire anyway
211
+ // and emit `closed` from the destroyed output.
212
+ unmount();
213
+ await new Promise((resolve) => setTimeout(resolve, DURATION.normal + 50));
214
+ const warnings = warn.mock.calls.map(([message]) => String(message));
215
+ warn.mockRestore();
216
+ expect(warnings.filter((message) => message.includes('NG0953'))).toEqual(
217
+ [],
218
+ );
219
+ expect(host.closed).not.toHaveBeenCalled();
220
+ });
221
+
222
+ describe('dragged by its handle', () => {
223
+ it('follows the finger down, lightening the backdrop as it goes', async () => {
224
+ const { host, backdrop, panel, handle } = await renderSheet();
225
+ await openSheet(host);
226
+ animate.mockClear();
227
+
228
+ fireEvent.touchstart(handle, touchAt(100));
229
+ // The open slide holds the panel in place until it's canceled.
230
+ expect(animate).toHaveBeenCalledWith(panel, [CANCEL, expect.any(String)]);
231
+ fireEvent.touchmove(handle, touchAt(200));
232
+ await waitForUpdate();
233
+ expect(transformOf(panel)).toBe('translateY(100px)');
234
+ // 100px of the 500px over which the backdrop clears: 4/5 of its dim.
235
+ expect(backdrop.getAttribute('style')).toContain('rgba(0, 0, 0, 0.4)');
236
+ });
237
+
238
+ it('closes when released past the threshold, sliding on from the finger', async () => {
239
+ const { host, panel, handle } = await renderSheet();
240
+ await openSheet(host);
241
+
242
+ fireEvent.touchstart(handle, touchAt(100));
243
+ fireEvent.touchmove(handle, touchAt(300));
244
+ fireEvent.touchend(handle, { eventType: 'catchEvent' });
245
+ expect(host.open()).toBe(false);
246
+ await waitForClose(host);
247
+ expect(animate).toHaveBeenCalledWith(panel, [
248
+ START,
249
+ expect.any(String),
250
+ [
251
+ { transform: 'translateY(200px)', opacity: 1 },
252
+ { transform: 'translateY(600px)', opacity: 0.8 },
253
+ ],
254
+ expect.objectContaining({ duration: DURATION.normal }),
255
+ ]);
256
+ expect(host.closed).toHaveBeenCalledOnce();
257
+ // Hidden, it's back in place for the next open.
258
+ expect(transformOf(panel)).toBe('translateY(0px)');
259
+ });
260
+
261
+ it('springs back when released or canceled short of the threshold', async () => {
262
+ const { host, panel, handle } = await renderSheet();
263
+ await openSheet(host);
264
+
265
+ fireEvent.touchstart(handle, touchAt(100));
266
+ fireEvent.touchmove(handle, touchAt(130));
267
+ fireEvent.touchend(handle, { eventType: 'catchEvent' });
268
+ await waitForUpdate();
269
+ expect(host.open()).toBe(true);
270
+ expect(animate).toHaveBeenCalledWith(panel, [
271
+ START,
272
+ expect.any(String),
273
+ [{ transform: 'translateY(30px)' }, { transform: 'translateY(0px)' }],
274
+ expect.objectContaining({ duration: DURATION.fast }),
275
+ ]);
276
+ expect(transformOf(panel)).toBe('translateY(0px)');
277
+
278
+ // A scroll that takes the touch over cancels it the same way.
279
+ fireEvent.touchstart(handle, touchAt(100));
280
+ fireEvent.touchmove(handle, touchAt(150));
281
+ fireEvent.touchcancel(handle, { eventType: 'catchEvent' });
282
+ await waitForUpdate();
283
+ expect(host.open()).toBe(true);
284
+ expect(transformOf(panel)).toBe('translateY(0px)');
285
+ });
286
+
287
+ it("doesn't move up past where it's docked", async () => {
288
+ const { host, panel, handle } = await renderSheet();
289
+ await openSheet(host);
290
+ fireEvent.touchstart(handle, touchAt(100));
291
+ fireEvent.touchmove(handle, touchAt(40));
292
+ await waitForUpdate();
293
+ expect(transformOf(panel)).toBe('translateY(0px)');
294
+ });
295
+
296
+ it('ignores touches without a position, and moves outside a drag', async () => {
297
+ const { host, panel, handle } = await renderSheet();
298
+ await openSheet(host);
299
+
300
+ // A move or release with no drag under way does nothing.
301
+ fireEvent.touchmove(handle, touchAt(300));
302
+ fireEvent.touchend(handle, { eventType: 'catchEvent' });
303
+ // Nor does a touch whose position is missing.
304
+ fireEvent.touchstart(handle, { eventType: 'catchEvent' });
305
+ fireEvent.touchmove(handle, touchAt(300));
306
+ fireEvent.touchstart(handle, { eventType: 'catchEvent', touches: [] });
307
+ fireEvent.touchmove(handle, touchAt(300));
308
+ await waitForUpdate();
309
+ expect(transformOf(panel)).toBe('translateY(0px)');
310
+
311
+ fireEvent.touchstart(handle, touchAt(100));
312
+ fireEvent.touchmove(handle, { eventType: 'catchEvent', touches: [] });
313
+ await waitForUpdate();
314
+ expect(transformOf(panel)).toBe('translateY(0px)');
315
+ expect(host.open()).toBe(true);
316
+ });
148
317
  });
149
318
 
150
- it('overlayStyle switches between visible and hidden variants', async () => {
151
- const { componentRef, container } = await render(UiBottomSheet);
152
- const inst = componentRef.instance as UiBottomSheet;
153
- let overlay = container.querySelector('overlay');
154
- expect(overlay?.getAttribute('style')).toContain('display: none');
155
-
156
- const view = container.querySelector('view') as Element;
157
- patchRefs(inst, view);
158
- inst.open.set(true);
159
- for (let i = 0; i < 10; i++) await waitForUpdate();
160
- overlay = container.querySelector('overlay');
161
- expect(overlay?.getAttribute('style')).not.toContain('display: none');
319
+ describe('dragged by mouse', () => {
320
+ /**
321
+ * Presses the handle, which catches its own mousedown.
322
+ */
323
+ const press = (handle: Element, y: number) =>
324
+ fireEvent.mousedown(handle, { eventType: 'catchEvent', y });
325
+ /**
326
+ * Moves the mouse, which the backdrop sees anywhere in the sheet.
327
+ */
328
+ const move = (backdrop: Element, y: number, buttons = 1) =>
329
+ fireEvent.mousemove(backdrop, { y, buttons });
330
+
331
+ it('closes when released past the threshold', async () => {
332
+ const { host, backdrop, panel, handle } = await renderSheet();
333
+ await openSheet(host);
334
+
335
+ press(handle, 100);
336
+ move(backdrop, 300);
337
+ await waitForUpdate();
338
+ expect(transformOf(panel)).toBe('translateY(200px)');
339
+ fireEvent.mouseup(backdrop);
340
+ expect(host.open()).toBe(false);
341
+ });
342
+
343
+ it('springs back when released short of the threshold', async () => {
344
+ const { host, backdrop, panel, handle } = await renderSheet();
345
+ await openSheet(host);
346
+
347
+ press(handle, 100);
348
+ move(backdrop, 130);
349
+ fireEvent.mouseup(backdrop);
350
+ await waitForUpdate();
351
+ expect(host.open()).toBe(true);
352
+ expect(transformOf(panel)).toBe('translateY(0px)');
353
+ });
354
+
355
+ it('ignores the backdrop click that follows a moved drag', async () => {
356
+ const { host, backdrop, handle } = await renderSheet();
357
+ await openSheet(host);
358
+
359
+ // A drag released above the panel: the browser's click lands on the
360
+ // backdrop, and must not close the sheet that just snapped back.
361
+ press(handle, 100);
362
+ move(backdrop, 40);
363
+ fireEvent.mouseup(backdrop);
364
+ fireEvent.tap(backdrop);
365
+ expect(host.open()).toBe(true);
366
+
367
+ // Only that one click is ignored: the next backdrop tap closes it.
368
+ fireEvent.tap(backdrop);
369
+ expect(host.open()).toBe(false);
370
+ });
371
+
372
+ it('a new press clears an ignore no click consumed', async () => {
373
+ const { host, backdrop, handle } = await renderSheet();
374
+ await openSheet(host);
375
+
376
+ // Released over the panel, so its catchtap ate the click.
377
+ press(handle, 100);
378
+ move(backdrop, 120);
379
+ fireEvent.mouseup(backdrop);
380
+ // A real backdrop click starts with a mousedown on the backdrop.
381
+ fireEvent.mousedown(backdrop);
382
+ fireEvent.tap(backdrop);
383
+ expect(host.open()).toBe(false);
384
+ });
385
+
386
+ it('a press without a move, or a touch drag, leaves the backdrop tap alone', async () => {
387
+ const { host, backdrop, handle } = await renderSheet();
388
+ await openSheet(host);
389
+
390
+ press(handle, 100);
391
+ fireEvent.mouseup(backdrop);
392
+ // A moved touch is never followed by a tap, so nothing is ignored.
393
+ fireEvent.touchstart(handle, touchAt(100));
394
+ fireEvent.touchmove(handle, touchAt(120));
395
+ fireEvent.touchend(handle, { eventType: 'catchEvent' });
396
+ fireEvent.tap(backdrop);
397
+ expect(host.open()).toBe(false);
398
+ });
399
+
400
+ it('ends a drag whose release it never saw', async () => {
401
+ const { host, backdrop, handle } = await renderSheet();
402
+ await openSheet(host);
403
+
404
+ press(handle, 100);
405
+ move(backdrop, 300);
406
+ // The button came up outside the window; the cursor is back without it.
407
+ move(backdrop, 120, 0);
408
+ // Released 200px down, past the threshold, so it closes.
409
+ expect(host.open()).toBe(false);
410
+ });
411
+
412
+ it('a move outside a drag leaves the sheet in place', async () => {
413
+ const { host, backdrop, panel } = await renderSheet();
414
+ await openSheet(host);
415
+ // The cursor passing over an open sheet with a button held elsewhere.
416
+ move(backdrop, 300);
417
+ await waitForUpdate();
418
+ expect(transformOf(panel)).toBe('translateY(0px)');
419
+ });
162
420
  });
163
421
  });