@astryxdesign/core 0.7.9-canary.76298e4 → 0.7.9-canary.7cda408
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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file NumberInput.tsx
|
|
3
|
-
* @input Uses React, useId, useState, useMemo, useCallback,
|
|
4
|
-
* @output Exports NumberInput component, NumberInputProps; optional steppers are hidden on coarse pointers
|
|
3
|
+
* @input Uses React, useId, useState, useMemo, useCallback, Field, Icon, InputGroupContext
|
|
4
|
+
* @output Exports NumberInput component, NumberInputProps; optional steppers are hidden on coarse pointers
|
|
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,8 +188,6 @@ 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.
|
|
193
191
|
* Direct text entry and keyboard stepping remain available.
|
|
194
192
|
* @default false
|
|
195
193
|
*/
|
|
@@ -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,
|
|
8
|
-
* @output Exports NumberInput component, NumberInputProps; optional steppers are hidden on coarse pointers
|
|
7
|
+
* @input Uses React, useId, useState, useMemo, useCallback, Field, Icon, InputGroupContext
|
|
8
|
+
* @output Exports NumberInput component, NumberInputProps; optional steppers are hidden on coarse pointers
|
|
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,
|
|
20
|
+
import { use, useId, useState, useMemo, useCallback, 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,7 +31,6 @@ 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";
|
|
35
34
|
import { inputDisabledFocusStyles } from "../Field/inputDisabledFocus.js";
|
|
36
35
|
import { useSize } from "../SizeContext/SizeContext.js";
|
|
37
36
|
import { useInputContainer } from "../hooks/useInputContainer.js";
|
|
@@ -144,24 +143,6 @@ registerEnglishFallback(english);
|
|
|
144
143
|
* With `hasClear`, onChange also receives `null` when the user clears the input.
|
|
145
144
|
*/
|
|
146
145
|
|
|
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
|
-
|
|
165
146
|
function getDecimalPlaces(value) {
|
|
166
147
|
const [coefficient, exponentText] = String(value).toLowerCase().split('e');
|
|
167
148
|
const fractionLength = coefficient.split('.')[1]?.length ?? 0;
|
|
@@ -305,13 +286,6 @@ export function NumberInput({
|
|
|
305
286
|
|
|
306
287
|
// Pending input while user is typing (null = show formatted value)
|
|
307
288
|
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
|
-
}
|
|
315
289
|
const [isFocused, setIsFocused] = useState(false);
|
|
316
290
|
|
|
317
291
|
// Disabled-reason tooltip. The tooltip listeners attach to the input container (which already exists) and
|
|
@@ -366,9 +340,6 @@ export function NumberInput({
|
|
|
366
340
|
// Preserve pending text while editing. Otherwise show the formatted value
|
|
367
341
|
// at rest and the raw numeric value while focused so it remains editable.
|
|
368
342
|
const displayValue = useMemo(() => {
|
|
369
|
-
if (holdDraft !== null) {
|
|
370
|
-
return formatEditableNumber(holdDraft, locale);
|
|
371
|
-
}
|
|
372
343
|
if (pendingInput !== null) {
|
|
373
344
|
return pendingInput;
|
|
374
345
|
}
|
|
@@ -376,19 +347,15 @@ export function NumberInput({
|
|
|
376
347
|
return '';
|
|
377
348
|
}
|
|
378
349
|
return isFocused ? formatEditableNumber(value, locale) : formattedValue;
|
|
379
|
-
}, [formattedValue,
|
|
350
|
+
}, [formattedValue, isFocused, locale, pendingInput, value]);
|
|
380
351
|
|
|
381
352
|
// Check if current pending input is valid (for styling purposes)
|
|
382
353
|
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
|
-
}
|
|
387
354
|
if (pendingInput === null || !pendingInput.trim()) {
|
|
388
355
|
return true;
|
|
389
356
|
}
|
|
390
357
|
return parseInput(pendingInput) !== null;
|
|
391
|
-
}, [
|
|
358
|
+
}, [pendingInput, parseInput]);
|
|
392
359
|
|
|
393
360
|
// Keep the whole text edit as a draft until an explicit commit boundary.
|
|
394
361
|
const handleInputChange = useCallback(e => {
|
|
@@ -490,152 +457,6 @@ export function NumberInput({
|
|
|
490
457
|
}
|
|
491
458
|
}, [getNextValue, isDisabled, isReadOnly, onChange, value]);
|
|
492
459
|
|
|
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
|
-
|
|
639
460
|
// Handle keyboard events
|
|
640
461
|
const handleKeyDown = useCallback(e => {
|
|
641
462
|
if (isDisabled) {
|
|
@@ -693,31 +514,8 @@ export function NumberInput({
|
|
|
693
514
|
return () => input.removeEventListener('wheel', handleWheel);
|
|
694
515
|
}, [isDisabled, isReadOnly, isWheelEnabled, stepValue]);
|
|
695
516
|
const mergedInputRef = useMemo(() => mergeRefs(ref, inputRef, wheelListenerRef), [ref, wheelListenerRef]);
|
|
696
|
-
|
|
697
|
-
|
|
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]);
|
|
517
|
+
const canIncrement = getNextValue(1) !== valueForStepping;
|
|
518
|
+
const canDecrement = getNextValue(-1) !== valueForStepping;
|
|
721
519
|
|
|
722
520
|
// Handle clear button click
|
|
723
521
|
const handleClear = useCallback(e => {
|
|
@@ -840,7 +638,6 @@ export function NumberInput({
|
|
|
840
638
|
}),
|
|
841
639
|
onClick: handleClear
|
|
842
640
|
}), statusIcon, hasNumberSteppers && /*#__PURE__*/_jsxs("div", {
|
|
843
|
-
ref: stepperColumnRef,
|
|
844
641
|
...{
|
|
845
642
|
className: "xkh2ocl x78zum5 xhj50sb xdt5ytf x2lah0s x12xnipv x5j3pgv x13i4uq0 xgbv0en x1t7ytsu x14ydlgx xb3r6kr x1g2gkjz xoao14b"
|
|
846
643
|
},
|
|
@@ -852,23 +649,8 @@ export function NumberInput({
|
|
|
852
649
|
"aria-label": t('@astryx.numberInput.incrementLabel', {
|
|
853
650
|
label
|
|
854
651
|
}),
|
|
855
|
-
onPointerDown: event =>
|
|
856
|
-
|
|
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
|
-
}
|
|
652
|
+
onPointerDown: event => event.preventDefault(),
|
|
653
|
+
onClick: () => {
|
|
872
654
|
// An aria-disabled stepper does nothing, as a natively disabled
|
|
873
655
|
// one did (spec:AST-085 FR2).
|
|
874
656
|
if (isDisabled || isReadOnly || !canIncrement) {
|
|
@@ -892,23 +674,8 @@ export function NumberInput({
|
|
|
892
674
|
"aria-label": t('@astryx.numberInput.decrementLabel', {
|
|
893
675
|
label
|
|
894
676
|
}),
|
|
895
|
-
onPointerDown: event =>
|
|
896
|
-
|
|
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
|
-
}
|
|
677
|
+
onPointerDown: event => event.preventDefault(),
|
|
678
|
+
onClick: () => {
|
|
912
679
|
// An aria-disabled stepper does nothing, as a natively disabled
|
|
913
680
|
// one did (spec:AST-085 FR2).
|
|
914
681
|
if (isDisabled || isReadOnly || !canDecrement) {
|
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.7cda408",
|
|
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.7cda408",
|
|
874
|
+
"@astryxdesign/cli": "0.7.9-canary.7cda408",
|
|
875
|
+
"@astryxdesign/interaction-harness": "0.7.9-canary.7cda408",
|
|
876
876
|
"@babel/cli": "^7.29.7",
|
|
877
877
|
"@babel/core": "^7.29.7",
|
|
878
878
|
"@babel/preset-react": "^7.29.7",
|
|
@@ -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.',
|
|
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,
|
|
8
|
-
* @output Exports NumberInput component, NumberInputProps; optional steppers are hidden on coarse pointers
|
|
7
|
+
* @input Uses React, useId, useState, useMemo, useCallback, Field, Icon, InputGroupContext
|
|
8
|
+
* @output Exports NumberInput component, NumberInputProps; optional steppers are hidden on coarse pointers
|
|
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,12 +24,9 @@ import {
|
|
|
24
24
|
useState,
|
|
25
25
|
useMemo,
|
|
26
26
|
useCallback,
|
|
27
|
-
useEffect,
|
|
28
27
|
useRef,
|
|
29
28
|
type FocusEvent,
|
|
30
29
|
type KeyboardEvent,
|
|
31
|
-
type MouseEvent,
|
|
32
|
-
type PointerEvent,
|
|
33
30
|
type ReactNode,
|
|
34
31
|
} from 'react';
|
|
35
32
|
import {flushSync} from 'react-dom';
|
|
@@ -59,7 +56,6 @@ import {joinAriaIDs} from '../utils/inputAria';
|
|
|
59
56
|
import {withholdCallerHandlers} from '../utils/disabledInteraction';
|
|
60
57
|
import {interactionOverlayStyles} from '../utils/interactionOverlay.stylex';
|
|
61
58
|
import {usePressFeedback} from '../hooks/usePressFeedback';
|
|
62
|
-
import {lockSelection, locksSelectionFor} from '../utils/selectionLock';
|
|
63
59
|
import {inputDisabledFocusStyles} from '../Field/inputDisabledFocus';
|
|
64
60
|
import {useSize} from '../SizeContext/SizeContext';
|
|
65
61
|
import {useInputContainer} from '../hooks/useInputContainer';
|
|
@@ -407,8 +403,6 @@ interface NumberInputPropsBase extends Omit<
|
|
|
407
403
|
/**
|
|
408
404
|
* Whether to show increment and decrement buttons at the end of the input.
|
|
409
405
|
* 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.
|
|
412
406
|
* Direct text entry and keyboard stepping remain available.
|
|
413
407
|
* @default false
|
|
414
408
|
*/
|
|
@@ -466,32 +460,6 @@ export type NumberInputProps =
|
|
|
466
460
|
|
|
467
461
|
type StepDirection = -1 | 1;
|
|
468
462
|
|
|
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
|
-
|
|
495
463
|
function getDecimalPlaces(value: number): number {
|
|
496
464
|
const [coefficient, exponentText] = String(value).toLowerCase().split('e');
|
|
497
465
|
const fractionLength = coefficient.split('.')[1]?.length ?? 0;
|
|
@@ -657,13 +625,6 @@ export function NumberInput({
|
|
|
657
625
|
|
|
658
626
|
// Pending input while user is typing (null = show formatted value)
|
|
659
627
|
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
|
-
}
|
|
667
628
|
const [isFocused, setIsFocused] = useState(false);
|
|
668
629
|
|
|
669
630
|
// Disabled-reason tooltip. The tooltip listeners attach to the input container (which already exists) and
|
|
@@ -728,9 +689,6 @@ export function NumberInput({
|
|
|
728
689
|
// Preserve pending text while editing. Otherwise show the formatted value
|
|
729
690
|
// at rest and the raw numeric value while focused so it remains editable.
|
|
730
691
|
const displayValue = useMemo(() => {
|
|
731
|
-
if (holdDraft !== null) {
|
|
732
|
-
return formatEditableNumber(holdDraft, locale);
|
|
733
|
-
}
|
|
734
692
|
if (pendingInput !== null) {
|
|
735
693
|
return pendingInput;
|
|
736
694
|
}
|
|
@@ -738,19 +696,15 @@ export function NumberInput({
|
|
|
738
696
|
return '';
|
|
739
697
|
}
|
|
740
698
|
return isFocused ? formatEditableNumber(value, locale) : formattedValue;
|
|
741
|
-
}, [formattedValue,
|
|
699
|
+
}, [formattedValue, isFocused, locale, pendingInput, value]);
|
|
742
700
|
|
|
743
701
|
// Check if current pending input is valid (for styling purposes)
|
|
744
702
|
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
|
-
}
|
|
749
703
|
if (pendingInput === null || !pendingInput.trim()) {
|
|
750
704
|
return true;
|
|
751
705
|
}
|
|
752
706
|
return parseInput(pendingInput) !== null;
|
|
753
|
-
}, [
|
|
707
|
+
}, [pendingInput, parseInput]);
|
|
754
708
|
|
|
755
709
|
// Keep the whole text edit as a draft until an explicit commit boundary.
|
|
756
710
|
const handleInputChange = useCallback(
|
|
@@ -896,167 +850,6 @@ export function NumberInput({
|
|
|
896
850
|
[getNextValue, isDisabled, isReadOnly, onChange, value],
|
|
897
851
|
);
|
|
898
852
|
|
|
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
|
-
|
|
1060
853
|
// Handle keyboard events
|
|
1061
854
|
const handleKeyDown = useCallback(
|
|
1062
855
|
(e: KeyboardEvent<HTMLInputElement>) => {
|
|
@@ -1134,15 +927,8 @@ export function NumberInput({
|
|
|
1134
927
|
[ref, wheelListenerRef],
|
|
1135
928
|
);
|
|
1136
929
|
|
|
1137
|
-
|
|
1138
|
-
const
|
|
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]);
|
|
930
|
+
const canIncrement = getNextValue(1) !== valueForStepping;
|
|
931
|
+
const canDecrement = getNextValue(-1) !== valueForStepping;
|
|
1146
932
|
|
|
1147
933
|
// Handle clear button click
|
|
1148
934
|
const handleClear = useCallback(
|
|
@@ -1283,7 +1069,7 @@ export function NumberInput({
|
|
|
1283
1069
|
)}
|
|
1284
1070
|
{statusIcon}
|
|
1285
1071
|
{hasNumberSteppers && (
|
|
1286
|
-
<div
|
|
1072
|
+
<div {...stylex.props(styles.numberSteppers)}>
|
|
1287
1073
|
<button
|
|
1288
1074
|
type="button"
|
|
1289
1075
|
tabIndex={-1}
|
|
@@ -1292,25 +1078,8 @@ export function NumberInput({
|
|
|
1292
1078
|
isDisabled || isReadOnly || !canIncrement || undefined
|
|
1293
1079
|
}
|
|
1294
1080
|
aria-label={t('@astryx.numberInput.incrementLabel', {label})}
|
|
1295
|
-
onPointerDown={event =>
|
|
1296
|
-
|
|
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
|
-
}
|
|
1081
|
+
onPointerDown={event => event.preventDefault()}
|
|
1082
|
+
onClick={() => {
|
|
1314
1083
|
// An aria-disabled stepper does nothing, as a natively disabled
|
|
1315
1084
|
// one did (spec:AST-085 FR2).
|
|
1316
1085
|
if (isDisabled || isReadOnly || !canIncrement) {
|
|
@@ -1340,25 +1109,8 @@ export function NumberInput({
|
|
|
1340
1109
|
isDisabled || isReadOnly || !canDecrement || undefined
|
|
1341
1110
|
}
|
|
1342
1111
|
aria-label={t('@astryx.numberInput.decrementLabel', {label})}
|
|
1343
|
-
onPointerDown={event =>
|
|
1344
|
-
|
|
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
|
-
}
|
|
1112
|
+
onPointerDown={event => event.preventDefault()}
|
|
1113
|
+
onClick={() => {
|
|
1362
1114
|
// An aria-disabled stepper does nothing, as a natively disabled
|
|
1363
1115
|
// one did (spec:AST-085 FR2).
|
|
1364
1116
|
if (isDisabled || isReadOnly || !canDecrement) {
|