Skip to content

Commit 7d10761

Browse files
committed
fix(breadcrumbs): improve overflow width calculation and rendering
performance
1 parent db03247 commit 7d10761

1 file changed

Lines changed: 45 additions & 27 deletions

File tree

packages/components/breadcrumbs/breadcrumbs.ts

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,26 @@ import {
1414
input,
1515
OnInit,
1616
Provider,
17+
signal,
1718
TemplateRef,
1819
viewChild,
1920
viewChildren,
2021
ViewEncapsulation
2122
} from '@angular/core';
23+
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
2224
import { RouterLink } from '@angular/router';
2325
import { KbqButton, KbqButtonModule, KbqButtonStyles } from '@koobiq/components/button';
2426
import { KbqComponentColors, KbqDefaultSizes, PopUpPlacements } from '@koobiq/components/core';
2527
import { KbqDropdownModule } from '@koobiq/components/dropdown';
2628
import { KbqIconModule } from '@koobiq/components/icon';
27-
import { KbqOverflowItem, KbqOverflowItemsModule } from '@koobiq/components/overflow-items';
29+
import {
30+
ElementVisibilityManager,
31+
KbqOverflowItem,
32+
KbqOverflowItemsModule,
33+
KbqOverflowItemsResult
34+
} from '@koobiq/components/overflow-items';
2835
import { KbqTitleModule } from '@koobiq/components/title';
36+
import { debounceTime, merge } from 'rxjs';
2937
import { KbqBreadcrumbsConfiguration, KbqBreadcrumbsWrapMode } from './breadcrumbs.types';
3038
import { RdxRovingFocusGroupDirective } from './roving-focus-group.directive';
3139
import { RdxRovingFocusItemDirective } from './roving-focus-item.directive';
@@ -195,11 +203,12 @@ export class KbqBreadcrumbs {
195203

196204
protected readonly separator = contentChild(KbqBreadcrumbsSeparator, { read: TemplateRef });
197205

198-
protected readonly items = contentChildren(forwardRef(() => KbqBreadcrumbItem));
206+
protected readonly items = contentChildren<KbqBreadcrumbItem>(forwardRef(() => KbqBreadcrumbItem));
199207

200208
private readonly breadcrumbsResult = viewChild('breadcrumbsResult', { read: ElementRef });
209+
private readonly result = viewChild(KbqOverflowItemsResult);
201210

202-
private readonly overflowItems = viewChildren(forwardRef(() => KbqOverflowItem));
211+
private readonly overflowItems = viewChildren<KbqOverflowItem>(forwardRef(() => KbqOverflowItem));
203212

204213
/**
205214
* Ensures at least minimum number of breadcrumb items are shown.
@@ -217,35 +226,20 @@ export class KbqBreadcrumbs {
217226
* @returns {number | null} The computed max width for overflow items or null if conditions are not met.
218227
* @docs-private
219228
*/
220-
protected readonly maxWidth = computed((): number | null => {
221-
const overflowItems = this.overflowItems();
222-
const max = this.max();
223-
224-
if (!overflowItems.length || max === null || max >= overflowItems.length || max < this.minVisibleItems) {
225-
return null;
226-
}
227-
228-
// Reorders overflow items to prioritize the first and last elements
229-
const sortedItems = [
230-
...overflowItems.slice(1, -1),
231-
overflowItems[0],
232-
overflowItems[overflowItems.length - 1]
233-
];
234-
235-
let visibleItemsWidth = 0;
236-
237-
for (let i = 0; i < max; i++) {
238-
visibleItemsWidth += this.getItemWidth(sortedItems[sortedItems.length - i - 1]);
239-
}
240-
241-
return visibleItemsWidth;
242-
});
229+
protected readonly maxWidth = signal<number | null>(null);
243230

244231
constructor() {
245232
const group = inject(RdxRovingFocusGroupDirective, { self: true });
246233

247234
group.orientation = 'horizontal';
248235

236+
// Defer measurement to after layout (same pattern as KbqOverflowItems.setupObservers).
237+
// computed() fires synchronously during change detection before the browser has finished
238+
// layout, so offsetWidth reads return incorrect values at that point.
239+
merge(toObservable(this.overflowItems), toObservable(this.max))
240+
.pipe(debounceTime(0), takeUntilDestroyed())
241+
.subscribe(() => this.maxWidth.set(this.calculateMaxWidth()));
242+
249243
effect(() => {
250244
const focusableItems = group.focusableItems();
251245
const expandButton = this.breadcrumbsResult()?.nativeElement;
@@ -258,7 +252,31 @@ export class KbqBreadcrumbs {
258252
});
259253
}
260254

261-
private getItemWidth(item?: KbqOverflowItem) {
255+
private calculateMaxWidth(): number | null {
256+
const overflowItems = this.overflowItems();
257+
const max = this.max();
258+
259+
if (!overflowItems.length || max === null || max >= overflowItems.length || max < this.minVisibleItems) {
260+
return null;
261+
}
262+
263+
// Reorders overflow items to prioritize the last and first elements
264+
const sortedItems = [
265+
...overflowItems.slice(1, -1),
266+
overflowItems[0],
267+
overflowItems[overflowItems.length - 1]
268+
];
269+
270+
let width = this.getItemWidth(this.result()) ?? 0;
271+
272+
for (let i = 0; i < max - 1; i++) {
273+
width += this.getItemWidth(sortedItems[sortedItems.length - i - 1]);
274+
}
275+
276+
return width;
277+
}
278+
279+
private getItemWidth(item?: ElementVisibilityManager) {
262280
return item ? item.element.offsetWidth : 0;
263281
}
264282
}

0 commit comments

Comments
 (0)