[skip ci] feat: prepare for public release

This commit is contained in:
Hampus Kraft committed 2026-01-02 19:27:51 +00:00
1 parent 197b23757f
commit 5ae825fc7d
199 files changed
+38391 -33358

No files matched your search

+50 -11
View File
@@ -65,10 +65,9 @@ export function useAntiShiftFloating(
} = options;
const floatingRef = React.useRef<HTMLElement>(null);
const [state, setState] = React.useState<FloatingState>({
x: -9999,
y: -9999,
isReady: false,
const [state, setState] = React.useState<FloatingState>(() => {
const {x, y} = target ? getInitialGuess(target, placement, offsetMainAxis, offsetCrossAxis) : {x: -9999, y: -9999};
return {x, y, isReady: false};
});
const cleanupRef = React.useRef<(() => void) | null>(null);
const isCalculatingRef = React.useRef(false);
@@ -93,13 +92,6 @@ export function useAntiShiftFloating(
return;
}
Object.assign(floating.style, {
visibility: 'hidden',
position: 'fixed',
left: '-9999px',
top: '-9999px',
});
const adaptivePadding = enableSmartBoundary ? getAdaptivePadding() : 8;
const shiftPadding = Math.max(6, adaptivePadding);
@@ -207,3 +199,50 @@ export function useAntiShiftFloating(
updatePosition,
};
}
function getInitialGuess(target: HTMLElement, placement: Placement, offsetMainAxis: number, offsetCrossAxis: number) {
const rect = target.getBoundingClientRect();
const [side, align = 'center'] = placement.split('-') as [string, string];
let x = rect.left;
let y = rect.top;
switch (side) {
case 'right':
x = rect.right + offsetMainAxis;
break;
case 'left':
x = rect.left - offsetMainAxis;
break;
case 'bottom':
y = rect.bottom + offsetMainAxis;
break;
case 'top':
y = rect.top - offsetMainAxis;
break;
default:
break;
}
if (side === 'top' || side === 'bottom') {
if (align === 'end') {
x = rect.right;
} else if (align === 'start') {
x = rect.left;
} else {
x = rect.left + rect.width / 2;
}
x += offsetCrossAxis;
} else {
if (align === 'end') {
y = rect.bottom;
} else if (align === 'start') {
y = rect.top;
} else {
y = rect.top + rect.height / 2;
}
y += offsetCrossAxis;
}
return {x, y};
}
+52 -11
View File
@@ -34,6 +34,7 @@ export function useReactionTooltip(hoverDelay = 500) {
const hoverTimerRef = React.useRef<NodeJS.Timeout | null>(null);
const closeTimerRef = React.useRef<NodeJS.Timeout | null>(null);
const isCalculatingRef = React.useRef(false);
const lastPointerRef = React.useRef<{x: number; y: number} | null>(null);
const [state, setState] = React.useState<ReactionTooltipState>({
x: 0,
@@ -58,6 +59,17 @@ export function useReactionTooltip(hoverDelay = 500) {
return !!targetRef.current?.contains(node) || !!tooltipRef.current?.contains(node);
}, []);
const updateLastPointer = React.useCallback((event: MouseEvent | React.MouseEvent | PointerEvent) => {
lastPointerRef.current = {x: event.clientX, y: event.clientY};
}, []);
const shouldKeepOpen = React.useCallback(() => {
const lastPointer = lastPointerRef.current;
if (!lastPointer) return false;
const element = document.elementFromPoint(lastPointer.x, lastPointer.y);
return isInHoverRegion(element);
}, [isInHoverRegion]);
const updatePosition = React.useCallback(async () => {
if (!state.isOpen || !targetRef.current || !tooltipRef.current || isCalculatingRef.current) {
return;
@@ -110,6 +122,15 @@ export function useReactionTooltip(hoverDelay = 500) {
setState({x: 0, y: 0, isOpen: false, isReady: false});
}, [clearTimers]);
const scheduleHide = React.useCallback(() => {
closeTimerRef.current = setTimeout(() => {
if (shouldKeepOpen()) {
return;
}
hide();
}, 100);
}, [hide, shouldKeepOpen]);
const handleMouseEnter = React.useCallback(() => {
clearTimers();
if (state.isOpen) return;
@@ -121,35 +142,37 @@ export function useReactionTooltip(hoverDelay = 500) {
const handleMouseLeave = React.useCallback(
(event: React.MouseEvent) => {
clearTimers();
updateLastPointer(event);
if (isInHoverRegion(event.relatedTarget)) {
return;
}
closeTimerRef.current = setTimeout(() => {
hide();
}, 100);
scheduleHide();
},
[hide, clearTimers],
[clearTimers, isInHoverRegion, scheduleHide, updateLastPointer],
);
const handleTooltipMouseEnter = React.useCallback(() => {
clearTimers();
}, [clearTimers]);
const handleTooltipMouseEnter = React.useCallback(
(event: React.MouseEvent) => {
clearTimers();
updateLastPointer(event);
},
[clearTimers, updateLastPointer],
);
const handleTooltipMouseLeave = React.useCallback(
(event: React.MouseEvent) => {
clearTimers();
updateLastPointer(event);
if (isInHoverRegion(event.relatedTarget)) {
return;
}
closeTimerRef.current = setTimeout(() => {
hide();
}, 100);
scheduleHide();
},
[hide, clearTimers, isInHoverRegion],
[clearTimers, isInHoverRegion, scheduleHide, updateLastPointer],
);
React.useEffect(() => {
@@ -183,6 +206,24 @@ export function useReactionTooltip(hoverDelay = 500) {
};
}, [state.isOpen, updatePosition]);
React.useEffect(() => {
if (!state.isOpen) {
return;
}
const handlePointerMove = (event: PointerEvent | MouseEvent) => {
lastPointerRef.current = {x: event.clientX, y: event.clientY};
};
window.addEventListener('pointermove', handlePointerMove, {passive: true});
window.addEventListener('mousemove', handlePointerMove, {passive: true});
return () => {
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('mousemove', handlePointerMove);
};
}, [state.isOpen]);
React.useEffect(() => {
return () => {
clearTimers();