copy-text-button: use Clipboard API

Co-authored-by: Himanshu Mishra <himanshu@orkohunter.net>
Signed-off-by: Vincenzo Scamporlino <me@vinzscam.dev>
This commit is contained in:
Vincenzo Scamporlino
2021-08-18 09:13:27 +02:00
parent 7c1e6deefe
commit d041655a7d
8 changed files with 86 additions and 43 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
Fix accessibility issue in `<CopyTextButton />`. The component doesn't render anymore an hidden `textarea` containing the text to be copied.
@@ -15,8 +15,6 @@
*/
import React from 'react';
import { fireEvent } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { renderInTestApp } from '@backstage/test-utils';
import { CodeSnippet } from './CodeSnippet';
@@ -56,19 +54,4 @@ describe('<CodeSnippet />', () => {
expect(getByText('2')).toBeInTheDocument();
expect(getByText('3')).toBeInTheDocument();
});
it('copy code using button', async () => {
jest.useFakeTimers();
document.execCommand = jest.fn();
const { getByTitle } = await renderInTestApp(
<CodeSnippet {...minProps} showCopyCodeButton />,
);
const button = getByTitle('Text copied to clipboard');
fireEvent.click(button);
act(() => {
jest.runAllTimers();
});
expect(document.execCommand).toHaveBeenCalled();
jest.useRealTimers();
});
});
@@ -42,7 +42,7 @@ export const CodeSnippet = ({
const mode = theme.palette.type === 'dark' ? dark : docco;
const highlightColor = theme.palette.type === 'dark' ? '#256bf3' : '#e6ffed';
return (
<div style={{ position: 'relative' }}>
<div style={{ position: 'relative' }} data-testid="code-snippet">
<SyntaxHighlighter
customStyle={customStyle}
language={language}
@@ -21,6 +21,10 @@ import { renderInTestApp } from '@backstage/test-utils';
import { CopyTextButton } from './CopyTextButton';
import { ApiRegistry, ApiProvider } from '@backstage/core-app-api';
import { errorApiRef, ErrorApi } from '@backstage/core-plugin-api';
// should we add this to dev dependencies?
// (already a dependency of react-use)
// eslint-disable-next-line import/no-extraneous-dependencies
import copy from 'copy-to-clipboard';
jest.mock('popper.js', () => {
const PopperJS = jest.requireActual('popper.js');
@@ -51,14 +55,16 @@ const apiRegistry = ApiRegistry.from([
],
]);
jest.mock('copy-to-clipboard', () => jest.fn());
describe('<CopyTextButton />', () => {
it('renders without exploding', async () => {
const { getByDisplayValue } = await renderInTestApp(
const { getByTestId } = await renderInTestApp(
<ApiProvider apis={apiRegistry}>
<CopyTextButton {...props} />
</ApiProvider>,
);
getByDisplayValue('mockText');
expect(getByTestId('copy-button')).toBeInTheDocument();
});
it('displays tooltip on click', async () => {
@@ -74,7 +80,7 @@ describe('<CopyTextButton />', () => {
act(() => {
jest.runAllTimers();
});
expect(document.execCommand).toHaveBeenCalled();
expect(copy).toHaveBeenCalledWith('mockText');
rendered.getByText('mockTooltip');
jest.useRealTimers();
});
@@ -14,11 +14,11 @@
* limitations under the License.
*/
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
import { IconButton, Tooltip } from '@material-ui/core';
import CopyIcon from '@material-ui/icons/FileCopy';
import PropTypes from 'prop-types';
import React, { MouseEventHandler, useRef, useState } from 'react';
import React, { MouseEventHandler, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
/**
* Copy text button with visual feedback in the form of
@@ -51,31 +51,17 @@ export const CopyTextButton = (props: Props) => {
...defaultProps,
...props,
};
const errorApi = useApi(errorApiRef);
const inputRef = useRef<HTMLTextAreaElement>(null);
const [open, setOpen] = useState(false);
const [, copyToClipboard] = useCopyToClipboard();
const handleCopyClick: MouseEventHandler = e => {
e.stopPropagation();
setOpen(true);
try {
if (inputRef.current) {
inputRef.current.select();
document.execCommand('copy');
}
} catch (error) {
errorApi.post(error);
}
copyToClipboard(text);
};
return (
<>
<textarea
ref={inputRef}
style={{ position: 'absolute', top: -9999, left: -9999 }}
defaultValue={text}
/>
<Tooltip
id="copy-test-tooltip"
title={tooltipText}
@@ -85,7 +71,7 @@ export const CopyTextButton = (props: Props) => {
open={open}
>
<IconButton onClick={handleCopyClick}>
<CopyIcon />
<CopyIcon data-testid="copy-button" />
</IconButton>
</Tooltip>
</>
@@ -20,9 +20,10 @@ import { PlainApiDefinitionWidget } from './PlainApiDefinitionWidget';
describe('<PlainApiDefinitionWidget />', () => {
it('renders plain text', async () => {
const { getByText } = await renderInTestApp(
const { getByTestId } = await renderInTestApp(
<PlainApiDefinitionWidget definition="Hello World" language="yaml" />,
);
expect(getByText(/Hello World/i)).toBeInTheDocument();
expect(getByTestId('code-snippet')).toMatchSnapshot();
});
});
@@ -0,0 +1,62 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<PlainApiDefinitionWidget /> renders plain text 1`] = `
<div
data-testid="code-snippet"
style="position: relative;"
>
<pre
style="display: block; overflow-x: auto; padding: 0.5em; color: rgb(0, 0, 0); background: rgb(248, 248, 255);"
>
<code
class="language-yaml"
style="white-space: pre;"
>
<span>
<span
style="color: rgb(33, 145, 97);"
>
Hello
</span>
<span>
</span>
<span
style="color: rgb(33, 145, 97);"
>
World
</span>
</span>
</code>
</pre>
<div
style="position: absolute; top: 0px; right: 0px;"
>
<button
class="MuiButtonBase-root MuiIconButton-root"
tabindex="0"
title="Text copied to clipboard"
type="button"
>
<span
class="MuiIconButton-label"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
data-testid="copy-button"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm-1 4l6 6v10c0 1.1-.9 2-2 2H7.99C6.89 23 6 22.1 6 21l.01-14c0-1.1.89-2 1.99-2h7zm-1 7h5.5L14 6.5V12z"
/>
</svg>
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
</div>
`;
@@ -55,7 +55,7 @@ describe('EntityBadgesDialog', () => {
);
await expect(
rendered.findByText('![test: badge](http://127.0.0.1/catalog/...)'),
rendered.findByText('test: badge'),
).resolves.toBeInTheDocument();
});
});