Skip to content

Commit

Permalink
feat(material/tooltip): add option to open tooltip at mouse position (#…
Browse files Browse the repository at this point in the history
…25202)

Add an input option `matTooltipPositionAtOrigin` to display the tooltip relative to the mouse or touch event that triggered it.

Fixes #8759
  • Loading branch information
MikeJerred committed Aug 10, 2022
1 parent 1d1247c commit 1337f36
Show file tree
Hide file tree
Showing 10 changed files with 269 additions and 27 deletions.
Expand Up @@ -98,7 +98,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
_preferredPositions: ConnectionPositionPair[] = [];

/** The origin element against which the overlay will be positioned. */
private _origin: FlexibleConnectedPositionStrategyOrigin;
_origin: FlexibleConnectedPositionStrategyOrigin;

/** The overlay pane element. */
private _pane: HTMLElement;
Expand Down
3 changes: 3 additions & 0 deletions src/components-examples/material/tooltip/index.ts
Expand Up @@ -16,6 +16,7 @@ import {TooltipMessageExample} from './tooltip-message/tooltip-message-example';
import {TooltipModifiedDefaultsExample} from './tooltip-modified-defaults/tooltip-modified-defaults-example';
import {TooltipOverviewExample} from './tooltip-overview/tooltip-overview-example';
import {TooltipPositionExample} from './tooltip-position/tooltip-position-example';
import {TooltipPositionAtOriginExample} from './tooltip-position-at-origin/tooltip-position-at-origin-example';
import {TooltipHarnessExample} from './tooltip-harness/tooltip-harness-example';

