@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,8 +1,26 @@
1
- import { Component, signal } from '@angular/core';
2
- import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
1
+ import { Component, signal, viewChild } from '@angular/core';
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, vi } 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 { fireLynxEvent } from '../../../test-utils/fire-lynx-event';
19
+ import {
20
+ type LynxMainThread,
21
+ lynxMainThread,
22
+ } from '../../../test-utils/lynx-main-thread';
23
+ import { UiIcon } from '../icon/icon';
6
24
  import {
7
25
  UiNavDrawer,
8
26
  UiNavDrawerContent,
@@ -13,7 +31,6 @@ import {
13
31
  } from './nav-drawer';
14
32
 
15
33
  @Component({
16
- standalone: true,
17
34
  imports: [
18
35
  UiNavDrawer,
19
36
  UiNavDrawerHeader,
@@ -21,247 +38,474 @@ import {
21
38
  UiNavDrawerItem,
22
39
  UiNavDrawerFooter,
23
40
  UiNavDrawerTrigger,
41
+ UiIcon,
24
42
  LYNX_ELEMENTS,
25
43
  ],
26
44
  template: `
27
- <ui-nav-drawer [(open)]="open">
28
- <ui-nav-drawer-header><text>Nav</text></ui-nav-drawer-header>
29
- <ui-nav-drawer-content>
30
- <ui-nav-drawer-item>Home</ui-nav-drawer-item>
31
- </ui-nav-drawer-content>
32
- <ui-nav-drawer-footer><text>Foot</text></ui-nav-drawer-footer>
33
- </ui-nav-drawer>
34
- <ui-nav-drawer-trigger>Open</ui-nav-drawer-trigger>
45
+ <ui-nav-drawer-trigger class="ml-2" (pressed)="open.set(true)" />
46
+ @if (shown()) {
47
+ <ui-nav-drawer
48
+ [(open)]="open"
49
+ [side]="side()"
50
+ class="shadow-lg"
51
+ (closed)="closes.set(closes() + 1)"
52
+ >
53
+ <ui-nav-drawer-header class="gap-3">
54
+ <text>Menu</text>
55
+ </ui-nav-drawer-header>
56
+ <ui-nav-drawer-content class="pt-2">
57
+ <ui-nav-drawer-item
58
+ [active]="active() === 'home'"
59
+ (pressed)="presses.set(presses() + 1)"
60
+ >
61
+ <text class="text-foreground">Home</text>
62
+ </ui-nav-drawer-item>
63
+ <!-- A group: items nested in other elements count too. -->
64
+ <view class="group">
65
+ <ui-nav-drawer-item #settings [active]="active() === 'settings'">
66
+ <text class="text-foreground">Settings</text>
67
+ </ui-nav-drawer-item>
68
+ </view>
69
+ <ui-nav-drawer-item
70
+ [variant]="deleteVariant()"
71
+ [autoClose]="false"
72
+ class="mt-4"
73
+ >
74
+ <ui-icon name="trash" size="sm" />
75
+ <text class="text-foreground">Delete account</text>
76
+ </ui-nav-drawer-item>
77
+ </ui-nav-drawer-content>
78
+ <ui-nav-drawer-footer class="gap-4">
79
+ <text>Version 1</text>
80
+ </ui-nav-drawer-footer>
81
+ </ui-nav-drawer>
82
+ }
83
+ <!-- What the trash icon looks like plain, and in the destructive color. -->
84
+ <ui-icon name="trash" size="sm" class="plain-trash" />
85
+ <ui-icon
86
+ name="trash"
87
+ size="sm"
88
+ color="var(--destructive)"
89
+ class="destructive-trash"
90
+ />
35
91
  `,
36
92
  })
37
93
  class NavDrawerHost {
38
- open = true;
94
+ readonly open = signal(false);
95
+ readonly side = signal<'left' | 'right'>('left');
96
+ readonly active = signal('settings');
97
+ readonly deleteVariant = signal<'default' | 'destructive'>('destructive');
98
+ // Takes the drawer out of the tree, as an `@if` or a route change would.
99
+ readonly shown = signal(true);
100
+ readonly closes = signal(0);
101
+ readonly presses = signal(0);
102
+ readonly drawer = viewChild.required(UiNavDrawer);
103
+ readonly settings = viewChild.required<UiNavDrawerItem>('settings');
39
104
  }
40
105
 
41
- describe('UiNavDrawer', () => {
42
- const patchRefs = (inst: UiNavDrawer, view: Element) => {
43
- (inst as unknown as { backdropRef: unknown }).backdropRef = signal({
44
- nativeElement: view,
45
- });
46
- (inst as unknown as { panelRef: unknown }).panelRef = signal({
47
- nativeElement: view,
48
- });
106
+ const renderDrawer = async () => {
107
+ const result = await render(NavDrawerHost);
108
+ const { container, getByText } = result;
109
+ const panel = () =>
110
+ container.querySelector('[accessibility-exclusive-focus]')!;
111
+ return {
112
+ ...result,
113
+ host: result.componentRef.instance as NavDrawerHost,
114
+ overlay: () => container.querySelector('overlay')!,
115
+ panel,
116
+ backdrop: () => panel().parentElement!,
117
+ scrollView: () => container.querySelector('scroll-view')!,
118
+ trigger: () => container.querySelector('[accessibility-element]')!,
119
+ // An item's own view, which holds its projected label.
120
+ item: (label: string) => getByText(label).parentElement!,
121
+ svg: (selector: string) => container.querySelector(`svg${selector}`)!,
49
122
  };
123
+ };
124
+
125
+ /**
126
+ * Waits for the drawer's open or close sequence, which runs on timers, then
127
+ * renders what it changed.
128
+ */
129
+ const after = async (ms: number) => {
130
+ await new Promise((resolve) => setTimeout(resolve, ms));
131
+ await waitForUpdate();
132
+ };
133
+
134
+ /**
135
+ * Long enough for the open sequence. The sequence is chained timers, and on
136
+ * Windows each can fire up to a clock tick (about 16ms) late, so 50ms was too
137
+ * short for the dialogs' sequences on CI's Windows runner.
138
+ */
139
+ const OPENED = 150;
140
+ /** Long enough for the close animation and the hide after it, as above. */
141
+ const CLOSED = DURATION.normal + 150;
142
+
143
+ /** `__ElementAnimate`'s first argument for starting an animation. */
144
+ const START = 0;
145
+ /** `__ElementAnimate`'s first argument for canceling an animation. */
146
+ const CANCEL = 3;
50
147
 
51
- it('renders header, content items, footer, and trigger', async () => {
52
- const { container } = await render(NavDrawerHost);
53
- expect(container).toBeTruthy();
148
+ /**
149
+ * The scale a pressable view's inline style holds, if any.
150
+ */
151
+ const scaleOf = (view: Element) =>
152
+ (view as HTMLElement).style.getPropertyValue('transform');
153
+
154
+ describe('UiNavDrawer', () => {
155
+ // Watched per test: the testing library resets the main thread after each.
156
+ let animate: MockInstance<LynxMainThread['__ElementAnimate']>;
157
+ beforeEach(() => {
158
+ animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
54
159
  });
160
+ afterEach(() => animate.mockRestore());
55
161
 
56
- it('backdrop tap closes the drawer', async () => {
57
- const { componentRef } = await render(UiNavDrawer);
58
- const inst = componentRef.instance as UiNavDrawer;
59
- inst.open.set(true);
60
- await waitForUpdate();
61
- (inst as unknown as { onBackdropTap: () => void }).onBackdropTap();
62
- expect(inst.open()).toBe(false);
162
+ it('renders each part with its projected content', async () => {
163
+ const { getByText, trigger } = await renderDrawer();
164
+ expect(getByText('Menu')).toBeTruthy();
165
+ expect(getByText('Home')).toBeTruthy();
166
+ expect(getByText('Settings')).toBeTruthy();
167
+ expect(getByText('Version 1')).toBeTruthy();
168
+ expect(trigger().querySelector('svg')).toBeTruthy();
63
169
  });
64
170
 
65
- it('close() sets open to false', async () => {
66
- const { componentRef } = await render(UiNavDrawer);
67
- const inst = componentRef.instance as UiNavDrawer;
68
- inst.open.set(true);
69
- inst.close();
70
- expect(inst.open()).toBe(false);
171
+ it("merges each part's class with its own, the given class winning", async () => {
172
+ const { container, panel, scrollView, item, trigger } =
173
+ await renderDrawer();
174
+ expect(panel().classList).toContain('bg-background');
175
+ expect(panel().classList).toContain('shadow-lg');
176
+ expect(scrollView().classList).toContain('h-full');
177
+ expect(scrollView().classList).toContain('pt-2');
178
+ expect(item('Delete account').classList).toContain('mt-4');
179
+ expect(trigger().classList).toContain('h-10');
180
+ expect(trigger().classList).toContain('ml-2');
181
+ // Each part's own view, not its host element, which keeps the bare class.
182
+ const header = container.querySelector('.border-b');
183
+ expect(header?.classList).toContain('gap-3');
184
+ expect(header?.classList).not.toContain('gap-1.5');
185
+ const footer = container.querySelector('.border-t');
186
+ expect(footer?.classList).toContain('gap-4');
187
+ expect(footer?.classList).not.toContain('gap-2');
71
188
  });
72
189
 
73
- it('panel tap is a no-op', async () => {
74
- const { componentRef } = await render(UiNavDrawer);
75
- const inst = componentRef.instance as UiNavDrawer;
76
- expect(() =>
77
- (inst as unknown as { onPanelTap: () => void }).onPanelTap(),
78
- ).not.toThrow();
190
+ it('stays hidden until it is opened', async () => {
191
+ const { host, overlay, panel } = await renderDrawer();
192
+ await after(CLOSED);
193
+ expect(overlay().getAttribute('style')).toContain('display: none');
194
+ expect(panel().getAttribute('accessibility-exclusive-focus')).toBe('false');
195
+ expect(animate).not.toHaveBeenCalled();
196
+ // Starting closed isn't closing: `closed` waits for a real close.
197
+ expect(host.closes()).toBe(0);
79
198
  });
80
199
 
81
- it('open effect runs the animate-in and animate-out lifecycles (left side)', async () => {
82
- const { componentRef, container } = await render(UiNavDrawer);
83
- const inst = componentRef.instance as UiNavDrawer;
84
- const view = container.querySelector('view') as Element;
85
- patchRefs(inst, view);
86
- inst.open.set(true);
87
- await waitForUpdate();
88
- await waitForUpdate();
89
- await waitForUpdate();
200
+ it('opens when the trigger is tapped', async () => {
201
+ const { host, trigger, overlay } = await renderDrawer();
202
+ fireEvent.tap(trigger());
203
+ await after(OPENED);
204
+ expect(host.open()).toBe(true);
205
+ expect(overlay().getAttribute('style')).not.toContain('display: none');
206
+ });
90
207
 
91
- inst.open.set(false);
92
- await new Promise((r) => setTimeout(r, 320));
93
- await waitForUpdate();
94
- expect(inst.open()).toBe(false);
208
+ it('slides in from the left and back out', async () => {
209
+ const { host, overlay, backdrop, panel } = await renderDrawer();
210
+ expect(panel().getAttribute('style')).toContain('left: 0');
211
+ expect(panel().classList).toContain('border-r');
212
+
213
+ host.open.set(true);
214
+ await after(OPENED);
215
+ expect(animate).toHaveBeenCalledWith(backdrop(), [
216
+ START,
217
+ expect.any(String),
218
+ [{ opacity: 0 }, { opacity: 1 }],
219
+ expect.objectContaining({ duration: DURATION.normal }),
220
+ ]);
221
+ expect(animate).toHaveBeenCalledWith(panel(), [
222
+ START,
223
+ expect.any(String),
224
+ [
225
+ { transform: 'translateX(-100%)', opacity: 0.8 },
226
+ { transform: 'translateX(0%)', opacity: 1 },
227
+ ],
228
+ expect.objectContaining({ duration: DURATION.slow }),
229
+ ]);
230
+
231
+ host.open.set(false);
232
+ await after(CLOSED);
233
+ expect(animate).toHaveBeenCalledWith(backdrop(), [
234
+ START,
235
+ expect.any(String),
236
+ [{ opacity: 1 }, { opacity: 0 }],
237
+ expect.any(Object),
238
+ ]);
239
+ expect(animate).toHaveBeenCalledWith(panel(), [
240
+ START,
241
+ expect.any(String),
242
+ [
243
+ { transform: 'translateX(0%)', opacity: 1 },
244
+ { transform: 'translateX(-100%)', opacity: 0.8 },
245
+ ],
246
+ expect.any(Object),
247
+ ]);
248
+ expect(overlay().getAttribute('style')).toContain('display: none');
249
+ expect(host.closes()).toBe(1);
95
250
  });
96
251
 
97
- it('open effect runs correctly for the right side', async () => {
98
- const { componentRef, container } = await render(UiNavDrawer);
99
- const inst = componentRef.instance as UiNavDrawer;
100
- setInputSignal(inst.side, 'right');
101
- await waitForUpdate();
102
- const view = container.querySelector('view') as Element;
103
- patchRefs(inst, view);
104
- inst.open.set(true);
105
- await waitForUpdate();
252
+ it('slides in from the right and back out', async () => {
253
+ const { host, panel } = await renderDrawer();
254
+ host.side.set('right');
106
255
  await waitForUpdate();
107
- await waitForUpdate();
108
- inst.open.set(false);
109
- await new Promise((r) => setTimeout(r, 320));
110
- expect(inst.open()).toBe(false);
111
- expect(
112
- Array.from(container.querySelectorAll('view')).some((el) =>
113
- el.getAttribute('class')?.includes('border-l'),
114
- ),
115
- ).toBe(true);
256
+ expect(panel().getAttribute('style')).toContain('right: 0');
257
+ expect(panel().classList).toContain('border-l');
258
+
259
+ host.open.set(true);
260
+ await after(OPENED);
261
+ expect(animate).toHaveBeenCalledWith(panel(), [
262
+ START,
263
+ expect.any(String),
264
+ [
265
+ { transform: 'translateX(100%)', opacity: 0.8 },
266
+ { transform: 'translateX(0%)', opacity: 1 },
267
+ ],
268
+ expect.any(Object),
269
+ ]);
270
+
271
+ host.open.set(false);
272
+ await after(CLOSED);
273
+ expect(animate).toHaveBeenCalledWith(panel(), [
274
+ START,
275
+ expect.any(String),
276
+ [
277
+ { transform: 'translateX(0%)', opacity: 1 },
278
+ { transform: 'translateX(100%)', opacity: 0.8 },
279
+ ],
280
+ expect.any(Object),
281
+ ]);
116
282
  });
117
283
 
118
- it('open effect bails when backdrop/panel refs are undefined', async () => {
119
- const { componentRef } = await render(UiNavDrawer);
120
- const inst = componentRef.instance as UiNavDrawer;
121
- inst.open.set(true);
122
- await waitForUpdate();
123
- await waitForUpdate();
124
- inst.open.set(false);
284
+ it('cancels the previous animations when it opens again', async () => {
285
+ const { host, backdrop, panel } = await renderDrawer();
286
+ host.open.set(true);
287
+ await after(OPENED);
288
+ host.open.set(false);
289
+ await after(CLOSED);
290
+ animate.mockClear();
291
+
292
+ host.open.set(true);
293
+ await after(OPENED);
294
+ // The close animations hold their last frame (fill: forwards) until
295
+ // they're canceled, which would hide the panel being opened.
296
+ expect(animate).toHaveBeenCalledWith(backdrop(), [
297
+ CANCEL,
298
+ expect.any(String),
299
+ ]);
300
+ expect(animate).toHaveBeenCalledWith(panel(), [CANCEL, expect.any(String)]);
301
+ });
302
+
303
+ it('moves a screen reader into the panel and keeps it there while open', async () => {
304
+ const { host, panel } = await renderDrawer();
305
+ const invoke = vi.spyOn(host.drawer().panelRef().nativeElement, 'invoke');
306
+ host.open.set(true);
307
+ await after(OPENED);
308
+ expect(invoke).toHaveBeenCalledWith('requestAccessibilityFocus');
309
+ expect(panel().getAttribute('accessibility-exclusive-focus')).toBe('true');
310
+ });
311
+
312
+ it('closes when the backdrop is tapped', async () => {
313
+ const { host, backdrop } = await renderDrawer();
314
+ host.open.set(true);
315
+ await after(OPENED);
316
+ fireEvent.tap(backdrop());
317
+ expect(host.open()).toBe(false);
318
+ await after(CLOSED);
319
+ expect(host.closes()).toBe(1);
320
+ });
321
+
322
+ it('stays open when the panel is tapped', async () => {
323
+ const { host, panel } = await renderDrawer();
324
+ host.open.set(true);
325
+ await after(OPENED);
326
+ // The panel catches its taps, so they don't reach the backdrop.
327
+ fireEvent.tap(panel(), { eventType: 'catchEvent' });
125
328
  await waitForUpdate();
126
- expect(inst.open()).toBe(false);
329
+ expect(host.open()).toBe(true);
127
330
  });
128
- });
129
331
 
130
- describe('UiNavDrawerHeader / Footer', () => {
131
- it('render projected content', async () => {
132
- const header = await render(UiNavDrawerHeader);
133
- const footer = await render(UiNavDrawerFooter);
134
- expect(header.container.querySelector('view')).toBeTruthy();
135
- expect(footer.container.querySelector('view')).toBeTruthy();
332
+ it('stops its close sequence when it is destroyed while closing', async () => {
333
+ const warn = vi.spyOn(console, 'warn');
334
+ const { host } = await renderDrawer();
335
+ host.open.set(true);
336
+ await after(OPENED);
337
+ host.open.set(false);
338
+ await after(0);
339
+ // Destroyed during the close animation. Its close timer
340
+ // (DURATION.normal + 20) used to fire anyway and emit `closed` from the
341
+ // destroyed output, which Angular warns about in dev builds.
342
+ host.shown.set(false);
343
+ await waitForUpdate();
344
+ await after(CLOSED);
345
+ const warnings = warn.mock.calls.map(([message]) => String(message));
346
+ warn.mockRestore();
347
+ expect(warnings.filter((message) => message.includes('NG0953'))).toEqual(
348
+ [],
349
+ );
350
+ expect(host.closes()).toBe(0);
136
351
  });
137
352
  });
138
353
 
139
354
  describe('UiNavDrawerContent', () => {
140
- it('renders the scroll-view', async () => {
141
- const parentResult = await render(UiNavDrawer);
142
- const parent = parentResult.componentRef.instance as UiNavDrawer;
143
- const contentResult = await render(UiNavDrawerContent, {
144
- providers: [{ provide: UiNavDrawer, useValue: parent }],
145
- });
146
- expect(contentResult.container.querySelector('scroll-view')).toBeTruthy();
355
+ it('scrolls the active item into view when the drawer opens', async () => {
356
+ const { host } = await renderDrawer();
357
+ const scroll = vi.spyOn(
358
+ host.settings().containerRef()!.nativeElement,
359
+ 'invoke',
360
+ );
361
+ host.open.set(true);
362
+ await after(OPENED);
363
+ // Instantly and centered, so the drawer slides in already scrolled.
364
+ expect(scroll).toHaveBeenCalledWith(
365
+ 'scrollIntoView',
366
+ expect.objectContaining({ block: 'center', behavior: 'auto' }),
367
+ );
147
368
  });
148
369
 
149
- it('onContentLayout scrolls active item into view when drawer is open', async () => {
150
- const parentResult = await render(UiNavDrawer);
151
- const parent = parentResult.componentRef.instance as UiNavDrawer;
152
- parent.open.set(true);
153
- await waitForUpdate();
370
+ it('scrolls the active item into view once its content is laid out', async () => {
371
+ const { host, scrollView } = await renderDrawer();
372
+ host.open.set(true);
373
+ await after(OPENED);
374
+ const scroll = vi.spyOn(
375
+ host.settings().containerRef()!.nativeElement,
376
+ 'invoke',
377
+ );
378
+ fireEvent.contentsizechanged(scrollView());
379
+ expect(scroll).toHaveBeenCalledWith('scrollIntoView', expect.any(Object));
380
+ });
154
381
 
155
- const contentResult = await render(UiNavDrawerContent, {
156
- providers: [{ provide: UiNavDrawer, useValue: parent }],
157
- });
158
- const content = contentResult.componentRef.instance as UiNavDrawerContent;
159
- (content as unknown as { onContentLayout: () => void }).onContentLayout();
160
- expect(parent.open()).toBe(true);
382
+ it("doesn't scroll while the drawer is closed", async () => {
383
+ const { host, scrollView } = await renderDrawer();
384
+ const scroll = vi.spyOn(
385
+ host.settings().containerRef()!.nativeElement,
386
+ 'invoke',
387
+ );
388
+ // Content collapsing as the overlay hides relays out too.
389
+ fireEvent.contentsizechanged(scrollView());
390
+ await after(OPENED);
391
+ expect(scroll).not.toHaveBeenCalled();
161
392
  });
162
393
 
163
- it('onContentLayout is a no-op when drawer is closed', async () => {
164
- const parentResult = await render(UiNavDrawer);
165
- const parent = parentResult.componentRef.instance as UiNavDrawer;
166
- const contentResult = await render(UiNavDrawerContent, {
167
- providers: [{ provide: UiNavDrawer, useValue: parent }],
168
- });
169
- const content = contentResult.componentRef.instance as UiNavDrawerContent;
170
- expect(() =>
171
- (content as unknown as { onContentLayout: () => void }).onContentLayout(),
172
- ).not.toThrow();
394
+ it("doesn't scroll when no item is active", async () => {
395
+ const { host } = await renderDrawer();
396
+ host.active.set('');
397
+ const scroll = vi.spyOn(
398
+ host.settings().containerRef()!.nativeElement,
399
+ 'invoke',
400
+ );
401
+ host.open.set(true);
402
+ await after(OPENED);
403
+ expect(scroll).not.toHaveBeenCalled();
173
404
  });
174
405
  });
175
406
 
176
407
  describe('UiNavDrawerItem', () => {
177
- const renderItem = async (autoClose = true) => {
178
- const parentResult = await render(UiNavDrawer);
179
- const parent = parentResult.componentRef.instance as UiNavDrawer;
180
- parent.open.set(true);
181
- await waitForUpdate();
408
+ it('marks the active item with the accent background', async () => {
409
+ const { host, item } = await renderDrawer();
410
+ expect(item('Settings').classList).toContain('bg-accent');
411
+ expect(item('Home').classList).toContain('bg-transparent');
182
412
 
183
- const itemResult = await render(UiNavDrawerItem, {
184
- providers: [{ provide: UiNavDrawer, useValue: parent }],
185
- });
186
- const item = itemResult.componentRef.instance as UiNavDrawerItem;
187
- setInputSignal(item.autoClose, autoClose);
413
+ host.active.set('home');
188
414
  await waitForUpdate();
189
- return { parent, item, itemResult };
190
- };
191
-
192
- it('onTap emits pressed and auto-closes the drawer by default', async () => {
193
- const { parent, item } = await renderItem();
194
- const emitted = vi.fn();
195
- item.pressed.subscribe(emitted);
196
- (item as unknown as { onTap: () => void }).onTap();
197
- expect(emitted).toHaveBeenCalledTimes(1);
198
- expect(parent.open()).toBe(false);
415
+ expect(item('Home').classList).toContain('bg-accent');
416
+ expect(item('Settings').classList).toContain('bg-transparent');
199
417
  });
200
418
 
201
- it('onTap does not auto-close when autoClose is false', async () => {
202
- const { parent, item } = await renderItem(false);
203
- (item as unknown as { onTap: () => void }).onTap();
204
- expect(parent.open()).toBe(true);
419
+ it('emits pressed and closes the drawer when tapped', async () => {
420
+ const { host, item } = await renderDrawer();
421
+ host.open.set(true);
422
+ await waitForUpdate();
423
+ fireEvent.tap(item('Home'));
424
+ expect(host.presses()).toBe(1);
425
+ expect(host.open()).toBe(false);
205
426
  });
206
427
 
207
- it('press handlers run cleanly', async () => {
208
- const { item } = await renderItem();
209
- const inst = item as unknown as {
210
- onPressStart: () => void;
211
- onPressEnd: () => void;
212
- onPressCancel: () => void;
213
- };
214
- expect(() => inst.onPressStart()).not.toThrow();
215
- expect(() => inst.onPressEnd()).not.toThrow();
216
- expect(() => inst.onPressCancel()).not.toThrow();
428
+ it('leaves the drawer open when tapped without autoClose', async () => {
429
+ const { host, item } = await renderDrawer();
430
+ host.open.set(true);
431
+ await waitForUpdate();
432
+ fireEvent.tap(item('Delete account'));
433
+ expect(host.open()).toBe(true);
217
434
  });
218
435
 
219
- it('active item carries the accent background', async () => {
220
- const { item, itemResult } = await renderItem();
221
- setInputSignal(item.active, true);
222
- await waitForUpdate();
223
- const view = itemResult.container.querySelector('view');
224
- expect(view?.getAttribute('class')).toContain('bg-accent');
436
+ it('scales down while pressed, and back on release', async () => {
437
+ const { item } = await renderDrawer();
438
+ const view = item('Home');
439
+
440
+ fireEvent.touchstart(view);
441
+ expect(scaleOf(view)).toBe('scale(0.985)');
442
+ fireEvent.touchend(view);
443
+ expect(scaleOf(view)).toBe('scale(1)');
444
+
445
+ // A scroll that takes the touch over cancels it.
446
+ fireEvent.touchstart(view);
447
+ fireEvent.touchcancel(view);
448
+ expect(scaleOf(view)).toBe('scale(1)');
449
+
450
+ // The mouse, on the web.
451
+ fireEvent.mousedown(view);
452
+ expect(scaleOf(view)).toBe('scale(0.985)');
453
+ fireEvent.mouseup(view);
454
+ expect(scaleOf(view)).toBe('scale(1)');
455
+ fireEvent.mousedown(view);
456
+ fireLynxEvent(view, 'mouseleave');
457
+ expect(scaleOf(view)).toBe('scale(1)');
225
458
  });
226
459
 
227
- it('scrollIntoView calls invoke() on the container ref when populated', async () => {
228
- const { item, itemResult } = await renderItem();
229
- const invoke = vi.fn();
230
- (item as unknown as { containerRef: unknown }).containerRef = signal({
231
- nativeElement: { invoke },
232
- });
233
- item.scrollIntoView();
234
- expect(invoke).toHaveBeenCalledWith('scrollIntoView', expect.any(Object));
235
-
236
- // No-container path: containerRef is undefined so `?.` short-circuits.
237
- (item as unknown as { containerRef: () => undefined }).containerRef = () =>
238
- undefined;
239
- expect(() => item.scrollIntoView()).not.toThrow();
240
- void itemResult;
460
+ it('turns its label and icons destructive', async () => {
461
+ const { host, item, svg } = await renderDrawer();
462
+ const icon = () => item('Delete account').querySelector('svg')!;
463
+ // Labels styled text-foreground follow the redirected variable.
464
+ expect(item('Delete account').classList).toContain(
465
+ '[--foreground:var(--destructive)]',
466
+ );
467
+ expect(icon().getAttribute('content')).toBe(
468
+ svg('.destructive-trash').getAttribute('content'),
469
+ );
470
+
471
+ host.deleteVariant.set('default');
472
+ await waitForUpdate();
473
+ expect(item('Delete account').classList).not.toContain(
474
+ '[--foreground:var(--destructive)]',
475
+ );
476
+ expect(icon().getAttribute('content')).toBe(
477
+ svg('.plain-trash').getAttribute('content'),
478
+ );
241
479
  });
242
480
  });
243
481
 
244
482
  describe('UiNavDrawerTrigger', () => {
245
- it('onTap emits pressed', async () => {
246
- const { componentRef } = await render(UiNavDrawerTrigger);
247
- const inst = componentRef.instance as unknown as UiNavDrawerTrigger & {
248
- onTap: () => void;
249
- };
250
- const emitted = vi.fn();
251
- inst.pressed.subscribe(emitted);
252
- inst.onTap();
253
- expect(emitted).toHaveBeenCalledTimes(1);
483
+ it('is a labeled button for screen readers', async () => {
484
+ const { trigger, getByRole } = await renderDrawer();
485
+ expect(getByRole('button', { name: 'Open navigation menu' })).toBe(
486
+ trigger(),
487
+ );
254
488
  });
255
489
 
256
- it('press handlers run cleanly', async () => {
257
- const { componentRef } = await render(UiNavDrawerTrigger);
258
- const inst = componentRef.instance as unknown as {
259
- onPressStart: () => void;
260
- onPressEnd: () => void;
261
- onPressCancel: () => void;
262
- };
263
- expect(() => inst.onPressStart()).not.toThrow();
264
- expect(() => inst.onPressEnd()).not.toThrow();
265
- expect(() => inst.onPressCancel()).not.toThrow();
490
+ it('scales down while pressed, and back on release', async () => {
491
+ const { trigger } = await renderDrawer();
492
+ const view = trigger();
493
+
494
+ fireEvent.touchstart(view);
495
+ expect(scaleOf(view)).toBe('scale(0.97)');
496
+ fireEvent.touchend(view);
497
+ expect(scaleOf(view)).toBe('scale(1)');
498
+
499
+ fireEvent.touchstart(view);
500
+ fireEvent.touchcancel(view);
501
+ expect(scaleOf(view)).toBe('scale(1)');
502
+
503
+ fireEvent.mousedown(view);
504
+ expect(scaleOf(view)).toBe('scale(0.97)');
505
+ fireEvent.mouseup(view);
506
+ expect(scaleOf(view)).toBe('scale(1)');
507
+ fireEvent.mousedown(view);
508
+ fireLynxEvent(view, 'mouseleave');
509
+ expect(scaleOf(view)).toBe('scale(1)');
266
510
  });
267
511
  });