@fountain-ui/lab 2.0.0-beta.28 → 2.0.0-beta.29
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/build/commonjs/BottomSheet/BottomSheet.js +177 -0
- package/build/commonjs/BottomSheet/BottomSheet.js.map +1 -0
- package/build/commonjs/BottomSheet/BottomSheetProps.js.map +1 -1
- package/build/commonjs/BottomSheet/useDynamicSnapPoints.js +49 -0
- package/build/commonjs/BottomSheet/useDynamicSnapPoints.js.map +1 -0
- package/build/module/BottomSheet/BottomSheet.js +161 -0
- package/build/module/BottomSheet/BottomSheet.js.map +1 -0
- package/build/module/BottomSheet/BottomSheetProps.js.map +1 -1
- package/build/module/BottomSheet/useDynamicSnapPoints.js +41 -0
- package/build/module/BottomSheet/useDynamicSnapPoints.js.map +1 -0
- package/build/typescript/BottomSheet/BottomSheet.d.ts +2 -0
- package/build/typescript/BottomSheet/BottomSheetProps.d.ts +4 -0
- package/build/typescript/BottomSheet/useDynamicSnapPoints.d.ts +11 -0
- package/package.json +3 -3
- package/src/BottomSheet/BottomSheet.tsx +184 -0
- package/src/BottomSheet/BottomSheetProps.ts +5 -0
- package/src/BottomSheet/useDynamicSnapPoints.ts +52 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = BottomSheet;
|
|
7
|
+
|
|
8
|
+
var _react = _interopRequireWildcard(require("react"));
|
|
9
|
+
|
|
10
|
+
var _reactNative = require("react-native");
|
|
11
|
+
|
|
12
|
+
var _core = require("@fountain-ui/core");
|
|
13
|
+
|
|
14
|
+
var _useDynamicSnapPoints = _interopRequireDefault(require("./useDynamicSnapPoints"));
|
|
15
|
+
|
|
16
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
17
|
+
|
|
18
|
+
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
|
|
19
|
+
|
|
20
|
+
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
|
21
|
+
|
|
22
|
+
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
23
|
+
|
|
24
|
+
const useStyles = function () {
|
|
25
|
+
const theme = (0, _core.useTheme)();
|
|
26
|
+
return {
|
|
27
|
+
root: {
|
|
28
|
+
justifyContent: 'flex-end',
|
|
29
|
+
zIndex: theme.zIndex.dialog
|
|
30
|
+
},
|
|
31
|
+
animated: {
|
|
32
|
+
alignSelf: 'center',
|
|
33
|
+
maxWidth: 720,
|
|
34
|
+
width: '100%'
|
|
35
|
+
},
|
|
36
|
+
paper: {
|
|
37
|
+
borderBottomLeftRadius: 0,
|
|
38
|
+
borderBottomRightRadius: 0,
|
|
39
|
+
flexGrow: 1,
|
|
40
|
+
overflow: 'hidden'
|
|
41
|
+
},
|
|
42
|
+
scrollView: {
|
|
43
|
+
flexGrow: 1
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function BottomSheet(props) {
|
|
49
|
+
const {
|
|
50
|
+
backdropOpacity,
|
|
51
|
+
children,
|
|
52
|
+
header,
|
|
53
|
+
index,
|
|
54
|
+
onChange,
|
|
55
|
+
snapPoints: initialSnapPoints
|
|
56
|
+
} = props;
|
|
57
|
+
const styles = useStyles();
|
|
58
|
+
const contentLayout = (0, _react.useRef)({
|
|
59
|
+
x: 0,
|
|
60
|
+
y: 0,
|
|
61
|
+
width: 0,
|
|
62
|
+
height: 0
|
|
63
|
+
});
|
|
64
|
+
const currentScrollY = (0, _react.useRef)(0);
|
|
65
|
+
const {
|
|
66
|
+
snapPoints,
|
|
67
|
+
handleContentLayout
|
|
68
|
+
} = (0, _useDynamicSnapPoints.default)({
|
|
69
|
+
index,
|
|
70
|
+
initialSnapPoints
|
|
71
|
+
});
|
|
72
|
+
const height = snapPoints[snapPoints.length - 1] ?? 0;
|
|
73
|
+
const translateY = height - (snapPoints[index] ?? 0);
|
|
74
|
+
const animatedY = (0, _core.useAnimatedValue)(translateY);
|
|
75
|
+
|
|
76
|
+
const isInsideScrollView = (locationX, locationY) => {
|
|
77
|
+
const {
|
|
78
|
+
current: {
|
|
79
|
+
x,
|
|
80
|
+
y,
|
|
81
|
+
width,
|
|
82
|
+
height
|
|
83
|
+
}
|
|
84
|
+
} = contentLayout;
|
|
85
|
+
return locationX >= x && locationX <= x + width && locationY >= y && locationY <= y + height;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const panResponder = (0, _react.useMemo)(() => _reactNative.PanResponder.create({
|
|
89
|
+
onMoveShouldSetPanResponder: (event, gestureState) => {
|
|
90
|
+
const {
|
|
91
|
+
nativeEvent: {
|
|
92
|
+
locationX,
|
|
93
|
+
locationY
|
|
94
|
+
}
|
|
95
|
+
} = event;
|
|
96
|
+
|
|
97
|
+
if (isInsideScrollView(locationX, locationY)) {
|
|
98
|
+
return gestureState.dy > 0 && currentScrollY.current === 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return true;
|
|
102
|
+
},
|
|
103
|
+
onPanResponderMove: (_, gestureState) => {
|
|
104
|
+
if (gestureState.dy > 0) {
|
|
105
|
+
animatedY.setValue(translateY + gestureState.dy);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
onPanResponderRelease: (_, gestureState) => {
|
|
109
|
+
if (gestureState.dy > 0 && gestureState.vy > 0.5) {
|
|
110
|
+
handleClose();
|
|
111
|
+
} else {
|
|
112
|
+
_reactNative.Animated.timing(animatedY, {
|
|
113
|
+
toValue: translateY,
|
|
114
|
+
useNativeDriver: false,
|
|
115
|
+
duration: 300
|
|
116
|
+
}).start();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}), [translateY]);
|
|
120
|
+
(0, _react.useEffect)(() => {
|
|
121
|
+
if (translateY >= 0) {
|
|
122
|
+
_reactNative.Animated.timing(animatedY, {
|
|
123
|
+
toValue: translateY,
|
|
124
|
+
useNativeDriver: false,
|
|
125
|
+
duration: 300
|
|
126
|
+
}).start();
|
|
127
|
+
}
|
|
128
|
+
}, [translateY]);
|
|
129
|
+
|
|
130
|
+
const handleClose = () => {
|
|
131
|
+
if (onChange) {
|
|
132
|
+
onChange(-1);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const handleScrollViewLayout = event => {
|
|
137
|
+
contentLayout.current = event.nativeEvent.layout;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const handleScroll = event => {
|
|
141
|
+
currentScrollY.current = event.nativeEvent.contentOffset.y;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const animatedStyles = [styles.animated, {
|
|
145
|
+
transform: [{
|
|
146
|
+
translateY: animatedY
|
|
147
|
+
}]
|
|
148
|
+
}];
|
|
149
|
+
const paperStyles = [styles.paper, { ...(height !== 0 ? {
|
|
150
|
+
height
|
|
151
|
+
} : {})
|
|
152
|
+
}];
|
|
153
|
+
const contentStyle = { ...(snapPoints[index] !== 0 ? {
|
|
154
|
+
height: snapPoints[index]
|
|
155
|
+
} : {})
|
|
156
|
+
};
|
|
157
|
+
return /*#__PURE__*/_react.default.createElement(_core.Modal, {
|
|
158
|
+
backdropOpacity: backdropOpacity,
|
|
159
|
+
onClose: handleClose,
|
|
160
|
+
visible: index >= 0,
|
|
161
|
+
style: (0, _core.css)([_core.StyleSheet.absoluteFill, styles.root])
|
|
162
|
+
}, /*#__PURE__*/_react.default.createElement(_reactNative.Animated.View, _extends({
|
|
163
|
+
style: animatedStyles
|
|
164
|
+
}, panResponder.panHandlers), /*#__PURE__*/_react.default.createElement(_core.Paper, {
|
|
165
|
+
elevation: 12,
|
|
166
|
+
style: paperStyles
|
|
167
|
+
}, /*#__PURE__*/_react.default.createElement(_reactNative.View, {
|
|
168
|
+
style: contentStyle,
|
|
169
|
+
onLayout: handleContentLayout
|
|
170
|
+
}, header, /*#__PURE__*/_react.default.createElement(_reactNative.ScrollView, {
|
|
171
|
+
onLayout: handleScrollViewLayout,
|
|
172
|
+
onScroll: handleScroll,
|
|
173
|
+
scrollEventThrottle: 300,
|
|
174
|
+
style: styles.scrollView
|
|
175
|
+
}, children)))));
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=BottomSheet.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["useStyles","theme","useTheme","root","justifyContent","zIndex","dialog","animated","alignSelf","maxWidth","width","paper","borderBottomLeftRadius","borderBottomRightRadius","flexGrow","overflow","scrollView","BottomSheet","props","backdropOpacity","children","header","index","onChange","snapPoints","initialSnapPoints","styles","contentLayout","useRef","x","y","height","currentScrollY","handleContentLayout","useDynamicSnapPoints","length","translateY","animatedY","useAnimatedValue","isInsideScrollView","locationX","locationY","current","panResponder","useMemo","PanResponder","create","onMoveShouldSetPanResponder","event","gestureState","nativeEvent","dy","onPanResponderMove","_","setValue","onPanResponderRelease","vy","handleClose","Animated","timing","toValue","useNativeDriver","duration","start","useEffect","handleScrollViewLayout","layout","handleScroll","contentOffset","animatedStyles","transform","paperStyles","contentStyle","css","StyleSheet","absoluteFill","panHandlers"],"sources":["BottomSheet.tsx"],"sourcesContent":["import React, { useEffect, useMemo, useRef } from 'react';\nimport {\n Animated,\n LayoutChangeEvent,\n LayoutRectangle,\n NativeScrollEvent,\n NativeSyntheticEvent,\n PanResponder,\n ScrollView,\n View,\n} from 'react-native';\nimport { css, Modal, Paper, StyleSheet, useAnimatedValue, useTheme } from '@fountain-ui/core';\nimport { NamedStylesStringUnion, UseStyles } from '@fountain-ui/styles';\nimport BottomSheetProps from './BottomSheetProps';\nimport useDynamicSnapPoints from './useDynamicSnapPoints';\n\ntype BottomSheetStyles = NamedStylesStringUnion<'root' | 'animated' | 'paper' | 'scrollView'>;\n\nconst useStyles: UseStyles<BottomSheetStyles> = function (): BottomSheetStyles {\n const theme = useTheme();\n\n return {\n root: {\n justifyContent: 'flex-end',\n zIndex: theme.zIndex.dialog,\n },\n animated: {\n alignSelf: 'center',\n maxWidth: 720,\n width: '100%',\n },\n paper: {\n borderBottomLeftRadius: 0,\n borderBottomRightRadius: 0,\n flexGrow: 1,\n overflow: 'hidden',\n },\n scrollView: {\n flexGrow: 1,\n },\n };\n};\n\nexport default function BottomSheet(props: BottomSheetProps) {\n const {\n backdropOpacity,\n children,\n header,\n index,\n onChange,\n snapPoints: initialSnapPoints,\n } = props;\n\n const styles = useStyles();\n\n const contentLayout = useRef<LayoutRectangle>({ x: 0, y: 0, width: 0, height: 0 });\n const currentScrollY = useRef<number>(0);\n\n const {\n snapPoints,\n handleContentLayout,\n } = useDynamicSnapPoints({\n index,\n initialSnapPoints,\n });\n\n const height = snapPoints[snapPoints.length - 1] ?? 0;\n const translateY = height - (snapPoints[index] ?? 0);\n\n const animatedY = useAnimatedValue(translateY);\n\n const isInsideScrollView = (locationX: number, locationY: number): boolean => {\n const {\n current: { x, y, width, height },\n } = contentLayout;\n\n return locationX >= x && locationX <= x + width && locationY >= y && locationY <= y + height;\n };\n\n const panResponder = useMemo(() => (\n PanResponder.create({\n onMoveShouldSetPanResponder: (event, gestureState) => {\n const {\n nativeEvent: { locationX, locationY },\n } = event;\n\n if (isInsideScrollView(locationX, locationY)) {\n return gestureState.dy > 0 && currentScrollY.current === 0;\n }\n\n return true;\n },\n onPanResponderMove: (_, gestureState) => {\n if (gestureState.dy > 0) {\n animatedY.setValue(translateY + gestureState.dy);\n }\n },\n onPanResponderRelease: (_, gestureState) => {\n if (gestureState.dy > 0 && gestureState.vy > 0.5) {\n handleClose();\n } else {\n Animated.timing(animatedY, {\n toValue: translateY,\n useNativeDriver: false,\n duration: 300,\n }).start();\n }\n },\n })\n ), [translateY]);\n\n useEffect(() => {\n if (translateY >= 0) {\n Animated.timing(animatedY, {\n toValue: translateY,\n useNativeDriver: false,\n duration: 300,\n }).start();\n }\n }, [translateY]);\n\n const handleClose = () => {\n if (onChange) {\n onChange(-1);\n }\n };\n\n const handleScrollViewLayout = (event: LayoutChangeEvent) => {\n contentLayout.current = event.nativeEvent.layout;\n };\n\n const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {\n currentScrollY.current = event.nativeEvent.contentOffset.y;\n };\n\n const animatedStyles = [\n styles.animated,\n { transform: [{ translateY: animatedY }] },\n ];\n\n const paperStyles = [\n styles.paper,\n { ...(height !== 0 ? { height } : {}) },\n ];\n\n const contentStyle = {\n ...(snapPoints[index] !== 0 ? { height: snapPoints[index] } : {}),\n };\n\n return (\n <Modal\n backdropOpacity={backdropOpacity}\n onClose={handleClose}\n visible={index >= 0}\n style={css([StyleSheet.absoluteFill, styles.root])}\n >\n <Animated.View\n style={animatedStyles}\n {...panResponder.panHandlers}\n >\n <Paper\n elevation={12}\n style={paperStyles}\n >\n <View\n style={contentStyle}\n onLayout={handleContentLayout}\n >\n {header}\n\n <ScrollView\n onLayout={handleScrollViewLayout}\n onScroll={handleScroll}\n scrollEventThrottle={300}\n style={styles.scrollView}\n >\n {children}\n </ScrollView>\n </View>\n </Paper>\n </Animated.View>\n </Modal>\n );\n}\n"],"mappings":";;;;;;;AAAA;;AACA;;AAUA;;AAGA;;;;;;;;;;AAIA,MAAMA,SAAuC,GAAG,YAA+B;EAC3E,MAAMC,KAAK,GAAG,IAAAC,cAAA,GAAd;EAEA,OAAO;IACHC,IAAI,EAAE;MACFC,cAAc,EAAE,UADd;MAEFC,MAAM,EAAEJ,KAAK,CAACI,MAAN,CAAaC;IAFnB,CADH;IAKHC,QAAQ,EAAE;MACNC,SAAS,EAAE,QADL;MAENC,QAAQ,EAAE,GAFJ;MAGNC,KAAK,EAAE;IAHD,CALP;IAUHC,KAAK,EAAE;MACHC,sBAAsB,EAAE,CADrB;MAEHC,uBAAuB,EAAE,CAFtB;MAGHC,QAAQ,EAAE,CAHP;MAIHC,QAAQ,EAAE;IAJP,CAVJ;IAgBHC,UAAU,EAAE;MACRF,QAAQ,EAAE;IADF;EAhBT,CAAP;AAoBH,CAvBD;;AAyBe,SAASG,WAAT,CAAqBC,KAArB,EAA8C;EACzD,MAAM;IACFC,eADE;IAEFC,QAFE;IAGFC,MAHE;IAIFC,KAJE;IAKFC,QALE;IAMFC,UAAU,EAAEC;EANV,IAOFP,KAPJ;EASA,MAAMQ,MAAM,GAAG1B,SAAS,EAAxB;EAEA,MAAM2B,aAAa,GAAG,IAAAC,aAAA,EAAwB;IAAEC,CAAC,EAAE,CAAL;IAAQC,CAAC,EAAE,CAAX;IAAcpB,KAAK,EAAE,CAArB;IAAwBqB,MAAM,EAAE;EAAhC,CAAxB,CAAtB;EACA,MAAMC,cAAc,GAAG,IAAAJ,aAAA,EAAe,CAAf,CAAvB;EAEA,MAAM;IACFJ,UADE;IAEFS;EAFE,IAGF,IAAAC,6BAAA,EAAqB;IACrBZ,KADqB;IAErBG;EAFqB,CAArB,CAHJ;EAQA,MAAMM,MAAM,GAAGP,UAAU,CAACA,UAAU,CAACW,MAAX,GAAoB,CAArB,CAAV,IAAqC,CAApD;EACA,MAAMC,UAAU,GAAGL,MAAM,IAAIP,UAAU,CAACF,KAAD,CAAV,IAAqB,CAAzB,CAAzB;EAEA,MAAMe,SAAS,GAAG,IAAAC,sBAAA,EAAiBF,UAAjB,CAAlB;;EAEA,MAAMG,kBAAkB,GAAG,CAACC,SAAD,EAAoBC,SAApB,KAAmD;IAC1E,MAAM;MACFC,OAAO,EAAE;QAAEb,CAAF;QAAKC,CAAL;QAAQpB,KAAR;QAAeqB;MAAf;IADP,IAEFJ,aAFJ;IAIA,OAAOa,SAAS,IAAIX,CAAb,IAAkBW,SAAS,IAAIX,CAAC,GAAGnB,KAAnC,IAA4C+B,SAAS,IAAIX,CAAzD,IAA8DW,SAAS,IAAIX,CAAC,GAAGC,MAAtF;EACH,CAND;;EAQA,MAAMY,YAAY,GAAG,IAAAC,cAAA,EAAQ,MACzBC,yBAAA,CAAaC,MAAb,CAAoB;IAChBC,2BAA2B,EAAE,CAACC,KAAD,EAAQC,YAAR,KAAyB;MAClD,MAAM;QACFC,WAAW,EAAE;UAAEV,SAAF;UAAaC;QAAb;MADX,IAEFO,KAFJ;;MAIA,IAAIT,kBAAkB,CAACC,SAAD,EAAYC,SAAZ,CAAtB,EAA8C;QAC1C,OAAOQ,YAAY,CAACE,EAAb,GAAkB,CAAlB,IAAuBnB,cAAc,CAACU,OAAf,KAA2B,CAAzD;MACH;;MAED,OAAO,IAAP;IACH,CAXe;IAYhBU,kBAAkB,EAAE,CAACC,CAAD,EAAIJ,YAAJ,KAAqB;MACrC,IAAIA,YAAY,CAACE,EAAb,GAAkB,CAAtB,EAAyB;QACrBd,SAAS,CAACiB,QAAV,CAAmBlB,UAAU,GAAGa,YAAY,CAACE,EAA7C;MACH;IACJ,CAhBe;IAiBhBI,qBAAqB,EAAE,CAACF,CAAD,EAAIJ,YAAJ,KAAqB;MACxC,IAAIA,YAAY,CAACE,EAAb,GAAkB,CAAlB,IAAuBF,YAAY,CAACO,EAAb,GAAkB,GAA7C,EAAkD;QAC9CC,WAAW;MACd,CAFD,MAEO;QACHC,qBAAA,CAASC,MAAT,CAAgBtB,SAAhB,EAA2B;UACvBuB,OAAO,EAAExB,UADc;UAEvByB,eAAe,EAAE,KAFM;UAGvBC,QAAQ,EAAE;QAHa,CAA3B,EAIGC,KAJH;MAKH;IACJ;EA3Be,CAApB,CADiB,EA8BlB,CAAC3B,UAAD,CA9BkB,CAArB;EAgCA,IAAA4B,gBAAA,EAAU,MAAM;IACZ,IAAI5B,UAAU,IAAI,CAAlB,EAAqB;MACjBsB,qBAAA,CAASC,MAAT,CAAgBtB,SAAhB,EAA2B;QACvBuB,OAAO,EAAExB,UADc;QAEvByB,eAAe,EAAE,KAFM;QAGvBC,QAAQ,EAAE;MAHa,CAA3B,EAIGC,KAJH;IAKH;EACJ,CARD,EAQG,CAAC3B,UAAD,CARH;;EAUA,MAAMqB,WAAW,GAAG,MAAM;IACtB,IAAIlC,QAAJ,EAAc;MACVA,QAAQ,CAAC,CAAC,CAAF,CAAR;IACH;EACJ,CAJD;;EAMA,MAAM0C,sBAAsB,GAAIjB,KAAD,IAA8B;IACzDrB,aAAa,CAACe,OAAd,GAAwBM,KAAK,CAACE,WAAN,CAAkBgB,MAA1C;EACH,CAFD;;EAIA,MAAMC,YAAY,GAAInB,KAAD,IAAoD;IACrEhB,cAAc,CAACU,OAAf,GAAyBM,KAAK,CAACE,WAAN,CAAkBkB,aAAlB,CAAgCtC,CAAzD;EACH,CAFD;;EAIA,MAAMuC,cAAc,GAAG,CACnB3C,MAAM,CAACnB,QADY,EAEnB;IAAE+D,SAAS,EAAE,CAAC;MAAElC,UAAU,EAAEC;IAAd,CAAD;EAAb,CAFmB,CAAvB;EAKA,MAAMkC,WAAW,GAAG,CAChB7C,MAAM,CAACf,KADS,EAEhB,EAAE,IAAIoB,MAAM,KAAK,CAAX,GAAe;MAAEA;IAAF,CAAf,GAA4B,EAAhC;EAAF,CAFgB,CAApB;EAKA,MAAMyC,YAAY,GAAG,EACjB,IAAIhD,UAAU,CAACF,KAAD,CAAV,KAAsB,CAAtB,GAA0B;MAAES,MAAM,EAAEP,UAAU,CAACF,KAAD;IAApB,CAA1B,GAA0D,EAA9D;EADiB,CAArB;EAIA,oBACI,6BAAC,WAAD;IACI,eAAe,EAAEH,eADrB;IAEI,OAAO,EAAEsC,WAFb;IAGI,OAAO,EAAEnC,KAAK,IAAI,CAHtB;IAII,KAAK,EAAE,IAAAmD,SAAA,EAAI,CAACC,gBAAA,CAAWC,YAAZ,EAA0BjD,MAAM,CAACvB,IAAjC,CAAJ;EAJX,gBAMI,6BAAC,qBAAD,CAAU,IAAV;IACI,KAAK,EAAEkE;EADX,GAEQ1B,YAAY,CAACiC,WAFrB,gBAII,6BAAC,WAAD;IACI,SAAS,EAAE,EADf;IAEI,KAAK,EAAEL;EAFX,gBAII,6BAAC,iBAAD;IACI,KAAK,EAAEC,YADX;IAEI,QAAQ,EAAEvC;EAFd,GAIKZ,MAJL,eAMI,6BAAC,uBAAD;IACI,QAAQ,EAAE4C,sBADd;IAEI,QAAQ,EAAEE,YAFd;IAGI,mBAAmB,EAAE,GAHzB;IAII,KAAK,EAAEzC,MAAM,CAACV;EAJlB,GAMKI,QANL,CANJ,CAJJ,CAJJ,CANJ,CADJ;AAkCH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":[],"sources":["BottomSheetProps.ts"],"sourcesContent":["import React from 'react';\nimport type { ComponentProps } from '@fountain-ui/core';\n\nexport default interface BottomSheetProps extends ComponentProps<{\n /**\n * Opacity for BackdropComponent\n * @default 0.5\n */\n backdropOpacity?: number;\n\n /**\n * BottomSheet children, usually the included sub-components.\n */\n children?: React.ReactNode;\n\n /**\n * Snap index. You could also provide -1 to bottom sheet in closed state.\n */\n index: number;\n\n /**\n * Callback fired when the index is changed.\n * Important! Use memoized value.\n */\n onChange?: (newIndex: number) => void;\n\n /**\n * Points for the bottom sheet to snap to, points should be sorted from bottom to top.\n * Important! Use memoized value.\n * Only number type or string type(~% format) can be used.\n */\n snapPoints: Array<number | string>;\n}> {}\n"],"mappings":""}
|
|
1
|
+
{"version":3,"names":[],"sources":["BottomSheetProps.ts"],"sourcesContent":["import React from 'react';\nimport type { ComponentProps } from '@fountain-ui/core';\n\nexport default interface BottomSheetProps extends ComponentProps<{\n /**\n * Opacity for BackdropComponent\n * @default 0.5\n */\n backdropOpacity?: number;\n\n /**\n * BottomSheet children, usually the included sub-components.\n */\n children?: React.ReactNode;\n\n /**\n * Area to be fixed on the top of the bottom sheet.\n */\n header?: React.ReactNode;\n\n /**\n * Snap index. You could also provide -1 to bottom sheet in closed state.\n */\n index: number;\n\n /**\n * Callback fired when the index is changed.\n * Important! Use memoized value.\n */\n onChange?: (newIndex: number) => void;\n\n /**\n * Points for the bottom sheet to snap to, points should be sorted from bottom to top.\n * Important! Use memoized value.\n * Only number type or string type(~% format) can be used.\n */\n snapPoints: Array<number | string>;\n}> {}\n"],"mappings":""}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = useDynamicSnapPoints;
|
|
7
|
+
|
|
8
|
+
var _react = require("react");
|
|
9
|
+
|
|
10
|
+
var _reactNative = require("react-native");
|
|
11
|
+
|
|
12
|
+
const convertHeightAsPixel = (windowHeight, value) => {
|
|
13
|
+
if (typeof value === 'number') {
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const percentageRegex = new RegExp(/^[0-9]+%$/);
|
|
18
|
+
|
|
19
|
+
if (percentageRegex.test(value)) {
|
|
20
|
+
const percentage = parseFloat(value) / 100;
|
|
21
|
+
return isNaN(percentage) ? 0 : Math.round(windowHeight * percentage);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return 0;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function useDynamicSnapPoints(props) {
|
|
28
|
+
const {
|
|
29
|
+
index: currentIndex,
|
|
30
|
+
initialSnapPoints
|
|
31
|
+
} = props;
|
|
32
|
+
const {
|
|
33
|
+
height: WINDOW_HEIGHT
|
|
34
|
+
} = (0, _reactNative.useWindowDimensions)();
|
|
35
|
+
const [snapPoints, setSnapPoints] = (0, _react.useState)(initialSnapPoints.map(snapPoint => convertHeightAsPixel(WINDOW_HEIGHT, snapPoint)));
|
|
36
|
+
const handleContentLayout = (0, _react.useCallback)(event => {
|
|
37
|
+
if (initialSnapPoints[currentIndex] === 'CONTENT_HEIGHT') {
|
|
38
|
+
const contentHeight = Math.min(Math.round(WINDOW_HEIGHT * 0.9), event.nativeEvent.layout.height);
|
|
39
|
+
setSnapPoints(prevState => prevState.map((snapPoint, index) => {
|
|
40
|
+
return currentIndex === index ? contentHeight : snapPoint;
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
}, [currentIndex, initialSnapPoints]);
|
|
44
|
+
return {
|
|
45
|
+
handleContentLayout,
|
|
46
|
+
snapPoints
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=useDynamicSnapPoints.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["convertHeightAsPixel","windowHeight","value","percentageRegex","RegExp","test","percentage","parseFloat","isNaN","Math","round","useDynamicSnapPoints","props","index","currentIndex","initialSnapPoints","height","WINDOW_HEIGHT","useWindowDimensions","snapPoints","setSnapPoints","useState","map","snapPoint","handleContentLayout","useCallback","event","contentHeight","min","nativeEvent","layout","prevState"],"sources":["useDynamicSnapPoints.ts"],"sourcesContent":["import { useCallback, useState } from 'react';\nimport { LayoutChangeEvent, useWindowDimensions } from 'react-native';\n\ninterface UseDynamicSnapPointsParams {\n index: number;\n initialSnapPoints: (number | string)[];\n}\n\ninterface UseDynamicSnapPointsReturns {\n handleContentLayout: (e: LayoutChangeEvent) => void;\n snapPoints: number[];\n}\n\nconst convertHeightAsPixel = (windowHeight: number, value: number | string): number => {\n if (typeof value === 'number') {\n return value;\n }\n\n const percentageRegex = new RegExp(/^[0-9]+%$/);\n if (percentageRegex.test(value)) {\n const percentage = parseFloat(value) / 100;\n return isNaN(percentage) ? 0 : Math.round(windowHeight * percentage);\n }\n\n return 0;\n};\n\nexport default function useDynamicSnapPoints(props: UseDynamicSnapPointsParams): UseDynamicSnapPointsReturns {\n const {\n index: currentIndex,\n initialSnapPoints,\n } = props;\n\n const { height: WINDOW_HEIGHT } = useWindowDimensions();\n\n const [snapPoints, setSnapPoints] = useState(initialSnapPoints.map((snapPoint) => convertHeightAsPixel(WINDOW_HEIGHT, snapPoint)));\n\n const handleContentLayout = useCallback((event: LayoutChangeEvent) => {\n if (initialSnapPoints[currentIndex] === 'CONTENT_HEIGHT') {\n const contentHeight = Math.min(Math.round(WINDOW_HEIGHT * 0.9), event.nativeEvent.layout.height);\n \n setSnapPoints(prevState => prevState.map((snapPoint, index) => {\n return currentIndex === index ? contentHeight : snapPoint;\n }));\n }\n }, [currentIndex, initialSnapPoints]);\n\n return {\n handleContentLayout,\n snapPoints,\n };\n}\n"],"mappings":";;;;;;;AAAA;;AACA;;AAYA,MAAMA,oBAAoB,GAAG,CAACC,YAAD,EAAuBC,KAAvB,KAA0D;EACnF,IAAI,OAAOA,KAAP,KAAiB,QAArB,EAA+B;IAC3B,OAAOA,KAAP;EACH;;EAED,MAAMC,eAAe,GAAG,IAAIC,MAAJ,CAAW,WAAX,CAAxB;;EACA,IAAID,eAAe,CAACE,IAAhB,CAAqBH,KAArB,CAAJ,EAAiC;IAC7B,MAAMI,UAAU,GAAGC,UAAU,CAACL,KAAD,CAAV,GAAoB,GAAvC;IACA,OAAOM,KAAK,CAACF,UAAD,CAAL,GAAoB,CAApB,GAAwBG,IAAI,CAACC,KAAL,CAAWT,YAAY,GAAGK,UAA1B,CAA/B;EACH;;EAED,OAAO,CAAP;AACH,CAZD;;AAce,SAASK,oBAAT,CAA8BC,KAA9B,EAA8F;EACzG,MAAM;IACFC,KAAK,EAAEC,YADL;IAEFC;EAFE,IAGFH,KAHJ;EAKA,MAAM;IAAEI,MAAM,EAAEC;EAAV,IAA4B,IAAAC,gCAAA,GAAlC;EAEA,MAAM,CAACC,UAAD,EAAaC,aAAb,IAA8B,IAAAC,eAAA,EAASN,iBAAiB,CAACO,GAAlB,CAAuBC,SAAD,IAAevB,oBAAoB,CAACiB,aAAD,EAAgBM,SAAhB,CAAzD,CAAT,CAApC;EAEA,MAAMC,mBAAmB,GAAG,IAAAC,kBAAA,EAAaC,KAAD,IAA8B;IAClE,IAAIX,iBAAiB,CAACD,YAAD,CAAjB,KAAoC,gBAAxC,EAA0D;MACtD,MAAMa,aAAa,GAAGlB,IAAI,CAACmB,GAAL,CAASnB,IAAI,CAACC,KAAL,CAAWO,aAAa,GAAG,GAA3B,CAAT,EAA0CS,KAAK,CAACG,WAAN,CAAkBC,MAAlB,CAAyBd,MAAnE,CAAtB;MAEAI,aAAa,CAACW,SAAS,IAAIA,SAAS,CAACT,GAAV,CAAc,CAACC,SAAD,EAAYV,KAAZ,KAAsB;QAC3D,OAAOC,YAAY,KAAKD,KAAjB,GAAyBc,aAAzB,GAAyCJ,SAAhD;MACH,CAF0B,CAAd,CAAb;IAGH;EACJ,CAR2B,EAQzB,CAACT,YAAD,EAAeC,iBAAf,CARyB,CAA5B;EAUA,OAAO;IACHS,mBADG;IAEHL;EAFG,CAAP;AAIH"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useMemo, useRef } from 'react';
|
|
4
|
+
import { Animated, PanResponder, ScrollView, View } from 'react-native';
|
|
5
|
+
import { css, Modal, Paper, StyleSheet, useAnimatedValue, useTheme } from '@fountain-ui/core';
|
|
6
|
+
import useDynamicSnapPoints from './useDynamicSnapPoints';
|
|
7
|
+
|
|
8
|
+
const useStyles = function () {
|
|
9
|
+
const theme = useTheme();
|
|
10
|
+
return {
|
|
11
|
+
root: {
|
|
12
|
+
justifyContent: 'flex-end',
|
|
13
|
+
zIndex: theme.zIndex.dialog
|
|
14
|
+
},
|
|
15
|
+
animated: {
|
|
16
|
+
alignSelf: 'center',
|
|
17
|
+
maxWidth: 720,
|
|
18
|
+
width: '100%'
|
|
19
|
+
},
|
|
20
|
+
paper: {
|
|
21
|
+
borderBottomLeftRadius: 0,
|
|
22
|
+
borderBottomRightRadius: 0,
|
|
23
|
+
flexGrow: 1,
|
|
24
|
+
overflow: 'hidden'
|
|
25
|
+
},
|
|
26
|
+
scrollView: {
|
|
27
|
+
flexGrow: 1
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default function BottomSheet(props) {
|
|
33
|
+
const {
|
|
34
|
+
backdropOpacity,
|
|
35
|
+
children,
|
|
36
|
+
header,
|
|
37
|
+
index,
|
|
38
|
+
onChange,
|
|
39
|
+
snapPoints: initialSnapPoints
|
|
40
|
+
} = props;
|
|
41
|
+
const styles = useStyles();
|
|
42
|
+
const contentLayout = useRef({
|
|
43
|
+
x: 0,
|
|
44
|
+
y: 0,
|
|
45
|
+
width: 0,
|
|
46
|
+
height: 0
|
|
47
|
+
});
|
|
48
|
+
const currentScrollY = useRef(0);
|
|
49
|
+
const {
|
|
50
|
+
snapPoints,
|
|
51
|
+
handleContentLayout
|
|
52
|
+
} = useDynamicSnapPoints({
|
|
53
|
+
index,
|
|
54
|
+
initialSnapPoints
|
|
55
|
+
});
|
|
56
|
+
const height = snapPoints[snapPoints.length - 1] ?? 0;
|
|
57
|
+
const translateY = height - (snapPoints[index] ?? 0);
|
|
58
|
+
const animatedY = useAnimatedValue(translateY);
|
|
59
|
+
|
|
60
|
+
const isInsideScrollView = (locationX, locationY) => {
|
|
61
|
+
const {
|
|
62
|
+
current: {
|
|
63
|
+
x,
|
|
64
|
+
y,
|
|
65
|
+
width,
|
|
66
|
+
height
|
|
67
|
+
}
|
|
68
|
+
} = contentLayout;
|
|
69
|
+
return locationX >= x && locationX <= x + width && locationY >= y && locationY <= y + height;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const panResponder = useMemo(() => PanResponder.create({
|
|
73
|
+
onMoveShouldSetPanResponder: (event, gestureState) => {
|
|
74
|
+
const {
|
|
75
|
+
nativeEvent: {
|
|
76
|
+
locationX,
|
|
77
|
+
locationY
|
|
78
|
+
}
|
|
79
|
+
} = event;
|
|
80
|
+
|
|
81
|
+
if (isInsideScrollView(locationX, locationY)) {
|
|
82
|
+
return gestureState.dy > 0 && currentScrollY.current === 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return true;
|
|
86
|
+
},
|
|
87
|
+
onPanResponderMove: (_, gestureState) => {
|
|
88
|
+
if (gestureState.dy > 0) {
|
|
89
|
+
animatedY.setValue(translateY + gestureState.dy);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
onPanResponderRelease: (_, gestureState) => {
|
|
93
|
+
if (gestureState.dy > 0 && gestureState.vy > 0.5) {
|
|
94
|
+
handleClose();
|
|
95
|
+
} else {
|
|
96
|
+
Animated.timing(animatedY, {
|
|
97
|
+
toValue: translateY,
|
|
98
|
+
useNativeDriver: false,
|
|
99
|
+
duration: 300
|
|
100
|
+
}).start();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}), [translateY]);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (translateY >= 0) {
|
|
106
|
+
Animated.timing(animatedY, {
|
|
107
|
+
toValue: translateY,
|
|
108
|
+
useNativeDriver: false,
|
|
109
|
+
duration: 300
|
|
110
|
+
}).start();
|
|
111
|
+
}
|
|
112
|
+
}, [translateY]);
|
|
113
|
+
|
|
114
|
+
const handleClose = () => {
|
|
115
|
+
if (onChange) {
|
|
116
|
+
onChange(-1);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const handleScrollViewLayout = event => {
|
|
121
|
+
contentLayout.current = event.nativeEvent.layout;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleScroll = event => {
|
|
125
|
+
currentScrollY.current = event.nativeEvent.contentOffset.y;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const animatedStyles = [styles.animated, {
|
|
129
|
+
transform: [{
|
|
130
|
+
translateY: animatedY
|
|
131
|
+
}]
|
|
132
|
+
}];
|
|
133
|
+
const paperStyles = [styles.paper, { ...(height !== 0 ? {
|
|
134
|
+
height
|
|
135
|
+
} : {})
|
|
136
|
+
}];
|
|
137
|
+
const contentStyle = { ...(snapPoints[index] !== 0 ? {
|
|
138
|
+
height: snapPoints[index]
|
|
139
|
+
} : {})
|
|
140
|
+
};
|
|
141
|
+
return /*#__PURE__*/React.createElement(Modal, {
|
|
142
|
+
backdropOpacity: backdropOpacity,
|
|
143
|
+
onClose: handleClose,
|
|
144
|
+
visible: index >= 0,
|
|
145
|
+
style: css([StyleSheet.absoluteFill, styles.root])
|
|
146
|
+
}, /*#__PURE__*/React.createElement(Animated.View, _extends({
|
|
147
|
+
style: animatedStyles
|
|
148
|
+
}, panResponder.panHandlers), /*#__PURE__*/React.createElement(Paper, {
|
|
149
|
+
elevation: 12,
|
|
150
|
+
style: paperStyles
|
|
151
|
+
}, /*#__PURE__*/React.createElement(View, {
|
|
152
|
+
style: contentStyle,
|
|
153
|
+
onLayout: handleContentLayout
|
|
154
|
+
}, header, /*#__PURE__*/React.createElement(ScrollView, {
|
|
155
|
+
onLayout: handleScrollViewLayout,
|
|
156
|
+
onScroll: handleScroll,
|
|
157
|
+
scrollEventThrottle: 300,
|
|
158
|
+
style: styles.scrollView
|
|
159
|
+
}, children)))));
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=BottomSheet.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["React","useEffect","useMemo","useRef","Animated","PanResponder","ScrollView","View","css","Modal","Paper","StyleSheet","useAnimatedValue","useTheme","useDynamicSnapPoints","useStyles","theme","root","justifyContent","zIndex","dialog","animated","alignSelf","maxWidth","width","paper","borderBottomLeftRadius","borderBottomRightRadius","flexGrow","overflow","scrollView","BottomSheet","props","backdropOpacity","children","header","index","onChange","snapPoints","initialSnapPoints","styles","contentLayout","x","y","height","currentScrollY","handleContentLayout","length","translateY","animatedY","isInsideScrollView","locationX","locationY","current","panResponder","create","onMoveShouldSetPanResponder","event","gestureState","nativeEvent","dy","onPanResponderMove","_","setValue","onPanResponderRelease","vy","handleClose","timing","toValue","useNativeDriver","duration","start","handleScrollViewLayout","layout","handleScroll","contentOffset","animatedStyles","transform","paperStyles","contentStyle","absoluteFill","panHandlers"],"sources":["BottomSheet.tsx"],"sourcesContent":["import React, { useEffect, useMemo, useRef } from 'react';\nimport {\n Animated,\n LayoutChangeEvent,\n LayoutRectangle,\n NativeScrollEvent,\n NativeSyntheticEvent,\n PanResponder,\n ScrollView,\n View,\n} from 'react-native';\nimport { css, Modal, Paper, StyleSheet, useAnimatedValue, useTheme } from '@fountain-ui/core';\nimport { NamedStylesStringUnion, UseStyles } from '@fountain-ui/styles';\nimport BottomSheetProps from './BottomSheetProps';\nimport useDynamicSnapPoints from './useDynamicSnapPoints';\n\ntype BottomSheetStyles = NamedStylesStringUnion<'root' | 'animated' | 'paper' | 'scrollView'>;\n\nconst useStyles: UseStyles<BottomSheetStyles> = function (): BottomSheetStyles {\n const theme = useTheme();\n\n return {\n root: {\n justifyContent: 'flex-end',\n zIndex: theme.zIndex.dialog,\n },\n animated: {\n alignSelf: 'center',\n maxWidth: 720,\n width: '100%',\n },\n paper: {\n borderBottomLeftRadius: 0,\n borderBottomRightRadius: 0,\n flexGrow: 1,\n overflow: 'hidden',\n },\n scrollView: {\n flexGrow: 1,\n },\n };\n};\n\nexport default function BottomSheet(props: BottomSheetProps) {\n const {\n backdropOpacity,\n children,\n header,\n index,\n onChange,\n snapPoints: initialSnapPoints,\n } = props;\n\n const styles = useStyles();\n\n const contentLayout = useRef<LayoutRectangle>({ x: 0, y: 0, width: 0, height: 0 });\n const currentScrollY = useRef<number>(0);\n\n const {\n snapPoints,\n handleContentLayout,\n } = useDynamicSnapPoints({\n index,\n initialSnapPoints,\n });\n\n const height = snapPoints[snapPoints.length - 1] ?? 0;\n const translateY = height - (snapPoints[index] ?? 0);\n\n const animatedY = useAnimatedValue(translateY);\n\n const isInsideScrollView = (locationX: number, locationY: number): boolean => {\n const {\n current: { x, y, width, height },\n } = contentLayout;\n\n return locationX >= x && locationX <= x + width && locationY >= y && locationY <= y + height;\n };\n\n const panResponder = useMemo(() => (\n PanResponder.create({\n onMoveShouldSetPanResponder: (event, gestureState) => {\n const {\n nativeEvent: { locationX, locationY },\n } = event;\n\n if (isInsideScrollView(locationX, locationY)) {\n return gestureState.dy > 0 && currentScrollY.current === 0;\n }\n\n return true;\n },\n onPanResponderMove: (_, gestureState) => {\n if (gestureState.dy > 0) {\n animatedY.setValue(translateY + gestureState.dy);\n }\n },\n onPanResponderRelease: (_, gestureState) => {\n if (gestureState.dy > 0 && gestureState.vy > 0.5) {\n handleClose();\n } else {\n Animated.timing(animatedY, {\n toValue: translateY,\n useNativeDriver: false,\n duration: 300,\n }).start();\n }\n },\n })\n ), [translateY]);\n\n useEffect(() => {\n if (translateY >= 0) {\n Animated.timing(animatedY, {\n toValue: translateY,\n useNativeDriver: false,\n duration: 300,\n }).start();\n }\n }, [translateY]);\n\n const handleClose = () => {\n if (onChange) {\n onChange(-1);\n }\n };\n\n const handleScrollViewLayout = (event: LayoutChangeEvent) => {\n contentLayout.current = event.nativeEvent.layout;\n };\n\n const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {\n currentScrollY.current = event.nativeEvent.contentOffset.y;\n };\n\n const animatedStyles = [\n styles.animated,\n { transform: [{ translateY: animatedY }] },\n ];\n\n const paperStyles = [\n styles.paper,\n { ...(height !== 0 ? { height } : {}) },\n ];\n\n const contentStyle = {\n ...(snapPoints[index] !== 0 ? { height: snapPoints[index] } : {}),\n };\n\n return (\n <Modal\n backdropOpacity={backdropOpacity}\n onClose={handleClose}\n visible={index >= 0}\n style={css([StyleSheet.absoluteFill, styles.root])}\n >\n <Animated.View\n style={animatedStyles}\n {...panResponder.panHandlers}\n >\n <Paper\n elevation={12}\n style={paperStyles}\n >\n <View\n style={contentStyle}\n onLayout={handleContentLayout}\n >\n {header}\n\n <ScrollView\n onLayout={handleScrollViewLayout}\n onScroll={handleScroll}\n scrollEventThrottle={300}\n style={styles.scrollView}\n >\n {children}\n </ScrollView>\n </View>\n </Paper>\n </Animated.View>\n </Modal>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":[],"sources":["BottomSheetProps.ts"],"sourcesContent":["import React from 'react';\nimport type { ComponentProps } from '@fountain-ui/core';\n\nexport default interface BottomSheetProps extends ComponentProps<{\n /**\n * Opacity for BackdropComponent\n * @default 0.5\n */\n backdropOpacity?: number;\n\n /**\n * BottomSheet children, usually the included sub-components.\n */\n children?: React.ReactNode;\n\n /**\n * Snap index. You could also provide -1 to bottom sheet in closed state.\n */\n index: number;\n\n /**\n * Callback fired when the index is changed.\n * Important! Use memoized value.\n */\n onChange?: (newIndex: number) => void;\n\n /**\n * Points for the bottom sheet to snap to, points should be sorted from bottom to top.\n * Important! Use memoized value.\n * Only number type or string type(~% format) can be used.\n */\n snapPoints: Array<number | string>;\n}> {}\n"],"mappings":""}
|
|
1
|
+
{"version":3,"names":[],"sources":["BottomSheetProps.ts"],"sourcesContent":["import React from 'react';\nimport type { ComponentProps } from '@fountain-ui/core';\n\nexport default interface BottomSheetProps extends ComponentProps<{\n /**\n * Opacity for BackdropComponent\n * @default 0.5\n */\n backdropOpacity?: number;\n\n /**\n * BottomSheet children, usually the included sub-components.\n */\n children?: React.ReactNode;\n\n /**\n * Area to be fixed on the top of the bottom sheet.\n */\n header?: React.ReactNode;\n\n /**\n * Snap index. You could also provide -1 to bottom sheet in closed state.\n */\n index: number;\n\n /**\n * Callback fired when the index is changed.\n * Important! Use memoized value.\n */\n onChange?: (newIndex: number) => void;\n\n /**\n * Points for the bottom sheet to snap to, points should be sorted from bottom to top.\n * Important! Use memoized value.\n * Only number type or string type(~% format) can be used.\n */\n snapPoints: Array<number | string>;\n}> {}\n"],"mappings":""}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import { useWindowDimensions } from 'react-native';
|
|
3
|
+
|
|
4
|
+
const convertHeightAsPixel = (windowHeight, value) => {
|
|
5
|
+
if (typeof value === 'number') {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const percentageRegex = new RegExp(/^[0-9]+%$/);
|
|
10
|
+
|
|
11
|
+
if (percentageRegex.test(value)) {
|
|
12
|
+
const percentage = parseFloat(value) / 100;
|
|
13
|
+
return isNaN(percentage) ? 0 : Math.round(windowHeight * percentage);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return 0;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default function useDynamicSnapPoints(props) {
|
|
20
|
+
const {
|
|
21
|
+
index: currentIndex,
|
|
22
|
+
initialSnapPoints
|
|
23
|
+
} = props;
|
|
24
|
+
const {
|
|
25
|
+
height: WINDOW_HEIGHT
|
|
26
|
+
} = useWindowDimensions();
|
|
27
|
+
const [snapPoints, setSnapPoints] = useState(initialSnapPoints.map(snapPoint => convertHeightAsPixel(WINDOW_HEIGHT, snapPoint)));
|
|
28
|
+
const handleContentLayout = useCallback(event => {
|
|
29
|
+
if (initialSnapPoints[currentIndex] === 'CONTENT_HEIGHT') {
|
|
30
|
+
const contentHeight = Math.min(Math.round(WINDOW_HEIGHT * 0.9), event.nativeEvent.layout.height);
|
|
31
|
+
setSnapPoints(prevState => prevState.map((snapPoint, index) => {
|
|
32
|
+
return currentIndex === index ? contentHeight : snapPoint;
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
}, [currentIndex, initialSnapPoints]);
|
|
36
|
+
return {
|
|
37
|
+
handleContentLayout,
|
|
38
|
+
snapPoints
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=useDynamicSnapPoints.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["useCallback","useState","useWindowDimensions","convertHeightAsPixel","windowHeight","value","percentageRegex","RegExp","test","percentage","parseFloat","isNaN","Math","round","useDynamicSnapPoints","props","index","currentIndex","initialSnapPoints","height","WINDOW_HEIGHT","snapPoints","setSnapPoints","map","snapPoint","handleContentLayout","event","contentHeight","min","nativeEvent","layout","prevState"],"sources":["useDynamicSnapPoints.ts"],"sourcesContent":["import { useCallback, useState } from 'react';\nimport { LayoutChangeEvent, useWindowDimensions } from 'react-native';\n\ninterface UseDynamicSnapPointsParams {\n index: number;\n initialSnapPoints: (number | string)[];\n}\n\ninterface UseDynamicSnapPointsReturns {\n handleContentLayout: (e: LayoutChangeEvent) => void;\n snapPoints: number[];\n}\n\nconst convertHeightAsPixel = (windowHeight: number, value: number | string): number => {\n if (typeof value === 'number') {\n return value;\n }\n\n const percentageRegex = new RegExp(/^[0-9]+%$/);\n if (percentageRegex.test(value)) {\n const percentage = parseFloat(value) / 100;\n return isNaN(percentage) ? 0 : Math.round(windowHeight * percentage);\n }\n\n return 0;\n};\n\nexport default function useDynamicSnapPoints(props: UseDynamicSnapPointsParams): UseDynamicSnapPointsReturns {\n const {\n index: currentIndex,\n initialSnapPoints,\n } = props;\n\n const { height: WINDOW_HEIGHT } = useWindowDimensions();\n\n const [snapPoints, setSnapPoints] = useState(initialSnapPoints.map((snapPoint) => convertHeightAsPixel(WINDOW_HEIGHT, snapPoint)));\n\n const handleContentLayout = useCallback((event: LayoutChangeEvent) => {\n if (initialSnapPoints[currentIndex] === 'CONTENT_HEIGHT') {\n const contentHeight = Math.min(Math.round(WINDOW_HEIGHT * 0.9), event.nativeEvent.layout.height);\n \n setSnapPoints(prevState => prevState.map((snapPoint, index) => {\n return currentIndex === index ? contentHeight : snapPoint;\n }));\n }\n }, [currentIndex, initialSnapPoints]);\n\n return {\n handleContentLayout,\n snapPoints,\n };\n}\n"],"mappings":"AAAA,SAASA,WAAT,EAAsBC,QAAtB,QAAsC,OAAtC;AACA,SAA4BC,mBAA5B,QAAuD,cAAvD;;AAYA,MAAMC,oBAAoB,GAAG,CAACC,YAAD,EAAuBC,KAAvB,KAA0D;EACnF,IAAI,OAAOA,KAAP,KAAiB,QAArB,EAA+B;IAC3B,OAAOA,KAAP;EACH;;EAED,MAAMC,eAAe,GAAG,IAAIC,MAAJ,CAAW,WAAX,CAAxB;;EACA,IAAID,eAAe,CAACE,IAAhB,CAAqBH,KAArB,CAAJ,EAAiC;IAC7B,MAAMI,UAAU,GAAGC,UAAU,CAACL,KAAD,CAAV,GAAoB,GAAvC;IACA,OAAOM,KAAK,CAACF,UAAD,CAAL,GAAoB,CAApB,GAAwBG,IAAI,CAACC,KAAL,CAAWT,YAAY,GAAGK,UAA1B,CAA/B;EACH;;EAED,OAAO,CAAP;AACH,CAZD;;AAcA,eAAe,SAASK,oBAAT,CAA8BC,KAA9B,EAA8F;EACzG,MAAM;IACFC,KAAK,EAAEC,YADL;IAEFC;EAFE,IAGFH,KAHJ;EAKA,MAAM;IAAEI,MAAM,EAAEC;EAAV,IAA4BlB,mBAAmB,EAArD;EAEA,MAAM,CAACmB,UAAD,EAAaC,aAAb,IAA8BrB,QAAQ,CAACiB,iBAAiB,CAACK,GAAlB,CAAuBC,SAAD,IAAerB,oBAAoB,CAACiB,aAAD,EAAgBI,SAAhB,CAAzD,CAAD,CAA5C;EAEA,MAAMC,mBAAmB,GAAGzB,WAAW,CAAE0B,KAAD,IAA8B;IAClE,IAAIR,iBAAiB,CAACD,YAAD,CAAjB,KAAoC,gBAAxC,EAA0D;MACtD,MAAMU,aAAa,GAAGf,IAAI,CAACgB,GAAL,CAAShB,IAAI,CAACC,KAAL,CAAWO,aAAa,GAAG,GAA3B,CAAT,EAA0CM,KAAK,CAACG,WAAN,CAAkBC,MAAlB,CAAyBX,MAAnE,CAAtB;MAEAG,aAAa,CAACS,SAAS,IAAIA,SAAS,CAACR,GAAV,CAAc,CAACC,SAAD,EAAYR,KAAZ,KAAsB;QAC3D,OAAOC,YAAY,KAAKD,KAAjB,GAAyBW,aAAzB,GAAyCH,SAAhD;MACH,CAF0B,CAAd,CAAb;IAGH;EACJ,CARsC,EAQpC,CAACP,YAAD,EAAeC,iBAAf,CARoC,CAAvC;EAUA,OAAO;IACHO,mBADG;IAEHJ;EAFG,CAAP;AAIH"}
|
|
@@ -10,6 +10,10 @@ export default interface BottomSheetProps extends ComponentProps<{
|
|
|
10
10
|
* BottomSheet children, usually the included sub-components.
|
|
11
11
|
*/
|
|
12
12
|
children?: React.ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* Area to be fixed on the top of the bottom sheet.
|
|
15
|
+
*/
|
|
16
|
+
header?: React.ReactNode;
|
|
13
17
|
/**
|
|
14
18
|
* Snap index. You could also provide -1 to bottom sheet in closed state.
|
|
15
19
|
*/
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { LayoutChangeEvent } from 'react-native';
|
|
2
|
+
interface UseDynamicSnapPointsParams {
|
|
3
|
+
index: number;
|
|
4
|
+
initialSnapPoints: (number | string)[];
|
|
5
|
+
}
|
|
6
|
+
interface UseDynamicSnapPointsReturns {
|
|
7
|
+
handleContentLayout: (e: LayoutChangeEvent) => void;
|
|
8
|
+
snapPoints: number[];
|
|
9
|
+
}
|
|
10
|
+
export default function useDynamicSnapPoints(props: UseDynamicSnapPointsParams): UseDynamicSnapPointsReturns;
|
|
11
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fountain-ui/lab",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.29",
|
|
4
4
|
"private": false,
|
|
5
5
|
"author": "Fountain-UI Team",
|
|
6
6
|
"description": "Incubator for Fountain-UI React components.",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@emotion/react": "^11.10.0",
|
|
19
19
|
"@emotion/styled": "^11.10.0",
|
|
20
|
-
"@fountain-ui/icons": "^2.0.0-beta.
|
|
20
|
+
"@fountain-ui/icons": "^2.0.0-beta.8",
|
|
21
21
|
"@fountain-ui/utils": "^2.0.0-beta.4",
|
|
22
22
|
"react-native-calendars": "1.1267.0"
|
|
23
23
|
},
|
|
@@ -70,5 +70,5 @@
|
|
|
70
70
|
"publishConfig": {
|
|
71
71
|
"access": "public"
|
|
72
72
|
},
|
|
73
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "019b85d550c729ccca7f3877d6aebe3fa02bd2ec"
|
|
74
74
|
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
LayoutChangeEvent,
|
|
5
|
+
LayoutRectangle,
|
|
6
|
+
NativeScrollEvent,
|
|
7
|
+
NativeSyntheticEvent,
|
|
8
|
+
PanResponder,
|
|
9
|
+
ScrollView,
|
|
10
|
+
View,
|
|
11
|
+
} from 'react-native';
|
|
12
|
+
import { css, Modal, Paper, StyleSheet, useAnimatedValue, useTheme } from '@fountain-ui/core';
|
|
13
|
+
import { NamedStylesStringUnion, UseStyles } from '@fountain-ui/styles';
|
|
14
|
+
import BottomSheetProps from './BottomSheetProps';
|
|
15
|
+
import useDynamicSnapPoints from './useDynamicSnapPoints';
|
|
16
|
+
|
|
17
|
+
type BottomSheetStyles = NamedStylesStringUnion<'root' | 'animated' | 'paper' | 'scrollView'>;
|
|
18
|
+
|
|
19
|
+
const useStyles: UseStyles<BottomSheetStyles> = function (): BottomSheetStyles {
|
|
20
|
+
const theme = useTheme();
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
root: {
|
|
24
|
+
justifyContent: 'flex-end',
|
|
25
|
+
zIndex: theme.zIndex.dialog,
|
|
26
|
+
},
|
|
27
|
+
animated: {
|
|
28
|
+
alignSelf: 'center',
|
|
29
|
+
maxWidth: 720,
|
|
30
|
+
width: '100%',
|
|
31
|
+
},
|
|
32
|
+
paper: {
|
|
33
|
+
borderBottomLeftRadius: 0,
|
|
34
|
+
borderBottomRightRadius: 0,
|
|
35
|
+
flexGrow: 1,
|
|
36
|
+
overflow: 'hidden',
|
|
37
|
+
},
|
|
38
|
+
scrollView: {
|
|
39
|
+
flexGrow: 1,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default function BottomSheet(props: BottomSheetProps) {
|
|
45
|
+
const {
|
|
46
|
+
backdropOpacity,
|
|
47
|
+
children,
|
|
48
|
+
header,
|
|
49
|
+
index,
|
|
50
|
+
onChange,
|
|
51
|
+
snapPoints: initialSnapPoints,
|
|
52
|
+
} = props;
|
|
53
|
+
|
|
54
|
+
const styles = useStyles();
|
|
55
|
+
|
|
56
|
+
const contentLayout = useRef<LayoutRectangle>({ x: 0, y: 0, width: 0, height: 0 });
|
|
57
|
+
const currentScrollY = useRef<number>(0);
|
|
58
|
+
|
|
59
|
+
const {
|
|
60
|
+
snapPoints,
|
|
61
|
+
handleContentLayout,
|
|
62
|
+
} = useDynamicSnapPoints({
|
|
63
|
+
index,
|
|
64
|
+
initialSnapPoints,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const height = snapPoints[snapPoints.length - 1] ?? 0;
|
|
68
|
+
const translateY = height - (snapPoints[index] ?? 0);
|
|
69
|
+
|
|
70
|
+
const animatedY = useAnimatedValue(translateY);
|
|
71
|
+
|
|
72
|
+
const isInsideScrollView = (locationX: number, locationY: number): boolean => {
|
|
73
|
+
const {
|
|
74
|
+
current: { x, y, width, height },
|
|
75
|
+
} = contentLayout;
|
|
76
|
+
|
|
77
|
+
return locationX >= x && locationX <= x + width && locationY >= y && locationY <= y + height;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const panResponder = useMemo(() => (
|
|
81
|
+
PanResponder.create({
|
|
82
|
+
onMoveShouldSetPanResponder: (event, gestureState) => {
|
|
83
|
+
const {
|
|
84
|
+
nativeEvent: { locationX, locationY },
|
|
85
|
+
} = event;
|
|
86
|
+
|
|
87
|
+
if (isInsideScrollView(locationX, locationY)) {
|
|
88
|
+
return gestureState.dy > 0 && currentScrollY.current === 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return true;
|
|
92
|
+
},
|
|
93
|
+
onPanResponderMove: (_, gestureState) => {
|
|
94
|
+
if (gestureState.dy > 0) {
|
|
95
|
+
animatedY.setValue(translateY + gestureState.dy);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
onPanResponderRelease: (_, gestureState) => {
|
|
99
|
+
if (gestureState.dy > 0 && gestureState.vy > 0.5) {
|
|
100
|
+
handleClose();
|
|
101
|
+
} else {
|
|
102
|
+
Animated.timing(animatedY, {
|
|
103
|
+
toValue: translateY,
|
|
104
|
+
useNativeDriver: false,
|
|
105
|
+
duration: 300,
|
|
106
|
+
}).start();
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
), [translateY]);
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (translateY >= 0) {
|
|
114
|
+
Animated.timing(animatedY, {
|
|
115
|
+
toValue: translateY,
|
|
116
|
+
useNativeDriver: false,
|
|
117
|
+
duration: 300,
|
|
118
|
+
}).start();
|
|
119
|
+
}
|
|
120
|
+
}, [translateY]);
|
|
121
|
+
|
|
122
|
+
const handleClose = () => {
|
|
123
|
+
if (onChange) {
|
|
124
|
+
onChange(-1);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const handleScrollViewLayout = (event: LayoutChangeEvent) => {
|
|
129
|
+
contentLayout.current = event.nativeEvent.layout;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
133
|
+
currentScrollY.current = event.nativeEvent.contentOffset.y;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const animatedStyles = [
|
|
137
|
+
styles.animated,
|
|
138
|
+
{ transform: [{ translateY: animatedY }] },
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const paperStyles = [
|
|
142
|
+
styles.paper,
|
|
143
|
+
{ ...(height !== 0 ? { height } : {}) },
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
const contentStyle = {
|
|
147
|
+
...(snapPoints[index] !== 0 ? { height: snapPoints[index] } : {}),
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<Modal
|
|
152
|
+
backdropOpacity={backdropOpacity}
|
|
153
|
+
onClose={handleClose}
|
|
154
|
+
visible={index >= 0}
|
|
155
|
+
style={css([StyleSheet.absoluteFill, styles.root])}
|
|
156
|
+
>
|
|
157
|
+
<Animated.View
|
|
158
|
+
style={animatedStyles}
|
|
159
|
+
{...panResponder.panHandlers}
|
|
160
|
+
>
|
|
161
|
+
<Paper
|
|
162
|
+
elevation={12}
|
|
163
|
+
style={paperStyles}
|
|
164
|
+
>
|
|
165
|
+
<View
|
|
166
|
+
style={contentStyle}
|
|
167
|
+
onLayout={handleContentLayout}
|
|
168
|
+
>
|
|
169
|
+
{header}
|
|
170
|
+
|
|
171
|
+
<ScrollView
|
|
172
|
+
onLayout={handleScrollViewLayout}
|
|
173
|
+
onScroll={handleScroll}
|
|
174
|
+
scrollEventThrottle={300}
|
|
175
|
+
style={styles.scrollView}
|
|
176
|
+
>
|
|
177
|
+
{children}
|
|
178
|
+
</ScrollView>
|
|
179
|
+
</View>
|
|
180
|
+
</Paper>
|
|
181
|
+
</Animated.View>
|
|
182
|
+
</Modal>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
@@ -13,6 +13,11 @@ export default interface BottomSheetProps extends ComponentProps<{
|
|
|
13
13
|
*/
|
|
14
14
|
children?: React.ReactNode;
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Area to be fixed on the top of the bottom sheet.
|
|
18
|
+
*/
|
|
19
|
+
header?: React.ReactNode;
|
|
20
|
+
|
|
16
21
|
/**
|
|
17
22
|
* Snap index. You could also provide -1 to bottom sheet in closed state.
|
|
18
23
|
*/
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import { LayoutChangeEvent, useWindowDimensions } from 'react-native';
|
|
3
|
+
|
|
4
|
+
interface UseDynamicSnapPointsParams {
|
|
5
|
+
index: number;
|
|
6
|
+
initialSnapPoints: (number | string)[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface UseDynamicSnapPointsReturns {
|
|
10
|
+
handleContentLayout: (e: LayoutChangeEvent) => void;
|
|
11
|
+
snapPoints: number[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const convertHeightAsPixel = (windowHeight: number, value: number | string): number => {
|
|
15
|
+
if (typeof value === 'number') {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const percentageRegex = new RegExp(/^[0-9]+%$/);
|
|
20
|
+
if (percentageRegex.test(value)) {
|
|
21
|
+
const percentage = parseFloat(value) / 100;
|
|
22
|
+
return isNaN(percentage) ? 0 : Math.round(windowHeight * percentage);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return 0;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default function useDynamicSnapPoints(props: UseDynamicSnapPointsParams): UseDynamicSnapPointsReturns {
|
|
29
|
+
const {
|
|
30
|
+
index: currentIndex,
|
|
31
|
+
initialSnapPoints,
|
|
32
|
+
} = props;
|
|
33
|
+
|
|
34
|
+
const { height: WINDOW_HEIGHT } = useWindowDimensions();
|
|
35
|
+
|
|
36
|
+
const [snapPoints, setSnapPoints] = useState(initialSnapPoints.map((snapPoint) => convertHeightAsPixel(WINDOW_HEIGHT, snapPoint)));
|
|
37
|
+
|
|
38
|
+
const handleContentLayout = useCallback((event: LayoutChangeEvent) => {
|
|
39
|
+
if (initialSnapPoints[currentIndex] === 'CONTENT_HEIGHT') {
|
|
40
|
+
const contentHeight = Math.min(Math.round(WINDOW_HEIGHT * 0.9), event.nativeEvent.layout.height);
|
|
41
|
+
|
|
42
|
+
setSnapPoints(prevState => prevState.map((snapPoint, index) => {
|
|
43
|
+
return currentIndex === index ? contentHeight : snapPoint;
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
}, [currentIndex, initialSnapPoints]);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
handleContentLayout,
|
|
50
|
+
snapPoints,
|
|
51
|
+
};
|
|
52
|
+
}
|