Skip to content
73 changes: 71 additions & 2 deletions packages/components/actions-panel/actions-panel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { lastValueFrom } from 'rxjs';
import { KBQ_ACTIONS_PANEL_DATA, KBQ_ACTIONS_PANEL_OVERLAY_SELECTOR, KbqActionsPanel } from './actions-panel';
import { KbqActionsPanelConfig } from './actions-panel-config';
import { KbqActionsPanelConfig, kbqActionsPanelDefaultConfigProvider } from './actions-panel-config';
import { KbqActionsPanelRef } from './actions-panel-ref';
import { KbqActionsPanelModule } from './module';

Expand Down Expand Up @@ -162,7 +162,7 @@ describe(KbqActionsPanelModule.name, () => {
expect(getOverlayPaneElement().style.maxWidth).toBe('500px');
});

it('should apply maxHeight', () => {
it('should apply minWidth', () => {
const { componentInstance } = createComponent(ActionsPanelController);

componentInstance.openFromTemplate({ minWidth: '50%' });
Expand Down Expand Up @@ -398,4 +398,73 @@ describe(KbqActionsPanelModule.name, () => {

expect(getOverlayContainerElement().classList.contains(selector)).toBeTruthy();
});

it('should apply containerClass as array', () => {
const { componentInstance } = createComponent(ActionsPanelController);

componentInstance.openFromTemplate({ containerClass: ['classA', 'classB'] });
expect(getActionsPanelContainerElement().classList.contains('classA')).toBeTruthy();
expect(getActionsPanelContainerElement().classList.contains('classB')).toBeTruthy();
});

it('should apply overlayPanelClass as array', () => {
const { componentInstance } = createComponent(ActionsPanelController);

componentInstance.openFromTemplate({ overlayPanelClass: ['classA', 'classB'] });
expect(getOverlayPaneElement().classList.contains('classA')).toBeTruthy();
expect(getOverlayPaneElement().classList.contains('classB')).toBeTruthy();
});

it('should close previously opened panel when opening a new one', async () => {
const fixture = createComponent(ActionsPanelController);
const { componentInstance } = fixture;

const firstRef = componentInstance.openFromTemplate();

expect(getActionsPanelContainerElement()).toBeInstanceOf(HTMLElement);

const afterClosed = lastValueFrom(firstRef.afterClosed);

componentInstance.openFromTemplate();
await fixture.whenStable();

await expect(afterClosed).resolves.toBeUndefined();
// New panel is now open
expect(getActionsPanelContainerElement()).toBeInstanceOf(HTMLElement);
});

it('should apply kbqActionsPanelDefaultConfigProvider', () => {
const { componentInstance } = createComponent(ActionsPanelController, [
kbqActionsPanelDefaultConfigProvider({ disableClose: true })
]);

componentInstance.openFromTemplate();
expect(getActionsPanelCloseButton()).toBeNull();
});

it('should set maxWidth on overlay element when overlayContainer is provided', () => {
const { componentInstance } = createComponent(ActionsPanelController);
const containerElement = componentInstance.elementRef.nativeElement;

jest.spyOn(containerElement, 'getBoundingClientRect').mockReturnValue({ width: 800 } as DOMRect);

const overlayContainer = { nativeElement: containerElement } as ElementRef<HTMLElement>;

componentInstance.openFromTemplate({ overlayContainer });

expect(getOverlayPaneElement().style.maxWidth).toBe('800px');
});

it('should ignore maxWidth config when overlayContainer is provided', () => {
const { componentInstance } = createComponent(ActionsPanelController);
const containerElement = componentInstance.elementRef.nativeElement;

jest.spyOn(containerElement, 'getBoundingClientRect').mockReturnValue({ width: 800 } as DOMRect);

const overlayContainer = { nativeElement: containerElement } as ElementRef<HTMLElement>;

componentInstance.openFromTemplate({ overlayContainer, maxWidth: '999px' });

expect(getOverlayPaneElement().style.maxWidth).toBe('800px');
});
});
25 changes: 19 additions & 6 deletions packages/components/actions-panel/actions-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,19 +193,32 @@ export class KbqActionsPanel implements OnDestroy {
if (overlayContainer) {
const { afterClosed } = actionsPanelRef;

this.syncOverlayMaxWidth(overlayContainer.nativeElement, overlayRef.overlayElement);
Comment thread
artembelik marked this conversation as resolved.
Comment thread
artembelik marked this conversation as resolved.

this.resizeObserver
.observe(overlayContainer.nativeElement)
.pipe(takeUntil(afterClosed))
.subscribe(() => {
const { width: maxWidth } = overlayContainer.nativeElement.getBoundingClientRect();

if (!maxWidth) return;

overlayRef.overlayElement.style.maxWidth = coerceCssPixelValue(maxWidth);
overlayRef.updatePosition();
if (this.syncOverlayMaxWidth(overlayContainer.nativeElement, overlayRef.overlayElement)) {
overlayRef.updatePosition();
}
});
}

return actionsPanelRef;
}

private syncOverlayMaxWidth(container: HTMLElement, overlayElement: HTMLElement): boolean {
const { width } = container.getBoundingClientRect();

if (!width) return false;

const maxWidth = coerceCssPixelValue(width);

if (overlayElement.style.maxWidth === maxWidth) return false;

overlayElement.style.maxWidth = maxWidth;

return true;
}
Comment thread
Copilot marked this conversation as resolved.
}
33 changes: 33 additions & 0 deletions packages/components/actions-panel/e2e.playwright-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,38 @@ test.describe('KbqActionsPanel', () => {
await e2eEnableDarkTheme(page);
await expect(screenshotTarget).toHaveScreenshot('2-dark.png');
});

test('items overflow on container resize', async ({ page }) => {
await page.goto('/E2eActionsPanelWithOverlayContainer');
const locator = getComponent(page);
const overlayContainer = getOverlayContainer(locator);
const getHiddenCount = () =>
page.evaluate(() => document.querySelectorAll('.kbq-overflow-item-hidden').length);
Comment thread
artembelik marked this conversation as resolved.

await getOpenButton(locator).click();

// Capture baseline hidden count at default container width (400px)
const hiddenCountDefault = await getHiddenCount();

// Widen the container so all items fit
await overlayContainer.evaluate(({ style }) => (style.width = '650px'));
await expect.poll(getHiddenCount).toBeLessThan(hiddenCountDefault);

const hiddenCountWide = await getHiddenCount();

expect(hiddenCountWide).toBeLessThan(hiddenCountDefault);

// Narrow the container so more items overflow
await overlayContainer.evaluate(({ style }) => (style.width = '200px'));
await expect.poll(getHiddenCount).toBeGreaterThan(hiddenCountWide);

const hiddenCountNarrow = await getHiddenCount();

expect(hiddenCountNarrow).toBeGreaterThan(hiddenCountWide);

// Restore to wide: items should reappear
await overlayContainer.evaluate(({ style }) => (style.width = '650px'));
await expect.poll(getHiddenCount).toBe(hiddenCountWide);
});
});
});
1 change: 1 addition & 0 deletions packages/components/breadcrumbs/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export class E2eBreadcrumbsStateAndStyle {
:host {
display: block;
padding: var(--kbq-size-s);
width: 300px;
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
Expand Down
105 changes: 105 additions & 0 deletions packages/components/overflow-items/e2e.playwright-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const blockOnPage = (page: Page, route: string, testid: string) => {
const horizontal = (page: Page, testid: string) => blockOnPage(page, '/E2eOverflowItemsHorizontal', testid);
const vertical = (page: Page, testid: string) => blockOnPage(page, '/E2eOverflowItemsVertical', testid);
const ordered = (page: Page, testid: string) => blockOnPage(page, '/E2eOverflowItemsOrdered', testid);
const additionalTargets = (page: Page, testid: string) =>
blockOnPage(page, '/E2eOverflowItemsAdditionalTargets', testid);
const dynamic = (page: Page, testid: string) => blockOnPage(page, '/E2eOverflowItemsDynamic', testid);

test.describe('KbqOverflowItems', () => {
test('should hide overflown items', async ({ page }) => {
Expand Down Expand Up @@ -287,4 +290,106 @@ test.describe('KbqOverflowItems', () => {
await expect(visibleItems(block)).toHaveCount(2);
await expect(visibleItems(block).first()).toHaveText('Item7');
});

test('should recalculate hidden items on live container resize', async ({ page }) => {
const { navigate, block } = horizontal(page, 'overflowItems_default');
const container = block.locator('.kbq-overflow-items');

await navigate();
await expect(hiddenItems(block)).toHaveCount(12);

await container.evaluate((element) => {
(element as HTMLElement).style.width = '600px';
});
await expect(hiddenItems(block)).toHaveCount(10);

await container.evaluate((element) => {
(element as HTMLElement).style.width = '400px';
});
await expect(hiddenItems(block)).toHaveCount(14);
});

test('should recalculate hidden items on resize when debounceTime is configured', async ({ page }) => {
const { navigate, block } = horizontal(page, 'overflowItems_debounceResize');
const container = block.locator('.kbq-overflow-items');

await navigate();
await expect(hiddenItems(block)).toHaveCount(12);

await container.evaluate((element) => {
(element as HTMLElement).style.width = '600px';
});
await expect(hiddenItems(block)).toHaveCount(10);
});

test('should recalculate on additionalResizeObserverTargets resize', async ({ page }) => {
const { navigate, block } = additionalTargets(page, 'overflowItemsAdditionalTargets_block');
const toggle = page.getByTestId('overflowItemsAdditionalTargets_toggle');

await navigate();
await expect(hiddenItems(block)).toHaveCount(11);

await toggle.click();

await expect(hiddenItems(block)).toHaveCount(14);
});

test('should hide overflown items with left margin', async ({ page }) => {
// hidden = 20 - floor((500 - 100) / (50 + 10)) = 14
const { navigate, block } = horizontal(page, 'overflowItems_marginLeft');

await navigate();

await expect(hiddenItems(block)).toHaveCount(14);
});

test('should display result score (vertical orientation)', async ({ page }) => {
// hidden = 20 - floor((500 - 50) / 50) = 11
const { navigate, block } = vertical(page, 'overflowItemsVertical_default');

await navigate();

await expect(result(block)).toHaveText('and 11 more');
});

test('should prevent hiding item with alwaysVisible when no space is available (vertical orientation)', async ({
page
}) => {
// containerHeight=49 < itemHeight=50, so every item except Item7 (alwaysVisible) is hidden
const { navigate, block } = vertical(page, 'overflowItemsVertical_alwaysVisibleNoSpace');

await navigate();

await expect(visibleItems(block)).toHaveCount(1);
await expect(visibleItems(block).first()).toHaveText('Item7');
});

test('should prevent hiding item with alwaysVisible attribute when reverseOverflowOrder is enabled (vertical orientation)', async ({
page
}) => {
const { navigate, block } = vertical(page, 'overflowItemsVertical_alwaysVisibleReverse');

await navigate();

await expect(visibleItems(block).first()).toHaveText('Item7');
});

test('should recalculate hidden items when items list changes dynamically', async ({ page }) => {
// containerWidth=100, resultWidth=50, itemWidth=50
// 4 items: 4*50=200 > 100 → 3 hidden (1 visible + result)
// 3 items: 3*50=150 > 100 → 2 hidden
// 1 item: 1*50=50 ≤ 100 → 0 hidden
const { navigate, block } = dynamic(page, 'overflowItemsDynamic_block');
const removeButton = page.getByTestId('overflowItemsDynamic_removeItem');

await navigate();
await expect(hiddenItems(block)).toHaveCount(3);

await removeButton.click();
await expect(hiddenItems(block)).toHaveCount(2);

await removeButton.click();
await removeButton.click();
await expect(hiddenItems(block)).toHaveCount(0);
});
});
Loading
Loading