Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Copy header link #30411

Open
wants to merge 3 commits into
base: next
Choose a base branch
from
Open

Conversation

scriptcoded
Copy link

@scriptcoded scriptcoded commented Jan 29, 2025

Closes #30407

What I did

  • Broke out createCopyToClipboardFunction to a separate file for clearer availability for reuse.
  • Added tests for createCopyToClipboardFunction. Decided not to fully test the fallback method since DOM mocking doesn't seem to be working and navigator.clipboard is pretty much generally available.
  • Made clicking the 🔗 icon next to a documentation header block also copy the URL. The copied URL is relative to the parent location.

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

This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!

  1. Start Storybook, for example using as sandbox, e.g. yarn task --task sandbox --start-from auto --template react-vite/default-ts
  2. Open Storybook in your browser
  3. Click on the 🔗 icon of any header on the documentation page.
  4. Notice that the link was copied, and links to the manager view, not the iframe.

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 canary-release-pr.yml --field pr=<PR_NUMBER>

name before after diff z %
createSize 0 B 0 B 0 B - -
generateSize 80.5 MB 80.5 MB 0 B 1.4 0%
initSize 80.5 MB 80.5 MB 0 B -0.87 0%
diffSize 97 B 97 B 0 B -0.9 0%
buildSize 7.24 MB 7.44 MB 198 kB 7.26 2.7%
buildSbAddonsSize 1.87 MB 1.87 MB 134 B 0.63 0%
buildSbCommonSize 195 kB 195 kB 0 B - 0%
buildSbManagerSize 1.88 MB 2.07 MB 197 kB 31.4 🔺9.5%
buildSbPreviewSize 0 B 0 B 0 B - -
buildStaticSize 0 B 0 B 0 B - -
buildPrebuildSize 3.94 MB 4.14 MB 198 kB 11.49 4.8%
buildPreviewSize 3.3 MB 3.3 MB 128 B 1.34 0%
testBuildSize 0 B 0 B 0 B - -
testBuildSbAddonsSize 0 B 0 B 0 B - -
testBuildSbCommonSize 0 B 0 B 0 B - -
testBuildSbManagerSize 0 B 0 B 0 B - -
testBuildSbPreviewSize 0 B 0 B 0 B - -
testBuildStaticSize 0 B 0 B 0 B - -
testBuildPrebuildSize 0 B 0 B 0 B - -
testBuildPreviewSize 0 B 0 B 0 B - -
name before after diff z %
createTime 7.2s 23.9s 16.6s 0.49 69.6%
generateTime 18.8s 19.2s 458ms -0.15 2.4%
initTime 4.8s 4.6s -174ms -0.88 -3.7%
buildTime 8.6s 9.7s 1s 0.42 11.1%
testBuildTime 0ms 0ms 0ms - -
devPreviewResponsive 4.7s 5.1s 361ms -0.33 7%
devManagerResponsive 3.6s 4s 424ms 0.11 10.5%
devManagerHeaderVisible 656ms 693ms 37ms -0.77 5.3%
devManagerIndexVisible 667ms 737ms 70ms -0.75 9.5%
devStoryVisibleUncached 3.7s 3.9s 184ms -0.29 4.7%
devStoryVisible 687ms 737ms 50ms -0.81 6.8%
devAutodocsVisible 636ms 746ms 110ms -0.02 14.7%
devMDXVisible 580ms 665ms 85ms -0.78 12.8%
buildManagerHeaderVisible 665ms 825ms 160ms -0.24 19.4%
buildManagerIndexVisible 750ms 910ms 160ms -0.28 17.6%
buildStoryVisible 585ms 811ms 226ms -0.13 27.9%
buildAutodocsVisible 614ms 639ms 25ms -0.26 3.9%
buildMDXVisible 562ms 732ms 170ms 0.52 23.2%

Greptile Summary

This PR refactors clipboard functionality in Storybook by introducing a reusable utility with modern API support and DOM fallback.

  • Added createCopyToClipboardFunction.ts with Clipboard API support and DOM fallback for older browsers
  • Added unit tests in createCopyToClipboardFunction.test.ts focusing on Clipboard API functionality
  • Integrated URL copying for header links in documentation using the new utility
  • Refactored syntaxhighlighter.tsx to use centralized clipboard functionality
  • Updated whats_new.tsx to use the new utility with copy feedback state

The changes improve code organization by centralizing clipboard functionality while maintaining backward compatibility through a DOM fallback mechanism. The implementation appears solid though some additional testing for URL handling would be beneficial.

💡 (1/5) You can manually trigger the bot by mentioning @greptileai in a comment!

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

