# Overflow items

- Entry point: `@koobiq/components/overflow-items`
- NgModule: `KbqOverflowItemsModule`
- Page: https://koobiq.io/en/components/overflow-items/overview

## Overview

Component for automatically hiding elements with dynamic adaptation to the container width.

Supports two modes of operation:

- Hiding from the end (default).
- Hiding from the start.

Configurable via the `reverseOverflowOrder` attribute.

**Example: Overflow items overview** (`overflow-items-overview`)

```ts overflow-items-overview-example.ts
import { ChangeDetectionStrategy, Component, model } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqBadgeModule } from '@koobiq/components/badge';
import { KbqOverflowItemsModule } from '@koobiq/components/overflow-items';
import { KbqToggleModule } from '@koobiq/components/toggle';

@Component({
    selector: 'overflow-items-overview-example',
    imports: [KbqOverflowItemsModule, KbqBadgeModule, KbqToggleModule, FormsModule],
    template: `
        <kbq-toggle class="layout-margin-bottom-m" [(ngModel)]="reverseOverflowOrder">
            Reverse overflow order
        </kbq-toggle>

        <div #kbqOverflowItems="kbqOverflowItems" kbqOverflowItems [reverseOverflowOrder]="reverseOverflowOrder()">
            @if (reverseOverflowOrder()) {
                <div class="layout-margin-right-xs" kbqOverflowItemsResult>
                    and {{ kbqOverflowItems.hiddenItemIDs().size }} more
                </div>
            }
            @for (item of items; track item) {
                <kbq-badge [kbqOverflowItem]="item" [class.layout-margin-right-xs]="!$last">
                    {{ item }}
                </kbq-badge>
            }
            @if (!reverseOverflowOrder()) {
                <div kbqOverflowItemsResult>and {{ kbqOverflowItems.hiddenItemIDs().size }} more</div>
            }
        </div>
    `,
    styles: `
        :host {
            display: flex;
            flex-direction: column;
            padding-right: var(--kbq-size-3xs);
        }

        .kbq-overflow-items {
            resize: horizontal;
            max-width: 100%;
            min-width: 150px;
            border: 1px solid var(--kbq-line-contrast-less);
            border-radius: var(--kbq-size-border-radius);
            padding: var(--kbq-size-xxs);
        }

        .kbq-overflow-items-result {
            text-wrap: nowrap;
            color: var(--kbq-foreground-contrast-secondary);
            user-select: none;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class OverflowItemsOverviewExample {
    readonly items = Array.from({ length: 20 }, (_, i) => `Item${i}`);
    readonly reverseOverflowOrder = model(false);
}
```

### Order of hiding

The order in which elements are hidden is determined using the `order` attribute for the `KbqOverflowItem` directive.

**Example: Overflow items result offset** (`overflow-items-with-order`)

```ts overflow-items-with-order-example.ts
import { ChangeDetectionStrategy, Component, computed, model } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqBadgeColors, KbqBadgeModule } from '@koobiq/components/badge';
import { KbqOverflowItemsModule } from '@koobiq/components/overflow-items';
import { KbqToggleModule } from '@koobiq/components/toggle';

