@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,11 @@
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
8
  import { describe, expect, it } from 'vitest';
5
- import { setInputSignal } from '../../../test-utils/set-input-signal';
6
9
  import {
7
10
  UiCard,
8
11
  UiCardContent,
@@ -13,7 +16,6 @@ import {
13
16
  } from './card';
14
17
 
15
18
  @Component({
16
- standalone: true,
17
19
  imports: [
18
20
  UiCard,
19
21
  UiCardHeader,
@@ -24,20 +26,47 @@ import {
24
26
  LYNX_ELEMENTS,
25
27
  ],
26
28
  template: `
27
- <ui-card>
28
- <ui-card-header>
29
- <ui-card-title>Title</ui-card-title>
30
- <ui-card-description>Desc</ui-card-description>
29
+ <ui-card [pressable]="pressable()" class="w-full">
30
+ <ui-card-header class="gap-1">
31
+ <ui-card-title class="text-lg">Title</ui-card-title>
32
+ <ui-card-description class="italic">Desc</ui-card-description>
31
33
  </ui-card-header>
32
- <ui-card-content><text>Body</text></ui-card-content>
33
- <ui-card-footer><text>Actions</text></ui-card-footer>
34
+ <ui-card-content class="pt-2"><text>Body</text></ui-card-content>
35
+ <ui-card-footer class="justify-end"><text>Actions</text></ui-card-footer>
34
36
  </ui-card>
35
37
  `,
36
38
  })
37
- class CardHost {}
39
+ class CardHost {
40
+ readonly pressable = signal(false);
41
+ }
42
+
43
+ /**
44
+ * The card's own view. Each component's host element renders as a view too,
45
+ * so it's found by the card's class rather than by being the first.
46
+ */
47
+ const cardView = (container: Element) => container.querySelector('.bg-card')!;
48
+
49
+ /**
50
+ * Fires a Lynx event `fireEvent` has no shortcut for, as Lynx dispatches it:
51
+ * `mouseleave` is the mouse's `touchcancel`.
52
+ */
53
+ const fireLynxEvent = (element: Element, name: string) =>
54
+ fireEvent(
55
+ element,
56
+ Object.assign(new Event(`bindEvent:${name}`), {
57
+ eventType: 'bindEvent',
58
+ eventName: name,
59
+ }),
60
+ );
61
+
62
+ /**
63
+ * The scale the card's inline style holds, if any.
64
+ */
65
+ const scaleOf = (view: Element) =>
66
+ (view as HTMLElement).style.getPropertyValue('transform');
38
67
 
39
68
  describe('UiCard', () => {
40
- it('renders each sub-component with projected content', async () => {
69
+ it('renders each part with its projected content', async () => {
41
70
  const { getByText } = await render(CardHost);
42
71
  expect(getByText('Title')).toBeTruthy();
43
72
  expect(getByText('Desc')).toBeTruthy();
@@ -45,66 +74,57 @@ describe('UiCard', () => {
45
74
  expect(getByText('Actions')).toBeTruthy();
46
75
  });
47
76
 
48
- it('renders without a host wrapper', async () => {
49
- const { container } = await render(UiCard);
50
- expect(container).toBeTruthy();
77
+ it("merges each part's class with its own", async () => {
78
+ const { container, getByText } = await render(CardHost);
79
+ expect(cardView(container).getAttribute('class')).toContain('bg-card');
80
+ expect(cardView(container).getAttribute('class')).toContain('w-full');
81
+ expect(getByText('Title').getAttribute('class')).toContain('text-lg');
82
+ expect(getByText('Desc').getAttribute('class')).toContain('italic');
51
83
  });
52
84
 
53
- it('press handlers are no-ops when pressable is false', async () => {
54
- const { componentRef } = await render(UiCard);
55
- const inst = componentRef.instance as unknown as {
56
- onPressStart: () => void;
57
- onPressEnd: () => void;
58
- onPressCancel: () => void;
59
- };
60
- expect(() => inst.onPressStart()).not.toThrow();
61
- expect(() => inst.onPressEnd()).not.toThrow();
62
- expect(() => inst.onPressCancel()).not.toThrow();
85
+ it("doesn't respond to presses unless it's pressable", async () => {
86
+ const { container } = await render(CardHost);
87
+ const view = cardView(container);
88
+ fireEvent.touchstart(view);
89
+ fireEvent.mousedown(view);
90
+ expect(scaleOf(view)).toBe('');
91
+ fireEvent.touchend(view);
92
+ fireEvent.touchcancel(view);
93
+ fireEvent.mouseup(view);
94
+ fireLynxEvent(view, 'mouseleave');
95
+ expect(scaleOf(view)).toBe('');
63
96
  });
64
97
 
65
- it('press handlers fire animations when pressable is true', async () => {
66
- const { componentRef } = await render(UiCard);
67
- const inst = componentRef.instance as UiCard;
68
- setInputSignal(inst.pressable, true);
98
+ it('scales down while pressed by touch, and back on release', async () => {
99
+ const { container, componentRef } = await render(CardHost);
100
+ (componentRef.instance as CardHost).pressable.set(true);
69
101
  await waitForUpdate();
70
- const cast = inst as unknown as {
71
- onPressStart: () => void;
72
- onPressEnd: () => void;
73
- onPressCancel: () => void;
74
- };
75
- // Run twice so `#pressAnim?.cancel()` gets exercised on the second call.
76
- cast.onPressStart();
77
- cast.onPressStart();
78
- cast.onPressEnd();
79
- cast.onPressCancel();
80
- // No assertion — coverage-only paths. Confirm no throw.
81
- expect(true).toBe(true);
82
- });
83
- });
102
+ const view = cardView(container);
84
103
 
85
- describe('Card sub-components', () => {
86
- it('UiCardHeader renders projected content', async () => {
87
- const { container } = await render(UiCardHeader);
88
- expect(container.querySelector('view')).toBeTruthy();
89
- });
104
+ fireEvent.touchstart(view);
105
+ expect(scaleOf(view)).toBe('scale(0.985)');
106
+ fireEvent.touchend(view);
107
+ expect(scaleOf(view)).toBe('scale(1)');
90
108
 
91
- it('UiCardTitle renders projected content', async () => {
92
- const { container } = await render(UiCardTitle);
93
- expect(container.querySelector('text')).toBeTruthy();
109
+ // A scroll that takes the touch over cancels it: the card scales back.
110
+ fireEvent.touchstart(view);
111
+ fireEvent.touchcancel(view);
112
+ expect(scaleOf(view)).toBe('scale(1)');
94
113
  });
95
114
 
96
- it('UiCardDescription renders projected content', async () => {
97
- const { container } = await render(UiCardDescription);
98
- expect(container.querySelector('text')).toBeTruthy();
99
- });
115
+ it('scales down while pressed by mouse, and back on release or leave', async () => {
116
+ const { container, componentRef } = await render(CardHost);
117
+ (componentRef.instance as CardHost).pressable.set(true);
118
+ await waitForUpdate();
119
+ const view = cardView(container);
100
120
 
101
- it('UiCardContent renders projected content', async () => {
102
- const { container } = await render(UiCardContent);
103
- expect(container.querySelector('view')).toBeTruthy();
104
- });
121
+ fireEvent.mousedown(view);
122
+ expect(scaleOf(view)).toBe('scale(0.985)');
123
+ fireEvent.mouseup(view);
124
+ expect(scaleOf(view)).toBe('scale(1)');
105
125
 
106
- it('UiCardFooter renders projected content', async () => {
107
- const { container } = await render(UiCardFooter);
108
- expect(container.querySelector('view')).toBeTruthy();
126
+ fireEvent.mousedown(view);
127
+ fireLynxEvent(view, 'mouseleave');
128
+ expect(scaleOf(view)).toBe('scale(1)');
109
129
  });
110
130
  });
@@ -0,0 +1,132 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { createChartRuntime } from '@tanstack/charts/runtime';
3
+ import { scaleLinear } from '@tanstack/charts/scales/linear';
4
+ import type { ChartDefinition, ChartPoint } from '@tanstack/charts/types';
5
+ import { defineChart, dot, lineY } from '@tanstack/charts/universal';
6
+ import { adjacentFocusPoint, createChartFocusModel } from './chart-focus';
7
+
8
+ const data = [
9
+ { x: 0, y: 1 },
10
+ { x: 1, y: 3 },
11
+ { x: 2, y: 2 },
12
+ ];
13
+ const scales = {
14
+ x: { scale: () => scaleLinear().domain([0, 2]) },
15
+ y: { scale: () => scaleLinear().domain([0, 4]) },
16
+ };
17
+
18
+ const modelFor = (definition: ChartDefinition<any, any, any>) => {
19
+ const scene = createChartRuntime().render(definition, {
20
+ width: 200,
21
+ height: 100,
22
+ });
23
+ return { scene, model: createChartFocusModel(scene, definition) };
24
+ };
25
+
26
+ describe('createChartFocusModel', () => {
27
+ it('resolves a touch to the nearest point', () => {
28
+ const definition = defineChart({
29
+ marks: [dot(data, { x: 'x', y: 'y' })],
30
+ scales,
31
+ });
32
+ const { scene, model } = modelFor(definition);
33
+ const target = scene.points[1]!;
34
+ expect(model.resolve(target.x + 2, target.y - 2)).toEqual([target]);
35
+ });
36
+
37
+ it('resolves nothing out of reach', () => {
38
+ const definition = defineChart({
39
+ marks: [dot(data, { x: 'x', y: 'y' })],
40
+ scales,
41
+ maxFocusDistance: 1,
42
+ });
43
+ const { model } = modelFor(definition);
44
+ expect(model.resolve(-500, -500)).toEqual([]);
45
+ });
46
+
47
+ it("groups a mark's series at one x with the group-x preset", () => {
48
+ const series = [
49
+ ...data.map((d) => ({ ...d, s: 'a' })),
50
+ ...data.map((d) => ({ ...d, y: d.y + 1, s: 'b' })),
51
+ ];
52
+ const definition = defineChart({
53
+ marks: [lineY(series, { x: 'x', y: 'y', z: 's' })],
54
+ scales,
55
+ focus: 'group-x',
56
+ });
57
+ const { scene, model } = modelFor(definition);
58
+ const point = scene.points.find((p) => p.xValue === 1)!;
59
+ const group = model.resolve(point.x, point.y);
60
+ expect(group.length).toBe(2);
61
+ expect(model.group(point).length).toBe(2);
62
+ });
63
+
64
+ it('turns focus off with focus: false', () => {
65
+ const definition = defineChart({
66
+ marks: [dot(data, { x: 'x', y: 'y' })],
67
+ scales,
68
+ focus: false,
69
+ });
70
+ const { scene, model } = modelFor(definition);
71
+ expect(model.resolve(scene.points[0]!.x, scene.points[0]!.y)).toEqual([]);
72
+ expect(model.navigation).toEqual([]);
73
+ });
74
+
75
+ it("uses the definition's spatial index", () => {
76
+ // Typed loosely: the index returns points of whatever datum the chart has.
77
+ const findNearest = vi.fn((): ChartPoint<any, any, any> | null => null);
78
+ const definition = defineChart({
79
+ marks: [dot(data, { x: 'x', y: 'y' })],
80
+ scales,
81
+ spatialIndex: () => ({ findNearest }),
82
+ });
83
+ const { model } = modelFor(definition);
84
+ expect(model.resolve(-500, -500)).toEqual([]);
85
+ expect(findNearest).toHaveBeenCalledWith(-500, -500, 48);
86
+ });
87
+
88
+ it('restores a point from an older scene by identity', () => {
89
+ const definition = defineChart({
90
+ marks: [dot(data, { x: 'x', y: 'y' })],
91
+ scales,
92
+ });
93
+ const first = modelFor(definition);
94
+ const second = modelFor(definition);
95
+ const restored = second.model.restore(first.scene.points[2]!);
96
+ expect(restored).toBe(second.scene.points[2]);
97
+ });
98
+
99
+ it('orders points left to right for stepping through them', () => {
100
+ const definition = defineChart({
101
+ marks: [dot([data[2]!, data[0]!, data[1]!], { x: 'x', y: 'y' })],
102
+ scales,
103
+ });
104
+ const { model } = modelFor(definition);
105
+ expect(model.navigation.map((p) => p.xValue)).toEqual([0, 1, 2]);
106
+ });
107
+ });
108
+
109
+ describe('adjacentFocusPoint', () => {
110
+ const definition = defineChart({
111
+ marks: [dot(data, { x: 'x', y: 'y' })],
112
+ scales,
113
+ });
114
+ const { model } = modelFor(definition);
115
+ const [first, second, third] = model.navigation;
116
+
117
+ it('starts at either end without a current point', () => {
118
+ expect(adjacentFocusPoint(model, null, 1)).toBe(first);
119
+ expect(adjacentFocusPoint(model, null, -1)).toBe(third);
120
+ });
121
+
122
+ it('steps and stops at the ends', () => {
123
+ expect(adjacentFocusPoint(model, first!, 1)).toBe(second);
124
+ expect(adjacentFocusPoint(model, first!, -1)).toBe(first);
125
+ expect(adjacentFocusPoint(model, third!, 1)).toBe(third);
126
+ });
127
+
128
+ it('returns null when there is nothing to step to', () => {
129
+ const empty = { ...model, navigation: [] };
130
+ expect(adjacentFocusPoint(empty, null, 1)).toBeNull();
131
+ });
132
+ });
@@ -0,0 +1,115 @@
1
+ import {
2
+ resolveChartFocusStrategy,
3
+ resolveChartPointerFocus,
4
+ restoreChartFocusPoint,
5
+ sameChartPointIdentity,
6
+ } from '@tanstack/charts/cursor/host';
7
+ import { focusDisabled } from '@tanstack/charts/focus/disabled';
8
+ import {
9
+ findNearestPoint,
10
+ viewportInteractionPoints,
11
+ } from '@tanstack/charts/scene';
12
+ import type {
13
+ ChartDefinition,
14
+ ChartFocusStrategy,
15
+ ChartPoint,
16
+ ChartScene,
17
+ } from '@tanstack/charts/types';
18
+
19
+ /**
20
+ * Which data points a touch lands on, and the order a screen reader steps
21
+ * through them.
22
+ *
23
+ * A port of TanStack's React Native focus model: it runs the definition's focus
24
+ * strategy (nearest point, a whole x column for `focus: 'x'`, …) over the
25
+ * scene's points rather than hit-testing painted shapes, which a single
26
+ * rasterized `<svg>` couldn't do anyway. One chart-level touch handler plus this
27
+ * model is also what TanStack recommends for native hosts, instead of a
28
+ * responder per mark.
29
+ */
30
+ export type ChartFocusModel = {
31
+ /** The focused group for a touch at scene coordinates, or `[]`. */
32
+ resolve(x: number, y: number): readonly ChartPoint[];
33
+ /** The group that focusing `point` shows (its whole x column, say). */
34
+ group(point: ChartPoint): readonly ChartPoint[];
35
+ /** Points in reading order, for stepping with accessibility actions. */
36
+ readonly navigation: readonly ChartPoint[];
37
+ /** The same datum's point in a newer scene, or `null` if it's gone. */
38
+ restore(point: ChartPoint): ChartPoint | null;
39
+ };
40
+
41
+ /** TanStack's default reach for pointer focus, in scene pixels. */
42
+ const DEFAULT_MAX_FOCUS_DISTANCE = 48;
43
+
44
+ export const createChartFocusModel = (
45
+ scene: ChartScene<any, any, any>,
46
+ definition: ChartDefinition<any, any, any>,
47
+ ): ChartFocusModel => {
48
+ const points = viewportInteractionPoints(scene);
49
+ const strategy: ChartFocusStrategy<any, any, any> | undefined =
50
+ definition.focus === false
51
+ ? focusDisabled
52
+ : resolveChartFocusStrategy(definition.focus);
53
+ const spatialIndex =
54
+ definition.focus === false
55
+ ? undefined
56
+ : definition.spatialIndex?.(points, { scene });
57
+ const maxDistance = definition.maxFocusDistance ?? DEFAULT_MAX_FOCUS_DISTANCE;
58
+
59
+ return {
60
+ resolve(x, y) {
61
+ const focused = resolveChartPointerFocus(
62
+ scene,
63
+ strategy,
64
+ x,
65
+ y,
66
+ maxDistance,
67
+ points,
68
+ );
69
+ if (focused) return focused;
70
+ const point = spatialIndex
71
+ ? spatialIndex.findNearest(x, y, maxDistance)
72
+ : findNearestPoint(scene, x, y, maxDistance, points);
73
+ const visible = point ? restoreChartFocusPoint(points, point) : null;
74
+ return visible ? [visible] : [];
75
+ },
76
+ group(point) {
77
+ return strategy?.group(points, { point }) ?? [point];
78
+ },
79
+ navigation: strategy?.navigation(points) ?? readingOrder(points),
80
+ restore(point) {
81
+ return restoreChartFocusPoint(points, point);
82
+ },
83
+ };
84
+ };
85
+
86
+ /** The point `direction` steps away from `current` in navigation order. */
87
+ export const adjacentFocusPoint = (
88
+ model: ChartFocusModel,
89
+ current: ChartPoint | null,
90
+ direction: -1 | 1,
91
+ ): ChartPoint | null => {
92
+ const { navigation } = model;
93
+ const index = current
94
+ ? navigation.findIndex((point) => sameChartPointIdentity(point, current))
95
+ : -1;
96
+ const next =
97
+ index < 0
98
+ ? direction > 0
99
+ ? 0
100
+ : navigation.length - 1
101
+ : Math.max(0, Math.min(navigation.length - 1, index + direction));
102
+ return navigation[next] ?? null;
103
+ };
104
+
105
+ /**
106
+ * Left to right, then top to bottom; ties keep scene order.
107
+ */
108
+ const readingOrder = (points: readonly ChartPoint[]): ChartPoint[] =>
109
+ points
110
+ .map((point, index) => ({ point, index }))
111
+ .sort(
112
+ (a, b) =>
113
+ a.point.x - b.point.x || a.point.y - b.point.y || a.index - b.index,
114
+ )
115
+ .map(({ point }) => point);