Skip to content

Commit

Permalink
Merge 4f68b09 into a031c2f
Browse files Browse the repository at this point in the history
  • Loading branch information
lbebber committed Oct 11, 2019
2 parents a031c2f + 4f68b09 commit cee21aa
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 77 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- `width` and `height` props on `product-summary-image`.

### Added
- Prop `buyButtonBehavior` to `ProductSummaryBuyButton`.
Expand Down
14 changes: 10 additions & 4 deletions docs/ProductSummaryImage.md
Expand Up @@ -9,10 +9,14 @@ This Component can be imported and used by any VTEX App.

## Table of Contents

- [Usage](#usage)
- [Blocks API](#blocks-api)
- [Configuration](#configuration)
- [Styles API](#styles-api)
- [Product Summary Image](#product-summary-image)
- [Description](#description)
- [Table of Contents](#table-of-contents)
- [Usage](#usage)
- [Blocks API](#blocks-api)
- [Configuration](#configuration)
- [Styles API](#styles-api)
- [CSS namespaces](#css-namespaces)

## Usage

Expand Down Expand Up @@ -41,6 +45,8 @@ Through the Storefront, you can change the `ProductSummaryImage`'s behavior and
| `showCollections` | `Boolean` | Set collection badges' visibility | `false` |
| `displayMode` | `Enum` | Set display mode of product summary (normal, inline) | `normal` |
| `hoverImageLabel` | `String` | Set this value to match the value of the `imageLabel` field of a product image, so this "secondary" image will be showed when user hovers its mouse over this component. | `""` |
| `width` | `[ResponsiveInput<Number>](https://github.com/vtex-apps/responsive-values#vtexresponsive-values)` | Sets the image width. | `undefined` |
| `height` | `[ResponsiveInput<Number>](https://github.com/vtex-apps/responsive-values#vtexresponsive-values)` | Sets the image height. | `undefined` |

### Styles API

Expand Down
4 changes: 3 additions & 1 deletion manifest.json
Expand Up @@ -28,7 +28,9 @@
"vtex.device-detector": "0.x",
"vtex.product-quantity": "1.x",
"vtex.product-specification-badges": "0.x",
"vtex.stack-layout": "0.x"
"vtex.stack-layout": "0.x",
"vtex.responsive-values": "0.x",
"vtex.css-handles": "0.x"
},
"$schema": "https://raw.githubusercontent.com/vtex/node-vtex-api/master/gen/manifest.schema"
}
7 changes: 7 additions & 0 deletions react/__mocks__/vtex.css-handles.js
@@ -0,0 +1,7 @@
export const useCssHandles = input =>
input.reduce((acc, cur) => {
acc[cur] = cur
return acc
}, {})

export const applyModifiers = (input, modifier) => input
6 changes: 6 additions & 0 deletions react/__mocks__/vtex.responsive-values.js
@@ -0,0 +1,6 @@
export const useResponsiveValues = input =>
Object.keys(input).reduce((acc, cur) => {
const value = input[cur]
acc[cur] = (value && value.desktop) || value
return acc
}, {})
2 changes: 1 addition & 1 deletion react/components/ProductSummary.js
Expand Up @@ -10,7 +10,7 @@ import {
} from 'vtex.product-summary-context/ProductSummaryContext'
import productSummary from '../productSummary.css'
import { productShape } from '../utils/propTypes'
import { mapCatalogProductToProductSummary } from '../utils/normarlize'
import { mapCatalogProductToProductSummary } from '../utils/normalize'

const ProductSummaryCustom = ({ product, actionOnClick, children }) => {
const { isLoading, isHovering } = useProductSummary()
Expand Down
189 changes: 120 additions & 69 deletions react/components/ProductSummaryImage/ProductImage.js
Expand Up @@ -4,11 +4,17 @@ import PropTypes from 'prop-types'
import { CollectionBadges, DiscountBadge } from 'vtex.store-components'
import classNames from 'classnames'
import { useDevice } from 'vtex.device-detector'
import { useResponsiveValues } from 'vtex.responsive-values'
import { useCssHandles, applyModifiers } from 'vtex.css-handles'

import { useProductSummary } from 'vtex.product-summary-context/ProductSummaryContext'

import productSummary from '../../productSummary.css'

import { changeImageUrlSize } from '../../utils/normalize'

const CSS_HANDLES = ['image', 'imageContainer', 'product', 'imagePlaceholder']

const maybeBadge = ({ listPrice, price, label }) => shouldShow => component => {
if (shouldShow) {
return (
Expand All @@ -32,52 +38,42 @@ const maybeCollection = ({ productClusters }) => shouldShow => component => {
return component
}

export const ImagePlaceholder = () => (
<div className="relative">
<div
className={`${productSummary.imagePlaceholder} absolute w-100 h-100 contain bg-center`}
/>
<svg
width="100%"
height="100%"
viewBox="0 0 512 512"
fill="none"
xmlns="http://www.w3.org/2000/svg"
data-testid="image-placeholder"
>
<rect width="512" height="512" fill="#F2F2F2" />
<rect
x="183.857"
y="180.2"
width="144.286"
height="150.474"
stroke="#CACBCC"
strokeWidth="2"
/>
<path d="M183.78 303.688H328.214" stroke="#CACBCC" strokeWidth="2" />
<path
d="M205.082 279.563L223.599 240.507L242.116 260.035L269.892 220.979L306.926 279.563H205.082Z"
stroke="#CACBCC"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M252.225 213.939C252.225 219.822 247.66 224.52 242.114 224.52C236.569 224.52 232.004 219.822 232.004 213.939C232.004 208.057 236.569 203.359 242.114 203.359C247.66 203.359 252.225 208.057 252.225 213.939Z"
stroke="#CACBCC"
strokeWidth="2"
/>
</svg>
</div>
)

const findHoverImage = (images, hoverImageLabel) => {
if (!hoverImageLabel) {
return null
}
return images.find(({ imageLabel }) => imageLabel === hoverImageLabel)
}

const Image = ({ src, width, height, onError, alt, className }) => {
const { isMobile } = useDevice()

const dpi = window.devicePixelRatio || (isMobile ? 2 : 1)

const shouldResize = !!(width || height)

return (
<img
src={
shouldResize ? changeImageUrlSize(src, width * dpi, height * dpi) : src
}
style={
shouldResize
? {
width,
height,
maxHeight: 'unset',
maxWidth: 'unset',
}
: null
}
alt={alt}
className={className}
onError={onError}
/>
)
}

const ProductImageContent = ({
product,
showBadge,
Expand All @@ -86,19 +82,37 @@ const ProductImageContent = ({
displayMode,
onError,
hoverImageLabel,
width: widthProp,
height: heightProp,
hasError,
}) => {
const {
productClusters,
productName: name,
sku: {
image: { imageUrl },
images,
},
} = product
const { productClusters, productName: name } = product || {}

const sku = product && product.sku

const imageUrl = path(['image', 'imageUrl'], sku)
const images = path(['images'], sku)

const { isMobile } = useDevice()
const handles = useCssHandles(CSS_HANDLES)

const [width, height] = [
// fallsback to the other remaining value, if not defined
parseFloat(widthProp || heightProp || 0),
parseFloat(heightProp || widthProp || 0),
]

const imageContentClassName = classNames({
if (!sku || hasError) {
return (
<ImagePlaceholder
width={width}
height={height}
handle={handles.productImage}
/>
)
}

const legacyImageClasses = classNames({
[productSummary.imageNormal]: displayMode !== 'inline',
[productSummary.imageInline]: displayMode === 'inline',
})
Expand All @@ -115,31 +129,50 @@ const ProductImageContent = ({
price: commertialOffer.Price,
label: badgeText,
})

const withCollection = maybeCollection({ productClusters })

const hoverImage = findHoverImage(images, hoverImageLabel)

const hoverImgClasses = classNames(
const imageClassname = classNames(legacyImageClasses, handles.image)

const hoverImageClassname = classNames(
'w-100 h-100 dn absolute top-0 left-0 z-999',
imageContentClassName,
applyModifiers(handles.image, 'hover'),
legacyImageClasses,
productSummary.hoverImage
)

const imgStackClasses = classNames(
'dib relative',
const legacyContainerClasses = classNames(
productSummary.imageStackContainer,
productSummary.hoverEffect
)

const containerClassname = classNames(
'dib relative',
handles.imageContainer,
legacyContainerClasses
)

const img = (
<div className={imgStackClasses}>
<img
className={imageContentClassName}
<div className={containerClassname}>
<Image
src={imageUrl}
width={width}
height={height}
alt={name}
className={imageClassname}
onError={onError}
/>
{hoverImage && !isMobile && (
<img src={hoverImage.imageUrl} alt={name} className={hoverImgClasses} />
<Image
src={hoverImage.imageUrl}
width={width}
height={height}
alt={name}
className={hoverImageClassname}
onError={onError}
/>
)}
</div>
)
Expand All @@ -150,35 +183,51 @@ const ProductImageContent = ({
)(img)
}

const ImagePlaceholder = ({ width, height, handle }) => (
<div style={{ width, height }}>
<div
className={`${handle} absolute w-100 h-100 contain bg-center`}
data-testid="image-placeholder"
/>
</div>
)

const ProductImage = ({
showBadge,
badgeText,
showCollections,
displayMode,
hoverImageLabel,
width: widthProp,
height: heightProp,
}) => {
const { product } = useProductSummary()

const { widthProp: width, heightProp: height } = useResponsiveValues({
widthProp,
heightProp,
})

const [error, setError] = useState(false)

const imageClassName = classNames(productSummary.imageContainer, {
'db w-100 center': displayMode !== 'inline',
})

return (
<div className={imageClassName}>
{path(['sku', 'image', 'imageUrl'], product) && !error ? (
<ProductImageContent
showBadge={showBadge}
badgeText={badgeText}
showCollections={showCollections}
displayMode={displayMode}
product={product}
onError={() => setError(true)}
hoverImageLabel={hoverImageLabel}
/>
) : (
<ImagePlaceholder />
)}
<ProductImageContent
showBadge={showBadge}
badgeText={badgeText}
showCollections={showCollections}
displayMode={displayMode}
product={product}
onError={() => setError(true)}
hoverImageLabel={hoverImageLabel}
width={width}
height={height}
hasError={error}
/>
</div>
)
}
Expand All @@ -193,6 +242,8 @@ ProductImage.propTypes = {
/** Display mode of the summary */
displayMode: PropTypes.oneOf(['normal', 'inline']),
hoverImageLabel: PropTypes.string,
width: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
height: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
}

ProductImage.defaultProps = {
Expand Down

0 comments on commit cee21aa

Please sign in to comment.