AbilitiesList.js
2 weeks ago
AdvancedDisclosure.js
2 weeks ago
ClaudeConnect.js
2 weeks ago
ConnectWizard.js
2 weeks ago
CopyButton.js
2 weeks ago
LinkButton.js
2 weeks ago
McpAdapterRequiredNote.js
2 weeks ago
CopyButton.js
101 lines
| 1 | import { __ } from '@wordpress/i18n'; |
| 2 | import { useCallback, useEffect, useRef, useState } from 'react'; |
| 3 | import { Button } from '@bsf/force-ui'; |
| 4 | import { Check, Copy, X } from 'lucide-react'; |
| 5 | |
| 6 | const legacyCopy = ( text ) => { |
| 7 | const textarea = document.createElement( 'textarea' ); |
| 8 | textarea.value = text; |
| 9 | textarea.style.position = 'fixed'; |
| 10 | textarea.style.opacity = '0'; |
| 11 | document.body.appendChild( textarea ); |
| 12 | textarea.select(); |
| 13 | let ok = false; |
| 14 | try { |
| 15 | ok = document.execCommand( 'copy' ); |
| 16 | } finally { |
| 17 | document.body.removeChild( textarea ); |
| 18 | } |
| 19 | return ok; |
| 20 | }; |
| 21 | |
| 22 | const copyText = ( text ) => { |
| 23 | if ( window.navigator.clipboard && window.isSecureContext ) { |
| 24 | return window.navigator.clipboard.writeText( text ).catch( () => { |
| 25 | if ( ! legacyCopy( text ) ) { |
| 26 | throw new Error( 'copy-failed' ); |
| 27 | } |
| 28 | } ); |
| 29 | } |
| 30 | if ( ! legacyCopy( text ) ) { |
| 31 | return Promise.reject( new Error( 'copy-failed' ) ); |
| 32 | } |
| 33 | return Promise.resolve(); |
| 34 | }; |
| 35 | |
| 36 | /** |
| 37 | * Reusable copy-to-clipboard button with a transient "Copied" confirmation. |
| 38 | * |
| 39 | * @param {Object} props |
| 40 | * @param {string} props.textToCopy Text placed on the clipboard. |
| 41 | * @param {string} [props.label] Optional visible label (icon-only if absent). |
| 42 | * @param {string} [props.variant] force-ui Button variant. |
| 43 | */ |
| 44 | const CopyButton = ( { textToCopy, label, variant = 'outline' } ) => { |
| 45 | const [ copied, setCopied ] = useState( false ); |
| 46 | const [ failed, setFailed ] = useState( false ); |
| 47 | const timerRef = useRef(); |
| 48 | |
| 49 | useEffect( () => () => clearTimeout( timerRef.current ), [] ); |
| 50 | |
| 51 | const handleCopy = useCallback( () => { |
| 52 | if ( ! textToCopy ) { |
| 53 | return; |
| 54 | } |
| 55 | copyText( textToCopy ) |
| 56 | .then( () => { |
| 57 | setFailed( false ); |
| 58 | setCopied( true ); |
| 59 | clearTimeout( timerRef.current ); |
| 60 | timerRef.current = setTimeout( () => setCopied( false ), 1500 ); |
| 61 | } ) |
| 62 | .catch( () => { |
| 63 | setCopied( false ); |
| 64 | setFailed( true ); |
| 65 | clearTimeout( timerRef.current ); |
| 66 | timerRef.current = setTimeout( () => setFailed( false ), 1500 ); |
| 67 | } ); |
| 68 | }, [ textToCopy ] ); |
| 69 | |
| 70 | let icon = <Copy className="text-icon-primary size-4" />; |
| 71 | if ( copied ) { |
| 72 | icon = <Check className="text-icon-success size-4" />; |
| 73 | } else if ( failed ) { |
| 74 | icon = <X className="text-icon-error size-4" />; |
| 75 | } |
| 76 | |
| 77 | let statusLabel = label || __( 'Copy to clipboard', 'presto-player' ); |
| 78 | if ( copied ) { |
| 79 | statusLabel = __( 'Copied', 'presto-player' ); |
| 80 | } else if ( failed ) { |
| 81 | statusLabel = __( 'Copy failed', 'presto-player' ); |
| 82 | } |
| 83 | |
| 84 | const buttonLabel = label ? statusLabel : null; |
| 85 | |
| 86 | return ( |
| 87 | <Button |
| 88 | size="md" |
| 89 | variant={ variant } |
| 90 | icon={ icon } |
| 91 | iconPosition="left" |
| 92 | onClick={ handleCopy } |
| 93 | aria-label={ statusLabel } |
| 94 | > |
| 95 | { buttonLabel } |
| 96 | </Button> |
| 97 | ); |
| 98 | }; |
| 99 | |
| 100 | export default CopyButton; |
| 101 |