├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── stale.yml └── workflows │ ├── ci.yml │ └── deploy-website.yml ├── .gitignore ├── .lgtm.yml ├── .npmignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── .storybook ├── main.js ├── manager.js └── preview-head.html ├── .travis.yml ├── CHANGELOG.md ├── CNAME ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── TROUBLESHOOTING.md ├── babel.config.json ├── codesandbox └── default │ ├── package.json │ ├── public │ └── index.html │ └── src │ ├── Carousel.js │ └── index.js ├── package.json ├── setupTests.js ├── src ├── CSSTranslate.ts ├── __tests__ │ ├── Carousel.tsx │ ├── SSR.tsx │ ├── __snapshots__ │ │ └── Carousel.tsx.snap │ └── animations.ts ├── assets │ ├── 1.jpeg │ ├── 2.jpeg │ ├── 3.jpeg │ ├── 4.jpeg │ ├── 5.jpeg │ ├── 6.jpeg │ ├── 7.jpeg │ └── meme.png ├── carousel.scss ├── components │ ├── Carousel │ │ ├── Arrow.tsx │ │ ├── Indicator.tsx │ │ ├── animations.ts │ │ ├── index.tsx │ │ ├── types.ts │ │ └── utils.ts │ ├── Thumbs.tsx │ └── _carousel.scss ├── cssClasses.ts ├── dimensions.ts ├── examples │ └── presentation │ │ └── presentation.scss ├── index.html ├── index.ts ├── main.scss ├── main.tsx ├── shims │ ├── document.ts │ └── window.ts └── styles │ ├── _variables.scss │ └── mixins │ ├── _animation.scss │ ├── _breakpoints.scss │ └── _utils.scss ├── stories ├── 01-basic.tsx └── 02-advanced.tsx ├── tsconfig.json ├── tsconfig.types.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | spaces_around_brackets = both 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [{package.json,*.yml}] 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.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 | **DISCLAIMER** 11 | This repo is not actively maintained by the owner. It depends mostly on contributions. Feel free to fork or to raise a PR and we will review when possible. 12 | 13 | **Describe the bug** 14 | A clear and concise description of what the bug is. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Example** 27 | Provide enough code to reproduce the bug (fork from https://codesandbox.io/s/lp602ljjj7 and send your link) 28 | 29 | **Screenshots** 30 | If applicable, add screenshots to help explain your problem. 31 | 32 | **Desktop (please complete the following information):** 33 | - OS: [e.g. iOS] 34 | - Browser [e.g. chrome, safari] 35 | - Version [e.g. 22] 36 | 37 | **Smartphone (please complete the following information):** 38 | - Device: [e.g. iPhone6] 39 | - OS: [e.g. iOS8.1] 40 | - Browser [e.g. stock browser, safari] 41 | - Version [e.g. 22] 42 | 43 | **Additional context** 44 | Add any other context about the problem here. 45 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 180 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 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | name: Test 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v1 9 | - uses: actions/setup-node@v1 10 | with: 11 | node-version: '10.x' 12 | 13 | - name: install 14 | uses: borales/actions-yarn@v2.0.0 15 | with: 16 | cmd: install 17 | 18 | - name: test 19 | uses: borales/actions-yarn@v2.0.0 20 | with: 21 | cmd: test 22 | 23 | - name: build 24 | uses: borales/actions-yarn@v2.0.0 25 | with: 26 | cmd: build 27 | -------------------------------------------------------------------------------- /.github/workflows/deploy-website.yml: -------------------------------------------------------------------------------- 1 | name: Deploy website 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | deploy: 7 | name: deploy 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v1 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: '10.x' 14 | registry-url: 'https://registry.npmjs.org' 15 | 16 | - name: install 17 | uses: borales/actions-yarn@v2.0.0 18 | with: 19 | cmd: install 20 | 21 | - name: test 22 | uses: borales/actions-yarn@v2.0.0 23 | with: 24 | cmd: jest 25 | 26 | - name: website:build 27 | uses: borales/actions-yarn@v2.0.0 28 | with: 29 | cmd: website:build 30 | 31 | - name: website:copy-assets 32 | uses: borales/actions-yarn@v2.0.0 33 | with: 34 | cmd: website:copy-assets 35 | 36 | - name: website:storybook 37 | uses: borales/actions-yarn@v2.0.0 38 | with: 39 | cmd: website:storybook 40 | 41 | - name: Deploy 🚀 42 | uses: JamesIves/github-pages-deploy-action@releases/v3 43 | with: 44 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} 45 | BRANCH: gh-pages 46 | FOLDER: temp/website 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | npm-debug.log 4 | .cache/ 5 | dev 6 | lib 7 | dist 8 | temp 9 | lib/main.js 10 | codesandbox/default/yarn.lock -------------------------------------------------------------------------------- /.lgtm.yml: -------------------------------------------------------------------------------- 1 | path_classifiers: 2 | generated: 3 | - "/lib" 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | tasks 3 | dist 4 | dev 5 | build 6 | stories 7 | temp 8 | codesandbox 9 | setupTests.js 10 | gulpfile.js 11 | lib/main.js 12 | .github 13 | .babelrc.js 14 | .lgtm.yml 15 | .prettierignore 16 | .prettierrc.json 17 | .cache 18 | .storybook 19 | stories 20 | yarn.lock 21 | .babelrc 22 | .editorconfig 23 | .nvmrc 24 | .travis.yml 25 | .gitignore 26 | CNAME 27 | generate-changelog.js 28 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v10.15.1 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dev/ 2 | dist/ 3 | lib/ 4 | node_modules/ 5 | .cache/ 6 | temp 7 | *.(jpeg|png|gif) 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "printWidth": 120, 4 | "trailingComma": "es5", 5 | "tabWidth": 4, 6 | "semi": true, 7 | "singleQuote": true 8 | } 9 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // Export a function. Accept the base config as the only param. 4 | module.exports = { 5 | stories: ['../stories/*.tsx'], 6 | addons: [ 7 | '@storybook/addon-actions', 8 | '@storybook/addon-viewport/register', 9 | '@storybook/addon-knobs', 10 | '@storybook/addon-storysource', 11 | ], 12 | webpackFinal: async (config, { configType }) => { 13 | config.module.rules.push({ 14 | test: /\.scss$/, 15 | use: ['style-loader', 'css-loader', 'sass-loader'], 16 | include: path.resolve(__dirname, '../src'), 17 | }); 18 | 19 | config.module.rules.push({ 20 | test: /stories\/(.+).tsx$/, 21 | loaders: [require.resolve('@storybook/addon-storysource/loader')], 22 | enforce: 'pre', 23 | }); 24 | 25 | config.module.rules.push({ 26 | test: /\.(ts|tsx)$/, 27 | use: [ 28 | { 29 | loader: require.resolve('babel-loader'), 30 | }, 31 | ], 32 | }); 33 | 34 | config.resolve.extensions.push('.ts', '.tsx'); 35 | 36 | config.performance.hints = false; 37 | 38 | return config; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /.storybook/manager.js: -------------------------------------------------------------------------------- 1 | import { addons } from '@storybook/addons'; 2 | 3 | addons.setConfig({ 4 | showPanel: true, 5 | panelPosition: 'right', 6 | }); 7 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | react-responsive-carousel.js.org 2 | -------------------------------------------------------------------------------- /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 leandrowd+github@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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Found a bug? Want a new feature? Don't like the docs? Please send a pull request or raise an issue. 4 | 5 | ## Raising issues 6 | 7 | When raising an issue, please add as much details as possible. Screenshots, video recordings, or anything else that can make it easier to reproduce the bug you are reporting. 8 | 9 | - A new option is to create a code pen with the code that causes the bug. Fork this [example](https://www.webpackbin.com/bins/-Kxr6IEf5zXSQvGCgKBR) and add your code there, then fork and add the new link to the issue. 10 | 11 | ## Creating Pull Requests 12 | 13 | Pull requests are always welcome. To speed up the review process, please ensure that your pull request have: 14 | 15 | - A good title and description message; 16 | - Recommended that each commit follows the commit message format #{issueId}: {commitDescriptionj} 17 | - Tests covering the changes; 18 | - Story (storybook) if it's a new feature; 19 | - Green builds; 20 | - Breaking changes are commited with a message that describes the issue. Example: `BREAKING CHANGE: styles based on class X need to be update to use class Y.` 21 | 22 | In order to send a Pull Request, you will need to setup your environment - check instructions below; 23 | 24 | ## How to setup the development environment 25 | 26 | Fork and clone the repo: 27 | 28 | - `git clone git@github.com:leandrowd/react-responsive-carousel.git` 29 | 30 | Ensure you have the right node version: 31 | 32 | - `nvm use` # or `nvm install` in case the right version is not installed. Find the right version looking at the `.nvmrc` file. 33 | 34 | Install dependencies: 35 | 36 | - `yarn install` 37 | 38 | Start the dev server: 39 | 40 | - `yarn start` and open the browser on `http://localhost:1234/index.html` 41 | 42 | Run the tests: 43 | 44 | - `yarn test` 45 | 46 | Format the files: 47 | 48 | - `yarn format:write` # this will also run as part of the pre-commit hook. CI will fail the build if unformatted files are pushed. 49 | 50 | Develop on storybooks (optional): 51 | 52 | - `yarn storybook` 53 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 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 | # React Responsive Carousel 2 | 3 | [![npm version](https://badge.fury.io/js/react-responsive-carousel.svg)](https://badge.fury.io/js/react-responsive-carousel) 4 | [![Build Status](https://travis-ci.org/leandrowd/react-responsive-carousel.svg?branch=master)](https://travis-ci.org/leandrowd/react-responsive-carousel) 5 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fleandrowd%2Freact-responsive-carousel.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fleandrowd%2Freact-responsive-carousel?ref=badge_shield) 6 | 7 | Powerful, lightweight and fully customizable carousel component for React apps. 8 | 9 | ### Important 10 | 11 | I don't have any time available to keep maintaining this package. If you have any request, try to sort it within the community. I'm able to merge pull requests that look safe from time to time but no commitment on timelines here. Feel free to fork it and publish under other name if you are in a hurry or to use another component. 12 | 13 | ### Features 14 | 15 | - Responsive 16 | - Mobile friendly 17 | - Swipe to slide 18 | - Mouse emulating touch 19 | - Server side rendering compatible 20 | - Keyboard navigation 21 | - Custom animation duration 22 | - Auto play w/ custom interval 23 | - Infinite loop 24 | - Horizontal or Vertical directions 25 | - Supports images, videos, text content or anything you want. Each direct child represents one slide! 26 | - Supports external controls 27 | - Highly customizable: 28 | - Custom thumbs 29 | - Custom arrows 30 | - Custom indicators 31 | - Custom status 32 | - Custom animation handlers 33 | 34 | ### Important links: 35 | 36 | - [Codesandbox playground](https://codesandbox.io/s/github/leandrowd/react-responsive-carousel/tree/master/codesandbox/default) 37 | - [Storybook](http://react-responsive-carousel.js.org/storybook/) 38 | - [Changelog](https://github.com/leandrowd/react-responsive-carousel/blob/master/CHANGELOG.md) 39 | - [Before contributing](https://github.com/leandrowd/react-responsive-carousel/blob/master/CONTRIBUTING.md) 40 | - [Troubleshooting](https://github.com/leandrowd/react-responsive-carousel/blob/master/TROUBLESHOOTING.md) 41 | 42 | ### Demo 43 | 44 | 45 | 46 | Check it out these [cool demos](http://react-responsive-carousel.js.org/storybook/index.html) created using [storybook](https://storybook.js.org/). The source code for each example is available [here](https://github.com/leandrowd/react-responsive-carousel/blob/master/stories/) 47 | 48 | Customize it yourself: 49 | 50 | - Codesandbox: 51 | 52 | ### Installing as a package 53 | 54 | `yarn add react-responsive-carousel` 55 | 56 | ### Usage 57 | 58 | ```javascript 59 | import React, { Component } from 'react'; 60 | import ReactDOM from 'react-dom'; 61 | import "react-responsive-carousel/lib/styles/carousel.min.css"; // requires a loader 62 | import { Carousel } from 'react-responsive-carousel'; 63 | 64 | class DemoCarousel extends Component { 65 | render() { 66 | return ( 67 | 68 |
69 | 70 |

Legend 1

71 |
72 |
73 | 74 |

Legend 2

75 |
76 |
77 | 78 |

Legend 3

79 |
80 |
81 | ); 82 | } 83 | }); 84 | 85 | ReactDOM.render(, document.querySelector('.demo-carousel')); 86 | 87 | // Don't forget to include the css in your page 88 | 89 | // Using webpack or parcel with a style loader 90 | // import styles from 'react-responsive-carousel/lib/styles/carousel.min.css'; 91 | 92 | // Using html tag: 93 | // 94 | ``` 95 | 96 | ### Props 97 | 98 | | Name | Value | Description | 99 | | ---------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 100 | | ariaLabel | `string` | Define the `aria-label` attribute for the root carousel element. The default is `undefined`, skipping the attribute from markup. | 101 | | axis | `'horizontal'`, `'vertical'` | Define the direction of the slider, defaults to `'horizontal'`. | 102 | | autoFocus | `boolean` | Force focus on the carousel when it renders. | 103 | | autoPlay | `boolean` | Change the slide automatically based on `interval` prop. | 104 | | centerMode | `boolean` | Center the current item and set the slide width based on `centerSlidePercentage`. | 105 | | centerSlidePercentage | `number` | Define the width percentage relative to the carousel width when `centerMode` is `true`. | 106 | | dynamicHeight | `boolean` | The height of the items will not be fixed. | 107 | | emulateTouch | `boolean` | Enable swipe on non-touch screens when `swipeable` is `true`. | 108 | | infiniteLoop | `boolean` | Going after the last item will move back to the first slide. | 109 | | interval | `number` | Interval in milliseconds to automatically go to the next item when `autoPlay` is true, defaults to `3000`. | 110 | | labels | `object` | Apply `aria-label` on carousel with an `object` with the properties `leftArrow`, `rightArrow` and `item`. The default is `{leftArrow: 'previous slide / item', rightArrow: 'next slide / item', item: 'slide item'}`. | 111 | | onClickItem | `function` | Callback to handle a click event on a slide, receives the current index and item as arguments. | 112 | | onClickThumb | `function` | Callback to handle a click event on a thumb, receives the current index and item as arguments. | 113 | | onChange | `function` | Callback to handle every time the selected item changes, receives the current index and item as arguments. | 114 | | onSwipeStart | `function` | Callback to handle when the swipe starts, receives a touch event as argument. | 115 | | onSwipeEnd | `function` | Callback to handle when the swipe ends, receives a touch event as argument. | 116 | | onSwipeMove | `function` | Callback triggered on every movement while swiping, receives a touch event as argument. | 117 | | preventMovementUntilSwipeScrollTolerance | `boolean` | Don't let the carousel scroll until the user swipe to the value specified on `swipeScrollTolerance`. | 118 | | renderArrowPrev | `function` | Render custom previous arrow. Receives a click handler, a `boolean` that shows if there's a previous item, and the accessibility label as arguments. | 119 | | renderArrowNext | `function` | Render custom previous arrow. Receives a click handler, a `boolean` that shows if there's a next item, and the accessibility label as arguments. | 120 | | renderIndicator | `function` | Render custom indicator. Receives a click handler, a `boolean` that shows if the item is selected, the item index, and the accessibility label as arguments. | 121 | | renderItem | `function` | Render a custom item. Receives an item of the carousel, and an `object` with the `isSelected` property as arguments. | 122 | | renderThumbs | `function` | Render prop to show the thumbs, receives the carousel items as argument. Get the `img` tag of each item of the slider, and render it by default. | 123 | | selectedItem | `number` | Set the selected item, defaults to `0`. | 124 | | showArrows | `boolean` | Enable previous and next arrow, defaults to `true`. | 125 | | showStatus | `boolean` | Enable status of the current item to the total, defaults to `true`. | 126 | | showIndicators | `boolean` | Enable indicators to select items, defaults to `true`. | 127 | | showThumbs | `boolean` | Enable thumbs, defaults to `true`. | 128 | | statusFormatter | `function` | Formatter that returns the status as a `string`, receives the current item and the total count as arguments. Defaults to `{currentItem} of {total}` format. | 129 | | stopOnHover | `boolean` | The slide will not change by `autoPlay` on hover, defaults to `true`. | 130 | | swipeable | `boolean` | Enable the user to swipe the carousel, defaults to `true`. | 131 | | swipeScrollTolerance | `number` | How many pixels it's needed to change the slide when swiping, defaults to `5`. | 132 | | thumbWidth | `number` | Width of the thumb, defaults to `80`. | 133 | | transitionTime | `number` | Duration of the animation of changing slides. | 134 | | useKeyboardArrows | `boolean` | Enable the arrows to move the slider when focused. | 135 | | verticalSwipe | `'natural'`, `'standard'` | Set the mode of swipe when the axis is `'vertical'`. The default is `'standard'`. | 136 | | width | `number` or `string` | The width of the carousel, defaults to `100%`. | 137 | 138 | ### Customizing 139 | 140 | #### Items (Slides) 141 | 142 | By default, each slide will be rendered as passed as children. If you need to customize them, use the prop `renderItem`. 143 | 144 | ``` 145 | renderItem: (item: React.ReactNode, options?: { isSelected: boolean }) => React.ReactNode; 146 | ``` 147 | 148 | #### Thumbs 149 | 150 | By default, thumbs are generated extracting the images in each slide. If you don't have images on your slides or if you prefer a different thumbnail, use the method `renderThumbs` to return a new list of images to be used as thumbs. 151 | 152 | ``` 153 | renderThumbs: (children: React.ReactChild[]) => React.ReactChild[] 154 | ``` 155 | 156 | #### Arrows 157 | 158 | By default, simple arrows are rendered on each side. If you need to customize them and the css is not enough, use the `renderArrowPrev` and `renderArrowNext`. The click handler is passed as argument to the prop and needs to be added as click handler in the custom arrow. 159 | 160 | ``` 161 | renderArrowPrev: (clickHandler: () => void, hasPrev: boolean, label: string) => React.ReactNode; 162 | renderArrowNext: (clickHandler: () => void, hasNext: boolean, label: string) => React.ReactNode; 163 | ``` 164 | 165 | #### Indicators 166 | 167 | By default, indicators will be rendered as those small little dots in the bottom part of the carousel. To customize them, use the `renderIndicator` prop. 168 | 169 | ``` 170 | renderIndicator: ( 171 | clickHandler: (e: React.MouseEvent | React.KeyboardEvent) => void, 172 | isSelected: boolean, 173 | index: number, 174 | label: string 175 | ) => React.ReactNode; 176 | ``` 177 | 178 | #### Take full control of the carousel 179 | 180 | If none of the previous options are enough, you can build your own controls for the carousel. Check an example at http://react-responsive-carousel.js.org/storybook/?path=/story/02-advanced--with-external-controls 181 | 182 | #### Custom Animations 183 | 184 | By default, the carousel uses the traditional 'slide' style animation. There is also a built in fade animation, which can be used by passing `'fade'` to the `animationHandler` prop. \*note: the 'fade' animation does not support swiping animations, so you may want to set `swipeable` to `false` 185 | 186 | If you would like something completely custom, you can pass custom animation handler functions to `animationHandler`, `swipeAnimationHandler`, and `stopSwipingHandler`. The animation handler functions accept props and state, and return styles for the contain list, default slide style, selected slide style, and previous slide style. Take a look at the fade animation handler for an idea of how they work: 187 | 188 | ```javascript 189 | const fadeAnimationHandler: AnimationHandler = (props, state): AnimationHandlerResponse => { 190 | const transitionTime = props.transitionTime + 'ms'; 191 | const transitionTimingFunction = 'ease-in-out'; 192 | 193 | let slideStyle: React.CSSProperties = { 194 | position: 'absolute', 195 | display: 'block', 196 | zIndex: -2, 197 | minHeight: '100%', 198 | opacity: 0, 199 | top: 0, 200 | right: 0, 201 | left: 0, 202 | bottom: 0, 203 | transitionTimingFunction: transitionTimingFunction, 204 | msTransitionTimingFunction: transitionTimingFunction, 205 | MozTransitionTimingFunction: transitionTimingFunction, 206 | WebkitTransitionTimingFunction: transitionTimingFunction, 207 | OTransitionTimingFunction: transitionTimingFunction, 208 | }; 209 | 210 | if (!state.swiping) { 211 | slideStyle = { 212 | ...slideStyle, 213 | WebkitTransitionDuration: transitionTime, 214 | MozTransitionDuration: transitionTime, 215 | OTransitionDuration: transitionTime, 216 | transitionDuration: transitionTime, 217 | msTransitionDuration: transitionTime, 218 | }; 219 | } 220 | 221 | return { 222 | slideStyle, 223 | selectedStyle: { ...slideStyle, opacity: 1, position: 'relative' }, 224 | prevStyle: { ...slideStyle }, 225 | }; 226 | }; 227 | ``` 228 | 229 | ### Videos 230 | 231 | If your carousel is about videos, keep in mind that it's up to you to control which videos will play. Using the `renderItem` prop, you will get information saying if the slide is selected or not and can use that to change the video state. Only play videos on selected slides to avoid issues. Check an example at http://react-responsive-carousel.js.org/storybook/?path=/story/02-advanced--youtube-autoplay-with-custom-thumbs 232 | 233 | ======================= 234 | 235 | ### Contributing 236 | 237 | The [contributing guide](https://github.com/leandrowd/react-responsive-carousel/blob/master/CONTRIBUTING.md) contains details on how to create pull requests and setup your dev environment. Please read it before contributing! 238 | 239 | ======================= 240 | 241 | ### Raising issues 242 | 243 | When raising an issue, please add as much details as possible. Screenshots, video recordings, or anything else that can make it easier to reproduce the bug you are reporting. 244 | 245 | - A new option is to create an example with the code that causes the bug. Fork this [example from codesandbox](https://codesandbox.io/s/github/leandrowd/react-responsive-carousel/tree/master/codesandbox/default) and add your code there. Don't forget to fork, save and add the link for the example to the issue. 246 | 247 | ======================= 248 | 249 | ## License 250 | 251 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fleandrowd%2Freact-responsive-carousel.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fleandrowd%2Freact-responsive-carousel?ref=badge_large) 252 | 253 | ``` 254 | 255 | ``` 256 | -------------------------------------------------------------------------------- /TROUBLESHOOTING.md: -------------------------------------------------------------------------------- 1 | # Thumbs not visible 2 | 3 | ### Error message: 4 | > No images found! Can't build the thumb list without images. If you don't need thumbs, set showThumbs={false} in the Carousel. Note that it's not possible to get images rendered inside custom components. 5 | 6 | Carousel will find the thumbs if they are rendered as direct children of the carousel or if they are inside a div or another normal html element in a way that it's possible to access the children of these elements from the carousel. 7 | 8 | For performance reasons, it's not possible to get images inside custom components. 9 | 10 | Good: 11 | ```javascript 12 | 13 | { 14 | images.map((url, index) => ( 15 |
16 | 17 |

