diff --git a/src/tab/demo/index.tsx b/src/tab/demo/index.tsx index 0e272d5..545f6ac 100644 --- a/src/tab/demo/index.tsx +++ b/src/tab/demo/index.tsx @@ -142,6 +142,16 @@ export class TabRouteComponent extends preact.Component { +
+

Swipeable

+ + content of tab 1 + content of tab 2 + content of tab 3 + content of tab 4 + +
+

Info Tab

diff --git a/src/tab/tabs.tsx b/src/tab/tabs.tsx index bf77b0d..a125ca0 100644 --- a/src/tab/tabs.tsx +++ b/src/tab/tabs.tsx @@ -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; @@ -40,8 +44,10 @@ const bem = createBEM('pant-tabs'); export class Tabs extends preact.Component { private containerRef = preact.createRef(); private tabListRef = preact.createRef(); + private contentRef = preact.createRef(); private lineRef = preact.createRef(); private stickyRef = preact.createRef(); + private touchHandler: TouchHandler; constructor(props: preact.RenderableProps) { super(props); @@ -58,7 +64,7 @@ export class Tabs extends preact.Component { } 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) { @@ -67,6 +73,9 @@ export class Tabs extends preact.Component { } this.updateLine(true); this.scrollIntoView(true); + if (swipeable) { + this.touchHandler = new TouchHandler(this.contentRef.current, { onTouchEnd: this.onTouchEnd.bind(this) }); + } } componentDidUpdate(): void { @@ -74,6 +83,25 @@ export class Tabs extends preact.Component { 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) { @@ -223,7 +251,7 @@ export class Tabs extends preact.Component { } }); const wrap = ( -
+
{animated ? this.warpAnimatedContents(contents) : contents}
); diff --git a/src/utils/touch-handler.ts b/src/utils/touch-handler.ts new file mode 100644 index 0000000..9b047ab --- /dev/null +++ b/src/utils/touch-handler.ts @@ -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; + } +}