feat(swmansion): drive backdrop from native detent index#28
Merged
Conversation
… sheet The backdrop fade is position-coupled: animatedIndex = position / openHeight. For a `'content'` detent the open height isn't known up front, and the previous code learned it by tracking the max position during the very open animation — so the divisor chased the current position and the ratio was always ~1, making the backdrop snap to fully opaque instantly. Stop chasing the position. Learn the open height from the position at the first open settle (so drag-to-dismiss and reopens get the position-coupled fade), and on that first open — before the height is known — fade the backdrop with a timing (matching the modal adapter) instead of jumping. Numeric detents are still seeded up front and unaffected. No onLayout, no extra re-render (shared value + refs only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…safe set Inline/portal sheets mount directly at the open index and animate in via the native animateIn — expand() is never called, so the first-open timing wasn't kicked for them (the common case; every open is a fresh mount since the sheet unmounts on close). Kick the timed backdrop fade in a mount effect too. Use animatedIndex.set(withTiming(...)) instead of `.value =` — the React Compiler disallows assigning to a custom hook's returned value; .set is the compiler-safe equivalent and still animates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Single contract for animatedIndex: position-coupled when the open height is known (exact, follows native open/close/drag), a timing fallback otherwise, and settle only confirms the endpoint when position-coupled or non-animated. The open settle previously hard-set animatedIndex to 0, cancelling the in-flight withTiming — so the backdrop snapped instead of fading (visible at any duration). Use a module-level resolveBackdropTarget() for the timed/instant fade, and strict typeof checks (no loose equality). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, trim comments - animateIn is part of the manager's open mechanism, not the consumer's to set: omit it from the props (like index/modal) and force it on internally. The fade is therefore always animated, so resolveBackdropTarget drops its animate flag. - Drive the backdrop coherently from one model: index changes / mount fade via timing, onPositionChange overrides with the exact position-coupled value once the open height is known, settle never snaps (it was cutting the fade short). - Reduce inline comments to the few non-obvious "why"s; keep the public JSDoc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… calls expand() The coordinator invokes expand() on every open (including inline/mount-at-open sheets), so it already kicks the backdrop fade. The mount useEffect was a redundant second kick — removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the seeded-from-detent + learned-at-settle openHeightRef (and the timed-vs-position duality it implied) with a single live peak-position ref, gating position-coupling on hasOpenedRef instead of on a known height. First open is the timing; once open, the drag follows the finger. Fewer comments. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pNativeView Bump @swmansion/react-native-bottom-sheet to 0.14 (lib + example). 0.14 makes onPositionChange a native direct event and adds wrapNativeView, so the position can be handled on the UI thread with a Reanimated worklet. The backdrop fade now runs entirely on the UI thread: pass Animated.createAnimatedComponent as wrapNativeView and a useEvent worklet as onPositionChange. The peak position and the has-opened guard become shared values; the JS-thread onPositionChange handler is gone (no per-frame bridge traffic). onPositionChange/wrapNativeView are adapter-owned (omitted from props). Open is still the timed fade; once open, the worklet follows the drag. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump @swmansion/react-native-bottom-sheet to 0.15.0-next.1, which adds the fractional `index` field to onPositionChange, and simplify the adapter to drive the shared backdrop with `animatedIndex.set(event.index - 1)` on the UI thread. Removes the JS-side reconstruction: peak-position tracking, the hasOpened shared-value gate (now a plain ref), and the timed open/close fade. Open, close, and drag-to-dismiss now all fade the backdrop from the native per-frame index.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The manager backdrop fade used to be reconstructed in JS from
onPositionChange, which only reported the sheet'sposition(points from the bottom). To turn that into a0..1open fraction the adapter had to know the open height — and for a'content'detent that height isn't known up front. The old code learned it by tracking the peak position during the open animation, which maderatio = position / maxSoFar ≈ 1every frame, so the backdrop snapped to fully opaque on the first open instead of fading. Working around it needed peak tracking, ahasOpenedgate, and a timed-fade fallback — fragile.Fix
Bump
@swmansion/react-native-bottom-sheetto0.15.0-next.1, which adds a native fractional detentindextoonPositionChange(the continuous counterpart ofonIndexChange; upstreamed in software-mansion-labs/react-native-bottom-sheet#10). The sheet now reports, per frame, exactly how far it is between detents — no height math required.The adapter drives the shared backdrop straight from it, on the UI thread (via
wrapNativeView):This removes all the JS reconstruction:
hasOpenedshared value (now a plainuseRef, used only to gate the lifecycle callback),Open, close, and drag-to-dismiss now all fade from the native per-frame index — including the first open of a
'content'-sized sheet, which is correct for free.Net: −121 / +64 in the adapter; the backdrop is now driven by one line instead of a hand-rolled position pipeline.
Validation
yarn typecheck,yarn eslint,yarn prepare(bob build incl. React Compiler) pass.