From 2b871261ac2801da0d4e7d39e60b2bf3fe5fc779 Mon Sep 17 00:00:00 2001 From: Oleg Semyonov Date: Tue, 26 Dec 2023 18:05:57 +0400 Subject: [PATCH 1/3] Provide explicit `width` and `height` for images See https://web.dev/articles/optimize-cls#images-without-dimensions --- src/components/Media/Media.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/Media/Media.tsx b/src/components/Media/Media.tsx index 653004a..31e3d6a 100644 --- a/src/components/Media/Media.tsx +++ b/src/components/Media/Media.tsx @@ -53,6 +53,8 @@ export function Media({ className, image, style, title }: Props) { .filter(Boolean) .join(', ')} sizes={`(max-width: 992px) 800px, (max-width: 576px) 400px, 1200px`} + width={image.originalWidth} + height={image.originalHeight} style={style} title={title} /> From 6723e621804a484df336aeba8d65fa1e3f4c4152 Mon Sep 17 00:00:00 2001 From: Oleg Semyonov Date: Wed, 27 Dec 2023 15:13:05 +0400 Subject: [PATCH 2/3] Allow passing custom `sizes` string, auto-calculate srcsets based on the sizes string --- src/components/Media/Media.tsx | 16 +++++++++--- .../Media/lib/convertSizesToWidths.test.ts | 22 ++++++++++++++++ .../Media/lib/convertSizesToWidths.ts | 7 ++++++ src/components/Media/lib/index.ts | 1 + src/elements/Image/Image.tsx | 25 ++++++++++++++++--- 5 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 src/components/Media/lib/convertSizesToWidths.test.ts create mode 100644 src/components/Media/lib/convertSizesToWidths.ts create mode 100644 src/components/Media/lib/index.ts diff --git a/src/components/Media/Media.tsx b/src/components/Media/Media.tsx index 31e3d6a..14fba77 100644 --- a/src/components/Media/Media.tsx +++ b/src/components/Media/Media.tsx @@ -2,6 +2,8 @@ import type { UploadcareImage } from '@prezly/uploadcare'; import classNames from 'classnames'; import type { CSSProperties } from 'react'; +import { convertSizesToWidths } from './lib'; + import './Media.scss'; interface Props { @@ -9,9 +11,16 @@ interface Props { image: UploadcareImage; style?: CSSProperties; title?: string; + sizes?: string; } -export function Media({ className, image, style, title }: Props) { +export function Media({ + className, + image, + style, + title, + sizes = '(max-width: 992px) 800px, (max-width: 576px) 400px, 1200px', +}: Props) { const computedClassName = classNames('prezly-slate-media', className, { 'prezly-slate-media--image': !image.isGif(), 'prezly-slate-media--video': image.isGif(), @@ -49,10 +58,11 @@ export function Media({ className, image, style, title }: Props) { alt={title} className={computedClassName} src={image.format().cdnUrl} - srcSet={[image.srcSet(1200), image.srcSet(800), image.srcSet(400)] + srcSet={convertSizesToWidths(sizes) + .map((width) => image.srcSet(width)) .filter(Boolean) .join(', ')} - sizes={`(max-width: 992px) 800px, (max-width: 576px) 400px, 1200px`} + sizes={sizes} width={image.originalWidth} height={image.originalHeight} style={style} diff --git a/src/components/Media/lib/convertSizesToWidths.test.ts b/src/components/Media/lib/convertSizesToWidths.test.ts new file mode 100644 index 0000000..06760cf --- /dev/null +++ b/src/components/Media/lib/convertSizesToWidths.test.ts @@ -0,0 +1,22 @@ +import { convertSizesToWidths } from './convertSizesToWidths'; + +describe('convertSizesToWidths', () => { + it('Returns empty array when `sizes` does not contain valid size defintions', () => { + expect(convertSizesToWidths('')).toEqual([]); + expect(convertSizesToWidths('test string 123')).toEqual([]); + expect(convertSizesToWidths('test, 123')).toEqual([]); + }); + + it('Returns array of width numbers when `sizes` is a valid size definition', () => { + expect( + convertSizesToWidths('(max-width: 992px) 800px, (max-width: 576px) 400px, 1200px'), + ).toEqual([800, 400, 1200]); + expect( + convertSizesToWidths('(max-width: 992px) 800w, (max-width: 576px) 400w, 100vw'), + ).toEqual([800, 400]); + }); + + it('Returns array of width numbers when `sizes` has just one size definition', () => { + expect(convertSizesToWidths('1200px')).toEqual([1200]); + }); +}); diff --git a/src/components/Media/lib/convertSizesToWidths.ts b/src/components/Media/lib/convertSizesToWidths.ts new file mode 100644 index 0000000..34cff85 --- /dev/null +++ b/src/components/Media/lib/convertSizesToWidths.ts @@ -0,0 +1,7 @@ +const SIZE_DEFINITION_REGEX = /(?:\((?:min|max)-width: \d+px\) ?)?(\d+)(?:px|w)/g; + +export function convertSizesToWidths(sizesString: string): number[] { + const matches = Array.from(sizesString.matchAll(SIZE_DEFINITION_REGEX)); + + return matches.map((match) => parseInt(match[1], 10)); +} diff --git a/src/components/Media/lib/index.ts b/src/components/Media/lib/index.ts new file mode 100644 index 0000000..4070b8b --- /dev/null +++ b/src/components/Media/lib/index.ts @@ -0,0 +1 @@ +export { convertSizesToWidths } from './convertSizesToWidths'; diff --git a/src/elements/Image/Image.tsx b/src/elements/Image/Image.tsx index cfb8a3c..aeaa23a 100644 --- a/src/elements/Image/Image.tsx +++ b/src/elements/Image/Image.tsx @@ -13,6 +13,7 @@ import './Image.scss'; interface Props extends HTMLAttributes { node: ImageNode; + sizes?: string; onDownload?: (image: UploadcareImage) => void; onPreviewOpen?: (image: UploadcareImage) => void; } @@ -37,7 +38,15 @@ const NEW_TAB_ATTRIBUTES: Partial> = { rel: 'noopener noreferrer', }; -export function Image({ children, className, node, onDownload, onPreviewOpen, ...props }: Props) { +export function Image({ + children, + className, + node, + sizes, + onDownload, + onPreviewOpen, + ...props +}: Props) { const { file, href, align, layout } = node; const isNewTab = node.new_tab ?? true; @@ -75,7 +84,12 @@ export function Image({ children, className, node, onDownload, onPreviewOpen, .. {...(isNewTab ? NEW_TAB_ATTRIBUTES : {})} style={containerStyle} > - + )} @@ -87,7 +101,12 @@ export function Image({ children, className, node, onDownload, onPreviewOpen, .. onClick={handleRolloverClick} style={containerStyle} > - + )} From d60df00c8f7150f054b91136dcdb328984f96aae Mon Sep 17 00:00:00 2001 From: Oleg Semyonov Date: Wed, 27 Dec 2023 15:17:34 +0400 Subject: [PATCH 3/3] Manually install eslint-config-prettier --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index bab47e6..673737c 100644 --- a/package.json +++ b/package.json @@ -127,6 +127,7 @@ "concurrently": "^6.5.1", "css-loader": "^6.7.1", "eslint": "^8.39.0", + "eslint-config-prettier": "^8.10.0", "gulp": "^4.0.2", "gulp-babel": "^8.0.0", "gulp-concat": "^2.6.1",