Skip to content

Conversation

@kamal0620040
Copy link

@kamal0620040 kamal0620040 commented Oct 9, 2025

Closes #32684

What I did

Fixed a bug where clicking a component in the sidebar would select a hidden first item instead of the first visible item when tag filtering is applied.

Problem

When using tag filters to hide certain stories, clicking on a component in the sidebar would still navigate to the first story in the component's children array, even if that story was filtered out. This resulted in showing a filtered/hidden story instead of the first visible one.

Solution

Modified the selectStory function in code/core/src/manager-api/modules/stories.ts to use filteredIndex instead of index when finding the leaf entry to navigate to. This ensures that only visible (non-filtered) stories are considered when selecting the first story of a component.

Changes Made

  • Changed selectStory to use filteredIndex for both main stories and refs
  • Added unit test to verify the fix works correctly

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

The new unit test (navigates to first visible story when component is selected and first story is filtered out) verifies:

  1. Creates a component with 3 stories where the first has a 'dev' tag
  2. Applies a filter to exclude stories with 'dev' tag
  3. Calls selectStory('component-a') to select the component
  4. Asserts it navigates to the first visible story instead of the hidden one

All existing selectStory tests continue to pass (14 tests total).

Manual testing

To manually test this fix:

  1. Run a sandbox template:

    yarn task --task sandbox --start-from auto --template react-vite/default-ts
  2. Create or find a component with multiple stories where the first story has a tag (e.g., dev)

  3. Open Storybook and use the tag filter icon in the sidebar to filter out stories with that tag

  4. Click on the component name in the sidebar (not individual stories)

  5. Expected behavior: The first visible/unfiltered story should be selected and displayed

  6. Before this fix: The hidden first story would be selected, showing nothing or wrong content

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. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Make sure this PR contains one of the labels below:

    Available labels
    • bug: Internal changes that fixes incorrect behavior.
    • maintenance: User-facing maintenance tasks.
    • dependencies: Upgrading (sometimes downgrading) dependencies.
    • build: Internal-facing build tooling & test updates. Will not show up in release changelog.
    • cleanup: Minor cleanup style change. Will not show up in release changelog.
    • documentation: Documentation only changes. Will not show up in release changelog.
    • feature request: Introducing a new feature.
    • BREAKING CHANGE: Changes that break compatibility in some way with current major version.
    • other: Changes that don't fit in the above categories.

🦋 Canary release

This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

core team members can create a canary release here or locally with gh workflow run --repo storybookjs/storybook publish.yml --field pr=<PR_NUMBER>

Summary by CodeRabbit

  • Bug Fixes

    • Selecting a component now opens the first visible story when earlier stories are filtered or hidden (e.g., dev-tagged entries), avoiding navigation to non-visible entries.
    • Navigation across referenced story lists now consistently respects filtered indexes, skipping filtered items and resolving selection to the correct visible story.
  • Tests

    • Added tests to confirm selection navigates to the first visible story when preceding stories are filtered out, covering both current and legacy selection pathways.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 9, 2025

📝 Walkthrough

Walkthrough

selectStory now reads and uses refs[ref].filteredIndex (instead of index) when resolving a target story and computing the navigation hash; tests were added to ensure selecting a component navigates to the first visible (non-filtered) story when earlier stories are filtered out.

Changes

Cohort / File(s) Summary
Manager API: use filteredIndex for navigation
code/core/src/manager-api/modules/stories.ts
Replaces reads of index with filteredIndex (including refs[ref].filteredIndex) when extracting state, resolving the selected story/component, and computing the navigation hash.
Tests: ensure navigation skips filtered stories
code/core/src/manager-api/tests/stories.test.ts
Adds two tests verifying that selecting a component navigates to the first visible story when earlier stories are filtered out; adds a StoryIndex type import and duplicates the scenario across selection codepaths.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant ManagerAPI as Manager API (selectStory)
  participant State as Manager State (refs.*.filteredIndex)
  participant Router

  User->>ManagerAPI: selectStory({ ref, storyId/component })
  ManagerAPI->>State: Read refs[ref].filteredIndex
  Note right of State #DDEEFF: filteredIndex contains only visible entries
  ManagerAPI->>ManagerAPI: Resolve target (use first visible story if component)
  ManagerAPI->>ManagerAPI: Compute navigation hash from filteredIndex
  ManagerAPI->>Router: navigate(hash)
  Router-->>User: URL and view updated
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f1d954 and 4b31b4c.

