@astryxdesign/core 0.7.6-canary.f899313 → 0.7.6-canary.faf6007

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 +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 when a gesture ends.
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 && isBasisMeasured) {
617
- persistState(autoSaveId, {
618
- size,
619
- isCollapsed
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
- }, [size, isCollapsed, autoSaveId, isBasisMeasured]);
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; only the basis is held.
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,+BAy5BlD;yBAz5Be,MAAM"}
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"}
@@ -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":"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"}
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
- // A control can hold focus without the field having seen a focus event:
528
- // the inline input server-rendered with `autofocus` (`hasAutoFocus`) is
529
- // focused before hydration, and the swap to the trigger in the first
530
- // render after it removes that input. React detaches the old control
531
- // before any effect of the swapping commit runs, so the live focus is
532
- // read during that render, while the control is still in the document —
533
- // a read of `document.activeElement` only, with no update made in render.
534
- const previousPresentationRef = useRef(isSheetPresentation);
535
- if (previousPresentationRef.current !== isSheetPresentation) {
536
- previousPresentationRef.current = isSheetPresentation;
537
- if (typeof document !== 'undefined') {
538
- const active = document.activeElement;
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 active = document.activeElement;
549
- if (active != null && active !== document.body) {
550
- return;
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
- isHandingOffFocusRef.current = true;
553
- (isSheetPresentation ? triggerRef.current : inputRef.current)?.focus();
554
- isHandingOffFocusRef.current = false;
555
- }, [isSheetPresentation]);
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: el => {
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;AAiBf,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"}
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
- // sheet then places focus itself, in the Effect below, once the modal is
180
- // gone and the sheet's content can take focus again: on the search field
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
- afterConfirmationRef.current = confirmed ? 'search' : 'clear-all';
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
- }, [announce, isClearConfirmationOpen, t]);
204
+ };
211
205
  const selectedIds = useMemo(() => new Set(value.map(v => v.id)), [value]);
212
206
 
213
207
  // ---- sheet session ----
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.7.6-canary.f899313",
3
+ "version": "0.7.6-canary.faf6007",
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.f899313",
864
- "@astryxdesign/cli": "0.7.6-canary.f899313",
865
- "@astryxdesign/interaction-harness": "0.7.6-canary.f899313",
863
+ "@astryxdesign/a11y-spec": "0.7.6-canary.faf6007",
864
+ "@astryxdesign/cli": "0.7.6-canary.faf6007",
865
+ "@astryxdesign/interaction-harness": "0.7.6-canary.faf6007",
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). | Current source, docs, and tests |
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` | Pointer, keyboard, snap, collapse, persistence; RTL drag; disabled keyboard | A changed transformation, an RTL drag that moves against the pointer, or keyboard input that resizes a disabled Handle fails. Gap: no test covers disabled pointer input or the disabled tab stop. | `audit:Resizable/behavior` |
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 when a gesture ends.
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 && isBasisMeasured) {
954
- persistState(autoSaveId, {size, isCollapsed});
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
- }, [size, isCollapsed, autoSaveId, isBasisMeasured]);
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; only the basis is held.
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 | 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 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 | 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 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, or keeps its focus, pressed state or tooltip on a thumb the drag has left | `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` |
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
@@ -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
- // A control can hold focus without the field having seen a focus event:
845
- // the inline input server-rendered with `autofocus` (`hasAutoFocus`) is
846
- // focused before hydration, and the swap to the trigger in the first
847
- // render after it removes that input. React detaches the old control
848
- // before any effect of the swapping commit runs, so the live focus is
849
- // read during that render, while the control is still in the document —
850
- // a read of `document.activeElement` only, with no update made in render.
851
- const previousPresentationRef = useRef(isSheetPresentation);
852
- if (previousPresentationRef.current !== isSheetPresentation) {
853
- previousPresentationRef.current = isSheetPresentation;
854
- if (typeof document !== 'undefined') {
855
- const active = document.activeElement;
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 active = document.activeElement;
869
- if (active != null && active !== document.body) {
870
- return;
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
- isHandingOffFocusRef.current = true;
873
- (isSheetPresentation ? triggerRef.current : inputRef.current)?.focus();
874
- isHandingOffFocusRef.current = false;
875
- }, [isSheetPresentation]);
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={el => {
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
- // sheet then places focus itself, in the Effect below, once the modal is
332
- // gone and the sheet's content can take focus again: on the search field
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
- afterConfirmationRef.current = confirmed ? 'search' : 'clear-all';
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
- }, [announce, isClearConfirmationOpen, t]);
356
+ };
363
357
  const selectedIds = useMemo(() => new Set(value.map(v => v.id)), [value]);
364
358
 
365
359
  // ---- sheet session ----