Skip to content

Docs: Fix Hash Anchor Scrolling#35064

Closed
mturac wants to merge 2 commits into
storybookjs:nextfrom
mturac:fix/issue-15934-anchor-hash-navigation
Closed

Docs: Fix Hash Anchor Scrolling#35064
mturac wants to merge 2 commits into
storybookjs:nextfrom
mturac:fix/issue-15934-anchor-hash-navigation

Conversation

@mturac

@mturac mturac commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • keep same-page docs hash links inside the preview document by scrolling the target element instead of emitting manager navigation
  • apply the same behavior to heading anchor links
  • add focused coverage for hash links, missing targets, target blank hash links, external links, and heading anchors

Fixes #15934

Testing

  • npx vitest run --config code/addons/docs/vitest.config.ts --no-cache code/addons/docs/src/blocks/blocks/mdx.test.tsx

Manual testing

  • Not run; covered by the focused docs block unit tests above.

Summary by CodeRabbit

  • Tests

    • Added comprehensive tests for MDX anchor and heading scroll behaviors, covering hash links, missing targets, external links, and target="_blank" handling.
  • Bug Fixes

    • In-page anchor links no longer emit navigation events and now smoothly scroll directly to targets.
    • Heading anchors updated to perform smooth scroll and use an aria label indicating "Scroll to heading".

@mturac mturac changed the title Fix docs hash anchor scrolling Docs: Fix Hash Anchor Scrolling Jun 5, 2026
@mturac

mturac commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

Updated the title to match the expected format. The remaining Danger failures are label-gate items I cannot set from the fork: bug, ci:normal, and qa:skip.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7782eb1a-365a-410c-9808-e06f70b7682d

📥 Commits

Reviewing files that changed from the base of the PR and between 875a4a1 and 04072a1.

📒 Files selected for processing (2)
  • code/addons/docs/src/blocks/blocks/mdx.test.tsx
  • code/addons/docs/src/blocks/blocks/mdx.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • code/addons/docs/src/blocks/blocks/mdx.tsx
  • code/addons/docs/src/blocks/blocks/mdx.test.tsx

📝 Walkthrough

Walkthrough

This PR updates MDX anchor and heading components to scroll to in-page targets directly instead of emitting navigation events. The scrollToElement utility is imported and click handlers are updated in both AnchorInPage and HeaderWithOcticonAnchor components. Comprehensive test coverage validates scrolling behavior, target detection, and external link handling.

Changes

In-page scroll behavior for MDX anchors and headings

Layer / File(s) Summary
Scroll implementation in anchors and headings
code/addons/docs/src/blocks/blocks/mdx.tsx
Import scrollToElement utility and update AnchorInPage and HeaderWithOcticonAnchor components to prevent default navigation, scroll to resolved in-page elements, and remove DocsContext usage for navigation.
Test coverage for scroll behavior
code/addons/docs/src/blocks/blocks/mdx.test.tsx
Set up test mocks for DocsContext and dependencies, then verify that hash link clicks scroll to target elements with smooth parameters, external links preserve default behavior, missing targets are handled correctly, and heading anchors scroll into view.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • storybookjs/storybook#34945: Both PRs modify addons/docs/src/blocks/blocks/mdx.tsx’s heading/anchor rendering—one changes click/navigation behavior to scroll in-page, while the other refactors heading anchor wrapper positioning/styling (and adjusts the <a> target attribute).
  • storybookjs/storybook#34368: Both PRs modify the MDX heading/anchor implementation in code/addons/docs/src/blocks/blocks/mdx.tsx (the click handler/behavior of the heading anchor and its surrounding component), so the changes are code-level related.
  • storybookjs/storybook#34271: The main PR changes MDX hash/heading anchor click handlers to prevent default and scroll to a target element by its id, while the retrieved PR changes how heading/subheading id values are generated via a docs-scoped slugger context—i.e., they both operate on the same id targets used for the scrolling behavior.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
code/addons/docs/src/blocks/blocks/mdx.tsx (1)

212-219: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the heading anchor aria label to match actual behavior.

The button no longer updates the URL/hash (default is prevented); it scrolls in-page instead. "Copy heading URL to address bar" is now misleading for assistive tech.

Suggested patch
-            ariaLabel="Copy heading URL to address bar"
+            ariaLabel="Scroll to heading"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code/addons/docs/src/blocks/blocks/mdx.tsx` around lines 212 - 219, The aria
label "Copy heading URL to address bar" is inaccurate because the anchor's
onClick prevents default and scrolls in-page; update the aria label on the
anchor element (the <a> with href={hash} and onClick={(event:
MouseEvent<HTMLAnchorElement>) => { event.preventDefault(); const element =
document.getElementById(id); ... }}) to reflect the actual behavior (e.g.,
"Scroll to heading" or "Jump to heading") so assistive tech describes the action
correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@code/addons/docs/src/blocks/blocks/mdx.test.tsx`:
- Around line 146-169: The test patches Element.prototype.scrollIntoView but
restores it only at the end, so an early throw would leave the global patched;
fix by wrapping the patch, render, interactions, and assertions in a try/finally
block where originalScrollIntoView (captured before assignment) is restored in
the finally clause; reference the local variables scrollIntoView and
originalScrollIntoView and the test code that calls render(<HeaderMdx ...>),
queries the anchor, fireEvent.click(anchor!), and asserts scrollIntoView calls —
move those steps inside try and place Element.prototype.scrollIntoView =
originalScrollIntoView in finally.
- Around line 13-59: Add the { spy: true } option to each vi.mock(...) call (the
mocks for './DocsContext', '../components', 'storybook/internal/components',
'`@storybook/icons`', and 'storybook/theming') and update usages to access mocked
exports via vi.mocked(...) for type safety; specifically wrap the mock factory
returns so callers can do const MockedDocsContext = vi.mocked(DocsContext) or
vi.mocked(LinkIcon) as needed, and keep the existing mock implementations for
Source, MockAnchor, styledFactory/styled, Button/Code/components, and LinkIcon
unchanged except for switching to vi.mock(..., { spy: true }) and using
vi.mocked(...) where the tests read from those modules.

