From 2232d5265faddc7373fb03d77cfc5925c9b07ce6 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Thu, 23 Apr 2026 10:21:52 -0500 Subject: [PATCH] Add basic nesting dropdown component Co-authored-by: Copilot --- client/components/dropdown/dropdown.jsx | 78 ++++++++++++++++++++++++ client/components/dropdown/dropdown.less | 24 ++++++++ 2 files changed, 102 insertions(+) create mode 100644 client/components/dropdown/dropdown.jsx create mode 100644 client/components/dropdown/dropdown.less diff --git a/client/components/dropdown/dropdown.jsx b/client/components/dropdown/dropdown.jsx new file mode 100644 index 000000000..c218ad476 --- /dev/null +++ b/client/components/dropdown/dropdown.jsx @@ -0,0 +1,78 @@ +// 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. +import './dropdown.less'; +import React, { useEffect } 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, id, 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; + + + // 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 ( +
+ + + + +
+ ); +}; + +export { Dropdown }; \ No newline at end of file diff --git a/client/components/dropdown/dropdown.less b/client/components/dropdown/dropdown.less new file mode 100644 index 000000000..da418da6c --- /dev/null +++ b/client/components/dropdown/dropdown.less @@ -0,0 +1,24 @@ +.menu-wrapper { + position: relative; + place-content: center; + &:is(.menu-bar > .menu-section > .menu-wrapper){ + display: inline-block; + } +} + +.menu-list { + z-index : 1000; + inset-block-start: anchor(bottom); + inset-inline-start: anchor(left); + position-try: flip-inline; + color: unset; // [popover] gets a `canvastext` color value from useragent. + > .menu-wrapper { + position:relative; + > .menu-list { + margin: 0 0px; + inset-block-start: anchor(top); + inset-inline-start: anchor(right); + position-try: flip-inline; + } + } +} \ No newline at end of file