From faf0d1a7a86a03f6271aeaabdd1f7c431282db5d Mon Sep 17 00:00:00 2001 From: Jacob Rief Date: Thu, 2 Jan 2025 15:24:56 +0100 Subject: [PATCH] replace react-tooltip against @floating-ui/react's tooltip --- client/admin/FolderAdmin.tsx | 2 - client/admin/FolderTabs.tsx | 53 +++++++--- client/admin/MenuBar.tsx | 74 +++++++------ client/admin/SearchField.tsx | 9 +- client/browser/FileSelectDialog.tsx | 8 +- client/browser/FinderFileSelect.tsx | 4 +- client/browser/MenuBar.tsx | 35 +++++-- client/common/DropDownMenu.tsx | 14 +-- client/common/FilterByLabel.tsx | 1 + client/common/SortingOptions.tsx | 1 + client/common/Tooltip.tsx | 156 ++++++++++++++++++++++++++++ client/common/UploadProgress.tsx | 2 +- client/finder-select.ts | 2 +- client/scss/_tooltip.scss | 12 +++ client/scss/finder-admin.scss | 9 +- client/scss/finder-browser.scss | 1 + client/scss/finder-select.scss | 2 + package.json | 5 +- 18 files changed, 312 insertions(+), 78 deletions(-) create mode 100644 client/common/Tooltip.tsx create mode 100644 client/scss/_tooltip.scss diff --git a/client/admin/FolderAdmin.tsx b/client/admin/FolderAdmin.tsx index 168d38f6b..eb7107651 100644 --- a/client/admin/FolderAdmin.tsx +++ b/client/admin/FolderAdmin.tsx @@ -5,7 +5,6 @@ import React, { useRef, useState } from 'react'; -import {Tooltip} from 'react-tooltip'; import { DndContext, DragOverlay, @@ -407,6 +406,5 @@ export default function FolderAdmin() { - ); } diff --git a/client/admin/FolderTabs.tsx b/client/admin/FolderTabs.tsx index b241b66e4..cc2a0daa8 100644 --- a/client/admin/FolderTabs.tsx +++ b/client/admin/FolderTabs.tsx @@ -1,5 +1,6 @@ import {useDroppable} from '@dnd-kit/core'; import React, {forwardRef, useImperativeHandle, useState} from 'react'; +import {Tooltip, TooltipContent, TooltipTrigger} from '../common/Tooltip'; import CloseIcon from '../icons/close.svg'; import PinIcon from '../icons/pin.svg'; import RecycleIcon from '../icons/recycle.svg'; @@ -42,33 +43,61 @@ function FolderTab(props) { } if (folder.id === 'return') return ( -
  • - -
  • + + +
  • + +
  • +
    + {gettext("Change to folder view")} +
    ); if (folder.id === 'parent') return ( -
  • - -
  • + + +
  • + +
  • +
    + {gettext("Change to parent folder")} +
    ); if (folder.is_root) return ( -
  • - {!isActive || isSearchResult ? : } -
  • + + +
  • + {!isActive || isSearchResult ? : } +
  • +
    + {gettext("Root folder")} +
    + ); + + const TrashFolder = () => ( + + + {gettext("Trash folder")} + ); if (folder.is_trash) return ( -
  • - {!isActive || isSearchResult ? : } +
  • + {!isActive || isSearchResult ? : }
  • ); return (
  • {!isActive || isSearchResult || settings.download_url ? {folder.name} : folder.name} - {folder.is_pinned ? : } + {folder.is_pinned ? + : + + + {gettext("Pin this folder")} + + }
  • ); } diff --git a/client/admin/MenuBar.tsx b/client/admin/MenuBar.tsx index cb98705bb..03728d798 100644 --- a/client/admin/MenuBar.tsx +++ b/client/admin/MenuBar.tsx @@ -6,13 +6,13 @@ import React, { useEffect, useImperativeHandle, useMemo, - useRef, useState, } from 'react'; import SearchField from './SearchField'; import DropDownMenu from '../common/DropDownMenu'; import FilterByLabel from '../common/FilterByLabel'; import SortingOptions from '../common/SortingOptions'; +import {Tooltip, TooltipContent, TooltipTrigger} from '../common/Tooltip'; import MoreVerticalIcon from '../icons/more-vertical.svg'; import CopyIcon from '../icons/copy.svg'; import TilesIcon from '../icons/tiles.svg'; @@ -130,6 +130,23 @@ function ExtraMenu(props) { } +function MenuItem(props) { + const {children, tooltip} = props; + const itemProps = Object.fromEntries(Object.entries(props).filter(([key]) => !['children', 'tooltip'].includes(key))); + + return ( + + +
  • + {children} +
  • +
    + {tooltip} +
    + ); +} + + const MenuBar = forwardRef((props: any, forwardedRef) => { const { currentFolderId, @@ -335,48 +352,37 @@ const MenuBar = forwardRef((props: any, forwardedRef) => {
  • -
  • setLayout('tiles')} - role="menuitem" data-tooltip-id="django-finder-tooltip" data-tooltip-content={gettext("Tiles view")}> + setLayout('tiles')} tooltip={gettext("Tiles view")}> -
  • -
  • setLayout('mosaic')} - role="menuitem" data-tooltip-id="django-finder-tooltip" data-tooltip-content={gettext("Mosaic view")}> -
  • -
  • setLayout('list')} - role="menuitem" data-tooltip-id="django-finder-tooltip" data-tooltip-content={gettext("List view")}> -
  • -
  • setLayout('columns')} - role="menuitem" data-tooltip-id="django-finder-tooltip" data-tooltip-content={gettext("Columns view")}> + + setLayout('mosaic')} tooltip={gettext("Mosaic view")}> + + + setLayout('list')} tooltip={gettext("List view")}> + + + setLayout('columns')} tooltip={gettext("Columns view")}> -
  • + {settings.labels && } -
  • + -
  • + {settings.is_trash ? (<> -
  • - -
  • -
  • - -
  • + + + + + + ) : (<> -
  • + -
  • -
  • + + -
  • +
    - + + + + + {gettext("Search for file")} +
  • {gettext("From current folder")}
  • {gettext("In all folders")}
  • diff --git a/client/browser/FileSelectDialog.tsx b/client/browser/FileSelectDialog.tsx index 372ddc921..88f784821 100644 --- a/client/browser/FileSelectDialog.tsx +++ b/client/browser/FileSelectDialog.tsx @@ -90,7 +90,7 @@ const FilesList = memo((props: any) => { }); -const FileSelectDialog = forwardRef((props: any, forwardedRef)=> { +const FileSelectDialog = forwardRef((props: any, forwardedRef) => { const {realm, baseUrl, csrfToken} = props; const [structure, setStructure] = useState({ root_folder: null, @@ -218,13 +218,13 @@ const FileSelectDialog = forwardRef((props: any, forwardedRef)=> { if (structure.last_folder !== folderId) throw new Error('Folder mismatch'); setUploadedFile(uploadedFiles[0]); - }; + } const selectFile = useCallback(fileInfo => { props.selectFile(fileInfo); setUploadedFile(null); refreshFilesList(); - props.closeDialog(); + props.dialogRef.current.close(); }, []); function scrollToCurrentFolder() { @@ -251,7 +251,7 @@ const FileSelectDialog = forwardRef((props: any, forwardedRef)=> { } setUploadedFile(null); refreshFilesList(); - props.closeDialog(); + props.dialogRef.current.close(); } console.log('FileSelectDialog', isDirty, structure); diff --git a/client/browser/FinderFileSelect.tsx b/client/browser/FinderFileSelect.tsx index 9299a4e08..bd6d866d6 100644 --- a/client/browser/FinderFileSelect.tsx +++ b/client/browser/FinderFileSelect.tsx @@ -1,6 +1,5 @@ import React, {useEffect, useRef, useState} from 'react'; import FileSelectDialog from './FileSelectDialog'; -import {Tooltip} from 'react-tooltip'; export default function FinderFileSelect(props) { @@ -111,9 +110,8 @@ export default function FinderFileSelect(props) { baseUrl={baseUrl} csrfToken={csrfToken} selectFile={selectFile} - closeDialog={() => dialogRef.current.close()} + dialogRef={dialogRef} /> - ); } diff --git a/client/browser/MenuBar.tsx b/client/browser/MenuBar.tsx index ccb16fa22..451e2d61b 100644 --- a/client/browser/MenuBar.tsx +++ b/client/browser/MenuBar.tsx @@ -1,15 +1,21 @@ -import React, {forwardRef, useImperativeHandle, useRef} from 'react'; +import React, {forwardRef, useImperativeHandle, useMemo, useRef} from 'react'; import DropDownMenu from '../common/DropDownMenu'; import SortingOptions from '../common/SortingOptions'; import FilterByLabel from '../common/FilterByLabel'; import SearchIcon from '../icons/search.svg'; import UploadIcon from '../icons/upload.svg'; +import {Tooltip, TooltipContent, TooltipTrigger} from "../common/Tooltip"; const MenuBar = forwardRef((props: any, forwardedRef) => { const {openUploader, labels, refreshFilesList, setDirty, setSearchQuery, searchRealm, setSearchRealm} = props; + const ref = useRef(null); const searchRef = useRef(null); + const rootNode = useMemo(() => { + return ref.current?.getRootNode().querySelector('dialog'); + }, [ref.current]); + useImperativeHandle(forwardedRef, () => ({ clearSearch: () => searchRef.current.value = '', })); @@ -43,7 +49,7 @@ const MenuBar = forwardRef((props: any, forwardedRef) => { } return ( -
      +
      • { onKeyDown={handleSearch} />
        - + + + + + {gettext("Search for file")} +
      • {gettext("From current folder")}
      • {gettext("In all folders")}
      • @@ -68,12 +79,16 @@ const MenuBar = forwardRef((props: any, forwardedRef) => {
    - - {labels && } -
  • - -
  • + + {labels && } + + +
  • + +
  • +
    + {gettext("Upload file")} +
    ); }); diff --git a/client/common/DropDownMenu.tsx b/client/common/DropDownMenu.tsx index f7f815de8..6d27f1ab4 100644 --- a/client/common/DropDownMenu.tsx +++ b/client/common/DropDownMenu.tsx @@ -1,9 +1,10 @@ import React, {useEffect, useRef} from 'react'; +import {Tooltip, TooltipTrigger, TooltipContent} from '../common/Tooltip'; export default function DropDownMenu(props){ const ref = useRef(null); - const Wrapper = props.wrapperElement ?? 'li'; + const WrapperElement = props.wrapperElement ?? 'li'; useEffect(() => { const handleClick = (event) => { @@ -38,19 +39,20 @@ export default function DropDownMenu(props){ }, []); return ( - +
    + ) } diff --git a/client/common/FilterByLabel.tsx b/client/common/FilterByLabel.tsx index 448d04dc1..4b2b69021 100644 --- a/client/common/FilterByLabel.tsx +++ b/client/common/FilterByLabel.tsx @@ -28,6 +28,7 @@ export default function FilterByLabel(props: any) { aria-selected={filter.length} className="filter-by-label with-caret" tooltip={gettext("Filter by label")} + root={props.root} >
  • changeFilter(null)}>{gettext("Clear all")}

  • {labels.map((label, index) => ( diff --git a/client/common/SortingOptions.tsx b/client/common/SortingOptions.tsx index bcc7bd2bd..4b1f35c22 100644 --- a/client/common/SortingOptions.tsx +++ b/client/common/SortingOptions.tsx @@ -34,6 +34,7 @@ export default function SortingOptions(props: any) { role="menuitem" className="sorting-options with-caret" tooltip={gettext("Change sorting order")} + root={props.root} >
  • {gettext("Unsorted")} diff --git a/client/common/Tooltip.tsx b/client/common/Tooltip.tsx new file mode 100644 index 000000000..1a91e970a --- /dev/null +++ b/client/common/Tooltip.tsx @@ -0,0 +1,156 @@ +import React, { + createContext, + forwardRef, + isValidElement, + HTMLProps, + ReactNode, + useContext, + useMemo, + useState, +} from 'react'; +import { + useFloating, + autoUpdate, + offset, + flip, + shift, + useHover, + useFocus, + useDismiss, + useRole, + useInteractions, + useMergeRefs, + FloatingPortal +} from '@floating-ui/react'; +import type {Placement} from "@floating-ui/react"; + +interface TooltipOptions { + initialOpen?: boolean; + placement?: Placement; + open?: boolean; + onOpenChange?: (open: boolean) => void; +} + +export function useTooltip({initialOpen=false, placement='bottom', open: controlledOpen, onOpenChange: setControlledOpen}: TooltipOptions = {}) { + const [uncontrolledOpen, setUncontrolledOpen] = useState(initialOpen); + const open = controlledOpen ?? uncontrolledOpen; + const setOpen = setControlledOpen ?? setUncontrolledOpen; + + const data = useFloating({ + placement, + open, + onOpenChange: setOpen, + whileElementsMounted: autoUpdate, + middleware: [ + offset(5), + flip({ + crossAxis: placement.includes('-'), + fallbackAxisSideDirection: 'start', + padding: 5, + }), + shift({padding: 5}), + ] + }); + + const context = data.context; + + const hover = useHover(context, { + move: false, + enabled: controlledOpen == null + }); + const focus = useFocus(context, { + enabled: controlledOpen == null + }); + const dismiss = useDismiss(context); + const role = useRole(context, {role: "tooltip"}); + + const interactions = useInteractions([hover, focus, dismiss, role]); + + return useMemo( + () => ({ + open, + setOpen, + ...interactions, + ...data, + }), + [open, setOpen, interactions, data] + ); +} + +type ContextType = ReturnType | null; + +const TooltipContext = createContext(null); + +export const useTooltipContext = () => { + const context = useContext(TooltipContext); + + if (context == null) { + throw new Error("Tooltip components must be wrapped in "); + } + + return context; +}; + +export function Tooltip({children, ...options}: { children: ReactNode } & TooltipOptions) { + // This can accept any props as options, e.g. `placement`, + // or other positioning options. + const tooltip = useTooltip(options); + return ( + + {children} + + ); +} + +export const TooltipTrigger = forwardRef< + HTMLElement, + HTMLProps & {asChild?: boolean} +>(({children, asChild = false, ...props}, forwardedRef) => { + const context = useTooltipContext(); + const childrenRef = (children as any).ref; + const ref = useMergeRefs([context.refs.setReference, forwardedRef, childrenRef]); + + // `asChild` allows the user to pass any element as the anchor + if (asChild && isValidElement(children)) { + return React.cloneElement( + children, + context.getReferenceProps({ + ref, + ...props, + ...children.props, + 'data-state': context.open ? 'open' : 'closed' + }) + ); + } + + return ( +
    + {children} +
    + ); +}); + + +export const TooltipContent = forwardRef< + HTMLDivElement, + HTMLProps & {root?: HTMLElement} +>(({style, ...props}, forwardedRef) => { + const context = useTooltipContext(); + const refs = useMergeRefs([context.refs.setFloating, forwardedRef]); + + return context.open ? ( + +
    + + ) : null; +}); diff --git a/client/common/UploadProgress.tsx b/client/common/UploadProgress.tsx index 9fe915665..af19c3ecd 100644 --- a/client/common/UploadProgress.tsx +++ b/client/common/UploadProgress.tsx @@ -1,4 +1,4 @@ -import React, {createRef, useEffect, useState} from 'react'; +import React, {useEffect, useState} from 'react'; export function ProgressOverlay(props) { diff --git a/client/finder-select.ts b/client/finder-select.ts index b1c3fab6c..7c82d26ab 100644 --- a/client/finder-select.ts +++ b/client/finder-select.ts @@ -7,7 +7,7 @@ window.addEventListener('DOMContentLoaded', (event) => { 'finder-file-select', r2wc(FinderFileSelect, { props: {'base-url': 'string', 'selected-file': 'json', 'style-url': 'string', realm: 'string'}, - shadow: 'closed', + shadow: 'open', }), ); }); diff --git a/client/scss/_tooltip.scss b/client/scss/_tooltip.scss new file mode 100644 index 000000000..e22c9f080 --- /dev/null +++ b/client/scss/_tooltip.scss @@ -0,0 +1,12 @@ +@import "colors"; + +.django-finder-tooltip { + background-color: $body-fg-color; + opacity: 0.9; + color: $body-bg-color; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + box-sizing: border-box; + width: max-content; + max-width: calc(100vw - 10px); +} diff --git a/client/scss/finder-admin.scss b/client/scss/finder-admin.scss index 539c31c18..0fedbf42b 100644 --- a/client/scss/finder-admin.scss +++ b/client/scss/finder-admin.scss @@ -3,6 +3,7 @@ @import "colors"; @import "progress"; @import "labels"; +@import "tooltip"; // messagelist appears after saving a file ul.messagelist { @@ -107,14 +108,18 @@ ul.messagelist { background-color: var(--primary); } - > svg { + > div { + display: inline-block; + } + + svg { height: 16px; width: 16px; margin: -2px 0; } } - > svg, > a > svg { + svg { display: inline-block; width: 24px; height: 24px; diff --git a/client/scss/finder-browser.scss b/client/scss/finder-browser.scss index 2c949474e..60709b826 100644 --- a/client/scss/finder-browser.scss +++ b/client/scss/finder-browser.scss @@ -3,6 +3,7 @@ @use "menubar"; @import "colors"; @import "progress"; +@import "tooltip"; .finder-file-select { figure { diff --git a/client/scss/finder-select.scss b/client/scss/finder-select.scss index d0c8fc745..4e566edc8 100644 --- a/client/scss/finder-select.scss +++ b/client/scss/finder-select.scss @@ -1,3 +1,5 @@ +@import "tooltip"; + finder-file-select { // hide the original input field without loosing the ability to focus it input { diff --git a/package.json b/package.json index c50f0d6ae..56687ec2b 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "devDependencies": { "@dnd-kit/core": "^6.1.0", "@dnd-kit/modifiers": "^6.0.1", + "@floating-ui/react": "^1.6.12", "@r2wc/react-to-web-component": "^2.0.3", "@types/react-dom": "^18.3.1", "@wavesurfer/react": "^1.0.7", @@ -22,10 +23,12 @@ "react-image-crop": "^11.0.7", "react-intersection-observer": "^9.13.1", "react-player": "^2.16.0", - "react-tooltip": "^5.28.0", "request": "^2.88.2", "sass": "^1.80.4", "typescript": "^5.6.3", "yargs-parser": "^21.1.1" + }, + "dependencies": { + "@floating-ui/react": "^0.27.2" } }