Skip to content

Commit

Permalink
fix(Dialog): "esc" key close dialogs in wrong order (#5021)
Browse files Browse the repository at this point in the history
* fix(Dialog): "esc" close dialogs in wrong order

Related items: #5019

* proposal to discuss

* last bits

* added password component to comment esc key handling approach

* add esc key handling to cascade select

* add esc key handling to split button + fix error in overlay panel

* fix: add speed dial to common esc handling approach

* fix: prop naming

* fix: format

---------

Co-authored-by: Melloware <[email protected]>
  • Loading branch information
avasuro and melloware authored Nov 29, 2023
1 parent 83ebb52 commit 11c9a6b
Show file tree
Hide file tree
Showing 17 changed files with 300 additions and 106 deletions.
35 changes: 24 additions & 11 deletions components/doc/common/apidoc/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -25898,22 +25898,35 @@
"returnType": "boolean",
"description": "Custom hook to detect if window size matches or not."
},
"useOnEscapeKey": {
"name": "useOnEscapeKey",
"useGlobalOnEscapeKey": {
"name": "useGlobalOnEscapeKey",
"parameters": [
{
"name": "ref",
"type": "RefObject<Element>",
"description": "The ref of the element to detect escape button click."
"name": "params",
"type": "object"
}
],
"returnType": "void",
"description": "Custom hook to use detect global escape button click."
},
"useDisplayOrder": {
"name": "useDisplayOrder",
"parameters": [
{
"name": "group",
"type": "string",
"description": "ID of the group of UI components to which component belongs"
},
{
"name": "callback",
"type": "any",
"description": "The callback to run when escape button clicked."
"name": "isVisible",
"type": "boolean",
"optional": true,
"default": "true",
"description": "Is current component currently visible"
}
],
"returnType": "void",
"description": "Custom hook to use detect escape button click."
"returnType": "number|undefined",
"description": "Custom hook to use display order of component of one and the same group."
}
}
},
Expand Down Expand Up @@ -54144,4 +54157,4 @@
}
}
}
}
}
12 changes: 10 additions & 2 deletions components/lib/cascadeselect/CascadeSelect.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import PrimeReact, { PrimeReactContext } from '../api/Api';
import { useHandleStyle } from '../componentbase/ComponentBase';
import { CSSTransition } from '../csstransition/CSSTransition';
import { useMountEffect, useOnEscapeKey, useOverlayListener, useUnmountEffect, useUpdateEffect } from '../hooks/Hooks';
import { ESC_KEY_HANDLING_PRIORITIES, useDisplayOrder, useGlobalOnEscapeKey, useMountEffect, useOverlayListener, useUnmountEffect, useUpdateEffect } from '../hooks/Hooks';
import { ChevronDownIcon } from '../icons/chevrondown';
import { OverlayService } from '../overlayservice/OverlayService';
import { Portal } from '../portal/Portal';
Expand Down Expand Up @@ -44,7 +44,15 @@ export const CascadeSelect = React.memo(
when: overlayVisibleState
});

useOnEscapeKey(overlayRef, overlayVisibleState, () => hide());
const cascadeSelectOverlayDisplayOrder = useDisplayOrder('cascade-select', overlayVisibleState);

useGlobalOnEscapeKey({
callback: () => {
hide();
},
when: overlayVisibleState,
priority: [ESC_KEY_HANDLING_PRIORITIES.CASCADE_SELECT, cascadeSelectOverlayDisplayOrder]
});

