Skip to content

Commit

Permalink
feat: introduce useMediaUp and useMediaDown (#23)
Browse files Browse the repository at this point in the history
Introduce useMediaUp and useMediaDown. Both accept a breakpoint and calculate if the current window width is up/down the breakpoint requirements. Uses window.matchMedia to validate the size and ResizeObserver to update the value on page resize.

closes #22
  • Loading branch information
dstoyanoff committed May 10, 2023
1 parent 03cebd7 commit aacd612
Show file tree
Hide file tree
Showing 12 changed files with 137 additions and 22 deletions.
8 changes: 4 additions & 4 deletions codecov.yml
Expand Up @@ -6,15 +6,15 @@ comment:
show_carryforward_flags: false
coverage:
precision: 2
range:
- 70.0
- 100.0
range: 70..1000
round: down
status:
changes: false
default_rules:
flag_coverage_not_uploaded_behavior: include
patch: true
project: true
project:
default:
target: 90%
github_checks:
annotations: true
13 changes: 9 additions & 4 deletions example/pages/index.tsx
@@ -1,6 +1,6 @@
/* eslint-disable */
import styled from "@emotion/styled";
import { Box, Flex, Grid, Typography } from "e-prim";
import { Box, Flex, Grid, Typography, useMediaDown, useMediaUp } from "e-prim";
import { ArrowIcon } from "../arrow-icon";
import { Button } from "../button";

Expand All @@ -9,8 +9,11 @@ const StyledBox = styled(Box)(({ theme: { palette } }) => ({
}));

export default function () {
const isUpMd = useMediaUp("md");
const isDownMd = useMediaDown("md");

return (
<Flex direction="column" gap={4} p={4} width="100%" m={{ xs: 2, md: 4 }}>
<Flex direction="column" gap={4} p={4} m={{ xs: 2, md: 4 }}>
<Typography variant="title.1">e-prim</Typography>

<Typography variant="body.1" as="p">
Expand All @@ -26,8 +29,7 @@ export default function () {
onClick={console.log}
p={4}
shadow="xl"
css={({ breakpoint, palette }) => ({
width: breakpoint.md,
css={({ palette }) => ({
color: palette.neutral[0],
})}
>
Expand Down Expand Up @@ -58,6 +60,9 @@ export default function () {
md: [1, 100],
}}
/>

{isUpMd && <Button>Desktop Button</Button>}
{isDownMd && <Button>Mobile Button</Button>}
</Flex>
);
}
1 change: 0 additions & 1 deletion example/styles.tsx
Expand Up @@ -11,7 +11,6 @@ export const GlobalStyles = () => {
padding: 0,
background: palette.neutral[7],
color: palette.neutral[0],
width: "100vh",
},
})}
/>
Expand Down
15 changes: 3 additions & 12 deletions lib/hooks/__tests__/use-color-by-key.test.tsx
Expand Up @@ -3,22 +3,13 @@
*/

import { renderHook } from "@testing-library/react";
import { wrapper } from "@/utils/mock-theme";
import { useColorByKey } from "../use-color-by-key";

jest.mock("@emotion/react", () => ({
useTheme: () => ({
palette: {
primary: {
normal: "#123456",
},
},
}),
}));

describe("useColorByKey", () => {
test("should resolve color from the theme by key", () => {
const hook = renderHook(() => useColorByKey("primary.normal"));
const hook = renderHook(() => useColorByKey("primary.normal"), { wrapper });

expect(hook.result.current).toEqual("#123456");
expect(hook.result.current).toEqual("#00659e");
});
});
60 changes: 60 additions & 0 deletions lib/hooks/__tests__/use-media.test.tsx
@@ -0,0 +1,60 @@
/**
* @jest-environment jsdom
*/

import { renderHook } from "@testing-library/react";
import { setMedia } from "mock-match-media";
import { wrapper } from "@/utils/mock-theme";
import { useMediaDown, useMediaUp } from "../use-media";

describe("useMediaUp", () => {
test("should return true if the window width is more than the breakpoint value", async () => {
setMedia({
width: "600px",
type: "screen",
orientation: "landscape"
});

const hook = renderHook(() => useMediaUp("md"), { wrapper });

expect(hook.result.current).toEqual(true);
});

test("should return false if the window width is less than the breakpoint value", async () => {
setMedia({
width: "400px",
type: "screen",
orientation: "landscape"
});

const hook = renderHook(() => useMediaUp("md"), { wrapper });

expect(hook.result.current).toEqual(false);
});
});

describe("useMediaDown", () => {
test("should return true if the window width is less than the breakpoint value", async () => {
setMedia({
width: "400px",
type: "screen",
orientation: "landscape"
});

const hook = renderHook(() => useMediaDown("md"), { wrapper });

expect(hook.result.current).toEqual(true);
});

test("should return false if the window width is less than the breakpoint value", async () => {
setMedia({
width: "600px",
type: "screen",
orientation: "landscape"
});

const hook = renderHook(() => useMediaDown("md"), { wrapper });

expect(hook.result.current).toEqual(false);
});
});
1 change: 1 addition & 0 deletions lib/hooks/index.ts
@@ -1 +1,2 @@
export * from "./use-color-by-key";
export * from "./use-media";
34 changes: 34 additions & 0 deletions lib/hooks/use-media.ts
@@ -0,0 +1,34 @@
import { useCallback, useEffect, useState } from "react";
import { useTheme } from "@emotion/react";
import { TBreakpoint } from "@/theme";

const useMatchMedia = (breakpoint: keyof TBreakpoint, queryPrefix: string) => {
const [isMatch, setIsMatch] = useState(false);
const { breakpoint: themeBreakpoint } = useTheme();

const query = `(${queryPrefix}: ${themeBreakpoint[breakpoint]}px)`;

const calculate = useCallback(() => setIsMatch(window.matchMedia(query).matches), [query]);

useEffect(() => {
calculate();
}, [calculate]);

useEffect(() => {
const observer = new ResizeObserver(() => {
calculate();
});

observer.observe(document.body);

return () => {
observer.unobserve(document.body);
};
}, [calculate]);

return isMatch;
}

export const useMediaUp = (breakpoint: keyof TBreakpoint) => useMatchMedia(breakpoint, "min-width");

export const useMediaDown = (breakpoint: keyof TBreakpoint) => useMatchMedia(breakpoint, "max-width");
4 changes: 4 additions & 0 deletions lib/jest-setup.ts
@@ -0,0 +1,4 @@
import "mock-match-media/jest-setup";

global.ResizeObserver = require('resize-observer-polyfill')

1 change: 1 addition & 0 deletions lib/jest.config.ts
Expand Up @@ -18,6 +18,7 @@ const config = {
testPathIgnorePatterns: ["<rootDir>/node_modules/", "<rootDir>/.next/"],
testEnvironment: "node",
moduleNameMapper: pathsToModuleNameMapper(tsConfig.compilerOptions.paths, { prefix: "<rootDir>" }),
setupFiles: ["./jest-setup.ts"],

transform: {
"^.+\\.(js|jsx|ts|tsx)$": [
Expand Down
3 changes: 2 additions & 1 deletion lib/package.json
Expand Up @@ -7,6 +7,7 @@
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"homepage": "https://dstoyanoff.github.io/e-prim",
"files": [
"dist/**/*",
"README.md"
Expand All @@ -30,4 +31,4 @@
"type": "git",
"url": "https://github.com/dstoyanoff/emotion-primitive.git"
}
}
}
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -30,8 +30,10 @@
"eslint-plugin-import": "^2.26.0",
"jest": "^29.3.1",
"jest-environment-jsdom": "^29.3.1",
"mock-match-media": "^0.4.2",
"react": "^18.2.0",
"react-dom": "18.2.0",
"resize-observer-polyfill": "^1.5.1",
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1",
"tsup": "^6.4.0",
Expand Down
17 changes: 17 additions & 0 deletions yarn.lock
Expand Up @@ -4222,6 +4222,11 @@ css-loader@^6.7.1:
postcss-value-parser "^4.2.0"
semver "^7.3.8"

css-mediaquery@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/css-mediaquery/-/css-mediaquery-0.1.2.tgz#6a2c37344928618631c54bd33cedd301da18bea0"
integrity sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==

css-minimizer-webpack-plugin@^4.0.0:
version "4.2.2"
resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-4.2.2.tgz#79f6199eb5adf1ff7ba57f105e3752d15211eb35"
Expand Down Expand Up @@ -7577,6 +7582,13 @@ minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==

mock-match-media@^0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/mock-match-media/-/mock-match-media-0.4.2.tgz#d054acd99579c58eec49b0a824cc7655ec6049e4"
integrity sha512-1Q/Z7cfqWVTZd5Iz0bLxqzl6/8vaPl4KxxciRSRu+TCeGxRlxGDgjwWre0KkHlfS01r1iGCUWuF0n3ftvS/C/A==
dependencies:
css-mediaquery "^0.1.2"

mrmime@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27"
Expand Down Expand Up @@ -9037,6 +9049,11 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==

resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==

resolve-cwd@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
Expand Down

0 comments on commit aacd612

Please sign in to comment.