import React from "react"; export function useAway(inRef:React.Ref, inHandler:(inAway:boolean)=>void) { const handlerRef = React.useRef(inHandler); handlerRef.current = inHandler; React.useEffect(()=> { const iterate = (element:EventTarget)=>element === inRef.current; const handler = (event:MouseEvent)=> handlerRef.current( event.composedPath().find(iterate) ? false : true ); document.body.addEventListener("click", handler); return ()=> document.body.removeEventListener("click", handler); } , []); } type CollapseStateGet = {Done:boolean, Open:boolean, Away:boolean}; type CollapseStateSet = (args:{Done?:boolean, Open?:boolean, Away?:boolean})=>void; type CollapseStateBinding = [state:CollapseStateGet, update:CollapseStateSet]; const CollapseContext = React.createContext([{Done:true, Open:true, Away:false}, (args)=>{}] as CollapseStateBinding); export const CollapseGroup =({children}:{children:React.JSX.Element|React.JSX.Element[]})=> { const [stateGet, stateSet] = React.useState({Done:true, Open:false, Away:false} as CollapseStateGet); const setAny:CollapseStateSet =(params)=> stateSet((old)=> ({...old, ...params})); React.useEffect(()=>{stateGet.Away && setAny({Open:false})}, [stateGet.Away]); return {children}; }; export const CollapseButton =()=> { const [stateGet, stateSet] = React.useContext(CollapseContext); const buttonRef:React.Ref = React.useRef(null); useAway(buttonRef, (Away)=> { if(Away) { stateGet.Open && stateSet({Open:!stateGet.Open, Done:false}); } else { stateSet({Open:!stateGet.Open, Done:false}) } }); return } export const CollapseMenu =(props:{children:React.JSX.Element|React.JSX.Element[]})=> { const [stateGet, stateSet] = React.useContext(CollapseContext); const refEl:React.RefObject = React.useRef(null); const refCollapse:React.RefObject = React.useRef(null); React.useEffect(()=> { if(refEl.current) { refCollapse.current = Collapser(refEl.current, stateGet.Open); } return ()=> refCollapse.current && refCollapse.current(); } , []); React.useEffect(()=> { console.log(`menu animation start:${stateGet.Open}`); refCollapse.current && refCollapse.current(stateGet.Open, 500, (state, id)=> { console.log("menu animation done", state, id); stateSet({Done:true}); } ); }, [stateGet.Open, stateGet.Done]); console.log(`menu re-render: open:${stateGet.Open} done:${stateGet.Done} away:${stateGet.Away}`) return
}> { props.children}
}; type DoneCallback =(openState:boolean, id:number)=>void; type CollapseControls =(inOpen?:boolean, inMs?:number, inDone?:DoneCallback)=>void; export function Collapser(inElement:HTMLElement, initialState = false) { let userDone:DoneCallback = (openState) => {}; let userMode = initialState; let frameRequest = 0; let id = Math.random(); const done = (inEvent:TransitionEvent)=> { if (inEvent.propertyName == "height" && inEvent.target == inElement) { inEvent.stopPropagation(); if (userMode) { inElement.style.height = "auto"; inElement.style.overflow = "visible"; } userDone(userMode, id); } }; inElement.addEventListener("transitionend", done); return function(inOpen?:boolean, inMs?:number, inDone?:DoneCallback) { console.log("collapser sees", inOpen); cancelAnimationFrame(frameRequest); if(arguments.length) { userDone = inDone|| ((m)=>{}) as DoneCallback; userMode = inOpen === true; inElement.style.height = inElement.clientHeight + "px"; inElement.style.overflow = "hidden"; inElement.style.transition = "none"; frameRequest = requestAnimationFrame(()=> { inElement.style.height = `${inOpen ? inElement.scrollHeight : 0}px`; inElement.style.transition = `height ${(inMs||1000) / 1000}s`; }); } else { inElement.removeEventListener("transitionend", done); } } as CollapseControls; }