@gravity-ui/uikit
Version:
Gravity UI base styling and components
118 lines (117 loc) • 6.36 kB
JavaScript
'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