UNPKG

react-tabs

Version:
405 lines (338 loc) 12.3 kB
'use strict'; var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _reactDom = require('react-dom'); var _classnames = require('classnames'); var _classnames2 = _interopRequireDefault(_classnames); var _jsStylesheet = require('js-stylesheet'); var _jsStylesheet2 = _interopRequireDefault(_jsStylesheet); var _uuid = require('../helpers/uuid'); var _uuid2 = _interopRequireDefault(_uuid); var _childrenPropType = require('../helpers/childrenPropType'); var _childrenPropType2 = _interopRequireDefault(_childrenPropType); var _Tab = require('./Tab'); var _Tab2 = _interopRequireDefault(_Tab); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } // Determine if a node from event.target is a Tab element function isTabNode(node) { return node.nodeName === 'LI' && node.getAttribute('role') === 'tab'; } // Determine if a tab node is disabled function isTabDisabled(node) { return node.getAttribute('aria-disabled') === 'true'; } var useDefaultStyles = true; module.exports = _react2.default.createClass({ displayName: 'Tabs', propTypes: { className: _react.PropTypes.string, selectedIndex: _react.PropTypes.number, onSelect: _react.PropTypes.func, focus: _react.PropTypes.bool, children: _childrenPropType2.default, forceRenderTabPanel: _react.PropTypes.bool }, childContextTypes: { forceRenderTabPanel: _react.PropTypes.bool }, statics: { setUseDefaultStyles: function setUseDefaultStyles(use) { useDefaultStyles = use; } }, getDefaultProps: function getDefaultProps() { return { selectedIndex: -1, focus: false, forceRenderTabPanel: false }; }, getInitialState: function getInitialState() { return this.copyPropsToState(this.props, this.state); }, getChildContext: function getChildContext() { return { forceRenderTabPanel: this.props.forceRenderTabPanel }; }, componentDidMount: function componentDidMount() { if (useDefaultStyles) { (0, _jsStylesheet2.default)(require('../helpers/styles.js')); // eslint-disable-line global-require } }, componentWillReceiveProps: function componentWillReceiveProps(newProps) { var _this = this; // Use a transactional update to prevent race conditions // when reading the state in copyPropsToState // See https://github.com/reactjs/react-tabs/issues/51 this.setState(function (state) { return _this.copyPropsToState(newProps, state); }); }, setSelected: function setSelected(index, focus) { // Don't do anything if nothing has changed if (index === this.state.selectedIndex) return; // Check index boundary if (index < 0 || index >= this.getTabsCount()) return; // Keep reference to last index for event handler var last = this.state.selectedIndex; // Check if the change event handler cancels the tab change var cancel = false; // Call change event handler if (typeof this.props.onSelect === 'function') { cancel = this.props.onSelect(index, last) === false; } if (!cancel) { // Update selected index this.setState({ selectedIndex: index, focus: focus === true }); } }, getNextTab: function getNextTab(index) { var count = this.getTabsCount(); // Look for non-disabled tab from index to the last tab on the right for (var i = index + 1; i < count; i++) { var tab = this.getTab(i); if (!isTabDisabled((0, _reactDom.findDOMNode)(tab))) { return i; } } // If no tab found, continue searching from first on left to index for (var _i = 0; _i < index; _i++) { var _tab = this.getTab(_i); if (!isTabDisabled((0, _reactDom.findDOMNode)(_tab))) { return _i; } } // No tabs are disabled, return index return index; }, getPrevTab: function getPrevTab(index) { var i = index; // Look for non-disabled tab from index to first tab on the left while (i--) { var tab = this.getTab(i); if (!isTabDisabled((0, _reactDom.findDOMNode)(tab))) { return i; } } // If no tab found, continue searching from last tab on right to index i = this.getTabsCount(); while (i-- > index) { var _tab2 = this.getTab(i); if (!isTabDisabled((0, _reactDom.findDOMNode)(_tab2))) { return i; } } // No tabs are disabled, return index return index; }, getTabsCount: function getTabsCount() { return this.props.children && this.props.children[0] ? _react2.default.Children.count(this.props.children[0].props.children) : 0; }, getPanelsCount: function getPanelsCount() { return _react2.default.Children.count(this.props.children.slice(1)); }, getTabList: function getTabList() { return this.refs.tablist; }, getTab: function getTab(index) { return this.refs['tabs-' + index]; }, getPanel: function getPanel(index) { return this.refs['panels-' + index]; }, getChildren: function getChildren() { var index = 0; var count = 0; var children = this.props.children; var state = this.state; var tabIds = this.tabIds = this.tabIds || []; var panelIds = this.panelIds = this.panelIds || []; var diff = this.tabIds.length - this.getTabsCount(); // Add ids if new tabs have been added // Don't bother removing ids, just keep them in case they are added again // This is more efficient, and keeps the uuid counter under control while (diff++ < 0) { tabIds.push((0, _uuid2.default)()); panelIds.push((0, _uuid2.default)()); } // Map children to dynamically setup refs return _react2.default.Children.map(children, function (child) { // null happens when conditionally rendering TabPanel/Tab // see https://github.com/rackt/react-tabs/issues/37 if (child === null) { return null; } var result = null; // Clone TabList and Tab components to have refs if (count++ === 0) { // TODO try setting the uuid in the "constructor" for `Tab`/`TabPanel` result = (0, _react.cloneElement)(child, { ref: 'tablist', children: _react2.default.Children.map(child.props.children, function (tab) { // null happens when conditionally rendering TabPanel/Tab // see https://github.com/rackt/react-tabs/issues/37 if (tab === null) { return null; } var ref = 'tabs-' + index; var id = tabIds[index]; var panelId = panelIds[index]; var selected = state.selectedIndex === index; var focus = selected && state.focus; index++; if (tab.type === _Tab2.default) { return (0, _react.cloneElement)(tab, { ref: ref, id: id, panelId: panelId, selected: selected, focus: focus }); } return tab; }) }); // Reset index for panels index = 0; } // Clone TabPanel components to have refs else { var ref = 'panels-' + index; var id = panelIds[index]; var tabId = tabIds[index]; var selected = state.selectedIndex === index; index++; result = (0, _react.cloneElement)(child, { ref: ref, id: id, tabId: tabId, selected: selected }); } return result; }); }, handleKeyDown: function handleKeyDown(e) { if (this.isTabFromContainer(e.target)) { var index = this.state.selectedIndex; var preventDefault = false; // Select next tab to the left if (e.keyCode === 37 || e.keyCode === 38) { index = this.getPrevTab(index); preventDefault = true; } // Select next tab to the right /* eslint brace-style:0 */ else if (e.keyCode === 39 || e.keyCode === 40) { index = this.getNextTab(index); preventDefault = true; } // This prevents scrollbars from moving around if (preventDefault) { e.preventDefault(); } this.setSelected(index, true); } }, handleClick: function handleClick(e) { var node = e.target; do { // eslint-disable-line no-cond-assign if (this.isTabFromContainer(node)) { if (isTabDisabled(node)) { return; } var index = [].slice.call(node.parentNode.children).indexOf(node); this.setSelected(index); return; } } while ((node = node.parentNode) !== null); }, // This is an anti-pattern, so sue me copyPropsToState: function copyPropsToState(props, state) { var selectedIndex = props.selectedIndex; // If no selectedIndex prop was supplied, then try // preserving the existing selectedIndex from state. // If the state has not selectedIndex, default // to the first tab in the TabList. // // TODO: Need automation testing around this // Manual testing can be done using examples/focus // See 'should preserve selectedIndex when typing' in specs/Tabs.spec.js if (selectedIndex === -1) { if (state && state.selectedIndex) { selectedIndex = state.selectedIndex; } else { selectedIndex = 0; } } return { selectedIndex: selectedIndex, focus: props.focus }; }, /** * Determine if a node from event.target is a Tab element for the current Tabs container. * If the clicked element is not a Tab, it returns false. * If it finds another Tabs container between the Tab and `this`, it returns false. */ isTabFromContainer: function isTabFromContainer(node) { // return immediately if the clicked element is not a Tab. if (!isTabNode(node)) { return false; } // Check if the first occurrence of a Tabs container is `this` one. var nodeAncestor = node.parentElement; var tabsNode = (0, _reactDom.findDOMNode)(this); do { if (nodeAncestor === tabsNode) return true;else if (nodeAncestor.getAttribute('data-tabs')) break; nodeAncestor = nodeAncestor.parentElement; } while (nodeAncestor); return false; }, render: function render() { var _this2 = this; // This fixes an issue with focus management. // // Ultimately, when focus is true, and an input has focus, // and any change on that input causes a state change/re-render, // focus gets sent back to the active tab, and input loses focus. // // Since the focus state only needs to be remembered // for the current render, we can reset it once the // render has happened. // // Don't use setState, because we don't want to re-render. // // See https://github.com/rackt/react-tabs/pull/7 if (this.state.focus) { setTimeout(function () { _this2.state.focus = false; }, 0); } var _props = this.props; var className = _props.className; var attributes = _objectWithoutProperties(_props, ['className']); // Delete all known props, so they don't get added to DOM delete attributes.selectedIndex; delete attributes.onSelect; delete attributes.focus; delete attributes.children; delete attributes.forceRenderTabPanel; delete attributes.onClick; delete attributes.onKeyDown; return _react2.default.createElement( 'div', _extends({}, attributes, { className: (0, _classnames2.default)('ReactTabs', 'react-tabs', className), onClick: this.handleClick, onKeyDown: this.handleKeyDown, 'data-tabs': true }), this.getChildren() ); } });