@blotch/dolan 22.2.3 → 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 (185) hide show
  1. package/dist/commands/add.js +9 -1
  2. package/dist/commands/add.js.map +1 -1
  3. package/dist/commands/add.test.js +36 -3
  4. package/dist/commands/add.test.js.map +1 -1
  5. package/dist/commands/doctor.js +187 -7
  6. package/dist/commands/doctor.js.map +1 -1
  7. package/dist/commands/doctor.test.js +280 -7
  8. package/dist/commands/doctor.test.js.map +1 -1
  9. package/dist/commands/init.d.ts +2 -2
  10. package/dist/commands/init.js +38 -18
  11. package/dist/commands/init.js.map +1 -1
  12. package/dist/commands/init.test.js +107 -1
  13. package/dist/commands/init.test.js.map +1 -1
  14. package/dist/commands/theme.js +1 -1
  15. package/dist/commands/theme.js.map +1 -1
  16. package/dist/commands/update.d.ts +5 -0
  17. package/dist/commands/update.js +126 -45
  18. package/dist/commands/update.js.map +1 -1
  19. package/dist/commands/update.test.js +314 -2
  20. package/dist/commands/update.test.js.map +1 -1
  21. package/dist/lockfile.d.ts +7 -0
  22. package/dist/lockfile.js +36 -0
  23. package/dist/lockfile.js.map +1 -1
  24. package/dist/lockfile.test.d.ts +1 -0
  25. package/dist/lockfile.test.js +25 -0
  26. package/dist/lockfile.test.js.map +1 -0
  27. package/dist/registry.d.ts +6 -0
  28. package/dist/registry.js +48 -2
  29. package/dist/registry.js.map +1 -1
  30. package/dist/registry.test.js +45 -3
  31. package/dist/registry.test.js.map +1 -1
  32. package/dist/test-utils.d.ts +5 -0
  33. package/dist/test-utils.js +6 -1
  34. package/dist/test-utils.js.map +1 -1
  35. package/dist/testing/fire-lynx-event.d.ts +14 -0
  36. package/dist/testing/fire-lynx-event.js +19 -0
  37. package/dist/testing/fire-lynx-event.js.map +1 -0
  38. package/dist/testing/fire-lynx-event.test.d.ts +1 -0
  39. package/dist/testing/fire-lynx-event.test.js +44 -0
  40. package/dist/testing/fire-lynx-event.test.js.map +1 -0
  41. package/dist/testing/lynx-main-thread.d.ts +22 -0
  42. package/dist/testing/lynx-main-thread.js +11 -0
  43. package/dist/testing/lynx-main-thread.js.map +1 -0
  44. package/dist/testing/lynx-main-thread.test.d.ts +1 -0
  45. package/dist/testing/lynx-main-thread.test.js +24 -0
  46. package/dist/testing/lynx-main-thread.test.js.map +1 -0
  47. package/dist/testing/set-input-signal.d.ts +5 -4
  48. package/dist/testing/set-input-signal.js +5 -4
  49. package/dist/testing/set-input-signal.js.map +1 -1
  50. package/dist/ui/components/accordion/accordion.test.ts +158 -144
  51. package/dist/ui/components/accordion/accordion.ts +19 -14
  52. package/dist/ui/components/action-sheet/action-sheet.test.ts +272 -187
  53. package/dist/ui/components/action-sheet/action-sheet.ts +40 -34
  54. package/dist/ui/components/alert/alert.test.ts +67 -19
  55. package/dist/ui/components/alert/alert.ts +14 -16
  56. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +206 -139
  57. package/dist/ui/components/alert-dialog/alert-dialog.ts +32 -26
  58. package/dist/ui/components/area-chart/area-chart.test.ts +81 -1
  59. package/dist/ui/components/avatar/avatar.test.ts +89 -45
  60. package/dist/ui/components/badge/badge.test.ts +57 -23
  61. package/dist/ui/components/badge/badge.ts +12 -14
  62. package/dist/ui/components/bar-chart/bar-chart.test.ts +82 -1
  63. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +373 -297
  64. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +34 -33
  65. package/dist/ui/components/button/button.test.ts +611 -2
  66. package/dist/ui/components/button/button.ts +431 -62
  67. package/dist/ui/components/button/index.ts +2 -0
  68. package/dist/ui/components/button-group/button-group.test.ts +484 -2
  69. package/dist/ui/components/button-group/button-group.ts +380 -68
  70. package/dist/ui/components/button-group/index.ts +3 -0
  71. package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +82 -1
  72. package/dist/ui/components/card/card.test.ts +82 -62
  73. package/dist/ui/components/chart/chart-presets.test.ts +197 -1
  74. package/dist/ui/components/chart/chart-presets.ts +8 -1
  75. package/dist/ui/components/chart/chart-theme.ts +12 -0
  76. package/dist/ui/components/chart/chart.test.ts +1017 -0
  77. package/dist/ui/components/chart/chart.ts +56 -43
  78. package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
  79. package/dist/ui/components/checkbox/checkbox.ts +23 -23
  80. package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
  81. package/dist/ui/components/collapsible/collapsible.ts +19 -14
  82. package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
  83. package/dist/ui/components/context-menu/context-menu.ts +110 -0
  84. package/dist/ui/components/context-menu/index.ts +1 -0
  85. package/dist/ui/components/dialog/dialog.test.ts +210 -152
  86. package/dist/ui/components/dialog/dialog.ts +29 -23
  87. package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
  88. package/dist/ui/components/dock/dock-scroll.ts +133 -0
  89. package/dist/ui/components/dock/dock.test.ts +749 -0
  90. package/dist/ui/components/dock/dock.ts +960 -0
  91. package/dist/ui/components/dock/index.ts +2 -0
  92. package/dist/ui/components/empty-state/empty-state.test.ts +28 -39
  93. package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
  94. package/dist/ui/components/glass/concentric-radius.ts +17 -0
  95. package/dist/ui/components/glass/glass-design.test.ts +78 -0
  96. package/dist/ui/components/glass/glass-design.ts +119 -0
  97. package/dist/ui/components/glass/glass.test.ts +338 -0
  98. package/dist/ui/components/glass/glass.ts +360 -0
  99. package/dist/ui/components/glass/index.ts +18 -0
  100. package/dist/ui/components/icon/icon-theme.test.ts +36 -0
  101. package/dist/ui/components/icon/icon-theme.ts +36 -13
  102. package/dist/ui/components/icon/icon.test.ts +52 -41
  103. package/dist/ui/components/input/input.test.ts +178 -101
  104. package/dist/ui/components/input/input.ts +15 -12
  105. package/dist/ui/components/label/label.test.ts +11 -24
  106. package/dist/ui/components/line-chart/line-chart.test.ts +76 -1
  107. package/dist/ui/components/list/list.test.ts +80 -49
  108. package/dist/ui/components/menu/index.ts +25 -0
  109. package/dist/ui/components/menu/menu-placement.test.ts +153 -0
  110. package/dist/ui/components/menu/menu-placement.ts +159 -0
  111. package/dist/ui/components/menu/menu-popup.test.ts +632 -0
  112. package/dist/ui/components/menu/menu-popup.ts +544 -0
  113. package/dist/ui/components/menu/menu.test.ts +429 -0
  114. package/dist/ui/components/menu/menu.ts +415 -0
  115. package/dist/ui/components/nav-drawer/nav-drawer.test.ts +425 -260
  116. package/dist/ui/components/nav-drawer/nav-drawer.ts +72 -56
  117. package/dist/ui/components/pagination/pagination.test.ts +136 -86
  118. package/dist/ui/components/progress/progress.test.ts +102 -46
  119. package/dist/ui/components/progress/progress.ts +52 -54
  120. package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
  121. package/dist/ui/components/radio-group/radio-group.ts +24 -24
  122. package/dist/ui/components/scatter-chart/scatter-chart.test.ts +74 -1
  123. package/dist/ui/components/scroll-area/scroll-area.test.ts +72 -36
  124. package/dist/ui/components/select/select.test.ts +138 -78
  125. package/dist/ui/components/select/select.ts +17 -11
  126. package/dist/ui/components/separator/separator.test.ts +18 -17
  127. package/dist/ui/components/sheet/sheet.test.ts +64 -11
  128. package/dist/ui/components/sheet/sheet.ts +9 -1
  129. package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
  130. package/dist/ui/components/skeleton/skeleton.ts +10 -16
  131. package/dist/ui/components/spinner/spinner.test.ts +64 -46
  132. package/dist/ui/components/spinner/spinner.ts +16 -19
  133. package/dist/ui/components/switch/switch.test.ts +116 -46
  134. package/dist/ui/components/switch/switch.ts +8 -13
  135. package/dist/ui/components/tab-bar/index.ts +1 -0
  136. package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
  137. package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
  138. package/dist/ui/components/tabs/tabs.test.ts +215 -157
  139. package/dist/ui/components/tabs/tabs.ts +110 -118
  140. package/dist/ui/components/textarea/textarea.test.ts +206 -100
  141. package/dist/ui/components/textarea/textarea.ts +14 -12
  142. package/dist/ui/components/toast/toast.test.ts +434 -350
  143. package/dist/ui/components/toast/toast.ts +42 -51
  144. package/dist/ui/components/toggle/toggle.test.ts +247 -140
  145. package/dist/ui/theme/chart-colors.test.ts +8 -3
  146. package/dist/ui/theme/dark.css +38 -2
  147. package/dist/ui/theme/default.css +55 -7
  148. package/dist/ui/theme/icon-colors.test.ts +8 -3
  149. package/dist/ui/theme/rgb-channels.test.ts +84 -0
  150. package/dist/ui/theme/tailwind-build.test.ts +114 -0
  151. package/dist/ui/theme/tailwind-css.test.ts +83 -0
  152. package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
  153. package/dist/ui/theme/tailwind-plugin.ts +93 -29
  154. package/dist/ui/theme/tailwind.css +113 -0
  155. package/dist/ui/utils/animate.test.ts +13 -1
  156. package/dist/utils/analyze.d.ts +14 -0
  157. package/dist/utils/analyze.js +15 -0
  158. package/dist/utils/analyze.js.map +1 -1
  159. package/dist/utils/analyze.test.js +14 -1
  160. package/dist/utils/analyze.test.js.map +1 -1
  161. package/dist/utils/installed.d.ts +12 -0
  162. package/dist/utils/installed.js +16 -0
  163. package/dist/utils/installed.js.map +1 -1
  164. package/dist/utils/installed.test.js +17 -1
  165. package/dist/utils/installed.test.js.map +1 -1
  166. package/dist/utils/resolve-paths.d.ts +4 -4
  167. package/dist/utils/resolve-paths.js +5 -7
  168. package/dist/utils/resolve-paths.js.map +1 -1
  169. package/dist/utils/resolve-paths.test.js +9 -5
  170. package/dist/utils/resolve-paths.test.js.map +1 -1
  171. package/dist/utils/tailwind.d.ts +25 -0
  172. package/dist/utils/tailwind.js +51 -0
  173. package/dist/utils/tailwind.js.map +1 -0
  174. package/dist/utils/tailwind.test.d.ts +1 -0
  175. package/dist/utils/tailwind.test.js +71 -0
  176. package/dist/utils/tailwind.test.js.map +1 -0
  177. package/package.json +9 -1
  178. package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -63
  179. package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -58
  180. package/dist/ui/components/button/button-render.test.ts +0 -132
  181. package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
  182. package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -59
  183. package/dist/ui/components/chart/chart-render.test.ts +0 -730
  184. package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -83
  185. package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -64
