UNPKG

@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
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);