From 75d7e3ed0822c3c46909994975425243c0f7f6d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Twardziak?= Date: Sun, 29 Dec 2024 23:27:05 +0100 Subject: [PATCH 01/11] fix(popover): adjust popover stories description --- packages/primitives/popover/stories/popover.docs.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/primitives/popover/stories/popover.docs.mdx b/packages/primitives/popover/stories/popover.docs.mdx index a9242cab..4ae7d8e7 100644 --- a/packages/primitives/popover/stories/popover.docs.mdx +++ b/packages/primitives/popover/stories/popover.docs.mdx @@ -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 From 0c3a9fc6729493f24a97250bf14cd33ae6f4e079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Twardziak?= Date: Sun, 29 Dec 2024 23:31:18 +0100 Subject: [PATCH 02/11] fix(core): skip readonly mode when destructuring rdx_positions --- packages/primitives/core/src/positioning/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/primitives/core/src/positioning/utils.ts b/packages/primitives/core/src/positioning/utils.ts index 13a3552c..7a3d2762 100644 --- a/packages/primitives/core/src/positioning/utils.ts +++ b/packages/primitives/core/src/positioning/utils.ts @@ -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) { From b07d177064bb1d6026c979d3a02e38fbfc9f753c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Twardziak?= Date: Sun, 29 Dec 2024 23:50:24 +0100 Subject: [PATCH 03/11] chore(tooltip): copy popover to tooltip-v2 and rename files --- packages/primitives/tooltip-v2/README.md | 3 + packages/primitives/tooltip-v2/index.ts | 32 ++ .../primitives/tooltip-v2/ng-package.json | 5 + .../src/tooltip-anchor.directive.ts | 67 ++++ .../tooltip-v2/src/tooltip-anchor.token.ts | 4 + .../tooltip-v2/src/tooltip-arrow.directive.ts | 149 +++++++ .../tooltip-v2/src/tooltip-arrow.token.ts | 4 + .../tooltip-v2/src/tooltip-close.directive.ts | 44 +++ .../tooltip-v2/src/tooltip-close.token.ts | 4 + .../tooltip-content-attributes.component.ts | 65 ++++ .../src/tooltip-content-attributes.token.ts | 6 + .../src/tooltip-content.directive.ts | 356 +++++++++++++++++ .../tooltip-v2/src/tooltip-root.directive.ts | 367 ++++++++++++++++++ .../tooltip-v2/src/tooltip-root.inject.ts | 9 + .../src/tooltip-trigger.directive.ts | 33 ++ .../tooltip-v2/src/tooltip.types.ts | 16 + .../tooltip-v2/src/utils/cdk-event.service.ts | 203 ++++++++++ .../tooltip-v2/src/utils/constants.ts | 1 + .../primitives/tooltip-v2/src/utils/types.ts | 7 + .../stories/tooltip-anchor.component.ts | 165 ++++++++ .../stories/tooltip-animations.component.ts | 121 ++++++ .../stories/tooltip-default.component.ts | 91 +++++ .../stories/tooltip-events.components.ts | 98 +++++ .../tooltip-initially-open.component.ts | 92 +++++ .../stories/tooltip-multiple.component.ts | 222 +++++++++++ .../stories/tooltip-positioning.component.ts | 136 +++++++ .../stories/tooltip-triggering.component.ts | 224 +++++++++++ .../tooltip-v2/stories/tooltip.docs.mdx | 156 ++++++++ .../tooltip-v2/stories/tooltip.stories.ts | 114 ++++++ .../tooltip-v2/stories/utils/constants.ts | 2 + .../stories/utils/containers.registry.ts | 63 +++ .../stories/utils/option-panel-base.class.ts | 36 ++ .../stories/utils/styles.constants.ts | 352 +++++++++++++++++ .../tooltip-v2/stories/utils/types.ts | 20 + .../utils/with-option-panel.component.ts | 180 +++++++++ 35 files changed, 3447 insertions(+) create mode 100644 packages/primitives/tooltip-v2/README.md create mode 100644 packages/primitives/tooltip-v2/index.ts create mode 100644 packages/primitives/tooltip-v2/ng-package.json create mode 100644 packages/primitives/tooltip-v2/src/tooltip-anchor.directive.ts create mode 100644 packages/primitives/tooltip-v2/src/tooltip-anchor.token.ts create mode 100644 packages/primitives/tooltip-v2/src/tooltip-arrow.directive.ts create mode 100644 packages/primitives/tooltip-v2/src/tooltip-arrow.token.ts create mode 100644 packages/primitives/tooltip-v2/src/tooltip-close.directive.ts create mode 100644 packages/primitives/tooltip-v2/src/tooltip-close.token.ts create mode 100644 packages/primitives/tooltip-v2/src/tooltip-content-attributes.component.ts create mode 100644 packages/primitives/tooltip-v2/src/tooltip-content-attributes.token.ts create mode 100644 packages/primitives/tooltip-v2/src/tooltip-content.directive.ts create mode 100644 packages/primitives/tooltip-v2/src/tooltip-root.directive.ts create mode 100644 packages/primitives/tooltip-v2/src/tooltip-root.inject.ts create mode 100644 packages/primitives/tooltip-v2/src/tooltip-trigger.directive.ts create mode 100644 packages/primitives/tooltip-v2/src/tooltip.types.ts create mode 100644 packages/primitives/tooltip-v2/src/utils/cdk-event.service.ts create mode 100644 packages/primitives/tooltip-v2/src/utils/constants.ts create mode 100644 packages/primitives/tooltip-v2/src/utils/types.ts create mode 100644 packages/primitives/tooltip-v2/stories/tooltip-anchor.component.ts create mode 100644 packages/primitives/tooltip-v2/stories/tooltip-animations.component.ts create mode 100644 packages/primitives/tooltip-v2/stories/tooltip-default.component.ts create mode 100644 packages/primitives/tooltip-v2/stories/tooltip-events.components.ts create mode 100644 packages/primitives/tooltip-v2/stories/tooltip-initially-open.component.ts create mode 100644 packages/primitives/tooltip-v2/stories/tooltip-multiple.component.ts create mode 100644 packages/primitives/tooltip-v2/stories/tooltip-positioning.component.ts create mode 100644 packages/primitives/tooltip-v2/stories/tooltip-triggering.component.ts create mode 100644 packages/primitives/tooltip-v2/stories/tooltip.docs.mdx create mode 100644 packages/primitives/tooltip-v2/stories/tooltip.stories.ts create mode 100644 packages/primitives/tooltip-v2/stories/utils/constants.ts create mode 100644 packages/primitives/tooltip-v2/stories/utils/containers.registry.ts create mode 100644 packages/primitives/tooltip-v2/stories/utils/option-panel-base.class.ts create mode 100644 packages/primitives/tooltip-v2/stories/utils/styles.constants.ts create mode 100644 packages/primitives/tooltip-v2/stories/utils/types.ts create mode 100644 packages/primitives/tooltip-v2/stories/utils/with-option-panel.component.ts diff --git a/packages/primitives/tooltip-v2/README.md b/packages/primitives/tooltip-v2/README.md new file mode 100644 index 00000000..7e25f59f --- /dev/null +++ b/packages/primitives/tooltip-v2/README.md @@ -0,0 +1,3 @@ +# @radix-ng/primitives/tooltip-v2 + +Secondary entry point of `@radix-ng/primitives`. It can be used by importing from `@radix-ng/primitives/tooltip-v2`. diff --git a/packages/primitives/tooltip-v2/index.ts b/packages/primitives/tooltip-v2/index.ts new file mode 100644 index 00000000..6d88e4f2 --- /dev/null +++ b/packages/primitives/tooltip-v2/index.ts @@ -0,0 +1,32 @@ +import { NgModule } from '@angular/core'; +import { RdxPopoverAnchorDirective } from './src/tooltip-anchor.directive'; +import { RdxPopoverArrowDirective } from './src/tooltip-arrow.directive'; +import { RdxPopoverCloseDirective } from './src/tooltip-close.directive'; +import { RdxPopoverContentAttributesComponent } from './src/tooltip-content-attributes.component'; +import { RdxPopoverContentDirective } from './src/tooltip-content.directive'; +import { RdxPopoverRootDirective } from './src/tooltip-root.directive'; +import { RdxPopoverTriggerDirective } from './src/tooltip-trigger.directive'; + +export * from './src/tooltip-anchor.directive'; +export * from './src/tooltip-arrow.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'; + +const _imports = [ + RdxPopoverArrowDirective, + RdxPopoverCloseDirective, + RdxPopoverContentDirective, + RdxPopoverTriggerDirective, + RdxPopoverRootDirective, + RdxPopoverAnchorDirective, + RdxPopoverContentAttributesComponent +]; + +@NgModule({ + imports: [..._imports], + exports: [..._imports] +}) +export class RdxPopoverModule {} diff --git a/packages/primitives/tooltip-v2/ng-package.json b/packages/primitives/tooltip-v2/ng-package.json new file mode 100644 index 00000000..bebf62dc --- /dev/null +++ b/packages/primitives/tooltip-v2/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "index.ts" + } +} diff --git a/packages/primitives/tooltip-v2/src/tooltip-anchor.directive.ts b/packages/primitives/tooltip-v2/src/tooltip-anchor.directive.ts new file mode 100644 index 00000000..74dc7d18 --- /dev/null +++ b/packages/primitives/tooltip-v2/src/tooltip-anchor.directive.ts @@ -0,0 +1,67 @@ +import { CdkOverlayOrigin } from '@angular/cdk/overlay'; +import { computed, Directive, ElementRef, forwardRef, inject } from '@angular/core'; +import { injectDocument } from '@radix-ng/primitives/core'; +import { RdxPopoverAnchorToken } from './tooltip-anchor.token'; +import { RdxPopoverRootDirective } from './tooltip-root.directive'; +import { injectPopoverRoot } from './tooltip-root.inject'; + +@Directive({ + selector: '[rdxPopoverAnchor]', + exportAs: 'rdxPopoverAnchor', + hostDirectives: [CdkOverlayOrigin], + host: { + type: 'button', + '[attr.id]': 'name()', + '[attr.aria-haspopup]': '"dialog"', + '(click)': 'click()' + }, + providers: [ + { + provide: RdxPopoverAnchorToken, + useExisting: forwardRef(() => RdxPopoverAnchorDirective) + } + ] +}) +export class RdxPopoverAnchorDirective { + /** + * @ignore + * If outside the root then null, otherwise the root directive - with optional `true` passed in as the first param. + * If outside the root and non-null value that means the html structure is wrong - popover inside popover. + * */ + protected popoverRoot = injectPopoverRoot(true); + /** @ignore */ + readonly elementRef = inject(ElementRef); + /** @ignore */ + readonly overlayOrigin = inject(CdkOverlayOrigin); + /** @ignore */ + readonly document = injectDocument(); + + /** @ignore */ + readonly name = computed(() => `rdx-popover-external-anchor-${this.popoverRoot?.uniqueId()}`); + + /** @ignore */ + click(): void { + this.emitOutsideClick(); + } + + /** @ignore */ + setPopoverRoot(popoverRoot: RdxPopoverRootDirective) { + this.popoverRoot = popoverRoot; + } + + private emitOutsideClick() { + if ( + !this.popoverRoot?.isOpen() || + this.popoverRoot?.popoverContentDirective().onOverlayOutsideClickDisabled() + ) { + return; + } + const clickEvent = new MouseEvent('click', { + view: this.document.defaultView, + bubbles: true, + cancelable: true, + relatedTarget: this.elementRef.nativeElement + }); + this.popoverRoot?.popoverTriggerDirective().elementRef.nativeElement.dispatchEvent(clickEvent); + } +} diff --git a/packages/primitives/tooltip-v2/src/tooltip-anchor.token.ts b/packages/primitives/tooltip-v2/src/tooltip-anchor.token.ts new file mode 100644 index 00000000..49bd5d10 --- /dev/null +++ b/packages/primitives/tooltip-v2/src/tooltip-anchor.token.ts @@ -0,0 +1,4 @@ +import { InjectionToken } from '@angular/core'; +import { RdxPopoverAnchorDirective } from './tooltip-anchor.directive'; + +export const RdxPopoverAnchorToken = new InjectionToken('RdxPopoverAnchorToken'); diff --git a/packages/primitives/tooltip-v2/src/tooltip-arrow.directive.ts b/packages/primitives/tooltip-v2/src/tooltip-arrow.directive.ts new file mode 100644 index 00000000..d27d1eb0 --- /dev/null +++ b/packages/primitives/tooltip-v2/src/tooltip-arrow.directive.ts @@ -0,0 +1,149 @@ +import { NumberInput } from '@angular/cdk/coercion'; +import { ConnectedOverlayPositionChange } from '@angular/cdk/overlay'; +import { + afterNextRender, + computed, + Directive, + effect, + ElementRef, + forwardRef, + inject, + input, + numberAttribute, + Renderer2, + signal, + untracked +} from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { + getArrowPositionParams, + getSideAndAlignFromAllPossibleConnectedPositions, + RDX_POSITIONING_DEFAULTS +} from '@radix-ng/primitives/core'; +import { RdxPopoverArrowToken } from './tooltip-arrow.token'; +import { injectPopoverRoot } from './tooltip-root.inject'; + +@Directive({ + selector: '[rdxPopoverArrow]', + providers: [ + { + provide: RdxPopoverArrowToken, + useExisting: forwardRef(() => RdxPopoverArrowDirective) + } + ] +}) +export class RdxPopoverArrowDirective { + /** @ignore */ + private readonly renderer = inject(Renderer2); + /** @ignore */ + private readonly popoverRoot = injectPopoverRoot(); + /** @ignore */ + readonly elementRef = inject(ElementRef); + + /** + * @description The width of the arrow in pixels. + * @default 10 + */ + readonly width = input(RDX_POSITIONING_DEFAULTS.arrow.width, { transform: numberAttribute }); + + /** + * @description The height of the arrow in pixels. + * @default 5 + */ + readonly height = input(RDX_POSITIONING_DEFAULTS.arrow.height, { transform: numberAttribute }); + + /** @ignore */ + readonly arrowSvgElement = computed(() => { + const width = this.width(); + const height = this.height(); + + const svgElement = this.renderer.createElement('svg', 'svg'); + this.renderer.setAttribute(svgElement, 'viewBox', '0 0 30 10'); + this.renderer.setAttribute(svgElement, 'width', String(width)); + this.renderer.setAttribute(svgElement, 'height', String(height)); + const polygonElement = this.renderer.createElement('polygon', 'svg'); + this.renderer.setAttribute(polygonElement, 'points', '0,0 30,0 15,10'); + this.renderer.setAttribute(svgElement, 'preserveAspectRatio', 'none'); + this.renderer.appendChild(svgElement, polygonElement); + + return svgElement; + }); + + /** @ignore */ + private readonly currentArrowSvgElement = signal(void 0); + /** @ignore */ + private readonly position = toSignal(this.popoverRoot.popoverContentDirective().positionChange()); + + /** @ignore */ + private anchorOrTriggerRect: DOMRect; + + constructor() { + afterNextRender({ + write: () => { + if (this.elementRef.nativeElement.parentElement) { + this.renderer.setStyle(this.elementRef.nativeElement.parentElement, 'position', 'relative'); + } + this.renderer.setStyle(this.elementRef.nativeElement, 'position', 'absolute'); + this.renderer.setStyle(this.elementRef.nativeElement, 'boxSizing', ''); + this.renderer.setStyle(this.elementRef.nativeElement, 'fontSize', '0px'); + } + }); + this.onArrowSvgElementChangeEffect(); + this.onContentPositionAndArrowDimensionsChangeEffect(); + } + + /** @ignore */ + private setAnchorOrTriggerRect() { + this.anchorOrTriggerRect = ( + this.popoverRoot.popoverAnchorDirective() ?? this.popoverRoot.popoverTriggerDirective() + ).elementRef.nativeElement.getBoundingClientRect(); + } + + /** @ignore */ + private setPosition(position: ConnectedOverlayPositionChange, arrowDimensions: { width: number; height: number }) { + this.setAnchorOrTriggerRect(); + const posParams = getArrowPositionParams( + getSideAndAlignFromAllPossibleConnectedPositions(position.connectionPair), + { width: arrowDimensions.width, height: arrowDimensions.height }, + { width: this.anchorOrTriggerRect.width, height: this.anchorOrTriggerRect.height } + ); + + this.renderer.setStyle(this.elementRef.nativeElement, 'top', posParams.top); + this.renderer.setStyle(this.elementRef.nativeElement, 'bottom', ''); + this.renderer.setStyle(this.elementRef.nativeElement, 'left', posParams.left); + this.renderer.setStyle(this.elementRef.nativeElement, 'right', ''); + this.renderer.setStyle(this.elementRef.nativeElement, 'transform', posParams.transform); + this.renderer.setStyle(this.elementRef.nativeElement, 'transformOrigin', posParams.transformOrigin); + } + + /** @ignore */ + 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 onContentPositionAndArrowDimensionsChangeEffect() { + effect(() => { + const position = this.position(); + const arrowDimensions = { width: this.width(), height: this.height() }; + untracked(() => { + if (!position) { + return; + } + this.setPosition(position, arrowDimensions); + }); + }); + } +} diff --git a/packages/primitives/tooltip-v2/src/tooltip-arrow.token.ts b/packages/primitives/tooltip-v2/src/tooltip-arrow.token.ts new file mode 100644 index 00000000..a99039bc --- /dev/null +++ b/packages/primitives/tooltip-v2/src/tooltip-arrow.token.ts @@ -0,0 +1,4 @@ +import { InjectionToken } from '@angular/core'; +import { RdxPopoverArrowDirective } from './tooltip-arrow.directive'; + +export const RdxPopoverArrowToken = new InjectionToken('RdxPopoverArrowToken'); diff --git a/packages/primitives/tooltip-v2/src/tooltip-close.directive.ts b/packages/primitives/tooltip-v2/src/tooltip-close.directive.ts new file mode 100644 index 00000000..8d53dc2c --- /dev/null +++ b/packages/primitives/tooltip-v2/src/tooltip-close.directive.ts @@ -0,0 +1,44 @@ +import { Directive, effect, ElementRef, forwardRef, inject, Renderer2, untracked } from '@angular/core'; +import { RdxPopoverCloseToken } from './tooltip-close.token'; +import { injectPopoverRoot } from './tooltip-root.inject'; + +@Directive({ + selector: '[rdxPopoverClose]', + host: { + type: 'button', + '(click)': 'popoverRoot.handleClose()' + }, + providers: [ + { + provide: RdxPopoverCloseToken, + useExisting: forwardRef(() => RdxPopoverCloseDirective) + } + ] +}) +export class RdxPopoverCloseDirective { + /** @ignore */ + protected readonly popoverRoot = injectPopoverRoot(); + /** @ignore */ + readonly elementRef = inject(ElementRef); + /** @ignore */ + private readonly renderer = inject(Renderer2); + + constructor() { + this.onIsControlledExternallyEffect(); + } + + /** @ignore */ + private onIsControlledExternallyEffect() { + effect(() => { + const isControlledExternally = this.popoverRoot.controlledExternally()(); + + untracked(() => { + this.renderer.setStyle( + this.elementRef.nativeElement, + 'display', + isControlledExternally ? 'none' : null + ); + }); + }); + } +} diff --git a/packages/primitives/tooltip-v2/src/tooltip-close.token.ts b/packages/primitives/tooltip-v2/src/tooltip-close.token.ts new file mode 100644 index 00000000..5cc244ea --- /dev/null +++ b/packages/primitives/tooltip-v2/src/tooltip-close.token.ts @@ -0,0 +1,4 @@ +import { InjectionToken } from '@angular/core'; +import { RdxPopoverCloseDirective } from './tooltip-close.directive'; + +export const RdxPopoverCloseToken = new InjectionToken('RdxPopoverCloseToken'); diff --git a/packages/primitives/tooltip-v2/src/tooltip-content-attributes.component.ts b/packages/primitives/tooltip-v2/src/tooltip-content-attributes.component.ts new file mode 100644 index 00000000..12fc4943 --- /dev/null +++ b/packages/primitives/tooltip-v2/src/tooltip-content-attributes.component.ts @@ -0,0 +1,65 @@ +import { ChangeDetectionStrategy, Component, computed, forwardRef } from '@angular/core'; +import { RdxPopoverContentAttributesToken } from './tooltip-content-attributes.token'; +import { injectPopoverRoot } from './tooltip-root.inject'; +import { RdxPopoverAnimationStatus, RdxPopoverState } from './tooltip.types'; + +@Component({ + selector: '[rdxPopoverContentAttributes]', + template: ` + + `, + host: { + '[attr.role]': '"dialog"', + '[attr.id]': 'name()', + '[attr.data-state]': 'popoverRoot.state()', + '[attr.data-side]': 'popoverRoot.popoverContentDirective().side()', + '[attr.data-align]': 'popoverRoot.popoverContentDirective().align()', + '[style]': 'disableAnimation() ? {animation: "none !important"} : null', + '(animationstart)': 'onAnimationStart($event)', + '(animationend)': 'onAnimationEnd($event)' + }, + providers: [ + { + provide: RdxPopoverContentAttributesToken, + useExisting: forwardRef(() => RdxPopoverContentAttributesComponent) + } + ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class RdxPopoverContentAttributesComponent { + /** @ignore */ + protected readonly popoverRoot = injectPopoverRoot(); + + /** @ignore */ + readonly name = computed(() => `rdx-popover-content-attributes-${this.popoverRoot.uniqueId()}`); + + /** @ignore */ + readonly disableAnimation = computed(() => !this.canAnimate()); + + /** @ignore */ + protected onAnimationStart(_: AnimationEvent) { + this.popoverRoot.cssAnimationStatus.set( + this.popoverRoot.state() === RdxPopoverState.OPEN + ? RdxPopoverAnimationStatus.OPEN_STARTED + : RdxPopoverAnimationStatus.CLOSED_STARTED + ); + } + + /** @ignore */ + protected onAnimationEnd(_: AnimationEvent) { + this.popoverRoot.cssAnimationStatus.set( + this.popoverRoot.state() === RdxPopoverState.OPEN + ? RdxPopoverAnimationStatus.OPEN_ENDED + : RdxPopoverAnimationStatus.CLOSED_ENDED + ); + } + + /** @ignore */ + private canAnimate() { + return ( + this.popoverRoot.cssAnimation() && + ((this.popoverRoot.cssOpeningAnimation() && this.popoverRoot.state() === RdxPopoverState.OPEN) || + (this.popoverRoot.cssClosingAnimation() && this.popoverRoot.state() === RdxPopoverState.CLOSED)) + ); + } +} diff --git a/packages/primitives/tooltip-v2/src/tooltip-content-attributes.token.ts b/packages/primitives/tooltip-v2/src/tooltip-content-attributes.token.ts new file mode 100644 index 00000000..8e4ae123 --- /dev/null +++ b/packages/primitives/tooltip-v2/src/tooltip-content-attributes.token.ts @@ -0,0 +1,6 @@ +import { InjectionToken } from '@angular/core'; +import { RdxPopoverContentAttributesComponent } from './tooltip-content-attributes.component'; + +export const RdxPopoverContentAttributesToken = new InjectionToken( + 'RdxPopoverContentAttributesToken' +); diff --git a/packages/primitives/tooltip-v2/src/tooltip-content.directive.ts b/packages/primitives/tooltip-v2/src/tooltip-content.directive.ts new file mode 100644 index 00000000..72fc76d2 --- /dev/null +++ b/packages/primitives/tooltip-v2/src/tooltip-content.directive.ts @@ -0,0 +1,356 @@ +import { BooleanInput, NumberInput } from '@angular/cdk/coercion'; +import { CdkConnectedOverlay, Overlay } from '@angular/cdk/overlay'; +import { + booleanAttribute, + computed, + DestroyRef, + Directive, + effect, + inject, + input, + numberAttribute, + OnInit, + output, + SimpleChange, + TemplateRef, + untracked +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + getAllPossibleConnectedPositions, + getContentPosition, + RDX_POSITIONING_DEFAULTS, + RdxPositionAlign, + RdxPositionSide, + RdxPositionSideAndAlignOffsets +} from '@radix-ng/primitives/core'; +import { filter, tap } from 'rxjs'; +import { injectPopoverRoot } from './tooltip-root.inject'; +import { RdxPopoverAttachDetachEvent } from './tooltip.types'; + +@Directive({ + selector: '[rdxPopoverContent]', + hostDirectives: [ + CdkConnectedOverlay + ] +}) +export class RdxPopoverContentDirective implements OnInit { + /** @ignore */ + private readonly popoverRoot = injectPopoverRoot(); + /** @ignore */ + private readonly templateRef = inject(TemplateRef); + /** @ignore */ + private readonly overlay = inject(Overlay); + /** @ignore */ + private readonly destroyRef = inject(DestroyRef); + /** @ignore */ + private readonly connectedOverlay = inject(CdkConnectedOverlay); + + /** @ignore */ + readonly name = computed(() => `rdx-popover-trigger-${this.popoverRoot.uniqueId()}`); + + /** + * @description The preferred side of the trigger to render against when open. Will be reversed when collisions occur and avoidCollisions is enabled. + * @default top + */ + readonly side = input(RdxPositionSide.Top); + /** + * @description The distance in pixels from the trigger. + * @default undefined + */ + readonly sideOffset = input(NaN, { + transform: numberAttribute + }); + /** + * @description The preferred alignment against the trigger. May change when collisions occur. + * @default center + */ + readonly align = input(RdxPositionAlign.Center); + /** + * @description An offset in pixels from the "start" or "end" alignment options. + * @default undefined + */ + readonly alignOffset = input(NaN, { + transform: numberAttribute + }); + + /** + * @description Whether to add some alternate positions of the content. + * @default false + */ + readonly alternatePositionsDisabled = input(false, { transform: booleanAttribute }); + + /** @description Whether to prevent `onOverlayEscapeKeyDown` handler from calling. */ + readonly onOverlayEscapeKeyDownDisabled = input(false, { transform: booleanAttribute }); + /** @description Whether to prevent `onOverlayOutsideClick` handler from calling. */ + readonly onOverlayOutsideClickDisabled = input(false, { transform: booleanAttribute }); + + /** + * @description Event handler called when the escape key is down. + * It can be prevented by setting `onOverlayEscapeKeyDownDisabled` input to `true`. + */ + readonly onOverlayEscapeKeyDown = output(); + /** + * @description Event handler called when a pointer event occurs outside the bounds of the component. + * It can be prevented by setting `onOverlayOutsideClickDisabled` input to `true`. + */ + readonly onOverlayOutsideClick = output(); + + /** + * @description Event handler called after the overlay is open + */ + readonly onOpen = output(); + /** + * @description Event handler called after the overlay is closed + */ + readonly onClosed = output(); + + /** @ingore */ + readonly positions = computed(() => this.computePositions()); + + constructor() { + this.onOriginChangeEffect(); + this.onPositionChangeEffect(); + } + + /** @ignore */ + ngOnInit() { + this.setScrollStrategy(); + this.setHasBackdrop(); + this.setDisableClose(); + this.onAttach(); + this.onDetach(); + this.connectKeydownEscape(); + this.connectOutsideClick(); + } + + /** @ignore */ + open() { + if (this.connectedOverlay.open) { + return; + } + const prevOpen = this.connectedOverlay.open; + this.connectedOverlay.open = true; + this.fireOverlayNgOnChanges('open', this.connectedOverlay.open, prevOpen); + } + + /** @ignore */ + close() { + if (!this.connectedOverlay.open) { + return; + } + const prevOpen = this.connectedOverlay.open; + this.connectedOverlay.open = false; + this.fireOverlayNgOnChanges('open', this.connectedOverlay.open, prevOpen); + } + + /** @ignore */ + positionChange() { + return this.connectedOverlay.positionChange.asObservable(); + } + + /** @ignore */ + private connectKeydownEscape() { + this.connectedOverlay.overlayKeydown + .asObservable() + .pipe( + filter( + () => + !this.onOverlayEscapeKeyDownDisabled() && + !this.popoverRoot.rdxCdkEventService?.primitivePreventedFromCdkEvent( + this.popoverRoot, + 'cdkOverlayEscapeKeyDown' + ) + ), + filter((event) => event.key === 'Escape'), + tap((event) => { + this.onOverlayEscapeKeyDown.emit(event); + }), + filter(() => !this.popoverRoot.firstDefaultOpen()), + tap(() => { + this.popoverRoot.handleClose(); + }), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(); + } + + /** @ignore */ + private connectOutsideClick() { + this.connectedOverlay.overlayOutsideClick + .asObservable() + .pipe( + filter( + () => + !this.onOverlayOutsideClickDisabled() && + !this.popoverRoot.rdxCdkEventService?.primitivePreventedFromCdkEvent( + this.popoverRoot, + 'cdkOverlayOutsideClick' + ) + ), + /** + * Handle the situation when an anchor is added and the anchor becomes the origin of the overlay + * hence the trigger will be considered the outside element + */ + filter((event) => { + return ( + !this.popoverRoot.popoverAnchorDirective() || + !this.popoverRoot + .popoverTriggerDirective() + .elementRef.nativeElement.contains(event.target as Element) + ); + }), + tap((event) => { + this.onOverlayOutsideClick.emit(event); + }), + filter(() => !this.popoverRoot.firstDefaultOpen()), + tap(() => { + this.popoverRoot.handleClose(); + }), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(); + } + + /** @ignore */ + private onAttach() { + this.connectedOverlay.attach + .asObservable() + .pipe( + tap(() => { + /** + * `this.onOpen.emit();` is being delegated to the root directive due to the opening animation + */ + this.popoverRoot.attachDetachEvent.set(RdxPopoverAttachDetachEvent.ATTACH); + }), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(); + } + + /** @ignore */ + private onDetach() { + this.connectedOverlay.detach + .asObservable() + .pipe( + tap(() => { + /** + * `this.onClosed.emit();` is being delegated to the root directive due to the closing animation + */ + this.popoverRoot.attachDetachEvent.set(RdxPopoverAttachDetachEvent.DETACH); + }), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(); + } + + /** @ignore */ + private setScrollStrategy() { + const prevScrollStrategy = this.connectedOverlay.scrollStrategy; + this.connectedOverlay.scrollStrategy = this.overlay.scrollStrategies.reposition(); + this.fireOverlayNgOnChanges('scrollStrategy', this.connectedOverlay.scrollStrategy, prevScrollStrategy); + } + + /** @ignore */ + private setHasBackdrop() { + const prevHasBackdrop = this.connectedOverlay.hasBackdrop; + this.connectedOverlay.hasBackdrop = false; + this.fireOverlayNgOnChanges('hasBackdrop', this.connectedOverlay.hasBackdrop, prevHasBackdrop); + } + + /** @ignore */ + private setDisableClose() { + const prevDisableClose = this.connectedOverlay.disableClose; + this.connectedOverlay.disableClose = true; + this.fireOverlayNgOnChanges('disableClose', this.connectedOverlay.disableClose, prevDisableClose); + } + + /** @ignore */ + private setOrigin(origin: CdkConnectedOverlay['origin']) { + const prevOrigin = this.connectedOverlay.origin; + this.connectedOverlay.origin = origin; + this.fireOverlayNgOnChanges('origin', this.connectedOverlay.origin, prevOrigin); + } + + /** @ignore */ + private setPositions(positions: CdkConnectedOverlay['positions']) { + const prevPositions = this.connectedOverlay.positions; + this.connectedOverlay.positions = positions; + this.fireOverlayNgOnChanges('positions', this.connectedOverlay.positions, prevPositions); + this.connectedOverlay.overlayRef?.updatePosition(); + } + + /** @ignore */ + private computePositions() { + const arrowHeight = this.popoverRoot.popoverArrowDirective()?.height() ?? 0; + const offsets: RdxPositionSideAndAlignOffsets = { + sideOffset: isNaN(this.sideOffset()) + ? arrowHeight || RDX_POSITIONING_DEFAULTS.offsets.side + : this.sideOffset(), + alignOffset: isNaN(this.alignOffset()) ? RDX_POSITIONING_DEFAULTS.offsets.align : this.alignOffset() + }; + const basePosition = getContentPosition({ + side: this.side(), + align: this.align(), + sideOffset: offsets.sideOffset, + alignOffset: offsets.alignOffset + }); + const positions = [basePosition]; + if (!this.alternatePositionsDisabled()) { + /** + * Alternate positions for better user experience along the X/Y axis (e.g. vertical/horizontal scrolling) + */ + const allPossibleConnectedPositions = getAllPossibleConnectedPositions(); + allPossibleConnectedPositions.forEach((_, key) => { + const sideAndAlignArray = key.split('|'); + if ( + (sideAndAlignArray[0] as RdxPositionSide) !== this.side() || + (sideAndAlignArray[1] as RdxPositionAlign) !== this.align() + ) { + positions.push( + getContentPosition({ + side: sideAndAlignArray[0] as RdxPositionSide, + align: sideAndAlignArray[1] as RdxPositionAlign, + sideOffset: offsets.sideOffset, + alignOffset: offsets.alignOffset + }) + ); + } + }); + } + return positions; + } + + private onOriginChangeEffect() { + effect(() => { + const origin = (this.popoverRoot.popoverAnchorDirective() ?? this.popoverRoot.popoverTriggerDirective()) + .overlayOrigin; + untracked(() => { + this.setOrigin(origin); + }); + }); + } + + /** @ignore */ + private onPositionChangeEffect() { + effect(() => { + const positions = this.positions(); + this.alternatePositionsDisabled(); + untracked(() => { + this.setPositions(positions); + }); + }); + } + + /** @ignore */ + private fireOverlayNgOnChanges( + input: K, + currentValue: V, + previousValue: V, + firstChange = false + ) { + this.connectedOverlay.ngOnChanges({ + [input]: new SimpleChange(previousValue, currentValue, firstChange) + }); + } +} diff --git a/packages/primitives/tooltip-v2/src/tooltip-root.directive.ts b/packages/primitives/tooltip-v2/src/tooltip-root.directive.ts new file mode 100644 index 00000000..6766bd01 --- /dev/null +++ b/packages/primitives/tooltip-v2/src/tooltip-root.directive.ts @@ -0,0 +1,367 @@ +import { BooleanInput } from '@angular/cdk/coercion'; +import { + afterNextRender, + booleanAttribute, + computed, + contentChild, + DestroyRef, + Directive, + effect, + inject, + input, + signal, + untracked, + ViewContainerRef +} from '@angular/core'; +import { RdxPopoverAnchorDirective } from './tooltip-anchor.directive'; +import { RdxPopoverAnchorToken } from './tooltip-anchor.token'; +import { RdxPopoverArrowToken } from './tooltip-arrow.token'; +import { RdxPopoverCloseToken } from './tooltip-close.token'; +import { RdxPopoverContentAttributesToken } from './tooltip-content-attributes.token'; +import { RdxPopoverContentDirective } from './tooltip-content.directive'; +import { RdxPopoverTriggerDirective } from './tooltip-trigger.directive'; +import { RdxPopoverAnimationStatus, RdxPopoverAttachDetachEvent, RdxPopoverState } from './tooltip.types'; +import { injectRdxCdkEventService } from './utils/cdk-event.service'; + +let nextId = 0; + +@Directive({ + selector: '[rdxPopoverRoot]', + exportAs: 'rdxPopoverRoot' +}) +export class RdxPopoverRootDirective { + /** @ignore */ + readonly uniqueId = signal(++nextId); + /** @ignore */ + readonly name = computed(() => `rdx-popover-root-${this.uniqueId()}`); + + /** + * @description The anchor directive that comes form outside the popover root + * @default undefined + */ + readonly anchor = input(void 0); + /** + * @description The open state of the popover when it is initially rendered. Use when you do not need to control its open state. + * @default false + */ + readonly defaultOpen = input(false, { transform: booleanAttribute }); + /** + * @description The controlled state of the popover. `open` input take precedence of `defaultOpen` input. + * @default undefined + */ + readonly open = input(void 0, { transform: booleanAttribute }); + /** + * @description Whether to control the state of the popover from external. Use in conjunction with `open` input. + * @default undefined + */ + readonly externalControl = input(void 0, { transform: booleanAttribute }); + /** + * @description Whether to take into account CSS opening/closing animations. + * @default false + */ + readonly cssAnimation = input(false, { transform: booleanAttribute }); + /** + * @description Whether to take into account CSS opening animations. `cssAnimation` input must be set to 'true' + * @default false + */ + readonly cssOpeningAnimation = input(false, { transform: booleanAttribute }); + /** + * @description Whether to take into account CSS closing animations. `cssAnimation` input must be set to 'true' + * @default false + */ + readonly cssClosingAnimation = input(false, { transform: booleanAttribute }); + + /** @ignore */ + readonly cssAnimationStatus = signal(null); + + /** @ignore */ + readonly popoverContentDirective = contentChild.required(RdxPopoverContentDirective); + /** @ignore */ + readonly popoverTriggerDirective = contentChild.required(RdxPopoverTriggerDirective); + /** @ignore */ + readonly popoverArrowDirective = contentChild(RdxPopoverArrowToken); + /** @ignore */ + readonly popoverCloseDirective = contentChild(RdxPopoverCloseToken); + /** @ignore */ + readonly popoverContentAttributesComponent = contentChild(RdxPopoverContentAttributesToken); + /** @ignore */ + private readonly internalPopoverAnchorDirective = contentChild(RdxPopoverAnchorToken); + + /** @ignore */ + readonly viewContainerRef = inject(ViewContainerRef); + /** @ignore */ + readonly rdxCdkEventService = injectRdxCdkEventService(); + /** @ignore */ + readonly destroyRef = inject(DestroyRef); + + /** @ignore */ + readonly state = signal(RdxPopoverState.CLOSED); + + /** @ignore */ + readonly attachDetachEvent = signal(RdxPopoverAttachDetachEvent.DETACH); + + /** @ignore */ + private readonly isFirstDefaultOpen = signal(false); + + /** @ignore */ + readonly popoverAnchorDirective = computed(() => this.internalPopoverAnchorDirective() ?? this.anchor()); + + constructor() { + this.rdxCdkEventService?.registerPrimitive(this); + this.destroyRef.onDestroy(() => this.rdxCdkEventService?.deregisterPrimitive(this)); + this.onStateChangeEffect(); + this.onCssAnimationStatusChangeChangeEffect(); + this.onOpenChangeEffect(); + this.onIsFirstDefaultOpenChangeEffect(); + this.onAnchorChangeEffect(); + this.emitOpenOrClosedEventEffect(); + afterNextRender({ + write: () => { + if (this.defaultOpen() && !this.open()) { + this.isFirstDefaultOpen.set(true); + } + } + }); + } + + /** @ignore */ + getAnimationParamsSnapshot() { + return { + cssAnimation: this.cssAnimation(), + cssOpeningAnimation: this.cssOpeningAnimation(), + cssClosingAnimation: this.cssClosingAnimation(), + cssAnimationStatus: this.cssAnimationStatus(), + attachDetachEvent: this.attachDetachEvent(), + state: this.state(), + canEmitOnOpenOrOnClosed: this.canEmitOnOpenOrOnClosed() + }; + } + + /** @ignore */ + controlledExternally() { + return this.externalControl; + } + + /** @ignore */ + firstDefaultOpen() { + return this.isFirstDefaultOpen(); + } + + /** @ignore */ + handleOpen(): void { + if (this.externalControl()) { + return; + } + this.setState(RdxPopoverState.OPEN); + } + + /** @ignore */ + handleClose(): void { + if (this.isFirstDefaultOpen()) { + this.isFirstDefaultOpen.set(false); + } + if (this.externalControl()) { + return; + } + this.setState(RdxPopoverState.CLOSED); + } + + /** @ignore */ + handleToggle(): void { + if (this.externalControl()) { + return; + } + this.isOpen() ? this.handleClose() : this.handleOpen(); + } + + /** @ignore */ + isOpen(state?: RdxPopoverState) { + return (state ?? this.state()) === RdxPopoverState.OPEN; + } + + /** @ignore */ + private setState(state = RdxPopoverState.CLOSED): void { + if (state === this.state()) { + return; + } + this.state.set(state); + } + + /** @ignore */ + private openContent(): void { + this.popoverContentDirective().open(); + if (!this.cssAnimation() || !this.cssOpeningAnimation()) { + this.cssAnimationStatus.set(null); + } + } + + /** @ignore */ + private closeContent(): void { + this.popoverContentDirective().close(); + if (!this.cssAnimation() || !this.cssClosingAnimation()) { + this.cssAnimationStatus.set(null); + } + } + + /** @ignore */ + private emitOnOpen(): void { + this.popoverContentDirective().onOpen.emit(); + } + + /** @ignore */ + private emitOnClosed(): void { + this.popoverContentDirective().onClosed.emit(); + } + + /** @ignore */ + private ifOpenOrCloseWithoutAnimations(state: RdxPopoverState) { + return ( + !this.popoverContentAttributesComponent() || + !this.cssAnimation() || + (this.cssAnimation() && !this.cssClosingAnimation() && state === RdxPopoverState.CLOSED) || + (this.cssAnimation() && !this.cssOpeningAnimation() && state === RdxPopoverState.OPEN) || + // !this.cssAnimationStatus() || + (this.cssOpeningAnimation() && + state === RdxPopoverState.OPEN && + [RdxPopoverAnimationStatus.OPEN_STARTED].includes(this.cssAnimationStatus()!)) || + (this.cssClosingAnimation() && + state === RdxPopoverState.CLOSED && + [RdxPopoverAnimationStatus.CLOSED_STARTED].includes(this.cssAnimationStatus()!)) + ); + } + + /** @ignore */ + private ifOpenOrCloseWithAnimations(cssAnimationStatus: RdxPopoverAnimationStatus | null) { + return ( + this.popoverContentAttributesComponent() && + this.cssAnimation() && + cssAnimationStatus && + ((this.cssOpeningAnimation() && + this.state() === RdxPopoverState.OPEN && + [RdxPopoverAnimationStatus.OPEN_ENDED].includes(cssAnimationStatus)) || + (this.cssClosingAnimation() && + this.state() === RdxPopoverState.CLOSED && + [RdxPopoverAnimationStatus.CLOSED_ENDED].includes(cssAnimationStatus))) + ); + } + + /** @ignore */ + private openOrClose(state: RdxPopoverState) { + const isOpen = this.isOpen(state); + isOpen ? this.openContent() : this.closeContent(); + } + + /** @ignore */ + private emitOnOpenOrOnClosed(state: RdxPopoverState) { + this.isOpen(state) + ? this.attachDetachEvent() === RdxPopoverAttachDetachEvent.ATTACH && this.emitOnOpen() + : this.attachDetachEvent() === RdxPopoverAttachDetachEvent.DETACH && this.emitOnClosed(); + } + + /** @ignore */ + private canEmitOnOpenOrOnClosed() { + return ( + !this.cssAnimation() || + (!this.cssOpeningAnimation() && this.state() === RdxPopoverState.OPEN) || + (this.cssOpeningAnimation() && + this.state() === RdxPopoverState.OPEN && + this.cssAnimationStatus() === RdxPopoverAnimationStatus.OPEN_ENDED) || + (!this.cssClosingAnimation() && this.state() === RdxPopoverState.CLOSED) || + (this.cssClosingAnimation() && + this.state() === RdxPopoverState.CLOSED && + this.cssAnimationStatus() === RdxPopoverAnimationStatus.CLOSED_ENDED) + ); + } + + /** @ignore */ + private onStateChangeEffect() { + let isFirst = true; + effect(() => { + const state = this.state(); + untracked(() => { + if (isFirst) { + isFirst = false; + return; + } + if (!this.ifOpenOrCloseWithoutAnimations(state)) { + return; + } + this.openOrClose(state); + }); + }, {}); + } + + /** @ignore */ + private onCssAnimationStatusChangeChangeEffect() { + let isFirst = true; + effect(() => { + const cssAnimationStatus = this.cssAnimationStatus(); + untracked(() => { + if (isFirst) { + isFirst = false; + return; + } + if (!this.ifOpenOrCloseWithAnimations(cssAnimationStatus)) { + return; + } + this.openOrClose(this.state()); + }); + }); + } + + /** @ignore */ + private emitOpenOrClosedEventEffect() { + let isFirst = true; + effect(() => { + this.attachDetachEvent(); + this.cssAnimationStatus(); + untracked(() => { + if (isFirst) { + isFirst = false; + return; + } + const canEmitOpenClose = untracked(() => this.canEmitOnOpenOrOnClosed()); + if (!canEmitOpenClose) { + return; + } + this.emitOnOpenOrOnClosed(this.state()); + }); + }); + } + + /** @ignore */ + private onOpenChangeEffect() { + effect(() => { + const open = this.open(); + untracked(() => { + this.setState(open ? RdxPopoverState.OPEN : RdxPopoverState.CLOSED); + }); + }); + } + + /** @ignore */ + private onIsFirstDefaultOpenChangeEffect() { + const effectRef = effect(() => { + const defaultOpen = this.defaultOpen(); + untracked(() => { + if (!defaultOpen || this.open()) { + effectRef.destroy(); + return; + } + this.handleOpen(); + }); + }); + } + + /** @ignore */ + private onAnchorChangeEffect = () => { + effect(() => { + const anchor = this.anchor(); + untracked(() => { + if (anchor) { + anchor.setPopoverRoot(this); + } + }); + }); + }; +} diff --git a/packages/primitives/tooltip-v2/src/tooltip-root.inject.ts b/packages/primitives/tooltip-v2/src/tooltip-root.inject.ts new file mode 100644 index 00000000..79291478 --- /dev/null +++ b/packages/primitives/tooltip-v2/src/tooltip-root.inject.ts @@ -0,0 +1,9 @@ +import { assertInInjectionContext, inject, isDevMode } from '@angular/core'; +import { RdxPopoverRootDirective } from './tooltip-root.directive'; + +export function injectPopoverRoot(optional?: false): RdxPopoverRootDirective; +export function injectPopoverRoot(optional: true): RdxPopoverRootDirective | null; +export function injectPopoverRoot(optional = false): RdxPopoverRootDirective | null { + isDevMode() && assertInInjectionContext(injectPopoverRoot); + return inject(RdxPopoverRootDirective, { optional }); +} diff --git a/packages/primitives/tooltip-v2/src/tooltip-trigger.directive.ts b/packages/primitives/tooltip-v2/src/tooltip-trigger.directive.ts new file mode 100644 index 00000000..e8fa639f --- /dev/null +++ b/packages/primitives/tooltip-v2/src/tooltip-trigger.directive.ts @@ -0,0 +1,33 @@ +import { CdkOverlayOrigin } from '@angular/cdk/overlay'; +import { computed, Directive, ElementRef, inject } from '@angular/core'; +import { injectPopoverRoot } from './tooltip-root.inject'; + +@Directive({ + selector: '[rdxPopoverTrigger]', + hostDirectives: [CdkOverlayOrigin], + host: { + type: 'button', + '[attr.id]': 'name()', + '[attr.aria-haspopup]': '"dialog"', + '[attr.aria-expanded]': 'popoverRoot.isOpen()', + '[attr.aria-controls]': 'popoverRoot.popoverContentDirective().name()', + '[attr.data-state]': 'popoverRoot.state()', + '(click)': 'click()' + } +}) +export class RdxPopoverTriggerDirective { + /** @ignore */ + protected readonly popoverRoot = injectPopoverRoot(); + /** @ignore */ + readonly elementRef = inject>(ElementRef); + /** @ignore */ + readonly overlayOrigin = inject(CdkOverlayOrigin); + + /** @ignore */ + readonly name = computed(() => `rdx-popover-trigger-${this.popoverRoot.uniqueId()}`); + + /** @ignore */ + click(): void { + this.popoverRoot.handleToggle(); + } +} diff --git a/packages/primitives/tooltip-v2/src/tooltip.types.ts b/packages/primitives/tooltip-v2/src/tooltip.types.ts new file mode 100644 index 00000000..ccf53f28 --- /dev/null +++ b/packages/primitives/tooltip-v2/src/tooltip.types.ts @@ -0,0 +1,16 @@ +export enum RdxPopoverState { + OPEN = 'open', + CLOSED = 'closed' +} + +export enum RdxPopoverAttachDetachEvent { + ATTACH = 'attach', + DETACH = 'detach' +} + +export enum RdxPopoverAnimationStatus { + OPEN_STARTED = 'open_started', + OPEN_ENDED = 'open_ended', + CLOSED_STARTED = 'closed_started', + CLOSED_ENDED = 'closed_ended' +} diff --git a/packages/primitives/tooltip-v2/src/utils/cdk-event.service.ts b/packages/primitives/tooltip-v2/src/utils/cdk-event.service.ts new file mode 100644 index 00000000..acf756b6 --- /dev/null +++ b/packages/primitives/tooltip-v2/src/utils/cdk-event.service.ts @@ -0,0 +1,203 @@ +import { + DestroyRef, + EnvironmentProviders, + inject, + Injectable, + InjectionToken, + isDevMode, + makeEnvironmentProviders, + NgZone, + Provider, + Renderer2, + VERSION +} from '@angular/core'; +import { injectDocument, injectWindow } from '@radix-ng/primitives/core'; +import { RdxCdkEventServiceWindowKey } from './constants'; +import { EventType, EventTypeAsPrimitiveConfigKey, PrimitiveConfig, PrimitiveConfigs } from './types'; + +function eventTypeAsPrimitiveConfigKey(eventType: EventType): EventTypeAsPrimitiveConfigKey { + return `prevent${eventType[0].toUpperCase()}${eventType.slice(1)}` as EventTypeAsPrimitiveConfigKey; +} + +@Injectable() +class RdxCdkEventService { + document = injectDocument(); + destroyRef = inject(DestroyRef); + ngZone = inject(NgZone); + renderer2 = inject(Renderer2); + window = injectWindow(); + + primitiveConfigs?: PrimitiveConfigs; + + onDestroyCallbacks: Set<() => void> = new Set([() => deleteRdxCdkEventServiceWindowKey(this.window)]); + + #clickDomRootEventCallbacks: Set<(event: MouseEvent) => void> = new Set(); + + constructor() { + this.#listenToClickDomRootEvent(); + this.#registerOnDestroyCallbacks(); + } + + registerPrimitive(primitiveInstance: T) { + if (!this.primitiveConfigs) { + this.primitiveConfigs = new Map(); + } + if (!this.primitiveConfigs.has(primitiveInstance)) { + this.primitiveConfigs.set(primitiveInstance, {}); + } + } + + deregisterPrimitive(primitiveInstance: T) { + if (this.primitiveConfigs?.has(primitiveInstance)) { + this.primitiveConfigs.delete(primitiveInstance); + } + } + + preventPrimitiveFromCdkEvent(primitiveInstance: T, eventType: EventType) { + this.#setPreventPrimitiveFromCdkEvent(primitiveInstance, eventType, true); + } + + allowPrimitiveForCdkEvent(primitiveInstance: T, eventType: EventType) { + this.#setPreventPrimitiveFromCdkEvent(primitiveInstance, eventType, false); + } + + preventPrimitiveFromCdkMultiEvents(primitiveInstance: T, eventTypes: EventType[]) { + eventTypes.forEach((eventType) => { + this.#setPreventPrimitiveFromCdkEvent(primitiveInstance, eventType, true); + }); + } + + allowPrimitiveForCdkMultiEvents(primitiveInstance: T, eventTypes: EventType[]) { + eventTypes.forEach((eventType) => { + this.#setPreventPrimitiveFromCdkEvent(primitiveInstance, eventType, false); + }); + } + + setPreventPrimitiveFromCdkMixEvents(primitiveInstance: T, eventTypes: PrimitiveConfig) { + Object.keys(eventTypes).forEach((eventType) => { + this.#setPreventPrimitiveFromCdkEvent( + primitiveInstance, + eventType as EventType, + eventTypes[eventTypeAsPrimitiveConfigKey(eventType as EventType)] + ); + }); + } + + primitivePreventedFromCdkEvent(primitiveInstance: T, eventType: EventType) { + return this.primitiveConfigs?.get(primitiveInstance)?.[eventTypeAsPrimitiveConfigKey(eventType)]; + } + + addClickDomRootEventCallback(callback: (event: MouseEvent) => void) { + this.#clickDomRootEventCallbacks.add(callback); + } + + removeClickDomRootEventCallback(callback: (event: MouseEvent) => void) { + return this.#clickDomRootEventCallbacks.delete(callback); + } + + #setPreventPrimitiveFromCdkEvent< + T extends object, + R extends EventType, + K extends PrimitiveConfig[EventTypeAsPrimitiveConfigKey] + >(primitiveInstance: T, eventType: R, value: K) { + if (!this.primitiveConfigs?.has(primitiveInstance)) { + isDevMode() && + console.error( + '[RdxCdkEventService.preventPrimitiveFromCdkEvent] RDX Primitive instance has not been registered!', + primitiveInstance + ); + return; + } + switch (eventType) { + case 'cdkOverlayOutsideClick': + this.primitiveConfigs.get(primitiveInstance)!.preventCdkOverlayOutsideClick = value; + break; + case 'cdkOverlayEscapeKeyDown': + this.primitiveConfigs.get(primitiveInstance)!.preventCdkOverlayEscapeKeyDown = value; + break; + } + } + + #registerOnDestroyCallbacks() { + this.destroyRef.onDestroy(() => { + this.onDestroyCallbacks.forEach((onDestroyCallback) => onDestroyCallback()); + this.onDestroyCallbacks.clear(); + }); + } + + #listenToClickDomRootEvent() { + const target = this.document; + const eventName = 'click'; + const options: boolean | AddEventListenerOptions | undefined = { capture: true }; + const callback = (event: MouseEvent) => { + this.#clickDomRootEventCallbacks.forEach((clickDomRootEventCallback) => clickDomRootEventCallback(event)); + }; + + const major = parseInt(VERSION.major); + const minor = parseInt(VERSION.minor); + + let destroyClickDomRootEventListener!: () => void; + /** + * @see src/cdk/platform/features/backwards-compatibility.ts in @angular/cdk + */ + if (major > 19 || (major === 19 && minor > 0) || (major === 0 && minor === 0)) { + destroyClickDomRootEventListener = this.ngZone.runOutsideAngular(() => { + const destroyClickDomRootEventListenerInternal = this.renderer2.listen( + target, + eventName, + callback, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + options + ); + return () => { + destroyClickDomRootEventListenerInternal(); + this.#clickDomRootEventCallbacks.clear(); + }; + }); + } else { + /** + * This part can get removed when v19.1 or higher is on the board + */ + destroyClickDomRootEventListener = this.ngZone.runOutsideAngular(() => { + target.addEventListener(eventName, callback, options); + return () => { + this.ngZone.runOutsideAngular(() => target.removeEventListener(eventName, callback, options)); + this.#clickDomRootEventCallbacks.clear(); + }; + }); + } + this.onDestroyCallbacks.add(destroyClickDomRootEventListener); + } +} + +const RdxCdkEventServiceToken = new InjectionToken('RdxCdkEventServiceToken'); + +const existsErrorMessage = 'RdxCdkEventService should be provided only once!'; + +const deleteRdxCdkEventServiceWindowKey = (window: Window & typeof globalThis) => { + delete (window as any)[RdxCdkEventServiceWindowKey]; +}; + +const getProvider: (throwWhenExists?: boolean) => Provider = (throwWhenExists = true) => ({ + provide: RdxCdkEventServiceToken, + useFactory: () => { + isDevMode() && console.log('providing RdxCdkEventService...'); + const window = injectWindow(); + if ((window as any)[RdxCdkEventServiceWindowKey]) { + if (throwWhenExists) { + throw Error(existsErrorMessage); + } else { + isDevMode() && console.warn(existsErrorMessage); + } + } + (window as any)[RdxCdkEventServiceWindowKey] ??= new RdxCdkEventService(); + return (window as any)[RdxCdkEventServiceWindowKey]; + } +}); + +export const provideRdxCdkEventServiceInRoot: () => EnvironmentProviders = () => + makeEnvironmentProviders([getProvider()]); +export const provideRdxCdkEventService: () => Provider = () => getProvider(false); + +export const injectRdxCdkEventService = () => inject(RdxCdkEventServiceToken, { optional: true }); diff --git a/packages/primitives/tooltip-v2/src/utils/constants.ts b/packages/primitives/tooltip-v2/src/utils/constants.ts new file mode 100644 index 00000000..3a81df29 --- /dev/null +++ b/packages/primitives/tooltip-v2/src/utils/constants.ts @@ -0,0 +1 @@ +export const RdxCdkEventServiceWindowKey = Symbol('__RdxCdkEventService__'); diff --git a/packages/primitives/tooltip-v2/src/utils/types.ts b/packages/primitives/tooltip-v2/src/utils/types.ts new file mode 100644 index 00000000..4d1c2409 --- /dev/null +++ b/packages/primitives/tooltip-v2/src/utils/types.ts @@ -0,0 +1,7 @@ +export type EventType = 'cdkOverlayOutsideClick' | 'cdkOverlayEscapeKeyDown'; +export type EventTypeCapitalized = Capitalize; +export type EventTypeAsPrimitiveConfigKey = `prevent${EventTypeCapitalized}`; +export type PrimitiveConfig = { + [value in EventTypeAsPrimitiveConfigKey]?: boolean; +}; +export type PrimitiveConfigs = Map; diff --git a/packages/primitives/tooltip-v2/stories/tooltip-anchor.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-anchor.component.ts new file mode 100644 index 00000000..a2d254c5 --- /dev/null +++ b/packages/primitives/tooltip-v2/stories/tooltip-anchor.component.ts @@ -0,0 +1,165 @@ +import { Component, viewChild } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { LucideAngularModule, MapPin, MapPinPlus, MountainSnow, TriangleAlert, X } from 'lucide-angular'; +import { RdxPopoverModule } from '../index'; +import { RdxPopoverAnchorDirective } from '../src/tooltip-anchor.directive'; +import { RdxPopoverContentAttributesComponent } from '../src/tooltip-content-attributes.component'; +import { provideRdxCdkEventService } from '../src/utils/cdk-event.service'; +import { containerAlert } from './utils/constants'; +import { OptionPanelBase } from './utils/option-panel-base.class'; +import styles from './utils/styles.constants'; +import { WithOptionPanelComponent } from './utils/with-option-panel.component'; + +@Component({ + selector: 'rdx-popover-anchor', + providers: [provideRdxCdkEventService()], + imports: [ + FormsModule, + RdxPopoverModule, + LucideAngularModule, + RdxPopoverContentAttributesComponent, + WithOptionPanelComponent, + RdxPopoverAnchorDirective + ], + styles: styles(), + template: ` +

Internal Anchor (within PopoverRoot)

+ +
+ + {{ containerAlert }} +
+
+ + + + + + +
+ +
+

Dimensions

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
ID: {{ popoverRootDirective1()?.uniqueId() }}
+
+ +

External Anchor (outside PopoverRoot)

+ +
+ + {{ containerAlert }} +
+
+ + + + + + +
+ +
+

Dimensions

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
ID: {{ popoverRootDirective2()?.uniqueId() }}
+
+ ` +}) +export class RdxPopoverAnchorComponent extends OptionPanelBase { + readonly popoverRootDirective1 = viewChild('root1'); + readonly popoverRootDirective2 = viewChild('root2'); + + readonly MountainSnowIcon = MountainSnow; + readonly XIcon = X; + readonly LucideMapPinPlusInside = MapPinPlus; + readonly LucideMapPinPlus = MapPin; + readonly TriangleAlert = TriangleAlert; + readonly containerAlert = containerAlert; +} diff --git a/packages/primitives/tooltip-v2/stories/tooltip-animations.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-animations.component.ts new file mode 100644 index 00000000..1ab82f10 --- /dev/null +++ b/packages/primitives/tooltip-v2/stories/tooltip-animations.component.ts @@ -0,0 +1,121 @@ +import { Component, signal, viewChild } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RdxPositionAlign, RdxPositionSide } from '@radix-ng/primitives/core'; +import { LucideAngularModule, MountainSnow, TriangleAlert, X } from 'lucide-angular'; +import { RdxPopoverModule, RdxPopoverRootDirective } from '../index'; +import { RdxPopoverContentAttributesComponent } from '../src/tooltip-content-attributes.component'; +import { provideRdxCdkEventService } from '../src/utils/cdk-event.service'; +import { containerAlert } from './utils/constants'; +import { OptionPanelBase } from './utils/option-panel-base.class'; +import styles from './utils/styles.constants'; +import { WithOptionPanelComponent } from './utils/with-option-panel.component'; + +@Component({ + selector: 'rdx-popover-animations', + providers: [provideRdxCdkEventService()], + imports: [ + FormsModule, + RdxPopoverModule, + LucideAngularModule, + RdxPopoverContentAttributesComponent, + WithOptionPanelComponent + ], + styles: styles(true), + template: ` + +
+ + CSS Animation + + On Opening Animation + + On Closing Animation +
+ +
+ + {{ containerAlert }} +
+
+ + + + +
+ +
+

Dimensions

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
ID: {{ popoverRootDirective()?.uniqueId() }}
+
+ ` +}) +export class RdxPopoverAnimationsComponent extends OptionPanelBase { + readonly popoverRootDirective = viewChild(RdxPopoverRootDirective); + + readonly MountainSnowIcon = MountainSnow; + readonly XIcon = X; + + readonly sides = RdxPositionSide; + readonly aligns = RdxPositionAlign; + + cssAnimation = signal(true); + cssOpeningAnimation = signal(true); + cssClosingAnimation = signal(true); + protected readonly TriangleAlert = TriangleAlert; + protected readonly containerAlert = containerAlert; +} diff --git a/packages/primitives/tooltip-v2/stories/tooltip-default.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-default.component.ts new file mode 100644 index 00000000..ddb6e247 --- /dev/null +++ b/packages/primitives/tooltip-v2/stories/tooltip-default.component.ts @@ -0,0 +1,91 @@ +import { Component, viewChild } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { LucideAngularModule, MountainSnow, TriangleAlert, X } from 'lucide-angular'; +import { RdxPopoverModule, RdxPopoverRootDirective } from '../index'; +import { RdxPopoverContentAttributesComponent } from '../src/tooltip-content-attributes.component'; +import { provideRdxCdkEventService } from '../src/utils/cdk-event.service'; +import { containerAlert } from './utils/constants'; +import { OptionPanelBase } from './utils/option-panel-base.class'; +import styles from './utils/styles.constants'; +import { WithOptionPanelComponent } from './utils/with-option-panel.component'; + +@Component({ + selector: 'rdx-popover-default', + providers: [provideRdxCdkEventService()], + imports: [ + FormsModule, + RdxPopoverModule, + LucideAngularModule, + RdxPopoverContentAttributesComponent, + WithOptionPanelComponent + ], + styles: styles(), + template: ` + +
+ + {{ containerAlert }} +
+
+ + + + +
+ +
+

Dimensions

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
ID: {{ popoverRootDirective()?.uniqueId() }}
+
+ ` +}) +export class RdxPopoverDefaultComponent extends OptionPanelBase { + readonly popoverRootDirective = viewChild(RdxPopoverRootDirective); + + readonly MountainSnowIcon = MountainSnow; + readonly XIcon = X; + protected readonly TriangleAlert = TriangleAlert; + protected readonly containerAlert = containerAlert; +} diff --git a/packages/primitives/tooltip-v2/stories/tooltip-events.components.ts b/packages/primitives/tooltip-v2/stories/tooltip-events.components.ts new file mode 100644 index 00000000..e02cbff8 --- /dev/null +++ b/packages/primitives/tooltip-v2/stories/tooltip-events.components.ts @@ -0,0 +1,98 @@ +import { Component, viewChild } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RdxPositionAlign, RdxPositionSide } from '@radix-ng/primitives/core'; +import { LucideAngularModule, MountainSnow, TriangleAlert, X } from 'lucide-angular'; +import { RdxPopoverModule, RdxPopoverRootDirective } from '../index'; +import { RdxPopoverContentAttributesComponent } from '../src/tooltip-content-attributes.component'; +import { provideRdxCdkEventService } from '../src/utils/cdk-event.service'; +import { containerAlert } from './utils/constants'; +import { OptionPanelBase } from './utils/option-panel-base.class'; +import styles from './utils/styles.constants'; +import { WithOptionPanelComponent } from './utils/with-option-panel.component'; + +@Component({ + selector: 'rdx-popover-events', + providers: [provideRdxCdkEventService()], + imports: [ + RdxPopoverModule, + LucideAngularModule, + FormsModule, + RdxPopoverContentAttributesComponent, + WithOptionPanelComponent + ], + styles: ` + ${styles()} + `, + template: ` + +
+ + {{ containerAlert }} +
+
+ + + + +
+ +
+

Dimensions

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
ID: {{ popoverRootDirective()?.uniqueId() }}
+
+ ` +}) +export class RdxPopoverEventsComponent extends OptionPanelBase { + readonly popoverRootDirective = viewChild(RdxPopoverRootDirective); + + readonly MountainSnowIcon = MountainSnow; + readonly XIcon = X; + + protected readonly sides = RdxPositionSide; + protected readonly aligns = RdxPositionAlign; + protected readonly containerAlert = containerAlert; + protected readonly TriangleAlert = TriangleAlert; +} diff --git a/packages/primitives/tooltip-v2/stories/tooltip-initially-open.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-initially-open.component.ts new file mode 100644 index 00000000..c11e9afc --- /dev/null +++ b/packages/primitives/tooltip-v2/stories/tooltip-initially-open.component.ts @@ -0,0 +1,92 @@ +import { Component, viewChild } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { LucideAngularModule, MountainSnow, TriangleAlert, X } from 'lucide-angular'; +import { RdxPopoverModule, RdxPopoverRootDirective } from '../index'; +import { RdxPopoverContentAttributesComponent } from '../src/tooltip-content-attributes.component'; +import { provideRdxCdkEventService } from '../src/utils/cdk-event.service'; +import { containerAlert } from './utils/constants'; +import { OptionPanelBase } from './utils/option-panel-base.class'; +import styles from './utils/styles.constants'; +import { WithOptionPanelComponent } from './utils/with-option-panel.component'; + +@Component({ + selector: 'rdx-popover-initially-open', + providers: [provideRdxCdkEventService()], + imports: [ + FormsModule, + RdxPopoverModule, + LucideAngularModule, + RdxPopoverContentAttributesComponent, + WithOptionPanelComponent + ], + styles: styles(), + template: ` + +
+ + {{ containerAlert }} +
+
+ + + + +
+ +
+

Dimensions

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
ID: {{ popoverRootDirective()?.uniqueId() }}
+
+ ` +}) +export class RdxPopoverInitiallyOpenComponent extends OptionPanelBase { + readonly popoverRootDirective = viewChild(RdxPopoverRootDirective); + + readonly MountainSnowIcon = MountainSnow; + readonly XIcon = X; + protected readonly containerAlert = containerAlert; + protected readonly TriangleAlert = TriangleAlert; +} diff --git a/packages/primitives/tooltip-v2/stories/tooltip-multiple.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-multiple.component.ts new file mode 100644 index 00000000..8921f114 --- /dev/null +++ b/packages/primitives/tooltip-v2/stories/tooltip-multiple.component.ts @@ -0,0 +1,222 @@ +import { Component, viewChild } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RdxPositionAlign, RdxPositionSide } from '@radix-ng/primitives/core'; +import { LucideAngularModule, MountainSnow, TriangleAlert, X } from 'lucide-angular'; +import { RdxPopoverModule } from '../index'; +import { RdxPopoverContentAttributesComponent } from '../src/tooltip-content-attributes.component'; +import { provideRdxCdkEventService } from '../src/utils/cdk-event.service'; +import { containerAlert } from './utils/constants'; +import { OptionPanelBase } from './utils/option-panel-base.class'; +import styles from './utils/styles.constants'; +import { WithOptionPanelComponent } from './utils/with-option-panel.component'; + +@Component({ + selector: 'rdx-popover-multiple', + providers: [provideRdxCdkEventService()], + imports: [ + FormsModule, + RdxPopoverModule, + LucideAngularModule, + RdxPopoverContentAttributesComponent, + WithOptionPanelComponent + ], + styles: styles(), + template: ` + +
+ + {{ containerAlert }} +
+
+ + + + +
+ +
+

Dimensions

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
ID: {{ popoverRootDirective1()?.uniqueId() }}
+
+ + +
+ + {{ containerAlert }} +
+
+ + + + +
+ +
+

Dimensions

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
ID: {{ popoverRootDirective2()?.uniqueId() }}
+
+ + +
+ + {{ containerAlert }} +
+
+ + + + +
+ +
+

Dimensions

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
ID: {{ popoverRootDirective3()?.uniqueId() }}
+
+ ` +}) +export class RdxPopoverMultipleComponent extends OptionPanelBase { + readonly popoverRootDirective1 = viewChild('root1'); + readonly popoverRootDirective2 = viewChild('root2'); + readonly popoverRootDirective3 = viewChild('root3'); + + readonly MountainSnowIcon = MountainSnow; + readonly XIcon = X; + readonly RdxPopoverSide = RdxPositionSide; + readonly RdxPopoverAlign = RdxPositionAlign; + protected readonly containerAlert = containerAlert; + protected readonly TriangleAlert = TriangleAlert; +} diff --git a/packages/primitives/tooltip-v2/stories/tooltip-positioning.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-positioning.component.ts new file mode 100644 index 00000000..52b0c08a --- /dev/null +++ b/packages/primitives/tooltip-v2/stories/tooltip-positioning.component.ts @@ -0,0 +1,136 @@ +import { Component, signal, viewChild } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RdxPositionAlign, RdxPositionSide } from '@radix-ng/primitives/core'; +import { LucideAngularModule, MountainSnow, TriangleAlert, X } from 'lucide-angular'; +import { RdxPopoverModule, RdxPopoverRootDirective } from '../index'; +import { RdxPopoverContentAttributesComponent } from '../src/tooltip-content-attributes.component'; +import { provideRdxCdkEventService } from '../src/utils/cdk-event.service'; +import { containerAlert } from './utils/constants'; +import { OptionPanelBase } from './utils/option-panel-base.class'; +import styles from './utils/styles.constants'; +import { WithOptionPanelComponent } from './utils/with-option-panel.component'; + +@Component({ + selector: 'rdx-popover-positioning', + providers: [provideRdxCdkEventService()], + imports: [ + FormsModule, + RdxPopoverModule, + LucideAngularModule, + RdxPopoverContentAttributesComponent, + WithOptionPanelComponent + ], + styles: styles(), + template: ` + +
+ Side: + + Align: + + SideOffset: + + AlignOffset: + +
+ +
+ + Disable alternate positions (to see the result, scroll the page to make the popover cross the viewport + boundary) +
+ +
+ + {{ containerAlert }} +
+
+ + + + +
+ +
+

Dimensions

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
ID: {{ popoverRootDirective()?.uniqueId() }}
+
+ ` +}) +export class RdxPopoverPositioningComponent extends OptionPanelBase { + readonly popoverRootDirective = viewChild(RdxPopoverRootDirective); + + readonly selectedSide = signal(RdxPositionSide.Top); + readonly selectedAlign = signal(RdxPositionAlign.Center); + readonly sideOffset = signal(void 0); + readonly alignOffset = signal(void 0); + readonly disableAlternatePositions = signal(false); + + readonly sides = RdxPositionSide; + readonly aligns = RdxPositionAlign; + + readonly MountainSnowIcon = MountainSnow; + readonly XIcon = X; + protected readonly containerAlert = containerAlert; + protected readonly TriangleAlert = TriangleAlert; +} diff --git a/packages/primitives/tooltip-v2/stories/tooltip-triggering.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-triggering.component.ts new file mode 100644 index 00000000..b2b5fab1 --- /dev/null +++ b/packages/primitives/tooltip-v2/stories/tooltip-triggering.component.ts @@ -0,0 +1,224 @@ +import { Component, signal, viewChild } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { LucideAngularModule, MountainSnow, TriangleAlert, X } from 'lucide-angular'; +import { RdxPopoverModule } from '../index'; +import { RdxPopoverContentAttributesComponent } from '../src/tooltip-content-attributes.component'; +import { provideRdxCdkEventService } from '../src/utils/cdk-event.service'; +import { containerAlert } from './utils/constants'; +import { OptionPanelBase } from './utils/option-panel-base.class'; +import styles from './utils/styles.constants'; +import { WithOptionPanelComponent } from './utils/with-option-panel.component'; + +@Component({ + selector: 'rdx-popover-triggering', + providers: [provideRdxCdkEventService()], + imports: [ + FormsModule, + RdxPopoverModule, + LucideAngularModule, + RdxPopoverContentAttributesComponent, + WithOptionPanelComponent + ], + styles: styles(), + template: ` +

Initially closed

+ +
+ + onOpenChange count: {{ counterOpenFalse() }} +
+ +
+ + External control +
+ +
+ + {{ containerAlert }} +
+
+ + + + +
+ +
+

Dimensions

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
ID: {{ popoverRootDirective1()?.uniqueId() }}
+
+ +

Initially open

+ +
+ + onOpenChange count: {{ counterOpenTrue() }} +
+ +
+ + External control +
+ +
+ + {{ containerAlert }} +
+
+ + + + +
+ +
+

Dimensions

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
ID: {{ popoverRootDirective2()?.uniqueId() }}
+
+ ` +}) +export class RdxPopoverTriggeringComponent extends OptionPanelBase { + readonly popoverRootDirective1 = viewChild('root1'); + readonly popoverRootDirective2 = viewChild('root2'); + + readonly MountainSnowIcon = MountainSnow; + readonly XIcon = X; + + isOpenFalse = signal(false); + counterOpenFalse = signal(0); + externalControlFalse = signal(true); + + isOpenTrue = signal(true); + counterOpenTrue = signal(0); + externalControlTrue = signal(true); + + triggerOpenFalse(): void { + this.isOpenFalse.update((value) => !value); + } + + countOpenFalse(open: boolean): void { + this.isOpenFalse.set(open); + this.counterOpenFalse.update((value) => value + 1); + } + + triggerOpenTrue(): void { + this.isOpenTrue.update((value) => !value); + } + + countOpenTrue(open: boolean): void { + this.isOpenTrue.set(open); + this.counterOpenTrue.update((value) => value + 1); + } + + protected readonly containerAlert = containerAlert; + protected readonly TriangleAlert = TriangleAlert; +} diff --git a/packages/primitives/tooltip-v2/stories/tooltip.docs.mdx b/packages/primitives/tooltip-v2/stories/tooltip.docs.mdx new file mode 100644 index 00000000..c8c6d465 --- /dev/null +++ b/packages/primitives/tooltip-v2/stories/tooltip.docs.mdx @@ -0,0 +1,156 @@ +import {ArgTypes, Canvas, Markdown, Meta, Source} from '@storybook/blocks'; +import * as PopoverStories from "./tooltip.stories"; +import {RdxPopoverRootDirective} from "../src/tooltip-root.directive"; +import {RdxPopoverContentDirective} from "../src/tooltip-content.directive"; +import {RdxPopoverArrowDirective} from "../src/tooltip-arrow.directive"; +import {RdxPopoverAnchorDirective} from "../src/tooltip-anchor.directive"; +import {RdxPopoverTriggerDirective} from "../src/tooltip-trigger.directive"; +import {RdxPopoverCloseDirective} from "../src/tooltip-close.directive"; +import {animationStylesOnly} from "./utils/styles.constants"; +import {RdxPopoverContentAttributesComponent} from "../src/tooltip-content-attributes.component"; + + + +# Popover + +#### A popup that displays information ready to interact with related to an element when the element receives click on it. + +## Examples +### Default + + + +### Multiple + + + +### Events + + + +### Positioning + + + +### External Triggering + + + +### Anchor + + + +### Initially Open + + + +### Animations + + + +### Animation Styles + + + + +## Features + +- ✅ Opens when the trigger is clicked. +- ✅ Closes when the trigger is clicked again or when pressing escape. +- ✅ Controllable from outside. + +## Anatomy + +```html + + + + +
+ + Add to library +
+
+
+
+``` + +## Import + +Get started with importing the directives: + + + +or + + + +## API Reference + +### Root +`RdxPopoverRootDirective` + +Contains all the parts of a popover. + + + +### Trigger +`RdxPopoverTriggerDirective` + +The button that toggles the popover. By default, the PopoverContent will position itself against the trigger. + + +{` +| Data attribute | Value | +|----------------|----------------| +| [data-state] | "closed" | "open" (type RdxPopoverState) +`} + + +### Anchor +`RdxPopoverAnchorDirective` + +An optional element to position the PopoverContent against. If this part is not used, the content will position alongside the PopoverTrigger. + +### Content +`RdxPopoverContentDirective` + +The component that pops out when the popover is open. + + + +### Content Attributes +`RdxPopoverContentAttributesComponent` + +A component with the content attributes that are necessary to run animations. + + + + +{` +| Data attribute | Value | +|----------------|----------------| +| [data-state] | "closed" | "open" (enum RdxPopoverState) +| [data-side] | "left" | "right" | "bottom" | "top" (enum RdxPositionSide) +| [data-align] | "start" | "end" | "center" (enum RdxPositionAlign) +`} + + +### Arrow +`RdxPopoverArrowDirective` + +An optional arrow element to render alongside the popover. This can be used to help visually link the trigger with the PopoverContent. Must be rendered inside PopoverContent. + + + +### Close +`RdxPopoverCloseDirective` + +An optional close button element to render alongside the popover. This can be used to close the PopoverContent. Must be rendered inside PopoverContent. diff --git a/packages/primitives/tooltip-v2/stories/tooltip.stories.ts b/packages/primitives/tooltip-v2/stories/tooltip.stories.ts new file mode 100644 index 00000000..bb6eafc5 --- /dev/null +++ b/packages/primitives/tooltip-v2/stories/tooltip.stories.ts @@ -0,0 +1,114 @@ +import { provideAnimations } from '@angular/platform-browser/animations'; +import { componentWrapperDecorator, Meta, moduleMetadata, StoryObj } from '@storybook/angular'; +import { LucideAngularModule, MountainSnow, X } from 'lucide-angular'; +import { RdxPopoverModule } from '../index'; +import { RdxPopoverAnchorComponent } from './tooltip-anchor.component'; +import { RdxPopoverAnimationsComponent } from './tooltip-animations.component'; +import { RdxPopoverDefaultComponent } from './tooltip-default.component'; +import { RdxPopoverEventsComponent } from './tooltip-events.components'; +import { RdxPopoverInitiallyOpenComponent } from './tooltip-initially-open.component'; +import { RdxPopoverMultipleComponent } from './tooltip-multiple.component'; +import { RdxPopoverPositioningComponent } from './tooltip-positioning.component'; +import { RdxPopoverTriggeringComponent } from './tooltip-triggering.component'; + +const html = String.raw; + +export default { + title: 'Primitives/Tooltip-v2', + decorators: [ + moduleMetadata({ + imports: [ + RdxPopoverModule, + RdxPopoverDefaultComponent, + RdxPopoverEventsComponent, + RdxPopoverPositioningComponent, + RdxPopoverTriggeringComponent, + RdxPopoverMultipleComponent, + RdxPopoverAnimationsComponent, + RdxPopoverInitiallyOpenComponent, + RdxPopoverAnchorComponent, + LucideAngularModule, + LucideAngularModule.pick({ MountainSnow, X }) + ], + providers: [provideAnimations()] + }), + componentWrapperDecorator( + (story) => html` +
+ ${story} +
+ ` + ) + ] +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => ({ + template: html` + + ` + }) +}; + +export const Multiple: Story = { + render: () => ({ + template: html` + + ` + }) +}; + +export const Events: Story = { + render: () => ({ + template: html` + + ` + }) +}; + +export const Positioning: Story = { + render: () => ({ + template: html` + + ` + }) +}; + +export const ExternalTriggering: Story = { + render: () => ({ + template: html` + + ` + }) +}; + +export const Anchor: Story = { + render: () => ({ + template: html` + + ` + }) +}; + +export const InitiallyOpen: Story = { + render: () => ({ + template: html` + + ` + }) +}; + +export const Animations: Story = { + render: () => ({ + template: html` + + ` + }) +}; diff --git a/packages/primitives/tooltip-v2/stories/utils/constants.ts b/packages/primitives/tooltip-v2/stories/utils/constants.ts new file mode 100644 index 00000000..831c11cc --- /dev/null +++ b/packages/primitives/tooltip-v2/stories/utils/constants.ts @@ -0,0 +1,2 @@ +export const containerAlert = + 'For the sake of option panels to play with the stories, the "onOverlayEscapeKeyDown" & "onOverlayOutsideClick" events are limited to the area inside the rectangle marked with a dashed line - the events work when the area is active (focused)'; diff --git a/packages/primitives/tooltip-v2/stories/utils/containers.registry.ts b/packages/primitives/tooltip-v2/stories/utils/containers.registry.ts new file mode 100644 index 00000000..1ead99ba --- /dev/null +++ b/packages/primitives/tooltip-v2/stories/utils/containers.registry.ts @@ -0,0 +1,63 @@ +import { isDevMode } from '@angular/core'; +import { RdxPopoverRootDirective } from '../../src/tooltip-root.directive'; +import { injectRdxCdkEventService } from '../../src/utils/cdk-event.service'; + +const containerRegistry: Map = new Map(); +let rdxCdkEventService: ReturnType | undefined = void 0; + +const domRootClickEventCallback: (event: MouseEvent) => void = (event: MouseEvent) => { + const target = event.target as HTMLElement; + const containers = Array.from(containerRegistry.keys()); + const containerContainingTarget = containers + .map((container) => { + container.classList.remove('focused'); + return container; + }) + .find((container) => { + return container.contains(target); + }); + containerContainingTarget?.classList.add('focused'); + Array.from(containerRegistry.entries()).forEach((item) => { + if (item[0] === containerContainingTarget) { + rdxCdkEventService?.allowPrimitiveForCdkMultiEvents(item[1], [ + 'cdkOverlayOutsideClick', + 'cdkOverlayEscapeKeyDown' + ]); + } else { + rdxCdkEventService?.preventPrimitiveFromCdkMultiEvents(item[1], [ + 'cdkOverlayOutsideClick', + 'cdkOverlayEscapeKeyDown' + ]); + } + }); +}; + +export function registerContainer(container: HTMLElement, popoverRoot: RdxPopoverRootDirective) { + if (containerRegistry.has(container)) { + return; + } + containerRegistry.set(container, popoverRoot); + if (containerRegistry.size === 1) { + rdxCdkEventService?.addClickDomRootEventCallback(domRootClickEventCallback); + } +} + +export function deregisterContainer(container: HTMLElement) { + if (!containerRegistry.has(container)) { + return; + } + containerRegistry.delete(container); + if (containerRegistry.size === 0) { + rdxCdkEventService?.removeClickDomRootEventCallback(domRootClickEventCallback); + unsetRdxCdkEventService(); + } +} + +export function setRdxCdkEventService(service: typeof rdxCdkEventService) { + isDevMode() && console.log('setRdxCdkEventService', service, rdxCdkEventService === service); + rdxCdkEventService ??= service; +} + +export function unsetRdxCdkEventService() { + rdxCdkEventService = void 0; +} diff --git a/packages/primitives/tooltip-v2/stories/utils/option-panel-base.class.ts b/packages/primitives/tooltip-v2/stories/utils/option-panel-base.class.ts new file mode 100644 index 00000000..2ea43dbf --- /dev/null +++ b/packages/primitives/tooltip-v2/stories/utils/option-panel-base.class.ts @@ -0,0 +1,36 @@ +import { afterNextRender, DestroyRef, Directive, ElementRef, inject, signal, viewChildren } from '@angular/core'; +import { injectDocument, RDX_POSITIONING_DEFAULTS } from '@radix-ng/primitives/core'; +import { RdxPopoverRootDirective } from '../../src/tooltip-root.directive'; +import { injectRdxCdkEventService } from '../../src/utils/cdk-event.service'; +import { deregisterContainer, registerContainer, setRdxCdkEventService } from './containers.registry'; +import { IArrowDimensions, IIgnoreClickOutsideContainer } from './types'; + +@Directive() +export abstract class OptionPanelBase implements IIgnoreClickOutsideContainer, IArrowDimensions { + onOverlayEscapeKeyDownDisabled = signal(false); + onOverlayOutsideClickDisabled = signal(false); + + arrowWidth = signal(RDX_POSITIONING_DEFAULTS.arrow.width); + arrowHeight = signal(RDX_POSITIONING_DEFAULTS.arrow.height); + + readonly elementRef = inject>(ElementRef); + readonly destroyRef = inject(DestroyRef); + readonly rootDirectives = viewChildren(RdxPopoverRootDirective); + readonly document = injectDocument(); + readonly rdxCdkEventService = injectRdxCdkEventService(); + + protected constructor() { + afterNextRender(() => { + this.elementRef.nativeElement.querySelectorAll('.container').forEach((container) => { + const popoverRootInsideContainer = this.rootDirectives().find((popoverRoot) => + container.contains(popoverRoot.popoverTriggerDirective().elementRef.nativeElement) + ); + if (popoverRootInsideContainer) { + setRdxCdkEventService(this.rdxCdkEventService); + registerContainer(container, popoverRootInsideContainer); + this.destroyRef.onDestroy(() => deregisterContainer(container)); + } + }); + }); + } +} diff --git a/packages/primitives/tooltip-v2/stories/utils/styles.constants.ts b/packages/primitives/tooltip-v2/stories/utils/styles.constants.ts new file mode 100644 index 00000000..ea3adb21 --- /dev/null +++ b/packages/primitives/tooltip-v2/stories/utils/styles.constants.ts @@ -0,0 +1,352 @@ +const appliedAnimations = ` +.PopoverContent[data-state='open'][data-side='top'] { + animation-name: rdxSlideDownAndFade; +} + +.PopoverContent[data-state='open'][data-side='right'] { + animation-name: rdxSlideLeftAndFade; +} + +.PopoverContent[data-state='open'][data-side='bottom'] { + animation-name: rdxSlideUpAndFade; +} + +.PopoverContent[data-state='open'][data-side='left'] { + animation-name: rdxSlideRightAndFade; +} + +.PopoverContent[data-state='closed'][data-side='top'] { + animation-name: rdxSlideDownAndFadeReverse; +} + +.PopoverContent[data-state='closed'][data-side='right'] { + animation-name: rdxSlideLeftAndFadeReverse; +} + +.PopoverContent[data-state='closed'][data-side='bottom'] { + animation-name: rdxSlideUpAndFadeReverse; +} + +.PopoverContent[data-state='closed'][data-side='left'] { + animation-name: rdxSlideRightAndFadeReverse; +} +`; + +const animationParams = ` +.PopoverContent { + animation-duration: 400ms; + animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); + will-change: transform, opacity; +} +`; + +const animationDefs = ` +/* Opening animations */ + +@keyframes rdxSlideUpAndFade { + from { + opacity: 0; + transform: translateY(2px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes rdxSlideRightAndFade { + from { + opacity: 0; + transform: translateX(-2px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes rdxSlideDownAndFade { + from { + opacity: 0; + transform: translateY(-2px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes rdxSlideLeftAndFade { + from { + opacity: 0; + transform: translateX(2px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +/* Closing animations */ + +@keyframes rdxSlideUpAndFadeReverse { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(2px); + } +} + +@keyframes rdxSlideRightAndFadeReverse { + from { + opacity: 1; + transform: translateX(0); + } + to { + opacity: 0; + transform: translateX(-2px); + } +} + +@keyframes rdxSlideDownAndFadeReverse { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(-2px); + } +} + +@keyframes rdxSlideLeftAndFadeReverse { + from { + opacity: 1; + transform: translateX(0); + } + to { + opacity: 0; + transform: translateX(2px); + } +} +`; + +const events = ` +/* =============== Event messages =============== */ + +.MessagesContainer { + padding: 20px; +} + +.Message { + color: var(--white-a12); + font-size: 15px; + line-height: 19px; + font-weight: bolder; +} + +.MessageId { + font-size: 75%; + font-weight: light; +} +`; + +const params = ` +/* =============== Params layout =============== */ + +.ParamsContainer { + display: flex; + column-gap: 8px; + color: var(--white-a12); + padding-bottom: 32px; +} +`; + +function styles(withAnimations = false, withEvents = false, withParams = true) { + return ` +.container { + height: 500px; + display: flex; + justify-content: center; + gap: 80px; + align-items: center; + border: 3px dashed var(--white-a8); + border-radius: 12px; + &.focused { + border-color: var(--white-a12); + -webkit-box-shadow: 0px 0px 24px 0px var(--white-a12); + -moz-box-shadow: 0px 0px 24px 0px var(--white-a12); + box-shadow: 0px 0px 24px 0px var(--white-a12); + } +} + +.ContainerAlerts { + display: flex; + gap: 6px; + color: var(--white-a8); + font-size: 16px; + line-height: 16px; + margin: 0 0 24px 0; +} + +/* reset */ +.reset { + all: unset; +} + +.ExampleSubtitle { + color: var(--white-a12); + font-size: 22px; + line-height: 26px; + font-weight: bolder; + margin: 46px 0 34px 16px; + padding-top: 22px; + &:not(:first-child) { + border-top: 2px solid var(--gray-a8); + } + &:first-child { + margin-top: 0; + } +} + +.PopoverId { + color: var(--white-a12); + font-size: 12px; + line-height: 14px; + font-weight: 800; + margin: 1px 0 24px 22px; +} + +.PopoverContent { + border-radius: 4px; + padding: 20px; + width: 260px; + background-color: white; + box-shadow: + hsl(206 22% 7% / 35%) 0px 10px 38px -10px, + hsl(206 22% 7% / 20%) 0px 10px 20px -15px; +} + +${withAnimations ? animationParams : ''} + +${withAnimations ? appliedAnimations : ''} + +.PopoverContent:focus { + box-shadow: + hsl(206 22% 7% / 35%) 0px 10px 38px -10px, + hsl(206 22% 7% / 20%) 0px 10px 20px -15px, + 0 0 0 2px var(--violet-7); +} + +.PopoverArrow { + fill: white; +} + +.PopoverClose { + font-family: inherit; + border-radius: 100%; + height: 25px; + width: 25px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--violet-11); + position: absolute; + top: 5px; + right: 5px; +} + +.PopoverClose:hover { + background-color: var(--violet-4); +} + +.PopoverClose:focus { + box-shadow: 0 0 0 2px var(--violet-7); +} + +.IconButton { + font-family: inherit; + border-radius: 100%; + height: 35px; + width: 35px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--violet-11); + background-color: white; + box-shadow: 0 2px 10px var(--black-a7); +} + +.IconButton:hover { + background-color: var(--violet-3); +} + +.IconButton:focus { + box-shadow: 0 0 0 2px black; +} + +.Fieldset { + display: flex; + gap: 20px; + align-items: center; +} + +.Label { + font-size: 13px; + color: var(--violet-11); + width: 75px; +} + +.Input { + width: 100%; + display: inline-flex; + align-items: center; + justify-content: center; + flex: 1; + border-radius: 4px; + padding: 0 10px; + font-size: 13px; + line-height: 1; + color: var(--violet-11); + box-shadow: 0 0 0 1px var(--violet-7); + height: 25px; +} + +.Input:focus { + box-shadow: 0 0 0 2px var(--violet-8); +} + +.Text { + margin: 0; + color: var(--mauve-12); + font-size: 15px; + line-height: 19px; + font-weight: 500; +} + +${withAnimations ? animationDefs : ''} + +${withParams ? params : ''} + +${withEvents ? events : ''} +`; +} + +export const animationStylesOnly = ` +${animationParams} + +${appliedAnimations} + +${animationDefs} +`; + +export const paramsAndEventsOnly = ` +${params} + +${events} +`; + +export default styles; diff --git a/packages/primitives/tooltip-v2/stories/utils/types.ts b/packages/primitives/tooltip-v2/stories/utils/types.ts new file mode 100644 index 00000000..d57e53d5 --- /dev/null +++ b/packages/primitives/tooltip-v2/stories/utils/types.ts @@ -0,0 +1,20 @@ +import { DestroyRef, ElementRef, Signal } from '@angular/core'; +import { RdxPopoverRootDirective } from '../../src/tooltip-root.directive'; +import { injectRdxCdkEventService } from '../../src/utils/cdk-event.service'; + +export interface IIgnoreClickOutsideContainer { + onOverlayEscapeKeyDownDisabled: Signal; + onOverlayOutsideClickDisabled: Signal; + elementRef: ElementRef; + destroyRef: DestroyRef; + rootDirectives: Signal>; + document: Document; + rdxCdkEventService: ReturnType; +} + +export interface IArrowDimensions { + arrowWidth: Signal; + arrowHeight: Signal; +} + +export type Message = { value: string; timeFromPrev: number }; diff --git a/packages/primitives/tooltip-v2/stories/utils/with-option-panel.component.ts b/packages/primitives/tooltip-v2/stories/utils/with-option-panel.component.ts new file mode 100644 index 00000000..71e40fbc --- /dev/null +++ b/packages/primitives/tooltip-v2/stories/utils/with-option-panel.component.ts @@ -0,0 +1,180 @@ +import { NgTemplateOutlet } from '@angular/common'; +import { + afterNextRender, + Component, + computed, + contentChild, + ElementRef, + inject, + isDevMode, + model, + signal +} from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RdxPopoverRootDirective } from '../../src/tooltip-root.directive'; +import { paramsAndEventsOnly } from './styles.constants'; +import { Message } from './types'; + +@Component({ + selector: 'popover-with-option-panel', + styles: paramsAndEventsOnly, + template: ` + + + @if (paramsContainerCounter() > 2) { +
+ } + +
+ + Disable (onOverlayEscapeKeyDown) event + + Disable (onOverlayOutsideClick) event +
+ +
+ Arrow width + + Arrow height + +
+ + + + @if (messages().length) { + +
+ @for (message of messages(); track i; let i = $index) { + + } +
+ } + + +

+ {{ index }}. + [({{ message.timeFromPrev }}ms) POPOVER ID {{ rootUniqueId() }}] + {{ message.value }} +

+
+ `, + imports: [ + ReactiveFormsModule, + FormsModule, + NgTemplateOutlet + ] +}) +export class WithOptionPanelComponent { + onOverlayEscapeKeyDownDisabled = model(false); + onOverlayOutsideClickDisabled = model(false); + + arrowWidth = model(0); + arrowHeight = model(0); + + readonly elementRef = inject>(ElementRef); + + readonly popoverRootDirective = contentChild.required(RdxPopoverRootDirective); + + readonly paramsContainerCounter = signal(0); + + readonly messages = signal([]); + readonly rootUniqueId = computed(() => this.popoverRootDirective().uniqueId()); + + /** + * There should be only one container. If there is more, en error is thrown. + */ + containers: Element[] | undefined = void 0; + paramsContainers: Element[] | undefined = void 0; + + previousMessageTimestamp: number | undefined = void 0; + + timeFromPrev = () => { + const now = Date.now(); + const timeFromPrev = + typeof this.previousMessageTimestamp === 'undefined' ? 0 : Date.now() - this.previousMessageTimestamp; + this.previousMessageTimestamp = now; + return timeFromPrev; + }; + + constructor() { + afterNextRender({ + read: () => { + this.popoverRootDirective().popoverContentDirective().onOpen.subscribe(this.onOpen); + this.popoverRootDirective().popoverContentDirective().onClosed.subscribe(this.onClose); + this.popoverRootDirective() + .popoverContentDirective() + .onOverlayOutsideClick.subscribe(this.onOverlayOutsideClick); + this.popoverRootDirective() + .popoverContentDirective() + .onOverlayEscapeKeyDown.subscribe(this.onOverlayEscapeKeyDown); + + /** + * There should be only one container. If there is more, en error is thrown. + */ + this.containers = Array.from(this.elementRef.nativeElement?.querySelectorAll('.container') ?? []); + if (this.containers.length > 1) { + if (isDevMode()) { + console.error('.elementRef.nativeElement', this.elementRef.nativeElement); + console.error('.containers', this.containers); + throw Error('each story should have only one container!'); + } + } + this.paramsContainers = Array.from( + this.elementRef.nativeElement?.querySelectorAll('.ParamsContainer') ?? [] + ); + + this.paramsContainerCounter.set(this.paramsContainers.length ?? 0); + } + }); + } + + private inContainers(element: Element) { + return !!this.containers?.find((container) => container.contains(element)); + } + + private inParamsContainers(element: Element) { + return !!this.paramsContainers?.find((container) => container.contains(element)); + } + + private onOverlayEscapeKeyDown = () => { + this.addMessage({ + value: `[PopoverRoot] Escape clicked! (disabled: ${this.onOverlayEscapeKeyDownDisabled()})`, + timeFromPrev: this.timeFromPrev() + }); + }; + + private onOverlayOutsideClick = () => { + this.addMessage({ + value: `[PopoverRoot] Mouse clicked outside the popover! (disabled: ${this.onOverlayOutsideClickDisabled()})`, + timeFromPrev: this.timeFromPrev() + }); + }; + + private onOpen = () => { + this.addMessage({ value: '[PopoverContent] Open', timeFromPrev: this.timeFromPrev() }); + }; + + private onClose = () => { + this.addMessage({ value: '[PopoverContent] Closed', timeFromPrev: this.timeFromPrev() }); + }; + + protected addMessage = (message: Message) => { + this.messages.update((messages) => { + return [ + message, + ...messages + ]; + }); + }; +} From 0b8df8e2c7a2537574f83c83e0cef509b10bc314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Twardziak?= Date: Mon, 30 Dec 2024 00:23:57 +0100 Subject: [PATCH 04/11] chore(tooltip): rename all popover to tooltip --- packages/primitives/tooltip-v2/index.ts | 30 ++--- .../src/tooltip-anchor.directive.ts | 35 +++-- .../tooltip-v2/src/tooltip-anchor.token.ts | 4 +- .../tooltip-v2/src/tooltip-arrow.directive.ts | 18 +-- .../tooltip-v2/src/tooltip-arrow.token.ts | 4 +- .../tooltip-v2/src/tooltip-close.directive.ts | 18 +-- .../tooltip-v2/src/tooltip-close.token.ts | 4 +- .../tooltip-content-attributes.component.ts | 46 +++---- .../src/tooltip-content-attributes.token.ts | 6 +- .../src/tooltip-content.directive.ts | 46 +++---- .../tooltip-v2/src/tooltip-root.directive.ts | 124 +++++++++--------- .../tooltip-v2/src/tooltip-root.inject.ts | 12 +- .../src/tooltip-trigger.directive.ts | 18 +-- .../tooltip-v2/src/tooltip.types.ts | 6 +- .../stories/tooltip-anchor.component.ts | 68 +++++----- .../stories/tooltip-animations.component.ts | 34 ++--- .../stories/tooltip-default.component.ts | 34 ++--- .../stories/tooltip-events.components.ts | 34 ++--- .../tooltip-initially-open.component.ts | 34 ++--- .../stories/tooltip-multiple.component.ts | 90 ++++++------- .../stories/tooltip-positioning.component.ts | 36 ++--- .../stories/tooltip-triggering.component.ts | 60 ++++----- .../tooltip-v2/stories/tooltip.docs.mdx | 102 +++++++------- .../tooltip-v2/stories/tooltip.stories.ts | 52 ++++---- .../stories/utils/containers.registry.ts | 8 +- .../stories/utils/option-panel-base.class.ts | 12 +- .../stories/utils/styles.constants.ts | 32 ++--- .../tooltip-v2/stories/utils/types.ts | 4 +- .../utils/with-option-panel.component.ts | 30 ++--- 29 files changed, 497 insertions(+), 504 deletions(-) diff --git a/packages/primitives/tooltip-v2/index.ts b/packages/primitives/tooltip-v2/index.ts index 6d88e4f2..0dca7571 100644 --- a/packages/primitives/tooltip-v2/index.ts +++ b/packages/primitives/tooltip-v2/index.ts @@ -1,11 +1,11 @@ import { NgModule } from '@angular/core'; -import { RdxPopoverAnchorDirective } from './src/tooltip-anchor.directive'; -import { RdxPopoverArrowDirective } from './src/tooltip-arrow.directive'; -import { RdxPopoverCloseDirective } from './src/tooltip-close.directive'; -import { RdxPopoverContentAttributesComponent } from './src/tooltip-content-attributes.component'; -import { RdxPopoverContentDirective } from './src/tooltip-content.directive'; -import { RdxPopoverRootDirective } from './src/tooltip-root.directive'; -import { RdxPopoverTriggerDirective } from './src/tooltip-trigger.directive'; +import { RdxTooltipAnchorDirective } from './src/tooltip-anchor.directive'; +import { RdxTooltipArrowDirective } from './src/tooltip-arrow.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'; @@ -16,17 +16,17 @@ export * from './src/tooltip-root.directive'; export * from './src/tooltip-trigger.directive'; const _imports = [ - RdxPopoverArrowDirective, - RdxPopoverCloseDirective, - RdxPopoverContentDirective, - RdxPopoverTriggerDirective, - RdxPopoverRootDirective, - RdxPopoverAnchorDirective, - RdxPopoverContentAttributesComponent + RdxTooltipArrowDirective, + RdxTooltipCloseDirective, + RdxTooltipContentDirective, + RdxTooltipTriggerDirective, + RdxTooltipRootDirective, + RdxTooltipAnchorDirective, + RdxTooltipContentAttributesComponent ]; @NgModule({ imports: [..._imports], exports: [..._imports] }) -export class RdxPopoverModule {} +export class RdxTooltipModule {} diff --git a/packages/primitives/tooltip-v2/src/tooltip-anchor.directive.ts b/packages/primitives/tooltip-v2/src/tooltip-anchor.directive.ts index 74dc7d18..0460c09a 100644 --- a/packages/primitives/tooltip-v2/src/tooltip-anchor.directive.ts +++ b/packages/primitives/tooltip-v2/src/tooltip-anchor.directive.ts @@ -1,13 +1,13 @@ import { CdkOverlayOrigin } from '@angular/cdk/overlay'; import { computed, Directive, ElementRef, forwardRef, inject } from '@angular/core'; import { injectDocument } from '@radix-ng/primitives/core'; -import { RdxPopoverAnchorToken } from './tooltip-anchor.token'; -import { RdxPopoverRootDirective } from './tooltip-root.directive'; -import { injectPopoverRoot } from './tooltip-root.inject'; +import { RdxTooltipAnchorToken } from './tooltip-anchor.token'; +import { RdxTooltipRootDirective } from './tooltip-root.directive'; +import { injectTooltipRoot } from './tooltip-root.inject'; @Directive({ - selector: '[rdxPopoverAnchor]', - exportAs: 'rdxPopoverAnchor', + selector: '[rdxTooltipAnchor]', + exportAs: 'rdxTooltipAnchor', hostDirectives: [CdkOverlayOrigin], host: { type: 'button', @@ -17,18 +17,18 @@ import { injectPopoverRoot } from './tooltip-root.inject'; }, providers: [ { - provide: RdxPopoverAnchorToken, - useExisting: forwardRef(() => RdxPopoverAnchorDirective) + provide: RdxTooltipAnchorToken, + useExisting: forwardRef(() => RdxTooltipAnchorDirective) } ] }) -export class RdxPopoverAnchorDirective { +export class RdxTooltipAnchorDirective { /** * @ignore - * If outside the root then null, otherwise the root directive - with optional `true` passed in as the first param. - * If outside the root and non-null value that means the html structure is wrong - popover inside popover. + * 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 popoverRoot = injectPopoverRoot(true); + protected rootDirective = injectTooltipRoot(true); /** @ignore */ readonly elementRef = inject(ElementRef); /** @ignore */ @@ -37,7 +37,7 @@ export class RdxPopoverAnchorDirective { readonly document = injectDocument(); /** @ignore */ - readonly name = computed(() => `rdx-popover-external-anchor-${this.popoverRoot?.uniqueId()}`); + readonly name = computed(() => `rdx-tooltip-external-anchor-${this.rootDirective?.uniqueId()}`); /** @ignore */ click(): void { @@ -45,15 +45,12 @@ export class RdxPopoverAnchorDirective { } /** @ignore */ - setPopoverRoot(popoverRoot: RdxPopoverRootDirective) { - this.popoverRoot = popoverRoot; + setRoot(root: RdxTooltipRootDirective) { + this.rootDirective = root; } private emitOutsideClick() { - if ( - !this.popoverRoot?.isOpen() || - this.popoverRoot?.popoverContentDirective().onOverlayOutsideClickDisabled() - ) { + if (!this.rootDirective?.isOpen() || this.rootDirective?.contentDirective().onOverlayOutsideClickDisabled()) { return; } const clickEvent = new MouseEvent('click', { @@ -62,6 +59,6 @@ export class RdxPopoverAnchorDirective { cancelable: true, relatedTarget: this.elementRef.nativeElement }); - this.popoverRoot?.popoverTriggerDirective().elementRef.nativeElement.dispatchEvent(clickEvent); + this.rootDirective?.triggerDirective().elementRef.nativeElement.dispatchEvent(clickEvent); } } diff --git a/packages/primitives/tooltip-v2/src/tooltip-anchor.token.ts b/packages/primitives/tooltip-v2/src/tooltip-anchor.token.ts index 49bd5d10..4d488784 100644 --- a/packages/primitives/tooltip-v2/src/tooltip-anchor.token.ts +++ b/packages/primitives/tooltip-v2/src/tooltip-anchor.token.ts @@ -1,4 +1,4 @@ import { InjectionToken } from '@angular/core'; -import { RdxPopoverAnchorDirective } from './tooltip-anchor.directive'; +import { RdxTooltipAnchorDirective } from './tooltip-anchor.directive'; -export const RdxPopoverAnchorToken = new InjectionToken('RdxPopoverAnchorToken'); +export const RdxTooltipAnchorToken = new InjectionToken('RdxTooltipAnchorToken'); diff --git a/packages/primitives/tooltip-v2/src/tooltip-arrow.directive.ts b/packages/primitives/tooltip-v2/src/tooltip-arrow.directive.ts index d27d1eb0..4db29f82 100644 --- a/packages/primitives/tooltip-v2/src/tooltip-arrow.directive.ts +++ b/packages/primitives/tooltip-v2/src/tooltip-arrow.directive.ts @@ -20,23 +20,23 @@ import { getSideAndAlignFromAllPossibleConnectedPositions, RDX_POSITIONING_DEFAULTS } from '@radix-ng/primitives/core'; -import { RdxPopoverArrowToken } from './tooltip-arrow.token'; -import { injectPopoverRoot } from './tooltip-root.inject'; +import { RdxTooltipArrowToken } from './tooltip-arrow.token'; +import { injectTooltipRoot } from './tooltip-root.inject'; @Directive({ - selector: '[rdxPopoverArrow]', + selector: '[rdxTooltipArrow]', providers: [ { - provide: RdxPopoverArrowToken, - useExisting: forwardRef(() => RdxPopoverArrowDirective) + provide: RdxTooltipArrowToken, + useExisting: forwardRef(() => RdxTooltipArrowDirective) } ] }) -export class RdxPopoverArrowDirective { +export class RdxTooltipArrowDirective { /** @ignore */ private readonly renderer = inject(Renderer2); /** @ignore */ - private readonly popoverRoot = injectPopoverRoot(); + private readonly rootDirective = injectTooltipRoot(); /** @ignore */ readonly elementRef = inject(ElementRef); @@ -72,7 +72,7 @@ export class RdxPopoverArrowDirective { /** @ignore */ private readonly currentArrowSvgElement = signal(void 0); /** @ignore */ - private readonly position = toSignal(this.popoverRoot.popoverContentDirective().positionChange()); + private readonly position = toSignal(this.rootDirective.contentDirective().positionChange()); /** @ignore */ private anchorOrTriggerRect: DOMRect; @@ -95,7 +95,7 @@ export class RdxPopoverArrowDirective { /** @ignore */ private setAnchorOrTriggerRect() { this.anchorOrTriggerRect = ( - this.popoverRoot.popoverAnchorDirective() ?? this.popoverRoot.popoverTriggerDirective() + this.rootDirective.anchorDirective() ?? this.rootDirective.triggerDirective() ).elementRef.nativeElement.getBoundingClientRect(); } diff --git a/packages/primitives/tooltip-v2/src/tooltip-arrow.token.ts b/packages/primitives/tooltip-v2/src/tooltip-arrow.token.ts index a99039bc..091799ec 100644 --- a/packages/primitives/tooltip-v2/src/tooltip-arrow.token.ts +++ b/packages/primitives/tooltip-v2/src/tooltip-arrow.token.ts @@ -1,4 +1,4 @@ import { InjectionToken } from '@angular/core'; -import { RdxPopoverArrowDirective } from './tooltip-arrow.directive'; +import { RdxTooltipArrowDirective } from './tooltip-arrow.directive'; -export const RdxPopoverArrowToken = new InjectionToken('RdxPopoverArrowToken'); +export const RdxTooltipArrowToken = new InjectionToken('RdxTooltipArrowToken'); diff --git a/packages/primitives/tooltip-v2/src/tooltip-close.directive.ts b/packages/primitives/tooltip-v2/src/tooltip-close.directive.ts index 8d53dc2c..84eb6567 100644 --- a/packages/primitives/tooltip-v2/src/tooltip-close.directive.ts +++ b/packages/primitives/tooltip-v2/src/tooltip-close.directive.ts @@ -1,23 +1,23 @@ import { Directive, effect, ElementRef, forwardRef, inject, Renderer2, untracked } from '@angular/core'; -import { RdxPopoverCloseToken } from './tooltip-close.token'; -import { injectPopoverRoot } from './tooltip-root.inject'; +import { RdxTooltipCloseToken } from './tooltip-close.token'; +import { injectTooltipRoot } from './tooltip-root.inject'; @Directive({ - selector: '[rdxPopoverClose]', + selector: '[rdxTooltipClose]', host: { type: 'button', - '(click)': 'popoverRoot.handleClose()' + '(click)': 'rootDirective.handleClose()' }, providers: [ { - provide: RdxPopoverCloseToken, - useExisting: forwardRef(() => RdxPopoverCloseDirective) + provide: RdxTooltipCloseToken, + useExisting: forwardRef(() => RdxTooltipCloseDirective) } ] }) -export class RdxPopoverCloseDirective { +export class RdxTooltipCloseDirective { /** @ignore */ - protected readonly popoverRoot = injectPopoverRoot(); + protected readonly rootDirective = injectTooltipRoot(); /** @ignore */ readonly elementRef = inject(ElementRef); /** @ignore */ @@ -30,7 +30,7 @@ export class RdxPopoverCloseDirective { /** @ignore */ private onIsControlledExternallyEffect() { effect(() => { - const isControlledExternally = this.popoverRoot.controlledExternally()(); + const isControlledExternally = this.rootDirective.controlledExternally()(); untracked(() => { this.renderer.setStyle( diff --git a/packages/primitives/tooltip-v2/src/tooltip-close.token.ts b/packages/primitives/tooltip-v2/src/tooltip-close.token.ts index 5cc244ea..93a9c08e 100644 --- a/packages/primitives/tooltip-v2/src/tooltip-close.token.ts +++ b/packages/primitives/tooltip-v2/src/tooltip-close.token.ts @@ -1,4 +1,4 @@ import { InjectionToken } from '@angular/core'; -import { RdxPopoverCloseDirective } from './tooltip-close.directive'; +import { RdxTooltipCloseDirective } from './tooltip-close.directive'; -export const RdxPopoverCloseToken = new InjectionToken('RdxPopoverCloseToken'); +export const RdxTooltipCloseToken = new InjectionToken('RdxTooltipCloseToken'); diff --git a/packages/primitives/tooltip-v2/src/tooltip-content-attributes.component.ts b/packages/primitives/tooltip-v2/src/tooltip-content-attributes.component.ts index 12fc4943..c7f224a9 100644 --- a/packages/primitives/tooltip-v2/src/tooltip-content-attributes.component.ts +++ b/packages/primitives/tooltip-v2/src/tooltip-content-attributes.component.ts @@ -1,65 +1,65 @@ import { ChangeDetectionStrategy, Component, computed, forwardRef } from '@angular/core'; -import { RdxPopoverContentAttributesToken } from './tooltip-content-attributes.token'; -import { injectPopoverRoot } from './tooltip-root.inject'; -import { RdxPopoverAnimationStatus, RdxPopoverState } from './tooltip.types'; +import { RdxTooltipContentAttributesToken } from './tooltip-content-attributes.token'; +import { injectTooltipRoot } from './tooltip-root.inject'; +import { RdxTooltipAnimationStatus, RdxTooltipState } from './tooltip.types'; @Component({ - selector: '[rdxPopoverContentAttributes]', + selector: '[rdxTooltipContentAttributes]', template: ` `, host: { '[attr.role]': '"dialog"', '[attr.id]': 'name()', - '[attr.data-state]': 'popoverRoot.state()', - '[attr.data-side]': 'popoverRoot.popoverContentDirective().side()', - '[attr.data-align]': 'popoverRoot.popoverContentDirective().align()', + '[attr.data-state]': 'rootDirective.state()', + '[attr.data-side]': 'rootDirective.contentDirective().side()', + '[attr.data-align]': 'rootDirective.contentDirective().align()', '[style]': 'disableAnimation() ? {animation: "none !important"} : null', '(animationstart)': 'onAnimationStart($event)', '(animationend)': 'onAnimationEnd($event)' }, providers: [ { - provide: RdxPopoverContentAttributesToken, - useExisting: forwardRef(() => RdxPopoverContentAttributesComponent) + provide: RdxTooltipContentAttributesToken, + useExisting: forwardRef(() => RdxTooltipContentAttributesComponent) } ], changeDetection: ChangeDetectionStrategy.OnPush }) -export class RdxPopoverContentAttributesComponent { +export class RdxTooltipContentAttributesComponent { /** @ignore */ - protected readonly popoverRoot = injectPopoverRoot(); + protected readonly rootDirective = injectTooltipRoot(); /** @ignore */ - readonly name = computed(() => `rdx-popover-content-attributes-${this.popoverRoot.uniqueId()}`); + readonly name = computed(() => `rdx-tooltip-content-attributes-${this.rootDirective.uniqueId()}`); /** @ignore */ readonly disableAnimation = computed(() => !this.canAnimate()); /** @ignore */ protected onAnimationStart(_: AnimationEvent) { - this.popoverRoot.cssAnimationStatus.set( - this.popoverRoot.state() === RdxPopoverState.OPEN - ? RdxPopoverAnimationStatus.OPEN_STARTED - : RdxPopoverAnimationStatus.CLOSED_STARTED + this.rootDirective.cssAnimationStatus.set( + this.rootDirective.state() === RdxTooltipState.OPEN + ? RdxTooltipAnimationStatus.OPEN_STARTED + : RdxTooltipAnimationStatus.CLOSED_STARTED ); } /** @ignore */ protected onAnimationEnd(_: AnimationEvent) { - this.popoverRoot.cssAnimationStatus.set( - this.popoverRoot.state() === RdxPopoverState.OPEN - ? RdxPopoverAnimationStatus.OPEN_ENDED - : RdxPopoverAnimationStatus.CLOSED_ENDED + this.rootDirective.cssAnimationStatus.set( + this.rootDirective.state() === RdxTooltipState.OPEN + ? RdxTooltipAnimationStatus.OPEN_ENDED + : RdxTooltipAnimationStatus.CLOSED_ENDED ); } /** @ignore */ private canAnimate() { return ( - this.popoverRoot.cssAnimation() && - ((this.popoverRoot.cssOpeningAnimation() && this.popoverRoot.state() === RdxPopoverState.OPEN) || - (this.popoverRoot.cssClosingAnimation() && this.popoverRoot.state() === RdxPopoverState.CLOSED)) + this.rootDirective.cssAnimation() && + ((this.rootDirective.cssOpeningAnimation() && this.rootDirective.state() === RdxTooltipState.OPEN) || + (this.rootDirective.cssClosingAnimation() && this.rootDirective.state() === RdxTooltipState.CLOSED)) ); } } diff --git a/packages/primitives/tooltip-v2/src/tooltip-content-attributes.token.ts b/packages/primitives/tooltip-v2/src/tooltip-content-attributes.token.ts index 8e4ae123..5d93d060 100644 --- a/packages/primitives/tooltip-v2/src/tooltip-content-attributes.token.ts +++ b/packages/primitives/tooltip-v2/src/tooltip-content-attributes.token.ts @@ -1,6 +1,6 @@ import { InjectionToken } from '@angular/core'; -import { RdxPopoverContentAttributesComponent } from './tooltip-content-attributes.component'; +import { RdxTooltipContentAttributesComponent } from './tooltip-content-attributes.component'; -export const RdxPopoverContentAttributesToken = new InjectionToken( - 'RdxPopoverContentAttributesToken' +export const RdxTooltipContentAttributesToken = new InjectionToken( + 'RdxTooltipContentAttributesToken' ); diff --git a/packages/primitives/tooltip-v2/src/tooltip-content.directive.ts b/packages/primitives/tooltip-v2/src/tooltip-content.directive.ts index 72fc76d2..e967a279 100644 --- a/packages/primitives/tooltip-v2/src/tooltip-content.directive.ts +++ b/packages/primitives/tooltip-v2/src/tooltip-content.directive.ts @@ -25,18 +25,18 @@ import { RdxPositionSideAndAlignOffsets } from '@radix-ng/primitives/core'; import { filter, tap } from 'rxjs'; -import { injectPopoverRoot } from './tooltip-root.inject'; -import { RdxPopoverAttachDetachEvent } from './tooltip.types'; +import { injectTooltipRoot } from './tooltip-root.inject'; +import { RdxTooltipAttachDetachEvent } from './tooltip.types'; @Directive({ - selector: '[rdxPopoverContent]', + selector: '[rdxTooltipContent]', hostDirectives: [ CdkConnectedOverlay ] }) -export class RdxPopoverContentDirective implements OnInit { +export class RdxTooltipContentDirective implements OnInit { /** @ignore */ - private readonly popoverRoot = injectPopoverRoot(); + private readonly rootDirective = injectTooltipRoot(); /** @ignore */ private readonly templateRef = inject(TemplateRef); /** @ignore */ @@ -47,7 +47,7 @@ export class RdxPopoverContentDirective implements OnInit { private readonly connectedOverlay = inject(CdkConnectedOverlay); /** @ignore */ - readonly name = computed(() => `rdx-popover-trigger-${this.popoverRoot.uniqueId()}`); + readonly name = computed(() => `rdx-tooltip-trigger-${this.rootDirective.uniqueId()}`); /** * @description The preferred side of the trigger to render against when open. Will be reversed when collisions occur and avoidCollisions is enabled. @@ -157,8 +157,8 @@ export class RdxPopoverContentDirective implements OnInit { filter( () => !this.onOverlayEscapeKeyDownDisabled() && - !this.popoverRoot.rdxCdkEventService?.primitivePreventedFromCdkEvent( - this.popoverRoot, + !this.rootDirective.rdxCdkEventService?.primitivePreventedFromCdkEvent( + this.rootDirective, 'cdkOverlayEscapeKeyDown' ) ), @@ -166,9 +166,9 @@ export class RdxPopoverContentDirective implements OnInit { tap((event) => { this.onOverlayEscapeKeyDown.emit(event); }), - filter(() => !this.popoverRoot.firstDefaultOpen()), + filter(() => !this.rootDirective.firstDefaultOpen()), tap(() => { - this.popoverRoot.handleClose(); + this.rootDirective.handleClose(); }), takeUntilDestroyed(this.destroyRef) ) @@ -183,8 +183,8 @@ export class RdxPopoverContentDirective implements OnInit { filter( () => !this.onOverlayOutsideClickDisabled() && - !this.popoverRoot.rdxCdkEventService?.primitivePreventedFromCdkEvent( - this.popoverRoot, + !this.rootDirective.rdxCdkEventService?.primitivePreventedFromCdkEvent( + this.rootDirective, 'cdkOverlayOutsideClick' ) ), @@ -194,18 +194,18 @@ export class RdxPopoverContentDirective implements OnInit { */ filter((event) => { return ( - !this.popoverRoot.popoverAnchorDirective() || - !this.popoverRoot - .popoverTriggerDirective() + !this.rootDirective.anchorDirective() || + !this.rootDirective + .triggerDirective() .elementRef.nativeElement.contains(event.target as Element) ); }), tap((event) => { this.onOverlayOutsideClick.emit(event); }), - filter(() => !this.popoverRoot.firstDefaultOpen()), + filter(() => !this.rootDirective.firstDefaultOpen()), tap(() => { - this.popoverRoot.handleClose(); + this.rootDirective.handleClose(); }), takeUntilDestroyed(this.destroyRef) ) @@ -219,9 +219,9 @@ export class RdxPopoverContentDirective implements OnInit { .pipe( tap(() => { /** - * `this.onOpen.emit();` is being delegated to the root directive due to the opening animation + * `this.onOpen.emit();` is being delegated to the rootDirective directive due to the opening animation */ - this.popoverRoot.attachDetachEvent.set(RdxPopoverAttachDetachEvent.ATTACH); + this.rootDirective.attachDetachEvent.set(RdxTooltipAttachDetachEvent.ATTACH); }), takeUntilDestroyed(this.destroyRef) ) @@ -235,9 +235,9 @@ export class RdxPopoverContentDirective implements OnInit { .pipe( tap(() => { /** - * `this.onClosed.emit();` is being delegated to the root directive due to the closing animation + * `this.onClosed.emit();` is being delegated to the rootDirective directive due to the closing animation */ - this.popoverRoot.attachDetachEvent.set(RdxPopoverAttachDetachEvent.DETACH); + this.rootDirective.attachDetachEvent.set(RdxTooltipAttachDetachEvent.DETACH); }), takeUntilDestroyed(this.destroyRef) ) @@ -282,7 +282,7 @@ export class RdxPopoverContentDirective implements OnInit { /** @ignore */ private computePositions() { - const arrowHeight = this.popoverRoot.popoverArrowDirective()?.height() ?? 0; + const arrowHeight = this.rootDirective.arrowDirective()?.height() ?? 0; const offsets: RdxPositionSideAndAlignOffsets = { sideOffset: isNaN(this.sideOffset()) ? arrowHeight || RDX_POSITIONING_DEFAULTS.offsets.side @@ -323,7 +323,7 @@ export class RdxPopoverContentDirective implements OnInit { private onOriginChangeEffect() { effect(() => { - const origin = (this.popoverRoot.popoverAnchorDirective() ?? this.popoverRoot.popoverTriggerDirective()) + const origin = (this.rootDirective.anchorDirective() ?? this.rootDirective.triggerDirective()) .overlayOrigin; untracked(() => { this.setOrigin(origin); diff --git a/packages/primitives/tooltip-v2/src/tooltip-root.directive.ts b/packages/primitives/tooltip-v2/src/tooltip-root.directive.ts index 6766bd01..4ace22e9 100644 --- a/packages/primitives/tooltip-v2/src/tooltip-root.directive.ts +++ b/packages/primitives/tooltip-v2/src/tooltip-root.directive.ts @@ -13,45 +13,45 @@ import { untracked, ViewContainerRef } from '@angular/core'; -import { RdxPopoverAnchorDirective } from './tooltip-anchor.directive'; -import { RdxPopoverAnchorToken } from './tooltip-anchor.token'; -import { RdxPopoverArrowToken } from './tooltip-arrow.token'; -import { RdxPopoverCloseToken } from './tooltip-close.token'; -import { RdxPopoverContentAttributesToken } from './tooltip-content-attributes.token'; -import { RdxPopoverContentDirective } from './tooltip-content.directive'; -import { RdxPopoverTriggerDirective } from './tooltip-trigger.directive'; -import { RdxPopoverAnimationStatus, RdxPopoverAttachDetachEvent, RdxPopoverState } from './tooltip.types'; +import { RdxTooltipAnchorDirective } from './tooltip-anchor.directive'; +import { RdxTooltipAnchorToken } from './tooltip-anchor.token'; +import { RdxTooltipArrowToken } from './tooltip-arrow.token'; +import { RdxTooltipCloseToken } from './tooltip-close.token'; +import { RdxTooltipContentAttributesToken } from './tooltip-content-attributes.token'; +import { RdxTooltipContentDirective } from './tooltip-content.directive'; +import { RdxTooltipTriggerDirective } from './tooltip-trigger.directive'; +import { RdxTooltipAnimationStatus, RdxTooltipAttachDetachEvent, RdxTooltipState } from './tooltip.types'; import { injectRdxCdkEventService } from './utils/cdk-event.service'; let nextId = 0; @Directive({ - selector: '[rdxPopoverRoot]', - exportAs: 'rdxPopoverRoot' + selector: '[rdxTooltipRoot]', + exportAs: 'rdxTooltipRoot' }) -export class RdxPopoverRootDirective { +export class RdxTooltipRootDirective { /** @ignore */ readonly uniqueId = signal(++nextId); /** @ignore */ - readonly name = computed(() => `rdx-popover-root-${this.uniqueId()}`); + readonly name = computed(() => `rdx-tooltip-root-${this.uniqueId()}`); /** - * @description The anchor directive that comes form outside the popover root + * @description The anchor directive that comes form outside the tooltip rootDirective * @default undefined */ - readonly anchor = input(void 0); + readonly anchor = input(void 0); /** - * @description The open state of the popover when it is initially rendered. Use when you do not need to control its open state. + * @description The open state of the tooltip when it is initially rendered. Use when you do not need to control its open state. * @default false */ readonly defaultOpen = input(false, { transform: booleanAttribute }); /** - * @description The controlled state of the popover. `open` input take precedence of `defaultOpen` input. + * @description The controlled state of the tooltip. `open` input take precedence of `defaultOpen` input. * @default undefined */ readonly open = input(void 0, { transform: booleanAttribute }); /** - * @description Whether to control the state of the popover from external. Use in conjunction with `open` input. + * @description Whether to control the state of the tooltip from external. Use in conjunction with `open` input. * @default undefined */ readonly externalControl = input(void 0, { transform: booleanAttribute }); @@ -72,20 +72,20 @@ export class RdxPopoverRootDirective { readonly cssClosingAnimation = input(false, { transform: booleanAttribute }); /** @ignore */ - readonly cssAnimationStatus = signal(null); + readonly cssAnimationStatus = signal(null); /** @ignore */ - readonly popoverContentDirective = contentChild.required(RdxPopoverContentDirective); + readonly contentDirective = contentChild.required(RdxTooltipContentDirective); /** @ignore */ - readonly popoverTriggerDirective = contentChild.required(RdxPopoverTriggerDirective); + readonly triggerDirective = contentChild.required(RdxTooltipTriggerDirective); /** @ignore */ - readonly popoverArrowDirective = contentChild(RdxPopoverArrowToken); + readonly arrowDirective = contentChild(RdxTooltipArrowToken); /** @ignore */ - readonly popoverCloseDirective = contentChild(RdxPopoverCloseToken); + readonly closeDirective = contentChild(RdxTooltipCloseToken); /** @ignore */ - readonly popoverContentAttributesComponent = contentChild(RdxPopoverContentAttributesToken); + readonly contentAttributesComponent = contentChild(RdxTooltipContentAttributesToken); /** @ignore */ - private readonly internalPopoverAnchorDirective = contentChild(RdxPopoverAnchorToken); + private readonly internalAnchorDirective = contentChild(RdxTooltipAnchorToken); /** @ignore */ readonly viewContainerRef = inject(ViewContainerRef); @@ -95,16 +95,16 @@ export class RdxPopoverRootDirective { readonly destroyRef = inject(DestroyRef); /** @ignore */ - readonly state = signal(RdxPopoverState.CLOSED); + readonly state = signal(RdxTooltipState.CLOSED); /** @ignore */ - readonly attachDetachEvent = signal(RdxPopoverAttachDetachEvent.DETACH); + readonly attachDetachEvent = signal(RdxTooltipAttachDetachEvent.DETACH); /** @ignore */ private readonly isFirstDefaultOpen = signal(false); /** @ignore */ - readonly popoverAnchorDirective = computed(() => this.internalPopoverAnchorDirective() ?? this.anchor()); + readonly anchorDirective = computed(() => this.internalAnchorDirective() ?? this.anchor()); constructor() { this.rdxCdkEventService?.registerPrimitive(this); @@ -152,7 +152,7 @@ export class RdxPopoverRootDirective { if (this.externalControl()) { return; } - this.setState(RdxPopoverState.OPEN); + this.setState(RdxTooltipState.OPEN); } /** @ignore */ @@ -163,7 +163,7 @@ export class RdxPopoverRootDirective { if (this.externalControl()) { return; } - this.setState(RdxPopoverState.CLOSED); + this.setState(RdxTooltipState.CLOSED); } /** @ignore */ @@ -175,12 +175,12 @@ export class RdxPopoverRootDirective { } /** @ignore */ - isOpen(state?: RdxPopoverState) { - return (state ?? this.state()) === RdxPopoverState.OPEN; + isOpen(state?: RdxTooltipState) { + return (state ?? this.state()) === RdxTooltipState.OPEN; } /** @ignore */ - private setState(state = RdxPopoverState.CLOSED): void { + private setState(state = RdxTooltipState.CLOSED): void { if (state === this.state()) { return; } @@ -189,7 +189,7 @@ export class RdxPopoverRootDirective { /** @ignore */ private openContent(): void { - this.popoverContentDirective().open(); + this.contentDirective().open(); if (!this.cssAnimation() || !this.cssOpeningAnimation()) { this.cssAnimationStatus.set(null); } @@ -197,7 +197,7 @@ export class RdxPopoverRootDirective { /** @ignore */ private closeContent(): void { - this.popoverContentDirective().close(); + this.contentDirective().close(); if (!this.cssAnimation() || !this.cssClosingAnimation()) { this.cssAnimationStatus.set(null); } @@ -205,71 +205,71 @@ export class RdxPopoverRootDirective { /** @ignore */ private emitOnOpen(): void { - this.popoverContentDirective().onOpen.emit(); + this.contentDirective().onOpen.emit(); } /** @ignore */ private emitOnClosed(): void { - this.popoverContentDirective().onClosed.emit(); + this.contentDirective().onClosed.emit(); } /** @ignore */ - private ifOpenOrCloseWithoutAnimations(state: RdxPopoverState) { + private ifOpenOrCloseWithoutAnimations(state: RdxTooltipState) { return ( - !this.popoverContentAttributesComponent() || + !this.contentAttributesComponent() || !this.cssAnimation() || - (this.cssAnimation() && !this.cssClosingAnimation() && state === RdxPopoverState.CLOSED) || - (this.cssAnimation() && !this.cssOpeningAnimation() && state === RdxPopoverState.OPEN) || + (this.cssAnimation() && !this.cssClosingAnimation() && state === RdxTooltipState.CLOSED) || + (this.cssAnimation() && !this.cssOpeningAnimation() && state === RdxTooltipState.OPEN) || // !this.cssAnimationStatus() || (this.cssOpeningAnimation() && - state === RdxPopoverState.OPEN && - [RdxPopoverAnimationStatus.OPEN_STARTED].includes(this.cssAnimationStatus()!)) || + state === RdxTooltipState.OPEN && + [RdxTooltipAnimationStatus.OPEN_STARTED].includes(this.cssAnimationStatus()!)) || (this.cssClosingAnimation() && - state === RdxPopoverState.CLOSED && - [RdxPopoverAnimationStatus.CLOSED_STARTED].includes(this.cssAnimationStatus()!)) + state === RdxTooltipState.CLOSED && + [RdxTooltipAnimationStatus.CLOSED_STARTED].includes(this.cssAnimationStatus()!)) ); } /** @ignore */ - private ifOpenOrCloseWithAnimations(cssAnimationStatus: RdxPopoverAnimationStatus | null) { + private ifOpenOrCloseWithAnimations(cssAnimationStatus: RdxTooltipAnimationStatus | null) { return ( - this.popoverContentAttributesComponent() && + this.contentAttributesComponent() && this.cssAnimation() && cssAnimationStatus && ((this.cssOpeningAnimation() && - this.state() === RdxPopoverState.OPEN && - [RdxPopoverAnimationStatus.OPEN_ENDED].includes(cssAnimationStatus)) || + this.state() === RdxTooltipState.OPEN && + [RdxTooltipAnimationStatus.OPEN_ENDED].includes(cssAnimationStatus)) || (this.cssClosingAnimation() && - this.state() === RdxPopoverState.CLOSED && - [RdxPopoverAnimationStatus.CLOSED_ENDED].includes(cssAnimationStatus))) + this.state() === RdxTooltipState.CLOSED && + [RdxTooltipAnimationStatus.CLOSED_ENDED].includes(cssAnimationStatus))) ); } /** @ignore */ - private openOrClose(state: RdxPopoverState) { + private openOrClose(state: RdxTooltipState) { const isOpen = this.isOpen(state); isOpen ? this.openContent() : this.closeContent(); } /** @ignore */ - private emitOnOpenOrOnClosed(state: RdxPopoverState) { + private emitOnOpenOrOnClosed(state: RdxTooltipState) { this.isOpen(state) - ? this.attachDetachEvent() === RdxPopoverAttachDetachEvent.ATTACH && this.emitOnOpen() - : this.attachDetachEvent() === RdxPopoverAttachDetachEvent.DETACH && this.emitOnClosed(); + ? this.attachDetachEvent() === RdxTooltipAttachDetachEvent.ATTACH && this.emitOnOpen() + : this.attachDetachEvent() === RdxTooltipAttachDetachEvent.DETACH && this.emitOnClosed(); } /** @ignore */ private canEmitOnOpenOrOnClosed() { return ( !this.cssAnimation() || - (!this.cssOpeningAnimation() && this.state() === RdxPopoverState.OPEN) || + (!this.cssOpeningAnimation() && this.state() === RdxTooltipState.OPEN) || (this.cssOpeningAnimation() && - this.state() === RdxPopoverState.OPEN && - this.cssAnimationStatus() === RdxPopoverAnimationStatus.OPEN_ENDED) || - (!this.cssClosingAnimation() && this.state() === RdxPopoverState.CLOSED) || + this.state() === RdxTooltipState.OPEN && + this.cssAnimationStatus() === RdxTooltipAnimationStatus.OPEN_ENDED) || + (!this.cssClosingAnimation() && this.state() === RdxTooltipState.CLOSED) || (this.cssClosingAnimation() && - this.state() === RdxPopoverState.CLOSED && - this.cssAnimationStatus() === RdxPopoverAnimationStatus.CLOSED_ENDED) + this.state() === RdxTooltipState.CLOSED && + this.cssAnimationStatus() === RdxTooltipAnimationStatus.CLOSED_ENDED) ); } @@ -334,7 +334,7 @@ export class RdxPopoverRootDirective { effect(() => { const open = this.open(); untracked(() => { - this.setState(open ? RdxPopoverState.OPEN : RdxPopoverState.CLOSED); + this.setState(open ? RdxTooltipState.OPEN : RdxTooltipState.CLOSED); }); }); } @@ -359,7 +359,7 @@ export class RdxPopoverRootDirective { const anchor = this.anchor(); untracked(() => { if (anchor) { - anchor.setPopoverRoot(this); + anchor.setRoot(this); } }); }); diff --git a/packages/primitives/tooltip-v2/src/tooltip-root.inject.ts b/packages/primitives/tooltip-v2/src/tooltip-root.inject.ts index 79291478..721ed42a 100644 --- a/packages/primitives/tooltip-v2/src/tooltip-root.inject.ts +++ b/packages/primitives/tooltip-v2/src/tooltip-root.inject.ts @@ -1,9 +1,9 @@ import { assertInInjectionContext, inject, isDevMode } from '@angular/core'; -import { RdxPopoverRootDirective } from './tooltip-root.directive'; +import { RdxTooltipRootDirective } from './tooltip-root.directive'; -export function injectPopoverRoot(optional?: false): RdxPopoverRootDirective; -export function injectPopoverRoot(optional: true): RdxPopoverRootDirective | null; -export function injectPopoverRoot(optional = false): RdxPopoverRootDirective | null { - isDevMode() && assertInInjectionContext(injectPopoverRoot); - return inject(RdxPopoverRootDirective, { optional }); +export function injectTooltipRoot(optional?: false): RdxTooltipRootDirective; +export function injectTooltipRoot(optional: true): RdxTooltipRootDirective | null; +export function injectTooltipRoot(optional = false): RdxTooltipRootDirective | null { + isDevMode() && assertInInjectionContext(injectTooltipRoot); + return inject(RdxTooltipRootDirective, { optional }); } diff --git a/packages/primitives/tooltip-v2/src/tooltip-trigger.directive.ts b/packages/primitives/tooltip-v2/src/tooltip-trigger.directive.ts index e8fa639f..505da3bc 100644 --- a/packages/primitives/tooltip-v2/src/tooltip-trigger.directive.ts +++ b/packages/primitives/tooltip-v2/src/tooltip-trigger.directive.ts @@ -1,33 +1,33 @@ import { CdkOverlayOrigin } from '@angular/cdk/overlay'; import { computed, Directive, ElementRef, inject } from '@angular/core'; -import { injectPopoverRoot } from './tooltip-root.inject'; +import { injectTooltipRoot } from './tooltip-root.inject'; @Directive({ - selector: '[rdxPopoverTrigger]', + selector: '[rdxTooltipTrigger]', hostDirectives: [CdkOverlayOrigin], host: { type: 'button', '[attr.id]': 'name()', '[attr.aria-haspopup]': '"dialog"', - '[attr.aria-expanded]': 'popoverRoot.isOpen()', - '[attr.aria-controls]': 'popoverRoot.popoverContentDirective().name()', - '[attr.data-state]': 'popoverRoot.state()', + '[attr.aria-expanded]': 'rootDirective.isOpen()', + '[attr.aria-controls]': 'rootDirective.contentDirective().name()', + '[attr.data-state]': 'rootDirective.state()', '(click)': 'click()' } }) -export class RdxPopoverTriggerDirective { +export class RdxTooltipTriggerDirective { /** @ignore */ - protected readonly popoverRoot = injectPopoverRoot(); + protected readonly rootDirective = injectTooltipRoot(); /** @ignore */ readonly elementRef = inject>(ElementRef); /** @ignore */ readonly overlayOrigin = inject(CdkOverlayOrigin); /** @ignore */ - readonly name = computed(() => `rdx-popover-trigger-${this.popoverRoot.uniqueId()}`); + readonly name = computed(() => `rdx-tooltip-trigger-${this.rootDirective.uniqueId()}`); /** @ignore */ click(): void { - this.popoverRoot.handleToggle(); + this.rootDirective.handleToggle(); } } diff --git a/packages/primitives/tooltip-v2/src/tooltip.types.ts b/packages/primitives/tooltip-v2/src/tooltip.types.ts index ccf53f28..e6c25169 100644 --- a/packages/primitives/tooltip-v2/src/tooltip.types.ts +++ b/packages/primitives/tooltip-v2/src/tooltip.types.ts @@ -1,14 +1,14 @@ -export enum RdxPopoverState { +export enum RdxTooltipState { OPEN = 'open', CLOSED = 'closed' } -export enum RdxPopoverAttachDetachEvent { +export enum RdxTooltipAttachDetachEvent { ATTACH = 'attach', DETACH = 'detach' } -export enum RdxPopoverAnimationStatus { +export enum RdxTooltipAnimationStatus { OPEN_STARTED = 'open_started', OPEN_ENDED = 'open_ended', CLOSED_STARTED = 'closed_started', diff --git a/packages/primitives/tooltip-v2/stories/tooltip-anchor.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-anchor.component.ts index a2d254c5..ab675530 100644 --- a/packages/primitives/tooltip-v2/stories/tooltip-anchor.component.ts +++ b/packages/primitives/tooltip-v2/stories/tooltip-anchor.component.ts @@ -1,9 +1,9 @@ import { Component, viewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { LucideAngularModule, MapPin, MapPinPlus, MountainSnow, TriangleAlert, X } from 'lucide-angular'; -import { RdxPopoverModule } from '../index'; -import { RdxPopoverAnchorDirective } from '../src/tooltip-anchor.directive'; -import { RdxPopoverContentAttributesComponent } from '../src/tooltip-content-attributes.component'; +import { RdxTooltipModule } from '../index'; +import { RdxTooltipAnchorDirective } from '../src/tooltip-anchor.directive'; +import { RdxTooltipContentAttributesComponent } from '../src/tooltip-content-attributes.component'; import { provideRdxCdkEventService } from '../src/utils/cdk-event.service'; import { containerAlert } from './utils/constants'; import { OptionPanelBase } from './utils/option-panel-base.class'; @@ -11,20 +11,20 @@ import styles from './utils/styles.constants'; import { WithOptionPanelComponent } from './utils/with-option-panel.component'; @Component({ - selector: 'rdx-popover-anchor', + selector: 'rdx-tooltip-anchor', providers: [provideRdxCdkEventService()], imports: [ FormsModule, - RdxPopoverModule, + RdxTooltipModule, LucideAngularModule, - RdxPopoverContentAttributesComponent, + RdxTooltipContentAttributesComponent, WithOptionPanelComponent, - RdxPopoverAnchorDirective + RdxTooltipAnchorDirective ], styles: styles(), template: ` -

Internal Anchor (within PopoverRoot)

- Internal Anchor (within TooltipRoot)

+
- - - -
-
@@ -75,20 +75,20 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
-
ID: {{ popoverRootDirective1()?.uniqueId() }}
-
+
ID: {{ rootDirective1()?.uniqueId() }}
+ -

External Anchor (outside PopoverRoot)

- External Anchor (outside TooltipRoot)

+
- - - -
-
@@ -139,22 +139,22 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
-
ID: {{ popoverRootDirective2()?.uniqueId() }}
-
+
ID: {{ rootDirective2()?.uniqueId() }}
+ ` }) -export class RdxPopoverAnchorComponent extends OptionPanelBase { - readonly popoverRootDirective1 = viewChild('root1'); - readonly popoverRootDirective2 = viewChild('root2'); +export class RdxTooltipAnchorComponent extends OptionPanelBase { + readonly rootDirective1 = viewChild('root1'); + readonly rootDirective2 = viewChild('root2'); readonly MountainSnowIcon = MountainSnow; readonly XIcon = X; diff --git a/packages/primitives/tooltip-v2/stories/tooltip-animations.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-animations.component.ts index 1ab82f10..7553f565 100644 --- a/packages/primitives/tooltip-v2/stories/tooltip-animations.component.ts +++ b/packages/primitives/tooltip-v2/stories/tooltip-animations.component.ts @@ -2,8 +2,8 @@ import { Component, signal, viewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { RdxPositionAlign, RdxPositionSide } from '@radix-ng/primitives/core'; import { LucideAngularModule, MountainSnow, TriangleAlert, X } from 'lucide-angular'; -import { RdxPopoverModule, RdxPopoverRootDirective } from '../index'; -import { RdxPopoverContentAttributesComponent } from '../src/tooltip-content-attributes.component'; +import { RdxTooltipModule, RdxTooltipRootDirective } from '../index'; +import { RdxTooltipContentAttributesComponent } from '../src/tooltip-content-attributes.component'; import { provideRdxCdkEventService } from '../src/utils/cdk-event.service'; import { containerAlert } from './utils/constants'; import { OptionPanelBase } from './utils/option-panel-base.class'; @@ -11,18 +11,18 @@ import styles from './utils/styles.constants'; import { WithOptionPanelComponent } from './utils/with-option-panel.component'; @Component({ - selector: 'rdx-popover-animations', + selector: 'rdx-tooltip-animations', providers: [provideRdxCdkEventService()], imports: [ FormsModule, - RdxPopoverModule, + RdxTooltipModule, LucideAngularModule, - RdxPopoverContentAttributesComponent, + RdxTooltipContentAttributesComponent, WithOptionPanelComponent ], styles: styles(true), template: ` - - -
-
@@ -91,21 +91,21 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
-
ID: {{ popoverRootDirective()?.uniqueId() }}
-
+
ID: {{ rootDirective()?.uniqueId() }}
+ ` }) -export class RdxPopoverAnimationsComponent extends OptionPanelBase { - readonly popoverRootDirective = viewChild(RdxPopoverRootDirective); +export class RdxTooltipAnimationsComponent extends OptionPanelBase { + readonly rootDirective = viewChild(RdxTooltipRootDirective); readonly MountainSnowIcon = MountainSnow; readonly XIcon = X; diff --git a/packages/primitives/tooltip-v2/stories/tooltip-default.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-default.component.ts index ddb6e247..958025e2 100644 --- a/packages/primitives/tooltip-v2/stories/tooltip-default.component.ts +++ b/packages/primitives/tooltip-v2/stories/tooltip-default.component.ts @@ -1,8 +1,8 @@ import { Component, viewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { LucideAngularModule, MountainSnow, TriangleAlert, X } from 'lucide-angular'; -import { RdxPopoverModule, RdxPopoverRootDirective } from '../index'; -import { RdxPopoverContentAttributesComponent } from '../src/tooltip-content-attributes.component'; +import { RdxTooltipModule, RdxTooltipRootDirective } from '../index'; +import { RdxTooltipContentAttributesComponent } from '../src/tooltip-content-attributes.component'; import { provideRdxCdkEventService } from '../src/utils/cdk-event.service'; import { containerAlert } from './utils/constants'; import { OptionPanelBase } from './utils/option-panel-base.class'; @@ -10,18 +10,18 @@ import styles from './utils/styles.constants'; import { WithOptionPanelComponent } from './utils/with-option-panel.component'; @Component({ - selector: 'rdx-popover-default', + selector: 'rdx-tooltip-default', providers: [provideRdxCdkEventService()], imports: [ FormsModule, - RdxPopoverModule, + RdxTooltipModule, LucideAngularModule, - RdxPopoverContentAttributesComponent, + RdxTooltipContentAttributesComponent, WithOptionPanelComponent ], styles: styles(), template: ` -
- - -
-
@@ -68,21 +68,21 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
-
ID: {{ popoverRootDirective()?.uniqueId() }}
-
+
ID: {{ rootDirective()?.uniqueId() }}
+ ` }) -export class RdxPopoverDefaultComponent extends OptionPanelBase { - readonly popoverRootDirective = viewChild(RdxPopoverRootDirective); +export class RdxTooltipDefaultComponent extends OptionPanelBase { + readonly rootDirective = viewChild(RdxTooltipRootDirective); readonly MountainSnowIcon = MountainSnow; readonly XIcon = X; diff --git a/packages/primitives/tooltip-v2/stories/tooltip-events.components.ts b/packages/primitives/tooltip-v2/stories/tooltip-events.components.ts index e02cbff8..90886eaa 100644 --- a/packages/primitives/tooltip-v2/stories/tooltip-events.components.ts +++ b/packages/primitives/tooltip-v2/stories/tooltip-events.components.ts @@ -2,8 +2,8 @@ import { Component, viewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { RdxPositionAlign, RdxPositionSide } from '@radix-ng/primitives/core'; import { LucideAngularModule, MountainSnow, TriangleAlert, X } from 'lucide-angular'; -import { RdxPopoverModule, RdxPopoverRootDirective } from '../index'; -import { RdxPopoverContentAttributesComponent } from '../src/tooltip-content-attributes.component'; +import { RdxTooltipModule, RdxTooltipRootDirective } from '../index'; +import { RdxTooltipContentAttributesComponent } from '../src/tooltip-content-attributes.component'; import { provideRdxCdkEventService } from '../src/utils/cdk-event.service'; import { containerAlert } from './utils/constants'; import { OptionPanelBase } from './utils/option-panel-base.class'; @@ -11,20 +11,20 @@ import styles from './utils/styles.constants'; import { WithOptionPanelComponent } from './utils/with-option-panel.component'; @Component({ - selector: 'rdx-popover-events', + selector: 'rdx-tooltip-events', providers: [provideRdxCdkEventService()], imports: [ - RdxPopoverModule, + RdxTooltipModule, LucideAngularModule, FormsModule, - RdxPopoverContentAttributesComponent, + RdxTooltipContentAttributesComponent, WithOptionPanelComponent ], styles: ` ${styles()} `, template: ` -
- - @@ -46,10 +46,10 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component'; [onOverlayEscapeKeyDownDisabled]="onOverlayEscapeKeyDownDisabled()" [onOverlayOutsideClickDisabled]="onOverlayOutsideClickDisabled()" [sideOffset]="8" - rdxPopoverContent + rdxTooltipContent > -
-
@@ -72,21 +72,21 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
-
ID: {{ popoverRootDirective()?.uniqueId() }}
-
+
ID: {{ rootDirective()?.uniqueId() }}
+ ` }) -export class RdxPopoverEventsComponent extends OptionPanelBase { - readonly popoverRootDirective = viewChild(RdxPopoverRootDirective); +export class RdxTooltipEventsComponent extends OptionPanelBase { + readonly rootDirective = viewChild(RdxTooltipRootDirective); readonly MountainSnowIcon = MountainSnow; readonly XIcon = X; diff --git a/packages/primitives/tooltip-v2/stories/tooltip-initially-open.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-initially-open.component.ts index c11e9afc..78469a06 100644 --- a/packages/primitives/tooltip-v2/stories/tooltip-initially-open.component.ts +++ b/packages/primitives/tooltip-v2/stories/tooltip-initially-open.component.ts @@ -1,8 +1,8 @@ import { Component, viewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { LucideAngularModule, MountainSnow, TriangleAlert, X } from 'lucide-angular'; -import { RdxPopoverModule, RdxPopoverRootDirective } from '../index'; -import { RdxPopoverContentAttributesComponent } from '../src/tooltip-content-attributes.component'; +import { RdxTooltipModule, RdxTooltipRootDirective } from '../index'; +import { RdxTooltipContentAttributesComponent } from '../src/tooltip-content-attributes.component'; import { provideRdxCdkEventService } from '../src/utils/cdk-event.service'; import { containerAlert } from './utils/constants'; import { OptionPanelBase } from './utils/option-panel-base.class'; @@ -10,18 +10,18 @@ import styles from './utils/styles.constants'; import { WithOptionPanelComponent } from './utils/with-option-panel.component'; @Component({ - selector: 'rdx-popover-initially-open', + selector: 'rdx-tooltip-initially-open', providers: [provideRdxCdkEventService()], imports: [ FormsModule, - RdxPopoverModule, + RdxTooltipModule, LucideAngularModule, - RdxPopoverContentAttributesComponent, + RdxTooltipContentAttributesComponent, WithOptionPanelComponent ], styles: styles(), template: ` -
- - @@ -43,10 +43,10 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component'; [onOverlayEscapeKeyDownDisabled]="onOverlayEscapeKeyDownDisabled()" [onOverlayOutsideClickDisabled]="onOverlayOutsideClickDisabled()" [sideOffset]="8" - rdxPopoverContent + rdxTooltipContent > -
-
@@ -69,21 +69,21 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
-
ID: {{ popoverRootDirective()?.uniqueId() }}
-
+
ID: {{ rootDirective()?.uniqueId() }}
+ ` }) -export class RdxPopoverInitiallyOpenComponent extends OptionPanelBase { - readonly popoverRootDirective = viewChild(RdxPopoverRootDirective); +export class RdxTooltipInitiallyOpenComponent extends OptionPanelBase { + readonly rootDirective = viewChild(RdxTooltipRootDirective); readonly MountainSnowIcon = MountainSnow; readonly XIcon = X; diff --git a/packages/primitives/tooltip-v2/stories/tooltip-multiple.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-multiple.component.ts index 8921f114..09887d53 100644 --- a/packages/primitives/tooltip-v2/stories/tooltip-multiple.component.ts +++ b/packages/primitives/tooltip-v2/stories/tooltip-multiple.component.ts @@ -2,8 +2,8 @@ import { Component, viewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { RdxPositionAlign, RdxPositionSide } from '@radix-ng/primitives/core'; import { LucideAngularModule, MountainSnow, TriangleAlert, X } from 'lucide-angular'; -import { RdxPopoverModule } from '../index'; -import { RdxPopoverContentAttributesComponent } from '../src/tooltip-content-attributes.component'; +import { RdxTooltipModule } from '../index'; +import { RdxTooltipContentAttributesComponent } from '../src/tooltip-content-attributes.component'; import { provideRdxCdkEventService } from '../src/utils/cdk-event.service'; import { containerAlert } from './utils/constants'; import { OptionPanelBase } from './utils/option-panel-base.class'; @@ -11,18 +11,18 @@ import styles from './utils/styles.constants'; import { WithOptionPanelComponent } from './utils/with-option-panel.component'; @Component({ - selector: 'rdx-popover-multiple', + selector: 'rdx-tooltip-multiple', providers: [provideRdxCdkEventService()], imports: [ FormsModule, - RdxPopoverModule, + RdxTooltipModule, LucideAngularModule, - RdxPopoverContentAttributesComponent, + RdxTooltipContentAttributesComponent, WithOptionPanelComponent ], styles: styles(), template: ` -
- - -
-
@@ -69,19 +69,19 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
-
ID: {{ popoverRootDirective1()?.uniqueId() }}
-
+
ID: {{ rootDirective1()?.uniqueId() }}
+ -
- - -
-
@@ -132,19 +132,19 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
-
ID: {{ popoverRootDirective2()?.uniqueId() }}
-
+
ID: {{ rootDirective2()?.uniqueId() }}
+ -
- - -
-
@@ -195,28 +195,28 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
-
ID: {{ popoverRootDirective3()?.uniqueId() }}
-
+
ID: {{ rootDirective3()?.uniqueId() }}
+ ` }) -export class RdxPopoverMultipleComponent extends OptionPanelBase { - readonly popoverRootDirective1 = viewChild('root1'); - readonly popoverRootDirective2 = viewChild('root2'); - readonly popoverRootDirective3 = viewChild('root3'); +export class RdxTooltipMultipleComponent extends OptionPanelBase { + readonly rootDirective1 = viewChild('root1'); + readonly rootDirective2 = viewChild('root2'); + readonly rootDirective3 = viewChild('root3'); readonly MountainSnowIcon = MountainSnow; readonly XIcon = X; - readonly RdxPopoverSide = RdxPositionSide; - readonly RdxPopoverAlign = RdxPositionAlign; + readonly RdxPositionSide = RdxPositionSide; + readonly RdxPositionAlign = RdxPositionAlign; protected readonly containerAlert = containerAlert; protected readonly TriangleAlert = TriangleAlert; } diff --git a/packages/primitives/tooltip-v2/stories/tooltip-positioning.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-positioning.component.ts index 52b0c08a..7d4e2f73 100644 --- a/packages/primitives/tooltip-v2/stories/tooltip-positioning.component.ts +++ b/packages/primitives/tooltip-v2/stories/tooltip-positioning.component.ts @@ -2,8 +2,8 @@ import { Component, signal, viewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { RdxPositionAlign, RdxPositionSide } from '@radix-ng/primitives/core'; import { LucideAngularModule, MountainSnow, TriangleAlert, X } from 'lucide-angular'; -import { RdxPopoverModule, RdxPopoverRootDirective } from '../index'; -import { RdxPopoverContentAttributesComponent } from '../src/tooltip-content-attributes.component'; +import { RdxTooltipModule, RdxTooltipRootDirective } from '../index'; +import { RdxTooltipContentAttributesComponent } from '../src/tooltip-content-attributes.component'; import { provideRdxCdkEventService } from '../src/utils/cdk-event.service'; import { containerAlert } from './utils/constants'; import { OptionPanelBase } from './utils/option-panel-base.class'; @@ -11,18 +11,18 @@ import styles from './utils/styles.constants'; import { WithOptionPanelComponent } from './utils/with-option-panel.component'; @Component({ - selector: 'rdx-popover-positioning', + selector: 'rdx-tooltip-positioning', providers: [provideRdxCdkEventService()], imports: [ FormsModule, - RdxPopoverModule, + RdxTooltipModule, LucideAngularModule, - RdxPopoverContentAttributesComponent, + RdxTooltipContentAttributesComponent, WithOptionPanelComponent ], styles: styles(), template: ` - - Disable alternate positions (to see the result, scroll the page to make the popover cross the viewport + Disable alternate positions (to see the result, scroll the page to make the tooltip cross the viewport boundary) @@ -65,8 +65,8 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component'; {{ containerAlert }}
- - @@ -78,10 +78,10 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component'; [alternatePositionsDisabled]="disableAlternatePositions()" [onOverlayEscapeKeyDownDisabled]="onOverlayEscapeKeyDownDisabled()" [onOverlayOutsideClickDisabled]="onOverlayOutsideClickDisabled()" - rdxPopoverContent + rdxTooltipContent > -
-
@@ -104,21 +104,21 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
-
ID: {{ popoverRootDirective()?.uniqueId() }}
-
+
ID: {{ rootDirective()?.uniqueId() }}
+ ` }) -export class RdxPopoverPositioningComponent extends OptionPanelBase { - readonly popoverRootDirective = viewChild(RdxPopoverRootDirective); +export class RdxTooltipPositioningComponent extends OptionPanelBase { + readonly rootDirective = viewChild(RdxTooltipRootDirective); readonly selectedSide = signal(RdxPositionSide.Top); readonly selectedAlign = signal(RdxPositionAlign.Center); diff --git a/packages/primitives/tooltip-v2/stories/tooltip-triggering.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-triggering.component.ts index b2b5fab1..c317fcf4 100644 --- a/packages/primitives/tooltip-v2/stories/tooltip-triggering.component.ts +++ b/packages/primitives/tooltip-v2/stories/tooltip-triggering.component.ts @@ -1,8 +1,8 @@ import { Component, signal, viewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { LucideAngularModule, MountainSnow, TriangleAlert, X } from 'lucide-angular'; -import { RdxPopoverModule } from '../index'; -import { RdxPopoverContentAttributesComponent } from '../src/tooltip-content-attributes.component'; +import { RdxTooltipModule } from '../index'; +import { RdxTooltipContentAttributesComponent } from '../src/tooltip-content-attributes.component'; import { provideRdxCdkEventService } from '../src/utils/cdk-event.service'; import { containerAlert } from './utils/constants'; import { OptionPanelBase } from './utils/option-panel-base.class'; @@ -10,19 +10,19 @@ import styles from './utils/styles.constants'; import { WithOptionPanelComponent } from './utils/with-option-panel.component'; @Component({ - selector: 'rdx-popover-triggering', + selector: 'rdx-tooltip-triggering', providers: [provideRdxCdkEventService()], imports: [ FormsModule, - RdxPopoverModule, + RdxTooltipModule, LucideAngularModule, - RdxPopoverContentAttributesComponent, + RdxTooltipContentAttributesComponent, WithOptionPanelComponent ], styles: styles(), template: `

Initially closed

-
- @@ -65,10 +65,10 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component'; [onOverlayOutsideClickDisabled]="onOverlayOutsideClickDisabled()" (onOpen)="countOpenFalse(true)" (onClosed)="countOpenFalse(false)" - rdxPopoverContent + rdxTooltipContent > -
-
@@ -91,20 +91,20 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
-
ID: {{ popoverRootDirective1()?.uniqueId() }}
-
+
ID: {{ rootDirective1()?.uniqueId() }}
+

Initially open

-
- @@ -147,10 +147,10 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component'; [onOverlayOutsideClickDisabled]="onOverlayOutsideClickDisabled()" (onOpen)="countOpenTrue(true)" (onClosed)="countOpenTrue(false)" - rdxPopoverContent + rdxTooltipContent > -
-
@@ -173,22 +173,22 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
-
ID: {{ popoverRootDirective2()?.uniqueId() }}
-
+
ID: {{ rootDirective2()?.uniqueId() }}
+ ` }) -export class RdxPopoverTriggeringComponent extends OptionPanelBase { - readonly popoverRootDirective1 = viewChild('root1'); - readonly popoverRootDirective2 = viewChild('root2'); +export class RdxTooltipTriggeringComponent extends OptionPanelBase { + readonly rootDirective1 = viewChild('root1'); + readonly rootDirective2 = viewChild('root2'); readonly MountainSnowIcon = MountainSnow; readonly XIcon = X; diff --git a/packages/primitives/tooltip-v2/stories/tooltip.docs.mdx b/packages/primitives/tooltip-v2/stories/tooltip.docs.mdx index c8c6d465..2a974a40 100644 --- a/packages/primitives/tooltip-v2/stories/tooltip.docs.mdx +++ b/packages/primitives/tooltip-v2/stories/tooltip.docs.mdx @@ -1,52 +1,52 @@ import {ArgTypes, Canvas, Markdown, Meta, Source} from '@storybook/blocks'; -import * as PopoverStories from "./tooltip.stories"; -import {RdxPopoverRootDirective} from "../src/tooltip-root.directive"; -import {RdxPopoverContentDirective} from "../src/tooltip-content.directive"; -import {RdxPopoverArrowDirective} from "../src/tooltip-arrow.directive"; -import {RdxPopoverAnchorDirective} from "../src/tooltip-anchor.directive"; -import {RdxPopoverTriggerDirective} from "../src/tooltip-trigger.directive"; -import {RdxPopoverCloseDirective} from "../src/tooltip-close.directive"; +import * as TooltipStories from "./tooltip.stories"; +import {RdxTooltipRootDirective} from "../src/tooltip-root.directive"; +import {RdxTooltipContentDirective} from "../src/tooltip-content.directive"; +import {RdxTooltipArrowDirective} from "../src/tooltip-arrow.directive"; +import {RdxTooltipAnchorDirective} from "../src/tooltip-anchor.directive"; +import {RdxTooltipTriggerDirective} from "../src/tooltip-trigger.directive"; +import {RdxTooltipCloseDirective} from "../src/tooltip-close.directive"; import {animationStylesOnly} from "./utils/styles.constants"; -import {RdxPopoverContentAttributesComponent} from "../src/tooltip-content-attributes.component"; +import {RdxTooltipContentAttributesComponent} from "../src/tooltip-content-attributes.component"; - + -# Popover +# Tooltip-v2 #### A popup that displays information ready to interact with related to an element when the element receives click on it. ## Examples ### Default - + ### Multiple - + ### Events - + ### Positioning - + ### External Triggering - + ### Anchor - + ### Initially Open - + ### Animations - + ### Animation Styles @@ -62,14 +62,14 @@ import {RdxPopoverContentAttributesComponent} from "../src/tooltip-content-attri ## Anatomy ```html - - + + - -
- + +
+ Add to library -
+
@@ -80,77 +80,77 @@ import {RdxPopoverContentAttributesComponent} from "../src/tooltip-content-attri Get started with importing the directives: + RdxTooltipRootDirective, + RdxTooltipRootTrigger, + RdxTooltipContentDirective, + RdxTooltipArrowDirective, + RdxTooltipAnchorDirective, + RdxTooltipCloseDirective +} from '@radix-ng/primitives/tooltip-v2';`} /> or - + ## API Reference ### Root -`RdxPopoverRootDirective` +`RdxTooltipRootDirective` -Contains all the parts of a popover. +Contains all the parts of a tooltip. - + ### Trigger -`RdxPopoverTriggerDirective` +`RdxTooltipTriggerDirective` -The button that toggles the popover. By default, the PopoverContent will position itself against the trigger. +The button that toggles the tooltip. By default, the TooltipContent will position itself against the trigger. {` | Data attribute | Value | |----------------|----------------| -| [data-state] | "closed" | "open" (type RdxPopoverState) +| [data-state] | "closed" | "open" (type RdxTooltipState) `} ### Anchor -`RdxPopoverAnchorDirective` +`RdxTooltipAnchorDirective` -An optional element to position the PopoverContent against. If this part is not used, the content will position alongside the PopoverTrigger. +An optional element to position the TooltipContent against. If this part is not used, the content will position alongside the TooltipTrigger. ### Content -`RdxPopoverContentDirective` +`RdxTooltipContentDirective` -The component that pops out when the popover is open. +The component that pops out when the tooltip is open. - + ### Content Attributes -`RdxPopoverContentAttributesComponent` +`RdxTooltipContentAttributesComponent` A component with the content attributes that are necessary to run animations. - + {` | Data attribute | Value | |----------------|----------------| -| [data-state] | "closed" | "open" (enum RdxPopoverState) +| [data-state] | "closed" | "open" (enum RdxTooltipState) | [data-side] | "left" | "right" | "bottom" | "top" (enum RdxPositionSide) | [data-align] | "start" | "end" | "center" (enum RdxPositionAlign) `} ### Arrow -`RdxPopoverArrowDirective` +`RdxTooltipArrowDirective` -An optional arrow element to render alongside the popover. This can be used to help visually link the trigger with the PopoverContent. Must be rendered inside PopoverContent. +An optional arrow element to render alongside the tooltip. This can be used to help visually link the trigger with the TooltipContent. Must be rendered inside TooltipContent. - + ### Close -`RdxPopoverCloseDirective` +`RdxTooltipCloseDirective` -An optional close button element to render alongside the popover. This can be used to close the PopoverContent. Must be rendered inside PopoverContent. +An optional close button element to render alongside the tooltip. This can be used to close the TooltipContent. Must be rendered inside TooltipContent. diff --git a/packages/primitives/tooltip-v2/stories/tooltip.stories.ts b/packages/primitives/tooltip-v2/stories/tooltip.stories.ts index bb6eafc5..424f04e4 100644 --- a/packages/primitives/tooltip-v2/stories/tooltip.stories.ts +++ b/packages/primitives/tooltip-v2/stories/tooltip.stories.ts @@ -1,15 +1,15 @@ import { provideAnimations } from '@angular/platform-browser/animations'; import { componentWrapperDecorator, Meta, moduleMetadata, StoryObj } from '@storybook/angular'; import { LucideAngularModule, MountainSnow, X } from 'lucide-angular'; -import { RdxPopoverModule } from '../index'; -import { RdxPopoverAnchorComponent } from './tooltip-anchor.component'; -import { RdxPopoverAnimationsComponent } from './tooltip-animations.component'; -import { RdxPopoverDefaultComponent } from './tooltip-default.component'; -import { RdxPopoverEventsComponent } from './tooltip-events.components'; -import { RdxPopoverInitiallyOpenComponent } from './tooltip-initially-open.component'; -import { RdxPopoverMultipleComponent } from './tooltip-multiple.component'; -import { RdxPopoverPositioningComponent } from './tooltip-positioning.component'; -import { RdxPopoverTriggeringComponent } from './tooltip-triggering.component'; +import { RdxTooltipModule } from '../index'; +import { RdxTooltipAnchorComponent } from './tooltip-anchor.component'; +import { RdxTooltipAnimationsComponent } from './tooltip-animations.component'; +import { RdxTooltipDefaultComponent } from './tooltip-default.component'; +import { RdxTooltipEventsComponent } from './tooltip-events.components'; +import { RdxTooltipInitiallyOpenComponent } from './tooltip-initially-open.component'; +import { RdxTooltipMultipleComponent } from './tooltip-multiple.component'; +import { RdxTooltipPositioningComponent } from './tooltip-positioning.component'; +import { RdxTooltipTriggeringComponent } from './tooltip-triggering.component'; const html = String.raw; @@ -18,15 +18,15 @@ export default { decorators: [ moduleMetadata({ imports: [ - RdxPopoverModule, - RdxPopoverDefaultComponent, - RdxPopoverEventsComponent, - RdxPopoverPositioningComponent, - RdxPopoverTriggeringComponent, - RdxPopoverMultipleComponent, - RdxPopoverAnimationsComponent, - RdxPopoverInitiallyOpenComponent, - RdxPopoverAnchorComponent, + RdxTooltipModule, + RdxTooltipDefaultComponent, + RdxTooltipEventsComponent, + RdxTooltipPositioningComponent, + RdxTooltipTriggeringComponent, + RdxTooltipMultipleComponent, + RdxTooltipAnimationsComponent, + RdxTooltipInitiallyOpenComponent, + RdxTooltipAnchorComponent, LucideAngularModule, LucideAngularModule.pick({ MountainSnow, X }) ], @@ -52,7 +52,7 @@ type Story = StoryObj; export const Default: Story = { render: () => ({ template: html` - + ` }) }; @@ -60,7 +60,7 @@ export const Default: Story = { export const Multiple: Story = { render: () => ({ template: html` - + ` }) }; @@ -68,7 +68,7 @@ export const Multiple: Story = { export const Events: Story = { render: () => ({ template: html` - + ` }) }; @@ -76,7 +76,7 @@ export const Events: Story = { export const Positioning: Story = { render: () => ({ template: html` - + ` }) }; @@ -84,7 +84,7 @@ export const Positioning: Story = { export const ExternalTriggering: Story = { render: () => ({ template: html` - + ` }) }; @@ -92,7 +92,7 @@ export const ExternalTriggering: Story = { export const Anchor: Story = { render: () => ({ template: html` - + ` }) }; @@ -100,7 +100,7 @@ export const Anchor: Story = { export const InitiallyOpen: Story = { render: () => ({ template: html` - + ` }) }; @@ -108,7 +108,7 @@ export const InitiallyOpen: Story = { export const Animations: Story = { render: () => ({ template: html` - + ` }) }; diff --git a/packages/primitives/tooltip-v2/stories/utils/containers.registry.ts b/packages/primitives/tooltip-v2/stories/utils/containers.registry.ts index 1ead99ba..b17af61d 100644 --- a/packages/primitives/tooltip-v2/stories/utils/containers.registry.ts +++ b/packages/primitives/tooltip-v2/stories/utils/containers.registry.ts @@ -1,8 +1,8 @@ import { isDevMode } from '@angular/core'; -import { RdxPopoverRootDirective } from '../../src/tooltip-root.directive'; +import { RdxTooltipRootDirective } from '../../src/tooltip-root.directive'; import { injectRdxCdkEventService } from '../../src/utils/cdk-event.service'; -const containerRegistry: Map = new Map(); +const containerRegistry: Map = new Map(); let rdxCdkEventService: ReturnType | undefined = void 0; const domRootClickEventCallback: (event: MouseEvent) => void = (event: MouseEvent) => { @@ -32,11 +32,11 @@ const domRootClickEventCallback: (event: MouseEvent) => void = (event: MouseEven }); }; -export function registerContainer(container: HTMLElement, popoverRoot: RdxPopoverRootDirective) { +export function registerContainer(container: HTMLElement, root: RdxTooltipRootDirective) { if (containerRegistry.has(container)) { return; } - containerRegistry.set(container, popoverRoot); + containerRegistry.set(container, root); if (containerRegistry.size === 1) { rdxCdkEventService?.addClickDomRootEventCallback(domRootClickEventCallback); } diff --git a/packages/primitives/tooltip-v2/stories/utils/option-panel-base.class.ts b/packages/primitives/tooltip-v2/stories/utils/option-panel-base.class.ts index 2ea43dbf..422ae4cb 100644 --- a/packages/primitives/tooltip-v2/stories/utils/option-panel-base.class.ts +++ b/packages/primitives/tooltip-v2/stories/utils/option-panel-base.class.ts @@ -1,6 +1,6 @@ import { afterNextRender, DestroyRef, Directive, ElementRef, inject, signal, viewChildren } from '@angular/core'; import { injectDocument, RDX_POSITIONING_DEFAULTS } from '@radix-ng/primitives/core'; -import { RdxPopoverRootDirective } from '../../src/tooltip-root.directive'; +import { RdxTooltipRootDirective } from '../../src/tooltip-root.directive'; import { injectRdxCdkEventService } from '../../src/utils/cdk-event.service'; import { deregisterContainer, registerContainer, setRdxCdkEventService } from './containers.registry'; import { IArrowDimensions, IIgnoreClickOutsideContainer } from './types'; @@ -15,19 +15,19 @@ export abstract class OptionPanelBase implements IIgnoreClickOutsideContainer, I readonly elementRef = inject>(ElementRef); readonly destroyRef = inject(DestroyRef); - readonly rootDirectives = viewChildren(RdxPopoverRootDirective); + readonly rootDirectives = viewChildren(RdxTooltipRootDirective); readonly document = injectDocument(); readonly rdxCdkEventService = injectRdxCdkEventService(); protected constructor() { afterNextRender(() => { this.elementRef.nativeElement.querySelectorAll('.container').forEach((container) => { - const popoverRootInsideContainer = this.rootDirectives().find((popoverRoot) => - container.contains(popoverRoot.popoverTriggerDirective().elementRef.nativeElement) + const rootInsideContainer = this.rootDirectives().find((rootDirective) => + container.contains(rootDirective.triggerDirective().elementRef.nativeElement) ); - if (popoverRootInsideContainer) { + if (rootInsideContainer) { setRdxCdkEventService(this.rdxCdkEventService); - registerContainer(container, popoverRootInsideContainer); + registerContainer(container, rootInsideContainer); this.destroyRef.onDestroy(() => deregisterContainer(container)); } }); diff --git a/packages/primitives/tooltip-v2/stories/utils/styles.constants.ts b/packages/primitives/tooltip-v2/stories/utils/styles.constants.ts index ea3adb21..1845f650 100644 --- a/packages/primitives/tooltip-v2/stories/utils/styles.constants.ts +++ b/packages/primitives/tooltip-v2/stories/utils/styles.constants.ts @@ -1,39 +1,39 @@ const appliedAnimations = ` -.PopoverContent[data-state='open'][data-side='top'] { +.TooltipContent[data-state='open'][data-side='top'] { animation-name: rdxSlideDownAndFade; } -.PopoverContent[data-state='open'][data-side='right'] { +.TooltipContent[data-state='open'][data-side='right'] { animation-name: rdxSlideLeftAndFade; } -.PopoverContent[data-state='open'][data-side='bottom'] { +.TooltipContent[data-state='open'][data-side='bottom'] { animation-name: rdxSlideUpAndFade; } -.PopoverContent[data-state='open'][data-side='left'] { +.TooltipContent[data-state='open'][data-side='left'] { animation-name: rdxSlideRightAndFade; } -.PopoverContent[data-state='closed'][data-side='top'] { +.TooltipContent[data-state='closed'][data-side='top'] { animation-name: rdxSlideDownAndFadeReverse; } -.PopoverContent[data-state='closed'][data-side='right'] { +.TooltipContent[data-state='closed'][data-side='right'] { animation-name: rdxSlideLeftAndFadeReverse; } -.PopoverContent[data-state='closed'][data-side='bottom'] { +.TooltipContent[data-state='closed'][data-side='bottom'] { animation-name: rdxSlideUpAndFadeReverse; } -.PopoverContent[data-state='closed'][data-side='left'] { +.TooltipContent[data-state='closed'][data-side='left'] { animation-name: rdxSlideRightAndFadeReverse; } `; const animationParams = ` -.PopoverContent { +.TooltipContent { animation-duration: 400ms; animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); will-change: transform, opacity; @@ -212,7 +212,7 @@ function styles(withAnimations = false, withEvents = false, withParams = true) { } } -.PopoverId { +.TooltipId { color: var(--white-a12); font-size: 12px; line-height: 14px; @@ -220,7 +220,7 @@ function styles(withAnimations = false, withEvents = false, withParams = true) { margin: 1px 0 24px 22px; } -.PopoverContent { +.TooltipContent { border-radius: 4px; padding: 20px; width: 260px; @@ -234,18 +234,18 @@ ${withAnimations ? animationParams : ''} ${withAnimations ? appliedAnimations : ''} -.PopoverContent:focus { +.TooltipContent:focus { box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px, 0 0 0 2px var(--violet-7); } -.PopoverArrow { +.TooltipArrow { fill: white; } -.PopoverClose { +.TooltipClose { font-family: inherit; border-radius: 100%; height: 25px; @@ -259,11 +259,11 @@ ${withAnimations ? appliedAnimations : ''} right: 5px; } -.PopoverClose:hover { +.TooltipClose:hover { background-color: var(--violet-4); } -.PopoverClose:focus { +.TooltipClose:focus { box-shadow: 0 0 0 2px var(--violet-7); } diff --git a/packages/primitives/tooltip-v2/stories/utils/types.ts b/packages/primitives/tooltip-v2/stories/utils/types.ts index d57e53d5..d2d1916b 100644 --- a/packages/primitives/tooltip-v2/stories/utils/types.ts +++ b/packages/primitives/tooltip-v2/stories/utils/types.ts @@ -1,5 +1,5 @@ import { DestroyRef, ElementRef, Signal } from '@angular/core'; -import { RdxPopoverRootDirective } from '../../src/tooltip-root.directive'; +import { RdxTooltipRootDirective } from '../../src/tooltip-root.directive'; import { injectRdxCdkEventService } from '../../src/utils/cdk-event.service'; export interface IIgnoreClickOutsideContainer { @@ -7,7 +7,7 @@ export interface IIgnoreClickOutsideContainer { onOverlayOutsideClickDisabled: Signal; elementRef: ElementRef; destroyRef: DestroyRef; - rootDirectives: Signal>; + rootDirectives: Signal>; document: Document; rdxCdkEventService: ReturnType; } diff --git a/packages/primitives/tooltip-v2/stories/utils/with-option-panel.component.ts b/packages/primitives/tooltip-v2/stories/utils/with-option-panel.component.ts index 71e40fbc..3f5dc337 100644 --- a/packages/primitives/tooltip-v2/stories/utils/with-option-panel.component.ts +++ b/packages/primitives/tooltip-v2/stories/utils/with-option-panel.component.ts @@ -11,12 +11,12 @@ import { signal } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { RdxPopoverRootDirective } from '../../src/tooltip-root.directive'; +import { RdxTooltipRootDirective } from '../../src/tooltip-root.directive'; import { paramsAndEventsOnly } from './styles.constants'; import { Message } from './types'; @Component({ - selector: 'popover-with-option-panel', + selector: 'tooltip-with-option-panel', styles: paramsAndEventsOnly, template: ` @@ -64,7 +64,7 @@ import { Message } from './types';

{{ index }}. - [({{ message.timeFromPrev }}ms) POPOVER ID {{ rootUniqueId() }}] + [({{ message.timeFromPrev }}ms) TOOLTIP ID {{ rootUniqueId() }}] {{ message.value }}

@@ -84,12 +84,12 @@ export class WithOptionPanelComponent { readonly elementRef = inject>(ElementRef); - readonly popoverRootDirective = contentChild.required(RdxPopoverRootDirective); + readonly rootDirective = contentChild.required(RdxTooltipRootDirective); readonly paramsContainerCounter = signal(0); readonly messages = signal([]); - readonly rootUniqueId = computed(() => this.popoverRootDirective().uniqueId()); + readonly rootUniqueId = computed(() => this.rootDirective().uniqueId()); /** * There should be only one container. If there is more, en error is thrown. @@ -110,14 +110,10 @@ export class WithOptionPanelComponent { constructor() { afterNextRender({ read: () => { - this.popoverRootDirective().popoverContentDirective().onOpen.subscribe(this.onOpen); - this.popoverRootDirective().popoverContentDirective().onClosed.subscribe(this.onClose); - this.popoverRootDirective() - .popoverContentDirective() - .onOverlayOutsideClick.subscribe(this.onOverlayOutsideClick); - this.popoverRootDirective() - .popoverContentDirective() - .onOverlayEscapeKeyDown.subscribe(this.onOverlayEscapeKeyDown); + this.rootDirective().contentDirective().onOpen.subscribe(this.onOpen); + this.rootDirective().contentDirective().onClosed.subscribe(this.onClose); + this.rootDirective().contentDirective().onOverlayOutsideClick.subscribe(this.onOverlayOutsideClick); + this.rootDirective().contentDirective().onOverlayEscapeKeyDown.subscribe(this.onOverlayEscapeKeyDown); /** * There should be only one container. If there is more, en error is thrown. @@ -149,24 +145,24 @@ export class WithOptionPanelComponent { private onOverlayEscapeKeyDown = () => { this.addMessage({ - value: `[PopoverRoot] Escape clicked! (disabled: ${this.onOverlayEscapeKeyDownDisabled()})`, + value: `[TooltipRoot] Escape clicked! (disabled: ${this.onOverlayEscapeKeyDownDisabled()})`, timeFromPrev: this.timeFromPrev() }); }; private onOverlayOutsideClick = () => { this.addMessage({ - value: `[PopoverRoot] Mouse clicked outside the popover! (disabled: ${this.onOverlayOutsideClickDisabled()})`, + value: `[TooltipRoot] Mouse clicked outside the tooltip! (disabled: ${this.onOverlayOutsideClickDisabled()})`, timeFromPrev: this.timeFromPrev() }); }; private onOpen = () => { - this.addMessage({ value: '[PopoverContent] Open', timeFromPrev: this.timeFromPrev() }); + this.addMessage({ value: '[TooltipContent] Open', timeFromPrev: this.timeFromPrev() }); }; private onClose = () => { - this.addMessage({ value: '[PopoverContent] Closed', timeFromPrev: this.timeFromPrev() }); + this.addMessage({ value: '[TooltipContent] Closed', timeFromPrev: this.timeFromPrev() }); }; protected addMessage = (message: Message) => { From 2750171b8d52afac7f8e9d9bbcdaad674d289390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Twardziak?= Date: Mon, 30 Dec 2024 00:45:22 +0100 Subject: [PATCH 05/11] chore(tooltip): add hover and focus actions --- .../src/tooltip-trigger.directive.ts | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/primitives/tooltip-v2/src/tooltip-trigger.directive.ts b/packages/primitives/tooltip-v2/src/tooltip-trigger.directive.ts index 505da3bc..193b4e2e 100644 --- a/packages/primitives/tooltip-v2/src/tooltip-trigger.directive.ts +++ b/packages/primitives/tooltip-v2/src/tooltip-trigger.directive.ts @@ -12,6 +12,10 @@ import { injectTooltipRoot } from './tooltip-root.inject'; '[attr.aria-expanded]': 'rootDirective.isOpen()', '[attr.aria-controls]': 'rootDirective.contentDirective().name()', '[attr.data-state]': 'rootDirective.state()', + '(pointerenter)': 'pointerenter()', + '(pointerleave)': 'pointerleave()', + '(focus)': 'focus()', + '(blur)': 'blur()', '(click)': 'click()' } }) @@ -26,8 +30,28 @@ export class RdxTooltipTriggerDirective { /** @ignore */ readonly name = computed(() => `rdx-tooltip-trigger-${this.rootDirective.uniqueId()}`); + /** @ignore */ + pointerenter(): void { + this.rootDirective.handleOpen(); + } + + /** @ignore */ + pointerleave(): void { + this.rootDirective.handleClose(); + } + + /** @ignore */ + focus(): void { + this.rootDirective.handleOpen(); + } + + /** @ignore */ + blur(): void { + this.rootDirective.handleClose(); + } + /** @ignore */ click(): void { - this.rootDirective.handleToggle(); + this.rootDirective.handleClose(); } } From f0c7ce11298dddb8f7f611921eb8c7935dbbe102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Twardziak?= Date: Mon, 30 Dec 2024 01:20:01 +0100 Subject: [PATCH 06/11] chore(tooltip): simplify the content & unleash close button --- .../tooltip-v2/src/tooltip-close.directive.ts | 7 +- .../tooltip-v2/src/tooltip-root.directive.ts | 4 +- .../stories/tooltip-anchor.component.ts | 44 ++----------- .../stories/tooltip-animations.component.ts | 20 +----- .../stories/tooltip-default.component.ts | 22 +------ .../stories/tooltip-events.components.ts | 22 +------ .../tooltip-initially-open.component.ts | 22 +------ .../stories/tooltip-multiple.component.ts | 66 ++----------------- .../stories/tooltip-positioning.component.ts | 22 +------ .../stories/tooltip-triggering.component.ts | 44 ++----------- .../stories/utils/styles.constants.ts | 12 ++-- 11 files changed, 36 insertions(+), 249 deletions(-) diff --git a/packages/primitives/tooltip-v2/src/tooltip-close.directive.ts b/packages/primitives/tooltip-v2/src/tooltip-close.directive.ts index 84eb6567..bdf6c927 100644 --- a/packages/primitives/tooltip-v2/src/tooltip-close.directive.ts +++ b/packages/primitives/tooltip-v2/src/tooltip-close.directive.ts @@ -2,11 +2,14 @@ import { Directive, effect, ElementRef, forwardRef, inject, Renderer2, untracked 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()' + '(click)': 'rootDirective.handleClose(true)' }, providers: [ { @@ -36,7 +39,7 @@ export class RdxTooltipCloseDirective { this.renderer.setStyle( this.elementRef.nativeElement, 'display', - isControlledExternally ? 'none' : null + isControlledExternally ? null : 'none' ); }); }); diff --git a/packages/primitives/tooltip-v2/src/tooltip-root.directive.ts b/packages/primitives/tooltip-v2/src/tooltip-root.directive.ts index 4ace22e9..c2f49ec1 100644 --- a/packages/primitives/tooltip-v2/src/tooltip-root.directive.ts +++ b/packages/primitives/tooltip-v2/src/tooltip-root.directive.ts @@ -156,11 +156,11 @@ export class RdxTooltipRootDirective { } /** @ignore */ - handleClose(): void { + handleClose(closeButton?: boolean): void { if (this.isFirstDefaultOpen()) { this.isFirstDefaultOpen.set(false); } - if (this.externalControl()) { + if (!closeButton && this.externalControl()) { return; } this.setState(RdxTooltipState.CLOSED); diff --git a/packages/primitives/tooltip-v2/stories/tooltip-anchor.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-anchor.component.ts index ab675530..ba140dcd 100644 --- a/packages/primitives/tooltip-v2/stories/tooltip-anchor.component.ts +++ b/packages/primitives/tooltip-v2/stories/tooltip-anchor.component.ts @@ -53,27 +53,9 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component'; >
-
-

Dimensions

-
- - -
-
- - -
-
- - -
-
- - -
-
+ Add to library
-
-

Dimensions

-
- - -
-
- - -
-
- - -
-
- - -
-
+ Add to library
-
-

Dimensions

-
- - -
-
- - -
-
- - -
-
- - -
-
+ Add to library
-
-

Dimensions

-
- - -
-
- - -
-
- - -
-
- - -
-
+ Add to library
-
-

Dimensions

-
- - -
-
- - -
-
- - -
-
- - -
-
+ Add to library
-
-

Dimensions

-
- - -
-
- - -
-
- - -
-
- - -
-
+ Add to library
-
-

Dimensions

-
- - -
-
- - -
-
- - -
-
- - -
-
+ Add to library
-
-

Dimensions

-
- - -
-
- - -
-
- - -
-
- - -
-
+ Add to library
-
-

Dimensions

-
- - -
-
- - -
-
- - -
-
- - -
-
+ Add to library
-
-

Dimensions

-
- - -
-
- - -
-
- - -
-
- - -
-
+ Add to library
-
-

Dimensions

-
- - -
-
- - -
-
- - -
-
- - -
-
+ Add to library
-
-

Dimensions

-
- - -
-
- - -
-
- - -
-
- - -
-
+ Add to library
Date: Mon, 30 Dec 2024 02:33:19 +0100 Subject: [PATCH 07/11] chore(tooltip): delay opening and closing tooltip --- .../tooltip-v2/src/tooltip-root.directive.ts | 62 +++++++++++++++++-- .../tooltip-v2/src/tooltip.types.ts | 5 ++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/packages/primitives/tooltip-v2/src/tooltip-root.directive.ts b/packages/primitives/tooltip-v2/src/tooltip-root.directive.ts index c2f49ec1..0ebe2b4b 100644 --- a/packages/primitives/tooltip-v2/src/tooltip-root.directive.ts +++ b/packages/primitives/tooltip-v2/src/tooltip-root.directive.ts @@ -1,4 +1,4 @@ -import { BooleanInput } from '@angular/cdk/coercion'; +import { BooleanInput, NumberInput } from '@angular/cdk/coercion'; import { afterNextRender, booleanAttribute, @@ -9,10 +9,13 @@ import { effect, inject, input, + numberAttribute, signal, untracked, ViewContainerRef } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { debounce, map, Subject, tap, timer } from 'rxjs'; import { RdxTooltipAnchorDirective } from './tooltip-anchor.directive'; import { RdxTooltipAnchorToken } from './tooltip-anchor.token'; import { RdxTooltipArrowToken } from './tooltip-arrow.token'; @@ -20,7 +23,12 @@ import { RdxTooltipCloseToken } from './tooltip-close.token'; import { RdxTooltipContentAttributesToken } from './tooltip-content-attributes.token'; import { RdxTooltipContentDirective } from './tooltip-content.directive'; import { RdxTooltipTriggerDirective } from './tooltip-trigger.directive'; -import { RdxTooltipAnimationStatus, RdxTooltipAttachDetachEvent, RdxTooltipState } from './tooltip.types'; +import { + RdxTooltipAction, + RdxTooltipAnimationStatus, + RdxTooltipAttachDetachEvent, + RdxTooltipState +} from './tooltip.types'; import { injectRdxCdkEventService } from './utils/cdk-event.service'; let nextId = 0; @@ -50,6 +58,18 @@ export class RdxTooltipRootDirective { * @default undefined */ readonly open = input(void 0, { transform: booleanAttribute }); + /** + * To customise the open delay for a specific tooltip. + */ + readonly openDelay = input(500, { + transform: numberAttribute + }); + /** + * To customise the close delay for a specific tooltip. + */ + readonly closeDelay = input(200, { + transform: numberAttribute + }); /** * @description Whether to control the state of the tooltip from external. Use in conjunction with `open` input. * @default undefined @@ -106,9 +126,13 @@ export class RdxTooltipRootDirective { /** @ignore */ readonly anchorDirective = computed(() => this.internalAnchorDirective() ?? this.anchor()); + /** @ignore */ + readonly actionSubject$ = new Subject(); + constructor() { this.rdxCdkEventService?.registerPrimitive(this); this.destroyRef.onDestroy(() => this.rdxCdkEventService?.deregisterPrimitive(this)); + this.actionSubscription(); this.onStateChangeEffect(); this.onCssAnimationStatusChangeChangeEffect(); this.onOpenChangeEffect(); @@ -152,7 +176,7 @@ export class RdxTooltipRootDirective { if (this.externalControl()) { return; } - this.setState(RdxTooltipState.OPEN); + this.actionSubject$.next(RdxTooltipAction.OPEN); } /** @ignore */ @@ -163,7 +187,7 @@ export class RdxTooltipRootDirective { if (!closeButton && this.externalControl()) { return; } - this.setState(RdxTooltipState.CLOSED); + this.actionSubject$.next(RdxTooltipAction.CLOSE); } /** @ignore */ @@ -364,4 +388,34 @@ export class RdxTooltipRootDirective { }); }); }; + + /** @ignore */ + private actionSubscription() { + this.actionSubject$ + .asObservable() + .pipe( + map((action) => { + console.log(action); + switch (action) { + case RdxTooltipAction.OPEN: + return { action, duration: this.openDelay() }; + case RdxTooltipAction.CLOSE: + return { action, duration: this.closeDelay() }; + } + }), + debounce((config) => timer(config.duration)), + tap((config) => { + switch (config.action) { + case RdxTooltipAction.OPEN: + this.setState(RdxTooltipState.OPEN); + break; + case RdxTooltipAction.CLOSE: + this.setState(RdxTooltipState.CLOSED); + break; + } + }), + takeUntilDestroyed() + ) + .subscribe(); + } } diff --git a/packages/primitives/tooltip-v2/src/tooltip.types.ts b/packages/primitives/tooltip-v2/src/tooltip.types.ts index e6c25169..ded4450f 100644 --- a/packages/primitives/tooltip-v2/src/tooltip.types.ts +++ b/packages/primitives/tooltip-v2/src/tooltip.types.ts @@ -3,6 +3,11 @@ export enum RdxTooltipState { CLOSED = 'closed' } +export enum RdxTooltipAction { + OPEN = 'open', + CLOSE = 'close' +} + export enum RdxTooltipAttachDetachEvent { ATTACH = 'attach', DETACH = 'detach' From 81eef0769b566b30e141ae45f935269e879272f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Twardziak?= Date: Mon, 30 Dec 2024 02:56:49 +0100 Subject: [PATCH 08/11] feat(tooltip): add option panel for delaying --- .../stories/tooltip-anchor.component.ts | 23 +++++++++++-- .../stories/tooltip-animations.component.ts | 6 ++++ .../stories/tooltip-default.component.ts | 6 +++- .../stories/tooltip-events.components.ts | 6 +++- .../tooltip-initially-open.component.ts | 6 +++- .../stories/tooltip-multiple.component.ts | 33 +++++++++++++++++-- .../stories/tooltip-positioning.component.ts | 6 +++- .../stories/tooltip-triggering.component.ts | 12 +++++++ .../stories/utils/option-panel-base.class.ts | 7 ++-- .../tooltip-v2/stories/utils/types.ts | 5 +++ .../utils/with-option-panel.component.ts | 12 ++++++- 11 files changed, 110 insertions(+), 12 deletions(-) diff --git a/packages/primitives/tooltip-v2/stories/tooltip-anchor.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-anchor.component.ts index ba140dcd..b6090f4e 100644 --- a/packages/primitives/tooltip-v2/stories/tooltip-anchor.component.ts +++ b/packages/primitives/tooltip-v2/stories/tooltip-anchor.component.ts @@ -27,17 +27,26 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
{{ containerAlert }}
- + @@ -73,10 +82,14 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
@@ -87,7 +100,13 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component'; - + diff --git a/packages/primitives/tooltip-v2/stories/tooltip-animations.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-animations.component.ts index 7ecd8135..81809985 100644 --- a/packages/primitives/tooltip-v2/stories/tooltip-animations.component.ts +++ b/packages/primitives/tooltip-v2/stories/tooltip-animations.component.ts @@ -25,10 +25,14 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
@@ -56,6 +60,8 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component'; [cssAnimation]="cssAnimation()" [cssOpeningAnimation]="cssOpeningAnimation()" [cssClosingAnimation]="cssClosingAnimation()" + [openDelay]="openDelay()" + [closeDelay]="closeDelay()" rdxTooltipRoot > diff --git a/packages/primitives/tooltip-v2/stories/tooltip-events.components.ts b/packages/primitives/tooltip-v2/stories/tooltip-events.components.ts index 1b77841a..27df8db0 100644 --- a/packages/primitives/tooltip-v2/stories/tooltip-events.components.ts +++ b/packages/primitives/tooltip-v2/stories/tooltip-events.components.ts @@ -27,17 +27,21 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
{{ containerAlert }}
- + diff --git a/packages/primitives/tooltip-v2/stories/tooltip-initially-open.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-initially-open.component.ts index f03fd639..fabece35 100644 --- a/packages/primitives/tooltip-v2/stories/tooltip-initially-open.component.ts +++ b/packages/primitives/tooltip-v2/stories/tooltip-initially-open.component.ts @@ -24,17 +24,21 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
{{ containerAlert }}
- + diff --git a/packages/primitives/tooltip-v2/stories/tooltip-multiple.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-multiple.component.ts index 3f4b349c..3f58e5bf 100644 --- a/packages/primitives/tooltip-v2/stories/tooltip-multiple.component.ts +++ b/packages/primitives/tooltip-v2/stories/tooltip-multiple.component.ts @@ -25,17 +25,26 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
{{ containerAlert }}
- + @@ -66,17 +75,26 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
{{ containerAlert }}
- + @@ -111,17 +129,26 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
{{ containerAlert }}
- + diff --git a/packages/primitives/tooltip-v2/stories/tooltip-positioning.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-positioning.component.ts index 7978810a..8f19d15b 100644 --- a/packages/primitives/tooltip-v2/stories/tooltip-positioning.component.ts +++ b/packages/primitives/tooltip-v2/stories/tooltip-positioning.component.ts @@ -25,10 +25,14 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
Side: @@ -65,7 +69,7 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component'; {{ containerAlert }}
- + diff --git a/packages/primitives/tooltip-v2/stories/tooltip-triggering.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-triggering.component.ts index 1c8f3238..ec36d254 100644 --- a/packages/primitives/tooltip-v2/stories/tooltip-triggering.component.ts +++ b/packages/primitives/tooltip-v2/stories/tooltip-triggering.component.ts @@ -25,10 +25,14 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
@@ -52,6 +56,8 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component'; @@ -89,10 +95,14 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component';
@@ -116,6 +126,8 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component'; diff --git a/packages/primitives/tooltip-v2/stories/utils/option-panel-base.class.ts b/packages/primitives/tooltip-v2/stories/utils/option-panel-base.class.ts index 422ae4cb..1eecdab0 100644 --- a/packages/primitives/tooltip-v2/stories/utils/option-panel-base.class.ts +++ b/packages/primitives/tooltip-v2/stories/utils/option-panel-base.class.ts @@ -3,16 +3,19 @@ import { injectDocument, RDX_POSITIONING_DEFAULTS } from '@radix-ng/primitives/c import { RdxTooltipRootDirective } from '../../src/tooltip-root.directive'; import { injectRdxCdkEventService } from '../../src/utils/cdk-event.service'; import { deregisterContainer, registerContainer, setRdxCdkEventService } from './containers.registry'; -import { IArrowDimensions, IIgnoreClickOutsideContainer } from './types'; +import { IArrowDimensions, IIgnoreClickOutsideContainer, IOpenCloseDelay } from './types'; @Directive() -export abstract class OptionPanelBase implements IIgnoreClickOutsideContainer, IArrowDimensions { +export abstract class OptionPanelBase implements IIgnoreClickOutsideContainer, IArrowDimensions, IOpenCloseDelay { onOverlayEscapeKeyDownDisabled = signal(false); onOverlayOutsideClickDisabled = signal(false); arrowWidth = signal(RDX_POSITIONING_DEFAULTS.arrow.width); arrowHeight = signal(RDX_POSITIONING_DEFAULTS.arrow.height); + openDelay = signal(500); + closeDelay = signal(200); + readonly elementRef = inject>(ElementRef); readonly destroyRef = inject(DestroyRef); readonly rootDirectives = viewChildren(RdxTooltipRootDirective); diff --git a/packages/primitives/tooltip-v2/stories/utils/types.ts b/packages/primitives/tooltip-v2/stories/utils/types.ts index d2d1916b..b3877b39 100644 --- a/packages/primitives/tooltip-v2/stories/utils/types.ts +++ b/packages/primitives/tooltip-v2/stories/utils/types.ts @@ -17,4 +17,9 @@ export interface IArrowDimensions { arrowHeight: Signal; } +export interface IOpenCloseDelay { + openDelay: Signal; + closeDelay: Signal; +} + export type Message = { value: string; timeFromPrev: number }; diff --git a/packages/primitives/tooltip-v2/stories/utils/with-option-panel.component.ts b/packages/primitives/tooltip-v2/stories/utils/with-option-panel.component.ts index 3f5dc337..645ec679 100644 --- a/packages/primitives/tooltip-v2/stories/utils/with-option-panel.component.ts +++ b/packages/primitives/tooltip-v2/stories/utils/with-option-panel.component.ts @@ -21,7 +21,7 @@ import { Message } from './types'; template: ` - @if (paramsContainerCounter() > 2) { + @if (paramsContainerCounter() > 3) {
} @@ -47,6 +47,13 @@ import { Message } from './types';
+
+ Open delay + + Close delay + +
+ @if (messages().length) { @@ -82,6 +89,9 @@ export class WithOptionPanelComponent { arrowWidth = model(0); arrowHeight = model(0); + openDelay = model(0); + closeDelay = model(0); + readonly elementRef = inject>(ElementRef); readonly rootDirective = contentChild.required(RdxTooltipRootDirective); From 407f0ece45410a62f5f9c6bf9dcfd6d6a1106030 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Twardziak?= Date: Mon, 30 Dec 2024 12:33:37 +0100 Subject: [PATCH 09/11] chore(tooltip): add explanations to multiple case story --- .../stories/tooltip-multiple.component.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/primitives/tooltip-v2/stories/tooltip-multiple.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-multiple.component.ts index 3f58e5bf..349ab8fc 100644 --- a/packages/primitives/tooltip-v2/stories/tooltip-multiple.component.ts +++ b/packages/primitives/tooltip-v2/stories/tooltip-multiple.component.ts @@ -22,6 +22,7 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component'; ], styles: styles(), template: ` +

Tooltip #1

ID: {{ rootDirective1()?.uniqueId() }}
+

Tooltip #2

{{ containerAlert }}
+
+ + [side]="'left'" + [align]="'start'" + [sideOffset]="16" + [alignOffset]="16" +
ID: {{ rootDirective2()?.uniqueId() }}
+

Tooltip #3

{{ containerAlert }}
+
+ + [side]="'right'" + [align]="'end'" + [sideOffset]="60" + [alignOffset]="60" +
Date: Mon, 30 Dec 2024 12:34:19 +0100 Subject: [PATCH 10/11] chore(popover): add explanations to multiple case story --- .../stories/popover-multiple.component.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/primitives/popover/stories/popover-multiple.component.ts b/packages/primitives/popover/stories/popover-multiple.component.ts index 8b11a12c..fdbfa35f 100644 --- a/packages/primitives/popover/stories/popover-multiple.component.ts +++ b/packages/primitives/popover/stories/popover-multiple.component.ts @@ -22,6 +22,7 @@ import { WithOptionPanelComponent } from './utils/with-option-panel.component'; ], styles: styles(), template: ` +

Popover #1

ID: {{ popoverRootDirective1()?.uniqueId() }}
+

Popover #2

{{ containerAlert }}
+
+ + [side]="'left'" + [align]="'start'" + [sideOffset]="16" + [alignOffset]="16" +
+
+ + [side]="'right'" + [align]="'end'" + [sideOffset]="60" + [alignOffset]="60" +
- - -
- - Add to library -
-
-
-
-
-
ID: {{ rootDirective()?.uniqueId() }}
-
- ` -}) -export class RdxTooltipEventsComponent extends OptionPanelBase { - readonly rootDirective = viewChild(RdxTooltipRootDirective); - - readonly MountainSnowIcon = MountainSnow; - readonly XIcon = X; - - protected readonly sides = RdxPositionSide; - protected readonly aligns = RdxPositionAlign; - protected readonly containerAlert = containerAlert; - protected readonly TriangleAlert = TriangleAlert; -} diff --git a/packages/primitives/tooltip-v2/stories/tooltip-positioning.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-positioning.component.ts deleted file mode 100644 index 8f19d15b..00000000 --- a/packages/primitives/tooltip-v2/stories/tooltip-positioning.component.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { Component, signal, viewChild } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { RdxPositionAlign, RdxPositionSide } from '@radix-ng/primitives/core'; -import { LucideAngularModule, MountainSnow, TriangleAlert, X } from 'lucide-angular'; -import { RdxTooltipModule, RdxTooltipRootDirective } from '../index'; -import { RdxTooltipContentAttributesComponent } from '../src/tooltip-content-attributes.component'; -import { provideRdxCdkEventService } from '../src/utils/cdk-event.service'; -import { containerAlert } from './utils/constants'; -import { OptionPanelBase } from './utils/option-panel-base.class'; -import styles from './utils/styles.constants'; -import { WithOptionPanelComponent } from './utils/with-option-panel.component'; - -@Component({ - selector: 'rdx-tooltip-positioning', - providers: [provideRdxCdkEventService()], - imports: [ - FormsModule, - RdxTooltipModule, - LucideAngularModule, - RdxTooltipContentAttributesComponent, - WithOptionPanelComponent - ], - styles: styles(), - template: ` - -
- Side: - - Align: - - SideOffset: - - AlignOffset: - -
- -
- - Disable alternate positions (to see the result, scroll the page to make the tooltip cross the viewport - boundary) -
- -
- - {{ containerAlert }} -
-
- - - - -
- - Add to library -
-
-
-
-
-
ID: {{ rootDirective()?.uniqueId() }}
-
- ` -}) -export class RdxTooltipPositioningComponent extends OptionPanelBase { - readonly rootDirective = viewChild(RdxTooltipRootDirective); - - readonly selectedSide = signal(RdxPositionSide.Top); - readonly selectedAlign = signal(RdxPositionAlign.Center); - readonly sideOffset = signal(void 0); - readonly alignOffset = signal(void 0); - readonly disableAlternatePositions = signal(false); - - readonly sides = RdxPositionSide; - readonly aligns = RdxPositionAlign; - - readonly MountainSnowIcon = MountainSnow; - readonly XIcon = X; - protected readonly containerAlert = containerAlert; - protected readonly TriangleAlert = TriangleAlert; -} diff --git a/packages/primitives/tooltip-v2/stories/tooltip-triggering.component.ts b/packages/primitives/tooltip-v2/stories/tooltip-triggering.component.ts deleted file mode 100644 index ec36d254..00000000 --- a/packages/primitives/tooltip-v2/stories/tooltip-triggering.component.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { Component, signal, viewChild } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { LucideAngularModule, MountainSnow, TriangleAlert, X } from 'lucide-angular'; -import { RdxTooltipModule } from '../index'; -import { RdxTooltipContentAttributesComponent } from '../src/tooltip-content-attributes.component'; -import { provideRdxCdkEventService } from '../src/utils/cdk-event.service'; -import { containerAlert } from './utils/constants'; -import { OptionPanelBase } from './utils/option-panel-base.class'; -import styles from './utils/styles.constants'; -import { WithOptionPanelComponent } from './utils/with-option-panel.component'; - -@Component({ - selector: 'rdx-tooltip-triggering', - providers: [provideRdxCdkEventService()], - imports: [ - FormsModule, - RdxTooltipModule, - LucideAngularModule, - RdxTooltipContentAttributesComponent, - WithOptionPanelComponent - ], - styles: styles(), - template: ` -

Initially closed

- -
- - onOpenChange count: {{ counterOpenFalse() }} -
- -
- - External control -
- -
- - {{ containerAlert }} -
-
- - - - -
- - Add to library -
-
-
-
-
-
ID: {{ rootDirective1()?.uniqueId() }}
-
- -

Initially open

- -
- - onOpenChange count: {{ counterOpenTrue() }} -
- -
- - External control -
- -
- - {{ containerAlert }} -
-
- - - - -
- - Add to library -
-
-
-
-
-
ID: {{ rootDirective2()?.uniqueId() }}
-
- ` -}) -export class RdxTooltipTriggeringComponent extends OptionPanelBase { - readonly rootDirective1 = viewChild('root1'); - readonly rootDirective2 = viewChild('root2'); - - readonly MountainSnowIcon = MountainSnow; - readonly XIcon = X; - - isOpenFalse = signal(false); - counterOpenFalse = signal(0); - externalControlFalse = signal(true); - - isOpenTrue = signal(true); - counterOpenTrue = signal(0); - externalControlTrue = signal(true); - - triggerOpenFalse(): void { - this.isOpenFalse.update((value) => !value); - } - - countOpenFalse(open: boolean): void { - this.isOpenFalse.set(open); - this.counterOpenFalse.update((value) => value + 1); - } - - triggerOpenTrue(): void { - this.isOpenTrue.update((value) => !value); - } - - countOpenTrue(open: boolean): void { - this.isOpenTrue.set(open); - this.counterOpenTrue.update((value) => value + 1); - } - - protected readonly containerAlert = containerAlert; - protected readonly TriangleAlert = TriangleAlert; -} diff --git a/packages/primitives/tooltip-v2/stories/tooltip.docs.mdx b/packages/primitives/tooltip-v2/stories/tooltip.docs.mdx deleted file mode 100644 index 2a974a40..00000000 --- a/packages/primitives/tooltip-v2/stories/tooltip.docs.mdx +++ /dev/null @@ -1,156 +0,0 @@ -import {ArgTypes, Canvas, Markdown, Meta, Source} from '@storybook/blocks'; -import * as TooltipStories from "./tooltip.stories"; -import {RdxTooltipRootDirective} from "../src/tooltip-root.directive"; -import {RdxTooltipContentDirective} from "../src/tooltip-content.directive"; -import {RdxTooltipArrowDirective} from "../src/tooltip-arrow.directive"; -import {RdxTooltipAnchorDirective} from "../src/tooltip-anchor.directive"; -import {RdxTooltipTriggerDirective} from "../src/tooltip-trigger.directive"; -import {RdxTooltipCloseDirective} from "../src/tooltip-close.directive"; -import {animationStylesOnly} from "./utils/styles.constants"; -import {RdxTooltipContentAttributesComponent} from "../src/tooltip-content-attributes.component"; - - - -# Tooltip-v2 - -#### A popup that displays information ready to interact with related to an element when the element receives click on it. - -## Examples -### Default - - - -### Multiple - - - -### Events - - - -### Positioning - - - -### External Triggering - - - -### Anchor - - - -### Initially Open - - - -### Animations - - - -### Animation Styles - - - - -## Features - -- ✅ Opens when the trigger is clicked. -- ✅ Closes when the trigger is clicked again or when pressing escape. -- ✅ Controllable from outside. - -## Anatomy - -```html - - - - -
- - Add to library -
-
-
-
-``` - -## Import - -Get started with importing the directives: - - - -or - - - -## API Reference - -### Root -`RdxTooltipRootDirective` - -Contains all the parts of a tooltip. - - - -### Trigger -`RdxTooltipTriggerDirective` - -The button that toggles the tooltip. By default, the TooltipContent will position itself against the trigger. - - -{` -| Data attribute | Value | -|----------------|----------------| -| [data-state] | "closed" | "open" (type RdxTooltipState) -`} - - -### Anchor -`RdxTooltipAnchorDirective` - -An optional element to position the TooltipContent against. If this part is not used, the content will position alongside the TooltipTrigger. - -### Content -`RdxTooltipContentDirective` - -The component that pops out when the tooltip is open. - - - -### Content Attributes -`RdxTooltipContentAttributesComponent` - -A component with the content attributes that are necessary to run animations. - - - - -{` -| Data attribute | Value | -|----------------|----------------| -| [data-state] | "closed" | "open" (enum RdxTooltipState) -| [data-side] | "left" | "right" | "bottom" | "top" (enum RdxPositionSide) -| [data-align] | "start" | "end" | "center" (enum RdxPositionAlign) -`} - - -### Arrow -`RdxTooltipArrowDirective` - -An optional arrow element to render alongside the tooltip. This can be used to help visually link the trigger with the TooltipContent. Must be rendered inside TooltipContent. - - - -### Close -`RdxTooltipCloseDirective` - -An optional close button element to render alongside the tooltip. This can be used to close the TooltipContent. Must be rendered inside TooltipContent. diff --git a/packages/primitives/tooltip-v2/stories/tooltip.stories.ts b/packages/primitives/tooltip-v2/stories/tooltip.stories.ts deleted file mode 100644 index 424f04e4..00000000 --- a/packages/primitives/tooltip-v2/stories/tooltip.stories.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { provideAnimations } from '@angular/platform-browser/animations'; -import { componentWrapperDecorator, Meta, moduleMetadata, StoryObj } from '@storybook/angular'; -import { LucideAngularModule, MountainSnow, X } from 'lucide-angular'; -import { RdxTooltipModule } from '../index'; -import { RdxTooltipAnchorComponent } from './tooltip-anchor.component'; -import { RdxTooltipAnimationsComponent } from './tooltip-animations.component'; -import { RdxTooltipDefaultComponent } from './tooltip-default.component'; -import { RdxTooltipEventsComponent } from './tooltip-events.components'; -import { RdxTooltipInitiallyOpenComponent } from './tooltip-initially-open.component'; -import { RdxTooltipMultipleComponent } from './tooltip-multiple.component'; -import { RdxTooltipPositioningComponent } from './tooltip-positioning.component'; -import { RdxTooltipTriggeringComponent } from './tooltip-triggering.component'; - -const html = String.raw; - -export default { - title: 'Primitives/Tooltip-v2', - decorators: [ - moduleMetadata({ - imports: [ - RdxTooltipModule, - RdxTooltipDefaultComponent, - RdxTooltipEventsComponent, - RdxTooltipPositioningComponent, - RdxTooltipTriggeringComponent, - RdxTooltipMultipleComponent, - RdxTooltipAnimationsComponent, - RdxTooltipInitiallyOpenComponent, - RdxTooltipAnchorComponent, - LucideAngularModule, - LucideAngularModule.pick({ MountainSnow, X }) - ], - providers: [provideAnimations()] - }), - componentWrapperDecorator( - (story) => html` -
- ${story} -
- ` - ) - ] -} as Meta; - -type Story = StoryObj; - -export const Default: Story = { - render: () => ({ - template: html` - - ` - }) -}; - -export const Multiple: Story = { - render: () => ({ - template: html` - - ` - }) -}; - -export const Events: Story = { - render: () => ({ - template: html` - - ` - }) -}; - -export const Positioning: Story = { - render: () => ({ - template: html` - - ` - }) -}; - -export const ExternalTriggering: Story = { - render: () => ({ - template: html` - - ` - }) -}; - -export const Anchor: Story = { - render: () => ({ - template: html` - - ` - }) -}; - -export const InitiallyOpen: Story = { - render: () => ({ - template: html` - - ` - }) -}; - -export const Animations: Story = { - render: () => ({ - template: html` - - ` - }) -}; diff --git a/packages/primitives/tooltip/README.md b/packages/primitives/tooltip/README.md index cbd16d7b..7da26267 100644 --- a/packages/primitives/tooltip/README.md +++ b/packages/primitives/tooltip/README.md @@ -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`. diff --git a/packages/primitives/tooltip/index.ts b/packages/primitives/tooltip/index.ts index 4da01a4f..0dca7571 100644 --- a/packages/primitives/tooltip/index.ts +++ b/packages/primitives/tooltip/index.ts @@ -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({ diff --git a/packages/primitives/tooltip-v2/src/tooltip-anchor.directive.ts b/packages/primitives/tooltip/src/tooltip-anchor.directive.ts similarity index 100% rename from packages/primitives/tooltip-v2/src/tooltip-anchor.directive.ts rename to packages/primitives/tooltip/src/tooltip-anchor.directive.ts diff --git a/packages/primitives/tooltip-v2/src/tooltip-anchor.token.ts b/packages/primitives/tooltip/src/tooltip-anchor.token.ts similarity index 100% rename from packages/primitives/tooltip-v2/src/tooltip-anchor.token.ts rename to packages/primitives/tooltip/src/tooltip-anchor.token.ts diff --git a/packages/primitives/tooltip/src/tooltip-arrow.directive.ts b/packages/primitives/tooltip/src/tooltip-arrow.directive.ts index 2ee65392..4db29f82 100644 --- a/packages/primitives/tooltip/src/tooltip-arrow.directive.ts +++ b/packages/primitives/tooltip/src/tooltip-arrow.directive.ts @@ -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, @@ -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, @@ -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); + readonly elementRef = inject(ElementRef); /** - * The width of the arrow in pixels. + * @description The width of the arrow in pixels. + * @default 10 */ - readonly width = input(10, { transform: numberAttribute }); + readonly width = input(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(5, { transform: numberAttribute }); - - /** - * @ignore - * */ - private triggerRect: DOMRect; - - /** @ignore */ - private readonly currentArrowSvgElement = signal(void 0); + readonly height = input(RDX_POSITIONING_DEFAULTS.arrow.height, { transform: numberAttribute }); /** @ignore */ readonly arrowSvgElement = computed(() => { @@ -74,6 +69,14 @@ export class RdxTooltipArrowDirective { return svgElement; }); + /** @ignore */ + private readonly currentArrowSvgElement = signal(void 0); + /** @ignore */ + private readonly position = toSignal(this.rootDirective.contentDirective().positionChange()); + + /** @ignore */ + private anchorOrTriggerRect: DOMRect; + constructor() { afterNextRender({ write: () => { @@ -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); @@ -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); + }); }); - }); + } } diff --git a/packages/primitives/tooltip-v2/src/tooltip-close.directive.ts b/packages/primitives/tooltip/src/tooltip-close.directive.ts similarity index 100% rename from packages/primitives/tooltip-v2/src/tooltip-close.directive.ts rename to packages/primitives/tooltip/src/tooltip-close.directive.ts diff --git a/packages/primitives/tooltip-v2/src/tooltip-close.token.ts b/packages/primitives/tooltip/src/tooltip-close.token.ts similarity index 100% rename from packages/primitives/tooltip-v2/src/tooltip-close.token.ts rename to packages/primitives/tooltip/src/tooltip-close.token.ts diff --git a/packages/primitives/tooltip-v2/src/tooltip-content-attributes.component.ts b/packages/primitives/tooltip/src/tooltip-content-attributes.component.ts similarity index 100% rename from packages/primitives/tooltip-v2/src/tooltip-content-attributes.component.ts rename to packages/primitives/tooltip/src/tooltip-content-attributes.component.ts diff --git a/packages/primitives/tooltip/src/tooltip-content-attributes.directive.ts b/packages/primitives/tooltip/src/tooltip-content-attributes.directive.ts deleted file mode 100644 index 53d8faa3..00000000 --- a/packages/primitives/tooltip/src/tooltip-content-attributes.directive.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Directive, inject } from '@angular/core'; -import { RdxTooltipContentToken } from './tooltip-content.token'; -import { RdxTooltipRootDirective } from './tooltip-root.directive'; - -@Directive({ - selector: '[rdxTooltipContentAttributes]', - host: { - '[attr.data-state]': 'tooltipRoot.state()', - '[attr.data-side]': 'tooltipContent.side()' - } -}) -export class RdxTooltipContentAttributesDirective { - readonly tooltipRoot = inject(RdxTooltipRootDirective); - readonly tooltipContent = inject(RdxTooltipContentToken); -} diff --git a/packages/primitives/tooltip-v2/src/tooltip-content-attributes.token.ts b/packages/primitives/tooltip/src/tooltip-content-attributes.token.ts similarity index 100% rename from packages/primitives/tooltip-v2/src/tooltip-content-attributes.token.ts rename to packages/primitives/tooltip/src/tooltip-content-attributes.token.ts diff --git a/packages/primitives/tooltip/src/tooltip-content.directive.ts b/packages/primitives/tooltip/src/tooltip-content.directive.ts index d5944f8a..e967a279 100644 --- a/packages/primitives/tooltip/src/tooltip-content.directive.ts +++ b/packages/primitives/tooltip/src/tooltip-content.directive.ts @@ -1,53 +1,356 @@ -import { NumberInput } from '@angular/cdk/coercion'; -import { computed, Directive, forwardRef, inject, input, numberAttribute, output, TemplateRef } from '@angular/core'; -import { getContentPosition, RdxPositionAlign, RdxPositionSide } from '@radix-ng/primitives/core'; -import { RdxTooltipContentToken } from './tooltip-content.token'; +import { BooleanInput, NumberInput } from '@angular/cdk/coercion'; +import { CdkConnectedOverlay, Overlay } from '@angular/cdk/overlay'; +import { + booleanAttribute, + computed, + DestroyRef, + Directive, + effect, + inject, + input, + numberAttribute, + OnInit, + output, + SimpleChange, + TemplateRef, + untracked +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + getAllPossibleConnectedPositions, + getContentPosition, + RDX_POSITIONING_DEFAULTS, + RdxPositionAlign, + RdxPositionSide, + RdxPositionSideAndAlignOffsets +} from '@radix-ng/primitives/core'; +import { filter, tap } from 'rxjs'; +import { injectTooltipRoot } from './tooltip-root.inject'; +import { RdxTooltipAttachDetachEvent } from './tooltip.types'; @Directive({ selector: '[rdxTooltipContent]', - providers: [{ provide: RdxTooltipContentToken, useExisting: forwardRef(() => RdxTooltipContentDirective) }] + hostDirectives: [ + CdkConnectedOverlay + ] }) -export class RdxTooltipContentDirective { +export class RdxTooltipContentDirective implements OnInit { /** @ignore */ - readonly templateRef = inject(TemplateRef); + private readonly rootDirective = injectTooltipRoot(); + /** @ignore */ + private readonly templateRef = inject(TemplateRef); + /** @ignore */ + private readonly overlay = inject(Overlay); + /** @ignore */ + private readonly destroyRef = inject(DestroyRef); + /** @ignore */ + private readonly connectedOverlay = inject(CdkConnectedOverlay); + + /** @ignore */ + readonly name = computed(() => `rdx-tooltip-trigger-${this.rootDirective.uniqueId()}`); /** - * The preferred side of the trigger to render against when open. Will be reversed when collisions occur and avoidCollisions is enabled. + * @description The preferred side of the trigger to render against when open. Will be reversed when collisions occur and avoidCollisions is enabled. + * @default top */ readonly side = input(RdxPositionSide.Top); - /** - * The distance in pixels from the trigger. + * @description The distance in pixels from the trigger. + * @default undefined */ - readonly sideOffset = input(0, { transform: numberAttribute }); - + readonly sideOffset = input(NaN, { + transform: numberAttribute + }); /** - * The preferred alignment against the trigger. May change when collisions occur. + * @description The preferred alignment against the trigger. May change when collisions occur. + * @default center */ readonly align = input(RdxPositionAlign.Center); + /** + * @description An offset in pixels from the "start" or "end" alignment options. + * @default undefined + */ + readonly alignOffset = input(NaN, { + transform: numberAttribute + }); /** - * An offset in pixels from the "start" or "end" alignment options. + * @description Whether to add some alternate positions of the content. + * @default false */ - readonly alignOffset = input(0, { transform: numberAttribute }); + readonly alternatePositionsDisabled = input(false, { transform: booleanAttribute }); - /** @ingore */ - readonly position = computed(() => - getContentPosition({ - side: this.side(), - align: this.align(), - sideOffset: this.sideOffset(), - alignOffset: this.alignOffset() - }) - ); + /** @description Whether to prevent `onOverlayEscapeKeyDown` handler from calling. */ + readonly onOverlayEscapeKeyDownDisabled = input(false, { transform: booleanAttribute }); + /** @description Whether to prevent `onOverlayOutsideClick` handler from calling. */ + readonly onOverlayOutsideClickDisabled = input(false, { transform: booleanAttribute }); /** - * Event handler called when the escape key is down. It can be prevented by calling event.preventDefault. + * @description Event handler called when the escape key is down. + * It can be prevented by setting `onOverlayEscapeKeyDownDisabled` input to `true`. + */ + readonly onOverlayEscapeKeyDown = output(); + /** + * @description Event handler called when a pointer event occurs outside the bounds of the component. + * It can be prevented by setting `onOverlayOutsideClickDisabled` input to `true`. */ - readonly onEscapeKeyDown = output(); + readonly onOverlayOutsideClick = output(); /** - * Event handler called when a pointer event occurs outside the bounds of the component. It can be prevented by calling event.preventDefault. + * @description Event handler called after the overlay is open + */ + readonly onOpen = output(); + /** + * @description Event handler called after the overlay is closed */ - readonly onPointerDownOutside = output(); + readonly onClosed = output(); + + /** @ingore */ + readonly positions = computed(() => this.computePositions()); + + constructor() { + this.onOriginChangeEffect(); + this.onPositionChangeEffect(); + } + + /** @ignore */ + ngOnInit() { + this.setScrollStrategy(); + this.setHasBackdrop(); + this.setDisableClose(); + this.onAttach(); + this.onDetach(); + this.connectKeydownEscape(); + this.connectOutsideClick(); + } + + /** @ignore */ + open() { + if (this.connectedOverlay.open) { + return; + } + const prevOpen = this.connectedOverlay.open; + this.connectedOverlay.open = true; + this.fireOverlayNgOnChanges('open', this.connectedOverlay.open, prevOpen); + } + + /** @ignore */ + close() { + if (!this.connectedOverlay.open) { + return; + } + const prevOpen = this.connectedOverlay.open; + this.connectedOverlay.open = false; + this.fireOverlayNgOnChanges('open', this.connectedOverlay.open, prevOpen); + } + + /** @ignore */ + positionChange() { + return this.connectedOverlay.positionChange.asObservable(); + } + + /** @ignore */ + private connectKeydownEscape() { + this.connectedOverlay.overlayKeydown + .asObservable() + .pipe( + filter( + () => + !this.onOverlayEscapeKeyDownDisabled() && + !this.rootDirective.rdxCdkEventService?.primitivePreventedFromCdkEvent( + this.rootDirective, + 'cdkOverlayEscapeKeyDown' + ) + ), + filter((event) => event.key === 'Escape'), + tap((event) => { + this.onOverlayEscapeKeyDown.emit(event); + }), + filter(() => !this.rootDirective.firstDefaultOpen()), + tap(() => { + this.rootDirective.handleClose(); + }), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(); + } + + /** @ignore */ + private connectOutsideClick() { + this.connectedOverlay.overlayOutsideClick + .asObservable() + .pipe( + filter( + () => + !this.onOverlayOutsideClickDisabled() && + !this.rootDirective.rdxCdkEventService?.primitivePreventedFromCdkEvent( + this.rootDirective, + 'cdkOverlayOutsideClick' + ) + ), + /** + * Handle the situation when an anchor is added and the anchor becomes the origin of the overlay + * hence the trigger will be considered the outside element + */ + filter((event) => { + return ( + !this.rootDirective.anchorDirective() || + !this.rootDirective + .triggerDirective() + .elementRef.nativeElement.contains(event.target as Element) + ); + }), + tap((event) => { + this.onOverlayOutsideClick.emit(event); + }), + filter(() => !this.rootDirective.firstDefaultOpen()), + tap(() => { + this.rootDirective.handleClose(); + }), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(); + } + + /** @ignore */ + private onAttach() { + this.connectedOverlay.attach + .asObservable() + .pipe( + tap(() => { + /** + * `this.onOpen.emit();` is being delegated to the rootDirective directive due to the opening animation + */ + this.rootDirective.attachDetachEvent.set(RdxTooltipAttachDetachEvent.ATTACH); + }), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(); + } + + /** @ignore */ + private onDetach() { + this.connectedOverlay.detach + .asObservable() + .pipe( + tap(() => { + /** + * `this.onClosed.emit();` is being delegated to the rootDirective directive due to the closing animation + */ + this.rootDirective.attachDetachEvent.set(RdxTooltipAttachDetachEvent.DETACH); + }), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(); + } + + /** @ignore */ + private setScrollStrategy() { + const prevScrollStrategy = this.connectedOverlay.scrollStrategy; + this.connectedOverlay.scrollStrategy = this.overlay.scrollStrategies.reposition(); + this.fireOverlayNgOnChanges('scrollStrategy', this.connectedOverlay.scrollStrategy, prevScrollStrategy); + } + + /** @ignore */ + private setHasBackdrop() { + const prevHasBackdrop = this.connectedOverlay.hasBackdrop; + this.connectedOverlay.hasBackdrop = false; + this.fireOverlayNgOnChanges('hasBackdrop', this.connectedOverlay.hasBackdrop, prevHasBackdrop); + } + + /** @ignore */ + private setDisableClose() { + const prevDisableClose = this.connectedOverlay.disableClose; + this.connectedOverlay.disableClose = true; + this.fireOverlayNgOnChanges('disableClose', this.connectedOverlay.disableClose, prevDisableClose); + } + + /** @ignore */ + private setOrigin(origin: CdkConnectedOverlay['origin']) { + const prevOrigin = this.connectedOverlay.origin; + this.connectedOverlay.origin = origin; + this.fireOverlayNgOnChanges('origin', this.connectedOverlay.origin, prevOrigin); + } + + /** @ignore */ + private setPositions(positions: CdkConnectedOverlay['positions']) { + const prevPositions = this.connectedOverlay.positions; + this.connectedOverlay.positions = positions; + this.fireOverlayNgOnChanges('positions', this.connectedOverlay.positions, prevPositions); + this.connectedOverlay.overlayRef?.updatePosition(); + } + + /** @ignore */ + private computePositions() { + const arrowHeight = this.rootDirective.arrowDirective()?.height() ?? 0; + const offsets: RdxPositionSideAndAlignOffsets = { + sideOffset: isNaN(this.sideOffset()) + ? arrowHeight || RDX_POSITIONING_DEFAULTS.offsets.side + : this.sideOffset(), + alignOffset: isNaN(this.alignOffset()) ? RDX_POSITIONING_DEFAULTS.offsets.align : this.alignOffset() + }; + const basePosition = getContentPosition({ + side: this.side(), + align: this.align(), + sideOffset: offsets.sideOffset, + alignOffset: offsets.alignOffset + }); + const positions = [basePosition]; + if (!this.alternatePositionsDisabled()) { + /** + * Alternate positions for better user experience along the X/Y axis (e.g. vertical/horizontal scrolling) + */ + const allPossibleConnectedPositions = getAllPossibleConnectedPositions(); + allPossibleConnectedPositions.forEach((_, key) => { + const sideAndAlignArray = key.split('|'); + if ( + (sideAndAlignArray[0] as RdxPositionSide) !== this.side() || + (sideAndAlignArray[1] as RdxPositionAlign) !== this.align() + ) { + positions.push( + getContentPosition({ + side: sideAndAlignArray[0] as RdxPositionSide, + align: sideAndAlignArray[1] as RdxPositionAlign, + sideOffset: offsets.sideOffset, + alignOffset: offsets.alignOffset + }) + ); + } + }); + } + return positions; + } + + private onOriginChangeEffect() { + effect(() => { + const origin = (this.rootDirective.anchorDirective() ?? this.rootDirective.triggerDirective()) + .overlayOrigin; + untracked(() => { + this.setOrigin(origin); + }); + }); + } + + /** @ignore */ + private onPositionChangeEffect() { + effect(() => { + const positions = this.positions(); + this.alternatePositionsDisabled(); + untracked(() => { + this.setPositions(positions); + }); + }); + } + + /** @ignore */ + private fireOverlayNgOnChanges( + input: K, + currentValue: V, + previousValue: V, + firstChange = false + ) { + this.connectedOverlay.ngOnChanges({ + [input]: new SimpleChange(previousValue, currentValue, firstChange) + }); + } } diff --git a/packages/primitives/tooltip/src/tooltip-content.token.ts b/packages/primitives/tooltip/src/tooltip-content.token.ts deleted file mode 100644 index 6e68f64e..00000000 --- a/packages/primitives/tooltip/src/tooltip-content.token.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { InjectionToken } from '@angular/core'; -import { RdxTooltipContentDirective } from './tooltip-content.directive'; - -export const RdxTooltipContentToken = new InjectionToken('RdxTooltipContentToken'); diff --git a/packages/primitives/tooltip/src/tooltip-root.directive.ts b/packages/primitives/tooltip/src/tooltip-root.directive.ts index 69455120..0ebe2b4b 100644 --- a/packages/primitives/tooltip/src/tooltip-root.directive.ts +++ b/packages/primitives/tooltip/src/tooltip-root.directive.ts @@ -1,363 +1,421 @@ import { BooleanInput, NumberInput } from '@angular/cdk/coercion'; -import { ConnectedPosition, Overlay, OverlayRef, PositionStrategy } from '@angular/cdk/overlay'; -import { TemplatePortal } from '@angular/cdk/portal'; -import { isPlatformBrowser } from '@angular/common'; import { + afterNextRender, booleanAttribute, computed, contentChild, DestroyRef, Directive, effect, - forwardRef, inject, - InjectionToken, input, numberAttribute, - OnInit, - output, - PLATFORM_ID, signal, untracked, - ViewContainerRef, - ViewRef + ViewContainerRef } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { injectDocument, injectWindow } from '@radix-ng/primitives/core'; -import { asyncScheduler, filter, take } from 'rxjs'; -import { RdxTooltipContentToken } from './tooltip-content.token'; +import { debounce, map, Subject, tap, timer } from 'rxjs'; +import { RdxTooltipAnchorDirective } from './tooltip-anchor.directive'; +import { RdxTooltipAnchorToken } from './tooltip-anchor.token'; +import { RdxTooltipArrowToken } from './tooltip-arrow.token'; +import { RdxTooltipCloseToken } from './tooltip-close.token'; +import { RdxTooltipContentAttributesToken } from './tooltip-content-attributes.token'; +import { RdxTooltipContentDirective } from './tooltip-content.directive'; import { RdxTooltipTriggerDirective } from './tooltip-trigger.directive'; -import { injectTooltipConfig } from './tooltip.config'; -import { RdxTooltipState } from './tooltip.types'; - -export const RdxTooltipRootToken = new InjectionToken('RdxTooltipRootToken'); +import { + RdxTooltipAction, + RdxTooltipAnimationStatus, + RdxTooltipAttachDetachEvent, + RdxTooltipState +} from './tooltip.types'; +import { injectRdxCdkEventService } from './utils/cdk-event.service'; -export function injectTooltipRoot(): RdxTooltipRootDirective { - return inject(RdxTooltipRootToken); -} +let nextId = 0; @Directive({ selector: '[rdxTooltipRoot]', - providers: [ - { - provide: RdxTooltipRootToken, - useExisting: forwardRef(() => RdxTooltipRootDirective) - } - ], exportAs: 'rdxTooltipRoot' }) -export class RdxTooltipRootDirective implements OnInit { - /** @ignore */ - private readonly viewContainerRef = inject(ViewContainerRef); - /** @ignore */ - private readonly destroyRef = inject(DestroyRef); - /** @ignore */ - private readonly overlay = inject(Overlay); - /** @ignore */ - private readonly platformId = inject(PLATFORM_ID); +export class RdxTooltipRootDirective { /** @ignore */ - private readonly document = injectDocument(); + readonly uniqueId = signal(++nextId); /** @ignore */ - private readonly window = injectWindow(); - /** @ignore */ - readonly tooltipConfig = injectTooltipConfig(); + readonly name = computed(() => `rdx-tooltip-root-${this.uniqueId()}`); /** - * The open state of the tooltip when it is initially rendered. Use when you do not need to control its open state. + * @description The anchor directive that comes form outside the tooltip rootDirective + * @default undefined + */ + readonly anchor = input(void 0); + /** + * @description The open state of the tooltip when it is initially rendered. Use when you do not need to control its open state. + * @default false */ readonly defaultOpen = input(false, { transform: booleanAttribute }); - /** - * The controlled open state of the tooltip. Must be used in conjunction with onOpenChange. + * @description The controlled state of the tooltip. `open` input take precedence of `defaultOpen` input. + * @default undefined */ readonly open = input(void 0, { transform: booleanAttribute }); - /** - * Override the duration given to the configuration to customise the open delay for a specific tooltip. + * To customise the open delay for a specific tooltip. */ - readonly delayDuration = input(this.tooltipConfig.delayDuration, { + readonly openDelay = input(500, { transform: numberAttribute }); - - /** @ignore */ - readonly disableHoverableContent = input( - this.tooltipConfig.disableHoverableContent ?? false, - { transform: booleanAttribute } - ); - /** - * Event handler called when the open state of the tooltip changes. + * To customise the close delay for a specific tooltip. + */ + readonly closeDelay = input(200, { + transform: numberAttribute + }); + /** + * @description Whether to control the state of the tooltip from external. Use in conjunction with `open` input. + * @default undefined + */ + readonly externalControl = input(void 0, { transform: booleanAttribute }); + /** + * @description Whether to take into account CSS opening/closing animations. + * @default false */ - readonly onOpenChange = output(); + readonly cssAnimation = input(false, { transform: booleanAttribute }); + /** + * @description Whether to take into account CSS opening animations. `cssAnimation` input must be set to 'true' + * @default false + */ + readonly cssOpeningAnimation = input(false, { transform: booleanAttribute }); + /** + * @description Whether to take into account CSS closing animations. `cssAnimation` input must be set to 'true' + * @default false + */ + readonly cssClosingAnimation = input(false, { transform: booleanAttribute }); /** @ignore */ - readonly isOpen = signal(this.defaultOpen()); + readonly cssAnimationStatus = signal(null); + /** @ignore */ - readonly isOpenDelayed = signal(true); + readonly contentDirective = contentChild.required(RdxTooltipContentDirective); /** @ignore */ - readonly wasOpenDelayed = signal(false); + readonly triggerDirective = contentChild.required(RdxTooltipTriggerDirective); /** @ignore */ - readonly state = computed(() => { - const currentIsOpen = this.isOpen(); - const currentWasOpenDelayed = this.wasOpenDelayed(); - - if (currentIsOpen) { - return currentWasOpenDelayed ? 'delayed-open' : 'instant-open'; - } - - return 'closed'; - }); + readonly arrowDirective = contentChild(RdxTooltipArrowToken); /** @ignore */ - readonly tooltipContentDirective = contentChild.required(RdxTooltipContentToken); + readonly closeDirective = contentChild(RdxTooltipCloseToken); /** @ignore */ - readonly tooltipTriggerDirective = contentChild.required(RdxTooltipTriggerDirective); - + readonly contentAttributesComponent = contentChild(RdxTooltipContentAttributesToken); /** @ignore */ - private openTimer = 0; + private readonly internalAnchorDirective = contentChild(RdxTooltipAnchorToken); + /** @ignore */ - private skipDelayTimer = 0; + readonly viewContainerRef = inject(ViewContainerRef); /** @ignore */ - private overlayRef?: OverlayRef; + readonly rdxCdkEventService = injectRdxCdkEventService(); /** @ignore */ - private instance?: ViewRef; + readonly destroyRef = inject(DestroyRef); + /** @ignore */ - private portal: TemplatePortal; + readonly state = signal(RdxTooltipState.CLOSED); + /** @ignore */ - private isControlledExternally = false; + readonly attachDetachEvent = signal(RdxTooltipAttachDetachEvent.DETACH); /** @ignore */ - ngOnInit(): void { - if (this.defaultOpen()) { - this.handleOpen(); - } + private readonly isFirstDefaultOpen = signal(false); - this.isControlledExternally = this.open() !== undefined; - } + /** @ignore */ + readonly anchorDirective = computed(() => this.internalAnchorDirective() ?? this.anchor()); /** @ignore */ - onTriggerEnter(): void { - if (this.isControlledExternally) { - return; - } + readonly actionSubject$ = new Subject(); - if (this.isOpenDelayed()) { - this.handleDelayedOpen(); - } else { - this.handleOpen(); - } + constructor() { + this.rdxCdkEventService?.registerPrimitive(this); + this.destroyRef.onDestroy(() => this.rdxCdkEventService?.deregisterPrimitive(this)); + this.actionSubscription(); + this.onStateChangeEffect(); + this.onCssAnimationStatusChangeChangeEffect(); + this.onOpenChangeEffect(); + this.onIsFirstDefaultOpenChangeEffect(); + this.onAnchorChangeEffect(); + this.emitOpenOrClosedEventEffect(); + afterNextRender({ + write: () => { + if (this.defaultOpen() && !this.open()) { + this.isFirstDefaultOpen.set(true); + } + } + }); } /** @ignore */ - onTriggerLeave(): void { - this.clearTimeout(this.openTimer); - this.handleClose(); + getAnimationParamsSnapshot() { + return { + cssAnimation: this.cssAnimation(), + cssOpeningAnimation: this.cssOpeningAnimation(), + cssClosingAnimation: this.cssClosingAnimation(), + cssAnimationStatus: this.cssAnimationStatus(), + attachDetachEvent: this.attachDetachEvent(), + state: this.state(), + canEmitOnOpenOrOnClosed: this.canEmitOnOpenOrOnClosed() + }; } /** @ignore */ - onOpen(): void { - this.clearTimeout(this.skipDelayTimer); - this.isOpenDelayed.set(false); + controlledExternally() { + return this.externalControl; } /** @ignore */ - onClose(): void { - this.clearTimeout(this.skipDelayTimer); - - if (isPlatformBrowser(this.platformId)) { - this.skipDelayTimer = this.window.setTimeout(() => { - this.isOpenDelayed.set(true); - }, this.tooltipConfig.skipDelayDuration); - } + firstDefaultOpen() { + return this.isFirstDefaultOpen(); } /** @ignore */ handleOpen(): void { - if (this.isControlledExternally) { + if (this.externalControl()) { return; } - - this.wasOpenDelayed.set(false); - this.setOpen(true); + this.actionSubject$.next(RdxTooltipAction.OPEN); } /** @ignore */ - handleClose(): void { - if (this.isControlledExternally) { + handleClose(closeButton?: boolean): void { + if (this.isFirstDefaultOpen()) { + this.isFirstDefaultOpen.set(false); + } + if (!closeButton && this.externalControl()) { return; } - - this.clearTimeout(this.openTimer); - this.setOpen(false); + this.actionSubject$.next(RdxTooltipAction.CLOSE); } /** @ignore */ - private handleOverlayKeydown(): void { - if (!this.overlayRef) { + handleToggle(): void { + if (this.externalControl()) { return; } + this.isOpen() ? this.handleClose() : this.handleOpen(); + } - this.overlayRef - .keydownEvents() - .pipe( - filter((event) => event.key === 'Escape'), - takeUntilDestroyed(this.destroyRef) - ) - .subscribe((event) => { - this.tooltipContentDirective().onEscapeKeyDown.emit(event); - - if (!event.defaultPrevented) { - this.handleClose(); - } - }); + /** @ignore */ + isOpen(state?: RdxTooltipState) { + return (state ?? this.state()) === RdxTooltipState.OPEN; } /** @ignore */ - private handlePointerDownOutside(): void { - if (!this.overlayRef) { + private setState(state = RdxTooltipState.CLOSED): void { + if (state === this.state()) { return; } - - this.overlayRef - .outsidePointerEvents() - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((event) => this.tooltipContentDirective().onPointerDownOutside.emit(event)); + this.state.set(state); } /** @ignore */ - private handleDelayedOpen(): void { - this.clearTimeout(this.openTimer); - - if (isPlatformBrowser(this.platformId)) { - this.openTimer = this.window.setTimeout(() => { - this.wasOpenDelayed.set(true); - this.setOpen(true); - }, this.delayDuration()); + private openContent(): void { + this.contentDirective().open(); + if (!this.cssAnimation() || !this.cssOpeningAnimation()) { + this.cssAnimationStatus.set(null); } } /** @ignore */ - private setOpen(open = false): void { - if (open) { - this.onOpen(); - - this.document.dispatchEvent(new CustomEvent('tooltip.open')); - } else { - this.onClose(); + private closeContent(): void { + this.contentDirective().close(); + if (!this.cssAnimation() || !this.cssClosingAnimation()) { + this.cssAnimationStatus.set(null); } - - this.isOpen.set(open); - this.onOpenChange.emit(open); } /** @ignore */ - private createOverlayRef(): OverlayRef { - if (this.overlayRef) { - return this.overlayRef; - } - - this.overlayRef = this.overlay.create({ - direction: undefined, - positionStrategy: this.getPositionStrategy(this.tooltipContentDirective().position()), - scrollStrategy: this.overlay.scrollStrategies.close() - }); - - this.overlayRef - .detachments() - .pipe(take(1), takeUntilDestroyed(this.destroyRef)) - .subscribe(() => this.detach()); - - this.handleOverlayKeydown(); - this.handlePointerDownOutside(); - - return this.overlayRef; + private emitOnOpen(): void { + this.contentDirective().onOpen.emit(); } /** @ignore */ - private show(): void { - this.overlayRef = this.createOverlayRef(); - - this.detach(); - - this.portal = - this.portal || - new TemplatePortal(this.tooltipContentDirective().templateRef, this.viewContainerRef, { - state: this.state, - side: this.tooltipContentDirective().side - }); - - this.instance = this.overlayRef.attach(this.portal); + private emitOnClosed(): void { + this.contentDirective().onClosed.emit(); } /** @ignore */ - private detach(): void { - if (this.overlayRef?.hasAttached()) { - this.overlayRef.detach(); - } + private ifOpenOrCloseWithoutAnimations(state: RdxTooltipState) { + return ( + !this.contentAttributesComponent() || + !this.cssAnimation() || + (this.cssAnimation() && !this.cssClosingAnimation() && state === RdxTooltipState.CLOSED) || + (this.cssAnimation() && !this.cssOpeningAnimation() && state === RdxTooltipState.OPEN) || + // !this.cssAnimationStatus() || + (this.cssOpeningAnimation() && + state === RdxTooltipState.OPEN && + [RdxTooltipAnimationStatus.OPEN_STARTED].includes(this.cssAnimationStatus()!)) || + (this.cssClosingAnimation() && + state === RdxTooltipState.CLOSED && + [RdxTooltipAnimationStatus.CLOSED_STARTED].includes(this.cssAnimationStatus()!)) + ); } /** @ignore */ - private hide(): void { - if (this.isControlledExternally && this.open()) { - return; - } + private ifOpenOrCloseWithAnimations(cssAnimationStatus: RdxTooltipAnimationStatus | null) { + return ( + this.contentAttributesComponent() && + this.cssAnimation() && + cssAnimationStatus && + ((this.cssOpeningAnimation() && + this.state() === RdxTooltipState.OPEN && + [RdxTooltipAnimationStatus.OPEN_ENDED].includes(cssAnimationStatus)) || + (this.cssClosingAnimation() && + this.state() === RdxTooltipState.CLOSED && + [RdxTooltipAnimationStatus.CLOSED_ENDED].includes(cssAnimationStatus))) + ); + } - asyncScheduler.schedule(() => { - this.instance?.destroy(); - }, this.tooltipConfig.hideDelayDuration ?? 0); + /** @ignore */ + private openOrClose(state: RdxTooltipState) { + const isOpen = this.isOpen(state); + isOpen ? this.openContent() : this.closeContent(); } /** @ignore */ - private getPositionStrategy(connectedPosition: ConnectedPosition): PositionStrategy { - return this.overlay - .position() - .flexibleConnectedTo(this.tooltipTriggerDirective().elementRef) - .withFlexibleDimensions(false) - .withPositions([ - connectedPosition - ]) - .withLockedPosition(); + private emitOnOpenOrOnClosed(state: RdxTooltipState) { + this.isOpen(state) + ? this.attachDetachEvent() === RdxTooltipAttachDetachEvent.ATTACH && this.emitOnOpen() + : this.attachDetachEvent() === RdxTooltipAttachDetachEvent.DETACH && this.emitOnClosed(); } /** @ignore */ - private clearTimeout(timeoutId: number): void { - if (isPlatformBrowser(this.platformId)) { - this.window.clearTimeout(timeoutId); - } + private canEmitOnOpenOrOnClosed() { + return ( + !this.cssAnimation() || + (!this.cssOpeningAnimation() && this.state() === RdxTooltipState.OPEN) || + (this.cssOpeningAnimation() && + this.state() === RdxTooltipState.OPEN && + this.cssAnimationStatus() === RdxTooltipAnimationStatus.OPEN_ENDED) || + (!this.cssClosingAnimation() && this.state() === RdxTooltipState.CLOSED) || + (this.cssClosingAnimation() && + this.state() === RdxTooltipState.CLOSED && + this.cssAnimationStatus() === RdxTooltipAnimationStatus.CLOSED_ENDED) + ); } /** @ignore */ - private readonly onIsOpenChangeEffect = effect(() => { - const isOpen = this.isOpen(); + private onStateChangeEffect() { + let isFirst = true; + effect(() => { + const state = this.state(); + untracked(() => { + if (isFirst) { + isFirst = false; + return; + } + if (!this.ifOpenOrCloseWithoutAnimations(state)) { + return; + } + this.openOrClose(state); + }); + }, {}); + } - untracked(() => { - if (isOpen) { - this.show(); - } else { - this.hide(); - } + /** @ignore */ + private onCssAnimationStatusChangeChangeEffect() { + let isFirst = true; + effect(() => { + const cssAnimationStatus = this.cssAnimationStatus(); + untracked(() => { + if (isFirst) { + isFirst = false; + return; + } + if (!this.ifOpenOrCloseWithAnimations(cssAnimationStatus)) { + return; + } + this.openOrClose(this.state()); + }); }); - }); + } /** @ignore */ - private readonly onPositionChangeEffect = effect(() => { - const position = this.tooltipContentDirective().position(); - - if (this.overlayRef) { - const positionStrategy = this.getPositionStrategy(position); + private emitOpenOrClosedEventEffect() { + let isFirst = true; + effect(() => { + this.attachDetachEvent(); + this.cssAnimationStatus(); + untracked(() => { + if (isFirst) { + isFirst = false; + return; + } + const canEmitOpenClose = untracked(() => this.canEmitOnOpenOrOnClosed()); + if (!canEmitOpenClose) { + return; + } + this.emitOnOpenOrOnClosed(this.state()); + }); + }); + } - this.overlayRef.updatePositionStrategy(positionStrategy); - } - }); + /** @ignore */ + private onOpenChangeEffect() { + effect(() => { + const open = this.open(); + untracked(() => { + this.setState(open ? RdxTooltipState.OPEN : RdxTooltipState.CLOSED); + }); + }); + } /** @ignore */ - private readonly onOpenChangeEffect = effect(() => { - const currentOpen = this.open(); - this.isControlledExternally = currentOpen !== undefined; + private onIsFirstDefaultOpenChangeEffect() { + const effectRef = effect(() => { + const defaultOpen = this.defaultOpen(); + untracked(() => { + if (!defaultOpen || this.open()) { + effectRef.destroy(); + return; + } + this.handleOpen(); + }); + }); + } - untracked(() => { - if (this.isControlledExternally) { - this.setOpen(currentOpen); - } + /** @ignore */ + private onAnchorChangeEffect = () => { + effect(() => { + const anchor = this.anchor(); + untracked(() => { + if (anchor) { + anchor.setRoot(this); + } + }); }); - }); + }; + + /** @ignore */ + private actionSubscription() { + this.actionSubject$ + .asObservable() + .pipe( + map((action) => { + console.log(action); + switch (action) { + case RdxTooltipAction.OPEN: + return { action, duration: this.openDelay() }; + case RdxTooltipAction.CLOSE: + return { action, duration: this.closeDelay() }; + } + }), + debounce((config) => timer(config.duration)), + tap((config) => { + switch (config.action) { + case RdxTooltipAction.OPEN: + this.setState(RdxTooltipState.OPEN); + break; + case RdxTooltipAction.CLOSE: + this.setState(RdxTooltipState.CLOSED); + break; + } + }), + takeUntilDestroyed() + ) + .subscribe(); + } } diff --git a/packages/primitives/tooltip-v2/src/tooltip-root.inject.ts b/packages/primitives/tooltip/src/tooltip-root.inject.ts similarity index 100% rename from packages/primitives/tooltip-v2/src/tooltip-root.inject.ts rename to packages/primitives/tooltip/src/tooltip-root.inject.ts diff --git a/packages/primitives/tooltip/src/tooltip-trigger.directive.ts b/packages/primitives/tooltip/src/tooltip-trigger.directive.ts index 0218f52c..193b4e2e 100644 --- a/packages/primitives/tooltip/src/tooltip-trigger.directive.ts +++ b/packages/primitives/tooltip/src/tooltip-trigger.directive.ts @@ -1,74 +1,57 @@ -import { Directive, ElementRef, inject } from '@angular/core'; -import { injectTooltipRoot } from './tooltip-root.directive'; +import { CdkOverlayOrigin } from '@angular/cdk/overlay'; +import { computed, Directive, ElementRef, inject } from '@angular/core'; +import { injectTooltipRoot } from './tooltip-root.inject'; @Directive({ selector: '[rdxTooltipTrigger]', + hostDirectives: [CdkOverlayOrigin], host: { - '[attr.data-state]': 'tooltipRoot.state()', - '(pointermove)': 'onPointerMove($event)', - '(pointerleave)': 'onPointerLeave()', - '(pointerdown)': 'onPointerDown()', - '(focus)': 'onFocus()', - '(blur)': 'onBlur()', - '(click)': 'onClick()' + type: 'button', + '[attr.id]': 'name()', + '[attr.aria-haspopup]': '"dialog"', + '[attr.aria-expanded]': 'rootDirective.isOpen()', + '[attr.aria-controls]': 'rootDirective.contentDirective().name()', + '[attr.data-state]': 'rootDirective.state()', + '(pointerenter)': 'pointerenter()', + '(pointerleave)': 'pointerleave()', + '(focus)': 'focus()', + '(blur)': 'blur()', + '(click)': 'click()' } }) export class RdxTooltipTriggerDirective { /** @ignore */ - readonly tooltipRoot = injectTooltipRoot(); + protected readonly rootDirective = injectTooltipRoot(); /** @ignore */ - readonly elementRef = inject>(ElementRef); - - /** @ignore */ - private isPointerDown = false; + readonly elementRef = inject>(ElementRef); /** @ignore */ - private isPointerInside = false; + readonly overlayOrigin = inject(CdkOverlayOrigin); /** @ignore */ - onPointerMove(event: PointerEvent): void { - if (event.pointerType === 'touch') { - return; - } - - if (!this.isPointerInside) { - this.tooltipRoot.onTriggerEnter(); - this.isPointerInside = true; - } - } + readonly name = computed(() => `rdx-tooltip-trigger-${this.rootDirective.uniqueId()}`); /** @ignore */ - onPointerLeave(): void { - this.isPointerInside = false; - this.tooltipRoot.onTriggerLeave(); + pointerenter(): void { + this.rootDirective.handleOpen(); } /** @ignore */ - onPointerDown(): void { - this.isPointerDown = true; - - this.elementRef.nativeElement.addEventListener( - 'pointerup', - () => { - this.isPointerDown = false; - }, - { once: true } - ); + pointerleave(): void { + this.rootDirective.handleClose(); } /** @ignore */ - onFocus(): void { - if (!this.isPointerDown) { - this.tooltipRoot.handleOpen(); - } + focus(): void { + this.rootDirective.handleOpen(); } /** @ignore */ - onBlur(): void { - this.tooltipRoot.handleClose(); + blur(): void { + this.rootDirective.handleClose(); } /** @ignore */ - onClick(): void { - this.tooltipRoot.handleClose(); + click(): void { + this.rootDirective.handleClose(); } } diff --git a/packages/primitives/tooltip/src/tooltip.config.ts b/packages/primitives/tooltip/src/tooltip.config.ts deleted file mode 100644 index e3f45844..00000000 --- a/packages/primitives/tooltip/src/tooltip.config.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { inject, InjectionToken, Provider } from '@angular/core'; -import { RdxTooltipConfig } from './tooltip.types'; - -export const defaultTooltipConfig: RdxTooltipConfig = { - delayDuration: 700, - skipDelayDuration: 300 -}; - -export const RdxTooltipConfigToken = new InjectionToken('RdxTooltipConfigToken'); - -export function provideRdxTooltipConfig(config: Partial): Provider[] { - return [ - { - provide: RdxTooltipConfigToken, - useValue: { ...defaultTooltipConfig, ...config } - } - ]; -} - -export function injectTooltipConfig(): RdxTooltipConfig { - return inject(RdxTooltipConfigToken, { optional: true }) ?? defaultTooltipConfig; -} diff --git a/packages/primitives/tooltip/src/tooltip.types.ts b/packages/primitives/tooltip/src/tooltip.types.ts index 3cf0af43..ded4450f 100644 --- a/packages/primitives/tooltip/src/tooltip.types.ts +++ b/packages/primitives/tooltip/src/tooltip.types.ts @@ -1,8 +1,21 @@ -export type RdxTooltipConfig = { - delayDuration: number; - skipDelayDuration: number; - disableHoverableContent?: boolean; - hideDelayDuration?: number; -}; +export enum RdxTooltipState { + OPEN = 'open', + CLOSED = 'closed' +} -export type RdxTooltipState = 'delayed-open' | 'instant-open' | 'closed'; +export enum RdxTooltipAction { + OPEN = 'open', + CLOSE = 'close' +} + +export enum RdxTooltipAttachDetachEvent { + ATTACH = 'attach', + DETACH = 'detach' +} + +export enum RdxTooltipAnimationStatus { + OPEN_STARTED = 'open_started', + OPEN_ENDED = 'open_ended', + CLOSED_STARTED = 'closed_started', + CLOSED_ENDED = 'closed_ended' +} diff --git a/packages/primitives/tooltip-v2/src/utils/cdk-event.service.ts b/packages/primitives/tooltip/src/utils/cdk-event.service.ts similarity index 100% rename from packages/primitives/tooltip-v2/src/utils/cdk-event.service.ts rename to packages/primitives/tooltip/src/utils/cdk-event.service.ts diff --git a/packages/primitives/tooltip-v2/src/utils/constants.ts b/packages/primitives/tooltip/src/utils/constants.ts similarity index 100% rename from packages/primitives/tooltip-v2/src/utils/constants.ts rename to packages/primitives/tooltip/src/utils/constants.ts diff --git a/packages/primitives/tooltip-v2/src/utils/types.ts b/packages/primitives/tooltip/src/utils/types.ts similarity index 100% rename from packages/primitives/tooltip-v2/src/utils/types.ts rename to packages/primitives/tooltip/src/utils/types.ts diff --git a/packages/primitives/tooltip-v2/stories/tooltip-anchor.component.ts b/packages/primitives/tooltip/stories/tooltip-anchor.component.ts similarity index 100% rename from packages/primitives/tooltip-v2/stories/tooltip-anchor.component.ts rename to packages/primitives/tooltip/stories/tooltip-anchor.component.ts diff --git a/packages/primitives/tooltip-v2/stories/tooltip-animations.component.ts b/packages/primitives/tooltip/stories/tooltip-animations.component.ts similarity index 100% rename from packages/primitives/tooltip-v2/stories/tooltip-animations.component.ts rename to packages/primitives/tooltip/stories/tooltip-animations.component.ts diff --git a/packages/primitives/tooltip-v2/stories/tooltip-default.component.ts b/packages/primitives/tooltip/stories/tooltip-default.component.ts similarity index 100% rename from packages/primitives/tooltip-v2/stories/tooltip-default.component.ts rename to packages/primitives/tooltip/stories/tooltip-default.component.ts diff --git a/packages/primitives/tooltip/stories/tooltip-events.components.ts b/packages/primitives/tooltip/stories/tooltip-events.components.ts index a8ad324f..27df8db0 100644 --- a/packages/primitives/tooltip/stories/tooltip-events.components.ts +++ b/packages/primitives/tooltip/stories/tooltip-events.components.ts @@ -1,157 +1,84 @@ -import { Component, ElementRef, viewChild } from '@angular/core'; -import { LucideAngularModule, Plus } from 'lucide-angular'; -import { RdxTooltipModule } from '../index'; +import { Component, viewChild } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RdxPositionAlign, RdxPositionSide } from '@radix-ng/primitives/core'; +import { LucideAngularModule, MountainSnow, TriangleAlert, X } from 'lucide-angular'; +import { RdxTooltipModule, RdxTooltipRootDirective } from '../index'; +import { RdxTooltipContentAttributesComponent } from '../src/tooltip-content-attributes.component'; +import { provideRdxCdkEventService } from '../src/utils/cdk-event.service'; +import { containerAlert } from './utils/constants'; +import { OptionPanelBase } from './utils/option-panel-base.class'; +import styles from './utils/styles.constants'; +import { WithOptionPanelComponent } from './utils/with-option-panel.component'; @Component({ selector: 'rdx-tooltip-events', + providers: [provideRdxCdkEventService()], imports: [ RdxTooltipModule, - LucideAngularModule + LucideAngularModule, + FormsModule, + RdxTooltipContentAttributesComponent, + WithOptionPanelComponent ], styles: ` - .container { - height: 150px; - display: flex; - justify-content: center; - align-items: center; - } - - /* reset */ - button { - all: unset; - } - - .TooltipContent { - border-radius: 4px; - padding: 10px 15px; - font-size: 15px; - line-height: 1; - color: var(--violet-11); - background-color: white; - box-shadow: - hsl(206 22% 7% / 35%) 0px 10px 38px -10px, - hsl(206 22% 7% / 20%) 0px 10px 20px -15px; - user-select: none; - animation-duration: 400ms; - animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); - will-change: transform, opacity; - } - .TooltipContent[data-state='delayed-open'][data-side='top'] { - animation-name: slideDownAndFade; - } - .TooltipContent[data-state='delayed-open'][data-side='right'] { - animation-name: slideLeftAndFade; - } - .TooltipContent[data-state='delayed-open'][data-side='bottom'] { - animation-name: slideUpAndFade; - } - .TooltipContent[data-state='delayed-open'][data-side='left'] { - animation-name: slideRightAndFade; - } - - .TooltipArrow { - fill: white; - } - - .IconButton { - font-family: inherit; - border-radius: 100%; - height: 35px; - width: 35px; - display: inline-flex; - align-items: center; - justify-content: center; - color: var(--violet-11); - background-color: white; - box-shadow: 0 2px 10px var(--black-a7); - } - .IconButton:hover { - background-color: var(--violet-3); - } - .IconButton:focus { - box-shadow: 0 0 0 2px black; - } - - @keyframes slideUpAndFade { - from { - opacity: 0; - transform: translateY(2px); - } - to { - opacity: 1; - transform: translateY(0); - } - } - - @keyframes slideRightAndFade { - from { - opacity: 0; - transform: translateX(-2px); - } - to { - opacity: 1; - transform: translateX(0); - } - } - - @keyframes slideDownAndFade { - from { - opacity: 0; - transform: translateY(-2px); - } - to { - opacity: 1; - transform: translateY(0); - } - } - - @keyframes slideLeftAndFade { - from { - opacity: 0; - transform: translateX(2px); - } - to { - opacity: 1; - transform: translateX(0); - } - } + ${styles()} `, template: ` -
- - + +
+ + {{ containerAlert }} +
+
+ + - -
- Add to library -
-
-
-
-
+ +
+ + Add to library +
+
+
+
+
+
ID: {{ rootDirective()?.uniqueId() }}
+ ` }) -export class RdxTooltipEventsComponent { - private readonly triggerElement = viewChild>('triggerElement'); - - readonly PlusIcon = Plus; - - onEscapeKeyDown(event: KeyboardEvent) { - event.preventDefault(); - } +export class RdxTooltipEventsComponent extends OptionPanelBase { + readonly rootDirective = viewChild(RdxTooltipRootDirective); - onPointerDownOutside(event: MouseEvent): void { - if (event.target === this.triggerElement()?.nativeElement) { - event.stopPropagation(); - } + readonly MountainSnowIcon = MountainSnow; + readonly XIcon = X; - event.preventDefault(); - } + protected readonly sides = RdxPositionSide; + protected readonly aligns = RdxPositionAlign; + protected readonly containerAlert = containerAlert; + protected readonly TriangleAlert = TriangleAlert; } diff --git a/packages/primitives/tooltip-v2/stories/tooltip-initially-open.component.ts b/packages/primitives/tooltip/stories/tooltip-initially-open.component.ts similarity index 100% rename from packages/primitives/tooltip-v2/stories/tooltip-initially-open.component.ts rename to packages/primitives/tooltip/stories/tooltip-initially-open.component.ts diff --git a/packages/primitives/tooltip-v2/stories/tooltip-multiple.component.ts b/packages/primitives/tooltip/stories/tooltip-multiple.component.ts similarity index 100% rename from packages/primitives/tooltip-v2/stories/tooltip-multiple.component.ts rename to packages/primitives/tooltip/stories/tooltip-multiple.component.ts diff --git a/packages/primitives/tooltip/stories/tooltip-positioning.component.ts b/packages/primitives/tooltip/stories/tooltip-positioning.component.ts index 5bc717b5..8f19d15b 100644 --- a/packages/primitives/tooltip/stories/tooltip-positioning.component.ts +++ b/packages/primitives/tooltip/stories/tooltip-positioning.component.ts @@ -1,177 +1,122 @@ -import { Component } from '@angular/core'; +import { Component, signal, viewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { RdxPositionAlign, RdxPositionSide } from '@radix-ng/primitives/core'; -import { LucideAngularModule, Plus } from 'lucide-angular'; -import { RdxTooltipModule } from '../index'; +import { LucideAngularModule, MountainSnow, TriangleAlert, X } from 'lucide-angular'; +import { RdxTooltipModule, RdxTooltipRootDirective } from '../index'; +import { RdxTooltipContentAttributesComponent } from '../src/tooltip-content-attributes.component'; +import { provideRdxCdkEventService } from '../src/utils/cdk-event.service'; +import { containerAlert } from './utils/constants'; +import { OptionPanelBase } from './utils/option-panel-base.class'; +import styles from './utils/styles.constants'; +import { WithOptionPanelComponent } from './utils/with-option-panel.component'; @Component({ selector: 'rdx-tooltip-positioning', + providers: [provideRdxCdkEventService()], imports: [ FormsModule, RdxTooltipModule, - LucideAngularModule + LucideAngularModule, + RdxTooltipContentAttributesComponent, + WithOptionPanelComponent ], - styles: ` - .container { - height: 150px; - display: flex; - justify-content: center; - align-items: center; - } - - /* reset */ - button { - all: unset; - } - - .TooltipContent { - border-radius: 4px; - padding: 10px 15px; - font-size: 15px; - line-height: 1; - color: var(--violet-11); - background-color: white; - box-shadow: - hsl(206 22% 7% / 35%) 0px 10px 38px -10px, - hsl(206 22% 7% / 20%) 0px 10px 20px -15px; - user-select: none; - animation-duration: 400ms; - animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); - will-change: transform, opacity; - } - .TooltipContent[data-state='delayed-open'][data-side='top'] { - animation-name: slideDownAndFade; - } - .TooltipContent[data-state='delayed-open'][data-side='right'] { - animation-name: slideLeftAndFade; - } - .TooltipContent[data-state='delayed-open'][data-side='bottom'] { - animation-name: slideUpAndFade; - } - .TooltipContent[data-state='delayed-open'][data-side='left'] { - animation-name: slideRightAndFade; - } - - .TooltipArrow { - fill: white; - } - - .IconButton { - font-family: inherit; - border-radius: 100%; - height: 35px; - width: 35px; - display: inline-flex; - align-items: center; - justify-content: center; - color: var(--violet-11); - background-color: white; - box-shadow: 0 2px 10px var(--black-a7); - } - .IconButton:hover { - background-color: var(--violet-3); - } - .IconButton:focus { - box-shadow: 0 0 0 2px black; - } - - @keyframes slideUpAndFade { - from { - opacity: 0; - transform: translateY(2px); - } - to { - opacity: 1; - transform: translateY(0); - } - } - - @keyframes slideRightAndFade { - from { - opacity: 0; - transform: translateX(-2px); - } - to { - opacity: 1; - transform: translateX(0); - } - } - - @keyframes slideDownAndFade { - from { - opacity: 0; - transform: translateY(-2px); - } - to { - opacity: 1; - transform: translateY(0); - } - } - - @keyframes slideLeftAndFade { - from { - opacity: 0; - transform: translateX(2px); - } - to { - opacity: 1; - transform: translateX(0); - } - } - - /* =============== Params layout =============== */ - - .ParamsContainer { - display: flex; - column-gap: 8px; - color: var(--white-a12); - margin-bottom: 32px; - } - `, + styles: styles(), template: ` -
- Side: - - Align: - - SideOffset: - -
+ +
+ Side: + + Align: + + SideOffset: + + AlignOffset: + +
+ +
+ + Disable alternate positions (to see the result, scroll the page to make the tooltip cross the viewport + boundary) +
-
- - +
+ + {{ containerAlert }} +
+
+ + - -
- Add to library -
- or do nothing -
-
-
-
-
+ +
+ + Add to library +
+
+
+
+
+
ID: {{ rootDirective()?.uniqueId() }}
+
` }) -export class RdxTooltipPositioningComponent { - readonly PlusIcon = Plus; +export class RdxTooltipPositioningComponent extends OptionPanelBase { + readonly rootDirective = viewChild(RdxTooltipRootDirective); - selectedSide = RdxPositionSide.Top; - selectedAlign = RdxPositionAlign.Center; - sideOffset = 8; + readonly selectedSide = signal(RdxPositionSide.Top); + readonly selectedAlign = signal(RdxPositionAlign.Center); + readonly sideOffset = signal(void 0); + readonly alignOffset = signal(void 0); + readonly disableAlternatePositions = signal(false); readonly sides = RdxPositionSide; readonly aligns = RdxPositionAlign; + + readonly MountainSnowIcon = MountainSnow; + readonly XIcon = X; + protected readonly containerAlert = containerAlert; + protected readonly TriangleAlert = TriangleAlert; } diff --git a/packages/primitives/tooltip/stories/tooltip-triggering.component.ts b/packages/primitives/tooltip/stories/tooltip-triggering.component.ts index 7a8f759c..ec36d254 100644 --- a/packages/primitives/tooltip/stories/tooltip-triggering.component.ts +++ b/packages/primitives/tooltip/stories/tooltip-triggering.component.ts @@ -1,175 +1,200 @@ -import { Component, signal } from '@angular/core'; +import { Component, signal, viewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { LucideAngularModule, Plus } from 'lucide-angular'; +import { LucideAngularModule, MountainSnow, TriangleAlert, X } from 'lucide-angular'; import { RdxTooltipModule } from '../index'; +import { RdxTooltipContentAttributesComponent } from '../src/tooltip-content-attributes.component'; +import { provideRdxCdkEventService } from '../src/utils/cdk-event.service'; +import { containerAlert } from './utils/constants'; +import { OptionPanelBase } from './utils/option-panel-base.class'; +import styles from './utils/styles.constants'; +import { WithOptionPanelComponent } from './utils/with-option-panel.component'; @Component({ selector: 'rdx-tooltip-triggering', + providers: [provideRdxCdkEventService()], imports: [ FormsModule, RdxTooltipModule, - LucideAngularModule + LucideAngularModule, + RdxTooltipContentAttributesComponent, + WithOptionPanelComponent ], - styles: ` - .container { - height: 150px; - display: flex; - justify-content: center; - align-items: center; - } - - /* reset */ - button { - all: unset; - } - - .TooltipContent { - border-radius: 4px; - padding: 10px 15px; - font-size: 15px; - line-height: 1; - color: var(--violet-11); - background-color: white; - box-shadow: - hsl(206 22% 7% / 35%) 0px 10px 38px -10px, - hsl(206 22% 7% / 20%) 0px 10px 20px -15px; - user-select: none; - animation-duration: 400ms; - animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); - will-change: transform, opacity; - } - .TooltipContent[data-state='delayed-open'][data-side='top'] { - animation-name: slideDownAndFade; - } - .TooltipContent[data-state='delayed-open'][data-side='right'] { - animation-name: slideLeftAndFade; - } - .TooltipContent[data-state='delayed-open'][data-side='bottom'] { - animation-name: slideUpAndFade; - } - .TooltipContent[data-state='delayed-open'][data-side='left'] { - animation-name: slideRightAndFade; - } - - .TooltipArrow { - fill: white; - } - - .IconButton { - font-family: inherit; - border-radius: 100%; - height: 35px; - width: 35px; - display: inline-flex; - align-items: center; - justify-content: center; - color: var(--violet-11); - background-color: white; - box-shadow: 0 2px 10px var(--black-a7); - } - .IconButton:hover { - background-color: var(--violet-3); - } - .IconButton:focus { - box-shadow: 0 0 0 2px black; - } - - @keyframes slideUpAndFade { - from { - opacity: 0; - transform: translateY(2px); - } - to { - opacity: 1; - transform: translateY(0); - } - } - - @keyframes slideRightAndFade { - from { - opacity: 0; - transform: translateX(-2px); - } - to { - opacity: 1; - transform: translateX(0); - } - } - - @keyframes slideDownAndFade { - from { - opacity: 0; - transform: translateY(-2px); - } - to { - opacity: 1; - transform: translateY(0); - } - } - - @keyframes slideLeftAndFade { - from { - opacity: 0; - transform: translateX(2px); - } - to { - opacity: 1; - transform: translateX(0); - } - } - - /* =============== Trigger layout =============== */ - - .TriggerContainer { - display: flex; - column-gap: 8px; - color: var(--white-a12); - margin-bottom: 32px; - align-items: baseline; - - button { - color: var(--violet-11); - background-color: white; - box-shadow: 0 2px 10px var(--black-a7); - padding: 4px 8px; - border-radius: 4px; - } - } - `, + styles: styles(), template: ` -
- - onOpenChange count: {{ counter() }} -
-
- - - - -
- Add to library -
- or do nothing -
-
-
-
-
+

Initially closed

+ +
+ + onOpenChange count: {{ counterOpenFalse() }} +
+ +
+ + External control +
+ +
+ + {{ containerAlert }} +
+
+ + + + +
+ + Add to library +
+
+
+
+
+
ID: {{ rootDirective1()?.uniqueId() }}
+
+ +

Initially open

+ +
+ + onOpenChange count: {{ counterOpenTrue() }} +
+ +
+ + External control +
+ +
+ + {{ containerAlert }} +
+
+ + + + +
+ + Add to library +
+
+
+
+
+
ID: {{ rootDirective2()?.uniqueId() }}
+
` }) -export class RdxTooltipTriggeringComponent { - readonly PlusIcon = Plus; +export class RdxTooltipTriggeringComponent extends OptionPanelBase { + readonly rootDirective1 = viewChild('root1'); + readonly rootDirective2 = viewChild('root2'); - isOpen = signal(false); - counter = signal(0); + readonly MountainSnowIcon = MountainSnow; + readonly XIcon = X; - trigger(): void { - this.isOpen.update((value) => !value); + isOpenFalse = signal(false); + counterOpenFalse = signal(0); + externalControlFalse = signal(true); + + isOpenTrue = signal(true); + counterOpenTrue = signal(0); + externalControlTrue = signal(true); + + triggerOpenFalse(): void { + this.isOpenFalse.update((value) => !value); + } + + countOpenFalse(open: boolean): void { + this.isOpenFalse.set(open); + this.counterOpenFalse.update((value) => value + 1); } - count(): void { - this.counter.update((value) => value + 1); + triggerOpenTrue(): void { + this.isOpenTrue.update((value) => !value); } + + countOpenTrue(open: boolean): void { + this.isOpenTrue.set(open); + this.counterOpenTrue.update((value) => value + 1); + } + + protected readonly containerAlert = containerAlert; + protected readonly TriangleAlert = TriangleAlert; } diff --git a/packages/primitives/tooltip/stories/tooltip.docs.mdx b/packages/primitives/tooltip/stories/tooltip.docs.mdx index 4e69a81d..ad922ed9 100644 --- a/packages/primitives/tooltip/stories/tooltip.docs.mdx +++ b/packages/primitives/tooltip/stories/tooltip.docs.mdx @@ -1,33 +1,63 @@ -import { ArgTypes, Canvas, Markdown, Meta } from '@storybook/blocks'; +import {ArgTypes, Canvas, Markdown, Meta, Source} from '@storybook/blocks'; import * as TooltipStories from "./tooltip.stories"; import {RdxTooltipRootDirective} from "../src/tooltip-root.directive"; import {RdxTooltipContentDirective} from "../src/tooltip-content.directive"; import {RdxTooltipArrowDirective} from "../src/tooltip-arrow.directive"; +import {RdxTooltipAnchorDirective} from "../src/tooltip-anchor.directive"; +import {RdxTooltipTriggerDirective} from "../src/tooltip-trigger.directive"; +import {RdxTooltipCloseDirective} from "../src/tooltip-close.directive"; +import {animationStylesOnly} from "./utils/styles.constants"; +import {RdxTooltipContentAttributesComponent} from "../src/tooltip-content-attributes.component"; # Tooltip -#### 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. -## Default +## Examples +### Default -## Positioning +### Multiple + + + +### Events + + + +### Positioning -## External Triggering +### External Triggering +### Anchor + + + +### Initially Open + + + +### Animations + + + +### Animation Styles + + + + ## Features -- ✅ Global configuration (injection token) to control display delay globally. -- ✅ Opens when the trigger is focused or hovered. -- ✅ Closes when the trigger is activated or when pressing escape. -- ✅ Supports custom timings. +- ✅ Opens when the trigger is clicked. +- ✅ Closes when the trigger is clicked again or when pressing escape. +- ✅ Controllable from outside. ## Anatomy @@ -35,8 +65,9 @@ import {RdxTooltipArrowDirective} from "../src/tooltip-arrow.directive"; - +
+ Add to library
@@ -48,84 +79,78 @@ import {RdxTooltipArrowDirective} from "../src/tooltip-arrow.directive"; Get started with importing the directives: -```typescript -import { + or -```typescript -import { RdxTooltipModule } from '@radix-ng/primitives/tooltip'; -``` + ## API Reference -## RdxTooltipConfig - - - {` - | Prop | Type | Default | Description - | ----- | ----- | ----- | ----- - | delayDuration | number | 700 | The duration from when the mouse enters a tooltip trigger until the tooltip opens. - | skipDelayDuration | number | 300 | How much time a user has to enter another trigger without incurring a delay again. - `} - - -## RdxTooltipRootDirective +### Root +`RdxTooltipRootDirective` Contains all the parts of a tooltip. -## RdxTooltipTriggerDirective +### Trigger +`RdxTooltipTriggerDirective` The button that toggles the tooltip. By default, the TooltipContent will position itself against the trigger. - {` - | Data attribute | Value - | ----- | ----- - | [data-state] | "closed" | "delayed-open" | "instant-open" (type RdxTooltipState) - `} +{` +| Data attribute | Value | +|----------------|----------------| +| [data-state] | "closed" | "open" (type RdxTooltipState) +`} -## RdxTooltipContentDirective +### Anchor +`RdxTooltipAnchorDirective` + +An optional element to position the TooltipContent against. If this part is not used, the content will position alongside the TooltipTrigger. + +### Content +`RdxTooltipContentDirective` The component that pops out when the tooltip is open. - - {` - | Data attribute | Value - | ----- | ----- - | [data-state] | "closed" | "delayed-open" | "instant-open" (enum RdxTooltipState) - | [data-side] | "left" | "right" | "bottom" | "top" (enum RdxPositionSide) - | [data-align] | "start" | "end" | "center" (enum RdxPositionAlign) - `} - +### Content Attributes +`RdxTooltipContentAttributesComponent` -## RdxTooltipContentAttributesDirective +A component with the content attributes that are necessary to run animations. -Directive state attributes + - {` - | Data attribute | Value - | ----- | ----- - | [data-state] | "closed" | "delayed-open" | "instant-open" (enum RdxTooltipState) - | [data-side] | "left" | "right" | "bottom" | "top" (enum RdxPositionSide) - `} +{` +| Data attribute | Value | +|----------------|----------------| +| [data-state] | "closed" | "open" (enum RdxTooltipState) +| [data-side] | "left" | "right" | "bottom" | "top" (enum RdxPositionSide) +| [data-align] | "start" | "end" | "center" (enum RdxPositionAlign) +`} -## RdxTooltipArrowDirective +### Arrow +`RdxTooltipArrowDirective` An optional arrow element to render alongside the tooltip. This can be used to help visually link the trigger with the TooltipContent. Must be rendered inside TooltipContent. + +### Close +`RdxTooltipCloseDirective` + +An optional close button element to render alongside the tooltip. This can be used to close the TooltipContent. Must be rendered inside TooltipContent. diff --git a/packages/primitives/tooltip/stories/tooltip.stories.ts b/packages/primitives/tooltip/stories/tooltip.stories.ts index a71d8487..de7964e9 100644 --- a/packages/primitives/tooltip/stories/tooltip.stories.ts +++ b/packages/primitives/tooltip/stories/tooltip.stories.ts @@ -1,8 +1,13 @@ import { provideAnimations } from '@angular/platform-browser/animations'; import { componentWrapperDecorator, Meta, moduleMetadata, StoryObj } from '@storybook/angular'; -import { LucideAngularModule, Plus } from 'lucide-angular'; +import { LucideAngularModule, MountainSnow, X } from 'lucide-angular'; import { RdxTooltipModule } from '../index'; +import { RdxTooltipAnchorComponent } from './tooltip-anchor.component'; +import { RdxTooltipAnimationsComponent } from './tooltip-animations.component'; +import { RdxTooltipDefaultComponent } from './tooltip-default.component'; import { RdxTooltipEventsComponent } from './tooltip-events.components'; +import { RdxTooltipInitiallyOpenComponent } from './tooltip-initially-open.component'; +import { RdxTooltipMultipleComponent } from './tooltip-multiple.component'; import { RdxTooltipPositioningComponent } from './tooltip-positioning.component'; import { RdxTooltipTriggeringComponent } from './tooltip-triggering.component'; @@ -14,11 +19,16 @@ export default { moduleMetadata({ imports: [ RdxTooltipModule, + RdxTooltipDefaultComponent, RdxTooltipEventsComponent, RdxTooltipPositioningComponent, RdxTooltipTriggeringComponent, + RdxTooltipMultipleComponent, + RdxTooltipAnimationsComponent, + RdxTooltipInitiallyOpenComponent, + RdxTooltipAnchorComponent, LucideAngularModule, - LucideAngularModule.pick({ Plus }) + LucideAngularModule.pick({ MountainSnow, X }) ], providers: [provideAnimations()] }), @@ -32,115 +42,6 @@ export default { > ${story}
- - ` ) ] @@ -151,20 +52,7 @@ type Story = StoryObj; export const Default: Story = { render: () => ({ template: html` -
- - - - -
- Add to library -
-
-
-
-
+ ` }) }; @@ -172,46 +60,7 @@ export const Default: Story = { export const Multiple: Story = { render: () => ({ template: html` -
- - - - -
- Add to library -
-
-
-
- - - - - -
- Add to library -
-
-
-
- - - - - -
- Add to library -
-
-
-
-
+ ` }) }; @@ -239,3 +88,27 @@ export const ExternalTriggering: Story = { ` }) }; + +export const Anchor: Story = { + render: () => ({ + template: html` + + ` + }) +}; + +export const InitiallyOpen: Story = { + render: () => ({ + template: html` + + ` + }) +}; + +export const Animations: Story = { + render: () => ({ + template: html` + + ` + }) +}; diff --git a/packages/primitives/tooltip-v2/stories/utils/constants.ts b/packages/primitives/tooltip/stories/utils/constants.ts similarity index 100% rename from packages/primitives/tooltip-v2/stories/utils/constants.ts rename to packages/primitives/tooltip/stories/utils/constants.ts diff --git a/packages/primitives/tooltip-v2/stories/utils/containers.registry.ts b/packages/primitives/tooltip/stories/utils/containers.registry.ts similarity index 100% rename from packages/primitives/tooltip-v2/stories/utils/containers.registry.ts rename to packages/primitives/tooltip/stories/utils/containers.registry.ts diff --git a/packages/primitives/tooltip-v2/stories/utils/option-panel-base.class.ts b/packages/primitives/tooltip/stories/utils/option-panel-base.class.ts similarity index 100% rename from packages/primitives/tooltip-v2/stories/utils/option-panel-base.class.ts rename to packages/primitives/tooltip/stories/utils/option-panel-base.class.ts diff --git a/packages/primitives/tooltip-v2/stories/utils/styles.constants.ts b/packages/primitives/tooltip/stories/utils/styles.constants.ts similarity index 100% rename from packages/primitives/tooltip-v2/stories/utils/styles.constants.ts rename to packages/primitives/tooltip/stories/utils/styles.constants.ts diff --git a/packages/primitives/tooltip-v2/stories/utils/types.ts b/packages/primitives/tooltip/stories/utils/types.ts similarity index 100% rename from packages/primitives/tooltip-v2/stories/utils/types.ts rename to packages/primitives/tooltip/stories/utils/types.ts diff --git a/packages/primitives/tooltip-v2/stories/utils/with-option-panel.component.ts b/packages/primitives/tooltip/stories/utils/with-option-panel.component.ts similarity index 100% rename from packages/primitives/tooltip-v2/stories/utils/with-option-panel.component.ts rename to packages/primitives/tooltip/stories/utils/with-option-panel.component.ts