Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(tooltip): tooltip-v2 - aligned to how popover functions #235

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/primitives/core/src/positioning/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function getContentPosition(
sideAndAlignWithOffsets: RdxPositionSideAndAlign & RdxPositionSideAndAlignOffsets
): ConnectedPosition {
const { side, align, sideOffset, alignOffset } = sideAndAlignWithOffsets;
const position = {
const position: ConnectedPosition = {
...(RDX_POSITIONS[side]?.[align] ?? RDX_POSITIONS[RdxPositionSide.Top][RdxPositionAlign.Center])
};
if (sideOffset || alignOffset) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
],
styles: styles(),
template: `
<p class="ExampleSubtitle">Popover #1</p>
<popover-with-option-panel
[arrowWidth]="arrowWidth()"
[arrowHeight]="arrowHeight()"
Expand Down Expand Up @@ -81,6 +82,7 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
<div class="PopoverId">ID: {{ popoverRootDirective1()?.uniqueId() }}</div>
</popover-with-option-panel>

<p class="ExampleSubtitle">Popover #2</p>
<popover-with-option-panel
[arrowWidth]="arrowWidth()"
[arrowHeight]="arrowHeight()"
Expand All @@ -93,6 +95,13 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
<lucide-angular [img]="TriangleAlert" size="16" />
{{ containerAlert }}
</div>
<div class="ContainerAlerts">
<lucide-angular [img]="TriangleAlert" size="16" />
<code>[side]="'left'"</code>
<code>[align]="'start'"</code>
<code>[sideOffset]="16"</code>
<code>[alignOffset]="16"</code>
</div>
<div class="container">
<ng-container #root2="rdxPopoverRoot" rdxPopoverRoot>
<button class="reset IconButton" rdxPopoverTrigger>
Expand Down Expand Up @@ -144,6 +153,7 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
<div class="PopoverId">ID: {{ popoverRootDirective2()?.uniqueId() }}</div>
</popover-with-option-panel>

<p class="ExampleSubtitle">Popover #3</p>
<popover-with-option-panel
[arrowWidth]="arrowWidth()"
[arrowHeight]="arrowHeight()"
Expand All @@ -156,6 +166,13 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
<lucide-angular [img]="TriangleAlert" size="16" />
{{ containerAlert }}
</div>
<div class="ContainerAlerts">
<lucide-angular [img]="TriangleAlert" size="16" />
<code>[side]="'right'"</code>
<code>[align]="'end'"</code>
<code>[sideOffset]="60"</code>
<code>[alignOffset]="60"</code>
</div>
<div class="container">
<ng-container #root3="rdxPopoverRoot" rdxPopoverRoot>
<button class="reset IconButton" rdxPopoverTrigger>
Expand Down
2 changes: 1 addition & 1 deletion packages/primitives/popover/stories/popover.docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {RdxPopoverContentAttributesComponent} from "../src/popover-content-attri

# Popover

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

## Examples
### Default
Expand Down
2 changes: 2 additions & 0 deletions packages/primitives/tooltip/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# @radix-ng/primitives/tooltip

Secondary entry point of `@radix-ng/primitives`. It can be used by importing from `@radix-ng/primitives/tooltip`.
15 changes: 10 additions & 5 deletions packages/primitives/tooltip/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import { NgModule } from '@angular/core';
import { RdxTooltipAnchorDirective } from './src/tooltip-anchor.directive';
import { RdxTooltipArrowDirective } from './src/tooltip-arrow.directive';
import { RdxTooltipContentAttributesDirective } from './src/tooltip-content-attributes.directive';
import { RdxTooltipCloseDirective } from './src/tooltip-close.directive';
import { RdxTooltipContentAttributesComponent } from './src/tooltip-content-attributes.component';
import { RdxTooltipContentDirective } from './src/tooltip-content.directive';
import { RdxTooltipRootDirective } from './src/tooltip-root.directive';
import { RdxTooltipTriggerDirective } from './src/tooltip-trigger.directive';

export * from './src/tooltip-anchor.directive';
export * from './src/tooltip-arrow.directive';
export * from './src/tooltip-content-attributes.directive';
export * from './src/tooltip-close.directive';
export * from './src/tooltip-content-attributes.component';
export * from './src/tooltip-content.directive';
export * from './src/tooltip-root.directive';
export * from './src/tooltip-trigger.directive';
export * from './src/tooltip.types';

const _imports = [
RdxTooltipArrowDirective,
RdxTooltipCloseDirective,
RdxTooltipContentDirective,
RdxTooltipTriggerDirective,
RdxTooltipContentAttributesDirective,
RdxTooltipRootDirective
RdxTooltipRootDirective,
RdxTooltipAnchorDirective,
RdxTooltipContentAttributesComponent
];

@NgModule({
Expand Down
64 changes: 64 additions & 0 deletions packages/primitives/tooltip/src/tooltip-anchor.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { CdkOverlayOrigin } from '@angular/cdk/overlay';
import { computed, Directive, ElementRef, forwardRef, inject } from '@angular/core';
import { injectDocument } from '@radix-ng/primitives/core';
import { RdxTooltipAnchorToken } from './tooltip-anchor.token';
import { RdxTooltipRootDirective } from './tooltip-root.directive';
import { injectTooltipRoot } from './tooltip-root.inject';

@Directive({
selector: '[rdxTooltipAnchor]',
exportAs: 'rdxTooltipAnchor',
hostDirectives: [CdkOverlayOrigin],
host: {
type: 'button',
'[attr.id]': 'name()',
'[attr.aria-haspopup]': '"dialog"',
'(click)': 'click()'
},
providers: [
{
provide: RdxTooltipAnchorToken,
useExisting: forwardRef(() => RdxTooltipAnchorDirective)
}
]
})
export class RdxTooltipAnchorDirective {
/**
* @ignore
* If outside the rootDirective then null, otherwise the rootDirective directive - with optional `true` passed in as the first param.
* If outside the rootDirective and non-null value that means the html structure is wrong - tooltip inside tooltip.
* */
protected rootDirective = injectTooltipRoot(true);
/** @ignore */
readonly elementRef = inject(ElementRef);
/** @ignore */
readonly overlayOrigin = inject(CdkOverlayOrigin);
/** @ignore */
readonly document = injectDocument();

/** @ignore */
readonly name = computed(() => `rdx-tooltip-external-anchor-${this.rootDirective?.uniqueId()}`);

/** @ignore */
click(): void {
this.emitOutsideClick();
}

/** @ignore */
setRoot(root: RdxTooltipRootDirective) {
this.rootDirective = root;
}

private emitOutsideClick() {
if (!this.rootDirective?.isOpen() || this.rootDirective?.contentDirective().onOverlayOutsideClickDisabled()) {
return;
}
const clickEvent = new MouseEvent('click', {
view: this.document.defaultView,
bubbles: true,
cancelable: true,
relatedTarget: this.elementRef.nativeElement
});
this.rootDirective?.triggerDirective().elementRef.nativeElement.dispatchEvent(clickEvent);
}
}
4 changes: 4 additions & 0 deletions packages/primitives/tooltip/src/tooltip-anchor.token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { InjectionToken } from '@angular/core';
import { RdxTooltipAnchorDirective } from './tooltip-anchor.directive';

export const RdxTooltipAnchorToken = new InjectionToken<RdxTooltipAnchorDirective>('RdxTooltipAnchorToken');
107 changes: 59 additions & 48 deletions packages/primitives/tooltip/src/tooltip-arrow.directive.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NumberInput } from '@angular/cdk/coercion';
import { ConnectionPositionPair } from '@angular/cdk/overlay';
import { ConnectedOverlayPositionChange } from '@angular/cdk/overlay';
import {
afterNextRender,
computed,
Expand All @@ -14,14 +14,17 @@ import {
signal,
untracked
} from '@angular/core';
import { getArrowPositionParams, getSideAndAlignFromAllPossibleConnectedPositions } from '@radix-ng/primitives/core';
import { toSignal } from '@angular/core/rxjs-interop';
import {
getArrowPositionParams,
getSideAndAlignFromAllPossibleConnectedPositions,
RDX_POSITIONING_DEFAULTS
} from '@radix-ng/primitives/core';
import { RdxTooltipArrowToken } from './tooltip-arrow.token';
import { RdxTooltipContentToken } from './tooltip-content.token';
import { injectTooltipRoot } from './tooltip-root.directive';
import { injectTooltipRoot } from './tooltip-root.inject';

@Directive({
selector: '[rdxTooltipArrow]',
standalone: true,
providers: [
{
provide: RdxTooltipArrowToken,
Expand All @@ -30,32 +33,24 @@ import { injectTooltipRoot } from './tooltip-root.directive';
]
})
export class RdxTooltipArrowDirective {
/** @ignore */
readonly tooltipRoot = injectTooltipRoot();
/** @ignore */
private readonly renderer = inject(Renderer2);
/** @ignore */
private readonly contentDirective = inject(RdxTooltipContentToken);
private readonly rootDirective = injectTooltipRoot();
/** @ignore */
private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
readonly elementRef = inject(ElementRef);

/**
* The width of the arrow in pixels.
* @description The width of the arrow in pixels.
* @default 10
*/
readonly width = input<number, NumberInput>(10, { transform: numberAttribute });
readonly width = input<number, NumberInput>(RDX_POSITIONING_DEFAULTS.arrow.width, { transform: numberAttribute });

/**
* The height of the arrow in pixels.
* @description The height of the arrow in pixels.
* @default 5
*/
readonly height = input<number, NumberInput>(5, { transform: numberAttribute });

/**
* @ignore
* */
private triggerRect: DOMRect;

/** @ignore */
private readonly currentArrowSvgElement = signal<HTMLOrSVGElement | undefined>(void 0);
readonly height = input<number, NumberInput>(RDX_POSITIONING_DEFAULTS.arrow.height, { transform: numberAttribute });

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

/** @ignore */
private readonly currentArrowSvgElement = signal<HTMLOrSVGElement | undefined>(void 0);
/** @ignore */
private readonly position = toSignal(this.rootDirective.contentDirective().positionChange());

/** @ignore */
private anchorOrTriggerRect: DOMRect;

constructor() {
afterNextRender({
write: () => {
Expand All @@ -85,20 +88,24 @@ export class RdxTooltipArrowDirective {
this.renderer.setStyle(this.elementRef.nativeElement, 'fontSize', '0px');
}
});
this.onArrowSvgElementChangeEffect();
this.onContentPositionAndArrowDimensionsChangeEffect();
}

/** @ignore */
private setTriggerRect() {
this.triggerRect = this.tooltipRoot.tooltipTriggerDirective().elementRef.nativeElement.getBoundingClientRect();
private setAnchorOrTriggerRect() {
this.anchorOrTriggerRect = (
this.rootDirective.anchorDirective() ?? this.rootDirective.triggerDirective()
).elementRef.nativeElement.getBoundingClientRect();
}

/** @ignore */
private setPosition(position: ConnectionPositionPair, arrowDimensions: { width: number; height: number }) {
this.setTriggerRect();
private setPosition(position: ConnectedOverlayPositionChange, arrowDimensions: { width: number; height: number }) {
this.setAnchorOrTriggerRect();
const posParams = getArrowPositionParams(
getSideAndAlignFromAllPossibleConnectedPositions(position),
getSideAndAlignFromAllPossibleConnectedPositions(position.connectionPair),
{ width: arrowDimensions.width, height: arrowDimensions.height },
{ width: this.triggerRect.width, height: this.triggerRect.height }
{ width: this.anchorOrTriggerRect.width, height: this.anchorOrTriggerRect.height }
);

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

/** @ignore */
private readonly onArrowSvgElementChangeEffect = effect(() => {
const arrowElement = this.arrowSvgElement();
untracked(() => {
const currentArrowSvgElement = this.currentArrowSvgElement();
if (currentArrowSvgElement) {
this.renderer.removeChild(this.elementRef.nativeElement, currentArrowSvgElement);
}
this.currentArrowSvgElement.set(arrowElement);
this.renderer.setStyle(this.elementRef.nativeElement, 'width', `${this.width()}px`);
this.renderer.setStyle(this.elementRef.nativeElement, 'height', `${this.height()}px`);
this.renderer.appendChild(this.elementRef.nativeElement, this.currentArrowSvgElement());
private onArrowSvgElementChangeEffect() {
effect(() => {
const arrowElement = this.arrowSvgElement();
untracked(() => {
const currentArrowSvgElement = this.currentArrowSvgElement();
if (currentArrowSvgElement) {
this.renderer.removeChild(this.elementRef.nativeElement, currentArrowSvgElement);
}
this.currentArrowSvgElement.set(arrowElement);
this.renderer.setStyle(this.elementRef.nativeElement, 'width', `${this.width()}px`);
this.renderer.setStyle(this.elementRef.nativeElement, 'height', `${this.height()}px`);
this.renderer.appendChild(this.elementRef.nativeElement, this.currentArrowSvgElement());
});
});
});
}

/** @ignore */
private readonly onContentPositionAndArrowDimensionsChangeEffect = effect(() => {
const position = this.contentDirective.position();
const arrowDimensions = { width: this.width(), height: this.height() };
untracked(() => {
if (!position) {
return;
}
this.setPosition(position, arrowDimensions);
private onContentPositionAndArrowDimensionsChangeEffect() {
effect(() => {
const position = this.position();
const arrowDimensions = { width: this.width(), height: this.height() };
untracked(() => {
if (!position) {
return;
}
this.setPosition(position, arrowDimensions);
});
});
});
}
}
47 changes: 47 additions & 0 deletions packages/primitives/tooltip/src/tooltip-close.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Directive, effect, ElementRef, forwardRef, inject, Renderer2, untracked } from '@angular/core';
import { RdxTooltipCloseToken } from './tooltip-close.token';
import { injectTooltipRoot } from './tooltip-root.inject';

/**
* TODO: to be removed? But it seems to be useful when controlled from outside
*/
@Directive({
selector: '[rdxTooltipClose]',
host: {
type: 'button',
'(click)': 'rootDirective.handleClose(true)'
},
providers: [
{
provide: RdxTooltipCloseToken,
useExisting: forwardRef(() => RdxTooltipCloseDirective)
}
]
})
export class RdxTooltipCloseDirective {
/** @ignore */
protected readonly rootDirective = injectTooltipRoot();
/** @ignore */
readonly elementRef = inject(ElementRef);
/** @ignore */
private readonly renderer = inject(Renderer2);

constructor() {
this.onIsControlledExternallyEffect();
}

/** @ignore */
private onIsControlledExternallyEffect() {
effect(() => {
const isControlledExternally = this.rootDirective.controlledExternally()();

untracked(() => {
this.renderer.setStyle(
this.elementRef.nativeElement,
'display',
isControlledExternally ? null : 'none'
);
});
});
}
}
Loading
Loading