Deprecate useListenClickOutsideByClassNameListener (#8242)

This commit is contained in:
Charles Bochet 2024-10-31 12:46:21 +01:00 committed by GitHub
parent d46820472c
commit 034e558758
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 52 additions and 119 deletions

View File

@ -6,6 +6,7 @@ import { Key } from 'ts-key-enum';
import { RecordBoardHeader } from '@/object-record/record-board/components/RecordBoardHeader'; import { RecordBoardHeader } from '@/object-record/record-board/components/RecordBoardHeader';
import { RecordBoardStickyHeaderEffect } from '@/object-record/record-board/components/RecordBoardStickyHeaderEffect'; import { RecordBoardStickyHeaderEffect } from '@/object-record/record-board/components/RecordBoardStickyHeaderEffect';
import { RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-board/constants/RecordBoardClickOutsideListenerId';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection'; import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
@ -16,7 +17,7 @@ import { recordStoreFamilyState } from '@/object-record/record-store/states/reco
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutsideV2 } from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useScrollRestoration } from '~/hooks/useScrollRestoration'; import { useScrollRestoration } from '~/hooks/useScrollRestoration';
@ -69,9 +70,15 @@ export const RecordBoard = () => {
const { resetRecordSelection, setRecordAsSelected } = const { resetRecordSelection, setRecordAsSelected } =
useRecordBoardSelection(recordBoardId); useRecordBoardSelection(recordBoardId);
useListenClickOutsideByClassName({ useListenClickOutsideV2({
classNames: ['record-board-card'], excludeClassNames: [
excludeClassNames: ['bottom-bar', 'action-menu-dropdown', 'command-menu'], 'bottom-bar',
'action-menu-dropdown',
'command-menu',
'modal-backdrop',
],
listenerId: RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID,
refs: [boardRef],
callback: resetRecordSelection, callback: resetRecordSelection,
}); });

View File

@ -0,0 +1 @@
export const RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID = 'record-board';

View File

@ -33,6 +33,12 @@ export const RecordTableInternalEffect = ({
); );
useListenClickOutsideV2({ useListenClickOutsideV2({
excludeClassNames: [
'bottom-bar',
'action-menu-dropdown',
'command-menu',
'modal-backdrop',
],
listenerId: RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID, listenerId: RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID,
refs: [tableBodyRef], refs: [tableBodyRef],
callback: () => { callback: () => {

View File

@ -1,13 +1,12 @@
import { fireEvent, renderHook } from '@testing-library/react';
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { fireEvent, render, renderHook } from '@testing-library/react';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { import {
ClickOutsideMode, ClickOutsideMode,
useListenClickOutside, useListenClickOutside,
useListenClickOutsideByClassName,
} from '../useListenClickOutside'; } from '../useListenClickOutside';
const containerRef = React.createRef<HTMLDivElement>(); const containerRef = React.createRef<HTMLDivElement>();
@ -77,59 +76,3 @@ describe('useListenClickOutside', () => {
expect(callback).toHaveBeenCalled(); expect(callback).toHaveBeenCalled();
}); });
}); });
describe('useListenClickOutsideByClassName', () => {
it('should trigger the callback when clicking outside the specified class names', () => {
const callback = jest.fn();
const { container } = render(
<div>
<div className="wont-trigger other-class">Inside</div>
<div className="will-trigger">Outside</div>
</div>,
);
renderHook(() =>
useListenClickOutsideByClassName({
classNames: ['wont-trigger'],
callback,
}),
);
act(() => {
const notClickableElement = container.querySelector('.will-trigger');
if (isDefined(notClickableElement)) {
fireEvent.mouseDown(notClickableElement);
fireEvent.click(notClickableElement);
}
});
expect(callback).toHaveBeenCalled();
});
it('should not trigger the callback when clicking inside the specified class names', () => {
const callback = jest.fn();
const { container } = render(
<div>
<div className="wont-trigger other-class">Inside</div>
<div className="will-trigger">Outside</div>
</div>,
);
renderHook(() =>
useListenClickOutsideByClassName({
classNames: ['wont-trigger'],
callback,
}),
);
act(() => {
const notClickableElement = container.querySelector('.wont-trigger');
if (isDefined(notClickableElement)) {
fireEvent.mouseDown(notClickableElement);
fireEvent.click(notClickableElement);
}
});
expect(callback).not.toHaveBeenCalled();
});
});

View File

@ -138,58 +138,3 @@ export const useListenClickOutside = <T extends Element>({
} }
}, [refs, callback, mode, enabled, isMouseDownInside]); }, [refs, callback, mode, enabled, isMouseDownInside]);
}; };
export const useListenClickOutsideByClassName = ({
classNames,
excludeClassNames,
callback,
}: {
classNames: string[];
excludeClassNames?: string[];
callback: () => void;
}) => {
useEffect(() => {
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
if (!(event.target instanceof Node)) return;
const clickedElement = event.target as HTMLElement;
let isClickedInside = false;
let isClickedOnExcluded = false;
let currentElement: HTMLElement | null = clickedElement;
while (currentElement) {
const currentClassList = currentElement.classList;
isClickedInside = classNames.some((className) =>
currentClassList.contains(className),
);
isClickedOnExcluded =
excludeClassNames?.some((className) =>
currentClassList.contains(className),
) ?? false;
if (isClickedInside || isClickedOnExcluded) {
break;
}
currentElement = currentElement.parentElement;
}
if (!isClickedInside && !isClickedOnExcluded) {
callback();
}
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchend', handleClickOutside, {
capture: true,
});
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchend', handleClickOutside, {
capture: true,
});
};
}, [callback, classNames, excludeClassNames]);
};

View File

@ -10,6 +10,7 @@ export enum ClickOutsideMode {
export type ClickOutsideListenerProps<T extends Element> = { export type ClickOutsideListenerProps<T extends Element> = {
refs: Array<React.RefObject<T>>; refs: Array<React.RefObject<T>>;
excludeClassNames?: string[];
callback: (event: MouseEvent | TouchEvent) => void; callback: (event: MouseEvent | TouchEvent) => void;
mode?: ClickOutsideMode; mode?: ClickOutsideMode;
listenerId: string; listenerId: string;
@ -18,6 +19,7 @@ export type ClickOutsideListenerProps<T extends Element> = {
export const useListenClickOutsideV2 = <T extends Element>({ export const useListenClickOutsideV2 = <T extends Element>({
refs, refs,
excludeClassNames,
callback, callback,
mode = ClickOutsideMode.compareHTMLRef, mode = ClickOutsideMode.compareHTMLRef,
listenerId, listenerId,
@ -106,11 +108,34 @@ export const useListenClickOutsideV2 = <T extends Element>({
.getValue(); .getValue();
if (mode === ClickOutsideMode.compareHTMLRef) { 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 const clickedOnAtLeastOneRef = refs
.filter((ref) => !!ref.current) .filter((ref) => !!ref.current)
.some((ref) => ref.current?.contains(event.target as Node)); .some((ref) => ref.current?.contains(event.target as Node));
if (!clickedOnAtLeastOneRef && !isMouseDownInside) { if (
!clickedOnAtLeastOneRef &&
!isMouseDownInside &&
!isClickedOnExcluded
) {
callback(event); callback(event);
} }
} }
@ -151,7 +176,13 @@ export const useListenClickOutsideV2 = <T extends Element>({
} }
} }
}, },
[mode, refs, callback, getClickOutsideListenerIsMouseDownInsideState], [
getClickOutsideListenerIsMouseDownInsideState,
mode,
refs,
excludeClassNames,
callback,
],
); );
useEffect(() => { useEffect(() => {