0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-08-06 08:57:38 +00:00
Files
homebrewery/client/components/dropdown/dropdown.jsx
T
Gazook89 359d87440b add oddbird polyfill - not quite working
Not quite working yet.  polyfill is loaded, but positioned elements are spanning whole viewport.  Parking this for now.
2026-04-24 22:06:52 -05:00

99 lines
3.6 KiB
React

/**
* 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 menuRef = useRef(null);
// Use setAttribute instead of the React style prop because React silently 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(()=>{
wrapperRef.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 <>
<span className='menu-name'>{groupName}</span><i className={`caret fas fa-caret-${isSubMenu ? 'right' : 'down'}`}></i>
</>;
} else {
return customTrigger;
}
};
return (
<div ref={wrapperRef} className={['menu-wrapper', className].join(' ')} role='none' >
<button
id={groupName.replace(' ', '-')}
className={['menu-item', color].join(' ')}
popoverTarget={menuId}
icon={icon}
aria-haspopup='menu'
role='menuitem'
disabled={children.length > 0 ? false : true}
>
{trigger(groupName)}
</button>
<MenuDepthContext.Provider value={depth + 1}>
<div
ref={menuRef}
id={menuId}
className='menu-list'
popover='auto'
role='menu'
>
{children}
</div>
</MenuDepthContext.Provider>
</div>
);
};
export { Dropdown };