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
21 changes: 21 additions & 0 deletions docs/components-api/toast.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,25 @@

# ExtraToast

> ✅ **Реализован**: `ExtraToastComponent` (`@cdek-it/angular-ui-kit`) соответствует спецификации.

Уведомление, отображаемое поверх интерфейса (overlay-вариант `ExtraMessage`; в потоке документа используется `ExtraMessage`).

Показ уведомлений устроен в два уровня: сам компонент `<extra-toast>` — это контейнер, монтируемый
один раз, а показ каждого отдельного уведомления выполняется отдельным императивным вызовом. Поэтому
и свойства ниже разделены на два независимых набора.

# Свойства компонента (`<extra-toast>`)

| Свойство | Описание | Типизация |
| -------- | ---------------------------------------------------------------- | --------- |
| `position` | позиция группы уведомлений на экране | `top-right \| top-left \| top-center \| bottom-right \| bottom-left \| bottom-center \| center` |
| `key` | ключ контейнера; показывает только уведомления с тем же `key` (несколько независимых очередей) | `string` |
| `life` | время автозакрытия по умолчанию (мс) для уведомлений этого контейнера | `number` |
| `appendTo` | куда монтируется контейнер; по умолчанию `body`, чтобы уведомления позиционировались относительно окна браузера, а не предка с `transform`/`filter`/`contain` | `'body' \| 'self' \| HTMLElement \| ElementRef<HTMLElement>` |

# Свойства уведомления (передаются при вызове показа)

| Свойство | Описание | Типизация |
| ------------ | ------------------------------------------------------------ | -------------------------------------- |
| `severity` | тип сообщения | `info \| success \| warning \| danger` |
Expand All @@ -15,13 +32,17 @@

# Слоты (content projection)

Относятся к уведомлению — так же передаются при вызове показа, а не проецируются в `<extra-toast>`.

| Слот | Описание |
| --------- | --------------------- |
| `content` | контент после caption |
| `footer` | контент футера |

# События

Относится к уведомлению — передаётся полем при вызове показа, а не эмитится самим `<extra-toast>`.

| Событие | Описание | Типизация |
| --------- | ------------------------------------ | ------------ |
| `onClose` | срабатывает при закрытии уведомления | `() => void` |
1 change: 0 additions & 1 deletion src/lib/components/toast/provide-toast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,3 @@ import { MessageService } from 'primeng/api';
export function provideExtraToast(): EnvironmentProviders {
return makeEnvironmentProviders([MessageService]);
}

3 changes: 0 additions & 3 deletions src/lib/components/toast/public_api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
export * from './toast.component';
export * from './toast.service';
export * from './provide-toast';



104 changes: 89 additions & 15 deletions src/lib/components/toast/toast.component.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { Component, Input } from '@angular/core';
import { Toast } from 'primeng/toast';
import { AfterViewInit, Component, ElementRef, inject, Input } from '@angular/core';
import { DOCUMENT, NgTemplateOutlet } from '@angular/common';
import { Toast, ToastCloseEvent } from 'primeng/toast';
import { ButtonDirective } from 'primeng/button';
import { SharedModule } from 'primeng/api';
import { PrimeNG } from 'primeng/config';

export type ExtraToastSeverity = 'success' | 'info' | 'warn' | 'error' | 'secondary' | 'contrast';
export type ExtraToastElementLike = HTMLElement | ElementRef<HTMLElement>;
/** Куда монтируется контейнер уведомлений; `'self'` оставляет его на месте в шаблоне. */
export type ExtraToastAppendTo = 'body' | 'self' | ExtraToastElementLike;

export type ExtraToastSeverity = 'info' | 'success' | 'warning' | 'danger';
export type ExtraToastPosition =
| 'top-right'
| 'top-left'
Expand All @@ -12,6 +19,16 @@ export type ExtraToastPosition =
| 'bottom-center'
| 'center';

/** Внутренний словарь PrimeNG-совместимых значений severity — публично не используется. */
type PrimeToastSeverity = 'success' | 'info' | 'warn' | 'error';

