From 1741abc3feb6d831dd1b78a28db8d149dddc0cbd Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Sat, 12 Oct 2024 23:12:27 -0500 Subject: [PATCH] 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. --- client/components/anchoredBox.jsx | 116 ++++++++++-------- client/components/anchoredBox.less | 8 +- .../homebrew/brewRenderer/toolBar/toolBar.jsx | 28 +++-- 3 files changed, 83 insertions(+), 69 deletions(-) diff --git a/client/components/anchoredBox.jsx b/client/components/anchoredBox.jsx index 7978e44d4..2f1735ebb 100644 --- a/client/components/anchoredBox.jsx +++ b/client/components/anchoredBox.jsx @@ -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 and a 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 ( - <> - -
handleKeyDown(evt)} - > -

{props.title}

- {children} -
- - ); + return <>{mappedChildren}; }; -const TriggerButton = forwardRef((props, ref)=>( - )); -export default AnchoredBox; +// forward ref for AnchoredBox +const AnchoredBox = forwardRef(({ visible, children, className, anchorId, ...props }, ref)=>( +
+ {children} +
+)); + +export { Anchored, AnchoredTrigger, AnchoredBox }; diff --git a/client/components/anchoredBox.less b/client/components/anchoredBox.less index 153cf84a6..69fa2d9c1 100644 --- a/client/components/anchoredBox.less +++ b/client/components/anchoredBox.less @@ -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; diff --git a/client/homebrew/brewRenderer/toolBar/toolBar.jsx b/client/homebrew/brewRenderer/toolBar/toolBar.jsx index fb2c4d2ae..bab33433f 100644 --- a/client/homebrew/brewRenderer/toolBar/toolBar.jsx +++ b/client/homebrew/brewRenderer/toolBar/toolBar.jsx @@ -3,7 +3,7 @@ const React = require('react'); const { useState, useEffect } = React; 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'; const MAX_ZOOM = 300; @@ -174,17 +174,21 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages, onStyleC > - - - - - - + + + +

Options

+ + + + +
+