diff --git a/packages/components/navbar-ic/navbar-ic-header.scss b/packages/components/navbar-ic/navbar-ic-header.scss index 9ce4e01d53..628a3996f5 100644 --- a/packages/components/navbar-ic/navbar-ic-header.scss +++ b/packages/components/navbar-ic/navbar-ic-header.scss @@ -42,10 +42,6 @@ &.kbq-navbar-ic-header_interactive { cursor: pointer; } - - &.kbq-collapsed .kbq-navbar-ic-title { - display: none; - } } a.kbq-navbar-ic-header { diff --git a/packages/components/navbar-ic/navbar-ic-header.ts b/packages/components/navbar-ic/navbar-ic-header.ts index a979bde015..44d38847bf 100644 --- a/packages/components/navbar-ic/navbar-ic-header.ts +++ b/packages/components/navbar-ic/navbar-ic-header.ts @@ -1,6 +1,7 @@ import { AfterContentInit, ChangeDetectionStrategy, + ChangeDetectorRef, Component, ContentChild, DestroyRef, @@ -10,8 +11,10 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { kbqInjectNativeElement, PopUpPlacements, PopUpTriggers } from '@koobiq/components/core'; import { KbqTooltipTrigger } from '@koobiq/components/tooltip'; +import { distinctUntilChanged } from 'rxjs/operators'; import { KbqNavbarIc } from './navbar-ic'; import { KbqNavbarIcLogo, KbqNavbarIcTitle } from './navbar-ic-item'; +import { toggleNavbarIcItemAnimation } from './navbar-ic.animation'; @Component({ standalone: true, @@ -19,6 +22,11 @@ import { KbqNavbarIcLogo, KbqNavbarIcTitle } from './navbar-ic-item'; exportAs: 'kbqNavbarIcHeader', template: `
+ + +
+ +
`, @@ -28,12 +36,14 @@ import { KbqNavbarIcLogo, KbqNavbarIcTitle } from './navbar-ic-item'; '[class.kbq-navbar-ic-header_interactive]': 'isLink' }, changeDetection: ChangeDetectionStrategy.OnPush, - encapsulation: ViewEncapsulation.None + encapsulation: ViewEncapsulation.None, + animations: [toggleNavbarIcItemAnimation()] }) export class KbqNavbarIcHeader extends KbqTooltipTrigger implements AfterContentInit { protected readonly navbar = inject(KbqNavbarIc); protected readonly nativeElement = kbqInjectNativeElement(); protected readonly destroyRef = inject(DestroyRef); + protected readonly changeDetectorRef = inject(ChangeDetectorRef); @ContentChild(KbqNavbarIcLogo) logo: KbqNavbarIcLogo; @ContentChild(KbqNavbarIcTitle) title: KbqNavbarIcTitle; @@ -61,6 +71,10 @@ export class KbqNavbarIcHeader extends KbqTooltipTrigger implements AfterContent .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => this.title?.checkTextOverflown()); + this.navbar.state.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)).subscribe(() => { + this.changeDetectorRef.detectChanges(); + }); + this.title?.checkTextOverflown(); if (this.title?.isTextOverflown) { diff --git a/packages/components/navbar-ic/navbar-ic-item.html b/packages/components/navbar-ic/navbar-ic-item.html index 8120e26d10..96a858ce25 100644 --- a/packages/components/navbar-ic/navbar-ic-item.html +++ b/packages/components/navbar-ic/navbar-ic-item.html @@ -2,7 +2,7 @@ @if (title && !collapsed) { -
+
@if (showDropDownAngle) { diff --git a/packages/components/navbar-ic/navbar-ic-item.scss b/packages/components/navbar-ic/navbar-ic-item.scss index 9ac71e42cd..4c6302be93 100644 --- a/packages/components/navbar-ic/navbar-ic-item.scss +++ b/packages/components/navbar-ic/navbar-ic-item.scss @@ -23,10 +23,6 @@ top: 0; right: 0; } - - & .kbq-navbar-ic-item__title-container { - display: none; - } } & > .kbq-icon { diff --git a/packages/components/navbar-ic/navbar-ic-item.ts b/packages/components/navbar-ic/navbar-ic-item.ts index 44b3910bba..33f152eaa6 100644 --- a/packages/components/navbar-ic/navbar-ic-item.ts +++ b/packages/components/navbar-ic/navbar-ic-item.ts @@ -34,6 +34,7 @@ import { KbqTooltipTrigger, TooltipModifier } from '@koobiq/components/tooltip'; import { Subject } from 'rxjs'; import { take } from 'rxjs/operators'; import { KbqNavbarIc } from './navbar-ic'; +import { toggleNavbarIcItemAnimation } from './navbar-ic.animation'; /** * The maximum number of characters that can be placed in a title without being wrapped. @@ -261,10 +262,12 @@ export class KbqNavbarIcFocusableItem implements AfterContentInit, AfterViewInit changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, hostDirectives: [KbqRectangleItem], - imports: [KbqIcon] + imports: [KbqIcon], + animations: [toggleNavbarIcItemAnimation()] }) export class KbqNavbarIcItem extends KbqTooltipTrigger implements AfterContentInit { readonly rectangleElement = inject(KbqRectangleItem); + readonly navbar = inject(KbqNavbarIc); readonly navbarFocusableItem = inject(KbqNavbarIcFocusableItem); private changeDetectorRef = inject(ChangeDetectorRef); @@ -310,11 +313,7 @@ export class KbqNavbarIcItem extends KbqTooltipTrigger implements AfterContentIn } get disabled(): boolean { - if (this._disabled !== undefined) { - return this._disabled; - } - - return !this.collapsed && !this.hasCroppedText; + return this.collapsed || !this.hasCroppedText || this._disabled; } set disabled(value) { @@ -341,7 +340,7 @@ export class KbqNavbarIcItem extends KbqTooltipTrigger implements AfterContentIn } this.rectangleElement.state.subscribe(() => { - this.collapsed = this.rectangleElement.collapsed; + this.disabled = this.collapsed = this.rectangleElement.collapsed; this.changeDetectorRef.markForCheck(); }); diff --git a/packages/components/navbar-ic/navbar-ic.animation.ts b/packages/components/navbar-ic/navbar-ic.animation.ts index 570410e003..84076e2178 100644 --- a/packages/components/navbar-ic/navbar-ic.animation.ts +++ b/packages/components/navbar-ic/navbar-ic.animation.ts @@ -4,5 +4,13 @@ export function toggleNavbarIcAnimation(): AnimationTriggerMetadata { return trigger('toggle', [ state('0', style({ width: '64px' })), state('1', style({ width: '240px' })), - transition('0 <=> 1', animate('150ms ease-out'))]); + transition('0 <=> 1', animate('200ms cubic-bezier(0.4, 0, 0.2, 1)'))]); +} + +export function toggleNavbarIcItemAnimation(): AnimationTriggerMetadata { + return trigger('toggle', [ + state('expanded', style({ opacity: 1, marginLeft: 0 })), + state('collapsed', style({ opacity: 0, marginLeft: '-8px' })), + transition('collapsed => expanded', animate('200ms 150ms cubic-bezier(0.4, 0, 0.2, 1)')), + transition('expanded => collapsed', animate('200ms cubic-bezier(0.4, 0, 0.2, 1)'))]); } diff --git a/packages/components/navbar-ic/navbar-ic.ts b/packages/components/navbar-ic/navbar-ic.ts index cb1cb68398..bdd23e960f 100644 --- a/packages/components/navbar-ic/navbar-ic.ts +++ b/packages/components/navbar-ic/navbar-ic.ts @@ -28,8 +28,8 @@ import { KBQ_LOCALE_SERVICE, KbqRectangleItem, ruRULocaleData } from '@koobiq/co import { KbqDropdownTrigger } from '@koobiq/components/dropdown'; import { KbqNotificationCenterTrigger } from '@koobiq/components/notification-center'; import { KbqPopoverTrigger } from '@koobiq/components/popover'; -import { BehaviorSubject, combineLatest, merge, Observable, Subject, Subscription } from 'rxjs'; -import { startWith } from 'rxjs/operators'; +import { BehaviorSubject, EMPTY, filter, merge, Observable, Subject, Subscription, timer } from 'rxjs'; +import { delay, startWith, switchMap, takeUntil, tap } from 'rxjs/operators'; import { KbqNavbarFocusableItemEvent, KbqNavbarIcFocusableItem, @@ -43,6 +43,12 @@ export enum KbqExpandEvents { hoverOrFocus } +/** minimum timeout to use KBQ_ENTER_DELAY */ +export const KBQ_MIN_TIMEOUT_FOR_ENTER_DELAY = 2000; + +/** delay before open navbar-ic */ +export const KBQ_ENTER_DELAY = 400; + /** default configuration of navbar-ic */ /** @docs-private */ export const KBQ_NAVBAR_IC_DEFAULT_CONFIGURATION = ruRULocaleData.navbarIc; @@ -218,6 +224,8 @@ export class KbqNavbarIc extends KbqFocusable implements AfterContentInit { readonly externalConfiguration = inject(KBQ_NAVBAR_IC_CONFIGURATION, { optional: true }); + readonly state = new BehaviorSubject<'expanded' | 'collapsed' | null>(null); + configuration; /** @docs-private */ @@ -276,6 +284,8 @@ export class KbqNavbarIc extends KbqFocusable implements AfterContentInit { set expanded(value: boolean) { this._expanded = value; + this.state.next(value || this.pinned || this.hasOpenedPopUp ? 'expanded' : 'collapsed'); + this.updateExpandedStateForItems(); } @@ -299,24 +309,55 @@ export class KbqNavbarIc extends KbqFocusable implements AfterContentInit { return this.expanded ? this.expandedWidth : this.collapsedWidth; } + private lastOpeningTime: number; + constructor() { super(); - this.animationDone.pipe(takeUntilDestroyed()).subscribe(this.updateTooltipForItems); - effect(this.updateExpandedStateForItems); - combineLatest([this.hovered, this.focused]) - .pipe(takeUntilDestroyed()) - .subscribe(([hovered, focused]) => { - if (this.pinned) return; + this.focused + .pipe( + delay(0), + filter(() => !this.pinned || !this.hasOpenedPopUp), + takeUntilDestroyed() + ) + .subscribe((focused) => { + if (this.hovered.value) return; this.expandEvent = KbqExpandEvents.hoverOrFocus; - this.expanded = hovered || focused; + this.expanded = focused; this.changeDetectorRef.markForCheck(); }); + this.hovered + .pipe( + filter(() => !this.pinned), + switchMap((hovered) => { + if (!hovered) { + if (this.expanded) { + this.lastOpeningTime = Date.now(); + } + + this.expanded = false; + + return EMPTY; + } + + return timer(this.getExpandDelay()).pipe( + tap(() => { + this.expandEvent = KbqExpandEvents.hoverOrFocus; + this.expanded = true; + this.changeDetectorRef.markForCheck(); + }), + takeUntil(this.hovered.pipe(filter((h) => !h))) + ); + }), + takeUntilDestroyed() + ) + .subscribe(); + this.focusMonitor.monitor(this.elementRef, true).subscribe((focusOrigin) => { this.focused.next(focusOrigin === 'keyboard'); }); @@ -366,6 +407,10 @@ export class KbqNavbarIc extends KbqFocusable implements AfterContentInit { } } + private getExpandDelay() { + return Date.now() - this.lastOpeningTime < KBQ_MIN_TIMEOUT_FOR_ENTER_DELAY ? 0 : KBQ_ENTER_DELAY; + } + protected updateTooltipForItems = () => this.items().forEach((item) => item.updateTooltip()); protected updateExpandedStateForItems = () => this.rectangleElements().forEach(this.updateItemExpandedState); diff --git a/tools/public_api_guard/components/navbar-ic.api.md b/tools/public_api_guard/components/navbar-ic.api.md index 251dd061de..bf2c4245e7 100644 --- a/tools/public_api_guard/components/navbar-ic.api.md +++ b/tools/public_api_guard/components/navbar-ic.api.md @@ -36,6 +36,12 @@ import { Signal } from '@angular/core'; import { Subject } from 'rxjs'; import { TooltipModifier } from '@koobiq/components/tooltip'; +// @public +export const KBQ_ENTER_DELAY = 400; + +// @public +export const KBQ_MIN_TIMEOUT_FOR_ENTER_DELAY = 2000; + // @public export const KBQ_NAVBAR_IC_CONFIGURATION: InjectionToken; @@ -162,6 +168,8 @@ export class KbqNavbarIc extends KbqFocusable implements AfterContentInit { popoverTrigger: Signal; rectangleElements: Signal; // (undocumented) + readonly state: BehaviorSubject<"expanded" | "collapsed" | null>; + // (undocumented) toggle(): void; toggleElement: Signal; // (undocumented) @@ -242,6 +250,8 @@ export class KbqNavbarIcFocusableItem implements AfterContentInit, AfterViewInit export class KbqNavbarIcHeader extends KbqTooltipTrigger implements AfterContentInit { constructor(); // (undocumented) + protected readonly changeDetectorRef: ChangeDetectorRef; + // (undocumented) protected readonly destroyRef: DestroyRef; // (undocumented) get isLink(): boolean; @@ -258,7 +268,7 @@ export class KbqNavbarIcHeader extends KbqTooltipTrigger implements AfterContent // (undocumented) get titleText(): string; // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -280,6 +290,8 @@ export class KbqNavbarIcItem extends KbqTooltipTrigger implements AfterContentIn get hasDropDownTrigger(): boolean; icon: KbqIcon; // (undocumented) + readonly navbar: KbqNavbarIc; + // (undocumented) readonly navbarFocusableItem: KbqNavbarIcFocusableItem; // (undocumented) ngAfterContentInit(): void;