Refactor FancyCursor to use mouseover delegation
- Moves expensive DOM checks (`closest`) and class toggling from `mousemove` loop to `mouseover` handler. - Removes `document.elementFromPoint` fallback to eliminate layout thrashing. - Updates tests to verify performance improvements. - Fixes `Hero.test.tsx` missing jsdom environment.
This commit is contained in:
@@ -86,26 +86,22 @@ describe('FancyCursor', () => {
|
||||
document.body.removeChild(link);
|
||||
});
|
||||
|
||||
it('uses fallback elementFromPoint if hoveredElement is null', () => {
|
||||
it('does not call elementFromPoint on mousemove', () => {
|
||||
// Reset elementFromPoint mock
|
||||
const mockElementFromPoint = vi.fn();
|
||||
// @ts-ignore
|
||||
document.elementFromPoint = mockElementFromPoint;
|
||||
|
||||
// We need to remount to reset internal state if any.
|
||||
// However, 'hoveredElement' variable was defined INSIDE the component in my edit.
|
||||
// Let's double check that.
|
||||
|
||||
render(<FancyCursor />);
|
||||
|
||||
// Trigger mousemove WITHOUT prior mouseover.
|
||||
// This should trigger the fallback.
|
||||
// Trigger mousemove
|
||||
fireEvent.mouseMove(window, { clientX: 10, clientY: 10 });
|
||||
|
||||
act(() => {
|
||||
vi.runAllTimers();
|
||||
});
|
||||
|
||||
expect(mockElementFromPoint).toHaveBeenCalledWith(10, 10);
|
||||
// Should NOT be called in the optimized version
|
||||
expect(mockElementFromPoint).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,10 +19,24 @@ export const FancyCursor = memo(() => {
|
||||
if (!cursor) return;
|
||||
|
||||
let animationFrameId: number;
|
||||
let hoveredElement: Element | null = null;
|
||||
|
||||
const updateCursorState = (el: Element | null) => {
|
||||
const classList = cursor.classList;
|
||||
|
||||
// Coercing to boolean with `!!` is a micro-optimization.
|
||||
const isResize = !!el?.closest('#custom-resize-handle');
|
||||
const isText = !!el?.closest('input[type="text"], input[type="email"], textarea, [contenteditable="true"]');
|
||||
const isLink = !!el?.closest('a, button, [role="button"], input[type="submit"], input[type="button"]');
|
||||
|
||||
// These classes are toggled based on the hovered element.
|
||||
// The actual visual styles are defined in your CSS files.
|
||||
classList.toggle('resize-hover', isResize);
|
||||
classList.toggle('text-hover', isText && !isResize);
|
||||
classList.toggle('link-hover', isLink && !isText && !isResize);
|
||||
};
|
||||
|
||||
const handleMouseOver = (e: MouseEvent) => {
|
||||
hoveredElement = e.target as Element;
|
||||
updateCursorState(e.target as Element);
|
||||
};
|
||||
|
||||
// The mouse move handler is throttled with requestAnimationFrame to ensure
|
||||
@@ -35,25 +49,6 @@ export const FancyCursor = memo(() => {
|
||||
// This shifts the cursor element by half its own width and height,
|
||||
// which effectively centers it on the pointer without affecting the visuals.
|
||||
cursor.style.transform = `translate3d(${e.clientX}px, ${e.clientY}px, 0) translate(-50%, -50%)`;
|
||||
|
||||
// Use the cached hovered element if available, otherwise fallback to elementFromPoint
|
||||
// This fallback is mostly for the initial state before any mouseover events
|
||||
if (!hoveredElement) {
|
||||
hoveredElement = document.elementFromPoint(e.clientX, e.clientY);
|
||||
}
|
||||
const el = hoveredElement;
|
||||
const classList = cursor.classList;
|
||||
|
||||
// Coercing to boolean with `!!` is a micro-optimization.
|
||||
const isResize = !!el?.closest('#custom-resize-handle');
|
||||
const isText = !!el?.closest('input[type="text"], input[type="email"], textarea, [contenteditable="true"]');
|
||||
const isLink = !!el?.closest('a, button, [role="button"], input[type="submit"], input[type="button"]');
|
||||
|
||||
// These classes are toggled based on the hovered element.
|
||||
// The actual visual styles are defined in your CSS files.
|
||||
classList.toggle('resize-hover', isResize);
|
||||
classList.toggle('text-hover', isText && !isResize);
|
||||
classList.toggle('link-hover', isLink && !isText && !isResize);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @vitest-environment jsdom
|
||||
import { render, fireEvent, act, cleanup } from "@testing-library/react";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { Hero } from "./Hero";
|
||||
|
||||
Reference in New Issue
Block a user