;
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 |
--------------------------------------------------------------------------------