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) { 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: {{ rootDirective1()?.uniqueId() }}
+ + +

External Anchor (outside TooltipRoot)

+ +
+ + {{ containerAlert }} +
+
+ + + + + + +
+ + Add to library +
+
+
+
+
+
ID: {{ rootDirective2()?.uniqueId() }}
+
+ ` +}) +export class RdxTooltipAnchorComponent extends OptionPanelBase { + readonly rootDirective1 = viewChild('root1'); + readonly rootDirective2 = 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/stories/tooltip-animations.component.ts b/packages/primitives/tooltip/stories/tooltip-animations.component.ts new file mode 100644 index 00000000..81809985 --- /dev/null +++ b/packages/primitives/tooltip/stories/tooltip-animations.component.ts @@ -0,0 +1,109 @@ +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-animations', + providers: [provideRdxCdkEventService()], + imports: [ + FormsModule, + RdxTooltipModule, + LucideAngularModule, + RdxTooltipContentAttributesComponent, + WithOptionPanelComponent + ], + styles: styles(true), + template: ` + +
+ + CSS Animation + + On Opening Animation + + On Closing Animation +
+ +
+ + {{ containerAlert }} +
+
+ + + + +
+ + Add to library +
+
+
+
+
+
ID: {{ rootDirective()?.uniqueId() }}
+
+ ` +}) +export class RdxTooltipAnimationsComponent extends OptionPanelBase { + readonly rootDirective = viewChild(RdxTooltipRootDirective); + + 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/stories/tooltip-default.component.ts b/packages/primitives/tooltip/stories/tooltip-default.component.ts new file mode 100644 index 00000000..432dd337 --- /dev/null +++ b/packages/primitives/tooltip/stories/tooltip-default.component.ts @@ -0,0 +1,77 @@ +import { Component, viewChild } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +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-default', + providers: [provideRdxCdkEventService()], + imports: [ + FormsModule, + RdxTooltipModule, + LucideAngularModule, + RdxTooltipContentAttributesComponent, + WithOptionPanelComponent + ], + styles: styles(), + template: ` + +
+ + {{ containerAlert }} +
+
+ + + + +
+ + Add to library +
+
+
+
+
+
ID: {{ rootDirective()?.uniqueId() }}
+
+ ` +}) +export class RdxTooltipDefaultComponent extends OptionPanelBase { + readonly rootDirective = viewChild(RdxTooltipRootDirective); + + readonly MountainSnowIcon = MountainSnow; + readonly XIcon = X; + protected readonly TriangleAlert = TriangleAlert; + protected readonly containerAlert = containerAlert; +} 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/stories/tooltip-initially-open.component.ts b/packages/primitives/tooltip/stories/tooltip-initially-open.component.ts new file mode 100644 index 00000000..fabece35 --- /dev/null +++ b/packages/primitives/tooltip/stories/tooltip-initially-open.component.ts @@ -0,0 +1,78 @@ +import { Component, viewChild } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +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-initially-open', + providers: [provideRdxCdkEventService()], + imports: [ + FormsModule, + RdxTooltipModule, + LucideAngularModule, + RdxTooltipContentAttributesComponent, + WithOptionPanelComponent + ], + styles: styles(), + template: ` + +
+ + {{ containerAlert }} +
+
+ + + + +
+ + Add to library +
+
+
+
+
+
ID: {{ rootDirective()?.uniqueId() }}
+
+ ` +}) +export class RdxTooltipInitiallyOpenComponent extends OptionPanelBase { + readonly rootDirective = viewChild(RdxTooltipRootDirective); + + readonly MountainSnowIcon = MountainSnow; + readonly XIcon = X; + protected readonly containerAlert = containerAlert; + protected readonly TriangleAlert = TriangleAlert; +} diff --git a/packages/primitives/tooltip/stories/tooltip-multiple.component.ts b/packages/primitives/tooltip/stories/tooltip-multiple.component.ts new file mode 100644 index 00000000..349ab8fc --- /dev/null +++ b/packages/primitives/tooltip/stories/tooltip-multiple.component.ts @@ -0,0 +1,212 @@ +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 } 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-multiple', + providers: [provideRdxCdkEventService()], + imports: [ + FormsModule, + RdxTooltipModule, + LucideAngularModule, + RdxTooltipContentAttributesComponent, + WithOptionPanelComponent + ], + styles: styles(), + template: ` +

Tooltip #1

+ +
+ + {{ containerAlert }} +
+
+ + + + +
+ + Add to library +
+
+
+
+
+
ID: {{ rootDirective1()?.uniqueId() }}
+
+ +

Tooltip #2

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

Tooltip #3

+ +
+ + {{ containerAlert }} +
+
+ + [side]="'right'" + [align]="'end'" + [sideOffset]="60" + [alignOffset]="60" +
+
+ + + + +
+ + Add to library +
+
+
+
+
+
ID: {{ rootDirective3()?.uniqueId() }}
+
+ ` +}) +export class RdxTooltipMultipleComponent extends OptionPanelBase { + readonly rootDirective1 = viewChild('root1'); + readonly rootDirective2 = viewChild('root2'); + readonly rootDirective3 = viewChild('root3'); + + readonly MountainSnowIcon = MountainSnow; + readonly XIcon = X; + readonly RdxPositionSide = RdxPositionSide; + readonly RdxPositionAlign = RdxPositionAlign; + protected readonly containerAlert = containerAlert; + protected readonly TriangleAlert = TriangleAlert; +} 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/stories/utils/constants.ts b/packages/primitives/tooltip/stories/utils/constants.ts new file mode 100644 index 00000000..831c11cc --- /dev/null +++ b/packages/primitives/tooltip/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/stories/utils/containers.registry.ts b/packages/primitives/tooltip/stories/utils/containers.registry.ts new file mode 100644 index 00000000..b17af61d --- /dev/null +++ b/packages/primitives/tooltip/stories/utils/containers.registry.ts @@ -0,0 +1,63 @@ +import { isDevMode } from '@angular/core'; +import { RdxTooltipRootDirective } 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, root: RdxTooltipRootDirective) { + if (containerRegistry.has(container)) { + return; + } + containerRegistry.set(container, root); + 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/stories/utils/option-panel-base.class.ts b/packages/primitives/tooltip/stories/utils/option-panel-base.class.ts new file mode 100644 index 00000000..1eecdab0 --- /dev/null +++ b/packages/primitives/tooltip/stories/utils/option-panel-base.class.ts @@ -0,0 +1,39 @@ +import { afterNextRender, DestroyRef, Directive, ElementRef, inject, signal, viewChildren } from '@angular/core'; +import { injectDocument, RDX_POSITIONING_DEFAULTS } from '@radix-ng/primitives/core'; +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, IOpenCloseDelay } from './types'; + +@Directive() +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); + readonly document = injectDocument(); + readonly rdxCdkEventService = injectRdxCdkEventService(); + + protected constructor() { + afterNextRender(() => { + this.elementRef.nativeElement.querySelectorAll('.container').forEach((container) => { + const rootInsideContainer = this.rootDirectives().find((rootDirective) => + container.contains(rootDirective.triggerDirective().elementRef.nativeElement) + ); + if (rootInsideContainer) { + setRdxCdkEventService(this.rdxCdkEventService); + registerContainer(container, rootInsideContainer); + this.destroyRef.onDestroy(() => deregisterContainer(container)); + } + }); + }); + } +} diff --git a/packages/primitives/tooltip/stories/utils/styles.constants.ts b/packages/primitives/tooltip/stories/utils/styles.constants.ts new file mode 100644 index 00000000..a50b342e --- /dev/null +++ b/packages/primitives/tooltip/stories/utils/styles.constants.ts @@ -0,0 +1,352 @@ +const appliedAnimations = ` +.TooltipContent[data-state='open'][data-side='top'] { + animation-name: rdxSlideDownAndFade; +} + +.TooltipContent[data-state='open'][data-side='right'] { + animation-name: rdxSlideLeftAndFade; +} + +.TooltipContent[data-state='open'][data-side='bottom'] { + animation-name: rdxSlideUpAndFade; +} + +.TooltipContent[data-state='open'][data-side='left'] { + animation-name: rdxSlideRightAndFade; +} + +.TooltipContent[data-state='closed'][data-side='top'] { + animation-name: rdxSlideDownAndFadeReverse; +} + +.TooltipContent[data-state='closed'][data-side='right'] { + animation-name: rdxSlideLeftAndFadeReverse; +} + +.TooltipContent[data-state='closed'][data-side='bottom'] { + animation-name: rdxSlideUpAndFadeReverse; +} + +.TooltipContent[data-state='closed'][data-side='left'] { + animation-name: rdxSlideRightAndFadeReverse; +} +`; + +const animationParams = ` +.TooltipContent { + 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; + } +} + +.TooltipId { + color: var(--white-a12); + font-size: 12px; + line-height: 14px; + font-weight: 800; + margin: 1px 0 24px 22px; +} + +.TooltipContent { + border-radius: 4px; + padding: 6px 10px; + 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 : ''} + +.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); +} + +.TooltipArrow { + fill: white; +} + +.TooltipClose { + font-family: inherit; + border-radius: 100%; + background-color: var(--white-a12); + height: 14px; + width: 14px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--violet-11); + position: absolute; + top: -12px; + right: -12px; +} + +.TooltipClose:hover { + background-color: var(--violet-4); +} + +.TooltipClose: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/stories/utils/types.ts b/packages/primitives/tooltip/stories/utils/types.ts new file mode 100644 index 00000000..b3877b39 --- /dev/null +++ b/packages/primitives/tooltip/stories/utils/types.ts @@ -0,0 +1,25 @@ +import { DestroyRef, ElementRef, Signal } from '@angular/core'; +import { RdxTooltipRootDirective } 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 interface IOpenCloseDelay { + openDelay: Signal; + closeDelay: Signal; +} + +export type Message = { value: string; timeFromPrev: number }; diff --git a/packages/primitives/tooltip/stories/utils/with-option-panel.component.ts b/packages/primitives/tooltip/stories/utils/with-option-panel.component.ts new file mode 100644 index 00000000..645ec679 --- /dev/null +++ b/packages/primitives/tooltip/stories/utils/with-option-panel.component.ts @@ -0,0 +1,186 @@ +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 { RdxTooltipRootDirective } from '../../src/tooltip-root.directive'; +import { paramsAndEventsOnly } from './styles.constants'; +import { Message } from './types'; + +@Component({ + selector: 'tooltip-with-option-panel', + styles: paramsAndEventsOnly, + template: ` + + + @if (paramsContainerCounter() > 3) { +
+ } + +
+ + Disable (onOverlayEscapeKeyDown) event + + Disable (onOverlayOutsideClick) event +
+ +
+ Arrow width + + Arrow height + +
+ +
+ Open delay + + Close delay + +
+ + + + @if (messages().length) { + +
+ @for (message of messages(); track i; let i = $index) { + + } +
+ } + + +

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

+
+ `, + imports: [ + ReactiveFormsModule, + FormsModule, + NgTemplateOutlet + ] +}) +export class WithOptionPanelComponent { + onOverlayEscapeKeyDownDisabled = model(false); + onOverlayOutsideClickDisabled = model(false); + + arrowWidth = model(0); + arrowHeight = model(0); + + openDelay = model(0); + closeDelay = model(0); + + readonly elementRef = inject>(ElementRef); + + readonly rootDirective = contentChild.required(RdxTooltipRootDirective); + + readonly paramsContainerCounter = signal(0); + + readonly messages = signal([]); + readonly rootUniqueId = computed(() => this.rootDirective().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.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. + */ + 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: `[TooltipRoot] Escape clicked! (disabled: ${this.onOverlayEscapeKeyDownDisabled()})`, + timeFromPrev: this.timeFromPrev() + }); + }; + + private onOverlayOutsideClick = () => { + this.addMessage({ + value: `[TooltipRoot] Mouse clicked outside the tooltip! (disabled: ${this.onOverlayOutsideClickDisabled()})`, + timeFromPrev: this.timeFromPrev() + }); + }; + + private onOpen = () => { + this.addMessage({ value: '[TooltipContent] Open', timeFromPrev: this.timeFromPrev() }); + }; + + private onClose = () => { + this.addMessage({ value: '[TooltipContent] Closed', timeFromPrev: this.timeFromPrev() }); + }; + + protected addMessage = (message: Message) => { + this.messages.update((messages) => { + return [ + message, + ...messages + ]; + }); + }; +}