├── .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 | [](https://github.com/femioladeji/react-slideshow)
4 | [](https://codecov.io/gh/femioladeji/react-slideshow)
5 | [](http://packagequality.com/#?package=react-slideshow-image)
6 | [](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 |
68 |
69 |
70 |
71 |
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 |
104 |
105 |
106 |
107 |
108 | );
109 | };
110 |
111 | const showDefaultIndicator = (
112 | isCurrentPageActive: boolean,
113 | key: number,
114 | indicatorProps: IndicatorPropsType
115 | ) => {
116 | return (
117 |
118 |
123 |
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 |
234 | {each}
235 |
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 |
252 | {each}
253 |
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 |
31 |
32 |
First Slide
33 |
34 |
35 |
Second Slide
36 |
37 |
38 |
39 |
40 |
41 |
42 |
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 | slideRef.current.goBack()}>Back
29 | slideRef.current.goNext()}>Next
30 | slideRef.current.goTo(parseInt(event.currentTarget.value))}>
31 | --Select--
32 | First
33 | Second
34 | Third
35 | Fourth
36 |
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 | slideRef.current.goBack()}>Back
31 | slideRef.current.goNext()}>Next
32 | slideRef.current.goTo(parseInt(event.currentTarget.value))}>
33 | --Select--
34 | First
35 | Second
36 | Third
37 | Fourth
38 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------