UNPKG

@base-ui/react

Version:

Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.

109 lines (105 loc) 4.57 kB
'use client'; import * as React from 'react'; import { useIsoLayoutEffect } from '@base-ui/utils/useIsoLayoutEffect'; import { useScrollLock } from '@base-ui/utils/useScrollLock'; import { useDismiss } from "../../floating-ui-react/index.mjs"; import { contains, getTarget } from "../../floating-ui-react/utils.mjs"; import { usePopupInteractionProps } from "../../utils/popups/index.mjs"; export function DialogInteractions({ store, parentContext, isDrawer }) { const open = store.useState('open'); const disablePointerDismissal = store.useState('disablePointerDismissal'); const modal = store.useState('modal'); const popupElement = store.useState('popupElement'); const floatingRootContext = store.useState('floatingRootContext'); const [ownNestedOpenDialogs, setOwnNestedOpenDialogs] = React.useState(0); const [ownNestedOpenDrawers, setOwnNestedOpenDrawers] = React.useState(0); const isTopmost = ownNestedOpenDialogs === 0; const dismiss = useDismiss(floatingRootContext, { outsidePressEvent() { if (store.context.internalBackdropRef.current || store.context.backdropRef.current) { return 'intentional'; } // Ensure `aria-hidden` on outside elements is removed immediately // on outside press when trapping focus. return { mouse: modal === 'trap-focus' ? 'sloppy' : 'intentional', touch: 'sloppy' }; }, outsidePress(event) { if (!store.context.outsidePressEnabledRef.current) { return false; } // For mouse events, only accept left button (button 0) // For touch events, a single touch is equivalent to left button if ('button' in event && event.button !== 0) { return false; } if ('touches' in event) { // Outside press can be handled on `touchend`, where the lifted point is // reported in `changedTouches` and `touches` contains any remaining // active points. Treat it as a single-finger tap only when exactly one // touch ended and no other fingers are still down. if (event.type === 'touchend') { if (event.changedTouches.length !== 1 || event.touches.length !== 0) { return false; } } else if (event.touches.length !== 1) { return false; } } const target = getTarget(event); if (isTopmost && !disablePointerDismissal) { // Only close if the click occurred on the dialog's owning backdrop. // This supports multiple modal dialogs that aren't nested in the React tree: // https://github.com/mui/base-ui/issues/1320 if (modal) { const internalBackdrop = store.context.internalBackdropRef.current; const backdrop = store.context.backdropRef.current; return internalBackdrop || backdrop ? internalBackdrop === target || backdrop === target || contains(target, popupElement) && !target?.hasAttribute('data-base-ui-portal') : true; } return true; } return false; }, escapeKey: isTopmost }); useScrollLock(open && modal === true, popupElement); // Listen for nested open/close events on this store to maintain the counts. // A close notification is an open notification with zeroed counts. store.useContextCallback('onNestedDialogOpen', (dialogCount, drawerCount) => { setOwnNestedOpenDialogs(dialogCount); setOwnNestedOpenDrawers(drawerCount); }); // Notify parent of our open/close state using parent callbacks, if any useIsoLayoutEffect(() => { if (parentContext?.onNestedDialogOpen) { if (open) { parentContext.onNestedDialogOpen(ownNestedOpenDialogs + 1, ownNestedOpenDrawers + (isDrawer ? 1 : 0)); } else { parentContext.onNestedDialogOpen(0, 0); } } return () => { if (parentContext?.onNestedDialogOpen && open) { parentContext.onNestedDialogOpen(0, 0); } }; }, [isDrawer, open, ownNestedOpenDialogs, ownNestedOpenDrawers, parentContext]); usePopupInteractionProps(store, { // `enabled` is not passed to `useDismiss`, so its props are always defined, // and `trigger` is the same object as `reference`. activeTriggerProps: dismiss.reference, inactiveTriggerProps: dismiss.trigger, // DialogPopup and DrawerPopup spread `FOCUSABLE_POPUP_PROPS` directly, so // this only needs to carry the dismiss handlers. popupProps: dismiss.floating, nestedOpenDialogCount: ownNestedOpenDialogs, nestedOpenDrawerCount: ownNestedOpenDrawers }); return null; }