Skip to content

Commit df20bf2

Browse files
authored
Revert "Revert "feat(material/tooltip): add option to open tooltip at… (#25439)
* Revert "Revert "feat(material/tooltip): add option to open tooltip at mouse position (#25202)" (#25430)" This reverts commit 08fba43. * fixup! Revert "Revert "feat(material/tooltip): add option to open tooltip at mouse position (#25202)" (#25430)"
1 parent aaf6a66 commit df20bf2

File tree

12 files changed

+269
-15
lines changed

12 files changed

+269
-15
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: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,77 @@ describe('MatTooltip', () => {
232232
expect(tooltipDirective._getOverlayPosition().fallback.overlayX).toBe('end');
233233
}));
234234

235+
it('should position center-bottom by default', fakeAsync(() => {
236+
// We don't bind mouse events on mobile devices.
237+
if (platform.IOS || platform.ANDROID) {
238+
return;
239+
}
240+
241+
TestBed.resetTestingModule()
242+
.configureTestingModule({
243+
imports: [MatLegacyTooltipModule, OverlayModule],
244+
declarations: [WideTooltipDemo],
245+
})
246+
.compileComponents();
247+
248+
const wideFixture = TestBed.createComponent(WideTooltipDemo);
249+
wideFixture.detectChanges();
250+
tooltipDirective = wideFixture.debugElement
251+
.query(By.css('button'))!
252+
.injector.get<MatLegacyTooltip>(MatLegacyTooltip);
253+
const button: HTMLButtonElement = wideFixture.nativeElement.querySelector('button');
254+
const triggerRect = button.getBoundingClientRect();
255+
256+
dispatchMouseEvent(button, 'mouseenter', triggerRect.right - 100, triggerRect.top + 100);
257+
wideFixture.detectChanges();
258+
tick();
259+
expect(tooltipDirective._isTooltipVisible()).toBe(true);
260+
261+
expect(tooltipDirective._overlayRef!.overlayElement.offsetLeft).toBeGreaterThan(
262+
triggerRect.left + 200,
263+
);
264+
expect(tooltipDirective._overlayRef!.overlayElement.offsetLeft).toBeLessThan(
265+
triggerRect.left + 300,
266+
);
267+
expect(tooltipDirective._overlayRef!.overlayElement.offsetTop).toBe(triggerRect.bottom);
268+
}));
269+
270+
it('should be able to override the default positionAtOrigin', fakeAsync(() => {
271+
// We don't bind mouse events on mobile devices.
272+
if (platform.IOS || platform.ANDROID) {
273+
return;
274+
}
275+
276+
TestBed.resetTestingModule()
277+
.configureTestingModule({
278+
imports: [MatLegacyTooltipModule, OverlayModule],
279+
declarations: [WideTooltipDemo],
280+
providers: [
281+
{
282+
provide: MAT_TOOLTIP_DEFAULT_OPTIONS,
283+
useValue: {positionAtOrigin: true},
284+
},
285+
],
286+
})
287+
.compileComponents();
288+
289+
const wideFixture = TestBed.createComponent(WideTooltipDemo);
290+
wideFixture.detectChanges();
291+
tooltipDirective = wideFixture.debugElement
292+
.query(By.css('button'))!
293+
.injector.get<MatLegacyTooltip>(MatLegacyTooltip);
294+
const button: HTMLButtonElement = wideFixture.nativeElement.querySelector('button');
295+
const triggerRect = button.getBoundingClientRect();
296+
297+
dispatchMouseEvent(button, 'mouseenter', triggerRect.left + 50, triggerRect.bottom - 10);
298+
wideFixture.detectChanges();
299+
tick();
300+
expect(tooltipDirective._isTooltipVisible()).toBe(true);
301+
302+
expect(tooltipDirective._overlayRef!.overlayElement.offsetLeft).toBe(triggerRect.left + 28);
303+
expect(tooltipDirective._overlayRef!.overlayElement.offsetTop).toBe(triggerRect.bottom - 10);
304+
}));
305+
235306
it('should be able to disable tooltip interactivity', fakeAsync(() => {
236307
TestBed.resetTestingModule()
237308
.configureTestingModule({
@@ -1556,6 +1627,17 @@ class TooltipDemoWithoutPositionBinding {
15561627
@ViewChild('button') button: ElementRef<HTMLButtonElement>;
15571628
}
15581629

1630+
@Component({
1631+
selector: 'app',
1632+
styles: [`button { width: 500px; height: 500px; }`],
1633+
template: `<button #button [matTooltip]="message">Button</button>`,
1634+
})
1635+
class WideTooltipDemo {
1636+
message = 'Test';
1637+
@ViewChild(MatLegacyTooltip) tooltip: MatLegacyTooltip;
1638+
@ViewChild('button') button: ElementRef<HTMLButtonElement>;
1639+
}
1640+
15591641
/** Asserts whether a tooltip directive has a tooltip instance. */
15601642
function assertTooltipInstance(tooltip: MatLegacyTooltip, shouldExist: boolean): void {
15611643
// 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

src/material/tooltip/tooltip.spec.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,78 @@ describe('MDC-based MatTooltip', () => {
234234
expect(tooltipDirective._getOverlayPosition().fallback.overlayX).toBe('end');
235235
}));
236236

237+
it('should position on the bottom-left by default', fakeAsync(() => {
238+
// We don't bind mouse events on mobile devices.
239+
if (platform.IOS || platform.ANDROID) {
240+
return;
241+
}
242+
243+
TestBed.resetTestingModule()
244+
.configureTestingModule({
245+
imports: [MatTooltipModule, OverlayModule],
246+
declarations: [WideTooltipDemo],
247+
})
248+
.compileComponents();
249+
250+
const wideFixture = TestBed.createComponent(WideTooltipDemo);
251+
wideFixture.detectChanges();
252+
tooltipDirective = wideFixture.debugElement
253+
.query(By.css('button'))!
254+
.injector.get<MatTooltip>(MatTooltip);
255+
const button: HTMLButtonElement = wideFixture.nativeElement.querySelector('button');
256+
const triggerRect = button.getBoundingClientRect();
257+
258+
dispatchMouseEvent(button, 'mouseenter', triggerRect.right - 100, triggerRect.top + 100);
259+
wideFixture.detectChanges();
260+
tick();
261+
expect(tooltipDirective._isTooltipVisible()).toBe(true);
262+
263+
expect(tooltipDirective._overlayRef!.overlayElement.offsetLeft).toBeLessThan(
264+
triggerRect.right - 250,
265+
);
266+
expect(tooltipDirective._overlayRef!.overlayElement.offsetTop).toBeGreaterThanOrEqual(
267+
triggerRect.bottom,
268+
);
269+
}));
270+
271+
it('should be able to override the default positionAtOrigin', fakeAsync(() => {
272+
// We don't bind mouse events on mobile devices.
273+
if (platform.IOS || platform.ANDROID) {
274+
return;
275+
}
276+
277+
TestBed.resetTestingModule()
278+
.configureTestingModule({
279+
imports: [MatTooltipModule, OverlayModule],
280+
declarations: [WideTooltipDemo],
281+
providers: [
282+
{
283+
provide: MAT_TOOLTIP_DEFAULT_OPTIONS,
284+
useValue: {positionAtOrigin: true},
285+
},
286+
],
287+
})
288+
.compileComponents();
289+
290+
const wideFixture = TestBed.createComponent(WideTooltipDemo);
291+
wideFixture.detectChanges();
292+
tooltipDirective = wideFixture.debugElement
293+
.query(By.css('button'))!
294+
.injector.get<MatTooltip>(MatTooltip);
295+
const button: HTMLButtonElement = wideFixture.nativeElement.querySelector('button');
296+
const triggerRect = button.getBoundingClientRect();
297+
298+
dispatchMouseEvent(button, 'mouseenter', triggerRect.right - 100, triggerRect.top + 100);
299+
wideFixture.detectChanges();
300+
tick();
301+
expect(tooltipDirective._isTooltipVisible()).toBe(true);
302+
303+
expect(tooltipDirective._overlayRef!.overlayElement.offsetLeft).toBe(
304+
triggerRect.right - 100 - 20,
305+
);
306+
expect(tooltipDirective._overlayRef!.overlayElement.offsetTop).toBe(triggerRect.top + 100);
307+
}));
308+
237309
it('should be able to disable tooltip interactivity', fakeAsync(() => {
238310
TestBed.resetTestingModule()
239311
.configureTestingModule({
@@ -1588,6 +1660,17 @@ class TooltipDemoWithoutPositionBinding {
15881660
@ViewChild('button') button: ElementRef<HTMLButtonElement>;
15891661
}
15901662

1663+
@Component({
1664+
selector: 'app',
1665+
styles: [`button { width: 500px; height: 500px; }`],
1666+
template: `<button #button [matTooltip]="message">Button</button>`,
1667+
})
1668+
class WideTooltipDemo {
1669+
message = 'Test';
1670+
@ViewChild(MatTooltip) tooltip: MatTooltip;
1671+
@ViewChild('button') button: ElementRef<HTMLButtonElement>;
1672+
}
1673+
15911674
/** Asserts whether a tooltip directive has a tooltip instance. */
15921675
function assertTooltipInstance(tooltip: MatTooltip, shouldExist: boolean): void {
15931676
// Note that we have to cast this to a boolean, because Jasmine will go into an infinite loop

0 commit comments

Comments
 (0)