Skip to content

Commit 7ff103c

Browse files
fix: safari 13 exception thrown in useScreenOrientation, add enabled option to disable the match
1 parent c82bc42 commit 7ff103c

File tree

5 files changed

+117
-13
lines changed

5 files changed

+117
-13
lines changed

src/useMediaQuery/index.dom.test.ts

+57-9
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
import {act, renderHook} from '@testing-library/react-hooks/dom';
2-
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
2+
import {type Mock, afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
33
import {useMediaQuery} from '../index.js';
44

55
describe('useMediaQuery', () => {
6-
const matchMediaMock = vi.fn((query: string) => ({
7-
matches: false,
8-
media: query,
9-
onchange: null,
10-
addEventListener: vi.fn(),
11-
removeEventListener: vi.fn(),
12-
dispatchEvent: vi.fn(),
13-
}));
6+
const matchMediaMock = vi.fn((query: string) => (
7+
query === '(orientation: unsupported)' ?
8+
undefined :
9+
{
10+
matches: false,
11+
media: query,
12+
onchange: null,
13+
addEventListener: vi.fn(),
14+
removeEventListener: vi.fn(),
15+
dispatchEvent: vi.fn(),
16+
}) as unknown as MediaQueryList & {
17+
matches: boolean;
18+
addEventListener: Mock;
19+
removeEventListener: Mock;
20+
dispatchEvent: Mock;
21+
});
1422

1523
vi.stubGlobal('matchMedia', matchMediaMock);
1624

@@ -30,6 +38,34 @@ describe('useMediaQuery', () => {
3038
expect(result.error).toBeUndefined();
3139
});
3240

41+
it('should return undefined and not thrown on unsupported when not enabled', () => {
42+
vi.stubGlobal('console', {
43+
error(error: string) {
44+
throw new Error(error);
45+
},
46+
});
47+
const {result, rerender, unmount} = renderHook(() => useMediaQuery('max-width : 768px', {enabled: false}));
48+
const {result: result2, rerender: rerender2, unmount: unmount2} = renderHook(() => useMediaQuery('(orientation: unsupported)', {enabled: false}));
49+
expect(result.error).toBeUndefined();
50+
expect(result.current).toBe(undefined);
51+
expect(result2.error).toBeUndefined();
52+
expect(result2.current).toBe(undefined);
53+
rerender('max-width : 768px');
54+
rerender2('(orientation: unsupported)');
55+
expect(result.error).toBeUndefined();
56+
expect(result.current).toBe(undefined);
57+
expect(result2.current).toBe(undefined);
58+
expect(result2.error).toBeUndefined();
59+
unmount();
60+
unmount2();
61+
expect(result.error).toBeUndefined();
62+
expect(result.current).toBe(undefined);
63+
expect(result2.error).toBeUndefined();
64+
expect(result2.current).toBe(undefined);
65+
vi.unstubAllGlobals();
66+
vi.stubGlobal('matchMedia', matchMediaMock);
67+
});
68+
3369
it('should return undefined on first render, if initializeWithValue is false', () => {
3470
const {result} = renderHook(() =>
3571
useMediaQuery('max-width : 768px', {initializeWithValue: false}));
@@ -147,4 +183,16 @@ describe('useMediaQuery', () => {
147183
unmount1();
148184
expect(mql.removeEventListener).toHaveBeenCalledTimes(1);
149185
});
186+
187+
it('should not throw when media query is not supported', () => {
188+
const {result, unmount, rerender} = renderHook(() => useMediaQuery('(orientation: unsupported)', {initializeWithValue: true}));
189+
expect(result.error).toBeUndefined();
190+
expect(result.current).toBe(undefined);
191+
rerender();
192+
expect(result.error).toBeUndefined();
193+
expect(result.current).toBe(undefined);
194+
unmount();
195+
expect(result.error).toBeUndefined();
196+
expect(result.current).toBe(undefined);
197+
});
150198
});

src/useMediaQuery/index.ssr.test.ts

+7
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,11 @@ describe('useMediaQuery', () => {
1818
useMediaQuery('max-width : 768px', {initializeWithValue: false}));
1919
expect(result.current).toBeUndefined();
2020
});
21+
22+
it('should return undefined on first render, when not enabled and initializeWithValue is set to true', () => {
23+
const {result} = renderHook(() =>
24+
useMediaQuery('max-width : 768px', {initializeWithValue: true, enabled: false}));
25+
expect(result.error).toBeUndefined();
26+
expect(result.current).toBeUndefined();
27+
});
2128
});

src/useMediaQuery/index.ts

+40-4
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,43 @@ import {isBrowser} from '../util/const.js';
33

44
const queriesMap = new Map<
55
string,
6-
{mql: MediaQueryList; dispatchers: Set<Dispatch<boolean>>; listener: () => void}
6+
{
7+
mql: MediaQueryList;
8+
dispatchers: Set<Dispatch<boolean>>;
9+
listener: () => void;
10+
}
711
>();
812

