Created the <MediaQuery /> component and useMediaQuery hook. (#12809)

Co-authored-by: Suzanne Aitchison <suzanne@forem.com>
This commit is contained in:
Nick Taylor 2021-02-25 14:28:44 -05:00 committed by GitHub
parent d1d4c00114
commit d90e5662ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 222 additions and 0 deletions

View file

@ -36,6 +36,7 @@ $colors: (
//
// List of breakpoints for our responsiveness.
// If you modify these breakpoints, ensure you update them in @components/useMediaQuery.js
$breakpoint-s: 640px;
$breakpoint-m: 768px;
$breakpoint-l: 1024px;

View file

@ -0,0 +1,59 @@
import { h } from 'preact';
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Components/Media Queries" />
# Media Queries
[Media queries](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries) allow us to create responsive experiences by changing the look and feel of a component based on the given media query. This can be done from CSS, but also programatically via JavaScript. This works well with a component because it **allows us to render only certain markup** based on the evaluated media query unlike a CSS media query which would apply to existing markup.
## Breakpoints
Currently, these are the breakpoints for widths we support out of the box. These can be used to build media queries in your code.
```javascript
export const BREAKPOINTS = Object.freeze({
Small: '640',
Medium: '768',
Large: '1024',
});
```
## useMediaQuery Custom Hook
A custom Preact hook used to evaluate and monitor a given media query.
```jsx
import { useMediaQuery } from '@components/useMediaQuery';
export function MediaQuery() {
const matchesBreakpoint = useMediaQuery(`(width >= ${BREAKPOINTS.Medium}px)`);
if (!matchesBreakpoint) {
return null;
}
return <p>I matched the breakpoint</p>;
}
```
## Media Query Component
The codebase currently has a lot of class based components, so a custom hook cannot be used. the `<MediaQuery />` component is perfect for this scenario. It encapsulates the custom `useMediaQuery` hook allowing us to reuse that logic and expose the result via a [render prop](https://reactjs.org/docs/render-props.html).
```jsx
import { MediaQuery } from '@components/MediaQuery';
<MediaQuery
query={`(width >= ${BREAKPOINTS.Medium}px)`}
render={(matches) => {
return matches && <p>This will render if the media query is matched.</p>;
}}
/>;
```
## Additional resources
- [Testing media queries programmatically](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Testing_media_queries)
- [Using media queries](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries)

View file

@ -0,0 +1,42 @@
import PropTypes from 'prop-types';
import { useMediaQuery } from './useMediaQuery';
/**
* A component for evaluating whether or not a CSS media query is matched or not.
*
* @param {object} props
* @param {string} props.query The media query to run.
* @param {function} props.render A render prop for using the result of the media query.
*
* @return {JSX.Element} Runs the render prop function to generate a JSX element
*
* @example
* import { MediaQuery } from '@components/MediaQuery';
*
* <MediaQuery
* query={`(width >= ${BREAKPOINTS.Medium}px)`}
* render={(matches) => {
* return (
* matches && (
* <aside className="crayons-layout__sidebar-left">
* <TagList
* availableTags={availableTags}
* selectedTag={selectedTag}
* onSelectTag={this.toggleTag}
* />
* </aside>
* )
* );
* }}
* />
*/
export function MediaQuery({ query, render }) {
const matchesBreakpoint = useMediaQuery(query);
return render(matchesBreakpoint);
}
MediaQuery.propTypes = {
query: PropTypes.string.isRequired,
render: PropTypes.func.isRequired,
};

View file

@ -0,0 +1,22 @@
import { h } from 'preact';
import { render } from '@testing-library/preact';
import { MediaQuery } from '@components/MediaQuery';
describe('<MediaQuery />', () => {
it('should call the render prop', () => {
global.window.matchMedia = jest.fn((query) => {
return {
matches: false,
media: query,
addListener: jest.fn(),
removeListener: jest.fn(),
};
});
const renderProp = jest.fn();
render(<MediaQuery query={'some media query'} render={renderProp} />);
expect(renderProp).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,49 @@
import { renderHook } from '@testing-library/preact-hooks';
import { cleanup } from '@testing-library/preact';
import { useMediaQuery } from '@components/useMediaQuery';
describe('useMediaQuery', () => {
it('should return false if the media query is not matched', () => {
const addListener = jest.fn();
const removeListener = jest.fn();
global.window.matchMedia = jest.fn((query) => {
return {
matches: false,
media: query,
addListener,
removeListener,
};
});
const { result } = renderHook(() => useMediaQuery('some media query'));
expect(addListener).toHaveBeenCalledTimes(1);
expect(result.current).toEqual(false);
cleanup();
expect(removeListener).toHaveBeenCalledTimes(1);
});
it('should return true if the media query is not matched', () => {
const addListener = jest.fn();
const removeListener = jest.fn();
global.window.matchMedia = jest.fn((query) => {
return {
matches: true,
media: query,
addListener,
removeListener,
};
});
const { result } = renderHook(() => useMediaQuery('some media query'));
expect(addListener).toHaveBeenCalledTimes(1);
expect(result.current).toEqual(true);
cleanup();
expect(removeListener).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,49 @@
import { useState, useEffect } from 'preact/hooks';
/**
* Pre-defined breakpoints for width.
*
* Note: These were copied from _import.scss.
*/
export const BREAKPOINTS = Object.freeze({
Small: '640',
Medium: '768',
Large: '1024',
});
/**
* A custom Preact hook for evaluating whether or not a CSS media query is matched or not.
*
* @param {string} query The media query to evaluate.
*
* @returns {boolean} True if the media query is matched, false otherwise.
*
* @example
* import { useMediaQuery } from '@components/useMediaQuery';
*
* function SomeComponent({ query }) {
* const matchesBreakpoint = useMediaQuery(query);
*
* if (!matchesBreakpoint) {
* return null;
* }
*
* return <SomeComponentThatMatchesMediaQuery />
* }
*/
export const useMediaQuery = (query) => {
const mediaQuery = window.matchMedia(query);
const [match, setMatch] = useState(!!mediaQuery.matches);
useEffect(() => {
const handler = () => {
setMatch(!!mediaQuery.matches);
};
mediaQuery.addListener(handler);
return () => mediaQuery.removeListener(handler);
});
return match;
};