# Modal

- Entry point: `@koobiq/components/modal`
- NgModule: `KbqModalModule`
- Page: https://koobiq.io/en/components/modal/overview

## Overview

A modal dialog opens as a window on top of the page, dims the underlying layer, and blocks all interaction with it. The modal dialog focuses the user's attention on its content.

**Example: Modal component** (`modal-component`)

```ts modal-component-example.ts
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqOptgroup } from '@koobiq/components/core';
import { KbqDivider } from '@koobiq/components/divider';
import { KbqDropdownModule } from '@koobiq/components/dropdown';
import { KbqIcon } from '@koobiq/components/icon';
import { KBQ_MODAL_DATA, KbqModalModule, KbqModalRef, KbqModalService } from '@koobiq/components/modal';
import { KbqTitleDirective } from '@koobiq/components/title';

interface CustomModalData {
    title: string;
    subtitle: string;
}

@Component({
    selector: 'custom-modal',
    imports: [
        KbqModalModule,
        KbqButtonModule,
        KbqDivider,
        KbqDropdownModule,
        KbqIcon,
        KbqOptgroup,
        KbqTitleDirective
    ],
    template: `
        <kbq-modal-title>{{ data.title }}</kbq-modal-title>

        <kbq-modal-body>
            <div class="layout-margin-bottom-l">{{ data.subtitle }}</div>
            <button kbq-button [kbqDropdownTriggerFor]="appDropdown">
                dropdown
                <i kbq-icon="kbq-chevron-down-s_16"></i>
            </button>

            <kbq-dropdown #appDropdown="kbqDropdown">
                <button kbq-dropdown-item>Text Normal</button>

                <button kbq-dropdown-item>
                    <i kbq-icon="kbq-circle-info_16" [color]="'contrast'"></i>
                    Text Normal
                </button>

                <button kbq-dropdown-item>
                    Text Normal
                    <div kbqDropdownItemCaption>Caption</div>
                </button>

                <button kbq-dropdown-item>
                    <i kbq-icon="kbq-circle-info_16" [color]="'contrast'"></i>
                    Text Normal
                    <div kbqDropdownItemCaption>Caption</div>
                </button>

                <button kbq-dropdown-item>
                    <i kbq-icon="kbq-circle-xs_16" [color]="'contrast'"></i>
                    Text Normal
                </button>

                <button kbq-dropdown-item kbq-title>
                    Text Normal and very long text Text Normal and very long text Text Normal and very long text Text
                    Normal and very long text
                </button>

                <kbq-divider />

                <div kbqDropdownGroupHeader>Header</div>

                <button disabled kbq-dropdown-item>Disabled</button>

                <kbq-divider />

                <kbq-optgroup label="Subheading" />

                <button disabled kbq-dropdown-item>
                    <i kbq-icon="kbq-circle-xs_16" [color]="'contrast'"></i>
                    Disabled with icon
                </button>

                <kbq-divider />

                <button kbq-dropdown-item>Point 2</button>

                <button kbq-dropdown-item>Point 3</button>

                <button kbq-dropdown-item>Link</button>
            </kbq-dropdown>
        </kbq-modal-body>

        <div kbq-modal-footer>
            <button kbq-button [color]="'contrast'" (click)="destroyModal('save')">Save</button>
            <button kbq-button autofocus (click)="destroyModal('close')">Close</button>
        </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomModalComponent {
    private readonly modalRef = inject(KbqModalRef);

    readonly data = inject<CustomModalData>(KBQ_MODAL_DATA);

    destroyModal(action: 'save' | 'close') {
        this.modalRef.destroy(action);
    }
}

