feat(plugin-techdocs): add copy to clipboard feedback

Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2022-02-26 09:48:28 +01:00
parent 5dcf41492f
commit 78dab02156
4 changed files with 108 additions and 43 deletions
@@ -753,7 +753,7 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => {
async (transformedElement: Element) =>
transformer(transformedElement, [
scrollIntoAnchor(),
copyToClipboard(),
copyToClipboard(theme),
addLinkClickListener({
baseUrl: window.location.origin,
onClick: (event: MouseEvent, url: string) => {
@@ -802,7 +802,7 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => {
},
}),
]),
[navigate, techdocsStorageApi],
[theme, navigate, techdocsStorageApi],
);
useEffect(() => {
@@ -16,6 +16,8 @@
import { createTestShadowDom } from '../../test-utils';
import { copyToClipboard } from './copyToClipboard';
import { lightTheme } from '@backstage/theme';
import { waitFor } from '@testing-library/react';
const clipboardSpy = jest.fn();
Object.defineProperty(navigator, 'clipboard', {
@@ -38,12 +40,17 @@ describe('copyToClipboard', () => {
`,
{
preTransformers: [],
postTransformers: [copyToClipboard()],
postTransformers: [copyToClipboard(lightTheme)],
},
);
shadowDom.querySelector('button')?.click();
await waitFor(() => {
const tooltip = document.querySelector('[role="tooltip"]');
expect(tooltip).toHaveTextContent('Copied to clipboard');
});
expect(clipboardSpy).toHaveBeenCalledWith(expectedClipboard);
});
@@ -60,7 +67,7 @@ describe('copyToClipboard', () => {
`,
{
preTransformers: [],
postTransformers: [copyToClipboard()],
postTransformers: [copyToClipboard(lightTheme)],
},
);
@@ -1,39 +0,0 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Transformer } from './transformer';
/**
* Recreates copy-to-clipboard functionality attached to <code> snippets that
* is native to mkdocs-material theme.
*/
export const copyToClipboard = (): Transformer => {
return dom => {
Array.from(dom.querySelectorAll('pre > code')).forEach(codeElem => {
const button = document.createElement('button');
const toBeCopied = codeElem.textContent || '';
button.className = 'md-clipboard md-icon';
button.title = 'Copy to clipboard';
button.innerHTML =
'<svg viewBox="0 0 24 24"><path d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg>';
button.addEventListener('click', () =>
navigator.clipboard.writeText(toBeCopied),
);
codeElem?.parentElement?.prepend(button);
});
return dom;
};
};
@@ -0,0 +1,97 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useState, useCallback } from 'react';
import ReactDom from 'react-dom';
import {
withStyles,
Theme,
ThemeProvider,
SvgIcon,
Tooltip,
} from '@material-ui/core';
const CopyToClipboardTooltip = withStyles(theme => ({
tooltip: {
fontSize: 'inherit',
color: theme.palette.text.primary,
margin: 0,
padding: theme.spacing(0.5),
backgroundColor: 'transparent',
boxShadow: 'none',
},
}))(Tooltip);
const CopyToClipboardIcon = () => (
<SvgIcon>
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" />
</SvgIcon>
);
type CopyToClipboardButtonProps = {
text: string;
};
const CopyToClipboardButton = ({ text }: CopyToClipboardButtonProps) => {
const [open, setOpen] = useState(false);
const handleClick = useCallback(() => {
navigator.clipboard.writeText(text);
setOpen(true);
}, [text]);
const handleClose = useCallback(() => {
setOpen(false);
}, [setOpen]);
return (
<CopyToClipboardTooltip
title="Copied to clipboard"
placement="left"
open={open}
onClose={handleClose}
leaveDelay={1000}
>
<button className="md-clipboard md-icon" onClick={handleClick}>
<CopyToClipboardIcon />
</button>
</CopyToClipboardTooltip>
);
};
import type { Transformer } from './transformer';
/**
* Recreates copy-to-clipboard functionality attached to <code> snippets that
* is native to mkdocs-material theme.
*/
export const copyToClipboard = (theme: Theme): Transformer => {
return dom => {
const codes = dom.querySelectorAll('pre > code');
for (const code of codes) {
const text = code.textContent || '';
const container = document.createElement('div');
code?.parentElement?.prepend(container);
ReactDom.render(
<ThemeProvider theme={theme}>
<CopyToClipboardButton text={text} />
</ThemeProvider>,
container,
);
}
return dom;
};
};