Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new 'raw' image layout #34339

Merged
merged 62 commits into from Mar 14, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
889a3c8
Update framework chunk test regex to not select nested dependencies
atcastle Oct 9, 2019
ebafcca
Update webpack-config.ts
Timer Oct 14, 2019
7ab3c8a
Merge branch 'canary' into canary
Timer Oct 14, 2019
2c62327
Add support for raw layout mode
atcastle Feb 14, 2022
4c769d3
Update image documentation for raw layout
atcastle Feb 14, 2022
5843826
documentation autoformatting
atcastle Feb 14, 2022
4252ffb
change typescript text fixture
atcastle Feb 14, 2022
c4fa96b
Merge remote-tracking branch 'origin' into image-layout-raw
atcastle Feb 16, 2022
4006b36
Refactor base image element in image component. Remove raw layout sup…
atcastle Feb 17, 2022
efad4b8
lint fixes
atcastle Feb 17, 2022
26a6103
lint fixes
atcastle Feb 17, 2022
f32440d
Fix typescript test
atcastle Feb 17, 2022
78904f2
fix tests
atcastle Feb 17, 2022
c457914
lint fix
atcastle Feb 17, 2022
bdd8836
Merge remote-tracking branch 'origin' into image-layout-raw
atcastle Feb 17, 2022
e812837
Allow all layouts to use style prop
atcastle Feb 18, 2022
90c778d
Move ImageElement component outside of Image
atcastle Feb 18, 2022
34411f2
Fix handling of noscript image attributes
atcastle Feb 18, 2022
64a0b70
lint fix
atcastle Feb 19, 2022
435091e
Merge remote-tracking branch 'origin' into image-layout-raw
atcastle Feb 19, 2022
2be4e4e
Merge remote-tracking branch 'origin' into image-layout-raw
atcastle Feb 19, 2022
6c9a9e4
Update docs to show style is now allowed on image componenet
atcastle Feb 19, 2022
d9f51f1
Additional test coverage for image style prop and raw
atcastle Feb 20, 2022
ebf5812
auto-lint
atcastle Feb 22, 2022
80ffd7e
Add support for priority to raw image and test it
atcastle Feb 22, 2022
b4fdc15
image.md wording tweak
atcastle Feb 22, 2022
24cf3dc
Test coverage for srcset behavior in raw layout
atcastle Feb 22, 2022
8c12019
Remove test focus
atcastle Feb 22, 2022
74ac553
Update docs/api-reference/next/image.md
atcastle Feb 22, 2022
8904782
Remove raw from ImageProps type and delete typescript-style test suite
atcastle Feb 23, 2022
2f91512
auto-lint
atcastle Feb 23, 2022
cac28f0
Documentation updates and move sizes overwrite warning
atcastle Feb 23, 2022
3be7243
auto-lint
atcastle Mar 9, 2022
9c5b9b8
Update packages/next/client/image.tsx
atcastle Mar 9, 2022
adb3a57
Update docs/api-reference/next/image.md
atcastle Mar 9, 2022
dd21513
Merge branch 'image-layout-raw' of github.com:atcastle/next.js into i…
atcastle Mar 9, 2022
6baa8be
auto-lint
atcastle Mar 9, 2022
19ffddc
Merge remote-tracking branch 'upstream/canary' into image-layout-raw
atcastle Mar 9, 2022
e03c65b
Add experimental flag for raw layout mode
atcastle Mar 9, 2022
20955eb
Fix image style test
atcastle Mar 9, 2022
b64bda7
Add aspect-ratio for raw images and only add height/width without sizes
atcastle Mar 9, 2022
78b9434
Update packages/next/client/image.tsx
atcastle Mar 9, 2022
7338c3a
Update docs/api-reference/next/image.md
atcastle Mar 9, 2022
bc42437
update docs for experimental raw layout
atcastle Mar 9, 2022
5eea550
Fix markdown links
atcastle Mar 9, 2022
d3d82b6
Documentation wording change
atcastle Mar 9, 2022
57edfe0
Merge branch 'canary' of github.com:atcastle/next.js into canary
atcastle Mar 10, 2022
d4e19f2
Merge branch 'canary' into image-layout-raw
atcastle Mar 10, 2022
a3aae1b
Merge branch 'canary' into image-layout-raw
atcastle Mar 10, 2022
2e6e84d
Merge branch 'image-layout-raw' of github.com:atcastle/next.js into i…
atcastle Mar 10, 2022
7525553
Fix type from merge
atcastle Mar 10, 2022
9f41f20
Update packages/next/client/image.tsx
atcastle Mar 10, 2022
c4654fa
Update docs/api-reference/next/image.md
atcastle Mar 10, 2022
820b152
Update packages/next/client/image.tsx
atcastle Mar 10, 2022
8563475
change to isLazy image element prop
atcastle Mar 10, 2022
74076ff
Merge branch 'image-layout-raw' of github.com:atcastle/next.js into i…
atcastle Mar 10, 2022
a000df1
clean up image element props
atcastle Mar 10, 2022
d48b36a
Use optional chaining for experimeental image opts
atcastle Mar 10, 2022
531b623
Add placeholder to imgElementArgs
atcastle Mar 10, 2022
d22691d
Only throw experimentalRaw error in non-prod
atcastle Mar 10, 2022
b5b1375
Apply suggestions from code review
styfle Mar 14, 2022
6a5ffac
Merge branch 'canary' into image-layout-raw
styfle Mar 14, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 9 additions & 2 deletions docs/api-reference/next/image.md
Expand Up @@ -16,6 +16,7 @@ description: Enable Image Optimization with the built-in Image component.

