@simoneggert/react-modal-sheet 5.4.3
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/LICENSE +21 -0
- package/README.md +994 -0
- package/dist/index.d.mts +188 -0
- package/dist/index.d.ts +188 -0
- package/dist/index.js +1511 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1502 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +87 -0
- package/src/SheetBackdrop.tsx +46 -0
- package/src/SheetContainer.tsx +53 -0
- package/src/SheetContent.tsx +117 -0
- package/src/SheetDragIndicator.tsx +52 -0
- package/src/SheetHeader.tsx +52 -0
- package/src/constants.ts +19 -0
- package/src/context.tsx +12 -0
- package/src/debug.ts +38 -0
- package/src/hooks/use-dimensions.ts +30 -0
- package/src/hooks/use-drag-constraints.ts +14 -0
- package/src/hooks/use-isomorphic-layout-effect.ts +5 -0
- package/src/hooks/use-keyboard-avoidance.ts +75 -0
- package/src/hooks/use-modal-effect.ts +161 -0
- package/src/hooks/use-prevent-scroll.ts +357 -0
- package/src/hooks/use-resize-observer.ts +31 -0
- package/src/hooks/use-safe-area-insets.ts +40 -0
- package/src/hooks/use-scroll-position.ts +142 -0
- package/src/hooks/use-sheet-state.ts +63 -0
- package/src/hooks/use-stable-callback.ts +18 -0
- package/src/hooks/use-virtual-keyboard.ts +269 -0
- package/src/index.tsx +41 -0
- package/src/sheet.tsx +414 -0
- package/src/snap.ts +242 -0
- package/src/styles.ts +110 -0
- package/src/types.tsx +154 -0
- package/src/utils.ts +116 -0
package/src/styles.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
|
|
3
|
+
export const styles = {
|
|
4
|
+
root: {
|
|
5
|
+
base: {
|
|
6
|
+
position: 'fixed',
|
|
7
|
+
top: 0,
|
|
8
|
+
bottom: 0,
|
|
9
|
+
left: 0,
|
|
10
|
+
right: 0,
|
|
11
|
+
overflow: 'hidden',
|
|
12
|
+
pointerEvents: 'none',
|
|
13
|
+
},
|
|
14
|
+
decorative: {},
|
|
15
|
+
},
|
|
16
|
+
backdrop: {
|
|
17
|
+
base: {
|
|
18
|
+
zIndex: 1,
|
|
19
|
+
position: 'fixed',
|
|
20
|
+
top: 0,
|
|
21
|
+
left: 0,
|
|
22
|
+
width: '100%',
|
|
23
|
+
height: '100%',
|
|
24
|
+
touchAction: 'none',
|
|
25
|
+
userSelect: 'none',
|
|
26
|
+
},
|
|
27
|
+
decorative: {
|
|
28
|
+
backgroundColor: 'rgba(0, 0, 0, 0.2)',
|
|
29
|
+
border: 'none',
|
|
30
|
+
WebkitTapHighlightColor: 'transparent',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
container: {
|
|
34
|
+
base: {
|
|
35
|
+
zIndex: 2,
|
|
36
|
+
position: 'absolute',
|
|
37
|
+
left: 0,
|
|
38
|
+
bottom: 0,
|
|
39
|
+
width: '100%',
|
|
40
|
+
pointerEvents: 'auto',
|
|
41
|
+
display: 'flex',
|
|
42
|
+
flexDirection: 'column',
|
|
43
|
+
},
|
|
44
|
+
decorative: {
|
|
45
|
+
backgroundColor: '#fff',
|
|
46
|
+
borderTopRightRadius: '8px',
|
|
47
|
+
borderTopLeftRadius: '8px',
|
|
48
|
+
boxShadow: '0px -2px 16px rgba(0, 0, 0, 0.3)',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
headerWrapper: {
|
|
52
|
+
base: {
|
|
53
|
+
width: '100%',
|
|
54
|
+
},
|
|
55
|
+
decorative: {},
|
|
56
|
+
},
|
|
57
|
+
header: {
|
|
58
|
+
base: {
|
|
59
|
+
width: '100%',
|
|
60
|
+
position: 'relative',
|
|
61
|
+
},
|
|
62
|
+
decorative: {
|
|
63
|
+
height: '40px',
|
|
64
|
+
display: 'flex',
|
|
65
|
+
alignItems: 'center',
|
|
66
|
+
justifyContent: 'center',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
indicatorWrapper: {
|
|
70
|
+
base: {
|
|
71
|
+
display: 'flex',
|
|
72
|
+
},
|
|
73
|
+
decorative: {},
|
|
74
|
+
},
|
|
75
|
+
indicator: {
|
|
76
|
+
base: {
|
|
77
|
+
display: 'inline-block',
|
|
78
|
+
},
|
|
79
|
+
decorative: {
|
|
80
|
+
width: '18px',
|
|
81
|
+
height: '4px',
|
|
82
|
+
borderRadius: '99px',
|
|
83
|
+
backgroundColor: '#ddd',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
content: {
|
|
87
|
+
base: {
|
|
88
|
+
minHeight: '0px',
|
|
89
|
+
position: 'relative',
|
|
90
|
+
flexGrow: 1,
|
|
91
|
+
display: 'flex',
|
|
92
|
+
flexDirection: 'column',
|
|
93
|
+
},
|
|
94
|
+
decorative: {},
|
|
95
|
+
},
|
|
96
|
+
scroller: {
|
|
97
|
+
base: {
|
|
98
|
+
height: '100%',
|
|
99
|
+
overflowY: 'auto',
|
|
100
|
+
overscrollBehaviorY: 'none',
|
|
101
|
+
},
|
|
102
|
+
decorative: {},
|
|
103
|
+
},
|
|
104
|
+
} satisfies Record<
|
|
105
|
+
string,
|
|
106
|
+
{
|
|
107
|
+
base: CSSProperties;
|
|
108
|
+
decorative: CSSProperties;
|
|
109
|
+
}
|
|
110
|
+
>;
|
package/src/types.tsx
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type DragHandler,
|
|
3
|
+
type EasingDefinition,
|
|
4
|
+
type MotionProps,
|
|
5
|
+
type MotionValue,
|
|
6
|
+
type motion,
|
|
7
|
+
} from 'motion/react';
|
|
8
|
+
import {
|
|
9
|
+
type ComponentPropsWithoutRef,
|
|
10
|
+
type ForwardRefExoticComponent,
|
|
11
|
+
type FunctionComponent,
|
|
12
|
+
type HTMLAttributes,
|
|
13
|
+
type ReactNode,
|
|
14
|
+
type RefAttributes,
|
|
15
|
+
type RefObject,
|
|
16
|
+
} from 'react';
|
|
17
|
+
|
|
18
|
+
export type SheetDetent = 'default' | 'full' | 'content';
|
|
19
|
+
|
|
20
|
+
type CommonProps = {
|
|
21
|
+
className?: string;
|
|
22
|
+
unstyled?: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type MotionDivProps = ComponentPropsWithoutRef<typeof motion.div>;
|
|
26
|
+
|
|
27
|
+
type MotionCommonProps = Omit<MotionDivProps, 'initial' | 'animate' | 'exit'>;
|
|
28
|
+
|
|
29
|
+
export interface SheetTweenConfig {
|
|
30
|
+
ease: EasingDefinition;
|
|
31
|
+
duration: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type SheetProps = {
|
|
35
|
+
unstyled?: boolean;
|
|
36
|
+
avoidKeyboard?: boolean;
|
|
37
|
+
children: ReactNode;
|
|
38
|
+
detent?: SheetDetent;
|
|
39
|
+
disableDismiss?: boolean;
|
|
40
|
+
disableDrag?: boolean;
|
|
41
|
+
disableScrollLocking?: boolean;
|
|
42
|
+
dragCloseThreshold?: number;
|
|
43
|
+
dragVelocityThreshold?: number;
|
|
44
|
+
initialSnap?: number; // index of snap points array
|
|
45
|
+
isOpen: boolean;
|
|
46
|
+
modalEffectRootId?: string;
|
|
47
|
+
modalEffectThreshold?: number;
|
|
48
|
+
mountPoint?: Element;
|
|
49
|
+
prefersReducedMotion?: boolean;
|
|
50
|
+
snapPoints?: number[];
|
|
51
|
+
tweenConfig?: SheetTweenConfig;
|
|
52
|
+
onClose: () => void;
|
|
53
|
+
onCloseEnd?: () => void;
|
|
54
|
+
onCloseStart?: () => void;
|
|
55
|
+
onOpenEnd?: () => void;
|
|
56
|
+
onOpenStart?: () => void;
|
|
57
|
+
onSnap?: (index: number) => void;
|
|
58
|
+
} & MotionCommonProps;
|
|
59
|
+
|
|
60
|
+
export type SheetContainerProps = MotionCommonProps &
|
|
61
|
+
CommonProps & {
|
|
62
|
+
children: ReactNode;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type SheetHeaderProps = MotionCommonProps &
|
|
66
|
+
CommonProps & {
|
|
67
|
+
children?: ReactNode;
|
|
68
|
+
disableDrag?: boolean;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type SheetContentProps = MotionCommonProps &
|
|
72
|
+
CommonProps & {
|
|
73
|
+
disableDrag?: boolean | ((args: SheetStateInfo) => boolean);
|
|
74
|
+
disableScroll?: boolean | ((args: SheetStateInfo) => boolean);
|
|
75
|
+
scrollRef?: RefObject<HTMLDivElement | null>;
|
|
76
|
+
scrollClassName?: string;
|
|
77
|
+
scrollStyle?: MotionProps['style'];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type SheetBackdropProps = MotionDivProps & CommonProps;
|
|
81
|
+
|
|
82
|
+
export type SheetDragIndicatorProps = HTMLAttributes<HTMLDivElement> &
|
|
83
|
+
CommonProps;
|
|
84
|
+
|
|
85
|
+
export interface SheetDragProps {
|
|
86
|
+
drag: 'y';
|
|
87
|
+
dragElastic: number;
|
|
88
|
+
dragMomentum: boolean;
|
|
89
|
+
dragPropagation: boolean;
|
|
90
|
+
onDrag: DragHandler;
|
|
91
|
+
onDragStart: DragHandler;
|
|
92
|
+
onDragEnd: DragHandler;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type SheetStateInfo = {
|
|
96
|
+
scrollPosition?: 'top' | 'bottom' | 'middle';
|
|
97
|
+
currentSnap?: number;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type SheetSnapPoint = {
|
|
101
|
+
snapIndex: number;
|
|
102
|
+
snapValue: number; // Absolute value from the bottom of the sheet
|
|
103
|
+
snapValueY: number; // Y value is inverted as `y = 0` means sheet is at the top
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export interface SheetContextType {
|
|
107
|
+
currentSnap?: number;
|
|
108
|
+
detent: SheetDetent;
|
|
109
|
+
disableDrag: boolean;
|
|
110
|
+
dragProps: SheetDragProps;
|
|
111
|
+
indicatorRotation: MotionValue<number>;
|
|
112
|
+
avoidKeyboard: boolean;
|
|
113
|
+
prefersReducedMotion: boolean;
|
|
114
|
+
sheetBoundsRef: (node: HTMLDivElement | null) => void;
|
|
115
|
+
sheetRef: RefObject<any>;
|
|
116
|
+
unstyled: boolean;
|
|
117
|
+
y: MotionValue<any>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface SheetScrollerContextType {
|
|
121
|
+
disableDrag: boolean;
|
|
122
|
+
setDragDisabled: () => void;
|
|
123
|
+
setDragEnabled: () => void;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
type SheetComponent = ForwardRefExoticComponent<
|
|
127
|
+
SheetProps & RefAttributes<any>
|
|
128
|
+
>;
|
|
129
|
+
|
|
130
|
+
type ContainerComponent = ForwardRefExoticComponent<
|
|
131
|
+
SheetContainerProps & RefAttributes<any>
|
|
132
|
+
>;
|
|
133
|
+
|
|
134
|
+
type HeaderComponent = ForwardRefExoticComponent<
|
|
135
|
+
SheetHeaderProps & RefAttributes<any>
|
|
136
|
+
>;
|
|
137
|
+
|
|
138
|
+
type BackdropComponent = ForwardRefExoticComponent<
|
|
139
|
+
SheetBackdropProps & RefAttributes<any>
|
|
140
|
+
>;
|
|
141
|
+
|
|
142
|
+
type ContentComponent = ForwardRefExoticComponent<
|
|
143
|
+
SheetContentProps & RefAttributes<any>
|
|
144
|
+
>;
|
|
145
|
+
|
|
146
|
+
interface SheetCompoundComponent {
|
|
147
|
+
Container: ContainerComponent;
|
|
148
|
+
Header: HeaderComponent;
|
|
149
|
+
DragIndicator: FunctionComponent<SheetDragIndicatorProps>;
|
|
150
|
+
Content: ContentComponent;
|
|
151
|
+
Backdrop: BackdropComponent;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type SheetCompound = SheetComponent & SheetCompoundComponent;
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { type CSSProperties, type ForwardedRef, type RefCallback } from 'react';
|
|
2
|
+
import { IS_SSR } from './constants';
|
|
3
|
+
|
|
4
|
+
export function applyStyles(
|
|
5
|
+
styles: { base: CSSProperties; decorative: CSSProperties },
|
|
6
|
+
unstyled: boolean
|
|
7
|
+
) {
|
|
8
|
+
return unstyled ? styles.base : { ...styles.base, ...styles.decorative };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isAscendingOrder(arr: number[]) {
|
|
12
|
+
for (let i = 0; i < arr.length; i++) {
|
|
13
|
+
if (arr[i + 1] < arr[i]) return false;
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function mergeRefs<T = any>(refs: ForwardedRef<T>[]): RefCallback<T> {
|
|
19
|
+
return (value: any) => {
|
|
20
|
+
refs.forEach((ref: any) => {
|
|
21
|
+
if (typeof ref === 'function') {
|
|
22
|
+
ref(value);
|
|
23
|
+
} else if (ref) {
|
|
24
|
+
ref.current = value;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isTouchDevice() {
|
|
31
|
+
if (IS_SSR) return false;
|
|
32
|
+
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function testPlatform(re: RegExp) {
|
|
36
|
+
return typeof window !== 'undefined' && window.navigator != null
|
|
37
|
+
? re.test(
|
|
38
|
+
// @ts-expect-error
|
|
39
|
+
window.navigator.userAgentData?.platform || window.navigator.platform
|
|
40
|
+
)
|
|
41
|
+
: false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function cached(fn: () => boolean) {
|
|
45
|
+
let res: boolean | null = null;
|
|
46
|
+
return () => {
|
|
47
|
+
if (res == null) {
|
|
48
|
+
res = fn();
|
|
49
|
+
}
|
|
50
|
+
return res;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const isMac = cached(function () {
|
|
55
|
+
return testPlatform(/^Mac/i);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const isIPhone = cached(function () {
|
|
59
|
+
return testPlatform(/^iPhone/i);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const isIPad = cached(function () {
|
|
63
|
+
// iPadOS 13 lies and says it's a Mac, but we can distinguish by detecting touch support.
|
|
64
|
+
return testPlatform(/^iPad/i) || (isMac() && navigator.maxTouchPoints > 1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export const isIOS = cached(function () {
|
|
68
|
+
return isIPhone() || isIPad();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/** Wait for an element to be rendered and visible */
|
|
72
|
+
export function waitForElement(
|
|
73
|
+
className: string,
|
|
74
|
+
interval = 50,
|
|
75
|
+
maxAttempts = 20
|
|
76
|
+
) {
|
|
77
|
+
return new Promise<HTMLElement | null>((resolve) => {
|
|
78
|
+
let attempts = 0;
|
|
79
|
+
const timer = setInterval(() => {
|
|
80
|
+
const element = document.getElementsByClassName(
|
|
81
|
+
className
|
|
82
|
+
)[0] as HTMLElement;
|
|
83
|
+
attempts++;
|
|
84
|
+
if (element || attempts >= maxAttempts) {
|
|
85
|
+
clearInterval(timer);
|
|
86
|
+
resolve(element);
|
|
87
|
+
}
|
|
88
|
+
}, interval);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// HTML input types that do not cause the software keyboard to appear.
|
|
93
|
+
const nonTextInputTypes = new Set([
|
|
94
|
+
'checkbox',
|
|
95
|
+
'radio',
|
|
96
|
+
'range',
|
|
97
|
+
'color',
|
|
98
|
+
'file',
|
|
99
|
+
'image',
|
|
100
|
+
'button',
|
|
101
|
+
'submit',
|
|
102
|
+
'reset',
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
export function willOpenKeyboard(target: Element) {
|
|
106
|
+
return (
|
|
107
|
+
(target instanceof HTMLInputElement &&
|
|
108
|
+
!nonTextInputTypes.has(target.type)) ||
|
|
109
|
+
target instanceof HTMLTextAreaElement ||
|
|
110
|
+
(target instanceof HTMLElement && target.isContentEditable)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function isHTTPS() {
|
|
115
|
+
return typeof window !== 'undefined' && window.isSecureContext;
|
|
116
|
+
}
|