├── .babelrc ├── .build.config.js ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .releaserc ├── .stylelintrc ├── .travis.yml ├── .webpack.config.js ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── docs ├── gulpTasks.js ├── src │ ├── Confirm │ │ └── index.js │ ├── Dropdown │ │ ├── index.js │ │ └── styles.less │ ├── Form │ │ └── index.js │ ├── List │ │ └── index.js │ ├── Modal │ │ └── index.js │ ├── Select │ │ └── index.js │ ├── Table │ │ └── index.js │ ├── Tooltip │ │ ├── index.js │ │ └── styles.less │ ├── components │ │ ├── CodeBlock.js │ │ ├── CodeBlockWrapper.js │ │ ├── ComponentExample.js │ │ ├── ComponentExampleWrapper.js │ │ ├── ComponentWrapper.js │ │ ├── PropertiesAPIBlock.js │ │ └── icons │ │ │ └── IconCode.js │ ├── fonts │ │ └── source-sans-pro │ │ │ ├── OFL.txt │ │ │ ├── SourceSansPro-Black.ttf │ │ │ ├── SourceSansPro-Black.woff │ │ │ ├── SourceSansPro-BlackItalic.ttf │ │ │ ├── SourceSansPro-BlackItalic.woff │ │ │ ├── SourceSansPro-Bold.ttf │ │ │ ├── SourceSansPro-Bold.woff │ │ │ ├── SourceSansPro-BoldItalic.ttf │ │ │ ├── SourceSansPro-BoldItalic.woff │ │ │ ├── SourceSansPro-ExtraLight.ttf │ │ │ ├── SourceSansPro-ExtraLight.woff │ │ │ ├── SourceSansPro-ExtraLightItalic.ttf │ │ │ ├── SourceSansPro-ExtraLightItalic.woff │ │ │ ├── SourceSansPro-Italic.ttf │ │ │ ├── SourceSansPro-Italic.woff │ │ │ ├── SourceSansPro-Light.ttf │ │ │ ├── SourceSansPro-Light.woff │ │ │ ├── SourceSansPro-LightItalic.ttf │ │ │ ├── SourceSansPro-LightItalic.woff │ │ │ ├── SourceSansPro-Regular.ttf │ │ │ ├── SourceSansPro-Regular.woff │ │ │ ├── SourceSansPro-Semibold.ttf │ │ │ ├── SourceSansPro-Semibold.woff │ │ │ ├── SourceSansPro-SemiboldItalic.ttf │ │ │ └── SourceSansPro-SemiboldItalic.woff │ ├── index.html │ ├── index.js │ └── vendor │ │ ├── canvas-script.js │ │ ├── prettify.css │ │ └── prettify.js └── styles │ ├── components │ └── fill │ │ ├── styles.less │ │ └── variables.less │ ├── content │ ├── code │ │ └── prettyprint.less │ ├── example-block │ │ └── styles.less │ └── fonts │ │ ├── styles.less │ │ └── variables.less │ ├── index.less │ └── layout │ ├── foundation.less │ ├── navigation.less │ ├── page-header.less │ ├── page.less │ └── sidebar.less ├── gulpfile.js ├── install_react.sh ├── jest ├── config.json ├── setupEnv.js └── setupTestFramework.js ├── main.js ├── package-lock.json ├── package.json ├── renovate.json └── src ├── Confirm ├── Confirm.js └── __tests__ │ └── Confirm-test.js ├── Dropdown ├── Dropdown.js ├── DropdownListTrigger.js ├── DropdownTrigger.js ├── __tests__ │ ├── Dropdown-test.js │ ├── DropdownListTrigger-test.js │ └── fixtures │ │ └── MockDropdownList.json └── styles.less ├── Form ├── FieldCheckbox.js ├── FieldInput.js ├── FieldPassword.js ├── FieldRadioButton.js ├── FieldSelect.js ├── FieldSubmit.js ├── FieldTextarea.js ├── FieldTypes.js ├── Form.js ├── FormControl.js ├── __tests__ │ ├── FieldCheckbox-test.js │ ├── FieldInput-test.js │ ├── FieldRadioButton-test.js │ ├── FieldSelect-test.js │ ├── Form-test.js │ └── FormControl-test.js ├── icons │ └── IconEdit.js ├── styles.less └── variables.less ├── List ├── List.js ├── ListItem.js ├── __tests__ │ ├── List-test.js │ └── fixtures │ │ └── MockList.json └── styles.less ├── Mixin └── BindMixin.js ├── Modal ├── Modal.js ├── ModalContents.js ├── __tests__ │ └── ModalContents-test.js ├── styles.less └── variables.less ├── Portal └── Portal.js ├── Select ├── Select.js ├── SelectOption.js └── __tests__ │ └── Select-test.js ├── Table ├── Table.js ├── __tests__ │ ├── Table-test.js │ └── fixtures │ │ └── MockTable.json └── styles.less ├── Tooltip ├── Tooltip.js ├── __tests__ │ └── Tooltip-test.js ├── styles.less └── variables.less ├── Util ├── DOMUtil.js ├── KeyboardUtil.js ├── Util.js └── __tests__ │ ├── DOMUtil-test.js │ └── Util-test.js ├── VirtualList ├── VirtualList.js └── __tests__ │ └── VirtualList-test.js ├── constants └── Keycodes.js ├── index.less └── overrides └── gemini-scrollbar.less /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | plugins: ['transform-runtime'], 3 | presets: ['es2015', 'react'] 4 | } 5 | -------------------------------------------------------------------------------- /.build.config.js: -------------------------------------------------------------------------------- 1 | var srcFolder = "./src"; 2 | var docsFolder = "./docs"; 3 | var docsSrc = docsFolder + "/src"; 4 | var docsDist = docsFolder + "/dist"; 5 | 6 | var dirs = { 7 | srcJS: srcFolder, 8 | srcCSS: srcFolder, 9 | docs: { 10 | src: docsSrc, 11 | dist: docsDist, 12 | srcFonts: docsSrc + "/fonts", 13 | distFonts: docsDist + "/fonts", 14 | srcJS: docsSrc, 15 | distJS: docsDist, 16 | srcCSS: docsFolder + "/styles", 17 | distCSS: docsDist + "/" 18 | } 19 | }; 20 | 21 | var files = { 22 | docs: { 23 | srcJS: dirs.docs.srcJS + "/index.js", 24 | distJS: dirs.docs.distJS + "/index.js", 25 | srcCSS: dirs.docs.srcCSS + "/index.less", 26 | distCSS: dirs.docs.distCSS + "/index.css", 27 | srcFonts: dirs.docs.srcFonts + "/**/*", 28 | srcHTML: dirs.docs.src + "/index.html", 29 | distHTML: dirs.docs.dist + "/index.html" 30 | } 31 | }; 32 | 33 | module.exports = { 34 | dirs: dirs, 35 | files: files 36 | }; 37 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | max_line_length = 80 9 | tab_width = 2 10 | indent_size = 2 11 | indent_style = space 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | end_of_line = lf 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "jsx": true, 4 | "modules": true, 5 | "sourceType": "module" 6 | }, 7 | "env": { 8 | "browser": true, 9 | "jasmine": true, 10 | "node": true, 11 | "es6": true 12 | }, 13 | "parserOptions": { 14 | "sourceType": "module", 15 | "ecmaFeatures": { 16 | "jsx": true, 17 | "experimentalObjectRestSpread": true 18 | } 19 | }, 20 | "plugins": [ 21 | "jsx-max-len", 22 | "react", 23 | "compat", 24 | "destructuring" 25 | ], 26 | "globals": { 27 | "cy": true, 28 | "Cypress": true, 29 | "context": true, 30 | "xcontext": true, 31 | "xit": true, 32 | "it": true, 33 | "expect": true, 34 | "jest": true, 35 | "ActiveXObject": true, 36 | "Dygraph": true, 37 | "d3": true 38 | }, 39 | "extends": [ 40 | "@dcos" 41 | ], 42 | "rules": { 43 | "prettier/prettier": "error", 44 | "arrow-parens": "off", 45 | "camelcase": [ 46 | "off", 47 | { 48 | "properties": "never" 49 | } 50 | ], // This causes errors with mixin properties 51 | "comma-dangle": [ 52 | "error", 53 | "never" 54 | ], 55 | "compat/compat": "error", 56 | "destructuring/in-params": "off", 57 | "destructuring/no-rename": "off", 58 | "eqeqeq": [ 59 | "error", 60 | "smart" 61 | ], 62 | "jsx-max-len/jsx-max-len": [ 63 | "error", 64 | { 65 | "lineMaxLength": 80, 66 | "tabWidth": 2, 67 | "maxAttributesPerLine": 1 68 | } 69 | ], 70 | "keyword-spacing": [ 71 | "error" 72 | ], 73 | "multiline-ternary": "off", // allow both multiline and single line 74 | "newline-before-return": "error", 75 | "no-mixed-operators": "off", 76 | "no-mixed-spaces-and-tabs": "error", 77 | "no-multiple-empty-lines": [ 78 | "error", 79 | { 80 | "max": 1 81 | } 82 | ], 83 | "no-nested-ternary": [ 84 | "error" 85 | ], 86 | "no-new": "off", 87 | "no-trailing-spaces": [ 88 | "error", 89 | { 90 | "skipBlankLines": false 91 | } 92 | ], 93 | "no-underscore-dangle": "off", 94 | "no-unneeded-ternary": [ 95 | "error" 96 | ], 97 | "no-unused-vars": [ 98 | "error", 99 | { 100 | "args": "after-used", 101 | "argsIgnorePattern": "PluginSDK|^_", 102 | "varsIgnorePattern": "^_" 103 | } 104 | ], 105 | "no-use-before-define": "off", 106 | "quotes": "off", 107 | "react/jsx-filename-extension": "off", 108 | "semi": [ 109 | "error" 110 | ], 111 | "space-before-blocks": [ 112 | "error", 113 | "always" 114 | ], 115 | "space-before-function-paren": "off", 116 | "spaced-comment": [ 117 | "error", 118 | "always" 119 | ], 120 | "strict": [ 121 | "error", 122 | "never" 123 | ], // <-- fix 124 | "valid-jsdoc": [ 125 | "error", 126 | { 127 | "requireReturn": false, 128 | "requireParamDescription": false 129 | } 130 | ], 131 | // Airbnb overwrites 132 | "array-callback-return": "off", 133 | "arrow-body-style": "off", 134 | "arrow-spacing": "off", 135 | "consistent-return": "off", 136 | "default-case": "off", 137 | "dot-notation": "off", 138 | "func-names": "off", 139 | "global-require": "off", 140 | "import/no-extraneous-dependencies": "off", 141 | "import/no-unresolved": "off", 142 | "jsx-a11y/img-has-alt": "off", 143 | "jsx-a11y/label-has-for": "off", 144 | "key-spacing": "off", 145 | "max-len": "off", 146 | "new-cap": "off", 147 | "newline-per-chained-call": "off", 148 | "no-case-declarations": "off", 149 | "no-console": "off", 150 | "no-else-return": "off", 151 | "no-empty": [ 152 | "error", 153 | { 154 | "allowEmptyCatch": true 155 | } 156 | ], 157 | "no-extra-label": "off", 158 | "no-floating-decimal": "off", 159 | "no-inner-declarations": "off", 160 | "no-labels": "off", 161 | "no-new-wrappers": "off", 162 | "no-param-reassign": "off", 163 | "no-restricted-syntax": "off", 164 | "no-return-assign": "off", 165 | "no-shadow": "off", 166 | "no-throw-literal": "off", 167 | "no-undef": "error", 168 | "no-var": "off", 169 | "object-curly-spacing": "off", 170 | "object-property-newline": "off", 171 | "one-var-declaration-per-line": "off", 172 | "one-var": "off", 173 | "operator-assignment": "off", 174 | "padded-blocks": "off", 175 | "prefer-arrow-callback": "off", 176 | "prefer-const": [ 177 | "error", 178 | { 179 | "destructuring": "all", 180 | "ignoreReadBeforeAssign": false 181 | } 182 | ], 183 | "prefer-rest-params": "off", 184 | "prefer-spread": "off", 185 | "prefer-template": "off", 186 | "quote-props": "off", 187 | "space-in-parens": "off", 188 | "space-infix-ops": "off", 189 | "space-unary-ops": "off", 190 | "vars-on-top": "off", 191 | "wrap-iife": "off", 192 | // React plugin 193 | "react/jsx-boolean-value": "off", 194 | "react/jsx-closing-bracket-location": "off", 195 | "react/jsx-equals-spacing": "off", 196 | "react/jsx-first-prop-new-line": "off", 197 | "react/jsx-no-bind": "off", 198 | "react/jsx-no-target-blank": "off", 199 | "react/jsx-no-undef": "error", 200 | "react/jsx-quotes": "off", 201 | "react/jsx-sort-prop-types": [ 202 | "off", 203 | { 204 | "ignoreCase": true 205 | } 206 | ], 207 | "react/jsx-sort-props": "off", 208 | "react/jsx-space-before-closing": "off", 209 | "react/jsx-uses-vars": "error", 210 | "react/no-did-mount-set-state": "error", 211 | "react/no-did-update-set-state": "error", 212 | "react/no-find-dom-node": "off", 213 | "react/no-is-mounted": "off", 214 | "react/no-multi-comp": "error", 215 | "react/no-render-return-value": "off", 216 | "react/no-string-refs": "off", 217 | "react/no-unknown-property": "off", 218 | "react/prefer-es6-class": "off", 219 | "react/prefer-stateless-function": "off", 220 | "react/prop-types": "off", // This causes errors with many 221 | "react/react-in-jsx-scope": "error", 222 | "react/require-extension": "off", 223 | "react/self-closing-comp": "off", 224 | "react/sort-comp": "off" 225 | } 226 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist 3 | git 4 | node_modules 5 | npm-debug.log 6 | /lib 7 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branch": "master" 3 | } 4 | 5 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-dcos" 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8.15" 4 | - "node" 5 | - "lts/*" 6 | 7 | jobs: 8 | include: 9 | - stage: lint 10 | name: "Lint && Dist" 11 | script: 12 | - npm run lint && npm run dist 13 | # Define the release stage that runs semantic-release 14 | - stage: release 15 | node_js: lts/* 16 | deploy: 17 | provider: script 18 | skip_cleanup: true 19 | script: 20 | - npx semantic-release 21 | 22 | before_install: 23 | - npm install -g npm@6.7 24 | install: 25 | - npm install 26 | - ./install_react.sh 27 | env: 28 | matrix: 29 | - REACT_VERSION=16.7 30 | - REACT_VERSION=16.8 31 | -------------------------------------------------------------------------------- /.webpack.config.js: -------------------------------------------------------------------------------- 1 | var config = require("./.build.config.js"); 2 | 3 | module.exports = { 4 | devtool: "source-map", 5 | entry: config.files.docs.srcJS, 6 | output: { filename: config.files.docs.distJS, publicPath: "" }, 7 | module: { 8 | loaders: [ 9 | { 10 | exclude: /node_modules/, 11 | loader: "babel-loader", 12 | query: { 13 | cacheDirectory: true 14 | }, 15 | test: /\.js$/ 16 | } 17 | ], 18 | postLoaders: [ 19 | { 20 | loader: "transform/cacheable?envify" 21 | } 22 | ] 23 | }, 24 | resolve: { 25 | extensions: ["", ".js"] 26 | }, 27 | watch: process.env.NODE_ENV === "development" 28 | }; 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unversioned 2 | 3 | ### Added 4 | 5 | - #180 – Adds ability to render disabled checkbox. 6 | - #260 - Add FieldSelect component 7 | - #257 - Added support for focus for input fields with type "text" in the Form component 8 | - #261 - Added Radio Button component to the Form component. 9 | 10 | ### Changed 11 | 12 | - #180 – Moved FieldCheckbox to FieldCheckboxMultiple, and created a new FieldCheckbox that only renders one checkbox. 13 | - #262 – Change Checkbox component to extend Radio Button component and reuse functionality. Functionality of FieldCheckbox and FieldCheckboxMultiple is now both contained in FieldCheckbox. 14 | - #275 - Pass through all data on change in Form FieldSelect 15 | - #276 - Change Form updating to delete fields that are no longer part of the definition. 16 | - #280 - Make sure Form always returns the latest model to on change handler 17 | - #279 - Make all form elements use classname/dedupe to allow for more class customizability 18 | - #289 - Don't transform form field select option ids to lower case 19 | - #296 - Set padding of modal inside scrollable component rathe than outside 20 | 21 | ### Fixed 22 | 23 | ## 0.14.0 – 01/22/2016 24 | 25 | ### Changed 26 | 27 | - #171 – Updated React to 0.14. React is along with other dependencies are now required as peerDependencies. 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Found an Issue? 2 | If you find a bug in the source code or a mistake in the documentation, you can 3 | help by submitting an issue [here](https://github.com/mesosphere/reactjs-components/issues). 4 | 5 | ## Development Setup 6 | 7 | 1. Clone this repository 8 | 2. Install [NPM](https://npmjs.org/) 9 | 3. Install dev dependencies 10 | 11 | ```sh 12 | npm install 13 | npm install -g gulp 14 | REACT_VERSION= ./install_react.sh 15 | ``` 16 | 4. Run the tests 17 | 18 | ```sh 19 | npm test 20 | ``` 21 | 22 | 5. Start the server and watch files 23 | 24 | ```sh 25 | npm run livereload 26 | ``` 27 | 28 | ## Adding npm package dependencies to package.json 29 | 30 | If you want to add a new npm package to `node_modules`: 31 | 32 | 1. Install the new package 33 | 34 | npm install [your package] --save 35 | will install and save to dependencies in package.json and 36 | 37 | npm install [your package] --save-dev 38 | will add it to devDependencies. 39 | 40 | 3. Commit to repository 41 | 42 | ### Publishing a new Release 43 | 44 | 1. Make sure you are on `master` branch and have pulled the latest changes. 45 | 46 | 2. Now do the release (there is an npm command for this): 47 | 48 | npm run release:[ | major | minor | patch | premajor | preminor | prepatch] 49 | 50 | After this you can pull down the latest module version from npm. 51 | 52 | ### Publishing a Beta Release 53 | 54 | 1. Make sure you are on `master` branch and have pulled the latest changes. 55 | 56 | 2. Run the `release:beta` NPM script: 57 | 58 | npm run release:prerelease 59 | or 60 | 61 | npm run release:beta 62 | 63 | ## Making a PR 64 | 65 | Before you submit your pull request consider the following guidelines: 66 | 67 | * Search [GitHub](https://github.com/mesosphere/reactjs-components/pulls) for an open or closed Pull Request 68 | that relates to your submission. You don't want to duplicate effort. 69 | * Make your changes in a new git branch: 70 | 71 | ```shell 72 | git checkout -b my-fix-branch master 73 | ``` 74 | 75 | * [Setup your editor](http://editorconfig.org/#download) 76 | 77 | * Create your patch, including appropriate unit test cases 78 | 79 | * Commit your changes using a [descriptive commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 80 | 81 | * Build your changes locally to ensure all the tests pass: 82 | 83 | ```shell 84 | npm run dist 85 | npm run test 86 | ``` 87 | 88 | * Push your branch to GitHub: 89 | 90 | ```shell 91 | git push origin my-fix-branch 92 | ``` 93 | 94 | * In GitHub, send a pull request to `reactjs-components:master`. 95 | 96 | * If we suggest changes then: 97 | * Make the required updates. 98 | * Re-run the test suite to ensure tests are still passing. 99 | * If necessary, rebase your branch and force push to your GitHub repository (this will update your Pull Request): 100 | 101 | ```shell 102 | git rebase master -i 103 | git push origin my-fix-branch -f 104 | ``` 105 | 106 | That's it! Thank you for your contribution! 107 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Mesosphere, Inc. 2 | 3 | This file has been modified from its original form by Mesosphere, Inc. 4 | All modifications made to this file by Mesosphere (the “Modifications”) 5 | are © 2015 Mesosphere, Inc. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, 14 | software distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | 19 | --- 20 | 21 | The original file on which the Modifications have been made were provided to 22 | Mesosphere pursuant to the following terms: 23 | 24 | Copyright 2012-2015 The Dojo Foundation 25 | Based on Underscore.js, copyright 2009-2015 Jeremy Ashkenas, 26 | DocumentCloud and Investigative Reporters & Editors 27 | 28 | Permission is hereby granted, free of charge, to any person obtaining 29 | a copy of this software and associated documentation files (the 30 | "Software"), to deal in the Software without restriction, including 31 | without limitation the rights to use, copy, modify, merge, publish, 32 | distribute, sublicense, and/or sell copies of the Software, and to 33 | permit persons to whom the Software is furnished to do so, subject to 34 | the following conditions: 35 | 36 | The above copyright notice and this permission notice shall be 37 | included in all copies or substantial portions of the Software. 38 | 39 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 40 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 41 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 42 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 43 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 44 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 45 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 46 | 47 | --- 48 | 49 | The original file on which the Modifications have been made were provided to 50 | Mesosphere pursuant to the following terms: 51 | 52 | Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative 53 | Reporters & Editors 54 | 55 | Permission is hereby granted, free of charge, to any person 56 | obtaining a copy of this software and associated documentation 57 | files (the "Software"), to deal in the Software without 58 | restriction, including without limitation the rights to use, 59 | copy, modify, merge, publish, distribute, sublicense, and/or sell 60 | copies of the Software, and to permit persons to whom the 61 | Software is furnished to do so, subject to the following 62 | conditions: 63 | 64 | The above copyright notice and this permission notice shall be 65 | included in all copies or substantial portions of the Software. 66 | 67 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 68 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 69 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 70 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 71 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 72 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 73 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 74 | OTHER DEALINGS IN THE SOFTWARE. 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReactJS-components [![Build Status](https://travis-ci.org/mesosphere/reactjs-components.svg?branch=master)](https://travis-ci.org/mesosphere/reactjs-components) [![david-dm](https://david-dm.org/mesosphere/reactjs-components.svg)](https://david-dm.org/mesosphere/reactjs-components) [![devDependency Status](https://david-dm.org/mesosphere/reactjs-components/dev-status.svg)](https://david-dm.org/mesosphere/reactjs-components#info=devDependencies) 2 | 3 | A library of reusable React components. For examples, take a look at our 4 | [kitchen sink](http://mesosphere.github.io/reactjs-components/). 5 | 6 | ## Available components 7 | * Dropdown 8 | * List 9 | * Form 10 | * Modal 11 | * Confirm 12 | * Side Panel 13 | * Table 14 | * Tooltip 15 | 16 | ## Using the components 17 | 18 | 19 | 1. From the command line inside of your project 20 | 21 | ``` 22 | npm install --save reactjs-components react react-gemini-scrollbar canvas-ui 23 | ``` 24 | 25 | 2. Import the component that you want to use 26 | 27 | ```js 28 | // es6 29 | import {Modal} from 'reactjs-components'; 30 | 31 | // es5 32 | var Modal = require('reactjs-components').Modal; 33 | ``` 34 | 35 | 3. Use as if it was any other component 36 | 37 | ```js 38 | // ... 39 | 40 | render: function () { 41 | return ( 42 | // ... 43 | 44 | // Content 45 | 46 | ); 47 | } 48 | ``` 49 | 50 | 4. Import LESS files which will add all styles for all components. 51 | 52 | ```less 53 | @import "./node_modules/canvas-ui/styles/canvas.less" 54 | @import (inline) "./node_modules/reactjs-components/lib/index.less" 55 | ``` 56 | 57 | ## Contributing 58 | See [here](https://github.com/mesosphere/reactjs-components/blob/master/CONTRIBUTING.md). 59 | -------------------------------------------------------------------------------- /docs/gulpTasks.js: -------------------------------------------------------------------------------- 1 | var autoprefixer = require("gulp-autoprefixer"); 2 | var browserSync = require("browser-sync"); 3 | var colorLighten = require("less-color-lighten"); 4 | var concat = require("gulp-concat"); 5 | var eslint = require("gulp-eslint"); 6 | var fs = require("fs"); 7 | var gulp = require("gulp"); 8 | var gutil = require("gulp-util"); 9 | var less = require("gulp-less"); 10 | var minifyCSS = require("gulp-minify-css"); 11 | var path = require("path"); 12 | var replace = require("gulp-replace"); 13 | var sourcemaps = require("gulp-sourcemaps"); 14 | var stylelint = require("gulp-stylelint"); 15 | var uglify = require("gulp-uglify"); 16 | var webpack = require("webpack"); 17 | 18 | var config = require("../.build.config"); 19 | var packageInfo = require("../package"); 20 | var webpackConfig = require("../.webpack.config"); 21 | 22 | var development = process.env.NODE_ENV === "development"; 23 | 24 | function browserSyncReload() { 25 | if (development) { 26 | browserSync.reload(); 27 | } 28 | } 29 | 30 | gulp.task("docs:browsersync", function() { 31 | browserSync.init({ 32 | open: false, 33 | port: 4200, 34 | server: { 35 | baseDir: config.dirs.docs.dist 36 | }, 37 | socket: { 38 | domain: "localhost:4200" 39 | } 40 | }); 41 | }); 42 | 43 | // Create a function so we can use it inside of webpack's watch function. 44 | function eslintFn() { 45 | return gulp 46 | .src([config.files.docs.srcJS]) 47 | .pipe(eslint()) 48 | .pipe(eslint.formatEach("stylish", process.stderr)); 49 | } 50 | 51 | gulp.task("docs:eslint", eslintFn); 52 | 53 | gulp.task("docs:fonts", function() { 54 | return gulp 55 | .src(config.files.docs.srcFonts) 56 | .pipe(gulp.dest(config.dirs.docs.distFonts)); 57 | }); 58 | 59 | gulp.task("docs:html", function() { 60 | return gulp 61 | .src(config.files.docs.srcHTML) 62 | .pipe(gulp.dest(config.dirs.docs.dist)) 63 | .on("end", browserSyncReload); 64 | }); 65 | 66 | gulp.task("docs:less", ["docs:stylelint"], function() { 67 | return gulp 68 | .src(config.files.docs.srcCSS, { read: true }, { ignorePath: "src" }) 69 | .pipe(sourcemaps.init()) 70 | .pipe( 71 | less({ 72 | paths: [config.dirs.docs.srcCSS], // @import paths 73 | plugins: [colorLighten] 74 | }) 75 | ) 76 | .on("error", function(err) { 77 | gutil.log(err); 78 | this.emit("end"); 79 | }) 80 | .pipe(autoprefixer()) 81 | .pipe(concat(config.files.docs.distCSS)) 82 | .pipe(sourcemaps.write()) 83 | .pipe(gulp.dest(".")) 84 | .pipe(browserSync.stream()); 85 | }); 86 | 87 | gulp.task("docs:minify-css", ["docs:less"], function() { 88 | return gulp 89 | .src(config.files.docs.distCSS) 90 | .pipe(minifyCSS()) 91 | .pipe(gulp.dest(config.dirs.docs.distCSS)); 92 | }); 93 | 94 | gulp.task("docs:minify-js", ["docs:replace-js-strings"], function() { 95 | return gulp 96 | .src(config.files.docs.distJS) 97 | .pipe( 98 | uglify({ 99 | mangle: true, 100 | compress: true 101 | }).on("error", function(e) { 102 | console.log(e); 103 | }) 104 | ) 105 | .pipe(gulp.dest(config.dirs.docs.distJS)); 106 | }); 107 | 108 | gulp.task("docs:stylelint", function() { 109 | return gulp 110 | .src([ 111 | config.dirs.docs.srcCSS + "/**/*.less", 112 | config.dirs.docs.srcJS + "/**/*.less", 113 | config.dirs.srcCSS + "/**/*.less", 114 | config.dirs.srcJS + "/**/*.less" 115 | ]) 116 | .pipe( 117 | stylelint({ 118 | reporters: [{ formatter: "string", console: true }] 119 | }) 120 | ); 121 | }); 122 | 123 | function replaceJsStringsFn() { 124 | return gulp 125 | .src(config.files.docs.distJS) 126 | .pipe( 127 | replace(/PROPTYPES_BLOCK\((.*?)\)/g, function(match, srcPath) { 128 | srcPath = path.join(__dirname, "..", srcPath); 129 | 130 | // Find the propTypes block in source file 131 | var srcContent = fs.readFileSync(srcPath, { encoding: "utf8" }); 132 | var matches = srcContent.match(/^\w+\.propTypes[\s\S]*?^};$/m); 133 | 134 | // Finally do the replace 135 | var contents = matches[0]; 136 | // Escape quotes that haven't already been escaped 137 | contents = contents.replace(/([^\\])"/g, '$1\\"'); 138 | // Escape newlines or carriage return that haven't already been escaped 139 | contents = contents.replace(/\n|\r/g, "\\n"); 140 | 141 | return contents; 142 | }) 143 | ) 144 | .pipe(replace("@@VERSION", packageInfo.version)) 145 | .pipe(gulp.dest(config.dirs.docs.distJS)) 146 | .on("end", browserSyncReload); 147 | } 148 | 149 | gulp.task("docs:replace-js-strings", ["docs:webpack"], replaceJsStringsFn); 150 | 151 | gulp.task("docs:watch", function() { 152 | gulp.watch(config.files.docs.srcHTML, ["docs:html"]); 153 | gulp.watch( 154 | [ 155 | config.dirs.docs.srcCSS + "/**/*.less", 156 | config.dirs.docs.srcJS + "/**/*.less", 157 | config.dirs.srcCSS + "/**/*.less", 158 | config.dirs.srcJS + "/**/*.less" 159 | ], 160 | ["docs:less"] 161 | ); 162 | // Why aren't we watching any JS files? Because we use webpack's 163 | // internal watch, which is faster due to insane caching. 164 | }); 165 | 166 | // Use webpack to compile jsx into js, 167 | gulp.task("docs:webpack", function(callback) { 168 | var isFirstRun = true; 169 | 170 | webpack(webpackConfig, function(err, stats) { 171 | if (err) { 172 | throw new gutil.PluginError("webpack", err); 173 | } 174 | 175 | gutil.log( 176 | "[webpack]", 177 | stats.toString({ 178 | children: false, 179 | chunks: false, 180 | colors: true, 181 | modules: false, 182 | timing: true 183 | }) 184 | ); 185 | 186 | if (isFirstRun) { 187 | // This runs on initial gulp webpack load. 188 | isFirstRun = false; 189 | callback(); 190 | } else { 191 | // This runs after webpack's internal watch rebuild. 192 | eslintFn(); 193 | replaceJsStringsFn(); 194 | } 195 | }); 196 | }); 197 | 198 | gulp.task("docs:default", [ 199 | "docs:webpack", 200 | "docs:eslint", 201 | "docs:fonts", 202 | "docs:replace-js-strings", 203 | "docs:less", 204 | "docs:html" 205 | ]); 206 | 207 | gulp.task("docs:dist", ["docs:default", "docs:minify-css", "docs:minify-js"]); 208 | 209 | gulp.task("docs:livereload", [ 210 | "docs:default", 211 | "docs:browsersync", 212 | "docs:watch" 213 | ]); 214 | -------------------------------------------------------------------------------- /docs/src/Confirm/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from "react"; 3 | /* eslint-enable no-unused-vars */ 4 | 5 | import BindMixin from "../../../src/Mixin/BindMixin"; 6 | import ComponentExample from "../components/ComponentExample"; 7 | import ComponentExampleWrapper from "../components/ComponentExampleWrapper"; 8 | import ComponentWrapper from "../components/ComponentWrapper"; 9 | import CodeBlock from "../components/CodeBlock"; 10 | import Confirm from "../../../src/Confirm/Confirm.js"; 11 | import PropertiesAPIBlock from "../components/PropertiesAPIBlock"; 12 | import Util from "../../../src/Util/Util"; 13 | 14 | class ConfirmExample extends Util.mixin(BindMixin) { 15 | get methodsToBind() { 16 | return ["handleOpenConfirm", "handleButtonCancel", "handleButtonConfirm"]; 17 | } 18 | 19 | constructor() { 20 | super(); 21 | 22 | this.state = { 23 | openConfirm: false 24 | }; 25 | } 26 | 27 | handleOpenConfirm() { 28 | this.setState({ openConfirm: true }); 29 | } 30 | 31 | handleButtonCancel() { 32 | this.setState({ openConfirm: false }); 33 | } 34 | 35 | handleButtonConfirm() { 36 | /* eslint-disable no-alert */ 37 | alert("Confirm was pressed!"); 38 | /* eslint-enable no-alert */ 39 | this.setState({ openConfirm: false }); 40 | } 41 | 42 | render() { 43 | return ( 44 | 48 | 51 | 52 | 53 | 59 | 65 |
66 | Would you like to perform this action? 67 |
68 |
69 |
70 | 71 | {`import {Confirm} from 'reactjs-components'; 72 | import React from 'react'; 73 | 74 | class ConfirmExample extends React.Component { 75 | render() { 76 | return ( 77 | 82 |
83 | Would you like to perform this action? 84 |
85 |
86 | ); 87 | } 88 | } 89 | `} 90 |
91 |
92 |
93 | ); 94 | } 95 | } 96 | 97 | module.exports = ConfirmExample; 98 | -------------------------------------------------------------------------------- /docs/src/Dropdown/styles.less: -------------------------------------------------------------------------------- 1 | .dropdown { 2 | 3 | .dropdown-menu-list { 4 | 5 | li { 6 | 7 | &.is-selected { 8 | color: @blue; 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/src/List/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import CodeBlock from "../components/CodeBlock"; 4 | import ComponentExample from "../components/ComponentExample"; 5 | import ComponentExampleWrapper from "../components/ComponentExampleWrapper"; 6 | import ComponentWrapper from "../components/ComponentWrapper"; 7 | import List from "../../../src/List/List.js"; 8 | import PropertiesAPIBlock from "../components/PropertiesAPIBlock"; 9 | 10 | class ListExample extends React.Component { 11 | constructor() { 12 | super(); 13 | 14 | this.state = { 15 | itemAdded: false 16 | }; 17 | 18 | this.handleExtraItemToggle = this.handleExtraItemToggle.bind(this); 19 | } 20 | 21 | handleExtraItemToggle() { 22 | this.setState({ 23 | itemAdded: !this.state.itemAdded 24 | }); 25 | } 26 | 27 | getComplexNestedList() { 28 | // Here's an example of a list with customized list items. 29 | var list = [ 30 | // First item 31 | { 32 | content: "Lorem ipsum dolor sit amet, consectetuer adipiscing elit." 33 | }, 34 | // Second item 35 | { 36 | // Nested items 37 | content: [ 38 | { 39 | className: "text-uppercase", 40 | style: { 41 | display: "block" 42 | }, 43 | tag: "strong", 44 | content: "Cu movet numquam." 45 | }, 46 | { 47 | className: "list a", 48 | tag: "ol", 49 | content: [ 50 | { 51 | content: "Aliquam tincidunt mauris eu risus." 52 | }, 53 | { 54 | content: [ 55 | { 56 | tag: "em", 57 | content: "Mauris placerat eleifend leo." 58 | }, 59 | { 60 | className: "list I", 61 | tag: "ol", 62 | content: [ 63 | { 64 | content: "Suspendisse laoreet. Fusce ut est sed dolor." 65 | }, 66 | { 67 | content: Gravida convallis. Morbi vitae ante. 68 | } 69 | ] 70 | } 71 | ] 72 | } 73 | ] 74 | } 75 | ] 76 | }, 77 | // Third item 78 | { 79 | content: "Vestibulum auctor dapibus neque." 80 | } 81 | ]; 82 | 83 | if (this.state.itemAdded) { 84 | list[1].content[1].content.push({ 85 | content: "A wild transitioned list item appears." 86 | }); 87 | } 88 | 89 | return list; 90 | } 91 | 92 | render() { 93 | let toggleText = "Add item"; 94 | 95 | if (this.state.itemAdded) { 96 | toggleText = "Remove item"; 97 | } 98 | 99 | return ( 100 | 104 |

105 | Create lists with custom elements and transitions. 106 |

107 |
108 |
109 | 112 |
113 |
114 | 115 | 116 |
117 |
118 | 119 |
120 |
121 | 127 |
128 |
129 |
130 | 131 | {`import {List} from 'reactjs-components'; 132 | import React from 'react'; 133 | 134 | var list = [ 135 | // First item 136 | { 137 | content: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.' 138 | }, 139 | // Second item 140 | { 141 | // Nested items 142 | content: [ 143 | { 144 | className: 'text-uppercase', 145 | style: { 146 | display: 'block' 147 | }, 148 | tag: 'strong', 149 | content: 'Cu movet numquam.' 150 | }, 151 | { 152 | className: 'list a', 153 | tag: 'ol', 154 | content: [ 155 | { 156 | content: 'Aliquam tincidunt mauris eu risus.' 157 | }, 158 | { 159 | content: [ 160 | { 161 | tag: 'em', 162 | content: 'Mauris placerat eleifend leo.' 163 | }, 164 | { 165 | className: 'list I', 166 | tag: 'ol', 167 | content: [ 168 | { 169 | content: 'Suspendisse laoreet. Fusce ut est sed dolor.' 170 | }, 171 | { 172 | content: Gravida convallis. Morbi vitae ante. 173 | } 174 | ] 175 | } 176 | ] 177 | } 178 | ] 179 | } 180 | ] 181 | }, 182 | // Third item 183 | { 184 | content: 'Vestibulum auctor dapibus neque.' 185 | } 186 | ]; 187 | 188 | class ListExample extends React.Component { 189 | render() { 190 | return 191 | } 192 | }`} 193 | 194 |
195 |
196 | ); 197 | } 198 | } 199 | 200 | module.exports = ListExample; 201 | -------------------------------------------------------------------------------- /docs/src/Select/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import CodeBlock from "../components/CodeBlock"; 4 | import ComponentExample from "../components/ComponentExample"; 5 | import ComponentExampleWrapper from "../components/ComponentExampleWrapper"; 6 | import ComponentWrapper from "../components/ComponentWrapper"; 7 | import Select from "../../../src/Select/Select.js"; 8 | import SelectOption from "../../../src/Select/SelectOption.js"; 9 | import PropertiesAPIBlock from "../components/PropertiesAPIBlock"; 10 | 11 | class SelectExample extends React.Component { 12 | render() { 13 | return ( 14 | 18 |

19 | To allow a enclosing form recognize the change of a{" "} 20 | Dropdown, it is necceccary that there is an{" "} 21 | input element which emits a proper change{" "} 22 | event. This is solved by this component. 23 |

24 | 27 | 30 | 31 | 32 |
33 |
34 | 49 |
50 |
51 |
52 | 53 | {`import { Select } from "reactjs-components"; 54 | import React from "react"; 55 | 56 | class SimpleSelectExample extends React.Component { 57 | render() { 58 | return ( 59 | 74 | ); 75 | } 76 | }`} 77 | 78 |
79 |
80 | ); 81 | } 82 | } 83 | 84 | module.exports = SelectExample; 85 | -------------------------------------------------------------------------------- /docs/src/Tooltip/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import CodeBlock from "../components/CodeBlock"; 4 | import ComponentExample from "../components/ComponentExample"; 5 | import ComponentExampleWrapper from "../components/ComponentExampleWrapper"; 6 | import ComponentWrapper from "../components/ComponentWrapper"; 7 | import PropertiesAPIBlock from "../components/PropertiesAPIBlock"; 8 | import Tooltip from "../../../src/Tooltip/Tooltip.js"; 9 | 10 | class ToolTipExample extends React.Component { 11 | render() { 12 | const tooltipContent = ( 13 |

14 | This tooltip is not interactive. 15 |

16 | ); 17 | 18 | const interactiveTooltipContent = ( 19 |
20 |

21 | This tooltip is interactive, so the user is able to interact with its 22 | contents. 23 |

24 |

25 | Example: Back to Top. 26 |

27 |
28 | ); 29 | 30 | return ( 31 | 35 |

36 | A tooltip appears when a user hover overs an element. The tooltips 37 | will adjust their position if the supplied positioning results in the 38 | tooltip rendering outside of the veiwport. 39 |

40 |

41 | The tooltip allows users to set the position and anchor. Position 42 | refers to the position of the tooltip relative to its trigger (which 43 | is its children), and acceptable values are top, right, bottom, and 44 | left. Anchor refers to where the tooltip is anchored on the triggered 45 | element, and acceptable values are start, center, and end. Think of 46 | flexbox alignment when thinking about the start and end values. 47 |

48 | 51 | 52 | 53 |

54 | Hover over the following buttons to observe the behavior of the 55 | tooltips. 56 |

57 |
58 | 64 | Top 65 | 66 | 73 | Bottom 74 | 75 | 85 | Left 86 | 87 | 97 | Right 98 | 99 |
100 |
101 | 102 | {`import {Tooltip} from 'reactjs-components'; 103 | import React from 'react'; 104 | 105 | class FormExample extends React.Component { 106 | render() { 107 | let tooltipContent = ( 108 |

109 | This tooltip is not interactive. 110 |

111 | ); 112 | 113 | let interactiveTooltipContent = ( 114 |
115 |

116 | This tooltip is interactive, so the user is able to interact with its contents. 117 |

118 |

119 | Example: Back to Top. 120 |

121 |
122 | ); 123 | 124 | return ( 125 |
126 |
127 |
128 | 131 | Top 132 | 133 | 136 | Bottom 137 | 138 | 142 | Left 143 | 144 | 148 | Right 149 | 150 |
151 |
152 |
153 | ); 154 | } 155 | }`} 156 |
157 |
158 |
159 | ); 160 | } 161 | } 162 | 163 | module.exports = ToolTipExample; 164 | -------------------------------------------------------------------------------- /docs/src/Tooltip/styles.less: -------------------------------------------------------------------------------- 1 | .tooltip { 2 | 3 | .small { 4 | color: fade(@white, 85%); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /docs/src/components/CodeBlock.js: -------------------------------------------------------------------------------- 1 | import classnames from "classnames/dedupe"; 2 | import PropTypes from "prop-types"; 3 | import React from "react"; 4 | 5 | class CodeBlock extends React.Component { 6 | render() { 7 | const panelInnerClasses = classnames( 8 | "panel-cell panel-cell-narrow panel-cell-short panel-cell-light panel-cell-code-block", 9 | this.props.panelInnerClassNames 10 | ); 11 | const preClasses = classnames( 12 | `prettyprint transparent flush lang-${this.props.language}`, 13 | this.props.preClassNames 14 | ); 15 | 16 | return ( 17 |
18 |
{this.props.children}
19 |
20 | ); 21 | } 22 | } 23 | 24 | const classPropTypes = PropTypes.oneOfType([ 25 | PropTypes.array, 26 | PropTypes.object, 27 | PropTypes.string 28 | ]); 29 | 30 | CodeBlock.defaultProps = { 31 | language: "javascript" 32 | }; 33 | 34 | CodeBlock.propTypes = { 35 | children: PropTypes.node.isRequired, 36 | language: PropTypes.string, 37 | panelInnerClassNames: classPropTypes, 38 | preClassNames: classPropTypes 39 | }; 40 | 41 | module.exports = CodeBlock; 42 | -------------------------------------------------------------------------------- /docs/src/components/CodeBlockWrapper.js: -------------------------------------------------------------------------------- 1 | import classnames from "classnames/dedupe"; 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | 5 | class CodeBlockWrapper extends React.Component { 6 | render() { 7 | const panelClasses = classnames( 8 | "panel pod flush-right flush-left flush-top", 9 | this.props.panelClassNames 10 | ); 11 | 12 | return
{this.props.children}
; 13 | } 14 | } 15 | 16 | CodeBlockWrapper.propTypes = { 17 | children: PropTypes.node.isRequired, 18 | panelClassNames: PropTypes.oneOfType([ 19 | PropTypes.array, 20 | PropTypes.object, 21 | PropTypes.string 22 | ]) 23 | }; 24 | 25 | module.exports = CodeBlockWrapper; 26 | -------------------------------------------------------------------------------- /docs/src/components/ComponentExample.js: -------------------------------------------------------------------------------- 1 | import classnames from "classnames/dedupe"; 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | 5 | class ComponentExample extends React.Component { 6 | render() { 7 | const classes = classnames("panel-cell", this.props.classNames); 8 | 9 | return
{this.props.children}
; 10 | } 11 | } 12 | 13 | ComponentExample.propTypes = { 14 | children: PropTypes.node.isRequired, 15 | classNames: PropTypes.oneOfType([ 16 | PropTypes.array, 17 | PropTypes.object, 18 | PropTypes.string 19 | ]) 20 | }; 21 | 22 | module.exports = ComponentExample; 23 | -------------------------------------------------------------------------------- /docs/src/components/ComponentExampleWrapper.js: -------------------------------------------------------------------------------- 1 | import classnames from "classnames/dedupe"; 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | 5 | class ComponentExampleWrapper extends React.Component { 6 | render() { 7 | const classes = classnames( 8 | "panel pod flush-top flush-right flush-left", 9 | this.props.className 10 | ); 11 | 12 | return
{this.props.children}
; 13 | } 14 | } 15 | 16 | ComponentExampleWrapper.propTypes = { 17 | children: PropTypes.node.isRequired, 18 | className: PropTypes.oneOfType([ 19 | PropTypes.array, 20 | PropTypes.object, 21 | PropTypes.string 22 | ]) 23 | }; 24 | 25 | module.exports = ComponentExampleWrapper; 26 | -------------------------------------------------------------------------------- /docs/src/components/ComponentWrapper.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import IconCode from "./icons/IconCode"; 5 | 6 | class ComponentWrapper extends React.Component { 7 | render() { 8 | const { props } = this; 9 | 10 | return ( 11 |
12 |

13 | {props.title} 14 | 15 | 16 | 17 |

18 | {props.children} 19 |
20 | ); 21 | } 22 | } 23 | 24 | ComponentWrapper.propTypes = { 25 | children: PropTypes.node.isRequired, 26 | srcURI: PropTypes.string.isRequired, 27 | title: PropTypes.string.isRequired 28 | }; 29 | 30 | module.exports = ComponentWrapper; 31 | -------------------------------------------------------------------------------- /docs/src/components/PropertiesAPIBlock.js: -------------------------------------------------------------------------------- 1 | import classNames from "classnames"; 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | 5 | import CodeBlock from "./CodeBlock"; 6 | import CodeBlockWrapper from "./CodeBlockWrapper"; 7 | import BindMixin from "../../../src/Mixin/BindMixin"; 8 | import Util from "../../../src/Util/Util"; 9 | 10 | class PropertiesAPIBlock extends Util.mixin(BindMixin) { 11 | get methodsToBind() { 12 | return ["handleToggleClick"]; 13 | } 14 | 15 | constructor() { 16 | super(); 17 | 18 | this.state = { 19 | open: false 20 | }; 21 | } 22 | 23 | handleToggleClick() { 24 | this.setState({ open: !this.state.open }); 25 | } 26 | 27 | render() { 28 | const toggleClasses = classNames( 29 | "h4 button button-link dropdown-toggle", 30 | "example-block-toggle flush", 31 | { 32 | open: this.state.open 33 | }, 34 | this.props.toggleClasses 35 | ); 36 | const panelInnerClassNames = classNames("example-block", { 37 | "is-expanded": !this.state.open 38 | }); 39 | 40 | return ( 41 |
42 |

43 | Properties API 44 |

45 | 46 | 47 | {this.props.propTypesBlock} 48 | 49 | 50 |
51 | ); 52 | } 53 | } 54 | 55 | PropertiesAPIBlock.propTypes = { 56 | toggleClasses: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), 57 | propTypesBlock: PropTypes.string.isRequired 58 | }; 59 | 60 | module.exports = PropertiesAPIBlock; 61 | -------------------------------------------------------------------------------- /docs/src/components/icons/IconCode.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | class IconCode extends React.Component { 5 | render() { 6 | return ( 7 | 15 | 16 | 17 | ); 18 | } 19 | } 20 | 21 | IconCode.defaultProps = { 22 | className: "icon icon-code" 23 | }; 24 | 25 | IconCode.propTypes = { 26 | className: PropTypes.string 27 | }; 28 | 29 | module.exports = IconCode; 30 | -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. 2 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 3 | This license is copied below, and is also available with a FAQ at: 4 | http://scripts.sil.org/OFL 5 | 6 | 7 | ----------------------------------------------------------- 8 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 9 | ----------------------------------------------------------- 10 | 11 | PREAMBLE 12 | The goals of the Open Font License (OFL) are to stimulate worldwide 13 | development of collaborative font projects, to support the font creation 14 | efforts of academic and linguistic communities, and to provide a free and 15 | open framework in which fonts may be shared and improved in partnership 16 | with others. 17 | 18 | The OFL allows the licensed fonts to be used, studied, modified and 19 | redistributed freely as long as they are not sold by themselves. The 20 | fonts, including any derivative works, can be bundled, embedded, 21 | redistributed and/or sold with any software provided that any reserved 22 | names are not used by derivative works. The fonts and derivatives, 23 | however, cannot be released under any other type of license. The 24 | requirement for fonts to remain under this license does not apply 25 | to any document created using the fonts or their derivatives. 26 | 27 | DEFINITIONS 28 | "Font Software" refers to the set of files released by the Copyright 29 | Holder(s) under this license and clearly marked as such. This may 30 | include source files, build scripts and documentation. 31 | 32 | "Reserved Font Name" refers to any names specified as such after the 33 | copyright statement(s). 34 | 35 | "Original Version" refers to the collection of Font Software components as 36 | distributed by the Copyright Holder(s). 37 | 38 | "Modified Version" refers to any derivative made by adding to, deleting, 39 | or substituting -- in part or in whole -- any of the components of the 40 | Original Version, by changing formats or by porting the Font Software to a 41 | new environment. 42 | 43 | "Author" refers to any designer, engineer, programmer, technical 44 | writer or other person who contributed to the Font Software. 45 | 46 | PERMISSION & CONDITIONS 47 | Permission is hereby granted, free of charge, to any person obtaining 48 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 49 | redistribute, and sell modified and unmodified copies of the Font 50 | Software, subject to the following conditions: 51 | 52 | 1) Neither the Font Software nor any of its individual components, 53 | in Original or Modified Versions, may be sold by itself. 54 | 55 | 2) Original or Modified Versions of the Font Software may be bundled, 56 | redistributed and/or sold with any software, provided that each copy 57 | contains the above copyright notice and this license. These can be 58 | included either as stand-alone text files, human-readable headers or 59 | in the appropriate machine-readable metadata fields within text or 60 | binary files as long as those fields can be easily viewed by the user. 61 | 62 | 3) No Modified Version of the Font Software may use the Reserved Font 63 | Name(s) unless explicit written permission is granted by the corresponding 64 | Copyright Holder. This restriction only applies to the primary font name as 65 | presented to the users. 66 | 67 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 68 | Software shall not be used to promote, endorse or advertise any 69 | Modified Version, except to acknowledge the contribution(s) of the 70 | Copyright Holder(s) and the Author(s) or with their explicit written 71 | permission. 72 | 73 | 5) The Font Software, modified or unmodified, in part or in whole, 74 | must be distributed entirely under this license, and must not be 75 | distributed under any other license. The requirement for fonts to 76 | remain under this license does not apply to any document created 77 | using the Font Software. 78 | 79 | TERMINATION 80 | This license becomes null and void if any of the above conditions are 81 | not met. 82 | 83 | DISCLAIMER 84 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 85 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 86 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 87 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 88 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 89 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 90 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 91 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 92 | OTHER DEALINGS IN THE FONT SOFTWARE. 93 | -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-Black.ttf -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-Black.woff -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-BlackItalic.ttf -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-BlackItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-BlackItalic.woff -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-Bold.ttf -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-Bold.woff -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-BoldItalic.ttf -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-BoldItalic.woff -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-ExtraLight.ttf -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-ExtraLight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-ExtraLight.woff -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-ExtraLightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-ExtraLightItalic.ttf -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-ExtraLightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-ExtraLightItalic.woff -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-Italic.ttf -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-Italic.woff -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-Light.ttf -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-Light.woff -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-LightItalic.ttf -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-LightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-LightItalic.woff -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-Regular.ttf -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-Regular.woff -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-Semibold.ttf -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-Semibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-Semibold.woff -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-SemiboldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-SemiboldItalic.ttf -------------------------------------------------------------------------------- /docs/src/fonts/source-sans-pro/SourceSansPro-SemiboldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d2iq-archive/reactjs-components/7341ca2675ca6f54aa825d0ae763732b9b4297c7/docs/src/fonts/source-sans-pro/SourceSansPro-SemiboldItalic.woff -------------------------------------------------------------------------------- /docs/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mesosphere UI Components 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/src/vendor/canvas-script.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | !(function($) { 3 | $(window).on("load", function() { 4 | init(); 5 | }); 6 | 7 | function init() { 8 | $.app = {}; 9 | $.app.stage = $("#stage"); 10 | $.app.scrollbar_width = get_scrollbar_width(); 11 | 12 | $(window).resize(function() { 13 | window_resize(); 14 | }); 15 | 16 | window_resize(); 17 | 18 | $(window).scroll(function() { 19 | window_scroll(); 20 | }); 21 | 22 | window_scroll(); 23 | scroll_to_anchor(); 24 | 25 | window.prettyPrint && prettyPrint(); 26 | 27 | page_content_navigation_update(); 28 | } 29 | 30 | function window_resize() { 31 | var responsive_viewport = $(window).width() + $.app.scrollbar_width; 32 | window_scroll(); 33 | 34 | $("#page-content-navigation").affix({ 35 | offset: { 36 | top: function() { 37 | var page_content_navigation = $("#page-content-navigation"); 38 | var page_content_navigation_threshold_top = $( 39 | "#page-content" 40 | ).offset().top; 41 | return page_content_navigation_threshold_top; 42 | }, 43 | 44 | bottom: function() { 45 | var footer_height = $("#footer").outerHeight(true); 46 | var page_content_navigation_margin_bottom = parseInt( 47 | jQuery("#page-content-navigation").css("margin-bottom") 48 | ); 49 | var page_content_navigation_threshold_bottom = 50 | footer_height + page_content_navigation_margin_bottom; 51 | return page_content_navigation_threshold_bottom; 52 | } 53 | } 54 | }); 55 | } 56 | 57 | window.window_resize = window_resize; 58 | 59 | function window_scroll() { 60 | var scroll_top = $(window).scrollTop() - $("#canvas").offset().top; 61 | page_content_navigation_update(); 62 | } 63 | 64 | function page_content_navigation_update() { 65 | var scroll_top = $(window).scrollTop() - $("#canvas").offset().top; 66 | var page_content_navigation = $("#page-content-navigation"); 67 | 68 | page_content_navigation.find("li").each(function() { 69 | $(this) 70 | .children("a") 71 | .each(function() { 72 | var el = $($(this).attr("href")); 73 | 74 | if (el != null) { 75 | var el_top = el.offset().top; 76 | var el_height = el.height(); 77 | 78 | if (scroll_top >= el_top - 1 && scroll_top < el_top + el_height) { 79 | $(this) 80 | .parent() 81 | .addClass("active"); 82 | } else { 83 | $(this) 84 | .parent() 85 | .removeClass("active"); 86 | } 87 | } 88 | }); 89 | }); 90 | } 91 | 92 | function get_browser_dimensions() { 93 | var dimensions = { 94 | width: 0, 95 | height: 0 96 | }; 97 | 98 | if ($(window)) { 99 | dimensions.width = $(window).width(); 100 | dimensions.height = $(window).height(); 101 | } 102 | 103 | return dimensions; 104 | } 105 | 106 | function get_scrollbar_width() { 107 | var div = $( 108 | '
' 109 | ); 110 | $("body").append(div); 111 | var w1 = $("div", div).innerWidth(); 112 | div.css("overflow-y", "auto"); 113 | var w2 = $("div", div).innerWidth(); 114 | $(div).remove(); 115 | return w1 - w2; 116 | } 117 | 118 | function scroll_to_anchor(speed) { 119 | if (typeof speed === "undefined" || speed === null) { 120 | speed = 500; 121 | } 122 | 123 | $("a[href*=#]:not([href=#])").click(function() { 124 | if ( 125 | location.pathname.replace(/^\//, "") == 126 | this.pathname.replace(/^\//, "") && 127 | location.hostname == this.hostname 128 | ) { 129 | var target = $(this.hash); 130 | target = target.length 131 | ? target 132 | : $("[name=" + this.hash.slice(1) + "]"); 133 | 134 | if (target.length) { 135 | $("html,body").animate( 136 | { 137 | scrollTop: target.offset().top 138 | }, 139 | speed 140 | ); 141 | 142 | return false; 143 | } 144 | } 145 | }); 146 | } 147 | })(window.jQuery); 148 | -------------------------------------------------------------------------------- /docs/src/vendor/prettify.css: -------------------------------------------------------------------------------- 1 | .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} 2 | -------------------------------------------------------------------------------- /docs/styles/components/fill/styles.less: -------------------------------------------------------------------------------- 1 | & when (@fill-enabled) { 2 | 3 | .fill-light { 4 | .element-shape-style(fill-light); 5 | } 6 | 7 | .fill-dark { 8 | .element-shape-style(fill-dark); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /docs/styles/components/fill/variables.less: -------------------------------------------------------------------------------- 1 | @fill-enabled: true; 2 | 3 | /* 4 | * Styling 5 | */ 6 | 7 | /* Light */ 8 | 9 | @fill-light-background-color: color-lighten(@grey-dark, 95); 10 | @fill-light-background-gradient-color-top: null; 11 | @fill-light-background-gradient-color-bottom: null; 12 | @fill-light-border-width: null; 13 | @fill-light-border-color: null; 14 | @fill-light-border-style: null; 15 | @fill-light-border-top-width: null; 16 | @fill-light-border-top-color: null; 17 | @fill-light-border-top-style: null; 18 | @fill-light-border-right-width: null; 19 | @fill-light-border-right-color: null; 20 | @fill-light-border-right-style: null; 21 | @fill-light-border-bottom-width: null; 22 | @fill-light-border-bottom-color: null; 23 | @fill-light-border-bottom-style: null; 24 | @fill-light-border-left-width: null; 25 | @fill-light-border-left-color: null; 26 | @fill-light-border-left-style: null; 27 | @fill-light-shadow: null; 28 | 29 | /* Dark */ 30 | 31 | @fill-dark-background-color: color-lighten(@grey-dark, -25); 32 | @fill-dark-background-gradient-color-top: null; 33 | @fill-dark-background-gradient-color-bottom: null; 34 | @fill-dark-border-width: null; 35 | @fill-dark-border-color: null; 36 | @fill-dark-border-style: null; 37 | @fill-dark-border-top-width: null; 38 | @fill-dark-border-top-color: null; 39 | @fill-dark-border-top-style: null; 40 | @fill-dark-border-right-width: null; 41 | @fill-dark-border-right-color: null; 42 | @fill-dark-border-right-style: null; 43 | @fill-dark-border-bottom-width: null; 44 | @fill-dark-border-bottom-color: null; 45 | @fill-dark-border-bottom-style: null; 46 | @fill-dark-border-left-width: null; 47 | @fill-dark-border-left-color: null; 48 | @fill-dark-border-left-style: null; 49 | @fill-dark-shadow: null; 50 | -------------------------------------------------------------------------------- /docs/styles/content/code/prettyprint.less: -------------------------------------------------------------------------------- 1 | /* Prettyprint */ 2 | 3 | /* GitHub Theme */ 4 | .prettyprint { 5 | font-family: 'Menlo', 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', 'Monaco', 'Consolas', monospace; 6 | 7 | .pln { 8 | color: #333; 9 | } 10 | 11 | @media screen { 12 | 13 | .str { 14 | color: #d14; 15 | } 16 | 17 | .kwd { 18 | color: #333; 19 | } 20 | 21 | .com { 22 | color: #998; 23 | } 24 | 25 | .typ { 26 | color: #458; 27 | } 28 | 29 | .lit { 30 | color: #458; 31 | } 32 | 33 | .pun { 34 | color: #333; 35 | } 36 | 37 | .opn { 38 | color: #333; 39 | } 40 | 41 | .clo { 42 | color: #333; 43 | } 44 | 45 | .tag { 46 | color: #000080; 47 | } 48 | 49 | .atn { 50 | color: #000080; 51 | } 52 | 53 | .atv { 54 | color: #d14; 55 | } 56 | 57 | .dec { 58 | color: #333; 59 | } 60 | 61 | .var { 62 | color: #000080; 63 | } 64 | 65 | .fun { 66 | color: #900; 67 | } 68 | } 69 | 70 | @media print, projection { 71 | 72 | .str { 73 | color: #060; 74 | } 75 | 76 | .kwd { 77 | color: #006; 78 | font-weight: 700; 79 | } 80 | 81 | .com { 82 | color: #600; 83 | font-style: italic; 84 | } 85 | 86 | .typ { 87 | color: #404; 88 | font-weight: 700; 89 | } 90 | 91 | .lit { 92 | color: #044; 93 | } 94 | 95 | .pun, 96 | .opn, 97 | .clo { 98 | color: #440; 99 | } 100 | 101 | .tag { 102 | color: #006; 103 | font-weight: 700; 104 | } 105 | 106 | .atn { 107 | color: #404; 108 | } 109 | 110 | .atv { 111 | color: #060; 112 | } 113 | } 114 | 115 | /* Specify class=linenums on a pre to get line numbering */ 116 | ol.linenums { 117 | margin-bottom: 0; 118 | margin-top: 0; 119 | } 120 | } 121 | 122 | /* Prettyprint (Inverse) */ 123 | .prettyprint.code-block-inverse { 124 | /*! Color themes for Google Code Prettify | MIT License | github.com/jmblog/color-themes-for-google-code-prettify */ 125 | background: #1b1918; 126 | border: 0 !important; 127 | font-family: 'Menlo', 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', 'Monaco', 'Consolas', monospace; 128 | 129 | .pln { 130 | color: #f1efee; 131 | } 132 | 133 | /* Specify class=linenums on a pre to get line numbering */ 134 | ol.linenums { 135 | color: #766e6b; 136 | margin-bottom: 0; 137 | margin-top: 0; 138 | } 139 | 140 | li.L0, 141 | li.L1, 142 | li.L2, 143 | li.L3, 144 | li.L4, 145 | li.L5, 146 | li.L6, 147 | li.L7, 148 | li.L8, 149 | li.L9 { 150 | background-color: #1b1918; 151 | list-style-type: decimal; 152 | padding-left: 1em; 153 | } 154 | 155 | @media screen { 156 | /* string content */ 157 | .str { 158 | color: #7b9726; 159 | } 160 | 161 | /* keyword */ 162 | .kwd { 163 | color: #6666ea; 164 | } 165 | 166 | /* comment */ 167 | .com { 168 | color: #766e6b; 169 | } 170 | 171 | /* type name */ 172 | 173 | .typ { 174 | color: #407ee7; 175 | } 176 | 177 | /* literal value */ 178 | .lit { 179 | color: #df5320; 180 | } 181 | 182 | /* punctuation */ 183 | .pun { 184 | color: #f1efee; 185 | } 186 | 187 | /* lisp open bracket */ 188 | .opn { 189 | color: #f1efee; 190 | } 191 | 192 | /* lisp close bracket */ 193 | .clo { 194 | color: #f1efee; 195 | } 196 | 197 | /* markup tag name */ 198 | .tag { 199 | color: #f22c40; 200 | } 201 | 202 | /* markup attribute name */ 203 | .atn { 204 | color: #df5320; 205 | } 206 | 207 | /* markup attribute value */ 208 | .atv { 209 | color: #3d97b8; 210 | } 211 | 212 | /* declaration */ 213 | .dec { 214 | color: #df5320; 215 | } 216 | 217 | /* variable name */ 218 | .var { 219 | color: #f22c40; 220 | } 221 | 222 | /* function name */ 223 | .fun { 224 | color: #407ee7; 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /docs/styles/content/example-block/styles.less: -------------------------------------------------------------------------------- 1 | .example-block-toggle { 2 | padding: 0; 3 | text-align: left; 4 | 5 | &.open { 6 | 7 | &:after { 8 | transform: rotate(180deg); 9 | } 10 | } 11 | } 12 | 13 | .example-block { 14 | overflow: hidden; 15 | 16 | &.is-expanded { 17 | height: @base-spacing-unit * 1.7; 18 | position: relative; 19 | 20 | &:after { 21 | background: linear-gradient(180deg, fade(@panel-cell-light-background-color, 0%) 0%, @panel-cell-light-background-color 90%); 22 | border-radius: 0 0 @panel-border-radius @panel-border-radius; 23 | bottom: 0; 24 | content: ''; 25 | height: 25px; 26 | left: 0; 27 | position: absolute; 28 | width: 100%; 29 | z-index: 1; 30 | } 31 | } 32 | } 33 | 34 | .component-title { 35 | 36 | a { 37 | display: inline; 38 | margin-left: @base-spacing-unit * 0.25; 39 | } 40 | 41 | &:hover { 42 | 43 | a { 44 | display: inline; 45 | } 46 | } 47 | } 48 | 49 | & when (@layout-screen-small-enabled) { 50 | 51 | @media (min-width: @layout-screen-small-min-width) { 52 | 53 | .example-block { 54 | 55 | &.is-expanded { 56 | height: @base-spacing-unit-screen-small * 1.7; 57 | } 58 | } 59 | 60 | .component-title { 61 | 62 | a { 63 | margin-left: @base-spacing-unit-screen-small * 0.25; 64 | } 65 | } 66 | } 67 | } 68 | 69 | & when (@layout-screen-medium-enabled) { 70 | 71 | @media (min-width: @layout-screen-medium-min-width) { 72 | 73 | .example-block { 74 | 75 | &.is-expanded { 76 | height: @base-spacing-unit-screen-medium * 1.7; 77 | } 78 | } 79 | 80 | .component-title { 81 | 82 | a { 83 | display: none; 84 | margin-left: @base-spacing-unit-screen-medium * 0.25; 85 | } 86 | } 87 | } 88 | } 89 | 90 | & when (@layout-screen-large-enabled) { 91 | 92 | @media (min-width: @layout-screen-large-min-width) { 93 | 94 | .example-block { 95 | 96 | &.is-expanded { 97 | height: @base-spacing-unit-screen-large * 1.7; 98 | } 99 | } 100 | 101 | .component-title { 102 | 103 | a { 104 | margin-left: @base-spacing-unit-screen-large * 0.25; 105 | } 106 | } 107 | } 108 | } 109 | 110 | & when (@layout-screen-jumbo-enabled) { 111 | 112 | @media (min-width: @layout-screen-jumbo-min-width) { 113 | 114 | .example-block { 115 | 116 | &.is-expanded { 117 | height: @base-spacing-unit-screen-jumbo * 1.7; 118 | } 119 | } 120 | 121 | .component-title { 122 | 123 | a { 124 | margin-left: @base-spacing-unit-screen-jumbo * 0.25; 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /docs/styles/content/fonts/styles.less: -------------------------------------------------------------------------------- 1 | & when (@fonts-enabled) { 2 | 3 | & when (@fonts-source-sans-pro-enabled) { 4 | 5 | // Source Sans Pro 6 | @font-face { 7 | font-family: 'Source Sans Pro'; 8 | font-style: normal; 9 | font-weight: 200; 10 | src: 11 | local('Source Sans Pro Extra Light'), 12 | local('Source-Sans–Pro-Extra-Light'), 13 | url('@{fonts-path}source-sans-pro/SourceSansPro-ExtraLight.woff') format('woff'), 14 | url('@{fonts-path}source-sans-pro/SourceSansPro-ExtraLight.ttf') format('truetype'); 15 | } 16 | 17 | @font-face { 18 | font-family: 'Source Sans Pro'; 19 | font-style: italic; 20 | font-weight: 200; 21 | src: 22 | local('Source Sans Pro Extra Light Italic'), 23 | local('Source-Sans–Pro-Extra-Light-Italic'), 24 | url('@{fonts-path}source-sans-pro/SourceSansPro-ExtraLightItalic.woff') format('woff'), 25 | url('@{fonts-path}source-sans-pro/SourceSansPro-ExtraLightItalic.ttf') format('truetype'); 26 | } 27 | 28 | @font-face { 29 | font-family: 'Source Sans Pro'; 30 | font-style: normal; 31 | font-weight: 300; 32 | src: 33 | local('Source Sans Pro Light'), 34 | local('Source-Sans–Pro-Light'), 35 | url('@{fonts-path}source-sans-pro/SourceSansPro-Light.woff') format('woff'), 36 | url('@{fonts-path}source-sans-pro/SourceSansPro-Light.ttf') format('truetype'); 37 | } 38 | 39 | @font-face { 40 | font-family: 'Source Sans Pro'; 41 | font-style: italic; 42 | font-weight: 300; 43 | src: 44 | local('Source Sans Pro Light Italic'), 45 | local('Source-Sans–Pro-Light-Italic'), 46 | url('@{fonts-path}source-sans-pro/SourceSansPro-LightItalic.woff') format('woff'), 47 | url('@{fonts-path}source-sans-pro/SourceSansPro-LightItalic.ttf') format('truetype'); 48 | } 49 | 50 | @font-face { 51 | font-family: 'Source Sans Pro'; 52 | font-style: normal; 53 | font-weight: 400; 54 | src: 55 | local('Source Sans Pro Regular'), 56 | local('Source-Sans–Pro-Regular'), 57 | url('@{fonts-path}source-sans-pro/SourceSansPro-Regular.woff') format('woff'), 58 | url('@{fonts-path}source-sans-pro/SourceSansPro-Regular.ttf') format('truetype'); 59 | } 60 | 61 | @font-face { 62 | font-family: 'Source Sans Pro'; 63 | font-style: italic; 64 | font-weight: 400; 65 | src: 66 | local('Source Sans Pro Italic'), 67 | local('Source-Sans–Pro-Italic'), 68 | url('@{fonts-path}source-sans-pro/SourceSansPro-Italic.woff') format('woff'), 69 | url('@{fonts-path}source-sans-pro/SourceSansPro-Italic.ttf') format('truetype'); 70 | } 71 | 72 | @font-face { 73 | font-family: 'Source Sans Pro'; 74 | font-style: normal; 75 | font-weight: 600; 76 | src: 77 | local('Source Sans Pro Semibold'), 78 | local('Source-Sans–Pro-Semibold'), 79 | url('@{fonts-path}source-sans-pro/SourceSansPro-Semibold.woff') format('woff'), 80 | url('@{fonts-path}source-sans-pro/SourceSansPro-Semibold.ttf') format('truetype'); 81 | } 82 | 83 | @font-face { 84 | font-family: 'Source Sans Pro'; 85 | font-style: italic; 86 | font-weight: 600; 87 | src: 88 | local('Source Sans Pro Semibold Italic'), 89 | local('Source-Sans–Pro-Semibold-Italic'), 90 | url('@{fonts-path}source-sans-pro/SourceSansPro-SemiboldItalic.woff') format('woff'), 91 | url('@{fonts-path}source-sans-pro/SourceSansPro-SemiboldItalic.ttf') format('truetype'); 92 | } 93 | 94 | @font-face { 95 | font-family: 'Source Sans Pro'; 96 | font-style: normal; 97 | font-weight: 700; 98 | src: 99 | local('Source Sans Pro Bold'), 100 | local('Source-Sans–Pro-Bold'), 101 | url('@{fonts-path}source-sans-pro/SourceSansPro-Bold.woff') format('woff'), 102 | url('@{fonts-path}source-sans-pro/SourceSansPro-Bold.ttf') format('truetype'); 103 | } 104 | 105 | @font-face { 106 | font-family: 'Source Sans Pro'; 107 | font-style: italic; 108 | font-weight: 700; 109 | src: 110 | local('Source Sans Pro Bold Italic'), 111 | local('Source-Sans–Pro-Bold-Italic'), 112 | url('@{fonts-path}source-sans-pro/SourceSansPro-BoldItalic.woff') format('woff'), 113 | url('@{fonts-path}source-sans-pro/SourceSansPro-BoldItalic.ttf') format('truetype'); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /docs/styles/content/fonts/variables.less: -------------------------------------------------------------------------------- 1 | @fonts-enabled: true; 2 | @fonts-path: '../../../fonts/'; 3 | 4 | /* 5 | * Source Sans Pro 6 | */ 7 | 8 | @fonts-source-sans-pro-enabled: true; 9 | -------------------------------------------------------------------------------- /docs/styles/index.less: -------------------------------------------------------------------------------- 1 | /* Config */ 2 | 3 | @path-images: '../images/'; 4 | @path-fonts: '../fonts/'; 5 | 6 | // Disabling stylelint for the following Javascript. 7 | /* stylelint-disable */ 8 | @cache-clear: `Math.floor((Math.random() * 1000000000) + 1)`; 9 | /* stylelint-enable */ 10 | 11 | /* Canvas */ 12 | @import '../../node_modules/cnvs/styles/cnvs.less'; 13 | 14 | /* Layout */ 15 | @import 'layout/foundation.less'; 16 | @import 'layout/sidebar.less'; 17 | @import 'layout/navigation.less'; 18 | @import 'layout/page.less'; 19 | @import 'layout/page-header.less'; 20 | 21 | /* Content */ 22 | @import 'content/code/prettyprint.less'; 23 | @import 'content/fonts/variables.less'; 24 | @import 'content/fonts/styles.less'; 25 | @import 'content/example-block/styles.less'; 26 | 27 | /* Components */ 28 | @import 'components/fill/variables.less'; 29 | @import 'components/fill/styles.less'; 30 | 31 | /* ReactJS Components */ 32 | @import (inline)'../../node_modules/gemini-scrollbar/gemini-scrollbar.css'; 33 | @import '../../src/overrides/gemini-scrollbar.less'; 34 | @import '../../src/Dropdown/styles.less'; 35 | @import '../../src/Form/styles.less'; 36 | @import '../../src/List/styles.less'; 37 | @import '../../src/Modal/styles.less'; 38 | @import '../../src/Table/styles.less'; 39 | @import '../../src/Tooltip/styles.less'; 40 | @import '../../src/index.less'; 41 | 42 | /* ----------------------------------------------------------------------------- 43 | Documentation-specific styles 44 | ----------------------------------------------------------------------------- */ 45 | @import '../src/Dropdown/styles.less'; 46 | @import '../src/Tooltip/styles.less'; 47 | -------------------------------------------------------------------------------- /docs/styles/layout/foundation.less: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | margin: 0; 5 | overflow: hidden; 6 | width: 100%; 7 | } 8 | 9 | .canvas { 10 | height: 100%; 11 | overflow: hidden; 12 | } 13 | -------------------------------------------------------------------------------- /docs/styles/layout/page-header.less: -------------------------------------------------------------------------------- 1 | @keyframes octocat-wave { 2 | 3 | 0%, 4 | 100% { 5 | transform: rotate(0); 6 | } 7 | 8 | 20%, 9 | 60% { 10 | transform: rotate(-25deg); 11 | } 12 | 13 | 40%, 14 | 80% { 15 | transform: rotate(10deg); 16 | } 17 | } 18 | 19 | .page-header { 20 | .clearfix(); 21 | 22 | .page-header-subheadline { 23 | 24 | a { 25 | text-decoration: none; 26 | } 27 | } 28 | 29 | .github-corner { 30 | bottom: 0; 31 | left: 50%; 32 | position: absolute; 33 | transform: translateX(-50%); 34 | 35 | &:hover { 36 | 37 | .octocat-tail { 38 | animation: octocat-wave 0.5s ease-in-out; 39 | } 40 | } 41 | 42 | .octocat { 43 | display: block; 44 | fill: desaturate(lighten(@grey-dark, 25%), 15%); 45 | height: 40px; 46 | width: auto; 47 | } 48 | 49 | .octocat-tail { 50 | transform-origin: 70px 110px; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /docs/styles/layout/page.less: -------------------------------------------------------------------------------- 1 | .page { 2 | overflow-x: hidden; 3 | overflow-y: auto; 4 | } 5 | -------------------------------------------------------------------------------- /docs/styles/layout/sidebar.less: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | background: color-lighten(@grey-dark, -50); 3 | box-shadow: 1px 0 0 0 fade(@grey-dark, 10%); 4 | } 5 | 6 | & when (@layout-screen-medium-enabled) { 7 | 8 | @media (min-width: @layout-screen-medium-min-width) { 9 | 10 | .sidebar { 11 | flex-basis: 200px; 12 | } 13 | } 14 | } 15 | 16 | & when (@layout-screen-jumbo-enabled) { 17 | 18 | @media (min-width: @layout-screen-jumbo-min-width) { 19 | 20 | .sidebar { 21 | flex-basis: 250px; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var eslint = require("gulp-eslint"); 2 | var gulp = require("gulp"); 3 | var stylelint = require("gulp-stylelint"); 4 | 5 | var config = require("./.build.config.js"); 6 | 7 | // Set up docs tasks too 8 | require("./docs/gulpTasks"); 9 | 10 | gulp.task("eslint", function() { 11 | return gulp 12 | .src([config.dirs.srcJS + "/**/*.?(js|jsx)"]) 13 | .pipe(eslint()) 14 | .pipe(eslint.formatEach("stylish", process.stderr)); 15 | }); 16 | 17 | gulp.task("stylelint", function() { 18 | return gulp.src(config.dirs.srcCSS + "/**/*.less").pipe( 19 | stylelint({ 20 | reporters: [{ formatter: "string", console: true }] 21 | }) 22 | ); 23 | }); 24 | -------------------------------------------------------------------------------- /install_react.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$REACT_VERSION" = "16.7" ]; then npm install react@16.7 react-dom@16.7; fi 4 | if [ "$REACT_VERSION" = "16.8" ]; then npm install react@16.8 react-dom@16.8; fi 5 | -------------------------------------------------------------------------------- /jest/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "automock": true, 3 | "testPathDirs": [ 4 | "src" 5 | ], 6 | "globals": { 7 | "__DEV__": true 8 | }, 9 | "transform": { 10 | ".*": "./node_modules/babel-jest" 11 | }, 12 | "setupFiles": [ 13 | "jest/setupEnv.js" 14 | ], 15 | "moduleFileExtensions": [ 16 | "js", 17 | "json" 18 | ], 19 | "setupTestFrameworkScriptFile": "jest/setupTestFramework.js", 20 | "testRegex": "/__tests__/.*\\-test\\.(es6|js)$", 21 | "modulePathIgnorePatterns": [ 22 | "/tmp/", 23 | "/node_modules/", 24 | "/.module-cache/" 25 | ], 26 | "timers": "fake", 27 | "unmockedModulePathPatterns": [ 28 | "./src/Mixin", 29 | "./src/Util", 30 | "babel-runtime", 31 | "classnames", 32 | "react", 33 | "react-dom", 34 | "react-addons-test-utils", 35 | "fbjs" 36 | ], 37 | "testPathIgnorePatterns": [ 38 | "/node_modules/" 39 | ] 40 | } -------------------------------------------------------------------------------- /jest/setupEnv.js: -------------------------------------------------------------------------------- 1 | // jsdom doesn't have support for requestAnimationFrame so we polyfill it. 2 | // https://gist.github.com/paulirish/1579671 3 | (function() { 4 | var lastTime = 0; 5 | var vendors = ["ms", "moz", "webkit", "o"]; 6 | 7 | for (var x = 0; x < vendors.length && !global.requestAnimationFrame; ++x) { 8 | global.requestAnimationFrame = global[vendors[x] + "RequestAnimationFrame"]; 9 | global.cancelAnimationFrame = 10 | global[vendors[x] + "CancelAnimationFrame"] || 11 | global[vendors[x] + "CancelRequestAnimationFrame"]; 12 | } 13 | 14 | if (!global.requestAnimationFrame) 15 | global.requestAnimationFrame = function(callback, element) { 16 | var currTime = new Date().getTime(); 17 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 18 | var id = global.setTimeout(function() { 19 | callback(currTime + timeToCall); 20 | }, timeToCall); 21 | 22 | lastTime = currTime + timeToCall; 23 | return id; 24 | }; 25 | 26 | if (!global.cancelAnimationFrame) { 27 | global.cancelAnimationFrame = function(id) { 28 | clearTimeout(id); 29 | }; 30 | } 31 | })(); 32 | -------------------------------------------------------------------------------- /jest/setupTestFramework.js: -------------------------------------------------------------------------------- 1 | if (process.env.TEAMCITY_VERSION != null) { 2 | require("../node_modules/jasmine-reporters/src/jasmine.teamcity_reporter"); 3 | jasmine.getEnv().addReporter(new jasmine.TeamcityReporter()); 4 | } 5 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | var Confirm = require("./lib/Confirm/Confirm"); 2 | var Dropdown = require("./lib/Dropdown/Dropdown"); 3 | var DropdownTrigger = require("./lib/Dropdown/DropdownTrigger").default; 4 | var DropdownListTrigger = require("./lib/Dropdown/DropdownListTrigger").default; 5 | var Select = require("./lib/Select/Select").default; 6 | var SelectOption = require("./lib/Select/SelectOption").default; 7 | var Form = require("./lib/Form/Form"); 8 | var List = require("./lib/List/List"); 9 | var Modal = require("./lib/Modal/Modal"); 10 | var Table = require("./lib/Table/Table"); 11 | var Tooltip = require("./lib/Tooltip/Tooltip"); 12 | 13 | module.exports = { 14 | Confirm: Confirm, 15 | Dropdown: Dropdown, 16 | DropdownTrigger: DropdownTrigger, 17 | DropdownListTrigger: DropdownListTrigger, 18 | Select: Select, 19 | SelectOption: SelectOption, 20 | Form: Form, 21 | List: List, 22 | Modal: Modal, 23 | Table: Table, 24 | Tooltip: Tooltip 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactjs-components", 3 | "version": "0.0.0-dev+semantically-released", 4 | "description": "React UI Components by Mesosphere", 5 | "author": { 6 | "name": "Mesosphere Frontend Team", 7 | "email": "web-public@mesosphere.com", 8 | "url": "http://www.mesosphere.com" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/mesosphere/reactjs-components" 13 | }, 14 | "license": "Apache-2.0", 15 | "files": [ 16 | "main.js", 17 | "lib" 18 | ], 19 | "main": "main.js", 20 | "dependencies": { 21 | "classnames": "2.2.6", 22 | "lodash": "^4.17.5", 23 | "lodash.throttle": "4.1.1", 24 | "prop-types": "^15.6.0" 25 | }, 26 | "devDependencies": { 27 | "@dcos/eslint-config": "0.3.1", 28 | "babel-cli": "6.26.0", 29 | "babel-core": "6.26.3", 30 | "babel-eslint": "5.0.0", 31 | "babel-jest": "19.0.0", 32 | "babel-loader": "6.2.3", 33 | "babel-plugin-transform-runtime": "6.5.2", 34 | "babel-preset-es2015": "6.5.0", 35 | "babel-preset-react": "6.5.0", 36 | "babel-runtime": "6.5.0", 37 | "browser-sync": "2.26.7", 38 | "cnvs": "1.1.14", 39 | "envify": "3.4.0", 40 | "eslint": "3.15.0", 41 | "eslint-plugin-compat": "1.0.2", 42 | "eslint-plugin-destructuring": "2.2.0", 43 | "eslint-plugin-import": "1.13.0", 44 | "eslint-plugin-jsx-a11y": "2.1.0", 45 | "eslint-plugin-jsx-max-len": "1.0.0", 46 | "eslint-plugin-prettier": "2.0.1", 47 | "eslint-plugin-react": "6.9.0", 48 | "eslint-plugin-test-names": "^2.1.1", 49 | "glob": "7.1.6", 50 | "gulp": "3.9.1", 51 | "gulp-autoprefixer": "7.0.1", 52 | "gulp-concat": "2.6.0", 53 | "gulp-connect": "2.3.1", 54 | "gulp-eslint": "3.0.1", 55 | "gulp-less": "3.0.5", 56 | "gulp-minify-css": "1.2.3", 57 | "gulp-replace": "0.5.4", 58 | "gulp-sourcemaps": "1.6.0", 59 | "gulp-stylelint": "3.9.0", 60 | "gulp-uglify": "2.1.2", 61 | "gulp-util": "3.0.7", 62 | "jest-cli": "17.0.3", 63 | "less-color-lighten": "0.0.1", 64 | "node-libs-browser": "1.0.0", 65 | "prettier": "1.17.0", 66 | "react": "^16.8.1", 67 | "react-dom": "^16.8.1", 68 | "react-gemini-scrollbar": "2.3.x", 69 | "react-transition-group": "4.4.1", 70 | "semantic-release": "^17.0.0", 71 | "source-map-loader": "1.0.1", 72 | "stylelint": "13.3.3", 73 | "stylelint-config-dcos": "0.0.3", 74 | "stylelint-config-standard": "20.0.0", 75 | "stylelint-webpack-plugin": "2.1.0", 76 | "transform-loader": "0.2.3", 77 | "webpack": "1.13.2" 78 | }, 79 | "peerDependencies": { 80 | "cnvs": "1.1.14", 81 | "react": ">= 16.8.0", 82 | "react-dom": ">= 16.8.0", 83 | "react-transition-group": ">= 2.5.0", 84 | "react-gemini-scrollbar": "^2.1.5 || ^2.3.0" 85 | }, 86 | "scripts": { 87 | "clean": "rm -rf dist docs/dist lib", 88 | "dist": "npm run clean && NODE_ENV='production' ./node_modules/.bin/gulp docs:dist", 89 | "predist-src": "rm -rf ./lib", 90 | "dist-src": "cp -r ./src ./lib && find ./lib -name '__tests__' -type d -exec rm -r '{}' + && ./node_modules/.bin/babel lib --out-dir lib", 91 | "livereload": "NODE_ENV='development' ./node_modules/.bin/gulp docs:livereload", 92 | "test": "NODE_PATH=node_modules jest --config=jest/config.json --no-cache", 93 | "release": "npm version -m 'Version %s'", 94 | "release:major": "npm run release -- major", 95 | "release:minor": "npm run release -- minor", 96 | "release:patch": "npm run release -- patch", 97 | "release:premajor": "npm run release -- premajor", 98 | "release:preminor": "npm run release -- preminor", 99 | "release:prepatch": "npm run release -- prepatch", 100 | "release:prerelease": "npm run release -- prerelease", 101 | "release:beta": "npm run release -- prerelease", 102 | "lint": "npm run stylelint && npm run eslint", 103 | "eslint": "eslint --quiet src/**/*.js docs/src/**/*.js", 104 | "stylelint": "stylelint src/**/*.less docs/src/**/*.less", 105 | "preversion": "npm run clean && npm run dist-src" 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "automerge": true 4 | } 5 | } 6 | 7 | -------------------------------------------------------------------------------- /src/Confirm/Confirm.js: -------------------------------------------------------------------------------- 1 | import classNames from "classnames"; 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | 5 | import Modal from "../Modal/Modal"; 6 | import Util from "../Util/Util"; 7 | 8 | class Confirm extends React.Component { 9 | getButtons() { 10 | const disabledConfig = { disabled: this.props.disabled }; 11 | 12 | const leftButtonClassName = classNames(this.props.leftButtonClassName); 13 | const rightButtonClassName = classNames( 14 | this.props.rightButtonClassName, 15 | disabledConfig 16 | ); 17 | 18 | let extraAttributes = {}; 19 | if (this.props.disabled) { 20 | extraAttributes = disabledConfig; 21 | } 22 | 23 | return ( 24 |
25 | 31 | 38 |
39 | ); 40 | } 41 | 42 | render() { 43 | const props = Util.exclude( 44 | this.props, 45 | "children", 46 | "disabled", 47 | "leftButtonText", 48 | "leftButtonClassName", 49 | "leftButtonCallback", 50 | "rightButtonText", 51 | "rightButtonClassName", 52 | "rightButtonCallback" 53 | ); 54 | 55 | return ( 56 | 64 | {this.props.children} 65 | 66 | ); 67 | } 68 | } 69 | 70 | Confirm.defaultProps = { 71 | open: true, 72 | disabled: false, 73 | onClose() {}, 74 | 75 | // Left button properties 76 | leftButtonText: "Cancel", 77 | leftButtonClassName: "button button-primary-link flush-left", 78 | leftButtonCallback() {}, 79 | // Right button properties 80 | rightButtonText: "Confirm", 81 | rightButtonClassName: "button button-primary", 82 | rightButtonCallback() {} 83 | }; 84 | 85 | Confirm.propTypes = { 86 | children: PropTypes.node.isRequired, 87 | 88 | open: PropTypes.bool.isRequired, 89 | disabled: PropTypes.bool, 90 | // This will be triggered by backdrop click 91 | onClose: PropTypes.func, 92 | 93 | // Left button properties 94 | leftButtonText: PropTypes.string.isRequired, 95 | leftButtonClassName: PropTypes.string, 96 | leftButtonCallback: PropTypes.func.isRequired, 97 | // Right button properties 98 | rightButtonText: PropTypes.string.isRequired, 99 | rightButtonClassName: PropTypes.string, 100 | rightButtonCallback: PropTypes.func.isRequired 101 | }; 102 | 103 | module.exports = Confirm; 104 | -------------------------------------------------------------------------------- /src/Confirm/__tests__/Confirm-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | var React = require("react"); 3 | /* eslint-enable no-unused-vars */ 4 | 5 | var TestUtils; 6 | if (React.version.match(/15.[0-5]/)) { 7 | TestUtils = require("react-addons-test-utils"); 8 | } else { 9 | TestUtils = require("react-dom/test-utils"); 10 | } 11 | 12 | jest.dontMock("../Confirm.js"); 13 | 14 | var Confirm = require("../Confirm.js"); 15 | 16 | describe("Confirm", function() { 17 | describe("enabled", function() { 18 | beforeEach(function() { 19 | this.closeCallback = jasmine.createSpy("closeCallback"); 20 | this.confirmCallback = jasmine.createSpy("confirmCallback"); 21 | this.instance = TestUtils.renderIntoDocument( 22 | 29 |
30 | Would you like to perform this action? 31 |
32 |
33 | ); 34 | 35 | // Gotta do this hacky thing, 36 | // because rendering the portal is not a good idea 37 | this.instance = TestUtils.renderIntoDocument(this.instance.getButtons()); 38 | }); 39 | 40 | it("calls the left button callback", function() { 41 | var button = this.instance.querySelector(".left-button"); 42 | TestUtils.Simulate.click(button); 43 | expect(this.closeCallback).toHaveBeenCalled(); 44 | }); 45 | 46 | it("calls the right button callback", function() { 47 | var button = this.instance.querySelector(".right-button"); 48 | TestUtils.Simulate.click(button); 49 | expect(this.confirmCallback).toHaveBeenCalled(); 50 | }); 51 | }); 52 | 53 | describe("disabled", function() { 54 | beforeEach(function() { 55 | this.closeCallback = jasmine.createSpy("closeCallback"); 56 | this.confirmCallback = jasmine.createSpy("confirmCallback"); 57 | this.instance = TestUtils.renderIntoDocument( 58 | 66 |
67 | Would you like to perform this action? 68 |
69 |
70 | ); 71 | 72 | // Gotta do this hacky thing, 73 | // because rendering the portal is not a good idea 74 | this.instance = TestUtils.renderIntoDocument(this.instance.getButtons()); 75 | }); 76 | 77 | it("does call the left button callback", function() { 78 | var button = this.instance.querySelector(".left-button"); 79 | TestUtils.Simulate.click(button); 80 | expect(this.closeCallback).toHaveBeenCalled(); 81 | }); 82 | 83 | it("does not call the right button callback", function() { 84 | var button = this.instance.querySelector(".right-button"); 85 | TestUtils.Simulate.click(button); 86 | expect(this.confirmCallback).not.toHaveBeenCalled(); 87 | }); 88 | 89 | it("sets the disabled class on buttons", function() { 90 | var buttons = this.instance.querySelectorAll(".button"); 91 | expect(buttons.length).toEqual(2); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /src/Dropdown/DropdownListTrigger.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import DropdownTrigger from "./DropdownTrigger"; 4 | 5 | export default class DropdownListTrigger extends DropdownTrigger { 6 | render() { 7 | const { className, disabled, selectedItem } = this.props; 8 | const html = selectedItem 9 | ? selectedItem.selectedHtml || selectedItem.html 10 | : this.props.placeholder || null; 11 | 12 | return ( 13 | 21 | ); 22 | } 23 | } 24 | 25 | DropdownListTrigger.defaultProps = { 26 | className: "", 27 | placeholder: "", 28 | selectedItem: null, 29 | onTrigger() {}, 30 | disabled: false 31 | }; 32 | 33 | DropdownListTrigger.propTypes = { 34 | className: PropTypes.string, 35 | placeholder: PropTypes.string, 36 | selectedItem: PropTypes.object, 37 | onTrigger: PropTypes.func, 38 | disabled: PropTypes.bool 39 | }; 40 | -------------------------------------------------------------------------------- /src/Dropdown/DropdownTrigger.js: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | export default class DropdownTrigger extends Component { 5 | constructor() { 6 | super(...arguments); 7 | 8 | this.handleTrigger = this.handleTrigger.bind(this); 9 | } 10 | 11 | handleTrigger(event) { 12 | if (!this.props.disabled) { 13 | this.props.onTrigger(event); 14 | } 15 | } 16 | } 17 | 18 | DropdownTrigger.defaultProps = { 19 | onTrigger() {}, 20 | disabled: false 21 | }; 22 | 23 | DropdownTrigger.propTypes = { 24 | onTrigger: PropTypes.func, 25 | disabled: PropTypes.bool 26 | }; 27 | -------------------------------------------------------------------------------- /src/Dropdown/__tests__/DropdownListTrigger-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from "react"; 3 | import ReactDOM from "react-dom"; 4 | /* eslint-enable no-unused-vars */ 5 | import DropdownListTrigger from "../DropdownListTrigger.js"; 6 | 7 | var TestUtils; 8 | if (React.version.match(/15.[0-5]/)) { 9 | TestUtils = require("react-addons-test-utils"); 10 | } else { 11 | TestUtils = require("react-dom/test-utils"); 12 | } 13 | 14 | describe("DropdownListTrigger", function() { 15 | it("renders with given props", function() { 16 | expect(() => 17 | TestUtils.renderIntoDocument( 18 | 29 | ) 30 | ).not.toThrow(); 31 | }); 32 | it("renders correctly without props", function() { 33 | expect(() => 34 | TestUtils.renderIntoDocument() 35 | ).not.toThrow(); 36 | }); 37 | describe("#onTrigger", function() { 38 | afterEach(function() { 39 | Array.prototype.slice 40 | .call(document.body.children) 41 | .forEach(function(node) { 42 | document.body.removeChild(node); 43 | }); 44 | }); 45 | it("triggers onTrigger", function() { 46 | const callback = jest.fn(); 47 | const instance = TestUtils.renderIntoDocument( 48 | 49 | ); 50 | const button = TestUtils.findRenderedDOMComponentWithTag( 51 | instance, 52 | "button" 53 | ); 54 | TestUtils.Simulate.click(button); 55 | expect(callback).toBeCalled(); 56 | }); 57 | it("triggers onTrigger", function() { 58 | const callback = jest.fn(); 59 | const instance = TestUtils.renderIntoDocument( 60 | 61 | ); 62 | const button = TestUtils.findRenderedDOMComponentWithTag( 63 | instance, 64 | "button" 65 | ); 66 | TestUtils.Simulate.click(button); 67 | expect(callback).not.toBeCalled(); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/Dropdown/__tests__/fixtures/MockDropdownList.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "className": "dropdown-menu-header", 4 | "html": "Foo", 5 | "id": "foo", 6 | "selectable": false, 7 | "selectedHtml": "Foo" 8 | }, 9 | { 10 | "html": "Bar", 11 | "id": "bar", 12 | "selectedHtml": "Bar" 13 | }, 14 | { 15 | "html": "Baz", 16 | "id": "baz", 17 | "selectedHtml": "Baz" 18 | }, 19 | { 20 | "html": "Quz", 21 | "id": "quz", 22 | "selectedHtml": "Quz" 23 | }, 24 | { 25 | "className": "dropdown-menu-divider", 26 | "id": "divider-a", 27 | "selectable": false 28 | }, 29 | { 30 | "className": "dropdown-menu-header", 31 | "html": "Corge", 32 | "id": "corge", 33 | "selectable": false 34 | }, 35 | { 36 | "html": "Grault", 37 | "id": "grault", 38 | "selectedHtml": "Grault" 39 | } 40 | ] 41 | -------------------------------------------------------------------------------- /src/Dropdown/styles.less: -------------------------------------------------------------------------------- 1 | @keyframes dropdown-down-enter { 2 | 3 | 0% { 4 | transform: translateY(-30px); 5 | } 6 | 7 | 100% { 8 | transform: translateY(0); 9 | } 10 | } 11 | 12 | @keyframes dropdown-down-exit { 13 | 14 | 0% { 15 | transform: translateY(0); 16 | } 17 | 18 | 100% { 19 | transform: translateY(-30px); 20 | } 21 | } 22 | 23 | @keyframes dropdown-up-enter { 24 | 25 | 0% { 26 | transform: translateY(30px); 27 | } 28 | 29 | 100% { 30 | transform: translateY(0); 31 | } 32 | } 33 | 34 | @keyframes dropdown-up-exit { 35 | 36 | 0% { 37 | transform: translateY(0); 38 | } 39 | 40 | 100% { 41 | transform: translateY(30px); 42 | } 43 | } 44 | 45 | @keyframes dropdown-fade-in { 46 | 47 | 0% { 48 | opacity: 0; 49 | } 50 | 51 | 100% { 52 | opacity: 1; 53 | } 54 | } 55 | 56 | @keyframes dropdown-fade-out { 57 | 58 | 0% { 59 | opacity: 1; 60 | } 61 | 62 | 100% { 63 | opacity: 0; 64 | } 65 | } 66 | 67 | .dropdown { 68 | display: inline-block; 69 | outline: none; 70 | 71 | &.open { 72 | 73 | .dropdown-toggle { 74 | z-index: 4; 75 | } 76 | 77 | .dropdown-menu { 78 | z-index: 3; 79 | 80 | &.inverse { 81 | 82 | li { 83 | 84 | &.is-selectable { 85 | color: #fff; 86 | 87 | &:hover { 88 | background: #51596a; 89 | } 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | .dropdown-toggle { 98 | position: relatlive; 99 | z-index: 2; 100 | 101 | &:after { 102 | 103 | .open & { 104 | // translate hack activates gpu to prevent jerky animation bug 105 | transform: rotate(180deg) translate3d(0, 0, 0); 106 | } 107 | } 108 | } 109 | 110 | .dropdown-menu { 111 | // This can be removed when Canvas is updated to toggle visibility: visible 112 | display: block !important; 113 | padding-bottom: 0; 114 | padding-top: 0; 115 | position: fixed; 116 | z-index: 1; 117 | 118 | &.up { 119 | bottom: 100%; 120 | margin-bottom: 5px; 121 | top: auto; 122 | transform-origin: bottom center; 123 | } 124 | 125 | &.down { 126 | bottom: auto; 127 | margin-top: 5px; 128 | top: 100%; 129 | transform-origin: top center; 130 | } 131 | 132 | em { 133 | font-style: italic; 134 | } 135 | 136 | .small { 137 | font-size: 1em; 138 | } 139 | } 140 | 141 | .dropdown-menu-items { 142 | padding-bottom: 0.5rem; 143 | padding-top: 0.5rem; 144 | } 145 | 146 | .dropdown-menu-concealer { 147 | pointer-events: none; 148 | visibility: hidden; 149 | } 150 | 151 | .dropdown-menu-down-enter, 152 | .dropdown-menu-down-enter-active { 153 | animation: dropdown-fade-in 250ms both, dropdown-down-enter 250ms both; 154 | } 155 | 156 | .dropdown-menu-down-exit { 157 | animation: dropdown-fade-out 250ms both, dropdown-down-exit 250ms both; 158 | z-index: 3; 159 | } 160 | 161 | .dropdown-menu-up-enter { 162 | animation: dropdown-fade-in 250ms both, dropdown-up-enter 250ms both; 163 | } 164 | 165 | .dropdown-menu-up-exit { 166 | animation: dropdown-fade-out 250ms both, dropdown-up-exit 250ms both; 167 | z-index: 3; 168 | } 169 | 170 | .dropdown-menu-list { 171 | 172 | ul { 173 | margin-bottom: 0; 174 | } 175 | 176 | li { 177 | cursor: default; 178 | 179 | &.is-selectable { 180 | cursor: pointer; 181 | 182 | &:hover { 183 | background: #f5f5f6; 184 | } 185 | } 186 | 187 | &.dropdown-menu-header { 188 | color: #adb0b8; 189 | font-size: 10px; 190 | font-weight: 400; 191 | margin: 14px 0 3px; 192 | padding-bottom: 0; 193 | padding-top: 0; 194 | text-transform: uppercase; 195 | } 196 | 197 | &.dropdown-menu-divider { 198 | background: #ebebed; 199 | height: 1px; 200 | margin: 9px 0; 201 | padding: 0; 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/Form/FieldCheckbox.js: -------------------------------------------------------------------------------- 1 | import classNames from "classnames/dedupe"; 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | import ReactDOM from "react-dom"; 5 | 6 | import FieldRadioButton from "./FieldRadioButton"; 7 | import Util from "../Util/Util"; 8 | 9 | class FieldCheckbox extends FieldRadioButton { 10 | constructor() { 11 | super(...arguments); 12 | 13 | this.refElements = {}; 14 | } 15 | 16 | componentDidMount() { 17 | super.componentDidMount(...arguments); 18 | this.updateCheckbox(); 19 | } 20 | 21 | componentDidUpdate() { 22 | super.componentDidUpdate(...arguments); 23 | this.updateCheckbox(); 24 | } 25 | 26 | updateCheckbox() { 27 | const { props, refElements } = this; 28 | Object.keys(refElements).forEach(function(refName) { 29 | const checkbox = ReactDOM.findDOMNode(refElements[refName]); 30 | let indeterminate; 31 | // Single checkbox 32 | if (props.name === refName) { 33 | indeterminate = props.indeterminate; 34 | } 35 | 36 | // Multiple checkboxes 37 | if (Util.isArray(props.startValue)) { 38 | indeterminate = Util.find(props.startValue, function(item) { 39 | return item.name === refName; 40 | }).indeterminate; 41 | } 42 | 43 | if (indeterminate != null) { 44 | checkbox.indeterminate = indeterminate; 45 | } 46 | }); 47 | } 48 | 49 | handleChange(eventName, name, event) { 50 | const { props } = this; 51 | 52 | if (eventName === "multipleChange") { 53 | props.handleEvent( 54 | eventName, 55 | props.name, 56 | { name, checked: event.target.checked }, 57 | event 58 | ); 59 | } 60 | 61 | if (eventName === "change") { 62 | props.handleEvent(eventName, props.name, event.target.checked, event); 63 | } 64 | } 65 | 66 | getItemLabel(attributes) { 67 | if (!attributes.label) { 68 | return null; 69 | } 70 | 71 | const checkboxLabelClass = classNames( 72 | "form-element-checkbox-label", 73 | attributes.checkboxLabelClass, 74 | this.props.checkboxLabelClass 75 | ); 76 | 77 | return {attributes.label}; 78 | } 79 | 80 | getItem(eventName, labelClass, attributes, index) { 81 | const labelClasses = classNames( 82 | "form-control-toggle form-control-toggle-custom", 83 | labelClass, 84 | { mute: attributes.disabled }, 85 | attributes.labelClass 86 | ); 87 | 88 | const indicatorClasses = classNames( 89 | "form-control-toggle-indicator", 90 | attributes.indicatorClass 91 | ); 92 | 93 | const htmlAttributes = Util.pick(attributes, [ 94 | "checked", 95 | "className", 96 | "disabled", 97 | "name" 98 | ]); 99 | 100 | return ( 101 | 111 | ); 112 | } 113 | 114 | getRowClass() { 115 | const { columnWidth, formElementClass } = this.props; 116 | 117 | return classNames( 118 | `form-row-element column-${columnWidth}`, 119 | formElementClass 120 | ); 121 | } 122 | } 123 | 124 | FieldCheckbox.defaultProps = { 125 | columnWidth: 12, 126 | handleEvent() {} 127 | }; 128 | 129 | FieldCheckbox.propTypes = { 130 | // Optional number of columns to take up of the grid 131 | columnWidth: PropTypes.number.isRequired, 132 | // Function to handle change event 133 | // (usually passed down from form definition) 134 | handleEvent: PropTypes.func, 135 | // Optional boolean, string, or react node. 136 | // If boolean: true - shows name as label; false - shows nothing. 137 | // If string: shows string as label. 138 | // If node: returns the node as the label. 139 | showLabel: PropTypes.oneOfType([ 140 | PropTypes.node, 141 | PropTypes.string, 142 | PropTypes.bool 143 | ]), 144 | // Optional object of error messages, with key equal to field property name 145 | validationError: PropTypes.object, 146 | 147 | // Classes 148 | checkboxButtonLabelClass: PropTypes.oneOfType([ 149 | PropTypes.array, 150 | PropTypes.object, 151 | PropTypes.string 152 | ]), 153 | startValue: PropTypes.oneOfType([PropTypes.array, PropTypes.bool]), 154 | labelClass: PropTypes.string, 155 | indicatorClasses: PropTypes.string, 156 | indeterminate: PropTypes.bool 157 | }; 158 | 159 | module.exports = FieldCheckbox; 160 | -------------------------------------------------------------------------------- /src/Form/FieldPassword.js: -------------------------------------------------------------------------------- 1 | import classNames from "classnames/dedupe"; 2 | /* eslint-disable no-unused-vars */ 3 | import React from "react"; 4 | /* eslint-enable no-unused-vars */ 5 | 6 | import FieldInput from "./FieldInput"; 7 | import IconEdit from "./icons/IconEdit"; 8 | 9 | const METHODS_TO_BIND = ["handleOnBlur", "handleOnFocus"]; 10 | 11 | class FieldPassword extends FieldInput { 12 | constructor() { 13 | super(); 14 | 15 | METHODS_TO_BIND.forEach(method => { 16 | this[method] = this[method].bind(this); 17 | }); 18 | } 19 | 20 | handleOnBlur(event) { 21 | if (this.focused) { 22 | this.focused = false; 23 | this.forceUpdate(); 24 | } 25 | 26 | this.props.handleEvent("blur", this.props.name, event.target.value, event); 27 | } 28 | 29 | handleOnFocus(event) { 30 | if (!this.focused) { 31 | this.focused = true; 32 | this.forceUpdate(); 33 | } 34 | 35 | this.props.handleEvent("focus", this.props.name, event.target.value, event); 36 | } 37 | 38 | getInputElement(attributes) { 39 | const { 40 | handleEvent, 41 | inlineIconClass, 42 | inlineTextClass, 43 | inputClass, 44 | renderer, 45 | sharedClass, 46 | writeType 47 | } = this.props; 48 | 49 | const classes = classNames(inputClass, sharedClass); 50 | let inputContent = null; 51 | attributes = this.bindEvents(attributes, handleEvent); 52 | attributes.onBlur = this.handleOnBlur; 53 | attributes.onFocus = this.handleOnFocus; 54 | 55 | let fieldValue = 56 | attributes.defaultPasswordValue || attributes.startValue || ""; 57 | 58 | if (this.focused) { 59 | fieldValue = attributes.startValue; 60 | } 61 | 62 | if (this.isEditing() || writeType === "input") { 63 | inputContent = ( 64 | 71 | ); 72 | } else { 73 | inputContent = ( 74 | 80 | 81 | {attributes.defaultPasswordValue} 82 | 83 | 84 | 85 | 86 | 87 | ); 88 | } 89 | 90 | if (renderer) { 91 | return renderer(inputContent); 92 | } 93 | 94 | return inputContent; 95 | } 96 | } 97 | 98 | module.exports = FieldPassword; 99 | -------------------------------------------------------------------------------- /src/Form/FieldRadioButton.js: -------------------------------------------------------------------------------- 1 | import classNames from "classnames/dedupe"; 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | 5 | import BindMixin from "../Mixin/BindMixin"; 6 | import Util from "../Util/Util"; 7 | 8 | class FieldRadioButton extends Util.mixin(BindMixin) { 9 | get methodsToBind() { 10 | return ["handleChange"]; 11 | } 12 | 13 | shouldComponentUpdate(nextProps) { 14 | return !Util.isEqual(this.props, nextProps); 15 | } 16 | 17 | hasError() { 18 | const { props } = this; 19 | const validationError = props.validationError; 20 | 21 | return !!(validationError && validationError[props.name]); 22 | } 23 | 24 | getErrorMsg() { 25 | const { helpBlockClass, name, validationError } = this.props; 26 | 27 | if (!this.hasError()) { 28 | return null; 29 | } 30 | 31 | return ( 32 |

{validationError[name]}

33 | ); 34 | } 35 | 36 | handleChange(eventName, name, event) { 37 | const { props } = this; 38 | 39 | if (eventName === "multipleChange") { 40 | const model = props.startValue.reduce( 41 | function(changedItems, item) { 42 | if (item.checked && item.name !== name) { 43 | item.checked = false; 44 | changedItems.push(item); 45 | } 46 | 47 | return changedItems; 48 | }, 49 | [{ name, checked: event.target.checked }] 50 | ); 51 | 52 | props.handleEvent(eventName, props.name, model, event); 53 | } 54 | 55 | if (eventName === "change") { 56 | props.handleEvent(eventName, props.name, event.target.checked, event); 57 | } 58 | } 59 | 60 | getLabel() { 61 | const { showLabel, name } = this.props; 62 | let label = name; 63 | 64 | if (!showLabel) { 65 | return null; 66 | } 67 | 68 | if (typeof showLabel === "string") { 69 | label = showLabel; 70 | } 71 | 72 | if (typeof showLabel !== "string" && showLabel !== true) { 73 | return showLabel; 74 | } 75 | 76 | return

{label}

; 77 | } 78 | 79 | getItemLabel(attributes) { 80 | if (!attributes.label) { 81 | return null; 82 | } 83 | 84 | const radioButtonLabelClass = classNames( 85 | "form-element-radio-button-label", 86 | attributes.radioButtonLabelClass, 87 | this.props.radioButtonLabelClass 88 | ); 89 | 90 | return {attributes.label}; 91 | } 92 | 93 | getItem(eventName, labelClass, attributes, index) { 94 | const labelClasses = classNames( 95 | "form-control-toggle form-control-toggle-custom", 96 | labelClass, 97 | { mute: attributes.disabled }, 98 | attributes.labelClass 99 | ); 100 | 101 | const indicatorClasses = classNames( 102 | "form-control-toggle-indicator", 103 | attributes.indicatorClass 104 | ); 105 | 106 | const htmlAttributes = Util.exclude( 107 | attributes, 108 | Object.keys(FieldRadioButton.propTypes) 109 | ); 110 | 111 | return ( 112 | 121 | ); 122 | } 123 | 124 | getItems() { 125 | const { labelClass, startValue } = this.props; 126 | 127 | if (!Util.isArray(startValue)) { 128 | // Fetch other attributes from props 129 | const value = {}; 130 | if (startValue != null) { 131 | value.checked = startValue; 132 | } 133 | const model = Util.extend({}, this.props, value); 134 | 135 | return this.getItem("change", labelClass, model, 0); 136 | } 137 | 138 | return startValue.map( 139 | this.getItem.bind(this, "multipleChange", labelClass) 140 | ); 141 | } 142 | 143 | getRowClass() { 144 | const { columnWidth, formElementClass } = this.props; 145 | 146 | return classNames( 147 | `form-row-element column-${columnWidth}`, 148 | formElementClass 149 | ); 150 | } 151 | 152 | render() { 153 | const { 154 | formGroupClass, 155 | formGroupErrorClass, 156 | itemWrapperClass 157 | } = this.props; 158 | 159 | const classes = classNames( 160 | { [formGroupErrorClass]: this.hasError() }, 161 | formGroupClass 162 | ); 163 | 164 | return ( 165 |
166 |
167 | {this.getLabel()} 168 |
{this.getItems()}
169 | {this.getErrorMsg()} 170 |
171 |
172 | ); 173 | } 174 | } 175 | 176 | FieldRadioButton.defaultProps = { 177 | columnWidth: 12, 178 | handleEvent() {} 179 | }; 180 | 181 | const classPropType = PropTypes.oneOfType([ 182 | PropTypes.array, 183 | PropTypes.object, 184 | PropTypes.string 185 | ]); 186 | 187 | FieldRadioButton.propTypes = { 188 | // Optional number of columns to take up of the grid 189 | columnWidth: PropTypes.number.isRequired, 190 | // Function to handle change event 191 | // (usually passed down from form definition) 192 | handleEvent: PropTypes.func, 193 | // Optional boolean, string, or react node. 194 | // If boolean: true - shows name as label; false - shows nothing. 195 | // If string: shows string as label. 196 | // If node: returns the node as the label. 197 | showLabel: PropTypes.oneOfType([ 198 | PropTypes.node, 199 | PropTypes.string, 200 | PropTypes.bool 201 | ]), 202 | // Attributes to pass to radio button(s) 203 | // (usually passed down from form definition) 204 | startValue: PropTypes.oneOfType([PropTypes.array, PropTypes.bool]), 205 | // Optional object of error messages, with key equal to field property name 206 | validationError: PropTypes.object, 207 | 208 | // Classes 209 | formElementClass: classPropType, 210 | formGroupClass: classPropType, 211 | // Class to be toggled, can be overridden by formGroupClass 212 | formGroupErrorClass: PropTypes.string, 213 | helpBlockClass: classPropType, 214 | itemWrapperClass: classPropType, 215 | labelClass: classPropType, 216 | radioButtonLabelClass: classPropType 217 | }; 218 | 219 | module.exports = FieldRadioButton; 220 | -------------------------------------------------------------------------------- /src/Form/FieldSelect.js: -------------------------------------------------------------------------------- 1 | import classNames from "classnames/dedupe"; 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | 5 | import Dropdown from "../Dropdown/Dropdown"; 6 | import Util from "../Util/Util"; 7 | 8 | class FieldSelect extends React.Component { 9 | shouldComponentUpdate(nextProps) { 10 | return !Util.isEqual(this.props, nextProps); 11 | } 12 | 13 | handleChange(selectedValue, event) { 14 | this.props.handleEvent( 15 | "change", 16 | this.props.name, 17 | selectedValue.id, 18 | event, 19 | selectedValue 20 | ); 21 | } 22 | 23 | hasError() { 24 | const { props } = this; 25 | const validationError = props.validationError; 26 | 27 | return !!(validationError && validationError[props.name]); 28 | } 29 | 30 | getDropDown(dropdownItems) { 31 | let { startValue, persistentID } = this.props; 32 | 33 | let classes = { 34 | buttonClassName: "button dropdown-toggle", 35 | dropdownMenuClassName: "dropdown-menu", 36 | dropdownMenuListClassName: "dropdown-menu-list", 37 | wrapperClassName: "dropdown" 38 | }; 39 | 40 | const propagatedProps = Util.exclude(this.props, [ 41 | ...Object.keys(classes), 42 | "persistentID", 43 | "startValue" 44 | ]); 45 | 46 | classes = Object.keys(classes).reduce((classSet, className) => { 47 | classSet[className] = classNames( 48 | classes[className], 49 | this.props[className] 50 | ); 51 | 52 | return classSet; 53 | }, classes); 54 | 55 | if (startValue == null) { 56 | startValue = dropdownItems[0]; 57 | } 58 | 59 | if (typeof startValue === "string") { 60 | // Try to find id in dropdown items 61 | const existingOption = dropdownItems.reduce(function(memo, option) { 62 | if (typeof option === "object") { 63 | if (option.id === startValue) { 64 | memo = option; 65 | } 66 | } 67 | 68 | return memo; 69 | }, null); 70 | 71 | if (existingOption) { 72 | startValue = existingOption; 73 | } else { 74 | startValue = { 75 | html: startValue, 76 | id: startValue 77 | }; 78 | } 79 | } 80 | 81 | return ( 82 | 91 | ); 92 | } 93 | 94 | getErrorMsg() { 95 | let errorMsg = null; 96 | const { props } = this; 97 | 98 | if (this.hasError()) { 99 | errorMsg = ( 100 |

101 | {props.validationError[props.name]} 102 |

103 | ); 104 | } 105 | 106 | return errorMsg; 107 | } 108 | 109 | getLabel() { 110 | let { label, showLabel } = this.props; 111 | 112 | if (!showLabel) { 113 | return null; 114 | } 115 | 116 | if (typeof showLabel === "string") { 117 | label = showLabel; 118 | } 119 | 120 | if (typeof showLabel !== "string" && showLabel !== true) { 121 | return showLabel; 122 | } 123 | 124 | return ; 125 | } 126 | 127 | render() { 128 | let { 129 | columnWidth, 130 | formGroupClass, 131 | formElementClass, 132 | formGroupErrorClass, 133 | options 134 | } = this.props; 135 | 136 | options = options.map(function(option) { 137 | if (typeof option === "string") { 138 | return { 139 | html: option, 140 | id: option 141 | }; 142 | } 143 | 144 | return option; 145 | }); 146 | 147 | const classes = classNames( 148 | { [formGroupErrorClass]: this.hasError() }, 149 | formGroupClass 150 | ); 151 | 152 | const rowClass = classNames( 153 | "form-row-element", 154 | `column-${columnWidth}`, 155 | formElementClass 156 | ); 157 | 158 | return ( 159 |
160 |
161 | {this.getLabel()} 162 | {this.getDropDown(options)} 163 | {this.getErrorMsg()} 164 |
165 |
166 | ); 167 | } 168 | } 169 | 170 | FieldSelect.defaultProps = { 171 | columnWidth: 12, 172 | handleEvent() {} 173 | }; 174 | 175 | const classPropType = PropTypes.oneOfType([ 176 | PropTypes.array, 177 | PropTypes.object, 178 | PropTypes.string 179 | ]); 180 | 181 | FieldSelect.propTypes = { 182 | // Optional number of columns to take up of the grid 183 | columnWidth: PropTypes.number.isRequired, 184 | 185 | // Name of the field property 186 | // (usually passed down from form definition) 187 | name: PropTypes.string.isRequired, 188 | // Optional boolean, string, or react node. 189 | // If boolean: true - shows name as label; false - shows nothing. 190 | // If string: shows string as label. 191 | // If node: returns the node as the label. 192 | showLabel: PropTypes.oneOfType([ 193 | PropTypes.node, 194 | PropTypes.string, 195 | PropTypes.bool 196 | ]), 197 | 198 | // Function to handle events like 'change', 'blur', 'focus', etc on the field 199 | // (usually passed down from form definition) 200 | handleEvent: PropTypes.func, 201 | 202 | // These are the options for the DropDown Component 203 | options: PropTypes.arrayOf( 204 | PropTypes.oneOfType([ 205 | PropTypes.string, 206 | PropTypes.shape({ 207 | html: PropTypes.node, 208 | id: PropTypes.string 209 | }) 210 | ]) 211 | ).isRequired, 212 | startValue: PropTypes.oneOfType([ 213 | PropTypes.string, 214 | PropTypes.shape({ 215 | html: PropTypes.node, 216 | id: PropTypes.string 217 | }) 218 | ]), 219 | persisitentID: PropTypes.string, 220 | 221 | // Optional object of error messages, with key equal to field property name 222 | validationError: PropTypes.object, 223 | 224 | // Classes 225 | formGroupClass: classPropType, 226 | // Class to be toggled, can be overridden by formGroupClass 227 | formGroupErrorClass: PropTypes.string, 228 | helpBlockClass: classPropType, 229 | formElementClass: classPropType, 230 | 231 | // Classes for the Dropdown 232 | buttonClassName: classPropType, 233 | dropdownMenuClassName: classPropType, 234 | dropdownMenuListClassName: classPropType, 235 | wrapperClassName: classPropType 236 | }; 237 | 238 | module.exports = FieldSelect; 239 | -------------------------------------------------------------------------------- /src/Form/FieldSubmit.js: -------------------------------------------------------------------------------- 1 | import classNames from "classnames/dedupe"; 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | 5 | import Util from "../Util/Util"; 6 | 7 | class FieldSubmit extends React.Component { 8 | shouldComponentUpdate(nextProps) { 9 | return !Util.isEqual(this.props, nextProps); 10 | } 11 | 12 | render() { 13 | const { 14 | buttonClass, 15 | columnWidth, 16 | buttonText, 17 | formGroupClass, 18 | handleSubmit, 19 | formElementClass 20 | } = this.props; 21 | 22 | const rowClass = classNames( 23 | `form-row-element column-${columnWidth}`, 24 | formElementClass 25 | ); 26 | 27 | return ( 28 |
29 |
30 |
31 | {buttonText} 32 |
33 |
34 |
35 | ); 36 | } 37 | } 38 | 39 | FieldSubmit.defaultProps = { 40 | buttonText: "Submit", 41 | handleSubmit() {} 42 | }; 43 | 44 | const classPropType = PropTypes.oneOfType([ 45 | PropTypes.array, 46 | PropTypes.object, 47 | PropTypes.string 48 | ]); 49 | 50 | FieldSubmit.propTypes = { 51 | // Optional number of columns to take up of the grid 52 | columnWidth: PropTypes.number.isRequired, 53 | 54 | // Function to handle when form is submitted 55 | // (usually passed down from form definition) 56 | handleSubmit: PropTypes.func, 57 | 58 | // Text inside button 59 | buttonText: PropTypes.string, 60 | 61 | // Classes 62 | buttonClass: classPropType, 63 | formGroupClass: classPropType, 64 | formElementClass: classPropType 65 | }; 66 | 67 | module.exports = FieldSubmit; 68 | -------------------------------------------------------------------------------- /src/Form/FieldTextarea.js: -------------------------------------------------------------------------------- 1 | import classNames from "classnames/dedupe"; 2 | /* eslint-disable no-unused-vars */ 3 | import React from "react"; 4 | import PropTypes from "prop-types"; 5 | /* eslint-enable no-unused-vars */ 6 | import throttle from "lodash.throttle"; 7 | 8 | import FieldInput from "./FieldInput"; 9 | import IconEdit from "./icons/IconEdit"; 10 | import Util from "../Util/Util"; 11 | 12 | class FieldTextarea extends FieldInput { 13 | get methodsToBind() { 14 | return [ 15 | "handleContentEditableBlur", 16 | "handleContentEditableChange", 17 | "handleContentEditableFocus" 18 | ]; 19 | } 20 | 21 | constructor() { 22 | super(...arguments); 23 | 24 | this.state = { height: this.props.minHeight }; 25 | this.updateTextareaHeight = throttle(this.updateTextareaHeight, 100); 26 | 27 | this.inputElementRef = React.createRef(); 28 | } 29 | 30 | componentDidMount() { 31 | super.componentDidMount(...arguments); 32 | 33 | if (!this.inputElementRef && !this.inputElementRef.current) { 34 | return; 35 | } 36 | 37 | if (this.isEditing() || this.props.writeType === "input") { 38 | this.updateTextareaHeight(this.inputElementRef.current); 39 | 40 | // React throws a warning if children are specified in an element with 41 | // contenteditable="true", so this hack allows us to set a default value 42 | // for this form field. 43 | if (this.props.startValue) { 44 | this.inputElementRef.current.textContent = this.props.startValue; 45 | } 46 | } 47 | } 48 | 49 | shouldComponentUpdate(nextProps, nextState) { 50 | return ( 51 | !Util.isEqual(this.props, nextProps) || 52 | !Util.isEqual(this.state, nextState) 53 | ); 54 | } 55 | 56 | handleContentEditableBlur(event) { 57 | const { props } = this; 58 | props.handleEvent("blur", props.name, event.target.innerText, event); 59 | } 60 | 61 | handleContentEditableChange(event) { 62 | const { props } = this; 63 | 64 | this.updateTextareaHeight(event.target); 65 | props.handleEvent("change", props.name, event.target.innerText, event); 66 | } 67 | 68 | handleContentEditableFocus(event) { 69 | const { props } = this; 70 | props.handleEvent("focus", props.name, event.target.innerText, event); 71 | } 72 | 73 | getInputElement(attributes) { 74 | const { 75 | inlineIconClass, 76 | inlineTextClass, 77 | inputClass, 78 | minHeight, 79 | renderer, 80 | sharedClass, 81 | value, 82 | writeType 83 | } = this.props; 84 | let inputContent = null; 85 | 86 | const classes = classNames("content-editable", inputClass, sharedClass); 87 | 88 | attributes = this.bindEvents(attributes); 89 | 90 | if (this.isEditing() || writeType === "input") { 91 | inputContent = ( 92 |
96 |
106 |
107 | ); 108 | } else { 109 | inputContent = ( 110 | 116 | 117 | {value || attributes.startValue} 118 | 119 | 120 | 121 | 122 | 123 | ); 124 | } 125 | 126 | if (renderer) { 127 | return renderer(inputContent); 128 | } 129 | 130 | return inputContent; 131 | } 132 | 133 | updateTextareaHeight(domElement) { 134 | const { minHeight, maxHeight, scrollHeightOffset } = this.props; 135 | let newHeight = minHeight; 136 | const { scrollHeight } = domElement; 137 | 138 | if (scrollHeight > minHeight && scrollHeight < maxHeight) { 139 | newHeight = scrollHeight + scrollHeightOffset; 140 | } else if (scrollHeight >= maxHeight) { 141 | newHeight = maxHeight; 142 | } 143 | 144 | if (newHeight !== this.state.height) { 145 | this.setState({ height: newHeight }); 146 | } 147 | } 148 | } 149 | 150 | FieldTextarea.defaultProps = { 151 | maxHeight: 400, 152 | minHeight: 100, 153 | scrollHeightOffset: 2 154 | }; 155 | 156 | FieldTextarea.propTypes = { 157 | maxHeight: PropTypes.number, 158 | minHeight: PropTypes.number, 159 | scrollHeightOffset: PropTypes.number 160 | }; 161 | 162 | module.exports = FieldTextarea; 163 | -------------------------------------------------------------------------------- /src/Form/FieldTypes.js: -------------------------------------------------------------------------------- 1 | import FieldCheckbox from "./FieldCheckbox"; 2 | import FieldInput from "./FieldInput"; 3 | import FieldPassword from "./FieldPassword"; 4 | import FieldRadioButton from "./FieldRadioButton"; 5 | import FieldSelect from "./FieldSelect"; 6 | import FieldSubmit from "./FieldSubmit"; 7 | import FieldTextarea from "./FieldTextarea"; 8 | 9 | const FieldTypes = { 10 | checkbox: FieldCheckbox, 11 | number: FieldInput, 12 | password: FieldPassword, 13 | radioButton: FieldRadioButton, 14 | submit: FieldSubmit, 15 | text: FieldInput, 16 | textarea: FieldTextarea, 17 | select: FieldSelect 18 | }; 19 | 20 | module.exports = FieldTypes; 21 | -------------------------------------------------------------------------------- /src/Form/FormControl.js: -------------------------------------------------------------------------------- 1 | import classNames from "classnames/dedupe"; 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | 5 | import FieldTypes from "./FieldTypes"; 6 | import Util from "../Util/Util"; 7 | 8 | class FormControl extends React.Component { 9 | renderGroup(definition) { 10 | const columnLength = definition.filter(function(fieldDefinition) { 11 | return !React.isValidElement(fieldDefinition); 12 | }).length; 13 | 14 | return definition.map((inputOptions, i) => { 15 | return this.renderDefinition( 16 | inputOptions, 17 | columnLength, 18 | i === definition.length - 1 19 | ); 20 | }); 21 | } 22 | 23 | renderType(definition, columnLength = 1, isLast) { 24 | const fieldTypeName = definition.fieldType; 25 | const FieldTypeComponent = FieldTypes[fieldTypeName]; 26 | const props = this.props; 27 | let columnWidth; 28 | 29 | if (definition.columnWidth == null) { 30 | const maxColumnWidth = props.maxColumnWidth; 31 | columnWidth = Math.floor(maxColumnWidth / columnLength); 32 | if (isLast) { 33 | columnWidth += maxColumnWidth % columnLength; 34 | } 35 | } else { 36 | columnWidth = definition.columnWidth; 37 | } 38 | 39 | let key = definition.key; 40 | // Fallback to using name, if no key provided 41 | if (key == null) { 42 | key = definition.name; 43 | } 44 | 45 | return ( 46 | 54 | ); 55 | } 56 | 57 | renderDefinition(definition, columnLength, isLast) { 58 | if (Util.isArray(definition)) { 59 | return this.renderGroup(definition); 60 | } 61 | 62 | if (React.isValidElement(definition)) { 63 | return definition; 64 | } 65 | 66 | return this.renderType(definition, columnLength, isLast); 67 | } 68 | 69 | render() { 70 | const props = this.props; 71 | let content = this.renderDefinition(props.definition); 72 | 73 | if (Util.isArray(content)) { 74 | content = Util.flatten(content); 75 | } 76 | 77 | return
{content}
; 78 | } 79 | } 80 | 81 | FormControl.propTypes = { 82 | // Optional number of columns in the grid 83 | maxColumnWidth: PropTypes.number, 84 | 85 | // Object with key as field propterty name, and value as current value 86 | currentValue: PropTypes.object, 87 | 88 | // Form definition to build the form from. Can be either: 89 | // 1. Array of field definitions will be created on same row 90 | // 2. Field definition (object) will create a single field in that row 91 | definition: PropTypes.oneOfType([PropTypes.object, PropTypes.array]) 92 | }; 93 | 94 | module.exports = FormControl; 95 | -------------------------------------------------------------------------------- /src/Form/__tests__/FieldRadioButton-test.js: -------------------------------------------------------------------------------- 1 | jest.dontMock("../FieldRadioButton"); 2 | 3 | /* eslint-disable no-unused-vars */ 4 | var React = require("react"); 5 | /* eslint-enable no-unused-vars */ 6 | 7 | var TestUtils; 8 | if (React.version.match(/15.[0-5]/)) { 9 | TestUtils = require("react-addons-test-utils"); 10 | } else { 11 | TestUtils = require("react-dom/test-utils"); 12 | } 13 | 14 | var FieldRadioButton = require("../FieldRadioButton"); 15 | 16 | describe("FieldRadioButton", function() { 17 | describe("#hasError", function() { 18 | it("should return true when error contains name", function() { 19 | var instance = TestUtils.renderIntoDocument( 20 | 25 | ); 26 | 27 | expect(instance.hasError()).toEqual(true); 28 | }); 29 | 30 | it("should return false when error doesn't contains name", function() { 31 | var instance = TestUtils.renderIntoDocument( 32 | 37 | ); 38 | 39 | expect(instance.hasError()).toEqual(false); 40 | }); 41 | 42 | it("should return false when error is undefined", function() { 43 | var instance = TestUtils.renderIntoDocument( 44 | 45 | ); 46 | 47 | expect(instance.hasError()).toEqual(false); 48 | }); 49 | }); 50 | 51 | describe("#getErrorMsg", function() { 52 | it("should return a label if validationError is true", function() { 53 | var instance = TestUtils.renderIntoDocument( 54 | 59 | ); 60 | 61 | expect(instance.getErrorMsg().type).toEqual("p"); 62 | }); 63 | 64 | it("should return null if validationError is false", function() { 65 | var instance = TestUtils.renderIntoDocument( 66 | 67 | ); 68 | 69 | expect(instance.getErrorMsg()).toEqual(null); 70 | }); 71 | }); 72 | 73 | describe("#getLabel", function() { 74 | it("should return a paragraph if showLabel has a value", function() { 75 | var instance = TestUtils.renderIntoDocument( 76 | 77 | ); 78 | 79 | expect(instance.getLabel().type).toEqual("p"); 80 | }); 81 | 82 | it("can handle a custom render function", function() { 83 | var instance = TestUtils.renderIntoDocument( 84 | hello} 87 | startValue={[]} 88 | /> 89 | ); 90 | 91 | expect(instance.getLabel().type).toEqual("h1"); 92 | }); 93 | 94 | it("should return null if showLabel doesn't has a value", function() { 95 | var instance = TestUtils.renderIntoDocument( 96 | 97 | ); 98 | 99 | expect(instance.getLabel()).toEqual(null); 100 | }); 101 | }); 102 | 103 | describe("#getItems", function() { 104 | beforeEach(function() { 105 | this.handleEventSpy = jasmine.createSpy("handleEvent"); 106 | this.instance = TestUtils.renderIntoDocument( 107 | 117 | ); 118 | this.children = TestUtils.scryRenderedDOMComponentsWithTag( 119 | this.instance, 120 | "label" 121 | ); 122 | 123 | this.inputChildren = TestUtils.scryRenderedDOMComponentsWithTag( 124 | this.instance, 125 | "input" 126 | ); 127 | }); 128 | 129 | it("should return an empty array if startValue is empty", function() { 130 | var instance = TestUtils.renderIntoDocument( 131 | 132 | ); 133 | 134 | expect(instance.getItems()).toEqual([]); 135 | }); 136 | 137 | it("should display items pass in from startValue", function() { 138 | expect(this.children.length).toEqual(3); 139 | }); 140 | 141 | it("should display parent labelClass on all items", function() { 142 | for (var i = 0; i < this.children.length; i++) { 143 | expect(this.children[i].className).toContain("foo"); 144 | } 145 | }); 146 | 147 | it("should override parent labelClass with item labelClass", function() { 148 | var instance = TestUtils.renderIntoDocument( 149 | 158 | ); 159 | var children = TestUtils.scryRenderedDOMComponentsWithTag( 160 | instance, 161 | "label" 162 | ); 163 | 164 | expect(children[1].className).toContain("bar"); 165 | }); 166 | 167 | it("should call handler with 'multipleChange' for items", function() { 168 | TestUtils.Simulate.change(this.inputChildren[0], { 169 | target: { checked: true } 170 | }); 171 | var args = this.handleEventSpy.calls.mostRecent().args; 172 | expect(args[0]).toEqual("multipleChange"); 173 | expect(args[1]).toEqual("foo"); 174 | expect(args[2]).toEqual([ 175 | { name: "foo", checked: true }, 176 | { name: "bar", checked: false } 177 | ]); 178 | }); 179 | 180 | it("should call handleChange with 'change' on single item", function() { 181 | var handleEventSpy = jasmine.createSpy("handleEvent"); 182 | var instance = TestUtils.renderIntoDocument( 183 | 189 | ); 190 | var input = TestUtils.findRenderedDOMComponentWithTag(instance, "input"); 191 | TestUtils.Simulate.change(input, { target: { checked: true } }); 192 | var args = handleEventSpy.calls.mostRecent().args; 193 | expect(args[0]).toEqual("change"); 194 | expect(args[1]).toEqual("foo"); 195 | expect(args[2]).toEqual(true); 196 | }); 197 | 198 | it("should display the checked of each item", function() { 199 | expect(this.inputChildren[0].checked).toEqual(false); 200 | expect(this.inputChildren[1].checked).toEqual(true); 201 | expect(this.inputChildren[2].checked).toEqual(false); 202 | }); 203 | 204 | it("should change value of other checked items to false", function() { 205 | this.instance.handleChange("multipleChange", "quis", { 206 | target: { checked: true } 207 | }); 208 | expect(this.handleEventSpy).toHaveBeenCalledWith( 209 | // Event name 210 | "multipleChange", 211 | // Field name 212 | "foo", 213 | // Changed radio buttons 214 | [{ name: "quis", checked: true }, { name: "bar", checked: false }], 215 | // Event fired 216 | { target: { checked: true } } 217 | ); 218 | }); 219 | }); 220 | }); 221 | -------------------------------------------------------------------------------- /src/Form/__tests__/FieldSelect-test.js: -------------------------------------------------------------------------------- 1 | jest.dontMock("../FieldSelect"); 2 | jest.dontMock("../icons/IconEdit"); 3 | jest.dontMock("../../Util/KeyboardUtil"); 4 | 5 | /* eslint-disable no-unused-vars */ 6 | var React = require("react"); 7 | /* eslint-enable no-unused-vars */ 8 | 9 | var TestUtils; 10 | if (React.version.match(/15.[0-5]/)) { 11 | TestUtils = require("react-addons-test-utils"); 12 | } else { 13 | TestUtils = require("react-dom/test-utils"); 14 | } 15 | 16 | var FieldSelect = require("../FieldSelect"); 17 | 18 | describe("FieldSelect", function() { 19 | describe("#hasError", function() { 20 | it("should return true when error contains name", function() { 21 | var instance = (instance = TestUtils.renderIntoDocument( 22 | 28 | )); 29 | 30 | expect(instance.hasError()).toEqual(true); 31 | }); 32 | 33 | it("should return false when error doesn't contains name", function() { 34 | var instance = (instance = TestUtils.renderIntoDocument( 35 | 41 | )); 42 | 43 | expect(instance.hasError()).toEqual(false); 44 | }); 45 | 46 | it("should return false when error is undefined", function() { 47 | var instance = (instance = TestUtils.renderIntoDocument( 48 | 53 | )); 54 | 55 | expect(instance.hasError()).toEqual(false); 56 | }); 57 | }); 58 | 59 | describe("#getLabel", function() { 60 | it("should return a label if showLabel is true", function() { 61 | var instance = TestUtils.renderIntoDocument( 62 | 68 | ); 69 | 70 | expect(instance.getLabel().type).toEqual("label"); 71 | }); 72 | 73 | it("can handle a custom render function", function() { 74 | var instance = TestUtils.renderIntoDocument( 75 | hello} 80 | /> 81 | ); 82 | 83 | expect(instance.getLabel().type).toEqual("h1"); 84 | }); 85 | 86 | it("should return null if showLabel is false", function() { 87 | var instance = TestUtils.renderIntoDocument( 88 | 94 | ); 95 | 96 | expect(instance.getLabel()).toEqual(null); 97 | }); 98 | }); 99 | 100 | describe("#getErrorMsg", function() { 101 | it("should return a label if validationError is true", function() { 102 | var instance = TestUtils.renderIntoDocument( 103 | 109 | ); 110 | 111 | expect(instance.getErrorMsg().type).toEqual("p"); 112 | }); 113 | 114 | it("should return null if validationError is false", function() { 115 | var instance = TestUtils.renderIntoDocument( 116 | 121 | ); 122 | 123 | expect(instance.getErrorMsg()).toEqual(null); 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /src/Form/__tests__/Form-test.js: -------------------------------------------------------------------------------- 1 | jest.dontMock("../Form"); 2 | jest.dontMock("../FormControl"); 3 | jest.dontMock("../FieldInput"); 4 | jest.dontMock("../FieldPassword"); 5 | jest.dontMock("../icons/IconEdit"); 6 | jest.dontMock("../FieldTypes"); 7 | jest.dontMock("../../Mixin/BindMixin"); 8 | jest.dontMock("../../Util/Util"); 9 | 10 | /* eslint-disable no-unused-vars */ 11 | var React = require("react"); 12 | /* eslint-enable no-unused-vars */ 13 | 14 | var TestUtils; 15 | if (React.version.match(/15.[0-5]/)) { 16 | TestUtils = require("react-addons-test-utils"); 17 | } else { 18 | TestUtils = require("react-dom/test-utils"); 19 | } 20 | 21 | var Form = require("../Form"); 22 | var FormControl = require("../FormControl"); 23 | 24 | function getInstance(definition, triggerSubmit, onSubmit) { 25 | return TestUtils.renderIntoDocument( 26 |
31 | ); 32 | } 33 | 34 | function getDefinition() { 35 | return [ 36 | { 37 | name: "username", 38 | value: "kennyt", 39 | validation: function(arg) { 40 | return arg.length < 15; 41 | }, 42 | placeholder: "Write text here.", 43 | fieldType: "text", 44 | validationErrorText: "Must be less than 15 characters.", 45 | required: true, 46 | showLabel: true, 47 | showError: "This is an error.", 48 | writeType: "edit" 49 | } 50 | ]; 51 | } 52 | 53 | function noop() {} 54 | 55 | describe("Form", function() { 56 | describe("#componentWillReceiveProps", function() { 57 | beforeEach(function() { 58 | this.instance = getInstance(getDefinition(), noop, noop); 59 | }); 60 | 61 | it("should update model if definition value is different", function() { 62 | this.instance.UNSAFE_componentWillReceiveProps({ 63 | definition: [ 64 | { name: "username", value: "kenny", showError: "This is an error." } 65 | ] 66 | }); 67 | 68 | expect(this.instance.state.model.username).toEqual("kenny"); 69 | }); 70 | 71 | it("should update model if definition error is different", function() { 72 | this.instance.UNSAFE_componentWillReceiveProps({ 73 | definition: [ 74 | { name: "username", value: "kennyt", showError: "different error" } 75 | ] 76 | }); 77 | 78 | expect(this.instance.state.erroredFields.username).toEqual( 79 | "different error" 80 | ); 81 | }); 82 | 83 | it("should delete field from model if no longer part of definition", function() { 84 | this.instance.UNSAFE_componentWillReceiveProps({ 85 | definition: [{ name: "password", value: "secretpassword" }] 86 | }); 87 | 88 | expect(this.instance.state.model.username).toEqual(undefined); 89 | }); 90 | }); 91 | 92 | describe("#triggerSubmit", function() { 93 | it("should call the triggerSubmit function on mount", function() { 94 | var triggerSubmit = jasmine.createSpy(); 95 | var instance = getInstance(getDefinition(), triggerSubmit, noop); 96 | 97 | expect(triggerSubmit).toHaveBeenCalled(); 98 | expect(triggerSubmit).toHaveBeenCalledWith(instance.handleSubmit); 99 | }); 100 | 101 | it("should expose a function to submit form", function() { 102 | var triggerSubmit; 103 | var onSubmit = jasmine.createSpy(); 104 | var instance = getInstance( 105 | getDefinition(), 106 | function(submit) { 107 | triggerSubmit = submit; 108 | }, 109 | onSubmit 110 | ); 111 | 112 | instance.validateSubmit = function() { 113 | return true; 114 | }; 115 | triggerSubmit(); 116 | 117 | expect(onSubmit).toHaveBeenCalled(); 118 | }); 119 | }); 120 | 121 | describe("#validateValue", function() { 122 | beforeEach(function() { 123 | this.instance = getInstance(getDefinition(), noop, noop); 124 | this.validationFn = function(arg) { 125 | return arg.length < 5; 126 | }; 127 | 128 | // This regex makes sure that the string has the words 'cat' and 'dog' 129 | this.validationRegex = /(?:.*(?:\b(?:cat|dog)\b)){2}/; 130 | this.fieldDefinition = [ 131 | { 132 | name: "username", 133 | validation: this.validationFn 134 | } 135 | ]; 136 | }); 137 | 138 | it("should validate with a function", function() { 139 | var value = "less"; 140 | var result = this.instance.validateValue( 141 | "username", 142 | value, 143 | this.fieldDefinition 144 | ); 145 | 146 | expect(result).toEqual(true); 147 | }); 148 | 149 | it("should return false if function validation fails", function() { 150 | var value = "a string that is too big"; 151 | var result = this.instance.validateValue( 152 | "username", 153 | value, 154 | this.fieldDefinition 155 | ); 156 | 157 | expect(result).toEqual(false); 158 | }); 159 | 160 | it("should validation with a regex pattern", function() { 161 | this.fieldDefinition[0].validation = this.validationRegex; 162 | var value = "cat and dog"; 163 | var result = this.instance.validateValue( 164 | "username", 165 | value, 166 | this.fieldDefinition 167 | ); 168 | 169 | expect(result).toEqual(true); 170 | }); 171 | 172 | it("should validation with a regex pattern", function() { 173 | this.fieldDefinition[0].validation = this.validationRegex; 174 | var value = "cat and lizard"; 175 | var result = this.instance.validateValue( 176 | "username", 177 | value, 178 | this.fieldDefinition 179 | ); 180 | 181 | expect(result).toEqual(false); 182 | }); 183 | }); 184 | 185 | describe("#getFormControls", function() { 186 | beforeEach(function() { 187 | this.instance = getInstance(getDefinition(), noop, noop); 188 | }); 189 | 190 | it("should return a FormControl for each item in definition", function() { 191 | var definition = [{}, {}, {}]; 192 | var result = this.instance.getFormControls(definition); 193 | 194 | expect(result.length).toEqual(definition.length); 195 | result.forEach(function(formControl) { 196 | expect(formControl.type).toEqual(FormControl); 197 | }); 198 | }); 199 | 200 | it("can handle arrays as an item", function() { 201 | var definition = [{}, [{}, {}], {}]; 202 | var result = this.instance.getFormControls(definition); 203 | 204 | expect(result.length).toEqual(definition.length); 205 | result.forEach(function(formControl) { 206 | expect(formControl.type).toEqual(FormControl); 207 | }); 208 | }); 209 | 210 | it("accepts a React element and returns it", function() { 211 | this.instance = getInstance([

foo

], noop, noop); 212 | expect(TestUtils.findRenderedDOMComponentWithTag(this.instance, "h1")); 213 | }); 214 | }); 215 | 216 | describe("custom render", function() { 217 | beforeEach(function() { 218 | var renderSpy = jasmine.createSpy(); 219 | var definition = [ 220 | { 221 | render: renderSpy 222 | } 223 | ]; 224 | 225 | this.instance = getInstance(definition, noop, noop); 226 | 227 | expect(renderSpy).toHaveBeenCalled(); 228 | }); 229 | }); 230 | }); 231 | -------------------------------------------------------------------------------- /src/Form/__tests__/FormControl-test.js: -------------------------------------------------------------------------------- 1 | jest.dontMock("../FormControl"); 2 | jest.dontMock("../FieldInput"); 3 | jest.dontMock("../icons/IconEdit"); 4 | jest.dontMock("../FieldTypes"); 5 | jest.dontMock("../../Util/Util"); 6 | 7 | /* eslint-disable no-unused-vars */ 8 | var React = require("react"); 9 | /* eslint-enable no-unused-vars */ 10 | 11 | var TestUtils; 12 | if (React.version.match(/15.[0-5]/)) { 13 | TestUtils = require("react-addons-test-utils"); 14 | } else { 15 | TestUtils = require("react-dom/test-utils"); 16 | } 17 | 18 | var FormControl = require("../FormControl"); 19 | 20 | function getDefinition() { 21 | return { 22 | name: "username", 23 | value: "string", 24 | validation: function(arg) { 25 | return arg.length < 15; 26 | }, 27 | placeholder: "What's up?", 28 | fieldType: "text", 29 | errorText: "Must be less than 8 characters", 30 | required: true, 31 | showLabel: true, 32 | writeType: "edit" 33 | }; 34 | } 35 | 36 | function getInstance() { 37 | return TestUtils.renderIntoDocument( 38 | 45 | ); 46 | } 47 | 48 | describe("FormControl", function() { 49 | describe("#renderDefinition", function() { 50 | beforeEach(function() { 51 | this.instance = getInstance(); 52 | 53 | this.prevRenderGroup = this.instance.renderGroup; 54 | this.prevRenderType = this.instance.renderType; 55 | 56 | this.instance.renderGroup = jasmine.createSpy(); 57 | this.instance.renderType = jasmine.createSpy(); 58 | }); 59 | 60 | afterEach(function() { 61 | this.instance.renderGroup = this.prevRenderGroup; 62 | this.instance.renderType = this.prevRenderType; 63 | }); 64 | 65 | it("calls #renderGroup if definition is an array", function() { 66 | this.instance.renderDefinition([]); 67 | 68 | expect(this.instance.renderGroup).toHaveBeenCalled(); 69 | }); 70 | 71 | it("calls #renderType if definition is an object", function() { 72 | this.instance.renderDefinition({}); 73 | 74 | expect(this.instance.renderType).toHaveBeenCalled(); 75 | }); 76 | }); 77 | 78 | describe("#renderGroup", function() { 79 | beforeEach(function() { 80 | this.instance = getInstance(); 81 | 82 | this.prevRenderDefinition = this.instance.renderDefinition; 83 | this.instance.renderDefinition = jest.genMockFunction(); 84 | }); 85 | 86 | afterEach(function() { 87 | this.instance.renderDefinition = this.prevRenderDefinition; 88 | }); 89 | 90 | it("calls #renderDefinition for every item in the array", function() { 91 | var items = [1, 2, 3, 4, 5]; 92 | this.instance.renderGroup(items); 93 | 94 | var calls = this.instance.renderDefinition.mock.calls; 95 | expect(calls.length).toEqual(5); 96 | 97 | calls.forEach(function(call, i) { 98 | // Expect first argument of each call to be the item. 99 | expect(call[0]).toEqual(items[i]); 100 | }); 101 | }); 102 | }); 103 | 104 | describe("#renderType", function() { 105 | beforeEach(function() { 106 | this.instance = getInstance(); 107 | }); 108 | 109 | it("calculates correct column width for last element", function() { 110 | var definition = { 111 | name: "username", 112 | fieldType: "text" 113 | }; 114 | var result = this.instance.renderType(definition, 7, true); 115 | 116 | expect(result.props.columnWidth).toEqual(7); 117 | }); 118 | 119 | it("calculates correct column width for all other", function() { 120 | var definition = { 121 | name: "username", 122 | fieldType: "text" 123 | }; 124 | var result = this.instance.renderType(definition, 7, false); 125 | 126 | expect(result.props.columnWidth).toEqual(1); 127 | }); 128 | 129 | it("uses the definitions columnWidth", function() { 130 | var definition = { 131 | name: "username", 132 | fieldType: "text", 133 | columnWidth: 3 134 | }; 135 | var result = this.instance.renderType(definition, 12, false); 136 | 137 | expect(result.props.columnWidth).toEqual(3); 138 | }); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /src/Form/icons/IconEdit.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | class IconInfo extends React.Component { 4 | render() { 5 | return ( 6 | 13 | icon-edit 14 | 15 | 16 | ); 17 | } 18 | } 19 | 20 | module.exports = IconInfo; 21 | -------------------------------------------------------------------------------- /src/Form/styles.less: -------------------------------------------------------------------------------- 1 | & when (@form-enabled) { 2 | 3 | .form { 4 | 5 | .row { 6 | 7 | &:last-child { 8 | 9 | .form-group { 10 | margin-bottom: 0; 11 | } 12 | } 13 | } 14 | } 15 | 16 | /* Form Control Group */ 17 | .form-control-group { 18 | 19 | &.filter-input-text-group { 20 | width: auto; 21 | 22 | // Hide native clear button in IE10+ 23 | ::-ms-clear { 24 | display: none; 25 | } 26 | 27 | .form-control-group-add-on-prepend { 28 | 29 | .icon { 30 | opacity: 0.2; 31 | 32 | &.active { 33 | opacity: 1; 34 | } 35 | } 36 | } 37 | 38 | .form-control-group-add-on-append { 39 | 40 | &.hidden { 41 | display: table-cell !important; 42 | opacity: 0; 43 | } 44 | 45 | a { 46 | 47 | .icon { 48 | opacity: 0.2; 49 | 50 | &:hover { 51 | opacity: 0.6; 52 | } 53 | 54 | &:active { 55 | opacity: 1; 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | /* Inline Form Elements */ 64 | .form-element-inline { 65 | appearance: none; 66 | border: none; 67 | border-bottom: 1px solid transparent; 68 | display: flex; 69 | height: auto; 70 | padding: @base-spacing-unit * 1/4 0; 71 | 72 | .form-element-inline-text { 73 | flex: 0 1 auto; 74 | min-width: @base-spacing-unit; 75 | overflow: hidden; 76 | text-overflow: ellipsis; 77 | white-space: nowrap; 78 | } 79 | 80 | .form-element-inline-icon { 81 | display: inline-block; 82 | fill: @form-control-border-color; 83 | flex: 1 0 16px; 84 | margin-left: @base-spacing-unit * 1/2; 85 | opacity: 0; 86 | transition: opacity 0.25s; 87 | vertical-align: middle; 88 | } 89 | 90 | &:hover { 91 | border: none; 92 | border-bottom: 1px solid transparent; 93 | 94 | .form-element-inline-icon { 95 | opacity: 1; 96 | } 97 | } 98 | 99 | &:focus { 100 | background: transparent; 101 | } 102 | } 103 | 104 | .form-row-edit { 105 | 106 | .form-element-inline { 107 | border-bottom: 1px solid @form-control-border-color; 108 | border-radius: 0; 109 | } 110 | } 111 | 112 | /* 'Textarea' Form Elements: A div with attribute contenteditable='true' */ 113 | .content-editable-wrapper { 114 | position: relative; 115 | } 116 | 117 | .content-editable { 118 | display: block; 119 | height: auto; 120 | left: 0; 121 | max-height: 100%; 122 | outline: none; 123 | overflow: auto; 124 | padding-bottom: 0.35rem; 125 | padding-top: 0.35rem; 126 | position: absolute; 127 | top: 0; 128 | white-space: pre-wrap; 129 | width: 100%; 130 | word-wrap: break-word; 131 | 132 | // Remove any rich-text styling that might be introduced by a user pasting 133 | // rich-text content or using rich-text keyboard shortcuts. 134 | * { 135 | background: none !important; 136 | border: none !important; 137 | color: inherit !important; 138 | font: inherit !important; 139 | margin: 0 !important; 140 | padding: 0 !important; 141 | text-decoration: inherit !important; 142 | text-shadow: inherit !important; 143 | 144 | &:before, 145 | &:after { 146 | display: none; 147 | } 148 | } 149 | } 150 | 151 | .form-group { 152 | 153 | textarea { 154 | resize: none; 155 | } 156 | } 157 | 158 | .form-control-toggle-custom { 159 | min-height: @toggle-custom-height; 160 | } 161 | } 162 | 163 | & when (@form-enabled) and (@layout-screen-small-enabled) { 164 | 165 | @media (min-width: @layout-screen-small-min-width) { 166 | /* Form Control Group */ 167 | .form-control-group { 168 | 169 | &.filter-input-text-group { 170 | width: 100px; 171 | } 172 | } 173 | } 174 | } 175 | 176 | & when (@form-enabled) and (@layout-screen-medium-enabled) { 177 | 178 | @media (min-width: @layout-screen-medium-min-width) { 179 | /* Form Control Group */ 180 | .form-control-group { 181 | 182 | &.filter-input-text-group { 183 | width: 200px; 184 | } 185 | } 186 | } 187 | } 188 | 189 | & when (@form-enabled) and (@layout-screen-large-enabled) { 190 | 191 | @media (min-width: @layout-screen-large-min-width) { 192 | 193 | .form-control-toggle-custom { 194 | min-height: @toggle-custom-height-screen-large; 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/Form/variables.less: -------------------------------------------------------------------------------- 1 | @form-control-disabled-color: @form-control-border-color; 2 | -------------------------------------------------------------------------------- /src/List/List.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import ListItem from "./ListItem"; 5 | import Util from "../Util/Util"; 6 | 7 | class List extends React.Component { 8 | getListItems(list, childIndex = 0) { 9 | const items = list.map((item, parentIndex) => { 10 | const key = `${parentIndex}.${childIndex}`; 11 | childIndex++; 12 | 13 | const htmlAttributes = Util.exclude(item, ["content"]); 14 | 15 | if (Util.isArrayLike(item.content)) { 16 | return ( 17 | 18 | {this.getListItems(item.content, childIndex)} 19 | 20 | ); 21 | } else { 22 | return ( 23 | 24 | {item.content} 25 | 26 | ); 27 | } 28 | }); 29 | 30 | return items; 31 | } 32 | 33 | render() { 34 | const { props } = this; 35 | const Tag = props.tag; 36 | 37 | // Uses all passed properties as attributes, excluding propTypes 38 | const htmlAttributes = Util.exclude(props, Object.keys(List.propTypes)); 39 | 40 | return ( 41 | 42 | {this.getListItems(props.content)} 43 | 44 | ); 45 | } 46 | } 47 | 48 | List.defaultProps = { 49 | className: "list", 50 | tag: "ul" 51 | }; 52 | 53 | List.propTypes = { 54 | className: PropTypes.string, 55 | // List of items in the list 56 | content: PropTypes.oneOfType([ 57 | PropTypes.arrayOf( 58 | // Each item in the array should be an object 59 | PropTypes.shape({ 60 | // Optionally add a class to a given item 61 | className: PropTypes.string, 62 | // An item can be a container of another ist 63 | items: PropTypes.array, 64 | // Optional tag for item instead of an `li` 65 | tag: PropTypes.string, 66 | // If this item isn't a list of other items just use a value 67 | value: PropTypes.string 68 | }) 69 | ), 70 | PropTypes.string 71 | ]).isRequired, 72 | // Optional tag for the container of the list 73 | tag: PropTypes.string 74 | }; 75 | 76 | module.exports = List; 77 | -------------------------------------------------------------------------------- /src/List/ListItem.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import Util from "../Util/Util"; 5 | 6 | class ListItem extends React.Component { 7 | render() { 8 | const { props } = this; 9 | const Tag = props.tag; 10 | 11 | // Uses all passed properties as attributes, excluding propTypes 12 | const htmlAttributes = Util.exclude(props, Object.keys(ListItem.propTypes)); 13 | 14 | return ( 15 | 16 | {props.children} 17 | 18 | ); 19 | } 20 | } 21 | 22 | ListItem.defaultProps = { 23 | className: "list-item", 24 | tag: "li" 25 | }; 26 | 27 | ListItem.propTypes = { 28 | children: PropTypes.node, 29 | className: PropTypes.string, 30 | tag: PropTypes.string 31 | }; 32 | 33 | module.exports = ListItem; 34 | -------------------------------------------------------------------------------- /src/List/__tests__/List-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | var React = require("react"); 3 | /* eslint-enable no-unused-vars */ 4 | 5 | var TestUtils; 6 | if (React.version.match(/15.[0-5]/)) { 7 | TestUtils = require("react-addons-test-utils"); 8 | } else { 9 | TestUtils = require("react-dom/test-utils"); 10 | } 11 | 12 | jest.dontMock("../List.js"); 13 | jest.dontMock("../ListItem.js"); 14 | jest.dontMock("./fixtures/MockList"); 15 | jest.dontMock("../../Util/Util"); 16 | 17 | var MockList = require("./fixtures/MockList"); 18 | var List = require("../List.js"); 19 | 20 | describe("List", function() { 21 | beforeEach(function() { 22 | this.listTag = "ul"; 23 | this.instance = TestUtils.renderIntoDocument( 24 | 25 | ); 26 | }); 27 | 28 | it("should render whatever tag it receives in its attributes", function() { 29 | var ulElements = TestUtils.scryRenderedDOMComponentsWithTag( 30 | this.instance, 31 | this.listTag 32 | ); 33 | 34 | expect(ulElements.length).toEqual(1); 35 | }); 36 | 37 | it("should render only one element with the class list", function() { 38 | var listItems = TestUtils.scryRenderedDOMComponentsWithClass( 39 | this.instance, 40 | "list" 41 | ); 42 | 43 | expect(listItems.length).toEqual(1); 44 | }); 45 | 46 | it("renders one row per top-level item in the list array", function() { 47 | var listItems = TestUtils.scryRenderedDOMComponentsWithClass( 48 | this.instance, 49 | "my-custom-row-class" 50 | ); 51 | 52 | expect(listItems.length).toEqual(MockList.length); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/List/__tests__/fixtures/MockList.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "className": "my-custom-row-class my-custom-row-class--first-row", 4 | "content": [ 5 | { 6 | "className": "my-custom-item-class", 7 | "style": { 8 | "display": "inline-block" 9 | }, 10 | "tag": "span", 11 | "content": "Row 1: Item 1" 12 | }, 13 | { 14 | "className": "my-custom-item-class", 15 | "style": { 16 | "display": "inline-block" 17 | }, 18 | "tag": "span", 19 | "content": "Row 1: Item 2" 20 | } 21 | ] 22 | }, 23 | { 24 | "className": "my-custom-row-class", 25 | "content": "Row 2: Item 1" 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /src/List/styles.less: -------------------------------------------------------------------------------- 1 | @keyframes list-fade-in-left { 2 | 3 | 0% { 4 | opacity: 0; 5 | transform: translateX(-2em); 6 | } 7 | 8 | 100% { 9 | opacity: 1; 10 | transform: translateX(0); 11 | } 12 | } 13 | 14 | @keyframes list-fade-out-right { 15 | 16 | 0% { 17 | opacity: 1; 18 | transform: translateX(0); 19 | } 20 | 21 | 100% { 22 | opacity: 0; 23 | transform: translateX(2em); 24 | } 25 | } 26 | 27 | .list-item-enter { 28 | animation: list-fade-in-left 500ms; 29 | } 30 | 31 | .list-item-exit { 32 | animation: list-fade-out-right 500ms; 33 | } 34 | -------------------------------------------------------------------------------- /src/Mixin/BindMixin.js: -------------------------------------------------------------------------------- 1 | const BindMixin = { 2 | UNSAFE_componentWillMount() { 3 | if (this.methodsToBind) { 4 | this.methodsToBind.forEach(method => { 5 | this[method] = this[method].bind(this); 6 | }); 7 | } 8 | } 9 | }; 10 | 11 | module.exports = BindMixin; 12 | -------------------------------------------------------------------------------- /src/Modal/Modal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import ModalContents from "./ModalContents.js"; 4 | import Portal from "../Portal/Portal.js"; 5 | 6 | /** 7 | * Wrapper for the Modal, to put it inside of a Portal. 8 | * The Modal needs its own lifecycle and therefore this wrapper is necessary 9 | */ 10 | 11 | class Modal extends React.Component { 12 | render() { 13 | return ( 14 | 15 | 16 | 17 | ); 18 | } 19 | } 20 | 21 | module.exports = Modal; 22 | -------------------------------------------------------------------------------- /src/Modal/styles.less: -------------------------------------------------------------------------------- 1 | // If you're including this file on its own, please 2 | // import these two files in your main less file: 3 | // * @import '../overrides/gemini-scrollbar.less'; 4 | // * @import (inline) '../../react-gemini-scrollbar/node_modules/gemini-scrollbar/gemini-scrollbar.css'; 5 | 6 | & when (@modal-enabled) { 7 | 8 | .modal-wrapper { 9 | position: fixed; 10 | z-index: @z-index-modal; 11 | } 12 | 13 | .modal { 14 | left: 50%; 15 | position: fixed; 16 | top: 50%; 17 | transform: translate(-50%, -50%); 18 | z-index: auto; 19 | } 20 | 21 | .modal-body-wrapper { 22 | flex: 1 1 auto; 23 | min-height: 0; 24 | position: relative; 25 | 26 | .container-scrollable { 27 | height: 100%; 28 | left: 0; 29 | position: absolute; 30 | top: 0; 31 | width: 100%; 32 | } 33 | } 34 | 35 | .modal-backdrop { 36 | height: 100%; 37 | left: 0; 38 | position: fixed; 39 | top: 0; 40 | width: 100%; 41 | z-index: auto; 42 | } 43 | 44 | .modal-enter, 45 | .modal-appear { 46 | 47 | .modal { 48 | opacity: 0; 49 | transform: translate(-50%, calc(-50% ~' - ' @modal-animation-offset)); 50 | } 51 | 52 | .modal-backdrop { 53 | opacity: 0; 54 | } 55 | 56 | &.modal-enter-active, 57 | &.modal-appear-active { 58 | 59 | .modal { 60 | opacity: 1; 61 | transform: translate(-50%, -50%); 62 | transition: transform @modal-animation-duration @modal-animation-easing, opacity @modal-animation-duration @modal-animation-easing; 63 | } 64 | 65 | .modal-backdrop { 66 | opacity: 1; 67 | transition: opacity @modal-animation-duration @modal-animation-easing; 68 | } 69 | } 70 | } 71 | 72 | .modal-exit { 73 | 74 | .modal { 75 | opacity: 1; 76 | transform: translate(-50%, -50%); 77 | } 78 | 79 | .modal-backdrop { 80 | opacity: 1; 81 | } 82 | 83 | &.modal-exit-active { 84 | 85 | .modal { 86 | opacity: 0; 87 | transform: translate(-50%, calc(-50% ~' - ' @modal-animation-offset)); 88 | transition: transform @modal-animation-duration @modal-animation-easing, opacity @modal-animation-duration @modal-animation-easing; 89 | } 90 | 91 | .modal-backdrop { 92 | opacity: 0; 93 | transition: opacity @modal-animation-duration @modal-animation-easing; 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Modal/variables.less: -------------------------------------------------------------------------------- 1 | @modal-enabled: true; 2 | 3 | @modal-animation-offset: 50px; 4 | 5 | @modal-animation-duration: 0.3s; 6 | @modal-animation-easing: ease; 7 | -------------------------------------------------------------------------------- /src/Portal/Portal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import ReactDOM from "react-dom"; 4 | 5 | class Portal extends React.Component { 6 | componentDidMount() { 7 | this.nodeEl = document.createElement("div"); 8 | document.body.appendChild(this.nodeEl); 9 | this.renderChildren(this.props); 10 | } 11 | 12 | componentWillUnmount() { 13 | ReactDOM.unmountComponentAtNode(this.nodeEl); 14 | document.body.removeChild(this.nodeEl); 15 | } 16 | 17 | UNSAFE_componentWillReceiveProps(nextProps) { 18 | this.renderChildren(nextProps); 19 | } 20 | 21 | renderChildren(props) { 22 | // Compatibility notice: This component will need to be restructured 23 | // once this API goes away in favor of React.createPortal (16+) 24 | // The issue is that there is no render callback in that API 25 | // so props.onRender will need to be called in some other way. 26 | return ReactDOM.unstable_renderSubtreeIntoContainer( 27 | this, 28 | props.children, 29 | this.nodeEl, 30 | props.onRender 31 | ); 32 | } 33 | 34 | render() { 35 | return null; 36 | } 37 | } 38 | 39 | Portal.propTypes = { 40 | children: PropTypes.node.isRequired, 41 | onRender: PropTypes.func 42 | }; 43 | 44 | module.exports = Portal; 45 | -------------------------------------------------------------------------------- /src/Select/Select.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import Dropdown from "../Dropdown/Dropdown"; 5 | import DropdownListTrigger from "../Dropdown/DropdownListTrigger"; 6 | 7 | export default class Select extends React.Component { 8 | constructor() { 9 | super(); 10 | this.input = React.createRef(); 11 | } 12 | buildItemsArray() { 13 | return React.Children.map(this.props.children, child => { 14 | return { 15 | html: child, 16 | id: child.props.value, 17 | selectedHtml: child.props.label || child.props.value, 18 | selectable: !child.props.disabled 19 | }; 20 | }); 21 | } 22 | 23 | handleInputChange(event) { 24 | this.props.onChange(event); 25 | } 26 | 27 | handleDropdownChange(selectedOption) { 28 | const nativeInputValueSetter = Object.getOwnPropertyDescriptor( 29 | window.HTMLInputElement.prototype, 30 | "value" 31 | ).set; 32 | 33 | if (this.input && this.input.current) { 34 | nativeInputValueSetter.call(this.input.current, selectedOption.id); 35 | this.input.current.dispatchEvent(new Event("input", { bubbles: true })); 36 | } 37 | } 38 | 39 | render() { 40 | return ( 41 |
42 | 52 | } 66 | /> 67 |
68 | ); 69 | } 70 | } 71 | 72 | Select.defaultProps = { 73 | className: "dropdown-select", 74 | onChange() {}, 75 | name: null, 76 | placeholder: "", 77 | value: "" 78 | }; 79 | 80 | Select.propTypes = { 81 | className: PropTypes.string, 82 | onChange: PropTypes.func, 83 | name: PropTypes.string, 84 | placeholder: PropTypes.string, 85 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) 86 | }; 87 | -------------------------------------------------------------------------------- /src/Select/SelectOption.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | export default class SelectOption extends React.Component { 5 | render() { 6 | return ( 7 |
{this.props.children || this.props.label || this.props.value}
8 | ); 9 | } 10 | } 11 | SelectOption.defaultProps = { 12 | value: null, 13 | label: null, 14 | disabled: false, 15 | selected: false 16 | }; 17 | SelectOption.propTypes = { 18 | value: PropTypes.string, 19 | label: PropTypes.string, 20 | disabled: PropTypes.bool 21 | }; 22 | -------------------------------------------------------------------------------- /src/Select/__tests__/Select-test.js: -------------------------------------------------------------------------------- 1 | jest.dontMock("../../Mixin/BindMixin"); 2 | jest.dontMock("../../Util/Util"); 3 | jest.dontMock("../../Util/DOMUtil"); 4 | jest.dontMock("../Select"); 5 | jest.dontMock("../SelectOption"); 6 | 7 | /* eslint-disable no-unused-vars */ 8 | var React = require("react"); 9 | /* eslint-enable no-unused-vars */ 10 | 11 | var TestUtils; 12 | if (React.version.match(/15.[0-5]/)) { 13 | TestUtils = require("react-addons-test-utils"); 14 | } else { 15 | TestUtils = require("react-dom/test-utils"); 16 | } 17 | 18 | var Select = require("../Select.js").default; 19 | var SelectOption = require("../SelectOption.js").default; 20 | 21 | describe("Select", function() { 22 | beforeEach(function() { 23 | this.callback = jasmine.createSpy(); 24 | this.select = TestUtils.renderIntoDocument( 25 | 53 | ); 54 | }); 55 | 56 | afterEach(function() { 57 | Array.prototype.slice.call(document.body.children).forEach(function(node) { 58 | document.body.removeChild(node); 59 | }); 60 | }); 61 | describe("#Init", function() { 62 | it("has correct classes", function() { 63 | expect(this.select.props.className).toEqual( 64 | "select-class-1 select-class-2" 65 | ); 66 | }); 67 | it("has correct name", function() { 68 | expect(this.select.props.name).toEqual("SelectName"); 69 | }); 70 | it("has correct change listener", function() { 71 | expect(this.select.props.onChange).toBe(this.callback); 72 | }); 73 | it("does not call change listener on init", function() { 74 | expect(this.callback).not.toBeCalled(); 75 | }); 76 | it("has correct initial value", function() { 77 | const input = TestUtils.findRenderedDOMComponentWithClass( 78 | this.select, 79 | "dropdown-select-input-value" 80 | ); 81 | expect(input.value).toEqual("option-3"); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/Table/__tests__/Table-test.js: -------------------------------------------------------------------------------- 1 | jest.dontMock("../Table"); 2 | jest.dontMock("../../Util/Util"); 3 | jest.dontMock("./fixtures/MockTable"); 4 | 5 | /* eslint-disable no-unused-vars */ 6 | var React = require("react"); 7 | /* eslint-enable no-unused-vars */ 8 | var ReactDOM = require("react-dom"); 9 | 10 | var TestUtils; 11 | if (React.version.match(/15.[0-5]/)) { 12 | TestUtils = require("react-addons-test-utils"); 13 | } else { 14 | TestUtils = require("react-dom/test-utils"); 15 | } 16 | 17 | var Table = require("../Table"); 18 | var DOMUtil = require("../../Util/DOMUtil"); 19 | 20 | var MockTable = require("./fixtures/MockTable"); 21 | 22 | describe("Table", function() { 23 | beforeEach(function() { 24 | this.callback = jasmine.createSpy(); 25 | this.sortBy = { 26 | prop: "name", 27 | order: "desc" 28 | }; 29 | this.instance = TestUtils.renderIntoDocument( 30 | 37 | ); 38 | this.instance.itemHeight = 0; 39 | this.getComputedDimensions = DOMUtil.getComputedDimensions; 40 | DOMUtil.getComputedDimensions = function() { 41 | return { width: 0, height: 0 }; 42 | }; 43 | }); 44 | 45 | afterEach(function() { 46 | DOMUtil.getComputedDimensions = this.getComputedDimensions; 47 | }); 48 | 49 | it("should render the proper number of columns", function() { 50 | expect( 51 | this.instance.getHeaders(MockTable.columns, this.sortBy).length 52 | ).toEqual(4); 53 | }); 54 | 55 | it("should call the callback when the data is sorted", function() { 56 | this.instance.handleSort(); 57 | expect(this.callback).toHaveBeenCalled(); 58 | }); 59 | 60 | describe("emptyMessage prop", function() { 61 | it("should display the custom empty message", function() { 62 | var instance = TestUtils.renderIntoDocument( 63 |
71 | ); 72 | 73 | this.node = document.createElement("tbody"); 74 | ReactDOM.render(instance.getEmptyRowCell([1]), this.node); 75 | let td = this.node.querySelector("td"); 76 | expect(td.textContent).toBe("Custom message"); 77 | }); 78 | 79 | it("should display the default empty message", function() { 80 | this.node = document.createElement("tbody"); 81 | ReactDOM.render(this.instance.getEmptyRowCell([1]), this.node); 82 | let td = this.node.querySelector("td"); 83 | expect(td.textContent).toBe(Table.defaultProps.emptyMessage); 84 | }); 85 | }); 86 | 87 | describe("initial sorting", function() { 88 | it("should sort the data descending on initial mount", function() { 89 | this.instance = TestUtils.renderIntoDocument( 90 |
97 | ); 98 | 99 | var tableRows = TestUtils.scryRenderedDOMComponentsWithTag( 100 | this.instance, 101 | "tr" 102 | ); 103 | 104 | expect(tableRows[1].children[0].textContent).toEqual("Zach"); 105 | expect(tableRows[5].children[0].textContent).toEqual("Francis"); 106 | }); 107 | 108 | it("should sort the data ascending on initial mount", function() { 109 | this.instance = TestUtils.renderIntoDocument( 110 |
118 | ); 119 | 120 | var tableRows = TestUtils.scryRenderedDOMComponentsWithTag( 121 | this.instance, 122 | "tr" 123 | ); 124 | 125 | expect(tableRows[1].children[0].textContent).toEqual("Francis"); 126 | expect(tableRows[5].children[0].textContent).toEqual("Zach"); 127 | }); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /src/Table/__tests__/fixtures/MockTable.json: -------------------------------------------------------------------------------- 1 | { 2 | "columns": [ 3 | { 4 | "className": "name", 5 | "heading": "Name", 6 | "prop": "name", 7 | "sortable": false 8 | }, 9 | { 10 | "className": "age", 11 | "heading": "Age", 12 | "prop": "age", 13 | "sortable": false 14 | }, 15 | { 16 | "className": "location", 17 | "defaultContent": "None Specified", 18 | "heading": "Location", 19 | "prop": "location", 20 | "sortable": false 21 | }, 22 | { 23 | "className": "gender", 24 | "heading": "Gender", 25 | "prop": "gender", 26 | "sortable": false 27 | } 28 | ], 29 | "rows": [ 30 | { 31 | "name": "Zach", 32 | "age": "11", 33 | "gender": "Male", 34 | "location": "San Francisco, CA", 35 | "id": "a" 36 | }, 37 | { 38 | "name": "Francis", 39 | "age": "34", 40 | "gender": "Female", 41 | "location": "Boston, MA", 42 | "id": "b" 43 | }, 44 | { 45 | "name": "Sandy", 46 | "age": "68", 47 | "gender": "Female", 48 | "location": "Kalamazoo, MI", 49 | "id": "c" 50 | }, 51 | { 52 | "name": "Jeffrey", 53 | "age": "21", 54 | "gender": "Male", 55 | "id": "d" 56 | }, 57 | { 58 | "name": "Louise", 59 | "age": "94", 60 | "gender": "Female", 61 | "location": "Boulder, CO", 62 | "id": "e" 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /src/Table/styles.less: -------------------------------------------------------------------------------- 1 | // If you're including this file manually please 2 | // import these two files in your main less file: 3 | // * @import "../overrides/gemini-scrollbar.less"; 4 | // * @import (inline) "../../react-gemini-scrollbar/node_modules/gemini-scrollbar/gemini-scrollbar.css"; 5 | 6 | .table { 7 | 8 | th { 9 | outline: none; 10 | user-select: none; 11 | } 12 | } 13 | 14 | .caret { 15 | border-left: 0.3em solid transparent; 16 | border-right: 0.3em solid transparent; 17 | border-top: 0.45em solid fade(@grey-dark, 50%); 18 | display: none; 19 | margin-left: 0.5em; 20 | vertical-align: middle; 21 | 22 | .inverse & { 23 | border-top: 0.45em solid fade(@white, 50%); 24 | } 25 | 26 | &--visible { 27 | display: inline-block; 28 | } 29 | 30 | &--asc { 31 | transform: rotate(180deg); 32 | } 33 | 34 | &--desc { 35 | transform: rotate(0); 36 | } 37 | } 38 | 39 | @keyframes table-fade-in-left { 40 | 41 | 0% { 42 | opacity: 0; 43 | transform: translateX(-2em); 44 | } 45 | 46 | 100% { 47 | opacity: 1; 48 | transform: translateX(0); 49 | } 50 | } 51 | 52 | @keyframes table-fade-out-right { 53 | 54 | 0% { 55 | opacity: 1; 56 | transform: translateX(0); 57 | } 58 | 59 | 100% { 60 | opacity: 0; 61 | transform: translateX(2em); 62 | } 63 | } 64 | 65 | .table-row-enter { 66 | animation: table-fade-in-left 0.5s; 67 | } 68 | 69 | .table-row-exit { 70 | animation: table-fade-out-right 0.5s; 71 | } 72 | -------------------------------------------------------------------------------- /src/Tooltip/__tests__/Tooltip-test.js: -------------------------------------------------------------------------------- 1 | jest.dontMock("../Tooltip"); 2 | jest.dontMock("../../Util/Util"); 3 | 4 | /* eslint-disable no-unused-vars */ 5 | var React = require("react"); 6 | /* eslint-enable no-unused-vars */ 7 | 8 | var TestUtils; 9 | if (React.version.match(/15.[0-5]/)) { 10 | TestUtils = require("react-addons-test-utils"); 11 | } else { 12 | TestUtils = require("react-dom/test-utils"); 13 | } 14 | 15 | var Tooltip = require("../Tooltip"); 16 | 17 | describe("Tooltip", function() { 18 | beforeEach(function() { 19 | this.instance = TestUtils.renderIntoDocument( 20 | 26 | Tooltip Trigger 27 | 28 | ); 29 | }); 30 | 31 | it("should render with the intended class", function() { 32 | expect( 33 | this.instance.tooltipNode.current.classList.contains("foo") 34 | ).toBeTruthy(); 35 | }); 36 | 37 | it("should render with the intended position as a class", function() { 38 | expect( 39 | this.instance.tooltipNode.current.classList.contains("position-bottom") 40 | ).toBeTruthy(); 41 | }); 42 | 43 | it("should render with the intended anchor as a class", function() { 44 | expect( 45 | this.instance.tooltipNode.current.classList.contains("anchor-start") 46 | ).toBeTruthy(); 47 | }); 48 | 49 | describe("#handleMouseEnter", function() { 50 | it("should set isOpen to true", function() { 51 | this.instance.handleMouseEnter(); 52 | 53 | expect(this.instance.state.isOpen).toBeTruthy(); 54 | }); 55 | 56 | it("should set anchor to what was returned from #getIdealPosition", function() { 57 | this.instance.handleMouseEnter("start", "bottom"); 58 | 59 | expect(this.instance.state.anchor).toEqual("start"); 60 | }); 61 | 62 | it("should set position to what was returned from #getIdealPosition", function() { 63 | this.instance.handleMouseEnter("start", "bottom"); 64 | 65 | expect(this.instance.state.position).toEqual("bottom"); 66 | }); 67 | }); 68 | 69 | describe("#handleMouseLeave", function() { 70 | it("should set isOpen to false", function() { 71 | this.instance.handleMouseLeave(); 72 | 73 | expect(this.instance.state.isOpen).toBeFalsy(); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/Tooltip/styles.less: -------------------------------------------------------------------------------- 1 | .tooltip-wrapper { 2 | display: inline-block; 3 | position: relative; 4 | } 5 | 6 | .tooltip-block-wrapper { 7 | display: block; 8 | } 9 | 10 | .tooltip { 11 | max-width: 600px; 12 | opacity: 0; 13 | pointer-events: none; 14 | position: fixed; 15 | transition: opacity 0.3s, visibility 0.3s; 16 | visibility: hidden; 17 | z-index: 9999; 18 | 19 | .tooltip-content { 20 | background: @tooltip-background; 21 | border-radius: @tooltip-border-radius; 22 | color: @tooltip-foreground; 23 | font-size: @tooltip-font-size; 24 | line-height: @tooltip-line-height; 25 | padding: @tooltip-padding-vertical @tooltip-padding-horizontal; 26 | position: relative; 27 | 28 | &:after { 29 | border-color: @tooltip-background; 30 | border-style: solid; 31 | border-width: @tooltip-arrow-border-width; 32 | content: ''; 33 | position: absolute; 34 | } 35 | } 36 | 37 | &.no-wrap { 38 | white-space: nowrap; 39 | } 40 | 41 | &.is-interactive { 42 | pointer-events: auto; 43 | } 44 | 45 | &.is-open { 46 | opacity: 1; 47 | visibility: visible; 48 | } 49 | 50 | &.position-bottom, 51 | &.position-top { 52 | 53 | &.anchor-center { 54 | transform: translateX(-50%); 55 | 56 | .tooltip-content { 57 | 58 | &:after { 59 | left: 50%; 60 | transform: translateX(-50%); 61 | } 62 | } 63 | } 64 | 65 | &.anchor-start { 66 | transform: translateX(@tooltip-anchor-offset * -1); 67 | 68 | .tooltip-content { 69 | 70 | &:after { 71 | left: @tooltip-arrow-offset; 72 | } 73 | } 74 | } 75 | 76 | &.anchor-end { 77 | transform: translateX(calc(~'-100% + @{tooltip-anchor-offset}')); 78 | 79 | .tooltip-content { 80 | 81 | &:after { 82 | right: @tooltip-arrow-offset; 83 | } 84 | } 85 | } 86 | } 87 | 88 | &.position-bottom { 89 | padding-top: @tooltip-arrow-border-width; 90 | 91 | .tooltip-content { 92 | 93 | &:after { 94 | border-left-color: transparent; 95 | border-right-color: transparent; 96 | border-top: none; 97 | bottom: 100%; 98 | } 99 | } 100 | } 101 | 102 | &.position-top { 103 | padding-bottom: @tooltip-arrow-border-width; 104 | 105 | .tooltip-content { 106 | 107 | &:after { 108 | border-bottom: none; 109 | border-left-color: transparent; 110 | border-right-color: transparent; 111 | top: 100%; 112 | } 113 | } 114 | } 115 | 116 | &.position-left, 117 | &.position-right { 118 | 119 | &.anchor-center { 120 | top: 50%; 121 | transform: translateY(-50%); 122 | 123 | .tooltip-content { 124 | 125 | &:after { 126 | top: 50%; 127 | transform: translateY(-50%); 128 | } 129 | } 130 | } 131 | 132 | &.anchor-start { 133 | transform: translateY(@tooltip-anchor-offset * -1); 134 | 135 | .tooltip-content { 136 | 137 | &:after { 138 | top: @tooltip-arrow-offset; 139 | } 140 | } 141 | } 142 | 143 | &.anchor-end { 144 | transform: translateY(calc(~'-100% + @{tooltip-anchor-offset}')); 145 | 146 | .tooltip-content { 147 | 148 | &:after { 149 | bottom: @tooltip-arrow-offset; 150 | } 151 | } 152 | } 153 | } 154 | 155 | &.position-left { 156 | padding-right: @tooltip-arrow-border-width; 157 | 158 | .tooltip-content { 159 | 160 | &:after { 161 | border-bottom-color: transparent; 162 | border-right: none; 163 | border-top-color: transparent; 164 | left: 100%; 165 | } 166 | } 167 | } 168 | 169 | &.position-right { 170 | padding-left: @tooltip-arrow-border-width; 171 | 172 | .tooltip-content { 173 | 174 | &:after { 175 | border-bottom-color: transparent; 176 | border-left: none; 177 | border-top-color: transparent; 178 | right: 100%; 179 | } 180 | } 181 | } 182 | 183 | a { 184 | color: @tooltip-link-foreground; 185 | text-decoration: underline; 186 | 187 | &:active { 188 | text-decoration: underline; 189 | } 190 | 191 | &:hover { 192 | color: @tooltip-link-foreground; 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/Tooltip/variables.less: -------------------------------------------------------------------------------- 1 | @tooltip-background: @grey-dark; 2 | @tooltip-foreground: fade(@white, 90%); 3 | @tooltip-link-foreground: @white; 4 | 5 | @tooltip-border-radius: 3px; 6 | @tooltip-font-size: 1.125rem; 7 | @tooltip-line-height: 1.125rem; 8 | 9 | @tooltip-padding-vertical: @base-spacing-unit * 1/2; 10 | @tooltip-padding-horizontal: @base-spacing-unit; 11 | 12 | @tooltip-arrow-border-width: 7px; 13 | @tooltip-arrow-offset: 8px; 14 | 15 | @tooltip-anchor-offset: @tooltip-arrow-offset + @tooltip-arrow-border-width; 16 | -------------------------------------------------------------------------------- /src/Util/DOMUtil.js: -------------------------------------------------------------------------------- 1 | var computeInnerBound = function(compstyle, acc, key) { 2 | var val = parseInt(compstyle[key], 10); 3 | 4 | if (isNaN(val)) { 5 | return acc; 6 | } else { 7 | return acc - val; 8 | } 9 | }; 10 | 11 | const DOMUtil = { 12 | closest(el, selector) { 13 | var currentEl = el; 14 | 15 | while (currentEl && currentEl.parentElement !== null) { 16 | if (currentEl[this.matchesFn] && currentEl[this.matchesFn](selector)) { 17 | return currentEl; 18 | } 19 | 20 | currentEl = currentEl.parentElement; 21 | } 22 | 23 | return null; 24 | }, 25 | 26 | getPageHeight() { 27 | var body = document.body; 28 | var html = document.documentElement; 29 | 30 | return Math.max( 31 | body.scrollHeight, 32 | body.offsetHeight, 33 | html.clientHeight, 34 | html.scrollHeight, 35 | html.offsetHeight 36 | ); 37 | }, 38 | 39 | getComputedDimensions(obj) { 40 | var compstyle; 41 | if (typeof window.getComputedStyle === "undefined") { 42 | compstyle = obj.currentStyle; 43 | } else { 44 | compstyle = window.getComputedStyle(obj); 45 | } 46 | 47 | var width = [ 48 | "borderLeftWidth", 49 | "borderRightWidth", 50 | "marginLeft", 51 | "marginRight", 52 | "paddingLeft", 53 | "paddingRight" 54 | ].reduce(computeInnerBound.bind(this, compstyle), obj.offsetWidth); 55 | 56 | var height = [ 57 | "borderTopWidth", 58 | "borderBottomWidth", 59 | "marginTop", 60 | "marginBottom", 61 | "paddingTop", 62 | "paddingBottom" 63 | ].reduce(computeInnerBound.bind(this, compstyle), obj.offsetHeight); 64 | 65 | return { 66 | width, 67 | height 68 | }; 69 | }, 70 | 71 | getNodeClearance(DOMNode) { 72 | if (!DOMNode) { 73 | return { 74 | bottom: 0, 75 | left: 0, 76 | right: 0, 77 | top: 0, 78 | boundingRect: { 79 | bottom: 0, 80 | height: 0, 81 | left: 0, 82 | right: 0, 83 | top: 0, 84 | width: 0, 85 | x: 0, 86 | y: 0 87 | } 88 | }; 89 | } 90 | 91 | const viewportHeight = DOMUtil.getViewportHeight(); 92 | const viewportWidth = DOMUtil.getViewportWidth(); 93 | const boundingRect = DOMNode.getBoundingClientRect(); 94 | 95 | return { 96 | bottom: viewportHeight - boundingRect.bottom, 97 | left: boundingRect.left, 98 | right: viewportWidth - boundingRect.right, 99 | top: boundingRect.top, 100 | boundingRect 101 | }; 102 | }, 103 | 104 | getViewportHeight() { 105 | return Math.max( 106 | document.documentElement.clientHeight || 0, 107 | window.innerHeight || 0 108 | ); 109 | }, 110 | 111 | getViewportWidth() { 112 | return Math.max( 113 | document.documentElement.clientWidth || 0, 114 | window.innerWidth || 0 115 | ); 116 | }, 117 | 118 | getScrollTop(element) { 119 | if (element === window || element === document) { 120 | return ( 121 | self.pageYOffset || 122 | document.documentElement.scrollTop || 123 | document.body.scrollTop 124 | ); 125 | } else { 126 | return element.scrollTop; 127 | } 128 | }, 129 | 130 | matchesFn: (function() { 131 | const el = document.querySelector("body"); 132 | const names = [ 133 | "matches", 134 | "matchesSelector", 135 | "msMatchesSelector", 136 | "oMatchesSelector", 137 | "mozMatchesSelector", 138 | "webkitMatchesSelector" 139 | ]; 140 | 141 | for (let i = 0; i < names.length; i++) { 142 | if (el[names[i]]) { 143 | return names[i]; 144 | } 145 | } 146 | 147 | return names[0]; 148 | })() 149 | }; 150 | 151 | module.exports = DOMUtil; 152 | -------------------------------------------------------------------------------- /src/Util/KeyboardUtil.js: -------------------------------------------------------------------------------- 1 | const KeyboardUtil = { 2 | keys: { 3 | enter: "Enter" 4 | } 5 | }; 6 | 7 | module.exports = KeyboardUtil; 8 | -------------------------------------------------------------------------------- /src/Util/__tests__/DOMUtil-test.js: -------------------------------------------------------------------------------- 1 | jest.dontMock("../DOMUtil"); 2 | 3 | /* eslint-disable no-unused-vars */ 4 | var React = require("react"); 5 | /* eslint-enable no-unused-vars */ 6 | 7 | var DOMUtil = require("../DOMUtil"); 8 | 9 | describe("DOMUtil", function() { 10 | describe("#closest", function() { 11 | it( 12 | "should should return the parent element when provided a selector and " + 13 | "element where the element is a child of the selection", 14 | function() { 15 | var el = { 16 | parentElement: { 17 | id: "something-fake", 18 | matches: function() { 19 | return true; 20 | } 21 | }, 22 | matches: function() { 23 | return false; 24 | } 25 | }; 26 | var match = DOMUtil.closest(el, ".fake-selector"); 27 | 28 | expect(match.id).toEqual("something-fake"); 29 | } 30 | ); 31 | 32 | it( 33 | "should should return null when provided a selector and element where " + 34 | "the element is not a child of the selection", 35 | function() { 36 | var el = { 37 | parentElement: null, 38 | matches: function() { 39 | return true; 40 | } 41 | }; 42 | var match = DOMUtil.closest(el, ".fake-selector"); 43 | 44 | expect(match).toEqual(null); 45 | } 46 | ); 47 | 48 | it( 49 | "should should return the provided element when the provided element" + 50 | "matches the selector AND has a parent element", 51 | function() { 52 | var el = { 53 | parentElement: { 54 | id: "something-fake", 55 | matches: function() { 56 | return false; 57 | } 58 | }, 59 | id: "child-element", 60 | matches: function() { 61 | return true; 62 | } 63 | }; 64 | var match = DOMUtil.closest(el, ".fake-selector"); 65 | 66 | expect(match.id).toEqual("child-element"); 67 | } 68 | ); 69 | }); 70 | 71 | describe("#getNodeClearance", function() { 72 | beforeEach(function() { 73 | DOMUtil.getViewportHeight = function() { 74 | return 600; 75 | }; 76 | 77 | DOMUtil.getViewportWidth = function() { 78 | return 600; 79 | }; 80 | }); 81 | 82 | it("should return the element's distance from all edges", function() { 83 | let node = { 84 | getBoundingClientRect: function() { 85 | return { 86 | bottom: 200, 87 | left: 100, 88 | right: 200, 89 | top: 100 90 | }; 91 | } 92 | }; 93 | 94 | let clearance = DOMUtil.getNodeClearance(node); 95 | 96 | expect(clearance.bottom).toEqual(400); 97 | expect(clearance.left).toEqual(100); 98 | expect(clearance.right).toEqual(400); 99 | expect(clearance.top).toEqual(100); 100 | }); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /src/Util/__tests__/Util-test.js: -------------------------------------------------------------------------------- 1 | jest.dontMock("../Util"); 2 | 3 | var Util = require("../Util"); 4 | 5 | describe("Util", function() { 6 | describe("#extend", function() { 7 | beforeEach(function() { 8 | this.originalObj = { 9 | a: 5, 10 | b: 10, 11 | c: 15 12 | }; 13 | }); 14 | 15 | it("should not change any properties if passed a single argument", function() { 16 | var newObj = Util.extend(this.originalObj); 17 | 18 | for (var key in this.originalObj) { 19 | expect(newObj[key]).toEqual(this.originalObj[key]); 20 | } 21 | }); 22 | 23 | it("should combine properties with the source", function() { 24 | var source = { 25 | a: "changed prop" 26 | }; 27 | 28 | var newObj = Util.extend(this.originalObj, source); 29 | expect(newObj.a).toEqual("changed prop"); 30 | }); 31 | 32 | it("should handle multiple arguments", function() { 33 | var obj1 = { 34 | a: "changed prop", 35 | b: "changed prop" 36 | }; 37 | 38 | var obj2 = { 39 | a: "overrode prop" 40 | }; 41 | 42 | var newObj = Util.extend(this.originalObj, obj1, obj2); 43 | expect(newObj.a).toEqual("overrode prop"); 44 | expect(newObj.b).toEqual("changed prop"); 45 | }); 46 | 47 | it("should not do anything if not passed an obj", function() { 48 | var string = "string"; 49 | var func = function() {}; 50 | func.fakeProp = "faked prop"; 51 | var nullVal = null; 52 | 53 | var newObj = Util.extend(this.originalObj, string, func, nullVal); 54 | 55 | for (var key in newObj) { 56 | expect(newObj[key]).toEqual(this.originalObj[key]); 57 | } 58 | }); 59 | }); 60 | 61 | describe("#exclude", function() { 62 | beforeEach(function() { 63 | this.object = { 64 | foo: "foo", 65 | bar: "bar", 66 | baz: "baz" 67 | }; 68 | }); 69 | 70 | it("doesn't exclude any properties", function() { 71 | var expectedResult = { 72 | foo: "foo", 73 | bar: "bar", 74 | baz: "baz" 75 | }; 76 | expect(Util.exclude(this.object, [])).toEqual(expectedResult); 77 | }); 78 | 79 | it("excludes one property", function() { 80 | var expectedResult = { 81 | foo: "foo", 82 | baz: "baz" 83 | }; 84 | expect(Util.exclude(this.object, ["bar"])).toEqual(expectedResult); 85 | }); 86 | 87 | it("excludes multiple properties", function() { 88 | var expectedResult = { 89 | baz: "baz" 90 | }; 91 | expect(Util.exclude(this.object, ["foo", "bar", "qux"])).toEqual( 92 | expectedResult 93 | ); 94 | }); 95 | 96 | it("doesn't modify the original object", function() { 97 | var expectedResult = { 98 | foo: "foo", 99 | bar: "bar", 100 | baz: "baz" 101 | }; 102 | Util.exclude(this.object, ["bar"]); 103 | expect(this.object).toEqual(expectedResult); 104 | }); 105 | }); 106 | 107 | describe("#capitalize", function() { 108 | it("capitalizes the string correctly", function() { 109 | expect(Util.capitalize("kenny")).toEqual("Kenny"); 110 | }); 111 | 112 | it("returns null if input is not a string", function() { 113 | expect(Util.capitalize(10)).toEqual(null); 114 | }); 115 | 116 | it("does nothing if string is already capitalized", function() { 117 | var capitalizedString = "Name"; 118 | expect(Util.capitalize(capitalizedString)).toEqual(capitalizedString); 119 | }); 120 | }); 121 | 122 | describe("#sortBy", function() { 123 | beforeEach(function() { 124 | this.collection = [ 125 | { name: "c", value: 1 }, 126 | { name: "t", value: 3 }, 127 | { name: "a", value: 3 } 128 | ]; 129 | }); 130 | 131 | it("should not mutate original collection", function() { 132 | let result = Util.sortBy(this.collection, "value"); 133 | 134 | expect(result !== this.collection).toBeTruthy(); 135 | }); 136 | 137 | it("should sort collection by prop", function() { 138 | let result = Util.sortBy(this.collection, "name"); 139 | 140 | expect(result).toEqual([ 141 | { name: "a", value: 3 }, 142 | { name: "c", value: 1 }, 143 | { name: "t", value: 3 } 144 | ]); 145 | }); 146 | 147 | it("should use custom compare function", function() { 148 | let result = Util.sortBy(this.collection, function(a, b) { 149 | var delta = a.value - b.value; 150 | return delta / Math.abs(delta || 0); 151 | }); 152 | 153 | expect(result).toEqual([ 154 | { name: "c", value: 1 }, 155 | { name: "t", value: 3 }, 156 | { name: "a", value: 3 } 157 | ]); 158 | }); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /src/constants/Keycodes.js: -------------------------------------------------------------------------------- 1 | const KEYCODES = { 2 | esc: 27 3 | }; 4 | 5 | module.exports = KEYCODES; 6 | -------------------------------------------------------------------------------- /src/index.less: -------------------------------------------------------------------------------- 1 | @import (inline) '../node_modules/gemini-scrollbar/gemini-scrollbar.css'; 2 | @import './overrides/gemini-scrollbar.less'; 3 | @import './Dropdown/styles.less'; 4 | @import './Form/variables.less'; 5 | @import './Form/styles.less'; 6 | @import './List/styles.less'; 7 | @import './Modal/variables.less'; 8 | @import './Modal/styles.less'; 9 | @import './Table/styles.less'; 10 | @import './Tooltip/variables.less'; 11 | @import './Tooltip/styles.less'; 12 | -------------------------------------------------------------------------------- /src/overrides/gemini-scrollbar.less: -------------------------------------------------------------------------------- 1 | .gm-scrollbar { 2 | border-radius: 4px; 3 | bottom: 3px; 4 | right: 3px; 5 | 6 | &.-vertical { 7 | top: 3px; 8 | width: 8px; 9 | z-index: 10000; 10 | } 11 | 12 | &.-horizontal { 13 | height: 8px; 14 | left: 3px; 15 | z-index: 10000; 16 | } 17 | 18 | .thumb { 19 | background-color: #333; 20 | opacity: 0.6; 21 | 22 | &:active, 23 | &:hover { 24 | background-color: #333; 25 | opacity: 0.9; 26 | } 27 | } 28 | } 29 | 30 | .gm-scrollbar-container { 31 | height: 100%; 32 | 33 | &.inverse { 34 | 35 | .gm-scrollbar { 36 | 37 | .thumb { 38 | background-color: #eee; 39 | 40 | &:active, 41 | &:hover { 42 | background-color: #eee; 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | .gm-scroll-view pre { 50 | overflow: visible; 51 | } 52 | 53 | // Scrollable areas 54 | .flex-container-col { 55 | display: flex; 56 | flex: 1; 57 | flex-direction: column; 58 | } 59 | 60 | .flex-container-row { 61 | display: flex; 62 | flex: 1; 63 | flex-direction: row; 64 | } 65 | 66 | .container-scrollable { 67 | flex: 1; 68 | overflow: auto; 69 | 70 | > .gm-scrollbar-container { 71 | height: 100%; 72 | width: 100%; 73 | } 74 | } 75 | 76 | .gm-prevented { 77 | 78 | .gm-scroll-view { 79 | height: 100%; 80 | overflow: auto; 81 | width: 100%; 82 | } 83 | } 84 | --------------------------------------------------------------------------------