diff --git a/package-lock.json b/package-lock.json index 11bc5d2c407..cc9907b1c2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -427,7 +427,7 @@ "deep-equal": "1.0.1", "global": "4.3.2", "make-error": "1.3.3", - "prop-types": "15.6.0", + "prop-types": "15.6.1", "react-inspector": "2.2.2", "uuid": "3.2.1" } @@ -443,7 +443,7 @@ "babel-runtime": "6.26.0", "global": "4.3.2", "marksy": "2.0.1", - "prop-types": "15.6.0", + "prop-types": "15.6.1", "react-addons-create-fragment": "15.6.2", "util-deprecate": "1.0.2" } @@ -461,7 +461,7 @@ "insert-css": "1.1.0", "lodash.debounce": "4.0.8", "moment": "2.20.1", - "prop-types": "15.6.0", + "prop-types": "15.6.1", "react-color": "2.13.8", "react-datetime": "2.13.0", "react-textarea-autosize": "4.3.2", @@ -476,7 +476,7 @@ "requires": { "@storybook/components": "3.3.12", "global": "4.3.2", - "prop-types": "15.6.0" + "prop-types": "15.6.1" } }, "@storybook/addon-options": { @@ -516,7 +516,7 @@ "requires": { "glamor": "2.20.40", "glamorous": "4.11.4", - "prop-types": "15.6.0" + "prop-types": "15.6.1" } }, "@storybook/mantra-core": { @@ -574,7 +574,7 @@ "lodash.pick": "4.4.0", "postcss-flexbugs-fixes": "3.3.0", "postcss-loader": "2.1.0", - "prop-types": "15.6.0", + "prop-types": "15.6.1", "qs": "6.5.1", "react-modal": "2.4.1", "redux": "3.7.2", @@ -641,7 +641,7 @@ "babel-runtime": "6.26.0", "create-react-class": "15.6.3", "hoist-non-react-statics": "1.2.0", - "prop-types": "15.6.0" + "prop-types": "15.6.1" }, "dependencies": { "hoist-non-react-statics": { @@ -767,7 +767,7 @@ "lodash.pick": "4.4.0", "lodash.sortby": "4.7.0", "podda": "1.2.2", - "prop-types": "15.6.0", + "prop-types": "15.6.1", "qs": "6.5.1", "react-fuzzy": "0.5.1", "react-icons": "2.2.7", @@ -785,7 +785,7 @@ "dev": true, "requires": { "exenv": "1.2.2", - "prop-types": "15.6.0", + "prop-types": "15.6.1", "warning": "3.0.0" } } @@ -2817,6 +2817,11 @@ "hoek": "4.2.0" } }, + "bootstrap-slider": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/bootstrap-slider/-/bootstrap-slider-10.0.0.tgz", + "integrity": "sha1-1O3ToQrwMZfQION5LTLqbTfLOyg=" + }, "bowser": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/bowser/-/bowser-1.9.2.tgz", @@ -5528,7 +5533,7 @@ "doctrine": "2.1.0", "has": "1.0.1", "jsx-ast-utils": "2.0.1", - "prop-types": "15.6.0" + "prop-types": "15.6.1" } }, "eslint-plugin-standard": { @@ -7207,7 +7212,7 @@ "fbjs": "0.8.16", "inline-style-prefixer": "3.0.8", "object-assign": "4.1.1", - "prop-types": "15.6.0", + "prop-types": "15.6.1", "through": "2.3.8" } }, @@ -13537,9 +13542,9 @@ } }, "prop-types": { - "version": "15.6.0", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", - "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz", + "integrity": "sha512-4ec7bY1Y66LymSUOH/zARVYObB23AT2h8cf6e/O6ZALB/N0sqZFEx7rq6EYPX2MkOdKORuooI/H5k9TlR4q7kQ==", "requires": { "fbjs": "0.8.16", "loose-envify": "1.3.1", @@ -13668,7 +13673,7 @@ "array-find": "1.0.0", "exenv": "1.2.2", "inline-style-prefixer": "2.0.5", - "prop-types": "15.6.0" + "prop-types": "15.6.1" }, "dependencies": { "inline-style-prefixer": { @@ -13799,7 +13804,7 @@ "fbjs": "0.8.16", "loose-envify": "1.3.1", "object-assign": "4.1.1", - "prop-types": "15.6.0" + "prop-types": "15.6.1" } }, "react-addons-create-fragment": { @@ -13823,7 +13828,7 @@ "dom-helpers": "3.3.1", "invariant": "2.2.2", "keycode": "2.1.9", - "prop-types": "15.6.0", + "prop-types": "15.6.1", "prop-types-extra": "1.0.1", "react-overlays": "0.7.4", "uncontrollable": "4.1.0", @@ -13851,7 +13856,7 @@ "requires": { "lodash": "4.17.5", "material-colors": "1.2.5", - "prop-types": "15.6.0", + "prop-types": "15.6.1", "reactcss": "1.2.3", "tinycolor2": "1.4.1" } @@ -13864,7 +13869,7 @@ "requires": { "create-react-class": "15.6.3", "object-assign": "3.0.0", - "prop-types": "15.6.0", + "prop-types": "15.6.1", "react-onclickoutside": "6.7.1" }, "dependencies": { @@ -14153,7 +14158,7 @@ "fbjs": "0.8.16", "loose-envify": "1.3.1", "object-assign": "4.1.1", - "prop-types": "15.6.0" + "prop-types": "15.6.1" } }, "react-error-overlay": { @@ -14167,7 +14172,7 @@ "resolved": "https://registry.npmjs.org/react-fontawesome/-/react-fontawesome-1.6.1.tgz", "integrity": "sha1-7dzhfn3HMaoJ/UoYZoimF5OhbFw=", "requires": { - "prop-types": "15.6.0" + "prop-types": "15.6.1" } }, "react-fuzzy": { @@ -14179,7 +14184,7 @@ "babel-runtime": "6.26.0", "classnames": "2.2.5", "fuse.js": "3.2.0", - "prop-types": "15.6.0" + "prop-types": "15.6.1" } }, "react-html-attributes": { @@ -14223,7 +14228,7 @@ "dev": true, "requires": { "exenv": "1.2.2", - "prop-types": "15.6.0" + "prop-types": "15.6.1" } }, "react-onclickoutside": { @@ -14239,7 +14244,7 @@ "requires": { "classnames": "2.2.5", "dom-helpers": "3.3.1", - "prop-types": "15.6.0", + "prop-types": "15.6.1", "prop-types-extra": "1.0.1", "warning": "3.0.0" } @@ -14253,7 +14258,7 @@ "@types/inline-style-prefixer": "3.0.1", "@types/react": "16.0.36", "inline-style-prefixer": "3.0.8", - "prop-types": "15.6.0", + "prop-types": "15.6.1", "react-style-proptype": "3.2.0" } }, @@ -14263,7 +14268,7 @@ "integrity": "sha512-Mafmkzj3oNmLSJNOlH+WWWyGIdzVLhPj+d12fDxQMQdwDQ5sMX7vQKOLpry4U+zRWieTCx448AyRKK0NLWuXmg==", "dev": true, "requires": { - "prop-types": "15.6.0" + "prop-types": "15.6.1" } }, "react-test-renderer": { @@ -14274,7 +14279,7 @@ "requires": { "fbjs": "0.8.16", "object-assign": "4.1.1", - "prop-types": "15.6.0" + "prop-types": "15.6.1" } }, "react-textarea-autosize": { @@ -14283,7 +14288,7 @@ "integrity": "sha1-lipSxoys6uQIwYrOzsKQSbgeQvo=", "dev": true, "requires": { - "prop-types": "15.6.0" + "prop-types": "15.6.1" } }, "react-transition-group": { @@ -14295,7 +14300,7 @@ "chain-function": "1.0.0", "dom-helpers": "3.3.1", "loose-envify": "1.3.1", - "prop-types": "15.6.0", + "prop-types": "15.6.1", "warning": "3.0.0" } }, @@ -14307,7 +14312,7 @@ "requires": { "babel-runtime": "6.26.0", "deep-equal": "1.0.1", - "prop-types": "15.6.0", + "prop-types": "15.6.1", "radium": "0.19.6", "shallowequal": "0.2.2", "velocity-react": "1.3.3" @@ -17152,7 +17157,7 @@ "dev": true, "requires": { "lodash": "3.10.1", - "prop-types": "15.6.0", + "prop-types": "15.6.1", "react-transition-group": "1.2.1", "velocity-animate": "1.5.1" }, diff --git a/package.json b/package.json index 2ab32f4e853..16a63808036 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ }, "homepage": "https://github.com/patternfly/patternfly-react#readme", "dependencies": { + "bootstrap-slider": "^10.0.0", "breakjs": "^1.0.0", "classnames": "^2.2.5", "patternfly": "^3.38.0", @@ -75,7 +76,7 @@ "node-sass": "^4.7.2", "prettier": "^1.9.2", "prettier-eslint": "^8.8.1", - "prop-types": "^15.6.0", + "prop-types": "^15.6.1", "raf": "^3.4.0", "react": "^16.2.0", "react-dev-utils": "^5.0.0", diff --git a/src/components/Slider/BSColumnsManager.js b/src/components/Slider/BSColumnsManager.js new file mode 100644 index 00000000000..71ef0ffc9c5 --- /dev/null +++ b/src/components/Slider/BSColumnsManager.js @@ -0,0 +1,55 @@ +const LAYOUT = { + label: { + xs: 1, + sm: 1, + md: 1 + }, + form: { + xs: 4, + sm: 4, + md: 2 + } +}; +const SIZES = ['xs', 'sm', 'md']; + +/** + * class BSColumnsManager - helps to keep track on the BS grid usage. + * + * @return {type} description + */ +export default class BSColumnsManager { + constructor() { + this.columnCounter = { xs: 0, sm: 0, md: 0 }; + } + + /** + * Calculates the element column width by iterating over its size settings + * in the LAYOUT object. + * returns a class string, E.G: 'col-xs-2 col-sm-4 col-md-4'. + * @param {String} elementName + * @return {string} + */ + getElementColumnsClass = (elementName, props) => + SIZES.map(size => { + if (elementName !== 'slider') { + const numberOfColumns = LAYOUT[elementName][size]; + this.columnCounter[size] += numberOfColumns; + return `col-${size}-${numberOfColumns}`; + } + const demoCounter = { xs: 0, sm: 0, md: 0 }; + + if (props.input || props.selectList) { + demoCounter[size] += LAYOUT.form[size]; + } + + if (props.icon || props.label) { + demoCounter[size] += LAYOUT.label[size]; + } + + return `col-${size}-${12 - demoCounter[size]}`; + }).join(' '); + + resetColumnsCount = () => { + this.columnCounter = { xs: 0, sm: 0, md: 0 }; + }; +} diff --git a/src/components/Slider/Form.js b/src/components/Slider/Form.js new file mode 100644 index 00000000000..5a4b8fe52fb --- /dev/null +++ b/src/components/Slider/Form.js @@ -0,0 +1,72 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Form, FormControl, FormGroup } from '../../index'; + +const SliderForm = props => { + const input = props.input ? ( + + + + ) : null; + + const selectList = props.selectList ? ( + + + {props.selectList.map((item, index) => ( + + ))} + + + ) : null; + + const BSclassName = props.columnsManager.getElementColumnsClass('form'); + + return ( +
+
+ {input} + {selectList} +
+
+ ); +}; + +SliderForm.propTypes = { + id: PropTypes.string, + min: PropTypes.number, + max: PropTypes.number, + value: PropTypes.oneOfType([PropTypes.array, PropTypes.number]).isRequired, + input: PropTypes.bool, + selectList: PropTypes.array, + formClass: PropTypes.string, + columnsManager: PropTypes.object, + onInputChange: PropTypes.func, + onFormatChange: PropTypes.func +}; + +SliderForm.defaultProps = { + id: null, + min: 0, + max: 100, + input: false, + selectList: [''], + formClass: null, + columnsManager: null, + onInputChange: v => v, + onFormatChange: v => v +}; + +export default SliderForm; diff --git a/src/components/Slider/Label.js b/src/components/Slider/Label.js new file mode 100644 index 00000000000..621dec8da7a --- /dev/null +++ b/src/components/Slider/Label.js @@ -0,0 +1,32 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Icon } from '../../index'; + +const SliderLabel = props => { + let label = null; + if (props.icon || props.label) { + const BSclassName = props.columnsManager.getElementColumnsClass('label'); + label = props.icon ? ( + + ) : ( +
{props.label}
+ ); + } + return label; +}; + +SliderLabel.propTypes = { + label: PropTypes.string, + labelClass: PropTypes.string, + icon: PropTypes.object, + columnsManager: PropTypes.object +}; + +SliderLabel.defaultProps = { + label: null, + labelClass: null, + icon: null, + columnsManager: null +}; + +export default SliderLabel; diff --git a/src/components/Slider/Slider.js b/src/components/Slider/Slider.js new file mode 100644 index 00000000000..8a624124525 --- /dev/null +++ b/src/components/Slider/Slider.js @@ -0,0 +1,95 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import WrappedBootstrapSlider from './WrappedBootstrapSlider'; +import BSColumnsManager from './BSColumnsManager'; +import Label from './Label'; +import Form from './Form'; +import './examples.css'; + +class Slider extends React.Component { + constructor(props) { + super(props); + + this.state = { + value: this.props.value, + tooltipFormat: this.props.selectList[0] + }; + + this.columnsManager = new BSColumnsManager(); + } + + onSlide = value => { + this.setState({ value }); + }; + + onInputChange = event => { + this.setState({ value: parseInt(event.target.value || 0, 10) }); + }; + + onFormatChange = event => { + this.setState({ tooltipFormat: event.target.value }); + }; + + render() { + return ( +
+
+ ); + } +} + +Slider.propTypes = { + id: PropTypes.string, + orientation: PropTypes.string, + min: PropTypes.number, + max: PropTypes.number, + step: PropTypes.number, + value: PropTypes.oneOfType([PropTypes.array, PropTypes.number]).isRequired, + toolTip: PropTypes.bool, + onSlide: PropTypes.func, + label: PropTypes.string, + labelClass: PropTypes.string, + icon: PropTypes.object, + input: PropTypes.bool, + inputClass: PropTypes.string, + sliderClass: PropTypes.string, + selectList: PropTypes.array, + formClass: PropTypes.string +}; + +Slider.defaultProps = { + id: null, + orientation: 'horizontal', + min: 0, + max: 100, + step: 1, + toolTip: false, + onSlide: event => event, + label: null, + labelClass: null, + input: false, + inputClass: null, + sliderClass: null, + icon: null, + selectList: [''], + formClass: null +}; + +export default Slider; diff --git a/src/components/Slider/Slider.stories.js b/src/components/Slider/Slider.stories.js new file mode 100644 index 00000000000..fb9ac46911c --- /dev/null +++ b/src/components/Slider/Slider.stories.js @@ -0,0 +1,106 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { withKnobs } from '@storybook/addon-knobs'; +import { defaultTemplate } from '../../../storybook/decorators/storyTemplates'; +import { DOCUMENTATION_URL } from '../../../storybook/constants'; +import { Slider } from './index'; + +const SliderStories = storiesOf('Slider', module); + +SliderStories.addDecorator(withKnobs); +SliderStories.addDecorator( + defaultTemplate({ + title: 'Slider', + documentationLink: `${DOCUMENTATION_URL.PATTERNFLY_ORG_WIDGETS}#Slider`, + reactBootstrapDocumentationLink: `${ + DOCUMENTATION_URL.REACT_BOOTSTRAP_COMPONENT + }slider/` + }) +); + +SliderStories.addWithInfo('Slider', () => ( +
+
+

A slider with an input field:

+
+
+ +
+

A slider with icon as a label and input field:

+
+
+ +
+

A slider with a range of values:

+
+
+ +
+

A slider with tick marks:

+
+
+ +
+

Highlight ranges on slider:

+
+
+ +
+
+
+)); diff --git a/src/components/Slider/WrappedBootstrapSlider.js b/src/components/Slider/WrappedBootstrapSlider.js new file mode 100644 index 00000000000..6195a476911 --- /dev/null +++ b/src/components/Slider/WrappedBootstrapSlider.js @@ -0,0 +1,70 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import BootstrapSlider from 'bootstrap-slider'; + +class WrappedBootstrapSlider extends React.Component { + constructor(props) { + super(props); + + this._slider = null; + } + + componentDidMount() { + if (this._slider) { + return; + } + + const that = this; + + this._slider = new BootstrapSlider(`input[id='${this.props.id}']`, { + ...this.props + }); + this._slider.on('slide', value => that._onSlide(value)); + this._slider.on('slideStop', value => that._onSlide(value)); + } + + componentWillUpdate(nextProps, nextState) { + this._slider.setValue(nextProps.value); + // Sets the tooltip format. + this._slider.setAttribute('formatter', nextProps.formatter); + // Adjust the tooltip to "sit" ontop of the slider's handle. #LibraryBug + this._slider.tooltip.style.marginLeft = `-${this._slider.tooltip + .offsetWidth / 2}px`; + } + + _onSlide = value => { + this.props.onSlide(value); + this._slider.setValue(value); + }; + + render() { + const { sliderClass, columnsManager } = this.props; + const BSClassName = columnsManager.getElementColumnsClass( + 'slider', + this.props + ); + + return ( +
+ +
+ ); + } +} + +WrappedBootstrapSlider.propTypes = { + id: PropTypes.string, + value: PropTypes.oneOfType([PropTypes.array, PropTypes.number]).isRequired, + onSlide: PropTypes.func, + sliderClass: PropTypes.string, + columnsManager: PropTypes.object +}; + +WrappedBootstrapSlider.defaultProps = { + id: null, + onSlide: event => event, + sliderClass: null, + columnsManager: null +}; + +export default WrappedBootstrapSlider; diff --git a/src/components/Slider/examples.css b/src/components/Slider/examples.css new file mode 100644 index 00000000000..ad882d1bdc3 --- /dev/null +++ b/src/components/Slider/examples.css @@ -0,0 +1,11 @@ +#slider351 .category1{ + background: orange; +} + +#slider351 .category2{ + background: purple; +} + +#slider351 .category3{ + background: green; +} diff --git a/src/components/Slider/index.js b/src/components/Slider/index.js new file mode 100644 index 00000000000..0f4a4a25fcb --- /dev/null +++ b/src/components/Slider/index.js @@ -0,0 +1 @@ +export { default as Slider } from './Slider'; diff --git a/storybook/webpack.config.js b/storybook/webpack.config.js index 5d5951865eb..dc285540bd6 100644 --- a/storybook/webpack.config.js +++ b/storybook/webpack.config.js @@ -81,5 +81,8 @@ module.exports = { } } ] + }, + externals: { + jquery: 'null' } };