mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2025-12-24 18:32:41 +00:00
Refactor AnchoredBox for greater flexibility
Big change to how the AnchoredBox component is structured so that it can be used in more than just one spot. Now composed of the wrapper Anchored, with two children AnchoredTrigger and AnchoredBox, each of which can have their own arbitrary children. Next steps will involve renaming the component file to Anchored.jsx, moving most styling out of the attached stylesheet (and into brewRenderer.less), and adding props to pass in Anchor Positioning properties.
This commit is contained in:
@@ -1,77 +1,91 @@
|
||||
import React, { useState, useRef, useEffect, forwardRef } from 'react';
|
||||
import React, { useState, useRef, forwardRef, useEffect, cloneElement, Children } from 'react';
|
||||
import './anchoredBox.less';
|
||||
|
||||
const AnchoredBox = ({ anchorPoint = 'center', className, children, ...props })=>{
|
||||
const [visible, setVisible] = useState(false);
|
||||
const triggerRef = useRef(null);
|
||||
const boxRef = useRef(null);
|
||||
// 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)
|
||||
!boxRef.current.contains(evt.target) &&
|
||||
triggerRef.current &&
|
||||
!triggerRef.current.contains(evt.target)
|
||||
) {
|
||||
setVisible(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener('click', handleClickOutside);
|
||||
|
||||
const iframe = document.querySelector('iframe');
|
||||
if(iframe) {
|
||||
iframe.addEventListener('load', ()=>{
|
||||
const iframeDoc = iframe.contentWindow.document;
|
||||
if(iframeDoc) {
|
||||
iframeDoc.addEventListener('click', handleClickOutside);
|
||||
}
|
||||
});
|
||||
}
|
||||
const handleEscapeKey = (evt)=>{
|
||||
if(evt.key === 'Escape') setVisible(false);
|
||||
};
|
||||
|
||||
window.addEventListener('click', handleClickOutside);
|
||||
window.addEventListener('keydown', handleEscapeKey);
|
||||
|
||||
return ()=>{
|
||||
window.removeEventListener('click', handleClickOutside);
|
||||
if(iframe?.contentWindow?.document) {
|
||||
iframe.contentWindow.document.removeEventListener('click', handleClickOutside);
|
||||
}
|
||||
window.removeEventListener('keydown', handleEscapeKey);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleClick = ()=>{
|
||||
setVisible(!visible);
|
||||
triggerRef.current?.focus();
|
||||
};
|
||||
const toggleVisibility = ()=>setVisible((prev)=>!prev);
|
||||
|
||||
const handleKeyDown = (evt)=>{
|
||||
if(evt.key === 'Escape') {
|
||||
setVisible(false);
|
||||
triggerRef.current?.focus();
|
||||
// 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 (
|
||||
<>
|
||||
<TriggerButton
|
||||
className={`${className} anchored-trigger${visible ? ' active' : ''}`}
|
||||
onClick={handleClick}
|
||||
ref={triggerRef}
|
||||
/>
|
||||
<div
|
||||
className={`anchored-box${visible ? ' active' : ''}`}
|
||||
ref={boxRef}
|
||||
onKeyDown={(evt)=>handleKeyDown(evt)}
|
||||
>
|
||||
<h1>{props.title}</h1>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
return <>{mappedChildren}</>;
|
||||
};
|
||||
|
||||
const TriggerButton = forwardRef((props, ref)=>(
|
||||
<button ref={ref} {...props}>
|
||||
<i className='fas fa-gear' />
|
||||
// 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>
|
||||
));
|
||||
|
||||
export default AnchoredBox;
|
||||
// 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 };
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
.anchored-trigger {
|
||||
@supports (anchor-name: --view-settings){
|
||||
anchor-name: --view-settings;
|
||||
}
|
||||
&.active {
|
||||
background-color: #444444;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.anchored-box {
|
||||
position:absolute;
|
||||
top: 30px;
|
||||
@supports (position-anchor: --view-settings){
|
||||
position-anchor: --view-settings;
|
||||
@supports (inset-block-start: anchor(bottom)){
|
||||
inset-block-start: anchor(bottom);
|
||||
}
|
||||
justify-self: anchor-center;
|
||||
|
||||
Reference in New Issue
Block a user