textCopyInput.tsx 3.7 KB

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