polen
Version:
A framework for delightful GraphQL developer portals
176 lines • 6.23 kB
JavaScript
/**
* State management for GraphQL token popovers
*
* Handles hover delays, pinning, and multiple popover coordination
*/
import { React as ReactHooks } from '#dep/react/index';
export const usePopoverState = (options = {}) => {
const { showDelay = 300, hideDelay = 200, allowMultiplePins = true, } = options;
const [hoveredId, setHoveredId] = ReactHooks.useState(null);
const [pinnedIds, setPinnedIds] = ReactHooks.useState(new Set());
const [pendingShowId, setPendingShowId] = ReactHooks.useState(null);
const [pendingHideId, setPendingHideId] = ReactHooks.useState(null);
// Timer refs
const showTimerRef = ReactHooks.useRef(null);
const hideTimerRef = ReactHooks.useRef(null);
// Clear any pending timers
const clearTimers = ReactHooks.useCallback(() => {
if (showTimerRef.current) {
clearTimeout(showTimerRef.current);
showTimerRef.current = null;
}
if (hideTimerRef.current) {
clearTimeout(hideTimerRef.current);
hideTimerRef.current = null;
}
setPendingShowId(null);
setPendingHideId(null);
}, []);
// Check if popover should be visible
const isOpen = ReactHooks.useCallback((id) => {
return hoveredId === id || pinnedIds.has(id);
}, [hoveredId, pinnedIds]);
// Check if popover is pinned
const isPinned = ReactHooks.useCallback((id) => {
return pinnedIds.has(id);
}, [pinnedIds]);
// Handle hover start
const onHoverStart = ReactHooks.useCallback((id) => {
// Don't show if already pinned
if (pinnedIds.has(id))
return;
// Cancel any pending hide for this ID
if (pendingHideId === id) {
clearTimeout(hideTimerRef.current);
hideTimerRef.current = null;
setPendingHideId(null);
return;
}
// Clear any existing timers
clearTimers();
// Set pending show
setPendingShowId(id);
// Start show timer
showTimerRef.current = setTimeout(() => {
setHoveredId(id);
setPendingShowId(null);
showTimerRef.current = null;
}, showDelay);
}, [pinnedIds, pendingHideId, clearTimers, showDelay]);
// Handle hover end
const onHoverEnd = ReactHooks.useCallback((id) => {
// Don't hide if pinned
if (pinnedIds.has(id))
return;
// Cancel any pending show
if (pendingShowId === id) {
clearTimeout(showTimerRef.current);
showTimerRef.current = null;
setPendingShowId(null);
return;
}
// Only hide if currently hovered
if (hoveredId === id) {
setPendingHideId(id);
hideTimerRef.current = setTimeout(() => {
setHoveredId(null);
setPendingHideId(null);
hideTimerRef.current = null;
}, hideDelay);
}
}, [pinnedIds, pendingShowId, hoveredId, hideDelay]);
// Handle popover content hover (cancels hide)
const onPopoverHover = ReactHooks.useCallback((id) => {
if (pendingHideId === id) {
clearTimeout(hideTimerRef.current);
hideTimerRef.current = null;
setPendingHideId(null);
}
}, [pendingHideId]);
// Handle popover content leave (starts hide timer)
const onPopoverLeave = ReactHooks.useCallback((id) => {
// Don't hide if pinned
if (pinnedIds.has(id))
return;
// Only hide if currently hovered
if (hoveredId === id) {
setPendingHideId(id);
hideTimerRef.current = setTimeout(() => {
setHoveredId(null);
setPendingHideId(null);
hideTimerRef.current = null;
}, hideDelay);
}
}, [pinnedIds, hoveredId, hideDelay]);
// Toggle pin state
const onTogglePin = ReactHooks.useCallback((id) => {
setPinnedIds(prev => {
const next = new Set(prev);
if (next.has(id)) {
// Unpin
next.delete(id);
}
else {
// Pin
if (!allowMultiplePins) {
// Clear other pins if multiple not allowed
next.clear();
}
next.add(id);
// Clear any hover state for this ID
if (hoveredId === id) {
setHoveredId(null);
}
// Clear any pending operations
if (pendingShowId === id) {
clearTimeout(showTimerRef.current);
showTimerRef.current = null;
setPendingShowId(null);
}
if (pendingHideId === id) {
clearTimeout(hideTimerRef.current);
hideTimerRef.current = null;
setPendingHideId(null);
}
}
return next;
});
}, [allowMultiplePins, hoveredId, pendingShowId, pendingHideId]);
// Unpin specific popover
const unpin = ReactHooks.useCallback((id) => {
setPinnedIds(prev => {
const next = new Set(prev);
next.delete(id);
return next;
});
}, []);
// Unpin all popovers
const unpinAll = ReactHooks.useCallback(() => {
setPinnedIds(new Set());
}, []);
// Cleanup timers on unmount and when dependencies change
ReactHooks.useEffect(() => {
return () => {
if (showTimerRef.current)
clearTimeout(showTimerRef.current);
if (hideTimerRef.current)
clearTimeout(hideTimerRef.current);
};
}, []);
// Clear timers when delay options change to prevent stale timers
ReactHooks.useEffect(() => {
clearTimers();
}, [showDelay, hideDelay, clearTimers]);
return {
isOpen,
isPinned,
onHoverStart,
onHoverEnd,
onTogglePin,
onPopoverHover,
onPopoverLeave,
unpin,
unpinAll,
};
};
//# sourceMappingURL=use-popover-state.js.map