diff --git a/.gitignore b/.gitignore index 304e9de5ed214..ea4c416671298 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ selenium *.swo *.out ui_framework/doc_site/build/*.js* +ui_framework/jest/report yarn.lock diff --git a/package.json b/package.json index d8979be4b7806..782094c1cfd30 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,9 @@ "mocha": "mocha", "mocha:debug": "mocha --debug-brk", "sterilize": "grunt sterilize", - "uiFramework:start": "grunt uiFramework:start" + "uiFramework:start": "grunt uiFramework:start", + "uiFramework:dev": "node tasks/utils/ui_framework_test --env=jsdom --watch", + "uiFramework:coverage": "node tasks/utils/ui_framework_test --env=jsdom --coverage" }, "repository": { "type": "git", @@ -95,6 +97,7 @@ "autoprefixer-loader": "2.0.0", "babel-cli": "6.18.0", "babel-core": "6.21.0", + "babel-jest": "18.0.0", "babel-loader": "6.2.10", "babel-plugin-add-module-exports": "0.2.1", "babel-polyfill": "6.20.0", @@ -199,6 +202,7 @@ }, "devDependencies": { "@elastic/eslint-config-kibana": "0.4.0", + "@spalger/babel-presets": "0.3.2", "angular-mocks": "1.4.7", "auto-release-sinon": "1.0.3", "babel-eslint": "6.1.2", @@ -210,8 +214,10 @@ "del": "1.2.1", "elasticdump": "2.1.1", "enzyme": "2.7.0", + "enzyme-to-json": "1.4.5", "eslint": "3.11.1", "eslint-plugin-babel": "4.0.0", + "eslint-plugin-jest": "19.0.1", "eslint-plugin-mocha": "4.7.0", "eslint-plugin-react": "6.10.3", "event-stream": "3.3.2", @@ -231,11 +237,14 @@ "gulp-sourcemaps": "1.7.3", "highlight.js": "9.0.0", "history": "2.1.1", + "html": "1.0.0", "html-loader": "0.4.3", "husky": "0.8.1", "image-diff": "1.6.0", "intern": "3.2.3", "istanbul-instrumenter-loader": "0.1.3", + "jest": "19.0.0", + "jest-cli": "19.0.0", "jsdom": "9.9.1", "karma": "1.2.0", "karma-chrome-launcher": "0.2.0", diff --git a/tasks/config/eslint.js b/tasks/config/eslint.js index ba5fb91a02080..2804d9661e64c 100644 --- a/tasks/config/eslint.js +++ b/tasks/config/eslint.js @@ -8,6 +8,8 @@ export default grunt => ({ 'src', 'tasks', 'test', + 'ui_framework/components', + 'ui_framework/doc_site', 'utilities', ], }, diff --git a/tasks/test.js b/tasks/test.js index 91729f70b43f3..a5ae3869c7e14 100644 --- a/tasks/test.js +++ b/tasks/test.js @@ -51,6 +51,7 @@ module.exports = function (grunt) { grunt.registerTask('test:coverage', [ 'run:testCoverageServer', 'karma:coverage' ]); grunt.registerTask('test:quick', [ + 'uiFramework:test', 'test:server', 'test:ui', 'test:browser', diff --git a/tasks/ui_framework_test.js b/tasks/ui_framework_test.js new file mode 100644 index 0000000000000..aef875ee85d49 --- /dev/null +++ b/tasks/ui_framework_test.js @@ -0,0 +1,37 @@ +const platform = require('os').platform(); +const config = require('./utils/ui_framework_test_config'); + +module.exports = function (grunt) { + grunt.registerTask('uiFramework:test', function () { + const done = this.async(); + Promise.all([uiFrameworkTest()]).then(done); + }); + + function uiFrameworkTest() { + const serverCmd = { + cmd: /^win/.test(platform) ? '.\\node_modules\\.bin\\jest.cmd' : './node_modules/.bin/jest', + args: [ + '--env=jsdom', + `--config=${JSON.stringify(config)}`, + ], + opts: { stdio: 'inherit' } + }; + + return new Promise((resolve, reject) => { + grunt.util.spawn(serverCmd, (error, result, code) => { + if (error || code !== 0) { + const message = result.stderr || result.stdout; + + grunt.log.error(message); + + return reject(); + } + + grunt.log.writeln(result); + + resolve(); + }); + + }); + } +}; diff --git a/tasks/utils/ui_framework_test.js b/tasks/utils/ui_framework_test.js new file mode 100644 index 0000000000000..6a9bf528ed7c1 --- /dev/null +++ b/tasks/utils/ui_framework_test.js @@ -0,0 +1,8 @@ +const jest = require('jest'); +const config = require('./ui_framework_test_config'); + +const argv = process.argv.slice(2); + +argv.push('--config', JSON.stringify(config)); + +jest.run(argv); diff --git a/tasks/utils/ui_framework_test_config.js b/tasks/utils/ui_framework_test_config.js new file mode 100644 index 0000000000000..9b62770f40048 --- /dev/null +++ b/tasks/utils/ui_framework_test_config.js @@ -0,0 +1,27 @@ +const path = require('path'); +const rootDir = 'ui_framework'; +const resolve = relativePath => path.resolve(__dirname, '..', '', relativePath); + +module.exports = { + rootDir, + collectCoverageFrom: [ + 'components/**/*.js', + // Seems to be a bug with jest or micromatch, in which the above glob doesn't match subsequent + // levels of directories, making this glob necessary. + 'components/**/**/*.js', + '!components/index.js', + '!components/**/*/index.js', + ], + coverageDirectory: '/jest/report', + coverageReporters: ['html'], + moduleFileExtensions: ['jsx', 'js', 'json'], + moduleNameMapper: { + '^.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm)$': resolve('config/jest/FileStub.js'), + '^.+\\.css$': resolve('config/jest/CSSStub.js'), + '^.+\\.scss$': resolve('config/jest/CSSStub.js') + }, + testPathIgnorePatterns: ['/(dist|doc_site|jest)/'], + testEnvironment: 'node', + testRegex: '.*\.test\.(js|jsx)$', + snapshotSerializers: ['/../node_modules/enzyme-to-json/serializer'] +}; diff --git a/ui_framework/.babelrc b/ui_framework/.babelrc new file mode 100644 index 0000000000000..9f3449c2e3053 --- /dev/null +++ b/ui_framework/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["react", "@spalger/babel-presets"] +} diff --git a/ui_framework/.eslintrc b/ui_framework/.eslintrc new file mode 100644 index 0000000000000..e20f525141316 --- /dev/null +++ b/ui_framework/.eslintrc @@ -0,0 +1,13 @@ +{ + "plugins": [ + "jest" + ], + "rules": { + "jest/no-disabled-tests": "error", + "jest/no-focused-tests": "error", + "jest/no-identical-title": "error" + }, + "env": { + "jest/globals": true + } +} diff --git a/ui_framework/README.md b/ui_framework/README.md index 1ba621de03179..44ff16a7a279d 100644 --- a/ui_framework/README.md +++ b/ui_framework/README.md @@ -1,53 +1,100 @@ # Kibana UI Framework -## Development +> The Kibana UI Framework is a collection of React UI components for quickly building user interfaces +> for Kibana. Not using React? No problem! You can still use the CSS behind each component. -* Start development server `npm run uiFramework:start`. -* View docs on `http://localhost:8020/`. +## Using the Framework -## What is this? +### Documentation -The Kibana UI Framework provides you with UI components you can quickly use to build user interfaces for Kibana. +You can view interactive documentation by running `npm run uiFramework:start` and then visiting +`http://localhost:8020/`. -The UI Framework comes with interactive examples which document how to use its various UI components. These components are currently only implemented in CSS, but eventually they'll grow to involve JS as well. +### React components -When you build a UI using this framework (e.g. a plugin's UI), you can rest assured it will integrate seamlessly into the overall Kibana UI. +Here are the components you can import from the Framnework: -## How to create a new component +```javascript +import { + KuiButton, + KuiButtonGroup, + KuiButtonIcon, +} from '../path/to/ui_framework/components'; +``` -There are two steps to creating a new component: +## Creating components -1. Create the CSS for the component in `ui_framework/components`. -2. Document it with examples in `ui_framework/doc_site`. +There are four steps to creating a new component: -### Create the component CSS +1. Create the SCSS for the component in `ui_framework/components`. +2. Create the React portion of the component. +3. Document it with examples in `ui_framework/doc_site`. +4. Write tests. + +### Create component SCSS 1. Create a directory for your component in `ui_framework/components`. 2. In this directory, create `_{component name}.scss`. -3. _Optional:_ Create any other components that should be logically-grouped in this directory (see below). -4. Create an `_index.scss` file in this directory that import all of the new component SCSS files you created. +3. _Optional:_ Create any other components that should be [logically-grouped](#logically-grouped-components) +in this directory. +4. Create an `_index.scss` file in this directory that import all of the new component SCSS files +you created. 5. Import the `_index.scss` file into `ui_framework/components/index.scss`. This makes your styles available to Kibana and the UI Framework documentation. -#### Logically-grouped components +### Create the React component + +1. Create the React component(s) in the same directory as the related SCSS file(s). +2. Export these components from an `index.js` file. +3. Re-export these components from `ui_framework/components/index.js`. -If a component has subcomponents (e.g. ToolBar and ToolBarSearch), tightly-coupled components (e.g. Button and ButtonGroup), or you just want to group some related components together (e.g. TextInput, TextArea, and CheckBox), then they belong in the same logicaly grouping. In this case, you can create additional SCSS files for these components in the same component directory. +This makes your React component available for import into Kibana. ### Document the component with examples -1. Create a directory for your example in `ui_framework/doc_site/src/views`. Name it the name of the component. -2. Create a `{component name}_example.jsx` file inside the directory. You'll use this file to define the different examples for your component. +1. Create a directory for your example in `ui_framework/doc_site/src/views`. Name it the name of the +component. +2. Create a `{component name}_example.js` file inside the directory. You'll use this file to define +the different examples for your component. 3. Add the route to this file in `ui_framework/doc_site/src/services/routes/Routes.js`. -4. In the `.jsx` file you created, define examples which demonstrate the component. An example consists of a title, an optional description, an HTML file and an optional JavaScript file. It might help to refer to other examples to see how they're structured. +4. In the `{component name}_example.js` file you created, define examples which demonstrate the component and describe +its role from a UI perspective. -The complexity of the component should determine how many examples you need to create, and how complex they should be. In general, your examples should demonstrate: +The complexity of the component should determine how many examples you need to create, and how +complex they should be. In general, your examples should demonstrate: * The most common use-cases for the component. -* How the component handles edge cases, e.g. overflowing content, text-based vs. element-based content. +* How the component handles edge cases, e.g. overflowing content, text-based vs. element-based +content. * The various states of the component, e.g. disabled, selected, empty of content, error state. -## Writing CSS +### Test the component + +1. Create test files with the name pattern of `{component name}.test.js`. +2. Create your tests. +3. Run tests with `npm run uiFramework:test`. + +You can check how well the components have been covered +by the tests by viewing the generated report at `ui_framework/jest/report/index.html`. + +#### React component development tips + +You can run `npm run uiFramework:dev` to watch your files and automatically run the tests when you +make changes. Under this command, the tests will run faster than under `uiFramework:test` because +they'll only test the files you've changed -- the code coverage report won't be re-genereated, +however. + +## Principles + +### Logically-grouped components + +If a component has subcomponents (e.g. ToolBar and ToolBarSearch), tightly-coupled components (e.g. +Button and ButtonGroup), or you just want to group some related components together (e.g. TextInput, +TextArea, and CheckBox), then they belong in the same logicaly grouping. In this case, you can create +additional SCSS files for these components in the same component directory. + +### Writing CSS Check out our [CSS style guide](https://github.com/elastic/kibana/blob/master/style_guides/css_style_guide.md). @@ -55,7 +102,9 @@ Check out our [CSS style guide](https://github.com/elastic/kibana/blob/master/st ### Dynamic, interactive documentation -By having a "living style guide", we relieve our designers of the burden of creating and maintaining static style guides. This also makes it easier for our engineers to translate mockups, prototypes, and wireframes into products. +By having a "living style guide", we relieve our designers of the burden of creating and maintaining +static style guides. This also makes it easier for our engineers to translate mockups, prototypes, +and wireframes into products. ### Copy-pasteable UI @@ -63,13 +112,18 @@ Engineers can copy and paste sample code into their projects to quickly get reli ### Remove CSS from the day-to-day -The CSS portion of this framework means engineers don't need to spend mental cycles translating a design into CSS. These cycles can be spent on the things critical to the identity of the specific project they're working on, like architecture and business logic. +The CSS portion of this framework means engineers don't need to spend mental cycles translating a +design into CSS. These cycles can be spent on the things critical to the identity of the specific +project they're working on, like architecture and business logic. -Once this framework also provides JS components, engineers won't even need to _see_ CSS -- it will be encapsulated behind the JS components' interfaces. +If they use the React components, engineers won't even need to _see_ CSS -- it will be encapsulated +behind the React components' interfaces. ### More UI tests === fewer UI bugs -By covering our UI components with great unit tests and having those tests live within the framework itself, we can rest assured that our UI layer is tested and remove some of that burden from out integration/end-to-end tests. +By covering our UI components with great unit tests and having those tests live within the framework +itself, we can rest assured that our UI layer is tested and remove some of that burden from our +integration/end-to-end tests. ## Why not just use Bootstrap? @@ -84,7 +138,8 @@ We also gain the ability to fix some of the common issues with third-party CSS f * They have non-semantic markup. * They deeply nest their selectors. -For a more in-depth analysis of the problems with Bootstrap (and similar frameworks), check out this article and the links it has at the bottom: ["Bootstrap Bankruptcy"](http://www.matthewcopeland.me/blog/2013/11/04/bootstrap-bankruptcy/). +For a more in-depth analysis of the problems with Bootstrap (and similar frameworks), check out this +article and the links it has at the bottom: ["Bootstrap Bankruptcy"](http://www.matthewcopeland.me/blog/2013/11/04/bootstrap-bankruptcy/). ## Examples of other in-house UI frameworks diff --git a/ui_framework/components/button/__snapshots__/button.test.js.snap b/ui_framework/components/button/__snapshots__/button.test.js.snap new file mode 100644 index 0000000000000..ab8b452e7019b --- /dev/null +++ b/ui_framework/components/button/__snapshots__/button.test.js.snap @@ -0,0 +1,124 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`KuiButton Baseline HTML attributes are rendered 1`] = ` + +`; + +exports[`KuiButton Baseline is rendered 1`] = ` + +`; + +exports[`KuiButton Props children is rendered 1`] = ` + +`; + +exports[`KuiButton Props icon is rendered with children 1`] = ` + +`; + +exports[`KuiButton Props icon is rendered without children 1`] = ` + +`; + +exports[`KuiButton Props iconPosition moves the icon to the right 1`] = ` + +`; + +exports[`KuiButton Props isLoading doesn't render the icon prop 1`] = ` + +`; + +exports[`KuiButton Props isLoading renders a spinner 1`] = ` + +`; + +exports[`KuiButton Props type basic renders the basic class 1`] = ` + +`; + +exports[`KuiButton Props type danger renders the danger class 1`] = ` + +`; + +exports[`KuiButton Props type hollow renders the hollow class 1`] = ` + +`; + +exports[`KuiButton Props type primary renders the primary class 1`] = ` + +`; diff --git a/ui_framework/components/button/__snapshots__/link_button.test.js.snap b/ui_framework/components/button/__snapshots__/link_button.test.js.snap new file mode 100644 index 0000000000000..6935d87e61b79 --- /dev/null +++ b/ui_framework/components/button/__snapshots__/link_button.test.js.snap @@ -0,0 +1,125 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`KuiLinkButton Baseline HTML attributes are rendered (and disabled renders a class) 1`] = ` + + + +`; + +exports[`KuiLinkButton Baseline is rendered 1`] = ` + + + +`; + +exports[`KuiLinkButton Props children is rendered 1`] = ` + + + + Hello + + + +`; + +exports[`KuiLinkButton Props icon is rendered with children 1`] = ` + + + Icon + + Hello + + + +`; + +exports[`KuiLinkButton Props icon is rendered without children 1`] = ` + + + Icon + + +`; + +exports[`KuiLinkButton Props iconPosition moves the icon to the right 1`] = ` + + + + Hello + + Icon + + +`; + +exports[`KuiLinkButton Props isLoading doesn't render the icon prop 1`] = ` + + + + + +`; + +exports[`KuiLinkButton Props isLoading renders a spinner 1`] = ` + + + + + +`; + +exports[`KuiLinkButton Props type basic renders the basic class 1`] = ` + + + +`; + +exports[`KuiLinkButton Props type danger renders the danger class 1`] = ` + + + +`; + +exports[`KuiLinkButton Props type hollow renders the hollow class 1`] = ` + + + +`; + +exports[`KuiLinkButton Props type primary renders the primary class 1`] = ` + + + +`; diff --git a/ui_framework/components/button/__snapshots__/submit_button.test.js.snap b/ui_framework/components/button/__snapshots__/submit_button.test.js.snap new file mode 100644 index 0000000000000..2217b8383b5b3 --- /dev/null +++ b/ui_framework/components/button/__snapshots__/submit_button.test.js.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`KuiSubmitButton Baseline HTML attributes are rendered 1`] = ` + +`; + +exports[`KuiSubmitButton Baseline is rendered 1`] = ` + +`; + +exports[`KuiSubmitButton Props children is rendered as value 1`] = ` + +`; + +exports[`KuiSubmitButton Props type basic renders the basic class 1`] = ` + +`; + +exports[`KuiSubmitButton Props type danger renders the danger class 1`] = ` + +`; + +exports[`KuiSubmitButton Props type hollow renders the hollow class 1`] = ` + +`; + +exports[`KuiSubmitButton Props type primary renders the primary class 1`] = ` + +`; diff --git a/ui_framework/components/button/_button.scss b/ui_framework/components/button/_button.scss index 95f3b2734e9a6..8aa11b3409754 100644 --- a/ui_framework/components/button/_button.scss +++ b/ui_framework/components/button/_button.scss @@ -1,8 +1,10 @@ /** * 1. Setting to inline-block guarantees the same height when applied to both * button elements and anchor tags. - * 2. Disable for Angular. - * 3. Safari won't respect :enabled:active on links. + * 2. Safari won't respect :enabled:active on links. + * 3. disabled isn't a valid attribute for links, so we also need to use this class. + * 4. Links can be focused when they're "disabled", so at least make them look like they're not + * focused. */ .kuiButton { display: inline-block; /* 1 */ @@ -16,28 +18,34 @@ border: none; border-radius: $buttonBorderRadius; + &.kuiButton-isDisabled, /* 3 */ &:disabled { cursor: default; - pointer-events: none; /* 2 */ } - &:active { /* 3 */ + &:active:not(.kuiButton-isDisabled) { /* 2 */ transform: translateY(1px); } &:focus { - @include focus; + outline: none; /* 4 */ + } + + &:focus:not(.kuiButton-isDisabled) { + @include focus; /* 4 */ } } .kuiButton--iconText { .kuiButton__icon { - &:first-child { - margin-right: 4px; + $iconSpacing: 8px; + + &:first-child:not(:only-child) { + margin-right: $iconSpacing; } - &:last-child { - margin-left: 4px; + &:last-child:not(:only-child) { + margin-left: $iconSpacing; } } } @@ -51,16 +59,17 @@ background-color: #F2F2F2; // Goes before hover, so that hover can override it. - &:focus { + &:focus:not(.kuiButton-isDisabled) { color: #5a5a5a !important; /* 1 */ } - &:hover, /* 2 */ - &:active { /* 2 */ + &:hover:not(.kuiButton-isDisabled), /* 2 */ + &:active:not(.kuiButton-isDisabled) { /* 2 */ color: #ffffff !important; /* 1 */ background-color: #9B9B9B !important; } + &.kuiButton-isDisabled, &:disabled { color: #9B9B9B; } @@ -74,17 +83,18 @@ color: #FFFFFF; background-color: #6EADC1; - &:hover, /* 2 */ - &:active { /* 2 */ + &:hover:not(.kuiButton-isDisabled), /* 2 */ + &:active:not(.kuiButton-isDisabled) { /* 2 */ color: #FFFFFF !important; /* 1 */ background-color: #006E8A; } + &.kuiButton-isDisabled, &:disabled { background-color: #B6D6E0; } - &:focus { + &:focus:not(.kuiButton-isDisabled) { color: #FFFFFF !important; /* 1 */ } } @@ -97,17 +107,18 @@ color: #FFFFFF; background-color: #D76051; - &:hover, /* 2 */ - &:active { /* 2 */ + &:hover:not(.kuiButton-isDisabled), /* 2 */ + &:active:not(.kuiButton-isDisabled) { /* 2 */ color: #FFFFFF !important; /* 1 */ background-color: #A52E1F; } + &.kuiButton-isDisabled, &:disabled { background-color: #efc0ba; } - &:focus { + &:focus:not(.kuiButton-isDisabled) { @include focus($focusDangerColor); color: #FFFFFF !important; /* 1 */ } @@ -122,12 +133,13 @@ color: $linkColor !important; /* 2 */ background-color: transparent; - &:hover, /* 3 */ - &:active { /* 3 */ + &:hover:not(.kuiButton-isDisabled), /* 3 */ + &:active:not(.kuiButton-isDisabled) { /* 3 */ color: $linkHoverColor !important; /* 1 */ text-decoration: underline; } + &.kuiButton-isDisabled, &:disabled { color: #dddddd !important; /* 1 */ } diff --git a/ui_framework/components/button/_index.scss b/ui_framework/components/button/_index.scss index 981ea6923233f..92ab9ca9a9e10 100644 --- a/ui_framework/components/button/_index.scss +++ b/ui_framework/components/button/_index.scss @@ -1,2 +1,2 @@ @import "button"; -@import "button_group"; +@import "button_group/button_group"; diff --git a/ui_framework/components/button/button.js b/ui_framework/components/button/button.js new file mode 100644 index 0000000000000..13e1481e3828a --- /dev/null +++ b/ui_framework/components/button/button.js @@ -0,0 +1,181 @@ +import React, { + PropTypes, +} from 'react'; +import classNames from 'classnames'; + +import { KuiButtonIcon } from './button_icon/button_icon'; + +const BUTTON_TYPES = [ + 'basic', + 'hollow', + 'danger', + 'primary', +]; + +const ICON_POSITIONS = [ + 'left', + 'right', +]; + +const DEFAULT_ICON_POSITION = 'left'; + +const buttonTypeToClassNameMap = { + basic: 'kuiButton--basic', + hollow: 'kuiButton--hollow', + danger: 'kuiButton--danger', + primary: 'kuiButton--primary', +}; + +const getClassName = ({ className, type, hasIcon = false, isDisabled }) => + classNames('kuiButton', className, buttonTypeToClassNameMap[type], { + 'kuiButton--iconText': hasIcon, + 'kuiButton-isDisabled': isDisabled, + }); + +const ContentWithIcon = ({ children, icon, iconPosition, isLoading }) => { + const iconOrLoading = isLoading + ? + : icon; + + // We need to wrap the children so that the icon's :first-child etc. pseudo-selectors get applied + // correctly. + const wrappedChildren = children ? {children} : undefined; + + switch(iconPosition) { + case 'left': + return ( + + {iconOrLoading} + {wrappedChildren} + + ); + + case 'right': + return ( + + {wrappedChildren} + {iconOrLoading} + + ); + } +}; + +const KuiButton = ({ + isLoading, + iconPosition = DEFAULT_ICON_POSITION, + className, + disabled, + type, + icon, + children, + ...rest +}) => { + return ( + + ); +}; + +KuiButton.propTypes = { + icon: PropTypes.node, + iconPosition: PropTypes.oneOf(ICON_POSITIONS), + children: PropTypes.node, + isLoading: PropTypes.bool, + type: PropTypes.oneOf(BUTTON_TYPES), + className: PropTypes.string, +}; + +const KuiLinkButton = ({ + isLoading, + icon, + iconPosition = DEFAULT_ICON_POSITION, + className, + disabled, + type, + children, + ...rest +}) => { + const onClick = e => { + if (disabled) { + e.preventDefault(); + } + }; + + return ( + + + {children} + + + ); +}; + +KuiLinkButton.propTypes = { + icon: PropTypes.node, + iconPosition: PropTypes.oneOf(ICON_POSITIONS), + isLoading: PropTypes.bool, + type: PropTypes.oneOf(BUTTON_TYPES), + className: PropTypes.string, + children: PropTypes.node, +}; + +const KuiSubmitButton = ({ + className, + disabled, + type, + children, + ...rest +}) => { + // NOTE: The `input` element is a void element and can't contain children. + return ( + + ); +}; + +KuiSubmitButton.propTypes = { + children: PropTypes.string, + type: PropTypes.oneOf(BUTTON_TYPES), + className: PropTypes.string, +}; + +export { + BUTTON_TYPES, + KuiButton, + KuiLinkButton, + KuiSubmitButton, +}; diff --git a/ui_framework/components/button/button.test.js b/ui_framework/components/button/button.test.js new file mode 100644 index 0000000000000..a5458777015e1 --- /dev/null +++ b/ui_framework/components/button/button.test.js @@ -0,0 +1,143 @@ +import React from 'react'; +import { render, shallow } from 'enzyme'; +import sinon from 'sinon'; + +import { + BUTTON_TYPES, + KuiButton, +} from './button'; + +describe('KuiButton', () => { + describe('Baseline', () => { + test('is rendered', () => { + const $button = render( + + ); + + expect($button) + .toMatchSnapshot(); + }); + + test('HTML attributes are rendered', () => { + const $button = render( + + ); + + expect($button) + .toMatchSnapshot(); + }); + }); + + describe('Props', () => { + describe('type', () => { + BUTTON_TYPES.forEach(type => { + describe(type, () => { + test(`renders the ${type} class`, () => { + const $button = render(); + expect($button).toMatchSnapshot(); + }); + }); + }); + }); + + describe('icon', () => { + test('is rendered with children', () => { + const $button = render( + + Hello + + ); + + expect($button) + .toMatchSnapshot(); + }); + + test('is rendered without children', () => { + const $button = render( + + ); + + expect($button) + .toMatchSnapshot(); + }); + }); + + describe('iconPosition', () => { + test('moves the icon to the right', () => { + const $button = render( + + Hello + + ); + + expect($button) + .toMatchSnapshot(); + }); + }); + + describe('children', () => { + test('is rendered', () => { + const $button = render( + + Hello + + ); + + expect($button) + .toMatchSnapshot(); + }); + }); + + describe('onClick', () => { + test(`isn't called upon instantiation`, () => { + const onClickHandler = sinon.stub(); + + shallow( + + ); + + sinon.assert.notCalled(onClickHandler); + }); + + test('is called when the button is clicked', () => { + const onClickHandler = sinon.stub(); + + const $button = shallow( + + ); + + $button.simulate('click'); + + sinon.assert.calledOnce(onClickHandler); + }); + }); + + describe('isLoading', () => { + test('renders a spinner', () => { + const $button = render( + + ); + + expect($button) + .toMatchSnapshot(); + }); + + test(`doesn't render the icon prop`, () => { + const $button = render( + + ); + + expect($button) + .toMatchSnapshot(); + }); + }); + }); +}); diff --git a/ui_framework/components/button/button_group/__snapshots__/button_group.test.js.snap b/ui_framework/components/button/button_group/__snapshots__/button_group.test.js.snap new file mode 100644 index 0000000000000..1bb2afd901f39 --- /dev/null +++ b/ui_framework/components/button/button_group/__snapshots__/button_group.test.js.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`KuiButtonGroup Baseline is rendered 1`] = ` +
+`; + +exports[`KuiButtonGroup Props children is rendered 1`] = ` +
+ Hello +
+`; + +exports[`KuiButtonGroup Props isUnited renders the united class 1`] = ` +
+`; diff --git a/ui_framework/components/button/_button_group.scss b/ui_framework/components/button/button_group/_button_group.scss similarity index 100% rename from ui_framework/components/button/_button_group.scss rename to ui_framework/components/button/button_group/_button_group.scss diff --git a/ui_framework/components/button/button_group/button_group.js b/ui_framework/components/button/button_group/button_group.js new file mode 100644 index 0000000000000..71748db4f9e1b --- /dev/null +++ b/ui_framework/components/button/button_group/button_group.js @@ -0,0 +1,24 @@ +import React, { + PropTypes, +} from 'react'; + +import classNames from 'classnames'; + +const KuiButtonGroup = props => { + const classes = classNames('kuiButtonGroup', { + 'kuiButtonGroup--united': props.isUnited, + }); + + return ( +
+ {props.children} +
+ ); +}; + +KuiButtonGroup.propTypes = { + children: PropTypes.node, + isUnited: PropTypes.bool, +}; + +export { KuiButtonGroup }; diff --git a/ui_framework/components/button/button_group/button_group.test.js b/ui_framework/components/button/button_group/button_group.test.js new file mode 100644 index 0000000000000..4d7ee5b90e6c0 --- /dev/null +++ b/ui_framework/components/button/button_group/button_group.test.js @@ -0,0 +1,43 @@ +import React from 'react'; +import { render } from 'enzyme'; + +import { KuiButtonGroup } from './button_group'; + +describe('KuiButtonGroup', () => { + describe('Baseline', () => { + test('is rendered', () => { + const $buttonGroup = render( + + ); + + expect($buttonGroup) + .toMatchSnapshot(); + }); + }); + + describe('Props', () => { + describe('children', () => { + test('is rendered', () => { + const $buttonGroup = render( + + Hello + + ); + + expect($buttonGroup) + .toMatchSnapshot(); + }); + }); + + describe('isUnited', () => { + test('renders the united class', () => { + const $buttonGroup = render( + + ); + + expect($buttonGroup) + .toMatchSnapshot(); + }); + }); + }); +}); diff --git a/ui_framework/components/button/button_icon/__snapshots__/button_icon.test.js.snap b/ui_framework/components/button/button_icon/__snapshots__/button_icon.test.js.snap new file mode 100644 index 0000000000000..6c63973e9d240 --- /dev/null +++ b/ui_framework/components/button/button_icon/__snapshots__/button_icon.test.js.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`KuiButtonIcon Baseline is rendered 1`] = ` + +`; + +exports[`KuiButtonIcon Props className renders the classes 1`] = ` + +`; + +exports[`KuiButtonIcon Props type create renders the create class 1`] = ` + +`; + +exports[`KuiButtonIcon Props type delete renders the delete class 1`] = ` + +`; + +exports[`KuiButtonIcon Props type loading renders the loading class 1`] = ` + +`; + +exports[`KuiButtonIcon Props type next renders the next class 1`] = ` + +`; + +exports[`KuiButtonIcon Props type previous renders the previous class 1`] = ` + +`; diff --git a/ui_framework/components/button/button_icon/button_icon.js b/ui_framework/components/button/button_icon/button_icon.js new file mode 100644 index 0000000000000..db82e91a7d9e9 --- /dev/null +++ b/ui_framework/components/button/button_icon/button_icon.js @@ -0,0 +1,41 @@ +import React, { + PropTypes, +} from 'react'; + +import classNames from 'classnames'; + +const ICON_TYPES = [ + 'create', + 'delete', + 'previous', + 'next', + 'loading', +]; + +const KuiButtonIcon = props => { + const typeToClassNameMap = { + create: 'fa-plus', + delete: 'fa-trash', + previous: 'fa-chevron-left', + next: 'fa-chevron-right', + loading: 'fa-spinner fa-spin', + }; + + const iconClasses = classNames('kuiButton__icon kuiIcon', props.className, { + [typeToClassNameMap[props.type]]: props.type, + }); + + return ( + + ); +}; + +KuiButtonIcon.propTypes = { + type: PropTypes.oneOf(ICON_TYPES), + className: PropTypes.string, +}; + +export { + ICON_TYPES, + KuiButtonIcon, +}; diff --git a/ui_framework/components/button/button_icon/button_icon.test.js b/ui_framework/components/button/button_icon/button_icon.test.js new file mode 100644 index 0000000000000..243f934785e06 --- /dev/null +++ b/ui_framework/components/button/button_icon/button_icon.test.js @@ -0,0 +1,46 @@ +import React from 'react'; +import { + render, +} from 'enzyme'; + +import { + ICON_TYPES, + KuiButtonIcon, +} from './button_icon'; + +describe('KuiButtonIcon', () => { + describe('Baseline', () => { + test('is rendered', () => { + const $buttonIcon = render( + + ); + + expect($buttonIcon) + .toMatchSnapshot(); + }); + }); + + describe('Props', () => { + describe('type', () => { + ICON_TYPES.forEach(type => { + describe(type, () => { + test(`renders the ${type} class`, () => { + const $buttonIcon = render(); + expect($buttonIcon).toMatchSnapshot(); + }); + }); + }); + }); + + describe('className', () => { + test('renders the classes', () => { + const $buttonIcon = render( + + ); + + expect($buttonIcon) + .toMatchSnapshot(); + }); + }); + }); +}); diff --git a/ui_framework/components/button/index.js b/ui_framework/components/button/index.js new file mode 100644 index 0000000000000..5cc6c07bfc39c --- /dev/null +++ b/ui_framework/components/button/index.js @@ -0,0 +1,7 @@ +export { + KuiButton, + KuiLinkButton, + KuiSubmitButton, +} from './button'; +export { KuiButtonIcon } from './button_icon/button_icon'; +export { KuiButtonGroup } from './button_group/button_group'; diff --git a/ui_framework/components/button/link_button.test.js b/ui_framework/components/button/link_button.test.js new file mode 100644 index 0000000000000..fb9dcdb260e0b --- /dev/null +++ b/ui_framework/components/button/link_button.test.js @@ -0,0 +1,154 @@ +import React from 'react'; +import { render } from 'enzyme'; + +import { + KuiLinkButton, +} from './button'; + +describe('KuiLinkButton', () => { + describe('Baseline', () => { + test('is rendered', () => { + const $button = render( + + ); + + expect($button) + .toMatchSnapshot(); + }); + + test('HTML attributes are rendered (and disabled renders a class)', () => { + const $button = render( + + ); + + expect($button) + .toMatchSnapshot(); + }); + }); + + describe('Props', () => { + describe('type', () => { + describe('basic', () => { + test('renders the basic class', () => { + const $button = render( + + ); + + expect($button) + .toMatchSnapshot(); + }); + }); + + describe('hollow', () => { + test('renders the hollow class', () => { + const $button = render( + + ); + + expect($button) + .toMatchSnapshot(); + }); + }); + + describe('danger', () => { + test('renders the danger class', () => { + const $button = render( + + ); + + expect($button) + .toMatchSnapshot(); + }); + }); + + describe('primary', () => { + test('renders the primary class', () => { + const $button = render( + + ); + + expect($button) + .toMatchSnapshot(); + }); + }); + }); + + describe('icon', () => { + test('is rendered with children', () => { + const $button = render( + + Hello + + ); + + expect($button) + .toMatchSnapshot(); + }); + + test('is rendered without children', () => { + const $button = render( + + ); + + expect($button) + .toMatchSnapshot(); + }); + }); + + describe('iconPosition', () => { + test('moves the icon to the right', () => { + const $button = render( + + Hello + + ); + + expect($button) + .toMatchSnapshot(); + }); + }); + + describe('children', () => { + test('is rendered', () => { + const $button = render( + + Hello + + ); + + expect($button) + .toMatchSnapshot(); + }); + }); + + describe('isLoading', () => { + test('renders a spinner', () => { + const $button = render( + + ); + + expect($button) + .toMatchSnapshot(); + }); + + test(`doesn't render the icon prop`, () => { + const $button = render( + + ); + + expect($button) + .toMatchSnapshot(); + }); + }); + }); +}); diff --git a/ui_framework/components/button/submit_button.test.js b/ui_framework/components/button/submit_button.test.js new file mode 100644 index 0000000000000..81b74cc4ced30 --- /dev/null +++ b/ui_framework/components/button/submit_button.test.js @@ -0,0 +1,119 @@ +import React from 'react'; +import { render, shallow } from 'enzyme'; +import sinon from 'sinon'; + +import { + KuiSubmitButton, +} from './button'; + +describe('KuiSubmitButton', () => { + describe('Baseline', () => { + test('is rendered', () => { + const $button = render( + + ); + + expect($button) + .toMatchSnapshot(); + }); + + test('HTML attributes are rendered', () => { + const $button = render( + + ); + + expect($button) + .toMatchSnapshot(); + }); + }); + + describe('Props', () => { + describe('type', () => { + describe('basic', () => { + test('renders the basic class', () => { + const $button = render( + + ); + + expect($button) + .toMatchSnapshot(); + }); + }); + + describe('hollow', () => { + test('renders the hollow class', () => { + const $button = render( + + ); + + expect($button) + .toMatchSnapshot(); + }); + }); + + describe('danger', () => { + test('renders the danger class', () => { + const $button = render( + + ); + + expect($button) + .toMatchSnapshot(); + }); + }); + + describe('primary', () => { + test('renders the primary class', () => { + const $button = render( + + ); + + expect($button) + .toMatchSnapshot(); + }); + }); + }); + + describe('children', () => { + test('is rendered as value', () => { + const $button = render( + + Hello + + ); + + expect($button) + .toMatchSnapshot(); + }); + }); + + describe('onClick', () => { + test(`isn't called upon instantiation`, () => { + const onClickHandler = sinon.stub(); + + shallow( + + ); + + sinon.assert.notCalled(onClickHandler); + }); + + test('is called when the button is clicked', () => { + const onClickHandler = sinon.stub(); + + const $button = shallow( + + ); + + $button.simulate('click'); + + sinon.assert.calledOnce(onClickHandler); + }); + }); + }); +}); diff --git a/ui_framework/components/index.js b/ui_framework/components/index.js new file mode 100644 index 0000000000000..105bb302e746e --- /dev/null +++ b/ui_framework/components/index.js @@ -0,0 +1,7 @@ +export { + KuiButton, + KuiButtonGroup, + KuiButtonIcon, + KuiLinkButton, + KuiSubmitButton, +} from './button'; diff --git a/ui_framework/dist/ui_framework.css b/ui_framework/dist/ui_framework.css index 673a921e1801b..306d6444599c9 100644 --- a/ui_framework/dist/ui_framework.css +++ b/ui_framework/dist/ui_framework.css @@ -119,8 +119,10 @@ body { /** * 1. Setting to inline-block guarantees the same height when applied to both * button elements and anchor tags. - * 2. Disable for Angular. - * 3. Safari won't respect :enabled:active on links. + * 2. Safari won't respect :enabled:active on links. + * 3. disabled isn't a valid attribute for links, so we also need to use this class. + * 4. Links can be focused when they're "disabled", so at least make them look like they're not + * focused. */ .kuiButton { display: inline-block; @@ -136,27 +138,29 @@ body { text-decoration: none; border: none; border-radius: 4px; } - .kuiButton:disabled { - cursor: default; - pointer-events: none; - /* 2 */ } - .kuiButton:active { - /* 3 */ + .kuiButton.kuiButton-isDisabled, .kuiButton:disabled { + cursor: default; } + .kuiButton:active:not(.kuiButton-isDisabled) { + /* 2 */ -webkit-transform: translateY(1px); transform: translateY(1px); } .kuiButton:focus { + outline: none; + /* 4 */ } + .kuiButton:focus:not(.kuiButton-isDisabled) { z-index: 1; /* 1 */ outline: none !important; /* 2 */ box-shadow: 0 0 0 1px #ffffff, 0 0 0 2px #6EADC1; - /* 3 */ } + /* 3 */ + /* 4 */ } -.kuiButton--iconText .kuiButton__icon:first-child { - margin-right: 4px; } +.kuiButton--iconText .kuiButton__icon:first-child:not(:only-child) { + margin-right: 8px; } -.kuiButton--iconText .kuiButton__icon:last-child { - margin-left: 4px; } +.kuiButton--iconText .kuiButton__icon:last-child:not(:only-child) { + margin-left: 8px; } /** * 1. Override Bootstrap. @@ -165,15 +169,15 @@ body { .kuiButton--basic { color: #5a5a5a; background-color: #F2F2F2; } - .kuiButton--basic:focus { + .kuiButton--basic:focus:not(.kuiButton-isDisabled) { color: #5a5a5a !important; /* 1 */ } - .kuiButton--basic:hover, .kuiButton--basic:active { + .kuiButton--basic:hover:not(.kuiButton-isDisabled), .kuiButton--basic:active:not(.kuiButton-isDisabled) { /* 2 */ color: #ffffff !important; /* 1 */ background-color: #9B9B9B !important; } - .kuiButton--basic:disabled { + .kuiButton--basic.kuiButton-isDisabled, .kuiButton--basic:disabled { color: #9B9B9B; } /** @@ -183,14 +187,14 @@ body { .kuiButton--primary { color: #FFFFFF; background-color: #6EADC1; } - .kuiButton--primary:hover, .kuiButton--primary:active { + .kuiButton--primary:hover:not(.kuiButton-isDisabled), .kuiButton--primary:active:not(.kuiButton-isDisabled) { /* 2 */ color: #FFFFFF !important; /* 1 */ background-color: #006E8A; } - .kuiButton--primary:disabled { + .kuiButton--primary.kuiButton-isDisabled, .kuiButton--primary:disabled { background-color: #B6D6E0; } - .kuiButton--primary:focus { + .kuiButton--primary:focus:not(.kuiButton-isDisabled) { color: #FFFFFF !important; /* 1 */ } @@ -201,14 +205,14 @@ body { .kuiButton--danger { color: #FFFFFF; background-color: #D76051; } - .kuiButton--danger:hover, .kuiButton--danger:active { + .kuiButton--danger:hover:not(.kuiButton-isDisabled), .kuiButton--danger:active:not(.kuiButton-isDisabled) { /* 2 */ color: #FFFFFF !important; /* 1 */ background-color: #A52E1F; } - .kuiButton--danger:disabled { + .kuiButton--danger.kuiButton-isDisabled, .kuiButton--danger:disabled { background-color: #efc0ba; } - .kuiButton--danger:focus { + .kuiButton--danger:focus:not(.kuiButton-isDisabled) { z-index: 1; /* 1 */ outline: none !important; @@ -227,12 +231,12 @@ body { color: #3CAED2 !important; /* 2 */ background-color: transparent; } - .kuiButton--hollow:hover, .kuiButton--hollow:active { + .kuiButton--hollow:hover:not(.kuiButton-isDisabled), .kuiButton--hollow:active:not(.kuiButton-isDisabled) { /* 3 */ color: #006E8A !important; /* 1 */ text-decoration: underline; } - .kuiButton--hollow:disabled { + .kuiButton--hollow.kuiButton-isDisabled, .kuiButton--hollow:disabled { color: #dddddd !important; /* 1 */ } diff --git a/ui_framework/doc_site/src/components/guide_demo/guide_demo.jsx b/ui_framework/doc_site/src/components/guide_demo/guide_demo.jsx index 1d48e57a9b1ee..11dd0953d8a90 100644 --- a/ui_framework/doc_site/src/components/guide_demo/guide_demo.jsx +++ b/ui_framework/doc_site/src/components/guide_demo/guide_demo.jsx @@ -15,16 +15,21 @@ export class GuideDemo extends Component { } update() { - // Inject HTML + // We'll just render the children if we have them. + if (this.props.children) { + return; + } + + // Inject HTML. this.content.innerHTML = this.props.html; - // Inject JS + // Inject JS. const js = document.createElement('script'); js.type = 'text/javascript'; js.innerHTML = this.props.js; this.content.appendChild(js); - // Inject CSS + // Inject CSS. const css = document.createElement('style'); css.innerHTML = this.props.css; this.content.appendChild(css); @@ -38,12 +43,14 @@ export class GuideDemo extends Component { return (
(this.content = c)}> + {this.props.children}
); } } GuideDemo.propTypes = { + children: PropTypes.node, js: PropTypes.string.isRequired, html: PropTypes.string.isRequired, css: PropTypes.string.isRequired, diff --git a/ui_framework/doc_site/src/services/index.js b/ui_framework/doc_site/src/services/index.js index fdebddbf05265..eaf038ac29727 100644 --- a/ui_framework/doc_site/src/services/index.js +++ b/ui_framework/doc_site/src/services/index.js @@ -1,12 +1,9 @@ +export { renderToHtml } from './string/render_to_html'; -export * from './example/create_example'; export { default as createExample } from './example/create_example'; -export * from './js_injector/js_injector'; export { default as JsInjector } from './js_injector/js_injector'; -export * from './routes/routes'; export { default as Routes } from './routes/routes'; -export * from './string/slugify'; export { default as Slugify } from './string/slugify'; diff --git a/ui_framework/doc_site/src/services/string/render_to_html.js b/ui_framework/doc_site/src/services/string/render_to_html.js new file mode 100644 index 0000000000000..95c1bc140f5b5 --- /dev/null +++ b/ui_framework/doc_site/src/services/string/render_to_html.js @@ -0,0 +1,17 @@ +import React from 'react'; + +import { + render, +} from 'enzyme'; + +import html from 'html'; + +export function renderToHtml(componentReference, props = {}) { + // Create the React element, render it and get its HTML, then format it prettily. + const element = React.createElement(componentReference, props); + const htmlString = render(element).html(); + return html.prettyPrint(htmlString, { + indent_size: 2, + unformatted: [], // Expand all tags, including spans + }); +} diff --git a/ui_framework/doc_site/src/store/index.js b/ui_framework/doc_site/src/store/index.js index eb667cd7bbe29..0ab30476a2dbf 100644 --- a/ui_framework/doc_site/src/store/index.js +++ b/ui_framework/doc_site/src/store/index.js @@ -1,5 +1,5 @@ export function getIsCodeViewerOpen(state) { - return state.codeViewer.isOpen + return state.codeViewer.isOpen; } export function getSections(state) { diff --git a/ui_framework/doc_site/src/views/app_view.jsx b/ui_framework/doc_site/src/views/app_view.jsx index e47d2df0e6ed7..41ef555d5c6cc 100644 --- a/ui_framework/doc_site/src/views/app_view.jsx +++ b/ui_framework/doc_site/src/views/app_view.jsx @@ -15,7 +15,7 @@ import { } from '../components'; // Inject version into header. -const pkg = require('json!../../../../package.json'); +const pkg = require('../../../../package.json'); export class AppView extends Component { constructor(props) { diff --git a/ui_framework/doc_site/src/views/button/button_basic.html b/ui_framework/doc_site/src/views/button/button_basic.html deleted file mode 100644 index 8dd4691218d93..0000000000000 --- a/ui_framework/doc_site/src/views/button/button_basic.html +++ /dev/null @@ -1,9 +0,0 @@ - - -
- - diff --git a/ui_framework/doc_site/src/views/button/button_basic.js b/ui_framework/doc_site/src/views/button/button_basic.js new file mode 100644 index 0000000000000..2dcd6357ba23e --- /dev/null +++ b/ui_framework/doc_site/src/views/button/button_basic.js @@ -0,0 +1,26 @@ +import React from 'react'; + +import { + KuiButton, +} from '../../../../components'; + +export default () => ( +
+ window.alert('Button clicked')} + > + Basic button + + +
+ + window.alert('Button clicked')} + disabled + > + Basic button, disabled + +
+); diff --git a/ui_framework/doc_site/src/views/button/button_danger.html b/ui_framework/doc_site/src/views/button/button_danger.html deleted file mode 100644 index bd9a40e315036..0000000000000 --- a/ui_framework/doc_site/src/views/button/button_danger.html +++ /dev/null @@ -1,9 +0,0 @@ - - -
- - diff --git a/ui_framework/doc_site/src/views/button/button_danger.js b/ui_framework/doc_site/src/views/button/button_danger.js new file mode 100644 index 0000000000000..7650dfe989ab5 --- /dev/null +++ b/ui_framework/doc_site/src/views/button/button_danger.js @@ -0,0 +1,23 @@ +import React from 'react'; + +import { + KuiButton, +} from '../../../../components'; + +export default () => ( +
+ + Danger button + + +
+ + + Danger button, disabled + +
+ +); diff --git a/ui_framework/doc_site/src/views/button/button_elements.html b/ui_framework/doc_site/src/views/button/button_elements.html deleted file mode 100644 index aa0bd55bd61ed..0000000000000 --- a/ui_framework/doc_site/src/views/button/button_elements.html +++ /dev/null @@ -1,17 +0,0 @@ - - -  - - - -  - - - Anchor element - diff --git a/ui_framework/doc_site/src/views/button/button_elements.js b/ui_framework/doc_site/src/views/button/button_elements.js new file mode 100644 index 0000000000000..2891b20f63699 --- /dev/null +++ b/ui_framework/doc_site/src/views/button/button_elements.js @@ -0,0 +1,57 @@ +import React from 'react'; + +import { + KuiButton, + KuiLinkButton, + KuiSubmitButton, +} from '../../../../components'; + +export default () => ( +
+ + Button element + + +   + +
{ + e.preventDefault(); + window.alert('Submit'); + }}> + + Submit input element + +
+ + +
{ + e.preventDefault(); + window.alert('Submit'); + }}> + + Submit input element, disabled + +
+ +   + + + Anchor element + + +   + + + Anchor element, disabled + +
+); diff --git a/ui_framework/doc_site/src/views/button/button_example.jsx b/ui_framework/doc_site/src/views/button/button_example.jsx index 4b8db83c278c7..5cbfb1da45792 100644 --- a/ui_framework/doc_site/src/views/button/button_example.jsx +++ b/ui_framework/doc_site/src/views/button/button_example.jsx @@ -3,6 +3,8 @@ import React, { PropTypes, } from 'react'; +import { renderToHtml } from '../../services'; + import { GuideDemo, GuideLink, @@ -12,21 +14,54 @@ import { GuideText, } from '../../components'; -const basicHtml = require('./button_basic.html'); -const hollowHtml = require('./button_hollow.html'); -const primaryHtml = require('./button_primary.html'); -const dangerHtml = require('./button_danger.html'); -const withIconHtml = require('./button_with_icon.html'); -const groupHtml = require('./button_group.html'); -const groupUnitedHtml = require('./button_group_united.html'); -const inToolBarHtml = require('./buttons_in_tool_bar.html'); -const elementsHtml = require('./button_elements.html'); +import Basic from './button_basic'; +const basicSource = require('!!raw!./button_basic'); +const basicHtml = renderToHtml(Basic); + +import Hollow from './button_hollow'; +const hollowSource = require('!!raw!./button_hollow'); +const hollowHtml = renderToHtml(Hollow); + +import Primary from './button_primary'; +const primarySource = require('!!raw!./button_primary'); +const primaryHtml = renderToHtml(Primary); + +import Danger from './button_danger'; +const dangerSource = require('!!raw!./button_danger'); +const dangerHtml = renderToHtml(Danger); + +import Loading from './button_loading'; +const loadingSource = require('!!raw!./button_loading'); +const loadingHtml = renderToHtml(Loading, { isLoading: true }); + +import WithIcon from './button_with_icon'; +const withIconSource = require('!!raw!./button_with_icon'); +const withIconHtml = renderToHtml(WithIcon); + +import ButtonGroup from './button_group'; +const buttonGroupSource = require('!!raw!./button_group'); +const buttonGroupHtml = renderToHtml(ButtonGroup); + +import ButtonGroupUnited from './button_group_united'; +const buttonGroupUnitedSource = require('!!raw!./button_group_united'); +const buttonGroupUnitedHtml = renderToHtml(ButtonGroupUnited); + +import InToolBar from './buttons_in_tool_bar'; +const inToolBarSource = require('!!raw!./buttons_in_tool_bar'); +const inToolBarHtml = renderToHtml(InToolBar); + +import Elements from './button_elements'; +const elementsSource = require('!!raw!./button_elements'); +const elementsHtml = renderToHtml(Elements); export default props => ( ( Use the basic Button in most situations. - + + + ( Use the hollow Button when presenting a neutral action, e.g. a "Cancel" button. - + + + ( need to present more than one of these at a time. - + + + ( Danger Buttons represent irreversible, potentially regrettable actions. - + + + + + + + + + - You can toss an icon into a Button, with or without text. + You can toss an icon into a Button, with or without text. You can also use a predefined icon + or specify custom icon classes. - + + + - - - - - + + + @@ -138,14 +203,17 @@ export default props => ( removed. - + + + ( This example verifies that Buttons are legible against the ToolBar's background. - + + + ( You can create a Button using a button element, link, or input[type="submit"]. - + + + ); diff --git a/ui_framework/doc_site/src/views/button/button_group.html b/ui_framework/doc_site/src/views/button/button_group.html deleted file mode 100644 index 842877133de98..0000000000000 --- a/ui_framework/doc_site/src/views/button/button_group.html +++ /dev/null @@ -1,19 +0,0 @@ -
- - - -
- -
- -
- -
diff --git a/ui_framework/doc_site/src/views/button/button_group.js b/ui_framework/doc_site/src/views/button/button_group.js new file mode 100644 index 0000000000000..36bf80bc7e292 --- /dev/null +++ b/ui_framework/doc_site/src/views/button/button_group.js @@ -0,0 +1,32 @@ +import React from 'react'; + +import { + KuiButton, + KuiButtonGroup, +} from '../../../../components'; + +export default () => ( +
+ + + Cancel + + + + Duplicate + + + + Save + + + +
+ + + + Button group with one button + + +
+); diff --git a/ui_framework/doc_site/src/views/button/button_group_united.html b/ui_framework/doc_site/src/views/button/button_group_united.html deleted file mode 100644 index 7f5b07836c805..0000000000000 --- a/ui_framework/doc_site/src/views/button/button_group_united.html +++ /dev/null @@ -1,24 +0,0 @@ -
- - - -
- -
- -
- - -
diff --git a/ui_framework/doc_site/src/views/button/button_group_united.js b/ui_framework/doc_site/src/views/button/button_group_united.js new file mode 100644 index 0000000000000..af05596ae636f --- /dev/null +++ b/ui_framework/doc_site/src/views/button/button_group_united.js @@ -0,0 +1,39 @@ +import React from 'react'; + +import { + KuiButton, + KuiButtonGroup, + KuiButtonIcon, +} from '../../../../components'; + +export default () => ( +
+ + + Option A + + + + Option B + + + + Option C + + + +
+ + + } + /> + + } + /> + +
+); diff --git a/ui_framework/doc_site/src/views/button/button_hollow.html b/ui_framework/doc_site/src/views/button/button_hollow.html deleted file mode 100644 index 0657a04f0aa4a..0000000000000 --- a/ui_framework/doc_site/src/views/button/button_hollow.html +++ /dev/null @@ -1,9 +0,0 @@ - - -
- - diff --git a/ui_framework/doc_site/src/views/button/button_hollow.js b/ui_framework/doc_site/src/views/button/button_hollow.js new file mode 100644 index 0000000000000..6532e6af1eaf9 --- /dev/null +++ b/ui_framework/doc_site/src/views/button/button_hollow.js @@ -0,0 +1,22 @@ +import React from 'react'; + +import { + KuiButton, +} from '../../../../components'; + +export default () => ( +
+ + Hollow button + + +
+ + + Hollow button, disabled + +
+); diff --git a/ui_framework/doc_site/src/views/button/button_loading.js b/ui_framework/doc_site/src/views/button/button_loading.js new file mode 100644 index 0000000000000..6b140bb5983e3 --- /dev/null +++ b/ui_framework/doc_site/src/views/button/button_loading.js @@ -0,0 +1,59 @@ +import React, { + Component, +} from 'react'; + +import { + KuiButtonIcon, + KuiButton, +} from '../../../../components'; + +export default class LoadingButton extends Component { + constructor(props) { + super(); + + this.state = { + isLoading: props.isLoading || false, + }; + + this.onClick = this.onClick.bind(this); + } + + onClick() { + this.setState({ + isLoading: true, + }); + + setTimeout(() => { + this.setState({ + isLoading: false, + }); + }, 3000); + } + + render() { + return ( +
+ + {this.state.isLoading ? 'Loading...' : 'Load more'} + + +
+ + } + isLoading={this.state.isLoading} + disabled={this.state.isLoading} + > + {this.state.isLoading ? 'Creating...' : 'Create'} + +
+ ); + } +} diff --git a/ui_framework/doc_site/src/views/button/button_primary.html b/ui_framework/doc_site/src/views/button/button_primary.html deleted file mode 100644 index 7d2de8ec4471f..0000000000000 --- a/ui_framework/doc_site/src/views/button/button_primary.html +++ /dev/null @@ -1,9 +0,0 @@ - - -
- - diff --git a/ui_framework/doc_site/src/views/button/button_primary.js b/ui_framework/doc_site/src/views/button/button_primary.js new file mode 100644 index 0000000000000..dbbf1e242208f --- /dev/null +++ b/ui_framework/doc_site/src/views/button/button_primary.js @@ -0,0 +1,22 @@ +import React from 'react'; + +import { + KuiButton, +} from '../../../../components'; + +export default () => ( +
+ + Primary button + + +
+ + + Primary button, disabled + +
+); diff --git a/ui_framework/doc_site/src/views/button/button_with_icon.html b/ui_framework/doc_site/src/views/button/button_with_icon.html deleted file mode 100644 index 089b0394a3788..0000000000000 --- a/ui_framework/doc_site/src/views/button/button_with_icon.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- - - -
- - diff --git a/ui_framework/doc_site/src/views/button/button_with_icon.js b/ui_framework/doc_site/src/views/button/button_with_icon.js new file mode 100644 index 0000000000000..1039953f73c15 --- /dev/null +++ b/ui_framework/doc_site/src/views/button/button_with_icon.js @@ -0,0 +1,61 @@ +import React from 'react'; + +import { + KuiButton, + KuiButtonIcon, +} from '../../../../components'; + +export default () => ( +
+ } + > + Create + + +
+ + } + > + Delete + + +
+ + } + > + Previous + + +
+ + } + iconPosition='right' + > + Next + + +
+ + } + > + Loading + + +
+ + } + /> +
+); diff --git a/ui_framework/doc_site/src/views/button/buttons_in_tool_bar.html b/ui_framework/doc_site/src/views/button/buttons_in_tool_bar.html deleted file mode 100644 index 976b704dcf590..0000000000000 --- a/ui_framework/doc_site/src/views/button/buttons_in_tool_bar.html +++ /dev/null @@ -1,25 +0,0 @@ -
- - - - - - - - - - - -
diff --git a/ui_framework/doc_site/src/views/button/buttons_in_tool_bar.js b/ui_framework/doc_site/src/views/button/buttons_in_tool_bar.js new file mode 100644 index 0000000000000..91c62478ad356 --- /dev/null +++ b/ui_framework/doc_site/src/views/button/buttons_in_tool_bar.js @@ -0,0 +1,42 @@ +import React from 'react'; + +import { + KuiButton, +} from '../../../../components'; + +export default () => ( +
+ + Basic button + + + + Basic button, disabled + + + + Primary button + + + + Primary button, disabled + + + + Danger button + + + + Danger button, disabled + +
+); diff --git a/ui_framework/doc_site/webpack.config.js b/ui_framework/doc_site/webpack.config.js index a337e840506d6..749f1defc080b 100644 --- a/ui_framework/doc_site/webpack.config.js +++ b/ui_framework/doc_site/webpack.config.js @@ -18,8 +18,18 @@ module.exports = { ] }, + // These are necessasry for using Enzyme with Webpack (https://github.com/airbnb/enzyme/blob/master/docs/guides/webpack.md). + externals: { + 'react/lib/ExecutionEnvironment': true, + 'react/lib/ReactContext': true, + 'react/addons': true, + }, + module: { loaders: [{ + test: /\.json$/, + loader: 'json-loader', + }, { test: /\.jsx?$/, loader: 'babel', exclude: /node_modules/, diff --git a/ui_framework/jest/css_stub.js b/ui_framework/jest/css_stub.js new file mode 100644 index 0000000000000..f053ebf7976e3 --- /dev/null +++ b/ui_framework/jest/css_stub.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/ui_framework/jest/file_stub.js b/ui_framework/jest/file_stub.js new file mode 100644 index 0000000000000..4c19aaf3ac6cc --- /dev/null +++ b/ui_framework/jest/file_stub.js @@ -0,0 +1 @@ +module.exports = `test-file-stub`;