Skip to content

Commit

Permalink
feat(tab): swipeable
Browse files Browse the repository at this point in the history
  • Loading branch information
webyom committed Sep 11, 2020
1 parent ee63e32 commit fc48e51
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 2 deletions.
10 changes: 10 additions & 0 deletions src/tab/demo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,16 @@ export class TabRouteComponent extends preact.Component {
</Tabs>
</section>

<section>
<h2>Swipeable</h2>
<Tabs animated swipeable>
<Tab title="Tab 1">content of tab 1</Tab>
<Tab title="Tab 2">content of tab 2</Tab>
<Tab title="Tab 3">content of tab 3</Tab>
<Tab title="Tab 4">content of tab 4</Tab>
</Tabs>
</section>

<section>
<h2>Info Tab</h2>
<Tabs>
Expand Down
32 changes: 30 additions & 2 deletions src/tab/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ import { createBEM } from '../utils/bem';
import { BORDER_TOP_BOTTOM } from '../utils/constant';
import { scrollLeftTo, setRootScrollTop, getElementTop } from '../utils/scroll';
import { isDef, addUnit } from '../utils';
import { TouchHandler } from '../utils/touch-handler';
import { Sticky } from '../sticky';
import { Title, TitleParentProps, TitleProps } from './title';
import './index.scss';

const MIN_SWIPE_DISTANCE = 50;

export type TabsProps = TitleParentProps & {
activeIndex?: number | string;
activeName?: string;
bordered?: boolean;
animated?: boolean;
swipeable?: boolean;
sticky?: boolean;
duration?: number | string;
lineHeight?: number | string;
Expand Down Expand Up @@ -40,8 +44,10 @@ const bem = createBEM('pant-tabs');
export class Tabs extends preact.Component<TabsProps, TabsState> {
private containerRef = preact.createRef<HTMLDivElement>();
private tabListRef = preact.createRef<HTMLDivElement>();
private contentRef = preact.createRef<HTMLDivElement>();
private lineRef = preact.createRef<HTMLDivElement>();
private stickyRef = preact.createRef<Sticky>();
private touchHandler: TouchHandler;

constructor(props: preact.RenderableProps<TabsProps>) {
super(props);
Expand All @@ -58,7 +64,7 @@ export class Tabs extends preact.Component<TabsProps, TabsState> {
}

componentDidMount(): void {
const { activeName, children } = this.props;
const { activeName, children, swipeable } = this.props;
if (activeName) {
const activeIndex = [].concat(children).findIndex(item => item.props.name === activeName);
if (activeIndex >= 0) {
Expand All @@ -67,13 +73,35 @@ export class Tabs extends preact.Component<TabsProps, TabsState> {
}
this.updateLine(true);
this.scrollIntoView(true);
if (swipeable) {
this.touchHandler = new TouchHandler(this.contentRef.current, { onTouchEnd: this.onTouchEnd.bind(this) });
}
}

componentDidUpdate(): void {
this.updateLine();
this.scrollIntoView();
}

componentWillUnmount(): void {
if (this.props.swipeable) {
this.touchHandler.destroy();
this.touchHandler = null;
}
}

private onTouchEnd(): void {
const { activeIndex } = this.state;
const { direction, offsetX, deltaX } = this.touchHandler.state;
if (direction === 'horizontal' && offsetX >= MIN_SWIPE_DISTANCE) {
if (deltaX > 0 && activeIndex !== 0) {
this.setActiveIndex(activeIndex - 1);
} else if (deltaX < 0 && activeIndex !== [].concat(this.props.children).length - 1) {
this.setActiveIndex(activeIndex + 1);
}
}
}

private scrollIntoView(immediate?: boolean): void {
const { type, scrollable, duration } = this.props;
if (type !== 'line' || !scrollable) {
Expand Down Expand Up @@ -223,7 +251,7 @@ export class Tabs extends preact.Component<TabsProps, TabsState> {
}
});
const wrap = (
<div class={bem('content', { animated: animated })}>
<div ref={this.contentRef} class={bem('content', { animated: animated })}>
{animated ? this.warpAnimatedContents(contents) : contents}
</div>
);
Expand Down
112 changes: 112 additions & 0 deletions src/utils/touch-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
const MIN_DISTANCE = 10;

type TouchHandlerOptions = {
onTouchStart?(event: TouchEvent): void;
onTouchMove?(event: TouchEvent): void;
onTouchEnd?(event: TouchEvent): void;
onTouchCancel?(event: TouchEvent): void;
};

type TouchHandlerState = {
startX: number;
startY: number;
deltaX: number;
deltaY: number;
offsetX: number;
offsetY: number;
direction: string;
};

export class TouchHandler {
private el: HTMLElement;
private opt: TouchHandlerOptions;
private startX = 0;
private startY = 0;
private deltaX = 0;
private deltaY = 0;
private offsetX = 0;
private offsetY = 0;
private direction = '';

constructor(el: HTMLElement, opt?: TouchHandlerOptions) {
this.el = el;
this.opt = opt || {};
this.onTouchStart = this.onTouchStart.bind(this);
this.onTouchMove = this.onTouchMove.bind(this);
this.onTouchEnd = this.onTouchEnd.bind(this);
this.onTouchCancel = this.onTouchCancel.bind(this);
this.bindTouchEvent();
}

private getDirection(x: number, y: number): string {
if (x > y && x > MIN_DISTANCE) {
return 'horizontal';
}

if (y > x && y > MIN_DISTANCE) {
return 'vertical';
}

return '';
}

private onTouchStart(event: TouchEvent): void {
this.resetTouchStatus();
this.startX = event.touches[0].clientX;
this.startY = event.touches[0].clientY;
this.opt.onTouchStart && this.opt.onTouchStart(event);
}

private onTouchMove(event: TouchEvent): void {
const touch = event.touches[0];
this.deltaX = touch.clientX - this.startX;
this.deltaY = touch.clientY - this.startY;
this.offsetX = Math.abs(this.deltaX);
this.offsetY = Math.abs(this.deltaY);
this.direction = this.direction || this.getDirection(this.offsetX, this.offsetY);
this.opt.onTouchMove && this.opt.onTouchMove(event);
}

private onTouchEnd(event: TouchEvent): void {
this.opt.onTouchEnd && this.opt.onTouchEnd(event);
}

private onTouchCancel(event: TouchEvent): void {
this.opt.onTouchCancel && this.opt.onTouchCancel(event);
}

private resetTouchStatus(): void {
this.direction = '';
this.deltaX = 0;
this.deltaY = 0;
this.offsetX = 0;
this.offsetY = 0;
}

private bindTouchEvent(): void {
this.el.addEventListener('touchstart', this.onTouchStart, false);
this.el.addEventListener('touchmove', this.onTouchMove, false);
this.el.addEventListener('touchend', this.onTouchEnd, false);
this.el.addEventListener('touchcancel', this.onTouchCancel, false);
}

get state(): TouchHandlerState {
return {
startX: this.startX,
startY: this.startY,
deltaX: this.deltaX,
deltaY: this.deltaY,
offsetX: this.offsetX,
offsetY: this.offsetY,
direction: this.direction,
};
}

destroy(): void {
this.el.removeEventListener('touchstart', this.onTouchStart);
this.el.removeEventListener('touchmove', this.onTouchMove);
this.el.removeEventListener('touchend', this.onTouchEnd);
this.el.removeEventListener('touchcancel', this.onTouchCancel);
this.el = null;
}
}

0 comments on commit fc48e51

Please sign in to comment.