├── .babelrc ├── .github ├── contributing.md ├── dependabot.yml ├── issue_template.md └── workflows │ ├── main.yml │ └── pages.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── UPGRADE_GUIDE.md ├── examples ├── captions │ ├── .env │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── App.tsx │ │ ├── images.tsx │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ └── styles.css │ └── tsconfig.json ├── custom-image-component │ ├── .env │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── App.tsx │ │ ├── images.tsx │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ └── styles.css │ └── tsconfig.json ├── custom-overlay │ ├── .env │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── App.tsx │ │ ├── images.ts │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ └── styles.css │ └── tsconfig.json ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages │ ├── _app.tsx │ ├── _meta.json │ ├── examples │ │ ├── _meta.json │ │ ├── captions.mdx │ │ ├── custom-image-component.mdx │ │ ├── custom-overlay.mdx │ │ ├── selection.mdx │ │ ├── with-react-image-lightbox.mdx │ │ └── with-yet-another-react-lightbox.mdx │ └── index.mdx ├── public │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── favicon.png │ └── site.webmanifest ├── selection │ ├── .env │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── App.tsx │ │ ├── images.ts │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ └── styles.css │ └── tsconfig.json ├── theme.config.tsx ├── tsconfig.json ├── with-react-image-lightbox │ ├── .env │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── App.tsx │ │ ├── images.ts │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ └── styles.css │ └── tsconfig.json └── with-yet-another-react-lightbox │ ├── .env │ ├── package-lock.json │ ├── package.json │ ├── public │ └── index.html │ ├── src │ ├── App.tsx │ ├── images.ts │ ├── index.tsx │ ├── react-app-env.d.ts │ └── styles.css │ └── tsconfig.json ├── jest.config.js ├── package-lock.json ├── package.json ├── playground ├── README.md ├── index.html └── index.tsx ├── rollup.config.js ├── setup-jest.js ├── src ├── CheckButton.tsx ├── Gallery.tsx ├── Image.tsx ├── buildLayout.ts ├── index.ts ├── styles.ts ├── types.ts └── useContainerWidth.ts ├── test ├── Gallery.e2e.test.ts ├── Gallery.test.tsx ├── __image_snapshots__ │ ├── gallery-e-2-e-test-ts-gallery-is-visually-correct-after-viewport-resize-1-snap.png │ ├── gallery-e-2-e-test-ts-gallery-is-visually-correct-after-viewport-resize-2-snap.png │ ├── gallery-e-2-e-test-ts-gallery-is-visually-correct-after-viewport-resize-3-snap.png │ ├── gallery-e-2-e-test-ts-gallery-is-visually-correct-after-viewport-resize-4-snap.png │ ├── gallery-e-2-e-test-ts-gallery-is-visually-correct-after-viewport-resize-5-snap.png │ ├── gallery-e-2-e-test-ts-gallery-is-visually-correct-after-viewport-resize-6-snap.png │ ├── gallery-e-2-e-test-ts-gallery-is-visually-correct-after-viewport-resize-7-snap.png │ ├── gallery-e-2-e-test-ts-gallery-is-visually-correct-on-react-16-1-snap.png │ ├── gallery-e-2-e-test-ts-gallery-is-visually-correct-on-react-17-1-snap.png │ ├── gallery-e-2-e-test-ts-gallery-is-visually-correct-on-react-18-1-snap.png │ ├── gallery-e-2-e-test-ts-gallery-is-visually-correct-when-container-width-is-decimal-1-snap.png │ ├── gallery-e-2-e-test-ts-gallery-is-visually-correct-when-images-are-selected-1-snap.png │ ├── gallery-e-2-e-test-ts-gallery-is-visually-correct-when-images-are-transparent-1-snap.png │ ├── gallery-e-2-e-test-ts-gallery-is-visually-correct-when-images-have-tags-1-snap.png │ ├── gallery-e-2-e-test-ts-gallery-is-visually-correct-when-images-have-tags-and-tag-style-prop-passed-1-snap.png │ ├── gallery-e-2-e-test-ts-gallery-is-visually-correct-when-margin-is-10-1-snap.png │ ├── gallery-e-2-e-test-ts-gallery-is-visually-correct-when-max-rows-is-2-1-snap.png │ ├── gallery-e-2-e-test-ts-gallery-is-visually-correct-when-nano-prop-passed-1-snap.png │ └── gallery-e-2-e-test-ts-gallery-is-visually-correct-when-row-height-is-100-1-snap.png ├── buildLayout.test.ts ├── images.ts └── styles.test.ts └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-typescript", 4 | "@babel/preset-env", 5 | ["@babel/preset-react", { 6 | "runtime": "automatic" 7 | }] 8 | ], 9 | "plugins": [] 10 | } -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | All contributions to [react-grid-gallery](https://github.com/benhowell/react-grid-gallery) are very welcome. Feature requests, issue reports and pull requests are greatly appreciated. 4 | 5 | ## Issue reports 6 | Unless you're making a clear and simple feature request, please use the template provided. Ignoring the template may cause extra work for maintainers and therefore could delay any action required. 7 | 8 | ## Feature requests 9 | Please start the title of your issue report with "Feature request:". If any of the template headings are not relevant to the request then please remove them before submitting the request. 10 | 11 | ## Pull requests 12 | Pull requests of all kinds are welcome. To ensure a smooth integration process, please follow the guidelines below: 13 | * Before making any radical changes, new features or major re-writes, please open an issue request first to discuss the changes with the maintainers. 14 | * Please ensure all examples in the `example` directory work correctly with your new changes. 15 | * If adding new functionality, please include a demo in the `examples` directory. 16 | * Only commit source, example and README files with your pull request. If your changes require extra libs/dependencies then please discuss this first with the maintainers by creating an issue request. 17 | * New functionality requires documentation be added to the README file. 18 | * Please follow the coding style of the project. 19 | 20 | 21 | Thanks for taking an interest in [react-grid-gallery](https://github.com/benhowell/react-grid-gallery). 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | versioning-strategy: increase 6 | schedule: 7 | interval: "monthly" -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Expected behaviour 2 | 3 | ### Actual behaviour 4 | 5 | ### Steps to reproduce behaviour 6 | 7 | ### Operating system 8 | 9 | ### Browser and version 10 | 11 | ### Hardware 12 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build-and-test: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: actions/setup-node@v2 10 | with: 11 | node-version: 16 12 | - run: npm ci 13 | - run: npm run build 14 | - run: npm test 15 | env: 16 | FORCE_COLOR: true 17 | - uses: actions/upload-artifact@v3 18 | if: failure() 19 | with: 20 | name: image_snapshots_diff_output 21 | path: test/__image_snapshots__/__diff_output__/ -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy examples to Pages 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | workflow_dispatch: 7 | 8 | 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | concurrency: 15 | group: "pages" 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | deploy: 20 | environment: 21 | name: github-pages 22 | url: ${{ steps.deployment.outputs.page_url }} 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v3 27 | - uses: actions/setup-node@v2 28 | with: 29 | node-version: 16 30 | - name: Install lib dependencies 31 | run: npm ci 32 | - name: Install demo site dependencies 33 | working-directory: ./examples 34 | run: npm ci 35 | - name: Install examples dependencies 36 | working-directory: ./examples 37 | run: npm run install-all 38 | - name: Build 39 | working-directory: ./examples 40 | run: npm run build 41 | - name: Setup Pages 42 | uses: actions/configure-pages@v2 43 | - name: Upload artifact 44 | uses: actions/upload-pages-artifact@v1 45 | with: 46 | path: './examples/out' 47 | - name: Deploy to GitHub Pages 48 | id: deployment 49 | uses: actions/deploy-pages@v1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | require.js 3 | *.bundle.js 4 | examples/dist 5 | dist/ 6 | lib/ 7 | .publish 8 | .DS_Store 9 | .idea 10 | .cache 11 | .next -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | The most recent changelog is available on the [releases page](https://github.com/benhowell/react-grid-gallery/releases). 4 | 5 | ### v0.5.6 / 2022-08-23 6 | 7 | * Fixed grid breakdown when container width is decimal [#170](https://github.com/benhowell/react-grid-gallery/pull/170), closes [#84](https://github.com/benhowell/react-grid-gallery/issues/84) 8 | * TypeScript declaration file added [#173](https://github.com/benhowell/react-grid-gallery/pull/173), closes [#145](https://github.com/benhowell/react-grid-gallery/issues/145) 9 | * Added defaultContainerWidth option for SSR rendering support [#175](https://github.com/benhowell/react-grid-gallery/pull/175) 10 | 11 | ### v0.5.5 / 2019-06-20 12 | 13 | * Added new lightBoxProps option to assign any prop directly to lightbox [PR #121](https://github.com/benhowell/react-grid-gallery/pull/121). Thanks [jimishf](https://github.com/JimishF). 14 | 15 | 16 | ### v0.5.4 / 2019-03-10 17 | 18 | * CSS class names are prefixed with ReactGridGallery_ 19 | * Add way to inject a custom thumbnail image component (for lazy-loading) [PR 104](https://github.com/benhowell/react-grid-gallery/pull/104). Thanks [pxpeterxu](https://github.com/pxpeterxu). 20 | * Fix crash when this.props.images.length - 1 < this.state.currentImage [PR #111](https://github.com/benhowell/react-grid-gallery/pull/111). Thanks [lryta](https://github.com/lryta). 21 | 22 | 23 | ### v0.5.3 / 2018-09-30 24 | 25 | * Added `nano` prop and functionality for base64 4x4 image placeholders [PR 101](https://github.com/benhowell/react-grid-gallery/pull/101). Thanks [Vadimuz](https://github.com/vadimuz). 26 | 27 | ### v0.5.2 / 2018-09-15 28 | 29 | * Added currentImageWillChange [PR 97](https://github.com/benhowell/react-grid-gallery/pull/97). Function to execute before lightbox image change. Useful for tracking current image shown in lightbox. Thanks [Approximator](https://github.com/approximator). 30 | 31 | 32 | ### v0.5.1 / 2018-06-29 33 | 34 | * Moved prop-types dependency from dev dependencies to dependencies 35 | 36 | 37 | ### v0.5.0 / 2018-06-26 38 | 39 | * Bumped [react-images](https://github.com/jossmac/react-images) to 0.5.16 to address [issue #83](https://github.com/benhowell/react-grid-gallery/issues/83). See https://github.com/jossmac/react-images/pull/172 for details. 40 | 41 | 42 | ### v0.4.11 / 2018-04-29 43 | 44 | * Fixed bug: propagate preloadNextImage to Lightbox [PR 78](https://github.com/benhowell/react-grid-gallery/pull/78). Thanks [ScottMRafferty](https://github.com/ScottMRafferty). 45 | 46 | 47 | ### v0.4.10 / 2018-04-29 48 | 49 | * Add contentWindow check [PR 77](https://github.com/benhowell/react-grid-gallery/pull/77). Thanks [forforf](https://github.com/forforf). 50 | 51 | 52 | ### v0.4.9 / 2018-04-27 53 | 54 | * Added optional alt tag to image props (defaults to empty string). 55 | 56 | 57 | ### v0.4.8 / 2018-01-20 58 | 59 | * Added image rotation/transformation functionality based upon EXIF orientation passed in the image `orientation` prop [PR 67](https://github.com/benhowell/react-grid-gallery/pull/67). Thanks [mis94](https://github.com/mis94). 60 | 61 | 62 | ### v0.4.7 / 2017-11-20 63 | 64 | * Added className to custom overlay. 65 | 66 | 67 | ### v0.4.6 / 2017-10-02 68 | 69 | * Added vendor specific prefixes to userSelect styling. 70 | 71 | 72 | ### v0.4.5 / 2017-10-02 73 | 74 | * Added ability to select thumbnailCaption text [PR 43](https://github.com/benhowell/react-grid-gallery/pull/43). Thanks [jakub-tucek](https://github.com/jakub-tucek). 75 | 76 | 77 | ### v0.4.4 / 2017-09-29 78 | 79 | * Added optional thumbnailCaption functionality [PR 42](https://github.com/benhowell/react-grid-gallery/pull/42). Thanks [jakub-tucek](https://github.com/jakub-tucek). 80 | 81 | * Updated acknowledgements. 82 | 83 | * Updated documentation. 84 | 85 | 86 | ### v0.4.3 / 2017-09-15 87 | 88 | * Fixed resize event not triggering on scroll bar presence change [PR 40](https://github.com/benhowell/react-grid-gallery/pull/40). Thanks [SimeonC](https://github.com/SimeonC). 89 | 90 | * Updated acknowledgements. 91 | 92 | 93 | ### v0.4.2 / 2017-07-23 94 | 95 | * Added optional `id` prop for the id attribute of the `` tag. 96 | 97 | * Added className attribute for the `` tag. 98 | 99 | * Updated documentation. 100 | 101 | 102 | ### v0.4.1 / 2017-07-20 103 | 104 | * Fixed `maxRows` not updating bug [PR 35](https://github.com/benhowell/react-grid-gallery/pull/35). Thanks [SimeonC](https://github.com/SimeonC). 105 | 106 | * Updated documentation. 107 | 108 | 109 | ### v0.4.0 / 2017-06-29 110 | 111 | * Added optional `tileViewportStyle` prop as a function to determine style of tile viewport. This function leverages [Function.prototype.call()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call). 112 | 113 | * Added optional `thumbnailStyle` prop as a function to determine style of each gallery thumbnail. This function leverages [Function.prototype.call()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call). 114 | 115 | * Refactored implementation of `onSelectImage` prop. This function leverages [Function.prototype.call()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call). 116 | 117 | * Refactored implementation of `onClickThumbnail` prop. This function leverages [Function.prototype.call()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call). 118 | 119 | * Refactored implementation of `lightboxWillOpen` prop. This function leverages [Function.prototype.call()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call). 120 | 121 | * Refactored implementation of `lightboxWillClose` prop. This function leverages [Function.prototype.call()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call). 122 | 123 | 124 | ### v0.3.7 / 2017-06-26 125 | 126 | * Added optional `tagStyle` prop to style thumbnail tags. 127 | 128 | 129 | ### v0.3.6 / 2017-06-07 130 | 131 | * Added optional lightbox prop `showLightboxThumbnails` to display thumbnails beneath the Lightbox image. 132 | 133 | * Added optional lightbox prop `onClickLightboxThumbnail` as a fn to execute when lightbox thumbnail clicked. Overrides internal function: gotoImage. 134 | 135 | 136 | ### v0.3.5 / 2017-06-04 137 | 138 | * Refactored for react 16 (moved from PropTypes to prop-types package). 139 | 140 | * Bumped [react-images](https://github.com/jossmac/react-images) to 0.5.4 which has been refactored for react 16. 141 | 142 | * Added `theme` pass-though prop [PR 27](https://github.com/benhowell/react-grid-gallery/pull/27). Thanks [danalloway](https://github.com/danalloway). 143 | 144 | * Updated acknowledgements. 145 | 146 | 147 | ### v0.3.4 / 2017-05-05 148 | 149 | * Bumped to react 15.5.4 150 | 151 | * Refactored to use new prop-types package (React.PropTyes deprecated). 152 | 153 | 154 | ### v0.3.3 / 2017-04-22 155 | 156 | * Added `customOverlay` functionality [PR 22](https://github.com/benhowell/react-grid-gallery/pull/22). Thanks [ValYouW](https://github.com/ValYouW). 157 | 158 | * Added demo to project page for `customOverlay`. 159 | 160 | * Updated documentation 161 | 162 | * Updated acknowledgements 163 | 164 | 165 | ### v0.3.2 / 2017-04-07 166 | 167 | * Added `maxRows` functionality [issue #21](https://github.com/benhowell/react-grid-gallery/issues/21). 168 | 169 | 170 | ### v0.3.1 / 2017-04-04 171 | 172 | * Added `lightboxWillOpen` and `lightBoxWillClose` functionality [PR 20](https://github.com/benhowell/react-grid-gallery/pull/20). Thanks [ValYouW](https://github.com/ValYouW). 173 | 174 | * Updated documentation for onClickThumbnail fn [issue #19](https://github.com/benhowell/react-grid-gallery/issues/19) 175 | 176 | * Updated acknowledgements 177 | 178 | 179 | ### v0.3.0 / 2017-01-14 180 | 181 | * Fixed bug where lightboxWidth does not exceed 1024px 182 | 183 | * Bumped [react-images](https://github.com/jossmac/react-images) to 0.5.2 184 | 185 | 186 | ### v0.2.10 / 2017-01-13 187 | 188 | * Fixed bug in passing lightboxWidth prop 189 | 190 | ### v0.2.9 / 2016-12-21 191 | 192 | * Added prop to set maximum width of lightbox. Defaults to 1024px. 193 | 194 | 195 | ### v0.2.8 / 2016-12-10 196 | 197 | * Updated documentation including correction of `onSelectImage` prop documentation. 198 | 199 | * More descriptive package keywords. 200 | 201 | * Grammatical tweaks. 202 | 203 | 204 | ### v0.2.7 / 2016-11-04 205 | 206 | ### Breaking Changes 207 | 208 | * Consistent naming scheme implemented both internally and externally. External breaking change to the `onImageSelected` prop which has been renamed `onSelectImage`. All internal instances of `Func` refactored to `Fn`. `handleClickImage` refactored to `onClickImage`. `handleResize` refactored to `onResize`. 209 | 210 | The following changes in v0.2.7 allow react-grid-gallery to be used in an (optionally) stateless way. 211 | 212 | * Added optional prop `onClickImage`. This prop takes a function and is triggered when a lightbox displayed image is clicked. Supplying this prop will override the default `onClickImage` function. 213 | 214 | * Added optional prop `onClickPrev`. This prop takes a function and is triggered when the left arrow in lightbox is clicked. Supplying this prop will override the default `onClickPrev` function. 215 | 216 | * Added optional prop `onClickNext`. This prop takes a function and is triggered when the right arrow in lightbox is clicked. Supplying this prop will override the default `onClickNext` function. 217 | 218 | * Added explicit `closeLightbox` function to the lightbox `onClose` prop. 219 | 220 | 221 | ### v0.2.6 / 2016-10-25 222 | 223 | * Added acknowledgements to docs. 224 | 225 | * Fixed [unitless style warning](https://github.com/benhowell/react-grid-gallery/pull/9). Thanks @szromek. 226 | 227 | ### v0.2.5 / 2016-09-26 228 | 229 | * Added image tagging functionality. Optional `tags` prop takes an array of objects containing tag attributes. `value` prop is the text shown on the tag and `title` prop is the text shown when hovering over the tag. e.g. `tags: [{value: "Ocean", title: "Ocean"}, {value: "People", title: "People"}]` 230 | 231 | ### v0.2.4 / 2016-09-17 232 | 233 | * `onImageSelected` prop function now takes two optional args, index (index of selected image in images array) and image (the selected image object). 234 | 235 | ### v0.2.3 / 2016-09-16 236 | 237 | ### Breaking changes 238 | 239 | * Image selection state now handled within image object by optional boolean prop `isSelected`. This greatly reduces complexity both within and outside the component as the image itself carries it's selected state. Therefore `selectedImages` prop has been removed. 240 | 241 | * `onSelectedImagesChange` prop removed due to the changes outlined above. 242 | 243 | * Optional `onImageSelected` prop added. This prop takes a function and an optional image object as a parameter. 244 | 245 | * `isSelected` removed as first class prop on Image (now a prop on the image item passed in) 246 | 247 | * Image `onToggleSelected` renamed to `onImageSelected`. 248 | 249 | ### v0.2.2 / 2016-09-11 250 | 251 | * Fixes [bug](https://github.com/benhowell/react-grid-gallery/issues/8) on small edge case whereby duplicate images causes an error (two children cannot have the same key) and subsequently only the first of any repeated image src can be rendered. 252 | 253 | ### v0.2.1 / 2016-09-11 254 | 255 | * Fixes [Bug](https://github.com/benhowell/react-grid-gallery/pull/7) where updating an image caused wrong aspect due to thumb not resizing. Bug caused by using array index as react key rather than something unique to the image. Thanks to [cust0dian](https://github.com/cust0dian) for the [pull request](https://github.com/benhowell/react-grid-gallery/pull/7) which fixes this issue by assigning src attribute as key. 256 | 257 | * Fixes [bug](https://github.com/benhowell/react-grid-gallery/pull/6) where only thumbnails are updated when images props changes, meaning re-render doesn't happen until window is resized. Thanks again to [cust0dian](https://github.com/cust0dian) for the [pull request](https://github.com/benhowell/react-grid-gallery/pull/6) which fixes this issue. 258 | 259 | ### v0.2.0 / 2016-09-03 260 | 261 | * Construction of thumbnail images and image rows removed from render. Thumbnails and rows now only rebuilt when container size changes. 262 | 263 | * `selectedImages` state now set via props change. 264 | 265 | * `onSelectedImagesChange` callback now called directly from `onToggleSelected`. Previously, a combination of setting `selectedImages` state and triggering `onSelectedImagesChange` inside `componentWillUpdate` caused a double render. 266 | 267 | * Internal image access now via state instead of props. 268 | 269 | * Thumbnail generation now atomic function rather than whole array at once. 270 | 271 | * * * 272 | 273 | ### v0.1.14 / 2016-08-22 274 | 275 | * `selectedImages` state set on `componentWillReceiveProps` allowing selections from outside component to trigger state update. 276 | 277 | ### v0.1.13 / 2016-08-22 278 | 279 | * Replaced legacy `ref` string with `ref` callback. Fixes multiple react owner issue when using [react-grid-gallery](https://github.com/benhowell/react-grid-gallery) inside a [reagent](https://github.com/reagent-project/reagent) project :) 280 | 281 | ### v0.1.12 / 2016-08-22 282 | 283 | * Replaced `ReactDOM.findDOMNode(this)` with ref, removed react-dom deps 284 | * Added conditional to ensure image onClick not fired when no function specified 285 | * Moved CheckButton styling (color, hoverColor, selectedColor) to props 286 | 287 | ### v0.1.11 / 2016-08-21 288 | 289 | * Fixed react-dom typo 290 | 291 | ### v0.1.10 / 2016-08-21 292 | 293 | * Added option to allow disabling of lightbox image display. `enableLightbox` (PropType.bool, default `true`) 294 | 295 | * Added option to allow passing in of function to execute on thumbnail click. `onClickThumbnail` (PropType.func, default `openLightbox`) 296 | 297 | ### v0.1.9 / 2016-08-19 298 | 299 | * Removed darkening effect on thumbnail hover when `enableImageSelection: false` 300 | 301 | ### v0.1.8 / 2016-08-17 302 | 303 | * Handful of code samples and demos added to project page. 304 | * PropType bugs fixed on Gallery and Image 305 | 306 | ### v0.1.7 / 2016-08-16 307 | 308 | * Gulp task ensenble to clean/build/deploy lib, web (gh-pages) and hacked up cljs js lib 309 | * Project page with examples, docs etc. 310 | * Updated options documentation 311 | 312 | ### v0.1.6 / 2016-08-15 313 | 314 | * Bumped [react-images](https://github.com/jossmac/react-images/) to v0.4.11 315 | * Enabled preloadNextImage option from [react-images](https://github.com/jossmac/react-images/) 316 | 317 | ### v0.1.5 / 2016-08-13 318 | 319 | * Removed commentary and dead code 320 | * Replaced simple functions with anonymous inline functions 321 | 322 | ### v0.1.4 / 2016-08-13 323 | 324 | * Added support for disabling image selection (optional) 325 | * Updated options documentation 326 | 327 | 328 | ### v0.1.3 / 2016-08-13 329 | 330 | * Added support for disabling image selection (optional) 331 | * Updated options documentation 332 | 333 | 334 | ### v0.1.2 / 2016-08-13 335 | 336 | * Added support for onSelectedImagesChange function (optional) 337 | * Updated options documentation 338 | 339 | 340 | ### v0.1.1 / 2016-08-11 341 | 342 | * Added support for all functional lightbox options 343 | * Updated README with options documentation 344 | 345 | ### v0.1.0 / 2016-08-11 346 | 347 | * Simplified thumbnail viewport 348 | * Fixed aspect bug on shrinkage effect on thumbnail selection 349 | 350 | * * * 351 | 352 | ### v0.0.4 / 2016-08-10 353 | 354 | * Shrinkage effect on thumbnail selection 355 | 356 | ### v0.0.3 / 2016-08-09 357 | 358 | * Darkening effect on thumbnail hover (increases visibility of check button) 359 | * Pointer cursor on thumbnail hover 360 | 361 | 362 | ### v0.0.2 / 2016-08-08 363 | 364 | * Full lightbox functionality provided by [react-images](https://github.com/jossmac/react-images/) by [@jossmac](https://github.com/jossmac) 365 | * Auto scaled, clipped and justified images to fit rowHeight prop 366 | * Image selection and gallery level reference to list of selected images 367 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Be nice. 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2018 Ben Howell 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Grid Gallery 2 | 3 | Justified image gallery component for [React](http://facebook.github.io/react/) inspired by [Google Photos](https://photos.google.com/). 4 | 5 | ### :tada: v1.0.0 is out! 6 | 7 | There are breaking changes with v0.5.x, check out the [migration guide](https://github.com/benhowell/react-grid-gallery/blob/master/UPGRADE_GUIDE.md) to learn more. Documentation for v0.5.x is [here](https://github.com/benhowell/react-grid-gallery/tree/v0.5.6). 8 | 9 | ## Live Demo & Examples 10 | 11 | https://benhowell.github.io/react-grid-gallery/ 12 | * [Image Selection](https://benhowell.github.io/react-grid-gallery/examples/selection) 13 | * [Custom Overlay](https://benhowell.github.io/react-grid-gallery/examples/custom-overlay) 14 | * [Thumbnail Captions](https://benhowell.github.io/react-grid-gallery/examples/captions) 15 | * [Custom Image Component](https://benhowell.github.io/react-grid-gallery/examples/custom-image-component) 16 | * [Lightbox integration `react-image-lightbox`](https://benhowell.github.io/react-grid-gallery/examples/with-react-image-lightbox) 17 | * [Lightbox integration `yet-another-react-lightbox`](https://benhowell.github.io/react-grid-gallery/examples/with-yet-another-react-lightbox) 18 | 19 | 20 | ## Installation 21 | 22 | Using [npm](https://www.npmjs.com/): 23 | 24 | ```shell 25 | npm install --save react-grid-gallery 26 | ``` 27 | 28 | ## Quick Start 29 | 30 | ```jsx 31 | import { Gallery } from "react-grid-gallery"; 32 | 33 | const images = [ 34 | { 35 | src: "https://c2.staticflickr.com/9/8817/28973449265_07e3aa5d2e_b.jpg", 36 | width: 320, 37 | height: 174, 38 | isSelected: true, 39 | caption: "After Rain (Jeshu John - designerspics.com)", 40 | }, 41 | { 42 | src: "https://c2.staticflickr.com/9/8356/28897120681_3b2c0f43e0_b.jpg", 43 | width: 320, 44 | height: 212, 45 | tags: [ 46 | { value: "Ocean", title: "Ocean" }, 47 | { value: "People", title: "People" }, 48 | ], 49 | alt: "Boats (Jeshu John - designerspics.com)", 50 | }, 51 | { 52 | src: "https://c4.staticflickr.com/9/8887/28897124891_98c4fdd82b_b.jpg", 53 | width: 320, 54 | height: 212, 55 | }, 56 | ]; 57 | 58 | 59 | ``` 60 | 61 | ## Image Options 62 | 63 | | Property | Type | Description | 64 | |:-----------------|:------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 65 | | src | string | Required. A string referring to any valid image resource (file, url, etc). | 66 | | width | number | Required. Width of the image. | 67 | | height | number | Required. Height of the image. | 68 | | nano | string:base64 | Optional. Thumbnail Base64 image will be injected to background under the main image. This provides a base64, 4x4 generated image whilst the image is being loaded. | 69 | | alt | string | Optional. Image alt attribute. | 70 | | tags | array | Optional. An array of objects containing tag attributes (value, title and key if value is element). e.g. `{value: "foo", title: "bar"}` or `{value: {tag.name}, title: tag.title, key: tag.key}` | 71 | | isSelected | bool | Optional. The selected state of the image. | 72 | | caption | string | ReactNode | Optional. Image caption. | 73 | | customOverlay | element | Optional. A custom element to be rendered as a thumbnail overlay on hover. | 74 | | thumbnailCaption | string | ReactNode | Optional. A thumbnail caption shown below thumbnail. | 75 | | orientation | number | Optional. Orientation of the image. Many newer digital cameras (both dSLR and Point & Shoot digicams) have a built-in orientation sensor. The output of this sensor is used to set the EXIF orientation flag in the image file's metatdata to reflect the positioning of the camera with respect to the ground (See [EXIF Orientation Page](http://jpegclub.org/exif_orientation.html) for more info). | 76 | 77 | ## Gallery Options 78 | 79 | | Property | Type | Description | 80 | |:------------------------|:--------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 81 | | images | array | Required. An array of objects containing image properties (see Image Options above). | 82 | | id | string | Optional, default `ReactGridGallery`. `id` attribute for `` tag. This prop may be useful for those who wish to discriminate between multiple galleries. | 83 | | enableImageSelection | bool | Optional, default `true`. Allow images to be selectable. Setting this option to `false` whilst supplying images with `isSelected: true` will result in those images being permanently selected. | 84 | | onSelect | func | Optional. Function to execute when an image is selected. Optional args: `index` (index of selected image in images array), `image` (the selected image), `event`. This function is only executable when `enableImageSelection: true`. | 85 | | rowHeight | number | Optional, default `180`. The height of each row in the gallery. | 86 | | maxRows | number | Optional. The maximum number of rows to show in the gallery. | 87 | | margin | number | Optional, default `2`. The margin around each image in the gallery. | 88 | | onClick | func | Optional. Function to execute when gallery image clicked. Optional args: `index` (index of selected image in images array), `image` (the clicked image), event (the click event). | 89 | | tagStyle | func | CSSProperties | Optional. Style or function that returns style to pass to tag elements. Optional args: `item` (the image item in `images`). Overrides internal tag style. | 90 | | tileViewportStyle | func | CSSProperties | Optional. Style or function to style the image tile viewport. Optional args: `item` (the image item in `images`). Overrides internal tileViewportStyle function. | 91 | | thumbnailStyle | func | CSSProperties | Optional. Style or function to style the image thumbnail. Optional args: `item` (the image item in `images`). Overrides internal thumbnailStyle function. | 92 | | thumbnailImageComponent | React component | Optional. Substitute in a React component that would get passed `imageProps` (the props that would have been passed to the `` tag) and `item` (the original item in `images`) to be used to render thumbnails; useful for lazy loading. | 93 | | defaultContainerWidth | number | Optional. Set default width for the container. This option is useful during server-side rendering when we want to generate an initial markup before we can detect the actual container width. | 94 | 95 | 96 | ### General Notes 97 | 98 | * [react-grid-gallery](https://github.com/benhowell/react-grid-gallery) is built for modern browsers and therefore IE support is limited to IE 11 and newer. 99 | 100 | * As the inspiration for this component comes from [Google Photos](https://photos.google.com/), very small thumbnails may not be the most aesthetically pleasing due to the border size applied when selected. A sensible rowHeight default of 180px has been chosen, but rowHeights down to 100px are still reasonable. 101 | 102 | * Gallery width is determined by the containing element. Therefore your containing element must have a width (%, em, px, whatever) **_before_** the gallery is loaded! 103 | 104 | * If you don't know your `width` and `height` values, you can find these out using any number of [javascript hacks](http://stackoverflow.com/a/1944298), bearing in mind the load penalty associated with these methods. 105 | 106 | 107 | ### Contributing 108 | All contributions to [react-grid-gallery](https://github.com/benhowell/react-grid-gallery) are very welcome. Feature requests, issue reports and pull requests are greatly appreciated. Please follow the [contribution guidelines](https://github.com/benhowell/react-grid-gallery/blob/master/.github/contributing.md) 109 | 110 | 111 | ### License 112 | React Grid Gallery is free to use for personal and commercial projects under the [MIT License](https://github.com/benhowell/react-grid-gallery/blob/master/LICENSE). Attribution is not required, but appreciated. 113 | 114 | 115 | ### Acknowledgements 116 | * [itoldya](https://github.com/itoldya) for the large overhaul of the code base to bring the library to its v1 release. 117 | 118 | * Visual design inspired by [Google Photos](https://photos.google.com/). 119 | 120 | * Thumbnail viewport implementation inspired by [GPlusGallery](http://fmaul.de/gallery-grid-example/) by Florian Maul. 121 | 122 | * Backend lightbox functionality via [React Images](https://github.com/jossmac/react-images) by [jossmac](https://github.com/jossmac). 123 | 124 | * The following gallery functions were obtained from the [React Images example](https://github.com/jossmac/react-images/blob/b85bd83ae651d0fd373bb495ac88670ee4dfadab/examples/src/components/Gallery.js) demo: closeLightbox, gotoNext, gotoPrevious, handleClickImage, openLightbox. 125 | 126 | * [cust0dian](https://github.com/cust0dian) for critical bug fixes in [PR 6](https://github.com/benhowell/react-grid-gallery/pull/6) and [PR 7](https://github.com/benhowell/react-grid-gallery/pull/7). 127 | 128 | * [ValYouW](https://github.com/ValYouW) for lightboxWillOpen and lightBoxWillClose functionality [PR 20](https://github.com/benhowell/react-grid-gallery/pull/20) and customOverlay option: [PR 22](https://github.com/benhowell/react-grid-gallery/pull/22). 129 | 130 | * [danalloway](https://github.com/danalloway) for theme pass-through prop [PR 27](https://github.com/benhowell/react-grid-gallery/pull/27) 131 | 132 | * [SimeonC](https://github.com/SimeonC) for _update thumbnails when maxRows changes_ [PR 35](https://github.com/benhowell/react-grid-gallery/pull/35) and _resize on scrollbar presence change_ [PR 40](https://github.com/benhowell/react-grid-gallery/pull/40) 133 | 134 | * [jakub-tucek](https://github.com/jakub-tucek) for thumbnailCaption functionality [PR 42](https://github.com/benhowell/react-grid-gallery/pull/42) 135 | 136 | * [mis94](https://github.com/mis94) for EXIF image rotation functionality [PR 67](https://github.com/benhowell/react-grid-gallery/pull/67) 137 | 138 | * [forforf](https://github.com/forforf) for contentWindow check [PR 77](https://github.com/benhowell/react-grid-gallery/pull/77) 139 | 140 | * [ScottMRafferty](https://github.com/ScottMRafferty) for preloadNextImage not propagating to Lightbox fix [PR 78](https://github.com/benhowell/react-grid-gallery/pull/78) 141 | 142 | * [Approximator](https://github.com/approximator) for currentImageWillChange (Function to execute before lightbox image change) [PR 97](https://github.com/benhowell/react-grid-gallery/pull/97). 143 | 144 | * [Vadimuz](https://github.com/vadimuz) for nano image props and functionality [PR 101](https://github.com/benhowell/react-grid-gallery/pull/101). 145 | 146 | * [pxpeterxu](https://github.com/pxpeterxu) for adding functionality to inject a custom thumbnail image component (for lazy-loading) [PR 104](https://github.com/benhowell/react-grid-gallery/pull/104). 147 | 148 | * [lryta](https://github.com/lryta) for fixing crash when this.props.images.length - 1 < this.state.currentImage [PR #111](https://github.com/benhowell/react-grid-gallery/pull/111). 149 | 150 | * [jimishf](https://github.com/JimishF) for lightBoxProps option to assign any prop directly to lightbox [PR #121](https://github.com/benhowell/react-grid-gallery/pull/121). 151 | 152 | * [kym6464](https://github.com/kym6464) for replacing deprecated defaultProps and for clearing of rollup cache on build [PR #298](https://github.com/benhowell/react-grid-gallery/pull/298) 153 | 154 | 155 | * Demo stock photos: 156 | * [Jeshu John - designerspics.com](https://designerspics.com) 157 | * [Gratisography](https://gratisography.com) 158 | * [Tom Eversley - isorepublic.com](https://isorepublic.com) 159 | * [Jan Vasek - jeshoots.com](https://unsplash.com/) 160 | * [moveast.me](https://moveast.me) 161 | * [贝莉儿 NG. - unsplash.com](https://unsplash.com/) 162 | * [Matthew Wiebe. - unsplash.com](https://unsplash.com/) 163 | -------------------------------------------------------------------------------- /UPGRADE_GUIDE.md: -------------------------------------------------------------------------------- 1 | # From v0.5.x 2 | 3 | The biggest change from v0.5.x to 1.x.x is that now this library has no lightbox functionality. 4 | Read [this discussion](https://github.com/benhowell/react-grid-gallery/discussions/179) to learn more about the motivation for that decision. 5 | So if you need lightbox integration please check out our [examples](https://benhowell.github.io/react-grid-gallery/). 6 | 7 | ## API changes 8 | Also, we made API polishing and renamed some props, and changed event handler signatures. 9 | 10 | ### Gallery Options changes 11 | - `onSelectImage` renamed to `onSelect` 12 | - `onClickThumbnail` renamed to `onClick` 13 | 14 | Both event handlers now receive the same arguments: 15 | ```ts 16 | (index: number, item: Image, event: MouseEvent) => void 17 | ``` 18 | 19 | Styling props such as `thumbnailStyle`, `tagStyle`, `tileViewportStyle` now get some extra data as arguments, read more in [the docs](https://github.com/benhowell/react-grid-gallery#gallery-options). 20 | 21 | In [v0.5.x](https://github.com/benhowell/react-grid-gallery/tree/v0.5.6), there was [hacky access](https://github.com/benhowell/react-grid-gallery/tree/v0.5.6#programmers-notes) to `this` in event handlers. 22 | After lightbox functionality was stripped out this hack was removed as well. 23 | 24 | ### Image Options changes 25 | - `thumbnail` renamed to `src` 26 | - `thumbnailWidth` renamed to `width` 27 | - `thumbnailHeight` renamed to `height` 28 | 29 | So now the minimum image object looks like this 30 | 31 | ```json 32 | { 33 | "src": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ee/Apples.jpg/320px-Apples.jpg", 34 | "width": 320, 35 | "height": 480 36 | } 37 | ``` 38 | 39 | 40 | ## No default import 41 | 42 | React grid gallery now uses only named export. Please update import to 43 | ```js 44 | import { Gallery } from "react-grid-gallery"; 45 | ``` 46 | -------------------------------------------------------------------------------- /examples/captions/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /examples/captions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-grid-gallery-captions", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.tsx", 7 | "dependencies": { 8 | "react": "^18.2.0", 9 | "react-dom": "^18.2.0", 10 | "react-grid-gallery": "1.0.1", 11 | "react-scripts": "^5.0.1" 12 | }, 13 | "devDependencies": { 14 | "@types/react": "18.2.28", 15 | "@types/react-dom": "18.2.13", 16 | "typescript": "^4.9.5" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject" 23 | }, 24 | "browserslist": [ 25 | ">0.2%", 26 | "not dead", 27 | "not ie <= 11", 28 | "not op_mini all" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /examples/captions/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/captions/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Gallery } from "react-grid-gallery"; 2 | import { images } from "./images"; 3 | 4 | export default function App() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /examples/captions/src/images.tsx: -------------------------------------------------------------------------------- 1 | import { Image } from "react-grid-gallery"; 2 | 3 | export const images: Image[] = [ 4 | { 5 | src: "https://c5.staticflickr.com/9/8768/28941110956_b05ab588c1_b.jpg", 6 | width: 240, 7 | height: 320, 8 | caption: "8H (gratisography.com)", 9 | thumbnailCaption: "8H", 10 | }, 11 | { 12 | src: "https://c3.staticflickr.com/9/8583/28354353794_9f2d08d8c0_b.jpg", 13 | width: 320, 14 | height: 190, 15 | caption: "286H (gratisography.com)", 16 | thumbnailCaption: "286H", 17 | }, 18 | { 19 | src: "https://c7.staticflickr.com/9/8569/28941134686_d57273d933_b.jpg", 20 | width: 320, 21 | height: 148, 22 | caption: "315H (gratisography.com)", 23 | thumbnailCaption: "315H", 24 | }, 25 | { 26 | src: "https://c6.staticflickr.com/9/8342/28897193381_800db6419e_b.jpg", 27 | width: 320, 28 | height: 213, 29 | caption: "201H (gratisography.com)", 30 | thumbnailCaption: "201H", 31 | }, 32 | { 33 | src: "https://c2.staticflickr.com/9/8239/28897202241_1497bec71a_b.jpg", 34 | width: 248, 35 | height: 320, 36 | caption: "Big Ben (Tom Eversley - isorepublic.com)", 37 | thumbnailCaption: "Big Ben", 38 | }, 39 | { 40 | src: "https://c7.staticflickr.com/9/8785/28687743710_3580fcb5f0_b.jpg", 41 | width: 320, 42 | height: 113, 43 | caption: "Red Zone - Paris (Tom Eversley - isorepublic.com)", 44 | thumbnailCaption: ( 45 | 46 | Red Zone - Paris 47 | 48 | ), 49 | }, 50 | { 51 | src: "https://c6.staticflickr.com/9/8520/28357073053_cafcb3da6f_b.jpg", 52 | width: 313, 53 | height: 320, 54 | caption: "Wood Glass (Tom Eversley - isorepublic.com)", 55 | thumbnailCaption: "Wood Glass", 56 | }, 57 | { 58 | src: "https://c8.staticflickr.com/9/8104/28973555735_ae7c208970_b.jpg", 59 | width: 320, 60 | height: 213, 61 | caption: "Flower Interior Macro (Tom Eversley - isorepublic.com)", 62 | thumbnailCaption: "Flower Interior Macro", 63 | }, 64 | ]; 65 | -------------------------------------------------------------------------------- /examples/captions/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import * as ReactDOMClient from "react-dom/client"; 3 | import "./styles.css"; 4 | 5 | import App from "./App"; 6 | 7 | const rootElement = document.getElementById("root"); 8 | if (!rootElement) throw new Error("rootElement not found"); 9 | 10 | const root = ReactDOMClient.createRoot(rootElement); 11 | 12 | root.render( 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /examples/captions/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/captions/src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 4 | font-size: 16px; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | } 8 | 9 | .ReactGridGallery_tile-description{ 10 | text-align: center; 11 | } -------------------------------------------------------------------------------- /examples/captions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/**/*" 4 | ], 5 | "compilerOptions": { 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "lib": [ 9 | "dom", 10 | "es2015" 11 | ], 12 | "jsx": "react-jsx", 13 | "target": "es5", 14 | "allowJs": true, 15 | "skipLibCheck": true, 16 | "allowSyntheticDefaultImports": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "module": "esnext", 20 | "moduleResolution": "node", 21 | "resolveJsonModule": true, 22 | "isolatedModules": true, 23 | "noEmit": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/custom-image-component/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /examples/custom-image-component/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-grid-gallery-custom-image-component", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.tsx", 7 | "dependencies": { 8 | "react": "^18.2.0", 9 | "react-dom": "^18.2.0", 10 | "react-grid-gallery": "1.0.1", 11 | "react-scripts": "^5.0.1" 12 | }, 13 | "devDependencies": { 14 | "@types/react": "18.2.28", 15 | "@types/react-dom": "18.2.13", 16 | "typescript": "^4.9.5" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject" 23 | }, 24 | "browserslist": [ 25 | ">0.2%", 26 | "not dead", 27 | "not ie <= 11", 28 | "not op_mini all" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /examples/custom-image-component/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/custom-image-component/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Gallery, ThumbnailImageProps } from "react-grid-gallery"; 2 | import { images } from "./images"; 3 | import { useState } from "react"; 4 | 5 | const ImageComponent = (props: ThumbnailImageProps) => { 6 | const [show, setShow] = useState(false); 7 | 8 | const { src, alt, style, title } = props.imageProps; 9 | if (show) { 10 | return {alt}; 11 | } 12 | 13 | return ( 14 |
setShow(true)} 17 | > 18 | Hover to show 19 |
20 | ); 21 | }; 22 | 23 | export default function App() { 24 | return ( 25 |
26 | 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /examples/custom-image-component/src/images.tsx: -------------------------------------------------------------------------------- 1 | import { Image } from "react-grid-gallery"; 2 | 3 | export const images: Image[] = [ 4 | { 5 | src: "https://c5.staticflickr.com/9/8768/28941110956_b05ab588c1_b.jpg", 6 | width: 240, 7 | height: 320, 8 | caption: "8H (gratisography.com)", 9 | }, 10 | { 11 | src: "https://c3.staticflickr.com/9/8583/28354353794_9f2d08d8c0_b.jpg", 12 | width: 320, 13 | height: 190, 14 | caption: "286H (gratisography.com)", 15 | }, 16 | { 17 | src: "https://c7.staticflickr.com/9/8569/28941134686_d57273d933_b.jpg", 18 | width: 320, 19 | height: 148, 20 | caption: "315H (gratisography.com)", 21 | }, 22 | { 23 | src: "https://c6.staticflickr.com/9/8342/28897193381_800db6419e_b.jpg", 24 | width: 320, 25 | height: 213, 26 | caption: "201H (gratisography.com)", 27 | }, 28 | { 29 | src: "https://c2.staticflickr.com/9/8239/28897202241_1497bec71a_b.jpg", 30 | width: 248, 31 | height: 320, 32 | caption: "Big Ben (Tom Eversley - isorepublic.com)", 33 | }, 34 | { 35 | src: "https://c7.staticflickr.com/9/8785/28687743710_3580fcb5f0_b.jpg", 36 | width: 320, 37 | height: 113, 38 | caption: "Red Zone - Paris (Tom Eversley - isorepublic.com)", 39 | }, 40 | { 41 | src: "https://c6.staticflickr.com/9/8520/28357073053_cafcb3da6f_b.jpg", 42 | width: 313, 43 | height: 320, 44 | caption: "Wood Glass (Tom Eversley - isorepublic.com)", 45 | }, 46 | { 47 | src: "https://c8.staticflickr.com/9/8104/28973555735_ae7c208970_b.jpg", 48 | width: 320, 49 | height: 213, 50 | caption: "Flower Interior Macro (Tom Eversley - isorepublic.com)", 51 | }, 52 | ]; 53 | -------------------------------------------------------------------------------- /examples/custom-image-component/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import * as ReactDOMClient from "react-dom/client"; 3 | import "./styles.css"; 4 | 5 | import App from "./App"; 6 | 7 | const rootElement = document.getElementById("root"); 8 | if (!rootElement) throw new Error("rootElement not found"); 9 | 10 | const root = ReactDOMClient.createRoot(rootElement); 11 | 12 | root.render( 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /examples/custom-image-component/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/custom-image-component/src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 4 | font-size: 16px; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | } 8 | 9 | .ReactGridGallery_tile-description{ 10 | text-align: center; 11 | } -------------------------------------------------------------------------------- /examples/custom-image-component/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/**/*" 4 | ], 5 | "compilerOptions": { 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "lib": [ 9 | "dom", 10 | "es2015" 11 | ], 12 | "jsx": "react-jsx", 13 | "target": "es5", 14 | "allowJs": true, 15 | "skipLibCheck": true, 16 | "allowSyntheticDefaultImports": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "module": "esnext", 20 | "moduleResolution": "node", 21 | "resolveJsonModule": true, 22 | "isolatedModules": true, 23 | "noEmit": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/custom-overlay/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /examples/custom-overlay/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-grid-gallery-custom-overlay", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.tsx", 7 | "dependencies": { 8 | "react": "^18.2.0", 9 | "react-dom": "^18.2.0", 10 | "react-grid-gallery": "1.0.1", 11 | "react-scripts": "^5.0.1" 12 | }, 13 | "devDependencies": { 14 | "@types/react": "18.2.28", 15 | "@types/react-dom": "18.2.13", 16 | "typescript": "^4.9.5" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject" 23 | }, 24 | "browserslist": [ 25 | ">0.2%", 26 | "not dead", 27 | "not ie <= 11", 28 | "not op_mini all" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /examples/custom-overlay/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/custom-overlay/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Gallery } from "react-grid-gallery"; 2 | import { images as IMAGES } from "./images"; 3 | 4 | const images = IMAGES.map((image) => ({ 5 | ...image, 6 | customOverlay: ( 7 |
8 |
{image.caption}
9 | {image.tags && 10 | image.tags.map((t, index) => ( 11 |
12 | {t.title} 13 |
14 | ))} 15 |
16 | ), 17 | })); 18 | 19 | export default function App() { 20 | return ( 21 |
22 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /examples/custom-overlay/src/images.ts: -------------------------------------------------------------------------------- 1 | import { Image } from "react-grid-gallery"; 2 | 3 | export const images: Image[] = [ 4 | { 5 | src: "https://c2.staticflickr.com/9/8817/28973449265_07e3aa5d2e_b.jpg", 6 | width: 320, 7 | height: 174, 8 | tags: [ 9 | { value: "Nature", title: "Nature" }, 10 | { value: "Flora", title: "Flora" }, 11 | ], 12 | caption: "After Rain (Jeshu John - designerspics.com)", 13 | }, 14 | { 15 | src: "https://c2.staticflickr.com/9/8356/28897120681_3b2c0f43e0_b.jpg", 16 | width: 320, 17 | height: 212, 18 | caption: "Boats (Jeshu John - designerspics.com)", 19 | }, 20 | { 21 | src: "https://c4.staticflickr.com/9/8887/28897124891_98c4fdd82b_b.jpg", 22 | width: 320, 23 | height: 212, 24 | caption: "Color Pencils (Jeshu John - designerspics.com)", 25 | }, 26 | { 27 | src: "https://c7.staticflickr.com/9/8546/28354329294_bb45ba31fa_b.jpg", 28 | width: 320, 29 | height: 213, 30 | caption: "Red Apples with other Red Fruit (foodiesfeed.com)", 31 | }, 32 | { 33 | src: "https://c6.staticflickr.com/9/8890/28897154101_a8f55be225_b.jpg", 34 | width: 320, 35 | height: 183, 36 | caption: "37H (gratispgraphy.com)", 37 | }, 38 | { 39 | src: "https://c5.staticflickr.com/9/8768/28941110956_b05ab588c1_b.jpg", 40 | width: 240, 41 | height: 320, 42 | tags: [{ value: "Nature", title: "Nature" }], 43 | caption: "8H (gratisography.com)", 44 | }, 45 | { 46 | src: "https://c3.staticflickr.com/9/8583/28354353794_9f2d08d8c0_b.jpg", 47 | width: 320, 48 | height: 190, 49 | caption: "286H (gratisography.com)", 50 | }, 51 | { 52 | src: "https://c7.staticflickr.com/9/8569/28941134686_d57273d933_b.jpg", 53 | width: 320, 54 | height: 148, 55 | tags: [{ value: "People", title: "People" }], 56 | caption: "315H (gratisography.com)", 57 | }, 58 | { 59 | src: "https://c6.staticflickr.com/9/8342/28897193381_800db6419e_b.jpg", 60 | width: 320, 61 | height: 213, 62 | caption: "201H (gratisography.com)", 63 | }, 64 | { 65 | src: "https://c2.staticflickr.com/9/8239/28897202241_1497bec71a_b.jpg", 66 | alt: "Big Ben - London", 67 | width: 248, 68 | height: 320, 69 | caption: "Big Ben (Tom Eversley - isorepublic.com)", 70 | }, 71 | { 72 | src: "https://c7.staticflickr.com/9/8785/28687743710_3580fcb5f0_b.jpg", 73 | alt: "Red Zone - Paris", 74 | width: 320, 75 | height: 113, 76 | tags: [{ value: "People", title: "People" }], 77 | caption: "Red Zone - Paris (Tom Eversley - isorepublic.com)", 78 | }, 79 | { 80 | src: "https://c6.staticflickr.com/9/8520/28357073053_cafcb3da6f_b.jpg", 81 | alt: "Wood Glass", 82 | width: 313, 83 | height: 320, 84 | caption: "Wood Glass (Tom Eversley - isorepublic.com)", 85 | }, 86 | { 87 | src: "https://c8.staticflickr.com/9/8104/28973555735_ae7c208970_b.jpg", 88 | width: 320, 89 | height: 213, 90 | caption: "Flower Interior Macro (Tom Eversley - isorepublic.com)", 91 | }, 92 | { 93 | src: "https://c4.staticflickr.com/9/8562/28897228731_ff4447ef5f_b.jpg", 94 | width: 320, 95 | height: 194, 96 | caption: "Old Barn (Tom Eversley - isorepublic.com)", 97 | }, 98 | { 99 | src: "https://c2.staticflickr.com/8/7577/28973580825_d8f541ba3f_b.jpg", 100 | alt: "Cosmos Flower", 101 | width: 320, 102 | height: 213, 103 | caption: "Cosmos Flower Macro (Tom Eversley - isorepublic.com)", 104 | }, 105 | { 106 | src: "https://c7.staticflickr.com/9/8106/28941228886_86d1450016_b.jpg", 107 | width: 271, 108 | height: 320, 109 | caption: "Orange Macro (Tom Eversley - isorepublic.com)", 110 | }, 111 | { 112 | src: "https://c1.staticflickr.com/9/8330/28941240416_71d2a7af8e_b.jpg", 113 | width: 320, 114 | height: 213, 115 | tags: [ 116 | { value: "Nature", title: "Nature" }, 117 | { value: "People", title: "People" }, 118 | ], 119 | caption: "Surfer Sunset (Tom Eversley - isorepublic.com)", 120 | }, 121 | { 122 | src: "https://c1.staticflickr.com/9/8707/28868704912_cba5c6600e_b.jpg", 123 | width: 320, 124 | height: 213, 125 | tags: [ 126 | { value: "People", title: "People" }, 127 | { value: "Sport", title: "Sport" }, 128 | ], 129 | caption: "Man on BMX (Tom Eversley - isorepublic.com)", 130 | }, 131 | { 132 | src: "https://c4.staticflickr.com/9/8578/28357117603_97a8233cf5_b.jpg", 133 | width: 320, 134 | height: 213, 135 | caption: "Ropeman - Thailand (Tom Eversley - isorepublic.com)", 136 | }, 137 | { 138 | src: "https://c4.staticflickr.com/8/7476/28973628875_069e938525_b.jpg", 139 | width: 320, 140 | height: 213, 141 | caption: "Time to Think (Tom Eversley - isorepublic.com)", 142 | }, 143 | { 144 | src: "https://c6.staticflickr.com/9/8593/28357129133_f04c73bf1e_b.jpg", 145 | width: 320, 146 | height: 179, 147 | tags: [ 148 | { value: "Nature", title: "Nature" }, 149 | { value: "Fauna", title: "Fauna" }, 150 | ], 151 | caption: "Untitled (Jan Vasek - jeshoots.com)", 152 | }, 153 | { 154 | src: "https://c6.staticflickr.com/9/8893/28897116141_641b88e342_b.jpg", 155 | width: 320, 156 | height: 215, 157 | tags: [{ value: "People", title: "People" }], 158 | caption: "Untitled (moveast.me)", 159 | }, 160 | { 161 | src: "https://c1.staticflickr.com/9/8056/28354485944_148d6a5fc1_b.jpg", 162 | width: 257, 163 | height: 320, 164 | caption: "A photo by 贝莉儿 NG. (unsplash.com)", 165 | }, 166 | { 167 | src: "https://c7.staticflickr.com/9/8824/28868764222_19f3b30773_b.jpg", 168 | width: 226, 169 | height: 320, 170 | caption: "A photo by Matthew Wiebe. (unsplash.com)", 171 | }, 172 | ]; 173 | -------------------------------------------------------------------------------- /examples/custom-overlay/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import * as ReactDOMClient from "react-dom/client"; 3 | import "./styles.css"; 4 | 5 | import App from "./App"; 6 | 7 | const rootElement = document.getElementById("root"); 8 | if (!rootElement) throw new Error("rootElement not found"); 9 | 10 | const root = ReactDOMClient.createRoot(rootElement); 11 | 12 | root.render( 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /examples/custom-overlay/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/custom-overlay/src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 4 | font-size: 16px; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | } 8 | 9 | .custom-overlay__caption { 10 | background-color: rgba(0, 0, 0, 0.8); 11 | max-height: 240px; 12 | overflow: hidden; 13 | position: absolute; 14 | bottom: 0; 15 | width: 100%; 16 | color: white; 17 | padding: 2px; 18 | font-size: 90%; 19 | } 20 | 21 | .custom-overlay__tag { 22 | word-wrap: break-word; 23 | display: inline-block; 24 | background-color: white; 25 | height: auto; 26 | font-size: 75%; 27 | font-weight: 600; 28 | line-height: 1; 29 | padding: .2em .6em .3em; 30 | border-radius: .25em; 31 | color: black; 32 | vertical-align: baseline; 33 | margin: 2px; 34 | } -------------------------------------------------------------------------------- /examples/custom-overlay/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/**/*" 4 | ], 5 | "compilerOptions": { 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "lib": [ 9 | "dom", 10 | "es2015" 11 | ], 12 | "jsx": "react-jsx", 13 | "target": "es5", 14 | "allowJs": true, 15 | "skipLibCheck": true, 16 | "allowSyntheticDefaultImports": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "module": "esnext", 20 | "moduleResolution": "node", 21 | "resolveJsonModule": true, 22 | "isolatedModules": true, 23 | "noEmit": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next.config.js: -------------------------------------------------------------------------------- 1 | const withNextra = require("nextra")({ 2 | theme: "nextra-theme-docs", 3 | themeConfig: "./theme.config.tsx", 4 | }); 5 | 6 | module.exports = { 7 | ...withNextra(), 8 | eslint: { 9 | ignoreDuringBuilds: true, 10 | }, 11 | typescript: { 12 | ignoreBuildErrors: true, 13 | }, 14 | basePath: "/react-grid-gallery", 15 | assetPrefix: "/react-grid-gallery/", 16 | images: { 17 | unoptimized: true, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-grid-gallery-examples", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build && next export", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "install:captions": "cd captions && npm install", 11 | "install:custom-image-component": "cd custom-image-component && npm install", 12 | "install:custom-overlay": "cd custom-overlay && npm install", 13 | "install:selection": "cd selection && npm install", 14 | "install:with-react-image-lightbox": "cd with-react-image-lightbox && npm install", 15 | "install:with-yet-another-react-lightbox": "cd with-yet-another-react-lightbox && npm install", 16 | "install-all": "run-p install:*" 17 | }, 18 | "dependencies": { 19 | "next": "^12.3.4", 20 | "nextra": "^2.6.2", 21 | "nextra-theme-docs": "^2.6.2" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "18.7.16", 25 | "@types/react": "18.0.19", 26 | "@types/react-dom": "18.0.6", 27 | "npm-run-all": "^4.1.5", 28 | "typescript": "4.8.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "nextra-theme-docs/style.css"; 2 | import "./../custom-overlay/src/styles.css"; 3 | import "./../selection/src/styles.css"; 4 | import "./../captions/src/styles.css"; 5 | 6 | // @ts-ignore 7 | export default function Nextra({ Component, pageProps }) { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /examples/pages/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": "Introduction", 3 | "examples": "Examples" 4 | } 5 | -------------------------------------------------------------------------------- /examples/pages/examples/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "selection": "Image Selection", 3 | "custom-overlay": "Custom Overlay", 4 | "captions": "Thumbnail Captions", 5 | "custom-image-component": "Custom Image Component", 6 | "with-react-image-lightbox": "Lightbox integration [react-image-lightbox]", 7 | "with-yet-another-react-lightbox": "Lightbox integration [yet-another-react-lightbox]" 8 | } 9 | -------------------------------------------------------------------------------- /examples/pages/examples/captions.mdx: -------------------------------------------------------------------------------- 1 | import App from "./../../captions/src/App"; 2 | 3 | # Thumbnail Captions 4 | 5 | A thumbnail caption shown below thumbnail. 6 | 7 | ```jsx 8 | const image1 = { 9 | src: "https://c2.staticflickr.com/9/8239/28897202241_1497bec71a_b.jpg", 10 | width: 248, 11 | height: 320, 12 | thumbnailCaption: "Big Ben", 13 | } 14 | 15 | return ( 16 | 17 | ); 18 | ``` 19 | 20 | 21 | ## Live Demo 22 | 23 | 24 | ## Source Code 25 | [react-grid-gallery/examples/captions](https://github.com/benhowell/react-grid-gallery/blob/master/examples/captions/src/App.tsx) 26 | 27 | ## CodeSandbox 28 | [![Edit react-grid-gallery-captions](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/benhowell/react-grid-gallery/tree/master/examples/captions?fontsize=14&hidenavigation=1&theme=dark) 29 | 30 | 31 | -------------------------------------------------------------------------------- /examples/pages/examples/custom-image-component.mdx: -------------------------------------------------------------------------------- 1 | import App from "./../../custom-image-component/src/App"; 2 | 3 | # Custom Image Component 4 | 5 | Substitute in a React component that would get passed `imageProps` (the props that would have been passed to the `` tag) and `item` (the original item in `images`) to be used to render thumbnails; useful for lazy loading. 6 | 7 | ```jsx 8 | import { ThumbnailImageProps } from "react-grid-gallery"; 9 | 10 | const ImageComponent = (props: ThumbnailImageProps) => { 11 | const [show, setShow] = useState(false); 12 | 13 | if (show) { 14 | return ; 15 | } 16 | 17 | return ( 18 |
setShow(true)}> 19 | Hover to show 20 |
21 | ); 22 | }; 23 | ``` 24 | 25 | ```jsx 26 | 30 | ``` 31 | 32 | 33 | ## Live Demo 34 | 35 | 36 | ## Source Code 37 | [react-grid-gallery/examples/custom-image-component](https://github.com/benhowell/react-grid-gallery/blob/master/examples/custom-image-component/src/App.tsx) 38 | 39 | ## CodeSandbox 40 | [![Edit react-grid-gallery-custom-image-component](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/benhowell/react-grid-gallery/tree/master/examples/custom-image-component?fontsize=14&hidenavigation=1&theme=dark) 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/pages/examples/custom-overlay.mdx: -------------------------------------------------------------------------------- 1 | import App from "./../../custom-overlay/src/App"; 2 | 3 | # Custom Overlay 4 | 5 | A custom element can be provided to be rendered as a thumbnail overlay on hover. 6 | 7 | ```jsx 8 | const image1 = { 9 | src: "https://c2.staticflickr.com/9/8356/28897120681_3b2c0f43e0_b.jpg", 10 | width: 320, 11 | height: 212, 12 | customOverlay: ( 13 |
14 |
Boats (Jeshu John - designerspics.com)
15 |
16 | ), 17 | } 18 | 19 | return ( 20 | 21 | ); 22 | ``` 23 | 24 | 25 | ## Live Demo 26 | 27 | 28 | ## Source Code 29 | [react-grid-gallery/examples/custom-overlay](https://github.com/benhowell/react-grid-gallery/blob/master/examples/custom-overlay/src/App.tsx) 30 | 31 | ## CodeSandbox 32 | [![Edit react-grid-gallery-custom-overlay](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/benhowell/react-grid-gallery/tree/master/examples/custom-overlay?fontsize=14&hidenavigation=1&theme=dark) 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/pages/examples/selection.mdx: -------------------------------------------------------------------------------- 1 | import App from "./../../selection/src/App"; 2 | 3 | # Image Selection 4 | 5 | ```jsx 6 | const [images, setImages] = useState(IMAGES); 7 | 8 | const handleSelect = (index: number, item: Image, event: MouseEvent) => { 9 | const nextImages = images.map((image, i) => 10 | i === index ? { ...image, isSelected: !image.isSelected } : image 11 | ); 12 | setImages(nextImages); 13 | }; 14 | 15 | return ( 16 | 17 | ) 18 | ``` 19 | 20 | ## Live Demo 21 | 22 | 23 | ## Source Code 24 | [react-grid-gallery/examples/selection](https://github.com/benhowell/react-grid-gallery/blob/master/examples/selection/src/App.tsx) 25 | 26 | ## CodeSandbox 27 | [![Edit react-grid-gallery-selection](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/benhowell/react-grid-gallery/tree/master/examples/selection?fontsize=14&hidenavigation=1&theme=dark) 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/pages/examples/with-react-image-lightbox.mdx: -------------------------------------------------------------------------------- 1 | import App from "./../../with-react-image-lightbox/src/App"; 2 | 3 | # Lightbox integration `react-image-lightbox` 4 | 5 | Example of a very basic integration with [react-image-lightbox](https://github.com/frontend-collective/react-image-lightbox). Click on a thumbnail to open the lightbox. 6 | 7 | ## Live Demo 8 | 9 | 10 | ## Source Code 11 | [react-grid-gallery/examples/with-react-image-lightbox](https://github.com/benhowell/react-grid-gallery/blob/master/examples/with-react-image-lightbox/src/App.tsx) 12 | 13 | ## CodeSandbox 14 | [![Edit react-grid-gallery-with-react-image-lightbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/benhowell/react-grid-gallery/tree/master/examples/with-react-image-lightbox?fontsize=14&hidenavigation=1&theme=dark) 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/pages/examples/with-yet-another-react-lightbox.mdx: -------------------------------------------------------------------------------- 1 | import App from "./../../with-yet-another-react-lightbox/src/App"; 2 | 3 | # Lightbox integration `yet-another-react-lightbox` 4 | 5 | Example of a very basic integration with [yet-another-react-lightbox](https://github.com/igordanchenko/yet-another-react-lightbox). Click on a thumbnail to open the lightbox. 6 | 7 | 8 | ## Live Demo 9 | 10 | 11 | ## Source Code 12 | [react-grid-gallery/examples/with-yet-another-react-lightbox](https://github.com/benhowell/react-grid-gallery/blob/master/examples/with-yet-another-react-lightbox/src/App.tsx) 13 | 14 | ## CodeSandbox 15 | [![Edit react-grid-gallery-with-yet-another-react-lightbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/benhowell/react-grid-gallery/tree/master/examples/with-yet-another-react-lightbox?fontsize=14&hidenavigation=1&theme=dark) 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/pages/index.mdx: -------------------------------------------------------------------------------- 1 | # React Grid Gallery 2 | 3 | Justified image gallery component for [React](http://facebook.github.io/react/) inspired by [Google Photos](https://photos.google.com/). 4 | 5 | ## Installation 6 | 7 | Using [npm](https://www.npmjs.com/): 8 | 9 | ```shell 10 | npm install --save react-grid-gallery 11 | ``` 12 | 13 | ## Quick Start 14 | 15 | ```jsx 16 | import { Gallery } from "react-grid-gallery"; 17 | 18 | const images = [ 19 | { 20 | src: "https://c2.staticflickr.com/9/8817/28973449265_07e3aa5d2e_b.jpg", 21 | width: 320, 22 | height: 174, 23 | isSelected: true, 24 | caption: "After Rain (Jeshu John - designerspics.com)", 25 | }, 26 | { 27 | src: "https://c2.staticflickr.com/9/8356/28897120681_3b2c0f43e0_b.jpg", 28 | width: 320, 29 | height: 212, 30 | tags: [ 31 | { value: "Ocean", title: "Ocean" }, 32 | { value: "People", title: "People" }, 33 | ], 34 | alt: "Boats (Jeshu John - designerspics.com)", 35 | }, 36 | { 37 | src: "https://c4.staticflickr.com/9/8887/28897124891_98c4fdd82b_b.jpg", 38 | width: 320, 39 | height: 212, 40 | }, 41 | ]; 42 | 43 | 44 | ``` 45 | 46 | ## Documentation 47 | 48 | The latest version of the documentation is available on the [GitHub repository](https://github.com/benhowell/react-grid-gallery#image-options). 49 | -------------------------------------------------------------------------------- /examples/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/examples/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /examples/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/examples/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /examples/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/examples/public/apple-touch-icon.png -------------------------------------------------------------------------------- /examples/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/examples/public/favicon-16x16.png -------------------------------------------------------------------------------- /examples/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/examples/public/favicon-32x32.png -------------------------------------------------------------------------------- /examples/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/examples/public/favicon.ico -------------------------------------------------------------------------------- /examples/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/examples/public/favicon.png -------------------------------------------------------------------------------- /examples/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /examples/selection/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /examples/selection/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-grid-gallery-selection", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.tsx", 7 | "dependencies": { 8 | "react": "^18.2.0", 9 | "react-dom": "^18.2.0", 10 | "react-grid-gallery": "1.0.1", 11 | "react-scripts": "^5.0.1" 12 | }, 13 | "devDependencies": { 14 | "@types/react": "18.2.28", 15 | "@types/react-dom": "18.2.13", 16 | "typescript": "^4.9.5" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject" 23 | }, 24 | "browserslist": [ 25 | ">0.2%", 26 | "not dead", 27 | "not ie <= 11", 28 | "not op_mini all" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /examples/selection/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/selection/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Gallery } from "react-grid-gallery"; 3 | import { images as IMAGES } from "./images"; 4 | 5 | export default function App() { 6 | const [images, setImages] = useState(IMAGES); 7 | const hasSelected = images.some((image) => image.isSelected); 8 | 9 | const handleSelect = (index: number) => { 10 | const nextImages = images.map((image, i) => 11 | i === index ? { ...image, isSelected: !image.isSelected } : image 12 | ); 13 | setImages(nextImages); 14 | }; 15 | 16 | const handleSelectAllClick = () => { 17 | const nextImages = images.map((image) => ({ 18 | ...image, 19 | isSelected: !hasSelected, 20 | })); 21 | setImages(nextImages); 22 | }; 23 | 24 | return ( 25 |
26 |
27 | 30 |
31 | 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /examples/selection/src/images.ts: -------------------------------------------------------------------------------- 1 | import { Image } from "react-grid-gallery"; 2 | 3 | export const images: Image[] = [ 4 | { 5 | src: "https://c2.staticflickr.com/9/8817/28973449265_07e3aa5d2e_b.jpg", 6 | width: 320, 7 | height: 174, 8 | tags: [ 9 | { value: "Nature", title: "Nature" }, 10 | { value: "Flora", title: "Flora" }, 11 | ], 12 | caption: "After Rain (Jeshu John - designerspics.com)", 13 | }, 14 | { 15 | src: "https://c2.staticflickr.com/9/8356/28897120681_3b2c0f43e0_b.jpg", 16 | width: 320, 17 | height: 212, 18 | caption: "Boats (Jeshu John - designerspics.com)", 19 | }, 20 | { 21 | src: "https://c4.staticflickr.com/9/8887/28897124891_98c4fdd82b_b.jpg", 22 | width: 320, 23 | height: 212, 24 | caption: "Color Pencils (Jeshu John - designerspics.com)", 25 | }, 26 | { 27 | src: "https://c7.staticflickr.com/9/8546/28354329294_bb45ba31fa_b.jpg", 28 | width: 320, 29 | height: 213, 30 | caption: "Red Apples with other Red Fruit (foodiesfeed.com)", 31 | }, 32 | { 33 | src: "https://c6.staticflickr.com/9/8890/28897154101_a8f55be225_b.jpg", 34 | width: 320, 35 | height: 183, 36 | caption: "37H (gratispgraphy.com)", 37 | }, 38 | { 39 | src: "https://c5.staticflickr.com/9/8768/28941110956_b05ab588c1_b.jpg", 40 | width: 240, 41 | height: 320, 42 | tags: [{ value: "Nature", title: "Nature" }], 43 | caption: "8H (gratisography.com)", 44 | }, 45 | { 46 | src: "https://c3.staticflickr.com/9/8583/28354353794_9f2d08d8c0_b.jpg", 47 | width: 320, 48 | height: 190, 49 | caption: "286H (gratisography.com)", 50 | }, 51 | { 52 | src: "https://c7.staticflickr.com/9/8569/28941134686_d57273d933_b.jpg", 53 | width: 320, 54 | height: 148, 55 | tags: [{ value: "People", title: "People" }], 56 | caption: "315H (gratisography.com)", 57 | }, 58 | { 59 | src: "https://c6.staticflickr.com/9/8342/28897193381_800db6419e_b.jpg", 60 | width: 320, 61 | height: 213, 62 | caption: "201H (gratisography.com)", 63 | }, 64 | { 65 | src: "https://c2.staticflickr.com/9/8239/28897202241_1497bec71a_b.jpg", 66 | alt: "Big Ben - London", 67 | width: 248, 68 | height: 320, 69 | caption: "Big Ben (Tom Eversley - isorepublic.com)", 70 | }, 71 | { 72 | src: "https://c7.staticflickr.com/9/8785/28687743710_3580fcb5f0_b.jpg", 73 | alt: "Red Zone - Paris", 74 | width: 320, 75 | height: 113, 76 | tags: [{ value: "People", title: "People" }], 77 | caption: "Red Zone - Paris (Tom Eversley - isorepublic.com)", 78 | }, 79 | { 80 | src: "https://c6.staticflickr.com/9/8520/28357073053_cafcb3da6f_b.jpg", 81 | alt: "Wood Glass", 82 | width: 313, 83 | height: 320, 84 | caption: "Wood Glass (Tom Eversley - isorepublic.com)", 85 | }, 86 | { 87 | src: "https://c8.staticflickr.com/9/8104/28973555735_ae7c208970_b.jpg", 88 | width: 320, 89 | height: 213, 90 | caption: "Flower Interior Macro (Tom Eversley - isorepublic.com)", 91 | }, 92 | { 93 | src: "https://c4.staticflickr.com/9/8562/28897228731_ff4447ef5f_b.jpg", 94 | width: 320, 95 | height: 194, 96 | caption: "Old Barn (Tom Eversley - isorepublic.com)", 97 | }, 98 | { 99 | src: "https://c2.staticflickr.com/8/7577/28973580825_d8f541ba3f_b.jpg", 100 | alt: "Cosmos Flower", 101 | width: 320, 102 | height: 213, 103 | caption: "Cosmos Flower Macro (Tom Eversley - isorepublic.com)", 104 | }, 105 | { 106 | src: "https://c7.staticflickr.com/9/8106/28941228886_86d1450016_b.jpg", 107 | width: 271, 108 | height: 320, 109 | caption: "Orange Macro (Tom Eversley - isorepublic.com)", 110 | }, 111 | { 112 | src: "https://c1.staticflickr.com/9/8330/28941240416_71d2a7af8e_b.jpg", 113 | width: 320, 114 | height: 213, 115 | tags: [ 116 | { value: "Nature", title: "Nature" }, 117 | { value: "People", title: "People" }, 118 | ], 119 | caption: "Surfer Sunset (Tom Eversley - isorepublic.com)", 120 | }, 121 | { 122 | src: "https://c1.staticflickr.com/9/8707/28868704912_cba5c6600e_b.jpg", 123 | width: 320, 124 | height: 213, 125 | tags: [ 126 | { value: "People", title: "People" }, 127 | { value: "Sport", title: "Sport" }, 128 | ], 129 | caption: "Man on BMX (Tom Eversley - isorepublic.com)", 130 | }, 131 | { 132 | src: "https://c4.staticflickr.com/9/8578/28357117603_97a8233cf5_b.jpg", 133 | width: 320, 134 | height: 213, 135 | caption: "Ropeman - Thailand (Tom Eversley - isorepublic.com)", 136 | }, 137 | { 138 | src: "https://c4.staticflickr.com/8/7476/28973628875_069e938525_b.jpg", 139 | width: 320, 140 | height: 213, 141 | caption: "Time to Think (Tom Eversley - isorepublic.com)", 142 | }, 143 | { 144 | src: "https://c6.staticflickr.com/9/8593/28357129133_f04c73bf1e_b.jpg", 145 | width: 320, 146 | height: 179, 147 | tags: [ 148 | { value: "Nature", title: "Nature" }, 149 | { value: "Fauna", title: "Fauna" }, 150 | ], 151 | caption: "Untitled (Jan Vasek - jeshoots.com)", 152 | }, 153 | { 154 | src: "https://c6.staticflickr.com/9/8893/28897116141_641b88e342_b.jpg", 155 | width: 320, 156 | height: 215, 157 | tags: [{ value: "People", title: "People" }], 158 | caption: "Untitled (moveast.me)", 159 | }, 160 | { 161 | src: "https://c1.staticflickr.com/9/8056/28354485944_148d6a5fc1_b.jpg", 162 | width: 257, 163 | height: 320, 164 | caption: "A photo by 贝莉儿 NG. (unsplash.com)", 165 | }, 166 | { 167 | src: "https://c7.staticflickr.com/9/8824/28868764222_19f3b30773_b.jpg", 168 | width: 226, 169 | height: 320, 170 | caption: "A photo by Matthew Wiebe. (unsplash.com)", 171 | }, 172 | ]; 173 | -------------------------------------------------------------------------------- /examples/selection/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import * as ReactDOMClient from "react-dom/client"; 3 | import "./styles.css"; 4 | 5 | import App from "./App"; 6 | 7 | const rootElement = document.getElementById("root"); 8 | if (!rootElement) throw new Error("rootElement not found"); 9 | 10 | const root = ReactDOMClient.createRoot(rootElement); 11 | 12 | root.render( 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /examples/selection/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/selection/src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 4 | font-size: 16px; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | } 8 | 9 | .p-t-1{ 10 | padding-top: 1rem; 11 | } 12 | 13 | .p-b-1{ 14 | padding-bottom: 1rem; 15 | } -------------------------------------------------------------------------------- /examples/selection/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/**/*" 4 | ], 5 | "compilerOptions": { 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "lib": [ 9 | "dom", 10 | "es2015" 11 | ], 12 | "jsx": "react-jsx", 13 | "target": "es5", 14 | "allowJs": true, 15 | "skipLibCheck": true, 16 | "allowSyntheticDefaultImports": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "module": "esnext", 20 | "moduleResolution": "node", 21 | "resolveJsonModule": true, 22 | "isolatedModules": true, 23 | "noEmit": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/theme.config.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | project: { link: "https://github.com/benhowell/react-grid-gallery" }, 3 | docsRepositoryBase: 4 | "https://github.com/benhowell/react-grid-gallery/blob/master/examples", 5 | useNextSeoProps() { 6 | return { 7 | titleTemplate: "%s – React Grid Gallery", 8 | }; 9 | }, 10 | faviconGlyph: "👋", 11 | footer: { 12 | text: ( 13 | 14 | Maintained by{" "} 15 | 20 | Ben Howell 21 | {" "} 22 | and{" "} 23 | 28 | Igor Isaev 29 | 30 | . 31 | 32 | ), 33 | }, 34 | logo: ( 35 | <> 36 | 37 | React Grid Gallery 38 | 39 | 40 | ), 41 | head: ( 42 | <> 43 | 44 | 48 | 52 | 57 | 62 | 63 | ), 64 | }; 65 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/with-react-image-lightbox/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /examples/with-react-image-lightbox/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-grid-gallery-with-react-image-lightbox", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.tsx", 7 | "dependencies": { 8 | "react": "^17.0.2", 9 | "react-dom": "^17.0.2", 10 | "react-grid-gallery": "1.0.1", 11 | "react-image-lightbox": "^5.1.4", 12 | "react-scripts": "^5.0.1" 13 | }, 14 | "devDependencies": { 15 | "@types/react": "17.0.20", 16 | "@types/react-dom": "17.0.9", 17 | "typescript": "^4.9.5" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test --env=jsdom", 23 | "eject": "react-scripts eject" 24 | }, 25 | "browserslist": [ 26 | ">0.2%", 27 | "not dead", 28 | "not ie <= 11", 29 | "not op_mini all" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /examples/with-react-image-lightbox/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/with-react-image-lightbox/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Gallery } from "react-grid-gallery"; 3 | import Lightbox from "react-image-lightbox"; 4 | import "react-image-lightbox/style.css"; 5 | import { images, CustomImage } from "./images"; 6 | 7 | export default function App() { 8 | const [index, setIndex] = useState(-1); 9 | 10 | const currentImage = images[index]; 11 | const nextIndex = (index + 1) % images.length; 12 | const nextImage = images[nextIndex] || currentImage; 13 | const prevIndex = (index + images.length - 1) % images.length; 14 | const prevImage = images[prevIndex] || currentImage; 15 | 16 | const handleClick = (index: number, item: CustomImage) => setIndex(index); 17 | const handleClose = () => setIndex(-1); 18 | const handleMovePrev = () => setIndex(prevIndex); 19 | const handleMoveNext = () => setIndex(nextIndex); 20 | 21 | return ( 22 |
23 | 28 | {!!currentImage && ( 29 | /* @ts-ignore */ 30 | 42 | )} 43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /examples/with-react-image-lightbox/src/images.ts: -------------------------------------------------------------------------------- 1 | import { Image } from "react-grid-gallery"; 2 | 3 | export interface CustomImage extends Image { 4 | original: string; 5 | } 6 | 7 | export const images: CustomImage[] = [ 8 | { 9 | src: "https://c2.staticflickr.com/9/8817/28973449265_07e3aa5d2e_b.jpg", 10 | original: "https://c2.staticflickr.com/9/8817/28973449265_07e3aa5d2e_b.jpg", 11 | width: 320, 12 | height: 174, 13 | tags: [ 14 | { value: "Nature", title: "Nature" }, 15 | { value: "Flora", title: "Flora" }, 16 | ], 17 | caption: "After Rain (Jeshu John - designerspics.com)", 18 | }, 19 | { 20 | src: "https://c2.staticflickr.com/9/8356/28897120681_3b2c0f43e0_b.jpg", 21 | original: "https://c2.staticflickr.com/9/8356/28897120681_3b2c0f43e0_b.jpg", 22 | width: 320, 23 | height: 212, 24 | caption: "Boats (Jeshu John - designerspics.com)", 25 | }, 26 | { 27 | src: "https://c4.staticflickr.com/9/8887/28897124891_98c4fdd82b_b.jpg", 28 | original: "https://c4.staticflickr.com/9/8887/28897124891_98c4fdd82b_b.jpg", 29 | width: 320, 30 | height: 212, 31 | caption: "Color Pencils (Jeshu John - designerspics.com)", 32 | }, 33 | { 34 | src: "https://c7.staticflickr.com/9/8546/28354329294_bb45ba31fa_b.jpg", 35 | original: "https://c7.staticflickr.com/9/8546/28354329294_bb45ba31fa_b.jpg", 36 | width: 320, 37 | height: 213, 38 | caption: "Red Apples with other Red Fruit (foodiesfeed.com)", 39 | }, 40 | { 41 | src: "https://c6.staticflickr.com/9/8890/28897154101_a8f55be225_b.jpg", 42 | original: "https://c6.staticflickr.com/9/8890/28897154101_a8f55be225_b.jpg", 43 | width: 320, 44 | height: 183, 45 | caption: "37H (gratispgraphy.com)", 46 | }, 47 | { 48 | src: "https://c5.staticflickr.com/9/8768/28941110956_b05ab588c1_b.jpg", 49 | original: "https://c5.staticflickr.com/9/8768/28941110956_b05ab588c1_b.jpg", 50 | width: 240, 51 | height: 320, 52 | tags: [{ value: "Nature", title: "Nature" }], 53 | caption: "8H (gratisography.com)", 54 | }, 55 | { 56 | src: "https://c3.staticflickr.com/9/8583/28354353794_9f2d08d8c0_b.jpg", 57 | original: "https://c3.staticflickr.com/9/8583/28354353794_9f2d08d8c0_b.jpg", 58 | width: 320, 59 | height: 190, 60 | caption: "286H (gratisography.com)", 61 | }, 62 | { 63 | src: "https://c7.staticflickr.com/9/8569/28941134686_d57273d933_b.jpg", 64 | original: "https://c7.staticflickr.com/9/8569/28941134686_d57273d933_b.jpg", 65 | width: 320, 66 | height: 148, 67 | tags: [{ value: "People", title: "People" }], 68 | caption: "315H (gratisography.com)", 69 | }, 70 | { 71 | src: "https://c6.staticflickr.com/9/8342/28897193381_800db6419e_b.jpg", 72 | original: "https://c6.staticflickr.com/9/8342/28897193381_800db6419e_b.jpg", 73 | width: 320, 74 | height: 213, 75 | caption: "201H (gratisography.com)", 76 | }, 77 | { 78 | src: "https://c2.staticflickr.com/9/8239/28897202241_1497bec71a_b.jpg", 79 | original: "https://c2.staticflickr.com/9/8239/28897202241_1497bec71a_b.jpg", 80 | alt: "Big Ben - London", 81 | width: 248, 82 | height: 320, 83 | caption: "Big Ben (Tom Eversley - isorepublic.com)", 84 | }, 85 | { 86 | src: "https://c7.staticflickr.com/9/8785/28687743710_3580fcb5f0_b.jpg", 87 | original: "https://c7.staticflickr.com/9/8785/28687743710_3580fcb5f0_b.jpg", 88 | alt: "Red Zone - Paris", 89 | width: 320, 90 | height: 113, 91 | tags: [{ value: "People", title: "People" }], 92 | caption: "Red Zone - Paris (Tom Eversley - isorepublic.com)", 93 | }, 94 | { 95 | src: "https://c6.staticflickr.com/9/8520/28357073053_cafcb3da6f_b.jpg", 96 | original: "https://c6.staticflickr.com/9/8520/28357073053_cafcb3da6f_b.jpg", 97 | alt: "Wood Glass", 98 | width: 313, 99 | height: 320, 100 | caption: "Wood Glass (Tom Eversley - isorepublic.com)", 101 | }, 102 | { 103 | src: "https://c8.staticflickr.com/9/8104/28973555735_ae7c208970_b.jpg", 104 | original: "https://c8.staticflickr.com/9/8104/28973555735_ae7c208970_b.jpg", 105 | width: 320, 106 | height: 213, 107 | caption: "Flower Interior Macro (Tom Eversley - isorepublic.com)", 108 | }, 109 | { 110 | src: "https://c4.staticflickr.com/9/8562/28897228731_ff4447ef5f_b.jpg", 111 | original: "https://c4.staticflickr.com/9/8562/28897228731_ff4447ef5f_b.jpg", 112 | width: 320, 113 | height: 194, 114 | caption: "Old Barn (Tom Eversley - isorepublic.com)", 115 | }, 116 | { 117 | src: "https://c2.staticflickr.com/8/7577/28973580825_d8f541ba3f_b.jpg", 118 | original: "https://c2.staticflickr.com/8/7577/28973580825_d8f541ba3f_b.jpg", 119 | alt: "Cosmos Flower", 120 | width: 320, 121 | height: 213, 122 | caption: "Cosmos Flower Macro (Tom Eversley - isorepublic.com)", 123 | }, 124 | { 125 | src: "https://c7.staticflickr.com/9/8106/28941228886_86d1450016_b.jpg", 126 | original: "https://c7.staticflickr.com/9/8106/28941228886_86d1450016_b.jpg", 127 | width: 271, 128 | height: 320, 129 | caption: "Orange Macro (Tom Eversley - isorepublic.com)", 130 | }, 131 | { 132 | src: "https://c1.staticflickr.com/9/8330/28941240416_71d2a7af8e_b.jpg", 133 | original: "https://c1.staticflickr.com/9/8330/28941240416_71d2a7af8e_b.jpg", 134 | width: 320, 135 | height: 213, 136 | tags: [ 137 | { value: "Nature", title: "Nature" }, 138 | { value: "People", title: "People" }, 139 | ], 140 | caption: "Surfer Sunset (Tom Eversley - isorepublic.com)", 141 | }, 142 | { 143 | src: "https://c1.staticflickr.com/9/8707/28868704912_cba5c6600e_b.jpg", 144 | original: "https://c1.staticflickr.com/9/8707/28868704912_cba5c6600e_b.jpg", 145 | width: 320, 146 | height: 213, 147 | tags: [ 148 | { value: "People", title: "People" }, 149 | { value: "Sport", title: "Sport" }, 150 | ], 151 | caption: "Man on BMX (Tom Eversley - isorepublic.com)", 152 | }, 153 | { 154 | src: "https://c4.staticflickr.com/9/8578/28357117603_97a8233cf5_b.jpg", 155 | original: "https://c4.staticflickr.com/9/8578/28357117603_97a8233cf5_b.jpg", 156 | width: 320, 157 | height: 213, 158 | caption: "Ropeman - Thailand (Tom Eversley - isorepublic.com)", 159 | }, 160 | { 161 | src: "https://c4.staticflickr.com/8/7476/28973628875_069e938525_b.jpg", 162 | original: "https://c4.staticflickr.com/8/7476/28973628875_069e938525_b.jpg", 163 | width: 320, 164 | height: 213, 165 | caption: "Time to Think (Tom Eversley - isorepublic.com)", 166 | }, 167 | { 168 | src: "https://c6.staticflickr.com/9/8593/28357129133_f04c73bf1e_b.jpg", 169 | original: "https://c6.staticflickr.com/9/8593/28357129133_f04c73bf1e_b.jpg", 170 | width: 320, 171 | height: 179, 172 | tags: [ 173 | { value: "Nature", title: "Nature" }, 174 | { value: "Fauna", title: "Fauna" }, 175 | ], 176 | caption: "Untitled (Jan Vasek - jeshoots.com)", 177 | }, 178 | { 179 | src: "https://c6.staticflickr.com/9/8893/28897116141_641b88e342_b.jpg", 180 | original: "https://c6.staticflickr.com/9/8893/28897116141_641b88e342_b.jpg", 181 | width: 320, 182 | height: 215, 183 | tags: [{ value: "People", title: "People" }], 184 | caption: "Untitled (moveast.me)", 185 | }, 186 | { 187 | src: "https://c1.staticflickr.com/9/8056/28354485944_148d6a5fc1_b.jpg", 188 | original: "https://c1.staticflickr.com/9/8056/28354485944_148d6a5fc1_b.jpg", 189 | width: 257, 190 | height: 320, 191 | caption: "A photo by 贝莉儿 NG. (unsplash.com)", 192 | }, 193 | { 194 | src: "https://c7.staticflickr.com/9/8824/28868764222_19f3b30773_b.jpg", 195 | original: "https://c7.staticflickr.com/9/8824/28868764222_19f3b30773_b.jpg", 196 | width: 226, 197 | height: 320, 198 | caption: "A photo by Matthew Wiebe. (unsplash.com)", 199 | }, 200 | ]; 201 | -------------------------------------------------------------------------------- /examples/with-react-image-lightbox/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { render } from "react-dom"; 2 | import "./styles.css"; 3 | 4 | import App from "./App"; 5 | 6 | const rootElement = document.getElementById("root"); 7 | if (!rootElement) throw new Error("rootElement not found"); 8 | 9 | render(, rootElement); 10 | -------------------------------------------------------------------------------- /examples/with-react-image-lightbox/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/with-react-image-lightbox/src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 4 | font-size: 16px; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | } -------------------------------------------------------------------------------- /examples/with-react-image-lightbox/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/**/*" 4 | ], 5 | "compilerOptions": { 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "lib": [ 9 | "dom", 10 | "es2015" 11 | ], 12 | "jsx": "react-jsx", 13 | "target": "es5", 14 | "allowJs": true, 15 | "skipLibCheck": true, 16 | "allowSyntheticDefaultImports": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "module": "esnext", 20 | "moduleResolution": "node", 21 | "resolveJsonModule": true, 22 | "isolatedModules": true, 23 | "noEmit": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/with-yet-another-react-lightbox/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /examples/with-yet-another-react-lightbox/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-grid-gallery-with-yet-another-react-lightbox", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.tsx", 7 | "dependencies": { 8 | "react": "^18.2.0", 9 | "react-dom": "^18.2.0", 10 | "react-grid-gallery": "1.0.1", 11 | "react-scripts": "^5.0.1", 12 | "yet-another-react-lightbox": "^3.14.0" 13 | }, 14 | "devDependencies": { 15 | "@types/react": "18.2.28", 16 | "@types/react-dom": "18.2.13", 17 | "typescript": "^4.9.5" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test --env=jsdom", 23 | "eject": "react-scripts eject" 24 | }, 25 | "browserslist": [ 26 | ">0.2%", 27 | "not dead", 28 | "not ie <= 11", 29 | "not op_mini all" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /examples/with-yet-another-react-lightbox/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/with-yet-another-react-lightbox/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Gallery } from "react-grid-gallery"; 3 | import Lightbox from "yet-another-react-lightbox"; 4 | import "yet-another-react-lightbox/styles.css"; 5 | import { images, CustomImage } from "./images"; 6 | 7 | const slides = images.map(({ original, width, height }) => ({ 8 | src: original, 9 | width, 10 | height, 11 | })); 12 | 13 | export default function App() { 14 | const [index, setIndex] = useState(-1); 15 | 16 | const handleClick = (index: number, item: CustomImage) => setIndex(index); 17 | 18 | return ( 19 |
20 | 25 | = 0} 28 | index={index} 29 | close={() => setIndex(-1)} 30 | /> 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /examples/with-yet-another-react-lightbox/src/images.ts: -------------------------------------------------------------------------------- 1 | import { Image } from "react-grid-gallery"; 2 | 3 | export interface CustomImage extends Image { 4 | original: string; 5 | } 6 | 7 | export const images: CustomImage[] = [ 8 | { 9 | src: "https://c2.staticflickr.com/9/8817/28973449265_07e3aa5d2e_b.jpg", 10 | original: "https://c2.staticflickr.com/9/8817/28973449265_07e3aa5d2e_b.jpg", 11 | width: 320, 12 | height: 174, 13 | tags: [ 14 | { value: "Nature", title: "Nature" }, 15 | { value: "Flora", title: "Flora" }, 16 | ], 17 | caption: "After Rain (Jeshu John - designerspics.com)", 18 | }, 19 | { 20 | src: "https://c2.staticflickr.com/9/8356/28897120681_3b2c0f43e0_b.jpg", 21 | original: "https://c2.staticflickr.com/9/8356/28897120681_3b2c0f43e0_b.jpg", 22 | width: 320, 23 | height: 212, 24 | caption: "Boats (Jeshu John - designerspics.com)", 25 | }, 26 | { 27 | src: "https://c4.staticflickr.com/9/8887/28897124891_98c4fdd82b_b.jpg", 28 | original: "https://c4.staticflickr.com/9/8887/28897124891_98c4fdd82b_b.jpg", 29 | width: 320, 30 | height: 212, 31 | caption: "Color Pencils (Jeshu John - designerspics.com)", 32 | }, 33 | { 34 | src: "https://c7.staticflickr.com/9/8546/28354329294_bb45ba31fa_b.jpg", 35 | original: "https://c7.staticflickr.com/9/8546/28354329294_bb45ba31fa_b.jpg", 36 | width: 320, 37 | height: 213, 38 | caption: "Red Apples with other Red Fruit (foodiesfeed.com)", 39 | }, 40 | { 41 | src: "https://c6.staticflickr.com/9/8890/28897154101_a8f55be225_b.jpg", 42 | original: "https://c6.staticflickr.com/9/8890/28897154101_a8f55be225_b.jpg", 43 | width: 320, 44 | height: 183, 45 | caption: "37H (gratispgraphy.com)", 46 | }, 47 | { 48 | src: "https://c5.staticflickr.com/9/8768/28941110956_b05ab588c1_b.jpg", 49 | original: "https://c5.staticflickr.com/9/8768/28941110956_b05ab588c1_b.jpg", 50 | width: 240, 51 | height: 320, 52 | tags: [{ value: "Nature", title: "Nature" }], 53 | caption: "8H (gratisography.com)", 54 | }, 55 | { 56 | src: "https://c3.staticflickr.com/9/8583/28354353794_9f2d08d8c0_b.jpg", 57 | original: "https://c3.staticflickr.com/9/8583/28354353794_9f2d08d8c0_b.jpg", 58 | width: 320, 59 | height: 190, 60 | caption: "286H (gratisography.com)", 61 | }, 62 | { 63 | src: "https://c7.staticflickr.com/9/8569/28941134686_d57273d933_b.jpg", 64 | original: "https://c7.staticflickr.com/9/8569/28941134686_d57273d933_b.jpg", 65 | width: 320, 66 | height: 148, 67 | tags: [{ value: "People", title: "People" }], 68 | caption: "315H (gratisography.com)", 69 | }, 70 | { 71 | src: "https://c6.staticflickr.com/9/8342/28897193381_800db6419e_b.jpg", 72 | original: "https://c6.staticflickr.com/9/8342/28897193381_800db6419e_b.jpg", 73 | width: 320, 74 | height: 213, 75 | caption: "201H (gratisography.com)", 76 | }, 77 | { 78 | src: "https://c2.staticflickr.com/9/8239/28897202241_1497bec71a_b.jpg", 79 | original: "https://c2.staticflickr.com/9/8239/28897202241_1497bec71a_b.jpg", 80 | alt: "Big Ben - London", 81 | width: 248, 82 | height: 320, 83 | caption: "Big Ben (Tom Eversley - isorepublic.com)", 84 | }, 85 | { 86 | src: "https://c7.staticflickr.com/9/8785/28687743710_3580fcb5f0_b.jpg", 87 | original: "https://c7.staticflickr.com/9/8785/28687743710_3580fcb5f0_b.jpg", 88 | alt: "Red Zone - Paris", 89 | width: 320, 90 | height: 113, 91 | tags: [{ value: "People", title: "People" }], 92 | caption: "Red Zone - Paris (Tom Eversley - isorepublic.com)", 93 | }, 94 | { 95 | src: "https://c6.staticflickr.com/9/8520/28357073053_cafcb3da6f_b.jpg", 96 | original: "https://c6.staticflickr.com/9/8520/28357073053_cafcb3da6f_b.jpg", 97 | alt: "Wood Glass", 98 | width: 313, 99 | height: 320, 100 | caption: "Wood Glass (Tom Eversley - isorepublic.com)", 101 | }, 102 | { 103 | src: "https://c8.staticflickr.com/9/8104/28973555735_ae7c208970_b.jpg", 104 | original: "https://c8.staticflickr.com/9/8104/28973555735_ae7c208970_b.jpg", 105 | width: 320, 106 | height: 213, 107 | caption: "Flower Interior Macro (Tom Eversley - isorepublic.com)", 108 | }, 109 | { 110 | src: "https://c4.staticflickr.com/9/8562/28897228731_ff4447ef5f_b.jpg", 111 | original: "https://c4.staticflickr.com/9/8562/28897228731_ff4447ef5f_b.jpg", 112 | width: 320, 113 | height: 194, 114 | caption: "Old Barn (Tom Eversley - isorepublic.com)", 115 | }, 116 | { 117 | src: "https://c2.staticflickr.com/8/7577/28973580825_d8f541ba3f_b.jpg", 118 | original: "https://c2.staticflickr.com/8/7577/28973580825_d8f541ba3f_b.jpg", 119 | alt: "Cosmos Flower", 120 | width: 320, 121 | height: 213, 122 | caption: "Cosmos Flower Macro (Tom Eversley - isorepublic.com)", 123 | }, 124 | { 125 | src: "https://c7.staticflickr.com/9/8106/28941228886_86d1450016_b.jpg", 126 | original: "https://c7.staticflickr.com/9/8106/28941228886_86d1450016_b.jpg", 127 | width: 271, 128 | height: 320, 129 | caption: "Orange Macro (Tom Eversley - isorepublic.com)", 130 | }, 131 | { 132 | src: "https://c1.staticflickr.com/9/8330/28941240416_71d2a7af8e_b.jpg", 133 | original: "https://c1.staticflickr.com/9/8330/28941240416_71d2a7af8e_b.jpg", 134 | width: 320, 135 | height: 213, 136 | tags: [ 137 | { value: "Nature", title: "Nature" }, 138 | { value: "People", title: "People" }, 139 | ], 140 | caption: "Surfer Sunset (Tom Eversley - isorepublic.com)", 141 | }, 142 | { 143 | src: "https://c1.staticflickr.com/9/8707/28868704912_cba5c6600e_b.jpg", 144 | original: "https://c1.staticflickr.com/9/8707/28868704912_cba5c6600e_b.jpg", 145 | width: 320, 146 | height: 213, 147 | tags: [ 148 | { value: "People", title: "People" }, 149 | { value: "Sport", title: "Sport" }, 150 | ], 151 | caption: "Man on BMX (Tom Eversley - isorepublic.com)", 152 | }, 153 | { 154 | src: "https://c4.staticflickr.com/9/8578/28357117603_97a8233cf5_b.jpg", 155 | original: "https://c4.staticflickr.com/9/8578/28357117603_97a8233cf5_b.jpg", 156 | width: 320, 157 | height: 213, 158 | caption: "Ropeman - Thailand (Tom Eversley - isorepublic.com)", 159 | }, 160 | { 161 | src: "https://c4.staticflickr.com/8/7476/28973628875_069e938525_b.jpg", 162 | original: "https://c4.staticflickr.com/8/7476/28973628875_069e938525_b.jpg", 163 | width: 320, 164 | height: 213, 165 | caption: "Time to Think (Tom Eversley - isorepublic.com)", 166 | }, 167 | { 168 | src: "https://c6.staticflickr.com/9/8593/28357129133_f04c73bf1e_b.jpg", 169 | original: "https://c6.staticflickr.com/9/8593/28357129133_f04c73bf1e_b.jpg", 170 | width: 320, 171 | height: 179, 172 | tags: [ 173 | { value: "Nature", title: "Nature" }, 174 | { value: "Fauna", title: "Fauna" }, 175 | ], 176 | caption: "Untitled (Jan Vasek - jeshoots.com)", 177 | }, 178 | { 179 | src: "https://c6.staticflickr.com/9/8893/28897116141_641b88e342_b.jpg", 180 | original: "https://c6.staticflickr.com/9/8893/28897116141_641b88e342_b.jpg", 181 | width: 320, 182 | height: 215, 183 | tags: [{ value: "People", title: "People" }], 184 | caption: "Untitled (moveast.me)", 185 | }, 186 | { 187 | src: "https://c1.staticflickr.com/9/8056/28354485944_148d6a5fc1_b.jpg", 188 | original: "https://c1.staticflickr.com/9/8056/28354485944_148d6a5fc1_b.jpg", 189 | width: 257, 190 | height: 320, 191 | caption: "A photo by 贝莉儿 NG. (unsplash.com)", 192 | }, 193 | { 194 | src: "https://c7.staticflickr.com/9/8824/28868764222_19f3b30773_b.jpg", 195 | original: "https://c7.staticflickr.com/9/8824/28868764222_19f3b30773_b.jpg", 196 | width: 226, 197 | height: 320, 198 | caption: "A photo by Matthew Wiebe. (unsplash.com)", 199 | }, 200 | ]; 201 | -------------------------------------------------------------------------------- /examples/with-yet-another-react-lightbox/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import * as ReactDOMClient from "react-dom/client"; 3 | import "./styles.css"; 4 | 5 | import App from "./App"; 6 | 7 | const rootElement = document.getElementById("root"); 8 | if (!rootElement) throw new Error("rootElement not found"); 9 | 10 | const root = ReactDOMClient.createRoot(rootElement); 11 | 12 | root.render( 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /examples/with-yet-another-react-lightbox/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/with-yet-another-react-lightbox/src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 4 | font-size: 16px; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | } -------------------------------------------------------------------------------- /examples/with-yet-another-react-lightbox/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/**/*" 4 | ], 5 | "compilerOptions": { 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "lib": [ 9 | "dom", 10 | "es2015" 11 | ], 12 | "jsx": "react-jsx", 13 | "target": "es5", 14 | "allowJs": true, 15 | "skipLibCheck": true, 16 | "allowSyntheticDefaultImports": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "module": "esnext", 20 | "moduleResolution": "node", 21 | "resolveJsonModule": true, 22 | "isolatedModules": true, 23 | "noEmit": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: "jsdom", 3 | preset: "jest-puppeteer", 4 | testPathIgnorePatterns: [".publish"], 5 | setupFilesAfterEnv: ["./setup-jest.js"], 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-grid-gallery", 3 | "version": "1.0.1", 4 | "description": "Justified gallery component for React.", 5 | "types": "dist/react-grid-gallery.d.ts", 6 | "main": "dist/react-grid-gallery.cjs.js", 7 | "module": "dist/react-grid-gallery.esm.js", 8 | "browser": "dist/react-grid-gallery.umd.js", 9 | "files": [ 10 | "dist", 11 | "src" 12 | ], 13 | "exports": { 14 | "types": "./dist/react-grid-gallery.d.ts", 15 | "browser": "./dist/react-grid-gallery.umd.js", 16 | "import": "./dist/react-grid-gallery.esm.js", 17 | "require": "./dist/react-grid-gallery.cjs.js" 18 | }, 19 | "sideEffects": false, 20 | "scripts": { 21 | "prepublishOnly": "npm run build", 22 | "clean": "rimraf dist && rimraf node_modules/.cache", 23 | "rollup:build": "rollup -c", 24 | "watch": "rollup -c -w", 25 | "build": "npm run clean && npm run rollup:build", 26 | "test": "jest", 27 | "test:unit": "jest --testPathIgnorePatterns=\\.e2e\\.test\\.js", 28 | "test:e2e": "jest --testPathPattern=\\.e2e\\.test\\.js" 29 | }, 30 | "peerDependencies": { 31 | "react": ">=16.14.0" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.23.2", 35 | "@babel/preset-env": "^7.24.5", 36 | "@babel/preset-react": "^7.22.15", 37 | "@babel/preset-typescript": "^7.23.2", 38 | "@rollup/plugin-babel": "^6.0.4", 39 | "@rollup/plugin-commonjs": "^25.0.5", 40 | "@rollup/plugin-node-resolve": "^15.2.3", 41 | "@rollup/plugin-replace": "^5.0.3", 42 | "@testing-library/jest-dom": "^6.4.2", 43 | "@testing-library/react": "^14.0.0", 44 | "@types/jest": "^29.5.5", 45 | "@types/jest-environment-puppeteer": "^5.0.6", 46 | "@types/jest-image-snapshot": "^6.4.0", 47 | "@types/react": "^18.2.28", 48 | "jest": "^29.7.0", 49 | "jest-environment-jsdom": "^29.7.0", 50 | "jest-image-snapshot": "^6.4.0", 51 | "jest-puppeteer": "^10.0.1", 52 | "puppeteer": "^21.3.8", 53 | "react": "^18.2.0", 54 | "react-dom": "^18.2.0", 55 | "rimraf": "^5.0.5", 56 | "rollup": "^2.79.1", 57 | "rollup-plugin-dts": "^4.2.2", 58 | "rollup-plugin-peer-deps-external": "^2.2.4", 59 | "rollup-plugin-sourcemaps": "^0.6.3", 60 | "rollup-plugin-typescript2": "^0.36.0", 61 | "typescript": "^4.8.2" 62 | }, 63 | "repository": { 64 | "type": "git", 65 | "url": "https://github.com/benhowell/react-grid-gallery.git" 66 | }, 67 | "keywords": [ 68 | "react", 69 | "react-component", 70 | "image", 71 | "images", 72 | "photo", 73 | "photos", 74 | "gallery", 75 | "select", 76 | "selectable", 77 | "justified", 78 | "tags", 79 | "tagging", 80 | "EXIF", 81 | "image gallery", 82 | "photo gallery" 83 | ], 84 | "author": "Ben Howell", 85 | "license": "MIT", 86 | "bugs": { 87 | "url": "https://github.com/benhowell/react-grid-gallery/issues" 88 | }, 89 | "homepage": "https://benhowell.github.io/react-grid-gallery/" 90 | } 91 | -------------------------------------------------------------------------------- /playground/README.md: -------------------------------------------------------------------------------- 1 | # Playground 2 | 3 | There are many possible ways to test your changes during development. Here is one using [parcel](https://parceljs.org/) bundler: 4 | 5 | ```shell 6 | npm install --location=global parcel-bundler 7 | cd playground 8 | parcel index.html 9 | ``` 10 | 11 | More detailed explanation https://dmitripavlutin.com/react-playground-setup/ -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | -------------------------------------------------------------------------------- /playground/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | import { Gallery } from "../src"; 4 | 5 | const image1 = { 6 | src: "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ee/Apples.jpg/320px-Apples.jpg", 7 | width: 320, 8 | height: 480, 9 | }; 10 | 11 | const image2 = { 12 | src: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Bananas.jpg/320px-Bananas.jpg", 13 | width: 320, 14 | height: 213, 15 | }; 16 | 17 | const images = [image1, image2]; 18 | 19 | function App(): JSX.Element { 20 | return ( 21 |
22 | 23 |
24 | ); 25 | } 26 | 27 | ReactDOM.render(, document.getElementById("root")); 28 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from "@rollup/plugin-commonjs"; 2 | import external from "rollup-plugin-peer-deps-external"; 3 | import sourcemaps from "rollup-plugin-sourcemaps"; 4 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 5 | import replace from "@rollup/plugin-replace"; 6 | import typescript from "rollup-plugin-typescript2"; 7 | import dts from "rollup-plugin-dts"; 8 | import pkg from "./package.json"; 9 | 10 | const input = "src/index.ts"; 11 | 12 | export default [ 13 | { 14 | input, 15 | output: { 16 | name: "ReactGridGallery", 17 | file: pkg.browser, 18 | format: "umd", 19 | sourcemap: true, 20 | globals: { 21 | react: "React", 22 | "react-dom": "ReactDOM", 23 | }, 24 | }, 25 | external: ["react", "react-dom"], 26 | plugins: [ 27 | nodeResolve({ browser: true }), 28 | commonjs({ include: /node_modules/ }), 29 | typescript({ 30 | tsconfigOverride: { 31 | compilerOptions: { target: "ES5" }, 32 | }, 33 | }), 34 | sourcemaps(), 35 | replace({ 36 | "process.env.NODE_ENV": JSON.stringify("production"), 37 | preventAssignment: true, 38 | }), 39 | ], 40 | }, 41 | { 42 | input, 43 | output: [ 44 | { file: pkg.main, format: "cjs", exports: "named", sourcemap: true }, 45 | { file: pkg.module, format: "es", exports: "named", sourcemap: true }, 46 | ], 47 | plugins: [external(), typescript(), sourcemaps()], 48 | }, 49 | { 50 | input, 51 | output: [{ file: pkg.types, format: "es" }], 52 | plugins: [dts()], 53 | }, 54 | ]; 55 | -------------------------------------------------------------------------------- /setup-jest.js: -------------------------------------------------------------------------------- 1 | import { TextEncoder, TextDecoder } from "util"; 2 | global.TextEncoder = TextEncoder; 3 | global.TextDecoder = TextDecoder; 4 | -------------------------------------------------------------------------------- /src/CheckButton.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import * as styles from "./styles"; 3 | import { CheckButtonProps } from "./types"; 4 | 5 | export const CheckButton = ({ 6 | isSelected = false, 7 | isVisible = true, 8 | onClick, 9 | color = "#FFFFFFB2", 10 | selectedColor = "#4285F4FF", 11 | hoverColor = "#FFFFFFFF", 12 | }: CheckButtonProps): JSX.Element => { 13 | const [hover, setHover] = useState(false); 14 | 15 | const circleStyle = { display: isSelected ? "block" : "none" }; 16 | const fillColor = isSelected ? selectedColor : hover ? hoverColor : color; 17 | 18 | const handleMouseOver = () => setHover(true); 19 | const handleMouseOut = () => setHover(false); 20 | 21 | return ( 22 |
30 | 37 | 45 | 46 | 47 | 48 | 49 | 57 | 58 | 59 | 60 | 61 |
62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /src/Gallery.tsx: -------------------------------------------------------------------------------- 1 | import { MouseEvent } from "react"; 2 | import { Image } from "./Image"; 3 | import { useContainerWidth } from "./useContainerWidth"; 4 | import { buildLayoutFlat } from "./buildLayout"; 5 | import { Image as ImageInterface, GalleryProps } from "./types"; 6 | import * as styles from "./styles"; 7 | 8 | export const Gallery = ({ 9 | images, 10 | id = "ReactGridGallery", 11 | enableImageSelection = true, 12 | onSelect = () => {}, 13 | rowHeight = 180, 14 | maxRows, 15 | margin = 2, 16 | defaultContainerWidth = 0, 17 | onClick = () => {}, 18 | tileViewportStyle, 19 | thumbnailStyle, 20 | tagStyle, 21 | thumbnailImageComponent, 22 | }: GalleryProps): JSX.Element => { 23 | const { containerRef, containerWidth } = useContainerWidth( 24 | defaultContainerWidth 25 | ); 26 | 27 | const thumbnails = buildLayoutFlat(images, { 28 | containerWidth, 29 | maxRows, 30 | rowHeight, 31 | margin, 32 | }); 33 | 34 | const handleSelect = (index: number, event: MouseEvent) => { 35 | event.preventDefault(); 36 | onSelect(index, images[index], event); 37 | }; 38 | 39 | const handleClick = (index: number, event: MouseEvent) => { 40 | onClick(index, images[index], event); 41 | }; 42 | 43 | return ( 44 |
45 |
46 | {thumbnails.map((item, index) => ( 47 | 61 | ))} 62 |
63 |
64 | ); 65 | }; 66 | 67 | Gallery.displayName = "Gallery"; 68 | -------------------------------------------------------------------------------- /src/Image.tsx: -------------------------------------------------------------------------------- 1 | import { useState, MouseEvent } from "react"; 2 | import { CheckButton } from "./CheckButton"; 3 | import { ImageExtended, ImageProps } from "./types"; 4 | import * as styles from "./styles"; 5 | import { getStyle } from "./styles"; 6 | 7 | export const Image = ({ 8 | item, 9 | thumbnailImageComponent: ThumbnailImageComponent, 10 | isSelectable = true, 11 | thumbnailStyle, 12 | tagStyle, 13 | tileViewportStyle, 14 | margin, 15 | index, 16 | onSelect, 17 | onClick, 18 | }: ImageProps): JSX.Element => { 19 | const styleContext = { item }; 20 | 21 | const [hover, setHover] = useState(false); 22 | 23 | const thumbnailProps = { 24 | key: index, 25 | "data-testid": "grid-gallery-item_thumbnail", 26 | src: item.src, 27 | alt: item.alt ? item.alt : "", 28 | title: typeof item.caption === "string" ? item.caption : null, 29 | style: getStyle(thumbnailStyle, styles.thumbnail, styleContext), 30 | }; 31 | 32 | const handleCheckButtonClick = (event: MouseEvent) => { 33 | if (!isSelectable) { 34 | return; 35 | } 36 | onSelect(index, event); 37 | }; 38 | 39 | const handleViewportClick = (event: MouseEvent) => { 40 | onClick(index, event); 41 | }; 42 | 43 | const thumbnailImageProps = { 44 | item, 45 | index, 46 | margin, 47 | onSelect, 48 | onClick, 49 | isSelectable, 50 | tileViewportStyle, 51 | thumbnailStyle, 52 | tagStyle, 53 | }; 54 | 55 | return ( 56 |
setHover(true)} 60 | onMouseLeave={() => setHover(false)} 61 | style={styles.galleryItem({ margin })} 62 | > 63 |
67 | 72 |
73 | 74 | {!!item.tags && ( 75 |
79 | {item.tags.map((tag, index) => ( 80 |
85 | 86 | {tag.value} 87 | 88 |
89 | ))} 90 |
91 | )} 92 | 93 | {!!item.customOverlay && ( 94 |
98 | {item.customOverlay} 99 |
100 | )} 101 | 102 |
108 | 109 |
115 | {ThumbnailImageComponent ? ( 116 | 120 | ) : ( 121 | 122 | )} 123 |
124 | {item.thumbnailCaption && ( 125 |
129 | {item.thumbnailCaption} 130 |
131 | )} 132 |
133 | ); 134 | }; 135 | -------------------------------------------------------------------------------- /src/buildLayout.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ImageExtended, 3 | Image, 4 | BuildLayoutOptions, 5 | ImageExtendedRow, 6 | } from "./types"; 7 | 8 | const calculateCutOff = ( 9 | items: T[], 10 | totalRowWidth: number, 11 | protrudingWidth: number 12 | ) => { 13 | const cutOff: number[] = []; 14 | let cutSum = 0; 15 | for (let i in items) { 16 | const item = items[i]; 17 | const fractionOfWidth = item.scaledWidth / totalRowWidth; 18 | cutOff[i] = Math.floor(fractionOfWidth * protrudingWidth); 19 | cutSum += cutOff[i]; 20 | } 21 | 22 | let stillToCutOff = protrudingWidth - cutSum; 23 | while (stillToCutOff > 0) { 24 | for (let i in cutOff) { 25 | cutOff[i]++; 26 | stillToCutOff--; 27 | if (stillToCutOff < 0) break; 28 | } 29 | } 30 | return cutOff; 31 | }; 32 | 33 | const getRow = ( 34 | images: T[], 35 | { containerWidth, rowHeight, margin }: BuildLayoutOptions 36 | ): [ImageExtendedRow, T[]] => { 37 | const row: ImageExtendedRow = []; 38 | const imgMargin = 2 * margin; 39 | const items = [...images]; 40 | 41 | let totalRowWidth = 0; 42 | while (items.length > 0 && totalRowWidth < containerWidth) { 43 | const item = items.shift(); 44 | const scaledWidth = Math.floor(rowHeight * (item.width / item.height)); 45 | const extendedItem: ImageExtended = { 46 | ...item, 47 | scaledHeight: rowHeight, 48 | scaledWidth, 49 | viewportWidth: scaledWidth, 50 | marginLeft: 0, 51 | }; 52 | row.push(extendedItem); 53 | totalRowWidth += extendedItem.scaledWidth + imgMargin; 54 | } 55 | 56 | const protrudingWidth = totalRowWidth - containerWidth; 57 | if (row.length > 0 && protrudingWidth > 0) { 58 | const cutoff = calculateCutOff(row, totalRowWidth, protrudingWidth); 59 | for (const i in row) { 60 | const pixelsToRemove = cutoff[i]; 61 | const item = row[i]; 62 | item.marginLeft = -Math.abs(Math.floor(pixelsToRemove / 2)); 63 | item.viewportWidth = item.scaledWidth - pixelsToRemove; 64 | } 65 | } 66 | 67 | return [row, items]; 68 | }; 69 | 70 | const getRows = ( 71 | images: T[], 72 | options: BuildLayoutOptions, 73 | rows: ImageExtendedRow[] = [] 74 | ): ImageExtendedRow[] => { 75 | const [row, imagesLeft] = getRow(images, options); 76 | const nextRows = [...rows, row]; 77 | 78 | if (options.maxRows && nextRows.length >= options.maxRows) { 79 | return nextRows; 80 | } 81 | if (imagesLeft.length) { 82 | return getRows(imagesLeft, options, nextRows); 83 | } 84 | return nextRows; 85 | }; 86 | 87 | export const buildLayout = ( 88 | images: T[], 89 | { containerWidth, maxRows, rowHeight, margin }: BuildLayoutOptions 90 | ): ImageExtendedRow[] => { 91 | rowHeight = typeof rowHeight === "undefined" ? 180 : rowHeight; 92 | margin = typeof margin === "undefined" ? 2 : margin; 93 | 94 | if (!images) return []; 95 | if (!containerWidth) return []; 96 | 97 | const options = { containerWidth, maxRows, rowHeight, margin }; 98 | return getRows(images, options); 99 | }; 100 | 101 | export const buildLayoutFlat = ( 102 | images: T[], 103 | options: BuildLayoutOptions 104 | ): ImageExtendedRow => { 105 | const rows = buildLayout(images, options); 106 | return [].concat.apply([], rows); 107 | }; 108 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export { Gallery } from "./Gallery"; 3 | export { CheckButton } from "./CheckButton"; 4 | export { buildLayout, buildLayoutFlat } from "./buildLayout"; 5 | -------------------------------------------------------------------------------- /src/styles.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from "react"; 2 | import { 3 | ImageExtended, 4 | StyleFunction, 5 | StyleFunctionContext, 6 | StyleProp, 7 | } from "./types"; 8 | 9 | export const getStyle = ( 10 | styleProp: StyleProp | undefined, 11 | fallback: StyleFunction, 12 | context: StyleFunctionContext 13 | ): CSSProperties => { 14 | if (typeof styleProp === "function") { 15 | return styleProp(context); 16 | } 17 | if (typeof styleProp === "object") { 18 | return styleProp; 19 | } 20 | return fallback(context); 21 | }; 22 | 23 | const rotationTransformMap: Record = { 24 | 3: "rotate(180deg)", 25 | 2: "rotateY(180deg)", 26 | 4: "rotate(180deg) rotateY(180deg)", 27 | 5: "rotate(270deg) rotateY(180deg)", 28 | 6: "rotate(90deg)", 29 | 7: "rotate(90deg) rotateY(180deg)", 30 | 8: "rotate(270deg)", 31 | }; 32 | 33 | const SELECTION_MARGIN = 16; 34 | 35 | export const gallery: CSSProperties = { 36 | display: "flex", 37 | flexWrap: "wrap", 38 | }; 39 | 40 | export const thumbnail = ({ item }: { item: ImageExtended }): CSSProperties => { 41 | const rotationTransformValue = rotationTransformMap[item.orientation]; 42 | 43 | const style = { 44 | cursor: "pointer", 45 | maxWidth: "none", 46 | width: item.scaledWidth, 47 | height: item.scaledHeight, 48 | marginLeft: item.marginLeft, 49 | marginTop: 0, 50 | transform: rotationTransformValue, 51 | }; 52 | 53 | if (item.isSelected) { 54 | const ratio = item.scaledWidth / item.scaledHeight; 55 | const viewportHeight = item.scaledHeight - SELECTION_MARGIN * 2; 56 | const viewportWidth = item.viewportWidth - SELECTION_MARGIN * 2; 57 | 58 | let height, width; 59 | if (item.scaledWidth > item.scaledHeight) { 60 | width = item.scaledWidth - SELECTION_MARGIN * 2; 61 | height = Math.floor(width / ratio); 62 | } else { 63 | height = item.scaledHeight - SELECTION_MARGIN * 2; 64 | width = Math.floor(height * ratio); 65 | } 66 | 67 | const marginTop = Math.abs(Math.floor((viewportHeight - height) / 2)); 68 | const marginLeft = Math.abs(Math.floor((viewportWidth - width) / 2)); 69 | 70 | style.width = width; 71 | style.height = height; 72 | style.marginLeft = marginLeft === 0 ? 0 : -marginLeft; 73 | style.marginTop = marginTop === 0 ? 0 : -marginTop; 74 | } 75 | 76 | return style; 77 | }; 78 | 79 | export const tileViewport = ({ 80 | item, 81 | }: { 82 | item: ImageExtended; 83 | }): CSSProperties => { 84 | const styles: CSSProperties = { 85 | width: item.viewportWidth, 86 | height: item.scaledHeight, 87 | overflow: "hidden", 88 | }; 89 | if (item.nano) { 90 | styles.background = `url(${item.nano})`; 91 | styles.backgroundSize = "cover"; 92 | styles.backgroundPosition = "center center"; 93 | } 94 | if (item.isSelected) { 95 | styles.width = item.viewportWidth - SELECTION_MARGIN * 2; 96 | styles.height = item.scaledHeight - SELECTION_MARGIN * 2; 97 | styles.margin = SELECTION_MARGIN; 98 | } 99 | return styles; 100 | }; 101 | 102 | export const customOverlay = ({ 103 | hover, 104 | }: { 105 | hover: boolean; 106 | }): CSSProperties => ({ 107 | pointerEvents: "none", 108 | opacity: hover ? 1 : 0, 109 | position: "absolute", 110 | height: "100%", 111 | width: "100%", 112 | }); 113 | 114 | export const galleryItem = ({ margin }: { margin: number }): CSSProperties => ({ 115 | margin, 116 | WebkitUserSelect: "none", 117 | position: "relative", 118 | background: "#eee", 119 | padding: "0px", 120 | }); 121 | 122 | export const tileOverlay = ({ 123 | showOverlay, 124 | }: { 125 | showOverlay: boolean; 126 | }): CSSProperties => ({ 127 | pointerEvents: "none", 128 | opacity: 1, 129 | position: "absolute", 130 | height: "100%", 131 | width: "100%", 132 | background: showOverlay 133 | ? "linear-gradient(to bottom,rgba(0,0,0,0.26),transparent 56px,transparent)" 134 | : "none", 135 | }); 136 | 137 | export const tileIconBar: CSSProperties = { 138 | pointerEvents: "none", 139 | opacity: 1, 140 | position: "absolute", 141 | height: "36px", 142 | width: "100%", 143 | }; 144 | 145 | export const tileDescription: CSSProperties = { 146 | background: "white", 147 | width: "100%", 148 | margin: 0, 149 | userSelect: "text", 150 | WebkitUserSelect: "text", 151 | MozUserSelect: "text", 152 | overflow: "hidden", 153 | }; 154 | 155 | export const bottomBar: CSSProperties = { 156 | padding: "2px", 157 | pointerEvents: "none", 158 | position: "absolute", 159 | minHeight: "0px", 160 | maxHeight: "160px", 161 | width: "100%", 162 | bottom: "0px", 163 | overflow: "hidden", 164 | }; 165 | 166 | export const tagItemBlock: CSSProperties = { 167 | display: "inline-block", 168 | cursor: "pointer", 169 | pointerEvents: "visible", 170 | margin: "2px", 171 | }; 172 | 173 | export const tagItem = (): CSSProperties => ({ 174 | display: "inline", 175 | padding: ".2em .6em .3em", 176 | fontSize: "75%", 177 | fontWeight: "600", 178 | lineHeight: "1", 179 | color: "yellow", 180 | background: "rgba(0,0,0,0.65)", 181 | textAlign: "center", 182 | whiteSpace: "nowrap", 183 | verticalAlign: "baseline", 184 | borderRadius: ".25em", 185 | }); 186 | 187 | export const checkButton = ({ 188 | isVisible, 189 | }: { 190 | isVisible: boolean; 191 | }): CSSProperties => ({ 192 | visibility: isVisible ? "visible" : "hidden", 193 | background: "none", 194 | float: "left", 195 | width: 36, 196 | height: 36, 197 | border: "none", 198 | padding: 6, 199 | cursor: "pointer", 200 | pointerEvents: "visible", 201 | }); 202 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { MouseEvent, CSSProperties, ReactNode, ComponentType } from "react"; 2 | 3 | type Key = string | number; 4 | 5 | export interface ImageTag { 6 | value: ReactNode; 7 | title: string; 8 | key?: Key; 9 | } 10 | 11 | export interface Image { 12 | key?: Key; 13 | src: string; 14 | width: number; 15 | height: number; 16 | nano?: string; 17 | alt?: string; 18 | tags?: ImageTag[]; 19 | isSelected?: boolean; 20 | caption?: ReactNode; 21 | customOverlay?: ReactNode; 22 | thumbnailCaption?: ReactNode; 23 | orientation?: number; 24 | } 25 | 26 | export type ImageExtended = T & { 27 | scaledWidth: number; 28 | scaledHeight: number; 29 | viewportWidth: number; 30 | marginLeft: number; 31 | }; 32 | 33 | export interface BuildLayoutOptions { 34 | containerWidth: number; 35 | maxRows?: number; 36 | rowHeight?: number; 37 | margin?: number; 38 | } 39 | 40 | export type ImageExtendedRow = ImageExtended[]; 41 | 42 | export type EventHandler = ( 43 | index: number, 44 | item: T, 45 | event: MouseEvent 46 | ) => void; 47 | 48 | export type StyleFunctionContext = { 49 | item: T; 50 | }; 51 | 52 | export type StyleFunction = ( 53 | context: StyleFunctionContext 54 | ) => CSSProperties; 55 | 56 | export type StyleProp = 57 | | CSSProperties 58 | | StyleFunction; 59 | 60 | export interface ImageProps { 61 | item: T; 62 | index: number; 63 | margin: number; 64 | isSelectable: boolean; 65 | onClick: (index: number, event: MouseEvent) => void; 66 | onSelect: (index: number, event: MouseEvent) => void; 67 | tileViewportStyle: StyleProp; 68 | thumbnailStyle: StyleProp; 69 | tagStyle: StyleProp; 70 | height?: number; 71 | thumbnailImageComponent?: ComponentType; 72 | } 73 | 74 | export interface ThumbnailImageComponentImageProps { 75 | key: string | number; 76 | src: string; 77 | alt: string; 78 | title: string | null; 79 | style: CSSProperties; 80 | } 81 | 82 | export type ThumbnailImageProps = 83 | ImageProps & { 84 | imageProps: ThumbnailImageComponentImageProps; 85 | }; 86 | 87 | export interface GalleryProps { 88 | images: T[]; 89 | id?: string; 90 | enableImageSelection?: boolean; 91 | onSelect?: EventHandler; 92 | rowHeight?: number; 93 | maxRows?: number; 94 | margin?: number; 95 | defaultContainerWidth?: number; 96 | onClick?: EventHandler; 97 | tileViewportStyle?: StyleProp; 98 | thumbnailStyle?: StyleProp; 99 | tagStyle?: StyleProp; 100 | thumbnailImageComponent?: ComponentType; 101 | } 102 | 103 | export interface CheckButtonProps { 104 | isSelected: boolean; 105 | isVisible: boolean; 106 | onClick: (event: MouseEvent) => void; 107 | color?: string; 108 | selectedColor?: string; 109 | hoverColor?: string; 110 | } 111 | -------------------------------------------------------------------------------- /src/useContainerWidth.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useCallback, useState } from "react"; 2 | 3 | export function useContainerWidth(defaultContainerWidth: number) { 4 | const ref = useRef(null); 5 | const observerRef = useRef(); 6 | 7 | const [containerWidth, setContainerWidth] = useState(defaultContainerWidth); 8 | 9 | const containerRef = useCallback((node: HTMLElement | null) => { 10 | observerRef.current?.disconnect(); 11 | observerRef.current = undefined; 12 | 13 | ref.current = node; 14 | 15 | const updateWidth = () => { 16 | if (!ref.current) { 17 | return; 18 | } 19 | let width = ref.current.clientWidth; 20 | try { 21 | width = ref.current.getBoundingClientRect().width; 22 | } catch (err) {} 23 | setContainerWidth(Math.floor(width)); 24 | }; 25 | 26 | updateWidth(); 27 | 28 | if (node && typeof ResizeObserver !== "undefined") { 29 | observerRef.current = new ResizeObserver(updateWidth); 30 | observerRef.current.observe(node); 31 | } 32 | }, []); 33 | 34 | return { containerRef, containerWidth }; 35 | } 36 | -------------------------------------------------------------------------------- /test/Gallery.e2e.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment puppeteer 3 | */ 4 | // @ts-ignore 5 | declare var ReactGridGallery, ReactDOM, React; 6 | 7 | import { toMatchImageSnapshot } from "jest-image-snapshot"; 8 | expect.extend({ toMatchImageSnapshot }); 9 | import { GalleryProps } from "../src"; 10 | import { images } from "./images"; 11 | 12 | const transparentPixel = 13 | "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="; 14 | const redPixel = 15 | "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8L+7yHwAF0gJbauK6vQAAAABJRU5ErkJggg=="; 16 | const bluePixel = 17 | "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mO0jfr3HwAFBwKW6YMOIwAAAABJRU5ErkJggg=="; 18 | 19 | const tags1 = [ 20 | // avoid using any symbols because of the difference in font rendering on the local OS and CI ubuntu 21 | { value: " ", title: " " }, 22 | { value: " ", title: " " }, 23 | ]; 24 | 25 | const getGalleryBrowserBuildPath = () => { 26 | try { 27 | return require.resolve("./../dist/react-grid-gallery.umd.js"); 28 | } catch (error) { 29 | if (error.code === "MODULE_NOT_FOUND") { 30 | console.error( 31 | `Gallery build file isn't found! Don't forget to perform "npm run build" before running e2e tests.` 32 | ); 33 | } 34 | throw error; 35 | } 36 | }; 37 | 38 | const renderGallery = async ( 39 | props: GalleryProps, 40 | options: { reactVersion?: number; timeout?: number; styles?: string } = {} 41 | ) => { 42 | const reactVersion = options.reactVersion || 18; 43 | const timeout = options.timeout || 10000; 44 | const styles = options.styles || ""; 45 | 46 | await page.setContent('
'); 47 | const reactScript = `https://unpkg.com/react@${reactVersion}/umd/react.development.js`; 48 | await page.addScriptTag({ url: reactScript }); 49 | const reactDOMScript = `https://unpkg.com/react-dom@${reactVersion}/umd/react-dom.development.js`; 50 | await page.addScriptTag({ url: reactDOMScript }); 51 | await page.addScriptTag({ path: getGalleryBrowserBuildPath() }); 52 | if (styles) { 53 | await page.addStyleTag({ content: styles }); 54 | } 55 | 56 | const latestReactRender = (props: GalleryProps) => { 57 | const root = ReactDOM.createRoot(document.getElementById("root")); 58 | root.render(React.createElement(ReactGridGallery.Gallery, props, null)); 59 | }; 60 | 61 | const previousReactRender = (props: GalleryProps) => { 62 | const root = document.getElementById("root"); 63 | ReactDOM.render( 64 | React.createElement(ReactGridGallery.Gallery, props, null), 65 | root 66 | ); 67 | }; 68 | 69 | const renderFunction = 70 | reactVersion >= 18 ? latestReactRender : previousReactRender; 71 | await page.evaluate(renderFunction, props); 72 | 73 | const imagesHaveLoaded = () => 74 | Array.from(document.images).every((i) => i.complete); 75 | await page.waitForFunction(imagesHaveLoaded, { timeout }); 76 | }; 77 | 78 | describe("Gallery is visually correct", () => { 79 | beforeEach(async () => { 80 | await page.setViewport({ width: 800, height: 800 }); 81 | }); 82 | 83 | it("on react16", async () => { 84 | await renderGallery({ images }, { reactVersion: 16 }); 85 | 86 | expect(await page.screenshot()).toMatchImageSnapshot(); 87 | }); 88 | 89 | it("on react17", async () => { 90 | await renderGallery({ images }, { reactVersion: 17 }); 91 | 92 | expect(await page.screenshot()).toMatchImageSnapshot(); 93 | }); 94 | 95 | it("on react18", async () => { 96 | await renderGallery({ images }, { reactVersion: 18 }); 97 | 98 | expect(await page.screenshot()).toMatchImageSnapshot(); 99 | }); 100 | 101 | it("after viewport resize", async () => { 102 | const sizes = [ 103 | [320, 570], 104 | [360, 640], 105 | [480, 854], 106 | [960, 540], 107 | [1024, 640], 108 | [1366, 768], 109 | [1920, 1080], 110 | ]; 111 | await renderGallery({ images }); 112 | 113 | for (const [width, height] of sizes) { 114 | await page.setViewport({ width, height }); 115 | 116 | expect(await page.screenshot()).toMatchImageSnapshot(); 117 | } 118 | }); 119 | 120 | it("when rowHeight is 100", async () => { 121 | await renderGallery({ images, rowHeight: 100 }); 122 | 123 | expect(await page.screenshot()).toMatchImageSnapshot(); 124 | }); 125 | 126 | it("when margin is 10", async () => { 127 | await renderGallery({ images, margin: 10 }); 128 | 129 | expect(await page.screenshot()).toMatchImageSnapshot(); 130 | }); 131 | 132 | it("when maxRows is 2", async () => { 133 | await renderGallery({ images, maxRows: 2 }); 134 | 135 | expect(await page.screenshot()).toMatchImageSnapshot(); 136 | }); 137 | 138 | it("when images are selected", async () => { 139 | const imagesWithSelection = images.map((i) => ({ ...i, isSelected: true })); 140 | 141 | await renderGallery({ images: imagesWithSelection }); 142 | 143 | expect(await page.screenshot()).toMatchImageSnapshot(); 144 | }); 145 | 146 | it("when images are transparent", async () => { 147 | const transparentImages = images.map((i) => ({ 148 | ...i, 149 | src: transparentPixel, 150 | })); 151 | 152 | await renderGallery({ images: transparentImages }); 153 | 154 | expect(await page.screenshot()).toMatchImageSnapshot(); 155 | }); 156 | 157 | it("when nano prop passed", async () => { 158 | const imagesWithNano = images.map((i, index) => ({ 159 | ...i, 160 | src: transparentPixel, 161 | nano: index % 2 ? redPixel : bluePixel, 162 | })); 163 | 164 | await renderGallery({ images: imagesWithNano }); 165 | 166 | expect(await page.screenshot()).toMatchImageSnapshot(); 167 | }); 168 | 169 | it("when images have tags", async () => { 170 | const imagesWithTags = images.map((i, index) => ({ 171 | ...i, 172 | tags: index % 2 ? tags1 : [], 173 | })); 174 | 175 | await renderGallery({ images: imagesWithTags }); 176 | 177 | expect(await page.screenshot()).toMatchImageSnapshot(); 178 | }); 179 | 180 | it("when images have tags and tagStyle prop passed", async () => { 181 | const imagesWithTags = images.map((i, index) => ({ 182 | ...i, 183 | tags: index % 2 ? tags1 : [], 184 | })); 185 | const tagStyle = { 186 | background: "white", 187 | padding: 10, 188 | opacity: 1, 189 | }; 190 | 191 | await renderGallery({ images: imagesWithTags, tagStyle }); 192 | 193 | expect(await page.screenshot()).toMatchImageSnapshot(); 194 | }); 195 | 196 | it("when container width is decimal", async () => { 197 | const styles = "#root { width: 474.7px }"; 198 | 199 | await renderGallery({ images }, { styles }); 200 | 201 | expect(await page.screenshot()).toMatchImageSnapshot(); 202 | }); 203 | }); 204 | -------------------------------------------------------------------------------- /test/Gallery.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from "react"; 2 | import "@testing-library/jest-dom"; 3 | import { render, fireEvent, screen } from "@testing-library/react"; 4 | import { renderToString } from "react-dom/server"; 5 | import { Gallery } from "../src/Gallery"; 6 | import { ThumbnailImageProps } from "../src/types"; 7 | 8 | const image1 = { 9 | src: "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ee/Apples.jpg/320px-Apples.jpg", 10 | width: 320, 11 | height: 480, 12 | }; 13 | 14 | const image2 = { 15 | src: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Bananas.jpg/320px-Bananas.jpg", 16 | width: 320, 17 | height: 213, 18 | }; 19 | 20 | const getItems = () => screen.getAllByTestId("grid-gallery-item"); 21 | const getItem = () => screen.getByTestId("grid-gallery-item"); 22 | const getItemThumbnail = () => 23 | screen.getByTestId("grid-gallery-item_thumbnail"); 24 | const getItemViewport = () => screen.getByTestId("grid-gallery-item_viewport"); 25 | const getItemCheckButton = () => 26 | screen.getByTestId("grid-gallery-item_check-button"); 27 | 28 | // emulating server-side rendering 29 | // https://github.com/testing-library/react-testing-library/issues/561#issuecomment-1189796200 30 | const renderStatic = (element: ReactElement) => 31 | render(
); 32 | 33 | describe("Gallery Component", () => { 34 | beforeEach(() => { 35 | jest.clearAllMocks(); 36 | 37 | // define clientWidth for gallery root element 38 | Object.defineProperty(Element.prototype, "clientWidth", { value: 400 }); 39 | // @ts-ignore 40 | Element.prototype.getBoundingClientRect = jest.fn(() => ({ width: 400 })); 41 | }); 42 | 43 | it("should assign default id value", () => { 44 | render(); 45 | 46 | expect(document.querySelector("#ReactGridGallery")).toBeInTheDocument(); 47 | }); 48 | 49 | it("should assign provided id value instead of default one", () => { 50 | const id = "customId"; 51 | 52 | render(); 53 | 54 | expect(document.querySelector("#ReactGridGallery")).not.toBeInTheDocument(); 55 | expect(document.getElementById(id)).toBeInTheDocument(); 56 | }); 57 | 58 | it("should set item height based on rowHeight prop", () => { 59 | render(); 60 | 61 | expect(getItemThumbnail()).toHaveStyle({ height: "100px" }); 62 | }); 63 | 64 | it("should render all provided images when maxRows is not passed", () => { 65 | const images = Array.from({ length: 20 }, () => image1); 66 | 67 | render(); 68 | 69 | expect(getItems().length).toEqual(20); 70 | }); 71 | 72 | it("should render only some of provided images when maxRows is passed", () => { 73 | const images = Array.from({ length: 20 }, () => image1); 74 | 75 | render(); 76 | 77 | expect(getItems().length).toBeLessThan(20); 78 | }); 79 | 80 | it("should render element with custom properties provided via thumbnailImageComponent prop", () => { 81 | const thumbnailImageComponent = (props: ThumbnailImageProps) => ( 82 | 83 | ); 84 | 85 | render( 86 | 90 | ); 91 | 92 | expect(getItemThumbnail()).toHaveClass("lazyload"); 93 | }); 94 | 95 | it("should set styles provided via thumbnailStyle prop on thumbnail element", () => { 96 | const thumbnailStyle = { background: "black", opacity: 0.42 }; 97 | 98 | render( thumbnailStyle} />); 99 | 100 | expect(getItemThumbnail()).toHaveStyle(thumbnailStyle); 101 | }); 102 | 103 | it("should set styles provided via tileViewportStyle prop on viewport element", () => { 104 | const tileViewportStyle = { background: "black", opacity: 0.42 }; 105 | 106 | render( 107 | tileViewportStyle} /> 108 | ); 109 | 110 | expect(getItemViewport()).toHaveStyle(tileViewportStyle); 111 | }); 112 | 113 | it("should set styles provided via tagStyle prop on tag element", () => { 114 | const tagStyle = { background: "black", opacity: 0.42 }; 115 | const image = { 116 | ...image1, 117 | tags: [{ value: "Vegetable", title: "Vegetable" }], 118 | }; 119 | 120 | render(); 121 | 122 | expect(screen.getByText("Vegetable")).toHaveStyle(tagStyle); 123 | }); 124 | 125 | it("should render all passed images after rerender", () => { 126 | const { rerender } = render(); 127 | 128 | expect(getItems().length).toEqual(1); 129 | 130 | rerender(); 131 | 132 | expect(getItems().length).toEqual(2); 133 | }); 134 | 135 | it("should render thumbnails on server-side when defaultContainerWidth prop is passed", () => { 136 | renderStatic( 137 | 138 | ); 139 | 140 | expect(getItems().length).toEqual(2); 141 | }); 142 | 143 | describe("Image Options", () => { 144 | it("should set thumbnail image src attribute based on src prop", () => { 145 | render(); 146 | 147 | expect(getItemThumbnail()).toHaveAttribute("src", image1.src); 148 | }); 149 | 150 | it("should set thumbnail image alt attribute based on alt prop", () => { 151 | const alt = "Image of apples"; 152 | const image = { ...image1, alt }; 153 | 154 | render(); 155 | 156 | expect(getItemThumbnail()).toHaveAttribute("alt", alt); 157 | }); 158 | 159 | it("should set thumbnail image title attribute based on caption prop", () => { 160 | const caption = "Apples"; 161 | const image = { ...image1, caption }; 162 | 163 | render(); 164 | 165 | expect(getItemThumbnail()).toHaveAttribute("title", caption); 166 | }); 167 | 168 | it("should not set thumbnail image title attribute when caption prop value is a react element", () => { 169 | const caption = Apples; 170 | const image = { ...image1, caption }; 171 | 172 | render(); 173 | 174 | expect(getItemThumbnail()).not.toHaveAttribute("title"); 175 | }); 176 | 177 | it("should render tag element", () => { 178 | const image = { ...image1, tags: [{ value: "Fruit", title: "Fruit" }] }; 179 | 180 | render(); 181 | 182 | expect(screen.getByText("Fruit")).toBeVisible(); 183 | }); 184 | 185 | it("should render tag element when tag value is a react element", () => { 186 | const tag1 = { 187 | value: Fruit, 188 | title: "Fruit", 189 | key: "1", 190 | }; 191 | const image = { 192 | ...image1, 193 | tags: [tag1], 194 | }; 195 | 196 | render(); 197 | 198 | expect(screen.getByText("Fruit")).toBeInTheDocument(); 199 | }); 200 | 201 | it("should add background to viewport element based on nano prop", () => { 202 | const nano = 203 | "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAH0lEQVQYV2NkQAX/GZH4/xkYGBhhAmAOSBJEwDkgAQCCrgQEjpMcPgAAAABJRU5ErkJggg=="; 204 | const image = { ...image1, nano }; 205 | 206 | render(); 207 | 208 | expect(getItemViewport()).toHaveStyle(`background: url(${nano})`); 209 | }); 210 | 211 | it("should not show overlay when gallery item is not hovered over", () => { 212 | const customOverlay = Custom Overlay; 213 | const image = { ...image1, customOverlay }; 214 | 215 | render(); 216 | 217 | expect(screen.getByText("Custom Overlay")).not.toBeVisible(); 218 | }); 219 | 220 | it("should show overlay when gallery item is hovered over", () => { 221 | const customOverlay = Custom Overlay; 222 | const image = { ...image1, customOverlay }; 223 | 224 | render(); 225 | fireEvent.mouseOver(getItem()); 226 | 227 | expect(screen.getByText("Custom Overlay")).toBeVisible(); 228 | }); 229 | 230 | it("should add thumbnail caption provided via thumbnailCaption prop", () => { 231 | const thumbnailCaption = Thumbnail Caption; 232 | const image = { ...image1, thumbnailCaption }; 233 | 234 | render(); 235 | 236 | expect(screen.getByText("Thumbnail Caption")).toBeVisible(); 237 | }); 238 | 239 | it("should transform image based on orientation prop value", () => { 240 | const image = { ...image1, orientation: 3 }; 241 | 242 | render(); 243 | 244 | expect(getItemThumbnail()).toHaveStyle(`transform: rotate(180deg)`); 245 | }); 246 | }); 247 | 248 | describe("Selection", () => { 249 | it("should show check-button when item is selected", () => { 250 | const image = { ...image1, isSelected: true }; 251 | 252 | render(); 253 | 254 | expect(getItemCheckButton()).toBeVisible(); 255 | }); 256 | 257 | it("should not show check-button when item is not selected", () => { 258 | render(); 259 | 260 | expect(getItemCheckButton()).not.toBeVisible(); 261 | }); 262 | 263 | it("should show check-button when item is hovered over and image selection is enabled", () => { 264 | render(); 265 | fireEvent.mouseOver(getItem()); 266 | 267 | expect(getItemCheckButton()).toBeVisible(); 268 | }); 269 | 270 | it("should not show check-button when item is hovered over and image selection isn't enabled", () => { 271 | render(); 272 | fireEvent.mouseOver(getItem()); 273 | 274 | expect(getItemCheckButton()).not.toBeVisible(); 275 | }); 276 | 277 | it("should call onSelect with index and image object arguments passed", () => { 278 | const handleSelect = jest.fn(); 279 | 280 | render( 281 | 286 | ); 287 | fireEvent.click(getItemCheckButton()); 288 | 289 | expect(handleSelect.mock.calls[0][0]).toEqual(0); 290 | expect(handleSelect.mock.calls[0][1]).toEqual(image1); 291 | }); 292 | }); 293 | }); 294 | -------------------------------------------------------------------------------- /test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-after-viewport-resize-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-after-viewport-resize-1-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-after-viewport-resize-2-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-after-viewport-resize-2-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-after-viewport-resize-3-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-after-viewport-resize-3-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-after-viewport-resize-4-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-after-viewport-resize-4-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-after-viewport-resize-5-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-after-viewport-resize-5-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-after-viewport-resize-6-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-after-viewport-resize-6-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-after-viewport-resize-7-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-after-viewport-resize-7-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-on-react-16-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-on-react-16-1-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-on-react-17-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-on-react-17-1-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-on-react-18-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-on-react-18-1-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-when-container-width-is-decimal-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-when-container-width-is-decimal-1-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-when-images-are-selected-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-when-images-are-selected-1-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-when-images-are-transparent-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-when-images-are-transparent-1-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-when-images-have-tags-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-when-images-have-tags-1-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-when-images-have-tags-and-tag-style-prop-passed-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-when-images-have-tags-and-tag-style-prop-passed-1-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-when-margin-is-10-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-when-margin-is-10-1-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-when-max-rows-is-2-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-when-max-rows-is-2-1-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-when-nano-prop-passed-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-when-nano-prop-passed-1-snap.png -------------------------------------------------------------------------------- /test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-when-row-height-is-100-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benhowell/react-grid-gallery/c55cb83b1939955a9f1ee5cdc4ccaaa75f6693ae/test/__image_snapshots__/gallery-e-2-e-test-ts-gallery-is-visually-correct-when-row-height-is-100-1-snap.png -------------------------------------------------------------------------------- /test/buildLayout.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buildLayout, 3 | buildLayoutFlat, 4 | BuildLayoutOptions, 5 | Image, 6 | } from "../src"; 7 | 8 | const image100x100 = { 9 | src: "", 10 | width: 100, 11 | height: 100, 12 | }; 13 | 14 | describe("buildLayout", () => { 15 | it("should return empty array when images param not passed", () => { 16 | const images = undefined as []; 17 | const options = { containerWidth: 1000 }; 18 | 19 | const rows = buildLayout(images, options); 20 | 21 | expect(rows).toEqual([]); 22 | }); 23 | 24 | it("should return empty array when containerWidth isn't defined", () => { 25 | const images = [image100x100]; 26 | const options = {} as BuildLayoutOptions; 27 | 28 | const rows = buildLayout(images, options); 29 | 30 | expect(rows).toEqual([]); 31 | }); 32 | 33 | it("should return empty array when containerWidth is 0", () => { 34 | const images = [image100x100]; 35 | const options = { containerWidth: 0 }; 36 | 37 | const rows = buildLayout(images, options); 38 | 39 | expect(rows).toEqual([]); 40 | }); 41 | 42 | it("should not modify passed images array", () => { 43 | const images = [image100x100]; 44 | const options = { containerWidth: 100 }; 45 | 46 | const rows = buildLayout(images, options); 47 | 48 | expect(rows).not.toBe(images); 49 | }); 50 | 51 | it("should return custom image attributes", () => { 52 | interface MyImage extends Image { 53 | customAttr: string; 54 | } 55 | const image: MyImage = { 56 | customAttr: "imageId", 57 | src: "", 58 | width: 100, 59 | height: 100, 60 | }; 61 | const options = { containerWidth: 100 }; 62 | 63 | const rows = buildLayout([image], options); 64 | 65 | expect(rows[0][0].customAttr).toBe("imageId"); 66 | }); 67 | 68 | it("should limit number of items when maxRows param passed", () => { 69 | const images = [image100x100, image100x100, image100x100]; 70 | const options = { 71 | containerWidth: 100, 72 | rowHeight: 100, 73 | margin: 0, 74 | maxRows: 1, 75 | }; 76 | 77 | const rows = buildLayout(images, options); 78 | 79 | expect(rows.length).toEqual(1); 80 | }); 81 | 82 | it("should not compress image when it's narrower than container", () => { 83 | const images = [image100x100]; 84 | const options = { containerWidth: 200, rowHeight: 100, margin: 0 }; 85 | 86 | const rows = buildLayout(images, options); 87 | 88 | expect(rows).toEqual([ 89 | [ 90 | expect.objectContaining({ 91 | viewportWidth: 100, 92 | marginLeft: 0, 93 | }), 94 | ], 95 | ]); 96 | }); 97 | 98 | it("should compress image and calculate cut off when it's wider the container", () => { 99 | const images = [image100x100]; 100 | const options = { containerWidth: 50, rowHeight: 100, margin: 0 }; 101 | 102 | const rows = buildLayout(images, options); 103 | 104 | expect(rows).toEqual([ 105 | [ 106 | expect.objectContaining({ 107 | viewportWidth: 50, 108 | marginLeft: -25, 109 | }), 110 | ], 111 | ]); 112 | }); 113 | 114 | it("should build a single row when images fit into it", () => { 115 | const images = [image100x100, image100x100, image100x100]; 116 | const options = { containerWidth: 201, rowHeight: 100, margin: 0 }; 117 | 118 | const rows = buildLayout(images, options); 119 | 120 | expect(rows).toEqual([ 121 | [ 122 | expect.objectContaining({ marginLeft: -16 }), 123 | expect.objectContaining({ marginLeft: -16 }), 124 | expect.objectContaining({ marginLeft: -16 }), 125 | ], 126 | ]); 127 | }); 128 | 129 | it("should build multiple rows when images don't fit into a single row", () => { 130 | const images = [image100x100, image100x100, image100x100]; 131 | const options = { containerWidth: 200, rowHeight: 100, margin: 0 }; 132 | 133 | const rows = buildLayout(images, options); 134 | 135 | expect(rows.length).toEqual(2); 136 | }); 137 | 138 | it("should build multiple rows when images could fit into a single row but also the margin is specified", () => { 139 | const images = [image100x100, image100x100, image100x100]; 140 | const options = { containerWidth: 200, rowHeight: 100, margin: 5 }; 141 | 142 | const rows = buildLayout(images, options); 143 | 144 | expect(rows).toEqual([ 145 | [ 146 | expect.objectContaining({ 147 | marginLeft: -5, 148 | viewportWidth: 90, 149 | }), 150 | expect.objectContaining({ 151 | marginLeft: -5, 152 | viewportWidth: 90, 153 | }), 154 | ], 155 | [ 156 | expect.objectContaining({ 157 | marginLeft: 0, 158 | viewportWidth: 100, 159 | }), 160 | ], 161 | ]); 162 | }); 163 | 164 | it("should fit multiple images into one row and calculate scaled width when rowHeight is specified", () => { 165 | const options = { containerWidth: 201, rowHeight: 50, margin: 0 }; 166 | const images = [image100x100, image100x100, image100x100, image100x100]; 167 | 168 | const rows = buildLayout(images, options); 169 | 170 | expect(rows).toEqual([ 171 | [ 172 | expect.objectContaining({ 173 | scaledWidth: 50, 174 | viewportWidth: 50, 175 | }), 176 | expect.objectContaining({ 177 | scaledWidth: 50, 178 | viewportWidth: 50, 179 | }), 180 | expect.objectContaining({ 181 | scaledWidth: 50, 182 | viewportWidth: 50, 183 | }), 184 | expect.objectContaining({ 185 | scaledWidth: 50, 186 | viewportWidth: 50, 187 | }), 188 | ], 189 | ]); 190 | }); 191 | }); 192 | 193 | describe("buildLayoutFlat", () => { 194 | it("should return all row items as a flat list", () => { 195 | const images = [image100x100, image100x100, image100x100]; 196 | const options = { containerWidth: 200, rowHeight: 100, margin: 0 }; 197 | 198 | const rows = buildLayoutFlat(images, options); 199 | 200 | expect(rows.length).toEqual(3); 201 | }); 202 | }); 203 | -------------------------------------------------------------------------------- /test/images.ts: -------------------------------------------------------------------------------- 1 | export const images = [ 2 | { 3 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=E14F5D", 4 | width: 320, 5 | height: 174, 6 | caption: "After Rain (Jeshu John - designerspics.com)", 7 | }, 8 | { 9 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=FFC349", 10 | width: 320, 11 | height: 212, 12 | caption: "Boats (Jeshu John - designerspics.com)", 13 | }, 14 | { 15 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=A6BC3E", 16 | width: 320, 17 | height: 212, 18 | caption: "Color Pencils (Jeshu John - designerspics.com)", 19 | }, 20 | { 21 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=92A43B", 22 | width: 320, 23 | height: 213, 24 | caption: "Red Apples with other Red Fruit (foodiesfeed.com)", 25 | }, 26 | { 27 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=1873D3", 28 | width: 320, 29 | height: 183, 30 | caption: "37H (gratispgraphy.com)", 31 | }, 32 | { 33 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=FFE787", 34 | width: 240, 35 | height: 320, 36 | caption: "8H (gratisography.com)", 37 | }, 38 | { 39 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=FFF8BC", 40 | width: 320, 41 | height: 190, 42 | caption: "286H (gratisography.com)", 43 | }, 44 | { 45 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=FFAF92", 46 | width: 320, 47 | height: 148, 48 | caption: "315H (gratisography.com)", 49 | }, 50 | { 51 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=FF876F", 52 | width: 320, 53 | height: 213, 54 | caption: "201H (gratisography.com)", 55 | }, 56 | { 57 | alt: "Big Ben - London", 58 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=492742", 59 | width: 248, 60 | height: 320, 61 | caption: "Big Ben (Tom Eversley - isorepublic.com)", 62 | }, 63 | { 64 | alt: "Red Zone - Paris", 65 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=E14F5D", 66 | width: 320, 67 | height: 113, 68 | caption: "Red Zone - Paris (Tom Eversley - isorepublic.com)", 69 | }, 70 | { 71 | alt: "Wood Glass", 72 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=FFC349", 73 | width: 313, 74 | height: 320, 75 | caption: "Wood Glass (Tom Eversley - isorepublic.com)", 76 | }, 77 | { 78 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=A6BC3E", 79 | width: 320, 80 | height: 213, 81 | caption: "Flower Interior Macro (Tom Eversley - isorepublic.com)", 82 | }, 83 | { 84 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=92A43B", 85 | width: 320, 86 | height: 194, 87 | caption: "Old Barn (Tom Eversley - isorepublic.com)", 88 | }, 89 | { 90 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=FFF8BC", 91 | width: 320, 92 | height: 213, 93 | caption: "Cosmos Flower Macro (Tom Eversley - isorepublic.com)", 94 | }, 95 | { 96 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=FFC562", 97 | width: 271, 98 | height: 320, 99 | caption: "Orange Macro (Tom Eversley - isorepublic.com)", 100 | }, 101 | { 102 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=FF6D74", 103 | width: 320, 104 | height: 213, 105 | caption: "Surfer Sunset (Tom Eversley - isorepublic.com)", 106 | }, 107 | { 108 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=4FDDC3", 109 | width: 320, 110 | height: 213, 111 | caption: "Man on BMX (Tom Eversley - isorepublic.com)", 112 | }, 113 | { 114 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=61A8E8", 115 | width: 320, 116 | height: 213, 117 | caption: "Ropeman - Thailand (Tom Eversley - isorepublic.com)", 118 | }, 119 | { 120 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=A2E0DB", 121 | width: 320, 122 | height: 213, 123 | caption: "Time to Think (Tom Eversley - isorepublic.com)", 124 | }, 125 | { 126 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=FEE1D3", 127 | width: 320, 128 | height: 179, 129 | caption: "Untitled (Jan Vasek - jeshoots.com)", 130 | }, 131 | { 132 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=F55E55", 133 | width: 320, 134 | height: 215, 135 | caption: "Untitled (moveast.me)", 136 | }, 137 | { 138 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=FF857A", 139 | width: 257, 140 | height: 320, 141 | caption: "A photo by 贝莉儿 NG. (unsplash.com)", 142 | }, 143 | { 144 | src: "https://www.schemecolor.com/images/color-image-thumb.php?tx&w=250&h=250&hex=492742", 145 | width: 226, 146 | height: 320, 147 | caption: "A photo by Matthew Wiebe. (unsplash.com)", 148 | }, 149 | ]; 150 | -------------------------------------------------------------------------------- /test/styles.test.ts: -------------------------------------------------------------------------------- 1 | import { thumbnail, tileViewport, getStyle } from "../src/styles"; 2 | import { ImageExtended } from "../src"; 3 | 4 | const baseItem: ImageExtended = { 5 | src: "", 6 | isSelected: false, 7 | width: 100, 8 | height: 100, 9 | scaledWidth: 100, 10 | scaledHeight: 100, 11 | marginLeft: 0, 12 | viewportWidth: 100, 13 | }; 14 | 15 | describe("styles", () => { 16 | describe("thumbnail", () => { 17 | it("should add transform property based on item.orientation", () => { 18 | const item = { ...baseItem, orientation: 3 }; 19 | 20 | const result = thumbnail({ item }); 21 | 22 | expect(result.transform).toEqual("rotate(180deg)"); 23 | }); 24 | 25 | it("should return styles when image is not selected", () => { 26 | const result = thumbnail({ item: baseItem }); 27 | 28 | expect(result).toEqual({ 29 | cursor: "pointer", 30 | maxWidth: "none", 31 | height: 100, 32 | marginLeft: 0, 33 | marginTop: 0, 34 | width: 100, 35 | }); 36 | }); 37 | 38 | it("should return styles when image is horizontal and selected", () => { 39 | const item = { 40 | ...baseItem, 41 | scaledWidth: 200, 42 | viewportWidth: 200, 43 | isSelected: true, 44 | }; 45 | 46 | const result = thumbnail({ item }); 47 | 48 | expect(result).toEqual({ 49 | cursor: "pointer", 50 | maxWidth: "none", 51 | height: 84, 52 | marginLeft: 0, 53 | marginTop: -8, 54 | width: 168, 55 | }); 56 | }); 57 | 58 | it("should return styles when image is vertical and selected", () => { 59 | const item = { 60 | ...baseItem, 61 | scaledWidth: 50, 62 | viewportWidth: 50, 63 | isSelected: true, 64 | }; 65 | 66 | const result = thumbnail({ item }); 67 | 68 | expect(result).toEqual({ 69 | cursor: "pointer", 70 | maxWidth: "none", 71 | height: 68, 72 | marginLeft: -8, 73 | marginTop: 0, 74 | width: 34, 75 | }); 76 | }); 77 | }); 78 | 79 | describe("tileViewport", () => { 80 | it("should add background properties based on item.nano", () => { 81 | const item = { ...baseItem, nano: "data:image/png;base64" }; 82 | 83 | const result = tileViewport({ item }); 84 | 85 | expect(result).toEqual( 86 | expect.objectContaining({ 87 | background: "url(data:image/png;base64)", 88 | backgroundSize: "cover", 89 | backgroundPosition: "center center", 90 | }) 91 | ); 92 | }); 93 | 94 | it("should return styles when item is not selected", () => { 95 | const result = tileViewport({ item: baseItem }); 96 | 97 | expect(result).toEqual({ 98 | height: 100, 99 | overflow: "hidden", 100 | width: 100, 101 | }); 102 | }); 103 | 104 | it("should return styles when item is selected", () => { 105 | const item = { ...baseItem, isSelected: true }; 106 | 107 | const result = tileViewport({ item }); 108 | 109 | expect(result).toEqual({ 110 | height: 68, 111 | margin: 16, 112 | overflow: "hidden", 113 | width: 68, 114 | }); 115 | }); 116 | }); 117 | 118 | describe("getStyle", () => { 119 | it("should return styles provided by style prop function", () => { 120 | const styleProp = () => ({ display: "flex" }); 121 | const fallback = () => ({ display: "none" }); 122 | 123 | const style = getStyle(styleProp, fallback, { item: baseItem }); 124 | 125 | expect(style).toEqual({ display: "flex" }); 126 | }); 127 | 128 | it("should return styles provided by style prop object", () => { 129 | const styleProp = { display: "flex" }; 130 | const fallback = () => ({ display: "none" }); 131 | 132 | const style = getStyle(styleProp, fallback, { item: baseItem }); 133 | 134 | expect(style).toEqual({ display: "flex" }); 135 | }); 136 | 137 | it("should return styles provided by style fallback function", () => { 138 | const fallback = () => ({ display: "none" }); 139 | 140 | const style = getStyle(undefined, fallback, { item: baseItem }); 141 | 142 | expect(style).toEqual({ display: "none" }); 143 | }); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "ESNext", 5 | "sourceMap": true, 6 | "jsx": "react-jsx", 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "noImplicitAny": true, 10 | "removeComments": true 11 | }, 12 | "include": ["src/**/*", "test/**/*"], 13 | "exclude": ["node_modules"] 14 | } 15 | --------------------------------------------------------------------------------