Skip to content

Commit cd0d64b

Browse files
separate copy button from the command
1 parent d0e405a commit cd0d64b

2 files changed

Lines changed: 105 additions & 14 deletions

File tree

src/components/InstallCommand/index.js

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {useEffect, useState} from 'react';
1+
import {useEffect, useRef, useState} from 'react';
22
import clsx from 'clsx';
33
import CodeBlock from '@theme/CodeBlock';
44
import styles from './styles.module.css';
@@ -47,21 +47,75 @@ export default function InstallCommand() {
4747
}, []);
4848

4949
const active = PLATFORMS.find((p) => p.id === platform) ?? PLATFORMS[0];
50+
const [copied, setCopied] = useState(false);
51+
const resetTimer = useRef(null);
52+
53+
useEffect(() => () => clearTimeout(resetTimer.current), []);
54+
55+
/* Fallback for when the async Clipboard API is unavailable or refused: it needs
56+
a secure context, and browsers decline it when the document is not focused.
57+
A throwaway textarea and execCommand works in those cases -- it is what
58+
Docusaurus's own copy button relies on. */
59+
function copyViaTextarea(text) {
60+
const field = document.createElement('textarea');
61+
field.value = text;
62+
field.setAttribute('readonly', '');
63+
field.style.position = 'fixed';
64+
field.style.opacity = '0';
65+
document.body.appendChild(field);
66+
field.select();
67+
let ok = false;
68+
try {
69+
ok = document.execCommand('copy');
70+
} catch {
71+
ok = false;
72+
}
73+
document.body.removeChild(field);
74+
return ok;
75+
}
76+
77+
async function copy() {
78+
let ok = false;
79+
try {
80+
await navigator.clipboard.writeText(active.command);
81+
ok = true;
82+
} catch {
83+
ok = copyViaTextarea(active.command);
84+
}
85+
/* Only confirm a copy that actually happened. If both routes fail the label
86+
stays put rather than claiming something untrue -- the whole command is on
87+
screen and can still be selected by hand. */
88+
if (!ok) {
89+
return;
90+
}
91+
setCopied(true);
92+
clearTimeout(resetTimer.current);
93+
resetTimer.current = setTimeout(() => setCopied(false), 2000);
94+
}
5095

5196
return (
5297
<div className={styles.wrapper}>
53-
<div className={styles.tabs} role="tablist" aria-label="Operating system">
54-
{PLATFORMS.map((p) => (
55-
<button
56-
key={p.id}
57-
type="button"
58-
role="tab"
59-
aria-selected={p.id === active.id}
60-
className={clsx(styles.tab, p.id === active.id && styles.tabActive)}
61-
onClick={() => setPlatform(p.id)}>
62-
{p.label}
63-
</button>
64-
))}
98+
<div className={styles.bar}>
99+
<div className={styles.tabs} role="tablist" aria-label="Operating system">
100+
{PLATFORMS.map((p) => (
101+
<button
102+
key={p.id}
103+
type="button"
104+
role="tab"
105+
aria-selected={p.id === active.id}
106+
className={clsx(styles.tab, p.id === active.id && styles.tabActive)}
107+
onClick={() => setPlatform(p.id)}>
108+
{p.label}
109+
</button>
110+
))}
111+
</div>
112+
<button
113+
type="button"
114+
className={styles.copy}
115+
onClick={copy}
116+
aria-label={`Copy the ${active.label} install command`}>
117+
{copied ? 'Copied' : 'Copy'}
118+
</button>
65119
</div>
66120
<CodeBlock language={active.language}>{active.command}</CodeBlock>
67121
</div>

src/components/InstallCommand/styles.module.css

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
.tabs {
66
display: flex;
77
gap: 0.25rem;
8-
margin-bottom: 0.5rem;
98
}
109

1110
.tab {
@@ -30,3 +29,41 @@
3029
background: var(--ifm-color-primary);
3130
color: #fff;
3231
}
32+
33+
.bar {
34+
display: flex;
35+
align-items: center;
36+
justify-content: space-between;
37+
gap: 1rem;
38+
margin-bottom: 0.5rem;
39+
}
40+
41+
.copy {
42+
border: 1px solid var(--phpdbg-card-border);
43+
background: transparent;
44+
color: var(--ifm-color-emphasis-700);
45+
border-radius: 0.5rem;
46+
padding: 0.35rem 0.9rem;
47+
font-size: 0.85rem;
48+
font-weight: 600;
49+
cursor: pointer;
50+
flex-shrink: 0;
51+
}
52+
53+
.copy:hover {
54+
border-color: var(--ifm-color-primary);
55+
color: var(--ifm-color-primary);
56+
}
57+
58+
/* Docusaurus floats its own copy and word-wrap buttons over the top-right of the
59+
block. The install command is one long line, so it runs underneath them however
60+
far it is scrolled. Copy lives in the bar above instead, and the command wraps
61+
so all of it is readable without scrolling sideways. */
62+
.wrapper :global(div[class*='buttonGroup']) {
63+
display: none;
64+
}
65+
66+
.wrapper :global(pre code) {
67+
white-space: pre-wrap;
68+
overflow-wrap: anywhere;
69+
}

0 commit comments

Comments
 (0)