@rific/scroll-view 0.4.2 → 0.5.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 +41 -1
- package/dist/index.d.mts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +69 -36
- package/dist/index.mjs +72 -39
- package/package.json +8 -6
- package/src/CustomList.tsx +9 -1
- package/src/PullSearch.tsx +5 -2
- package/src/ScrollView.tsx +16 -1
- package/src/ScrollViewContext.ts +3 -1
- package/src/ScrollViewFooter.tsx +35 -4
- package/src/ScrollViewHeader.tsx +25 -12
- package/src/ScrollViewProvider.tsx +10 -3
- package/src/SectionList.tsx +10 -2
- package/src/internal/HorizontalDots.tsx +13 -5
- package/src/internal/useScrollList.ts +37 -8
- package/src/internal/useStickyHeaders.ts +13 -3
- package/src/useScrollInit.ts +1 -0
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/CustomList.tsx
|
|
2
2
|
import { memo, useCallback as useCallback2, useContext as useContext4, useLayoutEffect as useLayoutEffect2, useMemo as useMemo2, useRef as useRef2 } from "react";
|
|
3
|
-
import { Dimensions, View } from "react-native";
|
|
3
|
+
import { Dimensions, Platform as Platform3, View } from "react-native";
|
|
4
4
|
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
5
5
|
import { runOnUI, useSharedValue as useSharedValue4 } from "react-native-reanimated";
|
|
6
6
|
|
|
@@ -35,6 +35,7 @@ import { runOnJS, useAnimatedScrollHandler, useSharedValue as useSharedValue2, w
|
|
|
35
35
|
import { createContext } from "react";
|
|
36
36
|
var ScrollViewContext = createContext({
|
|
37
37
|
blur: true,
|
|
38
|
+
footerAboveKeyboard: false,
|
|
38
39
|
footerHeight: 0,
|
|
39
40
|
footerHeightShared: null,
|
|
40
41
|
footerFixed: false,
|
|
@@ -64,7 +65,7 @@ var ScrollViewContext = createContext({
|
|
|
64
65
|
},
|
|
65
66
|
snapBackFooterShared: null,
|
|
66
67
|
snapBackHeaderShared: null,
|
|
67
|
-
tabBarHeight:
|
|
68
|
+
tabBarHeight: 0
|
|
68
69
|
});
|
|
69
70
|
|
|
70
71
|
// src/useScrollInit.ts
|
|
@@ -372,17 +373,21 @@ var useKeyboardInset = Platform2.OS === "web" ? useKeyboardInsetWeb : useKeyboar
|
|
|
372
373
|
var ZERO_INSET = { bottom: 0, top: 0 };
|
|
373
374
|
var ZERO_OFFSET = { x: 0, y: 0 };
|
|
374
375
|
var CHIP_SLIDE = 48;
|
|
375
|
-
function useScrollList({ footerFixed: footerFixedProp, headerFixed: headerFixedProp, isHorizontal, keyboardAware, pullSearchHeight, style } = {}) {
|
|
376
|
+
function useScrollList({ footerFixed: footerFixedProp, headerFixed: headerFixedProp, hideUntilMeasured, isHorizontal, keyboardAware, pullSearchHeight, style } = {}) {
|
|
376
377
|
const insets = useSafeAreaInsets();
|
|
377
378
|
const keyboardHeight = useKeyboardInset();
|
|
378
|
-
const { footerHeight, footerFixed: contextFooterFixed, headerHeight, headerFixed: contextHeaderFixed, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared } = useContext3(ScrollViewContext);
|
|
379
|
+
const { footerAboveKeyboard, footerHeight, footerFixed: contextFooterFixed, headerHeight, headerFixed: contextHeaderFixed, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared, tabBarHeight } = useContext3(ScrollViewContext);
|
|
379
380
|
const headerFixed = isHorizontal ? true : headerFixedProp ?? contextHeaderFixed;
|
|
380
381
|
const footerFixed = isHorizontal ? true : footerFixedProp ?? contextFooterFixed;
|
|
381
382
|
useEffect2(() => {
|
|
382
383
|
pullSearchHeightShared.value = usesContentInset ? pullSearchHeight ?? 0 : 0;
|
|
383
384
|
}, [pullSearchHeight, pullSearchHeightShared]);
|
|
384
|
-
const containerStyle = useMemo(() => headerHeight === null ? [{ flex: 1, opacity: 0 }, style] : [StyleSheet2.absoluteFill, style], [headerHeight, style]);
|
|
385
|
-
const insetGeometry = useMemo(() =>
|
|
385
|
+
const containerStyle = useMemo(() => headerHeight === null ? [{ flex: 1, opacity: hideUntilMeasured ? 0 : 1 }, style] : [StyleSheet2.absoluteFill, style], [headerHeight, hideUntilMeasured, style]);
|
|
386
|
+
const insetGeometry = useMemo(() => {
|
|
387
|
+
const footerReserve = footerFixed ? footerHeight || insets.bottom : insets.bottom;
|
|
388
|
+
const bottom = (!keyboardAware || footerFixed && footerAboveKeyboard ? footerReserve : Math.max(footerReserve, keyboardHeight)) + tabBarHeight;
|
|
389
|
+
return { bottom, top: headerHeight ?? 0 };
|
|
390
|
+
}, [footerAboveKeyboard, footerFixed, footerHeight, insets.bottom, tabBarHeight, headerHeight, keyboardAware, keyboardHeight]);
|
|
386
391
|
const contentInset = usesContentInset ? insetGeometry : ZERO_INSET;
|
|
387
392
|
const contentPadding = useMemo(() => usesContentInset || isHorizontal ? null : { paddingBottom: insetGeometry.bottom, paddingTop: insetGeometry.top }, [insetGeometry, isHorizontal]);
|
|
388
393
|
const [startedWithHeader] = useState3(headerHeight !== null);
|
|
@@ -408,7 +413,7 @@ function useScrollList({ footerFixed: footerFixedProp, headerFixed: headerFixedP
|
|
|
408
413
|
top,
|
|
409
414
|
transform: [{ translateY: chipHidden.value ? withTiming2(-CHIP_SLIDE) : withTiming2(0) }]
|
|
410
415
|
};
|
|
411
|
-
}, [isHorizontal, headerFixed, headerHeight, insets.top]);
|
|
416
|
+
}, [isHorizontal, headerFixed, headerHeight, insets.top, chipHidden, snapBackHeaderShared, headerOffset, scrollPosition, pullSearchHeightShared]);
|
|
412
417
|
return { chipHidden, chipStyle, containerStyle, contentInset, contentOffset, contentPadding, footerFixed, headerFixed, headerHeight, insets, isHorizontal: isHorizontal ?? false };
|
|
413
418
|
}
|
|
414
419
|
|
|
@@ -421,6 +426,10 @@ var CustomListInner = ({ chipProps, chipThreshold, component: List, renderFilter
|
|
|
421
426
|
const capturedGeneration = useSharedValue4(-1);
|
|
422
427
|
useLayoutEffect2(() => {
|
|
423
428
|
const generation = listGeneration.value;
|
|
429
|
+
if (Platform3.OS === "web") {
|
|
430
|
+
capturedGeneration.value = generation;
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
424
433
|
runOnUI((gen) => {
|
|
425
434
|
"worklet";
|
|
426
435
|
capturedGeneration.value = gen;
|
|
@@ -518,7 +527,7 @@ var Dot = ({ activeColor, index, inactiveColor, pageWidth, scrollPosition }) =>
|
|
|
518
527
|
const size = DOT_FULL + (DOT_SMALL - DOT_FULL) * t;
|
|
519
528
|
const backgroundColor = interpolateColor(t, [0, 1], [activeColor, inactiveColor]);
|
|
520
529
|
return { backgroundColor, borderRadius: size / 2, height: size, width: size };
|
|
521
|
-
});
|
|
530
|
+
}, [scrollPosition, pageWidth, index, activeColor, inactiveColor]);
|
|
522
531
|
return /* @__PURE__ */ jsx4(View2, { style: styles3.dotContainer, children: /* @__PURE__ */ jsx4(Animated2.View, { style: circleStyle }) });
|
|
523
532
|
};
|
|
524
533
|
var HorizontalDots = ({ total }) => {
|
|
@@ -529,15 +538,18 @@ var HorizontalDots = ({ total }) => {
|
|
|
529
538
|
const insetsBottom = insets.bottom;
|
|
530
539
|
const activeColor = theme.colors.primary;
|
|
531
540
|
const inactiveColor = theme.colors.outlineVariant;
|
|
532
|
-
const pillContainerStyle = useAnimatedStyle2(
|
|
533
|
-
|
|
534
|
-
|
|
541
|
+
const pillContainerStyle = useAnimatedStyle2(
|
|
542
|
+
() => ({
|
|
543
|
+
bottom: Math.max(footerHeightShared.value, insetsBottom) + 8
|
|
544
|
+
}),
|
|
545
|
+
[footerHeightShared, insetsBottom]
|
|
546
|
+
);
|
|
535
547
|
const rowStyle = useAnimatedStyle2(() => {
|
|
536
548
|
const page = scrollPosition.value / width;
|
|
537
549
|
const maxOffset = Math.max(0, (total - CLIP_COUNT) * DOT_STEP);
|
|
538
550
|
const offset = Math.max(0, Math.min((page - (CLIP_COUNT - 1) / 2) * DOT_STEP, maxOffset));
|
|
539
551
|
return { transform: [{ translateX: -offset }] };
|
|
540
|
-
});
|
|
552
|
+
}, [scrollPosition, width, total]);
|
|
541
553
|
return /* @__PURE__ */ jsx4(Animated2.View, { pointerEvents: "none", style: [styles3.pillContainer, pillContainerStyle], children: /* @__PURE__ */ jsxs2(View2, { style: styles3.pill, children: [
|
|
542
554
|
/* @__PURE__ */ jsx4(BlurView, { blur, style: StyleSheet3.absoluteFill }),
|
|
543
555
|
/* @__PURE__ */ jsx4(View2, { style: styles3.dotsClip, children: /* @__PURE__ */ jsx4(Animated2.View, { style: [styles3.dotsRow, rowStyle], children: Array.from({ length: total }).map((_, i) => /* @__PURE__ */ jsx4(Dot, { activeColor, index: i, inactiveColor, pageWidth: width, scrollPosition }, i)) }) })
|
|
@@ -844,7 +856,10 @@ var PullSearch = forwardRef(function PullSearch2({ debounce, onChangeText, onHei
|
|
|
844
856
|
(isHidden, wasHidden) => {
|
|
845
857
|
if (isHidden && !wasHidden) runOnJS3(blurInput)();
|
|
846
858
|
},
|
|
847
|
-
|
|
859
|
+
// scrollPosition has to be listed too, not just read — see useStickyHeaders' useAnimatedReaction
|
|
860
|
+
// for why: on web, without reanimated's Babel plugin active, an unlisted SharedValue mutation
|
|
861
|
+
// never re-triggers this reaction (confirmed empirically elsewhere in this package).
|
|
862
|
+
[barHeight, headerHeight, blurInput, scrollPosition]
|
|
848
863
|
);
|
|
849
864
|
const handleClear = useCallback5(() => {
|
|
850
865
|
setValue("");
|
|
@@ -875,7 +890,7 @@ var PullSearch = forwardRef(function PullSearch2({ debounce, onChangeText, onHei
|
|
|
875
890
|
const showY = -h;
|
|
876
891
|
const hideY = -h + barHeight;
|
|
877
892
|
return { opacity: interpolate(scrollPosition.value, [showY, hideY], [1, 0], Extrapolation.CLAMP) };
|
|
878
|
-
}, [barHeight, headerHeight]);
|
|
893
|
+
}, [barHeight, headerHeight, scrollPosition]);
|
|
879
894
|
return /* @__PURE__ */ jsx6(Animated3.View, { onLayout: handleLayout, style: [styles5.container, animatedStyle], children: /* @__PURE__ */ jsx6(Searchbar, { autoCorrect: false, onChangeText: (text) => setValue((text || "").trimStart()), onClearIconPress: handleClear, placeholder, placeholderTextColor: theme.colors.onSurfaceVariant, ref: inputRef, spellCheck: false, style: styles5.input, value }) });
|
|
880
895
|
});
|
|
881
896
|
var styles5 = StyleSheet4.create({
|
|
@@ -913,7 +928,7 @@ var scrollViewReducer = (state = defaultScrollViewSettings, action) => {
|
|
|
913
928
|
|
|
914
929
|
// src/ScrollView.tsx
|
|
915
930
|
import { memo as memo3, useCallback as useCallback6, useContext as useContext9, useLayoutEffect as useLayoutEffect4, useMemo as useMemo4, useRef as useRef6 } from "react";
|
|
916
|
-
import { View as View4 } from "react-native";
|
|
931
|
+
import { Platform as Platform4, View as View4 } from "react-native";
|
|
917
932
|
import { Gesture as Gesture3, GestureDetector as GestureDetector3 } from "react-native-gesture-handler";
|
|
918
933
|
import Animated4, { runOnUI as runOnUI2, useSharedValue as useSharedValue5 } from "react-native-reanimated";
|
|
919
934
|
import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
@@ -925,6 +940,10 @@ var ScrollViewInner = ({ chipProps, chipThreshold, children, contentContainerSty
|
|
|
925
940
|
const capturedGeneration = useSharedValue5(-1);
|
|
926
941
|
useLayoutEffect4(() => {
|
|
927
942
|
const generation = listGeneration.value;
|
|
943
|
+
if (Platform4.OS === "web") {
|
|
944
|
+
capturedGeneration.value = generation;
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
928
947
|
runOnUI2((gen) => {
|
|
929
948
|
"worklet";
|
|
930
949
|
capturedGeneration.value = gen;
|
|
@@ -989,8 +1008,9 @@ import Animated5, { useAnimatedStyle as useAnimatedStyle4 } from "react-native-r
|
|
|
989
1008
|
import { useSafeAreaInsets as useSafeAreaInsets3 } from "react-native-safe-area-context";
|
|
990
1009
|
import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
991
1010
|
var ScrollViewFooter = ({ children, style }) => {
|
|
992
|
-
const { blur, footerHeight, footerFixed, footerOffset, headerHeightShared, pullSearchHeightShared, scrollPosition, setFooterHeight, snapBackFooterShared } = useContext10(ScrollViewContext);
|
|
1011
|
+
const { blur, footerAboveKeyboard, footerHeight, footerFixed, footerOffset, headerHeightShared, pullSearchHeightShared, scrollPosition, setFooterHeight, snapBackFooterShared } = useContext10(ScrollViewContext);
|
|
993
1012
|
const insets = useSafeAreaInsets3();
|
|
1013
|
+
const keyboardHeight = useKeyboardInset();
|
|
994
1014
|
useEffect4(
|
|
995
1015
|
() => () => {
|
|
996
1016
|
setFooterHeight(0);
|
|
@@ -1010,10 +1030,12 @@ var ScrollViewFooter = ({ children, style }) => {
|
|
|
1010
1030
|
const effective = scrollPosition.value + headerHeightShared.value - pullSearchHeightShared.value;
|
|
1011
1031
|
if (effective <= 0) return { transform: [{ translateY: 0 }] };
|
|
1012
1032
|
return { transform: [{ translateY: Math.min(effective, footerHeight) }] };
|
|
1013
|
-
}, [footerHeight, footerFixed]);
|
|
1014
|
-
|
|
1033
|
+
}, [footerHeight, footerFixed, headerHeightShared, pullSearchHeightShared, scrollPosition, snapBackFooterShared, footerOffset]);
|
|
1034
|
+
const containerPaddingBottom = footerFixed && footerAboveKeyboard ? keyboardHeight : 0;
|
|
1035
|
+
const rowPaddingBottom = footerFixed && footerAboveKeyboard ? Math.max(insets.bottom - keyboardHeight, 0) : insets.bottom;
|
|
1036
|
+
return /* @__PURE__ */ jsxs5(Animated5.View, { onLayout: handleLayout, pointerEvents: "box-none", style: [styles6.footer, { paddingBottom: containerPaddingBottom }, footerStyle], children: [
|
|
1015
1037
|
footerHeight > 0 && /* @__PURE__ */ jsx8(BlurView2, { blur, style: StyleSheet5.absoluteFill }),
|
|
1016
|
-
/* @__PURE__ */ jsx8(View5, { style: [styles6.row, { paddingBottom:
|
|
1038
|
+
/* @__PURE__ */ jsx8(View5, { style: [styles6.row, { paddingBottom: rowPaddingBottom }, style], children })
|
|
1017
1039
|
] });
|
|
1018
1040
|
};
|
|
1019
1041
|
var styles6 = StyleSheet5.create({
|
|
@@ -1030,7 +1052,7 @@ import Animated6, { useAnimatedStyle as useAnimatedStyle5 } from "react-native-r
|
|
|
1030
1052
|
import { useSafeAreaInsets as useSafeAreaInsets4 } from "react-native-safe-area-context";
|
|
1031
1053
|
import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1032
1054
|
var ActionBg = ({ blur, style }) => blur ? /* @__PURE__ */ jsx9(View6, { style: [StyleSheet6.absoluteFill, styles7.actionBg, style], children: /* @__PURE__ */ jsx9(BlurView3, { blur: true, style: StyleSheet6.absoluteFill }) }) : /* @__PURE__ */ jsx9(View6, { style: [StyleSheet6.absoluteFill, styles7.actionBg, style], pointerEvents: "none", children: /* @__PURE__ */ jsx9(Surface, { style: StyleSheet6.absoluteFill, elevation: 1, children: null }) });
|
|
1033
|
-
var ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, backActionFixed, caption, centerContent, children, iconSize, style, title, topInset = true, trailingAction, trailingActionFixed = true }) => {
|
|
1055
|
+
var ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, backActionAccessibilityLabel, backActionFixed, caption, centerContent, children, iconSize, style, title, topInset = true, trailingAction, trailingActionFixed = true }) => {
|
|
1034
1056
|
const { blur, headerHeight, headerFixed, headerOffset, progress, progressing, pullSearchHeightShared, scrollPosition, setHeaderHeight, snapBackHeaderShared } = useContext11(ScrollViewContext);
|
|
1035
1057
|
const { settings } = useContext11(ScrollViewSettingsContext);
|
|
1036
1058
|
const effectiveBackActionFixed = backActionFixed ?? settings.backActionFixed;
|
|
@@ -1044,35 +1066,36 @@ var ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, backActionFi
|
|
|
1044
1066
|
}) => {
|
|
1045
1067
|
if (headerHeight !== height) setHeaderHeight(height);
|
|
1046
1068
|
};
|
|
1069
|
+
const actionMargin = 4;
|
|
1070
|
+
const contentMinHeight = actionSize + 2 * actionMargin;
|
|
1071
|
+
const unmeasuredHeight = top + contentMinHeight;
|
|
1047
1072
|
const translateStyle = useAnimatedStyle5(() => {
|
|
1048
1073
|
if (headerFixed) return { transform: [{ translateY: 0 }] };
|
|
1049
1074
|
if (snapBackHeaderShared.value) return { transform: [{ translateY: headerOffset.value }] };
|
|
1050
1075
|
const effective = scrollPosition.value + (headerHeight ?? 0) - pullSearchHeightShared.value;
|
|
1051
1076
|
if (effective <= 0) return { transform: [{ translateY: 0 }] };
|
|
1052
1077
|
return { transform: [{ translateY: -effective }] };
|
|
1053
|
-
}, [headerFixed, headerHeight]);
|
|
1078
|
+
}, [headerFixed, headerHeight, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared]);
|
|
1054
1079
|
const blurStyle = useAnimatedStyle5(() => {
|
|
1080
|
+
if (headerHeight === null) return { height: unmeasuredHeight };
|
|
1055
1081
|
if (!headerHeight) return { height: 0 };
|
|
1056
1082
|
if (headerFixed) return { height: headerHeight };
|
|
1057
|
-
const
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
}, [headerFixed, headerHeight, top]);
|
|
1083
|
+
const translateY = snapBackHeaderShared.value ? headerOffset.value : -Math.max(0, scrollPosition.value + headerHeight - pullSearchHeightShared.value);
|
|
1084
|
+
return { height: Math.max(headerHeight + translateY, top) };
|
|
1085
|
+
}, [headerFixed, headerHeight, top, unmeasuredHeight, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared]);
|
|
1061
1086
|
const progressStyle = useAnimatedStyle5(() => {
|
|
1062
1087
|
const h = headerHeight ?? 0;
|
|
1063
1088
|
if (headerFixed) return { top: h };
|
|
1064
1089
|
const slide = snapBackHeaderShared.value ? -headerOffset.value : Math.max(0, scrollPosition.value + h - pullSearchHeightShared.value);
|
|
1065
1090
|
return { top: Math.max(h - slide, top) };
|
|
1066
|
-
}, [headerFixed, headerHeight, top]);
|
|
1067
|
-
const actionMargin = 4;
|
|
1068
|
-
const contentMinHeight = actionSize + 2 * actionMargin;
|
|
1091
|
+
}, [headerFixed, headerHeight, top, headerOffset, pullSearchHeightShared, scrollPosition, snapBackHeaderShared]);
|
|
1069
1092
|
const actionTop = top + actionMargin;
|
|
1070
1093
|
const buttonStyle = { borderRadius: actionSize, height: actionSize, margin: 0, width: actionSize };
|
|
1071
1094
|
const actionShadow = { borderRadius: actionSize / 2, elevation: 2, shadowColor: "#000", shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.15, shadowRadius: 2 };
|
|
1072
1095
|
const leadingStyle = { alignItems: "center", height: actionSize, justifyContent: "center", left: actionMargin, position: "absolute", top: actionTop, width: actionSize, zIndex: 3, ...actionShadow };
|
|
1073
1096
|
const trailingStyle = { alignItems: "center", height: actionSize, justifyContent: "center", position: "absolute", right: actionMargin, top: actionTop, width: actionSize, zIndex: 3, ...actionShadow };
|
|
1074
1097
|
return /* @__PURE__ */ jsxs6(Fragment4, { children: [
|
|
1075
|
-
/* @__PURE__ */ jsx9(Animated6.View, { pointerEvents: "none", style: [styles7.blur, blurStyle], children: headerHeight !==
|
|
1098
|
+
/* @__PURE__ */ jsx9(Animated6.View, { pointerEvents: "none", style: [styles7.blur, blurStyle], children: headerHeight !== 0 && /* @__PURE__ */ jsx9(BlurView3, { blur, style: [styles7.blurInner, { height: headerHeight ?? unmeasuredHeight }] }) }),
|
|
1076
1099
|
/* @__PURE__ */ jsxs6(Animated6.View, { onLayout: handleLayout, pointerEvents: "box-none", style: [headerHeight === null ? styles7.headerInit : styles7.header, translateStyle], children: [
|
|
1077
1100
|
/* @__PURE__ */ jsx9(View6, { style: [{ paddingTop: top }, style], children: children ?? /* @__PURE__ */ jsxs6(View6, { style: [styles7.content, { minHeight: contentMinHeight }], children: [
|
|
1078
1101
|
/* @__PURE__ */ jsx9(View6, { style: styles7.side }),
|
|
@@ -1087,10 +1110,10 @@ var ScrollViewHeader = ({ actionSize = 48, actionStyle, backAction, backActionFi
|
|
|
1087
1110
|
/* @__PURE__ */ jsx9(Animated6.View, { pointerEvents: "none", style: [styles7.progress, progressStyle], children: /* @__PURE__ */ jsx9(ProgressBar, { indeterminate: progress === null, visible: progressing, progress: progress ?? void 0 }) }),
|
|
1088
1111
|
backAction && (effectiveBackActionFixed ? /* @__PURE__ */ jsxs6(Animated6.View, { style: leadingStyle, children: [
|
|
1089
1112
|
/* @__PURE__ */ jsx9(ActionBg, { blur, style: actionStyle }),
|
|
1090
|
-
/* @__PURE__ */ jsx9(Appbar.BackAction, { onPress: backAction, size: iconSize ?? actionSize / 2, style: buttonStyle })
|
|
1113
|
+
/* @__PURE__ */ jsx9(Appbar.BackAction, { accessibilityLabel: backActionAccessibilityLabel, onPress: backAction, size: iconSize ?? actionSize / 2, style: buttonStyle })
|
|
1091
1114
|
] }) : /* @__PURE__ */ jsxs6(Animated6.View, { style: [leadingStyle, translateStyle], children: [
|
|
1092
1115
|
/* @__PURE__ */ jsx9(ActionBg, { blur, style: actionStyle }),
|
|
1093
|
-
/* @__PURE__ */ jsx9(Appbar.BackAction, { onPress: backAction, size: iconSize ?? actionSize / 2, style: buttonStyle })
|
|
1116
|
+
/* @__PURE__ */ jsx9(Appbar.BackAction, { accessibilityLabel: backActionAccessibilityLabel, onPress: backAction, size: iconSize ?? actionSize / 2, style: buttonStyle })
|
|
1094
1117
|
] })),
|
|
1095
1118
|
trailingAction && (trailingActionFixed ? /* @__PURE__ */ jsxs6(View6, { style: trailingStyle, children: [
|
|
1096
1119
|
/* @__PURE__ */ jsx9(ActionBg, { blur, style: actionStyle }),
|
|
@@ -1123,7 +1146,7 @@ import { useCallback as useCallback7, useContext as useContext12, useEffect as u
|
|
|
1123
1146
|
import { Dimensions as Dimensions3 } from "react-native";
|
|
1124
1147
|
import { useSharedValue as useSharedValue6 } from "react-native-reanimated";
|
|
1125
1148
|
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
1126
|
-
var ScrollViewProvider = ({ blur, children, fixed = false, footerFixed, headerFixed, snapBack, snapBackFooter, snapBackHeader, tabBarHeight =
|
|
1149
|
+
var ScrollViewProvider = ({ blur, children, fixed = false, footerAboveKeyboard = false, footerFixed, headerFixed, snapBack, snapBackFooter, snapBackHeader, tabBarHeight = 0 }) => {
|
|
1127
1150
|
const { settings } = useContext12(ScrollViewSettingsContext);
|
|
1128
1151
|
const effectiveBlur = useBlur(blur);
|
|
1129
1152
|
const effectiveSnapBack = snapBack ?? settings.snapBack;
|
|
@@ -1149,10 +1172,11 @@ var ScrollViewProvider = ({ blur, children, fixed = false, footerFixed, headerFi
|
|
|
1149
1172
|
}, [effectiveSnapBack, effectiveSnapBackFooter, effectiveSnapBackHeader, snapBackFooterShared, snapBackHeaderShared]);
|
|
1150
1173
|
const setHeaderHeight = useCallback7(
|
|
1151
1174
|
(h) => {
|
|
1175
|
+
if (h === 0 && headerHeight) return;
|
|
1152
1176
|
setHeaderHeightState(h);
|
|
1153
1177
|
headerHeightShared.value = h ?? 0;
|
|
1154
1178
|
},
|
|
1155
|
-
[headerHeightShared]
|
|
1179
|
+
[headerHeight, headerHeightShared]
|
|
1156
1180
|
);
|
|
1157
1181
|
const onListUnmount = useCallback7(() => {
|
|
1158
1182
|
listGeneration.value += 1;
|
|
@@ -1177,7 +1201,7 @@ var ScrollViewProvider = ({ blur, children, fixed = false, footerFixed, headerFi
|
|
|
1177
1201
|
const scrollHeight = useMemo5(() => Dimensions3.get("window").height - (headerHeight ?? 0) - footerHeight, [headerHeight, footerHeight]);
|
|
1178
1202
|
const effectiveHeaderFixed = fixed || (headerFixed ?? settings.headerFixed);
|
|
1179
1203
|
const effectiveFooterFixed = fixed || (footerFixed ?? settings.footerFixed);
|
|
1180
|
-
const value = useMemo5(() => ({ blur: effectiveBlur, footerHeight, footerHeightShared, footerFixed: effectiveFooterFixed, footerOffset, headerHeight, headerHeightShared, headerFixed: effectiveHeaderFixed, headerOffset, jsListGeneration, listGeneration, onJsListUnmount, onListUnmount, progress, pullSearchHeightShared, progressing, scrollHeight, scrollPosition, setFooterHeight, setHeaderHeight, setProgress, setProgressing, snapBackFooterShared, snapBackHeaderShared, tabBarHeight }), [effectiveBlur, effectiveFooterFixed, effectiveHeaderFixed, footerHeight, footerHeightShared, footerOffset, headerHeight, headerHeightShared, headerOffset, jsListGeneration, listGeneration, onJsListUnmount, onListUnmount, progress, pullSearchHeightShared, progressing, scrollHeight, scrollPosition, setFooterHeight, setHeaderHeight, snapBackFooterShared, snapBackHeaderShared, tabBarHeight]);
|
|
1204
|
+
const value = useMemo5(() => ({ blur: effectiveBlur, footerAboveKeyboard, footerHeight, footerHeightShared, footerFixed: effectiveFooterFixed, footerOffset, headerHeight, headerHeightShared, headerFixed: effectiveHeaderFixed, headerOffset, jsListGeneration, listGeneration, onJsListUnmount, onListUnmount, progress, pullSearchHeightShared, progressing, scrollHeight, scrollPosition, setFooterHeight, setHeaderHeight, setProgress, setProgressing, snapBackFooterShared, snapBackHeaderShared, tabBarHeight }), [effectiveBlur, footerAboveKeyboard, effectiveFooterFixed, effectiveHeaderFixed, footerHeight, footerHeightShared, footerOffset, headerHeight, headerHeightShared, headerOffset, jsListGeneration, listGeneration, onJsListUnmount, onListUnmount, progress, pullSearchHeightShared, progressing, scrollHeight, scrollPosition, setFooterHeight, setHeaderHeight, snapBackFooterShared, snapBackHeaderShared, tabBarHeight]);
|
|
1181
1205
|
return /* @__PURE__ */ jsx10(ScrollViewContext.Provider, { value, children });
|
|
1182
1206
|
};
|
|
1183
1207
|
|
|
@@ -1205,7 +1229,7 @@ var ScrollViewSettingsProvider = ({ children, onChange, initialValue }) => {
|
|
|
1205
1229
|
|
|
1206
1230
|
// src/SectionList.tsx
|
|
1207
1231
|
import { memo as memo4, useCallback as useCallback10, useContext as useContext14, useEffect as useEffect6, useLayoutEffect as useLayoutEffect6, useMemo as useMemo7, useRef as useRef9 } from "react";
|
|
1208
|
-
import { Dimensions as Dimensions4, SectionList as RNSectionList, View as View7 } from "react-native";
|
|
1232
|
+
import { Dimensions as Dimensions4, Platform as Platform5, SectionList as RNSectionList, View as View7 } from "react-native";
|
|
1209
1233
|
import { Gesture as Gesture4, GestureDetector as GestureDetector4 } from "react-native-gesture-handler";
|
|
1210
1234
|
import Animated7, { runOnUI as runOnUI3, useSharedValue as useSharedValue8 } from "react-native-reanimated";
|
|
1211
1235
|
|
|
@@ -1252,7 +1276,12 @@ function useStickyHeaders(headerFixed, enabled) {
|
|
|
1252
1276
|
(curr, prev) => {
|
|
1253
1277
|
if (curr !== prev) runOnJS4(setActiveIndex)(curr);
|
|
1254
1278
|
},
|
|
1255
|
-
|
|
1279
|
+
// Same web-only reanimated gap as useAnimatedStyle elsewhere in this file: this "prepare"
|
|
1280
|
+
// function's own SharedValue reads (sectionYPositions, headerHeightShared, scrollPosition,
|
|
1281
|
+
// snapBackHeaderShared, headerOffset, pullSearchHeightShared) need to be listed explicitly too
|
|
1282
|
+
// — confirmed empirically that without them, useAnimatedReaction re-evaluates once on mount and
|
|
1283
|
+
// then never again on web, permanently freezing activeIndex/the active sticky section.
|
|
1284
|
+
[enabled, headerFixed, insetsTop, sectionYPositions, headerHeightShared, scrollPosition, snapBackHeaderShared, headerOffset, pullSearchHeightShared]
|
|
1256
1285
|
);
|
|
1257
1286
|
const clipStyle = useAnimatedStyle6(() => {
|
|
1258
1287
|
const hHeight = headerHeightShared.value;
|
|
@@ -1267,7 +1296,7 @@ function useStickyHeaders(headerFixed, enabled) {
|
|
|
1267
1296
|
stickyTop = effective <= 0 ? hHeight : Math.max(insetsTop, hHeight - effective);
|
|
1268
1297
|
}
|
|
1269
1298
|
return { top: stickyTop };
|
|
1270
|
-
}, [enabled, headerFixed, insetsTop]);
|
|
1299
|
+
}, [enabled, headerFixed, insetsTop, headerHeightShared, scrollPosition, snapBackHeaderShared, headerOffset, pullSearchHeightShared]);
|
|
1271
1300
|
const overlayStyle = useAnimatedStyle6(() => {
|
|
1272
1301
|
const hidden = { opacity: 0, top: 0, transform: [{ translateY: 0 }] };
|
|
1273
1302
|
if (!enabled) return hidden;
|
|
@@ -1295,7 +1324,7 @@ function useStickyHeaders(headerFixed, enabled) {
|
|
|
1295
1324
|
if (nextScreenY < pushThreshold) translateY = nextScreenY - pushThreshold;
|
|
1296
1325
|
}
|
|
1297
1326
|
return { opacity: 1, top: 0, transform: [{ translateY }] };
|
|
1298
|
-
}, [enabled, headerFixed, insetsTop]);
|
|
1327
|
+
}, [enabled, headerFixed, insetsTop, contentIndexShared, sectionYPositions, headerHeightShared, scrollPosition, snapBackHeaderShared, headerOffset, pullSearchHeightShared, sectionHeights]);
|
|
1299
1328
|
const measureHeader = useCallback9(
|
|
1300
1329
|
(index, pageY, height) => {
|
|
1301
1330
|
const contentY = pageY + scrollPosition.value;
|
|
@@ -1324,11 +1353,15 @@ var AnimatedSectionList = Animated7.createAnimatedComponent(RNSectionList);
|
|
|
1324
1353
|
var SectionListInner = ({ chipProps, chipThreshold, contentContainerStyle: externalContentContainerStyle, initialNumToRender = 20, maxToRenderPerBatch = 50, renderFilters, footerFixed: footerFixedProp, gesture, headerFixed: headerFixedProp, keyboardAware, ListHeaderComponent: externalListHeaderComponent, onChipPress, onMomentumScrollEnd: externalMomentumScrollEnd, onRefresh, onContentSizeChange, onScrollBeginDrag: externalScrollBeginDrag, onScrollEndDrag: externalScrollEndDrag, pullSearchHeight, refreshing, removeClippedSubviews = false, sections, showsVerticalScrollIndicator, style, ref: externalRef, windowSize = 100, ...props }) => {
|
|
1325
1354
|
const scrollView = useRef9(null);
|
|
1326
1355
|
const { stickySectionHeadersEnabled = true, renderSectionHeader, ...passThroughProps } = props;
|
|
1327
|
-
const { chipHidden, chipStyle, containerStyle, contentInset, contentOffset, contentPadding, footerFixed, headerFixed, headerHeight } = useScrollList({ footerFixed: footerFixedProp, headerFixed: headerFixedProp, keyboardAware, pullSearchHeight, style });
|
|
1356
|
+
const { chipHidden, chipStyle, containerStyle, contentInset, contentOffset, contentPadding, footerFixed, headerFixed, headerHeight } = useScrollList({ footerFixed: footerFixedProp, headerFixed: headerFixedProp, hideUntilMeasured: true, keyboardAware, pullSearchHeight, style });
|
|
1328
1357
|
const { listGeneration, onListUnmount } = useContext14(ScrollViewContext);
|
|
1329
1358
|
const capturedGeneration = useSharedValue8(-1);
|
|
1330
1359
|
useLayoutEffect6(() => {
|
|
1331
1360
|
const generation = listGeneration.value;
|
|
1361
|
+
if (Platform5.OS === "web") {
|
|
1362
|
+
capturedGeneration.value = generation;
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1332
1365
|
runOnUI3((gen) => {
|
|
1333
1366
|
"worklet";
|
|
1334
1367
|
capturedGeneration.value = gen;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rific/scroll-view",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Blur styled scroll system for React Native. Floating headers/footers, keyboard awareness, and scroll-away chrome",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"type": "commonjs",
|
|
28
28
|
"exports": {
|
|
29
29
|
".": {
|
|
30
|
-
"react-native": "./
|
|
30
|
+
"react-native": "./dist/index.js",
|
|
31
31
|
"types": "./dist/index.d.ts",
|
|
32
32
|
"import": "./dist/index.mjs",
|
|
33
33
|
"require": "./dist/index.js"
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"main": "dist/index.js",
|
|
37
37
|
"module": "dist/index.mjs",
|
|
38
|
-
"react-native": "
|
|
38
|
+
"react-native": "dist/index.js",
|
|
39
39
|
"types": "dist/index.d.ts",
|
|
40
40
|
"files": [
|
|
41
41
|
"dist",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
],
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
48
|
+
"build:watch": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
48
49
|
"fix": "eslint --fix",
|
|
49
50
|
"lint": "eslint",
|
|
50
51
|
"prepublishOnly": "npm run build",
|
|
@@ -59,7 +60,7 @@
|
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
62
|
"@reduxjs/toolkit": "^2.12.0",
|
|
62
|
-
"@rific/auto-paper": "^0.
|
|
63
|
+
"@rific/auto-paper": "^0.8.0",
|
|
63
64
|
"@testing-library/dom": "^10.4.1",
|
|
64
65
|
"@testing-library/react": "^16.3.2",
|
|
65
66
|
"@types/jest": "^30.0.0",
|
|
@@ -70,6 +71,7 @@
|
|
|
70
71
|
"eslint-config-prettier": "^10.1.8",
|
|
71
72
|
"eslint-plugin-package-json": "^1.0.0",
|
|
72
73
|
"eslint-plugin-prettier": "^5.5.5",
|
|
74
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
73
75
|
"eslint-plugin-react-native": "^5.0.0",
|
|
74
76
|
"eslint-plugin-simple-import-sort": "^13.0.0",
|
|
75
77
|
"jest": "^30.4.2",
|
|
@@ -86,11 +88,11 @@
|
|
|
86
88
|
"ts-jest": "^29.4.9",
|
|
87
89
|
"ts-node": "^10.9.2",
|
|
88
90
|
"tsup": "^8.0.0",
|
|
89
|
-
"typescript": "^
|
|
91
|
+
"typescript": "^6.0.3",
|
|
90
92
|
"typescript-eslint": "^8.59.3"
|
|
91
93
|
},
|
|
92
94
|
"peerDependencies": {
|
|
93
|
-
"@rific/auto-paper": "^0.
|
|
95
|
+
"@rific/auto-paper": "^0.8.0",
|
|
94
96
|
"react": ">=18.0.0",
|
|
95
97
|
"react-native": ">=0.76.0",
|
|
96
98
|
"react-native-gesture-handler": ">=2.0.0",
|
package/src/CustomList.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ComponentType, memo, type ReactElement, type ReactNode, type RefObject, useCallback, useContext, useLayoutEffect, useMemo, useRef } from 'react'
|
|
2
|
-
import { Dimensions, type NativeScrollEvent, type NativeSyntheticEvent, type StyleProp, View, type ViewStyle } from 'react-native'
|
|
2
|
+
import { Dimensions, type NativeScrollEvent, type NativeSyntheticEvent, Platform, type StyleProp, View, type ViewStyle } from 'react-native'
|
|
3
3
|
import { Gesture, GestureDetector, type GestureType } from 'react-native-gesture-handler'
|
|
4
4
|
import { runOnUI, useSharedValue } from 'react-native-reanimated'
|
|
5
5
|
|
|
@@ -52,8 +52,16 @@ const CustomListInner = <P extends object>({ chipProps, chipThreshold, component
|
|
|
52
52
|
// imperative scrollTo calls). Routing the write itself through runOnUI puts it on the UI thread's
|
|
53
53
|
// own serial queue, so it's guaranteed to run before any onScroll worklet invocation that could
|
|
54
54
|
// only be queued after this component finishes mounting.
|
|
55
|
+
//
|
|
56
|
+
// Web has no such thread split, though — see ScrollView.tsx's own doc comment on this same
|
|
57
|
+
// pattern for why runOnUI's deferred-to-next-frame behavior there leaves a real gap, and why a
|
|
58
|
+
// synchronous assignment is the correct fix rather than a workaround.
|
|
55
59
|
useLayoutEffect(() => {
|
|
56
60
|
const generation = listGeneration.value
|
|
61
|
+
if (Platform.OS === 'web') {
|
|
62
|
+
capturedGeneration.value = generation
|
|
63
|
+
return
|
|
64
|
+
}
|
|
57
65
|
runOnUI((gen: number) => {
|
|
58
66
|
'worklet'
|
|
59
67
|
capturedGeneration.value = gen
|
package/src/PullSearch.tsx
CHANGED
|
@@ -47,7 +47,10 @@ export const PullSearch = forwardRef<PullSearchHandle, PullSearchProps>(function
|
|
|
47
47
|
(isHidden, wasHidden) => {
|
|
48
48
|
if (isHidden && !wasHidden) runOnJS(blurInput)()
|
|
49
49
|
},
|
|
50
|
-
|
|
50
|
+
// scrollPosition has to be listed too, not just read — see useStickyHeaders' useAnimatedReaction
|
|
51
|
+
// for why: on web, without reanimated's Babel plugin active, an unlisted SharedValue mutation
|
|
52
|
+
// never re-triggers this reaction (confirmed empirically elsewhere in this package).
|
|
53
|
+
[barHeight, headerHeight, blurInput, scrollPosition]
|
|
51
54
|
)
|
|
52
55
|
|
|
53
56
|
const handleClear = useCallback(() => {
|
|
@@ -85,7 +88,7 @@ export const PullSearch = forwardRef<PullSearchHandle, PullSearchProps>(function
|
|
|
85
88
|
const showY = -h
|
|
86
89
|
const hideY = -h + barHeight
|
|
87
90
|
return { opacity: interpolate(scrollPosition.value, [showY, hideY], [1, 0], Extrapolation.CLAMP) }
|
|
88
|
-
}, [barHeight, headerHeight])
|
|
91
|
+
}, [barHeight, headerHeight, scrollPosition])
|
|
89
92
|
|
|
90
93
|
return (
|
|
91
94
|
<Animated.View onLayout={handleLayout} style={[styles.container, animatedStyle]}>
|
package/src/ScrollView.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { memo, type RefObject, useCallback, useContext, useLayoutEffect, useMemo, useRef } from 'react'
|
|
2
|
-
import { type NativeScrollEvent, type NativeSyntheticEvent, ScrollView as RNScrollView, type ScrollViewProps as RNScrollViewProps, View } from 'react-native'
|
|
2
|
+
import { type NativeScrollEvent, type NativeSyntheticEvent, Platform, ScrollView as RNScrollView, type ScrollViewProps as RNScrollViewProps, View } from 'react-native'
|
|
3
3
|
import { Gesture, GestureDetector, type GestureType } from 'react-native-gesture-handler'
|
|
4
4
|
import Animated, { runOnUI, useSharedValue } from 'react-native-reanimated'
|
|
5
5
|
|
|
@@ -47,8 +47,23 @@ const ScrollViewInner = ({ chipProps, chipThreshold, children, contentContainerS
|
|
|
47
47
|
// imperative scrollTo calls). Routing the write itself through runOnUI puts it on the UI thread's
|
|
48
48
|
// own serial queue, so it's guaranteed to run before any onScroll worklet invocation that could
|
|
49
49
|
// only be queued after this component finishes mounting.
|
|
50
|
+
//
|
|
51
|
+
// Web has no such thread split, though — react-native-worklets' own runOnUI there (see its
|
|
52
|
+
// threads.js) only enqueues the callback for the NEXT requestAnimationFrame, it doesn't run it
|
|
53
|
+
// synchronously. That opens exactly the gap the native path exists to close: a scroll event
|
|
54
|
+
// (including useScrollInit's own mount-time imperative scrollTo) landing before that deferred
|
|
55
|
+
// frame sees capturedGeneration still at its -1 placeholder, fails the generation check below,
|
|
56
|
+
// and — since this effect only ever runs once per mount — never gets a second chance, silently
|
|
57
|
+
// disabling scroll tracking (and therefore header/footer scroll-away) for this instance's entire
|
|
58
|
+
// lifetime. Assigning directly is both simpler and provably correct on web: single-threaded JS
|
|
59
|
+
// makes a synchronous write immediately visible to any read that follows it in program order, so
|
|
60
|
+
// there's no cross-thread visibility gap here to guard against in the first place.
|
|
50
61
|
useLayoutEffect(() => {
|
|
51
62
|
const generation = listGeneration.value
|
|
63
|
+
if (Platform.OS === 'web') {
|
|
64
|
+
capturedGeneration.value = generation
|
|
65
|
+
return
|
|
66
|
+
}
|
|
52
67
|
runOnUI((gen: number) => {
|
|
53
68
|
'worklet'
|
|
54
69
|
capturedGeneration.value = gen
|
package/src/ScrollViewContext.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { SharedValue } from 'react-native-reanimated'
|
|
|
3
3
|
|
|
4
4
|
export type ScrollViewContextType = {
|
|
5
5
|
blur: boolean
|
|
6
|
+
footerAboveKeyboard: boolean
|
|
6
7
|
footerHeight: number
|
|
7
8
|
footerHeightShared: SharedValue<number>
|
|
8
9
|
footerFixed: boolean
|
|
@@ -36,6 +37,7 @@ export type ScrollViewContextType = {
|
|
|
36
37
|
|
|
37
38
|
export const ScrollViewContext = createContext<ScrollViewContextType>({
|
|
38
39
|
blur: true,
|
|
40
|
+
footerAboveKeyboard: false,
|
|
39
41
|
footerHeight: 0,
|
|
40
42
|
footerHeightShared: null as unknown as SharedValue<number>,
|
|
41
43
|
footerFixed: false,
|
|
@@ -59,5 +61,5 @@ export const ScrollViewContext = createContext<ScrollViewContextType>({
|
|
|
59
61
|
setProgressing: () => {},
|
|
60
62
|
snapBackFooterShared: null as unknown as SharedValue<boolean>,
|
|
61
63
|
snapBackHeaderShared: null as unknown as SharedValue<boolean>,
|
|
62
|
-
tabBarHeight:
|
|
64
|
+
tabBarHeight: 0
|
|
63
65
|
})
|
package/src/ScrollViewFooter.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import Animated, { useAnimatedStyle } from 'react-native-reanimated'
|
|
|
5
5
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
6
6
|
|
|
7
7
|
import { ScrollViewContext } from './ScrollViewContext'
|
|
8
|
+
import { useKeyboardInset } from './useKeyboardInset'
|
|
8
9
|
|
|
9
10
|
export type ScrollViewFooterProps = {
|
|
10
11
|
children?: ReactNode | ReactNode[]
|
|
@@ -12,8 +13,9 @@ export type ScrollViewFooterProps = {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export const ScrollViewFooter = ({ children, style }: ScrollViewFooterProps) => {
|
|
15
|
-
const { blur, footerHeight, footerFixed, footerOffset, headerHeightShared, pullSearchHeightShared, scrollPosition, setFooterHeight, snapBackFooterShared } = useContext(ScrollViewContext)
|
|
16
|
+
const { blur, footerAboveKeyboard, footerHeight, footerFixed, footerOffset, headerHeightShared, pullSearchHeightShared, scrollPosition, setFooterHeight, snapBackFooterShared } = useContext(ScrollViewContext)
|
|
16
17
|
const insets = useSafeAreaInsets()
|
|
18
|
+
const keyboardHeight = useKeyboardInset()
|
|
17
19
|
useEffect(
|
|
18
20
|
() => () => {
|
|
19
21
|
setFooterHeight(0)
|
|
@@ -27,17 +29,46 @@ export const ScrollViewFooter = ({ children, style }: ScrollViewFooterProps) =>
|
|
|
27
29
|
}: LayoutChangeEvent) => {
|
|
28
30
|
if (footerHeight !== height) setFooterHeight(height)
|
|
29
31
|
}
|
|
32
|
+
// See ScrollViewHeader's translateStyle/blurStyle/progressStyle for why every SharedValue read
|
|
33
|
+
// here (scrollPosition, headerHeightShared, pullSearchHeightShared, snapBackFooterShared,
|
|
34
|
+
// footerOffset) also has to be listed explicitly — on web, reanimated doesn't pick up their
|
|
35
|
+
// mutations as reactive triggers unless they're in this array, unlike native.
|
|
30
36
|
const footerStyle = useAnimatedStyle(() => {
|
|
31
37
|
if (footerFixed) return { transform: [{ translateY: 0 }] }
|
|
32
38
|
if (snapBackFooterShared.value) return { transform: [{ translateY: footerOffset.value }] }
|
|
33
39
|
const effective = scrollPosition.value + headerHeightShared.value - pullSearchHeightShared.value
|
|
34
40
|
if (effective <= 0) return { transform: [{ translateY: 0 }] }
|
|
35
41
|
return { transform: [{ translateY: Math.min(effective, footerHeight) }] }
|
|
36
|
-
}, [footerHeight, footerFixed])
|
|
42
|
+
}, [footerHeight, footerFixed, headerHeightShared, pullSearchHeightShared, scrollPosition, snapBackFooterShared, footerOffset])
|
|
43
|
+
// Floating above the keyboard is done by GROWING this container (via paddingBottom) rather than
|
|
44
|
+
// translating it. Translating the whole bar left its bottom edge — and the BlurView filling it —
|
|
45
|
+
// floating above the true screen bottom, exposing a gap of whatever sits behind (visible as a
|
|
46
|
+
// strip above the keyboard, and worse at the keyboard's rounded top corners, which dip below its
|
|
47
|
+
// reported height). Growing the container instead keeps its `bottom: 0` anchor untouched: the
|
|
48
|
+
// blur backdrop always reaches all the way to the physical edge, sealing that gap entirely,
|
|
49
|
+
// while the extra height pushes the row (this bar's actual content) up by keyboardHeight, same
|
|
50
|
+
// as the old translateY did for it.
|
|
51
|
+
const containerPaddingBottom = footerFixed && footerAboveKeyboard ? keyboardHeight : 0
|
|
52
|
+
// The safe-area bottom inset exists to clear the home indicator at the physical screen edge.
|
|
53
|
+
// Once footerAboveKeyboard has floated this bar's content up above an open keyboard, it's no
|
|
54
|
+
// longer sitting at that edge — the keyboard itself now occupies that space — so the inset would
|
|
55
|
+
// just be dead space under the content (e.g. under a Save button) for no reason.
|
|
56
|
+
//
|
|
57
|
+
// This has to ramp continuously with keyboardHeight, not flip as a boolean once the keyboard
|
|
58
|
+
// fully closes: containerPaddingBottom above already tracks keyboardHeight every frame, so a
|
|
59
|
+
// step-function padding change here lands in the same frame keyboardHeight hits 0, growing this
|
|
60
|
+
// row by insets.bottom right as the container finishes shrinking back down — the content visually
|
|
61
|
+
// overshoots to the physical bottom edge and then snaps back up. Ramping this padding by the same
|
|
62
|
+
// keyboardHeight keeps the content's on-screen position moving continuously throughout: for
|
|
63
|
+
// keyboardHeight >= insets.bottom the padding is 0 (unchanged from mid-close), and for the last
|
|
64
|
+
// insets.bottom worth of keyboard travel, containerPaddingBottom's continued shrink and this
|
|
65
|
+
// padding's growth cancel out exactly, so the content holds still at the safe-area line instead
|
|
66
|
+
// of dropping past it and correcting back up.
|
|
67
|
+
const rowPaddingBottom = footerFixed && footerAboveKeyboard ? Math.max(insets.bottom - keyboardHeight, 0) : insets.bottom
|
|
37
68
|
return (
|
|
38
|
-
<Animated.View onLayout={handleLayout} pointerEvents='box-none' style={[styles.footer, footerStyle]}>
|
|
69
|
+
<Animated.View onLayout={handleLayout} pointerEvents='box-none' style={[styles.footer, { paddingBottom: containerPaddingBottom }, footerStyle]}>
|
|
39
70
|
{footerHeight > 0 && <BlurView blur={blur} style={StyleSheet.absoluteFill} />}
|
|
40
|
-
<View style={[styles.row, { paddingBottom:
|
|
71
|
+
<View style={[styles.row, { paddingBottom: rowPaddingBottom }, style]}>{children}</View>
|
|
41
72
|
</Animated.View>
|
|
42
73
|
)
|
|
43
74
|
}
|