Skip to content

feat(tabs): add swipe events #6294

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 36 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
9c7f20b
feat(tabs): add swipe events #2209
amcdnl Aug 5, 2017
7f2ff6f
feat(tabs): handle swipes in tab nav
amcdnl Aug 6, 2017
cb40cc8
feat(tabs): rtl, drag events, event consolidation
amcdnl Aug 12, 2017
aaa6cef
Merge branch 'master' into tab-swipe
amcdnl Aug 12, 2017
9c84af8
feat(tabs): fix pan-y not working in tab gestures
amcdnl Aug 13, 2017
c2ce180
Squashed commit of the following:
amcdnl Aug 14, 2017
43282e7
tests(tabs): add gesture tests, refactor gesture script, add enum fo…
amcdnl Aug 15, 2017
33d584e
chore(*): adding workaround comment
amcdnl Aug 15, 2017
897275a
feat(tabs): nit
amcdnl Aug 16, 2017
75ffcc2
Merge branch 'master' of https://github.com/amcdnl/material2 into tab…
amcdnl Aug 16, 2017
5557b80
Merge branch 'master' into tab-swipe
amcdnl Aug 16, 2017
36d422c
Merge branch 'master' into tab-swipe
amcdnl Aug 23, 2017
5928c20
Merge branch 'master' into tab-swipe
amcdnl Sep 4, 2017
343a8dd
Merge branch 'master' into tab-swipe
amcdnl Sep 4, 2017
b4d5bee
chore(nit): fix trailing whitespace
amcdnl Sep 5, 2017
031672f
chore(nit): added supression
amcdnl Sep 6, 2017
0733532
chore(nit): better tslint rule
amcdnl Sep 6, 2017
718ec6d
chore(nit): remove items from params
amcdnl Sep 11, 2017
eecd2bd
Merge branch 'master' into tab-swipe
amcdnl Sep 11, 2017
6875bba
Merge branch 'master' of https://github.com/angular/material2 into ta…
amcdnl Sep 14, 2017
e8aed99
chore(tests): fix slider spec post merge
amcdnl Sep 14, 2017
63c1ab5
Merge branch 'master' into tab-swipe
amcdnl Oct 3, 2017
8d79f7c
chore(merge): merge fixes
amcdnl Oct 3, 2017
79e2e11
Merge branch 'master' of https://github.com/angular/material2 into ta…
amcdnl Oct 20, 2017
e5a7d3e
Merge branch 'master' into tab-swipe
amcdnl Nov 19, 2017
c9ee561
Merge branch 'master' into tab-swipe
amcdnl Dec 2, 2017
5b801b0
chore: disable lint false-positi ves
amcdnl Dec 2, 2017
be263e6
Merge branch 'master' into tab-swipe
amcdnl May 4, 2018
c4a61a8
chore: remove redudant types
amcdnl May 6, 2018
a35a9a7
chore: combine imports
amcdnl May 6, 2018
9b22614
Merge branch 'master' into tab-swipe
amcdnl May 13, 2018
ca91b01
Merge remote-tracking branch 'origin/master' into tab-swipe
amcdnl May 20, 2018
59ef6b4
chore: build fixes
amcdnl Jun 10, 2018
acb5525
Merge branch 'master' into tab-swipe
amcdnl Jun 24, 2018
d68f2f0
Merge remote-tracking branch 'upstream/master' into tab-swipe
amcdnl Jul 15, 2018
52c5b4e
Merge branch 'master' into tab-swipe
amcdnl Jul 22, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/lib/core/gestures/gesture-annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,18 @@
export interface HammerInput {
preventDefault: () => {};
deltaX: number;
direction: HammerDirection;
type: string;
deltaY: number;
center: { x: number; y: number; };
}

/** @docs-private */
export enum HammerDirection {
Left = 2,
Right = 4
}

