import { html } from "htm"; import * as React from "preact"; /** @typedef {(props:{children:preact.VNode, class:string|null})=>preact.VNode} BasicElement */ /** @typedef {{Open:boolean, Done:boolean, Away:boolean}} Stage*/ /** @typedef {[set:Stage, get:React.StateUpdater]} Binding */ const BranchContext = React.createContext( /** @type Binding */ ([{ Open: false, Done: true, Away: false }, (_n) => {}]), ); export { BranchContext as Context }; /** @type BasicElement */ export const Branch = (props) => { /** @type Binding */ const stage = React.useState( /** @type Stage */ ({ Open: false, Done: true, Away: false }), ); /** @type Binding */ const stageParent = React.useContext(BranchContext); React.useEffect(() => { const [{ Open, Done, Away }] = stageParent; const [, Shut] = stage; if (!Open && Done) { Shut({ Open: false, Done: true, Away }); } }, [stageParent]); return React.createElement(BranchContext.Provider, { value: stage, children: props.children, }); }; /** @type BasicElement */ export const Button = (props) => { /** @type Binding */ const [stageGet, stageSet] = React.useContext(BranchContext); const handleClick = () => { console.log("set: open"); stageSet((state) => ({ ...state, Open: !state.Open, Done: false })); }; return html``; }; /** @type BasicElement */ export const Menu = (props) => { const [stageGet, stageSet] = React.useContext(BranchContext); const refElement = React.useRef(null); /** @type {React.MutableRefObject} */ const refCollapser = React.useRef(null); React.useEffect(() => { refElement.current && (refCollapser.current = Collapser(refElement.current)); }, []); const instant = stageGet.Open == false && stageGet.Done; refCollapser.current && refCollapser.current( stageGet.Open, instant ? 0 : 600, () => stageSet({ ...stageGet, Done: true }), ); /** @type React.MutableRefObject */ const refHandler = React.useRef(null); refHandler.current = (away) => { console.log("set: away"); stageSet((state) => ({ ...state, Away: away })); }; useAway(refElement, refHandler); return html`
${props.children}
`; }; /** @typedef {(Open:boolean)=>void} UserDone */ /** @typedef {(inOpen:boolean, inMilliseconds:number, inUserDone:UserDone)=>void | (()=>void) } Toggler */ /** @typedef {(inElement:HTMLElement)=>Toggler} Collapse */ /** @type Collapse */ const Collapser = (inElement) => { /** @type UserDone */ let userDone = () => {}; let userMode = false; /** @type number */ let frameRequest; /** @type {(inEvent:TransitionEvent)=>void} */ const done = (inEvent) => { if (inEvent.propertyName == "height" && inEvent.target == inElement) { inEvent.stopPropagation(); if (userMode) { inElement.style.height = "auto"; inElement.style.overflow = "visible"; } userDone(userMode); } }; inElement.addEventListener("transitionend", done); /** @type Toggler */ const show = (inOpen, inMs, inDone) => { cancelAnimationFrame(frameRequest); if ((!inOpen && !inMs) && !inDone) { inElement.removeEventListener("transitionend", done); } else { userDone = inDone; userMode = inOpen; 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}s`; }); } }; return show; }; /** @typedef {(away:boolean, e:Event)=>void} Handler */ /** @type {(inRef:React.MutableRefObject, inHandler:React.MutableRefObject)=>void} */ export const useAway = (inRef, inHandlerRef) => { /** @type React.MutableRefObject<(e:Event)=>void> */ const handlerClick = React.useRef((e) => { const index = inRef.current ? e.composedPath().indexOf(inRef.current) : -1; inHandlerRef.current && inHandlerRef.current(index < 0, e); }); React.useEffect(() => { document.addEventListener("click", handlerClick.current); return () => document.removeEventListener("click", handlerClick.current); }, []); };