From c8ef392e2f54c36fdd69d70d33bbbdcbe2cb961c Mon Sep 17 00:00:00 2001 From: Bart Nagel Date: Wed, 21 Dec 2022 17:10:25 -0800 Subject: [PATCH] feat: allow width and height to be passed through htmlAttributes [It's best practice to specify width and height on images][1], even if they are fluid, to help avoid layout shift. It tells the browser the expected size before it loads any image data, and an aspect ratio is derived from it so the browser can still know the image size even if CSS or something else is altering the image's width from its intrinsic one. Before this patch, there was not a way to allow width and height attributes to get through to the `img` element sent to the browser when using the react-imgix component while leaving the image fluid width (that is, with a srcset containing width descriptors). As soon as one specified width and height, react-imgix assumed the author wanted a fixed-width image, and it gave resolution descriptors in the srcset instead of width descriptors (and then the sizes attribute was ignored by the browser, [as per spec][2]). Imgix currently uses the width and height attributes to decide whether to use width or resolution descriptors, so to avoid altering that logic (which would be a breaking change), this patch allows width and height to be passed through the `htmlAttributes` prop. Prior to the patch they were swallowed if given in `htmlAttributes`. As of this patch they are allowed and passed through. This closes https://github.com/imgix/react-imgix/issues/891 In future it may make sense to decide srcset type based on the presence of the `sizes` attribute, as discussed in the above ticket. This would be a breaking change, however. As part of this patch, width and height are also allowed through when using this component in `Source` mode, since [width and height attributes are now allowed and encouraged in that context][3]. [1]: https://web.dev/optimize-cls/ [2]: https://html.spec.whatwg.org/multipage/images.html#srcset-attributes [3]: https://github.com/whatwg/html/pull/5894 --- README.md | 33 ++++++++++++++++++++++++++++--- src/react-imgix.jsx | 8 ++++---- test/unit/react-imgix.test.jsx | 36 ++++++++++++++++++++++++++++++---- 3 files changed, 66 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 2f470d63..f0ec6f4e 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ This will generate HTML similar to the following: Since imgix can generate as many derivative resolutions as needed, react-imgix calculates them programmatically, using the dimensions you specify. All of this information has been placed into the srcset and sizes attributes. -**Width and height known:** If the width and height are known beforehand, it is recommended that they are set explicitly: +**Width and height known and fixed:** If the width and height are known beforehand, and a fixed-size image is wanted, it is recommended that they are set explicitly: ```js import Imgix from "react-imgix"; @@ -127,17 +127,44 @@ import Imgix from "react-imgix"; [![Edit xp0348lv0z](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/charming-keller-kjnsq) +When width and height are specified, Imgix will give the image a srcset with resolution descriptors. + +**Width and height known but fluid:** If the image's intrinsic width and height are known but a fluid size image is wanted, [width and height should still be set to avoid layout shift](https://web.dev/optimize-cls/), but they must be set via `htmlAttributes` so as not to hint to Imgix to produce resolution descriptors in the srcset. + +```js +import Imgix from "react-imgix"; + + element + width: 200, + height: 100, + }} +/>; +``` + +In this example, Imgix will produce a srcset with width descriptors. + #### Server-Side Rendering React-imgix also works well on the server. Since react-imgix uses `srcset` and `sizes`, it allows the browser to render the correctly sized image immediately after the page has loaded. +If they are known, pass width and height attributes via `htmlAttributes` to help combat layout shift. ```js import Imgix from "react-imgix"; -; +; ``` -If the width and height are known beforehand, it is recommended that they are set explicitly: +If the width and height are known beforehand, and a fixed-size image is wanted, set width and height and do not set `sizes`: ```js import Imgix from "react-imgix"; diff --git a/src/react-imgix.jsx b/src/react-imgix.jsx index 2885b6c4..6c4557c9 100644 --- a/src/react-imgix.jsx +++ b/src/react-imgix.jsx @@ -247,8 +247,8 @@ class ReactImgix extends Component { const childProps = Object.assign({}, this.props.htmlAttributes, { [attributeConfig.sizes]: this.props.sizes, className: this.props.className, - width: width <= 1 ? null : width, - height: height <= 1 ? null : height, + width: width <= 1 ? null : width ?? this.props.htmlAttributes?.width, + height: height <= 1 ? null : height ?? this.props.htmlAttributes?.height, [attributeConfig.src]: src, ref: (el) => { this.imgRef = el; @@ -383,8 +383,8 @@ class SourceImpl extends Component { const childProps = Object.assign({}, this.props.htmlAttributes, { [attributeConfig.sizes]: this.props.sizes, className: this.props.className, - width: null, - height: null, + width: width <= 1 ? null : width ?? this.props.htmlAttributes?.width, + height: height <= 1 ? null : height ?? this.props.htmlAttributes?.height, ref: (el) => { this.sourceRef = el; if ( diff --git a/test/unit/react-imgix.test.jsx b/test/unit/react-imgix.test.jsx index b2e68e4a..b80874ab 100644 --- a/test/unit/react-imgix.test.jsx +++ b/test/unit/react-imgix.test.jsx @@ -232,6 +232,26 @@ describe("When in default mode", () => { }); }); }); + + describe("using the htmlAttributes prop", () => { + it("passes any attributes via htmlAttributes to the rendered element", () => { + const htmlAttributes = { + "data-src": "https://mysource.imgix.net/demo.png", + width: "200", + height: "100", + }; + sut = shallow( + + ); + expect(sut.props()["data-src"]).toEqual(htmlAttributes["data-src"]); + expect(sut.props()["width"]).toEqual(htmlAttributes["width"]); + expect(sut.props()["height"]).toEqual(htmlAttributes["height"]); + }); + }); }); describe("When in image mode", () => { @@ -359,10 +379,10 @@ describe("When in mode", () => { expect(srcSets[4].split(" ")[1]).toBe("5x"); }); - it("width and height should be nullable to pass HTML validation", () => { - const {width, height} = renderImage().props(); - expect(width).toBe(null); - expect(height).toBe(null); + it("width and height should be passed through to the img element", () => { + const { width, height } = renderImage().props(); + expect(width).toBe(100); + expect(height).toBe(100); }); }); @@ -417,6 +437,8 @@ describe("When in mode", () => { it("passes any attributes via htmlAttributes to the rendered element", () => { const htmlAttributes = { "data-src": "https://mysource.imgix.net/demo.png", + width: "200", + height: "100", }; sut = mount( mode", () => { expect(sut.props()["htmlAttributes"]["data-src"]).toEqual( htmlAttributes["data-src"] ); + expect(sut.props()["htmlAttributes"]["width"]).toEqual( + htmlAttributes["width"] + ); + expect(sut.props()["htmlAttributes"]["height"]).toEqual( + htmlAttributes["height"] + ); }); it("attaches a ref via htmlAttributes", () => {