@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,40 +1,111 @@
1
- import { Component } from '@angular/core';
2
- import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
1
+ import { Component, signal } 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 } from 'vitest';
5
- import { setInputSignal } from '../../../test-utils/set-input-signal';
8
+ import { describe, expect, it, vi } from 'vitest';
9
+ import { lynxMainThread } from '../../../test-utils/lynx-main-thread';
6
10
  import { UiScrollArea } from './scroll-area';
7
11
 
8
12
  @Component({
9
- standalone: true,
10
13
  imports: [UiScrollArea, LYNX_ELEMENTS],
11
- template: `<ui-scroll-area><text>Body</text></ui-scroll-area>`,
14
+ template: `
15
+ <ui-scroll-area
16
+ [orientation]="orientation()"
17
+ [height]="height()"
18
+ [bounces]="bounces()"
19
+ [scrollBar]="scrollBar()"
20
+ [nestedScroll]="nestedScroll()"
21
+ class="bg-card"
22
+ contentClass="gap-2"
23
+ (scrolledToStart)="reached.push('start')"
24
+ (scrolledToEnd)="reached.push('end')"
25
+ >
26
+ <text>Body</text>
27
+ </ui-scroll-area>
28
+ `,
12
29
  })
13
- class ScrollAreaHost {}
30
+ class ScrollAreaHost {
31
+ readonly orientation = signal<'vertical' | 'horizontal'>('vertical');
32
+ readonly height = signal('');
33
+ readonly bounces = signal(true);
34
+ readonly scrollBar = signal(true);
35
+ readonly nestedScroll = signal(false);
36
+ readonly reached: string[] = [];
37
+ }
38
+
39
+ const renderScrollArea = async () => {
40
+ const result = await render(ScrollAreaHost);
41
+ return {
42
+ ...result,
43
+ host: result.componentRef.instance as ScrollAreaHost,
44
+ scrollView: () => result.container.querySelector('scroll-view')!,
45
+ /** The view inside the scroll view that lays out the content. */
46
+ content: () => result.getByText('Body').parentElement!,
47
+ };
48
+ };
49
+
50
+ /**
51
+ * The values each boolean reached Lynx with, through `__SetAttribute`.
52
+ */
53
+ const watchAttributes = () => {
54
+ const setAttribute = vi.spyOn(lynxMainThread(), '__SetAttribute');
55
+ return {
56
+ last: (name: string): unknown =>
57
+ setAttribute.mock.calls.filter((call) => call[1] === name).at(-1)?.[2],
58
+ restore: () => setAttribute.mockRestore(),
59
+ };
60
+ };
14
61
 
