diff --git a/README.md b/README.md index c802b71d..f0a02674 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ Features tinted guide lens, interaction hint, hover intent, long-press gesture, [Integration with react-slick carousel](https://ethanselzer.github.io/react-image-magnify/#/react-slick) +[Specify Enlarged Image Container Dimensions](https://ethanselzer.github.io/react-image-magnify/#/dimensions) + Experiment with react-image-magnify [live on CodePen](https://codepen.io/ethanselzer/full/oePMNY/). Use the Change View button to select editing mode or for different layout options. Use the Fork button to save your changes. @@ -91,7 +93,8 @@ If you would like more information on responsive images, please try these resour | `hoverDelayInMs` | Number | No | 250 | Milliseconds to delay hover trigger. | | `hoverOffDelayInMs` | Number | No | 150 | Milliseconds to delay hover-off trigger. | | `lensStyle` | Object | No | | Style applied to tinted lens. | -| `enlargedImagePosition` | String | No | beside | Enlarged image position. Can be 'beside' or 'over'. | +| `enlargedImagePosition` | String | No | beside | Enlarged image position. Can be 'beside' or 'over'. | +| `enlargedImageContainerDimensions` | Object | No | {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'. | ### Touch Specific | Prop | Type | Required | Default | Description | diff --git a/example/README.md b/example/README.md index 20fa7287..077bd761 100644 --- a/example/README.md +++ b/example/README.md @@ -18,6 +18,8 @@ If your default browser does not start automatically, open a new browser window [Integration with react-slick](http://localhost:3000/#/react-slick). +[Specify Enlarged Image Container Dimensions](http://localhost:3000/#/dimensions). + ## Reference If you would like more information on responsive images, please try these resources: [https://cloudfour.com/thinks/responsive-images-101-definitions/](https://cloudfour.com/thinks/responsive-images-101-definitions/) diff --git a/example/src/pages/EnlargedImageContainerDimensions.js b/example/src/pages/EnlargedImageContainerDimensions.js new file mode 100644 index 00000000..b974f188 --- /dev/null +++ b/example/src/pages/EnlargedImageContainerDimensions.js @@ -0,0 +1,89 @@ +import React, { Component } from 'react'; +import ReactImageMagnify from '../pkg-lnk/ReactImageMagnify'; +import SpacedSpan from '../components/SpacedSpan'; + +import './app.css'; + +import watchImg355 from '../images/wristwatch_355.jpg'; +import watchImg481 from '../images/wristwatch_481.jpg'; +import watchImg584 from '../images/wristwatch_584.jpg'; +import watchImg687 from '../images/wristwatch_687.jpg'; +import watchImg770 from '../images/wristwatch_770.jpg'; +import watchImg861 from '../images/wristwatch_861.jpg'; +import watchImg955 from '../images/wristwatch_955.jpg'; +import watchImg1033 from '../images/wristwatch_1033.jpg'; +import watchImg1112 from '../images/wristwatch_1112.jpg'; +import watchImg1192 from '../images/wristwatch_1192.jpg'; +import watchImg1200 from '../images/wristwatch_1200.jpg'; + +export default class EnlargedImageContainerDimensions extends Component { + get srcSet() { + return [ + `${watchImg355} 355w`, + `${watchImg481} 481w`, + `${watchImg584} 584w`, + `${watchImg687} 687w`, + `${watchImg770} 770w`, + `${watchImg861} 861w`, + `${watchImg955} 955w`, + `${watchImg1033} 1033w`, + `${watchImg1112} 1112w`, + `${watchImg1192} 1192w`, + `${watchImg1200} 1200w`, + ].join(', '); + } + + render() { + return ( +
+
+ +
+
+

Enlarged Image Container Dimensions Example

+

+ Specify dimensions as percentage of small image or number of pixels. +

+

+ May be percentage for one dimension and number for the other. +

+

+ Exmample specifies width of + 200% + and height of + 100%. +

+

+ Please see + + + source code + + + for details. +

+
+
+
+ ); + } +} diff --git a/example/src/pages/FixedWidthSmallImage.js b/example/src/pages/FixedWidthSmallImage.js new file mode 100644 index 00000000..18fc4239 --- /dev/null +++ b/example/src/pages/FixedWidthSmallImage.js @@ -0,0 +1,38 @@ +import React, { Component } from 'react'; +import ReactImageMagnify from '../pkg-lnk/ReactImageMagnify'; + +import './app.css'; + +import watchImg from '../images/wristwatch_1200.jpg'; + +export default class extends Component { + render() { + return ( +
+
+ +
+
+

Fixed Width Small Image Example

+

Specify small image width and height as numbers

+

Small image is not fluid width.

+
+
+
+ ); + } +} diff --git a/example/src/pages/ReactSlick.js b/example/src/pages/ReactSlick.js index 36032bf1..1136c85c 100644 --- a/example/src/pages/ReactSlick.js +++ b/example/src/pages/ReactSlick.js @@ -15,7 +15,7 @@ import back_1020 from '../images/versace-blue/back-1020.jpg'; import back_1200 from '../images/versace-blue/back-1200.jpg'; import back_1426 from '../images/versace-blue/back-1426.jpg'; -import './App.css'; +import './app.css'; import './react-slick.css'; const frontSrcSet = [ @@ -87,25 +87,25 @@ export default class ReactSlickExample extends Component { ))}
-
+

