Fixes bug causing gallery to close on context menu open

This commit is contained in:
psychedelicious 2022-11-20 13:00:03 +11:00 committed by blessedcoolant
parent 76e7e82f5e
commit b6dd5b664c
5 changed files with 76 additions and 39 deletions

View File

@ -1,25 +1,37 @@
import { RefObject, useEffect } from 'react'; import { RefObject, useEffect, useRef } from 'react';
import { Rect } from 'react-konva';
const useClickOutsideWatcher = ( const watchers: {
ref: RefObject<HTMLElement>, ref: RefObject<HTMLElement>;
callback: () => void, enable: boolean;
req = true callback: () => void;
) => { }[] = [];
const useClickOutsideWatcher = () => {
useEffect(() => { useEffect(() => {
function handleClickOutside(e: MouseEvent) { function handleClickOutside(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) { watchers.forEach(({ ref, enable, callback }) => {
callback(); if (enable && ref.current && !ref.current.contains(e.target as Node)) {
} console.log('callback');
} callback();
if (req) { }
document.addEventListener('mousedown', handleClickOutside); });
} }
document.addEventListener('mousedown', handleClickOutside);
return () => { return () => {
if (req) { document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('mousedown', handleClickOutside);
}
}; };
}, [ref, req, callback]); }, []);
return {
addWatcher: (watcher: {
ref: RefObject<HTMLElement>;
callback: () => void;
enable: boolean;
}) => {
watchers.push(watcher);
},
};
}; };
export default useClickOutsideWatcher; export default useClickOutsideWatcher;

View File

