editableTabTitle.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import {useEffect, useMemo, useRef, useState} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {motion} from 'framer-motion';
  5. import {GrowingInput} from 'sentry/components/growingInput';
  6. import {Tooltip} from 'sentry/components/tooltip';
  7. interface EditableTabTitleProps {
  8. isEditing: boolean;
  9. isSelected: boolean;
  10. label: string;
  11. onChange: (newLabel: string) => void;
  12. setIsEditing: (isEditing: boolean) => void;
  13. disableEditing?: boolean;
  14. }
  15. function EditableTabTitle({
  16. label,
  17. onChange,
  18. isEditing,
  19. isSelected,
  20. setIsEditing,
  21. disableEditing,
  22. }: EditableTabTitleProps) {
  23. const [inputValue, setInputValue] = useState(label);
  24. useEffect(() => {
  25. setInputValue(label);
  26. }, [label]);
  27. const theme = useTheme();
  28. const inputRef = useRef<HTMLInputElement>(null);
  29. const isEmpty = !inputValue.trim();
  30. const memoizedStyles = useMemo(() => {
  31. return {fontWeight: isSelected ? theme.fontWeightBold : theme.fontWeightNormal};
  32. }, [isSelected, theme.fontWeightBold, theme.fontWeightNormal]);
  33. const handleOnBlur = (e: React.FocusEvent<HTMLInputElement, Element>) => {
  34. e.stopPropagation();
  35. e.preventDefault();
  36. const trimmedInputValue = inputValue.trim();
  37. if (!isEditing) {
  38. return;
  39. }
  40. if (isEmpty) {
  41. setInputValue(label);
  42. setIsEditing(false);
  43. return;
  44. }
  45. if (trimmedInputValue !== label) {
  46. onChange(trimmedInputValue);
  47. setInputValue(trimmedInputValue);
  48. }
  49. setIsEditing(false);
  50. };
  51. const handleOnKeyDown = (e: React.KeyboardEvent) => {
  52. if (e.key === 'Enter') {
  53. inputRef.current?.blur();
  54. }
  55. if (e.key === 'Escape') {
  56. setInputValue(label.trim());
  57. setIsEditing(false);
  58. }
  59. if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === ' ') {
  60. e.stopPropagation();
  61. }
  62. };
  63. useEffect(() => {
  64. if (isEditing) {
  65. requestAnimationFrame(() => {
  66. inputRef.current?.focus();
  67. inputRef.current?.select();
  68. });
  69. } else {
  70. inputRef.current?.blur();
  71. }
  72. }, [isEditing, inputRef]);
  73. const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  74. setInputValue(e.target.value);
  75. };
  76. return (
  77. <Tooltip title={label} disabled={isEditing} showOnlyOnOverflow skipWrapper>
  78. <motion.div layout="position" transition={{duration: 0.2}}>
  79. {isSelected && isEditing && !disableEditing ? (
  80. <StyledGrowingInput
  81. value={inputValue}
  82. onChange={handleOnChange}
  83. onKeyDown={handleOnKeyDown}
  84. onBlur={handleOnBlur}
  85. ref={inputRef}
  86. style={memoizedStyles}
  87. isEditing={isEditing}
  88. maxLength={128}
  89. onPointerDown={e => {
  90. e.stopPropagation();
  91. if (!isEditing) {
  92. e.preventDefault();
  93. }
  94. }}
  95. onMouseDown={e => {
  96. e.stopPropagation();
  97. if (!isEditing) {
  98. e.preventDefault();
  99. }
  100. }}
  101. />
  102. ) : (
  103. <UnselectedTabTitle
  104. onDoubleClick={() => setIsEditing(true)}
  105. onPointerDown={e => {
  106. if (isSelected) {
  107. e.stopPropagation();
  108. e.preventDefault();
  109. }
  110. }}
  111. onMouseDown={e => {
  112. if (isSelected) {
  113. e.stopPropagation();
  114. e.preventDefault();
  115. }
  116. }}
  117. isSelected={isSelected}
  118. >
  119. {label}
  120. </UnselectedTabTitle>
  121. )}
  122. </motion.div>
  123. </Tooltip>
  124. );
  125. }
  126. export default EditableTabTitle;
  127. const UnselectedTabTitle = styled('div')<{isSelected: boolean}>`
  128. height: 20px;
  129. max-width: ${p => (p.isSelected ? '325px' : '310px')};
  130. white-space: nowrap;
  131. overflow: hidden;
  132. text-overflow: ellipsis;
  133. padding-right: 1px;
  134. cursor: pointer;
  135. line-height: 1.45;
  136. `;
  137. const StyledGrowingInput = styled(GrowingInput)<{
  138. isEditing: boolean;
  139. }>`
  140. position: relative;
  141. border: none;
  142. margin: 0;
  143. padding: 0;
  144. background: transparent;
  145. min-height: 0px;
  146. height: 20px;
  147. border-radius: 0px;
  148. text-overflow: ellipsis;
  149. cursor: text;
  150. max-width: 325px;
  151. line-height: 1.45;
  152. &,
  153. &:focus,
  154. &:active,
  155. &:hover {
  156. box-shadow: none;
  157. }
  158. `;