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.

89 lines (88 loc) 3.34 kB
import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { createSelector, ReactStore } from '@base-ui-components/utils/store'; import { useRefWithInit } from '@base-ui-components/utils/useRefWithInit'; import { REASONS } from "../../utils/reasons.js"; import { createInitialPopupStoreState, popupStoreSelectors, PopupTriggerMap } from "../../utils/popups/index.js"; const selectors = { ...popupStoreSelectors, disabled: createSelector(state => state.disabled), instantType: createSelector(state => state.instantType), isInstantPhase: createSelector(state => state.isInstantPhase), trackCursorAxis: createSelector(state => state.trackCursorAxis), disableHoverablePopup: createSelector(state => state.disableHoverablePopup), lastOpenChangeReason: createSelector(state => state.openChangeReason), closeDelay: createSelector(state => state.closeDelay) }; export class TooltipStore extends ReactStore { constructor(initialState) { super({ ...createInitialState(), ...initialState }, { popupRef: /*#__PURE__*/React.createRef(), onOpenChange: undefined, onOpenChangeComplete: undefined, triggerElements: new PopupTriggerMap() }, selectors); } setOpen = (nextOpen, eventDetails) => { const reason = eventDetails.reason; const isHover = reason === REASONS.triggerHover; const isFocusOpen = nextOpen && reason === REASONS.triggerFocus; const isDismissClose = !nextOpen && (reason === REASONS.triggerPress || reason === REASONS.escapeKey); eventDetails.preventUnmountOnClose = () => { this.set('preventUnmountingOnClose', true); }; this.context.onOpenChange?.(nextOpen, eventDetails); if (eventDetails.isCanceled) { return; } const changeState = () => { const updatedState = { open: nextOpen, openChangeReason: reason }; if (isFocusOpen) { updatedState.instantType = 'focus'; } else if (isDismissClose) { updatedState.instantType = 'dismiss'; } else if (reason === REASONS.triggerHover) { updatedState.instantType = undefined; } // If a popup is closing, the `trigger` may be null. // We want to keep the previous value so that exit animations are played and focus is returned correctly. const newTriggerId = eventDetails.trigger?.id ?? null; if (newTriggerId || nextOpen) { updatedState.activeTriggerId = newTriggerId; updatedState.activeTriggerElement = eventDetails.trigger ?? null; } this.update(updatedState); }; if (isHover) { // If a hover reason is provided, we need to flush the state synchronously. This ensures // `node.getAnimations()` knows about the new state. ReactDOM.flushSync(changeState); } else { changeState(); } }; static useStore(externalStore, initialState) { // eslint-disable-next-line react-hooks/rules-of-hooks return useRefWithInit(() => { return externalStore ?? new TooltipStore(initialState); }).current; } } function createInitialState() { return { ...createInitialPopupStoreState(), disabled: false, instantType: undefined, isInstantPhase: true, trackCursorAxis: 'none', disableHoverablePopup: false, openChangeReason: null, closeDelay: 0 }; }