overlapping-cards-scroll 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/react-native-web.cjs +22 -4
- package/dist/react-native-web.js +21 -3
- package/dist/react-native.cjs +257 -30
- package/dist/react-native.js +258 -30
- package/dist/types/rn/OverlappingCardsScrollRN.native.d.ts +4 -26
- package/dist/types/rn/OverlappingCardsScrollRN.types.d.ts +74 -0
- package/dist/types/rn/OverlappingCardsScrollRN.web.d.ts +4 -18
- package/package.json +8 -3
- package/src/lib/OverlappingCardsScroll.css +206 -0
- package/src/lib/OverlappingCardsScroll.tsx +943 -0
- package/src/lib/index.ts +10 -0
- package/src/rn/OverlappingCardsScrollRN.native.tsx +868 -0
- package/src/rn/OverlappingCardsScrollRN.types.ts +102 -0
- package/src/rn/OverlappingCardsScrollRN.web.tsx +90 -0
- package/src/rn/RNWebDemo.tsx +241 -0
package/README.md
CHANGED
|
@@ -68,16 +68,24 @@ import 'overlapping-cards-scroll/styles.css'
|
|
|
68
68
|
## Props
|
|
69
69
|
|
|
70
70
|
- `children`: card nodes
|
|
71
|
+
- `items` (`{ id: string | number; name: string; jsx: ReactElement }[]`) optional alternative to `children`; required for named tabs (`showTabs`)
|
|
71
72
|
- `cardHeight` (`number | string`, default `300`)
|
|
72
73
|
- `cardWidth` (`number | string`) accepts px number (e.g. `250`) or percent string (e.g. `'50%'`)
|
|
73
74
|
- `cardWidthRatio` (`number`, default `1 / 3`)
|
|
74
75
|
- `basePeek` (`number`, default `64`)
|
|
75
76
|
- `minPeek` (`number`, default `10`)
|
|
76
77
|
- `maxPeek` (`number`, default `84`)
|
|
78
|
+
- `cardContainerStyle` (web: `CSSProperties`, RN native: `StyleProp<ViewStyle>`) applied to each positioned card container
|
|
77
79
|
- `showPageDots` (`boolean`, default `false`) renders clickable page dots
|
|
78
80
|
- `pageDotsPosition` (`'above' | 'below' | 'overlay'`, default `'below'`)
|
|
79
81
|
- `pageDotsOffset` (`number | string`, default `10`) distance from stage
|
|
80
82
|
- `pageDotsBehavior` (`'smooth' | 'auto'`, web only, default `'smooth'`)
|
|
83
|
+
- `showTabs` (`boolean`, default `false`) renders card-name tabs when `items` is provided
|
|
84
|
+
- `tabsPosition` (`'above' | 'below'`, default `'above'`)
|
|
85
|
+
- `tabsOffset` (`number | string`, default `10`)
|
|
86
|
+
- `tabsBehavior` (`'smooth' | 'auto'`, web only, default `'smooth'`)
|
|
87
|
+
- `tabsComponent` (custom tab renderer; supported on web and RN native)
|
|
88
|
+
- `tabsContainerComponent` (custom tab container renderer; supported on web and RN native)
|
|
81
89
|
- `snapToCardOnRelease` (`boolean`, default `true`) web/RN-web uses idle + mousemove snap, RN native uses `snapToInterval` on iOS
|
|
82
90
|
- `snapReleaseDelay` (`number`, default `800`) web/RN-web debounce before auto-snap
|
|
83
91
|
- `snapDecelerationRate` (`'normal' | 'fast' | number`, RN native only, default `'normal'`) controls fling feel while snapping
|
|
@@ -106,6 +114,14 @@ Expo development:
|
|
|
106
114
|
npm run dev:expo
|
|
107
115
|
```
|
|
108
116
|
|
|
117
|
+
The Expo app shell lives in `expo-demo/` and imports the demo screen from the package root via `../App`.
|
|
118
|
+
You can also run it directly with:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
cd expo-demo
|
|
122
|
+
npm run dev
|
|
123
|
+
```
|
|
124
|
+
|
|
109
125
|
Build package artifacts:
|
|
110
126
|
|
|
111
127
|
```bash
|
|
@@ -23,7 +23,7 @@ __export(OverlappingCardsScrollRN_web_exports, {
|
|
|
23
23
|
OverlappingCardsScrollRNFocusTrigger: () => OverlappingCardsScrollRNFocusTrigger
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(OverlappingCardsScrollRN_web_exports);
|
|
26
|
-
var
|
|
26
|
+
var import_react_native = require("react-native");
|
|
27
27
|
|
|
28
28
|
// src/lib/OverlappingCardsScroll.tsx
|
|
29
29
|
var import_react = require("react");
|
|
@@ -721,10 +721,19 @@ var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
|
721
721
|
function OverlappingCardsScrollRNFocusTrigger({
|
|
722
722
|
children = "Make principal",
|
|
723
723
|
className = "",
|
|
724
|
+
style = void 0,
|
|
725
|
+
textStyle = void 0,
|
|
726
|
+
behavior = "smooth",
|
|
727
|
+
transitionMode = "swoop",
|
|
728
|
+
disabled = false,
|
|
729
|
+
accessibilityLabel = void 0,
|
|
730
|
+
testID = void 0,
|
|
724
731
|
onPress = void 0,
|
|
725
732
|
onClick = void 0,
|
|
726
733
|
...buttonProps
|
|
727
734
|
}) {
|
|
735
|
+
void style;
|
|
736
|
+
void textStyle;
|
|
728
737
|
const handleClick = (event) => {
|
|
729
738
|
onClick == null ? void 0 : onClick(event);
|
|
730
739
|
onPress == null ? void 0 : onPress(event);
|
|
@@ -733,6 +742,11 @@ function OverlappingCardsScrollRNFocusTrigger({
|
|
|
733
742
|
OverlappingCardsScrollFocusTrigger,
|
|
734
743
|
{
|
|
735
744
|
className,
|
|
745
|
+
behavior,
|
|
746
|
+
transitionMode,
|
|
747
|
+
disabled,
|
|
748
|
+
"aria-label": accessibilityLabel,
|
|
749
|
+
"data-testid": testID,
|
|
736
750
|
onClick: handleClick,
|
|
737
751
|
...buttonProps,
|
|
738
752
|
children
|
|
@@ -740,7 +754,6 @@ function OverlappingCardsScrollRNFocusTrigger({
|
|
|
740
754
|
);
|
|
741
755
|
}
|
|
742
756
|
function OverlappingCardsScrollRN({
|
|
743
|
-
children,
|
|
744
757
|
style = void 0,
|
|
745
758
|
showsHorizontalScrollIndicator = true,
|
|
746
759
|
snapDecelerationRate = "normal",
|
|
@@ -750,9 +763,14 @@ function OverlappingCardsScrollRN({
|
|
|
750
763
|
void showsHorizontalScrollIndicator;
|
|
751
764
|
void snapDecelerationRate;
|
|
752
765
|
void snapDisableIntervalMomentum;
|
|
753
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
766
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native.View, { style: [styles.root, style], children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
767
|
+
OverlappingCardsScroll,
|
|
768
|
+
{
|
|
769
|
+
...overlappingCardsScrollProps
|
|
770
|
+
}
|
|
771
|
+
) });
|
|
754
772
|
}
|
|
755
|
-
var styles =
|
|
773
|
+
var styles = import_react_native.StyleSheet.create({
|
|
756
774
|
root: {
|
|
757
775
|
width: "100%",
|
|
758
776
|
minWidth: 0,
|
package/dist/react-native-web.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/rn/OverlappingCardsScrollRN.web.tsx
|
|
2
|
-
import { StyleSheet, View } from "react-native
|
|
2
|
+
import { StyleSheet, View } from "react-native";
|
|
3
3
|
|
|
4
4
|
// src/lib/OverlappingCardsScroll.tsx
|
|
5
5
|
import {
|
|
@@ -707,10 +707,19 @@ import { jsx as jsx2 } from "react/jsx-runtime";
|
|
|
707
707
|
function OverlappingCardsScrollRNFocusTrigger({
|
|
708
708
|
children = "Make principal",
|
|
709
709
|
className = "",
|
|
710
|
+
style = void 0,
|
|
711
|
+
textStyle = void 0,
|
|
712
|
+
behavior = "smooth",
|
|
713
|
+
transitionMode = "swoop",
|
|
714
|
+
disabled = false,
|
|
715
|
+
accessibilityLabel = void 0,
|
|
716
|
+
testID = void 0,
|
|
710
717
|
onPress = void 0,
|
|
711
718
|
onClick = void 0,
|
|
712
719
|
...buttonProps
|
|
713
720
|
}) {
|
|
721
|
+
void style;
|
|
722
|
+
void textStyle;
|
|
714
723
|
const handleClick = (event) => {
|
|
715
724
|
onClick == null ? void 0 : onClick(event);
|
|
716
725
|
onPress == null ? void 0 : onPress(event);
|
|
@@ -719,6 +728,11 @@ function OverlappingCardsScrollRNFocusTrigger({
|
|
|
719
728
|
OverlappingCardsScrollFocusTrigger,
|
|
720
729
|
{
|
|
721
730
|
className,
|
|
731
|
+
behavior,
|
|
732
|
+
transitionMode,
|
|
733
|
+
disabled,
|
|
734
|
+
"aria-label": accessibilityLabel,
|
|
735
|
+
"data-testid": testID,
|
|
722
736
|
onClick: handleClick,
|
|
723
737
|
...buttonProps,
|
|
724
738
|
children
|
|
@@ -726,7 +740,6 @@ function OverlappingCardsScrollRNFocusTrigger({
|
|
|
726
740
|
);
|
|
727
741
|
}
|
|
728
742
|
function OverlappingCardsScrollRN({
|
|
729
|
-
children,
|
|
730
743
|
style = void 0,
|
|
731
744
|
showsHorizontalScrollIndicator = true,
|
|
732
745
|
snapDecelerationRate = "normal",
|
|
@@ -736,7 +749,12 @@ function OverlappingCardsScrollRN({
|
|
|
736
749
|
void showsHorizontalScrollIndicator;
|
|
737
750
|
void snapDecelerationRate;
|
|
738
751
|
void snapDisableIntervalMomentum;
|
|
739
|
-
return /* @__PURE__ */ jsx2(View, { style: [styles.root, style], children: /* @__PURE__ */ jsx2(
|
|
752
|
+
return /* @__PURE__ */ jsx2(View, { style: [styles.root, style], children: /* @__PURE__ */ jsx2(
|
|
753
|
+
OverlappingCardsScroll,
|
|
754
|
+
{
|
|
755
|
+
...overlappingCardsScrollProps
|
|
756
|
+
}
|
|
757
|
+
) });
|
|
740
758
|
}
|
|
741
759
|
var styles = StyleSheet.create({
|
|
742
760
|
root: {
|
package/dist/react-native.cjs
CHANGED
|
@@ -28,7 +28,43 @@ var import_react_native = require("react-native");
|
|
|
28
28
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
29
29
|
var clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
30
30
|
var PAGE_DOT_POSITIONS = /* @__PURE__ */ new Set(["above", "below", "overlay"]);
|
|
31
|
+
var TAB_POSITIONS = /* @__PURE__ */ new Set(["above", "below"]);
|
|
31
32
|
var normalizePageDotsPosition = (value) => PAGE_DOT_POSITIONS.has(value) ? value : "below";
|
|
33
|
+
var normalizeTabsPosition = (value) => TAB_POSITIONS.has(value) ? value : "above";
|
|
34
|
+
var toNumericOffset = (value, fallback = 0) => {
|
|
35
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
if (typeof value === "string") {
|
|
39
|
+
const parsed = Number.parseFloat(value.trim());
|
|
40
|
+
if (Number.isFinite(parsed)) {
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return fallback;
|
|
45
|
+
};
|
|
46
|
+
var toNativeDimension = (value, fallback = 0) => {
|
|
47
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
if (typeof value === "string") {
|
|
51
|
+
const trimmed = value.trim();
|
|
52
|
+
if (trimmed === "auto") {
|
|
53
|
+
return "auto";
|
|
54
|
+
}
|
|
55
|
+
if (trimmed.endsWith("%")) {
|
|
56
|
+
const percent = Number.parseFloat(trimmed.slice(0, -1));
|
|
57
|
+
if (Number.isFinite(percent)) {
|
|
58
|
+
return `${percent}%`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const numeric = Number.parseFloat(trimmed);
|
|
62
|
+
if (Number.isFinite(numeric)) {
|
|
63
|
+
return numeric;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return fallback;
|
|
67
|
+
};
|
|
32
68
|
var resolveCardXAtProgress = (index, progress, layout) => {
|
|
33
69
|
const principalIndex = Math.floor(progress);
|
|
34
70
|
const transitionProgress = progress - principalIndex;
|
|
@@ -66,26 +102,69 @@ function OverlappingCardsScrollRNFocusTrigger({
|
|
|
66
102
|
children = "Make principal",
|
|
67
103
|
style = void 0,
|
|
68
104
|
textStyle = void 0,
|
|
105
|
+
behavior = "smooth",
|
|
69
106
|
transitionMode = "swoop",
|
|
107
|
+
disabled = false,
|
|
108
|
+
accessibilityLabel = void 0,
|
|
109
|
+
testID = void 0,
|
|
70
110
|
onPress = void 0,
|
|
111
|
+
onClick = void 0,
|
|
71
112
|
...pressableProps
|
|
72
113
|
}) {
|
|
73
114
|
const { canFocus, focusCard } = useOverlappingCardsScrollRNCardControl();
|
|
74
115
|
const handlePress = (event) => {
|
|
116
|
+
onClick == null ? void 0 : onClick(event);
|
|
75
117
|
onPress == null ? void 0 : onPress(event);
|
|
76
|
-
focusCard({
|
|
118
|
+
focusCard({
|
|
119
|
+
animated: behavior !== "auto",
|
|
120
|
+
transitionMode
|
|
121
|
+
});
|
|
77
122
|
};
|
|
78
123
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
79
124
|
import_react_native.Pressable,
|
|
80
125
|
{
|
|
81
126
|
style: ({ pressed }) => [styles.focusTrigger, pressed && styles.focusTriggerPressed, style],
|
|
82
|
-
disabled: !canFocus,
|
|
127
|
+
disabled: disabled || !canFocus,
|
|
128
|
+
accessibilityLabel,
|
|
129
|
+
testID,
|
|
83
130
|
onPress: handlePress,
|
|
84
131
|
...pressableProps,
|
|
85
132
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: [styles.focusTriggerText, textStyle], children })
|
|
86
133
|
}
|
|
87
134
|
);
|
|
88
135
|
}
|
|
136
|
+
function DefaultTabsContainerComponent({
|
|
137
|
+
children,
|
|
138
|
+
style,
|
|
139
|
+
ariaLabel
|
|
140
|
+
}) {
|
|
141
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { accessibilityRole: "tablist", accessibilityLabel: ariaLabel, style, children });
|
|
142
|
+
}
|
|
143
|
+
function DefaultTabsComponent({
|
|
144
|
+
name,
|
|
145
|
+
style,
|
|
146
|
+
textStyle,
|
|
147
|
+
accessibilityLabel,
|
|
148
|
+
accessibilityState,
|
|
149
|
+
onPress,
|
|
150
|
+
onClick
|
|
151
|
+
}) {
|
|
152
|
+
const handlePress = () => {
|
|
153
|
+
onClick();
|
|
154
|
+
onPress();
|
|
155
|
+
};
|
|
156
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
157
|
+
import_react_native.Pressable,
|
|
158
|
+
{
|
|
159
|
+
accessibilityRole: "tab",
|
|
160
|
+
accessibilityLabel,
|
|
161
|
+
accessibilityState,
|
|
162
|
+
onPress: handlePress,
|
|
163
|
+
style: ({ pressed }) => [styles.tab, pressed && styles.tabPressed, style],
|
|
164
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: [styles.tabText, textStyle], children: name })
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
}
|
|
89
168
|
var resolveCardWidth = (cardWidth, viewportWidth, fallbackRatio) => {
|
|
90
169
|
if (typeof cardWidth === "number" && Number.isFinite(cardWidth) && cardWidth > 0) {
|
|
91
170
|
return cardWidth;
|
|
@@ -105,26 +184,66 @@ var resolveCardWidth = (cardWidth, viewportWidth, fallbackRatio) => {
|
|
|
105
184
|
}
|
|
106
185
|
return viewportWidth * fallbackRatio;
|
|
107
186
|
};
|
|
108
|
-
function OverlappingCardsScrollRN({
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
187
|
+
function OverlappingCardsScrollRN(props) {
|
|
188
|
+
const {
|
|
189
|
+
style = void 0,
|
|
190
|
+
cardHeight = 300,
|
|
191
|
+
cardWidth = void 0,
|
|
192
|
+
cardWidthRatio = 1 / 3,
|
|
193
|
+
basePeek = 64,
|
|
194
|
+
minPeek = 10,
|
|
195
|
+
maxPeek = 84,
|
|
196
|
+
showsHorizontalScrollIndicator = true,
|
|
197
|
+
snapToCardOnRelease = true,
|
|
198
|
+
snapDecelerationRate = "normal",
|
|
199
|
+
snapDisableIntervalMomentum = false,
|
|
200
|
+
showPageDots = false,
|
|
201
|
+
pageDotsPosition = "below",
|
|
202
|
+
pageDotsOffset = 10,
|
|
203
|
+
focusTransitionDuration = 420,
|
|
204
|
+
cardContainerStyle = void 0,
|
|
205
|
+
showTabs = false,
|
|
206
|
+
tabsPosition = "above",
|
|
207
|
+
tabsOffset = 10,
|
|
208
|
+
tabsComponent: TabsComponent = DefaultTabsComponent,
|
|
209
|
+
tabsContainerComponent: TabsContainerComponent = DefaultTabsContainerComponent
|
|
210
|
+
} = props;
|
|
211
|
+
const hasItems = "items" in props && Array.isArray(props.items);
|
|
212
|
+
const hasChildren = "children" in props && props.children != null;
|
|
213
|
+
(0, import_react.useEffect)(() => {
|
|
214
|
+
if (hasItems && hasChildren) {
|
|
215
|
+
console.warn(
|
|
216
|
+
"OverlappingCardsScrollRN: Both `items` and `children` were provided. `items` takes precedence."
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}, [hasItems, hasChildren]);
|
|
220
|
+
const itemsProp = hasItems ? props.items : null;
|
|
221
|
+
const childrenProp = hasChildren ? props.children : null;
|
|
222
|
+
const cards = (0, import_react.useMemo)(() => {
|
|
223
|
+
if (itemsProp) {
|
|
224
|
+
return itemsProp.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Fragment, { children: item.jsx }, item.id));
|
|
225
|
+
}
|
|
226
|
+
return import_react.Children.toArray(childrenProp);
|
|
227
|
+
}, [childrenProp, itemsProp]);
|
|
228
|
+
const cardNames = (0, import_react.useMemo)(() => {
|
|
229
|
+
if (itemsProp) {
|
|
230
|
+
return itemsProp.map((item) => item.name);
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}, [itemsProp]);
|
|
127
234
|
const cardCount = cards.length;
|
|
235
|
+
const resolvedTabsPosition = normalizeTabsPosition(tabsPosition);
|
|
236
|
+
const showNavigationTabs = showTabs && cardCount > 1 && cardNames !== null;
|
|
237
|
+
const resolvedPageDotsOffset = toNumericOffset(pageDotsOffset, 10);
|
|
238
|
+
const resolvedTabsOffset = toNumericOffset(tabsOffset, 10);
|
|
239
|
+
const resolvedCardHeight = toNativeDimension(cardHeight, 300);
|
|
240
|
+
(0, import_react.useEffect)(() => {
|
|
241
|
+
if (showTabs && cardNames === null) {
|
|
242
|
+
console.warn(
|
|
243
|
+
"OverlappingCardsScrollRN: `showTabs` requires the `items` prop to provide card names. Tabs will not render."
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}, [cardNames, showTabs]);
|
|
128
247
|
const scrollRef = (0, import_react.useRef)(null);
|
|
129
248
|
const scrollX = (0, import_react.useRef)(new import_react_native.Animated.Value(0)).current;
|
|
130
249
|
const scrollXValueRef = (0, import_react.useRef)(0);
|
|
@@ -133,6 +252,7 @@ function OverlappingCardsScrollRN({
|
|
|
133
252
|
const focusTransitionIdRef = (0, import_react.useRef)(0);
|
|
134
253
|
const [viewportWidth, setViewportWidth] = (0, import_react.useState)(1);
|
|
135
254
|
const [focusTransition, setFocusTransition] = (0, import_react.useState)(null);
|
|
255
|
+
const [scrollProgress, setScrollProgress] = (0, import_react.useState)(0);
|
|
136
256
|
const layout = (0, import_react.useMemo)(() => {
|
|
137
257
|
const safeWidth = Math.max(1, viewportWidth);
|
|
138
258
|
const safeRatio = clamp(cardWidthRatio, 0.2, 0.95);
|
|
@@ -175,11 +295,26 @@ function OverlappingCardsScrollRN({
|
|
|
175
295
|
(0, import_react.useEffect)(() => {
|
|
176
296
|
const id = scrollX.addListener(({ value }) => {
|
|
177
297
|
scrollXValueRef.current = value;
|
|
298
|
+
if (!showNavigationTabs) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const nextProgress = cardCount > 1 ? clamp(value / layout.stepDistance, 0, cardCount - 1) : 0;
|
|
302
|
+
setScrollProgress(
|
|
303
|
+
(currentProgress) => Math.abs(currentProgress - nextProgress) < 1e-3 ? currentProgress : nextProgress
|
|
304
|
+
);
|
|
178
305
|
});
|
|
179
306
|
return () => {
|
|
180
307
|
scrollX.removeListener(id);
|
|
181
308
|
};
|
|
182
|
-
}, [scrollX]);
|
|
309
|
+
}, [cardCount, layout.stepDistance, scrollX, showNavigationTabs]);
|
|
310
|
+
(0, import_react.useEffect)(() => {
|
|
311
|
+
if (!showNavigationTabs) {
|
|
312
|
+
setScrollProgress(0);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const nextProgress = cardCount > 1 ? clamp(scrollXValueRef.current / layout.stepDistance, 0, cardCount - 1) : 0;
|
|
316
|
+
setScrollProgress(nextProgress);
|
|
317
|
+
}, [cardCount, layout.stepDistance, showNavigationTabs]);
|
|
183
318
|
(0, import_react.useEffect)(() => () => stopFocusTransitionAnimation(), [stopFocusTransitionAnimation]);
|
|
184
319
|
(0, import_react.useEffect)(() => {
|
|
185
320
|
if (cardCount > 1) {
|
|
@@ -214,6 +349,9 @@ function OverlappingCardsScrollRN({
|
|
|
214
349
|
const nextScrollLeft = clamp(safeIndex * layout.stepDistance, 0, layout.scrollRange);
|
|
215
350
|
const transitionMode = (_a = options.transitionMode) != null ? _a : "swoop";
|
|
216
351
|
if (transitionMode === "swoop" && cardCount > 1) {
|
|
352
|
+
if (showNavigationTabs) {
|
|
353
|
+
setScrollProgress(safeIndex);
|
|
354
|
+
}
|
|
217
355
|
const fromProgress = clamp(
|
|
218
356
|
scrollXValueRef.current / layout.stepDistance,
|
|
219
357
|
0,
|
|
@@ -270,6 +408,9 @@ function OverlappingCardsScrollRN({
|
|
|
270
408
|
if (((_c = options.animated) != null ? _c : true) === false) {
|
|
271
409
|
scrollX.setValue(nextScrollLeft);
|
|
272
410
|
scrollXValueRef.current = nextScrollLeft;
|
|
411
|
+
if (showNavigationTabs) {
|
|
412
|
+
setScrollProgress(safeIndex);
|
|
413
|
+
}
|
|
273
414
|
}
|
|
274
415
|
},
|
|
275
416
|
[
|
|
@@ -280,6 +421,7 @@ function OverlappingCardsScrollRN({
|
|
|
280
421
|
layout.scrollRange,
|
|
281
422
|
layout.stepDistance,
|
|
282
423
|
scrollX,
|
|
424
|
+
showNavigationTabs,
|
|
283
425
|
stopFocusTransitionAnimation
|
|
284
426
|
]
|
|
285
427
|
);
|
|
@@ -304,11 +446,13 @@ function OverlappingCardsScrollRN({
|
|
|
304
446
|
extrapolate: "clamp"
|
|
305
447
|
});
|
|
306
448
|
}, [focusTransition, focusTransitionProgress, layout.stepDistance, scrollX]);
|
|
449
|
+
const progress = showNavigationTabs ? scrollProgress : 0;
|
|
450
|
+
const activeIndex = Math.floor(progress);
|
|
307
451
|
const renderPageDots = (placement) => {
|
|
308
452
|
if (!showNavigationDots || resolvedPageDotsPosition !== placement) {
|
|
309
453
|
return null;
|
|
310
454
|
}
|
|
311
|
-
const rowStyle = placement === "above" ? [styles.pageDotsRow, { marginBottom:
|
|
455
|
+
const rowStyle = placement === "above" ? [styles.pageDotsRow, { marginBottom: resolvedPageDotsOffset }] : placement === "below" ? [styles.pageDotsRow, { marginTop: resolvedPageDotsOffset }] : [styles.pageDotsRow, styles.pageDotsOverlay, { bottom: resolvedPageDotsOffset }];
|
|
312
456
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
313
457
|
import_react_native.View,
|
|
314
458
|
{
|
|
@@ -344,12 +488,63 @@ function OverlappingCardsScrollRN({
|
|
|
344
488
|
}
|
|
345
489
|
);
|
|
346
490
|
};
|
|
491
|
+
const renderTabs = (position) => {
|
|
492
|
+
if (!showNavigationTabs || resolvedTabsPosition !== position || cardNames === null) {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
const containerStyle = position === "above" ? [styles.tabsRow, { marginBottom: resolvedTabsOffset }] : [styles.tabsRow, { marginTop: resolvedTabsOffset }];
|
|
496
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
497
|
+
TabsContainerComponent,
|
|
498
|
+
{
|
|
499
|
+
position,
|
|
500
|
+
className: `rn-ocs-tabs rn-ocs-tabs--${position}`,
|
|
501
|
+
style: containerStyle,
|
|
502
|
+
ariaLabel: "Card tabs",
|
|
503
|
+
cardNames,
|
|
504
|
+
activeIndex,
|
|
505
|
+
progress,
|
|
506
|
+
children: cardNames.map((name, index) => {
|
|
507
|
+
const influence = clamp(1 - Math.abs(progress - index), 0, 1);
|
|
508
|
+
const isPrincipal = influence > 0.98;
|
|
509
|
+
const animate = {
|
|
510
|
+
opacity: 0.45 + influence * 0.55
|
|
511
|
+
};
|
|
512
|
+
const pressTab = () => focusCard(index, {
|
|
513
|
+
animated: true,
|
|
514
|
+
transitionMode: "swoop"
|
|
515
|
+
});
|
|
516
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
517
|
+
TabsComponent,
|
|
518
|
+
{
|
|
519
|
+
name,
|
|
520
|
+
index,
|
|
521
|
+
position,
|
|
522
|
+
isPrincipal,
|
|
523
|
+
influence,
|
|
524
|
+
animate,
|
|
525
|
+
className: isPrincipal ? "rn-ocs-tab rn-ocs-tab--active" : "rn-ocs-tab",
|
|
526
|
+
style: { opacity: animate.opacity },
|
|
527
|
+
textStyle: isPrincipal ? styles.tabTextActive : void 0,
|
|
528
|
+
ariaLabel: `Go to ${name}`,
|
|
529
|
+
ariaCurrent: isPrincipal ? "page" : void 0,
|
|
530
|
+
accessibilityLabel: `Go to ${name}`,
|
|
531
|
+
accessibilityState: { selected: isPrincipal },
|
|
532
|
+
onPress: pressTab,
|
|
533
|
+
onClick: pressTab
|
|
534
|
+
},
|
|
535
|
+
`rn-ocs-tab-${position}-${index}`
|
|
536
|
+
);
|
|
537
|
+
})
|
|
538
|
+
}
|
|
539
|
+
);
|
|
540
|
+
};
|
|
347
541
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(OverlappingCardsScrollRNControllerContext.Provider, { value: controllerContextValue, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native.View, { style: [styles.shell, style], children: [
|
|
542
|
+
renderTabs("above"),
|
|
348
543
|
renderPageDots("above"),
|
|
349
544
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
350
545
|
import_react_native.View,
|
|
351
546
|
{
|
|
352
|
-
style: [styles.root, { height:
|
|
547
|
+
style: [styles.root, { height: resolvedCardHeight }],
|
|
353
548
|
onLayout: (event) => {
|
|
354
549
|
const width = event.nativeEvent.layout.width || 1;
|
|
355
550
|
setViewportWidth(Math.max(1, width));
|
|
@@ -360,8 +555,8 @@ function OverlappingCardsScrollRN({
|
|
|
360
555
|
{
|
|
361
556
|
ref: scrollRef,
|
|
362
557
|
horizontal: true,
|
|
363
|
-
style: [styles.scrollRegion, { height:
|
|
364
|
-
contentContainerStyle: { width: layout.trackWidth, height:
|
|
558
|
+
style: [styles.scrollRegion, { height: resolvedCardHeight }],
|
|
559
|
+
contentContainerStyle: { width: layout.trackWidth, height: resolvedCardHeight },
|
|
365
560
|
onScroll,
|
|
366
561
|
onScrollBeginDrag: cancelFocusTransition,
|
|
367
562
|
onMomentumScrollBegin: cancelFocusTransition,
|
|
@@ -371,7 +566,7 @@ function OverlappingCardsScrollRN({
|
|
|
371
566
|
snapToAlignment: shouldSnapToCard ? "start" : void 0,
|
|
372
567
|
decelerationRate: shouldSnapToCard ? snapDecelerationRate : "normal",
|
|
373
568
|
disableIntervalMomentum: shouldSnapToCard ? snapDisableIntervalMomentum : false,
|
|
374
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: [styles.track, { width: layout.trackWidth, height:
|
|
569
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: [styles.track, { width: layout.trackWidth, height: resolvedCardHeight }], children: cards.map((card, index) => {
|
|
375
570
|
var _a;
|
|
376
571
|
const restingRightX = index === 0 ? 0 : (index - 1) * layout.peek + layout.cardWidth;
|
|
377
572
|
const restingLeftX = index * layout.peek;
|
|
@@ -396,13 +591,14 @@ function OverlappingCardsScrollRN({
|
|
|
396
591
|
styles.card,
|
|
397
592
|
{
|
|
398
593
|
width: layout.cardWidth,
|
|
399
|
-
height:
|
|
594
|
+
height: resolvedCardHeight,
|
|
400
595
|
transform: [
|
|
401
596
|
{
|
|
402
597
|
translateX: import_react_native.Animated.add(scrollX, animatedCardX)
|
|
403
598
|
}
|
|
404
599
|
]
|
|
405
|
-
}
|
|
600
|
+
},
|
|
601
|
+
cardContainerStyle
|
|
406
602
|
],
|
|
407
603
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(OverlappingCardsScrollRNCardIndexContext.Provider, { value: index, children: card })
|
|
408
604
|
},
|
|
@@ -415,7 +611,8 @@ function OverlappingCardsScrollRN({
|
|
|
415
611
|
]
|
|
416
612
|
}
|
|
417
613
|
),
|
|
418
|
-
renderPageDots("below")
|
|
614
|
+
renderPageDots("below"),
|
|
615
|
+
renderTabs("below")
|
|
419
616
|
] }) });
|
|
420
617
|
}
|
|
421
618
|
var styles = import_react_native.StyleSheet.create({
|
|
@@ -466,6 +663,36 @@ var styles = import_react_native.StyleSheet.create({
|
|
|
466
663
|
borderRadius: 999,
|
|
467
664
|
backgroundColor: "#1f4666"
|
|
468
665
|
},
|
|
666
|
+
tabsRow: {
|
|
667
|
+
width: "100%",
|
|
668
|
+
flexDirection: "row",
|
|
669
|
+
alignItems: "center",
|
|
670
|
+
justifyContent: "center",
|
|
671
|
+
flexWrap: "wrap",
|
|
672
|
+
zIndex: 6
|
|
673
|
+
},
|
|
674
|
+
tab: {
|
|
675
|
+
borderRadius: 999,
|
|
676
|
+
borderWidth: 1,
|
|
677
|
+
borderColor: "rgba(30, 67, 99, 0.2)",
|
|
678
|
+
backgroundColor: "#eef5ff",
|
|
679
|
+
paddingHorizontal: 12,
|
|
680
|
+
paddingVertical: 6,
|
|
681
|
+
marginHorizontal: 4,
|
|
682
|
+
marginVertical: 4
|
|
683
|
+
},
|
|
684
|
+
tabPressed: {
|
|
685
|
+
opacity: 0.85
|
|
686
|
+
},
|
|
687
|
+
tabText: {
|
|
688
|
+
color: "#275070",
|
|
689
|
+
fontSize: 12,
|
|
690
|
+
fontWeight: "700",
|
|
691
|
+
letterSpacing: 0.2
|
|
692
|
+
},
|
|
693
|
+
tabTextActive: {
|
|
694
|
+
color: "#173047"
|
|
695
|
+
},
|
|
469
696
|
focusTrigger: {
|
|
470
697
|
alignSelf: "flex-start",
|
|
471
698
|
borderRadius: 99,
|