Use data attributes for click outside instead of classNames (#12228)

We previously used classnames to exclude elements from the click outside
listener.

With this PR we can now use `data-click-outside-id` instead of
`classNames` to target the elements we want to exclude from the click
outside listener.

We can also add `data-globally-prevent-click-outside` to a component to
globally prevent triggering click outside listeners for other
components. This attribute is especially useful for confirmation modals
and snackbar items.

Fixes #11785:


https://github.com/user-attachments/assets/318baa7e-0f82-4e3a-a447-bf981328462d
This commit is contained in:
Raphaël Bosi
2025-05-22 18:10:51 +02:00
committed by GitHub
parent 45c89a46d6
commit 051f0fc83f
28 changed files with 104 additions and 63 deletions

View File

@ -1,10 +1,10 @@
import { createContext } from 'react';
type ClickOutsideListenerContextType = {
excludeClassName: string | undefined;
excludedClickOutsideId: string | undefined;
};
export const ClickOutsideListenerContext =
createContext<ClickOutsideListenerContextType>({
excludeClassName: undefined,
excludedClickOutsideId: undefined,
});

View File

@ -4,12 +4,13 @@ import { clickOutsideListenerMouseDownHappenedComponentState } from '@/ui/utilit
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import React, { useEffect } from 'react';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
const CLICK_OUTSIDE_DEBUG_MODE = false;
export type ClickOutsideListenerProps<T extends Element> = {
refs: Array<React.RefObject<T>>;
excludeClassNames?: string[];
excludedClickOutsideIds?: string[];
callback: (event: MouseEvent | TouchEvent) => void;
listenerId: string;
enabled?: boolean;
@ -17,7 +18,7 @@ export type ClickOutsideListenerProps<T extends Element> = {
export const useListenClickOutside = <T extends Element>({
refs,
excludeClassNames,
excludedClickOutsideIds,
callback,
listenerId,
enabled = true,
@ -90,12 +91,18 @@ export const useListenClickOutside = <T extends Element>({
let currentElement: HTMLElement | null = clickedElement;
while (currentElement) {
const currentClassList = currentElement.classList;
const currentDataAttributes = currentElement.dataset;
const isGloballyExcluded =
currentDataAttributes?.globallyPreventClickOutside === 'true';
const clickOutsideId = currentDataAttributes?.clickOutsideId;
isClickedOnExcluded =
excludeClassNames?.some((className) =>
currentClassList.contains(className),
) ?? false;
isGloballyExcluded ||
(isDefined(clickOutsideId) &&
isDefined(excludedClickOutsideIds) &&
excludedClickOutsideIds.includes(clickOutsideId));
if (isClickedOnExcluded) {
break;
@ -140,7 +147,7 @@ export const useListenClickOutside = <T extends Element>({
clickOutsideListenerIsMouseDownInsideState,
clickOutsideListenerMouseDownHappenedState,
refs,
excludeClassNames,
excludedClickOutsideIds,
callback,
listenerId,
],