Skip to content

Commit b6a66f5

Browse files
authored
fix(navbar-ic): prevent accidental panel openings (#DS-4198) (#1085)
1 parent c168483 commit b6a66f5

8 files changed

Lines changed: 98 additions & 28 deletions

File tree

packages/components/navbar-ic/navbar-ic-header.scss

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,6 @@
4242
&.kbq-navbar-ic-header_interactive {
4343
cursor: pointer;
4444
}
45-
46-
&.kbq-collapsed .kbq-navbar-ic-title {
47-
display: none;
48-
}
4945
}
5046

5147
a.kbq-navbar-ic-header {

packages/components/navbar-ic/navbar-ic-header.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
AfterContentInit,
33
ChangeDetectionStrategy,
4+
ChangeDetectorRef,
45
Component,
56
ContentChild,
67
DestroyRef,
@@ -10,15 +11,22 @@ import {
1011
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
1112
import { kbqInjectNativeElement, PopUpPlacements, PopUpTriggers } from '@koobiq/components/core';
1213
import { KbqTooltipTrigger } from '@koobiq/components/tooltip';
14+
import { distinctUntilChanged } from 'rxjs/operators';
1315
import { KbqNavbarIc } from './navbar-ic';
1416
import { KbqNavbarIcLogo, KbqNavbarIcTitle } from './navbar-ic-item';
17+
import { toggleNavbarIcItemAnimation } from './navbar-ic.animation';
1518

1619
@Component({
1720
standalone: true,
1821
selector: 'kbq-navbar-ic-header, [kbq-navbar-ic-header]',
1922
exportAs: 'kbqNavbarIcHeader',
2023
template: `
2124
<div class="kbq-navbar-ic-header__inner">
25+
<ng-content select="[kbqNavbarIcLogo]" />
26+
27+
<div [@toggle]="navbar.state.value">
28+
<ng-content select="[kbqNavbarIcTitle]" />
29+
</div>
2230
<ng-content />
2331
</div>
2432
`,
@@ -28,12 +36,14 @@ import { KbqNavbarIcLogo, KbqNavbarIcTitle } from './navbar-ic-item';
2836
'[class.kbq-navbar-ic-header_interactive]': 'isLink'
2937
},
3038
changeDetection: ChangeDetectionStrategy.OnPush,
31-
encapsulation: ViewEncapsulation.None
39+
encapsulation: ViewEncapsulation.None,
40+
animations: [toggleNavbarIcItemAnimation()]
3241
})
3342
export class KbqNavbarIcHeader extends KbqTooltipTrigger implements AfterContentInit {
3443
protected readonly navbar = inject(KbqNavbarIc);
3544
protected readonly nativeElement = kbqInjectNativeElement();
3645
protected readonly destroyRef = inject(DestroyRef);
46+
protected readonly changeDetectorRef = inject(ChangeDetectorRef);
3747

3848
@ContentChild(KbqNavbarIcLogo) logo: KbqNavbarIcLogo;
3949
@ContentChild(KbqNavbarIcTitle) title: KbqNavbarIcTitle;
@@ -61,6 +71,10 @@ export class KbqNavbarIcHeader extends KbqTooltipTrigger implements AfterContent
6171
.pipe(takeUntilDestroyed(this.destroyRef))
6272
.subscribe(() => this.title?.checkTextOverflown());
6373

74+
this.navbar.state.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)).subscribe(() => {
75+
this.changeDetectorRef.detectChanges();
76+
});
77+
6478
this.title?.checkTextOverflown();
6579

