Skip to content

Commit ec733a5

Browse files
authored
feat: expand on content drag (stipsan#141)
1 parent 309f95b commit ec733a5

File tree

4 files changed

+77
-4
lines changed

4 files changed

+77
-4
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,12 @@ Type: `boolean`
140140
141141
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 `<input type="range" />` 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.
142142
143+
### expandOnContentDrag
144+
145+
Type: `boolean`
146+
147+
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.
148+
143149
## Events
144150
145151
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.

pages/fixtures/scrollable.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import cx from 'classnames'
22
import type { NextPage } from 'next'
3-
import { useRef } from 'react'
3+
import { useRef, useState } from 'react'
44
import scrollIntoView from 'smooth-scroll-into-view-if-needed'
55
import Button from '../../docs/fixtures/Button'
66
import CloseExample from '../../docs/fixtures/CloseExample'
@@ -55,6 +55,7 @@ const ScrollableFixturePage: NextPage<GetStaticProps> = ({
5555
meta,
5656
name,
5757
}) => {
58+
const [expandOnContentDrag, setExpandOnContentDrag] = useState(true)
5859
const focusRef = useRef<HTMLButtonElement>()
5960
const sheetRef = useRef<BottomSheetRef>()
6061

@@ -95,6 +96,7 @@ const ScrollableFixturePage: NextPage<GetStaticProps> = ({
9596
maxHeight / 4,
9697
maxHeight * 0.6,
9798
]}
99+
expandOnContentDrag={expandOnContentDrag}
98100
>
99101
<SheetContent>
100102
<div className="grid grid-cols-3 w-full gap-4">
@@ -137,6 +139,17 @@ const ScrollableFixturePage: NextPage<GetStaticProps> = ({
137139
Bottom
138140
</Button>
139141
</div>
142+
<div className="grid w-full">
143+
<Button
144+
className={[
145+
' text-sm px-2 py-1',
146+
{ 'text-xl': false, 'px-7': false, 'py-3': false },
147+
]}
148+
onClick={() => setExpandOnContentDrag(!expandOnContentDrag)}
149+
>
150+
{expandOnContentDrag ? 'Disable' : 'Enable'} expand on content drag
151+
</Button>
152+
</div>
140153
<p>
141154
The sheet will always try to set initial focus on the first
142155
interactive element it finds.

src/BottomSheet.tsx

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export const BottomSheet = React.forwardRef<
6868
onSpringCancel,
6969
onSpringEnd,
7070
reserveScrollBarGap = blocking,
71+
expandOnContentDrag = false,
7172
...props
7273
},
7374
forwardRef
@@ -102,6 +103,7 @@ export const BottomSheet = React.forwardRef<
102103
// Keeps track of the current height, or the height transitioning to
103104
const heightRef = useRef(0)
104105
const resizeSourceRef = useRef<ResizeSource>()
106+
const preventScrollingRef = useRef(false)
105107

106108
const prefersReducedMotion = useReducedMotion()
107109

@@ -445,8 +447,40 @@ export const BottomSheet = React.forwardRef<
445447
[send]
446448
)
447449

450+
useEffect(() => {
451+
const elem = scrollRef.current
452+
453+
const preventScrolling = e => {
454+
if (preventScrollingRef.current) {
455+
e.preventDefault()
456+
}
457+
}
458+
459+
const preventSafariOverscroll = e => {
460+
if (elem.scrollTop < 0) {
461+
requestAnimationFrame(() => {
462+
elem.style.overflow = 'hidden'
463+
elem.scrollTop = 0
464+
elem.style.removeProperty('overflow')
465+
})
466+
e.preventDefault()
467+
}
468+
}
469+
470+
if (expandOnContentDrag) {
471+
elem.addEventListener('scroll', preventScrolling)
472+
elem.addEventListener('touchmove', preventScrolling)
473+
elem.addEventListener('touchstart', preventSafariOverscroll)
474+
}
475+
return () => {
476+
elem.removeEventListener('scroll', preventScrolling)
477+
elem.removeEventListener('touchmove', preventScrolling)
478+
elem.removeEventListener('touchstart', preventSafariOverscroll)
479+
}
480+
}, [expandOnContentDrag, scrollRef])
481+
448482
const handleDrag = ({
449-
args: [{ closeOnTap = false } = {}] = [],
483+
args: [{ closeOnTap = false, isContentDragging = false } = {}] = [],
450484
cancel,
451485
direction: [, direction],
452486
down,
@@ -520,6 +554,20 @@ export const BottomSheet = React.forwardRef<
520554
)
521555
: predictedY
522556

557+
if (expandOnContentDrag && isContentDragging) {
558+
if (newY >= maxSnapRef.current) {
559+
newY = maxSnapRef.current
560+
}
561+
562+
if (memo === maxSnapRef.current && scrollRef.current.scrollTop > 0) {
563+
newY = maxSnapRef.current
564+
}
565+
566+
preventScrollingRef.current = newY < maxSnapRef.current;
567+
} else {
568+
preventScrollingRef.current = false
569+
}
570+
523571
if (first) {
524572
send('DRAG')
525573
}
@@ -618,7 +666,7 @@ export const BottomSheet = React.forwardRef<
618666
{header}
619667
</div>
620668
)}
621-
<div key="scroll" data-rsbs-scroll ref={scrollRef}>
669+
<div key="scroll" data-rsbs-scroll ref={scrollRef} {...(expandOnContentDrag ? bind({ isContentDragging: true }) : {})}>
622670
<div data-rsbs-content ref={contentRef}>
623671
{children}
624672
</div>

src/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,13 @@ export type Props = {
143143
/**
144144
* 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
145145
*/
146-
skipInitialTransition?: boolean
146+
skipInitialTransition?: boolean,
147+
148+
/**
149+
* 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.
150+
* @default expandOnContentDrag === false
151+
*/
152+
expandOnContentDrag?: boolean,
147153
} & Omit<React.PropsWithoutRef<JSX.IntrinsicElements['div']>, 'children'>
148154

149155
export interface RefHandles {

0 commit comments

Comments
 (0)