@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
JavaScript
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
};
}