mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
Compare commits
1 Commits
1cc13eebe1
...
feature/52
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20003ab72a |
@@ -16,6 +16,7 @@
|
|||||||
@import "../../../libs/ui/progress-bar/src/lib/progress-bar.scss";
|
@import "../../../libs/ui/progress-bar/src/lib/progress-bar.scss";
|
||||||
@import "../../../libs/ui/search-bar/src/search-bar.scss";
|
@import "../../../libs/ui/search-bar/src/search-bar.scss";
|
||||||
@import "../../../libs/ui/skeleton-loader/src/skeleton-loader.scss";
|
@import "../../../libs/ui/skeleton-loader/src/skeleton-loader.scss";
|
||||||
|
@import "../../../libs/ui/snackbar/src/snackbar.scss";
|
||||||
@import "../../../libs/ui/tooltip/src/tooltip.scss";
|
@import "../../../libs/ui/tooltip/src/tooltip.scss";
|
||||||
|
|
||||||
.input-control {
|
.input-control {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||||
import { ButtonComponent } from '@isa/ui/buttons';
|
import { ButtonComponent } from '@isa/ui/buttons';
|
||||||
import { injectDialog } from '@isa/ui/dialog';
|
import { injectDialog } from '@isa/ui/dialog';
|
||||||
import { SearchItemToRemitDialogComponent } from '@isa/remission/shared/search-item-to-remit-dialog';
|
import { SearchItemToRemitDialogComponent } from '@isa/remission/shared/search-item-to-remit-dialog';
|
||||||
|
import { SnackbarService } from '@isa/ui/snackbar';
|
||||||
|
import { isaActionCheck } from '@isa/icons';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'remission-feature-remission-start-card',
|
selector: 'remission-feature-remission-start-card',
|
||||||
templateUrl: './remission-start-card.component.html',
|
templateUrl: './remission-start-card.component.html',
|
||||||
@@ -11,9 +12,34 @@ import { SearchItemToRemitDialogComponent } from '@isa/remission/shared/search-i
|
|||||||
imports: [ButtonComponent],
|
imports: [ButtonComponent],
|
||||||
})
|
})
|
||||||
export class RemissionStartCardComponent {
|
export class RemissionStartCardComponent {
|
||||||
|
#snackbar = inject(SnackbarService);
|
||||||
searchItemToRemitDialog = injectDialog(SearchItemToRemitDialogComponent);
|
searchItemToRemitDialog = injectDialog(SearchItemToRemitDialogComponent);
|
||||||
|
|
||||||
startRemission() {
|
startRemission() {
|
||||||
this.searchItemToRemitDialog({ data: { searchTerm: 'Pokemon' } });
|
// this.searchItemToRemitDialog({ data: { searchTerm: 'Pokemon' } });
|
||||||
|
this.#snackbar.show({
|
||||||
|
message: 'Remission started successfully!',
|
||||||
|
icon: isaActionCheck,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: 'View Remission',
|
||||||
|
handler: () => {
|
||||||
|
// Navigate to the remission list or details page
|
||||||
|
// this.router.navigate(['/remissions']);
|
||||||
|
},
|
||||||
|
color: 'primary',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'New Remission',
|
||||||
|
handler: () => {
|
||||||
|
// Open the dialog to start a new remission
|
||||||
|
},
|
||||||
|
color: 'secondary',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'brand',
|
||||||
|
position: 'top-right',
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
libs/ui/snackbar/README.md
Normal file
7
libs/ui/snackbar/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# ui-snackbar
|
||||||
|
|
||||||
|
This library was generated with [Nx](https://nx.dev).
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `nx test ui-snackbar` to execute the unit tests.
|
||||||
34
libs/ui/snackbar/eslint.config.cjs
Normal file
34
libs/ui/snackbar/eslint.config.cjs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
const nx = require('@nx/eslint-plugin');
|
||||||
|
const baseConfig = require('../../../eslint.config.js');
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
...baseConfig,
|
||||||
|
...nx.configs['flat/angular'],
|
||||||
|
...nx.configs['flat/angular-template'],
|
||||||
|
{
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
rules: {
|
||||||
|
'@angular-eslint/directive-selector': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
type: 'attribute',
|
||||||
|
prefix: 'ui',
|
||||||
|
style: 'camelCase',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@angular-eslint/component-selector': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
type: 'element',
|
||||||
|
prefix: 'ui',
|
||||||
|
style: 'kebab-case',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.html'],
|
||||||
|
// Override or add rules here
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
20
libs/ui/snackbar/project.json
Normal file
20
libs/ui/snackbar/project.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "ui-snackbar",
|
||||||
|
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "libs/ui/snackbar/src",
|
||||||
|
"prefix": "ui",
|
||||||
|
"projectType": "library",
|
||||||
|
"tags": [],
|
||||||
|
"targets": {
|
||||||
|
"test": {
|
||||||
|
"executor": "@nx/vite:test",
|
||||||
|
"outputs": ["{options.reportsDirectory}"],
|
||||||
|
"options": {
|
||||||
|
"reportsDirectory": "../../../coverage/libs/ui/snackbar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nx/eslint:lint"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
libs/ui/snackbar/src/index.ts
Normal file
12
libs/ui/snackbar/src/index.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// Services
|
||||||
|
export * from './lib/snackbar.service';
|
||||||
|
|
||||||
|
// Components
|
||||||
|
export * from './lib/snackbar.component';
|
||||||
|
|
||||||
|
// Types and interfaces
|
||||||
|
export * from './lib/snackbar.types';
|
||||||
|
export * from './lib/snackbar-ref';
|
||||||
|
|
||||||
|
// Tokens (for advanced usage)
|
||||||
|
export * from './lib/snackbar.tokens';
|
||||||
223
libs/ui/snackbar/src/lib/_snackbar.scss
Normal file
223
libs/ui/snackbar/src/lib/_snackbar.scss
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
// Main snackbar styles (following button patterns)
|
||||||
|
.ui-snackbar {
|
||||||
|
@apply font-sans;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 6.25rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
min-width: 16rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snackbar content area
|
||||||
|
.ui-snackbar__content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-snackbar__icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-snackbar__message {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snackbar actions area
|
||||||
|
.ui-snackbar__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-snackbar__action {
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
// Override button min-width for snackbar actions
|
||||||
|
&.ui-text-button {
|
||||||
|
min-width: auto;
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-snackbar__dismiss {
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
// Make dismiss button smaller
|
||||||
|
&.ui-icon-button {
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size variants (following button patterns)
|
||||||
|
.ui-snackbar__small {
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
|
||||||
|
.ui-snackbar__icon {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-snackbar__medium {
|
||||||
|
padding: 0.5rem 1.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
|
||||||
|
.ui-snackbar__icon {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-snackbar__large {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
|
||||||
|
.ui-snackbar__icon {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-snackbar__message {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type/color variants (following button patterns)
|
||||||
|
.ui-snackbar__primary {
|
||||||
|
@apply bg-isa-secondary-600 text-isa-white;
|
||||||
|
|
||||||
|
.ui-snackbar__icon {
|
||||||
|
@apply text-isa-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-snackbar__action {
|
||||||
|
@apply text-isa-white hover:bg-isa-secondary-700 hover:bg-opacity-20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-snackbar__dismiss {
|
||||||
|
@apply text-isa-white hover:bg-isa-secondary-700 hover:bg-opacity-20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-snackbar__secondary {
|
||||||
|
@apply border border-solid border-isa-secondary-600 text-isa-secondary-600 bg-white;
|
||||||
|
|
||||||
|
.ui-snackbar__icon {
|
||||||
|
@apply text-isa-secondary-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-snackbar__action {
|
||||||
|
@apply text-isa-secondary-600 hover:bg-isa-neutral-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-snackbar__dismiss {
|
||||||
|
@apply text-isa-secondary-600 hover:bg-isa-neutral-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-snackbar__brand {
|
||||||
|
@apply bg-isa-accent-red text-isa-white;
|
||||||
|
|
||||||
|
.ui-snackbar__icon {
|
||||||
|
@apply text-isa-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-snackbar__action {
|
||||||
|
@apply text-isa-white hover:bg-isa-shades-red-600 hover:bg-opacity-20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-snackbar__dismiss {
|
||||||
|
@apply text-isa-white hover:bg-isa-shades-red-600 hover:bg-opacity-20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-snackbar__tertiary {
|
||||||
|
@apply bg-isa-neutral-300 text-isa-neutral-900;
|
||||||
|
|
||||||
|
.ui-snackbar__icon {
|
||||||
|
@apply text-isa-neutral-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-snackbar__action {
|
||||||
|
@apply text-isa-neutral-900 hover:bg-isa-neutral-400 hover:bg-opacity-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-snackbar__dismiss {
|
||||||
|
@apply text-isa-neutral-700 hover:bg-isa-neutral-400 hover:bg-opacity-50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animations
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOut {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position-specific animations
|
||||||
|
.ui-snackbar-overlay--top-left,
|
||||||
|
.ui-snackbar-overlay--bottom-left {
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOut {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
libs/ui/snackbar/src/lib/snackbar-ref.ts
Normal file
38
libs/ui/snackbar/src/lib/snackbar-ref.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { SnackbarComponent } from './snackbar.component';
|
||||||
|
import { InternalSnackbarData } from './snackbar.types';
|
||||||
|
import { ComponentRef } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a snackbar instance.
|
||||||
|
* Allows programmatic control over the snackbar.
|
||||||
|
*/
|
||||||
|
export class SnackbarRef {
|
||||||
|
readonly id = crypto.randomUUID();
|
||||||
|
|
||||||
|
/** Subject that emits when the snackbar is dismissed */
|
||||||
|
#dismissed = new Subject<void>();
|
||||||
|
|
||||||
|
#componentRef: ComponentRef<SnackbarComponent> | undefined;
|
||||||
|
|
||||||
|
get componentRef() {
|
||||||
|
return this.#componentRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Observable that emits after the snackbar is dismissed */
|
||||||
|
readonly dismissed$ = this.#dismissed.asObservable();
|
||||||
|
|
||||||
|
constructor(public readonly data: Readonly<InternalSnackbarData>) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismisses the snackbar
|
||||||
|
*/
|
||||||
|
dismiss(): void {
|
||||||
|
this.#dismissed.next();
|
||||||
|
this.#dismissed.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
_setComponentRef(componentRef: ComponentRef<SnackbarComponent>): void {
|
||||||
|
this.#componentRef = componentRef;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
libs/ui/snackbar/src/lib/snackbar.component.html
Normal file
24
libs/ui/snackbar/src/lib/snackbar.component.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<div class="ui-snackbar__content">
|
||||||
|
@if (icon()) {
|
||||||
|
<ng-icon name="snackbarIcon" class="ui-snackbar__icon" aria-hidden="true">
|
||||||
|
</ng-icon>
|
||||||
|
}
|
||||||
|
|
||||||
|
<span class="ui-snackbar__message">{{ message() }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (actions().length > 0) {
|
||||||
|
<div class="ui-snackbar__actions">
|
||||||
|
@for (action of actions(); track action.label) {
|
||||||
|
<button
|
||||||
|
uiTextButton
|
||||||
|
[color]="action.color === 'secondary' ? 'strong' : 'normal'"
|
||||||
|
size="medium"
|
||||||
|
(click)="onAction(action)"
|
||||||
|
class="ui-snackbar__action"
|
||||||
|
>
|
||||||
|
{{ action.label }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
84
libs/ui/snackbar/src/lib/snackbar.component.ts
Normal file
84
libs/ui/snackbar/src/lib/snackbar.component.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
computed,
|
||||||
|
inject,
|
||||||
|
ViewEncapsulation,
|
||||||
|
HostListener,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||||
|
import { TextButtonComponent, IconButtonComponent } from '@isa/ui/buttons';
|
||||||
|
import { SNACKBAR_DATA, SNACKBAR_REF } from './snackbar.tokens';
|
||||||
|
import { SnackbarAction } from './snackbar.types';
|
||||||
|
import { isaActionClose } from '@isa/icons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Individual snackbar component that displays messages with optional actions.
|
||||||
|
* Follows the Button component styling patterns for visual consistency.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ui-snackbar',
|
||||||
|
templateUrl: './snackbar.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: true,
|
||||||
|
imports: [NgIconComponent, TextButtonComponent],
|
||||||
|
providers: [provideIcons({ isaActionClose })],
|
||||||
|
host: {
|
||||||
|
'[class]': '["ui-snackbar", typeClass(), sizeClass()]',
|
||||||
|
'[attr.role]': '"alert"',
|
||||||
|
'[attr.aria-live]': '"polite"',
|
||||||
|
'[attr.data-what]': '"snackbar"',
|
||||||
|
'[attr.data-which]': 'data.type',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export class SnackbarComponent {
|
||||||
|
/** Injected snackbar data */
|
||||||
|
readonly data = inject(SNACKBAR_DATA);
|
||||||
|
|
||||||
|
/** Injected snackbar reference */
|
||||||
|
private readonly snackbarRef = inject(SNACKBAR_REF);
|
||||||
|
|
||||||
|
/** The type/color theme of the snackbar */
|
||||||
|
type = computed(() => this.data.type);
|
||||||
|
|
||||||
|
/** The size of the snackbar */
|
||||||
|
size = computed(() => this.data.size);
|
||||||
|
|
||||||
|
/** Computed CSS class based on the type */
|
||||||
|
typeClass = computed(() => `ui-snackbar__${this.type()}`);
|
||||||
|
|
||||||
|
/** Computed CSS class based on the size */
|
||||||
|
sizeClass = computed(() => `ui-snackbar__${this.size()}`);
|
||||||
|
|
||||||
|
/** The message to display */
|
||||||
|
message = computed(() => this.data.message);
|
||||||
|
|
||||||
|
/** Optional icon to display */
|
||||||
|
icon = computed(() => {
|
||||||
|
return this.data.icon || undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Action buttons configuration */
|
||||||
|
actions = computed(() => this.data.actions || []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles action button clicks
|
||||||
|
*/
|
||||||
|
onAction(action: SnackbarAction): void {
|
||||||
|
// this.snackbarRef._triggerAction(action.label);
|
||||||
|
action.handler();
|
||||||
|
|
||||||
|
// Dismiss snackbar after action if configured
|
||||||
|
if (!action.color || action.color === 'primary') {
|
||||||
|
this.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismisses the snackbar
|
||||||
|
*/
|
||||||
|
dismiss(): void {
|
||||||
|
this.snackbarRef.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
224
libs/ui/snackbar/src/lib/snackbar.service.ts
Normal file
224
libs/ui/snackbar/src/lib/snackbar.service.ts
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import { Injectable, inject, Injector, Provider } from '@angular/core';
|
||||||
|
import { Overlay, OverlayRef, PositionStrategy } from '@angular/cdk/overlay';
|
||||||
|
import { ComponentPortal } from '@angular/cdk/portal';
|
||||||
|
import {
|
||||||
|
InternalSnackbarData,
|
||||||
|
SnackbarPosition,
|
||||||
|
SnackbarConfig,
|
||||||
|
DEFAULT_SNACKBAR_CONFIG,
|
||||||
|
SnackbarData,
|
||||||
|
} from './snackbar.types';
|
||||||
|
import { SnackbarRef } from './snackbar-ref';
|
||||||
|
import { SnackbarComponent } from './snackbar.component';
|
||||||
|
import { SNACKBAR_DATA, SNACKBAR_REF } from './snackbar.tokens';
|
||||||
|
import { provideIcons } from '@ng-icons/core';
|
||||||
|
|
||||||
|
const POSITION_DISTANCE = '1rem';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for displaying snackbar notifications.
|
||||||
|
* Manages overlay creation, positioning, and snackbar lifecycle.
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class SnackbarService {
|
||||||
|
#overlay = inject(Overlay);
|
||||||
|
#injector = inject(Injector);
|
||||||
|
|
||||||
|
/** Snackbar Ref Queue */
|
||||||
|
#snackbarRefQueue: SnackbarRef[] = [];
|
||||||
|
|
||||||
|
/** Currently displayed snackbar reference */
|
||||||
|
#currentSnackbarRef: SnackbarRef | null = null;
|
||||||
|
|
||||||
|
show(data: SnackbarData): SnackbarRef {
|
||||||
|
// Apply default configuration
|
||||||
|
const d: InternalSnackbarData = {
|
||||||
|
...DEFAULT_SNACKBAR_CONFIG,
|
||||||
|
...data,
|
||||||
|
panelClass: data.panelClass || [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create SnackbarRef with enhanced dismiss callback
|
||||||
|
const snackbarRef = new SnackbarRef(d);
|
||||||
|
|
||||||
|
// Check if another snackbar is currently displayed
|
||||||
|
if (this.#currentSnackbarRef) {
|
||||||
|
// Add to queue
|
||||||
|
this.#snackbarRefQueue.push(snackbarRef);
|
||||||
|
return snackbarRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display immediately
|
||||||
|
this.#displaySnackbar(snackbarRef);
|
||||||
|
return snackbarRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismisses a specific snackbar by ID
|
||||||
|
*/
|
||||||
|
dismiss(snackbarId: string): void {
|
||||||
|
// Check if it's the currently displayed snackbar
|
||||||
|
if (
|
||||||
|
this.#currentSnackbarRef?.id === snackbarId &&
|
||||||
|
this.#currentSnackbarRef
|
||||||
|
) {
|
||||||
|
this.#currentSnackbarRef.dismiss();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's in the queue
|
||||||
|
const queueIndex = this.#snackbarRefQueue.findIndex(
|
||||||
|
(e) => e.id === snackbarId,
|
||||||
|
);
|
||||||
|
if (queueIndex !== -1) {
|
||||||
|
// Remove from queue and clean up
|
||||||
|
this.#snackbarRefQueue.splice(queueIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismisses all snackbars (current and queued)
|
||||||
|
*/
|
||||||
|
dismissAll(): void {
|
||||||
|
// Dismiss current snackbar
|
||||||
|
if (this.#currentSnackbarRef) {
|
||||||
|
this.#currentSnackbarRef.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the queue
|
||||||
|
this.#snackbarRefQueue.forEach((ref) => ref.dismiss());
|
||||||
|
}
|
||||||
|
|
||||||
|
#createOverlayRef(position: SnackbarPosition): OverlayRef {
|
||||||
|
// Create a new overlay if it doesn't exist
|
||||||
|
const overlayRef = this.#overlay.create({
|
||||||
|
positionStrategy: this.#createPositionStrategy(position),
|
||||||
|
panelClass: `ui-snackbar__${position}`,
|
||||||
|
hasBackdrop: false,
|
||||||
|
scrollStrategy: this.#overlay.scrollStrategies.reposition(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return overlayRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
#createPositionStrategy(
|
||||||
|
position: SnackbarPosition,
|
||||||
|
): PositionStrategy | undefined {
|
||||||
|
switch (position) {
|
||||||
|
case 'top-right':
|
||||||
|
return this.#overlay
|
||||||
|
.position()
|
||||||
|
.global()
|
||||||
|
.top(POSITION_DISTANCE)
|
||||||
|
.right(POSITION_DISTANCE);
|
||||||
|
case 'top-left':
|
||||||
|
return this.#overlay
|
||||||
|
.position()
|
||||||
|
.global()
|
||||||
|
.top(POSITION_DISTANCE)
|
||||||
|
.left(POSITION_DISTANCE);
|
||||||
|
case 'bottom-right':
|
||||||
|
return this.#overlay
|
||||||
|
.position()
|
||||||
|
.global()
|
||||||
|
.bottom(POSITION_DISTANCE)
|
||||||
|
.right(POSITION_DISTANCE);
|
||||||
|
case 'bottom-left':
|
||||||
|
return this.#overlay
|
||||||
|
.position()
|
||||||
|
.global()
|
||||||
|
.bottom(POSITION_DISTANCE)
|
||||||
|
.left(POSITION_DISTANCE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a snackbar immediately
|
||||||
|
*/
|
||||||
|
#displaySnackbar(snackbarRef: SnackbarRef): void {
|
||||||
|
if (this.#currentSnackbarRef) {
|
||||||
|
console.warn(
|
||||||
|
'Another snackbar is already displayed. Please wait until it is dismissed.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set current snackbar tracking
|
||||||
|
this.#currentSnackbarRef = snackbarRef;
|
||||||
|
|
||||||
|
// Get overlay reference
|
||||||
|
const overlayRef = this.#createOverlayRef(snackbarRef.data.position);
|
||||||
|
|
||||||
|
// Check if overlay is disposed or invalid
|
||||||
|
if (overlayRef.hasAttached()) {
|
||||||
|
console.warn(
|
||||||
|
'Overlay reference is invalid or already has content attached',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create component portal
|
||||||
|
const componentPortal = new ComponentPortal(
|
||||||
|
SnackbarComponent,
|
||||||
|
null,
|
||||||
|
this.#createInjector(snackbarRef.data, snackbarRef),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Attach to overlay
|
||||||
|
const componentRef = overlayRef.attach(componentPortal);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
snackbarRef.dismiss();
|
||||||
|
}, snackbarRef.data.duration);
|
||||||
|
|
||||||
|
snackbarRef._setComponentRef(componentRef);
|
||||||
|
|
||||||
|
snackbarRef.dismissed$.subscribe(() => {
|
||||||
|
overlayRef.detach();
|
||||||
|
|
||||||
|
this.#currentSnackbarRef = null;
|
||||||
|
this.#processQueue();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the queue after current snackbar is dismissed
|
||||||
|
*/
|
||||||
|
#processQueue(): void {
|
||||||
|
if (this.#snackbarRefQueue.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get next snackbar from queue
|
||||||
|
const nextSnackbarRef = this.#snackbarRefQueue.shift();
|
||||||
|
if (!nextSnackbarRef) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#displaySnackbar(nextSnackbarRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an injector for the snackbar component
|
||||||
|
*/
|
||||||
|
#createInjector(
|
||||||
|
data: InternalSnackbarData,
|
||||||
|
snackbarRef: SnackbarRef,
|
||||||
|
): Injector {
|
||||||
|
const providers: Provider[] = [
|
||||||
|
{ provide: SNACKBAR_DATA, useValue: data },
|
||||||
|
{ provide: SNACKBAR_REF, useValue: snackbarRef },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (data.icon) {
|
||||||
|
providers.push(provideIcons({ snackbarIcon: data.icon }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Injector.create({
|
||||||
|
parent: this.#injector,
|
||||||
|
providers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
15
libs/ui/snackbar/src/lib/snackbar.tokens.ts
Normal file
15
libs/ui/snackbar/src/lib/snackbar.tokens.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { InjectionToken } from '@angular/core';
|
||||||
|
import { InternalSnackbarData } from './snackbar.types';
|
||||||
|
import { SnackbarRef } from './snackbar-ref';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection token for the snackbar data
|
||||||
|
*/
|
||||||
|
export const SNACKBAR_DATA = new InjectionToken<InternalSnackbarData>(
|
||||||
|
'SNACKBAR_DATA',
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection token for the snackbar reference
|
||||||
|
*/
|
||||||
|
export const SNACKBAR_REF = new InjectionToken<SnackbarRef>('SNACKBAR_REF');
|
||||||
91
libs/ui/snackbar/src/lib/snackbar.types.ts
Normal file
91
libs/ui/snackbar/src/lib/snackbar.types.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Available snackbar color types following the Button component patterns
|
||||||
|
*/
|
||||||
|
export const SnackbarType = {
|
||||||
|
Primary: 'primary',
|
||||||
|
Secondary: 'secondary',
|
||||||
|
Brand: 'brand',
|
||||||
|
Tertiary: 'tertiary',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type SnackbarType = (typeof SnackbarType)[keyof typeof SnackbarType];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Available snackbar sizes following the Button component patterns
|
||||||
|
*/
|
||||||
|
export const SnackbarSize = {
|
||||||
|
Small: 'small',
|
||||||
|
Medium: 'medium',
|
||||||
|
Large: 'large',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type SnackbarSize = (typeof SnackbarSize)[keyof typeof SnackbarSize];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Available snackbar positions
|
||||||
|
*/
|
||||||
|
export const SnackbarPosition = {
|
||||||
|
TopRight: 'top-right',
|
||||||
|
BottomRight: 'bottom-right',
|
||||||
|
BottomLeft: 'bottom-left',
|
||||||
|
TopLeft: 'top-left',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type SnackbarPosition =
|
||||||
|
(typeof SnackbarPosition)[keyof typeof SnackbarPosition];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for a snackbar action button
|
||||||
|
*/
|
||||||
|
export interface SnackbarAction {
|
||||||
|
/** Label text for the action button */
|
||||||
|
label: string;
|
||||||
|
/** Handler function called when the action is clicked */
|
||||||
|
handler: () => void;
|
||||||
|
/** Optional color for the action button */
|
||||||
|
color?: 'primary' | 'secondary';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration options for creating a snackbar
|
||||||
|
*/
|
||||||
|
export interface SnackbarConfig {
|
||||||
|
/** The type/color theme of the snackbar */
|
||||||
|
type?: SnackbarType;
|
||||||
|
/** The size of the snackbar */
|
||||||
|
size?: SnackbarSize;
|
||||||
|
/** Duration in milliseconds before auto-dismiss (0 = no auto-dismiss) */
|
||||||
|
duration?: number;
|
||||||
|
/** Position of the snackbar on screen */
|
||||||
|
position?: SnackbarPosition;
|
||||||
|
|
||||||
|
/** Additional CSS classes to apply */
|
||||||
|
panelClass?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SnackbarData extends SnackbarConfig {
|
||||||
|
/** The message to display */
|
||||||
|
message: string;
|
||||||
|
/** Optional icon name to display */
|
||||||
|
icon?: string;
|
||||||
|
|
||||||
|
/** Action buttons to display in the snackbar */
|
||||||
|
actions?: SnackbarAction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data passed to a snackbar component instance
|
||||||
|
*/
|
||||||
|
export type InternalSnackbarData = SnackbarData & Required<SnackbarConfig>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default configuration values for snackbars
|
||||||
|
*/
|
||||||
|
export const DEFAULT_SNACKBAR_CONFIG: Required<
|
||||||
|
Omit<SnackbarConfig, 'actions' | 'icon' | 'panelClass'>
|
||||||
|
> = {
|
||||||
|
type: SnackbarType.Primary,
|
||||||
|
size: SnackbarSize.Medium,
|
||||||
|
duration: 5000,
|
||||||
|
position: SnackbarPosition.BottomRight,
|
||||||
|
};
|
||||||
1
libs/ui/snackbar/src/snackbar.scss
Normal file
1
libs/ui/snackbar/src/snackbar.scss
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@import './lib/snackbar';
|
||||||
13
libs/ui/snackbar/src/test-setup.ts
Normal file
13
libs/ui/snackbar/src/test-setup.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import '@angular/compiler';
|
||||||
|
import '@analogjs/vitest-angular/setup-zone';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BrowserTestingModule,
|
||||||
|
platformBrowserTesting,
|
||||||
|
} from '@angular/platform-browser/testing';
|
||||||
|
import { getTestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
getTestBed().initTestEnvironment(
|
||||||
|
BrowserTestingModule,
|
||||||
|
platformBrowserTesting(),
|
||||||
|
);
|
||||||
30
libs/ui/snackbar/tsconfig.json
Normal file
30
libs/ui/snackbar/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"importHelpers": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"module": "preserve"
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"typeCheckHostBindings": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.lib.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
27
libs/ui/snackbar/tsconfig.lib.json
Normal file
27
libs/ui/snackbar/tsconfig.lib.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../../dist/out-tsc",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"inlineSources": true,
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/test-setup.ts",
|
||||||
|
"jest.config.ts",
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"vite.config.ts",
|
||||||
|
"vite.config.mts",
|
||||||
|
"vitest.config.ts",
|
||||||
|
"vitest.config.mts",
|
||||||
|
"src/**/*.test.tsx",
|
||||||
|
"src/**/*.spec.tsx",
|
||||||
|
"src/**/*.test.js",
|
||||||
|
"src/**/*.spec.js",
|
||||||
|
"src/**/*.test.jsx",
|
||||||
|
"src/**/*.spec.jsx"
|
||||||
|
],
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
29
libs/ui/snackbar/tsconfig.spec.json
Normal file
29
libs/ui/snackbar/tsconfig.spec.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../../dist/out-tsc",
|
||||||
|
"types": [
|
||||||
|
"vitest/globals",
|
||||||
|
"vitest/importMeta",
|
||||||
|
"vite/client",
|
||||||
|
"node",
|
||||||
|
"vitest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"vite.config.ts",
|
||||||
|
"vite.config.mts",
|
||||||
|
"vitest.config.ts",
|
||||||
|
"vitest.config.mts",
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.test.tsx",
|
||||||
|
"src/**/*.spec.tsx",
|
||||||
|
"src/**/*.test.js",
|
||||||
|
"src/**/*.spec.js",
|
||||||
|
"src/**/*.test.jsx",
|
||||||
|
"src/**/*.spec.jsx",
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
],
|
||||||
|
"files": ["src/test-setup.ts"]
|
||||||
|
}
|
||||||
27
libs/ui/snackbar/vite.config.mts
Normal file
27
libs/ui/snackbar/vite.config.mts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/// <reference types='vitest' />
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import angular from '@analogjs/vite-plugin-angular';
|
||||||
|
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||||
|
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||||
|
|
||||||
|
export default defineConfig(() => ({
|
||||||
|
root: __dirname,
|
||||||
|
cacheDir: '../../../node_modules/.vite/libs/ui/snackbar',
|
||||||
|
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||||
|
// Uncomment this if you are using workers.
|
||||||
|
// worker: {
|
||||||
|
// plugins: [ nxViteTsPaths() ],
|
||||||
|
// },
|
||||||
|
test: {
|
||||||
|
watch: false,
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||||
|
setupFiles: ['src/test-setup.ts'],
|
||||||
|
reporters: ['default'],
|
||||||
|
coverage: {
|
||||||
|
reportsDirectory: '../../../coverage/libs/ui/snackbar',
|
||||||
|
provider: 'v8' as const,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -99,6 +99,7 @@
|
|||||||
"@isa/ui/progress-bar": ["libs/ui/progress-bar/src/index.ts"],
|
"@isa/ui/progress-bar": ["libs/ui/progress-bar/src/index.ts"],
|
||||||
"@isa/ui/search-bar": ["libs/ui/search-bar/src/index.ts"],
|
"@isa/ui/search-bar": ["libs/ui/search-bar/src/index.ts"],
|
||||||
"@isa/ui/skeleton-loader": ["libs/ui/skeleton-loader/src/index.ts"],
|
"@isa/ui/skeleton-loader": ["libs/ui/skeleton-loader/src/index.ts"],
|
||||||
|
"@isa/ui/snackbar": ["libs/ui/snackbar/src/index.ts"],
|
||||||
"@isa/ui/toolbar": ["libs/ui/toolbar/src/index.ts"],
|
"@isa/ui/toolbar": ["libs/ui/toolbar/src/index.ts"],
|
||||||
"@isa/ui/tooltip": ["libs/ui/tooltip/src/index.ts"],
|
"@isa/ui/tooltip": ["libs/ui/tooltip/src/index.ts"],
|
||||||
"@isa/utils/scroll-position": ["libs/utils/scroll-position/src/index.ts"],
|
"@isa/utils/scroll-position": ["libs/utils/scroll-position/src/index.ts"],
|
||||||
|
|||||||
Reference in New Issue
Block a user