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', }; 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..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 { EvoTooltipVariableArrowPosition } from '../enums/evo-tooltip-variable-arrow-position'; -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,16 @@ 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) => { + expect(isOpen).toBeTrue(); + }); + })); + + it('should handle touchstart event', fakeAsync(() => { + 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 9a59ffdf2..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,55 +1,63 @@ import { + DestroyRef, Directive, + effect, ElementRef, - EventEmitter, HostBinding, - Input, + inject, + input, OnDestroy, OnInit, - Output, + output, TemplateRef, } from '@angular/core'; -import {fromEvent, 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} 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'; +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'}); - @Output() evoTooltipOpen = new EventEmitter(); - @Output() evoTooltipClose = new EventEmitter(); + readonly position = input(EvoTooltipPosition.BOTTOM, {alias: 'evoTooltipPosition'}); - @HostBinding('class') get hostClasses(): string[] { - return ['evo-tooltip-trigger', ...(this.disabled ? ['evo-tooltip-trigger_disabled'] : [])]; - } + readonly disabled = input(false, {alias: 'evoTooltipDisabled'}); + + readonly config = input>(EVO_TOOLTIP_CONFIG, {alias: 'evoTooltipConfig'}); - readonly isOpen$: Observable = this.tooltipService.isOpen$; + readonly isVisibleArrow = input(true, {alias: 'evoTooltipVisibleArrow'}); - private readonly destroy$ = new Subject(); + readonly tooltipStyles = input({}, {alias: 'evoTooltipStyles'}); - constructor(private readonly elementRef: ElementRef, private readonly tooltipService: EvoTooltipService) {} + 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'] : [])]; + } + + 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(); @@ -57,46 +65,72 @@ export class EvoTooltipDirective implements OnInit, OnDestroy { ngOnDestroy(): void { this.hide(); - this.destroy$.next(); - this.destroy$.complete(); } hide(): void { this.tooltipService.hideTooltip(); } - show(event?: MouseEvent): void { - if (!this.content || this.tooltipService.hasAttached || this.disabled) { + show(): void { + const content = this.content(); + + if (!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, + content, + this.position() as EvoTooltipPosition, + {...EVO_TOOLTIP_CONFIG, ...this.config()}, ); + + this.initHideSubscription(tooltip); } 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(() => { - 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( - tap((isOpen) => { - isOpen ? this.evoTooltipOpen.emit() : this.evoTooltipClose.emit(); + distinctUntilChanged(), + tap((isOpen): void => { + if (isOpen) { + this.evoTooltipOpen.emit(); + } else { + this.evoTooltipClose.emit(); + } }), - takeUntil(this.destroy$), + takeUntilDestroyed(this.destroyRef), ) .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.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/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.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.scss b/projects/evo-ui-kit/src/lib/components/evo-tooltip/evo-tooltip.component.scss index 0ccbb3b20..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 @@ -1,4 +1,4 @@ -@import '../../styles/mixins'; +@import "../../styles/mixins"; :host { --evo-tooltip-horizontal-position-arrow: 50%; @@ -15,7 +15,7 @@ } .evo-tooltip { - $arrow-size: 8px; + $arrow-border-width: 8px; display: inline-block; position: relative; @@ -38,64 +38,19 @@ &:not(&_not-arrow):before { content: ""; position: absolute; - border-style: solid; - } - - &_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); - } + left: var(--evo-tooltip-horizontal-position-arrow); + top: var(--evo-tooltip-vertical-position-arrow); + width: 0; + height: 0; + border: $arrow-border-width solid transparent; + border-top: $arrow-border-width solid var(--evo-tooltip-background-color); } &_right-start, &_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); } } @@ -103,28 +58,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: rotate(180deg); } } @@ -132,28 +66,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); } } } 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..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 @@ -1,17 +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 {CommonModule} from '@angular/common'; +import {EvoTooltipStyleVariable} from './enums/evo-tooltip-style-variable'; @Component({ selector: 'evo-host-component', template: ` - +
Parent
+
Test template content
@@ -20,6 +20,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', () => { @@ -31,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(); @@ -44,6 +45,7 @@ describe('EvoTooltipComponent', () => { testHostComponent = testHostFixture.componentInstance; tooltipComponent = testHostComponent.tooltipComponent; tooltipService = TestBed.inject(EvoTooltipService); + tooltipService['_parentRef$'].next(testHostComponent.parentRef); testHostFixture.detectChanges(); }); @@ -56,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', () => { @@ -66,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', () => { @@ -76,21 +74,21 @@ 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', () => { 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); + const tooltipStyles: EvoTooltipStyles = tooltipComponent.styles(); + + Object.keys(styles).forEach((key): void => { + expect(tooltipStyles[key]).toEqual(styles[key]); }); }); @@ -99,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 9fb9f2eea..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,22 +2,48 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, + DestroyRef, ElementRef, HostBinding, + inject, OnDestroy, - OnInit, Renderer2, + Signal, TemplateRef, } from '@angular/core'; -import {BehaviorSubject, combineLatest, EMPTY, Observable, Subject} from 'rxjs'; -import {filter, map, pairwise, startWith, takeUntil, tap} from 'rxjs/operators'; -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 {EvoTooltipPosition} from './enums/evo-tooltip-position'; +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'; -import {EvoTooltipVariableArrowPosition} from './enums/evo-tooltip-variable-arrow-position'; +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, + 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', @@ -25,51 +51,45 @@ import {EvoTooltipVariableArrowPosition} from './enums/evo-tooltip-variable-arro styleUrls: ['./evo-tooltip.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, animations: [EVO_TOOLTIP_FADEIN_ANIMATION], + standalone: true, + imports: [ + NgTemplateOutlet + ] }) -export class EvoTooltipComponent implements OnInit, 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; +export class EvoTooltipComponent implements AfterViewInit, OnDestroy { + 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: Signal = toSignal(combineLatest([ + this.tooltipService.position$, + this.tooltipService.styles$, + this.tooltipService.parentRef$, + this.tooltipService.visibleArrow$, + ]).pipe( + map( + ([position, baseStyles, parentRef, visibleArrow]: [ + EvoTooltipPosition, + EvoTooltipStyles, + ElementRef, + boolean, + ]): EvoTooltipStyles => + visibleArrow && parentRef + ? {...baseStyles, ...this.calculateArrowStyles(parentRef, position)} + : baseStyles, + ), + )); @HostBinding('@fadeIn') fadeIn = true; - private readonly _positionArrowStyles$ = new BehaviorSubject(null); - private readonly _destroy$ = new Subject(); + private readonly destroyRef = inject(DestroyRef); 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), - // Вычисление стрелки нужно только для угловых позиций - 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); - }), - takeUntil(this._destroy$), - ) - .subscribe(); } ngAfterViewInit(): void { @@ -81,45 +101,78 @@ export class EvoTooltipComponent implements OnInit, 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 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; - - this._positionArrowStyles$.next({ - [EvoTooltipVariableArrowPosition.VERTICAL_POSITION_ARROW]: `${verticalPositionArrow}px`, - [EvoTooltipVariableArrowPosition.HORIZONTAL_POSITION_ARROW]: `${horizontalPositionArrow}px`, + 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; + + // tooltip after the parent + if (params.parentEnd < params.tooltipStart) { + return -EVO_TOOLTIP_ARROW_SIZE; + } + + // tooltip before the parent + if (params.parentStart > params.tooltipEnd) { + return tooltipSize; + } + + const minPosition = EVO_TOOLTIP_RADIUS; + const arrowStartEdge = this.getArrowStartEdge(params); + const maxPosition = tooltipSize - EVO_TOOLTIP_RADIUS - EVO_TOOLTIP_ARROW_SIZE; + + return Math.max(minPosition, Math.min(arrowStartEdge, maxPosition)); + } + + private calculateArrowStyles(parentRef: ElementRef, position: EvoTooltipPosition): EvoTooltipStyles { + 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, + position, + }); + + const horizontal = this.getArrowOffset({ + parentStart: parentRect.left, + parentEnd: parentRect.right, + tooltipStart: tooltipRect.left, + tooltipEnd: tooltipRect.right, + position, }); + + return { + [EvoTooltipStyleVariable.VERTICAL_POSITION_ARROW]: `${vertical}px`, + [EvoTooltipStyleVariable.HORIZONTAL_POSITION_ARROW]: `${horizontal}px`, + }; } } 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/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/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/public-api.ts b/projects/evo-ui-kit/src/lib/components/evo-tooltip/public-api.ts index 06bf44990..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,2 +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/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..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 @@ -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'; @@ -16,8 +16,7 @@ describe('EvoTooltipService', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [CommonModule, BrowserAnimationsModule], - declarations: [EvoTooltipComponent], + imports: [CommonModule, BrowserAnimationsModule, EvoTooltipComponent], schemas: [NO_ERRORS_SCHEMA], providers: [EvoTooltipService], }); @@ -46,8 +45,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 26c3e1f0d..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 @@ -1,28 +1,28 @@ -import {ComponentRef, ElementRef, Injectable, Injector, TemplateRef} from '@angular/core'; -import {ComponentPortal} from '@angular/cdk/portal'; import { ConnectedPosition, FlexibleConnectedPositionStrategy, Overlay, OverlayPositionBuilder, 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, 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_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_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 {EVO_TOOLTIP_OFFSET} from '../constants/evo-tooltip-offset'; +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; @@ -37,15 +37,14 @@ 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(); + 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; - private targetElement: EventTarget | null; constructor( private readonly overlay: Overlay, @@ -62,24 +61,27 @@ export class EvoTooltipService { this.isOpen$ = this._isOpen$.asObservable(); } + ngOnDestroy(): void { + this.destroy$.next(); + } + showTooltip( parentRef: ElementRef, 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._isOpen$.next(this.hasAttached); this.initSubscriptions(); + + return this.overlayRef.overlayElement; } hideTooltip(): void { @@ -89,7 +91,7 @@ export class EvoTooltipService { } this.overlayRef?.detach(); - this._isOpen$.next(!!this.overlayRef?.hasAttached()); + this._isOpen$.next(this.hasAttached); } setArrowVisibility(hasArrow: boolean): void { @@ -105,8 +107,8 @@ export class EvoTooltipService { !tooltipClassOrClasses ? [] : Array.isArray(tooltipClassOrClasses) - ? tooltipClassOrClasses - : [tooltipClassOrClasses], + ? tooltipClassOrClasses + : [tooltipClassOrClasses], ); } @@ -114,10 +116,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,13 +128,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.reposition(); - this.overlayRef = this.overlay.create({positionStrategy: this.positionStrategy, scrollStrategy}); + this.overlayRef = this.overlay.create({ + positionStrategy: this.positionStrategy, + scrollStrategy: this.getScrollStrategy(config) + }); + } + + 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 { @@ -145,30 +168,29 @@ 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(); + 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); }); + } - this.positionStrategy.positionChanges.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[] { 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'; 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"; 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';