@Component({
    selector: 'overflow-items-with-order-example',
    imports: [KbqOverflowItemsModule, KbqBadgeModule, KbqToggleModule, FormsModule],
    template: `
        <kbq-toggle class="layout-margin-bottom-m" [(ngModel)]="reverseOverflowOrder">
            Reverse overflow order
        </kbq-toggle>

        <div #kbqOverflowItems="kbqOverflowItems" kbqOverflowItems [reverseOverflowOrder]="reverseOverflowOrder()">
            @for (item of items; track $index) {
                @let isLastHiddenItem = $index === 5;

                <kbq-badge
                    [kbqOverflowItem]="item"
                    [class.layout-margin-right-xs]="!$last"
                    [order]="isLastHiddenItem ? this.lastItemOrder() : $index"
                    [badgeColor]="isLastHiddenItem ? badgeColor.Theme : badgeColor.FadeContrast"
                >
                    {{ item }}
                </kbq-badge>
            }
            <div class="layout-margin-right-xs" kbqOverflowItemsResult>
                and {{ kbqOverflowItems.hiddenItemIDs().size }} more
            </div>
        </div>
    `,
    styles: `
        :host {
            display: flex;
            flex-direction: column;
            padding-right: var(--kbq-size-3xs);
        }

        .kbq-overflow-items {
            resize: horizontal;
            max-width: 100%;
            min-width: 90px;
            min-height: var(--kbq-size-xxl);
            border: 1px solid var(--kbq-line-contrast-less);
            border-radius: var(--kbq-size-border-radius);
            padding: var(--kbq-size-xxs);
        }

        .kbq-overflow-items-result {
            text-wrap: nowrap;
            color: var(--kbq-foreground-contrast-secondary);
            user-select: none;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class OverflowItemsWithOrderExample {
    readonly items = Array.from({ length: 20 }, (_, i) => `Item${i}`);
    readonly badgeColor = KbqBadgeColors;
    readonly reverseOverflowOrder = model(false);
    readonly lastItemOrder = computed(() => (this.reverseOverflowOrder() ? +Infinity : -Infinity));
}
```

### Always visible elements

If you need some elements to always remain visible, you can use the `alwaysVisible` attribute for the `KbqOverflowItem` directive.

**Example: Overflow items with always visible item** (`overflow-items-with-always-visible-item`)

```ts overflow-items-with-always-visible-item-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqBadgeColors, KbqBadgeModule } from '@koobiq/components/badge';
import { KbqOverflowItemsModule } from '@koobiq/components/overflow-items';
import { KbqToggleModule } from '@koobiq/components/toggle';

@Component({
    selector: 'overflow-items-with-always-visible-item-example',
    imports: [KbqOverflowItemsModule, KbqBadgeModule, KbqToggleModule, FormsModule],
    template: `
        <div #kbqOverflowItems="kbqOverflowItems" kbqOverflowItems>
            @for (item of items; track item.id) {
                <kbq-badge
                    [kbqOverflowItem]="item.id"
                    [class.layout-margin-right-xs]="!$last"
                    [alwaysVisible]="item.alwaysVisible"
                    [badgeColor]="item.alwaysVisible ? badgeColor.Theme : badgeColor.FadeContrast"
                >
                    {{ item.id }}
                </kbq-badge>
            }
            <div kbqOverflowItemsResult>and {{ kbqOverflowItems.hiddenItemIDs().size }} more</div>
        </div>
    `,
    styles: `
        :host {
            display: flex;
            flex-direction: column;
            padding-right: var(--kbq-size-3xs);
        }

        .kbq-overflow-items {
            resize: horizontal;
            max-width: 100%;
            min-width: 150px;
            border: 1px solid var(--kbq-line-contrast-less);
            border-radius: var(--kbq-size-border-radius);
            padding: var(--kbq-size-xxs);
        }

        .kbq-overflow-items-result {
            text-wrap: nowrap;
            color: var(--kbq-foreground-contrast-secondary);
            user-select: none;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class OverflowItemsWithAlwaysVisibleItemExample {
    readonly items = Array.from({ length: 20 }, (_, i) => ({
        id: `Item${i}`,
        alwaysVisible: i === 3
    }));
    readonly badgeColor = KbqBadgeColors;
}
```

### Element alignment

Element alignment within the container is set using the CSS property [`justify-content`](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content).

**Example: Overflow items justify content** (`overflow-items-justify-content`)

```ts overflow-items-justify-content-example.ts
import { ChangeDetectionStrategy, Component, model } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqBadgeModule } from '@koobiq/components/badge';
import { KbqOverflowItemsModule } from '@koobiq/components/overflow-items';
import { KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'overflow-items-justify-content-example',
    imports: [KbqOverflowItemsModule, KbqBadgeModule, KbqSelectModule, FormsModule],
    template: `
        <kbq-form-field>
            <kbq-select [(ngModel)]="justifyContent">
                @for (option of justifyContentOptions; track option) {
                    <kbq-option [value]="option">{{ option }}</kbq-option>
                }
            </kbq-select>
        </kbq-form-field>

        <div #kbqOverflowItems="kbqOverflowItems" kbqOverflowItems [style.justify-content]="justifyContent()">
            @for (item of items; track item) {
                <kbq-badge [kbqOverflowItem]="item" [class.layout-margin-right-xs]="!$last">
                    {{ item }}
                </kbq-badge>
            }
            <div class="layout-margin-right-xs" kbqOverflowItemsResult>
                and {{ kbqOverflowItems.hiddenItemIDs().size }} more
            </div>
        </div>
    `,
    styles: `
        :host {
            display: flex;
            flex-direction: column;
            padding-right: var(--kbq-size-3xs);
        }

        .kbq-overflow-items {
            resize: horizontal;
            max-width: 100%;
            min-width: 150px;
            border: 1px solid var(--kbq-line-contrast-less);
            border-radius: var(--kbq-size-border-radius);
            padding: var(--kbq-size-xxs);
        }

        .kbq-overflow-items-result {
            text-wrap: nowrap;
            color: var(--kbq-foreground-contrast-secondary);
            user-select: none;
        }

        .kbq-form-field {
            margin-bottom: var(--kbq-size-m);
            max-width: 200px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class OverflowItemsJustifyContentExample {
    readonly items = Array.from({ length: 7 }, (_, i) => `Item${i}`);
    readonly justifyContentOptions = ['center', 'start', 'end', 'space-between', 'space-around'] as const;
    readonly justifyContent = model(this.justifyContentOptions[0]);
}
```

### Orientation

By default, elements are hidden horizontally. If you need to hide elements vertically, you can use the `orientation="vertical"` attribute.

**Example: Overflow items with vertical orientation** (`overflow-items-with-vertical-orientation`)

```ts overflow-items-with-vertical-orientation-example.ts
import { ChangeDetectionStrategy, Component, model } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqBadgeColors, KbqBadgeModule } from '@koobiq/components/badge';
import { KbqOverflowItemsModule } from '@koobiq/components/overflow-items';
import { KbqToggleModule } from '@koobiq/components/toggle';

@Component({
    selector: 'overflow-items-with-vertical-orientation-example',
    imports: [KbqOverflowItemsModule, KbqBadgeModule, KbqToggleModule, FormsModule],
    template: `
        <kbq-toggle class="layout-margin-bottom-m" [(ngModel)]="reverseOverflowOrder">
            Reverse overflow order
        </kbq-toggle>

        <div class="example-resize-container">
            <div
                #kbqOverflowItems="kbqOverflowItems"
                kbqOverflowItems
                orientation="vertical"
                [reverseOverflowOrder]="reverseOverflowOrder()"
            >
                @if (reverseOverflowOrder()) {
                    <div kbqOverflowItemsResult class="layout-margin-bottom-xs">
                        and {{ kbqOverflowItems.hiddenItemIDs().size }} more
                    </div>
                }
                @for (item of items; track item.id) {
                    <kbq-badge
                        [class.layout-margin-bottom-xs]="!$last"
                        [kbqOverflowItem]="item.id"
                        [alwaysVisible]="item.alwaysVisible"
                        [badgeColor]="item.alwaysVisible ? badgeColor.Theme : badgeColor.FadeContrast"
                    >
                        {{ item.id }}
                    </kbq-badge>
                }
                @if (!reverseOverflowOrder()) {
                    <div kbqOverflowItemsResult>and {{ kbqOverflowItems.hiddenItemIDs().size }} more</div>
                }
            </div>
        </div>
    `,
    styles: `
        :host {
            display: flex;
            flex-direction: column;
            align-items: center;
            height: 400px;
        }

        .example-resize-container {
            display: flex;
            overflow: hidden;
            resize: vertical;
            height: 100%;
            max-height: 100%;
            min-height: 60px;
            border: 1px solid var(--kbq-line-contrast-less);
            border-radius: var(--kbq-size-border-radius);
            padding: var(--kbq-size-xxs);
            width: 100px;
        }

        .kbq-overflow-items-result {
            text-wrap: nowrap;
            text-align: center;
            color: var(--kbq-foreground-contrast-secondary);
            user-select: none;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class OverflowItemsWithVerticalOrientationExample {
    readonly items = Array.from({ length: 20 }, (_, i) => ({
        id: `Item${i}`,
        alwaysVisible: i === 8
    }));
    readonly reverseOverflowOrder = model(false);
    readonly badgeColor = KbqBadgeColors;
}
```

### Delay when hiding/showing

The delay for hiding/showing elements is set using the `debounceTime` attribute. Enabling this option positively
affects performance when there are many elements or when the container size changes frequently.

```html
<div kbqOverflowItems [debounceTime]="300">...</div>
```

### Additional target for ResizeObserver

Sometimes the container size can change not only when the browser window is resized, but also when other elements on the page change size (for example: [Sidebar](https://koobiq.io/en/components/sidebar.md)). To track such changes, you can use the `additionalResizeObserverTargets` attribute.
The attribute accepts either a single element or an array of elements whose size changes will trigger recalculation of hidden elements. By default, `document.body` is tracked.

```html
<div kbqOverflowItems [additionalResizeObserverTargets]="sidebarContainerElement">...</div>
```

## API

### Directives

#### KbqOverflowItem (directive)

Directive for the item that can be hidden by the `KbqOverflowItems` directive.

```ts
@Directive({ selector: '[kbqOverflowItem]', exportAs: 'kbqOverflowItem' })
class KbqOverflowItem extends ElementVisibilityManager {
    readonly id = input.required<unknown>({ alias: 'kbqOverflowItem' });
    readonly order = input<number | null>(null);
    readonly alwaysVisible = input<boolean>(false);
}
```

- `[kbqOverflowItem]`: `unknown` — Unique identifier for the item.
- `[order]`: `number | null` — Defines the order in which the item is processed in the overflow container,
  without changing its original position in the `QueryList`.
- `[alwaysVisible]`: `boolean` — Element with this attribute will be ignored when hiding and will always remain visible. Defaults to `false`.

#### KbqOverflowItems (directive)

Directive for managing the visibility of items that overflow the container.

```ts
@Directive({ selector: '[kbqOverflowItems]', exportAs: 'kbqOverflowItems' })
class KbqOverflowItems {
    readonly reverseOverflowOrder = input<boolean>(false);
    readonly debounceTime = input<number>(0);
    readonly additionalResizeObserverTargets = input<Element | Element[]>(
        this.document.body
    );
    readonly orientation = input<KbqOrientation>('horizontal');
    readonly wrap = input<KbqFlexWrap>('nowrap');
    readonly changes = output<ReadonlySet<unknown>>();
    readonly hiddenItemIDs: Signal<ReadonlySet<unknown>>;
}
```

- `[reverseOverflowOrder]`: `boolean` — Whether the overflow order should be reversed. Defaults to `false`.

  ```text
  [0, 1, hidden, hidden] ---> [hidden, hidden, 2, 3]
  ```
- `[debounceTime]`: `number` — Debounce time for recalculating items visibility. Defaults to `0`.
- `[additionalResizeObserverTargets]`: `Element | Element[]` — List of additional elements to observe for resize changes. Defaults to `document.body`.
- `[orientation]`: `KbqOrientation` — Defines the orientation of the overflow items. Defaults to `'horizontal'`.
- `[wrap]`: `KbqFlexWrap` — Defines the wrap of the overflow items. Defaults to `'nowrap'`.
- `(changes)`: `ReadonlySet<unknown>` — Emits when the set of hidden items changes.
- `hiddenItemIDs`: `Signal<ReadonlySet<unknown>>` — Set of hidden item IDs.

#### KbqOverflowItemsResult (directive)

Directive for displaying the result of hidden items by the `KbqOverflowItems` directive.

```ts
@Directive({
    selector: '[kbqOverflowItemsResult]',
    exportAs: 'kbqOverflowItemsResult'
})
class KbqOverflowItemsResult extends ElementVisibilityManager {}
```

## Examples

### List with height limit

Items wrap across multiple lines and are hidden once they exceed the container height,
unlike the default nowrap mode where items overflow along a single line.

**Example: Overflow items as clamped list** (`overflow-items-as-clamped-list`)

```ts overflow-items-as-clamped-list-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqOverflowItem, KbqOverflowItems, KbqOverflowItemsResult } from '@koobiq/components/overflow-items';
import { KbqTooltipTrigger } from '@koobiq/components/tooltip';

@Component({
    selector: 'overflow-items-as-clamped-list-example',
    imports: [
        KbqOverflowItems,
        KbqOverflowItem,
        KbqOverflowItemsResult,
        KbqTooltipTrigger
    ],
    template: `
        <div
            #kbqOverflowItems="kbqOverflowItems"
            kbqOverflowItems
            style="max-height: 48px; min-width: 60px"
            [wrap]="'wrap'"
        >
            @for (item of countries; track item) {
                <span [kbqOverflowItem]="item" [class.layout-margin-right-xs]="!$last">
                    <span>
                        {{ item }}
                    </span>
                    <span>,</span>
                </span>
            }
            <span kbqOverflowItemsResult [kbqTooltip]="template" [kbqTooltipArrow]="false">
                and {{ kbqOverflowItems.hiddenItemIDs().size }}
            </span>

            <ng-template #template>
                @for (item of kbqOverflowItems.hiddenItemIDs().values(); track item) {
                    <span>
                        <span>{{ item }}</span>
                        @if (!$last) {
                            <span>,&nbsp;</span>
                        }
                    </span>
                }
            </ng-template>
        </div>
    `,
    styles: `
        :host {
            display: flex;
            flex-direction: column;
            padding-right: var(--kbq-size-3xs);
        }

        .kbq-overflow-item {
            display: inline-flex;
        }

        .kbq-overflow-items {
            resize: horizontal;
            max-width: 100%;
            min-width: 150px;
            border: 1px solid var(--kbq-line-contrast-less);
            border-radius: var(--kbq-size-border-radius);
            padding: var(--kbq-size-xxs);
        }

        .kbq-overflow-items-result {
            text-wrap: nowrap;
            text-align: center;
            color: var(--kbq-foreground-contrast-secondary);
            user-select: none;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class OverflowItemsAsClampedListExample {
    protected countries = [
        'Australia',
        'Austria',
        'Argentina',
        'Belgium',
        'Brazil',
        'United Kingdom',
        'Germany',
        'Greece',
        'Denmark',
        'Egypt',
        'India',
        'Spain',
        'Italy',
        'Canada',
        'Mexico',
        'Netherlands',
        'Norway',
        'Poland',
        'Portugal',
        'Russia',
        'United States',
        'Thailand',
        'Turkey',
        'France',
        'Japan'
    ];
}
```
