diff --git a/src/__test__/index.test.tsx b/src/__test__/index.test.tsx index 24687c1..27dd497 100644 --- a/src/__test__/index.test.tsx +++ b/src/__test__/index.test.tsx @@ -170,3 +170,42 @@ describe('`style` is passed to rendered nodes and merged correctly', () => { expect(container.firstChild).toMatchSnapshot(); }); }); + +describe('Device Pixel Ratio Detection', () => { + test('QRCodeCanvas reacts to devicePixelRatio changes', async () => { + // Import act from testing library + const { act } = await import('@testing-library/react'); + + // Mock window.devicePixelRatio + const originalDevicePixelRatio = window.devicePixelRatio; + Object.defineProperty(window, 'devicePixelRatio', { + writable: true, + value: 1, + }); + + const {rerender} = render(); + + // Simulate zoom change by changing devicePixelRatio and triggering resize + Object.defineProperty(window, 'devicePixelRatio', { + writable: true, + value: 2, + }); + + // Trigger resize event to simulate zoom wrapped in act + await act(async () => { + window.dispatchEvent(new Event('resize')); + }); + + // Re-render to trigger effect + rerender(); + + // The component should handle the change gracefully + expect(window.devicePixelRatio).toBe(2); + + // Restore original value + Object.defineProperty(window, 'devicePixelRatio', { + writable: true, + value: originalDevicePixelRatio, + }); + }); +}); diff --git a/src/index.tsx b/src/index.tsx index b6dd37a..8c62956 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -384,6 +384,11 @@ const QRCodeCanvas = React.forwardRef( // eslint-disable-next-line @typescript-eslint/no-unused-vars const [isImgLoaded, setIsImageLoaded] = React.useState(false); + // Track devicePixelRatio to re-render when zoom changes + const [devicePixelRatio, setDevicePixelRatio] = React.useState( + () => (typeof window !== 'undefined' ? window.devicePixelRatio : 1) || 1 + ); + const {margin, cells, numCells, calculatedImageSettings} = useQRCode({ value, level, @@ -395,6 +400,23 @@ const QRCodeCanvas = React.forwardRef( size, }); + // Listen for resize events to detect zoom changes + React.useEffect(() => { + if (typeof window === 'undefined') return; + + const handleResize = () => { + const newPixelRatio = window.devicePixelRatio || 1; + if (newPixelRatio !== devicePixelRatio) { + setDevicePixelRatio(newPixelRatio); + } + }; + + window.addEventListener('resize', handleResize); + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [devicePixelRatio]); + React.useEffect(() => { // Always update the canvas. It's cheap enough and we want to be correct // with the current state. @@ -428,7 +450,7 @@ const QRCodeCanvas = React.forwardRef( // matches the number of cells. This avoids rounding issues, but does // result in some potentially unwanted single pixel issues between // blocks, only in environments that don't support Path2D. - const pixelRatio = window.devicePixelRatio || 1; + const pixelRatio = devicePixelRatio; canvas.height = canvas.width = size * pixelRatio; const scale = (size / numCells) * pixelRatio; ctx.scale(scale, scale); @@ -465,7 +487,17 @@ const QRCodeCanvas = React.forwardRef( ); } } - }); + }, [ + cells, + numCells, + size, + bgColor, + fgColor, + margin, + calculatedImageSettings, + isImgLoaded, + devicePixelRatio, + ]); // Ensure we mark image loaded as false here so we trigger updating the // canvas in our other effect.