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 ( <> -