@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.
141 lines (137 loc) • 6.04 kB
JavaScript
;
'use client';
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.PopoverStore = void 0;
exports.createNullPopoverStore = createNullPopoverStore;
var React = _interopRequireWildcard(require("react"));
var ReactDOM = _interopRequireWildcard(require("react-dom"));
var _store = require("@base-ui/utils/store");
var _useTimeout = require("@base-ui/utils/useTimeout");
var _empty = require("@base-ui/utils/empty");
var _reasons = require("../../internals/reasons");
var _NullStore = require("../../utils/NullStore");
var _popups = require("../../utils/popups");
var _constants = require("../../internals/constants");
const selectors = {
..._popups.popupStoreSelectors,
disabled: state => state.disabled,
instantType: state => state.instantType,
openMethod: state => state.openMethod,
openChangeReason: state => state.openChangeReason,
modal: state => state.modal,
focusManagerModal: state => state.focusManagerModal,
stickIfOpen: state => state.stickIfOpen,
titleElementId: state => state.titleElementId,
descriptionElementId: state => state.descriptionElementId,
openOnHover: state => state.openOnHover,
closeDelay: state => state.closeDelay,
adaptiveOrigin: state => state.adaptiveOrigin
};
/**
* The store view that detached handle-backed triggers read from. Both the real `PopoverStore` and
* the inert fallback store satisfy it, so a trigger can read from whichever store the handle
* currently exposes. Narrowed to the members a trigger actually uses — the trigger-data members plus
* `setOpen` (called by the focus guards) — so the exposed surface can't bypass the open-change
* pipeline; on the detached fallback store every one of these mutations is a no-op.
*/
class PopoverStore extends _store.ReactStore {
constructor(initialState, floatingId, nested) {
const triggerElements = new _popups.PopupTriggerMap();
super(createInitialState(initialState, triggerElements, floatingId, nested), createInitialContext(triggerElements), selectors);
}
setOpen = (nextOpen, eventDetails) => {
const isHover = eventDetails.reason === _reasons.REASONS.triggerHover;
const isKeyboardClick = eventDetails.reason === _reasons.REASONS.triggerPress && eventDetails.event.detail === 0;
const isDismissClose = !nextOpen && (eventDetails.reason === _reasons.REASONS.escapeKey || eventDetails.reason == null);
const shouldPreventUnmountOnClose = (0, _popups.attachPreventUnmountOnClose)(eventDetails);
const activeTriggerId = this.select('activeTriggerId');
if (!nextOpen && eventDetails.reason === _reasons.REASONS.closePress && eventDetails.trigger == null && activeTriggerId != null) {
eventDetails.trigger = this.context.triggerElements.getById(activeTriggerId) ?? this.select('activeTriggerElement') ?? undefined;
}
this.context.onOpenChange?.(nextOpen, eventDetails);
if (eventDetails.isCanceled) {
return;
}
this.state.floatingRootContext.dispatchOpenChange(nextOpen, eventDetails);
const changeState = () => {
const updatedState = {
open: nextOpen,
openChangeReason: eventDetails.reason
};
(0, _popups.setPopupOpenState)(updatedState, nextOpen, eventDetails.trigger, shouldPreventUnmountOnClose());
this.update(updatedState);
};
if (isHover) {
// Only allow "patient" clicks to close the popover if it's open.
// If they clicked within 500ms of the popover opening, keep it open.
this.set('stickIfOpen', true);
this.context.stickIfOpenTimeout.start(_constants.PATIENT_CLICK_THRESHOLD, () => {
this.set('stickIfOpen', false);
});
ReactDOM.flushSync(changeState);
} else {
changeState();
}
let instantType;
if (isKeyboardClick) {
instantType = 'click';
} else if (isDismissClose) {
instantType = 'dismiss';
} else if (eventDetails.reason === _reasons.REASONS.focusOut) {
instantType = 'focus';
}
this.set('instantType', instantType);
};
}
/**
* Creates the inert fallback store used by detached handle-backed triggers while no
* `Popover.Root` is attached. It preserves a popover-specific trigger registry in context so
* detached triggers can register before migrating to the live root store. `setOpen` is a no-op
* (matching the inert reads/writes of `NullStore`), so a trigger can hand the store to focus-guard
* helpers that expect `setOpen` without it ever taking effect while detached.
*/
exports.PopoverStore = PopoverStore;
function createNullPopoverStore() {
const triggerElements = new _popups.PopupTriggerMap();
const store = new _NullStore.NullStore(Object.freeze(createInitialState(undefined, triggerElements)), Object.freeze(createInitialContext(triggerElements)), selectors);
return Object.assign(store, {
setOpen: _empty.NOOP
});
}
function createInitialState(initialState, triggerElements, floatingId, nested = false) {
const state = {
...(0, _popups.createInitialPopupStoreState)(),
disabled: false,
modal: false,
focusManagerModal: false,
instantType: undefined,
openMethod: null,
openChangeReason: null,
titleElementId: undefined,
descriptionElementId: undefined,
stickIfOpen: true,
openOnHover: false,
closeDelay: 0,
adaptiveOrigin: undefined,
...initialState
};
if (state.open && initialState?.mounted === undefined) {
state.mounted = true;
}
state.floatingRootContext = (0, _popups.createPopupFloatingRootContext)(triggerElements, floatingId, nested);
return state;
}
function createInitialContext(triggerElements) {
return {
popupRef: /*#__PURE__*/React.createRef(),
onOpenChange: undefined,
onOpenChangeComplete: undefined,
triggerFocusTargetRef: /*#__PURE__*/React.createRef(),
beforeContentFocusGuardRef: /*#__PURE__*/React.createRef(),
stickIfOpenTimeout: new _useTimeout.Timeout(),
triggerElements
};
}