├── .editorconfig ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── babel.config.js ├── doc-components ├── Api.js ├── CodeBlock.js ├── CustomArrows.js ├── Dots.js ├── DynamicContent.js ├── GitHub.js ├── Number.js ├── using-events.mdx └── using-full-container.mdx ├── jest.config.js ├── next.config.js ├── now.json ├── package.json ├── pages ├── _app.jsx ├── index.js └── index.mdx ├── public ├── 1.jpg ├── 2.jpg ├── 3.jpg └── 4.jpg ├── src ├── index.js ├── index.scss ├── react-slidy-slider.js └── slidy.js └── test ├── index.spec.js └── slidy.spec.js /.editorconfig: -------------------------------------------------------------------------------- 1 | ; http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.md] 14 | indent_size = 4 15 | 16 | [node_modules/**.js] 17 | codepaint = false 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node ### 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | 7 | # Dependency directory 8 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 9 | node_modules 10 | package-lock.json 11 | 12 | ### OSX ### 13 | .DS_Store 14 | .AppleDouble 15 | .LSOverride 16 | 17 | 18 | ### Sass ### 19 | .sass-cache 20 | *.css.map 21 | 22 | ### Other ### 23 | .tmp/ 24 | lib/ 25 | .vscode 26 | .cache 27 | .docz 28 | .npmrc 29 | .next 30 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | docs 3 | dist 4 | node_modules 5 | test 6 | scripts 7 | .babelrc 8 | .editorconfig 9 | .eslintrc 10 | .gitignore 11 | .jscsrc 12 | .sass-lint.yml 13 | .scss-lint.yml 14 | npm-debug.log 15 | webpack.config.js 16 | webpack.doc.config.js 17 | .cache 18 | .docz 19 | .next 20 | pages 21 | doc-components 22 | public 23 | next.config.js 24 | README.md 25 | LICENSE 26 | now.json 27 | jest.config.js 28 | babel.config.js 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Miguel Ángel Durán (midudev) 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 Slidy 2 | 3 | 🍃 React Slidy - Minimalistic and smooth touch slider component for React ⚛️ 4 | 5 | [![JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 6 | [![npm version](https://badge.fury.io/js/react-slidy.svg)](https://badge.fury.io/js/react-slidy) 7 | [![npm](https://img.shields.io/npm/dm/react-slidy.svg?maxAge=2592000)](https://www.npmjs.com/package/react-slidy) 8 | 9 | ## Features 10 | - 🖼️ 1:1 slider for any content 11 | - 📱 Optimized for mobile usage (block scroll on slide) 12 | - ⚡ Optimized for performance 13 | - ⌨️ Supports keyboard navigation 14 | - 😪 Lazy load support 15 | - ☝️ No dependencies, just one possible polyfill: intersection-observer polyfill 16 | - 🗜️ 1KB gzipped (*plus* 1KB if you need intersection-observer) 17 | 18 | ## Overview 19 | 20 | React Slidy is a simple and minimal slider component. The main objective is to **achieve the best performance and smoothness** on React apps, specially on mobile 📱. 21 | 22 | ## Browser compatibility 23 | 24 | Supported browsers are: 25 | 26 | * Chrome 27 | * Firefox 28 | * Safari 6+ 29 | * Internet Explorer 11+ 30 | * Microsoft Edge 12+ 31 | 32 | **If some of them doesn't work, please fill an issue.** 33 | 34 | ## The feature *x* is missing... 35 | 36 | React Slidy intention is to offer a simple API and functionality. If it doesn't fit all your needs, you might consider to use an alternative or do a fork. 37 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = api => { 2 | const isTest = api.env('test') 3 | 4 | if (isTest) { 5 | return { 6 | presets: [ 7 | ['@babel/preset-env', {targets: {node: 'current'}}], 8 | '@babel/preset-react' 9 | ] 10 | } 11 | } else { 12 | return { 13 | presets: ['next/babel'], 14 | plugins: [] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /doc-components/Api.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // generated with react-docgen 4 | const props = { 5 | ArrowLeft: { 6 | type: {name: 'elementType'}, 7 | required: false, 8 | description: 'Component to be used as the left arrow for the slider' 9 | }, 10 | ArrowRight: { 11 | type: {name: 'elementType'}, 12 | required: false, 13 | description: 'Component to be used as the right arrow for the slider' 14 | }, 15 | children: { 16 | type: { 17 | name: 'union', 18 | value: [{name: 'array'}, {name: 'object'}] 19 | }, 20 | required: true, 21 | description: 'Children to be used as slides for the slider' 22 | }, 23 | classNameBase: { 24 | type: {name: 'string'}, 25 | required: false, 26 | description: 27 | 'Class base to create all clases for elements. Styles might break if you modify it.', 28 | defaultValue: {value: "'react-Slidy'", computed: false} 29 | }, 30 | doAfterDestroy: { 31 | type: {name: 'func'}, 32 | required: false, 33 | description: 34 | 'Function that will be executed AFTER destroying the slider. Useful for clean up stuff', 35 | defaultValue: {value: 'function noop() {}', computed: false} 36 | }, 37 | doAfterInit: { 38 | type: {name: 'func'}, 39 | required: false, 40 | description: 41 | 'Function that will be executed AFTER initializing the slider', 42 | defaultValue: {value: 'function noop() {}', computed: false} 43 | }, 44 | doAfterSlide: { 45 | type: {name: 'func'}, 46 | required: false, 47 | description: 48 | 'Function that will be executed AFTER slide transition has ended', 49 | defaultValue: {value: 'function noop() {}', computed: false} 50 | }, 51 | doBeforeSlide: { 52 | type: {name: 'func'}, 53 | required: false, 54 | description: 'Function that will be executed BEFORE slide is happening', 55 | defaultValue: {value: 'function noop() {}', computed: false} 56 | }, 57 | ease: { 58 | type: {name: 'string'}, 59 | required: false, 60 | description: 'Ease mode to use on translations', 61 | defaultValue: {value: "'ease'", computed: false} 62 | }, 63 | imageObjectFit: { 64 | type: { 65 | name: 'enum', 66 | value: [ 67 | {value: "'cover'", computed: false}, 68 | {value: "'contain'", computed: false} 69 | ] 70 | }, 71 | required: false, 72 | description: 'Determine the object-fit property for the images' 73 | }, 74 | infiniteLoop: { 75 | type: {name: 'bool'}, 76 | required: false, 77 | description: 78 | 'Indicates if the slider will start with the first slide once it ends', 79 | defaultValue: {value: 'false', computed: false} 80 | }, 81 | itemsToPreload: { 82 | type: {name: 'number'}, 83 | required: false, 84 | description: 'Determine the number of items that will be preloaded', 85 | defaultValue: {value: '1', computed: false} 86 | }, 87 | initialSlide: { 88 | type: {name: 'number'}, 89 | required: false, 90 | description: 'Determine the first slide to start with', 91 | defaultValue: {value: '0', computed: false} 92 | }, 93 | keyboardNavigation: { 94 | type: {name: 'bool'}, 95 | required: false, 96 | description: 'Activate navigation by keyboard', 97 | defaultValue: {value: 'false', computed: false} 98 | }, 99 | lazyLoadSlider: { 100 | type: {name: 'bool'}, 101 | required: false, 102 | description: 103 | 'Determine if the slider will be lazy loaded using Intersection Observer', 104 | defaultValue: {value: 'true', computed: false} 105 | }, 106 | lazyLoadConfig: { 107 | type: { 108 | name: 'shape', 109 | value: { 110 | offset: { 111 | name: 'number', 112 | description: 'Distance which the slider will be loaded', 113 | required: false 114 | } 115 | } 116 | }, 117 | required: false, 118 | description: 119 | 'Configuration for lazy loading. Only needed if lazyLoadSlider is true', 120 | defaultValue: {value: '{\r\n offset: 150\r\n}', computed: false} 121 | }, 122 | numOfSlides: { 123 | type: {name: 'number'}, 124 | required: false, 125 | description: 'Number of slides to show at once', 126 | defaultValue: {value: '1', computed: false} 127 | }, 128 | sanitize: { 129 | type: {name: 'bool'}, 130 | required: false, 131 | description: 132 | 'Determine if we want to sanitize the slides or take numberOfSlider directly', 133 | defaultValue: {value: 'true', computed: false} 134 | }, 135 | slide: { 136 | type: {name: 'number'}, 137 | required: false, 138 | description: 139 | 'Change dynamically the slide number, perfect to use with dots', 140 | defaultValue: {value: '0', computed: false} 141 | }, 142 | showArrows: { 143 | type: {name: 'bool'}, 144 | required: false, 145 | description: 'Determine if arrows should be shown', 146 | defaultValue: {value: 'true', computed: false} 147 | }, 148 | slideSpeed: { 149 | type: {name: 'number'}, 150 | required: false, 151 | description: 'Determine the speed of the sliding animation', 152 | defaultValue: {value: '500', computed: false} 153 | }, 154 | useFullWidth: { 155 | type: {name: 'bool'}, 156 | required: false, 157 | description: 'Use the full width of the container for the image', 158 | defaultValue: {value: 'true', computed: false} 159 | }, 160 | useFullHeight: { 161 | type: {name: 'bool'}, 162 | required: false, 163 | description: 164 | 'Use the full height of the container adding some styles to the elements', 165 | defaultValue: {value: 'false', computed: false} 166 | } 167 | } 168 | 169 | export default function Api() { 170 | const propsKeys = Object.keys(props) 171 | 172 | return ( 173 | <> 174 | {propsKeys.map(propName => { 175 | const {defaultValue = {}, required, type, description} = 176 | props[propName] || {} 177 | const {value = undefined} = defaultValue 178 | if (typeof type === 'undefined') { 179 | console.warn( 180 | // eslint-disable-line 181 | 'It seem that you might have a prop with a defaultValue but it does not exist as propType' 182 | ) 183 | return 184 | } 185 | 186 | return ( 187 |
188 | 189 | {propName} 190 | {required ? * required : null} 191 | 192 | {type.name} 193 | {value && default value: {value}} 194 | {description &&

{description}

} 195 |
196 | ) 197 | })} 198 | 231 | 232 | ) 233 | } 234 | -------------------------------------------------------------------------------- /doc-components/CodeBlock.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import Highlight, {defaultProps} from 'prism-react-renderer' 3 | import PropTypes from 'prop-types' 4 | 5 | const theme = { 6 | plain: { 7 | color: '#393A34', 8 | backgroundColor: '#f6f8fa' 9 | }, 10 | styles: [ 11 | { 12 | types: ['comment', 'prolog', 'doctype', 'cdata'], 13 | style: { 14 | color: '#999988', 15 | fontStyle: 'italic' 16 | } 17 | }, 18 | { 19 | types: ['namespace'], 20 | style: { 21 | opacity: 0.7 22 | } 23 | }, 24 | { 25 | types: ['string', 'attr-value'], 26 | style: { 27 | color: '#e3116c' 28 | } 29 | }, 30 | { 31 | types: ['punctuation', 'operator'], 32 | style: { 33 | color: '#393A34' 34 | } 35 | }, 36 | { 37 | types: [ 38 | 'entity', 39 | 'url', 40 | 'symbol', 41 | 'number', 42 | 'boolean', 43 | 'variable', 44 | 'constant', 45 | 'property', 46 | 'regex', 47 | 'inserted' 48 | ], 49 | style: { 50 | color: '#36acaa' 51 | } 52 | }, 53 | { 54 | types: ['atrule', 'keyword', 'attr-name', 'selector'], 55 | style: { 56 | color: '#00a4db' 57 | } 58 | }, 59 | { 60 | types: ['function', 'deleted', 'tag'], 61 | style: { 62 | color: '#d73a49' 63 | } 64 | }, 65 | { 66 | types: ['function-variable'], 67 | style: { 68 | color: '#6f42c1' 69 | } 70 | }, 71 | { 72 | types: ['tag', 'selector', 'keyword'], 73 | style: { 74 | color: '#00009f' 75 | } 76 | } 77 | ] 78 | } 79 | 80 | export default function CodeBlock({showButton, children, className = ''}) { 81 | const [show, setShow] = useState(!showButton) 82 | const language = className.replace(/language-/, '') 83 | 84 | if (!show) 85 | return ( 86 | <> 87 | 88 | 104 | 105 | ) 106 | 107 | return ( 108 | 114 | {({className, style, tokens, getLineProps, getTokenProps}) => ( 115 |
116 |           {tokens.slice(0, tokens.length - 1).map((line, i) => (
117 |             
118 | {line.map((token, key) => ( 119 | 120 | ))} 121 |
122 | ))} 123 |
124 | )} 125 |
126 | ) 127 | } 128 | 129 | CodeBlock.propTypes = { 130 | className: PropTypes.string, 131 | children: PropTypes.string, 132 | showButton: PropTypes.bool 133 | } 134 | -------------------------------------------------------------------------------- /doc-components/CustomArrows.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import React from 'react' 3 | 4 | const buttonStyle = { 5 | background: 'transparent', 6 | border: 0, 7 | cursor: 'pointer', 8 | fontSize: 72, 9 | height: '30%', 10 | margin: 'auto 10px', 11 | padding: 15 12 | } 13 | 14 | function CustomArrow({emoji, ...props}) { 15 | return ( 16 | 21 | ) 22 | } 23 | 24 | export function CustomArrowLeft(props) { 25 | return 26 | } 27 | 28 | export function CustomArrowRight(props) { 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /doc-components/Dots.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import ReactSlidy from '../src/index' 3 | 4 | const SLIDES = ['/1.jpg', '/2.jpg', '/3.jpg', '/4.jpg'] 5 | 6 | const createStyles = isActive => ({ 7 | background: 'transparent', 8 | border: 0, 9 | color: isActive ? '#333' : '#ccc', 10 | cursor: 'pointer', 11 | fontSize: '48px' 12 | }) 13 | 14 | const Dots = () => { 15 | const [actualSlide, setActualSlide] = useState(0) 16 | 17 | const updateSlide = ({currentSlide}) => { 18 | setActualSlide(currentSlide) 19 | } 20 | 21 | return ( 22 | <> 23 | 24 | {SLIDES.map(src => ( 25 | 26 | ))} 27 | 28 |
29 | {SLIDES.map((_, index) => { 30 | return ( 31 | 38 | ) 39 | })} 40 |
41 | 42 | ) 43 | } 44 | 45 | export default Dots 46 | -------------------------------------------------------------------------------- /doc-components/DynamicContent.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import ReactSlidy from '../src/index' 3 | import Number from './Number' 4 | 5 | const DynamicContent = () => { 6 | const [slides, setSlides] = useState([0]) 7 | const slidesToRender = slides.map((_, index) => ( 8 | 9 | )) 10 | 11 | return ( 12 | <> 13 | 14 | {slidesToRender} 15 | 16 | ) 17 | } 18 | 19 | export default DynamicContent 20 | -------------------------------------------------------------------------------- /doc-components/GitHub.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export default function GitHubBadge({ 5 | slug, 6 | width = 36, 7 | height = 36, 8 | fill = 'black' 9 | }) { 10 | return ( 11 | <> 12 | 17 | 18 | 23 | 24 | 25 | 33 | 34 | ) 35 | } 36 | 37 | GitHubBadge.propTypes = { 38 | slug: PropTypes.string.isRequired, 39 | width: PropTypes.number, 40 | height: PropTypes.number, 41 | fill: PropTypes.string 42 | } 43 | -------------------------------------------------------------------------------- /doc-components/Number.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const style = { 4 | alignContent: 'center', 5 | alignItems: 'center', 6 | backgroundColor: '#eee', 7 | color: '#888', 8 | display: 'flex', 9 | fontSize: '100px', 10 | height: '250px', 11 | justifyContent: 'center', 12 | textShadow: '2px 2px 0px #aaa', 13 | width: '100%' 14 | } 15 | 16 | export default function Number({num}) { 17 | // eslint-disable-line 18 | return {num} 19 | } 20 | -------------------------------------------------------------------------------- /doc-components/using-events.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: with Events 3 | --- 4 | 5 | # Examples with Events 6 | 7 | import {Fragment, useState} from 'react' 8 | import { Playground, Props } from 'docz' 9 | import ReactSlidy from '../index.js' 10 | import Number from '../examples/Number' 11 | import '../index.scss' 12 | 13 | You could use `doBeforeSlide`, `doAfterSlide` and `doAfterDestroy` events to execute callbacks when some events are ocurring with the slider. 14 | 15 | ## doAfterInit 16 | 17 | Callback that will be executed after initializing the slider 18 | 19 | 20 | alert(`The slider is initialized`)}> 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ## doBeforeSlide 29 | 30 | Callback that will be executed BEFORE the slide transition has been completed. 31 | 32 | 33 | alert(`The next slide is the number ${nextSlide} and the current is ${currentSlide}`)}> 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ## doAfterSlide 42 | 43 | Callback that will be executed AFTER the slide transition has been completed. 44 | 45 | 46 | alert(`The next slide is the number ${currentSlide}`)}> 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ## doAfterDestroy 55 | 56 | Do you need to perform some clean up? You could use the `doAfterDestroy` callback that will be executed AFTER the slider is destroyed. 57 | 58 | 59 | {() => { 60 | const [render, setRender] = useState(true) 61 | 62 | return ( 63 | 64 | 67 | { 68 | render && alert('slider destroyed!')}> 69 | 70 | 71 | 72 | 73 | 74 | } 75 | 76 | ) 77 | }} 78 | 79 | 80 | Created and mantained by [midudev](https://midu.dev) -------------------------------------------------------------------------------- /doc-components/using-full-container.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: with Full Container usage 3 | --- 4 | 5 | # Examples with full container height 6 | 7 | import {Fragment, useState} from 'react' 8 | import { Playground, Props } from 'docz' 9 | import ReactSlidy from '../index.js' 10 | import Number from '../examples/Number' 11 | import '../index.scss' 12 | 13 | 14 |

WITHOUT useFullHeight prop

15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 |
23 |

24 | El panda, oso panda o panda gigante (Ailuropoda melanoleuca) es una especie de mamífero del orden de los carnívoros y aunque hay una gran controversia al respecto, los últimos estudios de su ADN lo engloban entre los miembros de la familia de los osos (Ursidae), siendo el oso de anteojos su pariente más cercano, si bien este pertenece a la subfamilia de los tremarctinos. Por otro lado, el panda rojo pertenece a una familia propia e independiente; Ailuridae. 25 |

26 |
27 |

WITH useFullHeight prop

28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 |
36 |

37 | El panda, oso panda o panda gigante (Ailuropoda melanoleuca) es una especie de mamífero del orden de los carnívoros y aunque hay una gran controversia al respecto, los últimos estudios de su ADN lo engloban entre los miembros de la familia de los osos (Ursidae), siendo el oso de anteojos su pariente más cercano, si bien este pertenece a la subfamilia de los tremarctinos. Por otro lado, el panda rojo pertenece a una familia propia e independiente; Ailuridae. 38 |

39 |
40 |

WITH useFullHeight AND imageObjectFit to 'cover' prop

41 |
42 |
43 | 44 | 45 | 46 | 47 | 48 |
49 |

50 | El panda, oso panda o panda gigante (Ailuropoda melanoleuca) es una especie de mamífero del orden de los carnívoros y aunque hay una gran controversia al respecto, los últimos estudios de su ADN lo engloban entre los miembros de la familia de los osos (Ursidae), siendo el oso de anteojos su pariente más cercano, si bien este pertenece a la subfamilia de los tremarctinos. Por otro lado, el panda rojo pertenece a una familia propia e independiente; Ailuridae. 51 |

52 |
53 |

WITH useFullHeight AND imageObjectFit to 'contain' prop

54 |
55 |
56 | 57 | 58 | 59 | 60 | 61 |
62 |

63 | El panda, oso panda o panda gigante (Ailuropoda melanoleuca) es una especie de mamífero del orden de los carnívoros y aunque hay una gran controversia al respecto, los últimos estudios de su ADN lo engloban entre los miembros de la familia de los osos (Ursidae), siendo el oso de anteojos su pariente más cercano, si bien este pertenece a la subfamilia de los tremarctinos. Por otro lado, el panda rojo pertenece a una familia propia e independiente; Ailuridae. 64 |

65 |
66 |
67 | 68 | Created and mantained by [midudev](https://midu.dev) -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/en/configuration.html 4 | */ 5 | 6 | module.exports = { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/private/var/folders/yz/d_2f89857v15v1s02h8rwdg00000gn/T/jest_dx", 15 | 16 | // Automatically clear mock calls and instances between every test 17 | clearMocks: true, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | // collectCoverage: false, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | coverageDirectory: "coverage", 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "/node_modules/" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | // coverageProvider: "babel", 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: undefined, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: undefined, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | // globals: {}, 64 | 65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 66 | // maxWorkers: "50%", 67 | 68 | // An array of directory names to be searched recursively up from the requiring module's location 69 | // moduleDirectories: [ 70 | // "node_modules" 71 | // ], 72 | 73 | // An array of file extensions your modules use 74 | // moduleFileExtensions: [ 75 | // "js", 76 | // "json", 77 | // "jsx", 78 | // "ts", 79 | // "tsx", 80 | // "node" 81 | // ], 82 | 83 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 84 | // moduleNameMapper: {}, 85 | 86 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 87 | // modulePathIgnorePatterns: [], 88 | 89 | // Activates notifications for test results 90 | // notify: false, 91 | 92 | // An enum that specifies notification mode. Requires { notify: true } 93 | // notifyMode: "failure-change", 94 | 95 | // A preset that is used as a base for Jest's configuration 96 | // preset: undefined, 97 | 98 | // Run tests from one or more projects 99 | // projects: undefined, 100 | 101 | // Use this configuration option to add custom reporters to Jest 102 | // reporters: undefined, 103 | 104 | // Automatically reset mock state between every test 105 | // resetMocks: false, 106 | 107 | // Reset the module registry before running each individual test 108 | // resetModules: false, 109 | 110 | // A path to a custom resolver 111 | // resolver: undefined, 112 | 113 | // Automatically restore mock state between every test 114 | // restoreMocks: false, 115 | 116 | // The root directory that Jest should scan for tests and modules within 117 | // rootDir: undefined, 118 | 119 | // A list of paths to directories that Jest should use to search for files in 120 | // roots: [ 121 | // "" 122 | // ], 123 | 124 | // Allows you to use a custom runner instead of Jest's default test runner 125 | // runner: "jest-runner", 126 | 127 | // The paths to modules that run some code to configure or set up the testing environment before each test 128 | // setupFiles: [], 129 | 130 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 131 | // setupFilesAfterEnv: [], 132 | 133 | // The number of seconds after which a test is considered as slow and reported as such in the results. 134 | // slowTestThreshold: 5, 135 | 136 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 137 | // snapshotSerializers: [], 138 | 139 | // The test environment that will be used for testing 140 | // testEnvironment: "jest-environment-jsdom", 141 | 142 | // Options that will be passed to the testEnvironment 143 | // testEnvironmentOptions: {}, 144 | 145 | // Adds a location field to test results 146 | // testLocationInResults: false, 147 | 148 | // The glob patterns Jest uses to detect test files 149 | // testMatch: [ 150 | // "**/__tests__/**/*.[jt]s?(x)", 151 | // "**/?(*.)+(spec|test).[tj]s?(x)" 152 | // ], 153 | 154 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 155 | // testPathIgnorePatterns: [ 156 | // "/node_modules/" 157 | // ], 158 | 159 | // The regexp pattern or array of patterns that Jest uses to detect test files 160 | // testRegex: [], 161 | 162 | // This option allows the use of a custom results processor 163 | // testResultsProcessor: undefined, 164 | 165 | // This option allows use of a custom test runner 166 | // testRunner: "jasmine2", 167 | 168 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 169 | // testURL: "http://localhost", 170 | 171 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 172 | // timers: "real", 173 | 174 | // A map from regular expressions to paths to transformers 175 | // transform: undefined, 176 | 177 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 178 | // transformIgnorePatterns: [ 179 | // "/node_modules/", 180 | // "\\.pnp\\.[^\\/]+$" 181 | // ], 182 | 183 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 184 | // unmockedModulePathPatterns: undefined, 185 | 186 | // Indicates whether each individual test should be reported during the run 187 | // verbose: undefined, 188 | 189 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 190 | // watchPathIgnorePatterns: [], 191 | 192 | // Whether to use watchman for file crawling 193 | // watchman: true, 194 | }; 195 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withMDX = require('@next/mdx')({ 2 | extension: /\.mdx?$/ 3 | }) 4 | 5 | module.exports = withMDX() 6 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "alias": ["react-slidy.now.sh", "react-slidy.midu.dev"], 4 | "name": "react-slidy" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-slidy", 3 | "version": "4.3.3", 4 | "main": "lib/", 5 | "keywords": [ 6 | "slider", 7 | "react", 8 | "lory", 9 | "slidy", 10 | "react-lory", 11 | "react-slidy", 12 | "slider react", 13 | "react slider" 14 | ], 15 | "scripts": { 16 | "dev": "next", 17 | "build": "next build", 18 | "start": "next start", 19 | "clean:lib": "rimraf ./lib/*", 20 | "lib": "npm run lib:scripts && npm run lib:styles && npm run lib:css", 21 | "lib:scripts": "babel src --out-dir lib --presets=babel-preset-sui", 22 | "lib:styles": "copyfiles -u 1 './src/**/*.scss' lib", 23 | "lib:css": "sass ./src/index.scss ./lib/styles.css", 24 | "lint": "npm run lint:js && npm run lint:sass", 25 | "lint:js": "sui-lint js", 26 | "lint:sass": "sui-lint sass", 27 | "prelib": "npm run clean:lib -s", 28 | "release": "np --no-cleanup", 29 | "prepare": "npm run lib", 30 | "test": "jest", 31 | "test:watch": "jest --watch" 32 | }, 33 | "license": "MIT", 34 | "repository": { 35 | "type": "git", 36 | "url": "git@github.com:midudev/react-slidy.git" 37 | }, 38 | "peerDependencies": { 39 | "prop-types": "15", 40 | "react": ">= 16.8.0", 41 | "react-dom": ">= 16.8.0" 42 | }, 43 | "devDependencies": { 44 | "@babel/cli": "7.16.8", 45 | "@babel/core": "7.16.7", 46 | "@babel/preset-env": "^7.16.8", 47 | "@mdx-js/loader": "1.6.22", 48 | "@next/mdx": "10.2.0", 49 | "@s-ui/lint": "3", 50 | "@size-limit/preset-small-lib": "4.10.2", 51 | "@testing-library/react": "11.2.7", 52 | "babel-jest": "26.6.3", 53 | "babel-preset-sui": "3", 54 | "copyfiles": "2.4.1", 55 | "jest": "26.6.3", 56 | "next": "12.0.7", 57 | "np": "7.5.0", 58 | "prism-react-renderer": "1.2.1", 59 | "prop-types": "15", 60 | "react": "17.0.2", 61 | "react-dom": "17.0.2", 62 | "rimraf": "3.0.2", 63 | "sass": "1.47.0", 64 | "size-limit": "4.10.2" 65 | }, 66 | "dependencies": { 67 | "intersection-observer": "0.10.0" 68 | }, 69 | "pre-commit": [ 70 | "lint" 71 | ], 72 | "eslintConfig": { 73 | "extends": [ 74 | "./node_modules/@s-ui/lint/eslintrc.js" 75 | ] 76 | }, 77 | "prettier": "./node_modules/@s-ui/lint/.prettierrc.js", 78 | "stylelint": { 79 | "extends": "./node_modules/@s-ui/lint/stylelint.config.js" 80 | }, 81 | "size-limit": [ 82 | { 83 | "limit": "3 KB", 84 | "path": "lib/*.js" 85 | } 86 | ] 87 | } 88 | -------------------------------------------------------------------------------- /pages/_app.jsx: -------------------------------------------------------------------------------- 1 | import '../src/index.scss' 2 | 3 | export default function MyApp({Component, pageProps}) { // eslint-disable-line 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Head from 'next/head' 3 | import {MDXProvider} from '@mdx-js/react' 4 | 5 | import IndexMDX from './index.mdx' 6 | import CodeBlock from '../doc-components/CodeBlock' 7 | import GitHubBagdge from '../doc-components/GitHub' 8 | 9 | const components = { 10 | pre: props =>
, 11 | code: CodeBlock 12 | } 13 | 14 | const Home = () => ( 15 | 16 | 17 | 18 | React Slidy 🍃 - a simple and minimal slider component for React 19 | 20 | 24 | 25 | 26 | 27 | 28 | 46 | 47 | ) 48 | 49 | export default Home 50 | -------------------------------------------------------------------------------- /pages/index.mdx: -------------------------------------------------------------------------------- 1 | # React Slidy 🍃 2 | 3 | #### a minimal and optimal slider for React ⚛️ in ~1KB +1KB if intersection-observer polyfill if needed 4 | 5 | import ReactSlidy from '../src/index' 6 | import Api from '../doc-components/Api' 7 | import DynamicContent from '../doc-components/DynamicContent' 8 | import Dots from '../doc-components/Dots' 9 | import Number from '../doc-components/Number' 10 | import {CustomArrowLeft, CustomArrowRight} from '../doc-components/CustomArrows' 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ```js showButton 20 | 21 | 22 | 23 | 24 | 25 | 26 | ``` 27 | 28 | ## How to install ℹ️ 29 | 30 | Install the latest version of the package: 31 | 32 | ``` 33 | npm install react-slidy --save 34 | ``` 35 | 36 | Import the component: 37 | 38 | ```js 39 | import ReactSlidy from 'react-slidy' 40 | ``` 41 | 42 | Import the styles: 43 | 44 | ```sass 45 | @import '~react-slidy/lib/index'; 46 | ``` 47 | 48 | or directly in your javascript file if you're using a bundler: 49 | 50 | ```js 51 | import 'react-slidy/lib/index.scss' 52 | // or using the css provided 53 | import 'react-slidy/lib/styles.css' 54 | ``` 55 | 56 | you could also load the CSS directly from HTML: 57 | 58 | ```html 59 | 60 | 61 | ``` 62 | 63 | ## Customization 👩‍🎨 64 | 65 | If you're using SASS, you could modify the following variables: 66 | 67 | ```sass 68 | $react-slidy-c-background: transparent !default; 69 | $react-slidy-c-nav-background: rgba(255, 255, 255, .8) !default; 70 | $react-slidy-c-nav-color: #aaaaaa !default; 71 | $react-slidy-c-transparent: rgba(0, 0, 0, 0) !default; 72 | $react-slidy-mh: 50px !default; 73 | $react-slidy-h-image: auto !default; 74 | ``` 75 | 76 | ## Examples 📖 77 | 78 | ### Preload all the images 🖼 79 | 80 | The slider is optimized in order to be lazy loaded and, then, load the images only when needed so you could notice an empty image while sliding. 81 | You could preload as much as images as you want by using the `itemsToPreload` in order to avoid that effect if you wish. 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | ```js showButton 91 | 92 | 93 | 94 | 95 | 96 | 97 | ``` 98 | 99 | ### Using with React components ⚛️ 100 | 101 | You could use the slider with React components. Just put them inside. Every child will be rendered as slide. For example: 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | ```js showButton 111 | 112 | 113 | 114 | 115 | 116 | 117 | ``` 118 | 119 | ### Using Keyboard Navigation ⌨️ 120 | 121 | You could use the `keyboardNavigation` prop in order to activate keyboard navigation. Try to use the left and right arrow in order to navigate the next slider. 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | ```js showButton 131 | 132 | 133 | 134 | 135 | 136 | 137 | ``` 138 | 139 | ### Using an anchor as wrapper 🔗 140 | 141 | While not recommendable, you could wrap the Slider with a clickable element like an anchor, and the next and previous buttons will work as expected while the rest of the slider is clickable. 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | ```js showButton 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | ``` 162 | 163 | ### Using Dynamic Content ⚡ 164 | 165 | You could easily add more content to the slider and it will adapt automatically for you. Try to click the button in order to add more content and check that how the new slides are being added. 166 | 167 | 168 | 169 | ```js showButton 170 | const [slides, setSlides] = useState([0]) 171 | const slidesToRender = slides.map((_, index) => ( 172 | 173 | )) 174 | 175 | return ( 176 | <> 177 | 178 | {slidesToRender} 179 | 180 | ) 181 | ``` 182 | 183 | ### Using Infinite Loop 184 | 185 | You could make your slider infinite. That meand when it arrive to the last slide, and the user clicks on next it starts again. And when the slider is on the first slide, and the user clicks on previous, it goes to the last slide. 186 | 187 | #### Simple example with 5 slides in total 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | ```js showButton 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | ``` 206 | 207 | ### Show multiple slides at once 🤹‍♂️ 208 | 209 | Use `numOfSlides` prop to determine the number of slides that will be shown at once. 210 | 211 | #### Simple example with 3 slides 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | ```js showButton 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | ``` 236 | 237 | #### Example with less slides than numOfSlides 238 | 239 | Using `numOfSlides` with sanitize the number of shown items if there's not enough children available to be used. For example, if `numOfSlides` is 5, but you have only two slides, it will show only two using the full width. 240 | 241 | 242 | 243 | 244 | 245 | 246 | ```js showButton 247 | 248 | 249 | 250 | 251 | ``` 252 | 253 | #### Example with less slides than numOfSlides but with sanitize as false 254 | 255 | You could, however, disable the previous behaviour by using the prop `sanitize` in order to avoid changing the `numOfSlides` prop on the fly. This mean the space will be divided by the numOfSlides even if there's no slides enough to show. 256 | 257 | 258 | 259 | 260 | 261 | 262 | ```js showButton 263 | 264 | 265 | 266 | 267 | ``` 268 | 269 | 270 | 271 | #### Creating dots ··· 272 | 273 | While React Slidy doesn't offer a built-in progress indicator, you could build one easily with a few lines of code thanks to its API. 274 | 275 | 276 | 277 | ```js showButton 278 | import React, {useState} from 'react' 279 | import ReactSlidy from '../../src/index' 280 | 281 | const SLIDES = ['/1.jpg', '/2.jpg', '/3.jpg', '/4.jpg'] 282 | 283 | const createStyles = isActive => ({ 284 | background: 'transparent', 285 | border: 0, 286 | color: isActive ? '#333' : '#ccc', 287 | cursor: 'pointer', 288 | fontSize: '32px' 289 | }) 290 | 291 | export default () => { 292 | const [actualSlide, setActualSlide] = useState(0) 293 | 294 | const updateSlide = ({currentSlide}) => { 295 | setActualSlide(currentSlide) 296 | } 297 | 298 | return ( 299 | <> 300 | 301 | {SLIDES.map(src => ( 302 | 303 | ))} 304 | 305 |
306 | {SLIDES.map((_, index) => { 307 | return ( 308 | 315 | ) 316 | })} 317 |
318 | 319 | ) 320 | } 321 | ``` 322 | 323 | ### Avoid slides to use the full width 324 | 325 | By default, slides uses the full width of the container. So, if you're using an image, it uses all the width available. You could avoid this by using the prop `useFullWidth`. 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | ```js showButton 334 | 335 | 336 | 337 | 338 | 339 | ``` 340 | 341 | #### Adapt slides to use the height available 342 | 343 | If you have slides with different heights you need to specify the maxHeight to be used for each slide, in order to avoid inner images or content to make the slider jump as they will automatically adapt to the available width and automatically detect the height. 344 | 345 |
346 | 347 | 351 | 355 | 359 | 363 | 364 |
365 | 366 | ```js showButton 367 |
368 | 369 | 373 | 377 | 381 | 385 | 386 |
387 | ``` 388 | 389 | #### Using custom arrows 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | ```js showButton 399 | const buttonStyle = { 400 | background: 'transparent', 401 | border: 0, 402 | cursor: 'pointer', 403 | fontSize: 72, 404 | height: '30%', 405 | margin: 'auto 10px', 406 | padding: 15 407 | } 408 | 409 | function CustomArrow({emoji, ...props}) { 410 | return ( 411 | 416 | ) 417 | } 418 | 419 | function CustomArrowLeft(props) { 420 | return 421 | } 422 | 423 | function CustomArrowRight(props) { 424 | return 425 | } 426 | 427 | ; 428 | 429 | 430 | 431 | 432 | 433 | ``` 434 | 435 | ## API 📖 436 | 437 | 438 | 439 | Created and mantained by [midudev](https://midu.dev) 440 | -------------------------------------------------------------------------------- /public/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/react-slidy/bd87a94bb930f55e02cb886235bcb500def0815a/public/1.jpg -------------------------------------------------------------------------------- /public/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/react-slidy/bd87a94bb930f55e02cb886235bcb500def0815a/public/2.jpg -------------------------------------------------------------------------------- /public/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/react-slidy/bd87a94bb930f55e02cb886235bcb500def0815a/public/3.jpg -------------------------------------------------------------------------------- /public/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/react-slidy/bd87a94bb930f55e02cb886235bcb500def0815a/public/4.jpg -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useRef, useState} from 'react' 2 | import PropTypes from 'prop-types' 3 | import ReactSlidySlider from './react-slidy-slider' 4 | 5 | function noop() {} 6 | 7 | const CLASSNAMES = { 8 | contain: 'react-Slidy--contain', 9 | cover: 'react-Slidy--cover', 10 | fullHeight: 'react-Slidy--fullHeight', 11 | fullWidth: 'react-Slidy--fullWidth' 12 | } 13 | 14 | const ReactSlidy = ({ 15 | ArrowLeft, 16 | ArrowRight, 17 | children, 18 | classNameBase = 'react-Slidy', 19 | doAfterDestroy = noop, 20 | doAfterInit = noop, 21 | doAfterSlide = noop, 22 | doBeforeSlide = noop, 23 | imageObjectFit, 24 | infiniteLoop = false, 25 | itemsToPreload = 1, 26 | initialSlide = 0, 27 | ease = 'ease', 28 | lazyLoadSlider = true, 29 | lazyLoadConfig = { 30 | offset: 150 31 | }, 32 | keyboardNavigation = false, 33 | numOfSlides = 1, 34 | sanitize = true, 35 | slide = 0, 36 | slideSpeed = 500, 37 | showArrows = true, 38 | useFullHeight = false, 39 | useFullWidth = true 40 | }) => { 41 | const [showSlider, setShowSlider] = useState(!lazyLoadSlider) 42 | const nodeEl = useRef(null) 43 | 44 | useEffect( 45 | function() { 46 | let observer 47 | 48 | if (lazyLoadSlider) { 49 | const initLazyLoadSlider = () => { 50 | // if we support IntersectionObserver, let's use it 51 | const {offset = 0} = lazyLoadConfig 52 | observer = new window.IntersectionObserver(handleIntersection, { 53 | rootMargin: `${offset}px 0px 0px` 54 | }) 55 | observer.observe(nodeEl.current) 56 | } 57 | 58 | if (!('IntersectionObserver' in window)) { 59 | import('intersection-observer').then(initLazyLoadSlider) 60 | } else { 61 | initLazyLoadSlider() 62 | } 63 | } 64 | 65 | return () => observer && observer.disconnect() 66 | }, 67 | [] // eslint-disable-line 68 | ) 69 | 70 | const handleIntersection = ([entry], observer) => { 71 | if (entry.isIntersecting || entry.intersectionRatio > 0) { 72 | observer.unobserve(entry.target) 73 | setShowSlider(true) 74 | } 75 | } 76 | 77 | const numOfSlidesSanitzed = Math.min( 78 | numOfSlides, 79 | React.Children.count(children) 80 | ) 81 | 82 | const rootClassName = [ 83 | classNameBase, 84 | useFullHeight && CLASSNAMES.fullHeight, 85 | useFullWidth && CLASSNAMES.fullWidth, 86 | imageObjectFit && CLASSNAMES[imageObjectFit] 87 | ] 88 | .filter(Boolean) 89 | .join(' ') 90 | 91 | const reactSlidySliderProps = { 92 | ArrowLeft, 93 | ArrowRight, 94 | children, 95 | classNameBase, 96 | doAfterDestroy, 97 | doAfterInit, 98 | doAfterSlide, 99 | doBeforeSlide, 100 | ease, 101 | infiniteLoop, 102 | initialSlide, 103 | itemsToPreload, 104 | keyboardNavigation, 105 | numOfSlides, 106 | showArrows, 107 | slide, 108 | slideSpeed 109 | } 110 | 111 | return ( 112 |
113 | {showSlider && ( 114 | 118 | {children} 119 | 120 | )} 121 |
122 | ) 123 | } 124 | 125 | ReactSlidy.propTypes = { 126 | /** Component to be used as the left arrow for the slider */ 127 | ArrowLeft: PropTypes.elementType, 128 | /** Component to be used as the right arrow for the slider */ 129 | ArrowRight: PropTypes.elementType, 130 | /** Children to be used as slides for the slider */ 131 | children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired, 132 | /** Class base to create all clases for elements. Styles might break if you modify it. */ 133 | classNameBase: PropTypes.string, 134 | /** Function that will be executed AFTER destroying the slider. Useful for clean up stuff */ 135 | doAfterDestroy: PropTypes.func, 136 | /** Function that will be executed AFTER initializing the slider */ 137 | doAfterInit: PropTypes.func, 138 | /** Function that will be executed AFTER slide transition has ended */ 139 | doAfterSlide: PropTypes.func, 140 | /** Function that will be executed BEFORE slide is happening */ 141 | doBeforeSlide: PropTypes.func, 142 | /** Ease mode to use on translations */ 143 | ease: PropTypes.string, 144 | /** Determine the object-fit property for the images */ 145 | imageObjectFit: PropTypes.oneOf(['cover', 'contain']), 146 | /** Indicates if the slider will start with the first slide once it ends */ 147 | infiniteLoop: PropTypes.bool, 148 | /** Determine the number of items that will be preloaded */ 149 | itemsToPreload: PropTypes.number, 150 | /** Determine the first slide to start with */ 151 | initialSlide: PropTypes.number, 152 | /** Activate navigation by keyboard */ 153 | keyboardNavigation: PropTypes.bool, 154 | /** Determine if the slider will be lazy loaded using Intersection Observer */ 155 | lazyLoadSlider: PropTypes.bool, 156 | /** Configuration for lazy loading. Only needed if lazyLoadSlider is true */ 157 | lazyLoadConfig: PropTypes.shape({ 158 | /** Distance which the slider will be loaded */ 159 | offset: PropTypes.number 160 | }), 161 | /** Number of slides to show at once */ 162 | numOfSlides: PropTypes.number, 163 | /** Determine if we want to sanitize the slides or take numberOfSlider directly */ 164 | sanitize: PropTypes.bool, 165 | /** Change dynamically the slide number, perfect to use with dots */ 166 | slide: PropTypes.number, 167 | /** Determine if arrows should be shown */ 168 | showArrows: PropTypes.bool, 169 | /** Determine the speed of the sliding animation */ 170 | slideSpeed: PropTypes.number, 171 | /** Use the full width of the container for the image */ 172 | useFullWidth: PropTypes.bool, 173 | /** Use the full height of the container adding some styles to the elements */ 174 | useFullHeight: PropTypes.bool 175 | } 176 | 177 | export default ReactSlidy 178 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | $react-slidy-c-background: transparent !default; 2 | $react-slidy-c-nav-background: rgba(255, 255, 255, 0.8) !default; 3 | $react-slidy-c-nav-color: #aaaaaa !default; 4 | $react-slidy-c-transparent: rgba(0, 0, 0, 0) !default; 5 | $react-slidy-h-image: auto !default; 6 | $react-slidy-mh: 50px !default; 7 | 8 | @mixin arrow($direction) { 9 | @if $direction == 'right' { 10 | margin-right: 6px; 11 | transform: rotate(45deg); 12 | } @else if $direction == 'left' { 13 | margin-left: 6px; 14 | transform: rotate(-135deg); 15 | } 16 | 17 | border-right: 3px solid $react-slidy-c-nav-color; 18 | border-top: 3px solid $react-slidy-c-nav-color; 19 | content: ''; 20 | display: inline-block; 21 | height: 24px; 22 | width: 24px; 23 | } 24 | 25 | .react-Slidy { 26 | -webkit-tap-highlight-color: $react-slidy-c-transparent; 27 | backface-visibility: hidden; 28 | background: $react-slidy-c-background; 29 | min-height: $react-slidy-mh; 30 | position: relative; 31 | user-select: none; 32 | 33 | &-arrow { 34 | align-items: center; 35 | bottom: 0; 36 | display: flex; 37 | margin: auto 0; 38 | opacity: 0; 39 | position: absolute; 40 | top: 0; 41 | transition: opacity 0.3s ease; 42 | width: auto; 43 | z-index: 1; 44 | 45 | &Left { 46 | left: 0; 47 | } 48 | 49 | &Right { 50 | right: 0; 51 | } 52 | 53 | @media screen and (max-width: 850px) { 54 | display: none; 55 | } 56 | } 57 | 58 | &-prev, 59 | &-next { 60 | background: $react-slidy-c-nav-background; 61 | cursor: pointer; 62 | height: 20%; 63 | justify-content: center; 64 | min-height: 56px; 65 | width: 40px; 66 | } 67 | 68 | &-next { 69 | border-radius: 10px 0 0 10px; 70 | 71 | &::after { 72 | @include arrow('right'); 73 | } 74 | } 75 | 76 | &-prev { 77 | border-radius: 0 10px 10px 0; 78 | 79 | &::after { 80 | @include arrow('left'); 81 | } 82 | } 83 | 84 | &--fullHeight { 85 | height: 100%; 86 | & > div li img { 87 | height: 100%; 88 | } 89 | } 90 | 91 | &--fullWidth > div li img { 92 | width: 100%; 93 | } 94 | 95 | &--contain li img { 96 | object-fit: contain; 97 | } 98 | 99 | &--cover li img { 100 | object-fit: cover; 101 | } 102 | 103 | &:hover > &-arrow { 104 | opacity: 1; 105 | } 106 | 107 | &:hover > &-arrow[disabled] { 108 | opacity: 0.2; 109 | } 110 | 111 | > div { 112 | font-size: 0; 113 | max-height: 100%; 114 | overflow: hidden; 115 | position: relative; 116 | white-space: nowrap; 117 | width: 100%; 118 | 119 | > ul { 120 | display: block; 121 | list-style: none; 122 | padding: 0; 123 | transition-property: transform; 124 | width: 100%; 125 | will-change: transform, transition-timing, transition-duration; 126 | 127 | & > li { 128 | display: inline-block; 129 | position: relative; 130 | user-select: none; 131 | vertical-align: middle; 132 | width: 100%; 133 | } 134 | } 135 | 136 | img { 137 | -webkit-backface-visibility: hidden; 138 | -webkit-perspective: 1000; 139 | 140 | display: block; 141 | height: $react-slidy-h-image; 142 | margin: 0 auto; 143 | max-width: 100%; 144 | pointer-events: none; 145 | touch-action: none; 146 | user-select: none; 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/react-slidy-slider.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import React, {Children, useEffect, useRef, useState} from 'react' 3 | import slidy from './slidy' 4 | 5 | function noop(_) {} 6 | 7 | function convertToArrayFrom(children) { 8 | return Array.isArray(children) ? children : [children] 9 | } 10 | 11 | function getItemsToRender({ 12 | index, 13 | maxIndex, 14 | items, 15 | itemsToPreload, 16 | numOfSlides 17 | }) { 18 | const preload = Math.max(itemsToPreload, numOfSlides) 19 | return items.slice(0, maxIndex + preload) 20 | } 21 | 22 | function destroySlider(slidyInstance, doAfterDestroy) { 23 | slidyInstance && slidyInstance.clean() && slidyInstance.destroy() 24 | doAfterDestroy() 25 | } 26 | 27 | const renderItem = numOfSlides => (item, index) => { 28 | const inlineStyle = numOfSlides !== 1 ? {width: `${100 / numOfSlides}%`} : {} 29 | return ( 30 |
  • 31 | {item} 32 |
  • 33 | ) 34 | } 35 | 36 | export default function ReactSlidySlider({ 37 | ArrowLeft, 38 | ArrowRight, 39 | children, 40 | classNameBase, 41 | doAfterDestroy, 42 | doAfterInit, 43 | doAfterSlide, 44 | doBeforeSlide, 45 | ease, 46 | infiniteLoop, 47 | initialSlide, 48 | itemsToPreload, 49 | keyboardNavigation, 50 | numOfSlides, 51 | showArrows, 52 | slide, 53 | slideSpeed 54 | }) { 55 | const [slidyInstance, setSlidyInstance] = useState({ 56 | goTo: noop, 57 | next: noop, 58 | prev: noop, 59 | updateItems: noop 60 | }) 61 | const [index, setIndex] = useState(initialSlide) 62 | const [maxIndex, setMaxIndex] = useState(initialSlide) 63 | const sliderContainerDOMEl = useRef(null) 64 | const slidesDOMEl = useRef(null) 65 | 66 | const items = Children.toArray(children).filter(child => child !== null); 67 | 68 | useEffect( 69 | function() { 70 | slide !== index && slidyInstance.goTo(slide) 71 | }, 72 | [slide] // eslint-disable-line 73 | ) 74 | 75 | useEffect( 76 | function() { 77 | let handleKeyboard 78 | const slidyInstance = slidy(sliderContainerDOMEl.current, { 79 | ease, 80 | doAfterSlide, 81 | doBeforeSlide, 82 | numOfSlides, 83 | slideSpeed, 84 | infiniteLoop, 85 | slidesDOMEl: slidesDOMEl.current, 86 | initialSlide: index, 87 | items: items.length, 88 | onNext: nextIndex => { 89 | setIndex(nextIndex) 90 | nextIndex > maxIndex && setMaxIndex(nextIndex) 91 | return nextIndex 92 | }, 93 | onPrev: nextIndex => { 94 | setIndex(nextIndex) 95 | return nextIndex 96 | } 97 | }) 98 | 99 | setSlidyInstance(slidyInstance) 100 | doAfterInit() 101 | 102 | if (keyboardNavigation) { 103 | handleKeyboard = e => { 104 | if (e.keyCode === 39) slidyInstance.next(e) 105 | else if (e.keyCode === 37) slidyInstance.prev(e) 106 | } 107 | document.addEventListener('keydown', handleKeyboard) 108 | } 109 | 110 | return () => { 111 | destroySlider(slidyInstance, doAfterDestroy) 112 | if (keyboardNavigation) { 113 | document.removeEventListener('keydown', handleKeyboard) 114 | } 115 | } 116 | }, 117 | [] // eslint-disable-line 118 | ) 119 | 120 | useEffect(function() { 121 | slidyInstance && slidyInstance.updateItems(items.length) 122 | }) 123 | 124 | const itemsToRender = getItemsToRender({ 125 | index, 126 | maxIndex, 127 | items, 128 | itemsToPreload, 129 | numOfSlides 130 | }) 131 | 132 | const handlePrev = e => slidyInstance.prev(e) 133 | const handleNext = e => items.length > numOfSlides && slidyInstance.next(e) 134 | 135 | const renderLeftArrow = () => { 136 | const disabled = index === 0 && !infiniteLoop 137 | const props = {disabled, onClick: handlePrev} 138 | const leftArrowClasses = `${classNameBase}-arrow ${classNameBase}-arrowLeft` 139 | if (ArrowLeft) return 140 | 141 | return ( 142 | 148 | ) 149 | } 150 | const renderRightArrow = () => { 151 | const disabled = 152 | (items.length <= numOfSlides || index === items.length - numOfSlides) && 153 | !infiniteLoop 154 | const props = {disabled, onClick: !disabled ? handleNext : undefined} 155 | const rightArrowClasses = `${classNameBase}-arrow ${classNameBase}-arrowRight` 156 | if (ArrowRight) 157 | return 158 | 159 | return ( 160 | 166 | ) 167 | } 168 | 169 | return ( 170 | <> 171 | {showArrows && ( 172 | <> 173 | {renderLeftArrow()} 174 | {renderRightArrow()} 175 | 176 | )} 177 |
    178 |
      {itemsToRender.map(renderItem(numOfSlides))}
    179 |
    180 | 181 | ) 182 | } 183 | -------------------------------------------------------------------------------- /src/slidy.js: -------------------------------------------------------------------------------- 1 | const LINEAR_ANIMATION = 'linear' 2 | const VALID_SWIPE_DISTANCE = 50 3 | const TRANSITION_END = 'transitionend' 4 | const {abs} = Math 5 | const EVENT_OPTIONS = {passive: false} 6 | 7 | export function translate(to, moveX, percentatge = 100) { 8 | const translation = to * percentatge * -1 9 | const x = moveX ? `calc(${translation}% - ${moveX}px)` : `${translation}%` 10 | return `translate3d(${x}, 0, 0)` 11 | } 12 | 13 | export function infiniteIndex(index, end) { 14 | if (index < 0) return end - 1 15 | else if (index >= end) return 0 16 | else return index 17 | } 18 | 19 | export function clampNumber(x, minValue, maxValue) { 20 | return Math.min(Math.max(x, minValue), maxValue) 21 | } 22 | 23 | function getTouchCoordinatesFromEvent(e) { 24 | return e.targetTouches ? e.targetTouches[0] : e.touches[0] 25 | } 26 | 27 | /** 28 | * 29 | * @param {number} duration 30 | * @param {string} ease 31 | * @param {number} index 32 | * @param {number} x 33 | * @param {number} percentatge 34 | */ 35 | function getTranslationCSS(duration, ease, index, x, percentatge) { 36 | const easeCssText = ease !== '' ? `transition-timing-function: ${ease};` : '' 37 | const durationCssText = duration ? `transition-duration: ${duration}ms;` : '' 38 | return `${easeCssText}${durationCssText}transform: ${translate( 39 | index, 40 | x, 41 | percentatge 42 | )};` 43 | } 44 | 45 | function cleanContainer(container) { 46 | // remove all the elements except the last one as it seems to be old data in the HTML 47 | // that's specially useful for dynamic content 48 | while (container.childElementCount > 1) { 49 | container !== null && container.removeChild(container.lastChild) 50 | } 51 | // tell that the clean is done 52 | return true 53 | } 54 | 55 | export default function slidy(containerDOMEl, options) { 56 | const { 57 | doAfterSlide, 58 | doBeforeSlide, 59 | ease, 60 | infiniteLoop, 61 | initialSlide, 62 | numOfSlides, 63 | onNext, 64 | onPrev, 65 | slidesDOMEl, 66 | slideSpeed 67 | } = options 68 | let {items} = options 69 | 70 | // if frameDOMEl is null, then we do nothing 71 | if (containerDOMEl === null) return 72 | 73 | // initialize some variables 74 | let index = initialSlide 75 | let isScrolling = false 76 | let transitionEndCallbackActivated = false 77 | 78 | // event handling 79 | let deltaX = 0 80 | let deltaY = 0 81 | let touchOffsetX = 0 82 | let touchOffsetY = 0 83 | 84 | /** 85 | * translates to a given position in a given time in milliseconds 86 | * 87 | * @param {number} duration time in milliseconds for the transistion 88 | * @param {string} ease easing css property 89 | * @param {number} x Number of pixels to fine tuning translation 90 | */ 91 | function _translate(duration, ease = '', x = 0) { 92 | const percentatge = 100 / numOfSlides 93 | slidesDOMEl.style.cssText = getTranslationCSS( 94 | duration, 95 | ease, 96 | index, 97 | x, 98 | percentatge 99 | ) 100 | } 101 | 102 | /** 103 | * slide function called by prev, next & touchend 104 | * 105 | * determine nextIndex and slide to next postion 106 | * under restrictions of the defined options 107 | * 108 | * @param {boolean} direction 'true' for right, 'false' for left 109 | */ 110 | function slide(direction) { 111 | const movement = direction === true ? 1 : -1 112 | 113 | // calculate the nextIndex according to the movement 114 | let nextIndex = index + 1 * movement 115 | 116 | /** 117 | * If the slider has the infiniteLoop option 118 | * nextIndex will start from the start when arrives to the end 119 | * and vice versa 120 | */ 121 | if (infiniteLoop) nextIndex = infiniteIndex(nextIndex, items) 122 | 123 | // nextIndex should be between 0 and items minus 1 124 | nextIndex = clampNumber(nextIndex, 0, items - 1) 125 | 126 | goTo(nextIndex) 127 | } 128 | 129 | function onTransitionEnd() { 130 | if (transitionEndCallbackActivated === true) { 131 | _translate(0) 132 | transitionEndCallbackActivated = false 133 | } 134 | } 135 | 136 | function onTouchstart(e) { 137 | const coords = getTouchCoordinatesFromEvent(e) 138 | isScrolling = undefined 139 | touchOffsetX = coords.pageX 140 | touchOffsetY = coords.pageY 141 | } 142 | 143 | function onTouchmove(e) { 144 | // ensure swiping with one touch and not pinching 145 | if (e.touches.length > 1 || (e.scale && e.scale !== 1)) return 146 | 147 | const coords = getTouchCoordinatesFromEvent(e) 148 | deltaX = coords.pageX - touchOffsetX 149 | deltaY = coords.pageY - touchOffsetY 150 | 151 | if (typeof isScrolling === 'undefined') { 152 | isScrolling = abs(deltaX) < abs(deltaY) 153 | if (!isScrolling) document.ontouchmove = e => e.preventDefault() 154 | return 155 | } 156 | 157 | if (!isScrolling) { 158 | e.preventDefault() 159 | _translate(0, LINEAR_ANIMATION, deltaX * -1) 160 | } 161 | } 162 | 163 | function onTouchend() { 164 | // hack the document to block scroll 165 | document.ontouchmove = () => true 166 | 167 | if (!isScrolling) { 168 | /** 169 | * is valid if: 170 | * -> swipe distance is greater than the specified valid swipe distance 171 | * -> swipe distance is more then a third of the swipe area 172 | * @isValidSlide {Boolean} 173 | */ 174 | const isValid = abs(deltaX) > VALID_SWIPE_DISTANCE 175 | 176 | /** 177 | * is out of bounds if: 178 | * -> index is 0 and deltaX is greater than 0 179 | * -> index is the last slide and deltaX is smaller than 0 180 | * @isOutOfBounds {Boolean} 181 | */ 182 | const direction = deltaX < 0 183 | const isOutOfBounds = 184 | (direction === false && index === 0) || 185 | (direction === true && index === items - 1) 186 | 187 | /** 188 | * If the swipe is valid and we're not out of bounds 189 | * -> Slide to the direction 190 | * otherwise: go back to the previous slide with a linear animation 191 | */ 192 | isValid === true && isOutOfBounds === false 193 | ? slide(direction) 194 | : _translate(slideSpeed, LINEAR_ANIMATION) 195 | } 196 | 197 | // reset variables with the initial values 198 | deltaX = deltaY = touchOffsetX = touchOffsetY = 0 199 | } 200 | 201 | /** 202 | * public 203 | * setup function 204 | */ 205 | function _setup() { 206 | slidesDOMEl.addEventListener(TRANSITION_END, onTransitionEnd) 207 | containerDOMEl.addEventListener('touchstart', onTouchstart, EVENT_OPTIONS) 208 | containerDOMEl.addEventListener('touchmove', onTouchmove, EVENT_OPTIONS) 209 | containerDOMEl.addEventListener('touchend', onTouchend, EVENT_OPTIONS) 210 | 211 | if (index !== 0) { 212 | _translate(0) 213 | } 214 | } 215 | 216 | /** 217 | * public 218 | * clean content of the slider 219 | */ 220 | function clean() { 221 | return cleanContainer(slidesDOMEl) 222 | } 223 | 224 | /** 225 | * public 226 | * @param {number} nextIndex Index number to go to 227 | */ 228 | function goTo(nextIndex) { 229 | // if the nextIndex and the current is the same, we don't need to do the slide 230 | if (nextIndex === index) return 231 | 232 | // if the nextIndex is possible according to number of items, then use it 233 | if (nextIndex <= items) { 234 | // execute the callback from the options before sliding 235 | doBeforeSlide({currentSlide: index, nextSlide: nextIndex}) 236 | // execute the internal callback 237 | nextIndex > index ? onNext(nextIndex) : onPrev(nextIndex) 238 | index = nextIndex 239 | } 240 | // translate to the next index by a defined duration and ease function 241 | _translate(slideSpeed, ease) 242 | 243 | // execute the callback from the options after sliding 244 | slidesDOMEl.addEventListener(TRANSITION_END, function cb(e) { 245 | doAfterSlide({currentSlide: index}) 246 | e.currentTarget.removeEventListener(e.type, cb) 247 | }) 248 | } 249 | 250 | /** 251 | * public 252 | * prev function: called on clickhandler 253 | */ 254 | function prev(e) { 255 | e.preventDefault() 256 | e.stopPropagation() 257 | slide(false) 258 | } 259 | 260 | /** 261 | * public 262 | * next function: called on clickhandler 263 | */ 264 | function next(e) { 265 | e.preventDefault() 266 | e.stopPropagation() 267 | slide(true) 268 | } 269 | 270 | /** 271 | * public 272 | * @param {number} newItems Number of items in the slider for dynamic content 273 | */ 274 | function updateItems(newItems) { 275 | items = newItems 276 | } 277 | 278 | /** 279 | * public 280 | * destroy function: called to gracefully destroy the slidy instance 281 | */ 282 | function destroy() { 283 | // remove all touch listeners 284 | containerDOMEl.removeEventListener( 285 | 'touchstart', 286 | onTouchstart, 287 | EVENT_OPTIONS 288 | ) 289 | containerDOMEl.removeEventListener('touchmove', onTouchmove, EVENT_OPTIONS) 290 | containerDOMEl.removeEventListener('touchend', onTouchend, EVENT_OPTIONS) 291 | // remove transition listeners 292 | slidesDOMEl.removeEventListener(TRANSITION_END, onTransitionEnd) 293 | } 294 | 295 | // trigger initial setup 296 | _setup() 297 | 298 | // expose public api 299 | return { 300 | clean, 301 | destroy, 302 | goTo, 303 | next, 304 | prev, 305 | slide, 306 | updateItems 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const {default: ReactSlidy} = require('../src/index') 3 | const {render, screen} = require('@testing-library/react') 4 | 5 | describe('ReactSlidy', () => { 6 | test('renders without problems without lazyLoad', () => { 7 | render( 8 | 9 | slide 1 10 | slide 2 11 | slide 3 12 | slide 4 13 | 14 | ) 15 | 16 | screen.getByText('slide 1') 17 | }) 18 | 19 | test('renders without problems with lazyLoad', async () => { 20 | render( 21 | 22 | slide 1 23 | slide 2 24 | slide 3 25 | slide 4 26 | 27 | ) 28 | 29 | await screen.findByText('slide 1') 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /test/slidy.spec.js: -------------------------------------------------------------------------------- 1 | const {clampNumber, infiniteIndex, translate} = require('../src/slidy') 2 | 3 | describe('clampNumber', () => { 4 | test('when a number that is less than min value is provided then the min value is returned', () => { 5 | const when = [ 6 | {value: 2, minValue: 4, maxValue: 8, expected: 4}, 7 | {value: -3, minValue: 0, maxValue: 4, expected: 0} 8 | ] 9 | 10 | when.forEach(({value, minValue, maxValue, expected}) => { 11 | expect(clampNumber(value, minValue, maxValue)).toBe(expected) 12 | }) 13 | }) 14 | 15 | test('when a number that is greater than max value is provided then the max value is returned', () => { 16 | expect(clampNumber(16, 4, 8)).toBe(8) 17 | }) 18 | 19 | test('when a number that is greater than max value is provided then the max value is returned', () => { 20 | expect(clampNumber(7, 4, 8)).toBe(7) 21 | }) 22 | }) 23 | 24 | describe('infiniteIndex', () => { 25 | test('when a number that is less than 0 is provided then the end value - 1 is returned', () => { 26 | const when = [ 27 | {value: -1, endValue: 4, expected: 3}, 28 | {value: -3, endValue: 9, expected: 8} 29 | ] 30 | 31 | when.forEach(({value, endValue, expected}) => { 32 | expect(infiniteIndex(value, endValue)).toBe(expected) 33 | }) 34 | }) 35 | 36 | test('when a number that is greater than end value is provided then 0 is returned', () => { 37 | expect(infiniteIndex(12, 8)).toBe(0) 38 | }) 39 | 40 | test('when a number that is between 0 and the end value is provided then the same value is returned', () => { 41 | const when = [ 42 | {value: 0, endValue: 4, expected: 0}, 43 | {value: 1, endValue: 9, expected: 1}, 44 | {value: 5, endValue: 9, expected: 5} 45 | ] 46 | 47 | when.forEach(({value, endValue, expected}) => { 48 | expect(infiniteIndex(value, endValue)).toBe(expected) 49 | }) 50 | }) 51 | }) 52 | 53 | describe('translate', () => { 54 | test('when you want to translate to the right, the next slide', () => { 55 | // when 56 | const to = 1 57 | const fineTuningPixels = -57 58 | const numberOfSlides = 1 59 | const percentage = 100 / numberOfSlides 60 | 61 | expect(translate(to, fineTuningPixels, percentage)).toBe( 62 | 'translate3d(calc(-100% - -57px), 0, 0)' 63 | ) 64 | }) 65 | 66 | test('when you want to translate to the left, the previous slide', () => { 67 | // when 68 | const to = -1 69 | const fineTuningPixels = -57 70 | const numberOfSlides = 1 71 | const percentage = 100 / numberOfSlides 72 | 73 | expect(translate(to, fineTuningPixels, percentage)).toBe( 74 | 'translate3d(calc(100% - -57px), 0, 0)' 75 | ) 76 | }) 77 | }) 78 | --------------------------------------------------------------------------------