15
62
  describe('UiScrollArea', () => {
16
- it('renders projected content in the scroll view', async () => {
17
- const { getByText } = await render(ScrollAreaHost);
18
- expect(getByText('Body')).toBeTruthy();
63
+ it('renders projected content in a vertical scroll view', async () => {
64
+ const { scrollView, content } = await renderScrollArea();
65
+ expect(scrollView().getAttribute('scroll-orientation')).toBe('vertical');
66
+ expect(scrollView().classList).toContain('bg-card');
67
+ expect(content().classList).toContain('flex-col');
68
+ expect(content().classList).toContain('gap-2');
19
69
  });
20
70
 
21
- it('emits an explicit height when the height input is set', async () => {
22
- const { componentRef, container } = await render(UiScrollArea);
23
- setInputSignal((componentRef.instance as UiScrollArea).height, '250px');
71
+ it('gives the scroll view an explicit height, which it needs to scroll', async () => {
72
+ const { host, scrollView } = await renderScrollArea();
73
+ expect(scrollView().getAttribute('style') ?? '').not.toContain('height');
74
+
75
+ host.height.set('250px');
24
76
  await waitForUpdate();
25
- const scroll = container.querySelector('scroll-view');
26
- expect(scroll?.getAttribute('style')).toContain('height: 250px');
77
+ expect(scrollView().getAttribute('style')).toContain('height: 250px');
27
78
  });
28
79
 
29
- it('flips the content layout for horizontal orientation', async () => {
30
- const { componentRef, container } = await render(UiScrollArea);
31
- setInputSignal(
32
- (componentRef.instance as UiScrollArea).orientation,
33
- 'horizontal',
34
- );
80
+ it('lays the content out in a row when horizontal', async () => {
81
+ const { host, scrollView, content } = await renderScrollArea();
82
+ host.orientation.set('horizontal');
35
83
  await waitForUpdate();
36
- // The inner content wrapper picks up flex-row for the horizontal branch.
37
- const rowEl = container.querySelector('.flex-row');
38
- expect(rowEl).toBeTruthy();
84
+ expect(scrollView().getAttribute('scroll-orientation')).toBe('horizontal');
85
+ expect(content().classList).toContain('flex-row');
86
+ });
87
+
88
+ it('hands Lynx real booleans, so the defaults that are on can be turned off', async () => {
89
+ // A false used to remove the attribute, which leaves Lynx's default: on.
90
+ const attributes = watchAttributes();
91
+ const { host } = await renderScrollArea();
92
+ expect(attributes.last('bounces')).toBe(true);
93
+ expect(attributes.last('scroll-bar-enable')).toBe(true);
94
+
95
+ host.bounces.set(false);
96
+ host.scrollBar.set(false);
97
+ host.nestedScroll.set(true);
98
+ await waitForUpdate();
99
+ expect(attributes.last('bounces')).toBe(false);
100
+ expect(attributes.last('scroll-bar-enable')).toBe(false);
101
+ expect(attributes.last('enable-nested-scroll')).toBe(true);
102
+ attributes.restore();
103
+ });
104
+
105
+ it('emits when scrolled to the start or the end', async () => {
106
+ const { host, scrollView } = await renderScrollArea();
107
+ fireEvent.scrolltolower(scrollView());
108
+ fireEvent.scrolltoupper(scrollView());
109
+ expect(host.reached).toEqual(['end', 'start']);
39
110
  });
40
111
  });
@@ -11,19 +11,19 @@ import { cn } from '../../utils/cn';
11
11
 
12
12
  @Component({
13
13
  selector: 'ui-scroll-area',
14
- standalone: true,
15
14
  imports: [LYNX_ELEMENTS],
16
15
  encapsulation: ViewEncapsulation.None,
17
- // `|| undefined` on boolean attributes removes the attribute entirely when
18
- // false. Passing the string "false" (what Angular produces for a false
19
- // binding without `|| undefined`) is still treated as truthy by Lynx's
20
- // native attribute parser, so absence is the only way to truly disable.
16
+ // The booleans are property bindings to <scroll-view>'s inputs, which hand
17
+ // Lynx a real `false`. They used to be `[attr.x]="x() || undefined"`, which
18
+ // removed the attribute when false, and a missing attribute means Lynx's
19
+ // default: on, for `bounces` and `scroll-bar-enable`. So neither could be
20
+ // turned off. (`[attr.x]` would send the string "false" instead.)
21
21
  template: `
22
22
  <scroll-view
23
23
  [attr.scroll-orientation]="orientation()"
24
- [attr.bounces]="bounces() || undefined"
25
- [attr.scroll-bar-enable]="scrollBar() || undefined"
26
- [attr.enable-nested-scroll]="nestedScroll() || undefined"
24
+ [bounces]="bounces()"
25
+ [scroll-bar-enable]="scrollBar()"
26
+ [enable-nested-scroll]="nestedScroll()"
27
27
  [attr.upper-threshold]="upperThreshold()"
28
28
  [attr.lower-threshold]="lowerThreshold()"
29
29
  [class]="scrollClass()"
@@ -1,107 +1,167 @@
1
- import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
2
- import { describe, expect, it } from 'vitest';
3
- import { setInputSignal } from '../../../test-utils/set-input-signal';
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 } from '@blotch/angular-lynx';
9
+ import { describe, expect, it, vi } from 'vitest';
4
10
  import { UiSelect, UiSelectItem } from './select';
5
11
 
12
+ @Component({
13
+ imports: [UiSelect, UiSelectItem, LYNX_ELEMENTS],
14
+ template: `
15
+ <ui-select
16
+ #select
17
+ [(value)]="value"
18
+ [placeholder]="placeholder()"
19
+ [disabled]="disabled()"
20
+ class="w-48"
21
+ (changed)="changed($event)"
22
+ >
23
+ <ui-select-item value="apple" label="Apple" />
24
+ <ui-select-item value="pear" label="Pear" class="font-bold" />
25
+ </ui-select>
26
+ <text class="open-from-code" (bindtap)="select.open()">Choose</text>
27
+ `,
28
+ })
29
+ class SelectHost {
30
+ readonly value = signal('');
31
+ readonly placeholder = signal('Pick a fruit');
32
+ readonly disabled = signal(false);
33
+ readonly changed = vi.fn((_value: string) => {});
34
+ }
35
+
36
+ const renderSelect = async () => {
37
+ const result = await render(SelectHost);
38
+ const { container } = result;
39
+ const overlay = container.querySelector<HTMLElement>('overlay')!;
40
+ return {
41
+ ...result,
42
+ host: result.componentRef.instance as SelectHost,
43
+ overlay,
44
+ trigger: container.querySelector('.border-input')!,
45
+ triggerText: () => container.querySelector('.border-input text')!,
46
+ // An item's tappable row, found by its label in the sheet.
47
+ item: (label: string) => within(overlay).getByText(label).parentElement!,
48
+ };
49
+ };
50
+
51
+ /**
52
+ * Waits out the sheet's open sequence: the overlay shows on one tick, and the
53
+ * panel slides in on the next.
54
+ */
55
+ const waitForOpen = async () => {
56
+ for (let i = 0; i < 3; i++) await waitForUpdate();
57
+ };
58
+
59
+ /**
60
+ * Waits for the sheet's close sequence to finish: the panel slides out, then
61
+ * the overlay hides. Polled rather than timed, since the sequence only starts
62
+ * on the update after the sheet is told to close.
63
+ */
64
+ const waitForClose = (overlay: Element) =>
65
+ vi.waitFor(() => expect(overlay.getAttribute('visible')).toBe('false'));
66
+
6
67
  describe('UiSelect', () => {
7
- it('renders the trigger for the current selection', async () => {
8
- const { container } = await render(UiSelect);
9
- expect(container).toBeTruthy();
68
+ it('shows the placeholder, muted, until a value is chosen', async () => {
69
+ const { triggerText, trigger } = await renderSelect();
70
+ expect(triggerText().textContent).toBe('Pick a fruit');
71
+ expect(triggerText().classList).toContain('text-muted-foreground');
72
+ expect(trigger.classList).toContain('w-48');
10
73
  });
11
74
 
12
- it('displays the placeholder when no value is selected', async () => {
13
- const { componentRef, container } = await render(UiSelect);
14
- const inst = componentRef.instance as UiSelect;
15
- setInputSignal(inst.placeholder, 'Choose...');
75
+ it("shows the chosen item's label", async () => {
76
+ const { host, triggerText } = await renderSelect();
77
+ host.value.set('pear');
16
78
  await waitForUpdate();
17
- expect(container.textContent).toContain('Choose...');
79
+ expect(triggerText().textContent).toBe('Pear');
80
+ expect(triggerText().classList).toContain('text-foreground');
18
81
  });
19
82
 
20
- it('displays the raw value when no matching item is projected', async () => {
21
- // Covers the `item?.label() ?? val` fallback path.
22
- const { componentRef, container } = await render(UiSelect);
23
- const inst = componentRef.instance as UiSelect;
24
- inst.value.set('unknown');
83
+ it('shows a value that no item has as it is', async () => {
84
+ const { host, triggerText } = await renderSelect();
85
+ host.value.set('kiwi');
25
86
  await waitForUpdate();
26
- expect(container.textContent).toContain('unknown');
87
+ expect(triggerText().textContent).toBe('kiwi');
27
88
  });
28
89
 
29
- it('select() updates the model, emits `changed`, and closes the sheet', async () => {
30
- const { componentRef } = await render(UiSelect);
31
- const inst = componentRef.instance as UiSelect;
32
- inst.open();
33
- const emitted: string[] = [];
34
- inst.changed.subscribe((v) => emitted.push(v));
35
- inst.select('b');
36
- expect(inst.value()).toBe('b');
37
- expect(emitted).toEqual(['b']);
90
+ it('opens its list when the trigger is tapped, and closes it from the sheet', async () => {
91
+ const { overlay, trigger } = await renderSelect();
92
+ expect(overlay.getAttribute('visible')).toBe('false');
93
+
94
+ fireEvent.tap(trigger);
95
+ await waitForOpen();
96
+ expect(overlay.getAttribute('visible')).toBe('true');
97
+
98
+ // Tapping the backdrop closes the sheet, which the select keeps track of:
99
+ // the next tap on the trigger opens it again.
100
+ fireEvent.tap(overlay.querySelector('view')!);
101
+ await waitForClose(overlay);
102
+ expect(overlay.getAttribute('visible')).toBe('false');
103
+ fireEvent.tap(trigger);
104
+ await waitForOpen();
105
+ expect(overlay.getAttribute('visible')).toBe('true');
38
106
  });
39
107
 
40
- it('toggle() flips the sheet open/closed', async () => {
41
- const { componentRef } = await render(UiSelect);
42
- const inst = componentRef.instance as UiSelect;
43
- inst.toggle();
44
- inst.toggle();
45
- // No throw; the internal `sheetOpen` signal is protected — verify indirectly
46
- // by ensuring subsequent open()/close() calls still work.
47
- inst.open();
48
- inst.close();
49
- expect(inst.value()).toBe('');
108
+ it('closes its list when the trigger is tapped again', async () => {
109
+ const { overlay, trigger } = await renderSelect();
110
+ fireEvent.tap(trigger);
111
+ await waitForOpen();
112
+ fireEvent.tap(trigger);
113
+ await waitForClose(overlay);
114
+ expect(overlay.getAttribute('visible')).toBe('false');
50
115
  });
51
116
 
52
- it('toggle() / open() are no-ops when disabled', async () => {
53
- const { componentRef } = await render(UiSelect);
54
- const inst = componentRef.instance as UiSelect;
55
- setInputSignal(inst.disabled, true);
56
- await waitForUpdate();
57
- // No throw and no exception paths — early-return branch is exercised.
58
- inst.toggle();
59
- inst.open();
60
- inst.close();
61
- expect(inst.value()).toBe('');
117
+ it('opens from code with open()', async () => {
118
+ const { overlay, getByText } = await renderSelect();
119
+ fireEvent.tap(getByText('Choose'));
120
+ await waitForOpen();
121
+ expect(overlay.getAttribute('visible')).toBe('true');
62
122
  });
63
123
 
64
- it('trigger dims when the select is disabled', async () => {
65
- const { componentRef, container } = await render(UiSelect);
66
- const inst = componentRef.instance as UiSelect;
67
- setInputSignal(inst.disabled, true);
124
+ it("dims and doesn't open while disabled", async () => {
125
+ const { host, overlay, trigger, getByText } = await renderSelect();
126
+ host.disabled.set(true);
68
127
  await waitForUpdate();
69
- const trigger = container.querySelector('view');
70
- expect(trigger?.getAttribute('class')).toContain('opacity-50');
128
+ expect(trigger.classList).toContain('opacity-50');
129
+
130
+ fireEvent.tap(trigger);
131
+ fireEvent.tap(getByText('Choose'));
132
+ await waitForOpen();
133
+ expect(overlay.getAttribute('visible')).toBe('false');
71
134
  });
72
135
  });
73
136
 
74
137
  describe('UiSelectItem', () => {
75
- it('onTap delegates to the parent select', async () => {
76
- const parentResult = await render(UiSelect);
77
- const parent = parentResult.componentRef.instance as UiSelect;
78
-
79
- const itemResult = await render(UiSelectItem, {
80
- providers: [{ provide: UiSelect, useValue: parent }],
81
- });
82
- const item = itemResult.componentRef.instance as UiSelectItem;
83
- setInputSignal(item.itemValue, 'x');
84
- setInputSignal(item.label, 'X');
85
- await waitForUpdate();
138
+ it('chooses its value when tapped, and closes the list', async () => {
139
+ const { host, overlay, trigger, triggerText, item } = await renderSelect();
140
+ fireEvent.tap(trigger);
141
+ await waitForOpen();
86
142
 
87
- (item as unknown as { onTap: () => void }).onTap();
88
- expect(parent.value()).toBe('x');
143
+ fireEvent.tap(item('Apple'));
144
+ expect(host.value()).toBe('apple');
145
+ expect(host.changed).toHaveBeenCalledWith('apple');
146
+ await waitForUpdate();
147
+ expect(triggerText().textContent).toBe('Apple');
148
+ await waitForClose(overlay);
149
+ expect(overlay.getAttribute('visible')).toBe('false');
89
150
  });
90
151
 
91
- it('highlights the selected item via bg-accent', async () => {
92
- const parentResult = await render(UiSelect);
93
- const parent = parentResult.componentRef.instance as UiSelect;
94
- parent.value.set('x');
95
-
96
- const itemResult = await render(UiSelectItem, {
97
- providers: [{ provide: UiSelect, useValue: parent }],
98
- });
99
- const item = itemResult.componentRef.instance as UiSelectItem;
100
- setInputSignal(item.itemValue, 'x');
101
- setInputSignal(item.label, 'X');
152
+ it('marks the chosen item with a highlight and a check', async () => {
153
+ const { host, item } = await renderSelect();
154
+ host.value.set('pear');
102
155
  await waitForUpdate();
156
+ expect(item('Pear').classList).toContain('bg-accent');
157
+ expect(item('Pear').querySelector('svg')).not.toBeNull();
158
+ expect(item('Apple').classList).not.toContain('bg-accent');
159
+ expect(item('Apple').querySelector('svg')).toBeNull();
160
+ });
103
161
 
104
- const view = itemResult.container.querySelector('view');
105
- expect(view?.getAttribute('class')).toContain('bg-accent');
162
+ it("adds the class to the item's own", async () => {
163
+ const { item } = await renderSelect();
164
+ expect(item('Pear').classList).toContain('font-bold');
165
+ expect(item('Pear').classList).toContain('px-4');
106
166
  });
107
167
  });
@@ -3,7 +3,6 @@ import {
3
3
  ViewEncapsulation,
4
4
  computed,
5
5
  contentChildren,
6
- forwardRef,
7
6
  inject,
8
7
  input,
9
8
  model,
@@ -18,17 +17,28 @@ import { cn } from '../../utils/cn';
18
17
  const CHEVRON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>`;
19
18
  const CHECK_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>`;
20
19
 
20
+ /**
21
+ * `UiSelectItem`, for `contentChildren()`. A call rather than the class
22
+ * itself: for a class, Angular's test build keeps an arrow function that
23
+ * nothing calls, which an app's coverage report would show as uncovered.
24
+ */
25
+ const selectItem = () => UiSelectItem;
26
+
21
27
  @Component({
22
28
  selector: 'ui-select',
23
29
  standalone: true,
24
30
  imports: [LYNX_ELEMENTS, UiBottomSheet],
25
31
  encapsulation: ViewEncapsulation.None,
32
+ // `sheetOpen` is bound both ways by hand rather than with `[(open)]`: for a
33
+ // two-way binding Angular also compiles a fallback that assigns the field,
34
+ // which never runs for a signal and so shows as an uncovered branch in an
35
+ // app's coverage report.
26
36
  template: `
27
37
  <view [class]="triggerClass()" (bindtap)="toggle()">
28
38
  <text [class]="valueTextClass()">{{ displayText() }}</text>
29
39
  <svg [attr.content]="chevronSvg" style="width: 16px; height: 16px;" />
30
40
  </view>
31
- <ui-bottom-sheet [(open)]="sheetOpen">
41
+ <ui-bottom-sheet [open]="sheetOpen()" (openChange)="sheetOpen.set($event)">
32
42
  <scroll-view scroll-orientation="vertical" style="max-height: 300px;">
33
43
  <view class="flex-col pb-4 flex">
34
44
  <ng-content />
@@ -47,21 +57,17 @@ export class UiSelect {
47
57
 
48
58
  protected readonly chevronSvg = CHEVRON_SVG;
49
59
 
50
- // Drives the shared bottom-sheet's `open` model via `[(open)]`. All the
51
- // overlay/animation/drag behavior lives in ui-bottom-sheet; select only
52
- // decides when the sheet is open.
60
+ // Drives the shared bottom-sheet's `open` model. All the overlay/animation/
61
+ // drag behavior lives in ui-bottom-sheet; select only decides when the sheet
62
+ // is open.
53
63
  protected readonly sheetOpen = signal(false);
54
64
 
55
- // `forwardRef` is required because UiSelectItem is defined later in this
56
- // file — without it, the reference would be `undefined` at class-definition
57
- // time and contentChildren would silently query nothing.
58
- /* v8 ignore next 1 -- forwardRef lambda; JIT+jsdom doesn't wire required child inputs */
59
- readonly itemRefs = contentChildren(forwardRef(() => UiSelectItem));
65
+ // The projected items, so the trigger can show the chosen one's label.
66
+ readonly itemRefs = contentChildren(selectItem());
60
67
 
61
68
  protected readonly displayText = computed(() => {
62
69
  const val = this.value();
63
70
  if (!val) return this.placeholder();
64
- /* v8 ignore next 1 -- .find predicate: unreachable when no content-projected items */
65
71
  const item = this.itemRefs().find((i) => i.itemValue() === val);
66
72
  return item?.label() ?? val;
67
73
  });
@@ -1,26 +1,27 @@
1
+ import { Component, signal } from '@angular/core';
1
2
  import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
2
3
  import { describe, expect, it } from 'vitest';
3
- import { setInputSignal } from '../../../test-utils/set-input-signal';
4
4
  import { UiSeparator } from './separator';
5
5
 
6
+ @Component({
7
+ imports: [UiSeparator],
8
+ template: `<ui-separator [orientation]="orientation()" class="my-2" />`,
9
+ })
10
+ class SeparatorHost {
11
+ readonly orientation = signal<'horizontal' | 'vertical'>('horizontal');
12
+ }
13
+
6
14
  describe('UiSeparator', () => {
7
- it('renders horizontally (default)', async () => {
8
- const { componentRef } = await render(UiSeparator);
9
- // The class binding lands on the host element, which is the component's
10
- // location.nativeElement (a real DOM node representing the ui-separator
11
- // host). Read the class directly from that node.
12
- const host = componentRef.location.nativeElement as HTMLElement;
13
- expect(host.getAttribute('class')).toContain('h-px');
14
- });
15
+ it('draws a horizontal line by default, and a vertical one when asked', async () => {
16
+ const { componentRef, container } = await render(SeparatorHost);
17
+ // The classes are on the host element, the separator's only element.
18
+ const line = container.querySelector('.bg-border')!;
19
+ expect(line.classList).toContain('h-px');
20
+ expect(line.classList).toContain('my-2');
15
21
 
16
- it('renders vertically when the orientation input is flipped', async () => {
17
- const { componentRef } = await render(UiSeparator);
18
- setInputSignal(
19
- (componentRef.instance as UiSeparator).orientation,
20
- 'vertical',
21
- );
22
+ (componentRef.instance as SeparatorHost).orientation.set('vertical');
22
23
  await waitForUpdate();
23
- const host = componentRef.location.nativeElement as HTMLElement;
24
- expect(host.getAttribute('class')).toContain('w-px');
24
+ expect(line.classList).toContain('w-px');
25
+ expect(line.classList).not.toContain('h-px');
25
26
  });
26
27
  });
@@ -1,7 +1,11 @@
1
- import { Component } from '@angular/core';
2
- import { render } from '@blotch/angular-lynx-testing-library';
1
+ import { Component, signal } 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 } from 'vitest';
8
+ import { describe, expect, it, vi } from 'vitest';
5
9
  import {
6
10
  UiSheet,
7
11
  UiSheetDescription,
@@ -11,7 +15,6 @@ import {
11
15
  } from './sheet';
12
16
 
13
17
  @Component({
14
- standalone: true,
15
18
  imports: [
16
19
  UiSheet,
17
20
  UiSheetHeader,
@@ -21,24 +24,74 @@ import {
21
24
  LYNX_ELEMENTS,
22
25
  ],
23
26
  template: `
24
- <ui-sheet [(open)]="open">
25
- <ui-sheet-header>
26
- <ui-sheet-title>Title</ui-sheet-title>
27
- <ui-sheet-description>Body</ui-sheet-description>
27
+ <ui-sheet [(open)]="open" class="gap-4" (closed)="closed()">
28
+ <ui-sheet-header class="items-center">
29
+ <ui-sheet-title class="text-xl">Title</ui-sheet-title>
30
+ <ui-sheet-description class="italic">Body</ui-sheet-description>
28
31
  </ui-sheet-header>
29
- <ui-sheet-footer><text>Buttons</text></ui-sheet-footer>
32
+ <ui-sheet-footer class="flex-row"><text>Buttons</text></ui-sheet-footer>
30
33
  </ui-sheet>
31
34
  `,
32
35
  })
33
36
  class SheetHost {
34
- open = true;
37
+ readonly open = signal(false);
38
+ readonly closed = vi.fn();
35
39
  }
36
40
 
41
+ const renderSheet = async () => {
42
+ const result = await render(SheetHost);
43
+ return {
44
+ ...result,
45
+ host: result.componentRef.instance as SheetHost,
46
+ overlay: result.container.querySelector('overlay')!,
47
+ backdrop: result.container.querySelector('overlay > view')!,
48
+ };
49
+ };
50
+
51
+ /**
52
+ * Opens the sheet and waits out its open sequence: the overlay shows on one
53
+ * tick, and the panel slides in on the next.
54
+ */
55
+ const openSheet = async (host: SheetHost) => {
56
+ host.open.set(true);
57
+ for (let i = 0; i < 3; i++) await waitForUpdate();
58
+ };
59
+
37
60
  describe('UiSheet', () => {
38
61
  it('renders header, title, description, and footer', async () => {
39
- const { getByText } = await render(SheetHost);
62
+ const { getByText } = await renderSheet();
40
63
  expect(getByText('Title')).toBeTruthy();
41
64
  expect(getByText('Body')).toBeTruthy();
42
65
  expect(getByText('Buttons')).toBeTruthy();
43
66
  });
67
+
68
+ it("merges each part's class with its own", async () => {
69
+ const { container, getByText } = await renderSheet();
70
+ // The sheet's class goes on the padded content wrapper, not the panel.
71
+ expect(container.querySelector('.px-6')?.classList).toContain('gap-4');
72
+ expect(container.querySelector('.mb-4')?.classList).toContain(
73
+ 'items-center',
74
+ );
75
+ expect(getByText('Title').classList).toContain('text-xl');
76
+ expect(getByText('Title').classList).toContain('font-semibold');
77
+ expect(getByText('Body').classList).toContain('italic');
78
+ expect(getByText('Body').classList).toContain('text-muted-foreground');
79
+ expect(container.querySelector('.pt-4')?.classList).toContain('flex-row');
80
+ });
81
+
82
+ it('opens with `open`, and reports a dismissal through it and `closed`', async () => {
83
+ const { host, overlay, backdrop } = await renderSheet();
84
+ expect(overlay.getAttribute('visible')).toBe('false');
85
+
86
+ await openSheet(host);
87
+ expect(overlay.getAttribute('visible')).toBe('true');
88
+
89
+ fireEvent.tap(backdrop);
90
+ expect(host.open()).toBe(false);
91
+ // Once the panel has slid out, the overlay hides and `closed` fires.
92
+ await vi.waitFor(() => expect(host.closed).toHaveBeenCalled());
93
+ await waitForUpdate();
94
+ expect(overlay.getAttribute('visible')).toBe('false');
95
+ expect(host.closed).toHaveBeenCalledOnce();
96
+ });
44
97
  });
@@ -26,8 +26,16 @@ import { cn } from '../../utils/cn';
26
26
  standalone: true,
27
27
  imports: [LYNX_ELEMENTS, UiBottomSheet],
28
28
  encapsulation: ViewEncapsulation.None,
29
+ // `open` is bound both ways by hand rather than with `[(open)]`: for a
30
+ // two-way binding Angular also compiles a fallback that assigns the field,
31
+ // which never runs for a signal and so shows as an uncovered branch in an
32
+ // app's coverage report.
29
33
  template: `
30
- <ui-bottom-sheet [(open)]="open" (closed)="closed.emit()">
34
+ <ui-bottom-sheet
35
+ [open]="open()"
36
+ (openChange)="open.set($event)"
37
+ (closed)="closed.emit()"
38
+ >
31
39
  <view [class]="contentClass()">
32
40
  <ng-content />
33
41
  </view>