-
-
Notifications
You must be signed in to change notification settings - Fork 71
feat: add copy to clipboard button for DomEvents #194
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
smeijer
merged 8 commits into
testing-library:develop
from
marcosvega91:pr/copy_to_clipboard
Jun 19, 2020
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
eb199d9
feat: draft copy to clipboard
marcosvega91 3249c06
Merge branch 'develop' into pr/copy_to_clipboard
marcosvega91 cf67c5e
refactor: remove old code
marcosvega91 5ec625c
feat: rename ResultCopyButton to CopyButton and use in DomEvents
marcosvega91 f2e2bf0
Merge branch 'develop' into pr/copy_to_clipboard
marcosvega91 adfb7e3
fix: after review
marcosvega91 af13a7f
refactor: revert update to IS_DEVTOOL
marcosvega91 20f5429
change fallback style
smeijer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
/* global chrome */ | ||
import React, { useState, useEffect } from 'react'; | ||
import IconButton from './IconButton'; | ||
import SuccessIcon from './icons/SuccessIcon'; | ||
import CopyIcon from './icons/CopyIcon'; | ||
|
||
const IS_DEVTOOL = !!(window.chrome && chrome.runtime && chrome.runtime.id); | ||
|
||
/** | ||
* | ||
* @param {string} suggestion | ||
*/ | ||
async function attemptCopyToClipboard(suggestion) { | ||
try { | ||
if (!IS_DEVTOOL && 'clipboard' in navigator) { | ||
await navigator.clipboard.writeText(suggestion); | ||
return true; | ||
} | ||
|
||
const input = Object.assign(document.createElement('input'), { | ||
type: 'text', | ||
value: suggestion, | ||
}); | ||
|
||
document.body.append(input); | ||
input.select(); | ||
document.execCommand('copy'); | ||
input.remove(); | ||
|
||
return true; | ||
} catch (error) { | ||
console.error(error); | ||
return false; | ||
} | ||
} | ||
|
||
/** | ||
* | ||
* @param {{ | ||
* text: string | function; | ||
* title: string; | ||
* className: string; | ||
* variant: string; | ||
* }} props | ||
*/ | ||
function CopyButton({ text, title, className, variant }) { | ||
const [copied, setCopied] = useState(false); | ||
|
||
useEffect(() => { | ||
if (copied) { | ||
const timeout = setTimeout(() => { | ||
setCopied(false); | ||
}, 1500); | ||
|
||
return () => clearTimeout(timeout); | ||
} | ||
}, [copied]); | ||
|
||
async function handleClick() { | ||
let textToCopy = text; | ||
if (typeof text === 'function') { | ||
textToCopy = text(); | ||
} | ||
const wasSuccessfullyCopied = await attemptCopyToClipboard(textToCopy); | ||
|
||
if (wasSuccessfullyCopied) { | ||
setCopied(true); | ||
} | ||
} | ||
|
||
return ( | ||
<IconButton | ||
variant={variant} | ||
onClick={copied ? undefined : handleClick} | ||
title={title} | ||
className={className} | ||
> | ||
{copied ? <SuccessIcon /> : <CopyIcon />} | ||
</IconButton> | ||
); | ||
} | ||
|
||
export default CopyButton; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import React from 'react'; | ||
import CopyButton from './CopyButton'; | ||
import { render, fireEvent, act, waitFor } from '@testing-library/react'; | ||
|
||
const defaultProps = { | ||
text: 'string', | ||
title: 'title', | ||
}; | ||
|
||
beforeEach(() => { | ||
delete window.navigator.clipboard; | ||
delete document.execCommand; | ||
}); | ||
|
||
it('renders without crashing given default props', () => { | ||
render(<CopyButton {...defaultProps} />); | ||
}); | ||
|
||
it('attempts to copy to clipboard through navigator.clipboard', async () => { | ||
const clipboardSpy = jest.fn(); | ||
|
||
window.navigator.clipboard = { | ||
writeText: clipboardSpy, | ||
}; | ||
|
||
const { getByRole } = render(<CopyButton {...defaultProps} />); | ||
|
||
await act(async () => { | ||
fireEvent.click(getByRole('button')); | ||
}); | ||
|
||
expect(clipboardSpy).toHaveBeenCalledWith(defaultProps.text); | ||
expect(clipboardSpy).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('attempts to copy with legacy methods if navigator.clipboard is unavailable', async () => { | ||
const execCommandSpy = jest.fn(); | ||
|
||
document.execCommand = execCommandSpy; | ||
|
||
const { getByRole } = render(<CopyButton {...defaultProps} />); | ||
|
||
await act(async () => { | ||
fireEvent.click(getByRole('button')); | ||
}); | ||
|
||
expect(execCommandSpy).toHaveBeenCalledWith('copy'); | ||
expect(execCommandSpy).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('temporarily shows a different icon after copying', async () => { | ||
jest.useFakeTimers(); | ||
const execCommandSpy = jest.fn(); | ||
|
||
document.execCommand = execCommandSpy; | ||
|
||
const { getByRole } = render(<CopyButton {...defaultProps} />); | ||
|
||
const button = getByRole('button'); | ||
|
||
const initialIcon = button.innerHTML; | ||
|
||
// act due to useEffect state change | ||
await act(async () => { | ||
fireEvent.click(button); | ||
}); | ||
|
||
await waitFor(() => { | ||
expect(button.innerHTML).not.toBe(initialIcon); | ||
}); | ||
|
||
// same here | ||
await act(async () => { | ||
jest.runAllTimers(); | ||
}); | ||
|
||
await waitFor(() => { | ||
expect(button.innerHTML).toBe(initialIcon); | ||
}); | ||
}); | ||
|
||
it('should accept funcition to get text to copy', async () => { | ||
const execCommandSpy = jest.fn(); | ||
const getTextToCopy = () => 'copy'; | ||
|
||
document.execCommand = execCommandSpy; | ||
|
||
const { getByRole } = render( | ||
<CopyButton {...defaultProps} text={getTextToCopy} />, | ||
); | ||
|
||
await act(async () => { | ||
fireEvent.click(getByRole('button')); | ||
}); | ||
|
||
expect(execCommandSpy).toHaveBeenCalledWith('copy'); | ||
expect(execCommandSpy).toHaveBeenCalledTimes(1); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.