diff --git a/.changeset/seven-clocks-jog.md b/.changeset/seven-clocks-jog.md
new file mode 100644
index 000000000..87bccd489
--- /dev/null
+++ b/.changeset/seven-clocks-jog.md
@@ -0,0 +1,5 @@
+---
+'@hashicorp/react-code-block': minor
+---
+
+Update to modern clipboard API
diff --git a/packages/code-block/index.test.js b/packages/code-block/index.test.js
index 090d1baf5..30b50b852 100644
--- a/packages/code-block/index.test.js
+++ b/packages/code-block/index.test.js
@@ -12,16 +12,21 @@ import {
} from '@testing-library/react'
import CodeBlock from './'
import { heapAttributes } from './analytics'
-// Mock copy-to-clipboard
-import copyToClipboard from './partials/clipboard-button/copy-to-clipboard'
-jest.mock('./partials/clipboard-button/copy-to-clipboard', () =>
- jest.fn().mockImplementation(() => true)
-)
// We want to make sure copied code is passed through processSnippet,
// we import it so that we don't have to manually recreate its output
import processSnippet from './utils/process-snippet'
-afterEach(cleanup)
+// mock the clipboard API
+Object.assign(navigator, {
+ clipboard: {
+ writeText: jest.fn(),
+ },
+})
+
+afterEach(() => {
+ cleanup()
+ jest.clearAllMocks()
+})
it('should render a root element with a `g-code-block` class', () => {
const { container } = render()
@@ -108,9 +113,10 @@ it('should use the `Copy` button to copy code to the clipboard', async () => {
fireEvent.click(buttonElem)
// Expect copyToClipboard to have been called with our code snippet
// (note: this function is mocked at the top of this test file)
- await waitFor(() => expect(copyToClipboard).toHaveBeenCalledTimes(1))
- expect(copyToClipboard).toHaveBeenCalledWith(codeString)
- copyToClipboard.mockClear()
+ await waitFor(() =>
+ expect(navigator.clipboard.writeText).toHaveBeenCalledTimes(1)
+ )
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(codeString)
})
it('should track a "Copy" event when the "Copy" button is clicked', async () => {
@@ -136,7 +142,6 @@ it('should track a "Copy" event when the "Copy" button is clicked', async () =>
})
// Cleanup
window.analytics = forMockRestore
- copyToClipboard.mockClear()
})
it('should call "onCopyCallback" when the "Copy" button is clicked', async () => {
@@ -156,9 +161,10 @@ it('should call "onCopyCallback" when the "Copy" button is clicked', async () =>
expect(buttonElem).toBeInTheDocument()
fireEvent.click(buttonElem)
// Expect onCopyCallback to have been called
- await waitFor(() => expect(onCopyCallback).toHaveBeenCalledTimes(1))
+ await waitFor(() =>
+ expect(navigator.clipboard.writeText).toHaveBeenCalledTimes(1)
+ )
expect(onCopyCallback).toBeCalledWith(true)
- copyToClipboard.mockClear()
})
it('should track a "Click" event when the root element is clicked', async () => {
@@ -192,11 +198,12 @@ it('should use process-snippet to strip the leading $ from shell snippets', asyn
const buttonElem = screen.getByText('Copy')
expect(buttonElem).toBeInTheDocument()
fireEvent.click(buttonElem)
- // Expect copyToClipboard to have been called with our code snippet
+ // Expect navigator.clipboard.writeText to have been called with our code snippet
// (note: this function is mocked at the top of this test file)
// We also expect the code to have been modified by processSnippet
const expectedCode = processSnippet(codeString)
- await waitFor(() => expect(copyToClipboard).toHaveBeenCalledTimes(1))
- expect(copyToClipboard).toHaveBeenCalledWith(expectedCode)
- copyToClipboard.mockClear()
+ await waitFor(() =>
+ expect(navigator.clipboard.writeText).toHaveBeenCalledTimes(1)
+ )
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(expectedCode)
})
diff --git a/packages/code-block/partials/clipboard-button/copy-to-clipboard.js b/packages/code-block/partials/clipboard-button/copy-to-clipboard.js
deleted file mode 100644
index 1d8846e8e..000000000
--- a/packages/code-block/partials/clipboard-button/copy-to-clipboard.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Copyright (c) HashiCorp, Inc.
- * SPDX-License-Identifier: MPL-2.0
- */
-
-/**
- * Copies a string of text to the clipboard
- * @param {string} string - string of text to copy
- * @returns {boolean} - `true` if successful, or `false` otherwise
- */
-function copyToClipboard(string) {
- // We can't do this if there's no `document`, so don't try
- if (typeof document === 'undefined') {
- console.error('copyToClipboard failed, as document is undefined')
- return false
- }
- // We also can't do this if the argument is not a string
- if (typeof string !== 'string') {
- console.error('copyToClipboard received non-string argument: ' + string)
- return false
- }
-
- let copyElem
-
- try {
- // Create a temporary `textarea` from which to select & copy
- let copyElem = document.createElement('textarea')
- copyElem.style.fontSize = '12pt' // Prevent zooming on iOS
- copyElem.value = string
- document.body.appendChild(copyElem)
- copyElem.select()
- document.execCommand('copy')
- document.body.removeChild(copyElem)
- return true
- } catch (err) {
- // We should try to clean up the tempElem
- // just in case it did get created
- console.error(err)
- document.body.removeChild(copyElem)
- return false
- }
-}
-
-export default copyToClipboard
diff --git a/packages/code-block/partials/clipboard-button/index.tsx b/packages/code-block/partials/clipboard-button/index.tsx
index dde662c83..d4fdbfdea 100644
--- a/packages/code-block/partials/clipboard-button/index.tsx
+++ b/packages/code-block/partials/clipboard-button/index.tsx
@@ -8,7 +8,6 @@ import classnames from 'classnames'
import { IconCheckSquare16 } from '@hashicorp/flight-icons/svg-react/check-square-16'
import { IconDuplicate16 } from '@hashicorp/flight-icons/svg-react/duplicate-16'
import { IconXSquare16 } from '@hashicorp/flight-icons/svg-react/x-square-16'
-import copyToClipboard from './copy-to-clipboard'
import analytics, { heapAttributes } from '../../analytics'
import s from './style.module.css'
@@ -43,8 +42,14 @@ function ClipboardButton({
}
// Otherwise, continue on...
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const isCopied = copyToClipboard(text!)
+ let isCopied = false
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ await navigator.clipboard.writeText(text!)
+ isCopied = true
+ } catch (err) {
+ // noop
+ }
// If there's an internal failure copying text, exit early to handle the error
if (!isCopied) {
diff --git a/packages/code-block/provider/use-indexed-tabs.test.js b/packages/code-block/provider/use-indexed-tabs.test.js
index 837ca7692..fe7463be2 100644
--- a/packages/code-block/provider/use-indexed-tabs.test.js
+++ b/packages/code-block/provider/use-indexed-tabs.test.js
@@ -17,12 +17,8 @@ afterEach(() => {
* useIndexedTabs hook works as expected.
*/
function TestComponent({ tabGroupIds, defaultTabIdx }) {
- const [
- localTabIdx,
- setActiveTabIdx,
- activeTabGroup,
- setActiveTabGroup,
- ] = useIndexedTabs(tabGroupIds, defaultTabIdx)
+ const [localTabIdx, setActiveTabIdx, activeTabGroup, setActiveTabGroup] =
+ useIndexedTabs(tabGroupIds, defaultTabIdx)
return (