@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
@@ -181,7 +181,14 @@ export interface PowerSearchBottomSheetProps {
181
181
  config: InternalConfig;
182
182
  /** The caller's config, handed to Editor overrides as they expect it. */
183
183
  configProp: PowerSearchConfig;
184
+ /** The committed filters; the draft the sheet edits starts from them. */
184
185
  filters: ReadonlyArray<PowerSearchFilter>;
186
+ /**
187
+ * The field's `onChange`. Done replays the draft's edits through it, in
188
+ * order, onto the field's latest filters, each call carrying that list
189
+ * with the edit applied; nothing is reported before Done, and nothing when
190
+ * the field is read-only or disabled at Done.
191
+ */
185
192
  onChange: (
186
193
  filters: ReadonlyArray<PowerSearchFilter>,
187
194
  changeType: PowerSearchChangeType,
@@ -208,6 +215,116 @@ export interface PowerSearchBottomSheetProps {
208
215
  // Styles
209
216
  // =============================================================================
210
217
 
218
+ /** One edit of the draft: the draft before and after it, and its type and index in the draft; Done replays it onto the field's latest filters. */
219
+ interface DraftEdit {
220
+ before: ReadonlyArray<PowerSearchFilter>;
221
+ after: ReadonlyArray<PowerSearchFilter>;
222
+ changeType: PowerSearchChangeType;
223
+ index: number;
224
+ }
225
+
226
+ // Whether two filter lists hold the same filters in the same order, by the
227
+ // key-order-insensitive signature the sheet matches filters with.
228
+ function hasSameFilters(
229
+ a: ReadonlyArray<PowerSearchFilter>,
230
+ b: ReadonlyArray<PowerSearchFilter>,
231
+ ): boolean {
232
+ return (
233
+ a.length === b.length &&
234
+ a.every(
235
+ (filter, index) =>
236
+ b[index] != null &&
237
+ filterSignature(filter) === filterSignature(b[index]),
238
+ )
239
+ );
240
+ }
241
+
242
+ // The filters `before` holds that `after` does not, as a multiset by
243
+ // signature, in `before` order.
244
+ function removedFilters(
245
+ before: ReadonlyArray<PowerSearchFilter>,
246
+ after: ReadonlyArray<PowerSearchFilter>,
247
+ ): PowerSearchFilter[] {
248
+ const remaining = new Map<string, number>();
249
+ for (const filter of after) {
250
+ const signature = filterSignature(filter);
251
+ remaining.set(signature, (remaining.get(signature) ?? 0) + 1);
252
+ }
253
+ return before.filter(filter => {
254
+ const signature = filterSignature(filter);
255
+ const count = remaining.get(signature) ?? 0;
256
+ if (count > 0) {
257
+ remaining.set(signature, count - 1);
258
+ return false;
259
+ }
260
+ return true;
261
+ });
262
+ }
263
+
264
+ /**
265
+ * Replays the draft's edits onto `base`, the field's latest filters, and
266
+ * reports each through `report` with the list after it. An add appends; an
267
+ * edit replaces the first filter matching the one it edited; a removal drops
268
+ * the filters it removed, by signature. An edit or removal whose target is
269
+ * not in the base list is skipped: the parent removed it meanwhile.
270
+ */
271
+ export function replayDraftEdits(
272
+ base: ReadonlyArray<PowerSearchFilter>,
273
+ edits: ReadonlyArray<DraftEdit>,
274
+ report: (
275
+ filters: ReadonlyArray<PowerSearchFilter>,
276
+ changeType: PowerSearchChangeType,
277
+ index: number,
278
+ ) => void,
279
+ ): ReadonlyArray<PowerSearchFilter> {
280
+ let current = base;
281
+ for (const edit of edits) {
282
+ if (edit.changeType === 'add') {
283
+ const added = edit.after[edit.index];
284
+ if (added == null) {
285
+ continue;
286
+ }
287
+ current = [...current, added];
288
+ report(current, 'add', current.length - 1);
289
+ } else if (edit.changeType === 'edit') {
290
+ const original = edit.before[edit.index];
291
+ const updated = edit.after[edit.index];
292
+ if (original == null || updated == null) {
293
+ continue;
294
+ }
295
+ const signature = filterSignature(original);
296
+ const index = current.findIndex(
297
+ filter => filterSignature(filter) === signature,
298
+ );
299
+ if (index === -1) {
300
+ continue;
301
+ }
302
+ current = current.map((filter, i) => (i === index ? updated : filter));
303
+ report(current, 'edit', index);
304
+ } else {
305
+ const removed = removedFilters(edit.before, edit.after).map(
306
+ filterSignature,
307
+ );
308
+ const indexes: number[] = [];
309
+ for (const signature of removed) {
310
+ const index = current.findIndex(
311
+ (filter, i) =>
312
+ !indexes.includes(i) && filterSignature(filter) === signature,
313
+ );
314
+ if (index !== -1) {
315
+ indexes.push(index);
316
+ }
317
+ }
318
+ if (indexes.length === 0) {
319
+ continue;
320
+ }
321
+ current = current.filter((_, i) => !indexes.includes(i));
322
+ report(current, 'remove', Math.min(...indexes));
323
+ }
324
+ }
325
+ return current;
326
+ }
327
+
211
328
  // The trailing plus a field row shows when choosing it adds the filter at
212
329
  // once, and the Add filter actions' glyph. Core's default icon set has no plus.
213
330
  function PlusIcon(props: SVGProps<SVGSVGElement>) {
@@ -834,8 +951,8 @@ export function PowerSearchBottomSheet({
834
951
  resultCountText,
835
952
  config,
836
953
  configProp,
837
- filters,
838
- onChange,
954
+ filters: committedFilters,
955
+ onChange: onCommit,
839
956
  searchSource,
840
957
  placeholder,
841
958
  isReadOnly,
@@ -854,6 +971,35 @@ export function PowerSearchBottomSheet({
854
971
  const formatList = useListFormatter();
855
972
  const isInteractive = !isDisabled && !isReadOnly;
856
973
 
974
+ // ---- draft ----
975
+ // One open flow edits one draft of the filter list; `onChange` waits for
976
+ // Done, and a dismissal with a changed draft asks first.
977
+ const [filtersAtOpen, setFiltersAtOpen] = useState(() => committedFilters);
978
+ const [filters, setFilters] = useState(() => committedFilters);
979
+ // The draft as of the last edit, for the edit log's `before` lists.
980
+ const draftRef = useRef(filters);
981
+ const draftEditsRef = useRef<DraftEdit[]>([]);
982
+ const onChange = useCallback(
983
+ (
984
+ next: ReadonlyArray<PowerSearchFilter>,
985
+ changeType: PowerSearchChangeType,
986
+ index: number,
987
+ ) => {
988
+ draftEditsRef.current.push({
989
+ before: draftRef.current,
990
+ after: next,
991
+ changeType,
992
+ index,
993
+ });
994
+ draftRef.current = next;
995
+ setFilters(next);
996
+ },
997
+ [],
998
+ );
999
+ const isDraftDirty = !hasSameFilters(filtersAtOpen, filters);
1000
+ const [isDiscardConfirmationOpen, setIsDiscardConfirmationOpen] =
1001
+ useState(false);
1002
+
857
1003
  const contentSearchFieldKey = config.config.contentSearchFieldKey;
858
1004
  const contentSearchOperator =
859
1005
  contentSearchFieldKey == null
@@ -961,9 +1107,6 @@ export function PowerSearchBottomSheet({
961
1107
  const [contentQuery, setContentQuery] = useState('');
962
1108
  const [isClearConfirmationOpen, setIsClearConfirmationOpen] = useState(false);
963
1109
  const clearAllButtonRef = useRef<HTMLButtonElement>(null);
964
- // Set while Clear all commits, so the result count that changes in that
965
- // commit is spoken with Clear all's outcome, as one message.
966
- const isClearingRef = useRef(false);
967
1110
 
968
1111
  // `useAnnounce` delivers inside the open Astryx modal that encloses its
969
1112
  // caller, and this component renders above the
@@ -990,13 +1133,21 @@ export function PowerSearchBottomSheet({
990
1133
  if (isOpen !== wasOpen) {
991
1134
  setWasOpen(isOpen);
992
1135
  if (isOpen) {
1136
+ // A fresh draft from the committed filters; the entry step is judged
1137
+ // against them, not the previous session's draft.
1138
+ setFiltersAtOpen(committedFilters);
1139
+ setFilters(committedFilters);
1140
+ draftRef.current = committedFilters;
1141
+ draftEditsRef.current = [];
1142
+ const startsOnAddFilter = committedFilters.length === 0 && isInteractive;
993
1143
  setFieldQuery('');
994
1144
  setContentQuery('');
995
- setStep(isEntryAddFilter ? 'fields' : 'manage');
996
- setFieldPickerOrigin(isEntryAddFilter ? 'trigger' : 'manage');
1145
+ setStep(startsOnAddFilter ? 'fields' : 'manage');
1146
+ setFieldPickerOrigin(startsOnAddFilter ? 'trigger' : 'manage');
997
1147
  } else {
998
1148
  setStep(null);
999
1149
  setIsClearConfirmationOpen(false);
1150
+ setIsDiscardConfirmationOpen(false);
1000
1151
  }
1001
1152
  }
1002
1153
 
@@ -1005,6 +1156,34 @@ export function PowerSearchBottomSheet({
1005
1156
  setStep(null);
1006
1157
  onOpenChange(false);
1007
1158
  }, [onOpenChange]);
1159
+ // Done replays each edit in order onto the field's latest filters, so a
1160
+ // consumer gets the calls a popover session makes and keeps a filter it
1161
+ // added meanwhile.
1162
+ const commitAndClose = useCallback(() => {
1163
+ // A field that became read-only while the flow was open reports nothing.
1164
+ if (isDraftDirty && isInteractive) {
1165
+ replayDraftEdits(committedFilters, draftEditsRef.current, onCommit);
1166
+ }
1167
+ draftEditsRef.current = [];
1168
+ closeFlow();
1169
+ }, [closeFlow, committedFilters, isDraftDirty, isInteractive, onCommit]);
1170
+ const requestDismiss = useCallback(() => {
1171
+ if (isDraftDirty) {
1172
+ setIsDiscardConfirmationOpen(true);
1173
+ } else {
1174
+ closeFlow();
1175
+ }
1176
+ }, [closeFlow, isDraftDirty]);
1177
+ const closeDiscardConfirmation = () => {
1178
+ // eslint-disable-next-line @eslint-react/dom-no-flush-sync -- the confirmation must be gone before focus moves back into the sheet
1179
+ flushSync(() => setIsDiscardConfirmationOpen(false));
1180
+ const target =
1181
+ stepRef.current === 'manage'
1182
+ ? (managerAddButtonRef.current ??
1183
+ manageSheetRef.current?.closest<HTMLElement>('[tabindex="-1"]'))
1184
+ : fieldsSheetRef.current?.closest<HTMLElement>('[tabindex="-1"]');
1185
+ target?.focus({preventScroll: true});
1186
+ };
1008
1187
 
1009
1188
  // Whether a Filters row is a button: the field interactive and the
1010
1189
  // filter not read-only. The row opens the value step — the editor for its
@@ -1279,17 +1458,8 @@ export function PowerSearchBottomSheet({
1279
1458
  // is quiet then, so the change is spoken here, inside the dialog. Only a
1280
1459
  // change after the flow opened is spoken.
1281
1460
  const announcedResultCountRef = useRef(resultCountText);
1282
- // The latest result count, for Clear all's handler to read after its
1283
- // commit has flushed.
1284
- const latestResultCountRef = useRef(resultCountText);
1285
- useLayoutEffect(() => {
1286
- latestResultCountRef.current = resultCountText;
1287
- }, [resultCountText]);
1288
1461
  useEffect(() => {
1289
- if (
1290
- isClearingRef.current ||
1291
- resultCountText === announcedResultCountRef.current
1292
- ) {
1462
+ if (resultCountText === announcedResultCountRef.current) {
1293
1463
  return;
1294
1464
  }
1295
1465
  announcedResultCountRef.current = resultCountText;
@@ -1630,52 +1800,31 @@ export function PowerSearchBottomSheet({
1630
1800
  // confirmation must be gone before focus moves into the sheet and the
1631
1801
  // outcome is routed into the sheet's region, so its close is flushed
1632
1802
  // first. The outcome is "Editable filters removed" while a read-only filter
1633
- // remains, otherwise "All filters removed", joined with a result count that
1634
- // changed in the same commit as one catalog message; focus moves to the
1635
- // sheet's first remaining control: a kept read-only row has none, so Add
1636
- // filter.
1803
+ // remains, otherwise "All filters removed"; the result count follows the
1804
+ // applied filters, which change on Done, so none changes here. Focus moves
1805
+ // to the sheet's first remaining control: a kept read-only row has none,
1806
+ // so Add filter.
1637
1807
  const closeClearConfirmation = (confirmed: boolean) => {
1638
1808
  const kept = filters.filter(filter => filter.isReadOnly);
1639
1809
  const firstRemovedIndex = filters.findIndex(filter => !filter.isReadOnly);
1640
1810
  const isClearing = confirmed && isInteractive && firstRemovedIndex >= 0;
1641
- isClearingRef.current = isClearing;
1642
- try {
1643
- // eslint-disable-next-line @eslint-react/dom-no-flush-sync -- the confirmation must be gone before focus moves into the sheet and the words are routed into it
1644
- flushSync(() => {
1645
- setIsClearConfirmationOpen(false);
1646
- if (isClearing) {
1647
- onChange(kept, 'remove', firstRemovedIndex);
1648
- }
1649
- });
1650
- } finally {
1651
- isClearingRef.current = false;
1652
- }
1811
+ // eslint-disable-next-line @eslint-react/dom-no-flush-sync -- the confirmation must be gone before focus moves into the sheet and the words are routed into it
1812
+ flushSync(() => {
1813
+ setIsClearConfirmationOpen(false);
1814
+ if (isClearing) {
1815
+ onChange(kept, 'remove', firstRemovedIndex);
1816
+ }
1817
+ });
1653
1818
  if (!isClearing) {
1654
1819
  clearAllButtonRef.current?.focus();
1655
1820
  return;
1656
1821
  }
1657
1822
  managerAddButtonRef.current?.focus({preventScroll: true});
1658
- const resultCount = latestResultCountRef.current;
1659
- const changedCount =
1660
- resultCount !== announcedResultCountRef.current ? resultCount : null;
1661
- announcedResultCountRef.current = resultCount;
1662
- if (kept.length > 0) {
1663
- announce(
1664
- changedCount == null
1665
- ? t('@astryx.powersearch.editableFiltersRemoved')
1666
- : t('@astryx.powersearch.sheet.editableFiltersClearedWithCount', {
1667
- resultCount: changedCount,
1668
- }),
1669
- );
1670
- } else {
1671
- announce(
1672
- changedCount == null
1673
- ? t('@astryx.powersearch.allFiltersRemoved')
1674
- : t('@astryx.powersearch.sheet.filtersClearedWithCount', {
1675
- resultCount: changedCount,
1676
- }),
1677
- );
1678
- }
1823
+ announce(
1824
+ kept.length > 0
1825
+ ? t('@astryx.powersearch.editableFiltersRemoved')
1826
+ : t('@astryx.powersearch.allFiltersRemoved'),
1827
+ );
1679
1828
  };
1680
1829
 
1681
1830
  const handleOperatorSelect = useCallback((operator: PowerSearchOperator) => {
@@ -1776,10 +1925,10 @@ export function PowerSearchBottomSheet({
1776
1925
  const handleManageOpenChange = useCallback(
1777
1926
  (isSheetOpen: boolean) => {
1778
1927
  if (!isSheetOpen && stepRef.current === 'manage') {
1779
- closeFlow();
1928
+ requestDismiss();
1780
1929
  }
1781
1930
  },
1782
- [closeFlow],
1931
+ [requestDismiss],
1783
1932
  );
1784
1933
  const handleFieldsOpenChange = useCallback(
1785
1934
  (isSheetOpen: boolean) => {
@@ -1787,13 +1936,13 @@ export function PowerSearchBottomSheet({
1787
1936
  return;
1788
1937
  }
1789
1938
  if (fieldPickerOrigin === 'trigger') {
1790
- closeFlow();
1939
+ requestDismiss();
1791
1940
  return;
1792
1941
  }
1793
1942
  queueManagerAddFocus();
1794
1943
  setStep('manage');
1795
1944
  },
1796
- [closeFlow, fieldPickerOrigin, queueManagerAddFocus],
1945
+ [fieldPickerOrigin, queueManagerAddFocus, requestDismiss],
1797
1946
  );
1798
1947
  const handleValueOpenChange = useCallback(
1799
1948
  (isSheetOpen: boolean) => {
@@ -2076,7 +2225,11 @@ export function PowerSearchBottomSheet({
2076
2225
  // is spoken in this sheet when it appears.
2077
2226
  isManageSheetOpen && (
2078
2227
  <EmptyState
2079
- title={t('@astryx.powersearch.sheet.noFilters')}
2228
+ title={t(
2229
+ isDraftDirty
2230
+ ? '@astryx.powersearch.sheet.noFiltersInDraft'
2231
+ : '@astryx.powersearch.sheet.noFilters',
2232
+ )}
2080
2233
  description={t(
2081
2234
  '@astryx.powersearch.sheet.noFiltersDescription',
2082
2235
  )}
@@ -2157,7 +2310,10 @@ export function PowerSearchBottomSheet({
2157
2310
  )}
2158
2311
  </div>
2159
2312
  </div>
2160
- {filterRows.length > 0 && (
2313
+ {/* The footer stays while the draft differs from the applied
2314
+ filters: an emptied draft still needs Done to apply the
2315
+ removals. */}
2316
+ {(filterRows.length > 0 || isDraftDirty) && (
2161
2317
  <div {...stylex.props(styles.footer, styles.fixedFooter)}>
2162
2318
  {isClearAllShown && (
2163
2319
  <Button
@@ -2178,7 +2334,7 @@ export function PowerSearchBottomSheet({
2178
2334
  ? styles.footerEqualAction
2179
2335
  : [styles.footerSoleAction, styles.touchHitArea]
2180
2336
  }
2181
- onClick={closeFlow}
2337
+ onClick={commitAndClose}
2182
2338
  width={isClearAllShown ? undefined : '100%'}
2183
2339
  />
2184
2340
  </div>
@@ -2373,7 +2529,7 @@ export function PowerSearchBottomSheet({
2373
2529
  variant="primary"
2374
2530
  width="100%"
2375
2531
  xstyle={styles.touchAction}
2376
- onClick={closeFlow}
2532
+ onClick={commitAndClose}
2377
2533
  />
2378
2534
  </div>
2379
2535
  )}
@@ -2518,6 +2674,25 @@ export function PowerSearchBottomSheet({
2518
2674
  onAction={() => closeClearConfirmation(true)}
2519
2675
  />
2520
2676
  )}
2677
+ {isDiscardConfirmationOpen && (
2678
+ <SheetConfirmation
2679
+ isOpen
2680
+ onOpenChange={isConfirmationOpen => {
2681
+ if (!isConfirmationOpen) {
2682
+ closeDiscardConfirmation();
2683
+ }
2684
+ }}
2685
+ title={t('@astryx.powersearch.sheet.discardTitle')}
2686
+ description={t('@astryx.powersearch.sheet.discardDescription')}
2687
+ actionLabel={t('@astryx.powersearch.sheet.discardConfirm')}
2688
+ cancelLabel={t('@astryx.powersearch.sheet.keepEditing')}
2689
+ onAction={() => {
2690
+ setIsDiscardConfirmationOpen(false);
2691
+ draftEditsRef.current = [];
2692
+ closeFlow();
2693
+ }}
2694
+ />
2695
+ )}
2521
2696
  </>
2522
2697
  );
2523
2698
  }
@@ -35,6 +35,7 @@ import {
35
35
  type ResizeGesture,
36
36
  type ResizeGestureModel,
37
37
  } from './resizeGesture';
38
+ import {separatorAttributes} from './separatorAttributes';
38
39
  import {themeProps} from '../utils/themeProps';
39
40
 
40
41
  import {useDevWarning} from '../hooks/useDevWarning';
@@ -529,19 +530,8 @@ export function ResizeHandle({
529
530
  }, [hasRegion, regionGesture]);
530
531
 
531
532
  // --- ARIA ---
532
- // When collapsed the panel's real size (0) sits below aria-valuemin, which
533
- // is invalid per WCAG 4.1.2. Clamp aria-valuenow to the minimum and announce
534
- // the true state via aria-valuetext instead; the valuetext is removed as
535
- // soon as the panel expands so the numeric value reads again.
536
533
  const sizeNow = resizable?._size;
537
534
  const minNow = resizable?._minSizePx;
538
- const ariaValueNow =
539
- sizeNow == null || minNow == null
540
- ? undefined
541
- : isCollapsed
542
- ? Math.max(sizeNow, minNow)
543
- : sizeNow;
544
- const ariaValueMin = minNow;
545
535
  // A separator with a value needs a maximum (WAI-ARIA, APG window splitter).
546
536
  // Unbounded regions report the basis they can fill; a hand-built props
547
537
  // object without it reports only a configured maximum.
@@ -549,14 +539,22 @@ export function ResizeHandle({
549
539
  ? (getEffectiveMaxPx(resizable) ??
550
540
  (resizable._maxSizePx !== Infinity ? resizable._maxSizePx : undefined))
551
541
  : undefined;
552
- const ariaValueMax =
553
- effectiveMax != null && Number.isFinite(effectiveMax)
554
- ? effectiveMax
555
- : undefined;
556
- const ariaValueText =
557
- resizable != null && isCollapsed
558
- ? t('@astryx.resizable.collapsed')
559
- : undefined;
542
+ // The attributes createResizable writes on a plain element, from the same
543
+ // derivation.
544
+ const aria = separatorAttributes({
545
+ direction,
546
+ isDisabled,
547
+ value:
548
+ sizeNow == null || minNow == null
549
+ ? undefined
550
+ : {
551
+ size: sizeNow,
552
+ minPx: minNow,
553
+ maxPx: effectiveMax,
554
+ isCollapsed,
555
+ collapsedText: t('@astryx.resizable.collapsed'),
556
+ },
557
+ });
560
558
 
561
559
  // The Grip pill takes the pointer as the Grab zone does, so a pill drawn
562
560
  // outside the zone still resizes.
@@ -591,14 +589,8 @@ export function ResizeHandle({
591
589
  <div
592
590
  ref={useMergedRefs(ref, handleRef)}
593
591
  role="separator"
594
- aria-orientation={isHorizontal ? 'vertical' : 'horizontal'}
595
- aria-valuenow={ariaValueNow}
596
- aria-valuemin={ariaValueMin}
597
- aria-valuemax={ariaValueMax}
598
- aria-valuetext={ariaValueText}
592
+ {...aria}
599
593
  aria-label={label}
600
- aria-disabled={isDisabled || undefined}
601
- tabIndex={isDisabled ? -1 : 0}
602
594
  onDoubleClick={() => {
603
595
  if (!isDisabled) {
604
596
  gesture?.doubleClick();