Skip to content

Commit 4126029

Browse files
crisbetommalerba
authored andcommitted
feat(tooltip): remove Hammer.js dependency (#17003)
Reworks the tooltip to remove its dependency on Hammer.js. Also adds an option to disable touch gestures so that text selection doesn't have to be disabled on the trigger. Fixes #16850.
1 parent 7893e8a commit 4126029

File tree

6 files changed

+307
-95
lines changed

6 files changed

+307
-95
lines changed

src/material/tooltip/BUILD.bazel

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ ng_module(
3131
"@npm//@angular/animations",
3232
"@npm//@angular/common",
3333
"@npm//@angular/core",
34-
"@npm//@angular/platform-browser",
3534
"@npm//rxjs",
3635
],
3736
)

src/material/tooltip/tooltip-module.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ import {OverlayModule} from '@angular/cdk/overlay';
1010
import {A11yModule} from '@angular/cdk/a11y';
1111
import {CommonModule} from '@angular/common';
1212
import {NgModule} from '@angular/core';
13-
import {GestureConfig, MatCommonModule} from '@angular/material/core';
14-
import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
13+
import {MatCommonModule} from '@angular/material/core';
1514
import {
1615
MatTooltip,
1716
TooltipComponent,
@@ -28,9 +27,6 @@ import {
2827
exports: [MatTooltip, TooltipComponent, MatCommonModule],
2928
declarations: [MatTooltip, TooltipComponent],
3029
entryComponents: [TooltipComponent],
31-
providers: [
32-
MAT_TOOLTIP_SCROLL_STRATEGY_FACTORY_PROVIDER,
33-
{provide: HAMMER_GESTURE_CONFIG, useClass: GestureConfig},
34-
]
30+
providers: [MAT_TOOLTIP_SCROLL_STRATEGY_FACTORY_PROVIDER]
3531
})
3632
export class MatTooltipModule {}

src/material/tooltip/tooltip.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,14 @@ the positions `before` and `after` should be used instead of `left` and `right`,
2525
### Showing and hiding
2626

2727
By default, the tooltip will be immediately shown when the user's mouse hovers over the tooltip's
28-
trigger element and immediately hides when the user's mouse leaves.
28+
trigger element and immediately hides when the user's mouse leaves.
2929

3030
On mobile, the tooltip is displayed when the user longpresses the element and hides after a
31-
delay of 1500ms. The longpress behavior requires HammerJS to be loaded on the page. To learn more
32-
about adding HammerJS to your app, check out the Gesture Support section of the Getting Started
33-
guide.
31+
delay of 1500ms.
3432

3533
#### Show and hide delays
3634

37-
To add a delay before showing or hiding the tooltip, you can use the inputs `matTooltipShowDelay`
35+
To add a delay before showing or hiding the tooltip, you can use the inputs `matTooltipShowDelay`
3836
and `matTooltipHideDelay` to provide a delay time in milliseconds.
3937

4038
The following example has a tooltip that waits one second to display after the user
@@ -58,7 +56,7 @@ which both accept a number in milliseconds to delay before applying the display
5856

5957
#### Disabling the tooltip from showing
6058

61-
To completely disable a tooltip, set `matTooltipDisabled`. While disabled, a tooltip will never be
59+
To completely disable a tooltip, set `matTooltipDisabled`. While disabled, a tooltip will never be
6260
shown.
6361

6462
### Accessibility

src/material/tooltip/tooltip.spec.ts

Lines changed: 166 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
SCROLL_THROTTLE_MS,
3939
TOOLTIP_PANEL_CLASS,
4040
MAT_TOOLTIP_DEFAULT_OPTIONS,
41+
TooltipTouchGestures,
4142
} from './index';
4243

4344

@@ -895,33 +896,175 @@ describe('MatTooltip', () => {
895896
}));
896897
});
897898

