Skip to content

feat: v4#751

Draft
lodev09 wants to merge 16 commits into
mainfrom
v4
Draft

feat: v4#751
lodev09 wants to merge 16 commits into
mainfrom
v4

Conversation

@lodev09

@lodev09 lodev09 commented Jul 20, 2026

Copy link
Copy Markdown
Owner

v4 layout rewrite — the sheet lays out synchronously per detent with Yoga owning all frames

lodev09 added 10 commits July 16, 2026 06:29
* feat!: synchronous per-detent container layout

Size the container to the sheet's visible height per detent instead of
screen height / largest detent. Requires RN 0.82+.

- iOS: report all size changes; immediate state updates relayout Yoga
  within the same UIKit layout pass
- Android: per-detent state sizing (grow before animation, shrink on
  settle); remove scroll expansion padding workaround
- JS: container always fills the sheet so flex layouts track detents
- Web: sized flex wrapper for non-scrollable sheets (auto/form keep
  content-fit flow)

* feat(ios): drop AutoLayout constraints in favor of Yoga layout

Container and footer frames now come purely from Yoga (synchronous
state updates land them in the same UIKit layout pass). Footer is
pinned via bottom:0 style; keyboard avoidance uses a transform.
Removes LayoutUtil.

* feat!: auto-detect scrollables, deprecate scrollable prop

ScrollViews/FlatLists now plug in directly like a regular view:

- Content fills the sheet by default (except with auto detents, which
  derive height from natural content)
- Native scroll wiring (insets, keyboard, nested scrolling, edge
  effects) always applies to the detected ScrollView
- iOS: removed scrollview frame-forcing; Yoga owns the frame
- scrollable prop is deprecated and a no-op on native

* feat(android): resize container in realtime while dragging

Track the sheet's visible height per-frame during drag (and post-release
settle) so the container resizes with the finger like iOS, killing the
settle flicker. Skip detent reconfigure mid-drag — the size change is
driven by the drag itself and resetting behavior state kills the gesture.

* feat(android): synchronous Fabric state updates via JNI

Kotlin's StateWrapper.updateState is hardcoded async, so the container's
Yoga layout lagged the sheet by a frame or two during drags (footer
jumping). Bridge to ConcreteState::updateState with unstable_Immediate —
commits and mounts within the same UI-thread frame, like iOS. Falls back
to the async path when the codegen lib isn't loadable (merged-so builds).

* docs: add PR link to changelog entries

* chore(docs): require Node 24 for Vercel builds

* chore: run install

* feat(web): track sheet visible height in realtime while dragging

* feat!: remove deprecated scrollable prop

Scrollables are auto-detected — the prop was already a no-op on native.
Web now always uses the sized content-fill layout for plugged scrollables.
Docs updated for auto-detection.
* feat: support auto detent with scrollables

Content shadow node fills the container (flexGrow/flexShrink via Fabric
state) when a ScrollView is pinned so the viewport is bounded per detent,
while the auto detent height is measured from the ScrollView's content
size (contentFrame - scrollFrame + contentSize). Size reports coalesce to
the next tick when pinned to avoid mid-transaction frame mismatches, and
iOS seeds screen dimensions pre-present so content can lay out and
measure before presenting (parity with Android).

* fix(ios): resolve auto detent height lazily for correct initial size

The auto detent captured contentHeight as a constant when detents were
set up, so a first present raced in-flight content measurement and
opened at a stale size before resizing. The resolver now reads
contentHeight + headerHeight at layout time, and size changes during
presentation invalidate detents so the in-flight animation retargets.

* fix(ios): keep detents stable across present/dismiss transitions

Size changes no longer reassign sheet.detents (which re-lays out the whole
presentation stack, glitching the sheet behind). Auto/peek detents resolve
lazily from snapshots; size changes just refresh snapshots + invalidateDetents.
Measure heights synchronously at present time so no mid-flight retarget snaps
the animation, and drop size updates while dismissing or not presented.

* chore(example): add item button to FlatListSheet to showcase auto detent resize

* chore(example): start FlatListSheet with 10 items

* docs: changelog for #743

* docs: drop bug fix changelog entry for #743

* docs: one changelog entry per pr

* chore(example): add remove item button to FlatListSheet

* feat(web): support auto detent with plugged scrollables

Mirror native: a plugged scrollable keeps the bounded (sized) layout so
its viewport can scroll, while the auto detent resolves from the natural
height (viewport replaced by scroll content size), observed for resizes.

Also fixes drag release being swallowed when the lowest snap offset is 0
(clamped auto) — wrapper overshoot now counts as movement — and switches
the drawer to overflow: clip so browser scroll-into-view can't offset it
and block sheet drags.

* chore: flatlist sheet auto detent only
* refactor(ios): unify position tracking into a single display link

Replace the five position tracking paths (layout emission, pan gesture
emission, fake transition view + coordinator timer, interactive dismiss
link, dispatch_after settle hacks) with one self-stopping CADisplayLink
that samples the presented view's live position.

- livePosition: model position + presentation-vs-model delta in window
  space + container translation; ancestor animations compose, persistent
  transforms cancel out
- Settle is detected (still frames + no in-flight animations) instead of
  timed, and consumes pending detent changes and offset learning
