├── .babelrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── README.md
├── demo
├── bundle.js
├── entry.js
└── index.html
├── package.json
├── src
└── MediaContext.js
├── test
└── MediaContext.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "stage-0",
5 | "react"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jxnblk/react-media-context/5b8e8ae37bb4dc36c23aaf93ac0fca7534289df3/.npmignore
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 6.2
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # react-media-context
3 |
4 | [](https://travis-ci.org/jxnblk/react-media-context)
5 |
6 | React higher-order component (HOC) to provide context for the currently matched media query.
7 |
8 | ## Getting Started
9 |
10 | ```sh
11 | npm i -S react-media-context
12 | ```
13 |
14 | ## Usage
15 |
16 | ```js
17 | import React from 'react'
18 | import MediaContext from 'react-media-context'
19 |
20 | const Title = (props, context) => {
21 | const { media } = context
22 | let fontSize = 32
23 | if (media.indexOf('large') > -1) {
24 | fontSize = 64
25 | }
26 |
27 | const style = {
28 | fontSize
29 | }
30 |
31 | return (
32 |
33 | Responsive Heading
34 |
35 | )
36 | }
37 |
38 | Title.contextTypes = {
39 | media: React.PropTypes.array
40 | }
41 |
42 | class App extends React.Component {
43 | render () {
44 |
45 |
46 |
47 | }
48 | }
49 | ```
50 |
51 | ## How is this different from *x*?
52 |
53 | Most other responsive React HOCs tend to work on the principle of *showing and hiding* children based on media queries. With this component, you can alter styling and functionality of components responsively.
54 |
55 | This could be handy for:
56 | - Changing font sizes based on the viewport width
57 | - Changing margin or padding
58 | - Grid systems
59 | - Dramatically altering page layout
60 | - Using different routing flows
61 | - Changing image sources based on pixel density (but, srcset is probably a better option)
62 |
63 | ## Props
64 |
65 | - `queries` (Object) - Media queries to match against on window resize
66 |
67 | ```js
68 | // Default
69 | {
70 | 'xsmall': 'screen and (max-width: 40em)',
71 | 'small': 'screen and (min-width: 40em)',
72 | 'medium': 'screen and (min-width: 52em)',
73 | 'large': 'screen and (min-width: 64em)'
74 | }
75 | ```
76 |
77 | MIT License
78 |
79 |
--------------------------------------------------------------------------------
/demo/entry.js:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react'
3 | import ReactDOM from 'react-dom'
4 | import MediaContext from '../src/MediaContext'
5 | import readme from 'html!markdown!../README.md'
6 |
7 | const div = document.getElementById('app')
8 |
9 | const Header = ({}, { media }) => {
10 | let fontSize = 32
11 |
12 | if (media.indexOf('large') > -1) {
13 | fontSize = 96
14 | } else if (media.indexOf('medium') > -1) {
15 | fontSize = 64
16 | }
17 |
18 | const sx = {
19 | root: {
20 | padding: media.indexOf('xsmall') > -1 ? 16 : 32
21 | },
22 | heading: {
23 | fontSize,
24 | margin: 0
25 | }
26 | }
27 |
28 | return (
29 |
30 |
31 | Hello
32 |
33 |
34 | {' provider'}
35 |
36 | GitHub
37 |
38 | )
39 | }
40 |
41 | const Readme = (props, { media }) => {
42 | const sx = {
43 | maxWidth: '48em',
44 | padding: media.indexOf('xsmall') > -1 ? 16 : 32
45 | }
46 |
47 | return (
48 |
51 | )
52 | }
53 |
54 | Readme.contextTypes = {
55 | media: React.PropTypes.array
56 | }
57 |
58 | Header.contextTypes = {
59 | media: React.PropTypes.array
60 | }
61 |
62 | class App extends React.Component {
63 | render () {
64 | return (
65 |
66 |
67 |
68 |
69 | )
70 | }
71 | }
72 |
73 | ReactDOM.render(, div)
74 |
75 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-media-context",
3 | "version": "1.0.0",
4 | "description": "React component to provide media query matches in context",
5 | "main": "dist/MediaContext.js",
6 | "scripts": {
7 | "prepublish": "mkdir -p dist && babel src --out-dir dist",
8 | "demo": "webpack -p",
9 | "start": "webpack-dev-server",
10 | "gh-pages": "gh-pages -d demo",
11 | "test": "mocha test --compilers js:babel-register"
12 | },
13 | "keywords": [
14 | "react",
15 | "react-component",
16 | "media-queries",
17 | "match-media",
18 | "responsive"
19 | ],
20 | "author": "Brent Jackson",
21 | "license": "MIT",
22 | "peerDependencies": {
23 | "react": "^15.1.0"
24 | },
25 | "devDependencies": {
26 | "babel-cli": "^6.10.1",
27 | "babel-loader": "^6.2.4",
28 | "babel-preset-es2015": "^6.9.0",
29 | "babel-preset-react": "^6.5.0",
30 | "babel-preset-stage-0": "^6.5.0",
31 | "babel-register": "^6.9.0",
32 | "enzyme": "^2.3.0",
33 | "expect": "^1.20.1",
34 | "gh-pages": "^0.11.0",
35 | "html-loader": "^0.4.3",
36 | "markdown-loader": "^0.1.7",
37 | "mocha": "^2.5.3",
38 | "mocha-jsdom": "^1.1.0",
39 | "react": "^15.1.0",
40 | "react-addons-test-utils": "^15.1.0",
41 | "react-dom": "^15.1.0",
42 | "webpack": "^1.13.1",
43 | "webpack-dev-server": "^1.14.1"
44 | },
45 | "dependencies": {
46 | "lodash": "^4.13.1"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/MediaContext.js:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react'
3 | import { debounce } from 'lodash'
4 |
5 | class MediaContext extends React.Component {
6 | constructor () {
7 | super()
8 | this.state = {
9 | media: []
10 | }
11 | this.match = this.match.bind(this)
12 | this.handleResize = debounce(this.handleResize.bind(this), 100)
13 | }
14 |
15 | getChildContext () {
16 | return this.state
17 | }
18 |
19 | match () {
20 | const { queries } = this.props
21 | const media = []
22 | for (var key in queries) {
23 | const { matches } = window.matchMedia(queries[key])
24 | if (matches) {
25 | media.push(key)
26 | }
27 | }
28 | this.setState({ media })
29 | }
30 |
31 | handleResize () {
32 | this.match()
33 | }
34 |
35 | componentDidMount () {
36 | this.match()
37 | window.addEventListener('resize', this.handleResize)
38 | }
39 |
40 | render () {
41 | return (
42 |
43 | {this.props.children}
44 |
45 | )
46 | }
47 | }
48 |
49 | MediaContext.childContextTypes = {
50 | media: React.PropTypes.array
51 | }
52 |
53 | MediaContext.propTypes = {
54 | queries: React.PropTypes.object
55 | }
56 |
57 | MediaContext.defaultProps = {
58 | queries: {
59 | 'xsmall': 'screen and (max-width: 40em)',
60 | 'small': 'screen and (min-width: 40em)',
61 | 'medium': 'screen and (min-width: 52em)',
62 | 'large': 'screen and (min-width: 64em)'
63 | }
64 | }
65 |
66 | export default MediaContext
67 |
68 |
--------------------------------------------------------------------------------
/test/MediaContext.js:
--------------------------------------------------------------------------------
1 |
2 | require('mocha-jsdom')()
3 |
4 | import React from 'react'
5 | import ReactDOMServer from 'react-dom/server'
6 | import expect from 'expect'
7 | import { mount } from 'enzyme'
8 | import MediaContext from '../src/MediaContext'
9 |
10 | describe('', () => {
11 | let tree
12 |
13 | before(() => {
14 | // Stub matchMedia
15 | window.matchMedia = () => {
16 | return {
17 | matches: false
18 | }
19 | }
20 | })
21 |
22 | it('should render', () => {
23 | expect(() => {
24 | tree = mount()
25 | }).toNotThrow()
26 | })
27 |
28 | it('should have a default queries prop', () => {
29 | expect(tree.props().queries).toBeAn('object')
30 | })
31 |
32 | it('should have child context', () => {
33 | expect(tree.node.getChildContext().media).toEqual([])
34 | })
35 |
36 | context('when xsmall matches', () => {
37 | before(() => {
38 | window.matchMedia = (query) => {
39 | return {
40 | matches: /max\-width:\s40em\)$/.test(query)
41 | }
42 | }
43 | tree = mount()
44 | })
45 |
46 | it('should include xsmall in the media context', () => {
47 | const { media } = tree.node.getChildContext()
48 | expect(media).toEqual(['xsmall'])
49 | })
50 | })
51 |
52 | context('when small matches', () => {
53 | before(() => {
54 | window.matchMedia = (query) => {
55 | return {
56 | matches: /min\-width:\s40em\)$/.test(query)
57 | }
58 | }
59 | tree = mount()
60 | })
61 |
62 | it('should include small in the media context', () => {
63 | const { media } = tree.node.getChildContext()
64 | expect(media).toEqual(['small'])
65 | })
66 | })
67 |
68 | context('when medium matches', () => {
69 | before(() => {
70 | window.matchMedia = (query) => {
71 | return {
72 | matches: /min\-width:\s52em\)$/.test(query)
73 | }
74 | }
75 | tree = mount()
76 | })
77 |
78 | it('should include medium in the media context', () => {
79 | const { media } = tree.node.getChildContext()
80 | expect(media).toEqual(['medium'])
81 | })
82 | })
83 |
84 | context('when large matches', () => {
85 | before(() => {
86 | window.matchMedia = (query) => {
87 | return {
88 | matches: /min\-width:\s64em\)$/.test(query)
89 | }
90 | }
91 | tree = mount()
92 | })
93 |
94 | it('should include large in the media context', () => {
95 | const { media } = tree.node.getChildContext()
96 | expect(media).toEqual(['large'])
97 | })
98 | })
99 |
100 | context('when setting custom queries', () => {
101 | before(() => {
102 | window.matchMedia = (query) => {
103 | return {
104 | matches: /min\-width:\s640px\)$/.test(query)
105 | }
106 | }
107 |
108 | tree = mount(
109 |
114 | )
115 | })
116 |
117 | it('should include custom media queries in context', () => {
118 | const { media } = tree.node.getChildContext()
119 | expect(media).toEqual(['desktop'])
120 | })
121 | })
122 |
123 | context('when resizing the window', () => {
124 | let match
125 | let handleResize
126 |
127 | before(() => {
128 | window.matchMedia = (query) => {
129 | return { matches: false }
130 | }
131 | match = expect.spyOn(MediaContext.prototype, 'match')
132 | handleResize = expect.spyOn(MediaContext.prototype, 'handleResize')
133 | tree = mount()
134 | const e = new Event('resize')
135 | window.dispatchEvent(e)
136 | })
137 |
138 | it('should call the handleResize method', () => {
139 | expect(handleResize.call.length).toEqual(1)
140 | })
141 |
142 | it('should call the match method', () => {
143 | expect(match.calls.length).toEqual(1)
144 | })
145 | })
146 |
147 | context('when rendering server-side', () => {
148 | it('should render', () => {
149 | expect(() => {
150 | const html = ReactDOMServer.renderToString()
151 | }).toNotThrow()
152 | })
153 | })
154 | })
155 |
156 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 |
2 | const config = {
3 | entry: './demo/entry.js',
4 | output: {
5 | path: __dirname + '/demo',
6 | filename: 'bundle.js'
7 | },
8 | module: {
9 | loaders: [
10 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel' }
11 | ]
12 | },
13 | devServer: {
14 | contentBase: 'demo'
15 | }
16 | }
17 |
18 | module.exports = config
19 |
20 |
--------------------------------------------------------------------------------