diff --git a/packages/vinext/src/shims/image.tsx b/packages/vinext/src/shims/image.tsx index 3b842e905..ee6ac5352 100644 --- a/packages/vinext/src/shims/image.tsx +++ b/packages/vinext/src/shims/image.tsx @@ -221,6 +221,21 @@ function isRemoteUrl(src: string): boolean { return src.startsWith("http://") || src.startsWith("https://") || src.startsWith("//"); } +function getFillStyle( + style?: React.CSSProperties, + backgroundStyle?: React.CSSProperties, +): React.CSSProperties { + return { + position: "absolute", + inset: 0, + width: "100%", + height: "100%", + objectFit: "cover", + ...backgroundStyle, + ...style, + }; +} + /** * Resolve src, width, height, blurDataURL from Image props (string or StaticImageData). * Shared by the Image component and getImageProps to keep behavior in sync. @@ -421,24 +436,15 @@ const Image = forwardRef(function Image( className={className} onLoad={handleLoad} onError={handleError} - style={ - fill - ? { - position: "absolute", - inset: 0, - width: "100%", - height: "100%", - objectFit: "cover", - ...style, - } - : style - } + style={fill ? getFillStyle(style) : style} {...rest} /> ); } - // For remote URLs, validate against remotePatterns then use @unpic/react + // For remote URLs, validate against remotePatterns. Non-fill images use + // @unpic/react for CDN URL transforms; fill uses a plain img so the DOM + // element keeps Next.js's absolute-positioned fill contract. if (isRemoteUrl(src)) { const validation = validateRemoteUrl(src); if (!validation.allowed) { @@ -453,26 +459,33 @@ const Image = forwardRef(function Image( } const sanitizedBlur = imgBlurDataURL ? sanitizeBlurDataURL(imgBlurDataURL) : undefined; + const blurStyle = + placeholder === "blur" && sanitizedBlur + ? { + backgroundImage: `url(${sanitizedBlur})`, + backgroundSize: "cover", + backgroundRepeat: "no-repeat", + backgroundPosition: "center", + } + : undefined; const bg = placeholder === "blur" && sanitizedBlur ? `url(${sanitizedBlur})` : undefined; if (fill) { return ( - ); } @@ -561,19 +574,7 @@ const Image = forwardRef(function Image( data-nimg={fill ? "fill" : "1"} onLoad={handleLoad} onError={handleError} - style={ - fill - ? { - position: "absolute", - inset: 0, - width: "100%", - height: "100%", - objectFit: "cover", - ...blurStyle, - ...style, - } - : { ...blurStyle, ...style } - } + style={fill ? getFillStyle(style, blurStyle) : { ...blurStyle, ...style }} {...rest} /> ); @@ -684,17 +685,7 @@ export function getImageProps(props: ImageProps): { sizes: sizes ?? (fill ? "100vw" : undefined), className, "data-nimg": fill ? "fill" : "1", - style: fill - ? { - position: "absolute" as const, - inset: 0, - width: "100%", - height: "100%", - objectFit: "cover" as const, - ...blurStyle, - ...style, - } - : { ...blurStyle, ...style }, + style: fill ? getFillStyle(style, blurStyle) : { ...blurStyle, ...style }, ...rest, } as React.ImgHTMLAttributes, }; diff --git a/tests/image-component.test.ts b/tests/image-component.test.ts index a9c963bd5..9a7b1ca97 100644 --- a/tests/image-component.test.ts +++ b/tests/image-component.test.ts @@ -79,6 +79,27 @@ describe("Image SSR rendering", () => { expect(html).toContain('sizes="100vw"'); }); + it("renders remote fill mode with absolute positioning", () => { + // Ported from Next.js: test/unit/next-image-get-img-props.test.ts + // https://github.com/vercel/next.js/blob/canary/test/unit/next-image-get-img-props.test.ts + const html = ReactDOMServer.renderToString( + React.createElement(Image, { + alt: "remote fill image", + src: "https://images.unsplash.com/photo-fill", + fill: true, + }), + ); + // Remote fill must preserve the same layout contract as local fill: + // the DOM img is absolutely positioned and marked as data-nimg="fill". + expect(html).not.toMatch(/width="\d+"/); + expect(html).not.toMatch(/height="\d+"/); + expect(html).toContain("position:absolute"); + expect(html).toContain("width:100%"); + expect(html).toContain("height:100%"); + expect(html).toContain('data-nimg="fill"'); + expect(html).toContain('sizes="100vw"'); + }); + it("renders with custom sizes prop", () => { const html = ReactDOMServer.renderToString( React.createElement(Image, {