react-native-app-onboard 0.1.9 → 0.2.1
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 +89 -7
- package/lib/commonjs/components/CustomPages.js +31 -55
- package/lib/commonjs/components/CustomPages.js.map +1 -1
- package/lib/commonjs/components/OnboardingPages.js +63 -79
- package/lib/commonjs/components/OnboardingPages.js.map +1 -1
- package/lib/commonjs/components/Page.js +8 -3
- package/lib/commonjs/components/Page.js.map +1 -1
- package/lib/commonjs/components/Pagination.js +75 -13
- package/lib/commonjs/components/Pagination.js.map +1 -1
- package/lib/commonjs/components/Swiper.js +58 -85
- package/lib/commonjs/components/Swiper.js.map +1 -1
- package/lib/commonjs/components/button.js +3 -1
- package/lib/commonjs/components/button.js.map +1 -1
- package/lib/commonjs/context/OnboardingContext.js +101 -21
- package/lib/commonjs/context/OnboardingContext.js.map +1 -1
- package/lib/commonjs/hooks/useOnboarding.js +1 -1
- package/lib/commonjs/hooks/useOnboarding.js.map +1 -1
- package/lib/commonjs/index.js +33 -2
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/utils/color.js +308 -0
- package/lib/commonjs/utils/color.js.map +1 -0
- package/lib/commonjs/utils/persistence.js +51 -0
- package/lib/commonjs/utils/persistence.js.map +1 -0
- package/lib/module/components/CustomPages.js +31 -55
- package/lib/module/components/CustomPages.js.map +1 -1
- package/lib/module/components/OnboardingPages.js +64 -79
- package/lib/module/components/OnboardingPages.js.map +1 -1
- package/lib/module/components/Page.js +8 -3
- package/lib/module/components/Page.js.map +1 -1
- package/lib/module/components/Pagination.js +76 -14
- package/lib/module/components/Pagination.js.map +1 -1
- package/lib/module/components/Swiper.js +59 -86
- package/lib/module/components/Swiper.js.map +1 -1
- package/lib/module/components/button.js +3 -1
- package/lib/module/components/button.js.map +1 -1
- package/lib/module/context/OnboardingContext.js +102 -22
- package/lib/module/context/OnboardingContext.js.map +1 -1
- package/lib/module/hooks/useOnboarding.js +1 -1
- package/lib/module/hooks/useOnboarding.js.map +1 -1
- package/lib/module/index.js +8 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/color.js +299 -0
- package/lib/module/utils/color.js.map +1 -0
- package/lib/module/utils/persistence.js +42 -0
- package/lib/module/utils/persistence.js.map +1 -0
- package/lib/typescript/src/components/CustomPages.d.ts +6 -2
- package/lib/typescript/src/components/CustomPages.d.ts.map +1 -1
- package/lib/typescript/src/components/OnboardingPages.d.ts +6 -2
- package/lib/typescript/src/components/OnboardingPages.d.ts.map +1 -1
- package/lib/typescript/src/components/Page.d.ts +2 -0
- package/lib/typescript/src/components/Page.d.ts.map +1 -1
- package/lib/typescript/src/components/Pagination.d.ts +9 -0
- package/lib/typescript/src/components/Pagination.d.ts.map +1 -1
- package/lib/typescript/src/components/Swiper.d.ts.map +1 -1
- package/lib/typescript/src/components/button.d.ts.map +1 -1
- package/lib/typescript/src/context/OnboardingContext.d.ts +9 -0
- package/lib/typescript/src/context/OnboardingContext.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useOnboarding.d.ts +3 -0
- package/lib/typescript/src/hooks/useOnboarding.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types/index.d.ts +13 -0
- package/lib/typescript/src/types/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/color.d.ts +18 -0
- package/lib/typescript/src/utils/color.d.ts.map +1 -0
- package/lib/typescript/src/utils/persistence.d.ts +31 -0
- package/lib/typescript/src/utils/persistence.d.ts.map +1 -0
- package/package.json +12 -6
- package/src/components/CustomPages.tsx +62 -69
- package/src/components/OnboardingPages.tsx +86 -89
- package/src/components/Page.tsx +8 -2
- package/src/components/Pagination.tsx +117 -29
- package/src/components/Swiper.tsx +65 -87
- package/src/components/button.tsx +6 -1
- package/src/context/OnboardingContext.tsx +145 -26
- package/src/hooks/useOnboarding.tsx +1 -3
- package/src/index.tsx +16 -1
- package/src/types/index.ts +13 -0
- package/src/utils/color.ts +284 -0
- package/src/utils/persistence.ts +58 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
View,
|
|
3
|
+
Pressable,
|
|
3
4
|
Animated,
|
|
4
5
|
StyleSheet,
|
|
5
6
|
type StyleProp,
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
type TextStyle,
|
|
8
9
|
} from 'react-native';
|
|
9
10
|
import React from 'react';
|
|
11
|
+
import { setAlpha } from '../utils/color';
|
|
10
12
|
import { useOnboarding } from '../hooks/useOnboarding';
|
|
11
13
|
import { Button } from './button';
|
|
12
14
|
|
|
@@ -19,9 +21,11 @@ type FooterProps = {
|
|
|
19
21
|
showDone?: boolean;
|
|
20
22
|
showSkip?: boolean;
|
|
21
23
|
showNext?: boolean;
|
|
24
|
+
showPrevious?: boolean;
|
|
22
25
|
nextLabel?: string | React.ReactNode;
|
|
23
26
|
skipLabel?: string | React.ReactNode;
|
|
24
27
|
doneLabel?: string | React.ReactNode;
|
|
28
|
+
previousLabel?: string | React.ReactNode;
|
|
25
29
|
paginationContainerStyle?: StyleProp<ViewStyle>;
|
|
26
30
|
buttonRightContainerStyle?: StyleProp<ViewStyle>;
|
|
27
31
|
buttonLeftContainerStyle?: StyleProp<ViewStyle>;
|
|
@@ -29,20 +33,30 @@ type FooterProps = {
|
|
|
29
33
|
doneLabelStyle?: StyleProp<TextStyle>;
|
|
30
34
|
hasSkipPosition?: boolean;
|
|
31
35
|
skipLabelStyle?: StyleProp<TextStyle>;
|
|
36
|
+
previousLabelStyle?: StyleProp<TextStyle>;
|
|
32
37
|
skipButtonContainerStyle?: StyleProp<ViewStyle>;
|
|
33
38
|
nextButtonContainerStyle?: StyleProp<ViewStyle>;
|
|
34
39
|
doneButtonContainerStyle?: StyleProp<ViewStyle>;
|
|
40
|
+
previousButtonContainerStyle?: StyleProp<ViewStyle>;
|
|
35
41
|
nextLabelStyle?: StyleProp<TextStyle>;
|
|
36
42
|
paginationPosition?: 'top' | 'bottom';
|
|
43
|
+
paginationStyle?: 'dots' | 'progress';
|
|
44
|
+
progressBarStyle?: StyleProp<ViewStyle>;
|
|
45
|
+
progressBarFillStyle?: StyleProp<ViewStyle>;
|
|
46
|
+
dotsAreTappable?: boolean;
|
|
47
|
+
mirror?: boolean;
|
|
37
48
|
onDone?: () => void;
|
|
38
49
|
onSkip?: () => void;
|
|
39
50
|
onNext?: () => void;
|
|
40
51
|
};
|
|
41
52
|
|
|
42
53
|
export function Pagination(props: FooterProps) {
|
|
43
|
-
const { isDone } =
|
|
54
|
+
const { isDone, currentPage, progress, scrollTo, previousPage } =
|
|
55
|
+
useOnboarding();
|
|
44
56
|
const dots = Array.from({ length: props.numberOfScreens }, (_, i) => i);
|
|
45
57
|
const width = props.width;
|
|
58
|
+
const showPrevious = props.showPrevious && currentPage > 0;
|
|
59
|
+
|
|
46
60
|
return (
|
|
47
61
|
<View
|
|
48
62
|
style={[
|
|
@@ -60,7 +74,15 @@ export function Pagination(props: FooterProps) {
|
|
|
60
74
|
props.buttonLeftContainerStyle,
|
|
61
75
|
]}
|
|
62
76
|
>
|
|
63
|
-
{
|
|
77
|
+
{showPrevious && (
|
|
78
|
+
<Button
|
|
79
|
+
onPress={() => previousPage()}
|
|
80
|
+
buttonTextStyle={props.previousLabelStyle}
|
|
81
|
+
buttonStyle={props.previousButtonContainerStyle}
|
|
82
|
+
label={props.previousLabel || 'Back'}
|
|
83
|
+
/>
|
|
84
|
+
)}
|
|
85
|
+
{!showPrevious && props.showSkip && !props.hasSkipPosition && (
|
|
64
86
|
<Button
|
|
65
87
|
onPress={props.onSkip}
|
|
66
88
|
buttonTextStyle={props.skipLabelStyle}
|
|
@@ -69,29 +91,82 @@ export function Pagination(props: FooterProps) {
|
|
|
69
91
|
/>
|
|
70
92
|
)}
|
|
71
93
|
</View>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
|
|
95
|
+
{props.paginationStyle === 'progress' ? (
|
|
96
|
+
<View
|
|
97
|
+
accessible
|
|
98
|
+
accessibilityRole="progressbar"
|
|
99
|
+
accessibilityValue={{ min: 0, max: 100, now: progress }}
|
|
100
|
+
style={[
|
|
101
|
+
styles.progressTrack,
|
|
102
|
+
// Derive a faint track from the (background-aware) fill color so it
|
|
103
|
+
// stays visible on both light and dark pages.
|
|
104
|
+
{ backgroundColor: setAlpha(props.color, 0.25) },
|
|
105
|
+
// Mirror the fill direction so it grows from the trailing edge.
|
|
106
|
+
props.mirror && styles.mirror,
|
|
107
|
+
props.progressBarStyle,
|
|
108
|
+
]}
|
|
109
|
+
>
|
|
110
|
+
<View
|
|
111
|
+
style={[
|
|
112
|
+
styles.progressFill,
|
|
113
|
+
{ backgroundColor: props.color, width: `${progress}%` },
|
|
114
|
+
props.progressBarFillStyle,
|
|
115
|
+
]}
|
|
116
|
+
/>
|
|
117
|
+
</View>
|
|
118
|
+
) : (
|
|
119
|
+
<View
|
|
120
|
+
style={[
|
|
121
|
+
styles.dotsContainer,
|
|
122
|
+
props.mirror && styles.mirror,
|
|
123
|
+
props.dotsContainerStyle,
|
|
124
|
+
]}
|
|
125
|
+
>
|
|
126
|
+
{dots.map((_, i) => {
|
|
127
|
+
const inputRange = [(i - 1) * width, i * width, (i + 1) * width];
|
|
128
|
+
const dotOpacity = props.animatedValue.interpolate({
|
|
129
|
+
inputRange,
|
|
130
|
+
outputRange: [0.3, 1, 0.3],
|
|
131
|
+
extrapolate: 'clamp',
|
|
132
|
+
});
|
|
133
|
+
const dotProps = {
|
|
134
|
+
accessibilityRole: props.dotsAreTappable
|
|
135
|
+
? ('button' as const)
|
|
136
|
+
: ('image' as const),
|
|
137
|
+
accessibilityLabel: `Page ${i + 1} of ${props.numberOfScreens}`,
|
|
138
|
+
accessibilityState: { selected: i === currentPage },
|
|
139
|
+
};
|
|
140
|
+
const dot = (
|
|
141
|
+
<Animated.View
|
|
142
|
+
key={i}
|
|
143
|
+
style={[
|
|
144
|
+
styles.dot,
|
|
145
|
+
{
|
|
146
|
+
backgroundColor: props.color,
|
|
147
|
+
opacity: dotOpacity,
|
|
148
|
+
},
|
|
149
|
+
]}
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
152
|
+
return props.dotsAreTappable ? (
|
|
153
|
+
<Pressable
|
|
154
|
+
key={i}
|
|
155
|
+
onPress={() => scrollTo(i)}
|
|
156
|
+
hitSlop={8}
|
|
157
|
+
{...dotProps}
|
|
158
|
+
>
|
|
159
|
+
{dot}
|
|
160
|
+
</Pressable>
|
|
161
|
+
) : (
|
|
162
|
+
<View key={i} {...dotProps}>
|
|
163
|
+
{dot}
|
|
164
|
+
</View>
|
|
165
|
+
);
|
|
166
|
+
})}
|
|
167
|
+
</View>
|
|
168
|
+
)}
|
|
169
|
+
|
|
95
170
|
<View
|
|
96
171
|
style={[
|
|
97
172
|
styles.buttons,
|
|
@@ -138,21 +213,34 @@ const styles = StyleSheet.create({
|
|
|
138
213
|
},
|
|
139
214
|
dotsContainer: {
|
|
140
215
|
flexDirection: 'row',
|
|
141
|
-
flex:
|
|
216
|
+
flex: 2,
|
|
142
217
|
justifyContent: 'center',
|
|
218
|
+
alignItems: 'center',
|
|
219
|
+
},
|
|
220
|
+
progressTrack: {
|
|
221
|
+
flex: 2,
|
|
222
|
+
height: 6,
|
|
223
|
+
borderRadius: 3,
|
|
224
|
+
marginHorizontal: 16,
|
|
225
|
+
overflow: 'hidden',
|
|
226
|
+
},
|
|
227
|
+
progressFill: {
|
|
228
|
+
height: '100%',
|
|
229
|
+
borderRadius: 3,
|
|
143
230
|
},
|
|
144
231
|
text: {
|
|
145
232
|
fontSize: 16,
|
|
146
233
|
},
|
|
147
234
|
buttons: {
|
|
148
|
-
|
|
235
|
+
flex: 1,
|
|
149
236
|
},
|
|
150
237
|
rightButton: {
|
|
151
238
|
alignItems: 'flex-end',
|
|
152
|
-
paddingRight: 30,
|
|
153
239
|
},
|
|
154
240
|
leftButton: {
|
|
155
241
|
alignItems: 'flex-start',
|
|
156
|
-
|
|
242
|
+
},
|
|
243
|
+
mirror: {
|
|
244
|
+
transform: [{ scaleX: -1 }],
|
|
157
245
|
},
|
|
158
246
|
});
|
|
@@ -1,109 +1,87 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { OnboardingPages } from './OnboardingPages';
|
|
3
3
|
import { CustomPages } from './CustomPages';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
Animated,
|
|
6
|
+
I18nManager,
|
|
7
|
+
type NativeSyntheticEvent,
|
|
8
|
+
type NativeScrollEvent,
|
|
9
|
+
} from 'react-native';
|
|
5
10
|
import { useOnboarding } from '../hooks/useOnboarding';
|
|
6
11
|
import type { OnboardingProps } from '../types';
|
|
7
12
|
|
|
8
13
|
export const Swiper: React.FC<OnboardingProps> = (props) => {
|
|
9
|
-
|
|
14
|
+
// `scrollX` is always JS-driven because the background-color interpolation
|
|
15
|
+
// cannot run on the native thread. When `useNativeDriver` is enabled we also
|
|
16
|
+
// keep `nativeScrollX`, driven natively, for transform/opacity animations
|
|
17
|
+
// (the pagination dots), and mirror its offset onto `scrollX` via a JS
|
|
18
|
+
// listener so the color interpolation keeps working.
|
|
19
|
+
const scrollX = React.useMemo(() => new Animated.Value(0), []);
|
|
20
|
+
const nativeScrollX = React.useMemo(() => new Animated.Value(0), []);
|
|
21
|
+
const nativeDriverEnabled = props.useNativeDriver ?? false;
|
|
22
|
+
const dotsAnimatedValue = nativeDriverEnabled ? nativeScrollX : scrollX;
|
|
23
|
+
|
|
24
|
+
// Direction handling: default to the device direction. We only mirror
|
|
25
|
+
// manually (via scaleX) when the requested direction differs from the
|
|
26
|
+
// device's — when they match, React Native already lays the row out
|
|
27
|
+
// correctly and an extra flip would double-invert it.
|
|
28
|
+
const rtl = props.rtl ?? I18nManager.isRTL;
|
|
29
|
+
const mirror = rtl !== I18nManager.isRTL;
|
|
30
|
+
|
|
10
31
|
const {
|
|
11
|
-
|
|
32
|
+
setFlatListRef,
|
|
12
33
|
setCurrentPage,
|
|
13
34
|
currentPage,
|
|
14
35
|
numberOfScreens,
|
|
15
36
|
nextPage,
|
|
16
37
|
scrollEnabled,
|
|
38
|
+
pauseAutoPlay,
|
|
17
39
|
} = useOnboarding();
|
|
40
|
+
|
|
41
|
+
const onScroll = React.useMemo(
|
|
42
|
+
() =>
|
|
43
|
+
nativeDriverEnabled
|
|
44
|
+
? Animated.event(
|
|
45
|
+
[{ nativeEvent: { contentOffset: { x: nativeScrollX } } }],
|
|
46
|
+
{
|
|
47
|
+
useNativeDriver: true,
|
|
48
|
+
listener: (event: NativeSyntheticEvent<NativeScrollEvent>) =>
|
|
49
|
+
scrollX.setValue(event.nativeEvent.contentOffset.x),
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
: Animated.event([{ nativeEvent: { contentOffset: { x: scrollX } } }], {
|
|
53
|
+
useNativeDriver: false,
|
|
54
|
+
}),
|
|
55
|
+
[nativeDriverEnabled, nativeScrollX, scrollX]
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Stop autoplay as soon as the user takes manual control of the slider.
|
|
59
|
+
const onScrollBeginDrag = React.useCallback(
|
|
60
|
+
() => pauseAutoPlay(),
|
|
61
|
+
[pauseAutoPlay]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const shared = {
|
|
65
|
+
setFlatListRef,
|
|
66
|
+
scrollX,
|
|
67
|
+
dotsAnimatedValue,
|
|
68
|
+
onScroll,
|
|
69
|
+
onScrollBeginDrag,
|
|
70
|
+
setPage: setCurrentPage,
|
|
71
|
+
currentPage,
|
|
72
|
+
numberOfScreens,
|
|
73
|
+
nextPage,
|
|
74
|
+
scrollEnabled,
|
|
75
|
+
mirror,
|
|
76
|
+
};
|
|
77
|
+
|
|
18
78
|
if (props.children) {
|
|
19
79
|
return (
|
|
20
|
-
<CustomPages
|
|
21
|
-
customFooter={props.customFooter}
|
|
22
|
-
showPagination={props.showPagination}
|
|
23
|
-
flatListRef={flatListRef}
|
|
24
|
-
scrollX={scrollX}
|
|
25
|
-
setPage={setCurrentPage}
|
|
26
|
-
scrollEnabled={scrollEnabled}
|
|
27
|
-
currentPage={currentPage}
|
|
28
|
-
numberOfScreens={numberOfScreens}
|
|
29
|
-
nextPage={nextPage}
|
|
30
|
-
showDone={props.showDone}
|
|
31
|
-
showNext={props.showNext}
|
|
32
|
-
onDone={props.onDone}
|
|
33
|
-
skipButtonContainerStyle={props.skipButtonContainerStyle}
|
|
34
|
-
nextButtonContainerStyle={props.nextButtonContainerStyle}
|
|
35
|
-
doneButtonContainerStyle={props.doneButtonContainerStyle}
|
|
36
|
-
skipButtonPosition={props.skipButtonPosition}
|
|
37
|
-
paginationContainerStyle={props.paginationContainerStyle}
|
|
38
|
-
paginationPosition={props.paginationPosition}
|
|
39
|
-
nextLabel={props.nextLabel}
|
|
40
|
-
skipLabel={props.skipLabel}
|
|
41
|
-
doneLabel={props.doneLabel}
|
|
42
|
-
showSkip={props.showSkip}
|
|
43
|
-
onSkip={props.onSkip}
|
|
44
|
-
scrollAnimationDuration={props.scrollAnimationDuration}
|
|
45
|
-
buttonLeftContainerStyle={props.buttonLeftContainerStyle}
|
|
46
|
-
buttonRightContainerStyle={props.buttonRightContainerStyle}
|
|
47
|
-
dotsContainerStyle={props.dotsContainerStyle}
|
|
48
|
-
doneLabelStyle={props.doneLabelStyle}
|
|
49
|
-
skipLabelStyle={props.skipLabelStyle}
|
|
50
|
-
nextLabelStyle={props.nextLabelStyle}
|
|
51
|
-
width={props.width}
|
|
52
|
-
color={props.color}
|
|
53
|
-
useNativeDriver={props.useNativeDriver}
|
|
54
|
-
imageContainerStyle={props.imageContainerStyle}
|
|
55
|
-
containerStyle={props.containerStyle}
|
|
56
|
-
titleContainerStyle={props.titleContainerStyle}
|
|
57
|
-
titleStyle={props.titleStyle}
|
|
58
|
-
subtitleStyle={props.subtitleStyle}
|
|
59
|
-
swap={props.swap}
|
|
60
|
-
>
|
|
80
|
+
<CustomPages {...props} {...shared}>
|
|
61
81
|
{props.children}
|
|
62
82
|
</CustomPages>
|
|
63
83
|
);
|
|
64
84
|
}
|
|
65
85
|
|
|
66
|
-
return
|
|
67
|
-
<OnboardingPages
|
|
68
|
-
showDone={props.showDone}
|
|
69
|
-
customFooter={props.customFooter}
|
|
70
|
-
flatListRef={flatListRef}
|
|
71
|
-
scrollX={scrollX}
|
|
72
|
-
setPage={setCurrentPage}
|
|
73
|
-
currentPage={currentPage}
|
|
74
|
-
paginationPosition={props.paginationPosition}
|
|
75
|
-
nextPage={nextPage}
|
|
76
|
-
showSkip={props.showSkip}
|
|
77
|
-
onDone={props.onDone}
|
|
78
|
-
pages={props.pages || []}
|
|
79
|
-
width={props.width}
|
|
80
|
-
showNext={props.showNext}
|
|
81
|
-
skipButtonContainerStyle={props.skipButtonContainerStyle}
|
|
82
|
-
nextButtonContainerStyle={props.nextButtonContainerStyle}
|
|
83
|
-
doneButtonContainerStyle={props.doneButtonContainerStyle}
|
|
84
|
-
skipLabelStyle={props.skipLabelStyle}
|
|
85
|
-
skipButtonPosition={props.skipButtonPosition}
|
|
86
|
-
showPagination={props.showPagination}
|
|
87
|
-
color={props.color}
|
|
88
|
-
onSkip={props.onSkip}
|
|
89
|
-
swap={props.swap}
|
|
90
|
-
scrollEnabled={scrollEnabled}
|
|
91
|
-
nextLabel={props.nextLabel}
|
|
92
|
-
skipLabel={props.skipLabel}
|
|
93
|
-
doneLabel={props.doneLabel}
|
|
94
|
-
scrollAnimationDuration={props.scrollAnimationDuration}
|
|
95
|
-
buttonLeftContainerStyle={props.buttonLeftContainerStyle}
|
|
96
|
-
buttonRightContainerStyle={props.buttonRightContainerStyle}
|
|
97
|
-
dotsContainerStyle={props.dotsContainerStyle}
|
|
98
|
-
doneLabelStyle={props.doneLabelStyle}
|
|
99
|
-
nextLabelStyle={props.nextLabelStyle}
|
|
100
|
-
useNativeDriver={props.useNativeDriver}
|
|
101
|
-
imageContainerStyle={props.imageContainerStyle}
|
|
102
|
-
containerStyle={props.containerStyle}
|
|
103
|
-
titleContainerStyle={props.titleContainerStyle}
|
|
104
|
-
titleStyle={props.titleStyle}
|
|
105
|
-
subtitleStyle={props.subtitleStyle}
|
|
106
|
-
paginationContainerStyle={props.paginationContainerStyle}
|
|
107
|
-
/>
|
|
108
|
-
);
|
|
86
|
+
return <OnboardingPages {...props} {...shared} pages={props.pages || []} />;
|
|
109
87
|
};
|
|
@@ -18,7 +18,12 @@ type ButtonProps = {
|
|
|
18
18
|
|
|
19
19
|
export const Button = (props: ButtonProps) => {
|
|
20
20
|
return typeof props.label === 'string' ? (
|
|
21
|
-
<TouchableOpacity
|
|
21
|
+
<TouchableOpacity
|
|
22
|
+
onPress={props.onPress}
|
|
23
|
+
style={props.buttonStyle}
|
|
24
|
+
accessibilityRole="button"
|
|
25
|
+
accessibilityLabel={props.label}
|
|
26
|
+
>
|
|
22
27
|
<Text
|
|
23
28
|
style={[
|
|
24
29
|
styles.text,
|
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
AccessibilityInfo,
|
|
4
|
+
Animated,
|
|
5
|
+
Dimensions,
|
|
6
|
+
type FlatList,
|
|
7
|
+
} from 'react-native';
|
|
3
8
|
|
|
4
9
|
export type SliderProps = {
|
|
5
10
|
currentPage: number;
|
|
6
11
|
numberOfScreens: number;
|
|
7
12
|
nextPage: (animated?: boolean) => void;
|
|
13
|
+
previousPage: (animated?: boolean) => void;
|
|
8
14
|
scrollTo: (index: number, animated?: boolean) => void;
|
|
9
15
|
};
|
|
10
16
|
|
|
11
17
|
type OnboardingContextType = SliderProps & {
|
|
12
18
|
setCurrentPage: (index: number) => void;
|
|
13
19
|
flatListRef: React.RefObject<FlatList>;
|
|
20
|
+
// Callback ref forwarded to the underlying FlatList. Exposing a setter (vs.
|
|
21
|
+
// the ref object itself) keeps consumers React-Compiler-safe: assigning a
|
|
22
|
+
// foreign ref object to a `ref` prop is flagged as "ref access during
|
|
23
|
+
// render", whereas a callback ref is not.
|
|
24
|
+
setFlatListRef: (node: FlatList | null) => void;
|
|
14
25
|
width?: number;
|
|
15
26
|
numberOfScreens: number;
|
|
16
27
|
progress: number;
|
|
17
28
|
scrollEnabled?: boolean;
|
|
18
29
|
enableScroll: React.Dispatch<React.SetStateAction<boolean | undefined>>;
|
|
19
30
|
isDone: boolean;
|
|
31
|
+
pauseAutoPlay: () => void;
|
|
32
|
+
resumeAutoPlay: () => void;
|
|
20
33
|
};
|
|
21
34
|
|
|
22
35
|
type OnboardingProviderProps = {
|
|
@@ -24,6 +37,11 @@ type OnboardingProviderProps = {
|
|
|
24
37
|
width?: number;
|
|
25
38
|
numberOfScreens: number;
|
|
26
39
|
scrollEnabled?: boolean;
|
|
40
|
+
onPageChange?: (index: number) => void;
|
|
41
|
+
scrollAnimationDuration?: number;
|
|
42
|
+
autoPlay?: boolean;
|
|
43
|
+
autoPlayInterval?: number;
|
|
44
|
+
loop?: boolean;
|
|
27
45
|
};
|
|
28
46
|
|
|
29
47
|
export const OnboardingContext = React.createContext<
|
|
@@ -32,10 +50,16 @@ export const OnboardingContext = React.createContext<
|
|
|
32
50
|
|
|
33
51
|
export const OnboardingProvider: React.FC<OnboardingProviderProps> = ({
|
|
34
52
|
children,
|
|
35
|
-
width
|
|
53
|
+
width: widthProp,
|
|
36
54
|
numberOfScreens,
|
|
37
55
|
scrollEnabled,
|
|
56
|
+
onPageChange,
|
|
57
|
+
scrollAnimationDuration,
|
|
58
|
+
autoPlay = false,
|
|
59
|
+
autoPlayInterval = 3000,
|
|
60
|
+
loop = false,
|
|
38
61
|
}) => {
|
|
62
|
+
const width = widthProp ?? Dimensions.get('window').width;
|
|
39
63
|
const getProgress = (page: number) => {
|
|
40
64
|
return Math.round(((page + 1) / numberOfScreens) * 100);
|
|
41
65
|
};
|
|
@@ -46,45 +70,140 @@ export const OnboardingProvider: React.FC<OnboardingProviderProps> = ({
|
|
|
46
70
|
const [enableScroll, setEnableScroll] = React.useState<boolean | undefined>(
|
|
47
71
|
scrollEnabled
|
|
48
72
|
);
|
|
49
|
-
const flatListRef = React.useRef<FlatList>(null);
|
|
73
|
+
const flatListRef = React.useRef<FlatList | null>(null);
|
|
74
|
+
const setFlatListRef = React.useCallback((node: FlatList | null) => {
|
|
75
|
+
flatListRef.current = node;
|
|
76
|
+
}, []);
|
|
77
|
+
// Tracks the latest page so timers/animations read a fresh value without
|
|
78
|
+
// needing to be recreated on every page change.
|
|
79
|
+
const currentPageRef = React.useRef(0);
|
|
80
|
+
const [isAutoPlaying, setIsAutoPlaying] = React.useState(autoPlay);
|
|
50
81
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
};
|
|
82
|
+
// Keep autoplay in sync if the `autoPlay` prop is toggled after mount.
|
|
83
|
+
React.useEffect(() => {
|
|
84
|
+
setIsAutoPlaying(autoPlay);
|
|
85
|
+
}, [autoPlay]);
|
|
56
86
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
87
|
+
// Dedicated value used to honor a custom scrollAnimationDuration. FlatList's
|
|
88
|
+
// own animated scroll has a fixed, platform-controlled duration, so when a
|
|
89
|
+
// duration is requested we drive the offset manually via this value.
|
|
90
|
+
const scrollAnim = React.useMemo(() => new Animated.Value(0), []);
|
|
91
|
+
React.useEffect(() => {
|
|
92
|
+
const id = scrollAnim.addListener(({ value }) => {
|
|
93
|
+
flatListRef.current?.scrollToOffset({ offset: value, animated: false });
|
|
94
|
+
});
|
|
95
|
+
return () => scrollAnim.removeListener(id);
|
|
96
|
+
}, [scrollAnim]);
|
|
66
97
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
flatListRef.current
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
98
|
+
const animateToOffset = React.useCallback(
|
|
99
|
+
(offset: number, animated: boolean) => {
|
|
100
|
+
if (!flatListRef.current) return;
|
|
101
|
+
if (animated && scrollAnimationDuration) {
|
|
102
|
+
scrollAnim.stopAnimation();
|
|
103
|
+
scrollAnim.setValue(width * currentPageRef.current);
|
|
104
|
+
Animated.timing(scrollAnim, {
|
|
105
|
+
toValue: offset,
|
|
106
|
+
duration: scrollAnimationDuration,
|
|
107
|
+
useNativeDriver: false,
|
|
108
|
+
}).start();
|
|
109
|
+
} else {
|
|
110
|
+
flatListRef.current.scrollToOffset({ offset, animated });
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
[scrollAnim, scrollAnimationDuration, width]
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const setCurrentPage = React.useCallback(
|
|
117
|
+
(index: number) => {
|
|
118
|
+
setPage(index);
|
|
119
|
+
currentPageRef.current = index;
|
|
120
|
+
setProgress(Math.round(((index + 1) / numberOfScreens) * 100));
|
|
121
|
+
setIsDone(index === numberOfScreens - 1);
|
|
122
|
+
onPageChange?.(index);
|
|
123
|
+
// No-op when no screen reader is active; announces the page otherwise.
|
|
124
|
+
AccessibilityInfo.announceForAccessibility(
|
|
125
|
+
`Page ${index + 1} of ${numberOfScreens}`
|
|
126
|
+
);
|
|
127
|
+
},
|
|
128
|
+
[numberOfScreens, onPageChange]
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const nextPage = React.useCallback(
|
|
132
|
+
(animated: boolean = true) => {
|
|
133
|
+
const current = currentPageRef.current;
|
|
134
|
+
if (current < numberOfScreens - 1) {
|
|
135
|
+
animateToOffset(width * (current + 1), animated);
|
|
136
|
+
setCurrentPage(current + 1);
|
|
137
|
+
} else if (loop) {
|
|
138
|
+
animateToOffset(0, animated);
|
|
139
|
+
setCurrentPage(0);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
[numberOfScreens, loop, width, animateToOffset, setCurrentPage]
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const previousPage = React.useCallback(
|
|
146
|
+
(animated: boolean = true) => {
|
|
147
|
+
const current = currentPageRef.current;
|
|
148
|
+
if (current > 0) {
|
|
149
|
+
animateToOffset(width * (current - 1), animated);
|
|
150
|
+
setCurrentPage(current - 1);
|
|
151
|
+
} else if (loop) {
|
|
152
|
+
animateToOffset(width * (numberOfScreens - 1), animated);
|
|
153
|
+
setCurrentPage(numberOfScreens - 1);
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
[numberOfScreens, loop, width, animateToOffset, setCurrentPage]
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const scrollTo = React.useCallback(
|
|
160
|
+
(index: number, animated: boolean = true) => {
|
|
161
|
+
if (index >= 0 && index < numberOfScreens) {
|
|
162
|
+
animateToOffset(index * width, animated);
|
|
163
|
+
setCurrentPage(index);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
[numberOfScreens, width, animateToOffset, setCurrentPage]
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const pauseAutoPlay = React.useCallback(() => setIsAutoPlaying(false), []);
|
|
170
|
+
const resumeAutoPlay = React.useCallback(
|
|
171
|
+
() => setIsAutoPlaying(autoPlay),
|
|
172
|
+
[autoPlay]
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Autoplay timer. Recreated whenever the page changes so it always advances
|
|
176
|
+
// from the current position; pauses when the user interacts with the slider.
|
|
177
|
+
React.useEffect(() => {
|
|
178
|
+
if (!isAutoPlaying || numberOfScreens <= 1) return;
|
|
179
|
+
if (!loop && currentPage >= numberOfScreens - 1) return;
|
|
180
|
+
const timer = setTimeout(() => nextPage(true), autoPlayInterval);
|
|
181
|
+
return () => clearTimeout(timer);
|
|
182
|
+
}, [
|
|
183
|
+
isAutoPlaying,
|
|
184
|
+
currentPage,
|
|
185
|
+
numberOfScreens,
|
|
186
|
+
autoPlayInterval,
|
|
187
|
+
loop,
|
|
188
|
+
nextPage,
|
|
189
|
+
]);
|
|
76
190
|
|
|
77
191
|
const contextValue: OnboardingContextType = {
|
|
78
192
|
scrollEnabled: enableScroll,
|
|
79
193
|
enableScroll: setEnableScroll,
|
|
194
|
+
width,
|
|
80
195
|
currentPage,
|
|
81
196
|
numberOfScreens,
|
|
82
197
|
nextPage,
|
|
198
|
+
previousPage,
|
|
83
199
|
setCurrentPage,
|
|
84
200
|
flatListRef,
|
|
201
|
+
setFlatListRef,
|
|
85
202
|
scrollTo,
|
|
86
203
|
progress,
|
|
87
204
|
isDone,
|
|
205
|
+
pauseAutoPlay,
|
|
206
|
+
resumeAutoPlay,
|
|
88
207
|
};
|
|
89
208
|
|
|
90
209
|
return (
|
|
@@ -4,9 +4,7 @@ import { OnboardingContext } from '../context/OnboardingContext';
|
|
|
4
4
|
export const useOnboarding = () => {
|
|
5
5
|
const context = React.useContext(OnboardingContext);
|
|
6
6
|
if (!context) {
|
|
7
|
-
throw new Error(
|
|
8
|
-
'useOnboardingContext must be used within an OnboardingProvider'
|
|
9
|
-
);
|
|
7
|
+
throw new Error('useOnboarding must be used within an OnboardingProvider');
|
|
10
8
|
}
|
|
11
9
|
return context;
|
|
12
10
|
};
|
package/src/index.tsx
CHANGED
|
@@ -1,15 +1,30 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Swiper } from './components
|
|
2
|
+
import { Swiper } from './components';
|
|
3
3
|
import { OnboardingProvider } from './context/OnboardingContext';
|
|
4
4
|
import type { OnboardingProps } from './types';
|
|
5
5
|
|
|
6
6
|
export { useOnboarding } from './hooks/useOnboarding';
|
|
7
|
+
export type { Page } from './components/Page';
|
|
8
|
+
export type { OnboardingProps } from './types';
|
|
9
|
+
export {
|
|
10
|
+
createOnboardingStorage,
|
|
11
|
+
hasCompletedOnboarding,
|
|
12
|
+
markOnboardingComplete,
|
|
13
|
+
resetOnboarding,
|
|
14
|
+
} from './utils/persistence';
|
|
15
|
+
export type { OnboardingStorageAdapter } from './utils/persistence';
|
|
7
16
|
|
|
8
17
|
export function Onboarding(props: OnboardingProps) {
|
|
9
18
|
const numberOfScreens = React.Children.count(props.children);
|
|
10
19
|
return (
|
|
11
20
|
<OnboardingProvider
|
|
21
|
+
width={props.width}
|
|
12
22
|
scrollEnabled={props.scrollEnabled}
|
|
23
|
+
onPageChange={props.onPageChange}
|
|
24
|
+
scrollAnimationDuration={props.scrollAnimationDuration}
|
|
25
|
+
autoPlay={props.autoPlay}
|
|
26
|
+
autoPlayInterval={props.autoPlayInterval}
|
|
27
|
+
loop={props.loop}
|
|
13
28
|
numberOfScreens={numberOfScreens || props.pages?.length || 0}
|
|
14
29
|
>
|
|
15
30
|
<Swiper {...props}>{props.children}</Swiper>
|