fix: Dashboard editable title weird behavior when adding spaces (#29667)

This commit is contained in:
Kamil Gabryjelski 2024-07-23 14:55:17 +02:00 committed by GitHub
parent 27dde2a811
commit 453e6deb97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 132 additions and 121 deletions

View File

@ -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>
);
},
);