@ -7,7 +7,10 @@ import {
useToast, useToast,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store'; import { useAppDispatch, useAppSelector } from 'app/store';
import { setCurrentImage } from 'features/gallery/store/gallerySlice'; import {
setCurrentImage,
setShouldHoldGalleryOpen,
} from 'features/gallery/store/gallerySlice';
import { FaCheck, FaTrashAlt } from 'react-icons/fa'; import { FaCheck, FaTrashAlt } from 'react-icons/fa';
import DeleteImageModal from './DeleteImageModal'; import DeleteImageModal from './DeleteImageModal';
import { memo, useState } from 'react'; import { memo, useState } from 'react';
@ -153,10 +156,9 @@ const HoverableImage = memo((props: HoverableImageProps) => {
return ( return (
<ContextMenu.Root <ContextMenu.Root
// onOpenChange={(open: boolean) => { onOpenChange={(open: boolean) => {
// dispatch(setShouldHoldGalleryOpen(open)); dispatch(setShouldHoldGalleryOpen(open));
// dispatch(setShouldShowGallery(true)); }}
// }}
> >
<ContextMenu.Trigger> <ContextMenu.Trigger>
<Box <Box
@ -204,6 +206,9 @@ const HoverableImage = memo((props: HoverableImageProps) => {
<ContextMenu.Content <ContextMenu.Content
className="hoverable-image-context-menu" className="hoverable-image-context-menu"
sticky={'always'} sticky={'always'}
onInteractOutside={(e) => {
e.detail.originalEvent.preventDefault();
}}
> >
<ContextMenu.Item <ContextMenu.Item
onClickCapture={handleUsePrompt} onClickCapture={handleUsePrompt}

View File

@ -3,6 +3,7 @@ import { NumberSize, Resizable } from 're-resizable';
import { import {
ChangeEvent, ChangeEvent,
useCallback,
useEffect, useEffect,
useLayoutEffect, useLayoutEffect,
useRef, useRef,
@ -39,7 +40,6 @@ import { BiReset } from 'react-icons/bi';
import IAICheckbox from 'common/components/IAICheckbox'; import IAICheckbox from 'common/components/IAICheckbox';
import { setDoesCanvasNeedScaling } from 'features/canvas/store/canvasSlice'; import { setDoesCanvasNeedScaling } from 'features/canvas/store/canvasSlice';
import _ from 'lodash'; import _ from 'lodash';
import useClickOutsideWatcher from 'common/hooks/useClickOutsideWatcher';
const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 320; const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 320;
@ -124,15 +124,15 @@ export default function ImageGallery() {
shouldPinGallery && dispatch(setDoesCanvasNeedScaling(true)); shouldPinGallery && dispatch(setDoesCanvasNeedScaling(true));
}; };
const handleCloseGallery = () => { const handleCloseGallery = useCallback(() => {
dispatch(setShouldShowGallery(false)); dispatch(setShouldShowGallery(false));
dispatch(setShouldHoldGalleryOpen(false));
dispatch( dispatch(
setGalleryScrollPosition( setGalleryScrollPosition(
galleryContainerRef.current ? galleryContainerRef.current.scrollTop : 0 galleryContainerRef.current ? galleryContainerRef.current.scrollTop : 0
) )
); );
dispatch(setShouldHoldGalleryOpen(false)); }, [dispatch]);
};
const handleClickLoadMore = () => { const handleClickLoadMore = () => {
dispatch(requestImages(currentCategory)); dispatch(requestImages(currentCategory));
@ -144,6 +144,7 @@ export default function ImageGallery() {
}; };
const setCloseGalleryTimer = () => { const setCloseGalleryTimer = () => {
if (shouldHoldGalleryOpen) return;
timeoutIdRef.current = window.setTimeout(() => handleCloseGallery(), 500); timeoutIdRef.current = window.setTimeout(() => handleCloseGallery(), 500);
}; };
@ -273,12 +274,25 @@ export default function ImageGallery() {
setShouldShowButtons(galleryWidth >= 280); setShouldShowButtons(galleryWidth >= 280);
}, [galleryWidth]); }, [galleryWidth]);
useClickOutsideWatcher(galleryRef, handleCloseGallery, !shouldPinGallery); useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (
galleryRef.current &&
!galleryRef.current.contains(e.target as Node)
) {
handleCloseGallery();
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [handleCloseGallery]);
return ( return (
<CSSTransition <CSSTransition
nodeRef={galleryRef} nodeRef={galleryRef}
in={shouldShowGallery || (shouldHoldGalleryOpen && !shouldPinGallery)} in={shouldShowGallery || shouldHoldGalleryOpen}
unmountOnExit unmountOnExit
timeout={200} timeout={200}
classNames="image-gallery-wrapper" classNames="image-gallery-wrapper"
@ -288,6 +302,7 @@ export default function ImageGallery() {
style={{ zIndex: shouldPinGallery ? 1 : 100 }} style={{ zIndex: shouldPinGallery ? 1 : 100 }}
data-pinned={shouldPinGallery} data-pinned={shouldPinGallery}
ref={galleryRef} ref={galleryRef}
// onMouseLeave={setCloseGalleryTimer}
onMouseLeave={!shouldPinGallery ? setCloseGalleryTimer : undefined} onMouseLeave={!shouldPinGallery ? setCloseGalleryTimer : undefined}
onMouseEnter={!shouldPinGallery ? cancelCloseGalleryTimer : undefined} onMouseEnter={!shouldPinGallery ? cancelCloseGalleryTimer : undefined}
onMouseOver={!shouldPinGallery ? cancelCloseGalleryTimer : undefined} onMouseOver={!shouldPinGallery ? cancelCloseGalleryTimer : undefined}

View File

@ -1,12 +1,11 @@
import { Tooltip } from '@chakra-ui/react'; import { Tooltip } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import _ from 'lodash'; import _ from 'lodash';
import { MouseEvent, ReactNode, useCallback, useRef } from 'react'; import React, { ReactNode, useCallback, useEffect, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs'; import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
import { CSSTransition } from 'react-transition-group'; import { CSSTransition } from 'react-transition-group';
import { RootState, useAppDispatch, useAppSelector } from 'app/store'; import { RootState, useAppDispatch, useAppSelector } from 'app/store';
import useClickOutsideWatcher from 'common/hooks/useClickOutsideWatcher';
import { import {
OptionsState, OptionsState,
setOptionsPanelScrollPosition, setOptionsPanelScrollPosition,
@ -101,15 +100,8 @@ const InvokeOptionsPanel = (props: Props) => {
); );
dispatch(setShouldShowOptionsPanel(false)); dispatch(setShouldShowOptionsPanel(false));
dispatch(setShouldHoldOptionsPanelOpen(false)); dispatch(setShouldHoldOptionsPanelOpen(false));
// dispatch(setDoesCanvasNeedScaling(true));
}, [dispatch, shouldPinOptionsPanel]); }, [dispatch, shouldPinOptionsPanel]);
useClickOutsideWatcher(
optionsPanelRef,
handleCloseOptionsPanel,
!shouldPinOptionsPanel
);
const setCloseOptionsPanelTimer = () => { const setCloseOptionsPanelTimer = () => {
timeoutIdRef.current = window.setTimeout( timeoutIdRef.current = window.setTimeout(
() => handleCloseOptionsPanel(), () => handleCloseOptionsPanel(),
@ -126,6 +118,21 @@ const InvokeOptionsPanel = (props: Props) => {
dispatch(setDoesCanvasNeedScaling(true)); dispatch(setDoesCanvasNeedScaling(true));
}; };
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (
optionsPanelRef.current &&
!optionsPanelRef.current.contains(e.target as Node)
) {
handleCloseOptionsPanel();
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [handleCloseOptionsPanel]);
return ( return (
<CSSTransition <CSSTransition
nodeRef={optionsPanelRef} nodeRef={optionsPanelRef}
@ -153,7 +160,7 @@ const InvokeOptionsPanel = (props: Props) => {
<div <div
className="options-panel" className="options-panel"
ref={optionsPanelContainerRef} ref={optionsPanelContainerRef}
onMouseLeave={(e: MouseEvent<HTMLDivElement>) => { onMouseLeave={(e: React.MouseEvent<HTMLDivElement>) => {
if (e.target !== optionsPanelContainerRef.current) { if (e.target !== optionsPanelContainerRef.current) {
cancelCloseOptionsPanelTimer(); cancelCloseOptionsPanelTimer();
} else { } else {

View File

@ -6,9 +6,7 @@ import { RootState, useAppDispatch, useAppSelector } from 'app/store';
import NodesWIP from 'common/components/WorkInProgress/NodesWIP'; import NodesWIP from 'common/components/WorkInProgress/NodesWIP';
import { PostProcessingWIP } from 'common/components/WorkInProgress/PostProcessingWIP'; import { PostProcessingWIP } from 'common/components/WorkInProgress/PostProcessingWIP';
import ImageToImageIcon from 'common/icons/ImageToImageIcon'; import ImageToImageIcon from 'common/icons/ImageToImageIcon';
import InpaintIcon from 'common/icons/InpaintIcon';
import NodesIcon from 'common/icons/NodesIcon'; import NodesIcon from 'common/icons/NodesIcon';
import OutpaintIcon from 'common/icons/OutpaintIcon';
import PostprocessingIcon from 'common/icons/PostprocessingIcon'; import PostprocessingIcon from 'common/icons/PostprocessingIcon';
import TextToImageIcon from 'common/icons/TextToImageIcon'; import TextToImageIcon from 'common/icons/TextToImageIcon';
import { import {