6 file(s) reviewed, 4 comment(s)
Edit PR Review Bot Settings | Greptile

Comment on lines +12 to +15
document.body.appendChild(tmp);
tmp.select();
document.execCommand('copy');
document.body.removeChild(tmp);
Copy link
Contributor

Choose a reason for hiding this comment

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

style: potential memory leak if an error occurs between appendChild and removeChild - wrap in try/finally

Suggested change
document.body.appendChild(tmp);
tmp.select();
document.execCommand('copy');
document.body.removeChild(tmp);
document.body.appendChild(tmp);
try {
tmp.select();
document.execCommand('copy');
} finally {
document.body.removeChild(tmp);
}

Copy link
Member

Choose a reason for hiding this comment

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

Adding the focus in the finally block would also be needed to ensure the app is restored to its intended state should there ever be a failure (execCommand is being removed, after all).

Comment on lines +13 to +15
global.document = {
createElement: vi.fn(),
} as any as Document;
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: Mocking document with just createElement is insufficient for testing the fallback copy mechanism. Need to mock body.appendChild, execCommand, body.removeChild, and activeElement.

Suggested change
global.document = {
createElement: vi.fn(),
} as any as Document;
global.document = {
createElement: vi.fn(),
body: {
appendChild: vi.fn(),
removeChild: vi.fn()
},
execCommand: vi.fn(),
activeElement: null
} as any as Document;

Comment on lines +56 to +59
// DOM mocking is not enabled which will cause the function to throw
try {
await copyToClipboard('text');
} catch (e) {}
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Silently catching all errors masks potential issues. Should verify the expected error is thrown when DOM APIs are not available.

Comment on lines +195 to +196
const copyUrl = new URL(window.parent.location.href);
copyUrl.hash = hash;
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: Using window.parent.location.href could break if Storybook is embedded in a multi-level iframe structure. Consider using a more robust way to get the root URL.

Copy link
Member

Choose a reason for hiding this comment

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

I don't think this is a valid concern here considering this is the Docs renderer's own code. What would more likely be a concern is if Storybook removes the iframe and uses Web Components to render preview in the future.

Considering DocsContainer.tsx is on the same level and also gets the URL, you could consider making a getRootUrl util in the utils.ts file in the same folder for both files to use.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

6 file(s) reviewed, 2 comment(s)
Edit PR Review Bot Settings | Greptile

Comment on lines +7 to +11
Object.assign(navigator, {
clipboard: {
writeText,
},
});
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Consider using vi.stubGlobal('navigator', {...}) instead of Object.assign for more reliable test setup and teardown

