UNPKG

@equinor/eds-core-react

Version:

The React implementation of the Equinor Design System

202 lines (199 loc) 5.42 kB
import { forwardRef, useMemo, useEffect } from 'react'; import styled, { ThemeProvider } from 'styled-components'; import { MenuProvider, useMenu } from './Menu.context.js'; import { MenuList } from './MenuList.js'; import { bordersTemplate, useToken, mergeRefs, useIsomorphicLayoutEffect, useGlobalKeyPress } from '@equinor/eds-utils'; import { menu } from './Menu.tokens.js'; import { offset, flip, shift, size, useFloating, autoUpdate, useInteractions, useDismiss } from '@floating-ui/react'; import { jsx } from 'react/jsx-runtime'; import { Paper } from '../Paper/Paper.js'; import { useEds } from '../EdsProvider/eds.context.js'; const { border } = menu; const MenuPaper = styled(Paper).withConfig({ displayName: "Menu__MenuPaper", componentId: "sc-1vlnqcj-0" })(["width:100%;min-width:fit-content;", ";"], bordersTemplate(border)); const StyledPopover = styled('div').withConfig({ shouldForwardProp: () => true //workaround to avoid warning until popover gets added to react types }).withConfig({ displayName: "Menu__StyledPopover", componentId: "sc-1vlnqcj-1" })(["inset:unset;border:0;padding:0;margin:0;overflow:visible;background-color:transparent;&::backdrop{background-color:transparent;}"]); const MenuContainer = /*#__PURE__*/forwardRef(function MenuContainer({ children, anchorEl, onClose: onCloseCallback, open, ...rest }, ref) { const { setOnClose, onClose, setInitialFocus, focusedIndex } = useMenu(); const closeMenuOnClickIndexes = useMemo(() => [], []); useEffect(() => { if (onClose === null && onCloseCallback) { setOnClose(onCloseCallback); } }, [onClose, onCloseCallback, setOnClose]); useEffect(() => { const openWithKey = e => { const { key } = e; //activate menu with arrows according to wai-aria best practices if (key === 'ArrowDown' || key === 'ArrowUp') { e.preventDefault(); e.stopPropagation(); anchorEl.dispatchEvent(new Event('click', { bubbles: true })); } switch (key) { case 'Enter': case 'ArrowDown': setInitialFocus('first'); break; case 'ArrowUp': setInitialFocus('last'); break; } }; if (anchorEl) anchorEl.addEventListener('keydown', openWithKey); return () => { if (anchorEl) anchorEl.removeEventListener('keydown', openWithKey); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [anchorEl]); useGlobalKeyPress('Escape', () => { if (open && onClose !== null) { onClose(); anchorEl.focus(); } }); useGlobalKeyPress('Enter', () => { if (open && onClose !== null && closeMenuOnClickIndexes.includes(focusedIndex)) { setTimeout(() => { if (window.document.contains(anchorEl)) { anchorEl.focus(); } }, 0); } }); const addCloseMenuOnClickIndex = index => { if (closeMenuOnClickIndexes.indexOf(index) < 0) { closeMenuOnClickIndexes.push(index); } }; return /*#__PURE__*/jsx(MenuList, { addCloseMenuOnClickIndex: addCloseMenuOnClickIndex, ...rest, ref: ref, children: children }); }); const Menu = /*#__PURE__*/forwardRef(function Menu({ anchorEl, open, placement = 'bottom', matchAnchorWidth = false, onClose, style, className, ...rest }, ref) { const { density } = useEds(); const token = useToken({ density }, menu); let floatingMiddleware = [offset(4), flip(), shift({ padding: 8 })]; if (matchAnchorWidth) { floatingMiddleware = [...floatingMiddleware, size({ apply({ rects, elements }) { Object.assign(elements.floating.style, { width: `${rects.reference.width}px` }); } })]; } const { x, y, refs, update, strategy, context } = useFloating({ elements: { reference: anchorEl }, placement, open, onOpenChange: onClose, middleware: floatingMiddleware }); const popoverRef = useMemo(() => mergeRefs(refs.setFloating, ref), [refs.setFloating, ref]); useIsomorphicLayoutEffect(() => { if (refs.reference.current && refs.floating.current && open) { return autoUpdate(refs.reference.current, refs.floating.current, update); } }, [refs.reference, refs.floating, update, open]); useIsomorphicLayoutEffect(() => { if (open) { refs.floating.current?.showPopover(); } else { refs.floating.current?.hidePopover(); } }, [open, refs.floating]); const { getFloatingProps } = useInteractions([useDismiss(context, { escapeKey: false })]); const props = { className }; const menuProps = { ...rest, onClose, anchorEl, open }; return /*#__PURE__*/jsx(ThemeProvider, { theme: token, children: /*#__PURE__*/jsx(StyledPopover, { popover: "manual", ...getFloatingProps({ ref: popoverRef, style: { ...style, position: strategy, top: y || 0, left: x || 0 } }), children: /*#__PURE__*/jsx(MenuPaper, { elevation: "raised", ...props, children: /*#__PURE__*/jsx(MenuProvider, { children: /*#__PURE__*/jsx(MenuContainer, { ...menuProps, ref: ref }) }) }) }) }); }); export { Menu };