Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### 🎉 New features

- New `TrueSheetPeek` component that marks part of the content as the sheet's peek content — the `peek` detent height covers the content through its bottom edge (measured natively), along with the `header` and `footer` heights. ([#730](https://github.com/lodev09/react-native-true-sheet/pull/730) by [@lodev09](https://github.com/lodev09))

## 3.11.4

### 🐛 Bug fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package com.lodev09.truesheet

import android.annotation.SuppressLint
import android.view.View
import android.view.ViewGroup
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.events.EventDispatcher
import com.facebook.react.util.RNLog
import com.facebook.react.views.view.ReactViewGroup

interface TrueSheetContainerViewDelegate {
Expand All @@ -13,6 +15,7 @@ interface TrueSheetContainerViewDelegate {
fun containerViewScrollViewDidChange()
fun containerViewHeaderDidChangeSize(width: Int, height: Int)
fun containerViewFooterDidChangeSize(width: Int, height: Int)
fun containerViewPeekDidChangeSize(width: Int, height: Int)
}

/**
Expand All @@ -24,18 +27,40 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
ReactViewGroup(reactContext),
TrueSheetContentViewDelegate,
TrueSheetHeaderViewDelegate,
TrueSheetFooterViewDelegate {
TrueSheetFooterViewDelegate,
TrueSheetPeekViewDelegate {

var delegate: TrueSheetContainerViewDelegate? = null

var contentView: TrueSheetContentView? = null
var headerView: TrueSheetHeaderView? = null
var footerView: TrueSheetFooterView? = null
var peekView: TrueSheetPeekView? = null

var contentHeight: Int = 0
var headerHeight: Int = 0
var footerHeight: Int = 0

/**
* Distance from the top of the content view to the bottom of the peek view.
* Includes the peek view's offset within the content (padding, views above it)
* so the peek detent reveals everything down to the peek content's bottom edge.
*/
val peekContentHeight: Int
get() {
val peek = peekView ?: return 0
val content = contentView ?: return peek.height

var bottom = peek.height
var view: View = peek
while (view !== content) {
bottom += view.top
view = view.parent as? View ?: return peek.height
}

return bottom
}

var insetAdjustment: TrueSheetInsetAdjustment = TrueSheetInsetAdjustment.AUTOMATIC
var scrollViewBottomInset: Int = 0
var scrollableEnabled: Boolean = false
Expand Down Expand Up @@ -75,6 +100,10 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
child.delegate = this
child.scrollableOptions = scrollableOptions
contentView = child

// Children mount bottom-up, so the content subtree is complete here.
// Late-mounted peek views attach themselves instead (see TrueSheetPeekView).
findPeekView(child)?.let { attachPeekView(it) }
}

is TrueSheetHeaderView -> {
Expand Down Expand Up @@ -113,6 +142,41 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
super.removeViewAt(index)
}

// ==================== Peek View ====================

private fun findPeekView(view: View): TrueSheetPeekView? {
if (view is TrueSheetPeekView) return view

if (view is ViewGroup) {
for (i in 0 until view.childCount) {
findPeekView(view.getChildAt(i))?.let { return it }
}
}

return null
}

fun attachPeekView(view: TrueSheetPeekView) {
if (peekView === view) return

if (peekView != null) {
RNLog.w(context as ThemedReactContext, "TrueSheet: Sheet can only have one peek component.")
return
}

peekView = view
view.delegate = this
peekViewDidChangeSize(view.width, view.height)
}

fun detachPeekView(view: TrueSheetPeekView) {
if (peekView !== view) return

view.delegate = null
peekView = null
peekViewDidChangeSize(0, 0)
}

// ==================== Delegate Implementations ====================

override fun contentViewDidChangeSize(width: Int, height: Int) {
Expand All @@ -138,6 +202,10 @@ class TrueSheetContainerView(reactContext: ThemedReactContext) :
delegate?.containerViewFooterDidChangeSize(width, height)
}

override fun peekViewDidChangeSize(width: Int, height: Int) {
delegate?.containerViewPeekDidChangeSize(width, height)
}

companion object {
const val TAG_NAME = "TrueSheet"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class TrueSheetPackage : BaseReactPackage() {
TrueSheetContainerViewManager(),
TrueSheetContentViewManager(),
TrueSheetHeaderViewManager(),
TrueSheetFooterViewManager()
TrueSheetFooterViewManager(),
TrueSheetPeekViewManager()
)
}
66 changes: 66 additions & 0 deletions android/src/main/java/com/lodev09/truesheet/TrueSheetPeekView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.lodev09.truesheet

import android.annotation.SuppressLint
import android.view.ViewParent
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.views.view.ReactViewGroup

/**
* Delegate interface for peek view size changes
*/
interface TrueSheetPeekViewDelegate {
fun peekViewDidChangeSize(width: Int, height: Int)
}

/**
* Peek view that marks part of the content as the sheet's peek content.
* The peek detent covers the content through this view's bottom edge.
*/
@SuppressLint("ViewConstructor")
class TrueSheetPeekView(context: ThemedReactContext) : ReactViewGroup(context) {
var delegate: TrueSheetPeekViewDelegate? = null

override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)

attachToContainerView()

// Position changes matter too — the peek's offset within the content
// affects the peek detent.
if (changed) {
delegate?.peekViewDidChangeSize(width, height)
}
}

override fun onAttachedToWindow() {
super.onAttachedToWindow()
attachToContainerView()
}