| Version | Changes |
| --------- | ------------------------------------------------------------------------------------------------- |
| `v12.0.11`| `raw` layout added |
| `v12.0.9` | `lazyRoot` prop added |
| `v12.0.0` | `formats` configuration added.<br/>AVIF support added.<br/>Wrapper `<div>` changed to `<span>`. |
| `v11.1.0` | `onLoadingComplete` and `lazyBoundary` props added. |
Expand Down Expand Up @@ -70,6 +71,8 @@ The layout behavior of the image as the viewport changes size.
| `fixed` | Sized to `width` and `height` exactly | `1x`, `2x` (based on [imageSizes](#image-sizes)) | N/A |
| `responsive` | Scale to fit width of container | `640w`, `750w`, ... `2048w`, `3840w` (based on [imageSizes](#image-sizes) and [deviceSizes](#device-sizes)) | `100vw` |
| `fill` | Grow in both X and Y axes to fill container | `640w`, `750w`, ... `2048w`, `3840w` (based on [imageSizes](#image-sizes) and [deviceSizes](#device-sizes)) | `100vw` |
| `raw` | Insert the image element with no responsive behavior | `640w`, `750w`, ... `2048w`, `3840w` (based on [imageSizes](#image-sizes) and [deviceSizes](#device-sizes)) | `100vw` |
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Insert the image element with no responsive behavior is misleading here. Point in case, the very next column describe how it auto-adjust its source according to responsive space available.

I think it would be better to say Insert the image element without any wrappers or without NextJs layouts applied .

The "responsive" aspect you're referring to is more about NextJs layouts than what people are expecting "responsive" to mean in an image context. That's why I would leave that out of the description so nobody confuses NextJs responsive layouts with responsive images, which are 2 totally different things.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this to "no automatic responsive behavior" which I think is accurate without implying that it cannot be made responsive. This column of the chart is describing behavior, rather than implementation, so I wouldn't be inclined to mention wrappers (or the lack thereof) at this point. Wrappers and styling are mentioned in the raw mode description just below this chart.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think it's a bit confusing.

Even with raw layout, I will still have an srcset and the browser will still choose the right version to download from that information. To me, I feel this is an automatic responsive behavior from the browser.

It might just be me, but I wouldn't underestimate what responsive can mean in different context.

With that said, I would tend to go more with something that mention NextJs layouts. Maybe something like Insert the image element with no automatic layout



- [Demo the `intrinsic` layout (default)](https://image-component.nextjs.gallery/layout-intrinsic)
- When `intrinsic`, the image will scale the dimensions down for smaller viewports, but maintain the original dimensions for larger viewports.
Expand All @@ -82,6 +85,10 @@ The layout behavior of the image as the viewport changes size.
- When `fill`, the image will stretch both width and height to the dimensions of the parent element, provided the parent element is relative.
- This is usually paired with the [`objectFit`](#objectFit) property.
- Ensure the parent element has `position: relative` in their stylesheet.
- When `raw`, the image will be rendered as a single image element with no wrappers, sizers or other responsive behavior.
- Unlike other layout modes, a `raw` image will pass through the `style` property to the underlying image.
atcastle marked this conversation as resolved.
Show resolved Hide resolved
- If your image styling will change the size of a `raw` image, you should include the `sizes` property for proper image serving.
- The other layout modes are optimized for performance and should cover nearly all use cases. It is recommended to try to use those modes before using `raw`.
- [Demo background image](https://image-component.nextjs.gallery/background)

### loader
Expand Down Expand Up @@ -120,7 +127,7 @@ const MyImage = (props) => {

A string that provides information about how wide the image will be at different breakpoints. Defaults to `100vw` (the full width of the screen) when using `layout="responsive"` or `layout="fill"`.

If you are using `layout="fill"` or `layout="responsive"`, it's important to assign `sizes` for any image that takes up less than the full viewport width.
If you are using `layout="fill"`, `layout="responsive"`, or `layout="raw"` it's important to assign `sizes` for any image that takes up less than the full viewport width.

For example, when the parent element will constrain the image to always be less than half the viewport width, use `sizes="50vw"`. Without `sizes`, the image will be sent at twice the necessary resolution, decreasing performance.

Expand Down Expand Up @@ -284,7 +291,7 @@ size, or format. Defaults to `false`.
Other properties on the `<Image />` component will be passed to the underlying
`img` element with the exception of the following:

- `style`. Use `className` instead.
- `style`. Only allowed on `layout="raw"` images. For others, use `className` instead.
- `srcSet`. Use
[Device Sizes](#device-sizes)
instead.
Expand Down
5 changes: 4 additions & 1 deletion docs/basic-features/image-optimization.md
Expand Up @@ -181,7 +181,8 @@ The image component has several different [layout modes](/docs/api-reference/nex

**Target the image with className, not based on DOM structure**

Regardless of the layout mode used, the Image component will have a consistent DOM structure of one `<img>` tag wrapped by exactly one `<span>`. For some modes, it may also have a sibling `<span>` for spacing. These additional `<span>` elements are critical to allow the component to prevent layout shifts.
For all of the standard layout modes, the Image component will have a consistent DOM structure of one `<img>` tag wrapped by exactly one `<span>`. For some modes, it may also have a sibling `<span>` for spacing. These additional `<span>` elements are critical to allow the component to prevent layout shifts.
atcastle marked this conversation as resolved.
Show resolved Hide resolved


The recommended way to style the inner `<img>` is to set the `className` prop on the Image component to the value of an imported [CSS Module](/docs/basic-features/built-in-css-support.md#adding-component-level-css). The value of `className` will be automatically applied to the underlying `<img>` element.

Expand All @@ -191,6 +192,8 @@ You cannot use [styled-jsx](/docs/basic-features/built-in-css-support.md#css-in-

You cannot use the `style` prop because the `<Image>` component does not pass it through to the underlying `<img>`.

> An additional `raw` layout mode is provided which removes the wrapper element and allows the `style` prop. This mode still requires `height` and `width` and is recommended only for advanced use cases that aren't covered by the primary layout modes.
atcastle marked this conversation as resolved.
Show resolved Hide resolved

**When using `layout='fill'`, the parent element must have `position: relative`**

This is necessary for the proper rendering of the image element in that layout mode.
Expand Down
87 changes: 80 additions & 7 deletions packages/next/client/image.tsx
Expand Up @@ -48,6 +48,7 @@ const VALID_LAYOUT_VALUES = [
'fixed',
'intrinsic',
'responsive',
'raw',
undefined,
] as const
type LayoutValue = typeof VALID_LAYOUT_VALUES[number]
Expand Down Expand Up @@ -107,15 +108,26 @@ export type ImageProps = Omit<
objectFit?: ImgElementStyle['objectFit']
objectPosition?: ImgElementStyle['objectPosition']
onLoadingComplete?: OnLoadingComplete
}
} & (
| {
layout?: Omit<LayoutValue, 'raw'>
}
| {
layout: 'raw'
style: ImgElementStyle
}
)

function getWidths(
{ deviceSizes, allSizes }: ImageConfig,
width: number | undefined,
layout: LayoutValue,
sizes: string | undefined
): { widths: number[]; kind: 'w' | 'x' } {
if (sizes && (layout === 'fill' || layout === 'responsive')) {
if (
atcastle marked this conversation as resolved.
Show resolved Hide resolved
sizes &&
(layout === 'fill' || layout === 'responsive' || layout === 'raw')
) {
// Find all the "vw" percent sizes used in the sizes prop
const viewportWidthRe = /(^|\s)(1?\d?\d)vw/g
const percentSizes = []
Expand All @@ -134,7 +146,8 @@ function getWidths(
if (
typeof width !== 'number' ||
layout === 'fill' ||
layout === 'responsive'
layout === 'responsive' ||
layout === 'raw'
) {
return { widths: deviceSizes, kind: 'w' }
}
Expand Down Expand Up @@ -425,9 +438,14 @@ export default function Image({
`Image with src "${src}" has both "priority" and "loading='lazy'" properties. Only one should be used.`
)
}
if (sizes && layout !== 'fill' && layout !== 'responsive') {
if (
sizes &&
layout !== 'fill' &&
layout !== 'responsive' &&
layout !== 'raw'
) {
console.warn(
`Image with src "${src}" has "sizes" property but it will be ignored. Only use "sizes" with "layout='fill'" or "layout='responsive'".`
`Image with src "${src}" has "sizes" property but it will be ignored. Only use "sizes" with 'fill,' 'responsive,' or 'raw' layout modes.`
atcastle marked this conversation as resolved.
Show resolved Hide resolved
)
}
if (placeholder === 'blur') {
Expand Down Expand Up @@ -456,7 +474,7 @@ export default function Image({
`Image with src "${src}" is using unsupported "ref" property. Consider using the "onLoadingComplete" property instead.`
)
}
if ('style' in rest) {
if ('style' in rest && layout !== 'raw') {
console.warn(
`Image with src "${src}" is using unsupported "style" property. Please use the "className" property instead.`
)
Expand Down Expand Up @@ -638,6 +656,22 @@ export default function Image({
})
}

let isRaw = false
const rawStyle: ImgElementStyle = {}

if (layout === 'raw') {
isRaw = true
const styleProp = 'style' in rest && rest.style ? rest.style : {}
Object.assign(
rawStyle,
{
...(objectFit ? { objectFit } : {}),
styfle marked this conversation as resolved.
Show resolved Hide resolved
...(objectPosition ? { objectPosition } : {}),
},
styleProp
)
}

let srcString: string = src

if (process.env.NODE_ENV !== 'production') {
Expand Down Expand Up @@ -680,7 +714,46 @@ export default function Image({
handleLoading(imgRef, srcString, layout, placeholder, onLoadingCompleteRef)
}, [srcString, layout, placeholder, isVisible])

return (
return isRaw ? (
atcastle marked this conversation as resolved.
Show resolved Hide resolved
<>
atcastle marked this conversation as resolved.
Show resolved Hide resolved
<img
{...rest}
{...imgAttributes}
decoding="async"
data-nimg={layout}
height={heightInt}
width={widthInt}
className={className}
ref={imgRef}
style={{ ...blurStyle, ...rawStyle }}
/>
{isLazy && (
<noscript>
<img
{...rest}
{...generateImgAttrs({
config,
src,
unoptimized,
layout,
width: widthInt,
quality: qualityInt,
sizes,
loader,
})}
decoding="async"
data-nimg={layout}
height={heightInt}
width={widthInt}
style={rawStyle}
className={className}
// @ts-ignore - TODO: upgrade to `@types/react@17`
loading={loading || 'lazy'}
/>
</noscript>
)}
</>
) : (
<span style={wrapperStyle}>
{hasSizer ? (
<span style={sizerStyle}>
Expand Down
48 changes: 48 additions & 0 deletions test/integration/image-component/default/pages/layout-raw.js
@@ -0,0 +1,48 @@
import React from 'react'
import Image from 'next/image'

const Page = () => {
return (
<div>
<p>Layout Raw</p>
<div id="image-container1">
<Image
id="raw1"
src="/wide.png"
width="1200"
height="700"
layout="raw"
objectFit="cover"
loading="eager"
></Image>
</div>
<div id="image-container2">
<Image
id="raw2"
src="/wide.png"
width="1200"
height="700"
objectFit="cover"
objectPosition="50% 50%"
style={{
paddingLeft: '4rem',
width: '100%',
objectPosition: '30% 30%',
}}
layout="raw"
></Image>
</div>
<div id="image-container3">
<Image
id="raw3"
src="/test.png"
width="400"
height="400"
layout="raw"
></Image>
</div>
</div>
)
}

export default Page
Expand Up @@ -84,6 +84,16 @@ const Page = () => {
idToCount={idToCount}
setIdToCount={setIdToCount}
/>

<ImageWithMessage
id="9"
src={require('../public/test.png')}
placeholder="blur"
layout="raw"
idToCount={idToCount}
setIdToCount={setIdToCount}
/>

<button id="toggle" onClick={() => setClicked(!clicked)}>
Toggle
</button>
Expand Down
38 changes: 38 additions & 0 deletions test/integration/image-component/default/test/index.test.js
Expand Up @@ -273,6 +273,10 @@ function runTests(mode) {
() => browser.eval(`document.getElementById("img8").currentSrc`),
/test-rect.jpg/
)
await check(
() => browser.eval(`document.getElementById("msg9").textContent`),
'loaded 1 img9 with dimensions 266x266'
)
} finally {
if (browser) {
await browser.close()
Expand Down Expand Up @@ -584,6 +588,40 @@ function runTests(mode) {
}
})

it('should render no wrappers or sizers and minimal styling with layout-raw', async () => {
let browser
try {
browser = await webdriver(appPort, '/layout-raw')

const numberOfChildren = await browser.eval(
`document.getElementById('image-container1').children.length`
)
expect(numberOfChildren).toBe(1)
const childElementType = await browser.eval(
`document.getElementById('image-container1').children[0].nodeName`
)
expect(childElementType).toBe('IMG')

expect(await browser.elementById('raw1').getAttribute('style')).toBe(
'object-fit:cover'
)

expect(await browser.elementById('raw2').getAttribute('style')).toBe(
'object-fit:cover;object-position:30% 30%;padding-left:4rem;width:100%'
)

expect(await browser.elementById('raw3').getAttribute('style')).toBeNull()
atcastle marked this conversation as resolved.
Show resolved Hide resolved

const warnings = (await browser.log('browser'))
.map((log) => log.message)
.join('\n')
} finally {
if (browser) {
await browser.close()
}
}
})

if (mode === 'dev') {
it('should show missing src error', async () => {
const browser = await webdriver(appPort, '/missing-src')
Expand Down