From ec733a5a64ce5324946305bc9616749fea81ddbd Mon Sep 17 00:00:00 2001 From: Nikita <2446589+n1k1tk@users.noreply.github.com> Date: Wed, 23 Jun 2021 03:06:45 +0300 Subject: [PATCH] feat: expand on content drag (#141) --- README.md | 6 ++++ pages/fixtures/scrollable.tsx | 15 +++++++++- src/BottomSheet.tsx | 52 +++++++++++++++++++++++++++++++++-- src/types.ts | 8 +++++- 4 files changed, 77 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0629eeec..84d8eef9 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,12 @@ Type: `boolean` iOS Safari, and some other mobile culprits, can be tricky if you're on a page that has scrolling overflow on `document.body`. Mobile browsers often prefer scrolling the page in these cases instead of letting you handle the touch interaction for UI such as the bottom sheet. Thus it's enabled by default. However it can be a bit agressive and can affect cases where you're putting a drag and drop element inside the bottom sheet. Such as `` and more. For these cases you can wrap them in a container and give them this data attribute `[data-body-scroll-lock-ignore]` to prevent intervention. Really handy if you're doing crazy stuff like putting mapbox-gl widgets inside bottom sheets. +### expandOnContentDrag + +Type: `boolean` + +Disabled by default. By default, a user can expand the bottom sheet only by dragging a header or the overlay. This option enables expanding the bottom sheet on the content dragging. + ## Events All events receive `SpringEvent` as their argument. The payload varies, but `type` is always present, which can be `'OPEN' | 'RESIZE' | 'SNAP' | 'CLOSE'` depending on the scenario. diff --git a/pages/fixtures/scrollable.tsx b/pages/fixtures/scrollable.tsx index 05b7ee50..e3c7fc9c 100644 --- a/pages/fixtures/scrollable.tsx +++ b/pages/fixtures/scrollable.tsx @@ -1,6 +1,6 @@ import cx from 'classnames' import type { NextPage } from 'next' -import { useRef } from 'react' +import { useRef, useState } from 'react' import scrollIntoView from 'smooth-scroll-into-view-if-needed' import Button from '../../docs/fixtures/Button' import CloseExample from '../../docs/fixtures/CloseExample' @@ -55,6 +55,7 @@ const ScrollableFixturePage: NextPage = ({ meta, name, }) => { + const [expandOnContentDrag, setExpandOnContentDrag] = useState(true) const focusRef = useRef() const sheetRef = useRef() @@ -95,6 +96,7 @@ const ScrollableFixturePage: NextPage = ({ maxHeight / 4, maxHeight * 0.6, ]} + expandOnContentDrag={expandOnContentDrag} >
@@ -137,6 +139,17 @@ const ScrollableFixturePage: NextPage = ({ Bottom
+
+ +

The sheet will always try to set initial focus on the first interactive element it finds. diff --git a/src/BottomSheet.tsx b/src/BottomSheet.tsx index b35341d5..83234c94 100644 --- a/src/BottomSheet.tsx +++ b/src/BottomSheet.tsx @@ -68,6 +68,7 @@ export const BottomSheet = React.forwardRef< onSpringCancel, onSpringEnd, reserveScrollBarGap = blocking, + expandOnContentDrag = false, ...props }, forwardRef @@ -102,6 +103,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 preventScrollingRef = useRef(false) const prefersReducedMotion = useReducedMotion() @@ -445,8 +447,40 @@ export const BottomSheet = React.forwardRef< [send] ) + useEffect(() => { + const elem = scrollRef.current + + const preventScrolling = e => { + if (preventScrollingRef.current) { + e.preventDefault() + } + } + + const preventSafariOverscroll = e => { + if (elem.scrollTop < 0) { + requestAnimationFrame(() => { + elem.style.overflow = 'hidden' + elem.scrollTop = 0 + elem.style.removeProperty('overflow') + }) + e.preventDefault() + } + } + + if (expandOnContentDrag) { + elem.addEventListener('scroll', preventScrolling) + elem.addEventListener('touchmove', preventScrolling) + elem.addEventListener('touchstart', preventSafariOverscroll) + } + return () => { + elem.removeEventListener('scroll', preventScrolling) + elem.removeEventListener('touchmove', preventScrolling) + elem.removeEventListener('touchstart', preventSafariOverscroll) + } + }, [expandOnContentDrag, scrollRef]) + const handleDrag = ({ - args: [{ closeOnTap = false } = {}] = [], + args: [{ closeOnTap = false, isContentDragging = false } = {}] = [], cancel, direction: [, direction], down, @@ -520,6 +554,20 @@ export const BottomSheet = React.forwardRef< ) : predictedY + if (expandOnContentDrag && isContentDragging) { + if (newY >= maxSnapRef.current) { + newY = maxSnapRef.current + } + + if (memo === maxSnapRef.current && scrollRef.current.scrollTop > 0) { + newY = maxSnapRef.current + } + + preventScrollingRef.current = newY < maxSnapRef.current; + } else { + preventScrollingRef.current = false + } + if (first) { send('DRAG') } @@ -618,7 +666,7 @@ export const BottomSheet = React.forwardRef< {header} )} -

+
{children}
diff --git a/src/types.ts b/src/types.ts index 242920c9..a572656b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -143,7 +143,13 @@ export type Props = { /** * Open immediatly instead of initially animating from a closed => open state, useful if the bottom sheet is visible by default and the animation would be distracting */ - skipInitialTransition?: boolean + skipInitialTransition?: boolean, + + /** + * Expand the bottom sheet on the content dragging. By default user can expand the bottom sheet only by dragging the header or overlay. This option enables expanding on dragging the content. + * @default expandOnContentDrag === false + */ + expandOnContentDrag?: boolean, } & Omit, 'children'> export interface RefHandles {