@@ -1,9 +1,25 @@
1
1
  import { Component, signal } from '@angular/core';
2
- import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
2
+ import {
3
+ fireEvent,
4
+ render,
5
+ waitForUpdate,
6
+ } from '@blotch/angular-lynx-testing-library';
3
7
  import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
4
- import { describe, expect, it, vi } from 'vitest';
8
+ import {
9
+ type MockInstance,
10
+ afterEach,
11
+ beforeEach,
12
+ describe,
13
+ expect,
14
+ it,
15
+ vi,
16
+ } from 'vitest';
5
17
  import { DURATION } from '../../utils/animate';
6
- import { setInputSignal } from '../../../test-utils/set-input-signal';
18
+ import { fireLynxEvent } from '../../../test-utils/fire-lynx-event';
19
+ import {
20
+ type LynxMainThread,
21
+ lynxMainThread,
22
+ } from '../../../test-utils/lynx-main-thread';
7
23
  import {
8
24
  UiActionSheet,
9
25
  UiActionSheetCancel,
@@ -20,239 +36,308 @@ import {
20
36
  LYNX_ELEMENTS,
21
37
  ],
22
38
  template: `
23
- <ui-action-sheet [(open)]="open">
24
- <ui-action-sheet-title>Pick</ui-action-sheet-title>
25
- <ui-action-sheet-item>Option</ui-action-sheet-item>
26
- <ui-action-sheet-cancel>Cancel</ui-action-sheet-cancel>
39
+ <ui-action-sheet [(open)]="open" class="pb-8" (closed)="closed()">
40
+ <ui-action-sheet-title class="py-4">Pick</ui-action-sheet-title>
41
+ <ui-action-sheet-item (pressed)="shared()">Share</ui-action-sheet-item>
42
+ <ui-action-sheet-item
43
+ variant="destructive"
44
+ class="bg-muted"
45
+ (pressed)="deleted()"
46
+ >
47
+ Delete
48
+ </ui-action-sheet-item>
49
+ <ui-action-sheet-cancel class="mt-2" />
27
50
  </ui-action-sheet>
28
51
  `,
29
52
  })
