diff --git a/CHANGELOG.md b/CHANGELOG.md index ef1c0ad4edbf..d93c3b996486 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 8.0.9 + +- Addon-docs: Fix MDX compilation when using `@vitejs/plugin-react-swc` with plugins - [#26837](https://github.com/storybookjs/storybook/pull/26837), thanks @JReinhold! +- CSF: Fix typings for control and other properties of argTypes - [#26824](https://github.com/storybookjs/storybook/pull/26824), thanks @kasperpeulen! +- Controls: Fix crashing when docgen extraction partially fails - [#26862](https://github.com/storybookjs/storybook/pull/26862), thanks @yannbf! +- Doc Tools: Signature Type Error Handling - [#26774](https://github.com/storybookjs/storybook/pull/26774), thanks @ethriel3695! +- Next.js: Move sharp into optional deps - [#26787](https://github.com/storybookjs/storybook/pull/26787), thanks @shuta13! +- Nextjs: Support next 14.2 useParams functionality - [#26874](https://github.com/storybookjs/storybook/pull/26874), thanks @yannbf! +- Test: Remove chai as dependency of @storybook/test - [#26852](https://github.com/storybookjs/storybook/pull/26852), thanks @kasperpeulen! +- UI: Fix sidebar search hanging when selecting a story in touch mode - [#26807](https://github.com/storybookjs/storybook/pull/26807), thanks @JReinhold! + ## 8.0.8 - Automigration: Fix name of VTA addon - [#26816](https://github.com/storybookjs/storybook/pull/26816), thanks @valentinpalkovic! diff --git a/MIGRATION.md b/MIGRATION.md index 561a9a61b899..a7959b77b8ba 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -5477,7 +5477,7 @@ Currently there is no recommended way of accessing the component options of a st ## From version 4.0.x to 4.1.x -There are are a few migrations you should be aware of in 4.1, including one unintentionally breaking change for advanced addon usage. +There are a few migrations you should be aware of in 4.1, including one unintentionally breaking change for advanced addon usage. ### Private addon config diff --git a/code/addons/controls/src/ControlsPanel.tsx b/code/addons/controls/src/ControlsPanel.tsx index 09d3bc45e44e..1f1a90cce007 100644 --- a/code/addons/controls/src/ControlsPanel.tsx +++ b/code/addons/controls/src/ControlsPanel.tsx @@ -35,8 +35,10 @@ export const ControlsPanel: FC = () => { const hasControls = Object.values(rows).some((arg) => arg?.control); const withPresetColors = Object.entries(rows).reduce((acc, [key, arg]) => { - if (arg?.control?.type !== 'color' || arg?.control?.presetColors) acc[key] = arg; - else acc[key] = { ...arg, control: { ...arg.control, presetColors } }; + const control = arg?.control; + if (typeof control !== 'object' || control?.type !== 'color' || control?.presetColors) + acc[key] = arg; + else acc[key] = { ...arg, control: { ...control, presetColors } }; return acc; }, {} as ArgTypes); diff --git a/code/addons/docs/src/preset.ts b/code/addons/docs/src/preset.ts index 31248e7af990..4b8811935ec9 100644 --- a/code/addons/docs/src/preset.ts +++ b/code/addons/docs/src/preset.ts @@ -166,7 +166,8 @@ export const viteFinal = async (config: any, options: Options) => { // add alias plugin early to ensure any other plugins that also add the aliases will override this // eg. the preact vite plugin adds its own aliases plugins.unshift(packageDeduplicationPlugin); - plugins.push(mdxPlugin(options)); + // mdx plugin needs to be before any react plugins + plugins.unshift(mdxPlugin(options)); return config; }; diff --git a/code/addons/links/package.json b/code/addons/links/package.json index bb5368c63666..c1d9bcbe1804 100644 --- a/code/addons/links/package.json +++ b/code/addons/links/package.json @@ -63,7 +63,7 @@ "prep": "node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/addon-bundle.ts" }, "dependencies": { - "@storybook/csf": "^0.1.2", + "@storybook/csf": "^0.1.4", "@storybook/global": "^5.0.0", "ts-dedent": "^2.0.0" }, diff --git a/code/addons/themes/docs/api.md b/code/addons/themes/docs/api.md index b499dd945744..0c8fc32996dd 100644 --- a/code/addons/themes/docs/api.md +++ b/code/addons/themes/docs/api.md @@ -166,7 +166,7 @@ export const withVuetifyTheme = ({ themes, defaultTheme }) => { setup() { const theme = useTheme(); - theme.global.name.value = selected; + theme.global.name.value = themes[selected]; return { theme, @@ -195,12 +195,14 @@ setup((app) => { export const decorators = [ withVuetifyTheme({ + // These keys are the labels that will be displayed in the toolbar theme switcher + // The values must match the theme keys from your VuetifyOptions themes: { light: 'light', dark: 'dark', - customTheme: 'myCustomTheme', + 'high contrast': 'highContrast', }, - defaultTheme: 'customTheme', // The key of your default theme + defaultTheme: 'light', // The key of your default theme }), ]; ``` diff --git a/code/addons/viewport/README.md b/code/addons/viewport/README.md index 3766d555f8b3..7975688745fd 100644 --- a/code/addons/viewport/README.md +++ b/code/addons/viewport/README.md @@ -22,7 +22,7 @@ export default { }; ``` -You should now be able to see the viewport addon icon in the the toolbar at the top of the screen. +You should now be able to see the viewport addon icon in the toolbar at the top of the screen. ## Usage diff --git a/code/e2e-tests/framework-nextjs.spec.ts b/code/e2e-tests/framework-nextjs.spec.ts index 61233dd5ac25..c1a15c2632ef 100644 --- a/code/e2e-tests/framework-nextjs.spec.ts +++ b/code/e2e-tests/framework-nextjs.spec.ts @@ -10,7 +10,7 @@ test.describe('Next.js', () => { // TODO: improve these E2E tests given that we have more version of Next.js to test // and this only tests nextjs/default-js test.skip( - !templateName?.includes('nextjs/default-js'), + !templateName?.includes('nextjs/default-ts'), 'Only run this test for the Frameworks that support next/navigation' ); @@ -66,7 +66,7 @@ test.describe('Next.js', () => { sbPage = new SbPage(page); await sbPage.navigateToStory( - 'stories/frameworks/nextjs-nextjs-default-js/Navigation', + 'stories/frameworks/nextjs-nextjs-default-ts/Navigation', 'default' ); root = sbPage.previewRoot(); @@ -100,7 +100,7 @@ test.describe('Next.js', () => { test.beforeEach(async ({ page }) => { sbPage = new SbPage(page); - await sbPage.navigateToStory('stories/frameworks/nextjs-nextjs-default-js/Router', 'default'); + await sbPage.navigateToStory('stories/frameworks/nextjs-nextjs-default-ts/Router', 'default'); root = sbPage.previewRoot(); }); diff --git a/code/frameworks/angular/src/client/docs/compodoc.ts b/code/frameworks/angular/src/client/docs/compodoc.ts index 9785842648d0..cc6425c6e545 100644 --- a/code/frameworks/angular/src/client/docs/compodoc.ts +++ b/code/frameworks/angular/src/client/docs/compodoc.ts @@ -200,7 +200,7 @@ const extractDefaultValueFromComments = (property: Property, value: any) => { const extractDefaultValue = (property: Property) => { try { - let value: string | boolean = property.defaultValue?.replace(/^'(.*)'$/, '$1'); + let value: string = property.defaultValue?.replace(/^'(.*)'$/, '$1'); value = castDefaultValue(property, value); if (value == null && property.jsdoctags?.length > 0) { diff --git a/code/frameworks/nextjs/package.json b/code/frameworks/nextjs/package.json index c141d0b9bde1..6b099ec73c6e 100644 --- a/code/frameworks/nextjs/package.json +++ b/code/frameworks/nextjs/package.json @@ -114,7 +114,6 @@ "resolve-url-loader": "^5.0.0", "sass-loader": "^12.4.0", "semver": "^7.3.5", - "sharp": "^0.32.6", "style-loader": "^3.3.1", "styled-jsx": "5.1.1", "ts-dedent": "^2.0.0", @@ -146,6 +145,9 @@ "optional": true } }, + "optionalDependencies": { + "sharp": "^0.33.3" + }, "engines": { "node": ">=18.0.0" }, diff --git a/code/frameworks/nextjs/src/next-image-loader-stub.ts b/code/frameworks/nextjs/src/next-image-loader-stub.ts index 6f69e8e274d3..dbc65b8fde63 100644 --- a/code/frameworks/nextjs/src/next-image-loader-stub.ts +++ b/code/frameworks/nextjs/src/next-image-loader-stub.ts @@ -2,18 +2,27 @@ import { interpolateName } from 'loader-utils'; import imageSizeOf from 'image-size'; import type { RawLoaderDefinition } from 'webpack'; import type { NextConfig } from 'next'; -import sharp from 'sharp'; import { cpus } from 'os'; +import { NextJsSharpError } from '@storybook/core-events/preview-errors'; interface LoaderOptions { filename: string; nextConfig: NextConfig; } -if (sharp.concurrency() > 1) { - // Reducing concurrency reduces the memory usage too. - const divisor = process.env.NODE_ENV === 'development' ? 4 : 2; - sharp.concurrency(Math.floor(Math.max(cpus().length / divisor, 1))); +let sharp: typeof import('sharp') | undefined; + +try { + sharp = require('sharp'); + if (sharp && sharp.concurrency() > 1) { + // Reducing concurrency reduces the memory usage too. + const divisor = process.env.NODE_ENV === 'development' ? 4 : 2; + sharp.concurrency(Math.floor(Math.max(cpus().length / divisor, 1))); + } +} catch (e) { + console.warn( + 'You have to install sharp in order to use image optimization features in Next.js. AVIF support is also disabled.' + ); } const nextImageLoaderStub: RawLoaderDefinition = async function NextImageLoader( @@ -37,10 +46,14 @@ const nextImageLoaderStub: RawLoaderDefinition = async function N let height; if (extension === 'avif') { - const transformer = sharp(content); - const result = await transformer.metadata(); - width = result.width; - height = result.height; + if (sharp) { + const transformer = sharp(content); + const result = await transformer.metadata(); + width = result.width; + height = result.height; + } else { + throw new NextJsSharpError(); + } } else { const result = imageSizeOf(this.resourcePath); width = result.width; diff --git a/code/frameworks/nextjs/src/routing/app-router-provider.tsx b/code/frameworks/nextjs/src/routing/app-router-provider.tsx index 478b8a59f9df..f81c29a5fe59 100644 --- a/code/frameworks/nextjs/src/routing/app-router-provider.tsx +++ b/code/frameworks/nextjs/src/routing/app-router-provider.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { LayoutRouterContext, AppRouterContext, @@ -7,7 +7,10 @@ import { import { PathnameContext, SearchParamsContext, + PathParamsContext, } from 'next/dist/shared/lib/hooks-client-context.shared-runtime'; +import { type Params } from 'next/dist/shared/lib/router/utils/route-matcher'; +import { PAGE_SEGMENT_KEY } from 'next/dist/shared/lib/segment'; import type { FlightRouterState } from 'next/dist/server/app-render/types'; import type { RouteParams } from './types'; @@ -16,6 +19,32 @@ type AppRouterProviderProps = { routeParams: RouteParams; }; +// Since Next 14.2.x +// https://github.com/vercel/next.js/pull/60708/files#diff-7b6239af735eba0c401e1a0db1a04dd4575c19a031934f02d128cf3ac813757bR106 +function getSelectedParams(currentTree: FlightRouterState, params: Params = {}): Params { + const parallelRoutes = currentTree[1]; + + for (const parallelRoute of Object.values(parallelRoutes)) { + const segment = parallelRoute[0]; + const isDynamicParameter = Array.isArray(segment); + const segmentValue = isDynamicParameter ? segment[1] : segment; + if (!segmentValue || segmentValue.startsWith(PAGE_SEGMENT_KEY)) continue; + + // Ensure catchAll and optional catchall are turned into an array + const isCatchAll = isDynamicParameter && (segment[2] === 'c' || segment[2] === 'oc'); + + if (isCatchAll) { + params[segment[0]] = segment[1].split('/'); + } else if (isDynamicParameter) { + params[segment[0]] = segment[1]; + } + + params = getSelectedParams(parallelRoute, params); + } + + return params; +} + const getParallelRoutes = (segmentsList: Array): FlightRouterState => { const segment = segmentsList.shift(); @@ -34,62 +63,67 @@ export const AppRouterProvider: React.FC { + return getSelectedParams(tree); + }, [tree]); // https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/app-router.tsx#L436 return ( - - - - + + + { - action('nextNavigation.refresh')(); + buildId: 'storybook', + tree, + focusAndScrollRef: { + apply: false, + hashFragment: null, + segmentPaths: [tree], + onlyHashChange: false, }, - ...restRouteParams, + nextUrl: pathname, }} > - { + action('nextNavigation.refresh')(); + }, + ...restRouteParams, }} > - {children} - - - - - + + {children} + + + + + + ); }; diff --git a/code/frameworks/nextjs/template/stories_nextjs-default-js/Head.stories.jsx b/code/frameworks/nextjs/template/stories_nextjs-default-js/Head.stories.jsx index 1e43bb39eba6..9bc032037c60 100644 --- a/code/frameworks/nextjs/template/stories_nextjs-default-js/Head.stories.jsx +++ b/code/frameworks/nextjs/template/stories_nextjs-default-js/Head.stories.jsx @@ -22,7 +22,7 @@ export default { }; export const Default = { - play: async ({ canvasElement }) => { + play: async () => { 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( diff --git a/code/frameworks/nextjs/template/stories_nextjs-default-js/Navigation.stories.jsx b/code/frameworks/nextjs/template/stories_nextjs-default-js/Navigation.stories.jsx deleted file mode 100644 index 166567aa456c..000000000000 --- a/code/frameworks/nextjs/template/stories_nextjs-default-js/Navigation.stories.jsx +++ /dev/null @@ -1,124 +0,0 @@ -import { - useRouter, - usePathname, - useSearchParams, - useParams, - useSelectedLayoutSegment, - useSelectedLayoutSegments, -} from 'next/navigation'; -import React from 'react'; - -function Component() { - const router = useRouter(); - const pathname = usePathname(); - const searchParams = useSearchParams(); - const params = useParams(); - const segment = useSelectedLayoutSegment(); - const segments = useSelectedLayoutSegments(); - - const searchParamsList = searchParams ? Array.from(searchParams.entries()) : []; - - const routerActions = [ - { - cb: () => router.back(), - name: 'Go back', - }, - { - cb: () => router.forward(), - name: 'Go forward', - }, - { - cb: () => router.prefetch('/prefetched-html'), - name: 'Prefetch', - }, - { - cb: () => router.push('/push-html', { forceOptimisticNavigation: true }), - name: 'Push HTML', - }, - { - cb: () => router.refresh(), - name: 'Refresh', - }, - { - cb: () => router.replace('/replaced-html', { forceOptimisticNavigation: true }), - name: 'Replace', - }, - ]; - - return ( -
-
pathname: {pathname}
-
segment: {segment}
-
segments: {segments.join(',')}
-
- searchparams:{' '} -
    - {searchParamsList.map(([key, value]) => ( -
  • - {key}: {value} -
  • - ))} -
-
-
- params:{' '} -
    - {Object.entries(params).map(([key, value]) => ( -
  • - {key}: {value} -
  • - ))} -
-
- {routerActions.map(({ cb, name }) => ( -
- -
- ))} -
- ); -} - -export default { - component: Component, - parameters: { - nextjs: { - appDirectory: true, - navigation: { - pathname: '/hello', - query: { - foo: 'bar', - }, - }, - }, - }, -}; - -export const Default = {}; - -export const WithSegmentDefined = { - parameters: { - nextjs: { - appDirectory: true, - navigation: { - segments: ['dashboard', 'settings'], - }, - }, - }, -}; - -export const WithSegmentDefinedForParams = { - parameters: { - nextjs: { - appDirectory: true, - navigation: { - segments: [ - ['slug', 'hello'], - ['framework', 'nextjs'], - ], - }, - }, - }, -}; diff --git a/code/frameworks/nextjs/template/stories_nextjs-default-ts/Navigation.stories.tsx b/code/frameworks/nextjs/template/stories_nextjs-default-ts/Navigation.stories.tsx index 6287d5ae2a73..f675ad7181ef 100644 --- a/code/frameworks/nextjs/template/stories_nextjs-default-ts/Navigation.stories.tsx +++ b/code/frameworks/nextjs/template/stories_nextjs-default-ts/Navigation.stories.tsx @@ -1,5 +1,11 @@ -// usePathname and useSearchParams are only usable if experimental: {appDir: true} is set in next.config.js -import { useRouter, usePathname, useSearchParams } from 'next/navigation'; +import { + useRouter, + usePathname, + useSearchParams, + useParams, + useSelectedLayoutSegment, + useSelectedLayoutSegments, +} from 'next/navigation'; import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; @@ -7,6 +13,9 @@ function Component() { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); + const params = useParams(); + const segment = useSelectedLayoutSegment(); + const segments = useSelectedLayoutSegments(); const searchParamsList = searchParams ? Array.from(searchParams.entries()) : []; @@ -42,6 +51,8 @@ function Component() { return (
pathname: {pathname}
+
segment: {segment}
+
segments: {segments.join(',')}
searchparams:{' '}
    @@ -52,6 +63,16 @@ function Component() { ))}
+
+ params:{' '} +
    + {Object.entries(params).map(([key, value]) => ( +
  • + {key}: {value} +
  • + ))} +
+
{routerActions.map(({ cb, name }) => (