Highlight dragged and target items when dragging in the file tree (#15384)

GitOrigin-RevId: 4eaace34434753f6674724adafcf3b0754365d15
This commit is contained in:
Alf Eaton 2023-10-25 10:24:58 +01:00 committed by Copybot
parent e22c1d70f3
commit 949d4facc7
5 changed files with 76 additions and 46 deletions

View file

@ -1,6 +1,5 @@
import { useRef } from 'react' import { useRef } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useDragLayer } from 'react-dnd'
import classNames from 'classnames' import classNames from 'classnames'
// a custom component rendered on top of a draggable area that renders the // a custom component rendered on top of a draggable area that renders the
@ -8,12 +7,12 @@ import classNames from 'classnames'
// https://react-dnd.github.io/react-dnd/examples/drag-around/custom-drag-layer // https://react-dnd.github.io/react-dnd/examples/drag-around/custom-drag-layer
// for more details. // for more details.
// Also used to display a container border when hovered. // Also used to display a container border when hovered.
function FileTreeDraggablePreviewLayer({ isOver }) { function FileTreeDraggablePreviewLayer({
const { isDragging, item, clientOffset } = useDragLayer(monitor => ({ isOver,
isDragging: monitor.isDragging(), isDragging,
item: monitor.getItem(), item,
clientOffset: monitor.getClientOffset(), clientOffset,
})) }) {
const ref = useRef() const ref = useRef()
return ( return (
@ -39,6 +38,11 @@ function FileTreeDraggablePreviewLayer({ isOver }) {
FileTreeDraggablePreviewLayer.propTypes = { FileTreeDraggablePreviewLayer.propTypes = {
isOver: PropTypes.bool.isRequired, isOver: PropTypes.bool.isRequired,
isDragging: PropTypes.bool.isRequired,
item: PropTypes.shape({
title: PropTypes.string.isRequired,
}),
clientOffset: PropTypes.number,
} }
function DraggablePreviewItem({ title }) { function DraggablePreviewItem({ title }) {

View file

@ -11,6 +11,7 @@ import FileTreeItemName from './file-tree-item-name'
import FileTreeItemMenu from './file-tree-item-menu' import FileTreeItemMenu from './file-tree-item-menu'
import { useFileTreeSelectable } from '../../contexts/file-tree-selectable' import { useFileTreeSelectable } from '../../contexts/file-tree-selectable'
import { useFileTreeActionable } from '../../contexts/file-tree-actionable' import { useFileTreeActionable } from '../../contexts/file-tree-actionable'
import { useDragDropManager } from 'react-dnd'
function FileTreeItemInner({ id, name, isSelected, icons }) { function FileTreeItemInner({ id, name, isSelected, icons }) {
const { permissionsLevel } = useEditorContext(editorContextPropTypes) const { permissionsLevel } = useEditorContext(editorContextPropTypes)
@ -24,7 +25,9 @@ function FileTreeItemInner({ id, name, isSelected, icons }) {
isSelected && isSelected &&
selectedEntityIds.size === 1 selectedEntityIds.size === 1
const { isDragging, dragRef, setIsDraggable } = useDraggable(id) const { dragRef, setIsDraggable } = useDraggable(id)
const dragDropItem = useDragDropManager().getMonitor().getItem()
const itemRef = useRef() const itemRef = useRef()
@ -58,7 +61,7 @@ function FileTreeItemInner({ id, name, isSelected, icons }) {
return ( return (
<div <div
className={classNames('entity', { className={classNames('entity', {
'dnd-draggable-dragging': isDragging, 'file-tree-entity-dragging': dragDropItem?.draggedEntityIds?.has(id),
})} })}
role="presentation" role="presentation"
ref={dragRef} ref={dragRef}

View file

@ -19,6 +19,8 @@ import { useDroppable } from '../contexts/file-tree-draggable'
import { useFileTreeSocketListener } from '../hooks/file-tree-socket-listener' import { useFileTreeSocketListener } from '../hooks/file-tree-socket-listener'
import FileTreeModalCreateFile from './modals/file-tree-modal-create-file' import FileTreeModalCreateFile from './modals/file-tree-modal-create-file'
import FileTreeInner from './file-tree-inner' import FileTreeInner from './file-tree-inner'
import { useDragLayer } from 'react-dnd'
import classnames from 'classnames'
const FileTreeRoot = React.memo(function FileTreeRoot({ const FileTreeRoot = React.memo(function FileTreeRoot({
refProviders, refProviders,
@ -66,14 +68,24 @@ function FileTreeRootFolder() {
const { isOver, dropRef } = useDroppable(fileTreeData._id) const { isOver, dropRef } = useDroppable(fileTreeData._id)
const dragLayer = useDragLayer(monitor => ({
isDragging: monitor.isDragging(),
item: monitor.getItem(),
clientOffset: monitor.getClientOffset(),
}))
return ( return (
<> <>
<FileTreeDraggablePreviewLayer isOver={isOver} /> <FileTreeDraggablePreviewLayer isOver={isOver} {...dragLayer} />
<FileTreeFolderList <FileTreeFolderList
folders={fileTreeData.folders} folders={fileTreeData.folders}
docs={fileTreeData.docs} docs={fileTreeData.docs}
files={fileTreeData.fileRefs} files={fileTreeData.fileRefs}
classes={{ root: 'file-tree-list' }} classes={{
root: classnames('file-tree-list', {
'file-tree-dragging': dragLayer.isDragging,
}),
}}
dropRef={dropRef} dropRef={dropRef}
dataTestId="file-tree-list-root" dataTestId="file-tree-list-root"
/> />

View file

@ -88,7 +88,7 @@ export function useDraggable(draggedEntityId) {
const [isDraggable, setIsDraggable] = useState(true) const [isDraggable, setIsDraggable] = useState(true)
const item = { type: DRAGGABLE_TYPE } const item = { type: DRAGGABLE_TYPE }
const [{ isDragging }, dragRef, preview] = useDrag({ const [{ isDragging, draggedEntityIds }, dragRef, preview] = useDrag({
item, // required, but overwritten by the return value of `begin` item, // required, but overwritten by the return value of `begin`
begin: () => { begin: () => {
const draggedEntityIds = getDraggedEntityIds( const draggedEntityIds = getDraggedEntityIds(
@ -104,6 +104,7 @@ export function useDraggable(draggedEntityId) {
isDragging: !!monitor.isDragging(), isDragging: !!monitor.isDragging(),
}), }),
canDrag: () => permissionsLevel !== 'readOnly' && isDraggable, canDrag: () => permissionsLevel !== 'readOnly' && isDraggable,
end: () => item,
}) })
// remove the automatic preview as we're using a custom preview via // remove the automatic preview as we're using a custom preview via
@ -116,6 +117,7 @@ export function useDraggable(draggedEntityId) {
dragRef, dragRef,
isDragging, isDragging,
setIsDraggable, setIsDraggable,
draggedEntityIds,
} }
} }

View file

@ -67,22 +67,6 @@
} }
} }
li.dnd-droppable-hover .entity-name,
li .entity-name.droppable-hover {
font-weight: bold;
background-color: @file-tree-item-hover-bg;
.fake-full-width-bg(@file-tree-item-hover-bg);
}
ul.droppable-hover li div.entity-name:hover {
background-color: transparent;
.fake-full-width-bg(transparent);
&.droppable-hover {
background-color: @file-tree-item-hover-bg;
.fake-full-width-bg(@file-tree-item-hover-bg);
}
}
ul.file-tree-list { ul.file-tree-list {
margin: 0; margin: 0;
overflow-x: hidden; overflow-x: hidden;
@ -144,10 +128,10 @@
&:focus { &:focus {
outline: none; outline: none;
} }
background-color: transparent;
&:hover { &:hover {
background-color: @file-tree-item-hover-bg; background-color: @file-tree-item-hover-bg;
}
&:hover {
// When the entity is a subfolder, the DOM element is "indented" via margin-left. This makes the // When the entity is a subfolder, the DOM element is "indented" via margin-left. This makes the
// element not fill the entire file-tree width (as it's spaced from the left-hand side via margin) // element not fill the entire file-tree width (as it's spaced from the left-hand side via margin)
// and, in consequence, the background gets clipped. The ::before pseudo-selector is used to fill // and, in consequence, the background gets clipped. The ::before pseudo-selector is used to fill
@ -163,13 +147,6 @@
} }
} }
.entity.dnd-draggable-dragging {
.entity-name:hover {
background-color: transparent;
.fake-full-width-bg(transparent);
}
}
i.fa { i.fa {
color: @file-tree-item-icon-color; color: @file-tree-item-icon-color;
font-size: 14px; font-size: 14px;
@ -309,15 +286,18 @@
> .entity { > .entity {
> .entity-name { > .entity-name {
color: @file-tree-item-selected-color; color: @file-tree-item-selected-color;
> div > i.fa, > div > i.fa,
> button > i.fa, > button > i.fa,
> i.fa, > i.fa,
.entity-menu-toggle i.fa { .entity-menu-toggle i.fa {
color: @file-tree-item-selected-color; color: @file-tree-item-selected-color;
} }
> i.linked-file-highlight { > i.linked-file-highlight {
color: @blue; color: @blue;
} }
background-color: @file-tree-item-selected-bg; background-color: @file-tree-item-selected-bg;
font-weight: bold; font-weight: bold;
padding-right: 32px; padding-right: 32px;
@ -332,13 +312,43 @@
} }
} }
} }
ul.file-tree-list li.selected.dnd-droppable-hover { }
> .entity {
> .entity-name { // while dragging, the previously selected item gets no highlight
background-color: @file-tree-item-hover-bg; ul.file-tree-list.file-tree-dragging li.selected .entity .entity-name {
.fake-full-width-bg(@file-tree-item-hover-bg); font-weight: normal;
background-color: transparent;
.fake-full-width-bg(transparent);
color: @file-tree-item-color;
i.fa {
color: @file-tree-item-icon-color !important;
} }
} }
// the items being dragged get the full "hover" colour
ul.file-tree-list.file-tree-dragging
li
.entity.file-tree-entity-dragging
.entity-name {
background-color: fade(@file-tree-item-hover-bg, 90%);
.fake-full-width-bg(fade(@file-tree-item-hover-bg, 90%));
color: @file-tree-item-color;
i.fa {
color: @file-tree-item-icon-color !important;
}
}
// the drop target gets the "selected" colour
ul.file-tree-list.file-tree-dragging
li.dnd-droppable-hover
.entity
.entity-name {
background-color: @file-tree-item-selected-bg;
.fake-full-width-bg(@file-tree-item-selected-bg);
color: @file-tree-item-selected-color;
i.fa {
color: @file-tree-item-selected-color !important;
} }
} }
@ -355,8 +365,7 @@
} }
} }
.dnd-draggable-preview-item, .dnd-draggable-preview-item {
.ui-draggable-dragging {
background-color: fade(@file-tree-item-selected-bg, 60%); background-color: fade(@file-tree-item-selected-bg, 60%);
color: @file-tree-item-selected-color; color: @file-tree-item-selected-color;
width: 75%; width: 75%;