react-tabs
Version:
An accessible and easy tab component for ReactJS
357 lines (286 loc) • 12.2 kB
JavaScript
function _extends() { _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; }; return _extends.apply(this, arguments); }
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; }
import PropTypes from 'prop-types';
import React, { cloneElement, Component } from 'react';
import cx from 'classnames';
import uuid from '../helpers/uuid';
import { childrenPropType } from '../helpers/propTypes';
import { getPanelsCount as _getPanelsCount, getTabsCount as _getTabsCount } from '../helpers/count';
import { deepMap } from '../helpers/childrenDeepMap';
import { isTabList, isTabPanel, isTab } from '../helpers/elementTypes'; // Determine if a node from event.target is a Tab element
function isTabNode(node) {
return 'getAttribute' in node && node.getAttribute('role') === 'tab';
} // Determine if a tab node is disabled
function isTabDisabled(node) {
return node.getAttribute('aria-disabled') === 'true';
}
var canUseActiveElement;
try {
canUseActiveElement = !!(typeof window !== 'undefined' && window.document && window.document.activeElement);
} catch (e) {
// Work around for IE bug when accessing document.activeElement in an iframe
// Refer to the following resources:
// http://stackoverflow.com/a/10982960/369687
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12733599
canUseActiveElement = false;
}
var UncontrolledTabs =
/*#__PURE__*/
function (_Component) {
_inheritsLoose(UncontrolledTabs, _Component);
function UncontrolledTabs() {
var _this;
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
_this = _Component.call.apply(_Component, [this].concat(args)) || this;
_this.tabNodes = [];
_this.handleKeyDown = function (e) {
if (_this.isTabFromContainer(e.target)) {
var index = _this.props.selectedIndex;
var preventDefault = false;
var useSelectedIndex = false;
if (e.keyCode === 32 || e.keyCode === 13) {
preventDefault = true;
useSelectedIndex = false;
_this.handleClick(e);
}
if (e.keyCode === 37 || e.keyCode === 38) {
// Select next tab to the left
index = _this.getPrevTab(index);
preventDefault = true;
useSelectedIndex = true;
} else if (e.keyCode === 39 || e.keyCode === 40) {
// Select next tab to the right
index = _this.getNextTab(index);
preventDefault = true;
useSelectedIndex = true;
} else if (e.keyCode === 35) {
// Select last tab (End key)
index = _this.getLastTab();
preventDefault = true;
useSelectedIndex = true;
} else if (e.keyCode === 36) {
// Select first tab (Home key)
index = _this.getFirstTab();
preventDefault = true;
useSelectedIndex = true;
} // This prevents scrollbars from moving around
if (preventDefault) {
e.preventDefault();
} // Only use the selected index in the state if we're not using the tabbed index
if (useSelectedIndex) {
_this.setSelected(index, e);
}
}
};
_this.handleClick = function (e) {
var node = e.target; // eslint-disable-next-line no-cond-assign
do {
if (_this.isTabFromContainer(node)) {
if (isTabDisabled(node)) {
return;
}
var index = [].slice.call(node.parentNode.children).filter(isTabNode).indexOf(node);
_this.setSelected(index, e);
return;
}
} while ((node = node.parentNode) !== null);
};
return _this;
}
var _proto = UncontrolledTabs.prototype;
_proto.setSelected = function setSelected(index, event) {
// Check index boundary
if (index < 0 || index >= this.getTabsCount()) return;
var _this$props = this.props,
onSelect = _this$props.onSelect,
selectedIndex = _this$props.selectedIndex; // Call change event handler
onSelect(index, selectedIndex, event);
};
_proto.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++) {
if (!isTabDisabled(this.getTab(i))) {
return i;
}
} // If no tab found, continue searching from first on left to index
for (var _i = 0; _i < index; _i++) {
if (!isTabDisabled(this.getTab(_i))) {
return _i;
}
} // No tabs are disabled, return index
return index;
};
_proto.getPrevTab = function getPrevTab(index) {
var i = index; // Look for non-disabled tab from index to first tab on the left
while (i--) {
if (!isTabDisabled(this.getTab(i))) {
return i;
}
} // If no tab found, continue searching from last tab on right to index
i = this.getTabsCount();
while (i-- > index) {
if (!isTabDisabled(this.getTab(i))) {
return i;
}
} // No tabs are disabled, return index
return index;
};
_proto.getFirstTab = function getFirstTab() {
var count = this.getTabsCount(); // Look for non disabled tab from the first tab
for (var i = 0; i < count; i++) {
if (!isTabDisabled(this.getTab(i))) {
return i;
}
}
return null;
};
_proto.getLastTab = function getLastTab() {
var i = this.getTabsCount(); // Look for non disabled tab from the last tab
while (i--) {
if (!isTabDisabled(this.getTab(i))) {
return i;
}
}
return null;
};
_proto.getTabsCount = function getTabsCount() {
var children = this.props.children;
return _getTabsCount(children);
};
_proto.getPanelsCount = function getPanelsCount() {
var children = this.props.children;
return _getPanelsCount(children);
};
_proto.getTab = function getTab(index) {
return this.tabNodes["tabs-" + index];
};
_proto.getChildren = function getChildren() {
var _this2 = this;
var index = 0;
var _this$props2 = this.props,
children = _this$props2.children,
disabledTabClassName = _this$props2.disabledTabClassName,
focus = _this$props2.focus,
forceRenderTabPanel = _this$props2.forceRenderTabPanel,
selectedIndex = _this$props2.selectedIndex,
selectedTabClassName = _this$props2.selectedTabClassName,
selectedTabPanelClassName = _this$props2.selectedTabPanelClassName;
this.tabIds = this.tabIds || [];
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) {
this.tabIds.push(uuid());
this.panelIds.push(uuid());
} // Map children to dynamically setup refs
return deepMap(children, function (child) {
var result = child; // Clone TabList and Tab components to have refs
if (isTabList(child)) {
var listIndex = 0; // Figure out if the current focus in the DOM is set on a Tab
// If it is we should keep the focus on the next selected tab
var wasTabFocused = false;
if (canUseActiveElement) {
wasTabFocused = React.Children.toArray(child.props.children).filter(isTab).some(function (tab, i) {
return document.activeElement === _this2.getTab(i);
});
}
result = cloneElement(child, {
children: deepMap(child.props.children, function (tab) {
var key = "tabs-" + listIndex;
var selected = selectedIndex === listIndex;
var props = {
tabRef: function tabRef(node) {
_this2.tabNodes[key] = node;
},
id: _this2.tabIds[listIndex],
panelId: _this2.panelIds[listIndex],
selected: selected,
focus: selected && (focus || wasTabFocused)
};
if (selectedTabClassName) props.selectedClassName = selectedTabClassName;
if (disabledTabClassName) props.disabledClassName = disabledTabClassName;
listIndex++;
return cloneElement(tab, props);
})
});
} else if (isTabPanel(child)) {
var props = {
id: _this2.panelIds[index],
tabId: _this2.tabIds[index],
selected: selectedIndex === index
};
if (forceRenderTabPanel) props.forceRender = forceRenderTabPanel;
if (selectedTabPanelClassName) props.selectedClassName = selectedTabPanelClassName;
index++;
result = cloneElement(child, props);
}
return result;
});
};
/**
* 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.
*/
_proto.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;
do {
if (nodeAncestor === this.node) return true;
if (nodeAncestor.getAttribute('data-tabs')) break;
nodeAncestor = nodeAncestor.parentElement;
} while (nodeAncestor);
return false;
};
_proto.render = function render() {
var _this3 = this;
// Delete all known props, so they don't get added to DOM
var _this$props3 = this.props,
children = _this$props3.children,
className = _this$props3.className,
disabledTabClassName = _this$props3.disabledTabClassName,
domRef = _this$props3.domRef,
focus = _this$props3.focus,
forceRenderTabPanel = _this$props3.forceRenderTabPanel,
onSelect = _this$props3.onSelect,
selectedIndex = _this$props3.selectedIndex,
selectedTabClassName = _this$props3.selectedTabClassName,
selectedTabPanelClassName = _this$props3.selectedTabPanelClassName,
attributes = _objectWithoutPropertiesLoose(_this$props3, ["children", "className", "disabledTabClassName", "domRef", "focus", "forceRenderTabPanel", "onSelect", "selectedIndex", "selectedTabClassName", "selectedTabPanelClassName"]);
return React.createElement("div", _extends({}, attributes, {
className: cx(className),
onClick: this.handleClick,
onKeyDown: this.handleKeyDown,
ref: function ref(node) {
_this3.node = node;
if (domRef) domRef(node);
},
"data-tabs": true
}), this.getChildren());
};
return UncontrolledTabs;
}(Component);
UncontrolledTabs.defaultProps = {
className: 'react-tabs',
focus: false
};
export { UncontrolledTabs as default };
UncontrolledTabs.propTypes = process.env.NODE_ENV !== "production" ? {
children: childrenPropType,
className: PropTypes.oneOfType([PropTypes.string, PropTypes.array, PropTypes.object]),
disabledTabClassName: PropTypes.string,
domRef: PropTypes.func,
focus: PropTypes.bool,
forceRenderTabPanel: PropTypes.bool,
onSelect: PropTypes.func.isRequired,
selectedIndex: PropTypes.number.isRequired,
selectedTabClassName: PropTypes.string,
selectedTabPanelClassName: PropTypes.string
} : {};