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:
@@ -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>
|
||||
</>
|
||||
|
||||
+3
-2
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+62
@@ -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(''),
|
||||
rendered.findByText('test: badge'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user