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
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 Original file line Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@


## Unreleased ## 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) ## 5.0.0 (2021-07-29)


### Breaking Change ### Breaking Change
Expand Down
1 change: 0 additions & 1 deletion packages/compose/package.json
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
"@wordpress/is-shallow-equal": "file:../is-shallow-equal", "@wordpress/is-shallow-equal": "file:../is-shallow-equal",
"@wordpress/keycodes": "file:../keycodes", "@wordpress/keycodes": "file:../keycodes",
"@wordpress/priority-queue": "file:../priority-queue", "@wordpress/priority-queue": "file:../priority-queue",
"clipboard": "^2.0.8",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"react-resize-aware": "^3.1.0", "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 Original file line Diff line number Diff line change
@@ -1,12 +1,7 @@
/**
* External dependencies
*/
import Clipboard from 'clipboard';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While the changes in this file are pretty convoluted, it is a deprecated hook after all. The changes in the other hook are a lot more palatable. :P I still think it's worth it to remove the dependency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the same train of thoughts, and came to the same conclusion — refactoring this deprecated hook allows us to remove the dependency from the repo :)


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


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


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


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


if ( ! ref.current ) { if ( ! ref.current ) {
return; return;
} }


// Clipboard listens to click events. // `triggers` is always an array, regardless of the value of the `ref` param.
clipboard.current = new Clipboard( ref.current, { if ( typeof ref.current === 'string' ) {
text: () => ( typeof text === 'function' ? text() : text ), // 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 ];
}
Comment on lines +40 to +50
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously, the different values for ref.current were handles by Clipboard's constructor



clipboard.current.on( 'success', ( { clearSelection, trigger } ) => { /**
// Clearing selection will move focus back to the triggering button, * @param {Event} e
// ensuring that it is not reset to the body, and further that it is */
// kept within the rendered node. const copyTextToClipboard = ( e ) => {
clearSelection(); 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 currentWindow?.navigator?.clipboard
if ( trigger ) { ?.writeText( textToCopy )
/** @type {HTMLElement} */ ( trigger ).focus(); .then( () => {
} if ( timeout ) {
setHasCopied( true );
clearTimeout( timeoutId );
timeoutId = setTimeout(
() => setHasCopied( false ),
timeout
);
}
} );
};


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


return () => { return () => {
if ( clipboard.current ) { triggers.forEach( ( t ) =>
clipboard.current.destroy(); t.removeEventListener( 'click', copyTextToClipboard )
} );
clearTimeout( timeoutId ); clearTimeout( timeoutId );
}; };
}, [ text, timeout, setHasCopied ] ); }, [ text, timeout, setHasCopied ] );
Expand Down
71 changes: 71 additions & 0 deletions packages/compose/src/hooks/use-copy-on-click/test/index.js
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* External dependencies
*/
import { 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();
} );
} ),
};

global.navigator.clipboard = mockClipboard;

describe( 'useCopyOnClick', () => {
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();
await waitFor( () =>
expect( screen.getByText( 'Click to copy' ) ).toBeInTheDocument()
);
} );
} );
42 changes: 17 additions & 25 deletions packages/compose/src/hooks/use-copy-to-clipboard/index.js
Original file line number Original file line Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* External dependencies
*/
import Clipboard from 'clipboard';

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


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


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


return () => { return () => {
clipboard.destroy(); trigger.removeEventListener( 'click', copyTextToClipboard );
}; };
}, [] ); }, [] );
} }
60 changes: 60 additions & 0 deletions packages/compose/src/hooks/use-copy-to-clipboard/test/index.js
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* External dependencies
*/
import { render, screen, fireEvent, waitFor } from '@testing-library/react';

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

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

return (
<button ref={ ref } { ...props }>
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();
} );
} ),
};

global.navigator.clipboard = mockClipboard;

describe( 'useCopyToClipboard', () => {
beforeEach( () => {
clipboardValue = undefined;
} );

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

it( 'should copy the text to the clipboard', async () => {
const successCallback = jest.fn();
const textToBeCopied = 'mango';
render(
<ExampleComponent
onSuccess={ successCallback }
text={ textToBeCopied }
/>
);

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

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