mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2025-12-27 15:52:39 +00:00
This commit only renames things, changes no logic. Any mention of "book", "view", or "mode" is renamed in relation to "spreads". The AnchoredBox.jsx file is renamed to Anchored.jsx Extra icons are deleted, and the remaining ones are renamed.
92 lines
2.9 KiB
JavaScript
92 lines
2.9 KiB
JavaScript
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.
|
|
|
|
|
|
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={ref}
|
|
className={`anchored-trigger${visible ? ' active' : ''} ${className}`}
|
|
onClick={toggleVisibility}
|
|
style={{ anchorName: `--${props.id}` }} // setting anchor properties here allows greater recyclability.
|
|
{...props}
|
|
>
|
|
{children}
|
|
</button>
|
|
));
|
|
|
|
// forward ref for AnchoredBox
|
|
const AnchoredBox = forwardRef(({ visible, children, className, anchorId, ...props }, ref)=>(
|
|
<div
|
|
ref={ref}
|
|
className={`anchored-box${visible ? ' active' : ''} ${className}`}
|
|
style={{ positionAnchor: `--${anchorId}` }} // setting anchor properties here allows greater recyclability.
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
));
|
|
|
|
export { Anchored, AnchoredTrigger, AnchoredBox };
|