├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── rollup.config.js └── src ├── buttons.js ├── carousel.js ├── carousel.test.js ├── index.js ├── indicator-dots.js └── keyboard-navigator.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | .env 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | !public 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | env: 3 | - CI=true 4 | node_js: 5 | - "6" 6 | before_script: 7 | - npm install react react-dom 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # re-carousel [![npm-version][npm-badge]][npm-link] [![size][size-src]][size-link] 2 | 3 | Minimal carousel component for React. 4 | 5 | demo: https://amio.github.io/re-carousel/ 6 | 7 | ### Usage 8 | 9 | `import Carousel from 're-carousel'` 10 | 11 | then: 12 | 13 | ```jsx 14 | 15 |
Frame 1
16 |
Frame 2
17 |
Frame 3
18 |
19 | ``` 20 | 21 | ### Attributes 22 | 23 | All attributes are optional. 24 | 25 | - `axis` {Enum} `'x'` or `'y'` (`'x'` by default) 26 | - `loop` {Boolean} `true` or `false` (`false` by default) toggle loop mode. 27 | - `auto` {Boolean} `true` or `false` (`false` by default) toggle auto sliding. 28 | - `interval` {Number} (`4000`ms by default) interval for auto sliding. 29 | - `duration` {Number} (`300`ms by default) duration for animation. 30 | - `onTransitionEnd` {Function({ prev: HTMLElement, current: HTMLElement, next: HTMLElement})} on frames transition end callback. 31 | - `widgets` {Array of ReactClass} Indicator and switcher could be various, 32 | so it's not builtin. Here's some example custom widgets 33 | ([dots indicator](src/indicator-dots.js), 34 | [prev/next buttons](src/buttons.js), [keyboard navigation](src/keyboard-navigator)): 35 | 36 | ```javascript 37 | import Carousel from 're-carousel' 38 | import IndicatorDots from './indicator-dots' 39 | import Buttons from './buttons' 40 | 41 | export default function carousel () { 42 | return 43 |
Frame 1
44 |
Frame 2
45 |
Frame 3
46 |
47 | } 48 | ``` 49 | - `frames` {Array of ReactElement} If you want to create frames programmatically, 50 | use this attribute: 51 | 52 | ```javascript 53 | import Carousel from 're-carousel' 54 | 55 | export default function carousel (props) { 56 | const frames = props.frameArray.map((frame, i) => { 57 | return
Frame {i}
58 | }) 59 | return 60 | These children element will be appended to Carousel, 61 | as normal element other than "frame". 62 | 63 | } 64 | ``` 65 | - `className` {String} Custom class name. 66 | 67 | ### Contributes 68 | 69 | ```bash 70 | npm run start # start local dev server 71 | npm run build # build lib 72 | npm run test # run tests 73 | ``` 74 | 75 | ## License 76 | 77 | [MIT][mit] © [Amio][author] 78 | 79 | [npm-badge]: https://badgen.net/npm/v/re-carousel 80 | [npm-link]: https://www.npmjs.com/package/re-carousel 81 | [size-src]: https://badgen.net/bundlephobia/minzip/re-carousel 82 | [size-link]: https://bundlephobia.com/result?p=re-carousel 83 | [mit]: http://opensource.org/licenses/MIT 84 | [author]: http://github.com/amio 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "re-carousel", 3 | "version": "2.4.0", 4 | "description": "Minimal carousel component for React", 5 | "homepage": "https://amio.github.io/re-carousel", 6 | "repository": "amio/re-carousel", 7 | "license": "MIT", 8 | "main": "dist/carousel.js", 9 | "dependencies": { 10 | "prop-types": "^15.7.2" 11 | }, 12 | "peerDependencies": { 13 | "react": "^15 || ^16", 14 | "react-dom": "^15 || ^16" 15 | }, 16 | "devDependencies": { 17 | "cross-env": "^5.2.0", 18 | "push-dir": "^0.4.1", 19 | "react": "^16.8.6", 20 | "react-dom": "^16.8.6", 21 | "react-scripts": "^3.0.1", 22 | "rollup": "^1.0.0", 23 | "rollup-plugin-buble": "^0.19.8", 24 | "rollup-plugin-uglify": "^6.0.2" 25 | }, 26 | "scripts": { 27 | "prepack": "npm run build", 28 | "build": "cross-env NODE_ENV=production && rollup -c", 29 | "start": "react-scripts start", 30 | "build-pages": "react-scripts build", 31 | "test:watch": "react-scripts test --env=jsdom", 32 | "test": "CI=true react-scripts test --env=jsdom", 33 | "eject": "react-scripts eject", 34 | "predeploy": "npm run build-pages", 35 | "deploy": "push-dir --dir=build --branch=gh-pages" 36 | }, 37 | "files": [ 38 | "dist/carousel.js" 39 | ], 40 | "browserslist": [ 41 | ">0.2%", 42 | "not dead", 43 | "not ie <= 11", 44 | "not op_mini all" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amio/re-carousel/22be33389df3e58d433133aa284e90d3d24eab53/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | re-carousel: minimal carousel component for react. 8 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import buble from 'rollup-plugin-buble' 2 | import { uglify } from 'rollup-plugin-uglify' 3 | 4 | export default { 5 | input: 'src/carousel.js', 6 | output: { 7 | file: 'dist/carousel.js', 8 | format: 'cjs' 9 | }, 10 | plugins: [ buble(), uglify() ], 11 | external: ['react', 'react-dom', 'prop-types'] 12 | } 13 | -------------------------------------------------------------------------------- /src/buttons.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import propTypes from 'prop-types' 3 | 4 | const styles = { 5 | wrapper: { 6 | position: 'absolute', 7 | width: '100%', 8 | zIndex: '100', 9 | bottom: '0', 10 | textAlign: 'center' 11 | }, 12 | btn: { 13 | width: '30px', 14 | height: '30px', 15 | cursor: 'pointer', 16 | userSelect: 'none', 17 | position: 'absolute', 18 | bottom: '0', 19 | font: '16px/30px sans-serif', 20 | color: 'rgba(255,255,255,0.8)' 21 | }, 22 | left: { 23 | left: '0' 24 | }, 25 | right: { 26 | right: '0' 27 | } 28 | } 29 | 30 | export default function Buttons (props) { 31 | const prevBtnStyle = Object.assign({}, styles.btn, styles.left) 32 | const nextBtnStyle = Object.assign({}, styles.btn, styles.right) 33 | const { index, total, loop, prevHandler, nextHandler } = props 34 | return ( 35 |
36 | { (loop || index !== 0) && ( 37 |
38 | )} 39 | { (loop || index !== total - 1) && ( 40 |
41 | )} 42 |
43 | ) 44 | } 45 | 46 | Buttons.propTypes = { 47 | index: propTypes.number.isRequired, 48 | total: propTypes.number.isRequired, 49 | prevHandler: propTypes.func, 50 | nextHandler: propTypes.func 51 | } 52 | -------------------------------------------------------------------------------- /src/carousel.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const styles = { 5 | wrapper: { 6 | width: '100%', 7 | height: '100%', 8 | position: 'relative' 9 | }, 10 | frame: { 11 | width: '100%', 12 | height: '100%', 13 | position: 'absolute' 14 | } 15 | } 16 | 17 | class Carousel extends React.Component { 18 | constructor (props) { 19 | super(props) 20 | 21 | this.state = { 22 | frames: [].concat(props.frames || props.children || []), 23 | current: 0 24 | } 25 | 26 | this.mounted = false 27 | this.debounceTimeoutId = null 28 | this.onTouchStart = this.onTouchStart.bind(this) 29 | this.onTouchMove = this.onTouchMove.bind(this) 30 | this.onTouchEnd = this.onTouchEnd.bind(this) 31 | this.onResize = this.onResize.bind(this) 32 | this.autoSlide = this.autoSlide.bind(this) 33 | this.prev = this.prev.bind(this) 34 | this.next = this.next.bind(this) 35 | 36 | if (props.loop === false && props.auto) { 37 | console.warn('[re-carousel] Auto-slide only works in loop mode.') 38 | } 39 | } 40 | 41 | componentDidMount () { 42 | this.mounted = true 43 | this.prepareAutoSlide() 44 | this.hideFrames() 45 | 46 | this.refs.wrapper.addEventListener('touchmove', this.onTouchMove, {capture: true}) 47 | this.refs.wrapper.addEventListener('touchend', this.onTouchEnd, {capture: true}) 48 | window.addEventListener('resize', this.onResize); 49 | } 50 | 51 | componentWillUnmount () { 52 | this.mounted = false 53 | this.clearAutoTimeout() 54 | clearTimeout(this.debounceTimeoutId) 55 | 56 | this.refs.wrapper.removeEventListener('touchmove', this.onTouchMove, {capture: true}) 57 | this.refs.wrapper.removeEventListener('touchend', this.onTouchEnd, {capture: true}) 58 | window.removeEventListener('resize', this.onResize); 59 | } 60 | 61 | componentDidUpdate(_, prevState) { 62 | if (this.state.frames.length && this.state.frames.length !== prevState.frames.length) { 63 | // reset to default state 64 | this.hideFrames() 65 | this.prepareAutoSlide() 66 | } 67 | } 68 | 69 | static getDerivedStateFromProps(nextProps, prevState) { 70 | const frames = [].concat(nextProps.frames || nextProps.children || []) 71 | const nextState = { frames } 72 | if (frames.length && frames.length !== prevState.frames.length) { 73 | nextState.current = 0 74 | } 75 | return nextState 76 | } 77 | 78 | hideFrames () { 79 | for (let i = 1; i < this.state.frames.length; i++) { 80 | this.refs['f' + i].style.opacity = 0 81 | } 82 | } 83 | 84 | onResize() { 85 | clearTimeout(this.debounceTimeoutId); 86 | this.debounceTimeoutId = setTimeout(() => { 87 | this.updateFrameSize(() => { 88 | this.prepareSiblingFrames(); 89 | }); 90 | }, 25); 91 | } 92 | 93 | onTouchStart (e) { 94 | if (this.state.total < 2) return 95 | // e.preventDefault() 96 | 97 | this.clearAutoTimeout() 98 | this.updateFrameSize() 99 | this.prepareSiblingFrames() 100 | 101 | const { pageX, pageY } = (e.touches && e.touches[0]) || e 102 | this.setState({ 103 | startX: pageX, 104 | startY: pageY, 105 | deltaX: 0, 106 | deltaY: 0 107 | }) 108 | 109 | this.refs.wrapper.addEventListener('mousemove', this.onTouchMove, {capture: true}) 110 | this.refs.wrapper.addEventListener('mouseup', this.onTouchEnd, {capture: true}) 111 | this.refs.wrapper.addEventListener('mouseleave', this.onTouchEnd, {capture: true}) 112 | } 113 | 114 | onTouchMove (e) { 115 | if (e.touches && e.touches.length > 1) return 116 | this.clearAutoTimeout() 117 | 118 | const { pageX, pageY } = (e.touches && e.touches[0]) || e 119 | let deltaX = pageX - this.state.startX 120 | let deltaY = pageY - this.state.startY 121 | this.setState({ 122 | deltaX: deltaX, 123 | deltaY: deltaY 124 | }) 125 | 126 | if (this.props.axis === 'x' && Math.abs(deltaX) > Math.abs(deltaY)) { 127 | e.preventDefault() 128 | e.stopPropagation() 129 | } 130 | if (this.props.axis === 'y' && Math.abs(deltaY) > Math.abs(deltaX)) { 131 | e.preventDefault() 132 | e.stopPropagation() 133 | } 134 | 135 | // when reach frames edge in non-loop mode, reduce drag effect. 136 | if (!this.props.loop) { 137 | if (this.state.current === this.state.frames.length - 1) { 138 | deltaX < 0 && (deltaX /= 3) 139 | deltaY < 0 && (deltaY /= 3) 140 | } 141 | if (this.state.current === 0) { 142 | deltaX > 0 && (deltaX /= 3) 143 | deltaY > 0 && (deltaY /= 3) 144 | } 145 | } 146 | 147 | this.moveFramesBy(deltaX, deltaY) 148 | } 149 | 150 | onTouchEnd () { 151 | const direction = this.decideEndPosition() 152 | direction && this.transitFramesTowards(direction) 153 | 154 | // cleanup 155 | this.refs.wrapper.removeEventListener('mousemove', this.onTouchMove, {capture: true}) 156 | this.refs.wrapper.removeEventListener('mouseup', this.onTouchEnd, {capture: true}) 157 | this.refs.wrapper.removeEventListener('mouseleave', this.onTouchEnd, {capture: true}) 158 | 159 | setTimeout(() => this.prepareAutoSlide(), this.props.duration) 160 | } 161 | 162 | decideEndPosition () { 163 | const { deltaX = 0, deltaY = 0, current, frames } = this.state 164 | const { axis, loop, minMove } = this.props 165 | 166 | switch (axis) { 167 | case 'x': 168 | if (loop === false) { 169 | if (current === 0 && deltaX > 0) return 'origin' 170 | if (current === frames.length - 1 && deltaX < 0) return 'origin' 171 | } 172 | if (Math.abs(deltaX) < minMove) return 'origin' 173 | return deltaX > 0 ? 'right' : 'left' 174 | case 'y': 175 | if (loop === false) { 176 | if (current === 0 && deltaY > 0) return 'origin' 177 | if (current === frames.length - 1 && deltaY < 0) return 'origin' 178 | } 179 | if (Math.abs(deltaY) < minMove) return 'origin' 180 | return deltaY > 0 ? 'down' : 'up' 181 | default: 182 | } 183 | } 184 | 185 | moveFramesBy (deltaX, deltaY) { 186 | const { prev, current, next } = this.state.movingFrames 187 | const { frameWidth, frameHeight } = this.state 188 | 189 | switch (this.props.axis) { 190 | case 'x': 191 | translateXY(current, deltaX, 0) 192 | if (deltaX < 0) { 193 | translateXY(next, deltaX + frameWidth, 0) 194 | } else { 195 | translateXY(prev, deltaX - frameWidth, 0) 196 | } 197 | break 198 | case 'y': 199 | translateXY(current, 0, deltaY) 200 | if (deltaY < 0) { 201 | translateXY(next, 0, deltaY + frameHeight) 202 | } else { 203 | translateXY(prev, 0, deltaY - frameHeight) 204 | } 205 | break 206 | default: 207 | } 208 | } 209 | 210 | prepareAutoSlide () { 211 | if (this.state.frames.length < 2) return 212 | 213 | this.clearAutoTimeout() 214 | this.updateFrameSize(() => { 215 | this.prepareSiblingFrames() 216 | }) 217 | 218 | // auto slide only avalible in loop mode 219 | if (this.mounted && this.props.loop && this.props.auto) { 220 | const slideTimeoutID = setTimeout(this.autoSlide, this.props.interval) 221 | this.setState({ slider: slideTimeoutID }) 222 | } 223 | } 224 | 225 | // auto slide to 'next' or 'prev' 226 | autoSlide (rel) { 227 | this.clearAutoTimeout() 228 | 229 | switch (rel) { 230 | case 'prev': 231 | this.transitFramesTowards(this.props.axis === 'x' ? 'right' : 'down') 232 | break 233 | case 'next': 234 | default: 235 | this.transitFramesTowards(this.props.axis === 'x' ? 'left' : 'up') 236 | } 237 | 238 | // prepare next move after animation 239 | setTimeout(() => this.prepareAutoSlide(), this.props.duration) 240 | } 241 | 242 | next () { 243 | const { current, frames } = this.state 244 | if (!this.props.loop && current === frames.length - 1) return false 245 | this.autoSlide('next') 246 | } 247 | 248 | prev () { 249 | if (!this.props.loop && this.state.current === 0) return false 250 | const { prev, next } = this.state.movingFrames 251 | 252 | if (prev === next) { 253 | // Reprepare start position of prev frame 254 | // (it was positioned as "next" frame) 255 | if (this.props.axis === 'x') { 256 | translateXY(prev, -this.state.frameWidth, 0, 0) 257 | } else { 258 | translateXY(prev, 0, -this.state.frameHeight, 0) 259 | } 260 | prev.getClientRects() // trigger layout 261 | } 262 | 263 | this.autoSlide('prev') 264 | } 265 | 266 | clearAutoTimeout () { 267 | clearTimeout(this.state.slider) 268 | } 269 | 270 | updateFrameSize (cb) { 271 | const { width, height } = window.getComputedStyle(this.refs.wrapper) 272 | this.setState({ 273 | frameWidth: parseFloat(width.split('px')[0]), 274 | frameHeight: parseFloat(height.split('px')[0]) 275 | }, cb) 276 | } 277 | 278 | getSiblingFrames () { 279 | return { 280 | current: this.refs['f' + this.getFrameId()], 281 | prev: this.refs['f' + this.getFrameId('prev')], 282 | next: this.refs['f' + this.getFrameId('next')] 283 | } 284 | } 285 | 286 | prepareSiblingFrames () { 287 | const siblings = this.getSiblingFrames() 288 | 289 | if (!this.props.loop) { 290 | this.state.current === 0 && (siblings.prev = undefined) 291 | this.state.current === this.state.frames.length - 1 && (siblings.next = undefined) 292 | } 293 | 294 | this.setState({ movingFrames: siblings }) 295 | 296 | // prepare frames position 297 | translateXY(siblings.current, 0, 0) 298 | if (this.props.axis === 'x') { 299 | translateXY(siblings.prev, -this.state.frameWidth, 0) 300 | translateXY(siblings.next, this.state.frameWidth, 0) 301 | } else { 302 | translateXY(siblings.prev, 0, -this.state.frameHeight) 303 | translateXY(siblings.next, 0, this.state.frameHeight) 304 | } 305 | 306 | return siblings 307 | } 308 | 309 | getFrameId (pos) { 310 | const { frames, current } = this.state 311 | const total = frames.length 312 | switch (pos) { 313 | case 'prev': 314 | return (current - 1 + total) % total 315 | case 'next': 316 | return (current + 1) % total 317 | default: 318 | return current 319 | } 320 | } 321 | 322 | transitFramesTowards (direction) { 323 | const { prev, current, next } = this.state.movingFrames 324 | const { duration, axis, onTransitionEnd } = this.props 325 | 326 | let newCurrentId = this.state.current 327 | switch (direction) { 328 | case 'up': 329 | translateXY(current, 0, -this.state.frameHeight, duration) 330 | translateXY(next, 0, 0, duration) 331 | newCurrentId = this.getFrameId('next') 332 | break 333 | case 'down': 334 | translateXY(current, 0, this.state.frameHeight, duration) 335 | translateXY(prev, 0, 0, duration) 336 | newCurrentId = this.getFrameId('prev') 337 | break 338 | case 'left': 339 | translateXY(current, -this.state.frameWidth, 0, duration) 340 | translateXY(next, 0, 0, duration) 341 | newCurrentId = this.getFrameId('next') 342 | break 343 | case 'right': 344 | translateXY(current, this.state.frameWidth, 0, duration) 345 | translateXY(prev, 0, 0, duration) 346 | newCurrentId = this.getFrameId('prev') 347 | break 348 | default: // back to origin 349 | translateXY(current, 0, 0, duration) 350 | if (axis === 'x') { 351 | translateXY(prev, -this.state.frameWidth, 0, duration) 352 | translateXY(next, this.state.frameWidth, 0, duration) 353 | } else if (axis === 'y') { 354 | translateXY(prev, 0, -this.state.frameHeight, duration) 355 | translateXY(next, 0, this.state.frameHeight, duration) 356 | } 357 | } 358 | 359 | onTransitionEnd && setTimeout(() => onTransitionEnd(this.getSiblingFrames()), duration) 360 | 361 | this.setState({ current: newCurrentId }) 362 | } 363 | 364 | // debugFrames () { 365 | // console.log('>>> DEBUG-FRAMES: current', this.state.current) 366 | // const len = this.state.frames.length 367 | // for (let i = 0; i < len; ++i) { 368 | // const ref = this.refs['f' + i] 369 | // console.info(ref.innerText.trim(), ref.style.transform) 370 | // } 371 | // } 372 | 373 | render () { 374 | const { frames, current } = this.state 375 | const { widgets, axis, loop, auto, interval } = this.props 376 | const wrapperStyle = objectAssign(styles.wrapper, this.props.style) 377 | 378 | return ( 379 |
380 |
386 | { 387 | frames.map((frame, i) => { 388 | const frameStyle = objectAssign({zIndex: 99 - i}, styles.frame) 389 | return
{frame}
390 | }) 391 | } 392 | { this.props.frames && this.props.children } 393 |
394 | { 395 | widgets && [].concat(widgets).map((Widget, i) => ( 396 | 403 | )) 404 | } 405 |
406 | ) 407 | } 408 | } 409 | 410 | Carousel.propTypes = { 411 | axis: PropTypes.oneOf(['x', 'y']), 412 | auto: PropTypes.bool, 413 | loop: PropTypes.bool, 414 | interval: PropTypes.number, 415 | duration: PropTypes.number, 416 | widgets: PropTypes.arrayOf(PropTypes.func), 417 | frames: PropTypes.arrayOf(PropTypes.element), 418 | style: PropTypes.object, 419 | minMove: PropTypes.number, 420 | onTransitionEnd: PropTypes.func 421 | } 422 | 423 | Carousel.defaultProps = { 424 | axis: 'x', 425 | auto: false, 426 | loop: false, 427 | interval: 5000, 428 | duration: 300, 429 | minMove: 42 430 | } 431 | 432 | function translateXY (el, x, y, duration = 0) { 433 | if (!el) return 434 | 435 | el.style.opacity = '1' 436 | 437 | // animation 438 | el.style.transitionDuration = duration + 'ms' 439 | el.style.webkitTransitionDuration = duration + 'ms' 440 | 441 | el.style.transform = `translate(${x}px, ${y}px)` 442 | el.style.webkitTransform = `translate(${x}px, ${y}px) translateZ(0)` 443 | } 444 | 445 | function objectAssign (target) { 446 | var output = Object(target) 447 | for (var index = 1; index < arguments.length; index++) { 448 | var source = arguments[index] 449 | if (source !== undefined && source !== null) { 450 | for (var nextKey in source) { 451 | if (source.hasOwnProperty(nextKey)) { 452 | output[nextKey] = source[nextKey] 453 | } 454 | } 455 | } 456 | } 457 | return output 458 | } 459 | 460 | export default Carousel 461 | -------------------------------------------------------------------------------- /src/carousel.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import Carousel from './carousel' 4 | 5 | global.it('renders 1 frame without crashing', () => { 6 | const div = document.createElement('div') 7 | ReactDOM.render(( 8 | 9 |

FRAME 2

10 |
11 | ), div) 12 | }) 13 | 14 | global.it('renders 2 frames without crashing', () => { 15 | const div = document.createElement('div') 16 | ReactDOM.render(( 17 | 18 |

FRAME 2

19 |

FRAME 3

20 |
21 | ), div) 22 | }) 23 | 24 | global.it('renders without crashing', () => { 25 | const div = document.createElement('div') 26 | ReactDOM.render(( 27 | 28 |

FRAME 1

29 |

FRAME 2

30 |

FRAME 3

31 |
32 | ), div) 33 | }) 34 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import Carousel from './carousel' 4 | import Dots from './indicator-dots' 5 | import Buttons from './buttons' 6 | 7 | // Main App 8 | class App extends React.Component { 9 | constructor () { 10 | super() 11 | this.state = { 12 | axis: 'x' 13 | } 14 | this.setAxis = axis => { 15 | return () => this.setState({'axis': axis}) 16 | } 17 | } 18 | render () { 19 | return ( 20 |
21 |
22 | horizontal 24 | vertical 26 |
27 | 28 |

