textCopyInput.tsx 3.4 KB

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