Repository: tim-soft/react-spring-lightbox Branch: master Commit: 0e41af991138 Files: 49 Total size: 99.8 KB Directory structure: gitextract_1p6vrvbp/ ├── .babelrc.js ├── .browserslistrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .travis.yml ├── .vscode/ │ └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example/ │ ├── README.md │ ├── components/ │ │ ├── GalleryLightbox/ │ │ │ ├── components/ │ │ │ │ ├── GridImage.jsx │ │ │ │ ├── LightboxArrowButton.jsx │ │ │ │ ├── LightboxButtonControl.jsx │ │ │ │ └── LightboxHeader.jsx │ │ │ └── index.jsx │ │ └── InlineLightbox/ │ │ └── index.jsx │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages/ │ │ ├── _app.jsx │ │ ├── _document.jsx │ │ └── index.tsx │ └── tsconfig.json ├── jest-setup.ts ├── jest.config.js ├── package.json ├── rollup.config.mjs ├── src/ │ ├── __tests__/ │ │ ├── components/ │ │ │ └── SimpleLightbox.tsx │ │ └── lightbox.test.tsx │ ├── components/ │ │ ├── CreatePortal/ │ │ │ └── index.tsx │ │ ├── ImageStage/ │ │ │ ├── components/ │ │ │ │ ├── Image/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ImagePager/ │ │ │ │ │ └── index.tsx │ │ │ │ └── SSRImagePager/ │ │ │ │ └── SSRImagePager.tsx │ │ │ ├── index.tsx │ │ │ └── utils/ │ │ │ ├── getTranslateOffsetsFromScale.ts │ │ │ ├── imageIsOutOfBounds.ts │ │ │ ├── index.ts │ │ │ ├── useDoubleClick.tsx │ │ │ └── useRefSize.tsx │ │ ├── PageContainer/ │ │ │ └── index.tsx │ │ └── index.tsx │ ├── index.tsx │ └── types/ │ └── ImagesList.ts ├── tsconfig.buildtypes.json └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc.js ================================================ module.exports = { plugins: [ ['@babel/plugin-transform-class-properties'], ['@babel/plugin-transform-object-rest-spread'], ['@babel/plugin-transform-runtime', { regenerator: false }], ], presets: ['@babel/env', '@babel/react', '@babel/preset-typescript'], }; ================================================ FILE: .browserslistrc ================================================ > 0.5%, last 2 versions, Firefox ESR, not dead ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 indent_style = space indent_size = 4 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true ================================================ FILE: .eslintignore ================================================ # See https://help.github.com/ignore-files/ for more about ignoring files. # dependencies /node_modules example/node_modules/** # production /build /dist /coverage # misc .DS_Store .env npm-debug.log* yarn-debug.log* yarn-error.log* yarn.lock* package.json* CHANGELOG.md* README.md* example/.DS_Store example/.env example/npm-debug.log* example/yarn-debug.log* example/yarn-error.log* example/yarn.lock* ================================================ FILE: .eslintrc.js ================================================ /* eslint-disable sort-keys */ /** * Configure ESLint * * https://eslint.org/docs/user-guide/configuring */ module.exports = { env: { browser: true, es6: true, jest: true, }, extends: [ 'plugin:react/recommended', 'plugin:import/warnings', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', ], globals: { document: true, window: true, }, parser: '@typescript-eslint/parser', parserOptions: { sourceType: 'module', }, plugins: [ 'prettier', 'jsx-a11y', 'react', 'react-hooks', 'import', 'sort-destructure-keys', '@typescript-eslint', ], root: true, rules: { 'prettier/prettier': ['error', { endOfLine: 'auto' }], // Enforce React Hooks rules // https://www.npmjs.com/package/eslint-plugin-react-hooks 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn', 'sort-destructure-keys/sort-destructure-keys': [ 'error', { caseSensitive: false }, ], 'sort-keys': ['error', 'asc', { caseSensitive: false, natural: false }], 'sort-vars': [ 'error', { ignoreCase: true, }, ], 'react/jsx-sort-props': ['error', { ignoreCase: true }], '@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/member-ordering': [ 'error', { default: { order: 'alphabetically', }, classes: { order: 'as-written', }, }, ], }, settings: { 'import/resolver': { node: true, 'eslint-import-resolver-typescript': true, }, react: { version: 'detect', }, }, }; ================================================ FILE: .gitignore ================================================ # See https://help.github.com/ignore-files/ for more about ignoring files. # dependencies node_modules # builds coverage build dist .rpt2_cache .next # misc .DS_Store .env .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: .prettierignore ================================================ node_modules/** .next/** dist/** coverage/** example/node_modules/** example/.next/** yarn.lock yarn-error.log .editorconfig .eslintignore .gitignore .prettierignore .browserslistrc ================================================ FILE: .prettierrc.js ================================================ /** * Configure Prettier * * https://prettier.io/docs/en/configuration.html#basic-configuration */ module.exports = { endOfLine: 'auto', semi: true, singleQuote: true, tabWidth: 4, }; ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - 'lts/*' cache: yarn: true directories: - node_modules install: yarn install jobs: include: - stage: lint script: yarn lint - stage: test script: yarn test - stage: build script: yarn build branches: only: master notifications: email: false ================================================ FILE: .vscode/settings.json ================================================ { "editor.formatOnSave": true, "javascript.validate.enable": false, "editor.tabSize": 4, "editor.detectIndentation": false, "eslint.enable": true } ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. ## Upcoming - Add `swipe up` to close lightbox ## [1.8.0] - 2023-10-06 - Add inline mode SSR support - fix next.js + styled-components + SWC client/server classname mismatch error - Upgrade several dependencies - @react-spring/web ^9.7 - rollup 3 - typescript 5 ### Potentially Breaking This release removes the esm build as it doesn't work well with next.js + SWC compiler ## [1.7.1] - 2022-09-12 ### Fixed - Allow vertical scrolling with an InlineLightbox on mobile (even while finger is over the image) ## [1.7.0] - 2022-07-28 - A few small bug fixes in situations where the `items` array may change size - Upgrade several dependencies - "@react-spring/web": "9.5.2" - "rollup": "^2.77.2", ### Added - `Lightbox` can now be used as a slider component embedded in a page like slick-slider by using the `inline` prop ## [1.6.0] - 2021-06-06 - Upgrade to `@react-spring/web@9.2.1` stable from `@tim-soft/react-spring-web@9.0.0-beta.36` ## [1.5.0] - 2021-02-17 - Rewrite project with typescript 4 - Upgrade `react-use-gesture@7.0.15` to `react-use-gesture@9.0.4` - This upgrade should fix some miscellaneous bugs such as `unable to spread non iterable instance` and more consistent trackpad support ### Added - The `images` prop now accepts a list of objects whose properties can be _almost_ any valid React `` prop including `srcset` and `loading` (lazy loading) If you use typescript, the exact type can be imported/used like this ```typescript import Lightbox, { ImagesListType } from 'react-spring-lightbox'; const images: ImagesListType = [ { alt: 'Windows 10 Dark Mode Setting', 'aria-details': 'Some details', 'aria-disabled': 'false', loading: 'lazy', src: 'https://timellenberger.com/static/blog-content/dark-mode/win10-dark-mode.jpg', srcSet: '/wp-content/uploads/flamingo4x.jpg 4x, /wp-content/uploads/flamingo3x.jpg 3x, /wp-content/uploads/flamingo2x.jpg 2x, /wp-content/uploads/flamingo1x.jpg 1x', }, { alt: 'macOS Mojave Dark Mode Setting', 'aria-details': 'Some details', 'aria-disabled': 'false', loading: 'lazy', src: 'https://timellenberger.com/static/blog-content/dark-mode/macos-dark-mode.png', srcSet: '/wp-content/uploads/flamingo4x.jpg 4x, /wp-content/uploads/flamingo3x.jpg 3x, /wp-content/uploads/flamingo2x.jpg 2x, /wp-content/uploads/flamingo1x.jpg 1x', }, ]; const SimpleLightbox = () => ; ``` The exact type is: ```typescript export type ImagesListItem = Omit< React.HTMLProps, 'draggable' | 'onClick' | 'onDragStart' | 'ref' > & { alt: string; loading?: 'auto' | 'eager' | 'lazy'; src: string }; ``` Which translates to any React `` prop minus `draggable`, `onClick`, `onDragStart` and `ref` as they are used internally. `alt` and `src` are required and explicitly support `loading` as it is an experimental chrome feature not included in `React.HTMLProps`. ## [1.4.11] - 2020-06-10 ### Fixed - Use aliased version of react-spring dependencies, fixes "Cannot read property 'ref' of null" error ## [1.4.10] - 2020-05-03 ### Added - Optimize output bundles with Terser - Apply `babel-plugin-styled-components` babel plugin to optimize styled-components styles ## [1.4.9] - 2020-05-02 ### Fixed - Fix partially off-screen image stage in ie11 ### Added - Upgrade to `rollup@2.7.6`, `react-use-gesture@7.0.15` and `@babel/****@7.9.6` ## [1.4.8] - 2020-04-08 ### Fixed - Dropped `lodash.clamp` dependency - Call onPrev/onNext callbacks on all paging events, even at the beginning or end of image array to allow for infinite paging ## [1.4.7] - 2020-04-05 ### Added - Lower distance and velocity gesture threshold for a paging between images - Allow click to zoom while a paging animation completes - Upgrade to `rollup@2.3.3` and `react-use-gesture@7.0.10` - Add `sideEffects: false` to `package.json` ## [1.4.6] - 2020-04-04 ### Fixed - Handle edge case bugs with `singleClickToZoom` option - Fix undefined errors in panning drag handler on initial drags ## [1.4.5] - 2020-03-27 ### Added - Add optional `singleClickToZoom` prop which allows single click/tap zooming on images ## [1.4.4] - 2020-03-24 ### Fixed - Add orientationchange event listener for ios devices ## [1.4.3] - 2020-03-23 ### Fixed - Drop lodash.merge - Fix image heights not adjusting on window resize ## [1.4.2] - 2020-03-15 ### Fixed - Remove need for react-use-measure and @juggle/resize-observer ## [1.4.1] - 2020-03-13 ### Fixed - Fix image stage height on Safari - Upgrade to react-use-gesture@7.0.5 - Upgrade to rollup@2.0.6 ## [1.4.0] - 2020-03-7 ### BREAKING CHANGE - Replaced inline styles with styled-components. This library now has a peer dependency on `styled-components@5` ### Fixed - Gigantic initial image size in Firefox and MS Edge - Click background to close functionality ### Added - Vendor prefixed styles - A resize observer polyfill is now included to support MS Edge ## [1.2.1] - 2020-03-5 ### Added - Added `renderImageOverlay` prop, renders a React component within the image stage, useful for creating UI overlays on top of the current image ## [1.2.0] - 2020-02-14 - Upgrade react-use-gesture v6 -> v7 - Upgrade all deps ## [1.1.7] - 2019-09-23 ### Fixed - Improved panning performance - Tweaked mousewheel swiping threshold - Upgrade to react-use-gesture v6 ## [1.1.4] - 2019-08-27 ### Added - Implement mousewheel paging of images ## [1.1.3] - 2019-08-19 ### Fixed - Prevent vertical dragging from paging images - Switch to @react-spring/web package ## [1.1.2] - 2019-08-17 ### Fixed - Properly dispose wheel event listener ## [1.1.1] - 2019-08-14 ### Fixed - Adjusted "pan out of bounds" threshold ## [1.1.0] - 2019-08-14 ### Added - Implement proper Ctrl + `Mousewheel` and `Trackpad Pinch` zooming ## [1.0.1] - 2019-08-7 Add testing suite and travis-ci config ## [1.0.0] - 2019-08-5 Upgrade deps and release as stable ## [0.0.3] - 2019-08-1 ### Changed - Renamed onClickNext => onNext - Renamed onClickPrev => onPrev ## [0.0.2] - 2019-07-31 Initial Release ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019-present Tim Ellenberger Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # react-spring-lightbox [![npm](https://img.shields.io/npm/v/react-spring-lightbox.svg?color=brightgreen&style=popout-square)](https://www.npmjs.com/package/react-spring-lightbox) [![NPM](https://img.shields.io/npm/l/react-spring-lightbox.svg?color=brightgreen&style=popout-square)](https://github.com/tim-soft/react-spring-lightbox/blob/master/LICENSE) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/react-spring-lightbox.svg?style=popout-square) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=popout-square) [![Travis (.org)](https://img.shields.io/travis/tim-soft/react-spring-lightbox?style=flat-square)](https://travis-ci.org/tim-soft/react-spring-lightbox) React-spring-lightbox is a flexible image gallery lightbox with native-feeling touch gestures and buttery smooth animations.


Docs     Codesandbox

## ✨ Features - :point_up:    `Mousewheel`, swipe or click+drag to page photos - :keyboard:  Keyboard controls Esc - :mouse2:  Ctrl + `Mousewheel` or `Trackpad Pinch` to zoom - :mag_right:  Double/Single-tap or double/single-click to zoom in/out - :ok_hand:    Pinch to zoom - :point_left:  Panning on zoomed-in images - :checkered_flag:  Highly performant spring based animations via [react-spring](https://github.com/react-spring/react-spring) - No external CSS - Implement your own UI - Supports IE 11, Edge, Safari, Chrome, Firefox and Opera - Full typescript support - Supports any `` attribute including `loading` (lazy loading), `srcset` and `aria-*` ## Install ```bash yarn add react-spring-lightbox ``` ## Usage The `images` prop now accepts a list of objects whose properties can be _almost_ any valid React `` prop including `srcset`, `loading` (lazy loading) and `aria-*` attributes. If you use typescript, the exact type can be imported from `import { ImagesListType } from 'react-spring-lightbox';` ```typescript import React, { useState } from 'react'; import Lightbox, { ImagesListType } from 'react-spring-lightbox'; const images: ImagesListType = [ { src: 'https://timellenberger.com/static/blog-content/dark-mode/win10-dark-mode.jpg', loading: 'lazy', alt: 'Windows 10 Dark Mode Setting', }, { src: 'https://timellenberger.com/static/blog-content/dark-mode/macos-dark-mode.png', loading: 'lazy', alt: 'macOS Mojave Dark Mode Setting', }, { src: 'https://timellenberger.com/static/blog-content/dark-mode/android-9-dark-mode.jpg', loading: 'lazy', alt: 'Android 9.0 Dark Mode Setting', }, ]; const CoolLightbox = () => { const [currentImageIndex, setCurrentIndex] = useState(0); const gotoPrevious = () => currentImageIndex > 0 && setCurrentIndex(currentImageIndex - 1); const gotoNext = () => currentImageIndex + 1 < images.length && setCurrentIndex(currentImageIndex + 1); return ( ()} // renderFooter={() => ()} // renderPrevButton={() => ()} // renderNextButton={() => ()} // renderImageOverlay={() => ()} /* Add styling */ // className="cool-class" // style={{ background: "grey" }} /* Handle closing */ // onClose={handleClose} /* Use single or double click to zoom */ // singleClickToZoom /* react-spring config for open/close animation */ // pageTransitionConfig={{ // from: { transform: "scale(0.75)", opacity: 0 }, // enter: { transform: "scale(1)", opacity: 1 }, // leave: { transform: "scale(0.75)", opacity: 0 }, // config: { mass: 1, tension: 320, friction: 32 } // }} /> ); }; export default CoolLightbox; ``` ## Props | Prop | Description | | -------------------- | ------------------------------------------------------------------------------------------------------------------ | | isOpen | Flag that dictates if the lightbox is open or closed | | onClose | Function that closes the Lightbox | | onPrev | Function that changes currentIndex to previous image in images | | onNext | Function that changes currentIndex to next image in images | | currentIndex | Index of image in images array that is currently shown | | renderHeader | A React component that renders above the image pager | | renderFooter | A React component that renders below the image pager | | renderPrevButton | A React component that is used for previous button in image pager | | renderNextButton | A React component that is used for next button in image pager | | renderImageOverlay | A React component that renders within the image stage, useful for creating UI overlays on top of the current image | | singleClickToZoom | Overrides the default behavior of double clicking causing an image zoom to a single click | | images | Array of image objects to be shown in Lightbox | | className | Classes are applied to the root lightbox component | | style | Inline styles are applied to the root lightbox component | | pageTransitionConfig | React-Spring useTransition config for page open/close animation | ## Local Development Clone the repo ```bash git clone https://github.com/tim-soft/react-spring-lightbox.git react-spring-lightbox cd react-spring-lightbox ``` Setup symlinks ```bash yarn link cd example yarn link react-spring-lightbox ``` Run the library in development mode ```bash yarn start ``` Run the example app in development mode ```bash cd example yarn dev ``` Changes to the library code should hot reload in the demo app ## License MIT © [Tim Ellenberger](https://github.com/tim-soft) ================================================ FILE: example/README.md ================================================ # react-spring-lightbox demo app This directory contains a Next.js app useful for testing and developing `react-spring-lightbox` Install dependencies ```bash yarn install ``` Run locally with hot module reloading ```bash yarn dev ``` ================================================ FILE: example/components/GalleryLightbox/components/GridImage.jsx ================================================ import * as React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; /** * A single image element in a masonry style image grid */ const GridImage = ({ index, key, left, onClick, photo, top }) => { const { alt, caption, height, src, width } = photo; return ( onClick(e, { index })} style={{ height, left, top, width }} > {alt}

{caption}

); }; GridImage.propTypes = { containerHeight: PropTypes.number.isRequired, index: PropTypes.number.isRequired, key: PropTypes.string.isRequired, left: PropTypes.number.isRequired, onClick: PropTypes.func.isRequired, photo: PropTypes.shape({ alt: PropTypes.string.isRequired, caption: PropTypes.string.isRequired, height: PropTypes.number.isRequired, src: PropTypes.string.isRequired, width: PropTypes.number.isRequired, }).isRequired, top: PropTypes.number.isRequired, }; export default GridImage; const Caption = styled.div` position: absolute; bottom: 0; width: 100%; background-color: ${({ theme }) => theme.accentColor}; color: ${({ theme }) => theme.pageContentLinkHoverColor}; h4 { text-align: center; margin: 1em 0; } `; const OverlayContainer = styled.div` position: relative; height: 100%; overflow: hidden; `; const ImageContainer = styled.div` display: block; position: absolute; cursor: pointer; border-width: 2px; border-color: transparent; border-style: solid; :hover { border-color: ${({ theme }) => theme.pageContentLinkHoverColor}; } `; const Image = styled.img` width: inherit; height: inherit; position: absolute; `; ================================================ FILE: example/components/GalleryLightbox/components/LightboxArrowButton.jsx ================================================ /* eslint-disable no-shadow */ import * as React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; import { IoIosArrowBack, IoIosArrowForward } from 'react-icons/io'; import { animated, useTransition } from '@react-spring/web'; import ButtonControl from './LightboxButtonControl'; const ArrowButton = ({ className, disabled, onClick, position }) => { const transitions = useTransition(!disabled, { enter: { opacity: 1 }, from: { opacity: 0 }, leave: { opacity: 0 }, }); return transitions( (props, item) => item && ( ), ); }; ArrowButton.propTypes = { disabled: PropTypes.bool, onClick: PropTypes.func.isRequired, position: PropTypes.oneOf(['left', 'right']).isRequired, }; ArrowButton.defaultProps = { disabled: false, }; export default ArrowButton; const StyledAnimatedDiv = styled(animated.div)` z-index: 999; `; const Button = styled(ButtonControl)` position: absolute; top: 0; bottom: 0; left: ${({ position }) => (position === 'left' ? 0 : 'unset')}; right: ${({ position }) => (position === 'right' ? 0 : 'unset')}; `; ================================================ FILE: example/components/GalleryLightbox/components/LightboxButtonControl.jsx ================================================ import styled from 'styled-components'; export default styled.button` z-index: 10; background: none; border-style: none; font-size: 50px; cursor: pointer; padding: 0; margin: 0; color: ${({ theme }) => theme.pageContentFontColor}; transition: color 0.2s linear; :hover { color: ${({ theme }) => theme.pageContentLinkHoverColor}; } :focus { outline: none; color: ${({ theme }) => theme.pageContentLinkHoverColor}; } `; ================================================ FILE: example/components/GalleryLightbox/components/LightboxHeader.jsx ================================================ import * as React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; import { IoIosClose } from 'react-icons/io'; import Color from 'color'; import ButtonControl from './LightboxButtonControl'; const LightboxHeader = ({ currentIndex, galleryTitle, images, onClose }) => ( {galleryTitle} {images[currentIndex].caption} {currentIndex + 1} / {images.length} ); LightboxHeader.propTypes = { currentIndex: PropTypes.number.isRequired, galleryTitle: PropTypes.string.isRequired, images: PropTypes.arrayOf( PropTypes.shape({ alt: PropTypes.string.isRequired, caption: PropTypes.string.isRequired, height: PropTypes.number, src: PropTypes.string.isRequired, width: PropTypes.number, }), ).isRequired, onClose: PropTypes.func.isRequired, }; export default LightboxHeader; const GalleryHeading = styled.h2` margin: 0 0 5px 0; font-weight: normal; `; const GallerySubheading = styled.h4` margin: 0; font-weight: normal; color: ${({ theme }) => theme.pageContentLinkHoverColor}; `; const PageIndicator = styled.span` white-space: nowrap; min-width: 60px; text-align: center; `; const RightSideContainer = styled.div` width: 117px; display: flex; justify-content: space-between; align-items: center; `; const CloseButton = styled(ButtonControl)` height: 100%; display: flex; border-left-style: solid; border-left-width: 3px; border-left-color: ${({ theme }) => theme.headerNavFontColor}; color: inherit; `; const LeftSideDescriptionContainer = styled.div` display: flex; flex-direction: column; justify-content: center; border-left-width: 3px; border-left-color: ${({ theme }) => theme.pageContentLinkHoverColor}; border-left-style: solid; padding: 8px 0 8px 10px; `; const TopHeaderBar = styled.header` z-index: 10; cursor: auto; display: flex; justify-content: space-between; padding: 10px 2px 10px 20px; color: ${({ theme }) => theme.headerNavFontColor}; background-color: ${({ theme }) => Color(theme.pageBackgroundColor).alpha(0.5).hsl().string()}; > * { height: inherit; } `; ================================================ FILE: example/components/GalleryLightbox/index.jsx ================================================ import { FiHeart, FiPrinter, FiShare } from 'react-icons/fi'; import React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; import Color from 'color'; import Gallery from 'react-photo-gallery'; import Lightbox from 'react-spring-lightbox'; import GridImage from './components/GridImage'; import LightboxHeader from './components/LightboxHeader'; import LightboxArrowButton from './components/LightboxArrowButton'; class BlogImageGallery extends React.Component { static propTypes = { galleryTitle: PropTypes.string.isRequired, imageMasonryDirection: PropTypes.oneOf(['column', 'row']), images: PropTypes.arrayOf( PropTypes.shape({ alt: PropTypes.string.isRequired, caption: PropTypes.string.isRequired, height: PropTypes.number, src: PropTypes.string.isRequired, width: PropTypes.number, }), ).isRequired, }; static defaultProps = { imageMasonryDirection: 'column', }; constructor() { super(); this.state = { clientSide: false, currentImageIndex: 0, lightboxIsOpen: false, }; } componentDidMount() { this.setState({ clientSide: true }); } openLightbox = (e, { index }) => { this.setState({ currentImageIndex: index, lightboxIsOpen: true, }); }; closeLightbox = () => { this.setState({ lightboxIsOpen: false, }); }; gotoPrevious = () => { const { currentImageIndex } = this.state; // If the current image isn't the first in the list, go to the previous if (currentImageIndex > 0) { this.setState({ currentImageIndex: currentImageIndex - 1, }); } }; gotoNext = () => { const { images } = this.props; const { currentImageIndex } = this.state; // If the current image isn't the list in the list, go to the next if (currentImageIndex + 1 < images.length) { this.setState({ currentImageIndex: currentImageIndex + 1, }); } }; /** * Sets breakpoints for column width based on containerWidth * * @int containerWidth The current width of the image grid */ columnConfig = (containerWidth) => { let columns = 1; if (containerWidth >= 500) columns = 2; if (containerWidth >= 900) columns = 3; if (containerWidth >= 1500) columns = 4; return columns; }; render() { const { clientSide, currentImageIndex, lightboxIsOpen } = this.state; const { galleryTitle, imageMasonryDirection, images } = this.props; // remove the height and width props for the lightbox images array const listboxImages = [...images].map((image) => { const newImage = { ...image }; delete newImage.height; delete newImage.width; return newImage; }); return ( {clientSide && ( )} ( )} renderImageOverlay={() => (

Create your own UI

)} renderNextButton={({ canNext }) => ( )} renderPrevButton={({ canPrev }) => ( )} singleClickToZoom />
); } } export default BlogImageGallery; const GalleryContainer = styled.div` overflow-y: auto; max-height: calc(100% - 4em); padding: 2em; `; const StyledLightbox = styled(Lightbox)` background: ${({ theme }) => Color(theme.accentColor).alpha(0.95).hsl().string()}; * ::selection { background: ${({ theme }) => theme.pageContentSelectionColor}; } * ::-moz-selection { background: ${({ theme }) => new Color(theme.pageContentSelectionColor).darken(0.57).hex()}; } `; const ImageOverlay = styled.div` position: absolute; top: 0%; right: 0%; border: ${({ theme }) => theme.pageContentSelectionColor} 1px solid; background: rgba(39, 39, 39, 0.5); p { color: ${({ theme }) => theme.pageContentSelectionColor}; text-align: center; font-weight: bold; font-size: 1.2em; margin: 0.5em 0; } svg { border: white 1px solid; fill: ${({ theme }) => theme.pageContentSelectionColor}; margin: 10px; padding: 5px; :hover { border: ${({ theme }) => theme.pageContentSelectionColor} 1px solid; fill: ${({ theme }) => new Color(theme.pageContentSelectionColor).darken(0.57).hex()}; cursor: pointer; } } `; ================================================ FILE: example/components/InlineLightbox/index.jsx ================================================ import * as React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; import Lightbox from 'react-spring-lightbox'; import LightboxArrowButton from '../GalleryLightbox/components/LightboxArrowButton'; const InlineLightbox = ({ images }) => { const [currentImageIndex, setCurrentImageIndex] = React.useState(0); const inlineCarouselElement = React.useRef(); React.useEffect(() => { inlineCarouselElement?.current?.addEventListener('wheel', preventWheel); setCurrentImageIndex(0); }, [inlineCarouselElement, images]); const preventWheel = (e) => { e.preventDefault(); e.stopPropagation(); return false; }; const canPrev = currentImageIndex > 0; const canNext = currentImageIndex + 1 < images.length; const gotoNext = () => { canNext ? setCurrentImageIndex(currentImageIndex + 1) : () => null; }; const gotoPrevious = () => { canPrev ? setCurrentImageIndex(currentImageIndex - 1) : null; }; return ( ( )} renderPrevButton={({ canPrev }) => ( )} singleClickToZoom /> ); }; export default InlineLightbox; InlineLightbox.propTypes = { images: PropTypes.arrayOf( PropTypes.shape({ alt: PropTypes.string.isRequired, caption: PropTypes.string.isRequired, height: PropTypes.number, src: PropTypes.string.isRequired, width: PropTypes.number, }), ).isRequired, }; const Container = styled.div` display: inline-flex; flex-direction: column; width: 100%; height: 384px; overflow: hidden; `; const StyledLightboxArrowButton = styled(LightboxArrowButton)` z-index: 10; button { font-size: 25px; } `; ================================================ FILE: example/next-env.d.ts ================================================ /// /// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. ================================================ FILE: example/next.config.js ================================================ /** @type {import('next').NextConfig} */ const nextConfig = { compiler: { styledComponents: true, }, transpilePackages: ['styled-components', 'react-spring-lightbox'], }; module.exports = nextConfig; ================================================ FILE: example/package.json ================================================ { "name": "react-spring-lightbox-example", "homepage": "https://tim-soft.github.io/react-spring-lightbox", "version": "0.0.0", "license": "MIT", "private": true, "dependencies": { "@react-spring/web": "link:../node_modules/@react-spring/web", "color": "^4.2.3", "lodash.clamp": "^4.0.3", "lodash.merge": "^4.6.2", "next": "13.5.4", "prop-types": "^15.6.2", "react": "link:../node_modules/react", "react-dom": "link:../node_modules/react-dom", "react-icons": "^4.2.0", "react-is": "link:../node_modules/react-is", "react-photo-gallery": "^8.0.0", "react-spring-lightbox": "link:..", "styled-components": "link:../node_modules/styled-components", "styled-normalize": "^8.0.7" }, "scripts": { "dev": "next", "start": "next start", "build": "next build" }, "browserslist": [ ">0.2%", "not dead", "not ie <= 11", "not op_mini all" ] } ================================================ FILE: example/pages/_app.jsx ================================================ import App from 'next/app'; import React from 'react'; import { createGlobalStyle, ThemeProvider } from 'styled-components'; import styledNormalize from 'styled-normalize'; export default class MyApp extends App { render() { const { Component, pageProps } = this.props; return ( <> {/* Adds some basic body styles */} ); } } /** * Adds global styles and normalize.css to the entire app * * http://nicolasgallagher.com/about-normalize-css/ * https://www.styled-components.com/docs/api#createglobalstyle */ const DefaultStyles = createGlobalStyle` ${styledNormalize} body { margin: 0; background: #1D1E1F; font-family: 'Montserrat', sans-serif; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; } `; ================================================ FILE: example/pages/_document.jsx ================================================ import * as React from 'react'; import Document from 'next/document'; import { ServerStyleSheet } from 'styled-components'; export default class MyDocument extends Document { static async getInitialProps(ctx) { const sheet = new ServerStyleSheet(); const originalRenderPage = ctx.renderPage; try { ctx.renderPage = () => originalRenderPage({ enhanceApp: (App) => (props) => sheet.collectStyles(), }); const initialProps = await Document.getInitialProps(ctx); return { ...initialProps, styles: ( <> {initialProps.styles} {sheet.getStyleElement()} ), }; } finally { sheet.seal(); } } } ================================================ FILE: example/pages/index.tsx ================================================ import * as React from 'react'; import styled from 'styled-components'; import GalleryLightbox from '../components/GalleryLightbox'; import InlineLightbox from '../components/InlineLightbox'; const images = [ { alt: 'Windows 10 Dark Mode Setting', caption: 'Windows 10 Dark Mode Setting', height: 2035, src: 'https://timellenberger.com/static/blog-content/dark-mode/win10-dark-mode.jpg', width: 2848, }, { alt: 'macOS Mojave Dark Mode Setting', caption: 'macOS Mojave Dark Mode Setting', height: 1218, src: 'https://timellenberger.com/static/blog-content/dark-mode/macos-dark-mode.png', width: 1200, }, { alt: 'Android 9.0 Dark Mode Setting', caption: 'Android 9.0 Dark Mode Setting', height: 600, src: 'https://timellenberger.com/static/blog-content/dark-mode/android-9-dark-mode.jpg', width: 1280, }, { alt: 'Windows 10 Dark Mode Setting#', caption: 'Windows 10 Dark Mode Setting#', height: 2035, src: 'https://timellenberger.com/static/blog-content/dark-mode/win10-dark-mode.jpg', width: 2848, }, { alt: 'Windows 10 Dark Mode Setting', caption: 'Windows 10 Dark Mode Setting', height: 2035, src: 'https://timellenberger.com/static/blog-content/dark-mode/win10-dark-mode.jpg', width: 2848, }, { alt: 'macOS Mojave Dark Mode Setting', caption: 'macOS Mojave Dark Mode Setting', height: 1218, src: 'https://timellenberger.com/static/blog-content/dark-mode/macos-dark-mode.png', width: 1200, }, { alt: 'Android 9.0 Dark Mode Setting', caption: 'Android 9.0 Dark Mode Setting', height: 600, src: 'https://timellenberger.com/static/blog-content/dark-mode/android-9-dark-mode.jpg', width: 1280, }, { alt: 'Windows 10 Dark Mode Setting#', caption: 'Windows 10 Dark Mode Setting#', height: 2035, src: 'https://timellenberger.com/static/blog-content/dark-mode/win10-dark-mode.jpg', width: 2848, }, { alt: 'Android 9.0 Dark Mode Setting', caption: 'Android 9.0 Dark Mode Setting', height: 600, src: 'https://timellenberger.com/static/blog-content/dark-mode/android-9-dark-mode.jpg', width: 1280, }, { alt: 'Windows 10 Dark Mode Setting#', caption: 'Windows 10 Dark Mode Setting#', height: 2035, src: 'https://timellenberger.com/static/blog-content/dark-mode/win10-dark-mode.jpg', width: 2848, }, { alt: 'macOS Mojave Dark Mode Setting', caption: 'macOS Mojave Dark Mode Setting', height: 1218, src: 'https://timellenberger.com/static/blog-content/dark-mode/macos-dark-mode.png', width: 1200, }, { alt: 'Android 9.0 Dark Mode Setting', caption: 'Android 9.0 Dark Mode Setting', height: 600, src: 'https://timellenberger.com/static/blog-content/dark-mode/android-9-dark-mode.jpg', width: 1280, }, { alt: 'Windows 10 Dark Mode Setting#', caption: 'Windows 10 Dark Mode Setting#', height: 2035, src: 'https://timellenberger.com/static/blog-content/dark-mode/win10-dark-mode.jpg', width: 2848, }, { alt: 'Android 9.0 Dark Mode Setting', caption: 'Android 9.0 Dark Mode Setting', height: 600, src: 'https://timellenberger.com/static/blog-content/dark-mode/android-9-dark-mode.jpg', width: 1280, }, { alt: 'Windows 10 Dark Mode Setting#', caption: 'Windows 10 Dark Mode Setting#', height: 2035, src: 'https://timellenberger.com/static/blog-content/dark-mode/win10-dark-mode.jpg', width: 2848, }, ]; const getRandomImages = (numImages: number) => { const imageArray = [...new Array(numImages)]; const getRandomInt = (min, max) => { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min) + min); //The maximum is exclusive and the minimum is inclusive }; const altCaption = 'picsum photo'; const randomizedArray = imageArray.map((imageObj) => { const imageURL = `https://picsum.photos/id/${getRandomInt( 1, 200, )}/1920/1280`; return { ...imageObj, alt: altCaption, caption: altCaption, src: imageURL, }; }); return randomizedArray; }; const HomePage = () => { const [inlineImages, setInlineImages] = React.useState(getRandomImages(15)); return ( Gallery Lightbox
Inline Lightbox 🎉🎉Inline content🎉🎉 🎉🎉Inline content🎉🎉
); }; export default HomePage; const Container = styled.div` height: 100%; width: 100%; user-select: none; background: #272727; color: #fff; padding: 50px 0; `; const Button = styled.button` border-radius: 10px; background: teal; color: #fff; padding: 16px 10px; margin: 20px auto; cursor: pointer; :hover { background: darkblue; } :active { background: darkseagreen; } `; const GalleryLightboxExample = styled.div` height: 100%; width: 100%; `; const InlineLightboxExampleContainer = styled.div` height: 100%; width: 100%; display: flex; flex-direction: column; `; const InlineLightboxExample = styled.div` width: 100%; display: flex; justify-content: center; `; const OtherInlineContent = styled.div` width: 400px; display: flex; justify-content: center; align-items: center; padding: 10px; background: blueviolet; `; const StyledH2 = styled.h2` text-align: center; `; ================================================ FILE: example/tsconfig.json ================================================ { "compilerOptions": { "allowJs": true, "esModuleInterop": true, "incremental": true, "isolatedModules": true, "jsx": "preserve", "lib": ["dom", "dom.iterable", "esnext"], "module": "esnext", "moduleResolution": "node", "noEmit": true, "resolveJsonModule": true, "skipLibCheck": true, "strict": false }, "exclude": ["node_modules"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"] } ================================================ FILE: jest-setup.ts ================================================ import '@testing-library/jest-dom'; ================================================ FILE: jest.config.js ================================================ /** * Configure Jest as the test runner for @testing-library * * @see https://jestjs.io/docs/en/configuration */ module.exports = { collectCoverage: true, coveragePathIgnorePatterns: ['/node_modules/', '/__tests__/'], preset: 'ts-jest', setupFilesAfterEnv: ['babel-polyfill', '/jest-setup.ts'], testEnvironment: 'jsdom', testMatch: ['**/__tests__/**/*.(spec|test).[jt]s?(x)'], transform: { '^.+\\.(js|jsx)$': 'babel-jest', '^.+\\.(ts|tsx)?$': 'ts-jest', }, }; ================================================ FILE: package.json ================================================ { "name": "react-spring-lightbox", "version": "1.8.0", "description": "A flexible image gallery lightbox with native-feeling touch gestures and buttery smooth animations, built with react-spring.", "author": "Tim Ellenberger ", "license": "MIT", "repository": "tim-soft/react-spring-lightbox", "sideEffects": false, "bugs": { "url": "https://github.com/tim-soft/react-spring-lightbox/issues" }, "homepage": "https://timellenberger.com", "keywords": [ "react", "spring", "lightbox", "modal", "gallery", "touch", "gestures", "images" ], "main": "dist/index.cjs.js", "types": "dist/index.d.ts", "engines": { "node": ">=16", "npm": ">=7" }, "scripts": { "fix": "yarn fix:eslint && yarn fix:prettier", "fix:eslint": "eslint --fix \"**/*.*\"", "fix:prettier": "prettier --write \"**/*.*\"", "lint": "yarn lint:eslint && yarn lint:prettier && yarn lint:ts", "lint:eslint": "eslint \"**/*.*\"", "lint:prettier": "prettier --check \"**/*.*\"", "lint:ts": "npx tsc --noEmit -p .", "test": "jest", "test:watch": "jest --watch", "build": "rollup --config && yarn run build:types", "build:types": "tsc --project tsconfig.buildtypes.json --emitDeclarationOnly", "start": "rollup --config --watch", "prepare": "yarn run build" }, "husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "*.{json,md}": [ "prettier --write", "git add --force" ], "*.{js, jsx}": [ "prettier --write", "eslint --no-ignore --fix", "git add --force" ] }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8", "styled-components": ">=5.X" }, "devDependencies": { "@babel/core": "^7.23.0", "@babel/plugin-transform-class-properties": "^7.18.6", "@babel/plugin-transform-object-rest-spread": "^7.20.7", "@babel/plugin-transform-runtime": "^7.22.15", "@babel/preset-env": "^7.22.20", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-commonjs": "^25.0.5", "@rollup/plugin-node-resolve": "^15.2.2", "@rollup/plugin-terser": "^0.4.4", "@testing-library/jest-dom": "^6.1.3", "@testing-library/react": "^14.0.0", "@types/jest": "^29.5.5", "@types/react": "^18.2.25", "@types/react-dom": "^18.2.10", "@types/styled-components": "5.1.28", "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", "babel-eslint": "10.1.0", "babel-polyfill": "^6.26.0", "cross-env": "^7.0.3", "eslint": "^8.50.0", "eslint-config-prettier": "^9.0.0", "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "^2.28.1", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-sort-destructure-keys": "^1.5.0", "husky": "^4.3.8", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "lint-staged": "^10.5.4", "prettier": "^3.0.3", "react": "^18", "react-dom": "^18", "react-is": "^18", "rollup": "^3.29.4", "rollup-plugin-filesize": "^10.0.0", "rollup-plugin-node-externals": "^6.1.2", "styled-components": "^5.3.5", "ts-jest": "^29.1.1", "tslib": "^2.6.2", "typescript": "^5.2.2" }, "files": [ "dist" ], "dependencies": { "@babel/runtime": "^7.23.1", "@react-spring/web": "^9.7.3", "react-use-gesture": "9.1.3" } } ================================================ FILE: rollup.config.mjs ================================================ import commonjs from '@rollup/plugin-commonjs'; import filesize from 'rollup-plugin-filesize'; import { nodeResolve } from '@rollup/plugin-node-resolve'; import { babel } from '@rollup/plugin-babel'; import terser from '@rollup/plugin-terser'; import { nodeExternals } from 'rollup-plugin-node-externals'; export default { input: './src/index.tsx', output: [ { exports: 'default', file: 'dist/index.cjs.js', format: 'cjs', interop: 'auto', sourcemap: true, }, ], plugins: [ nodeExternals(), nodeResolve(), commonjs({ extensions: ['.js', '.jsx', '.ts', '.tsx'], include: 'node_modules/**', }), babel({ babelHelpers: 'runtime', exclude: 'node_modules/**', extensions: ['.js', '.jsx', '.ts', '.tsx'], }), terser(), filesize(), ], }; ================================================ FILE: src/__tests__/components/SimpleLightbox.tsx ================================================ import React, { useState } from 'react'; import Lightbox, { ImagesListType } from '../../index'; const images: ImagesListType = [ { alt: 'Windows 10 Dark Mode Setting', src: 'https://timellenberger.com/static/blog-content/dark-mode/win10-dark-mode.jpg', }, { alt: 'macOS Mojave Dark Mode Setting', src: 'https://timellenberger.com/static/blog-content/dark-mode/macos-dark-mode.png', }, { alt: 'Android 9.0 Dark Mode Setting', src: 'https://timellenberger.com/static/blog-content/dark-mode/android-9-dark-mode.jpg', }, ]; const SimpleLightbox = ( props: Partial>, ) => { const [currentImageIndex, setCurrentIndex] = useState(0); const gotoPrevious = () => currentImageIndex > 0 && setCurrentIndex(currentImageIndex - 1); const gotoNext = () => currentImageIndex + 1 < images.length && setCurrentIndex(currentImageIndex + 1); return ( null} onNext={gotoNext} onPrev={gotoPrevious} {...props} /> ); }; export default SimpleLightbox; ================================================ FILE: src/__tests__/lightbox.test.tsx ================================================ import React from 'react'; import { render, fireEvent, screen } from '@testing-library/react'; import Lightbox from './components/SimpleLightbox'; describe('Lightbox', () => { describe('CreatePortal', () => { test('creates portal on render', () => { render(); const portalEl = document.body.querySelector('.lightbox-portal'); expect(portalEl).toBeTruthy(); }); }); describe('renderHeader', () => { test('renders custom header', () => { render(
} />, ); const lightboxContainer = screen.getByTestId('lightbox-container'); const lightboxHeader = screen.getByTestId('header'); // Lightbox container should have a header and pager body expect(lightboxContainer.childElementCount).toBe(2); expect(lightboxContainer).toBeInTheDocument(); // Header exists in the lightbox expect(lightboxHeader).toBeInTheDocument(); }); }); describe('renderFooter', () => { test('renders custom footer', () => { render(