Comment on lines 205 to +206
onClick={(event: SyntheticEvent) => {
copyToClipboard(copyUrl.href);
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: copyToClipboard should handle promise rejection from clipboard API

@storybook-pr-benchmarking
Copy link

storybook-pr-benchmarking bot commented Feb 3, 2025

Package Benchmarks

Commit: ef6bcc7, ran on 10 February 2025 at 15:01:53 UTC

The following packages have significant changes to their size or dependencies:

@storybook/core

Before After Difference
Dependency count 52 52 0
Self size 19.26 MB 19.45 MB 🚨 +198 KB 🚨
Dependency size 14.19 MB 14.19 MB 0 B
Bundle Size Analyzer Link Link

storybook

Before After Difference
Dependency count 53 53 0
Self size 23 KB 23 KB 0 B
Dependency size 33.45 MB 33.65 MB 🚨 +198 KB 🚨
Bundle Size Analyzer Link Link

sb

Before After Difference
Dependency count 54 54 0
Self size 1 KB 1 KB 0 B
Dependency size 33.47 MB 33.67 MB 🚨 +198 KB 🚨
Bundle Size Analyzer Link Link

@storybook/cli

Before After Difference
Dependency count 387 387 0
Self size 503 KB 503 KB 0 B
Dependency size 75.43 MB 75.63 MB 🚨 +198 KB 🚨
Bundle Size Analyzer Link Link

@storybook/codemod

Before After Difference
Dependency count 276 276 0
Self size 617 KB 617 KB 0 B
Dependency size 65.51 MB 65.71 MB 🚨 +198 KB 🚨
Bundle Size Analyzer Link Link

create-storybook

Before After Difference
Dependency count 111 111 0
Self size 1.11 MB 1.11 MB 0 B
Dependency size 42.63 MB 42.83 MB 🚨 +198 KB 🚨
Bundle Size Analyzer Link Link

Copy link

nx-cloud bot commented Feb 10, 2025

View your CI Pipeline Execution ↗ for commit b949c2d.

Command Status Duration Result
nx run-many -t build --parallel=15 ✅ Succeeded 14s View ↗

☁️ Nx Cloud last updated this comment at 2025-02-10 08:06:02 UTC

Copy link
Member

@Sidnioulz Sidnioulz left a comment

Choose a reason for hiding this comment

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

Good job! Works as advertised.

Note the comments on how you can improve memory usage and robustness of the URL handling. I'd love to see some tests for what is being copied, if you can sort out an AnchorMdx stories file. This part technically isn't covered by tests yet.

Other reviewers, please note: it is normal that when reopening a tab with the copied URL, the specific subsection does not load. This is a bug caused by navigate internally eating up URL hashes which I've fixed in another PR.

Comment on lines +12 to +15
document.body.appendChild(tmp);
tmp.select();
document.execCommand('copy');
document.body.removeChild(tmp);
Copy link
Member

Choose a reason for hiding this comment

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

Adding the focus in the finally block would also be needed to ensure the app is restored to its intended state should there ever be a failure (execCommand is being removed, after all).

Comment on lines +195 to +196
const copyUrl = new URL(window.parent.location.href);
copyUrl.hash = hash;
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this is a valid concern here considering this is the Docs renderer's own code. What would more likely be a concern is if Storybook removes the iframe and uses Web Components to render preview in the future.

Considering DocsContainer.tsx is on the same level and also gets the URL, you could consider making a getRootUrl util in the utils.ts file in the same folder for both files to use.

@@ -185,6 +192,9 @@ const HeaderWithOcticonAnchor: FC<PropsWithChildren<HeaderWithOcticonAnchorProps
const OcticonHeader = OcticonHeaders[as];
const hash = `#${id}`;

const copyUrl = new URL(window.parent.location.href);
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason why you don't use the whole URL but just the href? E.g. you are not preserving search params, but they could be used to filter content in some docs pages and could be relevant for the person copying the URL to provide an exact replica to the person receiving it.

Comment on lines +3 to +6
return (text: string) => navigator.clipboard.writeText(text);
}

return async (text: string) => {
Copy link
Member

Choose a reason for hiding this comment

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

nit: You could make these two functions top-level functions instead of arrow functions, so that no matter how often this file is imported at the top-level of new ES modules like you new in mdx.tsx, the same function declarations are reused instead of new ones being held in memory. Function declarations are hoisted and arrow functions aren't.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

6 file(s) reviewed, 2 comment(s)
Edit PR Review Bot Settings | Greptile

@@ -0,0 +1,21 @@
export function createCopyToClipboardFunction() {
if (navigator?.clipboard) {
Copy link
Contributor

Choose a reason for hiding this comment

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

style: consider checking for writeText method specifically since clipboard API may exist without writeText support

Suggested change
if (navigator?.clipboard) {
if (navigator?.clipboard?.writeText) {

Comment on lines 201 to 203
onCopyLink={() => {
navigator.clipboard?.writeText(whatsNewData.blogUrl ?? whatsNewData.url);
copyToClipboard(whatsNewData.blogUrl ?? whatsNewData.url);
}}
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: The copyToClipboard function returns a Promise but it's not being awaited here. This could cause issues with error handling.

Suggested change
onCopyLink={() => {
navigator.clipboard?.writeText(whatsNewData.blogUrl ?? whatsNewData.url);
copyToClipboard(whatsNewData.blogUrl ?? whatsNewData.url);
}}
onCopyLink={async () => {
try {
await copyToClipboard(whatsNewData.blogUrl ?? whatsNewData.url);
} catch (error) {
console.error('Failed to copy to clipboard:', error);
}
}}

@valentinpalkovic valentinpalkovic self-requested a review February 10, 2025 14:49
@JReinhold
Copy link
Contributor

I personally do not agree with the original problem statement.

We've found that users are confused with how heading linking works. Clicking the 🔗 icon next to the heading only scrolls to it and updates the URL, but does not copy it.

From my general experience, the "click heading to get URL"-feature does not automatically copy the URL to the clipboard. Can anyone share a few links to sites that do this today?
You can argue that clicking a heading to get the URL is often with the intend of copying it for sharing. But I find it more important to do what's expected, which is following the pattern established on the web.

Can anyone share a few links to sites that copies to the clipboard automatically today?

Here are a few examples of sites that have clickable headings without copying to clipboard:

In my search I found one website that added to the clipboard automatically, https://web.dev, but they do that with an explicit callout, speaking into my perception that it is generally unexpected:
image

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

Successfully merging this pull request may close these issues.

5 participants