diff --git a/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js b/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js index e17301a31a52f..8bf1b3082b159 100644 --- a/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js +++ b/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js @@ -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 @@ -35,6 +31,11 @@ const POPOVER_PROPS = { isAlternate: true, }; +function CopyMenuItem( { blocks, onCopy } ) { + const ref = useCopyToClipboard( () => serialize( blocks ), onCopy ); + return { __( 'Copy' ) }; +} + export function BlockSettingsDropdown( { clientIds, __experimentalSelectBlock, @@ -112,14 +113,10 @@ export function BlockSettingsDropdown( { clientId={ firstBlockClientId } /> ) } - serialize( blocks ) } - role="menuitem" - className="components-menu-item__button" + - { __( 'Copy' ) } - + /> { canDuplicate && ( { + createNotice( 'info', __( 'Copied URL to clipboard.' ), { + isDismissible: true, + type: 'snackbar', + } ); + } ); return ( - { hasCopied ? __( 'Copied!' ) : __( 'Copy URL' ) } + { __( 'Copy URL' ) } ); } diff --git a/packages/components/src/clipboard-button/index.js b/packages/components/src/clipboard-button/index.js index 2eec3b0d365e2..c0d969935451d 100644 --- a/packages/components/src/clipboard-button/index.js +++ b/packages/components/src/clipboard-button/index.js @@ -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, @@ -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 ); diff --git a/packages/components/src/clipboard-button/stories/index.js b/packages/components/src/clipboard-button/stories/index.js deleted file mode 100644 index 67052bbeab7cf..0000000000000 --- a/packages/components/src/clipboard-button/stories/index.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * External dependencies - */ -import { boolean, text } from '@storybook/addon-knobs'; - -/** - * WordPress dependencies - */ -import { useState } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import ClipboardButton from '../'; - -export default { - title: 'Components/ClipboardButton', - component: ClipboardButton, -}; - -const ClipboardButtonWithState = ( { copied, ...props } ) => { - const [ isCopied, setCopied ] = useState( copied ); - - return ( - setCopied( true ) } - onFinishCopy={ () => setCopied( false ) } - > - { isCopied ? 'Copied!' : `Copy "${ props.text }"` } - - ); -}; - -export const _default = () => { - const isPrimary = boolean( 'Is primary', true ); - const copyText = text( 'Text', 'Text' ); - - return ( - - ); -}; diff --git a/packages/compose/README.md b/packages/compose/README.md index b5b6fb6b2bc04..a5d36e2bae5ee 100644 --- a/packages/compose/README.md +++ b/packages/compose/README.md @@ -159,6 +159,8 @@ _Returns_ # **useCopyOnClick** +> **Deprecated** + Copies the text to the clipboard when the element is clicked. _Parameters_ @@ -171,6 +173,19 @@ _Returns_ - `boolean`: Whether or not the text has been copied. Resets after the timeout. +# **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. + # **useDebounce** Debounces a function with Lodash's `debounce`. A new debounced function will diff --git a/packages/compose/src/hooks/use-copy-on-click/index.js b/packages/compose/src/hooks/use-copy-on-click/index.js index fc1caa2d97d0b..89f43193c5c2b 100644 --- a/packages/compose/src/hooks/use-copy-on-click/index.js +++ b/packages/compose/src/hooks/use-copy-on-click/index.js @@ -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 @@ -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 ); diff --git a/packages/compose/src/hooks/use-copy-to-clipboard/index.js b/packages/compose/src/hooks/use-copy-to-clipboard/index.js new file mode 100644 index 0000000000000..b9cbf38fa3e99 --- /dev/null +++ b/packages/compose/src/hooks/use-copy-to-clipboard/index.js @@ -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(); + }; + }, [] ); +} diff --git a/packages/compose/src/index.js b/packages/compose/src/index.js index 5d1c1d0e5b032..5a42d7f19d8c1 100644 --- a/packages/compose/src/index.js +++ b/packages/compose/src/index.js @@ -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'; diff --git a/packages/edit-post/src/plugins/copy-content-menu-item/index.js b/packages/edit-post/src/plugins/copy-content-menu-item/index.js index 01198d4bd5770..11e1da532adc1 100644 --- a/packages/edit-post/src/plugins/copy-content-menu-item/index.js +++ b/packages/edit-post/src/plugins/copy-content-menu-item/index.js @@ -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 ( - - { hasCopied ? __( 'Copied!' ) : __( 'Copy all content' ) } - - ); -} + } -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 { __( 'Copy all content' ) }; +} diff --git a/packages/editor/src/components/error-boundary/index.js b/packages/editor/src/components/error-boundary/index.js index 6040e3b190d88..872f843ab7966 100644 --- a/packages/editor/src/components/error-boundary/index.js +++ b/packages/editor/src/components/error-boundary/index.js @@ -3,9 +3,19 @@ */ import { Component } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { Button, ClipboardButton } from '@wordpress/components'; +import { Button } from '@wordpress/components'; import { select } from '@wordpress/data'; import { Warning } from '@wordpress/block-editor'; +import { useCopyToClipboard } from '@wordpress/compose'; + +function CopyButton( { text, children } ) { + const ref = useCopyToClipboard( text ); + return ( + + ); +} class ErrorBoundary extends Component { constructor() { @@ -52,20 +62,12 @@ class ErrorBoundary extends Component { , - + { __( 'Copy Post Text' ) } - , - + , + { __( 'Copy Error' ) } - , + , ] } > { __( 'The editor has encountered an unexpected error.' ) } diff --git a/packages/editor/src/components/post-publish-panel/postpublish.js b/packages/editor/src/components/post-publish-panel/postpublish.js index c7aa69bddb20c..3be45aefa5946 100644 --- a/packages/editor/src/components/post-publish-panel/postpublish.js +++ b/packages/editor/src/components/post-publish-panel/postpublish.js @@ -6,17 +6,13 @@ import { get } from 'lodash'; /** * WordPress dependencies */ -import { - PanelBody, - Button, - ClipboardButton, - TextControl, -} from '@wordpress/components'; +import { PanelBody, Button, TextControl } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { Component, createRef } from '@wordpress/element'; import { withSelect } from '@wordpress/data'; import { safeDecodeURIComponent } from '@wordpress/url'; import { decodeEntities } from '@wordpress/html-entities'; +import { useCopyToClipboard } from '@wordpress/compose'; /** * Internal dependencies @@ -43,6 +39,15 @@ const getFuturePostUrl = ( post ) => { return post.permalink_template; }; +function CopyButton( { text, onCopy, children } ) { + const ref = useCopyToClipboard( text, onCopy ); + return ( + + ); +} + class PostPublishPanelPostpublish extends Component { constructor() { super( ...arguments ); @@ -126,16 +131,11 @@ class PostPublishPanelPostpublish extends Component { { viewPostLabel } ) } - - + { this.state.showCopyConfirmation ? __( 'Copied!' ) : __( 'Copy Link' ) } - + { children }