From b27c51f77eb41f6a3aee1e87c85ea7c8769b542f Mon Sep 17 00:00:00 2001 From: Andrei Miron Date: Fri, 17 Jul 2026 19:08:20 +0300 Subject: [PATCH] fix(hotspot): improve hover effect handling PIE-783 --- .../src/hotspot/__tests__/circle.test.jsx | 84 +++++++++---------- .../src/hotspot/__tests__/rectangle.test.jsx | 74 ++++++++-------- packages/hotspot/src/hotspot/circle.jsx | 25 +++--- packages/hotspot/src/hotspot/polygon.jsx | 31 ++++--- packages/hotspot/src/hotspot/rectangle.jsx | 26 +++--- 5 files changed, 118 insertions(+), 122 deletions(-) diff --git a/packages/hotspot/src/hotspot/__tests__/circle.test.jsx b/packages/hotspot/src/hotspot/__tests__/circle.test.jsx index 8ac04cba30..de0d08f719 100644 --- a/packages/hotspot/src/hotspot/__tests__/circle.test.jsx +++ b/packages/hotspot/src/hotspot/__tests__/circle.test.jsx @@ -70,7 +70,7 @@ describe('CircleComponent', () => { it('should render with correct position and radius', () => { const { getByTestId } = render(); const circle = getByTestId('circle'); - + expect(circle).toHaveAttribute('x', '50'); expect(circle).toHaveAttribute('y', '50'); expect(circle).toHaveAttribute('radius', '30'); @@ -79,21 +79,21 @@ describe('CircleComponent', () => { it('should render with hotspot color when not selected', () => { const { getByTestId } = render(); const circle = getByTestId('circle'); - + expect(circle).toHaveAttribute('fill', '#FF0000'); }); it('should render with selected color when selected', () => { const { getByTestId } = render(); const circle = getByTestId('circle'); - + expect(circle).toHaveAttribute('fill', '#00FF00'); }); it('should apply scale transform', () => { const { getByTestId } = render(); const group = getByTestId('group'); - + expect(group).toHaveAttribute('scaleX', '2'); expect(group).toHaveAttribute('scaleY', '2'); }); @@ -101,7 +101,7 @@ describe('CircleComponent', () => { it('should render with default scale of 1', () => { const { getByTestId } = render(); const group = getByTestId('group'); - + expect(group).toHaveAttribute('scaleX', '1'); expect(group).toHaveAttribute('scaleY', '1'); }); @@ -112,9 +112,9 @@ describe('CircleComponent', () => { const onClick = jest.fn(); const { getByTestId } = render(); const circle = getByTestId('circle'); - + fireEvent.click(circle); - + expect(onClick).toHaveBeenCalledWith({ id: 'circle1', selected: true, @@ -126,20 +126,20 @@ describe('CircleComponent', () => { const onClick = jest.fn(); const { getByTestId, rerender } = render(); const circle = getByTestId('circle'); - + fireEvent.click(circle); - + expect(onClick).toHaveBeenCalledWith({ id: 'circle1', selected: true, selector: 'Mouse', }); - + rerender(); - + const circleAfter = getByTestId('circle'); fireEvent.click(circleAfter); - + expect(onClick).toHaveBeenCalledWith({ id: 'circle1', selected: false, @@ -151,37 +151,37 @@ describe('CircleComponent', () => { const onClick = jest.fn(); const { getByTestId } = render(); const circle = getByTestId('circle'); - + fireEvent.click(circle); - + expect(onClick).not.toHaveBeenCalled(); }); it('should change cursor to pointer on mouse enter when not disabled', () => { const { getByTestId } = render(); const circle = getByTestId('circle'); - + fireEvent.mouseEnter(circle); - + expect(document.body.style.cursor).toBe('pointer'); }); it('should not change cursor when disabled', () => { const { getByTestId } = render(); const circle = getByTestId('circle'); - + fireEvent.mouseEnter(circle); - + expect(document.body.style.cursor).toBe('default'); }); it('should reset cursor to default on mouse leave', () => { const { getByTestId } = render(); const circle = getByTestId('circle'); - + fireEvent.mouseEnter(circle); fireEvent.mouseLeave(circle); - + expect(document.body.style.cursor).toBe('default'); }); }); @@ -190,9 +190,9 @@ describe('CircleComponent', () => { it('should show hover rect when hoverOutlineColor is provided', () => { const { container, getByTestId } = render(); const circle = getByTestId('circle'); - + fireEvent.mouseEnter(circle); - + const rects = container.querySelectorAll('[data-testid="rect"]'); expect(rects.length).toBeGreaterThan(0); }); @@ -208,9 +208,9 @@ describe('CircleComponent', () => { /> ); const circle = getByTestId('circle'); - + fireEvent.mouseEnter(circle); - + const rect = container.querySelector('[data-testid="rect"]'); if (rect) { // Rect should be positioned at (x - radius, y - radius) with width/height = radius * 2 @@ -226,12 +226,12 @@ describe('CircleComponent', () => { ); const circle = getByTestId('circle'); - + fireEvent.mouseEnter(circle); - + const rect = container.querySelector('[data-testid="rect"]'); if (rect) { - expect(rect).toHaveAttribute('stroke', 'transparent'); + expect(rect).toHaveAttribute('stroke', '#FFFF00'); } }); }); @@ -247,7 +247,7 @@ describe('CircleComponent', () => { showCorrectEnabled={false} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toBeInTheDocument(); }); @@ -262,7 +262,7 @@ describe('CircleComponent', () => { showCorrectEnabled={false} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toBeInTheDocument(); }); @@ -277,7 +277,7 @@ describe('CircleComponent', () => { showCorrectEnabled={false} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toBeInTheDocument(); }); @@ -292,7 +292,7 @@ describe('CircleComponent', () => { showCorrectEnabled={false} /> ); - + const icon = queryByTestId('icon-image'); expect(icon).not.toBeInTheDocument(); }); @@ -307,7 +307,7 @@ describe('CircleComponent', () => { showCorrectEnabled={true} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toBeInTheDocument(); }); @@ -322,7 +322,7 @@ describe('CircleComponent', () => { showCorrectEnabled={true} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toBeInTheDocument(); }); @@ -337,7 +337,7 @@ describe('CircleComponent', () => { showCorrectEnabled={true} /> ); - + const icon = queryByTestId('icon-image'); expect(icon).not.toBeInTheDocument(); }); @@ -352,7 +352,7 @@ describe('CircleComponent', () => { showCorrectEnabled={true} /> ); - + const icon = queryByTestId('icon-image'); expect(icon).not.toBeInTheDocument(); }); @@ -365,7 +365,7 @@ describe('CircleComponent', () => { markAsCorrect={true} /> ); - + const circle = getByTestId('circle'); expect(circle).toHaveAttribute('stroke', 'green'); }); @@ -379,7 +379,7 @@ describe('CircleComponent', () => { markAsCorrect={false} /> ); - + const circle = getByTestId('circle'); expect(circle).toHaveAttribute('stroke', 'red'); }); @@ -395,7 +395,7 @@ describe('CircleComponent', () => { showCorrectEnabled={false} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toHaveAttribute('data-tooltip', 'Great job!'); }); @@ -415,7 +415,7 @@ describe('CircleComponent', () => { showCorrectEnabled={false} /> ); - + const icon = getByTestId('icon-image'); // Icon should be at x - 10, y - 10 expect(icon).toHaveAttribute('data-x', '90'); @@ -431,7 +431,7 @@ describe('CircleComponent', () => { radius={5} /> ); - + const circle = getByTestId('circle'); expect(circle).toHaveAttribute('radius', '5'); }); @@ -443,7 +443,7 @@ describe('CircleComponent', () => { radius={200} /> ); - + const circle = getByTestId('circle'); expect(circle).toHaveAttribute('radius', '200'); }); @@ -455,7 +455,7 @@ describe('CircleComponent', () => { scale={0.5} /> ); - + const group = getByTestId('group'); expect(group).toHaveAttribute('scaleX', '0.5'); expect(group).toHaveAttribute('scaleY', '0.5'); diff --git a/packages/hotspot/src/hotspot/__tests__/rectangle.test.jsx b/packages/hotspot/src/hotspot/__tests__/rectangle.test.jsx index e865c9dbbe..183615fb3f 100644 --- a/packages/hotspot/src/hotspot/__tests__/rectangle.test.jsx +++ b/packages/hotspot/src/hotspot/__tests__/rectangle.test.jsx @@ -71,7 +71,7 @@ describe('RectComponent', () => { const { container } = render(); const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; - + expect(mainRect).toHaveAttribute('x', '10'); expect(mainRect).toHaveAttribute('y', '20'); expect(mainRect).toHaveAttribute('width', '100'); @@ -82,7 +82,7 @@ describe('RectComponent', () => { const { container } = render(); const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; - + expect(mainRect).toHaveAttribute('fill', '#FF0000'); }); @@ -90,14 +90,14 @@ describe('RectComponent', () => { const { container } = render(); const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; - + expect(mainRect).toHaveAttribute('fill', '#00FF00'); }); it('should apply scale transform', () => { const { getByTestId } = render(); const group = getByTestId('group'); - + expect(group).toHaveAttribute('scaleX', '1.5'); expect(group).toHaveAttribute('scaleY', '1.5'); }); @@ -105,7 +105,7 @@ describe('RectComponent', () => { it('should render with default scale of 1', () => { const { getByTestId } = render(); const group = getByTestId('group'); - + expect(group).toHaveAttribute('scaleX', '1'); expect(group).toHaveAttribute('scaleY', '1'); }); @@ -117,9 +117,9 @@ describe('RectComponent', () => { const { container } = render(); const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; - + fireEvent.click(mainRect); - + expect(onClick).toHaveBeenCalledWith({ id: 'rect1', selected: true, @@ -132,21 +132,21 @@ describe('RectComponent', () => { const { container, rerender } = render(); const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; - + fireEvent.click(mainRect); - + expect(onClick).toHaveBeenCalledWith({ id: 'rect1', selected: true, selector: 'Mouse', }); - + rerender(); - + const rectsAfter = container.querySelectorAll('[data-testid="rect"]'); const mainRectAfter = rectsAfter[rectsAfter.length - 1]; fireEvent.click(mainRectAfter); - + expect(onClick).toHaveBeenCalledWith({ id: 'rect1', selected: false, @@ -159,9 +159,9 @@ describe('RectComponent', () => { const { container } = render(); const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; - + fireEvent.click(mainRect); - + expect(onClick).not.toHaveBeenCalled(); }); @@ -169,9 +169,9 @@ describe('RectComponent', () => { const { container } = render(); const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; - + fireEvent.mouseEnter(mainRect); - + expect(document.body.style.cursor).toBe('pointer'); }); @@ -179,9 +179,9 @@ describe('RectComponent', () => { const { container } = render(); const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; - + fireEvent.mouseEnter(mainRect); - + expect(document.body.style.cursor).toBe('default'); }); @@ -189,10 +189,10 @@ describe('RectComponent', () => { const { container } = render(); const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; - + fireEvent.mouseEnter(mainRect); fireEvent.mouseLeave(mainRect); - + expect(document.body.style.cursor).toBe('default'); }); }); @@ -202,9 +202,9 @@ describe('RectComponent', () => { const { container } = render(); const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; - + fireEvent.mouseEnter(mainRect); - + const rectsAfterHover = container.querySelectorAll('[data-testid="rect"]'); expect(rectsAfterHover.length).toBeGreaterThan(1); }); @@ -215,12 +215,12 @@ describe('RectComponent', () => { ); const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; - + fireEvent.mouseEnter(mainRect); - + const hoverRect = container.querySelector('[stroke="#FFFF00"]'); if (hoverRect) { - expect(hoverRect).toHaveAttribute('stroke', 'transparent'); + expect(hoverRect).toHaveAttribute('stroke', '#FFFF00'); } }); }); @@ -236,7 +236,7 @@ describe('RectComponent', () => { showCorrectEnabled={false} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toBeInTheDocument(); expect(icon).toHaveAttribute('data-src'); @@ -252,7 +252,7 @@ describe('RectComponent', () => { showCorrectEnabled={false} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toBeInTheDocument(); }); @@ -267,7 +267,7 @@ describe('RectComponent', () => { showCorrectEnabled={false} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toBeInTheDocument(); }); @@ -282,7 +282,7 @@ describe('RectComponent', () => { showCorrectEnabled={false} /> ); - + const icon = queryByTestId('icon-image'); expect(icon).not.toBeInTheDocument(); }); @@ -297,7 +297,7 @@ describe('RectComponent', () => { showCorrectEnabled={true} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toBeInTheDocument(); }); @@ -312,7 +312,7 @@ describe('RectComponent', () => { showCorrectEnabled={true} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toBeInTheDocument(); }); @@ -327,7 +327,7 @@ describe('RectComponent', () => { showCorrectEnabled={true} /> ); - + const icon = queryByTestId('icon-image'); expect(icon).not.toBeInTheDocument(); }); @@ -342,7 +342,7 @@ describe('RectComponent', () => { showCorrectEnabled={true} /> ); - + const icon = queryByTestId('icon-image'); expect(icon).not.toBeInTheDocument(); }); @@ -355,7 +355,7 @@ describe('RectComponent', () => { markAsCorrect={true} /> ); - + const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; expect(mainRect).toHaveAttribute('stroke', 'green'); @@ -370,7 +370,7 @@ describe('RectComponent', () => { markAsCorrect={false} /> ); - + const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; expect(mainRect).toHaveAttribute('stroke', 'red'); @@ -387,7 +387,7 @@ describe('RectComponent', () => { showCorrectEnabled={false} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toHaveAttribute('data-tooltip', 'Correct answer!'); }); @@ -408,7 +408,7 @@ describe('RectComponent', () => { showCorrectEnabled={false} /> ); - + const icon = getByTestId('icon-image'); // Icon should be centered: x + width/2 - 10, y + height/2 - 10 expect(icon).toHaveAttribute('data-x', '50'); // 10 + 100/2 - 10 diff --git a/packages/hotspot/src/hotspot/circle.jsx b/packages/hotspot/src/hotspot/circle.jsx index d11928219c..82e0ca2d9b 100644 --- a/packages/hotspot/src/hotspot/circle.jsx +++ b/packages/hotspot/src/hotspot/circle.jsx @@ -92,17 +92,7 @@ class CircleComponent extends React.Component { const useHoveredStyle = (hovered || focused) && hoverOutlineColor; return ( - - {useHoveredStyle && ( - - )} + + {useHoveredStyle && ( + + )} {isEvaluateMode && iconSrc ? : null} ); diff --git a/packages/hotspot/src/hotspot/polygon.jsx b/packages/hotspot/src/hotspot/polygon.jsx index 27df355886..71027f273a 100644 --- a/packages/hotspot/src/hotspot/polygon.jsx +++ b/packages/hotspot/src/hotspot/polygon.jsx @@ -137,31 +137,30 @@ class PolygonComponent extends React.Component { const rectHeight = maxY - minY; return ( - - {useHoveredStyle && ( - - )} + + {useHoveredStyle && ( + + )} {isEvaluateMode && iconSrc ? : null} ); diff --git a/packages/hotspot/src/hotspot/rectangle.jsx b/packages/hotspot/src/hotspot/rectangle.jsx index 99108f4e6d..313dd31687 100644 --- a/packages/hotspot/src/hotspot/rectangle.jsx +++ b/packages/hotspot/src/hotspot/rectangle.jsx @@ -107,18 +107,7 @@ class RectComponent extends React.Component { const useHoveredStyle = (hovered || focused) && hoverOutlineColor; return ( - - {useHoveredStyle && ( - - )} + + {useHoveredStyle && ( + + )} {isEvaluateMode && iconSrc ? : null} );