/**
* Peek can be nested anywhere within the content, so it attaches itself to the
* nearest container instead of being mounted by it. The container also searches
* for it when the content is added to cover the initial (bottom-up) mount order.
*/
private fun attachToContainerView() {
if (delegate != null) return

var parent: ViewParent? = parent
while (parent != null && parent !is TrueSheetContainerView) {
parent = parent.parent
}

(parent as? TrueSheetContainerView)?.attachPeekView(this)
}

/**
* Called by the ViewManager when the view is dropped (Fabric unmount)
*/
fun detachFromContainerView() {
(delegate as? TrueSheetContainerView)?.detachPeekView(this)
}

companion object {
const val TAG_NAME = "TrueSheet"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.lodev09.truesheet

import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.PointerEvents
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.annotations.ReactProp

/**
* ViewManager for TrueSheetPeekView
* Marks part of the content as the sheet's peek content
*/
@ReactModule(name = TrueSheetPeekViewManager.REACT_CLASS)
class TrueSheetPeekViewManager : ViewGroupManager<TrueSheetPeekView>() {

override fun getName(): String = REACT_CLASS

override fun createViewInstance(reactContext: ThemedReactContext): TrueSheetPeekView = TrueSheetPeekView(reactContext)

override fun onDropViewInstance(view: TrueSheetPeekView) {
super.onDropViewInstance(view)
view.detachFromContainerView()
}

@ReactProp(name = "pointerEvents")
fun setPointerEvents(view: TrueSheetPeekView, pointerEventsStr: String?) {
view.pointerEvents = PointerEvents.parsePointerEvents(pointerEventsStr)
}

companion object {
const val REACT_CLASS = "TrueSheetPeekView"
}
}
5 changes: 5 additions & 0 deletions android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,11 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
viewController.positionFooter()
}

override fun containerViewPeekDidChangeSize(width: Int, height: Int) {
// Peek content height affects peek detents
updateSheetIfNeeded()
}

// ==================== RNScreensEventObserverDelegate ====================

override fun presenterScreenWillDisappear() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
private var cachedContentHeight: Int = 0
private var cachedHeaderHeight: Int = 0
private var cachedFooterHeight: Int = 0
private var cachedPeekContentHeight: Int = 0

override val contentHeight: Int
get() = containerView?.contentHeight ?: cachedContentHeight
Expand All @@ -286,6 +287,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
override val footerHeight: Int
get() = containerView?.footerHeight ?: cachedFooterHeight

override val peekContentHeight: Int
get() = containerView?.peekContentHeight ?: cachedPeekContentHeight

// Insets
// Target keyboard height used for detent calculations
override val keyboardInset: Int
Expand Down Expand Up @@ -841,6 +845,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
cachedContentHeight = it.contentHeight
cachedHeaderHeight = it.headerHeight
cachedFooterHeight = it.footerHeight
cachedPeekContentHeight = it.peekContentHeight
}

interactionState = InteractionState.Reconfiguring
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface TrueSheetDetentCalculatorDelegate {
val contentHeight: Int
val headerHeight: Int
val footerHeight: Int
val peekContentHeight: Int
val contentBottomInset: Int
val maxContentHeight: Int?
val keyboardInset: Int
Expand All @@ -34,17 +35,18 @@ class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
private val contentHeight: Int get() = delegate?.contentHeight ?: 0
private val headerHeight: Int get() = delegate?.headerHeight ?: 0
private val footerHeight: Int get() = delegate?.footerHeight ?: 0
private val peekContentHeight: Int get() = delegate?.peekContentHeight ?: 0
private val contentBottomInset: Int get() = delegate?.contentBottomInset ?: 0
private val maxContentHeight: Int? get() = delegate?.maxContentHeight
private val keyboardInset: Int get() = delegate?.keyboardInset ?: 0

/**
* Height for peek (-2.0) detents: header + footer height.
* Falls back to 150dp when neither is present.
* Height for peek (-2.0) detents: header + footer + peek content height.
* Falls back to 150dp when none is present.
*/
private val peekDetentHeight: Int
get() {
val height = headerHeight + footerHeight
val height = headerHeight + footerHeight + peekContentHeight
return if (height > 0) height else DEFAULT_PEEK_HEIGHT.dpToPx().toInt()
}

Expand Down
20 changes: 20 additions & 0 deletions docs/docs/guides/footer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,23 @@ When the keyboard opens, that bottom padding leaves a gap above the keyboard. Pa
:::note
On iPad, the sheet is displayed as a floating modal, so bottom padding is not needed.
:::

## Collapsing to the Footer

The footer height counts toward the [`"peek"`](../reference/types#sheetdetent) detent — collapse the sheet down to just the [`header`](header), footer, and any peeking content.

```tsx {4-4}
const App = () => {
return (
<TrueSheet
detents={['peek', 0.9]}
initialDetentIndex={0}
footer={<SheetActions />}
>
<BookingDetails />
</TrueSheet>
)
}
```

Since the footer floats, it stays visible at every detent. See [Peeking Content](peeking) for the full guide.
2 changes: 1 addition & 1 deletion docs/docs/guides/header.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ const App = () => {
}
```

The peek height automatically follows the measured header and footer sizes — no `onLayout` juggling needed. When neither `header` nor `footer` is provided, it falls back to a fixed height of `150`.
The peek height automatically follows the measured header and footer sizes — no `onLayout` juggling needed. You can also include part of the content via the `TrueSheetPeek` component — see [Peeking Content](peeking) for the full guide.

## Platform Support

Expand Down
Loading
Loading