├── .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 |
--------------------------------------------------------------------------------