Skip to content

Commit b7ed915

Browse files
committed
feat(tooltip): adjust arrow position
1 parent d312fcc commit b7ed915

File tree

3 files changed

+177
-93
lines changed

3 files changed

+177
-93
lines changed

projects/igniteui-angular/src/lib/directives/tooltip/tooltip-target.directive.ts

Lines changed: 40 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import { useAnimation } from '@angular/animations';
2-
import { Directive, OnInit, OnDestroy, Output, ElementRef, Optional, ViewContainerRef, HostListener, Input, EventEmitter, booleanAttribute, TemplateRef, ComponentRef, Renderer2, AfterViewInit } from '@angular/core';
2+
import {
3+
Directive, OnInit, OnDestroy, Output, ElementRef, Optional, ViewContainerRef, HostListener,
4+
Input, EventEmitter, booleanAttribute, TemplateRef, ComponentRef, Renderer2, OnChanges, SimpleChanges,
5+
} from '@angular/core';
36
import { Subject } from 'rxjs';
47
import { takeUntil } from 'rxjs/operators';
58
import { IgxNavigationService } from '../../core/navigation';
69
import { IBaseEventArgs } from '../../core/utils';
7-
import { PositionSettings } from '../../services/public_api';
10+
import { PositionSettings } from '../../services/public_api';
811
import { IgxToggleActionDirective } from '../toggle/toggle.directive';
912
import { IgxTooltipComponent } from './tooltip.component';
1013
import { IgxTooltipDirective } from './tooltip.directive';
1114
import { IgxTooltipCloseButtonComponent } from './tooltip-close-button.component';
1215
import { fadeOut, scaleInCenter } from 'igniteui-angular/animations';
1316
import { TooltipPlacement } from './enums';
14-
import { IgxTooltipPositionStrategy, PositionsMap, TooltipRegexes } from './tooltip.common';
15-
import { IgxDirectionality } from '../../services/direction/directionality';
17+
import { TooltipPositionStrategy, PositionsMap } from './tooltip.common';
1618

