textCopyInput.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. import {useCallback, useRef} from 'react';
  2. import {findDOMNode} from 'react-dom';
  3. import styled from '@emotion/styled';
  4. import Button from 'sentry/components/button';
  5. import Clipboard from 'sentry/components/clipboard';
  6. import {
  7. Input,
  8. InputGroup,
  9. InputProps,
  10. InputTrailingItems,
  11. } from 'sentry/components/inputGroup';
  12. import {IconCopy} from 'sentry/icons';
  13. import space from 'sentry/styles/space';
  14. import {selectText} from 'sentry/utils/selectText';
  15. interface Props extends Omit<InputProps, 'onCopy'> {
  16. /**
  17. * Text to copy
  18. */
  19. children: string;
  20. className?: string;
  21. disabled?: boolean;
  22. onCopy?: (value: string, event: React.MouseEvent) => void;
  23. /**
  24. * Always show the ending of a long overflowing text in input
  25. */
  26. rtl?: boolean;
  27. style?: React.CSSProperties;
  28. }
  29. function TextCopyInput({
  30. className,
  31. disabled,
  32. style,
  33. onCopy,
  34. rtl,
  35. size,
  36. children,
  37. ...inputProps
  38. }: Props) {
  39. const textRef = useRef<HTMLInputElement>(null);
  40. const handleSelectText = useCallback(() => {
  41. if (!textRef.current) {
  42. return;
  43. }
  44. // We use findDOMNode here because `this.textRef` is not a dom node,
  45. // it's a ref to AutoSelectText
  46. const node = findDOMNode(textRef.current); // eslint-disable-line react/no-find-dom-node
  47. if (!node || !(node instanceof HTMLElement)) {
  48. return;
  49. }
  50. if (rtl && node instanceof HTMLInputElement) {
  51. // we don't want to select the first character - \u202A, nor the last - \u202C
  52. node.setSelectionRange(1, node.value.length - 1);
  53. } else {
  54. selectText(node);
  55. }
  56. }, [rtl]);
  57. /**
  58. * Select text when copy button is clicked
  59. */
  60. const handleCopyClick = useCallback(
  61. (e: React.MouseEvent) => {
  62. if (!textRef.current) {
  63. return;
  64. }
  65. handleSelectText();
  66. onCopy?.(children, e);
  67. e.stopPropagation();
  68. },
  69. [handleSelectText, children, onCopy]
  70. );
  71. /**
  72. * We are using direction: rtl; to always show the ending of a long overflowing text in input.
  73. *
  74. * This however means that the trailing characters with BiDi class O.N. ('Other Neutrals') goes to the other side.
  75. * Hello! becomes !Hello and vice versa. This is a problem for us when we want to show path in this component, because
  76. * /user/local/bin becomes user/local/bin/. Wrapping in unicode characters for left-to-righ embedding solves this,
  77. * however we need to be aware of them when selecting the text - we are solving that by offsetting the selectionRange.
  78. */
  79. const inputValue = rtl ? '\u202A' + children + '\u202C' : children;
  80. return (
  81. <InputGroup className={className}>
  82. <StyledInput
  83. readOnly
  84. disabled={disabled}
  85. ref={textRef}
  86. style={style}
  87. value={inputValue}
  88. onClick={handleSelectText}
  89. size={size}
  90. rtl={rtl}
  91. {...inputProps}
  92. />
  93. <InputTrailingItems>
  94. <Clipboard hideUnsupported value={children}>
  95. <StyledCopyButton
  96. type="button"
  97. borderless
  98. disabled={disabled}
  99. onClick={handleCopyClick}
  100. >
  101. <IconCopy size={size === 'xs' ? 'xs' : 'sm'} />
  102. </StyledCopyButton>
  103. </Clipboard>
  104. </InputTrailingItems>
  105. </InputGroup>
  106. );
  107. }
  108. export default TextCopyInput;
  109. export const StyledInput = styled(Input)<{rtl?: boolean}>`
  110. direction: ${p => (p.rtl ? 'rtl' : 'ltr')};
  111. `;
  112. export const StyledCopyButton = styled(Button)`
  113. color: ${p => p.theme.subText};
  114. padding: ${space(0.5)};
  115. min-height: 0;
  116. height: auto;
  117. `;