Skip to content

Commit

Permalink
feat(react): add useMedia hook
Browse files Browse the repository at this point in the history
  • Loading branch information
hckhanh committed Mar 30, 2021
1 parent c1cb624 commit b944ada
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 2 deletions.
4 changes: 2 additions & 2 deletions packages/trakas-react/__tests__/hooks/useLocalStorage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import userEvent from "@testing-library/user-event";
import { useLocalStorage } from "../../src/hooks/useLocalStorage";

type TestComponentProps<T> = {
initialValue?: unknown;
value: unknown;
initialValue?: T;
value: T;
};

function TestComponent<T>(props: TestComponentProps<T>) {
Expand Down
42 changes: 42 additions & 0 deletions packages/trakas-react/__tests__/hooks/useMedia.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { render, screen } from "@testing-library/react";
import { useMedia } from "../../src/hooks/useMedia";
import { mockMatchMedia } from "../../utils/mocks";

function TestComponent() {
const minColumn = useMedia(["(min-width: 10px)", "(min-width: 0px)"], [1, 2], 0);
const maxColumn = useMedia(["(max-width: 10px)", "(max-width: 200px)"], [1, 4], 0);

return (
<>
<button aria-label="minColumn">{minColumn}</button>
<button aria-label="maxColumn">{maxColumn}</button>
</>
);
}

describe("useMedia", () => {
test("return value when matched", async () => {
mockMatchMedia((query) => ["(min-width: 0px)", "(max-width: 10px)"].includes(query));
render(<TestComponent />);

const minButtonElement = screen.getByRole("button", { name: "minColumn" });
const maxButtonElement = screen.getByRole("button", { name: "maxColumn" });

expect(minButtonElement).toHaveTextContent("2");
expect(maxButtonElement).toHaveTextContent("1");
});

test("return default values when no matches", async () => {
render(<TestComponent />);

const minButtonElement = screen.getByRole("button", { name: "minColumn" });
const maxButtonElement = screen.getByRole("button", { name: "maxColumn" });

expect(minButtonElement).toHaveTextContent("0");
expect(maxButtonElement).toHaveTextContent("0");
});

afterEach(() => {
mockMatchMedia(() => false);
});
});
4 changes: 4 additions & 0 deletions packages/trakas-react/setupTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom";
import { mockMatchMedia } from "./utils/mocks";

// Mock default matchMedia
mockMatchMedia(() => false);
38 changes: 38 additions & 0 deletions packages/trakas-react/src/hooks/useMedia.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* This source is referenced from amazing example of Gabe Ragland (https://twitter.com/gabe_ragland)
* learn more: https://usehooks.com/useMedia
*/

import { useEffect, useState } from "react";

export function useMedia<T>(queries: string[], values: T[], defaultValue: T): T {
// Array containing a media query list for each query
const mediaQueryLists = queries.map((q) => window.matchMedia(q));

// Function that gets value based on matching media query
const getValue = () => {
// Get index of first media query that matches
const index = mediaQueryLists.findIndex((mql) => mql.matches);
// Return related value or defaultValue if none
return values[index] || defaultValue;
};

// State and setter for matched value
const [value, setValue] = useState<T>(getValue);

useEffect(
() => {
// Event listener callback
// Note: By defining getValue outside of useEffect we ensure that it has
// current values of hook args (as this hook callback is created once on mount).
const handler = () => setValue(getValue);
// Set a listener for each media query with above handler as callback.
mediaQueryLists.forEach((mql) => mql.addEventListener("change", handler));
// Remove listeners on cleanup
return () => mediaQueryLists.forEach((mql) => mql.removeEventListener("change", handler));
},
[] // Empty array ensures effect is only run on mount and unmount
);

return value;
}
1 change: 1 addition & 0 deletions packages/trakas-react/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./hooks/useToggle";
export * from "./hooks/useDebounce";
export * from "./hooks/useLocalStorage";
export * from "./hooks/useMedia";
17 changes: 17 additions & 0 deletions packages/trakas-react/utils/mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export function mockMatchMedia(matches: (query: string) => boolean) {
// Mock matchMedia which is not implemented in JSDOM
// learn more: https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
Object.defineProperty(window, "matchMedia", {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: matches(query),
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
}

0 comments on commit b944ada

Please sign in to comment.