Carousel Example

-

+

Integration with  react-slick .

-

+

In-place enlargement for mouse and touch input.

-

+

Side-by-side enlargement not yet compatible with react-slick.

-

+

Responsive and fluid between breakpoints.

-

+

Initial file size optimized via srcSet @@ -116,10 +116,10 @@ export default class ReactSlickExample extends Component { attributes.

-

+

Please see - + source code diff --git a/example/src/pages/SideBySide.js b/example/src/pages/SideBySide.js index 99d47a59..c6204502 100644 --- a/example/src/pages/SideBySide.js +++ b/example/src/pages/SideBySide.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import ReactImageMagnify from '../pkg-lnk/ReactImageMagnify'; import SpacedSpan from '../components/SpacedSpan'; -import './App.css'; +import './app.css'; import watchImg355 from '../images/wristwatch_355.jpg'; import watchImg481 from '../images/wristwatch_481.jpg'; @@ -56,16 +56,16 @@ export default class BasicExample extends Component {

Basic Example

-

+

Side by Side enlargement for mouse input.

-

+

In place enlargement for touch input.

-

+

Fluid between breakpoints.

-

+

Initial file size optimized via srcSet @@ -76,10 +76,10 @@ export default class BasicExample extends Component { attributes.

-

+

Please see - + source code diff --git a/example/src/pages/App.css b/example/src/pages/app.css similarity index 80% rename from example/src/pages/App.css rename to example/src/pages/app.css index 630f58e6..ec8e789a 100644 --- a/example/src/pages/App.css +++ b/example/src/pages/app.css @@ -14,6 +14,11 @@ body { margin: 0 20px; } +.fixed__instructions { + flex: 1; + margin: 0 20px; +} + a { color: black; } @@ -40,4 +45,9 @@ a:hover { flex: 0 0 50%; padding-top: 30px; } + + .fixed__instructions { + padding-top: 30px; + margin: 0 10px; + } } diff --git a/example/src/pages/react-slick.css b/example/src/pages/react-slick.css index 714aa13e..f1bb6814 100644 --- a/example/src/pages/react-slick.css +++ b/example/src/pages/react-slick.css @@ -1,7 +1,7 @@ @import "~slick-carousel/slick/slick.css"; @import "~slick-carousel/slick/slick-theme.css"; -* { +.react-slick * { min-height: 0; min-width: 0; } diff --git a/example/src/router.js b/example/src/router.js index 8d61d62a..8c2f41ae 100644 --- a/example/src/router.js +++ b/example/src/router.js @@ -3,11 +3,15 @@ import { Router, Route } from 'react-router'; import SideBySide from './pages/SideBySide'; import ReactSlick from './pages/ReactSlick'; +import EnlargedImageContainerDimensions from './pages/EnlargedImageContainerDimensions'; +import FixedWidthSmallImage from './pages/FixedWidthSmallImage'; const Routes = (props) => ( + + ); diff --git a/src/EnlargedImage.js b/src/EnlargedImage.js index eb5c7e1c..4091e6a8 100644 --- a/src/EnlargedImage.js +++ b/src/EnlargedImage.js @@ -43,7 +43,10 @@ export default class extends React.Component { isActive: PropTypes.bool, isLazyLoaded: PropTypes.bool, largeImage: ImageShape, - smallImage: ImageShape, + containerDimensions: PropTypes.shape({ + width: PropTypes.number, + height: PropTypes.number + }), imagePosition: PropTypes.oneOf([ ENLARGED_IMAGE_POSITION.beside, ENLARGED_IMAGE_POSITION.over @@ -128,26 +131,28 @@ export default class extends React.Component { imagePosition, cursorOffset, largeImage, - smallImage, - position + containerDimensions, + position, + smallImage } = this.props; const { over: OVER } = ENLARGED_IMAGE_POSITION; const isInPlaceMode = imagePosition === OVER; if (isInPlaceMode) { - return getInPlaceEnlargedImageCoordinates( - smallImage, + return getInPlaceEnlargedImageCoordinates({ + containerDimensions, largeImage, position - ); + }); } - return getLensModeEnlargedImageCoordinates( - smallImage, + return getLensModeEnlargedImageCoordinates({ + containerDimensions, + cursorOffset, largeImage, position, - cursorOffset - ); + smallImage + }); } render() { @@ -163,7 +168,7 @@ export default class extends React.Component { onLoad = noop, onError = noop }, - smallImage, + containerDimensions, } = this.props; const { @@ -175,8 +180,8 @@ export default class extends React.Component { const defaultContainerStyle = this.getDefaultContainerStyle(); const computedContainerStyle = { - width: smallImage.width, - height: smallImage.height, + width: containerDimensions.width, + height: containerDimensions.height, opacity: this.state.isTransitionActive ? 1 : 0, transition: `opacity ${fadeDurationInMs}ms ease-in`, pointerEvents: 'none' diff --git a/src/ReactImageMagnify.js b/src/ReactImageMagnify.js index f39b2965..3e451497 100644 --- a/src/ReactImageMagnify.js +++ b/src/ReactImageMagnify.js @@ -8,6 +8,10 @@ import requiredIf from 'react-required-if'; import DisplayUntilActive from './hint/DisplayUntilActive'; import EnlargedImage from './EnlargedImage'; import { getLensCursorOffset } from './lib/lens'; +import { + getEnlargedImageContainerDimension, + getDefaultEnlargedImageContainerDimensions +} from './lib/dimensions'; import Hint from './hint/DefaultHint'; import ShadedLens from './shaded-lens'; import ImageShape from './prop-types/ImageShape'; @@ -50,6 +54,7 @@ class ReactImageMagnify extends React.Component { enlargedImageContainerStyle: PropTypes.object, enlargedImageClassName: PropTypes.string, enlargedImageStyle: PropTypes.object, + enlargedImageContainerDimensions: PropTypes.object, fadeDurationInMs: PropTypes.number, hintComponent: PropTypes.func, shouldHideHintAfterFirstActivation: PropTypes.bool, @@ -84,6 +89,10 @@ class ReactImageMagnify extends React.Component { }; static defaultProps = { + enlargedImageContainerDimensions: { + width: '100%', + height: '100%' + }, fadeDurationInMs: 300, hintComponent: Hint, shouldHideHintAfterFirstActivation: true, @@ -169,6 +178,65 @@ class ReactImageMagnify extends React.Component { return userDefinedEnlargedImagePosition || computedEnlargedImagePosition; } + get smallImage() { + const { + smallImage, + smallImage: { + isFluidWidth: isSmallImageFluidWidth + }, + } = this.props; + const { + smallImageWidth, + smallImageHeight + } = this.state; + + const fluidWidthSmallImage = objectAssign( + {}, + smallImage, + { + width: smallImageWidth, + height: smallImageHeight + } + ); + const fixedWidthSmallImage = smallImage; + + return isSmallImageFluidWidth + ? fluidWidthSmallImage + : fixedWidthSmallImage + } + + get isInPlaceMode() { + return this.getEnlargedImagePlacement() === ENLARGED_IMAGE_POSITION.over; + } + + get enlargedImageContainerDimensions() { + const { + enlargedImageContainerDimensions: { + width: containerWidth, + height: containerHeight + } + } = this.props; + const { + width: smallImageWidth, + height: smallImageHeight + } = this.smallImage; + + if (this.isInPlaceMode) { + return getDefaultEnlargedImageContainerDimensions(this.smallImage); + } + + return { + width: getEnlargedImageContainerDimension({ + containerDimension: containerWidth, + smallImageDimension: smallImageWidth + }), + height: getEnlargedImageContainerDimension({ + containerDimension: containerHeight, + smallImageDimension: smallImageHeight + }) + }; + } + render() { const { className, @@ -198,29 +266,14 @@ class ReactImageMagnify extends React.Component { style, } = this.props; + const smallImage = this.smallImage; + const { - smallImageWidth, - smallImageHeight, detectedInputType: { isTouchDetected } } = this.state; - const fluidWidthSmallImage = objectAssign( - {}, - this.props.smallImage, - { - width: smallImageWidth, - height: smallImageHeight - } - ); - - const fixedWidthSmallImage = this.props.smallImage; - - const smallImage = isSmallImageFluidWidth - ? fluidWidthSmallImage - : fixedWidthSmallImage - const fluidWidthContainerStyle = { width: 'auto', height: 'auto', @@ -270,7 +323,9 @@ class ReactImageMagnify extends React.Component { !isTouchDetected ); - const cursorOffset = getLensCursorOffset(smallImage, largeImage); + const enlargedImageContainerDimensions = this.enlargedImageContainerDimensions; + + const cursorOffset = getLensCursorOffset(smallImage, largeImage, enlargedImageContainerDimensions); return ( } { const expected = 'width:3px;height:4px;opacity:1;transition:opacity 0ms ease-in;pointer-events:none'; const renderedWrapper = shallowWrapper.render(); - expect(renderedWrapper.attr('style').endsWith(expected)).to.be.true; }); @@ -415,6 +414,10 @@ describe('Enlarged Image', () => { it('computes max coordinates and applies the result to CSS transfrom translate', () => { shallowWrapper.setProps({ + containerDimensions: { + width: 4, + height: 4 + }, cursorOffset: { x: 0, y: 0 @@ -483,6 +486,10 @@ describe('Enlarged Image', () => { }); const props = { + containerDimensions: { + width: 3, + height: 4 + }, cursorOffset: { x: 0, y: 0 diff --git a/test/lib/dimensions.spec.js b/test/lib/dimensions.spec.js new file mode 100644 index 00000000..167e4e1f --- /dev/null +++ b/test/lib/dimensions.spec.js @@ -0,0 +1,59 @@ +import { expect } from 'chai'; +import { + convertPercentageToDecimal, + getDefaultEnlargedImageContainerDimensions, + getEnlargedImageContainerDimension, + isPercentageFormat +} from '../../src/lib/dimensions'; + +describe('Dimensions Library', () => { + describe('isPercentageFormat', () => { + it('returns true when input is formatted as a percentage', () => { + const actual = isPercentageFormat('100%'); + expect(actual).to.be.true; + }); + + it('returns false when input is not formatted as a percentage', () => { + expect(isPercentageFormat('100')).to.be.false; + expect(isPercentageFormat(100)).to.be.false; + }) + }); + + describe('convertPercentageToDecimal', () => { + it('returns a decimal number for percentage input', () => { + expect(convertPercentageToDecimal('75%')).to.equal(0.75); + }); + }); + + describe('getEnlargedImageContainerDimension', () => { + it('returns correct value when container dimension is a percentage', () => { + const actual = getEnlargedImageContainerDimension({ + containerDimension: '50%', + smallImageDimension: 2 + }); + + expect(actual).to.equal(1); + }); + + it('returns correct value when container dimension is a number', () => { + const actual = getEnlargedImageContainerDimension({ + containerDimension: 4, + smallImageDimension: 2 + }); + + expect(actual).to.equal(4); + }); + }); + + describe('getDefaultEnlargedImageContainerDimensions', () => { + it('returns correct default enlarged image container dimensions', () => { + const smallImage = { + width: 1, + height: 2 + }; + const actual = getDefaultEnlargedImageContainerDimensions(smallImage); + + expect(actual).to.deep.equal(smallImage); + }); + }); +}); diff --git a/test/lib/image-coordinates.spec.js b/test/lib/image-coordinates.spec.js index 2d7f30f5..31347448 100644 --- a/test/lib/image-coordinates.spec.js +++ b/test/lib/image-coordinates.spec.js @@ -1,5 +1,4 @@ import { expect } from 'chai'; -import { getLensCursorOffset } from '../../src/lib/lens'; import { getLensModeEnlargedImageCoordinates, getInPlaceEnlargedImageCoordinates @@ -8,6 +7,10 @@ import { describe('Image Coordinates Library', () => { describe('getLensModeEnlargedImageCoordinates', () => { it('returns image coordinates relative to its container', () => { + const enlargedImageContainerDimensions = { + width: 4, + height: 4 + }; const smallImage = { width: 4, height: 4 @@ -20,18 +23,24 @@ describe('Image Coordinates Library', () => { x: 2, y: 2 }; - const lensCursorOffset = getLensCursorOffset(smallImage, largeImage); - const expected = { - x: -2, - y: -2 - }; + const lensCursorOffset = { x: 1, y: 1 }; - const actual = getLensModeEnlargedImageCoordinates(smallImage, largeImage, position, lensCursorOffset); + const actual = getLensModeEnlargedImageCoordinates({ + smallImage, + largeImage, + position, + cursorOffset: lensCursorOffset, + containerDimensions: enlargedImageContainerDimensions + }); - expect(actual).to.deep.equal(expected); + expect(actual).to.deep.equal({ x: -2, y: -2 }); }); it('clamps position according to lens', () => { + const enlargedImageContainerDimensions = { + width: 4, + height: 4 + }; const smallImage = { width: 4, height: 4 @@ -44,21 +53,23 @@ describe('Image Coordinates Library', () => { x: 1, y: 3 }; - const lensCursorOffset = getLensCursorOffset(smallImage, largeImage); - const expected = { - x: -0, - y: -4 - }; + const lensCursorOffset = { x: 1, y: 1 }; - const actual = getLensModeEnlargedImageCoordinates(smallImage, largeImage, position, lensCursorOffset); + const actual = getLensModeEnlargedImageCoordinates({ + smallImage, + largeImage, + position, + cursorOffset: lensCursorOffset, + containerDimensions: enlargedImageContainerDimensions + }); - expect(actual).to.deep.equal(expected); + expect(actual).to.deep.equal({ x: -0, y: -4 }); }); }); describe('getInPlaceEnlargedImageCoordinates', () => { it('returns image coordinates relative to its container', () => { - const container = { + const containerDimensions = { width: 4, height: 4 }; @@ -70,18 +81,14 @@ describe('Image Coordinates Library', () => { x: 2, y: 2 }; - const expected = { - x: -2, - y: -2 - }; - const actual = getInPlaceEnlargedImageCoordinates(container, largeImage, position); + const actual = getInPlaceEnlargedImageCoordinates({ containerDimensions, largeImage, position }); - expect(actual).to.deep.equal(expected); + expect(actual).to.deep.equal({ x: -2, y: -2 }); }); it('clamps coordinates to the container when position is outside', () => { - const container = { + const containerDimensions = { width: 4, height: 4 }; @@ -93,14 +100,10 @@ describe('Image Coordinates Library', () => { x: 5, y: -1 }; - const expected = { - x: -4, - y: 0 - }; - const actual = getInPlaceEnlargedImageCoordinates(container, largeImage, position); + const actual = getInPlaceEnlargedImageCoordinates({ containerDimensions, largeImage, position }); - expect(actual).to.deep.equal(expected); + expect(actual).to.deep.equal({ x: -4, y: 0 }); }); }); }); diff --git a/test/lib/lens.spec.js b/test/lib/lens.spec.js index cac497a5..92a47ea2 100644 --- a/test/lib/lens.spec.js +++ b/test/lib/lens.spec.js @@ -4,6 +4,10 @@ import { getLensCursorOffset } from '../../src/lib/lens'; describe('Lens Library', () => { describe('getLensCursorOffset', () => { it('returns a point representing the offset from the cursor to the top-left of the clear lens', () => { + const enlargedImageContainerDimensions = { + width: 4, + height: 4 + }; const smallImage = { width: 4, height: 4 @@ -17,12 +21,16 @@ describe('Lens Library', () => { y: 1 } - const actual = getLensCursorOffset(smallImage, largeImage); + const actual = getLensCursorOffset(smallImage, largeImage, enlargedImageContainerDimensions); expect(actual).to.deep.equal(expected); }); it('rounds values', () => { + const enlargedImageContainerDimensions = { + width: 4, + height: 6 + }; const smallImage = { width: 4, height: 6 @@ -36,7 +44,7 @@ describe('Lens Library', () => { y: 2 // rounded up from 1.5 } - const actual = getLensCursorOffset(smallImage, largeImage); + const actual = getLensCursorOffset(smallImage, largeImage, enlargedImageContainerDimensions); expect(actual).to.deep.equal(expected); }); diff --git a/test/react-image-magnify.spec.js b/test/react-image-magnify.spec.js index 821fd828..3ab17bfb 100644 --- a/test/react-image-magnify.spec.js +++ b/test/react-image-magnify.spec.js @@ -76,6 +76,10 @@ describe('React Image Magnify', () => { it('has correct default props', () => { expect(ReactImageMagnify.defaultProps).to.deep.equal({ + enlargedImageContainerDimensions: { + width: '100%', + height: '100%' + }, fadeDurationInMs: 300, hoverDelayInMs: 250, hoverOffDelayInMs: 150,