6680
if (this.title?.isTextOverflown) {

packages/components/navbar-ic/navbar-ic-item.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<ng-content select="[kbq-icon]" />
33

44
@if (title && !collapsed) {
5-
<div class="kbq-navbar-ic-item__title-container">
5+
<div class="kbq-navbar-ic-item__title-container" [@toggle]="navbar.state.value">
66
<ng-content select="[kbqNavbarIcTitle]" />
77

88
@if (showDropDownAngle) {

packages/components/navbar-ic/navbar-ic-item.scss

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,6 @@
2323
top: 0;
2424
right: 0;
2525
}
26-
27-
& .kbq-navbar-ic-item__title-container {
28-
display: none;
29-
}
3026
}
3127

3228
& > .kbq-icon {

packages/components/navbar-ic/navbar-ic-item.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { KbqTooltipTrigger, TooltipModifier } from '@koobiq/components/tooltip';
3434
import { Subject } from 'rxjs';
3535
import { take } from 'rxjs/operators';
3636
import { KbqNavbarIc } from './navbar-ic';
37+
import { toggleNavbarIcItemAnimation } from './navbar-ic.animation';
3738

3839
/**
3940
* 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
261262
changeDetection: ChangeDetectionStrategy.OnPush,
262263
encapsulation: ViewEncapsulation.None,
263264
hostDirectives: [KbqRectangleItem],
264-
imports: [KbqIcon]
265+
imports: [KbqIcon],
266+
animations: [toggleNavbarIcItemAnimation()]
265267
})
266268
export class KbqNavbarIcItem extends KbqTooltipTrigger implements AfterContentInit {
267269
readonly rectangleElement = inject(KbqRectangleItem);
270+
readonly navbar = inject(KbqNavbarIc);
268271
readonly navbarFocusableItem = inject(KbqNavbarIcFocusableItem);
269272

270273
private changeDetectorRef = inject(ChangeDetectorRef);
@@ -310,11 +313,7 @@ export class KbqNavbarIcItem extends KbqTooltipTrigger implements AfterContentIn
310313
}
311314

312315
get disabled(): boolean {
313-
if (this._disabled !== undefined) {
314-
return this._disabled;
315-
}
316-
317-
return !this.collapsed && !this.hasCroppedText;
316+
return this.collapsed || !this.hasCroppedText || this._disabled;
318317
}
319318

320319
set disabled(value) {
@@ -341,7 +340,7 @@ export class KbqNavbarIcItem extends KbqTooltipTrigger implements AfterContentIn
341340
}
342341

343342
this.rectangleElement.state.subscribe(() => {
344-
this.collapsed = this.rectangleElement.collapsed;
343+
this.disabled = this.collapsed = this.rectangleElement.collapsed;
345344

346345
this.changeDetectorRef.markForCheck();
347346
});

packages/components/navbar-ic/navbar-ic.animation.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,13 @@ export function toggleNavbarIcAnimation(): AnimationTriggerMetadata {
44
return trigger('toggle', [
55
state('0', style({ width: '64px' })),
66
state('1', style({ width: '240px' })),
7-
transition('0 <=> 1', animate('150ms ease-out'))]);
7+
transition('0 <=> 1', animate('200ms cubic-bezier(0.4, 0, 0.2, 1)'))]);
8+
}
9+
10+
export function toggleNavbarIcItemAnimation(): AnimationTriggerMetadata {
11+
return trigger('toggle', [
12+
state('expanded', style({ opacity: 1, marginLeft: 0 })),
13+
state('collapsed', style({ opacity: 0, marginLeft: '-8px' })),
14+
transition('collapsed => expanded', animate('200ms 150ms cubic-bezier(0.4, 0, 0.2, 1)')),
15+
transition('expanded => collapsed', animate('200ms cubic-bezier(0.4, 0, 0.2, 1)'))]);
816
}

packages/components/navbar-ic/navbar-ic.ts

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ import { KBQ_LOCALE_SERVICE, KbqRectangleItem, ruRULocaleData } from '@koobiq/co
2828
import { KbqDropdownTrigger } from '@koobiq/components/dropdown';
2929
import { KbqNotificationCenterTrigger } from '@koobiq/components/notification-center';
3030
import { KbqPopoverTrigger } from '@koobiq/components/popover';
31-
import { BehaviorSubject, combineLatest, merge, Observable, Subject, Subscription } from 'rxjs';
32-
import { startWith } from 'rxjs/operators';
31+
import { BehaviorSubject, EMPTY, filter, merge, Observable, Subject, Subscription, timer } from 'rxjs';
32+
import { delay, startWith, switchMap, takeUntil, tap } from 'rxjs/operators';
3333
import {
3434
KbqNavbarFocusableItemEvent,
3535
KbqNavbarIcFocusableItem,
@@ -43,6 +43,12 @@ export enum KbqExpandEvents {
4343
hoverOrFocus
4444
}
4545

46+
/** minimum timeout to use KBQ_ENTER_DELAY */
47+
export const KBQ_MIN_TIMEOUT_FOR_ENTER_DELAY = 2000;
48+
49+
/** delay before open navbar-ic */
50+
export const KBQ_ENTER_DELAY = 400;
51+
4652
/** default configuration of navbar-ic */
4753
/** @docs-private */
4854
export const KBQ_NAVBAR_IC_DEFAULT_CONFIGURATION = ruRULocaleData.navbarIc;
@@ -218,6 +224,8 @@ export class KbqNavbarIc extends KbqFocusable implements AfterContentInit {
218224

219225
readonly externalConfiguration = inject(KBQ_NAVBAR_IC_CONFIGURATION, { optional: true });
220226

227+
readonly state = new BehaviorSubject<'expanded' | 'collapsed' | null>(null);
228+
221229
configuration;
222230

223231
/** @docs-private */
@@ -276,6 +284,8 @@ export class KbqNavbarIc extends KbqFocusable implements AfterContentInit {
276284
set expanded(value: boolean) {
277285
this._expanded = value;
278286

287+
this.state.next(value || this.pinned || this.hasOpenedPopUp ? 'expanded' : 'collapsed');
288+
279289
this.updateExpandedStateForItems();
280290
}
281291

@@ -299,24 +309,55 @@ export class KbqNavbarIc extends KbqFocusable implements AfterContentInit {
299309
return this.expanded ? this.expandedWidth : this.collapsedWidth;
300310
}
301311

312+
private lastOpeningTime: number;
313+
302314
constructor() {
303315
super();
304316

305-
this.animationDone.pipe(takeUntilDestroyed()).subscribe(this.updateTooltipForItems);
306-
307317
effect(this.updateExpandedStateForItems);
308318

309-
combineLatest([this.hovered, this.focused])
310-
.pipe(takeUntilDestroyed())
311-
.subscribe(([hovered, focused]) => {
312-
if (this.pinned) return;
319+
this.focused
320+
.pipe(
321+
delay(0),
322+
filter(() => !this.pinned || !this.hasOpenedPopUp),
323+
takeUntilDestroyed()
324+
)
325+
.subscribe((focused) => {
326+
if (this.hovered.value) return;
313327

314328
this.expandEvent = KbqExpandEvents.hoverOrFocus;
315-
this.expanded = hovered || focused;
329+
this.expanded = focused;
316330

317331
this.changeDetectorRef.markForCheck();
318332
});
319333

334+
this.hovered
335+
.pipe(
336+
filter(() => !this.pinned),
337+
switchMap((hovered) => {
338+
if (!hovered) {
339+
if (this.expanded) {
340+
this.lastOpeningTime = Date.now();
341+
}
342+
343+
this.expanded = false;
344+
345+
return EMPTY;
346+
}
347+
348+
return timer(this.getExpandDelay()).pipe(
349+
tap(() => {
350+
this.expandEvent = KbqExpandEvents.hoverOrFocus;
351+
this.expanded = true;
352+
this.changeDetectorRef.markForCheck();
353+
}),
354+
takeUntil(this.hovered.pipe(filter((h) => !h)))
355+
);
356+
}),
357+
takeUntilDestroyed()
358+
)
359+
.subscribe();
360+
320361
this.focusMonitor.monitor(this.elementRef, true).subscribe((focusOrigin) => {
321362
this.focused.next(focusOrigin === 'keyboard');
322363
});
@@ -366,6 +407,10 @@ export class KbqNavbarIc extends KbqFocusable implements AfterContentInit {
366407
}
367408
}
368409

410+
private getExpandDelay() {
411+
return Date.now() - this.lastOpeningTime < KBQ_MIN_TIMEOUT_FOR_ENTER_DELAY ? 0 : KBQ_ENTER_DELAY;
412+
}
413+
369414
protected updateTooltipForItems = () => this.items().forEach((item) => item.updateTooltip());
370415

371416
protected updateExpandedStateForItems = () => this.rectangleElements().forEach(this.updateItemExpandedState);

tools/public_api_guard/components/navbar-ic.api.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ import { Signal } from '@angular/core';
3636
import { Subject } from 'rxjs';
3737
import { TooltipModifier } from '@koobiq/components/tooltip';
3838

39+
// @public
40+
export const KBQ_ENTER_DELAY = 400;
41+
42+
// @public
43+
export const KBQ_MIN_TIMEOUT_FOR_ENTER_DELAY = 2000;
44+
3945
// @public
4046
export const KBQ_NAVBAR_IC_CONFIGURATION: InjectionToken<unknown>;
4147

@@ -162,6 +168,8 @@ export class KbqNavbarIc extends KbqFocusable implements AfterContentInit {
162168
popoverTrigger: Signal<readonly any[]>;
163169
rectangleElements: Signal<readonly any[]>;
164170
// (undocumented)
171+
readonly state: BehaviorSubject<"expanded" | "collapsed" | null>;
172+
// (undocumented)
165173
toggle(): void;
166174
toggleElement: Signal<readonly any[]>;
167175
// (undocumented)
@@ -242,6 +250,8 @@ export class KbqNavbarIcFocusableItem implements AfterContentInit, AfterViewInit
242250
export class KbqNavbarIcHeader extends KbqTooltipTrigger implements AfterContentInit {
243251
constructor();
244252
// (undocumented)
253+
protected readonly changeDetectorRef: ChangeDetectorRef;
254+
// (undocumented)
245255
protected readonly destroyRef: DestroyRef;
246256
// (undocumented)
247257
get isLink(): boolean;
@@ -258,7 +268,7 @@ export class KbqNavbarIcHeader extends KbqTooltipTrigger implements AfterContent
258268
// (undocumented)
259269
get titleText(): string;
260270
// (undocumented)
261-
static ɵcmp: i0.ɵɵComponentDeclaration<KbqNavbarIcHeader, "kbq-navbar-ic-header, [kbq-navbar-ic-header]", ["kbqNavbarIcHeader"], {}, {}, ["logo", "title"], ["*"], true, never>;
271+
static ɵcmp: i0.ɵɵComponentDeclaration<KbqNavbarIcHeader, "kbq-navbar-ic-header, [kbq-navbar-ic-header]", ["kbqNavbarIcHeader"], {}, {}, ["logo", "title"], ["[kbqNavbarIcLogo]", "[kbqNavbarIcTitle]", "*"], true, never>;
262272
// (undocumented)
263273
static ɵfac: i0.ɵɵFactoryDeclaration<KbqNavbarIcHeader, never>;
264274
}
@@ -280,6 +290,8 @@ export class KbqNavbarIcItem extends KbqTooltipTrigger implements AfterContentIn
280290
get hasDropDownTrigger(): boolean;
281291
icon: KbqIcon;
282292
// (undocumented)
293+
readonly navbar: KbqNavbarIc;
294+
// (undocumented)
283295
readonly navbarFocusableItem: KbqNavbarIcFocusableItem;
284296
// (undocumented)
285297
ngAfterContentInit(): void;

0 commit comments

Comments
 (0)