@astryxdesign/core 0.7.8-canary.ffd29c5 → 0.7.8

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 (189) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/dist/BottomSheet/BottomSheetPanel.d.ts +1 -1
  3. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheetPanel.js +30 -14
  5. package/dist/BottomSheet/bottomSheetStack.js +1 -1
  6. package/dist/BottomSheet/useSheetGestures.d.ts +3 -1
  7. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  8. package/dist/BottomSheet/useSheetGestures.js +4 -0
  9. package/dist/CheckboxList/CheckboxList.d.ts +13 -2
  10. package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
  11. package/dist/CheckboxList/CheckboxList.js +10 -4
  12. package/dist/CheckboxList/CheckboxListContext.d.ts +7 -0
  13. package/dist/CheckboxList/CheckboxListContext.d.ts.map +1 -1
  14. package/dist/CheckboxList/CheckboxListContext.js +2 -1
  15. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  16. package/dist/CheckboxList/CheckboxListItem.js +14 -2
  17. package/dist/CheckboxList/requiredGroup.d.ts +22 -0
  18. package/dist/CheckboxList/requiredGroup.d.ts.map +1 -0
  19. package/dist/CheckboxList/requiredGroup.js +52 -0
  20. package/dist/DateInput/PickerWheel.d.ts +1 -1
  21. package/dist/DateInput/PickerWheel.d.ts.map +1 -1
  22. package/dist/DateInput/PickerWheel.js +27 -6
  23. package/dist/DateInput/Wheel.d.ts +2 -2
  24. package/dist/DateInput/Wheel.d.ts.map +1 -1
  25. package/dist/DateInput/Wheel.js +20 -6
  26. package/dist/DateInput/datePickerTouch.d.ts +13 -0
  27. package/dist/DateInput/datePickerTouch.d.ts.map +1 -0
  28. package/dist/DateInput/datePickerTouch.js +14 -0
  29. package/dist/DateInput/wheelOptions.d.ts +22 -2
  30. package/dist/DateInput/wheelOptions.d.ts.map +1 -1
  31. package/dist/DateInput/wheelOptions.js +14 -2
  32. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  33. package/dist/DateTimeInput/TouchDateTimeField.js +62 -119
  34. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  35. package/dist/HoverCard/useHoverCard.js +6 -1
  36. package/dist/Overlay/Overlay.d.ts.map +1 -1
  37. package/dist/Overlay/Overlay.js +7 -2
  38. package/dist/PowerSearch/PowerSearch.d.ts +4 -2
  39. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  40. package/dist/PowerSearch/PowerSearchBottomSheet.d.ts +24 -1
  41. package/dist/PowerSearch/PowerSearchBottomSheet.d.ts.map +1 -1
  42. package/dist/PowerSearch/PowerSearchBottomSheet.js +172 -50
  43. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  44. package/dist/Resizable/ResizeHandle.js +15 -15
  45. package/dist/Resizable/createResizable.d.ts +70 -16
  46. package/dist/Resizable/createResizable.d.ts.map +1 -1
  47. package/dist/Resizable/createResizable.js +180 -22
  48. package/dist/Resizable/separatorAttributes.d.ts +45 -0
  49. package/dist/Resizable/separatorAttributes.d.ts.map +1 -0
  50. package/dist/Resizable/separatorAttributes.js +35 -0
  51. package/dist/Resizable/useResizable.js +2 -2
  52. package/dist/TimeInput/TimeWheels.d.ts +32 -0
  53. package/dist/TimeInput/TimeWheels.d.ts.map +1 -0
  54. package/dist/TimeInput/TimeWheels.js +144 -0
  55. package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
  56. package/dist/TimeInput/TouchTimeField.js +63 -118
  57. package/dist/TimeInput/timeWheelRules.d.ts +124 -0
  58. package/dist/TimeInput/timeWheelRules.d.ts.map +1 -0
  59. package/dist/TimeInput/timeWheelRules.js +277 -0
  60. package/dist/Tokenizer/TokenizerBottomSheet.js +8 -12
  61. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  62. package/dist/Tooltip/useTooltip.js +6 -0
  63. package/dist/astryx.css +3 -1
  64. package/dist/gestures/GestureHost.d.ts.map +1 -1
  65. package/dist/gestures/GestureHost.js +3 -0
  66. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts.map +1 -1
  67. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.js +5 -2
  68. package/dist/i18n/generated-english/Resizable/createResizable.generated.d.ts +8 -0
  69. package/dist/i18n/generated-english/Resizable/createResizable.generated.d.ts.map +1 -0
  70. package/dist/i18n/generated-english/Resizable/createResizable.generated.js +13 -0
  71. package/dist/i18n/generated-english/TimeInput/TimeWheels.generated.d.ts +8 -0
  72. package/dist/i18n/generated-english/TimeInput/TimeWheels.generated.d.ts.map +1 -0
  73. package/dist/i18n/generated-english/TimeInput/TimeWheels.generated.js +17 -0
  74. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  75. package/dist/i18n/generated-locales/en.generated.js +5 -2
  76. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  77. package/dist/i18n/generated-locales/pseudo.generated.js +5 -2
  78. package/dist/keyboard/index.d.ts +17 -0
  79. package/dist/keyboard/index.d.ts.map +1 -0
  80. package/dist/keyboard/index.js +19 -0
  81. package/dist/keyboard/keyboardHeight.d.ts +149 -0
  82. package/dist/keyboard/keyboardHeight.d.ts.map +1 -0
  83. package/dist/keyboard/keyboardHeight.js +432 -0
  84. package/dist/keyboard/keyboardLayoutGuide.d.ts +59 -0
  85. package/dist/keyboard/keyboardLayoutGuide.d.ts.map +1 -0
  86. package/dist/keyboard/keyboardLayoutGuide.js +207 -0
  87. package/dist/keyboard/keyboardModel.d.ts +93 -0
  88. package/dist/keyboard/keyboardModel.d.ts.map +1 -0
  89. package/dist/keyboard/keyboardModel.js +220 -0
  90. package/dist/keyboard/rideKeyboard.d.ts +42 -0
  91. package/dist/keyboard/rideKeyboard.d.ts.map +1 -0
  92. package/dist/keyboard/rideKeyboard.js +77 -0
  93. package/dist/keyboard/useMobileKeyboard.d.ts +61 -0
  94. package/dist/keyboard/useMobileKeyboard.d.ts.map +1 -0
  95. package/dist/keyboard/useMobileKeyboard.js +699 -0
  96. package/dist/motion/android/index.d.ts +16 -0
  97. package/dist/motion/android/index.d.ts.map +1 -0
  98. package/dist/motion/android/index.js +18 -0
  99. package/dist/motion/android/keyboard.d.ts +83 -0
  100. package/dist/motion/android/keyboard.d.ts.map +1 -0
  101. package/dist/motion/android/keyboard.js +96 -0
  102. package/dist/motion/uikit/datePicker.d.ts +58 -0
  103. package/dist/motion/uikit/datePicker.d.ts.map +1 -0
  104. package/dist/motion/uikit/datePicker.js +60 -0
  105. package/dist/motion/uikit/index.d.ts +4 -3
  106. package/dist/motion/uikit/index.d.ts.map +1 -1
  107. package/dist/motion/uikit/index.js +4 -3
  108. package/dist/utils/interactionOverlay.stylex.d.ts +3 -2
  109. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  110. package/dist/utils/interactionOverlay.stylex.js +3 -2
  111. package/dist/utils/layoutViewport.d.ts +25 -0
  112. package/dist/utils/layoutViewport.d.ts.map +1 -0
  113. package/dist/utils/layoutViewport.js +102 -0
  114. package/dist/utils/pressFeedback.d.ts.map +1 -1
  115. package/dist/utils/pressFeedback.js +74 -9
  116. package/dist/utils/pressGesture.d.ts +4 -2
  117. package/dist/utils/pressGesture.d.ts.map +1 -1
  118. package/dist/utils/pressGesture.js +3 -1
  119. package/dist/utils/selectionLock.d.ts +10 -1
  120. package/dist/utils/selectionLock.d.ts.map +1 -1
  121. package/dist/utils/selectionLock.js +31 -5
  122. package/dist/utils/sharedMutationObserver.d.ts +2 -1
  123. package/dist/utils/sharedMutationObserver.d.ts.map +1 -1
  124. package/dist/utils/sharedMutationObserver.js +2 -1
  125. package/dist/utils/timeParser.d.ts +2 -1
  126. package/dist/utils/timeParser.d.ts.map +1 -1
  127. package/dist/utils/timeParser.js +29 -3
  128. package/locales/en.json +21 -9
  129. package/locales/pseudo.json +15 -6
  130. package/package.json +5 -5
  131. package/src/BottomSheet/BottomSheet.doc.mjs +1 -1
  132. package/src/BottomSheet/BottomSheetPanel.tsx +41 -13
  133. package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +5 -0
  134. package/src/BottomSheet/bottomSheetStack.ts +1 -1
  135. package/src/BottomSheet/useSheetGestures.ts +6 -0
  136. package/src/CheckboxList/CheckboxList.doc.mjs +7 -0
  137. package/src/CheckboxList/CheckboxList.tsx +34 -2
  138. package/src/CheckboxList/CheckboxListContext.tsx +9 -1
  139. package/src/CheckboxList/CheckboxListItem.tsx +23 -2
  140. package/src/CheckboxList/requiredGroup.ts +77 -0
  141. package/src/DateInput/PickerWheel.tsx +37 -4
  142. package/src/DateInput/Wheel.tsx +30 -3
  143. package/src/DateInput/datePickerTouch.ts +15 -0
  144. package/src/DateInput/wheelOptions.ts +31 -2
  145. package/src/DateTimeInput/TouchDateTimeField.tsx +94 -178
  146. package/src/HoverCard/useHoverCard.tsx +6 -1
  147. package/src/Overlay/Overlay.tsx +7 -2
  148. package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
  149. package/src/PowerSearch/PowerSearch.tsx +4 -2
  150. package/src/PowerSearch/PowerSearchBottomSheet.tsx +237 -62
  151. package/src/Resizable/ResizeHandle.tsx +18 -26
  152. package/src/Resizable/createResizable.ts +242 -27
  153. package/src/Resizable/separatorAttributes.ts +73 -0
  154. package/src/Resizable/useResizable.ts +2 -2
  155. package/src/TimeInput/TimeWheels.tsx +218 -0
  156. package/src/TimeInput/TouchTimeField.tsx +93 -157
  157. package/src/TimeInput/timeWheelRules.ts +352 -0
  158. package/src/Tokenizer/TokenizerBottomSheet.tsx +8 -12
  159. package/src/Tooltip/useTooltip.tsx +6 -0
  160. package/src/gestures/GestureHost.ts +5 -4
  161. package/src/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.ts +5 -2
  162. package/src/i18n/generated-english/Resizable/createResizable.generated.ts +14 -0
  163. package/src/i18n/generated-english/TimeInput/TimeWheels.generated.ts +18 -0
  164. package/src/i18n/generated-locales/en.generated.ts +6 -4
  165. package/src/i18n/generated-locales/pseudo.generated.ts +6 -4
  166. package/src/keyboard/README.md +197 -0
  167. package/src/keyboard/index.ts +48 -0
  168. package/src/keyboard/keyboardHeight.ts +543 -0
  169. package/src/keyboard/keyboardLayoutGuide.ts +266 -0
  170. package/src/keyboard/keyboardModel.ts +301 -0
  171. package/src/keyboard/rideKeyboard.ts +100 -0
  172. package/src/keyboard/useMobileKeyboard.ts +866 -0
  173. package/src/motion/MEASUREMENTS.md +10 -0
  174. package/src/motion/README.md +2 -0
  175. package/src/motion/android/index.ts +18 -0
  176. package/src/motion/android/keyboard.ts +96 -0
  177. package/src/motion/uikit/datePicker.ts +60 -0
  178. package/src/motion/uikit/index.ts +4 -3
  179. package/src/utils/interactionOverlay.stylex.ts +3 -2
  180. package/src/utils/layoutViewport.ts +104 -0
  181. package/src/utils/pressFeedback.ts +99 -21
  182. package/src/utils/pressGesture.ts +4 -2
  183. package/src/utils/selectionLock.ts +40 -6
  184. package/src/utils/sharedMutationObserver.ts +2 -1
  185. package/src/utils/timeParser.ts +40 -3
  186. package/dist/BottomSheet/useMobileKeyboard.d.ts +0 -21
  187. package/dist/BottomSheet/useMobileKeyboard.d.ts.map +0 -1
  188. package/dist/BottomSheet/useMobileKeyboard.js +0 -413
  189. package/src/BottomSheet/useMobileKeyboard.ts +0 -542
