textCopyInput.tsx 3.6 KB

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