From 0f3ee3e1aff6901befabf38dd84cfa1f3d2ab344 Mon Sep 17 00:00:00 2001 From: jquense Date: Sun, 1 Nov 2015 12:39:20 -0500 Subject: [PATCH] [removed] bootstrap mixin --- .babelrc | 3 +- .eslintrc | 13 +- docs/generate-metadata.js | 61 +++++++- docs/src/ComponentsPage.js | 19 +-- docs/src/PropTable.js | 2 +- docs/src/defaultPropDescriptions.js | 7 + package.json | 4 +- src/Alert.js | 17 +-- src/Badge.js | 7 +- src/BootstrapMixin.js | 55 -------- src/Button.js | 23 +-- src/ButtonGroup.js | 20 +-- src/ButtonToolbar.js | 12 +- src/Carousel.js | 25 ++-- src/CarouselItem.js | 6 +- src/CollapsibleNav.js | 2 - src/Dropdown.js | 14 +- src/DropdownButton.js | 11 +- src/DropdownMenu.js | 9 +- src/Label.js | 19 +-- src/ListGroupItem.js | 70 +++++----- src/MenuItem.js | 9 +- src/Modal.js | 6 +- src/ModalBody.js | 7 +- src/ModalDialog.js | 18 +-- src/ModalFooter.js | 7 +- src/ModalHeader.js | 10 +- src/ModalTitle.js | 10 +- src/Nav.js | 141 +++++++++---------- src/NavItem.js | 5 +- src/Navbar.js | 42 +++--- src/Pagination.js | 7 +- src/PaginationButton.js | 5 +- src/Panel.js | 29 ++-- src/PanelGroup.js | 14 +- src/Popover.js | 16 ++- src/ProgressBar.js | 121 ++++++++-------- src/SplitButton.js | 6 +- src/SubNav.js | 3 +- src/Tab.js | 7 +- src/Tabs.js | 6 +- src/Thumbnail.js | 13 +- src/Tooltip.js | 146 +++++++++---------- src/Well.js | 19 +-- src/index.js | 5 +- src/styleMaps.js | 67 ++++----- src/utils/bootstrapUtils.js | 152 ++++++++++++++++++++ test/BootstrapMixinSpec.js | 124 ----------------- test/ButtonGroupSpec.js | 2 +- test/ModalSpec.js | 9 +- test/ProgressBarSpec.js | 34 ++--- test/helpers.js | 2 +- test/utils/bootstrapUtilsSpec.js | 209 ++++++++++++++++++++++++++++ 53 files changed, 955 insertions(+), 695 deletions(-) create mode 100644 docs/src/defaultPropDescriptions.js delete mode 100644 src/BootstrapMixin.js create mode 100644 src/utils/bootstrapUtils.js delete mode 100644 test/BootstrapMixinSpec.js create mode 100644 test/utils/bootstrapUtilsSpec.js diff --git a/.babelrc b/.babelrc index 4d5093a16f..f17b0189e9 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,6 @@ { "stage": 1, "optional": ["runtime"], - "loose": ["all"] + "loose": ["all"], + "plugins": ["object-assign"] } diff --git a/.eslintrc b/.eslintrc index 203ab3dd84..13cea79b2e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -17,6 +17,7 @@ "comma-dangle": 0, "eqeqeq": [2, "allow-null"], "id-length": 0, + "no-eq-null": 0, "one-var": [2, { "initialized": "never" }], "prefer-const": 0, "no-param-reassign": 0, @@ -24,7 +25,15 @@ "babel/object-shorthand": 2, "react/jsx-boolean-value": 2, "react/jsx-no-duplicate-props": 2, - "react/prop-types": [2, { "ignore": [ "children", "className", "style" ] }], - "react/sort-comp": 0 + "react/sort-comp": 0, + "react/prop-types": [2, { "ignore": [ + "children", + "className", + "style", + "bsStyle", + "bsClass", + "bsSize" + ] + }], } } diff --git a/docs/generate-metadata.js b/docs/generate-metadata.js index 4341b94d58..96c9a3723e 100644 --- a/docs/generate-metadata.js +++ b/docs/generate-metadata.js @@ -3,6 +3,7 @@ import glob from 'glob'; import fsp from 'fs-promise'; import promisify from '../tools/promisify'; import marked from 'marked'; +import defaultDescriptions from './src/defaultPropDescriptions'; marked.setOptions({ xhtml: true @@ -18,6 +19,7 @@ let cleanDoclets = desc => { let cleanDocletValue = str => str.trim().replace(/^\{/, '').replace(/\}$/, ''); +let quote = str => str && `'${str}'`; let isLiteral = str => (/^('|")/).test(str.trim()); @@ -26,10 +28,11 @@ let isLiteral = str => (/^('|")/).test(str.trim()); * * @param {ComponentMetadata|PropMetadata} obj */ -function parseDoclets(obj) { - obj.doclets = metadata.parseDoclets(obj.desc || '') || {}; - obj.desc = cleanDoclets(obj.desc || ''); - obj.descHtml = marked(obj.desc || ''); +function parseDoclets(obj, propName) { + let desc = obj.desc || defaultDescriptions[propName] || ''; + obj.doclets = metadata.parseDoclets(desc) || {}; + obj.desc = cleanDoclets(desc); + obj.descHtml = marked(desc); } /** @@ -61,7 +64,7 @@ function applyPropDoclets(props, propName) { // Use @required to mark a prop as required // useful for custom propTypes where there isn't a `.isRequired` addon - if ( doclets.required) { + if (doclets.required) { prop.required = true; } @@ -71,12 +74,45 @@ function applyPropDoclets(props, propName) { } } +function addBootstrapPropTypes(Component, componentData) { + let propTypes = Component.propTypes || {}; + let defaultProps = Component.defaultProps || {}; + + function bsPropInfo(propName) { + let props = componentData.props; + let prop = propTypes[propName]; + + if (prop && !props[propName]) { + let values = prop._values || []; + + props[propName] = { + desc: '', + defaultValue: quote(defaultProps[propName]), + type: { + name: 'enum', + value: values.map( v => `"${v}"`), + } + }; + } + } + + bsPropInfo('bsStyle'); + bsPropInfo('bsSize'); + + if (propTypes.bsClass) { + componentData.props.bsClass = { + desc: '', + defaultValue: quote(defaultProps.bsClass), + type: { name: 'string' } + }; + } +} export default function generate(destination, options = { mixins: true, inferComponent: true }) { return globp(__dirname + '/../src/**/*.js') // eslint-disable-line no-path-concat .then( files => { let results = files.map( - filename => fsp.readFile(filename).then(content => metadata(content, options)) ); + filename => fsp.readFile(filename).then(content => metadata(content, options))); return Promise.all(results) .then( data => { @@ -84,14 +120,25 @@ export default function generate(destination, options = { mixins: true, inferCom data.forEach(components => { Object.keys(components).forEach(key => { + let Component; + + try { + // require the actual component to inspect props we can only get a runtime + Component = require('../src/' + key); + } catch (e) {} //eslint-disable-line + const component = components[key]; + if (Component) { + addBootstrapPropTypes(Component, component); + } + parseDoclets(component); Object.keys(component.props).forEach( propName => { const prop = component.props[propName]; - parseDoclets(prop); + parseDoclets(prop, propName); applyPropDoclets(component.props, propName); }); }); diff --git a/docs/src/ComponentsPage.js b/docs/src/ComponentsPage.js index 79dbbf1a79..bab62c6ead 100644 --- a/docs/src/ComponentsPage.js +++ b/docs/src/ComponentsPage.js @@ -225,16 +225,17 @@ const ComponentsPage = React.createClass({

Menu Item MenuItem

This is a component used in other components (see Buttons, Navbars).

It supports the basic anchor properties href, target, title.

-

It also supports different properties of the normal Bootstrap MenuItem. -

-

The callback is called with the following arguments: eventKey, href and target

+

+ It also supports different properties of the normal Bootstrap MenuItem.

+ +

The callback is called with the following arguments: eventKey, href and target

Props

diff --git a/docs/src/PropTable.js b/docs/src/PropTable.js index 0ab6b57743..fb82a6bf56 100644 --- a/docs/src/PropTable.js +++ b/docs/src/PropTable.js @@ -45,7 +45,7 @@ const PropTable = React.createClass({ render() { let propsData = this.propsData; - if ( !Object.keys(propsData).length) { + if (!Object.keys(propsData).length) { return ; } diff --git a/docs/src/defaultPropDescriptions.js b/docs/src/defaultPropDescriptions.js new file mode 100644 index 0000000000..92f647f3d6 --- /dev/null +++ b/docs/src/defaultPropDescriptions.js @@ -0,0 +1,7 @@ + +export default { + bsClass: 'Base css class name and prefix for the Component. Generally one should only change `bsClass` ' + + 'if they are providing new, non bootstrap, css styles for a component.', + bsStyle: 'Component visual or contextual style variants.', + bsSize: 'Component size variations.' +}; diff --git a/package.json b/package.json index 7b57c2b503..1a611d92cb 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "babel-core": "^5.8.25", "babel-eslint": "^4.1.3", "babel-loader": "^5.3.2", + "babel-plugin-object-assign": "^1.2.1", "bootstrap": "^3.3.5", "brfs": "^1.4.1", "chai": "^3.3.0", @@ -116,11 +117,12 @@ "babel-runtime": "^5.8.25", "classnames": "^2.1.5", "dom-helpers": "^2.4.0", + "invariant": "^2.1.2", "keycode": "^2.1.0", "lodash-compat": "^3.10.1", "react-overlays": "^0.5.0", - "uncontrollable": "^3.1.3", "react-prop-types": "^0.3.0", + "uncontrollable": "^3.1.3", "warning": "^2.1.0" }, "release-script": { diff --git a/src/Alert.js b/src/Alert.js index f3d35a6a5d..9a58cc313a 100644 --- a/src/Alert.js +++ b/src/Alert.js @@ -1,9 +1,9 @@ import React from 'react'; import classNames from 'classnames'; -import BootstrapMixin from './BootstrapMixin'; +import bootstrapUtils, { bsStyles, bsClass } from './utils/bootstrapUtils'; +import { State } from './styleMaps'; -const Alert = React.createClass({ - mixins: [BootstrapMixin], +let Alert = React.createClass({ propTypes: { onDismiss: React.PropTypes.func, @@ -13,8 +13,6 @@ const Alert = React.createClass({ getDefaultProps() { return { - bsClass: 'alert', - bsStyle: 'info', closeLabel: 'Close Alert' }; }, @@ -43,10 +41,10 @@ const Alert = React.createClass({ }, render() { - let classes = this.getBsClassSet(); + let classes = bootstrapUtils.getClassSet(this.props); let isDismissable = !!this.props.onDismiss; - classes['alert-dismissable'] = isDismissable; + classes[bootstrapUtils.prefix(this.props, 'dismissable')] = isDismissable; return (
@@ -68,4 +66,7 @@ const Alert = React.createClass({ } }); -export default Alert; + +export default bsStyles(State.values(), State.INFO, + bsClass('alert', Alert) +); diff --git a/src/Badge.js b/src/Badge.js index 618ffa4361..ade955a255 100644 --- a/src/Badge.js +++ b/src/Badge.js @@ -1,7 +1,7 @@ import React from 'react'; import ValidComponentChildren from './utils/ValidComponentChildren'; import classNames from 'classnames'; - +import tbsUtils from './utils/bootstrapUtils'; const Badge = React.createClass({ propTypes: { @@ -10,7 +10,8 @@ const Badge = React.createClass({ getDefaultProps() { return { - pullRight: false + pullRight: false, + bsClass: 'badge' }; }, @@ -24,7 +25,7 @@ const Badge = React.createClass({ render() { let classes = { 'pull-right': this.props.pullRight, - 'badge': this.hasContent() + [tbsUtils.prefix(this.props)]: this.hasContent() }; return ( = 0) { - classes[prefix + this.props.bsStyle] = true; - } else { - classes[this.props.bsStyle] = true; - } - } - } - - return classes; - }, - - prefixClass(subClass) { - return styleMaps.CLASSES[this.props.bsClass] + '-' + subClass; - } -}; - -export default BootstrapMixin; diff --git a/src/Button.js b/src/Button.js index 940cf9d57c..9181253cfb 100644 --- a/src/Button.js +++ b/src/Button.js @@ -1,12 +1,15 @@ import React from 'react'; import classNames from 'classnames'; -import BootstrapMixin from './BootstrapMixin'; import elementType from 'react-prop-types/lib/elementType'; const types = ['button', 'reset', 'submit']; -const Button = React.createClass({ - mixins: [BootstrapMixin], +import bootstrapUtils, { bsStyles, bsSizes, bsClass } from './utils/bootstrapUtils'; +import { Sizes, State, DEFAULT, PRIMARY, LINK } from './styleMaps'; + +const ButtonStyles = State.values().concat(DEFAULT, PRIMARY, LINK); + +let Button = React.createClass({ propTypes: { active: React.PropTypes.bool, @@ -32,8 +35,6 @@ const Button = React.createClass({ return { active: false, block: false, - bsClass: 'button', - bsStyle: 'default', disabled: false, navItem: false, navDropdown: false @@ -41,12 +42,14 @@ const Button = React.createClass({ }, render() { - let classes = this.props.navDropdown ? {} : this.getBsClassSet(); + let classes = this.props.navDropdown ? {} : bootstrapUtils.getClassSet(this.props); let renderFuncName; + let blockClass = bootstrapUtils.prefix(this.props, 'block'); + classes = { active: this.props.active, - 'btn-block': this.props.block, + [blockClass]: this.props.block, ...classes }; @@ -104,4 +107,8 @@ const Button = React.createClass({ Button.types = types; -export default Button; +export default bsStyles(ButtonStyles, DEFAULT, + bsSizes([Sizes.LARGE, Sizes.SMALL, Sizes.XSMALL], + bsClass('btn', Button) + ) +); diff --git a/src/ButtonGroup.js b/src/ButtonGroup.js index 533150d060..d727b54ea0 100644 --- a/src/ButtonGroup.js +++ b/src/ButtonGroup.js @@ -1,10 +1,10 @@ import React from 'react'; import classNames from 'classnames'; -import BootstrapMixin from './BootstrapMixin'; +import bootstrapUtils, { bsClass } from './utils/bootstrapUtils'; import all from 'react-prop-types/lib/all'; +import Button from './Button'; const ButtonGroup = React.createClass({ - mixins: [BootstrapMixin], propTypes: { vertical: React.PropTypes.bool, @@ -26,18 +26,20 @@ const ButtonGroup = React.createClass({ getDefaultProps() { return { block: false, - bsClass: 'button-group', justified: false, vertical: false }; }, render() { - let classes = this.getBsClassSet(); - classes['btn-group'] = !this.props.vertical; - classes['btn-group-vertical'] = this.props.vertical; - classes['btn-group-justified'] = this.props.justified; - classes['btn-block'] = this.props.block; + let classes = bootstrapUtils.getClassSet(this.props); + + classes[bootstrapUtils.prefix(this.props)] = !this.props.vertical; + classes[bootstrapUtils.prefix(this.props, 'vertical')] = this.props.vertical; + classes[bootstrapUtils.prefix(this.props, 'justified')] = this.props.justified; + + // this is annoying, since the class is `btn-block` not `btn-group-block` + classes[bootstrapUtils.prefix(Button.defaultProps, 'block')] = this.props.block; return (
- {this.props.indicators ? this.renderIndicators() : null} -
+ { + this.props.indicators ? this.renderIndicators() : null + } +
{ValidComponentChildren.map(this.props.children, this.renderItem)}
{this.props.controls ? this.renderControls() : null} @@ -161,16 +166,20 @@ const Carousel = React.createClass({ }, renderPrev() { + let classes = 'left ' + tbsUtils.prefix(this.props, 'control'); + return ( - + {this.props.prevIcon} ); }, renderNext() { + let classes = 'right ' + tbsUtils.prefix(this.props, 'control'); + return ( - + {this.props.nextIcon} ); @@ -219,7 +228,7 @@ const Carousel = React.createClass({ }, this); return ( -
    +
      {indicators}
    ); diff --git a/src/CarouselItem.js b/src/CarouselItem.js index 4a3b65fe40..2b5bbbd1fc 100644 --- a/src/CarouselItem.js +++ b/src/CarouselItem.js @@ -3,6 +3,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import TransitionEvents from './utils/TransitionEvents'; +import tbsUtils from './utils/bootstrapUtils'; const CarouselItem = React.createClass({ propTypes: { @@ -23,6 +24,7 @@ const CarouselItem = React.createClass({ getDefaultProps() { return { + bsStyle: 'carousel', active: false, animateIn: false, animateOut: false @@ -88,8 +90,10 @@ const CarouselItem = React.createClass({ }, renderCaption() { + let classes = tbsUtils.prefix(this.props, 'caption'); + return ( -
    +
    {this.props.caption}
    ); diff --git a/src/CollapsibleNav.js b/src/CollapsibleNav.js index 7f704e0d2a..cd71ecf836 100644 --- a/src/CollapsibleNav.js +++ b/src/CollapsibleNav.js @@ -1,5 +1,4 @@ import React, { cloneElement } from 'react'; -import BootstrapMixin from './BootstrapMixin'; import Collapse from './Collapse'; import classNames from 'classnames'; @@ -7,7 +6,6 @@ import ValidComponentChildren from './utils/ValidComponentChildren'; import createChainedFunction from './utils/createChainedFunction'; const CollapsibleNav = React.createClass({ - mixins: [BootstrapMixin], propTypes: { onSelect: React.PropTypes.func, diff --git a/src/Dropdown.js b/src/Dropdown.js index cb4ed6cfa9..4e72e8d9a6 100644 --- a/src/Dropdown.js +++ b/src/Dropdown.js @@ -11,6 +11,7 @@ import elementType from 'react-prop-types/lib/elementType'; import isRequiredForA11y from 'react-prop-types/lib/isRequiredForA11y'; import uncontrollable from 'uncontrollable'; +import bootstrapUtils from './utils/bootstrapUtils'; import ButtonGroup from './ButtonGroup'; import DropdownMenu from './DropdownMenu'; import DropdownToggle from './DropdownToggle'; @@ -87,12 +88,13 @@ class Dropdown extends React.Component { let children = this.extractChildren(); let Component = this.props.componentClass; - let props = omit(this.props, ['id', 'role']); + let props = omit(this.props, ['id', 'bsClass', 'role']); + let className = bootstrapUtils.prefix(this.props); const rootClasses = { open: this.props.open, disabled: this.props.disabled, - dropdown: !this.props.dropup, + [className]: !this.props.dropup, dropup: this.props.dropup }; @@ -204,7 +206,8 @@ class Dropdown extends React.Component { ref: 'menu', open, labelledBy: this.props.id, - pullRight: this.props.pullRight + pullRight: this.props.pullRight, + bsClass: this.props.bsClass }; menuProps.onClose = createChainedFunction( @@ -252,10 +255,13 @@ Dropdown.MENU_ROLE = MENU_ROLE; Dropdown.defaultProps = { componentClass: ButtonGroup, - alwaysFocusNextOnOpen: false + bsClass: 'dropdown' }; Dropdown.propTypes = { + + bsClass: React.PropTypes.string, + /** * The menu will open above the dropdown button, instead of below it. */ diff --git a/src/DropdownButton.js b/src/DropdownButton.js index b1298c4952..e0de69b99f 100644 --- a/src/DropdownButton.js +++ b/src/DropdownButton.js @@ -1,14 +1,10 @@ import React from 'react'; -import BootstrapMixin from './BootstrapMixin'; import Dropdown from './Dropdown'; import omit from 'lodash-compat/object/omit'; +import Button from './Button'; class DropdownButton extends React.Component { - constructor(props) { - super(props); - } - render() { let { title, ...props } = this.props; @@ -28,15 +24,16 @@ class DropdownButton extends React.Component { } DropdownButton.propTypes = { + bsStyle: Button.propTypes.bsStyle, + bsSize: Button.propTypes.bsSize, + /** * When used with the `title` prop, the noCaret option will not render a caret icon, in the toggle element. */ noCaret: React.PropTypes.bool, - title: React.PropTypes.node.isRequired, ...Dropdown.propTypes, - ...BootstrapMixin.propTypes }; DropdownButton.defaultProps = { diff --git a/src/DropdownMenu.js b/src/DropdownMenu.js index da448faa52..7e7cbaf95c 100644 --- a/src/DropdownMenu.js +++ b/src/DropdownMenu.js @@ -1,7 +1,9 @@ -import classNames from 'classnames'; import keycode from 'keycode'; import React from 'react'; import ReactDOM from 'react-dom'; +import classNames from 'classnames'; +import bootstrapUtils from './utils/bootstrapUtils'; + import RootCloseWrapper from 'react-overlays/lib/RootCloseWrapper'; import ValidComponentChildren from './utils/ValidComponentChildren'; import createChainedFunction from './utils/createChainedFunction'; @@ -93,8 +95,8 @@ class DropdownMenu extends React.Component { }); const classes = { - 'dropdown-menu': true, - 'dropdown-menu-right': pullRight + [bootstrapUtils.prefix(this.props, 'menu')]: true, + [bootstrapUtils.prefix(this.props, 'menu-right')]: pullRight }; let list = ( @@ -122,6 +124,7 @@ class DropdownMenu extends React.Component { DropdownMenu.defaultProps = { bsRole: 'menu', + bsClass: 'dropdown', pullRight: false }; diff --git a/src/Label.js b/src/Label.js index 6cbf8839a9..dcf5c4ed3c 100644 --- a/src/Label.js +++ b/src/Label.js @@ -1,19 +1,14 @@ import React from 'react'; import classNames from 'classnames'; -import BootstrapMixin from './BootstrapMixin'; +import bootstrapUtils, { bsStyles, bsClass } from './utils/bootstrapUtils'; +import { State, DEFAULT, PRIMARY } from './styleMaps'; -const Label = React.createClass({ - mixins: [BootstrapMixin], - - getDefaultProps() { - return { - bsClass: 'label', - bsStyle: 'default' - }; - }, +@bsClass('label') +@bsStyles(State.values().concat(DEFAULT, PRIMARY), DEFAULT) +class Label extends React.Component { render() { - let classes = this.getBsClassSet(); + let classes = bootstrapUtils.getClassSet(this.props); return ( @@ -21,6 +16,6 @@ const Label = React.createClass({ ); } -}); +} export default Label; diff --git a/src/ListGroupItem.js b/src/ListGroupItem.js index 221b1de2b6..8403a0b170 100644 --- a/src/ListGroupItem.js +++ b/src/ListGroupItem.js @@ -1,30 +1,13 @@ import React, { cloneElement } from 'react'; -import BootstrapMixin from './BootstrapMixin'; +import bootstrapUtils, { bsStyles, bsClass } from './utils/bootstrapUtils'; +import { State } from './styleMaps'; import classNames from 'classnames'; -const ListGroupItem = React.createClass({ - mixins: [BootstrapMixin], - - propTypes: { - bsStyle: React.PropTypes.oneOf(['danger', 'info', 'success', 'warning']), - className: React.PropTypes.string, - active: React.PropTypes.any, - disabled: React.PropTypes.any, - header: React.PropTypes.node, - listItem: React.PropTypes.bool, - onClick: React.PropTypes.func, - href: React.PropTypes.string - }, - - getDefaultProps() { - return { - bsClass: 'list-group-item', - listItem: false - }; - }, +class ListGroupItem extends React.Component { + render() { - let classes = this.getBsClassSet(); + let classes = bootstrapUtils.getClassSet(this.props); classes.active = this.props.active; classes.disabled = this.props.disabled; @@ -36,8 +19,9 @@ const ListGroupItem = React.createClass({ } else if (this.props.listItem) { return this.renderLi(classes); } + return this.renderSpan(classes); - }, + } renderLi(classes) { return ( @@ -46,7 +30,7 @@ const ListGroupItem = React.createClass({ {this.props.header ? this.renderStructuredContent() : this.props.children} ); - }, + } renderAnchor(classes) { return ( @@ -57,7 +41,7 @@ const ListGroupItem = React.createClass({ {this.props.header ? this.renderStructuredContent() : this.props.children} ); - }, + } renderButton(classes) { return ( @@ -68,7 +52,7 @@ const ListGroupItem = React.createClass({ {this.props.header ? this.renderStructuredContent() : this.props.children} ); - }, + } renderSpan(classes) { return ( @@ -77,31 +61,53 @@ const ListGroupItem = React.createClass({ {this.props.header ? this.renderStructuredContent() : this.props.children} ); - }, + } renderStructuredContent() { let header; + let headingClass = bootstrapUtils.prefix(this.props, 'heading'); + if (React.isValidElement(this.props.header)) { header = cloneElement(this.props.header, { key: 'header', - className: classNames(this.props.header.props.className, 'list-group-item-heading') + className: classNames(this.props.header.props.className, headingClass) }); } else { header = ( -

    +

    {this.props.header}

    ); } let content = ( -

    +

    {this.props.children}

    ); return [header, content]; } -}); +} + +ListGroupItem.propTypes = { + className: React.PropTypes.string, + active: React.PropTypes.any, + disabled: React.PropTypes.any, + header: React.PropTypes.node, + listItem: React.PropTypes.bool, + onClick: React.PropTypes.func, + eventKey: React.PropTypes.any, + href: React.PropTypes.string, + target: React.PropTypes.string +}; + +ListGroupItem.defaultTypes = { + listItem: false +}; -export default ListGroupItem; +export default bsStyles(State.values(), + bsClass('list-group-item', + ListGroupItem + ) +); diff --git a/src/MenuItem.js b/src/MenuItem.js index 448fb6de6d..084c56f37b 100644 --- a/src/MenuItem.js +++ b/src/MenuItem.js @@ -1,11 +1,12 @@ import classnames from 'classnames'; import React from 'react'; +import bootstrapUtils, { bsClass } from './utils/bootstrapUtils'; import all from 'react-prop-types/lib/all'; import SafeAnchor from './SafeAnchor'; import createChainedFunction from './utils/createChainedFunction'; -export default class MenuItem extends React.Component { +class MenuItem extends React.Component { constructor(props) { super(props); @@ -27,13 +28,15 @@ export default class MenuItem extends React.Component { } render() { + let headerClass = bootstrapUtils.prefix(this.props, 'header'); + if (this.props.divider) { return
  1. ; } if (this.props.header) { return ( -
  2. {this.props.children}
  3. +
  4. {this.props.children}
  5. ); } @@ -93,3 +96,5 @@ MenuItem.defaultProps = { disabled: false, header: false }; + +export default bsClass('dropdown', MenuItem); diff --git a/src/Modal.js b/src/Modal.js index 8208d5505a..e6a0dd45eb 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -4,6 +4,7 @@ import classNames from 'classnames'; import React, {cloneElement} from 'react'; import ReactDOM from 'react-dom'; import domUtils from './utils/domUtils'; +import bootstrapUtils from './utils/bootstrapUtils'; import getScrollbarSize from 'dom-helpers/util/scrollbarSize'; import EventListener from './utils/EventListener'; import createChainedFunction from './utils/createChainedFunction'; @@ -210,8 +211,9 @@ const Modal = React.createClass({ }, renderBackdrop(modal) { - let { animation, bsClass } = this.props; + let { animation } = this.props; let duration = Modal.BACKDROP_TRANSITION_DURATION; + let prefix = bootstrapUtils.prefix(this.props); // Don't handle clicks for "static" backdrops let onClick = this.props.backdrop === true ? @@ -220,7 +222,7 @@ const Modal = React.createClass({ let backdrop = (
    ); diff --git a/src/ModalBody.js b/src/ModalBody.js index 56da974930..33e48c9ab2 100644 --- a/src/ModalBody.js +++ b/src/ModalBody.js @@ -1,12 +1,13 @@ import React from 'react'; import classNames from 'classnames'; +import tbsUtils from './utils/bootstrapUtils'; class ModalBody extends React.Component { render() { return (
    + className={classNames(this.props.className, tbsUtils.prefix(this.props, 'body'))}> {this.props.children}
    ); @@ -17,11 +18,11 @@ ModalBody.propTypes = { /** * A css class applied to the Component */ - modalClassName: React.PropTypes.string + bsClass: React.PropTypes.string }; ModalBody.defaultProps = { - modalClassName: 'modal-body' + bsClass: 'modal' }; diff --git a/src/ModalDialog.js b/src/ModalDialog.js index 54929a4870..bd2d01ccd3 100644 --- a/src/ModalDialog.js +++ b/src/ModalDialog.js @@ -1,10 +1,10 @@ /* eslint-disable react/prop-types */ import React from 'react'; import classNames from 'classnames'; -import BootstrapMixin from './BootstrapMixin'; +import bootstrapUtils, { bsSizes } from './utils/bootstrapUtils'; +import { Sizes } from './styleMaps'; const ModalDialog = React.createClass({ - mixins: [BootstrapMixin], propTypes: { /** @@ -33,11 +33,11 @@ const ModalDialog = React.createClass({ display: 'block', ...this.props.style }; - let bsClass = this.props.bsClass; - let dialogClasses = this.getBsClassSet(); + let prefix = bootstrapUtils.prefix(this.props); + let dialogClasses = bootstrapUtils.getClassSet(this.props); delete dialogClasses.modal; - dialogClasses[`${bsClass}-dialog`] = true; + dialogClasses[`${prefix}-dialog`] = true; return (
    + className={classNames(this.props.className, prefix)}>
    -
    +
    { this.props.children }
    @@ -57,4 +57,6 @@ const ModalDialog = React.createClass({ } }); -export default ModalDialog; +export default bsSizes([Sizes.LARGE, Sizes.SMALL], + ModalDialog +); diff --git a/src/ModalFooter.js b/src/ModalFooter.js index feee8d8bf5..235ca6a4a1 100644 --- a/src/ModalFooter.js +++ b/src/ModalFooter.js @@ -1,12 +1,13 @@ import React from 'react'; import classNames from 'classnames'; +import tbsUtils from './utils/bootstrapUtils'; class ModalFooter extends React.Component { render() { return (
    + className={classNames(this.props.className, tbsUtils.prefix(this.props, 'footer'))}> {this.props.children}
    ); @@ -17,11 +18,11 @@ ModalFooter.propTypes = { /** * A css class applied to the Component */ - modalClassName: React.PropTypes.string + bsClass: React.PropTypes.string }; ModalFooter.defaultProps = { - modalClassName: 'modal-footer' + bsClass: 'modal' }; export default ModalFooter; diff --git a/src/ModalHeader.js b/src/ModalHeader.js index de7add8ad2..ee13a2f751 100644 --- a/src/ModalHeader.js +++ b/src/ModalHeader.js @@ -1,12 +1,13 @@ import React from 'react'; import classNames from 'classnames'; +import tbsUtils from './utils/bootstrapUtils'; class ModalHeader extends React.Component { render() { return (
    + className={classNames(this.props.className, tbsUtils.prefix(this.props, 'header'))}> { this.props.closeButton && @@ -196,4 +200,10 @@ const Navbar = React.createClass({ }); -export default Navbar; +const NAVBAR_STATES = [DEFAULT, INVERSE]; + +export default bsStyles(NAVBAR_STATES, DEFAULT, + bsClass('navbar', + Navbar + ) +); diff --git a/src/Pagination.js b/src/Pagination.js index f2990bf505..9b9b9fd04c 100644 --- a/src/Pagination.js +++ b/src/Pagination.js @@ -1,12 +1,11 @@ import React from 'react'; import classNames from 'classnames'; -import BootstrapMixin from './BootstrapMixin'; +import bootstrapUtils, { bsClass } from './utils/bootstrapUtils'; import PaginationButton from './PaginationButton'; import elementType from 'react-prop-types/lib/elementType'; import SafeAnchor from './SafeAnchor'; const Pagination = React.createClass({ - mixins: [BootstrapMixin], propTypes: { activePage: React.PropTypes.number, @@ -214,7 +213,7 @@ const Pagination = React.createClass({ return (
      + className={classNames(this.props.className, bootstrapUtils.getClassSet(this.props))}> {this.renderFirst()} {this.renderPrev()} {this.renderPageButtons()} @@ -225,4 +224,4 @@ const Pagination = React.createClass({ } }); -export default Pagination; +export default bsClass('pagination', Pagination); diff --git a/src/PaginationButton.js b/src/PaginationButton.js index 83aa81232e..ae52b04243 100644 --- a/src/PaginationButton.js +++ b/src/PaginationButton.js @@ -1,11 +1,9 @@ import React from 'react'; import classNames from 'classnames'; -import BootstrapMixin from './BootstrapMixin'; import createSelectedEvent from './utils/createSelectedEvent'; import elementType from 'react-prop-types/lib/elementType'; const PaginationButton = React.createClass({ - mixins: [BootstrapMixin], propTypes: { className: React.PropTypes.string, @@ -43,8 +41,7 @@ const PaginationButton = React.createClass({ render() { let classes = { active: this.props.active, - disabled: this.props.disabled, - ...this.getBsClassSet() + disabled: this.props.disabled }; let { diff --git a/src/Panel.js b/src/Panel.js index adb8e342cf..9ecf5d4893 100644 --- a/src/Panel.js +++ b/src/Panel.js @@ -1,11 +1,10 @@ import React, { cloneElement } from 'react'; import classNames from 'classnames'; - -import BootstrapMixin from './BootstrapMixin'; +import bootstrapUtils, { bsStyles, bsClass } from './utils/bootstrapUtils'; +import { State, PRIMARY, DEFAULT } from './styleMaps'; import Collapse from './Collapse'; -const Panel = React.createClass({ - mixins: [BootstrapMixin], +let Panel = React.createClass({ propTypes: { collapsible: React.PropTypes.bool, @@ -25,8 +24,6 @@ const Panel = React.createClass({ getDefaultProps() { return { - bsClass: 'panel', - bsStyle: 'default', defaultExpanded: false }; }, @@ -63,7 +60,7 @@ const Panel = React.createClass({ let {headerRole, panelRole, ...props} = this.props; return (
      {this.renderHeading(headerRole)} {this.props.collapsible ? this.renderCollapsibleBody(panelRole) : this.renderBody()} @@ -74,7 +71,7 @@ const Panel = React.createClass({ renderCollapsibleBody(panelRole) { let props = { - className: this.prefixClass('collapse'), + className: bootstrapUtils.prefix(this.props, 'collapse'), id: this.props.id, ref: 'panel', 'aria-hidden': !this.isExpanded() @@ -97,7 +94,7 @@ const Panel = React.createClass({ let allChildren = this.props.children; let bodyElements = []; let panelBodyChildren = []; - let bodyClass = this.prefixClass('body'); + let bodyClass = bootstrapUtils.prefix(this.props, 'body'); function getProps() { return {key: bodyElements.length}; @@ -165,7 +162,7 @@ const Panel = React.createClass({ this.renderCollapsibleTitle(header, headerRole) : header; } else { const className = classNames( - this.prefixClass('title'), header.props.className + bootstrapUtils.prefix(this.props, 'title'), header.props.className ); if (this.props.collapsible) { @@ -179,7 +176,7 @@ const Panel = React.createClass({ } return ( -
      +
      {header}
      ); @@ -202,7 +199,7 @@ const Panel = React.createClass({ renderCollapsibleTitle(header, headerRole) { return ( -

      +

      {this.renderAnchor(header, headerRole)}

      ); @@ -214,11 +211,15 @@ const Panel = React.createClass({ } return ( -
      +
      {this.props.footer}
      ); } }); -export default Panel; +const PANEL_STATES = State.values().concat(DEFAULT, PRIMARY); + +export default bsStyles(PANEL_STATES, DEFAULT, + bsClass('panel', Panel) +); diff --git a/src/PanelGroup.js b/src/PanelGroup.js index 45354acb1a..cddb76fe20 100644 --- a/src/PanelGroup.js +++ b/src/PanelGroup.js @@ -1,14 +1,11 @@ -/* eslint react/prop-types: [2, {ignore: "bsStyle"}] */ -/* BootstrapMixin contains `bsStyle` type validation */ - import React, { cloneElement } from 'react'; import classNames from 'classnames'; -import BootstrapMixin from './BootstrapMixin'; +import bootstrapUtils, { bsClass } from './utils/bootstrapUtils'; import ValidComponentChildren from './utils/ValidComponentChildren'; const PanelGroup = React.createClass({ - mixins: [BootstrapMixin], + propTypes: { accordion: React.PropTypes.bool, @@ -21,8 +18,7 @@ const PanelGroup = React.createClass({ getDefaultProps() { return { - accordion: false, - bsClass: 'panel-group' + accordion: false }; }, @@ -35,7 +31,7 @@ const PanelGroup = React.createClass({ }, render() { - let classes = this.getBsClassSet(); + let classes = bootstrapUtils.getClassSet(this.props); let {className, ...props} = this.props; if (this.props.accordion) { props.role = 'tablist'; } return ( @@ -93,4 +89,4 @@ const PanelGroup = React.createClass({ } }); -export default PanelGroup; +export default bsClass('panel-group', PanelGroup); diff --git a/src/Popover.js b/src/Popover.js index 4a36f77880..de3906ddea 100644 --- a/src/Popover.js +++ b/src/Popover.js @@ -1,13 +1,12 @@ import React from 'react'; import classNames from 'classnames'; -import BootstrapMixin from './BootstrapMixin'; +import tbsUtils from './utils/bootstrapUtils'; import isRequiredForA11y from 'react-prop-types/lib/isRequiredForA11y'; const Popover = React.createClass({ - mixins: [ BootstrapMixin ], - propTypes: { + /** * An html id attribute, necessary for accessibility * @type {string} @@ -53,13 +52,14 @@ const Popover = React.createClass({ getDefaultProps() { return { - placement: 'right' + placement: 'right', + bsClass: 'popover' }; }, render() { const classes = { - 'popover': true, + [tbsUtils.prefix(this.props)]: true, [this.props.placement]: true }; @@ -80,7 +80,7 @@ const Popover = React.createClass({
      {this.props.title ? this.renderTitle() : null} -
      +
      {this.props.children}
      @@ -89,7 +89,9 @@ const Popover = React.createClass({ renderTitle() { return ( -

      {this.props.title}

      +

      + {this.props.title} +

      ); } }); diff --git a/src/ProgressBar.js b/src/ProgressBar.js index 61edf3835e..bc9cbfa520 100644 --- a/src/ProgressBar.js +++ b/src/ProgressBar.js @@ -1,49 +1,34 @@ -/* eslint react/prop-types: [2, {ignore: "bsStyle"}] */ -/* BootstrapMixin contains `bsStyle` type validation */ - import React, { cloneElement, PropTypes } from 'react'; import Interpolate from './Interpolate'; -import BootstrapMixin from './BootstrapMixin'; +import bootstrapUtils, { bsStyles, bsClass } from './utils/bootstrapUtils'; +import { State } from './styleMaps'; import classNames from 'classnames'; - import ValidComponentChildren from './utils/ValidComponentChildren'; -const ProgressBar = React.createClass({ - propTypes: { - min: PropTypes.number, - now: PropTypes.number, - max: PropTypes.number, - label: PropTypes.node, - srOnly: PropTypes.bool, - striped: PropTypes.bool, - active: PropTypes.bool, - children: onlyProgressBar, // eslint-disable-line no-use-before-define - className: React.PropTypes.string, - interpolateClass: PropTypes.node, - /** - * @private - */ - isChild: PropTypes.bool - }, - - mixins: [BootstrapMixin], - - getDefaultProps() { - return { - bsClass: 'progress-bar', - min: 0, - max: 100, - active: false, - isChild: false, - srOnly: false, - striped: false - }; - }, +/** + * Custom propTypes checker + */ +function onlyProgressBar(props, propName, componentName) { + if (props[propName]) { + let error, childIdentifier; + + React.Children.forEach(props[propName], (child) => { + if (child.type !== ProgressBar) { //eslint-disable-line + childIdentifier = (child.type.displayName ? child.type.displayName : child.type); + error = new Error(`Children of ${componentName} can contain only ProgressBar components. Found ${childIdentifier}`); + } + }); + + return error; + } +} + +class ProgressBar extends React.Component { getPercentage(now, min, max) { const roundPrecision = 1000; return Math.round(((now - min) / (max - min) * 100) * roundPrecision) / roundPrecision; - }, + } render() { if (this.props.isChild) { @@ -70,14 +55,14 @@ const ProgressBar = React.createClass({ {content}
      ); - }, + } renderChildBar(child, index) { return cloneElement(child, { isChild: true, key: child.key ? child.key : index }); - }, + } renderProgressBar() { let { className, label, now, min, max, ...props } = this.props; @@ -98,9 +83,9 @@ const ProgressBar = React.createClass({ ); } - const classes = classNames(className, this.getBsClassSet(), { + const classes = classNames(className, bootstrapUtils.getClassSet(this.props), { active: this.props.active, - 'progress-bar-striped': this.props.active || this.props.striped + [bootstrapUtils.prefix(this.props, 'striped')]: this.props.active || this.props.striped }); return ( @@ -115,7 +100,7 @@ const ProgressBar = React.createClass({ {label}
      ); - }, + } renderLabel(percentage) { const InterpolateClass = this.props.interpolateClass || Interpolate; @@ -131,24 +116,38 @@ const ProgressBar = React.createClass({ ); } -}); - -/** - * Custom propTypes checker - */ -function onlyProgressBar(props, propName, componentName) { - if (props[propName]) { - let error, childIdentifier; - - React.Children.forEach(props[propName], (child) => { - if (child.type !== ProgressBar) { - childIdentifier = (child.type.displayName ? child.type.displayName : child.type); - error = new Error(`Children of ${componentName} can contain only ProgressBar components. Found ${childIdentifier}`); - } - }); - - return error; - } } -export default ProgressBar; +ProgressBar.propTypes = { + ...ProgressBar.propTypes, + min: PropTypes.number, + now: PropTypes.number, + max: PropTypes.number, + label: PropTypes.node, + srOnly: PropTypes.bool, + striped: PropTypes.bool, + active: PropTypes.bool, + children: onlyProgressBar, + className: React.PropTypes.string, + interpolateClass: PropTypes.node, + /** + * @private + */ + isChild: PropTypes.bool +}; + +ProgressBar.defaultProps = { + ...ProgressBar.defaultProps, + min: 0, + max: 100, + active: false, + isChild: false, + srOnly: false, + striped: false +}; + +export default bsStyles(State.values(), + bsClass('progress-bar', + ProgressBar + ) +); diff --git a/src/SplitButton.js b/src/SplitButton.js index 4aec5d3403..df599394ca 100644 --- a/src/SplitButton.js +++ b/src/SplitButton.js @@ -1,5 +1,4 @@ import React from 'react'; -import BootstrapMixin from './BootstrapMixin'; import Button from './Button'; import Dropdown from './Dropdown'; import SplitToggle from './SplitToggle'; @@ -13,8 +12,7 @@ class SplitButton extends React.Component { onClick, target, href, - // bsStyle is validated by 'Button' component - bsStyle, // eslint-disable-line + bsStyle, ...props } = this.props; let { disabled } = props; @@ -50,7 +48,7 @@ class SplitButton extends React.Component { SplitButton.propTypes = { ...Dropdown.propTypes, - ...BootstrapMixin.propTypes, + bsStyle: Button.propTypes.bsStyle, /** * @private diff --git a/src/SubNav.js b/src/SubNav.js index a4b9b4af6e..01241710a9 100644 --- a/src/SubNav.js +++ b/src/SubNav.js @@ -3,11 +3,10 @@ import classNames from 'classnames'; import ValidComponentChildren from './utils/ValidComponentChildren'; import createChainedFunction from './utils/createChainedFunction'; -import BootstrapMixin from './BootstrapMixin'; import SafeAnchor from './SafeAnchor'; const SubNav = React.createClass({ - mixins: [BootstrapMixin], + propTypes: { onSelect: React.PropTypes.func, diff --git a/src/Tab.js b/src/Tab.js index 2e1ce776a0..1182330adf 100644 --- a/src/Tab.js +++ b/src/Tab.js @@ -1,7 +1,7 @@ -import classNames from 'classnames'; import React from 'react'; import ReactDOM from 'react-dom'; - +import classNames from 'classnames'; +import tbsUtils from './utils/bootstrapUtils'; import TransitionEvents from './utils/TransitionEvents'; const Tab = React.createClass({ @@ -26,6 +26,7 @@ const Tab = React.createClass({ getDefaultProps() { return { + bsClass: 'tab', animation: true }; }, @@ -85,7 +86,7 @@ const Tab = React.createClass({ render() { let classes = { - 'tab-pane': true, + [tbsUtils.prefix(this.props, 'pane')]: true, 'fade': true, 'active': this.props.active || this.state.animateOut, 'in': this.props.active && !this.state.animateIn diff --git a/src/Tabs.js b/src/Tabs.js index d72df15543..d806cd865a 100644 --- a/src/Tabs.js +++ b/src/Tabs.js @@ -1,13 +1,13 @@ import classNames from 'classnames'; import React, { cloneElement } from 'react'; import ReactDOM from 'react-dom'; - import Col from './Col'; import Nav from './Nav'; import NavItem from './NavItem'; import styleMaps from './styleMaps'; import keycode from 'keycode'; import createChainedFunction from './utils/createChainedFunction'; +import tbsUtils from './utils/bootstrapUtils'; import ValidComponentChildren from './utils/ValidComponentChildren'; let paneId = (props, child) => child.props.id ? child.props.id : props.id && (props.id + '___pane___' + child.props.eventKey); @@ -97,6 +97,7 @@ const Tabs = React.createClass({ getDefaultProps() { return { + bsClass: 'tab', animation: true, tabWidth: 2, position: 'top', @@ -178,6 +179,7 @@ const Tabs = React.createClass({ const tabsProps = { ...props, bsStyle, + bsClass: undefined, stacked: isHorizontal, activeKey: this.getActiveKey(), onSelect: this.handleSelect, @@ -187,7 +189,7 @@ const Tabs = React.createClass({ const childTabs = ValidComponentChildren.map(children, this.renderTab); const panesProps = { - className: 'tab-content', + className: tbsUtils.prefix(this.props, 'content'), ref: 'panes' }; const childPanes = ValidComponentChildren.map(children, this.renderPane); diff --git a/src/Thumbnail.js b/src/Thumbnail.js index 09822ea916..5f94abf088 100644 --- a/src/Thumbnail.js +++ b/src/Thumbnail.js @@ -1,10 +1,9 @@ import React from 'react'; import classSet from 'classnames'; -import BootstrapMixin from './BootstrapMixin'; import SafeAnchor from './SafeAnchor'; +import bootstrapUtils, { bsClass } from './utils/bootstrapUtils'; const Thumbnail = React.createClass({ - mixins: [BootstrapMixin], propTypes: { alt: React.PropTypes.string, @@ -12,14 +11,8 @@ const Thumbnail = React.createClass({ src: React.PropTypes.string }, - getDefaultProps() { - return { - bsClass: 'thumbnail' - }; - }, - render() { - let classes = this.getBsClassSet(); + let classes = bootstrapUtils.getClassSet(this.props); if (this.props.href) { return ( @@ -48,4 +41,4 @@ const Thumbnail = React.createClass({ } }); -export default Thumbnail; +export default bsClass('thumbnail', Thumbnail); diff --git a/src/Tooltip.js b/src/Tooltip.js index a76b855dee..5af78fb47a 100644 --- a/src/Tooltip.js +++ b/src/Tooltip.js @@ -1,81 +1,87 @@ -import classNames from 'classnames'; import React from 'react'; +import classNames from 'classnames'; +import tbsUtils from './utils/bootstrapUtils'; import isRequiredForA11y from 'react-prop-types/lib/isRequiredForA11y'; -export default class Tooltip extends React.Component { +const Tooltip = React.createClass({ + + propTypes: { + /** + * An html id attribute, necessary for accessibility + * @type {string} + * @required + */ + id: isRequiredForA11y( + React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.number + ]) + ), + + /** + * Sets the direction the Tooltip is positioned towards. + */ + placement: React.PropTypes.oneOf(['top', 'right', 'bottom', 'left']), + + /** + * The "left" position value for the Tooltip. + */ + positionLeft: React.PropTypes.number, + /** + * The "top" position value for the Tooltip. + */ + positionTop: React.PropTypes.number, + /** + * The "left" position value for the Tooltip arrow. + */ + arrowOffsetLeft: React.PropTypes.oneOfType([ + React.PropTypes.number, React.PropTypes.string + ]), + /** + * The "top" position value for the Tooltip arrow. + */ + arrowOffsetTop: React.PropTypes.oneOfType([ + React.PropTypes.number, React.PropTypes.string + ]), + /** + * Title text + */ + title: React.PropTypes.node + }, + + getDefaultProps() { + return { + bsClass: 'tooltip', + placement: 'right' + }; + }, + render() { - const { - placement, - positionLeft, - positionTop, - arrowOffsetLeft, - arrowOffsetTop, - className, - style, - children, - ...props - } = this.props; + const classes = { + [tbsUtils.prefix(this.props)]: true, + [this.props.placement]: true + }; - return ( -
      -
      + const style = { + 'left': this.props.positionLeft, + 'top': this.props.positionTop, + ...this.props.style + }; + + const arrowStyle = { + 'left': this.props.arrowOffsetLeft, + 'top': this.props.arrowOffsetTop + }; -
      - {children} + return ( +
      +
      +
      + {this.props.children}
      ); } -} - -Tooltip.propTypes = { - /** - * An html id attribute, necessary for accessibility - * @type {string} - * @required - */ - id: isRequiredForA11y( - React.PropTypes.oneOfType([ - React.PropTypes.string, - React.PropTypes.number - ]) - ), - - /** - * The direction the tooltip is positioned towards - */ - placement: React.PropTypes.oneOf(['top', 'right', 'bottom', 'left']), - - /** - * The `left` position value for the tooltip - */ - positionLeft: React.PropTypes.number, - /** - * The `top` position value for the tooltip - */ - positionTop: React.PropTypes.number, - /** - * The `left` position value for the tooltip arrow - */ - arrowOffsetLeft: React.PropTypes.oneOfType([ - React.PropTypes.number, React.PropTypes.string - ]), - /** - * The `top` position value for the tooltip arrow - */ - arrowOffsetTop: React.PropTypes.oneOfType([ - React.PropTypes.number, React.PropTypes.string - ]) -}; +}); -Tooltip.defaultProps = { - placement: 'right' -}; +export default Tooltip; diff --git a/src/Well.js b/src/Well.js index 20398b040e..22d0f9cb69 100644 --- a/src/Well.js +++ b/src/Well.js @@ -1,18 +1,13 @@ import React from 'react'; import classNames from 'classnames'; -import BootstrapMixin from './BootstrapMixin'; - -const Well = React.createClass({ - mixins: [BootstrapMixin], - - getDefaultProps() { - return { - bsClass: 'well' - }; - }, +import bootstrapUtils, { bsSizes, bsClass } from './utils/bootstrapUtils'; +import { Sizes } from './styleMaps'; +@bsClass('well') +@bsSizes([Sizes.LARGE, Sizes.SMALL]) +class Well extends React.Component { render() { - let classes = this.getBsClassSet(); + let classes = bootstrapUtils.getClassSet(this.props); return (
      @@ -20,6 +15,6 @@ const Well = React.createClass({
      ); } -}); +} export default Well; diff --git a/src/index.js b/src/index.js index 7461e38dcc..19c15517e3 100644 --- a/src/index.js +++ b/src/index.js @@ -3,7 +3,6 @@ export Affix from './Affix'; export AffixMixin from './AffixMixin'; export Alert from './Alert'; export Badge from './Badge'; -export BootstrapMixin from './BootstrapMixin'; export Breadcrumb from './Breadcrumb'; export BreadcrumbItem from './BreadcrumbItem'; export Button from './Button'; @@ -51,7 +50,6 @@ export Row from './Row'; export SafeAnchor from './SafeAnchor'; export SplitButton from './SplitButton'; export SplitButton from './SplitButton'; -export styleMaps from './styleMaps'; export SubNav from './SubNav'; export Tab from './Tab'; export Table from './Table'; @@ -68,9 +66,10 @@ export * as FormControls from './FormControls'; import childrenValueInputValidation from './utils/childrenValueInputValidation'; import createChainedFunction from './utils/createChainedFunction'; import ValidComponentChildren from './utils/ValidComponentChildren'; - +import bootstrapUtils from './utils/bootstrapUtils'; export const utils = { + bootstrapUtils, childrenValueInputValidation, createChainedFunction, ValidComponentChildren diff --git a/src/styleMaps.js b/src/styleMaps.js index 4360d1a675..fc5297ab40 100644 --- a/src/styleMaps.js +++ b/src/styleMaps.js @@ -1,41 +1,15 @@ + +let constant = obj => { + return Object.assign( + Object.create({ + values() { + return Object.keys(this).map(k => this[k]); + } + }), obj); +}; + const styleMaps = { - CLASSES: { - 'alert': 'alert', - 'button': 'btn', - 'button-group': 'btn-group', - 'button-toolbar': 'btn-toolbar', - 'column': 'col', - 'input-group': 'input-group', - 'form': 'form', - 'glyphicon': 'glyphicon', - 'label': 'label', - 'thumbnail': 'thumbnail', - 'list-group-item': 'list-group-item', - 'panel': 'panel', - 'panel-group': 'panel-group', - 'pagination': 'pagination', - 'progress-bar': 'progress-bar', - 'nav': 'nav', - 'navbar': 'navbar', - 'modal': 'modal', - 'row': 'row', - 'well': 'well' - }, - STYLES: [ - 'default', - 'primary', - 'success', - 'info', - 'warning', - 'danger', - 'link', - 'inline', - 'tabs', - 'pills' - ], - addStyle(name) { - styleMaps.STYLES.push(name); - }, + SIZES: { 'large': 'lg', 'medium': 'md', @@ -49,4 +23,23 @@ const styleMaps = { GRID_COLUMNS: 12 }; +export const Sizes = constant({ + LARGE: 'large', + MEDIUM: 'medium', + SMALL: 'small', + XSMALL: 'xsmall' +}); + +export const State = constant({ + SUCCESS: 'success', + WARNING: 'warning', + DANGER: 'danger', + INFO: 'info' +}); + +export const DEFAULT = 'default'; +export const PRIMARY = 'primary'; +export const LINK = 'link'; +export const INVERSE = 'inverse'; + export default styleMaps; diff --git a/src/utils/bootstrapUtils.js b/src/utils/bootstrapUtils.js new file mode 100644 index 0000000000..9cae75e759 --- /dev/null +++ b/src/utils/bootstrapUtils.js @@ -0,0 +1,152 @@ +import { PropTypes } from 'react'; +import styleMaps from '../styleMaps'; +import invariant from 'invariant'; +import warning from 'warning'; + +function curry(fn) { + return (...args) => { + let last = args[args.length - 1]; + if (typeof last === 'function') { + return fn(...args); + } + return Component => fn(...args, Component); + }; +} + +function prefix(props = {}, variant) { + invariant((props.bsClass || '').trim(), 'A `bsClass` prop is required for this component'); + return props.bsClass + (variant ? '-' + variant : ''); +} + +export let bsClass = curry((defaultClass, Component) => { + let propTypes = Component.propTypes || (Component.propTypes = {}); + let defaultProps = Component.defaultProps || (Component.defaultProps = {}); + + propTypes.bsClass = PropTypes.string; + defaultProps.bsClass = defaultClass; + + return Component; +}); + +export let bsStyles = curry((styles, defaultStyle, Component) => { + if (typeof defaultStyle !== 'string') { + Component = defaultStyle; + defaultStyle = undefined; + } + + let existing = Component.STYLES || []; + let propTypes = Component.propTypes || {}; + + styles.forEach(style => { + if (existing.indexOf(style) === -1) { + existing.push(style); + } + }); + + let propType = PropTypes.oneOf(existing); + + // expose the values on the propType function for documentation + Component.STYLES = propType._values = existing; + + Component.propTypes = { + ...propTypes, + bsStyle: propType + }; + + if (defaultStyle !== undefined) { + let defaultProps = Component.defaultProps || (Component.defaultProps = {}); + defaultProps.bsStyle = defaultStyle; + } + + return Component; +}); + +export let bsSizes = curry((sizes, defaultSize, Component) => { + if (typeof defaultSize !== 'string') { + Component = defaultSize; + defaultSize = undefined; + } + + let existing = Component.SIZES || []; + let propTypes = Component.propTypes || {}; + + sizes.forEach(size => { + if (existing.indexOf(size) === -1) { + existing.push(size); + } + }); + + let values = existing.reduce((result, size) => { + if (styleMaps.SIZES[size] && styleMaps.SIZES[size] !== size) { + result.push(styleMaps.SIZES[size]); + } + return result.concat(size); + }, []); + + let propType = PropTypes.oneOf(values); + + propType._values = values; + + // expose the values on the propType function for documentation + Component.SIZES = existing; + + Component.propTypes = { + ...propTypes, + bsSize: propType + }; + + if (defaultSize !== undefined) { + let defaultProps = Component.defaultProps || (Component.defaultProps = {}); + defaultProps.bsSize = defaultSize; + } + + return Component; +}); + +export default { + + prefix, + + getClassSet(props) { + let classes = {}; + let bsClassName = prefix(props); + + if (bsClassName) { + let bsSize; + + classes[bsClassName] = true; + + if (props.bsSize) { + bsSize = styleMaps.SIZES[props.bsSize] || bsSize; + } + + if (bsSize) { + classes[prefix(props, bsSize)] = true; + } + + if (props.bsStyle) { + if (props.bsStyle.indexOf(prefix(props)) === 0) { + warning(false, // small migration convenience, since the old method required manual prefixing + 'bsStyle will automatically prefix custom values with the bsClass, so there is no ' + + 'need to append it manually. (bsStyle: ' + props.bsStyle + ', bsClass: ' + prefix(props) + ')' + ); + classes[props.bsStyle] = true; + } else { + classes[prefix(props, props.bsStyle)] = true; + } + } + } + + return classes; + }, + + /** + * Add a style variant to a Component. Mutates the propTypes of the component + * in order to validate the new variant. + */ + addStyle(Component, styleVariant) { + bsStyles(styleVariant, Component); + } +}; + +export let _curry = curry; diff --git a/test/BootstrapMixinSpec.js b/test/BootstrapMixinSpec.js deleted file mode 100644 index 560770162d..0000000000 --- a/test/BootstrapMixinSpec.js +++ /dev/null @@ -1,124 +0,0 @@ -import React from 'react'; -import ReactTestUtils from 'react/lib/ReactTestUtils'; -import BootstrapMixin from '../src/BootstrapMixin'; -import styleMaps from '../src/styleMaps'; -import { shouldWarn } from './helpers'; - -let Component; - -describe('BootstrapMixin', () => { - beforeEach(() => { - Component = React.createClass({ - mixins: [BootstrapMixin], - - render() { - return React.DOM.button(this.props); - } - }); - }); - - describe('#getBsClassSet', () => { - it('should return blank', () => { - let instance = ReactTestUtils.renderIntoDocument( - - content - - ); - assert.deepEqual(instance.getBsClassSet(), {}); - }); - - it('maps and validates OK default classes', () => { - function instanceClassSet(bsClass) { - let instance = ReactTestUtils.renderIntoDocument( - - content - - ); - return instance.getBsClassSet(); - } - - assert.deepEqual(instanceClassSet('column'), {'col': true}); - assert.deepEqual(instanceClassSet('button'), {'btn': true}); - assert.deepEqual(instanceClassSet('button-group'), {'btn-group': true}); - assert.deepEqual(instanceClassSet('label'), {'label': true}); - assert.deepEqual(instanceClassSet('alert'), {'alert': true}); - assert.deepEqual(instanceClassSet('input-group'), {'input-group': true}); - assert.deepEqual(instanceClassSet('form'), {'form': true}); - assert.deepEqual(instanceClassSet('panel'), {'panel': true}); - }); - - describe('Predefined Bootstrap styles', () => { - it('maps and validates OK default styles', () => { - function instanceClassSet(style) { - let instance = ReactTestUtils.renderIntoDocument( - - content - - ); - return instance.getBsClassSet(); - } - - assert.deepEqual(instanceClassSet('default'), {'btn': true, 'btn-default': true}); - assert.deepEqual(instanceClassSet('primary'), {'btn': true, 'btn-primary': true}); - assert.deepEqual(instanceClassSet('success'), {'btn': true, 'btn-success': true}); - assert.deepEqual(instanceClassSet('info'), {'btn': true, 'btn-info': true}); - assert.deepEqual(instanceClassSet('link'), {'btn': true, 'btn-link': true}); - assert.deepEqual(instanceClassSet('inline'), {'btn': true, 'btn-inline': true}); - }); - }); - - describe('Sizes', () => { - it('maps english words for sizes to bootstrap sizes constants', () => { - function instanceClassSet(size) { - let instance = ReactTestUtils.renderIntoDocument( - - content - - ); - return instance.getBsClassSet(); - } - - assert.deepEqual(instanceClassSet('large'), {'btn': true, 'btn-lg': true}); - assert.deepEqual(instanceClassSet('small'), {'btn': true, 'btn-sm': true}); - assert.deepEqual(instanceClassSet('medium'), {'btn': true, 'btn-md': true}); - assert.deepEqual(instanceClassSet('xsmall'), {'btn': true, 'btn-xs': true}); - }); - }); - - describe('Custom styles', () => { - it('should validate OK custom styles added via "addStyle()"', () => { - - styleMaps.addStyle('wacky'); - - let instance = ReactTestUtils.renderIntoDocument( - - content - - ); - assert.deepEqual(instance.getBsClassSet(), {'btn': true, 'btn-wacky': true}); - }); - - it('should allow custom styles as is but with validation warning', () => { - let instance = ReactTestUtils.renderIntoDocument( - - content - - ); - assert.deepEqual(instance.getBsClassSet(), {'btn': true, 'my-custom-class': true}); - shouldWarn('Invalid prop `bsStyle` of value `my-custom-class`'); - }); - }); - }); - - // todo: fix bad naming - describe('#prefixClass', () => { - it('allows custom sub-classes', () => { - let instance = ReactTestUtils.renderIntoDocument( - - content - - ); - assert.equal(instance.prefixClass('title'), 'btn-title'); - }); - }); -}); diff --git a/test/ButtonGroupSpec.js b/test/ButtonGroupSpec.js index 411ac7712d..66b7447672 100644 --- a/test/ButtonGroupSpec.js +++ b/test/ButtonGroupSpec.js @@ -22,7 +22,7 @@ describe('ButtonGroup', () => { it('Should add size', () => { let instance = ReactTestUtils.renderIntoDocument( - + diff --git a/test/ModalSpec.js b/test/ModalSpec.js index 332905d321..0ab4546ab6 100644 --- a/test/ModalSpec.js +++ b/test/ModalSpec.js @@ -4,7 +4,7 @@ import ReactDOM from 'react-dom'; import Modal from '../src/Modal'; -import {getOne, render, shouldWarn} from './helpers'; +import {getOne, render } from './helpers'; describe('Modal', () => { let mountPoint; @@ -128,7 +128,7 @@ describe('Modal', () => { it('Should pass className to the dialog', () => { let noOp = () => {}; let instance = render( - + Message , mountPoint); @@ -141,7 +141,7 @@ describe('Modal', () => { it('Should use bsClass on the dialog', () => { let noOp = () => {}; let instance = render( - + Message , mountPoint); @@ -153,9 +153,6 @@ describe('Modal', () => { assert.ok(dialog.children[0].children[0].className.match(/\bmymodal-content\b/)); assert.ok(instance.refs.backdrop.className.match(/\bmymodal-backdrop\b/)); - - - shouldWarn("Invalid prop 'bsClass' of value 'mymodal'"); }); it('Should pass bsSize to the dialog', () => { diff --git a/test/ProgressBarSpec.js b/test/ProgressBarSpec.js index 8ab3d1965c..0a6654b0fa 100644 --- a/test/ProgressBarSpec.js +++ b/test/ProgressBarSpec.js @@ -24,23 +24,15 @@ describe('ProgressBar', () => { it('Should have the default class', () => { let instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.ok(getProgressBarNode(instance).className.match(/\bprogress-bar-default\b/)); - }); - - it('Should have the primary class', () => { - let instance = ReactTestUtils.renderIntoDocument( - + ); - assert.ok(getProgressBarNode(instance).className.match(/\bprogress-bar-primary\b/)); + assert.ok(getProgressBarNode(instance).className.match(/\bprogress-bar\b/)); }); it('Should have the success class', () => { let instance = ReactTestUtils.renderIntoDocument( - + ); assert.ok(getProgressBarNode(instance).className.match(/\bprogress-bar-success\b/)); @@ -48,7 +40,7 @@ describe('ProgressBar', () => { it('Should have the warning class', () => { let instance = ReactTestUtils.renderIntoDocument( - + ); assert.ok(getProgressBarNode(instance).className.match(/\bprogress-bar-warning\b/)); @@ -98,7 +90,7 @@ describe('ProgressBar', () => { it('Should not have label', () => { let instance = ReactTestUtils.renderIntoDocument( - + ); assert.equal(ReactDOM.findDOMNode(instance).innerText, ''); @@ -106,21 +98,21 @@ describe('ProgressBar', () => { it('Should have label', () => { let instance = ReactTestUtils.renderIntoDocument( - + ); - assert.equal(ReactDOM.findDOMNode(instance).innerText, 'min:0, max:10, now:5, percent:50, bsStyle:primary'); + assert.equal(ReactDOM.findDOMNode(instance).innerText, 'min:0, max:10, now:5, percent:50, bsStyle:success'); }); it('Should have screen reader only label', () => { let instance = ReactTestUtils.renderIntoDocument( - + ); let srLabel = ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'sr-only'); - assert.equal(srLabel.innerText, 'min:0, max:10, now:5, percent:50, bsStyle:primary'); + assert.equal(srLabel.innerText, 'min:0, max:10, now:5, percent:50, bsStyle:success'); }); it('Should have a label that is a React component', () => { @@ -129,7 +121,7 @@ describe('ProgressBar', () => { ); let instance = ReactTestUtils.renderIntoDocument( - + ); assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'special-label')); @@ -141,7 +133,7 @@ describe('ProgressBar', () => { ); let instance = ReactTestUtils.renderIntoDocument( - + ); let srLabel = ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'sr-only'); diff --git a/test/helpers.js b/test/helpers.js index cabf22a8d0..6de6d2c073 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -18,7 +18,7 @@ export function render(element, mountPoint) { let mount = mountPoint || document.createElement('div'); let instance = ReactDOM.render(element, mount); - if (!instance.renderWithProps) { + if (instance && !instance.renderWithProps) { instance.renderWithProps = newProps => { return render( diff --git a/test/utils/bootstrapUtilsSpec.js b/test/utils/bootstrapUtilsSpec.js new file mode 100644 index 0000000000..644b78d403 --- /dev/null +++ b/test/utils/bootstrapUtilsSpec.js @@ -0,0 +1,209 @@ +import React from 'react'; +import tbsUtils, { bsStyles, bsSizes, _curry } from '../../src/utils/bootstrapUtils'; +import { render, shouldWarn } from '../helpers'; + +describe('bootstrapUtils', ()=> { + + function validatePropType(propTypes, prop, value, match) { + let result = propTypes[prop]({ [prop]: value }, prop, 'Component'); + + if (match) { + expect(result.message).to.match(match); + } else { + expect(result).to.not.exist; + } + } + + it('should prefix with bsClass', ()=> { + expect(tbsUtils.prefix({ bsClass: 'yolo'}, 'pie')).to.equal('yolo-pie'); + }); + + it('should return bsClass when there is no suffix', ()=> { + expect(tbsUtils.prefix({ bsClass: 'yolo'})).to.equal('yolo'); + expect(tbsUtils.prefix({ bsClass: 'yolo'}, '')).to.equal('yolo'); + expect(tbsUtils.prefix({ bsClass: 'yolo'}, null)).to.equal('yolo'); + }); + + it('returns a classSet of bsClass', ()=> { + expect(tbsUtils.getClassSet({ bsClass: 'btn' })).to.eql({'btn': true }); + }); + + it('returns a classSet of bsClass and style', ()=> { + expect( + tbsUtils.getClassSet({ bsClass: 'btn', bsStyle: 'primary' }) + ) + .to.eql({'btn': true, 'btn-primary': true }); + }); + + it('returns a classSet of bsClass and size', ()=> { + expect(tbsUtils + .getClassSet({ bsClass: 'btn', bsSize: 'large' })) + .to.eql({'btn': true, 'btn-lg': true }); + + expect(tbsUtils + .getClassSet({ bsClass: 'btn', bsSize: 'lg' })) + .to.eql({'btn': true, 'btn-lg': true }); + }); + + it('returns a classSet of bsClass, style and size', ()=> { + expect(tbsUtils + .getClassSet({ bsClass: 'btn', bsSize: 'lg', bsStyle: 'primary' })) + .to.eql({'btn': true, 'btn-lg': true, 'btn-primary': true }); + }); + + describe('decorators', ()=> { + it('should apply immediately if a component is supplied', ()=> { + let spy = sinon.spy(); + let component = function noop() {}; + + _curry(spy)(true, 'hi', component); + + expect(spy).to.have.been.calledOnce; + expect(spy).to.have.been.calledWith(true, 'hi', component); + }); + + it('should curry the method as a decorator', ()=> { + let spy = sinon.spy(); + let component = function noop() {}; + let decorator = _curry(spy)(true, 'hi'); + + expect(spy).to.have.not.been.calledOnce; + + decorator(component); + + expect(spy).to.have.been.calledOnce; + expect(spy).to.have.been.calledWith(true, 'hi', component); + }); + }); + + describe('bsStyles', ()=> { + + it('should add style to allowed propTypes', ()=> { + let Component = {}; + + bsStyles(['minimal', 'boss', 'plaid'])(Component); + + expect(Component.propTypes).to.exist; + + validatePropType(Component.propTypes, 'bsStyle', 'plaid'); + + validatePropType(Component.propTypes, 'bsStyle', 'not-plaid', + /expected one of \["minimal","boss","plaid"\]/); + }); + + it('should not override other propTypes', ()=> { + let Component = { propTypes: {other() {}}}; + + bsStyles(['minimal', 'boss', 'plaid'])(Component); + + expect(Component.propTypes).to.exist; + expect(Component.propTypes.other).to.exist; + }); + + it('should set a default if provided', ()=> { + let Component = { propTypes: {other() {}}}; + + bsStyles(['minimal', 'boss', 'plaid'], 'plaid')(Component); + + expect(Component.defaultProps).to.exist; + expect(Component.defaultProps.bsStyle).to.equal('plaid'); + }); + + it('should work with es6 classes', ()=> { + @bsStyles(['minimal', 'boss', 'plaid'], 'plaid') + class Component { + render() { return ; } + } + + let instance = render(); + + expect(instance.props.bsStyle).to.equal('plaid'); + + render(); + + shouldWarn(/expected one of \["minimal","boss","plaid"\]/); + }); + + it('should work with createClass', ()=> { + let Component = bsStyles(['minimal', 'boss', 'plaid'], 'plaid')( + React.createClass({ + render() { return ; } + }) + ); + + let instance = render(); + + expect(instance.props.bsStyle).to.equal('plaid'); + + render(); + + shouldWarn(/expected one of \["minimal","boss","plaid"\]/); + }); + }); + + describe('bsSizes', ()=> { + + it('should add size to allowed propTypes', ()=> { + let Component = {}; + + bsSizes(['large', 'small'])(Component); + + expect(Component.propTypes).to.exist; + + validatePropType(Component.propTypes, 'bsSize', 'small'); + validatePropType(Component.propTypes, 'bsSize', 'sm'); + + validatePropType(Component.propTypes, 'bsSize', 'superSmall', + /expected one of \["lg","large","sm","small"\]/); + }); + + it('should not override other propTypes', ()=> { + let Component = { propTypes: {other() {}}}; + + bsSizes(['smallish', 'micro', 'planet'])(Component); + + expect(Component.propTypes).to.exist; + expect(Component.propTypes.other).to.exist; + }); + + it('should set a default if provided', ()=> { + let Component = { propTypes: {other() {}}}; + + bsSizes(['smallish', 'micro', 'planet'], 'smallish')(Component); + + expect(Component.defaultProps).to.exist; + expect(Component.defaultProps.bsSize).to.equal('smallish'); + }); + + it('should work with es6 classes', ()=> { + @bsSizes(['smallish', 'micro', 'planet'], 'smallish') + class Component { + render() { return ; } + } + + let instance = render(); + + expect(instance.props.bsSize).to.equal('smallish'); + + render(); + + shouldWarn(/expected one of \["smallish","micro","planet"\]/); + }); + + it('should work with createClass', ()=> { + let Component = bsSizes(['smallish', 'micro', 'planet'], 'smallish')( + React.createClass({ + render() { return ; } + }) + ); + + let instance = render(); + + expect(instance.props.bsSize).to.equal('smallish'); + + render(); + + shouldWarn(/expected one of \["smallish","micro","planet"\]/); + }); + }); +});