UNPKG

@base-ui-components/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.

158 lines (154 loc) 6.35 kB
'use client'; import _formatErrorMessage from "@base-ui-components/utils/formatErrorMessage"; import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { useStableCallback } from '@base-ui-components/utils/useStableCallback'; import { usePopoverRootContext } from "../root/PopoverRootContext.js"; import { useButton } from "../../use-button/useButton.js"; import { triggerOpenStateMapping, pressableTriggerOpenStateMapping } from "../../utils/popupStateMapping.js"; import { useRenderElement } from "../../utils/useRenderElement.js"; import { CLICK_TRIGGER_IDENTIFIER } from "../../utils/constants.js"; import { safePolygon, useClick, useHoverReferenceInteraction, useInteractions } from "../../floating-ui-react/index.js"; import { OPEN_DELAY } from "../utils/constants.js"; import { useBaseUiId } from "../../utils/useBaseUiId.js"; import { FocusGuard } from "../../utils/FocusGuard.js"; import { contains, getNextTabbable, getTabbableAfterElement, getTabbableBeforeElement, isOutsideEvent } from "../../floating-ui-react/utils.js"; import { createChangeEventDetails } from "../../utils/createBaseUIEventDetails.js"; import { REASONS } from "../../utils/reasons.js"; import { useTriggerDataForwarding } from "../../utils/popups/index.js"; /** * A button that opens the popover. * Renders a `<button>` element. * * Documentation: [Base UI Popover](https://base-ui.com/react/components/popover) */ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; export const PopoverTrigger = /*#__PURE__*/React.forwardRef(function PopoverTrigger(componentProps, forwardedRef) { const { render, className, disabled = false, nativeButton = true, handle, payload, openOnHover = false, delay = OPEN_DELAY, closeDelay = 0, id: idProp, ...elementProps } = componentProps; const rootContext = usePopoverRootContext(true); const store = handle?.store ?? rootContext?.store; if (!store) { throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: <Popover.Trigger> must be either used within a <Popover.Root> component or provided with a handle.' : _formatErrorMessage(74)); } const thisTriggerId = useBaseUiId(idProp); const isTriggerActive = store.useState('isTriggerActive', thisTriggerId); const floatingContext = store.useState('floatingRootContext'); const isOpenedByThisTrigger = store.useState('isOpenedByTrigger', thisTriggerId); const [triggerElement, setTriggerElement] = React.useState(null); const { registerTrigger, isMountedByThisTrigger } = useTriggerDataForwarding(thisTriggerId, triggerElement, store, { payload, disabled, openOnHover, closeDelay }); const openReason = store.useState('openChangeReason'); const stickIfOpen = store.useState('stickIfOpen'); const openMethod = store.useState('openMethod'); const hoverProps = useHoverReferenceInteraction(floatingContext, { enabled: floatingContext != null && openOnHover && (openMethod !== 'touch' || openReason !== REASONS.triggerPress), mouseOnly: true, move: false, handleClose: safePolygon(), restMs: delay, delay: { close: closeDelay }, triggerElement, isActiveTrigger: isTriggerActive }); const click = useClick(floatingContext, { enabled: floatingContext != null, stickIfOpen }); const localProps = useInteractions([click]); const rootTriggerProps = store.useState('triggerProps', isMountedByThisTrigger); const state = React.useMemo(() => ({ disabled, open: isOpenedByThisTrigger }), [disabled, isOpenedByThisTrigger]); const { getButtonProps, buttonRef } = useButton({ disabled, native: nativeButton }); const stateAttributesMapping = React.useMemo(() => ({ open(value) { if (value && openReason === REASONS.triggerPress) { return pressableTriggerOpenStateMapping.open(value); } return triggerOpenStateMapping.open(value); } }), [openReason]); const element = useRenderElement('button', componentProps, { state, ref: [buttonRef, forwardedRef, registerTrigger, setTriggerElement], props: [localProps.getReferenceProps(), hoverProps, rootTriggerProps, { [CLICK_TRIGGER_IDENTIFIER]: '', id: thisTriggerId }, elementProps, getButtonProps], stateAttributesMapping }); const preFocusGuardRef = React.useRef(null); const handlePreFocusGuardFocus = useStableCallback(event => { ReactDOM.flushSync(() => { store.setOpen(false, createChangeEventDetails(REASONS.focusOut, event.nativeEvent, event.currentTarget)); }); const previousTabbable = getTabbableBeforeElement(preFocusGuardRef.current); previousTabbable?.focus(); }); const handleFocusTargetFocus = useStableCallback(event => { const positionerElement = store.select('positionerElement'); if (positionerElement && isOutsideEvent(event, positionerElement)) { store.context.beforeContentFocusGuardRef.current?.focus(); } else { ReactDOM.flushSync(() => { store.setOpen(false, createChangeEventDetails(REASONS.focusOut, event.nativeEvent, event.currentTarget)); }); let nextTabbable = getTabbableAfterElement(triggerElement); while (nextTabbable !== null && contains(positionerElement, nextTabbable) || nextTabbable?.hasAttribute('aria-hidden')) { const prevTabbable = nextTabbable; nextTabbable = getNextTabbable(nextTabbable); if (nextTabbable === prevTabbable) { break; } } nextTabbable?.focus(); } }); // A fragment with key is required to ensure that the `element` is mounted to the same DOM node // regardless of whether the focus guards are rendered or not. if (isTriggerActive) { return /*#__PURE__*/_jsxs(React.Fragment, { children: [/*#__PURE__*/_jsx(FocusGuard, { ref: preFocusGuardRef, onFocus: handlePreFocusGuardFocus }), /*#__PURE__*/_jsx(React.Fragment, { children: element }, thisTriggerId), /*#__PURE__*/_jsx(FocusGuard, { ref: store.context.triggerFocusTargetRef, onFocus: handleFocusTargetFocus })] }); } return /*#__PURE__*/_jsx(React.Fragment, { children: element }, thisTriggerId); }); if (process.env.NODE_ENV !== "production") PopoverTrigger.displayName = "PopoverTrigger";