@@ -138,6 +138,66 @@ export function pressFadeMs(element) {
138
138
  const reduced = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
139
139
  return reduced ? Math.min(scaled, PRESS_FADE_MS) : scaled;
140
140
  }
141
+
142
+ /**
143
+ * The release animations that still run for `element`'s press. Each is a
144
+ * finite CSS animation that has not finished. It runs on the element, on its
145
+ * `::before` or `::after`, or on a descendant whose nearest press owner is
146
+ * the element, because a component can draw its release on a part inside the
147
+ * surface. A press owner nested inside plays the release of its own press,
148
+ * so it never extends this one. The list is empty where the engine has no Web
149
+ * Animations.
150
+ */
151
+ function releaseAnimationsOf(element) {
152
+ if (typeof element.getAnimations !== 'function') {
153
+ return [];
154
+ }
155
+ return element.getAnimations({
156
+ subtree: true
157
+ }).filter(animation => {
158
+ // The effect of a CSS animation is a KeyframeEffect. Its target is the
159
+ // element, also when the animation runs on a pseudo-element.
160
+ const effect = animation.effect;
161
+ const target = effect?.target ?? null;
162
+ if (effect == null || target == null || !('animationName' in animation) || target !== element && target.closest(`[${PRESSABLE_ATTRIBUTE}]`) !== element) {
163
+ return false;
164
+ }
165
+ if (!Number.isFinite(Number(effect.getComputedTiming().endTime))) {
166
+ return false;
167
+ }
168
+ return animation.playState !== 'finished';
169
+ });
170
+ }
171
+
172
+ /**
173
+ * Calls `end` when the release on `element` is over. A busy main thread
174
+ * delays the first frame of the release animation, and with it the end. So a
175
+ * clock that starts at the `fading` write can cut the release short. When
176
+ * `clockMs` runs out, the release waits for each release animation that still
177
+ * runs or still waits for its first frame. It ends when those finish or are
178
+ * cancelled, as when the surface is unmounted or restyled. With no release
179
+ * animation (an instant release, a surface without one, an engine without Web
180
+ * Animations), the release ends at the clock. Returns a cancel.
181
+ */
182
+ function endAfterRelease(element, clockMs, end) {
183
+ let waiting = true;
184
+ const timer = setTimeout(() => {
185
+ const running = releaseAnimationsOf(element);
186
+ if (running.length === 0) {
187
+ end();
188
+ return;
189
+ }
190
+ void Promise.allSettled(running.map(async animation => animation.finished)).then(() => {
191
+ if (waiting) {
192
+ end();
193
+ }
194
+ });
195
+ }, clockMs);
196
+ return () => {
197
+ waiting = false;
198
+ clearTimeout(timer);
199
+ };
200
+ }
141
201
  const TIMER_EVENT = {
142
202
  fade: 'fadeEnd',
143
203
  flash: 'flashEnd',
@@ -152,7 +212,7 @@ function paintElement(element, paint) {
152
212
  }
153
213
  function clearTimer(slot) {
154
214
  if (slot.live?.timer != null) {
155
- clearTimeout(slot.live.timer);
215
+ slot.live.timer();
156
216
  slot.live.timer = null;
157
217
  }
158
218
  }
@@ -188,20 +248,25 @@ function apply(slot, step) {
188
248
  }
189
249
  if (step.timer != null) {
190
250
  const kind = step.timer;
191
- // A TIMER, never `requestAnimationFrame`: iOS throttles frame callbacks
192
- // while a scroll runs, so a frame-based delay would fire late, after the
193
- // scroll, and paint a control nobody is pressing any more.
194
- live.timer = setTimeout(() => {
251
+ const fire = () => {
195
252
  if (slot.live != null) {
196
253
  slot.live.timer = null;
197
254
  }
198
255
  apply(slot, pressGestureStep(slot.live?.gesture ?? idlePressGesture(), {
199
256
  kind: TIMER_EVENT[kind]
200
257
  }));
201
- },
202
- // The release fade follows the theme's tempo (AST-108). The onset delay
203
- // and the tap flash do not.
204
- kind === 'fade' ? pressFadeMs(live.element) : TIMER_MS[kind]);
258
+ };
259
+ if (kind === 'fade') {
260
+ // The release fade follows the theme's tempo (AST-108). It ends when
261
+ // its animation ends, however late a busy main thread let it start.
262
+ live.timer = endAfterRelease(live.element, pressFadeMs(live.element), fire);
263
+ } else {
264
+ // A TIMER, never `requestAnimationFrame`: iOS throttles frame callbacks
265
+ // while a scroll runs, so a frame-based delay would fire late, after the
266
+ // scroll, and paint a control nobody is pressing any more.
267
+ const timer = setTimeout(fire, TIMER_MS[kind]);
268
+ live.timer = () => clearTimeout(timer);
269
+ }
205
270
  }
206
271
  }
