Skip to content

Commit 50784c7

Browse files
committed
Add srcset support to externalLinkList thumbnails
At larger link widths - especially xxl in full-width sections - a single thumbnail can span well over 1024 CSS pixels, making the previously used linkThumbnailLarge (394) or medium (1024) variants blurry on high-DPI displays. Emit an srcset ramp keyed on linkWidth: xs, s -> linkThumbnailLarge (unchanged, small pre-cropped asset) m, l -> medium + large xl,xxl -> medium + large + ultra With the image_srcset feature flag disabled, falls back to the previous single-variant behavior.
1 parent 66f7080 commit 50784c7

4 files changed

Lines changed: 142 additions & 4 deletions

File tree

entry_types/scrolled/package/spec/contentElements/externalLinkList/frontend/ExternalLink-spec.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import React from 'react';
55
import {renderInEntry} from 'support';
66
import {screen} from '@testing-library/react';
77
import '@testing-library/jest-dom/extend-expect';
8+
import {features} from 'pageflow/frontend';
89

910
describe('ExternalLink', () => {
1011
it('renders link with href', () => {
@@ -237,6 +238,29 @@ describe('ExternalLink', () => {
237238
expect(getByRole('img')).toHaveAttribute('src', expect.stringContaining('medium'));
238239
});
239240

241+
it('falls back to linkThumbnailLarge for large linkWidth when image_srcset disabled', () => {
242+
const {getByRole} = renderInEntry(
243+
<ExternalLink configuration={{}}
244+
url=""
245+
loadImages={true}
246+
thumbnail={5}
247+
linkWidth="xxl" />,
248+
{
249+
seed: {
250+
imageFiles: [{permaId: 5}],
251+
imageFileUrlTemplates: {
252+
linkThumbnailLarge: ':id_partition/linkThumbnailLarge/image.jpg',
253+
medium: ':id_partition/medium/image.jpg',
254+
large: ':id_partition/large/image.jpg'
255+
},
256+
}
257+
}
258+
)
259+
260+
expect(getByRole('img')).toHaveAttribute('src', expect.stringContaining('linkThumbnailLarge'));
261+
expect(getByRole('img')).not.toHaveAttribute('srcset');
262+
});
263+
240264
it('uses medium image as thumbnail with thumbnail fit contain', () => {
241265
const {getByRole} = renderInEntry(
242266
<ExternalLink configuration={{}}
@@ -257,6 +281,86 @@ describe('ExternalLink', () => {
257281

258282
expect(getByRole('img')).toHaveAttribute('src', expect.stringContaining('medium'));
259283
});
284+
285+
describe('srcset', () => {
286+
beforeEach(() => features.enable('frontend', ['image_srcset']));
287+
afterEach(() => features.enabledFeatureNames = []);
288+
289+
it('uses medium and large srcset for linkWidth m', () => {
290+
const {getByRole} = renderInEntry(
291+
<ExternalLink configuration={{}}
292+
url=""
293+
loadImages={true}
294+
thumbnail={5}
295+
linkWidth="m" />,
296+
{
297+
seed: {
298+
imageFiles: [{permaId: 5, id: 1, width: 4000, height: 3000}],
299+
imageFileUrlTemplates: {
300+
linkThumbnailLarge: ':id_partition/linkThumbnailLarge/image.jpg',
301+
medium: ':id_partition/medium/image.jpg',
302+
large: ':id_partition/large/image.jpg'
303+
},
304+
}
305+
}
306+
)
307+
308+
expect(getByRole('img')).toHaveAttribute('srcset',
309+
'000/000/001/medium/image.jpg 1024w, 000/000/001/large/image.jpg 1920w');
310+
expect(getByRole('img')).toHaveAttribute('sizes',
311+
'(min-width: 950px) 50vw, 100vw');
312+
});
313+
314+
it('uses medium, large and ultra srcset for linkWidth xxl', () => {
315+
const {getByRole} = renderInEntry(
316+
<ExternalLink configuration={{}}
317+
url=""
318+
loadImages={true}
319+
thumbnail={5}
320+
linkWidth="xxl" />,
321+
{
322+
seed: {
323+
imageFiles: [{permaId: 5, id: 1, width: 4000, height: 3000}],
324+
imageFileUrlTemplates: {
325+
linkThumbnailLarge: ':id_partition/linkThumbnailLarge/image.jpg',
326+
medium: ':id_partition/medium/image.jpg',
327+
large: ':id_partition/large/image.jpg',
328+
ultra: ':id_partition/ultra/image.jpg'
329+
},
330+
}
331+
}
332+
)
333+
334+
expect(getByRole('img')).toHaveAttribute('srcset',
335+
'000/000/001/medium/image.jpg 1024w, ' +
336+
'000/000/001/large/image.jpg 1920w, ' +
337+
'000/000/001/ultra/image.jpg 3840w');
338+
expect(getByRole('img')).toHaveAttribute('sizes', '100vw');
339+
});
340+
341+
it('still uses linkThumbnailLarge for small linkWidth', () => {
342+
const {getByRole} = renderInEntry(
343+
<ExternalLink configuration={{}}
344+
url=""
345+
loadImages={true}
346+
thumbnail={5}
347+
linkWidth="s" />,
348+
{
349+
seed: {
350+
imageFiles: [{permaId: 5, id: 1, width: 4000, height: 3000}],
351+
imageFileUrlTemplates: {
352+
linkThumbnailLarge: ':id_partition/linkThumbnailLarge/image.jpg',
353+
medium: ':id_partition/medium/image.jpg',
354+
large: ':id_partition/large/image.jpg'
355+
},
356+
}
357+
}
358+
)
359+
360+
expect(getByRole('img')).toHaveAttribute('src', expect.stringContaining('linkThumbnailLarge'));
361+
expect(getByRole('img')).not.toHaveAttribute('srcset');
362+
});
363+
});
260364
});
261365

262366
function value(text) {

entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/ExternalLink.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ export function ExternalLink({id, configuration, contentElementId, ...props}) {
165165
aspectRatio={props.thumbnailAspectRatio}
166166
cropPosition={props.thumbnailCropPosition}
167167
fit={props.thumbnailFit}
168+
linkWidth={props.linkWidth}
168169
load={props.loadImages}
169170
showPlaceholder={isEditable}>
170171
<InlineFileRights configuration={configuration}

entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/ExternalLinkList.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ export function ExternalLinkList(props) {
135135
thumbnailFit={props.configuration.thumbnailFit || 'cover'}
136136
textPosition={props.configuration.textPosition || 'below'}
137137
textSize={props.configuration.textSize || 'small'}
138+
linkWidth={linkWidth}
138139
darkBackground={darkBackground}
139140
loadImages={shouldLoad}
140141
outlined={isSelected}

entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/Thumbnail.js

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import React from 'react';
22
import classNames from 'classnames';
3+
import {features} from 'pageflow/frontend';
34

45
import {Image, FilePlaceholder} from 'pageflow-scrolled/frontend';
56

67
import styles from './Thumbnail.module.css';
78

8-
export function Thumbnail({imageFile, aspectRatio, cropPosition, fit, load, showPlaceholder, children}) {
9+
export function Thumbnail({imageFile, aspectRatio, cropPosition, fit, linkWidth,
10+
load, showPlaceholder, children}) {
911
imageFile = {
1012
...imageFile,
1113
cropPosition
1214
};
1315

1416
const aspectRatioPadding = getAspectRatioPadding(aspectRatio, imageFile);
17+
const {variant, sizes} = variantAndSizes({aspectRatio, cropPosition, fit, linkWidth});
1518

1619
return (
1720
<div className={classNames(styles.thumbnail,
@@ -21,15 +24,44 @@ export function Thumbnail({imageFile, aspectRatio, cropPosition, fit, load, show
2124
<Image imageFile={imageFile}
2225
load={load}
2326
preferSvg={true}
24-
variant={((aspectRatio && aspectRatio !== 'wide') ||
25-
cropPosition ||
26-
fit === 'contain') ? 'medium' : 'linkThumbnailLarge'}
27+
variant={variant}
28+
sizes={sizes}
2729
fit={fit} />
2830
{children}
2931
</div>
3032
);
3133
}
3234

35+
function variantAndSizes({aspectRatio, cropPosition, fit, linkWidth}) {
36+
const needsUncropped = (aspectRatio && aspectRatio !== 'wide') ||
37+
cropPosition ||
38+
fit === 'contain';
39+
const bucket = linkWidthBucket(linkWidth);
40+
41+
if (!features.isEnabled('image_srcset') || bucket === 'small') {
42+
return {variant: needsUncropped ? 'medium' : 'linkThumbnailLarge'};
43+
}
44+
45+
if (bucket === 'medium') {
46+
return {
47+
variant: ['medium', 'large'],
48+
sizes: '(min-width: 950px) 50vw, 100vw'
49+
};
50+
}
51+
52+
return {variant: ['medium', 'large', 'ultra']};
53+
}
54+
55+
function linkWidthBucket(linkWidth) {
56+
if (linkWidth === 'xl' || linkWidth === 'xxl') {
57+
return 'large';
58+
}
59+
if (linkWidth === 'm' || linkWidth === 'l') {
60+
return 'medium';
61+
}
62+
return 'small';
63+
}
64+
3365
function getAspectRatioPadding(aspectRatio, imageFile) {
3466
if (aspectRatio === 'original' && imageFile) {
3567
return `${imageFile.height / imageFile.width * 100}%`;

0 commit comments

Comments
 (0)