const onOptionSelect = (event) => {
if (props.onChange) {
Expand Down
14 changes: 11 additions & 3 deletions components/lib/confirmpopup/ConfirmPopup.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import PrimeReact, { PrimeReactContext, localeOption } from '../api/Api';
import { Button } from '../button/Button';
import { useHandleStyle } from '../componentbase/ComponentBase';
import { CSSTransition } from '../csstransition/CSSTransition';
import { useOverlayListener, useUnmountEffect, useUpdateEffect } from '../hooks/Hooks';
import { ESC_KEY_HANDLING_PRIORITIES, useDisplayOrder, useGlobalOnEscapeKey, useOverlayListener, useUnmountEffect, useUpdateEffect } from '../hooks/Hooks';
import { OverlayService } from '../overlayservice/OverlayService';
import { Portal } from '../portal/Portal';
import { DomHandler, IconUtils, ObjectUtils, ZIndexUtils, classNames, mergeProps } from '../utils/Utils';
Expand Down Expand Up @@ -58,8 +58,16 @@ export const ConfirmPopup = React.memo(
const acceptLabel = getPropValue('acceptLabel') || localeOption('accept');
const rejectLabel = getPropValue('rejectLabel') || localeOption('reject');

useOnEscapeKey(overlayRef, props.dismissable && props.closeOnEscape, (event) => {
hide('hide');
const displayOrder = useDisplayOrder('dialog', visibleState);

useGlobalOnEscapeKey({
callback: () => {
if (props.dismissable && props.closeOnEscape) {
hide('hide');
}
},
when: visibleState,
priority: [ESC_KEY_HANDLING_PRIORITIES.DIALOG, displayOrder]
});

const [bindOverlayListener, unbindOverlayListener] = useOverlayListener({
Expand Down
36 changes: 17 additions & 19 deletions components/lib/dialog/Dialog.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import * as React from 'react';
import { useOnEscapeKey } from '../../lib/hooks/Hooks';
import PrimeReact, { PrimeReactContext, localeOption } from '../api/Api';
import { useHandleStyle } from '../componentbase/ComponentBase';
import { CSSTransition } from '../csstransition/CSSTransition';
import { useEventListener, useMountEffect, useUnmountEffect, useUpdateEffect } from '../hooks/Hooks';
import { useDisplayOrder, useEventListener, useMountEffect, useUnmountEffect, useUpdateEffect, useGlobalOnEscapeKey, ESC_KEY_HANDLING_PRIORITIES } from '../hooks/Hooks';
import { TimesIcon } from '../icons/times';
import { WindowMaximizeIcon } from '../icons/windowmaximize';
import { WindowMinimizeIcon } from '../icons/windowminimize';
Expand Down Expand Up @@ -50,18 +49,16 @@ export const Dialog = React.forwardRef((inProps, ref) => {

useHandleStyle(DialogBase.css.styles, isUnstyled, { name: 'dialog' });

useOnEscapeKey(maskRef, props.closable && props.closeOnEscape, (event) => {
const currentTarget = event.currentTarget;

if (!currentTarget || !currentTarget.primeDialogParams) {
return;
}

const params = currentTarget.primeDialogParams;
const paramLength = params.length;
const displayOrder = useDisplayOrder('dialog', visibleState);

onClose(event);
params.splice(paramLength - 1, 1);
useGlobalOnEscapeKey({
callback: (event) => {
if (props.closable && props.closeOnEscape) {
onClose(event);
}
},
when: visibleState,
priority: [ESC_KEY_HANDLING_PRIORITIES.DIALOG, displayOrder]
});

const [bindDocumentKeyDownListener, unbindDocumentKeyDownListener] = useEventListener({ type: 'keydown', listener: (event) => onKeyDown(event) });
Expand Down Expand Up @@ -319,12 +316,10 @@ export const Dialog = React.forwardRef((inProps, ref) => {

const enableDocumentSettings = () => {
bindGlobalListeners();
updateGlobalDialogsRegistry(true);
};

const disableDocumentSettings = () => {
unbindGlobalListeners();
updateGlobalDialogsRegistry(false);
};

const updateScrollBlocker = () => {
Expand All @@ -339,8 +334,8 @@ export const Dialog = React.forwardRef((inProps, ref) => {
};

const updateGlobalDialogsRegistry = (isMounted) => {
// Update current dialog info in global registry if it is mounted:
if (isMounted) {
// Update current dialog info in global registry if it is mounted and visible:
if (isMounted && visibleState) {
const newParam = { id: idState, hasBlockScroll: shouldBlockScroll };

// Create registry if not yet created:
Expand All @@ -356,7 +351,7 @@ export const Dialog = React.forwardRef((inProps, ref) => {
document.primeDialogParams = document.primeDialogParams.toSpliced(currentDialogIndexInRegistry, 1, newParam);
}
}
// Or remove it from global registry if unmounted:
// Or remove it from global registry if unmounted or invisible:
else {
document.primeDialogParams = document.primeDialogParams && document.primeDialogParams.filter((param) => param.id !== idState);
}
Expand Down Expand Up @@ -407,6 +402,8 @@ export const Dialog = React.forwardRef((inProps, ref) => {
};

useMountEffect(() => {
updateGlobalDialogsRegistry(true);

if (props.visible) {
setMaskVisibleState(true);
}
Expand Down Expand Up @@ -441,10 +438,11 @@ export const Dialog = React.forwardRef((inProps, ref) => {

useUpdateEffect(() => {
updateGlobalDialogsRegistry(true);
}, [shouldBlockScroll]);
}, [shouldBlockScroll, visibleState]);

useUnmountEffect(() => {
disableDocumentSettings();
updateGlobalDialogsRegistry(false);
DomHandler.removeInlineStyle(styleElement.current);
ZIndexUtils.clear(maskRef.current);
});
Expand Down
7 changes: 5 additions & 2 deletions components/lib/hooks/Hooks.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useClickOutside } from './useClickOutside';
import { useCounter } from './useCounter';
import { useDebounce } from './useDebounce';
import { useDisplayOrder } from './useDisplayOrder';
import { useEventListener } from './useEventListener';
import { useFavicon } from './useFavicon';
import { useGlobalOnEscapeKey, ESC_KEY_HANDLING_PRIORITIES } from './useGlobalOnEscapeKey';
import { useIntersectionObserver } from './useIntersectionObserver';
import { useInterval } from './useInterval';
import { useMatchMedia } from './useMatchMedia';
Expand All @@ -18,7 +20,6 @@ import { useTimeout } from './useTimeout';
import { useUnmountEffect } from './useUnmountEffect';
import { useUpdateEffect } from './useUpdateEffect';
import { useStyle } from './useStyle';
import { useOnEscapeKey } from './useOnEscapeKey';

export {
usePrevious,
Expand All @@ -42,6 +43,8 @@ export {
useMove,
useClickOutside,
useDebounce,
useDisplayOrder,
useMatchMedia,
useOnEscapeKey
useGlobalOnEscapeKey,
ESC_KEY_HANDLING_PRIORITIES
};
12 changes: 8 additions & 4 deletions components/lib/hooks/hooks.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,12 @@ export declare function useClickOutside(ref: React.RefObject<Element>, callback:
*/
export declare function useMatchMedia(query: string, when?: boolean): boolean;
/**
* Custom hook to use detect escape button click.
* @param {React.RefObject<Element>} ref - The ref of the element to detect escape button click.
* @param {*} callback - The callback to run when escape button clicked.
* Custom hook to use detect global escape button click.
*/
export declare function useOnEscapeKey(ref: React.RefObject<Element>, callback: any): void;
export declare function useGlobalOnEscapeKey(props: { callback: (event: KeyboardEvent) => void; when: boolean; priority: [number, number] }): void;
/**
* Custom hook to use display order of component of one and the same group
* @param {string} group
* @param {boolean} [isVisible]
*/
export declare function useDisplayOrder(group: string, isVisible?: boolean): number | undefined;
35 changes: 35 additions & 0 deletions components/lib/hooks/useDisplayOrder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as React from 'react';
import { UniqueComponentId } from '../utils/Utils';

const groupToDisplayedElements = {};

export const useDisplayOrder = (group, isVisible = true) => {
const [uid] = React.useState(() => UniqueComponentId());

const [displayOrder, setDisplayOrder] = React.useState();

React.useEffect(() => {
if (isVisible) {
if (!(group in groupToDisplayedElements)) {
groupToDisplayedElements[group] = [];
}

const newDisplayOrder = groupToDisplayedElements[group].length;

groupToDisplayedElements[group].push(uid);
setDisplayOrder(newDisplayOrder);

return () => {
delete groupToDisplayedElements[group][newDisplayOrder];
const lastOrder = groupToDisplayedElements[group].findLastIndex((el) => el !== undefined);

// Reduce array length, by removing undefined elements at the end of array:
groupToDisplayedElements[group].splice(lastOrder + 1);

setDisplayOrder(undefined);
};
}
}, [group, uid, isVisible]);

return displayOrder;
};
110 changes: 110 additions & 0 deletions components/lib/hooks/useGlobalOnEscapeKey.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { DomHandler } from '../utils/Utils';
import { useEffect } from 'react';

/**
* Priorities of different components (bigger number handled first)
*/
export const ESC_KEY_HANDLING_PRIORITIES = {
SIDEBAR: 100,
SLIDE_MENU: 200,
DIALOG: 300,
IMAGE: 400,
MENU: 500,
OVERLAY_PANEL: 600,
PASSWORD: 700,
CASCADE_SELECT: 800,
SPLIT_BUTTON: 900,
SPEED_DIAL: 1000
};

/**
* Object, that manages global escape key handling logic
*/
const globalEscKeyHandlingLogic = {
/**
* Mapping from ESC_KEY_HANDLING_PRIORITY to array of related listeners, grouped by priority
* @example
* Map<{
* [ESC_KEY_HANDLING_PRIORITIES.SIDEBAR]: Map<{
* 1: () => {...},
* 2: () => {...}
* }>,
* [ESC_KEY_HANDLING_PRIORITIES.DIALOG]: Map<{
* 1: () => {...},
* 2: () => {...}
* }>
* }>;
*/
escKeyListeners: new Map(),

/**
* Keydown handler (attached to any keydown)
*/
onGlobalKeyDown(event) {
// Do nothing if not an "esc" key is pressed:
if (event.key !== 'Esc' && event.key !== 'Escape') return;

const escKeyListeners = globalEscKeyHandlingLogic.escKeyListeners;
const maxPrimaryPriority = Math.max(...escKeyListeners.keys());

const theMostImportantEscHandlersSet = escKeyListeners.get(maxPrimaryPriority);

const maxSecondaryPriority = Math.max(...theMostImportantEscHandlersSet.keys());
const theMostImportantEscHandler = theMostImportantEscHandlersSet.get(maxSecondaryPriority);

theMostImportantEscHandler(event);
},

/**
* Attach global keydown listener if there are any "esc" key handlers assigned,
* otherwise detach.
*/
refreshGlobalKeyDownListener() {
const document = DomHandler.getTargetElement('document');

if (this.escKeyListeners.size > 0) {
document.addEventListener('keydown', this.onGlobalKeyDown);
} else {
document.removeEventListener('keydown', this.onGlobalKeyDown);
}
},

/**
* Add "Esc" key handler
*/
addListener(callback, [primaryPriority, secondaryPriority]) {
const escKeyListeners = this.escKeyListeners;

if (!escKeyListeners.has(primaryPriority)) {
escKeyListeners.set(primaryPriority, new Map());
}

const primaryPriorityListeners = escKeyListeners.get(primaryPriority);

// To prevent unexpected override of callback:
if (primaryPriorityListeners.has(secondaryPriority)) {
throw new Error(`Unexpected: global esc key listener with priority [${primaryPriority}, ${secondaryPriority}] already exists.`);
}

primaryPriorityListeners.set(secondaryPriority, callback);
this.refreshGlobalKeyDownListener();

return () => {
primaryPriorityListeners.delete(secondaryPriority);

if (primaryPriorityListeners.size === 0) {
escKeyListeners.delete(primaryPriority);
}

this.refreshGlobalKeyDownListener();
};
}
};

export const useGlobalOnEscapeKey = ({ callback, when, priority }) => {
useEffect(() => {
if (!when) return;

return globalEscKeyHandlingLogic.addListener(callback, priority);
}, [when, callback, priority]);
};
Loading

0 comments on commit 11c9a6b

Please sign in to comment.