/** @docs-private */
export interface HammerStatic {
new(element: HTMLElement | SVGElement, options?: any): HammerManager;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/slide-toggle/slide-toggle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {FormControl, FormsModule, NgModel, ReactiveFormsModule} from '@angular/f
import {defaultRippleAnimationConfig} from '@angular/material/core';
import {By, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
import {BidiModule, Direction} from '@angular/cdk/bidi';
import {TestGestureConfig} from '../slider/test-gesture-config';
import {MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS} from './slide-toggle-config';
import {MatSlideToggle, MatSlideToggleChange, MatSlideToggleModule} from './index';
import {TestGestureConfig} from '../core/gestures/test-gesture-config';

describe('MatSlideToggle without forms', () => {
let gestureConfig: TestGestureConfig;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/slider/slider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {ComponentFixture, TestBed, fakeAsync, flush} from '@angular/core/testing
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {By, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
import {MatSlider, MatSliderModule} from './index';
import {TestGestureConfig} from './test-gesture-config';
import {TestGestureConfig} from '../core/gestures/test-gesture-config';

describe('MatSlider', () => {
let gestureConfig: TestGestureConfig;
Expand Down
3 changes: 3 additions & 0 deletions src/lib/tabs/tab-body.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<!-- touch-action workaround for https://github.com/angular/material2/issues/6484 -->
<div class="mat-tab-body-content" #content
(swipe)="swipe.emit($event)"
[style.touch-action]="'pan-y'"
[@translateTab]="_position"
(@translateTab.start)="_onTranslateTabStarted($event)"
(@translateTab.done)="_onTranslateTabComplete($event)">
Expand Down
5 changes: 5 additions & 0 deletions src/lib/tabs/tab-body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
import {AnimationEvent} from '@angular/animations';
import {TemplatePortal, CdkPortalOutlet, PortalHostDirective} from '@angular/cdk/portal';
import {Directionality, Direction} from '@angular/cdk/bidi';
// tslint:disable-next-line:no-unused-variable
import {HammerInput} from '../core';
import {Subscription} from 'rxjs';
import {matTabsAnimations} from './tabs-animations';
import {startWith} from 'rxjs/operators';
Expand Down Expand Up @@ -140,6 +142,9 @@ export class MatTabBody implements OnInit, OnDestroy {
/** The portal host inside of this container into which the tab body content will be loaded. */
@ViewChild(PortalHostDirective) _portalHost: PortalHostDirective;

/** Event emitted when the tab body is swiped left/right */
@Output() swipe: EventEmitter<HammerInput> = new EventEmitter<HammerInput>();

/** The tab body content to display. */
@Input('content') _content: TemplatePortal;

Expand Down
1 change: 1 addition & 0 deletions src/lib/tabs/tab-group.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
[content]="tab.content"
[position]="tab.position"
[origin]="tab.origin"
(swipe)="_bodyContentSwiped($event)"
(_onCentered)="_removeTabBodyWrapperHeight()"
(_onCentering)="_setTabBodyWrapperHeight($event)">
</mat-tab-body>
Expand Down
62 changes: 59 additions & 3 deletions src/lib/tabs/tab-group.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ import {LEFT_ARROW} from '@angular/cdk/keycodes';
import {dispatchFakeEvent, dispatchKeyboardEvent} from '@angular/cdk/testing';
import {Component, OnInit, QueryList, ViewChild, ViewChildren} from '@angular/core';
import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
import {By, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
import {TestGestureConfig} from '../core/gestures/test-gesture-config';
import {Observable} from 'rxjs';
import {MatTab, MatTabGroup, MatTabHeaderPosition, MatTabsModule} from './index';

import {HammerDirection} from '../core/gestures/gesture-annotations';
// tslint:disable-next-line:no-unused-variable
import {ViewportRuler} from '@angular/cdk/scrolling';

describe('MatTabGroup', () => {
let gestureConfig: TestGestureConfig;

beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
imports: [MatTabsModule, NoopAnimationsModule],
Expand All @@ -22,6 +27,12 @@ describe('MatTabGroup', () => {
TemplateTabs,
TabGroupWithAriaInputs,
],
providers: [
{provide: HAMMER_GESTURE_CONFIG, useFactory: () => {
gestureConfig = new TestGestureConfig();
return gestureConfig;
}},
]
});

TestBed.compileComponents();
Expand Down Expand Up @@ -195,6 +206,41 @@ describe('MatTabGroup', () => {
.toBe(0, 'Expected no ripple to show up on label mousedown.');
});

it('should change tabs on body swipe', () => {
let component = fixture.debugElement.componentInstance;
component.selectedIndex = 0;
checkSelectedIndex(0, fixture);

// select the second tab
const body =
fixture.nativeElement.querySelector('.mat-tab-body-active .mat-tab-body-content');

dispatchSwipeEvent(body, HammerDirection.Left, gestureConfig);
checkSelectedIndex(1, fixture);

dispatchSwipeEvent(body, HammerDirection.Right, gestureConfig);
checkSelectedIndex(0, fixture);
});

it('should change tabs to next non-disabled on body swipe', () => {
let disabledFixture = TestBed.createComponent(DisabledTabsTestApp);
disabledFixture.detectChanges();

let component = disabledFixture.debugElement.componentInstance;
component.selectedIndex = 0;
checkSelectedIndex(0, disabledFixture);

// select the second tab
const body =
disabledFixture.nativeElement.querySelector('.mat-tab-body-active .mat-tab-body-content');

dispatchSwipeEvent(body, HammerDirection.Left, gestureConfig);
checkSelectedIndex(2, disabledFixture);

dispatchSwipeEvent(body, HammerDirection.Right, gestureConfig);
checkSelectedIndex(0, disabledFixture);
});

it('should set the isActive flag on each of the tabs', () => {
fixture.detectChanges();

Expand Down Expand Up @@ -567,7 +613,17 @@ describe('MatTabGroup', () => {
const child = fixture.debugElement.query(By.css('.child'));
expect(child.nativeElement).toBeDefined();
}));
});
});

function dispatchSwipeEvent(bodyElement: HTMLElement,
direction: HammerDirection,
config: TestGestureConfig): void {
config.emitEventForElement('swipe', bodyElement, {
deltaX: direction === HammerDirection.Left ? -100 : 100,
direction: direction,
srcEvent: {preventDefault: jasmine.createSpy('preventDefault')}
});
}

/**
* Checks that the `selectedIndex` has been updated; checks that the label and body have their
Expand Down
44 changes: 43 additions & 1 deletion src/lib/tabs/tab-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@ import {
OnDestroy,
Output,
QueryList,
Optional,
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import {Directionality} from '@angular/cdk/bidi';
import {
CanColor,
CanDisableRipple,
mixinColor,
mixinDisableRipple,
ThemePalette
ThemePalette,
HammerInput,
HammerDirection
} from '@angular/material/core';
import {merge, Subscription} from 'rxjs';
import {MatTab} from './tab';
Expand Down Expand Up @@ -147,6 +151,7 @@ export class MatTabGroup extends _MatTabGroupMixinBase implements AfterContentIn
private _groupId: number;

constructor(elementRef: ElementRef,
@Optional() private _dir: Directionality,
private _changeDetectorRef: ChangeDetectorRef) {
super(elementRef);
this._groupId = nextId++;
Expand Down Expand Up @@ -306,6 +311,42 @@ export class MatTabGroup extends _MatTabGroupMixinBase implements AfterContentIn
this.animationDone.emit();
}

/** Body content was swiped left/right */
_bodyContentSwiped(event: HammerInput): void {
if (this.selectedIndex === null) {
this.selectedIndex = 0;
}

if (event.direction === HammerDirection.Left || event.direction === HammerDirection.Right) {
let direction = event.direction;
if (this._dir.value === 'rtl') {
direction =
direction === HammerDirection.Left ? HammerDirection.Right : HammerDirection.Left;
}

if (this.selectedIndex !== 0 && direction === HammerDirection.Right) {
this._setActiveItemByIndex(this.selectedIndex - 1, -1);
} else if (this.selectedIndex < this._tabs.length && direction === HammerDirection.Left) {
this._setActiveItemByIndex(this.selectedIndex + 1, 1);
}
}
}

/**
* Sets the active item to the first enabled item starting at the index specified. If the
* item is disabled, it will move in the fallbackDelta direction until it either
* finds an enabled item or encounters the end of the list.
*/
private _setActiveItemByIndex(index: number, fallbackDelta: number): void {
const items = this._tabs.toArray();
if (!items[index]) { return; }
while (items[index].disabled) {
index += fallbackDelta;
if (!items[index]) { return; }
}
this.selectedIndex = index;
}

/** Handle click events, setting new selected index if appropriate. */
_handleClick(tab: MatTab, tabHeader: MatTabHeader, idx: number) {
if (!tab.disabled) {
Expand All @@ -320,4 +361,5 @@ export class MatTabGroup extends _MatTabGroupMixinBase implements AfterContentIn
}
return this.selectedIndex === idx ? 0 : -1;
}

}
2 changes: 2 additions & 0 deletions src/lib/tabs/tab-header.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
</div>

<div class="mat-tab-label-container" #tabListContainer
(swipe)="_handleSwipeDrag($event)"
(drag)="_handleSwipeDrag($event)"
(keydown)="_handleKeydown($event)">
<div class="mat-tab-list" #tabList role="tablist" (cdkObserveContent)="_onContentChanges()">
<div class="mat-tab-labels">
Expand Down
39 changes: 36 additions & 3 deletions src/lib/tabs/tab-header.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {HAMMER_GESTURE_CONFIG, By} from '@angular/platform-browser';
import {Direction, Directionality} from '@angular/cdk/bidi';
import {END, ENTER, HOME, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes';
import {PortalModule} from '@angular/cdk/portal';
import {ScrollDispatchModule, ViewportRuler} from '@angular/cdk/scrolling';
import {dispatchFakeEvent, dispatchKeyboardEvent} from '@angular/cdk/testing';
import {CommonModule} from '@angular/common';
import {Component, ViewChild} from '@angular/core';
Expand All @@ -14,18 +14,20 @@ import {
tick,
} from '@angular/core/testing';
import {MatRippleModule} from '@angular/material/core';
import {By} from '@angular/platform-browser';
import {MatInkBar} from './ink-bar';
import {MatTabHeader} from './tab-header';
import {MatTabLabelWrapper} from './tab-label-wrapper';
import {TestGestureConfig} from '../core/gestures/test-gesture-config';
import {Subject} from 'rxjs';

// tslint:disable-next-line:no-unused-variable
import {VIEWPORT_RULER_PROVIDER, ViewportRuler, ScrollDispatchModule} from '@angular/cdk/scrolling';

describe('MatTabHeader', () => {
let dir: Direction = 'ltr';
let change = new Subject();
let fixture: ComponentFixture<SimpleTabHeaderApp>;
let appComponent: SimpleTabHeaderApp;
let gestureConfig: TestGestureConfig;

beforeEach(async(() => {
dir = 'ltr';
Expand All @@ -40,6 +42,10 @@ describe('MatTabHeader', () => {
providers: [
ViewportRuler,
{provide: Directionality, useFactory: () => ({value: dir, change: change.asObservable()})},
{provide: HAMMER_GESTURE_CONFIG, useFactory: () => {
gestureConfig = new TestGestureConfig();
return gestureConfig;
}}
]
});

Expand Down Expand Up @@ -269,6 +275,21 @@ describe('MatTabHeader', () => {
.toBe(0, 'Expected no ripple to show up after mousedown');
});

it('should update pagination on swipe', () => {
appComponent.addTabsForScrolling();
fixture.detectChanges();

expect(appComponent.tabHeader.scrollDistance).toBe(0);

let tabListContainer = appComponent.tabHeader._tabListContainer.nativeElement;

dispatchSwipeEvent(tabListContainer, 100, gestureConfig);
fixture.detectChanges();

expect(appComponent.tabHeader.scrollDistance)
.toBe(appComponent.tabHeader._getMaxScrollDistance());
});

});

describe('rtl', () => {
Expand Down Expand Up @@ -392,3 +413,15 @@ class SimpleTabHeaderApp {
this.tabs.push({label: 'new'}, {label: 'new'}, {label: 'new'}, {label: 'new'});
}
}

function dispatchSwipeEvent(headerElement: HTMLElement, percent: number,
gestureConfig: TestGestureConfig): void {
let trackElement = headerElement.querySelector('.mat-tab-list')!;
let dimensions = trackElement.getBoundingClientRect();
let x = dimensions.left + (dimensions.width * percent);

gestureConfig.emitEventForElement('swipe', headerElement, {
deltaX: x * -1,
srcEvent: { preventDefault: jasmine.createSpy('preventDefault') }
});
}
9 changes: 9 additions & 0 deletions src/lib/tabs/tab-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import {HammerInput} from '../core';
import {CanDisableRipple, mixinDisableRipple} from '@angular/material/core';
import {merge, of as observableOf, Subject} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
Expand Down Expand Up @@ -326,6 +327,13 @@ export class MatTabHeader extends _MatTabHeaderMixinBase
this._checkScrollingControls();
}

/** Handle swipe events scrolling the tabs */
_handleSwipeDrag(event: HammerInput): void {
const curDistance = this.scrollDistance || 0;
this.scrollDistance = curDistance - event.deltaX;
this._updateTabScrollPosition();
}

/**
* Moves the tab list in the 'before' or 'after' direction (towards the beginning of the list or
* the end of the list, respectively). The distance to scroll is computed to be a third of the
Expand Down Expand Up @@ -436,4 +444,5 @@ export class MatTabHeader extends _MatTabHeaderMixinBase

this._inkBar.alignToElement(selectedLabelWrapper);
}

}