diff --git a/src/components/Atoms/Picture/Picture.js b/src/components/Atoms/Picture/Picture.js index 4385d769e..7b5bdf03b 100644 --- a/src/components/Atoms/Picture/Picture.js +++ b/src/components/Atoms/Picture/Picture.js @@ -1,32 +1,14 @@ import React, { useEffect, useState } from 'react'; -import styled, { withTheme } from 'styled-components'; +import { withTheme } from 'styled-components'; import PropTypes from 'prop-types'; - +import focalPointCalc from '../../../utils/focalPointCalc'; import 'lazysizes'; import 'lazysizes/plugins/blur-up/ls.blur-up'; +import { Wrapper, Image } from './Picture.style'; // Transparent pixel png const IMAGE_FALLBACK = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; -const Wrapper = styled.div` - ${({ objFitState, nonObjFitImage }) => (!objFitState && nonObjFitImage) && `background-image: url(${nonObjFitImage}); background-size: cover; background-position: center;`}; - display: block; - width: ${props => (props.width ? props.width : '100%')}; - height: ${props => (props.height ? props.height : '100%')}; - position: relative; - ${({ isBackgroundImage }) => isBackgroundImage && 'position: absolute; bottom: 0px; left: 0px; right: 0px; height: 100%;'}; - `; - -const Image = styled.img` - width: ${props => (props.width ? props.width : '100%')}; - height: ${props => (props.height ? props.height : 'auto')}; - display: block; - object-fit: ${props => (props.objectFit === 'none' && 'none') - || (props.objectFit === 'cover' && 'cover') - || (props.objectFit === 'contain' && 'contain')}; - ${({ objectFit, objFitState }) => (objectFit !== 'none' && !objFitState) && 'visibility: hidden;'}; // Allows image to provide the container height, but make it invisible -`; - /** Responsive Picture */ const Picture = ({ @@ -38,6 +20,7 @@ const Picture = ({ objectFit, imageLow, isBackgroundImage, + focalPoint, ...rest }) => { const document = typeof window !== 'undefined' ? window.document : null; @@ -58,6 +41,8 @@ const Picture = ({ nonObjFitImage = images.substring(0, images.indexOf('?')); } + const calculatedFocalPoints = focalPointCalc(focalPoint); + if (!images) { return ( ); @@ -109,6 +96,8 @@ const Picture = ({ data-lowsrc={imageLow} className="lazyload" objFitState={objFitState} + focalPointXPos={calculatedFocalPoints.x} + focalPointYPos={calculatedFocalPoints.y} /> ); @@ -128,7 +117,13 @@ Picture.propTypes = { ]), width: PropTypes.string, height: PropTypes.string, - isBackgroundImage: PropTypes.bool + isBackgroundImage: PropTypes.bool, + focalPoint: PropTypes.shape({ + focalPointX: PropTypes.number, + focalPointY: PropTypes.number, + rawImageWidth: PropTypes.number, + rawImageHeight: PropTypes.number + }) }; Picture.defaultProps = { @@ -139,7 +134,13 @@ Picture.defaultProps = { width: '100%', height: 'auto', alt: '', - isBackgroundImage: false + isBackgroundImage: false, + focalPoint: { + focalPointX: null, + focalPointY: null, + rawImageWidth: null, + rawImageHeigh: null + } }; export default withTheme(Picture); diff --git a/src/components/Atoms/Picture/Picture.style.js b/src/components/Atoms/Picture/Picture.style.js new file mode 100644 index 000000000..8d08af8f5 --- /dev/null +++ b/src/components/Atoms/Picture/Picture.style.js @@ -0,0 +1,26 @@ +import styled, { css } from 'styled-components'; + +const Wrapper = styled.div` + ${({ objFitState, nonObjFitImage }) => (!objFitState && nonObjFitImage) && `background-image: url(${nonObjFitImage}); background-size: cover; background-position: center;`}; + display: block; + width: ${props => (props.width ? props.width : '100%')}; + height: ${props => (props.height ? props.height : '100%')}; + position: relative; + ${({ isBackgroundImage }) => isBackgroundImage && 'position: absolute; bottom: 0px; left: 0px; right: 0px; height: 100%;'}; + `; + +const Image = styled.img` + width: ${props => (props.width ? props.width : '100%')}; + height: ${props => (props.height ? props.height : 'auto')}; + display: block; + object-fit: ${props => (props.objectFit === 'none' && 'none') + || (props.objectFit === 'cover' && 'cover') + || (props.objectFit === 'contain' && 'contain')}; + ${({ objectFit, objFitState }) => (objectFit !== 'none' && !objFitState) && 'visibility: hidden;'}; // Allows image to provide the container height, but make it invisible + + ${props => (props.objectFit === 'cover' && props.focalPointXPos && props.focalPointYPos) && css` + object-position: ${props.focalPointXPos} ${props.focalPointYPos}; + `} +`; + +export { Wrapper, Image }; diff --git a/src/components/Atoms/Picture/Picture.test.js b/src/components/Atoms/Picture/Picture.test.js index 7a7ef0079..63f472a40 100644 --- a/src/components/Atoms/Picture/Picture.test.js +++ b/src/components/Atoms/Picture/Picture.test.js @@ -1,11 +1,15 @@ -import React from 'react'; -import 'jest-styled-components'; -import renderWithTheme from '../../../hoc/shallowWithTheme'; -import Picture from './Picture'; -import { defaultData } from '../../../styleguide/data/data'; -it('renders correctly', () => { +import React from "react"; +import "jest-styled-components"; +import renderWithTheme from "../../../hoc/shallowWithTheme"; +import Picture from "./Picture"; +import { defaultData } from "../../../styleguide/data/data"; +it("renders correctly", () => { const tree = renderWithTheme( - + ).toJSON(); expect(tree).toMatchInlineSnapshot(` @@ -44,7 +48,7 @@ it('renders correctly', () => { `); }); -it('renders correctly with custom props', () => { +it("renders correctly with custom props", () => { const tree = renderWithTheme( { height: 100px; display: block; object-fit: cover; + object-position: NaN% NaN%; }
{ +import React from "react"; +import "jest-styled-components"; +import renderWithTheme from "../../../hoc/shallowWithTheme"; +import ArticleTeaser from "./ArticleTeaser"; +import { defaultData } from "../../../styleguide/data/data"; +it("renders article teaser correctly", () => { const tree = renderWithTheme( { height: 100%; display: block; object-fit: cover; + object-position: NaN% NaN%; } .c1 { @@ -258,7 +259,7 @@ it('renders article teaser correctly', () => { `); }); -it('renders press realese correctly', () => { +it("renders press realese correctly", () => { const tree = renderWithTheme( { height: 80px; display: block; object-fit: cover; + object-position: NaN% NaN%; } .c1 { diff --git a/src/components/Molecules/Card/__snapshots__/Card.test.js.snap b/src/components/Molecules/Card/__snapshots__/Card.test.js.snap index a43d1e642..e97070d1b 100644 --- a/src/components/Molecules/Card/__snapshots__/Card.test.js.snap +++ b/src/components/Molecules/Card/__snapshots__/Card.test.js.snap @@ -54,6 +54,7 @@ exports[`renders correctly with no body 1`] = ` height: 100%; display: block; object-fit: cover; + object-position: NaN% NaN%; } .c0 { diff --git a/src/components/Molecules/CardDs/__snapshots__/CardDs.test.js.snap b/src/components/Molecules/CardDs/__snapshots__/CardDs.test.js.snap index 7d0c2fb18..24cc4bc38 100644 --- a/src/components/Molecules/CardDs/__snapshots__/CardDs.test.js.snap +++ b/src/components/Molecules/CardDs/__snapshots__/CardDs.test.js.snap @@ -13,6 +13,7 @@ exports[`renders correctly 1`] = ` height: auto; display: block; object-fit: cover; + object-position: NaN% NaN%; } .c8 { diff --git a/src/components/Molecules/Promo/__snapshots__/Promo.test.js.snap b/src/components/Molecules/Promo/__snapshots__/Promo.test.js.snap index 0f12f43e0..1e94aab69 100644 --- a/src/components/Molecules/Promo/__snapshots__/Promo.test.js.snap +++ b/src/components/Molecules/Promo/__snapshots__/Promo.test.js.snap @@ -37,6 +37,7 @@ exports[`renders Promo correctly 1`] = ` height: 100%; display: block; object-fit: cover; + object-position: NaN% NaN%; } .c8 { @@ -278,6 +279,7 @@ exports[`renders Promo correctly end position 1`] = ` height: 100%; display: block; object-fit: cover; + object-position: NaN% NaN%; } .c8 { diff --git a/src/components/Molecules/SingleMessage/__snapshots__/SingleMessage.test.js.snap b/src/components/Molecules/SingleMessage/__snapshots__/SingleMessage.test.js.snap index 2135186af..cc8367312 100644 --- a/src/components/Molecules/SingleMessage/__snapshots__/SingleMessage.test.js.snap +++ b/src/components/Molecules/SingleMessage/__snapshots__/SingleMessage.test.js.snap @@ -22,6 +22,7 @@ exports[`renders Single Message with 100% vertical height image correctly 1`] = height: 100%; display: block; object-fit: cover; + object-position: NaN% NaN%; } .c0 { @@ -197,6 +198,7 @@ exports[`renders Single Message with Image correctly 1`] = ` height: 100%; display: block; object-fit: cover; + object-position: NaN% NaN%; } .c8 { @@ -411,6 +413,7 @@ exports[`renders Single Message with double image correctly 1`] = ` height: 100%; display: block; object-fit: cover; + object-position: NaN% NaN%; } .c0 { @@ -631,6 +634,7 @@ exports[`renders Single Message with full width correctly 1`] = ` height: 100%; display: block; object-fit: cover; + object-position: NaN% NaN%; } .c8 { @@ -859,6 +863,7 @@ exports[`renders Single Message with full width image and no text correctly 1`] height: 100%; display: block; object-fit: cover; + object-position: NaN% NaN%; } .c0 { diff --git a/src/components/Molecules/SingleMessageDS/SingleMessageDs.js b/src/components/Molecules/SingleMessageDS/SingleMessageDs.js index aadb8e773..2b1dc9fc2 100644 --- a/src/components/Molecules/SingleMessageDS/SingleMessageDs.js +++ b/src/components/Molecules/SingleMessageDS/SingleMessageDs.js @@ -27,11 +27,11 @@ const SingleMessageDs = ({ target, linkIcon, youTubeId, + focalPoint, ...rest }) => { const [isOpen, setIsOpen] = useState(false); - // const openModal = () => setIsOpen(true); const closeModal = () => setIsOpen(false); const Media = ( @@ -44,6 +44,7 @@ const SingleMessageDs = ({ objectFit="cover" width={width} height={height} + focalPoint={focalPoint} /> ); @@ -187,7 +188,13 @@ SingleMessageDs.propTypes = { target: PropTypes.string, children: PropTypes.node.isRequired, linkIcon: PropTypes.node, - youTubeId: PropTypes.string + youTubeId: PropTypes.string, + focalPoint: PropTypes.shape({ + focalPointX: PropTypes.number, + focalPointY: PropTypes.number, + rawImageWidth: PropTypes.number, + rawImageHeight: PropTypes.number + }) }; SingleMessageDs.defaultProps = { @@ -203,7 +210,13 @@ SingleMessageDs.defaultProps = { width: '100%', height: '100%', linkIcon: null, - youTubeId: null + youTubeId: null, + focalPoint: { + focalPointX: null, + focalPointY: null, + rawImageWidth: null, + rawImageHeigh: null + } }; export default SingleMessageDs; diff --git a/src/components/Molecules/SingleMessageDS/SingleMessageDs.md b/src/components/Molecules/SingleMessageDS/SingleMessageDs.md index 6d727e241..bd979863a 100644 --- a/src/components/Molecules/SingleMessageDS/SingleMessageDs.md +++ b/src/components/Molecules/SingleMessageDS/SingleMessageDs.md @@ -185,4 +185,122 @@ import Download from '../../Atoms/Icons/Download';
; +``` + +## FocalPoint example 1 + +```js +const focalPointImages = require('../../../styleguide/data/data').focalPointImages; +import Text from '../../Atoms/Text/Text'; +import Link from '../../Atoms/Link/Link'; +import styled from 'styled-components'; +import spacing from '../../../theme/shared/spacing'; + +// These will come directly via the CMS query in the proper CRcom +// context; I've just hardcoded the actual size values from the example +// image and focal point that focuses on the subject. +const exampleFocalPoint = { + rawImageWidth: 2000, + rawImageHeight: 945, + focalPointX: 1450, + focalPointY: 350 +}; + +const Title = styled(Text)` + letter-spacing: 0.03em; + text-transform: uppercase; + margin: ${spacing('md')} 0; + @media ${({ theme }) => theme.breakpoint('small')} { + margin-bottom: ${spacing('m')}; + } +`; +
+ + + Heading Line 1 Heading Line 2 + + + Whatever you’ve got planned, the Sport Relief shop has everything you need + to get you looking your best while you’re raising some cash. Also + available in Sainsbury’s stores and online and in selected Argos stores. + + +
; +``` + +## FocalPoint example 2 + +```js +const focalPointImagesTwo = require('../../../styleguide/data/data').focalPointImagesTwo; +import Text from '../../Atoms/Text/Text'; +import Link from '../../Atoms/Link/Link'; +import styled from 'styled-components'; +import spacing from '../../../theme/shared/spacing'; + +// These will come directly via the CMS query in the proper CRcom +// context; I've just hardcoded the actual size values from the example +// image and focal point that focuses on the subject. +const exampleFocalPoint = { + rawImageWidth: 1872, + rawImageHeight: 686, + focalPointX: 440, // Tall skyscraper on the left + focalPointY: 240 +}; + +const Title = styled(Text)` + letter-spacing: 0.03em; + text-transform: uppercase; + margin: ${spacing('md')} 0; + @media ${({ theme }) => theme.breakpoint('small')} { + margin-bottom: ${spacing('m')}; + } +`; +
+ + + Heading Line 1 Heading Line 2 + + + Whatever you’ve got planned, the Sport Relief shop has everything you need + to get you looking your best while you’re raising some cash. Also + available in Sainsbury’s stores and online and in selected Argos stores. + + +
; ``` \ No newline at end of file diff --git a/src/components/Molecules/SingleMessageDS/__snapshots__/SingleMessageDs.test.js.snap b/src/components/Molecules/SingleMessageDS/__snapshots__/SingleMessageDs.test.js.snap index 202b8079c..e0bea3b90 100644 --- a/src/components/Molecules/SingleMessageDS/__snapshots__/SingleMessageDs.test.js.snap +++ b/src/components/Molecules/SingleMessageDS/__snapshots__/SingleMessageDs.test.js.snap @@ -31,6 +31,7 @@ exports[`renders correctly 1`] = ` height: auto; display: block; object-fit: cover; + object-position: NaN% NaN%; } .c12 { diff --git a/src/styleguide/data/data.js b/src/styleguide/data/data.js index 41bda798e..1515d89f5 100644 --- a/src/styleguide/data/data.js +++ b/src/styleguide/data/data.js @@ -26,4 +26,24 @@ const mobileImages = { '//https://images.ctfassets.net/zsfivwzfgl3t/54DHIEgtwbr9TDkf70lToB/ffe8d6a8e9bbc224343f475a5c55c832/-CROP-Promo-Des_2000x945-_Kate.jpg?w=200&h=150&q=50 200w,//images.ctfassets.net/zsfivwzfgl3t/54DHIEgtwbr9TDkf70lToB/ffe8d6a8e9bbc224343f475a5c55c832/-CROP-Promo-Des_2000x945-_Kate.jpg?w=400&h=300&q=50 400w,//images.ctfassets.net/zsfivwzfgl3t/54DHIEgtwbr9TDkf70lToB/ffe8d6a8e9bbc224343f475a5c55c832/-CROP-Promo-Des_2000x945-_Kate.jpg?w=800&h=600&q=50 800w,//images.ctfassets.net/zsfivwzfgl3t/54DHIEgtwbr9TDkf70lToB/ffe8d6a8e9bbc224343f475a5c55c832/-CROP-Promo-Des_2000x945-_Kate.jpg?w=1200&h=900&q=50 1200w,//images.ctfassets.net/zsfivwzfgl3t/54DHIEgtwbr9TDkf70lToB/ffe8d6a8e9bbc224343f475a5c55c832/-CROP-Promo-Des_2000x945-_Kate.jpg?w=1440&h=1080&q=50 1440w' }; -export { defaultData, mobileImages }; +const focalPointImages = { + imageLow: + 'http://images.ctfassets.net/zsfivwzfgl3t/54DHIEgtwbr9TDkf70lToB/ffe8d6a8e9bbc224343f475a5c55c832/-CROP-Promo-Des_2000x945-_Kate.jpg?w=100&h=50&q=100', + image: + 'https://images.ctfassets.net/zsfivwzfgl3t/54DHIEgtwbr9TDkf70lToB/ffe8d6a8e9bbc224343f475a5c55c832/-CROP-Promo-Des_2000x945-_Kate.jpg', + images: + '//https://images.ctfassets.net/zsfivwzfgl3t/54DHIEgtwbr9TDkf70lToB/ffe8d6a8e9bbc224343f475a5c55c832/-CROP-Promo-Des_2000x945-_Kate.jpg?w=200&h=150&q=50 200w,//images.ctfassets.net/zsfivwzfgl3t/54DHIEgtwbr9TDkf70lToB/ffe8d6a8e9bbc224343f475a5c55c832/-CROP-Promo-Des_2000x945-_Kate.jpg?w=400&h=300&q=50 400w,//images.ctfassets.net/zsfivwzfgl3t/54DHIEgtwbr9TDkf70lToB/ffe8d6a8e9bbc224343f475a5c55c832/-CROP-Promo-Des_2000x945-_Kate.jpg?w=800&h=600&q=50 800w,//images.ctfassets.net/zsfivwzfgl3t/54DHIEgtwbr9TDkf70lToB/ffe8d6a8e9bbc224343f475a5c55c832/-CROP-Promo-Des_2000x945-_Kate.jpg?w=1200&h=900&q=50 1200w,//images.ctfassets.net/zsfivwzfgl3t/54DHIEgtwbr9TDkf70lToB/ffe8d6a8e9bbc224343f475a5c55c832/-CROP-Promo-Des_2000x945-_Kate.jpg?w=1440&h=1080&q=50 1440w' +}; + +const focalPointImagesTwo = { + imageLow: + 'https://images.ctfassets.net/zsfivwzfgl3t/YzhX9IaivFhLPecCoHrOf/0918e3bbbde4d8d0f00ba0d2ca0ab16d/panorama.png?w=100&h=50&q=100', + image: + 'https://images.ctfassets.net/zsfivwzfgl3t/YzhX9IaivFhLPecCoHrOf/0918e3bbbde4d8d0f00ba0d2ca0ab16d/panorama.png', + images: + '//https://images.ctfassets.net/zsfivwzfgl3t/YzhX9IaivFhLPecCoHrOf/0918e3bbbde4d8d0f00ba0d2ca0ab16d/panorama.png?w=200&h=150&q=50 200w,//images.ctfassets.net/zsfivwzfgl3t/YzhX9IaivFhLPecCoHrOf/0918e3bbbde4d8d0f00ba0d2ca0ab16d/panorama.png?w=400&h=300&q=50 400w,//images.ctfassets.net/zsfivwzfgl3t/YzhX9IaivFhLPecCoHrOf/0918e3bbbde4d8d0f00ba0d2ca0ab16d/panorama.png?w=800&h=600&q=50 800w,//images.ctfassets.net/zsfivwzfgl3t/YzhX9IaivFhLPecCoHrOf/0918e3bbbde4d8d0f00ba0d2ca0ab16d/panorama.png?w=1200&h=900&q=50 1200w,//images.ctfassets.net/zsfivwzfgl3t/YzhX9IaivFhLPecCoHrOf/0918e3bbbde4d8d0f00ba0d2ca0ab16d/panorama.png?w=1440&h=1080&q=50 1440w' +}; + +export { + defaultData, mobileImages, focalPointImages, focalPointImagesTwo +}; diff --git a/src/utils/focalPointCalc.js b/src/utils/focalPointCalc.js new file mode 100644 index 000000000..78070cd7e --- /dev/null +++ b/src/utils/focalPointCalc.js @@ -0,0 +1,31 @@ +/* A handy to make the percentage math nice and reusable */ +const focalPointCalc = focalPointData => { + // Calculate the focal points as percentages of the image dimensions + let x = (focalPointData.focalPointX / focalPointData.rawImageWidth) * 100; + let y = (focalPointData.focalPointY / focalPointData.rawImageHeight) * 100; + + // Some ugly maths to basically just sweetenen the percentage, resulting + // in a slightly higher value (up to * 1.1 around the 25% and 75% points) + // to get the position close to what we actually want visually with our + // fluid layout and 'cover' object-fit CSS rule + + const maths = true; + + if (maths) { + x *= (0.1 / 25) * (25 - Math.abs((x % (2 * 25)) - 25)) + 1; + y *= (0.1 / 25) * (25 - Math.abs((y % (2 * 25)) - 25)) + 1; + } + + // Round-up for best browser compatibility + x = Math.round(x); + y = Math.round(y); + + // Return this directly as a percentage to be used within CSS; + // hard pixel values won't ever work, given that fluid layout + return { + x: `${x}%`, + y: `${y}%` + }; +}; + +export default focalPointCalc;