diff --git a/.changeset/nervous-mails-peel.md b/.changeset/nervous-mails-peel.md
new file mode 100644
index 0000000000..530fa9023a
--- /dev/null
+++ b/.changeset/nervous-mails-peel.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core-components': patch
+---
+
+Fix accessibility issue in ``. The component doesn't render anymore an hidden `textarea` containing the text to be copied.
diff --git a/packages/core-components/src/components/CodeSnippet/CodeSnippet.test.tsx b/packages/core-components/src/components/CodeSnippet/CodeSnippet.test.tsx
index 298a8d5780..a4ffd3462b 100644
--- a/packages/core-components/src/components/CodeSnippet/CodeSnippet.test.tsx
+++ b/packages/core-components/src/components/CodeSnippet/CodeSnippet.test.tsx
@@ -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('', () => {
expect(getByText('2')).toBeInTheDocument();
expect(getByText('3')).toBeInTheDocument();
});
-
- it('copy code using button', async () => {
- jest.useFakeTimers();
- document.execCommand = jest.fn();
- const { getByTitle } = await renderInTestApp(
- ,
- );
- const button = getByTitle('Text copied to clipboard');
- fireEvent.click(button);
- act(() => {
- jest.runAllTimers();
- });
- expect(document.execCommand).toHaveBeenCalled();
- jest.useRealTimers();
- });
});
diff --git a/packages/core-components/src/components/CopyTextButton/CopyTextButton.test.tsx b/packages/core-components/src/components/CopyTextButton/CopyTextButton.test.tsx
index acd0fa809c..b84d3919e8 100644
--- a/packages/core-components/src/components/CopyTextButton/CopyTextButton.test.tsx
+++ b/packages/core-components/src/components/CopyTextButton/CopyTextButton.test.tsx
@@ -19,8 +19,9 @@ import { fireEvent } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { renderInTestApp } from '@backstage/test-utils';
import { CopyTextButton } from './CopyTextButton';
-import { ApiRegistry, ApiProvider } from '@backstage/core-app-api';
+import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
import { errorApiRef, ErrorApi } from '@backstage/core-plugin-api';
+import { useCopyToClipboard } from 'react-use';
jest.mock('popper.js', () => {
const PopperJS = jest.requireActual('popper.js');
@@ -33,6 +34,17 @@ jest.mock('popper.js', () => {
};
});
+jest.mock('react-use', () => {
+ const original = jest.requireActual('react-use');
+
+ return {
+ ...original,
+ useCopyToClipboard: jest
+ .fn()
+ .mockImplementation(original.useCopyToClipboard),
+ };
+});
+
const props = {
text: 'mockText',
tooltipDelay: 2,
@@ -43,9 +55,7 @@ const apiRegistry = ApiRegistry.from([
[
errorApiRef,
{
- post(error) {
- throw error;
- },
+ post: jest.fn(),
error$: jest.fn(),
} as ErrorApi,
],
@@ -53,17 +63,22 @@ const apiRegistry = ApiRegistry.from([
describe('', () => {
it('renders without exploding', async () => {
- const { getByDisplayValue } = await renderInTestApp(
+ const { getByTitle, queryByText } = await renderInTestApp(
,
);
- getByDisplayValue('mockText');
+ expect(getByTitle('mockTooltip')).toBeInTheDocument();
+ expect(queryByText('mockTooltip')).not.toBeInTheDocument();
});
- it('displays tooltip on click', async () => {
+ it('displays tooltip and copy the text on click', async () => {
jest.useFakeTimers();
- document.execCommand = jest.fn();
+
+ const spy = useCopyToClipboard as jest.Mock;
+ const copy = jest.fn();
+ spy.mockReturnValue([{}, copy]);
+
const rendered = await renderInTestApp(
@@ -74,8 +89,22 @@ describe('', () => {
act(() => {
jest.runAllTimers();
});
- expect(document.execCommand).toHaveBeenCalled();
+ expect(copy).toHaveBeenCalledWith('mockText');
rendered.getByText('mockTooltip');
jest.useRealTimers();
});
+
+ it('reports copy errors', async () => {
+ const spy = useCopyToClipboard as jest.Mock;
+
+ const error = new Error('just an error');
+ spy.mockReturnValue([{ error }, jest.fn()]);
+
+ await renderInTestApp(
+
+
+ ,
+ );
+ expect(apiRegistry.get(errorApiRef)?.post).toHaveBeenCalledWith(error);
+ });
});
diff --git a/packages/core-components/src/components/CopyTextButton/CopyTextButton.tsx b/packages/core-components/src/components/CopyTextButton/CopyTextButton.tsx
index b5bc516d1a..cc0ecaf76f 100644
--- a/packages/core-components/src/components/CopyTextButton/CopyTextButton.tsx
+++ b/packages/core-components/src/components/CopyTextButton/CopyTextButton.tsx
@@ -18,7 +18,8 @@ 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, useEffect, useState } from 'react';
+import { useCopyToClipboard } from 'react-use';
/**
* Copy text button with visual feedback in the form of
@@ -52,30 +53,23 @@ export const CopyTextButton = (props: Props) => {
...props,
};
const errorApi = useApi(errorApiRef);
- const inputRef = useRef(null);
const [open, setOpen] = useState(false);
+ const [{ error }, copyToClipboard] = useCopyToClipboard();
+
+ useEffect(() => {
+ if (error) {
+ errorApi.post(error);
+ }
+ }, [error, errorApi]);
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 (
<>
-
@@ -121,6 +121,10 @@ paths:
expect(getByText(/my-name/i)).toBeInTheDocument();
expect(getByText(/custom-type/i)).toBeInTheDocument();
- expect(getByText(/Custom Definition/i)).toBeInTheDocument();
+ expect(
+ getAllByText(
+ (_text, element) => element?.textContent === 'Custom Definition',
+ ).length,
+ ).toBeGreaterThan(0);
});
});
diff --git a/plugins/api-docs/src/components/PlainApiDefinitionWidget/PlainApiDefinitionWidget.test.tsx b/plugins/api-docs/src/components/PlainApiDefinitionWidget/PlainApiDefinitionWidget.test.tsx
index a71cb65be0..1600d608fb 100644
--- a/plugins/api-docs/src/components/PlainApiDefinitionWidget/PlainApiDefinitionWidget.test.tsx
+++ b/plugins/api-docs/src/components/PlainApiDefinitionWidget/PlainApiDefinitionWidget.test.tsx
@@ -20,9 +20,13 @@ import { PlainApiDefinitionWidget } from './PlainApiDefinitionWidget';
describe('', () => {
it('renders plain text', async () => {
- const { getByText } = await renderInTestApp(
+ const { getAllByText } = await renderInTestApp(
,
);
- expect(getByText(/Hello World/i)).toBeInTheDocument();
+
+ expect(
+ getAllByText((_text, element) => element?.textContent === 'Hello World')
+ .length,
+ ).toBeGreaterThan(0);
});
});
diff --git a/plugins/badges/src/components/EntityBadgesDialog.test.tsx b/plugins/badges/src/components/EntityBadgesDialog.test.tsx
index 7901a69cbd..1f5ee94e04 100644
--- a/plugins/badges/src/components/EntityBadgesDialog.test.tsx
+++ b/plugins/badges/src/components/EntityBadgesDialog.test.tsx
@@ -55,7 +55,7 @@ describe('EntityBadgesDialog', () => {
);
await expect(
- rendered.findByText(''),
+ rendered.findByText('test: badge'),
).resolves.toBeInTheDocument();
});
});