/** Маппинг собственного severity на PrimeNG; используется сервисом при отправке сообщения. */
export function toPrimeToastSeverity(severity: ExtraToastSeverity): PrimeToastSeverity {
if (severity === 'warning') return 'warn';
if (severity === 'danger') return 'error';
return severity;
}

const SEVERITY_ICONS: Record<string, string> = {
info: 'ti ti-info-circle',
success: 'ti ti-circle-check',
Expand All @@ -22,29 +39,86 @@ const SEVERITY_ICONS: Record<string, string> = {
@Component({
selector: 'extra-toast',
standalone: true,
imports: [Toast, SharedModule],
imports: [Toast, SharedModule, NgTemplateOutlet, ButtonDirective],
template: `
<p-toast [position]="position" [key]="key" [life]="life" [pt]="pt">
<ng-template #message let-message>
<div class="p-toast-accent-line"></div>
<i [class]="resolveIcon(message) + ' p-toast-message-icon'"></i>
<div class="p-toast-message-text">
<span class="p-toast-summary">{{ message.summary }}</span>
@if (message.detail) {
<div class="p-toast-detail">{{ message.detail }}</div>
<p-toast [position]="position" [key]="key" [life]="life" (onClose)="onMessageClose($event)">
<!-- headless-шаблон: вся разметка сообщения наша, включая кнопку закрытия (closeFn из контекста) -->
<ng-template #headless let-message let-closeFn="closeFn">
<div class="p-toast-message-content">
<div class="p-toast-accent-line"></div>
<i [class]="resolveIcon(message) + ' p-toast-message-icon'"></i>
<div class="p-toast-message-text">
@if (message.summary) {
<span class="p-toast-summary">{{ message.summary }}</span>
}
@if (message.detail) {
<div class="p-toast-detail">{{ message.detail }}</div>
}
</div>
@if (message.closable !== false) {
<button
type="button"
pButton
[text]="true"
icon="ti ti-x"
class="p-toast-close-button"
[attr.aria-label]="closeAriaLabel"
(click)="closeFn($event)"
></button>
}
@if (message.data?.content) {
<div class="p-toast-body">
<ng-container [ngTemplateOutlet]="message.data.content" />
</div>
}
@if (message.data?.footer) {
<div class="p-toast-footer">
<ng-container [ngTemplateOutlet]="message.data.footer" />
</div>
}
</div>
</ng-template>
</p-toast>
`
})
export class ExtraToastComponent {
export class ExtraToastComponent implements AfterViewInit {
@Input() position: ExtraToastPosition = 'top-right';
@Input() key: string | undefined = undefined;
@Input() life = 5000;
@Input() pt: Record<string, any> | undefined = undefined;
/**
* Куда монтируется контейнер уведомлений. По умолчанию `'body'`: уведомления позиционируются
* относительно окна браузера, а не ближайшего предка с `transform`/`filter`/`contain`
* (такой предок становится content block для `position: fixed` и «запирает» уведомления внутри себя).
*/
@Input() appendTo: ExtraToastAppendTo = 'body';

private readonly config = inject(PrimeNG);
private readonly document = inject(DOCUMENT);
private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef);

get closeAriaLabel(): string | undefined {
return this.config.translation?.aria?.close;
}

ngAfterViewInit(): void {
const target = this.resolveAppendTarget();
if (target && target !== this.elementRef.nativeElement.parentNode) {
target.appendChild(this.elementRef.nativeElement);
}
}

private resolveAppendTarget(): HTMLElement | undefined {
if (this.appendTo === 'self') return undefined;
if (this.appendTo === 'body') return this.document.body ?? undefined;
return this.appendTo instanceof ElementRef ? this.appendTo.nativeElement : this.appendTo;
}

resolveIcon(message: { severity?: string; icon?: string }): string {
return message.icon ?? SEVERITY_ICONS[message.severity ?? 'info'] ?? 'ti ti-info-circle';
return message.icon ?? SEVERITY_ICONS[message.severity ?? 'info'] ?? SEVERITY_ICONS['info'];
}

/** Диспетчеризует пользовательский onClose-колбэк конкретного сообщения (клик по крестику или таймер). */
onMessageClose(event: ToastCloseEvent): void {
(event.message?.data as { onClose?: () => void } | undefined)?.onClose?.();
}
}
Loading
Loading