Skip to content

Commit

Permalink
Add layout prop to Image component (vercel#18491)
Browse files Browse the repository at this point in the history
This PR introduces a new `layout` property.

This allows 3 possible values (`fixed`, `intrinsic`, or `responsive`) which solve many use cases we have seen since 10.0.0 and will hopefully avoid usage of `unsized`.

Fixes vercel#18351 

Co-authored-by: Joe Haddad <[email protected]>
  • Loading branch information
styfle and Timer authored Oct 30, 2020
1 parent 4263318 commit afa04d2
Show file tree
Hide file tree
Showing 9 changed files with 316 additions and 30 deletions.
1 change: 1 addition & 0 deletions docs/api-reference/next/image.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export default Home
- `src` - The path or URL to the source image. This is required.
- `width` - The intrinsic width of the source image in pixels. Must be an integer without a unit. Required unless `unsized` is true.
- `height` - The intrinsic height of the source image, in pixels. Must be an integer without a unit. Required unless `unsized` is true.
- `layout` - The rendered layout of the image. If `fixed`, the image dimensions will not change as the viewport changes (no responsiveness). If `intrinsic`, the image will scale the dimensions down for smaller viewports but maintain the original dimensions for larger viewports. If `responsive`, the image will scale the dimensions down for smaller viewports and scale up for larger viewports. Default `intrinsic`.
- `sizes` - Defines what proportion of the screen you expect the image to take up. Recommended, as it helps serve the correct sized image to each device. [More info](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-sizes).
- `quality` - The quality of the optimized image, an integer between 1 and 100 where 100 is the best quality. Default 75.
- `loading` - The loading behavior. When `lazy`, defer loading the image until it reaches a calculated distance from the viewport. When `eager`, load the image immediately. Default `lazy`. [More info](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-loading)
Expand Down
113 changes: 83 additions & 30 deletions packages/next/client/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ const loaders = new Map<LoaderKey, (props: LoaderProps) => string>([

type LoaderKey = 'imgix' | 'cloudinary' | 'akamai' | 'default'

const VALID_LAYOUT_VALUES = [
'fixed',
'intrinsic',
'responsive',
undefined,
] as const
type LayoutValue = typeof VALID_LAYOUT_VALUES[number]

type ImageData = {
deviceSizes: number[]
imageSizes: number[]
Expand All @@ -29,6 +37,7 @@ type ImageProps = Omit<
quality?: number | string
priority?: boolean
loading?: LoadingValue
layout?: LayoutValue
unoptimized?: boolean
} & (
| { width: number | string; height: number | string; unsized?: false }
Expand Down Expand Up @@ -201,6 +210,7 @@ export default function Image({
unoptimized = false,
priority = false,
loading,
layout,
className,
quality,
width,
Expand All @@ -218,6 +228,13 @@ export default function Image({
)}`
)
}
if (!VALID_LAYOUT_VALUES.includes(layout)) {
throw new Error(
`Image with src "${src}" has invalid "layout" property. Provided "${layout}" should be one of ${VALID_LAYOUT_VALUES.map(
String
).join(',')}.`
)
}
if (!VALID_LOADING_VALUES.includes(loading)) {
throw new Error(
`Image with src "${src}" has invalid "loading" property. Provided "${loading}" should be one of ${VALID_LOADING_VALUES.map(
Expand All @@ -232,6 +249,14 @@ export default function Image({
}
}

if (!layout) {
if (sizes) {
layout = 'responsive'
} else {
layout = 'intrinsic'
}
}

let lazy = loading === 'lazy'
if (!priority && typeof loading === 'undefined') {
lazy = true
Expand Down Expand Up @@ -265,25 +290,41 @@ export default function Image({
const heightInt = getInt(height)
const qualityInt = getInt(quality)

let divStyle: React.CSSProperties | undefined
let imgStyle: React.CSSProperties | undefined
let wrapperStyle: React.CSSProperties | undefined
let sizerStyle: React.CSSProperties | undefined
let sizerSvg: string | undefined
let imgStyle: React.CSSProperties | undefined
if (
typeof widthInt !== 'undefined' &&
typeof heightInt !== 'undefined' &&
!unsized
) {
// <Image src="i.png" width={100} height={100} />
// <Image src="i.png" width="100" height="100" />
const quotient = heightInt / widthInt
const ratio = isNaN(quotient) ? 1 : quotient * 100
wrapperStyle = {
maxWidth: '100%',
width: widthInt,
}
divStyle = {
position: 'relative',
paddingBottom: `${ratio}%`,
const paddingTop = isNaN(quotient) ? '100%' : `${quotient * 100}%`
if (layout === 'responsive') {
// <Image src="i.png" width="100" height="100" layout="responsive" />
wrapperStyle = { position: 'relative' }
sizerStyle = { paddingTop }
} else if (layout === 'intrinsic') {
// <Image src="i.png" width="100" height="100" layout="intrinsic" />
wrapperStyle = {
display: 'inline-block',
position: 'relative',
maxWidth: '100%',
}
sizerStyle = {
maxWidth: '100%',
}
sizerSvg = `<svg width="${widthInt}" height="${heightInt}" xmlns="http://www.w3.org/2000/svg" version="1.1"/>`
} else if (layout === 'fixed') {
// <Image src="i.png" width="100" height="100" layout="fixed" />
wrapperStyle = {
display: 'inline-block',
position: 'relative',
width: widthInt,
height: heightInt,
}
}
imgStyle = {
visibility: lazy ? 'hidden' : 'visible',
Expand Down Expand Up @@ -357,25 +398,37 @@ export default function Image({

return (
<div style={wrapperStyle}>
<div style={divStyle}>
{shouldPreload
? generatePreload({
src,
width: widthInt,
unoptimized,
sizes,
quality: qualityInt,
})
: ''}
<img
{...rest}
{...imgAttributes}
className={className}
sizes={sizes}
ref={thisEl}
style={imgStyle}
/>
</div>
{shouldPreload
? generatePreload({
src,
width: widthInt,
unoptimized,
sizes,
quality: qualityInt,
})
: null}
{sizerStyle ? (
<div style={sizerStyle}>
{sizerSvg ? (
<img
style={{ maxWidth: '100%', display: 'block' }}
alt=""
aria-hidden={true}
role="presentation"
src={`data:image/svg+xml;charset=utf-8,${sizerSvg}`}
/>
) : null}
</div>
) : null}
<img
{...rest}
{...imgAttributes}
decoding="async"
className={className}
sizes={sizes}
ref={thisEl}
style={imgStyle}
/>
</div>
)
}
Expand Down
41 changes: 41 additions & 0 deletions test/integration/image-component/default/pages/layout-fixed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react'
import Image from 'next/image'

const Page = () => {
return (
<div>
<p>Layout Fixed</p>
<Image
id="fixed1"
src="/wide.png"
width="1200"
height="700"
layout="fixed"
></Image>
<Image
id="fixed2"
src="/wide.png"
width="1200"
height="700"
layout="fixed"
></Image>
<Image
id="fixed3"
src="/wide.png"
width="1200"
height="700"
layout="fixed"
></Image>
<Image
id="fixed4"
src="/wide.png"
width="1200"
height="700"
layout="fixed"
></Image>
<p>Layout Fixed</p>
</div>
)
}

export default Page
41 changes: 41 additions & 0 deletions test/integration/image-component/default/pages/layout-intrinsic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react'
import Image from 'next/image'

const Page = () => {
return (
<div>
<p>Layout Intrinsic</p>
<Image
id="intrinsic1"
src="/wide.png"
width="1200"
height="700"
layout="intrinsic"
></Image>
<Image
id="intrinsic2"
src="/wide.png"
width="1200"
height="700"
layout="intrinsic"
></Image>
<Image
id="intrinsic3"
src="/wide.png"
width="1200"
height="700"
layout="intrinsic"
></Image>
<Image
id="intrinsic4"
src="/wide.png"
width="1200"
height="700"
layout="intrinsic"
></Image>
<p>Layout Intrinsic</p>
</div>
)
}

export default Page
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react'
import Image from 'next/image'

const Page = () => {
return (
<div>
<p>Layout Responsive</p>
<Image
id="responsive1"
src="/wide.png"
width="1200"
height="700"
layout="responsive"
></Image>
<Image
id="responsive2"
src="/wide.png"
width="1200"
height="700"
layout="responsive"
></Image>
<Image
id="responsive3"
src="/wide.png"
width="1200"
height="700"
layout="responsive"
></Image>
<Image
id="responsive4"
src="/wide.png"
width="1200"
height="700"
layout="responsive"
></Image>
<p>Layout Responsive</p>
</div>
)
}

export default Page
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit afa04d2

Please sign in to comment.