├── .eslintrc ├── .gitignore ├── .nvmrc ├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── async-component-loading │ ├── Big.js │ ├── Small.js │ ├── index.html │ └── index.js ├── custom-listener │ ├── index.html │ └── index.js ├── index.html ├── index.js ├── media-query │ ├── index.html │ └── index.js ├── responsive-navigation │ ├── DesktopNavigation.js │ ├── Header.js │ ├── MobileNavigation.js │ ├── index.html │ ├── index.js │ └── navigationItems.js ├── simple │ ├── index.html │ └── index.js └── sync-component-loading │ ├── index.html │ └── index.js ├── package.json ├── scripts └── webpack │ ├── examples.js │ ├── test.js │ └── webpack.tests.js └── src ├── MediaProvider.js ├── __tests__ ├── MediaProvider-test.js ├── composeGetters-test.js ├── composeListeners-test.js ├── createMediaQueryGetter-test.js ├── createMediaQueryListener-test.js ├── getCollindingKey-test.js ├── matchMedia-test.js ├── mocks │ └── matchMediaMock.js ├── viewportGetter-test.js ├── viewportListener-test.js └── warning-test.js ├── composeGetters.js ├── composeListeners.js ├── createConnector.js ├── createMediaQueryGetter.js ├── createMediaQueryListener.js ├── getCollidingKey.js ├── index.js ├── matchMedia.js ├── viewportGetter.js ├── viewportListener.js └── warning.js /.eslintrc: -------------------------------------------------------------------------------- 1 | parser: babel-eslint 2 | 3 | ecmaFeatures: 4 | modules: true 5 | jsx: true 6 | 7 | env: 8 | es6: true 9 | browser: true 10 | node: true 11 | 12 | globals: 13 | __TEST__: true 14 | __DEV__: true 15 | tape: true 16 | ga: true 17 | beopinion_t: true 18 | 19 | plugins: 20 | - react 21 | 22 | # 0: off, 1: warning, 2: error 23 | rules: 24 | strict: [2, "global"] 25 | 26 | # semicolons are useless 27 | semi: [2, "never"] 28 | 29 | max-len: [2, 80, 4] 30 | 31 | quotes: [2, "double"] 32 | 33 | # 2 spaces indentation 34 | indent: [2, 2] 35 | 36 | # trailing coma are cool for diff 37 | comma-dangle: [2, "always-multiline"] 38 | 39 | # enforce comma at eol (never before) 40 | comma-style: [2, "last"] 41 | 42 | no-underscore-dangle: 0 43 | no-fallthrough: 0 44 | 45 | camelcase: 0 46 | 47 | no-use-before-define: 0 48 | 49 | # eslint-plugin-react rules 50 | react/no-multi-comp: 0 51 | # react/prop-types: 2 52 | react/wrap-multilines: 2 53 | react/self-closing-comp: 2 54 | # little bug with listener(() => this.setState(s)) 55 | react/no-did-mount-set-state: 0 56 | react/no-did-update-set-state: 2 57 | react/jsx-uses-react: 2 58 | react/jsx-uses-vars: 2 59 | 60 | # https://github.com/babel/babel-eslint/issues/72 61 | no-unused-vars: 0 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | lib 3 | node_modules 4 | .DS_Store 5 | .DS_Store? 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 2.0.1 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - iojs 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Matthias Le Brun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-media-queries 2 | 3 | [![Build Status](https://travis-ci.org/bloodyowl/react-media-queries.svg?branch=master)](https://travis-ci.org/bloodyowl/react-media-queries) 4 | 5 | Extensible Media Queries for React. 6 | 7 | ## Install 8 | 9 | ```console 10 | $ npm install --save react-media-queries 11 | ``` 12 | 13 | ## API 14 | 15 | ### `` 16 | 17 | A component that provides Media Query data to the `matchMedia()` calls in the 18 | component hierarchy below. You can't use `matchMedia()` without wrapping the a 19 | component (e.g., the root component) in ``. 20 | 21 | #### Props 22 | 23 | * `children` (*ReactElement*) The root of your component hierarchy. 24 | - `getMedia` (*Function*): Return the current global media state. 25 | - `initialMedia` (*Object*): Provide default values for server-side rendering. 26 | - `listener` (*Function*): Listens to media changes, and returns a function that stops listening. 27 | 28 | #### Example 29 | 30 | ```javascript 31 | import React from "react" 32 | import { render } from "react-dom" 33 | import { MediaProvider } from "react-media-queries" 34 | import viewportListener from "react-media-queries/lib/viewportListener" 35 | import viewportGetter from "react-media-queries/lib/viewportGetter" 36 | 37 | render( 38 | 39 | 40 | , 41 | targetEl 42 | ) 43 | ``` 44 | 45 | ### matchMedia([resolveComponent][, mergeProps]) 46 | 47 | Connects a React component to the media data. It returns a new, connected 48 | component class (i.e., it does not modify the component class passed to it). 49 | 50 | #### Arguments 51 | 52 | - `resolveComponent(media, cb)` (*Function*): Resolves the component that will 53 | receive props. Resolution is synchronous when returning a component, and 54 | asynchronously when calling `cb` with the resolved component. 55 | - `mergeProps(ownProps, mediaProps, componentProps)` (*Function*): Custom prop merging 56 | 57 | #### Example 58 | 59 | ```javascript 60 | import React from "react" 61 | import { matchMedia } from "react-media-queries" 62 | import resolveComponentsSync from "./resolveComponentsSync" 63 | 64 | const App = ({ Component }) => ( 65 |
66 | {Component ? : "loading…"} 67 |
68 | ) 69 | 70 | const ResponsiveApp = matchMedia(resolveComponentsSync)(App) 71 | ``` 72 | 73 | Synchronous resolver: 74 | 75 | ```javascript 76 | const resolveComponentsSync = ({ mediaQuery, viewport }, cb) => { 77 | const isBig = mediaQuery.portrait.matches && (viewport.width > 400) 78 | return { 79 | Component: isBig ? require("./Big") : require("./Small"), 80 | } 81 | } 82 | ``` 83 | 84 | Asynchronous resolver: 85 | 86 | ```javascript 87 | const resolveComponentsAsync = ({ viewport }, cb) => { 88 | if(viewport.width > 400) { 89 | require.ensure([], () => { 90 | cb({ 91 | Component: require("./Big"), 92 | }) 93 | }) 94 | } else { 95 | require.ensure([], () => { 96 | cb({ 97 | Component: require("./Small"), 98 | }) 99 | }) 100 | } 101 | } 102 | ``` 103 | 104 | You can also mix the synchronous approach with the asynchronous one, for instance if you have the mobile component in your bundle and want to lazy-load the desktop one if needed : 105 | 106 | ```javascript 107 | const resolveComponentsAsync = ({ viewport }, cb) => { 108 | if(viewport.width > 400) { 109 | require.ensure([], () => { 110 | cb({ 111 | Component: require("./Big"), 112 | }) 113 | }) 114 | } else { 115 | return { 116 | Component: MobileComponent, 117 | } 118 | } 119 | } 120 | ``` 121 | 122 | ## Listeners 123 | 124 | Listeners determine when media data needs to be recalculated. There are 2 125 | predefined listeners: `viewportListener` and `createMediaQueryListener`. Custom 126 | listeners are also supported. 127 | 128 | ### viewportListener 129 | 130 | Listens to `resize` events on the `window`. 131 | 132 | ```javascript 133 | import viewportListener from "react-media-queries/lib/viewportListener" 134 | ``` 135 | 136 | ### createMediaQueryListener(mediaQueries: Object) 137 | 138 | Listens to `window.mediaMatch` events for the given Media Queries. 139 | 140 | ```javascript 141 | import createMediaQueryListener from "react-media-queries/lib/createMediaQueryListener" 142 | 143 | const mediaQueries = { 144 | small: "(min-width:300px)", 145 | medium: "(min-width: 400px)", 146 | large: "(min-width: 500px)", 147 | } 148 | 149 | const mediaQueryListener = createMediaQueryListener(mediaQueries) 150 | ``` 151 | 152 | ### composeListener(...listeners) 153 | 154 | Compose multiple listeners into one. 155 | 156 | ```javascript 157 | import composeListener from "react-media-queries/lib/composeListener" 158 | 159 | const listener = composeListener(viewportListener, mediaQueryListener) 160 | ``` 161 | 162 | ### Creating your own listener 163 | 164 | A listener is a function that accepts an `update` function argument. The 165 | listener should start listening to its event, and call `update` when it 166 | considers it needs to. The listener should return a function that removes the 167 | change listener. 168 | 169 | ```javascript 170 | const debouncedViewportListener = (update) => { 171 | const listener = debounce(update, 500) 172 | window.addEventListener("resize", listener) 173 | return () => window.removeEventListener("resize", listener) 174 | } 175 | ``` 176 | 177 | ## Getters 178 | 179 | Getters determine what media data is provided to components. There are 2 180 | predefined getters: `viewportGetter` and `createMediaQueryGetter`. Custom 181 | getters are also supported. 182 | 183 | ### viewportGetter 184 | 185 | Returns the current viewport dimensions in the form: `{ viewport: { height, width } }` 186 | 187 | ```javascript 188 | import viewportGetter from "react-media-queries/lib/viewportGetter" 189 | ``` 190 | 191 | ### createMediaQueryGetter(mediaQueries: Object) 192 | 193 | Returns the current Media Query states in the form: `{ mediaQuery: { [alias]: { 194 | matches, media } } }`. `matches` is a boolean, `media` is the Media Query 195 | string represented by the alias. 196 | 197 | ```javascript 198 | import createMediaQueryGetter from "react-media-queries/lib/createMediaQueryGetter" 199 | 200 | const mediaQueries = { 201 | small: "(min-width:300px)", 202 | medium: "(min-width: 400px)", 203 | large: "(min-width: 500px)", 204 | } 205 | 206 | const mediaQueryGetter = createMediaQueryGetter(mediaQueries) 207 | ``` 208 | 209 | ### composeGetters(...getters) 210 | 211 | Compose multiple getters into one. 212 | 213 | ```javascript 214 | import composeGetter from "react-media-queries/lib/composeGetter" 215 | 216 | const getter = composeGetter(viewportGetter, mediaQueryGetter) 217 | ``` 218 | 219 | ### Creating your own getter 220 | 221 | A getter must return an object representing the current state at that point in 222 | time. 223 | 224 | ```javascript 225 | const scrollGetter = () => ({ 226 | scrollY: window.pageYOffset, 227 | scrollX: window.pageXOffset, 228 | }) 229 | ``` 230 | 231 | ## [License](LICENSE) 232 | -------------------------------------------------------------------------------- /examples/async-component-loading/Big.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | export default () => ( 4 |
5 | {"I'm the big component"} 6 |
7 | ) 8 | -------------------------------------------------------------------------------- /examples/async-component-loading/Small.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | export default () => ( 4 |
5 | {"I'm the small component"} 6 |
7 | ) 8 | -------------------------------------------------------------------------------- /examples/async-component-loading/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Async Component Loading Example 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/async-component-loading/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { render } from "react-dom" 3 | import { matchMedia, MediaProvider } from "../../src" 4 | import viewportListener from "../../src/viewportListener" 5 | import viewportGetter from "../../src/viewportGetter" 6 | 7 | import "./index.html" 8 | 9 | const App = ({ Component }) => ( 10 |
11 | {Component ? : "loading …"} 12 |
13 | ) 14 | 15 | const resolveComponents = ({ viewport }, cb) => { 16 | if(viewport.width > 400) { 17 | require.ensure([], () => { 18 | cb({ 19 | Component: require("./Big"), 20 | }) 21 | }) 22 | } else { 23 | require.ensure([], () => { 24 | cb({ 25 | Component: require("./Small"), 26 | }) 27 | }) 28 | } 29 | } 30 | 31 | const ResponsiveApp = matchMedia(resolveComponents)(App) 32 | 33 | render( 34 | 37 | 38 | , 39 | document.body.appendChild(document.createElement("div")) 40 | ) 41 | -------------------------------------------------------------------------------- /examples/custom-listener/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Example 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/custom-listener/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { render } from "react-dom" 3 | import { matchMedia, MediaProvider } from "../../src" 4 | import viewportGetter from "../../src/viewportGetter" 5 | 6 | import "./index.html" 7 | 8 | const App = ({ viewport }) => ( 9 |
10 |
    11 |
  • viewport.width : {viewport.width}
  • 12 |
  • viewport.height : {viewport.height}
  • 13 |
14 |
15 | ) 16 | 17 | const WrappedApp = matchMedia()(App) 18 | 19 | const debounce = (func, delay) => { 20 | let timeout = null 21 | return (...args) => { 22 | if(timeout !== null) { 23 | clearTimeout(timeout) 24 | } 25 | timeout = setTimeout(() => { 26 | timeout = null 27 | func(...args) 28 | }, delay) 29 | return timeout 30 | } 31 | } 32 | 33 | const debouncedViewportListener = (update) => { 34 | const listener = debounce(update, 500) 35 | window.addEventListener("resize", listener) 36 | return () => window.removeEventListener("resize", listener) 37 | } 38 | 39 | render( 40 | 43 | 44 | , 45 | document.body.appendChild(document.createElement("div")) 46 | ) 47 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Media Queries examples 5 | 33 | 34 | 35 |
React Media Queries
36 |
Examples
37 | 38 | 39 | Async component loading 40 | 41 | 42 | Sync component loading 43 | 44 | 45 | 46 | Custom listener 47 | 48 | 49 | Simple 50 | 51 | 52 | Media query 53 | 54 | 55 | Responsive navigation 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | import "./index.html" 2 | -------------------------------------------------------------------------------- /examples/media-query/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Media Query Example 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/media-query/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { render } from "react-dom" 3 | import { matchMedia, MediaProvider } from "../../src" 4 | import createMediaQueryListener from "../../src/createMediaQueryListener" 5 | import createMediaQueryGetter from "../../src/createMediaQueryGetter" 6 | 7 | import "./index.html" 8 | 9 | const App = ({ mediaQuery }) => ( 10 |
11 | mediaQuery is: 12 |
    13 |
  • 14 | small {mediaQuery.small.media}: {`${mediaQuery.small.matches}`} 15 |
  • 16 |
  • 17 | medium {mediaQuery.medium.media}: {`${mediaQuery.medium.matches}`} 18 |
  • 19 |
  • 20 | large {mediaQuery.large.media}: {`${mediaQuery.large.matches}`} 21 |
  • 22 |
23 |
24 | ) 25 | 26 | const WrappedApp = matchMedia()(App) 27 | 28 | const mediaQueries = { 29 | small: "(min-width:300px)", 30 | medium: "(min-width: 400px)", 31 | large: "(min-width: 500px)", 32 | } 33 | 34 | render( 35 | 38 | 39 | , 40 | document.body.appendChild(document.createElement("div")) 41 | ) 42 | -------------------------------------------------------------------------------- /examples/responsive-navigation/DesktopNavigation.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createStyleSheet, rem, join } from "stile" 3 | 4 | const DesktopNavigation = ({ items }) => ( 5 |
6 | {items.map(({ label, href }, index) => 7 | 8 | {label} 9 | 10 | )} 11 |
12 | ) 13 | 14 | const styles = createStyleSheet({ 15 | item: { 16 | padding: join(rem(.25), rem(.5)), 17 | textDecoration: "none", 18 | color: "#333", 19 | }, 20 | }) 21 | 22 | export default DesktopNavigation 23 | -------------------------------------------------------------------------------- /examples/responsive-navigation/Header.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { matchMedia } from "../../src" 3 | import navigationItems from "./navigationItems" 4 | import { createStyleSheet, percent, rem } from "stile" 5 | 6 | const Header = ({ Navigation }) => ( 7 |
8 |
hey there
9 |
10 | {Navigation && } 11 |
12 |
13 | ) 14 | 15 | const styles = createStyleSheet({ 16 | header: { 17 | display: "flex", 18 | flexDirection: "row", 19 | alignItems: "center", 20 | justifyContent: "space-between", 21 | padding: rem(1), 22 | background: "linear-gradient(to bottom, #fff, #eee)", 23 | }, 24 | title: { 25 | flexShrink: 1, 26 | flexBasis: percent(50), 27 | }, 28 | }) 29 | 30 | const resolveComponents = ({ mediaQuery }, cb) => { 31 | if(mediaQuery.maxL.matches) { 32 | require.ensure([], () => { 33 | cb({ 34 | Navigation: require("./MobileNavigation"), 35 | }) 36 | }) 37 | } else { 38 | require.ensure([], () => { 39 | cb({ 40 | Navigation: require("./DesktopNavigation"), 41 | }) 42 | }) 43 | } 44 | } 45 | 46 | export default matchMedia(resolveComponents)(Header) 47 | -------------------------------------------------------------------------------- /examples/responsive-navigation/MobileNavigation.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import { createStyleSheet, rem, vw } from "stile" 3 | 4 | class MobileNavigation extends Component { 5 | 6 | state = { 7 | opened: false, 8 | } 9 | 10 | render() { 11 | const { items } = this.props 12 | const { opened } = this.state 13 | return ( 14 |
15 | 20 | {opened && 21 |
22 |
this.setState({ opened: false })}/> 25 |
26 | {items.map(({ label, href }, index) => 27 | 28 | {label} 29 | 30 | )} 31 |
32 |
33 | } 34 |
35 | ) 36 | } 37 | } 38 | 39 | const hamburger = ( 40 | 41 | 42 | 43 | 44 | 45 | ) 46 | 47 | const styles = createStyleSheet ({ 48 | button: { 49 | background: "none", 50 | border: "none", 51 | cursor: "pointer", 52 | padding: 0, 53 | }, 54 | overlay: { 55 | position: "fixed", 56 | top: 0, 57 | right: 0, 58 | bottom: 0, 59 | left: 0, 60 | background: "rgba(0,0,0,.6)", 61 | cursor: "pointer", 62 | }, 63 | navigation: { 64 | position: "fixed", 65 | top: 0, 66 | right: 0, 67 | bottom: 0, 68 | background: "#fff", 69 | zIndex: 100, 70 | display: "flex", 71 | flexDirection: "column", 72 | minWidth: vw(70), 73 | }, 74 | item: { 75 | padding: rem(1), 76 | textDecoration: "none", 77 | borderBottom: "1px solid #eee", 78 | color: "#888", 79 | }, 80 | }) 81 | 82 | export default MobileNavigation 83 | -------------------------------------------------------------------------------- /examples/responsive-navigation/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Responsive Navigation Example 7 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/responsive-navigation/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { render } from "react-dom" 3 | import Header from "./Header" 4 | import { MediaProvider } from "../../src" 5 | import createMediaQueryGetter from "../../src/createMediaQueryGetter" 6 | import createMediaQueryListener from "../../src/createMediaQueryListener" 7 | 8 | import "./index.html" 9 | 10 | const mediaQueries = { 11 | maxL: "(max-width: 400px)", 12 | } 13 | 14 | render( 15 | 18 |
19 | , 20 | document.body.appendChild(document.createElement("div")) 21 | ) 22 | -------------------------------------------------------------------------------- /examples/responsive-navigation/navigationItems.js: -------------------------------------------------------------------------------- 1 | const navigationItems = [ 2 | { 3 | label: "Home", 4 | href: "#", 5 | }, 6 | { 7 | label: "About", 8 | href: "#", 9 | }, 10 | { 11 | label: "Contact", 12 | href: "#", 13 | }, 14 | ] 15 | 16 | export default navigationItems 17 | -------------------------------------------------------------------------------- /examples/simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple Example 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/simple/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { render } from "react-dom" 3 | import { matchMedia, MediaProvider } from "../../src" 4 | import viewportListener from "../../src/viewportListener" 5 | import viewportGetter from "../../src/viewportGetter" 6 | 7 | import "./index.html" 8 | 9 | const App = ({ viewport }) => ( 10 |
11 |
    12 |
  • viewport.width : {viewport.width}
  • 13 |
  • viewport.height : {viewport.height}
  • 14 |
15 |
16 | ) 17 | 18 | const WrappedApp = matchMedia()(App) 19 | 20 | render( 21 | 24 | 25 | , 26 | document.body.appendChild(document.createElement("div")) 27 | ) 28 | -------------------------------------------------------------------------------- /examples/sync-component-loading/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Async Component Loading Example 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/sync-component-loading/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { render } from "react-dom" 3 | import { matchMedia, MediaProvider } from "../../src" 4 | import viewportListener from "../../src/viewportListener" 5 | import viewportGetter from "../../src/viewportGetter" 6 | 7 | import "./index.html" 8 | 9 | const Big = () => ( 10 |
11 | {"I'm the big component"} 12 |
13 | ) 14 | 15 | const Small = () => ( 16 |
17 | {"I'm the small component"} 18 |
19 | ) 20 | 21 | const App = ({ Component }) => ( 22 |
23 | {Component ? : "loading …"} 24 |
25 | ) 26 | 27 | const resolveComponents = ({ viewport }, cb) => { 28 | return { 29 | Component: viewport.width > 400 ? Big : Small, 30 | } 31 | } 32 | 33 | const WrappedApp = matchMedia(resolveComponents)(App) 34 | 35 | render( 36 | 39 | 40 | , 41 | document.body.appendChild(document.createElement("div")) 42 | ) 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-media-queries", 3 | "version": "2.0.1", 4 | "description": "provider and decorator to manage media queries with react", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib", 8 | "package.json", 9 | "README.md", 10 | "LICENSE" 11 | ], 12 | "scripts": { 13 | "start": "babel --stage 0 src --out-dir lib --ignore '__tests__'", 14 | "lint": "eslint src/**.js && eslint examples/**.js", 15 | "examples": "babel-node --stage 0 ./scripts/webpack/examples", 16 | "test": "npm run lint && babel-node --stage 0 ./scripts/webpack/test.js" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git@github.com:bloodyowl/react-media-queries" 21 | }, 22 | "keywords": [ 23 | "react", 24 | "media-query", 25 | "inline-styles" 26 | ], 27 | "author": "bloodyowl", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/bloodyowl/react-media-queries/issues" 31 | }, 32 | "homepage": "https://github.com/bloodyowl/react-media-queries", 33 | "peerDependencies": { 34 | "react": "^0.14.0" 35 | }, 36 | "devDependencies": { 37 | "babel": "^5.8.23", 38 | "babel-core": "^5.8.25", 39 | "babel-eslint": "^4.1.3", 40 | "babel-loader": "^5.3.2", 41 | "eslint": "^1.7.3", 42 | "eslint-plugin-react": "^3.6.3", 43 | "file-loader": "^0.8.4", 44 | "react": "^0.14.0", 45 | "react-dom": "^0.14.0", 46 | "react-test-utils": "0.0.1", 47 | "stile": "^1.0.0", 48 | "tape": "^4.2.1", 49 | "tape-catch": "^1.0.4", 50 | "webpack": "^1.12.2", 51 | "webpack-dev-server": "^1.12.1", 52 | "webpack-jsdom-tape-plugin": "^1.2.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /scripts/webpack/examples.js: -------------------------------------------------------------------------------- 1 | import webpack from "webpack" 2 | import WebpackDevServer from "webpack-dev-server" 3 | import path from "path" 4 | 5 | import JsdomTapePlugin from "webpack-jsdom-tape-plugin" 6 | 7 | const location = { 8 | protocol: "http://", 9 | host: "0.0.0.0", 10 | port: 3002, 11 | open: true, 12 | } 13 | 14 | const serverUrl = `${ location.protocol }${ location.host }:${ location.port }` 15 | 16 | const config = { 17 | entry: { 18 | "examples/simple/index": "./examples/simple/index.js", 19 | "examples/media-query/index": "./examples/media-query/index.js", 20 | "examples/async-component-loading/index": "./examples/async-component-loading/index.js", 21 | "examples/sync-component-loading/index": "./examples/sync-component-loading/index.js", 22 | "examples/custom-listener/index": "./examples/custom-listener/index.js", 23 | "examples/responsive-navigation/index": "./examples/responsive-navigation/index.js", 24 | "index": "./examples/index.js", 25 | }, 26 | output: { 27 | path: path.join(__dirname, "../../dist"), 28 | filename: "[name].js", 29 | publicPath: "/", 30 | }, 31 | module: { 32 | loaders: [ 33 | { 34 | test: /\.js$/, 35 | loader: "babel", 36 | exclude: /node_modules/, 37 | query: { 38 | stage: 0, 39 | }, 40 | }, 41 | { 42 | test: /\.html$/, 43 | loader: "file?name=[path][name].html", 44 | }, 45 | ], 46 | } 47 | } 48 | 49 | const server = new WebpackDevServer(webpack(config), { 50 | contentBase: config.output.path, 51 | hot: true, 52 | stats: { 53 | colors: true, 54 | chunkModules: false, 55 | assets: true, 56 | }, 57 | noInfo: true, 58 | historyApiFallback: true, 59 | }) 60 | 61 | server.listen( 62 | location.port, 63 | location.host, 64 | () => { 65 | console.log(`open ${ serverUrl }/examples in your browser`) 66 | } 67 | ) 68 | 69 | 70 | export default config 71 | -------------------------------------------------------------------------------- /scripts/webpack/test.js: -------------------------------------------------------------------------------- 1 | import webpack from "webpack" 2 | import WebpackDevServer from "webpack-dev-server" 3 | import path from "path" 4 | 5 | import JsdomTapePlugin from "webpack-jsdom-tape-plugin" 6 | 7 | const location = { 8 | protocol: "http://", 9 | host: "0.0.0.0", 10 | port: 3001, 11 | open: true, 12 | } 13 | 14 | const serverUrl = `${ location.protocol }${ location.host }:${ location.port }` 15 | 16 | const config = { 17 | entry: { 18 | "test": "./scripts/webpack/webpack.tests.js", 19 | }, 20 | output: { 21 | path: path.join(__dirname, "../../dist"), 22 | filename: "[name].js", 23 | publicPath: "/", 24 | }, 25 | plugins: [ 26 | new JsdomTapePlugin({ 27 | url: serverUrl, 28 | entry: ["test.js"], 29 | }), 30 | ], 31 | module: { 32 | loaders: [ 33 | { 34 | test: /\.js$/, 35 | loader: "babel", 36 | exclude: /node_modules/, 37 | query: { 38 | stage: 0, 39 | }, 40 | }, 41 | ], 42 | } 43 | } 44 | 45 | const server = new WebpackDevServer(webpack(config), { 46 | contentBase: config.output.path, 47 | hot: true, 48 | stats: { 49 | colors: true, 50 | chunkModules: false, 51 | assets: true, 52 | }, 53 | noInfo: true, 54 | historyApiFallback: true, 55 | }) 56 | 57 | server.listen( 58 | location.port, 59 | location.host 60 | ) 61 | 62 | export default config 63 | -------------------------------------------------------------------------------- /scripts/webpack/webpack.tests.js: -------------------------------------------------------------------------------- 1 | import "babel/polyfill" 2 | 3 | const context = require.context("../../src", true, /__tests__\/\S+\.js$/) 4 | 5 | context.keys().forEach(context) 6 | -------------------------------------------------------------------------------- /src/MediaProvider.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes, Children } from "react" 2 | 3 | const noop = () => {} 4 | 5 | class MediaProvider extends Component { 6 | static propTypes = { 7 | initialMedia: PropTypes.object, 8 | getMedia: PropTypes.func.isRequired, 9 | listener: PropTypes.func, 10 | } 11 | static defaultProps = { 12 | listener: noop, 13 | } 14 | static childContextTypes = { 15 | mediaQuery: PropTypes.object.isRequired, 16 | } 17 | state = {} 18 | componentWillMount() { 19 | const { initialMedia, getMedia } = this.props 20 | this.setState({ 21 | mediaQuery: initialMedia || getMedia(), 22 | }) 23 | } 24 | componentDidMount() { 25 | const { listener, getMedia } = this.props 26 | if(!listener) { 27 | return 28 | } 29 | this.removeListener = listener(() => 30 | this.setState({ 31 | mediaQuery: getMedia(), 32 | }) 33 | ) 34 | } 35 | componentWillUnmount() { 36 | if(!this.removeListener) { 37 | return 38 | } 39 | this.removeListener() 40 | this.removeListener = null 41 | } 42 | getChildContext() { 43 | const { mediaQuery } = this.state 44 | return { mediaQuery } 45 | } 46 | render() { 47 | const { children } = this.props 48 | return Children.only(children) 49 | } 50 | } 51 | 52 | export default MediaProvider 53 | -------------------------------------------------------------------------------- /src/__tests__/MediaProvider-test.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from "react" 2 | import { renderToStaticMarkup } from "react-dom/server" 3 | import { render, unmountComponentAtNode } from "react-dom" 4 | import { renderIntoDocument } from "react-test-utils" 5 | 6 | import MediaProvider from "../MediaProvider" 7 | import matchMedia from "../matchMedia" 8 | 9 | tape("MediaProvider handles initialMedia", (test) => { 10 | const initialMedia = { 11 | testProp: "FOO_BAR", 12 | } 13 | const Dummy = ({ testProp }) => ( 14 |
{testProp}
15 | ) 16 | const ResponsiveDummy = matchMedia()(Dummy) 17 | test.equal( 18 | renderToStaticMarkup( 19 | 20 | 21 | 22 | ), 23 | `
${ initialMedia.testProp }
`, 24 | "passes initial mediaQuery correctly" 25 | ) 26 | test.end() 27 | }) 28 | 29 | 30 | tape("MediaProvider throws if having no children", (test) => { 31 | const initialMedia = { 32 | testProp: "FOO_BAR", 33 | } 34 | test.throws(() => { 35 | renderToStaticMarkup( 36 | 37 | ) 38 | }) 39 | test.end() 40 | }) 41 | 42 | tape("MediaProvider throws if having more than one children", (test) => { 43 | const initialMedia = { 44 | testProp: "FOO_BAR", 45 | } 46 | const Dummy = ({ testProp }) => ( 47 |
{testProp}
48 | ) 49 | const ResponsiveDummy = matchMedia()(Dummy) 50 | test.throws(() => { 51 | renderToStaticMarkup( 52 | 53 | 54 | 55 | 56 | ) 57 | }) 58 | test.end() 59 | }) 60 | 61 | tape("MediaProvider handles media getting", (test) => { 62 | const media = { 63 | testProp: "TEST_PROP", 64 | } 65 | const getMedia = () => media 66 | const Dummy = ({ testProp }) => ( 67 |
{testProp}
68 | ) 69 | const ResponsiveDummy = matchMedia()(Dummy) 70 | test.equal( 71 | renderToStaticMarkup( 72 | 73 | 74 | 75 | ), 76 | `
${ media.testProp }
`, 77 | "passes mediaQuery from getMedia correctly" 78 | ) 79 | test.end() 80 | }) 81 | 82 | tape("MediaProvider prefers initialMedia to getMedia on mount", (test) => { 83 | const initialMedia = { 84 | testProp: "RIGHT", 85 | } 86 | const getMedia = () => ({ 87 | testProp: "WRONG", 88 | }) 89 | const Dummy = ({ testProp }) => ( 90 |
{testProp}
91 | ) 92 | const ResponsiveDummy = matchMedia()(Dummy) 93 | test.equal( 94 | renderToStaticMarkup( 95 | 98 | 99 | 100 | ), 101 | `
${ initialMedia.testProp }
`, 102 | "passes initialMedia on mount if available" 103 | ) 104 | test.end() 105 | }) 106 | 107 | tape("MediaProvider updates from listener", (test) => { 108 | let TEST_PROP = 0 109 | const getMedia = () => ({ 110 | testProp: TEST_PROP, 111 | }) 112 | let update = null 113 | 114 | @matchMedia() 115 | class Dummy extends Component { 116 | componentDidMount() { 117 | const { testProp } = this.props 118 | test.equal(testProp, 0, "gets media on mount") 119 | // delays so that parent componentDidMount are called 120 | setTimeout(() => { 121 | TEST_PROP = 1 122 | update() 123 | }) 124 | } 125 | componentWillReceiveProps(props) { 126 | const { testProp } = props 127 | test.equal(testProp, 1, "updates correctly") 128 | setTimeout(() => { 129 | unmountComponentAtNode(mountNode) 130 | }) 131 | } 132 | render() { 133 | const { testProp } = this.props 134 | return ( 135 |
{testProp}
136 | ) 137 | } 138 | } 139 | 140 | const listener = (u) => { 141 | update = u 142 | return () => { 143 | test.pass("teardown is called on unmount") 144 | test.end() 145 | } 146 | } 147 | 148 | const mountNode = document.createElement("div") 149 | 150 | render( 151 | 154 | 155 | , 156 | mountNode 157 | ) 158 | }) 159 | -------------------------------------------------------------------------------- /src/__tests__/composeGetters-test.js: -------------------------------------------------------------------------------- 1 | import composeGetters from "../composeGetters" 2 | 3 | tape("composeGetters", (test) => { 4 | const composed = composeGetters( 5 | () => ({ a: 1 }), 6 | () => ({ b: 1 }) 7 | ) 8 | test.deepEqual(composed(), {a: 1, b: 1}) 9 | test.end() 10 | }) 11 | -------------------------------------------------------------------------------- /src/__tests__/composeListeners-test.js: -------------------------------------------------------------------------------- 1 | import composeListeners from "../composeListeners" 2 | 3 | tape("composeListeners", (test) => { 4 | test.plan(4) 5 | const pass = () => test.pass() 6 | const setup = composeListeners( 7 | () => { 8 | pass() 9 | return pass 10 | }, 11 | () => { 12 | pass() 13 | return pass 14 | } 15 | ) 16 | const teardown = setup() 17 | teardown() 18 | }) 19 | -------------------------------------------------------------------------------- /src/__tests__/createMediaQueryGetter-test.js: -------------------------------------------------------------------------------- 1 | import createMediaQueryGetter from "../createMediaQueryGetter" 2 | import matchMediaMock from "./mocks/matchMediaMock" 3 | 4 | tape("createMediaQueryGetter", (test) => { 5 | const originalMatchMedia = window.matchMedia 6 | window.matchMedia = matchMediaMock 7 | const mediaQueries = { 8 | medium: "(min-width: 450px)", 9 | large: "(min-width: 750px)", 10 | } 11 | const mediaQueryGetter = createMediaQueryGetter(mediaQueries) 12 | test.equal(typeof mediaQueryGetter, "function", "returns a function") 13 | test.deepEqual( 14 | mediaQueryGetter(), 15 | { 16 | mediaQuery: { 17 | medium: { media: mediaQueries.medium, matches: true }, 18 | large: { media: mediaQueries.large, matches: true }, 19 | }, 20 | }, 21 | ) 22 | test.end() 23 | window.matchMedia = originalMatchMedia 24 | }) 25 | -------------------------------------------------------------------------------- /src/__tests__/createMediaQueryListener-test.js: -------------------------------------------------------------------------------- 1 | import createMediaQueryListener from "../createMediaQueryListener" 2 | import { createMockWithHook } from "./mocks/matchMediaMock" 3 | 4 | tape("createMediaQueryListener", (test) => { 5 | test.plan(2) 6 | const originalMatchMedia = window.matchMedia 7 | const mql = [] 8 | window.matchMedia = createMockWithHook((item) => mql.push(item)) 9 | const mediaQueries = { 10 | medium: "(min-width: 450px)", 11 | large: "(min-width: 750px)", 12 | } 13 | const mediaQueryListener = createMediaQueryListener(mediaQueries) 14 | test.equal(typeof mediaQueryListener, "function", "returns a function") 15 | const teardown = mediaQueryListener(() => { 16 | test.pass("calls update on change") 17 | }) 18 | mql[0].simulateChange() 19 | teardown() 20 | mql[0].simulateChange() 21 | window.matchMedia = originalMatchMedia 22 | }) 23 | -------------------------------------------------------------------------------- /src/__tests__/getCollindingKey-test.js: -------------------------------------------------------------------------------- 1 | import getCollidingKey from "../getCollidingKey" 2 | 3 | tape("getCollidingKey", (test) => { 4 | test.equal(getCollidingKey({a: 1}, {b: 2}), null) 5 | test.equal(getCollidingKey({a: 1}, {a: 2}), "a") 6 | test.equal(getCollidingKey({a: 1}, {b: 2}, {a: 3}), "a") 7 | test.equal(getCollidingKey({b: 1}, {a: 2}, {a: 3}), "a") 8 | test.end() 9 | }) 10 | -------------------------------------------------------------------------------- /src/__tests__/matchMedia-test.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from "react" 2 | import { renderToStaticMarkup } from "react-dom/server" 3 | import MediaProvider from "../MediaProvider" 4 | import matchMedia from "../matchMedia" 5 | 6 | tape("matchMedia", (test) => { 7 | const initialMedia = { 8 | testProp: "FOO_BAR", 9 | } 10 | const Dummy = (props) => { 11 | test.deepEqual(props, initialMedia, "passes props correcly") 12 | test.end() 13 | return
14 | } 15 | const ResponsiveDummy = matchMedia()(Dummy) 16 | renderToStaticMarkup( 17 | 18 | 19 | 20 | ) 21 | }) 22 | 23 | tape("matchMedia mergeProps", (test) => { 24 | const initialMedia = { 25 | testProp: "FOO_BAR", 26 | } 27 | const Dummy = (props) => { 28 | test.deepEqual(props, initialMedia, "passes props correcly") 29 | test.end() 30 | return
31 | } 32 | const resolveComponents = () => ({ 33 | bar: "baz", 34 | }) 35 | const mergeProps = (ownProps, mediaProps, componentProps) => { 36 | test.deepEqual(ownProps, { foo: "bar" }, "ownProps") 37 | test.deepEqual(mediaProps, initialMedia, "mediaProps") 38 | test.deepEqual(componentProps, {bar: "baz"}, "componentProps") 39 | return { 40 | ...initialMedia, 41 | } 42 | } 43 | const ResponsiveDummy = matchMedia(resolveComponents, mergeProps)(Dummy) 44 | renderToStaticMarkup( 45 | 46 | 47 | 48 | ) 49 | }) 50 | 51 | tape("matchMedia default mergeProps", (test) => { 52 | const initialMedia = { 53 | testProp: "FOO_BAR", 54 | } 55 | const Dummy = (props) => { 56 | test.deepEqual( 57 | props, 58 | {...initialMedia, foo: "baz"}, 59 | "passes props correctly" 60 | ) 61 | test.end() 62 | return
63 | } 64 | const resolveComponents = () => ({ 65 | foo: "baz", 66 | }) 67 | const ResponsiveDummy = matchMedia(resolveComponents)(Dummy) 68 | const initialConsoleError = console.error 69 | console.error = (message) => { 70 | test.equal( 71 | message, 72 | "react-media-queries : colliding key foo in props merge", 73 | "warns if colliding keys between ownProps & componentProps" 74 | ) 75 | } 76 | renderToStaticMarkup( 77 | 78 | 79 | 80 | ) 81 | console.error = initialConsoleError 82 | }) 83 | 84 | tape("matchMedia default mergeProps", (test) => { 85 | const initialMedia = { 86 | testProp: "FOO_BAR", 87 | } 88 | const Dummy = (props) => { 89 | test.deepEqual( 90 | props, 91 | {...initialMedia, foo: "baz"}, 92 | "passes props correctly" 93 | ) 94 | test.end() 95 | return
96 | } 97 | const resolveComponents = () => ({ 98 | foo: "baz", 99 | }) 100 | const ResponsiveDummy = matchMedia(resolveComponents)(Dummy) 101 | const initialConsoleError = console.error 102 | console.error = (message) => { 103 | test.equal( 104 | message, 105 | "react-media-queries : colliding key testProp in props merge", 106 | "warns if colliding keys between ownProps & mediaProps" 107 | ) 108 | } 109 | renderToStaticMarkup( 110 | 111 | 112 | 113 | ) 114 | console.error = initialConsoleError 115 | }) 116 | 117 | tape("matchMedia default mergeProps", (test) => { 118 | const initialMedia = { 119 | testProp: "FOO_BAR", 120 | } 121 | const Dummy = (props) => { 122 | test.deepEqual( 123 | props, 124 | { testProp: "baz", foo: "bar"}, 125 | "passes props correctly" 126 | ) 127 | test.end() 128 | return
129 | } 130 | const resolveComponents = () => ({ 131 | testProp: "baz", 132 | }) 133 | const ResponsiveDummy = matchMedia(resolveComponents)(Dummy) 134 | const initialConsoleError = console.error 135 | console.error = (message) => { 136 | test.equal( 137 | message, 138 | "react-media-queries : colliding key testProp in props merge", 139 | "warns if colliding keys between componentProps & mediaProps" 140 | ) 141 | } 142 | renderToStaticMarkup( 143 | 144 | 145 | 146 | ) 147 | console.error = initialConsoleError 148 | }) 149 | 150 | tape("matchMedia async resolve component", (test) => { 151 | const Dummy = ({ asyncValue }) => { 152 | if(asyncValue) { 153 | test.equal(asyncValue, 1, "asynchronously resolved") 154 | test.end() 155 | } else { 156 | test.equal(asyncValue, 0, "synchronously resolved") 157 | } 158 | return
159 | } 160 | const resolveComponents = (media, cb) => { 161 | test.deepEqual(media, initialMedia, "receives media correctly") 162 | setTimeout(() => { 163 | cb({ 164 | asyncValue: 1, 165 | }) 166 | }, 100) 167 | return { 168 | asyncValue: 0, 169 | } 170 | } 171 | const initialMedia = { foo: "bar" } 172 | const ResponsiveDummy = matchMedia(resolveComponents)(Dummy) 173 | renderToStaticMarkup( 174 | 175 | 176 | 177 | ) 178 | }) 179 | -------------------------------------------------------------------------------- /src/__tests__/mocks/matchMediaMock.js: -------------------------------------------------------------------------------- 1 | const map = new Map() 2 | 3 | class MediaQueryList { 4 | constructor(mediaQuery) { 5 | this.media = mediaQuery 6 | this.matches = true 7 | } 8 | addListener(func) { 9 | map.set(this, (map.get(this) || []).concat(func)) 10 | } 11 | removeListener(func) { 12 | map.set(this, (map.get(this) || []).filter((item) => item !== func)) 13 | } 14 | simulateChange() { 15 | (map.get(this) || []).forEach((func) => func()) 16 | } 17 | } 18 | 19 | const matchMediaMock = (mediaQuery) => { 20 | return new MediaQueryList(mediaQuery) 21 | } 22 | 23 | export const createMockWithHook = (onCreate) => { 24 | return (mediaQuery) => { 25 | const value = matchMediaMock(mediaQuery) 26 | onCreate(value) 27 | return value 28 | } 29 | } 30 | 31 | export default matchMediaMock 32 | -------------------------------------------------------------------------------- /src/__tests__/viewportGetter-test.js: -------------------------------------------------------------------------------- 1 | import viewportGetter from "../viewportGetter" 2 | 3 | tape("viewportGetter", (test) => { 4 | test.equal( 5 | Object.keys(viewportGetter()).length, 6 | 1 7 | ) 8 | test.equal( 9 | Object.keys(viewportGetter().viewport).length, 10 | 2 11 | ) 12 | test.equal( 13 | typeof viewportGetter().viewport.width, 14 | "number" 15 | ) 16 | test.equal( 17 | typeof viewportGetter().viewport.height, 18 | "number" 19 | ) 20 | test.end() 21 | }) 22 | -------------------------------------------------------------------------------- /src/__tests__/viewportListener-test.js: -------------------------------------------------------------------------------- 1 | import viewportListener from "../viewportListener" 2 | 3 | tape("viewportListener", (test) => { 4 | const originalAddEventListener = window.addEventListener 5 | const originalRemoveEventListener = window.removeEventListener 6 | let passedListener 7 | window.addEventListener = (type, listener) => { 8 | test.equal(type, "resize") 9 | test.equal(typeof listener, "function") 10 | passedListener = listener 11 | } 12 | window.removeEventListener = (type, listener) => { 13 | test.equal(type, "resize") 14 | test.equal(listener, passedListener) 15 | test.end() 16 | window.addEventListener = originalAddEventListener 17 | window.removeEventListener = originalRemoveEventListener 18 | } 19 | const teardown = viewportListener(() => {}) 20 | teardown() 21 | }) 22 | -------------------------------------------------------------------------------- /src/__tests__/warning-test.js: -------------------------------------------------------------------------------- 1 | import warning from "../warning" 2 | 3 | const skip = process.env.NODE_ENV === "production" 4 | 5 | tape("warning should not react if condition is true", { skip }, (test) => { 6 | const originalConsoleError = console.error 7 | console.error = () => test.fail() 8 | warning(true, "should not") 9 | test.pass("ok") 10 | test.end() 11 | console.error = originalConsoleError 12 | }) 13 | 14 | tape("warning should react if condition is false", { skip }, (test) => { 15 | const originalConsoleError = console.error 16 | console.error = () => test.pass() 17 | warning(false, "should") 18 | test.end() 19 | console.error = originalConsoleError 20 | }) 21 | 22 | tape("warning should replace %s segments and prefix", { skip }, (test) => { 23 | const originalConsoleError = console.error 24 | console.error = (message) => { 25 | test.equal(message, "react-media-queries : should replace") 26 | } 27 | warning(false, "should %s", "replace") 28 | test.end() 29 | console.error = originalConsoleError 30 | }) 31 | -------------------------------------------------------------------------------- /src/composeGetters.js: -------------------------------------------------------------------------------- 1 | const composeGetters = (...getters) => { 2 | return () => getters.reduce( 3 | (acc, func) => { 4 | return { 5 | ...acc, 6 | ...func(), 7 | } 8 | }, 9 | {} 10 | ) 11 | } 12 | 13 | export default composeGetters 14 | -------------------------------------------------------------------------------- /src/composeListeners.js: -------------------------------------------------------------------------------- 1 | const composeListeners = (...listeners) => { 2 | return (a) => listeners.reduce( 3 | (acc, func) => { 4 | const value = func(a) 5 | return (b) => { 6 | acc(b) 7 | value(b) 8 | } 9 | }, 10 | () => {} 11 | ) 12 | } 13 | 14 | export default composeListeners 15 | -------------------------------------------------------------------------------- /src/createConnector.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from "react" 2 | import warning from "./warning" 3 | import getCollidingKey from "./getCollidingKey" 4 | 5 | const defaultMergeProps = (ownProps, mediaProps, componentProps) => { 6 | if(process.env.NODE_ENV !== "production") { 7 | let key = getCollidingKey(ownProps, mediaProps, componentProps) 8 | if(key) { 9 | warning( 10 | false, 11 | "colliding key %s in props merge", 12 | key 13 | ) 14 | } 15 | } 16 | return { 17 | ...ownProps, 18 | ...mediaProps, 19 | ...componentProps, 20 | } 21 | } 22 | 23 | const defaultResolveComponents = () => null 24 | 25 | const createConnector = ( 26 | ComposedComponent, 27 | resolveComponents = defaultResolveComponents, 28 | mergeProps = defaultMergeProps 29 | ) => { 30 | class MatchMedia extends Component { 31 | static contextTypes = { 32 | mediaQuery: PropTypes.object, 33 | } 34 | componentWillMount() { 35 | this.mediaQuery = this.context.mediaQuery 36 | this.resolveComponents() 37 | } 38 | componentDidUpdate() { 39 | if(this.context.mediaQuery !== this.mediaQuery) { 40 | this.resolveComponents() 41 | this.mediaQuery = this.context.mediaQuery 42 | } 43 | } 44 | resolveComponents() { 45 | const callback = (components) => this.setState({ ...components }) 46 | const syncResolved = resolveComponents(this.context.mediaQuery, callback) 47 | if(typeof syncResolved === "object") { 48 | this.setState({ ...syncResolved }) 49 | } 50 | } 51 | render() { 52 | const { mediaQuery } = this.context 53 | return ( 54 | 56 | ) 57 | } 58 | } 59 | return MatchMedia 60 | } 61 | 62 | export default createConnector 63 | -------------------------------------------------------------------------------- /src/createMediaQueryGetter.js: -------------------------------------------------------------------------------- 1 | const createMediaQueryGetter = (mediaQueries) => () => { 2 | const mediaQuery = Object.keys(mediaQueries).reduce((results, alias) => { 3 | const mql = window.matchMedia(mediaQueries[alias]) 4 | const { matches, media } = mql 5 | results[alias] = { matches, media } 6 | return results 7 | }, {}) 8 | 9 | return { mediaQuery } 10 | } 11 | 12 | export default createMediaQueryGetter 13 | -------------------------------------------------------------------------------- /src/createMediaQueryListener.js: -------------------------------------------------------------------------------- 1 | const createMediaQueryListener = (mediaQueries) => (update) => { 2 | const mqlList = Object.keys(mediaQueries).map((alias) => { 3 | const mql = window.matchMedia(mediaQueries[alias]) 4 | mql.addListener(update) 5 | return mql 6 | }) 7 | 8 | return () => mqlList.forEach((mql) => mql.removeListener(update)) 9 | } 10 | 11 | export default createMediaQueryListener 12 | -------------------------------------------------------------------------------- /src/getCollidingKey.js: -------------------------------------------------------------------------------- 1 | const getCollidingKey = (...args) => { 2 | const map = {} 3 | let index = -1 4 | const length = args.length 5 | while(++index < length) { 6 | let object = args[index] 7 | for(let key in object) { 8 | if(!object.hasOwnProperty(key)) { 9 | continue 10 | } 11 | if(map.hasOwnProperty(key)) { 12 | return key 13 | } 14 | map[key] = true 15 | } 16 | } 17 | return null 18 | } 19 | 20 | export default getCollidingKey 21 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export matchMedia from "./matchMedia" 2 | export MediaProvider from "./MediaProvider" 3 | -------------------------------------------------------------------------------- /src/matchMedia.js: -------------------------------------------------------------------------------- 1 | import createConnector from "./createConnector" 2 | 3 | const matchMedia = (resolveComponents, mergeProps) => (ComposedComponent) => 4 | createConnector(ComposedComponent, resolveComponents, mergeProps) 5 | 6 | export default matchMedia 7 | -------------------------------------------------------------------------------- /src/viewportGetter.js: -------------------------------------------------------------------------------- 1 | const viewportGetter = () => { 2 | return { 3 | viewport: { 4 | height: window.innerHeight, 5 | width: window.innerWidth, 6 | }, 7 | } 8 | } 9 | 10 | export default viewportGetter 11 | -------------------------------------------------------------------------------- /src/viewportListener.js: -------------------------------------------------------------------------------- 1 | const viewportListener = (update) => { 2 | window.addEventListener("resize", update) 3 | return () => window.removeEventListener("resize", update) 4 | } 5 | 6 | export default viewportListener 7 | -------------------------------------------------------------------------------- /src/warning.js: -------------------------------------------------------------------------------- 1 | var warning = () => {} 2 | 3 | if( 4 | process.env.NODE_ENV !== "production" && 5 | typeof console !== "undefined" && 6 | typeof console.error === "function" 7 | ) { 8 | warning = function(condition, message, ...args) { 9 | if(condition) { 10 | return 11 | } 12 | let index = -1 13 | const warningMessage = message.replace(/%s/g, () => args[++index]) 14 | console.error("react-media-queries : " + warningMessage) 15 | } 16 | } 17 | 18 | export default warning 19 | --------------------------------------------------------------------------------