react-modal-sheet 4.4.0 → 5.1.0
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 +332 -188
- package/dist/index.d.mts +51 -39
- package/dist/index.d.ts +51 -39
- package/dist/index.js +854 -585
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +847 -579
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -3
- package/src/SheetBackdrop.tsx +19 -6
- package/src/SheetContainer.tsx +34 -25
- package/src/SheetContent.tsx +77 -14
- package/src/SheetDragIndicator.tsx +52 -0
- package/src/SheetHeader.tsx +30 -30
- package/src/constants.ts +5 -3
- package/src/context.tsx +2 -42
- package/src/hooks/use-dimensions.ts +4 -4
- package/src/hooks/use-drag-constraints.ts +3 -3
- package/src/hooks/use-modal-effect.ts +19 -23
- package/src/hooks/use-resize-observer.ts +31 -0
- package/src/hooks/use-scroll-position.ts +72 -0
- package/src/hooks/use-sheet-state.ts +63 -0
- package/src/hooks/use-virtual-keyboard.ts +153 -0
- package/src/index.tsx +14 -10
- package/src/sheet.tsx +247 -209
- package/src/snap.ts +226 -0
- package/src/styles.ts +94 -53
- package/src/types.tsx +83 -79
- package/src/utils.ts +8 -149
- package/src/SheetScroller.tsx +0 -96
- package/src/hooks/use-event-callbacks.ts +0 -44
package/src/SheetContent.tsx
CHANGED
|
@@ -1,34 +1,97 @@
|
|
|
1
|
+
import { type MotionStyle, motion } from 'motion/react';
|
|
1
2
|
import React, { forwardRef } from 'react';
|
|
2
|
-
import { motion } from 'motion/react';
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import { useSheetScrollerContext, useSheetContext } from './context';
|
|
4
|
+
import { useSheetContext } from './context';
|
|
6
5
|
import { useDragConstraints } from './hooks/use-drag-constraints';
|
|
7
|
-
import {
|
|
6
|
+
import { useScrollPosition } from './hooks/use-scroll-position';
|
|
8
7
|
import { styles } from './styles';
|
|
8
|
+
import { type SheetContentProps } from './types';
|
|
9
|
+
import { applyStyles, mergeRefs } from './utils';
|
|
9
10
|
|
|
10
|
-
export const SheetContent = forwardRef<any,
|
|
11
|
-
(
|
|
11
|
+
export const SheetContent = forwardRef<any, SheetContentProps>(
|
|
12
|
+
(
|
|
13
|
+
{
|
|
14
|
+
disableScroll: disableScrollProp,
|
|
15
|
+
disableDrag: disableDragProp,
|
|
16
|
+
children,
|
|
17
|
+
style: styleProp,
|
|
18
|
+
className = '',
|
|
19
|
+
scrollRef: scrollRefProp = null,
|
|
20
|
+
unstyled,
|
|
21
|
+
...rest
|
|
22
|
+
},
|
|
23
|
+
ref
|
|
24
|
+
) => {
|
|
12
25
|
const sheetContext = useSheetContext();
|
|
13
|
-
const
|
|
14
|
-
const
|
|
26
|
+
const dragConstraints = useDragConstraints();
|
|
27
|
+
const scroll = useScrollPosition();
|
|
28
|
+
|
|
29
|
+
const disableScroll =
|
|
30
|
+
typeof disableScrollProp === 'function'
|
|
31
|
+
? disableScrollProp({
|
|
32
|
+
scrollPosition: scroll.scrollPosition,
|
|
33
|
+
currentSnap: sheetContext.currentSnap,
|
|
34
|
+
})
|
|
35
|
+
: Boolean(disableScrollProp);
|
|
36
|
+
|
|
37
|
+
const disableDragDueToScroll =
|
|
38
|
+
!disableScroll &&
|
|
39
|
+
scroll.scrollPosition &&
|
|
40
|
+
scroll.scrollPosition !== 'top';
|
|
41
|
+
|
|
42
|
+
const disableDragDueToProp =
|
|
43
|
+
typeof disableDragProp === 'function'
|
|
44
|
+
? disableDragProp({
|
|
45
|
+
scrollPosition: scroll.scrollPosition,
|
|
46
|
+
currentSnap: sheetContext.currentSnap,
|
|
47
|
+
})
|
|
48
|
+
: Boolean(disableDragProp);
|
|
49
|
+
|
|
50
|
+
const disableDrag =
|
|
51
|
+
disableDragDueToProp ||
|
|
52
|
+
disableDragDueToScroll ||
|
|
53
|
+
sheetContext.disableDrag;
|
|
15
54
|
|
|
16
55
|
const dragProps =
|
|
17
|
-
disableDrag ||
|
|
56
|
+
disableDrag || sheetContext.disableDrag
|
|
18
57
|
? undefined
|
|
19
58
|
: sheetContext.dragProps;
|
|
20
59
|
|
|
60
|
+
const isUnstyled = unstyled ?? sheetContext.unstyled;
|
|
61
|
+
|
|
62
|
+
const contentStyle: MotionStyle = {
|
|
63
|
+
...applyStyles(styles.content, isUnstyled),
|
|
64
|
+
...styleProp,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const scrollStyle: MotionStyle = applyStyles(styles.scroller, isUnstyled);
|
|
68
|
+
|
|
69
|
+
if (sheetContext.avoidKeyboard) {
|
|
70
|
+
scrollStyle.paddingBottom =
|
|
71
|
+
'env(keyboard-inset-height, var(--keyboard-inset-height, 0px))';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (disableScroll) {
|
|
75
|
+
scrollStyle.overflowY = 'hidden';
|
|
76
|
+
}
|
|
77
|
+
|
|
21
78
|
return (
|
|
22
79
|
<motion.div
|
|
23
80
|
{...rest}
|
|
24
|
-
ref={mergeRefs([ref,
|
|
81
|
+
ref={mergeRefs([ref, dragConstraints.ref])}
|
|
25
82
|
className={`react-modal-sheet-content ${className}`}
|
|
26
|
-
style={
|
|
83
|
+
style={contentStyle}
|
|
27
84
|
{...dragProps}
|
|
28
|
-
dragConstraints={
|
|
29
|
-
onMeasureDragConstraints={
|
|
85
|
+
dragConstraints={dragConstraints.ref}
|
|
86
|
+
onMeasureDragConstraints={dragConstraints.onMeasure}
|
|
30
87
|
>
|
|
31
|
-
|
|
88
|
+
<motion.div
|
|
89
|
+
ref={mergeRefs([scroll.ref, scrollRefProp])}
|
|
90
|
+
style={scrollStyle}
|
|
91
|
+
className="react-modal-sheet-content-scroller"
|
|
92
|
+
>
|
|
93
|
+
{children}
|
|
94
|
+
</motion.div>
|
|
32
95
|
</motion.div>
|
|
33
96
|
);
|
|
34
97
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { motion, useTransform } from 'motion/react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { useSheetContext } from './context';
|
|
5
|
+
import { styles } from './styles';
|
|
6
|
+
import type { SheetDragIndicatorProps } from './types';
|
|
7
|
+
import { applyStyles } from './utils';
|
|
8
|
+
|
|
9
|
+
export function SheetDragIndicator({
|
|
10
|
+
style,
|
|
11
|
+
className = '',
|
|
12
|
+
unstyled,
|
|
13
|
+
...rest
|
|
14
|
+
}: SheetDragIndicatorProps) {
|
|
15
|
+
const sheetContext = useSheetContext();
|
|
16
|
+
|
|
17
|
+
const indicator1Transform = useTransform(
|
|
18
|
+
sheetContext.indicatorRotation,
|
|
19
|
+
(r) => `translateX(2px) rotate(${r}deg)`
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const indicator2Transform = useTransform(
|
|
23
|
+
sheetContext.indicatorRotation,
|
|
24
|
+
(r) => `translateX(-2px) rotate(${-1 * r}deg)`
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const isUnstyled = unstyled ?? sheetContext.unstyled;
|
|
28
|
+
|
|
29
|
+
const indicatorWrapperStyle = {
|
|
30
|
+
...applyStyles(styles.indicatorWrapper, isUnstyled),
|
|
31
|
+
...style,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const indicatorStyle = applyStyles(styles.indicator, isUnstyled);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
className={`react-modal-sheet-drag-indicator-container ${className}`}
|
|
39
|
+
style={indicatorWrapperStyle}
|
|
40
|
+
{...rest}
|
|
41
|
+
>
|
|
42
|
+
<motion.span
|
|
43
|
+
className="react-modal-sheet-drag-indicator"
|
|
44
|
+
style={{ ...indicatorStyle, transform: indicator1Transform }}
|
|
45
|
+
/>
|
|
46
|
+
<motion.span
|
|
47
|
+
className="react-modal-sheet-drag-indicator"
|
|
48
|
+
style={{ ...indicatorStyle, transform: indicator2Transform }}
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
package/src/SheetHeader.tsx
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
|
+
import { motion } from 'motion/react';
|
|
1
2
|
import React, { forwardRef } from 'react';
|
|
2
|
-
import { motion, useTransform } from 'motion/react';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { SheetDragIndicator } from './SheetDragIndicator';
|
|
5
5
|
import { useSheetContext } from './context';
|
|
6
6
|
import { useDragConstraints } from './hooks/use-drag-constraints';
|
|
7
|
-
import { mergeRefs } from './utils';
|
|
8
7
|
import { styles } from './styles';
|
|
8
|
+
import { type SheetHeaderProps } from './types';
|
|
9
|
+
import { applyStyles, mergeRefs } from './utils';
|
|
9
10
|
|
|
10
|
-
export const SheetHeader = forwardRef<any,
|
|
11
|
-
(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
export const SheetHeader = forwardRef<any, SheetHeaderProps>(
|
|
12
|
+
(
|
|
13
|
+
{ children, style, disableDrag, unstyled, className = '', ...rest },
|
|
14
|
+
ref
|
|
15
|
+
) => {
|
|
16
|
+
const sheetContext = useSheetContext();
|
|
17
|
+
const dragConstraints = useDragConstraints();
|
|
18
|
+
const dragProps =
|
|
19
|
+
disableDrag || sheetContext.disableDrag
|
|
20
|
+
? undefined
|
|
21
|
+
: sheetContext.dragProps;
|
|
15
22
|
|
|
16
|
-
const
|
|
17
|
-
indicatorRotation,
|
|
18
|
-
(r) => `translateX(2px) rotate(${r}deg)`
|
|
19
|
-
);
|
|
23
|
+
const isUnstyled = unstyled ?? sheetContext.unstyled;
|
|
20
24
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
const headerWrapperStyle = {
|
|
26
|
+
...applyStyles(styles.headerWrapper, isUnstyled),
|
|
27
|
+
...style,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const headerStyle = applyStyles(styles.header, isUnstyled);
|
|
25
31
|
|
|
26
32
|
return (
|
|
27
33
|
<motion.div
|
|
28
34
|
{...rest}
|
|
29
|
-
ref={mergeRefs([ref,
|
|
30
|
-
style={
|
|
31
|
-
{
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
ref={mergeRefs([ref, dragConstraints.ref])}
|
|
36
|
+
style={headerWrapperStyle}
|
|
37
|
+
className={`react-modal-sheet-header-container ${className}`}
|
|
38
|
+
{...dragProps}
|
|
39
|
+
dragConstraints={dragConstraints.ref}
|
|
40
|
+
onMeasureDragConstraints={dragConstraints.onMeasure}
|
|
34
41
|
>
|
|
35
42
|
{children || (
|
|
36
|
-
<div className="react-modal-sheet-header" style={
|
|
37
|
-
<
|
|
38
|
-
className="react-modal-sheet-drag-indicator"
|
|
39
|
-
style={{ ...styles.indicator, transform: indicator1Transform }}
|
|
40
|
-
/>
|
|
41
|
-
<motion.span
|
|
42
|
-
className="react-modal-sheet-drag-indicator"
|
|
43
|
-
style={{ ...styles.indicator, transform: indicator2Transform }}
|
|
44
|
-
/>
|
|
43
|
+
<div className="react-modal-sheet-header" style={headerStyle}>
|
|
44
|
+
<SheetDragIndicator />
|
|
45
45
|
</div>
|
|
46
46
|
)}
|
|
47
47
|
</motion.div>
|
package/src/constants.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
import type { SheetTweenConfig } from './types';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_HEIGHT = 'calc(100% - env(safe-area-inset-top) - 34px)';
|
|
2
4
|
|
|
3
5
|
export const IS_SSR = typeof window === 'undefined';
|
|
4
6
|
|
|
5
|
-
export const DEFAULT_TWEEN_CONFIG = {
|
|
7
|
+
export const DEFAULT_TWEEN_CONFIG: SheetTweenConfig = {
|
|
6
8
|
ease: 'easeOut',
|
|
7
9
|
duration: 0.2,
|
|
8
10
|
};
|
|
9
11
|
|
|
10
|
-
export const REDUCED_MOTION_TWEEN_CONFIG = {
|
|
12
|
+
export const REDUCED_MOTION_TWEEN_CONFIG: SheetTweenConfig = {
|
|
11
13
|
ease: 'linear',
|
|
12
14
|
duration: 0.01,
|
|
13
15
|
};
|
package/src/context.tsx
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
createContext,
|
|
4
|
-
useContext,
|
|
5
|
-
useState,
|
|
6
|
-
} from 'react';
|
|
7
|
-
import { type SheetScrollerContextType, type SheetContextType } from './types';
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import { type SheetContextType } from './types';
|
|
8
3
|
|
|
9
4
|
export const SheetContext = createContext<SheetContextType | undefined>(
|
|
10
5
|
undefined
|
|
@@ -15,38 +10,3 @@ export function useSheetContext() {
|
|
|
15
10
|
if (!context) throw new Error('Sheet context error');
|
|
16
11
|
return context;
|
|
17
12
|
}
|
|
18
|
-
|
|
19
|
-
export const SheetScrollerContext = createContext<
|
|
20
|
-
SheetScrollerContextType | undefined
|
|
21
|
-
>(undefined);
|
|
22
|
-
|
|
23
|
-
export function SheetScrollerContextProvider({
|
|
24
|
-
children,
|
|
25
|
-
}: {
|
|
26
|
-
children: ReactNode;
|
|
27
|
-
}) {
|
|
28
|
-
const sheetContext = useSheetContext();
|
|
29
|
-
const [disableDrag, setDisableDrag] = useState(!!sheetContext.disableDrag);
|
|
30
|
-
|
|
31
|
-
function setDragEnabled() {
|
|
32
|
-
if (!sheetContext.disableDrag) setDisableDrag(false);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function setDragDisabled() {
|
|
36
|
-
if (!disableDrag) setDisableDrag(true);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<SheetScrollerContext.Provider
|
|
41
|
-
value={{ disableDrag, setDragEnabled, setDragDisabled }}
|
|
42
|
-
>
|
|
43
|
-
{children}
|
|
44
|
-
</SheetScrollerContext.Provider>
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function useSheetScrollerContext() {
|
|
49
|
-
const context = useContext(SheetScrollerContext);
|
|
50
|
-
if (!context) throw new Error('Sheet scroller context error');
|
|
51
|
-
return context;
|
|
52
|
-
}
|
|
@@ -5,15 +5,15 @@ import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';
|
|
|
5
5
|
|
|
6
6
|
export function useDimensions() {
|
|
7
7
|
const [dimensions, setDimensions] = useState(() => ({
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
windowHeight: !IS_SSR ? window.innerHeight : 0,
|
|
9
|
+
windowWidth: !IS_SSR ? window.innerWidth : 0,
|
|
10
10
|
}));
|
|
11
11
|
|
|
12
12
|
useIsomorphicLayoutEffect(() => {
|
|
13
13
|
function handler() {
|
|
14
14
|
setDimensions({
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
windowHeight: window.innerHeight,
|
|
16
|
+
windowWidth: window.innerWidth,
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -8,7 +8,7 @@ import { type BoundingBox } from 'motion/react';
|
|
|
8
8
|
const constraints: BoundingBox = { bottom: 0, top: 0, left: 0, right: 0 };
|
|
9
9
|
|
|
10
10
|
export function useDragConstraints() {
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
return {
|
|
11
|
+
const ref = useRef<any>(null);
|
|
12
|
+
const onMeasure = useCallback(() => constraints, []);
|
|
13
|
+
return { ref, onMeasure };
|
|
14
14
|
}
|
|
@@ -1,25 +1,34 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type MotionValue, transform } from 'motion';
|
|
2
2
|
import { type RefObject } from 'react';
|
|
3
3
|
|
|
4
|
+
import type { SheetDetent, SheetSnapPoint } from '../types';
|
|
4
5
|
import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';
|
|
5
6
|
import { useSafeAreaInsets } from './use-safe-area-insets';
|
|
6
|
-
import { getSheetHeight, getSnapPoints } from '../utils';
|
|
7
7
|
|
|
8
8
|
export function useModalEffect({
|
|
9
9
|
y,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
detent,
|
|
11
|
+
rootId: _rootId,
|
|
12
|
+
sheetHeight,
|
|
13
|
+
snapPoints,
|
|
13
14
|
startThreshold,
|
|
14
15
|
}: {
|
|
15
16
|
y: MotionValue<number>;
|
|
17
|
+
detent: SheetDetent;
|
|
16
18
|
rootId?: string;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
sheetHeight: number;
|
|
20
|
+
snapPoints: SheetSnapPoint[];
|
|
19
21
|
startThreshold?: number;
|
|
20
22
|
}) {
|
|
21
23
|
const insetTop = useSafeAreaInsets().top;
|
|
22
24
|
|
|
25
|
+
let rootId: string | undefined = _rootId;
|
|
26
|
+
|
|
27
|
+
if (rootId && detent === 'full') {
|
|
28
|
+
console.warn('Using "full" detent with modal effect is not supported.');
|
|
29
|
+
rootId = undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
// Cleanup on unmount
|
|
24
33
|
useIsomorphicLayoutEffect(() => {
|
|
25
34
|
return () => {
|
|
@@ -33,15 +42,7 @@ export function useModalEffect({
|
|
|
33
42
|
const root = document.querySelector(`#${rootId}`) as HTMLDivElement;
|
|
34
43
|
if (!root) return;
|
|
35
44
|
|
|
36
|
-
let sheetHeight = 0;
|
|
37
|
-
|
|
38
45
|
const removeStartListener = y.on('animationStart', () => {
|
|
39
|
-
/**
|
|
40
|
-
* Only calculate the sheet height when the animation starts to avoid
|
|
41
|
-
* unnecessary layout calculations when y value changes.
|
|
42
|
-
*/
|
|
43
|
-
sheetHeight = getSheetHeight(sheetRef);
|
|
44
|
-
|
|
45
46
|
// biome-ignore lint/style/noNonNullAssertion: root is always defined here
|
|
46
47
|
setupModalEffect(rootId!);
|
|
47
48
|
});
|
|
@@ -55,17 +56,13 @@ export function useModalEffect({
|
|
|
55
56
|
|
|
56
57
|
let progress = Math.max(0, 1 - yValue / sheetHeight);
|
|
57
58
|
|
|
58
|
-
const snapPoints = snapPointsProp
|
|
59
|
-
? getSnapPoints({ snapPointsProp, sheetHeight })
|
|
60
|
-
: undefined;
|
|
61
|
-
|
|
62
59
|
/**
|
|
63
60
|
* Start the effect only if we have dragged over the second snap point
|
|
64
61
|
* to make the effect more natural as the sheet will reach it's final
|
|
65
62
|
* position when the user drags it over the second snap point.
|
|
66
63
|
*/
|
|
67
64
|
const snapThresholdPoint =
|
|
68
|
-
snapPoints
|
|
65
|
+
snapPoints.length > 1 ? snapPoints[snapPoints.length - 2] : undefined;
|
|
69
66
|
|
|
70
67
|
/**
|
|
71
68
|
* If we have snap points, we need to calculate the progress percentage
|
|
@@ -74,8 +71,7 @@ export function useModalEffect({
|
|
|
74
71
|
* and its end is different.
|
|
75
72
|
*/
|
|
76
73
|
if (snapThresholdPoint !== undefined) {
|
|
77
|
-
const snapThresholdValue =
|
|
78
|
-
sheetHeight - Math.min(snapThresholdPoint, sheetHeight);
|
|
74
|
+
const snapThresholdValue = snapThresholdPoint.snapValueY;
|
|
79
75
|
|
|
80
76
|
if (yValue <= snapThresholdValue) {
|
|
81
77
|
progress = (snapThresholdValue - yValue) / snapThresholdValue;
|
|
@@ -132,7 +128,7 @@ export function useModalEffect({
|
|
|
132
128
|
removeCompleteListener();
|
|
133
129
|
removeCancelListener();
|
|
134
130
|
};
|
|
135
|
-
}, [y, rootId, insetTop, startThreshold,
|
|
131
|
+
}, [y, rootId, insetTop, startThreshold, sheetHeight]);
|
|
136
132
|
}
|
|
137
133
|
|
|
138
134
|
function setupModalEffect(rootId: string) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { useStableCallback } from './use-stable-callback';
|
|
3
|
+
|
|
4
|
+
export function useResizeObserver<T extends Element = HTMLDivElement>(
|
|
5
|
+
callback: ResizeObserverCallback
|
|
6
|
+
) {
|
|
7
|
+
const observeRef = useRef<T | null>(null);
|
|
8
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
9
|
+
|
|
10
|
+
const debouncedCallback: ResizeObserverCallback = useStableCallback(
|
|
11
|
+
(entries, observer) => {
|
|
12
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
13
|
+
timeoutRef.current = setTimeout(() => callback(entries, observer), 32);
|
|
14
|
+
}
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const element = observeRef.current;
|
|
19
|
+
if (!element) return;
|
|
20
|
+
|
|
21
|
+
const observer = new ResizeObserver(debouncedCallback);
|
|
22
|
+
observer.observe(element, { box: 'border-box' });
|
|
23
|
+
|
|
24
|
+
return () => {
|
|
25
|
+
observer.disconnect();
|
|
26
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
27
|
+
};
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
return { observeRef };
|
|
31
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useScrollPosition() {
|
|
4
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
5
|
+
const [scrollPosition, setScrollPosition] = useState<
|
|
6
|
+
'top' | 'bottom' | 'middle' | undefined
|
|
7
|
+
>(undefined);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const element = ref.current;
|
|
11
|
+
if (!element) return;
|
|
12
|
+
|
|
13
|
+
let scrollTimeout: number | null = null;
|
|
14
|
+
|
|
15
|
+
function determineScrollPosition(element: HTMLDivElement) {
|
|
16
|
+
const { scrollTop, scrollHeight, clientHeight } = element;
|
|
17
|
+
const isScrollable = scrollHeight > clientHeight;
|
|
18
|
+
|
|
19
|
+
if (!isScrollable) {
|
|
20
|
+
// Reset scroll position if the content is not scrollable anymore
|
|
21
|
+
if (scrollPosition) setScrollPosition(undefined);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const isAtTop = scrollTop <= 0;
|
|
26
|
+
const isAtBottom =
|
|
27
|
+
Math.ceil(scrollHeight) - Math.ceil(scrollTop) ===
|
|
28
|
+
Math.ceil(clientHeight);
|
|
29
|
+
|
|
30
|
+
let position: 'top' | 'bottom' | 'middle';
|
|
31
|
+
|
|
32
|
+
if (isAtTop) {
|
|
33
|
+
position = 'top';
|
|
34
|
+
} else if (isAtBottom) {
|
|
35
|
+
position = 'bottom';
|
|
36
|
+
} else {
|
|
37
|
+
position = 'middle';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (position === scrollPosition) return;
|
|
41
|
+
setScrollPosition(position);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function onScroll(event: Event) {
|
|
45
|
+
if (event.currentTarget instanceof HTMLDivElement) {
|
|
46
|
+
const el = event.currentTarget;
|
|
47
|
+
if (scrollTimeout) clearTimeout(scrollTimeout);
|
|
48
|
+
// Debounce the scroll handler
|
|
49
|
+
scrollTimeout = setTimeout(() => determineScrollPosition(el), 32);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function onTouchStart(event: Event) {
|
|
54
|
+
if (event.currentTarget instanceof HTMLDivElement) {
|
|
55
|
+
determineScrollPosition(event.currentTarget);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
determineScrollPosition(element);
|
|
60
|
+
|
|
61
|
+
element.addEventListener('scroll', onScroll);
|
|
62
|
+
element.addEventListener('touchstart', onTouchStart);
|
|
63
|
+
|
|
64
|
+
return () => {
|
|
65
|
+
if (scrollTimeout) clearTimeout(scrollTimeout);
|
|
66
|
+
element.removeEventListener('scroll', onScroll);
|
|
67
|
+
element.removeEventListener('touchstart', onTouchStart);
|
|
68
|
+
};
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
return { ref, scrollPosition };
|
|
72
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useStableCallback } from './use-stable-callback';
|
|
3
|
+
|
|
4
|
+
type SheetState = 'closed' | 'opening' | 'open' | 'closing';
|
|
5
|
+
|
|
6
|
+
type UseSheetStatesProps = {
|
|
7
|
+
isOpen: boolean;
|
|
8
|
+
onClosed?: () => Promise<void> | void;
|
|
9
|
+
onOpening?: () => Promise<void> | void;
|
|
10
|
+
onOpen?: () => Promise<void> | void;
|
|
11
|
+
onClosing?: () => Promise<void> | void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function useSheetState({
|
|
15
|
+
isOpen,
|
|
16
|
+
onClosed: _onClosed,
|
|
17
|
+
onOpening: _onOpening,
|
|
18
|
+
onOpen: _onOpen,
|
|
19
|
+
onClosing: _onClosing,
|
|
20
|
+
}: UseSheetStatesProps) {
|
|
21
|
+
const [state, setState] = useState<SheetState>(isOpen ? 'opening' : 'closed');
|
|
22
|
+
const onClosed = useStableCallback(() => _onClosed?.());
|
|
23
|
+
const onOpening = useStableCallback(() => _onOpening?.());
|
|
24
|
+
const onOpen = useStableCallback(() => _onOpen?.());
|
|
25
|
+
const onClosing = useStableCallback(() => _onClosing?.());
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (isOpen && state === 'closed') {
|
|
29
|
+
setState('opening');
|
|
30
|
+
} else if (!isOpen && (state === 'open' || state === 'opening')) {
|
|
31
|
+
setState('closing');
|
|
32
|
+
}
|
|
33
|
+
}, [isOpen, state]);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
async function handle() {
|
|
37
|
+
switch (state) {
|
|
38
|
+
case 'closed':
|
|
39
|
+
await onClosed?.();
|
|
40
|
+
break;
|
|
41
|
+
|
|
42
|
+
case 'opening':
|
|
43
|
+
await onOpening?.();
|
|
44
|
+
setState('open');
|
|
45
|
+
break;
|
|
46
|
+
|
|
47
|
+
case 'open':
|
|
48
|
+
await onOpen?.();
|
|
49
|
+
break;
|
|
50
|
+
|
|
51
|
+
case 'closing':
|
|
52
|
+
await onClosing?.();
|
|
53
|
+
setState('closed');
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
handle().catch((error) => {
|
|
58
|
+
console.error('Internal sheet state error:', error);
|
|
59
|
+
});
|
|
60
|
+
}, [state]);
|
|
61
|
+
|
|
62
|
+
return state;
|
|
63
|
+
}
|