Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions src/components/CopyButton.js
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;
98 changes: 98 additions & 0 deletions src/components/CopyButton.test.js
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);
});
21 changes: 17 additions & 4 deletions src/components/DomEvents.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { VirtualScrollable } from './Scrollable';
import throttle from 'lodash.throttle';
import AutoSizer from 'react-virtualized-auto-sizer';
import IconButton from './IconButton';
import TrashcanIcon from './TrashcanIcon';
import TrashcanIcon from './icons/TrashcanIcon';
import CopyButton from './CopyButton';
import EmptyStreetImg from '../images/EmptyStreetImg';
import StickyList from './StickyList';

Expand Down Expand Up @@ -126,6 +127,11 @@ function DomEvents() {
setEventCount(0);
};

const getTextToCopy = () =>
buffer.current
.map((log) => `${log.target.toString()} - ${log.event.EventType}`)
.join('\n');

const flush = useCallback(
throttle(() => setEventCount(buffer.current.length), 16, {
leading: false,
Expand Down Expand Up @@ -182,9 +188,16 @@ function DomEvents() {
<div className="p-2 w-40">element</div>
<div className="flex-auto p-2 flex justify-between">
<span>selector</span>
<IconButton title="clear event log" onClick={reset}>
<TrashcanIcon />
</IconButton>
<div>
<CopyButton
text={getTextToCopy}
title="copy log"
className="mr-5"
/>
<IconButton title="clear event log" onClick={reset}>
<TrashcanIcon />
</IconButton>
</div>
</div>
</div>

Expand Down
11 changes: 8 additions & 3 deletions src/components/IconButton.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import React from 'react';

const variants = {
dark: 'text-gray-600 hover:text-gray-400',
light: 'text-gray-400 hover:text-gray-600',
white: 'text-white hover:text-white',
};

function IconButton({ children, title, variant, onClick, className }) {
const cssVariant = variants[variant] || variants['light'];
return (
<button
className={[
`pointer inline-flex focus:outline-none rounded-full flex items-center justify-center`,
variant === 'dark'
? 'text-gray-600 hover:text-gray-400'
: 'text-gray-400 hover:text-gray-600',
cssVariant,
className,
]
.filter(Boolean)
Expand Down
103 changes: 0 additions & 103 deletions src/components/ResultCopyButton.js

This file was deleted.

Loading