898-
describe('special cases', () => {
899+
describe('touch gestures', () => {
900+
beforeEach(() => {
901+
platform.ANDROID = true;
902+
});
903+
904+
it('should have a delay when showing on touchstart', fakeAsync(() => {
905+
const fixture = TestBed.createComponent(BasicTooltipDemo);
906+
fixture.detectChanges();
907+
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
908+
909+
dispatchFakeEvent(button, 'touchstart');
910+
fixture.detectChanges();
911+
tick(250); // Halfway through the delay.
912+
913+
assertTooltipInstance(fixture.componentInstance.tooltip, false);
914+
915+
tick(250); // Finish the delay.
916+
fixture.detectChanges();
917+
tick(500); // Finish the animation.
918+
919+
assertTooltipInstance(fixture.componentInstance.tooltip, true);
920+
}));
921+
922+
it('should be able to disable opening on touch', fakeAsync(() => {
923+
const fixture = TestBed.createComponent(BasicTooltipDemo);
924+
fixture.componentInstance.touchGestures = 'off';
925+
fixture.detectChanges();
926+
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
927+
928+
dispatchFakeEvent(button, 'touchstart');
929+
fixture.detectChanges();
930+
tick(500); // Finish the delay.
931+
fixture.detectChanges();
932+
tick(500); // Finish the animation.
933+
934+
assertTooltipInstance(fixture.componentInstance.tooltip, false);
935+
}));
936+
937+
it('should not prevent the default action on touchstart', () => {
938+
const fixture = TestBed.createComponent(BasicTooltipDemo);
939+
fixture.detectChanges();
940+
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
941+
const event = dispatchFakeEvent(button, 'touchstart');
942+
fixture.detectChanges();
943+
944+
expect(event.defaultPrevented).toBe(false);
945+
});
946+
947+
it('should close on touchend with a delay', fakeAsync(() => {
948+
const fixture = TestBed.createComponent(BasicTooltipDemo);
949+
fixture.detectChanges();
950+
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
951+
952+
dispatchFakeEvent(button, 'touchstart');
953+
fixture.detectChanges();
954+
tick(500); // Finish the open delay.
955+
fixture.detectChanges();
956+
tick(500); // Finish the animation.
957+
assertTooltipInstance(fixture.componentInstance.tooltip, true);
958+
959+
dispatchFakeEvent(button, 'touchend');
960+
fixture.detectChanges();
961+
tick(1000); // 2/3 through the delay
962+
assertTooltipInstance(fixture.componentInstance.tooltip, true);
963+
964+
tick(500); // Finish the delay.
965+
fixture.detectChanges();
966+
tick(500); // Finish the exit animation.
899967

900-
it('should clear the `user-select` when a tooltip is set on a text field', () => {
968+
assertTooltipInstance(fixture.componentInstance.tooltip, false);
969+
}));
970+
971+
it('should close on touchcancel with a delay', fakeAsync(() => {
972+
const fixture = TestBed.createComponent(BasicTooltipDemo);
973+
fixture.detectChanges();
974+
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
975+
976+
dispatchFakeEvent(button, 'touchstart');
977+
fixture.detectChanges();
978+
tick(500); // Finish the open delay.
979+
fixture.detectChanges();
980+
tick(500); // Finish the animation.
981+
assertTooltipInstance(fixture.componentInstance.tooltip, true);
982+
983+
dispatchFakeEvent(button, 'touchcancel');
984+
fixture.detectChanges();
985+
tick(1000); // 2/3 through the delay
986+
assertTooltipInstance(fixture.componentInstance.tooltip, true);
987+
988+
tick(500); // Finish the delay.
989+
fixture.detectChanges();
990+
tick(500); // Finish the exit animation.
991+
992+
assertTooltipInstance(fixture.componentInstance.tooltip, false);
993+
}));
994+
995+
it('should disable native touch interactions', () => {
996+
const fixture = TestBed.createComponent(BasicTooltipDemo);
997+
fixture.detectChanges();
998+
999+
const styles = fixture.nativeElement.querySelector('button').style;
1000+
expect(styles.touchAction || (styles as any).webkitUserDrag).toBe('none');
1001+
});
1002+
1003+
it('should allow native touch interactions if touch gestures are turned off', () => {
1004+
const fixture = TestBed.createComponent(BasicTooltipDemo);
1005+
fixture.componentInstance.touchGestures = 'off';
1006+
fixture.detectChanges();
1007+
1008+
const styles = fixture.nativeElement.querySelector('button').style;
1009+
expect(styles.touchAction || (styles as any).webkitUserDrag).toBeFalsy();
1010+
});
1011+
1012+
it('should allow text selection on inputs when gestures are set to auto', () => {
9011013
const fixture = TestBed.createComponent(TooltipOnTextFields);
902-
const instance = fixture.componentInstance;
1014+
fixture.detectChanges();
1015+
1016+
const inputStyle = fixture.componentInstance.input.nativeElement.style;
1017+
const textareaStyle = fixture.componentInstance.textarea.nativeElement.style;
1018+
1019+
expect(inputStyle.userSelect).toBeFalsy();
1020+
expect(inputStyle.webkitUserSelect).toBeFalsy();
1021+
expect(inputStyle.msUserSelect).toBeFalsy();
1022+
expect((inputStyle as any).MozUserSelect).toBeFalsy();
1023+
1024+
expect(textareaStyle.userSelect).toBeFalsy();
1025+
expect(textareaStyle.webkitUserSelect).toBeFalsy();
1026+
expect(textareaStyle.msUserSelect).toBeFalsy();
1027+
expect((textareaStyle as any).MozUserSelect).toBeFalsy();
1028+
});
9031029

1030+
it('should disable text selection on inputs when gestures are set to on', () => {
1031+
const fixture = TestBed.createComponent(TooltipOnTextFields);
1032+
fixture.componentInstance.touchGestures = 'on';
9041033
fixture.detectChanges();
9051034

906-
expect(instance.input.nativeElement.style.userSelect).toBeFalsy();
907-
expect(instance.input.nativeElement.style.webkitUserSelect).toBeFalsy();
908-
expect(instance.input.nativeElement.style.msUserSelect).toBeFalsy();
1035+
const inputStyle = fixture.componentInstance.input.nativeElement.style;
1036+
const inputUserSelect = inputStyle.userSelect || inputStyle.webkitUserSelect ||
1037+
inputStyle.msUserSelect || (inputStyle as any).MozUserSelect;
1038+
const textareaStyle = fixture.componentInstance.textarea.nativeElement.style;
1039+
const textareaUserSelect = textareaStyle.userSelect || textareaStyle.webkitUserSelect ||
1040+
textareaStyle.msUserSelect || (textareaStyle as any).MozUserSelect;
9091041

910-
expect(instance.textarea.nativeElement.style.userSelect).toBeFalsy();
911-
expect(instance.textarea.nativeElement.style.webkitUserSelect).toBeFalsy();
912-
expect(instance.textarea.nativeElement.style.msUserSelect).toBeFalsy();
1042+
expect(inputUserSelect).toBe('none');
1043+
expect(textareaUserSelect).toBe('none');
9131044
});
9141045

915-
it('should clear the `-webkit-user-drag` on draggable elements', () => {
1046+
it('should allow native dragging on draggable elements when gestures are set to auto', () => {
9161047
const fixture = TestBed.createComponent(TooltipOnDraggableElement);
917-
9181048
fixture.detectChanges();
9191049

9201050
expect(fixture.componentInstance.button.nativeElement.style.webkitUserDrag).toBeFalsy();
9211051
});
9221052

1053+
it('should disable native dragging on draggable elements when gestures are set to on', () => {
1054+
const fixture = TestBed.createComponent(TooltipOnDraggableElement);
1055+
fixture.componentInstance.touchGestures = 'on';
1056+
fixture.detectChanges();
1057+
1058+
const styles = fixture.componentInstance.button.nativeElement.style;
1059+
1060+
if ('webkitUserDrag' in styles) {
1061+
expect(styles.webkitUserDrag).toBe('none');
1062+
}
1063+
});
1064+
9231065
it('should not open on `mouseenter` on iOS', () => {
9241066
platform.IOS = true;
1067+
platform.ANDROID = false;
9251068

9261069
const fixture = TestBed.createComponent(BasicTooltipDemo);
9271070

@@ -934,6 +1077,7 @@ describe('MatTooltip', () => {
9341077

9351078
it('should not open on `mouseenter` on Android', () => {
9361079
platform.ANDROID = true;
1080+
platform.IOS = false;
9371081

9381082
const fixture = TestBed.createComponent(BasicTooltipDemo);
9391083

@@ -943,7 +1087,6 @@ describe('MatTooltip', () => {
9431087

9441088
assertTooltipInstance(fixture.componentInstance.tooltip, false);
9451089
});
946-
9471090
});
9481091

9491092
});
@@ -955,7 +1098,8 @@ describe('MatTooltip', () => {
9551098
*ngIf="showButton"
9561099
[matTooltip]="message"
9571100
[matTooltipPosition]="position"
958-
[matTooltipClass]="{'custom-one': showTooltipClass, 'custom-two': showTooltipClass }">
1101+
[matTooltipClass]="{'custom-one': showTooltipClass, 'custom-two': showTooltipClass }"
1102+
[matTooltipTouchGestures]="touchGestures">
9591103
Button
9601104
</button>`
9611105
})
@@ -964,6 +1108,7 @@ class BasicTooltipDemo {
9641108
message: any = initialTooltipMessage;
9651109
showButton: boolean = true;
9661110
showTooltipClass = false;
1111+
touchGestures: TooltipTouchGestures = 'auto';
9671112
@ViewChild(MatTooltip, {static: false}) tooltip: MatTooltip;
9681113
@ViewChild('button', {static: false}) button: ElementRef<HTMLButtonElement>;
9691114
}
@@ -1037,31 +1182,33 @@ class DataBoundAriaLabelTooltip {
10371182
template: `
10381183
<input
10391184
#input
1040-
style="user-select: none; -webkit-user-select: none"
1041-
matTooltip="Something">
1185+
matTooltip="Something"
1186+
[matTooltipTouchGestures]="touchGestures">
10421187
10431188
<textarea
10441189
#textarea
1045-
style="user-select: none; -webkit-user-select: none"
1046-
matTooltip="Another thing"></textarea>
1190+
matTooltip="Another thing"
1191+
[matTooltipTouchGestures]="touchGestures"></textarea>
10471192
`,
10481193
})
10491194
class TooltipOnTextFields {
10501195
@ViewChild('input', {static: false}) input: ElementRef<HTMLInputElement>;
10511196
@ViewChild('textarea', {static: false}) textarea: ElementRef<HTMLTextAreaElement>;
1197+
touchGestures: TooltipTouchGestures = 'auto';
10521198
}
10531199

10541200
@Component({
10551201
template: `
10561202
<button
10571203
#button
1058-
style="-webkit-user-drag: none;"
10591204
draggable="true"
1060-
matTooltip="Drag me"></button>
1205+
matTooltip="Drag me"
1206+
[matTooltipTouchGestures]="touchGestures"></button>
10611207
`,
10621208
})
10631209
class TooltipOnDraggableElement {
10641210
@ViewChild('button', {static: false}) button: ElementRef;
1211+
touchGestures: TooltipTouchGestures = 'auto';
10651212
}
10661213

10671214
@Component({

0 commit comments

Comments
 (0)