clipboard.tsx 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import {cloneElement, isValidElement, useCallback, useEffect, useState} from 'react';
  2. import {findDOMNode} from 'react-dom';
  3. import copy from 'copy-text-to-clipboard';
  4. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  5. import {t} from 'sentry/locale';
  6. type Props = {
  7. children: React.ReactNode;
  8. /**
  9. * Text to be copied on click
  10. */
  11. value: string;
  12. /**
  13. * Toast message to show on copy failures
  14. */
  15. errorMessage?: string;
  16. /**
  17. * Do not show a toast message on success
  18. */
  19. hideMessages?: boolean;
  20. /**
  21. * Hide children if browser does not support copying
  22. */
  23. hideUnsupported?: boolean;
  24. /**
  25. * Triggered if we fail to copy
  26. */
  27. onError?: () => void;
  28. /**
  29. * Trigger if we successfully copy
  30. */
  31. onSuccess?: () => void;
  32. /**
  33. * Message to show when we successfully copy
  34. */
  35. successMessage?: string;
  36. };
  37. /**
  38. * copy-text-to-clipboard relies on `document.execCommand('copy')`
  39. */
  40. function isSupported() {
  41. return !!document.queryCommandSupported?.('copy');
  42. }
  43. function Clipboard({
  44. hideMessages = false,
  45. successMessage = t('Copied to clipboard'),
  46. errorMessage = t('Error copying to clipboard'),
  47. value,
  48. onSuccess,
  49. onError,
  50. hideUnsupported,
  51. children,
  52. }: Props) {
  53. const [element, setElement] = useState<ReturnType<typeof findDOMNode>>();
  54. const handleClick = useCallback(() => {
  55. const copyWasSuccessful = copy(value);
  56. if (!copyWasSuccessful) {
  57. if (!hideMessages) {
  58. addErrorMessage(errorMessage);
  59. }
  60. onError?.();
  61. return;
  62. }
  63. if (!hideMessages) {
  64. addSuccessMessage(successMessage);
  65. }
  66. onSuccess?.();
  67. }, [value, onError, onSuccess, errorMessage, successMessage, hideMessages]);
  68. useEffect(() => {
  69. element?.addEventListener('click', handleClick);
  70. return () => element?.removeEventListener('click', handleClick);
  71. }, [handleClick, element]);
  72. // XXX: Instead of assigning the `onClick` to the cloned child element, we
  73. // attach a event listener, otherwise we would wipeout whatever click handler
  74. // may be assigned on the child.
  75. const handleMount = useCallback((ref: HTMLElement) => {
  76. // eslint-disable-next-line react/no-find-dom-node
  77. setElement(findDOMNode(ref));
  78. }, []);
  79. // Browser doesn't support `execCommand`
  80. if (hideUnsupported && !isSupported()) {
  81. return null;
  82. }
  83. if (!isValidElement(children)) {
  84. return null;
  85. }
  86. return cloneElement<any>(children, {ref: handleMount});
  87. }
  88. export default Clipboard;