Skip to content

Commit 1337f36

Browse files
authored
feat(material/tooltip): add option to open tooltip at mouse position (#25202)
Add an input option `matTooltipPositionAtOrigin` to display the tooltip relative to the mouse or touch event that triggered it. Fixes #8759
1 parent 1d1247c commit 1337f36

File tree

10 files changed

+269
-27
lines changed

10 files changed

+269
-27
lines changed

src/cdk/overlay/position/flexible-connected-position-strategy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
9898
_preferredPositions: ConnectionPositionPair[] = [];
9999

100100
/** The origin element against which the overlay will be positioned. */
101-
private _origin: FlexibleConnectedPositionStrategyOrigin;
101+
_origin: FlexibleConnectedPositionStrategyOrigin;
102102

103103
/** The overlay pane element. */
104104
private _pane: HTMLElement;

src/components-examples/material/tooltip/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {TooltipMessageExample} from './tooltip-message/tooltip-message-example';
1616
import {TooltipModifiedDefaultsExample} from './tooltip-modified-defaults/tooltip-modified-defaults-example';
1717
import {TooltipOverviewExample} from './tooltip-overview/tooltip-overview-example';
1818
import {TooltipPositionExample} from './tooltip-position/tooltip-position-example';
19+
import {TooltipPositionAtOriginExample} from './tooltip-position-at-origin/tooltip-position-at-origin-example';
1920
import {TooltipHarnessExample} from './tooltip-harness/tooltip-harness-example';
2021

2122
export {
@@ -29,6 +30,7 @@ export {
2930
TooltipModifiedDefaultsExample,
3031
TooltipOverviewExample,
3132
TooltipPositionExample,
33+
TooltipPositionAtOriginExample,
3234
};
3335

3436
const EXAMPLES = [
@@ -42,6 +44,7 @@ const EXAMPLES = [
4244
TooltipModifiedDefaultsExample,
4345
TooltipOverviewExample,
4446
TooltipPositionExample,
47+
TooltipPositionAtOriginExample,
4548
];
4649

4750
@NgModule({
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
button {
2+
width: 500px;
3+
height: 500px;
4+
}
5+
6+
.example-enabled-checkbox {
7+
margin-left: 8px;
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<button mat-raised-button
2+
matTooltip="Info about the action"
3+
[matTooltipPositionAtOrigin]="enabled.value"
4+
aria-label="Button that displays a tooltip when focused or hovered over">
5+
Action
6+
</button>
7+
8+
<mat-checkbox [formControl]="enabled" class="example-enabled-checkbox">
9+
Position at origin enabled
10+
</mat-checkbox>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {Component} from '@angular/core';
2+
import {FormControl} from '@angular/forms';
3+
4+
/**
5+
* @title Basic tooltip
6+
*/
7+
@Component({
8+
selector: 'tooltip-position-at-origin-example',
9+
templateUrl: 'tooltip-position-at-origin-example.html',
10+
styleUrls: ['tooltip-position-at-origin-example.css'],
11+
})
12+
export class TooltipPositionAtOriginExample {
13+
enabled = new FormControl(false);
14+
}

src/dev-app/tooltip/tooltip-demo.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@ <h3>Tooltip overview</h3>
2424

2525
<h3>Tooltip positioning</h3>
2626
<tooltip-position-example></tooltip-position-example>
27+
28+
<h3>Tooltip with position at origin</h3>
29+
<tooltip-position-at-origin-example></tooltip-position-at-origin-example>

src/material/legacy-tooltip/tooltip.spec.ts

Lines changed: 91 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
dispatchFakeEvent,
1212
dispatchKeyboardEvent,
1313
dispatchMouseEvent,
14+
dispatchTouchEvent,
1415
patchElementFocus,
1516
} from '../../cdk/testing/private';
1617
import {
@@ -232,6 +233,63 @@ describe('MatTooltip', () => {
232233
expect(tooltipDirective._getOverlayPosition().fallback.overlayX).toBe('end');
233234
}));
234235

236+
it('should position center-bottom by default', fakeAsync(() => {
237+
TestBed.resetTestingModule()
238+
.configureTestingModule({
239+
imports: [MatLegacyTooltipModule, OverlayModule],
240+
declarations: [WideTooltipDemo]
241+
})
242+
.compileComponents();
243+
244+
const wideFixture = TestBed.createComponent(WideTooltipDemo);
245+
wideFixture.detectChanges();
246+
tooltipDirective = wideFixture.debugElement
247+
.query(By.css('button'))!
248+
.injector.get<MatLegacyTooltip>(MatLegacyTooltip);
249+
const button: HTMLButtonElement = wideFixture.nativeElement.querySelector('button');
250+
const triggerRect = button.getBoundingClientRect();
251+
252+
dispatchMouseEvent(button, 'mouseenter', triggerRect.right - 100, triggerRect.top + 100);
253+
wideFixture.detectChanges();
254+
tick();
255+
expect(tooltipDirective._isTooltipVisible()).toBe(true);
256+
257+
expect(tooltipDirective._overlayRef!.overlayElement.offsetLeft).toBeGreaterThan(triggerRect.left + 200);
258+
expect(tooltipDirective._overlayRef!.overlayElement.offsetLeft).toBeLessThan(triggerRect.left + 300);
259+
expect(tooltipDirective._overlayRef!.overlayElement.offsetTop).toBe(triggerRect.bottom);
260+
}));
261+
262+
it('should be able to override the default positionAtOrigin', fakeAsync(() => {
263+
TestBed.resetTestingModule()
264+
.configureTestingModule({
265+
imports: [MatLegacyTooltipModule, OverlayModule],
266+
declarations: [WideTooltipDemo],
267+
providers: [
268+
{
269+
provide: MAT_TOOLTIP_DEFAULT_OPTIONS,
270+
useValue: {positionAtOrigin: true},
271+
},
272+
],
273+
})
274+
.compileComponents();
275+
276+
const wideFixture = TestBed.createComponent(WideTooltipDemo);
277+
wideFixture.detectChanges();
278+
tooltipDirective = wideFixture.debugElement
279+
.query(By.css('button'))!
280+
.injector.get<MatLegacyTooltip>(MatLegacyTooltip);
281+
const button: HTMLButtonElement = wideFixture.nativeElement.querySelector('button');
282+
const triggerRect = button.getBoundingClientRect();
283+
284+
dispatchMouseEvent(button, 'mouseenter', triggerRect.left + 50, triggerRect.bottom - 10);
285+
wideFixture.detectChanges();
286+
tick();
287+
expect(tooltipDirective._isTooltipVisible()).toBe(true);
288+
289+
expect(tooltipDirective._overlayRef!.overlayElement.offsetLeft).toBe(triggerRect.left + 28);
290+
expect(tooltipDirective._overlayRef!.overlayElement.offsetTop).toBe(triggerRect.bottom - 10);
291+
}));
292+
235293
it('should be able to disable tooltip interactivity', fakeAsync(() => {
236294
TestBed.resetTestingModule()
237295
.configureTestingModule({
@@ -1169,7 +1227,10 @@ describe('MatTooltip', () => {
11691227
fixture.detectChanges();
11701228
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
11711229

1172-
dispatchFakeEvent(button, 'touchstart');
1230+
const triggerRect = button.getBoundingClientRect();
1231+
const offsetX = triggerRect.right - 10;
1232+
const offsetY = triggerRect.top + 10;
1233+
dispatchTouchEvent(button, 'touchstart', offsetX, offsetY, offsetX, offsetY);
11731234
fixture.detectChanges();
11741235
tick(250); // Halfway through the delay.
11751236

@@ -1188,7 +1249,10 @@ describe('MatTooltip', () => {
11881249
fixture.detectChanges();
11891250
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
11901251

1191-
dispatchFakeEvent(button, 'touchstart');
1252+
const triggerRect = button.getBoundingClientRect();
1253+
const offsetX = triggerRect.right - 10;
1254+
const offsetY = triggerRect.top + 10;
1255+
dispatchTouchEvent(button, 'touchstart', offsetX, offsetY, offsetX, offsetY);
11921256
fixture.detectChanges();
11931257
tick(500); // Finish the delay.
11941258
fixture.detectChanges();
@@ -1201,7 +1265,10 @@ describe('MatTooltip', () => {
12011265
const fixture = TestBed.createComponent(BasicTooltipDemo);
12021266
fixture.detectChanges();
12031267
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
1204-
const event = dispatchFakeEvent(button, 'touchstart');
1268+
const triggerRect = button.getBoundingClientRect();
1269+
const offsetX = triggerRect.right - 10;
1270+
const offsetY = triggerRect.top + 10;
1271+
const event = dispatchTouchEvent(button, 'touchstart', offsetX, offsetY, offsetX, offsetY);
12051272
fixture.detectChanges();
12061273

12071274
expect(event.defaultPrevented).toBe(false);
@@ -1212,7 +1279,10 @@ describe('MatTooltip', () => {
12121279
fixture.detectChanges();
12131280
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
12141281

1215-
dispatchFakeEvent(button, 'touchstart');
1282+
const triggerRect = button.getBoundingClientRect();
1283+
const offsetX = triggerRect.right - 10;
1284+
const offsetY = triggerRect.top + 10;
1285+
dispatchTouchEvent(button, 'touchstart', offsetX, offsetY, offsetX, offsetY);
12161286
fixture.detectChanges();
12171287
tick(500); // Finish the open delay.
12181288
fixture.detectChanges();
@@ -1236,7 +1306,10 @@ describe('MatTooltip', () => {
12361306
fixture.detectChanges();
12371307
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
12381308

1239-
dispatchFakeEvent(button, 'touchstart');
1309+
const triggerRect = button.getBoundingClientRect();
1310+
const offsetX = triggerRect.right - 10;
1311+
const offsetY = triggerRect.top + 10;
1312+
dispatchTouchEvent(button, 'touchstart', offsetX, offsetY, offsetX, offsetY);
12401313
fixture.detectChanges();
12411314
tick(500); // Finish the open delay.
12421315
fixture.detectChanges();
@@ -1401,16 +1474,16 @@ describe('MatTooltip', () => {
14011474
const fixture = TestBed.createComponent(BasicTooltipDemo);
14021475
fixture.detectChanges();
14031476
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
1477+
const triggerRect = button.getBoundingClientRect();
14041478

1405-
dispatchFakeEvent(button, 'mouseenter');
1479+
dispatchMouseEvent(button, 'mouseenter', triggerRect.right - 10, triggerRect.top + 10);
14061480
fixture.detectChanges();
14071481
tick(500); // Finish the open delay.
14081482
fixture.detectChanges();
14091483
finishCurrentTooltipAnimation(overlayContainerElement, true);
14101484
assertTooltipInstance(fixture.componentInstance.tooltip, true);
14111485

14121486
// Simulate the pointer over the trigger.
1413-
const triggerRect = button.getBoundingClientRect();
14141487
const wheelEvent = createFakeEvent('wheel');
14151488
Object.defineProperties(wheelEvent, {
14161489
clientX: {get: () => triggerRect.left + 1},
@@ -1556,6 +1629,17 @@ class TooltipDemoWithoutPositionBinding {
15561629
@ViewChild('button') button: ElementRef<HTMLButtonElement>;
15571630
}
15581631

1632+
@Component({
1633+
selector: 'app',
1634+
styles: [`button { width: 500px; height: 500px; }`],
1635+
template: `<button #button [matTooltip]="message">Button</button>`,
1636+
})
1637+
class WideTooltipDemo {
1638+
message = 'Test';
1639+
@ViewChild(MatLegacyTooltip) tooltip: MatLegacyTooltip;
1640+
@ViewChild('button') button: ElementRef<HTMLButtonElement>;
1641+
}
1642+
15591643
/** Asserts whether a tooltip directive has a tooltip instance. */
15601644
function assertTooltipInstance(tooltip: MatLegacyTooltip, shouldExist: boolean): void {
15611645
// Note that we have to cast this to a boolean, because Jasmine will go into an infinite loop

src/material/tooltip/tooltip.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ CSS class that can be used for style (e.g. to add an arrow). The possible classe
2727

2828
<!-- example(tooltip-position) -->
2929

30+
To display the tooltip relative to the mouse or touch that triggered it, use the
31+
`matTooltipPositionAtOrigin` input.
32+
With this setting turned on, the tooltip will display relative to the origin of the trigger rather
33+
than the host element. In cases where the tooltip is not triggered by a touch event or mouse click,
34+
it will display the same as if this setting was turned off.
35+
3036
### Showing and hiding
3137

3238
By default, the tooltip will be immediately shown when the user's mouse hovers over the tooltip's

0 commit comments

Comments
 (0)