[ENG-1106] Rename text box improvements (#1348)

rename text box improvements

Co-authored-by: Utku <74243531+utkubakir@users.noreply.github.com>
This commit is contained in:
nikecandUtku authored and GitHub committed 2023-09-14 14:31:25 +00:00
1 parent 742dc2bb5a
commit 91f3bad92e
3 files changed
+115 -111

No files matched your search

@@ -11,8 +11,8 @@ import {
import { useKey } from 'rooks';
import { useLibraryMutation, useRspcLibraryContext } from '@sd/client';
import { toast, Tooltip } from '@sd/ui';
import { useIsTextTruncated, useOperatingSystem } from '~/hooks';
import { useExplorerViewContext } from '../ViewContext';
type Props = ComponentProps<'div'> & {
@@ -33,135 +33,125 @@ export const RenameTextBoxBase = forwardRef<HTMLDivElement | null, Props>(
const os = useOperatingSystem();
const [allowRename, setAllowRename] = useState(false);
const [renamable, setRenamable] = useState(false);
const renamable = useRef<boolean>(false);
const timeout = useRef<NodeJS.Timeout | null>(null);
const ref = useRef<HTMLDivElement>(null);
useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(_ref, () => ref.current);
//this is to determine if file name is truncated
const isTruncated = useIsTextTruncated(ref, text);
// Highlight file name up to extension or
// fully if it's a directory or has no extension
const highlightText = useCallback(() => {
if (ref?.current) {
const range = document.createRange();
const node = ref.current.firstChild;
if (!node) return;
if (!ref.current || !text) return;
const endRange = text?.lastIndexOf('.');
const node = ref.current.firstChild;
if (!node) return;
range.setStart(node, 0);
range.setEnd(node, endRange && endRange !== -1 ? endRange : text?.length || 0);
const endRange = text.lastIndexOf('.');
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(range);
}
const range = document.createRange();
range.setStart(node, 0);
range.setEnd(node, endRange !== -1 ? endRange : text.length);
const sel = window.getSelection();
if (!sel) return;
sel.removeAllRanges();
sel.addRange(range);
}, [text]);
// Blur field
function blur() {
if (ref?.current) {
ref.current.blur();
setAllowRename(false);
}
}
const blur = useCallback(() => ref.current?.blur(), []);
// Reset to original file name
function reset() {
if (ref?.current) {
ref.current.innerText = text || '';
const reset = () => ref.current && (ref.current.innerText = text ?? '');
const handleRename = async () => {
const newName = ref.current?.innerText.trim();
if (!newName || newName === text) {
reset();
return;
}
}
async function handleRename() {
if (!ref?.current) return;
const newName = ref?.current.innerText.trim();
if (!(newName && locationId)) return reset();
const oldName = text;
if (!oldName || newName === oldName) return;
await renameHandler(newName);
}
};
// Handle keydown events
function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
switch (e.key) {
case 'Tab':
case 'Tab': {
e.preventDefault();
blur();
break;
case 'Escape':
}
case 'Escape': {
e.stopPropagation();
reset();
blur();
break;
case 'z':
}
case 'z': {
if (os === 'macOS' ? e.metaKey : e.ctrlKey) {
reset();
highlightText();
}
break;
}
}
//this is to determine if file name is truncated
const isTruncated = useIsTextTruncated(ref, text);
// Focus and highlight when renaming is allowed
useEffect(() => {
if (allowRename) {
setTimeout(() => {
if (ref?.current) {
ref.current.focus();
highlightText();
}
});
}
}, [allowRename, explorerView, highlightText]);
// Handle renaming when triggered from outside
useEffect(() => {
if (!disabled) {
if (explorerView.isRenaming && !allowRename) setAllowRename(true);
else if (!explorerView.isRenaming && allowRename) setAllowRename(false);
}
}, [explorerView.isRenaming, disabled, allowRename]);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (ref?.current && !ref.current.contains(event.target as Node)) {
blur();
}
}
};
document.addEventListener('mousedown', handleClickOutside, true);
return () => {
document.removeEventListener('mousedown', handleClickOutside, true);
};
}, [ref]);
// Rename or blur on Enter key
useKey('Enter', (e) => {
if (allowRename) blur();
else if (!disabled) {
e.preventDefault();
setAllowRename(true);
explorerView.setIsRenaming(true);
const resetState = () => {
setAllowRename(false);
renamable.current = false;
if (timeout.current) {
clearTimeout(timeout.current);
timeout.current = null;
}
};
useKey('Enter', (e) => {
e.preventDefault();
if (allowRename) blur();
else if (!disabled) setAllowRename(true);
});
useEffect(() => {
const elem = ref.current;
const element = ref.current;
if (!element || !allowRename) return;
const scroll = (e: WheelEvent) => {
if (allowRename) {
e.preventDefault();
if (elem) elem.scrollTop += e.deltaY;
}
e.preventDefault();
element.scrollTop += e.deltaY;
};
elem?.addEventListener('wheel', scroll);
return () => elem?.removeEventListener('wheel', scroll);
}, [allowRename]);
highlightText();
element.addEventListener('wheel', scroll);
return () => element.removeEventListener('wheel', scroll);
}, [allowRename, highlightText]);
useEffect(() => {
if (!disabled) {
if (explorerView.isRenaming && !allowRename) setAllowRename(true);
else explorerView.setIsRenaming(allowRename);
} else resetState();
}, [explorerView.isRenaming, disabled, allowRename, explorerView]);
useEffect(() => {
const onMouseDown = (event: MouseEvent) => {
if (!ref.current?.contains(event.target as Node)) blur();
};
document.addEventListener('mousedown', onMouseDown, true);
return () => document.removeEventListener('mousedown', onMouseDown, true);
}, [blur]);
return (
<Tooltip label={!isTruncated || allowRename ? null : text} asChild>
@@ -171,27 +161,29 @@ export const RenameTextBoxBase = forwardRef<HTMLDivElement | null, Props>(
contentEditable={allowRename}
suppressContentEditableWarning
className={clsx(
'cursor-default truncate rounded-md px-1.5 py-px text-xs text-ink',
'cursor-default truncate rounded-md px-1.5 py-px text-xs text-ink outline-none',
allowRename && [
'whitespace-normal bg-app outline-none ring-2 ring-accent-deep',
'whitespace-normal bg-app ring-2 ring-accent-deep',
activeClassName
],
className
)}
onDoubleClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.button === 0 && setRenamable(!disabled)}
onDoubleClick={(e) => {
if (allowRename) e.stopPropagation();
renamable.current = false;
}}
onMouseDownCapture={(e) => e.button === 0 && (renamable.current = !disabled)}
onMouseUp={(e) => {
if (e.button === 0) {
if (renamable) {
setAllowRename(true);
explorerView.setIsRenaming(true);
}
setRenamable(false);
if (e.button === 0 || renamable.current || !allowRename) {
timeout.current = setTimeout(
() => renamable.current && setAllowRename(true),
350
);
}
}}
onBlur={async () => {
await handleRename();
setAllowRename(false);
onBlur={() => {
handleRename();
resetState();
explorerView.setIsRenaming(false);
}}
onKeyDown={handleKeyDown}
@@ -219,7 +211,7 @@ export const RenamePathTextBox = ({
// Reset to original file name
function reset() {
if (ref?.current) {
ref.current.innerText = props.text || '';
ref.current.innerText = fileName ?? '';
}
}
@@ -227,7 +219,6 @@ export const RenamePathTextBox = ({
// Handle renaming
async function rename(newName: string) {
// TODO: Warn user on rename fails
if (!props.locationId || !props.itemId || newName === fileName) {
reset();
return;
@@ -79,7 +79,9 @@ export default () => {
selected={selected}
cut={cut}
isRenaming={explorerView.isRenaming}
renamable={explorer.selectedItems.size === 1 && !quickPreviewStore.open}
renamable={
selected && explorer.selectedItems.size === 1 && !quickPreviewStore.open
}
/>
)}
</GridList>
@@ -33,7 +33,7 @@ import { useLayoutContext } from '../../Layout/Context';
import { useExplorerContext } from '../Context';
import { FileThumb } from '../FilePath/Thumb';
import { InfoPill } from '../Inspector';
import { getQuickPreviewStore } from '../QuickPreview/store';
import { getQuickPreviewStore, useQuickPreviewStore } from '../QuickPreview/store';
import {
createOrdering,
getOrderingDirection,
@@ -96,11 +96,13 @@ const HeaderColumnName = ({ name }: { name: string }) => {
type Range = [string, string];
export default () => {
const layout = useLayoutContext();
const explorer = useExplorerContext();
const explorerStore = useExplorerStore();
const settings = explorer.useSettingsSnapshot();
const explorerView = useExplorerViewContext();
const layout = useLayoutContext();
const settings = explorer.useSettingsSnapshot();
const quickPreviewStore = useQuickPreviewStore();
const tableRef = useRef<HTMLDivElement>(null);
const tableHeaderRef = useRef<HTMLDivElement>(null);
@@ -183,7 +185,11 @@ export default () => {
allowHighlight={false}
item={item}
selected={selected}
disabled={!selected || explorer.selectedItems.size > 1}
disabled={
!selected ||
explorer.selectedItems.size > 1 ||
quickPreviewStore.open
}
style={{ maxHeight: 36 }}
/>
</div>
@@ -263,7 +269,12 @@ export default () => {
}
}
],
[explorer.selectedItems, settings.colSizes, explorerStore.cutCopyState]
[
settings.colSizes,
explorer.selectedItems,
explorerStore.cutCopyState,
quickPreviewStore.open
]
);
const table = useReactTable({