Skip to content

Commit cc43467

Browse files
authored
fix: rewrite interpolations to always be in frame sync
1 parent d5485ac commit cc43467

File tree

7 files changed

+204
-96
lines changed

7 files changed

+204
-96
lines changed

docs/Footer.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
function Badge({
2+
name,
3+
version,
4+
}: {
5+
name: React.ReactNode
6+
version: React.ReactNode
7+
}) {
8+
return (
9+
<a
10+
className="flex text-sm"
11+
href={`https://www.npmjs.com/package/${name}/v/${version}`}
12+
>
13+
<span className="block pl-3 pr-2 py-1 text-white bg-gray-800 rounded-l-md">
14+
{name}
15+
</span>
16+
<span className="block pr-3 pl-2 py-1 bg-hero-lighter text-black rounded-r-md">
17+
v{version}
18+
</span>
19+
</a>
20+
)
21+
}
22+
23+
export default function Footer({
24+
version,
25+
reactSpringVersion,
26+
reactUseGestureVersion,
27+
}: {
28+
version: string
29+
reactSpringVersion: string
30+
reactUseGestureVersion: string
31+
}) {
32+
return (
33+
<footer className="px-10 py-32 grid md:grid-flow-col md:place-items-center place-content-center gap-8 bg-gray-900">
34+
<Badge name="react-spring-bottom-sheet" version={version} />
35+
<Badge name="react-spring" version={reactSpringVersion} />
36+
<Badge name="react-use-gesture" version={reactUseGestureVersion} />
37+
</footer>
38+
)
39+
}

docs/style.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,16 @@
2929
.is-iframe {
3030
--rsbs-ml: 0px;
3131
--rsbs-mr: 0px;
32+
33+
/* the bottom sheet doesn't need display cutout paddings when in the iframe */
34+
& [data-rsbs-has-footer='false'] [data-rsbs-content-padding] {
35+
padding-bottom: 0px;
36+
}
37+
& [data-rsbs-footer-padding] {
38+
padding-bottom: 16px;
39+
}
3240
}
41+
/* Used by things like the "Close example" links that are only needed when not in the iframe */
3342
.is-iframe .only-window {
3443
display: none;
3544
}

pages/_app.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,30 @@ import '../docs/style.css'
77
import '../src/style.css'
88

99
export async function getStaticProps() {
10-
const { version, description, homepage, name, meta = {} } = await import(
11-
'../package.json'
12-
)
10+
const [
11+
{ version, description, homepage, name, meta = {} },
12+
{ version: reactSpringVersion },
13+
{ version: reactUseGestureVersion },
14+
] = await Promise.all([
15+
import('../package.json'),
16+
import('react-spring/package.json'),
17+
import('react-use-gesture/package.json'),
18+
])
1319
if (!meta['og:site_name']) {
1420
meta['og:site_name'] = capitalize(name)
1521
}
1622

17-
return { props: { version, description, homepage, name, meta } }
23+
return {
24+
props: {
25+
version,
26+
description,
27+
homepage,
28+
name,
29+
meta,
30+
reactSpringVersion,
31+
reactUseGestureVersion,
32+
},
33+
}
1834
}
1935

2036
export type GetStaticProps = InferGetStaticPropsType<typeof getStaticProps>

pages/index.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import type { NextPage } from 'next'
21
import { aside, scrollable, simple, sticky } from '../docs/headings'
2+
import Footer from '../docs/Footer'
33
import Hero from '../docs/Hero'
4+
import MetaTags from '../docs/MetaTags'
45
import Nugget from '../docs/Nugget'
56
import StickyNugget from '../docs/StickyNugget'
6-
import MetaTags from '../docs/MetaTags'
7+
import type { NextPage } from 'next'
78
import type { GetStaticProps } from './_app'
89

910
export { getStaticProps } from './_app'
@@ -14,6 +15,8 @@ const IndexPage: NextPage<GetStaticProps> = ({
1415
description,
1516
homepage,
1617
meta,
18+
reactSpringVersion,
19+
reactUseGestureVersion,
1720
}) => (
1821
<>
1922
<MetaTags
@@ -57,6 +60,11 @@ const IndexPage: NextPage<GetStaticProps> = ({
5760
/>
5861
</div>
5962
</main>
63+
<Footer
64+
version={version}
65+
reactSpringVersion={reactSpringVersion}
66+
reactUseGestureVersion={reactUseGestureVersion}
67+
/>
6068
</>
6169
)
6270

src/BottomSheet.tsx

Lines changed: 70 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -126,36 +126,34 @@ export const BottomSheet = React.forwardRef<
126126
headerRef,
127127
})
128128

129+
// Setup refs that are used in cases where full control is needed over when a side effect is executed
130+
const maxHeightRef = useRef(maxHeight)
131+
const minSnapRef = useRef(minSnap)
132+
const maxSnapRef = useRef(maxSnap)
133+
const findSnapRef = useRef(findSnap)
134+
// Sync the refs with current state, giving the spring full control over when to respond to changes
135+
useEffect(() => {
136+
maxHeightRef.current = maxHeight
137+
maxSnapRef.current = maxSnap
138+
minSnapRef.current = minSnap
139+
findSnapRef.current = findSnap
140+
}, [findSnap, maxHeight, maxSnap, minSnap])
141+
129142
const defaultSnapRef = useRef(0)
130143
useEffect(() => {
131144
// Wait with selectin default snap until element dimensions are measured
132145
if (!ready) return
133146
console.count('selecting default snap')
134147

135-
defaultSnapRef.current = findSnap(getDefaultSnap)
136-
}, [findSnap, getDefaultSnap, ready])
148+
defaultSnapRef.current = findSnapRef.current(getDefaultSnap)
149+
}, [getDefaultSnap, ready])
150+
151+
// @TODO can be renamed or deleted
137152
// Wether to interpolate refs or states, useful when needing to transition between changed snapshot bounds
138153
const shouldInterpolateRefs = useRef(false)
139-
const minSnapRef = useRef(minSnap)
140-
const maxSnapRef = useRef(maxSnap)
154+
141155
// Adjust the height whenever the snap points are changed due to resize events
142156
useEffect(() => {
143-
// If we're not gonna interpolate the refs we'll just quietly update them
144-
if (!shouldInterpolateRefs.current) {
145-
maxSnapRef.current = maxSnap
146-
minSnapRef.current = minSnap
147-
148-
console.log(
149-
'Resizing due to',
150-
'maxSnap:',
151-
maxSnapRef.current !== maxSnap,
152-
'minSnap:',
153-
minSnapRef.current !== minSnap
154-
)
155-
156-
return
157-
}
158-
159157
if (shouldInterpolateRefs.current) {
160158
set({
161159
// @ts-expect-error
@@ -164,7 +162,7 @@ export const BottomSheet = React.forwardRef<
164162

165163
await onSpringStartRef.current?.({ type: 'RESIZE' })
166164

167-
const snap = findSnap(heightRef.current)
165+
const snap = findSnapRef.current(heightRef.current)
168166

169167
console.log('animate resize')
170168

@@ -177,8 +175,8 @@ export const BottomSheet = React.forwardRef<
177175
minSnapRef.current !== minSnap
178176
)
179177
// Adjust bounds to have enough room for the transition
180-
maxSnapRef.current = Math.max(snap, heightRef.current)
181-
minSnapRef.current = Math.min(snap, heightRef.current)
178+
// maxSnapRef.current = Math.max(snap, heightRef.current)
179+
//minSnapRef.current = Math.min(snap, heightRef.current)
182180
console.log('new maxSnapRef', maxSnapRef.current)
183181

184182
heightRef.current = snap
@@ -187,19 +185,22 @@ export const BottomSheet = React.forwardRef<
187185
await next({
188186
y: snap,
189187
backdrop: 1,
188+
maxHeight,
189+
maxSnap,
190+
minSnap,
190191
immediate: prefersReducedMotion.current,
191192
})
192193

193-
maxSnapRef.current = maxSnap
194-
minSnapRef.current = minSnap
194+
//maxSnapRef.current = maxSnap
195+
//minSnapRef.current = minSnap
195196

196197
onSpringEndRef.current?.({ type: 'RESIZE' })
197198

198199
console.groupEnd()
199200
},
200201
})
201202
}
202-
}, [findSnap, lastSnapRef, maxSnap, minSnap, prefersReducedMotion, set])
203+
}, [lastSnapRef, maxHeight, maxSnap, minSnap, prefersReducedMotion, set])
203204
useImperativeHandle(
204205
forwardRef,
205206
() => ({
@@ -261,7 +262,11 @@ export const BottomSheet = React.forwardRef<
261262
await next({
262263
y: defaultSnapRef.current,
263264
backdrop: 1,
264-
opacity: 1,
265+
ready: 1,
266+
maxHeight: maxHeightRef.current,
267+
maxSnap: maxSnapRef.current,
268+
// Using defaultSnapRef instead of minSnapRef to avoid animating `height` on open
269+
minSnap: defaultSnapRef.current,
265270
immediate: true,
266271
})
267272

@@ -281,7 +286,11 @@ export const BottomSheet = React.forwardRef<
281286
await next({
282287
y: defaultSnapRef.current,
283288
backdrop: 0,
284-
opacity: 0,
289+
ready: 0,
290+
maxHeight: maxHeightRef.current,
291+
maxSnap: maxSnapRef.current,
292+
// Using defaultSnapRef instead of minSnapRef to avoid animating `height` on open
293+
minSnap: defaultSnapRef.current,
285294
immediate: true,
286295
})
287296

@@ -298,7 +307,11 @@ export const BottomSheet = React.forwardRef<
298307
await next({
299308
y: 0,
300309
backdrop: 0,
301-
opacity: 1,
310+
ready: 1,
311+
maxHeight: maxHeightRef.current,
312+
maxSnap: maxSnapRef.current,
313+
// Using defaultSnapRef instead of minSnapRef to avoid animating `height` on open
314+
minSnap: defaultSnapRef.current,
302315
immediate: true,
303316
})
304317

@@ -309,7 +322,11 @@ export const BottomSheet = React.forwardRef<
309322
await next({
310323
y: defaultSnapRef.current,
311324
backdrop: 1,
312-
opacity: 1,
325+
ready: 1,
326+
maxHeight: maxHeightRef.current,
327+
maxSnap: maxSnapRef.current,
328+
// Using defaultSnapRef instead of minSnapRef to avoid animating `height` on open
329+
minSnap: defaultSnapRef.current,
313330
immediate: prefersReducedMotion.current,
314331
})
315332
}
@@ -368,18 +385,30 @@ export const BottomSheet = React.forwardRef<
368385

369386
if (maybeCancel()) return
370387

371-
heightRef.current = 0
388+
// Edge case for already closed
389+
if (heightRef.current === 0) {
390+
onSpringEndRef.current?.({ type: 'CLOSE' })
391+
return
392+
}
372393

373-
console.log('animate close')
394+
// Avoid animating the height property on close and stay within FLIP bounds by upping the minSnap
395+
next({
396+
minSnap: heightRef.current,
397+
immediate: true,
398+
})
399+
400+
heightRef.current = 0
374401

375402
await next({
376403
y: 0,
377404
backdrop: 0,
405+
maxHeight: maxHeightRef.current,
406+
maxSnap: maxSnapRef.current,
378407
immediate: prefersReducedMotion.current,
379408
})
380409
if (maybeCancel()) return
381410

382-
await next({ opacity: 0, immediate: true })
411+
await next({ ready: 0, immediate: true })
383412

384413
if (maybeCancel()) return
385414

@@ -496,7 +525,10 @@ export const BottomSheet = React.forwardRef<
496525
set({
497526
y: newY,
498527
backdrop: clamp(newY / minSnapRef.current, 0, 1),
499-
opacity: 1,
528+
ready: 1,
529+
maxHeight: maxHeightRef.current,
530+
maxSnap: maxSnapRef.current,
531+
minSnap: minSnapRef.current,
500532
immediate: prefersReducedMotion.current || down,
501533
config: {
502534
mass: relativeVelocity,
@@ -535,17 +567,7 @@ export const BottomSheet = React.forwardRef<
535567
throw new TypeError('minSnapRef is NaN!!')
536568
}
537569

538-
const interpolations = useSpringInterpolations({
539-
spring,
540-
maxHeight,
541-
// Select which values to use in the interpolation based on wether it's safe to trust the height
542-
maxSnapRef: shouldInterpolateRefs.current
543-
? maxSnapRef
544-
: { current: maxSnap },
545-
minSnapRef: shouldInterpolateRefs.current
546-
? minSnapRef
547-
: { current: minSnap },
548-
})
570+
const interpolations = useSpringInterpolations({ spring })
549571

550572
return (
551573
<animated.div
@@ -563,7 +585,9 @@ export const BottomSheet = React.forwardRef<
563585
// but allow overriding them/disabling them
564586
...style,
565587
// Not overridable as the "focus lock with opacity 0" trick rely on it
566-
opacity: spring.opacity,
588+
// @TODO the line below only fails on TS <4
589+
// @ts-ignore
590+
opacity: spring.ready,
567591
// Allows interactions on the rest of the page before the close transition is finished
568592
pointerEvents: !ready || off ? 'none' : undefined,
569593
}}

src/hooks/useSpring.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ import { useSpring as useReactSpring } from 'react-spring'
44
// Put in this file befause it makes it easier to type and I'm lazy! :D
55

66
export function useSpring() {
7-
return useReactSpring(() => ({ y: 0, opacity: 0, backdrop: 0 }))
7+
return useReactSpring(() => ({
8+
y: 0,
9+
ready: 0,
10+
backdrop: 0,
11+
maxHeight: 0,
12+
minSnap: 0,
13+
maxSnap: 0,
14+
}))
815
}
916

1017
export type Spring = ReturnType<typeof useSpring>[0]

0 commit comments

Comments
 (0)