/** * A dropdown menu component that uses the Anchor Positioning API to position the elements. It supports nested submenus as well. * Anchor Positioning is now supported in all major browsers. If support is needed for older browsers (namely, older firefox) * it is possible to use absolute positioning and calculations/resize observers to position things well enough, but adds another layer of complexity to the code. * @param {string} props.groupName - Name of the menu. Appears as the trigger text. * @param {string} [props.icon] - Icon to display in the trigger. * @param {string} [props.color] - Color class to add to the trigger. * @param {string} [props.className] - Additional classes for the menu wrapper. * @param {React.ReactNode} [props.customTrigger] - Custom element to use as a trigger. * @param {React.ReactNode} [props.children] - Child elements to render in the menu. * @param {'right'|'left'} [props.dir] - Preferred menu open direction. Currently does nothing. * @returns {React.JSX.Element} */ import './dropdown.less'; import React, { useEffect, useRef } from 'react'; import _ from 'lodash'; // use react context to keep track of the menu depth (menus in menus) const MenuDepthContext = React.createContext(0); const Dropdown = ({ groupName, className = null, icon, children, color = null, customTrigger, dir = 'right', ...props })=>{ const menuId = `${_.kebabCase(groupName)}-menu`; const anchorName = `--${menuId}`; const depth = React.useContext(MenuDepthContext); // A menu is a submenu if depth > 0 const isSubMenu = depth > 0; const wrapperRef = useRef(null); const triggerRef = useRef(null); const menuRef = useRef(null); // use setAttribute instead of the React style prop because React strips unknown CSS // properties (like anchor-name) from inline styles in browsers that don't support them. // setAttribute writes raw CSS text that the anchor positioning polyfill can read useEffect(()=>{ triggerRef.current?.setAttribute('style', `anchor-name: ${anchorName}`); menuRef.current?.setAttribute('style', `position-anchor: ${anchorName}`); }, [anchorName]); // hide popover with click inside iframe (not supported by light dismiss) useEffect(()=>{ const menuElement = document.getElementById(menuId); if(!menuElement) return; const handleClick = ()=>{ if(menuElement.matches(':popover-open')) { menuElement.hidePopover(); } }; // Listen for clicks from both the main document and the iframe document.addEventListener('iframe-click', handleClick); return ()=>{ document.removeEventListener('iframe-click', handleClick); }; }, [menuId]); // the trigger is the piece placed inside the opening button of the menu. // This method allows for creating a generic span with the group name, // or using a bespoke element (like a graphic) passed in from props to be used as the trigger const trigger = (groupName)=>{ if(!customTrigger){ return <> {groupName} >; } else { return customTrigger; } }; return (