diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-doc.js b/services/web/frontend/js/features/file-tree/components/file-tree-doc.js index 1c7bd54d53..252c5d1e21 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-doc.js +++ b/services/web/frontend/js/features/file-tree/components/file-tree-doc.js @@ -8,9 +8,16 @@ import Icon from '../../../shared/components/icon' import iconTypeFromName from '../util/icon-type-from-name' import classnames from 'classnames' -function FileTreeDoc({ name, id, isFile, isLinkedFile }) { +function FileTreeDoc({ + name, + id, + isFile, + isLinkedFile, + shouldShowVisualSelection, +}) { const { isSelected, props: selectableEntityProps } = useSelectableEntity( id, + shouldShowVisualSelection, isFile ) @@ -38,6 +45,7 @@ FileTreeDoc.propTypes = { id: PropTypes.string.isRequired, isFile: PropTypes.bool, isLinkedFile: PropTypes.bool, + shouldShowVisualSelection: PropTypes.bool, } export const FileTreeIcon = ({ isLinkedFile, name }) => { diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-folder-list.js b/services/web/frontend/js/features/file-tree/components/file-tree-folder-list.js index 0795fa3650..7f8c81ec07 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-folder-list.js +++ b/services/web/frontend/js/features/file-tree/components/file-tree-folder-list.js @@ -12,6 +12,7 @@ function FileTreeFolderList({ classes = {}, dropRef = null, children, + shouldShowVisualSelection, }) { files = files.map(file => ({ ...file, isFile: true })) const docsAndFiles = [...docs, ...files] @@ -32,6 +33,7 @@ function FileTreeFolderList({ folders={folder.folders} docs={folder.docs} files={folder.fileRefs} + shouldShowVisualSelection={shouldShowVisualSelection} /> ) })} @@ -43,6 +45,7 @@ function FileTreeFolderList({ id={doc._id} isFile={doc.isFile} isLinkedFile={doc.linkedFileData && !!doc.linkedFileData.provider} + shouldShowVisualSelection={shouldShowVisualSelection} /> ) })} @@ -60,6 +63,7 @@ FileTreeFolderList.propTypes = { }), dropRef: PropTypes.func, children: PropTypes.node, + shouldShowVisualSelection: PropTypes.bool, } function compareFunction(one, two) { diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-folder.js b/services/web/frontend/js/features/file-tree/components/file-tree-folder.js index 0f319db6f8..3514b4bc96 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-folder.js +++ b/services/web/frontend/js/features/file-tree/components/file-tree-folder.js @@ -14,10 +14,20 @@ import FileTreeItemInner from './file-tree-item/file-tree-item-inner' import FileTreeFolderList from './file-tree-folder-list' import usePersistedState from '../../../shared/hooks/use-persisted-state' -function FileTreeFolder({ name, id, folders, docs, files }) { +function FileTreeFolder({ + name, + id, + folders, + docs, + files, + shouldShowVisualSelection, +}) { const { t } = useTranslation() - const { isSelected, props: selectableEntityProps } = useSelectableEntity(id) + const { isSelected, props: selectableEntityProps } = useSelectableEntity( + id, + shouldShowVisualSelection + ) const { selectedEntityParentIds } = useFileTreeSelectable(id) @@ -87,6 +97,7 @@ function FileTreeFolder({ name, id, folders, docs, files }) { docs={docs} files={files} dropRef={dropRefList} + shouldShowVisualSelection={shouldShowVisualSelection} /> ) : null} @@ -99,6 +110,7 @@ FileTreeFolder.propTypes = { folders: PropTypes.array.isRequired, docs: PropTypes.array.isRequired, files: PropTypes.array.isRequired, + shouldShowVisualSelection: PropTypes.bool, } export default FileTreeFolder diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-root.js b/services/web/frontend/js/features/file-tree/components/file-tree-root.js index 2bf901d6cb..cf631801a9 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-root.js +++ b/services/web/frontend/js/features/file-tree/components/file-tree-root.js @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react' +import React, { useEffect, useState } from 'react' import PropTypes from 'prop-types' import withErrorBoundary from '../../../infrastructure/error-boundary' @@ -31,6 +31,17 @@ const FileTreeRoot = React.memo(function FileTreeRoot({ const { _id: projectId } = useProjectContext(projectContextPropTypes) const { fileTreeData } = useFileTreeData() const isReady = projectId && fileTreeData + const [shouldShowVisualSelection, setShouldShowVisualSelection] = + useState(true) + + const handleFileTreeClick = e => { + if (e.target.classList.contains('bottom-buffer')) { + setShouldShowVisualSelection(false) + return + } + + setShouldShowVisualSelection(e.target !== e.currentTarget) + } useEffect(() => { if (isReady) onInit() @@ -48,8 +59,15 @@ const FileTreeRoot = React.memo(function FileTreeRoot({ {isConnected ? null :
} -
- + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} +
+
@@ -59,7 +77,7 @@ const FileTreeRoot = React.memo(function FileTreeRoot({ ) }) -function FileTreeRootFolder() { +function FileTreeRootFolder({ shouldShowVisualSelection }) { useFileTreeSocketListener() const { fileTreeData } = useFileTreeData() @@ -75,6 +93,7 @@ function FileTreeRootFolder() { classes={{ root: 'file-tree-list' }} dropRef={dropRef} isOver={isOver} + shouldShowVisualSelection={shouldShowVisualSelection} >
  • @@ -82,6 +101,10 @@ function FileTreeRootFolder() { ) } +FileTreeRootFolder.propTypes = { + shouldShowVisualSelection: PropTypes.bool, +} + FileTreeRoot.propTypes = { onSelect: PropTypes.func.isRequired, onInit: PropTypes.func.isRequired, diff --git a/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.js b/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.js index f15832b4c3..be69ef9f21 100644 --- a/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.js +++ b/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.js @@ -150,6 +150,7 @@ export function FileTreeSelectableProvider({ onSelect, children }) { dispatch({ type: ACTION_TYPES.SELECT, id: found.entity._id }) } + window.addEventListener('editor.openDoc', handleOpenDoc) return () => window.removeEventListener('editor.openDoc', handleOpenDoc) }, [fileTreeData]) @@ -202,7 +203,11 @@ const editorContextPropTypes = { permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']), } -export function useSelectableEntity(id, isFile) { +export function useSelectableEntity( + id, + shouldShowVisualSelection = true, + isFile +) { const { view, setView } = useLayoutContext(layoutContextPropTypes) const { selectedEntityIds, selectOrMultiSelectEntity } = useContext( FileTreeSelectableContext @@ -244,7 +249,8 @@ export function useSelectableEntity(id, isFile) { [id, handleEvent, selectedEntityIds] ) - const isVisuallySelected = isSelected && view !== 'pdf' + const isVisuallySelected = + shouldShowVisualSelection && isSelected && view !== 'pdf' const props = useMemo( () => ({ className: classNames({ selected: isVisuallySelected }), diff --git a/services/web/frontend/js/features/history/components/file-tree/history-file-tree-folder-list.tsx b/services/web/frontend/js/features/history/components/file-tree/history-file-tree-folder-list.tsx index fb66c42445..b5950dd012 100644 --- a/services/web/frontend/js/features/history/components/file-tree/history-file-tree-folder-list.tsx +++ b/services/web/frontend/js/features/history/components/file-tree/history-file-tree-folder-list.tsx @@ -11,6 +11,7 @@ import { fileFinalPathname } from '../../utils/file-diff' type HistoryFileTreeFolderListProps = { folders: HistoryFileTree[] docs: HistoryDoc[] + shouldShowVisualSelection: boolean rootClassName?: string children?: ReactNode } @@ -19,6 +20,7 @@ function HistoryFileTreeFolderList({ folders, docs, rootClassName, + shouldShowVisualSelection, children, }: HistoryFileTreeFolderListProps) { const { selection, setSelection } = useHistoryContext() @@ -64,6 +66,7 @@ function HistoryFileTreeFolderList({ name={folder.name} folders={folder.folders} docs={folder.docs ?? []} + shouldShowVisualSelection={shouldShowVisualSelection} /> ))} {docs.map(doc => ( @@ -72,6 +75,7 @@ function HistoryFileTreeFolderList({ name={doc.name} file={doc} selected={ + shouldShowVisualSelection && !!selection.selectedFile && fileFinalPathname(selection.selectedFile) === doc.pathname } diff --git a/services/web/frontend/js/features/history/components/file-tree/history-file-tree-folder.tsx b/services/web/frontend/js/features/history/components/file-tree/history-file-tree-folder.tsx index 1ef4866996..260fb54c88 100644 --- a/services/web/frontend/js/features/history/components/file-tree/history-file-tree-folder.tsx +++ b/services/web/frontend/js/features/history/components/file-tree/history-file-tree-folder.tsx @@ -11,12 +11,14 @@ type HistoryFileTreeFolderProps = { name: string folders: HistoryFileTree[] docs: HistoryDoc[] + shouldShowVisualSelection: boolean } function HistoryFileTreeFolder({ name, folders, docs, + shouldShowVisualSelection, }: HistoryFileTreeFolderProps) { const { t } = useTranslation() @@ -58,7 +60,11 @@ function HistoryFileTreeFolder({
  • {expanded ? ( - + ) : null} ) diff --git a/services/web/frontend/js/features/history/components/history-file-tree.tsx b/services/web/frontend/js/features/history/components/history-file-tree.tsx index 39cf775936..e7d61e21c3 100644 --- a/services/web/frontend/js/features/history/components/history-file-tree.tsx +++ b/services/web/frontend/js/features/history/components/history-file-tree.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { useMemo, useEffect, useState } from 'react' import { orderBy, reduce } from 'lodash' import { useHistoryContext } from '../context/history-context' import { @@ -6,9 +6,12 @@ import { reducePathsToTree, } from '../utils/file-tree' import HistoryFileTreeFolderList from './file-tree/history-file-tree-folder-list' +import { fileTreeContainer } from './history-root' export default function HistoryFileTree() { const { selection } = useHistoryContext() + const [shouldShowVisualSelection, setShouldShowVisualSelection] = + useState(true) const fileTree = useMemo( () => reduce(selection.files, reducePathsToTree, []), @@ -25,11 +28,29 @@ export default function HistoryFileTree() { [sortedFileTree] ) + useEffect(() => { + const listener = function (e: MouseEvent) { + if ((e.target as HTMLElement).classList.contains('bottom-buffer')) { + setShouldShowVisualSelection(false) + return + } + + setShouldShowVisualSelection(e.target !== e.currentTarget) + } + + fileTreeContainer?.addEventListener('click', listener) + + return () => { + fileTreeContainer?.removeEventListener('click', listener) + } + }, []) + return (
  • diff --git a/services/web/frontend/js/features/history/components/history-root.tsx b/services/web/frontend/js/features/history/components/history-root.tsx index 71bfe2e4f8..69f3f0201e 100644 --- a/services/web/frontend/js/features/history/components/history-root.tsx +++ b/services/web/frontend/js/features/history/components/history-root.tsx @@ -8,7 +8,7 @@ import LoadingSpinner from '../../../shared/components/loading-spinner' import { ErrorBoundaryFallback } from '../../../shared/components/error-boundary-fallback' import withErrorBoundary from '../../../infrastructure/error-boundary' -const fileTreeContainer = document.getElementById('history-file-tree') +export const fileTreeContainer = document.getElementById('history-file-tree') function Main() { const { view } = useLayoutContext() diff --git a/services/web/frontend/stylesheets/app/editor/file-tree.less b/services/web/frontend/stylesheets/app/editor/file-tree.less index b40cac76c5..f3fc534ff9 100644 --- a/services/web/frontend/stylesheets/app/editor/file-tree.less +++ b/services/web/frontend/stylesheets/app/editor/file-tree.less @@ -90,8 +90,6 @@ ul.file-tree-list { margin: 0; overflow-x: hidden; - height: 100%; - flex-grow: 1; position: relative; overflow-y: auto; diff --git a/services/web/frontend/stylesheets/app/editor/history-react.less b/services/web/frontend/stylesheets/app/editor/history-react.less index b87d3ea9f0..eb5ab17abc 100644 --- a/services/web/frontend/stylesheets/app/editor/history-react.less +++ b/services/web/frontend/stylesheets/app/editor/history-react.less @@ -384,8 +384,6 @@ history-root { ul.history-file-tree-list { margin: 0; overflow-x: hidden; - height: 100%; - flex-grow: 1; position: relative; overflow-y: auto; diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-root.test.js b/services/web/test/frontend/features/file-tree/components/file-tree-root.test.js index e9e94f01b0..4ad0613a36 100644 --- a/services/web/test/frontend/features/file-tree/components/file-tree-root.test.js +++ b/services/web/test/frontend/features/file-tree/components/file-tree-root.test.js @@ -305,4 +305,38 @@ describe('', function () { // multiple items selected: no menu button is visible expect(screen.queryAllByRole('button', { name: 'Menu' })).to.have.length(0) }) + + it('deselects files when clicked outside the list but inside wrapping container', function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: '456def', name: 'main.tex' }], + folders: [], + fileRefs: [], + }, + ] + renderWithEditorContext( + null} + setRefProviderEnabled={() => null} + setStartedFreeTrial={() => null} + onSelect={onSelect} + onInit={onInit} + isConnected + />, + { + rootFolder, + projectId: '123abc', + rootDocId: '456def', + features: {}, + permissionsLevel: 'owner', + } + ) + + screen.getByRole('treeitem', { selected: true }) + fireEvent.click(screen.getByTestId('file-tree-inner')) + expect(screen.queryByRole('treeitem', { selected: true })).to.be.null + }) })