Skip to content

Commit f8263db

Browse files
refactor(tooltip): tooltip-v2 - aligned to how popover functions (#235)
@pimenovoleg merging :)
1 parent 40e14ba commit f8263db

39 files changed

+2938
-1091
lines changed

packages/primitives/core/src/positioning/utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export function getContentPosition(
1313
sideAndAlignWithOffsets: RdxPositionSideAndAlign & RdxPositionSideAndAlignOffsets
1414
): ConnectedPosition {
1515
const { side, align, sideOffset, alignOffset } = sideAndAlignWithOffsets;
16-
const position = {
16+
const position: ConnectedPosition = {
1717
...(RDX_POSITIONS[side]?.[align] ?? RDX_POSITIONS[RdxPositionSide.Top][RdxPositionAlign.Center])
1818
};
1919
if (sideOffset || alignOffset) {

packages/primitives/popover/stories/popover-multiple.component.ts

+17
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
2222
],
2323
styles: styles(),
2424
template: `
25+
<p class="ExampleSubtitle">Popover #1</p>
2526
<popover-with-option-panel
2627
[arrowWidth]="arrowWidth()"
2728
[arrowHeight]="arrowHeight()"
@@ -81,6 +82,7 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
8182
<div class="PopoverId">ID: {{ popoverRootDirective1()?.uniqueId() }}</div>
8283
</popover-with-option-panel>
8384
85+
<p class="ExampleSubtitle">Popover #2</p>
8486
<popover-with-option-panel
8587
[arrowWidth]="arrowWidth()"
8688
[arrowHeight]="arrowHeight()"
@@ -93,6 +95,13 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
9395
<lucide-angular [img]="TriangleAlert" size="16" />
9496
{{ containerAlert }}
9597
</div>
98+
<div class="ContainerAlerts">
99+
<lucide-angular [img]="TriangleAlert" size="16" />
100+
<code>[side]="'left'"</code>
101+
<code>[align]="'start'"</code>
102+
<code>[sideOffset]="16"</code>
103+
<code>[alignOffset]="16"</code>
104+
</div>
96105
<div class="container">
97106
<ng-container #root2="rdxPopoverRoot" rdxPopoverRoot>
98107
<button class="reset IconButton" rdxPopoverTrigger>
@@ -144,6 +153,7 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
144153
<div class="PopoverId">ID: {{ popoverRootDirective2()?.uniqueId() }}</div>
145154
</popover-with-option-panel>
146155
156+
<p class="ExampleSubtitle">Popover #3</p>
147157
<popover-with-option-panel
148158
[arrowWidth]="arrowWidth()"
149159
[arrowHeight]="arrowHeight()"
@@ -156,6 +166,13 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
156166
<lucide-angular [img]="TriangleAlert" size="16" />
157167
{{ containerAlert }}
158168
</div>
169+
<div class="ContainerAlerts">
170+
<lucide-angular [img]="TriangleAlert" size="16" />
171+
<code>[side]="'right'"</code>
172+
<code>[align]="'end'"</code>
173+
<code>[sideOffset]="60"</code>
174+
<code>[alignOffset]="60"</code>
175+
</div>
159176
<div class="container">
160177
<ng-container #root3="rdxPopoverRoot" rdxPopoverRoot>
161178
<button class="reset IconButton" rdxPopoverTrigger>

packages/primitives/popover/stories/popover.docs.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {RdxPopoverContentAttributesComponent} from "../src/popover-content-attri
1313

1414
# Popover
1515

16-
#### A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.
16+
#### A popup that displays information ready to interact with related to an element when the element receives click on it.
1717

1818
## Examples
1919
### Default

packages/primitives/tooltip/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
# @radix-ng/primitives/tooltip
2+
3+
Secondary entry point of `@radix-ng/primitives`. It can be used by importing from `@radix-ng/primitives/tooltip`.

packages/primitives/tooltip/index.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
11
import { NgModule } from '@angular/core';
2+
import { RdxTooltipAnchorDirective } from './src/tooltip-anchor.directive';
23
import { RdxTooltipArrowDirective } from './src/tooltip-arrow.directive';
3-
import { RdxTooltipContentAttributesDirective } from './src/tooltip-content-attributes.directive';
4+
import { RdxTooltipCloseDirective } from './src/tooltip-close.directive';
5+
import { RdxTooltipContentAttributesComponent } from './src/tooltip-content-attributes.component';
46
import { RdxTooltipContentDirective } from './src/tooltip-content.directive';
57
import { RdxTooltipRootDirective } from './src/tooltip-root.directive';
68
import { RdxTooltipTriggerDirective } from './src/tooltip-trigger.directive';
79

10+
export * from './src/tooltip-anchor.directive';
811
export * from './src/tooltip-arrow.directive';
9-
export * from './src/tooltip-content-attributes.directive';
12+
export * from './src/tooltip-close.directive';
13+
export * from './src/tooltip-content-attributes.component';
1014
export * from './src/tooltip-content.directive';
1115
export * from './src/tooltip-root.directive';
1216
export * from './src/tooltip-trigger.directive';
13-
export * from './src/tooltip.types';
1417

1518
const _imports = [
1619
RdxTooltipArrowDirective,
20+
RdxTooltipCloseDirective,
1721
RdxTooltipContentDirective,
1822
RdxTooltipTriggerDirective,
19-
RdxTooltipContentAttributesDirective,
20-
RdxTooltipRootDirective
23+
RdxTooltipRootDirective,
24+
RdxTooltipAnchorDirective,
25+
RdxTooltipContentAttributesComponent
2126
];
2227

2328
@NgModule({
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { CdkOverlayOrigin } from '@angular/cdk/overlay';
2+
import { computed, Directive, ElementRef, forwardRef, inject } from '@angular/core';
3+
import { injectDocument } from '@radix-ng/primitives/core';
4+
import { RdxTooltipAnchorToken } from './tooltip-anchor.token';
5+
import { RdxTooltipRootDirective } from './tooltip-root.directive';
6+
import { injectTooltipRoot } from './tooltip-root.inject';
7+
8+
@Directive({
9+
selector: '[rdxTooltipAnchor]',
10+
exportAs: 'rdxTooltipAnchor',
11+
hostDirectives: [CdkOverlayOrigin],
12+
host: {
13+
type: 'button',
14+
'[attr.id]': 'name()',
15+
'[attr.aria-haspopup]': '"dialog"',
16+
'(click)': 'click()'
17+
},
18+
providers: [
19+
{
20+
provide: RdxTooltipAnchorToken,
21+
useExisting: forwardRef(() => RdxTooltipAnchorDirective)
22+
}
23+
]
24+
})
25+
export class RdxTooltipAnchorDirective {
26+
/**
27+
* @ignore
28+
* If outside the rootDirective then null, otherwise the rootDirective directive - with optional `true` passed in as the first param.
29+
* If outside the rootDirective and non-null value that means the html structure is wrong - tooltip inside tooltip.
30+
* */
31+
protected rootDirective = injectTooltipRoot(true);
32+
/** @ignore */
33+
readonly elementRef = inject(ElementRef);
34+
/** @ignore */
35+
readonly overlayOrigin = inject(CdkOverlayOrigin);
36+
/** @ignore */
37+
readonly document = injectDocument();
38+
39+
/** @ignore */
40+
readonly name = computed(() => `rdx-tooltip-external-anchor-${this.rootDirective?.uniqueId()}`);
41+
42+
/** @ignore */
43+
click(): void {
44+
this.emitOutsideClick();
45+
}
46+
47+
/** @ignore */
48+
setRoot(root: RdxTooltipRootDirective) {
49+
this.rootDirective = root;
50+
}
51+
52+
private emitOutsideClick() {
53+
if (!this.rootDirective?.isOpen() || this.rootDirective?.contentDirective().onOverlayOutsideClickDisabled()) {
54+
return;
55+
}
56+
const clickEvent = new MouseEvent('click', {
57+
view: this.document.defaultView,
58+
bubbles: true,
59+
cancelable: true,
60+
relatedTarget: this.elementRef.nativeElement
61+
});
62+
this.rootDirective?.triggerDirective().elementRef.nativeElement.dispatchEvent(clickEvent);
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { InjectionToken } from '@angular/core';
2+
import { RdxTooltipAnchorDirective } from './tooltip-anchor.directive';
3+
4+
export const RdxTooltipAnchorToken = new InjectionToken<RdxTooltipAnchorDirective>('RdxTooltipAnchorToken');
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NumberInput } from '@angular/cdk/coercion';
2-
import { ConnectionPositionPair } from '@angular/cdk/overlay';
2+
import { ConnectedOverlayPositionChange } from '@angular/cdk/overlay';
33
import {
44
afterNextRender,
55
computed,
@@ -14,14 +14,17 @@ import {
1414
signal,
1515
untracked
1616
} from '@angular/core';
17-
import { getArrowPositionParams, getSideAndAlignFromAllPossibleConnectedPositions } from '@radix-ng/primitives/core';
17+
import { toSignal } from '@angular/core/rxjs-interop';
18+
import {
19+
getArrowPositionParams,
20+
getSideAndAlignFromAllPossibleConnectedPositions,
21+
RDX_POSITIONING_DEFAULTS
22+
} from '@radix-ng/primitives/core';
1823
import { RdxTooltipArrowToken } from './tooltip-arrow.token';
19-
import { RdxTooltipContentToken } from './tooltip-content.token';
20-
import { injectTooltipRoot } from './tooltip-root.directive';
24+
import { injectTooltipRoot } from './tooltip-root.inject';
2125

2226
@Directive({
2327
selector: '[rdxTooltipArrow]',
24-
standalone: true,
2528
providers: [
2629
{
2730
provide: RdxTooltipArrowToken,
@@ -30,32 +33,24 @@ import { injectTooltipRoot } from './tooltip-root.directive';
3033
]
3134
})
3235
export class RdxTooltipArrowDirective {
33-
/** @ignore */
34-
readonly tooltipRoot = injectTooltipRoot();
3536
/** @ignore */
3637
private readonly renderer = inject(Renderer2);
3738
/** @ignore */
38-
private readonly contentDirective = inject(RdxTooltipContentToken);
39+
private readonly rootDirective = injectTooltipRoot();
3940
/** @ignore */
40-
private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
41+
readonly elementRef = inject(ElementRef);
4142

4243
/**
43-
* The width of the arrow in pixels.
44+
* @description The width of the arrow in pixels.
45+
* @default 10
4446
*/
45-
readonly width = input<number, NumberInput>(10, { transform: numberAttribute });
47+
readonly width = input<number, NumberInput>(RDX_POSITIONING_DEFAULTS.arrow.width, { transform: numberAttribute });
4648

4749
/**
48-
* The height of the arrow in pixels.
50+
* @description The height of the arrow in pixels.
51+
* @default 5
4952
*/
50-
readonly height = input<number, NumberInput>(5, { transform: numberAttribute });
51-
52-
/**
53-
* @ignore
54-
* */
55-
private triggerRect: DOMRect;
56-
57-
/** @ignore */
58-
private readonly currentArrowSvgElement = signal<HTMLOrSVGElement | undefined>(void 0);
53+
readonly height = input<number, NumberInput>(RDX_POSITIONING_DEFAULTS.arrow.height, { transform: numberAttribute });
5954

6055
/** @ignore */
6156
readonly arrowSvgElement = computed<HTMLElement>(() => {
@@ -74,6 +69,14 @@ export class RdxTooltipArrowDirective {
7469
return svgElement;
7570
});
7671

72+
/** @ignore */
73+
private readonly currentArrowSvgElement = signal<HTMLOrSVGElement | undefined>(void 0);
74+
/** @ignore */
75+
private readonly position = toSignal(this.rootDirective.contentDirective().positionChange());
76+
77+
/** @ignore */
78+
private anchorOrTriggerRect: DOMRect;
79+
7780
constructor() {
7881
afterNextRender({
7982
write: () => {
@@ -85,20 +88,24 @@ export class RdxTooltipArrowDirective {
8588
this.renderer.setStyle(this.elementRef.nativeElement, 'fontSize', '0px');
8689
}
8790
});
91+
this.onArrowSvgElementChangeEffect();
92+
this.onContentPositionAndArrowDimensionsChangeEffect();
8893
}
8994

9095
/** @ignore */
91-
private setTriggerRect() {
92-
this.triggerRect = this.tooltipRoot.tooltipTriggerDirective().elementRef.nativeElement.getBoundingClientRect();
96+
private setAnchorOrTriggerRect() {
97+
this.anchorOrTriggerRect = (
98+
this.rootDirective.anchorDirective() ?? this.rootDirective.triggerDirective()
99+
).elementRef.nativeElement.getBoundingClientRect();
93100
}
94101

95102
/** @ignore */
96-
private setPosition(position: ConnectionPositionPair, arrowDimensions: { width: number; height: number }) {
97-
this.setTriggerRect();
103+
private setPosition(position: ConnectedOverlayPositionChange, arrowDimensions: { width: number; height: number }) {
104+
this.setAnchorOrTriggerRect();
98105
const posParams = getArrowPositionParams(
99-
getSideAndAlignFromAllPossibleConnectedPositions(position),
106+
getSideAndAlignFromAllPossibleConnectedPositions(position.connectionPair),
100107
{ width: arrowDimensions.width, height: arrowDimensions.height },
101-
{ width: this.triggerRect.width, height: this.triggerRect.height }
108+
{ width: this.anchorOrTriggerRect.width, height: this.anchorOrTriggerRect.height }
102109
);
103110

104111
this.renderer.setStyle(this.elementRef.nativeElement, 'top', posParams.top);
@@ -110,29 +117,33 @@ export class RdxTooltipArrowDirective {
110117
}
111118

112119
/** @ignore */
113-
private readonly onArrowSvgElementChangeEffect = effect(() => {
114-
const arrowElement = this.arrowSvgElement();
115-
untracked(() => {
116-
const currentArrowSvgElement = this.currentArrowSvgElement();
117-
if (currentArrowSvgElement) {
118-
this.renderer.removeChild(this.elementRef.nativeElement, currentArrowSvgElement);
119-
}
120-
this.currentArrowSvgElement.set(arrowElement);
121-
this.renderer.setStyle(this.elementRef.nativeElement, 'width', `${this.width()}px`);
122-
this.renderer.setStyle(this.elementRef.nativeElement, 'height', `${this.height()}px`);
123-
this.renderer.appendChild(this.elementRef.nativeElement, this.currentArrowSvgElement());
120+
private onArrowSvgElementChangeEffect() {
121+
effect(() => {
122+
const arrowElement = this.arrowSvgElement();
123+
untracked(() => {
124+
const currentArrowSvgElement = this.currentArrowSvgElement();
125+
if (currentArrowSvgElement) {
126+
this.renderer.removeChild(this.elementRef.nativeElement, currentArrowSvgElement);
127+
}
128+
this.currentArrowSvgElement.set(arrowElement);
129+
this.renderer.setStyle(this.elementRef.nativeElement, 'width', `${this.width()}px`);
130+
this.renderer.setStyle(this.elementRef.nativeElement, 'height', `${this.height()}px`);
131+
this.renderer.appendChild(this.elementRef.nativeElement, this.currentArrowSvgElement());
132+
});
124133
});
125-
});
134+
}
126135

127136
/** @ignore */
128-
private readonly onContentPositionAndArrowDimensionsChangeEffect = effect(() => {
129-
const position = this.contentDirective.position();
130-
const arrowDimensions = { width: this.width(), height: this.height() };
131-
untracked(() => {
132-
if (!position) {
133-
return;
134-
}
135-
this.setPosition(position, arrowDimensions);
137+
private onContentPositionAndArrowDimensionsChangeEffect() {
138+
effect(() => {
139+
const position = this.position();
140+
const arrowDimensions = { width: this.width(), height: this.height() };
141+
untracked(() => {
142+
if (!position) {
143+
return;
144+
}
145+
this.setPosition(position, arrowDimensions);
146+
});
136147
});
137-
});
148+
}
138149
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Directive, effect, ElementRef, forwardRef, inject, Renderer2, untracked } from '@angular/core';
2+
import { RdxTooltipCloseToken } from './tooltip-close.token';
3+
import { injectTooltipRoot } from './tooltip-root.inject';
4+
5+
/**
6+
* TODO: to be removed? But it seems to be useful when controlled from outside
7+
*/
8+
@Directive({
9+
selector: '[rdxTooltipClose]',
10+
host: {
11+
type: 'button',
12+
'(click)': 'rootDirective.handleClose(true)'
13+
},
14+
providers: [
15+
{
16+
provide: RdxTooltipCloseToken,
17+
useExisting: forwardRef(() => RdxTooltipCloseDirective)
18+
}
19+
]
20+
})
21+
export class RdxTooltipCloseDirective {
22+
/** @ignore */
23+
protected readonly rootDirective = injectTooltipRoot();
24+
/** @ignore */
25+
readonly elementRef = inject(ElementRef);
26+
/** @ignore */
27+
private readonly renderer = inject(Renderer2);
28+
29+
constructor() {
30+
this.onIsControlledExternallyEffect();
31+
}
32+
33+
/** @ignore */
34+
private onIsControlledExternallyEffect() {
35+
effect(() => {
36+
const isControlledExternally = this.rootDirective.controlledExternally()();
37+
38+
untracked(() => {
39+
this.renderer.setStyle(
40+
this.elementRef.nativeElement,
41+
'display',
42+
isControlledExternally ? null : 'none'
43+
);
44+
});
45+
});
46+
}
47+
}

0 commit comments

Comments
 (0)