react-toolbox/components/tabs/Tabs.js

222 lines
6.8 KiB
JavaScript
Raw Normal View History

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
const factory = (Tab, TabContent, FontIcon) => {
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,
fixed: PropTypes.bool,
hideMode: PropTypes.oneOf(['display', 'unmounted']),
2016-05-31 00:23:55 +03:00
index: PropTypes.number,
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 = {
index: 0,
fixed: false,
inverse: false,
2017-01-26 20:05:32 +03:00
hideMode: 'unmounted',
2016-05-31 00:23:55 +03:00
};
state = {
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() {
window.addEventListener('resize', this.handleResize);
this.handleResize();
2016-05-31 00:23:55 +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() {
window.removeEventListener('resize', this.handleResize);
clearTimeout(this.resizeTimeout);
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 = () => {
if (this.resizeTimeout) clearTimeout(this.resizeTimeout);
this.resizeTimeout = setTimeout(() => {
this.updatePointer(this.props.index);
this.updateArrows();
}, 100);
};
2017-01-26 20:05:32 +03:00
updatePointer = (idx) => {
if (this.navigationNode && this.navigationNode.children[idx]) {
this.updatePointerAnimationFrame = requestAnimationFrame(() => {
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`,
},
});
});
}
}
updateArrows = () => {
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();
this.setState({
arrows: {
left: scrollLeft > 0,
2017-01-26 20:05:32 +03:00
right: nav.right < (lastLabel.right - 5),
},
});
}
}
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);
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) => {
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
}
} 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,
}));
return this.props.hideMode === 'display'
? contentElements
: contentElements.filter((item, idx) => (idx === this.props.index));
2016-05-31 00:23:55 +03:00
}
2017-01-26 20:05:32 +03:00
render() {
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();
const classNamePointer = classnames(theme.pointer, {
2017-01-26 20:05:32 +03:00
[theme.disableAnimation]: disableAnimatedBottomBorder,
});
const classNames = classnames(theme.tabs, {
[theme.fixed]: fixed,
2017-01-26 20:05:32 +03:00
[theme.inverse]: inverse,
}, 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}>
<div className={theme.navigationContainer}>
{hasLeftArrow && <div className={theme.arrowContainer} onClick={this.scrollRight}>
<FontIcon className={theme.arrow} value="keyboard_arrow_left" />
</div>}
<div className={theme.navigation} role="tablist" ref={(node) => { this.navigationNode = node; }}>
{this.renderHeaders(headers)}
<span className={classNamePointer} style={this.state.pointer} />
</div>
{hasRightArrow && <div className={theme.arrowContainer} onClick={this.scrollLeft}>
<FontIcon className={theme.arrow} value="keyboard_arrow_right" />
</div>}
</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
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 };