diff --git a/packages/components-dev/dropdown/module.ts b/packages/components-dev/dropdown/module.ts
index d14cd0595..24f391211 100644
--- a/packages/components-dev/dropdown/module.ts
+++ b/packages/components-dev/dropdown/module.ts
@@ -16,6 +16,7 @@ import {
DropdownOpenByArrowDownExample,
DropdownOverviewExample,
DropdownRecursiveTemplateExample,
+ DropdownSafeTriangleExample,
DropdownWithFilterExample,
DropdownWithFooterExample,
DropdownXPositionExample
@@ -33,6 +34,7 @@ import { DevThemeToggle } from '../theme-toggle';
DropdownLazyloadDataExample,
DropdownOpenByArrowDownExample,
DropdownRecursiveTemplateExample,
+ DropdownSafeTriangleExample,
DropdownWithFilterExample,
DropdownWithFooterExample,
DropdownXPositionExample
@@ -47,6 +49,9 @@ import { DevThemeToggle } from '../theme-toggle';
+
+
+
diff --git a/packages/components/core/overlay/safe-triangle.spec.ts b/packages/components/core/overlay/safe-triangle.spec.ts
new file mode 100644
index 000000000..9b79ac393
--- /dev/null
+++ b/packages/components/core/overlay/safe-triangle.spec.ts
@@ -0,0 +1,74 @@
+import { KbqTriangle, getSafeTriangleVertices, isPointInRect, isPointInTriangle } from './safe-triangle';
+
+const rect = (left: number, top: number, right: number, bottom: number): DOMRect =>
+ ({ left, top, right, bottom, width: right - left, height: bottom - top, x: left, y: top }) as DOMRect;
+
+describe('isPointInRect', () => {
+ const target = rect(100, 100, 200, 200);
+
+ it('should be true for a point inside the rect', () => {
+ expect(isPointInRect({ x: 150, y: 150 }, target)).toBe(true);
+ });
+
+ it('should be true for a point exactly on an edge', () => {
+ expect(isPointInRect({ x: 100, y: 150 }, target)).toBe(true);
+ expect(isPointInRect({ x: 200, y: 150 }, target)).toBe(true);
+ });
+
+ it('should be false for a point outside the rect', () => {
+ expect(isPointInRect({ x: 50, y: 50 }, target)).toBe(false);
+ expect(isPointInRect({ x: 250, y: 150 }, target)).toBe(false);
+ });
+});
+
+describe('isPointInTriangle', () => {
+ const triangle: KbqTriangle = { a: { x: 0, y: 0 }, b: { x: 100, y: 0 }, c: { x: 0, y: 100 } };
+
+ it('should be true for a point inside the triangle', () => {
+ expect(isPointInTriangle({ x: 10, y: 10 }, triangle)).toBe(true);
+ });
+
+ it('should be true for a point exactly on an edge', () => {
+ expect(isPointInTriangle({ x: 50, y: 0 }, triangle)).toBe(true);
+ });
+
+ it('should be true for a vertex', () => {
+ expect(isPointInTriangle(triangle.a, triangle)).toBe(true);
+ });
+
+ it('should be false for a point outside the triangle', () => {
+ expect(isPointInTriangle({ x: 60, y: 60 }, triangle)).toBe(false);
+ expect(isPointInTriangle({ x: -10, y: -10 }, triangle)).toBe(false);
+ });
+});
+
+describe('getSafeTriangleVertices', () => {
+ it('should use the left edge when the submenu opens to the right of the origin', () => {
+ const origin = { x: 90, y: 50 };
+ const target = rect(100, 0, 300, 200);
+
+ expect(getSafeTriangleVertices(origin, target)).toEqual({
+ a: origin,
+ b: { x: 100, y: 0 },
+ c: { x: 100, y: 200 }
+ });
+ });
+
+ it('should use the right edge when the submenu opens to the left of the origin', () => {
+ const origin = { x: 310, y: 50 };
+ const target = rect(0, 0, 300, 200);
+
+ expect(getSafeTriangleVertices(origin, target)).toEqual({
+ a: origin,
+ b: { x: 300, y: 0 },
+ c: { x: 300, y: 200 }
+ });
+ });
+
+ it('should pick the nearer edge when the origin is directly above the panel', () => {
+ const target = rect(0, 100, 200, 300);
+
+ expect(getSafeTriangleVertices({ x: 190, y: 50 }, target).b).toEqual({ x: 200, y: 100 });
+ expect(getSafeTriangleVertices({ x: 10, y: 50 }, target).b).toEqual({ x: 0, y: 100 });
+ });
+});
diff --git a/packages/components/core/overlay/safe-triangle.ts b/packages/components/core/overlay/safe-triangle.ts
new file mode 100644
index 000000000..a79684089
--- /dev/null
+++ b/packages/components/core/overlay/safe-triangle.ts
@@ -0,0 +1,64 @@
+/**
+ * A simple (x, y) coordinate. Picked from the DOM's own `DOMPointReadOnly` rather than hand-rolled, so
+ * a plain `{ x, y }` literal (e.g. from a `MouseEvent`) satisfies it without constructing a `DOMPoint` —
+ * `DOMPoint` isn't implemented in every runtime (e.g. jsdom).
+ * @docs-private
+ */
+export type KbqPoint = Pick;
+
+/**
+ * A triangle described by its three vertices.
+ * @docs-private
+ */
+export interface KbqTriangle {
+ a: KbqPoint;
+ b: KbqPoint;
+ c: KbqPoint;
+}
+
+/**
+ * Whether `point` lies within (or on the edge of) `rect`.
+ * @docs-private
+ */
+export function isPointInRect(point: KbqPoint, rect: DOMRect): boolean {
+ return point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom;
+}
+
+/**
+ * Whether `point` lies within (or on the edge of) `triangle`.
+ *
+ * Uses the sign of the cross product of each triangle edge with the point: the point is inside
+ * only if it's consistently on the same side of all three edges.
+ * @docs-private
+ */
+export function isPointInTriangle(point: KbqPoint, triangle: KbqTriangle): boolean {
+ const { a, b, c } = triangle;
+
+ const sign = (p1: KbqPoint, p2: KbqPoint, p3: KbqPoint): number =>
+ (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y);
+
+ const d1 = sign(point, a, b);
+ const d2 = sign(point, b, c);
+ const d3 = sign(point, c, a);
+
+ const hasNegative = d1 < 0 || d2 < 0 || d3 < 0;
+ const hasPositive = d1 > 0 || d2 > 0 || d3 > 0;
+
+ return !(hasNegative && hasPositive);
+}
+
+/**
+ * Builds the "safe triangle" connecting `origin` (typically the pointer position where it left a
+ * trigger) to the top and bottom corners of `targetRect` (typically a submenu panel) that are nearest
+ * to `origin` — the submenu can open on either side of its trigger, so the nearest edge is picked by
+ * comparing distances rather than assuming a fixed side.
+ * @docs-private
+ */
+export function getSafeTriangleVertices(origin: KbqPoint, targetRect: DOMRect): KbqTriangle {
+ const nearX =
+ Math.abs(targetRect.left - origin.x) <= Math.abs(targetRect.right - origin.x)
+ ? targetRect.left
+ : targetRect.right;
+
+ return { a: origin, b: { x: nearX, y: targetRect.top }, c: { x: nearX, y: targetRect.bottom } };
+}
diff --git a/packages/components/core/public-api.ts b/packages/components/core/public-api.ts
index 5a9db679a..64d3ac527 100644
--- a/packages/components/core/public-api.ts
+++ b/packages/components/core/public-api.ts
@@ -17,6 +17,7 @@ export * from './overlay/auto-hide-scroll-strategy';
export * from './overlay/overlay-position-map';
export * from './overlay/panel-height';
export * from './overlay/panel-width';
+export * from './overlay/safe-triangle';
export * from './overlay/shadow-dom-overlay-container';
export * from './pop-up/index';
export * from './select/index';
diff --git a/packages/components/dropdown/dropdown-trigger.directive.ts b/packages/components/dropdown/dropdown-trigger.directive.ts
index 29ca59fb3..0df079acc 100644
--- a/packages/components/dropdown/dropdown-trigger.directive.ts
+++ b/packages/components/dropdown/dropdown-trigger.directive.ts
@@ -32,6 +32,7 @@ import {
defaultOffsetY,
DOWN_ARROW,
ENTER,
+ getSafeTriangleVertices,
kbqGetPanelWidthOrigin,
KbqPanelWidthOrigin,
KbqResolvedPanelWidth,
@@ -122,6 +123,7 @@ const positionMap = {
// attribute themselves.
'[attr.aria-expanded]': 'opened',
'(mousedown)': 'handleMousedown($event)',
+ '(mouseleave)': 'handleMouseLeave($event)',
'(keydown)': 'handleKeydown($event)',
'(click)': 'handleClick($event)'
},
@@ -372,6 +374,21 @@ export class KbqDropdownTrigger implements AfterContentInit, OnDestroy, KbqSibli
}
}
+ /**
+ * Starts safe-triangle protection when the pointer leaves a trigger whose submenu is open, so a
+ * sibling item crossed on the way to the submenu doesn't prematurely close it.
+ */
+ handleMouseLeave(event: MouseEvent): void {
+ if (!this.isNested() || !this._opened || !this.isBrowser || !this.parent.safeTriangle() || !this.overlayRef) {
+ return;
+ }
+
+ const panelRect = this.overlayRef.overlayElement.getBoundingClientRect();
+ const triangle = getSafeTriangleVertices({ x: event.clientX, y: event.clientY }, panelRect);
+
+ this.parent.activateSafeTriangle(triangle, panelRect, () => this.close());
+ }
+
/** Handles key presses on the trigger. */
handleKeydown(event: KeyboardEvent) {
const keyCode = event.keyCode;
@@ -429,6 +446,10 @@ export class KbqDropdownTrigger implements AfterContentInit, OnDestroy, KbqSibli
this.lastDestroyReason = reason;
+ if (this.isNested()) {
+ this.parent.deactivateSafeTriangle();
+ }
+
this.dropdown.resetActiveItem();
this.closeSubscription.unsubscribe();
@@ -671,7 +692,10 @@ export class KbqDropdownTrigger implements AfterContentInit, OnDestroy, KbqSibli
const hover = this.parent
? this.parent.hovered().pipe(
filter((active) => active !== this.dropdownItemInstance),
- filter(() => this._opened)
+ filter(() => this._opened),
+ // A protected safe triangle handles closing itself once the pointer actually
+ // leaves it — see `handleMouseLeave()`.
+ filter(() => !this.parent.isSafeTriangleActive())
)
: observableOf();
@@ -698,9 +722,17 @@ export class KbqDropdownTrigger implements AfterContentInit, OnDestroy, KbqSibli
// it won't be closed immediately after it is opened.
.pipe(
filter((active) => active === this.dropdownItemInstance && !active.disabled),
+ // Suppress opening while a sibling's safe triangle is being protected — otherwise this
+ // dropdown could open before the protected one has had a chance to close, leaving two
+ // submenus open at once.
+ filter(() => !this.parent.isSafeTriangleActive()),
delay(0, asapScheduler)
)
.subscribe(() => {
+ // Coming back to this trigger cancels any safe-triangle protection left over from a
+ // previous `mouseleave`.
+ this.parent.deactivateSafeTriangle();
+
this.openedBy = 'mouse';
// If the same dropdown is used between multiple triggers, it might still be animating
diff --git a/packages/components/dropdown/dropdown.component.ts b/packages/components/dropdown/dropdown.component.ts
index 6c9d36ead..5199784bc 100644
--- a/packages/components/dropdown/dropdown.component.ts
+++ b/packages/components/dropdown/dropdown.component.ts
@@ -3,6 +3,8 @@ import { FocusOrigin } from '@angular/cdk/a11y';
import { Direction } from '@angular/cdk/bidi';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DOWN_ARROW, UP_ARROW } from '@angular/cdk/keycodes';
+import { normalizePassiveListenerOptions } from '@angular/cdk/platform';
+import { DOCUMENT } from '@angular/common';
import {
AfterContentInit,
ChangeDetectionStrategy,
@@ -21,6 +23,7 @@ import {
TemplateRef,
ViewChild,
ViewEncapsulation,
+ booleanAttribute,
computed,
contentChild,
inject,
@@ -34,8 +37,12 @@ import {
KbqPanelMaxWidth,
KbqPanelMinWidth,
KbqPanelWidth,
+ KbqPoint,
+ KbqTriangle,
LEFT_ARROW,
- RIGHT_ARROW
+ RIGHT_ARROW,
+ isPointInRect,
+ isPointInTriangle
} from '@koobiq/components/core';
import { KbqFormField } from '@koobiq/components/form-field';
import { Observable, Subject, Subscription, merge } from 'rxjs';
@@ -53,6 +60,9 @@ import {
KbqDropdownPositionY
} from './dropdown.types';
+/** Options for binding a passive event listener. */
+const passiveEventListenerOptions = normalizePassiveListenerOptions({ passive: true }) as EventListenerOptions;
+
@Directive({
selector: '[kbqDropdownStaticContent]'
})
@@ -89,6 +99,7 @@ export class KbqDropdownFooter {}
export class KbqDropdown implements AfterContentInit, KbqDropdownPanel, OnInit, OnDestroy {
private elementRef = inject>(ElementRef);
private ngZone = inject(NgZone);
+ private document = inject(DOCUMENT);
private defaultOptions = inject(KBQ_DROPDOWN_DEFAULT_OPTIONS);
private readonly search = contentChild(KbqFormField);
@@ -259,6 +270,13 @@ export class KbqDropdown implements AfterContentInit, KbqDropdownPanel, OnInit,
{ transform: numberAttribute }
);
+ /**
+ * Whether nested dropdowns opened from this dropdown's items use a "safe triangle": while the
+ * pointer moves from a trigger toward its open submenu, sibling items it crosses over on the way
+ * don't prematurely close the submenu.
+ */
+ readonly safeTriangle = input(this.defaultOptions.safeTriangle ?? false, { transform: booleanAttribute });
+
/**
* `panelMinWidth` rendered as a CSS length for the `--kbq-dropdown-size-container-width-min`
* token, so the panel's CSS `min-width` floor tracks the input — mirroring how `panelMaxWidth`
@@ -298,6 +316,9 @@ export class KbqDropdown implements AfterContentInit, KbqDropdownPanel, OnInit,
/** Subscription to tab events on the dropdown panel */
private tabSubscription = Subscription.EMPTY;
+ /** Cleans up the safe-triangle `mousemove` listener. `null` when no triangle is being tracked. */
+ private safeTriangleCleanup: (() => void) | null = null;
+
ngOnInit() {
this.setPositionClasses();
}
@@ -330,6 +351,7 @@ export class KbqDropdown implements AfterContentInit, KbqDropdownPanel, OnInit,
this.directDescendantItems.destroy();
this.tabSubscription.unsubscribe();
this.closed.complete();
+ this.deactivateSafeTriangle();
}
/** Stream that emits whenever the hovered dropdown item changes. */
@@ -342,6 +364,55 @@ export class KbqDropdown implements AfterContentInit, KbqDropdownPanel, OnInit,
) as Observable;
}
+ /**
+ * Starts tracking the pointer against `triangle` (see `getSafeTriangleVertices`). While the
+ * pointer stays inside the triangle, the submenu it protects is kept open; `onExit` runs once the
+ * pointer either leaves the triangle or, failing that protection, lands inside `panelRect` (the
+ * submenu itself, which needs no further tracking). Replaces any triangle already being tracked.
+ * @docs-private
+ */
+ activateSafeTriangle(triangle: KbqTriangle, panelRect: DOMRect, onExit: () => void): void {
+ this.deactivateSafeTriangle();
+
+ this.safeTriangleCleanup = this.ngZone.runOutsideAngular(() => {
+ const listener = (event: MouseEvent) => {
+ const point: KbqPoint = { x: event.clientX, y: event.clientY };
+
+ if (isPointInRect(point, panelRect)) {
+ this.deactivateSafeTriangle();
+
+ return;
+ }
+
+ if (!isPointInTriangle(point, triangle)) {
+ this.deactivateSafeTriangle();
+ this.ngZone.run(onExit);
+ }
+ };
+
+ this.document.addEventListener('mousemove', listener, passiveEventListenerOptions);
+
+ return () => this.document.removeEventListener('mousemove', listener, passiveEventListenerOptions);
+ });
+ }
+
+ /**
+ * Stops tracking the current safe triangle, if any.
+ * @docs-private
+ */
+ deactivateSafeTriangle(): void {
+ this.safeTriangleCleanup?.();
+ this.safeTriangleCleanup = null;
+ }
+
+ /**
+ * Whether a safe triangle is currently being tracked.
+ * @docs-private
+ */
+ isSafeTriangleActive(): boolean {
+ return this.safeTriangleCleanup !== null;
+ }
+
/** Handle a keyboard event from the dropdown, delegating to the appropriate action. */
handleKeydown(event: KeyboardEvent) {
const keyCode = event.keyCode;
diff --git a/packages/components/dropdown/dropdown.en.md b/packages/components/dropdown/dropdown.en.md
index fadb4b77b..c2d20ba89 100644
--- a/packages/components/dropdown/dropdown.en.md
+++ b/packages/components/dropdown/dropdown.en.md
@@ -28,6 +28,17 @@ You can place auxiliary elements in the footer: [buttons](en/components/button),
+### Safe Area
+
+This mechanism prevents an open nested submenu from closing prematurely while the pointer is moving.
+The submenu stays open even if the pointer touches sibling items along the way, as long as the
+movement stays within the designated area. It can be configured in two ways:
+
+- locally — for a specific nested dropdown through the `safeTriangle` property;
+- globally — through `KBQ_DROPDOWN_DEFAULT_OPTIONS`.
+
+
+
### Navigation Wrap
A "cyclic navigation" mode where reaching one end of the list loops back to the other end.
diff --git a/packages/components/dropdown/dropdown.ru.md b/packages/components/dropdown/dropdown.ru.md
index 6f7ed0a32..246db9ddb 100644
--- a/packages/components/dropdown/dropdown.ru.md
+++ b/packages/components/dropdown/dropdown.ru.md
@@ -28,6 +28,17 @@
+### Безопасная зона
+
+Механизм предотвращает преждевременное закрытие вложенного меню при движения указателя мыши.
+Меню остаётся открытым, даже если указатель задевает соседние элементы — при условии, что движение происходит в пределах отведённой области.
+Настроить можно двумя способами:
+
+- локально — для конкретного вложенного меню с помощью свойства `safeTriangle`;
+- глобально — через `KBQ_DROPDOWN_DEFAULT_OPTIONS`.
+
+
+
### Циклическая навигация
Режим "циклической навигации" по списку,
diff --git a/packages/components/dropdown/dropdown.spec.ts b/packages/components/dropdown/dropdown.spec.ts
index d6807e925..65270176c 100644
--- a/packages/components/dropdown/dropdown.spec.ts
+++ b/packages/components/dropdown/dropdown.spec.ts
@@ -1404,6 +1404,127 @@ describe('KbqDropdown', () => {
expect(overlay.querySelectorAll(PANEL_SELECTOR).length).toBe(1);
}));
+ describe('safe triangle', () => {
+ /**
+ * Opens the level-one nested dropdown with `safeTriangle` enabled and mocks its overlay
+ * pane's rect to a fixed, predictable box (jsdom otherwise reports an all-zero rect).
+ */
+ const openLevelOneWithSafeTriangle = (): HTMLElement => {
+ compileTestComponent();
+ instance.safeTriangleEnabled = true;
+ fixture.detectChanges();
+
+ instance.rootTriggerEl().nativeElement.click();
+ fixture.detectChanges();
+
+ const levelOneTrigger = overlay.querySelector('#level-one-trigger')! as HTMLElement;
+
+ dispatchMouseEvent(levelOneTrigger, 'mouseenter');
+ fixture.detectChanges();
+ tick();
+ fixture.detectChanges();
+
+ const overlayPanes = overlay.querySelectorAll('.cdk-overlay-pane');
+ const nestedPane = overlayPanes[overlayPanes.length - 1] as HTMLElement;
+
+ jest.spyOn(nestedPane, 'getBoundingClientRect').mockReturnValue({
+ left: 300,
+ right: 500,
+ top: 50,
+ bottom: 250,
+ width: 200,
+ height: 200,
+ x: 300,
+ y: 50,
+ toJSON: () => ({})
+ } as DOMRect);
+
+ return levelOneTrigger;
+ };
+
+ it('should close immediately on a sibling hover when disabled (default)', fakeAsync(() => {
+ compileTestComponent();
+ instance.rootTriggerEl().nativeElement.click();
+ fixture.detectChanges();
+
+ const items = Array.from(overlay.querySelectorAll(`${PANEL_SELECTOR} ${ITEM_SELECTOR}`));
+ const levelOneTrigger = overlay.querySelector('#level-one-trigger')! as HTMLElement;
+
+ dispatchMouseEvent(levelOneTrigger, 'mouseenter');
+ fixture.detectChanges();
+ tick();
+
+ expect(overlay.querySelectorAll(PANEL_SELECTOR).length).toBe(2);
+
+ dispatchMouseEvent(levelOneTrigger, 'mouseleave', 100, 100);
+ dispatchMouseEvent(items[items.indexOf(levelOneTrigger) + 1], 'mouseenter');
+ fixture.detectChanges();
+ tick(500);
+
+ expect(overlay.querySelectorAll(PANEL_SELECTOR).length).toBe(1);
+ }));
+
+ it('should keep the submenu open while a sibling crossed en route is hovered', fakeAsync(() => {
+ const levelOneTrigger = openLevelOneWithSafeTriangle();
+ const items = Array.from(overlay.querySelectorAll(`${PANEL_SELECTOR} ${ITEM_SELECTOR}`));
+
+ // Leaves roughly level with the panel's top, heading toward it.
+ dispatchMouseEvent(levelOneTrigger, 'mouseleave', 100, 100);
+ fixture.detectChanges();
+
+ // Crossing the next sibling row on the way to the submenu no longer closes it.
+ dispatchMouseEvent(items[items.indexOf(levelOneTrigger) + 1], 'mouseenter');
+ fixture.detectChanges();
+ tick();
+
+ expect(overlay.querySelectorAll(PANEL_SELECTOR).length).toBe(2);
+
+ // Still heading toward the submenu (inside the triangle formed by the leave point and
+ // the panel's near-top/near-bottom corners at x=300).
+ dispatchMouseEvent(document, 'mousemove', 200, 125);
+ fixture.detectChanges();
+ tick();
+
+ expect(overlay.querySelectorAll(PANEL_SELECTOR).length).toBe(2);
+ }));
+
+ it('should close once the pointer leaves the safe triangle', fakeAsync(() => {
+ const levelOneTrigger = openLevelOneWithSafeTriangle();
+
+ dispatchMouseEvent(levelOneTrigger, 'mouseleave', 100, 100);
+ fixture.detectChanges();
+
+ // Well outside the triangle — the user gave up on the submenu.
+ dispatchMouseEvent(document, 'mousemove', 150, 400);
+ fixture.detectChanges();
+ tick(500);
+
+ expect(overlay.querySelectorAll(PANEL_SELECTOR).length).toBe(1);
+ }));
+
+ it('should keep the submenu open and stop tracking once the pointer reaches the panel', fakeAsync(() => {
+ const levelOneTrigger = openLevelOneWithSafeTriangle();
+ const items = Array.from(overlay.querySelectorAll(`${PANEL_SELECTOR} ${ITEM_SELECTOR}`));
+
+ dispatchMouseEvent(levelOneTrigger, 'mouseleave', 100, 100);
+ fixture.detectChanges();
+
+ dispatchMouseEvent(document, 'mousemove', 400, 150);
+ fixture.detectChanges();
+ tick();
+
+ expect(overlay.querySelectorAll(PANEL_SELECTOR).length).toBe(2);
+
+ // Tracking stopped once the pointer reached the panel, so a sibling hover now closes
+ // the submenu immediately again, same as when the triangle was never activated.
+ dispatchMouseEvent(items[items.indexOf(levelOneTrigger) + 1], 'mouseenter');
+ fixture.detectChanges();
+ tick(500);
+
+ expect(overlay.querySelectorAll(PANEL_SELECTOR).length).toBe(1);
+ }));
+ });
+
it('should open and close a nested dropdown with arrow keys in ltr', fakeAsync(() => {
compileTestComponent();
instance.rootTriggerEl().nativeElement.click();
@@ -2097,6 +2218,21 @@ describe('KbqDropdown default overrides', () => {
});
});
+describe('KbqDropdown safe triangle default override', () => {
+ it('should honor a `safeTriangle: true` default without setting the input explicitly', () => {
+ TestBed.configureTestingModule({
+ imports: [KbqDropdownModule, NoopAnimationsModule, SimpleDropdown],
+ providers: [{ provide: KBQ_DROPDOWN_DEFAULT_OPTIONS, useValue: { safeTriangle: true } }]
+ }).compileComponents();
+
+ const fixture = TestBed.createComponent(SimpleDropdown);
+
+ fixture.detectChanges();
+
+ expect(fixture.componentInstance.dropdown().safeTriangle()).toBe(true);
+ });
+});
+
@Component({
imports: [KbqDropdownModule],
template: `
@@ -2279,7 +2415,12 @@ class CustomDropdown {
Toggle alternate dropdown
-
+