Skip to content

Commit

Permalink
Copy to clipboard hook: use ref callback (#29643)
Browse files Browse the repository at this point in the history
* Copy to clipboard hook: use ref callback

* Use notice for file URL copy
  • Loading branch information
ellatrix authored Mar 17, 2021
1 parent 0f898c6 commit 4e9aa5d
Show file tree
Hide file tree
Showing 11 changed files with 171 additions and 134 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,14 @@ import { castArray, flow, noop } from 'lodash';
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import {
DropdownMenu,
MenuGroup,
MenuItem,
ClipboardButton,
} from '@wordpress/components';
import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { moreVertical } from '@wordpress/icons';

import { Children, cloneElement, useCallback } from '@wordpress/element';
import { serialize } from '@wordpress/blocks';
import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts';
import { useCopyToClipboard } from '@wordpress/compose';

/**
* Internal dependencies
Expand All @@ -35,6 +31,11 @@ const POPOVER_PROPS = {
isAlternate: true,
};

function CopyMenuItem( { blocks, onCopy } ) {
const ref = useCopyToClipboard( () => serialize( blocks ), onCopy );
return <MenuItem ref={ ref }>{ __( 'Copy' ) }</MenuItem>;
}

export function BlockSettingsDropdown( {
clientIds,
__experimentalSelectBlock,
Expand Down Expand Up @@ -112,14 +113,10 @@ export function BlockSettingsDropdown( {
clientId={ firstBlockClientId }
/>
) }
<ClipboardButton
text={ () => serialize( blocks ) }
role="menuitem"
className="components-menu-item__button"
<CopyMenuItem
blocks={ blocks }
onCopy={ onCopy }
>
{ __( 'Copy' ) }
</ClipboardButton>
/>
{ canDuplicate && (
<MenuItem
onClick={ flow(
Expand Down
18 changes: 12 additions & 6 deletions packages/block-library/src/file/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
ToolbarGroup,
ToolbarButton,
} from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { useSelect, useDispatch } from '@wordpress/data';
import {
BlockControls,
BlockIcon,
Expand All @@ -23,28 +23,34 @@ import {
useBlockProps,
store as blockEditorStore,
} from '@wordpress/block-editor';
import { useEffect, useState, useRef } from '@wordpress/element';
import { useCopyOnClick } from '@wordpress/compose';
import { useEffect, useState } from '@wordpress/element';
import { useCopyToClipboard } from '@wordpress/compose';
import { __, _x } from '@wordpress/i18n';
import { file as icon } from '@wordpress/icons';
import { store as coreStore } from '@wordpress/core-data';
import { store as noticesStore } from '@wordpress/notices';

/**
* Internal dependencies
*/
import FileBlockInspector from './inspector';

function ClipboardToolbarButton( { text, disabled } ) {
const ref = useRef();
const hasCopied = useCopyOnClick( ref, text );
const { createNotice } = useDispatch( noticesStore );
const ref = useCopyToClipboard( text, () => {
createNotice( 'info', __( 'Copied URL to clipboard.' ), {
isDismissible: true,
type: 'snackbar',
} );
} );

return (
<ToolbarButton
className="components-clipboard-toolbar-button"
ref={ ref }
disabled={ disabled }
>
{ hasCopied ? __( 'Copied!' ) : __( 'Copy URL' ) }
{ __( 'Copy URL' ) }
</ToolbarButton>
);
}
Expand Down
31 changes: 17 additions & 14 deletions packages/components/src/clipboard-button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import classnames from 'classnames';
* WordPress dependencies
*/
import { useRef, useEffect } from '@wordpress/element';
import { useCopyOnClick } from '@wordpress/compose';
import { useCopyToClipboard } from '@wordpress/compose';
import deprecated from '@wordpress/deprecated';

/**
* Internal dependencies
*/
import Button from '../button';

const TIMEOUT = 4000;

export default function ClipboardButton( {
className,
children,
Expand All @@ -22,23 +25,23 @@ export default function ClipboardButton( {
text,
...buttonProps
} ) {
const ref = useRef();
const hasCopied = useCopyOnClick( ref, text );
const lastHasCopied = useRef( hasCopied );
deprecated( 'wp.components.ClipboardButton', {
alternative: 'wp.compose.useCopyToClipboard',
} );

useEffect( () => {
if ( lastHasCopied.current === hasCopied ) {
return;
}
const timeoutId = useRef();
const ref = useCopyToClipboard( text, () => {
onCopy();
clearTimeout( timeoutId.current );

if ( hasCopied ) {
onCopy();
} else if ( onFinishCopy ) {
onFinishCopy();
if ( onFinishCopy ) {
timeoutId.current = setTimeout( () => onFinishCopy(), TIMEOUT );
}
} );

lastHasCopied.current = hasCopied;
}, [ onCopy, onFinishCopy, hasCopied ] );
useEffect( () => {
clearTimeout( timeoutId.current );
}, [] );

const classes = classnames( 'components-clipboard-button', className );

Expand Down
42 changes: 0 additions & 42 deletions packages/components/src/clipboard-button/stories/index.js

This file was deleted.

15 changes: 15 additions & 0 deletions packages/compose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ _Returns_

<a name="useCopyOnClick" href="#useCopyOnClick">#</a> **useCopyOnClick**

> **Deprecated**
Copies the text to the clipboard when the element is clicked.

_Parameters_
Expand All @@ -171,6 +173,19 @@ _Returns_

- `boolean`: Whether or not the text has been copied. Resets after the timeout.

<a name="useCopyToClipboard" href="#useCopyToClipboard">#</a> **useCopyToClipboard**

Copies the given text to the clipboard when the element is clicked.

_Parameters_

- _text_ `text|Function`: The text to copy. Use a function if not already available and expensive to compute.
- _onSuccess_ `Function`: Called when to text is copied.

_Returns_

- `RefObject`: A ref to assign to the target element.

<a name="useDebounce" href="#useDebounce">#</a> **useDebounce**

Debounces a function with Lodash's `debounce`. A new debounced function will
Expand Down
7 changes: 7 additions & 0 deletions packages/compose/src/hooks/use-copy-on-click/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import Clipboard from 'clipboard';
* WordPress dependencies
*/
import { useRef, useEffect, useState } from '@wordpress/element';
import deprecated from '@wordpress/deprecated';

/**
* Copies the text to the clipboard when the element is clicked.
*
* @deprecated
*
* @param {Object} ref Reference with the element.
* @param {string|Function} text The text to copy.
* @param {number} timeout Optional timeout to reset the returned
Expand All @@ -20,6 +23,10 @@ import { useRef, useEffect, useState } from '@wordpress/element';
* timeout.
*/
export default function useCopyOnClick( ref, text, timeout = 4000 ) {
deprecated( 'wp.compose.useCopyOnClick', {
alternative: 'wp.compose.useCopyToClipboard',
} );

const clipboard = useRef();
const [ hasCopied, setHasCopied ] = useState( false );

Expand Down
66 changes: 66 additions & 0 deletions packages/compose/src/hooks/use-copy-to-clipboard/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* External dependencies
*/
import Clipboard from 'clipboard';

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

/**
* Internal dependencies
*/
import useRefEffect from '../use-ref-effect';

/** @typedef {import('@wordpress/element').RefObject} RefObject */

function useUpdatedRef( value ) {
const ref = useRef( value );
ref.current = value;
return ref;
}

/**
* Copies the given text to the clipboard when the element is clicked.
*
* @param {text|Function} text The text to copy. Use a function if not
* already available and expensive to compute.
* @param {Function} onSuccess Called when to text is copied.
*
* @return {RefObject} A ref to assign to the target element.
*/
export default function useCopyToClipboard( text, onSuccess ) {
// Store the dependencies as refs and continuesly update them so they're
// fresh when the callback is called.
const textRef = useUpdatedRef( text );
const onSuccesRef = useUpdatedRef( onSuccess );
return useRefEffect( ( node ) => {
// Clipboard listens to click events.
const clipboard = new Clipboard( node, {
text() {
return 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();

if ( onSuccesRef.current ) {
onSuccesRef.current();
}
} );

return () => {
clipboard.destroy();
};
}, [] );
}
1 change: 1 addition & 0 deletions packages/compose/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export { default as withState } from './higher-order/with-state';
// Hooks
export { default as useConstrainedTabbing } from './hooks/use-constrained-tabbing';
export { default as useCopyOnClick } from './hooks/use-copy-on-click';
export { default as useCopyToClipboard } from './hooks/use-copy-to-clipboard';
export { default as __experimentalUseDialog } from './hooks/use-dialog';
export { default as __experimentalUseDragging } from './hooks/use-dragging';
export { default as useFocusOnMount } from './hooks/use-focus-on-mount';
Expand Down
48 changes: 15 additions & 33 deletions packages/edit-post/src/plugins/copy-content-menu-item/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,28 @@
* WordPress dependencies
*/
import { MenuItem } from '@wordpress/components';
import { withDispatch, withSelect } from '@wordpress/data';
import { useSelect, useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { useCopyOnClick, compose, ifCondition } from '@wordpress/compose';
import { useRef, useEffect } from '@wordpress/element';
import { useCopyToClipboard } from '@wordpress/compose';
import { store as noticesStore } from '@wordpress/notices';
import { store as editorStore } from '@wordpress/editor';

function CopyContentMenuItem( { createNotice, editedPostContent } ) {
const ref = useRef();
const hasCopied = useCopyOnClick( ref, editedPostContent );

useEffect( () => {
if ( ! hasCopied ) {
return;
}
export default function CopyContentMenuItem() {
const { createNotice } = useDispatch( noticesStore );
const getText = useSelect(
( select ) => () =>
select( editorStore ).getEditedPostAttribute( 'content' ),
[]
);

function onSuccess() {
createNotice( 'info', __( 'All content copied.' ), {
isDismissible: true,
type: 'snackbar',
} );
}, [ hasCopied ] );

return (
<MenuItem ref={ ref }>
{ hasCopied ? __( 'Copied!' ) : __( 'Copy all content' ) }
</MenuItem>
);
}
}

export default compose(
withSelect( ( select ) => ( {
editedPostContent: select( 'core/editor' ).getEditedPostAttribute(
'content'
),
} ) ),
withDispatch( ( dispatch ) => {
const { createNotice } = dispatch( noticesStore );
const ref = useCopyToClipboard( getText, onSuccess );

return {
createNotice,
};
} ),
ifCondition( ( { editedPostContent } ) => editedPostContent.length > 0 )
)( CopyContentMenuItem );
return <MenuItem ref={ ref }>{ __( 'Copy all content' ) }</MenuItem>;
}
Loading

0 comments on commit 4e9aa5d

Please sign in to comment.