Skip to content

Commit 3b1eb97

Browse files
zernoniaclaude
andcommitted
fix(Drawer): bounce on drag-release in Default + 4 position variants
The Default story and the 4 Position variants all reproduced the same animation-restart bug that the Snap Points variant had: Cause 1: CSS \`animation: none\` on \`[data-swiping]\` During drag, the \`[data-swiping]\` rule set \`animation: none\`, transitioning animation-name from e.g. \`drawer-slide-bottom-in\` to \`none\`. When data-swiping was removed on release, animation-name went BACK to \`drawer-slide-bottom-in\` — which the browser interprets as a brand-new animation declaration and restarts from keyframe 0. The drawer visibly bounced to its off-screen start position (translateY(100%), translateY(-100%), translateX(-100%), translateX(100%)) and then slid all the way back after every drag release. Cause 2: Keyframes animated \`transform\` The existing keyframes used \`transform: translateY(100%)\` etc, which is the same CSS property used to apply the drag offset. Even without cause 1, any keyframe running on top of the drag would clobber the inline transform for its duration. Fix (ported from the earlier Snap Points fix): - Rewrite all 8 slide keyframes to animate the independent \`translate\` CSS property instead of \`transform\`, so they compose with the inline \`transform\` on the popup. - Drop \`animation: none\` from the \`[data-swiping]\` overrides for all 5 position/default variants. The enter animation is inert within 450ms of mount, so there's no reason to suppress it during drag. We only need to zero transition-duration so the transform transition doesn't fight the raw pointer delta. - Delete the duplicate \`<style>\` block from \`_Drawer.vue\` (it had its own copy of the buggy keyframe + the \`animation: none\` rule). The Default variant now inherits from \`Drawer.story.vue\`'s global styles like every other variant, so the fix lives in exactly one place. Verified with raf-level instrumentation in histoire at 400x800 for both the Default (bottom) and Position-Top variants: Default + small downward drag to +20px: dt=1023 end-of-drag: y=616, transform=translateY(20), tDur=0s dt=1197 first release frame: y=616, transform=translateY(20), tDur=0.45s <-- starts exactly where drag ended dt=1205: translateY(19.15) dt=1213: translateY(18.23) ...smooth interpolation to translateY(0) Position-Top + small upward drag: y=0 -> drag -15px -> release -> settles at translateY(0) cleanly 44/44 tests still passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bf4a39b commit 3b1eb97

File tree

2 files changed

+37
-87
lines changed

2 files changed

+37
-87
lines changed

packages/core/src/Drawer/story/Drawer.story.vue

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -504,14 +504,21 @@ const navLinks = [
504504
/* === Keyframes === */
505505
@keyframes drawer-overlay-in { from { opacity: 0; } }
506506
@keyframes drawer-overlay-out { to { opacity: 0; } }
507-
@keyframes drawer-slide-bottom-in { from { transform: translateY(100%); } }
508-
@keyframes drawer-slide-bottom-out { to { transform: translateY(100%); } }
509-
@keyframes drawer-slide-top-in { from { transform: translateY(-100%); } }
510-
@keyframes drawer-slide-top-out { to { transform: translateY(-100%); } }
511-
@keyframes drawer-slide-left-in { from { transform: translateX(-100%); } }
512-
@keyframes drawer-slide-left-out { to { transform: translateX(-100%); } }
513-
@keyframes drawer-slide-right-in { from { transform: translateX(100%); } }
514-
@keyframes drawer-slide-right-out { to { transform: translateX(100%); } }
507+
/* Enter/exit keyframes use the independent `translate` CSS property rather
508+
* than `transform` so they compose with the inline `transform` (which
509+
* carries the live drag offset). If keyframes animated `transform` directly,
510+
* they would clobber the drag offset for the duration of the animation,
511+
* and any mid-drag release would visibly bounce to the start position of
512+
* the keyframe (e.g. translateY(100%) for a bottom drawer) before sliding
513+
* back to the final transform. */
514+
@keyframes drawer-slide-bottom-in { from { translate: 0 100%; } }
515+
@keyframes drawer-slide-bottom-out { to { translate: 0 100%; } }
516+
@keyframes drawer-slide-top-in { from { translate: 0 -100%; } }
517+
@keyframes drawer-slide-top-out { to { translate: 0 -100%; } }
518+
@keyframes drawer-slide-left-in { from { translate: -100% 0; } }
519+
@keyframes drawer-slide-left-out { to { translate: -100% 0; } }
520+
@keyframes drawer-slide-right-in { from { translate: 100% 0; } }
521+
@keyframes drawer-slide-right-out { to { translate: 100% 0; } }
515522
516523
/* === Overlay === */
517524
.drawer-overlay {
@@ -736,7 +743,7 @@ const navLinks = [
736743
animation: drawer-slide-bottom-out 450ms cubic-bezier(0.32, 0.72, 0, 1);
737744
}
738745
.drawer-content-bottom[data-swiping] {
739-
animation: none;
746+
/* See the matching comment on .drawer-content-snap[data-swiping]. */
740747
transition-duration: 0ms;
741748
user-select: none;
742749
}
@@ -763,7 +770,11 @@ const navLinks = [
763770
animation: drawer-slide-top-out 450ms cubic-bezier(0.32, 0.72, 0, 1);
764771
}
765772
.drawer-content-top[data-swiping] {
766-
animation: none;
773+
/* Do NOT set `animation: none` here — see the matching comment on
774+
* .drawer-content-snap[data-swiping] for the full explanation. Toggling
775+
* animation-name on drag triggers a full re-run of the enter keyframe on
776+
* release, bouncing the drawer to its off-screen start position. */
777+
transition-duration: 0ms;
767778
user-select: none;
768779
}
769780
@@ -789,7 +800,11 @@ const navLinks = [
789800
animation: drawer-slide-left-out 450ms cubic-bezier(0.32, 0.72, 0, 1);
790801
}
791802
.drawer-content-left[data-swiping] {
792-
animation: none;
803+
/* Do NOT set `animation: none` here — see the matching comment on
804+
* .drawer-content-snap[data-swiping] for the full explanation. Toggling
805+
* animation-name on drag triggers a full re-run of the enter keyframe on
806+
* release, bouncing the drawer to its off-screen start position. */
807+
transition-duration: 0ms;
793808
user-select: none;
794809
}
795810
@@ -815,7 +830,11 @@ const navLinks = [
815830
animation: drawer-slide-right-out 450ms cubic-bezier(0.32, 0.72, 0, 1);
816831
}
817832
.drawer-content-right[data-swiping] {
818-
animation: none;
833+
/* Do NOT set `animation: none` here — see the matching comment on
834+
* .drawer-content-snap[data-swiping] for the full explanation. Toggling
835+
* animation-name on drag triggers a full re-run of the enter keyframe on
836+
* release, bouncing the drawer to its off-screen start position. */
837+
transition-duration: 0ms;
819838
user-select: none;
820839
}
821840

packages/core/src/Drawer/story/_Drawer.vue

Lines changed: 6 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ import {
1212
DrawerTrigger,
1313
} from '..'
1414
15+
// Shared "default" drawer used by the Default story variant. All styles
16+
// (keyframes, .drawer-content-bottom, .drawer-handle, .drawer-overlay,
17+
// .drawer-button) are defined once in Drawer.story.vue's global <style>
18+
// block — do not duplicate them here or both rules compete and the
19+
// enter/exit keyframes collide with the drag transform on release
20+
// (the "bounce" bug).
1521
const open = ref(false)
1622
</script>
1723

@@ -41,78 +47,3 @@ const open = ref(false)
4147
</DrawerPortal>
4248
</DrawerRoot>
4349
</template>
44-
45-
<style>
46-
@keyframes drawer-overlay-in { from { opacity: 0; } }
47-
@keyframes drawer-overlay-out { to { opacity: 0; } }
48-
@keyframes drawer-slide-bottom-in {
49-
from { transform: translateY(100%); }
50-
to { transform: translateY(calc(var(--drawer-snap-point-offset, 0px) + var(--drawer-swipe-movement-y, 0px))); }
51-
}
52-
@keyframes drawer-slide-bottom-out { to { transform: translateY(100%); } }
53-
54-
.drawer-overlay {
55-
position: fixed;
56-
inset: 0;
57-
background-color: rgba(0, 0, 0, 0.2);
58-
}
59-
.drawer-overlay[data-state="open"] {
60-
animation: drawer-overlay-in 450ms cubic-bezier(0.32, 0.72, 0, 1);
61-
}
62-
.drawer-overlay[data-state="closed"] {
63-
animation: drawer-overlay-out 450ms cubic-bezier(0.32, 0.72, 0, 1);
64-
}
65-
66-
.drawer-content-bottom {
67-
position: fixed;
68-
bottom: 0;
69-
left: 0;
70-
right: 0;
71-
display: flex;
72-
flex-direction: column;
73-
background: white;
74-
border-radius: 1rem 1rem 0 0;
75-
outline: none;
76-
overflow-y: auto;
77-
overscroll-behavior: contain;
78-
transform: translateY(calc(var(--drawer-snap-point-offset, 0px) + var(--drawer-swipe-movement-y, 0px)));
79-
transition: transform 450ms cubic-bezier(0.32, 0.72, 0, 1);
80-
will-change: transform;
81-
}
82-
.drawer-content-bottom[data-state="open"] {
83-
animation: drawer-slide-bottom-in 450ms cubic-bezier(0.32, 0.72, 0, 1);
84-
}
85-
.drawer-content-bottom[data-state="closed"] {
86-
animation: drawer-slide-bottom-out 450ms cubic-bezier(0.32, 0.72, 0, 1);
87-
}
88-
.drawer-content-bottom[data-swiping] {
89-
animation: none;
90-
transition-duration: 0ms;
91-
user-select: none;
92-
}
93-
94-
.drawer-handle {
95-
width: 3rem;
96-
height: 0.25rem;
97-
margin: 1rem auto 0;
98-
border-radius: 9999px;
99-
background-color: #d1d5db;
100-
flex-shrink: 0;
101-
}
102-
103-
.drawer-button {
104-
display: inline-flex;
105-
height: 2.5rem;
106-
align-items: center;
107-
justify-content: center;
108-
padding: 0 0.875rem;
109-
border: 1px solid #e5e7eb;
110-
border-radius: 0.375rem;
111-
background: #f9fafb;
112-
font-size: 1rem;
113-
cursor: pointer;
114-
}
115-
.drawer-button:hover {
116-
background: #f3f4f6;
117-
}
118-
</style>

0 commit comments

Comments
 (0)