├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ ├── e2e.yml │ ├── npmpublish.yml │ ├── size.yml │ └── unit-test.yml ├── .gitignore ├── .npmignore ├── .storybook ├── main.js ├── manager.js ├── preview.js ├── style.css └── theme.js ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── __mocks__ ├── resize-observer-polyfill.js └── styleMock.js ├── __tests__ ├── fade.test.js ├── fade2.test.js ├── multiple-slides.test.js ├── slide.test.js ├── slide2.test.js ├── zoom.test.js └── zoom2.test.js ├── babel.config.js ├── cypress.config.ts ├── cypress ├── e2e │ └── slide │ │ ├── introduction.cy.js │ │ ├── slide.cy.js │ │ └── vertical.cy.js ├── fixtures │ └── example.json └── support │ ├── commands.ts │ ├── e2e.ts │ └── utils.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── css │ └── styles.css ├── fade.tsx ├── fadezoom.tsx ├── helpers.tsx ├── index.tsx ├── props.ts ├── slide.tsx ├── types.ts └── zoom.tsx ├── stories ├── CustomArrows.stories.mdx ├── CustomIndicators.stories.mdx ├── Fade.mdx ├── Fade.stories.tsx ├── Introduction.stories.mdx ├── Methods.mdx ├── Methods.stories.tsx ├── MultipleSlides.stories.mdx ├── Responsive.stories.mdx ├── Slide.stories.tsx ├── VerticalMode.stories.mdx ├── ZoomIn.mdx ├── ZoomIn.stories.tsx ├── ZoomOut.mdx └── ZoomOut.stories.tsx ├── test-utils.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | oot=true 2 | 3 | [*] 4 | trim_trailing_whitespace = true 5 | indent_style = space 6 | 7 | [*.tsx] 8 | indent_size = 4 -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = exports = { 2 | "rules": { 3 | "prettier/prettier": [ 4 | "error", 5 | { 6 | "endOfLine": "auto" 7 | }, 8 | ], 9 | } 10 | }; -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: E2E Tests 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | cypress-run: 7 | runs-on: macOS-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v4 11 | # Install NPM dependencies, cache them correctly 12 | # and run all Cypress tests 13 | - name: Cypress run 14 | uses: cypress-io/github-action@v6 15 | with: 16 | build: npm run build 17 | start: npm run storybook 18 | browser: chrome 19 | headed: true 20 | wait-on: http://localhost:6006 21 | config: viewportWidth=1000,viewportHeight=660 -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 18 15 | - run: npm install 16 | - run: npm test 17 | 18 | publish-npm: 19 | needs: test 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-node@v1 24 | with: 25 | node-version: 18 26 | registry-url: https://registry.npmjs.org/ 27 | - run: npm install 28 | - run: npm publish 29 | env: 30 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: [pull_request] 3 | jobs: 4 | size: 5 | runs-on: ubuntu-latest 6 | env: 7 | CI_JOB_NUMBER: 1 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: andresz1/size-limit-action@v1 11 | with: 12 | github_token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: Unit testing 2 | on: [pull_request] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['16.x', '18.x', '20.x'] 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v4 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | 25 | - name: Lint 26 | run: npm run lint 27 | 28 | - name: Test 29 | run: npm test --ci 30 | 31 | - name: Build 32 | run: npm run build 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /dist 12 | /public 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | /lib 25 | cypress/screenshots 26 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | public 2 | src 3 | scripts 4 | config 5 | coverage 6 | build 7 | .storybok 8 | stories 9 | __tests__ 10 | __mocks__ 11 | node_modules 12 | .circleci 13 | webpack.config.js 14 | webpack.config.dist.js 15 | LICENSE 16 | test-utils.js 17 | .github 18 | CODE_OF_CONDUCT.md 19 | cypress 20 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../stories/**/*.stories.@(ts|tsx|js|jsx|mdx)'], 3 | addons: [ 4 | '@storybook/addon-links', 5 | '@storybook/addon-essentials', 6 | '@storybook/addon-a11y' 7 | ], 8 | // https://storybook.js.org/docs/react/configure/typescript#mainjs-configuration 9 | typescript: { 10 | check: true, // type-check stories during Storybook build 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /.storybook/manager.js: -------------------------------------------------------------------------------- 1 | import { addons } from '@storybook/addons'; 2 | import theme from './theme'; 3 | 4 | addons.setConfig({ 5 | theme: theme 6 | }); -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | // https://storybook.js.org/docs/react/writing-stories/parameters#global-parameters 2 | /** library's css */ 3 | import '../src/css/styles.css'; 4 | /** storybook style */ 5 | import './style.css'; 6 | export const parameters = { 7 | // https://storybook.js.org/docs/react/essentials/actions#automatically-matching-args 8 | actions: { argTypesRegex: '^on.*' }, 9 | options: { 10 | storySort: { 11 | order: ['Introduction', 'Examples/Slide', 'Examples/Fade', 'Examples/Zoom', '*'], 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /.storybook/style.css: -------------------------------------------------------------------------------- 1 | .each-slide { 2 | display: flex; 3 | width: 100%; 4 | height: 400px; 5 | } 6 | 7 | .each-slide>div { 8 | width: 75%; 9 | } 10 | 11 | .each-slide>div img { 12 | width: 100%; 13 | height: 100%; 14 | object-fit: cover; 15 | } 16 | 17 | .each-slide p { 18 | width: 25%; 19 | font-size: 1em; 20 | display: flex; 21 | text-align: center; 22 | justify-content: center; 23 | align-items: center; 24 | margin: 0; 25 | background: #adceed; 26 | } 27 | 28 | 29 | .each-slide-effect > div { 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | background-size: cover; 34 | height: 350px; 35 | } 36 | 37 | .each-slide-effect span { 38 | padding: 20px; 39 | font-size: 20px; 40 | background: #efefef; 41 | text-align: center; 42 | } 43 | 44 | .indicator { 45 | cursor: pointer; 46 | padding: 10px; 47 | text-align: center; 48 | border: 1px #666 solid; 49 | margin: 0; 50 | } 51 | 52 | .indicator.active { 53 | color: #fff; 54 | background: #666; 55 | } -------------------------------------------------------------------------------- /.storybook/theme.js: -------------------------------------------------------------------------------- 1 | import { create } from '@storybook/theming'; 2 | export default create({ 3 | base: 'light', 4 | brandTitle: 'React Slideshow', 5 | brandUrl: 'https://react-slideshow-image.netlify.com', 6 | }); -------------------------------------------------------------------------------- /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 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at femidotexe@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Femi Oladeji 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-Slideshow 2 | 3 | [![Workflow](https://github.com/femioladeji/react-slideshow/actions/workflows/main.yml/badge.svg)](https://github.com/femioladeji/react-slideshow) 4 | [![codecov](https://codecov.io/gh/femioladeji/react-slideshow/branch/master/graph/badge.svg)](https://codecov.io/gh/femioladeji/react-slideshow) 5 | [![Package Quality](http://npm.packagequality.com/shield/react-slideshow-image.svg)](http://packagequality.com/#?package=react-slideshow-image) 6 | [![downloads](https://img.shields.io/npm/dm/react-slideshow-image.svg)](https://www.npmjs.com/package/react-slideshow-image) 7 | 8 | A simple slideshow component built with react that supports slide, fade and zoom effects. For full documentation click [here](https://react-slideshow-image.netlify.app/) 9 | 10 | ## Installation 11 | ``` 12 | npm install react-slideshow-image -S 13 | ``` 14 | 15 | ``` 16 | yarn add react-slideshow-image 17 | ``` 18 | 19 | You need to import the css style, you can do that by adding to the js file 20 | ```js 21 | import 'react-slideshow-image/dist/styles.css' 22 | 23 | ``` 24 | or to your css file 25 | ```css 26 | @import "react-slideshow-image/dist/styles.css"; 27 | 28 | ``` 29 | 30 | You can use three different effects of the slideshow. Check [examples](https://react-slideshow-image.netlify.app/) 31 | 32 | ## Slide Effect 33 | You can use this [playground](https://codesandbox.io/s/serene-lalande-yjmol) to tweak some values 34 | ```js 35 | import React from 'react'; 36 | import { Slide } from 'react-slideshow-image'; 37 | import 'react-slideshow-image/dist/styles.css' 38 | 39 | const spanStyle = { 40 | padding: '20px', 41 | background: '#efefef', 42 | color: '#000000' 43 | } 44 | 45 | const divStyle = { 46 | display: 'flex', 47 | alignItems: 'center', 48 | justifyContent: 'center', 49 | backgroundSize: 'cover', 50 | height: '400px' 51 | } 52 | const slideImages = [ 53 | { 54 | url: 'https://images.unsplash.com/photo-1509721434272-b79147e0e708?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80', 55 | caption: 'Slide 1' 56 | }, 57 | { 58 | url: 'https://images.unsplash.com/photo-1506710507565-203b9f24669b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1536&q=80', 59 | caption: 'Slide 2' 60 | }, 61 | { 62 | url: 'https://images.unsplash.com/photo-1536987333706-fc9adfb10d91?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80', 63 | caption: 'Slide 3' 64 | }, 65 | ]; 66 | 67 | const Slideshow = () => { 68 | return ( 69 |
70 | 71 | {slideImages.map((slideImage, index)=> ( 72 |
73 |
74 | {slideImage.caption} 75 |
76 |
77 | ))} 78 |
79 |
80 | ) 81 | } 82 | ``` 83 | 84 | ## Fade Effect 85 | You can use this [playground](https://codesandbox.io/s/admiring-wave-17e0j) to tweak some values 86 | ```js 87 | import React from 'react'; 88 | import { Fade } from 'react-slideshow-image'; 89 | import 'react-slideshow-image/dist/styles.css' 90 | 91 | const fadeImages = [ 92 | { 93 | url: 'https://images.unsplash.com/photo-1509721434272-b79147e0e708?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80', 94 | caption: 'First Slide' 95 | }, 96 | { 97 | url: 'https://images.unsplash.com/photo-1506710507565-203b9f24669b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1536&q=80', 98 | caption: 'Second Slide' 99 | }, 100 | { 101 | url: 'https://images.unsplash.com/photo-1536987333706-fc9adfb10d91?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80', 102 | caption: 'Third Slide' 103 | }, 104 | ]; 105 | 106 | const Slideshow = () => { 107 | return ( 108 |
109 | 110 | {fadeImages.map((fadeImage, index) => ( 111 |
112 | 113 |

{fadeImage.caption}

114 |
115 | ))} 116 |
117 |
118 | ) 119 | } 120 | ``` 121 | 122 | ## Zoom Effect 123 | You can use this [playground](https://codesandbox.io/s/priceless-bohr-ggirf) to tweak some values 124 | ```js 125 | import React from 'react'; 126 | import { Zoom } from 'react-slideshow-image'; 127 | import 'react-slideshow-image/dist/styles.css' 128 | 129 | const images = [ 130 | 'images/slide_2.jpg', 131 | 'images/slide_3.jpg', 132 | 'images/slide_4.jpg', 133 | 'images/slide_5.jpg', 134 | 'images/slide_6.jpg', 135 | 'images/slide_7.jpg' 136 | ]; 137 | 138 | const Slideshow = () => { 139 | return ( 140 |
141 | 142 | { 143 | images.map((each, index) => ) 144 | } 145 | 146 |
147 | ) 148 | } 149 | ``` 150 | 151 | ## Development 152 | If you want to run the app in development mode, you can run `npm start` to build the file in watch mode or `npm build` and then `npm pack` if you want to use it as a module in another project on your laptop. 153 | To run the storybook just run `npm run storybook` -------------------------------------------------------------------------------- /__mocks__/resize-observer-polyfill.js: -------------------------------------------------------------------------------- 1 | class ResizeObserver { 2 | observe() { 3 | // do nothing 4 | } 5 | unobserve() { 6 | // do nothing 7 | } 8 | } 9 | 10 | window.ResizeObserver = ResizeObserver; 11 | 12 | export default ResizeObserver; 13 | -------------------------------------------------------------------------------- /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | // __mocks__/styleMock.js 2 | 3 | module.exports = {}; -------------------------------------------------------------------------------- /__tests__/fade.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { cleanup, waitFor, fireEvent } from '@testing-library/react'; 3 | import { renderFade, images } from '../test-utils'; 4 | 5 | afterEach(cleanup); 6 | 7 | test('All children dom elements were loaded', () => { 8 | const { container } = renderFade(); 9 | const childrenElements = container.querySelectorAll( 10 | '.react-slideshow-fadezoom-images-wrap > div' 11 | ); 12 | expect(childrenElements.length).toEqual(images.length); 13 | }); 14 | 15 | test('The opacity and z-index of the first child are 1', () => { 16 | const { container } = renderFade(); 17 | const childrenElements = container.querySelectorAll( 18 | '.react-slideshow-fadezoom-images-wrap > div' 19 | ); 20 | expect(childrenElements[0].style.opacity).toBe('1'); 21 | expect(childrenElements[0].style.zIndex).toBe('1'); 22 | }); 23 | 24 | test('Left and right arrow navigation should show', () => { 25 | const { container } = renderFade(); 26 | let nav = container.querySelectorAll('.nav'); 27 | expect(nav.length).toEqual(2); 28 | }); 29 | 30 | test('indicators should not show since default value is false', () => { 31 | const { container } = renderFade(); 32 | let indicators = container.querySelectorAll('.indicators'); 33 | expect(indicators.length).toBe(0); 34 | }); 35 | 36 | const fadeProperties = { 37 | duration: 2000, 38 | transitionDuration: 200, 39 | indicators: true, 40 | arrows: false 41 | }; 42 | 43 | const fadeProperties2 = { 44 | duration: 2000, 45 | transitionDuration: 200, 46 | arrows: true 47 | }; 48 | 49 | test('Navigation arrows should not show if arrows props is false', () => { 50 | const { container } = renderFade(fadeProperties); 51 | let nav = container.querySelectorAll('.nav'); 52 | expect(nav.length).toBe(0); 53 | }); 54 | 55 | test('Nav arrow should be disabled on the first slide for infinite:false props', () => { 56 | const { container } = renderFade({ ...fadeProperties2, infinite: false }); 57 | let nav = container.querySelectorAll('.nav'); 58 | expect(nav[0].classList).toContain('disabled'); 59 | expect(nav[0].disabled).toBe(true); 60 | }); 61 | 62 | test("It shouldn't navigate if infinite false and previous arrow is clicked", async () => { 63 | const wrapperElement = document.createElement('div'); 64 | const { baseElement } = renderFade( 65 | { ...fadeProperties2, infinite: false, prevArrow:
Previous
}, 66 | wrapperElement 67 | ); 68 | const childrenElements = baseElement.querySelectorAll( 69 | '.react-slideshow-fadezoom-images-wrap > div' 70 | ); 71 | const nav = baseElement.querySelectorAll('.nav'); 72 | fireEvent.click(nav[0]); 73 | await waitFor( 74 | () => { 75 | expect(parseFloat(childrenElements[childrenElements.length - 1].style.opacity)).toBe(0); 76 | expect(parseFloat(childrenElements[0].style.opacity)).toBe(1); 77 | }, 78 | { timeout: fadeProperties2.transitionDuration } 79 | ); 80 | }); 81 | 82 | test("It shouldn't navigate to next if infinite false and next arrow is clicked on the last slide", async () => { 83 | const wrapperElement = document.createElement('div'); 84 | const { baseElement } = renderFade( 85 | { 86 | ...fadeProperties2, 87 | defaultIndex: 2, 88 | infinite: false, 89 | nextArrow:
Next
90 | }, 91 | wrapperElement 92 | ); 93 | const childrenElements = baseElement.querySelectorAll( 94 | '.react-slideshow-fadezoom-images-wrap > div' 95 | ); 96 | const nav = baseElement.querySelectorAll('.nav'); 97 | fireEvent.click(nav[1]); 98 | await waitFor( 99 | () => { 100 | expect(parseFloat(childrenElements[0].style.opacity)).toBe(0); 101 | expect(parseFloat(childrenElements[2].style.opacity)).toBe(1); 102 | }, 103 | { 104 | timeout: fadeProperties2.transitionDuration 105 | } 106 | ); 107 | }); 108 | 109 | test('It should show the previous image if back is clicked', async () => { 110 | const wrapperElement = document.createElement('div'); 111 | const { baseElement } = renderFade( 112 | { ...fadeProperties2, defaultIndex: 1 }, 113 | wrapperElement 114 | ); 115 | const childrenElements = baseElement.querySelectorAll( 116 | '.react-slideshow-fadezoom-images-wrap > div' 117 | ); 118 | const nav = baseElement.querySelectorAll('.nav'); 119 | fireEvent.click(nav[0]); 120 | await waitFor( 121 | () => { 122 | expect(Math.round(childrenElements[1].style.opacity)).toBe(0); 123 | expect(Math.round(childrenElements[0].style.opacity)).toBe(1); 124 | }, 125 | { timeout: fadeProperties2.transitionDuration } 126 | ); 127 | }); 128 | 129 | test('indciators should show with the exact number of children dots', () => { 130 | const { container } = renderFade(fadeProperties); 131 | let indicators = container.querySelectorAll('.indicators'); 132 | let dots = container.querySelectorAll('.indicators > li'); 133 | expect(indicators.length).toBe(1); 134 | expect(dots.length).toBe(images.length); 135 | }); 136 | 137 | test('When next or previous arrow is clicked, the right child shows up', async () => { 138 | const wrapperElement = document.createElement('div'); 139 | const { baseElement } = renderFade(fadeProperties2, wrapperElement); 140 | const childrenElements = baseElement.querySelectorAll( 141 | '.react-slideshow-fadezoom-images-wrap > div' 142 | ); 143 | const nav = baseElement.querySelectorAll('.nav'); 144 | fireEvent.click(nav[1]); 145 | await waitFor( 146 | () => { 147 | expect(parseFloat(childrenElements[1].style.opacity)).toBeGreaterThan(0); 148 | }, 149 | { timeout: fadeProperties2.transitionDuration } 150 | ); 151 | 152 | fireEvent.click(nav[0]); 153 | await waitFor( 154 | () => { 155 | expect(parseFloat(childrenElements[0].style.opacity)).toBeGreaterThan(0); 156 | }, 157 | { timeout: fadeProperties2.transitionDuration } 158 | ); 159 | }); 160 | 161 | test(`The second child should start transition to opacity after ${fadeProperties.duration}ms`, async () => { 162 | const { container } = renderFade(fadeProperties); 163 | await waitFor( 164 | () => { 165 | const childrenElements = container.querySelectorAll('.react-slideshow-fadezoom-images-wrap > div'); 166 | expect(parseFloat(childrenElements[1].style.opacity)).toBeGreaterThan(0); 167 | }, 168 | { timeout: fadeProperties.duration + fadeProperties.transitionDuration } 169 | ); 170 | }); 171 | 172 | test('When the pauseOnHover prop is true and the mouse hovers the container the slideshow stops', async () => { 173 | const wrapperElement = document.createElement('div'); 174 | const { baseElement } = renderFade( 175 | { ...fadeProperties, autoplay: true, pauseOnHover: true }, 176 | wrapperElement 177 | ); 178 | const childrenElements = baseElement.querySelectorAll( 179 | '.react-slideshow-fadezoom-images-wrap > div' 180 | ); 181 | 182 | fireEvent.mouseEnter(baseElement.querySelector('.react-slideshow-container')); 183 | // nothing happens on mouse enter 184 | await waitFor( 185 | () => { 186 | expect(Math.round(childrenElements[0].style.opacity)).toBe(1); 187 | expect(childrenElements[0].style.zIndex).toBe('1'); 188 | expect(Math.round(childrenElements[1].style.opacity)).toBe(0); 189 | expect(childrenElements[1].style.zIndex).toBe('0'); 190 | }, 191 | { timeout: fadeProperties.duration + fadeProperties.transitionDuration } 192 | ); 193 | fireEvent.mouseLeave(baseElement.querySelector('.react-slideshow-container')); 194 | // it resumes 195 | await waitFor( 196 | () => { 197 | expect(Math.round(childrenElements[0].style.opacity)).toBe(0); 198 | expect(Math.round(childrenElements[1].style.opacity)).toBe(1); 199 | }, 200 | { timeout: fadeProperties.duration + fadeProperties.transitionDuration } 201 | ); 202 | }); 203 | -------------------------------------------------------------------------------- /__tests__/fade2.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { cleanup, waitFor, fireEvent } from '@testing-library/react'; 3 | import { renderFade } from '../test-utils'; 4 | 5 | afterEach(cleanup); 6 | 7 | const options = { 8 | duration: 1000, 9 | transitionDuration: 50, 10 | indicators: true 11 | }; 12 | 13 | test('When the third indicator dot is clicked, the third child should show', async () => { 14 | const wrapperElement = document.createElement('div'); 15 | const { baseElement } = renderFade(options, wrapperElement); 16 | const dots = baseElement.querySelectorAll('.indicators li button'); 17 | fireEvent.click(dots[2]); 18 | await waitFor( 19 | () => { 20 | const childrenElements = baseElement.querySelectorAll( 21 | '.react-slideshow-fadezoom-images-wrap > div' 22 | ); 23 | expect(Math.round(childrenElements[2].style.opacity)).toBe(1); 24 | expect(childrenElements[2].style.zIndex).toBe('1'); 25 | }, 26 | { timeout: options.duration + options.transitionDuration + 100 } 27 | ); 28 | }); 29 | 30 | test('When the autoplay prop changes from false to true the slideshow plays again', async () => { 31 | const wrapperElement = document.createElement('div'); 32 | const { baseElement, rerender } = renderFade( 33 | { ...options, autoplay: false }, 34 | wrapperElement 35 | ); 36 | // nothing changes after duration and transitionDuration 37 | await waitFor( 38 | () => { 39 | const childrenElements = baseElement.querySelectorAll( 40 | '.react-slideshow-fadezoom-images-wrap > div' 41 | ); 42 | expect(Math.round(childrenElements[0].style.opacity)).toBe(1); 43 | expect(childrenElements[0].style.zIndex).toBe('1'); 44 | expect(Math.round(childrenElements[1].style.opacity)).toBe(0); 45 | expect(childrenElements[1].style.zIndex).toBe('0'); 46 | }, 47 | { timeout: options.duration + options.transitionDuration + 100 } 48 | ); 49 | renderFade({ ...options, autoplay: true }, false, rerender); 50 | await waitFor( 51 | () => { 52 | const childrenElements = baseElement.querySelectorAll( 53 | '.react-slideshow-fadezoom-images-wrap > div' 54 | ); 55 | expect(Math.round(childrenElements[0].style.opacity)).toBe(0); 56 | expect(Math.round(childrenElements[1].style.opacity)).toBe(1); 57 | }, 58 | { timeout: options.duration + options.transitionDuration + 100 } 59 | ); 60 | }); 61 | 62 | test('When the autoplay prop changes from true to false the slideshow stops', async () => { 63 | const wrapperElement = document.createElement('div'); 64 | const { baseElement, rerender } = renderFade( 65 | { ...options, autoplay: true }, 66 | wrapperElement 67 | ); 68 | // the slide plays since autoplay is true changes after duration and transitionDuration 69 | await waitFor( 70 | () => { 71 | const childrenElements = baseElement.querySelectorAll( 72 | '.react-slideshow-fadezoom-images-wrap > div' 73 | ); 74 | expect(Math.round(childrenElements[0].style.opacity)).toBe(0); 75 | expect(Math.round(childrenElements[1].style.opacity)).toBe(1); 76 | }, 77 | { timeout: options.duration + options.transitionDuration + 300 } 78 | ); 79 | renderFade({ ...options, autoplay: false }, wrapperElement, rerender); 80 | await waitFor( 81 | () => { 82 | const childrenElements = baseElement.querySelectorAll( 83 | '.react-slideshow-fadezoom-images-wrap > div' 84 | ); 85 | expect(Math.round(childrenElements[1].style.opacity)).toBe(1); 86 | expect(Math.round(childrenElements[2].style.opacity)).toBe(0); 87 | }, 88 | { timeout: options.duration + options.transitionDuration + 100 } 89 | ); 90 | }); 91 | 92 | test('When a valid defaultIndex prop is set, it shows that particular index first', () => { 93 | const wrapperElement = document.createElement('div'); 94 | const { baseElement } = renderFade( 95 | { ...options, defaultIndex: 1 }, 96 | wrapperElement 97 | ); 98 | const childrenElements = baseElement.querySelectorAll( 99 | '.react-slideshow-fadezoom-images-wrap > div' 100 | ); 101 | expect(parseInt(childrenElements[0].style.opacity)).toBe(0); 102 | expect(parseInt(childrenElements[1].style.opacity)).toBe(1); 103 | }); 104 | 105 | test('Custom prevArrow indicator can be set', async () => { 106 | const wrapperElement = document.createElement('div'); 107 | const { baseElement } = renderFade( 108 | { 109 | ...options, 110 | prevArrow:
Previous
111 | }, 112 | wrapperElement 113 | ); 114 | const childrenElements = baseElement.querySelectorAll( 115 | '.react-slideshow-fadezoom-images-wrap > div' 116 | ); 117 | expect(baseElement.querySelector('.previous')).toBeTruthy(); 118 | fireEvent.click(baseElement.querySelector('[data-type="prev"]')); 119 | await waitFor( 120 | () => { 121 | expect(Math.round(childrenElements[2].style.opacity)).toBe(1); 122 | }, 123 | { timeout: options.transitionDuration } 124 | ); 125 | }); 126 | 127 | test('Custom nextArrow indicator can be set', async () => { 128 | const wrapperElement = document.createElement('div'); 129 | const { baseElement } = renderFade( 130 | { 131 | ...options, 132 | nextArrow:
Next
133 | }, 134 | wrapperElement 135 | ); 136 | const childrenElements = baseElement.querySelectorAll( 137 | '.react-slideshow-fadezoom-images-wrap > div' 138 | ); 139 | expect(baseElement.querySelector('.next')).toBeTruthy(); 140 | fireEvent.click(baseElement.querySelector('[data-type="next"]')); 141 | await waitFor( 142 | () => { 143 | expect(Math.round(childrenElements[1].style.opacity)).toBe(1); 144 | }, 145 | { timeout: options.transitionDuration } 146 | ); 147 | }); 148 | 149 | test('shows custom indicators if it exists', () => { 150 | const wrapperElement = document.createElement('div'); 151 | const { baseElement } = renderFade( 152 | { 153 | ...options, 154 | indicators: index =>
{index + 1}
155 | }, 156 | wrapperElement 157 | ); 158 | const indicators = baseElement.querySelectorAll('.custom-indicator'); 159 | expect(indicators).toHaveLength(3); 160 | expect(indicators[0].innerHTML).toBe('1'); 161 | expect(indicators[1].innerHTML).toBe('2'); 162 | expect(indicators[2].innerHTML).toBe('3'); 163 | }); 164 | 165 | test('it calls onChange callback after every slide change', async () => { 166 | const wrapperElement = document.createElement('div'); 167 | const onChange = jest.fn(); 168 | const onStartChange = jest.fn(); 169 | const { baseElement } = renderFade( 170 | { 171 | ...options, 172 | onChange, 173 | onStartChange, 174 | autoplay: false 175 | }, 176 | wrapperElement 177 | ); 178 | const nav = baseElement.querySelectorAll('.nav'); 179 | fireEvent.click(nav[1]); 180 | await waitFor( 181 | () => { 182 | expect(onChange).toHaveBeenCalledWith(0, 1); 183 | expect(onStartChange).toHaveBeenCalledWith(0, 1); 184 | }, 185 | { timeout: options.transitionDuration + 50 } 186 | ); 187 | }); 188 | 189 | test('cssClass prop exists on element when it is passed', () => { 190 | const { container } = renderFade({ 191 | ...options, 192 | cssClass: 'myStyle' 193 | }); 194 | const wrapper = container.querySelector('.react-slideshow-fadezoom-wrapper'); 195 | expect(wrapper.classList).toContain('myStyle'); 196 | }); 197 | -------------------------------------------------------------------------------- /__tests__/multiple-slides.test.js: -------------------------------------------------------------------------------- 1 | import { cleanup, waitFor, fireEvent } from '@testing-library/react'; 2 | import { renderSlide, images } from '../test-utils'; 3 | 4 | const options = { 5 | duration: 1000, 6 | transitionDuration: 50, 7 | infinite: true, 8 | indicators: false, 9 | autoplay: false, 10 | slidesToShow: 2 11 | }; 12 | 13 | afterEach(cleanup); 14 | 15 | test('It adds preceeding and trailing slides based on number of slides to show', () => { 16 | const { container } = renderSlide(options); 17 | const childrenElements = container.querySelectorAll('.images-wrap > div'); 18 | expect(childrenElements.length).toEqual( 19 | images.length + options.slidesToShow * 2 20 | ); 21 | }); 22 | 23 | test('It shows 2 slides on the first page', () => { 24 | const { container } = renderSlide(options); 25 | const activeChildren = container.querySelectorAll( 26 | '.images-wrap > div.active' 27 | ); 28 | const allChildren = container.querySelectorAll('.images-wrap > div'); 29 | expect(activeChildren.length).toEqual(options.slidesToShow); 30 | // the first 2 are preceeding slides that's why the index 2 & 3 are the active ones 31 | expect(allChildren[2].classList).toContain('active'); 32 | expect(allChildren[3].classList).toContain('active'); 33 | }); 34 | 35 | test('it uses the default value of slideToScroll (1) if prop is not passed', async () => { 36 | const { container } = renderSlide(options); 37 | const nav = container.querySelectorAll('.nav'); 38 | const allChildren = container.querySelectorAll('.images-wrap > div'); 39 | fireEvent.click(nav[1]); 40 | await waitFor( 41 | () => { 42 | expect(allChildren[3].classList).toContain('active'); 43 | expect(allChildren[4].classList).toContain('active'); 44 | }, 45 | { timeout: options.transitionDuration + 50 } 46 | ); 47 | }); 48 | -------------------------------------------------------------------------------- /__tests__/slide.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { cleanup, waitFor, fireEvent } from '@testing-library/react'; 3 | import { renderSlide, images } from '../test-utils'; 4 | 5 | const options = { 6 | duration: 1000, 7 | transitionDuration: 50, 8 | infinite: true, 9 | indicators: true 10 | }; 11 | 12 | afterEach(cleanup); 13 | 14 | test('All slide children dom elements were loaded, the first and last are loaded twice', () => { 15 | const { container } = renderSlide(options); 16 | const childrenElements = container.querySelectorAll('.images-wrap > div'); 17 | expect(childrenElements.length).toEqual(images.length + 2); 18 | }); 19 | 20 | test('indciators should show with the exact number of children dots', () => { 21 | const { container } = renderSlide(options); 22 | let indicators = container.querySelectorAll('.indicators'); 23 | let dots = container.querySelectorAll('.indicators > li'); 24 | expect(indicators.length).toBe(1); 25 | expect(dots.length).toBe(images.length); 26 | }); 27 | 28 | test('Navigation arrows should show if not specified', () => { 29 | const { container } = renderSlide(options); 30 | let nav = container.querySelectorAll('.nav'); 31 | expect(nav.length).toBe(2); 32 | }); 33 | 34 | test('Previous navigation array should be disabled if infinite option is false', async () => { 35 | const { baseElement } = renderSlide({ 36 | ...options, 37 | infinite: false, 38 | prevArrow:
previous
39 | }); 40 | let nav = baseElement.querySelectorAll('.nav'); 41 | expect(nav[0].classList).toContain('disabled'); 42 | fireEvent.click(nav[0]); 43 | await waitFor( 44 | () => { 45 | expect(baseElement.querySelector('[data-index="0"]').classList).toContain( 46 | 'active' 47 | ); 48 | }, 49 | { timeout: options.transitionDuration } 50 | ); 51 | }); 52 | 53 | test('When next is clicked, the second child should have an active class', async () => { 54 | const wrapperElement = document.createElement('div'); 55 | const { baseElement } = renderSlide(options, wrapperElement); 56 | const childrenElements = baseElement.querySelectorAll('.images-wrap > div'); 57 | const nav = baseElement.querySelectorAll('.nav'); 58 | fireEvent.click(nav[1]); 59 | await waitFor( 60 | () => { 61 | expect(childrenElements[1].classList).toContain('active'); 62 | }, 63 | { timeout: options.transitionDuration } 64 | ); 65 | }); 66 | 67 | test("If infinite is false, it doesn't render extra slides", () => { 68 | const wrapperElement = document.createElement('div'); 69 | const { baseElement } = renderSlide( 70 | { 71 | ...options, 72 | infinite: false, 73 | defaultIndex: 2, 74 | nextArrow:
Next
75 | }, 76 | wrapperElement 77 | ); 78 | const childrenElements = baseElement.querySelectorAll('.images-wrap > div'); 79 | expect(childrenElements).toHaveLength(3); 80 | }); 81 | 82 | test('If infinite is false and next is clicked on the last image everything should remain the same', async () => { 83 | const wrapperElement = document.createElement('div'); 84 | const { baseElement } = renderSlide( 85 | { 86 | ...options, 87 | infinite: false, 88 | defaultIndex: 2, 89 | nextArrow:
Next
90 | }, 91 | wrapperElement 92 | ); 93 | const childrenElements = baseElement.querySelectorAll('.images-wrap > div'); 94 | const nav = baseElement.querySelectorAll('.nav'); 95 | fireEvent.click(nav[1]); 96 | await waitFor( 97 | () => { 98 | expect(childrenElements[2].classList).toContain('active'); 99 | }, 100 | { timeout: options.transitionDuration } 101 | ); 102 | }); 103 | 104 | test('When back is clicked, the third child should have an active class', async () => { 105 | const wrapperElement = document.createElement('div'); 106 | const { baseElement } = renderSlide(options, wrapperElement); 107 | const nav = baseElement.querySelectorAll('.nav'); 108 | fireEvent.click(nav[0]); 109 | await waitFor(() => { 110 | const childrenElements = baseElement.querySelectorAll('.images-wrap > div'); 111 | // index 3 was used because there are two extra divs, one at the beginning and end 112 | expect(childrenElements[3].classList).toContain('active'); 113 | }, { timeout: options.transitionDuration + 80 }); 114 | }); 115 | 116 | test('It should automatically show second child after first slide', async () => { 117 | const wrapperElement = document.createElement('div'); 118 | const { baseElement } = renderSlide(options, wrapperElement); 119 | await waitFor( 120 | () => { 121 | const childrenElements = baseElement.querySelectorAll( 122 | '.images-wrap > div' 123 | ); 124 | expect(childrenElements[2].classList).toContain('active'); 125 | }, 126 | { 127 | timeout: options.duration + options.transitionDuration + 700 128 | } 129 | ); 130 | }); 131 | 132 | test('When the pauseOnHover prop is true and the mouse hovers the container the slideshow stops', async () => { 133 | const wrapperElement = document.createElement('div'); 134 | const { baseElement } = renderSlide( 135 | { ...options, autoplay: true, pauseOnHover: true }, 136 | wrapperElement 137 | ); 138 | const childrenElements = baseElement.querySelectorAll('.images-wrap > div'); 139 | 140 | fireEvent.mouseEnter(baseElement.querySelector('.react-slideshow-container')); 141 | await waitFor( 142 | () => { 143 | expect(childrenElements[1].classList).toContain('active'); 144 | }, 145 | { 146 | timeout: options.duration + options.transitionDuration 147 | } 148 | ); 149 | fireEvent.mouseLeave(baseElement.querySelector('.react-slideshow-container')); 150 | await waitFor( 151 | () => { 152 | expect(childrenElements[2].classList).toContain('active'); 153 | }, 154 | { 155 | timeout: options.duration + options.transitionDuration + 1000 156 | } 157 | ); 158 | }); 159 | -------------------------------------------------------------------------------- /__tests__/slide2.test.js: -------------------------------------------------------------------------------- 1 | import { cleanup, fireEvent, waitFor } from '@testing-library/react'; 2 | import React from 'react'; 3 | import { renderSlide } from '../test-utils'; 4 | 5 | const options = { 6 | duration: 1000, 7 | transitionDuration: 50, 8 | infinite: true, 9 | indicators: true 10 | }; 11 | 12 | afterEach(cleanup); 13 | 14 | test('When the second indicator is clicked, the third child should have active class', async () => { 15 | const wrapperElement = document.createElement('div'); 16 | const onChange = jest.fn(); 17 | const onStartChange = jest.fn(); 18 | const { baseElement } = renderSlide( 19 | { ...options, autoplay: false, onChange, onStartChange }, 20 | wrapperElement 21 | ); 22 | let dots = baseElement.querySelectorAll('.indicators li button'); 23 | const childrenElements = baseElement.querySelectorAll('.images-wrap > div'); 24 | fireEvent.click(dots[1]); 25 | await waitFor( 26 | () => { 27 | expect(childrenElements[2].classList).toContain('active'); 28 | expect(onStartChange).toBeCalledWith(0, 1); 29 | expect(onChange).toBeCalledWith(0, 1); 30 | }, 31 | { 32 | timeout: options.transitionDuration + options.duration 33 | } 34 | ); 35 | }); 36 | 37 | test('When the autoplay prop changes from false to true the slideshow plays again', async () => { 38 | const wrapperElement = document.createElement('div'); 39 | const { baseElement, rerender } = renderSlide( 40 | { ...options, autoplay: false }, 41 | wrapperElement 42 | ); 43 | // nothing changes after duration and transitionDuration 44 | await waitFor( 45 | () => { 46 | const childrenElements = baseElement.querySelectorAll( 47 | '.images-wrap > div' 48 | ); 49 | expect(childrenElements[1].classList).toContain('active'); 50 | }, 51 | { 52 | timeout: options.duration + options.transitionDuration 53 | } 54 | ); 55 | renderSlide({ ...options, autoplay: true }, false, rerender); 56 | await waitFor( 57 | () => { 58 | const childrenElements = baseElement.querySelectorAll( 59 | '.images-wrap > div' 60 | ); 61 | expect(childrenElements[2].classList).toContain('active'); 62 | }, 63 | { 64 | timeout: options.duration + options.transitionDuration + 1000 65 | } 66 | ); 67 | }); 68 | 69 | test('When the autoplay prop changes from true to false the slideshow stops', async () => { 70 | const wrapperElement = document.createElement('div'); 71 | const { baseElement, rerender } = renderSlide( 72 | { ...options, autoplay: true }, 73 | wrapperElement 74 | ); 75 | // the slide plays since autoplay is true changes after duration and transitionDuration 76 | await waitFor( 77 | () => { 78 | const childrenElements = baseElement.querySelectorAll( 79 | '.images-wrap > div' 80 | ); 81 | expect(childrenElements[2].classList).toContain('active'); 82 | }, 83 | { 84 | timeout: options.duration + options.transitionDuration + 1000 85 | } 86 | ); 87 | renderSlide({ ...options, autoplay: false }, false, rerender); 88 | await waitFor( 89 | () => { 90 | const childrenElements = baseElement.querySelectorAll( 91 | '.images-wrap > div' 92 | ); 93 | expect(childrenElements[2].classList).toContain('active'); 94 | expect(childrenElements[3].classList.contains('active')).toBeFalsy(); 95 | }, 96 | { 97 | timeout: options.duration + options.transitionDuration 98 | } 99 | ); 100 | }); 101 | 102 | test('When a valid defaultIndex prop is set, it shows that particular index first', () => { 103 | const wrapperElement = document.createElement('div'); 104 | const { baseElement } = renderSlide( 105 | { ...options, defaultIndex: 1 }, 106 | wrapperElement 107 | ); 108 | const childrenElements = baseElement.querySelectorAll('.images-wrap > div'); 109 | expect(childrenElements[2].classList).toContain('active'); 110 | }); 111 | 112 | test('shows custom indicators if it exists', () => { 113 | const wrapperElement = document.createElement('div'); 114 | const { baseElement } = renderSlide( 115 | { 116 | ...options, 117 | indicators: index =>
{index + 1}
118 | }, 119 | wrapperElement 120 | ); 121 | const indicators = baseElement.querySelectorAll('.custom-indicator'); 122 | expect(indicators).toHaveLength(3); 123 | expect(indicators[0].innerHTML).toBe('1'); 124 | expect(indicators[1].innerHTML).toBe('2'); 125 | expect(indicators[2].innerHTML).toBe('3'); 126 | }); 127 | 128 | test('Custom nextArrow indicator can be set', async () => { 129 | const wrapperElement = document.createElement('div'); 130 | const { baseElement } = renderSlide( 131 | { 132 | ...options, 133 | nextArrow:
Next
134 | }, 135 | wrapperElement 136 | ); 137 | expect(baseElement.querySelector('.next')).toBeTruthy(); 138 | fireEvent.click(baseElement.querySelector('[data-type="next"]')); 139 | await waitFor( 140 | () => { 141 | expect(baseElement.querySelector('[data-index="1"]').classList).toContain('active'); 142 | }, 143 | { timeout: options.transitionDuration + 50 } 144 | ); 145 | }); 146 | 147 | test('Custom prevArrow indicator can be set', async () => { 148 | const wrapperElement = document.createElement('div'); 149 | const { baseElement } = renderSlide( 150 | { 151 | ...options, 152 | prevArrow:
Previous
153 | }, 154 | wrapperElement 155 | ); 156 | expect(baseElement.querySelector('.previous')).toBeTruthy(); 157 | fireEvent.click(baseElement.querySelector('[data-type="prev"]')); 158 | await waitFor( 159 | () => { 160 | expect(baseElement.querySelector('[data-index="2"]').classList).toContain('active'); 161 | }, 162 | { timeout: options.transitionDuration + 50 } 163 | ); 164 | }); 165 | 166 | test('it calls onChange callback after every slide change', async () => { 167 | const wrapperElement = document.createElement('div'); 168 | const mockFunction = jest.fn(); 169 | const { baseElement } = renderSlide( 170 | { 171 | ...options, 172 | onChange: mockFunction, 173 | autoplay: false 174 | }, 175 | wrapperElement 176 | ); 177 | const nav = baseElement.querySelectorAll('.nav'); 178 | 179 | fireEvent.click(nav[1]); 180 | await waitFor( 181 | () => { 182 | expect(mockFunction).toHaveBeenCalledWith(0, 1); 183 | }, 184 | { timeout: options.transitionDuration + 50 } 185 | ); 186 | }); 187 | 188 | test('cssClass prop exists on element when it is passed', () => { 189 | const { container } = renderSlide({ 190 | ...options, 191 | cssClass: 'myStyle' 192 | }); 193 | let wrapper = container.querySelector('.react-slideshow-wrapper, .slide'); 194 | expect(wrapper.classList).toContain('myStyle'); 195 | }); 196 | -------------------------------------------------------------------------------- /__tests__/zoom.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { cleanup, waitFor, fireEvent } from '@testing-library/react'; 3 | import { renderZoom, renderZoom2, images } from '../test-utils'; 4 | 5 | afterEach(cleanup); 6 | 7 | const zoomOut = { 8 | duration: 1000, 9 | transitionDuration: 50, 10 | indicators: true, 11 | scale: 0.4 12 | }; 13 | 14 | test('All children dom elements were loaded', () => { 15 | const { container } = renderZoom(zoomOut); 16 | const childrenElements = container.querySelectorAll('.react-slideshow-fadezoom-images-wrap > div'); 17 | expect(childrenElements.length).toEqual(images.length); 18 | }); 19 | 20 | test('indciators should show with the exact number of children dots', () => { 21 | const { container } = renderZoom(zoomOut); 22 | let indicators = container.querySelectorAll('.indicators'); 23 | let dots = container.querySelectorAll('.indicators li button'); 24 | expect(indicators.length).toBe(1); 25 | expect(dots.length).toBe(images.length); 26 | }); 27 | 28 | test('Navigation arrows should show if not specified', () => { 29 | const { container } = renderZoom(zoomOut); 30 | let nav = container.querySelectorAll('.nav'); 31 | expect(nav.length).toBe(2); 32 | }); 33 | 34 | test('Navigation arrows should not show', () => { 35 | const { container } = renderZoom({ ...zoomOut, arrows: false }); 36 | let nav = container.querySelectorAll('.nav'); 37 | expect(nav.length).toBe(0); 38 | }); 39 | 40 | test('Nav arrow should be disabled on the first slide for infinite:false props', () => { 41 | const { container } = renderZoom({ ...zoomOut, infinite: false }); 42 | let nav = container.querySelectorAll('.nav'); 43 | expect(nav[0].classList).toContain('disabled'); 44 | expect(nav[0].disabled).toBe(true); 45 | }); 46 | 47 | test("It shouldn't navigate if infinite false and previous arrow is clicked", async () => { 48 | const wrapperElement = document.createElement('div'); 49 | const { baseElement } = renderZoom( 50 | { ...zoomOut, infinite: false, arrows: true }, 51 | wrapperElement 52 | ); 53 | const childrenElements = baseElement.querySelectorAll('.react-slideshow-fadezoom-images-wrap > div'); 54 | const nav = baseElement.querySelectorAll('.nav'); 55 | fireEvent.click(nav[0]); 56 | await waitFor( 57 | () => { 58 | expect(parseFloat(childrenElements[childrenElements.length - 1].style.opacity)).toBe(0); 59 | expect(parseFloat(childrenElements[0].style.opacity)).toBe(1); 60 | }, 61 | { 62 | timeout: zoomOut.transitionDuration 63 | } 64 | ); 65 | }); 66 | 67 | test("It shouldn't navigate if infinite false and next arrow is clicked on the last slide", async () => { 68 | const wrapperElement = document.createElement('div'); 69 | const { baseElement } = renderZoom2( 70 | { 71 | ...zoomOut, 72 | infinite: false, 73 | arrows: true, 74 | defaultIndex: 1, 75 | autoplay: false, 76 | }, 77 | wrapperElement 78 | ); 79 | const childrenElements = baseElement.querySelectorAll('.react-slideshow-fadezoom-images-wrap > div'); 80 | const nav = baseElement.querySelectorAll('.nav'); 81 | fireEvent.click(nav[1]); 82 | await waitFor( 83 | () => { 84 | expect(parseFloat(childrenElements[0].style.opacity)).toBe(0); 85 | expect(parseFloat(childrenElements[childrenElements.length - 1].style.opacity)).toBe(1); 86 | }, 87 | { 88 | timeout: zoomOut.transitionDuration 89 | } 90 | ); 91 | }); 92 | 93 | test(`The second child should start transition to opacity and zIndex of 1 after ${zoomOut.duration}ms`, async () => { 94 | const onChange = jest.fn(); 95 | const { container } = renderZoom({ ...zoomOut, onChange }); 96 | await waitFor( 97 | () => { 98 | const childrenElements = container.querySelectorAll( 99 | '.react-slideshow-fadezoom-images-wrap > div' 100 | ); 101 | expect(parseFloat(childrenElements[1].style.opacity)).toBeGreaterThan(0); 102 | expect(onChange).toBeCalledWith(0, 1); 103 | expect(childrenElements[1].style.zIndex).toBe('1'); 104 | }, 105 | { 106 | timeout: zoomOut.duration + zoomOut.transitionDuration + 1000 107 | } 108 | ); 109 | }); 110 | 111 | test('When the pauseOnHover prop is true and the mouse hovers the container the slideshow stops', async () => { 112 | const wrapperElement = document.createElement('div'); 113 | const { baseElement } = renderZoom( 114 | { ...zoomOut, autoplay: true, pauseOnHover: true }, 115 | wrapperElement 116 | ); 117 | const childrenElements = baseElement.querySelectorAll('.react-slideshow-fadezoom-images-wrap > div'); 118 | 119 | fireEvent.mouseEnter(baseElement.querySelector('.react-slideshow-container')); 120 | await waitFor( 121 | () => { 122 | expect(parseFloat(childrenElements[0].style.opacity)).toBe(1); 123 | expect(parseFloat(childrenElements[1].style.opacity)).toBe(0); 124 | expect(parseFloat(childrenElements[childrenElements.length - 1].style.opacity)).toBe(0); 125 | }, 126 | { 127 | timeout: zoomOut.duration + zoomOut.transitionDuration 128 | } 129 | ); 130 | fireEvent.mouseLeave(baseElement.querySelector('.react-slideshow-container')); 131 | await waitFor( 132 | () => { 133 | expect(Math.round(childrenElements[0].style.opacity)).toBe(0); 134 | expect(Math.round(childrenElements[1].style.opacity)).toBe(1); 135 | expect(Math.round(childrenElements[childrenElements.length - 1].style.opacity)).toBe(0); 136 | }, 137 | { 138 | timeout: zoomOut.duration + zoomOut.transitionDuration + 100 139 | } 140 | ); 141 | }); 142 | -------------------------------------------------------------------------------- /__tests__/zoom2.test.js: -------------------------------------------------------------------------------- 1 | import { cleanup, waitFor, fireEvent } from '@testing-library/react'; 2 | import React from 'react'; 3 | import { renderZoom, renderZoom2 } from '../test-utils'; 4 | 5 | afterEach(cleanup); 6 | 7 | const zoomOut = { 8 | duration: 1000, 9 | transitionDuration: 50, 10 | indicators: true, 11 | scale: 0.4 12 | }; 13 | 14 | test('Clicking on the indicator should show the right slide', async () => { 15 | const wrapperElement = document.createElement('div'); 16 | const { baseElement } = renderZoom2( 17 | { ...zoomOut, infinite: false, autoplay: false }, 18 | wrapperElement 19 | ); 20 | const childrenElements = baseElement.querySelectorAll('.react-slideshow-fadezoom-images-wrap > div'); 21 | const indicators = baseElement.querySelectorAll('.indicators li button'); 22 | fireEvent.click(indicators[1]); 23 | await waitFor( 24 | () => { 25 | expect(parseFloat(Math.round(childrenElements[1].style.opacity))).toBe(1); 26 | }, 27 | { timeout: zoomOut.transitionDuration + 10 } 28 | ); 29 | }); 30 | 31 | test('When the autoplay prop changes from false to true the slideshow plays again', async () => { 32 | const wrapperElement = document.createElement('div'); 33 | const { baseElement, rerender } = renderZoom2( 34 | { ...zoomOut, autoplay: false }, 35 | wrapperElement 36 | ); 37 | // nothing changes after duration and transitionDuration 38 | await waitFor( 39 | () => { 40 | const childrenElements = baseElement.querySelectorAll( 41 | '.react-slideshow-fadezoom-images-wrap > div' 42 | ); 43 | expect(parseFloat(childrenElements[0].style.opacity)).toBe(1); 44 | }, 45 | { 46 | timeout: zoomOut.duration + zoomOut.transitionDuration 47 | } 48 | ); 49 | renderZoom2({ ...zoomOut, autoplay: true }, false, rerender); 50 | await waitFor( 51 | () => { 52 | const childrenElements = baseElement.querySelectorAll( 53 | '.react-slideshow-fadezoom-images-wrap > div' 54 | ); 55 | expect(Math.round(childrenElements[1].style.opacity)).toBe(1); 56 | }, 57 | { 58 | timeout: zoomOut.duration + zoomOut.transitionDuration + 100 59 | } 60 | ); 61 | }); 62 | 63 | test('When the autoplay prop changes from true to false the slideshow stops', async () => { 64 | const wrapperElement = document.createElement('div'); 65 | const { baseElement, rerender } = renderZoom2( 66 | { ...zoomOut, autoplay: true }, 67 | wrapperElement 68 | ); 69 | // the slide plays since autoplay is true changes after duration and transitionDuration 70 | await waitFor( 71 | () => { 72 | const childrenElements = baseElement.querySelectorAll( 73 | '.react-slideshow-fadezoom-images-wrap > div' 74 | ); 75 | expect(Math.round(childrenElements[1].style.opacity)).toBe(1); 76 | }, 77 | { 78 | timeout: zoomOut.duration + zoomOut.transitionDuration + 100 79 | } 80 | ); 81 | renderZoom2({ ...zoomOut, autoplay: false }, false, rerender); 82 | await waitFor( 83 | () => { 84 | const childrenElements = baseElement.querySelectorAll( 85 | '.react-slideshow-fadezoom-images-wrap > div' 86 | ); 87 | expect(Math.round(childrenElements[1].style.opacity)).toBe(1); 88 | }, 89 | { 90 | timeout: zoomOut.duration + zoomOut.transitionDuration + 100 91 | } 92 | ); 93 | }); 94 | 95 | test('When a valid defaultIndex prop is set, it shows that particular index first', () => { 96 | const wrapperElement = document.createElement('div'); 97 | const { baseElement } = renderZoom2( 98 | { ...zoomOut, defaultIndex: 1 }, 99 | wrapperElement 100 | ); 101 | const childrenElements = baseElement.querySelectorAll('.react-slideshow-fadezoom-images-wrap > div'); 102 | expect(parseInt(childrenElements[0].style.opacity)).toBe(0); 103 | expect(parseInt(childrenElements[1].style.opacity)).toBe(1); 104 | }); 105 | 106 | test('shows custom indicators if it exists', () => { 107 | const wrapperElement = document.createElement('div'); 108 | const { baseElement } = renderZoom2( 109 | { 110 | ...zoomOut, 111 | indicators: index =>
{index + 1}
112 | }, 113 | wrapperElement 114 | ); 115 | const indicators = baseElement.querySelectorAll('.custom-indicator'); 116 | expect(indicators).toHaveLength(2); 117 | expect(indicators[0].innerHTML).toBe('1'); 118 | expect(indicators[1].innerHTML).toBe('2'); 119 | }); 120 | 121 | test('Custom nextArrow indicator can be set', async () => { 122 | const wrapperElement = document.createElement('div'); 123 | const { baseElement } = renderZoom( 124 | { 125 | ...zoomOut, 126 | nextArrow:
Next
127 | }, 128 | wrapperElement 129 | ); 130 | expect(baseElement.querySelector('.next')).toBeTruthy(); 131 | fireEvent.click(baseElement.querySelector('[data-type="next"]')); 132 | await waitFor( 133 | () => { 134 | expect( 135 | Math.round(baseElement.querySelector('[data-index="1"]').style.opacity) 136 | ).toBe(1); 137 | }, 138 | { 139 | timeout: zoomOut.transitionDuration 140 | } 141 | ); 142 | }); 143 | 144 | test('Custom prevArrow indicator can be set', async () => { 145 | const wrapperElement = document.createElement('div'); 146 | const { baseElement } = renderZoom( 147 | { 148 | ...zoomOut, 149 | prevArrow:
Previous
150 | }, 151 | wrapperElement 152 | ); 153 | expect(baseElement.querySelector('.previous')).toBeTruthy(); 154 | fireEvent.click(baseElement.querySelector('[data-type="prev"]')); 155 | await waitFor( 156 | () => { 157 | expect( 158 | Math.round(baseElement.querySelector('[data-index="2"]').style.opacity) 159 | ).toBe(1); 160 | }, 161 | { 162 | timeout: zoomOut.transitionDuration 163 | } 164 | ); 165 | }); 166 | 167 | test('it calls onChange callback after every slide change', async () => { 168 | const wrapperElement = document.createElement('div'); 169 | const onChange = jest.fn(); 170 | const onStartChange = jest.fn(); 171 | const { baseElement } = renderZoom( 172 | { 173 | ...zoomOut, 174 | onChange, 175 | onStartChange, 176 | autoplay: false 177 | }, 178 | wrapperElement 179 | ); 180 | const nav = baseElement.querySelectorAll('.nav'); 181 | 182 | fireEvent.click(nav[1]); 183 | await waitFor( 184 | () => { 185 | expect(onChange).toHaveBeenCalledWith(0, 1); 186 | expect(onStartChange).toHaveBeenCalledWith(0, 1); 187 | }, 188 | { timeout: zoomOut.transitionDuration + 50 } 189 | ); 190 | }); 191 | 192 | test('cssClass prop exists on element when it is passed', () => { 193 | const { container } = renderZoom2({ 194 | ...zoomOut, 195 | cssClass: 'myStyle' 196 | }); 197 | let wrapper = container.querySelector('.react-slideshow-fadezoom-wrapper'); 198 | expect(wrapper.classList).toContain('myStyle'); 199 | }); 200 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = api => { 2 | const isTest = api.env('test'); 3 | if (isTest) { 4 | return { 5 | presets: [ 6 | ['@babel/preset-env', { 7 | targets: { 8 | node: 'current' 9 | } 10 | }], '@babel/preset-react'] 11 | }; 12 | } else { 13 | return { 14 | presets: [ 15 | '@babel/preset-env', 16 | '@babel/preset-react' 17 | ] 18 | }; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | 3 | export default defineConfig({ 4 | viewportWidth: 1000, 5 | chromeWebSecurity: false, 6 | e2e: { 7 | setupNodeEvents(on, config) { 8 | // implement node event listeners here 9 | on('task', { 10 | log(message) { 11 | console.log(message) 12 | 13 | return null 14 | }, 15 | }) 16 | }, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /cypress/e2e/slide/introduction.cy.js: -------------------------------------------------------------------------------- 1 | import { getSlideshowWidth, translateXRegex } from '../../support/utils'; 2 | 3 | describe('introduction slide functionality', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:6006'); 6 | cy.frameLoaded("#storybook-preview-iframe"); 7 | cy.wait(2000); 8 | cy.iframe('#storybook-preview-iframe') 9 | .find('.react-slideshow-container') 10 | .as("slide") 11 | }) 12 | 13 | it('loads the introduction slide and the slide with slide 1', async () => { 14 | cy.get('@slide').should('exist'); 15 | cy.get('@slide').find('.images-wrap').should('have.class', 'horizontal'); 16 | const width = await getSlideshowWidth(); 17 | cy.get('@slide').invoke('width').should('gt', 0); 18 | cy.get('@slide').find('.images-wrap > div').should('have.length', 5); 19 | cy.get('@slide').find('.images-wrap').should('have.css', 'width').and('match', new RegExp(`${width * 5}px`)) 20 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateXRegex(`-${width}`)) 21 | cy.get('@slide').find('.images-wrap > div:nth-of-type(2)').should('have.class', 'active') 22 | cy.get('@slide').find('.images-wrap > div:nth-of-type(2)').should('have.css', 'width').and('match', new RegExp(`${width}px`)) 23 | }); 24 | 25 | it('loads the next slides when the next button is clicked', async () => { 26 | cy.get('@slide').should('exist'); 27 | const width = await getSlideshowWidth(); 28 | cy.get('@slide').invoke('width').should('gt', 0); 29 | cy.get('@slide').find('.nav:last-of-type').click(); 30 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateXRegex(-2 * width)); 31 | cy.get('@slide').find('.nav:last-of-type').click(); 32 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateXRegex(-3 * width)); 33 | cy.get('@slide').find('.nav:last-of-type').click(); 34 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateXRegex(-1 * width)); 35 | }); 36 | 37 | it('loads the previous slides when the previous button is clicked', async () => { 38 | cy.get('@slide').should('exist'); 39 | const width = await getSlideshowWidth(); 40 | cy.get('@slide').invoke('width').should('gt', 0); 41 | cy.get('@slide').find('.nav:first-of-type').click(); 42 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateXRegex(-3 * width)); 43 | cy.get('@slide').find('.nav:first-of-type').click(); 44 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateXRegex(-2 * width)); 45 | cy.get('@slide').find('.nav:first-of-type').click(); 46 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateXRegex(-1 * width)); 47 | }); 48 | 49 | it('allows dragging the slide to go next and back', async () => { 50 | cy.get('@slide').should('exist'); 51 | const width = await getSlideshowWidth(); 52 | cy.get('@slide').invoke('width').should('gt', 0); 53 | // before the move 54 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateXRegex(-1 * width)); 55 | cy.get('@slide') 56 | .trigger('mousedown', { which: 1, clientX: 450 }) 57 | .trigger('mousemove', { clientX: 200 }) 58 | .trigger('mouseup', { force: true }); 59 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateXRegex(-2 * width)); 60 | 61 | cy.get('@slide') 62 | .trigger('mousedown', { which: 1, clientX: 200 }) 63 | .trigger('mousemove', { clientX08: 450 }) 64 | .trigger('mouseup', { force: true }); 65 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateXRegex(-1 * width)); 66 | }); 67 | }); -------------------------------------------------------------------------------- /cypress/e2e/slide/slide.cy.js: -------------------------------------------------------------------------------- 1 | import { translateXRegex, getAttributeRow, getSlideshowWidth } from '../../support/utils'; 2 | 3 | describe('slide functionality', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:6006/?path=/story/examples-slide--default'); 6 | cy.frameLoaded("#storybook-preview-iframe"); 7 | cy.iframe('#storybook-preview-iframe') 8 | .find('.react-slideshow-container') 9 | .as("slide") 10 | }); 11 | 12 | it('loads the slide accurately', async () => { 13 | cy.get('@slide').should('exist'); 14 | const width = await getSlideshowWidth(); 15 | cy.get('@slide').invoke('width').should('gt', 0); 16 | cy.get('@slide').find('.images-wrap').should('have.class', 'horizontal'); 17 | cy.get('@slide').find('.images-wrap > div').should('have.length', 5); 18 | cy.get('@slide').find('.images-wrap').should('have.css', 'width').and('match', new RegExp(`${width * 5}`)) 19 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateXRegex(-1 * width)) 20 | cy.get('@slide').find('.images-wrap > div:nth-of-type(2)').should('have.class', 'active') 21 | cy.get('@slide').find('.images-wrap > div:nth-of-type(2)').should('have.css', 'width').and('match', new RegExp(width)) 22 | }); 23 | 24 | it('shows the arrow by default and hides it when the property is set to false', async () => { 25 | cy.get('@slide').should('exist'); 26 | cy.get('@slide').invoke('width').should('gt', 0); 27 | cy.get('@slide').find('.nav').should('have.length', 2); 28 | cy.get('@slide').find('.nav:first-of-type').should('have.attr', 'data-type', 'prev'); 29 | cy.get('@slide').find('.nav:last-of-type').should('have.attr', 'data-type', 'next'); 30 | cy.get('@slide').find('.nav:first-of-type').should('be.visible'); 31 | cy.get('@slide').find('.nav:last-of-type').should('be.visible'); 32 | // click on the arrows prop and set it to false 33 | getAttributeRow("arrows").find('button').click(); 34 | cy.get('@slide').find('.nav').should('have.length', 0); 35 | // click the label to set it to true again 36 | getAttributeRow("arrows").find('label').click(); 37 | cy.get('@slide').find('.nav').should('have.length', 2); 38 | }); 39 | 40 | it('shows the indicator if the indicators prop is set to a truthy value', async () => { 41 | cy.get('@slide').should('exist'); 42 | const width = await getSlideshowWidth(); 43 | cy.get('@slide').invoke('width').should('gt', 0); 44 | cy.get('@slide').next().should('not.exist'); 45 | // change the indicators prop to truthy value 46 | getAttributeRow("indicators").find('button').click(); 47 | // cy.get('div#root').debug(); 48 | cy.get('@slide').next().should('exist'); 49 | cy.get('@slide').next().should('have.class', 'indicators'); 50 | cy.get('@slide').next().find('button').should('have.length', 3); 51 | // clicking on the second indicator should make the second slide active 52 | cy.get('@slide').next().find('li:nth-of-type(2) button').click(); 53 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateXRegex(-2 * width)); 54 | // clicking on the third indicator should make the third slide active 55 | cy.get('@slide').next().find('li:nth-of-type(3) button').click(); 56 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateXRegex(-3 * width)); 57 | // clicking on the third indicator should make the third slide active 58 | cy.get('@slide').next().find('li:nth-of-type(1) button').click(); 59 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateXRegex(-1 * width)); 60 | }); 61 | 62 | it('setting infinite to false should prevent previous from working on first slide and next from working on last slide', async () => { 63 | cy.get('@slide').should('exist'); 64 | const width = await getSlideshowWidth(); 65 | cy.get('@slide').invoke('width').should('gt', 0); 66 | // clicking on previous should work for now 67 | cy.get('@slide').find('.nav:first-of-type').click(); 68 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateXRegex(-3 * width)); 69 | // return back to first slide 70 | cy.get('@slide').find('.nav:last-of-type').click(); 71 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateXRegex(-1 * width)); 72 | // click on the infinite prop and set it to false 73 | getAttributeRow("infinite").find('button').click(); 74 | // now no need to render extra two slides 75 | cy.get('@slide').find('.images-wrap > div').should('have.length', 3); 76 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateXRegex('0')); 77 | cy.get('@slide').find('.nav:first-of-type').should('have.attr', 'disabled'); 78 | // click next to go to last slide 79 | cy.get('@slide').find('.nav:last-of-type').click(); 80 | cy.wait(3000); 81 | cy.get('@slide').find('.nav:last-of-type').click(); 82 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateXRegex(-2 * width)); 83 | cy.get('@slide').find('.nav:last-of-type').should('have.attr', 'disabled'); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /cypress/e2e/slide/vertical.cy.js: -------------------------------------------------------------------------------- 1 | import { translateYRegex } from '../../support/utils'; 2 | 3 | describe('vertical slide test', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:6006/?path=/story/examples-vertical--page'); 6 | cy.frameLoaded("#storybook-preview-iframe"); 7 | cy.iframe('#storybook-preview-iframe') 8 | .find('.react-slideshow-container') 9 | .as("slide") 10 | }); 11 | 12 | it('loads the vertical slide and the slide with slide 1', () => { 13 | cy.get('@slide').should('exist'); 14 | cy.get('@slide').find('.images-wrap').should('have.class', 'vertical'); 15 | cy.get('@slide').find('.images-wrap > div').should('have.length', 7); 16 | cy.get('@slide').find('.images-wrap').should('have.css', 'height').and('match', /2450px/) 17 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateYRegex(-350)) 18 | cy.get('@slide').find('.images-wrap > div:nth-of-type(2)').should('have.class', 'active') 19 | }); 20 | 21 | it('loads the next slides when the next button is clicked', () => { 22 | cy.get('@slide').should('exist'); 23 | cy.get('@slide').find('.nav').last().click(); 24 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateYRegex('-700')); 25 | cy.get('@slide').find('.nav').last().click(); 26 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateYRegex('-1050')); 27 | cy.get('@slide').find('.nav').last().click(); 28 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateYRegex('-1400')); 29 | }); 30 | 31 | it('loads the previous slides when the previous button is clicked', () => { 32 | cy.get('@slide').should('exist'); 33 | cy.get('@slide').find('.nav').first().click(); 34 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateYRegex('-1750')); 35 | cy.get('@slide').find('.nav').first().click(); 36 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateYRegex('-1400')); 37 | cy.get('@slide').find('.nav').first().click(); 38 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateYRegex('-1050')); 39 | }); 40 | 41 | it('allows dragging the slide to go next and back', () => { 42 | cy.get('@slide').should('exist'); 43 | // before the move 44 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateYRegex('-350')); 45 | cy.get('@slide') 46 | .trigger('mousedown', { which: 1, clientY: 344 }) 47 | .trigger('mousemove', { clientY: 200 }) 48 | .trigger('mouseup', { force: true }); 49 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateYRegex('-700')); 50 | cy.wait(2000); 51 | cy.get('@slide') 52 | .trigger('mousedown', { which: 1, clientY: 200 }) 53 | .trigger('mousemove', { clientY: 344 }) 54 | .trigger('mouseup', { force: true }); 55 | cy.get('@slide').find('.images-wrap').should('have.css', 'transform').and('match', translateYRegex('-350')); 56 | }); 57 | }); -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import 'cypress-iframe'; 3 | // *********************************************** 4 | // This example commands.ts shows you how to 5 | // create various custom commands and overwrite 6 | // existing commands. 7 | // 8 | // For more comprehensive examples of custom 9 | // commands please read more here: 10 | // https://on.cypress.io/custom-commands 11 | // *********************************************** 12 | // 13 | // 14 | // -- This is a parent command -- 15 | // Cypress.Commands.add('login', (email, password) => { ... }) 16 | // 17 | // 18 | // -- This is a child command -- 19 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 20 | // 21 | // 22 | // -- This is a dual command -- 23 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 24 | // 25 | // 26 | // -- This will overwrite an existing command -- 27 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 28 | // 29 | // declare global { 30 | // namespace Cypress { 31 | // interface Chainable { 32 | // login(email: string, password: string): Chainable 33 | // drag(subject: string, options?: Partial): Chainable 34 | // dismiss(subject: string, options?: Partial): Chainable 35 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 36 | // } 37 | // } 38 | // } -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') -------------------------------------------------------------------------------- /cypress/support/utils.ts: -------------------------------------------------------------------------------- 1 | import { at } from "cypress/types/lodash"; 2 | 3 | export const translateXRegex = (value: string | number) => { 4 | return new RegExp(`matrix\\(1, 0, 0, 1, ${value}, 0\\)`); 5 | } 6 | 7 | export const translateYRegex = (value: string | number) => { 8 | return new RegExp(`matrix\\(1, 0, 0, 1, 0, ${value}\\)`); 9 | } 10 | 11 | export const getAttributeRow = (attribute: string) => { 12 | return cy.get('.docblock-argstable').find('td').contains(attribute).parents('tr') 13 | } 14 | 15 | export const getSlideshowWidth = () => { 16 | return new Promise((resolve) => { 17 | cy.get('@slide').then(element => { 18 | // @ts-ignore 19 | resolve(element.width()); 20 | }); 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | collectCoverage: true, 4 | collectCoverageFrom: [ 5 | "src/**/*.{ts,tsx}", 6 | ], 7 | coverageDirectory: "coverage", 8 | coverageReporters: [ 9 | "json", 10 | "text", 11 | "lcov", 12 | "clover" 13 | ], 14 | moduleDirectories: [ 15 | "node_modules" 16 | ], 17 | 18 | moduleNameMapper: { 19 | "\\.(css|scss)$": "/__mocks__/styleMock.js" 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-slideshow-image", 3 | "version": "4.3.2", 4 | "description": "An image slideshow with react", 5 | "license": "MIT", 6 | "main": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "homepage": "https://react-slideshow-image.netlify.app", 12 | "keywords": [ 13 | "image", 14 | "react", 15 | "Image slider", 16 | "Slideshow", 17 | "react", 18 | "fade", 19 | "zoom" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/femioladeji/react-slideshow.git" 24 | }, 25 | "scripts": { 26 | "start": "tsdx watch", 27 | "build": "tsdx build && uglifycss src/css/styles.css > dist/styles.css", 28 | "test": "tsdx test --passWithNoTests", 29 | "lint": "tsdx lint", 30 | "prepare": "tsdx build && uglifycss src/css/styles.css > dist/styles.css", 31 | "size": "size-limit", 32 | "analyze": "size-limit --why", 33 | "storybook": "start-storybook -p 6006", 34 | "build-storybook": "build-storybook", 35 | "cypress": "cypress open", 36 | "e2e": "cypress run" 37 | }, 38 | "peerDependencies": { 39 | "react": ">=15" 40 | }, 41 | "husky": { 42 | "hooks": { 43 | "pre-commit": "tsdx lint" 44 | } 45 | }, 46 | "prettier": { 47 | "printWidth": 100, 48 | "semi": true, 49 | "singleQuote": true, 50 | "trailingComma": "es5" 51 | }, 52 | "author": "Femi Oladeji", 53 | "module": "dist/react-slideshow-image.esm.js", 54 | "size-limit": [ 55 | { 56 | "path": "dist/react-slideshow-image.cjs.production.min.js", 57 | "limit": "10 KB" 58 | }, 59 | { 60 | "path": "dist/react-slideshow-image.esm.js", 61 | "limit": "10 KB" 62 | } 63 | ], 64 | "dependencies": { 65 | "@tweenjs/tween.js": "^18.6.4", 66 | "resize-observer-polyfill": "^1.5.1" 67 | }, 68 | "devDependencies": { 69 | "@babel/core": "^7.18.0", 70 | "@size-limit/preset-small-lib": "^7.0.8", 71 | "@storybook/addon-a11y": "^6.5.9", 72 | "@storybook/addon-essentials": "^6.5.9", 73 | "@storybook/addon-links": "^6.5.9", 74 | "@storybook/addons": "^6.5.9", 75 | "@storybook/react": "^6.5.9", 76 | "@testing-library/react": "^12.1.5", 77 | "@types/mdx": "^2.0.2", 78 | "@types/react": "^18.0.9", 79 | "@types/react-dom": "^18.0.4", 80 | "babel-loader": "^8.2.5", 81 | "cypress": "^13.3.0", 82 | "cypress-iframe": "^1.0.1", 83 | "husky": "^8.0.1", 84 | "react": "^17.0.0", 85 | "react-dom": "^17.0.0", 86 | "react-is": "^17.0.0", 87 | "size-limit": "^7.0.8", 88 | "tsdx": "^0.14.1", 89 | "tslib": "^2.4.0", 90 | "typescript": "^4.6.4", 91 | "uglifycss": "0.0.29" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/css/styles.css: -------------------------------------------------------------------------------- 1 | .react-slideshow-container { 2 | display: -webkit-box; 3 | display: -ms-flexbox; 4 | display: flex; 5 | -webkit-box-align: center; 6 | -ms-flex-align: center; 7 | align-items: center; 8 | position: relative; 9 | } 10 | 11 | .react-slideshow-container .nav { 12 | z-index: 10; 13 | position: absolute; 14 | cursor: pointer; 15 | } 16 | 17 | .react-slideshow-container .nav:first-of-type { 18 | left: 0; 19 | } 20 | 21 | .react-slideshow-container .nav:last-of-type { 22 | right: 0; 23 | } 24 | 25 | .react-slideshow-container .default-nav { 26 | height: 30px; 27 | background: rgba(255, 255, 255, 0.6); 28 | width: 30px; 29 | border: none; 30 | text-align: center; 31 | color: #fff; 32 | border-radius: 50%; 33 | display: -webkit-box; 34 | display: -ms-flexbox; 35 | display: flex; 36 | -webkit-box-align: center; 37 | -ms-flex-align: center; 38 | align-items: center; 39 | -webkit-box-pack: center; 40 | -ms-flex-pack: center; 41 | justify-content: center; 42 | } 43 | 44 | .react-slideshow-container .default-nav:hover, 45 | .react-slideshow-container .default-nav:focus { 46 | background: #fff; 47 | color: #666; 48 | outline: none; 49 | } 50 | 51 | .react-slideshow-container .default-nav.disabled:hover { 52 | cursor: not-allowed; 53 | } 54 | 55 | .react-slideshow-container .default-nav:first-of-type { 56 | margin-right: -30px; 57 | border-right: none; 58 | border-top: none; 59 | } 60 | 61 | .react-slideshow-container .default-nav:last-of-type { 62 | margin-left: -30px; 63 | } 64 | 65 | .react-slideshow-container+ul.indicators { 66 | display: -webkit-box; 67 | display: -ms-flexbox; 68 | display: flex; 69 | -ms-flex-wrap: wrap; 70 | flex-wrap: wrap; 71 | -webkit-box-pack: center; 72 | -ms-flex-pack: center; 73 | justify-content: center; 74 | margin-top: 20px; 75 | } 76 | 77 | .react-slideshow-container+ul.indicators li { 78 | display: inline-block; 79 | position: relative; 80 | width: 7px; 81 | height: 7px; 82 | padding: 5px; 83 | margin: 0; 84 | } 85 | 86 | .react-slideshow-container+ul.indicators .each-slideshow-indicator { 87 | border: none; 88 | opacity: 0.25; 89 | cursor: pointer; 90 | background: transparent; 91 | color: transparent; 92 | } 93 | 94 | .react-slideshow-container+ul.indicators .each-slideshow-indicator:before { 95 | position: absolute; 96 | top: 0; 97 | left: 0; 98 | width: 7px; 99 | height: 7px; 100 | border-radius: 50%; 101 | content: ''; 102 | background: #000; 103 | text-align: center; 104 | } 105 | 106 | .react-slideshow-container+ul.indicators .each-slideshow-indicator:hover, 107 | .react-slideshow-container+ul.indicators .each-slideshow-indicator.active { 108 | opacity: 0.75; 109 | outline: none; 110 | } 111 | 112 | .react-slideshow-fadezoom-wrapper { 113 | width: 100%; 114 | overflow: hidden; 115 | } 116 | 117 | .react-slideshow-fadezoom-wrapper .react-slideshow-fadezoom-images-wrap { 118 | display: -webkit-box; 119 | display: -ms-flexbox; 120 | display: flex; 121 | -ms-flex-wrap: wrap; 122 | flex-wrap: wrap; 123 | } 124 | 125 | .react-slideshow-fadezoom-wrapper .react-slideshow-fadezoom-images-wrap>div { 126 | position: relative; 127 | opacity: 0; 128 | } 129 | 130 | .react-slideshow-wrapper .react-slideshow-fade-images-wrap>div[aria-hidden='true'] { 131 | display: none; 132 | } 133 | 134 | .react-slideshow-wrapper.slide { 135 | width: 100%; 136 | overflow: hidden; 137 | } 138 | 139 | .react-slideshow-wrapper .images-wrap.horizontal { 140 | display: -webkit-box; 141 | display: -ms-flexbox; 142 | display: flex; 143 | -ms-flex-wrap: wrap; 144 | flex-wrap: wrap; 145 | width : auto 146 | } 147 | 148 | .react-slideshow-wrapper .images-wrap>div[aria-hidden='true'] { 149 | display: none; 150 | } 151 | -------------------------------------------------------------------------------- /src/fade.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FadeZoom } from './fadezoom'; 3 | import { defaultProps } from './props'; 4 | import { FadeProps, SlideshowRef } from './types'; 5 | 6 | export const Fade = React.forwardRef((props, ref) => { 7 | return ; 8 | }); 9 | 10 | Fade.defaultProps = defaultProps; 11 | -------------------------------------------------------------------------------- /src/fadezoom.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useState, 3 | useRef, 4 | useEffect, 5 | useMemo, 6 | useImperativeHandle, 7 | useCallback, 8 | } from 'react'; 9 | import ResizeObserver from 'resize-observer-polyfill'; 10 | import { Group, Tween } from '@tweenjs/tween.js'; 11 | import { 12 | getEasing, 13 | getStartingIndex, 14 | showIndicators, 15 | showNextArrow, 16 | showPreviousArrow, 17 | } from './helpers'; 18 | import { ButtonClick, SlideshowRef, ZoomProps } from './types'; 19 | import { defaultProps } from './props'; 20 | 21 | export const FadeZoom = React.forwardRef((props, ref) => { 22 | const [index, setIndex] = useState( 23 | getStartingIndex(props.children, props.defaultIndex) 24 | ); 25 | const wrapperRef = useRef(null); 26 | const innerWrapperRef = useRef(null); 27 | const tweenGroup = useRef(new Group()); 28 | const timeout = useRef(); 29 | const resizeObserver = useRef(); 30 | const childrenCount = useMemo(() => React.Children.count(props.children), [props.children]); 31 | 32 | const applyStyle = useCallback(() => { 33 | if (innerWrapperRef.current && wrapperRef.current) { 34 | const wrapperWidth = wrapperRef.current.clientWidth; 35 | const fullwidth = wrapperWidth * childrenCount; 36 | innerWrapperRef.current.style.width = `${fullwidth}px`; 37 | for (let index = 0; index < innerWrapperRef.current.children.length; index++) { 38 | const eachDiv = innerWrapperRef.current.children[index]; 39 | if (eachDiv) { 40 | eachDiv.style.width = `${wrapperWidth}px`; 41 | eachDiv.style.left = `${index * -wrapperWidth}px`; 42 | eachDiv.style.display = `block`; 43 | } 44 | } 45 | } 46 | }, [wrapperRef, innerWrapperRef, childrenCount]); 47 | 48 | const initResizeObserver = useCallback(() => { 49 | if (wrapperRef.current) { 50 | resizeObserver.current = new ResizeObserver((entries) => { 51 | if (!entries) return; 52 | applyStyle(); 53 | }); 54 | resizeObserver.current.observe(wrapperRef.current); 55 | } 56 | }, [wrapperRef, applyStyle]); 57 | 58 | const play = useCallback(() => { 59 | const { autoplay, children, duration, infinite } = props; 60 | if ( 61 | autoplay && 62 | React.Children.count(children) > 1 && 63 | (infinite || index < React.Children.count(children) - 1) 64 | ) { 65 | timeout.current = setTimeout(moveNext, duration); 66 | } 67 | // eslint-disable-next-line react-hooks/exhaustive-deps 68 | }, [props, index]); 69 | 70 | useEffect(() => { 71 | initResizeObserver(); 72 | return () => { 73 | tweenGroup.current.removeAll(); 74 | clearTimeout(timeout.current); 75 | removeResizeObserver(); 76 | }; 77 | }, [initResizeObserver, tweenGroup]); 78 | 79 | useEffect(() => { 80 | clearTimeout(timeout.current); 81 | play(); 82 | }, [index, props.autoplay, play]); 83 | 84 | useEffect(() => { 85 | applyStyle(); 86 | }, [childrenCount, applyStyle]); 87 | 88 | useImperativeHandle(ref, () => ({ 89 | goNext: () => { 90 | moveNext(); 91 | }, 92 | goBack: () => { 93 | moveBack(); 94 | }, 95 | goTo: (index: number, options?: { skipTransition?: boolean }) => { 96 | if (options?.skipTransition) { 97 | setIndex(index); 98 | } else { 99 | moveTo(index); 100 | } 101 | }, 102 | })); 103 | 104 | const removeResizeObserver = () => { 105 | if (resizeObserver.current && wrapperRef.current) { 106 | resizeObserver.current.unobserve(wrapperRef.current); 107 | } 108 | }; 109 | 110 | const pauseSlides = () => { 111 | if (props.pauseOnHover) { 112 | clearTimeout(timeout.current); 113 | } 114 | }; 115 | 116 | const startSlides = () => { 117 | const { pauseOnHover, autoplay, duration } = props; 118 | if (pauseOnHover && autoplay) { 119 | timeout.current = setTimeout(() => moveNext(), duration); 120 | } 121 | }; 122 | 123 | const moveNext = () => { 124 | const { children, infinite } = props; 125 | if (!infinite && index === React.Children.count(children) - 1) { 126 | return; 127 | } 128 | transitionSlide((index + 1) % React.Children.count(children)); 129 | }; 130 | 131 | const moveBack = () => { 132 | const { children, infinite } = props; 133 | if (!infinite && index === 0) { 134 | return; 135 | } 136 | transitionSlide(index === 0 ? React.Children.count(children) - 1 : index - 1); 137 | }; 138 | 139 | const preTransition: ButtonClick = (event) => { 140 | const { currentTarget } = event; 141 | if (currentTarget.dataset.type === 'prev') { 142 | moveBack(); 143 | } else { 144 | moveNext(); 145 | } 146 | }; 147 | 148 | const animate = () => { 149 | requestAnimationFrame(animate); 150 | tweenGroup.current.update(); 151 | }; 152 | 153 | const transitionSlide = (newIndex: number) => { 154 | const existingTweens = tweenGroup.current.getAll(); 155 | if (!existingTweens.length) { 156 | if (!innerWrapperRef.current?.children[newIndex]) { 157 | newIndex = 0; 158 | } 159 | clearTimeout(timeout.current); 160 | const value = { opacity: 0, scale: 1 }; 161 | 162 | animate(); 163 | 164 | const tween = new Tween(value, tweenGroup.current) 165 | .to({ opacity: 1, scale: props.scale }, props.transitionDuration) 166 | .onUpdate((value) => { 167 | if (!innerWrapperRef.current) { 168 | return; 169 | } 170 | innerWrapperRef.current.children[newIndex].style.opacity = value.opacity; 171 | innerWrapperRef.current.children[index].style.opacity = 1 - value.opacity; 172 | innerWrapperRef.current.children[ 173 | index 174 | ].style.transform = `scale(${value.scale})`; 175 | }); 176 | tween.easing(getEasing(props.easing)); 177 | tween.onStart(() => { 178 | if (typeof props.onStartChange === 'function') { 179 | props.onStartChange(index, newIndex); 180 | } 181 | }); 182 | tween.onComplete(() => { 183 | if (innerWrapperRef.current) { 184 | setIndex(newIndex); 185 | innerWrapperRef.current.children[index].style.transform = `scale(1)`; 186 | } 187 | if (typeof props.onChange === 'function') { 188 | props.onChange(index, newIndex); 189 | } 190 | }); 191 | tween.start(); 192 | } 193 | }; 194 | 195 | const moveTo = (gotoIndex: number) => { 196 | if (gotoIndex !== index) { 197 | transitionSlide(gotoIndex); 198 | } 199 | }; 200 | 201 | const navigate: ButtonClick = (event) => { 202 | const { currentTarget } = event; 203 | if (!currentTarget.dataset.key) { 204 | return; 205 | } 206 | if (parseInt(currentTarget.dataset.key) !== index) { 207 | moveTo(parseInt(currentTarget.dataset.key)); 208 | } 209 | }; 210 | 211 | return ( 212 |
213 |
219 | {props.arrows && showPreviousArrow(props, index, preTransition)} 220 |
224 |
225 | {(React.Children.map(props.children, (thisArg) => thisArg) || []).map( 226 | (each, key) => ( 227 |
237 | {each} 238 |
239 | ) 240 | )} 241 |
242 |
243 | {props.arrows && showNextArrow(props, index, preTransition)} 244 |
245 | {props.indicators && showIndicators(props, index, navigate)} 246 |
247 | ); 248 | }); 249 | 250 | FadeZoom.defaultProps = defaultProps; 251 | -------------------------------------------------------------------------------- /src/helpers.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import { 3 | ButtonClick, 4 | FadeProps, 5 | IndicatorPropsType, 6 | Responsive, 7 | SlideProps, 8 | TweenEasingFn, 9 | ZoomProps, 10 | } from './types'; 11 | import { Easing } from '@tweenjs/tween.js'; 12 | 13 | export const getStartingIndex = (children: ReactNode, defaultIndex?: number): number => { 14 | if (defaultIndex && defaultIndex < React.Children.count(children)) { 15 | return defaultIndex; 16 | } 17 | return 0; 18 | }; 19 | 20 | export const getResponsiveSettings = ( 21 | wrapperWidth: number, 22 | responsive?: Array 23 | ): Responsive | undefined => { 24 | if (typeof window !== 'undefined' && Array.isArray(responsive)) { 25 | return responsive.find((each) => each.breakpoint <= wrapperWidth); 26 | } 27 | return; 28 | }; 29 | 30 | const EASING_METHODS: { [key: string]: TweenEasingFn } = { 31 | linear: Easing.Linear.None, 32 | ease: Easing.Quadratic.InOut, 33 | 'ease-in': Easing.Quadratic.In, 34 | 'ease-out': Easing.Quadratic.Out, 35 | cubic: Easing.Cubic.InOut, 36 | 'cubic-in': Easing.Cubic.In, 37 | 'cubic-out': Easing.Cubic.Out, 38 | }; 39 | 40 | export const getEasing = (easeMethod?: string): TweenEasingFn => { 41 | if (easeMethod) { 42 | return EASING_METHODS[easeMethod]; 43 | } 44 | return EASING_METHODS.linear; 45 | }; 46 | 47 | export const showPreviousArrow = ( 48 | { prevArrow, infinite }: FadeProps | SlideProps | ZoomProps, 49 | currentIndex: number, 50 | moveSlides: ButtonClick 51 | ): ReactNode => { 52 | const isDisabled = currentIndex <= 0 && !infinite; 53 | const props = { 54 | 'data-type': 'prev', 55 | 'aria-label': 'Previous Slide', 56 | disabled: isDisabled, 57 | onClick: moveSlides, 58 | }; 59 | if (prevArrow) { 60 | return React.cloneElement(prevArrow, { 61 | className: `${prevArrow.props.className || ''} nav ${isDisabled ? 'disabled' : ''}`, 62 | ...props, 63 | }); 64 | } 65 | const className = `nav default-nav ${isDisabled ? 'disabled' : ''}`; 66 | return ( 67 | 72 | ); 73 | }; 74 | 75 | export const showNextArrow = ( 76 | properties: FadeProps | SlideProps | ZoomProps, 77 | currentIndex: number, 78 | moveSlides: ButtonClick, 79 | responsiveSettings?: Responsive 80 | ) => { 81 | const { nextArrow, infinite, children } = properties; 82 | let slidesToScroll = 1; 83 | if (responsiveSettings) { 84 | slidesToScroll = responsiveSettings?.settings.slidesToScroll; 85 | } else if ('slidesToScroll' in properties) { 86 | slidesToScroll = properties.slidesToScroll || 1; 87 | } 88 | const isDisabled = currentIndex >= React.Children.count(children) - slidesToScroll && !infinite; 89 | const props = { 90 | 'data-type': 'next', 91 | 'aria-label': 'Next Slide', 92 | disabled: isDisabled, 93 | onClick: moveSlides, 94 | }; 95 | if (nextArrow) { 96 | return React.cloneElement(nextArrow, { 97 | className: `${nextArrow.props.className || ''} nav ${isDisabled ? 'disabled' : ''}`, 98 | ...props, 99 | }); 100 | } 101 | const className = `nav default-nav ${isDisabled ? 'disabled' : ''}`; 102 | return ( 103 | 108 | ); 109 | }; 110 | 111 | const showDefaultIndicator = ( 112 | isCurrentPageActive: boolean, 113 | key: number, 114 | indicatorProps: IndicatorPropsType 115 | ) => { 116 | return ( 117 |
  • 118 |
  • 124 | ); 125 | }; 126 | 127 | const showCustomIndicator = ( 128 | isCurrentPageActive: boolean, 129 | key: number, 130 | indicatorProps: any, 131 | eachIndicator: any 132 | ) => { 133 | return React.cloneElement(eachIndicator, { 134 | className: `${eachIndicator.props.className} ${isCurrentPageActive ? 'active' : ''}`, 135 | key, 136 | ...indicatorProps, 137 | }); 138 | }; 139 | 140 | export const showIndicators = ( 141 | props: FadeProps | SlideProps | ZoomProps, 142 | currentIndex: number, 143 | navigate: ButtonClick, 144 | responsiveSettings?: Responsive 145 | ): ReactNode => { 146 | const { children, indicators } = props; 147 | let slidesToScroll = 1; 148 | if (responsiveSettings) { 149 | slidesToScroll = responsiveSettings?.settings.slidesToScroll; 150 | } else if ('slidesToScroll' in props) { 151 | slidesToScroll = props.slidesToScroll || 1; 152 | } 153 | const pages = Math.ceil(React.Children.count(children) / slidesToScroll); 154 | return ( 155 |
      156 | {Array.from({ length: pages }, (_, key) => { 157 | const indicatorProps: IndicatorPropsType = { 158 | 'data-key': key, 159 | 'aria-label': `Go to slide ${key + 1}`, 160 | onClick: navigate, 161 | }; 162 | const isCurrentPageActive = 163 | Math.floor((currentIndex + slidesToScroll - 1) / slidesToScroll) === key; 164 | if (typeof indicators === 'function') { 165 | return showCustomIndicator( 166 | isCurrentPageActive, 167 | key, 168 | indicatorProps, 169 | indicators(key) 170 | ); 171 | } 172 | return showDefaultIndicator(isCurrentPageActive, key, indicatorProps); 173 | })} 174 |
    175 | ); 176 | }; 177 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | // // Please do not use types off of a default export module or else Storybook Docs will suffer. 2 | // // see: https://github.com/storybookjs/storybook/issues/9556 3 | 4 | export { Fade } from './fade'; 5 | export { Zoom } from './zoom'; 6 | export { Slide } from './slide'; 7 | export { FadeProps, ZoomProps, SlideProps, SlideshowRef } from './types'; 8 | -------------------------------------------------------------------------------- /src/props.ts: -------------------------------------------------------------------------------- 1 | export const defaultProps = { 2 | duration: 5000, 3 | transitionDuration: 1000, 4 | defaultIndex: 0, 5 | infinite: true, 6 | autoplay: true, 7 | indicators: false, 8 | arrows: true, 9 | pauseOnHover: true, 10 | easing: 'linear', 11 | canSwipe: true, 12 | cssClass: '', 13 | responsive: [], 14 | }; 15 | -------------------------------------------------------------------------------- /src/slide.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useState, 3 | useRef, 4 | useEffect, 5 | useMemo, 6 | useImperativeHandle, 7 | useCallback, 8 | } from 'react'; 9 | import ResizeObserver from 'resize-observer-polyfill'; 10 | import { Group, Tween } from '@tweenjs/tween.js'; 11 | import { 12 | getEasing, 13 | getResponsiveSettings, 14 | getStartingIndex, 15 | showIndicators, 16 | showNextArrow, 17 | showPreviousArrow, 18 | } from './helpers'; 19 | import { ButtonClick, SlideshowRef, SlideProps } from './types'; 20 | import { defaultProps } from './props'; 21 | 22 | export const Slide = React.forwardRef((props, ref) => { 23 | const [index, setIndex] = useState(getStartingIndex(props.children, props.defaultIndex)); 24 | const [wrapperSize, setWrapperSize] = useState(0); 25 | const wrapperRef = useRef(null); 26 | const innerWrapperRef = useRef(null); 27 | const tweenGroup = useRef(new Group()); 28 | const responsiveSettings = useMemo( 29 | () => getResponsiveSettings(wrapperSize, props.responsive), 30 | [wrapperSize, props.responsive] 31 | ); 32 | const slidesToScroll = useMemo(() => { 33 | if (responsiveSettings) { 34 | return responsiveSettings.settings.slidesToScroll; 35 | } 36 | return props.slidesToScroll || 1; 37 | }, [responsiveSettings, props.slidesToScroll]); 38 | const slidesToShow = useMemo(() => { 39 | if (responsiveSettings) { 40 | return responsiveSettings.settings.slidesToShow; 41 | } 42 | return props.slidesToShow || 1; 43 | }, [responsiveSettings, props.slidesToShow]); 44 | const childrenCount = useMemo(() => React.Children.count(props.children), [props.children]); 45 | const eachChildSize = useMemo(() => wrapperSize / slidesToShow, [wrapperSize, slidesToShow]); 46 | const timeout = useRef(); 47 | const resizeObserver = useRef(); 48 | let startingSwipePosition: number; 49 | let dragging: boolean = false; 50 | let distanceSwiped: number = 0; 51 | const translateType = props.vertical ? 'translateY' : 'translateX'; 52 | const swipeAttributeType = props.vertical ? 'clientY' : 'clientX'; 53 | const swipePageAttributeType = props.vertical ? 'pageY' : 'pageX'; 54 | 55 | const applyStyle = useCallback(() => { 56 | if (innerWrapperRef.current) { 57 | const fullSize = wrapperSize * innerWrapperRef.current.children.length; 58 | const attribute = props.vertical ? 'height' : 'width'; 59 | innerWrapperRef.current.style[attribute] = `${fullSize}px`; 60 | if (props.vertical && wrapperRef.current) { 61 | wrapperRef.current.style[attribute] = `${wrapperSize}px`; 62 | } 63 | for (let index = 0; index < innerWrapperRef.current.children.length; index++) { 64 | const eachDiv = innerWrapperRef.current.children[index]; 65 | if (eachDiv) { 66 | if (!props.vertical) { 67 | eachDiv.style[attribute] = `${eachChildSize}px`; 68 | } 69 | eachDiv.style.display = `block`; 70 | } 71 | } 72 | } 73 | }, [wrapperSize, eachChildSize]); 74 | 75 | const initResizeObserver = useCallback(() => { 76 | if (wrapperRef.current) { 77 | resizeObserver.current = new ResizeObserver((entries) => { 78 | if (!entries) return; 79 | setSize(); 80 | }); 81 | resizeObserver.current.observe(wrapperRef.current); 82 | } 83 | }, [wrapperRef]); 84 | 85 | const play = useCallback(() => { 86 | const { autoplay, infinite, duration } = props; 87 | if (autoplay && (infinite || index < childrenCount - 1)) { 88 | timeout.current = setTimeout(moveNext, duration); 89 | } 90 | // eslint-disable-next-line react-hooks/exhaustive-deps 91 | }, [props, childrenCount, index]); 92 | 93 | useEffect(() => { 94 | applyStyle(); 95 | }, [wrapperSize, applyStyle]); 96 | 97 | useEffect(() => { 98 | initResizeObserver(); 99 | return () => { 100 | tweenGroup.current.removeAll(); 101 | clearTimeout(timeout.current); 102 | removeResizeObserver(); 103 | }; 104 | }, [wrapperRef, initResizeObserver, tweenGroup]); 105 | 106 | useEffect(() => { 107 | clearTimeout(timeout.current); 108 | play(); 109 | }, [index, wrapperSize, props.autoplay, play]); 110 | 111 | useImperativeHandle(ref, () => ({ 112 | goNext: () => { 113 | moveNext(); 114 | }, 115 | goBack: () => { 116 | moveBack(); 117 | }, 118 | goTo: (index: number, options?: { skipTransition?: boolean }) => { 119 | if (options?.skipTransition) { 120 | setIndex(index); 121 | } else { 122 | moveTo(index); 123 | } 124 | }, 125 | })); 126 | 127 | const removeResizeObserver = () => { 128 | if (resizeObserver && wrapperRef.current) { 129 | resizeObserver.current.unobserve(wrapperRef.current); 130 | } 131 | }; 132 | 133 | const pauseSlides = () => { 134 | if (props.pauseOnHover) { 135 | clearTimeout(timeout.current); 136 | } 137 | }; 138 | 139 | const swipe = (event: React.MouseEvent | React.TouchEvent) => { 140 | if (props.canSwipe && dragging) { 141 | let position; 142 | if (window.TouchEvent && event.nativeEvent instanceof TouchEvent) { 143 | position = event.nativeEvent.touches[0][swipePageAttributeType]; 144 | } else { 145 | position = (event.nativeEvent as MouseEvent)[swipeAttributeType]; 146 | } 147 | if (position && startingSwipePosition) { 148 | let translateValue = eachChildSize * (index + getOffset()); 149 | const distance = position - startingSwipePosition; 150 | if (!props.infinite && index === childrenCount - slidesToScroll && distance < 0) { 151 | // if it is the last and infinite is false and you're swiping left 152 | // then nothing happens 153 | return; 154 | } 155 | if (!props.infinite && index === 0 && distance > 0) { 156 | // if it is the first and infinite is false and you're swiping right 157 | // then nothing happens 158 | return; 159 | } 160 | distanceSwiped = distance; 161 | translateValue -= distanceSwiped; 162 | innerWrapperRef.current.style.transform = `${translateType}(-${translateValue}px)`; 163 | } 164 | } 165 | }; 166 | 167 | const moveNext = () => { 168 | if (!props.infinite && index === childrenCount - slidesToScroll) { 169 | return; 170 | } 171 | const nextIndex = calculateIndex(index + slidesToScroll); 172 | transitionSlide(nextIndex); 173 | }; 174 | 175 | const moveBack = () => { 176 | if (!props.infinite && index === 0) { 177 | return; 178 | } 179 | let previousIndex = index - slidesToScroll; 180 | if (previousIndex % slidesToScroll) { 181 | previousIndex = Math.ceil(previousIndex / slidesToScroll) * slidesToScroll; 182 | } 183 | transitionSlide(previousIndex); 184 | }; 185 | 186 | const goToSlide: ButtonClick = ({ currentTarget }) => { 187 | if (!currentTarget.dataset.key) { 188 | return; 189 | } 190 | const datasetKey = parseInt(currentTarget.dataset.key); 191 | moveTo(datasetKey * slidesToScroll); 192 | }; 193 | 194 | const moveTo = (index: number) => { 195 | transitionSlide(calculateIndex(index)); 196 | }; 197 | 198 | const calculateIndex = (nextIndex: number): number => { 199 | if (nextIndex < childrenCount && nextIndex + slidesToScroll > childrenCount) { 200 | if ((childrenCount - slidesToScroll) % slidesToScroll) { 201 | return childrenCount - slidesToScroll; 202 | } 203 | return nextIndex; 204 | } 205 | return nextIndex; 206 | }; 207 | 208 | const startSlides = () => { 209 | if (dragging) { 210 | endSwipe(); 211 | } else if (props.pauseOnHover && props.autoplay) { 212 | timeout.current = setTimeout(moveNext, props.duration); 213 | } 214 | }; 215 | 216 | const moveSlides: ButtonClick = ({ currentTarget: { dataset } }) => { 217 | if (dataset.type === 'next') { 218 | moveNext(); 219 | } else { 220 | moveBack(); 221 | } 222 | }; 223 | 224 | const renderPreceedingSlides = () => { 225 | return React.Children.toArray(props.children) 226 | .slice(-slidesToShow) 227 | .map((each, index) => ( 228 | 236 | )); 237 | }; 238 | 239 | const renderTrailingSlides = () => { 240 | if (!props.infinite && slidesToShow === slidesToScroll) { 241 | return; 242 | } 243 | return React.Children.toArray(props.children) 244 | .slice(0, slidesToShow) 245 | .map((each, index) => ( 246 | 254 | )); 255 | }; 256 | 257 | const setSize = () => { 258 | const attribute = props.vertical ? 'clientHeight' : 'clientWidth'; 259 | if (props.vertical) { 260 | if (innerWrapperRef.current) { 261 | setWrapperSize(innerWrapperRef.current.children[0][attribute]); 262 | } 263 | } else { 264 | if (wrapperRef.current) { 265 | setWrapperSize(wrapperRef.current[attribute]); 266 | } 267 | } 268 | }; 269 | 270 | const startSwipe = (event: React.MouseEvent | React.TouchEvent) => { 271 | if (props.canSwipe) { 272 | if (window.TouchEvent && event.nativeEvent instanceof TouchEvent) { 273 | startingSwipePosition = event.nativeEvent.touches[0][swipePageAttributeType]; 274 | } else { 275 | startingSwipePosition = (event.nativeEvent as MouseEvent)[swipeAttributeType]; 276 | } 277 | clearTimeout(timeout.current); 278 | dragging = true; 279 | } 280 | }; 281 | 282 | const endSwipe = () => { 283 | if (props.canSwipe) { 284 | dragging = false; 285 | if (Math.abs(distanceSwiped) / wrapperSize > 0.2) { 286 | if (distanceSwiped < 0) { 287 | moveNext(); 288 | } else { 289 | moveBack(); 290 | } 291 | } else { 292 | if (Math.abs(distanceSwiped) > 0) { 293 | transitionSlide(index, 300); 294 | } 295 | } 296 | } 297 | }; 298 | 299 | const animate = () => { 300 | requestAnimationFrame(animate); 301 | tweenGroup.current.update(); 302 | }; 303 | 304 | const transitionSlide = (toIndex: number, animationDuration?: number) => { 305 | const transitionDuration = animationDuration || props.transitionDuration; 306 | const currentIndex = index; 307 | const existingTweens = tweenGroup.current.getAll(); 308 | if (!wrapperRef.current) { 309 | return; 310 | } 311 | const attribute = props.vertical ? 'clientHeight' : 'clientWidth'; 312 | const childSize = wrapperRef.current[attribute] / slidesToShow; 313 | if (!existingTweens.length) { 314 | clearTimeout(timeout.current); 315 | const value = { 316 | margin: -childSize * (currentIndex + getOffset()) + distanceSwiped, 317 | }; 318 | const tween = new Tween(value, tweenGroup.current) 319 | .to({ margin: -childSize * (toIndex + getOffset()) }, transitionDuration) 320 | .onUpdate((value) => { 321 | if (innerWrapperRef.current) { 322 | innerWrapperRef.current.style.transform = `${translateType}(${value.margin}px)`; 323 | } 324 | }); 325 | tween.easing(getEasing(props.easing)); 326 | 327 | animate(); 328 | 329 | let newIndex = toIndex; 330 | if (newIndex < 0) { 331 | newIndex = childrenCount - slidesToScroll; 332 | } else if (newIndex >= childrenCount) { 333 | newIndex = 0; 334 | } 335 | 336 | tween.onStart(() => { 337 | if (typeof props.onStartChange === 'function') { 338 | props.onStartChange(index, newIndex); 339 | } 340 | }); 341 | 342 | tween.onComplete(() => { 343 | distanceSwiped = 0; 344 | if (typeof props.onChange === 'function') { 345 | props.onChange(index, newIndex); 346 | } 347 | setIndex(newIndex); 348 | }); 349 | 350 | tween.start(); 351 | } 352 | }; 353 | 354 | const isSlideActive = (key: number) => { 355 | return key < index + slidesToShow && key >= index; 356 | }; 357 | 358 | const getOffset = (): number => { 359 | if (!props.infinite) { 360 | return 0; 361 | } 362 | return slidesToShow; 363 | }; 364 | 365 | const style = { 366 | transform: `${translateType}(-${(index + getOffset()) * eachChildSize}px)`, 367 | }; 368 | return ( 369 |
    370 |
    383 | {props.arrows && showPreviousArrow(props, index, moveSlides)} 384 |
    388 |
    393 | {props.infinite && renderPreceedingSlides()} 394 | {(React.Children.map(props.children, (thisArg) => thisArg) || []).map( 395 | (each, key) => { 396 | const isThisSlideActive = isSlideActive(key); 397 | return ( 398 |
    405 | {each} 406 |
    407 | ); 408 | } 409 | )} 410 | {renderTrailingSlides()} 411 |
    412 |
    413 | {props.arrows && showNextArrow(props, index, moveSlides, responsiveSettings)} 414 |
    415 | {!!props.indicators && showIndicators(props, index, goToSlide, responsiveSettings)} 416 |
    417 | ); 418 | }); 419 | 420 | Slide.defaultProps = defaultProps; 421 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, ReactNode } from 'react'; 2 | 3 | export interface Responsive { 4 | breakpoint: number; 5 | settings: { 6 | slidesToShow: number; 7 | slidesToScroll: number; 8 | }; 9 | } 10 | 11 | export interface BaseProps { 12 | /** the content of the component */ 13 | children: ReactNode; 14 | /** The time it takes (milliseconds) before next transition starts */ 15 | duration?: number; 16 | /** Determines how long the transition takes */ 17 | transitionDuration?: number; 18 | /** Specifies the first slide to display */ 19 | defaultIndex?: number; 20 | /** For specifying if there should be dots below the slideshow. If function; it will render the returned element */ 21 | indicators?: boolean | ((index?: number) => ReactNode); 22 | /** A custom element to serve as previous arrow */ 23 | prevArrow?: ReactElement; 24 | /** A custom element to serve as next arrow */ 25 | nextArrow?: ReactElement; 26 | /** Determines if there should be a navigational arrow for going to the next or previous slide */ 27 | arrows?: boolean; 28 | /** Responsible for determining if the slideshow should start automatically */ 29 | autoplay?: boolean; 30 | /** Specifies if the transition should loop infinitely */ 31 | infinite?: boolean; 32 | /** Determines whether the transition effect applies when the mouse hovers the slider */ 33 | pauseOnHover?: boolean; 34 | /** Determines whether the user can go to next or previous slide by the mouse or by touching */ 35 | canSwipe?: boolean; 36 | /** The timing transition function to use. You can use one of linear, ease, ease-in, ease-out, cubic, cubic-in, cubic-out */ 37 | easing?: string; 38 | /** Use this prop to add your custom css to the wrapper containing the sliders. Pass your css className as value for the cssClass prop */ 39 | cssClass?: string; 40 | /** Callback that gets triggered at the start of every transition. The oldIndex and newIndex are passed as arguments */ 41 | onStartChange?: (from: number, to: number) => void; 42 | /** Callback that gets triggered at the end of every transition. The oldIndex and newIndex are passed as arguments */ 43 | onChange?: (from: number, to: number) => void; 44 | /** Ref for the slideshow (carousel). This is useful for executing methods like goBack, goNext and goTo on the slideshow */ 45 | ref?: any; 46 | } 47 | 48 | export interface FadeProps extends BaseProps {} 49 | export interface ZoomProps extends BaseProps { 50 | /** Required when using zoom to specify the scale the current slide should be zoomed to. A number greater than 1 indicates zoom in. A number less than 1, indicates zoom out */ 51 | scale: number; 52 | } 53 | export interface SlideProps extends BaseProps { 54 | /** Set slidesToShow & slidesToScroll based on screen size. */ 55 | responsive?: Array; 56 | /** The number of slides to show on each page */ 57 | slidesToShow?: number; 58 | /** The number of slides to scroll */ 59 | slidesToScroll?: number; 60 | /** If slide should scroll vertically */ 61 | vertical?: boolean; 62 | } 63 | 64 | export type ButtonClick = (event: React.SyntheticEvent) => void; 65 | 66 | export type IndicatorPropsType = { 67 | 'data-key': number; 68 | 'aria-label': string; 69 | onClick: ButtonClick; 70 | }; 71 | 72 | export type TweenEasingFn = (amount: number) => number; 73 | 74 | export type SlideshowRef = { 75 | goNext: () => void; 76 | goBack: () => void; 77 | goTo: (index: number, options?: { skipTransition?: boolean }) => void; 78 | }; 79 | -------------------------------------------------------------------------------- /src/zoom.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FadeZoom } from './fadezoom'; 3 | import { defaultProps } from './props'; 4 | import { SlideshowRef, ZoomProps } from './types'; 5 | 6 | export const Zoom = React.forwardRef((props, ref) => { 7 | return ; 8 | }); 9 | 10 | Zoom.defaultProps = defaultProps; 11 | -------------------------------------------------------------------------------- /stories/CustomArrows.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Canvas, Meta, Story } from '@storybook/addon-docs'; 2 | import { Slide } from '../src'; 3 | 4 | 5 | 6 | ## Customizing Previous & Next arrow 7 | 8 | You can customize the previous and next arrow by setting the property as shown below 9 | 10 | ```tsx 11 | import React from 'react'; 12 | import { Slide } from 'react-slideshow-image'; 13 | import 'react-slideshow-image/dist/styles.css'; 14 | 15 | const buttonStyle = { 16 | width: "30px", 17 | background: 'none', 18 | border: '0px' 19 | }; 20 | 21 | const properties = { 22 | prevArrow: , 23 | nextArrow: 24 | } 25 | 26 | const Example = () => { 27 | return ( 28 |
    29 | 30 | {/* children here */} 31 | 32 |
    33 | ); 34 | }; 35 | 36 | export default Example; 37 | ``` 38 | 39 | You can also style the indicator 40 | ```css 41 | .indicator { 42 | cursor: pointer; 43 | padding: 10px; 44 | text-align: center; 45 | border: 1px #666 solid; 46 | margin: 0; 47 | } 48 | 49 | .indicator.active { 50 | color: #fff; 51 | background: #666; 52 | } 53 | ``` 54 | 55 | } 57 | prevArrow={} 58 | > 59 |
    60 |
    61 | 62 | Slide 1 63 | 64 |
    65 |
    66 |
    67 |
    68 | 69 | Slide 2 70 | 71 |
    72 |
    73 |
    74 |
    75 | 76 | Slide 3 77 | 78 |
    79 |
    80 |
    81 |
    82 | -------------------------------------------------------------------------------- /stories/CustomIndicators.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Canvas, Meta, Story } from '@storybook/addon-docs'; 2 | import { Zoom } from '../src'; 3 | 4 | 5 | 6 | ## Customizing Indicators 7 | 8 | The indicators shown below the slide can be customized to your design specification. 9 | It takes a boolean or a function. If it is set to true, it shows the default indicator style. 10 | If a function is passed then it displays the element returned in that function. 11 | The indicator for the current slide has an active class that can be used in styling it 12 | 13 | ```tsx 14 | import React from 'react'; 15 | import { Zoom } from 'react-slideshow-image'; 16 | import 'react-slideshow-image/dist/styles.css'; 17 | 18 | const indicators = (index) => (
    {index + 1}
    ); 19 | 20 | const Example = () => { 21 | 22 | return ( 23 |
    24 | 25 | {/* children here */} 26 | 27 |
    28 | ); 29 | }; 30 | 31 | export default Example; 32 | ``` 33 | 34 | You can also style the indicator 35 | ```css 36 | .indicator { 37 | cursor: pointer; 38 | padding: 10px; 39 | text-align: center; 40 | border: 1px #666 solid; 41 | margin: 0; 42 | } 43 | 44 | .indicator.active { 45 | color: #fff; 46 | background: #666; 47 | } 48 | ``` 49 | 50 | (
    {index + 1}
    )} 52 | scale={1.4} 53 | > 54 |
    55 |
    60 | 61 | Slide 1 62 | 63 |
    64 |
    65 |
    66 |
    71 | 72 | Slide 2 73 | 74 |
    75 |
    76 |
    77 |
    82 | 83 | Slide 3 84 | 85 |
    86 |
    87 |
    88 |
    -------------------------------------------------------------------------------- /stories/Fade.mdx: -------------------------------------------------------------------------------- 1 | import { Canvas, Meta, Story, ArgsTable } from '@storybook/addon-docs'; 2 | import { Fade } from '../src' 3 | 4 | ## Fade Effect 5 | 6 | Here's the javascript code 7 | ```tsx 8 | import React from 'react'; 9 | import { Fade } from 'react-slideshow-image'; 10 | import 'react-slideshow-image/dist/styles.css'; 11 | 12 | const FadeExample = () => { 13 | const images = [ 14 | "https://images.unsplash.com/photo-1509721434272-b79147e0e708?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80", 15 | "https://images.unsplash.com/photo-1506710507565-203b9f24669b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1536&q=80", 16 | "https://images.unsplash.com/photo-1536987333706-fc9adfb10d91?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80", 17 | ]; 18 | 19 | return ( 20 |
    21 | 22 |
    23 |
    24 | 25 |
    26 |

    First Slide

    27 |
    28 |
    29 |

    Second Slide

    30 |
    31 | 32 |
    33 |
    34 |
    35 |
    36 | 37 |
    38 |

    Third Slide

    39 |
    40 |
    41 |
    42 | ); 43 | }; 44 | 45 | export default FadeExample; 46 | ``` 47 | 48 | The CSS 49 | ```css 50 | .each-slide { 51 | display: flex; 52 | width: 100%; 53 | height: 400px; 54 | } 55 | 56 | .each-slide>div { 57 | width: 75%; 58 | } 59 | 60 | .each-slide>div img { 61 | width: 100%; 62 | height: 100%; 63 | object-fit: cover; 64 | } 65 | 66 | .each-slide p { 67 | width: 25%; 68 | font-size: 1em; 69 | display: flex; 70 | text-align: center; 71 | justify-content: center; 72 | align-items: center; 73 | margin: 0; 74 | background: #adceed; 75 | } 76 | ``` 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /stories/Fade.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { Fade } from '../src'; 4 | import type { FadeProps } from '../src'; 5 | import mdx from './Fade.mdx'; 6 | 7 | const images = [ 8 | "https://images.unsplash.com/photo-1509721434272-b79147e0e708?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80", 9 | "https://images.unsplash.com/photo-1506710507565-203b9f24669b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1536&q=80", 10 | "https://images.unsplash.com/photo-1536987333706-fc9adfb10d91?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80", 11 | "https://images.unsplash.com/photo-1444525873963-75d329ef9e1b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80" 12 | ]; 13 | 14 | const meta: Meta = { 15 | title: 'Examples/Fade', 16 | component: Fade, 17 | parameters: { 18 | controls: { expanded: true }, 19 | docs: { 20 | page: mdx 21 | } 22 | }, 23 | }; 24 | 25 | export default meta; 26 | 27 | const Template: Story = args => 28 |
    29 |
    30 | First image 31 |
    32 |

    First Slide

    33 |
    34 |
    35 |

    Second Slide

    36 |
    37 | Second image 38 |
    39 |
    40 |
    41 |
    42 | Third image 43 |
    44 |

    Third Slide

    45 |
    46 |
    ; 47 | 48 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test 49 | // https://storybook.js.org/docs/react/workflows/unit-testing 50 | export const Default = Template.bind({}); 51 | 52 | Default.args = {}; 53 | -------------------------------------------------------------------------------- /stories/Introduction.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Canvas, Meta, Story } from '@storybook/addon-docs'; 2 | 3 | 4 | 5 | # React slideshow 6 | 7 | React slideshow is a simple react component that supports slide, fade and zoom effects. It is easily customizable and you can edit some properties to fit your design. 8 | 9 | ## Installation 10 | ``` 11 | # npm 12 | npm install react-slideshow-image 13 | 14 | # yarn 15 | yarn add react-slideshow-image 16 | ``` 17 | 18 | You need to import the css file and you can do that by importing it in your js file 19 | ``` 20 | import 'react-slideshow-image/dist/styles.css' 21 | ``` 22 | 23 | Here's a basic example of how to use the library 24 | ```tsx 25 | import React from 'react'; 26 | import { Slide } from 'react-slideshow-image'; 27 | import 'react-slideshow-image/dist/styles.css'; 28 | 29 | const Example = () => { 30 | const images = [ 31 | "https://images.unsplash.com/photo-1509721434272-b79147e0e708?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80", 32 | "https://images.unsplash.com/photo-1506710507565-203b9f24669b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1536&q=80", 33 | "https://images.unsplash.com/photo-1536987333706-fc9adfb10d91?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80", 34 | ]; 35 | 36 | return ( 37 | 38 |
    39 |
    40 | Slide 1 41 |
    42 |
    43 |
    44 |
    45 | Slide 2 46 |
    47 |
    48 |
    49 |
    50 | Slide 3 51 |
    52 |
    53 |
    54 | ); 55 | }; 56 | 57 | export default Example; 58 | ``` 59 | 60 | and the css 61 | ```css 62 | .each-slide-effect > div { 63 | display: flex; 64 | align-items: center; 65 | justify-content: center; 66 | background-size: cover; 67 | height: 350px; 68 | } 69 | 70 | .each-slide-effect span { 71 | padding: 20px; 72 | font-size: 20px; 73 | background: #efefef; 74 | text-align: center; 75 | } 76 | ``` 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /stories/Methods.mdx: -------------------------------------------------------------------------------- 1 | import { Canvas, Meta, Story } from '@storybook/addon-docs'; 2 | import { Slide } from '../src'; 3 | 4 | 5 | 6 | ## Methods Slides 7 | 8 | The package supports three methods that can be used to control navigation. The `goBack()` method shows the previous slide while `goNext()` shows the next slide. 9 | The `goTo(index)` method goes to a particular index. It takes an integer as the parameter. You can also pass a second parameter of options e.g `{ skipTransition: true }`. This 10 | will ensure that the next index shows without any transition 11 | 12 | ```tsx 13 | import React from 'react'; 14 | import { Slide, SlideshowRef } from 'react-slideshow-image'; 15 | import 'react-slideshow-image/dist/styles.css'; 16 | 17 | const Example = () => { 18 | const slideRef = useRef(null) 19 | return ( 20 | <> 21 | 22 |
    First Slide
    23 |
    Second Slide
    24 |
    Third Slide
    25 |
    Fourth Slide
    26 |
    27 |
    28 | 29 | 30 | 37 |
    38 | 39 | ); 40 | }; 41 | 42 | export default Example; 43 | ``` 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /stories/Methods.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { Slide, SlideshowRef } from '../src'; 4 | import type { SlideProps } from '../src'; 5 | import mdx from './Methods.mdx'; 6 | 7 | const meta: Meta = { 8 | title: 'Examples/Methods', 9 | component: Slide, 10 | parameters: { 11 | controls: { expanded: true }, 12 | docs: { 13 | page: mdx, 14 | }, 15 | }, 16 | }; 17 | 18 | export default meta; 19 | 20 | const Template: Story = args => { 21 | const slideRef = useRef(null) 22 | return <> 23 | 24 |
    First Slide
    25 |
    Second Slide
    26 |
    Third Slide
    27 |
    Fourth Slide
    28 |
    29 |
    30 | 31 | 32 | 39 |
    40 | 41 | } 42 | 43 | export const Default = Template.bind({}); 44 | 45 | Default.args = {}; 46 | -------------------------------------------------------------------------------- /stories/MultipleSlides.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Canvas, Meta, Story } from '@storybook/addon-docs'; 2 | import { Slide } from '../src'; 3 | 4 | 5 | 6 | ## Multiple Slides 7 | 8 | The slide effect has support for multiple slides on a page. You can specify the number of slides to show and the number of slides to scroll 9 | 10 | ```tsx 11 | import React from 'react'; 12 | import { Slide } from 'react-slideshow-image'; 13 | import 'react-slideshow-image/dist/styles.css'; 14 | 15 | const Example = () => { 16 | return ( 17 |
    18 | 19 | {/* children here */} 20 | 21 |
    22 | ); 23 | }; 24 | 25 | export default Example; 26 | ``` 27 | 28 | 29 | 30 |
    First Slide
    31 |
    Second Slide
    32 |
    Third Slide
    33 |
    Fourth Slide
    34 |
    Sixth Slide
    35 |
    Seventh Slide
    36 |
    Eight Slide
    37 |
    38 |
    39 | -------------------------------------------------------------------------------- /stories/Responsive.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Canvas, Meta, Story } from '@storybook/addon-docs'; 2 | import { Slide } from '../src'; 3 | 4 | 5 | 6 | ## Responsive props 7 | 8 | The slide effect has support for multiple slides on a page based on the width of the slideshow. 9 | You can specify the number of slides to show or scroll based on the current width of the container of the slideshow. 10 | 11 | ```tsx 12 | import React from 'react'; 13 | import { Slide } from 'react-slideshow-image'; 14 | import 'react-slideshow-image/dist/styles.css'; 15 | 16 | const responsiveSettings = [ 17 | { 18 | breakpoint: 800, 19 | settings: { 20 | slidesToShow: 3, 21 | slidesToScroll: 3 22 | } 23 | }, 24 | { 25 | breakpoint: 500, 26 | settings: { 27 | slidesToShow: 2, 28 | slidesToScroll: 2 29 | } 30 | } 31 | ]; 32 | const Example = () => { 33 | return ( 34 |
    35 | 36 | {/* children here */} 37 | 38 |
    39 | ); 40 | }; 41 | 42 | export default Example; 43 | ``` 44 | 45 | 46 | 62 |
    First Slide
    63 |
    Second Slide
    64 |
    Third Slide
    65 |
    Fourth Slide
    66 |
    Sixth Slide
    67 |
    Seventh Slide
    68 |
    Eight Slide
    69 |
    70 |
    71 | -------------------------------------------------------------------------------- /stories/Slide.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { Slide } from '../src'; 4 | import type { SlideProps } from '../src'; 5 | 6 | const images = [ 7 | "https://images.unsplash.com/photo-1509721434272-b79147e0e708?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80", 8 | "https://images.unsplash.com/photo-1506710507565-203b9f24669b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1536&q=80", 9 | "https://images.unsplash.com/photo-1444525873963-75d329ef9e1b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80" 10 | ]; 11 | 12 | const meta: Meta = { 13 | title: 'Examples/Slide', 14 | component: Slide, 15 | parameters: { 16 | controls: { expanded: true }, 17 | }, 18 | }; 19 | 20 | export default meta; 21 | 22 | const Template: Story = args => 23 |
    24 |
    25 | Slide 1 26 |
    27 |
    28 |
    29 |
    30 | Slide 2 31 |
    32 |
    33 |
    34 |
    35 | Slide 3 36 |
    37 |
    38 |
    ; 39 | 40 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test 41 | // https://storybook.js.org/docs/react/workflows/unit-testing 42 | export const Default = Template.bind({}); 43 | 44 | Default.args = {}; 45 | -------------------------------------------------------------------------------- /stories/VerticalMode.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Canvas, Meta, Story } from '@storybook/addon-docs'; 2 | import { Slide } from '../src'; 3 | 4 | 5 | 6 | ## Vertical Mode 7 | The slide effect can be set to vertical mode by passing the `vertical` prop 8 | 9 | ```tsx 10 | import React from 'react'; 11 | import { Slide } from 'react-slideshow-image'; 12 | import 'react-slideshow-image/dist/styles.css'; 13 | 14 | const Example = () => { 15 | return ( 16 |
    17 | 18 | {/* children here */} 19 | 20 |
    21 | ); 22 | }; 23 | 24 | export default Example; 25 | ``` 26 | 27 | 28 | 29 |
    30 |
    31 | 32 | Slide 1 33 | 34 |
    35 |
    36 |
    37 |
    38 | 39 | Slide 2 40 | 41 |
    42 |
    43 |
    44 |
    45 | 46 | Slide 3 47 | 48 |
    49 |
    50 |
    51 |
    52 | 53 | Slide 4 54 | 55 |
    56 |
    57 |
    58 |
    59 | 60 | Slide 5 61 | 62 |
    63 |
    64 |
    65 |
    66 | -------------------------------------------------------------------------------- /stories/ZoomIn.mdx: -------------------------------------------------------------------------------- 1 | import { Canvas, Meta, Story, ArgsTable } from '@storybook/addon-docs'; 2 | import { Zoom } from '../src' 3 | 4 | ## Zoom in Effect 5 | 6 | Here's the javascript code 7 | ```tsx 8 | import React from 'react'; 9 | import { Zoom } from 'react-slideshow-image'; 10 | import 'react-slideshow-image/dist/styles.css'; 11 | 12 | const ZoomInExample = () => { 13 | const images = [ 14 | "https://images.unsplash.com/photo-1509721434272-b79147e0e708?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80", 15 | "https://images.unsplash.com/photo-1506710507565-203b9f24669b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1536&q=80", 16 | "https://images.unsplash.com/photo-1536987333706-fc9adfb10d91?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80", 17 | ]; 18 | 19 | return ( 20 | 21 | {images.map((each, index) => ( 22 |
    23 | Slide Image 24 |
    25 | ))} 26 |
    27 | ); 28 | }; 29 | 30 | export default ZoomInExample; 31 | ``` 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /stories/ZoomIn.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { Zoom } from '../src'; 4 | import type { ZoomProps } from '../src'; 5 | import mdx from './ZoomIn.mdx'; 6 | 7 | const images = [ 8 | "https://images.unsplash.com/photo-1444525873963-75d329ef9e1b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80", 9 | "https://images.unsplash.com/photo-1506710507565-203b9f24669b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1536&q=80", 10 | "https://images.unsplash.com/photo-1536987333706-fc9adfb10d91?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80", 11 | ]; 12 | 13 | const meta: Meta = { 14 | title: 'Examples/ZoomIn', 15 | component: Zoom, 16 | parameters: { 17 | controls: { expanded: true }, 18 | docs: { 19 | page: mdx, 20 | }, 21 | }, 22 | }; 23 | 24 | export default meta; 25 | 26 | const Template: Story = args => 27 | {images.map((each, index) => ( 28 |
    29 | Slide Image 30 |
    31 | ))} 32 |
    ; 33 | 34 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test 35 | // https://storybook.js.org/docs/react/workflows/unit-testing 36 | export const Default = Template.bind({}); 37 | 38 | Default.args = {}; 39 | -------------------------------------------------------------------------------- /stories/ZoomOut.mdx: -------------------------------------------------------------------------------- 1 | import { Canvas, Meta, Story, ArgsTable } from '@storybook/addon-docs'; 2 | import { Zoom } from '../src' 3 | 4 | ## Zoom out Effect 5 | 6 | Here's the javascript code 7 | ```tsx 8 | import React from 'react'; 9 | import { Zoom } from 'react-slideshow-image'; 10 | import 'react-slideshow-image/dist/styles.css'; 11 | 12 | const ZoomOutExample = () => { 13 | const images = [ 14 | "https://source.unsplash.com/random/300x300", 15 | "https://source.unsplash.com/random/300x300", 16 | "https://source.unsplash.com/random/300x300", 17 | ]; 18 | 19 | return ( 20 | 21 | {images.map((each, index) => ( 22 |
    23 | Slide Image 24 |
    25 | ))} 26 |
    27 | ); 28 | }; 29 | 30 | export default ZoomOutExample; 31 | ``` 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /stories/ZoomOut.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import { Zoom } from '../src'; 4 | import type { ZoomProps } from '../src'; 5 | import mdx from './ZoomOut.mdx'; 6 | 7 | const images = [ 8 | "https://images.unsplash.com/photo-1444525873963-75d329ef9e1b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80", 9 | "https://images.unsplash.com/photo-1506710507565-203b9f24669b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1536&q=80", 10 | "https://images.unsplash.com/photo-1536987333706-fc9adfb10d91?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80", 11 | ] 12 | 13 | const meta: Meta = { 14 | title: 'Examples/ZoomOut', 15 | component: Zoom, 16 | parameters: { 17 | controls: { expanded: true }, 18 | docs: { 19 | page: mdx, 20 | }, 21 | }, 22 | }; 23 | 24 | export default meta; 25 | 26 | const Template: Story = args => 27 | {images.map((each, index) => ( 28 |
    29 | Slide Image 30 |
    31 | ))} 32 |
    ; 33 | 34 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test 35 | // https://storybook.js.org/docs/react/workflows/unit-testing 36 | export const Default = Template.bind({}); 37 | 38 | Default.args = {}; 39 | -------------------------------------------------------------------------------- /test-utils.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from '@testing-library/react'; 3 | import 'resize-observer-polyfill'; 4 | import { Fade, Zoom, Slide } from './src'; 5 | 6 | jest.mock('resize-observer-polyfill'); 7 | 8 | export const images = [ 9 | 'images/slide_5.jpg', 10 | 'images/slide_6.jpg', 11 | 'images/slide_7.jpg' 12 | ]; 13 | 14 | export const renderFade = (props = {}, container, rerender) => { 15 | let options = {}; 16 | if (container) { 17 | options = { 18 | container: document.body.appendChild(container), 19 | baseElement: container 20 | } 21 | } 22 | let slideShow; 23 | if (rerender) { 24 | slideShow = rerender( 25 | 26 | {images.map((each, index) => ())} 27 | , options); 28 | } else { 29 | slideShow = render( 30 | 31 | {images.map((each, index) => ())} 32 | , options); 33 | } 34 | return slideShow; 35 | } 36 | 37 | export const renderZoom = (props = {}, container) => { 38 | let options = {}; 39 | if (container) { 40 | options = { 41 | container: document.body.appendChild(container), 42 | baseElement: container 43 | } 44 | } 45 | let slideShow = render( 46 | 47 | {images.map((each, index) => ())} 48 | , options); 49 | return slideShow; 50 | } 51 | 52 | export const renderZoom2 = (props = {}, container, rerender) => { 53 | let options = {}; 54 | if (container) { 55 | options = { 56 | container: document.body.appendChild(container), 57 | baseElement: container 58 | } 59 | } 60 | let slideShow; 61 | if (rerender) { 62 | slideShow = rerender( 63 | 64 | {images.slice(0, 2).map((each, index) => ( 65 | 66 | ))} 67 | , options); 68 | } else { 69 | slideShow = render( 70 | 71 | {images.slice(0, 2).map((each, index) => ())} 72 | , options); 73 | } 74 | return slideShow; 75 | } 76 | 77 | export const renderSlide = (props = {}, container, rerender) => { 78 | let options = {}; 79 | if (container) { 80 | options = { 81 | container: document.body.appendChild(container), 82 | baseElement: container 83 | } 84 | } 85 | let slideShow; 86 | if (rerender) { 87 | slideShow = rerender( 88 | 89 | {images.map((each, index) => ( 90 | 91 | ))} 92 | ); 93 | } else { 94 | slideShow = render( 95 | 96 | {images.map((each, index) => ())} 97 | , options); 98 | } 99 | return slideShow; 100 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | // use Node's module resolution algorithm, instead of the legacy TS one 23 | "moduleResolution": "node", 24 | // transpile JSX to React.createElement 25 | "jsx": "react", 26 | // interop between ESM and CJS modules. Recommended by TS 27 | "esModuleInterop": true, 28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 29 | "skipLibCheck": true, 30 | // error out if import and file system have a casing mismatch. Recommended by TS 31 | "forceConsistentCasingInFileNames": true, 32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 33 | "noEmit": true, 34 | } 35 | } 36 | --------------------------------------------------------------------------------