├── .gitignore
├── src
├── index.js
├── colors.js
├── nodeType.js
├── Button.js
├── renderNode.js
├── Badge.js
├── Controls.js
└── Swiper.js
├── .npmignore
├── babel.config.js
├── package.json
├── CHANGELOG.md
├── index.d.ts
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | build
3 | node_modules
4 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { Swiper as default } from './Swiper';
2 |
--------------------------------------------------------------------------------
/src/colors.js:
--------------------------------------------------------------------------------
1 | export default {
2 | primary: '#2089dc',
3 | grey3: '#86939e',
4 | };
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules
3 | package-lock.json
4 | yarn.lock
5 | .gitignore
6 | babel.config.js
7 |
--------------------------------------------------------------------------------
/src/nodeType.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export const nodeType = PropTypes.oneOfType([
4 | PropTypes.element,
5 | PropTypes.object,
6 | PropTypes.bool,
7 | PropTypes.func,
8 | ]);
9 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ["minify"],
5 | plugins: [
6 | "@babel/plugin-transform-react-jsx",
7 | "@babel/plugin-proposal-class-properties"
8 | ],
9 | };
10 | };
11 |
--------------------------------------------------------------------------------
/src/Button.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, TouchableOpacity, View } from 'react-native';
3 |
4 | export const Button = ({ onPress, title, titleStyle }) => {
5 | return (
6 |
7 |
8 | {title}
9 |
10 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/src/renderNode.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const renderNode = (Component, content, defaultProps) => {
4 | if (content == null || content === false) {
5 | return null;
6 | }
7 | if (React.isValidElement(content)) {
8 | return content;
9 | }
10 | if (typeof content === 'function') {
11 | return content();
12 | }
13 | // Just in case
14 | if (content === true) {
15 | return ;
16 | }
17 | if (typeof content === 'string' || typeof content === 'number') {
18 | return {content};
19 | }
20 | return ;
21 | };
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-web-swiper",
3 | "version": "2.2.4",
4 | "homepage": "https://github.com/reactrondev/react-native-web-swiper#readme",
5 | "types": "./index.d.ts",
6 | "main": "build/index.js",
7 | "license": "MIT",
8 | "keywords": [
9 | "react-native",
10 | "react-native-web",
11 | "swipe",
12 | "swiper",
13 | "slider"
14 | ],
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/reactrondev/react-native-web-swiper.git"
18 | },
19 | "scripts": {
20 | "build": "rm -rf build && babel src --out-dir build",
21 | "prepare": "npm run build"
22 | },
23 | "dependencies": {
24 | "prop-types": "^15.6.2"
25 | },
26 | "peerDependencies": {
27 | "react-native": "*"
28 | },
29 | "devDependencies": {
30 | "@babel/cli": "^7.2.3",
31 | "@babel/core": "^7.2.2",
32 | "@babel/plugin-proposal-class-properties": "^7.5.5",
33 | "@babel/plugin-transform-react-jsx": "^7.3.0",
34 | "babel-preset-minify": "^0.5.0"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | react-native-web-swiper
2 |
3 | # Change Log
4 |
5 | The format is based on [Keep a Changelog](http://keepachangelog.com/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## 2.2.4 - 2022-10-02
9 |
10 | - Updated `children` prop type. Support for React 18 @eungwang1
11 | ## 2.2.3 - 2022-01-25
12 |
13 | - Add `isActive` prop. @kopax
14 | - Add `activeIndex` prop to DotComponent. @kopax
15 | - Change DotComponent's generic props. @kopax
16 |
17 | ## 2.2.2 - 2021-12-13
18 |
19 | - Children re-rendering behaviour is updated per #74 by @jarredt
20 |
21 | ## 2.1.6 — 2020-07-27
22 |
23 | - This is a patch for 2.1.4 due to the failure of generating builds
24 |
25 | ## 2.1.5 — 2020-07-27 - DO NOT USE, use 2.1.6 and above instead
26 |
27 | - This is a patch for 2.1.4 due to the failure of generating builds
28 |
29 | ## 2.1.4 — 2020-07-27 - DO NOT USE, use 2.1.6 and above instead
30 |
31 | ### Fixed
32 |
33 | - Correct `gestureEnabled` prop ([#1c2d448](https://github.com/reactrondev/react-native-web-swiper/commit/1c2d448b2b4d882d57bb2a08efdf8522cb917376))
34 |
--------------------------------------------------------------------------------
/src/Badge.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { StyleSheet, Text, View, TouchableOpacity } from 'react-native';
4 |
5 | import { renderNode } from './renderNode';
6 |
7 | const Badge = props => {
8 | const {
9 | containerStyle,
10 | textStyle,
11 | badgeStyle,
12 | onPress,
13 | Component = onPress ? TouchableOpacity : View,
14 | value,
15 | theme,
16 | status,
17 | ...attributes
18 | } = props;
19 |
20 | const element = renderNode(Text, value, {
21 | style: StyleSheet.flatten([styles.text, textStyle && textStyle]),
22 | });
23 |
24 | return (
25 |
26 |
35 | {element}
36 |
37 |
38 | );
39 | };
40 |
41 | Badge.propTypes = {
42 | containerStyle: PropTypes.shape({
43 | style: PropTypes.any,
44 | }),
45 | badgeStyle: PropTypes.shape({
46 | style: PropTypes.any,
47 | }),
48 | textStyle: PropTypes.shape({
49 | style: PropTypes.any,
50 | }),
51 | value: PropTypes.node,
52 | onPress: PropTypes.func,
53 | Component: PropTypes.func,
54 | theme: PropTypes.object,
55 | status: PropTypes.oneOf(['primary', 'success', 'warning', 'error']),
56 | };
57 |
58 | Badge.defaultProps = {
59 | status: 'primary',
60 | };
61 |
62 | const size = 18;
63 | const miniSize = 8;
64 |
65 | const styles = {
66 | badge: (theme, status) => ({
67 | alignSelf: 'center',
68 | minWidth: size,
69 | height: size,
70 | borderRadius: size / 2,
71 | alignItems: 'center',
72 | justifyContent: 'center',
73 | backgroundColor: theme.colors[status],
74 | borderWidth: StyleSheet.hairlineWidth,
75 | borderColor: '#fff',
76 | }),
77 | miniBadge: {
78 | paddingHorizontal: 0,
79 | paddingVertical: 0,
80 | minWidth: miniSize,
81 | height: miniSize,
82 | borderRadius: miniSize / 2,
83 | },
84 | text: {
85 | fontSize: 12,
86 | color: 'white',
87 | paddingHorizontal: 4,
88 | },
89 | };
90 |
91 | export { Badge };
92 |
--------------------------------------------------------------------------------
/src/Controls.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { StyleSheet, Text, View } from 'react-native';
4 |
5 | import { nodeType } from './nodeType';
6 | import { renderNode } from './renderNode';
7 |
8 | import { Badge } from './Badge';
9 | import { Button } from './Button';
10 |
11 | import colors from './colors';
12 |
13 | const cellPositions = [
14 | 'top-left',
15 | 'top',
16 | 'top-right',
17 | 'left',
18 | 'center',
19 | 'right',
20 | 'bottom-left',
21 | 'bottom',
22 | 'bottom-right',
23 | ];
24 |
25 | export default class DefaultControls extends React.Component {
26 | dotsPos = (() => this._getPos(this.props.dotsPos, 'bottom', 'right'))();
27 | prevPos = (() =>
28 | this._getPos(this.props.prevPos, 'bottom-left', 'top-right'))();
29 | nextPos = (() => this._getPos(this.props.nextPos, 'bottom-right'))();
30 |
31 | constructor(props) {
32 | super(props);
33 |
34 | this._renderRow = this._renderRow.bind(this);
35 | this._renderCell = this._renderCell.bind(this);
36 | this._renderDot = this._renderDot.bind(this);
37 | this._renderButton = this._renderButton.bind(this);
38 | }
39 |
40 | _getPos(prop, horizontalDefault, verticalDefault) {
41 | return prop === false
42 | ? null
43 | : prop
44 | ? prop
45 | : verticalDefault && this.props.vertical
46 | ? verticalDefault
47 | : horizontalDefault;
48 | }
49 |
50 | _renderDot({ isActive, onPress }) {
51 | const { dotProps = {}, dotActiveStyle } = this.props;
52 | const { containerStyle, badgeStyle, ...others } = dotProps;
53 | return (
54 |
68 | );
69 | }
70 |
71 | _renderDots() {
72 | const {
73 | vertical,
74 | count,
75 | activeIndex,
76 | dotsTouchable,
77 | dotsWrapperStyle,
78 | DotComponent = this._renderDot,
79 | goTo,
80 | } = this.props;
81 | return (
82 |
88 | {Array.from({ length: count }, (v, i) => i).map(index => (
89 | goTo(index)}
95 | />
96 | ))}
97 |
98 | );
99 | }
100 |
101 | _renderButton({ type, title, titleStyle, onPress, ...props }) {
102 | return (
103 |
111 | );
112 | }
113 |
114 | _renderPrev() {
115 | const {
116 | goToPrev,
117 | isFirst,
118 | prevTitle,
119 | firstPrevElement,
120 | prevTitleStyle,
121 | PrevComponent = this._renderButton,
122 | } = this.props;
123 | if (isFirst) {
124 | return renderNode(Text, firstPrevElement);
125 | }
126 | return (
127 |
133 | );
134 | }
135 |
136 | _renderNext() {
137 | const {
138 | goToNext,
139 | isLast,
140 | nextTitle,
141 | lastNextElement,
142 | nextTitleStyle,
143 | NextComponent = this._renderButton,
144 | } = this.props;
145 | if (isLast) {
146 | return renderNode(Text, lastNextElement);
147 | }
148 | return (
149 |
155 | );
156 | }
157 |
158 | _renderCell({ name }) {
159 | const { cellsStyle = {}, cellsContent = {} } = this.props;
160 | return (
161 |
162 | {this.dotsPos === name && this._renderDots()}
163 | {this.prevPos === name && this._renderPrev()}
164 | {this.nextPos === name && this._renderNext()}
165 | {cellsContent[name] && renderNode(Text, cellsContent[name])}
166 |
167 | );
168 | }
169 |
170 | _renderRow({ rowAlign }) {
171 | const Cell = this._renderCell;
172 | const row = [
173 | `${!rowAlign ? '' : rowAlign + '-'}left`,
174 | rowAlign || 'center',
175 | `${!rowAlign ? '' : rowAlign + '-'}right`,
176 | ];
177 | const alignItems = ['flex-start', 'center', 'flex-end'];
178 | return (
179 |
180 | {row.map((name, index) => (
181 |
182 | |
183 |
184 | ))}
185 |
186 | );
187 | }
188 |
189 | render() {
190 | const Row = this._renderRow;
191 | return (
192 |
193 |
194 |
195 |
196 |
197 | );
198 | }
199 | }
200 |
201 | DefaultControls.propTypes = {
202 | cellsStyle: PropTypes.shape(
203 | cellPositions.reduce(
204 | (obj, item) => ({ ...obj, [item]: PropTypes.style }),
205 | {}
206 | )
207 | ),
208 | cellsContent: PropTypes.shape(
209 | cellPositions.reduce((obj, item) => ({ ...obj, [item]: nodeType }), {})
210 | ),
211 |
212 | dotsPos: PropTypes.oneOf([...cellPositions, true, false]),
213 | prevPos: PropTypes.oneOf([...cellPositions, true, false]),
214 | nextPos: PropTypes.oneOf([...cellPositions, true, false]),
215 | prevTitle: PropTypes.string,
216 | nextTitle: PropTypes.string,
217 |
218 | dotsTouchable: PropTypes.bool,
219 | dotsWrapperStyle: PropTypes.shape({
220 | style: PropTypes.any,
221 | }),
222 |
223 | dotProps: PropTypes.shape(Badge.propTypes),
224 | dotActiveStyle: PropTypes.shape({
225 | style: PropTypes.any,
226 | }),
227 | DotComponent: PropTypes.func,
228 |
229 | prevTitleStyle: PropTypes.shape({
230 | style: PropTypes.any,
231 | }),
232 | nextTitleStyle: PropTypes.shape({
233 | style: PropTypes.any,
234 | }),
235 | PrevComponent: PropTypes.func,
236 | NextComponent: PropTypes.func,
237 | firstPrevElement: nodeType,
238 | lastNextElement: nodeType,
239 |
240 | theme: PropTypes.object,
241 | vertical: PropTypes.bool,
242 | count: PropTypes.number,
243 | activeIndex: PropTypes.number,
244 | isFirst: PropTypes.bool,
245 | isLast: PropTypes.bool,
246 | goToPrev: PropTypes.func,
247 | goToNext: PropTypes.func,
248 | goTo: PropTypes.func,
249 | };
250 |
251 | DefaultControls.defaultProps = {
252 | prevTitle: 'Prev',
253 | nextTitle: 'Next',
254 | };
255 |
256 | const styles = {
257 | row: {
258 | flexDirection: 'row',
259 | height: 0,
260 | alignItems: 'center',
261 | margin: 20,
262 | },
263 | spaceHolder: alignItems => ({
264 | height: 0,
265 | flex: 1,
266 | alignItems,
267 | justifyContent: 'center',
268 | }),
269 | cell: {
270 | alignItems: 'center',
271 | justifyContent: 'center',
272 | position: 'absolute',
273 | },
274 | dotsWrapper: vertical => ({
275 | flexDirection: vertical ? 'column' : 'row',
276 | alignItems: 'center',
277 | justifyContent: 'center',
278 | minWidth: 1,
279 | minHeight: 1,
280 | }),
281 | dotsItemContainer: {
282 | margin: 3,
283 | },
284 | dotsItem: (theme, isActive) => ({
285 | backgroundColor: isActive ? theme.colors.primary : theme.colors.grey3,
286 | borderColor: 'transparent',
287 | }),
288 | buttonTitleStyle: (theme, type) => ({
289 | color: type === 'prev' ? theme.colors.grey3 : theme.colors.primary,
290 | }),
291 | hidden: {
292 | opacity: 0,
293 | },
294 | };
295 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {
3 | ViewStyle,
4 | TextStyle,
5 | StyleProp,
6 | } from 'react-native';
7 |
8 | type Falsy = undefined | null | false;
9 | interface RecursiveArray extends Array> {}
10 | /** Keep a brand of 'T' so that calls to `StyleSheet.flatten` can take `RegisteredStyle` and return `T`. */
11 | type RegisteredStyle = number & { __registeredStyleBrand: T };
12 |
13 | export type SwiperControlsCorners =
14 | | 'top-left'
15 | | 'top'
16 | | 'top-right'
17 | | 'left'
18 | | 'center'
19 | | 'right'
20 | | 'bottom-left'
21 | | 'bottom'
22 | | 'bottom-right';
23 |
24 | // TODO: optimize
25 | interface SwiperControlsCellsStyle {
26 | 'top-left'?: StyleProp;
27 | top?: StyleProp;
28 | 'top-right'?: StyleProp;
29 | left?: StyleProp;
30 | center?: StyleProp;
31 | right?: StyleProp;
32 | 'bottom-left'?: StyleProp;
33 | bottom?: StyleProp;
34 | 'bottom-right'?: StyleProp;
35 | }
36 | interface SwiperControlsCellsContent {
37 | 'top-left'?: React.ReactElement<{}>;
38 | top?: React.ReactElement<{}>;
39 | 'top-right'?: React.ReactElement<{}>;
40 | left?: React.ReactElement<{}>;
41 | center?: React.ReactElement<{}>;
42 | right?: React.ReactElement<{}>;
43 | 'bottom-left'?: React.ReactElement<{}>;
44 | bottom?: React.ReactElement<{}>;
45 | 'bottom-right'?: React.ReactElement<{}>;
46 | }
47 |
48 | export interface BadgeProps {
49 | /**
50 | * Text value to be displayed by badge
51 | *
52 | * @default null
53 | */
54 | value?: React.ReactNode;
55 |
56 | /**
57 | * Additional styling for badge (background) view component
58 | */
59 | badgeStyle?: StyleProp;
60 |
61 | /**
62 | * Style for the container
63 | */
64 | containerStyle?: StyleProp;
65 |
66 | /**
67 | * Style for the text in the badge
68 | */
69 | textStyle?: StyleProp;
70 |
71 | /**
72 | * Custom component to replace the badge component
73 | *
74 | * @default View (if onPress then TouchableOpacity)
75 | */
76 | Component?: React.ComponentClass;
77 |
78 | /**
79 | * Determines color of the indicator
80 | *
81 | * @default primary
82 | */
83 | status?: 'primary' | 'success' | 'warning' | 'error';
84 |
85 | /**
86 | * Function called when pressed on the badge
87 | */
88 | onPress?(): void;
89 | }
90 |
91 | interface SwiperControlsProps {
92 | /**
93 | * Controls corners placeholders styles
94 | */
95 | cellsStyle?: SwiperControlsCellsStyle;
96 |
97 | /**
98 | * Controls corners placeholders additional content
99 | */
100 | cellsContent?: SwiperControlsCellsContent;
101 |
102 | /**
103 | * Dots position
104 | *
105 | * @default 'bottom' | 'right' if vertical
106 | */
107 | dotsPos?: SwiperControlsCorners | boolean;
108 |
109 | /**
110 | * Prev button position
111 | *
112 | * @default 'bottom-left' | 'top-right' if vertical
113 | */
114 | prevPos?: SwiperControlsCorners | boolean;
115 |
116 | /**
117 | * Next button position
118 | *
119 | * @default 'bottom-right'
120 | */
121 | nextPos?: SwiperControlsCorners | boolean;
122 |
123 | /**
124 | * Prev button title
125 | *
126 | * @default Prev
127 | */
128 | prevTitle?: string;
129 |
130 | /**
131 | * Next button title
132 | *
133 | * @default Next
134 | */
135 | nextTitle?: string;
136 |
137 | /**
138 | * Touches over dots will move swiper to relative slide
139 | *
140 | * @default false
141 | */
142 | dotsTouchable?: boolean;
143 |
144 | /**
145 | * Dots wrapper View style
146 | */
147 | dotsWrapperStyle?: StyleProp;
148 |
149 | /**
150 | * Customizing dot with Badge props
151 | */
152 | dotProps?: BadgeProps;
153 |
154 | /**
155 | * Additional style to active dot
156 | */
157 | dotActiveStyle?: StyleProp;
158 |
159 | /**
160 | * Custom dot component
161 | */
162 | DotComponent?: React.ComponentType<{ index: number, isActive: boolean, onPress: any }>;
163 |
164 | /**
165 | * Customize prev button title
166 | */
167 | prevTitleStyle?: StyleProp;
168 |
169 | /**
170 | * Customize next button title
171 | */
172 | nextTitleStyle?: StyleProp;
173 |
174 | /**
175 | * Custom prev button component
176 | */
177 | PrevComponent?: React.ComponentClass;
178 |
179 | /**
180 | * Custom next button component
181 | */
182 | NextComponent?: React.ComponentClass;
183 |
184 | /**
185 | * Custom prev element on first slide (if not loop)
186 | */
187 | firstPrevElement?: React.ReactElement<{}>;
188 |
189 | /**
190 | * Custom next element on last slide (if not loop)
191 | */
192 | lastNextElement?: React.ReactElement<{}>;
193 | }
194 |
195 | // TODO: extends Animated.SpringAnimationConfig but without toValue
196 | interface SwiperSpringAnimationConfig {
197 | overshootClamping?: boolean;
198 | restDisplacementThreshold?: number;
199 | restSpeedThreshold?: number;
200 | velocity?: number | { x: number; y: number };
201 | bounciness?: number;
202 | speed?: number;
203 | tension?: number;
204 | friction?: number;
205 | stiffness?: number;
206 | mass?: number;
207 | damping?: number;
208 | }
209 |
210 | export interface SwiperProps {
211 | /**
212 | * Swiper vertical layout
213 | *
214 | * @default false
215 | */
216 | vertical?: boolean;
217 |
218 | /**
219 | * Initial slide index
220 | *
221 | * @default 0
222 | */
223 | from?: number;
224 |
225 | /**
226 | * Allow loop
227 | *
228 | * @default false
229 | */
230 | loop?: boolean;
231 |
232 | /**
233 | * Autoplay slider timeout in secs. Negative value will play reverse
234 | *
235 | * @default 0 (autoplay disabled)
236 | */
237 | timeout?: number;
238 |
239 | /**
240 | * Should the swiper's swiping gesture be enabled?
241 | *
242 | * @default true
243 | */
244 | gesturesEnabled?: () => boolean;
245 |
246 | /**
247 | * Tune spring animation on autoplay, touch release or slides changes via buttons
248 | */
249 | springConfig?: SwiperSpringAnimationConfig;
250 |
251 | /**
252 | * Initiate animation after swipe this distance.
253 | * It fix gesture collisions inside ScrollView
254 | *
255 | * @default 5
256 | */
257 | minDistanceToCapture?: number;
258 |
259 | /**
260 | * Minimal part of swiper width (or height for vertical) must be swiped
261 | * for changing index. Otherwise animation restore current slide.
262 | * Default value 0.2 means that 20% must be swiped for change index
263 | *
264 | * @default 0.2
265 | */
266 | minDistanceForAction?: number;
267 |
268 | /**
269 | * Swiper inner container position 'fixed' instead 'relative'.
270 | * Fix mobile safari vertical bounces
271 | *
272 | * @default false
273 | */
274 | positionFixed?: boolean;
275 |
276 | /**
277 | * Outer (root) container style
278 | */
279 | containerStyle?: StyleProp;
280 |
281 | /**
282 | * Inner container style
283 | */
284 | innerContainerStyle?: StyleProp;
285 |
286 | /**
287 | * Swipe area style
288 | */
289 | swipeAreaStyle?: StyleProp;
290 |
291 | /**
292 | * Each slide wrapper style
293 | */
294 | slideWrapperStyle?: StyleProp;
295 |
296 | /**
297 | * Dots and control buttons enabled
298 | *
299 | * @default true
300 | */
301 | controlsEnabled?: boolean;
302 |
303 | /**
304 | * Controls Properties
305 | */
306 | controlsProps?: SwiperControlsProps;
307 |
308 | /**
309 | * Custom controls component
310 | */
311 | Controls?: React.ComponentClass;
312 |
313 | /**
314 | * Any swiper animation start
315 | *
316 | * @param currentIndex
317 | */
318 | onAnimationStart?(currentIndex: number): void;
319 |
320 | /**
321 | * Any swiper animation end
322 | *
323 | * @param index
324 | */
325 | onAnimationEnd?(index: number): void;
326 |
327 | /**
328 | * Called when active index changed
329 | *
330 | * @param index
331 | */
332 | onIndexChanged?(index: number): void;
333 |
334 | /**
335 | * Children props for functional components
336 | */
337 | children?: React.ReactNode;
338 | }
339 |
340 | /**
341 | * Swiper component
342 | */
343 | export default class Swiper extends React.Component {
344 | /**
345 | * Go to next slide
346 | */
347 | goToNext(): void;
348 |
349 | /**
350 | * Go to previous slide
351 | */
352 | goToPrev(): void;
353 |
354 | /**
355 | * Go to slide by index
356 | */
357 | goTo(index: number): void;
358 |
359 | /**
360 | * Get current slide index
361 | */
362 | getActiveIndex(): number;
363 |
364 | /**
365 | * Manual start autoplay after manual stop
366 | */
367 | startAutoplay(): void;
368 |
369 | /**
370 | * Manual stop autoplay. Will be automatically restarted after any animation
371 | */
372 | stopAutoplay(): void;
373 | }
374 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-web-swiper
2 |
3 | Simple swiper / slider. Works both on React-Native and React-Native-Web.
4 |
5 | ## Demo
6 |
7 | Hybrid Snack: https://snack.expo.io/@oxyii/react-native-web-swiper
8 |
9 | ## Installation
10 |
11 | ```bash
12 | $ npm i react-native-web-swiper --save
13 | ```
14 |
15 | ## Usage
16 |
17 | ```jsx
18 | import React from 'react';
19 | import { StyleSheet, Text, View } from 'react-native';
20 | import Swiper from 'react-native-web-swiper';
21 |
22 | const styles = StyleSheet.create({
23 | container: {
24 | flex: 1,
25 | },
26 | slideContainer: {
27 | flex: 1,
28 | alignItems: 'center',
29 | justifyContent: 'center',
30 | },
31 | slide1: {
32 | backgroundColor: 'rgba(20,20,200,0.3)',
33 | },
34 | slide2: {
35 | backgroundColor: 'rgba(20,200,20,0.3)',
36 | },
37 | slide3: {
38 | backgroundColor: 'rgba(200,20,20,0.3)',
39 | },
40 | });
41 |
42 | export default class Screen extends React.Component {
43 | render() {
44 | return (
45 |
46 |
47 |
48 | Slide 1
49 |
50 |
51 | Slide 2
52 |
53 |
54 | Slide 3
55 |
56 |
57 |
58 | );
59 | }
60 | }
61 | ```
62 |
63 | ### With props
64 |
65 | ```jsx
66 | Your Custom Dot {activeIndex+1}/{index+1}
76 | }}
77 | >
78 | {/* Slide 1 */}
79 | {/* Slide 2 */}
80 | {/* ... */}
81 |
82 | ```
83 |
84 | ### Dynamic content
85 |
86 | The slide automatically gets `props.isActive`, `props.activeIndex` and `props.index`.
87 |
88 | ```jsx
89 | import React from 'react';
90 | import { Text, View } from 'react-native';
91 | import Swiper from 'react-native-web-swiper';
92 |
93 | type Props = {
94 | index?: number,
95 | activeIndex?: number,
96 | }
97 | export const SomeSlide = (props: Props) => (
98 |
99 | {props.activeIndex}/{props.index}{props.isActive ? ' (active)' : ''}
100 |
101 | )
102 |
103 | export default () => (
104 |
105 |
106 |
107 |
108 | )
109 | ```
110 |
111 | This is possible because `Swiper` used `cloneElement` and inject internally the `activeIndex` and `index` props to each slide. This also means that all slides will re-render on swipe, since the `activeIndex` prop value changes on swipe.
112 |
113 | ---
114 |
115 | ## Props
116 |
117 | | Prop | Default | Type | Description |
118 | | :------------------- |:------------:| :--------------------:| :-----------|
119 | | vertical | `false` | `boolean` | Swiper vertical layout |
120 | | from | `0` | `number` | Initial slide index |
121 | | loop | `false` | `boolean` | Set to `true` to enable continuous loop mode |
122 | | timeout | `0` | `number` | Delay between auto play transitions (in second). Set negative value for reverse autoplay :satisfied:. Autoplay disabled by default |
123 | | gesturesEnabled | `() => true` | `function` | Function that returns boolean value. Must return `false` to disable swiping mechanism. Does not disable Prev / Next buttons |
124 | | springConfig | | [`Animated.spring`](https://facebook.github.io/react-native/docs/animated#spring) | Tune spring animation on autoplay, touch release or slides changes via buttons |
125 | | minDistanceToCapture | `5` | `number` | Initiate animation after swipe this distance. It fix gesture collisions inside ScrollView |
126 | | minDistanceForAction | `0.2` | `number` | Minimal part of swiper width (or height for vertical) must be swiped for changing index. Otherwise animation restore current slide. Default value 0.2 means that 20% must be swiped for change index |
127 | | positionFixed | `false` | `boolean` | Swiper inner container position `fixed` instead `relative`. Fix mobile safari vertical bounce |
128 | | containerStyle | | `ViewPropTypes.style` | Outer (root) container style |
129 | | innerContainerStyle | | `ViewPropTypes.style` | Inner container style |
130 | | swipeAreaStyle | | `ViewPropTypes.style` | Swipe area style |
131 | | slideWrapperStyle | | `ViewPropTypes.style` | Each slide wrapper style |
132 | | controlsEnabled | `true` | `boolean` | Dots and control buttons visible and enabled |
133 | | Controls | | `React.Component` | Custom controls component |
134 | | onAnimationStart | | `function` | Any swiper animation start |
135 | | onAnimationEnd | | `function` | Any swiper animation end |
136 | | onIndexChanged | | `function` | Called when active index changed |
137 | | controlsProps | | `object` | see below |
138 |
139 | ### Controls Props
140 |
141 | Over the swiper we need to create a controls layer. But this layer will block the possibility of swiper layer control.
142 | We created 9 controls placeholders to solve this problem:
143 | `top-left`, `top`, `top-right`, `left`, `center`, `right`, `bottom-left`, `bottom` and `bottom-right`.
144 | You can adjust controls position by placing into relevant placeholder:
145 |
146 | ```jsx
147 | {/* Additional content in placeholder */}
162 | }
163 | }}
164 | />
165 | ```
166 |
167 | | Prop | Default | Type | Description |
168 | | :------------------- |:------------:| :-----------------------:| :-----------|
169 | | cellsStyle | | `object` | Controls corners placeholders styles. Allowed keys is: `top-left`, `top`, `top-right`, `left`, `center`, `right`, `bottom-left`, `bottom` and `bottom-right`, allowed values is `ViewPropTypes.style` |
170 | | cellsContent | | `object` | Controls corners placeholders additional content. Allowed keys is: `top-left`, `top`, `top-right`, `left`, `center`, `right`, `bottom-left`, `bottom` and `bottom-right`, allowed values is `string` **OR** `React element` |
171 | | dotsPos | `'bottom'` **OR** `'right'` if vertical | `boolean` **OR** `enum('top-left', 'top', 'top-right', 'left', 'center', 'right', 'bottom-left', 'bottom', 'bottom-right')` | Dots position |
172 | | prevPos | `'bottom-left'` **OR** `'top-right'` if vertical | `boolean` **OR** `enum('top-left', 'top', 'top-right', 'left', 'center', 'right', 'bottom-left', 'bottom', 'bottom-right')` | Prev button position |
173 | | nextPos | `'bottom-right'` | `boolean` **OR** `enum('top-left', 'top', 'top-right', 'left', 'center', 'right', 'bottom-left', 'bottom', 'bottom-right')` | Next button position |
174 | | prevTitle | `'Prev'` | `string` | Prev button title |
175 | | nextTitle | `'Next'` | `string` | Next button title |
176 | | prevTitleStyle | | `Text.propTypes.style` | Customize prev button title |
177 | | nextTitleStyle | | `Text.propTypes.style` | Customize next button title |
178 | | PrevComponent | | `React.Component` | Custom prev button component |
179 | | NextComponent | | `React.Component` | Custom next button component |
180 | | firstPrevElement | | `element` | Custom prev element on first slide (if not loop) |
181 | | lastNextElement | | `element` | Custom next element on last slide (if not loop) |
182 | | dotsTouchable | `false` | `boolean` | Touches over dots will move swiper to relative slide |
183 | | dotsWrapperStyle | | `ViewPropTypes.style` | Dots wrapper View style |
184 | | dotProps | | `object` | `react-native-elements` [Badge props](https://react-native-training.github.io/react-native-elements/docs/badge.html#props) |
185 | | dotActiveStyle | | `object` | Additional style to active dot. Will be added to dot [badgeStyle](https://react-native-training.github.io/react-native-elements/docs/badge.html#badgestyle) |
186 | | DotComponent | | `React.Component` | Custom dot component |
187 |
188 | ## Interaction methods
189 |
190 | Store a reference to the Swiper in your component by using the ref prop
191 | provided by React ([see docs](https://reactjs.org/docs/refs-and-the-dom.html)):
192 |
193 | ```jsx
194 | const swiperRef = useRef(null);
195 |
196 | ...
197 |
198 |
202 | ```
203 |
204 | Then you can manually trigger swiper from anywhere:
205 |
206 | ```jsx
207 | () => {
208 | swiperRef.current.goTo(1);
209 | swiperRef.current.goToPrev();
210 | swiperRef.current.goToNext();
211 | const index = swiperRef.current.getActiveIndex();
212 | };
213 | ```
214 |
--------------------------------------------------------------------------------
/src/Swiper.js:
--------------------------------------------------------------------------------
1 | import React, { cloneElement } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Animated, I18nManager, PanResponder, StyleSheet, View } from 'react-native';
4 |
5 | import DefaultControls from './Controls';
6 |
7 | const useNativeDriver = false; // because of RN #13377
8 |
9 | class Swiper extends React.Component {
10 | children = (() => React.Children.toArray(this.props.children))();
11 | count = (() => this.children.length)();
12 |
13 | startAutoplay() {
14 | const { timeout } = this.props;
15 | this.stopAutoplay();
16 | if (timeout) {
17 | this.autoplay = setTimeout(
18 | this._autoplayTimeout,
19 | Math.abs(timeout) * 1000
20 | );
21 | }
22 | }
23 |
24 | stopAutoplay() {
25 | this.autoplay && clearTimeout(this.autoplay);
26 | }
27 |
28 | goToNext() {
29 | this._goToNeighboring();
30 | }
31 |
32 | goToPrev() {
33 | this._goToNeighboring(true);
34 | }
35 |
36 | goTo(index = 0) {
37 | const delta = index - this.getActiveIndex();
38 | if (delta) {
39 | this._fixAndGo(delta);
40 | }
41 | }
42 |
43 | getActiveIndex() {
44 | return this.state.activeIndex;
45 | }
46 |
47 | // stop public methods
48 |
49 | _autoplayTimeout() {
50 | const { timeout } = this.props;
51 | this._goToNeighboring(timeout < 0);
52 | }
53 |
54 | _goToNeighboring(toPrev = false) {
55 | this._fixAndGo(toPrev ? -1 : 1);
56 | }
57 |
58 | constructor(props) {
59 | super(props);
60 |
61 | this._autoplayTimeout = this._autoplayTimeout.bind(this);
62 | this._onLayout = this._onLayout.bind(this);
63 | this._fixState = this._fixState.bind(this);
64 |
65 | this.goToPrev = this.goToPrev.bind(this);
66 | this.goToNext = this.goToNext.bind(this);
67 | this.goTo = this.goTo.bind(this);
68 |
69 | this.state = {
70 | x: 0,
71 | y: 0,
72 | width: 0,
73 | height: 0,
74 | activeIndex: props.from,
75 | pan: new Animated.ValueXY(),
76 | };
77 |
78 | this._animatedValueX = 0;
79 | this._animatedValueY = 0;
80 |
81 | this._panResponder = PanResponder.create(this._getPanResponderCallbacks());
82 | }
83 |
84 | componentDidMount() {
85 | this.state.pan.x.addListener(({ value }) => (this._animatedValueX = value));
86 | this.state.pan.y.addListener(({ value }) => (this._animatedValueY = value));
87 | this.startAutoplay();
88 | }
89 |
90 | componentWillUnmount() {
91 | this.stopAutoplay();
92 | this.state.pan.x.removeAllListeners();
93 | this.state.pan.y.removeAllListeners();
94 | }
95 |
96 | _getPanResponderCallbacks() {
97 | return {
98 | onPanResponderTerminationRequest: () => false,
99 | onMoveShouldSetResponderCapture: () => true,
100 | onMoveShouldSetPanResponderCapture: (e, gestureState) => {
101 | const { gesturesEnabled, vertical, minDistanceToCapture } = this.props;
102 |
103 | if (!gesturesEnabled()) {
104 | return false;
105 | }
106 |
107 | this.props.onAnimationStart &&
108 | this.props.onAnimationStart(this.getActiveIndex());
109 |
110 | const allow =
111 | Math.abs(vertical ? gestureState.dy : gestureState.dx) >
112 | minDistanceToCapture;
113 |
114 | if (allow) {
115 | this.stopAutoplay();
116 | }
117 |
118 | return allow;
119 | },
120 | onPanResponderGrant: () => this._fixState(),
121 | onPanResponderMove: Animated.event([
122 | null,
123 | this.props.vertical
124 | ? { dy: this.state.pan.y }
125 | : { dx: this.state.pan.x },
126 | ], { useNativeDriver: false }),
127 | onPanResponderRelease: (e, gesture) => {
128 | const { vertical, minDistanceForAction } = this.props;
129 | const { width, height } = this.state;
130 |
131 | this.startAutoplay();
132 |
133 | const correction = vertical
134 | ? gesture.moveY - gesture.y0
135 | : gesture.moveX - gesture.x0;
136 |
137 | if (
138 | Math.abs(correction) <
139 | (vertical ? height : width) * minDistanceForAction
140 | ) {
141 | this._spring({ x: 0, y: 0 });
142 | } else {
143 | this._changeIndex(correction > 0 ? (!vertical && I18nManager.isRTL ? 1 : -1) : (!vertical && I18nManager.isRTL ? -1 : 1));
144 | }
145 | },
146 | };
147 | }
148 |
149 | _spring(toValue) {
150 | const { springConfig, onAnimationEnd } = this.props;
151 | const { activeIndex } = this.state;
152 | Animated.spring(this.state.pan, {
153 | ...springConfig,
154 | toValue,
155 | useNativeDriver, // false, see top of file
156 | }).start(() => onAnimationEnd && onAnimationEnd(activeIndex));
157 | }
158 |
159 | _fixState() {
160 | const { vertical } = this.props;
161 | const { width, height, activeIndex } = this.state;
162 | this._animatedValueX = vertical ? 0 : width * activeIndex * (I18nManager.isRTL ? 1 : -1);
163 | this._animatedValueY = vertical ? height * activeIndex * -1 : 0;
164 | this.state.pan.setOffset({
165 | x: this._animatedValueX,
166 | y: this._animatedValueY,
167 | });
168 | this.state.pan.setValue({ x: 0, y: 0 });
169 | }
170 |
171 | _fixAndGo(delta) {
172 | this._fixState();
173 | this.props.onAnimationStart &&
174 | this.props.onAnimationStart(this.getActiveIndex());
175 | this._changeIndex(delta);
176 | }
177 |
178 | _changeIndex(delta = 1) {
179 | const { loop, vertical } = this.props;
180 | const { width, height, activeIndex } = this.state;
181 |
182 | let toValue = { x: 0, y: 0 };
183 | let skipChanges = !delta;
184 | let calcDelta = delta;
185 |
186 | if (activeIndex <= 0 && delta < 0) {
187 | skipChanges = !loop;
188 | calcDelta = this.count + delta;
189 | } else if (activeIndex + 1 >= this.count && delta > 0) {
190 | skipChanges = !loop;
191 | calcDelta = -1 * activeIndex + delta - 1;
192 | }
193 |
194 | if (skipChanges) {
195 | return this._spring(toValue);
196 | }
197 |
198 | this.stopAutoplay();
199 |
200 | let index = activeIndex + calcDelta;
201 | this.setState({ activeIndex: index });
202 |
203 | if (vertical) {
204 | toValue.y = height * -1 * calcDelta;
205 | } else {
206 | toValue.x = width * (I18nManager.isRTL ? 1 : -1) * calcDelta;
207 | }
208 | this._spring(toValue);
209 |
210 | this.startAutoplay();
211 | this.props.onIndexChanged && this.props.onIndexChanged(index);
212 | }
213 |
214 | _onLayout({
215 | nativeEvent: {
216 | layout: { x, y, width, height },
217 | },
218 | }) {
219 | this.setState({ x, y, width, height }, () => this._fixState());
220 | }
221 |
222 | render() {
223 | const { pan, x, y, width, height } = this.state;
224 |
225 | const {
226 | theme,
227 | loop,
228 | vertical,
229 | positionFixed,
230 | containerStyle,
231 | innerContainerStyle,
232 | swipeAreaStyle,
233 | slideWrapperStyle,
234 | controlsEnabled,
235 | controlsProps,
236 | Controls = DefaultControls,
237 | } = this.props;
238 |
239 | return (
240 |
244 |
250 |
260 | {this.children.map((el, i) => (
261 |
268 | {cloneElement(el, { activeIndex: this.getActiveIndex(), index: i, isActive: i === this.getActiveIndex() })}
269 |
270 | ))}
271 |
272 | {controlsEnabled && (
273 | = this.count}
281 | goToPrev={this.goToPrev}
282 | goToNext={this.goToNext}
283 | goTo={this.goTo}
284 | />
285 | )}
286 |
287 |
288 | );
289 | }
290 | }
291 |
292 | Swiper.propTypes = {
293 | vertical: PropTypes.bool,
294 | from: PropTypes.number,
295 | loop: PropTypes.bool,
296 | timeout: PropTypes.number,
297 | gesturesEnabled: PropTypes.func,
298 | springConfig: PropTypes.object,
299 | minDistanceToCapture: PropTypes.number, // inside ScrollView
300 | minDistanceForAction: PropTypes.number,
301 |
302 | onAnimationStart: PropTypes.func,
303 | onAnimationEnd: PropTypes.func,
304 | onIndexChanged: PropTypes.func,
305 |
306 | positionFixed: PropTypes.bool, // Fix safari vertical bounces
307 | containerStyle: PropTypes.shape({
308 | style: PropTypes.any,
309 | }),
310 | innerContainerStyle: PropTypes.shape({
311 | style: PropTypes.any,
312 | }),
313 | swipeAreaStyle: PropTypes.shape({
314 | style: PropTypes.any,
315 | }),
316 | slideWrapperStyle: PropTypes.shape({
317 | style: PropTypes.any,
318 | }),
319 |
320 | controlsEnabled: PropTypes.bool,
321 | controlsProps: PropTypes.shape(DefaultControls.propTypes),
322 | Controls: PropTypes.func,
323 |
324 | theme: PropTypes.object,
325 | };
326 |
327 | Swiper.defaultProps = {
328 | vertical: false,
329 | from: 0,
330 | loop: false,
331 | timeout: 0,
332 | gesturesEnabled: () => true,
333 | minDistanceToCapture: 5,
334 | minDistanceForAction: 0.2,
335 | positionFixed: false,
336 | controlsEnabled: true,
337 | };
338 |
339 | const styles = {
340 | root: {
341 | flex: 1,
342 | backgroundColor: 'transparent',
343 | },
344 | // Fix web vertical scaling (like expo v33-34)
345 | container: (positionFixed, x, y, width, height) => ({
346 | backgroundColor: 'transparent',
347 | // Fix safari vertical bounces
348 | position: positionFixed ? 'fixed' : 'relative',
349 | overflow: 'hidden',
350 | top: positionFixed ? y : 0,
351 | left: positionFixed ? x : 0,
352 | width,
353 | height,
354 | justifyContent: 'space-between',
355 | }),
356 | swipeArea: (vertical, count, width, height) => ({
357 | position: 'absolute',
358 | top: 0,
359 | left: 0,
360 | width: vertical ? width : width * count,
361 | height: vertical ? height * count : height,
362 | flexDirection: vertical ? 'column' : 'row',
363 | }),
364 | };
365 |
366 | export { Swiper };
367 |
--------------------------------------------------------------------------------