2017-04-17 17:14:17 +03:00
|
|
|
import React, { Component } from 'react';
|
|
|
|
import PropTypes from 'prop-types';
|
2016-05-26 22:01:54 +03:00
|
|
|
import classnames from 'classnames';
|
|
|
|
import { themr } from 'react-css-themr';
|
2017-01-26 20:05:32 +03:00
|
|
|
import { TABS } from '../identifiers';
|
|
|
|
import InjectFontIcon from '../font_icon/FontIcon';
|
|
|
|
import isComponentOfType from '../utils/is-component-of-type';
|
|
|
|
import InjectTab from './Tab';
|
|
|
|
import InjectTabContent from './TabContent';
|
2016-05-31 00:23:55 +03:00
|
|
|
|
2016-11-22 11:45:38 +03:00
|
|
|
const factory = (Tab, TabContent, FontIcon) => {
|
2017-01-24 13:51:30 +03:00
|
|
|
const isTab = child => isComponentOfType(Tab, child);
|
|
|
|
const isTabContent = child => isComponentOfType(TabContent, child);
|
|
|
|
|
2016-05-31 00:23:55 +03:00
|
|
|
class Tabs extends Component {
|
|
|
|
static propTypes = {
|
|
|
|
children: PropTypes.node,
|
|
|
|
className: PropTypes.string,
|
|
|
|
disableAnimatedBottomBorder: PropTypes.bool,
|
2016-08-12 05:55:45 +03:00
|
|
|
fixed: PropTypes.bool,
|
2016-08-30 23:08:22 +03:00
|
|
|
hideMode: PropTypes.oneOf(['display', 'unmounted']),
|
2016-05-31 00:23:55 +03:00
|
|
|
index: PropTypes.number,
|
2016-08-12 05:55:45 +03:00
|
|
|
inverse: PropTypes.bool,
|
2016-05-31 00:23:55 +03:00
|
|
|
onChange: PropTypes.func,
|
|
|
|
theme: PropTypes.shape({
|
2016-12-19 22:39:07 +03:00
|
|
|
arrow: PropTypes.string,
|
|
|
|
arrowContainer: PropTypes.string,
|
|
|
|
disableAnimation: PropTypes.string,
|
2016-08-12 06:31:08 +03:00
|
|
|
fixed: PropTypes.string,
|
|
|
|
inverse: PropTypes.string,
|
2016-05-31 00:23:55 +03:00
|
|
|
navigation: PropTypes.string,
|
2016-12-19 22:39:07 +03:00
|
|
|
navigationContainer: PropTypes.string,
|
2016-05-31 00:23:55 +03:00
|
|
|
pointer: PropTypes.string,
|
2017-01-26 20:05:32 +03:00
|
|
|
tabs: PropTypes.string,
|
|
|
|
}),
|
2016-05-31 00:23:55 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
static defaultProps = {
|
2016-08-12 05:55:45 +03:00
|
|
|
index: 0,
|
|
|
|
fixed: false,
|
2016-08-30 23:08:22 +03:00
|
|
|
inverse: false,
|
2017-01-26 20:05:32 +03:00
|
|
|
hideMode: 'unmounted',
|
2016-05-31 00:23:55 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
state = {
|
2016-11-17 20:05:14 +03:00
|
|
|
pointer: {},
|
2017-01-26 20:05:32 +03:00
|
|
|
arrows: {},
|
2016-05-31 00:23:55 +03:00
|
|
|
};
|
|
|
|
|
2017-01-26 20:05:32 +03:00
|
|
|
componentDidMount() {
|
2016-08-12 05:55:45 +03:00
|
|
|
window.addEventListener('resize', this.handleResize);
|
|
|
|
this.handleResize();
|
2016-05-31 00:23:55 +03:00
|
|
|
}
|
2016-03-16 20:49:12 +03:00
|
|
|
|
2017-04-02 15:08:25 +03:00
|
|
|
componentDidUpdate(prevProps) {
|
|
|
|
const { index, children } = this.props;
|
|
|
|
const { index: prevIndex, children: prevChildren } = prevProps;
|
|
|
|
|
|
|
|
if (index !== prevIndex || children !== prevChildren) {
|
|
|
|
this.updatePointer(index);
|
|
|
|
}
|
2016-05-31 00:23:55 +03:00
|
|
|
}
|
2015-09-19 19:48:09 +03:00
|
|
|
|
2017-01-26 20:05:32 +03:00
|
|
|
componentWillUnmount() {
|
2016-08-12 05:55:45 +03:00
|
|
|
window.removeEventListener('resize', this.handleResize);
|
|
|
|
clearTimeout(this.resizeTimeout);
|
2017-08-02 19:11:37 +03:00
|
|
|
if (this.updatePointerAnimationFrame) cancelAnimationFrame(this.updatePointerAnimationFrame);
|
2016-05-31 00:23:55 +03:00
|
|
|
}
|
2015-11-13 03:01:27 +03:00
|
|
|
|
2017-01-10 21:14:55 +03:00
|
|
|
handleHeaderClick = (idx) => {
|
|
|
|
if (this.props.onChange) {
|
|
|
|
this.props.onChange(idx);
|
|
|
|
}
|
2016-05-31 00:23:55 +03:00
|
|
|
};
|
|
|
|
|
2016-08-12 06:31:08 +03:00
|
|
|
handleResize = () => {
|
2016-11-22 11:45:38 +03:00
|
|
|
if (this.resizeTimeout) clearTimeout(this.resizeTimeout);
|
|
|
|
this.resizeTimeout = setTimeout(() => {
|
|
|
|
this.updatePointer(this.props.index);
|
|
|
|
this.updateArrows();
|
|
|
|
}, 100);
|
2016-08-12 05:55:45 +03:00
|
|
|
};
|
|
|
|
|
2017-01-26 20:05:32 +03:00
|
|
|
updatePointer = (idx) => {
|
2016-11-22 11:45:38 +03:00
|
|
|
if (this.navigationNode && this.navigationNode.children[idx]) {
|
2017-11-02 19:36:57 +03:00
|
|
|
this.updatePointerAnimationFrame = window.requestAnimationFrame(() => {
|
2017-04-02 15:08:25 +03:00
|
|
|
const nav = this.navigationNode.getBoundingClientRect();
|
|
|
|
const label = this.navigationNode.children[idx].getBoundingClientRect();
|
|
|
|
const scrollLeft = this.navigationNode.scrollLeft;
|
|
|
|
this.setState({
|
|
|
|
pointer: {
|
|
|
|
top: `${nav.height}px`,
|
|
|
|
left: `${(label.left + scrollLeft) - nav.left}px`,
|
|
|
|
width: `${label.width}px`,
|
|
|
|
},
|
|
|
|
});
|
2016-11-22 11:45:38 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
updateArrows = () => {
|
2017-01-11 21:45:46 +03:00
|
|
|
const idx = this.navigationNode.children.length - 2;
|
|
|
|
|
|
|
|
if (idx >= 0) {
|
|
|
|
const scrollLeft = this.navigationNode.scrollLeft;
|
|
|
|
const nav = this.navigationNode.getBoundingClientRect();
|
|
|
|
const lastLabel = this.navigationNode.children[idx].getBoundingClientRect();
|
|
|
|
|
2018-02-26 20:29:58 +03:00
|
|
|
this.setState({
|
|
|
|
arrows: {
|
|
|
|
left: scrollLeft > 0,
|
|
|
|
right: nav.right < (lastLabel.right - 5),
|
|
|
|
},
|
|
|
|
});
|
2017-01-11 21:45:46 +03:00
|
|
|
}
|
2016-11-22 11:45:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
scrollNavigation = (factor) => {
|
|
|
|
const oldScrollLeft = this.navigationNode.scrollLeft;
|
|
|
|
this.navigationNode.scrollLeft += factor * this.navigationNode.clientWidth;
|
|
|
|
if (this.navigationNode.scrollLeft !== oldScrollLeft) {
|
|
|
|
this.updateArrows();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
scrollRight = () =>
|
|
|
|
this.scrollNavigation(-1);
|
|
|
|
|
|
|
|
scrollLeft = () =>
|
|
|
|
this.scrollNavigation(+1);
|
2016-08-12 05:55:45 +03:00
|
|
|
|
2017-01-26 20:05:32 +03:00
|
|
|
parseChildren() {
|
2016-05-31 00:23:55 +03:00
|
|
|
const headers = [];
|
|
|
|
const contents = [];
|
|
|
|
|
|
|
|
React.Children.forEach(this.props.children, (item) => {
|
2017-01-24 13:51:30 +03:00
|
|
|
if (isTab(item)) {
|
2016-05-31 00:23:55 +03:00
|
|
|
headers.push(item);
|
|
|
|
if (item.props.children) {
|
2017-01-26 20:05:32 +03:00
|
|
|
contents.push(
|
|
|
|
<TabContent theme={this.props.theme}>
|
|
|
|
{item.props.children}
|
|
|
|
</TabContent>,
|
|
|
|
);
|
2016-05-31 00:23:55 +03:00
|
|
|
}
|
2017-01-24 13:51:30 +03:00
|
|
|
} else if (isTabContent(item)) {
|
2016-05-31 00:23:55 +03:00
|
|
|
contents.push(item);
|
2015-11-13 03:01:27 +03:00
|
|
|
}
|
2016-05-31 00:23:55 +03:00
|
|
|
});
|
2015-10-11 23:27:59 +03:00
|
|
|
|
2017-01-26 20:05:32 +03:00
|
|
|
return { headers, contents };
|
2016-05-31 00:23:55 +03:00
|
|
|
}
|
2015-10-11 23:27:59 +03:00
|
|
|
|
2017-01-26 20:05:32 +03:00
|
|
|
renderHeaders(headers) {
|
|
|
|
return headers.map((item, idx) => React.cloneElement(item, {
|
|
|
|
children: null,
|
|
|
|
key: idx, // eslint-disable-line
|
|
|
|
index: idx,
|
|
|
|
theme: this.props.theme,
|
|
|
|
active: this.props.index === idx,
|
|
|
|
onClick: (event, index) => {
|
|
|
|
this.handleHeaderClick(index);
|
|
|
|
if (item.props.onClick) item.props.onClick(event);
|
|
|
|
},
|
|
|
|
}));
|
2016-05-31 00:23:55 +03:00
|
|
|
}
|
2015-09-19 19:48:09 +03:00
|
|
|
|
2017-01-26 20:05:32 +03:00
|
|
|
renderContents(contents) {
|
|
|
|
const contentElements = contents.map((item, idx) => React.cloneElement(item, {
|
|
|
|
key: idx, // eslint-disable-line
|
|
|
|
theme: this.props.theme,
|
|
|
|
active: this.props.index === idx,
|
|
|
|
hidden: this.props.index !== idx && this.props.hideMode === 'display',
|
|
|
|
tabIndex: idx,
|
|
|
|
}));
|
2016-08-30 23:08:22 +03:00
|
|
|
|
2016-11-22 11:45:38 +03:00
|
|
|
return this.props.hideMode === 'display'
|
|
|
|
? contentElements
|
|
|
|
: contentElements.filter((item, idx) => (idx === this.props.index));
|
2016-05-31 00:23:55 +03:00
|
|
|
}
|
2016-03-28 18:29:25 +03:00
|
|
|
|
2017-01-26 20:05:32 +03:00
|
|
|
render() {
|
2016-11-22 11:45:38 +03:00
|
|
|
const { className, disableAnimatedBottomBorder, theme, fixed, inverse } = this.props;
|
|
|
|
const { left: hasLeftArrow, right: hasRightArrow } = this.state.arrows;
|
2016-05-31 00:23:55 +03:00
|
|
|
const { headers, contents } = this.parseChildren();
|
2016-11-22 11:45:38 +03:00
|
|
|
const classNamePointer = classnames(theme.pointer, {
|
2017-01-26 20:05:32 +03:00
|
|
|
[theme.disableAnimation]: disableAnimatedBottomBorder,
|
2016-11-22 11:45:38 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
const classNames = classnames(theme.tabs, {
|
|
|
|
[theme.fixed]: fixed,
|
2017-01-26 20:05:32 +03:00
|
|
|
[theme.inverse]: inverse,
|
2016-11-22 11:45:38 +03:00
|
|
|
}, className);
|
|
|
|
|
2016-05-31 00:23:55 +03:00
|
|
|
return (
|
2017-01-26 20:05:32 +03:00
|
|
|
<div data-react-toolbox="tabs" className={classNames}>
|
2016-11-17 20:05:14 +03:00
|
|
|
<div className={theme.navigationContainer}>
|
2016-11-22 11:45:38 +03:00
|
|
|
{hasLeftArrow && <div className={theme.arrowContainer} onClick={this.scrollRight}>
|
|
|
|
<FontIcon className={theme.arrow} value="keyboard_arrow_left" />
|
|
|
|
</div>}
|
2017-08-02 19:30:41 +03:00
|
|
|
<div className={theme.navigation} role="tablist" ref={(node) => { this.navigationNode = node; }}>
|
2016-11-17 20:05:14 +03:00
|
|
|
{this.renderHeaders(headers)}
|
2016-11-22 11:45:38 +03:00
|
|
|
<span className={classNamePointer} style={this.state.pointer} />
|
2017-08-02 19:30:41 +03:00
|
|
|
</div>
|
2016-11-22 11:45:38 +03:00
|
|
|
{hasRightArrow && <div className={theme.arrowContainer} onClick={this.scrollLeft}>
|
|
|
|
<FontIcon className={theme.arrow} value="keyboard_arrow_right" />
|
|
|
|
</div>}
|
2016-11-17 20:05:14 +03:00
|
|
|
</div>
|
2016-05-31 00:23:55 +03:00
|
|
|
{this.renderContents(contents)}
|
|
|
|
</div>
|
|
|
|
);
|
2016-04-10 20:08:21 +03:00
|
|
|
}
|
2015-11-13 03:01:27 +03:00
|
|
|
}
|
2015-09-19 19:48:09 +03:00
|
|
|
|
2016-05-31 00:23:55 +03:00
|
|
|
return Tabs;
|
|
|
|
};
|
2015-10-22 02:31:17 +03:00
|
|
|
|
2016-11-22 11:45:38 +03:00
|
|
|
const Tabs = factory(InjectTab, InjectTabContent, InjectFontIcon);
|
2016-05-31 00:23:55 +03:00
|
|
|
export default themr(TABS)(Tabs);
|
|
|
|
export { factory as tabsFactory };
|
|
|
|
export { Tabs };
|