Skip to content

Commit 0c6d073

Browse files
authored
feat: use <picture> elements for responsive images @W-18714493 (#2724)
1 parent 2712e5b commit 0c6d073

File tree

14 files changed

+1343
-342
lines changed

14 files changed

+1343
-342
lines changed

packages/template-retail-react-app/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- Show Automatic Bonus Products on Cart Page [#2704](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2704)
1717
- Support Standard Products [2697](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2697)
1818
- Fix passwordless race conditions in form submission [#2758](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2758)
19+
- Use `<picture>` element for responsive images [#2724](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2724)
1920

2021
## v6.1.0 (May 22, 2025)
2122

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

Lines changed: 91 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,112 @@
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, 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-
() => getResponsiveImageAttributes({src, widths, breakpoints: theme.breakpoints}),
29-
[src, widths, theme.breakpoints]
30-
)
52+
const [responsiveImageProps, numSources, effectiveImageProps, responsiveLinks] = useMemo(() => {
53+
const responsiveImageProps = getResponsivePictureAttributes({
54+
src,
55+
widths,
56+
breakpoints: theme.breakpoints
57+
})
58+
const effectiveImageProps = getImageAttributes(imageProps)
59+
const fetchPriority = effectiveImageProps.fetchPriority
60+
const responsiveLinks =
61+
!responsiveImageProps.links.length && fetchPriority === 'high'
62+
? [
63+
getImageLinkAttributes({
64+
...effectiveImageProps,
65+
fetchPriority, // React <18 vs. >=19 issue
66+
src: responsiveImageProps.src
67+
})
68+
]
69+
: responsiveImageProps.links.reduce((acc, link) => {
70+
const linkProps = getImageLinkAttributes({
71+
...effectiveImageProps,
72+
...link,
73+
fetchPriority, // React <18 vs. >=19 issue
74+
src: responsiveImageProps.src
75+
})
76+
if (linkProps) {
77+
acc.push(linkProps)
78+
}
79+
return acc
80+
}, [])
81+
return [
82+
responsiveImageProps,
83+
responsiveImageProps.sources.length,
84+
effectiveImageProps,
85+
responsiveLinks
86+
]
87+
}, [src, widths, theme.breakpoints])
3188

3289
return (
3390
<Box {...rest}>
34-
<Component {...responsiveImageProps} {...imageProps} />
91+
{numSources > 0 ? (
92+
<picture>
93+
{responsiveImageProps.sources.map(({srcSet, sizes, media}, idx) => (
94+
<source key={idx} media={media} sizes={sizes} srcSet={srcSet} />
95+
))}
96+
<Component {...effectiveImageProps} src={responsiveImageProps.src} />
97+
</picture>
98+
) : (
99+
<Component {...effectiveImageProps} src={responsiveImageProps.src} />
100+
)}
101+
102+
{isServer() && responsiveLinks.length > 0 && (
103+
<Helmet>
104+
{responsiveLinks.map((responsiveLinkProps, idx) => {
105+
const {href, ...rest} = responsiveLinkProps
106+
return <link key={idx} {...rest} href={href} />
107+
})}
108+
</Helmet>
109+
)}
35110
</Box>
36111
)
37112
}

0 commit comments

Comments
 (0)