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.

133 lines (131 loc) 5.1 kB
import * as React from 'react'; import { createSelector, ReactStore } from '@base-ui-components/utils/store'; import { EMPTY_OBJECT } from '@base-ui-components/utils/empty'; import { useRefWithInit } from '@base-ui-components/utils/useRefWithInit'; import { FloatingTreeStore } from "../../floating-ui-react/index.js"; import { createInitialPopupStoreState, popupStoreSelectors, PopupTriggerMap } from "../../utils/popups/index.js"; const selectors = { ...popupStoreSelectors, disabled: createSelector(state => state.parent.type === 'menubar' ? state.parent.context.disabled || state.disabled : state.disabled), modal: createSelector(state => (state.parent.type === undefined || state.parent.type === 'context-menu') && (state.modal ?? true)), allowMouseEnter: createSelector(state => state.parent.type === 'menu' ? state.parent.store.select('allowMouseEnter') : state.allowMouseEnter), stickIfOpen: createSelector(state => state.stickIfOpen), parent: createSelector(state => state.parent), rootId: createSelector(state => { if (state.parent.type === 'menu') { return state.parent.store.select('rootId'); } return state.parent.type !== undefined ? state.parent.context.rootId : state.rootId; }), activeIndex: createSelector(state => state.activeIndex), isActive: createSelector((state, itemIndex) => state.activeIndex === itemIndex), hoverEnabled: createSelector(state => state.hoverEnabled), instantType: createSelector(state => state.instantType), lastOpenChangeReason: createSelector(state => state.openChangeReason), floatingTreeRoot: createSelector(state => { if (state.parent.type === 'menu') { return state.parent.store.select('floatingTreeRoot'); } return state.floatingTreeRoot; }), floatingNodeId: createSelector(state => state.floatingNodeId), floatingParentNodeId: createSelector(state => state.floatingParentNodeId), itemProps: createSelector(state => state.itemProps), closeDelay: createSelector(state => state.closeDelay), keyboardEventRelay: createSelector(state => { if (state.keyboardEventRelay) { return state.keyboardEventRelay; } if (state.parent.type === 'menu') { return state.parent.store.select('keyboardEventRelay'); } return undefined; }) }; export class MenuStore extends ReactStore { constructor(initialState) { super({ ...createInitialState(), ...initialState }, { positionerRef: /*#__PURE__*/React.createRef(), popupRef: /*#__PURE__*/React.createRef(), typingRef: { current: false }, itemDomElements: { current: [] }, itemLabels: { current: [] }, allowMouseUpTriggerRef: { current: false }, triggerFocusTargetRef: /*#__PURE__*/React.createRef(), beforeContentFocusGuardRef: /*#__PURE__*/React.createRef(), onOpenChangeComplete: undefined, triggerElements: new PopupTriggerMap() }, selectors); // Sync `allowMouseEnter` with parent menu if applicable. this.observe(createSelector(state => state.allowMouseEnter), (allowMouseEnter, oldValue) => { // The allowMouseEnter !== oldValue check prevent calling parent store's set // on intialization. Without it, React might complain about updating one component during rendering another. if (this.state.parent.type === 'menu' && allowMouseEnter !== oldValue) { this.state.parent.store.set('allowMouseEnter', allowMouseEnter); } }); // Set up propagation of state from parent menu if applicable. this.unsubscribeParentListener = this.observe('parent', parent => { this.unsubscribeParentListener?.(); if (parent.type === 'menu') { this.unsubscribeParentListener = parent.store.subscribe(() => { this.notifyAll(); }); this.context.allowMouseUpTriggerRef = parent.store.context.allowMouseUpTriggerRef; return; } if (parent.type !== undefined) { this.context.allowMouseUpTriggerRef = parent.context.allowMouseUpTriggerRef; } this.unsubscribeParentListener = null; }); } setOpen(open, eventDetails) { this.state.floatingRootContext.context.events.emit('setOpen', { open, eventDetails }); } static useStore(externalStore, initialState) { // eslint-disable-next-line react-hooks/rules-of-hooks const store = useRefWithInit(() => { return externalStore ?? new MenuStore(initialState); }).current; return store; } unsubscribeParentListener = null; } function createInitialState() { return { ...createInitialPopupStoreState(), disabled: false, modal: true, allowMouseEnter: true, stickIfOpen: true, parent: { type: undefined }, rootId: undefined, activeIndex: null, hoverEnabled: true, instantType: undefined, openChangeReason: null, floatingTreeRoot: new FloatingTreeStore(), floatingNodeId: undefined, floatingParentNodeId: null, itemProps: EMPTY_OBJECT, keyboardEventRelay: undefined, closeDelay: 0 }; }