this.Carousel = Carousel }>
88 | ```
89 |
90 | The methods can be accessed on the `this.Carousel` instance:
91 | * `this.Carousel.next()`: Sets next card as center card.
92 | * `this.Carousel.prev()`: Sets previous card as center card.
93 | * `this.Carousel.goTo(index)`: Sets the specified number index as center card.
94 | * `this.Carousel.getCurrentIndex()`: Gets current card index.
95 |
96 | *NOTE*: If you choose to create the ref using React.createRef() instead of using a callback ref, the methods can be accessed on the `this.Carousel.current` instance.
97 |
98 | ## Credits
99 | Created by @strawbee at Tomorrow Ideas.
100 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-card-carousel",
3 | "version": "1.1.3",
4 | "description": "Simple React carousel.",
5 | "license": "MIT",
6 | "author": {
7 | "name": "Joy Hou",
8 | "email": "yuejiao.hou@gmail.com",
9 | "url": "https://github.com/strawbee"
10 | },
11 | "keywords": [
12 | "React",
13 | "simple",
14 | "fast",
15 | "carousel",
16 | "cards"
17 | ],
18 | "main": "./build/index.js",
19 | "scripts": {
20 | "start": "webpack --watch",
21 | "watch": "webpack-dev-server --inline --hot --port=8005",
22 | "build": "webpack",
23 | "test": "echo \"Error: no test specified\" && exit 1"
24 | },
25 | "peerDependencies": {
26 | "prop-types": "^15.6.0",
27 | "react": "^16.11.0",
28 | "react-dom": "^16.11.0"
29 | },
30 | "devDependencies": {
31 | "babel-cli": "^7.0.0-beta.3",
32 | "babel-core": "^6.26.3",
33 | "babel-eslint": "^10.0.3",
34 | "babel-loader": "^7.1.5",
35 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
36 | "babel-plugin-transform-react-jsx": "^6.24.1",
37 | "babel-preset-env": "^1.7.0",
38 | "babel-preset-react": "^6.16.0",
39 | "babel-preset-stage-0": "^6.24.1",
40 | "eslint": "^6.6.0",
41 | "path": "^0.12.7",
42 | "prop-types": "^15.6.0",
43 | "react": "^16.11.0",
44 | "react-dom": "^16.11.0",
45 | "webpack": "^4.41.2",
46 | "webpack-cli": "^3.3.9",
47 | "webpack-dev-server": "^3.9.0"
48 | },
49 | "dependencies": {}
50 | }
51 |
--------------------------------------------------------------------------------
/src/Cards.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import {
4 | STYLES,
5 | getOpacity,
6 | getZIndex,
7 | getTransform,
8 | getBoxShadow,
9 | getCursor,
10 | } from './styles.js';
11 | import { POSITION, ALIGNMENT, SPREAD } from './constants.js';
12 |
13 |
14 | /**
15 | * React Card Carousel
16 | * @returns {React.Node}
17 | */
18 | class Cards extends Component {
19 |
20 | constructor(props) {
21 | super(props);
22 |
23 | this.state = {
24 | current_index: props.disable_fade_in ? props.initial_index : null,
25 | interval: null,
26 | };
27 | }
28 |
29 | static propTypes = {
30 | alignment: PropTypes.oneOf([ALIGNMENT.HORIZONTAL, ALIGNMENT.VERTICAL]),
31 | spread: PropTypes.oneOf([SPREAD.NARROW, SPREAD.MEDIUM, SPREAD.WIDE]),
32 | initial_index: PropTypes.number,
33 | disable_keydown: PropTypes.bool,
34 | disable_box_shadow: PropTypes.bool,
35 | disable_fade_in: PropTypes.bool,
36 | autoplay: PropTypes.bool,
37 | autoplay_speed: PropTypes.number,
38 | afterChange: PropTypes.func,
39 | }
40 |
41 | static defaultProps = {
42 | alignment: ALIGNMENT.HORIZONTAL,
43 | spread: SPREAD.MEDIUM,
44 | initial_index: 0,
45 | disable_keydown: false,
46 | disable_box_shadow: false,
47 | disable_fade_in: false,
48 | autoplay: false,
49 | autoplay_speed: 5000,
50 | afterChange: () => {},
51 | }
52 |
53 | /**
54 | * @public
55 | * Sets current index state
56 | */
57 | goTo = (idx) => {
58 | this.setState({ current_index: Number(idx) }, this.props.afterChange);
59 | }
60 |
61 | /**
62 | * @public
63 | * Goes to next card
64 | */
65 | next = () => {
66 | if (this._is_mounted) {
67 | this._cardOnClick(POSITION.NEXT);
68 | }
69 | }
70 |
71 | /**
72 | * @public
73 | * Goes to previous card
74 | */
75 | prev = () => this._cardOnClick(POSITION.PREV);
76 |
77 | /**
78 | * @public
79 | * Gets current card index
80 | */
81 | getCurrentIndex = () => this.state.current_index;
82 |
83 | componentDidMount() {
84 | const {
85 | initial_index,
86 | disable_keydown,
87 | disable_fade_in,
88 | autoplay,
89 | } = this.props;
90 |
91 | this._is_mounted = true;
92 |
93 | // Triggers initial animation
94 | if (!disable_fade_in) setTimeout(() => {
95 | this.setState({ current_index: initial_index });
96 | }, 0.25);
97 |
98 | // Sets right and left key event listener
99 | if (!disable_keydown) {
100 | document.onkeydown = this._keydownEventListener;
101 | }
102 |
103 | // Sets autoplay interval
104 | if (autoplay) this._autoplay();
105 | }
106 |
107 | componentWillUnmount() {
108 | this._is_mounted = false;
109 | if (!this.props.disable_keydown) document.onkeydown = null;
110 | }
111 |
112 | /**
113 | * Event listener for left/right arrow keys
114 | */
115 | _keydownEventListener = (e) => {
116 | if (e.which === 39) {
117 | return this.next();
118 | }
119 | if (e.which === 37) {
120 | return this.prev();
121 | }
122 | }
123 |
124 | /**
125 | * Sets interval for advancing cards
126 | */
127 | _autoplay = () => {
128 | if (this._is_mounted) {
129 | const { autoplay_speed } = this.props;
130 | const interval = setInterval(this.next, autoplay_speed);
131 | this.setState({ interval });
132 | }
133 | }
134 |
135 | /**
136 | * Resets autoplay interval
137 | */
138 | _resetInterval = () => {
139 | clearInterval(this.state.interval);
140 | this._autoplay();
141 | }
142 |
143 | /**
144 | * Gets card class for a specific card index
145 | * @param {Number} index
146 | * @returns {String}
147 | */
148 | _getCardClass = (index) => {
149 | const { children } = this.props;
150 | const { current_index } = this.state;
151 |
152 | if (current_index === null) return POSITION.HIDDEN;
153 |
154 | if (index === current_index) return POSITION.CURRENT;
155 |
156 | if (index === current_index + 1
157 | || (index === 0 && current_index === React.Children.count(children) - 1)) {
158 | return POSITION.NEXT;
159 | }
160 |
161 | if (index === current_index - 1
162 | || (index === React.Children.count(children) - 1 && current_index === 0)) {
163 | return POSITION.PREV;
164 | }
165 |
166 | return POSITION.HIDDEN;
167 | }
168 |
169 | /**
170 | * Changes current_index state
171 | * @param {String} position
172 | */
173 | _cardOnClick = (position) => {
174 | const { children, autoplay } = this.props;
175 | const { current_index } = this.state;
176 |
177 | if (autoplay) this._resetInterval();
178 |
179 | if (position === POSITION.NEXT) {
180 | if (current_index === React.Children.count(children) - 1) {
181 | this.setState({ current_index: 0 }, this.props.afterChange);
182 | }
183 | else this.setState({ current_index: current_index + 1 }, this.props.afterChange);
184 | }
185 |
186 | else if (position === POSITION.PREV) {
187 | if (current_index === 0) {
188 | this.setState({ current_index: React.Children.count(children) - 1 }, this.props.afterChange);
189 | }
190 | else this.setState({ current_index: current_index - 1 }, this.props.afterChange);
191 | }
192 | }
193 |
194 | /**
195 | * @returns {React.Node}
196 | */
197 | ChildComponents = () => {
198 | const { alignment, spread, disable_box_shadow } = this.props;
199 |
200 | return React.Children.map(
201 | this.props.children, (child, index) => {
202 |
203 | const position = this._getCardClass(index);
204 |
205 | return (
206 | this._cardOnClick(position) }
209 | style={{
210 | ...STYLES.CARD,
211 | opacity: getOpacity(position),
212 | zIndex: getZIndex(position),
213 | transform: getTransform(position, alignment, spread),
214 | boxShadow: getBoxShadow(position, alignment, disable_box_shadow),
215 | cursor: getCursor(position, alignment),
216 | }}
217 | >
218 | { child }
219 |
220 | );
221 | });
222 | }
223 |
224 | render() {
225 | return (
226 |
227 |
228 |
229 | );
230 | }
231 | }
232 |
233 | export default Cards;
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Card position
3 | * @returns {Object}
4 | */
5 | export const POSITION = {
6 | 'PREV': 'prev',
7 | 'NEXT': 'next',
8 | 'CURRENT': 'current',
9 | 'HIDDEN': 'hidden',
10 | };
11 |
12 | /**
13 | * Cards alignment
14 | * @returns {Object}
15 | */
16 | export const ALIGNMENT ={
17 | 'HORIZONTAL': 'horizontal',
18 | 'VERTICAL': 'vertical',
19 | };
20 |
21 | /**
22 | * Cards alignment
23 | * @returns {Object}
24 | */
25 | export const SPREAD ={
26 | 'NARROW': 'narrow',
27 | 'MEDIUM': 'medium',
28 | 'WIDE': 'wide'
29 | };
--------------------------------------------------------------------------------
/src/styles.js:
--------------------------------------------------------------------------------
1 | import { POSITION, ALIGNMENT, SPREAD } from './constants';
2 |
3 |
4 | export const STYLES = {
5 | CONTAINER: {
6 | position: 'relative',
7 | width: '100%',
8 | height: '100%',
9 | margin: 0,
10 | padding: 0,
11 | },
12 | CARD: {
13 | position: 'absolute',
14 | left: '50%',
15 | top: '50%',
16 | transition: 'all 0.6s',
17 | }
18 | };
19 |
20 | /**
21 | * @param {String} position
22 | * @returns {Number}
23 | */
24 | export function getOpacity(position) {
25 | if (position === POSITION.HIDDEN) return 0;
26 | return 1;
27 | }
28 |
29 | /**
30 | * @param {String} position
31 | * @returns {Number}
32 | */
33 | export function getZIndex(position) {
34 | if (position === POSITION.HIDDEN) return 0;
35 | if (position === POSITION.CURRENT) return 2;
36 | return 1;
37 | }
38 |
39 | /**
40 | * @param {String} position
41 | * @returns {String}
42 | */
43 | export function getTransform(position, alignment, spread) {
44 | const { prev, next } = _getTranslationDistances(spread);
45 |
46 | if (alignment === ALIGNMENT.HORIZONTAL) {
47 | if (position === POSITION.PREV) return `translate(${ prev }, -50%) scale(0.82)`;
48 | if (position === POSITION.NEXT) return `translate(${ next }, -50%) scale(0.82)`;
49 | }
50 | if (alignment === ALIGNMENT.VERTICAL) {
51 | if (position === POSITION.PREV) return `translate(-50%, ${ prev }) scale(0.82)`;
52 | if (position === POSITION.NEXT) return `translate(-50%, ${ next }) scale(0.82)`;
53 | }
54 | if (position === POSITION.HIDDEN) return `translate(-50%, -50%) scale(0.5)`;
55 |
56 | return 'translate(-50%, -50%)';
57 | }
58 |
59 | /**
60 | * @param {String} position
61 | * @returns {String}
62 | */
63 | export function getBoxShadow(position, alignment, disable_box_shadow) {
64 | if (!disable_box_shadow && position === POSITION.CURRENT) {
65 | if (alignment === ALIGNMENT.HORIZONTAL) {
66 | return '30px 0px 20px -20px rgba(0, 0, 0, .4), -30px 0px 20px -20px rgba(0, 0, 0, .4)';
67 | }
68 | if (alignment === ALIGNMENT.VERTICAL) {
69 | return '0px 30px 20px -20px rgba(0, 0, 0, .4), 0px -30px 20px -20px rgba(0, 0, 0, .4)';
70 |
71 | }
72 | }
73 | return 'unset';
74 | }
75 |
76 | /**
77 | * @param {String} position
78 | * @returns {String}
79 | */
80 | export function getCursor(position, alignment) {
81 | if (position === POSITION.NEXT) {
82 | if (alignment === ALIGNMENT.HORIZONTAL) return 'e-resize';
83 | if (alignment === ALIGNMENT.VERTICAL) return 's-resize';
84 | }
85 | if (position === POSITION.PREV) {
86 | if (alignment === ALIGNMENT.HORIZONTAL) return 'w-resize';
87 | if (alignment === ALIGNMENT.VERTICAL) return 'n-resize';
88 | }
89 | return 'unset';
90 | }
91 |
92 | /**
93 | * @param {String} spread
94 | * @returns {Object}
95 | */
96 | function _getTranslationDistances(spread) {
97 | let prev, next;
98 | if (spread === SPREAD.MEDIUM) {
99 | prev = '-85%';
100 | next = '-15%';
101 | }
102 | else if (spread === SPREAD.NARROW) {
103 | prev = '-75%';
104 | next = '-25%';
105 | }
106 | else if (spread === SPREAD.WIDE) {
107 | prev = '-95%';
108 | next = '-5%';
109 | }
110 |
111 | return { prev, next };
112 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 |
3 | module.exports = {
4 | mode: 'production',
5 | entry: `${ __dirname }/src/Cards.jsx`,
6 | output: {
7 | path: path.resolve(__dirname, 'build'),
8 | filename: 'index.js',
9 | libraryTarget: 'commonjs2'
10 | },
11 | module: {
12 | rules: [
13 | {
14 | test: /\.jsx?$/,
15 | include: path.resolve(__dirname, 'src'),
16 | exclude: /(node_modules|build)/,
17 | use: 'babel-loader'
18 | }
19 | ]
20 | },
21 | externals: {
22 | 'react': 'commonjs react'
23 | }
24 | };
--------------------------------------------------------------------------------