react-modal-sheet 5.2.0 → 5.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -4
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +71 -56
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +72 -57
- package/dist/index.mjs.map +1 -1
- package/package.json +14 -14
- package/src/SheetContent.tsx +10 -1
- package/src/debug.ts +37 -0
- package/src/hooks/use-scroll-position.ts +63 -49
- package/src/types.tsx +1 -1
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ npm install motion
|
|
|
37
37
|
<details>
|
|
38
38
|
<summary><strong>📚 Table of contents</strong></summary>
|
|
39
39
|
|
|
40
|
-
- [What's new in v5](
|
|
40
|
+
- [What's new in v5](#-whats-new-in-v5)
|
|
41
41
|
- [Usage](#-usage)
|
|
42
42
|
- [Props](#%EF%B8%8F-props)
|
|
43
43
|
- [Methods and properties](#%EF%B8%8F-methods-and-properties)
|
|
@@ -57,6 +57,8 @@ Version 5 introduces several major improvements and breaking changes:
|
|
|
57
57
|
### 🔄 Breaking Changes
|
|
58
58
|
|
|
59
59
|
- **Removed `Sheet.Scroller`**: Scrolling is now handled automatically by `Sheet.Content`
|
|
60
|
+
- See [Scrolling behavior](#-scrolling-behavior) section for details
|
|
61
|
+
- See [Making scrollable sheet content always reachable](#making-scrollable-sheet-content-always-reachable) for guidance on how to implement auto padding behavior
|
|
60
62
|
- **Snap point order reversed**: Snap points now use ascending order (e.g., `[0, 0.5, 1]` instead of `[1, 0.5, 0]`)
|
|
61
63
|
- This aligns better with other bottom sheet libraries and makes more intuitive sense
|
|
62
64
|
- **Detent prop values changed**:
|
|
@@ -199,12 +201,18 @@ Below you can see an example of how to use these values:
|
|
|
199
201
|
|
|
200
202
|
```tsx
|
|
201
203
|
import { Sheet, SheetRef } from 'react-modal-sheet';
|
|
204
|
+
import { useTransform } from 'motion/react';
|
|
202
205
|
import { useState, useRef } from 'react';
|
|
203
206
|
|
|
204
207
|
function RefExample() {
|
|
205
208
|
const [isOpen, setOpen] = useState(false);
|
|
206
209
|
const ref = useRef<SheetRef>(null);
|
|
207
210
|
|
|
211
|
+
const background = useTransform(() => {
|
|
212
|
+
const y = ref.current?.y.get() ?? 0;
|
|
213
|
+
return y > 100 ? 'lightblue' : 'lightcoral';
|
|
214
|
+
});
|
|
215
|
+
|
|
208
216
|
function doSomething() {
|
|
209
217
|
console.log('> Current y value:', ref.current?.y.get());
|
|
210
218
|
console.log('> Current yInverted value:', ref.current?.yInverted.get());
|
|
@@ -218,7 +226,7 @@ function RefExample() {
|
|
|
218
226
|
<Sheet ref={ref} isOpen={isOpen} onClose={() => setOpen(false)}>
|
|
219
227
|
<Sheet.Container>
|
|
220
228
|
<Sheet.Content>
|
|
221
|
-
<motion.div style={{
|
|
229
|
+
<motion.div style={{ background }}>
|
|
222
230
|
Use animated y value in some way
|
|
223
231
|
</motion.div>
|
|
224
232
|
{/* Your content here */}
|
|
@@ -230,6 +238,61 @@ function RefExample() {
|
|
|
230
238
|
}
|
|
231
239
|
```
|
|
232
240
|
|
|
241
|
+
#### Making scrollable sheet content always reachable
|
|
242
|
+
|
|
243
|
+
One common use case for the `y` value is to apply padding bottom to the sheet content
|
|
244
|
+
when the sheet is not fully open. This ensures that the content is always reachable even when the sheet is partially open, eg. when using snap points.
|
|
245
|
+
|
|
246
|
+
> [!NOTE]
|
|
247
|
+
> Prior to v5 this was a built-in behavior of `Sheet.Scroller` via `autoPadding` prop but since `Sheet.Scroller` has been removed in v5 in favor of `Sheet.Content` handling scrolling internally, you can now implement this behavior yourself with the help of the `y` motion value.
|
|
248
|
+
>
|
|
249
|
+
> Also take a look at the [Creating custom scrollers](#creating-custom-scrollers) section for more advanced use cases when you need to implement your own scroller and potentially apply this padding behavior to that.
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
import { Sheet, SheetRef } from 'react-modal-sheet';
|
|
253
|
+
import { useTransform } from 'motion/react';
|
|
254
|
+
import { useState, useRef } from 'react';
|
|
255
|
+
|
|
256
|
+
const snapPoints = [0, 0.5, 1];
|
|
257
|
+
|
|
258
|
+
function PaddingExample() {
|
|
259
|
+
const [isOpen, setOpen] = useState(false);
|
|
260
|
+
const ref = useRef<SheetRef>(null);
|
|
261
|
+
|
|
262
|
+
// Add padding bottom based on how far the sheet is from being fully open
|
|
263
|
+
const paddingBottom = useTransform(() => {
|
|
264
|
+
return ref.current?.y.get() ?? 0;
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<>
|
|
269
|
+
<button onClick={() => setOpen(true)}>Open sheet</button>
|
|
270
|
+
|
|
271
|
+
<Sheet
|
|
272
|
+
ref={ref}
|
|
273
|
+
isOpen={isOpen}
|
|
274
|
+
onClose={() => setOpen(false)}
|
|
275
|
+
snapPoints={snapPoints}
|
|
276
|
+
initialSnap={1}
|
|
277
|
+
>
|
|
278
|
+
<Sheet.Container>
|
|
279
|
+
<Sheet.Header />
|
|
280
|
+
<Sheet.Content
|
|
281
|
+
// 👇 Apply styles to the inner scroller element
|
|
282
|
+
scrollStyle={{ paddingBottom }}
|
|
283
|
+
// 💡 Recommendation: disable drag for the content so that it doesn't interfere with scrolling
|
|
284
|
+
disableDrag
|
|
285
|
+
>
|
|
286
|
+
This content will always be reachable even when the sheet is not
|
|
287
|
+
fully open.
|
|
288
|
+
</Sheet.Content>
|
|
289
|
+
</Sheet.Container>
|
|
290
|
+
</Sheet>
|
|
291
|
+
</>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
233
296
|
### Detents
|
|
234
297
|
|
|
235
298
|
By default the sheet will take the full height of the page minus top padding and safe area inset. The `detent` prop controls the sheet's height behavior:
|
|
@@ -369,9 +432,42 @@ The keyboard avoidance feature:
|
|
|
369
432
|
|
|
370
433
|
If you need to disable keyboard avoidance, set `avoidKeyboard={false}`.
|
|
371
434
|
|
|
372
|
-
> [!
|
|
435
|
+
> [!IMPORTANT]
|
|
373
436
|
> If your sheet content is very long and some inputs are below the keyboard even after adding the padding, you might need to scroll the content into view manually when an input is focused.
|
|
374
437
|
|
|
438
|
+
#### Managing virtual keyboard manually
|
|
439
|
+
|
|
440
|
+
If the built-in keyboard avoidance doesn't work for your use case, you can disable it with `avoidKeyboard={false}` and manage the keyboard state manually with the help of the `useVirtualKeyboard` hook.
|
|
441
|
+
|
|
442
|
+
```tsx
|
|
443
|
+
import { Sheet, useVirtualKeyboard } from 'react-modal-sheet';
|
|
444
|
+
|
|
445
|
+
function ManualKeyboardExample() {
|
|
446
|
+
const [isOpen, setOpen] = useState(false);
|
|
447
|
+
|
|
448
|
+
// This hook manages the `--keyboard-inset-height` CSS variable for you
|
|
449
|
+
useVirtualKeyboard({ isEnabled: isOpen });
|
|
450
|
+
|
|
451
|
+
return (
|
|
452
|
+
// Disable default keyboard avoidance
|
|
453
|
+
<Sheet avoidKeyboard={false} isOpen={isOpen} onClose={() => setOpen(false)}>
|
|
454
|
+
<Sheet.Container>
|
|
455
|
+
<Sheet.Header />
|
|
456
|
+
<Sheet.Content
|
|
457
|
+
// Apply keyboard height as padding bottom to keep content above the keyboard
|
|
458
|
+
style={{ paddingBottom: 'var(--keyboard-inset-height)' }}
|
|
459
|
+
>
|
|
460
|
+
{/* Your content here */}
|
|
461
|
+
</Sheet.Content>
|
|
462
|
+
</Sheet.Container>
|
|
463
|
+
<Sheet.Backdrop />
|
|
464
|
+
</Sheet>
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
For a more detailed example of using `useVirtualKeyboard` see the [Creating custom scrollers](#creating-custom-scrollers) section below.
|
|
470
|
+
|
|
375
471
|
### 📱 Scrolling behavior
|
|
376
472
|
|
|
377
473
|
The `Sheet.Content` component manages scroll interactions internally and provides several ways to control scrolling:
|
|
@@ -495,7 +591,7 @@ function CustomScrollerExample() {
|
|
|
495
591
|
style={{
|
|
496
592
|
// Use CSS variable managed by `useVirtualKeyboard`
|
|
497
593
|
paddingBottom: 'var(--keyboard-inset-height)',
|
|
498
|
-
// Alternatively
|
|
594
|
+
// Alternatively apply keyboard padding via pixel value (maybe you want to add some extra padding to it)
|
|
499
595
|
paddingBottom: keyboardHeight,
|
|
500
596
|
// Some layout styles for custom content
|
|
501
597
|
height: '100%',
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { EasingDefinition, motion, MotionValue } from 'motion/react';
|
|
2
|
-
import { ForwardRefExoticComponent, ReactNode, ComponentPropsWithoutRef, RefAttributes, FunctionComponent, HTMLAttributes, RefObject
|
|
1
|
+
import { EasingDefinition, motion, MotionProps, MotionValue } from 'motion/react';
|
|
2
|
+
import { ForwardRefExoticComponent, ReactNode, ComponentPropsWithoutRef, RefAttributes, FunctionComponent, HTMLAttributes, RefObject } from 'react';
|
|
3
3
|
|
|
4
4
|
type SheetDetent = 'default' | 'full' | 'content';
|
|
5
5
|
type CommonProps = {
|
|
@@ -48,7 +48,7 @@ type SheetContentProps = MotionCommonProps & CommonProps & {
|
|
|
48
48
|
disableScroll?: boolean | ((args: SheetStateInfo) => boolean);
|
|
49
49
|
scrollRef?: RefObject<HTMLDivElement | null>;
|
|
50
50
|
scrollClassName?: string;
|
|
51
|
-
scrollStyle?:
|
|
51
|
+
scrollStyle?: MotionProps['style'];
|
|
52
52
|
};
|
|
53
53
|
type SheetBackdropProps = MotionCommonProps & CommonProps;
|
|
54
54
|
type SheetDragIndicatorProps = HTMLAttributes<HTMLDivElement> & CommonProps;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { EasingDefinition, motion, MotionValue } from 'motion/react';
|
|
2
|
-
import { ForwardRefExoticComponent, ReactNode, ComponentPropsWithoutRef, RefAttributes, FunctionComponent, HTMLAttributes, RefObject
|
|
1
|
+
import { EasingDefinition, motion, MotionProps, MotionValue } from 'motion/react';
|
|
2
|
+
import { ForwardRefExoticComponent, ReactNode, ComponentPropsWithoutRef, RefAttributes, FunctionComponent, HTMLAttributes, RefObject } from 'react';
|
|
3
3
|
|
|
4
4
|
type SheetDetent = 'default' | 'full' | 'content';
|
|
5
5
|
type CommonProps = {
|
|
@@ -48,7 +48,7 @@ type SheetContentProps = MotionCommonProps & CommonProps & {
|
|
|
48
48
|
disableScroll?: boolean | ((args: SheetStateInfo) => boolean);
|
|
49
49
|
scrollRef?: RefObject<HTMLDivElement | null>;
|
|
50
50
|
scrollClassName?: string;
|
|
51
|
-
scrollStyle?:
|
|
51
|
+
scrollStyle?: MotionProps['style'];
|
|
52
52
|
};
|
|
53
53
|
type SheetBackdropProps = MotionCommonProps & CommonProps;
|
|
54
54
|
type SheetDragIndicatorProps = HTMLAttributes<HTMLDivElement> & CommonProps;
|
package/dist/index.js
CHANGED
|
@@ -277,63 +277,88 @@ function useDragConstraints() {
|
|
|
277
277
|
const onMeasure = React4.useCallback(() => constraints, []);
|
|
278
278
|
return { ref, onMeasure };
|
|
279
279
|
}
|
|
280
|
+
var useIsomorphicLayoutEffect = IS_SSR ? React4.useEffect : React4.useLayoutEffect;
|
|
281
|
+
|
|
282
|
+
// src/hooks/use-stable-callback.ts
|
|
283
|
+
function useStableCallback(handler) {
|
|
284
|
+
const handlerRef = React4.useRef(void 0);
|
|
285
|
+
useIsomorphicLayoutEffect(() => {
|
|
286
|
+
handlerRef.current = handler;
|
|
287
|
+
});
|
|
288
|
+
return React4.useCallback((...args) => {
|
|
289
|
+
const fn = handlerRef.current;
|
|
290
|
+
return fn == null ? void 0 : fn(...args);
|
|
291
|
+
}, []);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/hooks/use-scroll-position.ts
|
|
280
295
|
function useScrollPosition(options = {}) {
|
|
281
296
|
const { debounceDelay = 32, isEnabled = true } = options;
|
|
297
|
+
const scrollTimeoutRef = React4.useRef(null);
|
|
282
298
|
const [element, setElement] = React4.useState(null);
|
|
283
299
|
const [scrollPosition, setScrollPosition] = React4.useState(void 0);
|
|
284
|
-
React4.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
let position;
|
|
297
|
-
if (isAtTop) {
|
|
298
|
-
position = "top";
|
|
299
|
-
} else if (isAtBottom) {
|
|
300
|
-
position = "bottom";
|
|
301
|
-
} else {
|
|
302
|
-
position = "middle";
|
|
303
|
-
}
|
|
304
|
-
if (position === scrollPosition) return;
|
|
305
|
-
setScrollPosition(position);
|
|
300
|
+
const scrollRef = React4.useMemo(
|
|
301
|
+
() => (element2) => {
|
|
302
|
+
setElement(element2);
|
|
303
|
+
},
|
|
304
|
+
[]
|
|
305
|
+
);
|
|
306
|
+
const determineScrollPosition = useStableCallback((element2) => {
|
|
307
|
+
const { scrollTop, scrollHeight, clientHeight } = element2;
|
|
308
|
+
const isScrollable2 = scrollHeight > clientHeight;
|
|
309
|
+
if (!isScrollable2) {
|
|
310
|
+
if (scrollPosition) setScrollPosition(void 0);
|
|
311
|
+
return;
|
|
306
312
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
debounceDelay
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
313
|
+
const isAtTop = scrollTop <= 0;
|
|
314
|
+
const isAtBottom = Math.ceil(scrollHeight) - Math.ceil(scrollTop) === Math.ceil(clientHeight);
|
|
315
|
+
let position;
|
|
316
|
+
if (isAtTop) {
|
|
317
|
+
position = "top";
|
|
318
|
+
} else if (isAtBottom) {
|
|
319
|
+
position = "bottom";
|
|
320
|
+
} else {
|
|
321
|
+
position = "middle";
|
|
320
322
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
323
|
+
element2.style.touchAction = isAtTop ? "pan-down" : "";
|
|
324
|
+
if (position === scrollPosition) return;
|
|
325
|
+
setScrollPosition(position);
|
|
326
|
+
});
|
|
327
|
+
const onScroll = useStableCallback((event) => {
|
|
328
|
+
if (event.currentTarget instanceof HTMLElement) {
|
|
329
|
+
const el = event.currentTarget;
|
|
330
|
+
if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);
|
|
331
|
+
if (debounceDelay === 0) {
|
|
332
|
+
determineScrollPosition(el);
|
|
333
|
+
} else {
|
|
334
|
+
scrollTimeoutRef.current = setTimeout(
|
|
335
|
+
() => determineScrollPosition(el),
|
|
336
|
+
debounceDelay
|
|
337
|
+
);
|
|
324
338
|
}
|
|
325
339
|
}
|
|
340
|
+
});
|
|
341
|
+
const onTouchStart = useStableCallback((event) => {
|
|
342
|
+
if (event.currentTarget instanceof HTMLElement) {
|
|
343
|
+
const element2 = event.currentTarget;
|
|
344
|
+
requestAnimationFrame(() => {
|
|
345
|
+
determineScrollPosition(element2);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
React4.useEffect(() => {
|
|
350
|
+
if (!element || !isEnabled) return;
|
|
326
351
|
determineScrollPosition(element);
|
|
327
352
|
element.addEventListener("scroll", onScroll);
|
|
328
353
|
element.addEventListener("touchstart", onTouchStart);
|
|
329
354
|
return () => {
|
|
330
|
-
if (
|
|
355
|
+
if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);
|
|
331
356
|
element.removeEventListener("scroll", onScroll);
|
|
332
357
|
element.removeEventListener("touchstart", onTouchStart);
|
|
333
358
|
};
|
|
334
359
|
}, [element, isEnabled]);
|
|
335
360
|
return {
|
|
336
|
-
scrollRef
|
|
361
|
+
scrollRef,
|
|
337
362
|
scrollPosition
|
|
338
363
|
};
|
|
339
364
|
}
|
|
@@ -354,7 +379,9 @@ var SheetContent = React4.forwardRef(
|
|
|
354
379
|
}, ref) => {
|
|
355
380
|
const sheetContext = useSheetContext();
|
|
356
381
|
const dragConstraints = useDragConstraints();
|
|
357
|
-
const scroll = useScrollPosition(
|
|
382
|
+
const scroll = useScrollPosition({
|
|
383
|
+
isEnabled: typeof disableScrollProp === "function" ? true : !disableScrollProp
|
|
384
|
+
});
|
|
358
385
|
const disableScroll = typeof disableScrollProp === "function" ? disableScrollProp({
|
|
359
386
|
scrollPosition: scroll.scrollPosition,
|
|
360
387
|
currentSnap: sheetContext.currentSnap
|
|
@@ -378,6 +405,9 @@ var SheetContent = React4.forwardRef(
|
|
|
378
405
|
if (disableScroll) {
|
|
379
406
|
scrollStyle.overflowY = "hidden";
|
|
380
407
|
}
|
|
408
|
+
if (dragProps !== void 0) {
|
|
409
|
+
scrollStyle.touchAction = "pan-down";
|
|
410
|
+
}
|
|
381
411
|
return /* @__PURE__ */ React4__default.default.createElement(
|
|
382
412
|
react.motion.div,
|
|
383
413
|
{
|
|
@@ -473,9 +503,6 @@ var SheetHeader = React4.forwardRef(
|
|
|
473
503
|
}
|
|
474
504
|
);
|
|
475
505
|
SheetHeader.displayName = "SheetHeader";
|
|
476
|
-
var useIsomorphicLayoutEffect = IS_SSR ? React4.useEffect : React4.useLayoutEffect;
|
|
477
|
-
|
|
478
|
-
// src/hooks/use-dimensions.ts
|
|
479
506
|
function useDimensions() {
|
|
480
507
|
const [dimensions, setDimensions] = React4.useState(() => ({
|
|
481
508
|
windowHeight: !IS_SSR ? window.innerHeight : 0,
|
|
@@ -836,18 +863,6 @@ function scrollIntoView(target) {
|
|
|
836
863
|
function willOpenKeyboard(target) {
|
|
837
864
|
return target instanceof HTMLInputElement && !nonTextInputTypes.has(target.type) || target instanceof HTMLTextAreaElement || target instanceof HTMLElement && target.isContentEditable;
|
|
838
865
|
}
|
|
839
|
-
function useStableCallback(handler) {
|
|
840
|
-
const handlerRef = React4.useRef(void 0);
|
|
841
|
-
useIsomorphicLayoutEffect(() => {
|
|
842
|
-
handlerRef.current = handler;
|
|
843
|
-
});
|
|
844
|
-
return React4.useCallback((...args) => {
|
|
845
|
-
const fn = handlerRef.current;
|
|
846
|
-
return fn == null ? void 0 : fn(...args);
|
|
847
|
-
}, []);
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
// src/hooks/use-sheet-state.ts
|
|
851
866
|
function useSheetState({
|
|
852
867
|
isOpen,
|
|
853
868
|
onClosed: _onClosed,
|