Skip to content

Commit 92d2ec8

Browse files
authored
Merge pull request #280 from rupeq/feat/use-device-pixel-ratio
[feat]: add useDevicePixelRatio hook
2 parents 65f76ba + ba44ad0 commit 92d2ec8

File tree

5 files changed

+156
-0
lines changed

5 files changed

+156
-0
lines changed

Diff for: src/hooks/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export * from './useDebounceValue/useDebounceValue';
1616
export * from './useDefault/useDefault';
1717
export * from './useDeviceMotion/useDeviceMotion';
1818
export * from './useDeviceOrientation/useDeviceOrientation';
19+
export * from './useDevicePixelRatio/useDevicePixelRatio';
1920
export * from './useDidUpdate/useDidUpdate';
2021
export * from './useDisclosure/useDisclosure';
2122
export * from './useDocumentEvent/useDocumentEvent';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { useDevicePixelRatio } from './useDevicePixelRatio';
2+
3+
const Demo = () => {
4+
const { supported, ratio } = useDevicePixelRatio();
5+
6+
if (!supported) {
7+
return <p>Device pixel ratio is not supported.</p>;
8+
}
9+
10+
return <p>Device pixel ratio (try to zoom page in and out): {ratio}</p>;
11+
};
12+
13+
export default Demo;
+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { act, renderHook } from '@testing-library/react';
2+
3+
import { useDevicePixelRatio } from './useDevicePixelRatio';
4+
5+
describe('useDevicePixelRatio', () => {
6+
describe('in unsupported environment', () => {
7+
afterEach(() => void vi.unstubAllGlobals());
8+
9+
it('should return supported as false when devicePixelRatio is not present', () => {
10+
vi.stubGlobal('devicePixelRatio', undefined);
11+
const { result } = renderHook(() => useDevicePixelRatio());
12+
expect(result.current.supported).toBeFalsy();
13+
expect(result.current.ratio).toBeNull();
14+
});
15+
16+
it('should return supported as false when matchMedia is not a function', () => {
17+
vi.stubGlobal('matchMedia', undefined);
18+
const { result } = renderHook(() => useDevicePixelRatio());
19+
expect(result.current.supported).toBeFalsy();
20+
expect(result.current.ratio).toBeNull();
21+
});
22+
});
23+
24+
describe('in supported environment', () => {
25+
const mediaQueryListMock = {
26+
matches: false,
27+
onchange: null,
28+
addListener: vi.fn(),
29+
removeListener: vi.fn(),
30+
addEventListener: vi.fn(),
31+
removeEventListener: vi.fn(),
32+
dispatchEvent: vi.fn()
33+
};
34+
35+
beforeEach(() => {
36+
vi.stubGlobal('devicePixelRatio', 2);
37+
vi.stubGlobal(
38+
'matchMedia',
39+
vi
40+
.fn<[string], MediaQueryList>()
41+
.mockImplementation((query) => ({ ...mediaQueryListMock, media: query }))
42+
);
43+
});
44+
45+
afterEach(() => void vi.unstubAllGlobals());
46+
47+
it('should return initial devicePixelRatio and supported', () => {
48+
const { result } = renderHook(() => useDevicePixelRatio());
49+
expect(result.current.supported).toBeTruthy();
50+
expect(result.current.ratio).toEqual(2);
51+
});
52+
53+
it('should return ratio when devicePixelRatio changes', () => {
54+
const { result } = renderHook(() => useDevicePixelRatio());
55+
expect(result.current.ratio).toEqual(2);
56+
57+
const listener = mediaQueryListMock.addEventListener.mock.calls[0][1];
58+
expect(typeof listener).toEqual('function');
59+
60+
Object.defineProperty(window, 'devicePixelRatio', {
61+
value: 3,
62+
configurable: true
63+
});
64+
65+
act(() => {
66+
listener(new MediaQueryListEvent('change'));
67+
});
68+
69+
expect(result.current.ratio).toEqual(3);
70+
});
71+
72+
it('should remove event listener on unmount', () => {
73+
const { unmount } = renderHook(() => useDevicePixelRatio());
74+
75+
expect(mediaQueryListMock.addEventListener).toHaveBeenCalledWith(
76+
'change',
77+
expect.any(Function)
78+
);
79+
80+
unmount();
81+
82+
expect(mediaQueryListMock.removeEventListener).toHaveBeenCalledWith(
83+
'change',
84+
expect.any(Function)
85+
);
86+
});
87+
});
88+
});

Diff for: src/hooks/useDevicePixelRatio/useDevicePixelRatio.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { useEffect, useState } from 'react';
2+
3+
export interface UseDevicePixelRatioReturn {
4+
/** The ratio of the resolution in physical pixels to the resolution in CSS pixels for the current display device. Equals to null if only `supported` = `false`. */
5+
ratio: number | null;
6+
/** Indicates if devicePixelRatio is available */
7+
supported: boolean;
8+
}
9+
10+
/**
11+
* @name useDevicePixelRatio
12+
* @description - Hook that returns the ratio of the resolution in physical pixels to the resolution in CSS pixels for the current display device
13+
* @category Display
14+
*
15+
* @returns {UseDevicePixelRatioReturn} The ratio and supported flag
16+
*
17+
* @example
18+
* const { supported, ratio } = useDevicePixelRatio();
19+
*/
20+
export const useDevicePixelRatio = (): UseDevicePixelRatioReturn => {
21+
const [ratio, setRatio] = useState<number | null>(null);
22+
23+
const supported =
24+
typeof window !== 'undefined' &&
25+
typeof window.matchMedia === 'function' &&
26+
typeof window.devicePixelRatio === 'number';
27+
28+
useEffect(() => {
29+
if (!supported) return;
30+
31+
const updatePixelRatio = () => setRatio(window.devicePixelRatio);
32+
33+
updatePixelRatio();
34+
35+
const media = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
36+
media.addEventListener('change', updatePixelRatio);
37+
return () => media.removeEventListener('change', updatePixelRatio);
38+
}, [supported]);
39+
40+
return { supported, ratio };
41+
};

Diff for: tests/setupTests.ts

+13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@ import { TextEncoder } from 'node:util';
22

33
globalThis.TextEncoder = TextEncoder;
44

5+
if (typeof window !== 'undefined' && typeof window.MediaQueryListEvent === 'undefined') {
6+
class MockMediaQueryListEvent extends Event {
7+
media: string;
8+
matches: boolean;
9+
constructor(type: string, eventInitDict?: MediaQueryListEventInit) {
10+
super(type, eventInitDict);
11+
this.media = eventInitDict?.media || '';
12+
this.matches = eventInitDict?.matches || false;
13+
}
14+
}
15+
16+
(window as any).MediaQueryListEvent = MockMediaQueryListEvent;
17+
518
if (typeof document !== 'undefined') {
619
const target = document.createElement('div');
720
target.id = 'target';

0 commit comments

Comments
 (0)