913
type QueryStateSetter = (matches: boolean) => void;
1014

1115
const createQueryEntry = (query: string) => {
1216
const mql = matchMedia(query);
17+
if (!mql) {
18+
if (
19+
typeof process === 'undefined' ||
20+
process.env === undefined ||
21+
process.env.NODE_ENV === 'development' ||
22+
process.env.NODE_ENV === 'test'
23+
) {
24+
console.error(`error: matchMedia('${query}') returned null, this means that the browser does not support this query or the query is invalid.`);
25+
}
26+
27+
return {
28+
mql: {
29+
onchange: null,
30+
matches: undefined as unknown as boolean,
31+
media: query,
32+
addEventListener: () => undefined as void,
33+
addListener: () => undefined as void,
34+
removeListener: () => undefined as void,
35+
removeEventListener: () => undefined as void,
36+
dispatchEvent: () => false as boolean,
37+
},
38+
dispatchers: new Set<Dispatch<boolean>>(),
39+
listener: () => undefined as void,
40+
};
41+
}
42+
1343
const dispatchers = new Set<QueryStateSetter>();
1444
const listener = () => {
1545
for (const d of dispatchers) {
@@ -61,6 +91,7 @@ const queryUnsubscribe = (query: string, setState: QueryStateSetter): void => {
6191

6292
type UseMediaQueryOptions = {
6393
initializeWithValue?: boolean;
94+
enabled?: boolean;
6495
};
6596

6697
/**
@@ -70,19 +101,20 @@ type UseMediaQueryOptions = {
70101
* @param options Hook options:
71102
* `initializeWithValue` (default: `true`) - Determine media query match state on first render. Setting
72103
* this to false will make the hook yield `undefined` on first render.
104+
* `enabled` (default: `true`) - Enable or disable the hook.
73105
*/
74106
export function useMediaQuery(
75107
query: string,
76108
options: UseMediaQueryOptions = {},
77109
): boolean | undefined {
78-
let {initializeWithValue = true} = options;
110+
let {initializeWithValue = true, enabled = true} = options;
79111

80112
if (!isBrowser) {
81113
initializeWithValue = false;
82114
}
83115

84116
const [state, setState] = useState<boolean | undefined>(() => {
85-
if (initializeWithValue) {
117+
if (initializeWithValue && enabled) {
86118
let entry = queriesMap.get(query);
87119
if (!entry) {
88120
entry = createQueryEntry(query);
@@ -94,12 +126,16 @@ export function useMediaQuery(
94126
});
95127

96128
useEffect(() => {
129+
if (!enabled) {
130+
return;
131+
}
132+
97133
querySubscribe(query, setState);
98134

99135
return () => {
100136
queryUnsubscribe(query, setState);
101137
};
102-
}, [query]);
138+
}, [query, enabled]);
103139

104140
return state;
105141
}

src/useScreenOrientation/index.ssr.test.ts

+7
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,11 @@ describe('useScreenOrientation', () => {
1111
const {result} = renderHook(() => useScreenOrientation({initializeWithValue: false}));
1212
expect(result.error).toBeUndefined();
1313
});
14+
15+
it('should return undefined on first render, when not enabled and initializeWithValue is set to true', () => {
16+
const {result} = renderHook(() =>
17+
useScreenOrientation({initializeWithValue: true, enabled: false}));
18+
expect(result.error).toBeUndefined();
19+
expect(result.current).toBeUndefined();
20+
});
1421
});

src/useScreenOrientation/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,25 @@ export type ScreenOrientation = 'portrait' | 'landscape';
44

55
type UseScreenOrientationOptions = {
66
initializeWithValue?: boolean;
7+
enabled?: boolean;
78
};
89

910
/**
1011
* Checks if screen is in `portrait` or `landscape` orientation.
1112
*
1213
* As `Screen Orientation API` is still experimental and not supported by Safari, this
1314
* hook uses CSS3 `orientation` media-query to check screen orientation.
15+
* @param options Hook options:
16+
* `initializeWithValue` (default: `true`) - Determine screen orientation on first render. Setting
17+
* this to false will make the hook yield `undefined` on first render.
18+
* `enabled` (default: `true`) - Enable or disable the hook.
1419
*/
1520
export function useScreenOrientation(
1621
options?: UseScreenOrientationOptions,
1722
): ScreenOrientation | undefined {
1823
const matches = useMediaQuery('(orientation: portrait)', {
1924
initializeWithValue: options?.initializeWithValue ?? true,
25+
enabled: options?.enabled,
2026
});
2127

2228
return matches === undefined ? undefined : (matches ? 'portrait' : 'landscape');

0 commit comments

Comments
 (0)