Skip to content

Commit 051e5f0

Browse files
gribnoysupedorivai
authored andcommitted
Change query to queries; Closes #69
1 parent 13ce020 commit 051e5f0

File tree

3 files changed

+262
-80
lines changed

3 files changed

+262
-80
lines changed

modules/Media.js

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,46 @@ import PropTypes from "prop-types";
33
import invariant from "invariant";
44
import json2mq from "json2mq";
55

6+
const queryType = PropTypes.oneOfType([
7+
PropTypes.string,
8+
PropTypes.object,
9+
PropTypes.arrayOf(PropTypes.object.isRequired)
10+
]);
11+
612
/**
713
* Conditionally renders based on whether or not a media query matches.
814
*/
915
class Media extends React.Component {
1016
static propTypes = {
11-
defaultMatches: PropTypes.bool,
12-
query: PropTypes.oneOfType([
13-
PropTypes.string,
14-
PropTypes.object,
15-
PropTypes.arrayOf(PropTypes.object.isRequired)
16-
]).isRequired,
17+
defaultMatches: PropTypes.objectOf(PropTypes.bool),
18+
queries: PropTypes.objectOf(queryType).isRequired,
1719
render: PropTypes.func,
1820
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
1921
targetWindow: PropTypes.object,
2022
onChange: PropTypes.func
2123
};
2224

23-
static defaultProps = {
24-
defaultMatches: true
25-
};
25+
queries = [];
2626

2727
state = {
28-
matches: this.props.defaultMatches
28+
matches:
29+
this.props.defaultMatches ||
30+
Object.keys(this.props.queries).reduce(
31+
(acc, key) => ({ ...acc, [key]: true }),
32+
{}
33+
)
2934
};
3035

3136
updateMatches = () => {
32-
const { matches } = this.mediaQueryList;
33-
34-
this.setState({ matches });
37+
const newMatches = this.queries.reduce(
38+
(acc, { name, mqList }) => ({ ...acc, [name]: mqList.matches }),
39+
{}
40+
);
41+
this.setState({ matches: newMatches });
3542

3643
const { onChange } = this.props;
3744
if (onChange) {
38-
onChange(matches);
45+
onChange(newMatches);
3946
}
4047
};
4148

@@ -49,32 +56,49 @@ class Media extends React.Component {
4956
"<Media targetWindow> does not support `matchMedia`."
5057
);
5158

52-
let { query } = this.props;
53-
if (typeof query !== "string") query = json2mq(query);
59+
const { queries } = this.props;
60+
61+
this.queries = Object.keys(queries).map(name => {
62+
const query = queries[name];
63+
const qs = typeof query !== "string" ? json2mq(query) : query;
64+
const mqList = targetWindow.matchMedia(qs);
65+
66+
mqList.addListener(this.updateMatches);
67+
68+
return { name, qs, mqList };
69+
});
5470

55-
this.mediaQueryList = targetWindow.matchMedia(query);
56-
this.mediaQueryList.addListener(this.updateMatches);
5771
this.updateMatches();
5872
}
5973

6074
componentWillUnmount() {
61-
this.mediaQueryList.removeListener(this.updateMatches);
75+
this.queries.forEach(({ mqList }) =>
76+
mqList.removeListener(this.updateMatches)
77+
);
6278
}
6379

6480
render() {
6581
const { children, render } = this.props;
6682
const { matches } = this.state;
6783

84+
const isAnyMatches = Object.keys(matches).some(key => matches[key]);
85+
6886
return render
69-
? matches
70-
? render()
87+
? isAnyMatches
88+
? render(matches)
7189
: null
7290
: children
7391
? typeof children === "function"
7492
? children(matches)
75-
: !Array.isArray(children) || children.length // Preact defaults to empty children array
76-
? matches
77-
? React.Children.only(children)
93+
: // Preact defaults to empty children array
94+
!Array.isArray(children) || children.length
95+
? isAnyMatches
96+
? // We have to check whether child is a composite component or not to decide should we
97+
// provide `matches` as a prop or not
98+
React.Children.only(children) &&
99+
typeof React.Children.only(children).type === "string"
100+
? React.Children.only(children)
101+
: React.cloneElement(React.Children.only(children), { matches })
78102
: null
79103
: null
80104
: null;

modules/__tests__/Media-ssr-test.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/** @jest-environment node */
2+
3+
import React from "react";
4+
import ReactDOMServer from "react-dom/server";
5+
import Media from "../Media";
6+
7+
describe("A <Media> in server environment", () => {
8+
const queries = {
9+
sm: "(max-width: 1000px)",
10+
lg: "(max-width: 2000px)",
11+
xl: "(max-width: 3000px)"
12+
};
13+
14+
describe("when no default matches prop provided", () => {
15+
it("should render its children as if all queries are matching", () => {
16+
const element = (
17+
<Media queries={queries}>
18+
{matches =>
19+
matches.sm &&
20+
matches.lg &&
21+
matches.xl && <span>All matches, render!</span>
22+
}
23+
</Media>
24+
);
25+
26+
const result = ReactDOMServer.renderToStaticMarkup(element);
27+
28+
expect(result).toBe("<span>All matches, render!</span>");
29+
});
30+
});
31+
32+
describe("when default matches prop provided", () => {
33+
const defaultMatches = {
34+
sm: true,
35+
lg: false,
36+
xl: false
37+
};
38+
39+
it("should render its children according to the provided defaultMatches", () => {
40+
const element = (
41+
<Media queries={queries} defaultMatches={defaultMatches}>
42+
{matches => (
43+
<div>
44+
{matches.sm && <span>small</span>}
45+
{matches.lg && <span>large</span>}
46+
{matches.xl && <span>extra large</span>}
47+
</div>
48+
)}
49+
</Media>
50+
);
51+
52+
const result = ReactDOMServer.renderToStaticMarkup(element);
53+
54+
expect(result).toBe("<div><span>small</span></div>");
55+
});
56+
});
57+
});

0 commit comments

Comments
 (0)