├── .babelrc ├── .circleci └── config.yml ├── .coveralls.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── example ├── .env ├── .gitignore ├── README.md ├── package.json ├── public │ ├── circle.yml │ ├── favicon.ico │ └── index.html ├── src │ ├── components │ │ ├── Basic.js │ │ ├── EnlargedImageContainerDimensions.js │ │ ├── ExampleHint.js │ │ ├── ExternalEnlargedImage.js │ │ ├── FixedWidthSmallImage.js │ │ ├── Header.js │ │ ├── Hint.js │ │ ├── Lens.js │ │ ├── ReactSlick.js │ │ ├── ReactSlickIntegration.js │ │ ├── ResponsiveImages.js │ │ └── SpacedSpan.js │ ├── images │ │ ├── header │ │ │ ├── github-logo.png │ │ │ └── npm-logo.png │ │ ├── versace-blue │ │ │ ├── back-1020.jpg │ │ │ ├── back-1200.jpg │ │ │ ├── back-1426.jpg │ │ │ ├── back-500.jpg │ │ │ ├── back-779.jpg │ │ │ ├── back-orig-1426x2000.png │ │ │ ├── front-1020.jpg │ │ │ ├── front-1200.jpg │ │ │ ├── front-1426.jpg │ │ │ ├── front-500.jpg │ │ │ ├── front-779.jpg │ │ │ └── front-orig-1426x2000.png │ │ ├── wristwatch_1033.jpg │ │ ├── wristwatch_1112.jpg │ │ ├── wristwatch_1192.jpg │ │ ├── wristwatch_1200.jpg │ │ ├── wristwatch_200.jpg │ │ ├── wristwatch_355.jpg │ │ ├── wristwatch_481.jpg │ │ ├── wristwatch_584.jpg │ │ ├── wristwatch_687.jpg │ │ ├── wristwatch_770.jpg │ │ ├── wristwatch_861.jpg │ │ └── wristwatch_955.jpg │ ├── index.css │ ├── index.js │ ├── pages │ │ ├── Basic.js │ │ ├── EnlargedImageContainerDimensions.js │ │ ├── ExternalEnlargedImage.js │ │ ├── FixedWidthSmallImage.js │ │ ├── Hint.js │ │ ├── Home.js │ │ ├── Lens.js │ │ ├── ReactSlickIntegration.js │ │ ├── ResponsiveImages.js │ │ └── Support.js │ ├── router.js │ └── styles │ │ ├── app.css │ │ ├── examples.css │ │ ├── header.css │ │ └── react-slick.css └── yarn.lock ├── images └── qrcode.png ├── package-lock.json ├── package.json ├── src ├── EnlargedImage.js ├── ReactImageMagnify.js ├── RenderEnlargedImage.js ├── constants │ └── index.js ├── hint │ ├── DefaultHint.js │ └── DisplayUntilActive.js ├── lens │ ├── negative-space │ │ ├── Lens.js │ │ ├── LensBottom.js │ │ ├── LensLeft.js │ │ ├── LensRight.js │ │ ├── LensTop.js │ │ └── index.js │ └── positive-space │ │ ├── assets │ │ ├── texture.gif │ │ └── textured-lens-data-uri.js │ │ └── index.js ├── lib │ ├── dimensions.js │ ├── imageCoordinates.js │ ├── imageRatio.js │ ├── lens.js │ └── styles.js ├── prop-types │ ├── EnlargedImage.js │ ├── Image.js │ ├── Lens.js │ └── Point.js └── utils │ └── index.js ├── test ├── .eslintrc ├── enlarged-image.spec.js ├── lens │ ├── lens.spec.js │ ├── negative-space.spec.js │ └── positive-space.spec.js ├── lib │ ├── dimensions.spec.js │ ├── image-coordinates.spec.js │ ├── image-ratio.spec.js │ └── lens.spec.js ├── react-image-magnify.spec.js ├── render-enlarged-image.spec.js ├── setup.js └── support │ ├── UserDefinedHint.js │ └── jsdom.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "es2015", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "stage-2", 10 | "react" 11 | ], 12 | "plugins": [ 13 | "transform-class-properties" 14 | ], 15 | "env": { 16 | "cjs": { 17 | "plugins": [ 18 | "transform-es2015-modules-commonjs" 19 | ] 20 | }, 21 | "test": { 22 | "plugins": [ 23 | "transform-es2015-modules-commonjs", 24 | "istanbul" 25 | ] 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/react-image-magnify 5 | branches: 6 | ignore: 7 | - gh-pages 8 | docker: 9 | - image: circleci/node:10.9.0 10 | steps: 11 | - checkout 12 | - run: 13 | name: install-npm 14 | command: npm install 15 | - save_cache: 16 | key: dependency-cache-{{ checksum "package.json" }} 17 | paths: 18 | - ./node_modules 19 | - run: 20 | name: test 21 | command: npm run test-ci 22 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: Mq8m0ECS8HPL2J1AJk7SjUUlUpNxkUv34 -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = spaces 9 | indent_size = 4 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | [package.json] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [*.css] 23 | indent_size = 2 24 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | .vscode/ 3 | coverage/ 4 | dist/ 5 | example/ 6 | stats.json -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "es6": true 8 | }, 9 | "parserOptions": { 10 | "ecmaVersion": 6, 11 | "sourceType": "module", 12 | "ecmaFeatures": { 13 | "jsx": true, 14 | "experimentalObjectRestSpread": true 15 | } 16 | }, 17 | "settings": { 18 | "react": { 19 | "version": "16.6" 20 | } 21 | }, 22 | "plugins": [ 23 | "import", 24 | "chai-friendly", 25 | "jsx-a11y", 26 | "react" 27 | ], 28 | "extends": [ 29 | "plugin:import/errors", 30 | "plugin:react/recommended", 31 | "eslint:recommended" 32 | ], 33 | "rules":{ 34 | "no-unused-expressions": 0, 35 | "chai-friendly/no-unused-expressions": 2, 36 | "react/prop-types": 0, 37 | "indent": ["error", 4], 38 | "react/no-deprecated": 1 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output/ 3 | coverage/ 4 | dist/ 5 | npm-debug.log 6 | node_modules/ 7 | stats.json -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "search.exclude": { 4 | "**/node_modules": true, 5 | "coverage": true, 6 | "dist": true, 7 | "example/build": true 8 | } 9 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Ethan Selzer 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-image-magnify 2 | 3 | A responsive React image zoom component for touch and mouse. 4 | 5 | Designed for shopping site product detail. 6 | 7 | Features Include: 8 | * In-place and side-by-side image enlargement 9 | * Positive or negative space guide lens options 10 | * Interaction hint 11 | * Configurable enlarged image dimensions 12 | * Optional enlarged image external render 13 | * Hover intent 14 | * Long-press gesture 15 | * Fade transitions 16 | * Basic react-slick carousel support 17 | 18 | ## Status 19 | [![CircleCI](https://img.shields.io/circleci/project/github/ethanselzer/react-image-magnify.svg)](https://circleci.com/gh/ethanselzer/react-image-magnify/tree/master) 20 | [![Coverage Status](https://coveralls.io/repos/github/ethanselzer/react-image-magnify/badge.svg?branch=master)](https://coveralls.io/github/ethanselzer/react-image-magnify?branch=master) 21 | [![npm](https://img.shields.io/npm/v/react-image-magnify.svg)](https://www.npmjs.com/package/react-image-magnify) 22 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) 23 | 24 | ## Demo 25 | Please visit the [react-image-magnify demo site](https://ethanselzer.github.io/react-image-magnify/#/) 26 | 27 | Experiment with react-image-magnify [live on CodePen](https://codepen.io/ethanselzer/full/oePMNY/). 28 | Use the Change View button to select editing mode or for different layout options. 29 | Use the Fork button to save your changes. 30 | 31 | 32 | ## Installation 33 | 34 | ```sh 35 | npm install react-image-magnify 36 | ``` 37 | 38 | ## Usage 39 | If you are upgrading from v1x to v2x, please see the [release notes](https://github.com/ethanselzer/react-image-magnify/releases/tag/v2.0.0). 40 | 41 | ```JavaScript 42 | import ReactImageMagnify from 'react-image-magnify'; 43 | ... 44 | 56 | ... 57 | ``` 58 | See more usage examples in the [example project](https://github.com/ethanselzer/react-image-magnify/tree/master/example). 59 | 60 | ## Required Props 61 | | Prop | Type | Default | Description | 62 | |-----------------------------|--------|---------|--------------------------------| 63 | | smallImage | Object | N/A | Small image information. See [Small Image](#small-image) below.| 64 | | largeImage | Object | N/A | Large image information. See [Large Image](#large-image) below.| 65 | ## Optional Styling Props 66 | | Prop | Type | Default | Description | 67 | |-----------------------------|--------|---------|---------------------------------| 68 | | className | String | N/A | CSS class applied to root container element. | 69 | | style | Object | N/A | Style applied to root container element. | 70 | | imageClassName | String | N/A | CSS class applied to small image element. | 71 | | imageStyle | Object | N/A | Style applied to small image element. | 72 | | lensStyle | Object | N/A | Style applied to tinted lens. | 73 | | enlargedImageContainerClassName| String | N/A | CSS class applied to enlarged image container element. | 74 | | enlargedImageContainerStyle | Object | N/A | Style applied to enlarged image container element. | 75 | | enlargedImageClassName | String | N/A | CSS class applied to enlarged image element. | 76 | | enlargedImageStyle | Object | N/A | Style applied to enlarged image element.| 77 | ## Optional Interaction Props 78 | | Prop | Type | Default | Description | 79 | |-----------------------------|--------|-------|---------------------------------| 80 | | fadeDurationInMs | Number | 300 | Milliseconds duration of magnified image fade in/fade out. | 81 | | hoverDelayInMs | Number | 250 | Milliseconds to delay hover trigger. | 82 | | hoverOffDelayInMs | Number | 150 | Milliseconds to delay hover-off trigger. | 83 | | isActivatedOnTouch | Boolean| false | Activate magnification immediately on touch. May impact scrolling.| 84 | | pressDuration | Number | 500 | Milliseconds to delay long-press activation (long touch). | 85 | | pressMoveThreshold | Number | 5 | Pixels of movement allowed during long-press activation. | 86 | ## Optional Behavioral Props 87 | | Prop | Type | Default | Description | 88 | |-----------------------------|--------|-------|---------------------------------| 89 | | enlargedImagePosition | String |beside (over for touch)| Enlarged image placement. Can be 'beside' or 'over'.| 90 | | enlargedImageContainerDimensions | Object | {width: '100%', height: '100%'} | Specify enlarged image container dimensions as an object with width and height properties. Values may be expressed as a percentage (e.g. '150%') or a number (e.g. 200). Percentage is based on small image dimension. Number is pixels. Not applied when enlargedImagePosition is set to 'over', the default for touch input. | 91 | | enlargedImagePortalId | String | N/A | Render enlarged image into an HTML element of your choosing by specifying the target element id. Requires React v16. Ignored for touch input by default - see isEnlargedImagePortalEnabledForTouch.| 92 | | isEnlargedImagePortalEnabledForTouch | Boolean | false | Specify portal rendering should be honored for touch input. | 93 | | hintComponent |Function|(Provided)| Reference to a component class or functional component. A Default is provided.| 94 | | shouldHideHintAfterFirstActivation| Boolean | true | Only show hint until the first interaction begins. | 95 | | isHintEnabled | Boolean| false | Enable hint feature. | 96 | | hintTextMouse | String |Hover to Zoom| Hint text for mouse. | 97 | | hintTextTouch | String |Long-Touch to Zoom| Hint text for touch. | 98 | | shouldUsePositiveSpaceLens | Boolean| false | Specify a positive space lens in place of the default negative space lens. | 99 | | lensComponent | Function | (Provided) | Specify a custom lens component. | 100 | 101 | ### Small Image 102 | ``` 103 | { 104 | src: String, (required) 105 | srcSet: String, 106 | sizes: String, 107 | width: Number, (required if isFluidWidth is not set) 108 | height: Number, (required if isFluidWidth is not set) 109 | isFluidWidth: Boolean, (default false) 110 | alt: String, 111 | onLoad: Function, 112 | onError: Function 113 | } 114 | ``` 115 | For more information on responsive images, please try these resources: 116 | [Responsive Images 101](https://cloudfour.com/thinks/responsive-images-101-definitions/) 117 | [Responsive Images - The srcset and sizes Attributes](https://bitsofco.de/the-srcset-and-sizes-attributes/) 118 | 119 | ### Large Image 120 | ``` 121 | { 122 | src: String, (required) 123 | srcSet: String, 124 | sizes: String, 125 | width: Number, (required) 126 | height: Number, (required) 127 | alt: String, (defaults to empty string) 128 | onLoad: Function, 129 | onError: Function 130 | } 131 | ``` 132 | 133 | ## Support 134 | 135 | Please [open an issue](https://github.com/ethanselzer/react-image-magnify/issues). 136 | 137 | ## Example Project 138 | ```ssh 139 | git clone https://github.com/ethanselzer/react-image-magnify.git 140 | cd react-image-magnify 141 | npm install 142 | npm run build 143 | cd example 144 | yarn 145 | yarn start 146 | ``` 147 | 148 | If your default browser does not start automatically, open a new browser window and go to localhost:3000 149 | 150 | ## Development 151 | 152 | ```ssh 153 | git clone https://github.com/ethanselzer/react-image-magnify.git 154 | cd react-image-magnify 155 | npm install 156 | npm run #See available commands 157 | ``` 158 | 159 | The [Example Project](#example-project) may be used in development. 160 | 161 | To rebuild the source automatically when changes are made, run `yarn run build-watch`. 162 | 163 | ## Contributing 164 | 165 | Please contribute using [Github Flow](https://guides.github.com/introduction/flow/). Create a branch, 166 | add commits, and [open a pull request](https://github.com/ethanselzer/react-image-magnify/compare/). 167 | 168 | ## Attribution 169 | 170 | Thanks to the following community members for opening Issues and Pull Requests. 171 | 172 | @damien916 173 | @colepatrickturner 174 | @andreatosatto90 175 | @nathanziarek 176 | @hombrew 177 | @smashercosmo 178 | @sk1e 179 | @vidries 180 | @ionutzp 181 | @sbloedel 182 | @spiderbites 183 | @Akarshit 184 | @eddy20vt 185 | @evannoronha 186 | @benjaminadk 187 | @nilsklimm 188 | @m4recek 189 | @yaser-ali-vp 190 | @carlgunderson 191 | @tojvan 192 | @kskonecka 193 | @Coriou 194 | 195 | You are awesome! ✨💫 196 | 197 | ## License 198 | 199 | MIT 200 | -------------------------------------------------------------------------------- /example/.env: -------------------------------------------------------------------------------- 1 | NODE_PATH=../dist/ 2 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | 12 | # misc 13 | .DS_Store 14 | npm-debug.log 15 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # react-image-magnify Examples Project 2 | 3 | ## Install and Start 4 | ```ssh 5 | git clone https://github.com/ethanselzer/react-image-magnify.git 6 | cd react-image-magnify 7 | yarn 8 | yarn run build 9 | cd example 10 | yarn 11 | yarn start 12 | ``` 13 | 14 | If your default browser does not start automatically, open a new browser window and go to localhost:3000 15 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "private": true, 5 | "homepage": "https://ethanselzer.github.io/react-image-magnify", 6 | "devDependencies": { 7 | "gh-pages": "1.1.0", 8 | "react-scripts": "1.1.2" 9 | }, 10 | "dependencies": { 11 | "bootstrap": "3.3.7", 12 | "react": "16.5.0", 13 | "react-bootstrap": "0.31.3", 14 | "react-dom": "16.5.0", 15 | "react-helmet": "5.2.0", 16 | "react-image-magnify": "2.7.3", 17 | "react-router": "3.0.5", 18 | "react-slick": "0.22.3", 19 | "slick-carousel": "1.8.1" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "eject": "react-scripts eject", 25 | "predeploy": "npm run build", 26 | "deploy": "gh-pages -d build" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/public/circle.yml: -------------------------------------------------------------------------------- 1 | general: 2 | branches: 3 | ignore: 4 | - gh-pages # list of branches to ignore 5 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Image Magnify 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /example/src/components/Basic.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactImageMagnify from 'ReactImageMagnify'; 3 | import SpacedSpan from '../components/SpacedSpan'; 4 | 5 | import '../styles/examples.css'; 6 | 7 | import watchImg687 from '../images/wristwatch_687.jpg'; 8 | import watchImg1200 from '../images/wristwatch_1200.jpg'; 9 | 10 | export default class BasicExample extends Component { 11 | render() { 12 | return ( 13 |
14 |
15 | 27 |
28 |
29 |

Basic Example

30 |

31 | Side by Side enlargement for mouse input. 32 |

33 |

34 | In place enlargement for touch input. 35 |

36 |

37 | Fluid between breakpoints. 38 |

39 |

40 | Please see 41 | 42 | 43 | source code 44 | 45 | 46 | for details. 47 |

48 |
49 |
50 |
51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /example/src/components/EnlargedImageContainerDimensions.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactImageMagnify from 'ReactImageMagnify'; 3 | import SpacedSpan from '../components/SpacedSpan'; 4 | 5 | import '../styles/examples.css'; 6 | 7 | import watchImg355 from '../images/wristwatch_355.jpg'; 8 | import watchImg481 from '../images/wristwatch_481.jpg'; 9 | import watchImg584 from '../images/wristwatch_584.jpg'; 10 | import watchImg687 from '../images/wristwatch_687.jpg'; 11 | import watchImg770 from '../images/wristwatch_770.jpg'; 12 | import watchImg861 from '../images/wristwatch_861.jpg'; 13 | import watchImg955 from '../images/wristwatch_955.jpg'; 14 | import watchImg1033 from '../images/wristwatch_1033.jpg'; 15 | import watchImg1112 from '../images/wristwatch_1112.jpg'; 16 | import watchImg1192 from '../images/wristwatch_1192.jpg'; 17 | import watchImg1200 from '../images/wristwatch_1200.jpg'; 18 | 19 | export default class EnlargedImageContainerDimensions extends Component { 20 | get srcSet() { 21 | return [ 22 | `${watchImg355} 355w`, 23 | `${watchImg481} 481w`, 24 | `${watchImg584} 584w`, 25 | `${watchImg687} 687w`, 26 | `${watchImg770} 770w`, 27 | `${watchImg861} 861w`, 28 | `${watchImg955} 955w`, 29 | `${watchImg1033} 1033w`, 30 | `${watchImg1112} 1112w`, 31 | `${watchImg1192} 1192w`, 32 | `${watchImg1200} 1200w`, 33 | ].join(', '); 34 | } 35 | 36 | render() { 37 | return ( 38 |
39 |
40 | 58 |
59 |
60 |

Enlarged Image Container Dimensions Example

61 |

62 | Specify dimensions as percentage of small image or number of pixels. 63 |

64 |

65 | May be percentage for one dimension and number for the other. 66 |

67 |

68 | Not applied when enlargedImagePosition is set to 'over', the default for touch input. 69 |

70 |

71 | This example specifies width of 72 | 200% 73 | and height of 74 | 100%. 75 |

76 |

77 | Please see 78 | 79 | 80 | source code 81 | 82 | 83 | for details. 84 |

85 |
86 |
87 |
88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /example/src/components/ExampleHint.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | 5 | function ExampleHint({ isTouchDetected }) { 6 | return ( 7 |
14 |
22 | 30 | 36 | { isTouchDetected ? 'Long-Press to Zoom' : 'Rollover to Zoom' } 37 | 38 |
39 |
40 | ); 41 | } 42 | 43 | ExampleHint.displayName = 'ExampleHint' 44 | 45 | ExampleHint.propTypes = { 46 | isTouchDetected: PropTypes.bool, 47 | hintTextMouse: PropTypes.string, 48 | hintTextTouch: PropTypes.string 49 | } 50 | 51 | export default ExampleHint; 52 | -------------------------------------------------------------------------------- /example/src/components/ExternalEnlargedImage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactSlick from '../components/ReactSlick'; 3 | import SpacedSpan from '../components/SpacedSpan'; 4 | 5 | import '../styles/examples.css'; 6 | 7 | export default class ReactSlickExample extends Component { 8 | render() { 9 | return ( 10 |
11 |
12 | 21 |
22 |
23 |
27 |

External Enlarged Image Example

28 |

29 | Render enlarged image into an HTML element of your choosing. 30 |

31 |

32 | Ignored for touch input by default but will be honored if 33 | isEnlargedImagePortalEnabledForTouch is implemented. 34 |

35 |

36 | Use cases include a scenario where an ancestor element of 37 | react-image-magnify implements overflow hidden. 38 |

39 |

40 | Requires React v16. 41 |

42 |

43 | Please see 44 | 45 | 46 | example source code 47 | 48 | 49 | for details. 50 |

51 |
52 |
53 |
54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /example/src/components/FixedWidthSmallImage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactImageMagnify from 'ReactImageMagnify'; 3 | 4 | import '../styles/app.css'; 5 | 6 | import watchImg from '../images/wristwatch_1200.jpg'; 7 | 8 | export default class extends Component { 9 | render() { 10 | return ( 11 |
12 |
13 | 27 |
28 |
29 |

Fixed Width Small Image Example

30 |

Specify small image width and height as numbers

31 |

Small image is not fluid width.

32 |
33 |
34 |
35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /example/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { MenuItem, Navbar, Nav, NavItem, NavDropdown } from 'react-bootstrap'; 3 | 4 | import npmLogo from '../images/header/npm-logo.png'; 5 | import githubLogo from '../images/header/github-logo.png'; 6 | 7 | import '../styles/header.css'; 8 | 9 | class Navigation extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | selectedNavKey: 0 15 | }; 16 | } 17 | 18 | componentDidMount() { 19 | const path = this.props.route.path; 20 | 21 | this.setState({ 22 | selectedNavKey: this.getNavKeyByRoutePath(path) 23 | }) 24 | } 25 | 26 | getNavKeyByRoutePath(path) { 27 | switch (path) { 28 | case '/' : 29 | return 1; 30 | case '/basic' : 31 | return 2.1; 32 | case '/hint' : 33 | return 2.2; 34 | case '/responsive-images' : 35 | return 2.3; 36 | case '/dimensions' : 37 | return 2.4; 38 | case '/react-slick' : 39 | return 2.5; 40 | case '/external' : 41 | return 2.6; 42 | case '/lens' : 43 | return 2.7; 44 | case '/image-magnify' : 45 | return 3.1; 46 | case '/support' : 47 | return 4; 48 | default : 49 | return 1; 50 | } 51 | } 52 | 53 | render() { 54 | return ( 55 | 56 | 57 | 58 | 59 | <ReactImageMagnify /> 60 | 61 | 62 | 63 | 64 | 65 | 81 | 97 | 98 | 99 | ); 100 | } 101 | } 102 | 103 | export default Navigation; 104 | -------------------------------------------------------------------------------- /example/src/components/Hint.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactImageMagnify from 'ReactImageMagnify'; 3 | import SpacedSpan from '../components/SpacedSpan'; 4 | 5 | import '../styles/examples.css'; 6 | 7 | import watchImg687 from '../images/wristwatch_687.jpg'; 8 | import watchImg1200 from '../images/wristwatch_1200.jpg'; 9 | 10 | export default class BasicExample extends Component { 11 | render() { 12 | return ( 13 |
14 |
15 | 29 |
30 |
31 |

Hint Example

32 |

33 | Helps inform users of zoom feature. 34 |

35 |

36 | Configurable text for mouse and touch inputs. English defaults provided. 37 |

38 |

39 | Shown before and after each activation in this example. 40 | Hidden after first interaction by default. 41 |

42 |

43 | Disabled by default. 44 |

45 |

46 | Custom component option. 47 |

48 |

49 | Please see 50 | 51 | 52 | source code 53 | 54 | 55 | for details. 56 |

57 |
58 |
59 |
60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /example/src/components/Lens.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactImageMagnify from 'ReactImageMagnify'; 3 | import SpacedSpan from '../components/SpacedSpan'; 4 | 5 | import '../styles/examples.css'; 6 | 7 | import watchImg687 from '../images/wristwatch_687.jpg'; 8 | import watchImg1200 from '../images/wristwatch_1200.jpg'; 9 | 10 | export default class BasicExample extends Component { 11 | render() { 12 | return ( 13 |
14 |
15 | 28 |
29 |
30 |

Alternate Lens

31 |

32 | Specify a positive space design 33 | in place of the default negative space design. 34 |

35 |

36 | Optionally override the default appearance by specifying a custom style, for example: 37 |

38 |
39 |                         lensStyle: {
40 |   background: 'hsla(0, 0%, 100%, .3)',
41 |   border: '1px solid #ccc'
42 | } 43 |
44 |

45 | Support for a custom lens is provided by 46 | the lensComponent prop. 47 |

48 |

49 | Please see 50 | 51 | 52 | source code 53 | 54 | 55 | for details. 56 |

57 |
58 |
59 |
60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /example/src/components/ReactSlick.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactImageMagnify from 'ReactImageMagnify'; 3 | import ReactSlick from 'react-slick'; 4 | 5 | import '../styles/react-slick.css'; 6 | 7 | import front_500 from '../images/versace-blue/front-500.jpg' 8 | import front_779 from '../images/versace-blue/front-779.jpg'; 9 | import front_1020 from '../images/versace-blue/front-1020.jpg'; 10 | import front_1200 from '../images/versace-blue/front-1200.jpg'; 11 | import front_1426 from '../images/versace-blue/front-1426.jpg'; 12 | 13 | import back_500 from '../images/versace-blue/back-500.jpg' 14 | import back_779 from '../images/versace-blue/back-779.jpg'; 15 | import back_1020 from '../images/versace-blue/back-1020.jpg'; 16 | import back_1200 from '../images/versace-blue/back-1200.jpg'; 17 | import back_1426 from '../images/versace-blue/back-1426.jpg'; 18 | 19 | const frontSrcSet = [ 20 | { src: front_500, setting: '500w' }, 21 | { src: front_779, setting: '779w' }, 22 | { src: front_1020, setting: '1020w' }, 23 | { src: front_1200, setting: '1200w' }, 24 | { src: front_1426, setting: '1426w' } 25 | ] 26 | .map(item => `${item.src} ${item.setting}`) 27 | .join(', '); 28 | 29 | const backSrcSet = [ 30 | { src: back_500, setting: '500w' }, 31 | { src: back_779, setting: '779w' }, 32 | { src: back_1020, setting: '1020w' }, 33 | { src: back_1200, setting: '1200w' }, 34 | { src: back_1426, setting: '1426w' } 35 | ] 36 | .map(item => `${item.src} ${item.setting}`) 37 | .join(', '); 38 | 39 | const dataSource = [ 40 | { 41 | srcSet: frontSrcSet, 42 | small: front_500, 43 | large: front_1426 44 | }, 45 | { 46 | srcSet: backSrcSet, 47 | small: back_500, 48 | large: back_1426 49 | } 50 | ]; 51 | 52 | export default class ReactSlickExample extends Component { 53 | render() { 54 | const { 55 | rimProps, 56 | rsProps 57 | } = this.props; 58 | 59 | return ( 60 | 70 | {dataSource.map((src, index) => ( 71 |
72 | 90 |
91 | ))} 92 |
93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /example/src/components/ReactSlickIntegration.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router'; 3 | import ReactSlick from '../components/ReactSlick'; 4 | import SpacedSpan from '../components/SpacedSpan'; 5 | 6 | import '../styles/app.css'; 7 | 8 | export default class ReactSlickExample extends Component { 9 | render() { 10 | return ( 11 |
12 |
13 | 20 |
21 |
22 |

Carousel Example

23 |

24 | Basic integration with  25 | 26 | react-slick 27 | 28 | . 29 |

30 |

31 | In-place enlargement for mouse and touch input. 32 |

33 |

34 | Side-by-side enlargement supported, please see  35 | 38 | External Enlarged Image Demo 39 | 40 | . 41 |

42 |

43 | Responsive and fluid between breakpoints. 44 |

45 |

46 | Initial file size optimized via 47 | 48 | srcset 49 | 50 | and 51 | 52 | sizes 53 | 54 | attributes. 55 |

56 |

57 | Please see 58 | 59 | 60 | example source code 61 | 62 | 63 | for details. 64 |

65 |
66 |
67 |
68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /example/src/components/ResponsiveImages.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactImageMagnify from 'ReactImageMagnify'; 3 | import SpacedSpan from '../components/SpacedSpan'; 4 | 5 | import '../styles/app.css'; 6 | 7 | import watchImg355 from '../images/wristwatch_355.jpg'; 8 | import watchImg481 from '../images/wristwatch_481.jpg'; 9 | import watchImg584 from '../images/wristwatch_584.jpg'; 10 | import watchImg687 from '../images/wristwatch_687.jpg'; 11 | import watchImg770 from '../images/wristwatch_770.jpg'; 12 | import watchImg861 from '../images/wristwatch_861.jpg'; 13 | import watchImg955 from '../images/wristwatch_955.jpg'; 14 | import watchImg1033 from '../images/wristwatch_1033.jpg'; 15 | import watchImg1112 from '../images/wristwatch_1112.jpg'; 16 | import watchImg1192 from '../images/wristwatch_1192.jpg'; 17 | import watchImg1200 from '../images/wristwatch_1200.jpg'; 18 | 19 | export default class BasicExample extends Component { 20 | get srcSet() { 21 | return [ 22 | `${watchImg355} 355w`, 23 | `${watchImg481} 481w`, 24 | `${watchImg584} 584w`, 25 | `${watchImg687} 687w`, 26 | `${watchImg770} 770w`, 27 | `${watchImg861} 861w`, 28 | `${watchImg955} 955w`, 29 | `${watchImg1033} 1033w`, 30 | `${watchImg1112} 1112w`, 31 | `${watchImg1192} 1192w`, 32 | `${watchImg1200} 1200w`, 33 | ].join(', '); 34 | } 35 | 36 | render() { 37 | return ( 38 |
39 |
40 | 54 |
55 |
56 |

Responsive Images Example

57 |

58 | Fluid between breakpoints. 59 |

60 |

61 | Initial file size optimized via 62 | 63 | srcset 64 | 65 | and 66 | 67 | sizes 68 | 69 | attributes. 70 |

71 |

72 | For more information on responsive images, please try these resources: 73 |
74 | 75 | Responsive Images 101 76 | 77 |
78 | 79 | Responsive Images - The srcset and sizes Attributes 80 | 81 |

82 |

83 | Please see 84 | 85 | 86 | source code 87 | 88 | 89 | for details. 90 |

91 |
92 |
93 |
94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /example/src/components/SpacedSpan.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function SpacedSpan({ className, children }) { 4 | return ( 5 | 6 | {' '}{children}{' '} 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /example/src/images/header/github-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/header/github-logo.png -------------------------------------------------------------------------------- /example/src/images/header/npm-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/header/npm-logo.png -------------------------------------------------------------------------------- /example/src/images/versace-blue/back-1020.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/versace-blue/back-1020.jpg -------------------------------------------------------------------------------- /example/src/images/versace-blue/back-1200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/versace-blue/back-1200.jpg -------------------------------------------------------------------------------- /example/src/images/versace-blue/back-1426.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/versace-blue/back-1426.jpg -------------------------------------------------------------------------------- /example/src/images/versace-blue/back-500.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/versace-blue/back-500.jpg -------------------------------------------------------------------------------- /example/src/images/versace-blue/back-779.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/versace-blue/back-779.jpg -------------------------------------------------------------------------------- /example/src/images/versace-blue/back-orig-1426x2000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/versace-blue/back-orig-1426x2000.png -------------------------------------------------------------------------------- /example/src/images/versace-blue/front-1020.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/versace-blue/front-1020.jpg -------------------------------------------------------------------------------- /example/src/images/versace-blue/front-1200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/versace-blue/front-1200.jpg -------------------------------------------------------------------------------- /example/src/images/versace-blue/front-1426.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/versace-blue/front-1426.jpg -------------------------------------------------------------------------------- /example/src/images/versace-blue/front-500.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/versace-blue/front-500.jpg -------------------------------------------------------------------------------- /example/src/images/versace-blue/front-779.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/versace-blue/front-779.jpg -------------------------------------------------------------------------------- /example/src/images/versace-blue/front-orig-1426x2000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/versace-blue/front-orig-1426x2000.png -------------------------------------------------------------------------------- /example/src/images/wristwatch_1033.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/wristwatch_1033.jpg -------------------------------------------------------------------------------- /example/src/images/wristwatch_1112.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/wristwatch_1112.jpg -------------------------------------------------------------------------------- /example/src/images/wristwatch_1192.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/wristwatch_1192.jpg -------------------------------------------------------------------------------- /example/src/images/wristwatch_1200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/wristwatch_1200.jpg -------------------------------------------------------------------------------- /example/src/images/wristwatch_200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/wristwatch_200.jpg -------------------------------------------------------------------------------- /example/src/images/wristwatch_355.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/wristwatch_355.jpg -------------------------------------------------------------------------------- /example/src/images/wristwatch_481.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/wristwatch_481.jpg -------------------------------------------------------------------------------- /example/src/images/wristwatch_584.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/wristwatch_584.jpg -------------------------------------------------------------------------------- /example/src/images/wristwatch_687.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/wristwatch_687.jpg -------------------------------------------------------------------------------- /example/src/images/wristwatch_770.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/wristwatch_770.jpg -------------------------------------------------------------------------------- /example/src/images/wristwatch_861.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/wristwatch_861.jpg -------------------------------------------------------------------------------- /example/src/images/wristwatch_955.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/example/src/images/wristwatch_955.jpg -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { hashHistory } from 'react-router'; 4 | import Routes from './router'; 5 | import './index.css'; 6 | 7 | ReactDOM.render( 8 | , 9 | document.getElementById('root') 10 | ); 11 | -------------------------------------------------------------------------------- /example/src/pages/Basic.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Col, 4 | Grid, 5 | Jumbotron, 6 | Row 7 | } from 'react-bootstrap'; 8 | import Helmet from 'react-helmet'; 9 | 10 | import Header from '../components/Header'; 11 | import Basic from '../components/Basic'; 12 | 13 | import 'bootstrap/dist/css/bootstrap.css'; 14 | import '../styles/app.css'; 15 | 16 | export default class extends Component { 17 | render() { 18 | return ( 19 |
20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example/src/pages/EnlargedImageContainerDimensions.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Col, 4 | Grid, 5 | Jumbotron, 6 | Row 7 | } from 'react-bootstrap'; 8 | import Helmet from 'react-helmet'; 9 | 10 | import Header from '../components/Header'; 11 | import EnlargedImageContainerDimensions from '../components/EnlargedImageContainerDimensions'; 12 | 13 | import 'bootstrap/dist/css/bootstrap.css'; 14 | import '../styles/app.css'; 15 | 16 | export default class extends Component { 17 | render() { 18 | return ( 19 |
20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example/src/pages/ExternalEnlargedImage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Col, 4 | Grid, 5 | Jumbotron, 6 | Row 7 | } from 'react-bootstrap'; 8 | import Helmet from 'react-helmet'; 9 | 10 | import Header from '../components/Header'; 11 | import ExternalEnlargedImage from '../components/ExternalEnlargedImage'; 12 | 13 | import 'bootstrap/dist/css/bootstrap.css'; 14 | import '../styles/app.css'; 15 | 16 | export default class extends Component { 17 | render() { 18 | return ( 19 |
20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example/src/pages/FixedWidthSmallImage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Col, 4 | Grid, 5 | Jumbotron, 6 | Row 7 | } from 'react-bootstrap'; 8 | import Helmet from 'react-helmet'; 9 | 10 | import Header from '../components/Header'; 11 | import FixedWidthSmallImage from '../components/FixedWidthSmallImage'; 12 | 13 | import 'bootstrap/dist/css/bootstrap.css'; 14 | import '../styles/app.css'; 15 | 16 | export default class extends Component { 17 | render() { 18 | return ( 19 |
20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example/src/pages/Hint.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Col, 4 | Grid, 5 | Jumbotron, 6 | Row 7 | } from 'react-bootstrap'; 8 | import Helmet from 'react-helmet'; 9 | 10 | import Header from '../components/Header'; 11 | import Hint from '../components/Hint'; 12 | 13 | import 'bootstrap/dist/css/bootstrap.css'; 14 | import '../styles/app.css'; 15 | 16 | export default class extends Component { 17 | render() { 18 | return ( 19 |
20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example/src/pages/Home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Helmet from 'react-helmet'; 3 | 4 | import Header from '../components/Header'; 5 | 6 | import 'bootstrap/dist/css/bootstrap.css'; 7 | import '../styles/app.css'; 8 | 9 | import { 10 | Clearfix, 11 | Col, 12 | Grid, 13 | Jumbotron, 14 | Nav, 15 | NavItem, 16 | Panel, 17 | Row 18 | } from 'react-bootstrap'; 19 | 20 | export default class extends Component { 21 | render() { 22 | return ( 23 |
24 | 25 |
26 | 27 | 28 |

Demo

29 |
30 |
31 | 32 | 33 | 34 | 35 | 44 | 45 | 46 | 47 | 48 | 49 | 54 | 55 | 56 | 57 | 58 |
59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /example/src/pages/Lens.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Col, 4 | Grid, 5 | Jumbotron, 6 | Row 7 | } from 'react-bootstrap'; 8 | import Helmet from 'react-helmet'; 9 | 10 | import Header from '../components/Header'; 11 | import Lens from '../components/Lens'; 12 | 13 | import 'bootstrap/dist/css/bootstrap.css'; 14 | import '../styles/app.css'; 15 | 16 | export default class extends Component { 17 | render() { 18 | return ( 19 |
20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example/src/pages/ReactSlickIntegration.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Col, 4 | Grid, 5 | Jumbotron, 6 | Row 7 | } from 'react-bootstrap'; 8 | import Helmet from 'react-helmet'; 9 | 10 | import Header from '../components/Header'; 11 | import ReactSlickIntegration from '../components/ReactSlickIntegration'; 12 | 13 | import 'bootstrap/dist/css/bootstrap.css'; 14 | import '../styles/app.css'; 15 | 16 | export default class extends Component { 17 | render() { 18 | return ( 19 |
20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example/src/pages/ResponsiveImages.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Col, 4 | Grid, 5 | Jumbotron, 6 | Row 7 | } from 'react-bootstrap'; 8 | import Helmet from 'react-helmet'; 9 | 10 | import Header from '../components/Header'; 11 | import ResponsiveImages from '../components/ResponsiveImages'; 12 | 13 | import 'bootstrap/dist/css/bootstrap.css'; 14 | import '../styles/app.css'; 15 | 16 | export default class extends Component { 17 | render() { 18 | return ( 19 |
20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example/src/pages/Support.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Col, 4 | Grid, 5 | Jumbotron, 6 | Row 7 | } from 'react-bootstrap'; 8 | import Helmet from 'react-helmet'; 9 | 10 | import Header from '../components/Header'; 11 | 12 | import 'bootstrap/dist/css/bootstrap.css'; 13 | import '../styles/app.css'; 14 | 15 | export default class extends Component { 16 | render() { 17 | return ( 18 |
19 | 20 |
21 | 22 | 23 | 24 | 25 |

Support

26 | 27 |
28 |
29 |
30 | 31 | 32 | 33 | Please  34 | 35 | open an issue 36 | on GitHub. Thanks! ✨ 37 | 38 | 39 | 40 |
41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /example/src/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Router, Route } from 'react-router'; 3 | 4 | import Home from './pages/Home'; 5 | import Basic from './pages/Basic'; 6 | import ResponsiveImages from './pages/ResponsiveImages'; 7 | import Hint from './pages/Hint'; 8 | import ReactSlickIntegration from './pages/ReactSlickIntegration'; 9 | import EnlargedImageContainerDimensions from './pages/EnlargedImageContainerDimensions'; 10 | import Lens from './pages/Lens'; 11 | import FixedWidthSmallImage from './pages/FixedWidthSmallImage'; 12 | import ExternalEnlargedImage from './pages/ExternalEnlargedImage'; 13 | import Support from './pages/Support'; 14 | 15 | const Routes = (props) => ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | 30 | export default Routes; 31 | -------------------------------------------------------------------------------- /example/src/styles/app.css: -------------------------------------------------------------------------------- 1 | .logo { 2 | margin-top: -4px; 3 | font: 25px Courier New, Courier, monospace; 4 | } 5 | 6 | h2 { 7 | font: 22px "Helvetica Neue", Helvetica, Arial, sans-serif; 8 | margin: 15px 0; 9 | } 10 | 11 | p.summary { 12 | font-size: 16px; 13 | line-height: 1.5 14 | } 15 | 16 | .summary__list { 17 | margin: 0; 18 | } 19 | 20 | .jumbotron { 21 | padding: 0; 22 | margin: 96px 0 10px; 23 | } 24 | 25 | .jumbotron--home { 26 | padding: 0; 27 | } 28 | 29 | li.active a { 30 | cursor: default; 31 | } 32 | 33 | .example { 34 | text-align: center; 35 | } 36 | 37 | .example__target { 38 | padding: 10px; 39 | position: relative; 40 | border: 1px solid #ccc; 41 | border-radius: 4px; 42 | transition: border .2s ease-in-out; 43 | text-align: center; 44 | } 45 | 46 | .example__target.example__target--basic { 47 | border-color: transparent; 48 | } 49 | 50 | .example__target:hover { 51 | border-color: #337ab7; 52 | } 53 | 54 | .example__instructions { 55 | margin-top: 10px; 56 | color: #337ab7; 57 | } 58 | 59 | .example__instructions--solo { 60 | margin-bottom: 10px; 61 | } 62 | 63 | .example__source-container { 64 | margin: 20px 0; 65 | } 66 | 67 | .example__source-css { 68 | margin-top: 15px; 69 | } 70 | 71 | .example__external-label { 72 | margin-top: 10px; 73 | } 74 | 75 | @media (min-width: 420px) { 76 | .jumbotron { 77 | margin-top: 51px; 78 | } 79 | } 80 | 81 | @media (min-width: 768px) { 82 | .jumbotron { 83 | margin-top: 51px; 84 | } 85 | 86 | .example__source-container { 87 | margin: 0; 88 | } 89 | } 90 | 91 | @media (min-width: 768px) and (max-width: 991px) { 92 | .jumbotron { 93 | margin-top: 97px; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /example/src/styles/examples.css: -------------------------------------------------------------------------------- 1 | .fluid { 2 | max-width: 1200px; 3 | margin: 0 auto; 4 | display: flex; 5 | flex-direction: column; 6 | font-family: Arial; 7 | line-height: 1.3; 8 | font-size: 16px; 9 | } 10 | 11 | .fluid__instructions { 12 | flex: 0 0 auto; 13 | margin: 0 20px; 14 | } 15 | 16 | .fixed__instructions { 17 | flex: 1; 18 | margin: 0 20px; 19 | } 20 | 21 | a { 22 | color: black; 23 | } 24 | 25 | a:hover { 26 | color: #666; 27 | } 28 | 29 | .code { 30 | font-family: Courier New, Courier, monospace; 31 | } 32 | 33 | @media (min-width: 480px) { 34 | .fluid { 35 | flex-direction: row; 36 | } 37 | 38 | .fluid__image-container{ 39 | flex: 0 0 30%; 40 | margin: 20px 0 20px 20px; 41 | } 42 | 43 | .fluid__instructions { 44 | flex: 0 0 50%; 45 | padding-top: 30px; 46 | } 47 | 48 | .fixed__instructions { 49 | padding-top: 30px; 50 | margin: 0 10px; 51 | } 52 | 53 | .portal { 54 | position: absolute; 55 | top: 40px; 56 | left: -30px; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /example/src/styles/header.css: -------------------------------------------------------------------------------- 1 | .github-link img { 2 | width: 32px; 3 | } 4 | 5 | .npm-link img { 6 | height: 32px; 7 | } 8 | 9 | .navbar-nav > li.npm-link > a { 10 | padding: 0 0 0 14px; 11 | } 12 | 13 | .navbar-nav > li.github-link > a { 14 | padding: 0 0 10px 14px; 15 | } 16 | 17 | @media (min-width: 768px) { 18 | .navbar-nav > li.github-link > a { 19 | padding: 10px 0 5px 0; 20 | margin-right: 20px; 21 | } 22 | 23 | .navbar-nav > li.npm-link > a { 24 | padding: 11px 0 5px 0; 25 | } 26 | 27 | ul.navbar-right { 28 | margin-right: 0; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/src/styles/react-slick.css: -------------------------------------------------------------------------------- 1 | @import "~slick-carousel/slick/slick.css"; 2 | @import "~slick-carousel/slick/slick-theme.css"; 3 | 4 | .react-slick * { 5 | min-height: 0; 6 | min-width: 0; 7 | } 8 | 9 | .react-slick .slick-prev, .react-slick .slick-next { 10 | background-color: rgb(187, 184, 184); 11 | border-radius: 10px; 12 | } 13 | 14 | .react-slick .fluid__instructions { 15 | margin-top: 30px; 16 | } 17 | 18 | @media (min-width: 480px) { 19 | .react-slick .fluid__image-container{ 20 | margin: 40px; 21 | } 22 | 23 | .react-slick .fluid__instructions { 24 | margin: 0 20px; 25 | padding-top: 20px; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /images/qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/images/qrcode.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-image-magnify", 3 | "version": "2.7.4", 4 | "description": "A responsive image zoom component designed for shopping sites.", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/ethanselzer/react-image-magnify.git" 8 | }, 9 | "author": "Ethan Selzer ", 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/ethanselzer/react-image-magnify/issues" 13 | }, 14 | "homepage": "https://github.com/ethanselzer/react-image-magnify", 15 | "keywords": [ 16 | "react", 17 | "image", 18 | "photo", 19 | "picture", 20 | "zoom", 21 | "enlarge", 22 | "magnify", 23 | "expand", 24 | "lens", 25 | "detail", 26 | "ecommerce", 27 | "store", 28 | "shopping", 29 | "product" 30 | ], 31 | "main": "dist/ReactImageMagnify.js", 32 | "module": "dist/es/ReactImageMagnify.js", 33 | "files": [ 34 | "dist", 35 | "LICENCE" 36 | ], 37 | "scripts": { 38 | "analyze": "npm run stats && webpack-bundle-analyzer stats.json", 39 | "build-umd": "webpack -p", 40 | "build-cjs": "cross-env BABEL_ENV=cjs babel src --out-dir dist", 41 | "build-es": "babel src --out-dir dist/es", 42 | "build": "rimraf dist && npm run build-cjs && npm run build-es && npm run build-umd", 43 | "build-watch": "cross-env BABEL_ENV=cjs babel --watch src --out-dir dist", 44 | "prepublishOnly": "npm run lint && npm test && npm run build", 45 | "lint": "eslint \"@(src|test)/**/*.js\"", 46 | "stats": "webpack -p --profile --json > stats.json", 47 | "test": "npm run lint && npm run test-only", 48 | "test-only": "cross-env BABEL_ENV=test mocha --recursive --require babel-core/register --require test/setup test", 49 | "test-watch": "npm run test-only -- --watch", 50 | "test-coverage": "rimraf coverage && nyc --reporter=lcov npm run test-only && nyc report", 51 | "test-ci": "npm run lint && npm run test-coverage && npm run coveralls", 52 | "coveralls": "nyc report --reporter=text-lcov | coveralls" 53 | }, 54 | "nyc": { 55 | "sourceMap": false, 56 | "instrument": false, 57 | "exclude": [ 58 | "test" 59 | ] 60 | }, 61 | "devDependencies": { 62 | "babel-cli": "^6.26.0", 63 | "babel-core": "^6.26.0", 64 | "babel-eslint": "^8.0.1", 65 | "babel-loader": "^7.1.2", 66 | "babel-plugin-istanbul": "^4.1.5", 67 | "babel-plugin-transform-class-properties": "^6.24.1", 68 | "babel-plugin-transform-es2015-modules-umd": "^6.24.1", 69 | "babel-preset-es2015": "^6.24.1", 70 | "babel-preset-react": "^6.24.1", 71 | "babel-preset-stage-2": "^6.24.1", 72 | "chai": "4.1.2", 73 | "coveralls": "3.0.0", 74 | "cross-env": "^5.0.5", 75 | "enzyme": "3.6.0", 76 | "enzyme-adapter-react-16": "1.5.0", 77 | "eslint": "^4.8.0", 78 | "eslint-plugin-chai-friendly": "^0.4.0", 79 | "eslint-plugin-import": "^2.7.0", 80 | "eslint-plugin-jsx-a11y": "^6.0.2", 81 | "eslint-plugin-react": "^7.4.0", 82 | "jsdom": "^12.0.0", 83 | "mocha": "4.0.0", 84 | "nyc": "^11.2.1", 85 | "react": "^16.5.0", 86 | "react-dom": "^16.5.0", 87 | "react-test-renderer": "^16.5.0", 88 | "rimraf": "^2.6.2", 89 | "sinon": "4.1.2", 90 | "webpack": "^3.6.0", 91 | "webpack-bundle-analyzer": "^2.9.0" 92 | }, 93 | "peerDependencies": { 94 | "react": "~0.14.9 || ^15.0.0 || ^16.0.0" 95 | }, 96 | "dependencies": { 97 | "clamp": "1.0.1", 98 | "detect-it": "3.0.3", 99 | "fast-deep-equal": "1.0.0", 100 | "object-assign": "4.1.1", 101 | "prop-types": "15.6.0", 102 | "react-cursor-position": "2.5.3", 103 | "react-required-if": "1.0.1" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/EnlargedImage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { 5 | getLensModeEnlargedImageCoordinates, 6 | getInPlaceEnlargedImageCoordinates 7 | } from './lib/imageCoordinates'; 8 | import { LargeImageShape } from './prop-types/Image'; 9 | import { ContainerDimensions } from './prop-types/EnlargedImage'; 10 | import { noop } from './utils'; 11 | import Point from './prop-types/Point'; 12 | import { 13 | getEnlargedImageContainerStyle, 14 | getEnlargedImageStyle 15 | } from './lib/styles'; 16 | 17 | export default class extends React.Component { 18 | constructor(props) { 19 | super(props); 20 | 21 | this.state = { 22 | isTransitionEntering: false, 23 | isTransitionActive: false, 24 | isTransitionLeaving: false, 25 | isTransitionDone: false 26 | }; 27 | 28 | this.timers = []; 29 | } 30 | 31 | static displayName = 'EnlargedImage'; 32 | 33 | static defaultProps = { 34 | fadeDurationInMs: 0, 35 | isLazyLoaded: true 36 | }; 37 | 38 | static propTypes = { 39 | containerClassName: PropTypes.string, 40 | containerStyle: PropTypes.object, 41 | cursorOffset: Point, 42 | position: Point, 43 | fadeDurationInMs: PropTypes.number, 44 | imageClassName: PropTypes.string, 45 | imageStyle: PropTypes.object, 46 | isActive: PropTypes.bool, 47 | isLazyLoaded: PropTypes.bool, 48 | largeImage: LargeImageShape, 49 | containerDimensions: ContainerDimensions, 50 | isPortalRendered: PropTypes.bool, 51 | isInPlaceMode: PropTypes.bool 52 | }; 53 | 54 | componentWillReceiveProps(nextProps) { 55 | this.scheduleCssTransition(nextProps); 56 | } 57 | 58 | componentWillUnmount() { 59 | this.timers.forEach((timerId) => { 60 | clearTimeout(timerId); 61 | }); 62 | } 63 | 64 | scheduleCssTransition(nextProps) { 65 | const { 66 | fadeDurationInMs, 67 | isActive, 68 | isPositionOutside 69 | } = this.props; 70 | const willIsActiveChange = isActive !== nextProps.isActive; 71 | const willIsPositionOutsideChange = isPositionOutside !== nextProps.isPositionOutside; 72 | 73 | if (!willIsActiveChange && !willIsPositionOutsideChange) { 74 | return; 75 | } 76 | 77 | if (nextProps.isActive && !nextProps.isPositionOutside) { 78 | this.setState({ 79 | isTrainsitionDone: false, 80 | isTransitionEntering: true 81 | }); 82 | 83 | this.timers.push(setTimeout(() => { 84 | this.setState({ 85 | isTransitionEntering: false, 86 | isTransitionActive: true 87 | }); 88 | }, 0)); 89 | } else { 90 | this.setState({ 91 | isTransitionLeaving: true, 92 | isTransitionActive: false 93 | }); 94 | 95 | this.timers.push(setTimeout(() => { 96 | this.setState({ 97 | isTransitionDone: true, 98 | isTransitionLeaving: false 99 | }); 100 | }, fadeDurationInMs)); 101 | } 102 | } 103 | 104 | getImageCoordinates() { 105 | const { 106 | cursorOffset, 107 | largeImage, 108 | containerDimensions, 109 | position, 110 | smallImage, 111 | isInPlaceMode 112 | } = this.props; 113 | 114 | if (isInPlaceMode) { 115 | return getInPlaceEnlargedImageCoordinates({ 116 | containerDimensions, 117 | largeImage, 118 | position 119 | }); 120 | } 121 | 122 | return getLensModeEnlargedImageCoordinates({ 123 | containerDimensions, 124 | cursorOffset, 125 | largeImage, 126 | position, 127 | smallImage 128 | }); 129 | } 130 | 131 | get isVisible() { 132 | const { 133 | isTransitionEntering, 134 | isTransitionActive, 135 | isTransitionLeaving 136 | } = this.state; 137 | 138 | return ( 139 | isTransitionEntering || 140 | isTransitionActive || 141 | isTransitionLeaving 142 | ); 143 | } 144 | 145 | get containerStyle() { 146 | const { 147 | containerStyle, 148 | containerDimensions, 149 | fadeDurationInMs, 150 | isPortalRendered, 151 | isInPlaceMode 152 | } = this.props; 153 | 154 | const { isTransitionActive } = this.state; 155 | 156 | return getEnlargedImageContainerStyle({ 157 | containerDimensions, 158 | containerStyle, 159 | fadeDurationInMs, 160 | isTransitionActive, 161 | isInPlaceMode, 162 | isPortalRendered 163 | }); 164 | } 165 | 166 | get imageStyle() { 167 | const { 168 | imageStyle, 169 | largeImage 170 | } = this.props; 171 | 172 | return getEnlargedImageStyle({ 173 | imageCoordinates: this.getImageCoordinates(), 174 | imageStyle, 175 | largeImage 176 | }); 177 | } 178 | 179 | render() { 180 | const { 181 | containerClassName, 182 | imageClassName, 183 | isLazyLoaded, 184 | largeImage, 185 | largeImage: { 186 | alt = '', 187 | onLoad = noop, 188 | onError = noop 189 | }, 190 | } = this.props; 191 | 192 | const component = ( 193 |
197 | 207 |
208 | ); 209 | 210 | if (isLazyLoaded) { 211 | return this.isVisible ? component : null; 212 | } 213 | 214 | return component; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/ReactImageMagnify.js: -------------------------------------------------------------------------------- 1 | import detectIt from 'detect-it'; 2 | import objectAssign from 'object-assign'; 3 | import PropTypes from 'prop-types'; 4 | import React from 'react'; 5 | import ReactCursorPosition from 'react-cursor-position'; 6 | 7 | import RenderEnlargedImage from './RenderEnlargedImage'; 8 | import NegativeSpaceLens from './lens/negative-space'; 9 | import PositiveSpaceLens from './lens/positive-space'; 10 | import DisplayUntilActive from './hint/DisplayUntilActive'; 11 | import Hint from './hint/DefaultHint'; 12 | 13 | import { getLensCursorOffset } from './lib/lens'; 14 | import { getEnlargedImageContainerDimension } from './lib/dimensions'; 15 | import { 16 | getContainerStyle, 17 | getSmallImageStyle 18 | } from './lib/styles'; 19 | import { 20 | LargeImageShape, 21 | SmallImageShape 22 | } from './prop-types/Image'; 23 | import { 24 | EnlargedImagePosition, 25 | EnlargedImageContainerDimensions 26 | } from './prop-types/EnlargedImage'; 27 | import { noop } from './utils'; 28 | import { 29 | INPUT_TYPE, 30 | ENLARGED_IMAGE_POSITION 31 | } from './constants'; 32 | 33 | class ReactImageMagnify extends React.Component { 34 | 35 | constructor(props) { 36 | super(props); 37 | 38 | const { primaryInput } = detectIt; 39 | const { 40 | mouse: MOUSE, 41 | touch: TOUCH 42 | } = INPUT_TYPE; 43 | 44 | this.state = { 45 | smallImageWidth: 0, 46 | smallImageHeight: 0, 47 | detectedInputType: { 48 | isMouseDeteced: (primaryInput === MOUSE), 49 | isTouchDetected: (primaryInput === TOUCH) 50 | } 51 | } 52 | 53 | this.onSmallImageLoad = this.onSmallImageLoad.bind(this); 54 | this.setSmallImageDimensionState = this.setSmallImageDimensionState.bind(this); 55 | this.onDetectedInputTypeChanged = this.onDetectedInputTypeChanged.bind(this); 56 | } 57 | 58 | static propTypes = { 59 | className: PropTypes.string, 60 | style: PropTypes.object, 61 | hoverDelayInMs: PropTypes.number, 62 | hoverOffDelayInMs: PropTypes.number, 63 | fadeDurationInMs: PropTypes.number, 64 | pressDuration: PropTypes.number, 65 | pressMoveThreshold: PropTypes.number, 66 | isActivatedOnTouch: PropTypes.bool, 67 | imageClassName: PropTypes.string, 68 | imageStyle: PropTypes.object, 69 | lensStyle: PropTypes.object, 70 | lensComponent: PropTypes.func, 71 | shouldUsePositiveSpaceLens: PropTypes.bool, 72 | smallImage: SmallImageShape, 73 | largeImage: LargeImageShape, 74 | enlargedImageContainerClassName: PropTypes.string, 75 | enlargedImageContainerStyle: PropTypes.object, 76 | enlargedImageClassName: PropTypes.string, 77 | enlargedImageStyle: PropTypes.object, 78 | enlargedImageContainerDimensions: EnlargedImageContainerDimensions, 79 | enlargedImagePosition: EnlargedImagePosition, 80 | enlargedImagePortalId: PropTypes.string, 81 | isEnlargedImagePortalEnabledForTouch: PropTypes.bool, 82 | hintComponent: PropTypes.func, 83 | hintTextMouse: PropTypes.string, 84 | hintTextTouch: PropTypes.string, 85 | isHintEnabled: PropTypes.bool, 86 | shouldHideHintAfterFirstActivation: PropTypes.bool 87 | }; 88 | 89 | static defaultProps = { 90 | enlargedImageContainerDimensions: { 91 | width: '100%', 92 | height: '100%' 93 | }, 94 | isEnlargedImagePortalEnabledForTouch: false, 95 | fadeDurationInMs: 300, 96 | hintComponent: Hint, 97 | shouldHideHintAfterFirstActivation: true, 98 | isHintEnabled: false, 99 | hintTextMouse: 'Hover to Zoom', 100 | hintTextTouch: 'Long-Touch to Zoom', 101 | hoverDelayInMs: 250, 102 | hoverOffDelayInMs: 150, 103 | shouldUsePositiveSpaceLens: false 104 | }; 105 | 106 | componentDidMount() { 107 | const { 108 | smallImage: { 109 | isFluidWidth 110 | } 111 | } = this.props; 112 | 113 | if (!isFluidWidth) { 114 | return; 115 | } 116 | 117 | this.setSmallImageDimensionState(); 118 | window.addEventListener('resize', this.setSmallImageDimensionState); 119 | } 120 | 121 | componentWillUnmount() { 122 | window.removeEventListener('resize', this.setSmallImageDimensionState); 123 | } 124 | 125 | onSmallImageLoad(e) { 126 | const { 127 | smallImage: { 128 | onLoad = noop, 129 | isFluidWidth 130 | } 131 | } = this.props; 132 | 133 | onLoad(e); 134 | 135 | if (!isFluidWidth) { 136 | return; 137 | } 138 | 139 | this.setSmallImageDimensionState(); 140 | } 141 | 142 | onDetectedInputTypeChanged(detectedInputType) { 143 | this.setState({ 144 | detectedInputType 145 | }); 146 | } 147 | 148 | setSmallImageDimensionState() { 149 | const { 150 | offsetWidth: smallImageWidth, 151 | offsetHeight: smallImageHeight 152 | } = this.smallImageEl; 153 | 154 | this.setState({ 155 | smallImageWidth, 156 | smallImageHeight 157 | }); 158 | } 159 | 160 | get smallImage() { 161 | const { 162 | smallImage, 163 | smallImage: { 164 | isFluidWidth 165 | }, 166 | } = this.props; 167 | 168 | if (!isFluidWidth) { 169 | return smallImage; 170 | } 171 | 172 | const { 173 | smallImageWidth: fluidWidth, 174 | smallImageHeight: fluidHeight 175 | } = this.state; 176 | 177 | return objectAssign( 178 | {}, 179 | smallImage, 180 | { 181 | width: fluidWidth, 182 | height: fluidHeight 183 | } 184 | ); 185 | } 186 | 187 | get enlargedImagePlacement() { 188 | const { 189 | enlargedImagePosition: userDefinedEnlargedImagePosition 190 | } = this.props; 191 | 192 | const { 193 | detectedInputType: { 194 | isTouchDetected 195 | } 196 | } = this.state; 197 | 198 | const computedEnlargedImagePosition = ( 199 | isTouchDetected 200 | ? ENLARGED_IMAGE_POSITION.over 201 | : ENLARGED_IMAGE_POSITION.beside 202 | ); 203 | 204 | return userDefinedEnlargedImagePosition || computedEnlargedImagePosition; 205 | } 206 | 207 | get isInPlaceMode() { 208 | const { over: OVER } = ENLARGED_IMAGE_POSITION; 209 | return this.enlargedImagePlacement === OVER; 210 | } 211 | 212 | get enlargedImageContainerDimensions() { 213 | const { 214 | enlargedImageContainerDimensions: { 215 | width: containerWidth, 216 | height: containerHeight 217 | } 218 | } = this.props; 219 | const { 220 | width: smallImageWidth, 221 | height: smallImageHeight 222 | } = this.smallImage; 223 | const isInPlaceMode = this.isInPlaceMode; 224 | 225 | return { 226 | width: getEnlargedImageContainerDimension({ 227 | containerDimension: containerWidth, 228 | smallImageDimension: smallImageWidth, 229 | isInPlaceMode 230 | }), 231 | height: getEnlargedImageContainerDimension({ 232 | containerDimension: containerHeight, 233 | smallImageDimension: smallImageHeight, 234 | isInPlaceMode 235 | }) 236 | }; 237 | } 238 | 239 | get isTouchDetected() { 240 | const { 241 | detectedInputType: { 242 | isTouchDetected 243 | } 244 | } = this.state; 245 | 246 | return isTouchDetected; 247 | } 248 | 249 | get shouldShowLens() { 250 | return ( 251 | !this.isInPlaceMode && 252 | !this.isTouchDetected 253 | ); 254 | } 255 | 256 | get lensComponent() { 257 | const { 258 | shouldUsePositiveSpaceLens, 259 | lensComponent 260 | } = this.props; 261 | 262 | if (lensComponent) { 263 | return lensComponent 264 | } 265 | 266 | if (shouldUsePositiveSpaceLens) { 267 | return PositiveSpaceLens; 268 | } 269 | 270 | return NegativeSpaceLens; 271 | } 272 | 273 | render() { 274 | const { 275 | className, 276 | style, 277 | hoverDelayInMs, 278 | hoverOffDelayInMs, 279 | isActivatedOnTouch, 280 | pressDuration, 281 | pressMoveThreshold, 282 | smallImage: { 283 | onError = noop 284 | }, 285 | imageClassName, 286 | imageStyle, 287 | lensStyle, 288 | largeImage, 289 | enlargedImageContainerClassName, 290 | enlargedImageContainerStyle, 291 | enlargedImageClassName, 292 | enlargedImageStyle, 293 | enlargedImagePortalId, 294 | isEnlargedImagePortalEnabledForTouch, 295 | fadeDurationInMs, 296 | hintComponent: HintComponent, 297 | isHintEnabled, 298 | hintTextMouse, 299 | hintTextTouch, 300 | shouldHideHintAfterFirstActivation 301 | } = this.props; 302 | 303 | const smallImage = this.smallImage; 304 | 305 | const { 306 | detectedInputType: { 307 | isTouchDetected 308 | } 309 | } = this.state; 310 | 311 | const cursorOffset = getLensCursorOffset( 312 | smallImage, 313 | largeImage, 314 | this.enlargedImageContainerDimensions 315 | ); 316 | 317 | const Lens = this.lensComponent; 318 | 319 | return ( 320 | 331 | this.smallImageEl = el, 339 | onLoad: this.onSmallImageLoad, 340 | onError 341 | }} /> 342 | {isHintEnabled && 343 | 346 | 351 | 352 | } 353 | {this.shouldShowLens && 354 | 360 | } 361 | 376 | 377 | ); 378 | } 379 | } 380 | 381 | export default ReactImageMagnify; 382 | -------------------------------------------------------------------------------- /src/RenderEnlargedImage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import objectAssign from 'object-assign'; 5 | import EnlargedImage from './EnlargedImage'; 6 | 7 | export default class RenderEnlargedImage extends Component { 8 | static propTypes = { 9 | isPortalEnabledForTouch: PropTypes.bool.isRequired, 10 | isTouchDetected: PropTypes.bool.isRequired, 11 | portalId: PropTypes.string 12 | } 13 | 14 | state = { isMounted: false } 15 | 16 | componentDidMount() { 17 | this.setState({ isMounted: true }); 18 | 19 | if (this.isPortalRendered) { 20 | const { portalId } = this.props; 21 | this.portalElement = document.getElementById(portalId); 22 | } 23 | } 24 | 25 | get isPortalIdImplemented() { 26 | return !!this.props.portalId; 27 | } 28 | 29 | get isPortalRendered() { 30 | const { 31 | isPortalEnabledForTouch, 32 | isTouchDetected 33 | } = this.props; 34 | 35 | if (!this.isPortalIdImplemented) { 36 | return false; 37 | } 38 | 39 | if (!isTouchDetected) { 40 | return true; 41 | } 42 | 43 | if (isPortalEnabledForTouch) { 44 | return true; 45 | } 46 | 47 | return false; 48 | } 49 | 50 | get isMounted() { 51 | return this.state.isMounted; 52 | } 53 | 54 | get compositProps() { 55 | return objectAssign( 56 | {}, 57 | this.props, 58 | { isPortalRendered: this.isPortalRendered } 59 | ); 60 | } 61 | 62 | render() { 63 | if (!this.isMounted) { 64 | return null; 65 | } 66 | 67 | const props = this.compositProps; 68 | 69 | if (this.isPortalRendered) { 70 | return ReactDOM.createPortal( 71 | , 72 | this.portalElement 73 | ); 74 | } 75 | 76 | return ( 77 | 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/constants/index.js: -------------------------------------------------------------------------------- 1 | export const INPUT_TYPE = { 2 | mouse: 'mouse', 3 | touch: 'touch' 4 | }; 5 | 6 | export const ENLARGED_IMAGE_POSITION = { 7 | over: 'over', 8 | beside: 'beside' 9 | } 10 | -------------------------------------------------------------------------------- /src/hint/DefaultHint.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | function DefaultHint({ isTouchDetected, hintTextMouse, hintTextTouch }) { 5 | return ( 6 |
13 |
21 | 29 | 35 | { isTouchDetected ? hintTextTouch : hintTextMouse } 36 | 37 |
38 |
39 | ); 40 | } 41 | 42 | DefaultHint.displayName = 'DefaultHint'; 43 | 44 | DefaultHint.propTypes = { 45 | isTouchDetected: PropTypes.bool, 46 | hintTextMouse: PropTypes.string, 47 | hintTextTouch: PropTypes.string 48 | } 49 | 50 | export default DefaultHint; 51 | -------------------------------------------------------------------------------- /src/hint/DisplayUntilActive.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class DisplayUntilActive extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.hasShown = false; 9 | } 10 | 11 | static propTypes = { 12 | children: PropTypes.element, 13 | isActive: PropTypes.bool, 14 | shouldHideAfterFirstActivation: PropTypes.bool 15 | }; 16 | 17 | static defaultProps = { 18 | shouldHideAfterFirstActivation: true 19 | }; 20 | 21 | setHasShown() { 22 | this.hasShown = true; 23 | } 24 | 25 | render () { 26 | const { 27 | props: { 28 | children, 29 | isActive, 30 | shouldHideAfterFirstActivation 31 | }, 32 | hasShown, 33 | } = this; 34 | const shouldShow = !isActive && (!hasShown || !shouldHideAfterFirstActivation); 35 | 36 | if (isActive && !hasShown) { 37 | this.setHasShown(); 38 | } 39 | 40 | return shouldShow ? children : null; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/lens/negative-space/Lens.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import objectAssign from 'object-assign'; 4 | 5 | const Lens = (props) => { 6 | const { 7 | fadeDurationInMs, 8 | isActive, 9 | isPositionOutside, 10 | style: parentSpecifiedStyle 11 | } = props; 12 | 13 | const defaultStyle = { 14 | width: 'auto', 15 | height: 'auto', 16 | top: 'auto', 17 | right: 'auto', 18 | bottom: 'auto', 19 | left: 'auto', 20 | display: 'block' 21 | }; 22 | 23 | const computedStyle = { 24 | position: 'absolute', 25 | opacity: (isActive && !isPositionOutside) ? 1 : 0, 26 | transition: `opacity ${fadeDurationInMs}ms ease-in` 27 | }; 28 | 29 | const compositStyle = objectAssign( 30 | {}, 31 | defaultStyle, 32 | parentSpecifiedStyle, 33 | computedStyle 34 | ); 35 | 36 | return
; 37 | } 38 | 39 | Lens.propTypes = { 40 | style: PropTypes.object, 41 | fadeDurationInMs: PropTypes.number, 42 | isActive: PropTypes.bool, 43 | translateX: PropTypes.number, 44 | translateY: PropTypes.number, 45 | userStyle: PropTypes.object 46 | }; 47 | 48 | Lens.defaultProps = { 49 | isActive: false, 50 | fadeDurationInMs: 0, 51 | translateX: 0, 52 | translateY: 0 53 | }; 54 | 55 | export default Lens; 56 | -------------------------------------------------------------------------------- /src/lens/negative-space/LensBottom.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import objectAssign from 'object-assign'; 3 | import clamp from 'clamp'; 4 | import Lens from './Lens'; 5 | import LensPropTypes from '../../prop-types/Lens'; 6 | 7 | const LensBottom = ({ 8 | cursorOffset, 9 | position, 10 | fadeDurationInMs, 11 | isActive, 12 | isPositionOutside, 13 | smallImage, 14 | style: parentSpecifiedStyle 15 | }) => { 16 | 17 | const clearLensHeight = cursorOffset.y * 2; 18 | const computedHeight = smallImage.height - position.y - cursorOffset.y; 19 | const maxHeight = smallImage.height - clearLensHeight; 20 | const height = clamp(computedHeight, 0, maxHeight); 21 | const clearLensBottom = position.y + cursorOffset.y; 22 | const top = Math.max(clearLensBottom, clearLensHeight); 23 | const computedStyle = { 24 | height: `${height}px`, 25 | width: '100%', 26 | top: `${top}px` 27 | }; 28 | 29 | return ( 30 | 40 | ); 41 | }; 42 | 43 | LensBottom.propTypes = LensPropTypes; 44 | 45 | export default LensBottom; 46 | -------------------------------------------------------------------------------- /src/lens/negative-space/LensLeft.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import objectAssign from 'object-assign'; 3 | import clamp from 'clamp'; 4 | import Lens from './Lens'; 5 | import LensPropTypes from '../../prop-types/Lens'; 6 | 7 | const LensLeft = ({ 8 | cursorOffset, 9 | position, 10 | fadeDurationInMs, 11 | isActive, 12 | isPositionOutside, 13 | smallImage, 14 | style: parentSpecifiedStyle 15 | }) => { 16 | const clearLensHeight = cursorOffset.y * 2; 17 | const clearLensWidth = cursorOffset.x * 2; 18 | const maxHeight = smallImage.height - clearLensHeight; 19 | const maxWidth = smallImage.width - clearLensWidth; 20 | const height = clearLensHeight; 21 | const width = clamp(position.x - cursorOffset.x, 0, maxWidth); 22 | const top = clamp(position.y - cursorOffset.y, 0, maxHeight); 23 | const computedStyle = { 24 | height: `${height}px`, 25 | width: `${width}px`, 26 | top: `${top}px`, 27 | left: '0px' 28 | }; 29 | 30 | return ( 31 | 41 | ); 42 | }; 43 | 44 | LensLeft.propTypes = LensPropTypes; 45 | 46 | export default LensLeft; 47 | -------------------------------------------------------------------------------- /src/lens/negative-space/LensRight.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import objectAssign from 'object-assign'; 3 | import clamp from 'clamp'; 4 | import Lens from './Lens'; 5 | import LensPropTypes from '../../prop-types/Lens'; 6 | 7 | const LensRight = ({ 8 | cursorOffset, 9 | position, 10 | fadeDurationInMs, 11 | isActive, 12 | isPositionOutside, 13 | smallImage, 14 | style: parentSpecifiedStyle 15 | }) => { 16 | const clearLensHeight = cursorOffset.y * 2; 17 | const clearLensWidth = cursorOffset.x * 2; 18 | const maxHeight = smallImage.height - clearLensHeight; 19 | const maxWidth = smallImage.width - clearLensWidth; 20 | const height = clearLensHeight; 21 | const width = clamp(smallImage.width - position.x - cursorOffset.x, 0, maxWidth); 22 | const top = clamp(position.y - cursorOffset.y, 0, maxHeight); 23 | const computedStyle = { 24 | height: `${height}px`, 25 | width: `${width}px`, 26 | top: `${top}px`, 27 | right: '0px' 28 | }; 29 | 30 | return ( 31 | 41 | ); 42 | }; 43 | 44 | LensRight.propTypes = LensPropTypes; 45 | 46 | export default LensRight; 47 | -------------------------------------------------------------------------------- /src/lens/negative-space/LensTop.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clamp from 'clamp'; 3 | import objectAssign from 'object-assign'; 4 | import Lens from './Lens'; 5 | import LensPropTypes from '../../prop-types/Lens'; 6 | 7 | const LensTop = ({ 8 | cursorOffset, 9 | position, 10 | fadeDurationInMs, 11 | isActive, 12 | isPositionOutside, 13 | smallImage, 14 | style: parentSpecifiedStyle 15 | }) => { 16 | const clearLensHeight = cursorOffset.y * 2; 17 | const maxHeight = smallImage.height - clearLensHeight; 18 | const height = clamp(position.y - cursorOffset.y, 0, maxHeight); 19 | const computedStyle = { 20 | height: `${height}px`, 21 | width: '100%', 22 | top: '0px' 23 | }; 24 | 25 | return ( 26 | 36 | ); 37 | }; 38 | 39 | LensTop.propTypes = LensPropTypes; 40 | 41 | export default LensTop; 42 | -------------------------------------------------------------------------------- /src/lens/negative-space/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import objectAssign from 'object-assign'; 3 | 4 | import LensTop from './LensTop'; 5 | import LensLeft from './LensLeft'; 6 | import LensRight from './LensRight'; 7 | import LensBottom from './LensBottom'; 8 | 9 | import LensPropTypes from '../../prop-types/Lens'; 10 | 11 | export default function NegativeSpaceLens(inputProps) { 12 | const { style: userSpecifiedStyle } = inputProps; 13 | 14 | const compositLensStyle = objectAssign( 15 | { backgroundColor: 'rgba(0,0,0,.4)' }, 16 | userSpecifiedStyle 17 | ); 18 | 19 | const props = objectAssign( 20 | {}, 21 | inputProps, 22 | { style: compositLensStyle } 23 | ); 24 | 25 | return ( 26 |
27 | 28 | 29 | 30 | 31 |
32 | ); 33 | } 34 | 35 | NegativeSpaceLens.propTypes = LensPropTypes; 36 | -------------------------------------------------------------------------------- /src/lens/positive-space/assets/texture.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanselzer/react-image-magnify/ff834d0667f437c9d0affcd0208c2b5ce09e92ac/src/lens/positive-space/assets/texture.gif -------------------------------------------------------------------------------- /src/lens/positive-space/assets/textured-lens-data-uri.js: -------------------------------------------------------------------------------- 1 | export default 'data:image/gif;base64,R0lGODlhZABkAPABAHOf4fj48yH5BAEAAAEALAAAAABkAGQAAAL+jI+py+0PowOB2oqvznz7Dn5iSI7SiabqWrbj68bwTLL2jUv0Lvf8X8sJhzmg0Yc8mojM5kmZjEKPzqp1MZVqs7Cr98rdisOXr7lJHquz57YwDV8j3XRb/C7v1vcovD8PwicY8VcISDGY2GDIKKf4mNAoKQZZeXg5aQk5yRml+dgZ2vOpKGraQpp4uhqYKsgKi+H6iln7N8sXG4u7p2s7ykvnyxos/DuMWtyGfKq8fAwd5nzGHN067VUtiv2lbV3GDfY9DhQu7p1pXoU+rr5ODk/j7sSePk9Ub33PlN+4jx8v4JJ/RQQa3EDwzcGFiBLi6AfN4UOGCyXegGjIoh0fisQ0rsD4y+NHjgZFqgB5y2Qfks1UPmEZ0OVLlIcKAAA7'; 2 | -------------------------------------------------------------------------------- /src/lens/positive-space/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import objectAssign from 'object-assign'; 3 | import LensPropTypes from '../../prop-types/Lens'; 4 | import clamp from 'clamp'; 5 | import dataUri from './assets/textured-lens-data-uri'; 6 | 7 | export default class PositiveSpaceLens extends Component { 8 | static propTypes = LensPropTypes 9 | 10 | static defaultProps = { 11 | style: {} 12 | } 13 | 14 | get dimensions() { 15 | const { 16 | cursorOffset: { 17 | x: cursorOffsetX, 18 | y: cursorOffsetY 19 | } 20 | } = this.props; 21 | 22 | return { 23 | width: cursorOffsetX * 2, 24 | height: cursorOffsetY * 2 25 | }; 26 | } 27 | 28 | get positionOffset() { 29 | const { 30 | cursorOffset: { 31 | x: cursorOffsetX, 32 | y: cursorOffsetY 33 | }, 34 | position: { 35 | x: positionX, 36 | y: positionY 37 | }, 38 | smallImage: { 39 | height: imageHeight, 40 | width: imageWidth 41 | } 42 | } = this.props; 43 | 44 | const { 45 | width, 46 | height 47 | } = this.dimensions 48 | 49 | const top = positionY - cursorOffsetY; 50 | const left = positionX - cursorOffsetX; 51 | const maxTop = imageHeight - height; 52 | const maxLeft = imageWidth - width; 53 | const minOffset = 0; 54 | 55 | return { 56 | top: clamp(top, minOffset, maxTop), 57 | left: clamp(left, minOffset, maxLeft) 58 | }; 59 | } 60 | 61 | get defaultStyle() { 62 | const { fadeDurationInMs } = this.props; 63 | 64 | return { 65 | transition: `opacity ${fadeDurationInMs}ms ease-in`, 66 | backgroundImage: `url(${dataUri})` 67 | }; 68 | } 69 | 70 | get userSpecifiedStyle() { 71 | const { 72 | style 73 | } = this.props; 74 | 75 | return style; 76 | } 77 | 78 | get isVisible() { 79 | const { 80 | isActive, 81 | isPositionOutside 82 | } = this.props; 83 | 84 | return ( 85 | isActive && 86 | !isPositionOutside 87 | ); 88 | } 89 | 90 | get priorityStyle() { 91 | const { 92 | width, 93 | height 94 | } = this.dimensions 95 | 96 | const { 97 | top, 98 | left 99 | } = this.positionOffset 100 | 101 | return { 102 | position: 'absolute', 103 | top: `${top}px`, 104 | left: `${left}px`, 105 | width: `${width}px`, 106 | height: `${height}px`, 107 | opacity: this.isVisible ? 1 : 0 108 | }; 109 | } 110 | 111 | get compositStyle() { 112 | return objectAssign( 113 | this.defaultStyle, 114 | this.userSpecifiedStyle, 115 | this.priorityStyle 116 | ); 117 | } 118 | 119 | render() { 120 | return ( 121 |
122 | ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/lib/dimensions.js: -------------------------------------------------------------------------------- 1 | export function isPercentageFormat(val) { 2 | return ( 3 | typeof val === 'string' && 4 | /^\d+%$/.test(val) 5 | ); 6 | } 7 | 8 | export function convertPercentageToDecimal(percentage) { 9 | return parseInt(percentage) / 100; 10 | } 11 | 12 | export function getEnlargedImageContainerDimension({ containerDimension, smallImageDimension, isInPlaceMode }) { 13 | if (isInPlaceMode) { 14 | return smallImageDimension; 15 | } 16 | 17 | if (isPercentageFormat(containerDimension)) { 18 | return smallImageDimension * convertPercentageToDecimal(containerDimension); 19 | } 20 | 21 | return containerDimension; 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/imageCoordinates.js: -------------------------------------------------------------------------------- 1 | import clamp from 'clamp'; 2 | import { 3 | getContainerToImageRatio, 4 | getSmallToLargeImageRatio 5 | } from './imageRatio'; 6 | 7 | function getMinCoordinates(container, largeImage) { 8 | return { 9 | x: ((largeImage.width - container.width) * -1), 10 | y: ((largeImage.height - container.height) * -1) 11 | }; 12 | } 13 | 14 | function getMaxCoordinates() { 15 | return { 16 | x: 0, 17 | y: 0 18 | }; 19 | } 20 | 21 | export function getLensModeEnlargedImageCoordinates({ 22 | containerDimensions, 23 | cursorOffset: lensCursorOffset, 24 | largeImage, 25 | position, 26 | smallImage 27 | }) { 28 | const adjustedPosition = getCursorPositionAdjustedForLens(position, lensCursorOffset); 29 | const ratio = getSmallToLargeImageRatio(smallImage, largeImage); 30 | const coordinates = { 31 | x: (Math.round(adjustedPosition.x * ratio.x) * -1), 32 | y: (Math.round(adjustedPosition.y * ratio.y) * -1) 33 | }; 34 | const minCoordinates = getMinCoordinates(containerDimensions, largeImage); 35 | const maxCoordinates = getMaxCoordinates(); 36 | 37 | return clampImageCoordinates(coordinates, minCoordinates, maxCoordinates); 38 | } 39 | 40 | export function getInPlaceEnlargedImageCoordinates({ 41 | containerDimensions, 42 | largeImage, 43 | position 44 | }) { 45 | const minCoordinates = getMinCoordinates(containerDimensions, largeImage); 46 | const maxCoordinates = getMaxCoordinates(); 47 | const ratio = getContainerToImageRatio(containerDimensions, largeImage); 48 | const coordinates = { 49 | x: (Math.round(position.x * ratio.x) * -1), 50 | y: (Math.round(position.y * ratio.y) * -1) 51 | }; 52 | 53 | return clampImageCoordinates(coordinates, minCoordinates, maxCoordinates); 54 | } 55 | 56 | function clampImageCoordinates(imageCoordinates, minCoordinates, maxCoordinates) { 57 | return { 58 | x: clamp(imageCoordinates.x, minCoordinates.x, maxCoordinates.x), 59 | y: clamp(imageCoordinates.y, minCoordinates.y, maxCoordinates.y) 60 | }; 61 | } 62 | 63 | function getCursorPositionAdjustedForLens(position, lensCursorOffset) { 64 | return { 65 | x: position.x - lensCursorOffset.x, 66 | y: position.y - lensCursorOffset.y 67 | }; 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/lib/imageRatio.js: -------------------------------------------------------------------------------- 1 | 2 | export function getSmallToLargeImageRatio(smallImage, largeImage) { 3 | return getSmallToLargeElementRatio(smallImage, largeImage); 4 | } 5 | 6 | export function getLargeToSmallImageRatio(smallImage, largeImage) { 7 | return { 8 | x: smallImage.width / largeImage.width, 9 | y: smallImage.height / largeImage.height 10 | }; 11 | } 12 | 13 | export function getContainerToImageRatio(container, image) { 14 | return getSmallToLargeElementRatio( 15 | container, 16 | { 17 | width: image.width - container.width, 18 | height: image.height - container.height 19 | } 20 | ); 21 | } 22 | 23 | function getSmallToLargeElementRatio(smallElement, largeElement) { 24 | return { 25 | x: largeElement.width / smallElement.width, 26 | y: largeElement.height / smallElement.height 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/lens.js: -------------------------------------------------------------------------------- 1 | import { getLargeToSmallImageRatio } from './imageRatio'; 2 | 3 | export function getLensCursorOffset(smallImage, largeImage, enlargedImageContainerDimensions) { 4 | const ratio = getLargeToSmallImageRatio(smallImage, largeImage); 5 | return { 6 | x: getLensCursorOffsetDimension(enlargedImageContainerDimensions.width, ratio.x), 7 | y: getLensCursorOffsetDimension(enlargedImageContainerDimensions.height, ratio.y) 8 | } 9 | } 10 | 11 | function getLensCursorOffsetDimension(enlargedImageContainerDimension, ratio) { 12 | return Math.round((enlargedImageContainerDimension * ratio) / 2); 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/styles.js: -------------------------------------------------------------------------------- 1 | import objectAssign from 'object-assign'; 2 | import isEqual from 'fast-deep-equal'; 3 | 4 | export function getContainerStyle(smallImage, userSpecifiedStyle) { 5 | const { 6 | isFluidWidth: isSmallImageFluidWidth, 7 | width, 8 | height 9 | } = smallImage; 10 | 11 | const fluidWidthContainerStyle = { 12 | width: 'auto', 13 | height: 'auto', 14 | fontSize: '0px', 15 | position: 'relative' 16 | } 17 | 18 | const fixedWidthContainerStyle = { 19 | width: `${width}px`, 20 | height: `${height}px`, 21 | position: 'relative' 22 | }; 23 | 24 | const priorityContainerStyle = isSmallImageFluidWidth 25 | ? fluidWidthContainerStyle 26 | : fixedWidthContainerStyle; 27 | 28 | const compositContainerStyle = objectAssign( 29 | { cursor: 'crosshair' }, 30 | userSpecifiedStyle, 31 | priorityContainerStyle 32 | ); 33 | 34 | return compositContainerStyle; 35 | } 36 | 37 | export function getSmallImageStyle(smallImage, style) { 38 | const { 39 | isFluidWidth: isSmallImageFluidWidth, 40 | width, 41 | height 42 | } = smallImage; 43 | 44 | const fluidWidthSmallImageStyle = { 45 | width: '100%', 46 | height: 'auto', 47 | display: 'block', 48 | pointerEvents: 'none' 49 | }; 50 | 51 | const fixedWidthSmallImageStyle = { 52 | width: `${width}px`, 53 | height: `${height}px`, 54 | pointerEvents: 'none' 55 | }; 56 | 57 | const prioritySmallImageStyle = isSmallImageFluidWidth 58 | ? fluidWidthSmallImageStyle 59 | : fixedWidthSmallImageStyle; 60 | 61 | const compositSmallImageStyle = objectAssign( 62 | {}, 63 | style, 64 | prioritySmallImageStyle 65 | ); 66 | 67 | return compositSmallImageStyle; 68 | } 69 | 70 | function getPrimaryEnlargedImageContainerStyle(isInPlaceMode, isPortalRendered) { 71 | const baseContainerStyle = { 72 | overflow: 'hidden' 73 | }; 74 | 75 | if (isPortalRendered) { 76 | return baseContainerStyle 77 | } 78 | 79 | const sharedPositionStyle = { 80 | position: 'absolute', 81 | top: '0px', 82 | }; 83 | 84 | if (isInPlaceMode) { 85 | return objectAssign( 86 | baseContainerStyle, 87 | sharedPositionStyle, 88 | { left: '0px' } 89 | ); 90 | } 91 | 92 | return objectAssign( 93 | baseContainerStyle, 94 | sharedPositionStyle, 95 | { 96 | left: '100%', 97 | marginLeft: '10px', 98 | border: '1px solid #d6d6d6' 99 | } 100 | ); 101 | } 102 | 103 | function getPriorityEnlargedImageContainerStyle(params) { 104 | const { 105 | containerDimensions, 106 | fadeDurationInMs, 107 | isTransitionActive 108 | } = params; 109 | 110 | return { 111 | width: containerDimensions.width, 112 | height: containerDimensions.height, 113 | opacity: isTransitionActive ? 1 : 0, 114 | transition: `opacity ${fadeDurationInMs}ms ease-in`, 115 | pointerEvents: 'none' 116 | }; 117 | } 118 | 119 | const enlargedImageContainerStyleCache = {}; 120 | 121 | export function getEnlargedImageContainerStyle(params) { 122 | const cache = enlargedImageContainerStyleCache; 123 | const { 124 | params: memoizedParams = {}, 125 | compositStyle: memoizedStyle 126 | } = cache; 127 | 128 | if (isEqual(memoizedParams, params)) { 129 | return memoizedStyle; 130 | } 131 | 132 | const { 133 | containerDimensions, 134 | containerStyle: userSpecifiedStyle, 135 | fadeDurationInMs, 136 | isTransitionActive, 137 | isInPlaceMode, 138 | isPortalRendered 139 | } = params; 140 | 141 | const primaryStyle = getPrimaryEnlargedImageContainerStyle(isInPlaceMode, isPortalRendered); 142 | const priorityStyle = getPriorityEnlargedImageContainerStyle({ 143 | containerDimensions, 144 | fadeDurationInMs, 145 | isTransitionActive 146 | }); 147 | 148 | cache.compositStyle = objectAssign( 149 | {}, 150 | primaryStyle, 151 | userSpecifiedStyle, 152 | priorityStyle 153 | ); 154 | cache.params = params; 155 | 156 | return cache.compositStyle; 157 | } 158 | 159 | export function getEnlargedImageStyle(params) { 160 | const { 161 | imageCoordinates, 162 | imageStyle: userSpecifiedStyle, 163 | largeImage 164 | } = params; 165 | 166 | const translate = `translate(${imageCoordinates.x}px, ${imageCoordinates.y}px)`; 167 | 168 | const priorityStyle = { 169 | width: largeImage.width, 170 | height: largeImage.height, 171 | transform: translate, 172 | WebkitTransform: translate, 173 | msTransform: translate, 174 | pointerEvents: 'none' 175 | }; 176 | 177 | const compositeImageStyle = objectAssign( 178 | {}, 179 | userSpecifiedStyle, 180 | priorityStyle 181 | ); 182 | 183 | return compositeImageStyle; 184 | } 185 | -------------------------------------------------------------------------------- /src/prop-types/EnlargedImage.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { ENLARGED_IMAGE_POSITION } from '../constants'; 3 | 4 | export const EnlargedImagePosition = PropTypes.oneOf([ 5 | ENLARGED_IMAGE_POSITION.beside, 6 | ENLARGED_IMAGE_POSITION.over 7 | ]); 8 | 9 | export const EnlargedImageContainerDimensions = PropTypes.shape({ 10 | width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 11 | height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]) 12 | }); 13 | 14 | export const ContainerDimensions = PropTypes.shape({ 15 | width: PropTypes.number, 16 | height: PropTypes.number 17 | }); 18 | -------------------------------------------------------------------------------- /src/prop-types/Image.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import requiredIf from 'react-required-if'; 3 | import objectAssign from 'object-assign'; 4 | 5 | const BaseImageShape = { 6 | alt: PropTypes.string, 7 | src: PropTypes.string.isRequired, 8 | srcSet: PropTypes.string, 9 | sizes: PropTypes.string, 10 | onLoad: PropTypes.func, 11 | onError: PropTypes.func 12 | } 13 | 14 | export const LargeImageShape = PropTypes.shape( 15 | objectAssign( 16 | {}, 17 | BaseImageShape, 18 | { 19 | width: PropTypes.number.isRequired, 20 | height: PropTypes.number.isRequired 21 | } 22 | ) 23 | ); 24 | 25 | export const SmallImageShape = PropTypes.shape( 26 | objectAssign( 27 | {}, 28 | BaseImageShape, 29 | { 30 | isFluidWidth: PropTypes.bool, 31 | width: requiredIf(PropTypes.number, props => !props.isFluidWidth), 32 | height: requiredIf(PropTypes.number, props => !props.isFluidWidth) 33 | } 34 | ) 35 | ); 36 | -------------------------------------------------------------------------------- /src/prop-types/Lens.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import Point from './Point'; 3 | import { SmallImageShape } from './Image'; 4 | 5 | export default { 6 | cursorOffset: Point, 7 | fadeDurationInMs: PropTypes.number, 8 | isActive: PropTypes.bool, 9 | isPositionOutside: PropTypes.bool, 10 | position: Point, 11 | smallImage: SmallImageShape, 12 | style: PropTypes.object 13 | }; 14 | -------------------------------------------------------------------------------- /src/prop-types/Point.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export default PropTypes.shape({ 4 | x: PropTypes.number.isRequired, 5 | y: PropTypes.number.isRequired 6 | }); -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export function noop() {} 2 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env":{ 3 | "node": true, 4 | "mocha": true 5 | }, 6 | "rules":{ 7 | 8 | } 9 | } -------------------------------------------------------------------------------- /test/enlarged-image.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import sinon from 'sinon'; 5 | import EnlargedImage from '../src/EnlargedImage'; 6 | import * as utils from '../src/utils'; 7 | 8 | describe('Enlarged Image', () => { 9 | let shallowWrapper; 10 | 11 | beforeEach(() => { 12 | shallowWrapper = getShallowWrapper(); 13 | 14 | shallowWrapper.setState({ 15 | isActive: true, 16 | isTransitionActive: true 17 | }); 18 | }); 19 | 20 | it('has display name EnlargedImage', () => { 21 | expect(EnlargedImage.displayName).to.equal('EnlargedImage'); 22 | }); 23 | 24 | it('has correct initial state', () => { 25 | const shallowWrapper = getShallowWrapper(); 26 | expect(shallowWrapper.state()).to.deep.equal({ 27 | isTransitionEntering: false, 28 | isTransitionActive: false, 29 | isTransitionLeaving: false, 30 | isTransitionDone: false 31 | }); 32 | }); 33 | 34 | it('has correct default props', () => { 35 | expect(EnlargedImage.defaultProps).to.deep.equal({ 36 | fadeDurationInMs: 0, 37 | isLazyLoaded: true 38 | }); 39 | }); 40 | 41 | it('renders lazily by default', () => { 42 | const wrapper = getShallowWrapper(); 43 | expect(wrapper.find('div')).to.have.length(0); 44 | }); 45 | 46 | it('renders nonlazily if isLazyLoaded is set to false', () => { 47 | const wrapper = getShallowWrapper({ isLazyLoaded: false }); 48 | expect(wrapper.find('div')).to.have.length(1); 49 | }); 50 | 51 | it('cleans up timers on teardown', () => { 52 | const instance = shallowWrapper.instance(); 53 | instance.timers = [1, 2]; 54 | sinon.spy(global, 'clearTimeout'); 55 | 56 | shallowWrapper.unmount(); 57 | 58 | expect(global.clearTimeout.calledTwice).to.be.true; 59 | expect(global.clearTimeout.getCall(0).args[0]).to.equal(1); 60 | expect(global.clearTimeout.getCall(1).args[0]).to.equal(2); 61 | global.clearTimeout.restore(); 62 | }); 63 | 64 | describe('Props API', () => { 65 | 66 | it('applies containerClassName to container CSS class', () => { 67 | shallowWrapper.setProps({ containerClassName: 'foo' }); 68 | 69 | const renderedWrapper = shallowWrapper.render(); 70 | 71 | expect(renderedWrapper.hasClass('foo')).to.be.true; 72 | }); 73 | 74 | it('applies containerStyle to container CSS style', () => { 75 | const borderValue = '2px dashed #000'; 76 | shallowWrapper.setProps({ 77 | containerStyle:{ border: borderValue } 78 | }); 79 | const renderedWrapper = shallowWrapper.render(); 80 | 81 | expect(renderedWrapper.css('border')).to.equal(borderValue); 82 | }); 83 | 84 | it('applies fadeDurationInMs to container CSS opacity transition', () => { 85 | shallowWrapper.setProps({ fadeDurationInMs: 100 }); 86 | const renderedWrapper = shallowWrapper.render(); 87 | 88 | expect(renderedWrapper.css('transition')).to.equal('opacity 100ms ease-in'); 89 | }); 90 | 91 | it('applies imageClassName to image CSS class', () => { 92 | shallowWrapper.setProps({ imageClassName: 'foo' }); 93 | 94 | const renderedWrapper = shallowWrapper.render(); 95 | 96 | expect(renderedWrapper.find('img').hasClass('foo')).to.be.true; 97 | }); 98 | 99 | it('applies imageStyle to image CSS style', () => { 100 | const borderValue = '2px dashed #000'; 101 | shallowWrapper.setProps({ 102 | imageStyle:{ border: borderValue } 103 | }); 104 | const renderedWrapper = shallowWrapper.render(); 105 | 106 | expect(renderedWrapper.find('img').css('border')).to.equal(borderValue); 107 | }); 108 | 109 | it('applies CSS to container element based on isInPlaceMode prop', () => { 110 | shallowWrapper.setProps({ isInPlaceMode: true }); 111 | expect(shallowWrapper.render().css('left')).to.equal('0px'); 112 | 113 | shallowWrapper.setProps({ isInPlaceMode: false }); 114 | expect(shallowWrapper.render().css('left')).to.equal('100%'); 115 | expect(shallowWrapper.render().css('margin-left')).to.equal('10px'); 116 | expect(shallowWrapper.render().css('border')).to.equal('1px solid #d6d6d6'); 117 | }); 118 | 119 | it('applies large image alt', () => { 120 | shallowWrapper.setProps({ largeImage: {alt: 'foo'}}); 121 | 122 | const renderedWrapper = shallowWrapper.render(); 123 | 124 | expect(renderedWrapper.find('img').attr('alt')).to.equal('foo'); 125 | }); 126 | 127 | it('defaults large image alt to empty string', () => { 128 | const renderedWrapper = shallowWrapper.render(); 129 | 130 | expect(renderedWrapper.find('img').attr('alt')).to.equal(''); 131 | }); 132 | 133 | it('applies large image src', () => { 134 | const renderedWrapper = shallowWrapper.render(); 135 | 136 | expect(renderedWrapper.find('img').attr('src')).to.equal('bar'); 137 | }); 138 | 139 | it('applies large image srcSet', () => { 140 | const renderedWrapper = shallowWrapper.render(); 141 | 142 | expect(renderedWrapper.find('img').attr('srcset')).to.equal('corge'); 143 | }); 144 | 145 | it('applies large image width', () => { 146 | const renderedWrapper = shallowWrapper.render(); 147 | 148 | expect(renderedWrapper.find('img').css('width')).to.equal('12px'); 149 | }); 150 | 151 | it('applies large image height', () => { 152 | const renderedWrapper = shallowWrapper.render(); 153 | 154 | expect(renderedWrapper.find('img').css('height')).to.equal('16px'); 155 | }); 156 | 157 | describe('Load Event', () => { 158 | it('supports a listener function', () => { 159 | const onLoad = sinon.spy(); 160 | shallowWrapper.setProps({ 161 | largeImage: Object.assign( 162 | {}, 163 | props.largeImage, 164 | { onLoad } 165 | ) 166 | }); 167 | 168 | shallowWrapper.find('img').simulate('load'); 169 | 170 | expect(onLoad.called).to.be.true; 171 | }); 172 | 173 | it('provides the browser event object to listener function', () => { 174 | const onLoad = sinon.spy(); 175 | shallowWrapper.setProps({ 176 | largeImage: Object.assign( 177 | {}, 178 | props.largeImage, 179 | { onLoad } 180 | ) 181 | }); 182 | const eventObject = {}; 183 | 184 | shallowWrapper.find('img').simulate('load', eventObject); 185 | 186 | const listenerArguments = onLoad.getCall(0).args; 187 | expect(listenerArguments.length).to.equal(1); 188 | expect(listenerArguments[0]).to.equal(eventObject); 189 | }); 190 | 191 | it('defaults the listener to noop', () => { 192 | sinon.spy(utils, 'noop'); 193 | const shallowWrapper = getShallowWrapper(); 194 | shallowWrapper.setState({ 195 | isActive: true, 196 | isTransitionActive: true 197 | }); 198 | 199 | shallowWrapper.find('img').simulate('load'); 200 | 201 | expect(utils.noop.called).to.be.true; 202 | 203 | utils.noop.restore(); 204 | }); 205 | }); 206 | 207 | describe('Error Event', () => { 208 | it('supports a listener function', () => { 209 | const onError = sinon.spy(); 210 | shallowWrapper.setProps({ 211 | largeImage: Object.assign( 212 | {}, 213 | props.largeImage, 214 | { onError } 215 | ) 216 | }); 217 | 218 | shallowWrapper.find('img').simulate('error'); 219 | 220 | expect(onError.called).to.be.true; 221 | }); 222 | 223 | it('provides the browser event object to listener function', () => { 224 | const onError = sinon.spy(); 225 | shallowWrapper.setProps({ 226 | largeImage: Object.assign( 227 | {}, 228 | props.largeImage, 229 | { onError } 230 | ) 231 | }); 232 | const eventObject = {}; 233 | 234 | shallowWrapper.find('img').simulate('error', eventObject); 235 | 236 | const listenerArguments = onError.getCall(0).args; 237 | expect(listenerArguments.length).to.equal(1); 238 | expect(listenerArguments[0]).to.equal(eventObject); 239 | }); 240 | 241 | it('defaults the listener to noop', () => { 242 | sinon.spy(utils, 'noop'); 243 | const shallowWrapper = getShallowWrapper(); 244 | shallowWrapper.setState({ 245 | isActive: true, 246 | isTransitionActive: true 247 | }); 248 | 249 | 250 | shallowWrapper.find('img').simulate('error'); 251 | 252 | expect(utils.noop.called).to.be.true; 253 | 254 | utils.noop.restore(); 255 | }); 256 | }); 257 | }); 258 | 259 | describe('Container Element', () => { 260 | 261 | it('displays if transition is entering', () => { 262 | shallowWrapper.setState({ 263 | isTransitionEntering: true, 264 | isTransitionActive: false, 265 | isTransitionLeaving: false, 266 | isTransitionDone: false 267 | }); 268 | 269 | expect(shallowWrapper.find('div').length).to.equal(1); 270 | }); 271 | 272 | it('displays if transition is active', () => { 273 | expect(shallowWrapper.find('div').length).to.equal(1); 274 | 275 | shallowWrapper.setState({ 276 | isTransitionEntering: false, 277 | isTransitionActive: true, 278 | isTransitionLeaving: false, 279 | isTransitionDone: false 280 | }); 281 | 282 | expect(shallowWrapper.find('div').length).to.equal(1); 283 | }); 284 | 285 | it('displays if transition is leaving', () => { 286 | shallowWrapper.setState({ 287 | isTransitionEntering: false, 288 | isTransitionActive: false, 289 | isTransitionLeaving: true, 290 | isTransitionDone: false 291 | }); 292 | 293 | expect(shallowWrapper.find('div').length).to.equal(1); 294 | }); 295 | 296 | it('does not display if transition is done', () => { 297 | shallowWrapper.setState({ 298 | isTransitionEntering: false, 299 | isTransitionActive: false, 300 | isTransitionLeaving: false, 301 | isTransitionDone: true, 302 | }); 303 | 304 | expect(shallowWrapper.find('div').length).to.equal(0); 305 | }); 306 | 307 | it('applies a value of 0 to CSS opacity property when transition is entering', () => { 308 | shallowWrapper.setState({ 309 | isTransitionEntering: true, 310 | isTransitionActive: false, 311 | isTransitionLeaving: false, 312 | isTransitionDone: false 313 | }); 314 | const renderedWrapper = shallowWrapper.render(); 315 | 316 | expect(renderedWrapper.css('opacity')).to.equal('0'); 317 | }); 318 | 319 | it('applies a value of 1 to CSS opacity property when transition is active', () => { 320 | shallowWrapper.setState({ 321 | isTransitionEntering: false, 322 | isTransitionActive: true, 323 | isTransitionLeaving: false, 324 | isTransitionDone: false 325 | }); 326 | const renderedWrapper = shallowWrapper.render(); 327 | 328 | expect(renderedWrapper.css('opacity')).to.equal('1'); 329 | }); 330 | 331 | it('applies a value of 0 to CSS opacity property when transition is leaving', () => { 332 | shallowWrapper.setState({ 333 | isTransitionEntering: false, 334 | isTransitionActive: false, 335 | isTransitionLeaving: true, 336 | isTransitionDone: false 337 | }); 338 | const renderedWrapper = shallowWrapper.render(); 339 | 340 | expect(renderedWrapper.css('opacity')).to.equal('0'); 341 | }); 342 | 343 | it('applies correct style for placement to the right side of the small image (default)', () => { 344 | const expected = 'overflow:hidden;position:absolute;top:0px;left:100%;margin-left:10px;border:1px solid #d6d6d6;width:3px;height:4px;opacity:1;transition:opacity 0ms ease-in;pointer-events:none'; 345 | 346 | const renderedWrapper = shallowWrapper.render(); 347 | 348 | expect(renderedWrapper.attr('style')).to.equal(expected); 349 | }); 350 | 351 | it('applies correct style for placement over the small image ', () => { 352 | const expected = 'overflow:hidden;position:absolute;top:0px;left:0px;width:3px;height:4px;opacity:1;transition:opacity 0ms ease-in;pointer-events:none'; 353 | shallowWrapper.setProps({ isInPlaceMode: true }); 354 | 355 | const renderedWrapper = shallowWrapper.render(); 356 | 357 | expect(renderedWrapper.attr('style')).to.equal(expected); 358 | }); 359 | 360 | it('applies correct style for portal rendering', () => { 361 | const expected = 'overflow:hidden;width:3px;height:4px;opacity:1;transition:opacity 0ms ease-in;pointer-events:none'; 362 | shallowWrapper.setProps({isPortalRendered: true}); 363 | 364 | const renderedWrapper = shallowWrapper.render(); 365 | 366 | expect(renderedWrapper.attr('style')).to.equal(expected); 367 | }); 368 | 369 | }); 370 | 371 | describe('Image Element', () => { 372 | 373 | it('computes cursor position and applies the result to CSS transfrom translate', () => { 374 | shallowWrapper.setProps({ 375 | position: { 376 | x: 1, 377 | y: 2 378 | } 379 | }); 380 | const renderedWrapper = shallowWrapper.render(); 381 | 382 | expect(renderedWrapper.find('img').css('transform')).to.equal('translate(-4px, -8px)'); 383 | }); 384 | 385 | it('computes cursor offset and applies the result to CSS transfrom translate', () => { 386 | shallowWrapper.setProps({ 387 | cursorOffset: { 388 | x: 1, 389 | y: 2 390 | }, 391 | position: { 392 | x: 2, 393 | y: 4 394 | } 395 | }); 396 | const renderedWrapper = shallowWrapper.render(); 397 | 398 | expect(renderedWrapper.find('img').css('transform')).to.equal('translate(-4px, -8px)'); 399 | }); 400 | 401 | it('computes image size ratio and applies the result to CSS transfrom translate', () => { 402 | shallowWrapper.setProps({ 403 | cursorOffset: { 404 | x: 0, 405 | y: 0 406 | }, 407 | isActive: true, 408 | position: { 409 | x: 1, 410 | y: 2 411 | }, 412 | largeImage: { 413 | src: 'foo', 414 | width: 8, 415 | height: 8 416 | }, 417 | smallImage: { 418 | src: 'bar', 419 | width: 4, 420 | height: 4 421 | } 422 | }); 423 | const renderedWrapper = shallowWrapper.render(); 424 | 425 | expect(renderedWrapper.find('img').css('transform')).to.equal('translate(-2px, -4px)'); 426 | }); 427 | 428 | it('computes max coordinates and applies the result to CSS transfrom translate', () => { 429 | shallowWrapper.setProps({ 430 | containerDimensions: { 431 | width: 4, 432 | height: 4 433 | }, 434 | cursorOffset: { 435 | x: 0, 436 | y: 0 437 | }, 438 | isPositionOutside: true, 439 | position: { 440 | x: 5, 441 | y: 5 442 | }, 443 | largeImage: { 444 | src: 'foo', 445 | width: 8, 446 | height: 8 447 | }, 448 | smallImage: { 449 | src: 'bar', 450 | width: 4, 451 | height: 4 452 | } 453 | }); 454 | const renderedWrapper = shallowWrapper.render(); 455 | 456 | expect(renderedWrapper.find('img').css('transform')).to.equal('translate(-4px, -4px)'); 457 | }); 458 | 459 | it('computes min coordinates and applies the result to CSS transfrom translate', () => { 460 | shallowWrapper.setProps({ 461 | cursorOffset: { 462 | x: 0, 463 | y: 0 464 | }, 465 | position: { 466 | x: -1, 467 | y: -1 468 | }, 469 | largeImage: { 470 | src: 'foo', 471 | width: 8, 472 | height: 8 473 | }, 474 | smallImage: { 475 | src: 'bar', 476 | width: 4, 477 | height: 4 478 | } 479 | }); 480 | const renderedWrapper = shallowWrapper.render(); 481 | 482 | expect(renderedWrapper.find('img').css('transform')).to.equal('translate(0px, 0px)'); 483 | }); 484 | 485 | it('applies vendor prefixes to CSS transform property', () => { 486 | shallowWrapper.setProps({ 487 | position: { 488 | x: 1, 489 | y: 2 490 | } 491 | }); 492 | const renderedWrapper = shallowWrapper.render(); 493 | 494 | expect(renderedWrapper.find('img').css('transform')).to.equal('translate(-4px, -8px)'); 495 | expect(renderedWrapper.find('img').css('-ms-transform')).to.equal('translate(-4px, -8px)'); 496 | expect(renderedWrapper.find('img').css('-webkit-transform')).to.equal('translate(-4px, -8px)'); 497 | }); 498 | 499 | }); 500 | 501 | const props = { 502 | containerDimensions: { 503 | width: 3, 504 | height: 4 505 | }, 506 | cursorOffset: { 507 | x: 0, 508 | y: 0 509 | }, 510 | position: { 511 | x: 0, 512 | y: 0 513 | }, 514 | fadeDurationInMs: 0, 515 | isActive: false, 516 | largeImage: { 517 | src: 'bar', 518 | srcSet: 'corge', 519 | width: 12, 520 | height: 16 521 | }, 522 | smallImage: { 523 | alt: 'baz', 524 | src: 'qux', 525 | srcSet: 'quux', 526 | width: 3, 527 | height: 4 528 | } 529 | }; 530 | 531 | function getShallowWrapper(optionalProps) { 532 | return shallow( 533 | 534 | ); 535 | } 536 | }); 537 | -------------------------------------------------------------------------------- /test/lens/lens.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import Lens from '../../src/lens/negative-space/Lens'; 5 | 6 | describe('Image Lens', () => { 7 | it('applies computed style', () => { 8 | const expected = 'width:auto;height:auto;top:auto;right:auto;bottom:auto;left:auto;display:block;position:absolute;opacity:0;transition:opacity 0ms ease-in'; 9 | 10 | const c = render(); 11 | 12 | expect(c.attr('style')).to.equal(expected); 13 | }); 14 | 15 | it('applies supplied style', () => { 16 | const expected = 'width:1px;height:2px;top:3px;right:4px;bottom:5px;left:6px;display:inline-block;background-color:#fff;cursor:pointer;' 17 | 18 | const c = render( 19 | 32 | ); 33 | 34 | expect(c.attr('style').startsWith(expected)).to.be.true; 35 | }); 36 | 37 | it('applies a value of 0 to CSS opacity property when isActive is unset', () => { 38 | const c = render(); 39 | 40 | expect(c.css('opacity')).to.equal('0'); 41 | }); 42 | 43 | it('applies a value of 1 to CSS opacity property when isActive is set', () => { 44 | const c = render(); 45 | 46 | expect(c.css('opacity')).to.equal('1'); 47 | }); 48 | 49 | it('applies default CSS opacity transition of 0 milliseconds', () => { 50 | const c = render(); 51 | 52 | expect(c.css('transition')).to.equal('opacity 0ms ease-in'); 53 | }); 54 | 55 | it('applies supplied CSS opacity transition', () => { 56 | const c = render(); 57 | 58 | expect(c.css('transition')).to.equal('opacity 100ms ease-in'); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/lens/negative-space.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import Lens from '../../src/lens/negative-space'; 5 | 6 | describe('Shaded Lens', () => { 7 | const smallImage = { 8 | alt: 'baz', 9 | isFluidWidth: false, 10 | src: 'qux', 11 | srcSet: 'quux', 12 | sizes: 'grault', 13 | width: 3, 14 | height: 4 15 | }; 16 | 17 | const props = { 18 | cursorOffset: { x: 0, y: 0 }, 19 | fadeDurationInMs: 100, 20 | isActive: true, 21 | isPositionOutside: false, 22 | position: { x: 1, y: 2 }, 23 | smallImage, 24 | style: {} 25 | }; 26 | 27 | const defaultBackgroundStyle = { backgroundColor: 'rgba(0,0,0,.4)' }; 28 | 29 | let mountedWrapper = mount(); 30 | 31 | beforeEach(() => { 32 | mountedWrapper = mount(); 33 | }); 34 | 35 | it('applies props to lens elements', () => { 36 | const expected = Object.assign( 37 | {}, 38 | props, 39 | { style: defaultBackgroundStyle } 40 | ); 41 | 42 | expect(mountedWrapper.find('LensTop').props()).to.deep.equal(expected); 43 | expect(mountedWrapper.find('LensLeft').props()).to.deep.equal(expected); 44 | expect(mountedWrapper.find('LensRight').props()).to.deep.equal(expected); 45 | expect(mountedWrapper.find('LensBottom').props()).to.deep.equal(expected); 46 | }); 47 | 48 | it('applies default sytle to lens elements', () => { 49 | const expected = defaultBackgroundStyle; 50 | 51 | expect(mountedWrapper.find('LensTop').props().style).to.deep.equal(expected); 52 | expect(mountedWrapper.find('LensLeft').props().style).to.deep.equal(expected); 53 | expect(mountedWrapper.find('LensRight').props().style).to.deep.equal(expected); 54 | expect(mountedWrapper.find('LensBottom').props().style).to.deep.equal(expected); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/lens/positive-space.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | import Lens from '../../src/lens/positive-space'; 5 | 6 | describe('Positive Space Lens', () => { 7 | const smallImage = { 8 | alt: 'baz', 9 | isFluidWidth: false, 10 | src: 'qux', 11 | srcSet: 'quux', 12 | sizes: 'grault', 13 | width: 6, 14 | height: 8 15 | }; 16 | 17 | const defaultProps = { 18 | cursorOffset: { x: 1, y: 2 }, 19 | fadeDurationInMs: 100, 20 | isActive: true, 21 | isPositionOutside: false, 22 | position: { x: 3, y: 4 }, 23 | smallImage, 24 | style: {} 25 | }; 26 | 27 | function getComponent(props) { 28 | const compositProps = Object.assign( 29 | {}, 30 | defaultProps, 31 | props 32 | ); 33 | 34 | return shallow( 35 | 36 | ) 37 | } 38 | 39 | let component = getComponent(); 40 | 41 | beforeEach(() => { 42 | component = getComponent(); 43 | }); 44 | 45 | it('defaults style to an empty object', () => { 46 | const component = getComponent({ style: undefined }); 47 | 48 | expect(component.prop('style')).to.exist; 49 | }) 50 | 51 | describe('Computed Functional Style', () => { 52 | it('computes correct height', () => { 53 | expect(component.prop('style').height).to.equal('4px'); 54 | }); 55 | 56 | it('computes correct width', () => { 57 | expect(component.prop('style').width).to.equal('2px'); 58 | }); 59 | 60 | it('prioritizes user specified style over default style', () => { 61 | const component = getComponent({ 62 | style: { 63 | transition: 'foo', 64 | backgroundImage: 'bar' 65 | } 66 | }); 67 | 68 | expect(component.prop('style').transition).to.equal('foo'); 69 | expect(component.prop('style').backgroundImage).to.equal('bar'); 70 | }); 71 | 72 | it('prioritizes computed style over user specified style', () => { 73 | const component = getComponent({ 74 | style: { 75 | position: 'foo', 76 | top: 'bar', 77 | left: 'baz', 78 | width: 'qux', 79 | height: 'grault', 80 | opacity: 'foobar' 81 | } 82 | }); 83 | 84 | expect(component.prop('style')).to.include({ 85 | position: 'absolute', 86 | top: '2px', 87 | left: '2px', 88 | width: '2px', 89 | height: '4px', 90 | opacity: 1 91 | }); 92 | }); 93 | 94 | describe('top', () => { 95 | it('computes min correctly', () => { 96 | const component = getComponent({ 97 | position: { 98 | x: 1, 99 | y: 1 100 | } 101 | }); 102 | expect(component.prop('style').top).to.equal('0px') 103 | }); 104 | 105 | it('computes midrange correctly', () => { 106 | expect(component.prop('style').top).to.equal('2px') 107 | }); 108 | 109 | it('computes max correctly', () => { 110 | const component = getComponent({ 111 | position: { 112 | x: 1, 113 | y: 7 114 | } 115 | }); 116 | expect(component.prop('style').top).to.equal('4px') 117 | }); 118 | }); 119 | 120 | describe('left', () => { 121 | it('computes min correctly', () => { 122 | const component = getComponent({ 123 | cursorOffset: { 124 | x: 2, 125 | y: 2 126 | }, 127 | position: { 128 | x: 1, 129 | y: 1 130 | } 131 | }); 132 | expect(component.prop('style').left).to.equal('0px') 133 | }); 134 | 135 | it('computes mindrange correctly', () => { 136 | expect(component.prop('style').left).to.equal('2px'); 137 | }); 138 | 139 | it('computes max correctly', () => { 140 | const component = getComponent({ 141 | cursorOffset: { 142 | x: 2, 143 | y: 2 144 | }, 145 | position: { 146 | x: 5, 147 | y: 1 148 | } 149 | }); 150 | expect(component.prop('style').left).to.equal('2px') 151 | }); 152 | }); 153 | 154 | describe('opacity', () => { 155 | it('sets opacity to 1 when active and not outside bounds', () => { 156 | expect(component.prop('style').opacity).to.equal(1); 157 | }); 158 | 159 | it('sets opacity to 0 when not active and not outside bounds', () => { 160 | const component = getComponent({ isActive: false }); 161 | 162 | expect(component.prop('style').opacity).to.equal(0); 163 | }); 164 | 165 | it('sets opacity to 0 when active and outside bounds', () => { 166 | const component = getComponent({ isPositionOutside: true }); 167 | 168 | expect(component.prop('style').opacity).to.equal(0); 169 | }); 170 | }); 171 | }); 172 | }); 173 | -------------------------------------------------------------------------------- /test/lib/dimensions.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { 3 | convertPercentageToDecimal, 4 | getEnlargedImageContainerDimension, 5 | isPercentageFormat 6 | } from '../../src/lib/dimensions'; 7 | 8 | describe('Dimensions Library', () => { 9 | describe('isPercentageFormat', () => { 10 | it('returns true when input is formatted as a percentage', () => { 11 | const actual = isPercentageFormat('100%'); 12 | expect(actual).to.be.true; 13 | }); 14 | 15 | it('returns false when input is not formatted as a percentage', () => { 16 | expect(isPercentageFormat('100')).to.be.false; 17 | expect(isPercentageFormat(100)).to.be.false; 18 | }) 19 | }); 20 | 21 | describe('convertPercentageToDecimal', () => { 22 | it('returns a decimal number for percentage input', () => { 23 | expect(convertPercentageToDecimal('75%')).to.equal(0.75); 24 | }); 25 | }); 26 | 27 | describe('getEnlargedImageContainerDimension', () => { 28 | it('returns correct value when container dimension is a percentage', () => { 29 | const actual = getEnlargedImageContainerDimension({ 30 | containerDimension: '50%', 31 | smallImageDimension: 2 32 | }); 33 | 34 | expect(actual).to.equal(1); 35 | }); 36 | 37 | it('returns correct value when container dimension is a number', () => { 38 | const actual = getEnlargedImageContainerDimension({ 39 | containerDimension: 4, 40 | smallImageDimension: 2 41 | }); 42 | 43 | expect(actual).to.equal(4); 44 | }); 45 | 46 | it('ignores containerDimension value when isInPlaceMode is set', () => { 47 | const actual = getEnlargedImageContainerDimension({ 48 | containerDimension: 4, 49 | smallImageDimension: 2, 50 | isInPlaceMode: true 51 | }); 52 | 53 | expect(actual).to.equal(2); 54 | }); 55 | 56 | it('honors user specified dimension when isInPlaceMode is not set', () => { 57 | const actual = getEnlargedImageContainerDimension({ 58 | containerDimension: 4, 59 | smallImageDimension: 2, 60 | isInPlaceMode: false 61 | }); 62 | 63 | expect(actual).to.equal(4); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/lib/image-coordinates.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { 3 | getLensModeEnlargedImageCoordinates, 4 | getInPlaceEnlargedImageCoordinates 5 | } from '../../src/lib/imageCoordinates'; 6 | 7 | describe('Image Coordinates Library', () => { 8 | describe('getLensModeEnlargedImageCoordinates', () => { 9 | it('returns image coordinates relative to its container', () => { 10 | const enlargedImageContainerDimensions = { 11 | width: 4, 12 | height: 4 13 | }; 14 | const smallImage = { 15 | width: 4, 16 | height: 4 17 | }; 18 | const largeImage = { 19 | width: 8, 20 | height: 8 21 | }; 22 | const position = { 23 | x: 2, 24 | y: 2 25 | }; 26 | const lensCursorOffset = { x: 1, y: 1 }; 27 | 28 | const actual = getLensModeEnlargedImageCoordinates({ 29 | smallImage, 30 | largeImage, 31 | position, 32 | cursorOffset: lensCursorOffset, 33 | containerDimensions: enlargedImageContainerDimensions 34 | }); 35 | 36 | expect(actual).to.deep.equal({ x: -2, y: -2 }); 37 | }); 38 | 39 | it('clamps position according to lens', () => { 40 | const enlargedImageContainerDimensions = { 41 | width: 4, 42 | height: 4 43 | }; 44 | const smallImage = { 45 | width: 4, 46 | height: 4 47 | }; 48 | const largeImage = { 49 | width: 8, 50 | height: 8 51 | }; 52 | const position = { 53 | x: 1, 54 | y: 3 55 | }; 56 | const lensCursorOffset = { x: 1, y: 1 }; 57 | 58 | const actual = getLensModeEnlargedImageCoordinates({ 59 | smallImage, 60 | largeImage, 61 | position, 62 | cursorOffset: lensCursorOffset, 63 | containerDimensions: enlargedImageContainerDimensions 64 | }); 65 | 66 | expect(actual).to.deep.equal({ x: -0, y: -4 }); 67 | }); 68 | }); 69 | 70 | describe('getInPlaceEnlargedImageCoordinates', () => { 71 | it('returns image coordinates relative to its container', () => { 72 | const containerDimensions = { 73 | width: 4, 74 | height: 4 75 | }; 76 | const largeImage = { 77 | width: 8, 78 | height: 8 79 | }; 80 | const position = { 81 | x: 2, 82 | y: 2 83 | }; 84 | 85 | const actual = getInPlaceEnlargedImageCoordinates({ containerDimensions, largeImage, position }); 86 | 87 | expect(actual).to.deep.equal({ x: -2, y: -2 }); 88 | }); 89 | 90 | it('clamps coordinates to the container when position is outside', () => { 91 | const containerDimensions = { 92 | width: 4, 93 | height: 4 94 | }; 95 | const largeImage = { 96 | width: 8, 97 | height: 8 98 | }; 99 | const position = { 100 | x: 5, 101 | y: -1 102 | }; 103 | 104 | const actual = getInPlaceEnlargedImageCoordinates({ containerDimensions, largeImage, position }); 105 | 106 | expect(actual).to.deep.equal({ x: -4, y: 0 }); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /test/lib/image-ratio.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { 3 | getSmallToLargeImageRatio, 4 | getLargeToSmallImageRatio, 5 | getContainerToImageRatio 6 | } from '../../src/lib/imageRatio'; 7 | 8 | describe('Image Ratio Library', () => { 9 | describe('getSmallToLargeImageRatio', () => { 10 | it('expresses the number of times the small image fits in the large image', () => { 11 | const smallImage = { 12 | width: 2, 13 | height: 3 14 | }; 15 | const largeImage = { 16 | width: 6, 17 | height: 9 18 | }; 19 | const expected = { 20 | x: 3, 21 | y: 3 22 | }; 23 | 24 | const actual = getSmallToLargeImageRatio(smallImage, largeImage); 25 | 26 | expect(actual).to.deep.equal(expected); 27 | }); 28 | 29 | it('supports images that are not proportional to one another', () => { 30 | const smallImage = { 31 | width: 2, 32 | height: 3 33 | }; 34 | const largeImage = { 35 | width: 6, 36 | height: 6 37 | }; 38 | const expected = { 39 | x: 3, 40 | y: 2 41 | }; 42 | 43 | const actual = getSmallToLargeImageRatio(smallImage, largeImage); 44 | 45 | expect(actual).to.deep.equal(expected); 46 | }); 47 | }); 48 | 49 | describe('getLargeToSmallImageRatio', () => { 50 | it('expresses the number of times the large image fits into the small image', () => { 51 | const smallImage = { 52 | width: 2, 53 | height: 4 54 | }; 55 | const largeImage = { 56 | width: 4, 57 | height: 8 58 | }; 59 | const expected = { 60 | x: 0.5, 61 | y: 0.5 62 | }; 63 | 64 | const actual = getLargeToSmallImageRatio(smallImage, largeImage); 65 | 66 | expect(actual).to.deep.equal(expected); 67 | }); 68 | 69 | it('supports input images that are not proportional to one another', () => { 70 | const smallImage = { 71 | width: 2, 72 | height: 3 73 | }; 74 | const largeImage = { 75 | width: 6, 76 | height: 6 77 | }; 78 | const expected = { 79 | x: 0.3333333333333333, 80 | y: 0.5 81 | }; 82 | 83 | const actual = getLargeToSmallImageRatio(smallImage, largeImage); 84 | 85 | expect(actual).to.deep.equal(expected); 86 | }); 87 | }); 88 | 89 | describe('getContainerToImageRatio', () => { 90 | it( 91 | `expresses how many times the dimensions of a container 92 | element fit into (the dimensions of an image element, minus 93 | the dimensions of the container element).` 94 | , () => { 95 | const containerElement = { 96 | width: 2, 97 | height: 3 98 | }; 99 | const largeImage = { 100 | width: 6, 101 | height: 9 102 | }; 103 | const expected = { 104 | x: 2, 105 | y: 2 106 | }; 107 | 108 | const actual = getContainerToImageRatio(containerElement, largeImage); 109 | 110 | expect(actual).to.deep.equal(expected); 111 | } 112 | ); 113 | 114 | it('supports images dimensions that are not proportional to their container dimensions', () => { 115 | const containerElement = { 116 | width: 2, 117 | height: 3 118 | }; 119 | const largeImage = { 120 | width: 6, 121 | height: 6 122 | }; 123 | const expected = { 124 | x: 2, 125 | y: 1 126 | }; 127 | 128 | const actual = getContainerToImageRatio(containerElement, largeImage); 129 | 130 | expect(actual).to.deep.equal(expected); 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /test/lib/lens.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { getLensCursorOffset } from '../../src/lib/lens'; 3 | 4 | describe('Lens Library', () => { 5 | describe('getLensCursorOffset', () => { 6 | it('returns a point representing the offset from the cursor to the top-left of the clear lens', () => { 7 | const enlargedImageContainerDimensions = { 8 | width: 4, 9 | height: 4 10 | }; 11 | const smallImage = { 12 | width: 4, 13 | height: 4 14 | }; 15 | const largeImage = { 16 | width: 8, 17 | height: 8 18 | }; 19 | const expected = { 20 | x: 1, 21 | y: 1 22 | } 23 | 24 | const actual = getLensCursorOffset(smallImage, largeImage, enlargedImageContainerDimensions); 25 | 26 | expect(actual).to.deep.equal(expected); 27 | }); 28 | 29 | it('rounds values', () => { 30 | const enlargedImageContainerDimensions = { 31 | width: 4, 32 | height: 6 33 | }; 34 | const smallImage = { 35 | width: 4, 36 | height: 6 37 | }; 38 | const largeImage = { 39 | width: 8, 40 | height: 12 41 | }; 42 | const expected = { 43 | x: 1, 44 | y: 2 // rounded up from 1.5 45 | } 46 | 47 | const actual = getLensCursorOffset(smallImage, largeImage, enlargedImageContainerDimensions); 48 | 49 | expect(actual).to.deep.equal(expected); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/react-image-magnify.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { mount, shallow } from 'enzyme'; 4 | import { expect } from 'chai'; 5 | import sinon from 'sinon'; 6 | 7 | import ReactImageMagnify from '../src/ReactImageMagnify'; 8 | import Hint from '../src/hint/DefaultHint'; 9 | import PositiveSpaceLens from '../src/lens/positive-space'; 10 | import UserDefinedHint from './support/UserDefinedHint'; 11 | import { ENLARGED_IMAGE_POSITION } from '../src/constants'; 12 | import * as utils from '../src/utils'; 13 | 14 | describe('React Image Magnify', () => { 15 | const smallImage = { 16 | alt: 'baz', 17 | isFluidWidth: false, 18 | src: 'qux', 19 | srcSet: 'quux', 20 | sizes: 'grault', 21 | width: 3, 22 | height: 4 23 | }; 24 | const largeImage = { 25 | alt: 'foo', 26 | src: 'bar', 27 | srcSet: 'corge', 28 | sizes: 'garply', 29 | width: 12, 30 | height: 16 31 | }; 32 | const { 33 | over: OVER 34 | } = ENLARGED_IMAGE_POSITION; 35 | 36 | function getCompositProps(props) { 37 | return Object.assign( 38 | { 39 | fadeDurationInMs: 0, 40 | hoverDelayInMs: 0, 41 | hoverOffDelayInMs: 0 42 | }, 43 | { 44 | largeImage, 45 | smallImage 46 | }, 47 | props 48 | ); 49 | } 50 | 51 | function getShallowWrapper(props) { 52 | return shallow( 53 | 54 | ); 55 | } 56 | 57 | function getMountedWrapper(props) { 58 | return mount( 59 | 60 | ); 61 | } 62 | 63 | function simulateWindowResize() { 64 | var event = new MouseEvent('resize', { 65 | 'view': window, 66 | 'bubbles': true, 67 | 'cancelable': true 68 | }); 69 | 70 | window.dispatchEvent(event); 71 | } 72 | 73 | let shallowWrapper = getShallowWrapper(); 74 | let mountedWrapper = getMountedWrapper(); 75 | 76 | beforeEach(() => { 77 | shallowWrapper = getShallowWrapper(); 78 | mountedWrapper = getMountedWrapper(); 79 | }); 80 | 81 | it('has correct default props', () => { 82 | expect(ReactImageMagnify.defaultProps).to.deep.equal({ 83 | enlargedImageContainerDimensions: { 84 | width: '100%', 85 | height: '100%' 86 | }, 87 | isEnlargedImagePortalEnabledForTouch: false, 88 | fadeDurationInMs: 300, 89 | hoverDelayInMs: 250, 90 | hoverOffDelayInMs: 150, 91 | hintComponent: Hint, 92 | shouldHideHintAfterFirstActivation: true, 93 | isHintEnabled: false, 94 | hintTextMouse: 'Hover to Zoom', 95 | hintTextTouch: 'Long-Touch to Zoom', 96 | "shouldUsePositiveSpaceLens": false 97 | }); 98 | }); 99 | 100 | it('sets initial smallImageWidth and smallImageHeight state to zero', () => { 101 | const instance = shallowWrapper.instance(); 102 | const state = instance.state; 103 | 104 | expect(state.smallImageWidth).to.equal(0); 105 | expect(state.smallImageHeight).to.equal(0); 106 | }); 107 | 108 | it('sets fluid small image dimensions state on small image load', () => { 109 | const mountedWrapper = getMountedWrapper({ 110 | smallImage: Object.assign( 111 | {}, 112 | smallImage, 113 | { isFluidWidth: true } 114 | ) 115 | }); 116 | const instance = mountedWrapper.instance(); 117 | sinon.spy(instance, 'setSmallImageDimensionState'); 118 | 119 | instance.onSmallImageLoad(); 120 | 121 | expect(instance.setSmallImageDimensionState.called).to.be.true; 122 | instance.setSmallImageDimensionState.restore(); 123 | }); 124 | 125 | it('does not set fixed small image dimensions state on small image load', () => { 126 | const mountedWrapper = getMountedWrapper(); 127 | const instance = mountedWrapper.instance(); 128 | sinon.spy(instance, 'setSmallImageDimensionState'); 129 | 130 | instance.onSmallImageLoad(); 131 | 132 | expect(instance.setSmallImageDimensionState.called).to.be.false; 133 | instance.setSmallImageDimensionState.restore(); 134 | }); 135 | 136 | it('sets environment state when onDetectedInputTypeChanged is called', () => { 137 | const mountedWrapper = getMountedWrapper(); 138 | const instance = mountedWrapper.instance(); 139 | const detectedInputType = { isTouchDetected: true, isMouseDetected: false }; 140 | 141 | instance.onDetectedInputTypeChanged(detectedInputType); 142 | 143 | expect(mountedWrapper.state('detectedInputType')).to.deep.equal(detectedInputType); 144 | }); 145 | 146 | it('applies isInPlaceMode to EnlargedImage component', () => { 147 | mountedWrapper.setProps({ enlargedImagePosition: OVER }); 148 | 149 | expect(mountedWrapper.find('EnlargedImage').prop('isInPlaceMode')).to.be.true; 150 | }); 151 | 152 | it('applies isTouchDetected to RenderEnlargedImage', () => { 153 | shallowWrapper.setState({ 154 | detectedInputType: { 155 | isTouchDetected: true 156 | } 157 | }); 158 | 159 | expect(shallowWrapper.find('RenderEnlargedImage').prop('isTouchDetected')).to.be.true; 160 | }); 161 | 162 | describe('Props API', () => { 163 | 164 | it('applies className to root component', () => { 165 | shallowWrapper.setProps({ className: 'foo' }); 166 | 167 | expect(shallowWrapper.find('ReactCursorPosition').prop('className')).to.equal('foo'); 168 | }); 169 | 170 | describe('style', () => { 171 | it('applies style to root component', () => { 172 | shallowWrapper.setProps({ style: { color: 'red' } }); 173 | 174 | expect(shallowWrapper.find('ReactCursorPosition').props().style.color).to.equal('red'); 175 | }); 176 | 177 | it('weights prioritized fluid root component style over user specified style', () => { 178 | const props = { 179 | style: { 180 | width: '1px', 181 | fontSize: '2px', 182 | position: 'absolute' 183 | }, 184 | smallImage: Object.assign( 185 | {}, 186 | smallImage, 187 | { isFluidWidth: true } 188 | ) 189 | }; 190 | shallowWrapper.setProps(props); 191 | 192 | const { style } = shallowWrapper.find('ReactCursorPosition').props(); 193 | expect(style.width).to.equal('auto'); 194 | expect(style.height).to.equal('auto'); 195 | expect(style.fontSize).to.equal('0px'); 196 | expect(style.position).to.equal('relative'); 197 | }); 198 | 199 | it('weights prioritized fixed width root component style over user specified style', () => { 200 | const props = { 201 | style: { 202 | width: '1px', 203 | height: '2px', 204 | position: 'absolute' 205 | } 206 | }; 207 | shallowWrapper.setProps(props); 208 | 209 | const { style } = shallowWrapper.find('ReactCursorPosition').props(); 210 | expect(style.width).to.equal('3px'); 211 | expect(style.height).to.equal('4px'); 212 | expect(style.position).to.equal('relative'); 213 | }); 214 | }); 215 | 216 | it('applies hoverDelayInMs to ReactHoverObserver component', () => { 217 | shallowWrapper.setProps({ hoverDelayInMs: 1 }); 218 | 219 | expect(shallowWrapper.find('ReactCursorPosition').prop('hoverDelayInMs')).to.equal(1); 220 | }); 221 | 222 | it('applies hoverOffDelayInMs to ReactHoverObserver component', () => { 223 | shallowWrapper.setProps({ hoverOffDelayInMs: 2 }); 224 | 225 | expect(shallowWrapper.find('ReactCursorPosition').prop('hoverOffDelayInMs')).to.equal(2); 226 | }); 227 | 228 | it('applies imageClassName to small image element', () => { 229 | shallowWrapper.setProps({ imageClassName: 'baz' }); 230 | 231 | expect(shallowWrapper.find('img').hasClass('baz')).to.be.true; 232 | }); 233 | 234 | describe('imageStyle', () => { 235 | it('applies imageStyle to small image element', () => { 236 | shallowWrapper.setProps({ imageStyle: { color: 'green' } }); 237 | 238 | expect(shallowWrapper.find('img').props().style.color).to.equal('green'); 239 | }); 240 | 241 | it('prioritizes required fixed width style over user specified style', () => { 242 | shallowWrapper.setProps({ 243 | imageStyle: { 244 | width: '10px', 245 | height: '11px' 246 | } 247 | }); 248 | 249 | const { style } = shallowWrapper.find('img').props(); 250 | expect(style.width).to.equal('3px'); 251 | expect(style.height).to.equal('4px'); 252 | }); 253 | 254 | it('prioritizes required fluid width style over user specified style', () => { 255 | shallowWrapper.setProps({ 256 | imageStyle: { 257 | width: '10px', 258 | height: '11px', 259 | display: 'inline-block' 260 | }, 261 | smallImage: Object.assign( 262 | {}, 263 | smallImage, 264 | { 265 | isFluidWidth: true 266 | } 267 | ) 268 | }); 269 | 270 | const { style } = shallowWrapper.find('img').props(); 271 | expect(style.width).to.equal('100%'); 272 | expect(style.height).to.equal('auto'); 273 | expect(style.display).to.equal('block'); 274 | }); 275 | 276 | }); 277 | 278 | describe('smallImage', () => { 279 | it('applies fixed width dimensions to root element', () => { 280 | const { style } = shallowWrapper.find('ReactCursorPosition').props(); 281 | 282 | expect(style.width).to.equal('3px'); 283 | expect(style.height).to.equal('4px'); 284 | }); 285 | 286 | it('does not apply fixed width dimensions to root element, in the fluid scenario', () => { 287 | shallowWrapper.setProps({ 288 | smallImage: { 289 | isFluidWidth: true, 290 | src: 'foo' 291 | } 292 | }); 293 | const { style } = shallowWrapper.find('ReactCursorPosition').props(); 294 | 295 | expect(style.width).to.equal('auto'); 296 | expect(style.height).to.equal('auto'); 297 | }); 298 | 299 | it('applies fixed width smallImage values to small image element', () => { 300 | const { alt, src, srcSet, sizes, style } = shallowWrapper.find('img').props(); 301 | 302 | expect(alt).to.equal(smallImage.alt); 303 | expect(src).to.equal(smallImage.src); 304 | expect(srcSet).to.equal(smallImage.srcSet); 305 | expect(sizes).to.equal(smallImage.sizes); 306 | expect(style.width).to.equal(smallImage.width + 'px'); 307 | expect(style.height).to.equal(smallImage.height + 'px'); 308 | }); 309 | 310 | it('applies fluid width smallImage values to small image element', () => { 311 | shallowWrapper.setProps({ 312 | smallImage: Object.assign( 313 | {}, 314 | smallImage, 315 | { 316 | isFluidWidth: true 317 | } 318 | ) 319 | }); 320 | 321 | const { alt, src, srcSet, sizes, style } = shallowWrapper.find('img').props(); 322 | expect(alt).to.equal(smallImage.alt); 323 | expect(src).to.equal(smallImage.src); 324 | expect(srcSet).to.equal(smallImage.srcSet); 325 | expect(sizes).to.equal(smallImage.sizes); 326 | expect(style.width).to.equal('100%'); 327 | expect(style.height).to.equal('auto'); 328 | }); 329 | 330 | it('provides fixed width smallImage to EnlargedImage component', () => { 331 | expect(mountedWrapper.find('EnlargedImage').prop('smallImage')).to.deep.equal(smallImage); 332 | }); 333 | 334 | it('provides fluid width smallImage to EnlargedImage component', () => { 335 | mountedWrapper.setProps({ 336 | smallImage: Object.assign( 337 | {}, 338 | smallImage, 339 | { 340 | isFluidWidth: true 341 | } 342 | ) 343 | }); 344 | 345 | const expected = Object.assign( 346 | {}, 347 | smallImage, 348 | { 349 | isFluidWidth: true, 350 | width: 0, 351 | height: 0 352 | } 353 | ); 354 | expect(mountedWrapper.find('EnlargedImage').prop('smallImage')).to.deep.equal(expected); 355 | }); 356 | 357 | describe('Load Event', () => { 358 | it('supports a listener function', () => { 359 | const onLoad = sinon.spy(); 360 | shallowWrapper.setProps({ 361 | smallImage: Object.assign( 362 | {}, 363 | smallImage, 364 | { onLoad } 365 | ) 366 | }); 367 | 368 | shallowWrapper.find('img').simulate('load'); 369 | 370 | expect(onLoad.called).to.be.true; 371 | }); 372 | 373 | it('provides the browser event object to listener function', () => { 374 | const onLoad = sinon.spy(); 375 | shallowWrapper.setProps({ 376 | smallImage: Object.assign( 377 | {}, 378 | smallImage, 379 | { onLoad } 380 | ) 381 | }); 382 | const eventObject = {}; 383 | 384 | shallowWrapper.find('img').simulate('load', eventObject); 385 | 386 | const listenerArguments = onLoad.getCall(0).args; 387 | expect(listenerArguments.length).to.equal(1); 388 | expect(listenerArguments[0]).to.equal(eventObject); 389 | }); 390 | 391 | it('defaults the listener to noop', () => { 392 | sinon.spy(utils, 'noop'); 393 | const shallowWrapper = getShallowWrapper(); 394 | shallowWrapper.setState({ 395 | isActive: true, 396 | isTransitionActive: true 397 | }); 398 | 399 | shallowWrapper.find('img').simulate('load'); 400 | 401 | expect(utils.noop.called).to.be.true; 402 | 403 | utils.noop.restore(); 404 | }); 405 | }); 406 | 407 | describe('Error Event', () => { 408 | it('supports a listener function', () => { 409 | const onError = sinon.spy(); 410 | shallowWrapper.setProps({ 411 | smallImage: Object.assign( 412 | {}, 413 | smallImage, 414 | { onError } 415 | ) 416 | }); 417 | 418 | shallowWrapper.find('img').simulate('error'); 419 | 420 | expect(onError.called).to.be.true; 421 | }); 422 | 423 | it('provides the browser event object to listener function', () => { 424 | const onError = sinon.spy(); 425 | shallowWrapper.setProps({ 426 | smallImage: Object.assign( 427 | {}, 428 | smallImage, 429 | { onError } 430 | ) 431 | }); 432 | const eventObject = {}; 433 | 434 | shallowWrapper.find('img').simulate('error', eventObject); 435 | 436 | const listenerArguments = onError.getCall(0).args; 437 | expect(listenerArguments.length).to.equal(1); 438 | expect(listenerArguments[0]).to.equal(eventObject); 439 | }); 440 | 441 | it('defaults the listener to noop', () => { 442 | sinon.spy(utils, 'noop'); 443 | const shallowWrapper = getShallowWrapper(); 444 | shallowWrapper.setState({ 445 | isActive: true, 446 | isTransitionActive: true 447 | }); 448 | 449 | shallowWrapper.find('img').simulate('error'); 450 | 451 | expect(utils.noop.called).to.be.true; 452 | 453 | utils.noop.restore(); 454 | }); 455 | }); 456 | 457 | describe('isFluidWidth', () => { 458 | it('applies fluid width style to container element, when set', () => { 459 | shallowWrapper.setProps({ 460 | smallImage: { 461 | isFluidWidth: true, 462 | src: 'foo' 463 | } 464 | }); 465 | const { style } = shallowWrapper.find('ReactCursorPosition').props(); 466 | 467 | expect(style.width).to.equal('auto'); 468 | expect(style.height).to.equal('auto'); 469 | }); 470 | 471 | it('applies fluid width style to small image element, when set', () => { 472 | shallowWrapper.setProps({ 473 | smallImage: { 474 | isFluidWidth: true, 475 | src: 'foo' 476 | } 477 | }); 478 | const { style } = shallowWrapper.find('img').props(); 479 | 480 | expect(style.width).to.equal('100%'); 481 | expect(style.height).to.equal('auto'); 482 | }); 483 | 484 | it('sets smallImageWidth and smallImageHeight state with offset values, when component mounts', () => { 485 | shallowWrapper.setProps({ 486 | smallImage: { 487 | isFluidWidth: true, 488 | src: 'foo' 489 | } 490 | }); 491 | const instance = shallowWrapper.instance(); 492 | instance.smallImageEl = { 493 | offsetWidth: 10, 494 | offsetHeight: 20 495 | } 496 | 497 | instance.componentDidMount(); 498 | 499 | expect(shallowWrapper.state().smallImageWidth).to.equal(10); 500 | expect(shallowWrapper.state().smallImageHeight).to.equal(20); 501 | }); 502 | 503 | it('listens for window resize event on mount', () => { 504 | sinon.spy(window, 'addEventListener'); 505 | 506 | getMountedWrapper({ 507 | smallImage: { 508 | isFluidWidth: true, 509 | src: 'foo' 510 | } 511 | }); 512 | 513 | expect(window.addEventListener.calledWith('resize')).to.be.true; 514 | window.addEventListener.restore(); 515 | }); 516 | 517 | it('removes window resize listener when unmounted', () => { 518 | sinon.spy(window, 'removeEventListener'); 519 | const mountedWrapper = getMountedWrapper({ 520 | smallImage: { 521 | isFluidWidth: true, 522 | src: 'foo' 523 | } 524 | }); 525 | mountedWrapper.unmount(); 526 | 527 | expect(window.removeEventListener.calledWith('resize')).to.be.true; 528 | window.removeEventListener.restore(); 529 | }); 530 | 531 | it('does not listen for window resize event when isFluidWidthSmallImage is not set', () => { 532 | sinon.spy(window, 'addEventListener'); 533 | 534 | getMountedWrapper(); 535 | 536 | expect(window.addEventListener.calledWith('resize')).to.be.false; 537 | window.addEventListener.restore(); 538 | }); 539 | 540 | it('sets small image offset height and width state when the browser is resized', () => { 541 | const mountedWrapper = getMountedWrapper({ 542 | smallImage: { 543 | isFluidWidth: true, 544 | src: 'foo' 545 | } 546 | }); 547 | const instance = mountedWrapper.instance(); 548 | instance.smallImageEl = { 549 | offsetWidth: 50, 550 | offsetHeight: 51 551 | }; 552 | 553 | simulateWindowResize(); 554 | 555 | expect(mountedWrapper.state('smallImageWidth')).to.equal(50); 556 | expect(mountedWrapper.state('smallImageHeight')).to.equal(51); 557 | }); 558 | }); 559 | }); 560 | 561 | it('applies enlargedImageContainerClassName to EnlargedImage component', () => { 562 | mountedWrapper.setProps({ enlargedImageContainerClassName: 'foo' }); 563 | 564 | expect(mountedWrapper.find('EnlargedImage').prop('containerClassName')).to.equal('foo'); 565 | }); 566 | 567 | it('applies enlargedImageContainerStyle to EnlargedImage component', () => { 568 | const style = { color: 'red' }; 569 | mountedWrapper.setProps({ enlargedImageContainerStyle: style }); 570 | 571 | expect(mountedWrapper.find('EnlargedImage').prop('containerStyle')).to.equal(style); 572 | }); 573 | 574 | it('applies enlargedImageClassName to EnlargedImage component', () => { 575 | mountedWrapper.setProps({ enlargedImageClassName: 'bar' }); 576 | 577 | expect(mountedWrapper.find('EnlargedImage').prop('imageClassName')).to.equal('bar'); 578 | }); 579 | 580 | it('applies enlargedImageStyle to EnlargedImage component', () => { 581 | const style = { color: 'blue' }; 582 | mountedWrapper.setProps({ enlargedImageStyle: style }); 583 | 584 | expect(mountedWrapper.find('EnlargedImage').prop('imageStyle').color).to.equal('blue'); 585 | }); 586 | 587 | it('applies fadeDurationInMs to EnlargedImage component', () => { 588 | mountedWrapper.setProps({ fadeDurationInMs: 1 }); 589 | 590 | expect(mountedWrapper.find('EnlargedImage').prop('fadeDurationInMs')).to.equal(1); 591 | }); 592 | 593 | it('applies largeImage to EnlargedImage component', () => { 594 | expect(mountedWrapper.find('EnlargedImage').prop('largeImage')).to.equal(largeImage); 595 | }); 596 | 597 | it('applies enlargedImagePortalId to RenderEnlargedImage component', () => { 598 | sinon.stub(ReactDOM, 'createPortal').callsFake(() => null); 599 | mountedWrapper.setProps({'enlargedImagePortalId': 'foo'}); 600 | 601 | expect(mountedWrapper.find('RenderEnlargedImage').prop('portalId')).to.equal('foo'); 602 | ReactDOM.createPortal.restore(); 603 | }); 604 | 605 | it('applies isPortalEnabledForTouch to RenderEnlargedImage component', () => { 606 | mountedWrapper.setProps({ isEnlargedImagePortalEnabledForTouch: true }); 607 | 608 | expect(mountedWrapper.find('RenderEnlargedImage').prop('isPortalEnabledForTouch')).to.be.true; 609 | }); 610 | 611 | describe('Hint', () => { 612 | it('is disabled by default', () => { 613 | const mountedWrapper = getMountedWrapper({ enlargedImagePosition: OVER }); 614 | 615 | const hint = mountedWrapper.find('DefaultHint'); 616 | 617 | expect(hint).to.have.length(0); 618 | }); 619 | 620 | it('supports enabling', () => { 621 | const mountedWrapper = getMountedWrapper({ 622 | isHintEnabled: true, 623 | enlargedImagePosition: OVER 624 | }); 625 | 626 | const hint = mountedWrapper.find('DefaultHint'); 627 | 628 | expect(hint).to.have.length(1); 629 | }); 630 | 631 | it('is hidden when magnification is active', (done) => { 632 | const mountedWrapper = getMountedWrapper({ 633 | className: 'foo', 634 | isHintEnabled: true, 635 | fadeDurationInMs: 0, 636 | enlargedImagePosition: OVER 637 | }); 638 | let hint = mountedWrapper.find('DefaultHint'); 639 | expect(hint).to.have.length(1); 640 | const rootComponent = mountedWrapper.find('ReactCursorPosition'); 641 | 642 | rootComponent.instance().onMouseEnter({}); 643 | 644 | setTimeout(() => { 645 | mountedWrapper.update(); 646 | hint = mountedWrapper.find('DefaultHint'); 647 | expect(hint).to.have.length(0); 648 | done(); 649 | }, 0); 650 | }); 651 | 652 | it('is hidden after first activation by default', (done) => { 653 | const mountedWrapper = getMountedWrapper({ 654 | isHintEnabled: true, 655 | fadeDurationInMs: 0, 656 | enlargedImagePosition: OVER 657 | }); 658 | let hint = mountedWrapper.find('DefaultHint'); 659 | expect(hint).to.have.length(1); 660 | const rootComponent = mountedWrapper.find('ReactCursorPosition'); 661 | 662 | rootComponent.instance().onMouseEnter({}); 663 | 664 | setTimeout(() => { 665 | mountedWrapper.update(); 666 | hint = mountedWrapper.find('DefaultHint'); 667 | expect(hint).to.have.length(0); 668 | 669 | rootComponent.instance().onMouseLeave({}); 670 | 671 | setTimeout(() => { 672 | mountedWrapper.update(); 673 | hint = mountedWrapper.find('DefaultHint'); 674 | expect(hint).to.have.length(0); 675 | done(); 676 | }, 0); 677 | }, 0); 678 | }); 679 | 680 | it('can be configured to always show when not active', (done) => { 681 | const mountedWrapper = getMountedWrapper({ 682 | isHintEnabled: true, 683 | shouldHideHintAfterFirstActivation: false, 684 | fadeDurationInMs: 0, 685 | enlargedImagePosition: OVER 686 | }); 687 | let hint = mountedWrapper.find('DefaultHint'); 688 | expect(hint).to.have.length(1); 689 | const rootComponent = mountedWrapper.find('ReactCursorPosition'); 690 | 691 | rootComponent.instance().onMouseEnter({}); 692 | 693 | setTimeout(() => { 694 | mountedWrapper.update(); 695 | hint = mountedWrapper.find('DefaultHint'); 696 | expect(hint).to.have.length(0); 697 | 698 | rootComponent.instance().onMouseLeave({}); 699 | 700 | setTimeout(() => { 701 | mountedWrapper.update(); 702 | hint = mountedWrapper.find('DefaultHint'); 703 | expect(hint).to.have.length(1); 704 | done(); 705 | }, 0); 706 | }, 0); 707 | }); 708 | 709 | it('supports default hint text for mouse environments', () => { 710 | const mountedWrapper = getMountedWrapper({ 711 | isHintEnabled: true, 712 | enlargedImagePosition: OVER 713 | }); 714 | 715 | const hint = mountedWrapper.find('DefaultHint'); 716 | 717 | expect(hint.text()).to.equal('Hover to Zoom'); 718 | }); 719 | 720 | it('supports default hint text for touch environments', () => { 721 | const mountedWrapper = getMountedWrapper({ 722 | isHintEnabled: true, 723 | enlargedImagePosition: OVER 724 | }); 725 | mountedWrapper.setState({ 726 | detectedInputType: { 727 | isMouseDetected: false, 728 | isTouchDetected: true 729 | } 730 | }); 731 | 732 | const hint = mountedWrapper.find('DefaultHint'); 733 | 734 | expect(hint.text()).to.equal('Long-Touch to Zoom'); 735 | }); 736 | 737 | it('supports user defined hint text for mouse environments', () => { 738 | const mountedWrapper = getMountedWrapper({ 739 | isHintEnabled: true, 740 | hintTextMouse: 'foo', 741 | enlargedImagePosition: OVER 742 | }); 743 | 744 | const hint = mountedWrapper.find('DefaultHint'); 745 | 746 | expect(hint.text()).to.equal('foo'); 747 | }); 748 | 749 | it('supports user defined hint text for touch environments', () => { 750 | const mountedWrapper = getMountedWrapper({ 751 | isHintEnabled: true, 752 | hintTextTouch: 'bar', 753 | enlargedImagePosition: OVER 754 | }); 755 | mountedWrapper.setState({ 756 | detectedInputType: { 757 | isMouseDetected: false, 758 | isTouchDetected: true 759 | } 760 | }); 761 | 762 | const hint = mountedWrapper.find('DefaultHint'); 763 | 764 | expect(hint.text()).to.equal('bar'); 765 | }); 766 | 767 | it('supports user defined hint component', () => { 768 | const mountedWrapper = getMountedWrapper({ 769 | isHintEnabled: true, 770 | hintComponent: UserDefinedHint, 771 | enlargedImagePosition: OVER 772 | }); 773 | 774 | const hint = mountedWrapper.find('UserDefinedHint'); 775 | 776 | expect(hint.text()).to.equal('User Defined Mouse'); 777 | }); 778 | 779 | it('provides correct props to user defined component', () => { 780 | const mountedWrapper = getMountedWrapper({ 781 | isHintEnabled: true, 782 | hintComponent: UserDefinedHint, 783 | enlargedImagePosition: OVER 784 | }); 785 | 786 | const hint = mountedWrapper.find('UserDefinedHint'); 787 | 788 | expect(hint.props()).to.deep.equal({ 789 | isTouchDetected: false, 790 | hintTextMouse: 'Hover to Zoom', 791 | hintTextTouch: 'Long-Touch to Zoom' 792 | }); 793 | }); 794 | }); 795 | 796 | describe('Lens', () => { 797 | it('defaults to negative space lens', () => { 798 | expect(shallowWrapper.find('NegativeSpaceLens')).to.have.lengthOf(1); 799 | }); 800 | 801 | it('can be configured to use positive space lens', () => { 802 | shallowWrapper.setProps({ shouldUsePositiveSpaceLens: true }); 803 | 804 | expect(shallowWrapper.find('PositiveSpaceLens')).to.have.lengthOf(1); 805 | }); 806 | 807 | it('can be configured to use a custom lens component', () => { 808 | shallowWrapper.setProps({ lensComponent: PositiveSpaceLens }); 809 | 810 | expect(shallowWrapper.find('PositiveSpaceLens')).to.have.lengthOf(1); 811 | }); 812 | 813 | it('applies fadeDurationInMs to lens component', () => { 814 | shallowWrapper.setProps({ fadeDurationInMs: 1 }); 815 | 816 | expect(shallowWrapper.find('NegativeSpaceLens').prop('fadeDurationInMs')).to.deep.equal(1); 817 | }); 818 | 819 | it('applies lensStyle to lens component', () => { 820 | shallowWrapper.setProps({lensStyle: { foo: 'bar' }}); 821 | 822 | expect(shallowWrapper.find('NegativeSpaceLens').prop('style')).to.deep.equal({ foo: 'bar' }); 823 | }); 824 | 825 | it('provides cursor offset to lens component', () => { 826 | const actual = shallowWrapper.find('NegativeSpaceLens').prop('cursorOffset'); 827 | 828 | expect(actual).to.exist; 829 | }); 830 | 831 | it('provides fixed width smallImage to lens component', () => { 832 | expect(shallowWrapper.find('NegativeSpaceLens').prop('smallImage')).to.deep.equal(smallImage); 833 | }); 834 | 835 | it('provides fluid width smallImage to lens component', () => { 836 | shallowWrapper.setProps({ 837 | fadeDurationInMs: 1, 838 | smallImage: Object.assign( 839 | {}, 840 | smallImage, 841 | { 842 | isFluidWidth: true, 843 | } 844 | ) 845 | }); 846 | 847 | const expected = Object.assign( 848 | {}, 849 | smallImage, 850 | { 851 | isFluidWidth: true, 852 | width: 0, 853 | height: 0 854 | } 855 | ); 856 | expect(shallowWrapper.find('NegativeSpaceLens').prop('smallImage')).to.deep.equal(expected); 857 | }); 858 | }); 859 | }); 860 | }); 861 | -------------------------------------------------------------------------------- /test/render-enlarged-image.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { expect } from 'chai'; 4 | import { shallow } from 'enzyme'; 5 | import sinon from 'sinon'; 6 | import RenderEnlargedImage from '../src/RenderEnlargedImage'; 7 | 8 | describe('RenderEnlargedImage', () => { 9 | let shallowWrapper = getShallowWrapper(); 10 | 11 | function getShallowWrapper(props) { 12 | return shallow(); 13 | } 14 | 15 | beforeEach(() => { 16 | shallowWrapper = getShallowWrapper(); 17 | }); 18 | 19 | describe('Component is Not Mounted', () => { 20 | it('renders null', () => { 21 | shallowWrapper.setState({isMounted: false}); 22 | 23 | expect(shallowWrapper.find('EnlargedImage').length).to.equal(0); 24 | }); 25 | }); 26 | 27 | describe('Component is Mounted', () => { 28 | it('sets isMounted state', () => { 29 | expect(shallowWrapper.state('isMounted')).to.be.true; 30 | }); 31 | 32 | it('sets instance portalElement property', () => { 33 | sinon.stub(ReactDOM, 'createPortal'); 34 | sinon.stub(document, 'getElementById').callsFake(id => id); 35 | shallowWrapper.setProps({ portalId: 'foo' }); 36 | const instance = shallowWrapper.instance(); 37 | 38 | instance.componentDidMount(); 39 | 40 | expect(instance.portalElement).to.equal('foo'); 41 | ReactDOM.createPortal.restore(); 42 | document.getElementById.restore(); 43 | }); 44 | 45 | describe('Mouse Input', () => { 46 | it('renders internally if portalId prop is not implemented', () => { 47 | expect(shallowWrapper.find('EnlargedImage').length).to.equal(1); 48 | }); 49 | 50 | it('renders to portal if protalId prop is implemented', () => { 51 | sinon.stub(ReactDOM, 'createPortal'); 52 | shallowWrapper.setProps({ portalId: 'foo' }); 53 | 54 | expect(ReactDOM.createPortal.called).to.be.true; 55 | ReactDOM.createPortal.restore(); 56 | }); 57 | }); 58 | 59 | describe('Touch Input', () => { 60 | it('renders internally, ignoring portalId implementation by default', () => { 61 | shallowWrapper.setProps({ 62 | portalId: 'foo', 63 | isTouchDetected: true 64 | }); 65 | 66 | expect(shallowWrapper.find('EnlargedImage').length).to.equal(1); 67 | }); 68 | 69 | it('renders to portal when isPortalEnabledForTouch is set', () => { 70 | sinon.stub(ReactDOM, 'createPortal'); 71 | shallowWrapper.setProps({ 72 | isTouchDetected: true, 73 | isPortalEnabledForTouch: true, 74 | portalId: 'foo' 75 | }); 76 | 77 | expect(ReactDOM.createPortal.called).to.be.true; 78 | ReactDOM.createPortal.restore(); 79 | }) 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | import './support/jsdom' 5 | 6 | configure({ adapter: new Adapter() }); 7 | -------------------------------------------------------------------------------- /test/support/UserDefinedHint.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | 5 | function UserDefinedHint({ isTouchDetected }) { 6 | return ( 7 |
14 |
22 | 30 | 35 | { isTouchDetected ? 'User Defined Touch' : 'User Defined Mouse' } 36 | 37 |
38 |
39 | ); 40 | } 41 | 42 | UserDefinedHint.displayName = 'UserDefinedHint' 43 | 44 | UserDefinedHint.propTypes = { 45 | isTouchDetected: PropTypes.bool, 46 | hintTextMouse: PropTypes.string, 47 | hintTextTouch: PropTypes.string 48 | } 49 | 50 | export default UserDefinedHint; 51 | -------------------------------------------------------------------------------- /test/support/jsdom.js: -------------------------------------------------------------------------------- 1 | const jsdom = require('jsdom'); 2 | const baseMarkup = ''; 3 | const { JSDOM } = jsdom; 4 | const { window } = new JSDOM(baseMarkup); 5 | 6 | global.window = window; 7 | global.document = window.document; 8 | global.HTMLElement = global.window.HTMLElement; 9 | global.MouseEvent = global.window.MouseEvent; 10 | global.navigator = { 11 | userAgent: 'node.js' 12 | }; 13 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 3 | 4 | module.exports = { 5 | entry: './src/ReactImageMagnify.js', 6 | output: { 7 | path: path.resolve(__dirname, './dist/umd'), 8 | filename: 'ReactImageMagnify.js', 9 | library: 'ReactImageMagnify', 10 | libraryTarget: 'umd' 11 | }, 12 | externals: { 13 | react: { 14 | commonjs: 'react', 15 | commonjs2: 'react', 16 | amd: 'react', 17 | umd: 'react', 18 | root: 'React' 19 | } 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.js$/, 25 | exclude: /node_modules/, 26 | loader: 'babel-loader' 27 | } 28 | ] 29 | }, 30 | plugins: [new BundleAnalyzerPlugin({ 31 | /** 32 | * Can be `server`, `static` or `disabled`. 33 | * In `server` mode analyzer will start HTTP server to show bundle report. 34 | * In `static` mode single HTML file with bundle report will be generated. 35 | * In `disabled` mode you can use this plugin to just generate Webpack Stats 36 | * JSON file by setting `generateStatsFile` to true. 37 | */ 38 | analyzerMode: 'disabled', 39 | })] 40 | }; 41 | --------------------------------------------------------------------------------