@Component({
    selector: 'modal-component-example',
    imports: [KbqButtonModule],
    template: `
        <button kbq-button (click)="openModal()">Open Modal</button>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ModalComponentExample {
    private readonly modalService = inject(KbqModalService);

    modalRef: KbqModalRef<CustomModalComponent, 'save' | 'close'>;

    openModal(): void {
        this.modalRef = this.modalService.open<CustomModalComponent, 'save' | 'close'>({
            kbqComponent: CustomModalComponent,
            data: {
                title: 'DoS attack',
                subtitle:
                    'In computing, a denial-of-service attack (DoS attack) is a cyber-attack in which the perpetrator seeks to make a machine or network resource unavailable to its intended users by temporarily or indefinitely disrupting services of a host connected to a network.'
            } satisfies CustomModalData
        });

        this.modalRef.afterOpen.subscribe(() => {
            console.log('[afterOpen] emitted!');
        });

        this.modalRef.afterClose.subscribe((action) => {
            console.log(`[afterClose] emitted, chosen action: ${action}`);
        });
    }
}
```

### When to use

Use modal dialogs for short-term tasks and user-triggered operations (you can open a short form, settings). It can be convenient to show hidden content in a modal window. Modals help accomplish tasks efficiently without losing context of the main page. Despite their versatility, modal windows are [not suitable for all situations](https://koobiq.io/en/components/modal.md#related-components).

### Component structure

- Header. Dialog title
- Close action
- Body. Contains message text, a question, controls, and input fields
- Footer. Action buttons, option to cancel the operation or close the dialog

Many of these parts may be absent. The body and the ability to close the dialog must remain. The required way to close the dialog is the × (cross) in the header or the "Cancel" ("Close") button in the footer.

#### Simple dialog without a header

Resembles a browser alert. Use when the system asks a single question or delivers a short message, to avoid redundant text duplication in the title and body of the modal window. Such dialogs do not have a close × icon in the top right corner.

There is no title to name the dialog for a screen reader here, so pass `kbqAriaLabel` — otherwise the dialog is announced unnamed.

**Example: Modal** (`modal-overview`)

```ts modal-overview-example.ts
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqModalService, ModalSize } from '@koobiq/components/modal';

@Component({
    selector: 'modal-overview-example',
    imports: [KbqButtonModule],
    template: `
        <button kbq-button (click)="showConfirmModal()">Open Confirm Modal</button>
        <button kbq-button (click)="showSuccessModal()">Open Success Modal</button>
        <button kbq-button (click)="showDeleteModal()">Open Delete Modal</button>
    `,
    styles: `
        :host {
            display: flex;
            flex-direction: column;
            width: 200px;
        }

        button:not(:last-child) {
            margin-bottom: var(--kbq-size-m);
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ModalOverviewExample {
    private readonly modalService = inject(KbqModalService);

    showConfirmModal(): void {
        this.modalService.confirm({
            kbqAriaLabel: 'Save changes',
            kbqSize: ModalSize.Small,
            kbqMaskClosable: true,
            kbqContent: 'Save changes?',
            kbqOkText: 'Save',
            kbqCancelText: 'Cancel',
            kbqOkClick: () => console.log('Save'),
            kbqCancelClick: () => console.log('Cancel')
        });
    }

    showSuccessModal(): void {
        this.modalService.success({
            kbqAriaLabel: 'Changes saved',
            kbqSize: ModalSize.Small,
            kbqMaskClosable: true,
            kbqContent: 'All changes are saved!',
            kbqOkText: 'ОК',
            kbqCancelText: 'Cancel',
            kbqOkClick: () => console.log('OK'),
            kbqCancelClick: () => console.log('Cancel')
        });
    }

    showDeleteModal(): void {
        this.modalService.delete({
            kbqAriaLabel: 'Delete customer',
            kbqContent:
                'The tasks, policies and tags associated with the customer will be deleted too. Delete selected customer?',
            kbqOkText: 'Delete',
            kbqCancelText: 'Cancel',
            kbqWidth: '480px',
            kbqMaskClosable: true,
            kbqOkClick: () => console.log('Delete'),
            kbqCancelClick: () => console.log('Cancel')
        });
    }
}
```

#### Dialog with a title and caption

Use a caption to provide additional context below the dialog title — for example, a brief description of the action or its scope. The caption is limited to two lines and truncates with an ellipsis if the text is longer.

**Example: Modal with caption** (`modal-caption`)

```ts modal-caption-example.ts
import { ChangeDetectionStrategy, Component, inject, TemplateRef } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqModalService, ModalSize } from '@koobiq/components/modal';

@Component({
    selector: 'modal-caption-example',
    imports: [KbqButtonModule],
    template: `
        <button kbq-button (click)="openWithStringCaption()">String caption</button>
        <button kbq-button (click)="openWithTemplateCaption(captionTpl)">Template caption</button>

        <ng-template #captionTpl>
            <span>
                Custom
                <strong>template</strong>
                caption caption caption caption caption caption caption caption caption caption
            </span>
        </ng-template>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
    host: {
        class: 'layout-row layout-gap-m'
    }
})
export class ModalCaptionExample {
    private readonly modalService = inject(KbqModalService);

    openWithStringCaption(): void {
        this.modalService.create({
            kbqTitle: 'Dialog title',
            kbqCaption: 'Additional context or description for the dialog',
            kbqContent: 'Modal body content',
            kbqOkText: 'Ok',
            kbqCancelText: 'Cancel'
        });
    }

    openWithTemplateCaption(kbqCaption: TemplateRef<object>): void {
        this.modalService.create({
            kbqTitle: 'Dialog title Dialog title Dialog title Dialog title g title Dialog titl g title Dialog titl',
            kbqCaption,
            kbqContent: 'Modal body content',
            kbqOkText: 'Ok',
            kbqCancelText: 'Cancel',
            kbqSize: ModalSize.Small
        });
    }
}
```

### How it works

The page is dimmed and cannot be interacted with: buttons cannot be clicked, there is no hover, scrolling, etc. On top of the dim, the modal dialog window opens.

A dialog may open from a user action. The system can also block the user with a modal dialog, but such abrupt interruption should be avoided whenever possible.

#### How to close the dialog

- Performing the primary or secondary dialog actions (buttons in the footer)
- The "Cancel" or "Close" button in the footer
- The × in the header — only when the dialog heading is unavoidable
- Clicking outside the modal window. By default, modal windows do not close this way. However, closing by clicking outside the window can be useful when there is no risk of data loss and the system is not awaiting user input (for example, when there are no terminal buttons in the window).
- The Esc key (equivalent to the "Cancel" button)

The dialog closes on a left mouse button click only: the right and middle buttons open the browser's context menu or trigger its own actions and leave the window alone. The [side panel](https://koobiq.io/en/components/sidepanel.md) behaves the same way.

All of these close the dialog the same way whether it was opened with `KbqModalService` or declared as `<kbq-modal [(kbqVisible)]>`, and each runs the OK or the Cancel flow first.

Each flow has an input and an output, and you bind one or the other:

| Member                              | What it does                                                                                          |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `[kbqOkClick]` / `[kbqCancelClick]` | Decides. Returning `false` — or a promise of `false` — keeps the dialog open; anything else closes it |
| `(kbqOnOk)` / `(kbqOnCancel)`       | Notifies. The dialog closes either way                                                                |

Binding both on the same dialog does not combine them: the handler decides the close, so the event is never emitted. Development mode reports it.

Do not close a modal window automatically by timeout, as the user may not have had time to read the information inside.

Sometimes the system cannot allow the user to return to interacting with the page. Even in this situation, the modal window should not be a dead end in the use case — action options must be provided. For example, if the system shows a window about an expired session, returning to the page in the background is not possible — but a link to the login page should be offered.

#### Remembering user data

If any data has been entered in the window, it must be remembered and shown again when the dialog is reopened. For example, when a create form was being filled out. If an edit form was opened in a modal, changes were started but not saved, and the modal was closed, then unsaved data should not be pre-filled when reopening.

#### Scrolling

The page background does not scroll. The height of the modal window is determined by its content. The window stretches in height and occupies the full available screen height, minus 48px padding on each side.

Only the central part (body) of the dialog scrolls. The header and footer remain visible. They cast a shadow. The scrollbar appears inside the modal content block. It should be flush with the right edge of the window, without any offset.

A long modal is a clear sign that a [different component or a separate page](https://koobiq.io/en/components/modal.md#related-components) should be used.

**Example: Modal scroll** (`modal-scroll`)

```ts modal-scroll-example.ts
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqModalService, ModalSize } from '@koobiq/components/modal';

@Component({
    selector: 'custom-modal',
    template: `
        @for (item of items; track item) {
            <p>{{ item }}</p>
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomModalComponent {
    items = Array.from({ length: 1000 }).map((_, i) => `Item #${i}`);
}

@Component({
    selector: 'modal-scroll-example',
    imports: [KbqButtonModule],
    template: `
        <button kbq-button (click)="createLongModal()">Open Modal</button>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ModalScrollExample {
    private readonly modalService = inject(KbqModalService);

    createLongModal(): void {
        this.modalService.create({
            kbqSize: ModalSize.Small,
            kbqTitle: 'Modal Title',
            kbqContent: CustomModalComponent,
            kbqOkText: 'Yes',
            kbqCancelText: 'No',
            kbqOkClick: () => console.log('Yes'),
            kbqCancelClick: () => console.log('No')
        });
    }
}
```

#### Focus and keyboard navigation

When a modal dialog is open, focus moves only through elements inside the window, including the × button and the footer buttons. Focus does not leave the modal.

##### Focus when opening a modal dialog

When a dialog opens, focus moves to an element inside the dialog. The focus placement depends on the nature and size of the content.

- Focus is placed on the first tabbable element of the dialog, unless otherwise specified. For a dialog with a header that is the × button, so mark the element you want instead with `autofocus` or `cdkFocusInitial` — either wins over the default
- If the dialog contains a form, focus should be placed in the first input field
- If the dialog window contains an important irreversible action, it is better to place focus on a different, less destructive element
- If the dialog contains only terminal buttons, it is useful to place focus on the button the user is most likely to press ("OK" or "Cancel", depending on the situation)
- If the modal window contains a lot of content with scrolling, placing focus on the first available element may automatically scroll the window content and hide the beginning. Set `kbqAutoFocus: 'dialog'` for that case: focus lands on the dialog itself, which is reachable by keyboard but does not scroll the body. `'first-heading'` focuses the title instead, and `false` leaves focus alone entirely — the caller then has to move it into the dialog

A dialog with no focusable control at all still takes focus off the trigger behind it: focus goes to the dialog element.

##### Focus after closing the modal window

When the dialog is closed, focus returns by default to the element that triggered it.

If the trigger element no longer exists, focus returns to another element appropriate to the interaction logic.

In some situations, focus may intentionally return to a different element than the one that originally opened the dialog:

- It is unlikely that the user will want to immediately open the same dialog again
- The task performed in the dialog is directly related to the next step in the workflow. For example, a table has a toolbar with an "Add rows" button. The button opens a dialog asking for the number of new rows. After the dialog is closed, focus is placed in the first cell of the first new row.

##### Keyboard navigation

| Key      | Action                                                                                                                                                               |
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Ctrl + ↵ | Performs the primary dialog action                                                                                                                                   |
| Esc      | Close the dialog, exactly as the Cancel button does — the `kbqCancelClick` handler runs first and can veto the close. Set `kbqCloseByESC: false` to turn the key off |

### Validation

A form may be placed in the window body. Terminal submit buttons must be located in the window footer. [Validation follows general rules](https://koobiq.io/en/other/validation.md).

### Design and animation

#### Position

The window opens centered on screen with a 48px offset from the top edge. If the page was scrolled, there should be no horizontal shift when the dialog opens (this often occurs when the scrollbar appears and disappears when opening or closing a modal).

#### Size

There is no horizontal scroll; content in the panel must fit within the width.

| Preset | Width         |
| ------ | ------------- |
| Small  | 400           |
| Medium | 640 (default) |
| Large  | 960           |

**Example: Modal sizes** (`modal-sizes`)

```ts modal-sizes-example.ts
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqModalService, ModalSize } from '@koobiq/components/modal';

@Component({
    selector: 'modal-sizes-example',
    imports: [KbqButtonModule],
    template: `
        <button kbq-button (click)="showModal(modalSize.Small)">Small</button>
        <button kbq-button (click)="showModal(modalSize.Medium)">Medium</button>
        <button kbq-button (click)="showModal(modalSize.Large)">Large</button>
        <button kbq-button (click)="showCustomModal()">Custom width</button>
    `,
    styles: `
        :host {
            display: flex;
            flex-direction: column;
            width: 200px;
        }

        button:not(:last-child) {
            margin-bottom: var(--kbq-size-m);
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ModalSizesExample {
    private readonly modalService = inject(KbqModalService);

    readonly modalSize = ModalSize;

    showModal(kbqSize: ModalSize): void {
        this.modalService.confirm({
            kbqAriaLabel: 'Save changes',
            kbqSize,
            kbqContent: 'Save changes?',
            kbqOkText: 'Save',
            kbqCancelText: 'Cancel',
            kbqOkClick: () => console.log('Save'),
            kbqCancelClick: () => console.log('Cancel')
        });
    }

    showCustomModal(): void {
        this.modalService.confirm({
            kbqAriaLabel: 'Save changes',
            kbqWidth: '600px',
            kbqContent: 'Save changes?',
            kbqOkText: 'Save',
            kbqCancelText: 'Cancel',
            kbqOkClick: () => console.log('Save'),
            kbqCancelClick: () => console.log('Cancel')
        });
    }
}
```

#### Button panel

Terminal buttons are located in the lower right corner of the footer. Order: primary action, secondary action, cancel. Additional action buttons are located in the lower left corner.

**Example: Modal template** (`modal-template`)

```ts modal-template-example.ts
import { ChangeDetectionStrategy, Component, inject, TemplateRef } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqDropdownModule } from '@koobiq/components/dropdown';
import { KbqIcon } from '@koobiq/components/icon';
import { KbqModalRef, KbqModalService, ModalSize } from '@koobiq/components/modal';
import { KbqSplitButton } from '@koobiq/components/split-button';

@Component({
    selector: 'modal-template-example',
    imports: [
        KbqButtonModule,
        KbqSplitButton,
        KbqIcon,
        KbqDropdownModule
    ],
    template: `
        <button kbq-button (click)="createModal(title, content, footer, sizes.Small)">Open Modal</button>

        <ng-template #title>DoS attack</ng-template>

        <ng-template #content>
            In computing, a denial-of-service attack (DoS attack) is a cyber-attack in which the perpetrator seeks to
            make a machine or network resource unavailable to its intended users by temporarily or indefinitely
            disrupting services of a host connected to a network.

            <kbq-split-button class="layout-margin-top-m">
                <button kbq-button (click)="createModal(title, content, footer)">Open next modal</button>
                <button kbq-button aria-label="More options" [kbqDropdownTriggerFor]="dropdown">
                    <i kbq-icon="kbq-chevron-down-s_16"></i>
                </button>
            </kbq-split-button>

            <kbq-dropdown #dropdown="kbqDropdown">
                <button kbq-dropdown-item (click)="createModal(title, content, footer, sizes.Small)">
                    Open small modal
                </button>
                <button kbq-dropdown-item (click)="createModal(title, content, footer, sizes.Medium)">
                    Open medium modal
                </button>
                <button kbq-dropdown-item (click)="createModal(title, content, footer, sizes.Large)">
                    Open large modal
                </button>
            </kbq-dropdown>
        </ng-template>

        <ng-template #footer>
            <div class="layout-row flex-grow layout-align-space-between">
                <button kbq-button (click)="destroyModal()">Add. action</button>

                <div>
                    <button
                        class="layout-margin-right-m"
                        kbq-button
                        kbq-modal-main-action
                        [color]="'contrast'"
                        (click)="destroyModal()"
                    >
                        Save
                    </button>
                    <button kbq-button (click)="destroyModal()">Cancel</button>
                </div>
            </div>
        </ng-template>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ModalTemplateExample {
    private readonly modalService = inject(KbqModalService);

    modalRef: KbqModalRef;
    sizes = ModalSize;

    createModal(
        kbqTitle: TemplateRef<object>,
        kbqContent: TemplateRef<object>,
        kbqFooter: TemplateRef<object>,
        size?: ModalSize
    ): void {
        this.modalRef = this.modalService.create({
            kbqTitle,
            kbqContent,
            kbqFooter,
            kbqMaskClosable: true,
            kbqSize: size,
            kbqOkClick: () => console.log('OK')
        });
    }

    destroyModal(): void {
        this.modalRef.triggerOk();
    }
}
```

#### Overlapping modal windows

Opening one modal dialog from another is incorrect. But if it happens (as an economy measure or forced workaround), only one dim overlay should remain. The page background and the parent modal sit beneath it. Multiple dim overlays is a mistake.

**Example: Modal multiple** (`modal-multiple`)

```ts modal-multiple-example.ts
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqModalService, ModalSize } from '@koobiq/components/modal';

@Component({
    selector: 'modal-multiple-example',
    imports: [KbqButtonModule],
    template: `
        <button kbq-button (click)="showConfirmModal()">Open two modals</button>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ModalMultipleExample {
    private readonly modalService = inject(KbqModalService);

    showConfirmModal(): void {
        this.modalService.confirm({
            kbqAriaLabel: 'Save changes',
            kbqSize: ModalSize.Medium,
            kbqBodyStyle: { height: '120px' },
            kbqMaskClosable: true,
            kbqContent: 'Save changes?',
            kbqOkText: 'Save',
            kbqCancelText: 'Cancel',
            kbqOkClick: () => console.log('Save'),
            kbqCancelClick: () => console.log('Cancel')
        });

        this.showSuccessModal();
    }

    showSuccessModal(): void {
        this.modalService.success({
            kbqAriaLabel: 'Changes saved',
            kbqSize: ModalSize.Small,
            kbqMaskClosable: true,
            kbqContent: 'All changes are saved!',
            kbqOkText: 'ОК',
            kbqCancelText: 'Cancel',
            kbqOkClick: () => console.log('OK'),
            kbqCancelClick: () => console.log('Cancel')
        });
    }
}
```

#### Fully custom content

When a modal is opened via `KbqModalService.open` with a `kbqComponent`, use the `kbq-modal-title`, `kbq-modal-body`, and `kbq-modal-footer` directives to compose the dialog. To add a caption, place `kbq-modal-caption` inside `kbq-modal-title` — it is rendered in the dialog header, below the title. These directives keep the standard layout and scroll shadows, so a long body still casts a shadow onto the header and footer, exactly as in a modal created via `KbqModalService.create`.

The title and the caption also name and describe the dialog itself, through `aria-labelledby` and `aria-describedby`, so a composed dialog is announced the same way a dialog created via `KbqModalService.create` is.

For a control that belongs next to the heading rather than in the footer — a settings button, a counter, a link to the documentation — mark it `kbqModalTitleActions` and place it inside `kbq-modal-title`. It is rendered beside the heading, outside the two-line clamp the title and caption are subject to, so a long title does not push it out of view:

```html
<kbq-modal-title>
    Title
    <button kbq-button kbqModalTitleActions>…</button>
</kbq-modal-title>
```

**Example: Modal full custom** (`modal-full-custom`)

```ts modal-full-custom-example.ts
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqModalModule, KbqModalRef, KbqModalService } from '@koobiq/components/modal';

@Component({
    selector: 'custom-modal',
    imports: [
        KbqModalModule,
        KbqButtonModule
    ],
    template: `
        <kbq-modal-title>
            Fully custom modal
            <kbq-modal-caption>Additional context or description for the dialog</kbq-modal-caption>
        </kbq-modal-title>

        <kbq-modal-body>
            @for (item of items; track item) {
                <p>{{ item }}</p>
            }
        </kbq-modal-body>

        <div kbq-modal-footer>
            <button kbq-button [color]="'contrast'" (click)="modalRef.close()">Save</button>
            <button kbq-button autofocus (click)="modalRef.close()">Close</button>
        </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomModalComponent {
    protected readonly modalRef = inject(KbqModalRef);

    items = Array.from({ length: 30 }).map((_, i) => `Item #${i}`);
}

@Component({
    selector: 'modal-full-custom-example',
    imports: [KbqButtonModule],
    template: `
        <button kbq-button (click)="openModal()">Open Modal</button>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ModalFullCustomExample {
    private readonly modalService = inject(KbqModalService);

    openModal(): void {
        this.modalService.open({
            kbqComponent: CustomModalComponent
        });
    }
}
```

### Related components

A modal dialog abruptly concentrates attention on itself, causes the user to lose focus, and prevents interaction with the page beneath it. Therefore, in most cases you should try to avoid modality. These approaches will help you treat the user considerately and avoid modal window stacking:

A [Popover](https://koobiq.io/en/components/popover.md) captures the user's attention less disruptively. It does not interfere with interacting with the rest of the page and does not limit freedom. A popover is easier to accidentally close. If the system is waiting for an important response, or it is unclear what will happen if the popover is closed, use the trusty modal dialog.

A [Side panel](https://koobiq.io/en/components/sidepanel.md) is something between a new page and a modal window. It stretches the full height of the browser window; long and narrow pages look better in it than in a modal window, while wide and low pages look worse. Side panels stack on each other well, while modal windows should not be used with such stacking.

**Inline messages and inline forms** help show new content on the same layer of the page and do so less intrusively. This technique is actively used to display errors for the entire form — or to warn the user in advance about something important. Inline forms help avoid modal modes, new screens, and nesting in complex forms. For example, when you need to add a new object consisting of several fields (first name, last name, middle name) to a collection.

**Accordion**, "Show more" pattern, [tooltip](https://koobiq.io/en/components/tooltip.md) will help when you simply need to show additional content. This option is suitable for less important things than inline messages.

A toast notification will briefly report the result of an operation or show a completely unimportant message. Toast notifications are used when it is acceptable for the person to miss the message, when the message can quickly become irrelevant, or when there is another way to read it (for example, in the notification center).

**A new page** replaces a modal window when a lot of content, a long form, or a list needs to be shown. This approach allows content to be presented fully, with focus and without loss of functionality (for example, navigation).

**The "Undo changes" pattern.** Instead of a "Are you sure?" modal dialog, it is often better to immediately perform the operation — and give the ability to undo it.

## API

### Components

#### KbqModalCaption (component)

Caption of a manually composed modal (`kbqComponent`). Projected into the header rendered by
`KbqModalTitle`, below the title, and clamped to two lines. The resulting markup matches the
header of a modal created via `KbqModalService.create`.

```ts
@Component({
    selector: '[kbq-modal-caption], kbq-modal-caption, [kbqModalCaption]'
})
class KbqModalCaption {}
```

#### KbqModalComponent (component)

```ts
@Component({ selector: 'kbq-modal' })
class KbqModalComponent<T = any, R = any> extends KbqModalRef<T, R> implements KbqModal {
    readonly kbqModalType = input<ModalType>(
        this.options?.kbqModalType ?? 'default'
    );
    readonly kbqComponent = input<Type<T> | undefined>(
        this.options?.kbqComponent
    );
    readonly kbqContent = input<string | TemplateRef<{}> | Type<T> | undefined>(
        this.options?.kbqContent
    );
    readonly kbqFooter = input<string | TemplateRef<{}> | IModalButtonOptions<T>[] | undefined>(
        this.options?.kbqFooter
    );
    readonly kbqVisible = model<boolean>(
        this.options?.kbqVisible ?? false
    );
    readonly kbqWidth = input<number | string | undefined>(
        this.options?.kbqWidth
    );
    readonly kbqSize = input<ModalSize>(
        this.options?.kbqSize ?? ModalSize.Medium
    );
    readonly kbqWrapClassName = input<string | undefined>(
        this.options?.kbqWrapClassName
    );
    readonly kbqClassName = input<string | undefined>(
        this.options?.kbqClassName
    );
    readonly kbqStyle = input<object | undefined>(
        this.options?.kbqStyle
    );
    readonly kbqTitle = input<string | TemplateRef<{}> | undefined>(
        this.options?.kbqTitle
    );
    readonly kbqCaption = input<string | TemplateRef<{}> | undefined>(
        this.options?.kbqCaption
    );
    readonly kbqCloseByESC = input<boolean>(
        this.options?.kbqCloseByESC ?? true
    );
    readonly kbqAutoFocus = input<KbqModalAutoFocus>(
        this.options?.kbqAutoFocus ?? 'first-tabbable'
    );
    readonly kbqAriaLabel = input<string | undefined>(
        this.options?.kbqAriaLabel
    );
    readonly kbqClosable = input<boolean>(
        this.options?.kbqClosable ?? true
    );
    readonly kbqMask = input<boolean>(this.options?.kbqMask ?? true);
    readonly kbqMaskClosable = input<boolean>(
        this.options?.kbqMaskClosable ?? false
    );
    readonly kbqMaskStyle = input<object | undefined>(
        this.options?.kbqMaskStyle
    );
    readonly kbqBodyStyle = input<object | undefined>(
        this.options?.kbqBodyStyle
    );
    readonly kbqOkText = input<string | undefined>(
        this.options?.kbqOkText
    );
    readonly kbqOkType = input<KbqButtonColor>(
        this.options?.kbqOkType ?? KbqComponentColors.Contrast
    );
    readonly kbqRestoreFocus = input<boolean>(
        this.options?.kbqRestoreFocus ?? true
    );
    readonly kbqOkLoading = input<boolean>(
        this.options?.kbqOkLoading ?? false
    );
    readonly kbqOkClick = input<OnClickCallback<T> | undefined>(
        this.options?.kbqOkClick
    );
    readonly kbqCancelText = input<string | undefined>(
        this.options?.kbqCancelText
    );
    readonly kbqCancelLoading = input<boolean>(
        this.options?.kbqCancelLoading ?? false
    );
    readonly kbqCancelClick = input<OnClickCallback<T> | undefined>(
        this.options?.kbqCancelClick
    );
    readonly kbqGetContainer = input<HTMLElement | OverlayRef | (() => HTMLElement | OverlayRef) | null>(
        this.options ? (this.options.kbqGetContainer ?? null) : () => this.overlay.create()
    );
    componentColors: typeof KbqComponentColors;
    readonly kbqAfterOpen: EventEmitter<void>;
    readonly kbqAfterClose: EventEmitter<R | undefined>;
    readonly kbqBeforeClose: EventEmitter<R | undefined>;
    readonly kbqOnOk: EventEmitter<T>;
    readonly kbqOnCancel: EventEmitter<T>;
    readonly modalContainer: Signal<ElementRef<any>>;
    readonly bodyContainer: Signal<ViewContainerRef>;
    readonly titleId: string;
    readonly captionId: string;
    get afterOpen(): Observable<void>;
    get beforeClose(): Observable<R | undefined>;
    get afterClose(): Observable<R | undefined>;
    get hidden(): boolean;
    open(): void;
    close(result?: R): void;
    destroy(result?: R): void;
    markForCheck(): void;
    triggerOk(): void;
    triggerCancel(): void;
    getInstance(): KbqModalComponent;
    getContentComponentRef(): ComponentRef<T>;
    getContentComponent(): T;
    getElement(): HTMLElement;
    onClickCloseBtn(): void;
}
```

- `[kbqModalType]`: `ModalType` — Layout the dialog renders.
- `[kbqComponent]`: `Type<T> | undefined` — The instance of component opened into the dialog.
- `[kbqContent]`: `string | TemplateRef<{}> | Type<T> | undefined` — Body of the dialog: text, a template or a component class. Falls back to `<ng-content>`.
- `[kbqFooter]`: `string | TemplateRef<{}> | IModalButtonOptions<T>[] | undefined` — Footer of the dialog: text, a template, or the buttons to render. Default modal ONLY.
- `[(kbqVisible)]`: `boolean` — Whether the dialog is shown. Two-way bindable as `[(kbqVisible)]`.
- `[kbqWidth]`: `number | string | undefined` — Explicit width, overriding the one `kbqSize` implies. A number is read as pixels.
- `[kbqSize]`: `ModalSize` — Width preset.
- `[kbqWrapClassName]`: `string | undefined` — Extra class names for the full-screen wrapper around the dialog.
- `[kbqClassName]`: `string | undefined` — Extra class names for the dialog element itself.
- `[kbqStyle]`: `object | undefined` — Inline styles for the dialog element.
- `[kbqTitle]`: `string | TemplateRef<{}> | undefined` — Heading of the dialog. Also becomes its accessible name.
- `[kbqCaption]`: `string | TemplateRef<{}> | undefined` — Secondary line under the heading. Also becomes the dialog's accessible description.
- `[kbqCloseByESC]`: `boolean` — Whether `Escape` cancels the dialog.
- `[kbqAutoFocus]`: `KbqModalAutoFocus` — Where focus lands when the dialog is shown.
- `[kbqAriaLabel]`: `string | undefined` — Accessible name for a dialog rendered without `kbqTitle` — a confirm or a header-less dialog.
- `[kbqClosable]`: `boolean` — Whether the header renders a close button.
- `[kbqMask]`: `boolean` — Whether the page behind the dialog is dimmed.
- `[kbqMaskClosable]`: `boolean` — Whether a click on the dim layer cancels the dialog.
- `[kbqMaskStyle]`: `object | undefined` — Inline styles for the dim layer.
- `[kbqBodyStyle]`: `object | undefined` — Inline styles for the body element.
- `[kbqOkText]`: `string | undefined` — Caption of the predefined OK button. The button is not rendered without it.
- `[kbqOkType]`: `KbqButtonColor` — Color of the predefined OK button.
- `[kbqRestoreFocus]`: `boolean` — Whether focus returns to the trigger when the dialog closes.
- `[kbqOkLoading]`: `boolean` — Whether the predefined OK button renders its progress state.
- `[kbqOkClick]`: `OnClickCallback<T> | undefined` — Decision handler of the predefined OK button. Returning `false` — or a promise of `false` —
  keeps the dialog open; anything else closes it. Receives the body component instance when the
  body is a component.

  Bind this **or** `(kbqOnOk)`, never both: a handler decides the close itself, so the event has
  nothing left to report and is not emitted. Binding both is reported in development mode.
- `[kbqCancelText]`: `string | undefined` — Caption of the predefined Cancel button. The button is not rendered without it.
- `[kbqCancelLoading]`: `boolean` — Whether the predefined Cancel button renders its progress state.
- `[kbqCancelClick]`: `OnClickCallback<T> | undefined` — Decision handler of the predefined Cancel button, the close button, `Escape` and the
  dim layer. Returning `false` — or a promise of `false` — keeps the dialog open.

  Bind this **or** `(kbqOnCancel)`, never both — see `kbqOkClick`.
- `[kbqGetContainer]`: `HTMLElement | OverlayRef | (() => HTMLElement | OverlayRef) | null` — Element or overlay the dialog is rendered into. Read once, on init.
- `[localeOverrides]`: `KbqPartialLocaleData | undefined` — Localized strings for this element and its subtree, keyed by locale section. Every key is optional at
  every depth.
- `kbqBeforeClose`: `EventEmitter<R | undefined>` — Emitted before the modal begins its closing animation.
- `kbqOnOk`: `EventEmitter<T>` — Emits when the predefined OK button is activated. A notification, not a decision — the dialog
  closes either way. Bind `[kbqOkClick]` instead to decide whether it closes.
- `kbqOnCancel`: `EventEmitter<T>` — Emits when the dialog is cancelled, by any of the controls `kbqCancelClick` covers. A
  notification, not a decision. Bind `[kbqCancelClick]` instead to decide whether it closes.
- `titleId`: `string` — Id of the element that names the dialog. Rendered on the title of a composed modal too.
- `captionId`: `string` — Id of the element that describes the dialog. Rendered on the caption of a composed modal too.
- `beforeClose`: `Observable<R | undefined>` — Observable alias for `kbqBeforeClose`
- `close()`: `void` — Starts the closing animation and resolves the `afterClose` contract with `result`.

  Closing an already closed dialog is a no-op: `afterClose` emits once per actual transition.

#### KbqModalTitle (component)

```ts
@Component({ selector: '[kbq-modal-title], kbq-modal-title, [kbqModalTitle]' })
class KbqModalTitle {}
```

- `[localeOverrides]`: `KbqPartialLocaleData | undefined` — Localized strings for this element and its subtree, keyed by locale section. Every key is optional at
  every depth.

### Directives

#### KbqModalBody (directive)

Scrollable body of a manually composed modal (`kbqComponent`). Publishes its scroll-shadow
state onto the modal instance so a sibling `KbqModalTitle`/`KbqModalFooter` can render the
matching shadow, since they have no direct template reference to this element.

```ts
@Directive({ selector: '[kbq-modal-body], kbq-modal-body, [kbqModalBody]' })
class KbqModalBody {}
```

#### KbqModalFooter (directive)

```ts
@Directive({
    selector: '[kbq-modal-footer], kbq-modal-footer, [kbqModalFooter]'
})
class KbqModalFooter {}
```

#### KbqModalMainAction (directive)

```ts
@Directive({ selector: '[kbq-modal-main-action]' })
class KbqModalMainAction {}
```

### Pipes

#### CssUnitPipe (pipe)

```ts
@Pipe({ name: 'toCssUnit' })
class CssUnitPipe implements PipeTransform {
    transform(
        value: number | string | null | undefined,
        defaultUnit?: string
    ): string | null;
}
```

- `transform()`: `string | null` — Returns `null` for a value that was never set, so the style binding it feeds is cleared.

### Services

#### KbqModalService (service)

```ts
@Injectable({ providedIn: 'root' })
class KbqModalService {
    get openModals(): KbqModalRef[];
    get afterAllClose(): Observable<void>;
    closeAll(): void;
    create<C, R = unknown>(
        options?: IModalOptionsForService<C>
    ): KbqModalRef<C, R>;
    confirm<C, R = unknown>(
        options?: IModalOptionsForService<C>,
        confirmType?: ConfirmType
    ): KbqModalRef<C, R>;
    open<C, R = unknown>(
        options?: IModalOptionsForService<C>
    ): KbqModalRef<C, R>;
    success<C, R = unknown>(
        options?: IModalOptionsForService<C>
    ): KbqModalRef<C, R>;
    delete<C, R = unknown>(
        options?: IModalOptionsForService<C>
    ): KbqModalRef<C, R>;
}
```

- `create()`: `KbqModalRef<C, R>` — Opens a dialog. Its lifetime is bound to `options.injector` — the caller's injector when one
  is passed, the root environment injector otherwise — so destroying the opener closes it.
- `delete()`: `KbqModalRef<C, R>` — Opens a confirm dialog styled for a destructive action — the `warn` confirm type.

### Classes

#### KbqModalRef (abstract class)

API class that public to users to handle the modal instance.
KbqModalRef is aim to avoid accessing to the modal instance directly by users.

```ts
abstract class KbqModalRef<C = any, R = unknown> {
    abstract afterOpen: Observable<void>;
    abstract beforeClose: Observable<R | undefined>;
    abstract afterClose: Observable<R | undefined>;
    abstract open(): void;
    abstract close(result?: R): void;
    abstract destroy(result?: R): void;
    abstract triggerOk(): void;
    abstract triggerCancel(): void;
    abstract getContentComponent(): C;
    abstract getElement(): HTMLElement;
    abstract getInstance(): KbqModalComponent;
    abstract markForCheck(): any;
}
```

- `triggerOk()`: `void` — Runs the OK flow exactly as the predefined OK button does: the `kbqOkClick` handler when one
  is bound, the `kbqOnOk` emitter otherwise.
- `triggerCancel()`: `void` — Runs the cancel flow exactly as the predefined Cancel button does.
- `getContentComponent()`: `C` — Return the component instance of kbqContent when specify kbqContent as a Component
  Note: this method may return undefined if the Component has not ready yet.
  (it only available after Modal's ngOnInit)
- `getElement()`: `HTMLElement` — Get the dom element of this Modal
- `getInstance()`: `KbqModalComponent` — Get the instance of the Modal itself
- `markForCheck()`: `any` — Call markForCheck for change detector

### Types

#### AnimationState (type)

Phase of the open/close animation the dialog is in, or `null` between phases.

```ts
type AnimationState = 'enter' | 'leave' | null;
```

#### ConfirmType (type)

```ts
type ConfirmType = 'confirm' | 'success' | 'warn';
```

#### IModalButtonOptions (interface)

```ts
interface IModalButtonOptions<T = any> {
    label: string;
    type?: string;
    shape?: string;
    ghost?: boolean;
    size?: string;
    autoLoading?: boolean;
    show?: boolean | ((this: IModalButtonOptions<T>, contentComponentInstance?: T) => boolean);
    loading?: boolean | ((this: IModalButtonOptions<T>, contentComponentInstance?: T) => boolean);
    disabled?: boolean | ((this: IModalButtonOptions<T>, contentComponentInstance?: T) => boolean);
    autoFocus?: boolean;
    kbqModalMainAction?: boolean;
    onClick?(
        this: IModalButtonOptions<T>,
        contentComponentInstance?: T
    ): void | {} | Promise<void | {}>;
}
```

- `show?`: `boolean | ((this: IModalButtonOptions<T>, contentComponentInstance?: T) => boolean)` — Whether the button is rendered.

  The callable form is re-evaluated on every change-detection pass over the dialog, which is
  what lets it track state the dialog knows nothing about — so it must be cheap and free of
  side effects. `contentComponentInstance` is passed only when the body is a component.
- `loading?`: `boolean | ((this: IModalButtonOptions<T>, contentComponentInstance?: T) => boolean)` — Whether the button renders its progress state. Re-evaluated per pass — see `IModalButtonOptions.show`.
- `disabled?`: `boolean | ((this: IModalButtonOptions<T>, contentComponentInstance?: T) => boolean)` — Whether the button is disabled. Re-evaluated per pass — see `IModalButtonOptions.show`.

#### IModalOptionsForService (interface)

```ts
interface IModalOptionsForService<T = any> extends ModalOptions<T> {
    injector?: Injector;
    kbqModalType?: ModalType;
    kbqVisible?: boolean;
    kbqWidth?: number | string;
    kbqSize?: ModalSize;
    kbqWrapClassName?: string;
    kbqClassName?: string;
    kbqStyle?: object;
    kbqTitle?: string | TemplateRef<{}>;
    kbqCaption?: string | TemplateRef<{}>;
    kbqContent?: string | TemplateRef<{}> | Type<C>;
    kbqComponent?: Type<C>;
    kbqClosable?: boolean;
    kbqMask?: boolean;
    kbqMaskClosable?: boolean;
    kbqMaskStyle?: object;
    kbqBodyStyle?: object;
    kbqFooter?: string | TemplateRef<{}> | IModalButtonOptions<C>[];
    kbqGetContainer?: HTMLElement | OverlayRef | (() => HTMLElement | OverlayRef) | null;
    kbqAfterOpen?: EventEmitter<void>;
    kbqAfterClose?: EventEmitter<R>;
    kbqCloseByESC?: boolean;
    kbqRestoreFocus?: boolean;
    kbqAutoFocus?: KbqModalAutoFocus;
    kbqAriaLabel?: string;
    kbqOkText?: string;
    kbqOkType?: KbqButtonColor;
    kbqOkLoading?: boolean;
    kbqOkClick?: OnClickCallback<C>;
    kbqOnOk?: EventEmitter<C>;
    kbqCancelText?: string;
    kbqCancelLoading?: boolean;
    kbqCancelClick?: OnClickCallback<C>;
    kbqOnCancel?: EventEmitter<C>;
    data?: unknown;
}
```

- `injector?`: `Injector` — The injector used to create the component that will be attached.
  If specified, it overrides the injector provided by `KbqModalService`.

  Its `DestroyRef` also owns the dialog: the modal is torn down when the injector is destroyed.
- `kbqModalType?`: `ModalType` — Layout the dialog renders. Default is `'default'`.
- `kbqVisible?`: `boolean` — Whether the dialog is shown. Managed by the service on the imperative path.
- `kbqWidth?`: `number | string` — Explicit width, overriding the one `kbqSize` implies. A number is read as pixels.
- `kbqSize?`: `ModalSize` — Width preset. Default is `ModalSize.Medium` (640px).
- `kbqWrapClassName?`: `string` — Extra class names for the full-screen wrapper around the dialog.
- `kbqClassName?`: `string` — Extra class names for the dialog element itself.
- `kbqStyle?`: `object` — Inline styles for the dialog element.
- `kbqTitle?`: `string | TemplateRef<{}>` — Heading of the dialog. Also becomes its accessible name.
- `kbqCaption?`: `string | TemplateRef<{}>` — Secondary line under the heading. Also becomes the dialog's accessible description.
- `kbqContent?`: `string | TemplateRef<{}> | Type<C>` — Body of the dialog: text, a template or a component class.
- `kbqComponent?`: `Type<C>` — The instance of component opened into the dialog.
- `kbqClosable?`: `boolean` — Whether the header renders a close button. Default is `true`.
- `kbqMask?`: `boolean` — Whether the page behind the dialog is dimmed. Default is `true`.
- `kbqMaskClosable?`: `boolean` — Whether a click on the dim layer cancels the dialog. Default is `false`.
- `kbqMaskStyle?`: `object` — Inline styles for the dim layer.
- `kbqBodyStyle?`: `object` — Inline styles for the body element.
- `kbqFooter?`: `string | TemplateRef<{}> | IModalButtonOptions<C>[]` — Footer of the dialog: text, a template, or the buttons to render. Default modal ONLY.
- `kbqGetContainer?`: `HTMLElement | OverlayRef | (() => HTMLElement | OverlayRef) | null` — Element or overlay the dialog is rendered into. STATIC — read once, on init.
- `kbqAfterOpen?`: `EventEmitter<void>` — Emitter that mirrors the dialog's own open event. It is subscribed to the dialog, never
  substituted for it, so `afterOpen` on the returned `KbqModalRef` keeps working alongside.
- `kbqAfterClose?`: `EventEmitter<R>` — Emitter that mirrors the dialog's own close event. It is subscribed to the dialog, never
  substituted for it, so `afterClose` on the returned `KbqModalRef` keeps working alongside.
- `kbqCloseByESC?`: `boolean` — Whether `Escape` cancels the dialog. Default is `true`.
- `kbqRestoreFocus?`: `boolean` — Whether focus returns to the trigger when the dialog closes. Default is `true`.
- `kbqAutoFocus?`: `KbqModalAutoFocus` — Where focus lands when the dialog is shown. Default is `'first-tabbable'`.
- `kbqAriaLabel?`: `string` — Accessible name for a dialog rendered without `kbqTitle` — a confirm or a header-less dialog.
- `kbqOkText?`: `string` — Caption of the predefined OK button. The button is not rendered without it.
- `kbqOkType?`: `KbqButtonColor` — Color of the predefined OK button. Default is `KbqComponentColors.Contrast`.
- `kbqOkLoading?`: `boolean` — Whether the predefined OK button renders its progress state.
- `kbqOkClick?`: `OnClickCallback<C>` — Decision handler of the predefined OK button. Returning `false` (or a promise of `false`)
  keeps the dialog open; anything else closes it. Pair of the `kbqOnOk` output — bind one.
- `kbqOnOk?`: `EventEmitter<C>` — Emitter notified when the predefined OK button is activated. Pair of `kbqOkClick` — bind one.
- `kbqCancelText?`: `string` — Caption of the predefined Cancel button. The button is not rendered without it.
- `kbqCancelLoading?`: `boolean` — Whether the predefined Cancel button renders its progress state.
- `kbqCancelClick?`: `OnClickCallback<C>` — Decision handler of the predefined Cancel button, the close button, `Escape` and the
  dim layer. Returning `false` (or a promise of `false`) keeps the dialog open. Pair of the
  `kbqOnCancel` output — bind one.
- `kbqOnCancel?`: `EventEmitter<C>` — Emitter notified when the dialog is cancelled. Pair of `kbqCancelClick` — bind one.
- `data?`: `unknown` — Data being injected into the child component.

#### KbqModal (interface)

The members of `KbqModalComponent` the composition directives (`kbq-modal-title`,
`kbq-modal-caption`, `kbq-modal-body`, `kbq-modal-footer`) depend on. They are projected into the
dialog, so they inject this token instead of the concrete host component.

```ts
interface KbqModal {
    readonly kbqClosable: Signal<boolean>;
    readonly componentColors: typeof KbqComponentColors;
    readonly titleId: string;
    readonly captionId: string;
    readonly bodyOverflow: Signal<KbqOverflowShadowState>;
    readonly afterOpen: Observable<void>;
    onClickCloseBtn(): void;
    registerTitle(): void;
    registerCaption(): void;
    registerFooter(): void;
    setBodyOverflow(state: KbqOverflowShadowState): void;
}
```

- `kbqClosable`: `Signal<boolean>` — Whether the dialog renders a close button in its header.
- `componentColors`: `typeof KbqComponentColors` — Palette the composed header binds the close-icon color from.
- `titleId`: `string` — Id the dialog element points `aria-labelledby` at. Belongs on the rendered title element.
- `captionId`: `string` — Id the dialog element points `aria-describedby` at. Belongs on the rendered caption element.
- `bodyOverflow`: `Signal<KbqOverflowShadowState>` — Scroll-shadow state of the composed body, so the header and footer can render matching shadows.
- `afterOpen`: `Observable<void>` — Emits once the dialog is fully shown.
- `onClickCloseBtn()`: `void` — Runs the cancel flow, exactly as the templated close button does.
- `registerTitle()`: `void` — Announces that a composed title is rendered, so the dialog can use it as its accessible name.
- `registerCaption()`: `void` — Announces that a composed caption is rendered, so the dialog can use it as its description.
- `registerFooter()`: `void` — Announces that a composed footer is rendered, so the body keeps its normal bottom padding.
- `setBodyOverflow()`: `void` — Publishes the composed body's scroll-shadow state.

#### KbqModalAutoFocus (type)

Where focus lands when the dialog is shown.

`[cdkFocusInitial]` and `autofocus` inside the dialog win over every value but `false`.

- `first-tabbable` — the first tabbable control, which for a titled dialog is the close button;
- `dialog` — the dialog element itself, so a long scrollable body is read from its start;
- `first-heading` — the dialog title, falling back to the dialog element;
- `false` — nothing is focused, and the caller is responsible for moving focus into the dialog.

```ts
type KbqModalAutoFocus = 'first-tabbable' | 'dialog' | 'first-heading' | false;
```

#### ModalOptions (interface)

```ts
interface ModalOptions<C = any, R = any> {
    kbqModalType?: ModalType;
    kbqVisible?: boolean;
    kbqWidth?: number | string;
    kbqSize?: ModalSize;
    kbqWrapClassName?: string;
    kbqClassName?: string;
    kbqStyle?: object;
    kbqTitle?: string | TemplateRef<{}>;
    kbqCaption?: string | TemplateRef<{}>;
    kbqContent?: string | TemplateRef<{}> | Type<C>;
    kbqComponent?: Type<C>;
    kbqClosable?: boolean;
    kbqMask?: boolean;
    kbqMaskClosable?: boolean;
    kbqMaskStyle?: object;
    kbqBodyStyle?: object;
    kbqFooter?: string | TemplateRef<{}> | IModalButtonOptions<C>[];
    kbqGetContainer?: HTMLElement | OverlayRef | (() => HTMLElement | OverlayRef) | null;
    kbqAfterOpen?: EventEmitter<void>;
    kbqAfterClose?: EventEmitter<R>;
    kbqCloseByESC?: boolean;
    kbqRestoreFocus?: boolean;
    kbqAutoFocus?: KbqModalAutoFocus;
    kbqAriaLabel?: string;
    kbqOkText?: string;
    kbqOkType?: KbqButtonColor;
    kbqOkLoading?: boolean;
    kbqOkClick?: OnClickCallback<C>;
    kbqOnOk?: EventEmitter<C>;
    kbqCancelText?: string;
    kbqCancelLoading?: boolean;
    kbqCancelClick?: OnClickCallback<C>;
    kbqOnCancel?: EventEmitter<C>;
    data?: unknown;
}
```

- `kbqModalType?`: `ModalType` — Layout the dialog renders. Default is `'default'`.
- `kbqVisible?`: `boolean` — Whether the dialog is shown. Managed by the service on the imperative path.
- `kbqWidth?`: `number | string` — Explicit width, overriding the one `kbqSize` implies. A number is read as pixels.
- `kbqSize?`: `ModalSize` — Width preset. Default is `ModalSize.Medium` (640px).
- `kbqWrapClassName?`: `string` — Extra class names for the full-screen wrapper around the dialog.
- `kbqClassName?`: `string` — Extra class names for the dialog element itself.
- `kbqStyle?`: `object` — Inline styles for the dialog element.
- `kbqTitle?`: `string | TemplateRef<{}>` — Heading of the dialog. Also becomes its accessible name.
- `kbqCaption?`: `string | TemplateRef<{}>` — Secondary line under the heading. Also becomes the dialog's accessible description.
- `kbqContent?`: `string | TemplateRef<{}> | Type<C>` — Body of the dialog: text, a template or a component class.
- `kbqComponent?`: `Type<C>` — The instance of component opened into the dialog.
- `kbqClosable?`: `boolean` — Whether the header renders a close button. Default is `true`.
- `kbqMask?`: `boolean` — Whether the page behind the dialog is dimmed. Default is `true`.
- `kbqMaskClosable?`: `boolean` — Whether a click on the dim layer cancels the dialog. Default is `false`.
- `kbqMaskStyle?`: `object` — Inline styles for the dim layer.
- `kbqBodyStyle?`: `object` — Inline styles for the body element.
- `kbqFooter?`: `string | TemplateRef<{}> | IModalButtonOptions<C>[]` — Footer of the dialog: text, a template, or the buttons to render. Default modal ONLY.
- `kbqGetContainer?`: `HTMLElement | OverlayRef | (() => HTMLElement | OverlayRef) | null` — Element or overlay the dialog is rendered into. STATIC — read once, on init.
- `kbqAfterOpen?`: `EventEmitter<void>` — Emitter that mirrors the dialog's own open event. It is subscribed to the dialog, never
  substituted for it, so `afterOpen` on the returned `KbqModalRef` keeps working alongside.
- `kbqAfterClose?`: `EventEmitter<R>` — Emitter that mirrors the dialog's own close event. It is subscribed to the dialog, never
  substituted for it, so `afterClose` on the returned `KbqModalRef` keeps working alongside.
- `kbqCloseByESC?`: `boolean` — Whether `Escape` cancels the dialog. Default is `true`.
- `kbqRestoreFocus?`: `boolean` — Whether focus returns to the trigger when the dialog closes. Default is `true`.
- `kbqAutoFocus?`: `KbqModalAutoFocus` — Where focus lands when the dialog is shown. Default is `'first-tabbable'`.
- `kbqAriaLabel?`: `string` — Accessible name for a dialog rendered without `kbqTitle` — a confirm or a header-less dialog.
- `kbqOkText?`: `string` — Caption of the predefined OK button. The button is not rendered without it.
- `kbqOkType?`: `KbqButtonColor` — Color of the predefined OK button. Default is `KbqComponentColors.Contrast`.
- `kbqOkLoading?`: `boolean` — Whether the predefined OK button renders its progress state.
- `kbqOkClick?`: `OnClickCallback<C>` — Decision handler of the predefined OK button. Returning `false` (or a promise of `false`)
  keeps the dialog open; anything else closes it. Pair of the `kbqOnOk` output — bind one.
- `kbqOnOk?`: `EventEmitter<C>` — Emitter notified when the predefined OK button is activated. Pair of `kbqOkClick` — bind one.
- `kbqCancelText?`: `string` — Caption of the predefined Cancel button. The button is not rendered without it.
- `kbqCancelLoading?`: `boolean` — Whether the predefined Cancel button renders its progress state.
- `kbqCancelClick?`: `OnClickCallback<C>` — Decision handler of the predefined Cancel button, the close button, `Escape` and the
  dim layer. Returning `false` (or a promise of `false`) keeps the dialog open. Pair of the
  `kbqOnCancel` output — bind one.
- `kbqOnCancel?`: `EventEmitter<C>` — Emitter notified when the dialog is cancelled. Pair of `kbqCancelClick` — bind one.
- `data?`: `unknown` — Data being injected into the child component.

#### ModalType (type)

```ts
type ModalType = 'default' | 'confirm' | 'custom';
```

#### OnClickCallback (type)

```ts
type OnClickCallback<T> = (instance: T) => (false | void | {}) | Promise<false | void | {}>;
```

### Enums

#### ModalSize (enum)

```ts
enum ModalSize {
    Small = 'small',
    Medium = 'medium',
    Large = 'large'
}
```

### Constants

#### KBQ_MODAL (const)

Injection token exposing the dialog to the directives composed inside it.

```ts
const KBQ_MODAL: InjectionToken<KbqModal>;
```

#### KBQ_MODAL_DATA (const)

Injection token that can be used to access the data that was passed in to a modal.

```ts
const KBQ_MODAL_DATA: InjectionToken<unknown>;
```

## Examples

### Custom Injector for Modal Component

A component passed as `kbqComponent` is created from the **root** environment injector, not from the component that called `KbqModalService`. Anything the caller provides itself is therefore invisible inside the modal: component-level services, tokens from a route or a lazily loaded module, and providers that reach the caller only through NgModules listed in its standalone `imports` — `DateAdapter`, `KBQ_DATE_FORMATS` and `DateFormatter` among them.

To make them available, pass the caller's `injector` to `modalService.open`; it becomes the parent of the modal content's injection hierarchy. Content passed as a `TemplateRef` is unaffected — it keeps the injector of the component that declared it.

A missing dependency surfaces as `NullInjectorError: No provider for …`, thrown while the modal content is being created.

**Example: Modal component With Injector** (`modal-component-with-injector`)

```ts modal-component-with-injector-example.ts
import { ChangeDetectionStrategy, Component, inject, Injectable, Injector } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqModalModule, KbqModalRef, KbqModalService } from '@koobiq/components/modal';

@Injectable()
export class ComponentLevelService {
    action() {
        console.log('Hello world!');
    }
}

@Component({
    selector: 'custom-modal',
    imports: [
        KbqModalModule,
        KbqButtonModule
    ],
    template: `
        <kbq-modal-title>title</kbq-modal-title>
        <kbq-modal-body>subtitle</kbq-modal-body>

        <div kbq-modal-footer>
            <button kbq-button [color]="'contrast'" (click)="customService.action()">
                Call method from injected service
            </button>
            <button kbq-button autofocus (click)="destroyModal('close')">Close</button>
        </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomModalComponent {
    private readonly modalRef = inject(KbqModalRef);
    protected readonly customService = inject(ComponentLevelService);

    destroyModal(action: 'save' | 'close') {
        this.modalRef.destroy(action);
    }
}

@Component({
    selector: 'modal-component-with-injector-example',
    imports: [KbqButtonModule],
    template: `
        <button kbq-button (click)="openModal()">Open Modal</button>
    `,
    providers: [ComponentLevelService],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ModalComponentWithInjectorExample {
    private readonly modalService = inject(KbqModalService);
    private readonly injector = inject(Injector);

    modalRef: KbqModalRef<CustomModalComponent, 'save' | 'close'>;

    openModal(): void {
        this.modalRef = this.modalService.open({
            kbqComponent: CustomModalComponent,
            // provide injector, so `ComponentLevelService` will be accessible inside `CustomModalComponent`
            injector: this.injector
        });

        this.modalRef.afterOpen.subscribe(() => {
            console.log('[afterOpen] emitted!');
        });

        this.modalRef.afterClose.subscribe((action) => {
            console.log(`[afterClose] emitted, chosen action: ${action}`);
        });
    }
}
```
