@@ -612,6 +612,101 @@ describe('<Drawer.SwipeArea />', () => {
612612 expect ( swipeArea ) . toHaveAttribute ( 'data-open' , '' ) ;
613613 } ) ;
614614
615+ it . skipIf ( isJSDOM ) (
616+ 'keeps the swipe-area movement on the popup when re-grabbed during close' ,
617+ async ( ) => {
618+ // Regression guard for the swipe-area re-grab flash. When the swipe area drives the open, the
619+ // popup's `--drawer-swipe-movement-*` are written imperatively by `applySwipeMovement`, but
620+ // the viewport's open-reset effect would otherwise zero them
621+ // (`resetSwipe` -> `syncDragStyles(false)`) on the same commit that flips `open` true,
622+ // flashing the popup fully open for a frame. The shared `swipeAreaActiveRef` must make the
623+ // viewport skip that reset while the swipe area owns the gesture.
624+ //
625+ // The flash only reproduces on a *re-grab*, i.e. when the popup is already mounted as the open
626+ // commit lands. A real exit animation keeps the popup mounted (`mounted` stays true) through
627+ // the close, so the re-grab below drives a fresh open while it is still in the DOM.
628+ globalThis . BASE_UI_ANIMATIONS_DISABLED = false ;
629+
630+ const style = `
631+ @keyframes swipe-regrab-exit {
632+ to {
633+ opacity: 0;
634+ }
635+ }
636+
637+ .swipe-regrab-popup {
638+ height: 200px;
639+ }
640+
641+ .swipe-regrab-popup[data-ending-style] {
642+ animation: swipe-regrab-exit 200ms;
643+ }
644+ ` ;
645+
646+ try {
647+ await render (
648+ < div >
649+ { /* eslint-disable-next-line react/no-danger */ }
650+ < style dangerouslySetInnerHTML = { { __html : style } } />
651+ < Drawer . Root >
652+ < Drawer . SwipeArea data-testid = "swipe-area" />
653+ < Drawer . Portal >
654+ < Drawer . Viewport >
655+ < Drawer . Popup className = "swipe-regrab-popup" data-testid = "popup" >
656+ < Drawer . Close > Close</ Drawer . Close >
657+ </ Drawer . Popup >
658+ </ Drawer . Viewport >
659+ </ Drawer . Portal >
660+ </ Drawer . Root >
661+ </ div > ,
662+ ) ;
663+
664+ const swipeArea = screen . getByTestId ( 'swipe-area' ) ;
665+
666+ await swipeUp ( swipeArea , 220 , 20 ) ;
667+ expect ( swipeArea ) . toHaveAttribute ( 'data-open' , '' ) ;
668+
669+ const popup = screen . getByTestId ( 'popup' ) ;
670+
671+ // Begin closing; the exit animation keeps the popup mounted while it plays.
672+ await act ( async ( ) => {
673+ screen . getByRole ( 'button' , { name : 'Close' } ) . click ( ) ;
674+ } ) ;
675+ await waitFor ( ( ) => {
676+ expect ( popup ) . toHaveAttribute ( 'data-ending-style' ) ;
677+ } ) ;
678+
679+ // Re-grab while the popup is still mounted mid-exit. A single move that locks the direction,
680+ // re-opens the drawer, and writes the movement (200 - 80 = 120px of remaining travel) on the
681+ // commit that flips `open` back to true.
682+ fireEvent . pointerDown ( swipeArea , {
683+ button : 0 ,
684+ buttons : 1 ,
685+ pointerId : 1 ,
686+ clientX : 10 ,
687+ clientY : 120 ,
688+ pointerType : 'mouse' ,
689+ } ) ;
690+ await flushMicrotasks ( ) ;
691+
692+ fireEvent . pointerMove ( swipeArea , {
693+ pointerId : 1 ,
694+ clientX : 10 ,
695+ clientY : 40 ,
696+ buttons : 1 ,
697+ pointerType : 'mouse' ,
698+ } ) ;
699+ await flushMicrotasks ( ) ;
700+
701+ expect ( swipeArea ) . toHaveAttribute ( 'data-open' , '' ) ;
702+ // The viewport's open-reset must not have clobbered the swipe area's value back to `0px`.
703+ expect ( popup . style . getPropertyValue ( '--drawer-swipe-movement-y' ) ) . toBe ( '120px' ) ;
704+ } finally {
705+ globalThis . BASE_UI_ANIMATIONS_DISABLED = true ;
706+ }
707+ } ,
708+ ) ;
709+
615710 it ( 'opens on a quick flick whose only move is already released (buttons: 0)' , async ( ) => {
616711 // On a fast flick — especially on a low-refresh-rate display — the pointer can be lifted before
617712 // the first `pointermove` is sampled, so the single move arrives with `buttons: 0` and no
0 commit comments