@astryxdesign/core 0.7.9-canary.4e7823a → 0.7.9-canary.57aa93c

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.
@@ -111,7 +111,8 @@ export interface HoverCardReturn {
111
111
  ref: RefCallback<HTMLElement>;
112
112
  /**
113
113
  * Ref for the positioning anchor element.
114
- * Injects anchorName style for CSS anchor positioning.
114
+ * While the layer is shown, and until its exit animation ends, the trigger
115
+ * carries the layer's CSS anchor name.
115
116
  */
116
117
  positionRef: RefCallback<HTMLElement>;
117
118
  /**
@@ -123,6 +124,7 @@ export interface HoverCardReturn {
123
124
  /**
124
125
  * The CSS anchor name to use for positioning.
125
126
  * Use this when you need to set anchorName manually (e.g., display:contents wrapper).
127
+ * Set it only while the layer is shown or its exit animation runs.
126
128
  */
127
129
  anchorId: string;
128
130
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"useHoverCard.d.ts","sourceRoot":"","sources":["../../src/HoverCard/useHoverCard.tsx"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAEH,OAAO,EAKL,KAAK,SAAS,EACd,KAAK,WAAW,EACjB,MAAM,OAAO,CAAC;AAGf,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AA8BlC;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEhE;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,iBAAiB,CAAC;AAEtD,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,GAAG,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAE9B;;;OAGG;IACH,WAAW,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAEtC;;;;OAIG;IACH,cAAc,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAEzC;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,EAAE,EAAE,MAAM,CAAC;IAEX;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;;;OAMG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;;;;OAOG;IACH,eAAe,EAAE,CACf,QAAQ,EAAE,SAAS,EACnB,KAAK,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,aAAa,CAAC,KAC5C,SAAS,CAAC;IAEf;;OAEG;IACH,IAAI,EAAE,MAAM,IAAI,CAAC;IAEjB;;OAEG;IACH,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;GAgBG;AAEH,wBAAgB,YAAY,CAAC,OAAO,GAAE,gBAAqB,GAAG,eAAe,CAke5E"}
1
+ {"version":3,"file":"useHoverCard.d.ts","sourceRoot":"","sources":["../../src/HoverCard/useHoverCard.tsx"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAEH,OAAO,EAKL,KAAK,SAAS,EACd,KAAK,WAAW,EACjB,MAAM,OAAO,CAAC;AAGf,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AA8BlC;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEhE;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,iBAAiB,CAAC;AAEtD,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,GAAG,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAE9B;;;;OAIG;IACH,WAAW,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAEtC;;;;OAIG;IACH,cAAc,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAEzC;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,EAAE,EAAE,MAAM,CAAC;IAEX;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;;;OAMG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;;;;OAOG;IACH,eAAe,EAAE,CACf,QAAQ,EAAE,SAAS,EACnB,KAAK,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,aAAa,CAAC,KAC5C,SAAS,CAAC;IAEf;;OAEG;IACH,IAAI,EAAE,MAAM,IAAI,CAAC;IAEjB;;OAEG;IACH,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;GAgBG;AAEH,wBAAgB,YAAY,CAAC,OAAO,GAAE,gBAAqB,GAAG,eAAe,CAke5E"}
@@ -194,12 +194,14 @@ export interface FixedLayerOptions extends BaseLayerOptions {
194
194
  export interface ContextLayerReturn {
195
195
  /**
196
196
  * Ref to attach to trigger element.
197
- * Injects anchorName style for CSS anchor positioning.
197
+ * While the layer is shown, and until its exit animation ends, the trigger
198
+ * carries the layer's CSS anchor name.
198
199
  */
199
200
  ref: RefCallback<HTMLElement>;
200
201
  /**
201
202
  * The CSS anchor name to use for positioning.
202
203
  * Use this when you need to set anchorName manually (e.g., display:contents wrapper).
204
+ * Set it only while the layer is shown or its exit animation runs.
203
205
  */
204
206
  anchorId: string;
205
207
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"useLayer.d.ts","sourceRoot":"","sources":["../../src/Layer/useLayer.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useLayer.d.ts","sourceRoot":"","sources":["../../src/Layer/useLayer.tsx"],"names":[],"mappings":"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"}
@@ -256,6 +256,20 @@ function untransform(rect, layer, style) {
256
256
  return new DOMRect(left, top, width, height);
257
257
  }
258
258
 
259
+ /**
260
+ * The finite animations still running on `layer` and its content. A layer
261
+ * that fades out keeps running them after the browser hides it, and it stays
262
+ * placed against its anchor until they end.
263
+ */
264
+ function exitAnimations(layer) {
265
+ if (layer == null || typeof layer.getAnimations !== 'function') {
266
+ return [];
267
+ }
268
+ return layer.getAnimations({
269
+ subtree: true
270
+ }).filter(animation => animation.playState === 'running' && animation.effect?.getComputedTiming().endTime !== Infinity);
271
+ }
272
+
259
273
  /**
260
274
  * The layer's box where its position option put it, without the transform an
261
275
  * entry animation (or anything else) applies to it: a Tooltip enters from a
@@ -837,6 +851,34 @@ function useLayerImplementation(options) {
837
851
  // stale-closure reads of the previous isOpen value.
838
852
  const isOpenRef = useRef(false);
839
853
 
854
+ // The exit of the layer that closed last, while it still paints. Its
855
+ // trigger stays an anchor until it ends, also when the trigger attaches
856
+ // again during it. Null once it has ended.
857
+ const exitRef = useRef(null);
858
+
859
+ // A closed trigger is no anchor, once the layer stops painting. A show
860
+ // during the exit keeps the name, and so does a later exit.
861
+ const releaseAnchor = useCallback(layer => {
862
+ const exit = {};
863
+ exitRef.current = exit;
864
+ const release = () => {
865
+ if (exitRef.current !== exit) {
866
+ return;
867
+ }
868
+ exitRef.current = null;
869
+ const trigger = triggerRef.current;
870
+ if (!isOpenRef.current && trigger != null) {
871
+ removeAnchorName(trigger, anchorId);
872
+ }
873
+ };
874
+ const running = exitAnimations(layer);
875
+ if (running.length === 0) {
876
+ release();
877
+ return;
878
+ }
879
+ void Promise.all(running.map(async animation => animation.finished.catch(() => undefined))).then(release);
880
+ }, [anchorId]);
881
+
840
882
  // The gesture during which the browser last closed this layer on its own.
841
883
  // Read through wasJustDismissed by triggers deciding whether a click is
842
884
  // theirs to act on.
@@ -1006,6 +1048,12 @@ function useLayerImplementation(options) {
1006
1048
  popover.removeAttribute(LAYER_CLEARS_ANCHOR_ATTRIBUTE);
1007
1049
  popover.removeAttribute(LAYER_SCROLLS_ATTRIBUTE);
1008
1050
  applyDeferredWritingContext(popover);
1051
+ // The trigger takes the layer's anchor name in the task that shows the
1052
+ // layer, so the layer's first frame is placed against it.
1053
+ const anchor = triggerRef.current;
1054
+ if (anchor != null) {
1055
+ addAnchorName(anchor, anchorId);
1056
+ }
1009
1057
  // A hide the browser runs itself (an Escape its close watcher answers)
1010
1058
  // is counted too, from `beforetoggle` until the element's `toggle`, a
1011
1059
  // task after the hide has returned. The show is asked of the browser
@@ -1013,6 +1061,11 @@ function useLayerImplementation(options) {
1013
1061
  // light dismiss at pointerup, then this show at the click); only a show
1014
1062
  // it refuses replays once the hide's count has ended.
1015
1063
  if (!showPopoverElement(popover)) {
1064
+ // The replay anchors the trigger again; until then it is closed. An
1065
+ // exit still running keeps the name.
1066
+ if (anchor != null && exitRef.current == null) {
1067
+ removeAnchorName(anchor, anchorId);
1068
+ }
1016
1069
  replayWhenSettled();
1017
1070
  return;
1018
1071
  }
@@ -1023,7 +1076,7 @@ function useLayerImplementation(options) {
1023
1076
  setIsOpen(true);
1024
1077
  onShow?.();
1025
1078
  }
1026
- }, [onShow, keepsAnchorClear, requestContextMount, applyDeferredWritingContext, showPopoverElement, isCurrentContextPopover, wasJustDismissed]);
1079
+ }, [anchorId, onShow, keepsAnchorClear, requestContextMount, applyDeferredWritingContext, showPopoverElement, isCurrentContextPopover, wasJustDismissed]);
1027
1080
  const hide = useCallback(() => {
1028
1081
  pendingShowRef.current = false;
1029
1082
  if (isOpenRef.current) {
@@ -1051,8 +1104,9 @@ function useLayerImplementation(options) {
1051
1104
  setIsOpen(false);
1052
1105
  onHide?.();
1053
1106
  }
1107
+ releaseAnchor(popoverRef.current);
1054
1108
  clearContextMount();
1055
- }, [onHide, clearContextMount]);
1109
+ }, [onHide, clearContextMount, releaseAnchor]);
1056
1110
 
1057
1111
  // A deferred show() replays through a ref so the queued closure reaches the
1058
1112
  // current callback rather than the one captured when it was queued.
@@ -1072,7 +1126,12 @@ function useLayerImplementation(options) {
1072
1126
  if (triggerRef.current && triggerRef.current !== el) {
1073
1127
  removeAnchorName(triggerRef.current, anchorId);
1074
1128
  }
1075
- if (el) {
1129
+
1130
+ // Only a shown layer's trigger is an anchor. WebKit's Linux ports run the
1131
+ // transform animation of an element that holds an anchor on the main
1132
+ // thread. A busy task would then freeze a surface that moves around a
1133
+ // closed trigger.
1134
+ if (el && (isOpenRef.current || exitRef.current != null)) {
1076
1135
  addAnchorName(el, anchorId);
1077
1136
  }
1078
1137
  const previous = triggerRef.current;
@@ -1171,11 +1230,12 @@ function useLayerImplementation(options) {
1171
1230
  openedPopoverRef.current = null;
1172
1231
  isOpenRef.current = false;
1173
1232
  rememberDismissal(e.currentTarget?.ownerDocument ?? document);
1233
+ releaseAnchor(e.currentTarget);
1174
1234
  setIsOpen(false);
1175
1235
  onHide?.();
1176
1236
  clearContextMount();
1177
1237
  }
1178
- }, [onHide, clearContextMount, rememberDismissal]);
1238
+ }, [onHide, clearContextMount, rememberDismissal, releaseAnchor]);
1179
1239
 
1180
1240
  // Ref callback for popover element — sets up the `toggle` listener.
1181
1241
  // Tracks the element + handler currently bound so the listener is removed
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @file NumberInput.tsx
3
- * @input Uses React, useId, useState, useMemo, useCallback, Field, Icon, InputGroupContext
4
- * @output Exports NumberInput component, NumberInputProps; optional steppers are hidden on coarse pointers
3
+ * @input Uses React, useId, useState, useMemo, useCallback, useEffect, Field, Icon, InputGroupContext
4
+ * @output Exports NumberInput component, NumberInputProps; optional steppers are hidden on coarse pointers, and a held one repeats into the draft
5
5
  * @position Core implementation; consumed by index.ts, tested by NumberInput.test.tsx
6
6
  *
7
7
  * SYNC: When modified, update these files to stay in sync:
@@ -188,6 +188,8 @@ interface NumberInputPropsBase extends Omit<FieldInputBaseProps, 'onChange' | 'd
188
188
  /**
189
189
  * Whether to show increment and decrement buttons at the end of the input.
190
190
  * The buttons are hidden on coarse pointers (such as touch screens).
191
+ * Holding a button with a mouse or a pen repeats the step and commits once,
192
+ * when the hold ends.
191
193
  * Direct text entry and keyboard stepping remain available.
192
194
  * @default false
193
195
  */
@@ -1 +1 @@
1
- {"version":3,"file":"NumberInput.d.ts","sourceRoot":"","sources":["../../src/NumberInput/NumberInput.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"NumberInput.d.ts","sourceRoot":"","sources":["../../src/NumberInput/NumberInput.tsx"],"names":[],"mappings":"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"}
@@ -4,8 +4,8 @@
4
4
 
5
5
  /**
6
6
  * @file NumberInput.tsx
7
- * @input Uses React, useId, useState, useMemo, useCallback, Field, Icon, InputGroupContext
8
- * @output Exports NumberInput component, NumberInputProps; optional steppers are hidden on coarse pointers
7
+ * @input Uses React, useId, useState, useMemo, useCallback, useEffect, Field, Icon, InputGroupContext
8
+ * @output Exports NumberInput component, NumberInputProps; optional steppers are hidden on coarse pointers, and a held one repeats into the draft
9
9
  * @position Core implementation; consumed by index.ts, tested by NumberInput.test.tsx
10
10
  *
11
11
  * SYNC: When modified, update these files to stay in sync:
@@ -17,7 +17,7 @@
17
17
  * - /apps/storybook/stories/NumberInput.stories.tsx (storybook stories)
18
18
  * - /packages/cli/assets/templates/blocks/components/NumberInput/ (showcase blocks)
19
19
  */
20
- import { use, useId, useState, useMemo, useCallback, useRef } from 'react';
20
+ import { use, useId, useState, useMemo, useCallback, useEffect, useRef } from 'react';
21
21
  import { flushSync } from 'react-dom';
22
22
  import * as stylex from '@stylexjs/stylex';
23
23
  import "../theme/tokens.stylex.js";
@@ -31,6 +31,7 @@ import { joinAriaIDs } from "../utils/inputAria.js";
31
31
  import { withholdCallerHandlers } from "../utils/disabledInteraction.js";
32
32
  import { interactionOverlayStyles } from "../utils/interactionOverlay.stylex.js";
33
33
  import { usePressFeedback } from "../hooks/usePressFeedback.js";
34
+ import { lockSelection, locksSelectionFor } from "../utils/selectionLock.js";
34
35
  import { inputDisabledFocusStyles } from "../Field/inputDisabledFocus.js";
35
36
  import { useSize } from "../SizeContext/SizeContext.js";
36
37
  import { useInputContainer } from "../hooks/useInputContainer.js";
@@ -143,6 +144,24 @@ registerEnglishFallback(english);
143
144
  * With `hasClear`, onChange also receives `null` when the user clears the input.
144
145
  */
145
146
 
147
+ /**
148
+ * A held stepper's timing: the first repeat comes `delayMs` after the press.
149
+ * Each later interval is `acceleration` times the one before, from
150
+ * `firstIntervalMs` down to `minIntervalMs`.
151
+ */
152
+ const STEPPER_HOLD = {
153
+ delayMs: 500,
154
+ firstIntervalMs: 250,
155
+ acceleration: 0.9,
156
+ minIntervalMs: 50
157
+ };
158
+
159
+ /**
160
+ * A held stepper press: its pointer, its next repeat, the value its repeats
161
+ * reached, whether the field stopped taking them, and the release of its
162
+ * selection lock.
163
+ */
164
+
146
165
  function getDecimalPlaces(value) {
147
166
  const [coefficient, exponentText] = String(value).toLowerCase().split('e');
148
167
  const fractionLength = coefficient.split('.')[1]?.length ?? 0;
@@ -286,6 +305,13 @@ export function NumberInput({
286
305
 
287
306
  // Pending input while user is typing (null = show formatted value)
288
307
  const [pendingInput, setPendingInput] = useState(null);
308
+ // The value a held stepper's repeats reached, shown until the hold ends.
309
+ const [holdDraft, setHoldDraft] = useState(null);
310
+ // A field that turns disabled or read-only drops the held value during
311
+ // render, so no commit shows a value that the release does not commit.
312
+ if (holdDraft !== null && (isDisabled || isReadOnly)) {
313
+ setHoldDraft(null);
314
+ }
289
315
  const [isFocused, setIsFocused] = useState(false);
290
316
 
291
317
  // Disabled-reason tooltip. The tooltip listeners attach to the input container (which already exists) and
@@ -340,6 +366,9 @@ export function NumberInput({
340
366
  // Preserve pending text while editing. Otherwise show the formatted value
341
367
  // at rest and the raw numeric value while focused so it remains editable.
342
368
  const displayValue = useMemo(() => {
369
+ if (holdDraft !== null) {
370
+ return formatEditableNumber(holdDraft, locale);
371
+ }
343
372
  if (pendingInput !== null) {
344
373
  return pendingInput;
345
374
  }
@@ -347,15 +376,19 @@ export function NumberInput({
347
376
  return '';
348
377
  }
349
378
  return isFocused ? formatEditableNumber(value, locale) : formattedValue;
350
- }, [formattedValue, isFocused, locale, pendingInput, value]);
379
+ }, [formattedValue, holdDraft, isFocused, locale, pendingInput, value]);
351
380
 
352
381
  // Check if current pending input is valid (for styling purposes)
353
382
  const isInputValid = useMemo(() => {
383
+ // A held value is a step on the grid, so it is always valid.
384
+ if (holdDraft !== null) {
385
+ return true;
386
+ }
354
387
  if (pendingInput === null || !pendingInput.trim()) {
355
388
  return true;
356
389
  }
357
390
  return parseInput(pendingInput) !== null;
358
- }, [pendingInput, parseInput]);
391
+ }, [holdDraft, pendingInput, parseInput]);
359
392
 
360
393
  // Keep the whole text edit as a draft until an explicit commit boundary.
361
394
  const handleInputChange = useCallback(e => {
@@ -457,6 +490,152 @@ export function NumberInput({
457
490
  }
458
491
  }, [getNextValue, isDisabled, isReadOnly, onChange, value]);
459
492
 
493
+ // A held stepper repeats into its own draft and commits once, when the hold
494
+ // ends.
495
+ const stepperHoldRef = useRef(null);
496
+ const isClickAfterHoldRef = useRef(false);
497
+ // The pointer of a repeated hold that another press cancelled. Its own
498
+ // release still swallows the click that follows.
499
+ const cancelledHoldPointerRef = useRef(null);
500
+
501
+ // Ends a hold with no commit, and drops the draft its repeats wrote.
502
+ const cancelStepperHold = useCallback(() => {
503
+ const hold = stepperHoldRef.current;
504
+ if (hold == null) {
505
+ return;
506
+ }
507
+ stepperHoldRef.current = null;
508
+ cancelledHoldPointerRef.current = hold.held != null ? hold.pointerId : null;
509
+ if (hold.timer != null) {
510
+ clearTimeout(hold.timer);
511
+ }
512
+ hold.unlockSelection?.();
513
+ setHoldDraft(null);
514
+ }, []);
515
+
516
+ // Syncs the held stepper's repeat timer with the field's editability: a
517
+ // field that turns disabled or read-only stops the repeats, and the release
518
+ // commits nothing.
519
+ useEffect(() => {
520
+ const hold = stepperHoldRef.current;
521
+ if (hold == null || !(isDisabled || isReadOnly)) {
522
+ return;
523
+ }
524
+ hold.isFrozen = true;
525
+ if (hold.timer != null) {
526
+ clearTimeout(hold.timer);
527
+ hold.timer = null;
528
+ }
529
+ }, [isDisabled, isReadOnly]);
530
+
531
+ // The stepper column's removal or unmount ends the hold with no commit, as
532
+ // its buttons can no longer hear the release.
533
+ const stepperColumnRef = useCallback(() => () => {
534
+ cancelStepperHold();
535
+ }, [cancelStepperHold]);
536
+ const startStepperHold = (direction, canStep, event) => {
537
+ isClickAfterHoldRef.current = false;
538
+ if (cancelledHoldPointerRef.current === event.pointerId) {
539
+ cancelledHoldPointerRef.current = null;
540
+ }
541
+ // Any new press ends a running hold, whichever pointer made it.
542
+ cancelStepperHold();
543
+ // Steppers render only under a non-coarse primary pointer, so a hold is a
544
+ // mouse or pen press. A finger keeps the single step of its click.
545
+ if (event.button !== 0 || event.pointerType === 'touch' || !canStep || isDisabled || isReadOnly) {
546
+ return;
547
+ }
548
+ const first = getNextValue(direction);
549
+ if (first == null) {
550
+ return;
551
+ }
552
+ // The release ends the hold wherever the pointer is by then.
553
+ event.currentTarget.setPointerCapture?.(event.pointerId);
554
+ const grid = {
555
+ direction,
556
+ min,
557
+ max,
558
+ step,
559
+ isIntegerOnly
560
+ };
561
+ const hold = {
562
+ pointerId: event.pointerId,
563
+ timer: null,
564
+ held: null,
565
+ intervalMs: STEPPER_HOLD.firstIntervalMs,
566
+ isFrozen: false,
567
+ // A pen's hold starts no text selection or callout.
568
+ unlockSelection: locksSelectionFor(event.pointerType) ? lockSelection([event.currentTarget], event.target) : null
569
+ };
570
+ const repeat = () => {
571
+ const next = hold.held == null ? first : getSteppedValue({
572
+ currentValue: hold.held,
573
+ ...grid
574
+ });
575
+ // At a bound the repeats stop, and the release still commits.
576
+ if (next == null || next === hold.held) {
577
+ hold.timer = null;
578
+ return;
579
+ }
580
+ hold.held = next;
581
+ setHoldDraft(next);
582
+ hold.timer = setTimeout(repeat, hold.intervalMs);
583
+ hold.intervalMs = Math.max(STEPPER_HOLD.minIntervalMs, hold.intervalMs * STEPPER_HOLD.acceleration);
584
+ };
585
+ hold.timer = setTimeout(repeat, STEPPER_HOLD.delayMs);
586
+ stepperHoldRef.current = hold;
587
+ };
588
+ const handleStepperContextMenu = event => {
589
+ const hold = stepperHoldRef.current;
590
+ if (hold == null) {
591
+ return;
592
+ }
593
+ // A pen's hold opens no callout menu. A mouse's context request, such as
594
+ // a Control-click, ends its hold with no commit and opens the menu.
595
+ if (hold.unlockSelection != null) {
596
+ event.preventDefault();
597
+ return;
598
+ }
599
+ cancelStepperHold();
600
+ // A click that follows the menu steps nothing, as after any ended hold.
601
+ cancelledHoldPointerRef.current = hold.pointerId;
602
+ isClickAfterHoldRef.current = true;
603
+ };
604
+ const endStepperHold = (end, pointerId) => {
605
+ const hold = stepperHoldRef.current;
606
+ // Only the pointer that holds can end the hold.
607
+ if (hold == null || hold.pointerId !== pointerId) {
608
+ if (cancelledHoldPointerRef.current === pointerId) {
609
+ cancelledHoldPointerRef.current = null;
610
+ // Only arm the swallow here: a lost capture must not clear it.
611
+ if (end === 'release') {
612
+ isClickAfterHoldRef.current = true;
613
+ }
614
+ }
615
+ return;
616
+ }
617
+ stepperHoldRef.current = null;
618
+ if (hold.timer != null) {
619
+ clearTimeout(hold.timer);
620
+ }
621
+ hold.unlockSelection?.();
622
+ // A press released before its first repeat is a click, and its click
623
+ // steps.
624
+ if (hold.held == null) {
625
+ return;
626
+ }
627
+ isClickAfterHoldRef.current = end === 'release';
628
+ setHoldDraft(null);
629
+ // A frozen hold commits nothing, so a typed draft under it stays.
630
+ if (hold.isFrozen || isDisabled || isReadOnly) {
631
+ return;
632
+ }
633
+ setPendingInput(null);
634
+ if (hold.held !== value) {
635
+ onChange(hold.held);
636
+ }
637
+ };
638
+
460
639
  // Handle keyboard events
461
640
  const handleKeyDown = useCallback(e => {
462
641
  if (isDisabled) {
@@ -514,8 +693,31 @@ export function NumberInput({
514
693
  return () => input.removeEventListener('wheel', handleWheel);
515
694
  }, [isDisabled, isReadOnly, isWheelEnabled, stepValue]);
516
695
  const mergedInputRef = useMemo(() => mergeRefs(ref, inputRef, wheelListenerRef), [ref, wheelListenerRef]);
517
- const canIncrement = getNextValue(1) !== valueForStepping;
518
- const canDecrement = getNextValue(-1) !== valueForStepping;
696
+
697
+ // While a hold runs, the steppers follow the value that it shows.
698
+ const {
699
+ canIncrement,
700
+ canDecrement
701
+ } = useMemo(() => {
702
+ const base = holdDraft ?? valueForStepping;
703
+ const grid = {
704
+ currentValue: base,
705
+ min,
706
+ max,
707
+ step,
708
+ isIntegerOnly
709
+ };
710
+ return {
711
+ canIncrement: getSteppedValue({
712
+ ...grid,
713
+ direction: 1
714
+ }) !== base,
715
+ canDecrement: getSteppedValue({
716
+ ...grid,
717
+ direction: -1
718
+ }) !== base
719
+ };
720
+ }, [holdDraft, isIntegerOnly, max, min, step, valueForStepping]);
519
721
 
520
722
  // Handle clear button click
521
723
  const handleClear = useCallback(e => {
@@ -638,6 +840,7 @@ export function NumberInput({
638
840
  }),
639
841
  onClick: handleClear
640
842
  }), statusIcon, hasNumberSteppers && /*#__PURE__*/_jsxs("div", {
843
+ ref: stepperColumnRef,
641
844
  ...{
642
845
  className: "xkh2ocl x78zum5 xhj50sb xdt5ytf x2lah0s x12xnipv x5j3pgv x13i4uq0 xgbv0en x1t7ytsu x14ydlgx xb3r6kr x1g2gkjz xoao14b"
643
846
  },
@@ -649,8 +852,23 @@ export function NumberInput({
649
852
  "aria-label": t('@astryx.numberInput.incrementLabel', {
650
853
  label
651
854
  }),
652
- onPointerDown: event => event.preventDefault(),
653
- onClick: () => {
855
+ onPointerDown: event => {
856
+ event.preventDefault();
857
+ startStepperHold(1, canIncrement, event);
858
+ },
859
+ onContextMenu: handleStepperContextMenu,
860
+ onPointerUp: event => endStepperHold('release', event.pointerId),
861
+ onPointerCancel: event => endStepperHold('cancel', event.pointerId),
862
+ onLostPointerCapture: event => endStepperHold('cancel', event.pointerId),
863
+ onClick: event => {
864
+ const isAfterHold = isClickAfterHoldRef.current;
865
+ isClickAfterHoldRef.current = false;
866
+ // A pointer's click after a held press's release steps nothing.
867
+ // A click with no pointer, such as a screen reader's, steps.
868
+ if (isAfterHold && event.detail > 0) {
869
+ inputRef.current?.focus();
870
+ return;
871
+ }
654
872
  // An aria-disabled stepper does nothing, as a natively disabled
655
873
  // one did (spec:AST-085 FR2).
656
874
  if (isDisabled || isReadOnly || !canIncrement) {
@@ -674,8 +892,23 @@ export function NumberInput({
674
892
  "aria-label": t('@astryx.numberInput.decrementLabel', {
675
893
  label
676
894
  }),
677
- onPointerDown: event => event.preventDefault(),
678
- onClick: () => {
895
+ onPointerDown: event => {
896
+ event.preventDefault();
897
+ startStepperHold(-1, canDecrement, event);
898
+ },
899
+ onContextMenu: handleStepperContextMenu,
900
+ onPointerUp: event => endStepperHold('release', event.pointerId),
901
+ onPointerCancel: event => endStepperHold('cancel', event.pointerId),
902
+ onLostPointerCapture: event => endStepperHold('cancel', event.pointerId),
903
+ onClick: event => {
904
+ const isAfterHold = isClickAfterHoldRef.current;
905
+ isClickAfterHoldRef.current = false;
906
+ // A pointer's click after a held press's release steps nothing.
907
+ // A click with no pointer, such as a screen reader's, steps.
908
+ if (isAfterHold && event.detail > 0) {
909
+ inputRef.current?.focus();
910
+ return;
911
+ }
679
912
  // An aria-disabled stepper does nothing, as a natively disabled
680
913
  // one did (spec:AST-085 FR2).
681
914
  if (isDisabled || isReadOnly || !canDecrement) {
@@ -173,6 +173,7 @@ export interface UsePopoverReturn {
173
173
  /**
174
174
  * The CSS anchor name to use for positioning.
175
175
  * Use when you need to set anchorName manually (e.g., display:contents wrapper).
176
+ * Set it only while the layer is shown or its exit animation runs.
176
177
  */
177
178
  anchorId: string;
178
179
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"usePopover.d.ts","sourceRoot":"","sources":["../../src/Popover/usePopover.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,EAAiC,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAE5E,OAAO,EAAW,KAAK,kBAAkB,EAAC,MAAM,mBAAmB,CAAC;AAGpE,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,kBAAkB,CAAC;AAgBnD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAUhD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,yCAAyC,CAAC;AA4E9E;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IAEtB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAE5B;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B;;;;;;;;;;OAUG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IAEzB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;;;;;;;;;;;OAaG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,UAAU,EAAE,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;IAE7C;;;;OAIG;IACH,UAAU,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAEnD;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;;OAIG;IACH,IAAI,EAAE,CAAC,OAAO,CAAC,EAAE;QAAC,aAAa,CAAC,EAAE,OAAO,CAAA;KAAC,KAAK,IAAI,CAAC;IAEpD;;OAEG;IACH,IAAI,EAAE,MAAM,IAAI,CAAC;IAEjB;;OAEG;IACH,MAAM,EAAE,MAAM,IAAI,CAAC;IAEnB;;OAEG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;OAEG;IACH,EAAE,EAAE,MAAM,CAAC;IAEX;;;;;;;;;;;OAWG;IACH,MAAM,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,EAAE,kBAAkB,KAAK,SAAS,CAAC;IAEvE;;OAEG;IACH,YAAY,EAAE;QACZ,eAAe,EAAE,QAAQ,GAAG,MAAM,CAAC;QACnC,eAAe,EAAE,OAAO,CAAC;QACzB,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AA4TD,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CAE5E"}
1
+ {"version":3,"file":"usePopover.d.ts","sourceRoot":"","sources":["../../src/Popover/usePopover.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,EAAiC,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAE5E,OAAO,EAAW,KAAK,kBAAkB,EAAC,MAAM,mBAAmB,CAAC;AAGpE,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,kBAAkB,CAAC;AAgBnD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAUhD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,yCAAyC,CAAC;AA4E9E;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IAEtB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAE5B;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B;;;;;;;;;;OAUG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IAEzB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;;;;;;;;;;;OAaG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,UAAU,EAAE,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;IAE7C;;;;OAIG;IACH,UAAU,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAEnD;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;;OAIG;IACH,IAAI,EAAE,CAAC,OAAO,CAAC,EAAE;QAAC,aAAa,CAAC,EAAE,OAAO,CAAA;KAAC,KAAK,IAAI,CAAC;IAEpD;;OAEG;IACH,IAAI,EAAE,MAAM,IAAI,CAAC;IAEjB;;OAEG;IACH,MAAM,EAAE,MAAM,IAAI,CAAC;IAEnB;;OAEG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;OAEG;IACH,EAAE,EAAE,MAAM,CAAC;IAEX;;;;;;;;;;;OAWG;IACH,MAAM,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,EAAE,kBAAkB,KAAK,SAAS,CAAC;IAEvE;;OAEG;IACH,YAAY,EAAE;QACZ,eAAe,EAAE,QAAQ,GAAG,MAAM,CAAC;QACnC,eAAe,EAAE,OAAO,CAAC;QACzB,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AA4TD,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CAE5E"}
@@ -104,7 +104,8 @@ export interface TooltipReturn {
104
104
  ref: RefCallback<HTMLElement>;
105
105
  /**
106
106
  * Ref for the positioning anchor element.
107
- * Injects anchorName style for CSS anchor positioning.
107
+ * While the layer is shown, and until its exit animation ends, the trigger
108
+ * carries the layer's CSS anchor name.
108
109
  */
109
110
  positionRef: RefCallback<HTMLElement>;
110
111
  /**
@@ -116,6 +117,7 @@ export interface TooltipReturn {
116
117
  /**
117
118
  * The CSS anchor name to use for positioning.
118
119
  * Use this when you need to set anchorName manually (e.g., display:contents wrapper).
120
+ * Set it only while the layer is shown or its exit animation runs.
119
121
  */
120
122
  anchorId: string;
121
123
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"useTooltip.d.ts","sourceRoot":"","sources":["../../src/Tooltip/useTooltip.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AAEH,OAAO,EAKL,KAAK,SAAS,EACd,KAAK,WAAW,EACjB,MAAM,OAAO,CAAC;AAGf,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AA8ClC;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE9D;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;AAEpD,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAEnC;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAEnC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,GAAG,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAE9B;;;OAGG;IACH,WAAW,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAEtC;;;;OAIG;IACH,cAAc,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAEzC;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;;;;OAOG;IACH,aAAa,EAAE,CACb,QAAQ,EAAE,SAAS,EACnB,KAAK,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,aAAa,CAAC,KAC5C,SAAS,CAAC;IAEf;;;;;;;;;;;;OAYG;IACH,IAAI,EAAE,MAAM,IAAI,CAAC;IAEjB;;;OAGG;IACH,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,wBAAgB,UAAU,CAAC,OAAO,GAAE,cAAmB,GAAG,aAAa,CAosBtE"}
1
+ {"version":3,"file":"useTooltip.d.ts","sourceRoot":"","sources":["../../src/Tooltip/useTooltip.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AAEH,OAAO,EAKL,KAAK,SAAS,EACd,KAAK,WAAW,EACjB,MAAM,OAAO,CAAC;AAGf,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AA8ClC;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE9D;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;AAEpD,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAEnC;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAEnC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,GAAG,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAE9B;;;;OAIG;IACH,WAAW,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAEtC;;;;OAIG;IACH,cAAc,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAEzC;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;;;;OAOG;IACH,aAAa,EAAE,CACb,QAAQ,EAAE,SAAS,EACnB,KAAK,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,aAAa,CAAC,KAC5C,SAAS,CAAC;IAEf;;;;;;;;;;;;OAYG;IACH,IAAI,EAAE,MAAM,IAAI,CAAC;IAEjB;;;OAGG;IACH,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,wBAAgB,UAAU,CAAC,OAAO,GAAE,cAAmB,GAAG,aAAa,CAosBtE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.7.9-canary.4e7823a",
3
+ "version": "0.7.9-canary.57aa93c",
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.9-canary.4e7823a",
874
- "@astryxdesign/cli": "0.7.9-canary.4e7823a",
875
- "@astryxdesign/interaction-harness": "0.7.9-canary.4e7823a",
873
+ "@astryxdesign/a11y-spec": "0.7.9-canary.57aa93c",
874
+ "@astryxdesign/cli": "0.7.9-canary.57aa93c",
875
+ "@astryxdesign/interaction-harness": "0.7.9-canary.57aa93c",
876
876
  "@babel/cli": "^7.29.7",
877
877
  "@babel/core": "^7.29.7",
878
878
  "@babel/preset-react": "^7.29.7",
@@ -177,7 +177,8 @@ export interface HoverCardReturn {
177
177
 
178
178
  /**
179
179
  * Ref for the positioning anchor element.
180
- * Injects anchorName style for CSS anchor positioning.
180
+ * While the layer is shown, and until its exit animation ends, the trigger
181
+ * carries the layer's CSS anchor name.
181
182
  */
182
183
  positionRef: RefCallback<HTMLElement>;
183
184
 
@@ -191,6 +192,7 @@ export interface HoverCardReturn {
191
192
  /**
192
193
  * The CSS anchor name to use for positioning.
193
194
  * Use this when you need to set anchorName manually (e.g., display:contents wrapper).
195
+ * Set it only while the layer is shown or its exit animation runs.
194
196
  */
195
197
  anchorId: string;
196
198
 
@@ -354,13 +354,15 @@ export interface FixedLayerOptions extends BaseLayerOptions {
354
354
  export interface ContextLayerReturn {
355
355
  /**
356
356
  * Ref to attach to trigger element.
357
- * Injects anchorName style for CSS anchor positioning.
357
+ * While the layer is shown, and until its exit animation ends, the trigger
358
+ * carries the layer's CSS anchor name.
358
359
  */
359
360
  ref: RefCallback<HTMLElement>;
360
361
 
361
362
  /**
362
363
  * The CSS anchor name to use for positioning.
363
364
  * Use this when you need to set anchorName manually (e.g., display:contents wrapper).
365
+ * Set it only while the layer is shown or its exit animation runs.
364
366
  */
365
367
  anchorId: string;
366
368
 
@@ -547,6 +549,24 @@ function untransform(
547
549
  return new DOMRect(left, top, width, height);
548
550
  }
549
551
 
552
+ /**
553
+ * The finite animations still running on `layer` and its content. A layer
554
+ * that fades out keeps running them after the browser hides it, and it stays
555
+ * placed against its anchor until they end.
556
+ */
557
+ function exitAnimations(layer: HTMLElement | null): Animation[] {
558
+ if (layer == null || typeof layer.getAnimations !== 'function') {
559
+ return [];
560
+ }
561
+ return layer
562
+ .getAnimations({subtree: true})
563
+ .filter(
564
+ animation =>
565
+ animation.playState === 'running' &&
566
+ animation.effect?.getComputedTiming().endTime !== Infinity,
567
+ );
568
+ }
569
+
550
570
  /**
551
571
  * The layer's box where its position option put it, without the transform an
552
572
  * entry animation (or anything else) applies to it: a Tooltip enters from a
@@ -1216,6 +1236,41 @@ function useLayerImplementation(
1216
1236
  // stale-closure reads of the previous isOpen value.
1217
1237
  const isOpenRef = useRef(false);
1218
1238
 
1239
+ // The exit of the layer that closed last, while it still paints. Its
1240
+ // trigger stays an anchor until it ends, also when the trigger attaches
1241
+ // again during it. Null once it has ended.
1242
+ const exitRef = useRef<object | null>(null);
1243
+
1244
+ // A closed trigger is no anchor, once the layer stops painting. A show
1245
+ // during the exit keeps the name, and so does a later exit.
1246
+ const releaseAnchor = useCallback(
1247
+ (layer: HTMLElement | null) => {
1248
+ const exit = {};
1249
+ exitRef.current = exit;
1250
+ const release = () => {
1251
+ if (exitRef.current !== exit) {
1252
+ return;
1253
+ }
1254
+ exitRef.current = null;
1255
+ const trigger = triggerRef.current;
1256
+ if (!isOpenRef.current && trigger != null) {
1257
+ removeAnchorName(trigger, anchorId);
1258
+ }
1259
+ };
1260
+ const running = exitAnimations(layer);
1261
+ if (running.length === 0) {
1262
+ release();
1263
+ return;
1264
+ }
1265
+ void Promise.all(
1266
+ running.map(async animation =>
1267
+ animation.finished.catch(() => undefined),
1268
+ ),
1269
+ ).then(release);
1270
+ },
1271
+ [anchorId],
1272
+ );
1273
+
1219
1274
  // The gesture during which the browser last closed this layer on its own.
1220
1275
  // Read through wasJustDismissed by triggers deciding whether a click is
1221
1276
  // theirs to act on.
@@ -1416,6 +1471,12 @@ function useLayerImplementation(
1416
1471
  popover.removeAttribute(LAYER_CLEARS_ANCHOR_ATTRIBUTE);
1417
1472
  popover.removeAttribute(LAYER_SCROLLS_ATTRIBUTE);
1418
1473
  applyDeferredWritingContext(popover);
1474
+ // The trigger takes the layer's anchor name in the task that shows the
1475
+ // layer, so the layer's first frame is placed against it.
1476
+ const anchor = triggerRef.current;
1477
+ if (anchor != null) {
1478
+ addAnchorName(anchor, anchorId);
1479
+ }
1419
1480
  // A hide the browser runs itself (an Escape its close watcher answers)
1420
1481
  // is counted too, from `beforetoggle` until the element's `toggle`, a
1421
1482
  // task after the hide has returned. The show is asked of the browser
@@ -1423,6 +1484,11 @@ function useLayerImplementation(
1423
1484
  // light dismiss at pointerup, then this show at the click); only a show
1424
1485
  // it refuses replays once the hide's count has ended.
1425
1486
  if (!showPopoverElement(popover)) {
1487
+ // The replay anchors the trigger again; until then it is closed. An
1488
+ // exit still running keeps the name.
1489
+ if (anchor != null && exitRef.current == null) {
1490
+ removeAnchorName(anchor, anchorId);
1491
+ }
1426
1492
  replayWhenSettled();
1427
1493
  return;
1428
1494
  }
@@ -1434,6 +1500,7 @@ function useLayerImplementation(
1434
1500
  onShow?.();
1435
1501
  }
1436
1502
  }, [
1503
+ anchorId,
1437
1504
  onShow,
1438
1505
  keepsAnchorClear,
1439
1506
  requestContextMount,
@@ -1470,8 +1537,9 @@ function useLayerImplementation(
1470
1537
  setIsOpen(false);
1471
1538
  onHide?.();
1472
1539
  }
1540
+ releaseAnchor(popoverRef.current);
1473
1541
  clearContextMount();
1474
- }, [onHide, clearContextMount]);
1542
+ }, [onHide, clearContextMount, releaseAnchor]);
1475
1543
 
1476
1544
  // A deferred show() replays through a ref so the queued closure reaches the
1477
1545
  // current callback rather than the one captured when it was queued.
@@ -1493,7 +1561,11 @@ function useLayerImplementation(
1493
1561
  removeAnchorName(triggerRef.current, anchorId);
1494
1562
  }
1495
1563
 
1496
- if (el) {
1564
+ // Only a shown layer's trigger is an anchor. WebKit's Linux ports run the
1565
+ // transform animation of an element that holds an anchor on the main
1566
+ // thread. A busy task would then freeze a surface that moves around a
1567
+ // closed trigger.
1568
+ if (el && (isOpenRef.current || exitRef.current != null)) {
1497
1569
  addAnchorName(el, anchorId);
1498
1570
  }
1499
1571
 
@@ -1603,12 +1675,13 @@ function useLayerImplementation(
1603
1675
  rememberDismissal(
1604
1676
  (e.currentTarget as HTMLElement | null)?.ownerDocument ?? document,
1605
1677
  );
1678
+ releaseAnchor(e.currentTarget as HTMLElement | null);
1606
1679
  setIsOpen(false);
1607
1680
  onHide?.();
1608
1681
  clearContextMount();
1609
1682
  }
1610
1683
  },
1611
- [onHide, clearContextMount, rememberDismissal],
1684
+ [onHide, clearContextMount, rememberDismissal, releaseAnchor],
1612
1685
  );
1613
1686
 
1614
1687
  // Ref callback for popover element — sets up the `toggle` listener.
@@ -160,7 +160,7 @@ export const docs = {
160
160
  name: 'hasNumberSteppers',
161
161
  type: 'boolean',
162
162
  description:
163
- 'Shows increment and decrement buttons at the end of the input on fine pointers. Hidden on coarse pointers (such as touch screens); text entry and keyboard stepping remain available.',
163
+ 'Shows increment and decrement buttons at the end of the input on fine pointers. Hidden on coarse pointers (such as touch screens); text entry and keyboard stepping remain available. Holding a button with a mouse or a pen repeats the step and commits once, when the hold ends.',
164
164
  default: 'false',
165
165
  },
166
166
  {
@@ -4,8 +4,8 @@
4
4
 
5
5
  /**
6
6
  * @file NumberInput.tsx
7
- * @input Uses React, useId, useState, useMemo, useCallback, Field, Icon, InputGroupContext
8
- * @output Exports NumberInput component, NumberInputProps; optional steppers are hidden on coarse pointers
7
+ * @input Uses React, useId, useState, useMemo, useCallback, useEffect, Field, Icon, InputGroupContext
8
+ * @output Exports NumberInput component, NumberInputProps; optional steppers are hidden on coarse pointers, and a held one repeats into the draft
9
9
  * @position Core implementation; consumed by index.ts, tested by NumberInput.test.tsx
10
10
  *
11
11
  * SYNC: When modified, update these files to stay in sync:
@@ -24,9 +24,12 @@ import {
24
24
  useState,
25
25
  useMemo,
26
26
  useCallback,
27
+ useEffect,
27
28
  useRef,
28
29
  type FocusEvent,
29
30
  type KeyboardEvent,
31
+ type MouseEvent,
32
+ type PointerEvent,
30
33
  type ReactNode,
31
34
  } from 'react';
32
35
  import {flushSync} from 'react-dom';
@@ -56,6 +59,7 @@ import {joinAriaIDs} from '../utils/inputAria';
56
59
  import {withholdCallerHandlers} from '../utils/disabledInteraction';
57
60
  import {interactionOverlayStyles} from '../utils/interactionOverlay.stylex';
58
61
  import {usePressFeedback} from '../hooks/usePressFeedback';
62
+ import {lockSelection, locksSelectionFor} from '../utils/selectionLock';
59
63
  import {inputDisabledFocusStyles} from '../Field/inputDisabledFocus';
60
64
  import {useSize} from '../SizeContext/SizeContext';
61
65
  import {useInputContainer} from '../hooks/useInputContainer';
@@ -403,6 +407,8 @@ interface NumberInputPropsBase extends Omit<
403
407
  /**
404
408
  * Whether to show increment and decrement buttons at the end of the input.
405
409
  * The buttons are hidden on coarse pointers (such as touch screens).
410
+ * Holding a button with a mouse or a pen repeats the step and commits once,
411
+ * when the hold ends.
406
412
  * Direct text entry and keyboard stepping remain available.
407
413
  * @default false
408
414
  */
@@ -460,6 +466,32 @@ export type NumberInputProps =
460
466
 
461
467
  type StepDirection = -1 | 1;
462
468
 
469
+ /**
470
+ * A held stepper's timing: the first repeat comes `delayMs` after the press.
471
+ * Each later interval is `acceleration` times the one before, from
472
+ * `firstIntervalMs` down to `minIntervalMs`.
473
+ */
474
+ const STEPPER_HOLD = {
475
+ delayMs: 500,
476
+ firstIntervalMs: 250,
477
+ acceleration: 0.9,
478
+ minIntervalMs: 50,
479
+ } as const;
480
+
481
+ /**
482
+ * A held stepper press: its pointer, its next repeat, the value its repeats
483
+ * reached, whether the field stopped taking them, and the release of its
484
+ * selection lock.
485
+ */
486
+ type StepperHold = {
487
+ pointerId: number;
488
+ timer: ReturnType<typeof setTimeout> | null;
489
+ held: number | null;
490
+ intervalMs: number;
491
+ isFrozen: boolean;
492
+ unlockSelection: (() => void) | null;
493
+ };
494
+
463
495
  function getDecimalPlaces(value: number): number {
464
496
  const [coefficient, exponentText] = String(value).toLowerCase().split('e');
465
497
  const fractionLength = coefficient.split('.')[1]?.length ?? 0;
@@ -625,6 +657,13 @@ export function NumberInput({
625
657
 
626
658
  // Pending input while user is typing (null = show formatted value)
627
659
  const [pendingInput, setPendingInput] = useState<string | null>(null);
660
+ // The value a held stepper's repeats reached, shown until the hold ends.
661
+ const [holdDraft, setHoldDraft] = useState<number | null>(null);
662
+ // A field that turns disabled or read-only drops the held value during
663
+ // render, so no commit shows a value that the release does not commit.
664
+ if (holdDraft !== null && (isDisabled || isReadOnly)) {
665
+ setHoldDraft(null);
666
+ }
628
667
  const [isFocused, setIsFocused] = useState(false);
629
668
 
630
669
  // Disabled-reason tooltip. The tooltip listeners attach to the input container (which already exists) and
@@ -689,6 +728,9 @@ export function NumberInput({
689
728
  // Preserve pending text while editing. Otherwise show the formatted value
690
729
  // at rest and the raw numeric value while focused so it remains editable.
691
730
  const displayValue = useMemo(() => {
731
+ if (holdDraft !== null) {
732
+ return formatEditableNumber(holdDraft, locale);
733
+ }
692
734
  if (pendingInput !== null) {
693
735
  return pendingInput;
694
736
  }
@@ -696,15 +738,19 @@ export function NumberInput({
696
738
  return '';
697
739
  }
698
740
  return isFocused ? formatEditableNumber(value, locale) : formattedValue;
699
- }, [formattedValue, isFocused, locale, pendingInput, value]);
741
+ }, [formattedValue, holdDraft, isFocused, locale, pendingInput, value]);
700
742
 
701
743
  // Check if current pending input is valid (for styling purposes)
702
744
  const isInputValid = useMemo(() => {
745
+ // A held value is a step on the grid, so it is always valid.
746
+ if (holdDraft !== null) {
747
+ return true;
748
+ }
703
749
  if (pendingInput === null || !pendingInput.trim()) {
704
750
  return true;
705
751
  }
706
752
  return parseInput(pendingInput) !== null;
707
- }, [pendingInput, parseInput]);
753
+ }, [holdDraft, pendingInput, parseInput]);
708
754
 
709
755
  // Keep the whole text edit as a draft until an explicit commit boundary.
710
756
  const handleInputChange = useCallback(
@@ -850,6 +896,167 @@ export function NumberInput({
850
896
  [getNextValue, isDisabled, isReadOnly, onChange, value],
851
897
  );
852
898
 
899
+ // A held stepper repeats into its own draft and commits once, when the hold
900
+ // ends.
901
+ const stepperHoldRef = useRef<StepperHold | null>(null);
902
+ const isClickAfterHoldRef = useRef(false);
903
+ // The pointer of a repeated hold that another press cancelled. Its own
904
+ // release still swallows the click that follows.
905
+ const cancelledHoldPointerRef = useRef<number | null>(null);
906
+
907
+ // Ends a hold with no commit, and drops the draft its repeats wrote.
908
+ const cancelStepperHold = useCallback(() => {
909
+ const hold = stepperHoldRef.current;
910
+ if (hold == null) {
911
+ return;
912
+ }
913
+ stepperHoldRef.current = null;
914
+ cancelledHoldPointerRef.current = hold.held != null ? hold.pointerId : null;
915
+ if (hold.timer != null) {
916
+ clearTimeout(hold.timer);
917
+ }
918
+ hold.unlockSelection?.();
919
+ setHoldDraft(null);
920
+ }, []);
921
+
922
+ // Syncs the held stepper's repeat timer with the field's editability: a
923
+ // field that turns disabled or read-only stops the repeats, and the release
924
+ // commits nothing.
925
+ useEffect(() => {
926
+ const hold = stepperHoldRef.current;
927
+ if (hold == null || !(isDisabled || isReadOnly)) {
928
+ return;
929
+ }
930
+ hold.isFrozen = true;
931
+ if (hold.timer != null) {
932
+ clearTimeout(hold.timer);
933
+ hold.timer = null;
934
+ }
935
+ }, [isDisabled, isReadOnly]);
936
+
937
+ // The stepper column's removal or unmount ends the hold with no commit, as
938
+ // its buttons can no longer hear the release.
939
+ const stepperColumnRef = useCallback(
940
+ () => () => {
941
+ cancelStepperHold();
942
+ },
943
+ [cancelStepperHold],
944
+ );
945
+
946
+ const startStepperHold = (
947
+ direction: StepDirection,
948
+ canStep: boolean,
949
+ event: PointerEvent<HTMLButtonElement>,
950
+ ) => {
951
+ isClickAfterHoldRef.current = false;
952
+ if (cancelledHoldPointerRef.current === event.pointerId) {
953
+ cancelledHoldPointerRef.current = null;
954
+ }
955
+ // Any new press ends a running hold, whichever pointer made it.
956
+ cancelStepperHold();
957
+ // Steppers render only under a non-coarse primary pointer, so a hold is a
958
+ // mouse or pen press. A finger keeps the single step of its click.
959
+ if (
960
+ event.button !== 0 ||
961
+ event.pointerType === 'touch' ||
962
+ !canStep ||
963
+ isDisabled ||
964
+ isReadOnly
965
+ ) {
966
+ return;
967
+ }
968
+ const first = getNextValue(direction);
969
+ if (first == null) {
970
+ return;
971
+ }
972
+ // The release ends the hold wherever the pointer is by then.
973
+ event.currentTarget.setPointerCapture?.(event.pointerId);
974
+ const grid = {direction, min, max, step, isIntegerOnly};
975
+ const hold: StepperHold = {
976
+ pointerId: event.pointerId,
977
+ timer: null,
978
+ held: null,
979
+ intervalMs: STEPPER_HOLD.firstIntervalMs,
980
+ isFrozen: false,
981
+ // A pen's hold starts no text selection or callout.
982
+ unlockSelection: locksSelectionFor(event.pointerType)
983
+ ? lockSelection([event.currentTarget], event.target)
984
+ : null,
985
+ };
986
+ const repeat = () => {
987
+ const next =
988
+ hold.held == null
989
+ ? first
990
+ : getSteppedValue({currentValue: hold.held, ...grid});
991
+ // At a bound the repeats stop, and the release still commits.
992
+ if (next == null || next === hold.held) {
993
+ hold.timer = null;
994
+ return;
995
+ }
996
+ hold.held = next;
997
+ setHoldDraft(next);
998
+ hold.timer = setTimeout(repeat, hold.intervalMs);
999
+ hold.intervalMs = Math.max(
1000
+ STEPPER_HOLD.minIntervalMs,
1001
+ hold.intervalMs * STEPPER_HOLD.acceleration,
1002
+ );
1003
+ };
1004
+ hold.timer = setTimeout(repeat, STEPPER_HOLD.delayMs);
1005
+ stepperHoldRef.current = hold;
1006
+ };
1007
+
1008
+ const handleStepperContextMenu = (event: MouseEvent<HTMLButtonElement>) => {
1009
+ const hold = stepperHoldRef.current;
1010
+ if (hold == null) {
1011
+ return;
1012
+ }
1013
+ // A pen's hold opens no callout menu. A mouse's context request, such as
1014
+ // a Control-click, ends its hold with no commit and opens the menu.
1015
+ if (hold.unlockSelection != null) {
1016
+ event.preventDefault();
1017
+ return;
1018
+ }
1019
+ cancelStepperHold();
1020
+ // A click that follows the menu steps nothing, as after any ended hold.
1021
+ cancelledHoldPointerRef.current = hold.pointerId;
1022
+ isClickAfterHoldRef.current = true;
1023
+ };
1024
+
1025
+ const endStepperHold = (end: 'release' | 'cancel', pointerId: number) => {
1026
+ const hold = stepperHoldRef.current;
1027
+ // Only the pointer that holds can end the hold.
1028
+ if (hold == null || hold.pointerId !== pointerId) {
1029
+ if (cancelledHoldPointerRef.current === pointerId) {
1030
+ cancelledHoldPointerRef.current = null;
1031
+ // Only arm the swallow here: a lost capture must not clear it.
1032
+ if (end === 'release') {
1033
+ isClickAfterHoldRef.current = true;
1034
+ }
1035
+ }
1036
+ return;
1037
+ }
1038
+ stepperHoldRef.current = null;
1039
+ if (hold.timer != null) {
1040
+ clearTimeout(hold.timer);
1041
+ }
1042
+ hold.unlockSelection?.();
1043
+ // A press released before its first repeat is a click, and its click
1044
+ // steps.
1045
+ if (hold.held == null) {
1046
+ return;
1047
+ }
1048
+ isClickAfterHoldRef.current = end === 'release';
1049
+ setHoldDraft(null);
1050
+ // A frozen hold commits nothing, so a typed draft under it stays.
1051
+ if (hold.isFrozen || isDisabled || isReadOnly) {
1052
+ return;
1053
+ }
1054
+ setPendingInput(null);
1055
+ if (hold.held !== value) {
1056
+ onChange(hold.held);
1057
+ }
1058
+ };
1059
+
853
1060
  // Handle keyboard events
854
1061
  const handleKeyDown = useCallback(
855
1062
  (e: KeyboardEvent<HTMLInputElement>) => {
@@ -927,8 +1134,15 @@ export function NumberInput({
927
1134
  [ref, wheelListenerRef],
928
1135
  );
929
1136
 
930
- const canIncrement = getNextValue(1) !== valueForStepping;
931
- const canDecrement = getNextValue(-1) !== valueForStepping;
1137
+ // While a hold runs, the steppers follow the value that it shows.
1138
+ const {canIncrement, canDecrement} = useMemo(() => {
1139
+ const base = holdDraft ?? valueForStepping;
1140
+ const grid = {currentValue: base, min, max, step, isIntegerOnly};
1141
+ return {
1142
+ canIncrement: getSteppedValue({...grid, direction: 1}) !== base,
1143
+ canDecrement: getSteppedValue({...grid, direction: -1}) !== base,
1144
+ };
1145
+ }, [holdDraft, isIntegerOnly, max, min, step, valueForStepping]);
932
1146
 
933
1147
  // Handle clear button click
934
1148
  const handleClear = useCallback(
@@ -1069,7 +1283,7 @@ export function NumberInput({
1069
1283
  )}
1070
1284
  {statusIcon}
1071
1285
  {hasNumberSteppers && (
1072
- <div {...stylex.props(styles.numberSteppers)}>
1286
+ <div ref={stepperColumnRef} {...stylex.props(styles.numberSteppers)}>
1073
1287
  <button
1074
1288
  type="button"
1075
1289
  tabIndex={-1}
@@ -1078,8 +1292,25 @@ export function NumberInput({
1078
1292
  isDisabled || isReadOnly || !canIncrement || undefined
1079
1293
  }
1080
1294
  aria-label={t('@astryx.numberInput.incrementLabel', {label})}
1081
- onPointerDown={event => event.preventDefault()}
1082
- onClick={() => {
1295
+ onPointerDown={event => {
1296
+ event.preventDefault();
1297
+ startStepperHold(1, canIncrement, event);
1298
+ }}
1299
+ onContextMenu={handleStepperContextMenu}
1300
+ onPointerUp={event => endStepperHold('release', event.pointerId)}
1301
+ onPointerCancel={event => endStepperHold('cancel', event.pointerId)}
1302
+ onLostPointerCapture={event =>
1303
+ endStepperHold('cancel', event.pointerId)
1304
+ }
1305
+ onClick={event => {
1306
+ const isAfterHold = isClickAfterHoldRef.current;
1307
+ isClickAfterHoldRef.current = false;
1308
+ // A pointer's click after a held press's release steps nothing.
1309
+ // A click with no pointer, such as a screen reader's, steps.
1310
+ if (isAfterHold && event.detail > 0) {
1311
+ inputRef.current?.focus();
1312
+ return;
1313
+ }
1083
1314
  // An aria-disabled stepper does nothing, as a natively disabled
1084
1315
  // one did (spec:AST-085 FR2).
1085
1316
  if (isDisabled || isReadOnly || !canIncrement) {
@@ -1109,8 +1340,25 @@ export function NumberInput({
1109
1340
  isDisabled || isReadOnly || !canDecrement || undefined
1110
1341
  }
1111
1342
  aria-label={t('@astryx.numberInput.decrementLabel', {label})}
1112
- onPointerDown={event => event.preventDefault()}
1113
- onClick={() => {
1343
+ onPointerDown={event => {
1344
+ event.preventDefault();
1345
+ startStepperHold(-1, canDecrement, event);
1346
+ }}
1347
+ onContextMenu={handleStepperContextMenu}
1348
+ onPointerUp={event => endStepperHold('release', event.pointerId)}
1349
+ onPointerCancel={event => endStepperHold('cancel', event.pointerId)}
1350
+ onLostPointerCapture={event =>
1351
+ endStepperHold('cancel', event.pointerId)
1352
+ }
1353
+ onClick={event => {
1354
+ const isAfterHold = isClickAfterHoldRef.current;
1355
+ isClickAfterHoldRef.current = false;
1356
+ // A pointer's click after a held press's release steps nothing.
1357
+ // A click with no pointer, such as a screen reader's, steps.
1358
+ if (isAfterHold && event.detail > 0) {
1359
+ inputRef.current?.focus();
1360
+ return;
1361
+ }
1114
1362
  // An aria-disabled stepper does nothing, as a natively disabled
1115
1363
  // one did (spec:AST-085 FR2).
1116
1364
  if (isDisabled || isReadOnly || !canDecrement) {
@@ -298,6 +298,7 @@ export interface UsePopoverReturn {
298
298
  /**
299
299
  * The CSS anchor name to use for positioning.
300
300
  * Use when you need to set anchorName manually (e.g., display:contents wrapper).
301
+ * Set it only while the layer is shown or its exit animation runs.
301
302
  */
302
303
  anchorId: string;
303
304
 
@@ -185,7 +185,8 @@ export interface TooltipReturn {
185
185
 
186
186
  /**
187
187
  * Ref for the positioning anchor element.
188
- * Injects anchorName style for CSS anchor positioning.
188
+ * While the layer is shown, and until its exit animation ends, the trigger
189
+ * carries the layer's CSS anchor name.
189
190
  */
190
191
  positionRef: RefCallback<HTMLElement>;
191
192
 
@@ -199,6 +200,7 @@ export interface TooltipReturn {
199
200
  /**
200
201
  * The CSS anchor name to use for positioning.
201
202
  * Use this when you need to set anchorName manually (e.g., display:contents wrapper).
203
+ * Set it only while the layer is shown or its exit animation runs.
202
204
  */
203
205
  anchorId: string;
204
206