15 |
20 | {opened &&
21 |
22 |
this.setState({ opened: false })}/>
25 |
32 |
33 | }
34 |
35 | )
36 | }
37 | }
38 |
39 | const hamburger = (
40 |
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 |
--------------------------------------------------------------------------------