Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 69 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,71 @@ 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);

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

expect(firstRef.afterClosed).toBeDefined();
// New panel is now open
expect(getActionsPanelContainerElement()).toBeInstanceOf(HTMLElement);
Comment thread
artembelik marked this conversation as resolved.
});

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');
});
});
21 changes: 15 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,28 @@ export class KbqActionsPanel implements OnDestroy {
if (overlayContainer) {
const { afterClosed } = actionsPanelRef;

this.syncOverlayMaxWidth(overlayContainer.nativeElement, overlayRef.overlayElement);
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;

overlayElement.style.maxWidth = coerceCssPixelValue(width);

return true;
}
}
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: 400px;
}
`,
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