Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: safari 13 exception thrown in useScreenOrientation, add enabled … #1

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 57 additions & 9 deletions src/useMediaQuery/index.dom.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import {act, renderHook} from '@testing-library/react-hooks/dom';
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
import {type Mock, afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
import {useMediaQuery} from '../index.js';

describe('useMediaQuery', () => {
const matchMediaMock = vi.fn((query: string) => ({
matches: false,
media: query,
onchange: null,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
}));
const matchMediaMock = vi.fn((query: string) => (
query === '(orientation: unsupported)' ?
undefined :
{
matches: false,
media: query,
onchange: null,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
}) as unknown as MediaQueryList & {
matches: boolean;
addEventListener: Mock;
removeEventListener: Mock;
dispatchEvent: Mock;
});

vi.stubGlobal('matchMedia', matchMediaMock);

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

it('should return undefined and not thrown on unsupported when not enabled', () => {
vi.stubGlobal('console', {
error(error: string) {
throw new Error(error);
},
});
const {result, rerender, unmount} = renderHook(() => useMediaQuery('max-width : 768px', {enabled: false}));
const {result: result2, rerender: rerender2, unmount: unmount2} = renderHook(() => useMediaQuery('(orientation: unsupported)', {enabled: false}));
expect(result.error).toBeUndefined();
expect(result.current).toBe(undefined);
expect(result2.error).toBeUndefined();
expect(result2.current).toBe(undefined);
rerender('max-width : 768px');
rerender2('(orientation: unsupported)');
expect(result.error).toBeUndefined();
expect(result.current).toBe(undefined);
expect(result2.current).toBe(undefined);
expect(result2.error).toBeUndefined();
unmount();
unmount2();
expect(result.error).toBeUndefined();
expect(result.current).toBe(undefined);
expect(result2.error).toBeUndefined();
expect(result2.current).toBe(undefined);
vi.unstubAllGlobals();
vi.stubGlobal('matchMedia', matchMediaMock);
});

it('should return undefined on first render, if initializeWithValue is false', () => {
const {result} = renderHook(() =>
useMediaQuery('max-width : 768px', {initializeWithValue: false}));
Expand Down Expand Up @@ -147,4 +183,16 @@ describe('useMediaQuery', () => {
unmount1();
expect(mql.removeEventListener).toHaveBeenCalledTimes(1);
});

it('should not throw when media query is not supported', () => {
const {result, unmount, rerender} = renderHook(() => useMediaQuery('(orientation: unsupported)', {initializeWithValue: true}));
expect(result.error).toBeUndefined();
expect(result.current).toBe(undefined);
rerender();
expect(result.error).toBeUndefined();
expect(result.current).toBe(undefined);
unmount();
expect(result.error).toBeUndefined();
expect(result.current).toBe(undefined);
});
});
7 changes: 7 additions & 0 deletions src/useMediaQuery/index.ssr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,11 @@ describe('useMediaQuery', () => {
useMediaQuery('max-width : 768px', {initializeWithValue: false}));
expect(result.current).toBeUndefined();
});

it('should return undefined on first render, when not enabled and initializeWithValue is set to true', () => {
const {result} = renderHook(() =>
useMediaQuery('max-width : 768px', {initializeWithValue: true, enabled: false}));
expect(result.error).toBeUndefined();
expect(result.current).toBeUndefined();
});
});
44 changes: 40 additions & 4 deletions src/useMediaQuery/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,43 @@ import {isBrowser} from '../util/const.js';

const queriesMap = new Map<
string,
{mql: MediaQueryList; dispatchers: Set<Dispatch<boolean>>; listener: () => void}
{
mql: MediaQueryList;
dispatchers: Set<Dispatch<boolean>>;
listener: () => void;
}
>();

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

const createQueryEntry = (query: string) => {
const mql = matchMedia(query);
if (!mql) {
if (
typeof process === 'undefined' ||
process.env === undefined ||
process.env.NODE_ENV === 'development' ||
process.env.NODE_ENV === 'test'
) {
console.error(`error: matchMedia('${query}') returned null, this means that the browser does not support this query or the query is invalid.`);
}

return {
mql: {
onchange: null,
matches: undefined as unknown as boolean,
media: query,
addEventListener: () => undefined as void,
addListener: () => undefined as void,
removeListener: () => undefined as void,
removeEventListener: () => undefined as void,
dispatchEvent: () => false as boolean,
},
dispatchers: new Set<Dispatch<boolean>>(),
listener: () => undefined as void,
};
}

const dispatchers = new Set<QueryStateSetter>();
const listener = () => {
for (const d of dispatchers) {
Expand Down Expand Up @@ -61,6 +91,7 @@ const queryUnsubscribe = (query: string, setState: QueryStateSetter): void => {

type UseMediaQueryOptions = {
initializeWithValue?: boolean;
enabled?: boolean;
};

/**
Expand All @@ -70,19 +101,20 @@ type UseMediaQueryOptions = {
* @param options Hook options:
* `initializeWithValue` (default: `true`) - Determine media query match state on first render. Setting
* this to false will make the hook yield `undefined` on first render.
* `enabled` (default: `true`) - Enable or disable the hook.
*/
export function useMediaQuery(
query: string,
options: UseMediaQueryOptions = {},
): boolean | undefined {
let {initializeWithValue = true} = options;
let {initializeWithValue = true, enabled = true} = options;

if (!isBrowser) {
initializeWithValue = false;
}

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

useEffect(() => {
if (!enabled) {
return;
}

querySubscribe(query, setState);

return () => {
queryUnsubscribe(query, setState);
};
}, [query]);
}, [query, enabled]);

return state;
}
7 changes: 7 additions & 0 deletions src/useScreenOrientation/index.ssr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,11 @@ describe('useScreenOrientation', () => {
const {result} = renderHook(() => useScreenOrientation({initializeWithValue: false}));
expect(result.error).toBeUndefined();
});

it('should return undefined on first render, when not enabled and initializeWithValue is set to true', () => {
const {result} = renderHook(() =>
useScreenOrientation({initializeWithValue: true, enabled: false}));
expect(result.error).toBeUndefined();
expect(result.current).toBeUndefined();
});
});
6 changes: 6 additions & 0 deletions src/useScreenOrientation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,25 @@ export type ScreenOrientation = 'portrait' | 'landscape';

type UseScreenOrientationOptions = {
initializeWithValue?: boolean;
enabled?: boolean;
};

/**
* Checks if screen is in `portrait` or `landscape` orientation.
*
* As `Screen Orientation API` is still experimental and not supported by Safari, this
* hook uses CSS3 `orientation` media-query to check screen orientation.
* @param options Hook options:
* `initializeWithValue` (default: `true`) - Determine screen orientation on first render. Setting
* this to false will make the hook yield `undefined` on first render.
* `enabled` (default: `true`) - Enable or disable the hook.
*/
export function useScreenOrientation(
options?: UseScreenOrientationOptions,
): ScreenOrientation | undefined {
const matches = useMediaQuery('(orientation: portrait)', {
initializeWithValue: options?.initializeWithValue ?? true,
enabled: options?.enabled,
});

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