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" ;
5
5
6
- import MediaQueryList from './MediaQueryList' ;
6
+ import MediaQueryListener from "./MediaQueryListener" ;
7
7
8
8
const queryType = PropTypes . oneOfType ( [
9
9
PropTypes . string ,
@@ -16,8 +16,12 @@ const queryType = PropTypes.oneOfType([
16
16
*/
17
17
class Media extends React . Component {
18
18
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 ) ,
21
25
render : PropTypes . func ,
22
26
children : PropTypes . oneOfType ( [ PropTypes . node , PropTypes . func ] ) ,
23
27
targetWindow : PropTypes . object ,
@@ -29,63 +33,101 @@ class Media extends React.Component {
29
33
constructor ( props ) {
30
34
super ( props ) ;
31
35
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
+
32
57
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 !== undefined ) {
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
+ }
34
70
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
41
72
} ;
42
73
return ;
43
74
}
44
75
45
76
this . initialize ( ) ;
46
77
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
48
79
// calling setState, which could trigger an unnecessary second render
49
80
this . state = {
50
81
matches :
51
82
this . props . defaultMatches !== undefined
52
83
? this . props . defaultMatches
53
84
: this . getMatches ( )
54
85
} ;
86
+
55
87
this . onChange ( ) ;
56
88
}
57
89
58
90
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 } ) ,
61
93
{ }
62
94
) ;
95
+
96
+ // return result;
97
+ return unwrapSingleQuery ( result ) ;
63
98
} ;
64
99
65
100
updateMatches = ( ) => {
66
101
const newMatches = this . getMatches ( ) ;
67
102
68
- this . setState ( ( ) => ( {
69
- matches : newMatches
70
- } ) , this . onChange ) ;
103
+ this . setState (
104
+ ( ) => ( {
105
+ matches : newMatches
106
+ } ) ,
107
+ this . onChange
108
+ ) ;
71
109
} ;
72
110
73
111
initialize ( ) {
74
112
const targetWindow = this . props . targetWindow || window ;
75
113
76
114
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`."
79
117
) ;
80
118
81
- const { queries } = this . props ;
119
+ const queries = this . props . queries || wrapInQueryObject ( this . props . query ) ;
82
120
83
121
this . queries = Object . keys ( queries ) . map ( name => {
84
122
const query = queries [ name ] ;
85
123
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
+ ) ;
87
129
88
- return { name, mqList } ;
130
+ return { name, mqListener } ;
89
131
} ) ;
90
132
}
91
133
@@ -107,35 +149,58 @@ class Media extends React.Component {
107
149
}
108
150
109
151
componentWillUnmount ( ) {
110
- this . queries . forEach ( ( { mqList } ) => mqList . cancel ( ) ) ;
152
+ this . queries . forEach ( ( { mqListener } ) => mqListener . cancel ( ) ) ;
111
153
}
112
154
113
155
render ( ) {
114
156
const { children, render } = this . props ;
115
157
const { matches } = this . state ;
116
158
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 ;
118
163
119
164
return render
120
165
? isAnyMatches
121
166
? render ( matches )
122
167
: null
123
168
: 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__ ;
138
202
}
203
+ return queryObject ;
139
204
}
140
205
141
206
export default Media ;
0 commit comments