From 8a9e83660ffaa592111e34163596794cf03038ff Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Thu, 29 Dec 2022 15:05:50 +0100 Subject: [PATCH] Add next/head support --- code/frameworks/nextjs/README.md | 7 ++++ .../nextjs/src/head-manager/decorator.tsx | 10 ++++++ .../head-manager/head-manager-provider.tsx | 22 ++++++++++++ code/frameworks/nextjs/src/preview.tsx | 12 ++++++- .../stories_default-js/Head.stories.jsx | 34 +++++++++++++++++++ 5 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 code/frameworks/nextjs/src/head-manager/decorator.tsx create mode 100644 code/frameworks/nextjs/src/head-manager/head-manager-provider.tsx create mode 100644 code/frameworks/nextjs/template/stories_default-js/Head.stories.jsx diff --git a/code/frameworks/nextjs/README.md b/code/frameworks/nextjs/README.md index af6d5950afa7..c31af8862c7e 100644 --- a/code/frameworks/nextjs/README.md +++ b/code/frameworks/nextjs/README.md @@ -31,6 +31,7 @@ - [`useSelectedLayoutSegment` and `useSelectedLayoutSegments` hook](#useselectedlayoutsegment-and-useselectedlayoutsegments-hook) - [Default Navigation Context](#default-navigation-context) - [Actions Integration Caveats](#actions-integration-caveats-1) + - [Next.js Head](#nextjs-head) - [Sass/Scss](#sassscss) - [Css/Sass/Scss Modules](#csssassscss-modules) - [Styled JSX](#styled-jsx) @@ -54,6 +55,8 @@ 👉 [Next.js Routing (next/router)](#nextjs-routing) +👉 [Next.js Head (next/head)](#nextjs-head) + 👉 [Next.js Navigation (next/navigation)](#nextjs-navigation) 👉 [Sass/Scss](#sassscss) @@ -599,6 +602,10 @@ export const parameters = { }; ``` +### Next.js Head + +[next/head](https://nextjs.org/docs/api-reference/next/head) is supported out of the box. You can use it in your stories like you would in your Next.js application. Please keep in mind, that the Head children are placed into the head element of the iframe that Storybook uses to render your stories. + ### Sass/Scss [Global sass/scss stylesheets](https://nextjs.org/docs/basic-features/built-in-css-support#sass-support) are supported without any additional configuration as well. Just import them into [preview.js](https://storybook.js.org/docs/react/configure/overview#configure-story-rendering) diff --git a/code/frameworks/nextjs/src/head-manager/decorator.tsx b/code/frameworks/nextjs/src/head-manager/decorator.tsx new file mode 100644 index 000000000000..794ad9c77cb9 --- /dev/null +++ b/code/frameworks/nextjs/src/head-manager/decorator.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; +import HeadManagerProvider from './head-manager-provider'; + +export const HeadManagerDecorator = (Story: React.FC): React.ReactNode => { + return ( + + + + ); +}; diff --git a/code/frameworks/nextjs/src/head-manager/head-manager-provider.tsx b/code/frameworks/nextjs/src/head-manager/head-manager-provider.tsx new file mode 100644 index 000000000000..6d8ab263f92c --- /dev/null +++ b/code/frameworks/nextjs/src/head-manager/head-manager-provider.tsx @@ -0,0 +1,22 @@ +import React, { useMemo } from 'react'; +import { HeadManagerContext } from 'next/dist/shared/lib/head-manager-context'; +import initHeadManager from 'next/dist/client/head-manager'; + +type HeadManagerValue = { + updateHead?: ((state: JSX.Element[]) => void) | undefined; + mountedInstances?: Set; + updateScripts?: ((state: any) => void) | undefined; + scripts?: any; + getIsSsr?: () => boolean; + appDir?: boolean | undefined; + nonce?: string | undefined; +}; + +const HeadManagerProvider: React.FC = ({ children }) => { + const headManager: HeadManagerValue = useMemo(initHeadManager, []); + headManager.getIsSsr = () => false; + + return {children}; +}; + +export default HeadManagerProvider; diff --git a/code/frameworks/nextjs/src/preview.tsx b/code/frameworks/nextjs/src/preview.tsx index ba2be7833421..dc5179bb829f 100644 --- a/code/frameworks/nextjs/src/preview.tsx +++ b/code/frameworks/nextjs/src/preview.tsx @@ -2,5 +2,15 @@ import './config/preview'; import { RouterDecorator } from './routing/decorator'; import { StyledJsxDecorator } from './styledJsx/decorator'; import './images/next-image-stub'; +import { HeadManagerDecorator } from './head-manager/decorator'; -export const decorators = [StyledJsxDecorator, RouterDecorator]; +function addNextHeadCount() { + const meta = document.createElement('meta'); + meta.name = 'next-head-count'; + meta.content = '0'; + document.head.appendChild(meta); +} + +addNextHeadCount(); + +export const decorators = [StyledJsxDecorator, RouterDecorator, HeadManagerDecorator]; diff --git a/code/frameworks/nextjs/template/stories_default-js/Head.stories.jsx b/code/frameworks/nextjs/template/stories_default-js/Head.stories.jsx new file mode 100644 index 000000000000..76c9d5983031 --- /dev/null +++ b/code/frameworks/nextjs/template/stories_default-js/Head.stories.jsx @@ -0,0 +1,34 @@ +/* eslint-disable no-undef */ +import { expect } from '@storybook/jest'; +import Head from 'next/head'; +import React from 'react'; +import { within, userEvent, waitFor } from '@storybook/testing-library'; + +function Component() { + return ( +
+ + Next.js Head Title + + + + + +

Hello world!

+
+ ); +} + +export default { + component: Component, +}; + +export const Default = { + play: async ({ canvasElement }) => { + await waitFor(() => expect(document.title).toEqual('Next.js Head Title')); + await expect(document.querySelectorAll('meta[property="og:title"]')).toHaveLength(1); + await expect(document.querySelector('meta[property="og:title"]').content).toEqual( + 'My new title' + ); + }, +};