├── .babelrc ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── docs.md │ └── feature_request.md └── stale.yml ├── .gitignore ├── .npmrc ├── .yarnrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── analytics.js ├── demoApp └── src │ ├── DemoApp.js │ └── index.js ├── doczrc.js ├── gatsby-node.js ├── libName.js ├── logo.png ├── package.json ├── rollup.config.demo.js ├── rollup.config.js ├── src ├── .eslintrc ├── docs │ ├── components │ │ ├── Button.js │ │ ├── Description.js │ │ ├── Flex.js │ │ ├── Item.js │ │ ├── ItemContainer.js │ │ ├── ItemWithLink.js │ │ ├── Poster.js │ │ ├── SimpleItem.js │ │ ├── Square.js │ │ ├── Title.js │ │ ├── breakPoints.js │ │ └── itemsCollection.js │ ├── mdx │ │ ├── autoTabIndexVisibleItems.mdx │ │ ├── breakpoints.mdx │ │ ├── easing.mdx │ │ ├── enableAutoPlay.mdx │ │ ├── focusOnSelect.mdx │ │ ├── goTo.mdx │ │ ├── index.mdx │ │ ├── initialFirstItem.mdx │ │ ├── isRTL.mdx │ │ ├── itemPadding.mdx │ │ ├── itemPosition.mdx │ │ ├── itemsToScroll.mdx │ │ ├── itemsToShow.mdx │ │ ├── onChange.mdx │ │ ├── onNextEnd.mdx │ │ ├── onNextStart.mdx │ │ ├── onPrevEnd.mdx │ │ ├── onPrevStart.mdx │ │ ├── onResize.mdx │ │ ├── outerSpacing.mdx │ │ ├── renderArrow.mdx │ │ ├── renderPagination.mdx │ │ ├── showEmptySlots.mdx │ │ ├── slideNext.mdx │ │ ├── styling.mdx │ │ └── verticalMode.mdx │ └── styles │ │ └── styling.css ├── react-elastic-carousel │ ├── actions │ │ ├── consts.js │ │ └── itemsActions.js │ ├── components │ │ ├── Arrow.js │ │ ├── Carousel.js │ │ ├── ItemWrapperContainer.js │ │ ├── Pagination │ │ │ ├── Dot.js │ │ │ ├── Pagination.js │ │ │ ├── __test__ │ │ │ │ └── Dot.js │ │ │ └── index.js │ │ ├── Track.js │ │ ├── __tests__ │ │ │ └── Carousel.test.js │ │ └── styled │ │ │ ├── Button.js │ │ │ ├── CarouselWrapper.js │ │ │ ├── ItemWrapper.js │ │ │ ├── Slider.js │ │ │ ├── SliderContainer.js │ │ │ ├── StyledCarousel.js │ │ │ └── index.js │ ├── consts.js │ ├── index.d.ts │ ├── index.js │ ├── reducers │ │ ├── __tests__ │ │ │ └── items.test.js │ │ └── items.js │ └── utils │ │ ├── __tests__ │ │ └── helpers.test.js │ │ └── helpers.js └── setupTests.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "modules": false 5 | }], 6 | "@babel/preset-react" 7 | ], 8 | "plugins": ["@babel/plugin-proposal-class-properties"] 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "standard", 5 | "standard-react", 6 | "plugin:prettier/recommended" 7 | ], 8 | "env": { 9 | "es6": true 10 | }, 11 | "plugins": [ 12 | "react", 13 | "prettier" 14 | ], 15 | "parserOptions": { 16 | "sourceType": "module" 17 | }, 18 | "rules": { 19 | // don't force es6 functions to include space before paren 20 | "space-before-function-paren": 0, 21 | 22 | // allow specifying true explicitly for boolean props 23 | "react/jsx-boolean-value": 0 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Sandbox link** 24 | Please provide a link to a [codesandbox](https://codesandbox.io/) with the reproduced bug 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Desktop (please complete the following information):** 30 | - OS: [e.g. iOS] 31 | - Browser [e.g. chrome, safari] 32 | - Version [e.g. 22] 33 | 34 | **Smartphone (please complete the following information):** 35 | - Device: [e.g. iPhone6] 36 | - OS: [e.g. iOS8.1] 37 | - Browser [e.g. stock browser, safari] 38 | - Version [e.g. 22] 39 | 40 | **Additional context** 41 | Add any other context about the problem here. 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/docs.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - bug 10 | - block-stale 11 | # Label to use when marking an issue as stale 12 | staleLabel: stale 13 | # Comment to post when marking an issue as stale. Set to `false` to disable 14 | markComment: > 15 | This issue has been automatically marked as stale because it has not had 16 | recent activity. It will be closed if no further activity occurs. Thank you 17 | for your contributions. 18 | # Comment to post when closing a stale issue. Set to `false` to disable 19 | closeComment: false 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # builds 8 | build 9 | dist 10 | demo 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | # docz 21 | .docz/cache/ 22 | 23 | # test-related files 24 | coverage 25 | 26 | 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | .vscode 32 | .editorconfig 33 | .docz -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | strict-ssl false 2 | registry "https://registry.npmjs.org/" -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | We use [Semantic Versioning](http://semver.org/). 4 | Each release can be found in the Github [Releases page](https://github.com/sag1v/react-elastic-carousel/releases). 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at sagiv.bengiat@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Sagiv ben giat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # react-elastic-carousel 3 | 4 | > A flexible and responsive carousel component for react 5 | 6 | [![NPM](https://img.shields.io/npm/v/react-elastic-carousel.svg?style=flat-square)](https://www.npmjs.com/package/react-elastic-carousel) ![npm](https://img.shields.io/npm/dw/react-elastic-carousel?style=flat-square) ![npm bundle size](https://img.shields.io/bundlephobia/min/react-elastic-carousel?style=flat-square) 7 | 8 | ## Why do we need yet another carousel component 9 | 10 | - **Element resize support (true responsiveness)** 11 | Most of the carousel components are responsive to the viewport size, but this is not a real responsive support as we can have an element with a `width:500px` on a `1200px` screen, most carousel component will "think" we are on a `1200px` mode because they "watch" the view-port's size and not the wrapping element's size. 12 | This is the reason why `react-eleastic-carousel` is using the [resize-observer](https://developers.google.com/web/updates/2016/10/resizeobserver) which gives us a true responsive support, not matter on what screen size we are. 13 | 14 | - **RTL (right-to-left) support** 15 | Supporting right-to-left languages requires a full support for right-to-left rendering and animations which is not supported in most of the carousel components out there. also, right-to-left support is [important and should be a standard for most applications](https://www.youtube.com/watch?v=A_3BfONFRUc). 16 | 17 | ## [Live Demos & Docs](https://sag1v.github.io/react-elastic-carousel/) 18 | 19 | ## Install 20 | 21 | ```bash 22 | npm install --save react-elastic-carousel 23 | ``` 24 | 25 | or 26 | 27 | ```bash 28 | yarn add react-elastic-carousel 29 | ``` 30 | 31 | ### Note 32 | 33 | `react-elastic-carousel` is using [styled-components](https://github.com/styled-components/styled-components) for styling, this means that you should install it as well: 34 | 35 | ```bash 36 | npm install --save styled-components 37 | ``` 38 | 39 | ## Usage 40 | 41 | ```jsx 42 | import React, { Component } from 'react'; 43 | import Carousel from 'react-elastic-carousel'; 44 | 45 | class App extends Component { 46 | state = { 47 | items: [ 48 | {id: 1, title: 'item #1'}, 49 | {id: 2, title: 'item #2'}, 50 | {id: 3, title: 'item #3'}, 51 | {id: 4, title: 'item #4'}, 52 | {id: 5, title: 'item #5'} 53 | ] 54 | } 55 | 56 | render () { 57 | const { items } = this.state; 58 | return ( 59 | 60 | {items.map(item =>
{item.title}
)} 61 |
62 | ) 63 | } 64 | } 65 | ``` 66 | 67 | ## Playground 68 | 69 | [![Edit react-elastic-carousel](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/21o46mkwnr) 70 | 71 | ## Development 72 | 73 | ```console 74 | git clone https://github.com/sag1v/react-elastic-carousel.git 75 | cd react-elastic-carousel 76 | yarn 77 | ``` 78 | 79 | ### To run the docs site run 80 | 81 | ```console 82 | yarn start 83 | ``` 84 | 85 | ### to run a demo Application run 86 | 87 | ```console 88 | yarn demo 89 | ``` 90 | 91 | The application is running at http://localhost:8888 92 | 93 | ## License 94 | 95 | MIT © [sag1v](https://github.com/sag1v) 96 | -------------------------------------------------------------------------------- /analytics.js: -------------------------------------------------------------------------------- 1 | {/* */} 2 | window.dataLayer = window.dataLayer || []; 3 | function gtag(){dataLayer.push(arguments);} 4 | gtag('js', new Date()); 5 | 6 | gtag('config', 'UA-127217040-1'); 7 | -------------------------------------------------------------------------------- /demoApp/src/DemoApp.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from "react"; 2 | import Carousel from "../../src/react-elastic-carousel/components/Carousel"; 3 | import styled from "styled-components"; 4 | 5 | const Item = styled.div` 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | color: #fff; 10 | background-color: green; 11 | width: 100%; 12 | height: 150px; 13 | margin: 15px; 14 | `; 15 | 16 | const Layout = styled.div` 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | height: 100vh; 21 | `; 22 | 23 | const ControlsLayout = styled.div` 24 | display: flex; 25 | flex-direction: column; 26 | margin: 25px; 27 | `; 28 | 29 | const StyledControlFields = styled.div` 30 | display: flex; 31 | margin: 5px; 32 | `; 33 | 34 | const breakPoints = [ 35 | { width: 200, itemsToShow: 1 }, 36 | { width: 600, itemsToShow: 2 }, 37 | ]; 38 | const toggle = (updater) => () => updater((o) => !o); 39 | 40 | const CheckBox = ({ label, onToggle, ...rest }) => { 41 | return ( 42 | 43 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | const serverItems = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 50 | 51 | const DemoApp = () => { 52 | const [show, setShow] = useState(true); 53 | const [enableAutoPlay, setEnableAutoPlay] = useState(false); 54 | const [items, setItems] = useState([]); 55 | const [itemsToShow, setItemsToShow] = useState(3); 56 | const [showArrows, setShowArrows] = useState(true); 57 | const [pagination, setPagination] = useState(true); 58 | const [verticalMode, setVerticalMode] = useState(false); 59 | const carouselRef = useRef(); 60 | 61 | const addItem = () => { 62 | setItems((currentItems) => [...currentItems, currentItems.length + 1]); 63 | }; 64 | 65 | const removeItem = () => { 66 | setItems((currentItems) => currentItems.slice(0, currentItems.length - 1)); 67 | }; 68 | 69 | const updateItemsToShow = ({ target }) => 70 | setItemsToShow(Number(target.value)); 71 | 72 | const goTo = ({ target }) => carouselRef.current.goTo(Number(target.value)); 73 | 74 | useEffect(() => { 75 | setTimeout(() => { 76 | setItems(serverItems); 77 | }, 2500); 78 | }, []); 79 | 80 | return ( 81 | 82 | 83 | 84 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 103 | 104 | 109 | 114 | 119 | 124 | 125 | {show && ( 126 | 134 | {items.map((item) => ( 135 | {item} 136 | ))} 137 | 138 | )} 139 | 140 | ); 141 | }; 142 | 143 | export default DemoApp; 144 | -------------------------------------------------------------------------------- /demoApp/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import DemoApp from "./DemoApp"; 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById("app") 10 | ); 11 | -------------------------------------------------------------------------------- /doczrc.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import pkg from './package.json'; 3 | 4 | const libName = pkg.name; 5 | 6 | export default { 7 | ignore: ['README.md', 'changelog.md', 'CODE_OF_CONDUCT.md', 'contributing.md', 'LICENSE.md'], 8 | //src: './src', 9 | dest: './demo', 10 | port: 8888, 11 | hashRouter: true, 12 | base: `/${libName}/`, 13 | menu: [ 14 | "Getting started", 15 | { 16 | name: 'Examples', 17 | menu: [ 18 | 'itemsToScroll', 19 | 'itemsToShow', 20 | 'breakPoints', 21 | 'verticalMode', 22 | 'initialActiveIndex', 23 | 'focusOnSelect', 24 | 'isRTL', 25 | 'enableAutoPlay', 26 | 'itemPadding', 27 | 'outerSpacing', 28 | 'showEmptySlots', 29 | 'itemPosition', 30 | 'easing', 31 | 'renderArrow', 32 | 'renderPagination', 33 | 'autoTabIndexVisibleItems', 34 | 'onChange', 35 | 'onNextStart', 36 | 'onNextEnd', 37 | 'onPrevStart', 38 | 'onPrevEnd', 39 | 'onResize', 40 | 'slideNext / slidePrev', 41 | 'goTo', 42 | 'Styling' 43 | ] 44 | } 45 | ], 46 | // htmlContext: { 47 | // head: { 48 | // scripts: [ 49 | // { 50 | // async: 'async', 51 | // src: 'https://www.googletagmanager.com/gtag/js?id=UA-127217040-1' 52 | // } 53 | // ], 54 | // raw: ``, 60 | // links: [{ 61 | // rel: 'stylesheet', 62 | // href: '//codemirror.net/theme/dracula.css' 63 | // }] 64 | // } 65 | // }, 66 | // themeConfig: { 67 | // // logo: { 68 | // // src: "//image.ibb.co/iACdcK/carousel_logo.png", 69 | // // width: "50%" 70 | // // }, 71 | // mode: 'light', 72 | // showPlaygroundEditor: true, 73 | // codemirrorTheme: 'dracula', 74 | // styles: { 75 | // body: { 76 | // fontFamily: "'Source Sans Pro', Helvetica, sans-serif", 77 | // fontSize: 16, 78 | // lineHeight: 1.6, 79 | // }, 80 | // container: { 81 | // width: '100%', 82 | // padding: ['20px 40px'], 83 | // }, 84 | // } 85 | // }, 86 | } 87 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const pkg = require('./package.json'); 3 | 4 | const libName = pkg.name; 5 | 6 | module.exports = { 7 | plugins: [ 8 | { 9 | resolve: `gatsby-plugin-google-analytics`, 10 | options: { 11 | trackingId: "UA-127217040-1", 12 | head: true, 13 | }, 14 | }, 15 | ], 16 | onCreateWebpackConfig: ({ 17 | stage, 18 | rules, 19 | loaders, 20 | plugins, 21 | actions, 22 | }) => { 23 | actions.setWebpackConfig({ 24 | resolve: { 25 | modules: [path.resolve(__dirname, '../src'), 'node_modules'], 26 | alias: { 27 | [libName]: path.join(__dirname, `/src/${libName}/index.js`) 28 | } 29 | } 30 | }) 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /libName.js: -------------------------------------------------------------------------------- 1 | import pkg from './package.json'; 2 | export default pkg.name; -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sag1v/react-elastic-carousel/6f129b51bf39b36d31375e39c66a8f0c4b669076/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-elastic-carousel", 3 | "version": "0.11.5", 4 | "description": "A flexible and responsive carousel component for react", 5 | "author": "sag1v (Sagiv Ben Giat)", 6 | "license": "MIT", 7 | "repository": "sag1v/react-elastic-carousel", 8 | "main": "dist/index.js", 9 | "module": "dist/index.es.js", 10 | "types": "./dist/index.d.ts", 11 | "jsnext:main": "dist/index.es.js", 12 | "publishConfig": { 13 | "registry": "https://registry.npmjs.org" 14 | }, 15 | "engines": { 16 | "node": ">=8", 17 | "npm": ">=5" 18 | }, 19 | "scripts": { 20 | "start": "concurrently -r -k -s all \"docz dev\" \"yarn run lint:watch\"", 21 | "demo": "concurrently -r -k -s all \"rollup --config rollup.config.demo.js --watch\" \"yarn run lint:watch\"", 22 | "lint:fix": "eslint src/* --fix", 23 | "lint:watch": "esw --watch --fix src/*", 24 | "test": "cross-env CI=1 react-scripts test --env=jsdom", 25 | "test:watch": "react-scripts test --env=jsdom", 26 | "prebuild": "yarn run lint:fix", 27 | "build": "rollup -c", 28 | "build-doc": "docz build", 29 | "deploy-doc": "gh-pages -d demo" 30 | }, 31 | "lint-staged": { 32 | "*.js": "eslint src/. --fix" 33 | }, 34 | "dependencies": { 35 | "classnames": "^2.2.6", 36 | "react-only-when": "^1.0.2", 37 | "react-swipeable": "^5.5.1", 38 | "resize-observer-polyfill": "1.5.0" 39 | }, 40 | "peerDependencies": { 41 | "prop-types": "^15.5.4", 42 | "react": "15 - 17", 43 | "react-dom": "15 - 17", 44 | "styled-components": "^5.1.0" 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "^7.3.4", 48 | "@babel/plugin-external-helpers": "^7.0.0", 49 | "@babel/plugin-proposal-class-properties": "^7.3.4", 50 | "@babel/plugin-proposal-decorators": "^7.0.0", 51 | "@babel/plugin-proposal-do-expressions": "^7.0.0", 52 | "@babel/plugin-proposal-export-default-from": "^7.0.0", 53 | "@babel/plugin-proposal-export-namespace-from": "^7.0.0", 54 | "@babel/plugin-proposal-function-bind": "^7.0.0", 55 | "@babel/plugin-proposal-function-sent": "^7.0.0", 56 | "@babel/plugin-proposal-json-strings": "^7.0.0", 57 | "@babel/plugin-proposal-logical-assignment-operators": "^7.0.0", 58 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0", 59 | "@babel/plugin-proposal-numeric-separator": "^7.0.0", 60 | "@babel/plugin-proposal-optional-chaining": "^7.0.0", 61 | "@babel/plugin-proposal-pipeline-operator": "^7.0.0", 62 | "@babel/plugin-proposal-throw-expressions": "^7.0.0", 63 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 64 | "@babel/plugin-syntax-import-meta": "^7.0.0", 65 | "@babel/preset-env": "^7.3.4", 66 | "@babel/preset-react": "^7.0.0", 67 | "@rollup/plugin-replace": "^2.3.4", 68 | "babel-eslint": "^9.0.0", 69 | "concurrently": "^4.1.0", 70 | "cross-env": "^5.1.4", 71 | "docz": "^2.3.1", 72 | "enzyme": "^3.6.0", 73 | "enzyme-adapter-react-16": "^1.5.0", 74 | "eslint": "5.12.0", 75 | "eslint-config-prettier": "^3.0.1", 76 | "eslint-config-standard": "^12.0.0", 77 | "eslint-config-standard-react": "^7.0.0", 78 | "eslint-plugin-import": "^2.13.0", 79 | "eslint-plugin-node": "^7.0.1", 80 | "eslint-plugin-prettier": "^2.6.2", 81 | "eslint-plugin-promise": "^4.0.0", 82 | "eslint-plugin-react": "^7.10.0", 83 | "eslint-plugin-standard": "^3.1.0", 84 | "eslint-watch": "^4.0.2", 85 | "gatsby-plugin-google-analytics": "^2.1.34", 86 | "gh-pages": "^2.2.0", 87 | "husky": "^4.3.0", 88 | "lint-staged": "^10.5.2", 89 | "prettier": "1.14.2", 90 | "prettier-eslint": "^8.8.2", 91 | "react": "^16.12.0", 92 | "react-dom": "^16.12.0", 93 | "react-resizable": "^1.7.5", 94 | "react-scripts": "^2.1.8", 95 | "react-test-renderer": "^16.5.2", 96 | "rollup": "^0.64.1", 97 | "rollup-plugin-alias": "^1.4.0", 98 | "rollup-plugin-auto-external": "^2.0.0", 99 | "rollup-plugin-babel": "^4.3.2", 100 | "rollup-plugin-commonjs": "^9.1.3", 101 | "rollup-plugin-copy": "^3.3.0", 102 | "rollup-plugin-livereload": "^2.0.0", 103 | "rollup-plugin-node-resolve": "^3.3.0", 104 | "rollup-plugin-postcss": "^1.6.2", 105 | "rollup-plugin-serve": "^1.1.0", 106 | "rollup-plugin-url": "^1.4.0", 107 | "styled-components": "^5.1.0" 108 | }, 109 | "files": [ 110 | "dist" 111 | ], 112 | "keywords": [ 113 | "react", 114 | "react-carousel", 115 | "carousel", 116 | "responsive", 117 | "reactjs", 118 | "carrousel", 119 | "slides", 120 | "flexibale", 121 | "rtl", 122 | "right-to-left", 123 | "resize", 124 | "touch" 125 | ], 126 | "resolutions": { 127 | "ansi-styles": "^3.2.0" 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /rollup.config.demo.js: -------------------------------------------------------------------------------- 1 | import babel from "rollup-plugin-babel"; 2 | import commonjs from "rollup-plugin-commonjs"; 3 | import postcss from "rollup-plugin-postcss"; 4 | import resolve from "rollup-plugin-node-resolve"; 5 | import url from "rollup-plugin-url"; 6 | import alias from "rollup-plugin-alias"; 7 | import serve from "rollup-plugin-serve"; 8 | import replace from "@rollup/plugin-replace"; 9 | import livereload from 'rollup-plugin-livereload' 10 | 11 | import libName from "./libName"; 12 | 13 | import * as ReactNamedExports from 'react'; 14 | import * as ReactIsNamedExports from 'react-is'; 15 | 16 | export default { 17 | input: `demoApp/src/index.js`, 18 | output: [ 19 | { 20 | file: "demoApp/dist/bundle.js", 21 | format: "cjs", 22 | sourcemap: true, 23 | exports: "named", 24 | }, 25 | ], 26 | plugins: [ 27 | alias({ 28 | "react-elastic-carousel": `src/${libName}/index.js`, 29 | }), 30 | //external(), 31 | postcss({ 32 | modules: false, 33 | }), 34 | url(), 35 | babel({ 36 | exclude: "node_modules/**", 37 | plugins: ["@babel/external-helpers"], 38 | }), 39 | resolve(), 40 | commonjs({ 41 | include: "node_modules/**", 42 | namedExports: { 43 | "node_modules/react-is/index.js": Object.keys(ReactIsNamedExports), 44 | "node_modules/react/index.js": Object.keys(ReactNamedExports), 45 | }, 46 | }), 47 | serve({ 48 | open: true, 49 | contentBase: "demoApp/dist", 50 | }), 51 | livereload(), 52 | replace({ 53 | "process.env.NODE_ENV": JSON.stringify("production"), 54 | }), 55 | ], 56 | }; 57 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import copy from "rollup-plugin-copy"; 4 | import external from "rollup-plugin-auto-external"; 5 | import postcss from 'rollup-plugin-postcss'; 6 | import resolve from 'rollup-plugin-node-resolve'; 7 | import url from 'rollup-plugin-url'; 8 | import alias from 'rollup-plugin-alias'; 9 | 10 | import pkg from './package.json' 11 | 12 | import libName from './libName'; 13 | 14 | export default { 15 | input: `src/${libName}/index.js`, 16 | output: [ 17 | { 18 | file: pkg.main, 19 | format: 'cjs', 20 | sourcemap: true, 21 | exports: 'named' 22 | }, 23 | { 24 | file: pkg.module, 25 | format: 'es', 26 | sourcemap: true 27 | } 28 | ], 29 | plugins: [ 30 | alias({ 31 | [libName]: `./src/${libName}/index.js` 32 | }), 33 | external(), 34 | postcss({ 35 | modules: false 36 | }), 37 | url(), 38 | babel({ 39 | exclude: 'node_modules/**', 40 | plugins: [ '@babel/external-helpers' ] 41 | }), 42 | resolve(), 43 | commonjs({ 44 | include: 'node_modules/**', 45 | namedExports: { 46 | 'node_modules/react-is/index.js': ['isValidElementType'] 47 | } 48 | }), 49 | copy({ 50 | targets: [{ src: `src/${libName}/index.d.ts`, dest: "dist" }], 51 | }) 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/docs/components/Button.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | export default styled.button` 3 | background: none; 4 | border: none; 5 | font-size: 2.5em; 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | cursor: pointer; 10 | user-select: none; 11 | &:disabled { 12 | cursor: not-allowed; 13 | } 14 | `; 15 | -------------------------------------------------------------------------------- /src/docs/components/Description.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Description = styled.div` 4 | padding-top: 10px; 5 | `; 6 | 7 | export default Description; 8 | -------------------------------------------------------------------------------- /src/docs/components/Flex.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Flex = styled.div` 4 | display: flex; 5 | flex-direction: ${({ direction }) => direction}; 6 | `; 7 | 8 | export default Flex; 9 | -------------------------------------------------------------------------------- /src/docs/components/Item.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Item = styled.div` 4 | transition: transform 200ms ease; 5 | box-sizing: border-box; 6 | width: 100%; 7 | max-width: 100%; 8 | padding: 15px; 9 | margin: 5px; 10 | border: 1px solid 11 | ${({ active }) => (active ? "rgba(0,0,0,.2)" : "rgba(0,0,0,.02)")}; 12 | color: #fff; 13 | background-color: #673ab7; 14 | &:hover { 15 | cursor: pointer; 16 | } 17 | `; 18 | 19 | export default Item; 20 | -------------------------------------------------------------------------------- /src/docs/components/ItemContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import Flex from "./Flex"; 4 | import Item from "./Item"; 5 | import Title from "./Title"; 6 | import Poster from "./Poster"; 7 | import Description from "./Description"; 8 | 9 | class ItemContainer extends Component { 10 | onClick = () => { 11 | const { onClick, id } = this.props; 12 | onClick && onClick(id); 13 | }; 14 | render() { 15 | const { title, img, description } = this.props; 16 | return ( 17 | 18 | 19 | {title} 20 | 21 | {img && } 22 | {description} 23 | 24 | 25 | 26 | ); 27 | } 28 | } 29 | 30 | ItemContainer.propTypes = { 31 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 32 | title: PropTypes.string, 33 | img: PropTypes.string, 34 | description: PropTypes.string, 35 | onClick: PropTypes.func 36 | }; 37 | 38 | export default ItemContainer; 39 | -------------------------------------------------------------------------------- /src/docs/components/ItemWithLink.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import Item from "./SimpleItem"; 4 | 5 | const ItemWithLink = ({ tabIndex, text }) => ( 6 | 7 | 8 | {text} 9 | 10 | 11 | ); 12 | 13 | ItemWithLink.propTypes = { 14 | text: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 15 | tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]) 16 | }; 17 | 18 | export default ItemWithLink; 19 | -------------------------------------------------------------------------------- /src/docs/components/Poster.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Poster = styled.div.attrs(({ src }) => { 4 | return { 5 | style: { 6 | backgroundImage: `url(${src})` 7 | } 8 | }; 9 | })` 10 | background-repeat: no-repeat; 11 | background-size: cover; 12 | background-position: center; 13 | height: 200px; 14 | `; 15 | 16 | export default Poster; 17 | -------------------------------------------------------------------------------- /src/docs/components/SimpleItem.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import Item from "./Item"; 3 | 4 | const SimpleItem = styled(Item)` 5 | font-family: Comic Sans MS, Comic Sans, cursive; 6 | font-weight: 100; 7 | display: flex; 8 | min-height: 100px; 9 | justify-content: center; 10 | align-items: center; 11 | font-size: 2.5em; 12 | max-width: ${({ maxWidth }) => maxWidth}; 13 | `; 14 | 15 | export default SimpleItem; 16 | -------------------------------------------------------------------------------- /src/docs/components/Square.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export default styled.div` 4 | cursor: pointer; 5 | transition: all 250ms ease-in; 6 | background-color: ${({ active }) => (active ? "#8bc34a" : "transparent")}; 7 | color: ${({ active }) => (active ? "#fff" : "333")}; 8 | transform: scale(${({ active }) => (!active ? 1.1 : 1)}); 9 | box-shadow: 0 0 2px 1px #555; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | width: 20px; 14 | height: 20px; 15 | margin: 10px 10px; 16 | `; 17 | -------------------------------------------------------------------------------- /src/docs/components/Title.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Title = styled.h3` 4 | margin: 0 0 5px 0; 5 | text-transform: uppercase; 6 | `; 7 | 8 | export default Title; 9 | -------------------------------------------------------------------------------- /src/docs/components/breakPoints.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { width: 1, itemsToShow: 1 }, 3 | { width: 550, itemsToShow: 2, itemsToScroll: 2 }, 4 | { width: 850, itemsToShow: 3 }, 5 | { width: 1150, itemsToShow: 4, itemsToScroll: 2 }, 6 | { width: 1450, itemsToShow: 5 }, 7 | { width: 1750, itemsToShow: 6 } 8 | ]; 9 | -------------------------------------------------------------------------------- /src/docs/components/itemsCollection.js: -------------------------------------------------------------------------------- 1 | import { numberToArray } from "../../react-elastic-carousel/utils/helpers"; 2 | export default (numOfItems = 12) => 3 | numberToArray(numOfItems).map(id => ({ 4 | id, 5 | title: `This is item #${id + 1}`, 6 | description: `this is the description for item ${id + 1}`, 7 | img: `https://picsum.photos/450/250?${id}` 8 | })); 9 | -------------------------------------------------------------------------------- /src/docs/mdx/autoTabIndexVisibleItems.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: autoTabIndexVisibleItems 3 | route: /autoTabIndexVisibleItems 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel from 'react-elastic-carousel'; 9 | import ItemWithLink from '../components/ItemWithLink'; 10 | 11 | # autoTabIndexVisibleItems 12 | Automatically inject `tabIndex:0` to visible items and `tabIndex:-1` to none visible items. 13 | 14 | **This prop defaults to `true`.** 15 | 16 | In some cases you might want users to directly focus nested elements inside the Item, 17 | so you can grab that `tabIndex` and pass it to whatever nested element you wish. 18 | 19 | In this example we pass the injected `tabIndex` to the `` element: 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ### link1 34 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. 35 | 36 | 37 | 38 | ### link2 39 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. 40 | 41 | 42 | 43 | ### link3 44 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. 45 | 46 | 47 | 48 | ### link4 49 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. 50 | 51 | 52 | 53 | ### link5 54 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. 55 | 56 | 57 | 58 | ### link6 59 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. -------------------------------------------------------------------------------- /src/docs/mdx/breakpoints.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: breakPoints 3 | route: /breakPoints 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel from 'react-elastic-carousel'; 9 | import Item from '../components/SimpleItem'; 10 | 11 | # breakPoints 12 | _This is the coolest feature and one of the main reasons that `react-elastic-carousel` was written._ 13 | 14 | The `breakPoints` array, allows you to set different `props` for different sizes of the element, regardless to the window's size. 15 | This is due to the fact that `react-elastic-carousel` is listening to the element's `resize` event instead of the window. 16 | 17 | #### You can pass an array of objects that will signal `react-elastic-carousel` to change props like `itemsToShow` and `itemsToScroll` (and others) on different `width` values. 18 | 19 | **ANY** prop can be passed directly to React Elastic Carousel or via a the breakpoint object. Keep in mind that a prop passed via a breakpoint will override a direct prop. 20 | 21 | ## Example 22 | (_Resize the container to see the changes..._) 23 | 24 | 25 | { 26 | class App extends React.Component{ 27 | constructor(props){ 28 | super(props); 29 | this.breakPoints = [ 30 | { width: 1, itemsToShow: 1 }, 31 | { width: 550, itemsToShow: 2, itemsToScroll: 2, pagination: false }, 32 | { width: 850, itemsToShow: 3 }, 33 | { width: 1150, itemsToShow: 4, itemsToScroll: 2 }, 34 | { width: 1450, itemsToShow: 5 }, 35 | { width: 1750, itemsToShow: 6 } 36 | ]; 37 | } 38 | render(){ 39 | return( 40 | 41 | 1 42 | 2 43 | 3 44 | 4 45 | 5 46 | 6 47 | 48 | ); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/docs/mdx/easing.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: easing 3 | route: /easing 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel from 'react-elastic-carousel'; 9 | import Item from '../components/SimpleItem'; 10 | 11 | # easing 12 | 13 | #### You can pass an easing pattern for the animations 14 | 15 | 16 | 21 | 1 22 | 2 23 | 3 24 | 4 25 | 5 26 | 6 27 | 28 | -------------------------------------------------------------------------------- /src/docs/mdx/enableAutoPlay.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: enableAutoPlay 3 | route: /enableAutoPlay 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel from 'react-elastic-carousel'; 9 | import Item from '../components/SimpleItem'; 10 | 11 | # enableAutoPlay 12 | #### Toggle `enableAutoPlay` and set `autoPlaySpeed` as you wish 13 | 14 | 15 | 16 | 1 17 | 2 18 | 3 19 | 4 20 | 5 21 | 6 22 | 23 | -------------------------------------------------------------------------------- /src/docs/mdx/focusOnSelect.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: focusOnSelect 3 | route: /focusOnSelect 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel from 'react-elastic-carousel'; 9 | import Item from '../components/SimpleItem'; 10 | 11 | # focusOnSelect 12 | #### Sliding to an item on click 13 | 14 | 15 | 19 | 1 20 | 2 21 | 3 22 | 4 23 | 5 24 | 6 25 | 26 | -------------------------------------------------------------------------------- /src/docs/mdx/goTo.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: goTo 3 | route: /goTo 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel from 'react-elastic-carousel'; 9 | import Item from '../components/SimpleItem'; 10 | 11 | # goTo 12 | #### You can trigger the `goTo` method via a `ref` to the `Carousel`: 13 | ``` 14 | this.carousel.goTo(someIndex) 15 | ``` 16 | 17 | 18 | { 19 | class App extends React.Component { 20 | constructor(props){ 21 | super(props); 22 | this.goto = this.goto.bind(this); 23 | } 24 | goto({target}){this.carousel.goTo(Number(target.value))} 25 | render() { 26 | return( 27 |
28 | 29 | 30 |
31 | this.carousel = ref}> 32 | 1 33 | 2 34 | 3 35 | 4 36 | 5 37 | 6 38 | 39 |
40 | ); 41 | } 42 | } 43 | } 44 | 45 |
-------------------------------------------------------------------------------- /src/docs/mdx/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Getting started 3 | route: / 4 | --- 5 | 6 | import {Playground, Props } from 'docz'; 7 | import Carousel from 'react-elastic-carousel'; 8 | import Item from '../components/SimpleItem'; 9 | 10 | # Getting started 11 | 12 | ## install 13 | 14 | ```bash 15 | npm install react-elastic-carousel 16 | ``` 17 | 18 | ## Import 19 | 20 | ```js 21 | import Carousel from 'react-elastic-carousel' 22 | ``` 23 | 24 | ## play 25 | 26 | 27 | 28 | 1 29 | 2 30 | 3 31 | 4 32 | 5 33 | 6 34 | 35 | 36 | 37 | ## Props 38 | 39 | 40 | 41 | ## Styling 42 | 43 | Almost every element in `react-elastic-carousel` has a css class with the `rec-` prefix (rec is short React Elastic Carousel). 44 | For example: `rec-arrow` for both arrow buttons or `rec-arrow-left` just for the left one. 45 | 46 | You can see a code example [here](/styling). -------------------------------------------------------------------------------- /src/docs/mdx/initialFirstItem.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: initialActiveIndex 3 | route: /initialActiveIndex 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel from 'react-elastic-carousel'; 9 | import Item from '../components/SimpleItem'; 10 | 11 | # initialActiveIndex 12 | 13 | #### The first active item on mount 14 | 15 | 16 | 17 | 1 18 | 2 19 | 3 20 | 4 21 | 5 22 | 6 23 | 24 | 25 | 26 | --- 27 | > If you are using the old prop `initialFirstItem`, then note that its deprecated and you should use `initialActiveIndex`. 28 | -------------------------------------------------------------------------------- /src/docs/mdx/isRTL.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: isRTL 3 | route: /isRTL 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel from 'react-elastic-carousel'; 9 | import Item from '../components/SimpleItem'; 10 | 11 | # isRTL 12 | #### Support for `right-to-left` layouts 13 | 14 | 15 | 16 | 1 17 | 2 18 | 3 19 | 4 20 | 5 21 | 6 22 | 23 | -------------------------------------------------------------------------------- /src/docs/mdx/itemPadding.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: itemPadding 3 | route: /itemPadding 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel from 'react-elastic-carousel'; 9 | import Item from '../components/SimpleItem'; 10 | 11 | # itemPadding 12 | #### Sets a `padding` to the wrapping element of each item 13 | 14 | 15 | 16 | 1 17 | 2 18 | 3 19 | 4 20 | 5 21 | 6 22 | 23 | -------------------------------------------------------------------------------- /src/docs/mdx/itemPosition.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: itemPosition 3 | route: /itemPosition 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel, {consts} from 'react-elastic-carousel'; 9 | import Item from '../components/SimpleItem'; 10 | 11 | # itemPosition 12 | #### Sets the position of the item within the item's wrapper. 13 | _You should import the `consts` object to pass the desired `itemPosition`_ 14 | 15 | ``` 16 | import Carousel, { consts } from 'react-elastic-carousel'; 17 | ``` 18 | 19 | 20 | 21 | 1 22 | 3 23 | 4 24 | 5 25 | 26 | -------------------------------------------------------------------------------- /src/docs/mdx/itemsToScroll.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: itemsToScroll 3 | route: /itemsToScroll 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel from 'react-elastic-carousel'; 9 | import Item from '../components/SimpleItem'; 10 | 11 | # itemsToScroll 12 | #### Number of items to scroll 13 | 14 | 15 | 16 | 1 17 | 2 18 | 3 19 | 4 20 | 5 21 | 6 22 | 23 | -------------------------------------------------------------------------------- /src/docs/mdx/itemsToShow.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: itemsToShow 3 | route: /itemsToShow 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel from 'react-elastic-carousel'; 9 | import Item from '../components/SimpleItem'; 10 | 11 | # itemsToShow 12 | #### Number of visible items 13 | 14 | 15 | 16 | 1 17 | 2 18 | 3 19 | 4 20 | 5 21 | 6 22 | 23 | -------------------------------------------------------------------------------- /src/docs/mdx/onChange.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: onChange 3 | route: /onChange 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel from 'react-elastic-carousel'; 9 | import Item from '../components/SimpleItem'; 10 | 11 | # onChange 12 | #### `onChange` will pass the new __current__ `item-object` and __current__ `page-index`: 13 | ``` 14 | (currentItem, pageIndex) => {/*...*/} 15 | ``` 16 | - The `item-object` has the shape of: 17 | ``` 18 | { 19 | item: {/* your item */}, 20 | index: // the item's index number 21 | } 22 | ``` 23 | 24 | 25 | 26 | alert(JSON.stringify({currentItem, pageIndex}))} 29 | > 30 | 1 31 | 2 32 | 3 33 | 4 34 | 5 35 | 6 36 | 37 | -------------------------------------------------------------------------------- /src/docs/mdx/onNextEnd.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: onNextEnd 3 | route: /onNextEnd 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel from 'react-elastic-carousel'; 9 | import Item from '../components/SimpleItem'; 10 | 11 | # onNextEnd 12 | #### `onNextEnd` will pass the new __current__ `item-object` and __current__ `page-index`: 13 | ``` 14 | (currentItem, pageIndex) => {/*...*/} 15 | ``` 16 | - The `item-object` has the shape of: 17 | ``` 18 | { 19 | item: {/* your item */}, 20 | index: // the item's index number 21 | } 22 | ``` 23 | 24 | 25 | 26 | alert(JSON.stringify({currentItem, pageIndex}))} 29 | > 30 | 1 31 | 2 32 | 3 33 | 4 34 | 5 35 | 6 36 | 37 | -------------------------------------------------------------------------------- /src/docs/mdx/onNextStart.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: onNextStart 3 | route: /onNextStart 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel from 'react-elastic-carousel'; 9 | import Item from '../components/SimpleItem'; 10 | 11 | # onNextStart 12 | #### `onNextStart` will pass both the __current__ and __next__ `item-object`: 13 | ``` 14 | (currentItem, nextItem) => {/*...*/} 15 | ``` 16 | - The `item-object` has the shape of: 17 | ``` 18 | { 19 | item: {/* your item */}, 20 | index: // the item's index number 21 | } 22 | ``` 23 | 24 | 25 | 26 | alert(JSON.stringify(currentItem) + `\n` + JSON.stringify(nextItem))} 29 | > 30 | 1 31 | 2 32 | 3 33 | 4 34 | 5 35 | 6 36 | 37 | -------------------------------------------------------------------------------- /src/docs/mdx/onPrevEnd.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: onPrevEnd 3 | route: /onPrevEnd 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel from 'react-elastic-carousel'; 9 | import Item from '../components/SimpleItem'; 10 | 11 | # onPrevEnd 12 | #### `onPrevEnd` will pass the new __current__ `item-object` and __current__ `page-index`: 13 | ``` 14 | (currentItem, pageIndex) => {/*...*/} 15 | ``` 16 | - The `item-object` has the shape of: 17 | ``` 18 | { 19 | item: {/* your item */}, 20 | index: // the item's index number 21 | } 22 | ``` 23 | 24 | 25 | 26 | alert(JSON.stringify({currentItem, pageIndex}))} 30 | > 31 | 1 32 | 2 33 | 3 34 | 4 35 | 5 36 | 6 37 | 38 | -------------------------------------------------------------------------------- /src/docs/mdx/onPrevStart.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: onPrevStart 3 | route: /onPrevStart 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel from 'react-elastic-carousel'; 9 | import Item from '../components/SimpleItem'; 10 | 11 | # onPrevStart 12 | #### `onPrevStart` will pass both the __current__ and __next__ `item-object`: 13 | ``` 14 | (currentItem, nextItem) => {/*...*/} 15 | ``` 16 | - The `item-object` has the shape of: 17 | ``` 18 | { 19 | item: {/* your item */}, 20 | index: // the item's index number 21 | } 22 | ``` 23 | 24 | 25 | 26 | alert(JSON.stringify(currentItem) + `\n` + JSON.stringify(nextItem))} 30 | > 31 | 1 32 | 2 33 | 3 34 | 4 35 | 5 36 | 6 37 | 38 | -------------------------------------------------------------------------------- /src/docs/mdx/onResize.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: onResize 3 | route: /onResize 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel from 'react-elastic-carousel'; 9 | import Item from '../components/SimpleItem'; 10 | import breakPoints from '../components/breakPoints'; 11 | 12 | # onResize 13 | #### You can listen to the `onResize` event of the `Carousel`, this will get invoked on each `resize` event of the root element and will return the current `breakPoint` object being used: 14 | 15 | ``` 16 | (currentBreakPoint) => {/*...*/} 17 | ``` 18 | 19 | 20 | console.log(currentBreakPoint)}> 21 | 1 22 | 2 23 | 3 24 | 4 25 | 5 26 | 6 27 | 28 | -------------------------------------------------------------------------------- /src/docs/mdx/outerSpacing.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: outerSpacing 3 | route: /outerSpacing 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel from 'react-elastic-carousel'; 9 | import Item from '../components/SimpleItem'; 10 | 11 | # outerSpacing 12 | #### Sets a margin to the beginning and the end of the slider 13 | #### (not compatible with `verticalMode` yet !) 14 | 15 | 16 | 17 | 1 18 | 2 19 | 3 20 | 4 21 | 5 22 | 6 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/docs/mdx/renderArrow.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: renderArrow 3 | route: /renderArrow 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel, {consts} from 'react-elastic-carousel'; 9 | import Button from '../components/Button'; 10 | import Item from '../components/SimpleItem'; 11 | 12 | # renderArrow 13 | 14 | ### You can provide custom Arrow buttons. 15 | 16 | ## Signature 17 | 18 | - You can pass a function that returns a react element, 19 | This function is a [render prop](https://reactjs.org/docs/render-props.html) and its signature is: 20 | ``` 21 | ({type, onClick, isEdge}) => {/* return your element here */} 22 | ``` 23 | 24 | - `type` will be either `consts.PREV` or `consts.NEXT`, depends on the relevant arrow direction. 25 | - `onClick` Is the hook to the inner `onClick` of the `Carousel`. You can attach it wherever you want within the returned component. 26 | - `isEdge` Will be true when there are no more items to show for this direction. 27 | 28 | _You should import the `consts` object to check the arrow direction_ 29 | ``` 30 | import Carousel, { consts } from 'react-elastic-carousel'; 31 | // .... 32 | 33 | type === consts.PREV 34 | ``` 35 | 36 | ## Demo 37 | 38 | 39 | { 40 | class App extends React.Component{ 41 | myArrow({type, onClick, isEdge}){ 42 | const pointer = type === consts.PREV ? '👈' : '👉'; 43 | return ( 44 | 47 | ); 48 | } 49 | render(){ 50 | return( 51 |
52 | 53 | 1 54 | 2 55 | 3 56 | 4 57 | 5 58 | 6 59 | 60 |
61 | ); 62 | } 63 | } 64 | } 65 |
-------------------------------------------------------------------------------- /src/docs/mdx/renderPagination.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: renderPagination 3 | route: /renderPagination 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel, {consts} from 'react-elastic-carousel'; 9 | import Square from '../components/Square'; 10 | import Flex from '../components/Flex'; 11 | import Item from '../components/SimpleItem'; 12 | 13 | # renderPagination 14 | 15 | ### You can provide custom pagination buttons. 16 | 17 | ## Signature 18 | 19 | - You can pass a function that returns a react element, 20 | This function is a [render prop](https://reactjs.org/docs/render-props.html) and its signature is: 21 | ``` 22 | ({ pages, activePage, onClick }) => {/* return your element here */} 23 | ``` 24 | 25 | - `pages` Array of page index. 26 | - `activePage` The current active page id. 27 | - `onClick` Is the hook to the inner `onClick` of the `Carousel`. You can attach it wherever you want within the returned component. 28 | 29 | ## Demo 30 | 31 | 32 | { 36 | return ( 37 | 38 | { 39 | pages.map((page) => { 40 | const isActivePage = activePage === page; 41 | return ( 42 | onClick(page)} active={isActivePage}/> 43 | ) 44 | })} 45 | 46 | ) 47 | }} 48 | > 49 | 1 50 | 2 51 | 3 52 | 4 53 | 5 54 | 6 55 | 7 56 | 8 57 | 9 58 | 10 59 | 60 | -------------------------------------------------------------------------------- /src/docs/mdx/showEmptySlots.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: showEmptySlots 3 | route: /showEmptySlots 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel from 'react-elastic-carousel'; 9 | import Item from '../components/SimpleItem'; 10 | 11 | # showEmptySlots 12 | #### Shows empty slots when there are less children than items to show (`children.length` < `itemsToShow`) 13 | #### (not compatible with `verticalMode` yet !) 14 | 15 | 16 | 17 | 1 18 | 2 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/docs/mdx/slideNext.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: slideNext / slidePrev 3 | route: /slideNext 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel from 'react-elastic-carousel'; 9 | import Item from '../components/SimpleItem'; 10 | 11 | # slideNext / slidePrev 12 | #### You can trigger the `slideNext` or `slidePrev` methods via a `ref` to the `Carousel`: 13 | 14 | ``` 15 | this.carousel.slideNext() 16 | this.carousel.slidePrev() 17 | ``` 18 | 19 | 20 | { 21 | class App extends React.Component{ 22 | render(){ 23 | return( 24 |
25 | 26 | 27 |
28 | this.carousel = ref}> 29 | 1 30 | 2 31 | 3 32 | 4 33 | 5 34 | 6 35 | 36 |
37 | ); 38 | } 39 | } 40 | } 41 | 42 |
-------------------------------------------------------------------------------- /src/docs/mdx/styling.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Styling 3 | route: /styling 4 | menu: Examples 5 | --- 6 | 7 | import {Playground, Props } from 'docz'; 8 | import Carousel from 'react-elastic-carousel'; 9 | import Item from '../components/SimpleItem'; 10 | import '../styles/styling.css'; 11 | 12 | # Styling 13 | #### Almost every element in `react-elastic-carousel` has a css class with the `rec-` prefix (rec is short React Elastic Carousel). 14 | 15 | ```css 16 | /* square buttons */ 17 | .rec.rec-arrow { 18 | border-radius: 0; 19 | } 20 | 21 | /* round buttons on hover */ 22 | .rec.rec-arrow:hover { 23 | border-radius: 50%; 24 | } 25 | 26 | /* hide disabled buttons */ 27 | .rec.rec-arrow:disabled { 28 | visibility: hidden; 29 | } 30 | 31 | /* disable default outline on focused items */ 32 | /* add custom outline on focused items */ 33 | .rec-carousel-item:focus { 34 | outline: none; 35 | box-shadow: inset 0 0 1px 1px lightgrey; 36 | } 37 | ``` 38 | 39 | 40 | 41 |
42 | 43 | 1 44 | 2 45 | 3 46 | 4 47 | 5 48 | 6 49 | 50 |
51 |
-------------------------------------------------------------------------------- /src/docs/mdx/verticalMode.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: verticalMode 3 | route: /verticalMode 4 | menu: Examples 5 | --- 6 | 7 | import {Playground } from 'docz'; 8 | import Carousel from 'react-elastic-carousel'; 9 | import Item from '../components/SimpleItem'; 10 | 11 | # verticalMode 12 | 13 | #### Display and animate the Carousel in a vertical layout 14 | 15 | 16 | 17 | 1 18 | 2 19 | 3 20 | 4 21 | 5 22 | 6 23 | 7 24 | 8 25 | 26 | -------------------------------------------------------------------------------- /src/docs/styles/styling.css: -------------------------------------------------------------------------------- 1 | /* square buttons */ 2 | .styling-example .rec.rec-arrow { 3 | border-radius: 0; 4 | } 5 | 6 | /* round buttons on hover */ 7 | .styling-example .rec.rec-arrow:hover { 8 | border-radius: 50%; 9 | } 10 | /* hide disabled buttons */ 11 | .styling-example .rec.rec-arrow:disabled { 12 | visibility: hidden; 13 | } 14 | /* disable default outline on focused items */ 15 | /* add custom outline on focused items */ 16 | .styling-example .rec-carousel-item:focus { 17 | outline: none; 18 | box-shadow: inset 0 0 1px 1px lightgrey; 19 | } 20 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/actions/consts.js: -------------------------------------------------------------------------------- 1 | export const NEXT_ITEM = "NEXT_ITEM"; 2 | export const PREV_ITEM = "PREV_ITEM"; 3 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/actions/itemsActions.js: -------------------------------------------------------------------------------- 1 | import { NEXT_ITEM, PREV_ITEM } from "./consts"; 2 | 3 | export const nextItemAction = (limit, itemsToScroll) => ({ 4 | type: NEXT_ITEM, 5 | limit, 6 | itemsToScroll 7 | }); 8 | 9 | export const prevItemAction = (limit, itemsToScroll) => ({ 10 | type: PREV_ITEM, 11 | limit, 12 | itemsToScroll 13 | }); 14 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/components/Arrow.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import cx from "classnames"; 4 | import { cssPrefix } from "../utils/helpers"; 5 | import { Button } from "./styled"; 6 | 7 | const directionIcons = { 8 | left: "❮", 9 | right: "❯", 10 | up: "❮", 11 | down: "❯" 12 | }; 13 | 14 | const arrowClassname = cssPrefix("arrow"); 15 | 16 | const rotateStyle = direction => { 17 | let rotate = {}; 18 | if (direction === Arrow.up || direction === Arrow.down) { 19 | rotate.transform = "rotate(90deg)"; 20 | } 21 | return rotate; 22 | }; 23 | const Arrow = ({ direction, onClick, icons, style, ...rest }) => { 24 | const arrows = { ...directionIcons, ...icons }; 25 | const styleObj = { 26 | ...rotateStyle(direction), 27 | ...style 28 | }; 29 | return ( 30 | 39 | ); 40 | }; 41 | 42 | Arrow.left = "left"; 43 | Arrow.right = "right"; 44 | Arrow.up = "up"; 45 | Arrow.down = "down"; 46 | 47 | Arrow.propTypes = { 48 | direction: PropTypes.oneOf(["left", "right", "up", "down"]).isRequired, 49 | icons: PropTypes.object, 50 | style: PropTypes.object, 51 | onClick: PropTypes.func 52 | }; 53 | 54 | export default Arrow; 55 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/components/Carousel.js: -------------------------------------------------------------------------------- 1 | import React, { Children } from "react"; 2 | import PropTypes from "prop-types"; 3 | import ResizeObserver from "resize-observer-polyfill"; 4 | import Only from "react-only-when"; 5 | import Track from "./Track"; 6 | import Arrow from "./Arrow"; 7 | import consts from "../consts"; 8 | import { activeIndexReducer } from "../reducers/items"; 9 | import { nextItemAction, prevItemAction } from "../actions/itemsActions"; 10 | import { 11 | SliderContainer, 12 | Slider, 13 | StyledCarousel, 14 | CarouselWrapper 15 | } from "./styled"; 16 | import { pipe, noop, cssPrefix, numberToArray } from "../utils/helpers"; 17 | import { Pagination } from "./Pagination"; 18 | 19 | class Carousel extends React.Component { 20 | isComponentMounted = false; 21 | state = { 22 | rootHeight: 0, 23 | childHeight: 0, 24 | sliderPosition: 0, 25 | swipedSliderPosition: 0, 26 | isSwiping: false, 27 | transitioning: false, 28 | transitionMs: this.props.transitionMs, 29 | activeIndex: this.props.initialActiveIndex || this.props.initialFirstItem, // support deprecated initialFirstItem 30 | pages: [], 31 | activePage: 0, 32 | sliderContainerWidth: 0 33 | }; 34 | 35 | componentDidMount() { 36 | this.isComponentMounted = true; 37 | this.initResizeObserver(); 38 | this.updateActivePage(); 39 | this.setPages(); 40 | } 41 | 42 | componentDidUpdate(prevProps, prevState) { 43 | const { 44 | enableAutoPlay, 45 | children, 46 | itemsToShow, 47 | itemsToScroll, 48 | breakPoints 49 | } = this.props; 50 | const { activeIndex, sliderContainerWidth } = this.state; 51 | const nextItem = this.getNextItemIndex(activeIndex, false); 52 | const currentChildrenLength = Children.toArray(children).length; 53 | const prevChildrenLength = Children.toArray(prevProps.children).length; 54 | // update pages (for pagination) 55 | if ( 56 | prevChildrenLength !== currentChildrenLength || 57 | prevProps.itemsToShow !== itemsToShow || 58 | prevProps.itemsToScroll !== itemsToScroll || 59 | prevProps.breakPoints !== breakPoints || 60 | sliderContainerWidth !== prevState.sliderContainerWidth 61 | ) { 62 | // we mimic a container resize to recalculate item width when itemsToShow are updated 63 | this.onContainerResize({ contentRect: { width: sliderContainerWidth } }); 64 | this.setPages(); 65 | this.updateActivePage(); 66 | } 67 | 68 | // autoplay update 69 | if (activeIndex === nextItem) { 70 | this.removeAutoPlay(); 71 | } else if (enableAutoPlay && !this.autoPlayIntervalId) { 72 | this.setAutoPlay(); 73 | } else if (!enableAutoPlay && this.autoPlayIntervalId) { 74 | this.removeAutoPlay(); 75 | } 76 | 77 | if (prevChildrenLength !== currentChildrenLength) { 78 | const { 79 | itemsToShow: calculatedItemsToShow 80 | } = this.getDerivedPropsFromBreakPoint(); 81 | // number of items is reduced (we don't care if number of items is increased) 82 | // we need to check if our current index is not out of boundaries 83 | // we need to include itemsToShow so we can fill up the slots 84 | const lastIndex = currentChildrenLength - 1; 85 | const isOutOfRange = activeIndex + calculatedItemsToShow > lastIndex; 86 | if (isOutOfRange) { 87 | // we are out of boundaries, go "back" to last item of the list (respect itemsToShow) 88 | this.goTo(Math.max(0, currentChildrenLength - calculatedItemsToShow)); 89 | } 90 | } 91 | } 92 | 93 | componentWillUnmount() { 94 | this.isComponentMounted = false; 95 | this.removeAutoPlay(); 96 | this.unSubscribeObserver(); 97 | } 98 | 99 | setRef = name => ref => (this[name] = ref); 100 | 101 | initResizeObserver = () => { 102 | this.ro = new ResizeObserver((entries, observer) => { 103 | for (const entry of entries) { 104 | if (entry.target === this.sliderContainer) { 105 | // we are using rAF because it fixes the infinite refresh with gatsby (ssr?). 106 | // TBH, I'm not sure i fully understand why. 107 | // see https://github.com/sag1v/react-elastic-carousel/issues/107 108 | window.requestAnimationFrame(() => { 109 | this.onContainerResize(entry); 110 | }); 111 | } 112 | if (entry.target === this.slider) { 113 | // we are using rAF because it fixes the infinite refresh with gatsby (ssr?). 114 | // TBH, I'm not sure i fully understand why 115 | // see https://github.com/sag1v/react-elastic-carousel/issues/107 116 | window.requestAnimationFrame(() => { 117 | this.onSliderResize(entry); 118 | }); 119 | } 120 | } 121 | }); 122 | 123 | this.ro.observe(this.sliderContainer); 124 | this.ro.observe(this.slider); 125 | }; 126 | 127 | unSubscribeObserver = () => this.ro.disconnect(); 128 | 129 | setAutoPlay = () => { 130 | const { autoPlaySpeed } = this.getDerivedPropsFromBreakPoint(); 131 | this.autoPlayIntervalId = setInterval(() => { 132 | if (this.isComponentMounted) { 133 | const { transitioning } = this.state; 134 | if (!transitioning) { 135 | this.slideNext(); 136 | } 137 | } 138 | }, autoPlaySpeed); 139 | }; 140 | 141 | removeAutoPlay = () => { 142 | if (this.autoPlayIntervalId) { 143 | clearInterval(this.autoPlayIntervalId); 144 | this.autoPlayIntervalId = null; 145 | } 146 | }; 147 | 148 | setPages = () => { 149 | const numOfPages = this.getNumOfPages(); 150 | const pages = numberToArray(numOfPages); 151 | this.setState({ pages }); 152 | }; 153 | 154 | onSliderTransitionEnd = fn => { 155 | this.slider.addEventListener("transitionend", fn); 156 | }; 157 | 158 | removeSliderTransitionHook = fn => { 159 | this.slider.removeEventListener("transitionend", fn); 160 | }; 161 | 162 | getDerivedPropsFromBreakPoint = () => { 163 | const { breakPoints, ...restOfProps } = this.props; 164 | const { sliderContainerWidth } = this.state; 165 | 166 | // default breakpoint from individual props 167 | let currentBreakPoint; 168 | // if breakpoints were added as props override the individual props 169 | if (breakPoints && breakPoints.length > 0) { 170 | currentBreakPoint = breakPoints 171 | .slice() // no mutations 172 | .reverse() // so we can find last match 173 | .find(bp => bp.width <= sliderContainerWidth); 174 | if (!currentBreakPoint) { 175 | /* in case we don't have a lower width than sliderContainerWidth 176 | * this mostly happens in initilization when sliderContainerWidth is 0 177 | */ 178 | currentBreakPoint = breakPoints[0]; 179 | } 180 | } 181 | // merge direct props with current breakpoint Props 182 | return { ...restOfProps, ...currentBreakPoint }; 183 | }; 184 | 185 | updateSliderPosition = () => { 186 | this.setState(state => { 187 | const { 188 | children, 189 | verticalMode, 190 | itemsToShow, 191 | transitionMs 192 | } = this.getDerivedPropsFromBreakPoint(); 193 | const { childHeight, activeIndex } = state; 194 | 195 | const childWidth = this.calculateChildWidth(); 196 | const totalItems = Children.toArray(children).length; 197 | const hiddenSlots = totalItems - itemsToShow; 198 | let moveBy = activeIndex * -1; 199 | const emptySlots = itemsToShow - (totalItems - activeIndex); 200 | if (emptySlots > 0 && hiddenSlots > 0) { 201 | moveBy = emptySlots + activeIndex * -1; 202 | } 203 | let sliderPosition = (verticalMode ? childHeight : childWidth) * moveBy; 204 | const newActiveIndex = 205 | emptySlots > 0 ? activeIndex - emptySlots : activeIndex; 206 | // go back from 0ms to whatever set by the user 207 | // We were at 0ms because we wanted to disable animation on resize 208 | // see https://github.com/sag1v/react-elastic-carousel/issues/94 209 | window.requestAnimationFrame(() => { 210 | if (this.isComponentMounted) { 211 | this.setState({ transitionMs }); 212 | } 213 | }); 214 | return { 215 | sliderPosition, 216 | activeIndex: newActiveIndex < 0 ? 0 : newActiveIndex 217 | }; 218 | }); 219 | }; 220 | 221 | onSliderResize = sliderNode => { 222 | if (!this.isComponentMounted) { 223 | return; 224 | } 225 | 226 | const { 227 | verticalMode, 228 | children, 229 | itemsToShow 230 | } = this.getDerivedPropsFromBreakPoint(); 231 | const { height: sliderHeight } = sliderNode.contentRect; 232 | const nextState = {}; 233 | const childrenLength = Children.toArray(children).length; 234 | if (verticalMode) { 235 | const childHeight = sliderHeight / childrenLength; 236 | // We use Math.min because we don't want to make the child smaller 237 | // if the number of children is smaller than itemsToShow. 238 | // (Because we do not want "empty slots") 239 | nextState.rootHeight = 240 | childHeight * Math.min(childrenLength, itemsToShow); 241 | nextState.childHeight = childHeight; 242 | } else { 243 | nextState.rootHeight = sliderHeight; 244 | } 245 | this.setState(nextState); 246 | }; 247 | 248 | calculateChildWidth = () => { 249 | const { sliderContainerWidth } = this.state; 250 | const { 251 | verticalMode, 252 | itemsToShow, 253 | showEmptySlots, 254 | children 255 | } = this.getDerivedPropsFromBreakPoint(); 256 | 257 | /* based on slider container's width, get num of items to show 258 | * and calculate child's width (and update it in state) 259 | */ 260 | const childrenLength = Children.toArray(children).length || 1; 261 | 262 | let childWidth = 0; 263 | if (verticalMode) { 264 | childWidth = sliderContainerWidth; 265 | } else { 266 | // When "showEmptySlots" is false 267 | // We use Math.min because we don't want to make the child smaller 268 | // if the number of children is smaller than itemsToShow. 269 | // (Because we do not want "empty slots") 270 | childWidth = 271 | sliderContainerWidth / 272 | (showEmptySlots ? itemsToShow : Math.min(childrenLength, itemsToShow)); 273 | } 274 | return childWidth; 275 | }; 276 | 277 | onContainerResize = sliderContainerNode => { 278 | const { width: newSliderContainerWidth } = sliderContainerNode.contentRect; 279 | // update slider container width 280 | // disable animation on resize see https://github.com/sag1v/react-elastic-carousel/issues/94 281 | const { 282 | outerSpacing, 283 | verticalMode: initialVerticalMode 284 | } = this.getDerivedPropsFromBreakPoint(); 285 | const containerWidth = 286 | newSliderContainerWidth - (initialVerticalMode ? 0 : outerSpacing * 2); 287 | 288 | if ( 289 | !this.isComponentMounted || 290 | this.state.sliderContainerWidth === newSliderContainerWidth 291 | ) { 292 | // prevent infinite loop 293 | return; 294 | } 295 | this.setState( 296 | { sliderContainerWidth: containerWidth, transitionMs: 0 }, 297 | () => { 298 | // we must get these props inside setState (get future props because its async) 299 | const { 300 | onResize, 301 | itemsToShow, 302 | children 303 | } = this.getDerivedPropsFromBreakPoint(); 304 | 305 | const childrenLength = Children.toArray(children).length || 1; 306 | 307 | this.setState( 308 | currentState => { 309 | // We might need to change the selected index when the size of the container changes 310 | // we are making sure the selected index is not out of boundaries and respecting itemsToShow 311 | // This usually happens with breakpoints. see https://github.com/sag1v/react-elastic-carousel/issues/122 312 | let activeIndex = currentState.activeIndex; 313 | // we take the lowest, in case itemsToShow is greater than childrenLength 314 | const maxItemsToShow = Math.min(childrenLength, itemsToShow); 315 | const endLimit = childrenLength - maxItemsToShow; 316 | if (activeIndex > endLimit) { 317 | activeIndex = endLimit; 318 | } 319 | 320 | return { activeIndex }; 321 | }, 322 | () => { 323 | /* Based on all of the above new data: 324 | * update slider position 325 | * get the new current breakpoint 326 | * pass the current breakpoint to the consumer's callback 327 | */ 328 | this.updateSliderPosition(); 329 | const currentBreakPoint = this.getDerivedPropsFromBreakPoint(); 330 | onResize(currentBreakPoint); 331 | } 332 | ); 333 | } 334 | ); 335 | }; 336 | 337 | tiltMovement = (position, distance = 20, duration = 150) => { 338 | this.setState(state => { 339 | return { 340 | isSwiping: true, 341 | swipedSliderPosition: position - distance 342 | }; 343 | }); 344 | setTimeout(() => { 345 | this.setState({ 346 | isSwiping: false, 347 | swipedSliderPosition: 0 348 | }); 349 | }, duration); 350 | }; 351 | 352 | convertChildToCbObj = index => { 353 | const { children } = this.getDerivedPropsFromBreakPoint(); 354 | // support decimal itemsToShow 355 | const roundedIdx = Math.round(index); 356 | const child = Children.toArray(children)[roundedIdx]; 357 | return { item: child.props, index: roundedIdx }; 358 | }; 359 | 360 | getNextItemIndex = (currentIndex, getPrev) => { 361 | const { 362 | children, 363 | itemsToShow, 364 | itemsToScroll 365 | } = this.getDerivedPropsFromBreakPoint(); 366 | const childrenLength = Children.toArray(children).length || 1; 367 | const notEnoughItemsToShow = itemsToShow > childrenLength; 368 | let limit = getPrev ? 0 : childrenLength - itemsToShow; 369 | 370 | if (notEnoughItemsToShow) { 371 | limit = 0; // basically don't move 372 | } 373 | const nextAction = getPrev 374 | ? prevItemAction(0, itemsToScroll) 375 | : nextItemAction(limit, itemsToScroll); 376 | const nextItem = activeIndexReducer(currentIndex, nextAction); 377 | return nextItem; 378 | }; 379 | 380 | getNextItemObj = getPrev => { 381 | const { children } = this.getDerivedPropsFromBreakPoint(); 382 | const { activeIndex } = this.state; 383 | const nextItemIndex = this.getNextItemIndex(activeIndex, getPrev); 384 | // support decimal itemsToShow 385 | const roundedIdx = Math.round(nextItemIndex); 386 | const asElement = Children.toArray(children)[roundedIdx]; 387 | const asObj = { item: asElement.props, index: roundedIdx }; 388 | return asObj; 389 | }; 390 | 391 | resetSwipe = () => { 392 | this.setState({ 393 | swipedSliderPosition: 0, 394 | transitioning: false, 395 | isSwiping: false 396 | }); 397 | }; 398 | 399 | onSwiping = data => { 400 | const { deltaX, absX, deltaY, absY, dir } = data; 401 | 402 | this.setState(state => { 403 | const { childHeight, activeIndex, sliderPosition } = state; 404 | const { 405 | itemsToShow, 406 | verticalMode, 407 | children, 408 | isRTL 409 | } = this.getDerivedPropsFromBreakPoint(); 410 | 411 | const childWidth = this.calculateChildWidth(); 412 | 413 | // determine how far can user swipe 414 | const childrenLength = Children.toArray(children).length || 1; 415 | const goingNext = 416 | (!verticalMode && dir === "Left" && !isRTL) || 417 | (!verticalMode && dir === "Right" && isRTL) || 418 | (verticalMode && dir === "Up"); 419 | const goingBack = 420 | (!verticalMode && dir === "Right" && !isRTL) || 421 | (!verticalMode && dir === "Left" && isRTL) || 422 | (verticalMode && dir === "Down"); 423 | 424 | const horizontalSwipe = dir === "Left" || dir === "Right"; 425 | const verticalSwipe = dir === "Up" || dir === "Down"; 426 | const horizontalMode = !verticalMode; 427 | 428 | let distanceSwipe = 0; 429 | const horizontalEdgeStoppage = childWidth / 2; 430 | const verticalEdgeStoppage = childHeight / 2; 431 | 432 | if (verticalMode) { 433 | if (verticalSwipe) { 434 | const trackSize = childrenLength * childHeight; 435 | if (goingNext) { 436 | distanceSwipe = 437 | trackSize - 438 | childHeight * activeIndex - 439 | itemsToShow * childHeight + 440 | verticalEdgeStoppage; 441 | } else if (goingBack) { 442 | distanceSwipe = childHeight * activeIndex + verticalEdgeStoppage; 443 | } 444 | } 445 | } else { 446 | if (horizontalSwipe) { 447 | const trackSize = childrenLength * childWidth; 448 | if (goingNext) { 449 | distanceSwipe = 450 | trackSize - 451 | childWidth * activeIndex - 452 | itemsToShow * childWidth + 453 | horizontalEdgeStoppage; 454 | } else if (goingBack) { 455 | distanceSwipe = childWidth * activeIndex + horizontalEdgeStoppage; 456 | } 457 | } 458 | } 459 | 460 | const shouldHorizontalSkipUpdate = 461 | (horizontalMode && verticalSwipe) || 462 | (horizontalMode && horizontalSwipe && absX > distanceSwipe); 463 | 464 | const shouldVerticalSkipUpdate = 465 | (verticalMode && horizontalSwipe) || 466 | (verticalMode && verticalSwipe && absY > distanceSwipe); 467 | 468 | if (shouldHorizontalSkipUpdate || shouldVerticalSkipUpdate) { 469 | // bail out of state update 470 | return; 471 | } 472 | let swipedSliderPosition; 473 | if (horizontalSwipe) { 474 | if (isRTL) { 475 | swipedSliderPosition = sliderPosition + deltaX; 476 | } else { 477 | swipedSliderPosition = sliderPosition - deltaX; 478 | } 479 | } else { 480 | swipedSliderPosition = sliderPosition - deltaY; 481 | } 482 | return { 483 | swipedSliderPosition, 484 | isSwiping: true, 485 | transitioning: true 486 | }; 487 | }); 488 | }; 489 | 490 | onSwiped = data => { 491 | // we need to handle all scenarios: 492 | // 1. Horizontal mode - swipe left or right 493 | // 2. Horizontal mode with RTL - swipe left or right 494 | // 3. vertical mode - swipe up or down 495 | 496 | const { absX, absY, dir } = data; 497 | const { childHeight, activeIndex } = this.state; 498 | const { 499 | verticalMode, 500 | isRTL, 501 | itemsToScroll 502 | } = this.getDerivedPropsFromBreakPoint(); 503 | const childWidth = this.calculateChildWidth(); 504 | 505 | let func = this.resetSwipe; 506 | const minSwipeDistanceHorizontal = childWidth / 5; 507 | const minSwipeDistanceVertical = childHeight / 5; 508 | const swipedLeft = dir === "Left"; 509 | const swipedRight = dir === "Right"; 510 | const swipedUp = dir === "Up"; 511 | const swipedDown = dir === "Down"; 512 | const verticalGoSwipe = 513 | verticalMode && 514 | (swipedUp || swipedDown) && 515 | absY > minSwipeDistanceVertical; 516 | 517 | const horizontalGoSwipe = 518 | !verticalMode && 519 | (swipedRight || swipedLeft) && 520 | absX > minSwipeDistanceHorizontal; 521 | 522 | let goodToGo = false; 523 | if (verticalGoSwipe || horizontalGoSwipe) { 524 | goodToGo = true; 525 | } 526 | 527 | if (goodToGo) { 528 | // we should go to a different item 529 | // determine what method we need to invoke 530 | 531 | if (verticalMode) { 532 | // get number of slides from user's swiping 533 | const numberOfSlidesViaSwipe = Math.ceil( 534 | (absY - minSwipeDistanceVertical) / childHeight 535 | ); 536 | // if user swipes more than itemsToScroll then we want to bypass itemsToScroll for a smoother scroll 537 | const numberOfSlidesTogo = Math.max( 538 | itemsToScroll, 539 | numberOfSlidesViaSwipe 540 | ); 541 | 542 | const backSlidesToGo = activeIndex - numberOfSlidesTogo; 543 | const forwardSlideTtoGo = activeIndex + numberOfSlidesTogo; 544 | 545 | // up or down 546 | if (swipedDown) { 547 | // func = this.onPrevStart; 548 | func = () => this.goTo(backSlidesToGo); 549 | } 550 | if (swipedUp) { 551 | // func = this.onNextStart; 552 | func = () => this.goTo(forwardSlideTtoGo); 553 | } 554 | } else { 555 | // get number of slides from user's swiping 556 | const numberOfSlidesViaSwipe = Math.ceil( 557 | (absX - minSwipeDistanceHorizontal) / childWidth 558 | ); 559 | // if user swipes more than itemsToScroll then we want to bypass itemsToScroll for a smoother scroll 560 | const numberOfSlidesTogo = Math.max( 561 | itemsToScroll, 562 | numberOfSlidesViaSwipe 563 | ); 564 | 565 | const backSlidesToGo = activeIndex - numberOfSlidesTogo; 566 | const forwardSlideTtoGo = activeIndex + numberOfSlidesTogo; 567 | 568 | // horizontal mode 569 | if (isRTL) { 570 | // flip sides 571 | if (swipedLeft) { 572 | // func = this.onPrevStart; 573 | func = () => this.goTo(backSlidesToGo); 574 | } 575 | if (swipedRight) { 576 | // func = this.onNextStart; 577 | func = () => this.goTo(forwardSlideTtoGo); 578 | } 579 | } else { 580 | // normal behavior 581 | if (swipedLeft) { 582 | // func = this.onNextStart; 583 | func = () => this.goTo(forwardSlideTtoGo); 584 | } 585 | if (swipedRight) { 586 | // func = this.onPrevStart; 587 | func = () => this.goTo(backSlidesToGo); 588 | } 589 | } 590 | } 591 | } 592 | // we are not "tilting" on edges, so we need to reset isSwiping and transitioning. 593 | // otherwise we wont slide back to edge 594 | this.setState({ isSwiping: false, transitioning: false }); 595 | func({ skipTilt: true }); 596 | }; 597 | 598 | onNextStart = options => { 599 | const { onNextStart } = this.getDerivedPropsFromBreakPoint(); 600 | const { activeIndex } = this.state; 601 | const nextItemObj = this.getNextItemObj(); 602 | const prevItemObj = this.convertChildToCbObj(activeIndex); 603 | onNextStart(prevItemObj, nextItemObj); 604 | this.slideNext(options); 605 | }; 606 | 607 | onPrevStart = options => { 608 | const { onPrevStart } = this.getDerivedPropsFromBreakPoint(); 609 | const { activeIndex } = this.state; 610 | const nextItemObj = this.getNextItemObj(true); 611 | const prevItemObj = this.convertChildToCbObj(activeIndex); 612 | onPrevStart(prevItemObj, nextItemObj); 613 | this.slidePrev(options); 614 | }; 615 | 616 | slideNext = (options = {}) => { 617 | const { skipTilt } = options; 618 | const { enableTilt } = this.getDerivedPropsFromBreakPoint(); 619 | const { activeIndex, sliderPosition } = this.state; 620 | const nextItem = this.getNextItemIndex(activeIndex, false); 621 | if (activeIndex !== nextItem) { 622 | this.goTo(nextItem); 623 | } else if (enableTilt && !skipTilt) { 624 | this.tiltMovement(sliderPosition, 20, 150); 625 | } 626 | }; 627 | 628 | slidePrev = (options = {}) => { 629 | const { skipTilt } = options; 630 | const { activeIndex } = this.state; 631 | const { enableTilt } = this.getDerivedPropsFromBreakPoint(); 632 | const prevItem = this.getNextItemIndex(activeIndex, true); 633 | if (activeIndex !== prevItem) { 634 | this.goTo(prevItem); 635 | } else if (enableTilt && !skipTilt) { 636 | this.tiltMovement(0, -20, 150); 637 | } 638 | }; 639 | 640 | onNextEnd = () => { 641 | const { onNextEnd, onChange } = this.getDerivedPropsFromBreakPoint(); 642 | const { activeIndex, activePage } = this.state; 643 | const nextItemObj = this.convertChildToCbObj(activeIndex); 644 | this.removeSliderTransitionHook(this.onNextEnd); 645 | this.setState({ transitioning: false }); 646 | onChange && onChange(nextItemObj, activePage); 647 | onNextEnd(nextItemObj, activePage); 648 | }; 649 | 650 | onPrevEnd = () => { 651 | const { onPrevEnd, onChange } = this.getDerivedPropsFromBreakPoint(); 652 | const { activeIndex, activePage } = this.state; 653 | const nextItemObj = this.convertChildToCbObj(activeIndex); 654 | this.removeSliderTransitionHook(this.onPrevEnd); 655 | this.setState({ transitioning: false }); 656 | onChange && onChange(nextItemObj, activePage); 657 | onPrevEnd(nextItemObj, activePage); 658 | }; 659 | 660 | generatePositionUpdater = ( 661 | direction, 662 | nextItemId, 663 | verticalMode, 664 | rest 665 | ) => state => { 666 | const { sliderPosition, childHeight, activeIndex } = state; 667 | const childWidth = this.calculateChildWidth(); 668 | 669 | let newSliderPosition = 0; 670 | const childSize = verticalMode ? childHeight : childWidth; 671 | if (direction === consts.NEXT) { 672 | newSliderPosition = 673 | sliderPosition - childSize * (nextItemId - activeIndex); 674 | } else { 675 | newSliderPosition = 676 | sliderPosition + childSize * (activeIndex - nextItemId); 677 | } 678 | 679 | return { 680 | sliderPosition: newSliderPosition, 681 | activeIndex: nextItemId, 682 | swipedSliderPosition: 0, 683 | isSwiping: false, 684 | ...rest 685 | }; 686 | }; 687 | 688 | goTo = nextItemId => { 689 | const { 690 | children, 691 | verticalMode, 692 | itemsToShow 693 | } = this.getDerivedPropsFromBreakPoint(); 694 | const { activeIndex } = this.state; 695 | const childrenLength = Children.toArray(children).length; 696 | let safeNextItemId = Math.max(0, nextItemId); // don't allow negative numbers 697 | const isPrev = activeIndex > safeNextItemId; 698 | const nextAvailableItem = this.getNextItemIndex(activeIndex, isPrev); 699 | const noChange = nextAvailableItem === activeIndex; 700 | const outOfBoundary = safeNextItemId + itemsToShow >= childrenLength; 701 | if (noChange) { 702 | return; 703 | } 704 | if (outOfBoundary) { 705 | // Either go to last index (respect itemsToShow) or 0 index if we can't fill the slider 706 | safeNextItemId = Math.max(0, childrenLength - itemsToShow); 707 | } 708 | let direction = consts.NEXT; 709 | let positionEndCb = this.onNextEnd; 710 | if (isPrev) { 711 | direction = consts.PREV; 712 | positionEndCb = this.onPrevEnd; 713 | } 714 | const stateUpdater = this.generatePositionUpdater( 715 | direction, 716 | safeNextItemId, 717 | verticalMode, 718 | { 719 | transitioning: true 720 | } 721 | ); 722 | this.setState(stateUpdater, () => { 723 | // callback 724 | pipe( 725 | this.updateActivePage(), 726 | this.onSliderTransitionEnd(positionEndCb) 727 | ); 728 | }); 729 | }; 730 | 731 | getNumOfPages = () => { 732 | const { children, itemsToShow } = this.getDerivedPropsFromBreakPoint(); 733 | const childrenLength = Children.toArray(children).length; 734 | const safeItemsToShow = Math.max(itemsToShow, 1); 735 | const numOfPages = Math.ceil(childrenLength / safeItemsToShow); 736 | return numOfPages || 1; 737 | }; 738 | 739 | updateActivePage = () => { 740 | this.setState(state => { 741 | const { itemsToShow, children } = this.getDerivedPropsFromBreakPoint(); 742 | const { activeIndex, activePage } = state; 743 | const numOfPages = this.getNumOfPages(); 744 | const childrenLength = Children.toArray(children).length; 745 | const inRangeItemsToShow = Math.min(childrenLength, itemsToShow); 746 | // watch out from 0 (so we wont divide by zero) 747 | const safeItemsToShow = Math.max(inRangeItemsToShow, 1); 748 | const newActivePage = Math.ceil(activeIndex / safeItemsToShow); 749 | const inRangeActivePageIndex = Math.min(numOfPages - 1, newActivePage); 750 | if (activePage !== inRangeActivePageIndex) { 751 | return { activePage: inRangeActivePageIndex }; 752 | } 753 | }); 754 | }; 755 | 756 | onIndicatorClick = indicatorId => { 757 | const { itemsToShow } = this.getDerivedPropsFromBreakPoint(); 758 | const gotoIndex = indicatorId * itemsToShow; 759 | this.setState({ activePage: indicatorId }); 760 | this.goTo(gotoIndex); 761 | }; 762 | 763 | render() { 764 | const { 765 | activePage, 766 | isSwiping, 767 | sliderPosition, 768 | swipedSliderPosition, 769 | rootHeight, 770 | pages, 771 | activeIndex, 772 | transitionMs 773 | } = this.state; 774 | const { 775 | className, 776 | style, 777 | itemsToShow, 778 | itemsToScroll, 779 | verticalMode, 780 | isRTL, 781 | easing, 782 | tiltEasing, 783 | children, 784 | focusOnSelect, 785 | autoTabIndexVisibleItems, 786 | itemPosition, 787 | itemPadding, 788 | outerSpacing, 789 | enableSwipe, 790 | enableMouseSwipe, 791 | pagination, 792 | showArrows, 793 | disableArrowsOnEnd, 794 | preventDefaultTouchmoveEvent, 795 | renderArrow, 796 | renderPagination 797 | } = this.getDerivedPropsFromBreakPoint(); 798 | 799 | const childWidth = this.calculateChildWidth(); 800 | 801 | const numOfPages = this.getNumOfPages(); 802 | 803 | /** Determine if arrows should be disabled */ 804 | const canSlidePrev = 805 | activeIndex !== this.getNextItemIndex(activeIndex, true); 806 | const canSlideNext = 807 | activeIndex !== this.getNextItemIndex(activeIndex, false); 808 | const disabledPrevArrow = !canSlidePrev && disableArrowsOnEnd; 809 | const disabledNextArrow = !canSlideNext && disableArrowsOnEnd; 810 | 811 | return ( 812 | 817 | 821 | 822 | {renderArrow ? ( 823 | renderArrow({ 824 | type: consts.PREV, 825 | onClick: this.onPrevStart, 826 | isEdge: !canSlidePrev 827 | }) 828 | ) : ( 829 | 834 | )} 835 | 836 | 840 | 853 | 870 | 871 | 872 | 873 | {renderArrow ? ( 874 | renderArrow({ 875 | type: consts.NEXT, 876 | onClick: this.onNextStart, 877 | isEdge: !canSlideNext 878 | }) 879 | ) : ( 880 | 885 | )} 886 | 887 | 888 | 889 | {renderPagination ? ( 890 | renderPagination({ 891 | pages: pages, 892 | activePage, 893 | onClick: this.onIndicatorClick 894 | }) 895 | ) : ( 896 | 901 | )} 902 | 903 | 904 | ); 905 | } 906 | } 907 | 908 | Carousel.defaultProps = { 909 | className: "", 910 | style: {}, 911 | verticalMode: false, 912 | isRTL: false, 913 | initialFirstItem: 0, 914 | initialActiveIndex: 0, 915 | showArrows: true, 916 | showEmptySlots: false, 917 | disableArrowsOnEnd: true, 918 | pagination: true, 919 | easing: "ease", 920 | tiltEasing: "ease", 921 | transitionMs: 500, 922 | enableTilt: true, 923 | enableSwipe: true, 924 | enableMouseSwipe: true, 925 | preventDefaultTouchmoveEvent: false, 926 | focusOnSelect: false, 927 | autoTabIndexVisibleItems: true, 928 | itemsToShow: 1, 929 | itemsToScroll: 1, 930 | itemPosition: consts.CENTER, 931 | itemPadding: [0, 0, 0, 0], 932 | outerSpacing: 0, 933 | enableAutoPlay: false, 934 | autoPlaySpeed: 2000, 935 | 936 | // callbacks 937 | onChange: noop, 938 | onNextEnd: noop, 939 | onPrevEnd: noop, 940 | onNextStart: noop, 941 | onPrevStart: noop, 942 | onResize: noop 943 | }; 944 | 945 | Carousel.propTypes = { 946 | /** Items to render */ 947 | children: PropTypes.node.isRequired, 948 | 949 | /** The css class for the root element */ 950 | className: PropTypes.string, 951 | 952 | /** The style object for the root element */ 953 | style: PropTypes.object, 954 | 955 | /** Display the Carousel in a vertical layout */ 956 | verticalMode: PropTypes.bool, 957 | 958 | /** Flip right to left */ 959 | isRTL: PropTypes.bool, 960 | 961 | /** Show dots for paging */ 962 | pagination: PropTypes.bool, 963 | 964 | /** Animation speed */ 965 | transitionMs: PropTypes.number, 966 | 967 | /** transition easing pattern */ 968 | easing: PropTypes.string, 969 | 970 | /** transition easing pattern for the tilt */ 971 | tiltEasing: PropTypes.string, 972 | 973 | /** The "bump" animation when reaching the last item */ 974 | enableTilt: PropTypes.bool, 975 | 976 | /** Number of visible items */ 977 | itemsToShow: PropTypes.number, 978 | 979 | /** Number of items to scroll */ 980 | itemsToScroll: PropTypes.number, 981 | 982 | /** Collection of objects with a width, itemsToShow and itemsToScroll */ 983 | breakPoints: PropTypes.arrayOf( 984 | PropTypes.shape({ 985 | width: PropTypes.number.isRequired, 986 | itemsToShow: PropTypes.number, 987 | itemsToScroll: PropTypes.number 988 | }) 989 | ), 990 | 991 | /** The initial active index when the component mounts */ 992 | initialActiveIndex: PropTypes.number, 993 | 994 | /** **DEPRECATED - use initialActiveIndex instead** The first items when the component mounts */ 995 | initialFirstItem: PropTypes.number, 996 | 997 | /** Show the arrow buttons */ 998 | showArrows: PropTypes.bool, 999 | 1000 | /** Show empty slots when children.length < itemsToShow (not compatible with verticalMode yet !) */ 1001 | showEmptySlots: PropTypes.bool, 1002 | 1003 | /** Disables the arrow button when there are no more items */ 1004 | disableArrowsOnEnd: PropTypes.bool, 1005 | 1006 | /** Go to item on click */ 1007 | focusOnSelect: PropTypes.bool, 1008 | 1009 | /** Automatically inject `tabIndex:0` to visible items */ 1010 | autoTabIndexVisibleItems: PropTypes.bool, 1011 | 1012 | /** A render prop for the arrow component 1013 | * - ({type, onClick}) =>
{type === 'prev' ? '<-' : '->'}
1014 | */ 1015 | renderArrow: PropTypes.func, 1016 | 1017 | /** A render prop for the pagination component 1018 | * - ({ pages, activePage, onClick }) => 1019 | */ 1020 | renderPagination: PropTypes.func, 1021 | 1022 | /** Position the element relative to it's wrapper (use the consts object) - consts.START | consts.CENTER | consts.END */ 1023 | itemPosition: PropTypes.oneOf([consts.START, consts.CENTER, consts.END]), 1024 | 1025 | /** A padding for each element */ 1026 | itemPadding: PropTypes.array, 1027 | 1028 | /** A margin at the beginning and at the end of the carousel (not compatible with verticalMode yet !) */ 1029 | outerSpacing: PropTypes.number, 1030 | 1031 | // swipe 1032 | /** Enable or disable swipe */ 1033 | enableSwipe: PropTypes.bool, 1034 | 1035 | /** Enable or disable mouse swipe */ 1036 | enableMouseSwipe: PropTypes.bool, 1037 | 1038 | /** Prevent page scroll on touchmove. 1039 | * Use this to stop the browser from scrolling while a user swipes. 1040 | * More details: https://github.com/FormidableLabs/react-swipeable#preventdefaulttouchmoveevent-details 1041 | */ 1042 | preventDefaultTouchmoveEvent: PropTypes.bool, 1043 | 1044 | // auto play 1045 | /** Enable or disable auto play */ 1046 | enableAutoPlay: PropTypes.bool, 1047 | 1048 | /** Set auto play speed (ms) */ 1049 | autoPlaySpeed: PropTypes.number, 1050 | 1051 | // callbacks 1052 | /** A callback for the change of an item 1053 | * - onChange(currentItemObject, currentPageIndex) => {} */ 1054 | onChange: PropTypes.func, 1055 | 1056 | /** A callback for the beginning of the next transition 1057 | * - onNextStart(prevItemObject, nextItemObject) => {} */ 1058 | onNextStart: PropTypes.func, 1059 | 1060 | /** A callback for the beginning of the prev transition 1061 | * - onPrevStart(prevItemObject, nextItemObject) => {} */ 1062 | onPrevStart: PropTypes.func, 1063 | 1064 | /** A callback for the end of the next transition 1065 | * - onNextEnd(nextItemObject, currentPageIndex) => {} */ 1066 | onNextEnd: PropTypes.func, 1067 | 1068 | /** A callback for the end of the prev transition 1069 | * - onPrevEnd(nextItemObject, currentPageIndex) => {} */ 1070 | onPrevEnd: PropTypes.func, 1071 | 1072 | /** A callback for the "slider-container" resize 1073 | * - onResize(currentBreakPoint) => {} */ 1074 | onResize: PropTypes.func 1075 | }; 1076 | 1077 | export default Carousel; 1078 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/components/ItemWrapperContainer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { ItemWrapper } from "./styled"; 4 | import { noop } from "../utils/helpers"; 5 | 6 | class ItemWrapperContainer extends React.Component { 7 | onClick = () => { 8 | const { onClick, id } = this.props; 9 | onClick(id); 10 | }; 11 | render() { 12 | return ; 13 | } 14 | } 15 | 16 | ItemWrapperContainer.defaultProps = { 17 | onClick: noop 18 | }; 19 | 20 | ItemWrapperContainer.propTypes = { 21 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 22 | onClick: PropTypes.func 23 | }; 24 | 25 | export default ItemWrapperContainer; 26 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/components/Pagination/Dot.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import styled from "styled-components"; 4 | import { cssPrefix } from "../../utils/helpers"; 5 | 6 | const boxShadow = "0 0 1px 2px rgba(0, 0, 0, 0.5)"; 7 | const activeBoxShadow = "0 0 1px 3px rgba(103,58,183,1)"; 8 | const hoveredBoxShadow = "0 0 1px 3px rgba(103,58,183,.5)"; 9 | 10 | const Dot = styled.button.attrs(({ type = "button" }) => ({ type }))` 11 | box-sizing: border-box; 12 | padding: 0; 13 | transition: all 250ms ease; 14 | border: none; 15 | margin: 5px; 16 | background-color: ${({ active }) => 17 | active ? "rgba(103,58,183,.5)" : "transparent"}; 18 | font-size: 1.3em; 19 | content: ""; 20 | height: 10px; 21 | width: 10px; 22 | box-shadow: ${({ active }) => (active ? activeBoxShadow : boxShadow)}; 23 | border-radius: 50%; 24 | outline: none; 25 | &:hover, 26 | &:focus { 27 | cursor: pointer; 28 | box-shadow: ${({ active }) => 29 | active ? activeBoxShadow : hoveredBoxShadow}; 30 | } 31 | `; 32 | 33 | class DotContainer extends React.Component { 34 | onClick = () => { 35 | const { onClick, id } = this.props; 36 | onClick(id); 37 | }; 38 | render() { 39 | const { active } = this.props; 40 | return ( 41 | 49 | ); 50 | } 51 | } 52 | 53 | DotContainer.propTypes = { 54 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 55 | active: PropTypes.bool, 56 | onClick: PropTypes.func 57 | }; 58 | 59 | export default DotContainer; 60 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/components/Pagination/Pagination.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import styled from "styled-components"; 4 | import Dot from "./Dot"; 5 | import { numberToArray, cssPrefix, noop } from "../../utils/helpers"; 6 | 7 | const Indicators = styled.div` 8 | display: flex; 9 | flex-wrap: wrap; 10 | margin-top: 15px; 11 | `; 12 | 13 | class Pagination extends React.Component { 14 | render() { 15 | const { numOfPages, activePage, onClick } = this.props; 16 | const pages = numberToArray(numOfPages); 17 | return ( 18 | 19 | {pages.map((item, i) => ( 20 | 21 | ))} 22 | 23 | ); 24 | } 25 | } 26 | 27 | Pagination.defaultProps = { 28 | onClick: noop 29 | }; 30 | 31 | Pagination.propTypes = { 32 | numOfPages: PropTypes.number.isRequired, 33 | activePage: PropTypes.number.isRequired, 34 | onClick: PropTypes.func 35 | }; 36 | 37 | export default Pagination; 38 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/components/Pagination/__test__/Dot.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Dot from "../Dot"; 3 | import { mount } from "enzyme"; 4 | 5 | describe("Dot", () => { 6 | it("renders", () => { 7 | mount(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/components/Pagination/index.js: -------------------------------------------------------------------------------- 1 | export { default as Pagination } from "./Pagination"; 2 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/components/Track.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Swipeable } from "react-swipeable"; 4 | import { cssPrefix } from "../utils/helpers"; 5 | import ItemWrapperContainer from "./ItemWrapperContainer"; 6 | 7 | const Track = ({ 8 | children, 9 | childWidth, 10 | autoTabIndexVisibleItems, 11 | enableSwipe, 12 | enableMouseSwipe, 13 | preventDefaultTouchmoveEvent, 14 | itemsToShow, 15 | itemsToScroll, 16 | currentItem, 17 | itemPosition, 18 | itemPadding, 19 | onSwiped, 20 | onSwiping, 21 | verticalMode, 22 | onItemClick 23 | }) => { 24 | const width = `${childWidth}px`; 25 | const paddingStyle = `${itemPadding.join("px ")}px`; 26 | const minVisibleItem = currentItem; 27 | const maxVisibleItem = currentItem + itemsToShow; 28 | const prevItem = minVisibleItem - itemsToScroll; 29 | const nextItem = maxVisibleItem + itemsToScroll; 30 | 31 | const originalChildren = React.Children.map(children, (child, idx) => { 32 | const isVisible = idx >= minVisibleItem && idx < maxVisibleItem; 33 | const isPrevItem = !isVisible && idx >= prevItem && idx < currentItem; 34 | const isNextItem = !isVisible && idx < nextItem && idx > currentItem; 35 | const itemClass = "carousel-item"; 36 | 37 | const childToRender = autoTabIndexVisibleItems 38 | ? React.cloneElement(child, { 39 | tabIndex: isVisible ? 0 : -1 40 | }) 41 | : child; 42 | return ( 43 |
52 | 59 | {childToRender} 60 | 61 |
62 | ); 63 | }); 64 | const toRender = enableSwipe ? ( 65 | 77 | {originalChildren} 78 | 79 | ) : ( 80 | originalChildren 81 | ); 82 | return toRender; 83 | }; 84 | 85 | Track.propTypes = { 86 | children: PropTypes.array.isRequired, 87 | itemsToShow: PropTypes.number.isRequired, 88 | noAutoTabbedItems: PropTypes.bool, 89 | currentItem: PropTypes.number.isRequired, 90 | itemPosition: PropTypes.string, 91 | itemPadding: PropTypes.array, 92 | childWidth: PropTypes.number, 93 | verticalMode: PropTypes.bool, 94 | enableSwipe: PropTypes.bool, 95 | enableMouseSwipe: PropTypes.bool, 96 | preventDefaultTouchmoveEvent: PropTypes.bool, 97 | onSwiped: PropTypes.func, 98 | onSwiping: PropTypes.func, 99 | onItemClick: PropTypes.func 100 | }; 101 | 102 | export default Track; 103 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/components/__tests__/Carousel.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { mount, shallow } from "enzyme"; 3 | import Carousel from "../Carousel"; 4 | import Slider from "../styled/Slider"; 5 | import Pagination from "../Pagination/Pagination"; 6 | import { numberToArray } from "../../utils/helpers"; 7 | 8 | describe("Carousel - public API (props)", () => { 9 | const Items = numberToArray(5).map(i => ( 10 |
11 | {i} 12 |
13 | )); 14 | 15 | it("renders without crashing", () => { 16 | shallow({Items}); 17 | }); 18 | 19 | it("renders children", () => { 20 | const wrapper = mount({Items}); 21 | const children = wrapper.find(Carousel).find(".test-child"); 22 | expect(children.length).toEqual(Items.length); 23 | }); 24 | 25 | it("one child wont break on next", () => { 26 | const wrapper = mount({Items[0]}); 27 | const nextButton = wrapper.find(Carousel).find("button.rec-arrow-right"); 28 | nextButton.simulate("click"); 29 | }); 30 | 31 | it("renders with className in root", () => { 32 | const testClassName = "test-root"; 33 | const wrapper = mount( 34 | {Items} 35 | ); 36 | const carousel = wrapper.first(); 37 | expect(carousel.hasClass(testClassName)); 38 | }); 39 | 40 | it("renders with style in root", () => { 41 | const styleToRender = { position: "fixed" }; 42 | const wrapper = mount({Items}); 43 | const carousel = wrapper.getDOMNode(); 44 | expect(carousel.style.position).toEqual("fixed"); 45 | }); 46 | 47 | it("verticalMode", () => { 48 | const wrapper = shallow({Items}); 49 | const slider = wrapper.find(Slider); 50 | expect(slider.props().verticalMode).toEqual(true); 51 | }); 52 | 53 | it("isRTL", () => { 54 | const wrapper = shallow({Items}); 55 | const slider = wrapper.find(Slider); 56 | expect(slider.props().isRTL).toEqual(true); 57 | }); 58 | 59 | it("pagination", () => { 60 | const wrapper = shallow({Items}); 61 | const pagination = wrapper.find(Pagination); 62 | expect(pagination.exists()).toEqual(true); 63 | }); 64 | 65 | it("renderPagination (renders custom pagination)", () => { 66 | const CustomPagination = () =>
test
; 67 | const renderPagination = () => ; 68 | const wrapper = shallow( 69 | {Items} 70 | ); 71 | 72 | const customPagination = wrapper.find(CustomPagination); 73 | expect(customPagination.exists()).toEqual(true); 74 | }); 75 | 76 | it("wont break with outerSpacing", () => { 77 | const wrapper = shallow({Items}); 78 | 79 | const carousel = wrapper.find(".rec-carousel"); 80 | expect(carousel.exists()).toEqual(true); 81 | }); 82 | }); 83 | 84 | describe("Carousel - public CSS classnames", () => { 85 | const publicClasses = [ 86 | "carousel-wrapper", 87 | "carousel", 88 | "slider-container", 89 | "slider", 90 | "carousel-item", 91 | "carousel-item-visible", 92 | "carousel-item-hidden", 93 | "carousel-item-prev", 94 | "carousel-item-next", 95 | "swipable", 96 | "dot", 97 | "dot_active", 98 | "pagination", 99 | "item-wrapper", 100 | "arrow" 101 | ]; 102 | const prefix = "rec"; 103 | const Items = numberToArray(5).map(i => ( 104 |
105 | {i} 106 |
107 | )); 108 | const carousel = mount( 109 | 110 | {Items} 111 | 112 | ); 113 | publicClasses.forEach(className => { 114 | const withPrefix = `${prefix}-${className}`; 115 | it(`renders ${withPrefix}`, () => { 116 | const withClass = carousel.find(`.${withPrefix}`); 117 | expect(withClass.exists()).toEqual(true); 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/components/styled/Button.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export default styled.button.attrs(({ type = "button" }) => ({ type }))` 4 | box-sizing: border-box; 5 | transition: all 0.3s ease; 6 | font-size: 1.6em; 7 | background-color: rgba(103, 58, 183, 0.1); 8 | color: ${props => (props.disabled ? "#999" : "#333")}; 9 | box-shadow: 0 0 2px 0px #333; 10 | border-radius: 50%; 11 | border: none; 12 | padding: 0; 13 | width: 50px; 14 | height: 50px; 15 | min-width: 50px; 16 | line-height: 50px; 17 | align-self: center; 18 | cursor: pointer; 19 | outline: none; 20 | &:hover:enabled, 21 | &:focus:enabled { 22 | color: #fff; 23 | background-color: rgba(103, 58, 183, 1); 24 | box-shadow: 0 0 2px 0 #333; 25 | } 26 | &:disabled { 27 | cursor: not-allowed; 28 | } 29 | `; 30 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/components/styled/CarouselWrapper.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export default styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | width: 100%; 8 | direction: ${({ isRTL }) => (isRTL ? "rtl" : "ltr")}; 9 | `; 10 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/components/styled/ItemWrapper.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import PropTypes from "prop-types"; 3 | import { cssPrefix } from "../../utils/helpers"; 4 | import consts from "../../consts"; 5 | 6 | const ItemWrapper = styled.div.attrs(({ style }) => ({ 7 | style, 8 | className: cssPrefix("item-wrapper") 9 | }))` 10 | box-sizing: border-box; 11 | display: flex; 12 | overflow: hidden; 13 | user-select: none; 14 | justify-content: ${({ itemPosition }) => itemPosition}; 15 | `; 16 | 17 | ItemWrapper.defaultProps = { 18 | style: {}, 19 | itemPosition: consts.CENTER 20 | }; 21 | 22 | ItemWrapper.propTypes = { 23 | children: PropTypes.element.isRequired, 24 | style: PropTypes.object, 25 | itemPosition: PropTypes.oneOf([consts.START, consts.CENTER, consts.END]) 26 | }; 27 | 28 | export default ItemWrapper; 29 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/components/styled/Slider.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const calcLeft = ({ 4 | isRTL, 5 | verticalMode, 6 | isSwiping, 7 | swipedSliderPosition, 8 | sliderPosition 9 | }) => { 10 | if (verticalMode || isRTL) { 11 | return "auto"; 12 | } else { 13 | return `${isSwiping ? swipedSliderPosition : sliderPosition}px`; 14 | } 15 | }; 16 | 17 | const calcRight = ({ 18 | isRTL, 19 | verticalMode, 20 | isSwiping, 21 | swipedSliderPosition, 22 | sliderPosition 23 | }) => { 24 | if (!verticalMode && isRTL) { 25 | return `${isSwiping ? swipedSliderPosition : sliderPosition}px`; 26 | } else { 27 | return "auto"; 28 | } 29 | }; 30 | 31 | const calcTop = ({ 32 | verticalMode, 33 | isSwiping, 34 | swipedSliderPosition, 35 | sliderPosition 36 | }) => { 37 | if (!verticalMode) { 38 | return "auto"; 39 | } else { 40 | return `${isSwiping ? swipedSliderPosition : sliderPosition}px`; 41 | } 42 | }; 43 | 44 | const calcTransition = ({ isSwiping, transitionMs, easing, tiltEasing }) => { 45 | const duration = isSwiping ? 0 : transitionMs; 46 | const effectiveEasing = isSwiping ? tiltEasing : easing; 47 | return `all ${duration}ms ${effectiveEasing}`; 48 | }; 49 | 50 | // We use attributes (style) to bypass multiple creation of classes (dynamic styling) 51 | export default styled.div.attrs(props => ({ 52 | style: { 53 | transition: calcTransition(props), 54 | left: calcLeft(props), 55 | right: calcRight(props), 56 | top: calcTop(props) 57 | } 58 | }))` 59 | position: absolute; 60 | display: flex; 61 | flex-direction: ${({ verticalMode }) => (verticalMode ? "column" : "row")}; 62 | ${({ verticalMode }) => (verticalMode ? "min-height: 100%;" : "")}; 63 | ${({ verticalMode, outerSpacing }) => 64 | verticalMode ? "" : `margin: 0 ${outerSpacing}px;`}; 65 | `; 66 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/components/styled/SliderContainer.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export default styled.div` 4 | overflow: hidden; 5 | position: relative; 6 | width: 100%; 7 | margin: 0 10px; 8 | `; 9 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/components/styled/StyledCarousel.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export default styled.div.attrs(props => ({ 4 | style: { 5 | height: props.size.height 6 | } 7 | }))` 8 | display: flex; 9 | flex-direction: row; 10 | width: 100%; 11 | `; 12 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/components/styled/index.js: -------------------------------------------------------------------------------- 1 | export { default as Button } from "./Button"; 2 | export { default as ItemWrapper } from "./ItemWrapper"; 3 | export { default as SliderContainer } from "./SliderContainer"; 4 | export { default as Slider } from "./Slider"; 5 | export { default as StyledCarousel } from "./StyledCarousel"; 6 | export { default as CarouselWrapper } from "./CarouselWrapper"; 7 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/consts.js: -------------------------------------------------------------------------------- 1 | export default { 2 | PREV: "PREV", 3 | NEXT: "NEXT", 4 | START: "flex-start", 5 | CENTER: "center", 6 | END: "flex-end" 7 | }; 8 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0 */ // --> OFF 2 | /* eslint no-undef: 0 */ // --> OFF 3 | import * as React from "react"; 4 | 5 | export type RenderArrowProps = { 6 | type: "PREV" | "NEXT"; 7 | onClick: () => void; 8 | isEdge: boolean; 9 | }; 10 | 11 | export type RenderPaginationProps = { 12 | pages: number[]; 13 | activePage: number; 14 | // The onClick event that sets the state of the carousel and sends 15 | // it to a specific page. 16 | onClick: (indicatorId: string) => void; 17 | }; 18 | 19 | export type ItemObject = { 20 | // Children's props 21 | object: any; 22 | index: number; 23 | }; 24 | 25 | export type Breakpoint = { 26 | itemsToScroll: number; 27 | itemsToShow: number; 28 | }; 29 | 30 | export interface ReactElasticCarouselProps { 31 | className?: string; 32 | // Defaults to 1 33 | itemsToShow?: number; 34 | // Defaults to false 35 | verticalMode?: boolean; 36 | // Defaults to false 37 | isRTL: boolean; 38 | // Defaults to true 39 | pagination?: boolean; 40 | // Defaults to 500 41 | transitionMs?: number; 42 | // Defaults to "ease" 43 | easing?: string; 44 | // Defaults to "ease" 45 | tiltEasing?: string; 46 | // Defaults to true. 47 | enableTilt?: boolean; 48 | // Defaults to 1 49 | itemsToScroll?: number; 50 | // Collection of objects with a width, itemsToShow and itemsToScroll 51 | breakPoints?: { 52 | width: number; 53 | itemsToShow?: number; 54 | itemsToScroll?: number; 55 | }[]; 56 | // Defaults to 0 57 | initialActiveIndex?: number; 58 | // Defaults to 0 59 | initialFirstItem?: number; 60 | // Defaults to true 61 | showArrows?: boolean; 62 | // Defaults to false (not compatible with verticalMode yet !) 63 | showEmptySlots?: boolean; 64 | // Defaults to true 65 | disableArrowsOnEnd?: boolean; 66 | // Defaults to boolean 67 | focusOnSelect?: boolean; 68 | // Function to generate your own navigation arrows. 69 | renderArrow?: (props: RenderArrowProps) => void; 70 | // Function to generate your own pagination component. 71 | renderPagination?: (props: RenderPaginationProps) => JSX.Element; 72 | // Defaults to "CENTER" 73 | itemPosition?: "START" | "CENTER" | "END"; 74 | // A padding for each element - Defaults to [0,0,0,0] 75 | itemPadding?: number[]; 76 | // A margin at the beginning and at the end of the carousel - Defaults to 0 (not compatible with verticalMode yet !) 77 | outerSpacing?: number; 78 | // Enable or disable swipe - Defaults to true 79 | enableSwipe?: boolean; 80 | /** Enable or disable mouse swipe */ 81 | enableMouseSwipe?: boolean; 82 | /** Prevent page scroll on touchmove. 83 | * Use this to stop the browser from scrolling while a user swipes. 84 | * More details: https://github.com/FormidableLabs/react-swipeable#preventdefaulttouchmoveevent-details 85 | */ 86 | preventDefaultTouchmoveEvent?: boolean; 87 | // Enable or disable auto play - Defaults to true 88 | enableAutoPlay?: boolean; 89 | /** Set auto play speed (ms) - Defaults to 2000 */ 90 | autoPlaySpeed?: number; 91 | // A callback for the change of an item 92 | onChange?: (currentItemObject: ItemObject, currentPageIndex: number) => void; 93 | // A callback for the beginning of the next transition 94 | onNextStart?: ( 95 | prevItemObject: ItemObject, 96 | nextItemObject: ItemObject 97 | ) => void; 98 | // A callback for the beginning of the prev transition 99 | onPrevStart?: ( 100 | prevItemObject: ItemObject, 101 | nextItemObject: ItemObject 102 | ) => void; 103 | // A callback for the end of the next transition 104 | onNextEnd?: (nextItemObject: ItemObject, currentPageIndex: number) => void; 105 | // A callback for the end of the prev transition 106 | onPrevEnd?: (nextItemObject: ItemObject, currentPageIndex: number) => void; 107 | // A callback for the "slider-container" resize 108 | onResize?: (currentBreakpoint: Breakpoint) => void; 109 | } 110 | 111 | declare class ReactElasticCarousel extends React.Component< 112 | ReactElasticCarouselProps 113 | > {} 114 | 115 | export default ReactElasticCarousel; 116 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./components/Carousel"; 2 | export { default as consts } from "./consts"; 3 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/reducers/__tests__/items.test.js: -------------------------------------------------------------------------------- 1 | import { activeIndexReducer } from "../items"; 2 | import { nextItemAction, prevItemAction } from "../../actions/itemsActions"; 3 | 4 | describe("activeIndexReducer", () => { 5 | it("returns default state", () => { 6 | const currentIndex = 5; 7 | const nextIndex = activeIndexReducer(currentIndex, {}); 8 | expect(nextIndex).toEqual(currentIndex); 9 | }); 10 | 11 | it("get +1 index", () => { 12 | const currentIndex = 3; 13 | const limit = 5; 14 | const itemsToScroll = 1; 15 | const action = nextItemAction(limit, itemsToScroll); 16 | const nextIndex = activeIndexReducer(currentIndex, action); 17 | expect(nextIndex).toEqual(currentIndex + 1); 18 | }); 19 | 20 | it("get -1 index", () => { 21 | const currentIndex = 3; 22 | const limit = 0; 23 | const itemsToScroll = 1; 24 | const action = prevItemAction(limit, itemsToScroll); 25 | const nextIndex = activeIndexReducer(currentIndex, action); 26 | expect(nextIndex).toEqual(currentIndex - 1); 27 | }); 28 | 29 | it("itemsToScroll", () => { 30 | const currentIndex = 3; 31 | const limit = 5; 32 | const itemsToScroll = 2; 33 | const action = nextItemAction(limit, itemsToScroll); 34 | const nextIndex = activeIndexReducer(currentIndex, action); 35 | expect(nextIndex).toEqual(limit); 36 | }); 37 | 38 | it("get last or first index if out of boundries", () => { 39 | const currentIndex = 3; 40 | const itemsToScroll = 5; 41 | 42 | // next 43 | const topLimit = 5; 44 | const nextAction = nextItemAction(topLimit, itemsToScroll); 45 | const nextIndex = activeIndexReducer(currentIndex, nextAction); 46 | 47 | // prev 48 | const startLimit = 0; 49 | const prevAction = prevItemAction(startLimit, itemsToScroll); 50 | const prevIndex = activeIndexReducer(currentIndex, prevAction); 51 | 52 | // assert 53 | expect(nextIndex).toEqual(topLimit); 54 | expect(prevIndex).toEqual(startLimit); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/reducers/items.js: -------------------------------------------------------------------------------- 1 | import { NEXT_ITEM, PREV_ITEM } from "../actions/consts"; 2 | 3 | export const activeIndexReducer = (state, action) => { 4 | const { limit, itemsToScroll, type } = action; 5 | switch (type) { 6 | case NEXT_ITEM: { 7 | let optimisticNextItem = state + itemsToScroll; 8 | const nextItem = limit >= optimisticNextItem ? optimisticNextItem : limit; 9 | return nextItem; 10 | } 11 | 12 | case PREV_ITEM: { 13 | let optimisticPrevItem = state - itemsToScroll; 14 | const prevItem = optimisticPrevItem >= limit ? optimisticPrevItem : limit; 15 | return prevItem; 16 | } 17 | 18 | default: 19 | return state; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/utils/__tests__/helpers.test.js: -------------------------------------------------------------------------------- 1 | import * as helpers from "../helpers"; 2 | 3 | describe("helpers", () => { 4 | it("numberToArray", () => { 5 | const length = 5; 6 | const arr = helpers.numberToArray(length); 7 | expect(Array.isArray(arr)).toEqual(true); 8 | expect(arr.length).toEqual(length); 9 | }); 10 | 11 | it("cssPrefix", () => { 12 | const css = helpers.cssPrefix("test"); 13 | expect(css).toEqual("rec rec-test"); 14 | }); 15 | 16 | it("cssPrefix multi keys", () => { 17 | const css = helpers.cssPrefix("test", "test2"); 18 | expect(css).toEqual("rec rec-test rec-test2"); 19 | }); 20 | 21 | it("pipe", () => { 22 | const inc = num => num + 1; 23 | const double = num => num * 2; 24 | const split = num => num / 2; 25 | 26 | const result = split(double(inc(2))); 27 | const piped = helpers.pipe( 28 | inc, 29 | double, 30 | split 31 | ); 32 | expect(result).toEqual(piped(2)); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/react-elastic-carousel/utils/helpers.js: -------------------------------------------------------------------------------- 1 | export const noop = () => {}; 2 | 3 | export const numberToArray = n => [...Array(n).keys()]; 4 | 5 | export const cssPrefix = (...classNames) => { 6 | const prefix = "rec"; 7 | const space = " "; 8 | let result = `${prefix}`; // initial it with global prefix; 9 | 10 | // in case of an array we add the class prefix per item; 11 | const chainedClasses = classNames.reduce((acc, current) => { 12 | if (current) { 13 | acc += `${space}${prefix}-${current}`; // we must keep spaces between class names 14 | } 15 | return acc; 16 | }, ""); 17 | result += chainedClasses; 18 | 19 | return result; 20 | }; 21 | 22 | export const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x); 23 | 24 | export const throttle = (func, limit) => { 25 | let lastFunc; 26 | let lastRan; 27 | return function() { 28 | const context = this; 29 | const args = arguments; 30 | if (!lastRan) { 31 | func.apply(context, args); 32 | lastRan = Date.now(); 33 | } else { 34 | clearTimeout(lastFunc); 35 | lastFunc = setTimeout(function() { 36 | if (Date.now() - lastRan >= limit) { 37 | func.apply(context, args); 38 | lastRan = Date.now(); 39 | } 40 | }, limit - (Date.now() - lastRan)); 41 | } 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | import { configure } from "enzyme"; 2 | import Adapter from "enzyme-adapter-react-16"; 3 | 4 | configure({ adapter: new Adapter() }); 5 | --------------------------------------------------------------------------------