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
33 changes: 14 additions & 19 deletions src/BottomSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
useAriaHider,
useFocusTrap,
useLayoutEffect,
useReady,
useReducedMotion,
useScrollLock,
useSnapPoints,
Expand Down Expand Up @@ -87,9 +86,6 @@ export const BottomSheet = React.forwardRef<
},
forwardRef
) {
// Before any animations can start we need to measure a few things, like the viewport and the dimensions of content, and header + footer if they exist
const { ready, registerReady } = useReady()

// Controls the drag handler, used by spring operations that happen outside the render loop in React
const canDragRef = useRef(false)

Expand Down Expand Up @@ -123,6 +119,20 @@ export const BottomSheet = React.forwardRef<

const prefersReducedMotion = useReducedMotion()

// Nothing can start until the viewport and the content have been measured
const { minSnap, maxSnap, maxHeight, findSnap, ready } = useSnapPoints({
contentRef,
controlledMaxHeight,
footerEnabled: !!footer,
footerRef,
getSnapPoints,
headerEnabled: header !== false,
headerRef,
heightRef,
lastSnapRef,
resizeSourceRef,
})

// "Plugins" huhuhu
const scrollLockRef = useScrollLock({
targetRef: scrollRef,
Expand All @@ -140,21 +150,6 @@ export const BottomSheet = React.forwardRef<
enabled: ready && blocking && initialFocusRef !== false,
})

const { minSnap, maxSnap, maxHeight, findSnap } = useSnapPoints({
contentRef,
controlledMaxHeight,
footerEnabled: !!footer,
footerRef,
getSnapPoints,
headerEnabled: header !== false,
headerRef,
heightRef,
lastSnapRef,
ready,
registerReady,
resizeSourceRef,
})

// Setup refs that are used in cases where full control is needed over when a side effect is executed
const maxHeightRef = useRef(maxHeight)
const minSnapRef = useRef(minSnap)
Expand Down
1 change: 0 additions & 1 deletion src/hooks/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export { useAriaHider } from './useAriaHider'
export { useFocusTrap } from './useFocusTrap'
export { useLayoutEffect } from './useLayoutEffect'
export { useReady } from './useReady'
export { useReducedMotion } from './useReducedMotion'
export { useScrollLock } from './useScrollLock'
export { useSnapPoints } from './useSnapPoints'
Expand Down
31 changes: 0 additions & 31 deletions src/hooks/useReady.tsx

This file was deleted.

78 changes: 23 additions & 55 deletions src/hooks/useSnapPoints.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import React, {
useCallback,
useDebugValue,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import React, { useCallback, useDebugValue, useRef, useState } from 'react'
import type {
defaultSnapProps,
MutableRef,
ResizeSource,
snapPoints,
} from '../types'
import { processSnapPoints, roundAndCheckForNaN } from '../utils'
import { useReady } from './useReady'
import { useLayoutEffect } from './useLayoutEffect'

export function useSnapPoints({
Expand All @@ -26,8 +18,6 @@ export function useSnapPoints({
headerRef,
heightRef,
lastSnapRef,
ready,
registerReady,
resizeSourceRef,
}: {
contentRef: React.RefObject<Element | null>
Expand All @@ -39,20 +29,18 @@ export function useSnapPoints({
headerRef: React.RefObject<Element | null>
heightRef: React.RefObject<number>
lastSnapRef: React.RefObject<number | null>
ready: boolean
registerReady: ReturnType<typeof useReady>['registerReady']
resizeSourceRef: MutableRef<ResizeSource | undefined>
}) {
const { maxHeight, minHeight, headerHeight, footerHeight } = useDimensions({
contentRef,
controlledMaxHeight,
footerEnabled,
footerRef,
headerEnabled,
headerRef,
registerReady,
resizeSourceRef,
})
const { maxHeight, minHeight, headerHeight, footerHeight, ready } =
useDimensions({
contentRef,
controlledMaxHeight,
footerEnabled,
footerRef,
headerEnabled,
headerRef,
resizeSourceRef,
})

const { snapPoints, minSnap, maxSnap } = processSnapPoints(
ready
Expand Down Expand Up @@ -94,7 +82,7 @@ export function useSnapPoints({

useDebugValue(`minSnap: ${minSnap}, maxSnap:${maxSnap}`)

return { minSnap, maxSnap, findSnap, maxHeight }
return { minSnap, maxSnap, findSnap, maxHeight, ready }
}

function useDimensions({
Expand All @@ -104,7 +92,6 @@ function useDimensions({
footerRef,
headerEnabled,
headerRef,
registerReady,
resizeSourceRef,
}: {
contentRef: React.RefObject<Element | null>
Expand All @@ -113,18 +100,9 @@ function useDimensions({
footerRef: React.RefObject<Element | null>
headerEnabled: boolean
headerRef: React.RefObject<Element | null>
registerReady: ReturnType<typeof useReady>['registerReady']
resizeSourceRef: MutableRef<ResizeSource | undefined>
}) {
const setReady = useMemo(
() => registerReady('contentHeight'),
[registerReady]
)
const maxHeight = useMaxHeight(
controlledMaxHeight,
registerReady,
resizeSourceRef
)
const maxHeight = useMaxHeight(controlledMaxHeight, resizeSourceRef)

const headerHeight = useElementSizeObserver(headerRef, {
label: 'headerHeight',
Expand All @@ -148,28 +126,26 @@ function useDimensions({

useDebugValue(`minHeight: ${minHeight}`)

const ready = contentHeight > 0
useEffect(() => {
if (ready) {
setReady()
}
}, [ready, setReady])

return {
maxHeight,
minHeight,
headerHeight,
footerHeight,
// Nothing can be measured or animated until the viewport and the content have a size.
// Deriving this instead of tracking it in state saves a render pass per measurement.
ready: maxHeight > 0 && contentHeight > 0,
}
}

// Respond to changes to padding, happens often on iOS when using env(safe-area-inset-bottom)
// and the user hides or shows the Safari browser toolbar
// And the user hides or shows the Safari browser toolbar
const observerOptions: ResizeObserverOptions = { box: 'border-box' }

function borderBoxHeight(entry: ResizeObserverEntry): number {
const boxSize = entry.borderBoxSize as unknown as
ResizeObserverSize | ResizeObserverSize[] | undefined
| ResizeObserverSize
| ResizeObserverSize[]
| undefined
const size = Array.isArray(boxSize) ? boxSize[0] : boxSize
if (size && typeof size.blockSize === 'number') {
return size.blockSize
Expand All @@ -179,6 +155,8 @@ function borderBoxHeight(entry: ResizeObserverEntry): number {

/**
* Hook for determining the size of an element using the Resize Observer API.
*
* @param ref - A React ref to an element
*/
function useElementSizeObserver(
ref: React.RefObject<Element | null>,
Expand Down Expand Up @@ -224,27 +202,18 @@ function useElementSizeObserver(
// Blazingly keep track of the current viewport height without blocking the thread, keeping that sweet 60fps on smartphones
function useMaxHeight(
controlledMaxHeight: number | undefined,
registerReady: ReturnType<typeof useReady>['registerReady'],
resizeSourceRef: MutableRef<ResizeSource | undefined>
) {
const setReady = useMemo(() => registerReady('maxHeight'), [registerReady])
const [maxHeight, setMaxHeight] = useState(() => {
if (controlledMaxHeight) {
return roundAndCheckForNaN(controlledMaxHeight)
}
return typeof window !== 'undefined' ? window.innerHeight : 0
})
const ready = maxHeight > 0
const raf = useRef(0)

useDebugValue(controlledMaxHeight ? 'controlled' : 'auto')

useEffect(() => {
if (ready) {
setReady()
}
}, [ready, setReady])

useLayoutEffect(() => {
// Bail if the max height is a controlled prop
if (controlledMaxHeight) {
Expand All @@ -271,13 +240,12 @@ function useMaxHeight(
window.addEventListener('resize', handleResize)
setMaxHeight(window.innerHeight)
resizeSourceRef.current = 'window'
setReady()

return () => {
window.removeEventListener('resize', handleResize)
cancelAnimationFrame(raf.current)
}
}, [controlledMaxHeight, setReady, resizeSourceRef])
}, [controlledMaxHeight, resizeSourceRef])

return maxHeight
}
35 changes: 30 additions & 5 deletions src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,25 @@
padding: 16px;
}
[data-rsbs-header] {
position: relative;
text-align: center;
user-select: none;
box-shadow: 0 1px 0
rgba(46, 59, 66, calc(var(--rsbs-content-opacity) * 0.125));
z-index: 1;
padding-top: calc(20px + env(safe-area-inset-top));
padding-bottom: 8px;
}
[data-rsbs-header]:after {
position: absolute;
content: '';
left: 0;
right: 0;
bottom: 0;
height: 1px;
pointer-events: none;
background-color: rgba(46, 59, 66, 0.125);
opacity: var(--rsbs-content-opacity);
will-change: opacity;
}
[data-rsbs-header]:before {
position: absolute;
content: '';
Expand All @@ -90,9 +101,11 @@
}
}
[data-rsbs-has-header='false'] [data-rsbs-header] {
box-shadow: none;
padding-top: calc(12px + env(safe-area-inset-top));
}
[data-rsbs-has-header='false'] [data-rsbs-header]:after {
display: none;
}
[data-rsbs-scroll] {
flex-shrink: 1;
flex-grow: 1;
Expand All @@ -116,12 +129,24 @@
overflow: hidden;
}
[data-rsbs-footer] {
box-shadow: 0 -1px 0 rgba(46, 59, 66, calc(var(--rsbs-content-opacity) * 0.125)),
0 2px 0 var(--rsbs-bg);
position: relative;
box-shadow: 0 2px 0 var(--rsbs-bg);
overflow: hidden;
z-index: 1;
padding-bottom: calc(16px + env(safe-area-inset-bottom));
}
[data-rsbs-footer]:before {
position: absolute;
content: '';
left: 0;
right: 0;
top: 0;
height: 1px;
pointer-events: none;
background-color: rgba(46, 59, 66, 0.125);
opacity: var(--rsbs-content-opacity);
will-change: opacity;
}

[data-rsbs-is-dismissable='true'],
[data-rsbs-is-dismissable='false']:is([data-rsbs-state='opening'], [data-rsbs-state='closing']) {
Expand Down