clipboard.tsx 2.4 KB

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