🎬 style: Reveal the Chat on Programmatic Drawer Closes (#14930)

This commit is contained in:
Danny Avila 2026-08-17 08:45:16 -04:00 • committed by GitHub
parent b743bc8fff
commit 6b9fe97990
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 46 additions and 4 deletions

View file

@ -494,7 +494,7 @@ describe('useDrawerSwipe — kickDrawerAnimation (button toggles)', () => {
kickDrawerAnimation(false, jest.fn());
expect(harness.drawer.style.transform).toBe('translate3d(-100%, 0, 0)');
expect(harness.pane.style.transform).toBe('translate3d(0, 0, 0)');
expect(harness.pane.style.transform).toBe('none');
harness.rerender({ open: false, enabled: true });
jest.runAllTimers();
@ -614,6 +614,33 @@ describe('useDrawerSwipe — kickDrawerAnimation (button toggles)', () => {
harness.unmount();
});
/**
* A programmatic close is a REVEAL: the pane repositions instantly under
* the opaque drawer's cover and only the drawer animates away. Animating
* the pane in from the right made every tap-to-navigate close visibly
* shift the chat leftward while the new conversation committed into the
* moving layer mid-slide. The gesture keeps the paired motion — a finger
* drags both — via `settle`, which this does not touch.
*/
it('reveals on a programmatic close: pane snaps under cover, only the drawer slides', () => {
jest.useFakeTimers();
const harness = setup(true);
kickDrawerAnimation(false, jest.fn());
expect(harness.drawer.style.transform).toBe('translate3d(-100%, 0, 0)');
expect(harness.drawer.style.transition).not.toBe('none');
expect(harness.pane.style.transform).toBe('none');
expect(harness.pane.style.transition).toBe('none');
harness.rerender({ open: false, enabled: true });
jest.runAllTimers();
expect(harness.pane.style.transition).not.toBe('none');
expect(harness.drawer.style.transform).toBe('');
jest.useRealTimers();
harness.unmount();
});
/** A superseded flip must never transiently commit the old direction —
* its commit would re-register the animator and get the NEWER flip
* dropped, stranding the drawer at the first intent. */

View file

@ -343,7 +343,9 @@ export default function useDrawerSwipe({
return;
}
drawer.style.willChange = 'transform';
pane.style.willChange = 'transform';
if (next) {
pane.style.willChange = 'transform';
}
/** Armed BEFORE the frame below (id: null — no deadline yet) so the
* staged write can verify its target is still wanted, and so a
* touchstart in between defers to the pending settle. A retarget or
@ -366,9 +368,22 @@ export default function useDrawerSwipe({
return;
}
drawer.style.transition = SIDEBAR_TRANSITION;
pane.style.transition = SIDEBAR_TRANSITION;
drawer.style.transform = next ? 'translate3d(0, 0, 0)' : 'translate3d(-100%, 0, 0)';
pane.style.transform = next ? 'translateX(100%)' : 'translate3d(0, 0, 0)';
if (next) {
pane.style.transition = SIDEBAR_TRANSITION;
pane.style.transform = 'translateX(100%)';
} else {
/** A programmatic close is a REVEAL: the pane repositions
* instantly beneath the opaque drawer's cover and only the
* drawer slides away, uncovering content already in place.
* Animating the pane in from the right made every
* tap-to-navigate close visibly shift the chat leftward while
* the new conversation committed into the moving layer
* mid-slide. The gesture keeps the paired both-move motion in
* `settle` — there a finger drags both surfaces. */
pane.style.transition = 'none';
pane.style.transform = 'none';
}
void drawer.getBoundingClientRect();
armed.id = setTimeout(() => {
settleRef.current = null;