@astryxdesign/core 0.7.8-canary.f10bc91 → 0.7.8-canary.f60d2bb

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.
@@ -76,8 +76,9 @@ export interface PowerSearchBottomSheetProps {
76
76
  filters: ReadonlyArray<PowerSearchFilter>;
77
77
  /**
78
78
  * The field's `onChange`. Done replays the draft's edits through it, in
79
- * order, each with the list as it stood after that edit; nothing is
80
- * reported before Done.
79
+ * order, onto the field's latest filters, each call carrying that list
80
+ * with the edit applied; nothing is reported before Done, and nothing when
81
+ * the field is read-only or disabled at Done.
81
82
  */
82
83
  onChange: (filters: ReadonlyArray<PowerSearchFilter>, changeType: PowerSearchChangeType, index: number) => void;
83
84
  /** The field typeahead source; the content search lists its suggestions. */
@@ -96,7 +97,7 @@ export interface PowerSearchBottomSheetProps {
96
97
  components?: PowerSearchComponents;
97
98
  editors: PowerSearchSheetEditors;
98
99
  }
99
- /** One edit of the draft: the list before it, the list after it, and how it was reported. */
100
+ /** 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. */
100
101
  interface DraftEdit {
101
102
  before: ReadonlyArray<PowerSearchFilter>;
102
103
  after: ReadonlyArray<PowerSearchFilter>;
@@ -1 +1 @@
1
- {"version":3,"file":"PowerSearchBottomSheet.d.ts","sourceRoot":"","sources":["../../src/PowerSearch/PowerSearchBottomSheet.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,EASZ,KAAK,SAAS,EAEf,MAAM,OAAO,CAAC;AAoBf,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAkCrD,OAAO,KAAK,EACV,wBAAwB,EACxB,sBAAsB,EACvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,0BAA0B,CAAC;AAe3D,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAIV,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EAEjB,iBAAiB,EACjB,eAAe,EAEhB,MAAM,SAAS,CAAC;AAajB;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB;IACtC,YAAY,EAAE,OAAO,YAAY,CAAC;IAClC,sBAAsB,EAAE,OAAO,sBAAsB,CAAC;IACtD,wBAAwB,EAAE,OAAO,wBAAwB,CAAC;CAC3D;AAED,MAAM,WAAW,2BAA2B;IAC1C,wEAAwE;IACxE,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACxC;;;OAGG;IACH,aAAa,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC7C;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,MAAM,EAAE,cAAc,CAAC;IACvB,yEAAyE;IACzE,UAAU,EAAE,iBAAiB,CAAC;IAC9B,yEAAyE;IACzE,OAAO,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;IAC1C;;;;OAIG;IACH,QAAQ,EAAE,CACR,OAAO,EAAE,aAAa,CAAC,iBAAiB,CAAC,EACzC,UAAU,EAAE,qBAAqB,EACjC,KAAK,EAAE,MAAM,KACV,IAAI,CAAC;IACV,4EAA4E;IAC5E,YAAY,EAAE,YAAY,CAAC,eAAe,CAAC,CAAC;IAC5C,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,qEAAqE;IACrE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,qBAAqB,CAAC;IACnC,OAAO,EAAE,uBAAuB,CAAC;CAClC;AAMD,6FAA6F;AAC7F,UAAU,SAAS;IACjB,MAAM,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;IACzC,KAAK,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;IACxC,UAAU,EAAE,qBAAqB,CAAC;IAClC,KAAK,EAAE,MAAM,CAAC;CACf;AAwCD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,aAAa,CAAC,iBAAiB,CAAC,EACtC,KAAK,EAAE,aAAa,CAAC,SAAS,CAAC,EAC/B,MAAM,EAAE,CACN,OAAO,EAAE,aAAa,CAAC,iBAAiB,CAAC,EACzC,UAAU,EAAE,qBAAqB,EACjC,KAAK,EAAE,MAAM,KACV,IAAI,GACR,aAAa,CAAC,iBAAiB,CAAC,CA+ClC;AAwmBD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,YAAY,EACZ,aAAa,EACb,eAAe,EACf,MAAM,EACN,UAAU,EACV,OAAO,EAAE,gBAAgB,EACzB,QAAQ,EAAE,QAAQ,EAClB,YAAY,EACZ,WAAW,EACX,UAAU,EACV,UAAU,EACV,QAAQ,EACR,oBAAoB,EACpB,UAAU,EACV,gBAAgB,EAChB,eAAe,EAAE,wBAAwB,EACzC,UAAU,EAAE,kBAAkB,EAC9B,OAAO,GACR,EAAE,2BAA2B,qBAksD7B;yBAttDe,sBAAsB"}
1
+ {"version":3,"file":"PowerSearchBottomSheet.d.ts","sourceRoot":"","sources":["../../src/PowerSearch/PowerSearchBottomSheet.tsx"],"names":[],"mappings":"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"}
@@ -104,7 +104,7 @@ registerEnglishFallback(english);
104
104
  // Styles
105
105
  // =============================================================================
106
106
 
107
- /** One edit of the draft: the list before it, the list after it, and how it was reported. */
107
+ /** 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. */
108
108
 
109
109
  // Whether two filter lists hold the same filters in the same order, by the
110
110
  // key-order-insensitive signature the sheet matches filters with.
@@ -765,12 +765,13 @@ export function PowerSearchBottomSheet({
765
765
  // consumer gets the calls a popover session makes and keeps a filter it
766
766
  // added meanwhile.
767
767
  const commitAndClose = useCallback(() => {
768
- if (isDraftDirty) {
768
+ // A field that became read-only while the flow was open reports nothing.
769
+ if (isDraftDirty && isInteractive) {
769
770
  replayDraftEdits(committedFilters, draftEditsRef.current, onCommit);
770
771
  }
771
772
  draftEditsRef.current = [];
772
773
  closeFlow();
773
- }, [closeFlow, committedFilters, isDraftDirty, onCommit]);
774
+ }, [closeFlow, committedFilters, isDraftDirty, isInteractive, onCommit]);
774
775
  const requestDismiss = useCallback(() => {
775
776
  if (isDraftDirty) {
776
777
  setIsDiscardConfirmationOpen(true);
@@ -33,8 +33,9 @@ export declare const pressVars: stylex.VarGroup<Readonly<{
33
33
  *
34
34
  * `defineConsts`, so the one number is inlined here at build time and is the
35
35
  * controller's constant at run time (`PRESS_FADE_MS` in pressGesture.ts):
36
- * the release animation and the timer that removes the attribute after it
37
- * cannot drift apart.
36
+ * the release animation and the controller's clock cannot drift apart. When
37
+ * that clock runs out, the controller waits for a release animation that a
38
+ * busy main thread started late, then removes the attribute.
38
39
  */
39
40
  export declare const pressConsts: {
40
41
  /** How long the release takes, in ms. UIKit's deselect clock. */
@@ -1 +1 @@
1
- {"version":3,"file":"interactionOverlay.stylex.d.ts","sourceRoot":"","sources":["../../src/utils/interactionOverlay.stylex.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;GAcG;AAmCH,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AAG3C;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS;;WAEpB,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW;IACtB,iEAAiE;;CAEjE,CAAC;AA8HH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8HnC,CAAC"}
1
+ {"version":3,"file":"interactionOverlay.stylex.d.ts","sourceRoot":"","sources":["../../src/utils/interactionOverlay.stylex.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;GAcG;AAmCH,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AAG3C;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS;;WAEpB,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW;IACtB,iEAAiE;;CAEjE,CAAC;AA8HH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8HnC,CAAC"}
@@ -78,8 +78,9 @@ export const pressVars = {
78
78
  *
79
79
  * `defineConsts`, so the one number is inlined here at build time and is the
80
80
  * controller's constant at run time (`PRESS_FADE_MS` in pressGesture.ts):
81
- * the release animation and the timer that removes the attribute after it
82
- * cannot drift apart.
81
+ * the release animation and the controller's clock cannot drift apart. When
82
+ * that clock runs out, the controller waits for a release animation that a
83
+ * busy main thread started late, then removes the attribute.
83
84
  */
84
85
  export const pressConsts = {
85
86
  releaseMs: 200
@@ -1 +1 @@
1
- {"version":3,"file":"pressFeedback.d.ts","sourceRoot":"","sources":["../../src/utils/pressFeedback.ts"],"names":[],"mappings":"AAkDA;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,0BAA0B,CAAC;AAE3D;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,sBAAsB,CAAC;AAErD,qDAAqD;AACrD,eAAO,MAAM,cAAc,EAAE;IAAC,QAAQ,CAAC,CAAC,mBAAmB,CAAC,EAAE,EAAE,CAAA;CAE/D,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAyE5C;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAUpD;AAqjBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,IAAI,CA0BjD"}
1
+ {"version":3,"file":"pressFeedback.d.ts","sourceRoot":"","sources":["../../src/utils/pressFeedback.ts"],"names":[],"mappings":"AAkDA;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,0BAA0B,CAAC;AAE3D;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,sBAAsB,CAAC;AAErD,qDAAqD;AACrD,eAAO,MAAM,cAAc,EAAE;IAAC,QAAQ,CAAC,CAAC,mBAAmB,CAAC,EAAE,EAAE,CAAA;CAE/D,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,OAAO,CAAC;AA0E5C;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAUpD;AAkoBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,IAAI,CA0BjD"}
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.7.8-canary.f10bc91",
3
+ "version": "0.7.8-canary.f60d2bb",
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,9 @@
870
870
  "react-dom": ">=19.0.0"
871
871
  },
872
872
  "devDependencies": {
873
- "@astryxdesign/a11y-spec": "0.7.8-canary.f10bc91",
874
- "@astryxdesign/cli": "0.7.8-canary.f10bc91",
875
- "@astryxdesign/interaction-harness": "0.7.8-canary.f10bc91",
873
+ "@astryxdesign/a11y-spec": "0.7.8-canary.f60d2bb",
874
+ "@astryxdesign/cli": "0.7.8-canary.f60d2bb",
875
+ "@astryxdesign/interaction-harness": "0.7.8-canary.f60d2bb",
876
876
  "@babel/cli": "^7.29.7",
877
877
  "@babel/core": "^7.29.7",
878
878
  "@babel/preset-react": "^7.29.7",
@@ -185,8 +185,9 @@ export interface PowerSearchBottomSheetProps {
185
185
  filters: ReadonlyArray<PowerSearchFilter>;
186
186
  /**
187
187
  * The field's `onChange`. Done replays the draft's edits through it, in
188
- * order, each with the list as it stood after that edit; nothing is
189
- * reported before Done.
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.
190
191
  */
191
192
  onChange: (
192
193
  filters: ReadonlyArray<PowerSearchFilter>,
@@ -214,7 +215,7 @@ export interface PowerSearchBottomSheetProps {
214
215
  // Styles
215
216
  // =============================================================================
216
217
 
217
- /** One edit of the draft: the list before it, the list after it, and how it was reported. */
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. */
218
219
  interface DraftEdit {
219
220
  before: ReadonlyArray<PowerSearchFilter>;
220
221
  after: ReadonlyArray<PowerSearchFilter>;
@@ -1159,12 +1160,13 @@ export function PowerSearchBottomSheet({
1159
1160
  // consumer gets the calls a popover session makes and keeps a filter it
1160
1161
  // added meanwhile.
1161
1162
  const commitAndClose = useCallback(() => {
1162
- if (isDraftDirty) {
1163
+ // A field that became read-only while the flow was open reports nothing.
1164
+ if (isDraftDirty && isInteractive) {
1163
1165
  replayDraftEdits(committedFilters, draftEditsRef.current, onCommit);
1164
1166
  }
1165
1167
  draftEditsRef.current = [];
1166
1168
  closeFlow();
1167
- }, [closeFlow, committedFilters, isDraftDirty, onCommit]);
1169
+ }, [closeFlow, committedFilters, isDraftDirty, isInteractive, onCommit]);
1168
1170
  const requestDismiss = useCallback(() => {
1169
1171
  if (isDraftDirty) {
1170
1172
  setIsDiscardConfirmationOpen(true);
@@ -76,8 +76,9 @@ export const pressVars = stylex.defineVars({
76
76
  *
77
77
  * `defineConsts`, so the one number is inlined here at build time and is the
78
78
  * controller's constant at run time (`PRESS_FADE_MS` in pressGesture.ts):
79
- * the release animation and the timer that removes the attribute after it
80
- * cannot drift apart.
79
+ * the release animation and the controller's clock cannot drift apart. When
80
+ * that clock runs out, the controller waits for a release animation that a
81
+ * busy main thread started late, then removes the attribute.
81
82
  */
82
83
  export const pressConsts = stylex.defineConsts({
83
84
  /** How long the release takes, in ms. UIKit's deselect clock. */
@@ -109,7 +109,8 @@ interface LiveGesture {
109
109
  * for a press of the Space key.
110
110
  */
111
111
  pointerId: number | null;
112
- timer: ReturnType<typeof setTimeout> | null;
112
+ /** Cancels the clock the current phase owes, or the release it waits for. */
113
+ timer: (() => void) | null;
113
114
  }
114
115
 
115
116
  /**
@@ -165,6 +166,76 @@ export function pressFadeMs(element: Element): number {
165
166
  return reduced ? Math.min(scaled, PRESS_FADE_MS) : scaled;
166
167
  }
167
168
 
169
+ /**
170
+ * The release animations that still run for `element`'s press. Each is a
171
+ * finite CSS animation that has not finished. It runs on the element, on its
172
+ * `::before` or `::after`, or on a descendant whose nearest press owner is
173
+ * the element, because a component can draw its release on a part inside the
174
+ * surface. A press owner nested inside plays the release of its own press,
175
+ * so it never extends this one. The list is empty where the engine has no Web
176
+ * Animations.
177
+ */
178
+ function releaseAnimationsOf(element: Element): Animation[] {
179
+ if (typeof element.getAnimations !== 'function') {
180
+ return [];
181
+ }
182
+ return element.getAnimations({subtree: true}).filter(animation => {
183
+ // The effect of a CSS animation is a KeyframeEffect. Its target is the
184
+ // element, also when the animation runs on a pseudo-element.
185
+ const effect = animation.effect as KeyframeEffect | null;
186
+ const target = effect?.target ?? null;
187
+ if (
188
+ effect == null ||
189
+ target == null ||
190
+ !('animationName' in animation) ||
191
+ (target !== element &&
192
+ target.closest(`[${PRESSABLE_ATTRIBUTE}]`) !== element)
193
+ ) {
194
+ return false;
195
+ }
196
+ if (!Number.isFinite(Number(effect.getComputedTiming().endTime))) {
197
+ return false;
198
+ }
199
+ return animation.playState !== 'finished';
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Calls `end` when the release on `element` is over. A busy main thread
205
+ * delays the first frame of the release animation, and with it the end. So a
206
+ * clock that starts at the `fading` write can cut the release short. When
207
+ * `clockMs` runs out, the release waits for each release animation that still
208
+ * runs or still waits for its first frame. It ends when those finish or are
209
+ * cancelled, as when the surface is unmounted or restyled. With no release
210
+ * animation (an instant release, a surface without one, an engine without Web
211
+ * Animations), the release ends at the clock. Returns a cancel.
212
+ */
213
+ function endAfterRelease(
214
+ element: Element,
215
+ clockMs: number,
216
+ end: () => void,
217
+ ): () => void {
218
+ let waiting = true;
219
+ const timer = setTimeout(() => {
220
+ const running = releaseAnimationsOf(element);
221
+ if (running.length === 0) {
222
+ end();
223
+ return;
224
+ }
225
+ void Promise.allSettled(
226
+ running.map(async animation => animation.finished),
227
+ ).then(() => {
228
+ if (waiting) {
229
+ end();
230
+ }
231
+ });
232
+ }, clockMs);
233
+ return () => {
234
+ waiting = false;
235
+ clearTimeout(timer);
236
+ };
237
+ }
238
+
168
239
  const TIMER_EVENT = {
169
240
  fade: 'fadeEnd',
170
241
  flash: 'flashEnd',
@@ -181,7 +252,7 @@ function paintElement(element: HTMLElement, paint: PressPaint): void {
181
252
 
182
253
  function clearTimer(slot: Slot): void {
183
254
  if (slot.live?.timer != null) {
184
- clearTimeout(slot.live.timer);
255
+ slot.live.timer();
185
256
  slot.live.timer = null;
186
257
  }
187
258
  }
@@ -217,25 +288,32 @@ function apply(slot: Slot, step: PressStep): void {
217
288
  }
218
289
  if (step.timer != null) {
219
290
  const kind = step.timer;
220
- // A TIMER, never `requestAnimationFrame`: iOS throttles frame callbacks
221
- // while a scroll runs, so a frame-based delay would fire late, after the
222
- // scroll, and paint a control nobody is pressing any more.
223
- live.timer = setTimeout(
224
- () => {
225
- if (slot.live != null) {
226
- slot.live.timer = null;
227
- }
228
- apply(
229
- slot,
230
- pressGestureStep(slot.live?.gesture ?? idlePressGesture(), {
231
- kind: TIMER_EVENT[kind],
232
- }),
233
- );
234
- },
235
- // The release fade follows the theme's tempo (AST-108). The onset delay
236
- // and the tap flash do not.
237
- kind === 'fade' ? pressFadeMs(live.element) : TIMER_MS[kind],
238
- );
291
+ const fire = () => {
292
+ if (slot.live != null) {
293
+ slot.live.timer = null;
294
+ }
295
+ apply(
296
+ slot,
297
+ pressGestureStep(slot.live?.gesture ?? idlePressGesture(), {
298
+ kind: TIMER_EVENT[kind],
299
+ }),
300
+ );
301
+ };
302
+ if (kind === 'fade') {
303
+ // The release fade follows the theme's tempo (AST-108). It ends when
304
+ // its animation ends, however late a busy main thread let it start.
305
+ live.timer = endAfterRelease(
306
+ live.element,
307
+ pressFadeMs(live.element),
308
+ fire,
309
+ );
310
+ } else {
311
+ // A TIMER, never `requestAnimationFrame`: iOS throttles frame callbacks
312
+ // while a scroll runs, so a frame-based delay would fire late, after the
313
+ // scroll, and paint a control nobody is pressing any more.
314
+ const timer = setTimeout(fire, TIMER_MS[kind]);
315
+ live.timer = () => clearTimeout(timer);
316
+ }
239
317
  }
240
318
  }
241
319
 
@@ -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: number = pressConsts.releaseMs;
68
70
 
@@ -119,7 +121,7 @@ export type PressEvent =
119
121
  | {kind: 'onset'}
120
122
  /** {@link PRESS_FLASH_MS} elapsed on a too-quick tap. */
121
123
  | {kind: 'flashEnd'}
122
- /** {@link PRESS_FADE_MS} elapsed; the paint is gone. */
124
+ /** The release is over: its animation ended, or its clock ran out with none running. */
123
125
  | {kind: 'fadeEnd'};
124
126
 
125
127
  export interface PressStep {