@astryxdesign/core 0.7.6-canary.c829c09 → 0.7.6-canary.c8b1b93
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/Resizable/useResizable.d.ts.map +1 -1
- package/dist/Resizable/useResizable.js +53 -10
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +8 -0
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +51 -47
- package/dist/Tokenizer/TokenizerBottomSheet.d.ts.map +1 -1
- package/dist/Tokenizer/TokenizerBottomSheet.js +8 -14
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +10 -3
- package/package.json +4 -4
- package/src/Resizable/Resizable.spec.md +11 -11
- package/src/Resizable/useResizable.ts +50 -7
- package/src/Slider/Slider.spec.md +18 -18
- package/src/Slider/Slider.tsx +8 -0
- package/src/Tokenizer/Tokenizer.tsx +54 -48
- package/src/Tokenizer/TokenizerBottomSheet.tsx +8 -14
- package/src/Typeahead/BaseTypeahead.tsx +18 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useResizable.d.ts","sourceRoot":"","sources":["../../src/Resizable/useResizable.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"useResizable.d.ts","sourceRoot":"","sources":["../../src/Resizable/useResizable.ts"],"names":[],"mappings":"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"}
|
|
@@ -487,10 +487,14 @@ function useSingleResizable(config) {
|
|
|
487
487
|
// otherwise move the bound under the pointer; the new basis applies once the
|
|
488
488
|
// gesture ends.
|
|
489
489
|
const gestureBasisRef = useRef(null);
|
|
490
|
-
// Only forces the render that releases the frozen basis
|
|
490
|
+
// Only forces the render that ends a gesture: it releases the frozen basis,
|
|
491
|
+
// and its commit writes the size the gesture settled on.
|
|
491
492
|
const [gestureTick, setGestureTick] = useState(0);
|
|
492
493
|
void gestureTick;
|
|
493
494
|
const basis = gestureBasisRef.current ?? liveBasis;
|
|
495
|
+
// The frozen basis also marks a gesture in flight, from `_onResizeStart`
|
|
496
|
+
// until `_onResizeEnd` or `_onResizeCancel`.
|
|
497
|
+
const isGestureActive = gestureBasisRef.current != null;
|
|
494
498
|
const didWarnMinRef = useRef(false);
|
|
495
499
|
const didWarnMaxRef = useRef(false);
|
|
496
500
|
const didWarnDefaultRef = useRef(false);
|
|
@@ -608,18 +612,55 @@ function useSingleResizable(config) {
|
|
|
608
612
|
devWarn('useResizable', `the resolved minimum (${resolvedMin}px) is above the resolved ` + `maximum (${resolvedMax}px). The maximum wins, as it always has.`);
|
|
609
613
|
}
|
|
610
614
|
}, [resolvedMin, resolvedMax]);
|
|
615
|
+
|
|
616
|
+
// The size a gesture in flight has reached, for a region that unmounts
|
|
617
|
+
// before the gesture ends.
|
|
618
|
+
const pendingWriteRef = useRef(null);
|
|
611
619
|
useEffect(() => {
|
|
620
|
+
// Synchronizes the `autoSaveId` entry in localStorage with the size.
|
|
612
621
|
// Nothing is written while the basis is still the temporary stand-in. The
|
|
613
622
|
// size on such a render is a placeholder, and persisting it overwrites the
|
|
614
623
|
// real saved size with a value derived from a basis that was never real
|
|
615
624
|
// (AST-010 Platform support, FR9).
|
|
616
|
-
if (autoSaveId
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
625
|
+
if (!autoSaveId || !isBasisMeasured) {
|
|
626
|
+
pendingWriteRef.current = null;
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
// Nor while a gesture is in flight. A drag commits a new size on every
|
|
630
|
+
// pointer move, and each write reaches every other open document of the
|
|
631
|
+
// origin as a `storage` event.
|
|
632
|
+
if (isGestureActive) {
|
|
633
|
+
pendingWriteRef.current = {
|
|
634
|
+
key: autoSaveId,
|
|
635
|
+
state: {
|
|
636
|
+
size,
|
|
637
|
+
isCollapsed
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
return;
|
|
621
641
|
}
|
|
622
|
-
|
|
642
|
+
// A gesture's one write happens here, in the commit that applies its
|
|
643
|
+
// release, not in the handler that ended it: this render is the first to
|
|
644
|
+
// know the final size. A basis that changed mid-gesture applies only at
|
|
645
|
+
// the release and can re-clamp the size (AST-010 FR7), and a move the
|
|
646
|
+
// release arrived before renders together with it. The handler sees
|
|
647
|
+
// neither.
|
|
648
|
+
pendingWriteRef.current = null;
|
|
649
|
+
persistState(autoSaveId, {
|
|
650
|
+
size,
|
|
651
|
+
isCollapsed
|
|
652
|
+
});
|
|
653
|
+
}, [size, isCollapsed, autoSaveId, isBasisMeasured, isGestureActive]);
|
|
654
|
+
|
|
655
|
+
// A region that unmounts mid-gesture never commits a release, so the size
|
|
656
|
+
// the gesture reached is written on the way out.
|
|
657
|
+
useEffect(() => () => {
|
|
658
|
+
const pending = pendingWriteRef.current;
|
|
659
|
+
pendingWriteRef.current = null;
|
|
660
|
+
if (pending != null) {
|
|
661
|
+
persistState(pending.key, pending.state);
|
|
662
|
+
}
|
|
663
|
+
}, []);
|
|
623
664
|
const collapse = useCallback(() => {
|
|
624
665
|
// The already-collapsed guard keeps a repeated call from re-notifying a
|
|
625
666
|
// transition that already happened.
|
|
@@ -683,7 +724,7 @@ function useSingleResizable(config) {
|
|
|
683
724
|
}, [collapsible, collapsedSize, setCollapsed, resolvedMin, resolvedMax, snaps, onSizeChange, onCollapseChange]);
|
|
684
725
|
|
|
685
726
|
// Releasing the frozen basis needs a render to apply whatever the basis
|
|
686
|
-
// became during the gesture.
|
|
727
|
+
// became during the gesture; that render's commit writes the final size.
|
|
687
728
|
const releaseGestureBasis = useCallback(() => {
|
|
688
729
|
if (gestureBasisRef.current != null) {
|
|
689
730
|
gestureBasisRef.current = null;
|
|
@@ -691,7 +732,8 @@ function useSingleResizable(config) {
|
|
|
691
732
|
}
|
|
692
733
|
}, []);
|
|
693
734
|
const onResizeEnd = useCallback(() => {
|
|
694
|
-
// Sizes are already committed during move;
|
|
735
|
+
// Sizes are already committed during move; the basis is held, and the
|
|
736
|
+
// commit that releases it writes the final size to storage.
|
|
695
737
|
releaseGestureBasis();
|
|
696
738
|
}, [releaseGestureBasis]);
|
|
697
739
|
|
|
@@ -699,7 +741,8 @@ function useSingleResizable(config) {
|
|
|
699
741
|
// capture, a handle unmounted mid-drag — is still over. It does not signal a
|
|
700
742
|
// resize end (that is #5297's contract, and the sizes were committed during
|
|
701
743
|
// move anyway), but the basis it froze has to come back, or every later
|
|
702
|
-
// percentage bound stays pinned to the container size at grab time
|
|
744
|
+
// percentage bound stays pinned to the container size at grab time, and the
|
|
745
|
+
// size it reached has to reach storage.
|
|
703
746
|
const onResizeCancel = releaseGestureBasis;
|
|
704
747
|
|
|
705
748
|
// The maximum a handle REPORTS (End uses only a configured maximum). A
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Slider.d.ts","sourceRoot":"","sources":["../../src/Slider/Slider.tsx"],"names":[],"mappings":"AA2CA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAShD,OAAO,EACL,KAAK,mBAAmB,EAEzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,gBAAgB,CAAC;AAU9C,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAC3C,mBAAmB,CAAC,cAAc,CAAC,EACnC,UAAU,CACX;IACC,mDAAmD;IACnD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,kDAAkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oDAAoD;IACpD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,oDAAoD;IACpD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,uCAAuC;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gCAAgC;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,WAAW,CAAC,EAAE,YAAY,GAAG,UAAU,CAAC;IACxC,uEAAuE;IACvE,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACxC,6DAA6D;IAC7D,YAAY,CAAC,EAAE,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;IAC3C,8DAA8D;IAC9D,KAAK,CAAC,EAAE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAC,EAAE,CAAC;IAC1C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACxD,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,iEAAiE;IACjE,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,8CAA8C;IAC9C,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC;IAC7C,iEAAiE;IACjE,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC;IAChD,8CAA8C;IAC9C,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,MAAM,WAAW,GAAG,iBAAiB,GAAG,gBAAgB,CAAC;AAmT/D;;;;;;;;GAQG;AACH,wBAAgB,MAAM,CAAC,EAAC,GAAG,EAAE,GAAG,KAAK,EAAC,EAAE,WAAW,+
|
|
1
|
+
{"version":3,"file":"Slider.d.ts","sourceRoot":"","sources":["../../src/Slider/Slider.tsx"],"names":[],"mappings":"AA2CA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAShD,OAAO,EACL,KAAK,mBAAmB,EAEzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,gBAAgB,CAAC;AAU9C,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAC3C,mBAAmB,CAAC,cAAc,CAAC,EACnC,UAAU,CACX;IACC,mDAAmD;IACnD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,kDAAkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oDAAoD;IACpD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,oDAAoD;IACpD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,uCAAuC;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gCAAgC;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,WAAW,CAAC,EAAE,YAAY,GAAG,UAAU,CAAC;IACxC,uEAAuE;IACvE,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACxC,6DAA6D;IAC7D,YAAY,CAAC,EAAE,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;IAC3C,8DAA8D;IAC9D,KAAK,CAAC,EAAE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAC,EAAE,CAAC;IAC1C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACxD,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,iEAAiE;IACjE,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,8CAA8C;IAC9C,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC;IAC7C,iEAAiE;IACjE,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC;IAChD,8CAA8C;IAC9C,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,MAAM,WAAW,GAAG,iBAAiB,GAAG,gBAAgB,CAAC;AAmT/D;;;;;;;;GAQG;AACH,wBAAgB,MAAM,CAAC,EAAC,GAAG,EAAE,GAAG,KAAK,EAAC,EAAE,WAAW,+BAi6BlD;yBAj6Be,MAAM"}
|
package/dist/Slider/Slider.js
CHANGED
|
@@ -529,6 +529,14 @@ export function Slider({
|
|
|
529
529
|
const isCoincident = isRange && values[0] === values[1];
|
|
530
530
|
let thumbIndex = pressedThumbIndex >= 0 ? pressedThumbIndex : getClosestThumb(newVal);
|
|
531
531
|
const parks = isCoincident && (pressedThumbIndex >= 0 || newVal === values[0]);
|
|
532
|
+
// Until the first movement, a parked press is held by the upper thumb,
|
|
533
|
+
// the one drawn over the lower one, wherever it landed: on the thumbs
|
|
534
|
+
// (which the upper thumb covers) or on their shared value elsewhere,
|
|
535
|
+
// such as its mark label. So its pressed paint shows, and a press that
|
|
536
|
+
// never moves leaves focus on the same thumb.
|
|
537
|
+
if (parks && pressedThumbIndex < 0) {
|
|
538
|
+
thumbIndex = 1;
|
|
539
|
+
}
|
|
532
540
|
// The first movement is measured from the pointer itself, not from a
|
|
533
541
|
// mark it pressed (a wide label sits away from its mark's value).
|
|
534
542
|
coincidentDragRef.current = parks ? getValueFromPosition(e.clientX, e.clientY) : null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tokenizer.d.ts","sourceRoot":"","sources":["../../src/Tokenizer/Tokenizer.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,EAYZ,KAAK,SAAS,EAEf,MAAM,OAAO,CAAC;AASf,OAAO,EACL,KAAK,mBAAmB,EAEzB,MAAM,qBAAqB,CAAC;AAQ7B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,gBAAgB,CAAC;AAG9C,OAAO,EAGL,KAAK,WAAW,EAKhB,KAAK,kBAAkB,EACxB,MAAM,UAAU,CAAC;AAIlB,OAAO,EAAiB,KAAK,QAAQ,EAAC,MAAM,SAAS,CAAC;AAWtD,OAAO,KAAK,EAAC,cAAc,EAAE,YAAY,EAAC,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"Tokenizer.d.ts","sourceRoot":"","sources":["../../src/Tokenizer/Tokenizer.tsx"],"names":[],"mappings":"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"}
|
|
@@ -50,6 +50,7 @@ import "../theme/tokens.stylex.js";
|
|
|
50
50
|
import { colorVars, spacingVars, sizeVars, typeScaleVars } from "../theme/tokens.stylex.js";
|
|
51
51
|
import { composeEventHandlers, mergeProps } from "../utils/index.js";
|
|
52
52
|
import { joinAriaIDs } from "../utils/inputAria.js";
|
|
53
|
+
import { isFocusDetached } from "../utils/focusReturn.js";
|
|
53
54
|
import { themeProps } from "../utils/themeProps.js";
|
|
54
55
|
import { useTranslator } from "../i18n/index.js";
|
|
55
56
|
import { useResolvedRequired } from "../hooks/useResolvedRequired.js";
|
|
@@ -506,14 +507,6 @@ export function Tokenizer({
|
|
|
506
507
|
sheetQueryRef.current = query;
|
|
507
508
|
onChangeQuery?.(query);
|
|
508
509
|
}, [onChangeQuery]);
|
|
509
|
-
// Whether the field's control — the inline input or the trigger — holds
|
|
510
|
-
// focus. Kept in a ref by the focus and blur handlers of both, because a
|
|
511
|
-
// control unmounted while focused fires no blur: when a presentation swap
|
|
512
|
-
// removes the focused control (the latch releasing into the popover, or
|
|
513
|
-
// `adaptive` changing while the sheet is closed), the new presentation's
|
|
514
|
-
// control takes the focus over once it has mounted, so focus never lands on
|
|
515
|
-
// the page body (`component:Tokenizer` FR16, `spec:AST-085` FR4).
|
|
516
|
-
const isControlFocusedRef = useRef(false);
|
|
517
510
|
// Set around a focus move the field makes itself — a presentation swap
|
|
518
511
|
// handing focus from one control to the other — so the receiving control
|
|
519
512
|
// does not report it as focus entering from outside.
|
|
@@ -524,35 +517,33 @@ export function Tokenizer({
|
|
|
524
517
|
// entry, and the one focus the trigger then gets — BottomSheet's return
|
|
525
518
|
// focus — is the entry to report.
|
|
526
519
|
const hasReportedFocusRef = useRef(false);
|
|
527
|
-
//
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
if (active != null && (active === inputRef.current || active === triggerRef.current)) {
|
|
540
|
-
isControlFocusedRef.current = true;
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
useEffect(() => {
|
|
545
|
-
if (!isControlFocusedRef.current) {
|
|
520
|
+
// When a presentation swap removes the focused control (the latch
|
|
521
|
+
// releasing into the popover, or `adaptive` changing while the sheet is
|
|
522
|
+
// closed), the new presentation's control takes the focus over, so focus
|
|
523
|
+
// never lands on the page body (`component:Tokenizer` FR16, `spec:AST-085`
|
|
524
|
+
// FR4). Both controls' callback refs own the hand-off
|
|
525
|
+
// (`architecture:react-component-runtime` INV6): the detaching control
|
|
526
|
+
// notes that it held focus — it is still in the document then, so this also
|
|
527
|
+
// covers a control focused before hydration (`hasAutoFocus`), which no focus
|
|
528
|
+
// event reported — and the attaching control takes it.
|
|
529
|
+
const isFocusPendingHandOffRef = useRef(false);
|
|
530
|
+
const handOffFocusRef = useCallback(node => {
|
|
531
|
+
if (node == null) {
|
|
546
532
|
return;
|
|
547
533
|
}
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
534
|
+
const takesFocus = isFocusPendingHandOffRef.current;
|
|
535
|
+
isFocusPendingHandOffRef.current = false;
|
|
536
|
+
if (takesFocus && isFocusDetached(node.ownerDocument)) {
|
|
537
|
+
isHandingOffFocusRef.current = true;
|
|
538
|
+
node.focus();
|
|
539
|
+
isHandingOffFocusRef.current = false;
|
|
551
540
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
541
|
+
return () => {
|
|
542
|
+
if (node.ownerDocument.activeElement === node) {
|
|
543
|
+
isFocusPendingHandOffRef.current = true;
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
}, []);
|
|
556
547
|
// The trigger's accessible description names the selected tokens: the
|
|
557
548
|
// button's children are tokens whose own content assistive technology
|
|
558
549
|
// leaves out of a button's name (`spec:AST-020` FR1).
|
|
@@ -603,10 +594,30 @@ export function Tokenizer({
|
|
|
603
594
|
focusTrigger: 'always',
|
|
604
595
|
isEnabled: showsDisabledMessage
|
|
605
596
|
});
|
|
597
|
+
// The inline field's wrapper holds every control a swap removes with the
|
|
598
|
+
// input — a token's Remove button, the inline Clear all — so it notes focus
|
|
599
|
+
// anywhere inside it for the hand-off above. It also anchors the
|
|
600
|
+
// disabled-message tooltip, whose handlers are gated internally by
|
|
601
|
+
// isEnabled, so attaching unconditionally is safe.
|
|
602
|
+
const tooltipAnchorRef = disabledMessageTooltip.ref;
|
|
603
|
+
const wrapperCallbackRef = useCallback(el => {
|
|
604
|
+
wrapperRef.current = el;
|
|
605
|
+
tooltipAnchorRef(el);
|
|
606
|
+
if (el == null) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
return () => {
|
|
610
|
+
if (el.contains(el.ownerDocument.activeElement)) {
|
|
611
|
+
isFocusPendingHandOffRef.current = true;
|
|
612
|
+
}
|
|
613
|
+
wrapperRef.current = null;
|
|
614
|
+
tooltipAnchorRef(null);
|
|
615
|
+
};
|
|
616
|
+
}, [tooltipAnchorRef]);
|
|
606
617
|
|
|
607
618
|
// The sheet presentation has no input; the public ref reaches the trigger,
|
|
608
619
|
// the control it stands for (`family:input-fields` FR10).
|
|
609
|
-
const triggerMergedRef = useMergedRefs(triggerRef, ref, disabledMessageTooltip.ref);
|
|
620
|
+
const triggerMergedRef = useMergedRefs(triggerRef, ref, disabledMessageTooltip.ref, handOffFocusRef);
|
|
610
621
|
useImperativeHandle(handleRef, () => ({
|
|
611
622
|
focus() {
|
|
612
623
|
(isSheetPresentation ? triggerRef : inputRef).current?.focus();
|
|
@@ -678,7 +689,6 @@ export function Tokenizer({
|
|
|
678
689
|
layer.show();
|
|
679
690
|
}
|
|
680
691
|
if (comingFromOutside) {
|
|
681
|
-
isControlFocusedRef.current = true;
|
|
682
692
|
if (!isHandingOffFocusRef.current) {
|
|
683
693
|
hasReportedFocusRef.current = true;
|
|
684
694
|
onFocus?.(e);
|
|
@@ -692,8 +702,9 @@ export function Tokenizer({
|
|
|
692
702
|
}, [isLayerMode, layer, isFocusInTokenizer, onFocus]);
|
|
693
703
|
const handleBlurCapture = useCallback(e => {
|
|
694
704
|
if (!isFocusInTokenizer(e.relatedTarget)) {
|
|
695
|
-
isControlFocusedRef.current = false;
|
|
696
705
|
hasReportedFocusRef.current = false;
|
|
706
|
+
// Focus left the field: no hand-off is owed any more.
|
|
707
|
+
isFocusPendingHandOffRef.current = false;
|
|
697
708
|
setIsFocusedWithin(false);
|
|
698
709
|
onBlur?.(e);
|
|
699
710
|
if (isLayerMode) {
|
|
@@ -915,13 +926,7 @@ export function Tokenizer({
|
|
|
915
926
|
left: 'anchor(start)'
|
|
916
927
|
};
|
|
917
928
|
const wrapperContent = /*#__PURE__*/_jsxs("div", {
|
|
918
|
-
ref:
|
|
919
|
-
wrapperRef.current = el;
|
|
920
|
-
// Anchor + hover/focus listeners for the disabled-message tooltip.
|
|
921
|
-
// Handlers are gated internally by isEnabled, so attaching
|
|
922
|
-
// unconditionally is safe.
|
|
923
|
-
disabledMessageTooltip.ref(el);
|
|
924
|
-
},
|
|
929
|
+
ref: wrapperCallbackRef,
|
|
925
930
|
role: "group",
|
|
926
931
|
"aria-label": label,
|
|
927
932
|
onClick: handleWrapperClick,
|
|
@@ -971,7 +976,7 @@ export function Tokenizer({
|
|
|
971
976
|
ref: useMergedRefs(inputRef,
|
|
972
977
|
// With `experimental`, the ref type admits the trigger too; in
|
|
973
978
|
// this presentation it receives the input.
|
|
974
|
-
ref),
|
|
979
|
+
ref, handOffFocusRef),
|
|
975
980
|
searchSource: isAtMax ? emptySource : filteredSource,
|
|
976
981
|
value: null,
|
|
977
982
|
onChange: handleAdd,
|
|
@@ -1079,7 +1084,6 @@ export function Tokenizer({
|
|
|
1079
1084
|
// trigger button, and the handler only reads the event.
|
|
1080
1085
|
callerOnClick),
|
|
1081
1086
|
onFocus: event => {
|
|
1082
|
-
isControlFocusedRef.current = true;
|
|
1083
1087
|
// BottomSheet focuses the trigger only after its exit completes;
|
|
1084
1088
|
// that focus is the signal that the retained sheet can leave. If
|
|
1085
1089
|
// the resolution moved to the popover meanwhile, the inline input
|
|
@@ -1106,8 +1110,8 @@ export function Tokenizer({
|
|
|
1106
1110
|
if (isSheetOpen) {
|
|
1107
1111
|
return;
|
|
1108
1112
|
}
|
|
1109
|
-
isControlFocusedRef.current = false;
|
|
1110
1113
|
hasReportedFocusRef.current = false;
|
|
1114
|
+
isFocusPendingHandOffRef.current = false;
|
|
1111
1115
|
onBlur?.(event);
|
|
1112
1116
|
},
|
|
1113
1117
|
onKeyDown: onKeyDown,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TokenizerBottomSheet.d.ts","sourceRoot":"","sources":["../../src/Tokenizer/TokenizerBottomSheet.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAML,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"TokenizerBottomSheet.d.ts","sourceRoot":"","sources":["../../src/Tokenizer/TokenizerBottomSheet.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAML,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAkBf,OAAO,KAAK,EAAC,cAAc,EAAE,YAAY,EAAC,MAAM,oBAAoB,CAAC;AASrE,sFAAsF;AACtF,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAyHzC,MAAM,WAAW,yBAAyB,CAAC,CAAC,SAAS,cAAc;IACjE,iCAAiC;IACjC,MAAM,EAAE,OAAO,CAAC;IAChB,uEAAuE;IACvE,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACxC,kEAAkE;IAClE,aAAa,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC7C;;;;;;;OAOG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IAC9B,+BAA+B;IAC/B,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,uCAAuC;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,eAAe,EAAE,SAAS,CAAC;IAC3B;;;;;OAKG;IACH,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE;QAAC,MAAM,EAAE,IAAI,CAAA;KAAC,KAAK,OAAO,CAAC;IACrD,2DAA2D;IAC3D,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE;QAAC,MAAM,EAAE,IAAI,CAAA;KAAC,KAAK,OAAO,CAAC;IACxD,kCAAkC;IAClC,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED;;;;;;;GAOG;AACH;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,cAAc,EAAE,EAC7D,MAAM,EACN,YAAY,EACZ,aAAa,EACb,KAAK,EACL,GAAG,OAAO,EACX,EAAE,yBAAyB,CAAC,CAAC,CAAC,+BAgC9B;yBAtCe,oBAAoB"}
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* - /packages/core/src/Tokenizer/Tokenizer.spec.md (FR16–FR24, AR6, PR6)
|
|
27
27
|
*/
|
|
28
28
|
import { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
29
|
+
import { flushSync } from 'react-dom';
|
|
29
30
|
import * as stylex from '@stylexjs/stylex';
|
|
30
31
|
import { AlertDialog } from "../AlertDialog/AlertDialog.js";
|
|
31
32
|
import { BottomSheet } from "../BottomSheet/BottomSheet.js";
|
|
@@ -176,8 +177,9 @@ function TokenizerSheetContent({
|
|
|
176
177
|
// trigger's press — so an open sheet with no confirmation pending carries
|
|
177
178
|
// no closed dialog (`architecture:component-style-authoring` INV7). Dialog
|
|
178
179
|
// has an entrance animation and no exit, so a close is its unmount; the
|
|
179
|
-
//
|
|
180
|
-
// gone and the sheet's content can take focus again
|
|
180
|
+
// close handler commits that unmount at once and then places focus itself,
|
|
181
|
+
// now that the modal is gone and the sheet's content can take focus again
|
|
182
|
+
// (`architecture:react-component-runtime` INV2): on the search field
|
|
181
183
|
// after a confirmed clear — the clear is spent, and the search field is
|
|
182
184
|
// where the next selection starts (FR24, `spec:AST-085` FR4) — and back on
|
|
183
185
|
// Clear all after a cancel. A confirmed clear is announced there too — a
|
|
@@ -189,25 +191,17 @@ function TokenizerSheetContent({
|
|
|
189
191
|
// dialog, so FR14 routes the words into it while the sheet is open.
|
|
190
192
|
const [isClearConfirmationOpen, setIsClearConfirmationOpen] = useState(false);
|
|
191
193
|
const clearAllRef = useRef(null);
|
|
192
|
-
const afterConfirmationRef = useRef(null);
|
|
193
194
|
const announce = useAnnounce();
|
|
194
195
|
const closeClearConfirmation = confirmed => {
|
|
195
|
-
|
|
196
|
-
setIsClearConfirmationOpen(false);
|
|
197
|
-
|
|
198
|
-
useEffect(() => {
|
|
199
|
-
if (isClearConfirmationOpen || afterConfirmationRef.current == null) {
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
const destination = afterConfirmationRef.current;
|
|
203
|
-
afterConfirmationRef.current = null;
|
|
204
|
-
if (destination === 'search') {
|
|
196
|
+
// eslint-disable-next-line @eslint-react/dom-no-flush-sync -- the confirmation must be gone before focus moves into the sheet and the words are routed into it
|
|
197
|
+
flushSync(() => setIsClearConfirmationOpen(false));
|
|
198
|
+
if (confirmed) {
|
|
205
199
|
searchRef.current?.focus();
|
|
206
200
|
announce(t('@astryx.tokenizer.allRemoved'));
|
|
207
201
|
} else {
|
|
208
202
|
clearAllRef.current?.focus();
|
|
209
203
|
}
|
|
210
|
-
}
|
|
204
|
+
};
|
|
211
205
|
const selectedIds = useMemo(() => new Set(value.map(v => v.id)), [value]);
|
|
212
206
|
|
|
213
207
|
// ---- sheet session ----
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BaseTypeahead.d.ts","sourceRoot":"","sources":["../../src/Typeahead/BaseTypeahead.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,EAMZ,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAIf,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,kBAAkB,CAAC;AA2BnD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,cAAc,EAAE,YAAY,EAAC,MAAM,SAAS,CAAC;AAY1D,MAAM,WAAW,kBAAkB,CAAC,CAAC,SAAS,cAAc,CAAE,SAAQ,IAAI,CACxE,SAAS,CAAC,WAAW,CAAC,EACtB,UAAU,CACX;IACC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAClC;;OAEG;IACH,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IAE9B;;OAEG;IACH,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IAEhB;;OAEG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC;IAEnC;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,SAAS,CAAC;IAEpC;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;;;;OAUG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;;;;;;;;;OAaG;IACH,eAAe,CAAC,EAAE,SAAS,CAAC;IAE5B;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;;;;;;OASG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAE9B;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;OAEG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAExC;;OAEG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAEzC;;;;;;;;;;;;;;;;;;;OAmBG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC;IAEtD;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;OAEG;IACH,WAAW,CAAC,EAAE,YAAY,CAAC;IAE3B;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAE1C;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IAE/D;;;;OAIG;IACH,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;CAC3B;AAoKD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,aAAa,
|
|
1
|
+
{"version":3,"file":"BaseTypeahead.d.ts","sourceRoot":"","sources":["../../src/Typeahead/BaseTypeahead.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,EAMZ,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAIf,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,kBAAkB,CAAC;AA2BnD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,cAAc,EAAE,YAAY,EAAC,MAAM,SAAS,CAAC;AAY1D,MAAM,WAAW,kBAAkB,CAAC,CAAC,SAAS,cAAc,CAAE,SAAQ,IAAI,CACxE,SAAS,CAAC,WAAW,CAAC,EACtB,UAAU,CACX;IACC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAClC;;OAEG;IACH,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IAE9B;;OAEG;IACH,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IAEhB;;OAEG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC;IAEnC;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,SAAS,CAAC;IAEpC;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;;;;OAUG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;;;;;;;;;OAaG;IACH,eAAe,CAAC,EAAE,SAAS,CAAC;IAE5B;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;;;;;;OASG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAE9B;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;OAEG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAExC;;OAEG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAEzC;;;;;;;;;;;;;;;;;;;OAmBG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC;IAEtD;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;OAEG;IACH,WAAW,CAAC,EAAE,YAAY,CAAC;IAE3B;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAE1C;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IAE/D;;;;OAIG;IACH,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;CAC3B;AAoKD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,aAAa,EAi8BrB,CAAC,CAAC,SAAS,cAAc,EAC5B,KAAK,EAAE,kBAAkB,CAAC,CAAC,CAAC,KACzB,KAAK,CAAC,YAAY,CAAC"}
|
|
@@ -715,14 +715,21 @@ export const BaseTypeahead = function BaseTypeahead({
|
|
|
715
715
|
if (!popover.isOpen) {
|
|
716
716
|
if (e.key === 'ArrowDown' && (hasEntriesOnFocus || query.length > 0)) {
|
|
717
717
|
e.preventDefault();
|
|
718
|
-
|
|
718
|
+
// Results held from a replaced search source are not current
|
|
719
|
+
// (spec:AST-092 FR4): they count as none, so the new source loads.
|
|
720
|
+
const hasResultsFromCurrentSource = results.length > 0 && resultsGenRef.current >= sourceGenRef.current;
|
|
721
|
+
const isQueryBelowMinLength = isBelowMinQueryLength(query, minQueryLength);
|
|
722
|
+
if (hasResultsFromCurrentSource) {
|
|
719
723
|
popover.show();
|
|
720
724
|
setHighlightedIndex(0);
|
|
725
|
+
} else if (results.length > 0 && query.length > 0 && !isQueryBelowMinLength) {
|
|
726
|
+
// The typed query runs again against the new source.
|
|
727
|
+
void performSearch(query);
|
|
721
728
|
} else if (hasEntriesOnFocus &&
|
|
722
729
|
// A below-threshold query was never searched; falling back to the
|
|
723
730
|
// bootstrap entries here would open a menu of suggestions that
|
|
724
731
|
// ignore what the user has already typed.
|
|
725
|
-
!
|
|
732
|
+
!isQueryBelowMinLength) {
|
|
726
733
|
void performBootstrap();
|
|
727
734
|
}
|
|
728
735
|
}
|
|
@@ -776,7 +783,7 @@ export const BaseTypeahead = function BaseTypeahead({
|
|
|
776
783
|
}
|
|
777
784
|
break;
|
|
778
785
|
}
|
|
779
|
-
}, [isDisabled, popover, results, highlightedIndex, handleSelect, hasEntriesOnFocus, query, minQueryLength, performBootstrap, externalOnKeyDown]);
|
|
786
|
+
}, [isDisabled, popover, results, highlightedIndex, handleSelect, hasEntriesOnFocus, query, minQueryLength, performBootstrap, performSearch, externalOnKeyDown]);
|
|
780
787
|
|
|
781
788
|
// Generate item ID for accessibility
|
|
782
789
|
const getItemId = useCallback(index => `${listboxId}-option-${index}`, [listboxId]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.7.6-canary.
|
|
3
|
+
"version": "0.7.6-canary.c8b1b93",
|
|
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",
|
|
@@ -860,9 +860,9 @@
|
|
|
860
860
|
"react-dom": ">=19.0.0"
|
|
861
861
|
},
|
|
862
862
|
"devDependencies": {
|
|
863
|
-
"@astryxdesign/a11y-spec": "0.7.6-canary.
|
|
864
|
-
"@astryxdesign/cli": "0.7.6-canary.
|
|
865
|
-
"@astryxdesign/interaction-harness": "0.7.6-canary.
|
|
863
|
+
"@astryxdesign/a11y-spec": "0.7.6-canary.c8b1b93",
|
|
864
|
+
"@astryxdesign/cli": "0.7.6-canary.c8b1b93",
|
|
865
|
+
"@astryxdesign/interaction-harness": "0.7.6-canary.c8b1b93",
|
|
866
866
|
"@babel/cli": "^7.29.7",
|
|
867
867
|
"@babel/core": "^7.29.7",
|
|
868
868
|
"@babel/preset-react": "^7.29.7",
|
|
@@ -39,15 +39,15 @@ system_specs: [spec:AST-010, spec:AST-020, spec:AST-085]
|
|
|
39
39
|
|
|
40
40
|
## Contract at a glance
|
|
41
41
|
|
|
42
|
-
| Area | Contract
|
|
43
|
-
| ----------------------- |
|
|
44
|
-
| Public contract | `useResizable` (size, bounds, collapse, snaps, persistence) and `ResizeHandle` (`direction`, `position`, `isReversed`, `isDisabled`, `hasDivider`, `isAlwaysVisible`, `pillPlacement`, `label`, `resizable`, `children`), plus the `pixel()` and `percent()` size helpers.
|
|
45
|
-
| Behavior | The hook owns a region's size in pixels; the Handle is a focusable window splitter that resizes it by pointer and keyboard.
|
|
46
|
-
| End-user impact | People drag or use the keyboard to resize a panel, collapse and restore it, and have the size remembered, with the divider moving the way the page reads.
|
|
47
|
-
| Builder impact | None; the record describes shipped behavior.
|
|
48
|
-
| Compatibility/readiness | Released behavior recorded as current; no API, default, or output changes. Implementation gaps are listed under Current-state impact.
|
|
49
|
-
| Review checks | Reject a Handle that loses its splitter role, values, or keys, arrows that move against the reading direction, a percentage default that rescales, or state, paint, storage, and ARIA that disagree.
|
|
50
|
-
| Governing rules | `spec:AST-010`; `spec:AST-020` FR15 (APG Window Splitter); `spec:AST-085`; `architecture:interaction-modality`; `architecture:component-theming-surface`; `architecture:public-component-api`.
|
|
42
|
+
| Area | Contract |
|
|
43
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
44
|
+
| Public contract | `useResizable` (size, bounds, collapse, snaps, persistence) and `ResizeHandle` (`direction`, `position`, `isReversed`, `isDisabled`, `hasDivider`, `isAlwaysVisible`, `pillPlacement`, `label`, `resizable`, `children`), plus the `pixel()` and `percent()` size helpers. |
|
|
45
|
+
| Behavior | The hook owns a region's size in pixels; the Handle is a focusable window splitter that resizes it by pointer and keyboard. |
|
|
46
|
+
| End-user impact | People drag or use the keyboard to resize a panel, collapse and restore it, and have the size remembered, with the divider moving the way the page reads. |
|
|
47
|
+
| Builder impact | None; the record describes shipped behavior. |
|
|
48
|
+
| Compatibility/readiness | Released behavior recorded as current; no API, default, or output changes. Implementation gaps are listed under Current-state impact. |
|
|
49
|
+
| Review checks | Reject a Handle that loses its splitter role, values, or keys, arrows that move against the reading direction, a percentage default that rescales, or settled state, paint, storage, and ARIA that disagree; storage holds the last settled size and receives the final size once when a drag ends, cancels, or unmounts. |
|
|
50
|
+
| Governing rules | `spec:AST-010`; `spec:AST-020` FR15 (APG Window Splitter); `spec:AST-085`; `architecture:interaction-modality`; `architecture:component-theming-surface`; `architecture:public-component-api`. |
|
|
51
51
|
|
|
52
52
|
This table is a review projection; the body below is authoritative.
|
|
53
53
|
|
|
@@ -136,7 +136,7 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
136
136
|
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
137
137
|
| FR1 | ResizeHandle renders one focusable Handle and one enlarged Grab zone (a `--spacing-4` strip on the resize axis); the default render also contains a Grip pill, and caller `children` replace the pill. | Current source, docs, and tests |
|
|
138
138
|
| FR2 | The Handle carries `resize-handle`, the Grip pill carries `resize-handle-pill`, and the Grab zone carries no public target. | Current source and docs |
|
|
139
|
-
| FR3 | Pointer drag, keyboard, double-click collapse, snapping, persistence, reversal, and size behavior are owned by ResizeHandle and useResizable; dragging under right-to-left inverts the pointer delta so the divider follows the pointer. A disabled Handle ignores pointer and keyboard input and exposes `aria-disabled`, and keeps its tab stop (`spec:AST-085` FR1). Implementation gap: a disabled Handle sets `tabIndex={-1}` (Current-state impact).
|
|
139
|
+
| FR3 | Pointer drag, keyboard, double-click collapse, snapping, persistence, reversal, and size behavior are owned by ResizeHandle and useResizable; dragging under right-to-left inverts the pointer delta so the divider follows the pointer. A disabled Handle ignores pointer and keyboard input and exposes `aria-disabled`, and keeps its tab stop (`spec:AST-085` FR1). With `autoSaveId`, storage holds the last settled size and receives the final size once when a drag ends, cancels, or unmounts. Implementation gap: a disabled Handle sets `tabIndex={-1}` (Current-state impact). | Current source, docs, and tests |
|
|
140
140
|
| FR4 | The Handle reflects the documented `direction` visual state on its target (`architecture:component-theming-surface` INV6). Implementation gap: it reflects no `direction` (Current-state impact). | Current source and docs |
|
|
141
141
|
| FR5 | A `percent(value, {min: pixel(value)})` or `percent(value, {max: pixel(value)})` resolves against one basis — the supplied container, or the viewport when no `containerRef` is given (`spec:AST-010` FR1, FR2); as a default it selects pixels once, and as a bound it stays live and clamps the selected pixels. A descriptor with both or neither bound is invalid. Selected state, persistence, callbacks, paint, and ARIA are pixels. | `spec:AST-010`; current source and tests |
|
|
142
142
|
| FR6 | `minSize` and `maxSize` are the bound props; numeric values are pixels. When resolved bounds conflict, the maximum wins. | `spec:AST-010`; current source and tests |
|
|
@@ -254,7 +254,7 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
254
254
|
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------- |
|
|
255
255
|
| FR1 | `ResizeHandle.test.tsx`; stable visual baseline `core-resizable--horizontal-split` | Default, collapsed, both axes | Handle and Grab zone regressions fail focused tests; a changed default handle comes back changed for review. Gap: no test renders `children` to prove they replace the Grip pill. | `audit:Resizable/anatomy` |
|
|
256
256
|
| FR2 | `themingTargets.test.ts`; source review | Handle, Grip pill, Grab zone | A removed target fails the inventory. `themingTargets.test.ts` catches only a renamed or undocumented target; no check fails if the Handle or Grip pill stops carrying its target. | `audit:Resizable/theming` |
|
|
257
|
-
| FR3 | `ResizeHandle.test.tsx` and `useResizable.test.ts`
|
|
257
|
+
| FR3 | `ResizeHandle.test.tsx` and `useResizable.test.ts`; storage timing: `ResizeHandle.test.tsx` ("saves the size once, when a drag ends on …", "saves the dragged size once when the split unmounts mid-drag", "still saves each keyboard step at once") and `useResizable.test.ts` ("writes the size a drag reached once, after … releases it", "writes the final size once when the release comes before the last move renders", "keeps the size a region was dragged to when it unmounts mid-drag", "writes only the re-clamped size when the container shrank mid-drag") | Pointer, keyboard, snap, collapse, persistence; RTL drag; disabled keyboard | A changed transformation, an RTL drag that moves against the pointer, keyboard input that resizes a disabled Handle, or a per-move, stale or missing final storage write fails. Gap: no test covers disabled pointer input or the disabled tab stop. | `audit:Resizable/behavior` |
|
|
258
258
|
| FR4 | Source and docs comparison | Horizontal and vertical Handle | Gap: no test detects that the documented `direction` is not reflected. | `audit:Resizable/theming` |
|
|
259
259
|
| FR5, ORD1–ORD3 | `useResizable.test.ts`, `utils.test.ts`, typecheck | Floor, ceiling, default; invalid shapes; basis changes; numeric container parity | Wrong resolution, a rescaled default, a stale clamp, an observer leak, or state, paint, storage, and ARIA that disagree fails. | `audit:Resizable/behavior` |
|
|
260
260
|
| FR6 | `useResizable.test.ts`; typecheck | Numeric bounds; conflicting bounds | A removed alias that type-checks, a numeric bound that is not pixels, or a minimum that wins a conflict fails. | `audit:Resizable/behavior` |
|
|
@@ -765,10 +765,14 @@ function useSingleResizable(config: UseResizableSingleConfig): ResizableRegion {
|
|
|
765
765
|
// otherwise move the bound under the pointer; the new basis applies once the
|
|
766
766
|
// gesture ends.
|
|
767
767
|
const gestureBasisRef = useRef<number | null>(null);
|
|
768
|
-
// Only forces the render that releases the frozen basis
|
|
768
|
+
// Only forces the render that ends a gesture: it releases the frozen basis,
|
|
769
|
+
// and its commit writes the size the gesture settled on.
|
|
769
770
|
const [gestureTick, setGestureTick] = useState(0);
|
|
770
771
|
void gestureTick;
|
|
771
772
|
const basis = gestureBasisRef.current ?? liveBasis;
|
|
773
|
+
// The frozen basis also marks a gesture in flight, from `_onResizeStart`
|
|
774
|
+
// until `_onResizeEnd` or `_onResizeCancel`.
|
|
775
|
+
const isGestureActive = gestureBasisRef.current != null;
|
|
772
776
|
|
|
773
777
|
const didWarnMinRef = useRef(false);
|
|
774
778
|
const didWarnMaxRef = useRef(false);
|
|
@@ -945,15 +949,52 @@ function useSingleResizable(config: UseResizableSingleConfig): ResizableRegion {
|
|
|
945
949
|
}
|
|
946
950
|
}, [resolvedMin, resolvedMax]);
|
|
947
951
|
|
|
952
|
+
// The size a gesture in flight has reached, for a region that unmounts
|
|
953
|
+
// before the gesture ends.
|
|
954
|
+
const pendingWriteRef = useRef<{
|
|
955
|
+
key: string;
|
|
956
|
+
state: PersistedResizableState;
|
|
957
|
+
} | null>(null);
|
|
958
|
+
|
|
948
959
|
useEffect(() => {
|
|
960
|
+
// Synchronizes the `autoSaveId` entry in localStorage with the size.
|
|
949
961
|
// Nothing is written while the basis is still the temporary stand-in. The
|
|
950
962
|
// size on such a render is a placeholder, and persisting it overwrites the
|
|
951
963
|
// real saved size with a value derived from a basis that was never real
|
|
952
964
|
// (AST-010 Platform support, FR9).
|
|
953
|
-
if (autoSaveId
|
|
954
|
-
|
|
965
|
+
if (!autoSaveId || !isBasisMeasured) {
|
|
966
|
+
pendingWriteRef.current = null;
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
// Nor while a gesture is in flight. A drag commits a new size on every
|
|
970
|
+
// pointer move, and each write reaches every other open document of the
|
|
971
|
+
// origin as a `storage` event.
|
|
972
|
+
if (isGestureActive) {
|
|
973
|
+
pendingWriteRef.current = {key: autoSaveId, state: {size, isCollapsed}};
|
|
974
|
+
return;
|
|
955
975
|
}
|
|
956
|
-
|
|
976
|
+
// A gesture's one write happens here, in the commit that applies its
|
|
977
|
+
// release, not in the handler that ended it: this render is the first to
|
|
978
|
+
// know the final size. A basis that changed mid-gesture applies only at
|
|
979
|
+
// the release and can re-clamp the size (AST-010 FR7), and a move the
|
|
980
|
+
// release arrived before renders together with it. The handler sees
|
|
981
|
+
// neither.
|
|
982
|
+
pendingWriteRef.current = null;
|
|
983
|
+
persistState(autoSaveId, {size, isCollapsed});
|
|
984
|
+
}, [size, isCollapsed, autoSaveId, isBasisMeasured, isGestureActive]);
|
|
985
|
+
|
|
986
|
+
// A region that unmounts mid-gesture never commits a release, so the size
|
|
987
|
+
// the gesture reached is written on the way out.
|
|
988
|
+
useEffect(
|
|
989
|
+
() => () => {
|
|
990
|
+
const pending = pendingWriteRef.current;
|
|
991
|
+
pendingWriteRef.current = null;
|
|
992
|
+
if (pending != null) {
|
|
993
|
+
persistState(pending.key, pending.state);
|
|
994
|
+
}
|
|
995
|
+
},
|
|
996
|
+
[],
|
|
997
|
+
);
|
|
957
998
|
|
|
958
999
|
const collapse = useCallback(() => {
|
|
959
1000
|
// The already-collapsed guard keeps a repeated call from re-notifying a
|
|
@@ -1049,7 +1090,7 @@ function useSingleResizable(config: UseResizableSingleConfig): ResizableRegion {
|
|
|
1049
1090
|
);
|
|
1050
1091
|
|
|
1051
1092
|
// Releasing the frozen basis needs a render to apply whatever the basis
|
|
1052
|
-
// became during the gesture.
|
|
1093
|
+
// became during the gesture; that render's commit writes the final size.
|
|
1053
1094
|
const releaseGestureBasis = useCallback(() => {
|
|
1054
1095
|
if (gestureBasisRef.current != null) {
|
|
1055
1096
|
gestureBasisRef.current = null;
|
|
@@ -1058,7 +1099,8 @@ function useSingleResizable(config: UseResizableSingleConfig): ResizableRegion {
|
|
|
1058
1099
|
}, []);
|
|
1059
1100
|
|
|
1060
1101
|
const onResizeEnd = useCallback(() => {
|
|
1061
|
-
// Sizes are already committed during move;
|
|
1102
|
+
// Sizes are already committed during move; the basis is held, and the
|
|
1103
|
+
// commit that releases it writes the final size to storage.
|
|
1062
1104
|
releaseGestureBasis();
|
|
1063
1105
|
}, [releaseGestureBasis]);
|
|
1064
1106
|
|
|
@@ -1066,7 +1108,8 @@ function useSingleResizable(config: UseResizableSingleConfig): ResizableRegion {
|
|
|
1066
1108
|
// capture, a handle unmounted mid-drag — is still over. It does not signal a
|
|
1067
1109
|
// resize end (that is #5297's contract, and the sizes were committed during
|
|
1068
1110
|
// move anyway), but the basis it froze has to come back, or every later
|
|
1069
|
-
// percentage bound stays pinned to the container size at grab time
|
|
1111
|
+
// percentage bound stays pinned to the container size at grab time, and the
|
|
1112
|
+
// size it reached has to reach storage.
|
|
1070
1113
|
const onResizeCancel = releaseGestureBasis;
|
|
1071
1114
|
|
|
1072
1115
|
// The maximum a handle REPORTS (End uses only a configured maximum). A
|
|
@@ -78,14 +78,14 @@ modes, states, and usage remain documented in `Slider.doc.mjs`.
|
|
|
78
78
|
|
|
79
79
|
## Behavioral and layout contract
|
|
80
80
|
|
|
81
|
-
| ID | Candidate invariant
|
|
82
|
-
| --- |
|
|
83
|
-
| FR1 | The current render places a filled range, one or two thumbs, and optional tick marks over the background track.
|
|
84
|
-
| FR2 | `Slider`, `Interactive control`, `Track`, and `Thumb` carry the `slider`, `slider-control`, `slider-track`, and `slider-thumb` targets respectively.
|
|
85
|
-
| FR3 | Filled range, tick marks, mark labels, and adjacent text value display are stable rendered parts without their own current Slider target.
|
|
86
|
-
| FR4 | Label and status presentation continue to use Field and FieldStatus; value tooltips continue to use Tooltip.
|
|
87
|
-
| FR5 | `onChangeEnd` fires once when an interaction ends — a pointer drag or track press is released or cancelled, or a keyboard step (arrow, Page, Home, End) completes — and carries the value the owner renders after its synchronous response to that interaction's `onChange` calls, including any clamp, snap, or rejection; it never carries a value Slider emitted that the owner did not render. A range reports both values the same way.
|
|
88
|
-
| FR6 | When both thumbs of a range share a value, a pointer press takes the thumb that can travel toward the pointer: a press on the track beside them takes the thumb on its side, and a press on the thumbs moves neither until the pointer moves, when the direction of that first movement picks the thumb, which takes the drag's focus, pressed state and value tooltip. A drag that began on the shared thumbs and comes back past their value moves the other thumb: the dragged thumb stops against it and the other follows the pointer, so with both at 80 a drag to 95 and back to 50 ends at [50, 80]. | Current source and focused tests | Accepted contract |
|
|
81
|
+
| ID | Candidate invariant | Basis | Review state |
|
|
82
|
+
| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | -------------------------------------------------- |
|
|
83
|
+
| FR1 | The current render places a filled range, one or two thumbs, and optional tick marks over the background track. | Current source, docs, and focused tests | Verified current behavior; no new behavior decided |
|
|
84
|
+
| FR2 | `Slider`, `Interactive control`, `Track`, and `Thumb` carry the `slider`, `slider-control`, `slider-track`, and `slider-thumb` targets respectively. | Current source, public docs, and DEC-1 | Approved additive target contract |
|
|
85
|
+
| FR3 | Filled range, tick marks, mark labels, and adjacent text value display are stable rendered parts without their own current Slider target. | Current source and public docs | Verified current asymmetry; not ratified as policy |
|
|
86
|
+
| FR4 | Label and status presentation continue to use Field and FieldStatus; value tooltips continue to use Tooltip. | Current source and focused tests | Verified composition boundary |
|
|
87
|
+
| FR5 | `onChangeEnd` fires once when an interaction ends — a pointer drag or track press is released or cancelled, or a keyboard step (arrow, Page, Home, End) completes — and carries the value the owner renders after its synchronous response to that interaction's `onChange` calls, including any clamp, snap, or rejection; it never carries a value Slider emitted that the owner did not render. A range reports both values the same way. | Maintainer decision (DEC-3) | Approved |
|
|
88
|
+
| FR6 | When both thumbs of a range share a value, a pointer press takes the thumb that can travel toward the pointer: a press on the track beside them takes the thumb on its side, and a press on the thumbs or on their shared value, such as its mark label, moves neither until the pointer moves, when the direction of that first movement picks the thumb, which takes the drag's focus, pressed state and value tooltip. Until then the upper thumb, drawn over the lower one, holds the press, its focus and its pressed state, so a press that never moves changes no value and leaves focus on the upper thumb. A drag that began on the shared thumbs and comes back past their value moves the other thumb: the dragged thumb stops against it and the other follows the pointer, so with both at 80 a drag to 95 and back to 50 ends at [50, 80]. | Current source and focused tests | Accepted contract |
|
|
89
89
|
|
|
90
90
|
### Observed current target asymmetry
|
|
91
91
|
|
|
@@ -226,16 +226,16 @@ separately listed value tooltip retains Tooltip's target.
|
|
|
226
226
|
|
|
227
227
|
## Verification map
|
|
228
228
|
|
|
229
|
-
| Contract | Verification | Representative states
|
|
230
|
-
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
231
|
-
| FR1 | `Slider.test.tsx` structure, range, and marks suites | Single, range, horizontal, vertical, marks
|
|
232
|
-
| AR1 | Slider accessibility contract (`internal/a11y-spec`, pending) bound to Slider; disabled-thumb Tab stop pending conformance; in jsdom and Chromium; `Slider.test.tsx` keyboard suites, including "follows the mirrored track under RTL: ArrowLeft increases, ArrowRight decreases, Up/Down unchanged" and "keeps ArrowLeft decreasing on a vertical track under RTL"; the Page, Home/End and Up/Down cases under the contract | Single and range; horizontal LTR and RTL; vertical; disabled; formatted value
|
|
233
|
-
| FR2 | `Slider.test.tsx`, `themingTargets.test.ts`, generated probe theme, and Chromium | Root, horizontal/vertical control, disabled control, track, and thumb
|
|
234
|
-
| FR3 | Source and consumer-doc review | Filled range, marks, labels, adjacent text value
|
|
235
|
-
| FR4 | Source inspection; focused tests cover label, status, and disabled-reason Tooltip only | Label, status, value tooltip, disabled reason
|
|
236
|
-
| FR5 | Pending: `Slider.test.tsx` change-end cases driven by an owner that renders, clamps, or rejects the emitted value (not yet in the suite) | Single and range; pointer release before and after the owner re-renders; each keyboard step
|
|
237
|
-
| FR6 | `Slider.test.tsx` coincident range thumbs cases | Both thumbs at the minimum, the maximum and mid-range; a track press on each side; a press on the thumbs moved either way; a drag that passes back over the shared value; right to left | A press or drag on coincident thumbs moves neither thumb, moves the thumb that cannot travel toward the pointer,
|
|
238
|
-
| Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and four current local targets
|
|
229
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
230
|
+
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
|
|
231
|
+
| FR1 | `Slider.test.tsx` structure, range, and marks suites | Single, range, horizontal, vertical, marks | Removing or misaligning stable parts breaks existing role, position, or mark assertions. | `audit:Slider/anatomy` |
|
|
232
|
+
| AR1 | Slider accessibility contract (`internal/a11y-spec`, pending) bound to Slider; disabled-thumb Tab stop pending conformance; in jsdom and Chromium; `Slider.test.tsx` keyboard suites, including "follows the mirrored track under RTL: ArrowLeft increases, ArrowRight decreases, Up/Down unchanged" and "keeps ArrowLeft decreasing on a vertical track under RTL"; the Page, Home/End and Up/Down cases under the contract | Single and range; horizontal LTR and RTL; vertical; disabled; formatted value | A thumb loses its role, name or value properties; an arrow, Page, Home or End key stops stepping or steps against the mirrored track; a range thumb crosses the other | `audit:Slider/accessibility` |
|
|
233
|
+
| FR2 | `Slider.test.tsx`, `themingTargets.test.ts`, generated probe theme, and Chromium | Root, horizontal/vertical control, disabled control, track, and thumb | A target class/state is missing, undocumented, or placed outside its owning anatomy. | `audit:Slider/theming` |
|
|
234
|
+
| FR3 | Source and consumer-doc review | Filled range, marks, labels, adjacent text value | A missing target is inaccurately documented as present or intentionally permanent. | `audit:Slider/theming` |
|
|
235
|
+
| FR4 | Source inspection; focused tests cover label, status, and disabled-reason Tooltip only | Label, status, value tooltip, disabled reason | Shared composition or its accessible association disappears; value-tooltip composition still lacks focused coverage. | `audit:Slider/anatomy` |
|
|
236
|
+
| FR5 | Pending: `Slider.test.tsx` change-end cases driven by an owner that renders, clamps, or rejects the emitted value (not yet in the suite) | Single and range; pointer release before and after the owner re-renders; each keyboard step | `onChangeEnd` reports a value the owner did not render, fires more than once per interaction, or skips a completed keyboard step | `audit:Slider/accessibility` |
|
|
237
|
+
| FR6 | `Slider.test.tsx` coincident range thumbs cases | Both thumbs at the minimum, the maximum and mid-range; a track press on each side; a press on the thumbs moved either way; a press that never moves, on the thumbs and on the mark label of their value; a drag that passes back over the shared value; right to left | A press or drag on coincident thumbs moves neither thumb, moves the thumb that cannot travel toward the pointer, keeps its focus, pressed state or tooltip on a thumb the drag has left, or leaves a press that never moves on the lower thumb | `audit:Slider/anatomy` |
|
|
238
|
+
| Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and four current local targets | Missing, extra, prefixed, stale, or unclaimed mappings fail repository validation. | `audit:Slider/theming` |
|
|
239
239
|
|
|
240
240
|
The focused Slider suite asserts the `slider-control` target's orientation and
|
|
241
241
|
disabled state. Source/metadata guards cover all four target declarations. The
|
package/src/Slider/Slider.tsx
CHANGED
|
@@ -834,6 +834,14 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
834
834
|
pressedThumbIndex >= 0 ? pressedThumbIndex : getClosestThumb(newVal);
|
|
835
835
|
const parks =
|
|
836
836
|
isCoincident && (pressedThumbIndex >= 0 || newVal === values[0]);
|
|
837
|
+
// Until the first movement, a parked press is held by the upper thumb,
|
|
838
|
+
// the one drawn over the lower one, wherever it landed: on the thumbs
|
|
839
|
+
// (which the upper thumb covers) or on their shared value elsewhere,
|
|
840
|
+
// such as its mark label. So its pressed paint shows, and a press that
|
|
841
|
+
// never moves leaves focus on the same thumb.
|
|
842
|
+
if (parks && pressedThumbIndex < 0) {
|
|
843
|
+
thumbIndex = 1;
|
|
844
|
+
}
|
|
837
845
|
// The first movement is measured from the pointer itself, not from a
|
|
838
846
|
// mark it pressed (a wide label sits away from its mark's value).
|
|
839
847
|
coincidentDragRef.current = parks
|
|
@@ -88,6 +88,7 @@ import {
|
|
|
88
88
|
import type {SearchableItem, SearchSource} from '../Typeahead/types';
|
|
89
89
|
import {composeEventHandlers, mergeProps} from '../utils';
|
|
90
90
|
import {joinAriaIDs} from '../utils/inputAria';
|
|
91
|
+
import {isFocusDetached} from '../utils/focusReturn';
|
|
91
92
|
import {themeProps} from '../utils/themeProps';
|
|
92
93
|
import {useTranslator} from '../i18n';
|
|
93
94
|
import type {TokenizerBottomSheetProps} from './TokenizerBottomSheet';
|
|
@@ -823,14 +824,6 @@ export function Tokenizer<T extends SearchableItem>({
|
|
|
823
824
|
},
|
|
824
825
|
[onChangeQuery],
|
|
825
826
|
);
|
|
826
|
-
// Whether the field's control — the inline input or the trigger — holds
|
|
827
|
-
// focus. Kept in a ref by the focus and blur handlers of both, because a
|
|
828
|
-
// control unmounted while focused fires no blur: when a presentation swap
|
|
829
|
-
// removes the focused control (the latch releasing into the popover, or
|
|
830
|
-
// `adaptive` changing while the sheet is closed), the new presentation's
|
|
831
|
-
// control takes the focus over once it has mounted, so focus never lands on
|
|
832
|
-
// the page body (`component:Tokenizer` FR16, `spec:AST-085` FR4).
|
|
833
|
-
const isControlFocusedRef = useRef(false);
|
|
834
827
|
// Set around a focus move the field makes itself — a presentation swap
|
|
835
828
|
// handing focus from one control to the other — so the receiving control
|
|
836
829
|
// does not report it as focus entering from outside.
|
|
@@ -841,38 +834,33 @@ export function Tokenizer<T extends SearchableItem>({
|
|
|
841
834
|
// entry, and the one focus the trigger then gets — BottomSheet's return
|
|
842
835
|
// focus — is the entry to report.
|
|
843
836
|
const hasReportedFocusRef = useRef(false);
|
|
844
|
-
//
|
|
845
|
-
//
|
|
846
|
-
//
|
|
847
|
-
//
|
|
848
|
-
//
|
|
849
|
-
//
|
|
850
|
-
//
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
if (
|
|
857
|
-
active != null &&
|
|
858
|
-
(active === inputRef.current || active === triggerRef.current)
|
|
859
|
-
) {
|
|
860
|
-
isControlFocusedRef.current = true;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
useEffect(() => {
|
|
865
|
-
if (!isControlFocusedRef.current) {
|
|
837
|
+
// When a presentation swap removes the focused control (the latch
|
|
838
|
+
// releasing into the popover, or `adaptive` changing while the sheet is
|
|
839
|
+
// closed), the new presentation's control takes the focus over, so focus
|
|
840
|
+
// never lands on the page body (`component:Tokenizer` FR16, `spec:AST-085`
|
|
841
|
+
// FR4). Both controls' callback refs own the hand-off
|
|
842
|
+
// (`architecture:react-component-runtime` INV6): the detaching control
|
|
843
|
+
// notes that it held focus — it is still in the document then, so this also
|
|
844
|
+
// covers a control focused before hydration (`hasAutoFocus`), which no focus
|
|
845
|
+
// event reported — and the attaching control takes it.
|
|
846
|
+
const isFocusPendingHandOffRef = useRef(false);
|
|
847
|
+
const handOffFocusRef = useCallback((node: HTMLElement | null) => {
|
|
848
|
+
if (node == null) {
|
|
866
849
|
return;
|
|
867
850
|
}
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
851
|
+
const takesFocus = isFocusPendingHandOffRef.current;
|
|
852
|
+
isFocusPendingHandOffRef.current = false;
|
|
853
|
+
if (takesFocus && isFocusDetached(node.ownerDocument)) {
|
|
854
|
+
isHandingOffFocusRef.current = true;
|
|
855
|
+
node.focus();
|
|
856
|
+
isHandingOffFocusRef.current = false;
|
|
871
857
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
858
|
+
return () => {
|
|
859
|
+
if (node.ownerDocument.activeElement === node) {
|
|
860
|
+
isFocusPendingHandOffRef.current = true;
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
}, []);
|
|
876
864
|
// The trigger's accessible description names the selected tokens: the
|
|
877
865
|
// button's children are tokens whose own content assistive technology
|
|
878
866
|
// leaves out of a button's name (`spec:AST-020` FR1).
|
|
@@ -947,6 +935,29 @@ export function Tokenizer<T extends SearchableItem>({
|
|
|
947
935
|
focusTrigger: 'always',
|
|
948
936
|
isEnabled: showsDisabledMessage,
|
|
949
937
|
});
|
|
938
|
+
// The inline field's wrapper holds every control a swap removes with the
|
|
939
|
+
// input — a token's Remove button, the inline Clear all — so it notes focus
|
|
940
|
+
// anywhere inside it for the hand-off above. It also anchors the
|
|
941
|
+
// disabled-message tooltip, whose handlers are gated internally by
|
|
942
|
+
// isEnabled, so attaching unconditionally is safe.
|
|
943
|
+
const tooltipAnchorRef = disabledMessageTooltip.ref;
|
|
944
|
+
const wrapperCallbackRef = useCallback(
|
|
945
|
+
(el: HTMLDivElement | null) => {
|
|
946
|
+
wrapperRef.current = el;
|
|
947
|
+
tooltipAnchorRef(el);
|
|
948
|
+
if (el == null) {
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
return () => {
|
|
952
|
+
if (el.contains(el.ownerDocument.activeElement)) {
|
|
953
|
+
isFocusPendingHandOffRef.current = true;
|
|
954
|
+
}
|
|
955
|
+
wrapperRef.current = null;
|
|
956
|
+
tooltipAnchorRef(null);
|
|
957
|
+
};
|
|
958
|
+
},
|
|
959
|
+
[tooltipAnchorRef],
|
|
960
|
+
);
|
|
950
961
|
|
|
951
962
|
// The sheet presentation has no input; the public ref reaches the trigger,
|
|
952
963
|
// the control it stands for (`family:input-fields` FR10).
|
|
@@ -954,6 +965,7 @@ export function Tokenizer<T extends SearchableItem>({
|
|
|
954
965
|
triggerRef,
|
|
955
966
|
ref as React.Ref<HTMLButtonElement> | undefined,
|
|
956
967
|
disabledMessageTooltip.ref,
|
|
968
|
+
handOffFocusRef,
|
|
957
969
|
);
|
|
958
970
|
|
|
959
971
|
useImperativeHandle(handleRef, () => ({
|
|
@@ -1034,7 +1046,6 @@ export function Tokenizer<T extends SearchableItem>({
|
|
|
1034
1046
|
layer.show();
|
|
1035
1047
|
}
|
|
1036
1048
|
if (comingFromOutside) {
|
|
1037
|
-
isControlFocusedRef.current = true;
|
|
1038
1049
|
if (!isHandingOffFocusRef.current) {
|
|
1039
1050
|
hasReportedFocusRef.current = true;
|
|
1040
1051
|
onFocus?.(e);
|
|
@@ -1052,8 +1063,9 @@ export function Tokenizer<T extends SearchableItem>({
|
|
|
1052
1063
|
const handleBlurCapture = useCallback(
|
|
1053
1064
|
(e: React.FocusEvent) => {
|
|
1054
1065
|
if (!isFocusInTokenizer(e.relatedTarget)) {
|
|
1055
|
-
isControlFocusedRef.current = false;
|
|
1056
1066
|
hasReportedFocusRef.current = false;
|
|
1067
|
+
// Focus left the field: no hand-off is owed any more.
|
|
1068
|
+
isFocusPendingHandOffRef.current = false;
|
|
1057
1069
|
setIsFocusedWithin(false);
|
|
1058
1070
|
onBlur?.(e);
|
|
1059
1071
|
if (isLayerMode) {
|
|
@@ -1327,13 +1339,7 @@ export function Tokenizer<T extends SearchableItem>({
|
|
|
1327
1339
|
|
|
1328
1340
|
const wrapperContent = (
|
|
1329
1341
|
<div
|
|
1330
|
-
ref={
|
|
1331
|
-
wrapperRef.current = el;
|
|
1332
|
-
// Anchor + hover/focus listeners for the disabled-message tooltip.
|
|
1333
|
-
// Handlers are gated internally by isEnabled, so attaching
|
|
1334
|
-
// unconditionally is safe.
|
|
1335
|
-
disabledMessageTooltip.ref(el);
|
|
1336
|
-
}}
|
|
1342
|
+
ref={wrapperCallbackRef}
|
|
1337
1343
|
role="group"
|
|
1338
1344
|
aria-label={label}
|
|
1339
1345
|
onClick={handleWrapperClick}
|
|
@@ -1399,6 +1405,7 @@ export function Tokenizer<T extends SearchableItem>({
|
|
|
1399
1405
|
// With `experimental`, the ref type admits the trigger too; in
|
|
1400
1406
|
// this presentation it receives the input.
|
|
1401
1407
|
ref as React.Ref<HTMLInputElement> | undefined,
|
|
1408
|
+
handOffFocusRef,
|
|
1402
1409
|
)}
|
|
1403
1410
|
searchSource={isAtMax ? emptySource : filteredSource}
|
|
1404
1411
|
value={null}
|
|
@@ -1533,7 +1540,6 @@ export function Tokenizer<T extends SearchableItem>({
|
|
|
1533
1540
|
React.MouseEventHandler<HTMLButtonElement> | undefined,
|
|
1534
1541
|
)}
|
|
1535
1542
|
onFocus={event => {
|
|
1536
|
-
isControlFocusedRef.current = true;
|
|
1537
1543
|
// BottomSheet focuses the trigger only after its exit completes;
|
|
1538
1544
|
// that focus is the signal that the retained sheet can leave. If
|
|
1539
1545
|
// the resolution moved to the popover meanwhile, the inline input
|
|
@@ -1562,8 +1568,8 @@ export function Tokenizer<T extends SearchableItem>({
|
|
|
1562
1568
|
if (isSheetOpen) {
|
|
1563
1569
|
return;
|
|
1564
1570
|
}
|
|
1565
|
-
isControlFocusedRef.current = false;
|
|
1566
1571
|
hasReportedFocusRef.current = false;
|
|
1572
|
+
isFocusPendingHandOffRef.current = false;
|
|
1567
1573
|
onBlur?.(event);
|
|
1568
1574
|
}}
|
|
1569
1575
|
onKeyDown={onKeyDown}
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
type ReactNode,
|
|
36
36
|
type RefObject,
|
|
37
37
|
} from 'react';
|
|
38
|
+
import {flushSync} from 'react-dom';
|
|
38
39
|
import * as stylex from '@stylexjs/stylex';
|
|
39
40
|
import {AlertDialog} from '../AlertDialog/AlertDialog';
|
|
40
41
|
import {BottomSheet} from '../BottomSheet/BottomSheet';
|
|
@@ -328,8 +329,9 @@ function TokenizerSheetContent<T extends SearchableItem>({
|
|
|
328
329
|
// trigger's press — so an open sheet with no confirmation pending carries
|
|
329
330
|
// no closed dialog (`architecture:component-style-authoring` INV7). Dialog
|
|
330
331
|
// has an entrance animation and no exit, so a close is its unmount; the
|
|
331
|
-
//
|
|
332
|
-
// gone and the sheet's content can take focus again
|
|
332
|
+
// close handler commits that unmount at once and then places focus itself,
|
|
333
|
+
// now that the modal is gone and the sheet's content can take focus again
|
|
334
|
+
// (`architecture:react-component-runtime` INV2): on the search field
|
|
333
335
|
// after a confirmed clear — the clear is spent, and the search field is
|
|
334
336
|
// where the next selection starts (FR24, `spec:AST-085` FR4) — and back on
|
|
335
337
|
// Clear all after a cancel. A confirmed clear is announced there too — a
|
|
@@ -341,25 +343,17 @@ function TokenizerSheetContent<T extends SearchableItem>({
|
|
|
341
343
|
// dialog, so FR14 routes the words into it while the sheet is open.
|
|
342
344
|
const [isClearConfirmationOpen, setIsClearConfirmationOpen] = useState(false);
|
|
343
345
|
const clearAllRef = useRef<HTMLButtonElement>(null);
|
|
344
|
-
const afterConfirmationRef = useRef<'search' | 'clear-all' | null>(null);
|
|
345
346
|
const announce = useAnnounce();
|
|
346
347
|
const closeClearConfirmation = (confirmed: boolean) => {
|
|
347
|
-
|
|
348
|
-
setIsClearConfirmationOpen(false);
|
|
349
|
-
|
|
350
|
-
useEffect(() => {
|
|
351
|
-
if (isClearConfirmationOpen || afterConfirmationRef.current == null) {
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
const destination = afterConfirmationRef.current;
|
|
355
|
-
afterConfirmationRef.current = null;
|
|
356
|
-
if (destination === 'search') {
|
|
348
|
+
// eslint-disable-next-line @eslint-react/dom-no-flush-sync -- the confirmation must be gone before focus moves into the sheet and the words are routed into it
|
|
349
|
+
flushSync(() => setIsClearConfirmationOpen(false));
|
|
350
|
+
if (confirmed) {
|
|
357
351
|
searchRef.current?.focus();
|
|
358
352
|
announce(t('@astryx.tokenizer.allRemoved'));
|
|
359
353
|
} else {
|
|
360
354
|
clearAllRef.current?.focus();
|
|
361
355
|
}
|
|
362
|
-
}
|
|
356
|
+
};
|
|
363
357
|
const selectedIds = useMemo(() => new Set(value.map(v => v.id)), [value]);
|
|
364
358
|
|
|
365
359
|
// ---- sheet session ----
|
|
@@ -1091,15 +1091,30 @@ export const BaseTypeahead = function BaseTypeahead<T extends SearchableItem>({
|
|
|
1091
1091
|
if (!popover.isOpen) {
|
|
1092
1092
|
if (e.key === 'ArrowDown' && (hasEntriesOnFocus || query.length > 0)) {
|
|
1093
1093
|
e.preventDefault();
|
|
1094
|
-
|
|
1094
|
+
// Results held from a replaced search source are not current
|
|
1095
|
+
// (spec:AST-092 FR4): they count as none, so the new source loads.
|
|
1096
|
+
const hasResultsFromCurrentSource =
|
|
1097
|
+
results.length > 0 && resultsGenRef.current >= sourceGenRef.current;
|
|
1098
|
+
const isQueryBelowMinLength = isBelowMinQueryLength(
|
|
1099
|
+
query,
|
|
1100
|
+
minQueryLength,
|
|
1101
|
+
);
|
|
1102
|
+
if (hasResultsFromCurrentSource) {
|
|
1095
1103
|
popover.show();
|
|
1096
1104
|
setHighlightedIndex(0);
|
|
1105
|
+
} else if (
|
|
1106
|
+
results.length > 0 &&
|
|
1107
|
+
query.length > 0 &&
|
|
1108
|
+
!isQueryBelowMinLength
|
|
1109
|
+
) {
|
|
1110
|
+
// The typed query runs again against the new source.
|
|
1111
|
+
void performSearch(query);
|
|
1097
1112
|
} else if (
|
|
1098
1113
|
hasEntriesOnFocus &&
|
|
1099
1114
|
// A below-threshold query was never searched; falling back to the
|
|
1100
1115
|
// bootstrap entries here would open a menu of suggestions that
|
|
1101
1116
|
// ignore what the user has already typed.
|
|
1102
|
-
!
|
|
1117
|
+
!isQueryBelowMinLength
|
|
1103
1118
|
) {
|
|
1104
1119
|
void performBootstrap();
|
|
1105
1120
|
}
|
|
@@ -1170,6 +1185,7 @@ export const BaseTypeahead = function BaseTypeahead<T extends SearchableItem>({
|
|
|
1170
1185
|
query,
|
|
1171
1186
|
minQueryLength,
|
|
1172
1187
|
performBootstrap,
|
|
1188
|
+
performSearch,
|
|
1173
1189
|
externalOnKeyDown,
|
|
1174
1190
|
],
|
|
1175
1191
|
);
|