react-native-app-onboard 0.1.9 → 0.2.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.
Files changed (73) hide show
  1. package/README.md +89 -7
  2. package/lib/commonjs/components/CustomPages.js +31 -55
  3. package/lib/commonjs/components/CustomPages.js.map +1 -1
  4. package/lib/commonjs/components/OnboardingPages.js +59 -74
  5. package/lib/commonjs/components/OnboardingPages.js.map +1 -1
  6. package/lib/commonjs/components/Page.js +8 -3
  7. package/lib/commonjs/components/Page.js.map +1 -1
  8. package/lib/commonjs/components/Pagination.js +75 -13
  9. package/lib/commonjs/components/Pagination.js.map +1 -1
  10. package/lib/commonjs/components/Swiper.js +58 -85
  11. package/lib/commonjs/components/Swiper.js.map +1 -1
  12. package/lib/commonjs/components/button.js +3 -1
  13. package/lib/commonjs/components/button.js.map +1 -1
  14. package/lib/commonjs/context/OnboardingContext.js +101 -21
  15. package/lib/commonjs/context/OnboardingContext.js.map +1 -1
  16. package/lib/commonjs/hooks/useOnboarding.js +1 -1
  17. package/lib/commonjs/hooks/useOnboarding.js.map +1 -1
  18. package/lib/commonjs/index.js +33 -2
  19. package/lib/commonjs/index.js.map +1 -1
  20. package/lib/commonjs/utils/persistence.js +51 -0
  21. package/lib/commonjs/utils/persistence.js.map +1 -0
  22. package/lib/module/components/CustomPages.js +31 -55
  23. package/lib/module/components/CustomPages.js.map +1 -1
  24. package/lib/module/components/OnboardingPages.js +60 -75
  25. package/lib/module/components/OnboardingPages.js.map +1 -1
  26. package/lib/module/components/Page.js +8 -3
  27. package/lib/module/components/Page.js.map +1 -1
  28. package/lib/module/components/Pagination.js +76 -14
  29. package/lib/module/components/Pagination.js.map +1 -1
  30. package/lib/module/components/Swiper.js +59 -86
  31. package/lib/module/components/Swiper.js.map +1 -1
  32. package/lib/module/components/button.js +3 -1
  33. package/lib/module/components/button.js.map +1 -1
  34. package/lib/module/context/OnboardingContext.js +102 -22
  35. package/lib/module/context/OnboardingContext.js.map +1 -1
  36. package/lib/module/hooks/useOnboarding.js +1 -1
  37. package/lib/module/hooks/useOnboarding.js.map +1 -1
  38. package/lib/module/index.js +8 -1
  39. package/lib/module/index.js.map +1 -1
  40. package/lib/module/utils/persistence.js +42 -0
  41. package/lib/module/utils/persistence.js.map +1 -0
  42. package/lib/typescript/src/components/CustomPages.d.ts +6 -2
  43. package/lib/typescript/src/components/CustomPages.d.ts.map +1 -1
  44. package/lib/typescript/src/components/OnboardingPages.d.ts +6 -2
  45. package/lib/typescript/src/components/OnboardingPages.d.ts.map +1 -1
  46. package/lib/typescript/src/components/Page.d.ts +2 -0
  47. package/lib/typescript/src/components/Page.d.ts.map +1 -1
  48. package/lib/typescript/src/components/Pagination.d.ts +9 -0
  49. package/lib/typescript/src/components/Pagination.d.ts.map +1 -1
  50. package/lib/typescript/src/components/Swiper.d.ts.map +1 -1
  51. package/lib/typescript/src/components/button.d.ts.map +1 -1
  52. package/lib/typescript/src/context/OnboardingContext.d.ts +9 -0
  53. package/lib/typescript/src/context/OnboardingContext.d.ts.map +1 -1
  54. package/lib/typescript/src/hooks/useOnboarding.d.ts +3 -0
  55. package/lib/typescript/src/hooks/useOnboarding.d.ts.map +1 -1
  56. package/lib/typescript/src/index.d.ts +4 -0
  57. package/lib/typescript/src/index.d.ts.map +1 -1
  58. package/lib/typescript/src/types/index.d.ts +13 -0
  59. package/lib/typescript/src/types/index.d.ts.map +1 -1
  60. package/lib/typescript/src/utils/persistence.d.ts +31 -0
  61. package/lib/typescript/src/utils/persistence.d.ts.map +1 -0
  62. package/package.json +13 -3
  63. package/src/components/CustomPages.tsx +62 -69
  64. package/src/components/OnboardingPages.tsx +79 -82
  65. package/src/components/Page.tsx +8 -2
  66. package/src/components/Pagination.tsx +121 -29
  67. package/src/components/Swiper.tsx +65 -87
  68. package/src/components/button.tsx +6 -1
  69. package/src/context/OnboardingContext.tsx +145 -26
  70. package/src/hooks/useOnboarding.tsx +1 -3
  71. package/src/index.tsx +16 -1
  72. package/src/types/index.ts +13 -0
  73. package/src/utils/persistence.ts +58 -0
@@ -1,6 +1,13 @@
1
- import React, { useRef } from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import tinycolor from 'tinycolor2';
3
- import { Animated, StyleSheet, Dimensions, FlatList } from 'react-native';
3
+ import {
4
+ Animated,
5
+ StyleSheet,
6
+ Dimensions,
7
+ FlatList,
8
+ type NativeSyntheticEvent,
9
+ type NativeScrollEvent,
10
+ } from 'react-native';
4
11
  import { Pagination } from './Pagination';
5
12
  import { OnboardingPage, type Page } from './Page';
6
13
  import type { OnboardingProps } from '../types';
@@ -12,9 +19,13 @@ type Props = OnboardingProps & {
12
19
  pages: Page[];
13
20
  currentPage: number;
14
21
  setPage: (newPageIndex: number) => void;
15
- flatListRef: React.RefObject<FlatList>;
22
+ setFlatListRef: (node: FlatList | null) => void;
16
23
  scrollX: Animated.Value;
24
+ dotsAnimatedValue: Animated.Value;
25
+ onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
26
+ onScrollBeginDrag: () => void;
17
27
  nextPage: () => void;
28
+ mirror?: boolean;
18
29
  };
19
30
 
20
31
  export const OnboardingPages = ({
@@ -22,10 +33,9 @@ export const OnboardingPages = ({
22
33
  showNext = true,
23
34
  ...props
24
35
  }: Props) => {
25
- const backgroundColorAnim = useRef(new Animated.Value(0)).current;
26
- const [previousPage, setPreviousPage] = React.useState(0);
36
+ const pageWidth = props.width || width;
27
37
  const currentPage_ = props.pages[props.currentPage];
28
- const currentBackgroundColor = currentPage_?.backgroundColor || '';
38
+ const currentBackgroundColor = currentPage_?.backgroundColor ?? 'white';
29
39
  const isLight = tinycolor(currentBackgroundColor).getBrightness() > 180;
30
40
  const footerBackgroundColor = isLight
31
41
  ? tinycolor(currentBackgroundColor).darken(30).toString()
@@ -35,27 +45,62 @@ export const OnboardingPages = ({
35
45
  ? tinycolor(footerBackgroundColor).darken(60).toString()
36
46
  : tinycolor(footerBackgroundColor).lighten(60).toString();
37
47
 
38
- const previousBackgroundColor =
39
- props.pages[previousPage]?.backgroundColor || 'white';
40
- // Interpolating background color based on backgroundColorAnim value
41
- const interpolatedBackgroundColor = backgroundColorAnim.interpolate({
42
- inputRange: [0, 1],
43
- outputRange: [currentBackgroundColor, previousBackgroundColor],
44
- extrapolate: 'clamp',
45
- });
48
+ const interpolatedBackgroundColor = useMemo(() => {
49
+ const pages = props.pages;
50
+ // interpolate() requires at least 2 stops; for 0 or 1 pages there is
51
+ // nothing to animate between, so use the solid current color.
52
+ if (pages.length < 2) return currentBackgroundColor;
53
+ const inputRange = pages.map((_, i) => i * pageWidth);
54
+ const outputRange = pages.map((p) => p.backgroundColor ?? 'white');
55
+ return props.scrollX.interpolate({
56
+ inputRange,
57
+ outputRange,
58
+ extrapolate: 'clamp',
59
+ });
60
+ }, [props.pages, props.scrollX, pageWidth, currentBackgroundColor]);
46
61
 
47
- const setPage_ = (newPageIndex: number) => {
48
- setPreviousPage(props.currentPage);
49
- props.setPage(newPageIndex);
62
+ const paginationProps = {
63
+ width: pageWidth,
64
+ onNext: props.nextPage,
65
+ onSkip: props.onSkip,
66
+ color,
67
+ showNext,
68
+ onDone: props.onDone,
69
+ showDone: props.showDone,
70
+ showPrevious: props.showPrevious,
71
+ backgroundColor: footerBackgroundColor,
72
+ animatedValue: props.dotsAnimatedValue,
73
+ showSkip: props.showSkip,
74
+ numberOfScreens: props.pages.length,
75
+ skipLabel: props.skipLabel,
76
+ nextLabel: props.nextLabel,
77
+ previousLabel: props.previousLabel,
78
+ hasSkipPosition: !!props.skipButtonPosition,
79
+ doneLabel: props.doneLabel,
80
+ paginationStyle: props.paginationStyle,
81
+ progressBarStyle: props.progressBarStyle,
82
+ progressBarFillStyle: props.progressBarFillStyle,
83
+ dotsAreTappable: props.dotsAreTappable,
84
+ mirror: props.mirror,
85
+ paginationContainerStyle: props.paginationContainerStyle,
86
+ buttonRightContainerStyle: props.buttonRightContainerStyle,
87
+ buttonLeftContainerStyle: props.buttonLeftContainerStyle,
88
+ dotsContainerStyle: props.dotsContainerStyle,
89
+ doneLabelStyle: props.doneLabelStyle,
90
+ skipLabelStyle: props.skipLabelStyle,
91
+ previousLabelStyle: props.previousLabelStyle,
92
+ nextLabelStyle: props.nextLabelStyle,
93
+ skipButtonContainerStyle: props.skipButtonContainerStyle,
94
+ nextButtonContainerStyle: props.nextButtonContainerStyle,
95
+ doneButtonContainerStyle: props.doneButtonContainerStyle,
96
+ previousButtonContainerStyle: props.previousButtonContainerStyle,
50
97
  };
51
98
 
52
99
  return (
53
100
  <Animated.View
54
101
  style={[
55
102
  styles.container,
56
- {
57
- backgroundColor: interpolatedBackgroundColor,
58
- },
103
+ { backgroundColor: interpolatedBackgroundColor },
59
104
  ]}
60
105
  >
61
106
  {props.skipButtonPosition && props.showSkip && (
@@ -73,62 +118,37 @@ export const OnboardingPages = ({
73
118
  props.customFooter &&
74
119
  props.customFooter({ nextPage: props.nextPage })}
75
120
  {!props.customFooter && showPagination && (
76
- <Pagination
77
- width={props.width || width}
78
- onNext={props.nextPage}
79
- onSkip={props.onSkip}
80
- color={color}
81
- showNext={showNext}
82
- onDone={props.onDone}
83
- showDone={props.showDone}
84
- backgroundColor={footerBackgroundColor}
85
- animatedValue={props.scrollX}
86
- showSkip={props.showSkip}
87
- numberOfScreens={props.pages.length}
88
- skipLabel={props.skipLabel}
89
- nextLabel={props.nextLabel}
90
- hasSkipPosition={!!props.skipButtonPosition}
91
- doneLabel={props.doneLabel}
92
- paginationContainerStyle={props.paginationContainerStyle}
93
- buttonRightContainerStyle={props.buttonRightContainerStyle}
94
- buttonLeftContainerStyle={props.buttonLeftContainerStyle}
95
- dotsContainerStyle={props.dotsContainerStyle}
96
- doneLabelStyle={props.doneLabelStyle}
97
- skipLabelStyle={props.skipLabelStyle}
98
- nextLabelStyle={props.nextLabelStyle}
99
- skipButtonContainerStyle={props.skipButtonContainerStyle}
100
- nextButtonContainerStyle={props.nextButtonContainerStyle}
101
- doneButtonContainerStyle={props.doneButtonContainerStyle}
102
- />
121
+ <Pagination {...paginationProps} />
103
122
  )}
104
123
  </>
105
124
  )}
106
125
  <Animated.FlatList
107
- ref={props.flatListRef}
126
+ ref={(node) => props.setFlatListRef(node as FlatList | null)}
108
127
  data={props.pages}
109
128
  horizontal
110
129
  pagingEnabled
111
130
  showsHorizontalScrollIndicator={false}
131
+ scrollEnabled={props.scrollEnabled}
132
+ style={props.mirror ? styles.mirror : undefined}
112
133
  keyExtractor={(_, index) => index.toString()}
113
134
  renderItem={({ item, index }) => (
114
135
  <OnboardingPage
115
136
  color={color}
116
- width={props.width || width}
137
+ width={pageWidth}
117
138
  swap={props.swap}
139
+ mirror={props.mirror}
118
140
  key={index}
119
141
  {...item}
120
142
  />
121
143
  )}
122
- onScroll={Animated.event(
123
- [{ nativeEvent: { contentOffset: { x: props.scrollX } } }],
124
- { useNativeDriver: false }
125
- )}
144
+ onScroll={props.onScroll}
145
+ onScrollBeginDrag={props.onScrollBeginDrag}
126
146
  scrollEventThrottle={16}
127
147
  onMomentumScrollEnd={(event) => {
128
148
  const pageIndex = Math.round(
129
- event.nativeEvent.contentOffset.x / (props.width || width)
149
+ event.nativeEvent.contentOffset.x / pageWidth
130
150
  );
131
- setPage_(pageIndex || 0);
151
+ props.setPage(pageIndex || 0);
132
152
  }}
133
153
  />
134
154
  {props.paginationPosition !== 'top' && (
@@ -137,33 +157,7 @@ export const OnboardingPages = ({
137
157
  props.customFooter &&
138
158
  props.customFooter({ nextPage: props.nextPage })}
139
159
  {!props.customFooter && showPagination && (
140
- <Pagination
141
- width={props.width || width}
142
- onNext={props.nextPage}
143
- onSkip={props.onSkip}
144
- color={color}
145
- hasSkipPosition={!!props.skipButtonPosition}
146
- onDone={props.onDone}
147
- showDone={props.showDone}
148
- backgroundColor={footerBackgroundColor}
149
- animatedValue={props.scrollX}
150
- showSkip={props.showSkip}
151
- numberOfScreens={props.pages.length}
152
- skipLabel={props.skipLabel}
153
- nextLabel={props.nextLabel}
154
- doneLabel={props.doneLabel}
155
- paginationContainerStyle={props.paginationContainerStyle}
156
- buttonRightContainerStyle={props.buttonRightContainerStyle}
157
- buttonLeftContainerStyle={props.buttonLeftContainerStyle}
158
- dotsContainerStyle={props.dotsContainerStyle}
159
- doneLabelStyle={props.doneLabelStyle}
160
- skipLabelStyle={props.skipLabelStyle}
161
- skipButtonContainerStyle={props.skipButtonContainerStyle}
162
- nextButtonContainerStyle={props.nextButtonContainerStyle}
163
- doneButtonContainerStyle={props.doneButtonContainerStyle}
164
- showNext={showNext}
165
- nextLabelStyle={props.nextLabelStyle}
166
- />
160
+ <Pagination {...paginationProps} />
167
161
  )}
168
162
  </>
169
163
  )}
@@ -202,4 +196,7 @@ const styles = StyleSheet.create({
202
196
  flex: 1,
203
197
  justifyContent: 'center',
204
198
  },
199
+ mirror: {
200
+ transform: [{ scaleX: -1 }],
201
+ },
205
202
  });
@@ -22,10 +22,12 @@ export type Page = {
22
22
  titleStyle?: StyleProp<TextStyle>;
23
23
  subtitleStyle?: StyleProp<TextStyle>;
24
24
  swap?: boolean;
25
+ /** Internal: counter-flips page content when the slider is mirrored for RTL. */
26
+ mirror?: boolean;
25
27
  };
26
28
 
27
29
  const { width, height } = Dimensions.get('window');
28
- const potrait = height > width;
30
+ const portrait = height > width;
29
31
 
30
32
  export function OnboardingPage(props: Page) {
31
33
  return (
@@ -35,6 +37,7 @@ export function OnboardingPage(props: Page) {
35
37
  { width: props.width },
36
38
  props.containerStyle,
37
39
  props.swap && styles.swapStyle,
40
+ props.mirror && styles.mirror,
38
41
  ]}
39
42
  >
40
43
  <View style={[styles.imageContainer, props.imageContainerStyle]}>
@@ -87,7 +90,7 @@ const styles = StyleSheet.create({
87
90
  },
88
91
  imageContainer: {
89
92
  flex: 0,
90
- paddingBottom: potrait ? 60 : 10,
93
+ paddingBottom: portrait ? 60 : 10,
91
94
  alignItems: 'center',
92
95
  width: '100%',
93
96
  },
@@ -97,4 +100,7 @@ const styles = StyleSheet.create({
97
100
  swapStyle: {
98
101
  flexDirection: 'column-reverse',
99
102
  },
103
+ mirror: {
104
+ transform: [{ scaleX: -1 }],
105
+ },
100
106
  });
@@ -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 tinycolor from 'tinycolor2';
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 } = useOnboarding();
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
- {props.showSkip && !props.hasSkipPosition && (
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,86 @@ export function Pagination(props: FooterProps) {
69
91
  />
70
92
  )}
71
93
  </View>
72
- <View style={styles.dotsContainer}>
73
- {dots.map((_, i) => {
74
- const inputRange = [(i - 1) * width, i * width, (i + 1) * width];
75
- const dotOpacity = props.animatedValue.interpolate({
76
- inputRange,
77
- outputRange: [0.3, 1, 0.3],
78
- extrapolate: 'clamp',
79
- });
80
- return (
81
- <Animated.View
82
- key={i}
83
- style={[
84
- styles.dot,
85
- {
86
- backgroundColor: props.color,
87
- opacity: dotOpacity,
88
- },
89
- props.dotsContainerStyle,
90
- ]}
91
- />
92
- );
93
- })}
94
- </View>
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
+ {
105
+ backgroundColor: tinycolor(props.color)
106
+ .setAlpha(0.25)
107
+ .toRgbString(),
108
+ },
109
+ // Mirror the fill direction so it grows from the trailing edge.
110
+ props.mirror && styles.mirror,
111
+ props.progressBarStyle,
112
+ ]}
113
+ >
114
+ <View
115
+ style={[
116
+ styles.progressFill,
117
+ { backgroundColor: props.color, width: `${progress}%` },
118
+ props.progressBarFillStyle,
119
+ ]}
120
+ />
121
+ </View>
122
+ ) : (
123
+ <View
124
+ style={[
125
+ styles.dotsContainer,
126
+ props.mirror && styles.mirror,
127
+ props.dotsContainerStyle,
128
+ ]}
129
+ >
130
+ {dots.map((_, i) => {
131
+ const inputRange = [(i - 1) * width, i * width, (i + 1) * width];
132
+ const dotOpacity = props.animatedValue.interpolate({
133
+ inputRange,
134
+ outputRange: [0.3, 1, 0.3],
135
+ extrapolate: 'clamp',
136
+ });
137
+ const dotProps = {
138
+ accessibilityRole: props.dotsAreTappable
139
+ ? ('button' as const)
140
+ : ('image' as const),
141
+ accessibilityLabel: `Page ${i + 1} of ${props.numberOfScreens}`,
142
+ accessibilityState: { selected: i === currentPage },
143
+ };
144
+ const dot = (
145
+ <Animated.View
146
+ key={i}
147
+ style={[
148
+ styles.dot,
149
+ {
150
+ backgroundColor: props.color,
151
+ opacity: dotOpacity,
152
+ },
153
+ ]}
154
+ />
155
+ );
156
+ return props.dotsAreTappable ? (
157
+ <Pressable
158
+ key={i}
159
+ onPress={() => scrollTo(i)}
160
+ hitSlop={8}
161
+ {...dotProps}
162
+ >
163
+ {dot}
164
+ </Pressable>
165
+ ) : (
166
+ <View key={i} {...dotProps}>
167
+ {dot}
168
+ </View>
169
+ );
170
+ })}
171
+ </View>
172
+ )}
173
+
95
174
  <View
96
175
  style={[
97
176
  styles.buttons,
@@ -138,21 +217,34 @@ const styles = StyleSheet.create({
138
217
  },
139
218
  dotsContainer: {
140
219
  flexDirection: 'row',
141
- flex: 1,
220
+ flex: 2,
142
221
  justifyContent: 'center',
222
+ alignItems: 'center',
223
+ },
224
+ progressTrack: {
225
+ flex: 2,
226
+ height: 6,
227
+ borderRadius: 3,
228
+ marginHorizontal: 16,
229
+ overflow: 'hidden',
230
+ },
231
+ progressFill: {
232
+ height: '100%',
233
+ borderRadius: 3,
143
234
  },
144
235
  text: {
145
236
  fontSize: 16,
146
237
  },
147
238
  buttons: {
148
- minWidth: 200,
239
+ flex: 1,
149
240
  },
150
241
  rightButton: {
151
242
  alignItems: 'flex-end',
152
- paddingRight: 30,
153
243
  },
154
244
  leftButton: {
155
245
  alignItems: 'flex-start',
156
- paddingLeft: 30,
246
+ },
247
+ mirror: {
248
+ transform: [{ scaleX: -1 }],
157
249
  },
158
250
  });
@@ -1,109 +1,87 @@
1
1
  import React from 'react';
2
2
  import { OnboardingPages } from './OnboardingPages';
3
3
  import { CustomPages } from './CustomPages';
4
- import { Animated } from 'react-native';
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
- const scrollX = React.useRef(new Animated.Value(0)).current;
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
- flatListRef,
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 onPress={props.onPress} style={props.buttonStyle}>
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,