@codecademy/gamut
68.3.068.3.1-alpha.0b3b4f.0
dist/PopoverContainer/hooks.js~
dist/PopoverContainer/hooks.jsModified+50−27
Index: package/dist/PopoverContainer/hooks.js
===================================================================
--- package/dist/PopoverContainer/hooks.js
+++ package/dist/PopoverContainer/hooks.js
@@ -1,19 +1,49 @@
-import { useEffect, useMemo } from 'react';
+import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
+import { isNullish } from '../utils/nullish';
import { findAllAdditionalScrollingParents, findResizingParent } from './utils';
+
+/**
+ * Minimal element shape required for popover positioning.
+ * Accepts both HTMLElement and TargetRef so Popover and PopoverContainer can share hooks.
+ */
+
+/** Resolves ref object to current element; returns null when unset. */
+export function getRefElement(ref) {
+ if (isNullish(ref)) return null;
+ return ref.current;
+}
+
+/** Casts minimal target to HTMLElement for utils that need full DOM (e.g. parentElement). */
+export function getTargetAsElement(target) {
+ return target;
+}
+
+/**
+ * Syncs ref.current to React state after each commit so hooks can depend on the
+ * resolved element when ref object identity is stable but .current updates.
+ */
+function useResolvedRefTarget(targetRef) {
+ const [resolved, setResolved] = useState(() => getRefElement(targetRef));
+
+ // ref.current updates do not change targetRef identity; run after every commit
+ // to sync. Functional setState bails out when the resolved node is unchanged.
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- intentional post-commit ref sync
+ useLayoutEffect(() => {
+ const el = getRefElement(targetRef);
+ setResolved(prev => prev === el ? prev : el);
+ });
+ return resolved;
+}
export const useScrollingParentsEffect = (targetRef, setTargetRect) => {
+ const resolvedTarget = useResolvedRefTarget(targetRef);
useEffect(() => {
- if (!targetRef.current) {
- return;
- }
- const target = targetRef.current;
- const scrollingParents = findAllAdditionalScrollingParents(target);
+ if (!resolvedTarget) return;
+ const scrollingParents = findAllAdditionalScrollingParents(getTargetAsElement(resolvedTarget));
const updatePosition = () => {
- setTargetRect(targetRef?.current?.getBoundingClientRect());
+ setTargetRect(resolvedTarget.getBoundingClientRect());
};
const cleanup = [];
-
- // Add listeners to all scrolling parents (window scroll handled by useWindowScroll)
scrollingParents.forEach(parent => {
if (parent.addEventListener) {
parent.addEventListener('scroll', updatePosition, {
passive: true
@@ -23,39 +53,32 @@
});
return () => {
cleanup.forEach(fn => fn());
};
- }, [targetRef, setTargetRect]);
+ }, [resolvedTarget, setTargetRect]);
};
export const useResizingParentEffect = (targetRef, setTargetRect) => {
+ const resolvedTarget = useResolvedRefTarget(targetRef);
useEffect(() => {
- // handles movement of target within a clipped container e.g. Drawer
- if (!targetRef.current || typeof ResizeObserver === 'undefined') {
- return;
- }
- const resizingParent = findResizingParent(targetRef.current);
- if (!resizingParent?.addEventListener) {
- return;
- }
+ if (!resolvedTarget || typeof ResizeObserver === 'undefined') return;
+ const resizingParent = findResizingParent(getTargetAsElement(resolvedTarget));
+ if (!resizingParent?.addEventListener) return;
const handler = () => {
- setTargetRect(targetRef?.current?.getBoundingClientRect());
+ setTargetRect(resolvedTarget.getBoundingClientRect());
};
const ro = new ResizeObserver(handler);
ro.observe(resizingParent);
return () => ro.unobserve(resizingParent);
- }, [targetRef, setTargetRect]);
+ }, [resolvedTarget, setTargetRect]);
};
/**
* Memoizes the list of scrolling parent elements for a target element.
- * This avoids expensive DOM traversals and getComputedStyle calls on every render.
* Returns an empty array if the target element is not available.
*/
export const useScrollingParents = targetRef => {
+ const resolvedTarget = useResolvedRefTarget(targetRef);
return useMemo(() => {
- if (!targetRef.current) {
- return [];
- }
- return findAllAdditionalScrollingParents(targetRef.current);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [targetRef.current]);
+ if (!resolvedTarget) return [];
+ return findAllAdditionalScrollingParents(getTargetAsElement(resolvedTarget));
+ }, [resolvedTarget]);
};
\ No newline at end of file