Skip to content

Commit

Permalink
feat: allow width and height to be passed through htmlAttributes
Browse files Browse the repository at this point in the history
[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 #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]: whatwg/html#5894
  • Loading branch information
tremby authored and luqven committed Feb 2, 2023
1 parent 6604108 commit c8ef392
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 11 deletions.
33 changes: 30 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";

<Imgix
src="https://assets.imgix.net/examples/pione.jpg"
sizes="(min-width: 1024px) 40vw, 90vw"
htmlAttributes={{ // These are ignored by Imgix but passed through to the <img> 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";

<Imgix src="https://assets.imgix.net/examples/pione.jpg" sizes="100vw" />;
<Imgix
src="https://assets.imgix.net/examples/pione.jpg"
sizes="100vw"
htmlAttributes={{
width: 400,
height: 250,
}}
/>;
```

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";
Expand Down
8 changes: 4 additions & 4 deletions src/react-imgix.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 (
Expand Down
36 changes: 32 additions & 4 deletions test/unit/react-imgix.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Imgix
src={"https://mysource.imgix.net/demo.png"}
sizes="100vw"
htmlAttributes={htmlAttributes}
/>
);
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", () => {
Expand Down Expand Up @@ -359,10 +379,10 @@ describe("When in <source> 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);
});
});

Expand Down Expand Up @@ -417,6 +437,8 @@ describe("When in <source> 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(
<Source
Expand All @@ -429,6 +451,12 @@ describe("When in <source> 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", () => {
Expand Down

0 comments on commit c8ef392

Please sign in to comment.