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 from scrollableAncestorChain(containing:) in gestureRecognizerShouldBegin
- Android:
BottomSheetHostView.findScrollableAtPoint, reached from findScrollableAtTouch
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
disableScrollableNegotiation boolean, 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.
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:
BottomSheetHostingView.scrollView(containing:in:), reached fromscrollableAncestorChain(containing:)ingestureRecognizerShouldBeginBottomSheetHostView.findScrollableAtPoint, reached fromfindScrollableAtTouchBoth walk the subtree by geometry. Neither stops at the topmost sibling, and neither consults hit testing,
pointerEventsorzIndex. 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:
'none'is the right mode, andTogether 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 aFlatListfilling 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:
Inside the existing branch:
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
disableScrollableNegotiationboolean, on both platforms, but the negotiation code has been refactored since, so I would rather agree the API before porting it.