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 unsized property to Image component #18059

Merged
merged 8 commits into from Oct 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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"]
}