You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

CopyButton.tsx 5.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. /* eslint-disable react/jsx-no-bind */
  2. import { Theme } from '@mui/material';
  3. import { withStyles } from '@mui/styles';
  4. import clsx from 'clsx';
  5. import React, { useEffect, useState } from 'react';
  6. import Icon from '../icons/components/Icon';
  7. import { IconCheck, IconCopy } from '../icons/svg';
  8. import { withPixelLineHeight } from '../styles/functions.web';
  9. import { copyText } from '../util/copyText';
  10. const styles = (theme: Theme) => {
  11. return {
  12. copyButton: {
  13. ...withPixelLineHeight(theme.typography.bodyLongRegular),
  14. borderRadius: theme.shape.borderRadius,
  15. display: 'flex',
  16. justifyContent: 'space-between',
  17. alignItems: 'center',
  18. padding: '8px 8px 8px 16px',
  19. marginTop: 5,
  20. width: 'calc(100% - 24px)',
  21. height: 24,
  22. background: theme.palette.action01,
  23. cursor: 'pointer',
  24. '&:hover': {
  25. backgroundColor: theme.palette.action01Hover,
  26. fontWeight: 600
  27. },
  28. '&.clicked': {
  29. background: theme.palette.success02
  30. },
  31. '& > div > svg > path': {
  32. fill: theme.palette.text01
  33. }
  34. },
  35. content: {
  36. overflow: 'hidden',
  37. textOverflow: 'ellipsis',
  38. whiteSpace: 'nowrap' as const,
  39. maxWidth: 292,
  40. marginRight: theme.spacing(3),
  41. '&.selected': {
  42. fontWeight: 600
  43. }
  44. }
  45. };
  46. };
  47. let mounted: boolean;
  48. type Props = {
  49. /**
  50. * Css class to apply on container.
  51. */
  52. className: string;
  53. /**
  54. * An object containing the CSS classes.
  55. */
  56. classes: any;
  57. /**
  58. * The displayed text.
  59. */
  60. displayedText: string;
  61. /**
  62. * The id of the button.
  63. */
  64. id?: string;
  65. /**
  66. * The text displayed on copy success.
  67. */
  68. textOnCopySuccess: string;
  69. /**
  70. * The text displayed on mouse hover.
  71. */
  72. textOnHover: string;
  73. /**
  74. * The text that needs to be copied (might differ from the displayedText).
  75. */
  76. textToCopy: string;
  77. };
  78. /**
  79. * Component meant to enable users to copy the conference URL.
  80. *
  81. * @returns {React$Element<any>}
  82. */
  83. function CopyButton({ classes, className, displayedText, textToCopy, textOnHover, textOnCopySuccess, id }: Props) {
  84. const [ isClicked, setIsClicked ] = useState(false);
  85. const [ isHovered, setIsHovered ] = useState(false);
  86. useEffect(() => {
  87. mounted = true;
  88. return () => {
  89. mounted = false;
  90. };
  91. }, []);
  92. /**
  93. * Click handler for the element.
  94. *
  95. * @returns {void}
  96. */
  97. async function onClick() {
  98. setIsHovered(false);
  99. const isCopied = await copyText(textToCopy);
  100. if (isCopied) {
  101. setIsClicked(true);
  102. setTimeout(() => {
  103. // avoid: Can't perform a React state update on an unmounted component
  104. if (mounted) {
  105. setIsClicked(false);
  106. }
  107. }, 2500);
  108. }
  109. }
  110. /**
  111. * Hover handler for the element.
  112. *
  113. * @returns {void}
  114. */
  115. function onHoverIn() {
  116. if (!isClicked) {
  117. setIsHovered(true);
  118. }
  119. }
  120. /**
  121. * Hover handler for the element.
  122. *
  123. * @returns {void}
  124. */
  125. function onHoverOut() {
  126. setIsHovered(false);
  127. }
  128. /**
  129. * KeyPress handler for accessibility.
  130. *
  131. * @param {React.KeyboardEventHandler<HTMLDivElement>} e - The key event to handle.
  132. *
  133. * @returns {void}
  134. */
  135. function onKeyPress(e: React.KeyboardEvent) {
  136. if (onClick && (e.key === ' ' || e.key === 'Enter')) {
  137. e.preventDefault();
  138. onClick();
  139. }
  140. }
  141. /**
  142. * Renders the content of the link based on the state.
  143. *
  144. * @returns {React$Element<any>}
  145. */
  146. function renderContent() {
  147. if (isClicked) {
  148. return (
  149. <>
  150. <div className = { clsx(classes.content, 'selected') }>
  151. <span role = { 'alert' }>{ textOnCopySuccess }</span>
  152. </div>
  153. <Icon src = { IconCheck } />
  154. </>
  155. );
  156. }
  157. return (
  158. <>
  159. <div className = { clsx(classes.content) }>
  160. <span> { isHovered ? textOnHover : displayedText } </span>
  161. </div>
  162. <Icon src = { IconCopy } />
  163. </>
  164. );
  165. }
  166. return (
  167. <div
  168. aria-label = { textOnHover }
  169. className = { clsx(className, classes.copyButton, isClicked ? ' clicked' : '') }
  170. id = { id }
  171. onBlur = { onHoverOut }
  172. onClick = { onClick }
  173. onFocus = { onHoverIn }
  174. onKeyPress = { onKeyPress }
  175. onMouseOut = { onHoverOut }
  176. onMouseOver = { onHoverIn }
  177. role = 'button'
  178. tabIndex = { 0 }>
  179. { renderContent() }
  180. </div>
  181. );
  182. }
  183. CopyButton.defaultProps = {
  184. className: ''
  185. };
  186. export default withStyles(styles)(CopyButton);