Прокручиваемые табы меню
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
const Pane = ({ className, children, key }) => (
<div className={`${className}`}>
{children}
</div>
);
Pane.propTypes = {
className: PropTypes.string.isRequired,
children: PropTypes.element.isRequired,
};
Pane.dafaultTypes = {
};
const StyledLink = styled(Pane)`
`;
export default StyledLink;
import React from 'react';
import PropTypes from 'prop-types';
import { NavLink } from 'react-router-dom';
import styled from 'styled-components';
import { Tabs } from 'components/Tabs';
import { debounce } from 'helpers';
import { COLOR, FONT } from '../../global.variables';
class ScrollableTabs extends Tabs {
static propTypes = {
className: PropTypes.string.isRequired,
active: PropTypes.number,
history: PropTypes.bool,
children: PropTypes.oneOfType([ PropTypes.array, PropTypes.element ]).isRequired,
};
static defaultProps = {
active: 1,
history: false,
};
static getTabsWidth(tabsWrap) {
let tabsWidth = 0;
const tabs = tabsWrap.children;
for (let i = 0; i < tabs.length; i += 1) {
const tab = tabs[i];
tabsWidth += tab.getBoundingClientRect().width;
}
return tabsWidth;
}
static getFirstTabWidth(tabsWrap) {
return tabsWrap.children[0].getBoundingClientRect().width;
}
static getLastTabWidth(tabsWrap) {
return tabsWrap.children[tabsWrap.children.length - 1].getBoundingClientRect().width;
}
state = {
active: this.props.active || 0,
xStartMove: 0, // координата начала движения курсора
xOffset: 0, // сдвиг курсора относительно кооринаты начала движения
xOffsetLastMouseUp: 0, // промежуточное хранилище последнего сдвига
offsetLimitPosition: 'start', // start, center, end
leftOffsetLimit: 0, // граница прокрутки слева
rightOffsetLimit: 0, // граница прокрутки справа
allowTabScroll: false, // разрешение прокрутки (зависит от ширины экрана)
};
componentDidMount() {
const { offsetLimitPosition } = this.state;
this.ODDS = 40; // прибавка к краю сдвига
this.clickableFlag = true; // разрешен ли клик по табу
this.movedFlag = false; // было ли выполнено прокручивание табов (было ли выполнено событие onMouseMove)
this.mouseMovingFlag = false; // отслеживание клика начала скроллинга
// this.allowDoubleTouch = true; // флаг предотвращения двойного срабатывания касания
this.getTabsData(this.navWrap);
this.checkScrollAllow(this.navWrap);
window.addEventListener('resize', debounce(() => {
this.checkScrollAllow(this.navWrap);
/* пересчитываем данные табов при ресайзе только для крайних выравниваний (start и end),
где в рассчетах используется ширина окна */
if (offsetLimitPosition === 'start' || offsetLimitPosition === 'end') {
this.getTabsData(this.navWrap);
}
}, 500));
}
getTabsData(tabsWrap) {
if (tabsWrap) {
const tabsWidth = ScrollableTabs.getTabsWidth(tabsWrap);
const firstTabWidth = ScrollableTabs.getFirstTabWidth(tabsWrap);
const lastTabWidth = ScrollableTabs.getLastTabWidth(tabsWrap);
const screenWidth = window.innerWidth;
let leftOffsetLimit = 0;
let rightOffsetLimit = 0;
switch (this.state.offsetLimitPosition) {
case 'start': // конец прокрутки при входе последнего таба в экран
leftOffsetLimit = ((tabsWidth - screenWidth) / 2) + this.ODDS;
rightOffsetLimit = ((tabsWidth - screenWidth) / 2) + this.ODDS;
break;
case 'center': // конец прокрутки - последний таб посередине экрана
leftOffsetLimit = tabsWidth - ((tabsWidth + lastTabWidth) / 2);
rightOffsetLimit = tabsWidth - ((tabsWidth + firstTabWidth) / 2);
break;
case 'end': // конец прокрутки перед выходом последнего таба за экран
leftOffsetLimit = tabsWidth + ((screenWidth - tabsWidth) / 2) - lastTabWidth;
rightOffsetLimit = tabsWidth + ((screenWidth - tabsWidth) / 2) - firstTabWidth;
break;
default:
leftOffsetLimit = ((tabsWidth - screenWidth) / 2) + this.ODDS;
rightOffsetLimit = ((tabsWidth - screenWidth) / 2) + this.ODDS;
break;
}
this.setState({
leftOffsetLimit,
rightOffsetLimit,
});
}
}
checkScrollAllow(tabsWrap) {
if (tabsWrap) {
const screenWidth = window.innerWidth;
const tabsWidth = ScrollableTabs.getTabsWidth(tabsWrap);
this.setState({ allowTabScroll: screenWidth < tabsWidth + this.ODDS });
if (screenWidth > tabsWidth + this.ODDS) {
this.setState({
xOffset: 0,
xOffsetLastMouseUp: 0,
});
}
}
}
mouseDownHandler(e) {
if (this.state.allowTabScroll) {
this.mouseMovingFlag = true;
let touchPosition;
if (e.touches) {
touchPosition = e.touches[0].screenX;
}
this.setState({
xStartMove: e.pageX || touchPosition,
});
}
this.clickableFlag = true;
}
mouseMoveHandler(e) {
if (this.mouseMovingFlag && this.state.allowTabScroll) {
let touchPosition;
if (e.touches) {
touchPosition = e.touches[0].screenX;
}
const clickTouchPosition = e.clientX || touchPosition;
const offsetFinal = (this.state.xOffsetLastMouseUp + clickTouchPosition) - this.state.xStartMove;
if (offsetFinal > -(this.state.leftOffsetLimit) &&
offsetFinal < this.state.rightOffsetLimit) {
this.setState({
xOffset: offsetFinal,
});
this.movedFlag = true;
}
}
}
mouseUpHandler(e) {
if (this.mouseMovingFlag) {
this.mouseMovingFlag = false;
this.setState({
xOffsetLastMouseUp: this.state.xOffset,
});
}
if (this.movedFlag) {
this.clickableFlag = false;
this.movedFlag = false;
} else {
const item = e.target;
if (item.getBoundingClientRect().x < 0) {
const actualOffset = (this.state.xOffset - item.getBoundingClientRect().x) + this.ODDS;
this.setState({
xOffset: actualOffset,
xOffsetLastMouseUp: actualOffset,
});
} else if ((item.getBoundingClientRect().x + item.getBoundingClientRect().width) > window.innerWidth) {
const actualOffset = this.state.xOffset - (item.getBoundingClientRect().x +
(item.getBoundingClientRect().width - window.innerWidth) + this.ODDS);
this.setState({
xOffset: actualOffset,
xOffsetLastMouseUp: actualOffset,
});
}
}
}
touchEndHandler(e) {
if (this.mouseMovingFlag) {
this.mouseMovingFlag = false;
this.setState({
xOffsetLastMouseUp: this.state.xOffset,
});
}
if (this.movedFlag) {
this.movedFlag = false;
} else {
const item = e.target;
if (item.getBoundingClientRect().x < 0) {
const actualOffset = (this.state.xOffset - item.getBoundingClientRect().x) + this.ODDS;
this.setState({
xOffset: actualOffset,
xOffsetLastMouseUp: actualOffset,
});
} else if ((item.getBoundingClientRect().x + item.getBoundingClientRect().width) > window.innerWidth) {
const actualOffset = this.state.xOffset - (item.getBoundingClientRect().x +
(item.getBoundingClientRect().width - window.innerWidth) + this.ODDS);
this.setState({
xOffset: actualOffset,
xOffsetLastMouseUp: actualOffset,
});
}
}
}
clickHandler() {
this.clickableFlag = true;
}
mouseLeaveHandler() {
if (this.mouseMovingFlag) {
this.mouseMovingFlag = false;
}
}
renderNav(children) {
if (this.props.history) {
return (
<div
className="nav"
ref={navWrap => {
this.navWrap = navWrap;
}}
style={{
transform: `translateX(${this.state.xOffset}px)`,
transition: `${this.movedFlag ? 'none' : 'transform 0.2s'}`,
}}
onMouseDown={e => {
// console.log('MOUSE DOWN')
e.stopPropagation();
e.preventDefault();
this.mouseDownHandler(e);
}}
onMouseUp={e => {
// console.log('MOUSE UP')
e.stopPropagation();
e.preventDefault();
this.mouseUpHandler(e);
}}
onMouseMove={e => {
// console.log('MOUSE MOVE')
e.stopPropagation();
e.preventDefault();
this.mouseMoveHandler(e);
}}
onClick={e => {
// console.log('MOUSE CLICK')
e.stopPropagation();
e.preventDefault();
this.clickHandler(e);
}}
onMouseLeave={e => {
// console.log('MOUSE LEAVE')
e.stopPropagation();
e.preventDefault();
this.mouseLeaveHandler(e);
}}
onTouchStart={e => {
// console.log('TOUCH START')
e.stopPropagation();
this.mouseDownHandler(e);
}}
onTouchEnd={e => {
// console.log('TOUCH END')
e.stopPropagation();
this.touchEndHandler(e);
}}
onTouchMove={e => {
// console.log('TOUCH MOVE')
e.stopPropagation();
this.mouseMoveHandler(e);
}}
>
{children.map((child, index) => (
<NavLink
className="nav-item"
key={child.props.name}
to={this.clickableFlag ? child.props.href : '#'}
activeClassName="active"
>
{child.props.label ? child.props.label : `Tab ${index + 1}`}
</NavLink>
))}
</div>
);
}
return (
<ul
className="nav"
ref={navWrap => {
this.navWrap = navWrap;
}}
style={{
transform: `translateX(${this.state.xOffset}px)`,
transition: `${this.movedFlag ? 'none' : 'transform 0.2s'}`,
}}
onMouseDown={e => {
// console.log('MOUSE DOWN')
e.stopPropagation();
e.preventDefault();
this.mouseDownHandler(e);
}}
onMouseUp={e => {
// console.log('MOUSE UP')
e.stopPropagation();
e.preventDefault();
this.mouseUpHandler(e);
}}
onMouseMove={e => {
// console.log('MOUSE MOVE')
e.stopPropagation();
e.preventDefault();
this.mouseMoveHandler(e);
}}
onClick={e => {
// console.log('MOUSE CLICK')
e.stopPropagation();
e.preventDefault();
this.clickHandler(e);
}}
onMouseLeave={e => {
// console.log('MOUSE LEAVE')
e.stopPropagation();
e.preventDefault();
this.mouseLeaveHandler(e);
}}
onTouchStart={e => {
// console.log('TOUCH START')
e.stopPropagation();
this.mouseDownHandler(e);
}}
onTouchEnd={e => {
// console.log('TOUCH END')
e.stopPropagation();
this.touchEndHandler(e);
}}
onTouchMove={e => {
// console.log('TOUCH MOVE')
e.stopPropagation();
this.mouseMoveHandler(e);
}}
>
{children.map((child, index) => (
<li
className={`nav-item ${this.state.active === index && 'active'}`}
key={child.props.name}
onClick={() => this.setState({ active: index })}
>
{child.props.label ? child.props.label : `Tab${index + 1}`}
</li>
))}
</ul>
);
}
}
const StyledTabs = styled(ScrollableTabs)`
background: #ffffff;
width: 100%;
.tabs-content {
padding: 16px;
}
.nav-wrapper {
width: 100%;
overflow: hidden;
}
.nav-inner {
position: relative;
width: 100%;
background:#fff;
border-bottom: 1px solid ${COLOR.bg_dark};
}
.nav {
display: flex;
flex-direction: row;
justify-content: center;
list-style: none;
padding: 0 16px;
}
.nav-item {
padding: 26px 20px 20px;
margin: 0;
transition: border 0.3s ease-in-out;
border-bottom: 3px solid transparent;
line-height: 1;
font-size: 13px;
text-align: center;
white-space: nowrap;
color: ${COLOR.aux};
text-decoration: none;
cursor: pointer;
&.active {
border-bottom: 2px solid ${COLOR.crimson};
color: ${COLOR.main};
}
}
`;
export default StyledTabs;