From 6231c1648eb9fc380b0b9d3f6600b0b0f258dae7 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 10 Aug 2022 18:44:02 +0000 Subject: [PATCH 1/2] Revert "Revert "feat(material/tooltip): add option to open tooltip at mouse position (#25202)" (#25430)" This reverts commit 08fba438edee1948c016cee0952c9bce9ceceb5b. --- .../flexible-connected-position-strategy.ts | 2 +- .../material/tooltip/index.ts | 3 + .../tooltip-position-at-origin-example.css | 8 ++ .../tooltip-position-at-origin-example.html | 10 ++ .../tooltip-position-at-origin-example.ts | 14 +++ src/dev-app/tooltip/tooltip-demo.html | 3 + src/material/legacy-tooltip/tooltip.spec.ts | 98 +++++++++++++++-- src/material/tooltip/tooltip.md | 6 ++ src/material/tooltip/tooltip.spec.ts | 100 ++++++++++++++++-- src/material/tooltip/tooltip.ts | 52 +++++++-- 10 files changed, 269 insertions(+), 27 deletions(-) create mode 100644 src/components-examples/material/tooltip/tooltip-position-at-origin/tooltip-position-at-origin-example.css create mode 100644 src/components-examples/material/tooltip/tooltip-position-at-origin/tooltip-position-at-origin-example.html create mode 100644 src/components-examples/material/tooltip/tooltip-position-at-origin/tooltip-position-at-origin-example.ts diff --git a/src/cdk/overlay/position/flexible-connected-position-strategy.ts b/src/cdk/overlay/position/flexible-connected-position-strategy.ts index 65f68bc434d3..fc9103663a26 100644 --- a/src/cdk/overlay/position/flexible-connected-position-strategy.ts +++ b/src/cdk/overlay/position/flexible-connected-position-strategy.ts @@ -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; diff --git a/src/components-examples/material/tooltip/index.ts b/src/components-examples/material/tooltip/index.ts index e3d793ea17db..4ad892a840eb 100644 --- a/src/components-examples/material/tooltip/index.ts +++ b/src/components-examples/material/tooltip/index.ts @@ -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 { @@ -29,6 +30,7 @@ export { TooltipModifiedDefaultsExample, TooltipOverviewExample, TooltipPositionExample, + TooltipPositionAtOriginExample, }; const EXAMPLES = [ @@ -42,6 +44,7 @@ const EXAMPLES = [ TooltipModifiedDefaultsExample, TooltipOverviewExample, TooltipPositionExample, + TooltipPositionAtOriginExample, ]; @NgModule({ diff --git a/src/components-examples/material/tooltip/tooltip-position-at-origin/tooltip-position-at-origin-example.css b/src/components-examples/material/tooltip/tooltip-position-at-origin/tooltip-position-at-origin-example.css new file mode 100644 index 000000000000..82d14164d994 --- /dev/null +++ b/src/components-examples/material/tooltip/tooltip-position-at-origin/tooltip-position-at-origin-example.css @@ -0,0 +1,8 @@ +button { + width: 500px; + height: 500px; +} + +.example-enabled-checkbox { + margin-left: 8px; +} diff --git a/src/components-examples/material/tooltip/tooltip-position-at-origin/tooltip-position-at-origin-example.html b/src/components-examples/material/tooltip/tooltip-position-at-origin/tooltip-position-at-origin-example.html new file mode 100644 index 000000000000..e1aee7f8f4b7 --- /dev/null +++ b/src/components-examples/material/tooltip/tooltip-position-at-origin/tooltip-position-at-origin-example.html @@ -0,0 +1,10 @@ + + + + Position at origin enabled + diff --git a/src/components-examples/material/tooltip/tooltip-position-at-origin/tooltip-position-at-origin-example.ts b/src/components-examples/material/tooltip/tooltip-position-at-origin/tooltip-position-at-origin-example.ts new file mode 100644 index 000000000000..1bfb48e4fb1b --- /dev/null +++ b/src/components-examples/material/tooltip/tooltip-position-at-origin/tooltip-position-at-origin-example.ts @@ -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); +} diff --git a/src/dev-app/tooltip/tooltip-demo.html b/src/dev-app/tooltip/tooltip-demo.html index 0c7f2bede630..b848c7acd64f 100644 --- a/src/dev-app/tooltip/tooltip-demo.html +++ b/src/dev-app/tooltip/tooltip-demo.html @@ -24,3 +24,6 @@

Tooltip overview

Tooltip positioning

+ +

Tooltip with position at origin

+ diff --git a/src/material/legacy-tooltip/tooltip.spec.ts b/src/material/legacy-tooltip/tooltip.spec.ts index 72a816d402cc..e64798260154 100644 --- a/src/material/legacy-tooltip/tooltip.spec.ts +++ b/src/material/legacy-tooltip/tooltip.spec.ts @@ -11,6 +11,7 @@ import { dispatchFakeEvent, dispatchKeyboardEvent, dispatchMouseEvent, + dispatchTouchEvent, patchElementFocus, } from '../../cdk/testing/private'; import { @@ -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); + 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); + 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({ @@ -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. @@ -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(); @@ -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); @@ -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(); @@ -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(); @@ -1401,8 +1474,9 @@ 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(); @@ -1410,7 +1484,6 @@ describe('MatTooltip', () => { 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}, @@ -1556,6 +1629,17 @@ class TooltipDemoWithoutPositionBinding { @ViewChild('button') button: ElementRef; } +@Component({ + selector: 'app', + styles: [`button { width: 500px; height: 500px; }`], + template: ``, +}) +class WideTooltipDemo { + message = 'Test'; + @ViewChild(MatLegacyTooltip) tooltip: MatLegacyTooltip; + @ViewChild('button') button: ElementRef; +} + /** 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 diff --git a/src/material/tooltip/tooltip.md b/src/material/tooltip/tooltip.md index 69e2aaf44d29..079bd174d1b5 100644 --- a/src/material/tooltip/tooltip.md +++ b/src/material/tooltip/tooltip.md @@ -27,6 +27,12 @@ CSS class that can be used for style (e.g. to add an arrow). The possible classe +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 diff --git a/src/material/tooltip/tooltip.spec.ts b/src/material/tooltip/tooltip.spec.ts index 369779bad0e3..c44feb0266ce 100644 --- a/src/material/tooltip/tooltip.spec.ts +++ b/src/material/tooltip/tooltip.spec.ts @@ -11,6 +11,7 @@ import { dispatchFakeEvent, dispatchKeyboardEvent, dispatchMouseEvent, + dispatchTouchEvent, patchElementFocus, } from '../../cdk/testing/private'; import { @@ -234,6 +235,62 @@ describe('MDC-based MatTooltip', () => { expect(tooltipDirective._getOverlayPosition().fallback.overlayX).toBe('end'); })); + it('should position on the bottom-left by default', fakeAsync(() => { + TestBed.resetTestingModule() + .configureTestingModule({ + imports: [MatTooltipModule, OverlayModule], + declarations: [WideTooltipDemo] + }) + .compileComponents(); + + const wideFixture = TestBed.createComponent(WideTooltipDemo); + wideFixture.detectChanges(); + tooltipDirective = wideFixture.debugElement + .query(By.css('button'))! + .injector.get(MatTooltip); + 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).toBeLessThan(triggerRect.right - 250); + expect(tooltipDirective._overlayRef!.overlayElement.offsetTop).toBeGreaterThanOrEqual(triggerRect.bottom); + })); + + it('should be able to override the default positionAtOrigin', fakeAsync(() => { + TestBed.resetTestingModule() + .configureTestingModule({ + imports: [MatTooltipModule, 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(MatTooltip); + 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).toBe(triggerRect.right - 100 - 20); + expect(tooltipDirective._overlayRef!.overlayElement.offsetTop).toBe(triggerRect.top + 100); + })); + it('should be able to disable tooltip interactivity', fakeAsync(() => { TestBed.resetTestingModule() .configureTestingModule({ @@ -1201,7 +1258,10 @@ describe('MDC-based 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. @@ -1220,7 +1280,10 @@ describe('MDC-based 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(); @@ -1233,7 +1296,10 @@ describe('MDC-based 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); @@ -1244,7 +1310,10 @@ describe('MDC-based 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(); @@ -1268,7 +1337,10 @@ describe('MDC-based 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(); @@ -1400,8 +1472,9 @@ describe('MDC-based 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(); @@ -1433,8 +1506,9 @@ describe('MDC-based 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(); @@ -1442,7 +1516,6 @@ describe('MDC-based MatTooltip', () => { 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}, @@ -1588,6 +1661,17 @@ class TooltipDemoWithoutPositionBinding { @ViewChild('button') button: ElementRef; } +@Component({ + selector: 'app', + styles: [`button { width: 500px; height: 500px; }`], + template: ``, +}) +class WideTooltipDemo { + message = 'Test'; + @ViewChild(MatTooltip) tooltip: MatTooltip; + @ViewChild('button') button: ElementRef; +} + /** Asserts whether a tooltip directive has a tooltip instance. */ function assertTooltipInstance(tooltip: MatTooltip, shouldExist: boolean): void { // Note that we have to cast this to a boolean, because Jasmine will go into an infinite loop diff --git a/src/material/tooltip/tooltip.ts b/src/material/tooltip/tooltip.ts index 4dbb65e9d9f5..c0dfeda735c0 100644 --- a/src/material/tooltip/tooltip.ts +++ b/src/material/tooltip/tooltip.ts @@ -128,6 +128,12 @@ export interface MatTooltipDefaultOptions { /** Default position for tooltips. */ position?: TooltipPosition; + /** + * Default value for whether tooltips should be positioned near the click or touch origin + * instead of outside the element bounding box. + */ + positionAtOrigin?: boolean; + /** Disables the ability for the user to interact with the tooltip element. */ disableTooltipInteractivity?: boolean; } @@ -159,6 +165,7 @@ export abstract class _MatTooltipBase private _portal: ComponentPortal; private _position: TooltipPosition = 'below'; + private _positionAtOrigin: boolean = false; private _disabled: boolean = false; private _tooltipClass: string | string[] | Set | {[key: string]: any}; private _scrollStrategy: () => ScrollStrategy; @@ -187,6 +194,16 @@ export abstract class _MatTooltipBase } } + @Input('matTooltipPositionAtOrigin') + get positionAtOrigin(): boolean { + return this._positionAtOrigin; + } + set positionAtOrigin(value: BooleanInput) { + this._positionAtOrigin = coerceBooleanProperty(value); + this._detach(); + this._overlayRef = null; + } + /** Disables the display of the tooltip. */ @Input('matTooltipDisabled') get disabled(): boolean { @@ -329,6 +346,10 @@ export abstract class _MatTooltipBase this.position = _defaultOptions.position; } + if (_defaultOptions.positionAtOrigin) { + this.positionAtOrigin = _defaultOptions.positionAtOrigin; + } + if (_defaultOptions.touchGestures) { this.touchGestures = _defaultOptions.touchGestures; } @@ -386,7 +407,7 @@ export abstract class _MatTooltipBase } /** Shows the tooltip after the delay in ms, defaults to tooltip-delay-show or 0ms if no input */ - show(delay: number = this.showDelay): void { + show(delay: number = this.showDelay, origin?: { x: number, y: number }): void { if ( this.disabled || !this.message || @@ -397,7 +418,7 @@ export abstract class _MatTooltipBase return; } - const overlayRef = this._createOverlay(); + const overlayRef = this._createOverlay(origin); this._detach(); this._portal = this._portal || new ComponentPortal(this._tooltipComponent, this._viewContainerRef); @@ -421,8 +442,8 @@ export abstract class _MatTooltipBase } /** Shows/hides the tooltip */ - toggle(): void { - this._isTooltipVisible() ? this.hide() : this.show(); + toggle(origin?: { x: number; y: number; }): void { + this._isTooltipVisible() ? this.hide() : this.show(undefined, origin); } /** Returns true if the tooltip is currently visible to the user */ @@ -431,9 +452,16 @@ export abstract class _MatTooltipBase } /** Create the overlay config and position strategy */ - private _createOverlay(): OverlayRef { + private _createOverlay(origin?: { x: number; y: number; }): OverlayRef { if (this._overlayRef) { - return this._overlayRef; + const existingStrategy = + this._overlayRef.getConfig().positionStrategy as FlexibleConnectedPositionStrategy; + + if ((!this.positionAtOrigin || !origin) && existingStrategy._origin instanceof ElementRef) { + return this._overlayRef; + } + + this._detach(); } const scrollableAncestors = this._scrollDispatcher.getAncestorScrollContainers( @@ -443,7 +471,7 @@ export abstract class _MatTooltipBase // Create connected position strategy that listens for scroll events to reposition. const strategy = this._overlay .position() - .flexibleConnectedTo(this._elementRef) + .flexibleConnectedTo(this.positionAtOrigin ? (origin || this._elementRef) : this._elementRef) .withTransformOriginOn(`.${this._cssClassPrefix}-tooltip`) .withFlexibleDimensions(false) .withViewportMargin(this._viewportMargin) @@ -686,9 +714,9 @@ export abstract class _MatTooltipBase if (this._platformSupportsMouseEvents()) { this._passiveListeners.push([ 'mouseenter', - () => { + event => { this._setupPointerExitEventsIfNeeded(); - this.show(); + this.show(undefined, { x: (event as MouseEvent).x, y: (event as MouseEvent).y }); }, ]); } else if (this.touchGestures !== 'off') { @@ -696,12 +724,14 @@ export abstract class _MatTooltipBase this._passiveListeners.push([ 'touchstart', - () => { + event => { + const touch = (event as TouchEvent).targetTouches[0]; + const origin = touch ? { x: touch.clientX, y: touch.clientY } : undefined; // Note that it's important that we don't `preventDefault` here, // because it can prevent click events from firing on the element. this._setupPointerExitEventsIfNeeded(); clearTimeout(this._touchstartTimeout); - this._touchstartTimeout = setTimeout(() => this.show(), LONGPRESS_DELAY); + this._touchstartTimeout = setTimeout(() => this.show(undefined, origin), LONGPRESS_DELAY); }, ]); } From 0bc84457c7b1fd0f0bd4bcb84c67cd28248b9819 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 10 Aug 2022 21:20:25 +0000 Subject: [PATCH 2/2] fixup! Revert "Revert "feat(material/tooltip): add option to open tooltip at mouse position (#25202)" (#25430)" --- src/material/legacy-tooltip/tooltip.spec.ts | 50 +++++++++--------- src/material/tooltip/tooltip.spec.ts | 57 ++++++++++----------- src/material/tooltip/tooltip.ts | 22 ++++---- tools/public_api_guard/cdk/overlay.md | 1 + tools/public_api_guard/material/tooltip.md | 16 ++++-- 5 files changed, 79 insertions(+), 67 deletions(-) diff --git a/src/material/legacy-tooltip/tooltip.spec.ts b/src/material/legacy-tooltip/tooltip.spec.ts index e64798260154..213f143e9f9d 100644 --- a/src/material/legacy-tooltip/tooltip.spec.ts +++ b/src/material/legacy-tooltip/tooltip.spec.ts @@ -11,7 +11,6 @@ import { dispatchFakeEvent, dispatchKeyboardEvent, dispatchMouseEvent, - dispatchTouchEvent, patchElementFocus, } from '../../cdk/testing/private'; import { @@ -234,10 +233,15 @@ describe('MatTooltip', () => { })); it('should position center-bottom by default', fakeAsync(() => { + // We don't bind mouse events on mobile devices. + if (platform.IOS || platform.ANDROID) { + return; + } + TestBed.resetTestingModule() .configureTestingModule({ imports: [MatLegacyTooltipModule, OverlayModule], - declarations: [WideTooltipDemo] + declarations: [WideTooltipDemo], }) .compileComponents(); @@ -254,12 +258,21 @@ describe('MatTooltip', () => { 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.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(() => { + // We don't bind mouse events on mobile devices. + if (platform.IOS || platform.ANDROID) { + return; + } + TestBed.resetTestingModule() .configureTestingModule({ imports: [MatLegacyTooltipModule, OverlayModule], @@ -1227,10 +1240,7 @@ describe('MatTooltip', () => { fixture.detectChanges(); const button: HTMLButtonElement = fixture.nativeElement.querySelector('button'); - const triggerRect = button.getBoundingClientRect(); - const offsetX = triggerRect.right - 10; - const offsetY = triggerRect.top + 10; - dispatchTouchEvent(button, 'touchstart', offsetX, offsetY, offsetX, offsetY); + dispatchFakeEvent(button, 'touchstart'); fixture.detectChanges(); tick(250); // Halfway through the delay. @@ -1249,10 +1259,7 @@ describe('MatTooltip', () => { fixture.detectChanges(); const button: HTMLButtonElement = fixture.nativeElement.querySelector('button'); - const triggerRect = button.getBoundingClientRect(); - const offsetX = triggerRect.right - 10; - const offsetY = triggerRect.top + 10; - dispatchTouchEvent(button, 'touchstart', offsetX, offsetY, offsetX, offsetY); + dispatchFakeEvent(button, 'touchstart'); fixture.detectChanges(); tick(500); // Finish the delay. fixture.detectChanges(); @@ -1265,10 +1272,7 @@ describe('MatTooltip', () => { const fixture = TestBed.createComponent(BasicTooltipDemo); fixture.detectChanges(); const button: HTMLButtonElement = fixture.nativeElement.querySelector('button'); - const triggerRect = button.getBoundingClientRect(); - const offsetX = triggerRect.right - 10; - const offsetY = triggerRect.top + 10; - const event = dispatchTouchEvent(button, 'touchstart', offsetX, offsetY, offsetX, offsetY); + const event = dispatchFakeEvent(button, 'touchstart'); fixture.detectChanges(); expect(event.defaultPrevented).toBe(false); @@ -1279,10 +1283,7 @@ describe('MatTooltip', () => { fixture.detectChanges(); const button: HTMLButtonElement = fixture.nativeElement.querySelector('button'); - const triggerRect = button.getBoundingClientRect(); - const offsetX = triggerRect.right - 10; - const offsetY = triggerRect.top + 10; - dispatchTouchEvent(button, 'touchstart', offsetX, offsetY, offsetX, offsetY); + dispatchFakeEvent(button, 'touchstart'); fixture.detectChanges(); tick(500); // Finish the open delay. fixture.detectChanges(); @@ -1306,10 +1307,7 @@ describe('MatTooltip', () => { fixture.detectChanges(); const button: HTMLButtonElement = fixture.nativeElement.querySelector('button'); - const triggerRect = button.getBoundingClientRect(); - const offsetX = triggerRect.right - 10; - const offsetY = triggerRect.top + 10; - dispatchTouchEvent(button, 'touchstart', offsetX, offsetY, offsetX, offsetY); + dispatchFakeEvent(button, 'touchstart'); fixture.detectChanges(); tick(500); // Finish the open delay. fixture.detectChanges(); @@ -1474,9 +1472,8 @@ describe('MatTooltip', () => { const fixture = TestBed.createComponent(BasicTooltipDemo); fixture.detectChanges(); const button: HTMLButtonElement = fixture.nativeElement.querySelector('button'); - const triggerRect = button.getBoundingClientRect(); - dispatchMouseEvent(button, 'mouseenter', triggerRect.right - 10, triggerRect.top + 10); + dispatchFakeEvent(button, 'mouseenter'); fixture.detectChanges(); tick(500); // Finish the open delay. fixture.detectChanges(); @@ -1484,6 +1481,7 @@ describe('MatTooltip', () => { 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}, diff --git a/src/material/tooltip/tooltip.spec.ts b/src/material/tooltip/tooltip.spec.ts index c44feb0266ce..73f2e884ae24 100644 --- a/src/material/tooltip/tooltip.spec.ts +++ b/src/material/tooltip/tooltip.spec.ts @@ -11,7 +11,6 @@ import { dispatchFakeEvent, dispatchKeyboardEvent, dispatchMouseEvent, - dispatchTouchEvent, patchElementFocus, } from '../../cdk/testing/private'; import { @@ -236,10 +235,15 @@ describe('MDC-based MatTooltip', () => { })); it('should position on the bottom-left by default', fakeAsync(() => { + // We don't bind mouse events on mobile devices. + if (platform.IOS || platform.ANDROID) { + return; + } + TestBed.resetTestingModule() .configureTestingModule({ imports: [MatTooltipModule, OverlayModule], - declarations: [WideTooltipDemo] + declarations: [WideTooltipDemo], }) .compileComponents(); @@ -256,11 +260,20 @@ describe('MDC-based MatTooltip', () => { tick(); expect(tooltipDirective._isTooltipVisible()).toBe(true); - expect(tooltipDirective._overlayRef!.overlayElement.offsetLeft).toBeLessThan(triggerRect.right - 250); - expect(tooltipDirective._overlayRef!.overlayElement.offsetTop).toBeGreaterThanOrEqual(triggerRect.bottom); + expect(tooltipDirective._overlayRef!.overlayElement.offsetLeft).toBeLessThan( + triggerRect.right - 250, + ); + expect(tooltipDirective._overlayRef!.overlayElement.offsetTop).toBeGreaterThanOrEqual( + triggerRect.bottom, + ); })); it('should be able to override the default positionAtOrigin', fakeAsync(() => { + // We don't bind mouse events on mobile devices. + if (platform.IOS || platform.ANDROID) { + return; + } + TestBed.resetTestingModule() .configureTestingModule({ imports: [MatTooltipModule, OverlayModule], @@ -287,7 +300,9 @@ describe('MDC-based MatTooltip', () => { tick(); expect(tooltipDirective._isTooltipVisible()).toBe(true); - expect(tooltipDirective._overlayRef!.overlayElement.offsetLeft).toBe(triggerRect.right - 100 - 20); + expect(tooltipDirective._overlayRef!.overlayElement.offsetLeft).toBe( + triggerRect.right - 100 - 20, + ); expect(tooltipDirective._overlayRef!.overlayElement.offsetTop).toBe(triggerRect.top + 100); })); @@ -1258,10 +1273,7 @@ describe('MDC-based MatTooltip', () => { fixture.detectChanges(); const button: HTMLButtonElement = fixture.nativeElement.querySelector('button'); - const triggerRect = button.getBoundingClientRect(); - const offsetX = triggerRect.right - 10; - const offsetY = triggerRect.top + 10; - dispatchTouchEvent(button, 'touchstart', offsetX, offsetY, offsetX, offsetY); + dispatchFakeEvent(button, 'touchstart'); fixture.detectChanges(); tick(250); // Halfway through the delay. @@ -1280,10 +1292,7 @@ describe('MDC-based MatTooltip', () => { fixture.detectChanges(); const button: HTMLButtonElement = fixture.nativeElement.querySelector('button'); - const triggerRect = button.getBoundingClientRect(); - const offsetX = triggerRect.right - 10; - const offsetY = triggerRect.top + 10; - dispatchTouchEvent(button, 'touchstart', offsetX, offsetY, offsetX, offsetY); + dispatchFakeEvent(button, 'touchstart'); fixture.detectChanges(); tick(500); // Finish the delay. fixture.detectChanges(); @@ -1296,10 +1305,7 @@ describe('MDC-based MatTooltip', () => { const fixture = TestBed.createComponent(BasicTooltipDemo); fixture.detectChanges(); const button: HTMLButtonElement = fixture.nativeElement.querySelector('button'); - const triggerRect = button.getBoundingClientRect(); - const offsetX = triggerRect.right - 10; - const offsetY = triggerRect.top + 10; - const event = dispatchTouchEvent(button, 'touchstart', offsetX, offsetY, offsetX, offsetY); + const event = dispatchFakeEvent(button, 'touchstart'); fixture.detectChanges(); expect(event.defaultPrevented).toBe(false); @@ -1310,10 +1316,7 @@ describe('MDC-based MatTooltip', () => { fixture.detectChanges(); const button: HTMLButtonElement = fixture.nativeElement.querySelector('button'); - const triggerRect = button.getBoundingClientRect(); - const offsetX = triggerRect.right - 10; - const offsetY = triggerRect.top + 10; - dispatchTouchEvent(button, 'touchstart', offsetX, offsetY, offsetX, offsetY); + dispatchFakeEvent(button, 'touchstart'); fixture.detectChanges(); tick(500); // Finish the open delay. fixture.detectChanges(); @@ -1337,10 +1340,7 @@ describe('MDC-based MatTooltip', () => { fixture.detectChanges(); const button: HTMLButtonElement = fixture.nativeElement.querySelector('button'); - const triggerRect = button.getBoundingClientRect(); - const offsetX = triggerRect.right - 10; - const offsetY = triggerRect.top + 10; - dispatchTouchEvent(button, 'touchstart', offsetX, offsetY, offsetX, offsetY); + dispatchFakeEvent(button, 'touchstart'); fixture.detectChanges(); tick(500); // Finish the open delay. fixture.detectChanges(); @@ -1472,9 +1472,8 @@ describe('MDC-based MatTooltip', () => { const fixture = TestBed.createComponent(BasicTooltipDemo); fixture.detectChanges(); const button: HTMLButtonElement = fixture.nativeElement.querySelector('button'); - const triggerRect = button.getBoundingClientRect(); - dispatchMouseEvent(button, 'mouseenter', triggerRect.right - 10, triggerRect.top + 10); + dispatchFakeEvent(button, 'mouseenter'); fixture.detectChanges(); tick(500); // Finish the open delay. fixture.detectChanges(); @@ -1506,9 +1505,8 @@ describe('MDC-based MatTooltip', () => { const fixture = TestBed.createComponent(BasicTooltipDemo); fixture.detectChanges(); const button: HTMLButtonElement = fixture.nativeElement.querySelector('button'); - const triggerRect = button.getBoundingClientRect(); - dispatchMouseEvent(button, 'mouseenter', triggerRect.right - 10, triggerRect.top + 10); + dispatchFakeEvent(button, 'mouseenter'); fixture.detectChanges(); tick(500); // Finish the open delay. fixture.detectChanges(); @@ -1516,6 +1514,7 @@ describe('MDC-based MatTooltip', () => { 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}, diff --git a/src/material/tooltip/tooltip.ts b/src/material/tooltip/tooltip.ts index c0dfeda735c0..806e8b181675 100644 --- a/src/material/tooltip/tooltip.ts +++ b/src/material/tooltip/tooltip.ts @@ -407,7 +407,7 @@ export abstract class _MatTooltipBase } /** Shows the tooltip after the delay in ms, defaults to tooltip-delay-show or 0ms if no input */ - show(delay: number = this.showDelay, origin?: { x: number, y: number }): void { + show(delay: number = this.showDelay, origin?: {x: number; y: number}): void { if ( this.disabled || !this.message || @@ -442,7 +442,7 @@ export abstract class _MatTooltipBase } /** Shows/hides the tooltip */ - toggle(origin?: { x: number; y: number; }): void { + toggle(origin?: {x: number; y: number}): void { this._isTooltipVisible() ? this.hide() : this.show(undefined, origin); } @@ -452,10 +452,10 @@ export abstract class _MatTooltipBase } /** Create the overlay config and position strategy */ - private _createOverlay(origin?: { x: number; y: number; }): OverlayRef { + private _createOverlay(origin?: {x: number; y: number}): OverlayRef { if (this._overlayRef) { - const existingStrategy = - this._overlayRef.getConfig().positionStrategy as FlexibleConnectedPositionStrategy; + const existingStrategy = this._overlayRef.getConfig() + .positionStrategy as FlexibleConnectedPositionStrategy; if ((!this.positionAtOrigin || !origin) && existingStrategy._origin instanceof ElementRef) { return this._overlayRef; @@ -471,7 +471,7 @@ export abstract class _MatTooltipBase // Create connected position strategy that listens for scroll events to reposition. const strategy = this._overlay .position() - .flexibleConnectedTo(this.positionAtOrigin ? (origin || this._elementRef) : this._elementRef) + .flexibleConnectedTo(this.positionAtOrigin ? origin || this._elementRef : this._elementRef) .withTransformOriginOn(`.${this._cssClassPrefix}-tooltip`) .withFlexibleDimensions(false) .withViewportMargin(this._viewportMargin) @@ -716,7 +716,11 @@ export abstract class _MatTooltipBase 'mouseenter', event => { this._setupPointerExitEventsIfNeeded(); - this.show(undefined, { x: (event as MouseEvent).x, y: (event as MouseEvent).y }); + let point = undefined; + if ((event as MouseEvent).x !== undefined && (event as MouseEvent).y !== undefined) { + point = event as MouseEvent; + } + this.show(undefined, point); }, ]); } else if (this.touchGestures !== 'off') { @@ -725,8 +729,8 @@ export abstract class _MatTooltipBase this._passiveListeners.push([ 'touchstart', event => { - const touch = (event as TouchEvent).targetTouches[0]; - const origin = touch ? { x: touch.clientX, y: touch.clientY } : undefined; + const touch = (event as TouchEvent).targetTouches?.[0]; + const origin = touch ? {x: touch.clientX, y: touch.clientY} : undefined; // Note that it's important that we don't `preventDefault` here, // because it can prevent click events from firing on the element. this._setupPointerExitEventsIfNeeded(); diff --git a/tools/public_api_guard/cdk/overlay.md b/tools/public_api_guard/cdk/overlay.md index a2ccaee9a40d..31371ac3d2db 100644 --- a/tools/public_api_guard/cdk/overlay.md +++ b/tools/public_api_guard/cdk/overlay.md @@ -184,6 +184,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy { // (undocumented) detach(): void; dispose(): void; + _origin: FlexibleConnectedPositionStrategyOrigin; positionChanges: Observable; get positions(): ConnectionPositionPair[]; _preferredPositions: ConnectionPositionPair[]; diff --git a/tools/public_api_guard/material/tooltip.md b/tools/public_api_guard/material/tooltip.md index 531b85e0e7ea..7dba5dde8140 100644 --- a/tools/public_api_guard/material/tooltip.md +++ b/tools/public_api_guard/material/tooltip.md @@ -107,10 +107,19 @@ export abstract class _MatTooltipBase implement _overlayRef: OverlayRef | null; get position(): TooltipPosition; set position(value: TooltipPosition); - show(delay?: number): void; + // (undocumented) + get positionAtOrigin(): boolean; + set positionAtOrigin(value: BooleanInput); + show(delay?: number, origin?: { + x: number; + y: number; + }): void; get showDelay(): number; set showDelay(value: NumberInput); - toggle(): void; + toggle(origin?: { + x: number; + y: number; + }): void; get tooltipClass(): string | string[] | Set | { [key: string]: any; }; @@ -125,7 +134,7 @@ export abstract class _MatTooltipBase implement // (undocumented) protected _viewportMargin: number; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration<_MatTooltipBase, never, never, { "position": "matTooltipPosition"; "disabled": "matTooltipDisabled"; "showDelay": "matTooltipShowDelay"; "hideDelay": "matTooltipHideDelay"; "touchGestures": "matTooltipTouchGestures"; "message": "matTooltip"; "tooltipClass": "matTooltipClass"; }, {}, never, never, false>; + static ɵdir: i0.ɵɵDirectiveDeclaration<_MatTooltipBase, never, never, { "position": "matTooltipPosition"; "positionAtOrigin": "matTooltipPositionAtOrigin"; "disabled": "matTooltipDisabled"; "showDelay": "matTooltipShowDelay"; "hideDelay": "matTooltipHideDelay"; "touchGestures": "matTooltipTouchGestures"; "message": "matTooltip"; "tooltipClass": "matTooltipClass"; }, {}, never, never, false>; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration<_MatTooltipBase, never>; } @@ -135,6 +144,7 @@ export interface MatTooltipDefaultOptions { disableTooltipInteractivity?: boolean; hideDelay: number; position?: TooltipPosition; + positionAtOrigin?: boolean; showDelay: number; touchendHideDelay: number; touchGestures?: TooltipTouchGestures;