fix: Dashboard editable title weird behavior when adding spaces (#29667)
This commit is contained in:
parent
27dde2a811
commit
453e6deb97
|
|
@ -20,6 +20,7 @@
|
||||||
import {
|
import {
|
||||||
ChangeEvent,
|
ChangeEvent,
|
||||||
KeyboardEvent,
|
KeyboardEvent,
|
||||||
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
|
|
@ -72,144 +73,154 @@ const titleStyles = (theme: SupersetTheme) => css`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -9999px;
|
left: -9999px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
white-space: pre;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DynamicEditableTitle = ({
|
export const DynamicEditableTitle = memo(
|
||||||
title,
|
({
|
||||||
placeholder,
|
title,
|
||||||
onSave,
|
placeholder,
|
||||||
canEdit,
|
onSave,
|
||||||
label,
|
canEdit,
|
||||||
}: DynamicEditableTitleProps) => {
|
label,
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
}: DynamicEditableTitleProps) => {
|
||||||
const [currentTitle, setCurrentTitle] = useState(title || '');
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const contentRef = useRef<HTMLInputElement>(null);
|
const [currentTitle, setCurrentTitle] = useState(title || '');
|
||||||
const [showTooltip, setShowTooltip] = useState(false);
|
const contentRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
|
|
||||||
const { width: inputWidth, ref: sizerRef } = useResizeDetector();
|
const { width: inputWidth, ref: sizerRef } = useResizeDetector();
|
||||||
const { width: containerWidth, ref: containerRef } = useResizeDetector({
|
const { width: containerWidth, ref: containerRef } = useResizeDetector({
|
||||||
refreshMode: 'debounce',
|
refreshMode: 'debounce',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentTitle(title);
|
setCurrentTitle(title);
|
||||||
}, [title]);
|
}, [title]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEditing && contentRef?.current) {
|
if (isEditing && contentRef?.current) {
|
||||||
contentRef.current.focus();
|
contentRef.current.focus();
|
||||||
// move cursor and scroll to the end
|
// move cursor and scroll to the end
|
||||||
if (contentRef.current.setSelectionRange) {
|
if (contentRef.current.setSelectionRange) {
|
||||||
const { length } = contentRef.current.value;
|
const { length } = contentRef.current.value;
|
||||||
contentRef.current.setSelectionRange(length, length);
|
contentRef.current.setSelectionRange(length, length);
|
||||||
contentRef.current.scrollLeft = contentRef.current.scrollWidth;
|
contentRef.current.scrollLeft = contentRef.current.scrollWidth;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}, [isEditing]);
|
||||||
}, [isEditing]);
|
|
||||||
|
|
||||||
// a trick to make the input grow when user types text
|
// a trick to make the input grow when user types text
|
||||||
// we make additional span component, place it somewhere out of view and copy input
|
// we make additional span component, place it somewhere out of view and copy input
|
||||||
// then we can measure the width of that span to resize the input element
|
// then we can measure the width of that span to resize the input element
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (sizerRef?.current) {
|
if (sizerRef?.current) {
|
||||||
sizerRef.current.textContent = currentTitle || placeholder;
|
sizerRef.current.textContent = currentTitle || placeholder;
|
||||||
}
|
}
|
||||||
}, [currentTitle, placeholder, sizerRef]);
|
}, [currentTitle, placeholder, sizerRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
contentRef.current &&
|
contentRef.current &&
|
||||||
contentRef.current.scrollWidth > contentRef.current.clientWidth
|
contentRef.current.scrollWidth > contentRef.current.clientWidth
|
||||||
) {
|
) {
|
||||||
setShowTooltip(true);
|
setShowTooltip(true);
|
||||||
} else {
|
} else {
|
||||||
setShowTooltip(false);
|
setShowTooltip(false);
|
||||||
}
|
}
|
||||||
}, [inputWidth, containerWidth]);
|
}, [inputWidth, containerWidth]);
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
if (!canEdit || isEditing) {
|
if (!canEdit || isEditing) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsEditing(true);
|
|
||||||
}, [canEdit, isEditing]);
|
|
||||||
|
|
||||||
const handleBlur = useCallback(() => {
|
|
||||||
if (!canEdit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const formattedTitle = currentTitle.trim();
|
|
||||||
setCurrentTitle(formattedTitle);
|
|
||||||
if (title !== formattedTitle) {
|
|
||||||
onSave(formattedTitle);
|
|
||||||
}
|
|
||||||
setIsEditing(false);
|
|
||||||
}, [canEdit, currentTitle, onSave, title]);
|
|
||||||
|
|
||||||
const handleChange = useCallback(
|
|
||||||
(ev: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
if (!canEdit || !isEditing) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setCurrentTitle(ev.target.value);
|
setIsEditing(true);
|
||||||
},
|
}, [canEdit, isEditing]);
|
||||||
[canEdit, isEditing],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleKeyPress = useCallback(
|
const handleBlur = useCallback(() => {
|
||||||
(ev: KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (!canEdit) {
|
if (!canEdit) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ev.key === 'Enter') {
|
const formattedTitle = currentTitle.trim();
|
||||||
ev.preventDefault();
|
setCurrentTitle(formattedTitle);
|
||||||
contentRef.current?.blur();
|
if (title !== formattedTitle) {
|
||||||
|
onSave(formattedTitle);
|
||||||
}
|
}
|
||||||
},
|
setIsEditing(false);
|
||||||
[canEdit],
|
}, [canEdit, currentTitle, onSave, title]);
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
const handleChange = useCallback(
|
||||||
<div css={titleStyles} ref={containerRef}>
|
(ev: ChangeEvent<HTMLInputElement>) => {
|
||||||
<Tooltip
|
if (!canEdit || !isEditing) {
|
||||||
id="title-tooltip"
|
return;
|
||||||
title={showTooltip && currentTitle && !isEditing ? currentTitle : null}
|
}
|
||||||
>
|
setCurrentTitle(ev.target.value);
|
||||||
{canEdit ? (
|
},
|
||||||
<input
|
[canEdit, isEditing],
|
||||||
data-test="editable-title-input"
|
);
|
||||||
className="dynamic-title-input"
|
|
||||||
aria-label={label ?? t('Title')}
|
|
||||||
ref={contentRef}
|
|
||||||
onChange={handleChange}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
onClick={handleClick}
|
|
||||||
onKeyPress={handleKeyPress}
|
|
||||||
placeholder={placeholder}
|
|
||||||
value={currentTitle}
|
|
||||||
css={css`
|
|
||||||
cursor: ${isEditing ? 'text' : 'pointer'};
|
|
||||||
|
|
||||||
${inputWidth &&
|
const handleKeyPress = useCallback(
|
||||||
inputWidth > 0 &&
|
(ev: KeyboardEvent<HTMLInputElement>) => {
|
||||||
css`
|
if (!canEdit) {
|
||||||
width: ${inputWidth + 1}px;
|
return;
|
||||||
|
}
|
||||||
|
if (ev.key === 'Enter') {
|
||||||
|
ev.preventDefault();
|
||||||
|
contentRef.current?.blur();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[canEdit],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div css={titleStyles} ref={containerRef}>
|
||||||
|
<Tooltip
|
||||||
|
id="title-tooltip"
|
||||||
|
title={
|
||||||
|
showTooltip && currentTitle && !isEditing ? currentTitle : null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{canEdit ? (
|
||||||
|
<input
|
||||||
|
data-test="editable-title-input"
|
||||||
|
className="dynamic-title-input"
|
||||||
|
aria-label={label ?? t('Title')}
|
||||||
|
ref={contentRef}
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onClick={handleClick}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={currentTitle}
|
||||||
|
css={css`
|
||||||
|
cursor: ${isEditing ? 'text' : 'pointer'};
|
||||||
|
|
||||||
|
${inputWidth &&
|
||||||
|
inputWidth > 0 &&
|
||||||
|
css`
|
||||||
|
width: ${inputWidth + 1}px;
|
||||||
|
`}
|
||||||
`}
|
`}
|
||||||
`}
|
/>
|
||||||
/>
|
) : (
|
||||||
) : (
|
<span
|
||||||
<span
|
className="dynamic-title"
|
||||||
className="dynamic-title"
|
aria-label={label ?? t('Title')}
|
||||||
aria-label={label ?? t('Title')}
|
ref={contentRef}
|
||||||
ref={contentRef}
|
data-test="editable-title"
|
||||||
data-test="editable-title"
|
>
|
||||||
>
|
{currentTitle}
|
||||||
{currentTitle}
|
</span>
|
||||||
</span>
|
)}
|
||||||
)}
|
</Tooltip>
|
||||||
</Tooltip>
|
<span
|
||||||
<span ref={sizerRef} className="input-sizer" aria-hidden tabIndex={-1} />
|
ref={sizerRef}
|
||||||
</div>
|
className="input-sizer"
|
||||||
);
|
aria-hidden
|
||||||
};
|
tabIndex={-1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue