diff --git a/.eslintrc.js b/.eslintrc.js index f6eccee1..b5e108e1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,6 +6,7 @@ module.exports = { extends: [ 'eslint:recommended', 'plugin:react/recommended', + 'plugin:react-hooks/recommended', 'plugin:import/errors', 'plugin:import/warnings', 'plugin:import/typescript', @@ -49,6 +50,7 @@ module.exports = { 'simple-import-sort/sort': 'error', 'react/prop-types': 'off', 'react/display-name': 'off', + 'react-hooks/exhaustive-deps': 'error', 'import/first': 'error', 'import/no-unresolved': 'off', 'import/no-namespace': 'error', @@ -69,5 +71,8 @@ module.exports = { }, settings: { 'import/extensions': ['.js', '.jsx', '.ts', '.tsx'], + react: { + version: 'detect', + }, }, }; diff --git a/package-lock.json b/package-lock.json index fa1f4578..460d225e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7304,6 +7304,12 @@ } } }, + "eslint-plugin-react-hooks": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.0.4.tgz", + "integrity": "sha512-equAdEIsUETLFNCmmCkiCGq6rkSK5MoJhXFPFYeUebcjKgBmWWcgVOqZyQC8Bv1BwVCnTq9tBxgJFgAJTWoJtA==", + "dev": true + }, "eslint-plugin-simple-import-sort": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-5.0.3.tgz", diff --git a/package.json b/package.json index 8895a6c7..9445218f 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "eslint-plugin-import": "^2.21.2", "eslint-plugin-prettier": "^3.1.3", "eslint-plugin-react": "^7.20.0", + "eslint-plugin-react-hooks": "^4.0.4", "eslint-plugin-simple-import-sort": "^5.0.3", "noty": "^3.2.0-beta", "prettier": "^2.0.5", diff --git a/src/components/external/ChonkyIcon.tsx b/src/components/external/ChonkyIcon.tsx index 8dc36dc2..5268a82b 100644 --- a/src/components/external/ChonkyIcon.tsx +++ b/src/components/external/ChonkyIcon.tsx @@ -3,8 +3,8 @@ import { faGitAlt } from '@fortawesome/free-brands-svg-icons/faGitAlt'; import { faLinux } from '@fortawesome/free-brands-svg-icons/faLinux'; import { faNodeJs } from '@fortawesome/free-brands-svg-icons/faNodeJs'; import { faPhp } from '@fortawesome/free-brands-svg-icons/faPhp'; -import { faRust } from '@fortawesome/free-brands-svg-icons/faRust'; import { faPython } from '@fortawesome/free-brands-svg-icons/faPython'; +import { faRust } from '@fortawesome/free-brands-svg-icons/faRust'; import { faUbuntu } from '@fortawesome/free-brands-svg-icons/faUbuntu'; import { faWindows } from '@fortawesome/free-brands-svg-icons/faWindows'; import { faArrowDown } from '@fortawesome/free-solid-svg-icons/faArrowDown'; diff --git a/src/components/external/DropdownButton.tsx b/src/components/external/DropdownButton.tsx index 8c0bebf2..d0a7ebcc 100644 --- a/src/components/external/DropdownButton.tsx +++ b/src/components/external/DropdownButton.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { FileAction } from '../../types/file-actions.types'; import { ChonkyIconName } from '../../types/icons.types'; +import { useFileActionTrigger } from '../../util/file-actions'; import { ChonkyIconFA } from './ChonkyIcon'; -import { useSmartToolbarButtonProps } from './ToolbarButton-hooks'; export interface DropdownButtonProps { text: string; @@ -48,16 +48,16 @@ export const SmartDropdownButton: React.FC = (props) = const { fileAction: action } = props; const { toolbarButton: button } = action; - if (!button) return null; - const { onClick, disabled } = useSmartToolbarButtonProps(action); + const { disabled, triggerAction } = useFileActionTrigger(action); + if (!button) return null; return ( ); diff --git a/src/components/external/FileBrowser.tsx b/src/components/external/FileBrowser.tsx index 5b04748d..b33626cb 100644 --- a/src/components/external/FileBrowser.tsx +++ b/src/components/external/FileBrowser.tsx @@ -26,7 +26,7 @@ import { useClickListener, useStaticValue } from '../../util/hooks-helpers'; import { useFilteredFiles, useSearch } from '../../util/search'; import { useSelection } from '../../util/selection'; import { useSpecialActionDispatcher } from '../../util/special-actions'; -import { useFileBrowserValidation } from '../../util/validation'; +import { useFileArrayValidation } from '../../util/validation'; import { ContextComposer, ContextProviderData } from '../internal/ContextComposer'; import { DnDFileListDragLayer } from '../internal/DnDFileListDragLayer'; import { ErrorMessage } from '../internal/ErrorMessage'; @@ -102,7 +102,7 @@ export const FileBrowser: React.FC = (props) => { const disableSelection = !!props.disableSelection; const enableDragAndDrop = !!props.enableDragAndDrop; - const validationResult = useFileBrowserValidation(files, folderChain); + const validationResult = useFileArrayValidation(files, folderChain); const sortedFiles = validationResult.cleanFiles; const cleanFolderChain = validationResult.cleanFolderChain; @@ -112,9 +112,7 @@ export const FileBrowser: React.FC = (props) => { selection, selectionSize, selectionUtilRef, - selectFiles, - toggleSelection, - clearSelection, + selectionModifiers, } = useSelection(sortedFiles, disableSelection); // TODO: Validate file actions @@ -122,6 +120,10 @@ export const FileBrowser: React.FC = (props) => { // duplicates. const extendedFileActions = [...fileActions, ...DefaultActions]; + // Deal with file text search + const { searchState, searchContexts } = useSearch(); + const filteredFiles = useFilteredFiles(sortedFiles, searchState.searchFilter); + const dispatchFileAction = useFileActionDispatcher( extendedFileActions, onFileAction @@ -130,19 +132,14 @@ export const FileBrowser: React.FC = (props) => { sortedFiles, selection, selectionUtilRef.current, - selectFiles, - toggleSelection, - clearSelection, + selectionModifiers, + searchState.setSearchBarVisible, dispatchFileAction ); - // Deal with file text search - const { searchState, searchContexts } = useSearch(); - const filteredFiles = useFilteredFiles(sortedFiles, searchState.searchFilter); - // Deal with clicks outside of Chonky const chonkyRootRef = useClickListener({ - onOutsideClick: clearSelection, + onOutsideClick: selectionModifiers.clearSelection, }); type ExtractContextType

= P extends React.Context ? T : never; @@ -216,6 +213,7 @@ export const FileBrowser: React.FC = (props) => { provider: data.context.Provider, value: data.value, })), + // eslint-disable-next-line react-hooks/exhaustive-deps contexts.map((data) => data.value) ); diff --git a/src/components/external/FileList-virtualization.tsx b/src/components/external/FileList-virtualization.tsx index 15a0eda5..95c675e3 100644 --- a/src/components/external/FileList-virtualization.tsx +++ b/src/components/external/FileList-virtualization.tsx @@ -45,7 +45,6 @@ export const useEntryRenderer = (files: FileArray) => { const selection = useContext(ChonkySelectionContext); const enableDragAndDrop = useContext(ChonkyEnableDragAndDropContext); // All hook parameters should go into `deps` array - const deps = [files, selection, enableDragAndDrop]; const entryRenderer = useCallback( ( virtualKey: string, @@ -96,7 +95,7 @@ export const useEntryRenderer = (files: FileArray) => { ); }, - deps + [files, selection, enableDragAndDrop] ); return entryRenderer; @@ -128,7 +127,6 @@ export const useGridRenderer = ( thumbsGridRef: React.Ref>, fillParentContainer: boolean ) => { - const deps = [files, entrySize, entryRenderer, thumbsGridRef, fillParentContainer]; return useCallback(({ width, height }) => { const isMobile = isMobileDevice(); const gutter = isMobile ? 5 : 8; @@ -173,5 +171,5 @@ export const useGridRenderer = ( tabIndex={null} /> ); - }, deps); + }, [files, entrySize, entryRenderer, thumbsGridRef, fillParentContainer]); }; diff --git a/src/components/external/FileSearch.tsx b/src/components/external/FileSearch.tsx index 8789a965..df20a7f6 100644 --- a/src/components/external/FileSearch.tsx +++ b/src/components/external/FileSearch.tsx @@ -8,7 +8,6 @@ import c from 'classnames'; import React, { useCallback, useContext, useEffect, useState } from 'react'; import { ChonkyIconName } from '../../types/icons.types'; -import { INTENTIONAL_EMPTY_DEPS } from '../../util/constants'; import { ChonkySearchBarVisibleContext, ChonkySearchFilterContext, @@ -30,43 +29,50 @@ export const FileSearch: React.FC = () => { useEffect(() => { setSearchBarEnabled(true); return () => setSearchBarEnabled(false); - }, INTENTIONAL_EMPTY_DEPS); + }, [setSearchBarEnabled]); // Show a loading indicator during debounce periods to help user realise that a // debounce period is in effect. const [showLoadingIndicator, setShowLoadingIndicator] = useState(false); - // Define a local search filter, and update it when global search filter updates - const [localSearchFilter, setLocalSearchFilter] = useState( - globalSearchFilter - ); - useEffect( - () => { - setShowLoadingIndicator(false); - if (globalSearchFilter === localSearchFilter) return; - setLocalSearchFilter(globalSearchFilter); - }, - - // `localSearchFilter` is deliberately not included in the deps below. This - // is because we don't want to re-set local search filter to itself. - [globalSearchFilter, setShowLoadingIndicator, setLocalSearchFilter] - ); + // Define a local search filter and its debounced version + const [localFilter, setLocalFilter] = useState(globalSearchFilter); + const [debouncedFilter, setDebouncedFilter] = useDebounce(localFilter, 500); - // Set global search filter to local search filter with debounce - const debouncedLocalSearchFilter = useDebounce(localSearchFilter, 500); + // === Debounced global filter update useEffect(() => { setShowLoadingIndicator(false); - const trimmedFilter = debouncedLocalSearchFilter.trim(); - if (trimmedFilter === globalSearchFilter) return; + const trimmedFilter = debouncedFilter.trim(); setGlobalSearchFilter(trimmedFilter); - }, [globalSearchFilter, setShowLoadingIndicator, debouncedLocalSearchFilter]); + }, [debouncedFilter, setShowLoadingIndicator, setGlobalSearchFilter]); + + // === Search bar showing/hiding logic + const inputRef = React.useRef(null); + useEffect(() => { + if (searchBarVisible) { + // When the search bar is shown, focus the input + if (inputRef.current) inputRef.current.focus(); + } else { + // When the search bar is hidden, clear out the search filter + setShowLoadingIndicator(false); + setLocalFilter(''); + setDebouncedFilter(''); + } + }, [ + inputRef, + searchBarVisible, + setShowLoadingIndicator, + setLocalFilter, + setDebouncedFilter, + ]); + // === Text input handler const handleInputChange = useCallback( (event: React.ChangeEvent) => { setShowLoadingIndicator(true); - setLocalSearchFilter(event.target.value); + setLocalFilter(event.target.value); }, - [setShowLoadingIndicator, setLocalSearchFilter] + [setShowLoadingIndicator, setLocalFilter] ); const className = c({ @@ -80,9 +86,10 @@ export const FileSearch: React.FC = () => { diff --git a/src/components/external/FileToolbar-hooks.tsx b/src/components/external/FileToolbar-hooks.tsx index bdbdc571..10accffd 100644 --- a/src/components/external/FileToolbar-hooks.tsx +++ b/src/components/external/FileToolbar-hooks.tsx @@ -19,8 +19,6 @@ import { ToolbarButtonGroup } from './ToolbarButtonGroup'; export const useFolderChainComponent = () => { const folderChain = useContext(ChonkyFolderChainContext); const dispatchChonkyAction = useContext(ChonkyDispatchFileActionContext); - // All hook params should go into `deps` - const deps = [folderChain, dispatchChonkyAction]; const folderChainComponent = useMemo(() => { if (!folderChain) return folderChain; @@ -76,13 +74,12 @@ export const useFolderChainComponent = () => { } } return

{comps}
; - }, deps); + }, [folderChain, dispatchChonkyAction]); return folderChainComponent; }; export const useToolbarButtonGroups = () => { const fileActions = useContext(ChonkyFileActionsContext); - const deps = [fileActions]; return useMemo(() => { // Create an array for normal toolbar buttons const buttonGroups: ToolbarButtonGroup[] = []; @@ -136,5 +133,5 @@ export const useToolbarButtonGroups = () => { } return { buttonGroups, openParentFolderButtonGroup, searchButtonGroup }; - }, deps); + }, [fileActions]); }; diff --git a/src/components/external/ToolbarButton-hooks.tsx b/src/components/external/ToolbarButton-hooks.tsx deleted file mode 100644 index 4afb8830..00000000 --- a/src/components/external/ToolbarButton-hooks.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { useContext, useMemo } from 'react'; -import { Undefinable } from 'tsdef'; - -import { FileAction } from '../../types/file-actions.types'; -import { FileData } from '../../types/files.types'; -import { - ChonkyDispatchFileActionContext, - ChonkyFilesContext, - ChonkyFolderChainContext, - ChonkySearchBarVisibleContext, - ChonkySelectionContext, - ChonkySelectionSizeContext, -} from '../../util/context'; -import { ChonkyActions } from '../../util/file-actions'; -import { SelectionHelper } from '../../util/selection'; - -export const useSmartToolbarButtonProps = (action: FileAction) => { - const files = useContext(ChonkyFilesContext); - const folderChain = useContext(ChonkyFolderChainContext); - const selection = useContext(ChonkySelectionContext); - const selectionSize = useContext(ChonkySelectionSizeContext); - const searchBarVisible = useContext(ChonkySearchBarVisibleContext); - const dispatchChonkyAction = useContext(ChonkyDispatchFileActionContext); - - const parentFolder = - folderChain && folderChain.length > 1 - ? folderChain[folderChain?.length - 2] - : null; - - const deps = [ - action, - files, - selection, - selectionSize, - dispatchChonkyAction, - parentFolder, - ]; - return useMemo(() => { - let actionSelectionSize: Undefinable = undefined; - let actionFiles: Undefinable> = undefined; - if (action.requiresSelection) { - if (action.fileFilter) { - actionSelectionSize = SelectionHelper.getSelectionSize( - files, - selection, - action.fileFilter - ); - actionFiles = SelectionHelper.getSelectedFiles( - files, - selection, - action.fileFilter - ); - } else { - actionSelectionSize = selectionSize; - actionFiles = SelectionHelper.getSelectedFiles(files, selection); - } - } - - const active = action.id === ChonkyActions.ToggleSearch.id && searchBarVisible; - - // Action target is tailored to the "Go up a directory" button at the moment - let actionTarget: Undefinable = undefined; - if (action.requiresParentFolder && parentFolder) { - if (action.fileFilter) { - if (action.fileFilter(parentFolder)) actionTarget = parentFolder; - } else { - actionTarget = parentFolder; - } - } - - const disabled = - (action.requiresSelection && actionSelectionSize === 0) || - (action.requiresParentFolder && !actionTarget); - // TODO: ^^^ Decouple `actionTarget` and `parentFolder`. - - const onClick = () => - dispatchChonkyAction({ - actionId: action.id, - target: actionTarget, - files: actionFiles, - }); - - return { active, onClick, disabled }; - }, deps); -}; diff --git a/src/components/external/ToolbarButton.tsx b/src/components/external/ToolbarButton.tsx index fb8a03c4..52bdcfb1 100644 --- a/src/components/external/ToolbarButton.tsx +++ b/src/components/external/ToolbarButton.tsx @@ -3,8 +3,8 @@ import React from 'react'; import { FileAction } from '../../types/file-actions.types'; import { ChonkyIconName } from '../../types/icons.types'; +import { useFileActionTrigger } from '../../util/file-actions'; import { ChonkyIconFA } from './ChonkyIcon'; -import { useSmartToolbarButtonProps } from './ToolbarButton-hooks'; export interface ToolbarButtonProps { text: string; @@ -68,9 +68,9 @@ export const SmartToolbarButton: React.FC = React.memo( const { fileAction: action } = props; const { toolbarButton: button } = action; - if (!button) return null; - const { active, onClick, disabled } = useSmartToolbarButtonProps(action); + const { active, triggerAction, disabled } = useFileActionTrigger(action); + if (!button) return null; return ( = React.memo( icon={button.icon} iconOnly={button.iconOnly} active={active} - onClick={onClick} + onClick={triggerAction} disabled={disabled} /> ); diff --git a/src/components/internal/BaseFileEntry-hooks.tsx b/src/components/internal/BaseFileEntry-hooks.tsx index 6dcff0c9..35fe2c63 100644 --- a/src/components/internal/BaseFileEntry-hooks.tsx +++ b/src/components/internal/BaseFileEntry-hooks.tsx @@ -28,7 +28,6 @@ export const useThumbnailUrl = ( ) => { const thumbnailGenerator = useContext(ChonkyThumbnailGeneratorContext); - const deps = [file, setThumbnailUrl, setThumbnailLoading, thumbnailGenerator]; useEffect(() => { let loadingCancelled = false; @@ -59,5 +58,5 @@ export const useThumbnailUrl = ( return () => { loadingCancelled = true; }; - }, deps); + }, [file, setThumbnailUrl, setThumbnailLoading, thumbnailGenerator]); }; diff --git a/src/components/internal/ClickableFileEntry-hooks.tsx b/src/components/internal/ClickableFileEntry-hooks.tsx index 00691f8a..22f9a479 100644 --- a/src/components/internal/ClickableFileEntry-hooks.tsx +++ b/src/components/internal/ClickableFileEntry-hooks.tsx @@ -13,7 +13,6 @@ export const useFileClickHandlers = (file: Nullable) => { const dispatchSpecialAction = useContext(ChonkyDispatchSpecialActionContext); // Prepare base handlers - const handlerDeps = [file, dispatchSpecialAction]; const onMouseClick = useCallback( ( event: MouseClickEvent, @@ -22,7 +21,7 @@ export const useFileClickHandlers = (file: Nullable) => { if (!file) return; dispatchSpecialAction({ - actionName: SpecialAction.MouseClickFile, + actionId: SpecialAction.MouseClickFile, clickType, file, altKey: event.altKey, @@ -30,21 +29,24 @@ export const useFileClickHandlers = (file: Nullable) => { shiftKey: event.shiftKey, }); }, - handlerDeps + [file, dispatchSpecialAction] ); - const onKeyboardClick = useCallback((event: KeyboardClickEvent) => { - if (!file) return; + const onKeyboardClick = useCallback( + (event: KeyboardClickEvent) => { + if (!file) return; - dispatchSpecialAction({ - actionName: SpecialAction.KeyboardClickFile, - file, - enterKey: event.enterKey, - spaceKey: event.spaceKey, - altKey: event.altKey, - ctrlKey: event.ctrlKey, - shiftKey: event.shiftKey, - }); - }, handlerDeps); + dispatchSpecialAction({ + actionId: SpecialAction.KeyboardClickFile, + file, + enterKey: event.enterKey, + spaceKey: event.spaceKey, + altKey: event.altKey, + ctrlKey: event.ctrlKey, + shiftKey: event.shiftKey, + }); + }, + [file, dispatchSpecialAction] + ); // Prepare single/double click handlers const onSingleClick = useCallback( diff --git a/src/components/internal/ClickableWrapper-hooks.tsx b/src/components/internal/ClickableWrapper-hooks.tsx index 9b548de6..8568a010 100644 --- a/src/components/internal/ClickableWrapper-hooks.tsx +++ b/src/components/internal/ClickableWrapper-hooks.tsx @@ -4,7 +4,7 @@ * @license MIT */ -import React, { useCallback, useContext, useMemo } from 'react'; +import React, { useCallback, useContext, useRef } from 'react'; import { Nilable, Nullable } from 'tsdef'; import { ChonkyDoubleClickDelayContext } from '../../util/context'; @@ -21,64 +21,65 @@ export const useClickHandler = ( ) => { const doubleClickDelay = useContext(ChonkyDoubleClickDelayContext); - const counter = useMemo( - () => ({ - clickCount: 0, - clickTimeout: null as Nullable, - }), - [] - ); + const counter = useRef({ + clickCount: 0, + clickTimeout: null as Nullable, + }); - const deps = [doubleClickDelay, onSingleClick, onDoubleClick]; - return useCallback((event: React.MouseEvent) => { - const mouseClickEvent: MouseClickEvent = { - altKey: event.altKey, - ctrlKey: event.ctrlKey, - shiftKey: event.shiftKey, - }; + return useCallback( + (event: React.MouseEvent) => { + const mouseClickEvent: MouseClickEvent = { + altKey: event.altKey, + ctrlKey: event.ctrlKey, + shiftKey: event.shiftKey, + }; - counter.clickCount++; - if (counter.clickCount === 1) { - if (onSingleClick) { - event.preventDefault(); - onSingleClick(mouseClickEvent); - } - counter.clickCount = 1; - // @ts-ignore - counter.clickTimeout = setTimeout( - () => (counter.clickCount = 0), - doubleClickDelay - ); - } else if (counter.clickCount === 2) { - if (onDoubleClick) { - event.preventDefault(); - onDoubleClick(mouseClickEvent); - } - if (typeof counter.clickTimeout === 'number') { - clearTimeout(counter.clickTimeout); - counter.clickTimeout = null; - counter.clickCount = 0; + counter.current.clickCount++; + if (counter.current.clickCount === 1) { + if (onSingleClick) { + event.preventDefault(); + onSingleClick(mouseClickEvent); + } + counter.current.clickCount = 1; + // @ts-ignore + counter.current.clickTimeout = setTimeout( + () => (counter.current.clickCount = 0), + doubleClickDelay + ); + } else if (counter.current.clickCount === 2) { + if (onDoubleClick) { + event.preventDefault(); + onDoubleClick(mouseClickEvent); + } + if (typeof counter.current.clickTimeout === 'number') { + clearTimeout(counter.current.clickTimeout); + counter.current.clickTimeout = null; + counter.current.clickCount = 0; + } } - } - }, deps); + }, + [doubleClickDelay, onSingleClick, onDoubleClick, counter] + ); }; export const useKeyDownHandler = (onKeyboardClick?: KeyboardClickEventHandler) => { - const deps = [onKeyboardClick]; - return useCallback((event: React.KeyboardEvent) => { - if (!onKeyboardClick) return; + return useCallback( + (event: React.KeyboardEvent) => { + if (!onKeyboardClick) return; - const keyboardClickEvent: KeyboardClickEvent = { - enterKey: event.nativeEvent.code === 'Enter', - spaceKey: event.nativeEvent.code === 'Space', - altKey: event.altKey, - ctrlKey: event.ctrlKey, - shiftKey: event.shiftKey, - }; + const keyboardClickEvent: KeyboardClickEvent = { + enterKey: event.nativeEvent.code === 'Enter', + spaceKey: event.nativeEvent.code === 'Space', + altKey: event.altKey, + ctrlKey: event.ctrlKey, + shiftKey: event.shiftKey, + }; - if (keyboardClickEvent.spaceKey || keyboardClickEvent.enterKey) { - event.preventDefault(); - onKeyboardClick(keyboardClickEvent); - } - }, deps); + if (keyboardClickEvent.spaceKey || keyboardClickEvent.enterKey) { + event.preventDefault(); + onKeyboardClick(keyboardClickEvent); + } + }, + [onKeyboardClick] + ); }; diff --git a/src/components/internal/DnDFileEntry.tsx b/src/components/internal/DnDFileEntry.tsx index e63c8587..c5e32eb1 100644 --- a/src/components/internal/DnDFileEntry.tsx +++ b/src/components/internal/DnDFileEntry.tsx @@ -35,7 +35,7 @@ export const DnDFileEntry: React.FC = (props) => { if (!FileHelper.isDraggable(file)) return; dispatchSpecialAction({ - actionName: SpecialAction.DragNDropStart, + actionId: SpecialAction.DragNDropStart, dragSource: file, }); }, [dispatchSpecialAction, file]); @@ -51,7 +51,7 @@ export const DnDFileEntry: React.FC = (props) => { } dispatchSpecialAction({ - actionName: SpecialAction.DragNDropEnd, + actionId: SpecialAction.DragNDropEnd, dragSource: file, dropTarget: dropResult.dropTarget, dropEffect: dropResult.dropEffect, @@ -73,7 +73,7 @@ export const DnDFileEntry: React.FC = (props) => { ); const canDrop = useCallback( (item: DnDFileEntryItem) => { - const isSameFile = item.file?.id === file?.id; + const isSameFile = file && item.file && file.id === item.file.id; return FileHelper.isDroppable(file) && !isSameFile; }, [file] @@ -103,7 +103,7 @@ export const DnDFileEntry: React.FC = (props) => { // Set drag preview to an empty image because `DnDFileListDragLayer` will // provide its own preview. preview(getEmptyImage(), { captureDraggingState: true }); - }, []); + }, [preview]); return (
[]; export type ReadonlyFileArray = ReadonlyArray>; export type FileFilter = (file: Nullable) => boolean; - -export interface FileSelection { - // `true` means selected, anything else - not selected - [fileId: string]: Undefinable; -} diff --git a/src/types/react.types.ts b/src/types/react.types.ts new file mode 100644 index 00000000..cf5cca67 --- /dev/null +++ b/src/types/react.types.ts @@ -0,0 +1,3 @@ +import React from 'react'; + +export type ReactStateSetter = React.Dispatch>; diff --git a/src/types/selection.types.ts b/src/types/selection.types.ts new file mode 100644 index 00000000..5371fcc4 --- /dev/null +++ b/src/types/selection.types.ts @@ -0,0 +1,12 @@ +import { Undefinable } from 'tsdef'; + +export interface FileSelection { + // `true` means selected, anything else - not selected + [fileId: string]: Undefinable; +} + +export interface SelectionModifiers { + selectFiles: (fileIds: string[], reset?: boolean) => void; + toggleSelection: (fileId: string, exclusive?: boolean) => void; + clearSelection: () => void; +} diff --git a/src/types/special-actions.types.ts b/src/types/special-actions.types.ts index a7ee1d4c..4963d81b 100644 --- a/src/types/special-actions.types.ts +++ b/src/types/special-actions.types.ts @@ -11,7 +11,7 @@ export enum SpecialAction { } export interface SpecialFileMouseClickAction { - actionName: SpecialAction.MouseClickFile; + actionId: SpecialAction.MouseClickFile; file: FileData; altKey: boolean; ctrlKey: boolean; @@ -20,7 +20,7 @@ export interface SpecialFileMouseClickAction { } export interface SpecialFileKeyboardClickAction { - actionName: SpecialAction.KeyboardClickFile; + actionId: SpecialAction.KeyboardClickFile; file: FileData; enterKey: boolean; spaceKey: boolean; @@ -30,16 +30,16 @@ export interface SpecialFileKeyboardClickAction { } export interface SpecialToggleSearchBarAction { - actionName: SpecialAction.KeyboardClickFile; + actionId: SpecialAction.ToggleSearchBar; } export interface SpecialDragNDropStartAction { - actionName: SpecialAction.DragNDropStart; + actionId: SpecialAction.DragNDropStart; dragSource: FileData; } export interface SpecialDragNDropEndAction { - actionName: SpecialAction.DragNDropEnd; + actionId: SpecialAction.DragNDropEnd; dragSource: FileData; dropTarget: FileData; dropEffect: 'move' | 'copy'; @@ -51,4 +51,5 @@ export type SpecialActionData = | SpecialToggleSearchBarAction | SpecialDragNDropStartAction | SpecialDragNDropEndAction; + export type InternalSpecialActionDispatcher = (actionData: SpecialActionData) => void; diff --git a/src/util/context.ts b/src/util/context.ts index f5b454eb..7d8a0336 100644 --- a/src/util/context.ts +++ b/src/util/context.ts @@ -4,7 +4,8 @@ import { Nullable } from 'tsdef'; import { DefaultEntrySize } from '../components/external/FileList-virtualization'; import { FileAction, InternalFileActionDispatcher } from '../types/file-actions.types'; import { FileEntrySize } from '../types/file-list-view.types'; -import { FileArray, FileSelection } from '../types/files.types'; +import { FileArray} from '../types/files.types'; +import { FileSelection } from '../types/selection.types'; import { InternalSpecialActionDispatcher } from '../types/special-actions.types'; import { ThumbnailGenerator } from '../types/thumbnails.types'; import { NOOP_FUNCTION } from './constants'; diff --git a/src/util/file-actions.ts b/src/util/file-actions.ts index ace30fec..771990a4 100644 --- a/src/util/file-actions.ts +++ b/src/util/file-actions.ts @@ -1,6 +1,6 @@ import Promise from 'bluebird'; -import { useCallback, useMemo } from 'react'; -import { Nullable } from 'tsdef'; +import { useCallback, useContext, useMemo } from 'react'; +import { Nullable, Undefinable } from 'tsdef'; import { FileAction, @@ -10,8 +10,18 @@ import { import { FileData } from '../types/files.types'; import { ChonkyIconName } from '../types/icons.types'; import { SpecialAction } from '../types/special-actions.types'; +import { + ChonkyDispatchFileActionContext, + ChonkyDispatchSpecialActionContext, + ChonkyFilesContext, + ChonkyFolderChainContext, + ChonkySearchBarVisibleContext, + ChonkySelectionContext, + ChonkySelectionSizeContext, +} from './context'; import { FileHelper } from './file-helper'; import { Logger } from './logger'; +import { SelectionHelper } from './selection'; import { isFunction } from './validation'; export const ChonkyActions = { @@ -113,7 +123,6 @@ export const useFileActionDispatcher = ( fileActions: FileAction[], onFileAction: Nullable ): InternalFileActionDispatcher => { - const actionMapDeps = [fileActions]; const actionMap = useMemo(() => { const actionMap = {}; if (Array.isArray(fileActions)) { @@ -122,9 +131,8 @@ export const useFileActionDispatcher = ( } } return actionMap; - }, actionMapDeps); + }, [fileActions]); - const dispatchFileActionDeps = [actionMap, onFileAction]; const dispatchFileAction: InternalFileActionDispatcher = useCallback( (actionData) => { const { actionId } = actionData; @@ -147,8 +155,98 @@ export const useFileActionDispatcher = ( ); } }, - dispatchFileActionDeps + [actionMap, onFileAction] ); return dispatchFileAction; }; + +export const useFileActionTrigger = (action: FileAction) => { + const files = useContext(ChonkyFilesContext); + const folderChain = useContext(ChonkyFolderChainContext); + const selection = useContext(ChonkySelectionContext); + const selectionSize = useContext(ChonkySelectionSizeContext); + const searchBarVisible = useContext(ChonkySearchBarVisibleContext); + const dispatchFileAction = useContext(ChonkyDispatchFileActionContext); + const dispatchSpecialAction = useContext(ChonkyDispatchSpecialActionContext); + + const parentFolder = + folderChain && folderChain.length > 1 + ? folderChain[folderChain?.length - 2] + : null; + + return useMemo(() => { + let actionSelectionSize: Undefinable = undefined; + let actionFiles: Undefinable> = undefined; + if (action.requiresSelection) { + if (action.fileFilter) { + actionSelectionSize = SelectionHelper.getSelectionSize( + files, + selection, + action.fileFilter + ); + actionFiles = SelectionHelper.getSelectedFiles( + files, + selection, + action.fileFilter + ); + } else { + actionSelectionSize = selectionSize; + actionFiles = SelectionHelper.getSelectedFiles(files, selection); + } + } + + const active = action.id === ChonkyActions.ToggleSearch.id && searchBarVisible; + + // Action target is tailored to the "Go up a directory" button at the moment + let actionTarget: Undefinable = undefined; + if (action.requiresParentFolder && parentFolder) { + if (action.fileFilter) { + if (action.fileFilter(parentFolder)) actionTarget = parentFolder; + } else { + actionTarget = parentFolder; + } + } + + const disabled = + (action.requiresSelection && actionSelectionSize === 0) || + (action.requiresParentFolder && !actionTarget); + // TODO: ^^^ Decouple `actionTarget` and `parentFolder`. + + const triggerAction = () => { + if (action.specialActionToDispatch) { + const specialActionId = action.specialActionToDispatch; + + switch (specialActionId) { + case SpecialAction.ToggleSearchBar: + dispatchSpecialAction({ + actionId: specialActionId, + }); + break; + default: + Logger.error( + `File action "${action.id}" tried to dispatch the ` + + `special action "${specialActionId}", but no ` + + `transform was defined for this action.` + ); + } + } + dispatchFileAction({ + actionId: action.id, + target: actionTarget, + files: actionFiles, + }); + }; + + return { active, disabled, triggerAction }; + }, [ + action, + files, + selection, + selectionSize, + searchBarVisible, + dispatchFileAction, + dispatchSpecialAction, + parentFolder, + ]); +}; diff --git a/src/util/file-icon-helper.ts b/src/util/file-icon-helper.ts index 989eef78..f057585d 100644 --- a/src/util/file-icon-helper.ts +++ b/src/util/file-icon-helper.ts @@ -107,7 +107,6 @@ const getIconTrie = memoize(() => { }); export const useIconData = (file: Nullable): FileIconData => { - const deps = [file]; return useMemo(() => { if (!file) return { icon: ChonkyIconName.loading, colorCode: 0 }; if (file.isDir === true) return { icon: ChonkyIconName.folder, colorCode: 0 }; @@ -115,7 +114,7 @@ export const useIconData = (file: Nullable): FileIconData => { const iconTrie = getIconTrie(); const match = iconTrie.getWithCheckpoints(file.name, '.', true); return match ? match : { icon: ChonkyIconName.file, colorCode: 32 }; - }, deps); + }, [file]); }; export const VideoExtensions: string[] = [ diff --git a/src/util/hooks-helpers.ts b/src/util/hooks-helpers.ts index f29f1dba..5fa0f6e4 100644 --- a/src/util/hooks-helpers.ts +++ b/src/util/hooks-helpers.ts @@ -1,6 +1,9 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; -export const useDebounce = (value: T, delay: number): T => { +export const useDebounce = ( + value: T, + delay: number +): [T, React.Dispatch>] => { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { @@ -13,7 +16,7 @@ export const useDebounce = (value: T, delay: number): T => { }; }, [value, delay]); - return debouncedValue; + return [debouncedValue, setDebouncedValue]; }; const UNINITIALIZED_SENTINEL = {}; @@ -32,7 +35,8 @@ interface UseClickListenerParams { export const useClickListener = ( params: UseClickListenerParams ) => { - const triggerComponentRef = useRef(); + const { onClick, onInsideClick, onOutsideClick } = params; + const triggerComponentRef = useRef(null); const clickListener = useCallback( (event: MouseEvent) => { @@ -41,20 +45,15 @@ export const useClickListener = ( triggerComponentRef.current.contains(event.target as any) ) { // Click originated from inside. - if (params.onInsideClick) params.onInsideClick(event); + if (onInsideClick) onInsideClick(event); } else { // Click originated from outside inside. - if (params.onOutsideClick) params.onOutsideClick(event); + if (onOutsideClick) onOutsideClick(event); } - if (params.onClick) params.onClick(event); + if (onClick) onClick(event); }, - [ - params.onClick, - params.onInsideClick, - params.onOutsideClick, - triggerComponentRef, - ] + [onClick, onInsideClick, onOutsideClick, triggerComponentRef] ); useEffect(() => { @@ -64,5 +63,5 @@ export const useClickListener = ( }; }, [clickListener]); - return triggerComponentRef as React.RefObject; + return triggerComponentRef; }; diff --git a/src/util/search.ts b/src/util/search.ts index 2791fd8e..768f084f 100644 --- a/src/util/search.ts +++ b/src/util/search.ts @@ -63,7 +63,6 @@ const useSearchContexts = (searchState: ReturnType) => { }; export const useFilteredFiles = (files: FileArray, searchFilter: string): FileArray => { - const deps = [files, searchFilter]; return useMemo(() => { if (!searchFilter) return files; const searcher = new FuzzySearch( @@ -72,5 +71,5 @@ export const useFilteredFiles = (files: FileArray, searchFilter: string): FileAr { caseSensitive: false, sort: true } ); return searcher.search(searchFilter); - }, deps); + }, [files, searchFilter]); }; diff --git a/src/util/selection.ts b/src/util/selection.ts index 07bc650d..8d0c4e68 100644 --- a/src/util/selection.ts +++ b/src/util/selection.ts @@ -5,9 +5,9 @@ import { FileArray, FileData, FileFilter, - FileSelection, ReadonlyFileArray, } from '../types/files.types'; +import { FileSelection, SelectionModifiers } from '../types/selection.types'; import { FileHelper } from './file-helper'; export const useSelection = (files: FileArray, disableSelection: boolean) => { @@ -23,10 +23,7 @@ export const useSelection = (files: FileArray, disableSelection: boolean) => { // Create callbacks for updating selection. These will update the React // state `selection`, causing re-renders. This is intentional. - const { selectFiles, toggleSelection, clearSelection } = useSelectionModifiers( - disableSelection, - setSelection - ); + const selectionModifiers = useSelectionModifiers(disableSelection, setSelection); // Create selection ref for functions that need selection but shouldn't re-render const selectionUtilRef = useRef( @@ -40,26 +37,26 @@ export const useSelection = (files: FileArray, disableSelection: boolean) => { selection, selectionSize, selectionUtilRef, - selectFiles, - toggleSelection, - clearSelection, + selectionModifiers, }; }; const useSelectionModifiers = ( disableSelection: boolean, setSelection: React.Dispatch> -) => { - const deps = [disableSelection, setSelection]; - const selectFiles = useCallback((fileIds: string[], reset: boolean = true) => { - if (disableSelection) return; +): SelectionModifiers => { + const selectFiles = useCallback( + (fileIds: string[], reset: boolean = true) => { + if (disableSelection) return; - setSelection((selection) => { - const newSelection = reset ? {} : { ...selection }; - for (const fileId of fileIds) newSelection[fileId] = true; - return newSelection; - }); - }, deps); + setSelection((selection) => { + const newSelection = reset ? {} : { ...selection }; + for (const fileId of fileIds) newSelection[fileId] = true; + return newSelection; + }); + }, + [disableSelection, setSelection] + ); const toggleSelection = useCallback( (fileId: string, exclusive: boolean = false) => { if (disableSelection) return; @@ -74,19 +71,24 @@ const useSelectionModifiers = ( return newSelection; }); }, - deps + [disableSelection, setSelection] ); const clearSelection = useCallback(() => { if (disableSelection) return; setSelection({}); - }, deps); + }, [disableSelection, setSelection]); + + const selectionModifiers = useMemo( + () => ({ + selectFiles, + toggleSelection, + clearSelection, + }), + [selectFiles, toggleSelection, clearSelection] + ); - return { - selectFiles, - toggleSelection, - clearSelection, - }; + return selectionModifiers; }; /** diff --git a/src/util/special-actions.ts b/src/util/special-actions.ts index d3d80e88..b25b8f9b 100644 --- a/src/util/special-actions.ts +++ b/src/util/special-actions.ts @@ -1,7 +1,9 @@ -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { InternalFileActionDispatcher } from '../types/file-actions.types'; -import { FileArray, FileSelection } from '../types/files.types'; +import { FileArray } from '../types/files.types'; +import { ReactStateSetter } from '../types/react.types'; +import { FileSelection, SelectionModifiers } from '../types/selection.types'; import { InternalSpecialActionDispatcher, SpecialAction, @@ -10,18 +12,11 @@ import { SpecialDragNDropStartAction, SpecialFileKeyboardClickAction, SpecialFileMouseClickAction, - SpecialToggleSearchBarAction, } from '../types/special-actions.types'; -import { INTENTIONAL_EMPTY_DEPS } from './constants'; import { ChonkyActions } from './file-actions'; import { FileHelper } from './file-helper'; import { Logger } from './logger'; -import { SelectionUtil, useSelection } from './selection'; - -interface SpecialMutableChonkyState { - files: FileArray; - selection: FileSelection; -} +import { SelectionUtil } from './selection'; /** * Returns a dispatch method meant to be used by child components. This dispatch @@ -32,73 +27,51 @@ export const useSpecialActionDispatcher = ( files: FileArray, selection: FileSelection, selectionUtil: SelectionUtil, - selectFiles: ReturnType['selectFiles'], - toggleSelection: ReturnType['toggleSelection'], - clearSelection: ReturnType['clearSelection'], + selectionModifiers: SelectionModifiers, + setSearchBarVisible: ReactStateSetter, dispatchFileAction: InternalFileActionDispatcher ): InternalSpecialActionDispatcher => { - // Generate mutable Chonky state object so that special action handlers can use - // up-to-date state without triggering re-renders - const specialStateDeps = [files, selection]; - const specialState = useMemo( - () => ({ - files, - selection, - }), - INTENTIONAL_EMPTY_DEPS - ); - useEffect(() => { - specialState.files = files; - specialState.selection = selection; - }, specialStateDeps); - // Create the special action handler map const specialActionHandlerMap = useSpecialFileActionHandlerMap( selectionUtil, - selectFiles, - toggleSelection, - clearSelection, + selectionModifiers, + setSearchBarVisible, dispatchFileAction ); // Process special actions using the handlers from the map - const dispatchSpecialActionDeps = [specialActionHandlerMap]; - const dispatchSpecialAction = useCallback((actionData: SpecialActionData) => { - const { actionName } = actionData; - const handler = specialActionHandlerMap[actionName]; - if (handler) { - try { - handler(actionData); - } catch (error) { + const dispatchSpecialAction = useCallback( + (actionData: SpecialActionData) => { + const { actionId } = actionData; + const handler = specialActionHandlerMap[actionId]; + if (handler) { + try { + handler(actionData); + } catch (error) { + Logger.error( + `Handler for special action "${actionId}" threw an error.`, + error + ); + } + } else { Logger.error( - `Handler for special action "${actionName}" threw an error.`, - error + `Internal components dispatched a "${actionId}" special action, ` + + `but no internal handler is available to process it.` ); } - } else { - Logger.error( - `Internal components dispatched a "${actionName}" special action, ` + - `but no internal handler is available to process it.` - ); - } - }, dispatchSpecialActionDeps); + }, + [specialActionHandlerMap] + ); return dispatchSpecialAction; }; export const useSpecialFileActionHandlerMap = ( selectionUtil: SelectionUtil, - selectFiles: ReturnType['selectFiles'], - toggleSelection: ReturnType['toggleSelection'], - clearSelection: ReturnType['clearSelection'], + selectionModifiers: SelectionModifiers, + setSearchBarVisible: ReactStateSetter, dispatchFileAction: InternalFileActionDispatcher ) => { // Define handlers in a map - const specialActionHandlerMapDeps = [ - selectFiles, - toggleSelection, - clearSelection, - dispatchFileAction, - ]; const specialActionHandlerMap = useMemo( () => ({ @@ -118,10 +91,13 @@ export const useSpecialFileActionHandlerMap = ( }); } else { if (FileHelper.isSelectable(data.file)) { - toggleSelection(data.file.id, !data.ctrlKey); + selectionModifiers.toggleSelection( + data.file.id, + !data.ctrlKey + ); // TODO: Handle range selections. } else { - if (!data.ctrlKey) clearSelection(); + if (!data.ctrlKey) selectionModifiers.clearSelection(); } } }, @@ -137,21 +113,20 @@ export const useSpecialFileActionHandlerMap = ( ), }); } else if (data.spaceKey && FileHelper.isSelectable(data.file)) { - toggleSelection(data.file.id, data.ctrlKey); + selectionModifiers.toggleSelection(data.file.id, data.ctrlKey); + // TODO: Handle range selections. } }, - [SpecialAction.ToggleSearchBar]: ( - data: SpecialToggleSearchBarAction - ) => { - // TODO: Flip the search bar visibility switch here. + [SpecialAction.ToggleSearchBar]: () => { + setSearchBarVisible((visible) => !visible); }, [SpecialAction.DragNDropStart]: (data: SpecialDragNDropStartAction) => { const file = data.dragSource; if (!selectionUtil.isSelected(file)) { - clearSelection(); + selectionModifiers.clearSelection(); if (FileHelper.isSelectable(file)) { - selectFiles([file.id]); + selectionModifiers.selectFiles([file.id]); } } }, @@ -175,8 +150,8 @@ export const useSpecialFileActionHandlerMap = ( files: droppedFiles, }); }, - } as { [actionName in SpecialAction]: (data: SpecialActionData) => void }), - specialActionHandlerMapDeps + } as { [actionId in SpecialAction]: (data: SpecialActionData) => void }), + [selectionUtil, selectionModifiers, setSearchBarVisible, dispatchFileAction] ); return specialActionHandlerMap; }; diff --git a/src/util/validation.ts b/src/util/validation.ts index 9b3276ae..1bd6a7d0 100644 --- a/src/util/validation.ts +++ b/src/util/validation.ts @@ -13,6 +13,7 @@ export const isFunction = (value: any): value is Function => { }; export const isMobileDevice = () => { + // noinspection JSDeprecatedSymbols return ( typeof window.orientation !== 'undefined' || navigator.userAgent.indexOf('IEMobile') !== -1 @@ -28,7 +29,7 @@ export const isMobileDevice = () => { * - some files are missing `name` field * - some files have invalid type (they are neither an object nor `null`) */ -export const useCleanFileArray = ( +export const cleanupFileArray = ( fileArray: AllowNull extends false ? FileArray : Nullable, allowNull: AllowNull ): { @@ -133,7 +134,7 @@ export interface ErrorMessageData { bullets: string[]; } -export const useFileBrowserValidation = ( +export const useFileArrayValidation = ( files: FileArray, folderChain: Nullable ): { @@ -141,11 +142,10 @@ export const useFileBrowserValidation = ( cleanFolderChain: Nullable; errorMessages: ErrorMessageData[]; } => { - const filesDeps = [files]; const { cleanFiles, errorMessages: filesErrorMessages } = useMemo(() => { const errorMessages: ErrorMessageData[] = []; - const cleanFilesResult = useCleanFileArray(files, false); + const cleanFilesResult = cleanupFileArray(files, false); if (cleanFilesResult.warningMessage) { const errorMessage = `The "files" prop passed to ${FileBrowser.name} did not pass validation. ` + @@ -165,36 +165,35 @@ export const useFileBrowserValidation = ( cleanFiles: cleanFilesResult.cleanFileArray, errorMessages, }; - }, filesDeps); - - const folderChainDeps = [folderChain]; - const { cleanFolderChain, errorMessages: folderChainErrorMessages } = useMemo( - () => { - const errorMessages: ErrorMessageData[] = []; - - const cleanFolderChainResult = useCleanFileArray(folderChain, true); - if (cleanFolderChainResult.warningMessage) { - const errorMessage = - `The "folderChain" prop passed to ${FileBrowser.name} did not pass validation. ` + - `${cleanFolderChainResult.warningMessage} ` + - `The following errors were encountered:`; - Logger.error( - errorMessage, - Logger.formatBullets(cleanFolderChainResult.warningBullets) - ); - errorMessages.push({ - message: errorMessage, - bullets: cleanFolderChainResult.warningBullets, - }); - } + }, [files]); - return { - cleanFolderChain: cleanFolderChainResult.cleanFileArray, - errorMessages, - }; - }, - folderChainDeps - ); + const { + cleanFolderChain, + errorMessages: folderChainErrorMessages, + } = useMemo(() => { + const errorMessages: ErrorMessageData[] = []; + + const cleanFolderChainResult = cleanupFileArray(folderChain, true); + if (cleanFolderChainResult.warningMessage) { + const errorMessage = + `The "folderChain" prop passed to ${FileBrowser.name} did not pass validation. ` + + `${cleanFolderChainResult.warningMessage} ` + + `The following errors were encountered:`; + Logger.error( + errorMessage, + Logger.formatBullets(cleanFolderChainResult.warningBullets) + ); + errorMessages.push({ + message: errorMessage, + bullets: cleanFolderChainResult.warningBullets, + }); + } + + return { + cleanFolderChain: cleanFolderChainResult.cleanFileArray, + errorMessages, + }; + }, [folderChain]); return { cleanFiles, diff --git a/style/_file-search.scss b/style/_file-search.scss index 0595ecdc..31934e47 100644 --- a/style/_file-search.scss +++ b/style/_file-search.scss @@ -1,11 +1,15 @@ @import 'util'; .chonky-file-search { + @include default-transition(height); margin-bottom: $file-browser-padding; margin-top: -$file-browser-padding; height: $searchbar-height; + opacity: 1; @at-root &-hidden { + height: 0 !important; + opacity: 0 !important; } &-input-group { diff --git a/style/main.css b/style/main.css index 03a4848e..f0e6bb86 100644 --- a/style/main.css +++ b/style/main.css @@ -385,10 +385,20 @@ button.chonky-folder-chain-entry:active { } .chonky-file-search { + -webkit-transition: height 100ms ease-in-out; + -moz-transition: height 100ms ease-in-out; + -o-transition: height 100ms ease-in-out; + transition: height 100ms ease-in-out; margin-bottom: 7.5px; margin-top: -7.5px; height: 28px; + opacity: 1; +} +.chonky-file-search-hidden { + height: 0 !important; + opacity: 0 !important; } + .chonky-file-search-input-group label, .chonky-file-search-input-group input { line-height: 26px; diff --git a/style/main.css.map b/style/main.css.map index efeeab59..f226bdb9 100644 --- a/style/main.css.map +++ b/style/main.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["_shared.scss","_util.scss","_text-placeholder.scss","_file-toolbar.scss","_file-toolbar-buttons.scss","_file-search.scss","_file-list.scss","_file-entry.scss"],"names":[],"mappings":"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","file":"main.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["_shared.scss","_util.scss","_text-placeholder.scss","_file-toolbar.scss","_file-toolbar-buttons.scss","_file-search.scss","_file-list.scss","_file-entry.scss"],"names":[],"mappings":"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","file":"main.css"} \ No newline at end of file