Skip to content

Commit 892f7ca

Browse files
authored
Merge pull request #2367 from tf/srcset
Add responsive image srcset support
2 parents d6a8ec7 + db32f5d commit 892f7ca

File tree

11 files changed

+554
-38
lines changed

11 files changed

+554
-38
lines changed

entry_types/scrolled/config/locales/de.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ de:
22
pageflow:
33
datawrapper_chart_embed_opt_in:
44
feature_name: Opt-in für Datawrapper Embeds
5+
image_srcset:
6+
feature_name: Responsive Bild-Srcset
57
defaultNavigation:
68
widget_type_name: Standard Navigation
79
editor:

entry_types/scrolled/config/locales/en.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ en:
22
pageflow:
33
datawrapper_chart_embed_opt_in:
44
feature_name: Opt-in for Datawrapper embeds
5+
image_srcset:
6+
feature_name: Responsive image srcset
57
defaultNavigation:
68
widget_type_name: Default navigation
79
editor:

entry_types/scrolled/lib/pageflow_scrolled/plugin.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ def configure(config)
196196
c.features.register('custom_palette_colors')
197197
c.features.register('decoration_effects')
198198
c.features.register('backdrop_size')
199+
c.features.register('image_srcset')
200+
c.features.enable_by_default('image_srcset')
199201

200202
c.features.register('faq_page_structured_data') do |feature_config|
201203
feature_config.entry_structured_data_types.register(

entry_types/scrolled/package/spec/contentElements/inlineImage/InlineImage-spec.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
import React from 'react';
12
import 'contentElements/inlineImage/frontend';
23
import {renderContentElement, usePageObjects} from 'support/pageObjects';
4+
import {renderInContentElement} from 'pageflow-scrolled/testHelpers';
35
import '@testing-library/jest-dom/extend-expect'
46

7+
import {InlineImage} from 'contentElements/inlineImage/InlineImage';
8+
import {features} from 'pageflow/frontend';
59
import {usePortraitOrientation} from 'frontend/usePortraitOrientation';
610
jest.mock('frontend/usePortraitOrientation');
711

@@ -190,6 +194,118 @@ describe('InlineImage', () => {
190194
});
191195
});
192196

