Skip to content

Commit

Permalink
feat(use-media-query): no longer supports Safari < 14 (#1782)
Browse files Browse the repository at this point in the history
* fix: remove unused files

* refactor: update comment for the `calcMaxWidth` function

* feat: 🎸 useMediaQuery hook no longer supports Safari < 14

BREAKING CHANGE: 🧨 useMediaQuery hook no longer supports Safari < 14

* test: add `istanbul ignore` comment

* refactor(use-media-query): replace state with matches

* test: add `instnbul ignore`  comment
  • Loading branch information
mg901 authored Feb 8, 2024
1 parent dc472eb commit f50a5a1
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 112 deletions.
4 changes: 2 additions & 2 deletions shared/calc-max-width/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/**
* Calculates the maximum breakpoint width based on the provided minimum width value.
*
* The maximum value is calculated as the minimum of the next one less 0.02px
* to work around the limitations of `min-` and `max-` prefixes and viewports with fractional widths.
* The maximum value is reduced by 0.02px to work around the limitations of
* `min-` and `max-` prefixes and viewports with fractional widths.
* See https://www.w3.org/TR/mediaqueries-4/#mq-min-max
* Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.
* See https://bugs.webkit.org/show_bug.cgi?id=178261
Expand Down
135 changes: 50 additions & 85 deletions use-media-query/index.browser.spec.js
Original file line number Diff line number Diff line change
@@ -1,105 +1,70 @@
// @vitest-environment jsdom

import { describe, beforeEach, vi, it, expect } from 'vitest';
import { renderHook } from '@testing-library/react';
import { describe, beforeAll, beforeEach, vi, it, expect } from 'vitest';
import { configure, renderHook } from '@testing-library/react';
import { useMediaQuery } from './index';

describe('useMediaQuery hook', () => {
// Arrange
const matches = {
'(min-width: 500px)': true,
'(min-width: 1000px)': false,
};
let matches = null;

describe('In Safari Browser less than v14', () => {
// Arrange
beforeEach(() => {
window.matchMedia = vi.fn((query) => ({
matches: matches[query] || false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
}));
});
beforeAll(() => {
configure({ reactStrictMode: true });

it('returns true if media query matches', () => {
// Act
const { result } = renderHook(() => useMediaQuery('(min-width: 500px)'));

// Assert
expect(result.current).toBe(true);
});

it('returns false if media query does not match', () => {
// Act
const { result } = renderHook(() => useMediaQuery('(min-width: 1200px)'));

// Assert
expect(result.current).toBe(false);
});

it('updates when media query changes', () => {
// Act
const { result, rerender } = renderHook(
({ query }) => useMediaQuery(query),
{ initialProps: { query: '(min-width: 500px)' } }
);

// Assert
expect(result.current).toBe(true);

// Act
rerender({ query: '(max-width: 800px)' });

// Assert
expect(result.current).toBe(false);
});
matches = {
'(min-width: 500px)': true,
'(min-width: 1000px)': false,
};
});

describe('In other browsers', () => {
// Arrange
beforeEach(() => {
window.matchMedia = vi.fn((query) => ({
matches: matches[query] || false,
media: query,
onchange: null,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}));
});
// Arrange
beforeEach(() => {
window.matchMedia = vi.fn((query) => ({
matches: matches[query] || false,
media: query,
onchange: null,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}));
});

it('returns true if media query matches', () => {
// Act
const { result } = renderHook(() => useMediaQuery('(min-width: 500px)'));
it('returns true if media query matches', () => {
// Act
const { result, unmount } = renderHook(() =>
useMediaQuery('(min-width: 500px)')
);

// Assert
expect(result.current).toBe(true);
});
// Assert
expect(result.current).toBe(true);
unmount();
});

it('returns false if media query does not match', () => {
// Act
const { result } = renderHook(() => useMediaQuery('(min-width: 1200px)'));
it('returns false if media query does not match', () => {
// Act
const { result, unmount } = renderHook(() =>
useMediaQuery('(min-width: 1200px)')
);

// Assert
expect(result.current).toBe(false);
});
// Assert
expect(result.current).toBe(false);
unmount();
});

it('updates when media query changes', () => {
// Act
const { result, rerender } = renderHook(
({ query }) => useMediaQuery(query),
{ initialProps: { query: '(min-width: 500px)' } }
);
it('updates when media query changes', () => {
// Act
const { result, rerender, unmount } = renderHook(
({ query }) => useMediaQuery(query),
{ initialProps: { query: '(min-width: 500px)' } }
);

// Assert
expect(result.current).toBe(true);
// Assert
expect(result.current).toBe(true);

// Act
rerender({ query: '(max-width: 800px)' });
// Act
rerender({ query: '(max-width: 800px)' });

// Assert
expect(result.current).toBe(false);
});
// Assert
expect(result.current).toBe(false);
unmount();
});
});
39 changes: 14 additions & 25 deletions use-media-query/index.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
const { useState, useLayoutEffect, useEffect } = require('react');

exports.useMediaQuery = useMediaQuery;

/* istanbul ignore next */
const isBrowser = typeof window !== 'undefined';
const IS_BROWSER = typeof window !== 'undefined';
/* istanbul ignore next */
const useEnhancedEffect = isBrowser ? useLayoutEffect : useEffect;
const useEnhancedEffect = IS_BROWSER ? useLayoutEffect : useEffect;

exports.useMediaQuery = useMediaQuery;

/**
* Custom hook for handling media queries.
* @param {string} query - The media query to match.
* @returns {boolean} - `true` if the media query matches, otherwise `false`.
*/
exports.useMediaQuery = function useMediaQuery(query) {
const [isMatch, setIsMatch] = useState(isBrowser && getMatches(query));
function useMediaQuery(query) {
const [state, setState] = useState(IS_BROWSER && getInitialState(query));

useEnhancedEffect(() => {
/* istanbul ignore next */
if (!isBrowser) return;

let mounted = true;
const mediaQueryList = window.matchMedia(query.replace(/^@media\s*/, ''));

Expand All @@ -25,33 +26,21 @@ exports.useMediaQuery = function useMediaQuery(query) {
if (!mounted) return;

/* istanbul ignore next */
setIsMatch(mediaQueryList.matches);
setState(mediaQueryList.matches);
};

// Safari < 14 can't use addEventListener on a MediaQueryList
// https://developer.mozilla.org/en-US/docs/Web/API/MediaQueryList#Browser_compatibility
const listenerMethod = mediaQueryList.addListener
? 'addListener'
: 'addEventListener';
mediaQueryList[listenerMethod]('change', handleChange);
setState(mediaQueryList.matches);

setIsMatch(mediaQueryList.matches);

// eslint-disable-next-line consistent-return
return () => {
mounted = false;

if (mediaQueryList.addListener) {
mediaQueryList.removeListener(handleChange);
} else {
mediaQueryList.removeEventListener('change', handleChange);
}
mediaQueryList.removeEventListener('change', handleChange);
};
}, [query]);

return isMatch;
};
return state;
}

function getMatches(query) {
function getInitialState(query) {
return window.matchMedia(query).matches;
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1322,6 +1322,11 @@
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339"
integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==

"@types/unist@^2.0.0":
version "2.0.10"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc"
integrity sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==

"@typescript-eslint/[email protected]":
version "6.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1"
Expand Down

0 comments on commit f50a5a1

Please sign in to comment.