diff --git a/code/e2e-tests/framework-nextjs.spec.ts b/code/e2e-tests/framework-nextjs.spec.ts index 75de974ec094..e8840576484f 100644 --- a/code/e2e-tests/framework-nextjs.spec.ts +++ b/code/e2e-tests/framework-nextjs.spec.ts @@ -21,6 +21,31 @@ test.describe('Next.js', () => { await new SbPage(page).waitUntilLoaded(); }); + // TODO: next/image, check if img.complete is true or false + test.describe('next/image', () => { + let sbPage: SbPage; + + test.beforeEach(async ({ page }) => { + sbPage = new SbPage(page); + }); + + test('should lazy load images by default', async () => { + await sbPage.navigateToStory('frameworks/nextjs/Image', 'lazy'); + + const img = sbPage.previewRoot().locator('img'); + + expect(await img.evaluate((image) => image.complete)).toBeFalsy(); + }); + + test('should eager load images when loading parameter is set to eager', async () => { + await sbPage.navigateToStory('frameworks/nextjs/Image', 'eager'); + + const img = sbPage.previewRoot().locator('img'); + + expect(await img.evaluate((image) => image.complete)).toBeTruthy(); + }); + }); + test.describe('next/navigation', () => { let root: Locator; let sbPage: SbPage; diff --git a/code/frameworks/nextjs/README.md b/code/frameworks/nextjs/README.md index b491cf2205f1..fc3b7b9b2e46 100644 --- a/code/frameworks/nextjs/README.md +++ b/code/frameworks/nextjs/README.md @@ -159,12 +159,16 @@ export default { framework: { name: '@storybook/nextjs', options: { + image: { + loading: 'eager', + }, nextConfigPath: path.resolve(__dirname, '../next.config.js'), }, }, }; ``` +- `image`: Props to pass to every instance of `next/image` - `nextConfigPath`: The absolute path to the `next.config.js` ### Next.js's Image Component diff --git a/code/frameworks/nextjs/src/images/context.ts b/code/frameworks/nextjs/src/images/context.ts new file mode 100644 index 000000000000..47139bb86709 --- /dev/null +++ b/code/frameworks/nextjs/src/images/context.ts @@ -0,0 +1,7 @@ +import { createContext } from 'react'; +import type * as _NextImage from 'next/image'; +import type * as _NextLegacyImage from 'next/legacy/image'; + +export const ImageContext = createContext< + Partial<_NextImage.ImageProps & _NextLegacyImage.ImageProps> +>({}); diff --git a/code/frameworks/nextjs/src/images/decorator.tsx b/code/frameworks/nextjs/src/images/decorator.tsx new file mode 100644 index 000000000000..f0917b3a3b50 --- /dev/null +++ b/code/frameworks/nextjs/src/images/decorator.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import type { Addon_StoryContext } from '@storybook/types'; +import { ImageContext } from './context'; + +export const ImageDecorator = ( + Story: React.FC, + { parameters }: Addon_StoryContext +): React.ReactNode => { + if (!parameters.nextjs?.image) { + return ; + } + + return ( + + + + ); +}; diff --git a/code/frameworks/nextjs/src/images/next-image-stub.tsx b/code/frameworks/nextjs/src/images/next-image-stub.tsx index 7fc7268b5c7d..9091d79e5d35 100644 --- a/code/frameworks/nextjs/src/images/next-image-stub.tsx +++ b/code/frameworks/nextjs/src/images/next-image-stub.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import type * as _NextImage from 'next/image'; import type * as _NextLegacyImage from 'next/legacy/image'; import semver from 'semver'; +import { ImageContext } from './context'; const defaultLoader = ({ src, width, quality }: _NextImage.ImageLoaderProps) => { const missingValues = []; @@ -38,7 +39,11 @@ const OriginalNextImage = NextImage.default; Object.defineProperty(NextImage, 'default', { configurable: true, value: (props: _NextImage.ImageProps) => { - return ; + const imageParameters = React.useContext(ImageContext); + + return ( + + ); }, }); @@ -48,9 +53,17 @@ if (semver.satisfies(process.env.__NEXT_VERSION!, '^13.0.0')) { Object.defineProperty(OriginalNextLegacyImage, 'default', { configurable: true, - value: (props: _NextLegacyImage.ImageProps) => ( - - ), + value: (props: _NextLegacyImage.ImageProps) => { + const imageParameters = React.useContext(ImageContext); + + return ( + + ); + }, }); } @@ -60,8 +73,16 @@ if (semver.satisfies(process.env.__NEXT_VERSION!, '^12.2.0')) { Object.defineProperty(OriginalNextFutureImage, 'default', { configurable: true, - value: (props: _NextImage.ImageProps) => ( - - ), + value: (props: _NextImage.ImageProps) => { + const imageParameters = React.useContext(ImageContext); + + return ( + + ); + }, }); } diff --git a/code/frameworks/nextjs/src/preview.tsx b/code/frameworks/nextjs/src/preview.tsx index 301dce86adbe..8894f4fb1d63 100644 --- a/code/frameworks/nextjs/src/preview.tsx +++ b/code/frameworks/nextjs/src/preview.tsx @@ -1,4 +1,5 @@ import './config/preview'; +import { ImageDecorator } from './images/decorator'; import { RouterDecorator } from './routing/decorator'; import { StyledJsxDecorator } from './styledJsx/decorator'; import './images/next-image-stub'; @@ -13,7 +14,12 @@ function addNextHeadCount() { addNextHeadCount(); -export const decorators = [StyledJsxDecorator, RouterDecorator, HeadManagerDecorator]; +export const decorators = [ + StyledJsxDecorator, + ImageDecorator, + RouterDecorator, + HeadManagerDecorator, +]; export const parameters = { docs: { diff --git a/code/frameworks/nextjs/template/stories/Image.stories.jsx b/code/frameworks/nextjs/template/stories/Image.stories.jsx index c30778804dfe..bf3808282a6f 100644 --- a/code/frameworks/nextjs/template/stories/Image.stories.jsx +++ b/code/frameworks/nextjs/template/stories/Image.stories.jsx @@ -48,3 +48,30 @@ export const Sized = { ], }, }; + +export const Lazy = { + args: { + src: 'https://storybook.js.org/images/placeholders/50x50.png', + width: 50, + height: 50, + }, + decorators: [ + (Story) => ( + <> +
+ {Story()} + + ), + ], +}; + +export const Eager = { + ...Lazy, + parameters: { + nextjs: { + image: { + loading: 'eager', + }, + }, + }, +};