UNPKG

material-ui-popup-state

Version:

easiest way to create menus, popovers, and poppers with material-ui

473 lines (460 loc) 12.1 kB
/* 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