1719
export interface ITooltipShowEventArgs extends IBaseEventArgs {
1820
target: IgxTooltipTargetDirective;
@@ -44,12 +46,7 @@ export interface ITooltipHideEventArgs extends IBaseEventArgs {
4446
selector: '[igxTooltipTarget]',
4547
standalone: true
4648
})
47-
export class IgxTooltipTargetDirective extends IgxToggleActionDirective implements OnInit, AfterViewInit, OnDestroy {
48-
49-
private _closeButtonRef?: ComponentRef<IgxTooltipCloseButtonComponent>;
50-
private _closeTemplate: TemplateRef<any>;
51-
private _sticky = false;
52-
49+
export class IgxTooltipTargetDirective extends IgxToggleActionDirective implements OnChanges, OnInit, OnDestroy {
5350
/**
5451
* Gets/sets the amount of milliseconds that should pass before showing the tooltip.
5552
*
@@ -97,8 +94,7 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
9794
this._placement = value;
9895

9996
if (this._overlayDefaults && this.target) {
100-
this._overlayDefaults.positionStrategy = new IgxTooltipPositionStrategy(this._positionsSettingsByPlacement, value, this.offset);
101-
this.target.positionArrow(value, this._arrowOffset);
97+
this._overlayDefaults.positionStrategy = new TooltipPositionStrategy(this._positionsSettingsByPlacement, this);
10298
}
10399
}
104100

@@ -108,7 +104,17 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
108104

109105
/** The offset of the tooltip from the target in pixels. Default value is 6. */
110106
@Input()
111-
public offset = 6;
107+
public set offset(value: number) {
108+
this._offset = value;
109+
110+
if (this._overlayDefaults && this.target) {
111+
this._overlayDefaults.positionStrategy = new TooltipPositionStrategy(this._positionsSettingsByPlacement, this);
112+
}
113+
}
114+
115+
public get offset(): number {
116+
return this._offset;
117+
}
112118

113119
/**
114120
* Controls whether the arrow element of the tooltip is rendered.
@@ -131,11 +137,14 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
131137
*/
132138
@Input()
133139
public set disableArrow(value: boolean) {
134-
this.target.disableArrow = value;
140+
if (this.target) {
141+
this.target.arrow.style.display = value ? 'none' : '';
142+
}
143+
this._disableArrow = value;
135144
}
136145

137146
public get disableArrow(): boolean {
138-
return this.target.disableArrow;
147+
return this._disableArrow;
139148
}
140149

141150
/**
@@ -314,14 +323,18 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
314323
private _destroy$ = new Subject<void>();
315324
private _autoHideDelay = 180;
316325
private _isForceClosed = false;
326+
private _disableArrow = false;
327+
private _offset = 6;
317328
private _placement: TooltipPlacement = TooltipPlacement.top;
329+
private _closeButtonRef?: ComponentRef<IgxTooltipCloseButtonComponent>;
330+
private _closeTemplate: TemplateRef<any>;
331+
private _sticky = false;
318332

319333
constructor(
320334
private _element: ElementRef,
321335
@Optional() private _navigationService: IgxNavigationService,
322336
private _viewContainerRef: ViewContainerRef,
323337
private _renderer: Renderer2,
324-
private _dir: IgxDirectionality,
325338
) {
326339
super(_element, _navigationService);
327340
}
@@ -395,13 +408,23 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
395408
}
396409
}
397410

411+
412+
/**
413+
* @hidden
414+
*/
415+
public ngOnChanges(changes: SimpleChanges): void {
416+
if (changes['disableArrow']) {
417+
this.target.arrow.style.display = changes['disableArrow'].currentValue ? 'none' : '';
418+
}
419+
}
420+
398421
/**
399422
* @hidden
400423
*/
401424
public override ngOnInit() {
402425
super.ngOnInit();
403426

404-
this._overlayDefaults.positionStrategy = new IgxTooltipPositionStrategy(this._positionsSettingsByPlacement, this.placement, this.offset);
427+
this._overlayDefaults.positionStrategy = new TooltipPositionStrategy(this._positionsSettingsByPlacement, this);
405428
this._overlayDefaults.closeOnOutsideClick = false;
406429
this._overlayDefaults.closeOnEscape = true;
407430

@@ -418,13 +441,6 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
418441
this.target.onHide = this._hideOnInteraction.bind(this);
419442
}
420443

421-
/**
422-
* @hidden
423-
*/
424-
public ngAfterViewInit() {
425-
this.target.positionArrow(this.placement, this._arrowOffset);
426-
}
427-
428444
/**
429445
* @hidden
430446
*/
@@ -468,27 +484,6 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
468484
return Object.assign({}, animations, positions);
469485
}
470486

471-
private get _arrowOffset(): number {
472-
if (this.placement.includes('-')) {
473-
// Horizontal start | end placement
474-
if (TooltipRegexes.horizontalStart.test(this.placement)) {
475-
return -8;
476-
}
477-
if (TooltipRegexes.horizontalEnd.test(this.placement)) {
478-
return 8;
479-
}
480-
481-
// Vertical start | end placement
482-
if (TooltipRegexes.start.test(this.placement)) {
483-
return this._dir.rtl ? 8 : -8;
484-
}
485-
if (TooltipRegexes.end.test(this.placement)) {
486-
return this._dir.rtl ? -8 : 8;
487-
}
488-
}
489-
return 0;
490-
}
491-
492487
private _checkOutletAndOutsideClick(): void {
493488
if (this.outlet) {
494489
this._overlayDefaults.outlet = this.outlet;

projects/igniteui-angular/src/lib/directives/tooltip/tooltip.common.ts

Lines changed: 123 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
import { AutoPositionStrategy } from '../../services/overlay/position/auto-position-strategy';
2-
import { ConnectedFit, HorizontalAlignment, PositionSettings, VerticalAlignment } from '../../services/overlay/utilities';
2+
import { ConnectedFit, HorizontalAlignment, Point, PositionSettings, Size, VerticalAlignment } from '../../services/overlay/utilities';
3+
import { IgxTooltipTargetDirective } from './tooltip-target.directive';
34
import { TooltipPlacement } from './enums';
5+
import { first } from '../../core/utils';
46

57
export const TooltipRegexes = Object.freeze({
6-
/** Matches horizontal `PopoverPlacement` start positions. */
7-
horizontalStart: /^(left|right)-start$/,
8+
/** Matches horizontal `TooltipPlacement` end positions. `left-end` | `right-end` */
9+
horizontalEnd: /^(left|right)-end$/,
810

9-
/** Matches horizontal `PopoverPlacement` end positions. */
10-
horizontalEnd: /^(left|right)-end$/,
11+
/** Matches vertical `TooltipPlacement` centered positions. `left` | `right` */
12+
horizontalCenter: /^(left|right)$/,
1113

12-
/** Matches vertical `PopoverPlacement` start positions. */
13-
start: /start$/,
14+
/**
15+
* Matches vertical `TooltipPlacement` positions.
16+
* `top` | `top-start` | `top-end` | `bottom` | `bottom-start` | `bottom-end`
17+
*/
18+
vertical: /^(top|bottom)(-(start|end))?$/,
1419

15-
/** Matches vertical `PopoverPlacement` end positions. */
16-
end: /end$/,
20+
/** Matches vertical `TooltipPlacement` end positions. `top-end` | `bottom-end` */
21+
verticalEnd: /^(top|bottom)-end$/,
22+
23+
/** Matches vertical `TooltipPlacement` centered positions. `top` | `bottom` */
24+
verticalCenter: /^(top|bottom)$/,
1725
});
1826

1927
export const PositionsMap = new Map<TooltipPlacement, PositionSettings>([
@@ -91,14 +99,68 @@ export const PositionsMap = new Map<TooltipPlacement, PositionSettings>([
9199
}]
92100
]);
93101

94-
export class IgxTooltipPositionStrategy extends AutoPositionStrategy {
102+
export class TooltipPositionStrategy extends AutoPositionStrategy {
103+
private _placement: TooltipPlacement;
104+
private _offSet: number;
105+
private _arrow: HTMLElement;
106+
private _tooltipSize: DOMRect;
95107

96108
constructor(
97109
settings: PositionSettings,
98-
private _placement: TooltipPlacement,
99-
private _offSet: number
110+
tooltipTarget: IgxTooltipTargetDirective
100111
) {
101112
super(settings);
113+
114+
this._placement = tooltipTarget.placement;
115+
this._offSet = tooltipTarget.offset;
116+
this._arrow = tooltipTarget.target.arrow;
117+
}
118+
119+
public override position(
120+
contentElement: HTMLElement,
121+
size: Size,
122+
document?: Document,
123+
initialCall?: boolean,
124+
target?: Point | HTMLElement
125+
): void {
126+
this._tooltipSize = contentElement.getBoundingClientRect();
127+
this.positionArrow(this._arrow, this._placement);
128+
super.position(contentElement, size, document, initialCall, target);
129+
}
130+
131+
protected override fitInViewport(element: HTMLElement, connectedFit: ConnectedFit): void {
132+
super.fitInViewport(element, connectedFit);
133+
134+
this._placement = this._getPlacementByPositionSettings(this.settings);
135+
this.positionArrow(this._arrow, this._placement);
136+
}
137+
138+
/**
139+
* Sets the position of the arrow relative to the tooltip element.
140+
*
141+
* @param arrow the arrow element of the tooltip.
142+
* @param placement the placement of the tooltip. Used to determine where the arrow should render.
143+
*/
144+
protected positionArrow(arrow: HTMLElement, placement: TooltipPlacement) {
145+
this._resetArrowPositionStyles(arrow);
146+
147+
const currentPlacement = first(placement.split('-'));
148+
149+
// The opposite side where the arrow element should render based on the `currentPlacement`
150+
const staticSide = {
151+
top: 'bottom',
152+
right: 'left',
153+
bottom: 'top',
154+
left: 'right',
155+
}[currentPlacement]!;
156+
157+
arrow.className = `igx-tooltip--${currentPlacement}`;
158+
159+
Object.assign(arrow.style, {
160+
top: this._getArrowPositionStyles(placement, arrow, 'horizontal'),
161+
left: this._getArrowPositionStyles(placement, arrow, 'vertical'),
162+
[staticSide]: '-4px',
163+
});
102164
}
103165

104166
protected override setStyle(
@@ -141,4 +203,53 @@ export class IgxTooltipPositionStrategy extends AutoPositionStrategy {
141203

142204
super.setStyle(element, targetRect, elementRect, connectedFit);
143205
}
206+
207+
private _resetArrowPositionStyles(arrow: HTMLElement): void {
208+
arrow.style.top = '';
209+
arrow.style.bottom = '';
210+
arrow.style.left = '';
211+
arrow.style.right = '';
212+
}
213+
214+
private _getArrowPositionStyles(placement: TooltipPlacement, arrow: HTMLElement, direction: 'horizontal' | 'vertical') {
215+
const arrowRects = arrow.getBoundingClientRect();
216+
217+
const arrowSize = TooltipRegexes.vertical.test(placement)
218+
? arrowRects.width
219+
: arrowRects.height;
220+
221+
const tooltipSize = TooltipRegexes.vertical.test(placement)
222+
? this._tooltipSize.width
223+
: this._tooltipSize.height;
224+
225+
const center = `${direction}Center`
226+
const end = `${direction}End`
227+
228+
if (TooltipRegexes[center].test(placement)) {
229+
const offset = tooltipSize / 2 - arrowSize / 2;
230+
return `${Math.round(offset)}px`;
231+
}
232+
if (TooltipRegexes[end].test(placement)) {
233+
const endOffset = TooltipRegexes.vertical.test(placement) ? 8 : 4;
234+
const offset = tooltipSize - (endOffset + arrowSize);
235+
return `${Math.round(offset)}px`;
236+
}
237+
238+
return '';
239+
}
240+
241+
private _getPlacementByPositionSettings(settings: PositionSettings): TooltipPlacement {
242+
const { horizontalDirection, horizontalStartPoint, verticalDirection, verticalStartPoint } = settings;
243+
244+
const mapArray = Array.from(PositionsMap.entries());
245+
const placement = mapArray.find(
246+
([_, val]) =>
247+
val.horizontalDirection === horizontalDirection &&
248+
val.horizontalStartPoint === horizontalStartPoint &&
249+
val.verticalDirection === verticalDirection &&
250+
val.verticalStartPoint === verticalStartPoint
251+
);
252+
253+
return placement ? placement[0] : undefined;
254+
}
144255
}

0 commit comments

Comments
 (0)