diff --git a/.changeset/fuzzy-zoos-kiss.md b/.changeset/fuzzy-zoos-kiss.md new file mode 100644 index 00000000000..a7cd829f17b --- /dev/null +++ b/.changeset/fuzzy-zoos-kiss.md @@ -0,0 +1,6 @@ +--- +"@chakra-ui/media-query": patch +--- + +Fixed an issue where the hook `useBreakpoint` did not work as expected with +custom breakpoints diff --git a/.changeset/large-fireants-check.md b/.changeset/large-fireants-check.md new file mode 100644 index 00000000000..985d1ca6255 --- /dev/null +++ b/.changeset/large-fireants-check.md @@ -0,0 +1,6 @@ +--- +"@chakra-ui/utils": patch +--- + +Fixed an issue where `queryString()` created invalid media queries when min and +max were set. diff --git a/packages/media-query/src/create-media-query.ts b/packages/media-query/src/create-media-query.ts deleted file mode 100644 index 061c3b7ead9..00000000000 --- a/packages/media-query/src/create-media-query.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Dict, isNumber, StringOrNumber } from "@chakra-ui/utils" - -export default function createMediaQueries(breakpoints: Dict): MediaQuery[] { - return ( - Object.entries(breakpoints) - // sort css units in ascending order to ensure media queries are generated - // in the correct order and reference to each other correctly aswell - .sort((a, b) => - Number.parseInt(a[1], 10) > Number.parseInt(b[1], 10) ? 1 : -1, - ) - .map(([breakpoint, minWidth], index, arr) => { - // given a following breakpoint - const next = arr[index + 1] - // this breakpoint must end prior the threshold of the next - const maxWidth = next ? next[1] : undefined - const query = createMediaQueryString(minWidth, maxWidth) - - return { - minWidth, - maxWidth, - breakpoint, - query, - } - }) - ) -} - -/** - * Create a media query string from the breakpoints, - * using a combination of `min-width` and `max-width`. - */ -function createMediaQueryString(minWidth: string, maxWidth?: string) { - const hasMinWidth = parseInt(minWidth, 10) >= 0 - - if (!hasMinWidth && !maxWidth) { - return "" - } - - let query = `(min-width: ${toMediaString(minWidth)})` - - if (!maxWidth) { - return query - } - - if (query) { - query += " and " - } - - query += `(max-width: ${toMediaString(subtract(maxWidth))})` - - return query -} - -interface MediaQuery { - breakpoint: string - maxWidth?: string - minWidth: string - query: string -} - -const measurementRegex = /(\d+\.?\d*)/u - -const calculateMeasurement = ( - value: StringOrNumber, - modifier: number, -): string => { - if (typeof value === "number") { - return `${value + modifier}` - } - - return value.replace( - measurementRegex, - (match) => `${parseFloat(match) + modifier}`, - ) -} - -/** - * 0.01 and 0.1 are too small of a difference for `px` breakpoint values - * - * @see https://github.com/chakra-ui/chakra-ui/issues/2188#issuecomment-712774785 - */ -function subtract(value: string) { - return calculateMeasurement(value, value.endsWith("px") ? -1 : -0.01) -} - -/** - * Convert media query value to string - */ -function toMediaString(value: StringOrNumber) { - return isNumber(value) ? `${value}px` : value -} diff --git a/packages/media-query/src/use-breakpoint.ts b/packages/media-query/src/use-breakpoint.ts index 45a9f5b086d..b53ba9105ea 100644 --- a/packages/media-query/src/use-breakpoint.ts +++ b/packages/media-query/src/use-breakpoint.ts @@ -1,18 +1,6 @@ +import React from "react" import { useEnvironment } from "@chakra-ui/react-env" import { useTheme } from "@chakra-ui/system" -import React from "react" -import createMediaQueries from "./create-media-query" - -interface Listener { - mediaQuery: MediaQueryList - handleChange: () => void -} - -export interface Breakpoint { - breakpoint: string - maxWidth?: string - minWidth: string -} /** * React hook used to get the current responsive media breakpoint. @@ -24,89 +12,73 @@ export interface Breakpoint { * to get the default breakpoint value from the user-agent */ export function useBreakpoint(defaultBreakpoint?: string) { - const { breakpoints } = useTheme() + const { __breakpoints } = useTheme() const env = useEnvironment() - const mediaQueries = React.useMemo( - () => createMediaQueries({ base: "0px", ...breakpoints }), - [breakpoints], + const queries = React.useMemo( + () => + __breakpoints?.details.map(({ minMaxQuery, breakpoint }) => ({ + breakpoint, + query: minMaxQuery.replace("@media screen and ", ""), + })) ?? [], + [__breakpoints], ) const [currentBreakpoint, setCurrentBreakpoint] = React.useState(() => { if (env.window.matchMedia) { - let maxBreakpoint - mediaQueries.forEach(({ query, ...breakpoint }) => { - const mediaQuery = env.window.matchMedia(query) - if (mediaQuery.matches) { - maxBreakpoint = breakpoint - } - }) - if (maxBreakpoint) { - return maxBreakpoint - } + // set correct breakpoint on first render + const matchingBreakpointDetail = queries.find( + ({ query }) => env.window.matchMedia(query).matches, + ) + return matchingBreakpointDetail?.breakpoint } - if (!defaultBreakpoint) { - return undefined - } - - const mediaQuery = mediaQueries.find( - ({ breakpoint }) => breakpoint === defaultBreakpoint, - ) - - if (mediaQuery) { - const { query, ...breakpoint } = mediaQuery - return breakpoint + if (defaultBreakpoint) { + // use fallback if available + const fallbackBreakpointDetail = queries.find( + ({ breakpoint }) => breakpoint === defaultBreakpoint, + ) + return fallbackBreakpointDetail?.breakpoint } return undefined }) - const current = currentBreakpoint?.breakpoint + React.useEffect(() => { + const allUnregisterFns = queries.map(({ breakpoint, query }) => { + const mediaQueryList = env.window.matchMedia(query) - const update = React.useCallback( - (query: MediaQueryList, breakpoint: Breakpoint) => { - if (query.matches && current !== breakpoint.breakpoint) { + if (mediaQueryList.matches) { setCurrentBreakpoint(breakpoint) } - }, - [current], - ) - - React.useEffect(() => { - const listeners = new Set() - - mediaQueries.forEach(({ query, ...breakpoint }) => { - const mediaQuery = env.window.matchMedia(query) - // trigger an initial update to determine media query - update(mediaQuery, breakpoint) - - const handleChange = () => { - update(mediaQuery, breakpoint) + const handleChange = (ev: MediaQueryListEvent) => { + if (ev.matches) { + setCurrentBreakpoint(breakpoint) + } } - // add media query-listener - mediaQuery.addListener(handleChange) - - // push the media query list handleChange - // so we can use it to remove Listener - listeners.add({ mediaQuery, handleChange }) + // add media query listener + if (typeof mediaQueryList.addEventListener === "function") { + mediaQueryList.addEventListener("change", handleChange) + } else { + mediaQueryList.addListener(handleChange) + } + // return unregister fn return () => { - // clean up 1 - mediaQuery.removeListener(handleChange) + if (typeof mediaQueryList.removeEventListener === "function") { + mediaQueryList.removeEventListener("change", handleChange) + } else { + mediaQueryList.removeListener(handleChange) + } } }) return () => { - // clean up 2: for safety - listeners.forEach(({ mediaQuery, handleChange }) => { - mediaQuery.removeListener(handleChange) - }) - listeners.clear() + allUnregisterFns.forEach((unregister) => unregister()) } - }, [mediaQueries, breakpoints, update, env.window]) + }, [queries, __breakpoints, env.window]) - return current + return currentBreakpoint } diff --git a/packages/media-query/tests/create-media-query.test.ts b/packages/media-query/tests/create-media-query.test.ts deleted file mode 100644 index 6d1c710b76e..00000000000 --- a/packages/media-query/tests/create-media-query.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { createBreakpoints } from "@chakra-ui/theme-tools" -import { breakpoints } from "./test-data" -import createMediaQueries from "../src/create-media-query" - -test("creates media queries for each named breakpoint", () => { - expect(createMediaQueries(breakpoints)).toMatchInlineSnapshot(` - Array [ - Object { - "breakpoint": "base", - "maxWidth": "100px", - "minWidth": "0px", - "query": "(min-width: 0px) and (max-width: 99px)", - }, - Object { - "breakpoint": "sm", - "maxWidth": "200px", - "minWidth": "100px", - "query": "(min-width: 100px) and (max-width: 199px)", - }, - Object { - "breakpoint": "md", - "maxWidth": "300px", - "minWidth": "200px", - "query": "(min-width: 200px) and (max-width: 299px)", - }, - Object { - "breakpoint": "lg", - "maxWidth": "400px", - "minWidth": "300px", - "query": "(min-width: 300px) and (max-width: 399px)", - }, - Object { - "breakpoint": "xl", - "maxWidth": "500px", - "minWidth": "400px", - "query": "(min-width: 400px) and (max-width: 499px)", - }, - Object { - "breakpoint": "customBreakpoint", - "maxWidth": undefined, - "minWidth": "500px", - "query": "(min-width: 500px)", - }, - ] - `) -}) - -test("matches snapshot (order matters)", () => { - const breakpoints = createBreakpoints({ - customBreakpoint: "20em", - sm: "36em", - md: "46.25em", - lg: "60em", - xs: "30em", - xl: "78.125em", - xxl: "95em", - }) - - expect(createMediaQueries(breakpoints)).toMatchInlineSnapshot(` - Array [ - Object { - "breakpoint": "base", - "maxWidth": "20em", - "minWidth": "0em", - "query": "(min-width: 0em) and (max-width: 19.99em)", - }, - Object { - "breakpoint": "customBreakpoint", - "maxWidth": "30em", - "minWidth": "20em", - "query": "(min-width: 20em) and (max-width: 29.99em)", - }, - Object { - "breakpoint": "xs", - "maxWidth": "36em", - "minWidth": "30em", - "query": "(min-width: 30em) and (max-width: 35.99em)", - }, - Object { - "breakpoint": "sm", - "maxWidth": "46.25em", - "minWidth": "36em", - "query": "(min-width: 36em) and (max-width: 46.24em)", - }, - Object { - "breakpoint": "md", - "maxWidth": "60em", - "minWidth": "46.25em", - "query": "(min-width: 46.25em) and (max-width: 59.99em)", - }, - Object { - "breakpoint": "lg", - "maxWidth": "78.125em", - "minWidth": "60em", - "query": "(min-width: 60em) and (max-width: 78.115em)", - }, - Object { - "breakpoint": "xl", - "maxWidth": "95em", - "minWidth": "78.125em", - "query": "(min-width: 78.125em) and (max-width: 94.99em)", - }, - Object { - "breakpoint": "xxl", - "maxWidth": undefined, - "minWidth": "95em", - "query": "(min-width: 95em)", - }, - ] - `) -}) diff --git a/packages/utils/src/breakpoint.ts b/packages/utils/src/breakpoint.ts index fd4a39c8250..71bd58f82cd 100644 --- a/packages/utils/src/breakpoint.ts +++ b/packages/utils/src/breakpoint.ts @@ -44,11 +44,10 @@ function subtract(value: string) { } function queryString(min: string | null, max?: string) { - const query = [] + const query = ["@media screen"] - if (min) query.push(`@media screen and (min-width: ${px(min)})`) - if (query.length > 0 && max) query.push("and") - if (max) query.push(`@media screen and (max-width: ${px(max)})`) + if (min) query.push("and", `(min-width: ${px(min)})`) + if (max) query.push("and", `(max-width: ${px(max)})`) return query.join(" ") } diff --git a/packages/utils/tests/responsive.test.ts b/packages/utils/tests/responsive.test.ts index faf802ca793..ceba3efb5ed 100644 --- a/packages/utils/tests/responsive.test.ts +++ b/packages/utils/tests/responsive.test.ts @@ -83,7 +83,7 @@ test("should work correctly", () => { "breakpoint": "base", "maxW": "319px", "maxWQuery": "@media screen and (max-width: 319px)", - "minMaxQuery": "@media screen and (min-width: 0px) and @media screen and (max-width: 319px)", + "minMaxQuery": "@media screen and (min-width: 0px) and (max-width: 319px)", "minW": "0px", "minWQuery": "@media screen and (min-width: 0px)", }, @@ -91,7 +91,7 @@ test("should work correctly", () => { "breakpoint": "sm", "maxW": "639px", "maxWQuery": "@media screen and (max-width: 639px)", - "minMaxQuery": "@media screen and (min-width: 320px) and @media screen and (max-width: 639px)", + "minMaxQuery": "@media screen and (min-width: 320px) and (max-width: 639px)", "minW": "320px", "minWQuery": "@media screen and (min-width: 320px)", }, @@ -99,7 +99,7 @@ test("should work correctly", () => { "breakpoint": "md", "maxW": "999px", "maxWQuery": "@media screen and (max-width: 999px)", - "minMaxQuery": "@media screen and (min-width: 640px) and @media screen and (max-width: 999px)", + "minMaxQuery": "@media screen and (min-width: 640px) and (max-width: 999px)", "minW": "640px", "minWQuery": "@media screen and (min-width: 640px)", }, @@ -107,14 +107,14 @@ test("should work correctly", () => { "breakpoint": "lg", "maxW": "3999px", "maxWQuery": "@media screen and (max-width: 3999px)", - "minMaxQuery": "@media screen and (min-width: 1000px) and @media screen and (max-width: 3999px)", + "minMaxQuery": "@media screen and (min-width: 1000px) and (max-width: 3999px)", "minW": "1000px", "minWQuery": "@media screen and (min-width: 1000px)", }, Object { "breakpoint": "xl", "maxW": undefined, - "maxWQuery": "", + "maxWQuery": "@media screen", "minMaxQuery": "@media screen and (min-width: 4000px)", "minW": "4000px", "minWQuery": "@media screen and (min-width: 4000px)", @@ -140,7 +140,7 @@ test("should work with createBreakpoint output", () => { "breakpoint": "base", "maxW": "319px", "maxWQuery": "@media screen and (max-width: 319px)", - "minMaxQuery": "@media screen and (min-width: 0em) and @media screen and (max-width: 319px)", + "minMaxQuery": "@media screen and (min-width: 0em) and (max-width: 319px)", "minW": "0em", "minWQuery": "@media screen and (min-width: 0em)", }, @@ -148,7 +148,7 @@ test("should work with createBreakpoint output", () => { "breakpoint": "sm", "maxW": "639px", "maxWQuery": "@media screen and (max-width: 639px)", - "minMaxQuery": "@media screen and (min-width: 320px) and @media screen and (max-width: 639px)", + "minMaxQuery": "@media screen and (min-width: 320px) and (max-width: 639px)", "minW": "320px", "minWQuery": "@media screen and (min-width: 320px)", }, @@ -156,7 +156,7 @@ test("should work with createBreakpoint output", () => { "breakpoint": "md", "maxW": "999px", "maxWQuery": "@media screen and (max-width: 999px)", - "minMaxQuery": "@media screen and (min-width: 640px) and @media screen and (max-width: 999px)", + "minMaxQuery": "@media screen and (min-width: 640px) and (max-width: 999px)", "minW": "640px", "minWQuery": "@media screen and (min-width: 640px)", }, @@ -164,14 +164,14 @@ test("should work with createBreakpoint output", () => { "breakpoint": "lg", "maxW": "3999px", "maxWQuery": "@media screen and (max-width: 3999px)", - "minMaxQuery": "@media screen and (min-width: 1000px) and @media screen and (max-width: 3999px)", + "minMaxQuery": "@media screen and (min-width: 1000px) and (max-width: 3999px)", "minW": "1000px", "minWQuery": "@media screen and (min-width: 1000px)", }, Object { "breakpoint": "xl", "maxW": undefined, - "maxWQuery": "", + "maxWQuery": "@media screen", "minMaxQuery": "@media screen and (min-width: 4000px)", "minW": "4000px", "minWQuery": "@media screen and (min-width: 4000px)",