📒 Files selected for processing (2)
  • code/core/src/manager-api/modules/stories.ts (1 hunks)
  • code/core/src/manager-api/tests/stories.test.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Adhere to ESLint and Prettier rules across all JS/TS source files

Files:

  • code/core/src/manager-api/modules/stories.ts
  • code/core/src/manager-api/tests/stories.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Fix type errors and prefer precise typings instead of using any or suppressions, consistent with strict mode

Files:

  • code/core/src/manager-api/modules/stories.ts
  • code/core/src/manager-api/tests/stories.test.ts
code/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

code/**/*.{test,spec}.{ts,tsx}: Place all test files under the code/ directory
Name test files as *.test.ts, *.test.tsx, *.spec.ts, or *.spec.tsx

Files:

  • code/core/src/manager-api/tests/stories.test.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/spy-mocking.mdc)

**/*.test.{ts,tsx,js,jsx}: Use vi.mock() with the spy: true option for all package and file mocks in Vitest tests
Place all mocks at the top of the test file before any test cases
Use vi.mocked() to type and access mocked functions
Implement mock behaviors in beforeEach blocks
Mock all required dependencies that the test subject uses
Mock implementations should be placed in beforeEach blocks
Each mock implementation should return a Promise for async functions
Mock implementations should match the expected return type of the original function
Use vi.mocked() to access and implement mock behaviors
Mock all required properties and methods that the test subject uses
Avoid direct function mocking without vi.mocked()
Avoid mock implementations outside of beforeEach blocks
Avoid mocking without the spy: true option
Avoid inline mock implementations within test cases
Avoid mocking only a subset of required dependencies
Mock at the highest level of abstraction needed
Keep mock implementations simple and focused
Use type-safe mocking with vi.mocked()
Document complex mock behaviors
Group related mocks together

Files:

  • code/core/src/manager-api/tests/stories.test.ts
🧬 Code graph analysis (2)
code/core/src/manager-api/modules/stories.ts (1)
code/core/src/manager-api/tests/url.test.js (1)
  • store (143-151)
code/core/src/manager-api/tests/stories.test.ts (1)
code/core/src/types/modules/indexer.ts (1)
  • StoryIndex (174-177)
🔇 Additional comments (2)
code/core/src/manager-api/modules/stories.ts (1)

428-431: LGTM! Fix correctly uses filteredIndex.

The change to use filteredIndex instead of index ensures that when a component is selected, findLeafEntry will traverse only visible (non-filtered) children, correctly navigating to the first visible story instead of a potentially hidden one.

code/core/src/manager-api/tests/stories.test.ts (1)

31-31: LGTM!

The StoryIndex type import is necessary for the test entries declaration.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
code/core/src/manager-api/modules/stories.ts (1)

429-431: LGTM — filteredIndex for sidebar selection
The change ensures sidebar clicks respect active filters. For consistency, evaluate whether jumpToComponent and jumpToStory should also use filteredIndex instead of index.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a4b5af and 1f1d954.

📒 Files selected for processing (2)
  • code/core/src/manager-api/modules/stories.ts (1 hunks)
  • code/core/src/manager-api/tests/stories.test.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Adhere to ESLint and Prettier rules across all JS/TS source files

Files:

  • code/core/src/manager-api/modules/stories.ts
  • code/core/src/manager-api/tests/stories.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Fix type errors and prefer precise typings instead of using any or suppressions, consistent with strict mode

Files:

  • code/core/src/manager-api/modules/stories.ts
  • code/core/src/manager-api/tests/stories.test.ts
code/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

code/**/*.{test,spec}.{ts,tsx}: Place all test files under the code/ directory
Name test files as *.test.ts, *.test.tsx, *.spec.ts, or *.spec.tsx

