retabbed
Version:
Primitive to build simple, flexible, WAI-ARIA compliant React tab components
527 lines (428 loc) • 16.4 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
var idCounter = 0;
/**
* This generates a unique ID for an instance of Downshift
* @return {String} the unique ID
*/
function generateId() {
return String(idCounter++);
}
/**
* Resets idCounter to 0. Used for SSR.
*/
function resetIdCounter() {
idCounter = 0;
}
function noop() {}
/**
* This is intended to be used to compose event handlers.
* They are executed in order until one of them sets
* `event.preventDownshiftDefault = true`.
* @param {...Function} fns the event handler functions
* @return {Function} the event handler to add to an element
*/
function callAllEventHandlers() {
for (var _len = arguments.length, fns = Array(_len), _key = 0; _key < _len; _key++) {
fns[_key] = arguments[_key];
}
return function (event) {
for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
args[_key2 - 1] = arguments[_key2];
}
return fns.some(function (fn) {
fn && fn.apply(undefined, [event].concat(args));
return event.preventTabsDefault || event.hasOwnProperty('nativeEvent') && event.nativeEvent.preventTabsDefault;
});
};
}
/**
* Takes an argument and if it's an array, returns the first item in the array
* otherwise returns the argument
* @param {*} arg the maybe-array
* @param {*} defaultValue the value if arg is falsey not defined
* @return {*} the arg or it's first item
*/
function unwrapArray(arg, defaultValue) {
var _arg = Array.isArray(arg) ? /* istanbul ignore next (preact) */arg[0] : arg;
if (!_arg && defaultValue) {
return defaultValue;
} else {
return _arg;
}
}
var classCallCheck = function (instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
};
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 inherits = function (subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
};
var objectWithoutProperties = function (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;
};
var possibleConstructorReturn = function (self, call) {
if (!self) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return call && (typeof call === "object" || typeof call === "function") ? call : self;
};
var ControlledTabs = function (_React$Component) {
inherits(ControlledTabs, _React$Component);
function ControlledTabs() {
var _temp, _this, _ret;
classCallCheck(this, ControlledTabs);
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _ret = (_temp = (_this = possibleConstructorReturn(this, _React$Component.call.apply(_React$Component, [this].concat(args))), _this), _this.tabs = [], _this.tabPanels = [], _this.id = _this.props.id || 'retabbed-' + generateId(), _this.focus = _this.props.defaultFocus, _this.clearTabs = function () {
_this.tabs = [];
}, _this.clearTabPanels = function () {
_this.tabPanels = [];
}, _this.handleClickAtTabIndex = function (index, event) {
event.preventDefault();
var _this$props = _this.props,
onSelect = _this$props.onSelect,
selectedIndex = _this$props.selectedIndex;
if (selectedIndex !== index) {
_this.focus = true;
onSelect(index, selectedIndex, event);
}
}, _this.handleKeyPressAtTabIndex = function (index, event) {
var _this$props2 = _this.props,
onSelect = _this$props2.onSelect,
selectedIndex = _this$props2.selectedIndex;
var keyCode = event.keyCode;
var _this2 = _this,
getPrevTab = _this2.getPrevTab,
getNextTab = _this2.getNextTab,
getFirstTab = _this2.getFirstTab,
getLastTab = _this2.getLastTab;
var fn = {
37: getPrevTab, // left
39: getNextTab, // right
38: getPrevTab, // up
40: getNextTab, // down
36: getFirstTab, // home
35: getLastTab // end
}[keyCode];
if (fn) {
_this.focus = true;
event.preventDefault();
var newSelectedIndex = fn(index, event);
onSelect && onSelect(newSelectedIndex, selectedIndex, event);
}
}, _this.getPrevTab = function (index) {
var i = index;
// Look for non-disabled tab from index to first tab on the left
while (i--) {
if (!_this.getTab(i).disabled) {
return i;
}
}
// If no tab found, continue searching from last tab on right to index
i = _this.getTabsCount();
while (i-- > index + 1) {
if (!_this.getTab(i).disabled) {
return i;
}
}
// No tabs are enabled, return index
// istanbul ignore next
return index;
}, _this.getNextTab = function (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 (!_this.getTab(i).disabled) {
return i;
}
}
// If no tab found, continue searching from first on left to index
for (var _i = 0; _i < index; _i++) {
if (!_this.getTab(_i).disabled) {
return _i;
}
}
// No tabs are enabled, return index
return index;
}, _this.getFirstTab = function () {
var count = _this.getTabsCount();
// Look for non disabled tab from the first tab
for (var i = 0; i < count; i++) {
// istanbul ignore else
if (!_this.getTab(i).disabled) {
return i;
}
}
// istanbul ignore next
return null;
}, _this.getLastTab = function () {
var i = _this.getTabsCount();
// Look for non disabled tab from the last tab
while (i--) {
if (!_this.getTab(i).disabled) {
return i;
}
}
// istanbul ignore next
return null;
}, _this.getTabListProps = function () {
var rest = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
return _extends({
role: 'tablist'
}, rest);
}, _this.getTabPanelProps = function () {
var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var id = _ref.id,
tabId = _ref.tabId,
_index = _ref.index,
rest = objectWithoutProperties(_ref, ['id', 'tabId', 'index']);
var item = {};
var index = _index;
if (index === undefined) {
_this.tabPanels.push(item);
index = _this.tabPanels.indexOf(item);
} else {
_this.tabPanels[index] = item;
}
return _extends({
role: 'tabpanel',
id: id || _this.id + '-tabpanel-' + index,
'aria-hidden': index !== _this.props.selectedIndex,
'aria-labelledby': tabId || _this.id + '-tab-' + index
}, rest);
}, _this.getTabProps = function () {
var _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var id = _ref2.id,
panelId = _ref2.panelId,
_index = _ref2.index,
onKeyDown = _ref2.onKeyDown,
onClick = _ref2.onClick,
rest = objectWithoutProperties(_ref2, ['id', 'panelId', 'index', 'onKeyDown', 'onClick']);
var selectedIndex = _this.props.selectedIndex;
var index = _index;
var item = { disabled: rest.disabled };
if (index === undefined) {
_this.tabs.push(item);
index = _this.tabs.indexOf(item);
} else {
_this.tabs[index] = item;
}
var selected = !rest.disabled && selectedIndex === index;
var eventHandlers = selected ? {
onKeyDown: callAllEventHandlers(onKeyDown, _this.handleKeyPressAtTabIndex.bind(_this, index))
} : {};
if (!rest.disabled) {
eventHandlers.onClick = callAllEventHandlers(onClick, _this.handleClickAtTabIndex.bind(_this, index));
}
// eslint-disable-next-line
var tabIndex = selected ? { tabIndex: 0 } : !rest.disabled ? { tabIndex: -1 } : {};
return _extends({
role: 'tab',
id: id || _this.id + '-tab-' + index,
focus: selected && _this.focus,
'aria-selected': selected,
'aria-disabled': rest.disabled,
'aria-controls': panelId || _this.id + '-tab-' + index
}, eventHandlers, tabIndex, rest);
}, _temp), possibleConstructorReturn(_this, _ret);
}
ControlledTabs.prototype.getTab = function getTab(i) {
return this.tabs[i];
};
ControlledTabs.prototype.getTabsCount = function getTabsCount() {
return this.tabs.length;
};
ControlledTabs.prototype.getStateAndHelpers = function getStateAndHelpers() {
var id = this.id,
getTabListProps = this.getTabListProps,
getTabPanelProps = this.getTabPanelProps,
getTabProps = this.getTabProps;
var selectedIndex = this.props.selectedIndex;
return {
id: id,
getTabListProps: getTabListProps,
getTabPanelProps: getTabPanelProps,
getTabProps: getTabProps,
selectedIndex: selectedIndex
};
};
ControlledTabs.prototype.render = function render() {
var children = unwrapArray(this.props.children, noop);
// because the tabs are rerendered every time we call the children
// we clear this out each render and it will be populated again as
// getTabProps is called.
this.clearTabs();
// because the tabs are rerendered every time we call the children
// we clear this out each render and it will be populated again as
// getTabPanelProps is called.
this.clearTabPanels();
var element = unwrapArray(children(this.getStateAndHelpers()));
return element;
};
return ControlledTabs;
}(React.Component);
process.env.NODE_ENV !== "production" ? ControlledTabs.propTypes = {
id: PropTypes.string,
defaultFocus: PropTypes.bool,
children: PropTypes.func.isRequired,
onSelect: PropTypes.func.isRequired,
selectedIndex: PropTypes.number.isRequired
} : void 0;
var UncontrolledTabs = function (_React$Component) {
inherits(UncontrolledTabs, _React$Component);
function UncontrolledTabs() {
var _temp, _this, _ret;
classCallCheck(this, UncontrolledTabs);
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _ret = (_temp = (_this = possibleConstructorReturn(this, _React$Component.call.apply(_React$Component, [this].concat(args))), _this), _this.state = {
selectedIndex: _this.props.defaultIndex
}, _this.handleSelect = function (index, prevIndex, event) {
var onSelect = _this.props.onSelect;
onSelect(index, prevIndex, event);
_this.setState({
selectedIndex: index
});
}, _temp), possibleConstructorReturn(_this, _ret);
}
UncontrolledTabs.prototype.render = function render() {
return React.createElement(ControlledTabs, _extends({}, this.props, {
onSelect: this.handleSelect,
selectedIndex: this.state.selectedIndex
}));
};
return UncontrolledTabs;
}(React.Component);
process.env.NODE_ENV !== "production" ? UncontrolledTabs.propTypes = {
id: PropTypes.string,
defaultFocus: PropTypes.bool,
children: PropTypes.func.isRequired,
onSelect: PropTypes.func.isRequired,
defaultIndex: PropTypes.number
} : void 0;
UncontrolledTabs.defaultProps = {
defaultIndex: 0
};
var Tabs = function (_React$Component) {
inherits(Tabs, _React$Component);
function Tabs() {
classCallCheck(this, Tabs);
return possibleConstructorReturn(this, _React$Component.apply(this, arguments));
}
Tabs.inUncontrolledMode = function inUncontrolledMode(props) {
return typeof props.selectedIndex === 'undefined';
};
Tabs.prototype.componentDidUpdate = function componentDidUpdate(prevProps) {
if (process.env.NODE_ENV !== 'production' && this.uncontrolledMode !== Tabs.inUncontrolledMode(prevProps)) {
throw new Error('Switching between controlled mode (by using `selectedIndex`) and uncontrolled mode is not supported in `Tabs`.\nFor more information about controlled and uncontrolled mode of react-tabs see the README.');
}
};
Tabs.prototype.render = function render() {
this.uncontrolledMode = Tabs.inUncontrolledMode(this.props);
return this.uncontrolledMode ? React.createElement(UncontrolledTabs, this.props) : React.createElement(ControlledTabs, this.props);
};
return Tabs;
}(React.Component);
process.env.NODE_ENV !== "production" ? Tabs.propTypes = {
id: PropTypes.string,
defaultFocus: PropTypes.bool,
children: PropTypes.func.isRequired,
onSelect: PropTypes.func,
defaultIndex: PropTypes.number,
selectedIndex: PropTypes.number
} : void 0;
Tabs.defaultProps = {
onSelect: noop
};
function unwrapExports (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
function createCommonjsModule(fn, module) {
return module = { exports: {} }, fn(module, module.exports), module.exports;
}
var getDisplayName_1 = createCommonjsModule(function (module, exports) {
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = getDisplayName;
function getDisplayName(Component) {
return Component.displayName || Component.name || (typeof Component === 'string' && Component.length > 0 ? Component : 'Unknown');
}
});
var getDisplayName = unwrapExports(getDisplayName_1);
var focusablePropTypes = {
focus: PropTypes.bool
};
function createFocusableComponent(BaseComponent) {
var refKey = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'ref';
var FocusableComponent = function (_React$Component) {
inherits(FocusableComponent, _React$Component);
function FocusableComponent() {
var _temp, _this, _ret;
classCallCheck(this, FocusableComponent);
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _ret = (_temp = (_this = possibleConstructorReturn(this, _React$Component.call.apply(_React$Component, [this].concat(args))), _this), _this.node = null, _this.setRef = function (ref) {
_this.node = ref;
}, _temp), possibleConstructorReturn(_this, _ret);
}
FocusableComponent.prototype.checkFocus = function checkFocus() {
if (this.props.focus) {
this.node.focus();
}
};
FocusableComponent.prototype.componentDidMount = function componentDidMount() {
this.checkFocus();
};
FocusableComponent.prototype.componentDidUpdate = function componentDidUpdate() {
this.checkFocus();
};
FocusableComponent.prototype.render = function render() {
var _refProps;
var _props = this.props,
focus = _props.focus,
props = objectWithoutProperties(_props, ['focus']);
var refProps = (_refProps = {}, _refProps[refKey] = this.setRef, _refProps);
return React.createElement(BaseComponent, _extends({}, refProps, props));
};
return FocusableComponent;
}(React.Component);
process.env.NODE_ENV !== "production" ? FocusableComponent.propTypes = focusablePropTypes : void 0;
FocusableComponent.displayName = 'Focusable(' + getDisplayName(BaseComponent);
return FocusableComponent;
}
export default Tabs;
export { createFocusableComponent, resetIdCounter };