mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-08-06 06:47:38 +00:00
359d87440b
Not quite working yet. polyfill is loaded, but positioned elements are spanning whole viewport. Parking this for now.
104 lines
3.4 KiB
React
104 lines
3.4 KiB
React
import React, { useState, useRef, forwardRef, useEffect, cloneElement, Children } from 'react';
|
|
import './Anchored.less';
|
|
|
|
// Anchored is a wrapper component that must have as children an <AnchoredTrigger> and a <AnchoredBox> component.
|
|
// AnchoredTrigger must have a unique `id` prop, which is passed up to Anchored, saved in state on mount, and
|
|
// then passed down through props into AnchoredBox. The `id` is used for the CSS Anchor Positioning properties.
|
|
// **The Anchor Positioning API is not available in Firefox yet**
|
|
// So in Firefox the positioning isn't perfect but is likely sufficient, and FF team seems to be working on the API quickly.
|
|
|
|
// When Anchor Positioning is added to Firefox, this can also be rewritten using the Popover API-- add the `popover` attribute
|
|
// to the container div, which will render the container in the *top level* and give it better interactions like
|
|
// click outside to dismiss. **Do not** add without Anchor, though, because positioning is very limited with the `popover`
|
|
// attribute.
|
|
|
|
|
|
const Anchored = ({ children })=>{
|
|
const [visible, setVisible] = useState(false);
|
|
const [anchorId, setAnchorId] = useState(null);
|
|
const boxRef = useRef(null);
|
|
const triggerRef = useRef(null);
|
|
|
|
// promote trigger id to Anchored id (to pass it back down to the box as "anchorId")
|
|
useEffect(()=>{
|
|
if(triggerRef.current){
|
|
setAnchorId(triggerRef.current.id);
|
|
}
|
|
}, []);
|
|
|
|
// close box on outside click or Escape key
|
|
useEffect(()=>{
|
|
const handleClickOutside = (evt)=>{
|
|
if(
|
|
boxRef.current &&
|
|
!boxRef.current.contains(evt.target) &&
|
|
triggerRef.current &&
|
|
!triggerRef.current.contains(evt.target)
|
|
) {
|
|
setVisible(false);
|
|
}
|
|
};
|
|
|
|
const handleEscapeKey = (evt)=>{
|
|
if(evt.key === 'Escape') setVisible(false);
|
|
};
|
|
|
|
window.addEventListener('click', handleClickOutside);
|
|
window.addEventListener('keydown', handleEscapeKey);
|
|
|
|
return ()=>{
|
|
window.removeEventListener('click', handleClickOutside);
|
|
window.removeEventListener('keydown', handleEscapeKey);
|
|
};
|
|
}, []);
|
|
|
|
const toggleVisibility = ()=>setVisible((prev)=>!prev);
|
|
|
|
// Map children to inject necessary props
|
|
const mappedChildren = Children.map(children, (child)=>{
|
|
if(child.type === AnchoredTrigger) {
|
|
return cloneElement(child, { ref: triggerRef, toggleVisibility, visible });
|
|
}
|
|
if(child.type === AnchoredBox) {
|
|
return cloneElement(child, { ref: boxRef, visible, anchorId });
|
|
}
|
|
return child;
|
|
});
|
|
|
|
return <>{mappedChildren}</>;
|
|
};
|
|
|
|
// forward ref for AnchoredTrigger
|
|
const AnchoredTrigger = forwardRef(({ toggleVisibility, visible, children, className, ...props }, ref)=>(
|
|
<button
|
|
ref={(el)=>{
|
|
// setAttribute bypasses React's style sanitization so the anchor polyfill can read it
|
|
el?.setAttribute('style', `anchor-name: --${props.id}`);
|
|
if(typeof ref === 'function') ref(el);
|
|
else if(ref) ref.current = el;
|
|
}}
|
|
className={`anchored-trigger${visible ? ' active' : ''} ${className}`}
|
|
onClick={toggleVisibility}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</button>
|
|
));
|
|
|
|
// forward ref for AnchoredBox
|
|
const AnchoredBox = forwardRef(({ visible, children, className, anchorId, ...props }, ref)=>(
|
|
<div
|
|
ref={(el)=>{
|
|
el?.setAttribute('style', `position-anchor: --${anchorId}`);
|
|
if(typeof ref === 'function') ref(el);
|
|
else if(ref) ref.current = el;
|
|
}}
|
|
className={`anchored-box${visible ? ' active' : ''} ${className}`}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
));
|
|
|
|
export { Anchored, AnchoredTrigger, AnchoredBox };
|