diff --git a/shared/calc-max-width/index.js b/shared/calc-max-width/index.js index 5b7f6ad4..098c52a6 100644 --- a/shared/calc-max-width/index.js +++ b/shared/calc-max-width/index.js @@ -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 diff --git a/use-media-query/index.browser.spec.js b/use-media-query/index.browser.spec.js index 371ce27b..b689181d 100644 --- a/use-media-query/index.browser.spec.js +++ b/use-media-query/index.browser.spec.js @@ -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(); }); }); diff --git a/use-media-query/index.js b/use-media-query/index.js index 8869bb9d..181296b4 100644 --- a/use-media-query/index.js +++ b/use-media-query/index.js @@ -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*/, '')); @@ -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; } diff --git a/yarn.lock b/yarn.lock index 04ac9807..7c7d9259 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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/scope-manager@6.21.0": version "6.21.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1"