├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .npmrc ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── example ├── data.js ├── entry.css └── entry.jsx ├── package-lock.json ├── package.json ├── scripts.sh ├── src └── index.jsx ├── test ├── browser.jsx └── server.jsx └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "latest", 4 | "react" 5 | ], 6 | "plugins": [ 7 | "add-module-exports", 8 | "transform-object-rest-spread", 9 | "transform-undefined-to-void", 10 | "transform-decorators-legacy", 11 | "transform-class-properties" 12 | ], 13 | "sourceMaps": "inline" 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://EditorConfig.org 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "react" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "node": true, 9 | "es6": true 10 | }, 11 | "rules": { 12 | "comma-dangle": [ 13 | 2, 14 | "never" 15 | ], 16 | "no-cond-assign": 2, 17 | "no-constant-condition": 2, 18 | "no-control-regex": 2, 19 | "no-debugger": 2, 20 | "no-dupe-keys": 2, 21 | "no-empty": 2, 22 | "no-empty-character-class": 2, 23 | "no-ex-assign": 2, 24 | "no-extra-boolean-cast": 2, 25 | "no-extra-parens": 0, 26 | "no-extra-semi": 2, 27 | "no-func-assign": 2, 28 | "no-inner-declarations": 2, 29 | "no-invalid-regexp": 2, 30 | "no-irregular-whitespace": 2, 31 | "no-negated-in-lhs": 2, 32 | "no-obj-calls": 2, 33 | "no-regex-spaces": 2, 34 | "quote-props": [ 35 | 1, 36 | "as-needed" 37 | ], 38 | "no-sparse-arrays": 2, 39 | "no-unreachable": 2, 40 | "use-isnan": 2, 41 | "valid-typeof": 2, 42 | "block-scoped-var": 0, 43 | "consistent-return": 2, 44 | "curly": [ 45 | 1, 46 | "multi-line" 47 | ], 48 | "default-case": 2, 49 | "dot-notation": 2, 50 | "eqeqeq": 2, 51 | "guard-for-in": 2, 52 | "no-alert": 1, 53 | "no-caller": 2, 54 | "no-div-regex": 2, 55 | "no-eq-null": 2, 56 | "no-eval": 2, 57 | "no-extend-native": 2, 58 | "no-extra-bind": 2, 59 | "no-fallthrough": 2, 60 | "no-floating-decimal": 2, 61 | "no-implied-eval": 2, 62 | "no-iterator": 2, 63 | "no-labels": 2, 64 | "no-lone-blocks": 2, 65 | "no-loop-func": 2, 66 | "no-multi-spaces": 1, 67 | "no-multi-str": 2, 68 | "no-native-reassign": 2, 69 | "no-new": 2, 70 | "no-new-func": 2, 71 | "no-new-wrappers": 2, 72 | "no-octal": 2, 73 | "no-octal-escape": 2, 74 | "no-proto": 2, 75 | "no-redeclare": 2, 76 | "no-return-assign": [ 77 | 2, 78 | "except-parens" 79 | ], 80 | "no-script-url": 2, 81 | "no-self-compare": 2, 82 | "no-sequences": 2, 83 | "no-throw-literal": 2, 84 | "no-unused-expressions": 0, 85 | "no-with": 2, 86 | "radix": 2, 87 | "vars-on-top": 2, 88 | "wrap-iife": 2, 89 | "yoda": [ 90 | 1, 91 | "never" 92 | ], 93 | "strict": [ 94 | 2, 95 | "global" 96 | ], 97 | "no-delete-var": 2, 98 | "no-label-var": 2, 99 | "no-shadow": 2, 100 | "no-shadow-restricted-names": 2, 101 | "no-undef": 2, 102 | "no-undef-init": 2, 103 | "no-undefined": 2, 104 | "no-unused-vars": [ 105 | 2, 106 | "all" 107 | ], 108 | "no-use-before-define": 2, 109 | "handle-callback-err": 2, 110 | "no-mixed-requires": 0, 111 | "no-new-require": 2, 112 | "no-path-concat": 2, 113 | "indent": [ 114 | 1, 115 | 2 116 | ], 117 | "brace-style": [ 118 | 1, 119 | "stroustrup", 120 | { 121 | "allowSingleLine": false 122 | } 123 | ], 124 | "comma-spacing": [ 125 | 1, 126 | { 127 | "before": false, 128 | "after": true 129 | } 130 | ], 131 | "comma-style": [ 132 | 2, 133 | "first" 134 | ], 135 | "consistent-this": [ 136 | 1, 137 | "self" 138 | ], 139 | "eol-last": 2, 140 | "func-names": 1, 141 | "func-style": [ 142 | 2, 143 | "expression" 144 | ], 145 | "key-spacing": [ 146 | 1, 147 | { 148 | "beforeColon": false, 149 | "afterColon": true 150 | } 151 | ], 152 | "keyword-spacing": 2, 153 | "max-nested-callbacks": [ 154 | 2, 155 | 3 156 | ], 157 | "new-cap": 2, 158 | "new-parens": 2, 159 | "no-array-constructor": 0, 160 | "no-inline-comments": 1, 161 | "no-lonely-if": 0, 162 | "no-mixed-spaces-and-tabs": 2, 163 | "no-multiple-empty-lines": 2, 164 | "no-nested-ternary": 0, 165 | "no-new-object": 2, 166 | "semi-spacing": [ 167 | 2, 168 | { 169 | "before": false, 170 | "after": true 171 | } 172 | ], 173 | "no-spaced-func": 1, 174 | "no-ternary": 0, 175 | "no-trailing-spaces": 2, 176 | "no-underscore-dangle": 0, 177 | "one-var": [ 178 | 1, 179 | { 180 | "var": "always", 181 | "let": "never", 182 | "const": "never" 183 | } 184 | ], 185 | "operator-assignment": [ 186 | 2, 187 | "always" 188 | ], 189 | "padded-blocks": [ 190 | 1, 191 | "never" 192 | ], 193 | "quotes": [ 194 | 2, 195 | "single" 196 | ], 197 | "semi": [ 198 | 2, 199 | "never" 200 | ], 201 | "sort-vars": 0, 202 | "space-before-blocks": 0, 203 | "space-before-function-paren": [ 204 | 1, 205 | "always" 206 | ], 207 | "object-curly-spacing": [ 208 | 1, 209 | "never" 210 | ], 211 | "array-bracket-spacing": [ 212 | 1, 213 | "never" 214 | ], 215 | "space-in-parens": [ 216 | 1, 217 | "never" 218 | ], 219 | "space-infix-ops": 2, 220 | "space-unary-ops": [ 221 | 1, 222 | { 223 | "words": true, 224 | "nonwords": false 225 | } 226 | ], 227 | "spaced-comment": [ 228 | 1, 229 | "always", 230 | { 231 | "exceptions": [ 232 | "-" 233 | ] 234 | } 235 | ], 236 | "wrap-regex": 0, 237 | "constructor-super": 2, 238 | "no-this-before-super": 2, 239 | "require-yield": 2, 240 | "prefer-spread": 1, 241 | "no-useless-call": 1, 242 | "no-invalid-this": 0, 243 | "no-implicit-coercion": 0, 244 | "no-const-assign": 2, 245 | "no-class-assign": 2, 246 | "init-declarations": 0, 247 | "callback-return": [ 248 | 0, 249 | [ 250 | "callback", 251 | "cb", 252 | "done", 253 | "next" 254 | ] 255 | ], 256 | "no-invalid-this": 2, 257 | "no-var": 1, 258 | "prefer-const": 1, 259 | "no-magic-numbers": [1, {"detectObjects": true, "enforceConst": true}], 260 | "arrow-spacing": [ 261 | 1, 262 | { 263 | "before": true, 264 | "after": true 265 | } 266 | ], 267 | "arrow-parens": 1, 268 | "jsx-quotes": [ 269 | 1, 270 | "prefer-double" 271 | ], 272 | "react/jsx-uses-react": 1, 273 | "react/jsx-uses-vars": 1, 274 | "react/jsx-no-undef": 2, 275 | "react/no-did-mount-set-state": 1, 276 | "react/no-did-update-set-state": 1, 277 | "react/no-multi-comp": 2, 278 | "react/no-unknown-property": 2, 279 | "react/self-closing-comp": 1, 280 | "react/sort-comp": [ 281 | 1, 282 | { 283 | "order": [ 284 | "contextTypes", 285 | "propTypes", 286 | "defaultProps", 287 | "state", 288 | "constructor", 289 | "statics", 290 | "lifecycle", 291 | "everything-else", 292 | "/^on.*$$/", 293 | "/^render([A-z]{1,})$/", 294 | "render" 295 | ], 296 | "groups": { 297 | "lifecycle": [ 298 | "getDefaultProps", 299 | "getInitialState", 300 | "componentWillMount", 301 | "componentDidMount", 302 | "componentWillReceiveProps", 303 | "shouldComponentUpdate", 304 | "componentWillUpdate", 305 | "componentDidUpdate", 306 | "componentWillUnmount" 307 | ] 308 | } 309 | } 310 | ], 311 | "react/prop-types": 2, 312 | "react/react-in-jsx-scope": 2, 313 | "react/jsx-wrap-multilines": 2 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.patch 10 | *.swp 11 | .DS_store 12 | pids 13 | logs 14 | results 15 | npm-debug.log 16 | node_modules 17 | .yo-rc.json 18 | test/temp 19 | *.es5 20 | .cache 21 | dist 22 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .travis.yml 2 | test/* 3 | .editorconfig 4 | .gitignore 5 | .jscsrc 6 | .travis.yml 7 | .git* 8 | .jshint* 9 | .npmignore 10 | .yo-rc.json 11 | .npmrc 12 | CONTRIBUTING.* 13 | CHANGELOG.* 14 | scripts.sh 15 | npm-debug.log 16 | .DS_Store 17 | .eslintrc 18 | *.jsx 19 | 20 | 21 | example/ 22 | test/ -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-prefix=^ 2 | save=true 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | # run 3 | node_js: 4 | # 4 is LTS 5 | - "4" 6 | - "stable" 7 | 8 | script: 9 | - "npm run nsp && npm run lint && npm test" 10 | 11 | notifications: 12 | email: false 13 | 14 | sudo: false 15 | 16 | before_install: 17 | - "npm install -g npm@latest" 18 | 19 | # https://docs.travis-ci.com/user/gui-and-headless-browsers/#Using-xvfb-to-Run-Tests-That-Require-a-GUI 20 | before_script: 21 | - "export DISPLAY=:99.0" 22 | - "sh -e /etc/init.d/xvfb start" 23 | - sleep 3 # give xvfb some time to start 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v3.0.3 | 2017-12-18 4 | * Feat switch to PureComponent and proptypes 5 | * fix(package): update autobind-decorator to version 2.0.0 6 | * fix(package): update proptypes to version 1.0.0 7 | * chore(package): update sinon to version 3.0.0 8 | * Chore(eslint) rm deprecated property 9 | * chore(package): update eslint to version 4.1.1 10 | * Chore(eslint) update for deprecated rule 11 | * chore(package): update eslint-plugin-react to version 7.1.0 12 | * chore(package): update babel-eslint to version 7.2.3 13 | * Chore(CI) switch back to firefox 14 | * Chore(deps) upgrade deps and add yarn 15 | * Chore(deps) rm pureRender and add proptypes 16 | * Chore switch to testing in Chrome 17 | * Chore(deps) install prop-types 18 | * chore(package): update sinon to version 2.0.0 19 | * chore(package): update browserify to version 14.0.0 20 | * chore(package): update dependencies 21 | 22 | ## v3.0.2 | 2016-11-16 23 | * Chore(build) files are now split between `dist` and `src`. This means that 24 | the files are no longer `.jsx` which allows flowtype to work correctly. #12. 25 | Fixed by @madole in #13 26 | 27 | ## v3.0.1 | 2016-09-02 28 | * Fix correct onResize method signature #oops 29 | 30 | ## v3.0.0 | 2016-09-02 31 | BREAKING CHANGES: 32 | 33 | * now require react 15 as a peer depencency 34 | * child must be a valid DOM node not a react component 35 | 36 | ## v2.0.2 | 2016-05-01 37 | * Fix: after first browser render, check sizes again. This should ensure the 38 | first render actually sets sizes. 39 | * Internal: run tests against LTS and Stable only 40 | * Internal: Travis CI: fix browser tests 41 | * Internal: switch to separate updated lodash packages 42 | * Internal: bump eslint and pure-render-decorator deps 43 | * Internal: fix typo in the ElementQuery import 44 | 45 | ## v2.0.1 | 2015-12-03 46 | * fix: Children merge classNames, not replace 47 | * internal: update nsp 48 | 49 | ## v2.0.0 | 2015-11-11 50 | * change: update for react 0.14 51 | * change: deps updated, including major updates 52 | * internal: upgrade react test tree (breaking) 53 | * internal: linter #cleanup 54 | * internal: update tests for new testTree syntax 55 | * internal: Only test on node 4 56 | * docs: note react 0.14 compatiblity 57 | * doc: cleanup TOC 58 | * doc: typos 59 | 60 | ## v1.1.1 | 2015-08-15 61 | * fix: prevent server-side memory leak by never registering for events on the 62 | server. 63 | 64 | ## v1.1.0 | 2015-07-23 65 | * add: PropTypes warn on invalid widths. Non-numbers continue to be invalid, 66 | but we're no also checking for widths of `0`. Setting to `0` will defeat 67 | server-side rendering. Use the `default` prop or just "mobile-first" CSS. 68 | * internal: tests now fail on React warnings 69 | 70 | ## v1.0.0 | 2015-07-09 71 | Init 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We're open source! We love contributions! An ordered list of helpful things: 4 | 5 | 1. failing tests 6 | 2. patches with tests 7 | 3. bare patches 8 | 4. bug reports 9 | 5. problem statements 10 | 6. feature requests 11 | 12 | [via](https://twitter.com/othiym23/status/515619157287526400) 13 | 14 | 15 | 16 | **Table of Contents** *generated with [DocToc](http://doctoc.herokuapp.com/)* 17 | 18 | - [Creating issues](#creating-issues) 19 | - [Bug Issues](#bug-issues) 20 | - [Feature Issues](#feature-issues) 21 | - [Assignees](#assignees) 22 | - [Milestones](#milestones) 23 | - [Labels](#labels) 24 | - [Developing](#developing) 25 | 26 | 27 | 28 | ## Creating issues 29 | GitHub issues can be treated like a massive, communal todo list. If you notice something wrong, toss an issue in and we'll get to it! 30 | 31 | **TL;DR Put issues into the right milestone if avaliable. Don't create new milestones or labels. Talk to the responsible person on a milestone before adding issues to a milestone that have a due date.** 32 | 33 | ### Bug Issues 34 | * mark with the "bug" label 35 | * The following things are helpful 36 | * screenshots with a description showing the problem. 37 | * js console or server logs 38 | * contact information of users who experienced this request 39 | * the time of the bug so that relevant logs can be found 40 | * The following things should always be included 41 | * the steps it would take to reproduce the issue 42 | * what happened when you followed those steps 43 | * what you expected to happen that didn't 44 | 45 | ### Feature Issues 46 | * should be marked with the "enhancement" label 47 | 48 | ### Assignees 49 | * assignees are responsible for completing an issue. Do not assign a person to an issue unless they agree to it. Generally, people should assign themselves. 50 | 51 | ### Milestones 52 | * If your issue fits into a milestone please add it there. Issues with no milestone are fine – they'll be gone through periodically and assigned. 53 | * Creation of new milestones is by group consensus only. Don't do it on your own. 54 | * A milestone with a due date should have a "responsible person" listed in the description. That doesn't mean that person is the sole person to work on it, just that they're the one responsible for coordinating efforts around that chunk of work. 55 | * → Once a milestone has a due date, only issues okay'd by the responsible person can be added. This ensures that a chunk of work can be delivered by the promised due date. 56 | 57 | ### Labels 58 | * issues don't get a "prioritize this!" or "CRITICAL" label unless they really apply. "I want this new feature now" does not qualify as important. Generally, these labels should only be applied by people setting up a batch of work. Abuse these labels and they'll become meaningless. 59 | * Creation of new labels is by group consensus. Don't do it on your own! 60 | 61 | Some good ways to make sure it's not missed: 62 | * try to add any appropriate labels. 63 | * If this is a browser bug, add the "browser" label, and prefix your issue title with the browser version and the URL you encountered the problem on. e.g. `IE 9: /wisps/xxx can't click on the search input` 64 | * screenshots are always handy 65 | * If your issue is urgent, there's probably a milestone that it belongs in. 66 | 67 | ## Developing 68 | 69 | * Please follow the styleguide: https://github.com/joeybaker/styleguide-js and https://github.com/joeybaker/styleguide-css 70 | * Please add tests, we'd like to hit 100% code coverage 71 | * Please write meaningful commit messages. Keep them somewhat short and meaningful. Commit messages like “meh”, “fix”, “lol” and so on are useless. Your are writing to your future self and everyone else. It’s important to be able to tell what a commit is all about from its message. 72 | 73 | “Write every commit message like the next person who reads it is an axe-wielding maniac who knows where you live”. 74 | 75 | Good commit messages: 76 | ![good commit messages](https://blog.rainforestqa.com/images/version-control-best-practices/good-commit-messages.png?1412114618) 77 | 78 | [via](https://blog.rainforestqa.com/2014-05-28-version-control-best-practices/) 79 | 80 | * Thank you for contributing! 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Joey Baker and Contributors 2 | All rights reserved. 3 | 4 | This code is released under the Artistic License 2.0. 5 | The text of the License follows: 6 | 7 | 8 | -------- 9 | 10 | 11 | The Artistic License 2.0 12 | 13 | Copyright (c) 2000-2006, The Perl Foundation. 14 | 15 | Everyone is permitted to copy and distribute verbatim copies 16 | of this license document, but changing it is not allowed. 17 | 18 | Preamble 19 | 20 | This license establishes the terms under which a given free software 21 | Package may be copied, modified, distributed, and/or redistributed. 22 | The intent is that the Copyright Holder maintains some artistic 23 | control over the development of that Package while still keeping the 24 | Package available as open source and free software. 25 | 26 | You are always permitted to make arrangements wholly outside of this 27 | license directly with the Copyright Holder of a given Package. If the 28 | terms of this license do not permit the full use that you propose to 29 | make of the Package, you should contact the Copyright Holder and seek 30 | a different licensing arrangement. 31 | 32 | Definitions 33 | 34 | "Copyright Holder" means the individual(s) or organization(s) 35 | named in the copyright notice for the entire Package. 36 | 37 | "Contributor" means any party that has contributed code or other 38 | material to the Package, in accordance with the Copyright Holder's 39 | procedures. 40 | 41 | "You" and "your" means any person who would like to copy, 42 | distribute, or modify the Package. 43 | 44 | "Package" means the collection of files distributed by the 45 | Copyright Holder, and derivatives of that collection and/or of 46 | those files. A given Package may consist of either the Standard 47 | Version, or a Modified Version. 48 | 49 | "Distribute" means providing a copy of the Package or making it 50 | accessible to anyone else, or in the case of a company or 51 | organization, to others outside of your company or organization. 52 | 53 | "Distributor Fee" means any fee that you charge for Distributing 54 | this Package or providing support for this Package to another 55 | party. It does not mean licensing fees. 56 | 57 | "Standard Version" refers to the Package if it has not been 58 | modified, or has been modified only in ways explicitly requested 59 | by the Copyright Holder. 60 | 61 | "Modified Version" means the Package, if it has been changed, and 62 | such changes were not explicitly requested by the Copyright 63 | Holder. 64 | 65 | "Original License" means this Artistic License as Distributed with 66 | the Standard Version of the Package, in its current version or as 67 | it may be modified by The Perl Foundation in the future. 68 | 69 | "Source" form means the source code, documentation source, and 70 | configuration files for the Package. 71 | 72 | "Compiled" form means the compiled bytecode, object code, binary, 73 | or any other form resulting from mechanical transformation or 74 | translation of the Source form. 75 | 76 | 77 | Permission for Use and Modification Without Distribution 78 | 79 | (1) You are permitted to use the Standard Version and create and use 80 | Modified Versions for any purpose without restriction, provided that 81 | you do not Distribute the Modified Version. 82 | 83 | 84 | Permissions for Redistribution of the Standard Version 85 | 86 | (2) You may Distribute verbatim copies of the Source form of the 87 | Standard Version of this Package in any medium without restriction, 88 | either gratis or for a Distributor Fee, provided that you duplicate 89 | all of the original copyright notices and associated disclaimers. At 90 | your discretion, such verbatim copies may or may not include a 91 | Compiled form of the Package. 92 | 93 | (3) You may apply any bug fixes, portability changes, and other 94 | modifications made available from the Copyright Holder. The resulting 95 | Package will still be considered the Standard Version, and as such 96 | will be subject to the Original License. 97 | 98 | 99 | Distribution of Modified Versions of the Package as Source 100 | 101 | (4) You may Distribute your Modified Version as Source (either gratis 102 | or for a Distributor Fee, and with or without a Compiled form of the 103 | Modified Version) provided that you clearly document how it differs 104 | from the Standard Version, including, but not limited to, documenting 105 | any non-standard features, executables, or modules, and provided that 106 | you do at least ONE of the following: 107 | 108 | (a) make the Modified Version available to the Copyright Holder 109 | of the Standard Version, under the Original License, so that the 110 | Copyright Holder may include your modifications in the Standard 111 | Version. 112 | 113 | (b) ensure that installation of your Modified Version does not 114 | prevent the user installing or running the Standard Version. In 115 | addition, the Modified Version must bear a name that is different 116 | from the name of the Standard Version. 117 | 118 | (c) allow anyone who receives a copy of the Modified Version to 119 | make the Source form of the Modified Version available to others 120 | under 121 | 122 | (i) the Original License or 123 | 124 | (ii) a license that permits the licensee to freely copy, 125 | modify and redistribute the Modified Version using the same 126 | licensing terms that apply to the copy that the licensee 127 | received, and requires that the Source form of the Modified 128 | Version, and of any works derived from it, be made freely 129 | available in that license fees are prohibited but Distributor 130 | Fees are allowed. 131 | 132 | 133 | Distribution of Compiled Forms of the Standard Version 134 | or Modified Versions without the Source 135 | 136 | (5) You may Distribute Compiled forms of the Standard Version without 137 | the Source, provided that you include complete instructions on how to 138 | get the Source of the Standard Version. Such instructions must be 139 | valid at the time of your distribution. If these instructions, at any 140 | time while you are carrying out such distribution, become invalid, you 141 | must provide new instructions on demand or cease further distribution. 142 | If you provide valid instructions or cease distribution within thirty 143 | days after you become aware that the instructions are invalid, then 144 | you do not forfeit any of your rights under this license. 145 | 146 | (6) You may Distribute a Modified Version in Compiled form without 147 | the Source, provided that you comply with Section 4 with respect to 148 | the Source of the Modified Version. 149 | 150 | 151 | Aggregating or Linking the Package 152 | 153 | (7) You may aggregate the Package (either the Standard Version or 154 | Modified Version) with other packages and Distribute the resulting 155 | aggregation provided that you do not charge a licensing fee for the 156 | Package. Distributor Fees are permitted, and licensing fees for other 157 | components in the aggregation are permitted. The terms of this license 158 | apply to the use and Distribution of the Standard or Modified Versions 159 | as included in the aggregation. 160 | 161 | (8) You are permitted to link Modified and Standard Versions with 162 | other works, to embed the Package in a larger work of your own, or to 163 | build stand-alone binary or bytecode versions of applications that 164 | include the Package, and Distribute the result without restriction, 165 | provided the result does not expose a direct interface to the Package. 166 | 167 | 168 | Items That are Not Considered Part of a Modified Version 169 | 170 | (9) Works (including, but not limited to, modules and scripts) that 171 | merely extend or make use of the Package, do not, by themselves, cause 172 | the Package to be a Modified Version. In addition, such works are not 173 | considered parts of the Package itself, and are not subject to the 174 | terms of this license. 175 | 176 | 177 | General Provisions 178 | 179 | (10) Any use, modification, and distribution of the Standard or 180 | Modified Versions is governed by this Artistic License. By using, 181 | modifying or distributing the Package, you accept this license. Do not 182 | use, modify, or distribute the Package, if you do not accept this 183 | license. 184 | 185 | (11) If your Modified Version has been derived from a Modified 186 | Version made by someone other than you, you are nevertheless required 187 | to ensure that your Modified Version complies with the requirements of 188 | this license. 189 | 190 | (12) This license does not grant you the right to use any trademark, 191 | service mark, tradename, or logo of the Copyright Holder. 192 | 193 | (13) This license includes the non-exclusive, worldwide, 194 | free-of-charge patent license to make, have made, use, offer to sell, 195 | sell, import and otherwise transfer the Package with respect to any 196 | patent claims licensable by the Copyright Holder that are necessarily 197 | infringed by the Package. If you institute patent litigation 198 | (including a cross-claim or counterclaim) against any party alleging 199 | that the Package constitutes direct or contributory patent 200 | infringement, then this Artistic License to you shall terminate on the 201 | date that such litigation is filed. 202 | 203 | (14) Disclaimer of Warranty: 204 | THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS 205 | IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED 206 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR 207 | NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL 208 | LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL 209 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 210 | DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF 211 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 212 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-element-query [![NPM version][npm-image]][npm-url] [![Dependency Status][daviddm-url]][daviddm-image] [![Build Status][travis-image]][travis-url] 2 | 3 | Once you start thinking in components, media queries, which are reliant on the size of the whole screen, don't work well because components are frequently not the full screen-width. 4 | 5 | Element queries solve this, but CSS [won't have element queries for a while](http://discourse.specifiction.org/t/element-queries/26). [eq.js](https://github.com/snugug/eq.js) does a good job, but it doesn't play nicely with React. Besides which, using the React render pipeline has some nice performance and API benefits. 6 | 7 | This component is performant, and works on both the server and the client. 8 | 9 | 10 | 11 | **Table of Contents** *generated with [DocToc](http://doctoc.herokuapp.com/)* 12 | 13 | - [Install](#install) 14 | - [React 15](#react-15) 15 | - [React 0.14](#react-014) 16 | - [React 0.13](#react-013) 17 | - [Usage](#usage) 18 | - [Props](#props) 19 | - [` sizes` **Required**](#array-sizes-required) 20 | - [` makeClassName`](#function-makeclassname) 21 | - [` default`](#string-default) 22 | - [` children`](#node-children) 23 | - [Methods](#methods) 24 | - [`static` ElementQuery.register `({component: element, sizes: , node: [, onResize: ]})`](#static-elementqueryregister-component-react-element-element-sizes-array-node-domnode-onresize--function) 25 | - [`static` ElementQuery.unregister `( element)`](#static-elementqueryunregister-react-element-element) 26 | - [`static` ElementQuery.listen()](#static-elementquerylisten) 27 | - [`static` ElementQuery.unregister()](#static-elementqueryunregister) 28 | - [Developing](#developing) 29 | - [Requirements](#requirements) 30 | - [Tests](#tests) 31 | - [License](#license) 32 | 33 | 34 | 35 | ## Install 36 | 37 | ```sh 38 | npm i -S react-element-query 39 | ``` 40 | 41 | ### React 15 42 | Version 3.0 supports React 15. 43 | 44 | ```sh 45 | npm i -S react-element-query@3 46 | ``` 47 | 48 | ### React 0.14 49 | Version 2.0 supports React 0.14. 50 | 51 | ```sh 52 | npm i -S react-element-query@2 53 | ``` 54 | 55 | ### React 0.13 56 | If you need react 0.13 compatibility, install version 1.x. 57 | 58 | ```sh 59 | npm i -S react-element-query@1 60 | ``` 61 | 62 | 63 | ## Usage 64 | 65 | ```js 66 | import React from 'react' 67 | import ElementQuery from 'react-element-query' 68 | React.render(( 71 |

