-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
104 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | ||
})), | ||
}); | ||
} |