Skip to content
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

feat(tabs): add swipe events #6294

Closed
wants to merge 36 commits into from
Closed
Show file tree
Hide file tree
Changes from 11 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
2 changes: 1 addition & 1 deletion src/lib/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export * from '@angular/cdk/overlay';
export {GestureConfig} from './gestures/gesture-config';
// Explicitly specify the interfaces which should be re-exported, because if everything
// is re-exported, module bundlers may run into issues with treeshaking.
export {HammerInput, HammerManager} from './gestures/gesture-annotations';
export {HammerInput, HammerManager, HammerDirection} from './gestures/gesture-annotations';

// Ripple
export * from './ripple/index';
Expand Down
10 changes: 9 additions & 1 deletion src/lib/core/gestures/gesture-annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,25 @@
/**
* Stripped-down HammerJS annotations to be used within Material, which are necessary,
* because HammerJS is an optional dependency. For the full annotations see:
* https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/hammerjs
* https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/hammerjs
*/

/** @docs-private */
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,7 +6,7 @@ import {
} from '@angular/core/testing';
import {NgModel, FormsModule, ReactiveFormsModule, FormControl} from '@angular/forms';
import {MdSlideToggle, MdSlideToggleChange, MdSlideToggleModule} from './index';
import {TestGestureConfig} from '../slider/test-gesture-config';
import {TestGestureConfig} from '../core/gestures/test-gesture-config';
import {dispatchFakeEvent} from '@angular/cdk/testing';
import {RIPPLE_FADE_IN_DURATION, RIPPLE_FADE_OUT_DURATION} from '../core/ripple/ripple-renderer';

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 @@ -3,7 +3,7 @@ import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {Component, DebugElement, ViewChild} from '@angular/core';
import {By, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
import {MdSlider, MdSliderModule} from './index';
import {TestGestureConfig} from './test-gesture-config';
import {TestGestureConfig} from '../core/gestures/test-gesture-config';
import {BidiModule} from '../core/bidi/index';
import {
DOWN_ARROW,
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)="onSwipe.emit($event)"
[style.touch-action]="'pan-y'"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment for why this needs to be an inline style? Also, since this is static, you don't need a binding; just style="touch-action: pan-y" is good.

Copy link
Contributor Author

@amcdnl amcdnl Aug 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you like the comment to be a HTML inline comment?

This actually doesn't work if you do it static like you suggested. I had tried that at first implementation.

Reference: hammerjs/hammer.js#1014

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I spent a bit too much time tracking this down. touch-action: none is coming from our material GestureConfig; adding the swipe gesture always sets the touch action to pan-y + pan-x (equivalent to none).

The "correct" fix for this would be to change how our gesture config add recognizers for certain events, but that's more complicated than we want to do right now. I filed #6484 to track this. Can you make your comment something like

<!-- touch-action workaround for https://github.com/angular/material2/issues/6484 -->

Comment can go above the element (I don't think a comment inside of a tag is valid html). HTML comments are stripped when packaging the release

[@translateTab]="_position"
(@translateTab.start)="_onTranslateTabStarted($event)"
(@translateTab.done)="_onTranslateTabComplete($event)">
Expand Down
5 changes: 4 additions & 1 deletion src/lib/tabs/tab-body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ import {
transition,
AnimationEvent,
} from '@angular/animations';
import {HammerInput} from '../core';
import {TemplatePortal, PortalHostDirective} from '@angular/cdk/portal';
import {Directionality, Direction} from '@angular/cdk/bidi';


/**
* These position states are used internally as animation states for the tab body. Setting the
* position state to left, right, or center will transition the tab body from its current
Expand Down Expand Up @@ -97,6 +97,9 @@ export class MdTabBody implements OnInit, AfterViewChecked {
/** Event emitted when the tab completes its animation towards the center. */
@Output() onCentered: EventEmitter<void> = new EventEmitter<void>(true);

/** Event emitted when the tab body is swiped left/right */
@Output() onSwipe: 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 @@ -33,6 +33,7 @@
[content]="tab.content"
[position]="tab.position"
[origin]="tab.origin"
(onSwipe)="_bodyContentSwiped($event)"
(onCentered)="_removeTabBodyWrapperHeight()"
(onCentering)="_setTabBodyWrapperHeight($event)">
</md-tab-body>
Expand Down
56 changes: 54 additions & 2 deletions src/lib/tabs/tab-group.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
import {Component, QueryList, ViewChild, ViewChildren} from '@angular/core';
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
import {By} from '@angular/platform-browser';
import {By, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
import {ViewportRuler} from '@angular/cdk/overlay';
import {dispatchFakeEvent, FakeViewportRuler} from '@angular/cdk/testing';
import {Observable} from 'rxjs/Observable';
import {MdTab, MdTabGroup, MdTabHeaderPosition, MdTabsModule} from './index';

import {TestGestureConfig} from '../core/gestures/test-gesture-config';
import {HammerDirection} from '../core';

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

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MdTabsModule, NoopAnimationsModule],
Expand All @@ -22,6 +25,10 @@ describe('MdTabGroup', () => {
],
providers: [
{provide: ViewportRuler, useClass: FakeViewportRuler},
{provide: HAMMER_GESTURE_CONFIG, useFactory: () => {
gestureConfig = new TestGestureConfig();
return gestureConfig;
}},
]
});

Expand Down Expand Up @@ -170,6 +177,41 @@ describe('MdTabGroup', () => {
.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 @@ -308,6 +350,16 @@ describe('MdTabGroup', () => {
});
});

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
* respective `active` classes
Expand Down
52 changes: 47 additions & 5 deletions src/lib/tabs/tab-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ import {
AfterContentInit,
AfterContentChecked,
OnDestroy,
Optional,
} from '@angular/core';
import {HammerInput, HammerDirection} from '../core';
import {Directionality} from '@angular/cdk/bidi';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {map} from '@angular/cdk/rxjs';
import {Observable} from 'rxjs/Observable';
Expand Down Expand Up @@ -143,7 +146,8 @@ export class MdTabGroup extends _MdTabGroupMixinBase implements AfterContentInit

constructor(_renderer: Renderer2,
elementRef: ElementRef,
private _changeDetectorRef: ChangeDetectorRef) {
private _changeDetectorRef: ChangeDetectorRef,
@Optional() private _dir: Directionality) {
super(_renderer, elementRef);
this._groupId = nextId++;
}
Expand Down Expand Up @@ -185,7 +189,7 @@ export class MdTabGroup extends _MdTabGroupMixinBase implements AfterContentInit
}
}