197+
describe('srcset', () => {
198+
beforeEach(() => features.enable('frontend', ['image_srcset']));
199+
afterEach(() => features.enabledFeatureNames = []);
200+
201+
function renderInlineImage({contentElementWidth = 0, ...seedOptions} = {}) {
202+
const result = renderInContentElement(
203+
<InlineImage contentElementId={42}
204+
contentElementWidth={contentElementWidth}
205+
configuration={{id: 100}} />,
206+
{seed: seedOptions}
207+
);
208+
result.simulateScrollPosition('near viewport');
209+
return result;
210+
}
211+
212+
it('uses medium and large srcset for default width', () => {
213+
const {getByRole} = renderInlineImage({
214+
imageFileUrlTemplates: {
215+
medium: ':id_partition/medium/image.jpg',
216+
large: ':id_partition/large/image.jpg'
217+
},
218+
imageFiles: [{permaId: 100, id: 1, width: 4000, height: 3000}]
219+
});
220+
221+
expect(getByRole('img')).toHaveAttribute('srcset',
222+
'000/000/001/medium/image.jpg 1024w, 000/000/001/large/image.jpg 1920w');
223+
expect(getByRole('img')).toHaveAttribute('sizes',
224+
'(min-width: 950px) 950px, 100vw');
225+
});
226+
227+
it('uses medium, large and ultra srcset for full width', () => {
228+
const {getByRole} = renderInlineImage({
229+
contentElementWidth: 3,
230+
imageFileUrlTemplates: {
231+
medium: ':id_partition/medium/image.jpg',
232+
large: ':id_partition/large/image.jpg',
233+
ultra: ':id_partition/ultra/image.jpg'
234+
},
235+
imageFiles: [{permaId: 100, id: 1, width: 4000, height: 3000}]
236+
});
237+
238+
expect(getByRole('img')).toHaveAttribute('srcset',
239+
'000/000/001/medium/image.jpg 1024w, ' +
240+
'000/000/001/large/image.jpg 1920w, ' +
241+
'000/000/001/ultra/image.jpg 3840w');
242+
expect(getByRole('img')).toHaveAttribute('sizes', '100vw');
243+
});
244+
245+
it('uses medium, large and ultra srcset with 1200px sizes for xl width', () => {
246+
const {getByRole} = renderInlineImage({
247+
contentElementWidth: 2,
248+
imageFileUrlTemplates: {
249+
medium: ':id_partition/medium/image.jpg',
250+
large: ':id_partition/large/image.jpg',
251+
ultra: ':id_partition/ultra/image.jpg'
252+
},
253+
imageFiles: [{permaId: 100, id: 1, width: 4000, height: 3000}]
254+
});
255+
256+
expect(getByRole('img')).toHaveAttribute('srcset',
257+
'000/000/001/medium/image.jpg 1024w, ' +
258+
'000/000/001/large/image.jpg 1920w, ' +
259+
'000/000/001/ultra/image.jpg 3840w');
260+
expect(getByRole('img')).toHaveAttribute('sizes',
261+
'(min-width: 950px) 1200px, 100vw');
262+
});
263+
264+
it('uses computed width descriptors for portrait images', () => {
265+
const {getByRole} = renderInlineImage({
266+
imageFileUrlTemplates: {
267+
medium: ':id_partition/medium/image.jpg',
268+
large: ':id_partition/large/image.jpg'
269+
},
270+
imageFiles: [{permaId: 100, id: 1, width: 2160, height: 3840}]
271+
});
272+
273+
expect(getByRole('img')).toHaveAttribute('srcset',
274+
'000/000/001/medium/image.jpg 576w, 000/000/001/large/image.jpg 1080w');
275+
});
276+
277+
it('uses plain medium variant for small widths', () => {
278+
const {getByRole} = renderInlineImage({
279+
contentElementWidth: -1,
280+
imageFileUrlTemplates: {
281+
medium: ':id_partition/medium/image.jpg',
282+
large: ':id_partition/large/image.jpg'
283+
},
284+
imageFiles: [{permaId: 100, id: 1, width: 200, height: 100}]
285+
});
286+
287+
expect(getByRole('img')).not.toHaveAttribute('srcset');
288+
expect(getByRole('img')).toHaveAttribute('src',
289+
'000/000/001/medium/image.jpg');
290+
});
291+
292+
it('falls back to original behavior when feature is disabled', () => {
293+
features.enabledFeatureNames = [];
294+
295+
const {getByRole} = renderInlineImage({
296+
imageFileUrlTemplates: {
297+
medium: ':id_partition/medium/image.jpg',
298+
large: ':id_partition/large/image.jpg'
299+
},
300+
imageFiles: [{permaId: 100, id: 1, width: 200, height: 100}]
301+
});
302+
303+
expect(getByRole('img')).not.toHaveAttribute('srcset');
304+
expect(getByRole('img')).toHaveAttribute('src',
305+
'000/000/001/medium/image.jpg');
306+
});
307+
});
308+
193309
describe('basic functionality', () => {
194310
it('renders with FitViewport and ContentElementBox', () => {
195311
const {getContentElement} = renderContentElement({

entry_types/scrolled/package/spec/entryState/useFile-spec.js

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,140 @@ describe('useFile', () => {
211211
});
212212
});
213213

214+
it('includes variantWidths for image files', () => {
215+
const {result} = renderHookInEntry(
216+
() => useFile({collectionName: 'imageFiles', permaId: 1}),
217+
{
218+
seed: {
219+
fileUrlTemplates: {
220+
imageFiles: {
221+
medium: '/image_files/:id_partition/medium/:basename.:processed_extension',
222+
large: '/image_files/:id_partition/large/:basename.:processed_extension',
223+
}
224+
},
225+
fileModelTypes: {
226+
imageFiles: 'Pageflow::ImageFile'
227+
},
228+
imageFiles: [
229+
{
230+
id: 100,
231+
permaId: 1,
232+
basename: 'image',
233+
extension: 'jpg',
234+
processedExtension: 'webp',
235+
width: 4000,
236+
height: 3000
237+
}
238+
]
239+
}
240+
}
241+
);
242+
243+
expect(result.current.variantWidths).toEqual([
244+
['1024w', 'medium'],
245+
['1920w', 'large']
246+
]);
247+
});
248+
249+
it('computes variantWidths based on actual image dimensions for portrait images', () => {
250+
const {result} = renderHookInEntry(
251+
() => useFile({collectionName: 'imageFiles', permaId: 1}),
252+
{
253+
seed: {
254+
fileUrlTemplates: {
255+
imageFiles: {
256+
medium: '/image_files/:id_partition/medium/:basename.:processed_extension',
257+
large: '/image_files/:id_partition/large/:basename.:processed_extension',
258+
}
259+
},
260+
fileModelTypes: {
261+
imageFiles: 'Pageflow::ImageFile'
262+
},
263+
imageFiles: [
264+
{
265+
id: 100,
266+
permaId: 1,
267+
basename: 'image',
268+
extension: 'jpg',
269+
processedExtension: 'webp',
270+
width: 2160,
271+
height: 3840
272+
}
273+
]
274+
}
275+
}
276+
);
277+
278+
expect(result.current.variantWidths).toEqual([
279+
['576w', 'medium'],
280+
['1080w', 'large']
281+
]);
282+
});
283+
284+
it('deduplicates variantWidths when variants produce same width', () => {
285+
const {result} = renderHookInEntry(
286+
() => useFile({collectionName: 'imageFiles', permaId: 1}),
287+
{
288+
seed: {
289+
fileUrlTemplates: {
290+
imageFiles: {
291+
medium: '/image_files/:id_partition/medium/:basename.:processed_extension',
292+
large: '/image_files/:id_partition/large/:basename.:processed_extension',
293+
ultra: '/image_files/:id_partition/ultra/:basename.:processed_extension',
294+
}
295+
},
296+
fileModelTypes: {
297+
imageFiles: 'Pageflow::ImageFile'
298+
},
299+
imageFiles: [
300+
{
301+
id: 100,
302+
permaId: 1,
303+
basename: 'image',
304+
extension: 'jpg',
305+
processedExtension: 'webp',
306+
width: 1920,
307+
height: 1080
308+
}
309+
]
310+
}
311+
}
312+
);
313+
314+
expect(result.current.variantWidths).toEqual([
315+
['1024w', 'medium'],
316+
['1920w', 'large']
317+
]);
318+
});
319+
320+
it('does not include variantWidths for video files', () => {
321+
const {result} = renderHookInEntry(
322+
() => useFile({collectionName: 'videoFiles', permaId: 1}),
323+
{
324+
seed: {
325+
fileUrlTemplates: {
326+
videoFiles: {
327+
high: '/video_files/:id_partition/high.mp4',
328+
},
329+
},
330+
fileModelTypes: {
331+
videoFiles: 'Pageflow::VideoFile'
332+
},
333+
videoFiles: [
334+
{
335+
id: 100,
336+
permaId: 1,
337+
basename: 'video',
338+
variants: ['high'],
339+
}
340+
]
341+
}
342+
}
343+
);
344+
345+
expect(result.current.variantWidths).toBeUndefined();
346+
});
347+
214348
it('falls back to file name for display name from watched collection', () => {
215349
const {result} = renderHookInEntry(
216350
() => useFile({collectionName: 'imageFiles', permaId: 1}),

0 commit comments

Comments
 (0)