FRAME 1

29 |

FRAME 2

30 |

FRAME 3

31 |
32 |
33 | ) 34 | } 35 | } 36 | 37 | ReactDOM.render(, document.getElementById('root')) 38 | -------------------------------------------------------------------------------- /src/indicator-dots.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | function Dot (props) { 5 | return ( 6 | 16 | ) 17 | } 18 | 19 | export default function IndicatorDots (props) { 20 | const wrapperStyle = { 21 | position: 'absolute', 22 | width: '100%', 23 | zIndex: '100', 24 | bottom: '0px', 25 | textAlign: 'center' 26 | } 27 | 28 | if (props.total < 2) { 29 | // Hide dots when there is only one dot. 30 | return
31 | } else { 32 | return ( 33 |
{ 34 | Array.apply(null, Array(props.total)).map((x, i) => { 35 | return 36 | }) 37 | }
38 | ) 39 | } 40 | } 41 | 42 | IndicatorDots.propTypes = { 43 | index: PropTypes.number.isRequired, 44 | total: PropTypes.number.isRequired 45 | } 46 | -------------------------------------------------------------------------------- /src/keyboard-navigator.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default class KeyboardNavigator extends React.Component { 4 | onKeydown = event => { 5 | const { prevHandler, nextHandler } = this.props 6 | switch (event.key) { 7 | case 'j': 8 | prevHandler() 9 | break; 10 | case 'k': 11 | nextHandler() 12 | break 13 | } 14 | } 15 | 16 | componentDidMount () { 17 | const { index, total, loop, prevHandler, nextHandler } = props 18 | window.addEventListener('keydown', this.onKeydown) 19 | } 20 | 21 | componentWillUnmount () { 22 | window.removeEventListener('keydown', this.onKeydown) 23 | } 24 | 25 | render () { return null } 26 | } 27 | --------------------------------------------------------------------------------