UNPKG

@gravity-ui/uikit

Version:

Gravity UI base styling and components

118 lines (117 loc) 6.36 kB
'use client'; import { jsx as _jsx } from "react/jsx-runtime"; import * as React from 'react'; import { FloatingFocusManager, FloatingNode, FloatingOverlay, FloatingTree, useDismiss, useFloating, useFloatingNodeId, useFloatingParentNodeId, useInteractions, useRole, useTransitionStatus, } from '@floating-ui/react'; import { isTabbable } from 'tabbable'; import { KeyCode } from "../../constants.js"; import { useForkRef } from "../../hooks/index.js"; import { useAnimateHeight, usePrevious } from "../../hooks/private/index.js"; import { Portal } from "../Portal/index.js"; import { block } from "../utils/cn.js"; import { filterDOMProps } from "../utils/filterDOMProps.js"; import { useLayer } from "../utils/layer-manager/index.js"; import i18n from "./i18n/index.js"; import "./Modal.css"; const b = block('modal'); const TRANSITION_DURATION = 150; function ModalComponent({ open = false, onOpenChange, keepMounted = false, disableBodyScrollLock = false, disableEscapeKeyDown, disableOutsideClick, initialFocus, returnFocus, disableVisuallyHiddenDismiss, onEscapeKeyDown, onOutsideClick, onClose, onEnterKeyDown, onTransitionIn, onTransitionInComplete, onTransitionOut, onTransitionOutComplete, children, style, contentOverflow = 'visible', className, contentClassName, container, qa, floatingRef, disableHeightTransition = false, ...restProps }) { useLayer({ open, type: 'modal' }); const handleOpenChange = React.useCallback((isOpen, event, reason) => { onOpenChange?.(isOpen, event, reason); if (isOpen || !event) { return; } let closeReason; if (reason === 'escape-key') { closeReason = 'escapeKeyDown'; } else if (reason === 'outside-press') { closeReason = 'outsideClick'; } else { closeReason = reason; } if (closeReason === 'escapeKeyDown' && onEscapeKeyDown) { onEscapeKeyDown(event); } if (closeReason === 'outsideClick' && onOutsideClick) { onOutsideClick(event); } onClose?.(event, closeReason); }, [onOpenChange, onEscapeKeyDown, onOutsideClick, onClose]); const floatingNodeId = useFloatingNodeId(); const { refs, elements, context } = useFloating({ nodeId: floatingNodeId, open, onOpenChange: handleOpenChange, }); const handleFloatingRef = useForkRef(refs.setFloating, floatingRef); const dismiss = useDismiss(context, { enabled: !disableOutsideClick || !disableEscapeKeyDown, outsidePress: !disableOutsideClick, escapeKey: !disableEscapeKeyDown, }); const role = useRole(context, { role: 'dialog' }); const { getFloatingProps } = useInteractions([dismiss, role]); const { isMounted, status } = useTransitionStatus(context, { duration: TRANSITION_DURATION }); const previousStatus = usePrevious(status); useAnimateHeight({ ref: refs.floating, enabled: status === 'open' && !disableHeightTransition, }); React.useEffect(() => { if (status === 'initial' && previousStatus === 'unmounted') { onTransitionIn?.(); } if (status === 'close' && previousStatus === 'open') { onTransitionOut?.(); } if (status === 'unmounted' && previousStatus === 'close') { onTransitionOutComplete?.(); } }, [previousStatus, status, onTransitionIn, onTransitionOut, onTransitionOutComplete]); const handleTransitionEnd = React.useCallback((event) => { // There are two simultaneous transitions running at the same time // Use specific name to only notify once if (status === 'open' && event.propertyName === 'transform' && event.target === elements.floating) { onTransitionInComplete?.(); } }, [status, onTransitionInComplete, elements.floating]); const handleKeyDown = React.useCallback((event) => { if (!onEnterKeyDown || event.key !== KeyCode.ENTER || event.defaultPrevented) { return; } const floatingElement = elements.floating; if (!floatingElement) { return; } const pathElements = event.nativeEvent.composedPath(); const index = pathElements.indexOf(floatingElement); const nestedElements = index < 0 ? pathElements : pathElements.slice(0, index); const nestedFloatingElementIndex = nestedElements.findIndex((el) => el?.hasAttribute('data-floating-ui-focusable')); if (nestedFloatingElementIndex < 0) { onEnterKeyDown(event.nativeEvent); return; } const hasInnerTabbableElements = nestedElements .slice(0, nestedFloatingElementIndex) .some((el) => isTabbable(el)); if (!hasInnerTabbableElements) { onEnterKeyDown(event.nativeEvent); } }, [elements.floating, onEnterKeyDown]); return (_jsx(FloatingNode, { id: floatingNodeId, children: isMounted || keepMounted ? (_jsx(Portal, { container: container, children: _jsx(FloatingOverlay, { style: style, className: b({ open }, className), "data-qa": qa, "data-floating-ui-status": status, lockScroll: !disableBodyScrollLock, children: _jsx("div", { className: b('content-aligner'), children: _jsx("div", { className: b('content-wrapper'), children: _jsx(FloatingFocusManager, { context: context, disabled: !isMounted, modal: isMounted, initialFocus: initialFocus ?? refs.floating, returnFocus: returnFocus, visuallyHiddenDismiss: disableVisuallyHiddenDismiss ? false : i18n('close'), restoreFocus: true, children: _jsx("div", { ...filterDOMProps(restProps, { labelable: true }), className: b('content', { 'has-scroll': contentOverflow === 'auto' }, contentClassName), ref: handleFloatingRef, ...getFloatingProps({ onTransitionEnd: handleTransitionEnd, onKeyDown: handleKeyDown, }), children: children }) }) }) }) }) })) : null })); } export function Modal(props) { const parentId = useFloatingParentNodeId(); if (parentId === null) { return (_jsx(FloatingTree, { children: _jsx(ModalComponent, { ...props }) })); } return _jsx(ModalComponent, { ...props }); } //# sourceMappingURL=Modal.js.map