material-ui-popup-state
Version:
easiest way to create menus, popovers, and poppers with material-ui
473 lines (460 loc) • 12.1 kB
JavaScript
/* eslint-env browser */
import { useCallback, useState, useRef, useEffect } from 'react';
import * as React from 'react';
import { useEvent } from "./useEvent.mjs";
const printedWarnings = {};
function warn(key, message) {
if (printedWarnings[key]) return;
printedWarnings[key] = true;
console.error('[material-ui-popup-state] WARNING', message); // eslint-disable-line no-console
}
export const initCoreState = {
isOpen: false,
setAnchorElUsed: false,
anchorEl: undefined,
anchorPosition: undefined,
hovered: false,
focused: false,
_openEventType: null,
_childPopupState: null,
_deferNextOpen: false,
_deferNextClose: false
};
// https://github.com/jcoreio/material-ui-popup-state/issues/138
// Webpack prod build doesn't like it if we refer to React.useId conditionally,
// but aliasing to a variable like this works
const _react = React;
const defaultPopupId = 'useId' in _react ? () => _react.useId() :
// istanbul ignore next
() => undefined;
export function usePopupState({
parentPopupState,
popupId = defaultPopupId(),
variant,
disableAutoFocus
}) {
const isMounted = useRef(true);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
const [state, _setState] = useState(initCoreState);
const setState = useCallback(state => {
if (isMounted.current) _setState(state);
}, []);
const setAnchorEl = useCallback(anchorEl => setState(state => ({
...state,
setAnchorElUsed: true,
anchorEl: anchorEl ?? undefined
})), []);
const toggle = useEvent(eventOrAnchorEl => {
if (state.isOpen) close(eventOrAnchorEl);else open(eventOrAnchorEl);
return state;
});
const open = useEvent(eventOrAnchorEl => {
const event = eventOrAnchorEl instanceof Element ? undefined : eventOrAnchorEl;
const element = eventOrAnchorEl instanceof Element ? eventOrAnchorEl : (eventOrAnchorEl === null || eventOrAnchorEl === void 0 ? void 0 : eventOrAnchorEl.currentTarget) instanceof Element ? eventOrAnchorEl.currentTarget : undefined;
if ((event === null || event === void 0 ? void 0 : event.type) === 'touchstart') {
setState(state => ({
...state,
_deferNextOpen: true
}));
return;
}
const clientX = event === null || event === void 0 ? void 0 : event.clientX;
const clientY = event === null || event === void 0 ? void 0 : event.clientY;
const anchorPosition = typeof clientX === 'number' && typeof clientY === 'number' ? {
left: clientX,
top: clientY
} : undefined;
const doOpen = state => {
if (!eventOrAnchorEl && !state.setAnchorElUsed && variant !== 'dialog') {
warn('missingEventOrAnchorEl', 'eventOrAnchorEl should be defined if setAnchorEl is not used');
}
if (parentPopupState) {
if (!parentPopupState.isOpen) return state;
setTimeout(() => parentPopupState._setChildPopupState(popupState));
}
const newState = {
...state,
isOpen: true,
anchorPosition,
hovered: (event === null || event === void 0 ? void 0 : event.type) === 'mouseover' || state.hovered,
focused: (event === null || event === void 0 ? void 0 : event.type) === 'focus' || state.focused,
_openEventType: event === null || event === void 0 ? void 0 : event.type
};
if (!state.setAnchorElUsed) {
if (event !== null && event !== void 0 && event.currentTarget) {
newState.anchorEl = event === null || event === void 0 ? void 0 : event.currentTarget;
} else if (element) {
newState.anchorEl = element;
}
}
return newState;
};
setState(state => {
if (state._deferNextOpen) {
setTimeout(() => setState(doOpen), 0);
return {
...state,
_deferNextOpen: false
};
} else {
return doOpen(state);
}
});
});
const doClose = state => {
const {
_childPopupState
} = state;
setTimeout(() => {
_childPopupState === null || _childPopupState === void 0 || _childPopupState.close();
parentPopupState === null || parentPopupState === void 0 || parentPopupState._setChildPopupState(null);
});
return {
...state,
isOpen: false,
hovered: false,
focused: false
};
};
const close = useEvent(eventOrAnchorEl => {
const event = eventOrAnchorEl instanceof Element ? undefined : eventOrAnchorEl;
if ((event === null || event === void 0 ? void 0 : event.type) === 'touchstart') {
setState(state => ({
...state,
_deferNextClose: true
}));
return;
}
setState(state => {
if (state._deferNextClose) {
setTimeout(() => setState(doClose), 0);
return {
...state,
_deferNextClose: false
};
} else {
return doClose(state);
}
});
});
const setOpen = useCallback((nextOpen, eventOrAnchorEl) => {
if (nextOpen) {
open(eventOrAnchorEl);
} else {
close(eventOrAnchorEl);
}
}, []);
const onMouseLeave = useEvent(event => {
const {
relatedTarget
} = event;
setState(state => {
if (state.hovered && !(relatedTarget instanceof Element && isElementInPopup(relatedTarget, popupState))) {
if (state.focused) {
return {
...state,
hovered: false
};
} else {
return doClose(state);
}
}
return state;
});
});
const onBlur = useEvent(event => {
if (!event) return;
const {
relatedTarget
} = event;
setState(state => {
if (state.focused && !(relatedTarget instanceof Element && isElementInPopup(relatedTarget, popupState))) {
if (state.hovered) {
return {
...state,
focused: false
};
} else {
return doClose(state);
}
}
return state;
});
});
const _setChildPopupState = useCallback(_childPopupState => setState(state => ({
...state,
_childPopupState
})), []);
const popupState = {
...state,
setAnchorEl,
popupId: popupId ?? undefined,
variant,
open,
close,
toggle,
setOpen,
onBlur,
onMouseLeave,
disableAutoFocus: disableAutoFocus ?? Boolean(state.hovered || state.focused),
_setChildPopupState
};
return popupState;
}
/**
* Creates a ref that sets the anchorEl for the popup.
*
* @param {object} popupState the argument passed to the child function of
* `PopupState`
*/
export function anchorRef({
setAnchorEl
}) {
return setAnchorEl;
}
function controlAriaProps({
isOpen,
popupId,
variant
}) {
return {
...(variant === 'popover' ? {
'aria-haspopup': true,
'aria-controls': isOpen ? popupId : undefined
} : variant === 'popper' ? {
'aria-describedby': isOpen ? popupId : undefined
} : undefined)
};
}
/**
* Creates props for a component that opens the popup when clicked.
*
* @param {object} popupState the argument passed to the child function of
* `PopupState`
*/
export function bindTrigger(popupState) {
return {
...controlAriaProps(popupState),
onClick: popupState.open,
onTouchStart: popupState.open
};
}
/**
* Creates props for a component that opens the popup on its contextmenu event (right click).
*
* @param {object} popupState the argument passed to the child function of
* `PopupState`
*/
export function bindContextMenu(popupState) {
return {
...controlAriaProps(popupState),
onContextMenu: e => {
e.preventDefault();
popupState.open(e);
}
};
}
/**
* Creates props for a component that toggles the popup when clicked.
*
* @param {object} popupState the argument passed to the child function of
* `PopupState`
*/
export function bindToggle(popupState) {
return {
...controlAriaProps(popupState),
onClick: popupState.toggle,
onTouchStart: popupState.toggle
};
}
/**
* Creates props for a component that opens the popup while hovered.
*
* @param {object} popupState the argument passed to the child function of
* `PopupState`
*/
export function bindHover(popupState) {
const {
open,
onMouseLeave
} = popupState;
return {
...controlAriaProps(popupState),
onTouchStart: open,
onMouseOver: open,
onMouseLeave
};
}
/**
* Creates props for a component that opens the popup while focused.
*
* @param {object} popupState the argument passed to the child function of
* `PopupState`
*/
export function bindFocus(popupState) {
const {
open,
onBlur
} = popupState;
return {
...controlAriaProps(popupState),
onFocus: open,
onBlur
};
}
/**
* Creates props for a component that opens the popup while double click.
*
* @param {object} popupState the argument passed to the child function of
* `PopupState`
*/
export function bindDoubleClick({
isOpen,
open,
popupId,
variant
}) {
return {
// $FlowFixMe
[variant === 'popover' ? 'aria-controls' : 'aria-describedby']: isOpen ? popupId : null,
'aria-haspopup': variant === 'popover' ? true : undefined,
onDoubleClick: open
};
}
/**
* Creates props for a `Popover` component.
*
* @param {object} popupState the argument passed to the child function of
* `PopupState`
*/
export function bindPopover({
isOpen,
anchorEl,
anchorPosition,
close,
popupId,
onMouseLeave,
disableAutoFocus,
_openEventType
}) {
const usePopoverPosition = _openEventType === 'contextmenu';
return {
id: popupId,
anchorEl,
anchorPosition,
anchorReference: usePopoverPosition ? 'anchorPosition' : 'anchorEl',
open: isOpen,
onClose: close,
onMouseLeave,
...(disableAutoFocus && {
disableAutoFocus: true,
disableEnforceFocus: true,
disableRestoreFocus: true
})
};
}
/**
* Creates props for a `Menu` component.
*
* @param {object} popupState the argument passed to the child function of
* `PopupState`
*/
/**
* Creates props for a `Popover` component.
*
* @param {object} popupState the argument passed to the child function of
* `PopupState`
*/
export function bindMenu({
isOpen,
anchorEl,
anchorPosition,
close,
popupId,
onMouseLeave,
disableAutoFocus,
_openEventType
}) {
const usePopoverPosition = _openEventType === 'contextmenu';
return {
id: popupId,
anchorEl,
anchorPosition,
anchorReference: usePopoverPosition ? 'anchorPosition' : 'anchorEl',
open: isOpen,
onClose: close,
onMouseLeave,
...(disableAutoFocus && {
autoFocus: false,
disableAutoFocusItem: true,
disableAutoFocus: true,
disableEnforceFocus: true,
disableRestoreFocus: true
})
};
}
/**
* Creates props for a `Popper` component.
*
* @param {object} popupState the argument passed to the child function of
* `PopupState`
*/
export function bindPopper({
isOpen,
anchorEl,
popupId,
onMouseLeave
}) {
return {
id: popupId,
anchorEl,
open: isOpen,
onMouseLeave
};
}
/**
* Creates props for a `Dialog` component.
*
* @param {object} popupState the argument passed to the child function of
* `PopupState`
*/
export function bindDialog({
isOpen,
close
}) {
return {
open: isOpen,
onClose: close
};
}
function getPopup(element, {
popupId
}) {
if (!popupId) return null;
const rootNode = typeof element.getRootNode === 'function' ? element.getRootNode() : document;
if (typeof rootNode.getElementById === 'function') {
return rootNode.getElementById(popupId);
}
return null;
}
function isElementInPopup(element, popupState) {
const {
anchorEl,
_childPopupState
} = popupState;
return isAncestor(anchorEl, element) || isAncestor(getPopup(element, popupState), element) || _childPopupState != null && isElementInPopup(element, _childPopupState);
}
function isAncestor(parent, child) {
if (!parent) return false;
while (child) {
if (child === parent) return true;
child = child.parentElement;
}
return false;
}
//# sourceMappingURL=hooks.mjs.map