Created the <MediaQuery /> component and useMediaQuery hook. (#12809)
Co-authored-by: Suzanne Aitchison <suzanne@forem.com>
This commit is contained in:
parent
d1d4c00114
commit
d90e5662ef
6 changed files with 222 additions and 0 deletions
1
app/assets/stylesheets/config/_import.scss
vendored
1
app/assets/stylesheets/config/_import.scss
vendored
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
42
app/javascript/shared/components/MediaQuery.jsx
Normal file
42
app/javascript/shared/components/MediaQuery.jsx
Normal 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,
|
||||
};
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
49
app/javascript/shared/components/useMediaQuery.js
Normal file
49
app/javascript/shared/components/useMediaQuery.js
Normal 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;
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue