diff --git a/.ackrc b/.ackrc index ac3018e347..bd92dbf50b 100644 --- a/.ackrc +++ b/.ackrc @@ -1,3 +1,4 @@ +--ignore-dir=.coverage --ignore-dir=lib --ignore-dir=dist --ignore-dir=amd @@ -7,3 +8,4 @@ --ignore-dir=tmp-bower-repo --ignore-file=match:test_bundle.js --ignore-file=match:components.html +--ignore-file=match:.orig diff --git a/.eslintrc b/.eslintrc index 196eb5aa3e..7e00e12148 100644 --- a/.eslintrc +++ b/.eslintrc @@ -49,7 +49,7 @@ "react/no-did-mount-set-state": 2, "react/no-did-update-set-state": 2, "react/no-multi-comp": 2, - "react/prop-types": [2, { "ignore": [ "children", "className" ] }], + "react/prop-types": [2, { "ignore": [ "children", "className", "style" ] }], "react/react-in-jsx-scope": 2, "react/self-closing-comp": 2, "react/sort-comp": 0, diff --git a/.projections.json b/.projections.json new file mode 100644 index 0000000000..d1519dd643 --- /dev/null +++ b/.projections.json @@ -0,0 +1,4 @@ +{ + "src/*.js": { "alternate": "test/{}Spec.js" }, + "test/*Spec.js": { "alternate": "src/{}.js" } +} diff --git a/CHANGELOG-alpha.md b/CHANGELOG-alpha.md new file mode 100644 index 0000000000..f59c83c0a9 --- /dev/null +++ b/CHANGELOG-alpha.md @@ -0,0 +1,16 @@ +v0.25.0-alpha.1 - Mon, 10 Aug 2015 19:41:20 GMT +----------------------------------------------- + +- [b688014](../../commit/b688014) [added] custom feedback icons for Input +- [83cdaa3](../../commit/83cdaa3) [added] formControlFeedback prop to Glyphicon +- [2ecac68](../../commit/2ecac68) [fixed] Modal uses provided className again +- [47bd7f6](../../commit/47bd7f6) [fixed] disabled pagination buttons should not fire 'onSelect' +- [c60dc03](../../commit/c60dc03) [fixed] only add aria-expanded to Collapse when an ARIA role is present + + + +v0.25.0-alpha.0 - Fri, 31 Jul 2015 19:37:39 GMT +----------------------------------------------- + +- [f6d32c4](../../commit/f6d32c4) [changed] deprecate 'utils/CustomPropTypes' exporting +- [caff9a0](../../commit/caff9a0) [removed] Factory support diff --git a/docs/.eslintrc b/docs/.eslintrc index 87ac3840ba..59c304ef96 100644 --- a/docs/.eslintrc +++ b/docs/.eslintrc @@ -1,6 +1,10 @@ { + "plugins": [ + "lodash" + ], "rules": { "comma-spacing": 0, + "lodash/import": 2, "react/no-multi-comp": 0, "react/prop-types": 0 }, diff --git a/docs/assets/style.css b/docs/assets/style.css index 98330c9927..d71f5df7a4 100644 --- a/docs/assets/style.css +++ b/docs/assets/style.css @@ -36,7 +36,7 @@ body { } } -.navbar>.container .navbar-brand, .navbar>.container-fluid .navbar-brand { +.bs-docs-nav .navbar-brand { color: #00d8ff; } @@ -173,6 +173,29 @@ body { } +.bs-example .super-colors { + background: -moz-linear-gradient( top , + rgba(255, 0, 0, 1) 0%, + rgba(255, 255, 0, 1) 15%, + rgba(0, 255, 0, 1) 30%, + rgba(0, 255, 255, 1) 50%, + rgba(0, 0, 255, 1) 65%, + rgba(255, 0, 255, 1) 80%, + rgba(255, 0, 0, 1) 100%); + background: -webkit-gradient(linear, left top, left bottom, + color-stop(0%, rgba(255, 0, 0, 1)), + color-stop(15%, rgba(255, 255, 0, 1)), + color-stop(30%, rgba(0, 255, 0, 1)), + color-stop(50%, rgba(0, 255, 255, 1)), + color-stop(65%, rgba(0, 0, 255, 1)), + color-stop(80%, rgba(255, 0, 255, 1)), + color-stop(100%, rgba(255, 0, 0, 1))); +} + +/*.bs-example .custom-menu > ul > li { + padding: 0 20px; +}*/ + .anchor, .anchor:hover, .anchor:active, @@ -199,3 +222,13 @@ h4:hover .anchor-icon, h4 a:focus .anchor-icon { opacity: 0.5; } + +.prop-desc pre { + border-radius: 0; + border-width: 0; + border-left-width: 3px; +} + +.prop-desc-heading { + margin-bottom: 10px; +} diff --git a/docs/examples/.eslintrc b/docs/examples/.eslintrc index 00c22821a7..946c5f8337 100644 --- a/docs/examples/.eslintrc +++ b/docs/examples/.eslintrc @@ -19,7 +19,9 @@ "Carousel", "CarouselItem", "Col", + "Dropdown", "DropdownButton", + "DropdownMenu", "FormControls", "Glyphicon", "Grid", @@ -30,12 +32,11 @@ "ListGroupItem", "Nav", "Navbar", + "NavDropdown", "NavItem", "MenuItem", "Modal", - "ModalTrigger", "OverlayTrigger", - "OverlayMixin", "Overlay", "PageHeader", "PageItem", @@ -47,9 +48,9 @@ "ProgressBar", "Row", "SplitButton", - "TabbedArea", + "Tab", "Table", - "TabPane", + "Tabs", "Tooltip", "Well", "Thumbnail", diff --git a/docs/examples/ButtonGroupJustified.js b/docs/examples/ButtonGroupJustified.js index 989cf46ddf..1ca9b95088 100644 --- a/docs/examples/ButtonGroupJustified.js +++ b/docs/examples/ButtonGroupJustified.js @@ -2,7 +2,7 @@ const buttonGroupInstance = ( - + Dropdown link Dropdown link diff --git a/docs/examples/ButtonGroupNested.js b/docs/examples/ButtonGroupNested.js index e1a9e1eb9d..b579fdb015 100644 --- a/docs/examples/ButtonGroupNested.js +++ b/docs/examples/ButtonGroupNested.js @@ -2,7 +2,7 @@ const buttonGroupInstance = ( - + Dropdown link Dropdown link diff --git a/docs/examples/ButtonGroupVertical.js b/docs/examples/ButtonGroupVertical.js index 9453906974..49cc941227 100644 --- a/docs/examples/ButtonGroupVertical.js +++ b/docs/examples/ButtonGroupVertical.js @@ -2,17 +2,17 @@ const buttonGroupInstance = ( - + Dropdown link Dropdown link - + Dropdown link Dropdown link - + Dropdown link Dropdown link diff --git a/docs/examples/CollapsibleNav.js b/docs/examples/CollapsibleNav.js index 2d04d65fc8..6eb611b076 100644 --- a/docs/examples/CollapsibleNav.js +++ b/docs/examples/CollapsibleNav.js @@ -4,13 +4,13 @@ const navbarInstance = ( ); } }); -React.render(, mountNode); +React.render(, mountNode); diff --git a/docs/examples/NavbarBasic.js b/docs/examples/NavbarBasic.js index 756b6634b7..a3923a627f 100644 --- a/docs/examples/NavbarBasic.js +++ b/docs/examples/NavbarBasic.js @@ -3,13 +3,13 @@ const navbarInstance = ( ); diff --git a/docs/examples/NavbarBrand.js b/docs/examples/NavbarBrand.js index e210c5270f..594d7b8505 100644 --- a/docs/examples/NavbarBrand.js +++ b/docs/examples/NavbarBrand.js @@ -3,13 +3,13 @@ const navbarInstance = ( ); diff --git a/docs/examples/NavbarCollapsible.js b/docs/examples/NavbarCollapsible.js index 35b9564cf1..91c34d5e87 100644 --- a/docs/examples/NavbarCollapsible.js +++ b/docs/examples/NavbarCollapsible.js @@ -3,13 +3,13 @@ const navbarInstance = ( ); diff --git a/docs/examples/SplitButtonBasic.js b/docs/examples/SplitButtonBasic.js index 4507b39a6a..de6aee53f2 100644 --- a/docs/examples/SplitButtonBasic.js +++ b/docs/examples/SplitButtonBasic.js @@ -2,7 +2,7 @@ const BUTTONS = ['Default', 'Primary', 'Success', 'Info', 'Warning', 'Danger']; function renderDropdownButton (title, i) { return ( - + Action Another action Something else here diff --git a/docs/examples/SplitButtonDropup.js b/docs/examples/SplitButtonDropup.js index abfe5cfe0f..a5c5018bfa 100644 --- a/docs/examples/SplitButtonDropup.js +++ b/docs/examples/SplitButtonDropup.js @@ -1,7 +1,7 @@ const buttonsInstance = (
- + Action Another action Something else here @@ -11,7 +11,7 @@ const buttonsInstance = ( - + Action Another action Something else here diff --git a/docs/examples/SplitButtonRight.js b/docs/examples/SplitButtonRight.js index b9254720a5..638a7ef9f2 100644 --- a/docs/examples/SplitButtonRight.js +++ b/docs/examples/SplitButtonRight.js @@ -1,5 +1,5 @@ const buttonsInstance = ( - + Action Another action Something else here diff --git a/docs/examples/TabbedAreaControlled.js b/docs/examples/TabbedAreaControlled.js deleted file mode 100644 index 9e197e496f..0000000000 --- a/docs/examples/TabbedAreaControlled.js +++ /dev/null @@ -1,24 +0,0 @@ -const ControlledTabArea = React.createClass({ - getInitialState() { - return { - key: 1 - }; - }, - - handleSelect(key) { - alert('selected ' + key); - this.setState({key}); - }, - - render() { - return ( - - TabPane 1 content - TabPane 2 content - TabPane 3 content - - ); - } -}); - -React.render(, mountNode); diff --git a/docs/examples/TabbedAreaNoAnimation.js b/docs/examples/TabbedAreaNoAnimation.js deleted file mode 100644 index be5d2b4ab2..0000000000 --- a/docs/examples/TabbedAreaNoAnimation.js +++ /dev/null @@ -1,9 +0,0 @@ -const tabbedAreaInstance = ( - - TabPane 1 content - TabPane 2 content - TabPane 3 content - -); - -React.render(tabbedAreaInstance, mountNode); diff --git a/docs/examples/TabbedAreaUncontrolled.js b/docs/examples/TabbedAreaUncontrolled.js deleted file mode 100644 index 4b4a7dffcf..0000000000 --- a/docs/examples/TabbedAreaUncontrolled.js +++ /dev/null @@ -1,9 +0,0 @@ -const tabbedAreaInstance = ( - - TabPane 1 content - TabPane 2 content - TabPane 3 content - -); - -React.render(tabbedAreaInstance, mountNode); diff --git a/docs/examples/TabsControlled.js b/docs/examples/TabsControlled.js new file mode 100644 index 0000000000..e68414ab3f --- /dev/null +++ b/docs/examples/TabsControlled.js @@ -0,0 +1,24 @@ +const ControlledTabs = React.createClass({ + getInitialState() { + return { + key: 1 + }; + }, + + handleSelect(key) { + alert('selected ' + key); + this.setState({key}); + }, + + render() { + return ( + + Tab 1 content + Tab 2 content + Tab 3 content + + ); + } +}); + +React.render(, mountNode); diff --git a/docs/examples/TabsNoAnimation.js b/docs/examples/TabsNoAnimation.js new file mode 100644 index 0000000000..fe33845f6a --- /dev/null +++ b/docs/examples/TabsNoAnimation.js @@ -0,0 +1,9 @@ +const tabsInstance = ( + + Tab 1 content + Tab 2 content + Tab 3 content + +); + +React.render(tabsInstance, mountNode); diff --git a/docs/examples/TabsUncontrolled.js b/docs/examples/TabsUncontrolled.js new file mode 100644 index 0000000000..e922101ecd --- /dev/null +++ b/docs/examples/TabsUncontrolled.js @@ -0,0 +1,9 @@ +const tabsInstance = ( + + Tab 1 content + Tab 2 content + Tab 3 content + +); + +React.render(tabsInstance, mountNode); diff --git a/docs/generate-metadata.js b/docs/generate-metadata.js index 2a85353451..bddf6908a1 100644 --- a/docs/generate-metadata.js +++ b/docs/generate-metadata.js @@ -72,7 +72,7 @@ function applyPropDoclets(props, propName){ } -export default function generate(destination, options = { mixins: true }){ +export default function generate(destination, options = { mixins: true, inferComponent: true }){ return globp(__dirname + '/../src/**/*.js') //eslint-disable-line no-path-concat .then( files => { diff --git a/docs/src/ComponentsPage.js b/docs/src/ComponentsPage.js index d2f241e165..141ab111dc 100644 --- a/docs/src/ComponentsPage.js +++ b/docs/src/ComponentsPage.js @@ -1,6 +1,8 @@ /* eslint react/no-did-mount-set-state: 0 */ import React from 'react'; +import getOffset from 'dom-helpers/query/offset'; +import css from 'dom-helpers/style'; import Affix from '../../src/Affix'; import Nav from '../../src/Nav'; @@ -33,9 +35,8 @@ const ComponentsPage = React.createClass({ componentDidMount() { let elem = React.findDOMNode(this.refs.sideNav); - let domUtils = Affix.domUtils; - let sideNavOffsetTop = domUtils.getOffset(elem).top; - let sideNavMarginTop = parseInt(domUtils.getComputedStyles(elem.firstChild).marginTop, 10); + let sideNavOffsetTop = getOffset(elem).top; + let sideNavMarginTop = parseInt(css(elem.firstChild, 'marginTop'), 10); let topNavHeight = React.findDOMNode(this.refs.topNav).offsetHeight; this.setState({ @@ -180,13 +181,42 @@ const ComponentsPage = React.createClass({

Trigger dropdown menus that align to the right of the button using the pullRight prop.

+

Dropdown Customization

+

+ If the default handling of the dropdown menu and toggle components aren't to your liking, you can + customize them, by using the more basic Dropdown Component to explicitly specify + the Toggle and Menu components +

+
+

Additional Import Options

+

+ As a convenience Toggle and Menu components available as static properties + on the Dropdown component. However, you can also import them directly, from + the /lib directory like: {"require('react-bootstrap/lib/DropdownToggle')"}. +

+
+ + +

Custom Dropdown Components

+ +

+ For those that want to customize everything, you can forgo the included Toggle and Menu components, + and create your own. In order to tell the Dropdown component what role your custom components play + add a special prop bsRole to your menu or toggle components. The Dropdown expects + at least one component with bsRole='toggle' and exactly one with bsRole='menu'. +

+ +

Props

-

DropdownButton

+

DropdownButton

SplitButton

+ +

Dropdown

+
{/* Menu Item */} @@ -513,29 +543,33 @@ const ComponentsPage = React.createClass({ {/* Tabbed Areas */}
-

Togglable tabs TabbedArea, TabPane

+

Togglable tabs Tabs, Tab

Add quick, dynamic tab functionality to transition through panes of local content.

Uncontrolled

Allow the component to control its own state.

- +

Controlled

Pass down the active state on render via props.

- +

No animation

Set the animation prop to false

- + + +

Left tabs

+

Set position to 'left'. Optionally, tabWidth can be passed the number of columns for the tabs.

+

Props

-

TabbedArea

- +

Tabs

+ -

TabPane

- +

Tab

+
{/* Pager */} diff --git a/docs/src/PropTable.js b/docs/src/PropTable.js index 250a70a8f6..46fb2314c8 100644 --- a/docs/src/PropTable.js +++ b/docs/src/PropTable.js @@ -1,27 +1,29 @@ import merge from 'lodash/object/merge'; import React from 'react'; - +import Glyphicon from '../../src/Glyphicon'; import Label from '../../src/Label'; import Table from '../../src/Table'; let cleanDocletValue = str => str.trim().replace(/^\{/, '').replace(/\}$/, ''); +let capitalize = str => str[0].toUpperCase() + str.substr(1); -function getPropsData(componentData, metadata){ - +function getPropsData(component, metadata){ + let componentData = metadata[component] || {}; let props = componentData.props || {}; if (componentData.composes) { - componentData.composes.forEach( other => { - props = merge({}, getPropsData(metadata[other] || {}, metadata), props); - + componentData.composes.forEach(other => { + if (other !== component) { + props = merge({}, getPropsData(other, metadata), props); + } }); } if (componentData.mixins) { componentData.mixins.forEach( other => { - if ( componentData.composes.indexOf(other) === -1) { - props = merge({}, getPropsData(metadata[other] || {}, metadata), props); + if (other !== component && componentData.composes.indexOf(other) === -1) { + props = merge({}, getPropsData(other, metadata), props); } }); } @@ -29,6 +31,8 @@ function getPropsData(componentData, metadata){ return props; } + + const PropTable = React.createClass({ contextTypes: { @@ -36,9 +40,7 @@ const PropTable = React.createClass({ }, componentWillMount(){ - let componentData = this.context.metadata[this.props.component] || {}; - - this.propsData = getPropsData(componentData, this.context.metadata); + this.propsData = getPropsData(this.props.component, this.context.metadata); }, render(){ @@ -85,9 +87,12 @@ const PropTable = React.createClass({ { propData.doclets.deprecated - &&
{'Deprecated: ' + propData.doclets.deprecated + ' '}
+ &&
+ {'Deprecated: ' + propData.doclets.deprecated + ' '} +
} -
+ { this.renderControllableNote(propData, propName) } +
); @@ -104,6 +109,37 @@ const PropTable = React.createClass({ ); }, + renderControllableNote(prop, propName){ + let controllable = prop.doclets.controllable; + let isHandler = this.getDisplayTypeName(prop.type.name) === 'function'; + + if (!controllable) { + return false; + } + + let text = isHandler ? ( + + controls {controllable} + + ) : ( + + controlled by: {controllable}, + initial prop: {'default' + capitalize(propName)} + + ); + + return ( +
+ + + +  { text } + + +
+ ); + }, + getType(prop) { let type = prop.type || {}; let name = this.getDisplayTypeName(type.name); diff --git a/docs/src/ReactPlayground.js b/docs/src/ReactPlayground.js index 6020303187..f351323aa5 100644 --- a/docs/src/ReactPlayground.js +++ b/docs/src/ReactPlayground.js @@ -18,7 +18,9 @@ const Col = require('../../src/Col'); const Collapse = require('../../src/Collapse'); const CollapsibleMixin = require('../../src/CollapsibleMixin'); const CollapsibleNav = require('../../src/CollapsibleNav'); +const Dropdown = require('../../src/Dropdown').default; const DropdownButton = require('../../src/DropdownButton'); +const DropdownMenu = require('../../src/DropdownMenu'); const Fade = require('../../src/Fade'); const FormControls = require('../../src/FormControls'); const Glyphicon = require('../../src/Glyphicon'); @@ -33,6 +35,7 @@ const Modal = require('../../src/Modal'); const Nav = require('../../src/Nav'); const Navbar = require('../../src/Navbar'); const NavItem = require('../../src/NavItem'); +const NavDropdown = require('../../src/NavDropdown'); const Overlay = require('../../src/Overlay'); const OverlayTrigger = require('../../src/OverlayTrigger'); const PageHeader = require('../../src/PageHeader'); @@ -42,13 +45,12 @@ const Pagination = require('../../src/Pagination'); const Panel = require('../../src/Panel'); const PanelGroup = require('../../src/PanelGroup'); const Popover = require('../../src/Popover'); -const Portal = require('../../src/Portal'); const ProgressBar = require('../../src/ProgressBar'); const Row = require('../../src/Row'); const SplitButton = require('../../src/SplitButton'); -const TabbedArea = require('../../src/TabbedArea'); +const Tab = require('../../src/Tab'); const Table = require('../../src/Table'); -const TabPane = require('../../src/TabPane'); +const Tabs = require('../../src/Tabs'); const Thumbnail = require('../../src/Thumbnail'); const Tooltip = require('../../src/Tooltip'); const Well = require('../../src/Well'); diff --git a/docs/src/Samples.js b/docs/src/Samples.js index fc58d7ed58..3722bc3dc4 100644 --- a/docs/src/Samples.js +++ b/docs/src/Samples.js @@ -19,6 +19,8 @@ export default { ButtonGroupJustified: require('fs').readFileSync(__dirname + '/../examples/ButtonGroupJustified.js', 'utf8'), ButtonGroupBlock: require('fs').readFileSync(__dirname + '/../examples/ButtonGroupBlock.js', 'utf8'), DropdownButtonBasic: require('fs').readFileSync(__dirname + '/../examples/DropdownButtonBasic.js', 'utf8'), + DropdownButtonCustom: require('fs').readFileSync(__dirname + '/../examples/DropdownButtonCustom.js', 'utf8'), + DropdownButtonCustomMenu: require('fs').readFileSync(__dirname + '/../examples/DropdownButtonCustomMenu.js', 'utf8'), SplitButtonBasic: require('fs').readFileSync(__dirname + '/../examples/SplitButtonBasic.js', 'utf8'), DropdownButtonSizes: require('fs').readFileSync(__dirname + '/../examples/DropdownButtonSizes.js', 'utf8'), DropdownButtonNoCaret: require('fs').readFileSync(__dirname + '/../examples/DropdownButtonNoCaret.js', 'utf8'), @@ -61,9 +63,10 @@ export default { NavbarBrand: require('fs').readFileSync(__dirname + '/../examples/NavbarBrand.js', 'utf8'), NavbarCollapsible: require('fs').readFileSync(__dirname + '/../examples/NavbarCollapsible.js', 'utf8'), CollapsibleNav: require('fs').readFileSync(__dirname + '/../examples/CollapsibleNav.js', 'utf8'), - TabbedAreaUncontrolled: require('fs').readFileSync(__dirname + '/../examples/TabbedAreaUncontrolled.js', 'utf8'), - TabbedAreaControlled: require('fs').readFileSync(__dirname + '/../examples/TabbedAreaControlled.js', 'utf8'), - TabbedAreaNoAnimation: require('fs').readFileSync(__dirname + '/../examples/TabbedAreaNoAnimation.js', 'utf8'), + TabsUncontrolled: require('fs').readFileSync(__dirname + '/../examples/TabsUncontrolled.js', 'utf8'), + TabsControlled: require('fs').readFileSync(__dirname + '/../examples/TabsControlled.js', 'utf8'), + TabsNoAnimation: require('fs').readFileSync(__dirname + '/../examples/TabsNoAnimation.js', 'utf8'), + LeftTabs: require('fs').readFileSync(__dirname + '/../examples/LeftTabs.js', 'utf8'), PagerDefault: require('fs').readFileSync(__dirname + '/../examples/PagerDefault.js', 'utf8'), PagerAligned: require('fs').readFileSync(__dirname + '/../examples/PagerAligned.js', 'utf8'), PagerDisabled: require('fs').readFileSync(__dirname + '/../examples/PagerDisabled.js', 'utf8'), diff --git a/karma.conf.js b/karma.conf.js index 2e82eeceb4..da4f415448 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -39,7 +39,7 @@ module.exports = function (config) { webpack: webpackConfig, webpackMiddleware: { - noInfo: isCI + noInfo: true }, reporters: reporters, diff --git a/package.json b/package.json index b343586e2e..bb4364a9c9 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,8 @@ "homepage": "http://react-bootstrap.github.io/", "scripts": { "build": "babel-node tools/build-cli.js", - "lib-build": "babel-node tools/build-cli.js --lib-only", - "test-watch": "npm run lib-build && karma start", - "test-coverage": "npm run lib-build && COVERAGE=true karma start --single-run", + "test-watch": "karma start", + "test-coverage": "COVERAGE=true karma start --single-run", "test": "npm run lint && npm run build && npm run tests-set", "tests-set": "karma start --single-run && _mocha --compilers js:babel-core/register test/server/*Spec.js", "lint": "eslint ./", @@ -94,7 +93,7 @@ "phantomjs": "^1.9.17", "portfinder": "^0.4.0", "react": "^0.13.3", - "react-component-metadata": "^1.2.1", + "react-component-metadata": "^1.3.0", "react-hot-loader": "^1.2.8", "react-router": "^0.13.3", "rimraf": "^2.4.2", @@ -109,7 +108,11 @@ }, "dependencies": { "babel-runtime": "^5.8.19", + "classnames": "^2.1.3", + "dom-helpers": "^2.2.4", + "keycode": "^2.0.0", "lodash": "^3.10.0", - "classnames": "^2.1.3" + "react-overlays": "^0.4.4", + "uncontrollable": "^3.0.0" } } diff --git a/src/Affix.js b/src/Affix.js index 81b7357a02..13dc893690 100644 --- a/src/Affix.js +++ b/src/Affix.js @@ -1,12 +1,8 @@ import React from 'react'; import classNames from 'classnames'; import AffixMixin from './AffixMixin'; -import domUtils from './utils/domUtils'; const Affix = React.createClass({ - statics: { - domUtils - }, mixins: [AffixMixin], diff --git a/src/Collapse.js b/src/Collapse.js index ab8e4b652f..c4a921679c 100644 --- a/src/Collapse.js +++ b/src/Collapse.js @@ -1,6 +1,8 @@ import React from 'react'; -import Transition from './Transition'; +import Transition from 'react-overlays/lib/Transition'; import domUtils from './utils/domUtils'; +import CustomPropTypes from './utils/CustomPropTypes'; +import deprecationWarning from './utils/deprecationWarning'; import createChainedFunction from './utils/createChainedFunction'; let capitalize = str => str[0].toUpperCase() + str.substr(1); @@ -16,12 +18,11 @@ const MARGINS = { function getDimensionValue(dimension, elem){ let value = elem[`offset${capitalize(dimension)}`]; - let computedStyles = domUtils.getComputedStyles(elem); let margins = MARGINS[dimension]; return (value + - parseInt(computedStyles[margins[0]], 10) + - parseInt(computedStyles[margins[1]], 10) + parseInt(domUtils.css(elem, margins[0]), 10) + + parseInt(domUtils.css(elem, margins[1]), 10) ); } @@ -138,7 +139,21 @@ Collapse.propTypes = { * finishing callbacks are fired even if the original browser transition end * events are canceled */ - duration: React.PropTypes.number, + timeout: React.PropTypes.number, + + /** + * duration + * @private + */ + duration: CustomPropTypes.all([ + React.PropTypes.number, + (props)=> { + if (props.duration != null){ + deprecationWarning('Collapse `duration`', 'the `timeout` prop'); + } + return null; + } + ]), /** * Callback fired before the component expands @@ -194,7 +209,7 @@ Collapse.propTypes = { Collapse.defaultProps = { in: false, - duration: 300, + timeout: 300, unmountOnExit: false, transitionAppear: false, diff --git a/src/Dropdown.js b/src/Dropdown.js new file mode 100644 index 0000000000..3800bec484 --- /dev/null +++ b/src/Dropdown.js @@ -0,0 +1,298 @@ +import React, { cloneElement } from 'react'; +import keycode from 'keycode'; +import classNames from 'classnames'; +import uncontrollable from 'uncontrollable'; +import ButtonGroup from './ButtonGroup'; +import DropdownToggle from './DropdownToggle'; +import DropdownMenu from './DropdownMenu'; +import CustomPropTypes from './utils/CustomPropTypes'; +import createChainedFunction from './utils/createChainedFunction'; +import find from 'lodash/collection/find'; +import omit from 'lodash/object/omit'; + +const TOGGLE_REF = 'toggle-btn'; + +export const TOGGLE_ROLE = DropdownToggle.defaultProps.bsRole; +export const MENU_ROLE = DropdownMenu.defaultProps.bsRole; + +class Dropdown extends React.Component { + + constructor(props) { + super(props); + + this.Toggle = DropdownToggle; + + this.toggleOpen = this.toggleOpen.bind(this); + this.handleClick = this.handleClick.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + this.handleClose = this.handleClose.bind(this); + this.extractChildren = this.extractChildren.bind(this); + + this.refineMenu = this.refineMenu.bind(this); + this.refineToggle = this.refineToggle.bind(this); + + this.childExtractors = [{ + key: 'toggle', + matches: child => child.props.bsRole === TOGGLE_ROLE, + refine: this.refineToggle + }, { + key: 'menu', + exclusive: true, + matches: child => child.props.bsRole === MENU_ROLE, + refine: this.refineMenu + }]; + + this.state = {}; + } + + componentDidMount() { + let menu = this.refs.menu; + if (this.props.open && menu.focusNext) { + menu.focusNext(); + } + } + + componentDidUpdate(prevProps, prevState) { + let menu = this.refs.menu; + if (this.props.open && !prevProps.open && menu.focusNext) { + menu.focusNext(); + } + } + + render() { + let children = this.extractChildren(); + let Component = this.props.componentClass; + + let props = omit(this.props, ['id']); + + const rootClasses = { + open: this.props.open, + dropdown: !this.props.dropup, + dropup: this.props.dropup + }; + + return ( + + { children } + + ); + } + + toggleOpen() { + let open = !this.props.open; + + if (this.props.onToggle){ + this.props.onToggle(open); + } + } + + handleClick(event) { + if (this.props.disabled) { + return; + } + + this.toggleOpen(); + } + + handleKeyDown(event) { + let focusNext = () => { + if (this.refs.menu.focusNext) { + this.refs.menu.focusNext(); + } + }; + + switch(event.keyCode) { + case keycode.codes.down: + if (!this.props.open) { + this.toggleOpen(); + } + else { + focusNext(); + } + event.preventDefault(); + break; + case keycode.codes.esc: + case keycode.codes.tab: + if (this.props.open) { + this.handleClose(event); + } + break; + } + } + + handleClose(event) { + if (!this.props.open) { + return; + } + + // we need to let the current event finish before closing the menu. + // otherwise the menu may close, shifting focus to document.body, before focus has moved + // to the next focusable input + if (event && event.keyCode === keycode.codes.tab){ + setTimeout(this.toggleOpen); + } + else { + this.toggleOpen(); + } + + if (event && event.type === 'keydown' && event.keyCode === keycode.codes.esc) { + let toggle = React.findDOMNode(this.refs[TOGGLE_REF]); + event.preventDefault(); + event.stopPropagation(); + toggle.focus(); + } + } + + extractChildren() { + let open = !!this.props.open; + let seen = {}; + + return React.Children.map(this.props.children, child => { + let extractor = find(this.childExtractors, x => x.matches(child)); + + if (extractor) { + if (seen[extractor.key]){ + return false; + } + + seen[extractor.key] = extractor.exclusive; + child = extractor.refine(child, open); + } + + return child; + }); + } + + refineMenu(menu, open) { + const menuProps = { + ref: 'menu', + open, + labelledBy: this.props.id, + pullRight: this.props.pullRight + }; + + menuProps.onClose = createChainedFunction( + menu.props.onClose, + this.props.onClose, + this.handleClose + ); + + menuProps.onSelect = createChainedFunction( + menu.props.onSelect, + this.props.onSelect, + this.handleClose + ); + + return cloneElement(menu, menuProps, menu.props.children); + } + + refineToggle(toggle, open) { + let toggleProps = { + open, + id: this.props.id, + ref: TOGGLE_REF + }; + + toggleProps.onClick = createChainedFunction( + toggle.props.onClick, + this.handleClick + ); + + toggleProps.onKeyDown = createChainedFunction( + toggle.props.onKeyDown, + this.handleKeyDown + ); + + return cloneElement(toggle, toggleProps, toggle.props.children); + } +} + +Dropdown.Toggle = DropdownToggle; + +Dropdown.TOGGLE_REF = TOGGLE_REF; + +Dropdown.defaultProps = { + componentClass: ButtonGroup +}; + +Dropdown.propTypes = { + /** + * The menu will open above the dropdown button, instead of below it. + */ + dropup: React.PropTypes.bool, + + /** + * An html id attribute, necessary for assistive technologies, such as screen readers. + * @type {string|number} + * @required + */ + id: CustomPropTypes.isRequiredForA11y( + React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.number + ]) + ), + + componentClass: CustomPropTypes.elementType, + + /** + * The children of a Dropdown may be a `` or a ``. + * @type {node} + */ + children: CustomPropTypes.all([ + CustomPropTypes.requiredRoles(TOGGLE_ROLE, MENU_ROLE), + CustomPropTypes.exclusiveRoles(MENU_ROLE) + ]), + + /** + * Whether or not component is disabled. + */ + disabled: React.PropTypes.bool, + + /** + * Align the menu to the right side of the Dropdown toggle + */ + pullRight: React.PropTypes.bool, + + /** + * Whether or not the Dropdown is visible. + * + * @controllable onToggle + */ + open: React.PropTypes.bool, + + /** + * A callback fired when the Dropdown closes. + */ + onClose: React.PropTypes.func, + + /** + * A callback fired when the Dropdown wishes to change visibility. Called with the requested + * `open` value. + * + * ```js + * function(Boolean isOpen){} + * ``` + * @controllable open + */ + onToggle: React.PropTypes.func, + + /** + * A callback fired when a menu item is selected. + * + * ```js + * function(Object event, Any eventKey) + * ``` + */ + onSelect: React.PropTypes.func +}; + +Dropdown = uncontrollable(Dropdown, { open: 'onToggle' }); + +Dropdown.Toggle = DropdownToggle; +Dropdown.Menu = DropdownMenu; + +export default Dropdown; diff --git a/src/DropdownButton.js b/src/DropdownButton.js index b25ed6697c..0bd169e2c7 100644 --- a/src/DropdownButton.js +++ b/src/DropdownButton.js @@ -1,135 +1,69 @@ -/* eslint react/prop-types: [2, {ignore: "bsSize"}] */ -/* BootstrapMixin contains `bsSize` type validation */ - -import React, { cloneElement } from 'react'; -import classNames from 'classnames'; - -import createChainedFunction from './utils/createChainedFunction'; +import React from 'react'; import BootstrapMixin from './BootstrapMixin'; -import DropdownStateMixin from './DropdownStateMixin'; -import Button from './Button'; -import ButtonGroup from './ButtonGroup'; -import DropdownMenu from './DropdownMenu'; -import ValidComponentChildren from './utils/ValidComponentChildren'; - -const DropdownButton = React.createClass({ - mixins: [BootstrapMixin, DropdownStateMixin], +import Dropdown from './Dropdown'; +import NavDropdown from './NavDropdown'; +import CustomPropTypes from './utils/CustomPropTypes'; +import deprecationWarning from './utils/deprecationWarning'; +import omit from 'lodash/object/omit'; - propTypes: { - pullRight: React.PropTypes.bool, - dropup: React.PropTypes.bool, - title: React.PropTypes.node, - href: React.PropTypes.string, - id: React.PropTypes.string, - onClick: React.PropTypes.func, - onSelect: React.PropTypes.func, - navItem: React.PropTypes.bool, - noCaret: React.PropTypes.bool, - buttonClassName: React.PropTypes.string, - className: React.PropTypes.string, - children: React.PropTypes.node - }, +class DropdownButton extends React.Component { - getDefaultProps() { - return { - pullRight: false, - dropup: false, - navItem: false, - noCaret: false - }; - }, + constructor(props) { + super(props); + } render() { - let renderMethod = this.props.navItem ? - 'renderNavItem' : 'renderButtonGroup'; - - let caret = this.props.noCaret ? - null : (); + let { title, navItem, ...props } = this.props; - return this[renderMethod]([ - , - - {ValidComponentChildren.map(this.props.children, this.renderMenuItem)} - - ]); - }, + let toggleProps = omit(props, Dropdown.ControlledComponent.propTypes); - renderButtonGroup(children) { - let groupClasses = { - 'open': this.state.open, - 'dropup': this.props.dropup - }; - - return ( - - {children} - - ); - }, - - renderNavItem(children) { - let classes = { - 'dropdown': true, - 'open': this.state.open, - 'dropup': this.props.dropup - }; + if (navItem){ + return ; + } return ( -
  • - {children} -
  • + + + {title} + + + {this.props.children} + + ); - }, - - renderMenuItem(child, index) { - // Only handle the option selection if an onSelect prop has been set on the - // component or it's child, this allows a user not to pass an onSelect - // handler and have the browser preform the default action. - let handleOptionSelect = this.props.onSelect || child.props.onSelect ? - this.handleOptionSelect : null; - - return cloneElement( - child, - { - // Capture onSelect events - onSelect: createChainedFunction(child.props.onSelect, handleOptionSelect), - key: child.key ? child.key : index + } +} + +DropdownButton.propTypes = { + /** + * When used with the `title` prop, the noCaret option will not render a caret icon, in the toggle element. + */ + noCaret: React.PropTypes.bool, + + /** + * Specify whether this Dropdown is part of a Nav component + * + * @type {bool} + * @deprecated Use the `NavDropdown` instead. + */ + navItem: CustomPropTypes.all([ + React.PropTypes.bool, + function(props, propName, componentName) { + if (props.navItem) { + deprecationWarning('navItem', 'NavDropdown component', 'https://github.com/react-bootstrap/react-bootstrap/issues/526'); } - ); - }, - - handleDropdownClick(e) { - e.preventDefault(); - - this.setDropdownState(!this.state.open); - }, - - handleOptionSelect(key) { - if (this.props.onSelect) { - this.props.onSelect(key); } - - this.setDropdownState(false); - } -}); + ]), + title: React.PropTypes.node.isRequired, + ...Dropdown.propTypes, + ...BootstrapMixin.propTypes +}; + +DropdownButton.defaultProps = { + pullRight: false, + dropup: false, + navItem: false, + noCaret: false +}; export default DropdownButton; diff --git a/src/DropdownMenu.js b/src/DropdownMenu.js index ce593ddc93..770684e26b 100644 --- a/src/DropdownMenu.js +++ b/src/DropdownMenu.js @@ -1,49 +1,135 @@ -import React, { cloneElement } from 'react'; +import React from 'react'; +import keycode from 'keycode'; import classNames from 'classnames'; +import RootCloseWrapper from 'react-overlays/lib/RootCloseWrapper'; import createChainedFunction from './utils/createChainedFunction'; -import ValidComponentChildren from './utils/ValidComponentChildren'; -const DropdownMenu = React.createClass({ - propTypes: { - pullRight: React.PropTypes.bool, - onSelect: React.PropTypes.func - }, +class DropdownMenu extends React.Component { + constructor(props) { + super(props); - getDefaultProps() { - return { - pullRight: false - }; - }, + this.focusNext = this.focusNext.bind(this); + this.focusPrevious = this.focusPrevious.bind(this); + this.getFocusableMenuItems = this.getFocusableMenuItems.bind(this); + this.getItemsAndActiveIndex = this.getItemsAndActiveIndex.bind(this); + + this.handleKeyDown = this.handleKeyDown.bind(this); + } + + handleKeyDown(event) { + + switch(event.keyCode) { + case keycode.codes.down: + this.focusNext(); + event.preventDefault(); + break; + case keycode.codes.up: + this.focusPrevious(); + event.preventDefault(); + break; + case keycode.codes.esc: + case keycode.codes.tab: + this.props.onClose(event); + break; + } + } + + focusNext() { + let { items, activeItemIndex } = this.getItemsAndActiveIndex(); + + if (activeItemIndex === items.length - 1) { + items[0].focus(); + return; + } + + items[activeItemIndex + 1].focus(); + } + + focusPrevious() { + let { items, activeItemIndex } = this.getItemsAndActiveIndex(); + + if (activeItemIndex === 0) { + items[items.length - 1].focus(); + return; + } + + items[activeItemIndex - 1].focus(); + } + + getItemsAndActiveIndex() { + let items = this.getFocusableMenuItems(); + let activeElement = document.activeElement; + let activeItemIndex = items.indexOf(activeElement); + + return {items, activeItemIndex}; + } + + getFocusableMenuItems() { + let menuNode = React.findDOMNode(this); + + if (menuNode === undefined) { + return []; + } + + return [].slice.call(menuNode.querySelectorAll('[tabIndex="-1"]'), 0); + } render() { - let classes = { - 'dropdown-menu': true, - 'dropdown-menu-right': this.props.pullRight - }; - - return ( -
      - {ValidComponentChildren.map(this.props.children, this.renderMenuItem)} -
    - ); - }, - - renderMenuItem(child, index) { - return cloneElement( - child, - { - // Capture onSelect events - onSelect: createChainedFunction(child.props.onSelect, this.props.onSelect), - - // Force special props to be transferred - key: child.key ? child.key : index - } + const items = React.Children.map(this.props.children, child => { + let { + children, + onKeyDown, + onSelect + } = child.props || {}; + + return React.cloneElement(child, { + onKeyDown: createChainedFunction(onKeyDown, this.handleKeyDown), + onSelect: createChainedFunction(onSelect, this.props.onSelect) + }, children); + }); + + const classes = { + 'dropdown-menu': true, + 'dropdown-menu-right': this.props.pullRight + }; + + let list = ( +
      + {items} +
    ); + + if (this.props.open) { + list = ( + + {list} + + ); + } + + return list; } -}); +} + +DropdownMenu.defaultProps = { + bsRole: 'menu', + pullRight: false +}; + +DropdownMenu.propTypes = { + open: React.PropTypes.bool, + pullRight: React.PropTypes.bool, + onClose: React.PropTypes.func, + labelledBy: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.number + ]), + onSelect: React.PropTypes.func +}; export default DropdownMenu; diff --git a/src/DropdownStateMixin.js b/src/DropdownStateMixin.js deleted file mode 100644 index 368c743c93..0000000000 --- a/src/DropdownStateMixin.js +++ /dev/null @@ -1,85 +0,0 @@ -import React from 'react'; -import domUtils from './utils/domUtils'; -import EventListener from './utils/EventListener'; - -/** - * Checks whether a node is within - * a root nodes tree - * - * @param {DOMElement} node - * @param {DOMElement} root - * @returns {boolean} - */ -function isNodeInRoot(node, root) { - while (node) { - if (node === root) { - return true; - } - node = node.parentNode; - } - - return false; -} - -const DropdownStateMixin = { - getInitialState() { - return { - open: false - }; - }, - - setDropdownState(newState, onStateChangeComplete) { - if (newState) { - this.bindRootCloseHandlers(); - } else { - this.unbindRootCloseHandlers(); - } - - this.setState({ - open: newState - }, onStateChangeComplete); - }, - - handleDocumentKeyUp(e) { - if (e.keyCode === 27) { - this.setDropdownState(false); - } - }, - - handleDocumentClick(e) { - // If the click originated from within this component - // don't do anything. - // e.srcElement is required for IE8 as e.target is undefined - let target = e.target || e.srcElement; - if (isNodeInRoot(target, React.findDOMNode(this))) { - return; - } - - this.setDropdownState(false); - }, - - bindRootCloseHandlers() { - let doc = domUtils.ownerDocument(this); - - this._onDocumentClickListener = - EventListener.listen(doc, 'click', this.handleDocumentClick); - this._onDocumentKeyupListener = - EventListener.listen(doc, 'keyup', this.handleDocumentKeyUp); - }, - - unbindRootCloseHandlers() { - if (this._onDocumentClickListener) { - this._onDocumentClickListener.remove(); - } - - if (this._onDocumentKeyupListener) { - this._onDocumentKeyupListener.remove(); - } - }, - - componentWillUnmount() { - this.unbindRootCloseHandlers(); - } -}; - -export default DropdownStateMixin; diff --git a/src/DropdownToggle.js b/src/DropdownToggle.js new file mode 100644 index 0000000000..5d3b3145b7 --- /dev/null +++ b/src/DropdownToggle.js @@ -0,0 +1,54 @@ +import React from 'react'; +import classNames from 'classnames'; +import Button from './Button'; +import CustomPropTypes from './utils/CustomPropTypes'; +import SafeAnchor from './SafeAnchor'; + +const CARET = ; + +export default class DropdownToggle extends React.Component { + render() { + const caret = this.props.noCaret ? null : CARET; + + const classes = { + 'dropdown-toggle': true + }; + + const Component = this.props.useAnchor ? SafeAnchor : Button; + + return ( + + {this.props.title || this.props.children}{caret} + + ); + } +} + +const titleAndChildrenValidation = CustomPropTypes.singlePropFrom([ + 'title', + 'children' +]); + +DropdownToggle.defaultProps = { + open: false, + useAnchor: false, + bsRole: 'toggle' +}; + +DropdownToggle.propTypes = { + bsRole: React.PropTypes.string, + children: titleAndChildrenValidation, + noCaret: React.PropTypes.bool, + open: React.PropTypes.bool, + title: titleAndChildrenValidation, + useAnchor: React.PropTypes.bool +}; + +DropdownToggle.isToggle = true; +DropdownToggle.titleProp = 'title'; +DropdownToggle.onClickProp = 'onClick'; diff --git a/src/Fade.js b/src/Fade.js index ce5a9a3370..62ce1b6798 100644 --- a/src/Fade.js +++ b/src/Fade.js @@ -1,11 +1,16 @@ import React from 'react'; -import Transition from './Transition'; +import Transition from 'react-overlays/lib/Transition'; +import CustomPropTypes from './utils/CustomPropTypes'; +import deprecationWarning from './utils/deprecationWarning'; class Fade extends React.Component { render() { + let timeout = this.props.timeout || this.props.duration; + return ( { + if (props.duration != null){ + deprecationWarning('Fade `duration`', 'the `timeout` prop'); + } + return null; + } + ]), /** * Callback fired before the component fades in @@ -71,7 +89,7 @@ Fade.propTypes = { Fade.defaultProps = { in: false, - duration: 300, + timeout: 300, unmountOnExit: false, transitionAppear: false }; diff --git a/src/MenuItem.js b/src/MenuItem.js index 49f3dfb7db..196fc36572 100644 --- a/src/MenuItem.js +++ b/src/MenuItem.js @@ -1,70 +1,88 @@ import React from 'react'; -import classNames from 'classnames'; +import classnames from 'classnames'; +import CustomPropTypes from './utils/CustomPropTypes'; import SafeAnchor from './SafeAnchor'; -const MenuItem = React.createClass({ - propTypes: { - header: React.PropTypes.bool, - divider: React.PropTypes.bool, - href: React.PropTypes.string, - title: React.PropTypes.string, - target: React.PropTypes.string, - onSelect: React.PropTypes.func, - eventKey: React.PropTypes.any, - active: React.PropTypes.bool, - disabled: React.PropTypes.bool - }, +export default class MenuItem extends React.Component { + constructor(props) { + super(props); - getDefaultProps() { - return { - active: false, - divider: false, - disabled: false, - header: false - }; - }, + this.handleClick = this.handleClick.bind(this); + } + + handleClick(event) { + if (!this.props.href || this.props.disabled) { + event.preventDefault(); + } - handleClick(e) { if (this.props.disabled) { - e.preventDefault(); return; } + if (this.props.onSelect) { - e.preventDefault(); - this.props.onSelect(this.props.eventKey, this.props.href, this.props.target); + this.props.onSelect(event, this.props.eventKey); } - }, - - renderAnchor() { - return ( - - {this.props.children} - - ); - }, + } render() { - let classes = { - 'dropdown-header': this.props.header, - 'divider': this.props.divider, - 'active': this.props.active, - 'disabled': this.props.disabled - }; + if (this.props.divider) { + return
  • ; + } - let children = null; if (this.props.header) { - children = this.props.children; - } else if (!this.props.divider) { - children = this.renderAnchor(); + return ( +
  • {this.props.children}
  • + ); } + const classes = { + disabled: this.props.disabled + }; + return ( -
  • - {children} +
  • + + {this.props.children} +
  • ); } -}); +} + +MenuItem.propTypes = { + disabled: React.PropTypes.bool, + divider: CustomPropTypes.all([ + React.PropTypes.bool, + function(props, propName, componentName) { + if (props.divider && props.children) { + return new Error('Children will not be rendered for dividers'); + } + } + ]), + eventKey: React.PropTypes.oneOfType([ + React.PropTypes.number, + React.PropTypes.string + ]), + header: React.PropTypes.bool, + href: React.PropTypes.string, + target: React.PropTypes.string, + title: React.PropTypes.string, + onKeyDown: React.PropTypes.func, + onSelect: React.PropTypes.func +}; -export default MenuItem; +MenuItem.defaultProps = { + divider: false, + disabled: false, + header: false +}; diff --git a/src/Modal.js b/src/Modal.js index 035f73f359..6b8f18210e 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -2,11 +2,12 @@ import React, { cloneElement } from 'react'; import classNames from 'classnames'; import domUtils from './utils/domUtils'; +import getScrollbarSize from 'dom-helpers/util/scrollbarSize'; import EventListener from './utils/EventListener'; import createChainedFunction from './utils/createChainedFunction'; import CustomPropTypes from './utils/CustomPropTypes'; -import Portal from './Portal'; +import Portal from 'react-overlays/lib/Portal'; import Fade from './Fade'; import ModalDialog from './ModalDialog'; import Body from './ModalBody'; @@ -67,28 +68,6 @@ function onFocus(context, handler) { return currentFocusListener; } -let scrollbarSize; - -function getScrollbarSize() { - if (scrollbarSize !== undefined) { - return scrollbarSize; - } - - let scrollDiv = document.createElement('div'); - - scrollDiv.style.position = 'absolute'; - scrollDiv.style.top = '-9999px'; - scrollDiv.style.width = '50px'; - scrollDiv.style.height = '50px'; - scrollDiv.style.overflow = 'scroll'; - - document.body.appendChild(scrollDiv); - scrollbarSize = scrollDiv.offsetWidth - scrollDiv.clientWidth; - document.body.removeChild(scrollDiv); - - scrollDiv = null; - return scrollbarSize; -} const Modal = React.createClass({ propTypes: { @@ -188,7 +167,7 @@ const Modal = React.createClass({ transitionAppear unmountOnExit in={show} - duration={Modal.TRANSITION_DURATION} + timeout={Modal.TRANSITION_DURATION} onExit={onExit} onExiting={onExiting} onExited={this.handleHidden} @@ -242,7 +221,7 @@ const Modal = React.createClass({
    { animation - ? {backdrop} + ? {backdrop} : backdrop } {modal} @@ -337,8 +316,7 @@ const Modal = React.createClass({ this.iosClickHack(); } - this.setState(this._getStyles() //eslint-disable-line react/no-did-mount-set-state - , () => this.focusModalContent()); + this.setState(this._getStyles(), () => this.focusModalContent()); }, onHide() { @@ -388,18 +366,16 @@ const Modal = React.createClass({ checkForFocus() { if (domUtils.canUseDom) { - try { - this.lastFocus = document.activeElement; - } - catch (e) {} // eslint-disable-line no-empty + this.lastFocus = domUtils.activeElement(document); } }, focusModalContent() { let modalContent = React.findDOMNode(this.refs.dialog); - let current = domUtils.activeElement(this); + let current = domUtils.activeElement(domUtils.ownerDocument(this)); let focusInModal = current && domUtils.contains(modalContent, current); + if (modalContent && this.props.autoFocus && !focusInModal) { this.lastFocus = current; modalContent.focus(); @@ -418,7 +394,7 @@ const Modal = React.createClass({ return; } - let active = domUtils.activeElement(this); + let active = domUtils.activeElement(domUtils.ownerDocument(this)); let modal = React.findDOMNode(this.refs.dialog); if (modal && modal !== active && !domUtils.contains(modal, active)) { diff --git a/src/NavDropdown.js b/src/NavDropdown.js new file mode 100644 index 0000000000..7e27fe0ce8 --- /dev/null +++ b/src/NavDropdown.js @@ -0,0 +1,32 @@ +import React from 'react'; +import Dropdown from './Dropdown'; + +class NavDropdown extends React.Component { + + render() { + let { children, title, noCaret, ...props } = this.props; + + return ( + + + {title} + + + {children} + + + ); + } +} + +NavDropdown.propTypes = { + noCaret: React.PropTypes.bool, + title: React.PropTypes.node.isRequired, + ...Dropdown.propTypes +}; + +export default NavDropdown; diff --git a/src/Overlay.js b/src/Overlay.js index e4082d2629..44e8f91d83 100644 --- a/src/Overlay.js +++ b/src/Overlay.js @@ -2,115 +2,43 @@ /* These properties are validated in 'Portal' and 'Position' components */ import React, { cloneElement } from 'react'; -import Portal from './Portal'; -import Position from './Position'; -import RootCloseWrapper from './RootCloseWrapper'; +import BaseOverlay from 'react-overlays/lib/Overlay'; import CustomPropTypes from './utils/CustomPropTypes'; import Fade from './Fade'; import classNames from 'classnames'; class Overlay extends React.Component { - constructor(props, context) { - super(props, context); - - this.state = {exited: !props.show}; - this.onHiddenListener = this.handleHidden.bind(this); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.show) { - this.setState({exited: false}); - } else if (!nextProps.animation) { - // Otherwise let handleHidden take care of marking exited. - this.setState({exited: true}); - } - } render() { let { - container - , containerPadding - , target - , placement - , rootClose - , children - , animation: Transition + children: child + , animation: transition , ...props } = this.props; - if (Transition === true) { - Transition = Fade; + if (transition === true) { + transition = Fade; } - // Don't un-render the overlay while it's transitioning out. - const mountOverlay = props.show || (Transition && !this.state.exited); - if (!mountOverlay) { - // Don't bother showing anything if we don't have to. - return null; - } - - let child = children; - - // Position is be inner-most because it adds inline styles into the child, - // which the other wrappers don't forward correctly. - child = ( - - {child} - - ); - - if (Transition) { - let { onExit, onExiting, onEnter, onEntering, onEntered } = props; - - // This animates the child node by injecting props, so it must precede - // anything that adds a wrapping div. - child = ( - - {child} - - ); - } else { + if (!transition) { child = cloneElement(child, { className: classNames('in', child.props.className) }); } - // This goes after everything else because it adds a wrapping div. - if (rootClose) { - child = ( - - {child} - - ); - } - return ( - + {child} - + ); } - - handleHidden(...args) { - this.setState({exited: true}); - - if (this.props.onExited) { - this.props.onExited(...args); - } - } } Overlay.propTypes = { - ...Portal.propTypes, - ...Position.propTypes, + ...BaseOverlay.propTypes, + /** * Set the visibility of the Overlay */ diff --git a/src/Panel.js b/src/Panel.js index b35bc7f749..56b2974806 100644 --- a/src/Panel.js +++ b/src/Panel.js @@ -15,7 +15,9 @@ const Panel = React.createClass({ footer: React.PropTypes.node, defaultExpanded: React.PropTypes.bool, expanded: React.PropTypes.bool, - eventKey: React.PropTypes.any + eventKey: React.PropTypes.any, + headerRole: React.PropTypes.string, + panelRole: React.PropTypes.string }, getDefaultProps() { @@ -55,26 +57,32 @@ const Panel = React.createClass({ }, render() { + let {headerRole, panelRole, ...props} = this.props; return ( -
    - {this.renderHeading()} - {this.props.collapsible ? this.renderCollapsibleBody() : this.renderBody()} + {this.renderHeading(headerRole)} + {this.props.collapsible ? this.renderCollapsibleBody(panelRole) : this.renderBody()} {this.renderFooter()}
    ); }, - renderCollapsibleBody() { - let collapseClass = this.prefixClass('collapse'); + renderCollapsibleBody(panelRole) { + let props = { + className: this.prefixClass('collapse'), + id: this.props.id, + ref: 'panel', + 'aria-hidden': !this.isExpanded() + }; + if (panelRole) { + props.role = panelRole; + } return ( -
    +
    {this.renderBody()}
    @@ -143,7 +151,7 @@ const Panel = React.createClass({ return React.isValidElement(child) && child.props.fill != null; }, - renderHeading() { + renderHeading(headerRole) { let header = this.props.header; if (!header) { @@ -152,7 +160,7 @@ const Panel = React.createClass({ if (!React.isValidElement(header) || Array.isArray(header)) { header = this.props.collapsible ? - this.renderCollapsibleTitle(header) : header; + this.renderCollapsibleTitle(header, headerRole) : header; } else { const className = classNames( this.prefixClass('title'), header.props.className @@ -161,7 +169,7 @@ const Panel = React.createClass({ if (this.props.collapsible) { header = cloneElement(header, { className, - children: this.renderAnchor(header.props.children) + children: this.renderAnchor(header.props.children, headerRole) }); } else { header = cloneElement(header, {className}); @@ -175,23 +183,25 @@ const Panel = React.createClass({ ); }, - renderAnchor(header) { + renderAnchor(header, headerRole) { return ( + aria-selected={this.isExpanded()} + onClick={this.handleSelect} + role={headerRole}> {header} ); }, - renderCollapsibleTitle(header) { + renderCollapsibleTitle(header, headerRole) { return ( -

    - {this.renderAnchor(header)} +

    + {this.renderAnchor(header, headerRole)}

    ); }, diff --git a/src/PanelGroup.js b/src/PanelGroup.js index fc18d97453..45354acb1a 100644 --- a/src/PanelGroup.js +++ b/src/PanelGroup.js @@ -36,9 +36,11 @@ const PanelGroup = React.createClass({ render() { let classes = this.getBsClassSet(); + let {className, ...props} = this.props; + if (this.props.accordion) { props.role = 'tablist'; } return ( -
    - {ValidComponentChildren.map(this.props.children, this.renderPanel)} +
    + {ValidComponentChildren.map(props.children, this.renderPanel)}
    ); }, @@ -54,6 +56,8 @@ const PanelGroup = React.createClass({ }; if (this.props.accordion) { + props.headerRole = 'tab'; + props.panelRole = 'tabpanel'; props.collapsible = true; props.expanded = (child.props.eventKey === activeKey); props.onSelect = this.handleSelect; diff --git a/src/Portal.js b/src/Portal.js index 7e87fcf08f..f27c32eb5c 100644 --- a/src/Portal.js +++ b/src/Portal.js @@ -1,93 +1,11 @@ -import React from 'react'; -import CustomPropTypes from './utils/CustomPropTypes'; -import domUtils from './utils/domUtils'; - -let Portal = React.createClass({ - - displayName: 'Portal', - - propTypes: { - /** - * The DOM Node that the Component will render it's children into - */ - container: CustomPropTypes.mountable - }, - - componentDidMount() { - this._renderOverlay(); - }, - - componentDidUpdate() { - this._renderOverlay(); - }, - - componentWillUnmount() { - this._unrenderOverlay(); - this._unmountOverlayTarget(); - }, - - _mountOverlayTarget() { - if (!this._overlayTarget) { - this._overlayTarget = document.createElement('div'); - this.getContainerDOMNode() - .appendChild(this._overlayTarget); - } - }, - - _unmountOverlayTarget() { - if (this._overlayTarget) { - this.getContainerDOMNode() - .removeChild(this._overlayTarget); - this._overlayTarget = null; - } - }, - - _renderOverlay() { - let overlay = !this.props.children - ? null - : React.Children.only(this.props.children); - - // Save reference for future access. - if (overlay !== null) { - this._mountOverlayTarget(); - this._overlayInstance = React.render(overlay, this._overlayTarget); - } else { - // Unrender if the component is null for transitions to null - this._unrenderOverlay(); - this._unmountOverlayTarget(); - } - }, - - _unrenderOverlay() { - if (this._overlayTarget) { - React.unmountComponentAtNode(this._overlayTarget); - this._overlayInstance = null; - } - }, - - render() { - return null; - }, - - getOverlayDOMNode() { - if (!this.isMounted()) { - throw new Error('getOverlayDOMNode(): A component must be mounted to have a DOM node.'); - } - - if (this._overlayInstance) { - if (this._overlayInstance.getWrappedDOMNode) { - return this._overlayInstance.getWrappedDOMNode(); - } else { - return React.findDOMNode(this._overlayInstance); - } - } - - return null; - }, - - getContainerDOMNode() { - return React.findDOMNode(this.props.container) || domUtils.ownerDocument(this).body; - } +import deprecationWarning from './utils/deprecationWarning'; +import Portal from 'react-overlays/lib/Portal'; + +export default deprecationWarning.wrapper(Portal, { + message: + 'The Portal component is deprecated in react-bootstrap. It has been moved to a more generic library: react-overlays. ' + + 'You can read more at: ' + + 'http://react-bootstrap.github.io/react-overlays/examples/#portal and ' + + 'https://github.com/react-bootstrap/react-bootstrap/issues/1084' }); -export default Portal; diff --git a/src/Position.js b/src/Position.js index 523c7973f7..903b35879b 100644 --- a/src/Position.js +++ b/src/Position.js @@ -1,136 +1,10 @@ -import React, { cloneElement } from 'react'; -import classNames from 'classnames'; -import domUtils from './utils/domUtils'; -import { calcOverlayPosition } from './utils/overlayPositionUtils'; -import CustomPropTypes from './utils/CustomPropTypes'; - -class Position extends React.Component { - constructor(props, context) { - super(props, context); - - this.state = { - positionLeft: null, - positionTop: null, - arrowOffsetLeft: null, - arrowOffsetTop: null - }; - - this._needsFlush = false; - this._lastTarget = null; - } - - componentDidMount() { - this.updatePosition(); - } - - componentWillReceiveProps() { - this._needsFlush = true; - } - - componentDidUpdate() { - if (this._needsFlush) { - this._needsFlush = false; - this.updatePosition(); - } - } - - componentWillUnmount() { - // Probably not necessary, but just in case holding a reference to the - // target causes problems somewhere. - this._lastTarget = null; - } - - render() { - const {children, className, ...props} = this.props; - const {positionLeft, positionTop, ...arrowPosition} = this.state; - - const child = React.Children.only(children); - return cloneElement( - child, - { - ...props, - ...arrowPosition, - positionTop, - positionLeft, - className: classNames(className, child.props.className), - style: { - ...child.props.style, - left: positionLeft, - top: positionTop - } - } - ); - } - - getTargetSafe() { - if (!this.props.target) { - return null; - } - - const target = this.props.target(this.props); - if (!target) { - // This is so we can just use === check below on all falsy targets. - return null; - } - - return target; - } - - updatePosition() { - const target = this.getTargetSafe(); - if (target === this._lastTarget) { - return; - } - this._lastTarget = target; - - if (!target) { - this.setState({ - positionLeft: null, - positionTop: null, - arrowOffsetLeft: null, - arrowOffsetTop: null - }); - - return; - } - - const overlay = React.findDOMNode(this); - const container = - React.findDOMNode(this.props.container) || - domUtils.ownerDocument(this).body; - - this.setState(calcOverlayPosition( - this.props.placement, - overlay, - target, - container, - this.props.containerPadding - )); - } -} - -Position.propTypes = { - /** - * Function mapping props to DOM node the component is positioned next to - */ - target: React.PropTypes.func, - /** - * "offsetParent" of the component - */ - container: CustomPropTypes.mountable, - /** - * Minimum spacing in pixels between container border and component border - */ - containerPadding: React.PropTypes.number, - /** - * How to position the component relative to the target - */ - placement: React.PropTypes.oneOf(['top', 'right', 'bottom', 'left']) -}; - -Position.defaultProps = { - containerPadding: 0, - placement: 'right' -}; - -export default Position; +import deprecationWarning from './utils/deprecationWarning'; +import Position from 'react-overlays/lib/Position'; + +export default deprecationWarning.wrapper(Position, { + message: + 'The Position component is deprecated in react-bootstrap. It has been moved to a more generic library: react-overlays. ' + + 'You can read more at: ' + + 'http://react-bootstrap.github.io/react-overlays/examples/#position and ' + + 'https://github.com/react-bootstrap/react-bootstrap/issues/1084' +}); diff --git a/src/RootCloseWrapper.js b/src/RootCloseWrapper.js deleted file mode 100644 index 35fecab769..0000000000 --- a/src/RootCloseWrapper.js +++ /dev/null @@ -1,87 +0,0 @@ -import React from 'react'; -import domUtils from './utils/domUtils'; -import EventListener from './utils/EventListener'; - -// TODO: Merge this logic with dropdown logic once #526 is done. - -// TODO: Consider using an ES6 symbol here, once we use babel-runtime. -const CLICK_WAS_INSIDE = '__click_was_inside'; - -function suppressRootClose(event) { - // Tag the native event to prevent the root close logic on document click. - // This seems safer than using event.nativeEvent.stopImmediatePropagation(), - // which is only supported in IE >= 9. - event.nativeEvent[CLICK_WAS_INSIDE] = true; -} - -export default class RootCloseWrapper extends React.Component { - constructor(props) { - super(props); - - this.handleDocumentClick = this.handleDocumentClick.bind(this); - this.handleDocumentKeyUp = this.handleDocumentKeyUp.bind(this); - } - - bindRootCloseHandlers() { - const doc = domUtils.ownerDocument(this); - - this._onDocumentClickListener = - EventListener.listen(doc, 'click', this.handleDocumentClick); - this._onDocumentKeyupListener = - EventListener.listen(doc, 'keyup', this.handleDocumentKeyUp); - } - - handleDocumentClick(e) { - // This is now the native event. - if (e[CLICK_WAS_INSIDE]) { - return; - } - - this.props.onRootClose(); - } - - handleDocumentKeyUp(e) { - if (e.keyCode === 27) { - this.props.onRootClose(); - } - } - - unbindRootCloseHandlers() { - if (this._onDocumentClickListener) { - this._onDocumentClickListener.remove(); - } - - if (this._onDocumentKeyupListener) { - this._onDocumentKeyupListener.remove(); - } - } - - componentDidMount() { - this.bindRootCloseHandlers(); - } - - render() { - // Wrap the child in a new element, so the child won't have to handle - // potentially combining multiple onClick listeners. - return ( -
    - {React.Children.only(this.props.children)} -
    - ); - } - - getWrappedDOMNode() { - // We can't use a ref to identify the wrapped child, since we might be - // stealing the ref from the owner, but we know exactly the DOM structure - // that will be rendered, so we can just do this to get the child's DOM - // node for doing size calculations in OverlayMixin. - return React.findDOMNode(this).children[0]; - } - - componentWillUnmount() { - this.unbindRootCloseHandlers(); - } -} -RootCloseWrapper.propTypes = { - onRootClose: React.PropTypes.func.isRequired -}; diff --git a/src/SplitButton.js b/src/SplitButton.js index 2710f9cf97..364577602d 100644 --- a/src/SplitButton.js +++ b/src/SplitButton.js @@ -1,115 +1,76 @@ -/* eslint react/prop-types: [2, {ignore: "bsSize"}] */ -/* BootstrapMixin contains `bsSize` type validation */ - import React from 'react'; -import classNames from 'classnames'; import BootstrapMixin from './BootstrapMixin'; -import DropdownStateMixin from './DropdownStateMixin'; import Button from './Button'; -import ButtonGroup from './ButtonGroup'; -import DropdownMenu from './DropdownMenu'; - -const SplitButton = React.createClass({ - mixins: [BootstrapMixin, DropdownStateMixin], +import Dropdown from './Dropdown'; +import SplitToggle from './SplitToggle'; - propTypes: { - pullRight: React.PropTypes.bool, - title: React.PropTypes.node, - href: React.PropTypes.string, - id: React.PropTypes.string, - target: React.PropTypes.string, - dropdownTitle: React.PropTypes.node, - dropup: React.PropTypes.bool, - onClick: React.PropTypes.func, - onSelect: React.PropTypes.func, - disabled: React.PropTypes.bool, - className: React.PropTypes.string, - children: React.PropTypes.node - }, - - getDefaultProps() { - return { - dropdownTitle: 'Toggle dropdown', - disabled: false, - dropup: false, - pullRight: false - }; - }, +class SplitButton extends React.Component { render() { - let groupClasses = { - 'open': this.state.open, - 'dropup': this.props.dropup - }; + let { + children, + title, + onClick, + target, + href, + // bsStyle is validated by 'Button' component + bsStyle, // eslint-disable-line + ...props } = this.props; - let button = ( - - ); + let { disabled } = props; - let dropdownButton = ( + let button = ( ); return ( - + {button} - {dropdownButton} - - {this.props.children} - - - ); - }, - - handleButtonClick(e) { - if (this.state.open) { - this.setDropdownState(false); - } - if (this.props.onClick) { - this.props.onClick(e, this.props.href, this.props.target); - } - }, + + + {children} + + + ); + } +} - handleDropdownClick(e) { - e.preventDefault(); +SplitButton.propTypes = { + //dropup: React.PropTypes.bool, + ...Dropdown.propTypes, + ...BootstrapMixin.propTypes, - this.setDropdownState(!this.state.open); - }, + /** + * @private + */ + onClick(){}, + target: React.PropTypes.string, + href: React.PropTypes.string, + /** + * The content of the split button. + */ + title: React.PropTypes.node.isRequired +}; - handleOptionSelect(key) { - if (this.props.onSelect) { - this.props.onSelect(key); - } +SplitButton.defaultProps = { + disabled: false, + dropup: false, + pullRight: false +}; - this.setDropdownState(false); - } -}); +SplitButton.Toggle = SplitToggle; export default SplitButton; diff --git a/src/SplitToggle.js b/src/SplitToggle.js new file mode 100644 index 0000000000..1b3a39afc4 --- /dev/null +++ b/src/SplitToggle.js @@ -0,0 +1,16 @@ +import React from 'react'; +import DropdownToggle from './DropdownToggle'; + +export default class SplitToggle extends React.Component { + render() { + return ( + + ); + } +} + +SplitToggle.defaultProps = DropdownToggle.defaultProps; diff --git a/src/Tab.js b/src/Tab.js new file mode 100644 index 0000000000..a1aad7d88f --- /dev/null +++ b/src/Tab.js @@ -0,0 +1,96 @@ +import React from 'react'; +import classNames from 'classnames'; +import TransitionEvents from './utils/TransitionEvents'; + +const Tab = React.createClass({ + propTypes: { + /** + * @private + */ + active: React.PropTypes.bool, + animation: React.PropTypes.bool, + onAnimateOutEnd: React.PropTypes.func, + disabled: React.PropTypes.bool, + title: React.PropTypes.node + }, + + getDefaultProps() { + return { + animation: true + }; + }, + + getInitialState() { + return { + animateIn: false, + animateOut: false + }; + }, + + componentWillReceiveProps(nextProps) { + if (this.props.animation) { + if (!this.state.animateIn && nextProps.active && !this.props.active) { + this.setState({ + animateIn: true + }); + } else if (!this.state.animateOut && !nextProps.active && this.props.active) { + this.setState({ + animateOut: true + }); + } + } + }, + + componentDidUpdate() { + if (this.state.animateIn) { + setTimeout(this.startAnimateIn, 0); + } + if (this.state.animateOut) { + TransitionEvents.addEndEventListener( + React.findDOMNode(this), + this.stopAnimateOut + ); + } + }, + + startAnimateIn() { + if (this.isMounted()) { + this.setState({ + animateIn: false + }); + } + }, + + stopAnimateOut() { + if (this.isMounted()) { + this.setState({ + animateOut: false + }); + + if (this.props.onAnimateOutEnd) { + this.props.onAnimateOutEnd(); + } + } + }, + + render() { + let classes = { + 'tab-pane': true, + 'fade': true, + 'active': this.props.active || this.state.animateOut, + 'in': this.props.active && !this.state.animateIn + }; + + return ( +
    + {this.props.children} +
    + ); + } +}); + +export default Tab; diff --git a/src/TabPane.js b/src/TabPane.js index 3e36db3542..4ea083b84f 100644 --- a/src/TabPane.js +++ b/src/TabPane.js @@ -1,90 +1,18 @@ import React from 'react'; -import classNames from 'classnames'; -import TransitionEvents from './utils/TransitionEvents'; +import deprecationWarning from './utils/deprecationWarning'; +import Tab from './Tab'; const TabPane = React.createClass({ - propTypes: { - active: React.PropTypes.bool, - animation: React.PropTypes.bool, - onAnimateOutEnd: React.PropTypes.func, - disabled: React.PropTypes.bool - }, - - getDefaultProps() { - return { - animation: true - }; - }, - - getInitialState() { - return { - animateIn: false, - animateOut: false - }; - }, - - componentWillReceiveProps(nextProps) { - if (this.props.animation) { - if (!this.state.animateIn && nextProps.active && !this.props.active) { - this.setState({ - animateIn: true - }); - } else if (!this.state.animateOut && !nextProps.active && this.props.active) { - this.setState({ - animateOut: true - }); - } - } - }, - - componentDidUpdate() { - if (this.state.animateIn) { - setTimeout(this.startAnimateIn, 0); - } - if (this.state.animateOut) { - TransitionEvents.addEndEventListener( - React.findDOMNode(this), - this.stopAnimateOut - ); - } - }, - - startAnimateIn() { - if (this.isMounted()) { - this.setState({ - animateIn: false - }); - } - }, - - stopAnimateOut() { - if (this.isMounted()) { - this.setState({ - animateOut: false - }); - - if (this.props.onAnimateOutEnd) { - this.props.onAnimateOutEnd(); - } - } + componentWillMount() { + deprecationWarning( + 'TabPane', 'Tab', + 'https://github.com/react-bootstrap/react-bootstrap/pull/1091' + ); }, render() { - let classes = { - 'tab-pane': true, - 'fade': true, - 'active': this.props.active || this.state.animateOut, - 'in': this.props.active && !this.state.animateIn - }; - return ( -
    - {this.props.children} -
    + ); } }); diff --git a/src/TabbedArea.js b/src/TabbedArea.js index 8bfb1e3ae4..3856b6fb35 100644 --- a/src/TabbedArea.js +++ b/src/TabbedArea.js @@ -1,162 +1,28 @@ -import React, { cloneElement } from 'react'; -import BootstrapMixin from './BootstrapMixin'; - +import React from 'react'; +import Tabs from './Tabs'; +import TabPane from './TabPane'; import ValidComponentChildren from './utils/ValidComponentChildren'; -import Nav from './Nav'; -import NavItem from './NavItem'; - -let panelId = (props, child) => child.props.id ? child.props.id : props.id && (props.id + '___panel___' + child.props.eventKey); -let tabId = (props, child) => child.props.id ? child.props.id + '___tab' : props.id && (props.id + '___tab___' + child.props.eventKey); - -function getDefaultActiveKeyFromChildren(children) { - let defaultActiveKey; - - ValidComponentChildren.forEach(children, function(child) { - if (defaultActiveKey == null) { - defaultActiveKey = child.props.eventKey; - } - }); - - return defaultActiveKey; -} +import deprecationWarning from './utils/deprecationWarning'; const TabbedArea = React.createClass({ - mixins: [BootstrapMixin], - - propTypes: { - activeKey: React.PropTypes.any, - defaultActiveKey: React.PropTypes.any, - bsStyle: React.PropTypes.oneOf(['tabs', 'pills']), - animation: React.PropTypes.bool, - id: React.PropTypes.string, - onSelect: React.PropTypes.func - }, - - getDefaultProps() { - return { - bsStyle: 'tabs', - animation: true - }; - }, - - getInitialState() { - let defaultActiveKey = this.props.defaultActiveKey != null ? - this.props.defaultActiveKey : getDefaultActiveKeyFromChildren(this.props.children); - - return { - activeKey: defaultActiveKey, - previousActiveKey: null - }; - }, - - componentWillReceiveProps(nextProps) { - if (nextProps.activeKey != null && nextProps.activeKey !== this.props.activeKey) { - // check if the 'previousActiveKey' child still exists - let previousActiveKey = this.props.activeKey; - React.Children.forEach(nextProps.children, (child) => { - if (React.isValidElement(child)) { - if (child.props.eventKey === previousActiveKey) { - this.setState({ - previousActiveKey - }); - return; - } - } - }); - } - }, - - handlePaneAnimateOutEnd() { - this.setState({ - previousActiveKey: null - }); - }, - - render() { - let { id, ...props } = this.props; - - function renderTabIfSet(child) { - return child.props.tab != null ? this.renderTab(child) : null; - } - - let nav = ( - - ); - - return ( -
    - {nav} -
    - {ValidComponentChildren.map(this.props.children, this.renderPane)} -
    -
    + componentWillMount() { + deprecationWarning( + 'TabbedArea', 'Tabs', + 'https://github.com/react-bootstrap/react-bootstrap/pull/1091' ); }, - getActiveKey() { - return this.props.activeKey != null ? this.props.activeKey : this.state.activeKey; - }, - - renderPane(child, index) { - let previousActiveKey = this.state.previousActiveKey; - - let shouldPaneBeSetActive = child.props.eventKey === this.getActiveKey(); - let thereIsNoActivePane = previousActiveKey == null; - - let paneIsAlreadyActive = previousActiveKey != null && child.props.eventKey === previousActiveKey; - - return cloneElement( - child, - { - active: shouldPaneBeSetActive && (thereIsNoActivePane || !this.props.animation), - id: panelId(this.props, child), - 'aria-labelledby': tabId(this.props, child), - key: child.key ? child.key : index, - animation: this.props.animation, - onAnimateOutEnd: paneIsAlreadyActive ? this.handlePaneAnimateOutEnd : null - } - ); - }, + render() { + const {children, ...props} = this.props; - renderTab(child) { - let {eventKey, className, tab, disabled } = child.props; + const tabs = ValidComponentChildren.map(children, function (child) { + const {tab: title, ...others} = child.props; + return ; + }); return ( - - {tab} - + {tabs} ); - }, - - shouldComponentUpdate() { - // Defer any updates to this component during the `onSelect` handler. - return !this._isChanging; - }, - - handleSelect(selectedKey) { - if (this.props.onSelect) { - this._isChanging = true; - this.props.onSelect(selectedKey); - this._isChanging = false; - return; - } - - // if there is no external handler, then use embedded one - let previousActiveKey = this.getActiveKey(); - if (selectedKey !== previousActiveKey) { - this.setState({ - activeKey: selectedKey, - previousActiveKey - }); - } } }); diff --git a/src/Tabs.js b/src/Tabs.js new file mode 100644 index 0000000000..ebea21243e --- /dev/null +++ b/src/Tabs.js @@ -0,0 +1,286 @@ +import React, { cloneElement } from 'react'; + +import Col from './Col'; +import Grid from './Grid'; +import Nav from './Nav'; +import NavItem from './NavItem'; +import Row from './Row'; +import styleMaps from './styleMaps'; + +import ValidComponentChildren from './utils/ValidComponentChildren'; + +let paneId = (props, child) => child.props.id ? child.props.id : props.id && (props.id + '___pane___' + child.props.eventKey); +let tabId = (props, child) => child.props.id ? child.props.id + '___tab' : props.id && (props.id + '___tab___' + child.props.eventKey); + +function getDefaultActiveKeyFromChildren(children) { + let defaultActiveKey; + + ValidComponentChildren.forEach(children, function(child) { + if (defaultActiveKey == null) { + defaultActiveKey = child.props.eventKey; + } + }); + + return defaultActiveKey; +} + +const Tabs = React.createClass({ + propTypes: { + activeKey: React.PropTypes.any, + defaultActiveKey: React.PropTypes.any, + /** + * Navigation style for tabs + * + * If not specified, it will be treated as `'tabs'` when vertically + * positioned and `'pills'` when horizontally positioned. + */ + bsStyle: React.PropTypes.oneOf(['tabs', 'pills']), + animation: React.PropTypes.bool, + id: React.PropTypes.string, + onSelect: React.PropTypes.func, + position: React.PropTypes.oneOf(['top', 'left', 'right']), + /** + * Number of grid columns for the tabs if horizontally positioned + * + * This accepts either a single width or a mapping of size to width. + */ + tabWidth: React.PropTypes.oneOfType([ + React.PropTypes.number, + React.PropTypes.object + ]), + /** + * Number of grid columns for the panes if horizontally positioned + * + * This accepts either a single width or a mapping of size to width. If not + * specified, it will be treated as `styleMaps.GRID_COLUMNS` minus + * `tabWidth`. + */ + paneWidth: React.PropTypes.oneOfType([ + React.PropTypes.number, + React.PropTypes.object + ]) + }, + + getDefaultProps() { + return { + animation: true, + tabWidth: 2, + position: 'top' + }; + }, + + getInitialState() { + let defaultActiveKey = this.props.defaultActiveKey != null ? + this.props.defaultActiveKey : getDefaultActiveKeyFromChildren(this.props.children); + + return { + activeKey: defaultActiveKey, + previousActiveKey: null + }; + }, + + componentWillReceiveProps(nextProps) { + if (nextProps.activeKey != null && nextProps.activeKey !== this.props.activeKey) { + // check if the 'previousActiveKey' child still exists + let previousActiveKey = this.props.activeKey; + React.Children.forEach(nextProps.children, (child) => { + if (React.isValidElement(child)) { + if (child.props.eventKey === previousActiveKey) { + this.setState({ + previousActiveKey + }); + return; + } + } + }); + } + }, + + handlePaneAnimateOutEnd() { + this.setState({ + previousActiveKey: null + }); + }, + + render() { + let { + id, + className, + style, + position, + bsStyle, + tabWidth, + paneWidth, + children, + ...props + } = this.props; + + const isHorizontal = position === 'left' || position === 'right'; + + if (bsStyle == null) { + bsStyle = isHorizontal ? 'pills' : 'tabs'; + } + + const containerProps = {id, className, style}; + + const tabsProps = { + ...props, + bsStyle, + stacked: isHorizontal, + activeKey: this.getActiveKey(), + onSelect: this.handleSelect, + ref: 'tabs', + role: 'tablist' + }; + const childTabs = ValidComponentChildren.map(children, this.renderTab); + + const panesProps = { + className: 'tab-content', + ref: 'panes' + }; + const childPanes = ValidComponentChildren.map(children, this.renderPane); + + if (isHorizontal) { + const {tabsColProps, panesColProps} = + this.getColProps({tabWidth, paneWidth}); + + const tabs = ( + + {childTabs} + + ); + const panes = ( + + {childPanes} + + ); + + let body; + if (position === 'left') { + body = ( + + {tabs} + {panes} + + ); + } else { + body = ( + + {panes} + {tabs} + + ); + } + + return ( + + {body} + + ); + } else { + return ( +
    + + +
    + {childPanes} +
    +
    + ); + } + }, + + getActiveKey() { + return this.props.activeKey != null ? this.props.activeKey : this.state.activeKey; + }, + + renderPane(child, index) { + let previousActiveKey = this.state.previousActiveKey; + + let shouldPaneBeSetActive = child.props.eventKey === this.getActiveKey(); + let thereIsNoActivePane = previousActiveKey == null; + + let paneIsAlreadyActive = previousActiveKey != null && child.props.eventKey === previousActiveKey; + + return cloneElement( + child, + { + active: shouldPaneBeSetActive && (thereIsNoActivePane || !this.props.animation), + id: paneId(this.props, child), + 'aria-labelledby': tabId(this.props, child), + key: child.key ? child.key : index, + animation: this.props.animation, + onAnimateOutEnd: paneIsAlreadyActive ? this.handlePaneAnimateOutEnd : null + } + ); + }, + + renderTab(child) { + if (child.props.title == null) { + return null; + } + + let {eventKey, title, disabled} = child.props; + + return ( + + {title} + + ); + }, + + getColProps({tabWidth, paneWidth}) { + let tabsColProps; + if (tabWidth instanceof Object) { + tabsColProps = tabWidth; + } else { + tabsColProps = {xs: tabWidth}; + } + + let panesColProps; + if (paneWidth == null) { + panesColProps = {}; + Object.keys(tabsColProps).forEach(function (size) { + panesColProps[size] = styleMaps.GRID_COLUMNS - tabsColProps[size]; + }); + } else if (paneWidth instanceof Object) { + panesColProps = paneWidth; + } else { + panesColProps = {xs: paneWidth}; + } + + return {tabsColProps, panesColProps}; + }, + + shouldComponentUpdate() { + // Defer any updates to this component during the `onSelect` handler. + return !this._isChanging; + }, + + handleSelect(selectedKey) { + if (this.props.onSelect) { + this._isChanging = true; + this.props.onSelect(selectedKey); + this._isChanging = false; + return; + } + + // if there is no external handler, then use embedded one + let previousActiveKey = this.getActiveKey(); + if (selectedKey !== previousActiveKey) { + this.setState({ + activeKey: selectedKey, + previousActiveKey + }); + } + } +}); + +export default Tabs; diff --git a/src/Tooltip.js b/src/Tooltip.js index b500244c37..2691b14abf 100644 --- a/src/Tooltip.js +++ b/src/Tooltip.js @@ -60,8 +60,7 @@ const Tooltip = React.createClass({ const style = { 'left': this.props.positionLeft, 'top': this.props.positionTop, - // we don't want to expose the `style` property - ...this.props.style // eslint-disable-line react/prop-types + ...this.props.style }; const arrowStyle = { diff --git a/src/Transition.js b/src/Transition.js index 8235d41f9f..5dea099006 100644 --- a/src/Transition.js +++ b/src/Transition.js @@ -1,273 +1,10 @@ -import React from 'react'; -import TransitionEvents from './utils/TransitionEvents'; -import classnames from 'classnames'; - -export const UNMOUNTED = 0; -export const EXITED = 1; -export const ENTERING = 2; -export const ENTERED = 3; -export const EXITING = 4; - -class Transition extends React.Component { - constructor(props, context) { - super(props, context); - - let initialStatus; - if (props.in) { - // Start enter transition in componentDidMount. - initialStatus = props.transitionAppear ? EXITED : ENTERED; - } else { - initialStatus = props.unmountOnExit ? UNMOUNTED : EXITED; - } - this.state = {status: initialStatus}; - - this.nextCallback = null; - } - - componentDidMount() { - if (this.props.transitionAppear && this.props.in) { - this.performEnter(this.props); - } - } - - componentWillReceiveProps(nextProps) { - const status = this.state.status; - if (nextProps.in) { - if (status === EXITING) { - this.performEnter(nextProps); - } else if (this.props.unmountOnExit) { - if (status === UNMOUNTED) { - // Start enter transition in componentDidUpdate. - this.setState({status: EXITED}); - } - } else if (status === EXITED) { - this.performEnter(nextProps); - } - - // Otherwise we're already entering or entered. - } else { - if (status === ENTERING || status === ENTERED) { - this.performExit(nextProps); - } - - // Otherwise we're already exited or exiting. - } - } - - componentDidUpdate() { - if (this.props.unmountOnExit && this.state.status === EXITED) { - // EXITED is always a transitional state to either ENTERING or UNMOUNTED - // when using unmountOnExit. - if (this.props.in) { - this.performEnter(this.props); - } else { - this.setState({status: UNMOUNTED}); - } - } - } - - componentWillUnmount() { - this.cancelNextCallback(); - } - - performEnter(props) { - this.cancelNextCallback(); - const node = React.findDOMNode(this); - - // Not this.props, because we might be about to receive new props. - props.onEnter(node); - - this.safeSetState({status: ENTERING}, () => { - this.props.onEntering(node); - - this.onTransitionEnd(node, () => { - this.safeSetState({status: ENTERED}, () => { - this.props.onEntered(node); - }); - }); - }); - } - - performExit(props) { - this.cancelNextCallback(); - const node = React.findDOMNode(this); - - // Not this.props, because we might be about to receive new props. - props.onExit(node); - - this.safeSetState({status: EXITING}, () => { - this.props.onExiting(node); - - this.onTransitionEnd(node, () => { - this.safeSetState({status: EXITED}, () => { - this.props.onExited(node); - }); - }); - }); - } - - cancelNextCallback() { - if (this.nextCallback !== null) { - this.nextCallback.cancel(); - this.nextCallback = null; - } - } - - safeSetState(nextState, callback) { - // This shouldn't be necessary, but there are weird race conditions with - // setState callbacks and unmounting in testing, so always make sure that - // we can cancel any pending setState callbacks after we unmount. - this.setState(nextState, this.setNextCallback(callback)); - } - - setNextCallback(callback) { - let active = true; - - this.nextCallback = (event) => { - if (active) { - active = false; - this.nextCallback = null; - - callback(event); - } - }; - - this.nextCallback.cancel = () => { - active = false; - }; - - return this.nextCallback; - } - - onTransitionEnd(node, handler) { - this.setNextCallback(handler); - - if (node) { - TransitionEvents.addEndEventListener(node, this.nextCallback); - setTimeout(this.nextCallback, this.props.duration); - } else { - setTimeout(this.nextCallback, 0); - } - } - - render() { - const status = this.state.status; - if (status === UNMOUNTED) { - return null; - } - - const {children, className, ...childProps} = this.props; - Object.keys(Transition.propTypes).forEach(key => delete childProps[key]); - - let transitionClassName; - if (status === EXITED) { - transitionClassName = this.props.exitedClassName; - } else if (status === ENTERING) { - transitionClassName = this.props.enteringClassName; - } else if (status === ENTERED) { - transitionClassName = this.props.enteredClassName; - } else if (status === EXITING) { - transitionClassName = this.props.exitingClassName; - } - - const child = React.Children.only(children); - return React.cloneElement( - child, - { - ...childProps, - className: classnames( - child.props.className, - className, - transitionClassName - ) - } - ); - } -} - -Transition.propTypes = { - /** - * Show the component; triggers the enter or exit animation - */ - in: React.PropTypes.bool, - - /** - * Unmount the component (remove it from the DOM) when it is not shown - */ - unmountOnExit: React.PropTypes.bool, - - /** - * Run the enter animation when the component mounts, if it is initially - * shown - */ - transitionAppear: React.PropTypes.bool, - - /** - * Duration of the animation in milliseconds, to ensure that finishing - * callbacks are fired even if the original browser transition end events are - * canceled - */ - duration: React.PropTypes.number, - - /** - * CSS class or classes applied when the component is exited - */ - exitedClassName: React.PropTypes.string, - /** - * CSS class or classes applied while the component is exiting - */ - exitingClassName: React.PropTypes.string, - /** - * CSS class or classes applied when the component is entered - */ - enteredClassName: React.PropTypes.string, - /** - * CSS class or classes applied while the component is entering - */ - enteringClassName: React.PropTypes.string, - - /** - * Callback fired before the "entering" classes are applied - */ - onEnter: React.PropTypes.func, - /** - * Callback fired after the "entering" classes are applied - */ - onEntering: React.PropTypes.func, - /** - * Callback fired after the "enter" classes are applied - */ - onEntered: React.PropTypes.func, - /** - * Callback fired before the "exiting" classes are applied - */ - onExit: React.PropTypes.func, - /** - * Callback fired after the "exiting" classes are applied - */ - onExiting: React.PropTypes.func, - /** - * Callback fired after the "exited" classes are applied - */ - onExited: React.PropTypes.func -}; - -// Name the function so it is clearer in the documentation -function noop() {} - -Transition.defaultProps = { - in: false, - duration: 300, - unmountOnExit: false, - transitionAppear: false, - - onEnter: noop, - onEntering: noop, - onEntered: noop, - - onExit: noop, - onExiting: noop, - onExited: noop -}; - -export default Transition; +import deprecationWarning from './utils/deprecationWarning'; +import Transition from 'react-overlays/lib/Transition'; + +export default deprecationWarning.wrapper(Transition, { + message: + 'The Transition component is deprecated in react-bootstrap. It has been moved to a more generic library: react-overlays. ' + + 'You can read more at: ' + + 'http://react-bootstrap.github.io/react-overlays/examples/#transition and ' + + 'https://github.com/react-bootstrap/react-bootstrap/issues/1084' +}); diff --git a/src/index.js b/src/index.js index f73f13c413..72e728bf4f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,5 @@ +import deprecationWarning from './utils/deprecationWarning'; + export Accordion from './Accordion'; export Affix from './Affix'; export AffixMixin from './AffixMixin'; @@ -13,9 +15,12 @@ export CarouselItem from './CarouselItem'; export Col from './Col'; export CollapsibleMixin from './CollapsibleMixin'; export CollapsibleNav from './CollapsibleNav'; + +export Dropdown from './Dropdown'; export DropdownButton from './DropdownButton'; -export DropdownMenu from './DropdownMenu'; -export DropdownStateMixin from './DropdownStateMixin'; +export NavDropdown from './NavDropdown'; +export SplitButton from './SplitButton'; + export FadeMixin from './FadeMixin'; export Glyphicon from './Glyphicon'; export Grid from './Grid'; @@ -53,9 +58,11 @@ export SafeAnchor from './SafeAnchor'; export SplitButton from './SplitButton'; export styleMaps from './styleMaps'; export SubNav from './SubNav'; +export Tab from './Tab'; export TabbedArea from './TabbedArea'; export Table from './Table'; export TabPane from './TabPane'; +export Tabs from './Tabs'; export Thumbnail from './Thumbnail'; export Tooltip from './Tooltip'; export Well from './Well'; @@ -67,4 +74,37 @@ export Collapse from './Collapse'; export Fade from './Fade'; export * as FormControls from './FormControls'; -export * as utils from './utils'; + +import domUtils from './utils/domUtils'; +import childrenValueInputValidation from './utils/childrenValueInputValidation'; +import createChainedFunction from './utils/createChainedFunction'; +import ValidComponentChildren from './utils/ValidComponentChildren'; +import CustomPropTypes from './utils/CustomPropTypes'; + +export const utils = { + childrenValueInputValidation, + createChainedFunction, + ValidComponentChildren, + CustomPropTypes, + domUtils: createDeprecationWrapper(domUtils, 'utils/domUtils', 'npm install dom-helpers'), +}; + +function createDeprecationWrapper(obj, deprecated, instead, link){ + let wrapper = {}; + + if (process.env.NODE_ENV === 'production'){ + return obj; + } + + Object.keys(obj).forEach(key => { + Object.defineProperty(wrapper, key, { + get(){ + deprecationWarning(deprecated, instead, link); + return obj[key]; + }, + set(x){ obj[key] = x; } + }); + }); + + return wrapper; +} diff --git a/src/styleMaps.js b/src/styleMaps.js index 5d8e72cd56..4360d1a675 100644 --- a/src/styleMaps.js +++ b/src/styleMaps.js @@ -45,7 +45,8 @@ const styleMaps = { 'md': 'md', 'sm': 'sm', 'xs': 'xs' - } + }, + GRID_COLUMNS: 12 }; export default styleMaps; diff --git a/src/templates/factory.index.js.template b/src/templates/factory.index.js.template deleted file mode 100644 index ba70624988..0000000000 --- a/src/templates/factory.index.js.template +++ /dev/null @@ -1,13 +0,0 @@ -import warning from 'react/lib/warning'; - -<% _.forEach(components, function (component) { %> -import <%= component %> from './<%= component %>'; -<% }); %> - -warning(false, 'Support for factories will be removed in v0.25, for details see https://github.com/react-bootstrap/react-bootstrap/issues/825'); - -export default { -<% _.forEach(components, function (component) { %> - <%= component %>, -<% }); %> -} diff --git a/src/templates/factory.js.template b/src/templates/factory.js.template deleted file mode 100644 index d9a3042ba8..0000000000 --- a/src/templates/factory.js.template +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; -import warning from 'react/lib/warning'; -import <%= name %> from '../<%= name %>'; - -warning(false, 'Support for factories will be removed in v0.25, for details see https://github.com/react-bootstrap/react-bootstrap/issues/825'); - -export default React.createFactory(<%= name %>); diff --git a/src/utils/CustomPropTypes.js b/src/utils/CustomPropTypes.js index 808de84c2e..8f6c0a00af 100644 --- a/src/utils/CustomPropTypes.js +++ b/src/utils/CustomPropTypes.js @@ -1,8 +1,35 @@ import React from 'react'; import warning from 'react/lib/warning'; +import childrenToArray from './childrenToArray'; const ANONYMOUS = '<>'; +/** + * Create chain-able isRequired validator + * + * Largely copied directly from: + * https://github.com/facebook/react/blob/0.11-stable/src/core/ReactPropTypes.js#L94 + */ +function createChainableTypeChecker(validate) { + function checkType(isRequired, props, propName, componentName) { + componentName = componentName || ANONYMOUS; + if (props[propName] == null) { + if (isRequired) { + return new Error( + `Required prop '${propName}' was not specified in '${componentName}'.` + ); + } + } else { + return validate(props, propName, componentName); + } + } + + let chainedCheckType = checkType.bind(null, false); + chainedCheckType.isRequired = checkType.bind(null, true); + + return chainedCheckType; +} + const CustomPropTypes = { deprecated(propType, explanation){ @@ -28,6 +55,54 @@ const CustomPropTypes = { }; }, + requiredRoles(...roles) { + return createChainableTypeChecker( + function requiredRolesValidator(props, propName, component) { + let missing; + let children = childrenToArray(props.children); + + let inRole = (role, child) => role === child.props.bsRole; + + roles.every(role => { + if (!children.some(child => inRole(role, child))){ + missing = role; + return false; + } + return true; + }); + + if (missing) { + return new Error(`(children) ${component} - Missing a required child with bsRole: ${missing}. ` + + `${component} must have at least one child of each of the following bsRoles: ${roles.join(', ')}`); + } + }); + }, + + exclusiveRoles(...roles) { + return createChainableTypeChecker( + function exclusiveRolesValidator(props, propName, component) { + let children = childrenToArray(props.children); + let duplicate; + + roles.every(role => { + let childrenWithRole = children.filter(child => child.props.bsRole === role); + + if (childrenWithRole.length > 1){ + duplicate = role; + return false; + } + return true; + }); + + if (duplicate) { + return new Error( + `(children) ${component} - Duplicate children detected of bsRole: ${duplicate}. ` + + `Only one child each allowed with the following bsRoles: ${roles.join(', ')}`); + } + + }); + }, + /** * Checks whether a prop provides a DOM element * @@ -84,32 +159,6 @@ function errMsg(props, propName, componentName, msgContinuation) { ` supplied to '${componentName}'${msgContinuation}`; } -/** - * Create chain-able isRequired validator - * - * Largely copied directly from: - * https://github.com/facebook/react/blob/0.11-stable/src/core/ReactPropTypes.js#L94 - */ -function createChainableTypeChecker(validate) { - function checkType(isRequired, props, propName, componentName) { - componentName = componentName || ANONYMOUS; - if (props[propName] == null) { - if (isRequired) { - return new Error( - `Required prop '${propName}' was not specified in '${componentName}'.` - ); - } - } else { - return validate(props, propName, componentName); - } - } - - let chainedCheckType = checkType.bind(null, false); - chainedCheckType.isRequired = checkType.bind(null, true); - - return chainedCheckType; -} - function createMountableChecker() { function validate(props, propName, componentName) { if (typeof props[propName] !== 'object' || diff --git a/src/utils/childrenToArray.js b/src/utils/childrenToArray.js new file mode 100644 index 0000000000..f1127886a1 --- /dev/null +++ b/src/utils/childrenToArray.js @@ -0,0 +1,15 @@ +import validChildren from './ValidComponentChildren'; + +export default function childrenAsArray(children) { + let result = []; + + if (children === undefined) { + return result; + } + + validChildren.forEach(children, child => { + result.push(child); + }); + + return result; +} diff --git a/src/utils/deprecationWarning.js b/src/utils/deprecationWarning.js index e4115967c9..3028dc2f10 100644 --- a/src/utils/deprecationWarning.js +++ b/src/utils/deprecationWarning.js @@ -2,18 +2,40 @@ import warning from 'react/lib/warning'; const warned = {}; -export default function deprecationWarning(oldname, newname, link) { - const warnKey = `${oldname}\n${newname}`; - if (warned[warnKey]) { - return; +function deprecationWarning(oldname, newname, link) { + let message; + + if (typeof oldname === 'object'){ + message = oldname.message; } + else { + message = `${oldname} is deprecated. Use ${newname} instead.`; - let message = `${oldname} is deprecated. Use ${newname} instead.`; + if (link) { + message += `\nYou can read more about it at ${link}`; + } + } - if (link) { - message += `\nYou can read more about it at ${link}`; + if (warned[message]) { + return; } warning(false, message); - warned[warnKey] = true; + warned[message] = true; } + + +deprecationWarning.wrapper = function(Component, ...args){ + return class DeprecatedComponent extends Component { + componentWillMount(...methodArgs){ + deprecationWarning(...args); + + if (super.componentWillMount) { + super.componentWillMount(...methodArgs); + } + } + }; +}; + +export default deprecationWarning; + diff --git a/src/utils/domUtils.js b/src/utils/domUtils.js index f0df13f8cf..b9f99c7e49 100644 --- a/src/utils/domUtils.js +++ b/src/utils/domUtils.js @@ -1,83 +1,33 @@ import React from 'react'; +import canUseDom from 'dom-helpers/util/inDOM'; +import getOwnerDocument from 'dom-helpers/ownerDocument'; +import getOwnerWindow from 'dom-helpers/ownerWindow'; -let canUseDom = !!( - typeof window !== 'undefined' && - window.document && - window.document.createElement -); +import contains from 'dom-helpers/query/contains'; +import activeElement from 'dom-helpers/activeElement'; +import getOffset from 'dom-helpers/query/offset'; +import offsetParent from 'dom-helpers/query/offsetParent'; +import getPosition from 'dom-helpers/query/position'; + +import css from 'dom-helpers/style'; -/** - * Get elements owner document - * - * @param {ReactComponent|HTMLElement} componentOrElement - * @returns {HTMLElement} - */ function ownerDocument(componentOrElement) { let elem = React.findDOMNode(componentOrElement); - return (elem && elem.ownerDocument) || document; + return getOwnerDocument((elem && elem.ownerDocument) || document); } function ownerWindow(componentOrElement) { let doc = ownerDocument(componentOrElement); - return doc.defaultView - ? doc.defaultView - : doc.parentWindow; -} - -/** - * get the active element, safe in IE - * @return {HTMLElement} - */ -function getActiveElement(componentOrElement){ - let doc = ownerDocument(componentOrElement); - - try { - return doc.activeElement || doc.body; - } catch (e) { - return doc.body; - } + return getOwnerWindow(doc); } -/** - * Shortcut to compute element style - * - * @param {HTMLElement} elem - * @returns {CssStyle} - */ +//TODO remove in 0.26 function getComputedStyles(elem) { return ownerDocument(elem).defaultView.getComputedStyle(elem, null); } -/** - * Get elements offset - * - * TODO: REMOVE JQUERY! - * - * @param {HTMLElement} DOMNode - * @returns {{top: number, left: number}} - */ -function getOffset(DOMNode) { - if (window.jQuery) { - return window.jQuery(DOMNode).offset(); - } - - let docElem = ownerDocument(DOMNode).documentElement; - let box = { top: 0, left: 0 }; - - // If we don't have gBCR, just use 0,0 rather than error - // BlackBerry 5, iOS 3 (original iPhone) - if ( typeof DOMNode.getBoundingClientRect !== 'undefined' ) { - box = DOMNode.getBoundingClientRect(); - } - - return { - top: box.top + window.pageYOffset - docElem.clientTop, - left: box.left + window.pageXOffset - docElem.clientLeft - }; -} - /** * Get the height of the document * @@ -87,65 +37,6 @@ function getDocumentHeight() { return Math.max(document.documentElement.offsetHeight, document.height, document.body.scrollHeight, document.body.offsetHeight); } -/** - * Get elements position - * - * TODO: REMOVE JQUERY! - * - * @param {HTMLElement} elem - * @param {HTMLElement?} offsetParent - * @returns {{top: number, left: number}} - */ -function getPosition(elem, offsetParent) { - let offset, - parentOffset; - - if (window.jQuery) { - if (!offsetParent) { - return window.jQuery(elem).position(); - } - - offset = window.jQuery(elem).offset(); - parentOffset = window.jQuery(offsetParent).offset(); - - // Get element offset relative to offsetParent - return { - top: offset.top - parentOffset.top, - left: offset.left - parentOffset.left - }; - } - - parentOffset = {top: 0, left: 0}; - - // Fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is its only offset parent - if (getComputedStyles(elem).position === 'fixed' ) { - // We assume that getBoundingClientRect is available when computed position is fixed - offset = elem.getBoundingClientRect(); - - } else { - if (!offsetParent) { - // Get *real* offsetParent - offsetParent = offsetParentFunc(elem); - } - - // Get correct offsets - offset = getOffset(elem); - if ( offsetParent.nodeName !== 'HTML') { - parentOffset = getOffset(offsetParent); - } - - // Add offsetParent borders - parentOffset.top += parseInt(getComputedStyles(offsetParent).borderTopWidth, 10); - parentOffset.left += parseInt(getComputedStyles(offsetParent).borderLeftWidth, 10); - } - - // Subtract parent offsets and element margins - return { - top: offset.top - parentOffset.top - parseInt(getComputedStyles(elem).marginTop, 10), - left: offset.left - parentOffset.left - parseInt(getComputedStyles(elem).marginLeft, 10) - }; -} - /** * Get an element's size * @@ -165,58 +56,17 @@ function getSize(elem) { return rect; } -/** - * Get parent element - * - * @param {HTMLElement?} elem - * @returns {HTMLElement} - */ -function offsetParentFunc(elem) { - let docElem = ownerDocument(elem).documentElement; - let offsetParent = elem.offsetParent || docElem; - - while ( offsetParent && ( offsetParent.nodeName !== 'HTML' && - getComputedStyles(offsetParent).position === 'static' ) ) { - offsetParent = offsetParent.offsetParent; - } - - return offsetParent || docElem; -} - -/** - * Cross browser .contains() polyfill - * @param {HTMLElement} elem - * @param {HTMLElement} inner - * @return {bool} - */ -function contains(elem, inner){ - function ie8Contains(root, node) { - while (node) { - if (node === root) { - return true; - } - node = node.parentNode; - } - return false; - } - - return (elem && elem.contains) - ? elem.contains(inner) - : (elem && elem.compareDocumentPosition) - ? elem === inner || !!(elem.compareDocumentPosition(inner) & 16) - : ie8Contains(elem, inner); -} - export default { canUseDom, + css, + getComputedStyles, contains, ownerWindow, ownerDocument, - getComputedStyles, getOffset, getDocumentHeight, getPosition, getSize, - activeElement: getActiveElement, - offsetParent: offsetParentFunc + activeElement, + offsetParent }; diff --git a/src/utils/index.js b/src/utils/index.js index f024432a7b..e84c9f5b33 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,5 +1,13 @@ +import deprecationWarning from './deprecationWarning'; + export childrenValueInputValidation from './childrenValueInputValidation'; export createChainedFunction from './createChainedFunction'; -export CustomPropTypes from './CustomPropTypes'; + +deprecationWarning('utils/domUtils', 'npm install dom-helpers'); export domUtils from './domUtils'; + export ValidComponentChildren from './ValidComponentChildren'; + +deprecationWarning('utils/CustomPropTypes', 'npm install react-prop-types', + 'https://github.com/react-bootstrap/react-bootstrap/issues/937'); +export CustomPropTypes from './CustomPropTypes'; diff --git a/src/utils/overlayPositionUtils.js b/src/utils/overlayPositionUtils.js index d8909ec2ac..de8cfba920 100644 --- a/src/utils/overlayPositionUtils.js +++ b/src/utils/overlayPositionUtils.js @@ -1,109 +1,2 @@ -import domUtils from './domUtils'; -const utils = { - - getContainerDimensions(containerNode) { - let size, scroll; - - if (containerNode.tagName === 'BODY') { - size = { - width: window.innerWidth, - height: window.innerHeight - }; - scroll = - domUtils.ownerDocument(containerNode).documentElement.scrollTop || - containerNode.scrollTop; - } else { - size = domUtils.getSize(containerNode); - scroll = containerNode.scrollTop; - } - - return {...size, scroll}; - }, - - getPosition(target, container) { - const offset = container.tagName === 'BODY' ? - domUtils.getOffset(target) : domUtils.getPosition(target, container); - const size = domUtils.getSize(target); - return {...offset, ...size}; - }, - - calcOverlayPosition(placement, overlayNode, target, container, padding) { - const childOffset = utils.getPosition(target, container); - - const {height: overlayHeight, width: overlayWidth} = domUtils.getSize(overlayNode); - - let positionLeft, positionTop, arrowOffsetLeft, arrowOffsetTop; - - if (placement === 'left' || placement === 'right') { - positionTop = childOffset.top + (childOffset.height - overlayHeight) / 2; - - if (placement === 'left') { - positionLeft = childOffset.left - overlayWidth; - } else { - positionLeft = childOffset.left + childOffset.width; - } - - const topDelta = getTopDelta(positionTop, overlayHeight, container, padding); - - positionTop += topDelta; - arrowOffsetTop = 50 * (1 - 2 * topDelta / overlayHeight) + '%'; - arrowOffsetLeft = null; - - } else if (placement === 'top' || placement === 'bottom') { - positionLeft = childOffset.left + (childOffset.width - overlayWidth) / 2; - - if (placement === 'top') { - positionTop = childOffset.top - overlayHeight; - } else { - positionTop = childOffset.top + childOffset.height; - } - - const leftDelta = getLeftDelta(positionLeft, overlayWidth, container, padding); - positionLeft += leftDelta; - arrowOffsetLeft = 50 * (1 - 2 * leftDelta / overlayWidth) + '%'; - arrowOffsetTop = null; - } else { - throw new Error( - `calcOverlayPosition(): No such placement of "${placement }" found.` - ); - } - - return { positionLeft, positionTop, arrowOffsetLeft, arrowOffsetTop }; - } -}; - - -function getTopDelta(top, overlayHeight, container, padding) { - const containerDimensions = utils.getContainerDimensions(container); - const containerScroll = containerDimensions.scroll; - const containerHeight = containerDimensions.height; - - const topEdgeOffset = top - padding - containerScroll; - const bottomEdgeOffset = top + padding - containerScroll + overlayHeight; - - if (topEdgeOffset < 0) { - return -topEdgeOffset; - } else if (bottomEdgeOffset > containerHeight) { - return containerHeight - bottomEdgeOffset; - } else { - return 0; - } -} - -function getLeftDelta(left, overlayWidth, container, padding) { - const containerDimensions = utils.getContainerDimensions(container); - const containerWidth = containerDimensions.width; - - const leftEdgeOffset = left - padding; - const rightEdgeOffset = left + padding + overlayWidth; - - if (leftEdgeOffset < 0) { - return -leftEdgeOffset; - } else if (rightEdgeOffset > containerWidth) { - return containerWidth - rightEdgeOffset; - } else { - return 0; - } -} -export default utils; +export * from 'react-overlays/lib/utils/overlayPositionUtils'; diff --git a/test/DropdownButtonSpec.js b/test/DropdownButtonSpec.js index 56ac8bd078..00a38d3c7f 100644 --- a/test/DropdownButtonSpec.js +++ b/test/DropdownButtonSpec.js @@ -1,234 +1,181 @@ import React from 'react'; import ReactTestUtils from 'react/lib/ReactTestUtils'; import DropdownButton from '../src/DropdownButton'; -import MenuItem from '../src/MenuItem'; import DropdownMenu from '../src/DropdownMenu'; -import Button from '../src/Button'; +import MenuItem from '../src/MenuItem'; +import { shouldWarn } from './helpers'; -describe('DropdownButton', function () { - let instance; - afterEach(function() { - if (instance && ReactTestUtils.isCompositeComponent(instance) && instance.isMounted()) { - React.unmountComponentAtNode(React.findDOMNode(instance)); - } - }); +describe('DropdownButton', function() { - it('Should render button correctly', function () { - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content - - ); + const simpleDropdown = ( + + Item 1 + Item 2 + Item 3 + Item 4 + + ); + + it('renders title prop', function() { + const instance = ReactTestUtils.renderIntoDocument(simpleDropdown); + const buttonNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON')); - let button = React.findDOMNode(ReactTestUtils.findRenderedComponentWithType(instance, Button)); - assert.ok(React.findDOMNode(instance).className.match(/\bbtn-group\b/)); - assert.ok(React.findDOMNode(instance).className.match(/\btest-class\b/)); - assert.ok(button.className.match(/\bbtn\b/)); - assert.equal(button.nodeName, 'BUTTON'); - assert.equal(button.type, 'button'); - assert.ok(button.className.match(/\bdropdown-toggle\b/)); - assert.ok(button.lastChild.className.match(/\bcaret\b/)); - assert.equal(button.innerText.trim(), 'Title'); + buttonNode.innerText.should.match(/Simple Dropdown/); }); - it('Should render menu correctly', function () { - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content - - ); + it('renders dropdown toggle button', function() { + const instance = ReactTestUtils.renderIntoDocument(simpleDropdown); - let menu = ReactTestUtils.findRenderedComponentWithType(instance, DropdownMenu); - let allMenuItems = ReactTestUtils.scryRenderedComponentsWithType(menu, MenuItem); - assert.equal(allMenuItems.length, 2); + const buttonNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON')); + + buttonNode.tagName.should.equal('BUTTON'); + buttonNode.className.should.match(/\bbtn[ $]/); + buttonNode.className.should.match(/\bbtn-default\b/); + buttonNode.className.should.match(/\bdropdown-toggle\b/); + buttonNode.getAttribute('type').should.equal('button'); + buttonNode.getAttribute('aria-expanded').should.equal('false'); + buttonNode.getAttribute('id').should.be.ok; }); - it('Should pass props to button', function () { - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content + it('renders single MenuItem child', function() { + const instance = ReactTestUtils.renderIntoDocument( + + Item 1 ); - let button = React.findDOMNode(ReactTestUtils.findRenderedComponentWithType(instance, Button)); - assert.ok(button.className.match(/\bbtn-primary\b/)); - assert.equal(button.getAttribute('id'), 'testId'); - assert.ok(button.disabled); - }); - - it('Should be closed by default', function () { - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content - ); + const menuNode = React.findDOMNode( + ReactTestUtils.findRenderedComponentWithType(instance, DropdownMenu)); - assert.notOk(React.findDOMNode(instance).className.match(/\bopen\b/)); + expect(menuNode.children.length).to.equal(1); }); - it('Should open when clicked', function () { - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content + + it('forwards pullRight to menu', function() { + const instance = ReactTestUtils.renderIntoDocument( + + Item 1 ); + const menu = ReactTestUtils.findRenderedComponentWithType(instance, DropdownMenu); - ReactTestUtils.SimulateNative.click(React.findDOMNode(instance.refs.dropdownButton)); - assert.ok(React.findDOMNode(instance).className.match(/\bopen\b/)); + menu.props.pullRight.should.be.true; }); - it('should call onSelect with eventKey when MenuItem is clicked', function (done) { - function handleSelect(eventKey) { - assert.equal(eventKey, '2'); - assert.equal(instance.state.open, false); - done(); - } - - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content + it('renders bsSize', function() { + const instance = ReactTestUtils.renderIntoDocument( + + Item 1 ); + const node = React.findDOMNode(instance); - let menuItems = ReactTestUtils.scryRenderedComponentsWithType(instance, MenuItem); - assert.equal(menuItems.length, 2); - ReactTestUtils.SimulateNative.click( - ReactTestUtils.findRenderedDOMComponentWithTag(menuItems[1], 'a') - ); + node.className.should.match(/\bbtn-group-sm\b/); }); - it('should call MenuItem onSelect with eventKey when MenuItem is clicked', function (done) { - function handleSelect(eventKey) { - assert.equal(eventKey, '2'); - assert.equal(instance.state.open, false); - done(); - } - - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content + it('renders bsStyle', function() { + const instance = ReactTestUtils.renderIntoDocument( + + Item 1 ); + const buttonNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON')); - let menuItems = ReactTestUtils.scryRenderedComponentsWithType(instance, MenuItem); - assert.equal(menuItems.length, 2); - ReactTestUtils.SimulateNative.click( - ReactTestUtils.findRenderedDOMComponentWithTag(menuItems[1], 'a') - ); + buttonNode.className.should.match(/\bbtn-success\b/); }); - it('should not set onSelect to child with no onSelect prop', function () { - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content - - ); - let menuItems = ReactTestUtils.scryRenderedComponentsWithType(instance, MenuItem); - assert.notOk(menuItems[0].props.onSelect); - }); + it('forwards onSelect handler to MenuItems', function(done) { + const selectedEvents = []; - describe('when open', function () { - beforeEach(function () { - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content - - ); + const onSelect = (event, eventKey) => { + selectedEvents.push(eventKey); - instance.setDropdownState(true); - }); + if (selectedEvents.length === 4) { + selectedEvents.should.eql(['1', '2', '3', '4']); + done(); + } + }; + const instance = ReactTestUtils.renderIntoDocument( + + Item 1 + Item 2 + Item 3 + Item 4 + + ); - it('should close on click', function () { - let evt = document.createEvent('HTMLEvents'); - evt.initEvent('click', true, true); - document.documentElement.dispatchEvent(evt); + const menuItems = ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'A'); - assert.notOk(React.findDOMNode(instance).className.match(/\bopen\b/)); + menuItems.forEach(item => { + ReactTestUtils.Simulate.click(item); }); }); - it('Should render li when in nav', function () { - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content + it('closes when child MenuItem is selected', function() { + const instance = ReactTestUtils.renderIntoDocument( + + Item 1 ); + const node = React.findDOMNode(instance); + const buttonNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON')); + + const menuItem = React.findDOMNode( + ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'A')); - let li = React.findDOMNode(instance); - let button = React.findDOMNode(ReactTestUtils.findRenderedComponentWithType(instance, Button)); - assert.equal(li.nodeName, 'LI'); - assert.ok(li.className.match(/\bdropdown\b/)); - assert.ok(li.className.match(/\btest-class\b/)); - assert.equal(button.nodeName, 'A'); - assert.ok(button.className.match(/\bdropdown-toggle\b/)); - assert.ok(button.lastChild.className.match(/\bcaret\b/)); - assert.equal(button.innerText.trim(), 'Title'); + ReactTestUtils.Simulate.click(buttonNode); + node.className.should.match(/\bopen\b/); + ReactTestUtils.Simulate.click(menuItem); + node.className.should.not.match(/\bopen\b/); }); - it('should render a caret by default', function() { - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content - + it('does not close when onToggle is controlled', function() { + const handleSelect = () => {}; + + const instance = ReactTestUtils.renderIntoDocument( + + Item 1 + ); - let button = React.findDOMNode(ReactTestUtils.findRenderedComponentWithType(instance, Button)); - let carets = button.getElementsByClassName('caret'); - assert.equal(carets.length, 1); - }); + const node = React.findDOMNode(instance); + const buttonNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON')); - it('should not render a caret if noCaret prop is given', function() { - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content - - ); + const menuItem = React.findDOMNode( + ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'A')); - let button = React.findDOMNode(ReactTestUtils.findRenderedComponentWithType(instance, Button)); - let carets = button.getElementsByClassName('caret'); - assert.equal(carets.length, 0); + ReactTestUtils.Simulate.click(buttonNode); + node.className.should.match(/\bopen\b/); + ReactTestUtils.Simulate.click(menuItem); + + node.className.should.match(/\bopen\b/); }); - it('should set button class when buttonClassName is given', function() { - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content - - ); + it('warn about the navItem deprecation', function() { + const props = { + title: 'some title', + navItem: true + }; - let button = React.findDOMNode(ReactTestUtils.findRenderedComponentWithType(instance, Button)); - assert.ok(button.className.match(/\btest-class\b/)); + DropdownButton.propTypes.navItem(props, 'navItem', 'DropdownButton'); + shouldWarn(/navItem.*NavDropdown component/); }); - it('should set onClick on Button', function (done) { - function handleClick() { - done(); - } - - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content + it('Should pass props to button', function () { + const instance = ReactTestUtils.renderIntoDocument( + + MenuItem 1 content + MenuItem 2 content ); - let button = ReactTestUtils.findRenderedComponentWithType(instance, Button); - ReactTestUtils.SimulateNative.click( - ReactTestUtils.findRenderedDOMComponentWithClass(button, 'dropdown-toggle') - ); + const buttonNode = React.findDOMNode( + ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON')); + + assert.ok(buttonNode.className.match(/\bbtn-primary\b/)); + assert.equal(buttonNode.getAttribute('id'), 'testId'); + assert.ok(buttonNode.disabled); }); }); diff --git a/test/DropdownMenuSpec.js b/test/DropdownMenuSpec.js index 0dad0af4f0..b3e7e56d7f 100644 --- a/test/DropdownMenuSpec.js +++ b/test/DropdownMenuSpec.js @@ -2,106 +2,189 @@ import React from 'react'; import ReactTestUtils from 'react/lib/ReactTestUtils'; import DropdownMenu from '../src/DropdownMenu'; import MenuItem from '../src/MenuItem'; +import keycode from 'keycode'; + +describe('DropdownMenu', function() { + const simpleMenu = ( + + Item 1 + Item 2 + Item 3 + Item 4 + + ); + + it('renders ul with dropdown-menu class', function() { + const instance = ReactTestUtils.renderIntoDocument(simpleMenu); + const node = React.findDOMNode(instance); + + node.tagName.should.equal('UL'); + node.className.should.match(/\bdropdown-menu\b/); + }); -describe('DropdownMenu', function () { - it('Should render menu correctly', function () { - let Parent = React.createClass({ - render(){ - return ( - - MenuItem 1 content - MenuItem 2 content - - ); - } - }); - - let instance = ReactTestUtils.renderIntoDocument(); + it('has role="menu"', function() { + const instance = ReactTestUtils.renderIntoDocument(simpleMenu); + const node = React.findDOMNode(instance); - let node = React.findDOMNode(instance); + node.getAttribute('role').should.equal('menu'); + }); - assert.ok(node.className.match(/\bdropdown-menu\b/)); - assert.equal(node.nodeName, 'UL'); - assert.equal(node.getAttribute('role'), 'menu'); + it('has aria-labelledby=', function() { + const instance1 = ReactTestUtils.renderIntoDocument(); + const instance2 = ReactTestUtils.renderIntoDocument(); + const node1 = React.findDOMNode(instance1); + const node2 = React.findDOMNode(instance2); - let allMenuItems = ReactTestUtils.scryRenderedComponentsWithType(instance, MenuItem); - assert.equal(allMenuItems.length, 2); - assert.equal(allMenuItems[0], instance.refs.item1); - assert.equal(allMenuItems[1], instance.refs.item2); + node1.getAttribute('aria-labelledby').should.equal('herpa'); + node2.getAttribute('aria-labelledby').should.equal('derpa'); }); - it('Should pass props to dropdown', function () { - let instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content + it('forwards onSelect handler to MenuItems', function(done) { + const selectedEvents = []; + const onSelect = (event, eventKey) => { + selectedEvents.push(eventKey); + + if (selectedEvents.length === 4) { + selectedEvents.should.eql(['1', '2', '3', '4']); + done(); + } + }; + const instance = ReactTestUtils.renderIntoDocument( + + Item 1 + Item 2 + Item 3 + Item 4 ); - let node = React.findDOMNode(instance); - assert.ok(node.className.match(/\bnew-fancy-class\b/)); - }); + const menuItems = ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'A'); - it('should call onSelect with eventKey when MenuItem is clicked', function (done) { - function handleSelect(eventKey) { - assert.equal(eventKey, '2'); - done(); - } + menuItems.forEach(item => { + ReactTestUtils.Simulate.click(item); + }); + }); - let instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content + it('applies pull right', function() { + const instance = ReactTestUtils.renderIntoDocument( + + Item ); + const node = React.findDOMNode(instance); - let menuItems = ReactTestUtils.scryRenderedComponentsWithType(instance, MenuItem); - assert.equal(menuItems.length, 2); - ReactTestUtils.SimulateNative.click( - ReactTestUtils.findRenderedDOMComponentWithTag(menuItems[1], 'a') - ); + node.className.should.match(/\bdropdown-menu-right\b/); }); - it('should call all onSelect handlers when MenuItem is clicked', function (done) { - let i = 0; - function handleSelect(eventKey) { - assert.equal(eventKey, '2'); - i += 1; - if ( i >= 2 ) { - done(); - } - } + describe('focusable state', function() { + let focusableContainer; - let instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content - - ); + beforeEach(function() { + focusableContainer = document.createElement('div'); + document.body.appendChild(focusableContainer); + }); - let menuItems = ReactTestUtils.scryRenderedComponentsWithType(instance, MenuItem); - assert.equal(menuItems.length, 2); - ReactTestUtils.SimulateNative.click( - ReactTestUtils.findRenderedDOMComponentWithTag(menuItems[1], 'a') - ); - }); + afterEach(function() { + React.unmountComponentAtNode(focusableContainer); + document.body.removeChild(focusableContainer); + }); - it('should call not preventDefault with no onSelect handlers when MenuItem is clicked', function (done) { - window.__someGlobalTestCallback = function() { - delete window.__someGlobalTestCallback; - done(); - }; + it('clicking anything outside the menu will request close', function() { + const requestClose = sinon.stub(); + const instance = React.render( +
    + + + Item + +
    , focusableContainer); + + const button = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON').getDOMNode(); + + const evt = document.createEvent('MouseEvent'); + evt.initMouseEvent('click', true, true); + button.dispatchEvent(evt); + + requestClose.should.have.been.calledOnce; + requestClose.getCall(0).args.length.should.equal(0); + }); + + describe('Keyboard Navigation', function() { + it('sets focus on next menu item when the key "down" is pressed', function() { + const instance = React.render(simpleMenu, focusableContainer); + + const items = ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'A'); + items.length.should.equal(4); + items[0].getDOMNode().focus(); + + for (let i = 1; i < items.length; i++) { + ReactTestUtils.Simulate.keyDown(document.activeElement, { keyCode: keycode('down') }); + document.activeElement.should.equal(items[i].getDOMNode()); + } + }); + + it('with last item is focused when the key "down" is pressed first item gains focus', function() { + const instance = React.render(simpleMenu, focusableContainer); + + const items = ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'A'); + items.length.should.equal(4); + items[3].getDOMNode().focus(); + + ReactTestUtils.Simulate.keyDown(document.activeElement, { keyCode: keycode('down') }); + document.activeElement.should.equal(items[0].getDOMNode()); + }); + + it('sets focus on previous menu item when the key "up" is pressed', function() { + const instance = React.render(simpleMenu, focusableContainer); + + const items = ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'A'); + items.length.should.equal(4); + items[3].getDOMNode().focus(); + for (let i = 2; i >= 0; i--) { + ReactTestUtils.Simulate.keyDown(document.activeElement, { keyCode: keycode('up') }); + document.activeElement.should.equal(items[i].getDOMNode()); + } + }); + + it('with first item focused when the key "up" is pressed last item gains focus', function() { + const instance = React.render(simpleMenu, focusableContainer); + + const items = ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'A'); + items.length.should.equal(4); + items[0].getDOMNode().focus(); + + ReactTestUtils.Simulate.keyDown(document.activeElement, { keyCode: keycode('up') }); + document.activeElement.should.equal(items[3].getDOMNode()); + }); + + ['esc', 'tab'].forEach(key => { + it(`when the key "${key}" is pressed the requestClose prop is invoked with the originating event`, function() { + const requestClose = sinon.spy(); + const instance = React.render( + + Item + , focusableContainer); + + const item = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'A').getDOMNode(); + + ReactTestUtils.Simulate.keyDown(item, { keyCode: keycode(key) }); + + requestClose.should.have.been.calledOnce; + requestClose.getCall(0).args[0].keyCode.should.equal(keycode(key)); + }); + }); + }); + }); + + it('Should pass props to dropdown', function () { let instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content + + MenuItem 1 content ); - let menuItems = ReactTestUtils.scryRenderedComponentsWithType(instance, MenuItem); - let evt = document.createEvent('HTMLEvents'); - evt.initEvent('click', true, true); - React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(menuItems[1], 'a')) - .dispatchEvent(evt); + let node = React.findDOMNode(instance); + assert.ok(node.className.match(/\bnew-fancy-class\b/)); }); }); diff --git a/test/DropdownSpec.js b/test/DropdownSpec.js new file mode 100644 index 0000000000..24003ca6f5 --- /dev/null +++ b/test/DropdownSpec.js @@ -0,0 +1,447 @@ +import React from 'react'; +import ReactTestUtils from 'react/lib/ReactTestUtils'; +import Dropdown from '../src/Dropdown'; +import DropdownMenu from '../src/DropdownMenu'; +import MenuItem from '../src/MenuItem'; +import { shouldWarn } from './helpers'; +import keycode from 'keycode'; + +class CustomMenu extends React.Component { + render() { + return ( +
    + {this.props.children} +
    + ); + } +} + +describe('Dropdown', function() { + let BaseDropdown = Dropdown.ControlledComponent; + + const dropdownChildren = [ + + Child Title + , + + Item 1 + Item 2 + Item 3 + Item 4 + + ]; + + const simpleDropdown = ( + + {dropdownChildren} + + ); + + it('renders div with dropdown class', function() { + const instance = ReactTestUtils.renderIntoDocument(simpleDropdown); + const node = React.findDOMNode(instance); + + node.tagName.should.equal('DIV'); + node.className.should.match(/\bdropdown\b/); + node.className.should.not.match(/\bdropup\b/); + }); + + it('renders div with dropup class', function() { + const instance = ReactTestUtils.renderIntoDocument( + + {dropdownChildren} + + ); + const node = React.findDOMNode(instance); + + node.tagName.should.equal('DIV'); + node.className.should.not.match(/\bdropdown\b/); + node.className.should.match(/\bdropup\b/); + }); + + it('renders toggle with Dropdown.Toggle', function() { + const instance = ReactTestUtils.renderIntoDocument( + simpleDropdown + ); + + const buttonNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON')); + + buttonNode.innerText.should.match(/Child Title/); + + buttonNode.tagName.should.equal('BUTTON'); + buttonNode.className.should.match(/\bbtn[ $]/); + buttonNode.className.should.match(/\bbtn-default\b/); + buttonNode.className.should.match(/\bdropdown-toggle\b/); + buttonNode.getAttribute('type').should.equal('button'); + buttonNode.getAttribute('aria-expanded').should.equal('false'); + buttonNode.getAttribute('id').should.be.ok; + }); + + + it('renders dropdown toggle button caret', function() { + const instance = ReactTestUtils.renderIntoDocument(simpleDropdown); + const caretNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'caret')); + + caretNode.tagName.should.equal('SPAN'); + }); + + it('does not render toggle button caret', function() { + const instance = ReactTestUtils.renderIntoDocument( + + Child Text + + ); + const caretNode = ReactTestUtils.scryRenderedDOMComponentsWithClass(instance, 'caret'); + + caretNode.length.should.equal(0); + }); + + it('renders custom menu', function() { + const instance = ReactTestUtils.renderIntoDocument( + + Child Text + + + Item 1 + + + ); + + ReactTestUtils.scryRenderedComponentsWithType(instance, DropdownMenu).length.should.equal(0); + ReactTestUtils.scryRenderedComponentsWithType(instance, CustomMenu).length.should.equal(1); + }); + + it('prop validation with multiple menus', function() { + const props = { + title: 'herpa derpa', + children: [( + Child Text + ), ( + + Item 1 + + ), ( + + Item 1 + + )] + }; + + let err = BaseDropdown.propTypes.children(props, 'children', 'DropdownButton'); + err.message.should.match(/Duplicate children.*bsRole: menu/); + }); + + it('only renders one menu', function() { + const instance = ReactTestUtils.renderIntoDocument( + + Child Text + + + Item 1 + + + Item 1 + + + ); + + ReactTestUtils.scryRenderedComponentsWithType(instance, DropdownMenu).length.should.equal(0); + ReactTestUtils.scryRenderedComponentsWithType(instance, CustomMenu).length.should.equal(1); + + shouldWarn(/Duplicate children.*bsRole: menu/); + }); + + + it('forwards pullRight to menu', function() { + const instance = ReactTestUtils.renderIntoDocument( + + {dropdownChildren} + + ); + const menu = ReactTestUtils.findRenderedComponentWithType(instance, DropdownMenu); + + menu.props.pullRight.should.be.true; + }); + + + // NOTE: The onClick event handler is invoked for both the Enter and Space + // keys as well since the component is a button. I cannot figure out how to + // get ReactTestUtils to simulate such though. + it('toggles open/closed when clicked', function() { + const instance = ReactTestUtils.renderIntoDocument(simpleDropdown); + const node = React.findDOMNode(instance); + const buttonNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON')); + + node.className.should.not.match(/\bopen\b/); + buttonNode.getAttribute('aria-expanded').should.equal('false'); + + ReactTestUtils.Simulate.click(buttonNode); + + node.className.should.match(/\bopen\b/); + buttonNode.getAttribute('aria-expanded').should.equal('true'); + + ReactTestUtils.Simulate.click(buttonNode); + + node.className.should.not.match(/\bopen\b/); + buttonNode.getAttribute('aria-expanded').should.equal('false'); + }); + + it('when focused and closed toggles open when the key "down" is pressed', function() { + const instance = ReactTestUtils.renderIntoDocument(simpleDropdown); + const node = React.findDOMNode(instance); + const buttonNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON')); + + ReactTestUtils.Simulate.keyDown(buttonNode, { keyCode: keycode('down') }); + + node.className.should.match(/\bopen\b/); + buttonNode.getAttribute('aria-expanded').should.equal('true'); + }); + + it('button has aria-haspopup attribute (As per W3C WAI-ARIA Spec)', function() { + const instance = ReactTestUtils.renderIntoDocument(simpleDropdown); + const buttonNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON')); + + buttonNode.getAttribute('aria-haspopup').should.equal('true'); + }); + + + it('closes when child MenuItem is selected', function() { + const instance = ReactTestUtils.renderIntoDocument( + simpleDropdown + ); + + const node = React.findDOMNode(instance); + const buttonNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON')); + + const menuItem = React.findDOMNode( + ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'A')[0]); + + ReactTestUtils.Simulate.click(buttonNode); + node.className.should.match(/\bopen\b/); + ReactTestUtils.Simulate.click(menuItem); + node.className.should.not.match(/\bopen\b/); + }); + + it('does not close when onToggle is controlled', function() { + const handleSelect = () => {}; + + const instance = ReactTestUtils.renderIntoDocument( + + {dropdownChildren} + + ); + + const node = React.findDOMNode(instance); + const buttonNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON')); + + const menuItem = React.findDOMNode( + ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'A')[0]); + + ReactTestUtils.Simulate.click(buttonNode); + node.className.should.match(/\bopen\b/); + ReactTestUtils.Simulate.click(menuItem); + + node.className.should.match(/\bopen\b/); + }); + + it('is open with explicit prop', function() { + class OpenProp extends React.Component { + constructor(props) { + super(props); + + this.state = { + open: false + }; + } + + render () { + return ( +
    + + {}} + title='Prop open control' + id='test-id' + > + {dropdownChildren} + +
    + ); + } + } + + const instance = ReactTestUtils.renderIntoDocument(); + const outerToggle = ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'outer-button'); + const dropdownNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'dropdown')); + + dropdownNode.className.should.not.match(/\bopen\b/); + ReactTestUtils.Simulate.click(outerToggle); + dropdownNode.className.should.match(/\bopen\b/); + ReactTestUtils.Simulate.click(outerToggle); + dropdownNode.className.should.not.match(/\bopen\b/); + }); + + it('has aria-labelledby same id as toggle button', function() { + const instance = ReactTestUtils.renderIntoDocument(simpleDropdown); + const node = React.findDOMNode(instance); + const buttonNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON')); + const menuNode = node.children[1]; + + buttonNode.getAttribute('id').should.equal(menuNode.getAttribute('aria-labelledby')); + }); + + describe('PropType validation', function() { + + describe('children', function() { + + it('menu is exclusive', function() { + + const props = { + children: [ + , + , + + ] + }; + BaseDropdown.propTypes.children(props, 'children', 'Dropdown') + .message.should.match(/Duplicate children.*bsRole: menu/); + }); + + it('menu is required', function() { + + const props = { + children: [ + + ] + }; + + BaseDropdown.propTypes.children(props, 'children', 'Dropdown') + .message.should.match(/Missing a required child.*bsRole: menu/); + }); + + it('toggles are not exclusive', function() { + + const props = { + children: [ + , + , + + ] + }; + + expect(BaseDropdown.propTypes.children(props, 'children', 'Dropdown')) + .to.not.exist; + }); + + it('toggle is required', function() { + + const props = { + children: [ + + ] + }; + + BaseDropdown.propTypes.children(props, 'children', 'Dropdown') + .message.should.match(/Missing a required child.*bsRole: toggle/); + }); + + }); + + }); + + + describe('focusable state', function() { + let focusableContainer; + + beforeEach(function() { + focusableContainer = document.createElement('div'); + document.body.appendChild(focusableContainer); + }); + + afterEach(function() { + React.unmountComponentAtNode(focusableContainer); + document.body.removeChild(focusableContainer); + }); + + it('when focused and closed sets focus on first menu item when the key "down" is pressed', function() { + const instance = React.render(simpleDropdown, focusableContainer); + const buttonNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON')); + + buttonNode.focus(); + + ReactTestUtils.Simulate.keyDown(buttonNode, { keyCode: keycode('down') }); + + const firstMenuItemAnchor = React.findDOMNode( + ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'A')[0]); + + document.activeElement.should.equal(firstMenuItemAnchor); + }); + + + it('when focused and open does not toggle closed when the key "down" is pressed', function() { + const instance = ReactTestUtils.renderIntoDocument(simpleDropdown); + const node = React.findDOMNode(instance); + const buttonNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON')); + + ReactTestUtils.Simulate.click(buttonNode); + ReactTestUtils.Simulate.keyDown(buttonNode, { keyCode: keycode('down') }); + + node.className.should.match(/\bopen\b/); + buttonNode.getAttribute('aria-expanded').should.equal('true'); + }); + + // This test is more complicated then it appears to need. This is + // because there was an intermittent failure of the test when not structured this way + // The failure occured when all tests in the suite were run together, but not a subset of the tests. + // + // I am fairly confident that the failure is due to a test specific conflict and not an actual bug. + it('when open and the key "esc" is pressed the menu is closed and focus is returned to the button', function() { + const instance = React.render( + + {dropdownChildren} + + , focusableContainer); + + const buttonNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON')); + const firstMenuItemAnchor = React.findDOMNode(ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'A')[0]); + + document.activeElement.should.equal(firstMenuItemAnchor); + + ReactTestUtils.Simulate.keyDown(firstMenuItemAnchor, { type: 'keydown', keyCode: keycode('esc') }); + + document.activeElement.should.equal(buttonNode); + }); + + it('when open and the key "tab" is pressed the menu is closed and focus is progress to the next focusable element', done => { + const instance = React.render( +
    + {simpleDropdown} + +
    , focusableContainer); + + const node = ReactTestUtils.findRenderedComponentWithType(instance, Dropdown); + + const buttonNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(node, 'BUTTON')); + + ReactTestUtils.Simulate.click(buttonNode); + buttonNode.getAttribute('aria-expanded').should.equal('true'); + + ReactTestUtils.Simulate.keyDown(buttonNode, { key: keycode('tab'), keyCode: keycode('tab') }); + + setTimeout(() => { + buttonNode.getAttribute('aria-expanded').should.equal('false'); + done(); + }); + + + // simulating a tab event doesn't actually shift focus. + // at least that seems to be the case according to SO. + // hence no assert on the input having focus. + }); + }); + +}); diff --git a/test/DropdownToggleSpec.js b/test/DropdownToggleSpec.js new file mode 100644 index 0000000000..ed80410d63 --- /dev/null +++ b/test/DropdownToggleSpec.js @@ -0,0 +1,94 @@ +import React from 'react'; +import ReactTestUtils from 'react/lib/ReactTestUtils'; +import DropdownToggle from '../src/DropdownToggle'; + +describe('DropdownToggle', function() { + const simpleToggle = ; + + it('renders toggle button', function() { + const instance = ReactTestUtils.renderIntoDocument(simpleToggle); + const buttonNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON')); + + buttonNode.className.should.match(/\bbtn[ $]/); + buttonNode.className.should.match(/\bbtn-default\b/); + buttonNode.className.should.match(/\bdropdown-toggle\b/); + buttonNode.getAttribute('aria-expanded').should.equal('false'); + }); + + it('renders title prop', function() { + const instance = ReactTestUtils.renderIntoDocument(simpleToggle); + const buttonNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON')); + + buttonNode.innerText.should.match(/herpa derpa/); + }); + + it('renders title children', function() { + const instance = ReactTestUtils.renderIntoDocument( + +

    herpa derpa

    +
    + ); + const button = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON'); + const h3Node = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(button, 'H3')); + + h3Node.innerText.should.match(/herpa derpa/); + }); + + it('renders dropdown toggle button caret', function() { + const instance = ReactTestUtils.renderIntoDocument(simpleToggle); + const caretNode = ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'caret').getDOMNode(); + + caretNode.tagName.should.equal('SPAN'); + }); + + it('does not render toggle button caret', function() { + const instance = ReactTestUtils.renderIntoDocument( + + ); + const caretNode = ReactTestUtils.scryRenderedDOMComponentsWithClass(instance, 'caret'); + + caretNode.length.should.equal(0); + }); + + it('forwards onClick handler', function(done) { + const handleClick = (event) => { + event.should.be.ok; + done(); + }; + const instance = ReactTestUtils.renderIntoDocument( + + ); + const button = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON'); + + ReactTestUtils.Simulate.click(button); + }); + + it('forwards id', function() { + const id = 'testid'; + const instance = ReactTestUtils.renderIntoDocument( + + ); + const button = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON')); + + button.getAttribute('id').should.equal(id); + }); + + it('forwards bsStyle', function() { + const style = 'success'; + const instance = ReactTestUtils.renderIntoDocument( + + ); + const button = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON')); + + button.className.should.match(/\bbtn-success\b/); + }); + + it('forwards bsSize', function() { + const instance = ReactTestUtils.renderIntoDocument( + + ); + const button = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'BUTTON')); + + button.className.should.match(/\bbtn-sm\b/); + }); +}); diff --git a/test/FactoriesSpec.js b/test/FactoriesSpec.js deleted file mode 100644 index 040d636430..0000000000 --- a/test/FactoriesSpec.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import components from '../tools/public-components'; - -let props = { - ButtonInput: {value: 'button'}, - Glyphicon: {glyph: 'star'}, - Modal: {onHide() {}}, - ModalTrigger: {modal: React.DOM.div(null)}, - OverlayTrigger: {overlay: React.DOM.div(null)} -}; - -function createTest(component) { - let factory = require(`../lib/factories/${component}`); - describe('factories', function () { - it(`Should have a ${component} factory`, function () { - assert.ok(React.isValidElement(factory(props[component]))); - }); - }); -} - -components.map(component => createTest(component)); diff --git a/test/InputSpec.js b/test/InputSpec.js index 340e50df44..b7216de4ec 100644 --- a/test/InputSpec.js +++ b/test/InputSpec.js @@ -144,8 +144,8 @@ describe('Input', function () { it('renders btn-group with dropdown', function() { let instance = ReactTestUtils.renderIntoDocument( - - One + + One
    } /> ); diff --git a/test/MenuItemSpec.js b/test/MenuItemSpec.js index 906454bef4..f1c5c0f223 100644 --- a/test/MenuItemSpec.js +++ b/test/MenuItemSpec.js @@ -1,86 +1,147 @@ import React from 'react'; import ReactTestUtils from 'react/lib/ReactTestUtils'; import MenuItem from '../src/MenuItem'; +import { shouldWarn } from './helpers'; -describe('MenuItem', function () { - it('should output an li', function () { - let instance = ReactTestUtils.renderIntoDocument( - - Title +describe('MenuItem', function() { + it('renders divider', function() { + const instance = ReactTestUtils.renderIntoDocument(); + const node = React.findDOMNode(instance); + + node.className.should.match(/\bdivider\b/); + node.getAttribute('role').should.equal('separator'); + }); + + it('renders divider not children', function() { + const instance = ReactTestUtils.renderIntoDocument( + + Some child ); - assert.equal(React.findDOMNode(instance).nodeName, 'LI'); - assert.equal(React.findDOMNode(instance).getAttribute('role'), 'presentation'); + const node = React.findDOMNode(instance); + + node.className.should.match(/\bdivider\b/); + node.innerHTML.should.not.match(/Some child/); + shouldWarn('Children will not be rendered for dividers'); }); - it('should pass through props', function () { - let instance = ReactTestUtils.renderIntoDocument( + it('renders header', function() { + const instance = ReactTestUtils.renderIntoDocument(Header Text); + const node = React.findDOMNode(instance); + + node.className.should.match(/\bdropdown-header\b/); + node.getAttribute('role').should.equal('heading'); + node.innerHTML.should.match(/Header Text/); + }); + + it('renders menu item link', function(done) { + const instance = ReactTestUtils.renderIntoDocument( - Title + onKeyDown={() => done()} + href='/herpa-derpa'> + Item ); + const node = React.findDOMNode(instance); + const anchor = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'A').getDOMNode(); - let node = React.findDOMNode(instance); - assert(node.className.match(/\btest-class\b/)); - assert.equal(node.getAttribute('href'), null); - assert.equal(node.getAttribute('title'), null); - assert.ok(node.className.match(/\bactive\b/)); + node.getAttribute('role').should.equal('presentation'); + anchor.getAttribute('role').should.equal('menuitem'); + anchor.getAttribute('tabIndex').should.equal('-1'); + anchor.getAttribute('href').should.equal('/herpa-derpa'); - let anchorNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a')); - assert.notOk(anchorNode.className.match(/\btest-class\b/)); - assert.equal(anchorNode.getAttribute('href'), '#hi-mom!'); - assert.equal(anchorNode.getAttribute('title'), 'hi mom!'); + anchor.innerHTML.should.match(/Item/); + + ReactTestUtils.Simulate.keyDown(anchor, { keyCode: 1 }); }); - it('should have an anchor', function () { - let instance = ReactTestUtils.renderIntoDocument( - - Title - + it('click handling with onSelect prop', function() { + const handleSelect = (event, eventKey) => { + eventKey.should.equal('1'); + }; + const instance = ReactTestUtils.renderIntoDocument( + Item ); + const anchor = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'A'); - let anchor = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a'); - assert.equal(React.findDOMNode(anchor).getAttribute('tabIndex'), '-1'); + ReactTestUtils.Simulate.click(anchor); }); - it('should fire callback on click of link', function (done) { - let selectOp = function (selectedKey) { - assert.equal(selectedKey, '1'); - done(); + it('click handling with onSelect prop (no eventKey)', function() { + const handleSelect = (event, eventKey) => { + expect(eventKey).to.be.undefined; }; - let instance = ReactTestUtils.renderIntoDocument( - - Title - + const instance = ReactTestUtils.renderIntoDocument( + Item ); - let anchor = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a'); + const anchor = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'A'); + ReactTestUtils.Simulate.click(anchor); }); - it('should be a divider with no children', function () { - let instance = ReactTestUtils.renderIntoDocument( - - Title - + it('does not fire onSelect when divider is clicked', function() { + const handleSelect = (event, selectedEvent) => { + throw new Error('Should not invoke onSelect with divider flag applied'); + }; + const instance = ReactTestUtils.renderIntoDocument( + ); + ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'A').length.should.equal(0); + const li = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'li'); - assert(React.findDOMNode(instance).className.match(/\bdivider\b/), 'Has no divider class'); - assert.equal(React.findDOMNode(instance).innerText, ''); + ReactTestUtils.Simulate.click(li); }); - it('should be a header with no anchor', function () { + it('does not fire onSelect when header is clicked', function() { + const handleSelect = (event, selectedEvent) => { + throw new Error('Should not invoke onSelect with divider flag applied'); + }; + const instance = ReactTestUtils.renderIntoDocument( + Header content + ); + ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'A').length.should.equal(0); + const li = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'li'); + + ReactTestUtils.Simulate.click(li); + }); + + it('disabled link', function() { + const handleSelect = (event, selectEvent) => { + throw new Error('Should not invoke onSelect event'); + }; + const instance = ReactTestUtils.renderIntoDocument( + Text + ); + const node = React.findDOMNode(instance); + const anchor = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'A'); + + node.className.should.match(/\bdisabled\b/); + + ReactTestUtils.Simulate.click(anchor); + }); + + it('should pass through props', function () { let instance = ReactTestUtils.renderIntoDocument( - + Title ); - assert(React.findDOMNode(instance).className.match(/\bdropdown-header\b/), 'Has no header class'); - assert.equal(React.findDOMNode(instance).innerHTML, 'Title'); + let node = React.findDOMNode(instance); + + assert(node.className.match(/\btest-class\b/)); + assert.equal(node.getAttribute('href'), null); + assert.equal(node.getAttribute('title'), null); + + let anchorNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a')); + + assert.notOk(anchorNode.className.match(/\btest-class\b/)); + assert.equal(anchorNode.getAttribute('href'), '#hi-mom!'); + assert.equal(anchorNode.getAttribute('title'), 'hi mom!'); }); it('Should set target attribute on anchor', function () { @@ -94,26 +155,14 @@ describe('MenuItem', function () { assert.equal(React.findDOMNode(anchor).getAttribute('target'), '_blank'); }); - it('Should call `onSelect` with target attribute', function (done) { - function handleSelect(key, href, target) { - assert.equal(href, 'link'); - assert.equal(target, '_blank'); - done(); - } + it('should output an li', function () { let instance = ReactTestUtils.renderIntoDocument( - + Title ); - ReactTestUtils.Simulate.click(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a')); + assert.equal(React.findDOMNode(instance).nodeName, 'LI'); + assert.equal(React.findDOMNode(instance).getAttribute('role'), 'presentation'); }); - it('Should be `disabled` link', function () { - let instance = ReactTestUtils.renderIntoDocument( - - Title - - ); - assert.ok(React.findDOMNode(instance).className.match(/\bdisabled\b/)); - }); }); diff --git a/test/NavDropdownSpec.js b/test/NavDropdownSpec.js new file mode 100644 index 0000000000..e70ec453cc --- /dev/null +++ b/test/NavDropdownSpec.js @@ -0,0 +1,65 @@ +import React from 'react'; +import ReactTestUtils from 'react/lib/ReactTestUtils'; +import NavDropdown from '../src/NavDropdown'; +import MenuItem from '../src/MenuItem'; + +describe('NavDropdown', function() { + + it('Should render li when in nav', function () { + const instance = ReactTestUtils.renderIntoDocument( + + MenuItem 1 content + MenuItem 2 content + + ); + + let li = React.findDOMNode(instance); + let button = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'dropdown-toggle')); + + assert.equal(li.nodeName, 'LI'); + assert.ok(li.className.match(/\bdropdown\b/)); + assert.ok(li.className.match(/\btest-class\b/)); + assert.equal(button.nodeName, 'A'); + assert.equal(button.innerText.trim(), 'Title'); + }); + + it('is open with explicit prop', function() { + class OpenProp extends React.Component { + constructor(props) { + super(props); + + this.state = { + open: false + }; + } + + render () { + return ( +
    + + {}} + title='Prop open control' + id='test-id'> + Item 1 + +
    + ); + } + } + + const instance = ReactTestUtils.renderIntoDocument(); + const outerToggle = ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'outer-button'); + const dropdownNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'dropdown')); + + dropdownNode.className.should.not.match(/\bopen\b/); + ReactTestUtils.Simulate.click(outerToggle); + dropdownNode.className.should.match(/\bopen\b/); + ReactTestUtils.Simulate.click(outerToggle); + dropdownNode.className.should.not.match(/\bopen\b/); + }); +}); diff --git a/test/OverlayDeprecationSpec.js b/test/OverlayDeprecationSpec.js new file mode 100644 index 0000000000..c8eb2f03f7 --- /dev/null +++ b/test/OverlayDeprecationSpec.js @@ -0,0 +1,28 @@ +import React from 'react'; +import ReactTestUtils from 'react/lib/ReactTestUtils'; +import Position from '../src/Position'; +import Transition from '../src/Transition'; +import Portal from '../src/Portal'; + +import { shouldWarn } from './helpers'; + +describe('Components moved to react-overlays', ()=>{ + + it('should warn about Position', ()=>{ + ReactTestUtils.renderIntoDocument(
    ); + + shouldWarn(/Position component is deprecated/); + }); + + it('should warn about Transition', ()=>{ + ReactTestUtils.renderIntoDocument(
    ); + + shouldWarn(/Transition component is deprecated/); + }); + + it('should warn about Portal', ()=>{ + ReactTestUtils.renderIntoDocument(
    ); + + shouldWarn(/Portal component is deprecated/); + }); +}); diff --git a/test/OverlayTriggerSpec.js b/test/OverlayTriggerSpec.js index 78b63502b4..cb48067872 100644 --- a/test/OverlayTriggerSpec.js +++ b/test/OverlayTriggerSpec.js @@ -82,7 +82,6 @@ describe('OverlayTrigger', function() { ); overlayTrigger = React.findDOMNode(instance); - ReactTestUtils.Simulate.click(overlayTrigger); }); diff --git a/test/PanelGroupSpec.js b/test/PanelGroupSpec.js index 43f8cbdae9..fc65496379 100644 --- a/test/PanelGroupSpec.js +++ b/test/PanelGroupSpec.js @@ -47,4 +47,58 @@ describe('PanelGroup', function () { assert.notOk(panel.state.collapsing); }); + + describe('Web Accessibility', function() { + let instance, panelBodies, panelGroup, links; + + beforeEach(function() { + instance = ReactTestUtils.renderIntoDocument( + + Panel 1 + Panel 2 + + ); + let accordion = ReactTestUtils.findRenderedComponentWithType(instance, PanelGroup); + panelGroup = ReactTestUtils.findRenderedDOMComponentWithClass(accordion, 'panel-group'); + panelBodies = ReactTestUtils.scryRenderedDOMComponentsWithClass(panelGroup, 'panel-collapse'); + links = ReactTestUtils.scryRenderedDOMComponentsWithClass(panelGroup, 'panel-heading') + .map(function(header) { + return ReactTestUtils.findRenderedDOMComponentWithTag(header, 'a'); + }); + }); + + it('Should have a role of tablist', function() { + assert.equal(panelGroup.props.role, 'tablist'); + }); + + it('Should provide each header tab with role of tab', function() { + assert.equal(links[0].props.role, 'tab'); + assert.equal(links[1].props.role, 'tab'); + }); + + it('Should provide the panelBodies with role of tabpanel', function() { + assert.equal(panelBodies[0].props.role, 'tabpanel'); + }); + + it('Should provide each panel with an aria-labelledby referencing the corresponding header', function() { + assert.equal(panelBodies[0].props.id, links[0].props['aria-controls']); + assert.equal(panelBodies[1].props.id, links[1].props['aria-controls']); + }); + + it('Should maintain each tab aria-selected state', function() { + assert.equal(links[0].props['aria-selected'], true); + assert.equal(links[1].props['aria-selected'], false); + }); + + it('Should maintain each tab aria-hidden state', function() { + assert.equal(panelBodies[0].props['aria-hidden'], false); + assert.equal(panelBodies[1].props['aria-hidden'], true); + }); + + afterEach(function() { + if (instance && ReactTestUtils.isCompositeComponent(instance) && instance.isMounted()) { + React.unmountComponentAtNode(React.findDOMNode(instance)); + } + }); + }); }); diff --git a/test/PortalSpec.js b/test/PortalSpec.js deleted file mode 100644 index 66a1b49fc0..0000000000 --- a/test/PortalSpec.js +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import ReactTestUtils from 'react/lib/ReactTestUtils'; -import Portal from '../src/Portal'; - -describe('Portal', function () { - let instance; - - let Overlay = React.createClass({ - render() { - return ( -
    - {this.props.overlay} -
    - ); - }, - getOverlayDOMNode(){ - return this.refs.p.getOverlayDOMNode(); - } - }); - - afterEach(function() { - if (instance && ReactTestUtils.isCompositeComponent(instance) && instance.isMounted()) { - React.unmountComponentAtNode(React.findDOMNode(instance)); - } - }); - - it('Should render overlay into container (DOMNode)', function() { - let container = document.createElement('div'); - - instance = ReactTestUtils.renderIntoDocument( - } /> - ); - - assert.equal(container.querySelectorAll('#test1').length, 1); - }); - - it('Should render overlay into container (ReactComponent)', function() { - let Container = React.createClass({ - render() { - return } />; - } - }); - - instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.equal(React.findDOMNode(instance).querySelectorAll('#test1').length, 1); - }); - - it('Should not render a null overlay', function() { - let Container = React.createClass({ - render() { - return ; - } - }); - - instance = ReactTestUtils.renderIntoDocument( - - ); - - assert.equal(instance.refs.overlay.getOverlayDOMNode(), null); - }); - - it('Should render only an overlay', function() { - let OnlyOverlay = React.createClass({ - render() { - return {this.props.overlay}; - } - }); - - let overlayInstance = ReactTestUtils.renderIntoDocument( - } /> - ); - - assert.equal(overlayInstance.refs.p.getOverlayDOMNode().nodeName, 'DIV'); - }); -}); diff --git a/test/PositionSpec.js b/test/PositionSpec.js deleted file mode 100644 index ea1405fd14..0000000000 --- a/test/PositionSpec.js +++ /dev/null @@ -1,218 +0,0 @@ -import pick from 'lodash/object/pick'; -import React from 'react'; -import ReactTestUtils from 'react/lib/ReactTestUtils'; - -import Position from '../src/Position'; -import overlayPositionUtils from '../src/utils/overlayPositionUtils'; - -import {render} from './helpers'; - -describe('Position', function () { - it('Should output a child', function () { - let instance = ReactTestUtils.renderIntoDocument( - - Text - - ); - assert.equal(React.findDOMNode(instance).nodeName, 'SPAN'); - }); - - it('Should warn about several children', function () { - expect(() => { - ReactTestUtils.renderIntoDocument( - - Text - Another Text - - ); - }).to.throw(Error, /onlyChild must be passed a children with exactly one child/); - }); - - describe('position recalculation', function () { - beforeEach(function () { - sinon.spy(overlayPositionUtils, 'calcOverlayPosition'); - sinon.spy(Position.prototype, 'componentWillReceiveProps'); - }); - - afterEach(function () { - overlayPositionUtils.calcOverlayPosition.restore(); - Position.prototype.componentWillReceiveProps.restore(); - }); - - it('Should only recalculate when target changes', function () { - class TargetChanger extends React.Component { - constructor(props) { - super(props); - - this.state = { - target: 'foo', - fakeProp: 0 - }; - } - - render() { - return ( -
    -
    -
    - - this.refs[this.state.target]} - fakeProp={this.state.fakeProp} - > -
    - -
    - ); - } - } - - const instance = ReactTestUtils.renderIntoDocument(); - - // Position calculates initial position. - expect(Position.prototype.componentWillReceiveProps) - .to.have.not.been.called; - expect(overlayPositionUtils.calcOverlayPosition) - .to.have.been.calledOnce; - - instance.setState({target: 'bar'}); - - // Position receives new props and recalculates position. - expect(Position.prototype.componentWillReceiveProps) - .to.have.been.calledOnce; - expect(overlayPositionUtils.calcOverlayPosition) - .to.have.been.calledTwice; - - instance.setState({fakeProp: 1}); - - // Position receives new props but should not recalculate position. - expect(Position.prototype.componentWillReceiveProps) - .to.have.been.calledTwice; - expect(overlayPositionUtils.calcOverlayPosition) - .to.have.been.calledTwice; - }); - }); - - describe('position calculation', function () { - let mountPoint; - - beforeEach(function () { - mountPoint = document.createElement('div'); - document.body.appendChild(mountPoint); - }); - - afterEach(function () { - React.unmountComponentAtNode(mountPoint); - document.body.removeChild(mountPoint); - }); - - function checkPosition(placement, targetPosition, expected) { - class FakeOverlay extends React.Component { - render() { - return ( -
    - ); - } - } - - class FakeContainer extends React.Component { - render() { - return ( -
    -
    - - React.findDOMNode(this.refs.target)} - container={this} - containerPadding={50} - placement={placement} - > - - -
    - ); - } - } - - const expectedPosition = { - positionLeft: expected[0], - positionTop: expected[1], - arrowOffsetLeft: expected[2], - arrowOffsetTop: expected[3] - }; - - it('Should calculate the correct position', function() { - const instance = render(, mountPoint); - - const calculatedPosition = pick( - instance.refs.overlay.props, Object.keys(expectedPosition) - ); - expect(calculatedPosition).to.eql(expectedPosition); - }); - } - - [ - { - placement: 'left', - noOffset: [50, 200, null, '50%'], - offsetBefore: [-200, 50, null, '0%'], - offsetAfter: [300, 350, null, '100%'] - }, - { - placement: 'top', - noOffset: [200, 50, '50%', null], - offsetBefore: [50, -200, '0%', null], - offsetAfter: [350, 300, '100%', null] - }, - { - placement: 'bottom', - noOffset: [200, 350, '50%', null], - offsetBefore: [50, 100, '0%', null], - offsetAfter: [350, 600, '100%', null] - }, - { - placement: 'right', - noOffset: [350, 200, null, '50%'], - offsetBefore: [100, 50, null, '0%'], - offsetAfter: [600, 350, null, '100%'] - } - ].forEach(function(testCase) { - const placement = testCase.placement; - - describe(`placement = ${placement}`, function() { - describe('no viewport offset', function() { - checkPosition( - placement, {left: 250, top: 250}, testCase.noOffset - ); - }); - - describe('viewport offset before', function() { - checkPosition( - placement, {left: 0, top: 0}, testCase.offsetBefore - ); - }); - - describe('viewport offset after', function() { - checkPosition( - placement, {left: 500, top: 500}, testCase.offsetAfter - ); - }); - }); - }); - }); - - // ToDo: add remaining tests -}); diff --git a/test/SplitButtonSpec.js b/test/SplitButtonSpec.js index c5b152bfa5..c16f0de782 100644 --- a/test/SplitButtonSpec.js +++ b/test/SplitButtonSpec.js @@ -4,216 +4,102 @@ import SplitButton from '../src/SplitButton'; import MenuItem from '../src/MenuItem'; import Button from '../src/Button'; -describe('SplitButton', function () { - let instance; - afterEach(function() { - if (instance && ReactTestUtils.isCompositeComponent(instance) && instance.isMounted()) { - React.unmountComponentAtNode(React.findDOMNode(instance)); - } +describe('SplitButton', function() { + const simple = ( + + Item 1 + Item 2 + Item 3 + Item 4 + + ); + + it('should open the menu when dropdown button is clicked', function () { + const instance = ReactTestUtils.renderIntoDocument(simple); + + const toggleNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'dropdown-toggle')); + const splitButtonNode = React.findDOMNode(instance); + + splitButtonNode.className.should.not.match(/open/); + ReactTestUtils.Simulate.click(toggleNode); + splitButtonNode.className.should.match(/open/); }); - it('Should render button correctly', function () { - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content - - ); - - let button = React.findDOMNode(instance.refs.button); - let dropdownButton = React.findDOMNode(instance.refs.dropdownButton); - assert.ok(React.findDOMNode(instance).className.match(/\bbtn-group\b/)); - assert.ok(button.className.match(/\bbtn\b/)); - assert.equal(button.nodeName, 'BUTTON'); - assert.equal(button.type, 'button'); - assert.ok(dropdownButton.className.match(/\bdropdown-toggle\b/)); - assert.equal(button.innerText.trim(), 'Title'); - assert.ok(dropdownButton.childNodes[0].className.match(/\bsr-only\b/)); - assert.equal(dropdownButton.childNodes[0].innerText.trim(), 'Toggle dropdown'); - assert.ok(dropdownButton.childNodes[1].className.match(/\bcaret\b/)); - assert.equal(dropdownButton.childNodes[2].style.letterSpacing, '-0.3em'); - assert.equal(dropdownButton.childNodes.length, 3); - }); - - it('Should render menu correctly', function () { - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content - - ); - - let menu = React.findDOMNode(instance.refs.menu); - assert.ok(menu.className.match(/\bdropdown-menu\b/)); - assert.equal(menu.getAttribute('role'), 'menu'); - assert.equal(menu.firstChild.nodeName, 'LI'); - assert.equal(menu.firstChild.innerText, 'MenuItem 1 content'); - assert.equal(menu.lastChild.nodeName, 'LI'); - assert.equal(menu.lastChild.innerText, 'MenuItem 2 content'); - }); - - it('Should pass dropdownTitle to dropdown button', function () { - let CustomTitle = React.createClass({ render() { return ; } }); - instance = ReactTestUtils.renderIntoDocument( - } dropdownTitle={}> - MenuItem 1 content - MenuItem 2 content - - ); - - assert.ok(ReactTestUtils.findRenderedComponentWithType(instance.refs.button, CustomTitle)); - assert.ok(ReactTestUtils.findRenderedComponentWithType(instance.refs.dropdownButton, CustomTitle)); - }); + it('should not open the menu when other button is clicked', function() { + const instance = ReactTestUtils.renderIntoDocument(simple); - it('Should pass props to button', function () { - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content - - ); - - let button = React.findDOMNode(instance.refs.button); - assert.ok(button.className.match(/\bbtn-primary\b/)); - }); - - it('Should pass disabled to both buttons', function() { - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content - - ); + const buttonNode = React.findDOMNode(ReactTestUtils.scryRenderedComponentsWithType(instance, Button)[0]); + const splitButtonNode = React.findDOMNode(instance); - let button = React.findDOMNode(instance.refs.button); - assert.ok(button.disabled); - let dropdownButton = React.findDOMNode(instance.refs.dropdownButton); - assert.ok(dropdownButton.disabled); + splitButtonNode.className.should.not.match(/open/); + ReactTestUtils.Simulate.click(buttonNode); + splitButtonNode.className.should.not.match(/open/); }); - it('Should pass id to button group', function () { - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content + it('should invoke onClick when SplitButton.Button is clicked (prop)', function(done) { + const instance = ReactTestUtils.renderIntoDocument( + done() }> + Item 1 ); - assert.equal(React.findDOMNode(instance).getAttribute('id'), 'testId'); + const buttonNode = React.findDOMNode(ReactTestUtils.scryRenderedComponentsWithType(instance, Button)[0]); + ReactTestUtils.Simulate.click(buttonNode); }); - it('Should be closed by default', function () { - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content - - ); - assert.notOk(React.findDOMNode(instance).className.match(/\bopen\b/)); - }); + it('should not invoke onClick when SplitButton.Toggle is clicked (prop)', function(done) { + let onClickSpy = sinon.spy(); - it('Should open when clicked', function () { - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content + const instance = ReactTestUtils.renderIntoDocument( + + Item 1 ); - ReactTestUtils.SimulateNative.click(React.findDOMNode(instance.refs.dropdownButton)); + const toggleNode = React.findDOMNode( + ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'dropdown-toggle')); - assert.ok(React.findDOMNode(instance).className.match(/\bopen\b/)); - }); + ReactTestUtils.Simulate.click(toggleNode); - it('should call onSelect with eventKey when MenuItem is clicked', function (done) { - function handleSelect(eventKey) { - assert.equal(eventKey, '2'); + setTimeout(()=> { + onClickSpy.should.not.have.been.called; done(); - } - - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content - - ); - - let menuItems = ReactTestUtils.scryRenderedComponentsWithType(instance, MenuItem); - assert.equal(menuItems.length, 2); - ReactTestUtils.SimulateNative.click( - ReactTestUtils.findRenderedDOMComponentWithTag(menuItems[1], 'a') - ); + }, 10); }); - it('Should have dropup class', function () { - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content + it('Should pass disabled to both buttons', function () { + const instance = ReactTestUtils.renderIntoDocument( + + Item 1 ); - assert.ok(React.findDOMNode(instance).className.match(/\bdropup\b/)); - }); + const toggleNode = React.findDOMNode( + ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'dropdown-toggle')); - it('Should pass pullRight prop to menu', function () { - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content - - ); + const buttonNode = React.findDOMNode( + ReactTestUtils.scryRenderedComponentsWithType(instance, Button)[0]); - assert.ok(instance.refs.menu.props.pullRight); + expect(toggleNode.disabled).to.be.true; + expect(buttonNode.disabled).to.be.true; }); it('Should set target attribute on anchor', function () { - instance = ReactTestUtils.renderIntoDocument( - + const instance = ReactTestUtils.renderIntoDocument( + MenuItem 1 content ); let anchors = ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'a'); - assert.equal(anchors.length, 2); let linkElement = React.findDOMNode(anchors[0]); - assert.equal(linkElement.target, '_blank'); - }); - it('Should call `onClick` with target attribute', function (done) { - function handleClick(key, href, target) { - assert.equal(target, '_blank'); - done(); - } - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - - ); - - let buttons = ReactTestUtils.scryRenderedComponentsWithType(instance, Button); - ReactTestUtils.Simulate.click(ReactTestUtils.findRenderedDOMComponentWithTag(buttons[0], 'a')); + assert.equal(linkElement.target, '_blank'); }); - describe('when open', function () { - beforeEach(function () { - instance = ReactTestUtils.renderIntoDocument( - - MenuItem 1 content - MenuItem 2 content - - ); - - instance.setDropdownState(true); - }); - - it('should close when button is clicked', function () { - let evt = document.createEvent('HTMLEvents'); - evt.initEvent('click', true, true); - document.documentElement.dispatchEvent(evt); - - assert.notOk(React.findDOMNode(instance).className.match(/\bopen\b/)); - }); - }); }); diff --git a/test/TabPaneSpec.js b/test/TabSpec.js similarity index 78% rename from test/TabPaneSpec.js rename to test/TabSpec.js index ecc704da91..6babd9db32 100644 --- a/test/TabPaneSpec.js +++ b/test/TabSpec.js @@ -1,18 +1,18 @@ import React from 'react'; import ReactTestUtils from 'react/lib/ReactTestUtils'; -import TabPane from '../src/TabPane'; +import Tab from '../src/Tab'; -describe('TabPane', function () { +describe('Tab', function () { it('Should have class', function () { let instance = ReactTestUtils.renderIntoDocument( - Item content + Item content ); assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'tab-pane')); }); it('Should add active class', function () { let instance = ReactTestUtils.renderIntoDocument( - Item content + Item content ); assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'active')); }); @@ -21,7 +21,7 @@ describe('TabPane', function () { it('Should have aria-hidden', function () { let instance = ReactTestUtils.renderIntoDocument( - Item content + Item content ); assert.equal(React.findDOMNode(instance).getAttribute('aria-hidden'), 'false'); @@ -29,7 +29,7 @@ describe('TabPane', function () { it('Should have role', function () { let instance = ReactTestUtils.renderIntoDocument( - Item content + Item content ); assert.equal(React.findDOMNode(instance).getAttribute('role'), 'tabpanel'); diff --git a/test/TabbedAreaSpec.js b/test/TabbedAreaSpec.js deleted file mode 100644 index 876b09dcb8..0000000000 --- a/test/TabbedAreaSpec.js +++ /dev/null @@ -1,321 +0,0 @@ -import React from 'react'; -import ReactTestUtils from 'react/lib/ReactTestUtils'; -import TabbedArea from '../src/TabbedArea'; -import NavItem from '../src/NavItem'; -import TabPane from '../src/TabPane'; -import ValidComponentChildren from '../src/utils/ValidComponentChildren'; -import { render } from './helpers'; - -describe('TabbedArea', function () { - it('Should show the correct tab', function () { - let instance = ReactTestUtils.renderIntoDocument( - - Tab 1 content - Tab 2 content - - ); - - let panes = ReactTestUtils.scryRenderedComponentsWithType(instance, TabPane); - - assert.equal(panes[0].props.active, true); - assert.equal(panes[1].props.active, false); - - let tabbedArea = ReactTestUtils.findRenderedComponentWithType(instance, TabbedArea); - - assert.equal(tabbedArea.refs.tabs.props.activeKey, 1); - }); - - it('Should only show the tabs with `TabPane.props.tab` set', function () { - let instance = ReactTestUtils.renderIntoDocument( - - Tab 1 content - Tab 2 content - Tab 3 content - - ); - - let tabbedArea = ReactTestUtils.findRenderedComponentWithType(instance, TabbedArea); - - assert.equal(ValidComponentChildren.numberOf(instance.refs.tabs.props.children), 2); - assert.equal(tabbedArea.refs.tabs.props.activeKey, 3); - }); - - it('Should allow tab to have React components', function () { - let tabTitle = ( - Tab 2 - ); - let instance = ReactTestUtils.renderIntoDocument( - - Tab 1 content - Tab 2 content - - ); - - assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance.refs.tabs, 'special-tab')); - }); - - it('Should call onSelect when tab is selected', function (done) { - function onSelect(key) { - assert.equal(key, '2'); - done(); - } - - let tab2 = Tab2; - let instance = ReactTestUtils.renderIntoDocument( - - Tab 1 content - Tab 2 content - - ); - - - ReactTestUtils.Simulate.click( - ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'tab2') - ); - }); - - it('Should have children with the correct DOM properties', function () { - let instance = ReactTestUtils.renderIntoDocument( - - Tab 1 content - Tab 2 content - - ); - - let panes = ReactTestUtils.scryRenderedComponentsWithType(instance, TabPane); - - assert.ok(React.findDOMNode(panes[0]).className.match(/\bcustom\b/)); - assert.equal(React.findDOMNode(panes[0]).id, 'pane0id'); - }); - - it('Should show the correct initial pane', function () { - let instance = ReactTestUtils.renderIntoDocument( - - Tab 1 content - Tab 2 content - - ); - - let tabbedArea = ReactTestUtils.findRenderedComponentWithType(instance, TabbedArea); - - let panes = ReactTestUtils.scryRenderedComponentsWithType(instance, TabPane); - - assert.equal(panes[0].props.active, false); - assert.equal(panes[1].props.active, true); - - assert.equal(tabbedArea.refs.tabs.props.activeKey, 2); - }); - - it('Should show the correct first tab with no active key value', function () { - let instance = ReactTestUtils.renderIntoDocument( - - Tab 1 content - Tab 2 content - - ); - - let tabbedArea = ReactTestUtils.findRenderedComponentWithType(instance, TabbedArea); - let panes = ReactTestUtils.scryRenderedComponentsWithType(instance, TabPane); - - assert.equal(panes[0].props.active, true); - assert.equal(panes[1].props.active, false); - - assert.equal(tabbedArea.refs.tabs.props.activeKey, 1); - }); - - it('Should show the correct first tab with `React.Children.map` children values', function () { - let panes = [ -
    Tab 1 content
    , -
    Tab 2 content
    - ]; - let paneComponents = React.Children.map(panes, function(child, index) { - return {child}; - }); - - let instance = ReactTestUtils.renderIntoDocument( - - {paneComponents} - {null} - - ); - - assert.equal(instance.refs.tabs.props.activeKey, 0); - }); - - it('Should show the correct tab when selected', function () { - let tab1 = Tab 1; - let instance = ReactTestUtils.renderIntoDocument( - - Tab 1 content - Tab 2 content - - ); - - let tabbedArea = ReactTestUtils.findRenderedComponentWithType(instance, TabbedArea); - let panes = ReactTestUtils.scryRenderedComponentsWithType(instance, TabPane); - - ReactTestUtils.Simulate.click( - ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'tab1') - ); - - assert.equal(panes[0].props.active, true); - assert.equal(panes[1].props.active, false); - assert.equal(tabbedArea.refs.tabs.props.activeKey, 1); - }); - - it('Should pass default bsStyle (of "tabs") to Nav', function () { - let instance = ReactTestUtils.renderIntoDocument( - - Tab 1 content - Tab 2 content - - ); - - assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'nav-tabs')); - }); - - it('Should pass bsStyle to Nav', function () { - let instance = ReactTestUtils.renderIntoDocument( - - Tab 1 content - Tab 2 content - - ); - - assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'nav-pills')); - }); - - it('Should pass className to rendered Tab NavItem', function () { - let instance = ReactTestUtils.renderIntoDocument( - - Tab 1 content - Tab 3 content - - ); - - let tabPane = ReactTestUtils.scryRenderedComponentsWithType(instance, TabPane); - - assert.equal(tabPane.length, 2); - assert.equal(React.findDOMNode(tabPane[1]).getAttribute('class').match(/pull-right/)[0], 'pull-right'); - }); - - it('Should pass disabled to NavItem', function () { - let instance = ReactTestUtils.renderIntoDocument( - - Tab 1 content - Tab 2 content - - ); - - assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'disabled')); - }); - - it('Should not show content when clicking disabled tab', function () { - let tab1 = Tab 1; - let instance = ReactTestUtils.renderIntoDocument( - - Tab 1 content - Tab 2 content - - ); - - let tabbedArea = ReactTestUtils.findRenderedComponentWithType(instance, TabbedArea); - let panes = ReactTestUtils.scryRenderedComponentsWithType(instance, TabPane); - - ReactTestUtils.Simulate.click( - ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'tab1') - ); - - assert.equal(panes[0].props.active, false); - assert.equal(panes[1].props.active, true); - assert.equal(tabbedArea.refs.tabs.props.activeKey, 2); - }); - - describe('animation', function () { - let mountPoint; - - beforeEach(()=>{ - mountPoint = document.createElement('div'); - document.body.appendChild(mountPoint); - }); - - afterEach(function () { - React.unmountComponentAtNode(mountPoint); - document.body.removeChild(mountPoint); - }); - - function checkTabRemovingWithAnimation(animation) { - it(`should correctly set "active" after tabPane is removed with "animation=${animation}"`, function() { - let instance = render( - - Tab 1 content - Tab 2 content - - , mountPoint); - - let panes = ReactTestUtils.scryRenderedComponentsWithType(instance, TabPane); - - assert.equal(panes[0].props.active, false); - assert.equal(panes[1].props.active, true); - - // second tab has been removed - render( - - Tab 1 content - - , mountPoint); - - assert.equal(panes[0].props.active, true); - }); - } - - checkTabRemovingWithAnimation(true); - checkTabRemovingWithAnimation(false); - }); - - describe('Web Accessibility', function(){ - - it('Should generate ids from parent id', function () { - let instance = ReactTestUtils.renderIntoDocument( - - Tab 1 content - Tab 2 content - - ); - - let tabs = ReactTestUtils.scryRenderedComponentsWithType(instance, NavItem); - - tabs.every(tab => - assert.ok(tab.props['aria-controls'] && tab.props.linkId)); - }); - - it('Should add aria-controls', function () { - let instance = ReactTestUtils.renderIntoDocument( - - Tab 1 content - Tab 2 content - - ); - - let panes = ReactTestUtils.scryRenderedComponentsWithType(instance, TabPane); - - assert.equal(panes[0].props['aria-labelledby'], 'pane-1___tab'); - assert.equal(panes[1].props['aria-labelledby'], 'pane-2___tab'); - }); - - it('Should add aria-controls', function () { - let instance = ReactTestUtils.renderIntoDocument( - - Tab 1 content - Tab 2 content - - ); - - let tabs = ReactTestUtils.scryRenderedComponentsWithType(instance, NavItem); - - assert.equal(tabs[0].props['aria-controls'], 'pane-1'); - assert.equal(tabs[1].props['aria-controls'], 'pane-2'); - }); - - }); -}); diff --git a/test/TabsSpec.js b/test/TabsSpec.js new file mode 100644 index 0000000000..bf5e3e000d --- /dev/null +++ b/test/TabsSpec.js @@ -0,0 +1,491 @@ +import React from 'react'; +import ReactTestUtils from 'react/lib/ReactTestUtils'; + +import Col from '../src/Col'; +import Grid from '../src/Grid'; +import Nav from '../src/Nav'; +import NavItem from '../src/NavItem'; +import Row from '../src/Row'; +import Tab from '../src/Tab'; +import Tabs from '../src/Tabs'; + +import ValidComponentChildren from '../src/utils/ValidComponentChildren'; + +import { render } from './helpers'; + +describe('Tabs', function () { + it('Should show the correct tab', function () { + let instance = ReactTestUtils.renderIntoDocument( + + Tab 1 content + Tab 2 content + + ); + + let panes = ReactTestUtils.scryRenderedComponentsWithType(instance, Tab); + + assert.equal(panes[0].props.active, true); + assert.equal(panes[1].props.active, false); + + let tabs = ReactTestUtils.findRenderedComponentWithType(instance, Tabs); + + assert.equal(tabs.refs.tabs.props.activeKey, 1); + }); + + it('Should only show the tabs with `Tab.props.title` set', function () { + let instance = ReactTestUtils.renderIntoDocument( + + Tab 1 content + Tab 2 content + Tab 3 content + + ); + + let tabs = ReactTestUtils.findRenderedComponentWithType(instance, Tabs); + + assert.equal(ValidComponentChildren.numberOf(instance.refs.tabs.props.children), 2); + assert.equal(tabs.refs.tabs.props.activeKey, 3); + }); + + it('Should allow tab to have React components', function () { + let tabTitle = ( + Tab 2 + ); + let instance = ReactTestUtils.renderIntoDocument( + + Tab 1 content + Tab 2 content + + ); + + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance.refs.tabs, 'special-tab')); + }); + + it('Should call onSelect when tab is selected', function (done) { + function onSelect(key) { + assert.equal(key, '2'); + done(); + } + + let tab2 = Tab2; + let instance = ReactTestUtils.renderIntoDocument( + + Tab 1 content + Tab 2 content + + ); + + + ReactTestUtils.Simulate.click( + ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'tab2') + ); + }); + + it('Should have children with the correct DOM properties', function () { + let instance = ReactTestUtils.renderIntoDocument( + + Tab 1 content + Tab 2 content + + ); + + let panes = ReactTestUtils.scryRenderedComponentsWithType(instance, Tab); + + assert.ok(React.findDOMNode(panes[0]).className.match(/\bcustom\b/)); + assert.equal(React.findDOMNode(panes[0]).id, 'pane0id'); + }); + + it('Should show the correct initial pane', function () { + let instance = ReactTestUtils.renderIntoDocument( + + Tab 1 content + Tab 2 content + + ); + + let tabs = ReactTestUtils.findRenderedComponentWithType(instance, Tabs); + + let panes = ReactTestUtils.scryRenderedComponentsWithType(instance, Tab); + + assert.equal(panes[0].props.active, false); + assert.equal(panes[1].props.active, true); + + assert.equal(tabs.refs.tabs.props.activeKey, 2); + }); + + it('Should show the correct first tab with no active key value', function () { + let instance = ReactTestUtils.renderIntoDocument( + + Tab 1 content + Tab 2 content + + ); + + let tabs = ReactTestUtils.findRenderedComponentWithType(instance, Tabs); + let panes = ReactTestUtils.scryRenderedComponentsWithType(instance, Tab); + + assert.equal(panes[0].props.active, true); + assert.equal(panes[1].props.active, false); + + assert.equal(tabs.refs.tabs.props.activeKey, 1); + }); + + it('Should show the correct first tab with `React.Children.map` children values', function () { + let panes = [ +
    Tab 1 content
    , +
    Tab 2 content
    + ]; + let paneComponents = React.Children.map(panes, function(child, index) { + return {child}; + }); + + let instance = ReactTestUtils.renderIntoDocument( + + {paneComponents} + {null} + + ); + + assert.equal(instance.refs.tabs.props.activeKey, 0); + }); + + it('Should show the correct tab when selected', function () { + let tab1 = Tab 1; + let instance = ReactTestUtils.renderIntoDocument( + + Tab 1 content + Tab 2 content + + ); + + let tabs = ReactTestUtils.findRenderedComponentWithType(instance, Tabs); + let panes = ReactTestUtils.scryRenderedComponentsWithType(instance, Tab); + + ReactTestUtils.Simulate.click( + ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'tab1') + ); + + assert.equal(panes[0].props.active, true); + assert.equal(panes[1].props.active, false); + assert.equal(tabs.refs.tabs.props.activeKey, 1); + }); + + it('Should pass default bsStyle (of "tabs") to Nav', function () { + let instance = ReactTestUtils.renderIntoDocument( + + Tab 1 content + Tab 2 content + + ); + + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'nav-tabs')); + }); + + it('Should pass bsStyle to Nav', function () { + let instance = ReactTestUtils.renderIntoDocument( + + Tab 1 content + Tab 2 content + + ); + + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'nav-pills')); + }); + + it('Should pass disabled to Nav', function () { + let instance = ReactTestUtils.renderIntoDocument( + + Tab 1 content + Tab 2 content + + ); + + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'disabled')); + }); + + it('Should not show content when clicking disabled tab', function () { + let tab1 = Tab 1; + let instance = ReactTestUtils.renderIntoDocument( + + Tab 1 content + Tab 2 content + + ); + + let tabs = ReactTestUtils.findRenderedComponentWithType(instance, Tabs); + let panes = ReactTestUtils.scryRenderedComponentsWithType(instance, Tab); + + ReactTestUtils.Simulate.click( + ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'tab1') + ); + + assert.equal(panes[0].props.active, false); + assert.equal(panes[1].props.active, true); + assert.equal(tabs.refs.tabs.props.activeKey, 2); + }); + + + describe('when the position prop is not provided', function() { + let instance; + + beforeEach(function() { + instance = ReactTestUtils.renderIntoDocument( + + Tab content + + ); + }); + + it('doesn\'t stack the tabs', function () { + let nav = ReactTestUtils.findRenderedComponentWithType(instance, Nav); + + expect(nav.props.bsStyle).to.equal('tabs'); + expect(nav.props.stacked).to.not.be.ok; + }); + + it('doesn\'t apply column styling', function () { + let tabs = instance.refs.tabs; + let panes = instance.refs.panes; + + expect(React.findDOMNode(tabs).className).to.not.match(/\bcol\b/); + expect(React.findDOMNode(panes).className).to.not.match(/\bcol\b/); + }); + + it('doesn\'t render grid elements', function () { + const grids = ReactTestUtils.scryRenderedComponentsWithType( + instance, Grid + ); + const rows = ReactTestUtils.scryRenderedComponentsWithType( + instance, Row + ); + const cols = ReactTestUtils.scryRenderedComponentsWithType( + instance, Col + ); + + expect(grids).to.be.empty; + expect(rows).to.be.empty; + expect(cols).to.be.empty; + }); + }); + + + describe('when the position prop is "left"', function() { + describe('when tabWidth is not provided', function() { + let instance; + + beforeEach(function () { + instance = ReactTestUtils.renderIntoDocument( + + Tab content + + ); + }); + + it('Should stack the tabs', function () { + let nav = ReactTestUtils.findRenderedComponentWithType(instance, Nav); + + expect(nav.props.bsStyle).to.equal('pills'); + expect(nav.props.stacked).to.be.ok; + }); + + it('Should have a left nav with a width of 2', function() { + let tabs = instance.refs.tabs; + let panes = instance.refs.panes; + + expect(React.findDOMNode(tabs).className).to.match(/\bcol-xs-2\b/); + expect(React.findDOMNode(panes).className).to.match(/\bcol-xs-10\b/); + }); + + it('renders grid elements', function () { + const grids = ReactTestUtils.scryRenderedComponentsWithType( + instance, Grid + ); + const rows = ReactTestUtils.scryRenderedComponentsWithType( + instance, Row + ); + const cols = ReactTestUtils.scryRenderedComponentsWithType( + instance, Col + ); + + expect(grids).to.have.length(1); + expect(rows).to.have.length(1); + expect(cols).to.have.length(2); + }); + }); + + describe('when only tabWidth is provided', function() { + it('Should have a left nav with the width that was provided', function() { + let instance = ReactTestUtils.renderIntoDocument( + + Tab content + + ); + + let tabs = instance.refs.tabs; + let panes = instance.refs.panes; + + expect(React.findDOMNode(tabs).className).to.match(/\bcol-xs-3\b/); + expect(React.findDOMNode(panes).className).to.match(/\bcol-xs-9\b/); + }); + }); + + describe('when simple tabWidth and paneWidth are provided', function() { + let instance; + + beforeEach(function () { + instance = ReactTestUtils.renderIntoDocument( + + Tab content + + ); + }); + + it('Should have the provided widths', function() { + let tabs = instance.refs.tabs; + let panes = instance.refs.panes; + + expect(React.findDOMNode(tabs).className).to.match(/\bcol-xs-4\b/); + expect(React.findDOMNode(panes).className).to.match(/\bcol-xs-7\b/); + }); + }); + + describe('when complex tabWidth and paneWidth are provided', function() { + let instance; + + beforeEach(function () { + instance = ReactTestUtils.renderIntoDocument( + + Tab content + + ); + }); + + it('Should have the provided widths', function() { + let tabs = instance.refs.tabs; + let panes = instance.refs.panes; + + expect(React.findDOMNode(tabs).className) + .to.match(/\bcol-xs-4\b/).and.to.match(/\bcol-md-3\b/); + expect(React.findDOMNode(panes).className) + .to.match(/\bcol-xs-7\b/).and.to.match(/\bcol-md-8\b/); + }); + }); + }); + + describe('animation', function () { + let mountPoint; + + beforeEach(()=>{ + mountPoint = document.createElement('div'); + document.body.appendChild(mountPoint); + }); + + afterEach(function () { + React.unmountComponentAtNode(mountPoint); + document.body.removeChild(mountPoint); + }); + + function checkTabRemovingWithAnimation(animation) { + it(`should correctly set "active" after Tab is removed with "animation=${animation}"`, function() { + let instance = render( + + Tab 1 content + Tab 2 content + + , mountPoint); + + let panes = ReactTestUtils.scryRenderedComponentsWithType(instance, Tab); + + assert.equal(panes[0].props.active, false); + assert.equal(panes[1].props.active, true); + + // second tab has been removed + render( + + Tab 1 content + + , mountPoint); + + assert.equal(panes[0].props.active, true); + }); + } + + checkTabRemovingWithAnimation(true); + checkTabRemovingWithAnimation(false); + }); + + describe('Web Accessibility', function(){ + let instance; + beforeEach(function(){ + instance = ReactTestUtils.renderIntoDocument( + + Tab 1 content + Tab 2 content + + ); + }); + + it('Should generate ids from parent id', function () { + let tabs = ReactTestUtils.scryRenderedComponentsWithType(instance, NavItem); + + tabs.every(tab => + assert.ok(tab.props['aria-controls'] && tab.props.linkId)); + }); + + it('Should add aria-controls', function () { + let panes = ReactTestUtils.scryRenderedComponentsWithType(instance, Tab); + + assert.equal(panes[0].props['aria-labelledby'], 'pane-1___tab'); + assert.equal(panes[1].props['aria-labelledby'], 'pane-2___tab'); + }); + + it('Should add aria-controls', function () { + let tabs = ReactTestUtils.scryRenderedComponentsWithType(instance, NavItem); + + assert.equal(tabs[0].props['aria-controls'], 'pane-1'); + assert.equal(tabs[1].props['aria-controls'], 'pane-2'); + }); + + it('Should add role=tablist to the nav', function () { + let nav = ReactTestUtils.findRenderedComponentWithType(instance, Nav); + + assert.equal(nav.props.role, 'tablist'); + }); + + it('Should add aria-selected to the nav item for the selected tab', function() { + let tabs = ReactTestUtils.scryRenderedComponentsWithType(instance, NavItem); + let link1 = ReactTestUtils.findRenderedDOMComponentWithTag(tabs[0], 'a'); + let link2 = ReactTestUtils.findRenderedDOMComponentWithTag(tabs[1], 'a'); + + assert.equal(link1.props['aria-selected'], false); + assert.equal(link2.props['aria-selected'], true); + }); + }); + + it('Should not pass className to Nav', function () { + let instance = ReactTestUtils.renderIntoDocument( + + Tab 1 content + Tab 2 content + + ); + let myTabClass = ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'my-tab-class'); + let myNavItem = ReactTestUtils.scryRenderedDOMComponentsWithClass(instance, 'nav-pills')[0]; + assert.notDeepEqual(myTabClass, myNavItem); + }); + + it('Should pass className, Id, and style to Tabs', function () { + let instance = ReactTestUtils.renderIntoDocument( + + ); + assert.equal(React.findDOMNode(instance).getAttribute('class'), 'my-tabs-class'); + assert.equal(React.findDOMNode(instance).getAttribute('id'), 'my-tabs-id'); + assert.deepEqual(React.findDOMNode(instance).getAttribute('style'), 'opacity:0.5;'); + + }); +}); diff --git a/test/TransitionSpec.js b/test/TransitionSpec.js deleted file mode 100644 index 1592e646f7..0000000000 --- a/test/TransitionSpec.js +++ /dev/null @@ -1,279 +0,0 @@ -import React from 'react'; -import ReactTestUtils from 'react/lib/ReactTestUtils'; -import { render } from './helpers'; -import Transition, {UNMOUNTED, EXITED, ENTERING, ENTERED, EXITING} from - '../src/Transition'; - -describe('Transition', function () { - it('should not transition on mount', function(){ - let instance = render( - { throw new Error('should not Enter'); }}> -
    -
    - ); - - expect(instance.state.status).to.equal(ENTERED); - }); - - it('should transition on mount with transitionAppear', done =>{ - let instance = ReactTestUtils.renderIntoDocument( - done()} - > -
    -
    - ); - - expect(instance.state.status).to.equal(EXITED); - }); - - describe('entering', ()=> { - let instance; - - beforeEach(function(){ - instance = render( - -
    - - ); - }); - - it('should fire callbacks', done => { - let onEnter = sinon.spy(); - let onEntering = sinon.spy(); - - expect(instance.state.status).to.equal(EXITED); - - instance = instance.renderWithProps({ - in: true, - - onEnter, - - onEntering, - - onEntered(){ - expect(onEnter.calledOnce).to.be.ok; - expect(onEntering.calledOnce).to.be.ok; - expect(onEnter.calledBefore(onEntering)).to.be.ok; - done(); - } - }); - }); - - it('should move to each transition state', done => { - let count = 0; - - expect(instance.state.status).to.equal(EXITED); - - instance = instance.renderWithProps({ - in: true, - - onEnter(){ - count++; - expect(instance.state.status).to.equal(EXITED); - }, - - onEntering(){ - count++; - expect(instance.state.status).to.equal(ENTERING); - }, - - onEntered(){ - expect(instance.state.status).to.equal(ENTERED); - expect(count).to.equal(2); - done(); - } - }); - }); - - it('should apply classes at each transition state', done => { - let count = 0; - - expect(instance.state.status).to.equal(EXITED); - - instance = instance.renderWithProps({ - in: true, - - onEnter(node){ - count++; - expect(node.className).to.equal(''); - }, - - onEntering(node){ - count++; - expect(node.className).to.equal('test-entering'); - }, - - onEntered(node){ - expect(node.className).to.equal('test-enter'); - expect(count).to.equal(2); - done(); - } - }); - }); - }); - - describe('exiting', ()=> { - let instance; - - beforeEach(function(){ - instance = render( - -
    - - ); - }); - - it('should fire callbacks', done => { - let onExit = sinon.spy(); - let onExiting = sinon.spy(); - - expect(instance.state.status).to.equal(ENTERED); - - instance = instance.renderWithProps({ - in: false, - - onExit, - - onExiting, - - onExited(){ - expect(onExit.calledOnce).to.be.ok; - expect(onExiting.calledOnce).to.be.ok; - expect(onExit.calledBefore(onExiting)).to.be.ok; - done(); - } - }); - }); - - it('should move to each transition state', done => { - let count = 0; - - expect(instance.state.status).to.equal(ENTERED); - - instance = instance.renderWithProps({ - in: false, - - onExit(){ - count++; - expect(instance.state.status).to.equal(ENTERED); - }, - - onExiting(){ - count++; - expect(instance.state.status).to.equal(EXITING); - }, - - onExited(){ - expect(instance.state.status).to.equal(EXITED); - expect(count).to.equal(2); - done(); - } - }); - }); - - it('should apply classes at each transition state', done => { - let count = 0; - - expect(instance.state.status).to.equal(ENTERED); - - instance = instance.renderWithProps({ - in: false, - - onExit(node){ - count++; - expect(node.className).to.equal(''); - }, - - onExiting(node){ - count++; - expect(node.className).to.equal('test-exiting'); - }, - - onExited(node){ - expect(node.className).to.equal('test-exit'); - expect(count).to.equal(2); - done(); - } - }); - }); - }); - - describe('unmountOnExit', () => { - class UnmountTransition extends React.Component { - constructor(props) { - super(props); - - this.state = {in: props.initialIn}; - } - - render() { - return ( - -
    - - ); - } - - getStatus() { - return this.refs.transition.state.status; - } - } - - it('should mount when entering', done => { - const instance = render( - { - expect(instance.getStatus()).to.equal(EXITED); - expect(React.findDOMNode(instance)).to.exist; - - done(); - }} - /> - ); - - expect(instance.getStatus()).to.equal(UNMOUNTED); - expect(React.findDOMNode(instance)).to.not.exist; - - instance.setState({in: true}); - }); - - it('should unmount after exiting', done => { - const instance = render( - { - expect(instance.getStatus()).to.equal(UNMOUNTED); - expect(React.findDOMNode(instance)).to.not.exist; - - done(); - }} - /> - ); - - expect(instance.getStatus()).to.equal(ENTERED); - expect(React.findDOMNode(instance)).to.exist; - - instance.setState({in: false}); - }); - }); -}); diff --git a/tools/.eslintrc b/tools/.eslintrc index 7753f32e77..77e68a9f56 100644 --- a/tools/.eslintrc +++ b/tools/.eslintrc @@ -1,4 +1,8 @@ { + "env": { + "node": true + }, + "parser": "babel-eslint", "rules": { "no-console": 0 } diff --git a/tools/build-cli.js b/tools/build-cli.js index d3b69fed77..a447d72df9 100644 --- a/tools/build-cli.js +++ b/tools/build-cli.js @@ -3,7 +3,6 @@ import 'colors'; import build from './build'; import docs from '../docs/build'; -import lib from './lib/build'; import { setExecOptions } from './exec'; import yargs from 'yargs'; @@ -14,11 +13,6 @@ const argv = yargs demand: false, default: false }) - .option('lib-only', { - demand: false, - default: false, - describe: 'Used for factories testing' - }) .option('verbose', { demand: false, default: false, @@ -35,9 +29,7 @@ setExecOptions(argv); let buildProcess; -if (argv.libOnly) { - buildProcess = lib(argv); -} else if (argv.docsOnly) { +if (argv.docsOnly) { buildProcess = docs(argv); } else { buildProcess = build(argv); diff --git a/tools/generateFactories.js b/tools/generateFactories.js deleted file mode 100644 index 0e094c7e94..0000000000 --- a/tools/generateFactories.js +++ /dev/null @@ -1,23 +0,0 @@ -import _ from 'lodash'; -import path from 'path'; -import fs from 'fs'; -import { srcRoot } from './constants'; -import components from './public-components'; -import { buildContent } from './buildBabel'; - -export default function generateFactories(destination, babelOptions = {}) { - - function generateCompiledFile(file, content) { - const outpath = path.join(destination, 'factories', `${file}.js`); - buildContent(content, __dirname, outpath, babelOptions); - } - - const indexTemplate = fs.readFileSync(path.join(srcRoot, 'templates', 'factory.index.js.template')); - const factoryTemplate = fs.readFileSync(path.join(srcRoot, 'templates', 'factory.js.template')); - - generateCompiledFile( 'index', _.template(indexTemplate)({components}) ); - - return Promise.all( - components.map( name => generateCompiledFile( name, _.template(factoryTemplate)({name}) )) - ); -} diff --git a/tools/lib/build.js b/tools/lib/build.js index 01a47e4cbb..2869824453 100644 --- a/tools/lib/build.js +++ b/tools/lib/build.js @@ -2,7 +2,6 @@ import 'colors'; import { exec } from '../exec'; import fsp from 'fs-promise'; import { srcRoot, libRoot } from '../constants'; -import generateFactories from '../generateFactories'; import { buildFolder } from '../buildBabel'; export default function BuildCommonJs() { @@ -10,9 +9,6 @@ export default function BuildCommonJs() { return exec(`rimraf ${libRoot}`) .then(() => fsp.mkdirs(libRoot)) - .then(() => Promise.all([ - generateFactories(libRoot), - buildFolder(srcRoot, libRoot) - ])) + .then(() => buildFolder(srcRoot, libRoot)) .then(() => console.log('Built: '.cyan + 'npm module'.green)); } diff --git a/tools/release-scripts/test.js b/tools/release-scripts/test.js index dc1e311756..75175fbaa4 100644 --- a/tools/release-scripts/test.js +++ b/tools/release-scripts/test.js @@ -4,7 +4,7 @@ import { exec } from '../exec'; function test() { console.log('Running: '.cyan + 'tests'.green); - return exec('npm run lib-build && npm run tests-set') + return exec('npm run tests-set') .then(() => console.log('Completed: '.cyan + 'tests'.green)); } diff --git a/webpack/docs.config.js b/webpack/docs.config.js index ceff28f49d..7964a543fa 100644 --- a/webpack/docs.config.js +++ b/webpack/docs.config.js @@ -21,6 +21,9 @@ if (options.debug) { } export default _.extend({}, baseConfig, { + + devtool: options.debug ? 'source-map' : null, + entry: { bundle: options.debug ? devEntryBundle : entryFile },