diff --git a/.github/workflows/generate-sandboxes-next.yml b/.github/workflows/generate-sandboxes-next.yml index f6ab2f7c822f..c20f5491ef30 100644 --- a/.github/workflows/generate-sandboxes-next.yml +++ b/.github/workflows/generate-sandboxes-next.yml @@ -43,7 +43,7 @@ jobs: run: yarn wait-on http://localhost:6001 working-directory: ./code - name: Generate - run: yarn generate-sandboxes --local-registry --exclude=angular-cli/prerelease + run: yarn generate-sandboxes --local-registry --exclude=angular-cli/prerelease --debug working-directory: ./code - name: Publish run: yarn publish-sandboxes --remote=https://storybook-bot:${{ secrets.PAT_STORYBOOK_BOT}}@github.com/storybookjs/sandboxes.git --push --branch=next diff --git a/.github/workflows/prepare-patch-release.yml b/.github/workflows/prepare-patch-release.yml index 237d3f87e186..e4f8e38df502 100644 --- a/.github/workflows/prepare-patch-release.yml +++ b/.github/workflows/prepare-patch-release.yml @@ -88,15 +88,6 @@ jobs: git config --global user.email '32066757+storybook-bot@users.noreply.github.com' yarn release:pick-patches - - name: Cancel when 0 picked - if: steps.pick-patches.outputs.pr-count == '0' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # From https://stackoverflow.com/a/75809743 - run: | - gh run cancel ${{ github.run_id }} - gh run watch ${{ github.run_id }} - - name: Bump version deferred id: bump-version if: steps.unreleased-changes.outputs.has-changes-to-release == 'true' diff --git a/CHANGELOG.md b/CHANGELOG.md index febe34319967..65162adc174c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 7.4.5 + +- UI: Fix infinite hook call causing browsers to freeze - [#24291](https://github.com/storybookjs/storybook/pull/24291), thanks [@yannbf](https://github.com/yannbf)! + ## 7.4.4 - Core: Fix Promise cycle bug in useSharedState - [#24268](https://github.com/storybookjs/storybook/pull/24268), thanks [@ndelangen](https://github.com/ndelangen)! diff --git a/code/frameworks/nextjs/src/font/webpack/loader/local/get-font-face-declarations.ts b/code/frameworks/nextjs/src/font/webpack/loader/local/get-font-face-declarations.ts index 20e1df8deeb2..006c7f126f5b 100644 --- a/code/frameworks/nextjs/src/font/webpack/loader/local/get-font-face-declarations.ts +++ b/code/frameworks/nextjs/src/font/webpack/loader/local/get-font-face-declarations.ts @@ -11,7 +11,7 @@ export async function getFontFaceDeclarations(options: LoaderOptions, rootContex const localFontSrc = options.props.src as LocalFontSrc; // Parent folder relative to the root context - const parentFolder = options.filename.split('/').slice(0, -1).join('/').replace(rootContext, ''); + const parentFolder = path.dirname(options.filename).replace(rootContext, ''); const { validateData } = require('../utils/local-font-utils'); const { weight, style, variable } = validateData('', options.props); @@ -23,9 +23,13 @@ export async function getFontFaceDeclarations(options: LoaderOptions, rootContex 6 )}`; + const arePathsWin32Format = /^[a-z]:\\/iu.test(options.filename); + const cleanWin32Path = (pathString: string): string => + arePathsWin32Format ? pathString.replace(/\\/gu, '/') : pathString; + const getFontFaceCSS = () => { if (typeof localFontSrc === 'string') { - const localFontPath = path.join(parentFolder, localFontSrc); + const localFontPath = cleanWin32Path(path.join(parentFolder, localFontSrc)); return `@font-face { font-family: ${id}; @@ -34,7 +38,7 @@ export async function getFontFaceDeclarations(options: LoaderOptions, rootContex } return localFontSrc .map((font) => { - const localFontPath = path.join(parentFolder, font.path); + const localFontPath = cleanWin32Path(path.join(parentFolder, font.path)); return `@font-face { font-family: ${id}; diff --git a/code/lib/cli/src/project_types.ts b/code/lib/cli/src/project_types.ts index 25c058dee818..05c64b402423 100644 --- a/code/lib/cli/src/project_types.ts +++ b/code/lib/cli/src/project_types.ts @@ -5,11 +5,6 @@ function ltMajor(versionRange: string, major: number) { return validRange(versionRange) && minVersion(versionRange).major < major; } -function gtMajor(versionRange: string, major: number) { - // Uses validRange to avoid a throw from minVersion if an invalid range gets passed - return validRange(versionRange) && minVersion(versionRange).major > major; -} - function eqMajor(versionRange: string, major: number) { // Uses validRange to avoid a throw from minVersion if an invalid range gets passed return validRange(versionRange) && minVersion(versionRange).major === major; @@ -162,9 +157,7 @@ export const supportedTemplates: TemplateConfiguration[] = [ }, { preset: ProjectType.NEXTJS, - dependencies: { - next: (versionRange) => eqMajor(versionRange, 9) || gtMajor(versionRange, 9), - }, + dependencies: ['next'], matcherFunction: ({ dependencies }) => { return dependencies.every(Boolean); }, diff --git a/code/lib/manager-api/src/index.tsx b/code/lib/manager-api/src/index.tsx index 3b0a21bdcb94..951dc494a93a 100644 --- a/code/lib/manager-api/src/index.tsx +++ b/code/lib/manager-api/src/index.tsx @@ -397,7 +397,6 @@ export function useSharedState(stateId: string, defaultState?: S) { existingState, STORYBOOK_ADDON_STATE[stateId] ? STORYBOOK_ADDON_STATE[stateId] : defaultState ); - let quicksync = false; if (state === defaultState && defaultState !== undefined) { @@ -409,7 +408,7 @@ export function useSharedState(stateId: string, defaultState?: S) { if (quicksync) { api.setAddonState(stateId, defaultState); } - }); + }, [quicksync]); const setState = async (s: S | API_StateMerger, options?: Options) => { const result = await api.setAddonState(stateId, s, options); diff --git a/docs/configure/styling-and-css.md b/docs/configure/styling-and-css.md index b328359af00f..92ac363e6b47 100644 --- a/docs/configure/styling-and-css.md +++ b/docs/configure/styling-and-css.md @@ -35,6 +35,8 @@ CSS-in-JS libraries are designed to use basic JavaScript, and they often work in If you need webfonts to be available, you may need to add some code to the [`.storybook/preview-head.html`](./story-rendering.md#adding-to-head) file. We recommend including any assets with your Storybook if possible, in which case you likely want to configure the [static file location](./images-and-assets.md#serving-static-files-via-storybook-configuration). + + ## Troubleshooting ### Styles aren't being applied with Angular @@ -56,7 +58,7 @@ The latest Angular releases introduced significant changes in configuring and st } ``` -Additionally, if you need Storybook-specific styles that are separate from your application, you can configure the styles with [Storybook's custom builder](../get-started/install.md), which will override the application's styles: +Additionally, if you need Storybook-specific styles that are separate from your application, you can configure the styles with [Storybook's custom builder](../get-started/install.md#troubleshooting), which will override the application's styles: ```json { @@ -121,3 +123,5 @@ Starting with version `14.1.8`, Nx uses the Storybook builder directly, which me ``` When Nx runs, it will load Storybook's configuration and styling based on [`storybook`'s `browserTarget`](https://nx.dev/storybook/extra-topics-for-angular-projects#setting-up-browsertarget). + + diff --git a/docs/configure/theming.md b/docs/configure/theming.md index 5650ddc9899a..15828905eaab 100644 --- a/docs/configure/theming.md +++ b/docs/configure/theming.md @@ -2,6 +2,8 @@ title: 'Theming' --- + + Storybook is theme-able using a lightweight theming API. ## Global theming diff --git a/docs/snippets/common/storybook-addon-panel-initial.js.mdx b/docs/snippets/common/storybook-addon-panel-initial.js.mdx index 707e8bb199a7..f097d264461a 100644 --- a/docs/snippets/common/storybook-addon-panel-initial.js.mdx +++ b/docs/snippets/common/storybook-addon-panel-initial.js.mdx @@ -3,7 +3,7 @@ import React from 'react'; -import { addons, types } from '@storybook/preview-api'; +import { addons, types } from '@storybook/manager-api'; import { AddonPanel } from '@storybook/components'; diff --git a/docs/snippets/common/storybook-addons-api-usechannel.js.mdx b/docs/snippets/common/storybook-addons-api-usechannel.js.mdx index 0d66a5955350..dd6d238d1c48 100644 --- a/docs/snippets/common/storybook-addons-api-usechannel.js.mdx +++ b/docs/snippets/common/storybook-addons-api-usechannel.js.mdx @@ -7,6 +7,8 @@ import { AddonPanel, Button } from '@storybook/components'; import { STORY_CHANGED } from '@storybook/core-events'; +import { useChannel } from '@storybook/manager-api'; + export const Panel = () => { // Creates a Storybook API channel and subscribes to the STORY_CHANGED event const emit = useChannel({ diff --git a/docs/snippets/common/storybook-auto-docs-mdx-file.mdx.mdx b/docs/snippets/common/storybook-auto-docs-mdx-file.mdx.mdx index 835b3a41372e..499b7f22a570 100644 --- a/docs/snippets/common/storybook-auto-docs-mdx-file.mdx.mdx +++ b/docs/snippets/common/storybook-auto-docs-mdx-file.mdx.mdx @@ -19,7 +19,7 @@ It's often used to apply consistent positioning for content across pages in an a ## Usage - + # List diff --git a/docs/snippets/react/page-story-args-within-story.js.mdx b/docs/snippets/react/page-story-args-within-story.js.mdx new file mode 100644 index 000000000000..282e81f79cf7 --- /dev/null +++ b/docs/snippets/react/page-story-args-within-story.js.mdx @@ -0,0 +1,31 @@ +```js +// my-component/component.stories.js|jsx + +import { useArgs } from '@storybook/preview-api'; +import { Checkbox } from './checkbox'; + +export default { + title: 'Inputs/Checkbox', + component: Checkbox, +}; + +export const Example = { + args: { + isChecked: false, + label: 'Try Me!', + }, + /** + * ๐Ÿ‘‡ To avoid linting issues, it is recommended to use a function with a capitalized name. + * If you are not concerned with linting, you may use an arrow function. + */ + render: function Render(args) { + const [{ isChecked }, updateArgs] = useArgs(); + + function onChange() { + updateArgs({ isChecked: !isChecked }); + } + + return ; + }, +}; +``` diff --git a/docs/snippets/react/page-story-args-within-story.ts-4-9.mdx b/docs/snippets/react/page-story-args-within-story.ts-4-9.mdx new file mode 100644 index 000000000000..5a23cb8f523e --- /dev/null +++ b/docs/snippets/react/page-story-args-within-story.ts-4-9.mdx @@ -0,0 +1,35 @@ +```ts +// my-component/component.stories.ts|tsx + +import { StoryObj, Meta } from '@storybook/react'; +import { useArgs } from '@storybook/preview-api'; +import { Checkbox } from './checkbox'; + +const meta = { + title: 'Inputs/Checkbox', + component: Checkbox, +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +export const Example = { + args: { + isChecked: false, + label: 'Try Me!', + }, + /** + * ๐Ÿ‘‡ To avoid linting issues, it is recommended to use a function with a capitalized name. + * If you are not concerned with linting, you may use an arrow function. + */ + render: function Render(args) { + const [{ isChecked }, updateArgs] = useArgs(); + + function onChange() { + updateArgs({ isChecked: !isChecked }); + } + + return ; + }, +} satisfies Story; +``` diff --git a/docs/snippets/react/page-story-args-within-story.ts.mdx b/docs/snippets/react/page-story-args-within-story.ts.mdx new file mode 100644 index 000000000000..137e37cbf3d6 --- /dev/null +++ b/docs/snippets/react/page-story-args-within-story.ts.mdx @@ -0,0 +1,35 @@ +```ts +// my-component/component.stories.ts|tsx + +import { StoryObj, Meta } from '@storybook/react'; +import { useArgs } from '@storybook/preview-api'; +import { Checkbox } from './checkbox'; + +const meta: Meta = { + title: 'Inputs/Checkbox', + component: Checkbox, +}; +export default meta; + +type Story = StoryObj; + +export const Example: Story = { + args: { + isChecked: false, + label: 'Try Me!', + }, + /** + * ๐Ÿ‘‡ To avoid linting issues, it is recommended to use a function with a capitalized name. + * If you are not concerned with linting, you may use an arrow function. + */ + render: function Render(args) { + const [{ isChecked }, updateArgs] = useArgs(); + + function onChange() { + updateArgs({ isChecked: !isChecked }); + } + + return ; + }, +}; +``` diff --git a/docs/writing-stories/args.md b/docs/writing-stories/args.md index 807797e5675f..26a61edfb322 100644 --- a/docs/writing-stories/args.md +++ b/docs/writing-stories/args.md @@ -213,6 +213,25 @@ Similarly, special formats are available for dates and colors. Date objects will Args specified through the URL will extend and override any default values of args set on the story. + + +## Setting args from within a story + +Interactive components often need to be controlled by their containing component or page to respond to events, modify their state and reflect those changes in the UI. For example, when a user toggles a switch component, the switch should be checked, and the arg shown in Storybook should reflect the change. To enable this, you can use the `useArgs` API exported by `@storybook/preview-api`: + + + + + + + + + ## Mapping to complex arg values Complex values such as JSX elements cannot be serialized to the manager (e.g., the Controls addon) or synced with the URL. Arg values can be "mapped" from a simple string to a complex type using the `mapping` property in `argTypes` to work around this limitation. It works in any arg but makes the most sense when used with the `select` control type. diff --git a/scripts/release/pick-patches.ts b/scripts/release/pick-patches.ts index ef31087ef976..eab1743dcc7b 100644 --- a/scripts/release/pick-patches.ts +++ b/scripts/release/pick-patches.ts @@ -48,10 +48,6 @@ export const run = async (_: unknown) => { spinner.warn('No PRs found.'); } - if (process.env.GITHUB_ACTIONS === 'true') { - setOutput('pr-count', JSON.stringify(patchPRs.length)); - } - const failedCherryPicks: string[] = []; // eslint-disable-next-line no-restricted-syntax diff --git a/scripts/sandbox/generate.ts b/scripts/sandbox/generate.ts index 0e477b1a6220..22a4eccf4d14 100755 --- a/scripts/sandbox/generate.ts +++ b/scripts/sandbox/generate.ts @@ -125,6 +125,10 @@ const runGenerators = async ( localRegistry = true, debug = false ) => { + if (debug) { + console.log('Debug mode enabled. Verbose logs will be printed to the console.'); + } + console.log(`๐Ÿคนโ€โ™‚๏ธ Generating sandboxes with a concurrency of ${maxConcurrentTasks}`); const limit = pLimit(maxConcurrentTasks);