207
272
 
@@ -31,7 +31,9 @@ export declare const PRESS_FLASH_MS = 100;
31
31
  * How long the paint takes to leave after a release: UIKit's deselect
32
32
  * crossfade. The one number, shared with the stylesheet: the release
33
33
  * animation in interactionOverlay.stylex.ts runs on the same constant, so the
34
- * timer that removes the attribute and the fade cannot drift apart.
34
+ * controller's release clock and the fade cannot drift apart. When the clock
35
+ * runs out, the controller waits for a release animation that still runs,
36
+ * then removes the attribute.
35
37
  */
36
38
  export declare const PRESS_FADE_MS: number;
37
39
  /**
@@ -98,7 +100,7 @@ export type PressEvent = {
98
100
  | {
99
101
  kind: 'flashEnd';
100
102
  }
101
- /** {@link PRESS_FADE_MS} elapsed; the paint is gone. */
103
+ /** The release is over: its animation ended, or its clock ran out with none running. */
102
104
  | {
103
105
  kind: 'fadeEnd';
104
106
  };
@@ -1 +1 @@
1
- {"version":3,"file":"pressGesture.d.ts","sourceRoot":"","sources":["../../src/utils/pressGesture.ts"],"names":[],"mappings":"AA4BA;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,MAAM,CAAC;AAElC;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,KAAK,CAAC;AAEhC;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,MAAM,CAAC;AAElC;;;;;GAKG;AACH,eAAO,MAAM,aAAa,EAAE,MAA8B,CAAC;AAE3D;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAEzC,2EAA2E;AAC3E,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,IAAI,GAAG,QAAQ,CAAC;AAElD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,UAAU,GACpB,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,YAAY,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE5E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,sDAAsD;IACtD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,UAAU,GAClB;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAC;AACtC;;;;GAIG;GACD;IAAC,IAAI,EAAE,YAAY,CAAA;CAAC;AACtB,0EAA0E;GACxE;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAC,GACpC;IAAC,IAAI,EAAE,IAAI,CAAA;CAAC;AACd,4FAA4F;GAC1F;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAC;AAClB,iEAAiE;GAC/D;IAAC,IAAI,EAAE,OAAO,CAAA;CAAC;AACjB,yDAAyD;GACvD;IAAC,IAAI,EAAE,UAAU,CAAA;CAAC;AACpB,wDAAwD;GACtD;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,CAAC;AAEtB,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B;;;;;;;;;OASG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,IAAI,CAAC;CACnD;AAkBD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,YAAY,EACrB,KAAK,EAAE,UAAU,GAChB,SAAS,CA6GX;AAED,wBAAgB,gBAAgB,IAAI,YAAY,CAE/C"}
1
+ {"version":3,"file":"pressGesture.d.ts","sourceRoot":"","sources":["../../src/utils/pressGesture.ts"],"names":[],"mappings":"AA4BA;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,MAAM,CAAC;AAElC;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,KAAK,CAAC;AAEhC;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,MAAM,CAAC;AAElC;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,EAAE,MAA8B,CAAC;AAE3D;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAEzC,2EAA2E;AAC3E,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,IAAI,GAAG,QAAQ,CAAC;AAElD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,UAAU,GACpB,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,YAAY,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE5E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,sDAAsD;IACtD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,UAAU,GAClB;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAC;AACtC;;;;GAIG;GACD;IAAC,IAAI,EAAE,YAAY,CAAA;CAAC;AACtB,0EAA0E;GACxE;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAC,GACpC;IAAC,IAAI,EAAE,IAAI,CAAA;CAAC;AACd,4FAA4F;GAC1F;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAC;AAClB,iEAAiE;GAC/D;IAAC,IAAI,EAAE,OAAO,CAAA;CAAC;AACjB,yDAAyD;GACvD;IAAC,IAAI,EAAE,UAAU,CAAA;CAAC;AACpB,wFAAwF;GACtF;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,CAAC;AAEtB,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B;;;;;;;;;OASG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,IAAI,CAAC;CACnD;AAkBD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,YAAY,EACrB,KAAK,EAAE,UAAU,GAChB,SAAS,CA6GX;AAED,wBAAgB,gBAAgB,IAAI,YAAY,CAE/C"}
@@ -62,7 +62,9 @@ export const PRESS_FLASH_MS = 100;
62
62
  * How long the paint takes to leave after a release: UIKit's deselect
