From 8906c23c63861b2aa071225296ef4aa95c9c5d96 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Fri, 10 Nov 2023 23:54:47 +0100 Subject: [PATCH] Fixed callback firing on clickoutside but mousedown inside. (#2434) Co-authored-by: Charles Bochet --- .../ui/layout/modal/components/Modal.tsx | 2 +- .../right-drawer/components/RightDrawer.tsx | 2 +- .../hooks/useListenClickOutside.ts | 82 ++++++++++++++++--- 3 files changed, 72 insertions(+), 14 deletions(-) diff --git a/front/src/modules/ui/layout/modal/components/Modal.tsx b/front/src/modules/ui/layout/modal/components/Modal.tsx index 8566059bb..9d40af2ce 100644 --- a/front/src/modules/ui/layout/modal/components/Modal.tsx +++ b/front/src/modules/ui/layout/modal/components/Modal.tsx @@ -158,7 +158,7 @@ export const Modal = ({ useListenClickOutside({ refs: [modalRef], callback: () => onClose?.(), - mode: ClickOutsideMode.absolute, + mode: ClickOutsideMode.comparePixels, }); const { diff --git a/front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx b/front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx index 83af5cec0..59faefb7a 100644 --- a/front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx +++ b/front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx @@ -56,7 +56,7 @@ export const RightDrawer = () => { useListenClickOutside({ refs: [rightDrawerRef], callback: () => closeRightDrawer(), - mode: ClickOutsideMode.absolute, + mode: ClickOutsideMode.comparePixels, }); const theme = useTheme(); diff --git a/front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutside.ts b/front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutside.ts index 5425e0f01..b3a879216 100644 --- a/front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutside.ts +++ b/front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutside.ts @@ -1,14 +1,14 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; export enum ClickOutsideMode { - absolute = 'absolute', - dom = 'dom', + comparePixels = 'comparePixels', + compareHTMLRef = 'compareHTMLRef', } export const useListenClickOutside = ({ refs, callback, - mode = ClickOutsideMode.dom, + mode = ClickOutsideMode.compareHTMLRef, enabled = true, }: { refs: Array>; @@ -16,19 +16,19 @@ export const useListenClickOutside = ({ mode?: ClickOutsideMode; enabled?: boolean; }) => { + const [isMouseDownInside, setIsMouseDownInside] = useState(false); + useEffect(() => { - const handleClickOutside = (event: MouseEvent | TouchEvent) => { - if (mode === ClickOutsideMode.dom) { + const handleMouseDown = (event: MouseEvent | TouchEvent) => { + if (mode === ClickOutsideMode.compareHTMLRef) { const clickedOnAtLeastOneRef = refs .filter((ref) => !!ref.current) .some((ref) => ref.current?.contains(event.target as Node)); - if (!clickedOnAtLeastOneRef) { - callback(event); - } + setIsMouseDownInside(clickedOnAtLeastOneRef); } - if (mode === ClickOutsideMode.absolute) { + if (mode === ClickOutsideMode.comparePixels) { const clickedOnAtLeastOneRef = refs .filter((ref) => !!ref.current) .some((ref) => { @@ -57,28 +57,86 @@ export const useListenClickOutside = ({ } return true; }); - if (!clickedOnAtLeastOneRef) { + + setIsMouseDownInside(clickedOnAtLeastOneRef); + } + }; + + const handleClickOutside = (event: MouseEvent | TouchEvent) => { + if (mode === ClickOutsideMode.compareHTMLRef) { + const clickedOnAtLeastOneRef = refs + .filter((ref) => !!ref.current) + .some((ref) => ref.current?.contains(event.target as Node)); + + if (!clickedOnAtLeastOneRef && !isMouseDownInside) { + callback(event); + } + } + + if (mode === ClickOutsideMode.comparePixels) { + const clickedOnAtLeastOneRef = refs + .filter((ref) => !!ref.current) + .some((ref) => { + if (!ref.current) { + return false; + } + + const { x, y, width, height } = ref.current.getBoundingClientRect(); + + const clientX = + 'clientX' in event + ? event.clientX + : event.changedTouches[0].clientX; + const clientY = + 'clientY' in event + ? event.clientY + : event.changedTouches[0].clientY; + + if ( + clientX < x || + clientX > x + width || + clientY < y || + clientY > y + height + ) { + return false; + } + return true; + }); + + if (!clickedOnAtLeastOneRef && !isMouseDownInside) { callback(event); } } }; if (enabled) { + document.addEventListener('mousedown', handleMouseDown, { + capture: true, + }); document.addEventListener('click', handleClickOutside, { capture: true }); + document.addEventListener('touchstart', handleMouseDown, { + capture: true, + }); document.addEventListener('touchend', handleClickOutside, { capture: true, }); return () => { + document.removeEventListener('mousedown', handleMouseDown, { + capture: true, + }); document.removeEventListener('click', handleClickOutside, { capture: true, }); + document.removeEventListener('touchstart', handleMouseDown, { + capture: true, + }); document.removeEventListener('touchend', handleClickOutside, { capture: true, }); }; } - }, [refs, callback, mode, enabled]); + }, [refs, callback, mode, enabled, isMouseDownInside]); }; export const useListenClickOutsideByClassName = ({