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

@ -79,7 +79,6 @@ export const DropdownContent = ({
useListenClickOutside({
refs: [floatingUiRefs.floating, floatingUiRefs.domReference],
listenerId: dropdownId,
excludeClassNames: ['confirmation-modal'],
callback: (event) => {
if (activeDropdownFocusId !== dropdownId) return;
@ -118,7 +117,7 @@ export const DropdownContent = ({
maxWidth: dropdownMaxWidth,
};
const { excludeClassName } = useContext(ClickOutsideListenerContext);
const { excludedClickOutsideId } = useContext(ClickOutsideListenerContext);
return (
<>
@ -127,24 +126,23 @@ export const DropdownContent = ({
)}
<FloatingPortal>
<div className={excludeClassName}>
<StyledDropdownContentContainer
ref={floatingUiRefs.setFloating}
style={dropdownMenuStyles}
role="listbox"
id={`${dropdownId}-options`}
>
<OverlayContainer>
<DropdownMenu
id={dropdownId}
width={dropdownWidth}
data-select-disable
>
{dropdownComponents}
</DropdownMenu>
</OverlayContainer>
</StyledDropdownContentContainer>
</div>
<StyledDropdownContentContainer
ref={floatingUiRefs.setFloating}
style={dropdownMenuStyles}
role="listbox"
id={`${dropdownId}-options`}
data-click-outside-id={excludedClickOutsideId}
>
<OverlayContainer>
<DropdownMenu
id={dropdownId}
width={dropdownWidth}
data-select-disable
>
{dropdownComponents}
</DropdownMenu>
</OverlayContainer>
</StyledDropdownContentContainer>
</FloatingPortal>
</>
);

View File

@ -119,7 +119,7 @@ export const ConfirmationModal = ({
isClosable={true}
padding="large"
modalVariant={modalVariant}
className="confirmation-modal"
data-globally-prevent-click-outside
>
<StyledCenteredTitle>
<H1Title title={title} fontColor={H1TitleFontColor.Primary} />

View File

@ -4,7 +4,8 @@ import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkey
import { ModalComponentInstanceContext } from '@/ui/layout/modal/contexts/ModalComponentInstanceContext';
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
import { MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_CLASS_NAME } from '@/ui/layout/modal/constants/ModalClickOutsideListenerExcludedClassName';
import { MODAL_BACKDROP_CLICK_OUTSIDE_ID } from '@/ui/layout/modal/constants/ModalBackdropClickOutsideId';
import { MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_ID } from '@/ui/layout/modal/constants/ModalClickOutsideListenerExcludedClassName';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
@ -229,8 +230,7 @@ export const Modal = ({
>
<ClickOutsideListenerContext.Provider
value={{
excludeClassName:
MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_CLASS_NAME,
excludedClickOutsideId: MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_ID,
}}
>
<ModalHotkeysAndClickOutsideEffect
@ -242,7 +242,7 @@ export const Modal = ({
/>
<StyledBackDrop
data-testid="modal-backdrop"
className="modal-backdrop"
data-click-outside-id={MODAL_BACKDROP_CLICK_OUTSIDE_ID}
onMouseDown={stopEventPropagation}
modalVariant={modalVariant}
>

View File

@ -1,5 +1,6 @@
import { DIALOG_CLICK_OUTSIDE_ID } from '@/ui/feedback/dialog-manager/constants/DialogClickOutsideId';
import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope';
import { MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_CLASS_NAME } from '@/ui/layout/modal/constants/ModalClickOutsideListenerExcludedClassName';
import { MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_ID } from '@/ui/layout/modal/constants/ModalClickOutsideListenerExcludedClassName';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { Key } from 'ts-key-enum';
@ -45,9 +46,9 @@ export const ModalHotkeysAndClickOutsideEffect = ({
useListenClickOutside({
refs: [modalRef],
excludeClassNames: [
MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_CLASS_NAME,
'dialog-manager-dialog',
excludedClickOutsideIds: [
MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_ID,
DIALOG_CLICK_OUTSIDE_ID,
],
listenerId: `MODAL_CLICK_OUTSIDE_LISTENER_ID_${modalId}`,
callback: () => {

View File

@ -0,0 +1 @@
export const MODAL_BACKDROP_CLICK_OUTSIDE_ID = 'modal-backdrop';

View File

@ -1,2 +1,2 @@
export const MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_CLASS_NAME =
export const MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_ID =
'modal-click-outside-listener-excluded';

View File

@ -1,6 +1,7 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
import { PAGE_HEADER_COMMAND_MENU_BUTTON_CLICK_OUTSIDE_ID } from '@/ui/layout/page-header/constants/PageHeaderCommandMenuButtonClickOutsideId';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { i18n } from '@lingui/core';
@ -124,7 +125,9 @@ export const PageHeaderToggleCommandMenuButton = () => {
animatedSvg={
<AnimatedIcon isCommandMenuOpened={isCommandMenuOpened} />
}
className="page-header-command-menu-button"
data-click-outside-id={
PAGE_HEADER_COMMAND_MENU_BUTTON_CLICK_OUTSIDE_ID
}
dataTestId="page-header-command-menu-button"
size={isMobile ? 'medium' : 'small'}
variant="secondary"

View File

@ -0,0 +1,2 @@
export const PAGE_HEADER_COMMAND_MENU_BUTTON_CLICK_OUTSIDE_ID =
'page-header-command-menu-button';

View File

@ -5,6 +5,7 @@ import { useRecoilValue } from 'recoil';
import { NavigationDrawerCollapseButton } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton';
import { PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID } from '@/ui/layout/page/constants/PageActionContainerClickOutsideId';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import {
@ -144,7 +145,9 @@ export const PageHeader = ({
</StyledTopBarIconStyledTitleContainer>
</StyledLeftContainer>
<StyledPageActionContainer className="page-action-container">
<StyledPageActionContainer
data-click-outside-id={PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID}
>
{children}
</StyledPageActionContainer>
</StyledTopBarContainer>

View File

@ -0,0 +1 @@
export const PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID = 'page-action-container';