Skip to content

Commit 0a86480

Browse files
committed
feat: use <picture> element for responsive images @W-18714493
1 parent 1c08822 commit 0a86480

File tree

11 files changed

+2225
-521
lines changed

11 files changed

+2225
-521
lines changed

packages/template-retail-react-app/app/components/dynamic-image/index.jsx

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,107 @@
11
/*
2-
* Copyright (c) 2021, salesforce.com, inc.
2+
* Copyright (c) 2025, Salesforce, Inc.
33
* All rights reserved.
44
* SPDX-License-Identifier: BSD-3-Clause
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77
import React, {useMemo} from 'react'
8+
import {Helmet} from 'react-helmet'
89
import PropTypes from 'prop-types'
910
import {Box, useTheme} from '@salesforce/retail-react-app/app/components/shared/ui'
10-
import Image from '@salesforce/retail-react-app/app/components/image'
11-
import {getResponsiveImageAttributes} from '@salesforce/retail-react-app/app/utils/responsive-image'
11+
import {Img} from '@salesforce/retail-react-app/app/components/shared/ui'
12+
import {getResponsivePictureAttributes} from '@salesforce/retail-react-app/app/utils/responsive-image'
13+
import {
14+
getImageAttributes,
15+
getImageLinkAttributes
16+
} from '@salesforce/retail-react-app/app/utils/image'
17+
import {isServer} from '@salesforce/retail-react-app/app/components/image/utils'
1218

1319
/**
14-
* Quickly create a responsive image using your Dynamic Imaging Service
20+
* Responsive image component optimized to work with the Dynamic Imaging Service.
21+
* Via this component it's easy to create a `<picture>` element with related
22+
* theme-aware `<source>` elements and responsive preloading for high-priority
23+
* images.
1524
* @example
1625
* // Widths without a unit are interpreted as px values
1726
* <DynamicImage src="http://example.com/image.jpg[?sw={width}&q=60]" widths={[100, 360, 720]} />
1827
* <DynamicImage src="http://example.com/image.jpg[?sw={width}&q=60]" widths={{base: 100, sm: 360, md: 720}} />
1928
* // You can also use units of px or vw
2029
* <DynamicImage src="http://example.com/image.jpg[?sw={width}&q=60]" widths={['50vw', '100vw', '500px']} />
30+
* @see {@link https://web.dev/learn/design/responsive-images}
31+
* @see {@link https://web.dev/learn/design/picture-element}
32+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/picture}
33+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Responsive_images}
2134
* @see {@link https://help.salesforce.com/s/articleView?id=cc.b2c_image_transformation_service.htm&type=5}
2235
*/
2336
const DynamicImage = ({src, widths, densities, imageProps, as, ...rest}) => {
24-
const Component = as ? as : Image
37+
const Component = as ? as : Img
2538
const theme = useTheme()
2639

27-
const responsiveImageProps = useMemo(
28-
() =>
29-
getResponsiveImageAttributes({src, widths, densities, breakpoints: theme.breakpoints}),
30-
[src, widths, densities, theme.breakpoints]
31-
)
40+
const [responsiveImageProps, l, effectiveImageProps, responsiveLinks] = useMemo(() => {
41+
const responsiveImageProps = getResponsivePictureAttributes({
42+
src,
43+
widths,
44+
densities,
45+
breakpoints: theme.breakpoints
46+
})
47+
const l = responsiveImageProps.sources.length
48+
const effectiveImageProps = getImageAttributes(imageProps)
49+
const fetchPriority = effectiveImageProps.fetchPriority
50+
const responsiveLinks =
51+
!responsiveImageProps.links.length && fetchPriority === 'high'
52+
? [
53+
getImageLinkAttributes({
54+
...effectiveImageProps,
55+
fetchPriority, // React <18 vs. >=19 issue
56+
src: responsiveImageProps.src
57+
})
58+
]
59+
: responsiveImageProps.links.reduce((acc, link) => {
60+
const linkProps = getImageLinkAttributes({
61+
...effectiveImageProps,
62+
...link,
63+
fetchPriority, // React <18 vs. >=19 issue
64+
src: responsiveImageProps.src
65+
})
66+
if (linkProps) {
67+
acc.push(linkProps)
68+
}
69+
return acc
70+
}, [])
71+
return [responsiveImageProps, l, effectiveImageProps, responsiveLinks]
72+
}, [src, widths, densities, theme.breakpoints])
3273

3374
return (
3475
<Box {...rest}>
35-
<Component {...responsiveImageProps} {...imageProps} />
76+
{l > 0 ? (
77+
<picture>
78+
{responsiveImageProps.sources.map(({srcSet, sizes, media, src}, idx) => {
79+
if (idx < l - 1) {
80+
return <source key={idx} media={media} sizes={sizes} srcSet={srcSet} />
81+
}
82+
return (
83+
<Component
84+
key={idx}
85+
{...effectiveImageProps}
86+
sizes={sizes}
87+
srcSet={srcSet}
88+
src={responsiveImageProps.src}
89+
/>
90+
)
91+
})}
92+
</picture>
93+
) : (
94+
<Component {...effectiveImageProps} src={responsiveImageProps.src} />
95+
)}
96+
97+
{isServer() && responsiveLinks.length > 0 && (
98+
<Helmet>
99+
{responsiveLinks.map((responsiveLinkProps, idx) => {
100+
const {href, ...rest} = responsiveLinkProps
101+
return <link key={idx} {...rest} href={href} />
102+
})}
103+
</Helmet>
104+
)}
36105
</Box>
37106
)
38107
}
@@ -49,7 +118,7 @@ DynamicImage.propTypes = {
49118
/**
50119
* Image density factors to apply relative to the breakpoints. Will be mapped to the corresponding `srcSet`.
51120
*/
52-
densities: PropTypes.array,
121+
densities: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
53122
/**
54123
* Props to pass to the inner image component
55124
*/

0 commit comments

Comments
 (0)