From 383e2062594de202ae77c25ac2ed9ae1137bcb2b Mon Sep 17 00:00:00 2001 From: Cody Olsen Date: Thu, 21 Jan 2021 22:37:10 +0100 Subject: [PATCH] feat: add source to RESIZE events Fixes #53 --- README.md | 5 ++++- src/BottomSheet.tsx | 27 +++++++++++++++++++++++---- src/hooks/useSnapPoints.tsx | 35 ++++++++++++++++++++++++++++++----- src/types.ts | 7 ++++++- 4 files changed, 63 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index c0a6d385..10b3caa9 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,10 @@ If the user reopens the sheet before it's done animating it'll trigger this even #### RESIZE -Fires whenever there's been a window resize event, or if the header, footer or content have changed its height in such a way that the valid snap points have changed. In the future (#53) you'll be able to differentiate between what triggered the resize. +Type: `{ source: 'window' | 'maxheightprop' | 'element }` + +Fires whenever there's been a window resize event, or if the header, footer or content have changed its height in such a way that the valid snap points have changed. +`source` tells you what caused the resize. If the resize comes from a `window.onresize` event it's set to `'window'`. `'maxheightprop'` is if the `maxHeight` prop is used, and is fired whenever it changes. And `'element'` is whenever the header, footer or content resize observers detect a change. #### SNAP diff --git a/src/BottomSheet.tsx b/src/BottomSheet.tsx index 74655ce8..07a8d2f5 100644 --- a/src/BottomSheet.tsx +++ b/src/BottomSheet.tsx @@ -29,6 +29,7 @@ import type { defaultSnapProps, Props, RefHandles, + ResizeSource, SnapPointProps, } from './types' import { debugging } from './utils' @@ -99,6 +100,7 @@ export const BottomSheet = React.forwardRef< // Keeps track of the current height, or the height transitioning to const heightRef = useRef(0) + const resizeSourceRef = useRef() const prefersReducedMotion = useReducedMotion() @@ -131,6 +133,7 @@ export const BottomSheet = React.forwardRef< lastSnapRef, ready, registerReady, + resizeSourceRef, }) // Setup refs that are used in cases where full control is needed over when a side effect is executed @@ -196,7 +199,11 @@ export const BottomSheet = React.forwardRef< [] ), onResizeCancel: useCallback( - () => onSpringCancelRef.current?.({ type: 'RESIZE' }), + () => + onSpringCancelRef.current?.({ + type: 'RESIZE', + source: resizeSourceRef.current, + }), [] ), onOpenEnd: useCallback( @@ -212,7 +219,11 @@ export const BottomSheet = React.forwardRef< [] ), onResizeEnd: useCallback( - () => onSpringEndRef.current?.({ type: 'RESIZE' }), + () => + onSpringEndRef.current?.({ + type: 'RESIZE', + source: resizeSourceRef.current, + }), [] ), }, @@ -235,7 +246,11 @@ export const BottomSheet = React.forwardRef< [] ), onResizeStart: useCallback( - async () => onSpringStartRef.current?.({ type: 'RESIZE' }), + async () => + onSpringStartRef.current?.({ + type: 'RESIZE', + source: resizeSourceRef.current, + }), [] ), onSnapEnd: useCallback( @@ -255,7 +270,11 @@ export const BottomSheet = React.forwardRef< [] ), onResizeEnd: useCallback( - async () => onSpringEndRef.current?.({ type: 'RESIZE' }), + async () => + onSpringEndRef.current?.({ + type: 'RESIZE', + source: resizeSourceRef.current, + }), [] ), renderVisuallyHidden: useCallback( diff --git a/src/hooks/useSnapPoints.tsx b/src/hooks/useSnapPoints.tsx index cac45767..8277f5fe 100644 --- a/src/hooks/useSnapPoints.tsx +++ b/src/hooks/useSnapPoints.tsx @@ -7,7 +7,7 @@ import React, { useState, } from 'react' import { ResizeObserver, ResizeObserverEntry } from '@juggle/resize-observer' -import type { defaultSnapProps, snapPoints } from '../types' +import type { defaultSnapProps, ResizeSource, snapPoints } from '../types' import { processSnapPoints, roundAndCheckForNaN } from '../utils' import { useReady } from './useReady' import { ResizeObserverOptions } from '@juggle/resize-observer/lib/ResizeObserverOptions' @@ -24,6 +24,7 @@ export function useSnapPoints({ lastSnapRef, ready, registerReady, + resizeSourceRef, }: { contentRef: React.RefObject controlledMaxHeight?: number @@ -36,6 +37,7 @@ export function useSnapPoints({ lastSnapRef: React.RefObject ready: boolean registerReady: ReturnType['registerReady'] + resizeSourceRef: React.MutableRefObject }) { const { maxHeight, minHeight, headerHeight, footerHeight } = useDimensions({ contentRef: contentRef, @@ -45,6 +47,7 @@ export function useSnapPoints({ headerEnabled, headerRef, registerReady, + resizeSourceRef, }) const { snapPoints, minSnap, maxSnap } = processSnapPoints( @@ -100,6 +103,7 @@ function useDimensions({ headerEnabled, headerRef, registerReady, + resizeSourceRef, }: { contentRef: React.RefObject controlledMaxHeight?: number @@ -108,24 +112,32 @@ function useDimensions({ headerEnabled: boolean headerRef: React.RefObject registerReady: ReturnType['registerReady'] + resizeSourceRef: React.MutableRefObject }) { const setReady = useMemo(() => registerReady('contentHeight'), [ registerReady, ]) - const maxHeight = useMaxHeight(controlledMaxHeight, registerReady) + const maxHeight = useMaxHeight( + controlledMaxHeight, + registerReady, + resizeSourceRef + ) // @TODO probably better to forward props instead of checking refs to decide if it's enabled const headerHeight = useElementSizeObserver(headerRef, { label: 'headerHeight', enabled: headerEnabled, + resizeSourceRef, }) const contentHeight = useElementSizeObserver(contentRef, { label: 'contentHeight', enabled: true, + resizeSourceRef, }) const footerHeight = useElementSizeObserver(footerRef, { label: 'footerHeight', enabled: footerEnabled, + resizeSourceRef, }) const minHeight = Math.min(maxHeight - headerHeight - footerHeight, contentHeight) + @@ -161,7 +173,15 @@ const observerOptions: ResizeObserverOptions = { */ function useElementSizeObserver( ref: React.RefObject, - { label, enabled }: { label: string; enabled: boolean } + { + label, + enabled, + resizeSourceRef, + }: { + label: string + enabled: boolean + resizeSourceRef: React.MutableRefObject + } ): number { let [size, setSize] = useState(0) @@ -170,6 +190,7 @@ function useElementSizeObserver( const handleResize = useCallback((entries: ResizeObserverEntry[]) => { // we only observe one element, so accessing the first entry here is fine setSize(entries[0].borderBoxSize[0].blockSize) + resizeSourceRef.current = 'element' }, []) useEffect(() => { @@ -191,7 +212,8 @@ function useElementSizeObserver( // Blazingly keep track of the current viewport height without blocking the thread, keeping that sweet 60fps on smartphones function useMaxHeight( controlledMaxHeight, - registerReady: ReturnType['registerReady'] + registerReady: ReturnType['registerReady'], + resizeSourceRef: React.MutableRefObject ) { const setReady = useMemo(() => registerReady('maxHeight'), [registerReady]) const [maxHeight, setMaxHeight] = useState(() => @@ -214,6 +236,7 @@ function useMaxHeight( // Bail if the max height is a controlled prop if (controlledMaxHeight) { setMaxHeight(roundAndCheckForNaN(controlledMaxHeight)) + resizeSourceRef.current = 'maxheightprop' return } @@ -227,19 +250,21 @@ function useMaxHeight( // throttle state changes using rAF raf.current = requestAnimationFrame(() => { setMaxHeight(window.innerHeight) + resizeSourceRef.current = 'window' raf.current = 0 }) } window.addEventListener('resize', handleResize) setMaxHeight(window.innerHeight) + resizeSourceRef.current = 'window' setReady() return () => { window.removeEventListener('resize', handleResize) cancelAnimationFrame(raf.current) } - }, [controlledMaxHeight, setReady]) + }, [controlledMaxHeight, setReady, resizeSourceRef]) return maxHeight } diff --git a/src/types.ts b/src/types.ts index 870bb910..25a8d950 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,6 +23,11 @@ export type SnapPointProps = { export type snapPoints = (props: SnapPointProps) => number[] | number +/** + * `window` comes from window.onresize, maxheightprop is if the `maxHeight` prop is used, and `element` comes from the resize observers that listens to header, footer and the content area + */ +export type ResizeSource = 'window' | 'maxheightprop' | 'element' + export type defaultSnapProps = { /** The snap points currently in use, this can be controlled by providing a `snapPoints` function on the bottom sheet. */ snapPoints: number[] @@ -34,7 +39,7 @@ export type defaultSnapProps = { export type SpringEvent = | { type: 'OPEN' } | { type: 'CLOSE' } - | { type: 'RESIZE' } + | { type: 'RESIZE'; source: ResizeSource } | { type: 'SNAP'; source: 'dragging' | 'custom' | string } export type Props = {