Skip to content

Manager: Type index titles by their contents - #36659

Merged
valentinpalkovic merged 6 commits into
nextfrom
valentin/manager-index-title-types
Oct 20, 2026
Merged

valentinpalkovic merged 6 commits into
nextfrom
valentin/manager-index-title-types

Conversation

@valentinpalkovic

@valentinpalkovic valentinpalkovic commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Closes #35513
Supersedes #35522

What I did

The manager index types every title by its position: the last segment of a title always becomes a component.
An unattached MDX page therefore gets a fake component wrapper with a single Docs child.
The sidebar hides that wrapper at render time, but search, keyboard navigation and addons see it, so in search the Typography page shows up as "Docs" under the path brand / Typography.

This rebuilds the manager hash so that each title is typed by what the full index puts under it.
The hash then matches what the sidebar renders, and filters can no longer change its shape.

index entries (all, before filters)
   │  split titles into paths
   ▼
untyped title nodes ── typeOf(node) ──▶ root | group | component | docs (the page takes the title's slot)
   │  prune entries the filters remove; a filter never changes a type
   ▼
manager hash (index and filteredIndex have the same shape)

The rule that decides it:

const typeOf = (node: TitleNode): TitleNodeType => {
  if (node.isRoot) return 'root';
  if (node.children.some((id) => titleNodes.has(id))) return 'group';
  if (node.children.some((id) => entriesById.get(id)!.type === 'story')) return 'component';
  return node.children.length === 1 ? 'docs' : 'group';
};

Manager hash of the internal Storybook, next against this branch, every entry dumped through api.resolveStory and diffed (children lists omitted):

- brand-typography        type=component name=Typography parent=brand            depth=1
- brand-typography--docs  type=docs      name=Docs       parent=brand-typography depth=2
+ brand-typography--docs  type=docs      name=Typography parent=brand            depth=1
- addons-docs-docs2-error        type=component name=Error parent=addons-docs-docs2       depth=3
- addons-docs-docs2-error--docs  type=docs      name=Docs  parent=addons-docs-docs2-error depth=4
+ addons-docs-docs2-error--docs  type=docs      name=Error parent=addons-docs-docs2       depth=3

Across 2,129 entries the only differences are 10 removed wrappers, their docs pages taking their place, and the parents' children lists.
Tags, import paths and every story, test and component entry are identical.

Sidebar search for "typography":

- brand-typography--docs   Docs        brand / Typography
+ brand-typography--docs   Typography  brand

Why the type comes from the full index

#35522 hoisted wrappers after filtering, which made filteredIndex and index disagree.
With every story of a component filtered out, index still had the component while filteredIndex did not, so api.selectStory('Button') and linkTo('Button') silently did nothing.
Captured from a unit test against that branch:

AssertionError: expected [] to deeply equal [ 'expected docs nav' ]

Here a component whose stories are all filtered out stays a component with its docs child, and navigation works.
A regression test covers this and fails when the type is computed from the filtered set.

Follow-on changes

  • The sidebar no longer hoists single-docs components at render time. The single-story hoist stays in the Tree.
  • The search-layer dedup workaround from Search: Show docs stories as docs search result item instead of a story-group search result item #35495 is removed. The e2e test from that PR passes without it.
  • getComponentLookupList treats a docs entry without a component parent as its own group, so Alt+ArrowUp/Down still lands on these pages.
  • selectStory(title) and linkTo(title) fall back to the docs entry with that title, because no hash key equals the title anymore.
  • API_DocsEntry.parent is optional, because a top-level MDX page has no parent.
  • A title with several docs pages and no stories is now a group instead of a component.
  • A deep link to a title id such as ?path=/docs/brand-typography used to resolve through the wrapper. The STORY_SPECIFIED handler now follows the entry the preview resolved, so the link still opens the page instead of "Couldn't find story matching id".

Breaking change for addons

useStorybookState().index and filteredIndex change shape for docs-only titles, so this is listed in MIGRATION.md under 11.0.
sidebar.renderLabel and sidebar filter functions receive the docs entry instead of the wrapper and its Docs child.
api.jumpToComponent is renamed to api.jumpToSubtree.

QA walkthrough

Every step below ran in a real browser against the internal Storybook with a few extra fixtures (on the valentin/manager-index-title-types-qa branch, one commit on top of this PR): a top-level MDX page, two MDX pages sharing a title, an MDX page sharing a title with a CSF file, and stories that call linkTo.
The recording is the Playwright run of the same steps; each step asserts its result, so the video only exists because every step passed.
The top-right banner says what is being tested, the bottom-right badge shows the key combination a user presses, and the bottom caption shows the result.

qa-36659.mp4

Checklist from #35513

Core change

Keyboard navigation

  • Rename jumpToComponent to jumpToSubtree. The shortcut keys prevComponent / nextComponent keep their names so customized bindings carry over.
  • getComponentLookupList treats a docs page without a component parent as its own group.
  • Refactor findSiblingStoryId instead. Not needed, the item above covers it.
  • Neighbour preloading: selecting the Button story preloads ["guides-faq--setup","welcome--docs"].
  • Alt+ArrowUp/Down land on and leave docs pages in both directions; both list ends stay put.
  • New copy for the menu and shortcut labels: Previous component or page / Next component or page.

Navigation and API

  • selectStory / linkTo by title: linkTo('Welcome'), linkTo('Guides/Intro'), linkTo('Guides/FAQ') (first page of the group).
  • Legacy permalinks: linkTo('Guides/Intro', 'Docs'), selectStory('Guides/Button', 'Primary'), selectStory('Guides/FAQ', 'Usage'); an unknown title still throws.
  • findLeafEntry call sites: selectStory('Guides/Button') opens its docs page while every story is filtered out.
  • Test entries: the full-hash diff shows every story and test entry unchanged.
  • importPath: set on components and on each hoisted page; "Open in editor" is offered on docs pages.

Consumers

  • Deep links with partial ids: ?path=/docs/guides-intro and ?path=/docs/welcome were broken by the change and are fixed here, with an e2e test.
  • Status rollup: components still show their stories' status; docs pages carry none.
  • sidebar.renderLabel / renderAriaLabel receive the docs entry (docs Welcome); documented in MIGRATION.md.
  • Composed refs: the Icons ref's top-level Introduction page is hoisted too; old index versions are covered by the refs unit tests.
  • Mobile navigation (bottom bar and menu), keyboard tree navigation (ArrowUp/Down, Enter). ArrowLeft/Right never collapsed or expanded on next either; that is fixed separately in Manager: Fix ArrowLeft and ArrowRight not collapsing sidebar nodes #36660.
  • Raw index.json: unchanged (pages still named Docs, no wrappers).

Edge cases

  • Several docs pages sharing a title become a group (Guides/FAQ).
  • A CSF file and an MDX page sharing a title stay a component (Guides/Button with Notes).
  • Stories removed only by filters: the shape no longer depends on filters; Button stays a component under the Documentation filter.
  • Docs mode (--docs): a title left with one page collapses into it; FAQ stays a group.
  • Name divergence: legacy linkTo(title, 'Docs') still resolves.

Tests

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

  1. Run yarn storybook:ui from the code/ directory.
  2. Open http://localhost:6006/?path=/docs/brand-typography--docs.
  3. In the sidebar, check that "Color Palette" and "Typography" under "Brand" are docs pages with no folder around them.
  4. Type typography in the sidebar search. Check that the first result is "Typography" with the path "brand", not "Docs".
  5. Type Monospace. Check that the heading result reads "Typography / Monospace".
  6. With "Typography" selected, press Alt+ArrowUp. Check that "Color Palette" opens.
  7. Open "Addons / Docs / Docs2" in the sidebar. Check that "Error", "No Title" and "Tags" are docs pages directly under "Docs2".

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update MIGRATION.MD

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes.
  • Declare whether manual QA will be needed for this PR during the next release, through qa:needed or qa:skip
  • Make sure this PR contains one of the change category labels (feature request, bug, maintenance, etc.).

🤖 Generated with Claude Code

The manager index typed every title by its position, so an unattached
MDX page got a synthetic component wrapper that the sidebar hid at
render time and search, keyboard navigation and addons did not.

Build untyped title nodes from the full index first, then type each
node from what sits under it: other titles make a group, any story
makes a component, a single docs page takes the title's place, and
several docs pages make a group. Filters only prune entries, so index
and filteredIndex always agree on the shape.

The sidebar's render-time docs hoist and the search-layer workaround
from #35495 are no longer needed and are removed.

Closes #35513

Co-authored-by: Prajakta Ukirde <165979795+prajaktaukirde@users.noreply.github.com>
@valentinpalkovic valentinpalkovic added bug ci:normal Run our default set of CI jobs (choose this for most PRs). qa:needed Pull Requests that will need manual QA prior to release. labels Oct 7, 2026
A URL such as ?path=/docs/brand-typography used to resolve through the
component wrapper. Without the wrapper the id is not in the index, so
the STORY_SPECIFIED handler treated it as a known leaf, pushed it back
to the preview and showed "Couldn't find story matching id".

Only a story or docs entry counts as the current selection now, so the
manager follows the entry the preview resolved from the partial id.
@storybook-app-bot

storybook-app-bot Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Package Benchmarks

Commit: 525bc13, ran on 19 October 2026 at 14:03:05 UTC

No significant changes detected, all good. 👏

The jump also stops on docs pages without a component, so the name and
the menu and shortcut labels now say so. The shortcut keys keep their
names so customized bindings carry over.
@Sidnioulz
Sidnioulz self-requested a review October 19, 2026 07:57
@Sidnioulz Sidnioulz self-assigned this Oct 19, 2026
Comment thread code/core/src/manager-api/modules/stories.ts Outdated
Comment thread code/core/src/manager-api/modules/stories.ts
Comment thread code/core/src/manager-api/modules/stories.ts Outdated
Comment thread code/core/src/manager/components/sidebar/Search.tsx
Comment thread code/core/src/manager/components/sidebar/Tree.tsx Outdated
Comment thread MIGRATION.md Outdated
@valentinpalkovic
valentinpalkovic marked this pull request as ready for review October 19, 2026 13:06
A component whose stories are all hidden, for example with autodocs and
'!dev' on the meta, is left with only its docs entry after filtering.
The sidebar showed it as one docs page before the index change; restore
that render-time hoist so the documented docs-only pattern looks the
same.
…x-title-types

# Conflicts:
#	MIGRATION.md
#	code/core/src/manager/components/sidebar/Tree.stories.tsx
#	code/core/src/manager/components/sidebar/Tree.tsx
Rename the leaf check in the STORY_SPECIFIED handler to isLeaf, and use
the established 'unattached docs page' term in the jumpToSubtree JSDoc.
@valentinpalkovic
valentinpalkovic merged commit 1f1cdf0 into next Oct 20, 2026
139 of 144 checks passed
@valentinpalkovic
valentinpalkovic deleted the valentin/manager-index-title-types branch October 20, 2026 10:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug ci:normal Run our default set of CI jobs (choose this for most PRs). qa:needed Pull Requests that will need manual QA prior to release.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Index: Remove synthetic component wrappers for unattached docs

2 participants