0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-02 23:42:44 +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:
Gazook89
2024-10-12 23:12:27 -05:00
parent 27f471791d
commit 1741abc3fe
3 changed files with 83 additions and 69 deletions

View File

@@ -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'; import './anchoredBox.less';
const AnchoredBox = ({ anchorPoint = 'center', className, children, ...props })=>{ // Anchored is a wrapper component that must have as children an <AnchoredTrigger> and a <AnchoredBox> component.
const [visible, setVisible] = useState(false); // AnchoredTrigger must have a unique `id` prop, which is passed up to Anchored, saved in state on mount, and
const triggerRef = useRef(null); // then passed down through props into AnchoredBox. The `id` is used for the CSS Anchor Positioning properties.
const boxRef = useRef(null); // **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(()=>{ useEffect(()=>{
const handleClickOutside = (evt)=>{ const handleClickOutside = (evt)=>{
if( if(
boxRef.current && boxRef.current &&
!boxRef.current.contains(evt.target) && !boxRef.current.contains(evt.target) &&
triggerRef.current && triggerRef.current &&
!triggerRef.current.contains(evt.target) !triggerRef.current.contains(evt.target)
) { ) {
setVisible(false); setVisible(false);
} }
}; };
window.addEventListener('click', handleClickOutside);
const iframe = document.querySelector('iframe'); const handleEscapeKey = (evt)=>{
if(iframe) { if(evt.key === 'Escape') setVisible(false);
iframe.addEventListener('load', ()=>{ };
const iframeDoc = iframe.contentWindow.document;
if(iframeDoc) { window.addEventListener('click', handleClickOutside);
iframeDoc.addEventListener('click', handleClickOutside); window.addEventListener('keydown', handleEscapeKey);
}
});
}
return ()=>{ return ()=>{
window.removeEventListener('click', handleClickOutside); window.removeEventListener('click', handleClickOutside);
if(iframe?.contentWindow?.document) { window.removeEventListener('keydown', handleEscapeKey);
iframe.contentWindow.document.removeEventListener('click', handleClickOutside);
}
}; };
}, []); }, []);
const handleClick = ()=>{ const toggleVisibility = ()=>setVisible((prev)=>!prev);
setVisible(!visible);
triggerRef.current?.focus();
};
const handleKeyDown = (evt)=>{ // Map children to inject necessary props
if(evt.key === 'Escape') { const mappedChildren = Children.map(children, (child)=>{
setVisible(false); if(child.type === AnchoredTrigger) {
triggerRef.current?.focus(); return cloneElement(child, { ref: triggerRef, toggleVisibility, visible });
} }
}; if(child.type === AnchoredBox) {
return cloneElement(child, { ref: boxRef, visible, anchorId });
}
return child;
});
return ( return <>{mappedChildren}</>;
<>
<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>
</>
);
}; };
const TriggerButton = forwardRef((props, ref)=>( // forward ref for AnchoredTrigger
<button ref={ref} {...props}> const AnchoredTrigger = forwardRef(({ toggleVisibility, visible, children, className, ...props }, ref)=>(
<i className='fas fa-gear' /> <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> </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 };

View File

@@ -1,17 +1,13 @@
.anchored-trigger { .anchored-trigger {
@supports (anchor-name: --view-settings){
anchor-name: --view-settings;
}
&.active { &.active {
background-color: #444444; background-color: #444444;
} }
} }
.anchored-box { .anchored-box {
position:absolute; position:absolute;
top: 30px; top: 30px;
@supports (position-anchor: --view-settings){ @supports (inset-block-start: anchor(bottom)){
position-anchor: --view-settings;
inset-block-start: anchor(bottom); inset-block-start: anchor(bottom);
} }
justify-self: anchor-center; justify-self: anchor-center;

View File

@@ -3,7 +3,7 @@ const React = require('react');
const { useState, useEffect } = React; const { useState, useEffect } = React;
const _ = require('lodash'); const _ = require('lodash');
import AnchoredBox from '../../../components/anchoredBox.jsx'; import { Anchored, AnchoredBox, AnchoredTrigger} from '../../../components/anchoredBox.jsx';
// import * as ZoomIcons from '../../../icons/icon-components/zoomIcons.jsx'; // import * as ZoomIcons from '../../../icons/icon-components/zoomIcons.jsx';
const MAX_ZOOM = 300; const MAX_ZOOM = 300;
@@ -174,17 +174,21 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages, onStyleC
><i className='fac flow-view-alt' /></button> ><i className='fac flow-view-alt' /></button>
</div> </div>
<AnchoredBox id='view-mode-options' className='tool' title='Options'> <Anchored>
<label title='Modify the horizontal space between pages.'>Column gap<input type='range' min={0} max={200} defaultValue={10} className='range-input' onChange={(evt)=>onStyleChange({ '.pages': { columnGap: `${evt.target.value}px` }})} /></label> <AnchoredTrigger id='view-settings' className='tool' title='Spread options'><i className='fas fa-gear' /></AnchoredTrigger>
<label title='Modify the vertical space between rows of pages.'>Row gap<input type='range' min={0} max={200} defaultValue={10} className='range-input' onChange={(evt)=>onStyleChange({ '.pages': { rowGap: `${evt.target.value}px` } })} /></label> <AnchoredBox title='Options'>
<label title='Start 1st page on the right side, such as if you have cover page.'>Start on right <h1>Options</h1>
<input type='checkbox' <label title='Modify the horizontal space between pages.'>Column gap<input type='range' min={0} max={200} defaultValue={10} className='range-input' onChange={(evt)=>onStyleChange({ '.pages': { columnGap: `${evt.target.value}px` }})} /></label>
onChange={()=>setStartOnRight(!startOnRight)} <label title='Modify the vertical space between rows of pages.'>Row gap<input type='range' min={0} max={200} defaultValue={10} className='range-input' onChange={(evt)=>onStyleChange({ '.pages': { rowGap: `${evt.target.value}px` } })} /></label>
checked={startOnRight} <label title='Start 1st page on the right side, such as if you have cover page.'>Start on right
title={arrangement !== 'facing' ? 'Switch to Facing to enable toggle.' : null} /> <input type='checkbox'
</label> onChange={()=>setStartOnRight(!startOnRight)}
<label title='Toggle the page shadow on every page.'>Page shadows<input type='checkbox' checked={pageShadows} onChange={()=>setPageShadows(!pageShadows)} /></label> checked={startOnRight}
</AnchoredBox> title={arrangement !== 'facing' ? 'Switch to Facing to enable toggle.' : null} />
</label>
<label title='Toggle the page shadow on every page.'>Page shadows<input type='checkbox' checked={pageShadows} onChange={()=>setPageShadows(!pageShadows)} /></label>
</AnchoredBox>
</Anchored>
</div> </div>
<div className='group'> <div className='group'>