software-mansion-labs / software-mansion-labs/react-native-bottom-sheet
No way to nominate a drag region when the content reaches the sheet's top edge (scrollableNegotiation: 'none')
Nobody has claimed this yet.
- Dominant language
- Kotlin
- Stars
- 432
- Forks
- 21
- Avg merge
- 7d 7h
- Merged PRs (30d)
- 1
Description
Summary
With scrollableNegotiation: 'none', a sheet whose scrollable content reaches the sheet's own top edge has no drag region at all. There is no way to nominate one from JS, and the usual workarounds (overlaying a handle, reordering it, making it the topmost sibling, blocking touches) cannot work, because the decision never looks at touch delivery.
Why the usual workarounds do not apply
Both platforms answer "may this drag begin" by searching for a vertically-scrollable view whose bounds contain the touch point:
- iOS:
BottomSheetHostingView.scrollView(containing:in:), reached fromscrollableAncestorChain(containing:)ingestureRecognizerShouldBegin - Android:
BottomSheetHostView.findScrollableAtPoint, reached fromfindScrollableAtTouch
Both walk the subtree by geometry. Neither stops at the topmost sibling, and neither consults hit testing, pointerEvents or zIndex. So if the scrollable's frame covers a point, that point cannot start a sheet drag, whatever is painted on top of it and in whatever order.
That is the correct default. It only becomes a dead end when the content is meant to reach the sheet's top edge.
The case
A sheet with a full-bleed header (an image, a map preview) over a list, where:
- scrolling the list must never move the sheet, so
'none'is the right mode, and - the header must reach the sheet's own rounded top edge rather than start below a band of bare surface, so the list's frame starts at y = 0.
Together those leave nowhere to drag from. Pulling the header out of the scrollable works but pins it, which costs a screenful on a list-first sheet.
Reproduce with any 'none' sheet whose first child is a FlatList filling the sheet: the grabber is inert no matter where it is drawn.
Proposal
A prop that names the drag region directly, replacing the geometric search rather than adding to it:
/**
* When > 0, a drag may only BEGIN within this many points of the sheet's own
* top edge. Only meaningful with `scrollableNegotiation: 'none'`; 0 (default)
* leaves current behaviour untouched.
*/
dragRegionTopInset?: number;
Inside the existing branch:
if dragRegionTopInset > 0 {
if locationInContainer.y > dragRegionTopInset { return false }
} else if scrollableChain is not empty {
// current behaviour
}
A single number rather than a rect, because the handle is always a full-width band at the top: no measurement from JS, nothing to re-measure on rotation or a detent change.
It is also stricter than today in that mode. The current rule makes any incidental non-scrollable area a drag handle; naming the band means only the band is.
Notes
- Happy to open a PR if the shape looks right. I have this working as a local patch on 0.15.3 against the older
disableScrollableNegotiationboolean, on both platforms, but the negotiation code has been refactored since, so I would rather agree the API before porting it. - An alternative shape is nominating a view instead of an inset (a ref or a tag the native side can compare against). That is more flexible and more code; the inset covers the handle case, which I suspect is most of it.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the existing negotiation paths: iOS BottomSheetHostingView.scrollView(containing:in:), scrollableAncestorChain(containing:), and gestureRecognizerShouldBegin, plus Android BottomSheetHostView.findScrollableAtPoint and findScrollableAtTouch. Trace the scrollableNegotiation: 'none' branch on both platforms before deciding how the proposed inset enters the search. Done means the top band can begin a drag while points below it remain blocked, with default behavior unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- kotlin, react-native, swift, typescript
- Domain
- mobile-dev
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100