@equinor/eds-core-react
Version:
The React implementation of the Equinor Design System
202 lines (199 loc) • 5.42 kB
JavaScript
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 };