- Offset learning is now flag-driven, so keyboard/rotation moves no
  longer risk corrupting learned detent offsets
- Kicks cascade up the sheet stack so a parent emits realtime while it
  moves behind a presenting/dragging child sheet
- Drag-release snaps and programmatic resizes are now realtime-tracked
  (actual UIKit spring) instead of a single non-realtime emit

* chore: changelog
* feat!: content lays out naturally like a regular view

Content no longer fills the sheet by default. It wraps its children's
natural height like a regular view or a react-navigation screen — pass
`flex: 1` via `style` to fill the sheet's visible height per detent.

Scroll views are only special-cased for keyboard handling and `auto`
sizing (where the sheet height derives from the scroll content, so the
viewport is force-bounded). For fixed detents, bound scrollables with
`flex: 1` like any regular view.

* docs: changelog for #746
* feat: add headerOptions prop with absolute header position

* docs: add headerOptions documentation

* docs: add changelog entry
* feat!: add footerOptions prop with relative footer position

Footer now lays out relative by default — it takes up space below the
content (pinned to the bottom edge via Yoga) and its height is included
in the auto detent calculation. Set footerOptions.position to 'absolute'
to restore the previous floating behavior.

* feat: exclude relative footer from peek detent calculation

A relative footer is laid out below the content, so it's pushed
off-screen at the peek detent and contributes no height.
* feat: relative footer owns the bottom inset for auto detent

* docs: changelog
* feat: footer owns the bottom inset adjustment

A relative footer now absorbs the bottom safe-area inset as padding via a
custom shadow node (state-driven, like the content view) — its background
fills the inset and the auto height stays consistent with or without a
footer. Applied on iOS, Android, and Web.

* fix(android): apply footer inset padding synchronously

The async state update raced setupSheetDetents — the behavior could be
configured against the unpadded footer height, landing the auto detent an
inset short. Bridge the footer state through TrueSheetStateUpdater
(unstable_Immediate) so the footer is padded in the same frame.

* feat: footer owns the inset for absolute position too

The footer absorbs the bottom safe-area inset whenever insetAdjustment is
automatic, regardless of position. The inset is skipped while the keyboard
is open — the footer hugs the keyboard with no gap. Peek detents exclude
the inset when an absolute footer carries it, avoiding a double count.

* chore(example): set footer background via footerStyle

The native footer absorbs the bottom inset, so the background must be on
the footer component itself to fill it. Drops the manual inset padding
left in GestureSheet.

* docs: update changelog
…osition

Absolute footers reserve content bottom padding; relative footers don't.
@vercel

vercel Bot commented Jul 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-native-true-sheet Ready Ready Preview, Comment Jul 22, 2026 1:48am

Request Review

* fix: keyboard scroll misses the bottom inset with a footer

The footer's safe-area inset drop raced the keyboard caret scroll and
detent setup on iOS (async state update) — push it with
unstable_Immediate like the container size, matching Android's sync
updater.

An absolute footer rises above the keyboard and covers the content's
bottom edge, but the keyboard inset only compensated the keyboard —
include the footer's occlusion (height minus the dropped inset) on both
platforms so focused inputs clear the footer without a manually tuned
keyboardScrollOffset.

* docs: changelog entry for #752
Keyboard-open drags read as negative slideOffset (detents collapse to the
expanded position), sinking the footer behind the keyboard. On release,
the keyboard-hide reconfigure shrinks the container without repositioning
the bottom-pinned footer, and onSlide skipped positioning during keyboard
transitions - reposition same-frame and on every slide frame instead.
TextInput touches raise disallow-intercept (native + JS responder block),
which is only swallowed for draggable EditText targets - but targets
inside the pinned scrollable never qualified. When the scrollable can't
scroll (fully expanded, content fits) it can't nested-drag the sheet
either, so the gesture died. Only skip the draggable-chain check when
the pinned scrollable can actually scroll or hosts pull-to-refresh.
@lodev09 lodev09 changed the title feat!: v4 — synchronous per-detent layout, natural content, relative footer feat: v4 Jul 21, 2026
* fix(ios): skip size reports while stacked behind a child sheet

* docs: changelog
* fix: relative footer stays in the layout flow behind the keyboard

Only an absolute footer floats above the keyboard now. A relative footer
keeps its position and safe-area inset, and the keyboard scroll inset
subtracts its height since it shields part of the keyboard's overlap.

* docs: changelog
* chore: upgrade examples to Expo SDK 57 and RN 0.86

- expo example → SDK 57 (expo ~57.0.7, expo-* 57.x)
- react-native 0.85.3 → 0.86.0 across root, bare, expo, shared
- reanimated 4.3.1 → 4.5.0, worklets 0.8.3 → 0.10.0, gesture-handler ~2.32.0
- drop react-native canBecomeFirstResponder patch (upstreamed in 0.86)
- keep react-native-screens 4.25.2 (SDK 57 pinned) + TrueSheet modal patch
- regenerate bare ios Podfile.lock/pbxproj for RN 0.86

* docs: changelog

* fix(example): bump bare React-Core-prebuilt to 0.86.0

Stale 0.85.3 prebuilt React.xcframework linked old jsi (Runtime&),
crashing on launch against RN 0.86's IRuntime& symbol.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant