From 7e86b50b25b55071ab4f4408be0234e64da89096 Mon Sep 17 00:00:00 2001 From: "a.kiselev" Date: Mon, 21 Jul 2025 10:38:52 +0300 Subject: [PATCH 01/10] fix(evo-tooltip): fixed behavior --- .../directives/evo-tooltip.directive.ts | 6 +- .../evo-tooltip/evo-tooltip.component.scss | 102 ++---------------- .../evo-tooltip/evo-tooltip.component.ts | 81 +++++++------- .../lib/components/evo-tooltip/public-api.ts | 3 + .../services/evo-tooltip.service.ts | 4 +- 5 files changed, 64 insertions(+), 132 deletions(-) diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.ts index 9a59ffdf2..795a8f8c2 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.ts @@ -9,7 +9,7 @@ import { Output, TemplateRef, } from '@angular/core'; -import {fromEvent, Observable, Subject} from 'rxjs'; +import {fromEvent, merge, Observable, Subject} from 'rxjs'; import {takeUntil, tap, throttleTime} from 'rxjs/operators'; import {EvoTooltipService} from '../services/evo-tooltip.service'; import {EvoTooltipPositionType} from '../types/evo-tooltip-position-type'; @@ -80,7 +80,9 @@ export class EvoTooltipDirective implements OnInit, OnDestroy { } private initSubscriptions(): void { - fromEvent(this.elementRef.nativeElement, 'mouseenter') + const element = this.elementRef.nativeElement; + + merge(fromEvent(element, 'mouseenter'), fromEvent(element, 'touchstart')) .pipe( throttleTime(this.config?.showDelay ?? EVO_TOOLTIP_CONFIG.showDelay), tap(() => { diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.scss b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.scss index 0ccbb3b20..d4fb1cd23 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.scss +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.scss @@ -1,4 +1,4 @@ -@import '../../styles/mixins'; +@import "../../styles/mixins"; :host { --evo-tooltip-horizontal-position-arrow: 50%; @@ -15,8 +15,6 @@ } .evo-tooltip { - $arrow-size: 8px; - display: inline-block; position: relative; background-color: var(--evo-tooltip-background-color); @@ -39,34 +37,19 @@ content: ""; position: absolute; border-style: solid; + left: var(--evo-tooltip-horizontal-position-arrow); + top: var(--evo-tooltip-vertical-position-arrow); + width: 0; + height: 0; + border-width: 0 8px 8px 8px; + border-color: transparent transparent var(--evo-tooltip-background-color) transparent; } &_top-start, &_top, &_top-end { &:before { - border-width: $arrow-size $arrow-size 0 $arrow-size; - border-color: var(--evo-tooltip-background-color) transparent transparent transparent; - bottom: -$arrow-size; - } - } - - &_top-start { - &:before { - left: var(--evo-tooltip-horizontal-position-arrow); - } - } - - &_top { - &:before { - left: 50%; - transform: translateX(-50%); - } - } - - &_top-end { - &:before { - right: var(--evo-tooltip-horizontal-position-arrow); + transform: rotate(180deg); } } @@ -74,28 +57,7 @@ &_right, &_right-end { &:before { - border-width: $arrow-size $arrow-size $arrow-size 0; - border-color: transparent var(--evo-tooltip-background-color) transparent transparent; - left: -$arrow-size; - } - } - - &_right-start { - &:before { - top: var(--evo-tooltip-vertical-position-arrow); - } - } - - &_right { - &:before { - top: 50%; - transform: translateY(-50%); - } - } - - &_right-end { - &:before { - bottom: var(--evo-tooltip-vertical-position-arrow); + transform: rotate(-90deg) translateX(-4px) translateY(-12px); } } @@ -103,28 +65,7 @@ &_bottom, &_bottom-end { &:before { - border-width: 0 $arrow-size $arrow-size $arrow-size; - border-color: transparent transparent var(--evo-tooltip-background-color) transparent; - top: -$arrow-size; - } - } - - &_bottom-start { - &:before { - left: var(--evo-tooltip-horizontal-position-arrow); - } - } - - &_bottom { - &:before { - left: 50%; - transform: translateX(-50%); - } - } - - &_bottom-end { - &:before { - right: var(--evo-tooltip-horizontal-position-arrow); + transform: translateY(-8px); } } @@ -132,28 +73,7 @@ &_left, &_left-end { &:before { - border-width: $arrow-size 0 $arrow-size $arrow-size; - border-color: transparent transparent transparent var(--evo-tooltip-background-color); - right: -$arrow-size; - } - } - - &_left-start { - &:before { - top: var(--evo-tooltip-vertical-position-arrow); - } - } - - &_left { - &:before { - top: 50%; - transform: translateY(-50%); - } - } - - &_left-end { - &:before { - bottom: var(--evo-tooltip-vertical-position-arrow); + transform: rotate(90deg) translateX(4px) translateY(4px); } } } diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.ts index 9fb9f2eea..7d781f96c 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.ts @@ -16,7 +16,6 @@ import {EvoTooltipService} from './services/evo-tooltip.service'; import {EvoTooltipStyles} from './interfaces/evo-tooltip-styles'; import {EvoTooltipPosition} from './enums/evo-tooltip-position'; import {EVO_TOOLTIP_ARROW_SIZE} from './constants/evo-tooltip-arrow-size'; -import {EVO_TOOLTIP_RADIUS} from './constants/evo-tooltip-radius'; import {EvoTooltipVariableArrowPosition} from './enums/evo-tooltip-variable-arrow-position'; @Component({ @@ -52,18 +51,6 @@ export class EvoTooltipComponent implements OnInit, AfterViewInit, OnDestroy { combineLatest([this.position$, this.tooltipService.parentRef$, this.visibleArrow$]) .pipe( filter(([_position, _parentRef, visibleArrow]) => visibleArrow), - // Вычисление стрелки нужно только для угловых позиций - filter(([position]) => { - switch (position) { - case EvoTooltipPosition.TOP: - case EvoTooltipPosition.RIGHT: - case EvoTooltipPosition.BOTTOM: - case EvoTooltipPosition.LEFT: - return false; - default: - return true; - } - }), tap(([_, parentRef]) => { this.setArrowPosition(parentRef); }), @@ -92,34 +79,52 @@ export class EvoTooltipComponent implements OnInit, AfterViewInit, OnDestroy { this._destroy$.complete(); } + private getArrowOffset(params: { + parentStart: number; + parentEnd: number; + tooltipStart: number; + tooltipEnd: number; + }): number { + const tooltipSize = params.tooltipEnd - params.tooltipStart; + + if (params.parentEnd <= params.tooltipStart) { + return 0; + } + + if (params.parentStart >= params.tooltipEnd) { + return tooltipSize; + } + + const parentSize = params.parentEnd - params.parentStart; + + const parentMiddleOffset = params.parentStart + parentSize / 2; + const elementMiddleOffset = params.tooltipStart + tooltipSize / 2; + + const diff = elementMiddleOffset - parentMiddleOffset; + return elementMiddleOffset - diff - params.tooltipStart - EVO_TOOLTIP_ARROW_SIZE / 2; + } + private setArrowPosition(parentRef: ElementRef): void { - // Для того чтобы стрелка тянулась к центру родителя - берем середину - const widthParent = parentRef.nativeElement.offsetWidth / 2; - const heightParent = parentRef.nativeElement.offsetHeight / 2; - const isParentLonger = widthParent >= this.elementRef.nativeElement.offsetWidth; - const isParentHigher = heightParent >= this.elementRef.nativeElement.offsetHeight; - // Если середина родителя оказывается меньше тултипа - берем середину родителя иначе размер тултипа - // Это проверка на максимальное смещение, смещение стрелки не должно быть больше размера тултипа - const width = isParentLonger ? this.elementRef.nativeElement.offsetWidth : widthParent; - const height = isParentHigher ? this.elementRef.nativeElement.offsetHeight : heightParent; - - const positionArrow = (size: number, isParentBigger: boolean): number => - // Если середина родителя больше тултипа - // То берем размер тултипа и отнимаем размер стрелки и радиуса - // Иначе берем середину родителя и отнимаем половину стрелки - // Это условие нужно чтобы стрелка не смещалась - isParentBigger ? size - EVO_TOOLTIP_ARROW_SIZE - EVO_TOOLTIP_RADIUS : size - EVO_TOOLTIP_ARROW_SIZE / 2; - let verticalPositionArrow = positionArrow(height, isParentHigher); - let horizontalPositionArrow = positionArrow(width, isParentLonger); - - // Проверка на минимальное смещение, смещение стрелки не должно быть меньше размера радиуса тултипа 8px - horizontalPositionArrow = - horizontalPositionArrow > EVO_TOOLTIP_RADIUS ? horizontalPositionArrow : EVO_TOOLTIP_RADIUS; - verticalPositionArrow = verticalPositionArrow > EVO_TOOLTIP_RADIUS ? verticalPositionArrow : EVO_TOOLTIP_RADIUS; + const parentRect = (parentRef.nativeElement as HTMLElement).getBoundingClientRect(); + const tooltipRect = (this.elementRef.nativeElement as HTMLElement).getBoundingClientRect(); + + const vertical = this.getArrowOffset({ + parentStart: parentRect.top, + parentEnd: parentRect.bottom, + tooltipStart: tooltipRect.top, + tooltipEnd: tooltipRect.bottom, + }); + + const horizontal = this.getArrowOffset({ + parentStart: parentRect.left, + parentEnd: parentRect.right, + tooltipStart: tooltipRect.left, + tooltipEnd: tooltipRect.right, + }); this._positionArrowStyles$.next({ - [EvoTooltipVariableArrowPosition.VERTICAL_POSITION_ARROW]: `${verticalPositionArrow}px`, - [EvoTooltipVariableArrowPosition.HORIZONTAL_POSITION_ARROW]: `${horizontalPositionArrow}px`, + [EvoTooltipVariableArrowPosition.VERTICAL_POSITION_ARROW]: `${vertical}px`, + [EvoTooltipVariableArrowPosition.HORIZONTAL_POSITION_ARROW]: `${horizontal}px`, }); } } diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/public-api.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/public-api.ts index 06bf44990..16718cd89 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/public-api.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/public-api.ts @@ -1,2 +1,5 @@ export * from './evo-tooltip.module'; export * from './directives/evo-tooltip.directive'; +export * from './types/evo-tooltip-position-type'; +export * from './interfaces/evo-tooltip-config'; +export * from './interfaces/evo-tooltip-styles'; diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.ts index 26c3e1f0d..f5da213e3 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.ts @@ -135,7 +135,9 @@ export class EvoTooltipService { .flexibleConnectedTo(elementRef) .withPositions(this.getPositions(position)); - const scrollStrategy = this.overlay.scrollStrategies.reposition(); + const scrollStrategy = this.overlay.scrollStrategies.close({ + threshold: 10, + }); this.overlayRef = this.overlay.create({positionStrategy: this.positionStrategy, scrollStrategy}); } From 02911a2b463a13188c9c6dadf089ac68a387e601 Mon Sep 17 00:00:00 2001 From: "a.kiselev" Date: Sat, 26 Jul 2025 13:01:07 +0300 Subject: [PATCH 02/10] fix(evo-tooltip): fixed behavior and tests --- .../directives/evo-tooltip.directive.spec.ts | 16 ++++++-- .../enums/evo-tooltip-style-variable.ts | 12 ++++++ .../evo-tooltip-variable-arrow-position.ts | 4 -- .../evo-tooltip/evo-tooltip.component.scss | 21 ++++------ .../evo-tooltip/evo-tooltip.component.spec.ts | 13 ++++--- .../evo-tooltip/evo-tooltip.component.ts | 38 +++++++++++++------ .../interfaces/evo-tooltip-styles.ts | 15 ++++++-- .../services/evo-tooltip.service.spec.ts | 6 +-- .../services/evo-tooltip.service.ts | 2 +- 9 files changed, 83 insertions(+), 44 deletions(-) create mode 100644 projects/evo-ui-kit/src/lib/components/evo-tooltip/enums/evo-tooltip-style-variable.ts delete mode 100644 projects/evo-ui-kit/src/lib/components/evo-tooltip/enums/evo-tooltip-variable-arrow-position.ts diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.spec.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.spec.ts index 8e3a94f72..57c39104d 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.spec.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.spec.ts @@ -3,7 +3,7 @@ import { EvoTooltipDirective } from './evo-tooltip.directive'; import { Component } from '@angular/core'; import { EvoTooltipPosition } from '../enums/evo-tooltip-position'; import { EvoTooltipStyles } from '../interfaces/evo-tooltip-styles'; -import { EvoTooltipVariableArrowPosition } from '../enums/evo-tooltip-variable-arrow-position'; +import { EvoTooltipStyleVariable } from '../enums/evo-tooltip-style-variable'; import { CommonModule } from '@angular/common'; import { EvoTooltipService } from '../services/evo-tooltip.service'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @@ -32,8 +32,8 @@ class TestHostComponent { config = { showDelay: 0, hideDelay: 0 }; visibleArrow = true; styles: EvoTooltipStyles = { - [EvoTooltipVariableArrowPosition.VERTICAL_POSITION_ARROW]: '10px', - [EvoTooltipVariableArrowPosition.HORIZONTAL_POSITION_ARROW]: '20px', + [EvoTooltipStyleVariable.VERTICAL_POSITION_ARROW]: '10px', + [EvoTooltipStyleVariable.HORIZONTAL_POSITION_ARROW]: '20px', }; classes = ['class-1', 'class-2']; onOpen = jasmine.createSpy('onOpen'); @@ -120,4 +120,14 @@ describe('EvoTooltipDirective', () => { expect(isOpen).toBeTrue(); }); })); + + it('should handle touchstart event', fakeAsync(() => { + const element = fixture.debugElement.children[0].nativeElement; + element.dispatchEvent(new MouseEvent('touchstart')); + tick(0); + fixture.detectChanges(); + tooltipService.isOpen$.pipe(first()).subscribe((isOpen) => { + expect(isOpen).toBeTrue(); + }); + })); }); diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/enums/evo-tooltip-style-variable.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/enums/evo-tooltip-style-variable.ts new file mode 100644 index 000000000..5f6a5806f --- /dev/null +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/enums/evo-tooltip-style-variable.ts @@ -0,0 +1,12 @@ +export enum EvoTooltipStyleVariable { + HORIZONTAL_POSITION_ARROW = '--evo-tooltip-horizontal-position-arrow', + VERTICAL_POSITION_ARROW = '--evo-tooltip-vertical-position-arrow', + + COLOR = '--evo-tooltip-color', + BACKGROUND_COLOR = '--evo-tooltip-background-color', + MAX_WIDTH = '--evo-tooltip-max-width', + + PADDING = '--evo-tooltip-padding', + + BORDER_RADIUS = '--evo-tooltip-border-radius', +} diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/enums/evo-tooltip-variable-arrow-position.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/enums/evo-tooltip-variable-arrow-position.ts deleted file mode 100644 index cd74a502d..000000000 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/enums/evo-tooltip-variable-arrow-position.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum EvoTooltipVariableArrowPosition { - HORIZONTAL_POSITION_ARROW = '--evo-tooltip-horizontal-position-arrow', - VERTICAL_POSITION_ARROW = '--evo-tooltip-vertical-position-arrow', -} diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.scss b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.scss index d4fb1cd23..565e6d82e 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.scss +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.scss @@ -15,6 +15,8 @@ } .evo-tooltip { + $arrow-border-width: 8px; + display: inline-block; position: relative; background-color: var(--evo-tooltip-background-color); @@ -36,28 +38,19 @@ &:not(&_not-arrow):before { content: ""; position: absolute; - border-style: solid; left: var(--evo-tooltip-horizontal-position-arrow); top: var(--evo-tooltip-vertical-position-arrow); width: 0; height: 0; - border-width: 0 8px 8px 8px; - border-color: transparent transparent var(--evo-tooltip-background-color) transparent; - } - - &_top-start, - &_top, - &_top-end { - &:before { - transform: rotate(180deg); - } + border: $arrow-border-width solid transparent; + border-top: $arrow-border-width solid var(--evo-tooltip-background-color); } &_right-start, &_right, &_right-end { &:before { - transform: rotate(-90deg) translateX(-4px) translateY(-12px); + transform: rotate(90deg); } } @@ -65,7 +58,7 @@ &_bottom, &_bottom-end { &:before { - transform: translateY(-8px); + transform: rotate(180deg); } } @@ -73,7 +66,7 @@ &_left, &_left-end { &:before { - transform: rotate(90deg) translateX(4px) translateY(4px); + transform: rotate(-90deg); } } } diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.spec.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.spec.ts index 3ce4b0b11..f27e466e6 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.spec.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.spec.ts @@ -1,16 +1,17 @@ import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {EvoTooltipComponent} from './evo-tooltip.component'; import {EvoTooltipService} from './services/evo-tooltip.service'; -import {NO_ERRORS_SCHEMA, Component, ViewChild, TemplateRef} from '@angular/core'; +import {Component, ElementRef, NO_ERRORS_SCHEMA, TemplateRef, ViewChild} from '@angular/core'; import {EvoTooltipPosition} from './enums/evo-tooltip-position'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {EvoTooltipStyles} from './interfaces/evo-tooltip-styles'; -import {EvoTooltipVariableArrowPosition} from './enums/evo-tooltip-variable-arrow-position'; +import {EvoTooltipStyleVariable} from './enums/evo-tooltip-style-variable'; import {CommonModule} from '@angular/common'; @Component({ selector: 'evo-host-component', template: ` +
Parent
Test template content
@@ -20,6 +21,7 @@ import {CommonModule} from '@angular/common'; class TestHostComponent { @ViewChild(EvoTooltipComponent, {static: true}) tooltipComponent: EvoTooltipComponent; @ViewChild('testTemplate', {static: true}) testTemplate: TemplateRef; + @ViewChild('tooltipParent', {static: true}) parentRef: ElementRef } describe('EvoTooltipComponent', () => { @@ -44,6 +46,7 @@ describe('EvoTooltipComponent', () => { testHostComponent = testHostFixture.componentInstance; tooltipComponent = testHostComponent.tooltipComponent; tooltipService = TestBed.inject(EvoTooltipService); + tooltipService['_parentRef$'].next(testHostComponent.parentRef); testHostFixture.detectChanges(); }); @@ -83,14 +86,14 @@ describe('EvoTooltipComponent', () => { it('should update styles when styles$ changes', () => { const styles: EvoTooltipStyles = { - [EvoTooltipVariableArrowPosition.VERTICAL_POSITION_ARROW]: '10px', - [EvoTooltipVariableArrowPosition.HORIZONTAL_POSITION_ARROW]: '20px', + [EvoTooltipStyleVariable.MAX_WIDTH]: 'auto', + [EvoTooltipStyleVariable.PADDING]: 0, }; tooltipService['_styles$'].next(styles); testHostFixture.detectChanges(); tooltipComponent.styles$.subscribe((value) => { - expect(value).toEqual(styles); + Object.keys(styles).forEach((key: string) => expect(value[key]).toEqual(styles[key])); }); }); diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.ts index 7d781f96c..e0ee5174d 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.ts @@ -15,8 +15,9 @@ import {EVO_TOOLTIP_FADEIN_ANIMATION} from './constants/evo-tooltip-fadein.anima import {EvoTooltipService} from './services/evo-tooltip.service'; import {EvoTooltipStyles} from './interfaces/evo-tooltip-styles'; import {EvoTooltipPosition} from './enums/evo-tooltip-position'; +import {EvoTooltipStyleVariable} from './enums/evo-tooltip-style-variable'; +import {EVO_TOOLTIP_RADIUS} from './constants/evo-tooltip-radius'; import {EVO_TOOLTIP_ARROW_SIZE} from './constants/evo-tooltip-arrow-size'; -import {EvoTooltipVariableArrowPosition} from './enums/evo-tooltip-variable-arrow-position'; @Component({ selector: 'evo-tooltip', @@ -86,22 +87,37 @@ export class EvoTooltipComponent implements OnInit, AfterViewInit, OnDestroy { tooltipEnd: number; }): number { const tooltipSize = params.tooltipEnd - params.tooltipStart; + const parentSize = params.parentEnd - params.parentStart; - if (params.parentEnd <= params.tooltipStart) { - return 0; + // tooltip after the parent + if (params.parentEnd < params.tooltipStart) { + return -EVO_TOOLTIP_ARROW_SIZE; } - if (params.parentStart >= params.tooltipEnd) { + // tooltip before the parent + if (params.parentStart > params.tooltipEnd) { return tooltipSize; } - const parentSize = params.parentEnd - params.parentStart; + const tooltipCenter = Math.round(tooltipSize / 2); + const parentCenterOnTooltip = Math.round(parentSize / 2 + params.parentStart - params.tooltipStart); - const parentMiddleOffset = params.parentStart + parentSize / 2; - const elementMiddleOffset = params.tooltipStart + tooltipSize / 2; + const defaultArrowOffset = parentCenterOnTooltip - EVO_TOOLTIP_ARROW_SIZE / 2; + + // centers are is one positions + if (tooltipCenter === parentCenterOnTooltip) { + return defaultArrowOffset; + } + + const minPosition = EVO_TOOLTIP_RADIUS; + const maxPosition = tooltipSize - EVO_TOOLTIP_ARROW_SIZE - EVO_TOOLTIP_RADIUS; + + // parent center inside tooltip + if (defaultArrowOffset >= minPosition && parentCenterOnTooltip <= maxPosition) { + return defaultArrowOffset; + } - const diff = elementMiddleOffset - parentMiddleOffset; - return elementMiddleOffset - diff - params.tooltipStart - EVO_TOOLTIP_ARROW_SIZE / 2; + return tooltipCenter > parentCenterOnTooltip ? minPosition : maxPosition; } private setArrowPosition(parentRef: ElementRef): void { @@ -123,8 +139,8 @@ export class EvoTooltipComponent implements OnInit, AfterViewInit, OnDestroy { }); this._positionArrowStyles$.next({ - [EvoTooltipVariableArrowPosition.VERTICAL_POSITION_ARROW]: `${vertical}px`, - [EvoTooltipVariableArrowPosition.HORIZONTAL_POSITION_ARROW]: `${horizontal}px`, + [EvoTooltipStyleVariable.VERTICAL_POSITION_ARROW]: `${vertical}px`, + [EvoTooltipStyleVariable.HORIZONTAL_POSITION_ARROW]: `${horizontal}px`, }); } } diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/interfaces/evo-tooltip-styles.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/interfaces/evo-tooltip-styles.ts index ff1afa77f..fd118f006 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/interfaces/evo-tooltip-styles.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/interfaces/evo-tooltip-styles.ts @@ -1,5 +1,14 @@ -import {EvoTooltipVariableArrowPosition} from '../enums/evo-tooltip-variable-arrow-position'; +import {EvoTooltipStyleVariable} from '../enums/evo-tooltip-style-variable'; -export type EvoTooltipStyles = { - [key in EvoTooltipVariableArrowPosition]: string; +export interface EvoTooltipStyles { + [EvoTooltipStyleVariable.HORIZONTAL_POSITION_ARROW]?: string; + [EvoTooltipStyleVariable.VERTICAL_POSITION_ARROW]?: string; + + [EvoTooltipStyleVariable.COLOR]?: string; + [EvoTooltipStyleVariable.BACKGROUND_COLOR]?: string; + + [EvoTooltipStyleVariable.MAX_WIDTH]?: string | number; + + [EvoTooltipStyleVariable.PADDING]?: string | number; + [EvoTooltipStyleVariable.BORDER_RADIUS]?: string | number; }; diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.spec.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.spec.ts index 498201c13..345d01221 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.spec.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.spec.ts @@ -4,7 +4,7 @@ import {EvoTooltipPosition} from '../enums/evo-tooltip-position'; import {EvoTooltipStyles} from '../interfaces/evo-tooltip-styles'; import {ElementRef} from '@angular/core'; import {first} from 'rxjs/operators'; -import {EvoTooltipVariableArrowPosition} from '../enums/evo-tooltip-variable-arrow-position'; +import {EvoTooltipStyleVariable} from '../enums/evo-tooltip-style-variable'; import {CommonModule} from '@angular/common'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {EvoTooltipComponent} from '../evo-tooltip.component'; @@ -46,8 +46,8 @@ describe('EvoTooltipService', () => { it('should set tooltip styles', () => { const styles: EvoTooltipStyles = { - [EvoTooltipVariableArrowPosition.VERTICAL_POSITION_ARROW]: '10px', - [EvoTooltipVariableArrowPosition.HORIZONTAL_POSITION_ARROW]: '20px', + [EvoTooltipStyleVariable.VERTICAL_POSITION_ARROW]: '10px', + [EvoTooltipStyleVariable.HORIZONTAL_POSITION_ARROW]: '20px', }; service.setTooltipStyles(styles); service.styles$.pipe(first()).subscribe((value) => { diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.ts index f5da213e3..8979160f9 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.ts @@ -17,9 +17,9 @@ import {EVO_CONNECTED_POSITION} from '../constants/evo-tooltip-connected-positio import {EVO_PRIORITY_POSITIONS_ORDER} from '../constants/evo-tooltip-priority-positions-order'; import {EVO_DEFAULT_POSITIONS_ORDER} from '../constants/evo-tooltip-default-positions-order'; import {EVO_TOOLTIP_ARROW_SIZE} from '../constants/evo-tooltip-arrow-size'; -import {EVO_TOOLTIP_RADIUS} from '../constants/evo-tooltip-radius'; import {EvoTooltipStyles} from '../interfaces/evo-tooltip-styles'; import {EVO_TOOLTIP_OFFSET} from '../constants/evo-tooltip-offset'; +import {EVO_TOOLTIP_RADIUS} from '../constants/evo-tooltip-radius'; @Injectable() export class EvoTooltipService { From 9cb8ca19ba36b385c1b0ef6f225b6420415aab55 Mon Sep 17 00:00:00 2001 From: "a.kiselev" Date: Tue, 19 Aug 2025 16:51:43 +0300 Subject: [PATCH 03/10] feat(evo-tooltip): added service to public api --- projects/evo-ui-kit/src/lib/components/evo-tooltip/public-api.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/public-api.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/public-api.ts index 16718cd89..732c50d68 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/public-api.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/public-api.ts @@ -3,3 +3,4 @@ export * from './directives/evo-tooltip.directive'; export * from './types/evo-tooltip-position-type'; export * from './interfaces/evo-tooltip-config'; export * from './interfaces/evo-tooltip-styles'; +export * from './services/evo-tooltip.service'; From 558bb88d0aca3c7fe012b9e68ef9776990a85a91 Mon Sep 17 00:00:00 2001 From: "a.kiselev" Date: Wed, 20 Aug 2025 13:27:24 +0300 Subject: [PATCH 04/10] feat(evo-tooltip): fixed subscription and scroll strategies added to config --- .../directives/evo-tooltip.directive.ts | 51 +++++++--- .../evo-tooltip/evo-tooltip.component.ts | 12 +-- .../interfaces/evo-tooltip-config.ts | 3 + .../services/evo-tooltip.service.ts | 93 +++++++++---------- .../types/evo-tooltip-scroll-strategy.ts | 1 + 5 files changed, 89 insertions(+), 71 deletions(-) create mode 100644 projects/evo-ui-kit/src/lib/components/evo-tooltip/types/evo-tooltip-scroll-strategy.ts diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.ts index 795a8f8c2..2c81fc23d 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.ts @@ -9,14 +9,14 @@ import { Output, TemplateRef, } from '@angular/core'; -import {fromEvent, merge, Observable, Subject} from 'rxjs'; -import {takeUntil, tap, throttleTime} from 'rxjs/operators'; -import {EvoTooltipService} from '../services/evo-tooltip.service'; -import {EvoTooltipPositionType} from '../types/evo-tooltip-position-type'; -import {EvoTooltipConfig} from '../interfaces/evo-tooltip-config'; +import {EMPTY, filter, fromEvent, merge, Subject} from 'rxjs'; +import {catchError, debounceTime, distinctUntilChanged, first, map, takeUntil, tap, throttleTime} from 'rxjs/operators'; import {EVO_TOOLTIP_CONFIG} from '../constants/evo-tooltip-config'; import {EvoTooltipPosition} from '../enums/evo-tooltip-position'; +import {EvoTooltipConfig} from '../interfaces/evo-tooltip-config'; import {EvoTooltipStyles} from '../interfaces/evo-tooltip-styles'; +import {EvoTooltipService} from '../services/evo-tooltip.service'; +import {EvoTooltipPositionType} from '../types/evo-tooltip-position-type'; @Directive({ selector: '[evoTooltip]', @@ -45,8 +45,6 @@ export class EvoTooltipDirective implements OnInit, OnDestroy { return ['evo-tooltip-trigger', ...(this.disabled ? ['evo-tooltip-trigger_disabled'] : [])]; } - readonly isOpen$: Observable = this.tooltipService.isOpen$; - private readonly destroy$ = new Subject(); constructor(private readonly elementRef: ElementRef, private readonly tooltipService: EvoTooltipService) {} @@ -65,18 +63,19 @@ export class EvoTooltipDirective implements OnInit, OnDestroy { this.tooltipService.hideTooltip(); } - show(event?: MouseEvent): void { + show(): void { if (!this.content || this.tooltipService.hasAttached || this.disabled) { return; } - this.tooltipService.showTooltip( + const tooltip = this.tooltipService.showTooltip( this.elementRef, this.content, this.position as EvoTooltipPosition, {...EVO_TOOLTIP_CONFIG, ...this.config}, - event?.target, ); + + this.initHideSubscription(tooltip); } private initSubscriptions(): void { @@ -85,20 +84,44 @@ export class EvoTooltipDirective implements OnInit, OnDestroy { merge(fromEvent(element, 'mouseenter'), fromEvent(element, 'touchstart')) .pipe( throttleTime(this.config?.showDelay ?? EVO_TOOLTIP_CONFIG.showDelay), - tap(() => { - this.show(); - }), + tap(() => this.show()), takeUntil(this.destroy$), ) .subscribe(); this.tooltipService.isOpen$ .pipe( + distinctUntilChanged(), tap((isOpen) => { - isOpen ? this.evoTooltipOpen.emit() : this.evoTooltipClose.emit(); + if (isOpen) { + this.evoTooltipOpen.emit(); + } else { + this.evoTooltipClose.emit(); + } }), takeUntil(this.destroy$), ) .subscribe(); } + + private initHideSubscription(tooltip: HTMLElement): void { + const mouseLeaveParentElement$ = fromEvent(tooltip, 'mouseleave').pipe(map(() => this.elementRef.nativeElement)); + const mouseLeaveOverlayElement$ = fromEvent(this.elementRef.nativeElement, 'mouseleave').pipe(map(() => tooltip)); + + merge(mouseLeaveParentElement$, mouseLeaveOverlayElement$) + .pipe( + filter((element) => !element.matches(':hover')), + debounceTime(this.config?.hideDelay ?? EVO_TOOLTIP_CONFIG.hideDelay), + filter(() => !tooltip.matches(':hover') && !this.elementRef.nativeElement.matches(':hover')), + first(), + catchError(() => { + this.tooltipService.hideTooltip(); + return EMPTY; + }), + takeUntil(this.destroy$), + ) + .subscribe(() => { + this.tooltipService.hideTooltip(); + }); + } } diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.ts index e0ee5174d..cb64f135d 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.ts @@ -11,13 +11,13 @@ import { } from '@angular/core'; import {BehaviorSubject, combineLatest, EMPTY, Observable, Subject} from 'rxjs'; import {filter, map, pairwise, startWith, takeUntil, tap} from 'rxjs/operators'; +import {EVO_TOOLTIP_ARROW_SIZE} from './constants/evo-tooltip-arrow-size'; import {EVO_TOOLTIP_FADEIN_ANIMATION} from './constants/evo-tooltip-fadein.animation'; -import {EvoTooltipService} from './services/evo-tooltip.service'; -import {EvoTooltipStyles} from './interfaces/evo-tooltip-styles'; +import {EVO_TOOLTIP_RADIUS} from './constants/evo-tooltip-radius'; import {EvoTooltipPosition} from './enums/evo-tooltip-position'; import {EvoTooltipStyleVariable} from './enums/evo-tooltip-style-variable'; -import {EVO_TOOLTIP_RADIUS} from './constants/evo-tooltip-radius'; -import {EVO_TOOLTIP_ARROW_SIZE} from './constants/evo-tooltip-arrow-size'; +import {EvoTooltipStyles} from './interfaces/evo-tooltip-styles'; +import {EvoTooltipService} from './services/evo-tooltip.service'; @Component({ selector: 'evo-tooltip', @@ -52,9 +52,7 @@ export class EvoTooltipComponent implements OnInit, AfterViewInit, OnDestroy { combineLatest([this.position$, this.tooltipService.parentRef$, this.visibleArrow$]) .pipe( filter(([_position, _parentRef, visibleArrow]) => visibleArrow), - tap(([_, parentRef]) => { - this.setArrowPosition(parentRef); - }), + tap(([_, parentRef]) => this.setArrowPosition(parentRef)), takeUntil(this._destroy$), ) .subscribe(); diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/interfaces/evo-tooltip-config.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/interfaces/evo-tooltip-config.ts index 097510ff3..0b31df2c8 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/interfaces/evo-tooltip-config.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/interfaces/evo-tooltip-config.ts @@ -1,5 +1,8 @@ +import {EvoTooltipScrollStrategy} from '../types/evo-tooltip-scroll-strategy'; + export interface EvoTooltipConfig { // The default delay in ms before hiding the tooltip hideDelay?: number; showDelay?: number; + scrollStrategy?: EvoTooltipScrollStrategy; } diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.ts index 8979160f9..f3c121680 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.ts @@ -1,25 +1,23 @@ -import {ComponentRef, ElementRef, Injectable, Injector, TemplateRef} from '@angular/core'; -import {ComponentPortal} from '@angular/cdk/portal'; import { ConnectedPosition, FlexibleConnectedPositionStrategy, Overlay, OverlayPositionBuilder, - OverlayRef, + OverlayRef, ScrollStrategy, } from '@angular/cdk/overlay'; -import {BehaviorSubject, EMPTY, fromEvent, merge, Observable, Subject} from 'rxjs'; -import {catchError, debounceTime, filter, first, map} from 'rxjs/operators'; -import {EvoTooltipConfig} from '../interfaces/evo-tooltip-config'; -import {EVO_TOOLTIP_CONFIG} from '../constants/evo-tooltip-config'; -import {EvoTooltipComponent} from '../evo-tooltip.component'; -import {EvoTooltipPosition} from '../enums/evo-tooltip-position'; +import {ComponentPortal} from '@angular/cdk/portal'; +import {ComponentRef, ElementRef, Injectable, Injector, TemplateRef} from '@angular/core'; +import {BehaviorSubject, EMPTY, Observable, Subject} from 'rxjs'; +import {EVO_TOOLTIP_ARROW_SIZE} from '../constants/evo-tooltip-arrow-size'; import {EVO_CONNECTED_POSITION} from '../constants/evo-tooltip-connected-position'; -import {EVO_PRIORITY_POSITIONS_ORDER} from '../constants/evo-tooltip-priority-positions-order'; import {EVO_DEFAULT_POSITIONS_ORDER} from '../constants/evo-tooltip-default-positions-order'; -import {EVO_TOOLTIP_ARROW_SIZE} from '../constants/evo-tooltip-arrow-size'; -import {EvoTooltipStyles} from '../interfaces/evo-tooltip-styles'; import {EVO_TOOLTIP_OFFSET} from '../constants/evo-tooltip-offset'; +import {EVO_PRIORITY_POSITIONS_ORDER} from '../constants/evo-tooltip-priority-positions-order'; import {EVO_TOOLTIP_RADIUS} from '../constants/evo-tooltip-radius'; +import {EvoTooltipPosition} from '../enums/evo-tooltip-position'; +import {EvoTooltipComponent} from '../evo-tooltip.component'; +import {EvoTooltipStyles} from '../interfaces/evo-tooltip-styles'; +import {EvoTooltipConfig} from '../public-api'; @Injectable() export class EvoTooltipService { @@ -37,7 +35,6 @@ export class EvoTooltipService { private readonly _position$ = new BehaviorSubject(EvoTooltipPosition.BOTTOM); private readonly _parentRef$ = new BehaviorSubject(null); private readonly _tooltipClasses$ = new BehaviorSubject([]); - private readonly _config$ = new BehaviorSubject(EVO_TOOLTIP_CONFIG); private readonly _styles$ = new BehaviorSubject(null); private readonly _visibleArrow$ = new BehaviorSubject(true); private readonly _isOpen$ = new Subject(); @@ -45,7 +42,6 @@ export class EvoTooltipService { private overlayRef: OverlayRef; private positionStrategy: FlexibleConnectedPositionStrategy; private tooltipComponentRef: ComponentRef | null; - private targetElement: EventTarget | null; constructor( private readonly overlay: Overlay, @@ -67,19 +63,18 @@ export class EvoTooltipService { content: string | TemplateRef, position: EvoTooltipPosition, config: EvoTooltipConfig, - targetElement?: EventTarget | null, - ): void { + ): HTMLElement { this._parentRef$.next(parentRef); - this.targetElement = targetElement ?? null; this.setContent(content); this._position$.next(position); - this._config$.next(config); - this.createOverlay(parentRef, position); + this.createOverlay(parentRef, position, config); this.createPortal(); this._isOpen$.next(this.overlayRef.hasAttached()); this.initSubscriptions(); + + return this.overlayRef.overlayElement; } hideTooltip(): void { @@ -105,8 +100,8 @@ export class EvoTooltipService { !tooltipClassOrClasses ? [] : Array.isArray(tooltipClassOrClasses) - ? tooltipClassOrClasses - : [tooltipClassOrClasses], + ? tooltipClassOrClasses + : [tooltipClassOrClasses], ); } @@ -114,10 +109,6 @@ export class EvoTooltipService { return this.overlayRef?.hasAttached() ?? false; } - get config(): EvoTooltipConfig { - return this._config$.value; - } - private get parentRef(): ElementRef { return this._parentRef$.value; } @@ -130,15 +121,38 @@ export class EvoTooltipService { } } - private createOverlay(elementRef: ElementRef, position: EvoTooltipPosition): void { + private createOverlay(elementRef: ElementRef, position: EvoTooltipPosition, config: EvoTooltipConfig): void { this.positionStrategy = this.overlayPositionBuilder .flexibleConnectedTo(elementRef) .withPositions(this.getPositions(position)); - const scrollStrategy = this.overlay.scrollStrategies.close({ - threshold: 10, + this.overlayRef = this.overlay.create({ + positionStrategy: this.positionStrategy, + scrollStrategy: this.getScrollStrategy(config) }); - this.overlayRef = this.overlay.create({positionStrategy: this.positionStrategy, scrollStrategy}); + } + + private getScrollStrategy(config: EvoTooltipConfig): ScrollStrategy { + switch (config?.scrollStrategy) { + case 'reposition': { + return this.overlay.scrollStrategies.reposition(); + } + + case 'block': { + return this.overlay.scrollStrategies.block(); + } + + case 'noop': { + return this.overlay.scrollStrategies.noop(); + } + + case 'close': + default: { + return this.overlay.scrollStrategies.close({ + threshold: 10, + }); + } + } } private createPortal(): void { @@ -147,27 +161,6 @@ export class EvoTooltipService { } private initSubscriptions(): void { - const parentElement = this.targetElement ? this.targetElement : this.parentRef?.nativeElement; - const overlayElement = this.overlayRef.overlayElement; - - const mouseLeaveParentElement$ = fromEvent(parentElement, 'mouseleave').pipe(map(() => overlayElement)); - const mouseLeaveOverlayElement$ = fromEvent(overlayElement, 'mouseleave').pipe(map(() => parentElement)); - - merge(mouseLeaveParentElement$, mouseLeaveOverlayElement$) - .pipe( - filter((element) => !element.matches(':hover')), - debounceTime(this.config?.hideDelay ?? EVO_TOOLTIP_CONFIG.hideDelay), - filter(() => !parentElement.matches(':hover') && !overlayElement.matches(':hover')), - first(), - catchError(() => { - this.hideTooltip(); - return EMPTY; - }), - ) - .subscribe(() => { - this.hideTooltip(); - }); - this.positionStrategy.positionChanges.subscribe((value) => { this._position$.next(value.connectionPair.panelClass as EvoTooltipPosition); }); diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/types/evo-tooltip-scroll-strategy.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/types/evo-tooltip-scroll-strategy.ts new file mode 100644 index 000000000..e899cd4ce --- /dev/null +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/types/evo-tooltip-scroll-strategy.ts @@ -0,0 +1 @@ +export type EvoTooltipScrollStrategy = 'noop' | 'close' | 'reposition' | 'block'; From c3d9b1c3ce62e4e2ebc9876bc2df63dc018298e7 Mon Sep 17 00:00:00 2001 From: "a.kiselev" Date: Wed, 20 Aug 2025 13:31:26 +0300 Subject: [PATCH 05/10] feat(evo-tooltip-config): added scroll strategy to config --- .../lib/components/evo-tooltip/constants/evo-tooltip-config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/constants/evo-tooltip-config.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/constants/evo-tooltip-config.ts index a77faf984..2defc0e33 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/constants/evo-tooltip-config.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/constants/evo-tooltip-config.ts @@ -3,4 +3,5 @@ import {EvoTooltipConfig} from '../interfaces/evo-tooltip-config'; export const EVO_TOOLTIP_CONFIG: EvoTooltipConfig = { hideDelay: 300, showDelay: 100, + scrollStrategy: 'close', }; From 09f0bc6480529a40e8ec8708711bb4a7fbab1ab7 Mon Sep 17 00:00:00 2001 From: "a.kiselev" Date: Thu, 21 Aug 2025 15:42:39 +0300 Subject: [PATCH 06/10] feat(evo-tooltip): stories added --- .../lib/components/evo-tooltip/public-api.ts | 1 + src/stories/components/evo-tooltip.stories.ts | 96 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 src/stories/components/evo-tooltip.stories.ts diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/public-api.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/public-api.ts index 732c50d68..79a592ce9 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/public-api.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/public-api.ts @@ -1,6 +1,7 @@ export * from './evo-tooltip.module'; export * from './directives/evo-tooltip.directive'; export * from './types/evo-tooltip-position-type'; +export * from './enums/evo-tooltip-position'; export * from './interfaces/evo-tooltip-config'; export * from './interfaces/evo-tooltip-styles'; export * from './services/evo-tooltip.service'; diff --git a/src/stories/components/evo-tooltip.stories.ts b/src/stories/components/evo-tooltip.stories.ts new file mode 100644 index 000000000..e611e8bc4 --- /dev/null +++ b/src/stories/components/evo-tooltip.stories.ts @@ -0,0 +1,96 @@ +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {EvoButtonModule, EvoTooltipModule, EvoTooltipPosition} from '@evotor-dev/ui-kit'; +import {moduleMetadata} from '@storybook/angular'; + +export default { + title: 'Components/Tooltip', + + decorators: [ + moduleMetadata({ + imports: [EvoTooltipModule, EvoButtonModule, BrowserAnimationsModule], + }), + ], +}; + +const POSITIONS_LIST: EvoTooltipPosition[] = [ + EvoTooltipPosition.TOP_START, + EvoTooltipPosition.TOP, + EvoTooltipPosition.TOP_END, + + EvoTooltipPosition.BOTTOM_START, + EvoTooltipPosition.BOTTOM, + EvoTooltipPosition.BOTTOM_END, + + EvoTooltipPosition.RIGHT_START, + EvoTooltipPosition.RIGHT, + EvoTooltipPosition.RIGHT_END, + + EvoTooltipPosition.LEFT_START, + EvoTooltipPosition.LEFT, + EvoTooltipPosition.LEFT_END, +]; + +export const Default = () => ({ + template: ` + +
+ + +
    +
  • +

    {{item | titlecase }}

    +

    Tooltip parent with some content. Lorem ipsum dolor sit

    +
  • +
+ + + Some tooltip content + +
+ `, + props: { + POSITIONS_LIST: POSITIONS_LIST, + isDisabled: false + } +}); + +Default.storyName = 'default'; From 4b3858c84f0b914c1c818349c5c3d77e16ea2af0 Mon Sep 17 00:00:00 2001 From: "a.kiselev" Date: Mon, 1 Sep 2025 15:28:30 +0300 Subject: [PATCH 07/10] feat(evo-tooltip): stories added --- .../directives/evo-tooltip.directive.ts | 11 +++-- .../services/evo-tooltip.service.ts | 49 ++++++++++++++----- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.ts index 2c81fc23d..24bb0ecfa 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.ts @@ -118,10 +118,13 @@ export class EvoTooltipDirective implements OnInit, OnDestroy { this.tooltipService.hideTooltip(); return EMPTY; }), - takeUntil(this.destroy$), + takeUntil(merge( + this.destroy$, + this.tooltipService.isOpen$.pipe( + filter((isOpened: boolean) => !isOpened) + ) + )), ) - .subscribe(() => { - this.tooltipService.hideTooltip(); - }); + .subscribe(() => this.tooltipService.hideTooltip()); } } diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.ts index f3c121680..978ca940c 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.ts @@ -3,11 +3,13 @@ import { FlexibleConnectedPositionStrategy, Overlay, OverlayPositionBuilder, - OverlayRef, ScrollStrategy, + OverlayRef, + ScrollStrategy, } from '@angular/cdk/overlay'; import {ComponentPortal} from '@angular/cdk/portal'; -import {ComponentRef, ElementRef, Injectable, Injector, TemplateRef} from '@angular/core'; -import {BehaviorSubject, EMPTY, Observable, Subject} from 'rxjs'; +import {ComponentRef, ElementRef, Injectable, Injector, OnDestroy, TemplateRef} from '@angular/core'; +import {BehaviorSubject, EMPTY, merge, Observable, Subject} from 'rxjs'; +import {filter, take, takeUntil, tap} from 'rxjs/operators'; import {EVO_TOOLTIP_ARROW_SIZE} from '../constants/evo-tooltip-arrow-size'; import {EVO_CONNECTED_POSITION} from '../constants/evo-tooltip-connected-position'; import {EVO_DEFAULT_POSITIONS_ORDER} from '../constants/evo-tooltip-default-positions-order'; @@ -20,7 +22,7 @@ import {EvoTooltipStyles} from '../interfaces/evo-tooltip-styles'; import {EvoTooltipConfig} from '../public-api'; @Injectable() -export class EvoTooltipService { +export class EvoTooltipService implements OnDestroy { readonly stringContent$: Observable = EMPTY; readonly templateContent$: Observable | null> = EMPTY; readonly position$: Observable = EMPTY; @@ -38,9 +40,10 @@ export class EvoTooltipService { private readonly _styles$ = new BehaviorSubject(null); private readonly _visibleArrow$ = new BehaviorSubject(true); private readonly _isOpen$ = new Subject(); + private readonly destroy$ = new Subject(); - private overlayRef: OverlayRef; - private positionStrategy: FlexibleConnectedPositionStrategy; + private overlayRef: OverlayRef | null = null; + private positionStrategy: FlexibleConnectedPositionStrategy | null = null; private tooltipComponentRef: ComponentRef | null; constructor( @@ -58,6 +61,10 @@ export class EvoTooltipService { this.isOpen$ = this._isOpen$.asObservable(); } + ngOnDestroy(): void { + this.destroy$.next(); + } + showTooltip( parentRef: ElementRef, content: string | TemplateRef, @@ -71,7 +78,7 @@ export class EvoTooltipService { this.createOverlay(parentRef, position, config); this.createPortal(); - this._isOpen$.next(this.overlayRef.hasAttached()); + this._isOpen$.next(this.hasAttached); this.initSubscriptions(); return this.overlayRef.overlayElement; @@ -84,7 +91,7 @@ export class EvoTooltipService { } this.overlayRef?.detach(); - this._isOpen$.next(!!this.overlayRef?.hasAttached()); + this._isOpen$.next(this.hasAttached); } setArrowVisibility(hasArrow: boolean): void { @@ -161,9 +168,29 @@ export class EvoTooltipService { } private initSubscriptions(): void { - this.positionStrategy.positionChanges.subscribe((value) => { - this._position$.next(value.connectionPair.panelClass as EvoTooltipPosition); - }); + if (this.positionStrategy) { + this.positionStrategy.positionChanges.pipe( + takeUntil( + merge(this.destroy$, this._isOpen$.pipe( + filter((isOpened: boolean) => !isOpened), + )) + ), + ).subscribe((value) => { + this._position$.next(value.connectionPair.panelClass as EvoTooltipPosition); + }); + } + + if (this.overlayRef) { + this.overlayRef.detachments().pipe( + take(1), + tap(() => { + this.positionStrategy = null; + this.overlayRef = null; + this._isOpen$.next(false); + }), + takeUntil(this.destroy$), + ).subscribe(); + } } private getPositions(position: EvoTooltipPosition): ConnectedPosition[] { From 184ef871c4694c7b53e7955a8b43af488509e9dd Mon Sep 17 00:00:00 2001 From: "a.kiselev" Date: Fri, 1 May 2026 12:57:56 +0300 Subject: [PATCH 08/10] fix(evo-tooltip): fixed arrow logic --- .../evo-tooltip/evo-tooltip.component.ts | 122 +++++++++++------- 1 file changed, 74 insertions(+), 48 deletions(-) diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.ts index cb64f135d..cf3940ae2 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.ts @@ -5,12 +5,11 @@ import { ElementRef, HostBinding, OnDestroy, - OnInit, Renderer2, TemplateRef, } from '@angular/core'; -import {BehaviorSubject, combineLatest, EMPTY, Observable, Subject} from 'rxjs'; -import {filter, map, pairwise, startWith, takeUntil, tap} from 'rxjs/operators'; +import {combineLatest, Observable, Subject} from 'rxjs'; +import {map, pairwise, startWith, takeUntil, tap} from 'rxjs/operators'; import {EVO_TOOLTIP_ARROW_SIZE} from './constants/evo-tooltip-arrow-size'; import {EVO_TOOLTIP_FADEIN_ANIMATION} from './constants/evo-tooltip-fadein.animation'; import {EVO_TOOLTIP_RADIUS} from './constants/evo-tooltip-radius'; @@ -19,6 +18,28 @@ import {EvoTooltipStyleVariable} from './enums/evo-tooltip-style-variable'; import {EvoTooltipStyles} from './interfaces/evo-tooltip-styles'; import {EvoTooltipService} from './services/evo-tooltip.service'; +const START_POSITIONS_LIST: ReadonlyArray = [ + EvoTooltipPosition.TOP_START, + EvoTooltipPosition.BOTTOM_START, + EvoTooltipPosition.LEFT_START, + EvoTooltipPosition.RIGHT_START, +]; + +const END_POSITIONS_LIST: ReadonlyArray = [ + EvoTooltipPosition.TOP_END, + EvoTooltipPosition.BOTTOM_END, + EvoTooltipPosition.LEFT_END, + EvoTooltipPosition.RIGHT_END, +]; + +interface TooltipArrowCalcParams { + parentStart: number; + parentEnd: number; + tooltipStart: number; + tooltipEnd: number; + position: EvoTooltipPosition; +} + @Component({ selector: 'evo-tooltip', templateUrl: './evo-tooltip.component.html', @@ -26,37 +47,40 @@ import {EvoTooltipService} from './services/evo-tooltip.service'; changeDetection: ChangeDetectionStrategy.OnPush, animations: [EVO_TOOLTIP_FADEIN_ANIMATION], }) -export class EvoTooltipComponent implements OnInit, AfterViewInit, OnDestroy { +export class EvoTooltipComponent implements AfterViewInit, OnDestroy { readonly position$: Observable = this.tooltipService.position$; readonly stringContent$: Observable = this.tooltipService.stringContent$; readonly templateContent$: Observable> = this.tooltipService.templateContent$; readonly visibleArrow$: Observable = this.tooltipService.visibleArrow$; - readonly styles$: Observable = EMPTY; + + readonly styles$: Observable = combineLatest([ + this.position$, + this.tooltipService.styles$, + this.tooltipService.parentRef$, + this.visibleArrow$, + ]).pipe( + map( + ([position, baseStyles, parentRef, visibleArrow]: [ + EvoTooltipPosition, + EvoTooltipStyles, + ElementRef, + boolean, + ]) => + visibleArrow && parentRef + ? {...baseStyles, ...this.calculateArrowStyles(parentRef, position)} + : baseStyles, + ), + ); @HostBinding('@fadeIn') fadeIn = true; - private readonly _positionArrowStyles$ = new BehaviorSubject(null); private readonly _destroy$ = new Subject(); constructor( private readonly elementRef: ElementRef, private readonly tooltipService: EvoTooltipService, private readonly renderer: Renderer2, - ) { - this.styles$ = combineLatest([this.tooltipService.styles$, this._positionArrowStyles$]).pipe( - map(([style1, style2]) => ({...style1, ...style2})), - ); - } - - ngOnInit(): void { - combineLatest([this.position$, this.tooltipService.parentRef$, this.visibleArrow$]) - .pipe( - filter(([_position, _parentRef, visibleArrow]) => visibleArrow), - tap(([_, parentRef]) => this.setArrowPosition(parentRef)), - takeUntil(this._destroy$), - ) - .subscribe(); - } + ) {} ngAfterViewInit(): void { this.tooltipService.tooltipClasses$ @@ -78,14 +102,28 @@ export class EvoTooltipComponent implements OnInit, AfterViewInit, OnDestroy { this._destroy$.complete(); } - private getArrowOffset(params: { - parentStart: number; - parentEnd: number; - tooltipStart: number; - tooltipEnd: number; - }): number { + private getArrowStartEdge({parentStart, parentEnd, tooltipStart, position}: TooltipArrowCalcParams): number { + const parentWidth = parentEnd - parentStart; + const parentCenter = Math.round(parentWidth / 2); + + const arrowHalf = EVO_TOOLTIP_ARROW_SIZE / 2; + const safeBoundary = EVO_TOOLTIP_RADIUS + arrowHalf; + + let idealCenter: number; + + if (START_POSITIONS_LIST.includes(position)) { + idealCenter = Math.min(parentCenter, safeBoundary); + } else if (END_POSITIONS_LIST.includes(position)) { + idealCenter = Math.max(parentCenter, parentWidth - safeBoundary); + } else { + idealCenter = parentCenter; + } + + return Math.round(idealCenter + parentStart - tooltipStart - arrowHalf); + } + + private getArrowOffset(params: TooltipArrowCalcParams): number { const tooltipSize = params.tooltipEnd - params.tooltipStart; - const parentSize = params.parentEnd - params.parentStart; // tooltip after the parent if (params.parentEnd < params.tooltipStart) { @@ -97,28 +135,14 @@ export class EvoTooltipComponent implements OnInit, AfterViewInit, OnDestroy { return tooltipSize; } - const tooltipCenter = Math.round(tooltipSize / 2); - const parentCenterOnTooltip = Math.round(parentSize / 2 + params.parentStart - params.tooltipStart); - - const defaultArrowOffset = parentCenterOnTooltip - EVO_TOOLTIP_ARROW_SIZE / 2; - - // centers are is one positions - if (tooltipCenter === parentCenterOnTooltip) { - return defaultArrowOffset; - } - const minPosition = EVO_TOOLTIP_RADIUS; - const maxPosition = tooltipSize - EVO_TOOLTIP_ARROW_SIZE - EVO_TOOLTIP_RADIUS; + const arrowStartEdge = this.getArrowStartEdge(params); + const maxPosition = tooltipSize - EVO_TOOLTIP_RADIUS - EVO_TOOLTIP_ARROW_SIZE; - // parent center inside tooltip - if (defaultArrowOffset >= minPosition && parentCenterOnTooltip <= maxPosition) { - return defaultArrowOffset; - } - - return tooltipCenter > parentCenterOnTooltip ? minPosition : maxPosition; + return Math.max(minPosition, Math.min(arrowStartEdge, maxPosition)); } - private setArrowPosition(parentRef: ElementRef): void { + private calculateArrowStyles(parentRef: ElementRef, position: EvoTooltipPosition): EvoTooltipStyles { const parentRect = (parentRef.nativeElement as HTMLElement).getBoundingClientRect(); const tooltipRect = (this.elementRef.nativeElement as HTMLElement).getBoundingClientRect(); @@ -127,6 +151,7 @@ export class EvoTooltipComponent implements OnInit, AfterViewInit, OnDestroy { parentEnd: parentRect.bottom, tooltipStart: tooltipRect.top, tooltipEnd: tooltipRect.bottom, + position, }); const horizontal = this.getArrowOffset({ @@ -134,11 +159,12 @@ export class EvoTooltipComponent implements OnInit, AfterViewInit, OnDestroy { parentEnd: parentRect.right, tooltipStart: tooltipRect.left, tooltipEnd: tooltipRect.right, + position, }); - this._positionArrowStyles$.next({ + return { [EvoTooltipStyleVariable.VERTICAL_POSITION_ARROW]: `${vertical}px`, [EvoTooltipStyleVariable.HORIZONTAL_POSITION_ARROW]: `${horizontal}px`, - }); + }; } } From cb7eb04fb3b106f2fb778a06fd1f2ffbdb1dbcac Mon Sep 17 00:00:00 2001 From: "a.kiselev" Date: Fri, 1 May 2026 13:33:37 +0300 Subject: [PATCH 09/10] refactor(evo-tooltip): tooltip as standalone and use signals --- .../directives/evo-tooltip.directive.spec.ts | 58 ++++++------ .../directives/evo-tooltip.directive.ts | 88 ++++++++++--------- .../evo-tooltip/evo-tooltip.component.html | 28 +++--- .../evo-tooltip/evo-tooltip.component.spec.ts | 35 +++----- .../evo-tooltip/evo-tooltip.component.ts | 40 +++++---- .../evo-tooltip/evo-tooltip.module.ts | 9 +- .../services/evo-tooltip.service.spec.ts | 3 +- 7 files changed, 130 insertions(+), 131 deletions(-) diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.spec.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.spec.ts index 57c39104d..ed9a7e990 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.spec.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.spec.ts @@ -1,19 +1,18 @@ -import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { EvoTooltipDirective } from './evo-tooltip.directive'; -import { Component } from '@angular/core'; -import { EvoTooltipPosition } from '../enums/evo-tooltip-position'; -import { EvoTooltipStyles } from '../interfaces/evo-tooltip-styles'; -import { EvoTooltipStyleVariable } from '../enums/evo-tooltip-style-variable'; -import { CommonModule } from '@angular/common'; -import { EvoTooltipService } from '../services/evo-tooltip.service'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { first } from 'rxjs/operators'; -import { EvoTooltipComponent } from '../evo-tooltip.component'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {EvoTooltipDirective} from './evo-tooltip.directive'; +import {Component, NO_ERRORS_SCHEMA} from '@angular/core'; +import {EvoTooltipPosition} from '../enums/evo-tooltip-position'; +import {EvoTooltipStyles} from '../interfaces/evo-tooltip-styles'; +import {EvoTooltipStyleVariable} from '../enums/evo-tooltip-style-variable'; +import {CommonModule} from '@angular/common'; +import {EvoTooltipService} from '../services/evo-tooltip.service'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {first} from 'rxjs/operators'; +import {By} from "@angular/platform-browser"; @Component({ template: ` -
{ - let component: TestHostComponent; let fixture: ComponentFixture; + + let component: TestHostComponent; + let triggerEl: HTMLElement; + let directive: EvoTooltipDirective; let tooltipService: EvoTooltipService; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [TestHostComponent, EvoTooltipDirective, EvoTooltipComponent], - imports: [CommonModule, BrowserAnimationsModule], + declarations: [TestHostComponent], + imports: [CommonModule, BrowserAnimationsModule, EvoTooltipDirective], providers: [EvoTooltipService], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(TestHostComponent); component = fixture.componentInstance; - directive = fixture.debugElement.children[0].injector.get(EvoTooltipDirective); + + const directiveEl = fixture.debugElement.query(By.directive(EvoTooltipDirective)); + triggerEl = directiveEl.nativeElement; + + directive = directiveEl.injector.get(EvoTooltipDirective); tooltipService = TestBed.inject(EvoTooltipService); fixture.detectChanges(); }); @@ -66,15 +73,13 @@ describe('EvoTooltipDirective', () => { }); it('should have correct host classes', () => { - const element = fixture.debugElement.children[0].nativeElement; - expect(element.classList.contains('evo-tooltip-trigger')).toBeTrue(); + expect(triggerEl.classList.contains('evo-tooltip-trigger')).toBeTrue(); }); it('should add disabled class when disabled', () => { component.disabled = true; fixture.detectChanges(); - const element = fixture.debugElement.children[0].nativeElement; - expect(element.classList.contains('evo-tooltip-trigger_disabled')).toBeTrue(); + expect(triggerEl.classList.contains('evo-tooltip-trigger_disabled')).toBeTrue(); }); it('should emit open event when tooltip is shown', fakeAsync(() => { @@ -104,7 +109,8 @@ describe('EvoTooltipDirective', () => { })); it('should not show tooltip when content is empty', fakeAsync(() => { - directive.content = null; + component.content = null; + fixture.detectChanges(); directive.show(); tick(0); fixture.detectChanges(); @@ -112,8 +118,7 @@ describe('EvoTooltipDirective', () => { })); it('should handle mouseenter event', fakeAsync(() => { - const element = fixture.debugElement.children[0].nativeElement; - element.dispatchEvent(new MouseEvent('mouseenter')); + triggerEl.dispatchEvent(new MouseEvent('mouseenter')); tick(0); fixture.detectChanges(); tooltipService.isOpen$.pipe(first()).subscribe((isOpen) => { @@ -122,8 +127,7 @@ describe('EvoTooltipDirective', () => { })); it('should handle touchstart event', fakeAsync(() => { - const element = fixture.debugElement.children[0].nativeElement; - element.dispatchEvent(new MouseEvent('touchstart')); + triggerEl.dispatchEvent(new MouseEvent('touchstart')); tick(0); fixture.detectChanges(); tooltipService.isOpen$.pipe(first()).subscribe((isOpen) => { diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.ts index 24bb0ecfa..3e1c2f731 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/directives/evo-tooltip.directive.ts @@ -1,15 +1,17 @@ import { + DestroyRef, Directive, + effect, ElementRef, - EventEmitter, HostBinding, - Input, + inject, + input, OnDestroy, OnInit, - Output, + output, TemplateRef, } from '@angular/core'; -import {EMPTY, filter, fromEvent, merge, Subject} from 'rxjs'; +import {EMPTY, filter, fromEvent, merge} from 'rxjs'; import {catchError, debounceTime, distinctUntilChanged, first, map, takeUntil, tap, throttleTime} from 'rxjs/operators'; import {EVO_TOOLTIP_CONFIG} from '../constants/evo-tooltip-config'; import {EvoTooltipPosition} from '../enums/evo-tooltip-position'; @@ -17,37 +19,45 @@ import {EvoTooltipConfig} from '../interfaces/evo-tooltip-config'; import {EvoTooltipStyles} from '../interfaces/evo-tooltip-styles'; import {EvoTooltipService} from '../services/evo-tooltip.service'; import {EvoTooltipPositionType} from '../types/evo-tooltip-position-type'; +import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; @Directive({ selector: '[evoTooltip]', exportAs: 'evoTooltip', + standalone: true, providers: [EvoTooltipService], }) export class EvoTooltipDirective implements OnInit, OnDestroy { - @Input('evoTooltip') content: string | TemplateRef; - @Input('evoTooltipPosition') position: EvoTooltipPositionType | string = EvoTooltipPosition.BOTTOM; - @Input('evoTooltipDisabled') disabled = false; - @Input('evoTooltipConfig') config: Partial; - @Input() set evoTooltipVisibleArrow(visibleArrow: boolean) { - this.tooltipService.setArrowVisibility(visibleArrow); - } - @Input() set evoTooltipStyles(tooltipStyles: EvoTooltipStyles) { - this.tooltipService.setTooltipStyles(tooltipStyles); - } - @Input() set evoTooltipClass(tooltipClass: string | string[]) { - this.tooltipService.setTooltipClass(tooltipClass); - } + readonly content = input>('', {alias: 'evoTooltip'}); + + readonly position = input(EvoTooltipPosition.BOTTOM, {alias: 'evoTooltipPosition'}); + + readonly disabled = input(false, {alias: 'evoTooltipDisabled'}); + + readonly config = input>(EVO_TOOLTIP_CONFIG, {alias: 'evoTooltipConfig'}); + + readonly isVisibleArrow = input(true, {alias: 'evoTooltipVisibleArrow'}); - @Output() evoTooltipOpen = new EventEmitter(); - @Output() evoTooltipClose = new EventEmitter(); + readonly tooltipStyles = input({}, {alias: 'evoTooltipStyles'}); + + readonly tooltipClass = input([], {alias: 'evoTooltipClass'}); + + readonly evoTooltipOpen = output() + readonly evoTooltipClose = output() + + private readonly destroyRef = inject(DestroyRef); + private readonly elementRef = inject(ElementRef); + private readonly tooltipService = inject(EvoTooltipService); @HostBinding('class') get hostClasses(): string[] { - return ['evo-tooltip-trigger', ...(this.disabled ? ['evo-tooltip-trigger_disabled'] : [])]; + return ['evo-tooltip-trigger', ...(this.disabled() ? ['evo-tooltip-trigger_disabled'] : [])]; } - private readonly destroy$ = new Subject(); - - constructor(private readonly elementRef: ElementRef, private readonly tooltipService: EvoTooltipService) {} + constructor() { + effect((): void => this.tooltipService.setArrowVisibility(this.isVisibleArrow())); + effect((): void => this.tooltipService.setTooltipStyles(this.tooltipStyles())); + effect((): void => this.tooltipService.setTooltipClass(this.tooltipClass())); + } ngOnInit(): void { this.initSubscriptions(); @@ -55,8 +65,6 @@ export class EvoTooltipDirective implements OnInit, OnDestroy { ngOnDestroy(): void { this.hide(); - this.destroy$.next(); - this.destroy$.complete(); } hide(): void { @@ -64,15 +72,17 @@ export class EvoTooltipDirective implements OnInit, OnDestroy { } show(): void { - if (!this.content || this.tooltipService.hasAttached || this.disabled) { + const content = this.content(); + + if (!content || this.tooltipService.hasAttached || this.disabled()) { return; } const tooltip = this.tooltipService.showTooltip( this.elementRef, - this.content, - this.position as EvoTooltipPosition, - {...EVO_TOOLTIP_CONFIG, ...this.config}, + content, + this.position() as EvoTooltipPosition, + {...EVO_TOOLTIP_CONFIG, ...this.config()}, ); this.initHideSubscription(tooltip); @@ -83,23 +93,23 @@ export class EvoTooltipDirective implements OnInit, OnDestroy { merge(fromEvent(element, 'mouseenter'), fromEvent(element, 'touchstart')) .pipe( - throttleTime(this.config?.showDelay ?? EVO_TOOLTIP_CONFIG.showDelay), - tap(() => this.show()), - takeUntil(this.destroy$), + throttleTime(this.config()?.showDelay ?? EVO_TOOLTIP_CONFIG.showDelay), + tap((): void => this.show()), + takeUntilDestroyed(this.destroyRef), ) .subscribe(); this.tooltipService.isOpen$ .pipe( distinctUntilChanged(), - tap((isOpen) => { + tap((isOpen): void => { if (isOpen) { this.evoTooltipOpen.emit(); } else { this.evoTooltipClose.emit(); } }), - takeUntil(this.destroy$), + takeUntilDestroyed(this.destroyRef), ) .subscribe(); } @@ -111,19 +121,15 @@ export class EvoTooltipDirective implements OnInit, OnDestroy { merge(mouseLeaveParentElement$, mouseLeaveOverlayElement$) .pipe( filter((element) => !element.matches(':hover')), - debounceTime(this.config?.hideDelay ?? EVO_TOOLTIP_CONFIG.hideDelay), + debounceTime(this.config()?.hideDelay ?? EVO_TOOLTIP_CONFIG.hideDelay), filter(() => !tooltip.matches(':hover') && !this.elementRef.nativeElement.matches(':hover')), first(), catchError(() => { this.tooltipService.hideTooltip(); return EMPTY; }), - takeUntil(merge( - this.destroy$, - this.tooltipService.isOpen$.pipe( - filter((isOpened: boolean) => !isOpened) - ) - )), + takeUntil(this.tooltipService.isOpen$.pipe(filter((isOpened: boolean) => !isOpened))), + takeUntilDestroyed(this.destroyRef), ) .subscribe(() => this.tooltipService.hideTooltip()); } diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.html b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.html index dc12a1f5e..a1776c097 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.html +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.html @@ -1,16 +1,14 @@ - -
- {{ content }} -
-
- - - - + @if (stringContent(); as content) { + {{ content }} + } @else { + @if (templateContent(); as content) { + + } + } +
diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.spec.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.spec.ts index f27e466e6..d11bc25a9 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.spec.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.spec.ts @@ -6,13 +6,12 @@ import {EvoTooltipPosition} from './enums/evo-tooltip-position'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {EvoTooltipStyles} from './interfaces/evo-tooltip-styles'; import {EvoTooltipStyleVariable} from './enums/evo-tooltip-style-variable'; -import {CommonModule} from '@angular/common'; @Component({ selector: 'evo-host-component', template: `
Parent
- +
Test template content
@@ -33,8 +32,8 @@ describe('EvoTooltipComponent', () => { beforeEach( waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [EvoTooltipComponent, TestHostComponent], - imports: [BrowserAnimationsModule, CommonModule], + declarations: [TestHostComponent], + imports: [BrowserAnimationsModule, EvoTooltipComponent], schemas: [NO_ERRORS_SCHEMA], providers: [EvoTooltipService], }).compileComponents(); @@ -59,9 +58,7 @@ describe('EvoTooltipComponent', () => { tooltipService['_position$'].next(position); testHostFixture.detectChanges(); - tooltipComponent.position$.subscribe((value) => { - expect(value).toBe(position); - }); + expect(tooltipComponent.position()).toBe(position); }); it('should update string content when stringContent$ changes', () => { @@ -69,9 +66,7 @@ describe('EvoTooltipComponent', () => { tooltipService['_stringContent$'].next(content); testHostFixture.detectChanges(); - tooltipComponent.stringContent$.subscribe((value) => { - expect(value).toBe(content); - }); + expect(tooltipComponent.stringContent()).toBe(content); }); it('should update template content when templateContent$ changes', () => { @@ -79,9 +74,7 @@ describe('EvoTooltipComponent', () => { tooltipService['_templateContent$'].next(template); testHostFixture.detectChanges(); - tooltipComponent.templateContent$.subscribe((value) => { - expect(value).toBe(template); - }); + expect(tooltipComponent.templateContent()).toBe(template); }); it('should update styles when styles$ changes', () => { @@ -92,8 +85,10 @@ describe('EvoTooltipComponent', () => { tooltipService['_styles$'].next(styles); testHostFixture.detectChanges(); - tooltipComponent.styles$.subscribe((value) => { - Object.keys(styles).forEach((key: string) => expect(value[key]).toEqual(styles[key])); + const tooltipStyles: EvoTooltipStyles = tooltipComponent.styles(); + + Object.keys(styles).forEach((key): void => { + expect(tooltipStyles[key]).toEqual(styles[key]); }); }); @@ -102,14 +97,6 @@ describe('EvoTooltipComponent', () => { tooltipService['_visibleArrow$'].next(visibleArrow); testHostFixture.detectChanges(); - tooltipComponent.visibleArrow$.subscribe((value) => { - expect(value).toBe(visibleArrow); - }); - }); - - it('should unsubscribe from all observables on destroy', () => { - const destroySpy = spyOn(tooltipComponent['_destroy$'], 'next'); - tooltipComponent.ngOnDestroy(); - expect(destroySpy).toHaveBeenCalled(); + expect(tooltipComponent.visibleArrow()).toBe(visibleArrow); }); }); diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.ts index cf3940ae2..2d2a7635d 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.ts @@ -2,14 +2,17 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, + DestroyRef, ElementRef, HostBinding, + inject, OnDestroy, Renderer2, + Signal, TemplateRef, } from '@angular/core'; -import {combineLatest, Observable, Subject} from 'rxjs'; -import {map, pairwise, startWith, takeUntil, tap} from 'rxjs/operators'; +import {combineLatest} from 'rxjs'; +import {map, pairwise, startWith, tap} from 'rxjs/operators'; import {EVO_TOOLTIP_ARROW_SIZE} from './constants/evo-tooltip-arrow-size'; import {EVO_TOOLTIP_FADEIN_ANIMATION} from './constants/evo-tooltip-fadein.animation'; import {EVO_TOOLTIP_RADIUS} from './constants/evo-tooltip-radius'; @@ -17,6 +20,8 @@ import {EvoTooltipPosition} from './enums/evo-tooltip-position'; import {EvoTooltipStyleVariable} from './enums/evo-tooltip-style-variable'; import {EvoTooltipStyles} from './interfaces/evo-tooltip-styles'; import {EvoTooltipService} from './services/evo-tooltip.service'; +import {takeUntilDestroyed, toSignal} from "@angular/core/rxjs-interop"; +import {NgTemplateOutlet} from "@angular/common"; const START_POSITIONS_LIST: ReadonlyArray = [ EvoTooltipPosition.TOP_START, @@ -46,18 +51,22 @@ interface TooltipArrowCalcParams { styleUrls: ['./evo-tooltip.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, animations: [EVO_TOOLTIP_FADEIN_ANIMATION], + standalone: true, + imports: [ + NgTemplateOutlet + ] }) export class EvoTooltipComponent implements AfterViewInit, OnDestroy { - readonly position$: Observable = this.tooltipService.position$; - readonly stringContent$: Observable = this.tooltipService.stringContent$; - readonly templateContent$: Observable> = this.tooltipService.templateContent$; - readonly visibleArrow$: Observable = this.tooltipService.visibleArrow$; + readonly position: Signal = toSignal(this.tooltipService.position$); + readonly stringContent: Signal = toSignal(this.tooltipService.stringContent$); + readonly templateContent: Signal> = toSignal(this.tooltipService.templateContent$); + readonly visibleArrow: Signal = toSignal(this.tooltipService.visibleArrow$); - readonly styles$: Observable = combineLatest([ - this.position$, + readonly styles: Signal = toSignal(combineLatest([ + this.tooltipService.position$, this.tooltipService.styles$, this.tooltipService.parentRef$, - this.visibleArrow$, + this.tooltipService.visibleArrow$, ]).pipe( map( ([position, baseStyles, parentRef, visibleArrow]: [ @@ -65,22 +74,23 @@ export class EvoTooltipComponent implements AfterViewInit, OnDestroy { EvoTooltipStyles, ElementRef, boolean, - ]) => + ]): EvoTooltipStyles => visibleArrow && parentRef ? {...baseStyles, ...this.calculateArrowStyles(parentRef, position)} : baseStyles, ), - ); + )); @HostBinding('@fadeIn') fadeIn = true; - private readonly _destroy$ = new Subject(); + private readonly destroyRef = inject(DestroyRef); constructor( private readonly elementRef: ElementRef, private readonly tooltipService: EvoTooltipService, private readonly renderer: Renderer2, - ) {} + ) { + } ngAfterViewInit(): void { this.tooltipService.tooltipClasses$ @@ -91,15 +101,13 @@ export class EvoTooltipComponent implements AfterViewInit, OnDestroy { (a || []).forEach((oldClass) => this.renderer.removeClass(this.elementRef.nativeElement, oldClass)); (b || []).forEach((newClass) => this.renderer.addClass(this.elementRef.nativeElement, newClass)); }), - takeUntil(this._destroy$), + takeUntilDestroyed(this.destroyRef), ) .subscribe(); } ngOnDestroy(): void { this.fadeIn = false; - this._destroy$.next(); - this._destroy$.complete(); } private getArrowStartEdge({parentStart, parentEnd, tooltipStart, position}: TooltipArrowCalcParams): number { diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.module.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.module.ts index 4c2c36cf7..b15de7170 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.module.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.module.ts @@ -1,12 +1,9 @@ import {NgModule} from '@angular/core'; -import {CommonModule} from '@angular/common'; -import {EvoTooltipComponent} from './evo-tooltip.component'; import {EvoTooltipDirective} from './directives/evo-tooltip.directive'; -import {OverlayModule} from '@angular/cdk/overlay'; @NgModule({ - imports: [OverlayModule, CommonModule], - declarations: [EvoTooltipComponent, EvoTooltipDirective], + imports: [EvoTooltipDirective], exports: [EvoTooltipDirective], }) -export class EvoTooltipModule {} +export class EvoTooltipModule { +} diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.spec.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.spec.ts index 345d01221..6f9221459 100644 --- a/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.spec.ts +++ b/projects/evo-ui-kit/src/lib/components/evo-tooltip/services/evo-tooltip.service.spec.ts @@ -16,8 +16,7 @@ describe('EvoTooltipService', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [CommonModule, BrowserAnimationsModule], - declarations: [EvoTooltipComponent], + imports: [CommonModule, BrowserAnimationsModule, EvoTooltipComponent], schemas: [NO_ERRORS_SCHEMA], providers: [EvoTooltipService], }); From f3cac3e230ac142c72582b3833a500436834b323 Mon Sep 17 00:00:00 2001 From: "a.kiselev" Date: Tue, 5 May 2026 19:43:50 +0300 Subject: [PATCH 10/10] feat(evo-tooltip-trigger): use styles --- .../components/evo-tooltip-trigger.scss} | 0 projects/evo-ui-kit/src/lib/styles/main.scss | 1 + 2 files changed, 1 insertion(+) rename projects/evo-ui-kit/src/lib/{components/evo-tooltip/assets/evo-tooltip-global.scss => styles/components/evo-tooltip-trigger.scss} (100%) diff --git a/projects/evo-ui-kit/src/lib/components/evo-tooltip/assets/evo-tooltip-global.scss b/projects/evo-ui-kit/src/lib/styles/components/evo-tooltip-trigger.scss similarity index 100% rename from projects/evo-ui-kit/src/lib/components/evo-tooltip/assets/evo-tooltip-global.scss rename to projects/evo-ui-kit/src/lib/styles/components/evo-tooltip-trigger.scss diff --git a/projects/evo-ui-kit/src/lib/styles/main.scss b/projects/evo-ui-kit/src/lib/styles/main.scss index 1f5990798..21c191fb1 100644 --- a/projects/evo-ui-kit/src/lib/styles/main.scss +++ b/projects/evo-ui-kit/src/lib/styles/main.scss @@ -22,3 +22,4 @@ @import './components/evo-form.scss'; @import './components/evo-dropdown.scss'; @import './components/skeleton.scss'; +@import "components/evo-tooltip-trigger";