diff --git a/invokeai/frontend/web/src/common/hooks/useCallbackOnDragEnter.ts b/invokeai/frontend/web/src/common/hooks/useCallbackOnDragEnter.ts new file mode 100644 index 0000000000..f0a1c2ed74 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useCallbackOnDragEnter.ts @@ -0,0 +1,29 @@ +import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'; +import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; +import { dropTargetForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter'; +import { useTimeoutCallback } from 'common/hooks/useTimeoutCallback'; +import type { RefObject } from 'react'; +import { useEffect } from 'react'; + +export const useCallbackOnDragEnter = (cb: () => void, ref: RefObject, delay = 300) => { + const [run, cancel] = useTimeoutCallback(cb, delay); + + useEffect(() => { + const element = ref.current; + if (!element) { + return; + } + return combine( + dropTargetForElements({ + element, + onDragEnter: run, + onDragLeave: cancel, + }), + dropTargetForExternal({ + element, + onDragEnter: run, + onDragLeave: cancel, + }) + ); + }, [cancel, ref, run]); +}; diff --git a/invokeai/frontend/web/src/common/hooks/useTimeoutCallback.ts b/invokeai/frontend/web/src/common/hooks/useTimeoutCallback.ts new file mode 100644 index 0000000000..0406d3eae5 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useTimeoutCallback.ts @@ -0,0 +1,21 @@ +import { useCallback, useMemo, useRef } from 'react'; + +export const useTimeoutCallback = (callback: () => void, delay: number, onCancel?: () => void) => { + const timeoutRef = useRef(null); + const cancel = useCallback(() => { + if (timeoutRef.current !== null) { + window.clearTimeout(timeoutRef.current); + timeoutRef.current = null; + onCancel?.(); + } + }, [onCancel]); + const callWithTimeout = useCallback(() => { + cancel(); + timeoutRef.current = window.setTimeout(() => { + callback(); + timeoutRef.current = null; + }, delay); + }, [callback, cancel, delay]); + const api = useMemo(() => [callWithTimeout, cancel] as const, [callWithTimeout, cancel]); + return api; +}; diff --git a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx index fb29cd6744..8974a9c326 100644 --- a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx +++ b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx @@ -1,10 +1,12 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library'; import { IconButton, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { setActiveTab } from 'features/ui/store/uiSlice'; import type { TabName } from 'features/ui/store/uiTypes'; -import { forwardRef, memo, type ReactElement, useCallback } from 'react'; +import type { ReactElement } from 'react'; +import { memo, useCallback, useRef } from 'react'; const sx: SystemStyleObject = { '&[data-selected=true]': { @@ -12,32 +14,32 @@ const sx: SystemStyleObject = { }, }; -export const TabButton = memo( - forwardRef(({ tab, icon, label }: { tab: TabName; icon: ReactElement; label: string }, ref) => { - const dispatch = useAppDispatch(); - const activeTabName = useAppSelector(selectActiveTab); - const onClick = useCallback(() => { - dispatch(setActiveTab(tab)); - }, [dispatch, tab]); +export const TabButton = memo(({ tab, icon, label }: { tab: TabName; icon: ReactElement; label: string }) => { + const dispatch = useAppDispatch(); + const ref = useRef(null); + const activeTabName = useAppSelector(selectActiveTab); + const selectTab = useCallback(() => { + dispatch(setActiveTab(tab)); + }, [dispatch, tab]); + useCallbackOnDragEnter(selectTab, ref, 300); - return ( - - - - ); - }) -); + return ( + + + + ); +}); TabButton.displayName = 'TabButton';