Skip to content

Commit

Permalink
Add unsized property to Image component (#18059)
Browse files Browse the repository at this point in the history
Implements the `unsized` property from RFC #16832

cc @atcastle
  • Loading branch information
styfle committed Oct 21, 2020
1 parent 2ab3445 commit e8f402a
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 17 deletions.
64 changes: 48 additions & 16 deletions packages/next/client/image.tsx
Expand Up @@ -16,16 +16,17 @@ type ImageData = {

type ImageProps = Omit<
JSX.IntrinsicElements['img'],
'src' | 'srcSet' | 'ref'
'src' | 'srcSet' | 'ref' | 'width' | 'height'
> & {
src: string
width: number
height: number
quality?: string
priority?: boolean
lazy?: boolean
unoptimized?: boolean
}
} & (
| { width: number; height: number; unsized?: false }
| { width?: number; height?: number; unsized: true }
)

const imageData: ImageData = process.env.__NEXT_IMAGE_OPTS as any
const breakpoints = imageData.sizes || [640, 1024, 1600]
Expand Down Expand Up @@ -141,13 +142,14 @@ function generatePreload({
export default function Image({
src,
sizes,
width,
height,
unoptimized = false,
priority = false,
lazy = false,
className,
quality,
width,
height,
unsized,
...rest
}: ImageProps) {
const thisEl = useRef<HTMLImageElement>(null)
Expand Down Expand Up @@ -219,11 +221,47 @@ export default function Image({
// it's too late for preloads
const shouldPreload = priority && typeof window === 'undefined'

const ratio = (height / width) * 100
const paddingBottom = `${isNaN(ratio) ? 1 : ratio}%`
let divStyle: React.CSSProperties | undefined
let imgStyle: React.CSSProperties | undefined
if (typeof height === 'number' && typeof width === 'number' && !unsized) {
// <Image src="i.png" width=100 height=100 />
const quotient = height / width
const ratio = isNaN(quotient) ? 1 : quotient * 100
divStyle = {
position: 'relative',
paddingBottom: `${ratio}%`,
}
imgStyle = {
height: '100%',
left: '0',
position: 'absolute',
top: '0',
width: '100%',
}
} else if (
typeof height === 'undefined' &&
typeof width === 'undefined' &&
unsized
) {
// <Image src="i.png" unsized />
if (process.env.NODE_ENV !== 'production') {
if (priority) {
// <Image src="i.png" unsized priority />
console.warn(
`Image with src ${src} has both priority and unsized attributes. Only one should be used.`
)
}
}
} else {
if (process.env.NODE_ENV !== 'production') {
console.error(
`Image with src ${src} must use width and height attributes or unsized attribute.`
)
}
}

return (
<div style={{ position: 'relative', paddingBottom }}>
<div style={divStyle}>
{shouldPreload
? generatePreload({
src,
Expand All @@ -238,13 +276,7 @@ export default function Image({
className={className}
sizes={sizes}
ref={thisEl}
style={{
height: '100%',
left: '0',
position: 'absolute',
top: '0',
width: '100%',
}}
style={imgStyle}
/>
</div>
)
Expand Down
1 change: 1 addition & 0 deletions test/integration/image-component/default/pages/index.js
Expand Up @@ -6,6 +6,7 @@ const Page = () => {
<div>
<p>Hello World</p>
<Image id="basic-image" src="/test.jpg" width={400} height={400}></Image>
<Image id="unsized-image" src="/test.png" unsized></Image>
<p id="stubtext">This is the index page</p>
</div>
)
Expand Down
13 changes: 12 additions & 1 deletion test/integration/image-component/default/test/index.test.js
Expand Up @@ -21,7 +21,7 @@ let appPort
let app

function runTests() {
it('should load the image', async () => {
it('should load the images', async () => {
let browser
try {
browser = await webdriver(appPort, '/')
Expand All @@ -35,6 +35,17 @@ function runTests() {

return 'result-correct'
}, /result-correct/)

await check(async () => {
const result = await browser.eval(
`document.getElementById('unsized-image').naturalWidth`
)
if (result === 0) {
throw new Error('Incorrectly loaded image')
}

return 'result-correct'
}, /result-correct/)
} finally {
if (browser) {
await browser.close()
Expand Down
5 changes: 5 additions & 0 deletions test/integration/image-component/typescript/next.config.js
@@ -0,0 +1,5 @@
module.exports = {
images: {
domains: ['via.placeholder.com'],
},
}
17 changes: 17 additions & 0 deletions test/integration/image-component/typescript/pages/invalid.tsx
@@ -0,0 +1,17 @@
import React from 'react'
import Image from 'next/image'

const Invalid = () => {
return (
<div>
<h1>Hello World</h1>
<Image
id="no-width-or-height"
src="https://via.placeholder.com/500"
></Image>
<p id="stubtext">This is the invalid usage</p>
</div>
)
}

export default Invalid
24 changes: 24 additions & 0 deletions test/integration/image-component/typescript/pages/valid.tsx
@@ -0,0 +1,24 @@
import React from 'react'
import Image from 'next/image'

const Page = () => {
return (
<div>
<p>Hello World</p>
<Image
id="with-and-height"
src="https://via.placeholder.com/500"
width={500}
height={500}
></Image>
<Image
id="unsized-image"
src="https://via.placeholder.com/100"
unsized
></Image>
<p id="stubtext">This is valid usage of the Image component</p>
</div>
)
}

export default Page
59 changes: 59 additions & 0 deletions test/integration/image-component/typescript/test/index.test.js
@@ -0,0 +1,59 @@
/* eslint-env jest */

import { join } from 'path'
import {
renderViaHTTP,
findPort,
launchApp,
nextBuild,
killApp,
} from 'next-test-utils'

jest.setTimeout(1000 * 60 * 2)

const appDir = join(__dirname, '..')
let appPort
let app
let output

const handleOutput = (msg) => {
output += msg
}

describe('TypeScript Image Component', () => {
describe('next build', () => {
it('should fail to build invalid usage of the Image component', async () => {
const { stderr, code } = await nextBuild(appDir, [], { stderr: true })
expect(stderr).toMatch(/Failed to compile/)
expect(stderr).toMatch(/is not assignable to type/)
expect(code).toBe(1)
})
})

describe('next dev', () => {
beforeAll(async () => {
output = ''
appPort = await findPort()
app = await launchApp(appDir, appPort, {
onStdout: handleOutput,
onStderr: handleOutput,
})
})
afterAll(() => killApp(app))

it('should render the valid Image usage and not print error', async () => {
const html = await renderViaHTTP(appPort, '/valid', {})
expect(html).toMatch(/This is valid usage of the Image component/)
expect(output).not.toMatch(
/must use width and height attributes or unsized attribute/
)
})

it('should print error when invalid Image usage', async () => {
await renderViaHTTP(appPort, '/invalid', {})
expect(output).toMatch(
/must use width and height attributes or unsized attribute/
)
})
})
})
19 changes: 19 additions & 0 deletions test/integration/image-component/typescript/tsconfig.json
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"esModuleInterop": true,
"module": "esnext",
"jsx": "preserve",
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true
},
"exclude": ["node_modules"],
"include": ["next-env.d.ts", "components", "pages"]
}

0 comments on commit e8f402a

Please sign in to comment.