diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index b4e902f0947..5f0b96974f3 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -8,7 +8,7 @@ import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/ap import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import type { PartialAppConfig } from 'app/types/invokeai'; import ImageUploadOverlay from 'common/components/ImageUploadOverlay'; -import { useFocusRegionWatcher } from 'common/hooks/interactionScopes'; +import { useFocusRegionWatcher } from 'common/hooks/focus'; import { useClearStorage } from 'common/hooks/useClearStorage'; import { useFullscreenDropzone } from 'common/hooks/useFullscreenDropzone'; import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys'; diff --git a/invokeai/frontend/web/src/app/components/GlobalImageHotkeys.tsx b/invokeai/frontend/web/src/app/components/GlobalImageHotkeys.tsx index cd0c106f86e..1009ae467dc 100644 --- a/invokeai/frontend/web/src/app/components/GlobalImageHotkeys.tsx +++ b/invokeai/frontend/web/src/app/components/GlobalImageHotkeys.tsx @@ -1,6 +1,6 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { useAppSelector } from 'app/store/storeHooks'; -import { useIsRegionFocused } from 'common/hooks/interactionScopes'; +import { useIsRegionFocused } from 'common/hooks/focus'; import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { useImageActions } from 'features/gallery/hooks/useImageActions'; import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors'; diff --git a/invokeai/frontend/web/src/common/hooks/focus.ts b/invokeai/frontend/web/src/common/hooks/focus.ts new file mode 100644 index 00000000000..a5d4e1de443 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/focus.ts @@ -0,0 +1,182 @@ +import { useStore } from '@nanostores/react'; +import { logger } from 'app/logging/logger'; +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import type { Atom } from 'nanostores'; +import { atom, computed } from 'nanostores'; +import type { RefObject } from 'react'; +import { useEffect } from 'react'; +import { objectKeys } from 'tsafe'; + +/** + * We need to manage focus regions to conditionally enable hotkeys: + * - Some hotkeys should only be enabled when a specific region is focused. + * - Some hotkeys may conflict with other regions, so we need to disable them when a specific region is focused. For + * example, `esc` is used to clear the gallery selection, but it is also used to cancel a filter or transform on the + * canvas. + * + * To manage focus regions, we use a system of hooks and stores: + * - `useFocusRegion` is a hook that registers an element as part of a focus region. When that element is focused, by + * click or any other action, that region is set as the focused region. Optionally, focus can be set on mount. This + * is useful for components like the image viewer. + * - `useIsRegionFocused` is a hook that returns a boolean indicating if a specific region is focused. + * - `useFocusRegionWatcher` is a hook that listens for focus events on the window. When an element is focused, it + * checks if it is part of a focus region and sets that region as the focused region. + */ + +// + +const log = logger('system'); + +/** + * The names of the focus regions. + */ +type FocusRegionName = 'gallery' | 'layers' | 'canvas' | 'workflows' | 'viewer'; + +/** + * A map of focus regions to the elements that are part of that region. + */ +const REGION_TARGETS: Record> = { + gallery: new Set(), + layers: new Set(), + canvas: new Set(), + workflows: new Set(), + viewer: new Set(), +} as const; + +/** + * The currently-focused region or `null` if no region is focused. + */ +const $focusedRegion = atom(null); + +/** + * A map of focus regions to atoms that indicate if that region is focused. + */ +const FOCUS_REGIONS = objectKeys(REGION_TARGETS).reduce( + (acc, region) => { + acc[`$${region}`] = computed($focusedRegion, (focusedRegion) => focusedRegion === region); + return acc; + }, + {} as Record<`$${FocusRegionName}`, Atom> +); + +/** + * Sets the focused region, logging a trace level message. + */ +const setFocus = (region: FocusRegionName | null) => { + $focusedRegion.set(region); + log.trace(`Focus changed: ${region}`); +}; + +type UseFocusRegionOptions = { + focusOnMount?: boolean; +}; + +/** + * Registers an element as part of a focus region. When that element is focused, by click or any other action, that + * region is set as the focused region. Optionally, focus can be set on mount. + * + * On unmount, if the element is the last element in the region and the region is focused, the focused region is set to + * `null`. + * + * @param region The focus region name. + * @param ref The ref of the element to register. + * @param options The options. + */ +export const useFocusRegion = ( + region: FocusRegionName, + ref: RefObject, + options?: UseFocusRegionOptions +) => { + useEffect(() => { + if (!ref.current) { + return; + } + + const { focusOnMount = false } = { focusOnMount: false, ...options }; + + const element = ref.current; + + REGION_TARGETS[region].add(element); + + if (focusOnMount) { + setFocus(region); + } + + return () => { + REGION_TARGETS[region].delete(element); + + if (REGION_TARGETS[region].size === 0 && $focusedRegion.get() === region) { + setFocus(null); + } + }; + }, [options, ref, region]); +}; + +/** + * Returns a boolean indicating if a specific region is focused. + * @param region The focus region name. + */ +export const useIsRegionFocused = (region: FocusRegionName) => { + return useStore(FOCUS_REGIONS[`$${region}`]); +}; + +/** + * Listens for focus events on the window. When an element is focused, it checks if it is part of a focus region and sets + * that region as the focused region. The region corresponding to the deepest element is set. + */ +const onFocus = (_: FocusEvent) => { + const activeElement = document.activeElement; + if (!(activeElement instanceof HTMLElement)) { + return; + } + + const regionCandidates: { region: FocusRegionName; element: HTMLElement }[] = []; + + for (const region of objectKeys(REGION_TARGETS)) { + for (const element of REGION_TARGETS[region]) { + if (element.contains(activeElement)) { + regionCandidates.push({ region, element }); + } + } + } + + if (regionCandidates.length === 0) { + return; + } + + // Sort by the shallowest element + regionCandidates.sort((a, b) => { + if (b.element.contains(a.element)) { + return -1; + } + if (a.element.contains(b.element)) { + return 1; + } + return 0; + }); + + // Set the region of the deepest element + const focusedRegion = regionCandidates[0]?.region; + + if (!focusedRegion) { + log.warn('No focused region found'); + return; + } + + setFocus(focusedRegion); +}; + +/** + * Listens for focus events on the window. When an element is focused, it checks if it is part of a focus region and sets + * that region as the focused region. This is a singleton. + */ +export const useFocusRegionWatcher = () => { + useAssertSingleton('useFocusRegionWatcher'); + + useEffect(() => { + window.addEventListener('focus', onFocus, { capture: true }); + return () => { + window.removeEventListener('focus', onFocus, { capture: true }); + }; + }, []); +}; diff --git a/invokeai/frontend/web/src/common/hooks/interactionScopes.ts b/invokeai/frontend/web/src/common/hooks/interactionScopes.ts deleted file mode 100644 index 759073dd7da..00000000000 --- a/invokeai/frontend/web/src/common/hooks/interactionScopes.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { useStore } from '@nanostores/react'; -import { logger } from 'app/logging/logger'; -import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; -import type { Atom } from 'nanostores'; -import { computed, deepMap } from 'nanostores'; -import type { RefObject } from 'react'; -import { useEffect } from 'react'; - -const log = logger('system'); - -const REGION_NAMES = ['gallery', 'layers', 'canvas', 'workflows', 'viewer', 'settings'] as const; - -type FocusRegionName = (typeof REGION_NAMES)[number]; -type FocusRegionData = { name: FocusRegionName; targets: Set }; -type FocusRegionState = { - focusedRegion: FocusRegionName | null; - regions: Record; -}; - -const initialData = REGION_NAMES.reduce( - (state, region) => { - state.regions[region] = { name: region, targets: new Set() }; - return state; - }, - { - focusedRegion: null, - regions: {}, - } as FocusRegionState -); - -const $focusRegionState = deepMap(initialData); -export const $focusedRegion = computed($focusRegionState, (regions) => regions.focusedRegion); -export const FOCUS_REGIONS = REGION_NAMES.reduce( - (acc, region) => { - acc[`$${region}`] = computed($focusRegionState, (state) => state.focusedRegion === region); - return acc; - }, - {} as Record<`$${FocusRegionName}`, Atom> -); - -const setFocus = (region: FocusRegionName | null) => { - $focusRegionState.setKey('focusedRegion', region); - log.trace(`Focus changed: ${region}`); -}; - -type UseFocusRegionOptions = { - focusOnMount?: boolean; -}; - -export const useFocusRegion = ( - region: FocusRegionName, - ref: RefObject, - options?: UseFocusRegionOptions -) => { - useEffect(() => { - if (!ref.current) { - return; - } - - const { focusOnMount = false } = { focusOnMount: false, ...options }; - - const element = ref.current; - - const regionData = $focusRegionState.get().regions[region]; - - const targets = new Set(regionData.targets); - targets.add(element); - $focusRegionState.setKey(`regions.${region}.targets`, targets); - - if (focusOnMount) { - setFocus(region); - } - - return () => { - const regionData = $focusRegionState.get().regions[region]; - const targets = new Set(regionData.targets); - targets.delete(element); - $focusRegionState.setKey(`regions.${region}.targets`, targets); - - if (targets.size === 0 && $focusRegionState.get().focusedRegion === region) { - setFocus(null); - } - }; - }, [options, ref, region]); -}; - -export const useIsRegionFocused = (region: FocusRegionName) => { - return useStore(FOCUS_REGIONS[`$${region}`]); -}; - -const onFocus = (_: FocusEvent) => { - const activeElement = document.activeElement; - if (!(activeElement instanceof HTMLElement)) { - return; - } - - const regionCandidates: { region: FocusRegionName; element: HTMLElement }[] = []; - - const state = $focusRegionState.get(); - for (const regionData of Object.values(state.regions)) { - for (const element of regionData.targets) { - if (element.contains(activeElement)) { - regionCandidates.push({ region: regionData.name, element }); - } - } - } - - if (regionCandidates.length === 0) { - return; - } - - // Sort by the shallowest element - regionCandidates.sort((a, b) => { - if (b.element.contains(a.element)) { - return -1; - } - if (a.element.contains(b.element)) { - return 1; - } - return 0; - }); - - // Set the region of the deepest element - const focusedRegion = regionCandidates[0]?.region; - - if (!focusedRegion) { - log.warn('No focused region found'); - return; - } - - setFocus(focusedRegion); -}; - -export const useFocusRegionWatcher = () => { - useAssertSingleton('useFocusRegionWatcher'); - - useEffect(() => { - window.addEventListener('focus', onFocus, { capture: true }); - return () => { - window.removeEventListener('focus', onFocus, { capture: true }); - }; - }, []); -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx index f0b11c85694..20e27c75601 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx @@ -1,6 +1,6 @@ import { Divider, Flex } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; -import { useFocusRegion } from 'common/hooks/interactionScopes'; +import { useFocusRegion } from 'common/hooks/focus'; import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons'; import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList'; import { EntityListSelectedEntityActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx index 69d605c554a..2573753c863 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx @@ -1,6 +1,6 @@ import { ContextMenu, Flex, MenuList } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; -import { useFocusRegion } from 'common/hooks/interactionScopes'; +import { useFocusRegion } from 'common/hooks/focus'; import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask'; import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus'; import { CanvasAlertsSendingToGallery } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSendingTo'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx index 891916d6732..0171c8c5256 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx @@ -1,7 +1,7 @@ import { Button, ButtonGroup, Flex, FormControl, FormLabel, Heading, Spacer, Switch } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useIsRegionFocused } from 'common/hooks/interactionScopes'; +import { useIsRegionFocused } from 'common/hooks/focus'; import { FilterSettings } from 'features/controlLayers/components/Filters/FilterSettings'; import { FilterTypeSelect } from 'features/controlLayers/components/Filters/FilterTypeSelect'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx index ca497373eee..7d01422854d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx @@ -1,7 +1,7 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useIsRegionFocused } from 'common/hooks/interactionScopes'; +import { useIsRegionFocused } from 'common/hooks/focus'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice'; import { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton.tsx index 4707c7d6b6f..3809601a62e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton.tsx @@ -1,7 +1,7 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useIsRegionFocused } from 'common/hooks/interactionScopes'; +import { useIsRegionFocused } from 'common/hooks/focus'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { selectImageCount, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton.tsx index f3e19c595ff..3d064abc2f7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton.tsx @@ -1,7 +1,7 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useIsRegionFocused } from 'common/hooks/interactionScopes'; +import { useIsRegionFocused } from 'common/hooks/focus'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { selectImageCount, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarResetViewButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarResetViewButton.tsx index 2b8c64aa59e..88aae8a2ba3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarResetViewButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbarResetViewButton.tsx @@ -1,5 +1,5 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useIsRegionFocused } from 'common/hooks/interactionScopes'; +import { useIsRegionFocused } from 'common/hooks/focus'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx index d1df002cc79..817cbe962ae 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Transform/Transform.tsx @@ -1,7 +1,7 @@ import { Button, ButtonGroup, Flex, FormControl, FormLabel, Heading, Spacer, Switch } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useIsRegionFocused } from 'common/hooks/interactionScopes'; +import { useIsRegionFocused } from 'common/hooks/focus'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types'; import { diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx index 8d1631ba3ec..bd29d1f1745 100644 --- a/invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/GalleryPanelContent.tsx @@ -1,6 +1,6 @@ import { Box, Button, Collapse, Divider, Flex, IconButton, useDisclosure } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useFocusRegion } from 'common/hooks/interactionScopes'; +import { useFocusRegion } from 'common/hooks/focus'; import { GalleryHeader } from 'features/gallery/components/GalleryHeader'; import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors'; import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySelectionCountTag.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySelectionCountTag.tsx index fe03149df7d..01fe12d520d 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySelectionCountTag.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySelectionCountTag.tsx @@ -1,6 +1,6 @@ import { Tag, TagCloseButton, TagLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useIsRegionFocused } from 'common/hooks/interactionScopes'; +import { useIsRegionFocused } from 'common/hooks/focus'; import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages'; import { selectionChanged } from 'features/gallery/store/gallerySlice'; import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx index 3e3073504db..47c82ca3d7b 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx @@ -1,6 +1,6 @@ import { Box, Flex, IconButton } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; -import { useFocusRegion } from 'common/hooks/interactionScopes'; +import { useFocusRegion } from 'common/hooks/focus'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { CanvasAlertsSendingToCanvas } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSendingTo'; import { CompareToolbar } from 'features/gallery/components/ImageViewer/CompareToolbar'; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts index e57804a2d01..47524721746 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts @@ -1,6 +1,6 @@ import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useIsRegionFocused } from 'common/hooks/interactionScopes'; +import { useIsRegionFocused } from 'common/hooks/focus'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { $canvasRightPanelTab } from 'features/controlLayers/store/ephemeral'; import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx index 08a80cfa72d..acc2fde7536 100644 --- a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx @@ -2,7 +2,7 @@ import 'reactflow/dist/style.css'; import { Flex } from '@invoke-ai/ui-library'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; -import { useFocusRegion } from 'common/hooks/interactionScopes'; +import { useFocusRegion } from 'common/hooks/focus'; import { AddNodeCmdk } from 'features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk'; import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel'; import WorkflowEditorSettings from 'features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings'; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 3c4e17e21e5..aaeea376a19 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -1,7 +1,7 @@ import { useGlobalMenuClose, useToken } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks'; -import { useFocusRegion, useIsRegionFocused } from 'common/hooks/interactionScopes'; +import { useFocusRegion, useIsRegionFocused } from 'common/hooks/focus'; import { useConnection } from 'features/nodes/hooks/useConnection'; import { useCopyPaste } from 'features/nodes/hooks/useCopyPaste'; import { useSyncExecutionState } from 'features/nodes/hooks/useExecutionState';