Skip to content

Commit b949f91

Browse files
authored
feat: Add copy to clipboard Heading (#1999)
closes: #1993 (comment) Create a new `Heading` component using the [Swizzling](https://docusaurus.io/docs/swizzling) method. Updated how the "Copy to clipboard" buttons look so it is consistent with the web. Also had an issue with scrolling to the desired `id`, so I had to [useBrokenLinks](https://docusaurus.io/docs/3.2.1/docusaurus-core#useBrokenLinks) hook
1 parent 946db0d commit b949f91

File tree

2 files changed

+132
-0
lines changed

2 files changed

+132
-0
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { useThemeConfig } from '@docusaurus/theme-common';
2+
import { translate } from '@docusaurus/Translate';
3+
import useBrokenLinks from '@docusaurus/useBrokenLinks';
4+
import clsx from 'clsx';
5+
import React, { useEffect } from 'react';
6+
7+
import { LinkIcon } from '@apify/ui-icons';
8+
import { useCopyToClipboard } from '@apify/ui-library';
9+
10+
import styles from './styles.module.css';
11+
12+
export default function Heading({ as: As, id, ...props }) {
13+
const brokenLinks = useBrokenLinks();
14+
const {
15+
navbar: { hideOnScroll },
16+
} = useThemeConfig();
17+
18+
const { isCopied, copyToClipboard } = useCopyToClipboard();
19+
20+
// Register the anchor ID so Docusaurus can scroll to it
21+
useEffect(() => {
22+
if (id) {
23+
brokenLinks.collectAnchor(id);
24+
25+
// Handle scroll on page load if this heading matches the URL hash
26+
const hash = decodeURIComponent(window.location.hash.slice(1));
27+
if (hash === id) {
28+
// Use setTimeout to ensure the page is fully rendered
29+
setTimeout(() => {
30+
const element = document.getElementById(id);
31+
if (element) {
32+
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
33+
}
34+
}, 100);
35+
}
36+
}
37+
}, [id, brokenLinks]);
38+
39+
// H1 headings and headings without an id shouldn't have the copy to clipboard button
40+
if (As === 'h1' || !id) {
41+
return <As {...props} {...(id && { id })} />;
42+
}
43+
44+
const handleCopy = async (e) => {
45+
e.preventDefault();
46+
const url = new URL(window.location.href);
47+
url.hash = `#${id ?? ''}`;
48+
window.location.hash = `#${id ?? ''}`;
49+
await copyToClipboard(url.toString());
50+
};
51+
52+
const anchorTitle = translate(
53+
{
54+
id: 'theme.common.headingLinkTitle',
55+
message: 'Direct link to {heading}',
56+
description: 'Title for link to heading',
57+
},
58+
{
59+
heading: typeof props.children === 'string' ? props.children : id,
60+
},
61+
);
62+
63+
return (
64+
<As
65+
{...props}
66+
className={clsx(
67+
'anchor',
68+
hideOnScroll
69+
? styles.anchorWithHideOnScrollNavbar
70+
: styles.anchorWithStickyNavbar,
71+
props.className,
72+
)}
73+
id={id}>
74+
{props.children}
75+
<a
76+
onClick={handleCopy}
77+
href={`#${id}`}
78+
className={clsx(styles.headingCopyIcon, isCopied && styles.copied)}
79+
aria-label={anchorTitle}
80+
>
81+
<LinkIcon size="16" />
82+
</a>
83+
</As>
84+
);
85+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
.anchorWithStickyNavbar {
2+
scroll-margin-top: calc(var(--ifm-navbar-height) + 0.5rem);
3+
}
4+
5+
.anchorWithHideOnScrollNavbar {
6+
scroll-margin-top: 0.5rem;
7+
}
8+
9+
.headingCopyIcon {
10+
display: none;
11+
position: relative;
12+
left: .4rem;
13+
color: var(--ifm-color-emphasis-700);
14+
text-decoration: none;
15+
}
16+
17+
.headingCopyIcon svg {
18+
stroke: var(--ifm-color-primary);
19+
max-height: 1em !important;
20+
}
21+
22+
h2:hover .headingCopyIcon,
23+
h3:hover .headingCopyIcon,
24+
h4:hover .headingCopyIcon,
25+
h5:hover .headingCopyIcon,
26+
h6:hover .headingCopyIcon {
27+
display: inline-block;
28+
}
29+
30+
.headingCopyIcon.copied {
31+
display: inline-block !important;
32+
}
33+
34+
.headingCopyIcon.copied svg {
35+
display: none;
36+
}
37+
38+
.headingCopyIcon.copied::after {
39+
content: 'Copied!';
40+
font-size: 12px;
41+
color: var(--ifm-font-color-secondary);
42+
font-weight: 600;
43+
margin-left: 8px;
44+
vertical-align: middle;
45+
line-height: 1;
46+
}
47+

0 commit comments

Comments
 (0)