Refactor and fixes dropdown bugs (#8807)
Fixes https://github.com/twentyhq/twenty/issues/8788 Fixes https://github.com/twentyhq/twenty/issues/8793 Fixes https://github.com/twentyhq/twenty/issues/8791 Fixes https://github.com/twentyhq/twenty/issues/8890 Fixes https://github.com/twentyhq/twenty/issues/8893 - [x] Also : Icon buttons under dropdown are visible without blur :  - [x] Also : <img width="237" alt="image" src="https://github.com/user-attachments/assets/e4c70936-beff-4481-89cb-0a32a36e0ee2"> - [x] Also : <img width="335" alt="image" src="https://github.com/user-attachments/assets/5be60395-6baf-49eb-8d40-197add049e20"> - [x] Also : <img width="287" alt="image" src="https://github.com/user-attachments/assets/a317561f-7986-4d70-a1c0-deee4f4e268a"> - Button create new without padding - Container is expanding - [x] Also : <img width="303" alt="image" src="https://github.com/user-attachments/assets/09f8a27f-91db-4191-acdc-aaaeedaf6da5"> - [x] Also : <img width="133" alt="image" src="https://github.com/user-attachments/assets/fe17b32e-f7a4-46c4-8040-239eaf8198e8"> Font is cut at bottom ? - [x] Also : <img width="385" alt="image" src="https://github.com/user-attachments/assets/7bab2092-2936-4112-a2ee-d32d6737e304"> The component should flip and not resize in this situation - [x] Also : <img width="244" alt="image" src="https://github.com/user-attachments/assets/5384f49a-71f9-4638-a60c-158cc8c83f81"> - [x] Also : 
This commit is contained in:
@ -1,12 +1,12 @@
|
||||
import { fireEvent, renderHook } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { fireEvent, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import {
|
||||
ClickOutsideMode,
|
||||
useListenClickOutsideV2,
|
||||
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2';
|
||||
useListenClickOutside,
|
||||
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const containerRef = React.createRef<HTMLDivElement>();
|
||||
@ -19,13 +19,13 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
);
|
||||
|
||||
const listenerId = 'listenerId';
|
||||
describe('useListenClickOutsideV2', () => {
|
||||
describe('useListenClickOutside', () => {
|
||||
it('should trigger the callback when clicking outside the specified refs', () => {
|
||||
const callback = jest.fn();
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useListenClickOutsideV2({
|
||||
useListenClickOutside({
|
||||
refs: [containerRef],
|
||||
callback,
|
||||
listenerId,
|
||||
@ -46,7 +46,7 @@ describe('useListenClickOutsideV2', () => {
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useListenClickOutsideV2({
|
||||
useListenClickOutside({
|
||||
refs: [nullRef],
|
||||
callback,
|
||||
mode: ClickOutsideMode.comparePixels,
|
||||
@ -68,7 +68,7 @@ describe('useListenClickOutsideV2', () => {
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useListenClickOutsideV2({
|
||||
useListenClickOutside({
|
||||
refs: [containerRef],
|
||||
callback,
|
||||
listenerId,
|
||||
@ -91,7 +91,7 @@ describe('useListenClickOutsideV2', () => {
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useListenClickOutsideV2({
|
||||
useListenClickOutside({
|
||||
refs: [containerRef],
|
||||
callback,
|
||||
mode: ClickOutsideMode.comparePixels,
|
||||
@ -1,78 +0,0 @@
|
||||
import { fireEvent, renderHook } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import {
|
||||
ClickOutsideMode,
|
||||
useListenClickOutside,
|
||||
} from '../useListenClickOutside';
|
||||
|
||||
const containerRef = React.createRef<HTMLDivElement>();
|
||||
const nullRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<div ref={containerRef}>{children}</div>
|
||||
);
|
||||
|
||||
describe('useListenClickOutside', () => {
|
||||
it('should trigger the callback when clicking outside the specified refs', () => {
|
||||
const callback = jest.fn();
|
||||
|
||||
renderHook(
|
||||
() => useListenClickOutside({ refs: [containerRef], callback }),
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
fireEvent.mouseDown(document);
|
||||
fireEvent.click(document);
|
||||
});
|
||||
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call the callback when clicking inside the specified refs using pixel comparison', () => {
|
||||
const callback = jest.fn();
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useListenClickOutside({
|
||||
refs: [containerRef, nullRef],
|
||||
callback,
|
||||
mode: ClickOutsideMode.comparePixels,
|
||||
}),
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
if (isDefined(containerRef.current)) {
|
||||
fireEvent.mouseDown(containerRef.current);
|
||||
fireEvent.click(containerRef.current);
|
||||
}
|
||||
});
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call the callback when clicking outside the specified refs using pixel comparison', () => {
|
||||
const callback = jest.fn();
|
||||
|
||||
renderHook(() =>
|
||||
useListenClickOutside({
|
||||
refs: [containerRef, nullRef],
|
||||
callback,
|
||||
mode: ClickOutsideMode.comparePixels,
|
||||
}),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
// Simulate a click outside the specified refs
|
||||
fireEvent.mouseDown(document.body);
|
||||
fireEvent.click(document.body);
|
||||
});
|
||||
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -2,10 +2,7 @@ import { useEffect } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
|
||||
import {
|
||||
ClickOutsideListenerProps,
|
||||
useListenClickOutsideV2,
|
||||
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2';
|
||||
|
||||
import { ClickOutsideListenerCallback } from '@/ui/utilities/pointer-event/types/ClickOutsideListenerCallback';
|
||||
import { toSpliced } from '~/utils/array/toSpliced';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
@ -17,35 +14,6 @@ export const useClickOutsideListener = (componentId: string) => {
|
||||
getClickOutsideListenerMouseDownHappenedState,
|
||||
} = useClickOustideListenerStates(componentId);
|
||||
|
||||
const useListenClickOutside = <T extends Element>({
|
||||
callback,
|
||||
refs,
|
||||
enabled,
|
||||
mode,
|
||||
}: Omit<ClickOutsideListenerProps<T>, 'listenerId'>) => {
|
||||
return useListenClickOutsideV2({
|
||||
listenerId: componentId,
|
||||
refs,
|
||||
callback: useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(event) => {
|
||||
callback(event);
|
||||
|
||||
const additionalCallbacks = snapshot
|
||||
.getLoadable(getClickOutsideListenerCallbacksState)
|
||||
.getValue();
|
||||
|
||||
additionalCallbacks.forEach((additionalCallback) => {
|
||||
additionalCallback.callbackFunction(event);
|
||||
});
|
||||
},
|
||||
[callback],
|
||||
),
|
||||
enabled,
|
||||
mode,
|
||||
});
|
||||
};
|
||||
|
||||
const toggleClickOutsideListener = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(activated: boolean) => {
|
||||
@ -152,7 +120,6 @@ export const useClickOutsideListener = (componentId: string) => {
|
||||
};
|
||||
|
||||
return {
|
||||
useListenClickOutside,
|
||||
toggleClickOutsideListener,
|
||||
useRegisterClickOutsideListenerCallback,
|
||||
};
|
||||
|
||||
@ -1,140 +1,266 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState';
|
||||
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
|
||||
|
||||
export enum ClickOutsideMode {
|
||||
comparePixels = 'comparePixels',
|
||||
compareHTMLRef = 'compareHTMLRef',
|
||||
}
|
||||
|
||||
export const useListenClickOutside = <T extends Element>({
|
||||
refs,
|
||||
callback,
|
||||
mode = ClickOutsideMode.compareHTMLRef,
|
||||
enabled = true,
|
||||
}: {
|
||||
export type ClickOutsideListenerProps<T extends Element> = {
|
||||
refs: Array<React.RefObject<T>>;
|
||||
excludeClassNames?: string[];
|
||||
callback: (event: MouseEvent | TouchEvent) => void;
|
||||
mode?: ClickOutsideMode;
|
||||
listenerId: string;
|
||||
hotkeyScope?: string;
|
||||
enabled?: boolean;
|
||||
}) => {
|
||||
const [isMouseDownInside, setIsMouseDownInside] = useState(false);
|
||||
};
|
||||
|
||||
export const useListenClickOutside = <T extends Element>({
|
||||
refs,
|
||||
excludeClassNames,
|
||||
callback,
|
||||
mode = ClickOutsideMode.compareHTMLRef,
|
||||
listenerId,
|
||||
hotkeyScope,
|
||||
enabled = true,
|
||||
}: ClickOutsideListenerProps<T>) => {
|
||||
const {
|
||||
getClickOutsideListenerIsMouseDownInsideState,
|
||||
getClickOutsideListenerIsActivatedState,
|
||||
getClickOutsideListenerMouseDownHappenedState,
|
||||
} = useClickOustideListenerStates(listenerId);
|
||||
|
||||
const handleMouseDown = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(event: MouseEvent | TouchEvent) => {
|
||||
const clickOutsideListenerIsActivated = snapshot
|
||||
.getLoadable(getClickOutsideListenerIsActivatedState)
|
||||
.getValue();
|
||||
|
||||
set(getClickOutsideListenerMouseDownHappenedState, true);
|
||||
|
||||
const currentHotkeyScopes = snapshot
|
||||
.getLoadable(internalHotkeysEnabledScopesState)
|
||||
.getValue();
|
||||
|
||||
const isListeningBasedOnHotkeyScope = hotkeyScope
|
||||
? currentHotkeyScopes.includes(hotkeyScope)
|
||||
: true;
|
||||
|
||||
const isListening =
|
||||
clickOutsideListenerIsActivated &&
|
||||
enabled &&
|
||||
isListeningBasedOnHotkeyScope;
|
||||
|
||||
if (!isListening) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === ClickOutsideMode.compareHTMLRef) {
|
||||
const clickedOnAtLeastOneRef = refs
|
||||
.filter((ref) => !!ref.current)
|
||||
.some((ref) => ref.current?.contains(event.target as Node));
|
||||
|
||||
set(
|
||||
getClickOutsideListenerIsMouseDownInsideState,
|
||||
clickedOnAtLeastOneRef,
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
set(
|
||||
getClickOutsideListenerIsMouseDownInsideState,
|
||||
clickedOnAtLeastOneRef,
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
getClickOutsideListenerIsActivatedState,
|
||||
getClickOutsideListenerMouseDownHappenedState,
|
||||
hotkeyScope,
|
||||
enabled,
|
||||
mode,
|
||||
refs,
|
||||
getClickOutsideListenerIsMouseDownInsideState,
|
||||
],
|
||||
);
|
||||
|
||||
const handleClickOutside = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(event: MouseEvent | TouchEvent) => {
|
||||
const clickOutsideListenerIsActivated = snapshot
|
||||
.getLoadable(getClickOutsideListenerIsActivatedState)
|
||||
.getValue();
|
||||
|
||||
const currentHotkeyScopes = snapshot
|
||||
.getLoadable(internalHotkeysEnabledScopesState)
|
||||
.getValue();
|
||||
|
||||
const isListeningBasedOnHotkeyScope = hotkeyScope
|
||||
? currentHotkeyScopes.includes(hotkeyScope)
|
||||
: true;
|
||||
|
||||
const isListening =
|
||||
clickOutsideListenerIsActivated &&
|
||||
enabled &&
|
||||
isListeningBasedOnHotkeyScope;
|
||||
|
||||
const isMouseDownInside = snapshot
|
||||
.getLoadable(getClickOutsideListenerIsMouseDownInsideState)
|
||||
.getValue();
|
||||
|
||||
const hasMouseDownHappened = snapshot
|
||||
.getLoadable(getClickOutsideListenerMouseDownHappenedState)
|
||||
.getValue();
|
||||
|
||||
if (mode === ClickOutsideMode.compareHTMLRef) {
|
||||
const clickedElement = event.target as HTMLElement;
|
||||
let isClickedOnExcluded = false;
|
||||
let currentElement: HTMLElement | null = clickedElement;
|
||||
|
||||
while (currentElement) {
|
||||
const currentClassList = currentElement.classList;
|
||||
|
||||
isClickedOnExcluded =
|
||||
excludeClassNames?.some((className) =>
|
||||
currentClassList.contains(className),
|
||||
) ?? false;
|
||||
|
||||
if (isClickedOnExcluded) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentElement = currentElement.parentElement;
|
||||
}
|
||||
|
||||
const clickedOnAtLeastOneRef = refs
|
||||
.filter((ref) => !!ref.current)
|
||||
.some((ref) => ref.current?.contains(event.target as Node));
|
||||
|
||||
if (
|
||||
isListening &&
|
||||
hasMouseDownHappened &&
|
||||
!clickedOnAtLeastOneRef &&
|
||||
!isMouseDownInside &&
|
||||
!isClickedOnExcluded
|
||||
) {
|
||||
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 &&
|
||||
isListening &&
|
||||
hasMouseDownHappened
|
||||
) {
|
||||
callback(event);
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
getClickOutsideListenerIsActivatedState,
|
||||
hotkeyScope,
|
||||
enabled,
|
||||
getClickOutsideListenerIsMouseDownInsideState,
|
||||
getClickOutsideListenerMouseDownHappenedState,
|
||||
mode,
|
||||
refs,
|
||||
excludeClassNames,
|
||||
callback,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
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));
|
||||
document.addEventListener('mousedown', handleMouseDown, {
|
||||
capture: true,
|
||||
});
|
||||
document.addEventListener('click', handleClickOutside, { capture: true });
|
||||
document.addEventListener('touchstart', handleMouseDown, {
|
||||
capture: true,
|
||||
});
|
||||
document.addEventListener('touchend', handleClickOutside, {
|
||||
capture: true,
|
||||
});
|
||||
|
||||
setIsMouseDownInside(clickedOnAtLeastOneRef);
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
setIsMouseDownInside(clickedOnAtLeastOneRef);
|
||||
}
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
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, isMouseDownInside]);
|
||||
}, [refs, callback, mode, handleClickOutside, handleMouseDown]);
|
||||
};
|
||||
|
||||
@ -1,266 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState';
|
||||
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
|
||||
|
||||
export enum ClickOutsideMode {
|
||||
comparePixels = 'comparePixels',
|
||||
compareHTMLRef = 'compareHTMLRef',
|
||||
}
|
||||
|
||||
export type ClickOutsideListenerProps<T extends Element> = {
|
||||
refs: Array<React.RefObject<T>>;
|
||||
excludeClassNames?: string[];
|
||||
callback: (event: MouseEvent | TouchEvent) => void;
|
||||
mode?: ClickOutsideMode;
|
||||
listenerId: string;
|
||||
hotkeyScope?: string;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export const useListenClickOutsideV2 = <T extends Element>({
|
||||
refs,
|
||||
excludeClassNames,
|
||||
callback,
|
||||
mode = ClickOutsideMode.compareHTMLRef,
|
||||
listenerId,
|
||||
hotkeyScope,
|
||||
enabled = true,
|
||||
}: ClickOutsideListenerProps<T>) => {
|
||||
const {
|
||||
getClickOutsideListenerIsMouseDownInsideState,
|
||||
getClickOutsideListenerIsActivatedState,
|
||||
getClickOutsideListenerMouseDownHappenedState,
|
||||
} = useClickOustideListenerStates(listenerId);
|
||||
|
||||
const handleMouseDown = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(event: MouseEvent | TouchEvent) => {
|
||||
const clickOutsideListenerIsActivated = snapshot
|
||||
.getLoadable(getClickOutsideListenerIsActivatedState)
|
||||
.getValue();
|
||||
|
||||
set(getClickOutsideListenerMouseDownHappenedState, true);
|
||||
|
||||
const currentHotkeyScopes = snapshot
|
||||
.getLoadable(internalHotkeysEnabledScopesState)
|
||||
.getValue();
|
||||
|
||||
const isListeningBasedOnHotkeyScope = hotkeyScope
|
||||
? currentHotkeyScopes.includes(hotkeyScope)
|
||||
: true;
|
||||
|
||||
const isListening =
|
||||
clickOutsideListenerIsActivated &&
|
||||
enabled &&
|
||||
isListeningBasedOnHotkeyScope;
|
||||
|
||||
if (!isListening) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === ClickOutsideMode.compareHTMLRef) {
|
||||
const clickedOnAtLeastOneRef = refs
|
||||
.filter((ref) => !!ref.current)
|
||||
.some((ref) => ref.current?.contains(event.target as Node));
|
||||
|
||||
set(
|
||||
getClickOutsideListenerIsMouseDownInsideState,
|
||||
clickedOnAtLeastOneRef,
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
set(
|
||||
getClickOutsideListenerIsMouseDownInsideState,
|
||||
clickedOnAtLeastOneRef,
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
getClickOutsideListenerIsActivatedState,
|
||||
getClickOutsideListenerMouseDownHappenedState,
|
||||
hotkeyScope,
|
||||
enabled,
|
||||
mode,
|
||||
refs,
|
||||
getClickOutsideListenerIsMouseDownInsideState,
|
||||
],
|
||||
);
|
||||
|
||||
const handleClickOutside = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(event: MouseEvent | TouchEvent) => {
|
||||
const clickOutsideListenerIsActivated = snapshot
|
||||
.getLoadable(getClickOutsideListenerIsActivatedState)
|
||||
.getValue();
|
||||
|
||||
const currentHotkeyScopes = snapshot
|
||||
.getLoadable(internalHotkeysEnabledScopesState)
|
||||
.getValue();
|
||||
|
||||
const isListeningBasedOnHotkeyScope = hotkeyScope
|
||||
? currentHotkeyScopes.includes(hotkeyScope)
|
||||
: true;
|
||||
|
||||
const isListening =
|
||||
clickOutsideListenerIsActivated &&
|
||||
enabled &&
|
||||
isListeningBasedOnHotkeyScope;
|
||||
|
||||
const isMouseDownInside = snapshot
|
||||
.getLoadable(getClickOutsideListenerIsMouseDownInsideState)
|
||||
.getValue();
|
||||
|
||||
const hasMouseDownHappened = snapshot
|
||||
.getLoadable(getClickOutsideListenerMouseDownHappenedState)
|
||||
.getValue();
|
||||
|
||||
if (mode === ClickOutsideMode.compareHTMLRef) {
|
||||
const clickedElement = event.target as HTMLElement;
|
||||
let isClickedOnExcluded = false;
|
||||
let currentElement: HTMLElement | null = clickedElement;
|
||||
|
||||
while (currentElement) {
|
||||
const currentClassList = currentElement.classList;
|
||||
|
||||
isClickedOnExcluded =
|
||||
excludeClassNames?.some((className) =>
|
||||
currentClassList.contains(className),
|
||||
) ?? false;
|
||||
|
||||
if (isClickedOnExcluded) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentElement = currentElement.parentElement;
|
||||
}
|
||||
|
||||
const clickedOnAtLeastOneRef = refs
|
||||
.filter((ref) => !!ref.current)
|
||||
.some((ref) => ref.current?.contains(event.target as Node));
|
||||
|
||||
if (
|
||||
isListening &&
|
||||
hasMouseDownHappened &&
|
||||
!clickedOnAtLeastOneRef &&
|
||||
!isMouseDownInside &&
|
||||
!isClickedOnExcluded
|
||||
) {
|
||||
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 &&
|
||||
isListening &&
|
||||
hasMouseDownHappened
|
||||
) {
|
||||
callback(event);
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
getClickOutsideListenerIsActivatedState,
|
||||
hotkeyScope,
|
||||
enabled,
|
||||
getClickOutsideListenerIsMouseDownInsideState,
|
||||
getClickOutsideListenerMouseDownHappenedState,
|
||||
mode,
|
||||
refs,
|
||||
excludeClassNames,
|
||||
callback,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
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, handleClickOutside, handleMouseDown]);
|
||||
};
|
||||
Reference in New Issue
Block a user