63
63
  * crossfade. The one number, shared with the stylesheet: the release
64
64
  * animation in interactionOverlay.stylex.ts runs on the same constant, so the
65
- * timer that removes the attribute and the fade cannot drift apart.
65
+ * controller's release clock and the fade cannot drift apart. When the clock
66
+ * runs out, the controller waits for a release animation that still runs,
67
+ * then removes the attribute.
66
68
  */
67
69
  export const PRESS_FADE_MS = pressConsts.releaseMs;
68
70
 
@@ -19,6 +19,15 @@ export interface DragSelectionReleaseOptions {
19
19
  */
20
20
  isCancel?: boolean;
21
21
  }
22
+ /** A drag's selection hold: call it to release the hold. */
23
+ export interface DragSelectionHold {
24
+ (options?: DragSelectionReleaseOptions): void;
25
+ /**
26
+ * Called after the lift's own handlers ran. A selection they made since
27
+ * the release stays: the hold then puts it back, not the press's.
28
+ */
29
+ keepLiftSelection(): void;
30
+ }
22
31
  /**
23
32
  * Refuse a new selection while a mouse or pen drag that a recognizer claimed
24
33
  * runs, until the returned function runs. `kept` is the page's selection
@@ -38,7 +47,7 @@ export interface DragSelectionReleaseOptions {
38
47
  * follows a cancel. `onReleased` runs once the document's hold has ended.
39
48
  * The page listeners exist only during a press. Idempotent.
40
49
  */
41
- export declare function lockDragSelection(elements: Iterable<Element>, view: Window | null | undefined, kept: ReadonlyArray<AbstractRange>, onReleased?: () => void): (options?: DragSelectionReleaseOptions) => void;
50
+ export declare function lockDragSelection(elements: Iterable<Element>, view: Window | null | undefined, kept: ReadonlyArray<AbstractRange>, onReleased?: () => void): DragSelectionHold;
42
51
  /**
43
52
  * The page's selection now, its ranges copied; none for a collapsed or empty
44
53
  * one. A drag's hold keeps what this read when the press began.
@@ -1 +1 @@
1
- {"version":3,"file":"selectionLock.d.ts","sourceRoot":"","sources":["../../src/utils/selectionLock.ts"],"names":[],"mappings":"AAoDA;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAE9D;AAWD;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,EAC3B,MAAM,EAAE,WAAW,GAAG,IAAI,GACzB,MAAM,IAAI,CA4BZ;AAED,mEAAmE;AACnE,MAAM,WAAW,2BAA2B;IAC1C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,EAC3B,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC/B,IAAI,EAAE,aAAa,CAAC,aAAa,CAAC,EAClC,UAAU,CAAC,EAAE,MAAM,IAAI,GACtB,CAAC,OAAO,CAAC,EAAE,2BAA2B,KAAK,IAAI,CA+EjD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC9B,aAAa,CAAC,aAAa,CAAC,CAiB9B;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,QAAQ,EACb,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,MAAM,EAAE,WAAW,GAAG,IAAI,GACzB,OAAO,CA+CT"}
1
+ {"version":3,"file":"selectionLock.d.ts","sourceRoot":"","sources":["../../src/utils/selectionLock.ts"],"names":[],"mappings":"AAoDA;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAE9D;AAWD;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,EAC3B,MAAM,EAAE,WAAW,GAAG,IAAI,GACzB,MAAM,IAAI,CA4BZ;AAED,mEAAmE;AACnE,MAAM,WAAW,2BAA2B;IAC1C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,4DAA4D;AAC5D,MAAM,WAAW,iBAAiB;IAChC,CAAC,OAAO,CAAC,EAAE,2BAA2B,GAAG,IAAI,CAAC;IAC9C;;;OAGG;IACH,iBAAiB,IAAI,IAAI,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,EAC3B,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC/B,IAAI,EAAE,aAAa,CAAC,aAAa,CAAC,EAClC,UAAU,CAAC,EAAE,MAAM,IAAI,GACtB,iBAAiB,CAuGnB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC9B,aAAa,CAAC,aAAa,CAAC,CAiB9B;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,QAAQ,EACb,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,MAAM,EAAE,WAAW,GAAG,IAAI,GACzB,OAAO,CA+CT"}
@@ -84,6 +84,8 @@ export function lockSelection(elements, target) {
84
84
 
85
85
  /** How a drag's selection hold ends: at a lift, or at a cancel. */
86
86
 
87
+ /** A drag's selection hold: call it to release the hold. */
88
+
87
89
  /**
88
90
  * Refuse a new selection while a mouse or pen drag that a recognizer claimed
89
91
  * runs, until the returned function runs. `kept` is the page's selection
@@ -105,7 +107,11 @@ export function lockSelection(elements, target) {
105
107
  */