---

Outside diff comments:
In `@code/addons/docs/src/blocks/blocks/mdx.tsx`:
- Around line 212-219: The aria label "Copy heading URL to address bar" is
inaccurate because the anchor's onClick prevents default and scrolls in-page;
update the aria label on the anchor element (the <a> with href={hash} and
onClick={(event: MouseEvent<HTMLAnchorElement>) => { event.preventDefault();
const element = document.getElementById(id); ... }}) to reflect the actual
behavior (e.g., "Scroll to heading" or "Jump to heading") so assistive tech
describes the action correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b86bbcf5-03e2-49c6-b300-dd3309af79cf

📥 Commits

Reviewing files that changed from the base of the PR and between 13ce265 and 875a4a1.

📒 Files selected for processing (2)
  • code/addons/docs/src/blocks/blocks/mdx.test.tsx
  • code/addons/docs/src/blocks/blocks/mdx.tsx

Comment on lines +13 to +59
vi.mock('./DocsContext', () => ({
DocsContext: React.createContext({
channel: { emit: emitMock },
}),
}));

vi.mock('../components', () => ({
Source: ({ children }: any) => <pre>{children}</pre>,
}));

vi.mock('storybook/internal/components', () => {
const MockAnchor = React.forwardRef<HTMLAnchorElement, any>(({ children, ...props }, ref) => (
<a ref={ref} {...props}>
{children}
</a>
));
MockAnchor.displayName = 'MockAnchor';

return {
Button: ({ children }: any) => <>{children}</>,
Code: ({ children }: any) => <code>{children}</code>,
components: {
a: MockAnchor,
},
nameSpaceClassNames: (props: any) => props,
};
});

vi.mock('@storybook/icons', () => ({
LinkIcon: () => <span data-testid="link-icon" />,
}));

vi.mock('storybook/theming', () => {
const styledFactory = (tag: React.ElementType) => () => {
const Styled = React.forwardRef<any, any>(({ children, ...props }, ref) =>
React.createElement(tag, { ...props, ref }, children)
);
Styled.displayName = `Styled${typeof tag === 'string' ? tag : 'Component'}`;
return Styled;
};

return {
styled: new Proxy(styledFactory, {
get: (_target, tag) => styledFactory(tag as string),
}),
};
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update Vitest module mocks to match the repo spy-mocking contract in code/addons/docs/src/blocks/blocks/mdx.test.tsx.

Add the required { spy: true } option to the vi.mock() calls (e.g., ./DocsContext, ../components, storybook/internal/components, @storybook/icons, storybook/theming) and use vi.mocked() for type-safe access to the mocked exports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code/addons/docs/src/blocks/blocks/mdx.test.tsx` around lines 13 - 59, Add
the { spy: true } option to each vi.mock(...) call (the mocks for
'./DocsContext', '../components', 'storybook/internal/components',
'`@storybook/icons`', and 'storybook/theming') and update usages to access mocked
exports via vi.mocked(...) for type safety; specifically wrap the mock factory
returns so callers can do const MockedDocsContext = vi.mocked(DocsContext) or
vi.mocked(LinkIcon) as needed, and keep the existing mock implementations for
Source, MockAnchor, styledFactory/styled, Button/Code/components, and LinkIcon
unchanged except for switching to vi.mock(..., { spy: true }) and using
vi.mocked(...) where the tests read from those modules.

Comment thread code/addons/docs/src/blocks/blocks/mdx.test.tsx
@mturac

mturac commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review follow-up in the latest commit: the heading anchor label now matches the scroll behavior, and the test restores the patched scrollIntoView prototype in a finally block. I left the mock spy/type-safety suggestion unchanged since these mocks are local stubs and the focused test coverage still exercises the changed behavior.\n\nFocused docs test passes locally.

@Sidnioulz Sidnioulz self-assigned this Jun 5, 2026
@Sidnioulz Sidnioulz added bug addon: docs ci:normal Run our default set of CI jobs (choose this for most PRs). qa:skip Pull Requests that do not need any QA. labels Jun 5, 2026
@Sidnioulz Sidnioulz self-requested a review June 5, 2026 12:57
@mturac mturac closed this Jun 5, 2026
@mturac mturac deleted the fix/issue-15934-anchor-hash-navigation branch June 5, 2026 13:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

addon: docs agent-scan:human bug ci:normal Run our default set of CI jobs (choose this for most PRs). qa:skip Pull Requests that do not need any QA.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Clicking on anchor with in page bookmark replaces Storybook frame

2 participants