Skip to content

Commit f6bc028

Browse files
committed
feat(tooltip): add arrow, placement, offset
1 parent c5ff1bd commit f6bc028

File tree

8 files changed

+328
-71
lines changed

8 files changed

+328
-71
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { mkenum } from '../../core/utils';
2+
3+
export const TooltipPlacement = /*@__PURE__*/mkenum({
4+
top: 'top',
5+
topStart: 'top-start',
6+
topEnd: 'top-end',
7+
bottom: 'bottom',
8+
bottomStart: 'bottom-start',
9+
bottomEnd: 'bottom-end',
10+
right: 'right',
11+
rightStart: 'right-start',
12+
rightEnd: 'right-end',
13+
left: 'left',
14+
leftStart: 'left-start',
15+
leftEnd: 'left-end'
16+
});
17+
export type TooltipPlacement = (typeof TooltipPlacement)[keyof typeof TooltipPlacement];

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { IgxTooltipDirective } from './tooltip.directive';
33

44
export * from './tooltip.directive';
55
export * from './tooltip-target.directive';
6+
export { TooltipPlacement } from './enums';
67

78
/* NOTE: Tooltip directives collection for ease-of-use import in standalone components scenario */
89
export const IGX_TOOLTIP_DIRECTIVES = [

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

Lines changed: 79 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import { useAnimation } from '@angular/animations';
2-
import { Directive, OnInit, OnDestroy, Output, ElementRef, Optional, ViewContainerRef, HostListener, Input, EventEmitter, booleanAttribute, TemplateRef, ComponentRef, Renderer2 } from '@angular/core';
2+
import { Directive, OnInit, OnDestroy, Output, ElementRef, Optional, ViewContainerRef, HostListener, Input, EventEmitter, booleanAttribute, TemplateRef, ComponentRef, Renderer2, AfterViewInit } from '@angular/core';
33
import { Subject } from 'rxjs';
44
import { takeUntil } from 'rxjs/operators';
55
import { IgxNavigationService } from '../../core/navigation';
66
import { IBaseEventArgs } from '../../core/utils';
7-
import { AutoPositionStrategy, HorizontalAlignment, PositionSettings } from '../../services/public_api';
7+
import { PositionSettings } from '../../services/public_api';
88
import { IgxToggleActionDirective } from '../toggle/toggle.directive';
99
import { IgxTooltipComponent } from './tooltip.component';
1010
import { IgxTooltipDirective } from './tooltip.directive';
1111
import { IgxTooltipCloseButtonComponent } from './tooltip-close-button.component';
1212
import { fadeOut, scaleInCenter } from 'igniteui-angular/animations';
13+
import { TooltipPlacement } from './enums';
14+
import { IgxTooltipPositionStrategy, PositionsMap, TooltipRegexes } from './tooltip.common';
15+
import { IgxDirectionality } from '../../services/direction/directionality';
1316

1417
export interface ITooltipShowEventArgs extends IBaseEventArgs {
1518
target: IgxTooltipTargetDirective;
@@ -41,7 +44,7 @@ export interface ITooltipHideEventArgs extends IBaseEventArgs {
4144
selector: '[igxTooltipTarget]',
4245
standalone: true
4346
})
44-
export class IgxTooltipTargetDirective extends IgxToggleActionDirective implements OnInit, OnDestroy {
47+
export class IgxTooltipTargetDirective extends IgxToggleActionDirective implements OnInit, AfterViewInit, OnDestroy {
4548

4649
private _closeButtonRef?: ComponentRef<IgxTooltipCloseButtonComponent>;
4750
private _closeTemplate: TemplateRef<any>;
@@ -81,6 +84,32 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
8184
@Input()
8285
public hideDelay = 300;
8386

87+
88+
/**
89+
* Where to place the tooltip relative to the target element. Default value is `top`.
90+
* ```html
91+
* <igx-icon [igxTooltipTarget]="tooltipRef" placement="bottom-start">info</igx-icon>
92+
* <span #tooltipRef="tooltip" igxTooltip>Hello there, I am a tooltip!</span>
93+
* ```
94+
*/
95+
@Input()
96+
public set placement(value: TooltipPlacement) {
97+
this._placement = value;
98+
99+
if (this._overlayDefaults && this.target) {
100+
this._overlayDefaults.positionStrategy = new IgxTooltipPositionStrategy(this._positionsSettingsByPlacement, value, this.offset);
101+
this.target.positionArrow(value, this._arrowOffset);
102+
}
103+
}
104+
105+
public get placement(): TooltipPlacement {
106+
return this._placement;
107+
}
108+
109+
/** The offset of the tooltip from the target in pixels. Default value is 6. */
110+
@Input()
111+
public offset = 6;
112+
84113
/**
85114
* Controls whether the arrow element of the tooltip is rendered.
86115
* Set to true to hide the arrow. Default value is `false`.
@@ -286,9 +315,15 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
286315
private _destroy$ = new Subject<void>();
287316
private _autoHideDelay = 180;
288317
private _isForceClosed = false;
289-
290-
constructor(private _element: ElementRef,
291-
@Optional() private _navigationService: IgxNavigationService, private _viewContainerRef: ViewContainerRef, private _renderer: Renderer2) {
318+
private _placement: TooltipPlacement = TooltipPlacement.top;
319+
320+
constructor(
321+
private _element: ElementRef,
322+
@Optional() private _navigationService: IgxNavigationService,
323+
private _viewContainerRef: ViewContainerRef,
324+
private _renderer: Renderer2,
325+
private _dir: IgxDirectionality,
326+
) {
292327
super(_element, _navigationService);
293328
}
294329

@@ -363,14 +398,7 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
363398
public override ngOnInit() {
364399
super.ngOnInit();
365400

366-
const positionSettings: PositionSettings = {
367-
horizontalDirection: HorizontalAlignment.Center,
368-
horizontalStartPoint: HorizontalAlignment.Center,
369-
openAnimation: useAnimation(scaleInCenter, { params: { duration: '150ms' } }),
370-
closeAnimation: useAnimation(fadeOut, { params: { duration: '75ms' } })
371-
};
372-
373-
this._overlayDefaults.positionStrategy = new AutoPositionStrategy(positionSettings);
401+
this._overlayDefaults.positionStrategy = new IgxTooltipPositionStrategy(this._positionsSettingsByPlacement, this.placement, this.offset);
374402
this._overlayDefaults.closeOnOutsideClick = false;
375403
this._overlayDefaults.closeOnEscape = true;
376404

@@ -387,6 +415,13 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
387415
this.target.onHide = this._hideOnInteraction.bind(this);
388416
}
389417

418+
/**
419+
* @hidden
420+
*/
421+
public ngAfterViewInit() {
422+
this.target.positionArrow(this.placement, this._arrowOffset);
423+
}
424+
390425
/**
391426
* @hidden
392427
*/
@@ -421,6 +456,36 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
421456
return Object.assign({}, this._overlayDefaults, this.overlaySettings);
422457
}
423458

459+
private get _positionsSettingsByPlacement(): PositionSettings {
460+
const positions = PositionsMap.get(this.placement);
461+
const animations = {
462+
openAnimation: useAnimation(scaleInCenter, { params: { duration: '150ms' } }),
463+
closeAnimation: useAnimation(fadeOut, { params: { duration: '75ms' } })
464+
}
465+
return Object.assign({}, animations, positions);
466+
}
467+
468+
private get _arrowOffset(): number {
469+
if (this.placement.includes('-')) {
470+
// Horizontal start | end placement
471+
if (TooltipRegexes.horizontalStart.test(this.placement)) {
472+
return -8;
473+
}
474+
if (TooltipRegexes.horizontalEnd.test(this.placement)) {
475+
return 8;
476+
}
477+
478+
// Vertical start | end placement
479+
if (TooltipRegexes.start.test(this.placement)) {
480+
return this._dir.rtl ? 8 : -8;
481+
}
482+
if (TooltipRegexes.end.test(this.placement)) {
483+
return this._dir.rtl ? -8 : 8;
484+
}
485+
}
486+
return 0;
487+
}
488+
424489
private _checkOutletAndOutsideClick(): void {
425490
if (this.outlet) {
426491
this._overlayDefaults.outlet = this.outlet;
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { AutoPositionStrategy } from '../../services/overlay/position/auto-position-strategy';
2+
import { ConnectedFit, HorizontalAlignment, PositionSettings, VerticalAlignment } from '../../services/overlay/utilities';
3+
import { TooltipPlacement } from './enums';
4+
5+
export const TooltipRegexes = Object.freeze({
6+
/** Matches horizontal `PopoverPlacement` start positions. */
7+
horizontalStart: /^(left|right)-start$/,
8+
9+
/** Matches horizontal `PopoverPlacement` end positions. */
10+
horizontalEnd: /^(left|right)-end$/,
11+
12+
/** Matches vertical `PopoverPlacement` start positions. */
13+
start: /start$/,
14+
15+
/** Matches vertical `PopoverPlacement` end positions. */
16+
end: /end$/,
17+
});
18+
19+
export const PositionsMap = new Map<TooltipPlacement, PositionSettings>([
20+
['top', {
21+
horizontalDirection: HorizontalAlignment.Center,
22+
horizontalStartPoint: HorizontalAlignment.Center,
23+
verticalDirection: VerticalAlignment.Top,
24+
verticalStartPoint: VerticalAlignment.Top,
25+
}],
26+
['top-start', {
27+
horizontalDirection: HorizontalAlignment.Right,
28+
horizontalStartPoint: HorizontalAlignment.Left,
29+
verticalDirection: VerticalAlignment.Top,
30+
verticalStartPoint: VerticalAlignment.Top,
31+
}],
32+
['top-end', {
33+
horizontalDirection: HorizontalAlignment.Left,
34+
horizontalStartPoint: HorizontalAlignment.Right,
35+
verticalDirection: VerticalAlignment.Top,
36+
verticalStartPoint: VerticalAlignment.Top,
37+
}],
38+
['bottom', {
39+
horizontalDirection: HorizontalAlignment.Center,
40+
horizontalStartPoint: HorizontalAlignment.Center,
41+
verticalDirection: VerticalAlignment.Bottom,
42+
verticalStartPoint: VerticalAlignment.Bottom,
43+
}],
44+
['bottom-start', {
45+
horizontalDirection: HorizontalAlignment.Right,
46+
horizontalStartPoint: HorizontalAlignment.Left,
47+
verticalDirection: VerticalAlignment.Bottom,
48+
verticalStartPoint: VerticalAlignment.Bottom,
49+
}],
50+
['bottom-end', {
51+
horizontalDirection: HorizontalAlignment.Left,
52+
horizontalStartPoint: HorizontalAlignment.Right,
53+
verticalDirection: VerticalAlignment.Bottom,
54+
verticalStartPoint: VerticalAlignment.Bottom,
55+
}],
56+
['right', {
57+
horizontalDirection: HorizontalAlignment.Right,
58+
horizontalStartPoint: HorizontalAlignment.Right,
59+
verticalDirection: VerticalAlignment.Middle,
60+
verticalStartPoint: VerticalAlignment.Middle,
61+
}],
62+
['right-start', {
63+
horizontalDirection: HorizontalAlignment.Right,
64+
horizontalStartPoint: HorizontalAlignment.Right,
65+
verticalDirection: VerticalAlignment.Bottom,
66+
verticalStartPoint: VerticalAlignment.Top,
67+
}],
68+
['right-end', {
69+
horizontalDirection: HorizontalAlignment.Right,
70+
horizontalStartPoint: HorizontalAlignment.Right,
71+
verticalDirection: VerticalAlignment.Top,
72+
verticalStartPoint: VerticalAlignment.Bottom,
73+
}],
74+
['left', {
75+
horizontalDirection: HorizontalAlignment.Left,
76+
horizontalStartPoint: HorizontalAlignment.Left,
77+
verticalDirection: VerticalAlignment.Middle,
78+
verticalStartPoint: VerticalAlignment.Middle,
79+
}],
80+
['left-start', {
81+
horizontalDirection: HorizontalAlignment.Left,
82+
horizontalStartPoint: HorizontalAlignment.Left,
83+
verticalDirection: VerticalAlignment.Bottom,
84+
verticalStartPoint: VerticalAlignment.Top,
85+
}],
86+
['left-end', {
87+
horizontalDirection: HorizontalAlignment.Left,
88+
horizontalStartPoint: HorizontalAlignment.Left,
89+
verticalDirection: VerticalAlignment.Top,
90+
verticalStartPoint: VerticalAlignment.Bottom,
91+
}]
92+
]);
93+
94+
export class IgxTooltipPositionStrategy extends AutoPositionStrategy {
95+
96+
constructor(
97+
settings: PositionSettings,
98+
private _placement: TooltipPlacement,
99+
private _offSet: number
100+
) {
101+
super(settings);
102+
}
103+
104+
protected override setStyle(
105+
element: HTMLElement,
106+
targetRect: Partial<DOMRect>,
107+
elementRect: Partial<DOMRect>,
108+
connectedFit: ConnectedFit
109+
): void {
110+
switch (this._placement) {
111+
case 'top':
112+
case 'top-start':
113+
case 'top-end':
114+
connectedFit.verticalOffset = -this._offSet;
115+
connectedFit.horizontalOffset = 0;
116+
break;
117+
118+
case 'bottom':
119+
case 'bottom-start':
120+
case 'bottom-end':
121+
connectedFit.verticalOffset = this._offSet;
122+
connectedFit.horizontalOffset = 0;
123+
break;
124+
125+
case 'right':
126+
case 'right-start':
127+
case 'right-end':
128+
connectedFit.verticalOffset = 0;
129+
connectedFit.horizontalOffset = this._offSet;
130+
break;
131+
132+
case 'left':
133+
case 'left-start':
134+
case 'left-end':
135+
connectedFit.verticalOffset = 0;
136+
connectedFit.horizontalOffset = -this._offSet;
137+
break;
138+
default:
139+
break;
140+
}
141+
142+
super.setStyle(element, targetRect, elementRect, connectedFit);
143+
}
144+
}

0 commit comments

Comments
 (0)