diff --git a/apps/docs/src/app/core/component-docs/breadcrumb/breadcrumb-docs.component.html b/apps/docs/src/app/core/component-docs/breadcrumb/breadcrumb-docs.component.html
index 70248d6b88f..60b0ebcac6c 100644
--- a/apps/docs/src/app/core/component-docs/breadcrumb/breadcrumb-docs.component.html
+++ b/apps/docs/src/app/core/component-docs/breadcrumb/breadcrumb-docs.component.html
@@ -20,15 +20,3 @@
-
- Responsive Breadcrumbs
-
- The breadcrumb will automatically refer to its parent element to know whether to show breadcrumbs or to collapse
- them into the overflow menu. You can provide an HTMLElement to the [containerElement] input to set your own
- container element. Note that the responsiveness feature will not function properly if the containerElement or parent
- element have a set fixed width.
-
-
-
-
-
diff --git a/apps/docs/src/app/core/component-docs/breadcrumb/breadcrumb-docs.component.ts b/apps/docs/src/app/core/component-docs/breadcrumb/breadcrumb-docs.component.ts
index 2da69aadf2c..0f008b40ab9 100644
--- a/apps/docs/src/app/core/component-docs/breadcrumb/breadcrumb-docs.component.ts
+++ b/apps/docs/src/app/core/component-docs/breadcrumb/breadcrumb-docs.component.ts
@@ -1,7 +1,6 @@
import { Component } from '@angular/core';
import breadcrumbHrefExample from '!./examples/breadcrumb-href-example.component.html?raw';
-import breadcrumbResponsiveExample from '!./examples/breadcrumb-responsive-example.component.html?raw';
import breadcrumbRouterLinkExample from '!./examples/breadcrumb-routerLink-example.component.html?raw';
import { ExampleFile } from '../../../documentation/core-helpers/code-example/example-file';
@@ -25,12 +24,4 @@ export class BreadcrumbDocsComponent {
fileName: 'fd-breadcrumb-href-example'
}
];
-
- breadcrumbResponsiveHtml: ExampleFile[] = [
- {
- language: 'html',
- code: breadcrumbResponsiveExample,
- fileName: 'fd-breadcrumb-responsive-example'
- }
- ];
}
diff --git a/apps/docs/src/app/core/component-docs/breadcrumb/breadcrumb-docs.module.ts b/apps/docs/src/app/core/component-docs/breadcrumb/breadcrumb-docs.module.ts
index 278ba2d7d89..109e365840d 100644
--- a/apps/docs/src/app/core/component-docs/breadcrumb/breadcrumb-docs.module.ts
+++ b/apps/docs/src/app/core/component-docs/breadcrumb/breadcrumb-docs.module.ts
@@ -4,7 +4,6 @@ import { ApiComponent } from '../../../documentation/core-helpers/api/api.compon
import { API_FILES } from '../../api-files';
import {
BreadcrumbHrefExampleComponent,
- BreadcrumbResponsiveExampleComponent,
BreadcrumbRouterLinkExampleComponent
} from './examples/breadcrumb-examples.component';
import { BreadcrumbHeaderComponent } from './breadcrumb-header/breadcrumb-header.component';
@@ -31,8 +30,7 @@ const routes: Routes = [
BreadcrumbDocsComponent,
BreadcrumbHeaderComponent,
BreadcrumbHrefExampleComponent,
- BreadcrumbRouterLinkExampleComponent,
- BreadcrumbResponsiveExampleComponent
+ BreadcrumbRouterLinkExampleComponent
],
providers: [moduleDeprecationsProvider(DeprecatedBreadcrumbsCompactDirective)]
})
diff --git a/apps/docs/src/app/core/component-docs/breadcrumb/examples/breadcrumb-examples.component.ts b/apps/docs/src/app/core/component-docs/breadcrumb/examples/breadcrumb-examples.component.ts
index a6bab6bdd92..49982156729 100644
--- a/apps/docs/src/app/core/component-docs/breadcrumb/examples/breadcrumb-examples.component.ts
+++ b/apps/docs/src/app/core/component-docs/breadcrumb/examples/breadcrumb-examples.component.ts
@@ -37,9 +37,3 @@ export class BreadcrumbRouterLinkExampleComponent {
]
})
export class BreadcrumbHrefExampleComponent {}
-
-@Component({
- selector: 'fd-breadcrumb-responsive-example',
- templateUrl: './breadcrumb-responsive-example.component.html'
-})
-export class BreadcrumbResponsiveExampleComponent {}
diff --git a/apps/docs/src/app/core/component-docs/breadcrumb/examples/breadcrumb-responsive-example.component.html b/apps/docs/src/app/core/component-docs/breadcrumb/examples/breadcrumb-responsive-example.component.html
deleted file mode 100644
index a07511971f2..00000000000
--- a/apps/docs/src/app/core/component-docs/breadcrumb/examples/breadcrumb-responsive-example.component.html
+++ /dev/null
@@ -1,16 +0,0 @@
-
diff --git a/e2e/wdio/core/pages/dynamic-page.po.ts b/e2e/wdio/core/pages/dynamic-page.po.ts
index 466211241e5..94b07de9cc6 100644
--- a/e2e/wdio/core/pages/dynamic-page.po.ts
+++ b/e2e/wdio/core/pages/dynamic-page.po.ts
@@ -30,7 +30,7 @@ export class DynamicPagePo extends CoreBaseComponentPo {
flexibleColumn = '.fd-flexible-column-layout__column ';
article = '.fd-dynamic-page-section-example';
breadcrumbLink = '.fd-dynamic-page__breadcrumb-wrapper a';
- currentBreadcrumbLink = '.fd-dynamic-page__breadcrumb-wrapper fd-breadcrumb-item:last-child span';
+ currentBreadcrumbLink = '.fd-dynamic-page__breadcrumb-wrapper .fd-overflow-layout__item--last span';
open(): void {
super.open(this.url);
diff --git a/libs/core/src/lib/breadcrumb/breadcrumb-item.component.ts b/libs/core/src/lib/breadcrumb/breadcrumb-item.component.ts
index 60a978eb0a0..15c96a9ff32 100644
--- a/libs/core/src/lib/breadcrumb/breadcrumb-item.component.ts
+++ b/libs/core/src/lib/breadcrumb/breadcrumb-item.component.ts
@@ -1,14 +1,14 @@
+import { DomPortal } from '@angular/cdk/portal';
import {
AfterViewInit,
- ChangeDetectorRef,
+ ChangeDetectionStrategy,
Component,
ContentChild,
ElementRef,
forwardRef,
- Renderer2
+ ViewEncapsulation
} from '@angular/core';
import { LinkComponent } from '@fundamental-ngx/core/link';
-import { DomPortal } from '@angular/cdk/portal';
/**
* Breadcrumb item directive. Must have child breadcrumb link directives.
@@ -21,54 +21,84 @@ import { DomPortal } from '@angular/cdk/portal';
*/
@Component({
selector: 'fd-breadcrumb-item',
- template: '
',
+ template: '',
host: {
class: 'fd-breadcrumb__item'
- }
+ },
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class BreadcrumbItemComponent implements AfterViewInit {
- /** @hidden */
- get elementRef(): ElementRef {
- return this._elementRef;
- }
-
/** @hidden */
@ContentChild(forwardRef(() => LinkComponent))
breadcrumbLink: LinkComponent;
- /** @hidden */
- get width(): number {
- return this._elementRef.nativeElement.getBoundingClientRect().width;
- }
-
/** In case there is no link in Item and breadcrumb item is non-interactive, we move whole item content to menu item title */
breadcrumbItemPortal: DomPortal;
/** When breadcrumb item has link in it, we are moving link content to menu item title */
linkContentPortal: DomPortal;
- constructor(
- private _elementRef: ElementRef,
- private renderer2: Renderer2,
- private _cdR: ChangeDetectorRef
- ) {}
+ /**
+ * Breadcrumb item dom portal.
+ */
+ portal: DomPortal;
+
+ /** @hidden */
+ private _attached = false;
+
+ /** @hidden */
+ constructor(public readonly elementRef: ElementRef) {}
/** @hidden */
- get needsClickProxy(): boolean {
+ get _needsClickProxy(): boolean {
return (
!!this.breadcrumbLink?.elementRef().nativeElement.getAttribute('href') || !!this.breadcrumbLink.routerLink
);
}
- show = (): void => this.renderer2.setStyle(this._elementRef.nativeElement, 'display', 'inline-block');
- hide = (): void => this.renderer2.setStyle(this._elementRef.nativeElement, 'display', 'none');
-
/** @hidden */
ngAfterViewInit(): void {
- if (this.breadcrumbLink) {
+ this._attach();
+ }
+
+ /**
+ * Sets breadcrumb item dom portal.
+ */
+ setPortal(): void {
+ if (!this.portal) {
+ this.portal = new DomPortal(this.elementRef);
+ }
+ }
+
+ /** @hidden */
+ _detach(): void {
+ if (!this._attached) {
+ return;
+ }
+
+ if (this.linkContentPortal?.isAttached) {
+ this.linkContentPortal?.detach();
+ }
+
+ if (this.breadcrumbItemPortal?.isAttached) {
+ this.breadcrumbItemPortal?.detach();
+ }
+
+ this._attached = false;
+ }
+
+ /** @hidden */
+ _attach(): void {
+ if (this._attached) {
+ return;
+ }
+
+ if (this.breadcrumbLink && this.breadcrumbLink.contentSpan) {
this.linkContentPortal = new DomPortal(this.breadcrumbLink.contentSpan.nativeElement);
}
+
this.breadcrumbItemPortal = new DomPortal(this.elementRef.nativeElement.firstElementChild as Element);
- this._cdR.detectChanges();
+ this._attached = true;
}
}
diff --git a/libs/core/src/lib/breadcrumb/breadcrumb.component.html b/libs/core/src/lib/breadcrumb/breadcrumb.component.html
index 91181efaf02..b0ccf163ad3 100644
--- a/libs/core/src/lib/breadcrumb/breadcrumb.component.html
+++ b/libs/core/src/lib/breadcrumb/breadcrumb.component.html
@@ -1,52 +1,74 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0" [fdMenuTrigger]="menu">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ …
+
-
+
-
-
-
-
- ...
-
-
-
+
diff --git a/libs/core/src/lib/breadcrumb/breadcrumb.component.scss b/libs/core/src/lib/breadcrumb/breadcrumb.component.scss
index 0bfc1cf97ef..8a159551678 100644
--- a/libs/core/src/lib/breadcrumb/breadcrumb.component.scss
+++ b/libs/core/src/lib/breadcrumb/breadcrumb.component.scss
@@ -1,10 +1,18 @@
@import '~fundamental-styles/dist/breadcrumb';
.fd-breadcrumb {
- display: inline-block;
+ display: flex;
white-space: nowrap;
.fd-breadcrumb__collapsed {
cursor: pointer;
}
}
+
+.fd-breadcrumb__item:last-child::after {
+ content: '/';
+}
+
+.fd-overflow-layout__item--last .fd-breadcrumb__item::after {
+ content: none;
+}
diff --git a/libs/core/src/lib/breadcrumb/breadcrumb.component.spec.ts b/libs/core/src/lib/breadcrumb/breadcrumb.component.spec.ts
index 2fcc4ecb10c..0e5b38c8865 100644
--- a/libs/core/src/lib/breadcrumb/breadcrumb.component.spec.ts
+++ b/libs/core/src/lib/breadcrumb/breadcrumb.component.spec.ts
@@ -1,8 +1,10 @@
+import { PortalModule } from '@angular/cdk/portal';
import { Component, NO_ERRORS_SCHEMA, ViewChild } from '@angular/core';
import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
-import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
+import { OverflowLayoutModule } from '@fundamental-ngx/core/overflow-layout';
import { PopoverModule } from '@fundamental-ngx/core/popover';
import { MenuModule } from '@fundamental-ngx/core/menu';
import { IconModule } from '@fundamental-ngx/core/icon';
@@ -40,7 +42,16 @@ describe('BreadcrumbComponent', () => {
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [BreadcrumbComponent, BreadcrumbItemComponent, BreadcrumbWrapperComponent],
- imports: [PopoverModule, MenuModule, IconModule, LinkModule, RouterModule, RouterTestingModule],
+ imports: [
+ PopoverModule,
+ MenuModule,
+ IconModule,
+ LinkModule,
+ RouterModule,
+ RouterTestingModule,
+ OverflowLayoutModule,
+ PortalModule
+ ],
providers: [RtlService],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
@@ -59,25 +70,26 @@ describe('BreadcrumbComponent', () => {
expect(component).toBeTruthy();
});
- it('should handle onResize - enlarging the screen', async () => {
- spyOn(component.elementRef.nativeElement.parentElement as Element, 'getBoundingClientRect').and.returnValue({
- width: component.elementRef.nativeElement.getBoundingClientRect().width + 100
- } as any);
+ it('should handle onResize - enlarging the screen', fakeAsync(() => {
+ const hiddenItemsCountSpy = spyOn(component, '_onHiddenItemsCountChange').and.callThrough();
+ component.elementRef.nativeElement.parentElement!.style.width = '500px';
component.onResize();
- await whenStable(fixture);
+ tick(1000);
- expect(component._collapsedBreadcrumbItems.length).toBe(0);
- });
+ expect(hiddenItemsCountSpy).toHaveBeenCalledWith(0);
+ }));
- it('should handle onResize - shrinking the screen', () => {
- spyOn(component.elementRef.nativeElement.parentElement as Element, 'getBoundingClientRect').and.returnValue({
- width: component.elementRef.nativeElement.getBoundingClientRect().width / 2
- } as any);
+ it('should handle onResize - shrinking the screen', fakeAsync(() => {
+ const hiddenItemsCountSpy = spyOn(component, '_onHiddenItemsCountChange').and.callThrough();
+ component.elementRef.nativeElement.parentElement!.style.width = '200px';
component.onResize();
+ fixture.detectChanges();
- expect(component._collapsedBreadcrumbItems.length).toBeGreaterThan(1);
- });
+ tick(1000);
+
+ expect(hiddenItemsCountSpy).toHaveBeenCalledWith(2);
+ }));
});
diff --git a/libs/core/src/lib/breadcrumb/breadcrumb.component.ts b/libs/core/src/lib/breadcrumb/breadcrumb.component.ts
index 5d1dff90dac..f409136ed4b 100644
--- a/libs/core/src/lib/breadcrumb/breadcrumb.component.ts
+++ b/libs/core/src/lib/breadcrumb/breadcrumb.component.ts
@@ -5,19 +5,20 @@ import {
Component,
ContentChildren,
ElementRef,
- forwardRef,
+ EventEmitter,
Input,
- NgZone,
- OnDestroy,
+ isDevMode,
OnInit,
Optional,
+ Output,
QueryList,
ViewChild,
ViewEncapsulation
} from '@angular/core';
+import { OverflowLayoutComponent } from '@fundamental-ngx/core/overflow-layout';
import { BreadcrumbItemComponent } from './breadcrumb-item.component';
-import { ResizeObserverService, RtlService } from '@fundamental-ngx/core/utils';
-import { BehaviorSubject, debounceTime, firstValueFrom, map, startWith, Subscription, tap } from 'rxjs';
+import { DestroyedService, RtlService } from '@fundamental-ngx/core/utils';
+import { BehaviorSubject, takeUntil } from 'rxjs';
import { MenuComponent } from '@fundamental-ngx/core/menu';
import { Placement } from '@fundamental-ngx/core/shared';
@@ -33,8 +34,6 @@ import { Placement } from '@fundamental-ngx/core/shared';
* ```
*/
@Component({
- // TODO to be discussed
- // eslint-disable-next-line
selector: 'fd-breadcrumb',
host: {
class: 'fd-breadcrumb'
@@ -42,127 +41,99 @@ import { Placement } from '@fundamental-ngx/core/shared';
templateUrl: './breadcrumb.component.html',
styleUrls: ['./breadcrumb.component.scss'],
encapsulation: ViewEncapsulation.None,
- changeDetection: ChangeDetectionStrategy.OnPush
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [DestroyedService]
})
-export class BreadcrumbComponent implements AfterViewInit, OnInit, OnDestroy {
- /** @hidden */
- @ContentChildren(forwardRef(() => BreadcrumbItemComponent))
- _breadcrumbItems: QueryList;
-
- /** @hidden */
- @ViewChild(MenuComponent)
- _menuComponent: MenuComponent;
-
- @ViewChild('overflowBreadcrumbsContainer')
- private readonly _overflowContainer: ElementRef;
-
- /** @hidden */
- _collapsedBreadcrumbItems: Array = [];
-
- /** @hidden */
- _placement$ = new BehaviorSubject('bottom-start');
-
+export class BreadcrumbComponent implements OnInit, AfterViewInit {
/**
+ * @deprecated
+ * Breadcrumbs component now uses more advanced calculation mechanism without the need of specifying the container element.
+ *
* The element to act as the breadcrumb container. When provided, the breadcrumb's responsive collapsing behavior
* performs better. When not provided, the immediate parent element's width will be used.
*/
@Input()
- containerElement: HTMLElement;
+ set containerElement(_: HTMLElement) {
+ if (isDevMode()) {
+ console.warn(
+ 'Breadcrumbs component now uses more advanced calculation mechanism without the need of specifying the container element.'
+ );
+ }
+ }
/** Whether to append items to the overflow dropdown in reverse order. Default is true. */
@Input()
reverse = false;
+ /**
+ * Event emitted when visible items count is changed.
+ */
+ @Output()
+ visibleItemsCount = new EventEmitter();
+
+ /**
+ * Event emitted when hidden items count is changed.
+ */
+ @Output()
+ hiddenItemsCount = new EventEmitter();
+
/** @hidden */
- _containerBoundary: number;
+ @ContentChildren(BreadcrumbItemComponent)
+ private readonly _contentItems: QueryList;
/** @hidden */
- private _subscriptions = new Subscription();
+ @ViewChild(MenuComponent)
+ private readonly _menuComponent: MenuComponent;
/** @hidden */
- private _itemToSize = new Map();
+ @ViewChild(OverflowLayoutComponent)
+ private readonly _overflowLayout: OverflowLayoutComponent;
+ /**
+ * @hidden
+ * Array of breadcrumb items.
+ */
+ _items: BreadcrumbItemComponent[] = [];
+
+ /** @hidden */
+ _placement$ = new BehaviorSubject('bottom-start');
+
+ /** @hidden */
constructor(
- public readonly elementRef: ElementRef,
- @Optional() private readonly _rtlService: RtlService | null,
- private readonly _cdRef: ChangeDetectorRef,
- private readonly _resizeObserver: ResizeObserverService,
- private readonly _ngZone: NgZone
+ public elementRef: ElementRef,
+ private _onDestroy$: DestroyedService,
+ @Optional() private _rtlService: RtlService | null,
+ private _cdr: ChangeDetectorRef
) {}
/** @hidden */
ngOnInit(): void {
- if (this._rtlService) {
- this._subscriptions.add(
- this._rtlService.rtl.subscribe((value) => this._placement$.next(value ? 'bottom-end' : 'bottom-start'))
- );
- }
- }
-
- /** @hidden */
- ngAfterViewInit(): void {
- this._subscriptions.add(
- this._breadcrumbItems.changes
- .pipe(
- startWith(this._breadcrumbItems),
- map((items) => items.toArray() as BreadcrumbItemComponent[]),
- map((items) => items.map((item) => [item, item.width]) as [BreadcrumbItemComponent, number][]),
- tap((itemToSize: [BreadcrumbItemComponent, number][]) => (this._itemToSize = new Map(itemToSize)))
- )
- .subscribe()
- );
- firstValueFrom(this._ngZone.onStable).then(() => {
- this._subscriptions.add(
- this._resizeObserver
- .observe(this.containerElement || (this.elementRef.nativeElement.parentElement as Element))
- // Add small delay for performance reasons.
- .pipe(debounceTime(30))
- .subscribe(() => this.onResize())
- );
- });
+ this._rtlService?.rtl
+ .pipe(takeUntil(this._onDestroy$))
+ .subscribe((value) => this._placement$.next(value ? 'bottom-end' : 'bottom-start'));
}
/** @hidden */
- ngOnDestroy(): void {
- this._subscriptions.unsubscribe();
+ onResize(): void {
+ this._overflowLayout.triggerRecalculation();
}
/**
- * Callback function when breadcrumbs container has been resized.
- */
- onResize(): void {
- if (!this.elementRef.nativeElement.parentElement) {
- return;
- }
- this._containerBoundary = this.elementRef.nativeElement.parentElement.getBoundingClientRect().width;
-
- if (this.containerElement) {
- this._containerBoundary = this.containerElement.getBoundingClientRect().width;
+ * We catch interactions with item, Enter, Space, Mouse click and Touch click,
+ * if original element had router link we are proxying click to that element
+ * */
+ itemClicked(breadcrumbItem: any, $event: any): void {
+ if (breadcrumbItem.needsClickProxy) {
+ $event.preventDefault();
+ breadcrumbItem.breadcrumbLink.elementRef().nativeElement.click();
}
+ }
- if (this._overflowContainer) {
- this._containerBoundary -= this._overflowContainer.nativeElement.getBoundingClientRect().width;
- }
+ /** @hidden */
+ ngAfterViewInit(): void {
+ this._setItems();
- let visibleSum = 0;
- const breadcrumbItemComponents = this._breadcrumbItems.toArray();
- let i;
- for (i = breadcrumbItemComponents.length - 1; i >= 0; i--) {
- const breadcrumbItem = breadcrumbItemComponents[i];
- const itemSize = this._itemToSize.has(breadcrumbItem)
- ? (this._itemToSize.get(breadcrumbItem) as number)
- : breadcrumbItem.width;
- if (visibleSum + itemSize <= this._containerBoundary) {
- visibleSum += itemSize;
- breadcrumbItem.show();
- } else {
- break;
- }
- }
- const collapsedBreadcrumbItems = breadcrumbItemComponents.slice(0, ++i);
- this._collapsedBreadcrumbItems = this.reverse ? collapsedBreadcrumbItems : collapsedBreadcrumbItems.reverse();
- this._collapsedBreadcrumbItems.forEach((item) => item.hide());
- this._cdRef.detectChanges();
+ this._contentItems.changes.subscribe(() => this._setItems());
}
/** @hidden */
@@ -171,14 +142,29 @@ export class BreadcrumbComponent implements AfterViewInit, OnInit, OnDestroy {
event.preventDefault();
}
- /**
- * We catch interactions with item, Enter, Space, Mouse click and Touch click,
- * if original element had router link we are proxying click to that element
- * */
- itemClicked(breadcrumbItem: BreadcrumbItemComponent, $event: any): void {
- if (breadcrumbItem.needsClickProxy) {
- $event.preventDefault();
- breadcrumbItem.breadcrumbLink.elementRef().nativeElement.click();
+ /** @hidden */
+ _onHiddenChange(isHidden: boolean, breadcrumb: BreadcrumbItemComponent): void {
+ if (!isHidden) {
+ breadcrumb._detach();
+ } else {
+ breadcrumb._attach();
}
}
+
+ /** @hidden */
+ _onVisibleItemsCountChange(visibleItemsCount: number): void {
+ this.visibleItemsCount.emit(visibleItemsCount);
+ }
+
+ /** @hidden */
+ _onHiddenItemsCountChange(hiddenItemsCount: number): void {
+ this.hiddenItemsCount.emit(hiddenItemsCount);
+ }
+
+ /** @hidden */
+ private _setItems(): void {
+ this._contentItems.forEach((item) => item.setPortal());
+ this._items = this._contentItems.toArray();
+ this._cdr.detectChanges();
+ }
}
diff --git a/libs/core/src/lib/breadcrumb/breadcrumb.module.ts b/libs/core/src/lib/breadcrumb/breadcrumb.module.ts
index 471957adbc4..9fa87f34725 100644
--- a/libs/core/src/lib/breadcrumb/breadcrumb.module.ts
+++ b/libs/core/src/lib/breadcrumb/breadcrumb.module.ts
@@ -1,5 +1,7 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
+import { ButtonModule } from '@fundamental-ngx/core/button';
+import { OverflowLayoutModule } from '@fundamental-ngx/core/overflow-layout';
import { BreadcrumbComponent } from './breadcrumb.component';
import { BreadcrumbItemComponent } from './breadcrumb-item.component';
@@ -21,7 +23,9 @@ import { ContentDensityModule } from '@fundamental-ngx/core/content-density';
LinkModule,
PortalModule,
PipeModule,
- ContentDensityModule
+ ContentDensityModule,
+ OverflowLayoutModule,
+ ButtonModule
],
exports: [
BreadcrumbComponent,
diff --git a/libs/core/src/lib/breadcrumb/test.ts b/libs/core/src/lib/breadcrumb/test.ts
index f7496773ee9..949a08108b3 100644
--- a/libs/core/src/lib/breadcrumb/test.ts
+++ b/libs/core/src/lib/breadcrumb/test.ts
@@ -4,6 +4,7 @@ import 'core-js/es/reflect';
import 'zone.js';
import 'zone.js/testing';
+import '@angular/localize/init';
import { getTestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
diff --git a/libs/core/src/lib/dynamic-page/dynamic-page-header/header/dynamic-page-header.component.spec.ts b/libs/core/src/lib/dynamic-page/dynamic-page-header/header/dynamic-page-header.component.spec.ts
index 3e3d4f7a476..0b264a4a2fd 100644
--- a/libs/core/src/lib/dynamic-page/dynamic-page-header/header/dynamic-page-header.component.spec.ts
+++ b/libs/core/src/lib/dynamic-page/dynamic-page-header/header/dynamic-page-header.component.spec.ts
@@ -70,7 +70,7 @@ describe('DynamicPageTitleComponent', () => {
header.size = 'small';
- tick(5);
+ tick(50);
expect(breadcrumbSpy).toHaveBeenCalled();
expect(contentToolbarSpy).toHaveBeenCalledWith('small');
diff --git a/libs/core/src/lib/dynamic-page/test.ts b/libs/core/src/lib/dynamic-page/test.ts
index f7496773ee9..54954bff26d 100644
--- a/libs/core/src/lib/dynamic-page/test.ts
+++ b/libs/core/src/lib/dynamic-page/test.ts
@@ -2,7 +2,7 @@
import 'core-js/es/reflect';
import 'zone.js';
-
+import '@angular/localize/init';
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
diff --git a/libs/core/src/lib/overflow-layout/directives/overflow-expand.directive.ts b/libs/core/src/lib/overflow-layout/directives/overflow-expand.directive.ts
index 42481ba8c27..fc27b89c269 100644
--- a/libs/core/src/lib/overflow-layout/directives/overflow-expand.directive.ts
+++ b/libs/core/src/lib/overflow-layout/directives/overflow-expand.directive.ts
@@ -1,4 +1,4 @@
-import { Directive, TemplateRef } from '@angular/core';
+import { Directive, Input, TemplateRef } from '@angular/core';
import { OverflowExpand, OverflowExpandDirectiveContext } from '../interfaces/overflow-expand.interface';
import { FD_OVERFLOW_EXPAND } from '../tokens/overflow-expand.token';
@@ -14,7 +14,10 @@ import { FD_OVERFLOW_EXPAND } from '../tokens/overflow-expand.token';
}
]
})
-export class OverflowExpandDirective implements OverflowExpand {
+export class OverflowExpandDirective implements OverflowExpand {
+ @Input()
+ fdOverflowExpandItems: T;
+
/** @hidden */
static ngTemplateContextGuard(
dir: OverflowExpandDirective,
@@ -24,5 +27,5 @@ export class OverflowExpandDirective implements OverflowExpand {
}
/** @hidden */
- constructor(public templateRef: TemplateRef) {}
+ constructor(public templateRef: TemplateRef>) {}
}
diff --git a/libs/core/src/lib/overflow-layout/directives/overflow-item-container-ref.directive.ts b/libs/core/src/lib/overflow-layout/directives/overflow-item-container-ref.directive.ts
index 8065c0466e8..d29b23d69ac 100644
--- a/libs/core/src/lib/overflow-layout/directives/overflow-item-container-ref.directive.ts
+++ b/libs/core/src/lib/overflow-layout/directives/overflow-item-container-ref.directive.ts
@@ -28,7 +28,7 @@ export class OverflowItemContainerRefDirective {
if (value && !this._detached) {
this._viewRef = this._viewContainerRef.detach()!;
this._detached = true;
- } else if (!value && this._viewRef && this._detached) {
+ } else if (!value && this._viewRef && !this._viewRef.destroyed && this._detached) {
this._viewRef = this._viewContainerRef.insert(this._viewRef);
this._detached = false;
}
diff --git a/libs/core/src/lib/overflow-layout/directives/overflow-item-ref.directive.ts b/libs/core/src/lib/overflow-layout/directives/overflow-item-ref.directive.ts
index 0c82539a409..4080e1ff054 100644
--- a/libs/core/src/lib/overflow-layout/directives/overflow-item-ref.directive.ts
+++ b/libs/core/src/lib/overflow-layout/directives/overflow-item-ref.directive.ts
@@ -1,4 +1,4 @@
-import { Directive, ElementRef, TemplateRef } from '@angular/core';
+import { Directive, ElementRef, Input, TemplateRef } from '@angular/core';
import { OverflowItemDirectiveContext, OverflowItemRef } from '../interfaces/overflow-item-ref.interface';
import { OverflowItem } from '../interfaces/overflow-item.interface';
import { FD_OVERFLOW_ITEM_REF } from '../tokens/overflow-item-ref.token';
@@ -15,7 +15,7 @@ import { FD_OVERFLOW_ITEM_REF } from '../tokens/overflow-item-ref.token';
}
]
})
-export class OverflowItemRefDirective implements OverflowItemRef {
+export class OverflowItemRefDirective implements OverflowItemRef {
/**
* Element ref of the `fdOverflowLayoutItem` directive.
*/
@@ -27,12 +27,33 @@ export class OverflowItemRefDirective implements OverflowItemRef {
/**
* Whether the item is hidden.
*/
- hidden = false;
+ get hidden(): boolean {
+ return this._hidden;
+ }
+
+ set hidden(value: boolean) {
+ this._hidden = value;
+ this.overflowItem.hiddenChange.emit(value);
+ }
+ _hidden = false;
/**
* Index of the item in the array of Overflow Layout Component's items.
*/
index: number;
+ /** Whether this item is last in the array. */
+ first: boolean;
+
+ /** Whether this item is first in the array. */
+ last: boolean;
+
+ /** Whether the item is softly hidden. */
+ softHidden = true;
+
+ /** Item instance. Used for correct autocomplete. */
+ @Input('fdOverflowItemRef')
+ item: T;
+
/** @hidden */
static ngTemplateContextGuard(
dir: OverflowItemRefDirective,
@@ -42,7 +63,7 @@ export class OverflowItemRefDirective implements OverflowItemRef {
}
/** @hidden */
- constructor(public templateRef: TemplateRef) {}
+ constructor(public templateRef: TemplateRef>) {}
/**
* Sets the element reference of the `fdOverflowLayoutItem` directive.`
diff --git a/libs/core/src/lib/overflow-layout/directives/overflow-layout-item-container.directive.ts b/libs/core/src/lib/overflow-layout/directives/overflow-layout-item-container.directive.ts
index 022a28792bc..3401048dea1 100644
--- a/libs/core/src/lib/overflow-layout/directives/overflow-layout-item-container.directive.ts
+++ b/libs/core/src/lib/overflow-layout/directives/overflow-layout-item-container.directive.ts
@@ -1,4 +1,4 @@
-import { ContentChild, Directive, ElementRef, HostBinding } from '@angular/core';
+import { ContentChild, Directive, ElementRef, HostBinding, Input } from '@angular/core';
import { OverflowItemContainerRefDirective } from './overflow-item-container-ref.directive';
@Directive({
@@ -9,6 +9,27 @@ export class OverflowLayoutItemContainerDirective {
@HostBinding('class')
private readonly _initialClass = 'fd-overflow-layout__item';
+ /**
+ * Whether this item is the first one in the array.
+ */
+ @Input()
+ @HostBinding('class.fd-overflow-layout__item--last')
+ last = false;
+
+ /**
+ * Whether this item is the last one in the array.
+ */
+ @Input()
+ @HostBinding('class.fd-overflow-layout__item--first')
+ first = false;
+
+ /**
+ * Whether this item is softly hidden. Used during free space calculation without flickering of the items.
+ */
+ @Input()
+ @HostBinding('class.fd-overflow-layout__item--soft-hidden')
+ softHidden = true;
+
/**
* Container reference.
*/
diff --git a/libs/core/src/lib/overflow-layout/directives/overflow-layout-item.directive.ts b/libs/core/src/lib/overflow-layout/directives/overflow-layout-item.directive.ts
index b8cd83a427b..eeff0209521 100644
--- a/libs/core/src/lib/overflow-layout/directives/overflow-layout-item.directive.ts
+++ b/libs/core/src/lib/overflow-layout/directives/overflow-layout-item.directive.ts
@@ -1,12 +1,14 @@
import {
Directive,
ElementRef,
+ EventEmitter,
HostBinding,
HostListener,
Inject,
Input,
OnInit,
Optional,
+ Output,
SkipSelf
} from '@angular/core';
import { OverflowItemRef } from '../interfaces/overflow-item-ref.interface';
@@ -46,6 +48,13 @@ export class OverflowLayoutItemDirective implements OverflowItem, OnInit {
get forceVisibility(): boolean {
return this._forceVisibility;
}
+
+ /**
+ * Event emitted when `hidden` property has been changed.
+ */
+ @Output()
+ hiddenChange = new EventEmitter();
+
/**
* Whether the item is hidden.
*/
@@ -53,6 +62,10 @@ export class OverflowLayoutItemDirective implements OverflowItem, OnInit {
return this._overflowItemRef?.hidden === true;
}
+ set hidden(value: boolean) {
+ this.hiddenChange.emit(value);
+ }
+
/** @hidden */
@HostBinding('attr.tabindex')
private get _tabindex(): number {
diff --git a/libs/core/src/lib/overflow-layout/index.ts b/libs/core/src/lib/overflow-layout/index.ts
index 9bebed127fa..da06a562fb6 100644
--- a/libs/core/src/lib/overflow-layout/index.ts
+++ b/libs/core/src/lib/overflow-layout/index.ts
@@ -12,3 +12,5 @@ export * from './interfaces/overflow-popover-content.interface';
export * from './tokens/overflow-expand.token';
export * from './tokens/overflow-item-ref.token';
export * from './tokens/overflow-item.token';
+export * from './tokens/overflow-container.token';
+export * from './overflow-layout.service';
diff --git a/libs/core/src/lib/overflow-layout/interfaces/overflow-expand.interface.ts b/libs/core/src/lib/overflow-layout/interfaces/overflow-expand.interface.ts
index 2ff236cb89b..8bb18d18e87 100644
--- a/libs/core/src/lib/overflow-layout/interfaces/overflow-expand.interface.ts
+++ b/libs/core/src/lib/overflow-layout/interfaces/overflow-expand.interface.ts
@@ -1,11 +1,11 @@
import { TemplateRef } from '@angular/core';
import { OverflowItemRef } from './overflow-item-ref.interface';
-export type OverflowExpandDirectiveContext = { $implicit: OverflowItemRef[] };
+export type OverflowExpandDirectiveContext = { $implicit: OverflowItemRef[] };
-export interface OverflowExpand {
+export interface OverflowExpand {
/**
* Template reference of the directive.
*/
- templateRef: TemplateRef;
+ templateRef: TemplateRef>;
}
diff --git a/libs/core/src/lib/overflow-layout/interfaces/overflow-item-ref.interface.ts b/libs/core/src/lib/overflow-layout/interfaces/overflow-item-ref.interface.ts
index 8a7d16ee338..c313f703af8 100644
--- a/libs/core/src/lib/overflow-layout/interfaces/overflow-item-ref.interface.ts
+++ b/libs/core/src/lib/overflow-layout/interfaces/overflow-item-ref.interface.ts
@@ -1,9 +1,15 @@
import { ElementRef, TemplateRef } from '@angular/core';
import { OverflowItem } from './overflow-item.interface';
-export type OverflowItemDirectiveContext = { $implicit: boolean; index: number };
+export type OverflowItemDirectiveContext = {
+ $implicit: boolean;
+ index: number;
+ first: boolean;
+ last: boolean;
+ item: T;
+};
-export interface OverflowItemRef {
+export interface OverflowItemRef {
/**
* Element reference.
*/
@@ -20,10 +26,22 @@ export interface OverflowItemRef {
* The index of the item in the array of items.
*/
index: number;
+
+ /** Whether this item is last in the array. */
+ first: boolean;
+
+ /** Whether this item is first in the array. */
+ last: boolean;
+
+ /** Whether the item is softly hidden. */
+ softHidden: boolean;
/**
* Template reference of the directive.
*/
- templateRef: TemplateRef;
+ templateRef: TemplateRef>;
+
+ /** Item instance. Used for correct autocomplete. */
+ item: T;
/**
* Sets the element reference of the directive.
diff --git a/libs/core/src/lib/overflow-layout/interfaces/overflow-item.interface.ts b/libs/core/src/lib/overflow-layout/interfaces/overflow-item.interface.ts
index 13717402ce1..b33c6234ba0 100644
--- a/libs/core/src/lib/overflow-layout/interfaces/overflow-item.interface.ts
+++ b/libs/core/src/lib/overflow-layout/interfaces/overflow-item.interface.ts
@@ -1,5 +1,5 @@
import { FocusableOption } from '@angular/cdk/a11y';
-import { ElementRef } from '@angular/core';
+import { ElementRef, EventEmitter } from '@angular/core';
export interface OverflowItem extends FocusableOption {
/**
@@ -18,4 +18,6 @@ export interface OverflowItem extends FocusableOption {
* Whether the item is hidden.
*/
hidden: boolean;
+
+ hiddenChange: EventEmitter;
}
diff --git a/libs/core/src/lib/overflow-layout/overflow-layout.component.html b/libs/core/src/lib/overflow-layout/overflow-layout.component.html
index 933196642f3..9bc1546f269 100644
--- a/libs/core/src/lib/overflow-layout/overflow-layout.component.html
+++ b/libs/core/src/lib/overflow-layout/overflow-layout.component.html
@@ -1,11 +1,20 @@
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libs/core/src/lib/overflow-layout/overflow-layout.component.scss b/libs/core/src/lib/overflow-layout/overflow-layout.component.scss
index 89d68e4e37c..c2e769d070c 100644
--- a/libs/core/src/lib/overflow-layout/overflow-layout.component.scss
+++ b/libs/core/src/lib/overflow-layout/overflow-layout.component.scss
@@ -52,4 +52,10 @@ $fd-block: 'fd-overflow-layout';
display: block;
white-space: normal;
}
+
+ &__item {
+ &--soft-hidden {
+ opacity: 0 !important;
+ }
+ }
}
diff --git a/libs/core/src/lib/overflow-layout/overflow-layout.component.spec.ts b/libs/core/src/lib/overflow-layout/overflow-layout.component.spec.ts
index 281b4465e1e..e70f540e170 100644
--- a/libs/core/src/lib/overflow-layout/overflow-layout.component.spec.ts
+++ b/libs/core/src/lib/overflow-layout/overflow-layout.component.spec.ts
@@ -1,5 +1,5 @@
import { Component, ViewChild } from '@angular/core';
-import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { OverflowLayoutItemDirective } from './directives/overflow-layout-item.directive';
import { OverflowLayoutComponent } from './overflow-layout.component';
@@ -12,12 +12,14 @@ import { OverflowLayoutModule } from './overflow-layout.module';
+ >
+ {{ i }}
+
@@ -69,20 +71,18 @@ describe('OverflowLayoutComponent', () => {
);
});
- it('should render automatic amount of items', async (done) => {
+ it('should render automatic amount of items', fakeAsync(() => {
+ tick(1000);
const expectedAmount = Math.floor(component.containerWidth / component.elementsWidth);
-
- component.overflowLayout.visibleItemsCount.subscribe((value) => {
- expect(value).toEqual(expectedAmount);
- done();
- });
-
+ const visibleItemsCountSpy = spyOn(component.overflowLayout.visibleItemsCount, 'emit').and.callThrough();
component.maxItems = Infinity;
fixture.detectChanges();
- await fixture.whenStable();
+ tick(1000);
+
+ expect(visibleItemsCountSpy).toHaveBeenCalledWith(expectedAmount);
expect(fixture.debugElement.queryAll(By.directive(OverflowLayoutItemDirective)).length).toEqual(expectedAmount);
- });
+ }));
it('should react on items resize', async () => {
component.elementsWidth = 300;
diff --git a/libs/core/src/lib/overflow-layout/overflow-layout.component.ts b/libs/core/src/lib/overflow-layout/overflow-layout.component.ts
index 36818513520..c93738fb333 100644
--- a/libs/core/src/lib/overflow-layout/overflow-layout.component.ts
+++ b/libs/core/src/lib/overflow-layout/overflow-layout.component.ts
@@ -1,4 +1,3 @@
-import { FocusKeyManager } from '@angular/cdk/a11y';
import { DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW, TAB, UP_ARROW } from '@angular/cdk/keycodes';
import {
Component,
@@ -22,14 +21,15 @@ import {
OnDestroy,
Input
} from '@angular/core';
-import { KeyUtil, resizeObservable, RtlService } from '@fundamental-ngx/core/utils';
-import { debounceTime, distinctUntilChanged, filter, skip, Subject, Subscription } from 'rxjs';
+import { KeyUtil, RtlService } from '@fundamental-ngx/core/utils';
+import { debounceTime, Subject, Subscription } from 'rxjs';
import { OverflowLayoutItemContainerDirective } from './directives/overflow-layout-item-container.directive';
import { OverflowContainer } from './interfaces/overflow-container.interface';
import { OverflowExpand } from './interfaces/overflow-expand.interface';
import { OverflowItemRef } from './interfaces/overflow-item-ref.interface';
import { OverflowItem } from './interfaces/overflow-item.interface';
import { OverflowPopoverContent } from './interfaces/overflow-popover-content.interface';
+import { OverflowLayoutConfig, OverflowLayoutService } from './overflow-layout.service';
import { FD_OVERFLOW_CONTAINER } from './tokens/overflow-container.token';
import { FD_OVERFLOW_EXPAND } from './tokens/overflow-expand.token';
import { FD_OVERFLOW_ITEM_REF } from './tokens/overflow-item-ref.token';
@@ -45,7 +45,8 @@ import { FD_OVERFLOW_ITEM } from './tokens/overflow-item.token';
{
provide: FD_OVERFLOW_CONTAINER,
useExisting: OverflowLayoutComponent
- }
+ },
+ OverflowLayoutService
]
})
export class OverflowLayoutComponent implements AfterViewInit, OnDestroy, OverflowContainer {
@@ -65,6 +66,18 @@ export class OverflowLayoutComponent implements AfterViewInit, OnDestroy, Overfl
return this._maxVisibleItems;
}
+ /** Direction of the fitting items calculation. */
+ @Input()
+ showMorePosition: 'left' | 'right' = 'right';
+
+ /** Whether to render hidden items in reverse order. */
+ @Input()
+ reverseHiddenItems = false;
+
+ /** Whether to enable keyboard navigation. */
+ @Input()
+ enableKeyboardNavigation = true;
+
/**
* Event, triggered when amount of visible items has been changed.
*/
@@ -81,7 +94,7 @@ export class OverflowLayoutComponent implements AfterViewInit, OnDestroy, Overfl
* @hidden
* List of items to display.
*/
- @ContentChildren(FD_OVERFLOW_ITEM_REF)
+ @ContentChildren(FD_OVERFLOW_ITEM_REF, { descendants: true })
_items: QueryList;
/**
@@ -95,7 +108,7 @@ export class OverflowLayoutComponent implements AfterViewInit, OnDestroy, Overfl
* @hidden
* List of items that can be focused.
*/
- @ContentChildren(FD_OVERFLOW_ITEM)
+ @ContentChildren(FD_OVERFLOW_ITEM, { descendants: true })
_overflowItems: QueryList;
/**
@@ -139,12 +152,6 @@ export class OverflowLayoutComponent implements AfterViewInit, OnDestroy, Overfl
@HostBinding('class')
private readonly _initialClass = 'fd-overflow-layout';
- /** @hidden */
- private _keyboardEventsManager: FocusKeyManager;
-
- /** @hidden */
- private _listenToItemResize = true;
-
/** @hidden */
private readonly _subscription = new Subscription();
@@ -155,25 +162,32 @@ export class OverflowLayoutComponent implements AfterViewInit, OnDestroy, Overfl
private _fillTrigger$ = new Subject();
/** @hidden */
- private _dir: 'rtl' | 'ltr' = 'ltr';
+ private _maxVisibleItems = Infinity;
/** @hidden */
- private _maxVisibleItems = Infinity;
+ private _canListenToResize = false;
/** @hidden */
constructor(
- private _cdr: ChangeDetectorRef,
- private _zone: NgZone,
- private _elRef: ElementRef,
- @Optional() private _rtlService: RtlService
+ protected _cdr: ChangeDetectorRef,
+ protected _zone: NgZone,
+ protected _elRef: ElementRef,
+ protected _overflowLayoutService: OverflowLayoutService,
+ @Optional() protected _rtlService: RtlService
) {
- this._subscription.add(this._fillTrigger$.pipe(debounceTime(30)).subscribe(() => this._fitVisibleItems()));
+ this._subscription.add(
+ this._fillTrigger$.pipe(debounceTime(30)).subscribe(() => this._overflowLayoutService.fitVisibleItems())
+ );
}
/**
* Triggers layout recalculation of the items.
*/
triggerRecalculation(): void {
+ if (!this._canListenToResize) {
+ return;
+ }
+ this._overflowLayoutService.setConfig(this._getConfig());
this._fillTrigger$.next();
}
@@ -182,26 +196,6 @@ export class OverflowLayoutComponent implements AfterViewInit, OnDestroy, Overfl
this._subscription.unsubscribe();
}
- /** @hidden */
- @HostListener('keyup', ['$event'])
- private _keyUpHandler(event: KeyboardEvent): void {
- if (KeyUtil.isKeyCode(event, TAB)) {
- const index = this._allItems.findIndex(
- (item) => item.overflowItem?.focusable && item.elementRef.nativeElement === event.target
- );
- if (index !== -1) {
- this._keyboardEventsManager.setActiveItem(index);
- }
- }
-
- if (KeyUtil.isKeyCode(event, [DOWN_ARROW, UP_ARROW, LEFT_ARROW, RIGHT_ARROW])) {
- event.preventDefault();
-
- // passing the event to key manager so, we get a change fired
- this._keyboardEventsManager.onKeydown(event);
- }
- }
-
/**
* Sets current focused element.
* @param element Element that needs to be focused.
@@ -210,7 +204,7 @@ export class OverflowLayoutComponent implements AfterViewInit, OnDestroy, Overfl
const index = this._overflowItems.toArray().findIndex((item) => item === element);
if (index !== -1) {
- this._keyboardEventsManager.setActiveItem(index);
+ this._overflowLayoutService._keyboardEventsManager.setActiveItem(index);
}
}
@@ -236,173 +230,72 @@ export class OverflowLayoutComponent implements AfterViewInit, OnDestroy, Overfl
/** @hidden */
ngAfterViewInit(): void {
- this._fitVisibleItems();
- this._setFocusKeyManager();
- this._listenToChanges();
- this._subscribeToRtl();
- }
-
- /** @hidden */
- private _listenToChanges(): void {
this._subscription.add(
- this._items.changes.subscribe(() => {
- setTimeout(() => {
- this._fitVisibleItems();
- });
+ this._overflowLayoutService.detectChanges.subscribe(() => {
+ this._cdr.detectChanges();
})
);
- this._listenToSizeChanges(this._elRef.nativeElement, this._itemsWrapper.nativeElement);
- }
+ this._subscription.add(
+ this._overflowLayoutService.onResult.subscribe((result) => {
+ this._hiddenItems = result.hiddenItems;
+ this._showMore = result.showMore;
+ this.hiddenItemsCount.emit(result.hiddenItems.length);
+ this.visibleItemsCount.emit(this._allItems.filter((i) => !i.hidden).length);
+ this._cdr.detectChanges();
+ })
+ );
- /** @hidden */
- private _listenToSizeChanges(...elements: HTMLElement[]): void {
- elements.forEach((element) =>
- this._subscription.add(
- resizeObservable(element)
- .pipe(
- skip(1),
- filter(() => this._listenToItemResize),
- distinctUntilChanged(),
- debounceTime(30)
- )
- .subscribe(() => {
- setTimeout(() => {
- this._fitVisibleItems();
- });
- })
- )
+ this._subscription.add(
+ this._items.changes.subscribe(() => {
+ this._allItems = this._items.toArray();
+ this._cdr.detectChanges();
+ })
);
- }
- /** @hidden */
- private _fitVisibleItems(): void {
- this._listenToItemResize = false;
this._allItems = this._items.toArray();
- this._visibleItems.forEach((i) => (i.containerRef.hidden = false));
- this._allItems.forEach((item, index) => {
- item.hidden = false;
- item.index = index;
- });
- this._cdr.detectChanges();
- const containerWidth = this._elRef.nativeElement.getBoundingClientRect().width;
- const itemsContainerWidth = this._itemsWrapper.nativeElement.getBoundingClientRect().width;
-
- if (
- containerWidth >= itemsContainerWidth &&
- this._visibleItems.length <= this.maxVisibleItems &&
- this._hiddenItems.length === 0
- ) {
- this._showMore = false;
- this._cdr.detectChanges();
- this._listenToItemResize = true;
- return;
- }
- this._showMore = true;
- let fittingElmCount = 0;
- let fittingElmsWidth = 0;
- let shouldHideItems = false;
this._cdr.detectChanges();
- const showMoreContainerWidth = this._showMoreContainer.nativeElement.getBoundingClientRect().width;
- let layoutWidth = this._layoutContainer.nativeElement.getBoundingClientRect().width;
+ this._overflowLayoutService.startListening(this._getConfig());
- // Try to find all forced visible items
- const forcedItemsIndexes = this._getForcedItemsIndexes();
-
- forcedItemsIndexes.forEach((itemIndex) => {
- const container = this._visibleItems.get(itemIndex);
- if (!container) {
- return;
- }
- const elementSize = this._getElementWidth(container.elementRef.nativeElement);
-
- layoutWidth -= elementSize;
- });
-
- if (layoutWidth < 0 && forcedItemsIndexes.length > 0) {
- console.warn(
- 'There is no enough space to fit all forced visible items into the container. Please adjust their visibility accordingly.'
- );
- }
-
- this._visibleItems.forEach((item, index) => {
- const itemRef = this._allItems[index];
- if (shouldHideItems && !itemRef.overflowItem.forceVisibility) {
- item.containerRef.hidden = true;
- itemRef.hidden = true;
- return;
- }
-
- const elementSize = this._getElementWidth(item.elementRef.nativeElement);
- const combinedWidth = fittingElmsWidth + elementSize;
-
- if (
- (combinedWidth <= layoutWidth ||
- (item === this._visibleItems.last && combinedWidth <= layoutWidth + showMoreContainerWidth)) &&
- fittingElmCount < this.maxVisibleItems
- ) {
- fittingElmsWidth += elementSize;
- fittingElmCount++;
- } else if (!itemRef.overflowItem.forceVisibility) {
- shouldHideItems = true;
- item.containerRef.hidden = true;
- itemRef.hidden = true;
- }
- });
-
- this._hiddenItems = this._allItems.filter((i) => i.hidden);
- this.visibleItemsCount.emit(this._allItems.filter((i) => !i.hidden).length);
- this.hiddenItemsCount.emit(this._hiddenItems.length);
-
- this._showMore = this._hiddenItems.length > 0;
-
- this._cdr.detectChanges();
-
- this._listenToItemResize = true;
+ this._canListenToResize = true;
}
/** @hidden */
- private _setFocusKeyManager(): void {
- this._dir = this._rtlService?.rtl.value ? 'rtl' : 'ltr';
- this._keyboardEventsManager = new FocusKeyManager(this._overflowItems)
- .withWrap()
- .withHorizontalOrientation(this._dir)
- .skipPredicate((item) => !item.focusable || item.hidden);
+ private _getConfig(): OverflowLayoutConfig {
+ return {
+ visibleItems: this._visibleItems,
+ items: this._items,
+ itemsWrapper: this._itemsWrapper.nativeElement,
+ showMoreContainer: this._showMoreContainer.nativeElement,
+ layoutContainerElement: this._layoutContainer.nativeElement,
+ maxVisibleItems: this.maxVisibleItems,
+ direction: this.showMorePosition,
+ enableKeyboardNavigation: this.enableKeyboardNavigation,
+ reverseHiddenItems: this.reverseHiddenItems
+ };
}
- /** @hidden Rtl change subscription */
- private _subscribeToRtl(): void {
- if (!this._rtlService) {
+ /** @hidden */
+ @HostListener('keyup', ['$event'])
+ private _keyUpHandler(event: KeyboardEvent): void {
+ if (!this.enableKeyboardNavigation) {
return;
}
+ if (KeyUtil.isKeyCode(event, TAB)) {
+ const index = this._allItems.findIndex(
+ (item) => item.overflowItem?.focusable && item.elementRef.nativeElement === event.target
+ );
+ if (index !== -1) {
+ this._overflowLayoutService._keyboardEventsManager.setActiveItem(index);
+ }
+ }
- const rtlSub = this._rtlService.rtl.subscribe((isRtl) => {
- this._dir = isRtl ? 'rtl' : 'ltr';
-
- this._keyboardEventsManager = this._keyboardEventsManager.withHorizontalOrientation(isRtl ? 'rtl' : 'ltr');
- });
-
- this._subscription.add(rtlSub);
- }
-
- /** @hidden */
- private _getForcedItemsIndexes(): number[] {
- return this._allItems
- .map((item, index) => (item.overflowItem.forceVisibility ? index : -1))
- .filter((i) => i > -1);
- }
-
- /**
- * @hidden
- * Returns combined width of the element including margins.
- * @param element Element to calculate width of.
- */
- private _getElementWidth(element: HTMLElement): number {
- const elementStyle = getComputedStyle(element);
- const elementWidth = element.getBoundingClientRect().width;
- const elementSize = elementWidth + parseFloat(elementStyle.marginLeft) + parseFloat(elementStyle.marginRight);
+ if (KeyUtil.isKeyCode(event, [DOWN_ARROW, UP_ARROW, LEFT_ARROW, RIGHT_ARROW])) {
+ event.preventDefault();
- return elementSize;
+ // passing the event to key manager so, we get a change fired
+ this._overflowLayoutService._keyboardEventsManager.onKeydown(event);
+ }
}
}
diff --git a/libs/core/src/lib/overflow-layout/overflow-layout.service.ts b/libs/core/src/lib/overflow-layout/overflow-layout.service.ts
new file mode 100644
index 00000000000..18f057eb6c8
--- /dev/null
+++ b/libs/core/src/lib/overflow-layout/overflow-layout.service.ts
@@ -0,0 +1,319 @@
+import { FocusKeyManager } from '@angular/cdk/a11y';
+import { ElementRef, Injectable, OnDestroy, Optional, QueryList } from '@angular/core';
+import { resizeObservable, RtlService } from '@fundamental-ngx/core/utils';
+import { debounceTime, distinctUntilChanged, filter, Observable, skip, Subject, Subscription } from 'rxjs';
+import { OverflowLayoutItemContainerDirective } from './directives/overflow-layout-item-container.directive';
+import { OverflowItemRef } from './interfaces/overflow-item-ref.interface';
+import { OverflowItem } from './interfaces/overflow-item.interface';
+
+export interface OverflowLayoutConfig {
+ items: QueryList;
+ visibleItems: QueryList;
+ itemsWrapper: HTMLElement;
+ showMoreContainer: HTMLElement;
+ layoutContainerElement: HTMLElement;
+ maxVisibleItems: number;
+ direction: 'left' | 'right';
+ enableKeyboardNavigation: boolean;
+ reverseHiddenItems: boolean;
+}
+
+export class OverflowLayoutListeningResult {
+ showMore = false;
+ items: OverflowItemRef[] = [];
+ hiddenItems: OverflowItemRef[] = [];
+ visibleItems: OverflowItemRef[] = [];
+}
+
+@Injectable()
+export class OverflowLayoutService implements OnDestroy {
+ /**
+ * Overflow Layout config.
+ */
+ config: OverflowLayoutConfig;
+
+ /**
+ * Overflow Layout calculation result.
+ */
+ result = new OverflowLayoutListeningResult();
+
+ /** @hidden */
+ _keyboardEventsManager: FocusKeyManager;
+
+ /** @hidden */
+ private _listenToItemResize = true;
+
+ /** @hidden */
+ private readonly _subscription = new Subscription();
+
+ /** @hidden */
+ private _allItems: OverflowItemRef[] = [];
+
+ /** @hidden */
+ private _hiddenItems: OverflowItemRef[] = [];
+
+ /** @hidden */
+ private _overflowItems: OverflowItem[] = [];
+
+ /** @hidden */
+ private _detectChanges$ = new Subject();
+
+ /** @hidden */
+ private _result$ = new Subject();
+
+ /** @hidden */
+ private _dir: 'rtl' | 'ltr' = 'ltr';
+
+ /**
+ * Observable which emits when changes detection is required.
+ */
+ get detectChanges(): Observable {
+ return this._detectChanges$.asObservable();
+ }
+
+ /**
+ * Observable which emits when new calculation result is available.
+ */
+ get onResult(): Observable {
+ return this._result$.asObservable();
+ }
+
+ /** @hidden */
+ constructor(private _elRef: ElementRef, @Optional() private _rtlService: RtlService | null) {}
+
+ /** @hidden */
+ ngOnDestroy(): void {
+ this._subscription.unsubscribe();
+ }
+
+ startListening(config: OverflowLayoutConfig): void {
+ this.setConfig(config);
+ this.fitVisibleItems();
+ this._setFocusKeyManager();
+ this._listenToChanges();
+ this._subscribeToRtl();
+ }
+
+ setConfig(config: OverflowLayoutConfig): void {
+ this.config = config;
+ }
+
+ private _emitResult(): void {
+ this._result$.next(this.result);
+ }
+
+ /** @hidden */
+ private _listenToChanges(): void {
+ this._subscription.add(
+ this.config.items.changes.subscribe(() => {
+ setTimeout(() => {
+ this.fitVisibleItems();
+ });
+ })
+ );
+
+ this._listenToSizeChanges(this._elRef.nativeElement, this.config.itemsWrapper);
+ }
+
+ /** @hidden */
+ private _listenToSizeChanges(...elements: HTMLElement[]): void {
+ elements.forEach((element) =>
+ this._subscription.add(
+ resizeObservable(element)
+ .pipe(
+ skip(1),
+ filter(() => this._listenToItemResize),
+ distinctUntilChanged(),
+ debounceTime(30)
+ )
+ .subscribe(() => {
+ setTimeout(() => {
+ this.fitVisibleItems();
+ });
+ })
+ )
+ );
+ }
+
+ /** @hidden */
+ fitVisibleItems(): void {
+ this._listenToItemResize = false;
+ this._allItems = this.config.items.toArray();
+
+ let allItems = this.config.items.toArray();
+
+ allItems.forEach((item, index) => {
+ // Softly hide previously completely hidden item in order to correctly calculate it's size.
+ item.softHidden = true;
+ item.hidden = false;
+ item.index = index;
+ item.first = index === 0;
+ item.last = index === allItems.length - 1;
+ });
+
+ this._detectChanges$.next();
+
+ allItems = this.config.direction === 'right' ? allItems : allItems.reverse();
+ const visibleContainerItems =
+ this.config.direction === 'right'
+ ? this.config.visibleItems.toArray()
+ : this.config.visibleItems.toArray().reverse();
+ visibleContainerItems.forEach((i) => (i.containerRef.hidden = false));
+
+ this.result.showMore = false;
+ this._emitResult();
+ const containerWidth = this._elRef.nativeElement.getBoundingClientRect().width;
+ const itemsContainerWidth = this.config.itemsWrapper.getBoundingClientRect().width;
+
+ if (
+ containerWidth >= itemsContainerWidth &&
+ this.config.visibleItems.length <= this.config.maxVisibleItems &&
+ this._hiddenItems.length === 0
+ ) {
+ // Make all items fully visible.
+ allItems.forEach((item) => {
+ item.softHidden = false;
+ });
+ this.result.showMore = false;
+ this.result.hiddenItems = this._hiddenItems;
+ this._emitResult();
+ this._listenToItemResize = true;
+ return;
+ }
+ this.result.showMore = true;
+ this._emitResult();
+ let fittingElmCount = 0;
+ let fittingElmsWidth = 0;
+ let shouldHideItems = false;
+
+ const showMoreContainerWidth = Math.ceil(this.config.showMoreContainer.getBoundingClientRect().width);
+ let layoutWidth = Math.ceil(this.config.layoutContainerElement.getBoundingClientRect().width);
+
+ // Try to find all forced visible items
+ const forcedItemsIndexes = this._getForcedItemsIndexes();
+
+ forcedItemsIndexes.forEach((itemIndex) => {
+ const container = this.config.visibleItems.get(itemIndex);
+ if (!container) {
+ return;
+ }
+ const elementSize = this._getElementWidth(container.elementRef.nativeElement);
+
+ layoutWidth -= elementSize;
+ });
+
+ if (layoutWidth < 0 && forcedItemsIndexes.length > 0) {
+ console.warn(
+ 'There is no enough space to fit all forced visible items into the container. Please adjust their visibility accordingly.'
+ );
+ }
+
+ this._detectChanges$.next();
+
+ visibleContainerItems.forEach((item, index) => {
+ const itemRef = allItems[index];
+ if (shouldHideItems && !itemRef.overflowItem.forceVisibility) {
+ item.containerRef.hidden = true;
+ item.softHidden = false;
+ itemRef.hidden = true;
+ return;
+ }
+
+ const elementSize = this._getElementWidth(item.elementRef.nativeElement);
+ const combinedWidth = fittingElmsWidth + elementSize;
+
+ const condition =
+ (combinedWidth <= layoutWidth ||
+ (item === this.config.visibleItems.last &&
+ combinedWidth <= layoutWidth + showMoreContainerWidth)) &&
+ fittingElmCount < this.config.maxVisibleItems;
+
+ if (condition) {
+ fittingElmsWidth += elementSize;
+ fittingElmCount++;
+ } else if (!itemRef.overflowItem.forceVisibility) {
+ shouldHideItems = true;
+ item.softHidden = false;
+ item.containerRef.hidden = true;
+ itemRef.hidden = true;
+ }
+ });
+
+ // Reverse original order back.
+ allItems = this.config.direction === 'right' ? allItems : allItems.reverse();
+
+ allItems.forEach((item) => {
+ item.softHidden = false;
+ });
+
+ let hiddenItems = allItems.filter((i) => i.hidden);
+ hiddenItems = !this.config.reverseHiddenItems ? hiddenItems.reverse() : hiddenItems;
+ const visibleItems = allItems.filter((i) => !i.hidden);
+
+ visibleItems.forEach((item, index) => {
+ item.index = index;
+ item.first = index === 0;
+ item.last = index === visibleItems.length - 1;
+ });
+
+ this._hiddenItems = hiddenItems.map((item, index) => {
+ item.first = index === 0;
+ item.last = index === hiddenItems.length - 1;
+ item.index = index;
+ return item;
+ });
+
+ this.result.showMore = this._hiddenItems.length > 0;
+ this.result.hiddenItems = this._hiddenItems;
+ this._emitResult();
+
+ this._listenToItemResize = true;
+ }
+
+ /** @hidden */
+ private _setFocusKeyManager(): void {
+ if (!this.config.enableKeyboardNavigation) {
+ return;
+ }
+ this._dir = this._rtlService?.rtl.value ? 'rtl' : 'ltr';
+ this._keyboardEventsManager = new FocusKeyManager(this._overflowItems)
+ .withWrap()
+ .withHorizontalOrientation(this._dir)
+ .skipPredicate((item) => !item.focusable || item.hidden);
+ }
+
+ /** @hidden Rtl change subscription */
+ private _subscribeToRtl(): void {
+ if (!this._rtlService || !this.config.enableKeyboardNavigation) {
+ return;
+ }
+
+ this._subscription.add(
+ this._rtlService.rtl.subscribe((isRtl) => {
+ this._dir = isRtl ? 'rtl' : 'ltr';
+
+ this._keyboardEventsManager = this._keyboardEventsManager.withHorizontalOrientation(this._dir);
+ })
+ );
+ }
+
+ /** @hidden */
+ private _getForcedItemsIndexes(): number[] {
+ return this._allItems
+ .map((item, index) => (item.overflowItem.forceVisibility ? index : -1))
+ .filter((i) => i > -1);
+ }
+
+ /**
+ * @hidden
+ * Returns combined width of the element including margins.
+ * @param element Element to calculate width of.
+ */
+ private _getElementWidth(element: HTMLElement): number {
+ const elementStyle = getComputedStyle(element);
+ const elementWidth = element.getBoundingClientRect().width;
+ const elementSize = elementWidth + parseFloat(elementStyle.marginLeft) + parseFloat(elementStyle.marginRight);
+
+ return Math.ceil(elementSize);
+ }
+}
diff --git a/libs/core/src/lib/overflow-layout/test.ts b/libs/core/src/lib/overflow-layout/test.ts
index f7496773ee9..54954bff26d 100644
--- a/libs/core/src/lib/overflow-layout/test.ts
+++ b/libs/core/src/lib/overflow-layout/test.ts
@@ -2,7 +2,7 @@
import 'core-js/es/reflect';
import 'zone.js';
-
+import '@angular/localize/init';
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
diff --git a/libs/core/src/lib/time/time-column/time-column.component.ts b/libs/core/src/lib/time/time-column/time-column.component.ts
index ba9e2f7576e..18231c81568 100644
--- a/libs/core/src/lib/time/time-column/time-column.component.ts
+++ b/libs/core/src/lib/time/time-column/time-column.component.ts
@@ -189,21 +189,18 @@ export class TimeColumnComponent = Selectable
/** @hidden */
private _viewInit$ = new BehaviorSubject(false);
+ private _resize$ = new BehaviorSubject(false);
+
/** @hidden */
private _subscriptions: Subscription = new Subscription();
/** @hidden */
constructor(private _changeDetRef: ChangeDetectorRef, private _elmRef: ElementRef) {
this._subscriptions.add(
- combineLatest([
- this._viewInit$,
- this._elementsAtOnce$,
- this._offset$,
- resizeObservable(this._elmRef.nativeElement)
- ])
+ combineLatest([this._viewInit$, this._elementsAtOnce$, this._offset$, this._resize$])
.pipe(
filter(([viewInit]) => viewInit),
- tap(([, elementsAtOnce, offset, size]) => {
+ tap(([, elementsAtOnce, offset]) => {
const averageHeight =
this.items.toArray().reduce((acc, next) => acc + next.getHeight(), 0) / this.items.length;
this.wrapperHeight = averageHeight * elementsAtOnce;
@@ -236,6 +233,11 @@ export class TimeColumnComponent = Selectable
/** @hidden */
ngAfterViewInit(): void {
+ this._subscriptions.add(
+ resizeObservable(this._elmRef.nativeElement).subscribe(() => {
+ this._resize$.next(true);
+ })
+ );
this._viewInit$.next(true);
}
diff --git a/libs/core/src/lib/utils/functions/resize-observable.ts b/libs/core/src/lib/utils/functions/resize-observable.ts
index 4a25adc4731..58bb0453aea 100644
--- a/libs/core/src/lib/utils/functions/resize-observable.ts
+++ b/libs/core/src/lib/utils/functions/resize-observable.ts
@@ -10,13 +10,20 @@ import { map } from 'rxjs/operators';
export function resizeObservable(target: Element, options?: ResizeObserverOptions): Observable {
if ('ResizeObserver' in window) {
return new Observable((subscriber) => {
+ let animationFrame: number;
const ro = new ResizeObserver((entries) => {
- subscriber.next(entries);
+ animationFrame = window.requestAnimationFrame(() => {
+ subscriber.next(entries);
+ });
});
ro.observe(target, options);
return function unsubscribe(): void {
+ if (animationFrame) {
+ window.cancelAnimationFrame(animationFrame);
+ }
+ ro.unobserve(target);
ro.disconnect();
};
});
diff --git a/libs/fn/src/lib/select/select.component.ts b/libs/fn/src/lib/select/select.component.ts
index 9310cd79fbe..a75e9e47ca1 100644
--- a/libs/fn/src/lib/select/select.component.ts
+++ b/libs/fn/src/lib/select/select.component.ts
@@ -108,9 +108,11 @@ export class SelectComponent implements AfterContentInit, OnDestroy, ControlValu
ngAfterContentInit(): void {
this._selectWidth = this._elRef.nativeElement.getBoundingClientRect().width;
- resizeObservable(this._elRef.nativeElement).subscribe(() => {
- this._selectWidth = this._elRef.nativeElement.getBoundingClientRect().width;
- });
+ this._subscriptions.add(
+ resizeObservable(this._elRef.nativeElement).subscribe(() => {
+ this._selectWidth = this._elRef.nativeElement.getBoundingClientRect().width;
+ })
+ );
if (!this._internalValue) {
return;