@syncfusion/react-popups
Version:
A package of React popup components such as Tooltip that is used to display information or messages in separate pop-ups.
448 lines (447 loc) • 21.2 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import * as React from 'react';
import { forwardRef, useRef, useImperativeHandle, useCallback, useState, useEffect, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { Popup, getZindexPartial } from '../popup/popup';
import { Button, Color, Variant } from '@syncfusion/react-buttons';
import { L10n, preRender, useProviderContext, useDraggable, getUniqueID } from '@syncfusion/react-base';
import { useResize } from '../common/resize';
import { ResizerRightIcon, CloseIcon } from '@syncfusion/react-icons';
const DIALOG_VIEWPORT_MARGIN = 20;
/**
* Specifies a Dialog component that provides a modal or non-modal overlay to display content above the main interface.
*
* The Dialog component can be used to create alerts, confirmation dialogs, forms, or any content that requires
* user attention or interaction. It supports multiple customization options including positioning, animation effects,
* header/footer structure, and accessibility features.
*
* Use header and footer props to create a structured dialog layout:
*
* ```typescript
* import { Dialog } from "@syncfusion/react-popups";
*
* const [isOpen, setIsOpen] = useState(false);
*
* <Dialog open={isOpen} onClose={() => setIsOpen(false)} modal={true} header="Dialog Title" footer={<><button onClick={() => setIsOpen(false)}>Close</button></>} >
* <p>This is the dialog content.</p>
* </Dialog>
* ```
*/
export const Dialog = forwardRef((props, ref) => {
const { open, header, footer, modal = true, onClose, onDragStart, onDrag, onDragStop, onResizeStart, onResize, onResizeStop, closeIcon = true, initialFocusRef, draggable = false, resizable = false, resizeHandles = ['SouthEast'], position = 'Center', animation = { effect: 'Fade', duration: 400, delay: 0 }, target = typeof document !== 'undefined' ? document.body : undefined, fullScreen = false, className = '', children, style, id = getUniqueID('dialog'), ...restProps } = props;
const dialogElementRef = useRef(null);
const closeButtonRef = useRef(null);
const previousFocusElementRef = useRef(null);
const [dialogState, setDialogState] = useState({ internalOpen: open, dynamicMaxHeight: '' });
const { internalOpen, dynamicMaxHeight } = dialogState;
const { locale } = useProviderContext();
const draggableOptions = useMemo(() => ({
handle: '.sf-dlg-header-content',
clone: false,
dragArea: target && target instanceof HTMLElement ? target : (typeof document !== 'undefined' ? document.body : undefined),
abort: '.sf-dlg-closeicon-btn',
isDragScroll: true,
dragStart: (args) => {
if (!draggable) {
args.cancel = true;
return;
}
if (onDragStart) {
onDragStart(args);
}
},
drag: (args) => {
if (!draggable) {
return;
}
if (onDrag) {
onDrag(args.event);
}
},
dragStop: (args) => {
if (!draggable) {
return;
}
if (onDragStop) {
onDragStop(args.event);
}
}
}), [draggable, target, onDragStart, onDrag, onDragStop]);
const stableRef = useRef(null);
const draggableRef = useRef(useDraggable({ current: (draggable ? stableRef.current : null) }, draggableOptions));
const styleConstraints = useMemo(() => {
const parseDimensionValue = (value, defaultValue) => {
if (value === undefined) {
return defaultValue;
}
if (typeof value === 'number') {
return value;
}
return value;
};
const defaultMaxWidth = fullScreen && typeof window !== 'undefined' ? window.innerWidth : typeof window !== 'undefined' ? window.innerWidth - DIALOG_VIEWPORT_MARGIN * 2 : 800;
const defaultMaxHeight = fullScreen && typeof window !== 'undefined' ? window.innerHeight : typeof window !== 'undefined' ? window.innerHeight - DIALOG_VIEWPORT_MARGIN * 2 : 600;
return {
minWidth: parseDimensionValue(style?.minWidth, 100),
minHeight: parseDimensionValue(style?.minHeight, 100),
maxWidth: parseDimensionValue(style?.maxWidth, defaultMaxWidth),
maxHeight: parseDimensionValue(style?.maxHeight, defaultMaxHeight)
};
}, [style?.minWidth, style?.minHeight, style?.maxWidth, style?.maxHeight, fullScreen]);
const resizeOptions = useMemo(() => ({
enabled: resizable && !fullScreen,
handles: resizeHandles,
boundary: target && target instanceof HTMLElement ? target : undefined,
minWidth: styleConstraints.minWidth,
minHeight: styleConstraints.minHeight,
maxWidth: styleConstraints.maxWidth,
maxHeight: styleConstraints.maxHeight,
onResizeStart: onResizeStart,
onResize: onResize,
onResizeStop: onResizeStop
}), [resizable, resizeHandles, target, styleConstraints]);
const { renderResizeHandles } = useResize(stableRef, resizeOptions);
useEffect(() => {
preRender('dialog');
return () => {
if (draggableRef.current && draggableRef.current.element?.current) {
draggableRef.current.destroy?.();
}
stableRef.current = null;
draggableRef.current = null;
dialogElementRef.current = null;
closeButtonRef.current = null;
previousFocusElementRef.current = null;
if (typeof window !== 'undefined') {
window.removeEventListener('resize', handleResize);
}
setDialogState({
internalOpen: false,
dynamicMaxHeight: ''
});
};
}, []);
const getFocusableElements = useCallback(() => {
let currentElements = [];
if (stableRef.current) {
const selector = ['a[href]:not([tabindex="-1"])', 'button:not([disabled]):not([tabindex="-1"])', 'input:not([disabled]):not([tabindex="-1"])', 'select:not([disabled]):not([tabindex="-1"])', 'textarea:not([disabled]):not([tabindex="-1"])', 'details:not([tabindex="-1"])', '[contenteditable]:not([tabindex="-1"])', '[tabindex]:not([tabindex="-1"])'
].join(',');
const elements = stableRef.current.querySelectorAll(selector);
currentElements = Array.from(elements).filter((el) => !el.hasAttribute('disabled'));
}
return currentElements;
}, [stableRef, children, open]);
useEffect(() => {
if (open) {
if (typeof document === 'undefined' || typeof window === 'undefined') {
return;
}
previousFocusElementRef.current = document.activeElement;
if (modal) {
const currentTarget = target && target instanceof HTMLElement ? target : document.body;
const originalStyles = {
overflow: currentTarget.style.overflow,
paddingRight: currentTarget.style.paddingRight
};
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
currentTarget.style.overflow = 'hidden';
currentTarget.style.paddingRight = `${scrollbarWidth}px`;
return () => {
currentTarget.style.overflow = originalStyles.overflow;
currentTarget.style.paddingRight = originalStyles.paddingRight;
if (previousFocusElementRef.current && open) {
previousFocusElementRef.current.focus();
}
};
}
}
else if (previousFocusElementRef.current) {
previousFocusElementRef.current.focus();
previousFocusElementRef.current = null;
}
return () => {
if (previousFocusElementRef.current && open) {
previousFocusElementRef.current.focus();
}
};
}, [internalOpen, modal]);
useEffect(() => {
if (open) {
setDialogState((prevState) => ({
...prevState,
internalOpen: true
}));
}
}, [open]);
const calculateMaxHeight = useCallback(() => {
if (fullScreen) {
return '100%';
}
if (target && target !== document.body && typeof window !== 'undefined') {
return (target.offsetHeight < window.innerHeight) ? (target.offsetHeight - DIALOG_VIEWPORT_MARGIN) + 'px' : (window.innerHeight - DIALOG_VIEWPORT_MARGIN) + 'px';
}
return (typeof window !== 'undefined' ? (window.innerHeight - DIALOG_VIEWPORT_MARGIN) : 600) + 'px';
}, [fullScreen, target]);
useEffect(() => {
if (open) {
setDialogState((prevState) => ({
...prevState,
dynamicMaxHeight: calculateMaxHeight()
}));
}
}, [open, calculateMaxHeight]);
const handleResize = useCallback(() => {
setDialogState((prevState) => ({
...prevState,
dynamicMaxHeight: calculateMaxHeight()
}));
}, [calculateMaxHeight]);
useEffect(() => {
if (!open || !internalOpen) {
return;
}
if (dialogElementRef.current) {
setDialogState((prevState) => ({
...prevState,
dynamicMaxHeight: calculateMaxHeight()
}));
}
if (typeof window !== 'undefined') {
window.addEventListener('resize', handleResize);
}
return () => {
if (typeof window !== 'undefined') {
window.removeEventListener('resize', handleResize);
}
};
}, [open, internalOpen, handleResize, calculateMaxHeight]);
const publicAPI = useMemo(() => ({
open,
modal,
draggable,
resizable,
resizeHandles,
position,
animation,
target,
fullScreen,
closeIcon
}), [open, modal, draggable, resizable, resizeHandles, position, animation, target, fullScreen]);
useImperativeHandle(ref, () => ({
...publicAPI,
element: dialogElementRef.current
}), [publicAPI]);
const handleDialogKeyDown = useCallback((e) => {
if (e.key === 'Escape' && onClose) {
e.preventDefault();
onClose(e);
if (typeof document !== 'undefined' && document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}
}, [onClose]);
const handleTabKey = useCallback((e) => {
if (!modal) {
return;
}
const focusableElements = getFocusableElements();
if (focusableElements.length === 0) {
return;
}
switch (e.key) {
case 'Tab':
{
const currentElement = (typeof document !== 'undefined' ? document.activeElement : null);
const currentElementIndex = focusableElements.indexOf(currentElement);
if (currentElementIndex === -1) {
e.preventDefault();
focusableElements[focusableElements.length - 1]?.focus?.();
return;
}
e.preventDefault();
let nextIndex;
if (e.shiftKey) {
nextIndex = currentElementIndex <= 0 ? focusableElements.length - 1 : currentElementIndex - 1;
}
else {
nextIndex = currentElementIndex >= focusableElements.length - 1 ? 0 : currentElementIndex + 1;
}
focusableElements[nextIndex]?.focus?.();
break;
}
}
}, [modal, getFocusableElements]);
const handlePopupOpen = useCallback(() => {
if (dialogElementRef.current) {
setDialogState((prevState) => ({
...prevState,
dynamicMaxHeight: calculateMaxHeight()
}));
}
if (initialFocusRef?.current) {
if ('element' in initialFocusRef.current && initialFocusRef.current.element) {
initialFocusRef.current.element.focus();
}
else {
initialFocusRef.current.focus();
}
}
else {
const elements = getFocusableElements();
const elementsWithoutCloseButton = elements.filter((el) => { return (!closeButtonRef.current || (closeButtonRef.current && el !== closeButtonRef.current.element)); });
if (elementsWithoutCloseButton.length > 0) {
elementsWithoutCloseButton[0].focus();
}
else if (closeButtonRef.current?.element) {
closeButtonRef.current.element.focus();
}
}
}, [initialFocusRef, calculateMaxHeight, getFocusableElements]);
const handlePopupClose = useCallback(() => {
if (dialogElementRef.current) {
setDialogState((prevState) => ({
...prevState,
internalOpen: false
}));
}
}, []);
const handleCloseIconClick = useCallback((e) => {
onClose?.(e);
}, [onClose]);
const handleOverlayClick = useCallback((event) => {
if (modal && event.target === event.currentTarget) {
onClose?.(event);
}
}, [modal, onClose]);
const closeIconTitle = useMemo(() => {
const l10n = L10n('dialog', { close: 'Close' }, locale);
return l10n.getConstant('close');
}, [locale]);
const renderCloseIcon = useCallback(() => {
return (_jsx(Button, { ref: closeButtonRef, icon: typeof closeIcon === 'boolean' ? _jsx(CloseIcon, { "aria-label": closeIconTitle }) : closeIcon, title: closeIconTitle, "aria-label": closeIconTitle, color: Color.Secondary, onClick: handleCloseIconClick, className: "sf-dlg-closeicon-btn", variant: Variant.Standard }));
}, [closeIcon, handleCloseIconClick, modal, closeIconTitle]);
const mapDialogEffectToPopupAnimation = (effect, isShow) => {
const animationMappings = {
'Fade': { show: 'FadeIn', hide: 'FadeOut' },
'FadeZoom': { show: 'FadeZoomIn', hide: 'FadeZoomOut' },
'FlipLeftDown': { show: 'FlipLeftDownIn', hide: 'FlipLeftDownOut' },
'FlipLeftUp': { show: 'FlipLeftUpIn', hide: 'FlipLeftUpOut' },
'FlipRightDown': { show: 'FlipRightDownIn', hide: 'FlipRightDownOut' },
'FlipRightUp': { show: 'FlipRightUpIn', hide: 'FlipRightUpOut' },
'FlipXDown': { show: 'FlipXDownIn', hide: 'FlipXDownOut' },
'FlipXUp': { show: 'FlipXUpIn', hide: 'FlipXUpOut' },
'FlipYLeft': { show: 'FlipYLeftIn', hide: 'FlipYLeftOut' },
'FlipYRight': { show: 'FlipYRightIn', hide: 'FlipYRightOut' },
'SlideBottom': { show: 'SlideBottomIn', hide: 'SlideBottomOut' },
'SlideLeft': { show: 'SlideLeftIn', hide: 'SlideLeftOut' },
'SlideRight': { show: 'SlideRightIn', hide: 'SlideRightOut' },
'SlideTop': { show: 'SlideTopIn', hide: 'SlideTopOut' },
'Zoom': { show: 'ZoomIn', hide: 'ZoomOut' },
'None': { show: 'FadeIn', hide: 'FadeOut' }
};
const defaultAnimation = {
show: 'FadeIn',
hide: 'FadeOut'
};
const mapping = animationMappings[effect] || defaultAnimation;
return isShow ? mapping.show : mapping.hide;
};
const dialogClasses = useMemo(() => {
return [
'sf-control',
'sf-dialog',
modal ? 'sf-dlg-modal' : '',
fullScreen ? 'sf-dlg-fullscreen' : '',
resizable && !fullScreen ? 'sf-dlg-resizable' : '',
className
].filter(Boolean).join(' ');
}, [modal, fullScreen, className, resizable]);
const positionClassName = useMemo(() => {
if (style?.top && style?.left || position === null) {
return;
}
const [X, Y] = position?.split(/(?=[A-Z])/);
if (X && Y) {
return `sf-dlg-${X?.toLowerCase()}-${Y?.toLowerCase()}`;
}
return 'sf-dlg-center';
}, [position, style]);
const popupPosition = useMemo(() => {
const [X, Y] = position ? position?.split(/(?=[A-Z])/) : [];
if (X && Y) {
return { X: X, Y: Y };
}
return { X: 'Center', Y: 'Center' };
}, [position]);
const dialogContainerClasses = useMemo(() => {
return [
'sf-dlg-container',
positionClassName
].filter(Boolean).join(' ');
}, [positionClassName]);
const calculatedStyle = useMemo(() => {
const styleWithoutZIndex = { ...style };
delete styleWithoutZIndex.zIndex;
return {
maxHeight: fullScreen ? '100%' : dynamicMaxHeight,
position: styleWithoutZIndex.left && styleWithoutZIndex.top ? 'absolute' : 'relative',
...((!styleWithoutZIndex.left && !styleWithoutZIndex.top) ? {
top: '0px',
left: '0px'
} : {}),
...(fullScreen ? {
width: '100vw',
height: '100vh',
maxWidth: '100%',
maxHeight: '100%'
} : {}),
...(!fullScreen && resizable ? {
minWidth: styleConstraints.minWidth,
minHeight: styleConstraints.minHeight,
maxWidth: styleConstraints.maxWidth,
maxHeight: styleConstraints.maxHeight
} : {}),
...styleWithoutZIndex
};
}, [position, fullScreen, style, dynamicMaxHeight, resizable, styleConstraints]);
const { zIndexBase, zIndexPopup, zIndexOverlay } = useMemo(() => {
let baseValue = typeof style?.zIndex === 'number' ? style.zIndex : 1000;
if (baseValue === 1000 && dialogElementRef.current) {
baseValue = getZindexPartial(dialogElementRef.current);
}
return {
zIndexBase: Math.max(2, baseValue),
zIndexPopup: Math.max(3, baseValue + 1),
zIndexOverlay: Math.max(1, baseValue - 1)
};
}, [style?.zIndex, open, internalOpen, dialogElementRef.current]);
if (!internalOpen) {
return null;
}
const dialogContent = (_jsxs("div", { className: dialogContainerClasses, ref: dialogElementRef, style: {
zIndex: zIndexBase,
position: typeof document !== 'undefined' && target !== document.body ? 'absolute' : 'fixed'
}, onKeyDown: handleDialogKeyDown, children: [_jsxs(Popup, { ref: (el) => {
stableRef.current = el?.element;
}, open: open, position: popupPosition, zIndex: zIndexPopup, relateTo: target, onOpen: handlePopupOpen, onClose: handlePopupClose, className: dialogClasses, animation: {
show: {
name: mapDialogEffectToPopupAnimation(animation.effect, true),
duration: animation.effect === 'None' ? 0 : animation.duration,
delay: animation.delay,
timingFunction: 'ease-out'
},
hide: {
name: mapDialogEffectToPopupAnimation(animation.effect, false),
duration: animation.effect === 'None' ? 0 : animation.duration,
delay: animation.delay,
timingFunction: 'ease-in'
}
}, style: calculatedStyle, role: "dialog", id: id, ...(children && { 'aria-describedby': `${id}_dialog-content` }), ...(header && { 'aria-labelledby': `${id}_dialog-header` }), ...(!header && { 'aria-label': 'Dialog' }), "aria-modal": modal ? 'true' : 'false', tabIndex: -1, onKeyDown: handleTabKey, ...restProps, children: [(header || closeIcon) && (_jsxs("div", { className: "sf-dlg-header-content", id: `${id}_dialog-header`, children: [closeIcon && renderCloseIcon(), header && (_jsx("div", { className: "sf-dlg-header sf-ellipsis", id: `${id}_title`, children: header }))] })), _jsx("div", { className: "sf-dlg-content", id: `${id}_dialog-content`, children: children }), footer && (_jsx("div", { className: "sf-dlg-footer-content sf-content-end", children: footer })), (resizable && !fullScreen) &&
renderResizeHandles(_jsx(ResizerRightIcon, { fill: 'currentColor' }))] }), modal && (_jsx("div", { className: "sf-dlg-overlay", style: {
zIndex: zIndexOverlay
}, onClick: handleOverlayClick, role: "presentation" }))] }));
const portalTarget = target && target instanceof HTMLElement ? target : (typeof document !== 'undefined' ? document.body : undefined);
return createPortal(dialogContent, portalTarget);
});
Dialog.displayName = 'Dialog';
export default React.memo(Dialog);