Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
- [Sidebar rewrite: manager API changes](#sidebar-rewrite-manager-api-changes)
- [Escape is no longer a configurable shortcut](#escape-is-no-longer-a-configurable-shortcut)
- ['Create new story' UI feature removed](#create-new-story-ui-feature-removed)
- [Sidebar: standalone docs pages are no longer wrapped in a component](#sidebar-standalone-docs-pages-are-no-longer-wrapped-in-a-component)
- [Test runner support ended](#test-runner-support-ended)
- [Vitest Addon: requires Vitest 4.0 or higher](#vitest-addon-requires-vitest-40-or-higher)
- [Vitest Addon: `setProjectAnnotations` must not be called in setup files](#vitest-addon-setprojectannotations-must-not-be-called-in-setup-files)
Expand Down Expand Up @@ -1155,6 +1156,35 @@ Saving a story from the Controls panel is unchanged: you can still adjust contro

The channel events `CREATE_NEW_STORYFILE_REQUEST`, `CREATE_NEW_STORYFILE_RESPONSE`, `FILE_COMPONENT_SEARCH_REQUEST` and `FILE_COMPONENT_SEARCH_RESPONSE`, their payload types in `storybook/internal/core-events`, and the `generateStoryFile` export from `storybook/internal/core-server` are removed too.

### Sidebar: standalone docs pages are no longer wrapped in a component

A docs page that doesn't belong to a component (for example an MDX file with `<Meta title="Introduction" />`) used to appear in the manager index as a component with a single `Docs` child.
It is now a single `docs` entry.
Addons that read `useStorybookState().index`, `sidebar.renderLabel` or sidebar filters see that entry directly.

More generally, the manager index (`useStorybookState().index` and `filteredIndex`) used to type every title by its position, so the last segment of a title was always a `component`.

Each title is now typed by everything the story index puts under it:

| Under the title | Entry in the manager index |
| --- | --- |
| Other titles | `group` (or `root`) |
| At least one story | `component` |
| Exactly one docs page and no stories | the `docs` entry itself, with the title's `name`, `depth` and `parent` (`undefined` at the top level) |
| Several docs pages and no stories | `group` |

The type comes from the full index, so `index` and `filteredIndex` always agree on it.
Filters only remove entries: a component whose stories are all filtered out stays a `component` with its docs child in `filteredIndex`.
The sidebar still shows such a component as a single docs page, for example for [docs-only stories](https://storybook.js.org/docs/writing-stories/tags#docs-only-stories).

Functions passed to `sidebar.renderLabel` and sidebar filters receive the `docs` entry instead of the old wrapper and its `Docs` child.
Sidebar search lists these pages under their own name, for example `Introduction` instead of `Docs` under `Introduction`.
`api.selectStory('Introduction')` and `linkTo('Introduction')` still resolve to the docs page.

`api.jumpToComponent` is renamed to `api.jumpToSubtree`, because it now also stops on docs pages that are not attached to a component.
Its menu entries and shortcut labels read `Previous component or page` and `Next component or page`.
The shortcut keys `prevComponent` and `nextComponent` keep their names, so customized shortcuts carry over.

### Test runner support ended

Official support for [`@storybook/test-runner`](https://github.com/storybookjs/test-runner) has ended. The package stays published and accepts Storybook 11 and later as a peer dependency, so existing setups can keep running it at their own risk, but it no longer receives fixes or compatibility updates and prints a warning on every run.
Expand Down
90 changes: 90 additions & 0 deletions code/core/src/manager-api/lib/stories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,4 +243,94 @@ describe('transformStoryIndexToStoriesHash', () => {
expect(result['story-2']).toBeUndefined();
expect(result['2']).toBeUndefined();
});

describe('title types', () => {
const options = {
provider: { getConfig: () => ({ sidebar: {} }) } as any,
docsOptions: { docsMode: false },
filters: {},
allStatuses: {},
};
const docs = (id: string, title: string, name = 'Docs') => ({
id,
type: 'docs' as const,
title,
name,
importPath: `./${id}.mdx`,
storiesImports: [],
tags: [],
});
const index: API_PreparedStoryIndex = {
v: 5,
entries: {
'welcome--docs': docs('welcome--docs', 'Welcome'),
'guides-intro--docs': docs('guides-intro--docs', 'Guides/Intro'),
'guides-faq--setup': docs('guides-faq--setup', 'Guides/FAQ', 'Setup'),
'guides-faq--usage': docs('guides-faq--usage', 'Guides/FAQ', 'Usage'),
'guides-button--docs': docs('guides-button--docs', 'Guides/Button'),
'guides-button--primary': {
id: 'guides-button--primary',
type: 'story',
subtype: 'story',
title: 'Guides/Button',
name: 'Primary',
importPath: './button.stories.ts',
tags: [],
},
},
};
const result = transformStoryIndexToStoriesHash(index, options);

it('puts a single unattached docs page in place of its title', () => {
expect(result['guides-intro']).toBeUndefined();
expect(result['guides-intro--docs']).toMatchObject({
name: 'Intro',
parent: 'guides',
depth: 1,
});
expect(result['guides']).toMatchObject({
children: ['guides-intro--docs', 'guides-faq', 'guides-button'],
});
});

it('puts a root-level unattached docs page at the root', () => {
expect(result['welcome']).toBeUndefined();
expect(result['welcome--docs']).toMatchObject({
name: 'Welcome',
parent: undefined,
depth: 0,
});
});

it('makes a title with several docs pages and no stories a group', () => {
expect(result['guides-faq']).toMatchObject({
type: 'group',
children: ['guides-faq--setup', 'guides-faq--usage'],
});
expect(result['guides-faq--setup']).toMatchObject({ name: 'Setup', parent: 'guides-faq' });
});

it('keeps the component of a docs page that has stories', () => {
expect(result['guides-button']).toMatchObject({
type: 'component',
children: ['guides-button--docs', 'guides-button--primary'],
});
expect(result['guides-button--docs']).toMatchObject({
name: 'Docs',
parent: 'guides-button',
});
});

it('keeps the component when filters hide all of its stories', () => {
const filtered = transformStoryIndexToStoriesHash(index, {
...options,
filters: { hideStories: (entry) => entry.type !== 'story' },
});
expect(filtered['guides-button']).toMatchObject({
type: 'component',
children: ['guides-button--docs'],
});
expect(filtered['guides-button--primary']).toBeUndefined();
});
});
});
Loading
Loading