ngAfterContentInit() {
ngAfterContentInit(): void {
this._subscribeToTabLabels();

// Subscribe to changes in the amount of tabs, in order to be
Expand All @@ -196,7 +200,7 @@ export class MdTabGroup extends _MdTabGroupMixinBase implements AfterContentInit
});
}

ngOnDestroy() {
ngOnDestroy(): void {
if (this._tabsSubscription) {
this._tabsSubscription.unsubscribe();
}
Expand All @@ -214,7 +218,7 @@ export class MdTabGroup extends _MdTabGroupMixinBase implements AfterContentInit
this._isInitialized = true;
}

_focusChanged(index: number) {
_focusChanged(index: number): void {
this.focusChange.emit(this._createChangeEvent(index));
}

Expand All @@ -233,7 +237,7 @@ export class MdTabGroup extends _MdTabGroupMixinBase implements AfterContentInit
* binding to be updated, we need to subscribe to changes in it and trigger change detection
* manually.
*/
private _subscribeToTabLabels() {
private _subscribeToTabLabels(): void {
if (this._tabLabelSubscription) {
this._tabLabelSubscription.unsubscribe();
}
Expand Down Expand Up @@ -276,4 +280,42 @@ export class MdTabGroup extends _MdTabGroupMixinBase implements AfterContentInit
this._tabBodyWrapperHeight = this._tabBodyWrapper.nativeElement.clientHeight;
this._renderer.setStyle(this._tabBodyWrapper.nativeElement, 'height', '');
}

/** 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,
items = this._tabs.toArray()): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why include the items array as a param? I don't see it passed in by anyone

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was a pattern from another component that I used that @jelbourn referred me to. Do you think it should just be an variable defined in the function body?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can remove it if the param is never passed

if (!items[index]) { return; }
while (items[index].disabled) {
index += fallbackDelta;
if (!items[index]) { return; }
}
this.selectedIndex = index;
}

}
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
36 changes: 34 additions & 2 deletions src/lib/tabs/tab-header.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
} from '@angular/core/testing';
import {Component, ViewChild, ViewContainerRef} from '@angular/core';
import {CommonModule} from '@angular/common';
import {By} from '@angular/platform-browser';
import {By, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
import {ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes';
import {PortalModule} from '@angular/cdk/portal';
import {ViewportRuler} from '@angular/cdk/overlay';
Expand All @@ -14,14 +14,15 @@ import {MdRippleModule} from '../core/ripple/index';
import {MdInkBar} from './ink-bar';
import {MdTabLabelWrapper} from './tab-label-wrapper';
import {Subject} from 'rxjs/Subject';

import {TestGestureConfig} from '../core/gestures/test-gesture-config';


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

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

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

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

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

let tabListContainer = appComponent.mdTabHeader._tabListContainer.nativeElement;

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

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

});

describe('rtl', () => {
Expand Down Expand Up @@ -336,3 +356,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 @@ -24,6 +24,7 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
} from '@angular/core';
import {HammerInput} from '../core';
import {Directionality, Direction} from '@angular/cdk/bidi';
import {RIGHT_ARROW, LEFT_ARROW, ENTER, SPACE} from '@angular/cdk/keycodes';
import {auditTime, startWith} from '@angular/cdk/rxjs';
Expand Down Expand Up @@ -320,6 +321,13 @@ export class MdTabHeader extends _MdTabHeaderMixinBase
}
get scrollDistance(): number { return this._scrollDistance; }

/** 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 @@ -430,4 +438,5 @@ export class MdTabHeader extends _MdTabHeaderMixinBase

this._inkBar.alignToElement(selectedLabelWrapper);
}

}