I have a `.small` when over 150px and `.large` when over 300px

72 |
), document.createElement('div')) 73 | ``` 74 | 75 | ## Props 76 | ### ` sizes` **Required** 77 | An array of objects containing minimum widths and classnames to apply. `[{name: 'large', width: 300}]` will apply the class `.large` to the child when the element is `300px` or larger. 78 | 79 | ### ` makeClassName` 80 | Takes the `name` for the matched size and returns a different classname. Useful if you want to apply a namespace to all your class names. 81 | 82 | ### ` default` 83 | The server has no way to know the browser window width, and therefore, can't calculate the element width, so by default, it assumes there is no element query class applied. If you'd like to set a different default, pass a size name. Defaults to `''`. 84 | 85 | ### ` children` 86 | The child that will get the element query magic. A few caveats: 87 | 88 | * This _must_ be a single child 89 | * This _must_ be a valid DOM element. e.g. `
` not `` 90 | 91 | ## Methods 92 | ### `static` ElementQuery.register `({component: element, sizes: , node: [, onResize: ]})` 93 | Allows you to use the ElementQuery throttled resize event for your own purposes. This allows you to have only one resize listener attached over your whole app. 94 | 95 | `onResize` is an optional callback so that is called with the arguments: ` element, sizes`. If not defined, the default behavior is used. 96 | 97 | ### `static` ElementQuery.unregister `( element)` 98 | Once a component is registered, you can unregister it. Do this when the component is unmounted. 99 | 100 | ### `static` ElementQuery.listen() 101 | Manually attach the resize listener. This is called automatically when a component is registered. 102 | 103 | ### `static` ElementQuery.unregister() 104 | Manually remove the resize listener. This is called automatically when all components have been unregistered. 105 | 106 | 107 | ## Developing 108 | To publish, run `npm run release -- [{patch,minor,major}]` 109 | 110 | _NOTE: you might need to `sudo ln -s /usr/local/bin/node /usr/bin/node` to ensure node is in your path for the git hooks to work_ 111 | 112 | ### Requirements 113 | * **npm > 2.0.0** So that passing args to a npm script will work. `npm i -g npm` 114 | * **git > 1.8.3** So that `git push --follow-tags` will work. `brew install git` 115 | 116 | ### Tests 117 | Tests are in [tape](https://github.com/substack/tape). 118 | 119 | * `npm test` will run both server and browser tests 120 | * `npm run test-browser` and `npm run test-server` run their respective tests 121 | * `npm run tdd-server` will run the server tests on every file change. 122 | * `npm run tdd-browser` will run the browser tests on every file change. 123 | 124 | ## License 125 | 126 | Artistic 2.0 © [Joey Baker](http://byjoeybaker.com) and contributors. A copy of the license can be found in the file `LICENSE`. 127 | 128 | 129 | [npm-url]: https://npmjs.org/package/react-element-query 130 | [npm-image]: https://badge.fury.io/js/react-element-query.svg 131 | [travis-url]: https://travis-ci.org/joeybaker/react-element-query 132 | [travis-image]: https://travis-ci.org/joeybaker/react-element-query.svg?branch=master 133 | [daviddm-url]: https://david-dm.org/joeybaker/react-element-query.svg?theme=shields.io 134 | [daviddm-image]: https://david-dm.org/joeybaker/react-element-query 135 | -------------------------------------------------------------------------------- /example/data.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'Element Query' 3 | } 4 | -------------------------------------------------------------------------------- /example/entry.css: -------------------------------------------------------------------------------- 1 | @import "../../_entry/"; 2 | 3 | .header { 4 | color: white; 5 | } 6 | 7 | .small { 8 | background: blue; 9 | } 10 | 11 | .large { 12 | background: red; 13 | } 14 | -------------------------------------------------------------------------------- /example/entry.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ElementQuery from '../index.jsx' 3 | import a11y from 'react-a11y' 4 | 5 | const WIDTH_LARGE = 500 6 | const WIDTH_SMALL = 300 7 | const WIDTH_TINY = 100 8 | const SIZE_LARGE = 'large' 9 | const SIZE_SMALL = 'small' 10 | 11 | // expose React for debugging 12 | window.React = React 13 | a11y(React) 14 | 15 | window.ElementQuery = ElementQuery 16 | 17 | const app = document.getElementById('app') 18 | const el1 = ( 19 |

300px and 500px breakpoints

20 |
) 21 | const el2 = ( 22 |

300px and 100px breakpoints

23 |
) 24 | 25 | const removeEl = () => { 26 | React.render((
27 | cleared. check the console to see that there are no more elements being listened to 28 |
), app, () => { 29 | console.info('elements being listened to:', ElementQuery.componentMap.size) 30 | }) 31 | } 32 | 33 | React.render((
34 |

elements are blue when they're small and red when they're large

35 | {el1} 36 |
37 |

I'm in a div that's 40% of the window width

38 | {el2} 39 |
40 | 41 |
), app) 42 | 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "3.0.3", 3 | "name": "react-element-query", 4 | "description": "Element queries for react components.", 5 | "author": { 6 | "name": "Joey Baker", 7 | "email": "joey@byjoeybaker.com", 8 | "url": "http://byjoeybaker.com" 9 | }, 10 | "repository": "joeybaker/react-element-query", 11 | "license": "Artistic-2.0", 12 | "directories": { 13 | "test": "test" 14 | }, 15 | "keywords": [ 16 | "css", 17 | "element query", 18 | "react" 19 | ], 20 | "scripts": { 21 | "start": "f () { node dev.js ${@-example} | garnish; }; f", 22 | "test": "npm run -s test-server && npm run -s test-browser", 23 | "test-browser": "browserify -t babelify test/browser.jsx --external react/addons --external react/lib/ReactContext --external react/lib/ExecutionEnvironment | tap-closer | smokestack -b firefox | tap-spec", 24 | "test-server": "babel-tape-runner test/server.jsx | tap-spec", 25 | "tdd-server": "nodemon -x npm -i node_modules/ -e js,jsx -- run -s test-server", 26 | "tdd-browser": "mkdir -p .cache; mkdir -p .cache/test; touch .cache/test/browser.js 2> /dev/null; watchify test/browser.jsx --transform babelify --external react/addons --external react/lib/ReactContext --external react/lib/ExecutionEnvironment --debug --outfile .cache/test/browser.js & devtool --watch .cache/test/browser.js --console --browser-field --no-node-timers --show .cache/test/browser.js | faucet", 27 | "watch": "babel src --watch --out-dir dist", 28 | "note1": "we can't have nice things. prepublish also runs on npm install https://github.com/npm/npm/issues/6394 in-publish hacks around this", 29 | "prepublish": "in-publish && npm prune && npm run -s gitPush || in-install", 30 | "note2": "eslint will always pull from the global eslintrc file, disable that so that we're only looking at the local", 31 | "note3": "travis doesn't play nicely with !#/bin/bash in the script file, so we have to explicitly set bash", 32 | "lint": "/bin/bash -c 'source ./scripts.sh && lint'", 33 | "note4": "the diff-filter option below gets all files but deleted ones", 34 | "lint-staged": "git diff --diff-filter=ACMRTUXB --cached --name-only | grep '.*\\..jsx$' | grep -v 'node_modules' | xargs eslint --ext .js --ext .jsx", 35 | "requireGitClean": "/bin/bash -c 'source ./scripts.sh && git_require_clean_work_tree'", 36 | "nsp": "nsp check", 37 | "note5": "--no-verify skips the commit hook", 38 | "dmn": "dmn gen -f . && if [[ $(git diff --shortstat 2> /dev/null | tail -n1) != '' ]]; then git add .npmignore && git commit --no-verify -m'update npmignore'; fi", 39 | "doctoc": "doctoc README.md && if [ -f CONTRIBUTING.md ]; then doctoc CONTRIBUTING.md; fi && if [[ $(git diff --shortstat -- README.md 2> /dev/null | tail -n1) != '' || $(git diff --shortstat -- CONTRIBUTING.md 2> /dev/null | tail -n1) != '' ]]; then git add README.md CONTRIBUTING.md && git commit --no-verify -m'table of contents update'; fi", 40 | "gitPull": "git pull --rebase origin master", 41 | "gitPush": "git push --follow-tags --no-verify && git push --tags --no-verify", 42 | "build": "cross-env NODE_ENV=production && babel src --out-dir dist", 43 | "release": "f () { source ./scripts.sh && npm run requireGitClean && npm run gitPull && npm run dmn && npm run doctoc && npm run build && npm run lint && npm test && npm_release public $@; }; f" 44 | }, 45 | "config": { 46 | "notes": "important to correct the path of npm so that the git hook doesn't error", 47 | "ghooks": { 48 | "pre-commit": "PATH=$PATH:/usr/local/bin:/usr/local/sbin && npm run lint-staged", 49 | "pre-push": "PATH=$PATH:/usr/local/bin:/usr/local/sbin && npm run dmn && npm run doctoc && npm run lint && npm test", 50 | "update": "PATH=$PATH:/usr/local/bin:/usr/local/sbin && npm install" 51 | } 52 | }, 53 | "main": "dist/index.js", 54 | "browser": "dist/index.js", 55 | "devDependencies": { 56 | "babel-cli": "^6.14.0", 57 | "babel-eslint": "^7.2.3", 58 | "babel-plugin-add-module-exports": "^0.2.1", 59 | "babel-plugin-transform-class-properties": "^6.11.5", 60 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 61 | "babel-plugin-transform-object-rest-spread": "^6.8.0", 62 | "babel-plugin-transform-undefined-to-void": "^6.8.0", 63 | "babel-preset-latest": "^6.14.0", 64 | "babel-preset-react": "^6.11.1", 65 | "babel-tape-runner": "^2.0.1", 66 | "babelify": "^7.3.0", 67 | "browserify": "^14.0.0", 68 | "cross-env": "^5.1.1", 69 | "devtool": "^2.2.0", 70 | "dmn": "^1.0.5", 71 | "doctoc": "^1.2.0", 72 | "enzyme": "^2.8.2", 73 | "eslint": "^4.1.1", 74 | "eslint-plugin-react": "^7.1.0", 75 | "faucet": "0.0.1", 76 | "ghooks": "^1.3.2", 77 | "hihat": "^2.6.4", 78 | "in-publish": "^2.0.0", 79 | "nodemon": "^1.10.2", 80 | "nsp": "^2.6.1", 81 | "prop-types": "^15.5.8", 82 | "react": "^15.5.4", 83 | "react-a11y": "^0.3.3", 84 | "react-addons-test-utils": "^15.3.1", 85 | "react-dom": "^15.5.4", 86 | "react-test-renderer": "^15.5.4", 87 | "sinon": "^3.0.0", 88 | "smokestack": "^3.2.0", 89 | "smokestack-watch": "^0.4.1", 90 | "tap-closer": "^1.0.0", 91 | "tap-dev-tool": "^1.3.0", 92 | "tap-spec": "^4.1.1", 93 | "tape": "^4.6.0", 94 | "watchify": "^3.7.0" 95 | }, 96 | "peerDependencies": { 97 | "react": ">=0.15.0", 98 | "react-dom": ">=0.15.0" 99 | }, 100 | "dependencies": { 101 | "autobind-decorator": "^2.0.0", 102 | "lodash.first": "^3.0.0", 103 | "lodash.identity": "^3.0.0", 104 | "lodash.isnumber": "^3.0.3", 105 | "lodash.sortby": "^4.7.0", 106 | "proptypes": "^1.0.0", 107 | "raf": "^3.3.0" 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /scripts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # strict mode http://redsymbol.net/articles/unofficial-bash-strict-mode/ 3 | set -euo pipefail 4 | IFS=$'\n\t' 5 | 6 | function lint(){ 7 | eslint --no-eslintrc --config .eslintrc.json "${@-.}" --ext .jsx --ext .js --ext .es6 8 | } 9 | 10 | function git_require_clean_work_tree(){ 11 | git diff --exit-code 12 | } 13 | 14 | function find_changelog_file(){ 15 | # find the changelog file 16 | local CHANGELOG="" 17 | if test "$CHANGELOG" = ""; then 18 | CHANGELOG="$(ls | egrep '^(change|history)' -i | head -n1)" 19 | if test "$CHANGELOG" = ""; then 20 | CHANGELOG="CHANGELOG.md"; 21 | fi 22 | fi 23 | echo $CHANGELOG 24 | } 25 | 26 | function find_last_git_tag(){ 27 | node -pe "a=$(npm version); 'v' + a[require('./package.json').name]" 28 | } 29 | 30 | # based on https://github.com/tj/git-extras/blob/master/bin/git-changelog 31 | function generate_git_changelog(){ 32 | GIT_LOG_OPTS="--no-merges" 33 | local DATE 34 | DATE=$(date +'%Y-%m-%d') 35 | local HEAD='## ' 36 | 37 | # get the commits between the most recent tag and the second most recent 38 | local lasttag 39 | lasttag=$(find_last_git_tag) 40 | local version 41 | version=$(git describe --tags --abbrev=0 "$lasttag" 2>/dev/null) 42 | local previous_version 43 | previous_version=$(git describe --tags --abbrev=0 "$lasttag^" 2>/dev/null) 44 | # if we don't have a previous version to look at 45 | if test -z "$version"; then 46 | local head="$HEAD$DATE" 47 | local changes 48 | changes=$(git log $GIT_LOG_OPTS --pretty="format:* %s%n" 2>/dev/null) 49 | # the more common case, there's a version to git the changes betwen 50 | else 51 | local head="$HEAD$version | $DATE" 52 | # tail to get remove the first line, which will always just be the version commit 53 | # awk to remove empty lines 54 | local changes 55 | changes=$(tail -n +2 <<< "$(git log $GIT_LOG_OPTS --pretty="format:* %s%n" "$previous_version..$version" 2>/dev/null)" | awk NF) 56 | fi 57 | 58 | local CHANGELOG 59 | CHANGELOG=$(find_changelog_file) 60 | 61 | echo "Editing $CHANGELOG" 62 | # insert the changes after the header (assumes markdown) 63 | # this shells out to node b/c I couldn't figure out how to do it with awk 64 | local tmp_changelog=/tmp/changelog 65 | node -e "console.log(require('fs').readFileSync(process.argv[1]).toString().replace(/(#.*?\n\n)/, '\$1' + process.argv.slice(2).join('\n') + '\n\n'))" "$CHANGELOG" "$head" "$changes" > $tmp_changelog 66 | 67 | # open the changelog in the editor for editing 68 | ${EDITOR:-'vi'} $tmp_changelog 69 | mv $tmp_changelog "$CHANGELOG" 70 | } 71 | 72 | function git_ammend_tag(){ 73 | local changelog_file 74 | local changes 75 | changes=$(git diff --minimal --diff-filter=M --unified=0 --color=never "$changelog_file" | grep '^\+' | egrep -v '^\+\+' | cut -c 2-) 76 | changelog_file="$(find_changelog_file)" 77 | 78 | git add "$changelog_file" 79 | git commit --amend --no-edit --no-verify 80 | git tag "$(find_last_git_tag)" -f -a -m "$changes" 81 | } 82 | 83 | function npm_release(){ 84 | local access="${1-public}" 85 | local version="${2-patch}" 86 | 87 | npm version "$version" && generate_git_changelog && git_ammend_tag && npm run gitPush && npm publish --access "$access" 88 | } 89 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React, {PureComponent, Children, cloneElement} from 'react' 2 | import PropTypes from 'proptypes' 3 | import identity from 'lodash.identity' 4 | import sortBy from 'lodash.sortby' 5 | import first from 'lodash.first' 6 | import isNumber from 'lodash.isnumber' 7 | import raf from 'raf' 8 | import autobind from 'autobind-decorator' 9 | 10 | const isBrowser = typeof window !== 'undefined' 11 | 12 | export default class ElementQuery extends PureComponent { 13 | static propTypes = { 14 | children: PropTypes.node.isRequired 15 | , default: PropTypes.string 16 | , sizes: PropTypes.arrayOf(PropTypes.shape({ 17 | name: PropTypes.string.isRequired 18 | , width: (props, propName, componentName) => { 19 | const size = props[propName] 20 | if (!isNumber(size)) { 21 | return new Error(`${componentName} received a width of \`${size}\` for \`${props.name}\`. A number was expected.`) 22 | } 23 | 24 | if (size === 0) { 25 | return new Error(`${componentName} received a width of \`${size}\` for \`${props.name}\`. Widths are min-widths, and should be treated as "mobile-first". The default state can be set with the \`default\` prop, or even better with the "default" styles in CSS.`) 26 | } 27 | return null 28 | } 29 | })).isRequired 30 | , makeClassName: PropTypes.func 31 | } 32 | 33 | static defaultProps = { 34 | // if no default is defined, assume no className. This is the default browser 35 | // behavior 36 | default: '' 37 | , sizes: [] 38 | , makeClassName: identity 39 | , children: 40 | } 41 | 42 | constructor (props) { 43 | super(props) 44 | this.state = {size: props.default, sizes: ElementQuery.sortSizes(this.props.sizes)} 45 | } 46 | 47 | componentDidMount () { 48 | ElementQuery.register({ 49 | component: this 50 | , sizes: this.state.sizes 51 | , node: this.node 52 | }) 53 | 54 | ElementQuery.sizeComponent({ 55 | component: this 56 | , sizes: this.state.sizes 57 | , node: this.node 58 | }) 59 | 60 | // wait a few frames then check sizes again 61 | raf(() => raf(() => { 62 | ElementQuery.sizeComponent({ 63 | component: this 64 | , sizes: this.state.sizes 65 | , node: this.node 66 | }) 67 | })) 68 | } 69 | 70 | componentWillReceiveProps (newProps) { 71 | this.setState({sizes: ElementQuery.sortSizes(newProps.sizes)}) 72 | } 73 | 74 | componentWillUnmount () { 75 | ElementQuery.unregister(this) 76 | } 77 | 78 | static _isListening = false 79 | 80 | static _componentMap = new Map() 81 | 82 | // use only one global listener … for perf! 83 | static listen () { 84 | window.addEventListener('resize', ElementQuery.onResize) 85 | ElementQuery._isListening = true 86 | } 87 | 88 | static unListen () { 89 | window.removeEventListener('resize', ElementQuery.onResize) 90 | ElementQuery._isListening = false 91 | } 92 | 93 | static register ({component, sizes, onResize, node}) { 94 | if (!isBrowser) return 95 | 96 | ElementQuery._componentMap.set(component, { 97 | sizes 98 | , node 99 | // if a custom onResize callback is passed, e.g. using this lib just for 100 | // the resize event listener, use that. Else, assume we're sizing the 101 | // component 102 | , onResize: onResize || ElementQuery.sizeComponent 103 | }) 104 | 105 | if (!ElementQuery._isListening && isBrowser) ElementQuery.listen() 106 | } 107 | 108 | static unregister (component) { 109 | if (!isBrowser) return 110 | 111 | ElementQuery._componentMap.delete(component) 112 | if (!ElementQuery._componentMap.size && isBrowser) ElementQuery.unListen() 113 | } 114 | 115 | static sizeComponents () { 116 | ElementQuery._componentMap.forEach((componentOptions, component) => { 117 | componentOptions.onResize({component 118 | , sizes: componentOptions.sizes 119 | , node: componentOptions.node 120 | }) 121 | }) 122 | } 123 | 124 | static sizeComponent ({component, sizes = [], node}) { 125 | if (!node) return 126 | 127 | const width = node.clientWidth 128 | const smallestSize = first(sizes) 129 | 130 | let matchedSize = '' 131 | let matchedWidth = smallestSize.width 132 | 133 | // use Array#some() here because #forEach() has no early exit 134 | sizes.some((test) => { 135 | // check for: 136 | // 1. the el width is greater or equal to the test width 137 | // 2. the el width is greater or equal to the min test width 138 | if (width >= test.width && width >= matchedWidth) { 139 | matchedSize = test.name 140 | matchedWidth = test.width 141 | return false 142 | } 143 | // once that condition isn't true, we've found the correct match; bail 144 | return true 145 | }) 146 | component.setSize(matchedSize) 147 | } 148 | 149 | // becuase we're going to itterate through by size, we need to ensure that the 150 | // sizes are sorted 151 | static sortSizes (sizes) { 152 | return sortBy(sizes, 'width') 153 | } 154 | 155 | @autobind 156 | setSize (size) { 157 | this.setState({size}) 158 | } 159 | 160 | @autobind 161 | setNode (node) { 162 | this.node = node 163 | } 164 | 165 | @autobind 166 | makeChild (child, className) { 167 | // just add our new class name onto the chilren, this alleviates the need to 168 | // create a wrapper div 169 | const classNames = [] 170 | const existingClassName = child.props.className 171 | if (existingClassName) classNames.push(existingClassName) 172 | if (className) classNames.push(className) 173 | 174 | return cloneElement(child, { 175 | className: classNames.join(' ') 176 | , ref: this.setNode 177 | }) 178 | } 179 | 180 | static onResize () { 181 | if (ElementQuery._frame) raf.cancel(ElementQuery._frame) 182 | ElementQuery._frame = raf(ElementQuery.sizeComponents) 183 | } 184 | 185 | render () { 186 | const size = isBrowser 187 | ? this.state.size 188 | : this.props.default 189 | const className = size ? this.props.makeClassName(size) : '' 190 | const {children} = this.props 191 | const child = Array.isArray(children) && Children.count(children) === 1 192 | ? children[0] 193 | : children 194 | 195 | // because we're going to just apply the className onto the child, we can 196 | // only accept one. React doesn't let us return an array of children. 197 | // returning a wrapper div is undesirable because it creates un-expected DOM 198 | // like real element queries, this enables the user to do things like wrap 199 | // an `
  • ` in an element query and not break HTML semantics, or use 200 | // element query and not break expectations around things like flexbox. 201 | return this.makeChild(Children.only(child), className) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /test/browser.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-multi-comp */ 2 | 3 | import test from 'tape' 4 | import ElementQuery from '../src/index.jsx' 5 | import React from 'react' 6 | import {mount} from 'enzyme' 7 | import sinon from 'sinon' 8 | 9 | const WIDTH_LARGE = 300 10 | const WIDTH_SMALL = 100 11 | const SIZE_LARGE = 'large' 12 | const SIZE_SMALL = 'small' 13 | const large = {name: SIZE_LARGE, width: WIDTH_LARGE} 14 | const small = {name: SIZE_SMALL, width: WIDTH_SMALL} 15 | const sizes = [large, small] 16 | 17 | // TODO: move move me to a module like `react-tape`, and maybe have it be a 18 | // browserify transform? 19 | // proptype failures will go to console.warn, fail the tests when one is seen 20 | test('test setup', (t) => { 21 | console._error = console.error 22 | console.error = (...args) => { 23 | t.fail(args.join(' ')) 24 | } 25 | // resize the window to be large so we're sure that's not affecting the elements 26 | window.resizeTo(WIDTH_LARGE * 2, 1) 27 | t.pass('ok') 28 | t.end() 29 | }) 30 | 31 | 32 | test('small size', (t) => { 33 | const wrapper = document.createElement('div') 34 | wrapper.style.width = `${WIDTH_SMALL}px` 35 | document.body.appendChild(wrapper) 36 | const tree = mount( 37 |

    hi

    38 |
    39 | , {attachTo: wrapper} 40 | ) 41 | 42 | t.equal( 43 | tree.state('sizes')[0].width 44 | , small.width 45 | , 'sorts the sizes by small width first' 46 | ) 47 | 48 | t.equal( 49 | tree.state('size') 50 | , small.name 51 | , 'matches the min width for the smallest size, not going to a larger size' 52 | ) 53 | 54 | t.end() 55 | }) 56 | 57 | test('large size', (t) => { 58 | const wrapper = document.createElement('div') 59 | wrapper.style.width = `${WIDTH_LARGE + WIDTH_SMALL}px` 60 | document.body.appendChild(wrapper) 61 | const tree = mount( 62 |

    hi

    63 | , {attachTo: wrapper} 64 | ) 65 | t.equal( 66 | tree.state('size') 67 | , large.name 68 | , 'matches the min width, for the largest size' 69 | ) 70 | 71 | t.end() 72 | }) 73 | 74 | test('fails on invalid sizes', (t) => { 75 | const errorStub = sinon.stub() 76 | const _error = console.error 77 | console.error = errorStub 78 | 79 | const nonNumberSizes = [{name: 'hi', width: 'not a number'}] 80 | mount(hi) 81 | 82 | t.ok( 83 | errorStub.calledOnce 84 | , 'errors when a non-number width is passed' 85 | ) 86 | errorStub.reset() 87 | 88 | const zeroWidth = [{name: 'hi', width: 0}] 89 | mount(hi) 90 | 91 | t.ok( 92 | errorStub.calledOnce 93 | , 'errors when a `0` width is passed' 94 | ) 95 | errorStub.reset() 96 | 97 | console.error = _error 98 | t.end() 99 | }) 100 | -------------------------------------------------------------------------------- /test/server.jsx: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import ElementQuery from '../src/index.jsx' 3 | import React from 'react' 4 | import {renderToString} from 'react-dom/server' 5 | 6 | const WIDTH_LARGE = 300 7 | const WIDTH_SMALL = 100 8 | const SIZE_LARGE = 'large' 9 | const SIZE_SMALL = 'small' 10 | 11 | test('server', (t) => { 12 | const large = {name: SIZE_LARGE, width: WIDTH_LARGE} 13 | const small = {name: SIZE_SMALL, width: WIDTH_SMALL} 14 | const sizes = [large, small] 15 | 16 | const html = renderToString(

    hi

    ) 17 | 18 | t.ok( 19 | html.includes('

    hi

    25 | )) 26 | 27 | t.ok( 28 | htmlWithDefault.includes(`class="${small.name}"`) 29 | , 'renders the default class name' 30 | ) 31 | 32 | t.end() 33 | }) 34 | --------------------------------------------------------------------------------