@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,6 +1,7 @@
1
1
  // cspell:words WAAPI tweens
2
2
  import {
3
3
  type ElementRef,
4
+ type OnDestroy,
4
5
  Component,
5
6
  ViewEncapsulation,
6
7
  computed,
@@ -59,29 +60,44 @@ const BACKDROP_CLEAR_DISTANCE = 500;
59
60
  */
60
61
  @Component({
61
62
  selector: 'ui-bottom-sheet',
62
- standalone: true,
63
63
  imports: [LYNX_ELEMENTS],
64
64
  encapsulation: ViewEncapsulation.None,
65
65
  template: `
66
66
  <overlay [attr.visible]="overlayVisible()" [style]="overlayStyle()">
67
+ <!--
68
+ The mouse drag's move/up listeners sit on the full-screen backdrop,
69
+ not the handle: a touch keeps targeting the element it started on, but
70
+ a mouse event targets whatever is under the cursor, so a cursor that
71
+ outruns the thin handle strip (or drags up off the panel) would stop
72
+ driving the drag. Mouse events bubble here from anywhere in the sheet,
73
+ including the handle and the panel. There's no mouseleave: like a
74
+ touch, the drag carries on until release, and Chrome doesn't deliver
75
+ mouseleave to Lynx web anyway (see onMouseDragMove for a release
76
+ outside the view).
77
+ -->
67
78
  <view
68
79
  #backdrop
69
80
  class="h-full w-full"
70
81
  [style]="backdropStyle()"
71
82
  (bindtap)="onBackdropTap()"
83
+ (bindmousedown)="onBackdropMouseDown()"
84
+ (bindmousemove)="onMouseDragMove($event)"
85
+ (bindmouseup)="onDragEnd()"
72
86
  >
73
87
  <view
74
88
  #panel
75
89
  [class]="panelClass()"
76
90
  [style]="panelPositionStyle()"
91
+ [accessibility-exclusive-focus]="overlayVisible()"
77
92
  (catchtap)="onPanelTap()"
78
93
  >
79
94
  <view
80
95
  class="items-center pb-3 pt-2 flex justify-center"
81
96
  (catchtouchstart)="onHandleTouchStart($event)"
82
97
  (catchtouchmove)="onHandleTouchMove($event)"
83
- (catchtouchend)="onHandleTouchEnd()"
84
- (catchtouchcancel)="onHandleTouchEnd()"
98
+ (catchtouchend)="onDragEnd()"
99
+ (catchtouchcancel)="onDragEnd()"
100
+ (catchmousedown)="onHandleMouseDown($event)"
85
101
  >
86
102
  <view class="h-1 w-10 rounded-full bg-muted" />
87
103
  </view>
@@ -91,7 +107,7 @@ const BACKDROP_CLEAR_DISTANCE = 500;
91
107
  </overlay>
92
108
  `,
93
109
  })
94
- export class UiBottomSheet {
110
+ export class UiBottomSheet implements OnDestroy {
95
111
  readonly open = model(false);
96
112
  readonly userClass = input<string>('', { alias: 'class' });
97
113
 
@@ -110,11 +126,16 @@ export class UiBottomSheet {
110
126
  : 'position: fixed; overflow: visible; width: 100vw; height: 100vh; top: 0; left: 0; z-index: 999; display: none;',
111
127
  );
112
128
 
113
- readonly backdropRef = viewChild<ElementRef>('backdrop');
114
- readonly panelRef = viewChild<ElementRef>('panel');
129
+ // Required: both are always in the template (the overlay is hidden, never
130
+ // removed), and nothing reads them before the first render.
131
+ readonly backdropRef = viewChild.required<ElementRef>('backdrop');
132
+ readonly panelRef = viewChild.required<ElementRef>('panel');
115
133
  #backdropAnim?: AnimationHandle;
116
134
  #panelAnim?: AnimationHandle;
117
135
  #hasBeenOpen = false;
136
+ // The open and close sequences' pending timers, so ngOnDestroy can stop
137
+ // them.
138
+ readonly #timers = new Set<ReturnType<typeof setTimeout>>();
118
139
 
119
140
  // --- Drag-to-dismiss state ---
120
141
  // Live downward drag distance (px), applied to the panel as a translateY via
@@ -125,22 +146,35 @@ export class UiBottomSheet {
125
146
  // True only between touchstart and touchend on the handle, so unrelated move
126
147
  // events can't shift the sheet.
127
148
  #isDragging = false;
149
+ // Set when a mouse drag that actually moved is released. The browser follows
150
+ // that mouseup with a click on the nearest element under both the mousedown
151
+ // and the mouseup, which is the backdrop whenever the cursor ended up off the
152
+ // panel, and Lynx web maps that click to the backdrop's tap. Without this, a
153
+ // drag released above the panel (where the sheet snaps back) would close it.
154
+ #ignoreNextBackdropTap = false;
155
+ // Whether the current drag is a mouse drag that has moved: only those are
156
+ // followed by a click. A moved touch produces no tap.
157
+ #mouseDragMoved = false;
128
158
 
129
- constructor() {
130
- // `#hasBeenOpen` prevents the close animation from running on the initial
131
- // effect evaluation when `open` starts as false. Without this guard, the
132
- // first run would call #doClose() and emit `closed` before the sheet
133
- // has ever been opened.
134
- effect(() => {
135
- const isOpen = this.open();
136
- if (isOpen) {
137
- this.#hasBeenOpen = true;
138
- this.#doOpen();
139
- } else if (this.#hasBeenOpen) {
140
- this.#doClose();
141
- }
142
- });
143
- }
159
+ /**
160
+ * Opens or closes the sheet as `open` changes. A field rather than a
161
+ * constructor: Angular's test build keeps a function for a constructor that
162
+ * nothing calls, which an app's coverage report would show as uncovered.
163
+ *
164
+ * `#hasBeenOpen` prevents the close animation from running on the initial
165
+ * effect evaluation when `open` starts as false. Without this guard, the
166
+ * first run would call #doClose() and emit `closed` before the sheet
167
+ * has ever been opened.
168
+ */
169
+ protected readonly openOrClose = effect(() => {
170
+ const isOpen = this.open();
171
+ if (isOpen) {
172
+ this.#hasBeenOpen = true;
173
+ this.#doOpen();
174
+ } else if (this.#hasBeenOpen) {
175
+ this.#doClose();
176
+ }
177
+ });
144
178
 
145
179
  protected readonly panelClass = computed(() =>
146
180
  cn(
@@ -184,9 +218,24 @@ export class UiBottomSheet {
184
218
  });
185
219
 
186
220
  protected onBackdropTap(): void {
221
+ if (this.#ignoreNextBackdropTap) {
222
+ this.#ignoreNextBackdropTap = false;
223
+ return;
224
+ }
187
225
  this.open.set(false);
188
226
  }
189
227
 
228
+ /**
229
+ * A fresh press starts a fresh click, so drop any ignore left over from a
230
+ * drag whose release didn't produce a backdrop click (one released over the
231
+ * panel, whose `catchtap` swallows it). Otherwise the next real backdrop tap
232
+ * would be ignored. The handle catches its own mousedown, which is fine: it
233
+ * starts a new drag that resets the flag on release.
234
+ */
235
+ protected onBackdropMouseDown(): void {
236
+ this.#ignoreNextBackdropTap = false;
237
+ }
238
+
190
239
  /**
191
240
  * No-op tap handler for the panel. `catchtap` (vs `bindtap`) already stops
192
241
  * the tap from bubbling to the backdrop — Lynx controls propagation via the
@@ -196,19 +245,14 @@ export class UiBottomSheet {
196
245
  protected onPanelTap(): void {}
197
246
 
198
247
  /**
199
- * Begin a drag on the handle strip. The open slideIn holds the panel at
200
- * translateY(0%) via fill:'forwards', which would override our inline
201
- * transform — cancel it so `dragOffset` (applied through panelPositionStyle)
202
- * actually moves the panel from here on.
248
+ * Begin a drag on the handle strip.
203
249
  */
204
250
  protected onHandleTouchStart(e: {
205
251
  touches?: ReadonlyArray<{ clientY: number }>;
206
252
  }): void {
207
253
  const y = e.touches?.[0]?.clientY;
208
254
  if (y == null) return;
209
- this.#panelAnim?.cancel();
210
- this.#isDragging = true;
211
- this.#dragStartY = y;
255
+ this.#startDrag(y);
212
256
  }
213
257
 
214
258
  protected onHandleTouchMove(e: {
@@ -217,20 +261,53 @@ export class UiBottomSheet {
217
261
  if (!this.#isDragging) return;
218
262
  const y = e.touches?.[0]?.clientY;
219
263
  if (y == null) return;
220
- // Clamp to >= 0: the sheet is already docked at the bottom, so only a
221
- // downward (dismiss-direction) drag should move it.
222
- this.dragOffset.set(Math.max(0, y - this.#dragStartY));
264
+ this.#moveDrag(y);
265
+ }
266
+
267
+ /**
268
+ * The mouse twin of onHandleTouchStart, so the handle drags on web. A
269
+ * desktop browser fires no touch events for a mouse, and Lynx web passes
270
+ * DOM events straight through rather than turning mouse input into touches.
271
+ * On a touch-only device no mouse events fire, so touch keeps driving it.
272
+ *
273
+ * Reads `y`, not `clientY`: Lynx PC reports a mouse event's clientY relative
274
+ * to the element under the cursor, which changes as the cursor moves across
275
+ * the sheet, while `y` is relative to the Lynx view on PC and web alike.
276
+ */
277
+ protected onHandleMouseDown(e: { y: number }): void {
278
+ this.#startDrag(e.y);
223
279
  }
224
280
 
225
281
  /**
226
- * Release the drag. Past the threshold we set `open` to false and let the
282
+ * Bound on the backdrop (see the template), so it also sees every move the
283
+ * cursor makes over an open sheet. Only those during a drag count.
284
+ */
285
+ protected onMouseDragMove(e: { y: number; buttons: number }): void {
286
+ if (!this.#isDragging) return;
287
+ // No button held means the mouseup happened where the sheet couldn't see
288
+ // it: a release outside the Lynx view (past the edge of a docs preview, or
289
+ // outside the browser window) goes to the page, and web-core only listens
290
+ // inside the view. Treat the first move back as the release, or the panel
291
+ // would stay mid-drag.
292
+ if (e.buttons === 0) {
293
+ this.onDragEnd();
294
+ return;
295
+ }
296
+ this.#mouseDragMoved = true;
297
+ this.#moveDrag(e.y);
298
+ }
299
+
300
+ /**
301
+ * Release the drag, from a touchend, touchcancel or mouseup, or a mouse
302
+ * move with no button held. Past the threshold we set `open` to false and let the
227
303
  * effect drive teardown (#doClose → drag-aware #animateOut), which keeps the
228
304
  * two-way `open` binding in sync and reuses the `closed` emit. Otherwise the
229
305
  * sheet springs back to fully open.
230
306
  */
231
- protected onHandleTouchEnd(): void {
307
+ protected onDragEnd(): void {
232
308
  if (!this.#isDragging) return;
233
309
  this.#isDragging = false;
310
+ this.#ignoreNextBackdropTap = this.#mouseDragMoved;
234
311
  if (this.dragOffset() > DISMISS_THRESHOLD) {
235
312
  this.open.set(false);
236
313
  } else {
@@ -238,15 +315,55 @@ export class UiBottomSheet {
238
315
  }
239
316
  }
240
317
 
318
+ /**
319
+ * The open slideIn holds the panel at translateY(0%) via fill:'forwards',
320
+ * which would override our inline transform — cancel it so `dragOffset`
321
+ * (applied through panelPositionStyle) actually moves the panel from here on.
322
+ */
323
+ #startDrag(y: number): void {
324
+ this.#panelAnim?.cancel();
325
+ this.#isDragging = true;
326
+ this.#mouseDragMoved = false;
327
+ this.#dragStartY = y;
328
+ }
329
+
330
+ #moveDrag(y: number): void {
331
+ // Clamp to >= 0: the sheet is already docked at the bottom, so only a
332
+ // downward (dismiss-direction) drag should move it.
333
+ this.dragOffset.set(Math.max(0, y - this.#dragStartY));
334
+ }
335
+
336
+ /**
337
+ * The sheet can be destroyed while its open or close sequence is still
338
+ * running, for example by an `@if` around it or a route change right after
339
+ * closing it. The rest of the sequence then ran on the destroyed component:
340
+ * it animated detached elements and emitted `closed` from a destroyed
341
+ * output, which Angular warns about in dev builds (NG0953).
342
+ */
343
+ ngOnDestroy(): void {
344
+ for (const timer of this.#timers) clearTimeout(timer);
345
+ }
346
+
347
+ /**
348
+ * `setTimeout`, tracked so that ngOnDestroy can cancel it.
349
+ */
350
+ #later(callback: () => void, ms: number): void {
351
+ const timer = setTimeout(() => {
352
+ this.#timers.delete(timer);
353
+ callback();
354
+ }, ms);
355
+ this.#timers.add(timer);
356
+ }
357
+
241
358
  /**
242
359
  * Two-phase open: make the overlay visible first so native elements exist
243
360
  * in the tree, then animate on the next frame. Without this, animate()
244
361
  * targets elements that haven't been flushed to native yet and silently fails.
245
362
  */
246
363
  #doOpen(): void {
247
- setTimeout(() => {
364
+ this.#later(() => {
248
365
  this.overlayVisible.set(true);
249
- setTimeout(() => this.#animateIn(), 0);
366
+ this.#later(() => this.#animateIn(), 0);
250
367
  }, 0);
251
368
  }
252
369
 
@@ -257,9 +374,9 @@ export class UiBottomSheet {
257
374
  * before the fade finishes.
258
375
  */
259
376
  #doClose(): void {
260
- setTimeout(() => {
377
+ this.#later(() => {
261
378
  this.#animateOut();
262
- setTimeout(() => {
379
+ this.#later(() => {
263
380
  this.overlayVisible.set(false);
264
381
  this.closed.emit();
265
382
  // Reset for the next open: the sheet returns to its docked position
@@ -270,14 +387,18 @@ export class UiBottomSheet {
270
387
  }
271
388
 
272
389
  #animateIn(): void {
273
- const backdrop = this.backdropRef()?.nativeElement;
274
- const panel = this.panelRef()?.nativeElement;
275
- if (!backdrop || !panel) return;
390
+ const backdrop = this.backdropRef().nativeElement;
391
+ const panel = this.panelRef().nativeElement;
276
392
 
277
393
  this.#backdropAnim?.cancel();
278
394
  this.#panelAnim?.cancel();
279
395
 
280
396
  this.#backdropAnim = fadeIn(backdrop, { duration: DURATION.normal });
397
+ // Move a screen reader into the panel. It would otherwise stay on whatever
398
+ // opened it, which is now behind the overlay. With the panel's
399
+ // accessibility-exclusive-focus (see the template), it then can't leave
400
+ // the panel until it closes.
401
+ panel.invoke?.('requestAccessibilityFocus');
281
402
  this.#panelAnim = slideIn(panel, 'up', {
282
403
  duration: DURATION.slow,
283
404
  easing: EASING.sheet,
@@ -285,9 +406,8 @@ export class UiBottomSheet {
285
406
  }
286
407
 
287
408
  #animateOut(): void {
288
- const backdrop = this.backdropRef()?.nativeElement;
289
- const panel = this.panelRef()?.nativeElement;
290
- if (!backdrop || !panel) return;
409
+ const backdrop = this.backdropRef().nativeElement;
410
+ const panel = this.panelRef().nativeElement;
291
411
 
292
412
  this.#backdropAnim?.cancel();
293
413
  this.#panelAnim?.cancel();
@@ -321,17 +441,14 @@ export class UiBottomSheet {
321
441
  * continuing smoothly from wherever the finger let go (px → 0px).
322
442
  */
323
443
  #snapBack(fromOffset: number): void {
324
- const panel = this.panelRef()?.nativeElement;
325
444
  this.#panelAnim?.cancel();
326
- if (panel) {
327
- this.#panelAnim = panel.animate(
328
- [
329
- { transform: `translateY(${fromOffset}px)` },
330
- { transform: 'translateY(0px)' },
331
- ],
332
- { duration: DURATION.fast, easing: EASING.spring, fill: 'forwards' },
333
- );
334
- }
445
+ this.#panelAnim = this.panelRef().nativeElement.animate(
446
+ [
447
+ { transform: `translateY(${fromOffset}px)` },
448
+ { transform: 'translateY(0px)' },
449
+ ],
450
+ { duration: DURATION.fast, easing: EASING.spring, fill: 'forwards' },
451
+ );
335
452
  // Match the inline transform to the animation's resting state so the two
336
453
  // agree once fill:'forwards' hands control back to the style binding.
337
454
  this.dragOffset.set(0);