Legend

18 |
19 | )) 20 | } 21 |
22 | ``` 23 | 24 | Good: 25 | ```javascript 26 | 27 | { 28 | images.map((url, index) => ( 29 | 30 | )) 31 | } 32 | 33 | ``` 34 | 35 | Bad: 36 | ```javascript 37 | const ImgSlider = ({ url }) => ( 38 |
39 | 40 |
41 | ); 42 | 43 | 44 | { 45 | images.map((url, index) => ) 46 | } 47 | 48 | ``` 49 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "modules": "commonjs", 7 | "targets": ["last 2 versions", "not dead"] 8 | } 9 | ], 10 | "@babel/preset-react", 11 | "@babel/preset-typescript" 12 | ], 13 | "plugins": ["@babel/plugin-proposal-class-properties"] 14 | } 15 | -------------------------------------------------------------------------------- /codesandbox/default/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-responsive-carousel", 3 | "version": "1.0.0", 4 | "description": "Use this playground to play around or to create an example when reporting an issue for the package react-responsive-carousel.", 5 | "keywords": [ 6 | "carousel", 7 | "react", 8 | "react-responsive-carousel" 9 | ], 10 | "homepage": "http://leandrowd.github.io/react-responsive-carousel/", 11 | "main": "src/index.js", 12 | "dependencies": { 13 | "react": "^16.9.0", 14 | "react-dom": "^16.9.0", 15 | "react-responsive-carousel": "^3.2.23", 16 | "react-scripts": "1.1.0" 17 | }, 18 | "devDependencies": {}, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /codesandbox/default/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 17 | 18 | 19 | 28 | React App 29 | 30 | 31 | 32 | 35 |
36 | 46 | 47 | -------------------------------------------------------------------------------- /codesandbox/default/src/Carousel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Carousel } from 'react-responsive-carousel'; 3 | 4 | export default () => ( 5 | 6 |
7 | 8 |

Legend 1

9 |
10 |
11 | 12 |

Legend 2

13 |
14 |
15 | 16 |

Legend 3

17 |
18 |
19 | 20 |

Legend 4

21 |
22 |
23 | 24 |

Legend 5

25 |
26 |
27 | 28 |

Legend 6

29 |
30 |
31 | 32 |

Legend 7

33 |
34 |
35 | 36 |

Legend 8

37 |
38 |
39 | 40 |

Legend 9

41 |
42 |
43 | 44 |

Legend 10

45 |
46 |
47 | 48 |

Legend 11

49 |
50 |
51 | 52 |

Legend 12

53 |
54 |
55 | 56 |

Legend 13

57 |
58 |
59 | 60 |

Legend 14

61 |
62 |
63 | ); 64 | -------------------------------------------------------------------------------- /codesandbox/default/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import Carousel from './Carousel'; 4 | import 'react-responsive-carousel/lib/styles/carousel.min.css'; 5 | 6 | const App = () => ( 7 |
8 | 9 |
10 | ); 11 | 12 | render(, document.getElementById('root')); 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-responsive-carousel", 3 | "version": "3.2.22", 4 | "description": "React Responsive Carousel", 5 | "author": { 6 | "name": "Leandro Augusto Lemos", 7 | "url": "http://leandrowd.github.io/" 8 | }, 9 | "main": "lib/js/index.js", 10 | "types": "lib/ts/index.d.ts", 11 | "license": "MIT", 12 | "keywords": [ 13 | "react", 14 | "carousel", 15 | "gallery", 16 | "image-gallery", 17 | "slider", 18 | "responsive", 19 | "swipe", 20 | "mobile-friendly", 21 | "react-component", 22 | "view" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/leandrowd/react-responsive-carousel.git" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/leandrowd/react-responsive-carousel/issues" 30 | }, 31 | "homepage": "http://leandrowd.github.io/react-responsive-carousel/", 32 | "scripts": { 33 | "start": "parcel src/index.html src/assets/**/*", 34 | "storybook": "start-storybook -p 9001 -s ./src -c .storybook", 35 | "changelog": "auto-changelog --ignore-commit-pattern=\"(Merge pull request|Merge branch|Updating changelog|Prepare for publishing)\" --breaking-pattern \"BREAKING CHANGE:\"", 36 | "update-codesandbox": "cd codesandbox/default && yarn add react-responsive-carousel@latest", 37 | "format": "prettier \"**/*.{js,ts,tsx,json}\"", 38 | "format:check": "yarn format --check", 39 | "format:write": "yarn format --write", 40 | "test": "yarn format:check && yarn typecheck && yarn jest && yarn jest-ssr", 41 | "jest": "jest", 42 | "jest-ssr": "jest --testEnvironment=node ./src/__tests__/SSR.tsx", 43 | "typecheck": "tsc -p tsconfig.json --noEmit", 44 | "update-snapshots": "jest --updateSnapshot", 45 | "build": "yarn lib:build", 46 | "lib:build": "yarn lib:build-js && yarn lib:build-styles && yarn lib:build-types", 47 | "lib:build-js": "babel ./src -d lib/js --ignore './src/__tests__' --extensions .ts,.tsx --config-file ./babel.config.json", 48 | "lib:build-styles": "mkdirp lib/styles && node-sass src/carousel.scss > lib/styles/carousel.css && node-sass --output-style compressed src/carousel.scss > lib/styles/carousel.min.css", 49 | "lib:build-types": "tsc -p tsconfig.types.json", 50 | "lib:pre-publish": "npm version patch && git push origin master", 51 | "lib:publish": "npm publish && git push --tags", 52 | "lib:post-publish": "yarn changelog && yarn update-codesandbox && git add . && git commit -m 'Updating changelog and codesandbox' && git push origin master", 53 | "lib:build-and-publish": "yarn lib:build && yarn lib:pre-publish && npm run lib:publish && yarn lib:post-publish", 54 | "website:build": "parcel build ./src/index.html --out-dir temp/website", 55 | "website:copy-assets": "cp -r ./src/assets temp/website/assets && cp -r ./CNAME temp/website/CNAME", 56 | "website:storybook": "build-storybook -s ./src -o ./temp/website/storybook", 57 | "website:deploy": "gh-pages -d temp/website", 58 | "website:create-and-publish": "yarn website:build && yarn website:copy-assets && yarn website:storybook && yarn website:deploy", 59 | "prepublish-to-npm": "git pull && yarn build", 60 | "publish-to-npm": "npm version patch && npm publish && git push --tags", 61 | "postpublish-to-npm": "yarn changelog && yarn update-codesandbox && git add . && git commit -m 'Updating changelog and codesandbox' && git push origin master", 62 | "prepublish-prerelease-to-npm": "git pull && yarn build && git add . && git commit -m 'Prepare for publishing prerelease'", 63 | "publish-prerelease-to-npm": "(git pull && npm version prerelease --preid=next && npm publish)" 64 | }, 65 | "devDependencies": { 66 | "@babel/cli": "^7.8.4", 67 | "@babel/core": "^7.9.0", 68 | "@babel/plugin-proposal-class-properties": "^7.8.3", 69 | "@babel/preset-env": "^7.9.5", 70 | "@babel/preset-react": "^7.9.4", 71 | "@babel/preset-typescript": "^7.9.0", 72 | "@kadira/react-storybook-addon-info": "^3.4.0", 73 | "@kadira/storybook": "^2.35.3", 74 | "@storybook/addon-actions": "^5.3.18", 75 | "@storybook/addon-essentials": "^5.3.18", 76 | "@storybook/addon-knobs": "^5.3.18", 77 | "@storybook/addon-storysource": "^5.3.18", 78 | "@storybook/addon-viewport": "^5.3.18", 79 | "@storybook/react": "^5.3.18", 80 | "@types/classnames": "^2.2.10", 81 | "@types/enzyme": "^3.10.5", 82 | "@types/jest": "^25.2.1", 83 | "@types/react": "^16.9.34", 84 | "@types/react-dom": "^16.9.6", 85 | "@types/react-test-renderer": "^16.9.2", 86 | "auto-changelog": "^1.10.2", 87 | "babel-loader": "^8.1.0", 88 | "css-loader": "^3.5.2", 89 | "enzyme": "^3.11.0", 90 | "enzyme-adapter-react-16": "^1.15.2", 91 | "gh-pages": "^2.2.0", 92 | "husky": "^3.0.9", 93 | "jest-cli": "^25.3.0", 94 | "mkdirp": "^1.0.4", 95 | "node-sass": "^4.13.1", 96 | "parcel-bundler": "^1.12.4", 97 | "prettier": "^1.18.2", 98 | "pretty-quick": "^2.0.0", 99 | "react": "^16.9.0", 100 | "react-dom": "^16.9.0", 101 | "react-player": "^1.15.3", 102 | "react-test-renderer": "^16.9.0", 103 | "sass-loader": "^8.0.2", 104 | "style-loader": "^1.1.3", 105 | "typescript": "^3.8.3" 106 | }, 107 | "dependencies": { 108 | "classnames": "^2.2.5", 109 | "prop-types": "^15.5.8", 110 | "react-easy-swipe": "^0.0.21" 111 | }, 112 | "jest": { 113 | "unmockedModulePathPatterns": [ 114 | "node_modules" 115 | ], 116 | "rootDir": "src", 117 | "setupFilesAfterEnv": [ 118 | "../setupTests.js" 119 | ] 120 | }, 121 | "husky": { 122 | "hooks": { 123 | "pre-commit": "pretty-quick --staged" 124 | } 125 | }, 126 | "auto-changelog": { 127 | "output": "CHANGELOG.md", 128 | "template": "keepachangelog", 129 | "unreleased": true, 130 | "commitLimit": true 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /setupTests.js: -------------------------------------------------------------------------------- 1 | import Enzyme from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | Enzyme.configure({ adapter: new Adapter() }); 4 | -------------------------------------------------------------------------------- /src/CSSTranslate.ts: -------------------------------------------------------------------------------- 1 | export default (position: number, metric: 'px' | '%', axis: 'horizontal' | 'vertical') => { 2 | const positionPercent = position === 0 ? position : position + metric; 3 | const positionCss = axis === 'horizontal' ? [positionPercent, 0, 0] : [0, positionPercent, 0]; 4 | const transitionProp = 'translate3d'; 5 | 6 | const translatedPosition = '(' + positionCss.join(',') + ')'; 7 | 8 | return transitionProp + translatedPosition; 9 | }; 10 | -------------------------------------------------------------------------------- /src/__tests__/Carousel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { shallow, mount, ReactWrapper } from 'enzyme'; 4 | import renderer from 'react-test-renderer'; 5 | import * as index from '../index'; 6 | // @ts-ignore 7 | import Swipe, { ReactEasySwipeProps } from 'react-easy-swipe'; 8 | import Carousel from '../components/Carousel'; 9 | import Thumbs from '../components/Thumbs'; 10 | import getDocument from '../shims/document'; 11 | import getWindow from '../shims/window'; 12 | import { 13 | CarouselProps, 14 | AnimationHandler, 15 | SwipeAnimationHandler, 16 | StopSwipingHandler, 17 | } from '../components/Carousel/types'; 18 | import { getPosition } from '../components/Carousel/utils'; 19 | import { slideSwipeAnimationHandler } from '../components/Carousel/animations'; 20 | 21 | const findDOMNodeWithinWrapper = (wrapper: ReactWrapper, domNode: HTMLElement) => { 22 | return wrapper.findWhere((n) => n.getDOMNode() === domNode).simulate('click'); 23 | }; 24 | 25 | describe('Slider', function() { 26 | jest.autoMockOff(); 27 | 28 | let window: Window; 29 | let document: Document; 30 | let component: ReactWrapper; 31 | let componentInstance: any; 32 | let totalChildren: number; 33 | let lastItemIndex: number; 34 | const animationHandler: AnimationHandler = jest.fn(); 35 | const swipeAnimationHandler: SwipeAnimationHandler = jest.fn(slideSwipeAnimationHandler); 36 | const stopSwipingHandler: StopSwipingHandler = jest.fn(); 37 | 38 | const bootstrap = (props: Partial, children: CarouselProps['children']) => { 39 | window = getWindow(); 40 | document = getDocument(); 41 | 42 | component = mount>({children}); 43 | 44 | componentInstance = component.instance(); 45 | 46 | totalChildren = children && children.length ? React.Children.count(componentInstance.props.children) : 0; 47 | lastItemIndex = totalChildren - 1; 48 | }; 49 | 50 | const baseChildren = [ 51 | , 52 | , 53 | , 54 | , 55 | , 56 | , 57 | , 58 | ]; 59 | 60 | const renderDefaultComponent = ({ children = baseChildren, ...props }: Partial) => { 61 | props = { animationHandler, swipeAnimationHandler, stopSwipingHandler, ...props }; 62 | bootstrap(props, children); 63 | }; 64 | 65 | const renderForSnapshot = (props: Partial, children: CarouselProps['children']) => { 66 | return renderer.create({children}).toJSON(); 67 | }; 68 | 69 | beforeEach(() => { 70 | renderDefaultComponent({}); 71 | }); 72 | 73 | describe('Exports', () => { 74 | it('should export Carousel from the main index file', () => { 75 | expect(index.Carousel).toBe(Carousel); 76 | }); 77 | it('should export Thumbs from the main index file', () => { 78 | expect(index.Thumbs).toBe(Thumbs); 79 | }); 80 | }); 81 | 82 | describe('Basics', () => { 83 | describe('DisplayName', () => { 84 | it('should be Carousel', () => { 85 | expect(Carousel.displayName).toBe('Carousel'); 86 | }); 87 | }); 88 | 89 | describe('Default Props', () => { 90 | describe('values', () => { 91 | const props: Partial = { 92 | axis: 'horizontal', 93 | centerSlidePercentage: 80, 94 | interval: 3000, 95 | labels: { 96 | leftArrow: 'previous slide / item', 97 | rightArrow: 'next slide / item', 98 | item: 'slide item', 99 | }, 100 | selectedItem: 0, 101 | showArrows: true, 102 | showIndicators: true, 103 | showStatus: true, 104 | showThumbs: true, 105 | stopOnHover: true, 106 | swipeScrollTolerance: 5, 107 | swipeable: true, 108 | transitionTime: 350, 109 | verticalSwipe: 'standard', 110 | width: '100%', 111 | }; 112 | 113 | Object.keys(props).forEach((prop) => { 114 | it(`should have ${prop} as ${props[prop as keyof CarouselProps]}`, () => { 115 | expect(component.prop(prop)).toBeDefined(); 116 | expect(component.prop(prop)).toEqual(props[prop as keyof CarouselProps]); 117 | }); 118 | }); 119 | }); 120 | 121 | describe('methods', () => { 122 | it('renderArrowPrev should return a button', () => { 123 | expect(componentInstance.props.renderArrowPrev!(jest.fn(), true, 'prev')).toMatchSnapshot(); 124 | }); 125 | 126 | it('renderArrowNext should return a button', () => { 127 | expect(componentInstance.props.renderArrowNext!(jest.fn(), true, 'next')).toMatchSnapshot(); 128 | }); 129 | 130 | it('renderIndicator should return a list item', () => { 131 | expect(componentInstance.props.renderIndicator!(jest.fn(), true, 0, 'slide')).toMatchSnapshot(); 132 | }); 133 | 134 | it('renderItem should pass through the item', () => { 135 | expect(componentInstance.props.renderItem!(
item
)).toMatchSnapshot(); 136 | }); 137 | 138 | it('renderThumbs should return a list of images extracted from the children', () => { 139 | expect( 140 | componentInstance.props.renderThumbs!([ 141 |
  • 142 | 143 |

    Legend 1

    144 |
  • , 145 |
  • 146 | 147 |

    Legend 2

    148 |
  • , 149 |
  • 150 | 151 |

    Legend 3

    152 |
  • , 153 |
  • 154 | 155 |

    Legend 4

    156 |
  • , 157 |
  • 158 | 159 |

    Legend 5

    160 |
  • , 161 |
  • 162 | 163 |

    Legend 6

    164 |
  • , 165 |
  • 166 | 167 |

    Legend 7

    168 |
  • , 169 | ]) 170 | ).toMatchSnapshot(); 171 | }); 172 | 173 | it('statusFormatter should return a string', () => { 174 | expect(componentInstance.props.statusFormatter!(1, 3)).toEqual('1 of 3'); 175 | }); 176 | }); 177 | }); 178 | 179 | describe('Initial State', () => { 180 | const props = { 181 | selectedItem: 0, 182 | hasMount: false, 183 | }; 184 | 185 | Object.entries(props).forEach((key, value) => { 186 | it(`should have ${key} as ${value}`, () => { 187 | expect(component.state('selectedItem')).toBe(0); 188 | expect(component.state('hasMount')).toBe(false); 189 | }); 190 | }); 191 | }); 192 | }); 193 | 194 | describe('componentDidMount', () => { 195 | it('should bind the events', () => { 196 | componentInstance.bindEvents = jest.fn(); 197 | componentInstance.componentDidMount(); 198 | expect(componentInstance.bindEvents).toHaveBeenCalledTimes(1); 199 | }); 200 | 201 | it('should not bind the events if there are no children', () => { 202 | bootstrap({}, undefined); 203 | componentInstance.bindEvents = jest.fn(); 204 | componentInstance.componentDidMount(); 205 | expect(componentInstance.bindEvents).not.toHaveBeenCalled(); 206 | }); 207 | 208 | it('should bind the events if children were lazy loaded (through componentDidUpdate)', () => { 209 | bootstrap({}, undefined); 210 | componentInstance.bindEvents = jest.fn(); 211 | expect(componentInstance.bindEvents).not.toHaveBeenCalled(); 212 | 213 | component.setProps({ 214 | children: [], 215 | }); 216 | 217 | expect(componentInstance.bindEvents).toHaveBeenCalledTimes(1); 218 | }); 219 | }); 220 | 221 | describe('componentDidUpdate', () => { 222 | it('should unbind the events', () => { 223 | componentInstance.setState({ swiping: false }); 224 | componentInstance.componentDidUpdate({}, { swiping: true }); 225 | expect(stopSwipingHandler).toHaveBeenCalledTimes(1); 226 | }); 227 | }); 228 | 229 | describe('componentWillUnmount', () => { 230 | beforeEach(() => { 231 | componentInstance.unbindEvents = jest.fn(); 232 | componentInstance.componentWillUnmount(); 233 | }); 234 | it('should unbind the events', () => { 235 | expect(componentInstance.unbindEvents).toHaveBeenCalledTimes(1); 236 | }); 237 | }); 238 | 239 | describe('bindEvents', () => { 240 | describe('when useKeyboardArrows is false', () => { 241 | beforeEach(() => { 242 | window.addEventListener = jest.fn(); 243 | document.addEventListener = jest.fn(); 244 | componentInstance.bindEvents(); 245 | }); 246 | 247 | it('should bind resize to updateSizes', () => { 248 | expect(window.addEventListener).toHaveBeenCalledWith('resize', componentInstance.updateSizes); 249 | }); 250 | 251 | it('should bind DOMContentLoaded to updateSizes', () => { 252 | expect(window.addEventListener).toHaveBeenCalledWith('DOMContentLoaded', componentInstance.updateSizes); 253 | }); 254 | 255 | it('should not bind keydown to navigateWithKeyboard', () => { 256 | expect(document.addEventListener).not.toHaveBeenCalledWith( 257 | 'keydown', 258 | componentInstance.navigateWithKeyboard 259 | ); 260 | }); 261 | }); 262 | 263 | describe('when useKeyboardArrows is true', () => { 264 | beforeEach(() => { 265 | renderDefaultComponent({ 266 | useKeyboardArrows: true, 267 | }); 268 | 269 | window.addEventListener = jest.fn(); 270 | document.addEventListener = jest.fn(); 271 | componentInstance.bindEvents(); 272 | }); 273 | 274 | it('should bind resize to updateSizes', () => { 275 | expect(window.addEventListener).toHaveBeenCalledWith('resize', componentInstance.updateSizes); 276 | }); 277 | 278 | it('should bind DOMContentLoaded to updateSizes', () => { 279 | expect(window.addEventListener).toHaveBeenCalledWith('DOMContentLoaded', componentInstance.updateSizes); 280 | }); 281 | 282 | it('should bind keydown to navigateWithKeyboard', () => { 283 | expect(document.addEventListener).toHaveBeenCalledWith( 284 | 'keydown', 285 | componentInstance.navigateWithKeyboard 286 | ); 287 | }); 288 | }); 289 | }); 290 | 291 | describe('unbindEvents', () => { 292 | describe('when useKeyboardArrows is false', () => { 293 | beforeEach(() => { 294 | window.removeEventListener = jest.fn(); 295 | document.removeEventListener = jest.fn(); 296 | componentInstance.unbindEvents(); 297 | }); 298 | 299 | it('should unbind resize to updateSizes', () => { 300 | expect(window.removeEventListener).toHaveBeenCalledWith('resize', componentInstance.updateSizes); 301 | }); 302 | 303 | it('should unbind DOMContentLoaded to updateSizes', () => { 304 | expect(window.removeEventListener).toHaveBeenCalledWith( 305 | 'DOMContentLoaded', 306 | componentInstance.updateSizes 307 | ); 308 | }); 309 | 310 | it('should not unbind keydown to navigateWithKeyboard', () => { 311 | expect(document.removeEventListener).not.toHaveBeenCalledWith( 312 | 'keydown', 313 | componentInstance.navigateWithKeyboard 314 | ); 315 | }); 316 | 317 | it('should not set a tabIndex on the carousel-root', () => { 318 | expect(component.find('.carousel-root[tabIndex=0]').length).toBe(0); 319 | }); 320 | }); 321 | 322 | describe('when useKeyboardArrows is true', () => { 323 | beforeEach(() => { 324 | renderDefaultComponent({ 325 | useKeyboardArrows: true, 326 | }); 327 | 328 | window.removeEventListener = jest.fn(); 329 | document.removeEventListener = jest.fn(); 330 | componentInstance.unbindEvents(); 331 | }); 332 | 333 | it('should unbind resize to updateSizes', () => { 334 | expect(window.removeEventListener).toHaveBeenCalledWith('resize', componentInstance.updateSizes); 335 | }); 336 | 337 | it('should unbind DOMContentLoaded to updateSizes', () => { 338 | expect(window.removeEventListener).toHaveBeenCalledWith( 339 | 'DOMContentLoaded', 340 | componentInstance.updateSizes 341 | ); 342 | }); 343 | 344 | it('should unbind keydown to navigateWithKeyboard', () => { 345 | expect(document.removeEventListener).toHaveBeenCalledWith( 346 | 'keydown', 347 | componentInstance.navigateWithKeyboard 348 | ); 349 | }); 350 | 351 | it('should set a tabIndex on the carousel-root', () => { 352 | expect(component.find('.carousel-root[tabIndex=0]').length).toBe(1); 353 | }); 354 | }); 355 | }); 356 | 357 | describe('getInitialImage', () => { 358 | it('Returns the first image within the declared selected item', () => { 359 | renderDefaultComponent({ 360 | selectedItem: 2, 361 | }); 362 | 363 | const initialImage = componentInstance.getInitialImage(); 364 | const expectedMatchingImageComponent = baseChildren[2]; 365 | 366 | expect(initialImage.src.endsWith(expectedMatchingImageComponent.props.src)).toEqual(true); 367 | }); 368 | }); 369 | 370 | describe('navigateWithKeyboard', () => { 371 | const setActiveElement = (element: HTMLElement) => { 372 | (document.activeElement as any) = element; 373 | }; 374 | 375 | beforeEach(() => { 376 | // jsdom has issues with activeElement so we are hacking it for this specific scenario 377 | Object.defineProperty(document, 'activeElement', { 378 | writable: true, 379 | }); 380 | }); 381 | 382 | describe('Axis === horizontal', () => { 383 | beforeEach(() => { 384 | renderDefaultComponent({ 385 | axis: 'horizontal', 386 | useKeyboardArrows: true, 387 | }); 388 | 389 | componentInstance.increment = jest.fn(); 390 | componentInstance.decrement = jest.fn(); 391 | }); 392 | 393 | it('should not navigate if the focus is outside of the carousel', () => { 394 | componentInstance.navigateWithKeyboard({ keyCode: 39 }); 395 | componentInstance.navigateWithKeyboard({ keyCode: 37 }); 396 | 397 | expect(componentInstance.increment).not.toHaveBeenCalled(); 398 | expect(componentInstance.decrement).not.toHaveBeenCalled(); 399 | }); 400 | 401 | it('should call only increment on ArrowRight (39)', () => { 402 | setActiveElement(componentInstance.carouselWrapperRef); 403 | 404 | componentInstance.navigateWithKeyboard({ keyCode: 39 }); 405 | 406 | expect(componentInstance.increment).toHaveBeenCalledTimes(1); 407 | expect(componentInstance.decrement).not.toHaveBeenCalled(); 408 | }); 409 | 410 | it('should call only decrement on ArrowLeft (37)', () => { 411 | setActiveElement(componentInstance.carouselWrapperRef); 412 | 413 | componentInstance.navigateWithKeyboard({ keyCode: 37 }); 414 | 415 | expect(componentInstance.decrement).toHaveBeenCalledTimes(1); 416 | expect(componentInstance.increment).not.toHaveBeenCalled(); 417 | }); 418 | 419 | it('should not call increment on ArrowDown (40)', () => { 420 | setActiveElement(componentInstance.carouselWrapperRef); 421 | 422 | componentInstance.navigateWithKeyboard({ keyCode: 40 }); 423 | 424 | expect(componentInstance.increment).not.toHaveBeenCalled(); 425 | expect(componentInstance.decrement).not.toHaveBeenCalled(); 426 | }); 427 | 428 | it('should not call decrement on ArrowUp (38)', () => { 429 | setActiveElement(componentInstance.carouselWrapperRef); 430 | 431 | componentInstance.navigateWithKeyboard({ keyCode: 38 }); 432 | 433 | expect(componentInstance.decrement).not.toHaveBeenCalled(); 434 | expect(componentInstance.increment).not.toHaveBeenCalled(); 435 | }); 436 | }); 437 | 438 | describe('Axis === vertical', () => { 439 | beforeEach(() => { 440 | renderDefaultComponent({ 441 | axis: 'vertical', 442 | useKeyboardArrows: true, 443 | }); 444 | 445 | componentInstance.increment = jest.fn(); 446 | componentInstance.decrement = jest.fn(); 447 | }); 448 | 449 | it('should not navigate if the focus is outside of the carousel', () => { 450 | componentInstance.navigateWithKeyboard({ keyCode: 40 }); 451 | componentInstance.navigateWithKeyboard({ keyCode: 38 }); 452 | 453 | expect(componentInstance.increment).not.toHaveBeenCalled(); 454 | expect(componentInstance.decrement).not.toHaveBeenCalled(); 455 | }); 456 | 457 | it('should call only increment on ArrowDown (40)', () => { 458 | setActiveElement(componentInstance.carouselWrapperRef); 459 | componentInstance.navigateWithKeyboard({ keyCode: 40 }); 460 | 461 | expect(componentInstance.increment).toHaveBeenCalledTimes(1); 462 | expect(componentInstance.decrement).not.toHaveBeenCalled(); 463 | }); 464 | 465 | it('should call only decrement on ArrowUp (38)', () => { 466 | setActiveElement(componentInstance.carouselWrapperRef); 467 | componentInstance.navigateWithKeyboard({ keyCode: 38 }); 468 | 469 | expect(componentInstance.decrement).toHaveBeenCalledTimes(1); 470 | expect(componentInstance.increment).not.toHaveBeenCalled(); 471 | }); 472 | 473 | it('should not call increment on ArrowRight (39)', () => { 474 | setActiveElement(componentInstance.carouselWrapperRef); 475 | componentInstance.navigateWithKeyboard({ keyCode: 39 }); 476 | 477 | expect(componentInstance.increment).not.toHaveBeenCalled(); 478 | expect(componentInstance.decrement).not.toHaveBeenCalled(); 479 | }); 480 | 481 | it('should not call decrement on ArrowLeft (37)', () => { 482 | setActiveElement(componentInstance.carouselWrapperRef); 483 | componentInstance.navigateWithKeyboard({ keyCode: 37 }); 484 | 485 | expect(componentInstance.decrement).not.toHaveBeenCalled(); 486 | expect(componentInstance.increment).not.toHaveBeenCalled(); 487 | }); 488 | }); 489 | }); 490 | 491 | describe('changeItem', () => { 492 | beforeEach(() => { 493 | componentInstance.selectItem = jest.fn(); 494 | componentInstance.getFirstItem = jest.fn().mockReturnValue(2); 495 | componentInstance.changeItem(1)(); 496 | }); 497 | 498 | it('should call selectItem sending selectedItem as 1', () => { 499 | expect(componentInstance.selectItem.mock.calls[0][0]).toEqual({ 500 | selectedItem: 1, 501 | }); 502 | }); 503 | }); 504 | 505 | describe('selectItem', () => { 506 | beforeEach(() => { 507 | componentInstance.setState = jest.fn(); 508 | componentInstance.handleOnChange = jest.fn(); 509 | componentInstance.selectItem({ 510 | selectedItem: 1, 511 | ramdomNumber: 2, 512 | }); 513 | }); 514 | 515 | it('should call setState sending the argument received, with previousItem', () => { 516 | expect(componentInstance.setState.mock.calls[0][0]).toEqual({ 517 | previousItem: 0, 518 | selectedItem: 1, 519 | ramdomNumber: 2, 520 | }); 521 | }); 522 | 523 | it('should call handleOnChange sending only selectedItem', () => { 524 | expect(componentInstance.handleOnChange.mock.calls[0][0]).toBe(1); 525 | }); 526 | }); 527 | 528 | it('should add a thumb-wrapper container', () => { 529 | expect(component.find('.thumbs-wrapper').length).toBe(1); 530 | }); 531 | 532 | it('should insert aria-label if provided', () => { 533 | const ariaLabel = 'Carousel title'; 534 | renderDefaultComponent({ ariaLabel }); 535 | expect(component.find(`[aria-label="${ariaLabel}"]`)).toBeTruthy(); 536 | }); 537 | 538 | describe('Moving', () => { 539 | beforeEach(() => { 540 | componentInstance.showArrows = true; 541 | componentInstance.lastPosition = 3; 542 | componentInstance.visibleItems = 3; 543 | }); 544 | 545 | it('should set the selectedItem from the props', () => { 546 | renderDefaultComponent({ selectedItem: 3 }); 547 | expect(componentInstance.state.selectedItem).toBe(3); 548 | }); 549 | 550 | it('should update the position of the Carousel if selectedItem is changed', () => { 551 | findDOMNodeWithinWrapper(component, componentInstance.itemsRef[2]).simulate('click'); 552 | expect(componentInstance.state.selectedItem).toBe(2); 553 | 554 | findDOMNodeWithinWrapper(component, componentInstance.itemsRef[3]).simulate('click'); 555 | expect(componentInstance.state.selectedItem).toBe(3); 556 | }); 557 | }); 558 | 559 | describe('Selecting', () => { 560 | it('should set the index as selectedItem when clicked', () => { 561 | expect(componentInstance.state.selectedItem).toBe(0); 562 | 563 | findDOMNodeWithinWrapper(component, componentInstance.itemsRef[1]).simulate('click'); 564 | expect(componentInstance.state.selectedItem).toBe(1); 565 | 566 | findDOMNodeWithinWrapper(component, componentInstance.itemsRef[3]).simulate('click'); 567 | expect(componentInstance.state.selectedItem).toBe(3); 568 | }); 569 | 570 | it('should call a given onSelectItem function when an item is clicked', () => { 571 | var mockedFunction = jest.fn(); 572 | 573 | renderDefaultComponent({ onClickItem: mockedFunction }); 574 | 575 | findDOMNodeWithinWrapper(component, componentInstance.itemsRef[1]).simulate('click'); 576 | expect(mockedFunction).toBeCalled(); 577 | 578 | findDOMNodeWithinWrapper(component, componentInstance.itemsRef[0]).simulate('click'); 579 | expect(componentInstance.state.selectedItem).toBe(0); 580 | }); 581 | 582 | it('should call onSelectItem function when exactly 1 child is present', () => { 583 | var mockedFunction = jest.fn(); 584 | 585 | renderDefaultComponent({ 586 | children: [], 587 | onClickItem: mockedFunction, 588 | }); 589 | expect(componentInstance.state.selectedItem).toBe(0); 590 | 591 | findDOMNodeWithinWrapper(component, componentInstance.itemsRef[0]).simulate('click'); 592 | expect(componentInstance.state.selectedItem).toBe(0); 593 | expect(mockedFunction).toBeCalled(); 594 | }); 595 | }); 596 | 597 | const findDOMNodeByClass = (instance: any, classNames: string) => 598 | (ReactDOM.findDOMNode(instance)! as HTMLElement).querySelectorAll(classNames); 599 | 600 | describe('Navigating', () => { 601 | beforeEach(() => { 602 | componentInstance.showArrows = true; 603 | }); 604 | 605 | it('should disable the left arrow if we are showing the first item', () => { 606 | findDOMNodeWithinWrapper(component, componentInstance.itemsRef[0]).simulate('click'); 607 | expect( 608 | findDOMNodeByClass(componentInstance, '.carousel-slider .control-prev.control-disabled') 609 | ).toHaveLength(1); 610 | }); 611 | 612 | it('should enable the left arrow if we are showing other than the first item', () => { 613 | findDOMNodeWithinWrapper(component, componentInstance.itemsRef[1]).simulate('click'); 614 | expect( 615 | findDOMNodeByClass(componentInstance, '.carousel-slider .control-prev.control-disabled') 616 | ).toHaveLength(0); 617 | }); 618 | 619 | it('should disable the right arrow if we reach the lastPosition', () => { 620 | findDOMNodeWithinWrapper(component, componentInstance.itemsRef[1]).simulate('click'); 621 | expect( 622 | findDOMNodeByClass(componentInstance, '.carousel-slider .control-next.control-disabled') 623 | ).toHaveLength(0); 624 | 625 | findDOMNodeWithinWrapper(component, componentInstance.itemsRef[6]).simulate('click'); 626 | expect( 627 | findDOMNodeByClass(componentInstance, '.carousel-slider .control-next.control-disabled') 628 | ).toHaveLength(1); 629 | }); 630 | }); 631 | 632 | describe('Infinite Loop', () => { 633 | beforeEach(() => { 634 | renderDefaultComponent({ 635 | infiniteLoop: true, 636 | }); 637 | }); 638 | 639 | it('should enable the prev arrow if we are showing the first item', () => { 640 | findDOMNodeWithinWrapper(component, componentInstance.itemsRef[0]).simulate('click'); 641 | expect( 642 | findDOMNodeByClass(componentInstance, '.carousel-slider .control-prev.control-disabled') 643 | ).toHaveLength(0); 644 | }); 645 | 646 | it('should enable the right arrow if we reach the lastPosition', () => { 647 | findDOMNodeWithinWrapper(component, componentInstance.itemsRef[6]).simulate('click'); 648 | expect( 649 | findDOMNodeByClass(componentInstance, '.carousel-slider .control-next.control-disabled') 650 | ).toHaveLength(0); 651 | }); 652 | 653 | it('should move to the first one if increment was called in the last', () => { 654 | componentInstance.setState({ 655 | selectedItem: lastItemIndex, 656 | }); 657 | 658 | expect(componentInstance.state.selectedItem).toBe(lastItemIndex); 659 | 660 | componentInstance.increment(); 661 | 662 | expect(componentInstance.state.selectedItem).toBe(0); 663 | }); 664 | 665 | it('should move to the last one if decrement was called in the first', () => { 666 | expect(componentInstance.state.selectedItem).toBe(0); 667 | 668 | componentInstance.decrement(); 669 | 670 | expect(componentInstance.state.selectedItem).toBe(lastItemIndex); 671 | }); 672 | 673 | it('should render the clone slides', () => { 674 | expect( 675 | component 676 | .find('.slide') 677 | .at(0) 678 | .key() 679 | ).toContain('itemKey6clone'); 680 | expect( 681 | component 682 | .find('.slide') 683 | .at(8) 684 | .key() 685 | ).toContain('itemKey0clone'); 686 | }); 687 | 688 | it('should work with minimal children', () => { 689 | renderDefaultComponent({ 690 | children: [, ], 691 | infiniteLoop: true, 692 | }); 693 | componentInstance.decrement(); 694 | expect(componentInstance.state.selectedItem).toBe(lastItemIndex); 695 | 696 | renderDefaultComponent({ 697 | children: [], 698 | infiniteLoop: true, 699 | }); 700 | componentInstance.decrement(); 701 | expect(componentInstance.state.selectedItem).toBe(lastItemIndex); 702 | }); 703 | 704 | it('should not render any Swipe component with one child', () => { 705 | renderDefaultComponent({ 706 | children: [], 707 | infiniteLoop: true, 708 | }); 709 | 710 | expect(component.find(Swipe).length).toBe(0); 711 | }); 712 | }); 713 | 714 | describe('Auto Play', () => { 715 | beforeEach(() => { 716 | jest.useFakeTimers(); 717 | window.addEventListener = jest.fn(); 718 | 719 | renderDefaultComponent({ 720 | autoPlay: true, 721 | }); 722 | }); 723 | 724 | afterEach(() => { 725 | jest.useRealTimers(); 726 | }); 727 | 728 | it('should disable when only 1 child is present', () => { 729 | renderDefaultComponent({ 730 | children: [], 731 | autoPlay: true, 732 | }); 733 | 734 | expect(componentInstance.state.selectedItem).toBe(0); 735 | 736 | jest.runOnlyPendingTimers(); 737 | 738 | expect(componentInstance.state.selectedItem).toBe(0); 739 | }); 740 | 741 | it('should change items automatically', () => { 742 | expect(componentInstance.state.selectedItem).toBe(0); 743 | 744 | jest.runOnlyPendingTimers(); 745 | 746 | expect(componentInstance.state.selectedItem).toBe(1); 747 | 748 | jest.runOnlyPendingTimers(); 749 | 750 | expect(componentInstance.state.selectedItem).toBe(2); 751 | }); 752 | 753 | it('should not move automatically if hovering', () => { 754 | componentInstance.stopOnHover(); 755 | 756 | expect(componentInstance.state.selectedItem).toBe(0); 757 | 758 | jest.runOnlyPendingTimers(); 759 | 760 | expect(componentInstance.state.selectedItem).toBe(0); 761 | 762 | componentInstance.autoPlay(); 763 | 764 | jest.runOnlyPendingTimers(); 765 | 766 | expect(componentInstance.state.selectedItem).toBe(1); 767 | }); 768 | 769 | it('should restart auto-play after disabling it via props', () => { 770 | expect(componentInstance.state.selectedItem).toBe(0); 771 | 772 | jest.runOnlyPendingTimers(); 773 | 774 | expect(componentInstance.state.selectedItem).toBe(1); 775 | 776 | component.setProps({ 777 | autoPlay: false, 778 | }); 779 | 780 | jest.runOnlyPendingTimers(); 781 | 782 | expect(componentInstance.state.selectedItem).toBe(1); 783 | 784 | component.setProps({ 785 | autoPlay: true, 786 | }); 787 | 788 | jest.runOnlyPendingTimers(); 789 | 790 | expect(componentInstance.state.selectedItem).toBe(2); 791 | }); 792 | 793 | it('should reset when changing the slide through indicator', () => { 794 | renderDefaultComponent({ interval: 3000, autoPlay: true }); 795 | jest.advanceTimersByTime(2000); 796 | 797 | expect(componentInstance.state.selectedItem).toBe(0); 798 | 799 | const changeToSecondItem = componentInstance.changeItem(1); 800 | // it only runs with an event 801 | changeToSecondItem(new MouseEvent('click')); 802 | 803 | jest.advanceTimersByTime(1000); 804 | 805 | expect(componentInstance.state.selectedItem).toBe(1); 806 | }); 807 | }); 808 | 809 | describe('Infinite Loop and Auto Play', () => { 810 | beforeEach(() => { 811 | jest.useFakeTimers(); 812 | window.addEventListener = jest.fn(); 813 | 814 | renderDefaultComponent({ 815 | children: [ 816 | , 817 | , 818 | , 819 | ], 820 | infiniteLoop: true, 821 | autoPlay: true, 822 | }); 823 | }); 824 | 825 | afterEach(() => { 826 | jest.useRealTimers(); 827 | }); 828 | 829 | it('should automatically loop infinitely', () => { 830 | expect(componentInstance.state.selectedItem).toBe(0); 831 | 832 | jest.runOnlyPendingTimers(); 833 | 834 | expect(componentInstance.state.selectedItem).toBe(1); 835 | 836 | jest.runOnlyPendingTimers(); 837 | 838 | expect(componentInstance.state.selectedItem).toBe(2); 839 | 840 | jest.runOnlyPendingTimers(); 841 | 842 | expect(componentInstance.state.selectedItem).toBe(0); 843 | 844 | jest.runOnlyPendingTimers(); 845 | 846 | expect(componentInstance.state.selectedItem).toBe(1); 847 | 848 | jest.runOnlyPendingTimers(); 849 | 850 | expect(componentInstance.state.selectedItem).toBe(2); 851 | }); 852 | }); 853 | 854 | describe('Mouse enter/leave', () => { 855 | describe('onMouseEnter', () => { 856 | it('should set isMouseEntered to true', () => { 857 | componentInstance.stopOnHover(); 858 | expect(componentInstance.state.isMouseEntered).toBe(true); 859 | }); 860 | 861 | it('should stop auto play when hovering', () => { 862 | componentInstance.clearAutoPlay = jest.fn(); 863 | componentInstance.stopOnHover(); 864 | expect(componentInstance.clearAutoPlay).toHaveBeenCalledTimes(1); 865 | }); 866 | }); 867 | 868 | describe('onMouseLeave', () => { 869 | it('should set isMouseEntered to false', () => { 870 | componentInstance.startOnLeave(); 871 | expect(componentInstance.state.isMouseEntered).toBe(false); 872 | }); 873 | 874 | it('should start auto play again after hovering', () => { 875 | componentInstance.autoPlay = jest.fn(); 876 | componentInstance.startOnLeave(); 877 | expect(componentInstance.autoPlay).toHaveBeenCalledTimes(1); 878 | }); 879 | }); 880 | }); 881 | 882 | describe('Focus', () => { 883 | describe('calling forceFocus', () => { 884 | it('should call carousel wrapper focus', () => { 885 | componentInstance.carouselWrapperRef.focus = jest.fn(); 886 | componentInstance.forceFocus(); 887 | expect(componentInstance.carouselWrapperRef.focus).toHaveBeenCalledTimes(1); 888 | }); 889 | }); 890 | 891 | describe('AutoFocus === true', () => { 892 | it('should call forceFocus on componentDidMount', () => { 893 | const forceFocusSpy = jest.spyOn(Carousel.prototype, 'forceFocus'); 894 | renderDefaultComponent({ autoFocus: true }); 895 | expect(forceFocusSpy).toHaveBeenCalledTimes(1); 896 | forceFocusSpy.mockReset(); 897 | forceFocusSpy.mockRestore(); 898 | }); 899 | 900 | it('should call forceFocus conditionally on componentDidUpdate', () => { 901 | componentInstance.forceFocus = jest.fn(); 902 | 903 | component.setProps({ autoFocus: false }); 904 | expect(componentInstance.forceFocus).toHaveBeenCalledTimes(0); 905 | 906 | component.setProps({ autoFocus: true }); 907 | expect(componentInstance.forceFocus).toHaveBeenCalledTimes(1); 908 | }); 909 | }); 910 | }); 911 | 912 | describe('Swiping', () => { 913 | describe('onSwipeStart', () => { 914 | it('should set swiping to true', () => { 915 | componentInstance.onSwipeStart(); 916 | expect(componentInstance.state.swiping).toBe(true); 917 | }); 918 | 919 | it('should call onSwipeStart callback', () => { 920 | var onSwipeStartFunction = jest.fn(); 921 | renderDefaultComponent({ onSwipeStart: onSwipeStartFunction }); 922 | 923 | componentInstance.onSwipeStart(); 924 | expect(onSwipeStartFunction).toBeCalled(); 925 | }); 926 | }); 927 | 928 | describe('onSwipeMove', () => { 929 | beforeEach(() => { 930 | renderDefaultComponent({ preventMovementUntilSwipeScrollTolerance: true }); 931 | }); 932 | 933 | it('should return true to stop scrolling if there was movement in the same direction as the carousel axis', () => { 934 | expect( 935 | componentInstance.onSwipeMove({ 936 | x: 10, 937 | y: 0, 938 | }) 939 | ).toBe(true); 940 | }); 941 | 942 | it('should return false to allow scrolling if there was no movement in the same direction as the carousel axis', () => { 943 | expect( 944 | componentInstance.onSwipeMove({ 945 | x: 0, 946 | y: 10, 947 | }) 948 | ).toBe(false); 949 | }); 950 | 951 | it('should call the swipeAnimationHandler when onSwipeMove is fired', () => { 952 | componentInstance.onSwipeMove({ 953 | x: 10, 954 | y: 0, 955 | }); 956 | 957 | expect(swipeAnimationHandler).toHaveBeenCalled(); 958 | }); 959 | 960 | it('should call onSwipeMove callback', () => { 961 | var onSwipeMoveFunction = jest.fn(); 962 | renderDefaultComponent({ onSwipeMove: onSwipeMoveFunction }); 963 | 964 | componentInstance.onSwipeMove({ x: 0, y: 10 }); 965 | expect(onSwipeMoveFunction).toHaveBeenCalled(); 966 | }); 967 | }); 968 | 969 | describe('onSwipeEnd', () => { 970 | it('should set swiping to false', () => { 971 | componentInstance.onSwipeEnd(); 972 | expect(componentInstance.state.swiping).toBe(false); 973 | }); 974 | 975 | it('should stop autoplay', () => { 976 | componentInstance.clearAutoPlay = jest.fn(); 977 | componentInstance.onSwipeEnd(); 978 | expect(componentInstance.clearAutoPlay).toHaveBeenCalledTimes(1); 979 | }); 980 | 981 | it('should not start autoplay again', () => { 982 | componentInstance.autoPlay = jest.fn(); 983 | componentInstance.onSwipeEnd(); 984 | expect(componentInstance.autoPlay).toHaveBeenCalledTimes(0); 985 | }); 986 | 987 | it('should start autoplay again when autoplay is true', () => { 988 | renderDefaultComponent({ autoPlay: true }); 989 | componentInstance.autoPlay = jest.fn(); 990 | componentInstance.onSwipeEnd(); 991 | expect(componentInstance.autoPlay).toHaveBeenCalledTimes(1); 992 | }); 993 | it('should call onSwipeEnd callback', () => { 994 | var onSwipeEndFunction = jest.fn(); 995 | renderDefaultComponent({ onSwipeEnd: onSwipeEndFunction }); 996 | 997 | componentInstance.onSwipeEnd(); 998 | expect(onSwipeEndFunction).toBeCalled(); 999 | }); 1000 | }); 1001 | 1002 | describe("verticalSwipe === 'standard'", () => { 1003 | it('should pass the correct props to ', () => { 1004 | renderDefaultComponent({ 1005 | axis: 'vertical', 1006 | }); 1007 | 1008 | const swipeProps: ReactEasySwipeProps = component 1009 | .find(Swipe) 1010 | .first() 1011 | .props(); 1012 | 1013 | expect(swipeProps.onSwipeUp).toBe(componentInstance.onSwipeForward); 1014 | expect(swipeProps.onSwipeDown).toBe(componentInstance.onSwipeBackwards); 1015 | }); 1016 | }); 1017 | 1018 | describe("verticalSwipe === 'natural'", () => { 1019 | it('should pass the correct props to ', () => { 1020 | renderDefaultComponent({ 1021 | axis: 'vertical', 1022 | verticalSwipe: 'natural', 1023 | }); 1024 | 1025 | const swipeProps: ReactEasySwipeProps = component 1026 | .find(Swipe) 1027 | .first() 1028 | .props(); 1029 | 1030 | expect(swipeProps.onSwipeUp).toBe(componentInstance.onSwipeBackwards); 1031 | expect(swipeProps.onSwipeDown).toBe(componentInstance.onSwipeForward); 1032 | }); 1033 | }); 1034 | 1035 | describe('emulateTouch', () => { 1036 | it('should cancel click when swipe forward and backwards with emulated touch', () => { 1037 | renderDefaultComponent({ 1038 | emulateTouch: true, 1039 | }); 1040 | 1041 | let currentIndex = componentInstance.state.selectedItem; 1042 | const items = componentInstance.props.children; 1043 | 1044 | componentInstance.onSwipeForward(); 1045 | componentInstance.handleClickItem(currentIndex, items[currentIndex]); 1046 | ++currentIndex; 1047 | 1048 | expect(componentInstance.state.selectedItem).toEqual(currentIndex); 1049 | 1050 | componentInstance.onSwipeBackwards(); 1051 | componentInstance.handleClickItem(currentIndex, items[currentIndex]); 1052 | --currentIndex; 1053 | 1054 | expect(componentInstance.state.selectedItem).toEqual(currentIndex); 1055 | }); 1056 | }); 1057 | }); 1058 | 1059 | describe('center mode', () => { 1060 | beforeEach(() => { 1061 | renderDefaultComponent({ 1062 | centerMode: true, 1063 | }); 1064 | }); 1065 | 1066 | describe('getPosition', () => { 1067 | it('should return regular tranform calculation for vertical axis', () => { 1068 | renderDefaultComponent({ 1069 | centerMode: true, 1070 | axis: 'vertical', 1071 | }); 1072 | const props = componentInstance.props; 1073 | 1074 | expect(getPosition(0, props)).toBe(0); 1075 | expect(getPosition(1, props)).toBe(-100); 1076 | expect(getPosition(2, props)).toBe(-200); 1077 | expect(getPosition(3, props)).toBe(-300); 1078 | expect(getPosition(4, props)).toBe(-400); 1079 | expect(getPosition(5, props)).toBe(-500); 1080 | expect(getPosition(6, props)).toBe(-600); 1081 | }); 1082 | 1083 | it('should return padded transform calculation for horizontal axis', () => { 1084 | const props = componentInstance.props; 1085 | expect(getPosition(0, props)).toBe(0); 1086 | expect(getPosition(1, props)).toBe(-70); 1087 | expect(getPosition(2, props)).toBe(-150); 1088 | expect(getPosition(3, props)).toBe(-230); 1089 | expect(getPosition(4, props)).toBe(-310); 1090 | expect(getPosition(5, props)).toBe(-390); 1091 | // last one takes up more space 1092 | expect(getPosition(6, props)).toBe(-460); 1093 | }); 1094 | 1095 | it('should return padded tranform calculation for custom centerSlidePercentage', () => { 1096 | renderDefaultComponent({ 1097 | centerMode: true, 1098 | centerSlidePercentage: 50, 1099 | }); 1100 | 1101 | const props = componentInstance.props; 1102 | 1103 | expect(getPosition(0, props)).toBe(0); 1104 | expect(getPosition(1, props)).toBe(-25); 1105 | expect(getPosition(2, props)).toBe(-75); 1106 | expect(getPosition(3, props)).toBe(-125); 1107 | expect(getPosition(4, props)).toBe(-175); 1108 | expect(getPosition(5, props)).toBe(-225); 1109 | expect(getPosition(6, props)).toBe(-250); 1110 | }); 1111 | }); 1112 | 1113 | describe('slide style', () => { 1114 | it('should have a min-width of 80%', () => { 1115 | const slide = shallow(component.find('.slide').get(0)); 1116 | expect(slide.prop('style')).toHaveProperty('minWidth', '80%'); 1117 | }); 1118 | 1119 | it('should have min-width defined by centerSlidePercentage', () => { 1120 | renderDefaultComponent({ 1121 | centerMode: true, 1122 | centerSlidePercentage: 50, 1123 | }); 1124 | const slide = shallow(component.find('.slide').get(0)); 1125 | expect(slide.prop('style')).toHaveProperty('minWidth', '50%'); 1126 | }); 1127 | 1128 | it('should not be present for vertical axis', () => { 1129 | renderDefaultComponent({ 1130 | centerMode: true, 1131 | axis: 'vertical', 1132 | }); 1133 | const slide = shallow(component.find('.slide').get(0)); 1134 | expect(slide.prop('style')).toEqual({}); 1135 | }); 1136 | }); 1137 | }); 1138 | 1139 | describe('Snapshots', () => { 1140 | it('default', () => { 1141 | expect(renderForSnapshot({}, baseChildren)).toMatchSnapshot(); 1142 | }); 1143 | 1144 | it('no thumbs', () => { 1145 | expect( 1146 | renderForSnapshot( 1147 | { 1148 | showThumbs: false, 1149 | }, 1150 | baseChildren 1151 | ) 1152 | ).toMatchSnapshot(); 1153 | }); 1154 | 1155 | it('no arrows', () => { 1156 | expect( 1157 | renderForSnapshot( 1158 | { 1159 | showArrows: false, 1160 | }, 1161 | baseChildren 1162 | ) 1163 | ).toMatchSnapshot(); 1164 | }); 1165 | 1166 | it('no indicators', () => { 1167 | expect( 1168 | renderForSnapshot( 1169 | { 1170 | showIndicators: false, 1171 | }, 1172 | baseChildren 1173 | ) 1174 | ).toMatchSnapshot(); 1175 | }); 1176 | 1177 | it('no indicators', () => { 1178 | expect( 1179 | renderForSnapshot( 1180 | { 1181 | showStatus: false, 1182 | }, 1183 | baseChildren 1184 | ) 1185 | ).toMatchSnapshot(); 1186 | }); 1187 | 1188 | it('custom class name', () => { 1189 | expect( 1190 | renderForSnapshot( 1191 | { 1192 | className: 'my-custom-carousel', 1193 | }, 1194 | baseChildren 1195 | ) 1196 | ).toMatchSnapshot(); 1197 | }); 1198 | 1199 | it('custom width', () => { 1200 | expect( 1201 | renderForSnapshot( 1202 | { 1203 | width: '700px', 1204 | }, 1205 | baseChildren 1206 | ) 1207 | ).toMatchSnapshot(); 1208 | }); 1209 | 1210 | it('vertical axis', () => { 1211 | expect( 1212 | renderForSnapshot( 1213 | { 1214 | axis: 'vertical', 1215 | }, 1216 | baseChildren 1217 | ) 1218 | ).toMatchSnapshot(); 1219 | }); 1220 | 1221 | it('no children at mount', () => { 1222 | expect(renderForSnapshot({}, undefined)).toMatchSnapshot(); 1223 | }); 1224 | 1225 | it('center mode', () => { 1226 | expect( 1227 | renderForSnapshot( 1228 | { 1229 | centerMode: true, 1230 | }, 1231 | baseChildren 1232 | ) 1233 | ).toMatchSnapshot(); 1234 | }); 1235 | 1236 | it('swipeable false', () => { 1237 | expect( 1238 | renderForSnapshot( 1239 | { 1240 | swipeable: false, 1241 | }, 1242 | baseChildren 1243 | ) 1244 | ).toMatchSnapshot(); 1245 | }); 1246 | 1247 | it('infinite loop', () => { 1248 | expect( 1249 | renderForSnapshot( 1250 | { 1251 | infiniteLoop: true, 1252 | }, 1253 | baseChildren 1254 | ) 1255 | ).toMatchSnapshot(); 1256 | }); 1257 | }); 1258 | 1259 | jest.autoMockOn(); 1260 | }); 1261 | -------------------------------------------------------------------------------- /src/__tests__/SSR.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOMServer from 'react-dom/server'; 3 | import Carousel from '../components/Carousel'; 4 | 5 | describe('SSR', () => { 6 | it('should be able to render the component without throwing', () => { 7 | expect(() => 8 | ReactDOMServer.renderToStaticMarkup( 9 | 10 | 11 | 12 | 13 | ) 14 | ).not.toThrow(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/__tests__/animations.ts: -------------------------------------------------------------------------------- 1 | import Carousel from '../components/Carousel'; 2 | import { CarouselProps, CarouselState } from '../components/Carousel/types'; 3 | import { 4 | fadeAnimationHandler, 5 | slideAnimationHandler, 6 | slideSwipeAnimationHandler, 7 | } from '../components/Carousel/animations'; 8 | 9 | /** 10 | * Test suite for the default animation handlers 11 | */ 12 | describe('Default Animations', () => { 13 | let props: CarouselProps; 14 | let state: CarouselState; 15 | const setState = jest.fn(); 16 | 17 | beforeEach(() => { 18 | props = Carousel.defaultProps; 19 | state = { 20 | initialized: false, 21 | previousItem: 0, 22 | selectedItem: 1, 23 | hasMount: false, 24 | isMouseEntered: false, 25 | autoPlay: true, 26 | swiping: false, 27 | swipeMovementStarted: false, 28 | cancelClick: false, 29 | itemSize: 1, 30 | itemListStyle: {}, 31 | slideStyle: {}, 32 | selectedStyle: {}, 33 | prevStyle: {}, 34 | }; 35 | }); 36 | 37 | describe('slideAnimationHandler', () => { 38 | it('should return itemListStyle with a transform prop', () => { 39 | const response = slideAnimationHandler(props, state); 40 | expect(response).toHaveProperty('itemListStyle'); 41 | expect(response.itemListStyle).toHaveProperty('transform'); 42 | }); 43 | 44 | it('should return a transition time on itemListStyle if not swiping', () => { 45 | const response = slideAnimationHandler(props, state); 46 | expect(response.itemListStyle).toHaveProperty('transitionDuration'); 47 | }); 48 | }); 49 | 50 | describe('slideSwipeAnimationHandler', () => { 51 | it('should return empty object if preventMovementUntilSwipeScrollTolerance is true and the tolerance has not been reached', () => { 52 | props = { ...props, swipeScrollTolerance: 10, preventMovementUntilSwipeScrollTolerance: true }; 53 | expect( 54 | slideSwipeAnimationHandler( 55 | { 56 | x: 5, 57 | y: 10, 58 | }, 59 | props, 60 | state, 61 | setState 62 | ) 63 | ).toEqual({}); 64 | }); 65 | 66 | it('should return itemListStyle if preventMovementUntilSwipeScrollTolerance is true and movement has already begun', () => { 67 | props = { ...props, swipeScrollTolerance: 10, preventMovementUntilSwipeScrollTolerance: true }; 68 | state = { ...state, swipeMovementStarted: true }; 69 | 70 | expect( 71 | slideSwipeAnimationHandler( 72 | { 73 | x: 5, 74 | y: 10, 75 | }, 76 | props, 77 | state, 78 | setState 79 | ) 80 | ).toHaveProperty('itemListStyle'); 81 | }); 82 | 83 | it('should return itemListStyle if preventMovementUntilSwipeScrollTolerance is true and the tolerance has been reached', () => { 84 | props = { ...props, swipeScrollTolerance: 10, preventMovementUntilSwipeScrollTolerance: true }; 85 | 86 | expect( 87 | slideSwipeAnimationHandler( 88 | { 89 | x: 30, 90 | y: 10, 91 | }, 92 | props, 93 | state, 94 | setState 95 | ) 96 | ).toHaveProperty('itemListStyle'); 97 | }); 98 | 99 | it('should still return itemListStyle if preventMovementUntilSwipeScrollTolerance is false and the tolerance has not been reached', () => { 100 | props = { ...props, swipeScrollTolerance: 10, preventMovementUntilSwipeScrollTolerance: false }; 101 | 102 | expect( 103 | slideSwipeAnimationHandler( 104 | { 105 | x: 5, 106 | y: 10, 107 | }, 108 | props, 109 | state, 110 | setState 111 | ) 112 | ).toHaveProperty('itemListStyle'); 113 | }); 114 | }); 115 | 116 | describe('fade animation handler', () => { 117 | it('should return a slideStyle, selectedStyle, and prevStyle', () => { 118 | const response = fadeAnimationHandler(props, state); 119 | expect(response).toHaveProperty('slideStyle'); 120 | expect(response).toHaveProperty('selectedStyle'); 121 | expect(response).toHaveProperty('prevStyle'); 122 | }); 123 | 124 | it('should give selectedStyle an opacity of 1 and position of relative', () => { 125 | const response = fadeAnimationHandler(props, state); 126 | expect(response.selectedStyle?.opacity).toEqual(1); 127 | expect(response.selectedStyle?.position).toEqual('relative'); 128 | }); 129 | 130 | it('should give default slideStyle a negative z-index, opacity 0, and position absolute', () => { 131 | const response = fadeAnimationHandler(props, state); 132 | expect(response.slideStyle?.opacity).toEqual(0); 133 | expect(response.slideStyle?.position).toEqual('absolute'); 134 | expect(response.slideStyle?.zIndex).toEqual(-2); 135 | }); 136 | 137 | it('should give prevStyle a negative z-index, opacity 0, and position absolute', () => { 138 | const response = fadeAnimationHandler(props, state); 139 | expect(response.prevStyle?.opacity).toEqual(0); 140 | expect(response.prevStyle?.position).toEqual('absolute'); 141 | expect(response.prevStyle?.zIndex).toEqual(-2); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /src/assets/1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leandrowd/react-responsive-carousel/dd345ebd8be0219231c0756870ffd068dce5f8f0/src/assets/1.jpeg -------------------------------------------------------------------------------- /src/assets/2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leandrowd/react-responsive-carousel/dd345ebd8be0219231c0756870ffd068dce5f8f0/src/assets/2.jpeg -------------------------------------------------------------------------------- /src/assets/3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leandrowd/react-responsive-carousel/dd345ebd8be0219231c0756870ffd068dce5f8f0/src/assets/3.jpeg -------------------------------------------------------------------------------- /src/assets/4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leandrowd/react-responsive-carousel/dd345ebd8be0219231c0756870ffd068dce5f8f0/src/assets/4.jpeg -------------------------------------------------------------------------------- /src/assets/5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leandrowd/react-responsive-carousel/dd345ebd8be0219231c0756870ffd068dce5f8f0/src/assets/5.jpeg -------------------------------------------------------------------------------- /src/assets/6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leandrowd/react-responsive-carousel/dd345ebd8be0219231c0756870ffd068dce5f8f0/src/assets/6.jpeg -------------------------------------------------------------------------------- /src/assets/7.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leandrowd/react-responsive-carousel/dd345ebd8be0219231c0756870ffd068dce5f8f0/src/assets/7.jpeg -------------------------------------------------------------------------------- /src/assets/meme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leandrowd/react-responsive-carousel/dd345ebd8be0219231c0756870ffd068dce5f8f0/src/assets/meme.png -------------------------------------------------------------------------------- /src/carousel.scss: -------------------------------------------------------------------------------- 1 | // basic imports 2 | @import "styles/variables"; 3 | @import "styles/mixins/breakpoints"; 4 | @import "styles/mixins/animation"; 5 | @import "styles/mixins/utils"; 6 | 7 | // buttons, form inputs, etc... 8 | @import "components/carousel"; 9 | -------------------------------------------------------------------------------- /src/components/Carousel/Arrow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import klass from '../../cssClasses'; // Assuming cssClasses.ts is in src/ 3 | 4 | interface ArrowProps { 5 | direction: 'prev' | 'next'; 6 | onClickHandler: () => void; 7 | enabled: boolean; 8 | label: string; 9 | } 10 | 11 | const Arrow: React.FC = ({ direction, onClickHandler, enabled, label }) => { 12 | const isPrev = direction === 'prev'; 13 | const arrowClassName = isPrev ? klass.ARROW_PREV(!enabled) : klass.ARROW_NEXT(!enabled); 14 | 15 | return 103 | ) 104 | } 105 | renderArrowNext={(onClickHandler, hasNext, label) => 106 | hasNext && ( 107 | 110 | ) 111 | } 112 | renderIndicator={(onClickHandler, isSelected, index, label) => { 113 | if (isSelected) { 114 | return ( 115 |
  • 120 | ); 121 | } 122 | return ( 123 |
  • 134 | ); 135 | }} 136 | > 137 | {baseChildren.props.children} 138 | 139 | ); 140 | }; 141 | 142 | export const fixedWidth = () => {baseChildren.props.children}; 143 | export const noChildren = () => ; 144 | export const noImages = () => ( 145 | 146 |
    147 | Text 01 148 |
    149 |
    150 | Text 02 151 |
    152 |
    153 | ); 154 | 155 | export const dynamicHeightImages = () => ( 156 | 157 |
    158 | 159 |
    160 |
    161 | 162 |
    163 |
    164 | 165 |
    166 |
    167 | 168 |
    169 |
    170 | 171 |
    172 |
    173 | 174 |
    175 |
    176 | ); 177 | 178 | export const fade = () => ( 179 | 180 | {baseChildren.props.children} 181 | 182 | ); 183 | -------------------------------------------------------------------------------- /stories/02-advanced.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactPlayer from 'react-player'; 3 | import Carousel, { Props } from '../src/components/Carousel'; 4 | 5 | // carousel styles 6 | import '../src/main.scss'; 7 | import '../src/carousel.scss'; 8 | import '../src/examples/presentation/presentation.scss'; 9 | 10 | const createCarouselItemImage = (index, options = {}) => ( 11 |
    12 | 13 |

    Legend {index}

    14 |
    15 | ); 16 | 17 | const baseChildren =
    {[1, 2, 3, 4, 5].map(createCarouselItemImage)}
    ; 18 | 19 | export default { 20 | title: '02 - Advanced', 21 | component: Carousel, 22 | }; 23 | 24 | export const lazyLoaded = () => { 25 | class LazyLoadedCarousel extends Component<{}, { slides: Props['children'] }> { 26 | constructor(props) { 27 | super(props); 28 | 29 | this.state = { 30 | slides: null, 31 | }; 32 | 33 | this.loadSlides = this.loadSlides.bind(this); 34 | } 35 | 36 | loadSlides() { 37 | this.setState({ 38 | slides: baseChildren.props.children, 39 | }); 40 | } 41 | 42 | render() { 43 | return ( 44 |
    45 |

    Click the button to asynchronously load the slides

    46 | 49 | {this.state.slides} 50 |
    51 | ); 52 | } 53 | } 54 | 55 | return ; 56 | }; 57 | 58 | const YoutubeSlide = ({ url, isSelected }: { url: string; isSelected?: boolean }) => ( 59 | 60 | ); 61 | 62 | export const youtubeAutoplayWithCustomThumbs = () => { 63 | const customRenderItem = (item, props) => ; 64 | 65 | const getVideoThumb = (videoId) => `https://img.youtube.com/vi/${videoId}/default.jpg`; 66 | 67 | const getVideoId = (url) => url.substr('https://www.youtube.com/embed/'.length, url.length); 68 | 69 | const customRenderThumb = (children) => 70 | children.map((item) => { 71 | const videoId = getVideoId(item.props.url); 72 | return ; 73 | }); 74 | 75 | return ( 76 | 77 | 78 | 79 | 80 | 81 | 82 | ); 83 | }; 84 | 85 | export const withExternalControls = () => { 86 | class ExternalControlledCarousel extends Component<{}, { currentSlide: number; autoPlay: boolean }> { 87 | constructor(props) { 88 | super(props); 89 | 90 | this.state = { 91 | currentSlide: 0, 92 | autoPlay: true, 93 | }; 94 | } 95 | 96 | next = () => { 97 | this.setState((state) => ({ 98 | currentSlide: state.currentSlide + 1, 99 | })); 100 | }; 101 | 102 | prev = () => { 103 | this.setState((state) => ({ 104 | currentSlide: state.currentSlide - 1, 105 | })); 106 | }; 107 | 108 | changeAutoPlay = () => { 109 | this.setState((state) => ({ 110 | autoPlay: !state.autoPlay, 111 | })); 112 | }; 113 | 114 | updateCurrentSlide = (index) => { 115 | const { currentSlide } = this.state; 116 | 117 | if (currentSlide !== index) { 118 | this.setState({ 119 | currentSlide: index, 120 | }); 121 | } 122 | }; 123 | 124 | render() { 125 | const buttonStyle = { fontSize: 20, padding: '5px 20px', margin: '5px 0px' }; 126 | const containerStyle = { margin: '5px 0 20px' }; 127 | return ( 128 |
    129 |
    130 |

    131 | Use the buttons below to change the selected item in the carousel 132 |
    133 | 134 | 135 | Note that the external controls might not respect the carousel boundaries but the 136 | carousel won't go past it. 137 | 138 | 139 |

    140 |

    External slide value: {this.state.currentSlide}

    141 | 144 | 147 | 150 |
    151 | 157 | {baseChildren.props.children} 158 | 159 |
    160 | ); 161 | } 162 | } 163 | 164 | return ; 165 | }; 166 | 167 | export const presentationMode = () => ( 168 | 169 |
    170 |

    Presentation mode

    171 |
    172 |
    173 |

    It's just a couple of new styles...

    174 |
    175 |
    176 |

    ...and the carousel can be used to present something!

    177 |
    178 |
    179 | 180 |
    181 |
    182 |

    183 | See the source code... 184 |

    185 |
    186 |
    187 |

    It supports:

    188 |
      189 |
    • Headers (h1 - h6)
    • 190 |
    • Paragraphs (p)
    • 191 |
    • Images and videos (Youtube, Vimeo)
    • 192 |
    • 193 | Lists 194 |
        195 |
      1. Ordered lists (ol)
      2. 196 |
      3. Bullet points (ul)
      4. 197 |
      198 |
    • 199 |
    200 |
    201 |
    202 |

    Pre baked slides:

    203 |
      204 |
    • Primary - for titles
    • 205 |
    • Secondary - for subtitles
    • 206 |
    • Content
    • 207 |
    208 |
    209 |
    210 |