Skip to content
Closed
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
34 changes: 0 additions & 34 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/compose/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Enhancements

- Refactored `useCopyToClipboard` and (deprecated) `useCopyOnClick` hooks to use native Clipboard API instead of third-party dependency `clipboard.js`, removing it from the repo ([#37713](https://github.com/WordPress/gutenberg/pull/37713)).

## 5.0.0 (2021-07-29)

### Breaking Change
Expand Down
1 change: 0 additions & 1 deletion packages/compose/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
"@wordpress/is-shallow-equal": "file:../is-shallow-equal",
"@wordpress/keycodes": "file:../keycodes",
"@wordpress/priority-queue": "file:../priority-queue",
"clipboard": "^2.0.8",
"lodash": "^4.17.21",
"mousetrap": "^1.6.5",
"react-resize-aware": "^3.1.0",
Expand Down
70 changes: 40 additions & 30 deletions packages/compose/src/hooks/use-copy-on-click/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
/**
* External dependencies
*/
import Clipboard from 'clipboard';

/**
* WordPress dependencies
*/
import { useRef, useEffect, useState } from '@wordpress/element';
import { useEffect, useState } from '@wordpress/element';
import deprecated from '@wordpress/deprecated';

/* eslint-disable jsdoc/no-undefined-types */
Expand All @@ -30,45 +25,60 @@ export default function useCopyOnClick( ref, text, timeout = 4000 ) {
alternative: 'wp.compose.useCopyToClipboard',
} );

/** @type {import('react').MutableRefObject<Clipboard | undefined>} */
const clipboard = useRef();
const [ hasCopied, setHasCopied ] = useState( false );

useEffect( () => {
/** @type {number | undefined} */
let timeoutId;
/** @type Array<Element>) */
let triggers = [];

if ( ! ref.current ) {
return;
}

// Clipboard listens to click events.
clipboard.current = new Clipboard( ref.current, {
text: () => ( typeof text === 'function' ? text() : text ),
} );
// `triggers` is always an array, regardless of the value of the `ref` param.
if ( typeof ref.current === 'string' ) {
// expect `ref` to be a DOM selector
triggers = Array.from( document.querySelectorAll( ref.current ) );
} else if ( 'length' in ref.current ) {
// Expect `ref` to be a `NodeList`
triggers = Array.from( ref.current );
} else {
// Expect `ref` to be a single `Element`
triggers = [ ref.current ];
}

clipboard.current.on( 'success', ( { clearSelection, trigger } ) => {
// Clearing selection will move focus back to the triggering button,
// ensuring that it is not reset to the body, and further that it is
// kept within the rendered node.
clearSelection();
/**
* @param {Event} e
*/
const copyTextToClipboard = ( e ) => {
const trigger = /** @type {HTMLElement | null} */ ( e.target );
const currentWindow = trigger?.ownerDocument.defaultView || window;
const textToCopy = typeof text === 'function' ? text() : text || '';

// Handle ClipboardJS focus bug, see https://github.com/zenorocha/clipboard.js/issues/680
if ( trigger ) {
/** @type {HTMLElement} */ ( trigger ).focus();
}
currentWindow?.navigator?.clipboard
?.writeText( textToCopy )
.then( () => {
if ( timeout ) {
setHasCopied( true );
clearTimeout( timeoutId );
timeoutId = setTimeout(
() => setHasCopied( false ),
timeout
);
}
} );
};

if ( timeout ) {
setHasCopied( true );
clearTimeout( timeoutId );
timeoutId = setTimeout( () => setHasCopied( false ), timeout );
}
} );
triggers.forEach( ( t ) =>
t.addEventListener( 'click', copyTextToClipboard )
);

return () => {
if ( clipboard.current ) {
clipboard.current.destroy();
}
triggers.forEach( ( t ) =>
t.removeEventListener( 'click', copyTextToClipboard )
);
clearTimeout( timeoutId );
};
}, [ text, timeout, setHasCopied ] );
Expand Down
82 changes: 82 additions & 0 deletions packages/compose/src/hooks/use-copy-on-click/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* External dependencies
*/
import {
act,
render,
screen,
fireEvent,
waitFor,
} from '@testing-library/react';

/**
* WordPress dependencies
*/
import { useRef } from '@wordpress/element';

/**
* Internal dependencies
*/
import useCopyOnClick from '../';

const ExampleComponent = ( { text, ...props } ) => {
const ref = useRef();

const hasCopied = useCopyOnClick( ref, text, 1000 );

return (
<button ref={ ref } { ...props }>
{ hasCopied ? 'Copied' : 'Click to copy' }
</button>
);
};

let clipboardValue;

const originalClipboard = global.navigator.clipboard;
const mockClipboard = {
writeText: jest.fn().mockImplementation( ( text ) => {
return new Promise( ( resolve ) => {
clipboardValue = text;
resolve();
} );
} ),
};

describe( 'useCopyOnClick', () => {
beforeAll( () => {
global.navigator.clipboard = mockClipboard;
} );

beforeEach( () => {
clipboardValue = undefined;
} );

afterAll( () => {
global.navigator.clipboard = originalClipboard;
} );

it( 'should copy the text to the clipboard and display a warning notice', async () => {
const textToBeCopied = 'mango';
render( <ExampleComponent text={ textToBeCopied } /> );

expect( console ).toHaveWarned();

const triggerButton = screen.getByText( 'Click to copy' );
fireEvent.click( triggerButton );

await waitFor( () =>
expect( clipboardValue ).toEqual( textToBeCopied )
);

// Check that the displayed text changes as a way of testing
// the `hasCopied` logic
expect( screen.getByText( 'Copied' ) ).toBeInTheDocument();

act( () => {
jest.runAllTimers();
} );

expect( screen.getByText( 'Click to copy' ) ).toBeInTheDocument();
} );
} );
43 changes: 18 additions & 25 deletions packages/compose/src/hooks/use-copy-to-clipboard/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* External dependencies
*/
import Clipboard from 'clipboard';

/**
* WordPress dependencies
*/
Expand Down Expand Up @@ -39,32 +34,30 @@ export default function useCopyToClipboard( text, onSuccess ) {
// fresh when the callback is called.
const textRef = useUpdatedRef( text );
const onSuccessRef = useUpdatedRef( onSuccess );
return useRefEffect( ( node ) => {
// Clipboard listens to click events.
const clipboard = new Clipboard( node, {
text() {
return typeof textRef.current === 'function'
return useRefEffect( ( trigger ) => {
if ( ! trigger ) {
return;
}

const copyTextToClipboard = () => {
const currentWindow = trigger.ownerDocument.defaultView || window;
const textToCopy =
typeof textRef.current === 'function'
? textRef.current()
: textRef.current || '';
},
} );

clipboard.on( 'success', ( { clearSelection } ) => {
// Clearing selection will move focus back to the triggering
// button, ensuring that it is not reset to the body, and
// further that it is kept within the rendered node.
clearSelection();
// Handle ClipboardJS focus bug, see
// https://github.com/zenorocha/clipboard.js/issues/680
node.focus();
currentWindow?.navigator?.clipboard
?.writeText( textToCopy )
.then( () => {
// Invoke callback
onSuccessRef?.current?.();
} );
};

if ( onSuccessRef.current ) {
onSuccessRef.current();
}
} );
trigger.addEventListener( 'click', copyTextToClipboard );

return () => {
clipboard.destroy();
trigger.removeEventListener( 'click', copyTextToClipboard );
};
}, [] );
}
Loading