Files:

  • code/core/src/manager-api/tests/stories.test.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/spy-mocking.mdc)

**/*.test.{ts,tsx,js,jsx}: Use vi.mock() with the spy: true option for all package and file mocks in Vitest tests
Place all mocks at the top of the test file before any test cases
Use vi.mocked() to type and access mocked functions
Implement mock behaviors in beforeEach blocks
Mock all required dependencies that the test subject uses
Mock implementations should be placed in beforeEach blocks
Each mock implementation should return a Promise for async functions
Mock implementations should match the expected return type of the original function
Use vi.mocked() to access and implement mock behaviors
Mock all required properties and methods that the test subject uses
Avoid direct function mocking without vi.mocked()
Avoid mock implementations outside of beforeEach blocks
Avoid mocking without the spy: true option
Avoid inline mock implementations within test cases
Avoid mocking only a subset of required dependencies
Mock at the highest level of abstraction needed
Keep mock implementations simple and focused
Use type-safe mocking with vi.mocked()
Document complex mock behaviors
Group related mocks together

Files:

  • code/core/src/manager-api/tests/stories.test.ts
🧬 Code graph analysis (2)
code/core/src/manager-api/modules/stories.ts (1)
code/core/src/manager-api/tests/url.test.js (1)
  • store (143-151)
code/core/src/manager-api/tests/stories.test.ts (1)
code/core/src/types/modules/indexer.ts (1)
  • StoryIndex (174-177)
🔇 Additional comments (1)
code/core/src/manager-api/tests/stories.test.ts (1)

31-31: LGTM! Type import is correct.

The StoryIndex type import is properly used to type the test entries and follows TypeScript best practices.

Comment on lines 1215 to 1263
it('navigates to first visible story when component is selected and first story is filtered out', async () => {
const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' };
const moduleArgs = createMockModuleArgs({ initialState });
const { api } = initStories(moduleArgs as unknown as ModuleArgs);
const { navigate, store } = moduleArgs;

const entries: StoryIndex['entries'] = {
'component-a--story-1': {
type: 'story',
subtype: 'story',
id: 'component-a--story-1',
title: 'Component A',
name: 'Story 1',
tags: ['dev'],
importPath: './component-a.ts',
},
'component-a--story-2': {
type: 'story',
subtype: 'story',
id: 'component-a--story-2',
title: 'Component A',
name: 'Story 2',
tags: [],
importPath: './component-a.ts',
},
'component-a--story-3': {
type: 'story',
subtype: 'story',
id: 'component-a--story-3',
title: 'Component A',
name: 'Story 3',
tags: [],
importPath: './component-a.ts',
},
};

await store.setState({
filters: {
'test-filter': (entry: any) => !entry.tags?.includes('dev'),
},
});

await api.setIndex({ v: 5, entries });

api.selectStory('component-a');

expect(navigate).toHaveBeenCalledWith('/story/component-a--story-2');
});
});
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Move test inside the selectStory describe block for consistency.

The test logic is correct and validates the fix well. However, it's placed outside the describe('selectStory', ...) block (which ends at line 1214), breaking the test organization pattern. All other selectStory tests are grouped within that describe block.

Apply this diff to move the test inside the describe block:

-  });
   it('navigates to first visible story when component is selected and first story is filtered out', async () => {
       const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' };
       const moduleArgs = createMockModuleArgs({ initialState });
       const { api } = initStories(moduleArgs as unknown as ModuleArgs);
       const { navigate, store } = moduleArgs;
 
       const entries: StoryIndex['entries'] = {
         'component-a--story-1': {
           type: 'story',
           subtype: 'story',
           id: 'component-a--story-1',
           title: 'Component A',
           name: 'Story 1',
           tags: ['dev'],
           importPath: './component-a.ts',
         },
         'component-a--story-2': {
           type: 'story',
           subtype: 'story',
           id: 'component-a--story-2',
           title: 'Component A',
           name: 'Story 2',
           tags: [],
           importPath: './component-a.ts',
         },
         'component-a--story-3': {
           type: 'story',
           subtype: 'story',
           id: 'component-a--story-3',
           title: 'Component A',
           name: 'Story 3',
           tags: [],
           importPath: './component-a.ts',
         },
       };
 
       await store.setState({
         filters: {
           'test-filter': (entry: any) => !entry.tags?.includes('dev'),
         },
       });
 
       await api.setIndex({ v: 5, entries });
 
       api.selectStory('component-a');
 
       expect(navigate).toHaveBeenCalledWith('/story/component-a--story-2');
     });
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('navigates to first visible story when component is selected and first story is filtered out', async () => {
const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' };
const moduleArgs = createMockModuleArgs({ initialState });
const { api } = initStories(moduleArgs as unknown as ModuleArgs);
const { navigate, store } = moduleArgs;
const entries: StoryIndex['entries'] = {
'component-a--story-1': {
type: 'story',
subtype: 'story',
id: 'component-a--story-1',
title: 'Component A',
name: 'Story 1',
tags: ['dev'],
importPath: './component-a.ts',
},
'component-a--story-2': {
type: 'story',
subtype: 'story',
id: 'component-a--story-2',
title: 'Component A',
name: 'Story 2',
tags: [],
importPath: './component-a.ts',
},
'component-a--story-3': {
type: 'story',
subtype: 'story',
id: 'component-a--story-3',
title: 'Component A',
name: 'Story 3',
tags: [],
importPath: './component-a.ts',
},
};
await store.setState({
filters: {
'test-filter': (entry: any) => !entry.tags?.includes('dev'),
},
});
await api.setIndex({ v: 5, entries });
api.selectStory('component-a');
expect(navigate).toHaveBeenCalledWith('/story/component-a--story-2');
});
});
it('navigates to first visible story when component is selected and first story is filtered out', async () => {
const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' };
const moduleArgs = createMockModuleArgs({ initialState });
const { api } = initStories(moduleArgs as unknown as ModuleArgs);
const { navigate, store } = moduleArgs;
const entries: StoryIndex['entries'] = {
'component-a--story-1': {
type: 'story',
subtype: 'story',
id: 'component-a--story-1',
title: 'Component A',
name: 'Story 1',
tags: ['dev'],
importPath: './component-a.ts',
},
'component-a--story-2': {
type: 'story',
subtype: 'story',
id: 'component-a--story-2',
title: 'Component A',
name: 'Story 2',
tags: [],
importPath: './component-a.ts',
},
'component-a--story-3': {
type: 'story',
subtype: 'story',
id: 'component-a--story-3',
title: 'Component A',
name: 'Story 3',
tags: [],
importPath: './component-a.ts',
},
};
await store.setState({
filters: {
'test-filter': (entry: any) => !entry.tags?.includes('dev'),
},
});
await api.setIndex({ v: 5, entries });
api.selectStory('component-a');
expect(navigate).toHaveBeenCalledWith('/story/component-a--story-2');
});
});
🤖 Prompt for AI Agents
In code/core/src/manager-api/tests/stories.test.ts around lines 1215 to 1263,
the test "navigates to first visible story when component is selected and first
story is filtered out" is located outside the describe('selectStory', ...) block
(which ends at line 1214); move the entire it(...) block so it sits inside that
describe('selectStory', ...) block to match the other selectStory tests and
maintain test organization and scoping. Ensure you place it before the closing
brace of the describe, preserve the test body unchanged, and update any
surrounding indentation if needed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kamal0620040, thank you for moving the test into the selectStory describe block! The test organization is now consistent with the rest of the test suite. 🎉


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@kamal0620040 kamal0620040 force-pushed the fix/filtered-story-selection branch from 1f1d954 to f2336c5 Compare October 9, 2025 17:13
@valentinpalkovic
Copy link
Contributor

Hi @kamal0620040

Thank you for your contribution. We have already worked on it internally.

Superseded by #32715.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Opening a component may select a hidden item

2 participants