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}} Stage*/ /** @typedef {[set:Stage, get:React.StateUpdater]} Binding */ const BranchContext = React.createContext( /** @type Binding */ ([{ Open: false, Done: true }, (_n) => {}]), ); export { BranchContext as Context }; /** @type BasicElement */ export const Branch = (props) => { /** @type Binding */ const stage = React.useState( /** @type Stage */ ({ Open: false, Done: true }), ); /** @type Binding */ const stageParent = React.useContext(BranchContext); React.useEffect(() => { const [{ Open, Done }] = stageParent; const [, Shut] = stage; if (!Open && Done) { Shut({ Open: false, Done: true }); } }, [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 handler = () => { const value = { Open: !stageGet.Open, Done: false }; stageSet(value); console.log(value); }; 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)); }, []); React.useEffect(() => { const instant = stageGet.Open == false && stageGet.Done; refCollapser.current && refCollapser.current( stageGet.Open, instant ? 0 : 600, () => stageSet({ ...stageGet, Done: true }), ); }, [stageGet.Open]); return html`
${props.children}
`; }; /** @typedef {(Open:boolean)=>void} UserDone */ /** @typedef {(inOpen:boolean, inMilliseconds:number, inUserDone:UserDone)=>void} Toggler */ /** @typedef {(inElement:HTMLElement)=>Toggler} Collapse */ /** @type Collapse */ const Collapser = (inElement) => { /** @type UserDone */ let userDone = () => {}; let userMode = false; const collapse = inElement; /** @type {(inEvent:TransitionEvent)=>void} */ const done = (inEvent) => { if (inEvent.propertyName == "height" && inEvent.target == collapse) { inEvent.stopPropagation(); if (userMode) { collapse.style.height = "auto"; collapse.style.overflow = "visible"; userDone(true); } else { userDone(false); } } }; /** @type Toggler */ const show = (inOpen, inMs, inDone) => { userMode = inOpen; collapse.removeEventListener("transitionend", done); userDone = inDone; collapse.addEventListener("transitionend", done); collapse.style.height = collapse.clientHeight + "px"; collapse.style.overflow = "hidden"; collapse.style.transition = "none"; requestAnimationFrame(() => { collapse.style.height = `${inOpen ? collapse.scrollHeight : 0}px`; collapse.style.transition = `height ${inMs / 1000}s`; }); }; return show; }; /** @typedef {(e:Event)=>void} Handler */ /** @type {(inRef:React.MutableRefObject, inHandler:Handler)=>void} */ export const useAway = (inRef, inHandler) => { /** @type React.MutableRefObject */ const handlerUser = React.useRef(inHandler); handlerUser.current = inHandler; /** @type React.MutableRefObject */ const handlerClick = React.useRef((e) => { const index = inRef.current ? e.composedPath().indexOf(inRef.current) : -1; if (index < 0) { handlerUser.current(e); } }); React.useEffect(() => { document.addEventListener("click", handlerClick.current); return () => document.removeEventListener("click", handlerClick.current); }, []); };