Skip to content

Commit b7ccb8d

Browse files
committed
feat: use <picture> element for responsive images @W-18714493
1 parent 717940d commit b7ccb8d

File tree

12 files changed

+2382
-554
lines changed

12 files changed

+2382
-554
lines changed

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

Lines changed: 103 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,123 @@
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
15-
* @example
16-
* // Widths without a unit are interpreted as px values
17-
* <DynamicImage src="http://example.com/image.jpg[?sw={width}&q=60]" widths={[100, 360, 720]} />
18-
* <DynamicImage src="http://example.com/image.jpg[?sw={width}&q=60]" widths={{base: 100, sm: 360, md: 720}} />
19-
* // You can also use units of px or vw
20-
* <DynamicImage src="http://example.com/image.jpg[?sw={width}&q=60]" widths={['50vw', '100vw', '500px']} />
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.
24+
* @example Widths without a unit defined as array (interpreted as px values)
25+
* <DynamicImage
26+
* src="http://example.com/image.jpg[?sw={width}&q=60]"
27+
* widths={[100, 360, 720]} />
28+
* @example Widths without a unit defined as object (interpreted as px values)
29+
* <DynamicImage
30+
* src="http://example.com/image.jpg[?sw={width}&q=60]"
31+
* widths={{base: 100, sm: 360, md: 720}} />
32+
* @example Widths with mixed px and vw units defined as array
33+
* <DynamicImage
34+
* src="http://example.com/image.jpg[?sw={width}&q=60]"
35+
* widths={['50vw', '100vw', '500px']} />
36+
* @example Eagerly load image with high priority and responsive preloading
37+
* <DynamicImage
38+
* src="http://example.com/image.jpg[?sw={width}&q=60]"
39+
* widths={['50vw', '50vw', '20vw', '20vw', '25vw']}
40+
* imageProps={{loading: 'eager'}}
41+
* />
42+
* @see {@link https://web.dev/learn/design/responsive-images}
43+
* @see {@link https://web.dev/learn/design/picture-element}
44+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/picture}
45+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Responsive_images}
2146
* @see {@link https://help.salesforce.com/s/articleView?id=cc.b2c_image_transformation_service.htm&type=5}
2247
*/
2348
const DynamicImage = ({src, widths, densities, imageProps, as, ...rest}) => {
24-
const Component = as ? as : Image
49+
const Component = as ? as : Img
2550
const theme = useTheme()
2651

27-
const responsiveImageProps = useMemo(
28-
() =>
29-
getResponsiveImageAttributes({src, widths, densities, breakpoints: theme.breakpoints}),
30-
[src, widths, densities, theme.breakpoints]
31-
)
52+
const [responsiveImageProps, numSources, effectiveImageProps, responsiveLinks] = useMemo(() => {
53+
const responsiveImageProps = getResponsivePictureAttributes({
54+
src,
55+
widths,
56+
densities,
57+
breakpoints: theme.breakpoints
58+
})
59+
const effectiveImageProps = getImageAttributes(imageProps)
60+
const fetchPriority = effectiveImageProps.fetchPriority
61+
const responsiveLinks =
62+
!responsiveImageProps.links.length && fetchPriority === 'high'
63+
? [
64+
getImageLinkAttributes({
65+
...effectiveImageProps,
66+
fetchPriority, // React <18 vs. >=19 issue
67+
src: responsiveImageProps.src
68+
})
69+
]
70+
: responsiveImageProps.links.reduce((acc, link) => {
71+
const linkProps = getImageLinkAttributes({
72+
...effectiveImageProps,
73+
...link,
74+
fetchPriority, // React <18 vs. >=19 issue
75+
src: responsiveImageProps.src
76+
})
77+
if (linkProps) {
78+
acc.push(linkProps)
79+
}
80+
return acc
81+
}, [])
82+
return [
83+
responsiveImageProps,
84+
responsiveImageProps.sources.length,
85+
effectiveImageProps,
86+
responsiveLinks
87+
]
88+
}, [src, widths, densities, theme.breakpoints])
3289

3390
return (
3491
<Box {...rest}>
35-
<Component {...responsiveImageProps} {...imageProps} />
92+
{numSources > 0 ? (
93+
<picture>
94+
{responsiveImageProps.sources.map(({srcSet, sizes, media}, idx) => {
95+
if (idx < numSources - 1) {
96+
return <source key={idx} media={media} sizes={sizes} srcSet={srcSet} />
97+
}
98+
return (
99+
<Component
100+
key={idx}
101+
{...effectiveImageProps}
102+
sizes={sizes}
103+
srcSet={srcSet}
104+
src={responsiveImageProps.src}
105+
/>
106+
)
107+
})}
108+
</picture>
109+
) : (
110+
<Component {...effectiveImageProps} src={responsiveImageProps.src} />
111+
)}
112+
113+
{isServer() && responsiveLinks.length > 0 && (
114+
<Helmet>
115+
{responsiveLinks.map((responsiveLinkProps, idx) => {
116+
const {href, ...rest} = responsiveLinkProps
117+
return <link key={idx} {...rest} href={href} />
118+
})}
119+
</Helmet>
120+
)}
36121
</Box>
37122
)
38123
}
@@ -49,7 +134,7 @@ DynamicImage.propTypes = {
49134
/**
50135
* Image density factors to apply relative to the breakpoints. Will be mapped to the corresponding `srcSet`.
51136
*/
52-
densities: PropTypes.array,
137+
densities: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
53138
/**
54139
* Props to pass to the inner image component
55140
*/

0 commit comments

Comments
 (0)