30
53
  class ActionSheetHost {
31
- open = true;
54
+ readonly open = signal(false);
55
+ readonly closed = vi.fn();
56
+ readonly shared = vi.fn();
57
+ readonly deleted = vi.fn();
32
58
  }
33
59
 
34
- describe('UiActionSheet', () => {
35
- it('stops its close timers when it is destroyed while closing', async () => {
36
- const warn = vi.spyOn(console, 'warn');
37
- const { componentRef, unmount } = await render(UiActionSheet);
38
- const inst = componentRef.instance as UiActionSheet;
39
- inst.open.set(true);
40
- for (let i = 0; i < 5; i++) await waitForUpdate();
41
- inst.open.set(false);
42
- await waitForUpdate();
43
- // Destroyed during the close animation, as an `@if` around the action sheet
44
- // would do. Its close timer (DURATION.normal + 20) used to fire anyway
45
- // and emit `closed` from the destroyed output.
46
- unmount();
47
- await new Promise((r) => setTimeout(r, DURATION.normal + 50));
48
- const warnings = warn.mock.calls.map(([message]) => String(message));
49
- warn.mockRestore();
50
- expect(warnings.filter((message) => message.includes('NG0953'))).toEqual(
51
- [],
52
- );
53
- });
60
+ /**
61
+ * A cancel button given its own label.
62
+ */
63
+ @Component({
64
+ imports: [UiActionSheet, UiActionSheetCancel],
65
+ template: `
66
+ <ui-action-sheet>
67
+ <ui-action-sheet-cancel label="Dismiss" />
68
+ </ui-action-sheet>
69
+ `,
70
+ })
71
+ class CancelLabelHost {}
54
72
 
55
- it('moves a screen reader into the panel and keeps it there while open', async () => {
56
- const { componentRef, container } = await render(UiActionSheet);
57
- const inst = componentRef.instance as UiActionSheet;
58
- const panel = container.querySelector(
59
- '[accessibility-exclusive-focus]',
60
- ) as Element & { invoke?: unknown };
61
- expect(panel.getAttribute('accessibility-exclusive-focus')).toBe('false');
62
- const invoke = vi.fn();
63
- panel.invoke = invoke;
64
- (inst as unknown as { backdropRef: unknown }).backdropRef = signal({
65
- nativeElement: panel,
66
- });
67
- (inst as unknown as { panelRef: unknown }).panelRef = signal({
68
- nativeElement: panel,
69
- });
70
- inst.open.set(true);
71
- for (let i = 0; i < 5; i++) await waitForUpdate();
72
- expect(invoke).toHaveBeenCalledWith('requestAccessibilityFocus');
73
- expect(panel.getAttribute('accessibility-exclusive-focus')).toBe('true');
74
- });
73
+ /**
74
+ * The Lynx function an element's `invoke()` calls to run one of its UI
75
+ * methods, such as moving a screen reader's focus. The testing library
76
+ * defines it on the global object.
77
+ */
78
+ type InvokeUIMethod = {
79
+ __InvokeUIMethod: (
80
+ element: Element,
81
+ method: string,
82
+ params: Record<string, unknown>,
83
+ callback: (result: { code: number }) => void,
84
+ ) => void;
85
+ };
75
86
 
76
- const patchRefs = (inst: UiActionSheet, view: Element) => {
77
- (inst as unknown as { backdropRef: unknown }).backdropRef = signal({
78
- nativeElement: view,
79
- });
80
- (inst as unknown as { panelRef: unknown }).panelRef = signal({
81
- nativeElement: view,
82
- });
87
+ /** `__ElementAnimate`'s first argument for starting an animation. */
88
+ const START = 0;
89
+
90
+ const renderSheet = async () => {
91
+ const result = await render(ActionSheetHost);
92
+ const { container } = result;
93
+ return {
94
+ ...result,
95
+ host: result.componentRef.instance as ActionSheetHost,
96
+ overlay: container.querySelector('overlay')!,
97
+ backdrop: container.querySelector('overlay > view')!,
98
+ panel: container.querySelector('[accessibility-exclusive-focus]')!,
99
+ // A button's pressable tile: the view around its label.
100
+ button: (label: string) => result.getByText(label).parentElement!,
83
101
  };
102
+ };
103
+
104
+ /**
105
+ * Opens the action sheet and waits out its open sequence: the overlay shows
106
+ * on one tick, and the panel slides in on the next.
107
+ */
108
+ const openSheet = async (host: ActionSheetHost) => {
109
+ host.open.set(true);
110
+ for (let i = 0; i < 3; i++) await waitForUpdate();
111
+ };
84
112
 
85
- it('renders the sheet, title, item, and cancel content', async () => {
86
- const { getByText } = await render(ActionSheetHost);
113
+ /**
114
+ * Waits for the close sequence to finish: the panel slides out, then the
115
+ * overlay hides and `closed` fires. Polled rather than timed, since the
116
+ * sequence only starts on the update after `open` changes.
117
+ */
118
+ const waitForClose = async (host: ActionSheetHost) => {
119
+ await vi.waitFor(() => expect(host.closed).toHaveBeenCalled());
120
+ await waitForUpdate();
121
+ };
122
+
123
+ /**
124
+ * The scale a button's inline style holds, if any.
125
+ */
126
+ const scaleOf = (element: Element) =>
127
+ (element as HTMLElement).style.getPropertyValue('transform');
128
+
129
+ /**
130
+ * Presses a button and lets go every way Lynx reports it, checking that it
131
+ * scales down while pressed and back up once released.
132
+ */
133
+ const expectPressFeedback = (button: Element) => {
134
+ fireEvent.touchstart(button);
135
+ expect(scaleOf(button)).toBe('scale(0.985)');
136
+ fireEvent.touchend(button);
137
+ expect(scaleOf(button)).toBe('scale(1)');
138
+
139
+ // A scroll that takes the touch over cancels it: the button scales back.
140
+ fireEvent.touchstart(button);
141
+ fireEvent.touchcancel(button);
142
+ expect(scaleOf(button)).toBe('scale(1)');
143
+
144
+ // On web, the mouse drives the same feedback.
145
+ fireEvent.mousedown(button);
146
+ expect(scaleOf(button)).toBe('scale(0.985)');
147
+ fireEvent.mouseup(button);
148
+ expect(scaleOf(button)).toBe('scale(1)');
149
+
150
+ fireEvent.mousedown(button);
151
+ fireLynxEvent(button, 'mouseleave');
152
+ expect(scaleOf(button)).toBe('scale(1)');
153
+ };
154
+
155
+ describe('UiActionSheet', () => {
156
+ // Watched per test: the testing library resets the main thread after each.
157
+ let animate: MockInstance<LynxMainThread['__ElementAnimate']>;
158
+ beforeEach(() => {
159
+ animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
160
+ });
161
+ afterEach(() => animate.mockRestore());
162
+
163
+ it('stays hidden until opened, then shows its title and buttons', async () => {
164
+ const { host, overlay, getByText } = await renderSheet();
165
+ expect(overlay.getAttribute('style')).toContain('display: none');
166
+ expect(overlay.getAttribute('visible')).toBe('false');
167
+
168
+ await openSheet(host);
169
+ expect(overlay.getAttribute('style')).not.toContain('display: none');
170
+ expect(overlay.getAttribute('visible')).toBe('true');
87
171
  expect(getByText('Pick')).toBeTruthy();
88
- expect(getByText('Option')).toBeTruthy();
172
+ expect(getByText('Share')).toBeTruthy();
173
+ expect(getByText('Delete')).toBeTruthy();
89
174
  expect(getByText('Cancel')).toBeTruthy();
90
175
  });
91
176
 
92
- it('backdrop tap closes the sheet', async () => {
93
- const { componentRef } = await render(UiActionSheet);
94
- const inst = componentRef.instance as UiActionSheet;
95
- inst.open.set(true);
96
- await waitForUpdate();
97
- (inst as unknown as { onBackdropTap: () => void }).onBackdropTap();
98
- expect(inst.open()).toBe(false);
177
+ it("merges the class into the panel's own", async () => {
178
+ const { panel } = await renderSheet();
179
+ expect(panel.classList).toContain('pb-8');
180
+ expect(panel.classList).not.toContain('pb-6');
181
+ expect(panel.classList).toContain('px-4');
99
182
  });
100
183
 
101
- it('close() sets open to false', async () => {
102
- const { componentRef } = await render(UiActionSheet);
103
- const inst = componentRef.instance as UiActionSheet;
104
- inst.open.set(true);
105
- inst.close();
106
- expect(inst.open()).toBe(false);
184
+ it('fades the backdrop in and slides the panel up when opened', async () => {
185
+ const { host, backdrop, panel } = await renderSheet();
186
+ await openSheet(host);
187
+ expect(animate).toHaveBeenCalledWith(backdrop, [
188
+ START,
189
+ expect.any(String),
190
+ [{ opacity: 0 }, { opacity: 1 }],
191
+ expect.objectContaining({ duration: DURATION.normal }),
192
+ ]);
193
+ expect(animate).toHaveBeenCalledWith(panel, [
194
+ START,
195
+ expect.any(String),
196
+ [
197
+ { transform: 'translateY(100%)', opacity: 0.8 },
198
+ { transform: 'translateY(0%)', opacity: 1 },
199
+ ],
200
+ expect.objectContaining({ duration: DURATION.slow }),
201
+ ]);
107
202
  });
108
203
 
109
- it('panel tap is a no-op', async () => {
110
- const { componentRef } = await render(UiActionSheet);
111
- const inst = componentRef.instance as UiActionSheet;
112
- expect(() =>
113
- (inst as unknown as { onPanelTap: () => void }).onPanelTap(),
114
- ).not.toThrow();
115
- });
204
+ it('moves a screen reader into the panel and keeps it there while open', async () => {
205
+ const invoke = vi.spyOn(
206
+ globalThis as unknown as InvokeUIMethod,
207
+ '__InvokeUIMethod',
208
+ );
209
+ const { host, panel } = await renderSheet();
210
+ expect(panel.getAttribute('accessibility-exclusive-focus')).toBe('false');
116
211
 
117
- it('open effect runs the animate-in and animate-out lifecycles', async () => {
118
- const { componentRef, container } = await render(UiActionSheet);
119
- const inst = componentRef.instance as UiActionSheet;
120
- const view = container.querySelector('view') as Element;
121
- patchRefs(inst, view);
212
+ await openSheet(host);
213
+ expect(invoke).toHaveBeenCalledWith(
214
+ panel,
215
+ 'requestAccessibilityFocus',
216
+ expect.anything(),
217
+ expect.any(Function),
218
+ );
219
+ expect(panel.getAttribute('accessibility-exclusive-focus')).toBe('true');
220
+ invoke.mockRestore();
221
+ });
122
222
 
123
- inst.open.set(true);
124
- await waitForUpdate();
125
- await waitForUpdate();
126
- await waitForUpdate();
223
+ it('closes when the backdrop is tapped, sliding the panel down', async () => {
224
+ const { host, overlay, backdrop, panel } = await renderSheet();
225
+ await openSheet(host);
127
226
 
128
- inst.open.set(false);
129
- await new Promise((r) => setTimeout(r, 320));
130
- await waitForUpdate();
131
- expect(inst.open()).toBe(false);
227
+ fireEvent.tap(backdrop);
228
+ expect(host.open()).toBe(false);
229
+ await waitForClose(host);
230
+ expect(animate).toHaveBeenCalledWith(backdrop, [
231
+ START,
232
+ expect.any(String),
233
+ [{ opacity: 1 }, { opacity: 0 }],
234
+ expect.objectContaining({ duration: DURATION.normal }),
235
+ ]);
236
+ expect(animate).toHaveBeenCalledWith(panel, [
237
+ START,
238
+ expect.any(String),
239
+ [
240
+ { transform: 'translateY(0%)', opacity: 1 },
241
+ { transform: 'translateY(100%)', opacity: 0.8 },
242
+ ],
243
+ expect.objectContaining({ duration: DURATION.normal }),
244
+ ]);
245
+ // Once the slide is over, the overlay hides and `closed` fires, only for
246
+ // this close: an action sheet that starts closed hasn't closed before.
247
+ expect(overlay.getAttribute('style')).toContain('display: none');
248
+ expect(host.closed).toHaveBeenCalledOnce();
132
249
  });
133
250
 
134
- it('open effect bails when backdrop/panel refs are undefined', async () => {
135
- const { componentRef } = await render(UiActionSheet);
136
- const inst = componentRef.instance as UiActionSheet;
137
- const closed = vi.fn();
138
- inst.closed.subscribe(closed);
139
- inst.open.set(true);
140
- await waitForUpdate();
141
- await waitForUpdate();
142
- inst.open.set(false);
143
- await waitForUpdate();
144
- // Wait out the close sequence, so #animateOut runs (and bails) while the
145
- // component is still alive. Destroying it clears the sequence's timers.
146
- await new Promise((r) => setTimeout(r, DURATION.normal + 50));
147
- expect(inst.open()).toBe(false);
148
- expect(closed).toHaveBeenCalledOnce();
251
+ it('stays open when the panel is tapped', async () => {
252
+ const { host, panel } = await renderSheet();
253
+ await openSheet(host);
254
+ // The panel catches its taps, so they never reach the backdrop.
255
+ fireEvent.tap(panel, { eventType: 'catchEvent' });
256
+ expect(host.open()).toBe(true);
149
257
  });
150
258
 
151
- it('applies user class on the panel', async () => {
152
- const { componentRef, container } = await render(UiActionSheet);
153
- const inst = componentRef.instance as UiActionSheet;
154
- setInputSignal(inst.userClass, 'my-panel');
259
+ it('stops its close timers when it is destroyed while closing', async () => {
260
+ const warn = vi.spyOn(console, 'warn');
261
+ const { host, unmount } = await renderSheet();
262
+ await openSheet(host);
263
+ host.open.set(false);
155
264
  await waitForUpdate();
156
- expect(
157
- Array.from(container.querySelectorAll('view')).some((el) =>
158
- el.getAttribute('class')?.includes('my-panel'),
159
- ),
160
- ).toBe(true);
265
+ // Destroyed during the close animation, as an `@if` around the action
266
+ // sheet would do. Its close timer (DURATION.normal + 20) used to fire
267
+ // anyway and emit `closed` from the destroyed output.
268
+ unmount();
269
+ await new Promise((resolve) => setTimeout(resolve, DURATION.normal + 50));
270
+ const warnings = warn.mock.calls.map(([message]) => String(message));
271
+ warn.mockRestore();
272
+ expect(warnings.filter((message) => message.includes('NG0953'))).toEqual(
273
+ [],
274
+ );
275
+ expect(host.closed).not.toHaveBeenCalled();
161
276
  });
162
277
  });
163
278
 
164
279
  describe('UiActionSheetTitle', () => {
165
- it('renders projected content', async () => {
166
- const { container } = await render(UiActionSheetTitle);
167
- expect(container.querySelector('view')).toBeTruthy();
168
- expect(container.querySelector('text')).toBeTruthy();
280
+ it('shows its text muted, with the class on its container', async () => {
281
+ const { getByText } = await renderSheet();
282
+ const title = getByText('Pick');
283
+ expect(title.classList).toContain('text-muted-foreground');
284
+ expect(title.parentElement?.classList).toContain('py-4');
285
+ expect(title.parentElement?.classList).not.toContain('py-3');
169
286
  });
170
287
  });
171
288
 
172
289
  describe('UiActionSheetItem', () => {
173
- const renderItem = async () => {
174
- const parentResult = await render(UiActionSheet);
175
- const parent = parentResult.componentRef.instance as UiActionSheet;
176
- parent.open.set(true);
177
- await waitForUpdate();
290
+ it('reports the press and closes the action sheet when tapped', async () => {
291
+ const { host, button } = await renderSheet();
292
+ await openSheet(host);
178
293
 
179
- const itemResult = await render(UiActionSheetItem, {
180
- providers: [{ provide: UiActionSheet, useValue: parent }],
181
- });
182
- return { parent, itemResult };
183
- };
294
+ fireEvent.tap(button('Share'));
295
+ expect(host.shared).toHaveBeenCalledOnce();
296
+ expect(host.deleted).not.toHaveBeenCalled();
297
+ expect(host.open()).toBe(false);
298
+ });
184
299
 
185
- it('onTap emits `pressed` and closes the parent sheet', async () => {
186
- const { parent, itemResult } = await renderItem();
187
- const inst = itemResult.componentRef
188
- .instance as unknown as UiActionSheetItem & {
189
- onTap: () => void;
190
- };
191
- const emitted = vi.fn();
192
- inst.pressed.subscribe(emitted);
193
- inst.onTap();
194
- expect(emitted).toHaveBeenCalledTimes(1);
195
- expect(parent.open()).toBe(false);
300
+ it('colors its label by variant', async () => {
301
+ const { getByText } = await renderSheet();
302
+ expect(getByText('Share').classList).toContain('text-primary');
303
+ expect(getByText('Delete').classList).toContain('text-destructive');
196
304
  });
197
305
 
198
- it('press handlers run cleanly', async () => {
199
- const { itemResult } = await renderItem();
200
- const inst = itemResult.componentRef.instance as unknown as {
201
- onPressStart: () => void;
202
- onPressEnd: () => void;
203
- onPressCancel: () => void;
204
- };
205
- expect(() => inst.onPressStart()).not.toThrow();
206
- expect(() => inst.onPressEnd()).not.toThrow();
207
- expect(() => inst.onPressCancel()).not.toThrow();
306
+ it("merges the class into its tile's own", async () => {
307
+ const { button } = await renderSheet();
308
+ expect(button('Delete').classList).toContain('bg-muted');
309
+ expect(button('Delete').classList).toContain('border-t');
208
310
  });
209
311
 
210
- it('applies destructive variant text class', async () => {
211
- const { itemResult } = await renderItem();
212
- const inst = itemResult.componentRef.instance as UiActionSheetItem;
213
- setInputSignal(inst.variant, 'destructive');
214
- await waitForUpdate();
215
- const text = itemResult.container.querySelector('text');
216
- expect(text?.getAttribute('class')).toContain('text-destructive');
312
+ it('scales down while pressed, and back on release', async () => {
313
+ const { button } = await renderSheet();
314
+ expectPressFeedback(button('Share'));
217
315
  });
218
316
  });
219
317
 
220
318
  describe('UiActionSheetCancel', () => {
221
- const renderCancel = async () => {
222
- const parentResult = await render(UiActionSheet);
223
- const parent = parentResult.componentRef.instance as UiActionSheet;
224
- parent.open.set(true);
225
- await waitForUpdate();
319
+ it('closes the action sheet when tapped', async () => {
320
+ const { host, button } = await renderSheet();
321
+ await openSheet(host);
226
322
 
227
- const cancelResult = await render(UiActionSheetCancel, {
228
- providers: [{ provide: UiActionSheet, useValue: parent }],
229
- });
230
- return { parent, cancelResult };
231
- };
323
+ fireEvent.tap(button('Cancel'));
324
+ expect(host.open()).toBe(false);
325
+ });
232
326
 
233
- it('renders the default cancel label', async () => {
234
- const { cancelResult } = await renderCancel();
235
- expect(cancelResult.container.textContent).toContain('Cancel');
327
+ it('shows Cancel, or the label it is given', async () => {
328
+ expect((await renderSheet()).getByText('Cancel')).toBeTruthy();
329
+ const { getByText } = await render(CancelLabelHost);
330
+ expect(getByText('Dismiss')).toBeTruthy();
236
331
  });
237
332
 
238
- it('onTap closes the parent sheet', async () => {
239
- const { parent, cancelResult } = await renderCancel();
240
- const inst = cancelResult.componentRef.instance as unknown as {
241
- onTap: () => void;
242
- };
243
- inst.onTap();
244
- expect(parent.open()).toBe(false);
333
+ it("merges the class into its tile's own", async () => {
334
+ const { button } = await renderSheet();
335
+ expect(button('Cancel').classList).toContain('mt-2');
336
+ expect(button('Cancel').classList).toContain('bg-card');
245
337
  });
246
338
 
247
- it('press handlers run cleanly', async () => {
248
- const { cancelResult } = await renderCancel();
249
- const inst = cancelResult.componentRef.instance as unknown as {
250
- onPressStart: () => void;
251
- onPressEnd: () => void;
252
- onPressCancel: () => void;
253
- };
254
- expect(() => inst.onPressStart()).not.toThrow();
255
- expect(() => inst.onPressEnd()).not.toThrow();
256
- expect(() => inst.onPressCancel()).not.toThrow();
339
+ it('scales down while pressed, and back on release', async () => {
340
+ const { button } = await renderSheet();
341
+ expectPressFeedback(button('Cancel'));
257
342
  });
258
343
  });