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:
@ -7,10 +7,10 @@ import { useDarkMode } from 'storybook-dark-mode';
|
|||||||
import { RootDecorator } from '../src/testing/decorators/RootDecorator';
|
import { RootDecorator } from '../src/testing/decorators/RootDecorator';
|
||||||
import { mockedUserJWT } from '../src/testing/mock-data/jwt';
|
import { mockedUserJWT } from '../src/testing/mock-data/jwt';
|
||||||
|
|
||||||
|
import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
|
||||||
import 'react-loading-skeleton/dist/skeleton.css';
|
import 'react-loading-skeleton/dist/skeleton.css';
|
||||||
import 'twenty-ui/style.css';
|
import 'twenty-ui/style.css';
|
||||||
import { THEME_DARK, THEME_LIGHT, ThemeContextProvider } from 'twenty-ui/theme';
|
import { THEME_DARK, THEME_LIGHT, ThemeContextProvider } from 'twenty-ui/theme';
|
||||||
import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
|
|
||||||
|
|
||||||
initialize({
|
initialize({
|
||||||
onUnhandledRequest: async (request: Request) => {
|
onUnhandledRequest: async (request: Request) => {
|
||||||
@ -48,7 +48,7 @@ const preview: Preview = {
|
|||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<ThemeContextProvider theme={theme}>
|
<ThemeContextProvider theme={theme}>
|
||||||
<ClickOutsideListenerContext.Provider
|
<ClickOutsideListenerContext.Provider
|
||||||
value={{ excludeClassName: undefined }}
|
value={{ excludedClickOutsideId: undefined }}
|
||||||
>
|
>
|
||||||
<Story />
|
<Story />
|
||||||
</ClickOutsideListenerContext.Provider>
|
</ClickOutsideListenerContext.Provider>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { ActionComponent } from '@/action-menu/actions/display/components/ActionComponent';
|
import { ActionComponent } from '@/action-menu/actions/display/components/ActionComponent';
|
||||||
import { ActionScope } from '@/action-menu/actions/types/ActionScope';
|
import { ActionScope } from '@/action-menu/actions/types/ActionScope';
|
||||||
import { ActionType } from '@/action-menu/actions/types/ActionType';
|
import { ActionType } from '@/action-menu/actions/types/ActionType';
|
||||||
|
import { ACTION_MENU_DROPDOWN_CLICK_OUTSIDE_ID } from '@/action-menu/constants/ActionMenuDropdownClickOutsideId';
|
||||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||||
import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState';
|
import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState';
|
||||||
@ -81,7 +82,9 @@ export const RecordIndexActionMenuDropdown = () => {
|
|||||||
y: actionMenuDropdownPosition.y ?? 0,
|
y: actionMenuDropdownPosition.y ?? 0,
|
||||||
}}
|
}}
|
||||||
dropdownComponents={
|
dropdownComponents={
|
||||||
<StyledDropdownMenuContainer className="action-menu-dropdown">
|
<StyledDropdownMenuContainer
|
||||||
|
data-click-outside-id={ACTION_MENU_DROPDOWN_CLICK_OUTSIDE_ID}
|
||||||
|
>
|
||||||
<DropdownMenuItemsContainer>
|
<DropdownMenuItemsContainer>
|
||||||
<SelectableList
|
<SelectableList
|
||||||
hotkeyScope={ActionMenuDropdownHotkeyScope.ActionMenuDropdown}
|
hotkeyScope={ActionMenuDropdownHotkeyScope.ActionMenuDropdown}
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export const ACTION_MENU_DROPDOWN_CLICK_OUTSIDE_ID = 'action-menu-dropdown';
|
||||||
@ -30,7 +30,7 @@ export const App = () => {
|
|||||||
<ExceptionHandlerProvider>
|
<ExceptionHandlerProvider>
|
||||||
<HelmetProvider>
|
<HelmetProvider>
|
||||||
<ClickOutsideListenerContext.Provider
|
<ClickOutsideListenerContext.Provider
|
||||||
value={{ excludeClassName: undefined }}
|
value={{ excludedClickOutsideId: undefined }}
|
||||||
>
|
>
|
||||||
<AppRouter />
|
<AppRouter />
|
||||||
</ClickOutsideListenerContext.Provider>
|
</ClickOutsideListenerContext.Provider>
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { useExecuteTasksOnAnyLocationChange } from '@/app/hooks/useExecuteTasksO
|
|||||||
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
|
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
|
||||||
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
|
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
|
||||||
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
|
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
|
||||||
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||||
@ -89,6 +90,12 @@ export const PageChangeEffect = () => {
|
|||||||
const { executeTasksOnAnyLocationChange } =
|
const { executeTasksOnAnyLocationChange } =
|
||||||
useExecuteTasksOnAnyLocationChange();
|
useExecuteTasksOnAnyLocationChange();
|
||||||
|
|
||||||
|
const { closeCommandMenu } = useCommandMenu();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
closeCommandMenu();
|
||||||
|
}, [location.pathname, closeCommandMenu]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!previousLocation || previousLocation !== location.pathname) {
|
if (!previousLocation || previousLocation !== location.pathname) {
|
||||||
setPreviousLocation(location.pathname);
|
setPreviousLocation(location.pathname);
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { COMMAND_MENU_ANIMATION_VARIANTS } from '@/command-menu/constants/CommandMenuAnimationVariants';
|
import { COMMAND_MENU_ANIMATION_VARIANTS } from '@/command-menu/constants/CommandMenuAnimationVariants';
|
||||||
|
import { COMMAND_MENU_CLICK_OUTSIDE_ID } from '@/command-menu/constants/CommandMenuClickOutsideId';
|
||||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||||
import { CommandMenuAnimationVariant } from '@/command-menu/types/CommandMenuAnimationVariant';
|
import { CommandMenuAnimationVariant } from '@/command-menu/types/CommandMenuAnimationVariant';
|
||||||
import { CommandMenuHotkeyScope } from '@/command-menu/types/CommandMenuHotkeyScope';
|
import { CommandMenuHotkeyScope } from '@/command-menu/types/CommandMenuHotkeyScope';
|
||||||
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
|
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
|
||||||
|
import { PAGE_HEADER_COMMAND_MENU_BUTTON_CLICK_OUTSIDE_ID } from '@/ui/layout/page-header/constants/PageHeaderCommandMenuButtonClickOutsideId';
|
||||||
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
|
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
@ -62,14 +64,14 @@ export const CommandMenuOpenContainer = ({
|
|||||||
refs: [commandMenuRef],
|
refs: [commandMenuRef],
|
||||||
callback: handleClickOutside,
|
callback: handleClickOutside,
|
||||||
listenerId: 'COMMAND_MENU_LISTENER_ID',
|
listenerId: 'COMMAND_MENU_LISTENER_ID',
|
||||||
excludeClassNames: ['page-header-command-menu-button'],
|
excludedClickOutsideIds: [PAGE_HEADER_COMMAND_MENU_BUTTON_CLICK_OUTSIDE_ID],
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledCommandMenu
|
<StyledCommandMenu
|
||||||
data-testid="command-menu"
|
data-testid="command-menu"
|
||||||
|
data-click-outside-id={COMMAND_MENU_CLICK_OUTSIDE_ID}
|
||||||
ref={commandMenuRef}
|
ref={commandMenuRef}
|
||||||
className="command-menu"
|
|
||||||
animate={targetVariantForAnimation}
|
animate={targetVariantForAnimation}
|
||||||
initial="closed"
|
initial="closed"
|
||||||
exit="closed"
|
exit="closed"
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export const COMMAND_MENU_CLICK_OUTSIDE_ID = 'command-menu';
|
||||||
@ -3,7 +3,9 @@ import { DragDropContext, OnDragEndResponder } from '@hello-pangea/dnd'; // Atla
|
|||||||
import { useContext, useRef } from 'react';
|
import { useContext, useRef } from 'react';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
|
import { ACTION_MENU_DROPDOWN_CLICK_OUTSIDE_ID } from '@/action-menu/constants/ActionMenuDropdownClickOutsideId';
|
||||||
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
|
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
|
||||||
|
import { COMMAND_MENU_CLICK_OUTSIDE_ID } from '@/command-menu/constants/CommandMenuClickOutsideId';
|
||||||
import { RecordBoardHeader } from '@/object-record/record-board/components/RecordBoardHeader';
|
import { RecordBoardHeader } from '@/object-record/record-board/components/RecordBoardHeader';
|
||||||
import { RecordBoardScrollToFocusedCardEffect } from '@/object-record/record-board/components/RecordBoardScrollToFocusedCardEffect';
|
import { RecordBoardScrollToFocusedCardEffect } from '@/object-record/record-board/components/RecordBoardScrollToFocusedCardEffect';
|
||||||
import { RecordBoardStickyHeaderEffect } from '@/object-record/record-board/components/RecordBoardStickyHeaderEffect';
|
import { RecordBoardStickyHeaderEffect } from '@/object-record/record-board/components/RecordBoardStickyHeaderEffect';
|
||||||
@ -13,6 +15,7 @@ import { useActiveRecordBoardCard } from '@/object-record/record-board/hooks/use
|
|||||||
import { useFocusedRecordBoardCard } from '@/object-record/record-board/hooks/useFocusedRecordBoardCard';
|
import { useFocusedRecordBoardCard } from '@/object-record/record-board/hooks/useFocusedRecordBoardCard';
|
||||||
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
|
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
|
||||||
import { RecordBoardDeactivateBoardCardEffect } from '@/object-record/record-board/record-board-card/components/RecordBoardDeactivateBoardCardEffect';
|
import { RecordBoardDeactivateBoardCardEffect } from '@/object-record/record-board/record-board-card/components/RecordBoardDeactivateBoardCardEffect';
|
||||||
|
import { RECORD_BOARD_CARD_CLICK_OUTSIDE_ID } from '@/object-record/record-board/record-board-card/constants/RecordBoardCardClickOutsideId';
|
||||||
import { RecordBoardColumn } from '@/object-record/record-board/record-board-column/components/RecordBoardColumn';
|
import { RecordBoardColumn } from '@/object-record/record-board/record-board-column/components/RecordBoardColumn';
|
||||||
import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoardScope';
|
import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoardScope';
|
||||||
import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext';
|
import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext';
|
||||||
@ -24,7 +27,9 @@ import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record
|
|||||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
|
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
|
||||||
|
import { MODAL_BACKDROP_CLICK_OUTSIDE_ID } from '@/ui/layout/modal/constants/ModalBackdropClickOutsideId';
|
||||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||||
|
import { PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID } from '@/ui/layout/page/constants/PageActionContainerClickOutsideId';
|
||||||
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
|
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
|
||||||
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
@ -104,13 +109,12 @@ export const RecordBoard = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useListenClickOutside({
|
useListenClickOutside({
|
||||||
excludeClassNames: [
|
excludedClickOutsideIds: [
|
||||||
'bottom-bar',
|
ACTION_MENU_DROPDOWN_CLICK_OUTSIDE_ID,
|
||||||
'action-menu-dropdown',
|
COMMAND_MENU_CLICK_OUTSIDE_ID,
|
||||||
'command-menu',
|
MODAL_BACKDROP_CLICK_OUTSIDE_ID,
|
||||||
'modal-backdrop',
|
PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID,
|
||||||
'page-action-container',
|
RECORD_BOARD_CARD_CLICK_OUTSIDE_ID,
|
||||||
'record-board-card',
|
|
||||||
],
|
],
|
||||||
listenerId: RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID,
|
listenerId: RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID,
|
||||||
refs: [],
|
refs: [],
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { ActionMenuDropdownHotkeyScope } from '@/action-menu/types/ActionMenuDro
|
|||||||
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
|
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
|
||||||
import { RecordBoardCardBody } from '@/object-record/record-board/record-board-card/components/RecordBoardCardBody';
|
import { RecordBoardCardBody } from '@/object-record/record-board/record-board-card/components/RecordBoardCardBody';
|
||||||
import { RecordBoardCardHeader } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHeader';
|
import { RecordBoardCardHeader } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHeader';
|
||||||
|
import { RECORD_BOARD_CARD_CLICK_OUTSIDE_ID } from '@/object-record/record-board/record-board-card/constants/RecordBoardCardClickOutsideId';
|
||||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||||
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
|
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
@ -197,7 +198,7 @@ export const RecordBoardCard = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledBoardCardWrapper
|
<StyledBoardCardWrapper
|
||||||
className="record-board-card"
|
data-click-outside-id={RECORD_BOARD_CARD_CLICK_OUTSIDE_ID}
|
||||||
onContextMenu={handleActionMenuDropdown}
|
onContextMenu={handleActionMenuDropdown}
|
||||||
>
|
>
|
||||||
<InView>
|
<InView>
|
||||||
|
|||||||
@ -50,7 +50,6 @@ export const RecordBoardCardDraggableContainer = ({
|
|||||||
{...draggableProvided?.dragHandleProps}
|
{...draggableProvided?.dragHandleProps}
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...draggableProvided?.draggableProps}
|
{...draggableProvided?.draggableProps}
|
||||||
className="record-board-card"
|
|
||||||
data-selectable-id={recordId}
|
data-selectable-id={recordId}
|
||||||
data-select-disable
|
data-select-disable
|
||||||
>
|
>
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export const RECORD_BOARD_CARD_CLICK_OUTSIDE_ID = 'record-board-card';
|
||||||
@ -1,8 +1,12 @@
|
|||||||
|
import { ACTION_MENU_DROPDOWN_CLICK_OUTSIDE_ID } from '@/action-menu/constants/ActionMenuDropdownClickOutsideId';
|
||||||
|
import { COMMAND_MENU_CLICK_OUTSIDE_ID } from '@/command-menu/constants/CommandMenuClickOutsideId';
|
||||||
import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope';
|
import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope';
|
||||||
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
|
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
|
||||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||||
import { useLeaveTableFocus } from '@/object-record/record-table/hooks/internal/useLeaveTableFocus';
|
import { useLeaveTableFocus } from '@/object-record/record-table/hooks/internal/useLeaveTableFocus';
|
||||||
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||||
|
import { MODAL_BACKDROP_CLICK_OUTSIDE_ID } from '@/ui/layout/modal/constants/ModalBackdropClickOutsideId';
|
||||||
|
import { PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID } from '@/ui/layout/page/constants/PageActionContainerClickOutsideId';
|
||||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
|
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
@ -23,12 +27,11 @@ export const RecordTableBodyFocusClickOutsideEffect = ({
|
|||||||
const setHotkeyScope = useSetHotkeyScope();
|
const setHotkeyScope = useSetHotkeyScope();
|
||||||
|
|
||||||
useListenClickOutside({
|
useListenClickOutside({
|
||||||
excludeClassNames: [
|
excludedClickOutsideIds: [
|
||||||
'bottom-bar',
|
ACTION_MENU_DROPDOWN_CLICK_OUTSIDE_ID,
|
||||||
'action-menu-dropdown',
|
COMMAND_MENU_CLICK_OUTSIDE_ID,
|
||||||
'command-menu',
|
PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID,
|
||||||
'modal-backdrop',
|
MODAL_BACKDROP_CLICK_OUTSIDE_ID,
|
||||||
'page-action-container',
|
|
||||||
],
|
],
|
||||||
listenerId: RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID,
|
listenerId: RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID,
|
||||||
refs: [tableBodyRef],
|
refs: [tableBodyRef],
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { Key } from 'ts-key-enum';
|
|||||||
|
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
|
|
||||||
|
import { DIALOG_CLICK_OUTSIDE_ID } from '@/ui/feedback/dialog-manager/constants/DialogClickOutsideId';
|
||||||
import { DIALOG_LISTENER_ID } from '@/ui/feedback/dialog-manager/constants/DialogListenerId';
|
import { DIALOG_LISTENER_ID } from '@/ui/feedback/dialog-manager/constants/DialogListenerId';
|
||||||
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
|
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
@ -138,6 +139,7 @@ export const Dialog = ({
|
|||||||
animate="open"
|
animate="open"
|
||||||
exit="closed"
|
exit="closed"
|
||||||
className={className}
|
className={className}
|
||||||
|
data-click-outside-id={DIALOG_CLICK_OUTSIDE_ID}
|
||||||
>
|
>
|
||||||
<StyledDialogContainer
|
<StyledDialogContainer
|
||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
|
|||||||
@ -15,7 +15,6 @@ export const DialogManager = ({ children }: React.PropsWithChildren) => {
|
|||||||
{dialogInternal.queue.map(({ buttons, children, id, message, title }) => (
|
{dialogInternal.queue.map(({ buttons, children, id, message, title }) => (
|
||||||
<Dialog
|
<Dialog
|
||||||
key={id}
|
key={id}
|
||||||
className="dialog-manager-dialog"
|
|
||||||
{...{ title, message, buttons, id, children }}
|
{...{ title, message, buttons, id, children }}
|
||||||
onClose={() => closeDialog(id)}
|
onClose={() => closeDialog(id)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export const DIALOG_CLICK_OUTSIDE_ID = 'dialog-manager-dialog';
|
||||||
@ -203,6 +203,7 @@ export const SnackBar = ({
|
|||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
title={message || defaultAriaLabelByVariant[variant]}
|
title={message || defaultAriaLabelByVariant[variant]}
|
||||||
{...{ className, id, role, variant }}
|
{...{ className, id, role, variant }}
|
||||||
|
data-globally-prevent-click-outside
|
||||||
>
|
>
|
||||||
<StyledProgressBar
|
<StyledProgressBar
|
||||||
barColor={theme.snackBar[variant].backgroundColor}
|
barColor={theme.snackBar[variant].backgroundColor}
|
||||||
|
|||||||
@ -79,7 +79,6 @@ export const DropdownContent = ({
|
|||||||
useListenClickOutside({
|
useListenClickOutside({
|
||||||
refs: [floatingUiRefs.floating, floatingUiRefs.domReference],
|
refs: [floatingUiRefs.floating, floatingUiRefs.domReference],
|
||||||
listenerId: dropdownId,
|
listenerId: dropdownId,
|
||||||
excludeClassNames: ['confirmation-modal'],
|
|
||||||
callback: (event) => {
|
callback: (event) => {
|
||||||
if (activeDropdownFocusId !== dropdownId) return;
|
if (activeDropdownFocusId !== dropdownId) return;
|
||||||
|
|
||||||
@ -118,7 +117,7 @@ export const DropdownContent = ({
|
|||||||
maxWidth: dropdownMaxWidth,
|
maxWidth: dropdownMaxWidth,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { excludeClassName } = useContext(ClickOutsideListenerContext);
|
const { excludedClickOutsideId } = useContext(ClickOutsideListenerContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -127,24 +126,23 @@ export const DropdownContent = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<FloatingPortal>
|
<FloatingPortal>
|
||||||
<div className={excludeClassName}>
|
<StyledDropdownContentContainer
|
||||||
<StyledDropdownContentContainer
|
ref={floatingUiRefs.setFloating}
|
||||||
ref={floatingUiRefs.setFloating}
|
style={dropdownMenuStyles}
|
||||||
style={dropdownMenuStyles}
|
role="listbox"
|
||||||
role="listbox"
|
id={`${dropdownId}-options`}
|
||||||
id={`${dropdownId}-options`}
|
data-click-outside-id={excludedClickOutsideId}
|
||||||
>
|
>
|
||||||
<OverlayContainer>
|
<OverlayContainer>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
id={dropdownId}
|
id={dropdownId}
|
||||||
width={dropdownWidth}
|
width={dropdownWidth}
|
||||||
data-select-disable
|
data-select-disable
|
||||||
>
|
>
|
||||||
{dropdownComponents}
|
{dropdownComponents}
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</OverlayContainer>
|
</OverlayContainer>
|
||||||
</StyledDropdownContentContainer>
|
</StyledDropdownContentContainer>
|
||||||
</div>
|
|
||||||
</FloatingPortal>
|
</FloatingPortal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -119,7 +119,7 @@ export const ConfirmationModal = ({
|
|||||||
isClosable={true}
|
isClosable={true}
|
||||||
padding="large"
|
padding="large"
|
||||||
modalVariant={modalVariant}
|
modalVariant={modalVariant}
|
||||||
className="confirmation-modal"
|
data-globally-prevent-click-outside
|
||||||
>
|
>
|
||||||
<StyledCenteredTitle>
|
<StyledCenteredTitle>
|
||||||
<H1Title title={title} fontColor={H1TitleFontColor.Primary} />
|
<H1Title title={title} fontColor={H1TitleFontColor.Primary} />
|
||||||
|
|||||||
@ -4,7 +4,8 @@ import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkey
|
|||||||
import { ModalComponentInstanceContext } from '@/ui/layout/modal/contexts/ModalComponentInstanceContext';
|
import { ModalComponentInstanceContext } from '@/ui/layout/modal/contexts/ModalComponentInstanceContext';
|
||||||
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
|
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 { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||||
import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
|
import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
@ -229,8 +230,7 @@ export const Modal = ({
|
|||||||
>
|
>
|
||||||
<ClickOutsideListenerContext.Provider
|
<ClickOutsideListenerContext.Provider
|
||||||
value={{
|
value={{
|
||||||
excludeClassName:
|
excludedClickOutsideId: MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_ID,
|
||||||
MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_CLASS_NAME,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalHotkeysAndClickOutsideEffect
|
<ModalHotkeysAndClickOutsideEffect
|
||||||
@ -242,7 +242,7 @@ export const Modal = ({
|
|||||||
/>
|
/>
|
||||||
<StyledBackDrop
|
<StyledBackDrop
|
||||||
data-testid="modal-backdrop"
|
data-testid="modal-backdrop"
|
||||||
className="modal-backdrop"
|
data-click-outside-id={MODAL_BACKDROP_CLICK_OUTSIDE_ID}
|
||||||
onMouseDown={stopEventPropagation}
|
onMouseDown={stopEventPropagation}
|
||||||
modalVariant={modalVariant}
|
modalVariant={modalVariant}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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 { 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 { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
@ -45,9 +46,9 @@ export const ModalHotkeysAndClickOutsideEffect = ({
|
|||||||
|
|
||||||
useListenClickOutside({
|
useListenClickOutside({
|
||||||
refs: [modalRef],
|
refs: [modalRef],
|
||||||
excludeClassNames: [
|
excludedClickOutsideIds: [
|
||||||
MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_CLASS_NAME,
|
MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_ID,
|
||||||
'dialog-manager-dialog',
|
DIALOG_CLICK_OUTSIDE_ID,
|
||||||
],
|
],
|
||||||
listenerId: `MODAL_CLICK_OUTSIDE_LISTENER_ID_${modalId}`,
|
listenerId: `MODAL_CLICK_OUTSIDE_LISTENER_ID_${modalId}`,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export const MODAL_BACKDROP_CLICK_OUTSIDE_ID = 'modal-backdrop';
|
||||||
@ -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';
|
'modal-click-outside-listener-excluded';
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||||
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
|
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 { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { i18n } from '@lingui/core';
|
import { i18n } from '@lingui/core';
|
||||||
@ -124,7 +125,9 @@ export const PageHeaderToggleCommandMenuButton = () => {
|
|||||||
animatedSvg={
|
animatedSvg={
|
||||||
<AnimatedIcon isCommandMenuOpened={isCommandMenuOpened} />
|
<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"
|
dataTestId="page-header-command-menu-button"
|
||||||
size={isMobile ? 'medium' : 'small'}
|
size={isMobile ? 'medium' : 'small'}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
export const PAGE_HEADER_COMMAND_MENU_BUTTON_CLICK_OUTSIDE_ID =
|
||||||
|
'page-header-command-menu-button';
|
||||||
@ -5,6 +5,7 @@ import { useRecoilValue } from 'recoil';
|
|||||||
|
|
||||||
import { NavigationDrawerCollapseButton } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton';
|
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 { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
import {
|
import {
|
||||||
@ -144,7 +145,9 @@ export const PageHeader = ({
|
|||||||
</StyledTopBarIconStyledTitleContainer>
|
</StyledTopBarIconStyledTitleContainer>
|
||||||
</StyledLeftContainer>
|
</StyledLeftContainer>
|
||||||
|
|
||||||
<StyledPageActionContainer className="page-action-container">
|
<StyledPageActionContainer
|
||||||
|
data-click-outside-id={PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</StyledPageActionContainer>
|
</StyledPageActionContainer>
|
||||||
</StyledTopBarContainer>
|
</StyledTopBarContainer>
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export const PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID = 'page-action-container';
|
||||||
@ -1,10 +1,10 @@
|
|||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
|
|
||||||
type ClickOutsideListenerContextType = {
|
type ClickOutsideListenerContextType = {
|
||||||
excludeClassName: string | undefined;
|
excludedClickOutsideId: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ClickOutsideListenerContext =
|
export const ClickOutsideListenerContext =
|
||||||
createContext<ClickOutsideListenerContextType>({
|
createContext<ClickOutsideListenerContextType>({
|
||||||
excludeClassName: undefined,
|
excludedClickOutsideId: undefined,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,12 +4,13 @@ import { clickOutsideListenerMouseDownHappenedComponentState } from '@/ui/utilit
|
|||||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
const CLICK_OUTSIDE_DEBUG_MODE = false;
|
const CLICK_OUTSIDE_DEBUG_MODE = false;
|
||||||
|
|
||||||
export type ClickOutsideListenerProps<T extends Element> = {
|
export type ClickOutsideListenerProps<T extends Element> = {
|
||||||
refs: Array<React.RefObject<T>>;
|
refs: Array<React.RefObject<T>>;
|
||||||
excludeClassNames?: string[];
|
excludedClickOutsideIds?: string[];
|
||||||
callback: (event: MouseEvent | TouchEvent) => void;
|
callback: (event: MouseEvent | TouchEvent) => void;
|
||||||
listenerId: string;
|
listenerId: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
@ -17,7 +18,7 @@ export type ClickOutsideListenerProps<T extends Element> = {
|
|||||||
|
|
||||||
export const useListenClickOutside = <T extends Element>({
|
export const useListenClickOutside = <T extends Element>({
|
||||||
refs,
|
refs,
|
||||||
excludeClassNames,
|
excludedClickOutsideIds,
|
||||||
callback,
|
callback,
|
||||||
listenerId,
|
listenerId,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
@ -90,12 +91,18 @@ export const useListenClickOutside = <T extends Element>({
|
|||||||
let currentElement: HTMLElement | null = clickedElement;
|
let currentElement: HTMLElement | null = clickedElement;
|
||||||
|
|
||||||
while (currentElement) {
|
while (currentElement) {
|
||||||
const currentClassList = currentElement.classList;
|
const currentDataAttributes = currentElement.dataset;
|
||||||
|
|
||||||
|
const isGloballyExcluded =
|
||||||
|
currentDataAttributes?.globallyPreventClickOutside === 'true';
|
||||||
|
|
||||||
|
const clickOutsideId = currentDataAttributes?.clickOutsideId;
|
||||||
|
|
||||||
isClickedOnExcluded =
|
isClickedOnExcluded =
|
||||||
excludeClassNames?.some((className) =>
|
isGloballyExcluded ||
|
||||||
currentClassList.contains(className),
|
(isDefined(clickOutsideId) &&
|
||||||
) ?? false;
|
isDefined(excludedClickOutsideIds) &&
|
||||||
|
excludedClickOutsideIds.includes(clickOutsideId));
|
||||||
|
|
||||||
if (isClickedOnExcluded) {
|
if (isClickedOnExcluded) {
|
||||||
break;
|
break;
|
||||||
@ -140,7 +147,7 @@ export const useListenClickOutside = <T extends Element>({
|
|||||||
clickOutsideListenerIsMouseDownInsideState,
|
clickOutsideListenerIsMouseDownInsideState,
|
||||||
clickOutsideListenerMouseDownHappenedState,
|
clickOutsideListenerMouseDownHappenedState,
|
||||||
refs,
|
refs,
|
||||||
excludeClassNames,
|
excludedClickOutsideIds,
|
||||||
callback,
|
callback,
|
||||||
listenerId,
|
listenerId,
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user