@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.
- package/dist/HoverCard/useHoverCard.d.ts +3 -1
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/Layer/useLayer.d.ts +3 -1
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +64 -4
- package/dist/NumberInput/NumberInput.d.ts +4 -2
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +244 -11
- package/dist/Popover/usePopover.d.ts +1 -0
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.d.ts +3 -1
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/HoverCard/useHoverCard.tsx +3 -1
- package/src/Layer/useLayer.tsx +77 -4
- package/src/NumberInput/NumberInput.doc.mjs +1 -1
- package/src/NumberInput/NumberInput.tsx +259 -11
- package/src/Popover/usePopover.tsx +1 -0
- package/src/Tooltip/useTooltip.tsx +3 -1
|
@@ -111,7 +111,8 @@ export interface HoverCardReturn {
|
|
|
111
111
|
ref: RefCallback<HTMLElement>;
|
|
112
112
|
/**
|
|
113
113
|
* Ref for the positioning anchor element.
|
|
114
|
-
*
|
|
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
|
|
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"}
|
package/dist/Layer/useLayer.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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":"AAIA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,EAQZ,KAAK,SAAS,EACd,KAAK,WAAW,EACjB,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,kBAAkB,CAAC;AAuInD;;GAEG;AACH,UAAU,kBAAkB;IAC1B,aAAa,EAAE,KAAK,CAAC,mBAAmB,CAAC,WAAW,CAAC,CAAC;IACtD;;;OAGG;IACH,cAAc,EAAE,KAAK,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC;CACtD;AAED;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,KAAK,CAAC;AAEjE;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;AAExD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;;;;;;;OAcG;IACH,WAAW,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAClC;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,EAAE,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACpB;;;;OAIG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC;IACpD;;OAEG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC;CACrD;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV;;OAEG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;CAC7B;AAED;;GAEG;AACH,UAAU,gBAAgB;IACxB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,gBAAgB;IAC3D,IAAI,EAAE,SAAS,CAAC;IAChB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAkB,SAAQ,gBAAgB;IACzD,IAAI,EAAE,OAAO,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC
|
|
1
|
+
{"version":3,"file":"useLayer.d.ts","sourceRoot":"","sources":["../../src/Layer/useLayer.tsx"],"names":[],"mappings":"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"}
|
package/dist/Layer/useLayer.js
CHANGED
|
@@ -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
|
-
|
|
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":"AAIA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,
|
|
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
|
-
|
|
518
|
-
|
|
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 =>
|
|
653
|
-
|
|
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 =>
|
|
678
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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.
|
|
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.
|
|
874
|
-
"@astryxdesign/cli": "0.7.9-canary.
|
|
875
|
-
"@astryxdesign/interaction-harness": "0.7.9-canary.
|
|
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
|
-
*
|
|
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
|
|
package/src/Layer/useLayer.tsx
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
931
|
-
const canDecrement =
|
|
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 =>
|
|
1082
|
-
|
|
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 =>
|
|
1113
|
-
|
|
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
|
-
*
|
|
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
|
|