Skip to content

Commit 5fac22a

Browse files
committed
First stab at backward compatibility of query prop
1 parent 25981eb commit 5fac22a

File tree

3 files changed

+287
-66
lines changed

3 files changed

+287
-66
lines changed

modules/Media.js

Lines changed: 106 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import React from 'react';
2-
import PropTypes from 'prop-types';
3-
import invariant from 'invariant';
4-
import json2mq from 'json2mq';
1+
import React from "react";
2+
import PropTypes from "prop-types";
3+
import invariant from "invariant";
4+
import json2mq from "json2mq";
55

6-
import MediaQueryList from './MediaQueryList';
6+
import MediaQueryListener from "./MediaQueryListener";
77

88
const queryType = PropTypes.oneOfType([
99
PropTypes.string,
@@ -16,8 +16,12 @@ const queryType = PropTypes.oneOfType([
1616
*/
1717
class Media extends React.Component {
1818
static propTypes = {
19-
defaultMatches: PropTypes.objectOf(PropTypes.bool),
20-
queries: PropTypes.objectOf(queryType).isRequired,
19+
defaultMatches: PropTypes.oneOfType([
20+
PropTypes.bool,
21+
PropTypes.objectOf(PropTypes.bool)
22+
]),
23+
query: queryType,
24+
queries: PropTypes.objectOf(queryType),
2125
render: PropTypes.func,
2226
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
2327
targetWindow: PropTypes.object,
@@ -29,63 +33,101 @@ class Media extends React.Component {
2933
constructor(props) {
3034
super(props);
3135

36+
invariant(
37+
!(!props.query && !props.queries) || (props.query && props.queries),
38+
'<Media> must be supplied with either "query" or "queries"'
39+
);
40+
41+
invariant(
42+
props.defaultMatches === undefined ||
43+
!props.query ||
44+
typeof props.defaultMatches === "boolean",
45+
"<Media> when query is set, defaultMatches must be a boolean, received " +
46+
typeof props.defaultMatches
47+
);
48+
49+
invariant(
50+
props.defaultMatches === undefined ||
51+
!props.queries ||
52+
typeof props.defaultMatches === "object",
53+
"<Media> when queries is set, defaultMatches must be a object of booleans, received " +
54+
typeof props.defaultMatches
55+
);
56+
3257
if (typeof window !== "object") {
33-
// In case we're rendering on the server
58+
// In case we're rendering on the server, apply the default matches
59+
let matches;
60+
if (props.defaultMatches) {
61+
matches = props.defaultMatches;
62+
} else if (props.query) {
63+
matches = true;
64+
} /* if (props.queries) */ else {
65+
matches = Object.keys(this.props.queries).reduce(
66+
(acc, key) => ({ ...acc, [key]: true }),
67+
{}
68+
);
69+
}
3470
this.state = {
35-
matches:
36-
this.props.defaultMatches ||
37-
Object.keys(this.props.queries).reduce(
38-
(acc, key) => ({ ...acc, [key]: true }),
39-
{}
40-
)
71+
matches
4172
};
4273
return;
4374
}
4475

4576
this.initialize();
4677

47-
// Instead of calling this.updateMatches, we manually set the state to prevent
78+
// Instead of calling this.updateMatches, we manually set the initial state to prevent
4879
// calling setState, which could trigger an unnecessary second render
4980
this.state = {
5081
matches:
5182
this.props.defaultMatches !== undefined
5283
? this.props.defaultMatches
5384
: this.getMatches()
5485
};
86+
5587
this.onChange();
5688
}
5789

5890
getMatches = () => {
59-
return this.queries.reduce(
60-
(acc, { name, mqList }) => ({ ...acc, [name]: mqList.matches }),
91+
const result = this.queries.reduce(
92+
(acc, { name, mqListener }) => ({ ...acc, [name]: mqListener.matches }),
6193
{}
6294
);
95+
96+
// return result;
97+
return unwrapSingleQuery(result);
6398
};
6499

65100
updateMatches = () => {
66101
const newMatches = this.getMatches();
67102

68-
this.setState(() => ({
69-
matches: newMatches
70-
}), this.onChange);
103+
this.setState(
104+
() => ({
105+
matches: newMatches
106+
}),
107+
this.onChange
108+
);
71109
};
72110

73111
initialize() {
74112
const targetWindow = this.props.targetWindow || window;
75113

76114
invariant(
77-
typeof targetWindow.matchMedia === 'function',
78-
'<Media targetWindow> does not support `matchMedia`.'
115+
typeof targetWindow.matchMedia === "function",
116+
"<Media targetWindow> does not support `matchMedia`."
79117
);
80118

81-
const { queries } = this.props;
119+
const queries = this.props.queries || wrapInQueryObject(this.props.query);
82120

83121
this.queries = Object.keys(queries).map(name => {
84122
const query = queries[name];
85123
const qs = typeof query !== "string" ? json2mq(query) : query;
86-
const mqList = new MediaQueryList(targetWindow, qs, this.updateMatches);
124+
const mqListener = new MediaQueryListener(
125+
targetWindow,
126+
qs,
127+
this.updateMatches
128+
);
87129

88-
return { name, mqList };
130+
return { name, mqListener };
89131
});
90132
}
91133

@@ -107,35 +149,58 @@ class Media extends React.Component {
107149
}
108150

109151
componentWillUnmount() {
110-
this.queries.forEach(({ mqList }) => mqList.cancel());
152+
this.queries.forEach(({ mqListener }) => mqListener.cancel());
111153
}
112154

113155
render() {
114156
const { children, render } = this.props;
115157
const { matches } = this.state;
116158

117-
const isAnyMatches = Object.keys(matches).some(key => matches[key]);
159+
const isAnyMatches =
160+
typeof matches === "object"
161+
? Object.keys(matches).some(key => matches[key])
162+
: matches;
118163

119164
return render
120165
? isAnyMatches
121166
? render(matches)
122167
: null
123168
: children
124-
? typeof children === 'function'
125-
? children(matches)
126-
: // Preact defaults to empty children array
127-
!Array.isArray(children) || children.length
128-
? isAnyMatches
129-
? // We have to check whether child is a composite component or not to decide should we
130-
// provide `matches` as a prop or not
131-
React.Children.only(children) &&
132-
typeof React.Children.only(children).type === "string"
133-
? React.Children.only(children)
134-
: React.cloneElement(React.Children.only(children), { matches })
135-
: null
136-
: null
137-
: null;
169+
? typeof children === "function"
170+
? children(matches)
171+
: !Array.isArray(children) || children.length // Preact defaults to empty children array
172+
? isAnyMatches
173+
? // We have to check whether child is a composite component or not to decide should we
174+
// provide `matches` as a prop or not
175+
React.Children.only(children) &&
176+
typeof React.Children.only(children).type === "string"
177+
? React.Children.only(children)
178+
: React.cloneElement(React.Children.only(children), { matches })
179+
: null
180+
: null
181+
: null;
182+
}
183+
}
184+
185+
/**
186+
* Wraps a single query in an object. This is used to provide backward compatibility with
187+
* the old `query` prop (as opposed to `queries`). If only a single query is passed, the object
188+
* will be unpacked down the line, but this allows our internals to assume an object of queries
189+
* at all times.
190+
*/
191+
function wrapInQueryObject(query) {
192+
return { __DEFAULT__: query };
193+
}
194+
195+
/**
196+
* Unwraps an object of queries, if it was originally passed as a single query.
197+
*/
198+
function unwrapSingleQuery(queryObject) {
199+
const queryNames = Object.keys(queryObject);
200+
if (queryNames.length === 1 && queryNames[0] === "__DEFAULT__") {
201+
return queryObject.__DEFAULT__;
138202
}
203+
return queryObject;
139204
}
140205

141206
export default Media;

modules/MediaQueryList.js renamed to modules/MediaQueryListener.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export default class MediaQueryList {
1+
export default class MediaQueryListener {
22
constructor(targetWindow, query, listener) {
33
this.nativeMediaQueryList = targetWindow.matchMedia(query);
44
this.active = true;

0 commit comments

Comments
 (0)