├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github
├── FUNDING.yml
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── .prettierrc
├── .travis.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── __mocks__
└── fileMock.js
├── babel.config.js
├── example
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── App.js
│ ├── App.test.js
│ ├── components
│ │ ├── Carousel1.js
│ │ ├── Carousel2.js
│ │ ├── Carousel3.js
│ │ ├── Carousel4.js
│ │ ├── Carousel5.js
│ │ ├── Carousel6.js
│ │ ├── Carousel7.js
│ │ ├── Carousel8.js
│ │ ├── Footer.js
│ │ └── Header.js
│ ├── index.css
│ ├── index.js
│ └── utils
│ │ └── TwoWayMap.js
└── yarn.lock
├── package.json
├── react-gallery-carousel-gatsby
├── .gitignore
├── README.md
├── gatsby-config.js
├── package.json
└── src
│ ├── images
│ └── icon.png
│ └── pages
│ ├── 404.js
│ └── index.js
├── react-gallery-carousel-nextjs
├── .gitignore
├── README.md
├── components
│ ├── Carousel1.js
│ ├── Carousel2.js
│ ├── Carousel3.js
│ ├── Carousel4.js
│ └── Carousel5.js
├── next.config.js
├── package-lock.json
├── package.json
├── pages
│ ├── _app.js
│ └── index.js
├── public
│ ├── favicon.ico
│ └── vercel.svg
└── styles
│ ├── Home.module.css
│ └── globals.css
├── src
├── .eslintrc
├── common
│ └── common.scss
├── components
│ ├── Carousel
│ │ ├── Carousel.js
│ │ ├── Carousel.module.css
│ │ ├── Carousel.test.js
│ │ ├── __snapshots__
│ │ │ └── Carousel.test.js.snap
│ │ ├── constants.js
│ │ ├── index.js
│ │ └── props.js
│ ├── IconButton
│ │ ├── IconButton.js
│ │ ├── IconButton.module.css
│ │ └── index.js
│ ├── Image
│ │ ├── Image.js
│ │ ├── Image.module.css
│ │ ├── ImageThumbnail.js
│ │ ├── constants.js
│ │ └── index.js
│ ├── Slide
│ │ ├── Slide.js
│ │ ├── Slide.module.css
│ │ └── index.js
│ ├── Slides
│ │ ├── Slides.js
│ │ ├── Slides.module.css
│ │ └── index.js
│ ├── Thumbnail
│ │ ├── Thumbnail.js
│ │ ├── Thumbnail.module.scss
│ │ └── index.js
│ ├── Thumbnails
│ │ ├── Thumbnails.js
│ │ ├── Thumbnails.module.scss
│ │ └── index.js
│ ├── UserSlide
│ │ ├── UserSlide.js
│ │ ├── UserSlide.module.scss
│ │ └── index.js
│ └── Widgets
│ │ ├── Widgets.js
│ │ ├── Widgets.module.css
│ │ └── index.js
├── index.js
├── index.test.js
└── utils
│ ├── ReversedMap.js
│ ├── SlidesFactory.js
│ ├── SlidesFactory.test.js
│ ├── isSSR.js
│ ├── useAnchor.js
│ ├── useEventListener.js
│ ├── useFixedPosition.js
│ ├── useIntersectionObserver.js
│ ├── useKeyboard.js
│ ├── useKeys.js
│ ├── useMediaQuery.js
│ ├── useMouse.js
│ ├── useMouseDrag.js
│ ├── useNoDrag.js
│ ├── useNoOverScroll.js
│ ├── useNoSwipe.js
│ ├── useSlides.js
│ ├── useSwipe.js
│ ├── useTimer.js
│ ├── useTouch.js
│ └── validators.js
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/
2 | dist/
3 | node_modules/
4 | public/
5 | .snapshots/
6 | *.min.js
7 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": [
4 | "standard",
5 | "standard-react",
6 | "plugin:prettier/recommended",
7 | "prettier/standard",
8 | "prettier/react"
9 | ],
10 | "env": {
11 | "node": true,
12 | "jest": true
13 | },
14 | "parserOptions": {
15 | "ecmaVersion": 2020,
16 | "ecmaFeatures": {
17 | "legacyDecorators": true,
18 | "jsx": true
19 | }
20 | },
21 | "settings": {
22 | "react": {
23 | "version": "17"
24 | }
25 | },
26 | "plugins": [
27 | "react-hooks"
28 | ],
29 | "rules": {
30 | "space-before-function-paren": 0,
31 | "react/prop-types": 0,
32 | "react/jsx-handler-names": 0,
33 | "react/jsx-fragments": 0,
34 | "react/no-unused-prop-types": 0,
35 | "import/export": 0,
36 | "react-hooks/rules-of-hooks": "error",
37 | "react-hooks/exhaustive-deps": "warn"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: yifaneye
2 | patreon: yifanai
3 | buymeacoffee: yifanai
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # See https://help.github.com/ignore-files/ for more about ignoring files.
3 |
4 | # dependencies
5 | node_modules
6 |
7 | # builds
8 | build
9 | dist
10 | .rpt2_cache
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 |
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "jsxSingleQuote": true,
4 | "semi": true,
5 | "tabWidth": 2,
6 | "bracketSpacing": true,
7 | "jsxBracketSameLine": false,
8 | "arrowParens": "always",
9 | "trailingComma": "none"
10 | }
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 14
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # ```react-gallery-carousel```
2 |
3 | [](https://www.npmjs.com/package/react-gallery-carousel)
4 |
5 | ## v0.4.0 (2023-01-28)
6 |
7 | ### Enhancements
8 |
9 | - Add support for custom elements.
10 |
11 | - Improve format in the README file.
12 |
13 | ## v0.3.0 (2022-10-15)
14 |
15 | ### Bug Fixes
16 |
17 | - Fix issue related to maximizing/minimizing not working when shouldMaximizeOnClick/shouldMinimizeOnClick is on, for Toggle Device Toolbar (simulated mobile devices).
18 | ([Issue #74](https://github.com/yifaneye/react-gallery-carousel/issues/74))
19 |
20 | - Fix issue related to `npm install` not working for `/example`.
21 | ([Issue #72](https://github.com/yifaneye/react-gallery-carousel/issues/72))
22 |
23 | ### Enhancements
24 |
25 | - Update package dependencies. ([Issue #71](https://github.com/yifaneye/react-gallery-carousel/issues/71))
26 |
27 | ## v0.2.10 (2022-08-03)
28 |
29 | ### Enhancements
30 |
31 | - Add react 18 to peer dependencies.
32 |
33 | ## v0.2.9 (2022-01-30)
34 |
35 | ### Bug Fixes
36 |
37 | - Third attempt to fix swiping stuck after zoom issue.
38 | ([Issue #44](https://github.com/yifaneye/react-gallery-carousel/issues/44))
39 |
40 | ## v0.2.8 (2021-12-23)
41 |
42 | ### Bug Fixes
43 |
44 | - Second attempt to fix swiping stuck after zoom issue.
45 | ([Issue #44](https://github.com/yifaneye/react-gallery-carousel/issues/44))
46 |
47 | ### Enhancements
48 |
49 | - Simplify Gatsby example
50 |
51 | ## v0.2.7 (2021-12-18)
52 |
53 | ### Enhancements
54 |
55 | - Fix "invalid or unexpected token" issue in Next.js.
56 | ([Issue #51](https://github.com/yifaneye/react-gallery-carousel/issues/51))
57 |
58 | - Remove request to "object Object"
59 | ([Issue #52](https://github.com/yifaneye/react-gallery-carousel/issues/52))
60 |
61 | ## v0.2.6 (2021-12-06)
62 |
63 | ### Enhancements
64 |
65 | - Fix swiping stuck after zoom issue.
66 | ([Issue #44](https://github.com/yifaneye/react-gallery-carousel/issues/44))
67 |
68 | - Remove title for icons.
69 | ([Issue #60](https://github.com/yifaneye/react-gallery-carousel/issues/60))
70 |
71 | ## v0.2.5 (2021-10-30)
72 |
73 | ### Enhancements
74 |
75 | - Added **support for custom children for the thumbnails**.
76 | ([Issue #40](https://github.com/yifaneye/react-gallery-carousel/issues/40))
77 |
78 | - Enhanced docs.
79 |
80 | ## v0.2.4 (2021-08-22)
81 |
82 | ### Enhancements
83 |
84 | - Enhanced docs.
85 | ([Issue #43](https://github.com/yifaneye/react-gallery-carousel/issues/43))
86 |
87 | - Enhanced code and examples.
88 | - Set up momentum-based mouse dragging on the thumbnails.
89 |
90 | ## v0.2.3 (2021-07-12)
91 |
92 | ### Enhancements
93 |
94 | - Made the code robuster.
95 | ([Issue #31](https://github.com/yifaneye/react-gallery-carousel/issues/31))
96 |
97 | - Enhanced docs.
98 | ([Issue #35](https://github.com/yifaneye/react-gallery-carousel/issues/35))
99 |
100 | - Added **support for server-side rendering (SSR)**.
101 | ([Issue #36](https://github.com/yifaneye/react-gallery-carousel/issues/36))
102 |
103 | - Enhanced examples.
104 | ([Issue #45](https://github.com/yifaneye/react-gallery-carousel/issues/45))
105 |
106 | ## v0.2.2 (2021-05-16)
107 |
108 | ### Enhancements
109 |
110 | - Enhanced the documentation regarding user-managed slides.
111 | ([Issue #28](https://github.com/yifaneye/react-gallery-carousel/issues/28))
112 |
113 | - Updated the [demo site](https://yifanai.com/rgc).
114 |
115 | ## v0.2.1 (2021-05-08)
116 |
117 | ### Bug Fixes
118 |
119 | - Fixed issue where caption for an image always has vertical and horizontal scroll bars even they are not required.
120 | ([Issue #26](https://github.com/yifaneye/react-gallery-carousel/issues/26))
121 |
122 | ### Enhancements
123 |
124 | - Enhanced content on the [demo site](https://yifanai.com/rgc).
125 |
126 | - Enhanced examples and content in the [documentation](https://yifanai.com/rgc).
127 | ([Issue #28](https://github.com/yifaneye/react-gallery-carousel/issues/28))
128 |
129 | ## v0.2.0 (2021-05-02)
130 |
131 | ### Bug Fixes
132 |
133 | - Fixed a bug where left and right keys do not work when a widget is focused by listening keydown events on the entire carousel.
134 | ([Issue #15](https://github.com/yifaneye/react-gallery-carousel/issues/15))
135 |
136 | - Fixed a bug where swiping on a slide does not freeze autoplay countdown.
137 | ([Issue #18](https://github.com/yifaneye/react-gallery-carousel/issues/18))
138 |
139 | ### Enhancements
140 |
141 | - Allowed the size of the thumbnails to be customized.
142 | ([Issue #11](https://github.com/yifaneye/react-gallery-carousel/issues/11))
143 |
144 | - Added handlers and listeners to the carousel.
145 | ([Issue #12](https://github.com/yifaneye/react-gallery-carousel/issues/12))
146 |
147 | - Enabled smooth transition on the thumbnails.
148 | ([Issue #13](https://github.com/yifaneye/react-gallery-carousel/issues/13))
149 |
150 | - Added aria-live region for accessibility.
151 |
152 | ## v0.1.4 (2021-04-18)
153 |
154 | ### Bug Fixes
155 |
156 | - Fixed a minor bug where required props is undefined by defining default props.
157 | ([Issue #7](https://github.com/yifaneye/react-gallery-carousel/issues/7))
158 |
159 | ### Enhancements
160 |
161 | - Enhanced content and styles on the [demo site](https://yifanai.com/rgc).
162 |
163 | ## v0.1.3 (2021-04-12)
164 |
165 | ### Bug Fixes
166 |
167 | - Fixed a bug where touch swiping did not work on the slides when the images are dynamically set in the useEffect() hook by developer users.
168 | ([Issue #5](https://github.com/yifaneye/react-gallery-carousel/issues/5))
169 |
170 | ### Enhancements
171 |
172 | - Enhanced content, styles, responsiveness and performance (from 98% to 100%) on the [demo site](https://yifanai.com/rgc).
173 |
174 | ## v0.1.2 (2021-04-11)
175 |
176 | ### Bug Fixes
177 |
178 | - Fixed a bug where the last slide is not displayed starting from the second loop when there are exactly 2 slides.
179 | ([Issue #2](https://github.com/yifaneye/react-gallery-carousel/issues/2))
180 |
181 | ### Enhancements
182 |
183 | - Removed the use of translate3d to make the performance consistent across browsers and platforms.
184 | - Reduced the file size of the fallback image from 7.59 kB to 3.35 kB.
185 | - Made the text displayed in the index board robuster.
186 |
187 | ## v0.1.1 (2021-04-10)
188 |
189 | ### Bug Fixes
190 |
191 | - Fixed a bug where image URLs those are not known ahead of time cannot be placed into the carousel.
192 | ([Issue #1](https://github.com/yifaneye/react-gallery-carousel/issues/1))
193 |
194 | ### Enhancements
195 |
196 | - Enabled hitting Enter (Return) key on the focused thumbnail to display that image as the current image.
197 | - Enabled selecting text in the index board using cursor or finger.
198 | - Removed the effect of hover on thumbnails on touch devices.
199 |
200 | ## v0.1.0 (2021-03-31)
201 |
202 | ### Initial work
203 |
204 | - Complete a dependency-free React carousel component with support for lazy loading, pinch zoom, touch swiping, mouse dragging, velocity detection, maximization, thumbnails, keyboard navigation and accessibility.
205 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | admin@yifanai.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | When contributing to this repository, please first discuss the change you wish to make via issue,
4 | email, or any other method with the owners of this repository before making a change.
5 |
6 | Please note we have a code of conduct, please follow it in all your interactions with the project.
7 |
8 | ## Pull Request Process
9 |
10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a
11 | build.
12 | 2. Update the README.md with details of changes to the interface, this includes props, environment
13 | variables, exposed ports, useful file locations and container parameters.
14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this
15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
16 | 4. You may merge the Pull Request in once you have the sign-off of 1 other developer, or if you
17 | do not have permission to do that, you may request the reviewer to merge it for you.
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Yifan Ai
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/__mocks__/fileMock.js:
--------------------------------------------------------------------------------
1 | module.exports = '';
2 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['@babel/preset-env', '@babel/preset-react']
3 | };
4 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | This example was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | It is linked to the react-gallery-carousel package in the parent directory for development purposes.
4 |
5 | You can run `yarn install` and then `yarn start` to test your package.
6 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-gallery-carousel-example",
3 | "homepage": ".",
4 | "version": "0.0.0",
5 | "private": true,
6 | "scripts": {
7 | "start": "node ../node_modules/react-scripts/bin/react-scripts.js start",
8 | "build": "node ../node_modules/react-scripts/bin/react-scripts.js build",
9 | "test": "node ../node_modules/react-scripts/bin/react-scripts.js test",
10 | "eject": "node ../node_modules/react-scripts/bin/react-scripts.js eject"
11 | },
12 | "dependencies": {
13 | "react": "link:../node_modules/react",
14 | "react-dom": "link:../node_modules/react-dom",
15 | "react-gallery-carousel": "link:..",
16 | "react-github-btn": "^1.2.0",
17 | "react-responsive-button": "^0.2.1",
18 | "react-scripts": "link:../node_modules/react-scripts"
19 | },
20 | "devDependencies": {
21 | "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
22 | "puppeteer": "^7.1.0"
23 | },
24 | "eslintConfig": {
25 | "extends": "react-app"
26 | },
27 | "browserslist": [
28 | ">0.2%",
29 | "not dead",
30 | "not ie <= 11",
31 | "not op_mini all"
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/example/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yifaneye/react-gallery-carousel/a3f452b143f6c1149477a36e43ee4698e67d3363/example/public/favicon.ico
--------------------------------------------------------------------------------
/example/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
30 | react-gallery-carousel
31 |
32 |
33 |
34 |
35 |
36 | You need to enable JavaScript to run this app.
37 |
38 |
39 |
40 |
41 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/example/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "react-gallery-carousel",
3 | "name": "react-gallery-carousel",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/example/public/robots.txt:
--------------------------------------------------------------------------------
1 | # robots.txt for https://yifaneye.github.io/react-gallery-carousel/
2 |
3 | User-agent: *
4 | Allow: *
5 |
--------------------------------------------------------------------------------
/example/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Header from './components/Header';
3 | import Carousel1 from './components/Carousel1';
4 | import Carousel2 from './components/Carousel2';
5 | import Carousel3 from './components/Carousel3';
6 | import Carousel4 from './components/Carousel4';
7 | import Carousel5 from './components/Carousel5';
8 | import Carousel6 from './components/Carousel6';
9 | import Carousel7 from './components/Carousel7';
10 | import Carousel8 from './components/Carousel8';
11 | import Footer from './components/Footer';
12 |
13 | const imageIDs = Array(30) // the maximum is currently 149
14 | .fill(1)
15 | .map((_, i) => i + 1);
16 | const images = imageIDs.map((imageID) => {
17 | return {
18 | src: `https://placedog.net/400/240?id=${imageID}`,
19 | srcset: `https://placedog.net/400/240?id=${imageID} 400w, https://placedog.net/700/420?id=${imageID} 700w, https://placedog.net/1000/600?id=${imageID} 1000w`,
20 | sizes: '(max-width: 1000px) 400px, (max-width: 2000px) 700px, 1000px',
21 | alt: `Dog No. ${imageID}. Dogs are domesticated mammals, not natural wild animals.`,
22 | thumbnail: `https://placedog.net/100/60?id=${imageID}`
23 | };
24 | });
25 |
26 | const App = () => {
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 | };
42 |
43 | export default App;
44 |
--------------------------------------------------------------------------------
/example/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import puppeteer from 'puppeteer';
5 | const { test } = global;
6 |
7 | // unit testing
8 | test('Carousel renders without crashing', () => {
9 | // since .scrollTo() isn't implemented in jsdom
10 | Element.prototype.scrollTo = () => {};
11 |
12 | global.IntersectionObserver = function () {
13 | return {
14 | observe: () => {},
15 | disconnect: () => {}
16 | };
17 | };
18 |
19 | Object.defineProperty(window, 'matchMedia', {
20 | writable: true,
21 | value: (query) => ({
22 | matches: false,
23 | media: query,
24 | onchange: null,
25 | addEventListener: () => {},
26 | removeEventListener: () => {}
27 | })
28 | });
29 |
30 | const div = document.createElement('div');
31 | ReactDOM.render( , div);
32 | ReactDOM.unmountComponentAtNode(div);
33 | });
34 |
35 | // automated browser (e2e) testing
36 | test('Carousel can be controlled', async () => {
37 | const browser = await puppeteer.launch({
38 | // headless: false,
39 | // slowMo: 100,
40 | // devtools: true,
41 | // product: 'firefox',
42 | // args: ['--window-size=998, 1178']
43 | });
44 |
45 | const page = await browser.newPage();
46 | const navigationPromise = page.waitForNavigation();
47 | await page.goto('http://localhost:3000');
48 | await page.setViewport({ width: 998, height: 1178 });
49 | await navigationPromise;
50 | await page.waitForTimeout(1000);
51 |
52 | // await page.waitForSelector('[aria-label="Go to Next Slide"]');
53 | await page.click('[aria-label="Go to Next Slide"]');
54 |
55 | // await page.waitForSelector('[aria-label="Go to Previous Slide"]');
56 | await page.click('[aria-label="Go to Previous Slide"]');
57 |
58 | // await page.waitForSelector('[aria-label="Maximize Slides"]');
59 | await page.click('[aria-label="Maximize Slides"]');
60 |
61 | // await page.waitForSelector('[aria-label="Minimize Slides"]');
62 | await page.click('[aria-label="Minimize Slides"]');
63 |
64 | // await page.waitForSelector('[aria-label="Go to Slide 3"]');
65 | await page.click('[aria-label="Go to Slide 3"]');
66 |
67 | // const index = await page.$eval(
68 | // '[aria-label="Slide 3 of 3"]',
69 | // (el) => el.textContent
70 | // );
71 | // expect(index).toBe('3 / 3');
72 |
73 | // await page.waitForSelector('[aria-label="Start Autoplay"]');
74 | await page.click('[aria-label="Start Autoplay"]');
75 |
76 | // await page.waitForSelector('[aria-label="Pause Autoplay"]');
77 | await page.click('[aria-label="Pause Autoplay"]');
78 |
79 | // await page.screenshot({ path: 'record/screenshot.jpg' });
80 | // await page.pdf({ path: 'record/print.pdf', format: 'a4' });
81 |
82 | await browser.close();
83 | });
84 |
--------------------------------------------------------------------------------
/example/src/components/Carousel1.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TwoWayMap from '../utils/TwoWayMap';
3 | import Carousel from 'react-gallery-carousel';
4 | import 'react-gallery-carousel/dist/index.css';
5 |
6 | const Carousel1 = () => {
7 | const indexToTitle = new TwoWayMap({
8 | 0: 'Introduction',
9 | 1: 'Get%20Started',
10 | 2: 'Usage'
11 | });
12 |
13 | return (
14 |
15 |
25 |
26 |
{
40 | // const title = indexToTitle.get(curIndex);
41 | // window.history.replaceState(
42 | // undefined,
43 | // undefined,
44 | // `#${title}`
45 | // );
46 | // document.title = `${title} | react-gallery-carousel`;
47 | // }} // this callback can be set to update the document title and URL hash on index update
48 | style={{ userSelect: 'text' }}
49 | >
50 |
51 |
Introduction
52 |
53 | react-gallery-carousel is a mobile-friendly
54 | dependency-free React carousel component with support for touch,
55 | mouse dragging, lazy loading, thumbnails, modal, keyboard
56 | navigation, RTL and pinch to zoom.
57 |
58 |
Demo
59 |
/
60 |
61 | GitHub
62 |
63 |
/
64 |
65 | npm
66 |
67 |
/
68 |
69 | Documentation
70 |
71 |
72 |
73 |
Get Started
74 |
npm install react-gallery-carousel --save
75 |
76 | or
77 |
78 |
yarn add react-gallery-carousel
79 |
80 |
81 |
Usage
82 |
The default carousel shown below as example 2 is created by:
83 |
{' '}
84 |
85 |
86 |
87 |
88 | );
89 | };
90 |
91 | export default Carousel1;
92 |
--------------------------------------------------------------------------------
/example/src/components/Carousel2.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Carousel from 'react-gallery-carousel';
3 | import 'react-gallery-carousel/dist/index.css';
4 |
5 | const Carousel2 = ({ images }) => {
6 | return (
7 |
8 |
9 |
10 | Example 2: Default carousel with images{' '}
11 | code
12 |
13 |
14 | A default carousel example has lazy loading and preloading (the 2
15 | adjacent images on either side of the current image); touch swiping
16 | and mouse dragging on the carousel; touch swiping, mouse dragging and
17 | wheel scrolling on the thumbnails; touch swipe down to exit the
18 | maximized carousel; and keyboard navigation.
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default Carousel2;
29 |
--------------------------------------------------------------------------------
/example/src/components/Carousel3.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Carousel from 'react-gallery-carousel';
3 | import 'react-gallery-carousel/dist/index.css';
4 |
5 | const Carousel3 = ({ images }) => {
6 | return (
7 |
8 |
9 |
10 | Example 3: Default carousel with images and with right-to-left (RTL){' '}
11 | code
12 |
13 |
14 | A default carousel example has lazy loading and preloading; touch
15 | swiping and mouse dragging on the carousel; touch swiping, mouse
16 | dragging and wheel scrolling on the thumbnails; touch swipe down to
17 | exit the maximized carousel; and keyboard navigation.
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | export default Carousel3;
28 |
--------------------------------------------------------------------------------
/example/src/components/Carousel4.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import Carousel from 'react-gallery-carousel';
3 | import 'react-gallery-carousel/dist/index.css';
4 |
5 | const Carousel4 = ({ images }) => {
6 | const [dynamicImages, setDynamicImages] = useState([]);
7 |
8 | useEffect(() => {
9 | setDynamicImages(images);
10 | }, [setDynamicImages, images]);
11 |
12 | return (
13 |
14 |
15 |
16 | Example 4: Customized carousel with dynamic images{' '}
17 | (available from v0.1.1) {' '}
18 | code
19 |
20 |
21 | This example has images dynamically set in the{' '}
22 | useEffect()
hook. This customized example additionally
23 | has click to enter and exit the maximized carousel ;
24 | custom initial index; custom widget positions; custom thumbnails,
25 | custom dot buttons and captions for the maximized carousel; custom
26 | active and passive dot buttons; and custom styles for the
27 | non-maximized carousel.
28 |
29 |
30 |
31 |
51 | 🔳
52 |
53 | }
54 | passiveIcon={
55 |
56 | 🔲
57 |
58 | }
59 | />
60 |
61 |
62 | );
63 | };
64 |
65 | export default Carousel4;
66 |
--------------------------------------------------------------------------------
/example/src/components/Carousel5.js:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 | import Carousel from 'react-gallery-carousel';
3 | import 'react-gallery-carousel/dist/index.css';
4 | import { Button } from 'react-responsive-button';
5 | import 'react-responsive-button/dist/index.css';
6 |
7 | const Carousel5 = ({ images }) => {
8 | const carouselRef = useRef(null);
9 |
10 | return (
11 |
12 |
13 |
14 | Example 5: Default carousel with imperative handlers{' '}
15 | (available from v0.2.0) {' '}
16 | code
17 |
18 |
19 | To customize the carousel in a declarative manner, pass the props
20 | (e.g. isAutoPlaying
, isMaximized
,{' '}
21 | index
). To customize the carousel in an imperative
22 | manner, use the following handlers (on the ref):
23 |
24 |
25 |
26 |
27 | carouselRef.current.play()}
30 | >
31 | play()
32 |
33 |
34 |
35 | carouselRef.current.pause()}
38 | >
39 | pause()
40 |
41 |
42 |
43 | carouselRef.current.toggleIsPlaying()}
46 | >
47 | toggleIsPlaying()
48 |
49 |
50 |
51 |
52 |
53 | carouselRef.current.maximize()}
57 | >
58 | maximize()
59 |
60 |
61 |
62 | carouselRef.current.minimize()}
66 | >
67 | minimize()
68 |
69 |
70 |
71 | carouselRef.current.toggleIsMaximized()}
75 | >
76 | toggleIsMaximized()
77 |
78 |
79 |
80 |
81 |
82 | carouselRef.current.goLeft()}
86 | >
87 | goLeft()
88 |
89 |
90 |
91 | carouselRef.current.goRight()}
95 | >
96 | goRight()
97 |
98 |
99 |
100 | carouselRef.current.goToIndex(0)}
104 | >
105 | goToIndex(0)
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 | );
116 | };
117 |
118 | export default Carousel5;
119 |
--------------------------------------------------------------------------------
/example/src/components/Carousel6.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Carousel from 'react-gallery-carousel';
3 | import 'react-gallery-carousel/dist/index.css';
4 |
5 | const Carousel6 = ({ images }) => {
6 | const thumbnails = images.map((_, index) => (
7 |
8 | {index + 1}
9 |
10 | ));
11 | const imageElements = images.map((image, index) => (
12 |
18 | ));
19 |
20 | return (
21 |
22 |
23 |
24 | Example 6: Carousel with custom children and custom thumbnails{' '}
25 | code
26 |
27 |
28 |
29 | {imageElements}
30 |
31 |
32 | );
33 | };
34 |
35 | export default Carousel6;
36 |
--------------------------------------------------------------------------------
/example/src/components/Carousel7.js:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 | import Carousel from 'react-gallery-carousel';
3 | import 'react-gallery-carousel/dist/index.css';
4 |
5 | const Carousel7 = ({ images }) => {
6 | const carouselRef = useRef(null);
7 |
8 | return (
9 |
10 |
11 |
12 | Example 7: Default carousel with custom icons{' '}
13 | code
14 |
15 |
16 | To customize the icons, pass custom icon component to props (e.g.{' '}
17 | leftIcon
, rightIcon
).
18 |
19 |
20 |
21 |
36 | ↗️
37 |
38 | }
39 | minIcon={
40 |
48 | ↙️
49 |
50 | }
51 | leftIcon={
52 |
60 | ◀️
61 |
62 | }
63 | rightIcon={
64 |
72 | ▶️
73 |
74 | }
75 | />
76 |
77 |
78 | );
79 | };
80 |
81 | export default Carousel7;
82 |
--------------------------------------------------------------------------------
/example/src/components/Carousel8.js:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 | import Carousel from 'react-gallery-carousel';
3 | import 'react-gallery-carousel/dist/index.css';
4 | import { Button } from 'react-responsive-button';
5 |
6 | const Carousel8 = ({ images }) => {
7 | const carouselRef = useRef(null);
8 |
9 | return (
10 |
11 |
12 |
13 | Example 8: Default carousel with custom elements{' '}
14 | (available from v0.4.0) {' '}
15 | code
16 |
17 |
18 | To use custom elements, set the widget props (e.g.{' '}
19 | hasLeftButton
, hasRightButton
) to{' '}
20 | false
, then pass the custom elements to the{' '}
21 | elements
prop.
22 |
23 |
24 |
25 |
36 | carouselRef.current.goLeft()}
46 | >
47 |
55 | ⬅️
56 |
57 |
58 | carouselRef.current.goRight()}
68 | >
69 |
77 | ➡️
78 |
79 |
80 | >
81 | }
82 | />
83 |
84 |
85 | );
86 | };
87 |
88 | export default Carousel8;
89 |
--------------------------------------------------------------------------------
/example/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import GitHubButton from 'react-github-btn';
3 |
4 | const Footer = () => {
5 | return (
6 |
42 | );
43 | };
44 |
45 | export default Footer;
46 |
--------------------------------------------------------------------------------
/example/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import GitHubButton from 'react-github-btn';
3 |
4 | const Header = () => {
5 | return (
6 |
7 |
8 | react-gallery-carousel
9 |
10 | Mobile-friendly Carousel with batteries included (supporting touch,
11 | mouse emulation, lazy loading, thumbnails, fullscreen, RTL, keyboard
12 | navigation and customisations).
13 |
14 |
15 |
21 | Star
22 |
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default Header;
30 |
--------------------------------------------------------------------------------
/example/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
6 | sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
13 | monospace;
14 | padding: 0.2em 0.4em;
15 | margin: 0;
16 | font-size: 85%;
17 | background-color: rgba(27, 31, 35, 0.05);
18 | border-radius: 6px;
19 | }
20 |
21 | .text-slide {
22 | padding: 1rem 50px;
23 | }
24 |
25 | .vertical-separator {
26 | margin: 1em 0;
27 | }
28 |
29 | .carousel-page {
30 | max-width: 1200px;
31 | margin: 0 auto;
32 | overflow: hidden;
33 | }
34 |
35 | .carousel-page-header-container {
36 | width: 100%;
37 | }
38 |
39 | .carousel-page-header {
40 | display: flex;
41 | flex-direction: column;
42 | align-items: center;
43 | padding: 0 1em;
44 | }
45 |
46 | .action-container {
47 | padding: 0 1em;
48 | }
49 |
50 | .star-button-container {
51 | height: 20px;
52 | margin-top: 10px;
53 | margin-bottom: 10px;
54 | }
55 |
56 | .section {
57 | margin-bottom: 60px;
58 | }
59 |
60 | .section-header {
61 | padding: 0 1em;
62 | }
63 |
64 | .carousel-container {
65 | width: 100%;
66 | height: calc(100vw * 0.8);
67 | }
68 |
69 | @media only screen and (min-width: 500px) and (max-width: 1280px) {
70 | .carousel-container {
71 | height: calc(100vw * 0.6);
72 | max-height: 768px;
73 | }
74 | }
75 |
76 | @media only screen and (min-width: 1280px) {
77 | .carousel-container {
78 | height: 768px;
79 | }
80 | }
81 |
82 | .carousel-container.short {
83 | height: 300px;
84 | }
85 |
86 | .buttons {
87 | margin: 10px 0;
88 | display: flex;
89 | flex-wrap: wrap;
90 | }
91 |
92 | .button-container {
93 | margin-bottom: 10px;
94 | }
95 |
96 | .button-container:not(:last-child) {
97 | margin-right: 10px;
98 | }
99 |
100 | .framed-carousel {
101 | border: 5px solid #eee;
102 | overflow: hidden;
103 | border-radius: 10px;
104 | will-change: transform;
105 | mask-image: -webkit-radial-gradient(white, black);
106 | box-sizing: border-box;
107 | }
108 |
109 | .icon-text {
110 | color: white;
111 | }
112 |
113 | .image-responsive {
114 | width: 100%;
115 | height: 100%;
116 | object-fit: cover;
117 | overflow: hidden;
118 | }
119 |
--------------------------------------------------------------------------------
/example/src/index.js:
--------------------------------------------------------------------------------
1 | import './index.css';
2 |
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 | import App from './App';
6 |
7 | ReactDOM.render( , document.getElementById('root'));
8 |
--------------------------------------------------------------------------------
/example/src/utils/TwoWayMap.js:
--------------------------------------------------------------------------------
1 | export default class TwoWayMap {
2 | constructor(map) {
3 | this.map = map;
4 | this.reversedMap = {};
5 |
6 | Object.keys(map).forEach((key) => {
7 | const value = map[key];
8 | this.reversedMap[value] = key;
9 | });
10 | }
11 |
12 | get = (key) => {
13 | return this.map[key];
14 | };
15 |
16 | getReversed = (key) => {
17 | return this.reversedMap[key];
18 | };
19 | }
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-gallery-carousel",
3 | "version": "0.4.0",
4 | "description": "Mobile-friendly Carousel with batteries included (supporting touch, mouse emulation, lazy loading, thumbnails, fullscreen, RTL, keyboard navigation and customisations).",
5 | "author": "yifaneye",
6 | "license": "MIT",
7 | "repository": "yifaneye/react-gallery-carousel",
8 | "main": "dist/index.js",
9 | "module": "dist/index.modern.js",
10 | "source": "src/index.js",
11 | "engines": {
12 | "node": ">=14"
13 | },
14 | "scripts": {
15 | "build": "microbundle-crl --no-compress --format modern,cjs",
16 | "start": "microbundle-crl watch --no-compress --format modern,cjs",
17 | "prepare": "run-s build",
18 | "test": "run-s test:unit test:lint test:build",
19 | "test:build": "run-s build",
20 | "test:lint": "eslint .",
21 | "test:unit": "cross-env CI=1 react-scripts test --env=jsdom",
22 | "test:watch": "react-scripts test --env=jsdom",
23 | "predeploy": "cd example && yarn install && yarn run build",
24 | "deploy": "gh-pages -d example/build"
25 | },
26 | "peerDependencies": {
27 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
28 | },
29 | "devDependencies": {
30 | "babel-eslint": "^10.0.3",
31 | "cross-env": "^7.0.2",
32 | "eslint": "^6.8.0",
33 | "eslint-config-prettier": "^6.7.0",
34 | "eslint-config-standard": "^14.1.0",
35 | "eslint-config-standard-react": "^9.2.0",
36 | "eslint-plugin-import": "^2.18.2",
37 | "eslint-plugin-node": "^11.0.0",
38 | "eslint-plugin-prettier": "^3.1.1",
39 | "eslint-plugin-promise": "^4.2.1",
40 | "eslint-plugin-react": "^7.17.0",
41 | "eslint-plugin-react-hooks": "^4.2.0",
42 | "eslint-plugin-standard": "^4.0.1",
43 | "gh-pages": "^2.2.0",
44 | "microbundle-crl": "^0.13.10",
45 | "node-sass": "^5.0.0",
46 | "npm-run-all": "^4.1.5",
47 | "prettier": "^2.0.4",
48 | "prop-types": "^15.7.2",
49 | "react": "^16.13.1",
50 | "react-dom": "^16.13.1",
51 | "react-scripts": "^3.4.1",
52 | "react-test-renderer": "16.13.1"
53 | },
54 | "files": [
55 | "dist"
56 | ],
57 | "dependencies": {},
58 | "jest": {
59 | "moduleNameMapper": {
60 | "\\.(jpg)$": "/__mocks__/fileMock.js",
61 | "\\.(png)$": "/__mocks__/fileMock.js",
62 | "\\.(css)$": "/__mocks__/fileMock.js"
63 | }
64 | },
65 | "keywords": [
66 | "carousel",
67 | "gallery",
68 | "lightbox",
69 | "slider",
70 | "slideshow",
71 | "swiper",
72 | "react",
73 | "component",
74 | "lazy load",
75 | "preload",
76 | "touch",
77 | "swipe",
78 | "pinch",
79 | "zoom",
80 | "mouse",
81 | "mouse emulation",
82 | "drag",
83 | "keyboard navigation",
84 | "accessibility",
85 | "a11y"
86 | ]
87 | }
88 |
--------------------------------------------------------------------------------
/react-gallery-carousel-gatsby/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .cache/
3 | public
4 |
--------------------------------------------------------------------------------
/react-gallery-carousel-gatsby/README.md:
--------------------------------------------------------------------------------
1 | # react-gallery-carousel Gatsby example
2 |
--------------------------------------------------------------------------------
/react-gallery-carousel-gatsby/gatsby-config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | siteMetadata: {
3 | title: 'react-gallery-carousel Gatsby example',
4 | description:
5 | 'react-gallery-carousel is a mobile-friendly dependency-free React carousel component with support for touch, mouse dragging, lazy loading, thumbnails, modal, keyboard navigation, RTL and pinch to zoom.',
6 | url: 'https://yifanai.com/rgc',
7 | image: '/images/icon.png',
8 | twitterUsername: '@yifaneye'
9 | },
10 | plugins: []
11 | };
12 |
--------------------------------------------------------------------------------
/react-gallery-carousel-gatsby/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-gallery-carousel-gatsby",
3 | "version": "0.0.0",
4 | "private": true,
5 | "description": "react-gallery-carousel Gatsby example",
6 | "author": "Yifan Ai",
7 | "keywords": [
8 | "gatsby"
9 | ],
10 | "scripts": {
11 | "develop": "gatsby develop",
12 | "start": "gatsby develop",
13 | "build": "gatsby build",
14 | "serve": "gatsby serve",
15 | "clean": "gatsby clean"
16 | },
17 | "dependencies": {
18 | "gatsby": "^3.4.1",
19 | "react": "link:../node_modules/react",
20 | "react-dom": "link:../node_modules/react-dom",
21 | "react-gallery-carousel": "link:..",
22 | "react-github-btn": "^1.2.0",
23 | "react-responsive-button": "^0.2.1",
24 | "react-scripts": "link:../node_modules/react-scripts"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/react-gallery-carousel-gatsby/src/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yifaneye/react-gallery-carousel/a3f452b143f6c1149477a36e43ee4698e67d3363/react-gallery-carousel-gatsby/src/images/icon.png
--------------------------------------------------------------------------------
/react-gallery-carousel-gatsby/src/pages/404.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Link } from 'gatsby';
3 |
4 | // styles
5 | const pageStyles = {
6 | color: '#232129',
7 | padding: '96px',
8 | fontFamily: '-apple-system, Roboto, sans-serif, serif'
9 | };
10 | const headingStyles = {
11 | marginTop: 0,
12 | marginBottom: 64,
13 | maxWidth: 320
14 | };
15 |
16 | const paragraphStyles = {
17 | marginBottom: 48
18 | };
19 | const codeStyles = {
20 | color: '#8A6534',
21 | padding: 4,
22 | backgroundColor: '#FFF4DB',
23 | fontSize: '1.25rem',
24 | borderRadius: 4
25 | };
26 |
27 | // markup
28 | const NotFoundPage = () => {
29 | return (
30 |
31 | Not found
32 | Page not found
33 |
34 | Sorry{' '}
35 |
36 | 😔
37 | {' '}
38 | we couldn’t find what you were looking for.
39 |
40 | {process.env.NODE_ENV === 'development' ? (
41 | <>
42 |
43 | Try creating a page in src/pages/
.
44 |
45 | >
46 | ) : null}
47 |
48 | Go home.
49 |
50 |
51 | );
52 | };
53 |
54 | export default NotFoundPage;
55 |
--------------------------------------------------------------------------------
/react-gallery-carousel-gatsby/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import App from '../../../example/src/App.js';
2 | import '../../../example/src/index.css';
3 |
4 | export default App;
5 |
--------------------------------------------------------------------------------
/react-gallery-carousel-nextjs/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
--------------------------------------------------------------------------------
/react-gallery-carousel-nextjs/README.md:
--------------------------------------------------------------------------------
1 | # react-gallery-carousel Gatsby example
2 |
--------------------------------------------------------------------------------
/react-gallery-carousel-nextjs/components/Carousel1.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // import TwoWayMap from '../utils/TwoWayMap';
3 | import Carousel from 'react-gallery-carousel';
4 |
5 | const Carousel1 = () => {
6 | // const indexToTitle = new TwoWayMap({
7 | // 0: 'Introduction',
8 | // 1: 'Get%20Started',
9 | // 2: 'Usage'
10 | // });
11 |
12 | return (
13 |
14 |
24 |
25 |
{
39 | // const title = indexToTitle.get(curIndex);
40 | // window.history.replaceState(
41 | // undefined,
42 | // undefined,
43 | // `#${title}`
44 | // );
45 | // document.title = `${title} | react-gallery-carousel`;
46 | // }} // this callback can be set to update the document title and URL hash on index update
47 | style={{ userSelect: 'text' }}
48 | >
49 |
50 |
Introduction
51 |
52 | react-gallery-carousel is a mobile-friendly
53 | dependency-free React carousel component with support for touch,
54 | mouse dragging, lazy loading, thumbnails, modal, keyboard
55 | navigation, RTL and pinch to zoom.
56 |
57 |
Demo
58 |
/
59 |
60 | GitHub
61 |
62 |
/
63 |
64 | npm
65 |
66 |
/
67 |
68 | Documentation
69 |
70 |
71 |
72 |
Get Started
73 |
npm install react-gallery-carousel --save
74 |
75 | or
76 |
77 |
yarn add react-gallery-carousel
78 |
79 |
80 |
Usage
81 |
The default carousel shown below as example 2 is created by:
82 |
{' '}
83 |
84 |
85 |
86 |
87 | );
88 | };
89 |
90 | export default Carousel1;
91 |
--------------------------------------------------------------------------------
/react-gallery-carousel-nextjs/components/Carousel2.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Carousel from 'react-gallery-carousel';
3 |
4 | const Carousel2 = ({ images }) => {
5 | return (
6 |
7 |
8 |
9 | Example 2: Default carousel with images{' '}
10 | code
11 |
12 |
13 | A default carousel example has lazy loading and preloading (the 2
14 | adjacent images on either side of the current image); touch swiping
15 | and mouse dragging on the carousel; touch swiping, mouse dragging and
16 | wheel scrolling on the thumbnails; touch swipe down to exit the
17 | maximized carousel; and keyboard navigation.
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | export default Carousel2;
28 |
--------------------------------------------------------------------------------
/react-gallery-carousel-nextjs/components/Carousel3.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Carousel from 'react-gallery-carousel';
3 |
4 | const Carousel3 = ({ images }) => {
5 | return (
6 |
7 |
8 |
9 | Example 3: Default carousel with images and with right-to-left (RTL){' '}
10 | code
11 |
12 |
13 | A default carousel example has lazy loading and preloading; touch
14 | swiping and mouse dragging on the carousel; touch swiping, mouse
15 | dragging and wheel scrolling on the thumbnails; touch swipe down to
16 | exit the maximized carousel; and keyboard navigation.
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | };
25 |
26 | export default Carousel3;
27 |
--------------------------------------------------------------------------------
/react-gallery-carousel-nextjs/components/Carousel4.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import Carousel from 'react-gallery-carousel';
3 |
4 | const Carousel4 = ({ images }) => {
5 | const [dynamicImages, setDynamicImages] = useState([]);
6 |
7 | useEffect(() => {
8 | setDynamicImages(images);
9 | }, [setDynamicImages, images]);
10 |
11 | return (
12 |
13 |
14 |
15 | Example 4: Customized carousel with dynamic images{' '}
16 | (available from v0.1.1) {' '}
17 | code
18 |
19 |
20 | This example has images dynamically set in the{' '}
21 | useEffect()
hook. This customized example additionally
22 | has click to enter and exit the maximized carousel ;
23 | custom initial index; custom widget positions; custom thumbnails,
24 | custom dot buttons and captions for the maximized carousel; custom
25 | active and passive dot buttons; and custom styles for the
26 | non-maximized carousel.
27 |
28 |
29 |
30 |
50 | 🔳
51 |
52 | }
53 | passiveIcon={
54 |
55 | 🔲
56 |
57 | }
58 | />
59 |
60 |
61 | );
62 | };
63 |
64 | export default Carousel4;
65 |
--------------------------------------------------------------------------------
/react-gallery-carousel-nextjs/components/Carousel5.js:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 | import Carousel from 'react-gallery-carousel';
3 | import { Button } from 'react-responsive-button';
4 |
5 | const Carousel5 = ({ images }) => {
6 | const carouselRef = useRef(null);
7 |
8 | return (
9 |
10 |
11 |
12 | Example 5: Default carousel with imperative handlers{' '}
13 | (available from v0.2.0) {' '}
14 | code
15 |
16 |
17 | To customize the carousel in a declarative manner, pass the props
18 | (e.g. isAutoPlaying
, isMaximized
,{' '}
19 | index
). To customize the carousel in an imperative
20 | manner, use the following handlers (on the ref):
21 |
22 |
23 |
24 |
25 | carouselRef.current.play()}
28 | >
29 | play()
30 |
31 |
32 |
33 | carouselRef.current.pause()}
36 | >
37 | pause()
38 |
39 |
40 |
41 | carouselRef.current.toggleIsPlaying()}
44 | >
45 | toggleIsPlaying()
46 |
47 |
48 |
49 |
50 |
51 | carouselRef.current.maximize()}
55 | >
56 | maximize()
57 |
58 |
59 |
60 | carouselRef.current.minimize()}
64 | >
65 | minimize()
66 |
67 |
68 |
69 | carouselRef.current.toggleIsMaximized()}
73 | >
74 | toggleIsMaximized()
75 |
76 |
77 |
78 |
79 |
80 | carouselRef.current.goLeft()}
84 | >
85 | goLeft()
86 |
87 |
88 |
89 | carouselRef.current.goRight()}
93 | >
94 | goRight()
95 |
96 |
97 |
98 | carouselRef.current.goToIndex(0)}
102 | >
103 | goToIndex(0)
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 | );
114 | };
115 |
116 | export default Carousel5;
117 |
--------------------------------------------------------------------------------
/react-gallery-carousel-nextjs/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | reactStrictMode: true
3 | };
4 |
--------------------------------------------------------------------------------
/react-gallery-carousel-nextjs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-gallery-carousel-nextjs",
3 | "version": "0.0.0",
4 | "private": true,
5 | "description": "react-gallery-carousel Next.js example",
6 | "author": "Yifan Ai",
7 | "keywords": [
8 | "nextjs"
9 | ],
10 | "scripts": {
11 | "dev": "next dev",
12 | "build": "next build",
13 | "start": "next start",
14 | "lint": "next lint"
15 | },
16 | "dependencies": {
17 | "next": "11.0.1",
18 | "react": "link:../node_modules/react",
19 | "react-dom": "link:../node_modules/react-dom",
20 | "react-gallery-carousel": "link:..",
21 | "react-github-btn": "^1.2.0",
22 | "react-responsive-button": "^0.2.1",
23 | "react-scripts": "link:../node_modules/react-scripts"
24 | },
25 | "devDependencies": {
26 | "eslint": "7.30.0",
27 | "eslint-config-next": "11.0.1"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/react-gallery-carousel-nextjs/pages/_app.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '../styles/globals.css';
3 | import Carousel1 from '../components/Carousel1';
4 | import Carousel2 from '../components/Carousel2';
5 | import Carousel3 from '../components/Carousel3';
6 | import Carousel4 from '../components/Carousel4';
7 | import Carousel5 from '../components/Carousel5';
8 | import 'react-gallery-carousel/dist/index.css';
9 |
10 | const imageIDs = Array(30) // the maximum is currently 149
11 | .fill(1)
12 | .map((_, i) => i + 1);
13 | const images = imageIDs.map((imageID) => {
14 | return {
15 | src: `https://placedog.net/400/240?id=${imageID}`,
16 | srcset: `https://placedog.net/400/240?id=${imageID} 400w, https://placedog.net/700/420?id=${imageID} 700w, https://placedog.net/1000/600?id=${imageID} 1000w`,
17 | sizes: '(max-width: 1000px) 400px, (max-width: 2000px) 700px, 1000px',
18 | alt: `Dog No. ${imageID}. Dogs are domesticated mammals, not natural wild animals.`,
19 | thumbnail: `https://placedog.net/100/60?id=${imageID}`
20 | };
21 | });
22 |
23 | function MyApp() {
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 |
35 | export default MyApp;
36 |
--------------------------------------------------------------------------------
/react-gallery-carousel-nextjs/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MyApp from './_app';
3 |
4 | export default function Home() {
5 | return ;
6 | }
7 |
--------------------------------------------------------------------------------
/react-gallery-carousel-nextjs/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yifaneye/react-gallery-carousel/a3f452b143f6c1149477a36e43ee4698e67d3363/react-gallery-carousel-nextjs/public/favicon.ico
--------------------------------------------------------------------------------
/react-gallery-carousel-nextjs/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/react-gallery-carousel-nextjs/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | min-height: 100vh;
3 | padding: 0 0.5rem;
4 | display: flex;
5 | flex-direction: column;
6 | justify-content: center;
7 | align-items: center;
8 | height: 100vh;
9 | }
10 |
11 | .main {
12 | padding: 5rem 0;
13 | flex: 1;
14 | display: flex;
15 | flex-direction: column;
16 | justify-content: center;
17 | align-items: center;
18 | }
19 |
20 | .footer {
21 | width: 100%;
22 | height: 100px;
23 | border-top: 1px solid #eaeaea;
24 | display: flex;
25 | justify-content: center;
26 | align-items: center;
27 | }
28 |
29 | .footer a {
30 | display: flex;
31 | justify-content: center;
32 | align-items: center;
33 | flex-grow: 1;
34 | }
35 |
36 | .title a {
37 | color: #0070f3;
38 | text-decoration: none;
39 | }
40 |
41 | .title a:hover,
42 | .title a:focus,
43 | .title a:active {
44 | text-decoration: underline;
45 | }
46 |
47 | .title {
48 | margin: 0;
49 | line-height: 1.15;
50 | font-size: 4rem;
51 | }
52 |
53 | .title,
54 | .description {
55 | text-align: center;
56 | }
57 |
58 | .description {
59 | line-height: 1.5;
60 | font-size: 1.5rem;
61 | }
62 |
63 | .code {
64 | background: #fafafa;
65 | border-radius: 5px;
66 | padding: 0.75rem;
67 | font-size: 1.1rem;
68 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
69 | Bitstream Vera Sans Mono, Courier New, monospace;
70 | }
71 |
72 | .grid {
73 | display: flex;
74 | align-items: center;
75 | justify-content: center;
76 | flex-wrap: wrap;
77 | max-width: 800px;
78 | margin-top: 3rem;
79 | }
80 |
81 | .card {
82 | margin: 1rem;
83 | padding: 1.5rem;
84 | text-align: left;
85 | color: inherit;
86 | text-decoration: none;
87 | border: 1px solid #eaeaea;
88 | border-radius: 10px;
89 | transition: color 0.15s ease, border-color 0.15s ease;
90 | width: 45%;
91 | }
92 |
93 | .card:hover,
94 | .card:focus,
95 | .card:active {
96 | color: #0070f3;
97 | border-color: #0070f3;
98 | }
99 |
100 | .card h2 {
101 | margin: 0 0 1rem 0;
102 | font-size: 1.5rem;
103 | }
104 |
105 | .card p {
106 | margin: 0;
107 | font-size: 1.25rem;
108 | line-height: 1.5;
109 | }
110 |
111 | .logo {
112 | height: 1em;
113 | margin-left: 0.5rem;
114 | }
115 |
116 | @media (max-width: 600px) {
117 | .grid {
118 | width: 100%;
119 | flex-direction: column;
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/react-gallery-carousel-nextjs/styles/globals.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
6 | sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
13 | monospace;
14 | padding: 0.2em 0.4em;
15 | margin: 0;
16 | font-size: 85%;
17 | background-color: rgba(27, 31, 35, 0.05);
18 | border-radius: 6px;
19 | }
20 |
21 | .text-slide {
22 | padding: 1rem 50px;
23 | }
24 |
25 | .vertical-separator {
26 | margin: 1em 0;
27 | }
28 |
29 | .carousel-page {
30 | max-width: 1200px;
31 | margin: 0 auto;
32 | overflow: hidden;
33 | }
34 |
35 | .carousel-page-header-container {
36 | width: 100%;
37 | }
38 |
39 | .carousel-page-header {
40 | display: flex;
41 | flex-direction: column;
42 | align-items: center;
43 | padding: 0 1em;
44 | }
45 |
46 | .action-container {
47 | padding: 0 1em;
48 | }
49 |
50 | .star-button-container {
51 | height: 20px;
52 | margin-top: 10px;
53 | margin-bottom: 10px;
54 | }
55 |
56 | .section {
57 | margin-bottom: 60px;
58 | }
59 |
60 | .section-header {
61 | padding: 0 1em;
62 | }
63 |
64 | .carousel-container {
65 | width: 100%;
66 | height: calc(100vw * 0.8);
67 | }
68 |
69 | @media only screen and (min-width: 500px) and (max-width: 1280px) {
70 | .carousel-container {
71 | height: calc(100vw * 0.6);
72 | max-height: 768px;
73 | }
74 | }
75 |
76 | @media only screen and (min-width: 1280px) {
77 | .carousel-container {
78 | height: 768px;
79 | }
80 | }
81 |
82 | .carousel-container.short {
83 | height: 300px;
84 | }
85 |
86 | .buttons {
87 | margin: 10px 0;
88 | display: flex;
89 | flex-wrap: wrap;
90 | }
91 |
92 | .button-container {
93 | margin-bottom: 10px;
94 | }
95 |
96 | .button-container:not(:last-child) {
97 | margin-right: 10px;
98 | }
99 |
100 | .framed-carousel {
101 | border: 5px solid #eee;
102 | overflow: hidden;
103 | border-radius: 10px;
104 | will-change: transform;
105 | mask-image: -webkit-radial-gradient(white, black);
106 | box-sizing: border-box;
107 | }
108 |
109 | .icon-text {
110 | color: white;
111 | }
112 |
--------------------------------------------------------------------------------
/src/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "jest": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/common/common.scss:
--------------------------------------------------------------------------------
1 | $scale: 10%;
2 | $margin: 3px;
3 |
--------------------------------------------------------------------------------
/src/components/Carousel/Carousel.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | forwardRef,
3 | Fragment,
4 | useCallback,
5 | useEffect,
6 | useImperativeHandle,
7 | useRef,
8 | useState
9 | } from 'react';
10 | import styles from './Carousel.module.css';
11 | import Slides from '../Slides';
12 | import Thumbnails from '../Thumbnails';
13 | import {
14 | LeftButton,
15 | RightButton,
16 | MediaButton,
17 | SizeButton,
18 | IndexBoard,
19 | DotButtons
20 | } from '../Widgets';
21 | import isSSR from '../../utils/isSSR';
22 | import useKeys from '../../utils/useKeys';
23 | import useSwipe from '../../utils/useSwipe';
24 | import useTimer from '../../utils/useTimer';
25 | import useSlides from '../../utils/useSlides';
26 | import useKeyboard from '../../utils/useKeyboard';
27 | import useMediaQuery from '../../utils/useMediaQuery';
28 | import useEventListener from '../../utils/useEventListener';
29 | import useFixedPosition from '../../utils/useFixedPosition';
30 | import {
31 | MAX_SWIPE_DOWN_DISTANCE,
32 | WIDGET_POSITIONS,
33 | WIDGET_POSITIONS_WITH_RTL
34 | } from './constants';
35 | import ReversedMap from '../../utils/ReversedMap';
36 | import { propTypes, defaultProps, getSettings } from './props';
37 |
38 | const GalleryCarousel = (props, ref) => {
39 | /* initialize references */
40 | const documentRef = useRef(isSSR ? undefined : document);
41 | const maximizedBackgroundRef = useRef(null);
42 | const carouselRef = useRef(null);
43 | const slidesContainerRef = useRef(null);
44 | const slidesRef = useRef(null);
45 | const slideMinRef = useRef(null);
46 | const slideMaxRef = useRef(null);
47 |
48 | /* process slides */
49 | const hasImages = 'images' in props; // true even the 'images' prop is an empty Array
50 | const children = Array.isArray(props.children)
51 | ? props.children
52 | : [props.children];
53 | const rawSlides = hasImages ? props.images : children;
54 | const [slides, slidesElements] = useSlides(rawSlides, {
55 | index: props.index,
56 | isLoop: props.isLoop
57 | });
58 | const thumbnailElements = props.thumbnails || slidesElements;
59 | const nSlides = slides.length;
60 | const increment = props.isRTL ? -1 : +1;
61 | const slidesMin = `${nSlides * -increment}00%`;
62 | const slidesMax = `${nSlides * increment}00%`;
63 |
64 | /* handle current index change */
65 | const [, setCurIndex] = useState(slides.curIndex);
66 |
67 | const applyCurIndexUpdate = (curIndex) => {
68 | setCurIndex(curIndex);
69 | props.onIndexChange({
70 | curIndex: slides.curIndex,
71 | curIndexForDisplay: slides.curIndexForDisplay
72 | });
73 | };
74 |
75 | /* handle autoplay and reduced motion setting */
76 | const [
77 | isPlaying,
78 | setIsPlaying,
79 | { stopTimer, restartTimer }
80 | ] = useTimer(
81 | props.canAutoPlay && props.autoPlayInterval,
82 | props.isAutoPlaying,
83 | () => updateIndex(+1)
84 | );
85 |
86 | const handleMediaButtonClick = () => {
87 | setIsPlaying((isPlaying) => !isPlaying);
88 | };
89 |
90 | const isReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
91 |
92 | useEffect(() => {
93 | if (isReducedMotion) setIsPlaying(false);
94 | }, [isReducedMotion, setIsPlaying]);
95 |
96 | /* set up smart pause */
97 | const [wasPlaying, setWasPlaying] = useState(false);
98 | const handleVisibilityChange = useCallback(() => {
99 | if (document.visibilityState !== 'visible') {
100 | // user switches tab away from the page
101 | setWasPlaying(isPlaying);
102 | setIsPlaying(false);
103 | } else {
104 | // user switches tab back to the page
105 | setIsPlaying(wasPlaying);
106 | }
107 | }, [isPlaying, setIsPlaying, wasPlaying, setWasPlaying]);
108 |
109 | useEventListener(
110 | isSSR ? undefined : document,
111 | 'visibilitychange',
112 | handleVisibilityChange
113 | );
114 |
115 | /* handle maximization/minimization and full screen */
116 | const [isMaximized, setIsMaximized] = useFixedPosition(
117 | props.isMaximized,
118 | slidesContainerRef
119 | );
120 |
121 | const handleSizeButtonClick = () => {
122 | setIsMaximized((isMaximized) => !isMaximized);
123 | };
124 |
125 | /* handle UI updates */
126 | const applyTransitionDuration = (
127 | displacementX = 0,
128 | speed = props.transitionSpeed,
129 | hasToUpdate = true
130 | ) => {
131 | if (!props.hasTransition) return;
132 | if (isReducedMotion) return;
133 |
134 | // calculate transition duration
135 | const swipedDistance = Math.abs(displacementX);
136 | const transitionDistance =
137 | hasToUpdate && slidesRef.current
138 | ? Math.abs(slidesRef.current.clientWidth - swipedDistance)
139 | : swipedDistance;
140 | speed = hasToUpdate ? speed : props.swipeRollbackSpeed;
141 | let duration = transitionDistance / speed;
142 |
143 | // flatten transitionDurations
144 | if (duration > props.transitionDurationLimit / 2)
145 | duration =
146 | (Math.atan((2 * duration) / props.transitionDurationLimit) *
147 | props.transitionDurationLimit *
148 | 2) /
149 | Math.PI;
150 |
151 | // bound duration to a range
152 | if (props.transitionDurationMin)
153 | duration = Math.max(duration, props.transitionDurationMin);
154 |
155 | // transitionDurationMax has precedence over transitionDurationMin
156 | if (props.transitionDurationMax)
157 | duration = Math.min(duration, props.transitionDurationMax);
158 |
159 | // make duration greater or equal to autoPlayInterval
160 | if (isPlaying && duration > props.autoPlayInterval)
161 | duration = props.autoPlayInterval * 1;
162 |
163 | // apply transition duration for the period of duration
164 | if (slidesRef.current)
165 | slidesRef.current.style.transitionDuration = `${duration}ms`;
166 | setTimeout(() => {
167 | // revert temporary style changes made on the slides for transition and looping
168 | if (slidesRef.current) slidesRef.current.style.transitionDuration = null;
169 | }, duration);
170 | };
171 |
172 | // handle touch swipe down specifically
173 | const applyTransitionY = (displacementX = 0, displacementY = 0) => {
174 | // do not update the maximized carousel when it is above its original position
175 | // to hint the user that swiping up will not be able to minimize the carousel
176 | const distance = displacementY > 0 ? displacementY : 0;
177 | const portion = 1 - distance / MAX_SWIPE_DOWN_DISTANCE;
178 |
179 | // move and scale the element
180 | if (carouselRef.current) {
181 | // check whether the update is necessary
182 | if (
183 | carouselRef.current.style.transform !==
184 | `translate(${displacementX}px, ${displacementY}px) scale(${portion})`
185 | ) {
186 | carouselRef.current.style.transform = `translate(${displacementX}px, ${displacementY}px) scale(${portion})`;
187 | }
188 | }
189 |
190 | // update opacity of the background
191 | if (maximizedBackgroundRef.current) {
192 | // check whether the update is necessary
193 | if (maximizedBackgroundRef.current.style.opacity !== portion) {
194 | maximizedBackgroundRef.current.style.opacity = portion;
195 | }
196 | }
197 | };
198 |
199 | const applyTransitionX = useCallback(
200 | (displacementX = 0) => {
201 | const targetPosition =
202 | displacementX === 0
203 | ? `${-100 * slides.curIndex * increment}%`
204 | : `calc(${-100 * slides.curIndex * increment}% + ${displacementX}px)`;
205 | // move the element
206 | if (slidesRef.current) {
207 | slidesRef.current.style.transform = `translateX(${targetPosition})`;
208 | }
209 | },
210 | [slides.curIndex, increment]
211 | );
212 |
213 | // change to the current slide before browser paints
214 | useEffect(() => applyTransitionX(), [applyTransitionX]);
215 |
216 | /* handle implicit current index update (e.g. +1 or -1) */
217 | const shouldCalibrateIndex = props.isLoop && nSlides > 1;
218 |
219 | const handleSwipeMoveX = (displacementX) => {
220 | props.onSwipeMoveX(displacementX);
221 | // stop the timer for autoplay if there is a timer
222 | // should not use setIsPlaying(false) here since it will update the icon in the media button
223 | if (props.canAutoPlay) stopTimer();
224 |
225 | const change = -displacementX * increment;
226 |
227 | // calibrate index for looping of the carousel
228 | if (shouldCalibrateIndex) {
229 | if (slides.isMinIndex() && change < 0 && slideMaxRef.current) {
230 | slideMaxRef.current.style.transform = `translateX(${slidesMin})`;
231 | } else if (slides.isMaxIndex() && change > 0 && slideMinRef.current) {
232 | slideMinRef.current.style.transform = `translateX(${slidesMax})`;
233 | } else {
234 | if (slideMinRef.current) slideMinRef.current.style.transform = null;
235 | if (slideMaxRef.current) slideMaxRef.current.style.transform = null;
236 | }
237 | }
238 |
239 | // update UI
240 | applyTransitionX(displacementX);
241 | };
242 |
243 | const updateIndex = (change, displacementX = 0, speed) => {
244 | // restart the timer for autoplay if there is a timer and the index update is being roll-backed
245 | if (props.canAutoPlay && change === 0) restartTimer();
246 |
247 | // calibrate index for looping of the carousel
248 | if (shouldCalibrateIndex && slideMinRef.current && slideMaxRef.current) {
249 | if (slides.isMinIndex() && change < 0) {
250 | slideMinRef.current.style.transform = `translateX(${slidesMax})`;
251 | slideMaxRef.current.style.transform = null;
252 | } else if (slides.isMaxIndex() && change > 0) {
253 | slideMinRef.current.style.transform = null;
254 | slideMaxRef.current.style.transform = `translateX(${slidesMin})`;
255 | } else if (change !== 0) {
256 | slideMinRef.current.style.transform = null;
257 | slideMaxRef.current.style.transform = null;
258 | } // if change === 0 then the adjacent slides shall be kept in place for the rollback transition
259 | }
260 |
261 | // update UI
262 | if (slides.calibrateIndex(change) && shouldCalibrateIndex) {
263 | // remove carry-over transitionDuration
264 | if (slidesRef.current) slidesRef.current.style.transitionDuration = null;
265 | applyTransitionX(displacementX);
266 | }
267 |
268 | slides.updateIndex(change);
269 | applyTransitionDuration(displacementX, speed, change !== 0);
270 | applyTransitionY(0, 0);
271 | applyTransitionX();
272 | applyCurIndexUpdate(slides.curIndex);
273 | };
274 |
275 | const rollBackIndexUpdate = () => updateIndex(0, 0, 0);
276 |
277 | useEventListener(
278 | isSSR ? undefined : window,
279 | 'orientationchange',
280 | rollBackIndexUpdate
281 | );
282 |
283 | /* handle explicit current index update (e.g. go to slide number 16) */
284 | const goToIndex = (index) => {
285 | // set both the first and the last slide back into their respective original places
286 | if (slideMinRef.current) slideMinRef.current.style.transform = null;
287 | if (slideMaxRef.current) slideMaxRef.current.style.transform = null;
288 |
289 | // update carousel
290 | slides.goToIndex(index);
291 | applyTransitionX();
292 | applyCurIndexUpdate(slides.curIndex);
293 | };
294 |
295 | // set up callbacks (i.e. 1 callback for each dotButton and each thumbnail)
296 | const indices = slides.allIndices;
297 | const goToIndexCallbacks = indices.map((index) => () => goToIndex(index));
298 | const goToIndexCallbacksObject = indices.reduce(
299 | (obj, key, index) => ({ ...obj, [key]: goToIndexCallbacks[index] }),
300 | {}
301 | );
302 |
303 | /* handle keyboard events */
304 | useKeys(documentRef, { Escape: () => setIsMaximized(() => false) });
305 |
306 | useKeyboard(carouselRef);
307 |
308 | const goLeft = () => updateIndex(-increment);
309 | const goRight = () => updateIndex(+increment);
310 |
311 | useKeys(carouselRef, {
312 | ArrowLeft: goLeft,
313 | ArrowRight: goRight,
314 | /* can not use useEnter hook here to mimic user click, since a click
315 | on slidesContainer should not and will not trigger anything */
316 | Enter: (event) => {
317 | // ignore ('Enter' key) keydown events on widgets (buttons) bubbling up to here
318 | if (event.target !== event.currentTarget) return;
319 | handleSizeButtonClick();
320 | }
321 | });
322 |
323 | /* handle mouse and touch events */
324 | // store isMaximized to combat stale closure
325 | const isMaximizedRef = useRef(isMaximized);
326 | isMaximizedRef.current = isMaximized;
327 |
328 | const handleSwipeMoveY = (displacementX, displacementY) => {
329 | props.onSwipeMoveY(displacementX, displacementY);
330 | if (!props.shouldMinimizeOnSwipeDown) return;
331 | if (isMaximizedRef.current) applyTransitionY(displacementX, displacementY);
332 | };
333 |
334 | const handleSwipeEndDown = () => {
335 | props.onSwipeEndDown();
336 | if (!props.shouldMinimizeOnSwipeDown) return;
337 | applyTransitionY(0, 0);
338 | setIsMaximized(() => false);
339 | rollBackIndexUpdate();
340 | };
341 |
342 | const handleTap = () => {
343 | props.onTap();
344 | if (isMaximizedRef.current && props.shouldMinimizeOnClick)
345 | setIsMaximized(() => false);
346 | else if (!isMaximizedRef.current && props.shouldMaximizeOnClick)
347 | setIsMaximized(() => true);
348 | };
349 |
350 | const mouseEventHandlers = useSwipe(
351 | slidesContainerRef,
352 | props.swipeThreshold,
353 | {
354 | onSwipeMoveX: handleSwipeMoveX,
355 | onSwipeMoveY: handleSwipeMoveY,
356 | onSwipeEndLeft: (displacementX, speed) =>
357 | updateIndex(increment, displacementX, speed),
358 | onSwipeEndRight: (displacementX, speed) =>
359 | updateIndex(-increment, displacementX, speed),
360 | onSwipeEndDisqualified: (displacementX, speed) =>
361 | updateIndex(0, displacementX, speed),
362 | onSwipeEndDown: handleSwipeEndDown,
363 | onTap: handleTap
364 | }
365 | );
366 | // touch event handlers are already added to slidesContainerRef by useSwipe hook at this point
367 |
368 | /* process class names */
369 | const propsClassName = 'className' in props ? ' ' + props.className : '';
370 | const galleryClassName = hasImages ? ' ' + styles.gallery : '';
371 | const carouselClassName = styles.carousel + propsClassName + galleryClassName;
372 | const maxCarouselClassName = styles.maxCarousel + galleryClassName;
373 |
374 | /* process components for maximized carousel */
375 | // placeholder to be placed at the carousel's non-maximized position
376 | const carouselPlaceholder = isMaximized && (
377 |
378 | );
379 |
380 | // background to be placed behind the maximized carousel
381 | const maxCarouselBackground = isMaximized && (
382 |
387 | );
388 |
389 | /* process settings */
390 | const settings = getSettings(
391 | props,
392 | ['objectFit', 'hasCaptions', 'hasThumbnails'],
393 | isMaximized
394 | );
395 |
396 | const widgetSettings = getSettings(
397 | props,
398 | [
399 | 'hasLeftButton',
400 | 'hasRightButton',
401 | 'hasMediaButton',
402 | 'hasSizeButton',
403 | 'hasDotButtons',
404 | 'hasIndexBoard'
405 | ],
406 | isMaximized
407 | );
408 |
409 | const leftButton = widgetSettings.hasLeftButton && (
410 |
418 | );
419 |
420 | const rightButton = widgetSettings.hasRightButton && (
421 |
429 | );
430 |
431 | const mediaButton = widgetSettings.hasMediaButton && props.canAutoPlay && (
432 |
440 | );
441 |
442 | const sizeButton = widgetSettings.hasSizeButton && (
443 |
451 | );
452 |
453 | const indexBoard = widgetSettings.hasIndexBoard && (
454 |
460 | );
461 |
462 | const dotButtons = widgetSettings.hasDotButtons && (
463 |
472 | );
473 |
474 | const thumbnails = settings.hasThumbnails && (
475 |
487 | );
488 |
489 | // organize and sort the widgets
490 | const widgetsObj = {
491 | hasLeftButton: leftButton,
492 | hasRightButton: rightButton,
493 | hasMediaButton: mediaButton,
494 | hasSizeButton: sizeButton,
495 | hasDotButtons: dotButtons,
496 | hasIndexBoard: indexBoard
497 | };
498 | const positionsToWidgets = new ReversedMap(widgetSettings);
499 | const positions = props.isRTL ? WIDGET_POSITIONS_WITH_RTL : WIDGET_POSITIONS;
500 | const widgets = (
501 | <>
502 | {positions.map(
503 | (position, index) =>
504 | widgetsObj[positionsToWidgets.get(position)] && (
505 |
506 | {
507 | widgetsObj[
508 | positionsToWidgets.get(position).replace(/AtMax$/, '')
509 | ]
510 | }
511 |
512 | )
513 | )}
514 | >
515 | );
516 |
517 | /* provide handlers for controlling the carousel in an imperative way */
518 | useImperativeHandle(ref, () => ({
519 | play: () => setIsPlaying(() => true),
520 | pause: () => setIsPlaying(() => false),
521 | toggleIsPlaying: () => setIsPlaying((isPlaying) => !isPlaying),
522 | maximize: () => setIsMaximized(() => true),
523 | minimize: () => setIsMaximized(() => false),
524 | toggleIsMaximized: () => setIsMaximized((isMaximized) => !isMaximized),
525 | goLeft: goLeft,
526 | goRight: goRight,
527 | goToIndex: goToIndex
528 | }));
529 |
530 | return (
531 | <>
532 | {carouselPlaceholder}
533 | {maxCarouselBackground}
534 |
546 |
547 |
553 |
568 |
569 | {widgets}
570 | {props.elements && props.elements}
571 |
572 | {thumbnails}
573 |
574 | >
575 | );
576 | };
577 |
578 | export const Carousel = forwardRef(GalleryCarousel);
579 |
580 | Carousel.displayName = 'Carousel';
581 | Carousel.propTypes = propTypes;
582 | Carousel.defaultProps = defaultProps;
583 |
--------------------------------------------------------------------------------
/src/components/Carousel/Carousel.module.css:
--------------------------------------------------------------------------------
1 | .carousel {
2 | position: relative;
3 | width: 100%;
4 | height: 100%;
5 | user-select: none;
6 | display: flex;
7 | background-color: #fff;
8 | flex-flow: column;
9 | align-items: stretch;
10 | box-sizing: border-box;
11 | }
12 |
13 | .maxCarousel {
14 | position: fixed;
15 | top: 0;
16 | left: 0;
17 | right: 0;
18 | bottom: 0;
19 | z-index: 2000;
20 | width: 100%;
21 | height: 100%;
22 | background-color: #fff;
23 | transform: scale(1);
24 | transform-origin: top center;
25 | display: flex;
26 | flex-flow: column;
27 | align-items: stretch;
28 | }
29 |
30 | .carouselInner {
31 | width: 100%;
32 | height: 100%;
33 | overflow: hidden;
34 | position: relative;
35 | flex: 0 1 auto;
36 | }
37 |
38 | .slidesContainer {
39 | width: 100%;
40 | height: 100%;
41 | overflow: hidden;
42 | display: flex;
43 | justify-content: flex-start;
44 | align-items: flex-start;
45 | touch-action: auto;
46 | flex: 0 1 auto;
47 | }
48 |
49 | .gallery {
50 | background-color: #000;
51 | }
52 |
53 | [data-is-not-keyboard-user='true'] *:focus {
54 | outline: none;
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/Carousel/Carousel.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import Carousel from '../Carousel';
4 | import { defaultProps } from './props';
5 | const { describe, it, expect } = global;
6 |
7 | const images = [900, 800, 700, 600, 500].map((size) => ({
8 | src: `https://placedog.net/${size}/${size}`
9 | }));
10 |
11 | const imageIDs = Array(6)
12 | .fill(1)
13 | .map((_, i) => i + 1);
14 | const images2 = imageIDs.map((imageID) => {
15 | return {
16 | src: `https://placedog.net/8${imageID}0/6${imageID}0?id=${imageID}`,
17 | alt: `Dog No. ${imageID}. Dogs are domesticated mammals, not natural wild animals. They were originally bred from wolves. They have been bred by humans for a long time, and were the first animals ever to be domesticated.`,
18 | thumbnail: `https://placedog.net/8${imageID}/6${imageID}?id=${imageID}`
19 | };
20 | });
21 |
22 | // snapshot testing
23 | describe('Carousel', () => {
24 | it('is truthy', () => {
25 | expect(Carousel).toBeTruthy();
26 | });
27 |
28 | global.IntersectionObserver = function () {
29 | return {
30 | observe: () => {},
31 | disconnect: () => {}
32 | };
33 | };
34 |
35 | Object.defineProperty(window, 'matchMedia', {
36 | writable: true,
37 | value: (query) => ({
38 | matches: false,
39 | media: query,
40 | onchange: null,
41 | addEventListener: () => {},
42 | removeEventListener: () => {}
43 | })
44 | });
45 |
46 | it('renders the same as snapshot', () => {
47 | const component = renderer.create(
48 |
49 | );
50 | const tree = component.toJSON();
51 | expect(tree).toMatchSnapshot();
52 | });
53 |
54 | it('can be maximized and renders the same as snapshot', () => {
55 | const component2 = renderer.create(
56 |
57 | );
58 | const tree2 = component2.toJSON();
59 | expect(tree2).toMatchSnapshot();
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/src/components/Carousel/constants.js:
--------------------------------------------------------------------------------
1 | // Swiping down longer than MAX_SWIPE_DOWN_DISTANCE will not have more visual changes
2 | export const MAX_SWIPE_DOWN_DISTANCE = 1500; // px
3 |
4 | export const WIDGET_POSITIONS = [
5 | 'topLeft',
6 | 'topCenter',
7 | 'topRight',
8 | 'centerLeft',
9 | 'centerCenter',
10 | 'centerRight',
11 | 'bottomLeft',
12 | 'bottomCenter',
13 | 'bottomRight',
14 | 'top',
15 | 'bottom'
16 | ];
17 | // place large widgets those have multiple focusable elements at the end,
18 | // so that keyboard users can get to their desired elements easier and faster.
19 |
20 | export const WIDGET_POSITIONS_WITH_RTL = [
21 | 'topRight',
22 | 'topCenter',
23 | 'topLeft',
24 | 'centerRight',
25 | 'centerCenter',
26 | 'centerLeft',
27 | 'bottomRight',
28 | 'bottomCenter',
29 | 'bottomLeft',
30 | 'top',
31 | 'bottom'
32 | ];
33 |
--------------------------------------------------------------------------------
/src/components/Carousel/index.js:
--------------------------------------------------------------------------------
1 | export { Carousel as default } from './Carousel';
2 |
--------------------------------------------------------------------------------
/src/components/Carousel/props.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import {
3 | compareToProp,
4 | fallbackProps,
5 | numberBetween,
6 | positiveNumber,
7 | objectFitStyles,
8 | smallWidgetPositions,
9 | largeWidgetPositions
10 | } from '../../utils/validators';
11 |
12 | export const propTypes = {
13 | images: PropTypes.array && fallbackProps(['children']),
14 | children: PropTypes.oneOfType([
15 | PropTypes.arrayOf(PropTypes.node),
16 | PropTypes.node
17 | ]),
18 | thumbnails: PropTypes.arrayOf(PropTypes.node),
19 | isRTL: PropTypes.bool.isRequired,
20 | isLoop: PropTypes.bool.isRequired,
21 | isMaximized: PropTypes.bool.isRequired,
22 | index: positiveNumber(true),
23 | shouldLazyLoad: PropTypes.bool.isRequired,
24 | canAutoPlay: PropTypes.bool.isRequired,
25 | isAutoPlaying: PropTypes.bool.isRequired,
26 | autoPlayInterval: positiveNumber(false),
27 | hasTransition: PropTypes.bool.isRequired,
28 | swipeThreshold: numberBetween(0, 1),
29 | swipeRollbackSpeed: positiveNumber(true),
30 | transitionSpeed: positiveNumber(true),
31 | transitionDurationLimit: positiveNumber(true),
32 | transitionDurationMin: positiveNumber(true),
33 | transitionDurationMax: compareToProp('>=', 'transitionDurationMin'),
34 | widgetsHasShadow: PropTypes.bool.isRequired,
35 | hasLeftButton: smallWidgetPositions.isRequired,
36 | hasRightButton: smallWidgetPositions.isRequired,
37 | hasMediaButton: smallWidgetPositions.isRequired,
38 | hasSizeButton: smallWidgetPositions.isRequired,
39 | hasIndexBoard: smallWidgetPositions.isRequired,
40 | hasDotButtons: largeWidgetPositions.isRequired,
41 | hasCaptions: largeWidgetPositions.isRequired,
42 | hasThumbnails: PropTypes.bool.isRequired,
43 | hasLeftButtonAtMax: smallWidgetPositions,
44 | hasRightButtonAtMax: smallWidgetPositions,
45 | hasMediaButtonAtMax: smallWidgetPositions,
46 | hasSizeButtonAtMax: smallWidgetPositions,
47 | hasIndexBoardAtMax: smallWidgetPositions,
48 | hasDotButtonsAtMax: largeWidgetPositions,
49 | hasCaptionsAtMax: largeWidgetPositions,
50 | hasThumbnailsAtMax: PropTypes.bool,
51 | leftIcon: PropTypes.node,
52 | rightIcon: PropTypes.node,
53 | playIcon: PropTypes.node,
54 | pauseIcon: PropTypes.node,
55 | minIcon: PropTypes.node,
56 | maxIcon: PropTypes.node,
57 | activeIcon: PropTypes.node,
58 | passiveIcon: PropTypes.node,
59 | elements: PropTypes.node,
60 | shouldSwipeOnMouse: PropTypes.bool.isRequired,
61 | shouldMaximizeOnClick: PropTypes.bool.isRequired,
62 | shouldMinimizeOnClick: PropTypes.bool.isRequired,
63 | shouldMinimizeOnSwipeDown: PropTypes.bool.isRequired,
64 | onIndexChange: PropTypes.func.isRequired,
65 | className: PropTypes.string,
66 | style: PropTypes.object,
67 | objectFit: objectFitStyles.isRequired,
68 | objectFitAtMax: objectFitStyles.isRequired,
69 | thumbnailWidth: PropTypes.string,
70 | thumbnailHeight: PropTypes.string,
71 | zIndexAtMax: PropTypes.number
72 | };
73 |
74 | export const defaultProps = {
75 | index: 0,
76 | isRTL: false,
77 | isLoop: true,
78 | isMaximized: false,
79 | shouldLazyLoad: true,
80 | canAutoPlay: true,
81 | isAutoPlaying: false,
82 | autoPlayInterval: 5000, // ms
83 | hasTransition: true,
84 | swipeThreshold: 0.1, // * 100%
85 | swipeRollbackSpeed: 0.1, // px/ms
86 | transitionSpeed: 1, // px/ms
87 | transitionDurationMin: 250, // ms
88 | transitionDurationLimit: 750, // ms
89 | widgetsHasShadow: false,
90 | hasLeftButton: 'centerLeft',
91 | hasRightButton: 'centerRight',
92 | hasMediaButton: 'topLeft',
93 | hasSizeButton: 'topRight',
94 | hasIndexBoard: 'topCenter',
95 | hasDotButtons: false,
96 | hasCaptions: false,
97 | hasThumbnails: true,
98 | shouldSwipeOnMouse: true,
99 | shouldMaximizeOnClick: false,
100 | shouldMinimizeOnClick: false,
101 | shouldMinimizeOnSwipeDown: true,
102 | onIndexChange: () => {},
103 | onSwipeMoveX: () => {},
104 | onSwipeMoveY: () => {},
105 | onSwipeEndDown: () => {},
106 | onTap: () => {},
107 | objectFit: 'cover',
108 | objectFitAtMax: 'contain'
109 | };
110 |
111 | // get props according to whether the carousel isMaximized
112 | export const getSettings = (props, propNames, isMaximized) => {
113 | const newProps = propNames.map((propName) => {
114 | if (!isMaximized) return props[propName];
115 | const propNameAtMax = propName + 'AtMax';
116 | // only take the props[propNameAtMax] if it is specified
117 | if (propNameAtMax in props) return props[propNameAtMax];
118 | // take the props[propName] as a fallback
119 | return props[propName];
120 | });
121 | return propNames.reduce(
122 | (obj, key, index) => ({ ...obj, [key]: newProps[index] }),
123 | {}
124 | );
125 | };
126 |
--------------------------------------------------------------------------------
/src/components/IconButton/IconButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './IconButton.module.css';
3 | import PropTypes from 'prop-types';
4 |
5 | // use inline SVGs to reduce the number of additional requests
6 | const icons = {
7 | left: (
8 |
21 | ),
22 | right: (
23 |
36 | ),
37 | play: (
38 |
51 | ),
52 | pause: (
53 |
66 | ),
67 | max: (
68 |
81 | ),
82 | min: (
83 |
96 | ),
97 | active: (
98 |
99 |
108 |
109 |
110 |
111 | ),
112 | passive: (
113 |
114 |
123 |
124 |
125 |
126 | )
127 | };
128 |
129 | export const IconButton = (props) => {
130 | // only take the user-supplied icon if it is not undefined;
131 | // use the default icon as a fallback
132 | const icon = props.icon !== undefined ? props.icon : icons[props.name];
133 | return (
134 |
143 | {icon}
144 |
145 | );
146 | };
147 |
148 | IconButton.propTypes = {
149 | icon: PropTypes.node,
150 | name: PropTypes.oneOf([
151 | 'left',
152 | 'right',
153 | 'play',
154 | 'pause',
155 | 'max',
156 | 'min',
157 | 'active',
158 | 'passive'
159 | ]).isRequired,
160 | hasShadow: PropTypes.bool.isRequired,
161 | label: PropTypes.string.isRequired,
162 | onClick: PropTypes.func
163 | };
164 |
--------------------------------------------------------------------------------
/src/components/IconButton/IconButton.module.css:
--------------------------------------------------------------------------------
1 | .button {
2 | position: relative;
3 | margin: 0;
4 | border: none;
5 | padding: 0;
6 |
7 | display: inline-block;
8 | appearance: none;
9 | opacity: 0.75;
10 | background-color: transparent;
11 | background-position: center;
12 | background-repeat: no-repeat;
13 |
14 | cursor: pointer;
15 | user-select: none;
16 | touch-action: manipulation;
17 | }
18 |
19 | .buttonShadow {
20 | filter: drop-shadow(0px 0px 3px #888);
21 | }
22 |
23 | .button:hover {
24 | opacity: 1;
25 | }
26 |
27 | @media (hover: none) {
28 | .button:hover {
29 | opacity: 0.75;
30 | }
31 | }
32 |
33 | .iconWrapper {
34 | display: flex;
35 | justify-content: center;
36 | align-items: center;
37 | }
38 |
39 | .rectangle {
40 | width: 50px;
41 | height: 60px;
42 | }
43 |
44 | .square {
45 | width: 50px;
46 | height: 50px;
47 | }
48 |
49 | .circle {
50 | width: 20px;
51 | height: 20px;
52 | border-radius: 50%;
53 | }
54 |
55 | .icon {
56 | stroke: #bbb;
57 | stroke-opacity: 1;
58 | stroke-width: 1px;
59 | fill: #fff;
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/IconButton/index.js:
--------------------------------------------------------------------------------
1 | export { IconButton as default } from './IconButton';
2 |
--------------------------------------------------------------------------------
/src/components/Image/Image.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment, useRef, useState } from 'react';
2 | import styles from './Image.module.css';
3 | import { PLACEHOLDER_IMAGE } from './constants';
4 | import { Caption } from '../Widgets';
5 | import useIntersectionObserver from '../../utils/useIntersectionObserver';
6 | import PropTypes from 'prop-types';
7 | import {
8 | objectFitStyles,
9 | largeWidgetPositions,
10 | imageObject,
11 | elementRef
12 | } from '../../utils/validators';
13 |
14 | const handleError = (event) => {
15 | event.target.src = PLACEHOLDER_IMAGE;
16 | };
17 |
18 | const LazyLoadedImage = (props) => {
19 | const imageRef = useRef(null);
20 | const isInViewport = useIntersectionObserver(
21 | imageRef,
22 | props.slidesContainerRef,
23 | '0px 101% 0px 101%'
24 | // preload 2 images on either side of the slides' container (viewport)
25 | );
26 | const [shouldShowThumbnail, setShouldShowThumbnail] = useState(
27 | props.image.thumbnail
28 | );
29 | const [hasError, setHasError] = useState(false);
30 |
31 | const handleLoad = () => {
32 | if (isInViewport) setShouldShowThumbnail(false);
33 | // the low quality image (props.image.thumbnail) will be hidden
34 | };
35 |
36 | const handleError = () => {
37 | setHasError(true);
38 | };
39 |
40 | let { src, srcset, alt, thumbnail, ...otherImageProps } = props.image;
41 |
42 | src = isInViewport && !hasError ? src : PLACEHOLDER_IMAGE;
43 | srcset = isInViewport && !hasError ? srcset : null;
44 | thumbnail = isInViewport && !hasError ? thumbnail : PLACEHOLDER_IMAGE;
45 |
46 | return (
47 | <>
48 |
59 |
68 | >
69 | );
70 | };
71 |
72 | LazyLoadedImage.propTypes = {
73 | slidesContainerRef: elementRef.isRequired,
74 | image: imageObject.isRequired,
75 | style: PropTypes.object.isRequired
76 | };
77 |
78 | export const Image = (props) => {
79 | const objectFit = props.objectFit === 'cover' ? null : props.objectFit;
80 | const style = { objectFit: objectFit };
81 |
82 | const { src, alt, srcset, thumbnail, ...otherImageProps } = props.image;
83 |
84 | const image = props.shouldLazyLoad ? (
85 |
90 | ) : (
91 |
101 | );
102 |
103 | return (
104 |
105 | {image}
106 | {props.hasCaption && props.image.alt && (
107 |
112 | )}
113 |
114 | );
115 | };
116 |
117 | Image.propTypes = {
118 | objectFit: objectFitStyles.isRequired,
119 | image: imageObject.isRequired,
120 | shouldLazyLoad: PropTypes.bool.isRequired,
121 | slidesContainerRef: elementRef.isRequired,
122 | hasCaption: largeWidgetPositions.isRequired,
123 | widgetsHasShadow: PropTypes.bool.isRequired
124 | };
125 |
--------------------------------------------------------------------------------
/src/components/Image/Image.module.css:
--------------------------------------------------------------------------------
1 | .figure {
2 | position: relative;
3 | height: 100%;
4 | width: 100%;
5 | margin: 0;
6 | border: 0;
7 | border-image-width: 0;
8 | padding: 0;
9 | user-select: none;
10 | overflow: hidden;
11 | }
12 |
13 | .image {
14 | position: relative;
15 | height: 100%;
16 | width: 100%;
17 | margin: 0;
18 | border: 0;
19 | border-image-width: 0;
20 | padding: 0;
21 | object-fit: cover;
22 | user-select: none;
23 | -webkit-user-drag: none;
24 | }
25 |
26 | .thumbnail {
27 | position: absolute;
28 | height: 100%;
29 | width: 100%;
30 | top: 0;
31 | left: 0;
32 | right: 0;
33 | bottom: 0;
34 | object-fit: cover;
35 | user-select: none;
36 | -webkit-user-drag: none;
37 | }
38 |
39 | .thumbnail.hidden {
40 | display: none;
41 | }
42 |
43 | .thumbnail::before,
44 | .image::before {
45 | content: '';
46 | display: block;
47 | position: absolute;
48 | width: 100%;
49 | min-width: 100%;
50 | height: 100%;
51 | min-height: 100%;
52 | top: 0;
53 | left: 0;
54 | right: 0;
55 | bottom: 0;
56 | background: #444;
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/Image/ImageThumbnail.js:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 | import styles from './Image.module.css';
3 | import { PLACEHOLDER_IMAGE } from './constants';
4 | import useIntersectionObserver from '../../utils/useIntersectionObserver';
5 | import PropTypes from 'prop-types';
6 | import { elementRef, imageObject } from '../../utils/validators';
7 |
8 | const handleError = (event) => {
9 | // permanently replace the image with the fallback image
10 | event.target.src = PLACEHOLDER_IMAGE;
11 | };
12 |
13 | const LazyLoadedImageThumbnail = (props) => {
14 | const imageRef = useRef(null);
15 | const isInViewport = useIntersectionObserver(
16 | imageRef,
17 | props.thumbnailsContainerRef,
18 | '0px 20% 0px 20%'
19 | // preload approximately 2 image thumbnails on either side of the thumbnails' container (viewport)
20 | // 'approximately' is due to the presence of margin between adjacent images
21 | );
22 |
23 | // temporarily replace the image with the placeholder image
24 | const src = isInViewport ? props.src : PLACEHOLDER_IMAGE;
25 |
26 | return (
27 |
35 | );
36 | };
37 |
38 | LazyLoadedImageThumbnail.propTypes = {
39 | thumbnailsContainerRef: elementRef.isRequired,
40 | src: PropTypes.string.isRequired,
41 | alt: PropTypes.string
42 | };
43 |
44 | export const ImageThumbnail = (props) => {
45 | // use the original image as fallback for the thumbnail
46 | const src = props.image.thumbnail || props.image.src;
47 | const alt = props.image.alt || null;
48 |
49 | if (props.shouldLazyLoad)
50 | return (
51 |
56 | );
57 |
58 | return (
59 |
67 | );
68 | };
69 |
70 | ImageThumbnail.propTypes = {
71 | image: imageObject.isRequired,
72 | shouldLazyLoad: PropTypes.bool.isRequired,
73 | thumbnailsContainerRef: elementRef.isRequired
74 | };
75 |
--------------------------------------------------------------------------------
/src/components/Image/constants.js:
--------------------------------------------------------------------------------
1 | export const PLACEHOLDER_IMAGE =
2 | 'data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==';
3 |
--------------------------------------------------------------------------------
/src/components/Image/index.js:
--------------------------------------------------------------------------------
1 | export { Image as default } from './Image';
2 | export { ImageThumbnail } from './ImageThumbnail';
3 |
--------------------------------------------------------------------------------
/src/components/Slide/Slide.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './Slide.module.css';
3 | import Image from '../Image';
4 | import UserSlide from '../UserSlide';
5 | import PropTypes from 'prop-types';
6 | import {
7 | elementRef,
8 | largeWidgetPositions,
9 | objectFitStyles,
10 | slideObject
11 | } from '../../utils/validators';
12 |
13 | export const Slide = (props) => {
14 | const slide = props.isImage ? (
15 |
23 | ) : (
24 |
25 | );
26 | return (
27 |
33 | {slide}
34 |
35 | );
36 | };
37 |
38 | Slide.propTypes = {
39 | isImage: PropTypes.bool.isRequired,
40 | slide: slideObject.isRequired,
41 | shouldLazyLoad: PropTypes.bool.isRequired,
42 | objectFit: objectFitStyles.isRequired,
43 | widgetsHasShadow: PropTypes.bool.isRequired,
44 | hasCaption: largeWidgetPositions.isRequired,
45 | slidesContainerRef: elementRef.isRequired,
46 | reference: elementRef,
47 | isCurrent: PropTypes.bool.isRequired
48 | };
49 |
--------------------------------------------------------------------------------
/src/components/Slide/Slide.module.css:
--------------------------------------------------------------------------------
1 | .slide {
2 | height: 100%;
3 | min-width: 100%;
4 | width: 100%;
5 | max-width: 100%;
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/Slide/index.js:
--------------------------------------------------------------------------------
1 | export { Slide as default } from './Slide';
2 |
--------------------------------------------------------------------------------
/src/components/Slides/Slides.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './Slides.module.css';
3 | import Slide from '../Slide';
4 | import PropTypes from 'prop-types';
5 | import {
6 | positiveNumber,
7 | elementRef,
8 | largeWidgetPositions,
9 | objectFitStyles
10 | } from '../../utils/validators';
11 |
12 | export const Slides = (props) => {
13 | const slides = props.slides;
14 |
15 | return (
16 |
24 | {slides.map((slide, index) => {
25 | let reference = null;
26 | if (index === 0) reference = props.minRef;
27 | else if (index === props.length - 1) reference = props.maxRef;
28 | return (
29 |
42 | );
43 | })}
44 |
45 | );
46 | };
47 |
48 | Slides.propTypes = {
49 | slides: PropTypes.array.isRequired,
50 | isRTL: PropTypes.bool.isRequired,
51 | slidesRef: elementRef.isRequired,
52 | minRef: elementRef.isRequired,
53 | length: positiveNumber(true),
54 | maxRef: elementRef.isRequired,
55 | slidesContainerRef: elementRef.isRequired,
56 | hasImages: PropTypes.bool.isRequired,
57 | shouldLazyLoad: PropTypes.bool.isRequired,
58 | objectFit: objectFitStyles.isRequired,
59 | widgetsHasShadow: PropTypes.bool.isRequired,
60 | hasCaptions: largeWidgetPositions.isRequired,
61 | curIndex: PropTypes.number
62 | };
63 |
--------------------------------------------------------------------------------
/src/components/Slides/Slides.module.css:
--------------------------------------------------------------------------------
1 | .slides {
2 | height: 100%;
3 | width: 100%;
4 | list-style: none;
5 | margin: 0;
6 | padding: 0;
7 | display: flex;
8 | flex-direction: row;
9 | justify-content: flex-start;
10 | align-items: flex-start;
11 | transform: translateZ(0);
12 | transition-duration: 0s;
13 | transition-property: transform;
14 | transition-timing-function: linear;
15 | will-change: transform;
16 | backface-visibility: hidden;
17 | }
18 |
19 | .slides.RTL {
20 | flex-direction: row-reverse;
21 | }
22 |
23 | .hasImages {
24 | cursor: grab;
25 | }
26 |
27 | :global(.isGrabbing) > .hasImages {
28 | cursor: grabbing;
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/Slides/index.js:
--------------------------------------------------------------------------------
1 | export { Slides as default } from './Slides';
2 |
--------------------------------------------------------------------------------
/src/components/Thumbnail/Thumbnail.js:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 | import styles from './Thumbnail.module.scss';
3 | import { ImageThumbnail } from '../Image';
4 | import { UserSlideThumbnail } from '../UserSlide';
5 | import useNoDrag from '../../utils/useNoDrag';
6 | import useKeys from '../../utils/useKeys';
7 | import PropTypes from 'prop-types';
8 | import { elementRef, slideObject } from '../../utils/validators';
9 |
10 | export const Thumbnail = (props) => {
11 | const reference = useRef(null);
12 |
13 | const slide = props.isImage ? (
14 |
19 | ) : (
20 |
21 | );
22 |
23 | const className = `${styles.thumbnail}${
24 | props.isCurrent ? ' ' + styles.currentThumbnail : ''
25 | }`;
26 |
27 | // customize the width of the thumbnail
28 | const style =
29 | 'width' in props
30 | ? {
31 | minWidth: props.width,
32 | width: props.width,
33 | maxWidth: props.width
34 | }
35 | : {};
36 |
37 | const ref = props.isCurrent ? props.reference : reference;
38 |
39 | useNoDrag(ref); // prevent dragging on FireFox
40 |
41 | useKeys(ref, { Enter: props.onClick }); // allow keyboard navigation
42 |
43 | return (
44 |
52 | {slide}
53 |
54 | );
55 | };
56 |
57 | Thumbnail.propTypes = {
58 | isImage: PropTypes.bool.isRequired,
59 | thumbnailsContainerRef: elementRef.isRequired,
60 | slide: slideObject.isRequired,
61 | thumbnail: slideObject.isRequired,
62 | shouldLazyLoad: PropTypes.bool.isRequired,
63 | isCurrent: PropTypes.bool.isRequired,
64 | width: PropTypes.string,
65 | reference: elementRef.isRequired,
66 | onClick: PropTypes.func.isRequired
67 | };
68 |
--------------------------------------------------------------------------------
/src/components/Thumbnail/Thumbnail.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../common/common.scss';
2 |
3 | $passiveOpacity: 0.5;
4 | $activeOpacity: 1;
5 |
6 | .thumbnail {
7 | display: block;
8 | height: 100%;
9 | min-width: $scale;
10 | width: $scale;
11 | max-width: $scale;
12 | overflow: hidden;
13 | margin-right: $margin;
14 | padding: 0;
15 | opacity: $passiveOpacity;
16 | list-style: none;
17 | user-select: none;
18 | -webkit-user-drag: none;
19 | }
20 |
21 | .currentThumbnail {
22 | opacity: $activeOpacity;
23 | }
24 |
25 | .thumbnail:not(.currentThumbnail):hover {
26 | opacity: $activeOpacity;
27 | }
28 |
29 | /* prevent thumbnail to stay in the hover state on touch devices,
30 | since there is no such thing as "hover" on touch devices */
31 | @media (hover: none) {
32 | .thumbnail:not(.currentThumbnail):hover {
33 | opacity: $passiveOpacity;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/Thumbnail/index.js:
--------------------------------------------------------------------------------
1 | export { Thumbnail as default } from './Thumbnail';
2 |
--------------------------------------------------------------------------------
/src/components/Thumbnails/Thumbnails.js:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 | import styles from './Thumbnails.module.scss';
3 | import Thumbnail from '../Thumbnail';
4 | import useAnchor from '../../utils/useAnchor';
5 | import useNoOverScroll from '../../utils/useNoOverScroll';
6 | import useMouseDrag from '../../utils/useMouseDrag';
7 | import PropTypes from 'prop-types';
8 |
9 | export const Thumbnails = (props) => {
10 | const callbacks = props.callbacks;
11 | const thumbnailsContainerRef = useRef(null);
12 | const thumbnailRef = useRef(null);
13 |
14 | useAnchor(thumbnailRef, props.isMaximized);
15 |
16 | // customize the height of the thumbnails wrapper which wraps the thumbnails
17 | const style = 'height' in props ? { flexBasis: props.height } : {};
18 |
19 | const wheelEventHandler = useNoOverScroll(thumbnailsContainerRef);
20 | const mouseEventHandlers = useMouseDrag(thumbnailsContainerRef);
21 |
22 | return (
23 |
30 |
33 | {Object.keys(callbacks).map((key, index) => {
34 | return (
35 |
47 | );
48 | })}
49 |
50 |
51 | );
52 | };
53 |
54 | Thumbnails.propTypes = {
55 | callbacks: PropTypes.objectOf(PropTypes.func.isRequired).isRequired,
56 | isMaximized: PropTypes.bool.isRequired,
57 | width: PropTypes.string,
58 | height: PropTypes.string,
59 | isRTL: PropTypes.bool.isRequired,
60 | slides: PropTypes.array.isRequired,
61 | thumbnails: PropTypes.array.isRequired,
62 | hasImages: PropTypes.bool.isRequired,
63 | shouldLazyLoad: PropTypes.bool.isRequired,
64 | curIndex: PropTypes.number.isRequired
65 | };
66 |
67 | Thumbnails.defaultProps = {
68 | curIndex: 0
69 | };
70 |
--------------------------------------------------------------------------------
/src/components/Thumbnails/Thumbnails.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../common/common.scss';
2 |
3 | .thumbnailsWrapper {
4 | flex: 0 0 $scale;
5 | margin: $margin 0 0;
6 | padding: 0;
7 | overflow-y: hidden;
8 | overflow-x: scroll;
9 | cursor: grab;
10 | scrollbar-width: none;
11 |
12 | /* Prevent the default behaviour of "scroll chaining" where parent element
13 | gets scrolled when the child element is over scrolled,
14 | in order to prevent going to the previous or the next page. */
15 | overflow-scrolling: auto;
16 | -webkit-overflow-scrolling: touch;
17 | -ms-scroll-chaining: none;
18 | overscroll-behavior-x: contain;
19 | -ms-overflow-style: none;
20 | }
21 |
22 | :global(.isGrabbing).thumbnailsWrapper {
23 | cursor: grabbing;
24 | }
25 |
26 | .thumbnailsWrapper::-webkit-scrollbar {
27 | display: none;
28 | }
29 |
30 | .thumbnails {
31 | width: 100%;
32 | height: 100%;
33 | margin: 0;
34 | padding: 0;
35 | list-style: none;
36 | display: flex;
37 | flex-direction: row;
38 | justify-content: flex-start;
39 | align-items: flex-start;
40 | }
41 |
42 | .thumbnails.RTL {
43 | flex-direction: row-reverse;
44 | /*thumbnails would be frozen (can not be scrolled) with justify-content: flex-start;*/
45 | justify-content: flex-end;
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/Thumbnails/index.js:
--------------------------------------------------------------------------------
1 | export { Thumbnails as default } from './Thumbnails';
2 |
--------------------------------------------------------------------------------
/src/components/UserSlide/UserSlide.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './UserSlide.module.scss';
3 | import PropTypes from 'prop-types';
4 |
5 | export const UserSlide = (props) => {
6 | return {props.slide}
;
7 | };
8 |
9 | UserSlide.propTypes = {
10 | slide: PropTypes.node.isRequired
11 | };
12 |
13 | export const UserSlideThumbnail = (props) => {
14 | return (
15 |
16 | {props.slide}
17 |
18 | );
19 | };
20 |
21 | UserSlideThumbnail.propTypes = {
22 | slide: PropTypes.node.isRequired
23 | };
24 |
--------------------------------------------------------------------------------
/src/components/UserSlide/UserSlide.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../common/common.scss';
2 |
3 | .userSlide {
4 | height: 100%;
5 | width: 100%;
6 | display: flex;
7 | flex-direction: row;
8 | justify-content: center;
9 | align-items: flex-start;
10 | overflow-wrap: break-word;
11 | word-break: normal;
12 | white-space: normal;
13 | user-select: auto;
14 | }
15 |
16 | .thumbnail {
17 | user-select: none;
18 | -ms-zoom: $scale;
19 | zoom: $scale;
20 | font-size: $scale;
21 | text-size-adjust: none;
22 | pointer-events: none;
23 | }
24 |
25 | .thumbnail * {
26 | text-size-adjust: none;
27 | }
28 |
29 | .thumbnail > * {
30 | pointer-events: none;
31 | }
32 |
33 | /*Only for Chrome, Opera and Edge*/
34 | @media screen and (-webkit-min-device-pixel-ratio: 0) and (min-resolution: 0.001dpcm) {
35 | .thumbnail {
36 | font-size: 100%;
37 | }
38 | }
39 |
40 | /*Only for FireFox*/
41 | @-moz-document url-prefix() {
42 | .thumbnail {
43 | font-size: $scale;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/UserSlide/index.js:
--------------------------------------------------------------------------------
1 | export { UserSlide as default, UserSlideThumbnail } from './UserSlide';
2 |
--------------------------------------------------------------------------------
/src/components/Widgets/Widgets.js:
--------------------------------------------------------------------------------
1 | import React, { memo, useRef } from 'react';
2 | import styles from './Widgets.module.css';
3 | import IconButton from '../IconButton';
4 | import useNoSwipe from '../../utils/useNoSwipe';
5 | import PropTypes from 'prop-types';
6 | import {
7 | smallWidgetPositions,
8 | largeWidgetPositions
9 | } from '../../utils/validators';
10 |
11 | const arrowButtonPropTypes = {
12 | position: smallWidgetPositions.isRequired,
13 | isDisabled: PropTypes.bool.isRequired,
14 | icon: PropTypes.node,
15 | isRTL: PropTypes.bool.isRequired,
16 | hasShadow: PropTypes.bool.isRequired,
17 | onClick: PropTypes.func.isRequired
18 | };
19 |
20 | export const LeftButton = (props) => {
21 | return (
22 |
30 |
37 |
38 | );
39 | };
40 |
41 | LeftButton.propTypes = arrowButtonPropTypes;
42 |
43 | export const RightButton = (props) => {
44 | return (
45 |
53 |
60 |
61 | );
62 | };
63 |
64 | RightButton.propTypes = arrowButtonPropTypes;
65 |
66 | export const MediaButton = (props) => {
67 | return (
68 |
69 |
76 |
77 | );
78 | };
79 |
80 | MediaButton.propTypes = {
81 | position: smallWidgetPositions.isRequired,
82 | isPlaying: PropTypes.bool.isRequired,
83 | pauseIcon: PropTypes.node,
84 | playIcon: PropTypes.node,
85 | hasShadow: PropTypes.bool.isRequired,
86 | onClick: PropTypes.func.isRequired
87 | };
88 |
89 | export const SizeButton = (props) => {
90 | return (
91 |
92 |
99 |
100 | );
101 | };
102 |
103 | SizeButton.propTypes = {
104 | position: smallWidgetPositions.isRequired,
105 | isMaximized: PropTypes.bool.isRequired,
106 | minIcon: PropTypes.node,
107 | maxIcon: PropTypes.node,
108 | hasShadow: PropTypes.bool.isRequired,
109 | onClick: PropTypes.func.isRequired
110 | };
111 |
112 | export const IndexBoard = (props) => {
113 | const ref = useRef(null);
114 |
115 | return (
116 |
128 |
129 | {/* make curIndex and totalIndices fallback to 0 */}
130 | {props.curIndex || 0} / {props.totalIndices || 0}
131 |
132 |
133 | );
134 | };
135 |
136 | IndexBoard.propTypes = {
137 | position: smallWidgetPositions.isRequired,
138 | hasShadow: PropTypes.bool.isRequired,
139 | curIndex: PropTypes.number.isRequired,
140 | totalIndices: PropTypes.number.isRequired
141 | };
142 |
143 | IndexBoard.defaultProps = {
144 | curIndex: 0,
145 | totalIndices: 0
146 | };
147 |
148 | export const DotButtons = (props) => {
149 | const callbacks = props.callbacks;
150 |
151 | return (
152 |
153 |
158 | {Object.keys(callbacks).map((key, index) => (
159 |
175 | ))}
176 |
177 |
178 | );
179 | };
180 |
181 | DotButtons.propTypes = {
182 | callbacks: PropTypes.objectOf(PropTypes.func).isRequired,
183 | position: largeWidgetPositions.isRequired,
184 | isRTL: PropTypes.bool.isRequired,
185 | curIndex: PropTypes.number.isRequired,
186 | activeIcon: PropTypes.node,
187 | passiveIcon: PropTypes.node,
188 | hasShadow: PropTypes.bool.isRequired
189 | };
190 |
191 | DotButtons.defaultProps = {
192 | curIndex: 0
193 | };
194 |
195 | // memo is useful here
196 | export const Caption = memo((props) => {
197 | const captionRef = useRef(null);
198 |
199 | // allow the user to select text using cursor or finger
200 | useNoSwipe(captionRef);
201 |
202 | return (
203 |
216 | {props.text}
217 |
218 | );
219 | });
220 |
221 | Caption.type.displayName = 'Caption';
222 |
223 | Caption.propTypes = {
224 | position: largeWidgetPositions.isRequired,
225 | hasShadow: PropTypes.bool.isRequired,
226 | text: PropTypes.string
227 | };
228 |
--------------------------------------------------------------------------------
/src/components/Widgets/Widgets.module.css:
--------------------------------------------------------------------------------
1 | .widgetWrapper {
2 | position: absolute;
3 | display: flex;
4 | justify-content: center;
5 | align-items: center;
6 | }
7 |
8 | .disabled {
9 | opacity: 0.25;
10 | pointer-events: none;
11 | cursor: not-allowed;
12 | }
13 |
14 | .textWrapper {
15 | height: 50px;
16 | padding: 0 10px 0;
17 | display: flex;
18 | flex-direction: row;
19 | justify-content: center;
20 | align-items: center;
21 | user-select: text;
22 | cursor: text;
23 | }
24 |
25 | .captionWrapper {
26 | width: 100%;
27 | max-width: 100%;
28 | max-height: 100%;
29 | overflow: auto;
30 | display: flex;
31 | flex-direction: row;
32 | justify-content: center;
33 | align-items: flex-start;
34 | user-select: text;
35 | cursor: text;
36 | }
37 |
38 | .text {
39 | background: #fff;
40 | font-weight: bold;
41 | padding: 5px;
42 | opacity: 0.75;
43 | height: min-content;
44 | }
45 |
46 | .shadow {
47 | filter: drop-shadow(0px 0px 5px #888);
48 | }
49 |
50 | .buttonsWrapper {
51 | height: 100%;
52 | overflow-x: hidden;
53 | display: flex;
54 | flex-direction: row;
55 | justify-content: center;
56 | align-items: center;
57 | flex-wrap: wrap;
58 | }
59 |
60 | .buttonsWrapper.RTL {
61 | flex-direction: row-reverse;
62 | }
63 |
64 | /*center positions*/
65 |
66 | .centerLeft {
67 | top: 50%;
68 | left: 0;
69 | transform: translateY(-50%);
70 | }
71 |
72 | .centerCenter {
73 | top: 50%;
74 | left: 50%;
75 | transform: translate(-50%, -50%);
76 | }
77 |
78 | .centerRight {
79 | top: 50%;
80 | right: 0;
81 | transform: translateY(-50%);
82 | }
83 |
84 | /*top positions*/
85 |
86 | .topLeft {
87 | top: 0;
88 | left: 0;
89 | }
90 |
91 | .topCenter {
92 | top: 0;
93 | left: 0;
94 | right: 0;
95 | margin: 0 auto;
96 | width: fit-content;
97 | }
98 |
99 | .top {
100 | top: 0;
101 | left: 0;
102 | right: 0;
103 | margin: 0 auto;
104 | width: fit-content;
105 | }
106 |
107 | .topRight {
108 | top: 0;
109 | right: 0;
110 | }
111 |
112 | /*bottom positions*/
113 |
114 | .bottomLeft {
115 | bottom: 0;
116 | left: 0;
117 | }
118 |
119 | .bottomCenter {
120 | bottom: 0;
121 | left: 0;
122 | right: 0;
123 | margin: 0 auto;
124 | width: fit-content;
125 | }
126 |
127 | .bottom {
128 | bottom: 0;
129 | left: 0;
130 | right: 0;
131 | margin: 0 auto;
132 | width: fit-content;
133 | }
134 |
135 | .bottomRight {
136 | bottom: 0;
137 | right: 0;
138 | }
139 |
--------------------------------------------------------------------------------
/src/components/Widgets/index.js:
--------------------------------------------------------------------------------
1 | export {
2 | LeftButton,
3 | RightButton,
4 | MediaButton,
5 | SizeButton,
6 | DotButtons,
7 | IndexBoard,
8 | Caption
9 | } from './Widgets';
10 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Carousel from './components/Carousel';
2 |
3 | export default Carousel;
4 |
--------------------------------------------------------------------------------
/src/index.test.js:
--------------------------------------------------------------------------------
1 | import Carousel from '.';
2 | const { describe, it, expect } = global;
3 |
4 | // unit testing
5 | describe('Carousel', () => {
6 | it('is truthy', () => {
7 | expect(Carousel).toBeTruthy();
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/src/utils/ReversedMap.js:
--------------------------------------------------------------------------------
1 | export default class ReversedMap {
2 | constructor(map, isMaximized) {
3 | this.reversedMap = {};
4 |
5 | Object.keys(map).forEach((key) => {
6 | if (isMaximized && key.endsWith('AtMax')) return;
7 | const value = map[key];
8 | this.reversedMap[value] = key;
9 | });
10 | }
11 |
12 | get = (key) => {
13 | return this.reversedMap[key];
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/SlidesFactory.js:
--------------------------------------------------------------------------------
1 | // keep SlidesFactory for future use (for other features)
2 | export default class SlidesFactory {
3 | CreateSlides(slides, options) {
4 | // if (options.isRTL) return new SlidesWithRTL(slides, options);
5 | return new Slides(slides, options);
6 | }
7 | }
8 |
9 | class Slides {
10 | constructor(items, { index, isLoop = false }) {
11 | this._isLoop = isLoop;
12 |
13 | // generate _slides
14 | this._slides = items;
15 | if (!this._slides || !this._slides.length) return;
16 | this._length = this._slides.length;
17 |
18 | // calculate indices
19 | this._minIndex = 0;
20 | this._maxIndex = this._length - 1;
21 | this._headIndex = this._minIndex;
22 | this._curIndex = this._convertCurIndexForDisplayToCurIndex(index);
23 | }
24 |
25 | _isIndexInRange(index) {
26 | return this._minIndex <= index && index <= this._maxIndex;
27 | }
28 |
29 | _convertCurIndexForDisplayToCurIndex(index) {
30 | if (!index) return this._headIndex;
31 | if (!this._isIndexInRange(index)) return this._headIndex;
32 | return index;
33 | }
34 |
35 | get slides() {
36 | return this._slides;
37 | }
38 |
39 | get length() {
40 | return this._length;
41 | }
42 |
43 | get curIndex() {
44 | return this._curIndex;
45 | }
46 |
47 | get curIndexForDisplay() {
48 | return this._curIndex + 1;
49 | }
50 |
51 | isMinIndex() {
52 | return this._curIndex === this._minIndex;
53 | }
54 |
55 | isMaxIndex() {
56 | return this._curIndex === this._maxIndex;
57 | }
58 |
59 | static _range(min, max) {
60 | const length = max - min + 1;
61 | return Array(length)
62 | .fill(min)
63 | .map((x, index) => x + index);
64 | }
65 |
66 | get allIndices() {
67 | if (!this._length) return [];
68 | return this.constructor._range(this._minIndex, this._maxIndex);
69 | }
70 |
71 | calibrateIndex(change) {
72 | if (!this._length) return false;
73 | if (!this._isLoop) return false;
74 | if (this._curIndex === this._minIndex && change < 0) {
75 | this._curIndex = this._maxIndex + 1;
76 | return true;
77 | } else if (this._curIndex === this._maxIndex && change > 0) {
78 | this._curIndex = this._minIndex - 1;
79 | return true;
80 | }
81 | return false;
82 | }
83 |
84 | canUpdateIndex(change) {
85 | if (!this._length) return false;
86 | if (change === 0) return false;
87 | if (this._isLoop) return true;
88 | return this._isIndexInRange(this._curIndex + change);
89 | }
90 |
91 | updateIndex(change) {
92 | if (!this._length) return false;
93 | if (!this.canUpdateIndex(change)) return false;
94 | this._curIndex = Math.abs(
95 | (this._length + this._curIndex + change) % this._length
96 | );
97 | return true;
98 | }
99 |
100 | _canGoToIndex(index) {
101 | if (!this._length) return false;
102 | return this._isIndexInRange(index);
103 | }
104 |
105 | goToIndex(index) {
106 | if (!this._length) return false;
107 | if (!this._canGoToIndex(index)) return false;
108 | this._curIndex = index;
109 | return true;
110 | }
111 | }
112 |
113 | /*
114 | // deprecated code used for RTL support, in favour of CSS flex-direction: row-reverse,
115 | // since merely reverse the order of slides in RTL carousel can not reverse
116 | // the order of "tabbing" (keyboard navigation) of dot buttons and thumbnails.
117 |
118 | class SlidesWithRTL extends Slides {
119 | constructor(items, options) {
120 | super(items, options);
121 |
122 | // generate _slides
123 | this._slides = [...items].reverse();
124 |
125 | // calculate indices
126 | this._headIndex = this._maxIndex;
127 | this._curIndex = this._convertCurIndexForDisplayToCurIndex(options.index);
128 | }
129 |
130 | _convertCurIndexForDisplayToCurIndex(index) {
131 | if (!index) return this._headIndex;
132 | index -= 1;
133 | if (!this._isIndexInRange(index)) return this._headIndex;
134 | return this._headIndex - index;
135 | }
136 |
137 | get curIndexForDisplay() {
138 | return this._headIndex - this._curIndex + 1;
139 | }
140 | }
141 | */
142 |
--------------------------------------------------------------------------------
/src/utils/SlidesFactory.test.js:
--------------------------------------------------------------------------------
1 | import SlidesFactory from './SlidesFactory';
2 | const { describe, it, expect } = global;
3 |
4 | const slidesFactory = new SlidesFactory();
5 |
6 | // Tests for base cases
7 | const items = [1, 2, 3, 4, 5, 6];
8 |
9 | describe('6 items without RTL', () => {
10 | const slides = slidesFactory.CreateSlides(items, {});
11 | const expectedCurIndex = 0;
12 | it('has slides in the correct order', () => {
13 | expect(slides.slides).toStrictEqual([1, 2, 3, 4, 5, 6]);
14 | });
15 | it('has the correct current index', () => {
16 | expect(slides.curIndex).toBe(expectedCurIndex);
17 | });
18 | it('cannot move left', () => {
19 | expect(slides.calibrateIndex(-1)).toBe(false);
20 | expect(slides.updateIndex(-1)).toBe(false);
21 | expect(slides.curIndex).toBe(expectedCurIndex);
22 | });
23 | it('can move right', () => {
24 | expect(slides.calibrateIndex(+1)).toBe(false);
25 | expect(slides.updateIndex(+1)).toBe(true);
26 | expect(slides.curIndex).toBe(expectedCurIndex + 1);
27 | });
28 | });
29 |
30 | describe('6 items with RTL', () => {
31 | const slides = slidesFactory.CreateSlides(items, { isRTL: true });
32 | const expectedCurIndex = 0;
33 | it('has slides in the correct order', () => {
34 | expect(slides.slides).toStrictEqual([1, 2, 3, 4, 5, 6]);
35 | });
36 | it('has the correct current index', () => {
37 | expect(slides.curIndex).toBe(expectedCurIndex);
38 | });
39 | it('cannot move right', () => {
40 | expect(slides.calibrateIndex(-1)).toBe(false);
41 | expect(slides.updateIndex(-1)).toBe(false);
42 | expect(slides.curIndex).toBe(expectedCurIndex);
43 | });
44 | it('can move left', () => {
45 | expect(slides.calibrateIndex(+1)).toBe(false);
46 | expect(slides.updateIndex(+1)).toBe(true);
47 | expect(slides.curIndex).toBe(expectedCurIndex + 1);
48 | });
49 | });
50 |
51 | // Test for edge case
52 | describe('0 item without RTL', () => {
53 | const slides = slidesFactory.CreateSlides([], {});
54 | const expectedCurIndex = undefined;
55 | it('does not have a slide', () => {
56 | expect(slides.slides).toStrictEqual([]);
57 | });
58 | it('does not have current index', () => {
59 | expect(slides.curIndex).toBe(expectedCurIndex);
60 | });
61 | it('cannot move left', () => {
62 | expect(slides.calibrateIndex(-1)).toBe(false);
63 | expect(slides.updateIndex(-1)).toBe(false);
64 | expect(slides.curIndex).toBe(expectedCurIndex);
65 | });
66 | it('cannot move right', () => {
67 | expect(slides.calibrateIndex(+1)).toBe(false);
68 | expect(slides.updateIndex(+1)).toBe(false);
69 | expect(slides.curIndex).toBe(expectedCurIndex);
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/src/utils/isSSR.js:
--------------------------------------------------------------------------------
1 | // determine if the code is executed on a server (as oppose to a browser)
2 | const isSSR = !(
3 | typeof window !== 'undefined' && window.document?.createElement
4 | );
5 |
6 | export default isSSR;
7 |
--------------------------------------------------------------------------------
/src/utils/useAnchor.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect } from 'react';
2 | import useEventListener from './useEventListener';
3 | import useMediaQuery from './useMediaQuery';
4 | import isSSR from './isSSR';
5 |
6 | const useAnchor = (elementRef, isMaximized) => {
7 | const element = elementRef && elementRef.current;
8 | const container = element && element.parentNode.parentNode;
9 |
10 | // the word 'was' is used to denote the condition before React has updated the DOM,
11 | // that is in contrast with the condition after React has updated the DOM in useEffect().
12 | const wasInitialRender = !(element && container);
13 | const wasLeftMost = element && element.offsetLeft <= 0;
14 | const wasRightMost =
15 | element &&
16 | container &&
17 | element.offsetLeft + element.clientWidth >= container.clientWidth;
18 |
19 | // get reduced motion setting for determining the need to smooth scrolling for later
20 | const isReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
21 |
22 | // center the element in the container without smoothness
23 | const centerElement = useCallback(() => {
24 | const element = elementRef.current;
25 | if (!element) return;
26 | const container = element.parentNode.parentNode;
27 |
28 | // cannot use element.scrollIntoView(element, { behavior: 'smooth', block: 'nearest', inline: 'center' });
29 | // because it will also cause unwanted vertical movement when the element is not vertically in the viewport
30 | // (e.g. the element is somewhere down the page)
31 | container.scrollTo({
32 | top: 0,
33 | left:
34 | element.offsetLeft - container.clientWidth / 2 + element.clientWidth / 2
35 | });
36 | }, [elementRef]);
37 |
38 | // center the element in the container with smoothness under certain conditions
39 | const centerElementSmoothly = useCallback(() => {
40 | const element = elementRef.current;
41 | if (!element) return;
42 | const container = element.parentNode.parentNode;
43 |
44 | const isLeftMost = element && element.offsetLeft === 0;
45 | const isRightMost =
46 | element &&
47 | container &&
48 | element.offsetLeft + element.clientWidth >= container.clientWidth;
49 |
50 | // cannot use CSS scroll-behavior: smooth; due to the necessity of dynamically determining the need to smooth scrolling;
51 | // the need to smooth scrolling is determined dynamically because:
52 | // 1. smooth scrolling for the initial render with the current index not on the left-most thumbnail will traverse intermediate thumbnails, thus it will download unnecessary thumbnails;
53 | // 2. smooth scrolling for moving between the left-most thumbnail and the right-most thumbnail will traverse intermediate slides (essentially all the thumbnails), thus it will download unnecessary thumbnails;
54 | // 3. smooth scrolling should not be applied for users with reduce motion setting turned on
55 | const options =
56 | wasInitialRender ||
57 | (wasLeftMost && isRightMost) ||
58 | (wasRightMost && isLeftMost) ||
59 | isReducedMotion
60 | ? {}
61 | : { behavior: 'smooth' };
62 |
63 | container.scrollTo({
64 | top: 0,
65 | left:
66 | element.offsetLeft -
67 | container.clientWidth / 2 +
68 | element.clientWidth / 2,
69 | ...options
70 | });
71 | // smooth scrolling on Element.scrollTo(), currently does not work on Safari and IE, unlike Window.scrollTo();
72 | // in the future, a polyfill is therefore needed here for smooth scrolling to work across browsers
73 | }, [
74 | elementRef,
75 | wasInitialRender,
76 | wasLeftMost,
77 | wasRightMost,
78 | isReducedMotion
79 | ]);
80 |
81 | // center the current element without smoothness on init, on maximize and on minimize
82 | useEffect(() => centerElement(), [centerElement, isMaximized]);
83 |
84 | // center the current element with smoothness on index update
85 | useEffect(() => centerElementSmoothly());
86 |
87 | // center the current element on click
88 | useEventListener(elementRef.current, 'click', centerElement);
89 |
90 | // center the current element on resize (including orientationchange)
91 | useEventListener(isSSR ? undefined : window, 'resize', centerElement);
92 | };
93 |
94 | export default useAnchor;
95 |
--------------------------------------------------------------------------------
/src/utils/useEventListener.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 |
3 | const useEventListener = (element, event, callback) => {
4 | const callbackRef = useRef(null);
5 | callbackRef.current = callback;
6 |
7 | useEffect(() => {
8 | const callback = callbackRef.current;
9 | if (element) element.addEventListener(event, callback);
10 | return () => {
11 | if (element) element.removeEventListener(event, callback);
12 | };
13 | }, [element, event, callback]);
14 | };
15 |
16 | export default useEventListener;
17 |
--------------------------------------------------------------------------------
/src/utils/useFixedPosition.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | const useFixedPosition = (initialState, elementToFocusRef) => {
4 | const [isFixed, setIsFixed] = useState(initialState);
5 |
6 | useEffect(() => {
7 | const scrollX = window.scrollX;
8 | const scrollY = window.scrollY;
9 |
10 | const bodyElement = document.querySelector('body');
11 | const overflowValue = bodyElement.style.overflow;
12 |
13 | if (isFixed) {
14 | bodyElement.style.overflow = 'hidden';
15 | if (elementToFocusRef.current) elementToFocusRef.current.focus();
16 | }
17 |
18 | return () => {
19 | if (isFixed) {
20 | window.scrollTo(scrollX, scrollY);
21 | bodyElement.style.overflow = overflowValue;
22 | }
23 | };
24 | }, [isFixed, elementToFocusRef]);
25 |
26 | return [isFixed, setIsFixed];
27 | };
28 |
29 | export default useFixedPosition;
30 |
--------------------------------------------------------------------------------
/src/utils/useIntersectionObserver.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import isSSR from './isSSR';
3 |
4 | const useIntersectionObserver = (
5 | elementRef,
6 | rootRef,
7 | rootMargin = '0px 0px 0px 0px'
8 | ) => {
9 | const [isIntersecting, setIsIntersecting] = useState(false);
10 |
11 | useEffect(() => {
12 | // fallback for browsers those do not support IntersectionObserver (i.e. IE)
13 | if (!('IntersectionObserver' in window)) {
14 | return () => {};
15 | }
16 |
17 | const root = rootRef?.current ? rootRef.current : null;
18 |
19 | // eslint-disable-next-line no-undef
20 | const observer = new IntersectionObserver(
21 | ([entry], observer) => {
22 | if (!isIntersecting && entry.isIntersecting) {
23 | setIsIntersecting(true);
24 | observer.disconnect();
25 | }
26 | },
27 | // increase the size of the viewport using rootMargin to preload images
28 | { root: root, rootMargin: rootMargin, threshold: 0 }
29 | );
30 | if (elementRef.current) observer.observe(elementRef.current);
31 |
32 | return () => {
33 | if (observer) observer.disconnect();
34 | };
35 | }, [rootRef, rootMargin, elementRef, isIntersecting]);
36 |
37 | // fallback for SSR
38 | if (isSSR) {
39 | return false;
40 | }
41 |
42 | // fallback for browsers those do not support IntersectionObserver (i.e. IE)
43 | if (!('IntersectionObserver' in window)) {
44 | return true;
45 | }
46 |
47 | return isIntersecting;
48 | };
49 |
50 | export default useIntersectionObserver;
51 |
--------------------------------------------------------------------------------
/src/utils/useKeyboard.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | // import styles from '../components/Carousel/Carousel.module.css';
3 |
4 | const useKeyboard = (elementRef) => {
5 | useEffect(() => {
6 | const element = elementRef.current;
7 |
8 | const handleMouseDown = () => {
9 | // no need to check elementRef.current here,
10 | // because event listener is added on element
11 | if (!element) return;
12 | element.setAttribute('data-is-not-keyboard-user', 'true');
13 | // cannot use classList due to classList will get changed
14 | // element.classList.add(styles.isNotKeyboardUser);
15 | };
16 |
17 | const handleKeyDown = (event) => {
18 | if (!element) return;
19 | if (event.key !== 'Tab') return;
20 | element.setAttribute('data-is-not-keyboard-user', 'false');
21 | };
22 |
23 | if (element) {
24 | element.addEventListener('mousedown', handleMouseDown);
25 | element.addEventListener('keydown', handleKeyDown);
26 | }
27 |
28 | return () => {
29 | if (element) {
30 | element.removeEventListener('mousedown', handleMouseDown);
31 | element.removeEventListener('keydown', handleKeyDown);
32 | }
33 | };
34 | }, [elementRef]);
35 | };
36 |
37 | export default useKeyboard;
38 |
--------------------------------------------------------------------------------
/src/utils/useKeys.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 |
3 | const useKeys = (elementRef, callbacks) => {
4 | const callbackRef = useRef(null);
5 | callbackRef.current = callbacks;
6 |
7 | useEffect(() => {
8 | const handleKeyDown = (event) => {
9 | callbackRef.current[event.key] && callbackRef.current[event.key](event);
10 | };
11 |
12 | const element = elementRef.current;
13 | if (element) element.addEventListener('keydown', handleKeyDown);
14 |
15 | return () => {
16 | if (element) element.removeEventListener('keydown', handleKeyDown);
17 | };
18 | }, [elementRef, callbackRef]);
19 | };
20 |
21 | export default useKeys;
22 |
--------------------------------------------------------------------------------
/src/utils/useMediaQuery.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import isSSR from './isSSR';
3 |
4 | const useMediaQuery = (query) => {
5 | const mediaQueryList = !isSSR && window.matchMedia(query);
6 | const [matches, setMatches] = useState(mediaQueryList.matches);
7 |
8 | useEffect(() => {
9 | const callback = () => setMatches(mediaQueryList.matches);
10 | mediaQueryList.addEventListener
11 | ? mediaQueryList.addEventListener('change', callback)
12 | : window.addEventListener('resize', callback);
13 |
14 | return () =>
15 | mediaQueryList.removeEventListener
16 | ? mediaQueryList.removeEventListener('change', callback)
17 | : window.removeEventListener('resize', callback);
18 | }, [mediaQueryList]);
19 |
20 | return matches;
21 | };
22 |
23 | export default useMediaQuery;
24 |
--------------------------------------------------------------------------------
/src/utils/useMouse.js:
--------------------------------------------------------------------------------
1 | const useMouse = (elementRef, { onMouseMove, onMouseUp, onTap }) => {
2 | let isMouseDown = false;
3 | let isMouseMoved = false;
4 | let mouseDownX = 0;
5 | let previousX = 0;
6 | let previousTime = Date.now();
7 | let instantaneousVelocity = 0;
8 |
9 | const handleMouseDown = (event) => {
10 | if (elementRef.current) elementRef.current.classList.add('isGrabbing');
11 | if (event.buttons > 0) isMouseDown = true;
12 | mouseDownX = event.clientX;
13 | previousX = event.clientX;
14 | previousTime = Date.now();
15 | };
16 |
17 | const handleMouseMove = (event) => {
18 | if (isMouseDown && event.buttons > 0) {
19 | onMouseMove(event.clientX - mouseDownX, 0, event.clientX - previousX);
20 | isMouseMoved = true;
21 | instantaneousVelocity =
22 | (event.clientX - previousX) / (Date.now() - previousTime);
23 | previousX = event.clientX;
24 | previousTime = Date.now();
25 | }
26 | };
27 |
28 | const handleMouseUp = (event) => {
29 | if (elementRef.current) elementRef.current.classList.remove('isGrabbing');
30 | if (isMouseDown) {
31 | if (isMouseMoved) {
32 | // can not calculate velocity here since event.clientX === previousX;
33 | // ignore vertical displacement by not passing it as argument
34 | onMouseUp(event.clientX - mouseDownX, 0, instantaneousVelocity, event);
35 | } else onTap();
36 | }
37 | isMouseDown = false; // reset isMouseDown for next series of mouse events
38 | isMouseMoved = false; // reset isMouseMoved for next series of mouse events
39 | };
40 |
41 | return {
42 | onMouseDown: (event) => handleMouseDown(event),
43 | onMouseMove: (event) => handleMouseMove(event),
44 | onMouseUpCapture: (event) => handleMouseUp(event),
45 | onMouseLeave: (event) => handleMouseUp(event)
46 | };
47 | };
48 |
49 | export default useMouse;
50 |
--------------------------------------------------------------------------------
/src/utils/useMouseDrag.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import useMouse from './useMouse';
3 |
4 | const useMouseDrag = (elementRef) => {
5 | const handleMouseMove = (displacementX, displacementY, deltaX) => {
6 | const element = elementRef.current;
7 | if (!element) return;
8 |
9 | // convert drag to scroll
10 | // displacementX is the cumulative change, whereas deltaX is the individual change
11 | element.scrollTo({
12 | top: 0,
13 | left: element.scrollLeft - deltaX
14 | });
15 | };
16 |
17 | const handleMouseUp = (displacementX, displacementY, velocity, event) => {
18 | // allow a mouseup event after a tiny mouse movement (<= 2px) to propagate
19 | if (Math.abs(displacementX) <= 2) return;
20 |
21 | // prevent any other mouseup events to propagate
22 | event.stopPropagation();
23 |
24 | /* set up momentum-based dragging by giving inertia to the element which allows it
25 | to continue travelling in its direction and gradually slow down by friction,
26 | like the mouse drag version of "-webkit-overflow-scrolling: touch;" */
27 | const speed = Math.abs(velocity);
28 | // neglect dragging with small instantaneous velocity
29 | if (speed < 0.5) return false;
30 |
31 | const element = elementRef.current;
32 | const childElement = element && element.childNodes[0];
33 | const isLeftMost = element && element.scrollLeft === 0;
34 | const isRightMost =
35 | element &&
36 | childElement &&
37 | element.scrollLeft + element.clientWidth >= childElement.scrollWidth;
38 | // neglect dragging at the two ends
39 | if (isLeftMost || isRightMost) return false;
40 |
41 | const CONSTANT = 0.025;
42 | const totalTime = speed / CONSTANT;
43 | let initialTime;
44 | let previousElapsedTime;
45 |
46 | function step(currentTime) {
47 | // assign value to initialTime for the initial step (run of this function)
48 | if (initialTime === undefined) initialTime = currentTime;
49 |
50 | // calculate the change in displacement since the last time
51 | const elapsedTime = currentTime - initialTime;
52 | const intervalDiff = elapsedTime - previousElapsedTime;
53 | const intervalSum = elapsedTime + previousElapsedTime;
54 | const distance =
55 | speed * intervalDiff - (CONSTANT * intervalDiff * intervalSum) / 2;
56 | const displacement = velocity < 0 ? -distance : distance;
57 |
58 | // update the UI which shifts the element
59 | if (!isNaN(displacement)) {
60 | element.scrollTo({
61 | top: 0,
62 | left: element.scrollLeft - displacement
63 | });
64 | }
65 |
66 | // check if the elapsedTime is within the totalTime
67 | if (elapsedTime < totalTime) {
68 | // record the elapsedTime
69 | previousElapsedTime = elapsedTime;
70 | window.requestAnimationFrame(step);
71 | }
72 | }
73 |
74 | window.requestAnimationFrame(step);
75 |
76 | return false;
77 | };
78 |
79 | useEffect(() => {
80 | // disable selection on the element to ensure the value of
81 | // CSS 'cursor' property is not the default 'text' on select on Safari
82 | if (elementRef.current) elementRef.current.onselectstart = () => false;
83 | }, [elementRef]);
84 |
85 | return useMouse(elementRef, {
86 | onMouseMove: handleMouseMove,
87 | onMouseUp: handleMouseUp,
88 | onTap: () => {}
89 | });
90 | };
91 |
92 | export default useMouseDrag;
93 |
--------------------------------------------------------------------------------
/src/utils/useNoDrag.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 |
3 | const useNoDrag = (elementRef) => {
4 | useEffect(() => {
5 | const element = elementRef.current;
6 | const handleDrag = (e) => e.preventDefault();
7 |
8 | if (element) element.addEventListener('dragstart', handleDrag);
9 |
10 | return () => {
11 | if (element) element.removeEventListener('dragstart', handleDrag);
12 | };
13 | }, [elementRef]);
14 | };
15 |
16 | export default useNoDrag;
17 |
--------------------------------------------------------------------------------
/src/utils/useNoOverScroll.js:
--------------------------------------------------------------------------------
1 | const useNoOverScroll = (elementRef) => {
2 | // Prevent the default behaviour of "scroll chaining" where parent element
3 | // gets scrolled when the child element is over scrolled,
4 | // in order to prevent going to the previous or the next page.
5 | // This code is for Safari only, while other browsers are taken care by CSS
6 | return (event) => {
7 | if (Math.abs(event.deltaX) < Math.abs(event.deltaY)) return;
8 | const { scrollLeft, scrollWidth, offsetWidth } = elementRef.current;
9 | if (
10 | (scrollLeft + event.deltaX < 0 ||
11 | scrollLeft + event.deltaX > scrollWidth - offsetWidth) &&
12 | event.cancelable
13 | )
14 | event.preventDefault();
15 | };
16 | };
17 |
18 | export default useNoOverScroll;
19 |
--------------------------------------------------------------------------------
/src/utils/useNoSwipe.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 |
3 | const useNoSwipe = (elementRef) => {
4 | useEffect(() => {
5 | const el = elementRef.current;
6 | const handleSwipe = (e) => {
7 | e.stopPropagation();
8 | };
9 |
10 | if (el) {
11 | el.addEventListener('mousedown', handleSwipe);
12 | el.addEventListener('touchstart', handleSwipe, { passive: true });
13 | }
14 |
15 | return () => {
16 | if (!el) {
17 | el.removeEventListener('mousedown', handleSwipe);
18 | el.removeEventListener('touchstart', handleSwipe, { passive: true });
19 | }
20 | };
21 | }, [elementRef]);
22 | };
23 |
24 | export default useNoSwipe;
25 |
--------------------------------------------------------------------------------
/src/utils/useSlides.js:
--------------------------------------------------------------------------------
1 | import SlidesFactory from './SlidesFactory';
2 | import { useMemo } from 'react';
3 |
4 | const useSlides = (items, { index, isLoop }) => {
5 | return useMemo(() => {
6 | const slidesFactory = new SlidesFactory();
7 | const slides = slidesFactory.CreateSlides(items, {
8 | index: index,
9 | isLoop: isLoop
10 | });
11 | const slidesElements = slides.slides;
12 | return [slides, slidesElements];
13 | }, [items, index, isLoop]);
14 | };
15 |
16 | export default useSlides;
17 |
--------------------------------------------------------------------------------
/src/utils/useSwipe.js:
--------------------------------------------------------------------------------
1 | import useTouch from './useTouch';
2 | import useMouse from './useMouse';
3 | import useNoDrag from './useNoDrag';
4 |
5 | const useSwipe = (
6 | elementRef,
7 | swipePercentageMin,
8 | {
9 | onSwipeMoveX,
10 | onSwipeMoveY,
11 | onSwipeEndRight,
12 | onSwipeEndLeft,
13 | onSwipeEndDisqualified,
14 | onSwipeEndDown,
15 | onTap
16 | }
17 | ) => {
18 | let isInitialSwipeVertical;
19 |
20 | const handleSwipeEnd = (displacementX, displacementY = 0, velocity = 0) => {
21 | const { clientWidth: width, clientHeight: height } = elementRef.current;
22 | const distanceXMin = width * swipePercentageMin;
23 | const distanceYMin = height * swipePercentageMin;
24 | const speed = Math.abs(velocity);
25 | if (
26 | !isInitialSwipeVertical &&
27 | // displacementX <= -Math.abs(displacementY) &&
28 | displacementX <= -distanceXMin
29 | )
30 | onSwipeEndLeft(displacementX, speed);
31 | else if (
32 | !isInitialSwipeVertical &&
33 | // displacementX >= Math.abs(displacementY) &&
34 | displacementX >= distanceXMin
35 | )
36 | onSwipeEndRight(displacementX, speed);
37 | else if (
38 | isInitialSwipeVertical &&
39 | // Math.abs(displacementX) < displacementY &&
40 | displacementY >= distanceYMin
41 | )
42 | onSwipeEndDown();
43 | else onSwipeEndDisqualified(displacementX, speed);
44 | isInitialSwipeVertical = undefined;
45 | };
46 |
47 | const handleSwipeMove = (displacementX, displacementY = 0) => {
48 | if (isInitialSwipeVertical === false) onSwipeMoveX(displacementX);
49 | else if (isInitialSwipeVertical) onSwipeMoveY(displacementX, displacementY);
50 | else {
51 | // when isInitialVerticalSwipe is undefined
52 | isInitialSwipeVertical =
53 | displacementY !== 0 &&
54 | Math.abs(displacementX) < Math.abs(displacementY);
55 | handleSwipeMove(displacementX, displacementY);
56 | }
57 | };
58 |
59 | // have to use event listeners (active event listeners) to deal with undesired tiny vertical movements
60 | useTouch(elementRef, {
61 | onTouchMove: handleSwipeMove,
62 | onTouchEnd: handleSwipeEnd,
63 | onTap: onTap
64 | });
65 |
66 | const mouseEventHandlers = useMouse(elementRef, {
67 | onMouseMove: handleSwipeMove,
68 | onMouseUp: handleSwipeEnd,
69 | onTap: onTap
70 | });
71 |
72 | useNoDrag(elementRef); // prevent dragging on FireFox
73 |
74 | return mouseEventHandlers;
75 | };
76 |
77 | export default useSwipe;
78 |
--------------------------------------------------------------------------------
/src/utils/useTimer.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | const getTimer = (interval, callback) => {
4 | const _interval = interval;
5 | const _callback = callback;
6 | let _timer;
7 |
8 | function start() {
9 | _timer = setInterval(_callback, _interval);
10 | }
11 |
12 | function stop() {
13 | _timer && clearInterval(_timer);
14 | }
15 |
16 | function restart() {
17 | stop();
18 | start();
19 | }
20 |
21 | return { start, stop, restart };
22 | };
23 |
24 | const useTimer = (interval, isStarted, callback) => {
25 | const timer = interval ? getTimer(interval, callback) : null;
26 | const [isRunning, setIsRunning] = useState(!!timer && isStarted);
27 |
28 | const start = () => !!timer && timer.start();
29 | const stop = () => !!timer && timer.stop();
30 | const restart = () => !!timer && timer.restart();
31 |
32 | useEffect(() => {
33 | if (isRunning) start();
34 |
35 | return () => {
36 | stop();
37 | };
38 | });
39 |
40 | return [isRunning, setIsRunning, { stopTimer: stop, restartTimer: restart }];
41 | };
42 |
43 | export default useTimer;
44 |
--------------------------------------------------------------------------------
/src/utils/useTouch.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 |
3 | const getTouchDistinguisher = () => {
4 | const pinchTouchIdentifiers = new Set(); // record all pinch touch identifiers
5 |
6 | function _recordPinchTouchIdentifiers(event) {
7 | for (const touch of event.touches) {
8 | pinchTouchIdentifiers.add(touch.identifier);
9 | }
10 | }
11 |
12 | function _isPinch(event) {
13 | return event.touches !== undefined && event.touches.length > 1;
14 | }
15 |
16 | function _wasPinch(event) {
17 | // check for the one finger that was part of a multi-finger pinch zoom
18 | return (
19 | event.changedTouches &&
20 | pinchTouchIdentifiers.has(event.changedTouches[0].identifier)
21 | );
22 | }
23 |
24 | function isPinch(event) {
25 | if (_isPinch(event)) {
26 | _recordPinchTouchIdentifiers(event);
27 | return true;
28 | }
29 | return _wasPinch(event);
30 | }
31 |
32 | return { isPinch };
33 | };
34 |
35 | const useTouch = (elementRef, { onTouchMove, onTouchEnd, onTap }) => {
36 | const touchDistinguisher = getTouchDistinguisher();
37 | let touchStartX = 0;
38 | let touchStartY = 0;
39 | let isTouchStarted = false;
40 | let isTouchMoved = false;
41 | let previousX = 0;
42 | let previousTime = Date.now();
43 | let instantaneousVelocity = 0;
44 |
45 | const handleVerticalMovement = (event, displacementX, displacementY) => {
46 | if (Math.abs(displacementX) > Math.abs(displacementY) && event.cancelable)
47 | event.preventDefault();
48 | };
49 |
50 | const shouldOmitEvent = (event, displacementX = 0) => {
51 | if (
52 | // touchDistinguisher only works for iOS from my investigation
53 | navigator.platform.match(/iPhone|iPad|iPod|MacIntel/) &&
54 | touchDistinguisher.isPinch(event)
55 | )
56 | return true;
57 |
58 | // window.visualViewport is not yet supported on IE
59 | if (!('visualViewport' in window)) return false;
60 |
61 | const { scale, offsetLeft, width } = window.visualViewport;
62 | if (scale <= 1.1) return false;
63 | // pan right at or beyond the left edge
64 | if (offsetLeft <= 0 && displacementX > 0) return false;
65 | // pan left at or beyond the right edge
66 | // noinspection RedundantIfStatementJS
67 | if (offsetLeft + width >= width * scale && displacementX < 0) return false;
68 | return true;
69 | };
70 |
71 | const handleTouchStart = (event) => {
72 | event.stopPropagation();
73 | isTouchStarted = true;
74 | touchStartX = event.touches[0].clientX;
75 | touchStartY = event.touches[0].clientY;
76 | previousX = touchStartX;
77 | previousTime = Date.now();
78 | };
79 |
80 | const handleTouchMove = (event) => {
81 | event.stopPropagation();
82 | if (!isTouchStarted) return;
83 | const displacementX = event.changedTouches[0].clientX - touchStartX;
84 | if (shouldOmitEvent(event, displacementX)) return;
85 | const displacementY = event.changedTouches[0].clientY - touchStartY;
86 | handleVerticalMovement(event, displacementX, displacementY);
87 | onTouchMove(displacementX, displacementY);
88 | isTouchMoved = true;
89 | instantaneousVelocity =
90 | (event.changedTouches[0].clientX - previousX) /
91 | (Date.now() - previousTime);
92 | previousX = event.changedTouches[0].clientX;
93 | previousTime = Date.now();
94 | };
95 |
96 | const handleTouchEnd = (event) => {
97 | // prevent the event from being recognized additionally as a mouse event on simulated mobile devices (e.g. Toggle Device Toolbar on Chrome).
98 | event.preventDefault();
99 | event.stopPropagation();
100 | if (!isTouchStarted) return;
101 | const displacementX = event.changedTouches[0].clientX - touchStartX;
102 | if (shouldOmitEvent(event, displacementX)) {
103 | onTouchEnd(0, 0, 0);
104 | return;
105 | }
106 | const displacementY = event.changedTouches[0].clientY - touchStartY;
107 | handleVerticalMovement(event, displacementX, displacementY);
108 | if (isTouchMoved)
109 | // can not calculate velocity here since event.clientX === previousX;
110 | onTouchEnd(displacementX, displacementY, instantaneousVelocity);
111 | else onTap();
112 | isTouchStarted = false; // reset isTouchStarted for next series of touch events
113 | isTouchMoved = false; // reset isTouchMoved for next series of touch events
114 | };
115 |
116 | // to fix the dependency, I needed to wrapping pretty much everything in this
117 | // file in useCallback or useMemo, so I disable exhaustive-deps check for now
118 | // eslint-disable-next-line react-hooks/exhaustive-deps
119 | const events = [
120 | { event: 'touchstart', callback: handleTouchStart },
121 | { event: 'touchmove', callback: handleTouchMove },
122 | { event: 'touchend', callback: handleTouchEnd },
123 | { event: 'touchcancel', callback: handleTouchEnd }
124 | ];
125 |
126 | useEffect(() => {
127 | const el = elementRef.current;
128 | if (el)
129 | // use active event listeners to have event.cancelable === true, for later use to in calling event.preventDefault()
130 | events.forEach(({ event, callback }) =>
131 | el.addEventListener(event, callback, { passive: false })
132 | );
133 |
134 | return () => {
135 | if (el)
136 | events.forEach(({ event, callback }) =>
137 | el.removeEventListener(event, callback)
138 | );
139 | };
140 | }, [elementRef, events]);
141 | };
142 |
143 | export default useTouch;
144 |
--------------------------------------------------------------------------------
/src/utils/validators.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export const positiveNumber = (allow0 = false, optional = true) => {
4 | return (props, propName, componentName) => {
5 | const prop = props[propName];
6 | if (optional && prop === undefined) return;
7 | if (typeof prop !== 'number' || prop < 0 || (!allow0 && prop === 0))
8 | return new Error(
9 | `Invalid prop \`${propName}\` of type \`${typeof prop}\` supplied to \`${componentName}\`, expected \`number\` ${
10 | allow0 ? '>=' : '>'
11 | } 0.`
12 | );
13 | };
14 | };
15 |
16 | export const numberBetween = (
17 | min,
18 | max,
19 | { includeMin = false, includeMax = false, optional = true } = {}
20 | ) => {
21 | return (props, propName, componentName) => {
22 | const prop = props[propName];
23 | if (optional && prop === undefined) return;
24 | if (
25 | typeof prop !== 'number' ||
26 | !(min <= prop <= max) ||
27 | (!includeMin && min === prop) ||
28 | (!includeMax && max === prop)
29 | )
30 | return new Error(
31 | `Invalid prop \`${propName}\` of type \`${typeof prop}\` supplied to \`${componentName}\`, expected ${min} ${
32 | includeMin ? '<=' : '<'
33 | } \`number\` ${includeMax ? '<=' : '<'} ${max}.`
34 | );
35 | };
36 | };
37 |
38 | const comparator = {
39 | '>=': (a, b) => a >= b
40 | };
41 |
42 | export const compareToProp = (operator, otherPropName, optional = true) => {
43 | return (props, propName, componentName) => {
44 | const prop = props[propName];
45 | const otherProp = props[otherPropName];
46 | if (optional && prop === undefined) return;
47 | if (
48 | typeof prop !== 'number' ||
49 | typeof otherProp !== 'number' ||
50 | !comparator[operator](prop, otherProp)
51 | )
52 | return new Error(
53 | `Invalid prop \`${propName}\` of type \`${typeof prop}\` supplied to \`${componentName}\`, expected ${propName} ${operator} ${otherPropName}.`
54 | );
55 | };
56 | };
57 |
58 | export const fallbackProps = (fallbackProps) => {
59 | return (props, propName, componentName) => {
60 | const prop = props[propName];
61 | if (prop !== undefined) return;
62 | for (const fallbackProp of fallbackProps) {
63 | if (props[fallbackProp] !== undefined) return;
64 | }
65 | return new Error(
66 | `The props \`${fallbackProps}\` and \`${propName} are marked as at least one required in \`${componentName}, but their values are all \`undefined\`.`
67 | );
68 | };
69 | };
70 |
71 | export const elementRef = PropTypes.shape({ current: PropTypes.object });
72 |
73 | export const objectFitStyles = PropTypes.oneOf([
74 | 'contain',
75 | 'cover',
76 | 'fill',
77 | 'none',
78 | 'scale-down'
79 | ]);
80 |
81 | export const smallWidgetPositions = PropTypes.oneOf([
82 | false,
83 | 'topLeft',
84 | 'topCenter',
85 | 'topRight',
86 | 'centerLeft',
87 | 'centerCenter',
88 | 'centerRight',
89 | 'bottomLeft',
90 | 'bottomCenter',
91 | 'bottomRight'
92 | ]);
93 |
94 | export const largeWidgetPositions = PropTypes.oneOf([false, 'top', 'bottom']);
95 |
96 | export const slideObject = PropTypes.oneOfType([
97 | PropTypes.object.isRequired,
98 | PropTypes.element.isRequired
99 | ]);
100 |
101 | export const imageObject = PropTypes.shape({
102 | src: PropTypes.string.isRequired,
103 | srcset: PropTypes.string,
104 | alt: PropTypes.string,
105 | thumbnail: PropTypes.string
106 | });
107 |
--------------------------------------------------------------------------------