From 76bad07bdfed01dc502c90b34672d7fd9032a4fe Mon Sep 17 00:00:00 2001 From: Alex Castle Date: Mon, 14 Mar 2022 07:25:23 -0700 Subject: [PATCH] Add new 'raw' image layout (#34339) This PR adds a new layout mode for images called `raw`, as discussed with the core team a while back. This mode has the following characteristics: - No wrapper `span` around the `img` element - No sizer svg - Almost no styles automatically added to the `img` element - `style` parameter is allowed and is passed through to the underlying `img` element This also adds documentation changes to describe the new component. There are a few tradeoffs and DX decisions that may warrant discussion/revision before merging. I'll add a few comments to highlight those issues. - Related to #18637 --- docs/api-reference/next/image.md | 64 +++- docs/basic-features/image-optimization.md | 4 +- packages/next/build/webpack-config.ts | 1 + packages/next/client/image.tsx | 334 ++++++++++++------ packages/next/server/config-shared.ts | 6 + .../image-component/default/next.config.js | 7 + .../default/pages/blurry-placeholder.js | 11 + .../default/pages/invalid-style.js | 13 - .../default/pages/layout-raw.js | 47 +++ .../default/pages/on-loading-complete.js | 10 + .../image-component/default/pages/priority.js | 8 + .../default/pages/style-prop.js | 55 +++ .../default/test/index.test.js | 130 ++++++- .../typescript-style/next.config.js | 5 - .../typescript-style/pages/invalid.tsx | 19 - .../typescript-style/test/index.test.js | 17 - .../typescript-style/tsconfig.json | 20 -- 17 files changed, 534 insertions(+), 217 deletions(-) create mode 100644 test/integration/image-component/default/next.config.js delete mode 100644 test/integration/image-component/default/pages/invalid-style.js create mode 100644 test/integration/image-component/default/pages/layout-raw.js create mode 100644 test/integration/image-component/default/pages/style-prop.js delete mode 100644 test/integration/image-component/typescript-style/next.config.js delete mode 100644 test/integration/image-component/typescript-style/pages/invalid.tsx delete mode 100644 test/integration/image-component/typescript-style/test/index.test.js delete mode 100644 test/integration/image-component/typescript-style/tsconfig.json diff --git a/docs/api-reference/next/image.md b/docs/api-reference/next/image.md index 4fa36b777d21..4035fa8c65ec 100644 --- a/docs/api-reference/next/image.md +++ b/docs/api-reference/next/image.md @@ -14,16 +14,17 @@ description: Enable Image Optimization with the built-in Image component.
Version History -| Version | Changes | -| --------- | ------------------------------------------------------------------------------------------------- | -| `v12.1.0` | `dangerouslyAllowSVG` and `contentSecurityPolicy` configuration added. | -| `v12.0.9` | `lazyRoot` prop added. | -| `v12.0.0` | `formats` configuration added.
AVIF support added.
Wrapper `
` changed to ``. | -| `v11.1.0` | `onLoadingComplete` and `lazyBoundary` props added. | -| `v11.0.0` | `src` prop support for static import.
`placeholder` prop added.
`blurDataURL` prop added. | -| `v10.0.5` | `loader` prop added. | -| `v10.0.1` | `layout` prop added. | -| `v10.0.0` | `next/image` introduced. | +| Version | Changes | +| --------- | ----------------------------------------------------------------------------------------------------- | +| `v12.1.1` | `style` prop added. Experimental[\*](#experimental-raw-layout-mode) support for `layout="raw"` added. | +| `v12.1.0` | `dangerouslyAllowSVG` and `contentSecurityPolicy` configuration added. | +| `v12.0.9` | `lazyRoot` prop added. | +| `v12.0.0` | `formats` configuration added.
AVIF support added.
Wrapper `
` changed to ``. | +| `v11.1.0` | `onLoadingComplete` and `lazyBoundary` props added. | +| `v11.0.0` | `src` prop support for static import.
`placeholder` prop added.
`blurDataURL` prop added. | +| `v10.0.5` | `loader` prop added. | +| `v10.0.1` | `layout` prop added. | +| `v10.0.0` | `next/image` introduced. |
@@ -65,12 +66,13 @@ The `` component accepts a number of additional properties beyond those The layout behavior of the image as the viewport changes size. -| `layout` | Behavior | `srcSet` | `sizes` | -| --------------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------- | -| `intrinsic` (default) | Scale *down* to fit width of container, up to image size | `1x`, `2x` (based on [imageSizes](#image-sizes)) | N/A | -| `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` | +| `layout` | Behavior | `srcSet` | `sizes` | Has wrapper and sizer | +| ---------------------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -------- | --------------------- | +| `intrinsic` (default) | Scale *down* to fit width of container, up to image size | `1x`, `2x` (based on [imageSizes](#image-sizes)) | N/A | yes | +| `fixed` | Sized to `width` and `height` exactly | `1x`, `2x` (based on [imageSizes](#image-sizes)) | N/A | yes | +| `responsive` | Scale to fit width of container | `640w`, `750w`, ... `2048w`, `3840w` (based on [imageSizes](#image-sizes) and [deviceSizes](#device-sizes)) | `100vw` | yes | +| `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` | yes | +| `raw`[\*](#experimental-raw-layout-mode) | Insert the image element with no automatic layout behavior | Behaves like `responsive` if the image has the `sizes` prop, and like `fixed` if it does not | optional | no | - [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. @@ -83,6 +85,9 @@ 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`[\*](#experimental-raw-layout-mode), the image will be rendered as a single image element with no wrappers, sizers or other responsive behavior. + - If your image styling will change the size of a `raw` image, you should include the `sizes` property for proper image serving. Otherwise your image will receive a fixed height and width. + - 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 @@ -121,7 +126,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"`[\*](#experimental-raw-layout-mode) 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. @@ -162,6 +167,12 @@ Try it out: In some cases, you may need more advanced usage. The `` component optionally accepts the following advanced properties. +### style + +Allows [passing CSS styles](https://reactjs.org/docs/dom-elements.html#style) to the underlying image element. + +Note that all `layout` modes other than `"raw"`[\*](#experimental-raw-layout-mode) apply their own styles to the image element, and these automatic styles take precedence over the `style` prop. + ### objectFit Defines how the image will fit into its parent container when using `layout="fill"`. @@ -285,7 +296,6 @@ size, or format. Defaults to `false`. Other properties on the `` component will be passed to the underlying `img` element with the exception of the following: -- `style`. Use `className` instead. - `srcSet`. Use [Device Sizes](#device-sizes) instead. @@ -455,6 +465,24 @@ module.exports = { } ``` +### Experimental "raw" layout mode + +The image component currently supports an additional `layout="raw"` mode, which renders the image without wrappers or styling. This layout mode is currently an experimental feature, while user feedback is gathered. As there is the possibility of breaking changes to the `layout="raw"` interface, the feature is locked behind an experimental feature flag. If you would like to use the `raw` layout mode, you must add the following to your `next.config.js`: + +```js +module.exports = { + experimental: { + images: { + layoutRaw: true, + }, + }, +} +``` + +> Note on CLS with `layout="raw"`: +> It is possible to cause [layout shift](https://web.dev/cls/) with the image component in `raw` mode. If you include a `sizes` property, the image component will not pass `height` and `width` attributes to the image, to allow you to apply your own responsive sizing. +> An [aspect-ratio](https://developer.mozilla.org/en-US/docs/Web/CSS/aspect-ratio) style property is automatically applied to prevent layout shift, but this won't apply on [older browsers](https://caniuse.com/mdn-css_properties_aspect-ratio). + ### Animated Images The default [loader](#loader) will automatically bypass Image Optimization for animated images and serve the image as-is. diff --git a/docs/basic-features/image-optimization.md b/docs/basic-features/image-optimization.md index 0c7cb5a6a292..9de528f1910b 100644 --- a/docs/basic-features/image-optimization.md +++ b/docs/basic-features/image-optimization.md @@ -181,7 +181,7 @@ 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 `` tag wrapped by exactly one ``. For some modes, it may also have a sibling `` for spacing. These additional `` elements are critical to allow the component to prevent layout shifts. +For most layout modes, the Image component will have a DOM structure of one `` tag wrapped by exactly one ``. For some modes, it may also have a sibling `` for spacing. These additional `` elements are critical to allow the component to prevent layout shifts. The recommended way to style the inner `` 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 `` element. @@ -189,8 +189,6 @@ Alternatively, you can import a [global stylesheet](/docs/basic-features/built-i You cannot use [styled-jsx](/docs/basic-features/built-in-css-support.md#css-in-js) because it's scoped to the current component. -You cannot use the `style` prop because the `` component does not pass it through to the underlying ``. - **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. diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 9f96954d4d41..3e1baa95b172 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1351,6 +1351,7 @@ export default async function getBaseWebpackConfig( imageSizes: config.images.imageSizes, path: config.images.path, loader: config.images.loader, + experimentalLayoutRaw: config.experimental?.images?.layoutRaw, ...(dev ? { // pass domains in development to allow validating on the client diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index d152015405a0..45c0185cabfd 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -11,6 +11,8 @@ import { ImageConfigContext } from '../shared/lib/image-config-context' import { warnOnce } from '../shared/lib/utils' import { normalizePathTrailingSlash } from './normalize-trailing-slash' +const experimentalLayoutRaw = (process.env.__NEXT_IMAGE_OPTS as any) + ?.experimentalLayoutRaw const configEnv = process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete const loadedImageURLs = new Set() const allImgs = new Map< @@ -50,6 +52,7 @@ const VALID_LAYOUT_VALUES = [ 'fixed', 'intrinsic', 'responsive', + 'raw', undefined, ] as const type LayoutValue = typeof VALID_LAYOUT_VALUES[number] @@ -98,7 +101,7 @@ function isStaticImport(src: string | StaticImport): src is StaticImport { export type ImageProps = Omit< JSX.IntrinsicElements['img'], - 'src' | 'srcSet' | 'ref' | 'width' | 'height' | 'loading' | 'style' + 'src' | 'srcSet' | 'ref' | 'width' | 'height' | 'loading' > & { src: string | StaticImport width?: number | string @@ -118,13 +121,33 @@ export type ImageProps = Omit< onLoadingComplete?: OnLoadingComplete } +type ImageElementProps = Omit & { + srcString: string + imgAttributes: GenImgAttrsResult + heightInt: number | undefined + widthInt: number | undefined + qualityInt: number | undefined + layout: LayoutValue + imgStyle: ImgElementStyle + blurStyle: ImgElementStyle + isLazy: boolean + imgRef: React.RefObject + loading: LoadingValue + config: ImageConfig + unoptimized: boolean + loader: ImageLoader +} + 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 ( + 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 = [] @@ -325,6 +348,7 @@ export default function Image({ quality, width, height, + style, objectFit, objectPosition, onLoadingComplete, @@ -333,8 +357,6 @@ export default function Image({ blurDataURL, ...all }: ImageProps) { - const imgRef = useRef(null) - const configContext = useContext(ImageConfigContext) const config: ImageConfig = useMemo(() => { const c = configEnv || configContext || imageConfigDefault @@ -395,6 +417,65 @@ export default function Image({ isLazy = false } + const [setIntersection, isIntersected] = useIntersection({ + rootRef: lazyRoot, + rootMargin: lazyBoundary, + disabled: !isLazy, + }) + const isVisible = !isLazy || isIntersected + + const wrapperStyle: JSX.IntrinsicElements['span']['style'] = { + boxSizing: 'border-box', + display: 'block', + overflow: 'hidden', + width: 'initial', + height: 'initial', + background: 'none', + opacity: 1, + border: 0, + margin: 0, + padding: 0, + } + const sizerStyle: JSX.IntrinsicElements['span']['style'] = { + boxSizing: 'border-box', + display: 'block', + width: 'initial', + height: 'initial', + background: 'none', + opacity: 1, + border: 0, + margin: 0, + padding: 0, + } + let hasSizer = false + let sizerSvgUrl: string | undefined + const layoutStyle: ImgElementStyle = { + position: 'absolute', + top: 0, + left: 0, + bottom: 0, + right: 0, + + boxSizing: 'border-box', + padding: 0, + border: 'none', + margin: 'auto', + + display: 'block', + width: 0, + height: 0, + minWidth: '100%', + maxWidth: '100%', + minHeight: '100%', + maxHeight: '100%', + + objectFit, + objectPosition, + } + + if (process.env.NODE_ENV !== 'production' && layout !== 'raw' && style) { + } + if (process.env.NODE_ENV !== 'production') { if (!src) { throw new Error( @@ -410,6 +491,11 @@ export default function Image({ ).join(',')}.` ) } + if (layout === 'raw' && !experimentalLayoutRaw) { + throw new Error( + `The "raw" layout is currently experimental and may be subject to breaking changes. To use layout="raw", include \`experimental: { images: { layoutRaw: true } }\` in your next.config.js file.` + ) + } if ( (typeof widthInt !== 'undefined' && isNaN(widthInt)) || (typeof heightInt !== 'undefined' && isNaN(heightInt)) @@ -435,9 +521,19 @@ 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 (layout === 'raw' && (objectFit || objectPosition)) { + throw new Error( + `Image with src "${src}" has "layout='raw'" and 'objectFit' or 'objectPosition'. For raw images, these and other styles should be specified using the 'style' attribute.` + ) + } + if ( + sizes && + layout !== 'fill' && + layout !== 'responsive' && + layout !== 'raw' + ) { warnOnce( - `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 "layout='fill'", "layout='responsive'", or "layout='raw'` ) } if (placeholder === 'blur') { @@ -466,11 +562,6 @@ export default function Image({ `Image with src "${src}" is using unsupported "ref" property. Consider using the "onLoadingComplete" property instead.` ) } - if ('style' in rest) { - warnOnce( - `Image with src "${src}" is using unsupported "style" property. Please use the "className" property instead.` - ) - } if (!unoptimized && loader !== defaultImageLoader) { const urlStr = loader({ @@ -491,6 +582,19 @@ export default function Image({ } } + if (style && layout !== 'raw') { + let overwrittenStyles = Object.keys(style).filter( + (key) => key in layoutStyle + ) + if (overwrittenStyles.length) { + warnOnce( + `Image with src ${src} is assigned the following styles, which are overwritten by automatically-generated styles: ${overwrittenStyles.join( + ', ' + )}` + ) + } + } + if ( typeof window !== 'undefined' && !perfObserver && @@ -528,61 +632,14 @@ export default function Image({ } } - const [setIntersection, isIntersected] = useIntersection({ - rootRef: lazyRoot, - rootMargin: lazyBoundary, - disabled: !isLazy, - }) - const isVisible = !isLazy || isIntersected - - const wrapperStyle: JSX.IntrinsicElements['span']['style'] = { - boxSizing: 'border-box', - display: 'block', - overflow: 'hidden', - width: 'initial', - height: 'initial', - background: 'none', - opacity: 1, - border: 0, - margin: 0, - padding: 0, - } - const sizerStyle: JSX.IntrinsicElements['span']['style'] = { - boxSizing: 'border-box', - display: 'block', - width: 'initial', - height: 'initial', - background: 'none', - opacity: 1, - border: 0, - margin: 0, - padding: 0, - } - let hasSizer = false - let sizerSvgUrl: string | undefined - const imgStyle: ImgElementStyle = { - position: 'absolute', - top: 0, - left: 0, - bottom: 0, - right: 0, - - boxSizing: 'border-box', - padding: 0, - border: 'none', - margin: 'auto', - - display: 'block', - width: 0, - height: 0, - minWidth: '100%', - maxWidth: '100%', - minHeight: '100%', - maxHeight: '100%', + const imgStyle = Object.assign( + {}, + style, + layout === 'raw' + ? { aspectRatio: `${widthInt} / ${heightInt}` } + : layoutStyle + ) - objectFit, - objectPosition, - } const blurStyle = placeholder === 'blur' ? { @@ -686,6 +743,7 @@ export default function Image({ typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect const onLoadingCompleteRef = useRef(onLoadingComplete) + const imgRef = useRef(null) useEffect(() => { onLoadingCompleteRef.current = onLoadingComplete }, [onLoadingComplete]) @@ -697,34 +755,109 @@ export default function Image({ useEffect(() => { handleLoading(imgRef, srcString, layout, placeholder, onLoadingCompleteRef) }, [srcString, layout, placeholder, isVisible]) - + const imgElementArgs = { + isLazy, + imgAttributes, + heightInt, + widthInt, + qualityInt, + layout, + className, + imgStyle, + blurStyle, + imgRef, + loading, + config, + unoptimized, + placeholder, + loader, + srcString, + ...rest, + } return ( - - {hasSizer ? ( - - {sizerSvgUrl ? ( - + <> + {layout === 'raw' ? ( + + ) : ( + + {hasSizer ? ( + + {sizerSvgUrl ? ( + + ) : null} + ) : null} + + )} + {priority ? ( + // Note how we omit the `href` attribute, as it would only be relevant + // for browsers that do not support `imagesrcset`, and in those cases + // it would likely cause the incorrect image to be preloaded. + // + // https://html.spec.whatwg.org/multipage/semantics.html#attr-link-imagesrcset + + + ) : null} + + ) +} + +const ImageElement = ({ + imgAttributes, + heightInt, + widthInt, + qualityInt, + layout, + className, + imgStyle, + blurStyle, + isLazy, + imgRef, + placeholder, + loading, + sizes, + srcString, + config, + unoptimized, + loader, + ...rest +}: ImageElementProps) => { + return ( + <> )} - - {priority ? ( - // Note how we omit the `href` attribute, as it would only be relevant - // for browsers that do not support `imagesrcset`, and in those cases - // it would likely cause the incorrect image to be preloaded. - // - // https://html.spec.whatwg.org/multipage/semantics.html#attr-link-imagesrcset - - - - ) : null} - + ) } diff --git a/packages/next/server/config-shared.ts b/packages/next/server/config-shared.ts index 7bb46fb39f02..baa1e4d71b89 100644 --- a/packages/next/server/config-shared.ts +++ b/packages/next/server/config-shared.ts @@ -107,6 +107,9 @@ export interface ExperimentalConfig { urlImports?: NonNullable['buildHttp'] outputFileTracingRoot?: string outputStandalone?: boolean + images?: { + layoutRaw: boolean + } middlewareSourceMaps?: boolean } @@ -471,6 +474,9 @@ export const defaultConfig: NextConfig = { fullySpecified: false, outputFileTracingRoot: process.env.NEXT_PRIVATE_OUTPUT_TRACE_ROOT || '', outputStandalone: !!process.env.NEXT_PRIVATE_STANDALONE, + images: { + layoutRaw: false, + }, }, } diff --git a/test/integration/image-component/default/next.config.js b/test/integration/image-component/default/next.config.js new file mode 100644 index 000000000000..7f8d4e88d0f7 --- /dev/null +++ b/test/integration/image-component/default/next.config.js @@ -0,0 +1,7 @@ +module.exports = { + experimental: { + images: { + layoutRaw: true, + }, + }, +} diff --git a/test/integration/image-component/default/pages/blurry-placeholder.js b/test/integration/image-component/default/pages/blurry-placeholder.js index 58751c0b9afb..e030fc8ccbb3 100644 --- a/test/integration/image-component/default/pages/blurry-placeholder.js +++ b/test/integration/image-component/default/pages/blurry-placeholder.js @@ -15,6 +15,17 @@ export default function Page() { blurDataURL="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='400' viewBox='0 0 400 400'%3E%3Cfilter id='blur' filterUnits='userSpaceOnUse' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20' edgeMode='duplicate' /%3E%3CfeComponentTransfer%3E%3CfeFuncA type='discrete' tableValues='1 1' /%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Cimage filter='url(%23blur)' href='data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMDAwMDAwQEBAQFBQUFBQcHBgYHBwsICQgJCAsRCwwLCwwLEQ8SDw4PEg8bFRMTFRsfGhkaHyYiIiYwLTA+PlT/wAALCAAKAAoBAREA/8QAMwABAQEAAAAAAAAAAAAAAAAAAAcJEAABAwUAAwAAAAAAAAAAAAAFAAYRAQMEEyEVMlH/2gAIAQEAAD8Az1bLPaxhiuk0QdeCOLDtHixN2dmd2bsc5FPX7VTREX//2Q==' x='0' y='0' height='100%25' width='100%25'/%3E%3C/svg%3E" /> + + { - return ( -
- -
- ) -} - -export default Page diff --git a/test/integration/image-component/default/pages/layout-raw.js b/test/integration/image-component/default/pages/layout-raw.js new file mode 100644 index 000000000000..ed93d2ae2ec6 --- /dev/null +++ b/test/integration/image-component/default/pages/layout-raw.js @@ -0,0 +1,47 @@ +import React from 'react' +import Image from 'next/image' + +const Page = () => { + return ( +
+

Layout Raw

+
+ +
+
+ +
+
+ +
+
+ ) +} + +export default Page diff --git a/test/integration/image-component/default/pages/on-loading-complete.js b/test/integration/image-component/default/pages/on-loading-complete.js index 88ad4cc2a54a..11418881fdf3 100644 --- a/test/integration/image-component/default/pages/on-loading-complete.js +++ b/test/integration/image-component/default/pages/on-loading-complete.js @@ -84,6 +84,16 @@ const Page = () => { idToCount={idToCount} setIdToCount={setIdToCount} /> + + + diff --git a/test/integration/image-component/default/pages/priority.js b/test/integration/image-component/default/pages/priority.js index 2d4f58690907..3adb39db7984 100644 --- a/test/integration/image-component/default/pages/priority.js +++ b/test/integration/image-component/default/pages/priority.js @@ -35,6 +35,14 @@ const Page = () => { height="700" layout="responsive" /> +

This is the priority page

) diff --git a/test/integration/image-component/default/pages/style-prop.js b/test/integration/image-component/default/pages/style-prop.js new file mode 100644 index 000000000000..17a8faa27b4f --- /dev/null +++ b/test/integration/image-component/default/pages/style-prop.js @@ -0,0 +1,55 @@ +import React from 'react' +import Image from 'next/image' + +const Page = () => { + return ( +
+

Style prop usage and warnings

+ + + + + +
+ ) +} + +export default Page diff --git a/test/integration/image-component/default/test/index.test.js b/test/integration/image-component/default/test/index.test.js index 9bc8665170f2..514f71516beb 100644 --- a/test/integration/image-component/default/test/index.test.js +++ b/test/integration/image-component/default/test/index.test.js @@ -135,6 +135,11 @@ function runTests(mode) { imagesrcset: '/_next/image?url=%2Fwide.png&w=640&q=75 640w, /_next/image?url=%2Fwide.png&w=750&q=75 750w, /_next/image?url=%2Fwide.png&w=828&q=75 828w, /_next/image?url=%2Fwide.png&w=1080&q=75 1080w, /_next/image?url=%2Fwide.png&w=1200&q=75 1200w, /_next/image?url=%2Fwide.png&w=1920&q=75 1920w, /_next/image?url=%2Fwide.png&w=2048&q=75 2048w, /_next/image?url=%2Fwide.png&w=3840&q=75 3840w', }, + { + imagesizes: '', + imagesrcset: + '/_next/image?url=%2Ftest.webp&w=1200&q=75 1x, /_next/image?url=%2Ftest.webp&w=3840&q=75 2x', + }, ]) const warnings = (await browser.log('browser')) @@ -273,6 +278,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 400x400' + ) } finally { if (browser) { await browser.close() @@ -584,6 +593,101 @@ 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( + `aspect-ratio:1200 / 700` + ) + expect(await browser.elementById('raw1').getAttribute('srcset')).toBe( + `/_next/image?url=%2Fwide.png&w=1200&q=75 1x, /_next/image?url=%2Fwide.png&w=3840&q=75 2x` + ) + + expect(await browser.elementById('raw2').getAttribute('style')).toBe( + 'padding-left:4rem;width:100%;object-position:30% 30%;aspect-ratio:1200 / 700' + ) + expect(await browser.elementById('raw2').getAttribute('srcset')).toBe( + `/_next/image?url=%2Fwide.png&w=16&q=75 16w, /_next/image?url=%2Fwide.png&w=32&q=75 32w, /_next/image?url=%2Fwide.png&w=48&q=75 48w, /_next/image?url=%2Fwide.png&w=64&q=75 64w, /_next/image?url=%2Fwide.png&w=96&q=75 96w, /_next/image?url=%2Fwide.png&w=128&q=75 128w, /_next/image?url=%2Fwide.png&w=256&q=75 256w, /_next/image?url=%2Fwide.png&w=384&q=75 384w, /_next/image?url=%2Fwide.png&w=640&q=75 640w, /_next/image?url=%2Fwide.png&w=750&q=75 750w, /_next/image?url=%2Fwide.png&w=828&q=75 828w, /_next/image?url=%2Fwide.png&w=1080&q=75 1080w, /_next/image?url=%2Fwide.png&w=1200&q=75 1200w, /_next/image?url=%2Fwide.png&w=1920&q=75 1920w, /_next/image?url=%2Fwide.png&w=2048&q=75 2048w, /_next/image?url=%2Fwide.png&w=3840&q=75 3840w` + ) + + expect(await browser.elementById('raw3').getAttribute('style')).toBe( + 'aspect-ratio:400 / 400' + ) + expect(await browser.elementById('raw3').getAttribute('srcset')).toBe( + `/_next/image?url=%2Ftest.png&w=640&q=75 1x, /_next/image?url=%2Ftest.png&w=828&q=75 2x` + ) + } finally { + if (browser) { + await browser.close() + } + } + }) + it('should handle the styles prop appropriately', async () => { + let browser + try { + browser = await webdriver(appPort, '/style-prop') + + expect( + await browser.elementById('with-styles').getAttribute('style') + ).toBe( + 'border-radius:10px;padding:0;position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%' + ) + expect( + await browser + .elementById('with-overlapping-styles-intrinsic') + .getAttribute('style') + ).toBe( + 'width:0;border-radius:10px;margin:auto;position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;display:block;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%' + ) + expect( + await browser + .elementById('with-overlapping-styles-raw') + .getAttribute('style') + ).toBe('width:10px;border-radius:10px;margin:15px;aspect-ratio:400 / 400') + expect( + await browser + .elementById('without-styles-responsive') + .getAttribute('style') + ).toBe( + 'position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%' + ) + expect( + await browser.elementById('without-styles-raw').getAttribute('style') + ).toBe('aspect-ratio:400 / 400') + + if (mode === 'dev') { + await waitFor(1000) + const warnings = (await browser.log('browser')) + .map((log) => log.message) + .join('\n') + expect(warnings).toMatch( + /Image with src \/test.png is assigned the following styles, which are overwritten by automatically-generated styles: padding/gm + ) + expect(warnings).toMatch( + /Image with src \/test.jpg is assigned the following styles, which are overwritten by automatically-generated styles: width, margin/gm + ) + expect(warnings).not.toMatch( + /Image with src \/test.webp is assigned the following styles/gm + ) + } + } finally { + if (browser) { + await browser.close() + } + } + }) + if (mode === 'dev') { it('should show missing src error', async () => { const browser = await webdriver(appPort, '/missing-src') @@ -690,18 +794,6 @@ function runTests(mode) { ) }) - it('should warn when style prop is used', async () => { - const browser = await webdriver(appPort, '/invalid-style') - - await check(async () => { - return (await browser.log('browser')) - .map((log) => log.message) - .join('\n') - }, /Image with src (.*)jpg(.*) is using unsupported "style" property(.*)/gm) - - expect(await hasRedbox(browser)).toBe(false) - }) - it('should not warn when Image is child of p', async () => { const browser = await webdriver(appPort, '/inside-paragraph') @@ -831,6 +923,7 @@ function runTests(mode) { } return 'done' }, 'done') + await waitFor(1000) const warnings = (await browser.log('browser')) .map((log) => log.message) .filter((log) => log.startsWith('Image with src')) @@ -1001,6 +1094,10 @@ function runTests(mode) { `background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='400' viewBox='0 0 400 400'%3E%3Cfilter id='blur' filterUnits='userSpaceOnUse' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20' edgeMode='duplicate' /%3E%3CfeComponentTransfer%3E%3CfeFuncA type='discrete' tableValues='1 1' /%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Cimage filter='url(%23blur)' href='data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMDAwMDAwQEBAQFBQUFBQcHBgYHBwsICQgJCAsRCwwLCwwLEQ8SDw4PEg8bFRMTFRsfGhkaHyYiIiYwLTA+PlT/wAALCAAKAAoBAREA/8QAMwABAQEAAAAAAAAAAAAAAAAAAAcJEAABAwUAAwAAAAAAAAAAAAAFAAYRAQMEEyEVMlH/2gAIAQEAAD8Az1bLPaxhiuk0QdeCOLDtHixN2dmd2bsc5FPX7VTREX//2Q==' x='0' y='0' height='100%25' width='100%25'/%3E%3C/svg%3E")` ) + expect($html('#blurry-placeholder-raw')[0].attribs.style).toContain( + `background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='400' viewBox='0 0 400 400'%3E%3Cfilter id='blur' filterUnits='userSpaceOnUse' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20' edgeMode='duplicate' /%3E%3CfeComponentTransfer%3E%3CfeFuncA type='discrete' tableValues='1 1' /%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Cimage filter='url(%23blur)' href='data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMDAwMDAwQEBAQFBQUFBQcHBgYHBwsICQgJCAsRCwwLCwwLEQ8SDw4PEg8bFRMTFRsfGhkaHyYiIiYwLTA+PlT/wAALCAAKAAoBAREA/8QAMwABAQEAAAAAAAAAAAAAAAAAAAcJEAABAwUAAwAAAAAAAAAAAAAFAAYRAQMEEyEVMlH/2gAIAQEAAD8Az1bLPaxhiuk0QdeCOLDtHixN2dmd2bsc5FPX7VTREX//2Q==' x='0' y='0' height='100%25' width='100%25'/%3E%3C/svg%3E")` + ) + expect($html('#blurry-placeholder')[0].attribs.style).toContain( `background-position:0% 0%` ) @@ -1039,6 +1136,15 @@ function runTests(mode) { ), 'none' ) + await check( + async () => + await getComputedStyle( + browser, + 'blurry-placeholder-raw', + 'background-image' + ), + 'none' + ) expect( await getComputedStyle( browser, diff --git a/test/integration/image-component/typescript-style/next.config.js b/test/integration/image-component/typescript-style/next.config.js deleted file mode 100644 index 64af59160546..000000000000 --- a/test/integration/image-component/typescript-style/next.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - images: { - domains: ['via.placeholder.com'], - }, -} diff --git a/test/integration/image-component/typescript-style/pages/invalid.tsx b/test/integration/image-component/typescript-style/pages/invalid.tsx deleted file mode 100644 index 06a6635269a0..000000000000 --- a/test/integration/image-component/typescript-style/pages/invalid.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' -import Image from 'next/image' - -const Invalid = () => { - return ( -
-

Invalid TS

- -

This is the invalid usage

-
- ) -} - -export default Invalid diff --git a/test/integration/image-component/typescript-style/test/index.test.js b/test/integration/image-component/typescript-style/test/index.test.js deleted file mode 100644 index c89386a8743d..000000000000 --- a/test/integration/image-component/typescript-style/test/index.test.js +++ /dev/null @@ -1,17 +0,0 @@ -/* eslint-env jest */ - -import { nextBuild } from 'next-test-utils' -import { join } from 'path' - -const appDir = join(__dirname, '..') - -describe('TypeScript Image Component with Styles', () => { - describe('next build', () => { - it('should fail to build when the `style` prop is passed to ', async () => { - const { stderr, code } = await nextBuild(appDir, [], { stderr: true }) - expect(stderr).toMatch(/Failed to compile/) - expect(stderr).toMatch(/Property 'style' does not exist on type/) - expect(code).toBe(1) - }) - }) -}) diff --git a/test/integration/image-component/typescript-style/tsconfig.json b/test/integration/image-component/typescript-style/tsconfig.json deleted file mode 100644 index 9ada0f9fde61..000000000000 --- a/test/integration/image-component/typescript-style/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "esModuleInterop": true, - "module": "esnext", - "jsx": "preserve", - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noEmit": true, - "incremental": true, - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true - }, - "exclude": ["node_modules"], - "include": ["next-env.d.ts", "components", "pages"] -}