export {
Expand All @@ -29,6 +30,7 @@ export {
TooltipModifiedDefaultsExample,
TooltipOverviewExample,
TooltipPositionExample,
TooltipPositionAtOriginExample,
};

const EXAMPLES = [
Expand All @@ -42,6 +44,7 @@ const EXAMPLES = [
TooltipModifiedDefaultsExample,
TooltipOverviewExample,
TooltipPositionExample,
TooltipPositionAtOriginExample,
];

@NgModule({
Expand Down
@@ -0,0 +1,8 @@
button {
width: 500px;
height: 500px;
}

.example-enabled-checkbox {
margin-left: 8px;
}
@@ -0,0 +1,10 @@
<button mat-raised-button
matTooltip="Info about the action"
[matTooltipPositionAtOrigin]="enabled.value"
aria-label="Button that displays a tooltip when focused or hovered over">
Action
</button>

<mat-checkbox [formControl]="enabled" class="example-enabled-checkbox">
Position at origin enabled
</mat-checkbox>
@@ -0,0 +1,14 @@
import {Component} from '@angular/core';
import {FormControl} from '@angular/forms';

/**
* @title Basic tooltip
*/
@Component({
selector: 'tooltip-position-at-origin-example',
templateUrl: 'tooltip-position-at-origin-example.html',
styleUrls: ['tooltip-position-at-origin-example.css'],
})
export class TooltipPositionAtOriginExample {
enabled = new FormControl(false);
}
3 changes: 3 additions & 0 deletions src/dev-app/tooltip/tooltip-demo.html
Expand Up @@ -24,3 +24,6 @@ <h3>Tooltip overview</h3>

<h3>Tooltip positioning</h3>
<tooltip-position-example></tooltip-position-example>

<h3>Tooltip with position at origin</h3>
<tooltip-position-at-origin-example></tooltip-position-at-origin-example>
98 changes: 91 additions & 7 deletions src/material/legacy-tooltip/tooltip.spec.ts
Expand Up @@ -11,6 +11,7 @@ import {
dispatchFakeEvent,
dispatchKeyboardEvent,
dispatchMouseEvent,
dispatchTouchEvent,
patchElementFocus,
} from '../../cdk/testing/private';
import {
Expand Down Expand Up @@ -232,6 +233,63 @@ describe('MatTooltip', () => {
expect(tooltipDirective._getOverlayPosition().fallback.overlayX).toBe('end');
}));

it('should position center-bottom by default', fakeAsync(() => {
TestBed.resetTestingModule()
.configureTestingModule({
imports: [MatLegacyTooltipModule, OverlayModule],
declarations: [WideTooltipDemo]
})
.compileComponents();

const wideFixture = TestBed.createComponent(WideTooltipDemo);
wideFixture.detectChanges();
tooltipDirective = wideFixture.debugElement
.query(By.css('button'))!
.injector.get<MatLegacyTooltip>(MatLegacyTooltip);
const button: HTMLButtonElement = wideFixture.nativeElement.querySelector('button');
const triggerRect = button.getBoundingClientRect();

dispatchMouseEvent(button, 'mouseenter', triggerRect.right - 100, triggerRect.top + 100);
wideFixture.detectChanges();
tick();
expect(tooltipDirective._isTooltipVisible()).toBe(true);

expect(tooltipDirective._overlayRef!.overlayElement.offsetLeft).toBeGreaterThan(triggerRect.left + 200);
expect(tooltipDirective._overlayRef!.overlayElement.offsetLeft).toBeLessThan(triggerRect.left + 300);
expect(tooltipDirective._overlayRef!.overlayElement.offsetTop).toBe(triggerRect.bottom);
}));

it('should be able to override the default positionAtOrigin', fakeAsync(() => {
TestBed.resetTestingModule()
.configureTestingModule({
imports: [MatLegacyTooltipModule, OverlayModule],
declarations: [WideTooltipDemo],
providers: [
{
provide: MAT_TOOLTIP_DEFAULT_OPTIONS,
useValue: {positionAtOrigin: true},
},
],
})
.compileComponents();

const wideFixture = TestBed.createComponent(WideTooltipDemo);
wideFixture.detectChanges();
tooltipDirective = wideFixture.debugElement
.query(By.css('button'))!
.injector.get<MatLegacyTooltip>(MatLegacyTooltip);
const button: HTMLButtonElement = wideFixture.nativeElement.querySelector('button');
const triggerRect = button.getBoundingClientRect();

dispatchMouseEvent(button, 'mouseenter', triggerRect.left + 50, triggerRect.bottom - 10);
wideFixture.detectChanges();
tick();
expect(tooltipDirective._isTooltipVisible()).toBe(true);

expect(tooltipDirective._overlayRef!.overlayElement.offsetLeft).toBe(triggerRect.left + 28);
expect(tooltipDirective._overlayRef!.overlayElement.offsetTop).toBe(triggerRect.bottom - 10);
}));

it('should be able to disable tooltip interactivity', fakeAsync(() => {
TestBed.resetTestingModule()
.configureTestingModule({
Expand Down Expand Up @@ -1169,7 +1227,10 @@ describe('MatTooltip', () => {
fixture.detectChanges();
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');

dispatchFakeEvent(button, 'touchstart');
const triggerRect = button.getBoundingClientRect();
const offsetX = triggerRect.right - 10;
const offsetY = triggerRect.top + 10;
dispatchTouchEvent(button, 'touchstart', offsetX, offsetY, offsetX, offsetY);
fixture.detectChanges();
tick(250); // Halfway through the delay.

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

dispatchFakeEvent(button, 'touchstart');
const triggerRect = button.getBoundingClientRect();
const offsetX = triggerRect.right - 10;
const offsetY = triggerRect.top + 10;
dispatchTouchEvent(button, 'touchstart', offsetX, offsetY, offsetX, offsetY);
fixture.detectChanges();
tick(500); // Finish the delay.
fixture.detectChanges();
Expand All @@ -1201,7 +1265,10 @@ describe('MatTooltip', () => {
const fixture = TestBed.createComponent(BasicTooltipDemo);
fixture.detectChanges();
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
const event = dispatchFakeEvent(button, 'touchstart');
const triggerRect = button.getBoundingClientRect();
const offsetX = triggerRect.right - 10;
const offsetY = triggerRect.top + 10;
const event = dispatchTouchEvent(button, 'touchstart', offsetX, offsetY, offsetX, offsetY);
fixture.detectChanges();

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

dispatchFakeEvent(button, 'touchstart');
const triggerRect = button.getBoundingClientRect();
const offsetX = triggerRect.right - 10;
const offsetY = triggerRect.top + 10;
dispatchTouchEvent(button, 'touchstart', offsetX, offsetY, offsetX, offsetY);
fixture.detectChanges();
tick(500); // Finish the open delay.
fixture.detectChanges();
Expand All @@ -1236,7 +1306,10 @@ describe('MatTooltip', () => {
fixture.detectChanges();
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');

dispatchFakeEvent(button, 'touchstart');
const triggerRect = button.getBoundingClientRect();
const offsetX = triggerRect.right - 10;
const offsetY = triggerRect.top + 10;
dispatchTouchEvent(button, 'touchstart', offsetX, offsetY, offsetX, offsetY);
fixture.detectChanges();
tick(500); // Finish the open delay.
fixture.detectChanges();
Expand Down Expand Up @@ -1401,16 +1474,16 @@ describe('MatTooltip', () => {
const fixture = TestBed.createComponent(BasicTooltipDemo);
fixture.detectChanges();
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
const triggerRect = button.getBoundingClientRect();

dispatchFakeEvent(button, 'mouseenter');
dispatchMouseEvent(button, 'mouseenter', triggerRect.right - 10, triggerRect.top + 10);
fixture.detectChanges();
tick(500); // Finish the open delay.
fixture.detectChanges();
finishCurrentTooltipAnimation(overlayContainerElement, true);
assertTooltipInstance(fixture.componentInstance.tooltip, true);

// Simulate the pointer over the trigger.
const triggerRect = button.getBoundingClientRect();
const wheelEvent = createFakeEvent('wheel');
Object.defineProperties(wheelEvent, {
clientX: {get: () => triggerRect.left + 1},
Expand Down Expand Up @@ -1556,6 +1629,17 @@ class TooltipDemoWithoutPositionBinding {
@ViewChild('button') button: ElementRef<HTMLButtonElement>;
}

@Component({
selector: 'app',
styles: [`button { width: 500px; height: 500px; }`],
template: `<button #button [matTooltip]="message">Button</button>`,
})
class WideTooltipDemo {
message = 'Test';
@ViewChild(MatLegacyTooltip) tooltip: MatLegacyTooltip;
@ViewChild('button') button: ElementRef<HTMLButtonElement>;
}

/** Asserts whether a tooltip directive has a tooltip instance. */
function assertTooltipInstance(tooltip: MatLegacyTooltip, shouldExist: boolean): void {
// Note that we have to cast this to a boolean, because Jasmine will go into an infinite loop
Expand Down
6 changes: 6 additions & 0 deletions src/material/tooltip/tooltip.md
Expand Up @@ -27,6 +27,12 @@ CSS class that can be used for style (e.g. to add an arrow). The possible classe

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

To display the tooltip relative to the mouse or touch that triggered it, use the
`matTooltipPositionAtOrigin` input.
With this setting turned on, the tooltip will display relative to the origin of the trigger rather
than the host element. In cases where the tooltip is not triggered by a touch event or mouse click,
it will display the same as if this setting was turned off.

### Showing and hiding

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

0 comments on commit 1337f36

Please sign in to comment.