From 10064bdc0bc8c8c16599d63106bba1ffe99a0983 Mon Sep 17 00:00:00 2001 From: dmail Date: Wed, 6 May 2026 17:11:40 +0200 Subject: [PATCH 1/7] work --- packages/frontend/navi/src/box/box.jsx | 61 +++++++-------- .../frontend/navi/src/box/use_element_ref.js | 75 +++++++++++++++++++ 2 files changed, 106 insertions(+), 30 deletions(-) create mode 100644 packages/frontend/navi/src/box/use_element_ref.js diff --git a/packages/frontend/navi/src/box/box.jsx b/packages/frontend/navi/src/box/box.jsx index d8b5d12069..3f925db381 100644 --- a/packages/frontend/navi/src/box/box.jsx +++ b/packages/frontend/navi/src/box/box.jsx @@ -53,7 +53,7 @@ import { normalizeStyles } from "@jsenv/dom"; import { toChildArray } from "preact"; -import { useContext, useRef } from "preact/hooks"; +import { useContext } from "preact/hooks"; import { withPropsClassName } from "../utils/with_props_class_name.js"; import { BoxFlowContext } from "./box_flow_context.jsx"; @@ -70,7 +70,7 @@ import { PSEUDO_NAMED_STYLES_DEFAULT, PSEUDO_STATE_DEFAULT, } from "./pseudo_styles.js"; -import { useEarlyDOMEffect } from "./use_early_dom_effect.js"; +import { useElementRefEffect } from "./use_element_ref.js"; import.meta.css = /* css */ ` @layer navi { @@ -200,8 +200,7 @@ export const Box = (props) => { separator, ...rest } = props; - const defaultRef = useRef(); - const ref = props.ref || defaultRef; + let ref; const TagName = as; const defaultDisplay = getDefaultDisplay(TagName); @@ -620,32 +619,34 @@ export const Box = (props) => { styleDeps.push(...pseudoClasses); } } - // TODO: just use ref function, it will be called same time as early dom effect + give the dom node + be standard - // we need to implent our styleDeps tracking but that's likely very easy - useEarlyDOMEffect((boxEl) => { - const pseudoStateEl = pseudoStateSelector - ? boxEl.querySelector(pseudoStateSelector) - : boxEl; - const visualEl = visualSelector - ? boxEl.querySelector(visualSelector) - : null; - return initPseudoStyles(pseudoStateEl, { - pseudoClasses: innerPseudoClasses, - pseudoState: innerPseudoState, - effect: (state) => { - applyStyle( - boxEl, - boxStyles, - state, - boxPseudoNamedStyles, - preventInitialTransition, - ); - }, - elementToImpact: boxEl, - elementListeningPseudoState: - visualEl === pseudoStateEl ? null : visualEl, - }); - }, styleDeps); + ref = useElementRefEffect( + props.ref, + (boxEl) => { + const pseudoStateEl = pseudoStateSelector + ? boxEl.querySelector(pseudoStateSelector) + : boxEl; + const visualEl = visualSelector + ? boxEl.querySelector(visualSelector) + : null; + return initPseudoStyles(pseudoStateEl, { + pseudoClasses: innerPseudoClasses, + pseudoState: innerPseudoState, + effect: (state) => { + applyStyle( + boxEl, + boxStyles, + state, + boxPseudoNamedStyles, + preventInitialTransition, + ); + }, + elementToImpact: boxEl, + elementListeningPseudoState: + visualEl === pseudoStateEl ? null : visualEl, + }); + }, + styleDeps, + ); } // When hasChildFunction is used it means diff --git a/packages/frontend/navi/src/box/use_element_ref.js b/packages/frontend/navi/src/box/use_element_ref.js new file mode 100644 index 0000000000..506c42e35e --- /dev/null +++ b/packages/frontend/navi/src/box/use_element_ref.js @@ -0,0 +1,75 @@ +import { useRef } from "preact/hooks"; + +/** + * Returns a ref callback that forwards the DOM node to `externalRef` if provided. + * Always maintains an internal `.current` property pointing to the current DOM node. + */ + +export const useElementRef = (externalRef) => { + const elRef = useRef(null); + return externalRef || elRef; +}; + +/** + * Like useElementRef, but also calls `onElement(el)` when the element mounts + * or when deps change. The return value of `onElement` is used as a cleanup + * function called on unmount or before re-running. + * + * @param {function|object|null} externalRef - Optional ref to forward to + * @param {function} onElement - Called with the DOM element on mount or when deps change + * @param {Array} deps - onElement is re-called only when deps change (like useEffect deps) + */ + +export const useElementRefEffect = (externalRef, onElement, deps) => { + const cleanupRef = useRef(null); + const elRef = useRef(null); + const prevDepsRef = useRef(undefined); + + const ref = (el) => { + elRef.current = el; + if (externalRef) { + if (typeof externalRef === "function") { + externalRef(el); + } else { + externalRef.current = el; + } + } + if (!el) { + const cleanup = cleanupRef.current; + if (cleanup) { + cleanup(); + cleanupRef.current = null; + } + prevDepsRef.current = undefined; + return; + } + const prevDeps = prevDepsRef.current; + let depsChanged; + if (!prevDeps || prevDeps.length !== deps.length) { + depsChanged = true; + } else { + depsChanged = false; + for (let i = 0; i < deps.length; i++) { + if (!Object.is(deps[i], prevDeps[i])) { + depsChanged = true; + break; + } + } + } + if (!depsChanged) { + return; + } + if (cleanupRef.current) { + cleanupRef.current(); + cleanupRef.current = null; + } + prevDepsRef.current = deps; + const cleanup = onElement(el); + if (typeof cleanup === "function") { + cleanupRef.current = cleanup; + } + }; + ref.current = elRef.current; + + return ref; +}; From 5938cdf9955ac2773fdbfc7aca406457f22f3301 Mon Sep 17 00:00:00 2001 From: dmail Date: Wed, 6 May 2026 17:17:04 +0200 Subject: [PATCH 2/7] work --- .../frontend/navi/src/box/use_element_ref.js | 71 +++++++++++-------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/packages/frontend/navi/src/box/use_element_ref.js b/packages/frontend/navi/src/box/use_element_ref.js index 506c42e35e..ef5621ba12 100644 --- a/packages/frontend/navi/src/box/use_element_ref.js +++ b/packages/frontend/navi/src/box/use_element_ref.js @@ -11,38 +11,35 @@ export const useElementRef = (externalRef) => { }; /** - * Like useElementRef, but also calls `onElement(el)` when the element mounts - * or when deps change. The return value of `onElement` is used as a cleanup - * function called on unmount or before re-running. + * Keeps a DOM element in sync with `syncElement(el)` whenever deps change. + * - If element is already mounted: runs syncElement immediately during render. + * - If not yet mounted: runs syncElement in the ref callback when element arrives. + * - Calls cleanup (if returned by syncElement) before each re-run and on unmount. * * @param {function|object|null} externalRef - Optional ref to forward to - * @param {function} onElement - Called with the DOM element on mount or when deps change - * @param {Array} deps - onElement is re-called only when deps change (like useEffect deps) + * @param {function} syncElement - Called with the DOM element when deps change + * @param {Array} deps - syncElement is re-called only when deps change */ -export const useElementRefEffect = (externalRef, onElement, deps) => { +export const useElementRefEffect = (externalRef, syncElement, deps) => { const cleanupRef = useRef(null); const elRef = useRef(null); const prevDepsRef = useRef(undefined); - const ref = (el) => { - elRef.current = el; - if (externalRef) { - if (typeof externalRef === "function") { - externalRef(el); - } else { - externalRef.current = el; - } + const runSync = (el) => { + if (cleanupRef.current) { + cleanupRef.current(); + cleanupRef.current = null; } - if (!el) { - const cleanup = cleanupRef.current; - if (cleanup) { - cleanup(); - cleanupRef.current = null; - } - prevDepsRef.current = undefined; - return; + prevDepsRef.current = deps; + const cleanup = syncElement(el); + if (typeof cleanup === "function") { + cleanupRef.current = cleanup; } + }; + + // If element already mounted, check deps and sync during render. + if (elRef.current) { const prevDeps = prevDepsRef.current; let depsChanged; if (!prevDeps || prevDeps.length !== deps.length) { @@ -56,20 +53,32 @@ export const useElementRefEffect = (externalRef, onElement, deps) => { } } } - if (!depsChanged) { - return; + if (depsChanged) { + runSync(elRef.current); } - if (cleanupRef.current) { - cleanupRef.current(); - cleanupRef.current = null; + } + + const ref = (el) => { + elRef.current = el; + if (externalRef) { + if (typeof externalRef === "function") { + externalRef(el); + } else { + externalRef.current = el; + } } - prevDepsRef.current = deps; - const cleanup = onElement(el); - if (typeof cleanup === "function") { - cleanupRef.current = cleanup; + if (el) { + runSync(el); + } else { + if (cleanupRef.current) { + cleanupRef.current(); + cleanupRef.current = null; + } + prevDepsRef.current = undefined; } }; ref.current = elRef.current; return ref; }; + From f88c30ce563a197d708734c9c9567297a46fb5e5 Mon Sep 17 00:00:00 2001 From: dmail Date: Wed, 6 May 2026 17:19:23 +0200 Subject: [PATCH 3/7] work --- packages/frontend/navi/src/box/use_element_ref.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/frontend/navi/src/box/use_element_ref.js b/packages/frontend/navi/src/box/use_element_ref.js index ef5621ba12..a1f70b5325 100644 --- a/packages/frontend/navi/src/box/use_element_ref.js +++ b/packages/frontend/navi/src/box/use_element_ref.js @@ -1,10 +1,10 @@ import { useRef } from "preact/hooks"; /** - * Returns a ref callback that forwards the DOM node to `externalRef` if provided. - * Always maintains an internal `.current` property pointing to the current DOM node. + * Returns either the external ref passed via props, or a local ref as fallback. + * Useful when a component needs to access its own DOM node but must also support + * an optional ref forwarded by the parent. */ - export const useElementRef = (externalRef) => { const elRef = useRef(null); return externalRef || elRef; @@ -20,7 +20,6 @@ export const useElementRef = (externalRef) => { * @param {function} syncElement - Called with the DOM element when deps change * @param {Array} deps - syncElement is re-called only when deps change */ - export const useElementRefEffect = (externalRef, syncElement, deps) => { const cleanupRef = useRef(null); const elRef = useRef(null); @@ -81,4 +80,3 @@ export const useElementRefEffect = (externalRef, syncElement, deps) => { return ref; }; - From 3935eadecfe5eeb626a7f7004fd81c5d529fcc38 Mon Sep 17 00:00:00 2001 From: dmail Date: Wed, 6 May 2026 17:27:05 +0200 Subject: [PATCH 4/7] work --- .../src/box/demos/box_style_timing_demo.html | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/frontend/navi/src/box/demos/box_style_timing_demo.html b/packages/frontend/navi/src/box/demos/box_style_timing_demo.html index e0c20f05d5..ae3af5dc53 100644 --- a/packages/frontend/navi/src/box/demos/box_style_timing_demo.html +++ b/packages/frontend/navi/src/box/demos/box_style_timing_demo.html @@ -7,6 +7,25 @@
+
+ native div ref calls: 0 + Box ref calls: 0 +