106
108
  export function lockDragSelection(elements, view, kept, onReleased) {
107
109
  const doc = view?.document ?? null;
108
- const roots = shadowRootsOfRanges(kept);
110
+ let keptRanges = kept;
111
+ let roots = shadowRootsOfRanges(keptRanges);
112
+ // The selection when the lift released the hold, until the lift's
113
+ // handlers ran.
114
+ let atLift = null;
109
115
  const restore = () => {
110
116
  const selection = view?.getSelection();
111
117
  // The focused field keeps its own selection (a drag never starts in one).
@@ -113,7 +119,7 @@ export function lockDragSelection(elements, view, kept, onReleased) {
113
119
  return;
114
120
  }
115
121
  const held = heldRanges(selection, roots);
116
- if (kept.length === 0) {
122
+ if (keptRanges.length === 0) {
117
123
  if (held.some(range => !range.collapsed)) {
118
124
  selection.removeAllRanges();
119
125
  }
@@ -121,10 +127,10 @@ export function lockDragSelection(elements, view, kept, onReleased) {
121
127
  }
122
128
  // A render can remove what the kept selection held: then there is
123
129
  // nothing to put back.
124
- if (sameRanges(held, kept) || !kept.every(isValidRange)) {
130
+ if (sameRanges(held, keptRanges) || !keptRanges.every(isValidRange)) {
125
131
  return;
126
132
  }
127
- selectRanges(selection, kept);
133
+ selectRanges(selection, keptRanges);
128
134
  };
129
135
  restore();
130
136
  const set = new Set(elements);
@@ -160,11 +166,12 @@ export function lockDragSelection(elements, view, kept, onReleased) {
160
166
  onReleased?.();
161
167
  }
162
168
  };
163
- return ({
169
+ const release = ({
164
170
  isCancel = false
165
171
  } = {}) => {
166
172
  if (stage === 'held') {
167
173
  stage = 'lifted';
174
+ atLift = selectionRangesOf(view);
168
175
  for (const element of set) {
169
176
  element.removeEventListener('selectstart', refuse);
170
177
  }
@@ -180,6 +187,25 @@ export function lockDragSelection(elements, view, kept, onReleased) {
180
187
  releaseDocument();
181
188
  }
182
189
  };
190
+ release.keepLiftSelection = () => {
191
+ if (stage !== 'lifted' || atLift == null) {
192
+ return;
193
+ }
194
+ const before = atLift;
195
+ atLift = null;
196
+ const keepIfChanged = () => {
197
+ const now = selectionRangesOf(view);
198
+ if (stage !== 'done' && !sameRanges(now, before)) {
199
+ keptRanges = now;
200
+ roots = shadowRootsOfRanges(now);
201
+ }
202
+ };
203
+ keepIfChanged();
204
+ // A click's state update renders in a microtask, and its layout effect
205
+ // may select then. That still runs before the press's own mouseup.
206
+ queueMicrotask(keepIfChanged);
207
+ };
208
+ return release;
183
209
  }
184
210
 
185
211
  /**
@@ -4,7 +4,8 @@
4
4
  * @output Exports observeMutations (one shared observer per set of options)
5
5
  * and observeContent, its text-and-children form
6
6
  * @position Internal utility; consumed by useTruncation, hitArea, AppShell's
7
- * main-overflow observer and ModalAnnounceProvider's presentation watch. The mutation counterpart of sharedResizeObserver:
7
+ * main-overflow observer, ModalAnnounceProvider's presentation watch and
8
+ * CheckboxList's required group. The mutation counterpart of sharedResizeObserver:
8
9
  * one observer for every element observed with the same options, so a
9
10
  * repeated cell, row or region does not create one observer per instance.
10
11
  */
@@ -1 +1 @@
1
- {"version":3,"file":"sharedMutationObserver.d.ts","sourceRoot":"","sources":["../../src/utils/sharedMutationObserver.ts"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AAEH,KAAK,gBAAgB,GAAG,MAAM,IAAI,CAAC;AA4DnC;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,IAAI,EACb,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,gBAAgB,GACzB,MAAM,IAAI,CAmCZ;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,gBAAgB,GACzB,MAAM,IAAI,CAEZ"}
1
+ {"version":3,"file":"sharedMutationObserver.d.ts","sourceRoot":"","sources":["../../src/utils/sharedMutationObserver.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,KAAK,gBAAgB,GAAG,MAAM,IAAI,CAAC;AA4DnC;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,IAAI,EACb,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,gBAAgB,GACzB,MAAM,IAAI,CAmCZ;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,gBAAgB,GACzB,MAAM,IAAI,CAEZ"}
@@ -8,7 +8,8 @@
8
8
  * @output Exports observeMutations (one shared observer per set of options)
9
9
  * and observeContent, its text-and-children form
10
10
  * @position Internal utility; consumed by useTruncation, hitArea, AppShell's
11
- * main-overflow observer and ModalAnnounceProvider's presentation watch. The mutation counterpart of sharedResizeObserver:
11
+ * main-overflow observer, ModalAnnounceProvider's presentation watch and
12
+ * CheckboxList's required group. The mutation counterpart of sharedResizeObserver:
12
13
  * one observer for every element observed with the same options, so a
13
14
  * repeated cell, row or region does not create one observer per instance.
14
15
  */
@@ -65,7 +65,8 @@ export declare function compareTime(a: ISOTimeString | undefined, b: ISOTimeStri
65
65
  */
66
66
  export declare function isTimeInRange(time: ISOTimeString, min?: ISOTimeString, max?: ISOTimeString): boolean;
67
67
  /**
68
- * Clamps a time to a min/max range.
68
+ * Clamps a time to a min/max range. Without seconds, a bound that falls
69
+ * between whole minutes gives the nearest whole minute inside the range.
69
70
  */
70
71
  export declare function clampTime(time: ISOTimeString, min?: ISOTimeString, max?: ISOTimeString, includeSeconds?: boolean): ISOTimeString;
71
72
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"timeParser.d.ts","sourceRoot":"","sources":["../../src/utils/timeParser.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG;IAAC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAA;CAAC,CAAC;AAEzE;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAMvE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CA6B5D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,UAAU,EAChB,cAAc,GAAE,OAAe,GAC9B,aAAa,CAUf;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,aAAa,GAAG,SAAS,EAC/B,cAAc,GAAE,OAAe,GAC9B,MAAM,CAqBR;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,aAAa,GAAG,SAAS,EAC/B,cAAc,GAAE,OAAe,GAC9B,MAAM,CAmBR;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,cAAc,GAAE,OAAe,GAC9B,aAAa,GAAG,IAAI,CA6HtB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,CAAC,EAAE,aAAa,GAAG,SAAS,EAC5B,CAAC,EAAE,aAAa,GAAG,SAAS,GAC3B,MAAM,CA4BR;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,GAClB,OAAO,CAQT;AAED;;GAEG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,EACnB,cAAc,GAAE,OAAe,GAC9B,aAAa,CAoBf;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,aAAa,EACnB,YAAY,EAAE,MAAM,EACpB,cAAc,GAAE,OAAe,GAC9B,aAAa,CA2Bf"}
1
+ {"version":3,"file":"timeParser.d.ts","sourceRoot":"","sources":["../../src/utils/timeParser.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG;IAAC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAA;CAAC,CAAC;AAEzE;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAMvE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CA6B5D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,UAAU,EAChB,cAAc,GAAE,OAAe,GAC9B,aAAa,CAUf;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,aAAa,GAAG,SAAS,EAC/B,cAAc,GAAE,OAAe,GAC9B,MAAM,CAqBR;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,aAAa,GAAG,SAAS,EAC/B,cAAc,GAAE,OAAe,GAC9B,MAAM,CAmBR;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,cAAc,GAAE,OAAe,GAC9B,aAAa,GAAG,IAAI,CA6HtB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,CAAC,EAAE,aAAa,GAAG,SAAS,EAC5B,CAAC,EAAE,aAAa,GAAG,SAAS,GAC3B,MAAM,CA4BR;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,GAClB,OAAO,CAQT;AAED;;;GAGG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,EACnB,cAAc,GAAE,OAAe,GAC9B,aAAa,CA0Bf;AAgCD;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,aAAa,EACnB,YAAY,EAAE,MAAM,EACpB,cAAc,GAAE,OAAe,GAC9B,aAAa,CA2Bf"}
@@ -289,7 +289,8 @@ export function isTimeInRange(time, min, max) {
289
289
  }
290
290
 
291
291
  /**
292
- * Clamps a time to a min/max range.
292
+ * Clamps a time to a min/max range. Without seconds, a bound that falls
293
+ * between whole minutes gives the nearest whole minute inside the range.
293
294
  */
294
295
  export function clampTime(time, min, max, includeSeconds = false) {
295
296
  const parsed = parseISOTime(time);
@@ -299,18 +300,43 @@ export function clampTime(time, min, max, includeSeconds = false) {
299
300
  if (min && compareTime(time, min) < 0) {
300
301
  const parsedMin = parseISOTime(min);
301
302
  if (parsedMin) {
302
- return formatISOTime(parsedMin, includeSeconds);
303
+ return formatISOTime(onWholeMinute(parsedMin, 1, includeSeconds, max), includeSeconds);
303
304
  }
304
305
  }
305
306
  if (max && compareTime(time, max) > 0) {
306
307
  const parsedMax = parseISOTime(max);
307
308
  if (parsedMax) {
308
- return formatISOTime(parsedMax, includeSeconds);
309
+ return formatISOTime(onWholeMinute(parsedMax, -1, includeSeconds, min), includeSeconds);
309
310
  }
310
311
  }
311
312
  return time;
312
313
  }
313
314
 
315
+ /**
316
+ * A bound as a time without seconds reaches it: the next whole minute up from
317
+ * a minimum, or the whole minute down from a maximum. With no whole minute
318
+ * before the other bound, the bound stays as it is.
319
+ */
320
+ function onWholeMinute(bound, direction, includeSeconds, other) {
321
+ if (includeSeconds || bound.second === 0) {
322
+ return bound;
323
+ }
324
+ const minutes = bound.hour * 60 + bound.minute + (direction === 1 ? 1 : 0);
325
+ if (minutes > 23 * 60 + 59) {
326
+ return bound;
327
+ }
328
+ const rounded = {
329
+ hour: Math.floor(minutes / 60),
330
+ minute: minutes % 60,
331
+ second: 0
332
+ };
333
+ const order = compareTime(formatISOTime(rounded, true), other);
334
+ if (other && (direction === 1 ? order > 0 : order < 0)) {
335
+ return bound;
336
+ }
337
+ return rounded;
338
+ }
339
+
314
340
  /**
315
341
  * Adjusts a time by adding/subtracting minutes.
316
342
  *
package/locales/en.json CHANGED
@@ -267,6 +267,10 @@
267
267
  "defaultMessage": "No filters applied",
268
268
  "description": "Heading of the PowerSearch Filters sheet empty state when no filter is active (experimental bottom-sheet presentation). Neutral tone."
269
269
  },
270
+ "@astryx.powersearch.sheet.noFiltersInDraft": {
271
+ "defaultMessage": "No filters",
272
+ "description": "Heading of the PowerSearch Filters sheet empty state while the sheet's draft is empty but the field still has applied filters, which Done replaces. Must not say the filters are applied or removed yet. Neutral tone."
273
+ },
270
274
  "@astryx.powersearch.sheet.noFiltersDescription": {
271
275
  "defaultMessage": "Add a filter to narrow your results and find what you need faster.",
272
276
  "description": "Guidance line under the PowerSearch Filters sheet empty state, above its Add filter action. One short sentence."
@@ -293,7 +297,23 @@
293
297
  },
294
298
  "@astryx.powersearch.sheet.done": {
295
299
  "defaultMessage": "Done",
296
- "description": "Primary button label in the PowerSearch Filters sheet footer that closes the sheet. Short; marks the end of the filtering task."
300
+ "description": "Primary button label in the PowerSearch Filters sheet footer that commits the sheet's filter draft and closes the sheet. Short; marks the end of the filtering task."
301
+ },
302
+ "@astryx.powersearch.sheet.discardTitle": {
303
+ "defaultMessage": "Discard changes?",
304
+ "description": "Title of the confirmation dialog shown when someone tries to dismiss the PowerSearch filter sheet after changing its filter draft. A question."
305
+ },
306
+ "@astryx.powersearch.sheet.discardDescription": {
307
+ "defaultMessage": "Your changes to the filters haven't been applied.",
308
+ "description": "Body of the dirty-dismiss confirmation inside the PowerSearch filter sheet. Done applies the draft; a dismissal would drop it."
309
+ },
310
+ "@astryx.powersearch.sheet.keepEditing": {
311
+ "defaultMessage": "Keep editing",
312
+ "description": "Label of the safe cancel action in the PowerSearch dirty-dismiss confirmation. It closes the confirmation and returns to the unchanged draft."
313
+ },
314
+ "@astryx.powersearch.sheet.discardConfirm": {
315
+ "defaultMessage": "Discard changes",
316
+ "description": "Label of the destructive action in the PowerSearch dirty-dismiss confirmation. It closes the sheet without applying its filter draft."
297
317
  },
298
318
  "@astryx.powersearch.sheet.search": {
299
319
  "defaultMessage": "Search",
@@ -367,14 +387,6 @@
367
387
  "defaultMessage": "{label} {value}",
368
388
  "description": "A Filters row's \"field operator\" text joined with its value, as the removal announcement reads it (for example \"Status is Open\"). Translators may reorder the two parts."
369
389
  },
370
- "@astryx.powersearch.sheet.filtersClearedWithCount": {
371
- "defaultMessage": "All filters removed. {resultCount}",
372
- "description": "Polite screen-reader announcement after Clear all in the PowerSearch touch sheet when the result count changed with it; resultCount is the field's already formatted result count (for example \"240 results\"). Live-region text, never shown on screen."
373
- },
374
- "@astryx.powersearch.sheet.editableFiltersClearedWithCount": {
375
- "defaultMessage": "Editable filters removed. {resultCount}",
376
- "description": "Polite screen-reader announcement after Clear all in the PowerSearch touch sheet removes the editable filters, keeps read-only ones, and the result count changed with it; resultCount is the field's already formatted result count (for example \"240 results\"). Live-region text, never shown on screen."
377
- },
378
390
  "@astryx.alertDialog.cancel": {
379
391
  "defaultMessage": "Cancel",
380
392
  "description": "Button label on the secondary/dismiss button of an AlertDialog (modal confirmation). Imperative verb; consumers usually override with task-specific text."
@@ -200,6 +200,9 @@
200
200
  "@astryx.powersearch.sheet.noFilters": {
201
201
  "defaultMessage": "⟦Ñó ƒíłţéřš àþþłíéð⟧"
202
202
  },
203
+ "@astryx.powersearch.sheet.noFiltersInDraft": {
204
+ "defaultMessage": "⟦Ñó ƒíłţéřš⟧"
205
+ },
203
206
  "@astryx.powersearch.sheet.noFiltersDescription": {
204
207
  "defaultMessage": "⟦Àðð à ƒíłţéř ţó ñàřřóŵ ýóúř řéšúłţš àñð ƒíñð ŵĥàţ ýóú ñééð ƒàšţéř.⟧"
205
208
  },
@@ -221,6 +224,18 @@
221
224
  "@astryx.powersearch.sheet.done": {
222
225
  "defaultMessage": "⟦Ðóñé⟧"
223
226
  },
227
+ "@astryx.powersearch.sheet.discardTitle": {
228
+ "defaultMessage": "⟦Ðíšçàřð çĥàñĝéš?⟧"
229
+ },
230
+ "@astryx.powersearch.sheet.discardDescription": {
231
+ "defaultMessage": "⟦Ýóúř çĥàñĝéš ţó ţĥé ƒíłţéřš ĥàṽéñ'ţ ƀééñ àþþłíéð.⟧"
232
+ },
233
+ "@astryx.powersearch.sheet.keepEditing": {
234
+ "defaultMessage": "⟦Ķééþ éðíţíñĝ⟧"
235
+ },
236
+ "@astryx.powersearch.sheet.discardConfirm": {
237
+ "defaultMessage": "⟦Ðíšçàřð çĥàñĝéš⟧"
238
+ },
224
239
  "@astryx.powersearch.sheet.search": {
225
240
  "defaultMessage": "⟦Šéàřçĥ⟧"
226
241
  },
@@ -275,12 +290,6 @@
275
290
  "@astryx.powersearch.sheet.labelValue": {
276
291
  "defaultMessage": "⟦{label} {value}⟧"
277
292
  },
278
- "@astryx.powersearch.sheet.filtersClearedWithCount": {
279
- "defaultMessage": "⟦Àłł ƒíłţéřš řéɱóṽéð. {resultCount}⟧"
280
- },
281
- "@astryx.powersearch.sheet.editableFiltersClearedWithCount": {
282
- "defaultMessage": "⟦Éðíţàƀłé ƒíłţéřš řéɱóṽéð. {resultCount}⟧"
283
- },
284
293
  "@astryx.alertDialog.cancel": {
285
294
  "defaultMessage": "⟦Çàñçéł⟧"
286
295
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.7.8-canary.ffd29c5",
3
+ "version": "0.7.8",
4
4
  "displayName": "Astryx Core",
5
5
  "description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
6
6
  "author": "Meta Open Source",
@@ -870,9 +870,6 @@
870
870
  "react-dom": ">=19.0.0"
871
871
  },
872
872
  "devDependencies": {
873
- "@astryxdesign/a11y-spec": "0.7.8-canary.ffd29c5",
874
- "@astryxdesign/cli": "0.7.8-canary.ffd29c5",
875
- "@astryxdesign/interaction-harness": "0.7.8-canary.ffd29c5",
876
873
  "@babel/cli": "^7.29.7",
877
874
  "@babel/core": "^7.29.7",
878
875
  "@babel/preset-react": "^7.29.7",
@@ -883,7 +880,10 @@
883
880
  "@testing-library/jest-dom": "^6.6.0",
884
881
  "@testing-library/react": "^16.3.2",
885
882
  "@types/babel__core": "^7.20.5",
886
- "tailwindcss": "^4.3.2"
883
+ "tailwindcss": "^4.3.2",
884
+ "@astryxdesign/cli": "0.7.8",
885
+ "@astryxdesign/interaction-harness": "0.0.0",
886
+ "@astryxdesign/a11y-spec": "0.0.0"
887
887
  },
888
888
  "dependencies": {
889
889
  "intl-messageformat": "^11.2.9"
@@ -152,7 +152,7 @@ export const docs = {
152
152
  name: 'height',
153
153
  type: "'hug' | 'capped' | 'tall' | number | string",
154
154
  description:
155
- "How tall the sheet is. Named budgets: 'hug' fits its content up to 92% of the viewport, 'capped' is a scrolling mid-height panel (~62%), and 'tall' is a pinned near-full panel (~92%) for content that streams in. Or pass a number (px) / CSS length for a custom budget. Give snapPoints to let the user drag between heights. On shorter viewports the sheet fills the available height. Only a fully expanded 'tall' sheet provides mobile-keyboard accommodation: it stays put and scrolls each focused control above the keyboard. Hug, Capped, numeric and CSS-length heights never do, and a Tall sheet stops doing it the moment the user drags it to a shorter detent, resuming when they drag it back. Outside that state the sheet neither moves nor adds keyboard scroll space, and the browser's own focus reveal is left in place; on iOS that reveal can shift the whole page.",
155
+ "How tall the sheet is. Named budgets: 'hug' fits its content up to 92% of the viewport, 'capped' is a scrolling mid-height panel (~62%), and 'tall' is a pinned near-full panel (~92%) for content that streams in. Or pass a number (px) / CSS length for a custom budget. Give snapPoints to let the user drag between heights. On shorter viewports the sheet fills the available height. Only a fully expanded 'tall' sheet provides mobile-keyboard accommodation: it stays put and scrolls each focused control above the keyboard. Hug, Capped, numeric and CSS-length heights never do, and a Tall sheet stops doing it the moment the user drags it to a shorter detent, resuming when they drag it back. Outside that state the sheet neither moves nor adds keyboard scroll space, and the browser's own focus reveal is left in place; on iOS that reveal can shift the whole page. This accommodation applies where the visual viewport shrinks for the keyboard, as in Safari on iOS, Firefox for Android and Chrome on Android, where the sheet places the focused control just above the keyboard as it comes up, and the control rides the rest of Gboard\u2019s slide up, and the slide down after it loses focus; a hide Chrome reports late, such as Back, rides down from Chrome\u2019s report (`astryx docs browser-support`).",
156
156
  default: "'capped'",
157
157
  },
158
158
  {
@@ -14,7 +14,7 @@
14
14
  * - /packages/core/src/BottomSheet/BottomSheet.tsx
15
15
  * - /packages/core/src/BottomSheet/BottomSheetPanel.test.tsx
16
16
  * - /packages/core/src/BottomSheet/snapOffsets.ts
17
- * - /packages/core/src/BottomSheet/useMobileKeyboard.ts
17
+ * - /packages/core/src/keyboard/useMobileKeyboard.ts
18
18
  * - /packages/core/src/BottomSheet/useSheetGestures.ts
19
19
  * - /packages/core/src/Layout/container.stylex.ts (bottom-sheet padding chain)
20
20
  */
@@ -66,7 +66,7 @@ import {
66
66
  resolveSnapPoints,
67
67
  type BottomSheetSnapPoint,
68
68
  } from './snapOffsets';
69
- import {useMobileKeyboard} from './useMobileKeyboard';
69
+ import {useMobileKeyboard} from '../keyboard/useMobileKeyboard';
70
70
  import {
71
71
  scrollPreservationPadding,
72
72
  sheetLayoutHeight,
@@ -316,13 +316,29 @@ const styles = stylex.create({
316
316
  },
317
317
  tallKeyboardBody: {
318
318
  scrollPaddingBlockEnd: MOBILE_KEYBOARD_BOTTOM_CLEARANCE,
319
+ },
320
+ // The inset the keyboard takes at the end of a Tall sheet's content
321
+ // (useMobileKeyboard), so its last control can scroll above the keyboard.
322
+ // It is the content box's last in-flow box, after the content's own
323
+ // children, not a box after the content. Firefox resolves the content's
324
+ // `min-height: min-content` as `auto` in the block axis. The content box
325
+ // then keeps the body's height while its children overflow it. An inset
326
+ // after that box sat inside the overflow and added no scroll range. In
327
+ // Firefox for Android, the body's scrollHeight was 1491 with and without a
328
+ // 357 px inset.
329
+ tallKeyboardContent: {
319
330
  '::after': {
320
331
  content: '""',
321
332
  display: 'block',
322
- blockSize: 'var(--_sheet-keyboard-inset, 0px)',
333
+ blockSize: 0,
323
334
  pointerEvents: 'none',
324
335
  },
325
336
  },
337
+ keyboardInset: (inset: number) => ({
338
+ '::after': {
339
+ blockSize: inset,
340
+ },
341
+ }),
326
342
  budget: {
327
343
  height: `calc(var(--_sheet-budget) + ${OVERSCROLL_PADDING}px)`,
328
344
  },
@@ -564,6 +580,9 @@ export function BottomSheetPanel({
564
580
  ...props
565
581
  }: BottomSheetPanelProps) {
566
582
  const elementRef = useRef<HTMLDivElement | null>(null);
583
+ const dismissKeyboardRef = useRef<
584
+ ((focusTarget?: HTMLElement) => void) | null
585
+ >(null);
567
586
  const previousStateRef = useRef(state);
568
587
  const reactivatedEntranceRef = useRef(false);
569
588
  const onMotionStartRef = useRef(onMotionStart);
@@ -639,7 +658,6 @@ export function BottomSheetPanel({
639
658
  dragOffset,
640
659
  settledOffset,
641
660
  isDragging,
642
- isTraveling,
643
661
  isDismissedByTouchDrag,
644
662
  sheetHeight,
645
663
  scrollPreservationInset,
@@ -653,6 +671,10 @@ export function BottomSheetPanel({
653
671
  offscreenBlockEndInset: OVERSCROLL_PADDING,
654
672
  onDismiss,
655
673
  snapHeights,
674
+ onTravelStart: () => {
675
+ // Focusing the sheet dismisses the keyboard as the sheet starts to move.
676
+ dismissKeyboardRef.current?.(elementRef.current ?? undefined);
677
+ },
656
678
  onScrimOpacity,
657
679
  hasFixedHeight: consumerHeight != null,
658
680
  });
@@ -686,17 +708,19 @@ export function BottomSheetPanel({
686
708
  isPresentedRef.current = isPresented;
687
709
  sheetRef(isPresented ? elementRef.current : null);
688
710
  }, [isPresented, sheetRef]);
689
- useMobileKeyboard({
690
- bodyRef: bodyElementRef,
691
- bottomClearance: MOBILE_KEYBOARD_BOTTOM_CLEARANCE,
692
- isEnabled: height === 'tall',
693
- isFullyExpanded: settledOffset === 0,
694
- isPageScrollLocked,
695
- isSheetTraveling: isTraveling,
711
+ const keyboard = useMobileKeyboard(elementRef, {
712
+ move: 'ride',
713
+ inset: true,
714
+ offset: MOBILE_KEYBOARD_BOTTOM_CLEARANCE,
715
+ isEnabled: height === 'tall' && isPresented,
716
+ isExpanded: settledOffset === 0,
696
717
  isOpen: isInteractive,
697
- isPresented,
698
- sheetRef: elementRef,
718
+ isPageScrollLocked,
699
719
  });
720
+ const keyboardInset = keyboard.inset;
721
+ // The drag's own callback dismisses the keyboard as the sheet starts to
722
+ // move. It runs in the gesture, so it reads the newest dismiss.
723
+ dismissKeyboardRef.current = keyboard.dismiss;
700
724
  // Keep controller registration attached to one stable host ref. React
701
725
  // detaches an old callback ref when a consumer supplies a new identity; if
702
726
  // that public ref were merged with setElement, an ordinary parent rerender
@@ -943,6 +967,10 @@ export function BottomSheetPanel({
943
967
  getContentProps<HTMLDivElement>(
944
968
  stylex.props(
945
969
  styles.content,
970
+ height === 'tall' && styles.tallKeyboardContent,
971
+ height === 'tall' &&
972
+ keyboardInset > 0 &&
973
+ styles.keyboardInset(keyboardInset),
946
974
  ...container(
947
975
  usesThemePadding
948
976
  ? {useThemeDefault: 'bottom-sheet'}
@@ -29,6 +29,11 @@ export const docs = {
29
29
  group: 'BottomSheet',
30
30
  category: 'Overlay',
31
31
  keywords: ['bottom sheet', 'switcher', 'multi-step', 'flow', 'wizard'],
32
+ deprecated: {
33
+ replacement: 'BottomSheet',
34
+ description:
35
+ 'Write each step as its own BottomSheet with isOpen, controlled by one state value that names the open step; moving on from inside the open step replaces it in place (see BottomSheet, Replacing a sheet). BottomSheetSwitcher keeps working until a future minor release removes it.',
36
+ },
32
37
  playground: {
33
38
  overlay: true,
34
39
  overlayControl: {
@@ -1881,7 +1881,7 @@ export function getBottomSheetBeneath(
1881
1881
 
1882
1882
  // preventScroll on both: presenting a sheet must not scroll the page to reveal
1883
1883
  // what it just focused. For an autofocused field that reveal is the mobile
1884
- // keyboard's, and it moves the document under a fixed sheet; useMobileKeyboard
1884
+ // keyboard's, and it moves the document under a fixed sheet. useMobileKeyboard
1885
1885
  // brings the field into view within the sheet instead.
1886
1886
  /**
1887
1887
  * Move focus into a sheet panel. Focus already inside the sheet stays put: