UNPKG

4.94 kBTypeScriptView Raw
1import React, { Component } from 'react';
2import {
3 Dimensions,
4 NativeScrollEvent,
5 NativeSyntheticEvent,
6 Platform,
7 ScrollView,
8 ScrollViewProps,
9 StyleProp,
10 View,
11 ViewStyle,
12} from 'react-native';
13
14type Props = typeof PageSlider.defaultProps & {
15 children?: React.ReactNode;
16 contentPaddingVertical?: number;
17 selectedPage?: number;
18 style?: StyleProp<ViewStyle>;
19 onCurrentPageChange: (currentPage: number) => void;
20 onSelectedPageChange: (selectedPage: number) => void;
21};
22
23export class PageSlider extends Component<Props> {
24 static defaultProps = {
25 mode: 'page' as 'page' | 'card',
26 pageMargin: 8,
27 peek: 24,
28 };
29
30 private offsetX = 0;
31
32 private initialSelectedPage: number | undefined;
33
34 private hasDoneInitialScroll = false;
35
36 private scrollView: ScrollView | null = null;
37
38 constructor(props: Props) {
39 super(props);
40
41 // Android scrollView.scrollTo on component mount workaround
42 this.initialSelectedPage = this.props.selectedPage;
43 }
44
45 componentDidMount(): void {
46 if (Platform.OS === 'ios' && this.props.selectedPage) {
47 // Doesn't work in Android
48 this.scrollToPage(this.props.selectedPage, false);
49 }
50 }
51
52 componentDidUpdate(prevProps: Props): void {
53 const currentPage = this.getCurrentPage();
54
55 if (
56 prevProps.selectedPage !== this.props.selectedPage &&
57 this.props.selectedPage !== currentPage &&
58 this.props.selectedPage !== undefined
59 ) {
60 this.scrollToPage(this.props.selectedPage);
61 }
62 }
63
64 private onContentSizeChange = (width: number, height: number): void => {
65 if (
66 Platform.OS === 'android' &&
67 width &&
68 height &&
69 this.initialSelectedPage &&
70 !this.hasDoneInitialScroll
71 ) {
72 this.scrollToPage(this.initialSelectedPage, false);
73
74 this.hasDoneInitialScroll = true;
75 }
76 };
77
78 private onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>): void => {
79 this.offsetX = e.nativeEvent.contentOffset.x;
80
81 const currentPage = this.getCurrentPage();
82 this.props.onCurrentPageChange(currentPage);
83 };
84
85 private onMomentumScrollEnd = (): void => {
86 const currentPage = this.getCurrentPage();
87 if (this.props.selectedPage !== currentPage) {
88 this.props.onSelectedPageChange(currentPage);
89 }
90 };
91
92 // Calculates page's width according to selected mode
93 private getPageWidth = (): number => {
94 const { width } = Dimensions.get('screen');
95 if (this.props.mode === 'page') {
96 return width;
97 }
98
99 const { peek, pageMargin } = this.props;
100
101 return width - 2 * peek + 2 * pageMargin;
102 };
103
104 // Get currently visible page
105 private getCurrentPage = (): number => {
106 const pageWidth = this.getPageWidth();
107
108 return Math.floor(this.offsetX / pageWidth - 0.5) + 1;
109 };
110
111 // Scroll to page by index
112 private scrollToPage = (index: number, animated = true): void => {
113 const pageWidth = this.getPageWidth();
114
115 this.scrollView?.scrollTo({ y: 0, x: index * pageWidth, animated });
116 };
117
118 render(): JSX.Element {
119 const { children, mode, style } = this.props;
120 const { width } = Dimensions.get('screen');
121
122 const pageStyle = {
123 height: '100%',
124 };
125 let scrollViewProps: ScrollViewProps = {};
126
127 // Setup pages and ScrollView according to selected mode
128 if (mode === 'page') {
129 Object.assign(pageStyle, {
130 width,
131 });
132 } else if (mode === 'card') {
133 const { contentPaddingVertical, peek, pageMargin } = this.props;
134
135 scrollViewProps = {
136 contentContainerStyle: {
137 paddingHorizontal: peek - pageMargin,
138 paddingVertical: contentPaddingVertical,
139 },
140 decelerationRate: 'fast',
141 snapToAlignment: 'start',
142 snapToInterval: this.getPageWidth(),
143 };
144
145 if (Platform.OS === 'ios') {
146 /**
147 * pagingEnabled must be enabled on Android but cause warning on iOS
148 * when snapToInterval is set
149 */
150 scrollViewProps.pagingEnabled = false;
151 }
152
153 Object.assign(pageStyle, {
154 marginHorizontal: pageMargin,
155 width: width - 2 * peek,
156 });
157 }
158
159 // Wrap children
160 const pages = React.Children.map(children, (page) => {
161 // Skip no valid react elements (null, boolean, undefined and etc.)
162 if (!React.isValidElement(page)) {
163 return null;
164 }
165
166 return (
167 <View key={page.key} style={pageStyle}>
168 {page}
169 </View>
170 );
171 });
172
173 return (
174 <ScrollView
175 style={style}
176 ref={(scrollView) => {
177 this.scrollView = scrollView;
178 }}
179 horizontal
180 pagingEnabled
181 showsHorizontalScrollIndicator={false}
182 onContentSizeChange={this.onContentSizeChange}
183 onScroll={this.onScroll}
184 onMomentumScrollEnd={this.onMomentumScrollEnd}
185 scrollEventThrottle={8}
186 {...scrollViewProps}
187 >
188 {pages}
189 </ScrollView>
190 );
191 }
192}