@lumaui/angular 0.1.4 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/lumaui-angular.mjs +2557 -27
- package/fesm2022/lumaui-angular.mjs.map +1 -1
- package/package.json +3 -3
- package/types/lumaui-angular.d.ts +1210 -20
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { input, computed, HostBinding, Directive, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
-
import { buttonVariants, cardVariants, cardContentVariants, cardTitleVariants, cardDescriptionVariants } from '@lumaui/core';
|
|
2
|
+
import { input, computed, HostBinding, Directive, ChangeDetectionStrategy, Component, output, InjectionToken, inject, ElementRef, Renderer2, effect, signal, HostListener, PLATFORM_ID, TemplateRef, ViewContainerRef, ApplicationRef, Injector, createComponent, Injectable } from '@angular/core';
|
|
3
|
+
import { buttonVariants, badgeVariants, cardVariants, cardContentVariants, cardTitleVariants, cardDescriptionVariants, accordionItemVariants, accordionContentWrapperVariants, accordionTriggerVariants, accordionTitleVariants, accordionIconVariants, accordionContentVariants, tooltipVariants, tabsListVariants, tabsTriggerVariants, tabsPanelVariants, tabsIndicatorVariants, modalOverlayVariants, modalContainerVariants, modalHeaderVariants, modalTitleVariants, modalContentVariants, modalFooterVariants, modalCloseVariants, toastCloseVariants, toastItemVariants, toastIconVariants, toastContentVariants, toastTitleVariants, toastMessageVariants, toastContainerVariants } from '@lumaui/core';
|
|
4
|
+
import { isPlatformBrowser, DOCUMENT } from '@angular/common';
|
|
5
|
+
import { LiveAnnouncer } from '@angular/cdk/a11y';
|
|
6
|
+
import { Subject, interval } from 'rxjs';
|
|
7
|
+
import { takeWhile, filter } from 'rxjs/operators';
|
|
4
8
|
|
|
5
|
-
class
|
|
9
|
+
class LmButtonDirective {
|
|
6
10
|
// Signal-based inputs with lm prefix (Angular 20+)
|
|
7
11
|
lmVariant = input('primary', ...(ngDevMode ? [{ debugName: "lmVariant" }] : []));
|
|
8
12
|
lmSize = input('md', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
|
|
@@ -16,10 +20,10 @@ class ButtonDirective {
|
|
|
16
20
|
get hostClasses() {
|
|
17
21
|
return this.classes();
|
|
18
22
|
}
|
|
19
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type:
|
|
20
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type:
|
|
23
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmButtonDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
24
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmButtonDirective, isStandalone: true, selector: "button[lumaButton], a[lumaButton]", inputs: { lmVariant: { classPropertyName: "lmVariant", publicName: "lmVariant", isSignal: true, isRequired: false, transformFunction: null }, lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null }, lmDisabled: { classPropertyName: "lmDisabled", publicName: "lmDisabled", isSignal: true, isRequired: false, transformFunction: null }, lmType: { classPropertyName: "lmType", publicName: "lmType", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.type": "lmType()", "attr.disabled": "lmDisabled() ? \"\" : null", "class": "this.hostClasses" } }, ngImport: i0 });
|
|
21
25
|
}
|
|
22
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type:
|
|
26
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmButtonDirective, decorators: [{
|
|
23
27
|
type: Directive,
|
|
24
28
|
args: [{
|
|
25
29
|
selector: 'button[lumaButton], a[lumaButton]',
|
|
@@ -33,7 +37,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
|
|
|
33
37
|
args: ['class']
|
|
34
38
|
}] } });
|
|
35
39
|
|
|
36
|
-
class
|
|
40
|
+
class LmBadgeDirective {
|
|
41
|
+
// Computed class string - layout only, no variants
|
|
42
|
+
classes = computed(() => badgeVariants(), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
43
|
+
get hostClasses() {
|
|
44
|
+
return this.classes();
|
|
45
|
+
}
|
|
46
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmBadgeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
47
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type: LmBadgeDirective, isStandalone: true, selector: "[lumaBadge]", host: { properties: { "class": "this.hostClasses" } }, ngImport: i0 });
|
|
48
|
+
}
|
|
49
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmBadgeDirective, decorators: [{
|
|
50
|
+
type: Directive,
|
|
51
|
+
args: [{
|
|
52
|
+
selector: '[lumaBadge]',
|
|
53
|
+
}]
|
|
54
|
+
}], propDecorators: { hostClasses: [{
|
|
55
|
+
type: HostBinding,
|
|
56
|
+
args: ['class']
|
|
57
|
+
}] } });
|
|
58
|
+
|
|
59
|
+
class LmCardComponent {
|
|
37
60
|
/**
|
|
38
61
|
* Card visual style variant
|
|
39
62
|
* - default: Gradient border wrapper style (default)
|
|
@@ -45,23 +68,23 @@ class CardComponent {
|
|
|
45
68
|
// Computed class strings based on variant
|
|
46
69
|
wrapperClasses = computed(() => cardVariants({ variant: this.lmVariant() }), ...(ngDevMode ? [{ debugName: "wrapperClasses" }] : []));
|
|
47
70
|
contentClasses = computed(() => cardContentVariants({ variant: this.lmVariant() }), ...(ngDevMode ? [{ debugName: "contentClasses" }] : []));
|
|
48
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type:
|
|
49
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.9", type:
|
|
71
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
72
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.9", type: LmCardComponent, isStandalone: true, selector: "luma-card", inputs: { lmVariant: { classPropertyName: "lmVariant", publicName: "lmVariant", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div [class]=\"wrapperClasses()\">\n <div [class]=\"contentClasses()\">\n <ng-content></ng-content>\n </div>\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
50
73
|
}
|
|
51
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type:
|
|
74
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmCardComponent, decorators: [{
|
|
52
75
|
type: Component,
|
|
53
76
|
args: [{ selector: 'luma-card', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div [class]=\"wrapperClasses()\">\n <div [class]=\"contentClasses()\">\n <ng-content></ng-content>\n </div>\n</div>\n" }]
|
|
54
77
|
}], propDecorators: { lmVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmVariant", required: false }] }] } });
|
|
55
78
|
|
|
56
|
-
class
|
|
79
|
+
class LmCardTitleDirective {
|
|
57
80
|
// Signal-based inputs with lm prefix (Angular 20+)
|
|
58
81
|
lmSize = input('normal', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
|
|
59
82
|
// Computed class string
|
|
60
83
|
classes = computed(() => cardTitleVariants({ size: this.lmSize() }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
61
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type:
|
|
62
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type:
|
|
84
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmCardTitleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
85
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmCardTitleDirective, isStandalone: true, selector: "[lumaCardTitle]", inputs: { lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
|
|
63
86
|
}
|
|
64
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type:
|
|
87
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmCardTitleDirective, decorators: [{
|
|
65
88
|
type: Directive,
|
|
66
89
|
args: [{
|
|
67
90
|
selector: '[lumaCardTitle]',
|
|
@@ -71,15 +94,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
|
|
|
71
94
|
}]
|
|
72
95
|
}], propDecorators: { lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }] } });
|
|
73
96
|
|
|
74
|
-
class
|
|
97
|
+
class LmCardDescriptionDirective {
|
|
75
98
|
// Signal-based inputs with lm prefix (Angular 20+)
|
|
76
99
|
lmSize = input('normal', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
|
|
77
100
|
// Computed class string
|
|
78
101
|
classes = computed(() => cardDescriptionVariants({ size: this.lmSize() }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
79
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type:
|
|
80
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type:
|
|
102
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmCardDescriptionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
103
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmCardDescriptionDirective, isStandalone: true, selector: "[lumaCardDescription]", inputs: { lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
|
|
81
104
|
}
|
|
82
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type:
|
|
105
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmCardDescriptionDirective, decorators: [{
|
|
83
106
|
type: Directive,
|
|
84
107
|
args: [{
|
|
85
108
|
selector: '[lumaCardDescription]',
|
|
@@ -89,11 +112,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
|
|
|
89
112
|
}]
|
|
90
113
|
}], propDecorators: { lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }] } });
|
|
91
114
|
|
|
92
|
-
class
|
|
93
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type:
|
|
94
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type:
|
|
115
|
+
class LmCardHeaderDirective {
|
|
116
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmCardHeaderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
117
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type: LmCardHeaderDirective, isStandalone: true, selector: "[lumaCardHeader]", host: { classAttribute: "mb-4" }, ngImport: i0 });
|
|
95
118
|
}
|
|
96
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type:
|
|
119
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmCardHeaderDirective, decorators: [{
|
|
97
120
|
type: Directive,
|
|
98
121
|
args: [{
|
|
99
122
|
selector: '[lumaCardHeader]',
|
|
@@ -103,11 +126,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
|
|
|
103
126
|
}]
|
|
104
127
|
}] });
|
|
105
128
|
|
|
106
|
-
class
|
|
107
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type:
|
|
108
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type:
|
|
129
|
+
class LmCardContentDirective {
|
|
130
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmCardContentDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
131
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type: LmCardContentDirective, isStandalone: true, selector: "[lumaCardContent]", ngImport: i0 });
|
|
109
132
|
}
|
|
110
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type:
|
|
133
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmCardContentDirective, decorators: [{
|
|
111
134
|
type: Directive,
|
|
112
135
|
args: [{
|
|
113
136
|
selector: '[lumaCardContent]',
|
|
@@ -117,11 +140,2518 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
|
|
|
117
140
|
}]
|
|
118
141
|
}] });
|
|
119
142
|
|
|
143
|
+
/**
|
|
144
|
+
* AccordionGroupComponent
|
|
145
|
+
*
|
|
146
|
+
* Optional wrapper component that coordinates multiple accordion items.
|
|
147
|
+
* Supports controlled pattern for implementing business logic like:
|
|
148
|
+
* - Single item open at a time
|
|
149
|
+
* - Multiple items open
|
|
150
|
+
* - Always keep first/last item open
|
|
151
|
+
* - Maximum number of open items
|
|
152
|
+
*
|
|
153
|
+
* @example Controlled single mode
|
|
154
|
+
* ```html
|
|
155
|
+
* <luma-accordion-group [lmValue]="activeItem()" (lmValueChange)="activeItem.set($event)">
|
|
156
|
+
* <luma-accordion-item lmId="item-1">...</luma-accordion-item>
|
|
157
|
+
* <luma-accordion-item lmId="item-2">...</luma-accordion-item>
|
|
158
|
+
* </luma-accordion-group>
|
|
159
|
+
* ```
|
|
160
|
+
*
|
|
161
|
+
* @example Controlled multiple mode
|
|
162
|
+
* ```html
|
|
163
|
+
* <luma-accordion-group [lmValue]="activeItems()" (lmValueChange)="activeItems.set($event)">
|
|
164
|
+
* <luma-accordion-item lmId="item-1">...</luma-accordion-item>
|
|
165
|
+
* <luma-accordion-item lmId="item-2">...</luma-accordion-item>
|
|
166
|
+
* </luma-accordion-group>
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
class LmAccordionGroupComponent {
|
|
170
|
+
/**
|
|
171
|
+
* Controlled value for which items are open
|
|
172
|
+
* - null: uncontrolled mode (each item manages its own state)
|
|
173
|
+
* - string: single item mode (ID of open item)
|
|
174
|
+
* - string[]: multiple items mode (IDs of open items)
|
|
175
|
+
*/
|
|
176
|
+
lmValue = input(null, ...(ngDevMode ? [{ debugName: "lmValue" }] : []));
|
|
177
|
+
/**
|
|
178
|
+
* Emitted when an item is toggled
|
|
179
|
+
* Returns the new value (string for single mode, string[] for multiple)
|
|
180
|
+
*/
|
|
181
|
+
lmValueChange = output();
|
|
182
|
+
/**
|
|
183
|
+
* Force single mode even when lmValue is an array
|
|
184
|
+
* When true, only one item can be open at a time
|
|
185
|
+
*/
|
|
186
|
+
lmSingle = input(false, ...(ngDevMode ? [{ debugName: "lmSingle" }] : []));
|
|
187
|
+
/**
|
|
188
|
+
* Toggle an item by its ID
|
|
189
|
+
* Called by child AccordionItemComponent when toggled
|
|
190
|
+
*/
|
|
191
|
+
toggleItem(itemId) {
|
|
192
|
+
const current = this.lmValue();
|
|
193
|
+
// If uncontrolled, do nothing (items manage their own state)
|
|
194
|
+
if (current === null || current === undefined) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
let newValue;
|
|
198
|
+
if (this.lmSingle() || typeof current === 'string') {
|
|
199
|
+
// Single mode: toggle between the item and empty
|
|
200
|
+
newValue = current === itemId ? '' : itemId;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// Multiple mode: add/remove from array
|
|
204
|
+
const arr = Array.isArray(current) ? [...current] : [];
|
|
205
|
+
const index = arr.indexOf(itemId);
|
|
206
|
+
if (index >= 0) {
|
|
207
|
+
arr.splice(index, 1);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
arr.push(itemId);
|
|
211
|
+
}
|
|
212
|
+
newValue = arr;
|
|
213
|
+
}
|
|
214
|
+
this.lmValueChange.emit(newValue);
|
|
215
|
+
}
|
|
216
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmAccordionGroupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
217
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.9", type: LmAccordionGroupComponent, isStandalone: true, selector: "luma-accordion-group", inputs: { lmValue: { classPropertyName: "lmValue", publicName: "lmValue", isSignal: true, isRequired: false, transformFunction: null }, lmSingle: { classPropertyName: "lmSingle", publicName: "lmSingle", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { lmValueChange: "lmValueChange" }, host: { properties: { "style.gap": "\"var(--luma-accordion-item-gap)\"" }, classAttribute: "flex flex-col" }, ngImport: i0, template: '<ng-content></ng-content>', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
218
|
+
}
|
|
219
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmAccordionGroupComponent, decorators: [{
|
|
220
|
+
type: Component,
|
|
221
|
+
args: [{
|
|
222
|
+
selector: 'luma-accordion-group',
|
|
223
|
+
template: '<ng-content></ng-content>',
|
|
224
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
225
|
+
host: {
|
|
226
|
+
class: 'flex flex-col',
|
|
227
|
+
'[style.gap]': '"var(--luma-accordion-item-gap)"',
|
|
228
|
+
},
|
|
229
|
+
}]
|
|
230
|
+
}], propDecorators: { lmValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmValue", required: false }] }], lmValueChange: [{ type: i0.Output, args: ["lmValueChange"] }], lmSingle: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSingle", required: false }] }] } });
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Injection token for accordion item
|
|
234
|
+
* Allows child directives to access parent accordion item state
|
|
235
|
+
*/
|
|
236
|
+
const ACCORDION_ITEM = new InjectionToken('AccordionItem');
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* AccordionItemComponent
|
|
240
|
+
*
|
|
241
|
+
* Wrapper component that contains the trigger and content.
|
|
242
|
+
* Can be used standalone or within an AccordionGroupComponent.
|
|
243
|
+
*
|
|
244
|
+
* @example Standalone usage
|
|
245
|
+
* ```html
|
|
246
|
+
* <luma-accordion-item>
|
|
247
|
+
* <button lumaAccordionTrigger>
|
|
248
|
+
* <span lumaAccordionTitle>Title</span>
|
|
249
|
+
* <svg lumaAccordionIcon>...</svg>
|
|
250
|
+
* </button>
|
|
251
|
+
* <div lumaAccordionContent>Content here...</div>
|
|
252
|
+
* </luma-accordion-item>
|
|
253
|
+
* ```
|
|
254
|
+
*
|
|
255
|
+
* @example With variants
|
|
256
|
+
* ```html
|
|
257
|
+
* <luma-accordion-item lmVariant="filled">
|
|
258
|
+
* ...
|
|
259
|
+
* </luma-accordion-item>
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
class LmAccordionItemComponent {
|
|
263
|
+
group = inject(LmAccordionGroupComponent, { optional: true });
|
|
264
|
+
el = inject(ElementRef);
|
|
265
|
+
renderer = inject(Renderer2);
|
|
266
|
+
previousClasses = [];
|
|
267
|
+
constructor() {
|
|
268
|
+
// Effect to reactively manage CVA classes without replacing user classes
|
|
269
|
+
effect(() => {
|
|
270
|
+
// Remove previous CVA classes
|
|
271
|
+
this.previousClasses.forEach((c) => {
|
|
272
|
+
this.renderer.removeClass(this.el.nativeElement, c);
|
|
273
|
+
});
|
|
274
|
+
// Add new CVA classes (preserves user-provided classes)
|
|
275
|
+
const newClasses = this.wrapperClasses()
|
|
276
|
+
.split(' ')
|
|
277
|
+
.filter((c) => c);
|
|
278
|
+
newClasses.forEach((c) => {
|
|
279
|
+
this.renderer.addClass(this.el.nativeElement, c);
|
|
280
|
+
});
|
|
281
|
+
this.previousClasses = newClasses;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Unique identifier for this item (required when using AccordionGroup)
|
|
286
|
+
*/
|
|
287
|
+
lmId = input('', ...(ngDevMode ? [{ debugName: "lmId" }] : []));
|
|
288
|
+
/**
|
|
289
|
+
* Visual style variant
|
|
290
|
+
* - default: Standard with border
|
|
291
|
+
* - bordered: FAQ-style stacked items
|
|
292
|
+
* - filled: Solid background (unified trigger/content)
|
|
293
|
+
*/
|
|
294
|
+
lmVariant = input('default', ...(ngDevMode ? [{ debugName: "lmVariant" }] : []));
|
|
295
|
+
/**
|
|
296
|
+
* Initial/controlled open state (for standalone usage)
|
|
297
|
+
*/
|
|
298
|
+
lmOpen = input(false, ...(ngDevMode ? [{ debugName: "lmOpen" }] : []));
|
|
299
|
+
/**
|
|
300
|
+
* Whether the accordion item is disabled
|
|
301
|
+
*/
|
|
302
|
+
lmDisabled = input(false, ...(ngDevMode ? [{ debugName: "lmDisabled" }] : []));
|
|
303
|
+
/**
|
|
304
|
+
* Emitted when the open state changes
|
|
305
|
+
* Useful for tracking/analytics
|
|
306
|
+
*/
|
|
307
|
+
lmOpenChange = output();
|
|
308
|
+
// Internal state for uncontrolled mode
|
|
309
|
+
_isOpen = signal(false, ...(ngDevMode ? [{ debugName: "_isOpen" }] : []));
|
|
310
|
+
/**
|
|
311
|
+
* Computed open state
|
|
312
|
+
* Priority: group controlled > lmOpen input > internal state
|
|
313
|
+
*/
|
|
314
|
+
isOpen = computed(() => {
|
|
315
|
+
// If in a controlled group, check group value
|
|
316
|
+
if (this.group?.lmValue() !== null && this.group?.lmValue() !== undefined) {
|
|
317
|
+
const value = this.group.lmValue();
|
|
318
|
+
const id = this.lmId();
|
|
319
|
+
return Array.isArray(value) ? value.includes(id) : value === id;
|
|
320
|
+
}
|
|
321
|
+
// Otherwise use input or internal state
|
|
322
|
+
return this.lmOpen() || this._isOpen();
|
|
323
|
+
}, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
|
|
324
|
+
// CVA classes
|
|
325
|
+
wrapperClasses = computed(() => accordionItemVariants({
|
|
326
|
+
variant: this.lmVariant(),
|
|
327
|
+
}), ...(ngDevMode ? [{ debugName: "wrapperClasses" }] : []));
|
|
328
|
+
contentWrapperClasses = computed(() => accordionContentWrapperVariants({ open: this.isOpen() }), ...(ngDevMode ? [{ debugName: "contentWrapperClasses" }] : []));
|
|
329
|
+
/**
|
|
330
|
+
* Toggle the accordion open/closed state
|
|
331
|
+
*/
|
|
332
|
+
toggle() {
|
|
333
|
+
if (this.lmDisabled())
|
|
334
|
+
return;
|
|
335
|
+
if (this.group && this.group.lmValue() !== null) {
|
|
336
|
+
// Controlled by group
|
|
337
|
+
this.group.toggleItem(this.lmId());
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
// Uncontrolled mode
|
|
341
|
+
this._isOpen.update((v) => !v);
|
|
342
|
+
}
|
|
343
|
+
this.lmOpenChange.emit(this.isOpen());
|
|
344
|
+
}
|
|
345
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmAccordionItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
346
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.9", type: LmAccordionItemComponent, isStandalone: true, selector: "luma-accordion-item", inputs: { lmId: { classPropertyName: "lmId", publicName: "lmId", isSignal: true, isRequired: false, transformFunction: null }, lmVariant: { classPropertyName: "lmVariant", publicName: "lmVariant", isSignal: true, isRequired: false, transformFunction: null }, lmOpen: { classPropertyName: "lmOpen", publicName: "lmOpen", isSignal: true, isRequired: false, transformFunction: null }, lmDisabled: { classPropertyName: "lmDisabled", publicName: "lmDisabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { lmOpenChange: "lmOpenChange" }, host: { properties: { "attr.data-state": "isOpen() ? \"open\" : \"closed\"", "attr.data-variant": "lmVariant()" } }, providers: [
|
|
347
|
+
{ provide: ACCORDION_ITEM, useExisting: LmAccordionItemComponent },
|
|
348
|
+
], ngImport: i0, template: "<!-- Slot for trigger button -->\n<ng-content select=\"[lumaAccordionTrigger]\"></ng-content>\n\n<!-- Content wrapper with grid-rows animation -->\n<div [class]=\"contentWrapperClasses()\">\n <div class=\"overflow-hidden\">\n <ng-content select=\"[lumaAccordionContent]\"></ng-content>\n </div>\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
349
|
+
}
|
|
350
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmAccordionItemComponent, decorators: [{
|
|
351
|
+
type: Component,
|
|
352
|
+
args: [{ selector: 'luma-accordion-item', changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
353
|
+
{ provide: ACCORDION_ITEM, useExisting: LmAccordionItemComponent },
|
|
354
|
+
], host: {
|
|
355
|
+
'[attr.data-state]': 'isOpen() ? "open" : "closed"',
|
|
356
|
+
'[attr.data-variant]': 'lmVariant()',
|
|
357
|
+
}, template: "<!-- Slot for trigger button -->\n<ng-content select=\"[lumaAccordionTrigger]\"></ng-content>\n\n<!-- Content wrapper with grid-rows animation -->\n<div [class]=\"contentWrapperClasses()\">\n <div class=\"overflow-hidden\">\n <ng-content select=\"[lumaAccordionContent]\"></ng-content>\n </div>\n</div>\n" }]
|
|
358
|
+
}], ctorParameters: () => [], propDecorators: { lmId: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmId", required: false }] }], lmVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmVariant", required: false }] }], lmOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmOpen", required: false }] }], lmDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDisabled", required: false }] }], lmOpenChange: [{ type: i0.Output, args: ["lmOpenChange"] }] } });
|
|
359
|
+
|
|
360
|
+
let uniqueId$2 = 0;
|
|
361
|
+
/**
|
|
362
|
+
* AccordionTriggerDirective
|
|
363
|
+
*
|
|
364
|
+
* Applied to a div element to make it the clickable trigger for the accordion.
|
|
365
|
+
* Uses div instead of button for maximum layout flexibility.
|
|
366
|
+
* Handles ARIA attributes and keyboard navigation automatically.
|
|
367
|
+
*
|
|
368
|
+
* @example Basic usage
|
|
369
|
+
* ```html
|
|
370
|
+
* <div lumaAccordionTrigger>
|
|
371
|
+
* <span lumaAccordionTitle>Title</span>
|
|
372
|
+
* <span lumaAccordionIcon>
|
|
373
|
+
* <svg>...</svg>
|
|
374
|
+
* </span>
|
|
375
|
+
* </div>
|
|
376
|
+
* ```
|
|
377
|
+
*
|
|
378
|
+
* @example Custom layout
|
|
379
|
+
* ```html
|
|
380
|
+
* <div lumaAccordionTrigger class="grid grid-cols-[auto_1fr_auto] gap-4">
|
|
381
|
+
* <svg class="w-6 h-6">...</svg>
|
|
382
|
+
* <div>
|
|
383
|
+
* <span lumaAccordionTitle>Title</span>
|
|
384
|
+
* <p class="text-sm">Description</p>
|
|
385
|
+
* </div>
|
|
386
|
+
* <span lumaAccordionIcon>
|
|
387
|
+
* <svg>...</svg>
|
|
388
|
+
* </span>
|
|
389
|
+
* </div>
|
|
390
|
+
* ```
|
|
391
|
+
*/
|
|
392
|
+
class LmAccordionTriggerDirective {
|
|
393
|
+
item = inject(ACCORDION_ITEM);
|
|
394
|
+
id = ++uniqueId$2;
|
|
395
|
+
triggerId = `luma-accordion-trigger-${this.id}`;
|
|
396
|
+
contentId = `luma-accordion-content-${this.id}`;
|
|
397
|
+
classes = computed(() => accordionTriggerVariants(), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
398
|
+
onClick(event) {
|
|
399
|
+
if (this.item.lmDisabled())
|
|
400
|
+
return;
|
|
401
|
+
event.preventDefault();
|
|
402
|
+
this.item.toggle();
|
|
403
|
+
}
|
|
404
|
+
onKeydown(event) {
|
|
405
|
+
if (this.item.lmDisabled())
|
|
406
|
+
return;
|
|
407
|
+
// Space and Enter don't trigger on div natively - manual handler required
|
|
408
|
+
if (event.code === 'Space' || event.code === 'Enter') {
|
|
409
|
+
event.preventDefault();
|
|
410
|
+
this.item.toggle();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmAccordionTriggerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
414
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type: LmAccordionTriggerDirective, isStandalone: true, selector: "div[lumaAccordionTrigger]", host: { attributes: { "role": "button" }, listeners: { "click": "onClick($event)", "keydown": "onKeydown($event)" }, properties: { "class": "classes()", "attr.tabindex": "item.lmDisabled() ? -1 : 0", "attr.aria-expanded": "item.isOpen()", "attr.aria-controls": "contentId", "attr.aria-disabled": "item.lmDisabled() ? \"true\" : null", "id": "triggerId" } }, ngImport: i0 });
|
|
415
|
+
}
|
|
416
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmAccordionTriggerDirective, decorators: [{
|
|
417
|
+
type: Directive,
|
|
418
|
+
args: [{
|
|
419
|
+
selector: 'div[lumaAccordionTrigger]',
|
|
420
|
+
host: {
|
|
421
|
+
'[class]': 'classes()',
|
|
422
|
+
role: 'button',
|
|
423
|
+
'[attr.tabindex]': 'item.lmDisabled() ? -1 : 0',
|
|
424
|
+
'[attr.aria-expanded]': 'item.isOpen()',
|
|
425
|
+
'[attr.aria-controls]': 'contentId',
|
|
426
|
+
'[attr.aria-disabled]': 'item.lmDisabled() ? "true" : null',
|
|
427
|
+
'[id]': 'triggerId',
|
|
428
|
+
},
|
|
429
|
+
}]
|
|
430
|
+
}], propDecorators: { onClick: [{
|
|
431
|
+
type: HostListener,
|
|
432
|
+
args: ['click', ['$event']]
|
|
433
|
+
}], onKeydown: [{
|
|
434
|
+
type: HostListener,
|
|
435
|
+
args: ['keydown', ['$event']]
|
|
436
|
+
}] } });
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* AccordionTitleDirective
|
|
440
|
+
*
|
|
441
|
+
* Applies typography styles to the accordion title.
|
|
442
|
+
* Supports size variants for different visual hierarchies.
|
|
443
|
+
*
|
|
444
|
+
* @example Basic usage
|
|
445
|
+
* ```html
|
|
446
|
+
* <span lumaAccordionTitle>What is Luma UI?</span>
|
|
447
|
+
* ```
|
|
448
|
+
*
|
|
449
|
+
* @example With size variant
|
|
450
|
+
* ```html
|
|
451
|
+
* <span lumaAccordionTitle lmSize="lg">Large Title</span>
|
|
452
|
+
* ```
|
|
453
|
+
*/
|
|
454
|
+
class LmAccordionTitleDirective {
|
|
455
|
+
/**
|
|
456
|
+
* Size variant for the title
|
|
457
|
+
* - sm: Small text for compact UIs
|
|
458
|
+
* - md: Default size (base text)
|
|
459
|
+
* - lg: Large text for emphasis
|
|
460
|
+
*/
|
|
461
|
+
lmSize = input('md', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
|
|
462
|
+
classes = computed(() => accordionTitleVariants({ size: this.lmSize() }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
463
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmAccordionTitleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
464
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmAccordionTitleDirective, isStandalone: true, selector: "[lumaAccordionTitle]", inputs: { lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
|
|
465
|
+
}
|
|
466
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmAccordionTitleDirective, decorators: [{
|
|
467
|
+
type: Directive,
|
|
468
|
+
args: [{
|
|
469
|
+
selector: '[lumaAccordionTitle]',
|
|
470
|
+
host: {
|
|
471
|
+
'[class]': 'classes()',
|
|
472
|
+
},
|
|
473
|
+
}]
|
|
474
|
+
}], propDecorators: { lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }] } });
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* AccordionIconDirective
|
|
478
|
+
*
|
|
479
|
+
* Applies rotation animation to the accordion icon (typically a chevron).
|
|
480
|
+
* Must be placed on a wrapper element (span or div), not directly on the SVG.
|
|
481
|
+
* Automatically rotates based on the open state of the parent accordion item.
|
|
482
|
+
*
|
|
483
|
+
* @example With span wrapper (recommended)
|
|
484
|
+
* ```html
|
|
485
|
+
* <span lumaAccordionIcon>
|
|
486
|
+
* <svg viewBox="0 0 24 24" class="w-4 h-4">
|
|
487
|
+
* <path stroke="currentColor" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
488
|
+
* </svg>
|
|
489
|
+
* </span>
|
|
490
|
+
* ```
|
|
491
|
+
*
|
|
492
|
+
* @example With div wrapper
|
|
493
|
+
* ```html
|
|
494
|
+
* <div lumaAccordionIcon>
|
|
495
|
+
* <my-chevron-icon></my-chevron-icon>
|
|
496
|
+
* </div>
|
|
497
|
+
* ```
|
|
498
|
+
*
|
|
499
|
+
* @example Customize rotation via CSS variable
|
|
500
|
+
* ```html
|
|
501
|
+
* <span lumaAccordionIcon style="--luma-accordion-icon-rotation: 90deg">
|
|
502
|
+
* <svg>...</svg>
|
|
503
|
+
* </span>
|
|
504
|
+
* ```
|
|
505
|
+
*/
|
|
506
|
+
class LmAccordionIconDirective {
|
|
507
|
+
item = inject(ACCORDION_ITEM);
|
|
508
|
+
classes = computed(() => accordionIconVariants({ open: this.item.isOpen() }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
509
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmAccordionIconDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
510
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type: LmAccordionIconDirective, isStandalone: true, selector: "span[lumaAccordionIcon], div[lumaAccordionIcon]", host: { properties: { "class": "classes()", "attr.aria-hidden": "true" } }, ngImport: i0 });
|
|
511
|
+
}
|
|
512
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmAccordionIconDirective, decorators: [{
|
|
513
|
+
type: Directive,
|
|
514
|
+
args: [{
|
|
515
|
+
selector: 'span[lumaAccordionIcon], div[lumaAccordionIcon]',
|
|
516
|
+
host: {
|
|
517
|
+
'[class]': 'classes()',
|
|
518
|
+
'[attr.aria-hidden]': 'true',
|
|
519
|
+
},
|
|
520
|
+
}]
|
|
521
|
+
}] });
|
|
522
|
+
|
|
523
|
+
let uniqueId$1 = 0;
|
|
524
|
+
/**
|
|
525
|
+
* AccordionContentDirective
|
|
526
|
+
*
|
|
527
|
+
* Applied to the content area of the accordion.
|
|
528
|
+
* Handles visibility, ARIA attributes, and fade animation.
|
|
529
|
+
*
|
|
530
|
+
* @example Basic usage
|
|
531
|
+
* ```html
|
|
532
|
+
* <div lumaAccordionContent>
|
|
533
|
+
* <p>Your content here...</p>
|
|
534
|
+
* </div>
|
|
535
|
+
* ```
|
|
536
|
+
*/
|
|
537
|
+
class LmAccordionContentDirective {
|
|
538
|
+
item = inject(ACCORDION_ITEM);
|
|
539
|
+
trigger = inject(LmAccordionTriggerDirective, { optional: true });
|
|
540
|
+
id = ++uniqueId$1;
|
|
541
|
+
contentId = `luma-accordion-content-${this.id}`;
|
|
542
|
+
triggerId = computed(() => this.trigger?.triggerId ?? null, ...(ngDevMode ? [{ debugName: "triggerId" }] : []));
|
|
543
|
+
classes = computed(() => accordionContentVariants({ open: this.item.isOpen() }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
544
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmAccordionContentDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
545
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type: LmAccordionContentDirective, isStandalone: true, selector: "[lumaAccordionContent]", host: { attributes: { "role": "region" }, properties: { "class": "classes()", "id": "contentId", "attr.aria-labelledby": "triggerId()", "attr.hidden": "!item.isOpen() ? \"\" : null" } }, ngImport: i0 });
|
|
546
|
+
}
|
|
547
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmAccordionContentDirective, decorators: [{
|
|
548
|
+
type: Directive,
|
|
549
|
+
args: [{
|
|
550
|
+
selector: '[lumaAccordionContent]',
|
|
551
|
+
host: {
|
|
552
|
+
'[class]': 'classes()',
|
|
553
|
+
role: 'region',
|
|
554
|
+
'[id]': 'contentId',
|
|
555
|
+
'[attr.aria-labelledby]': 'triggerId()',
|
|
556
|
+
'[attr.hidden]': '!item.isOpen() ? "" : null',
|
|
557
|
+
},
|
|
558
|
+
}]
|
|
559
|
+
}] });
|
|
560
|
+
|
|
561
|
+
class LmTooltipDirective {
|
|
562
|
+
el = inject(ElementRef);
|
|
563
|
+
renderer = inject(Renderer2);
|
|
564
|
+
platformId = inject(PLATFORM_ID);
|
|
565
|
+
// Inputs with lm prefix following Lumo convention
|
|
566
|
+
lumaTooltip = input.required(...(ngDevMode ? [{ debugName: "lumaTooltip" }] : []));
|
|
567
|
+
lmPosition = input('top', ...(ngDevMode ? [{ debugName: "lmPosition" }] : []));
|
|
568
|
+
lmHtml = input(false, ...(ngDevMode ? [{ debugName: "lmHtml" }] : []));
|
|
569
|
+
lmTrigger = input('hover', ...(ngDevMode ? [{ debugName: "lmTrigger" }] : []));
|
|
570
|
+
lmDelay = input(0, ...(ngDevMode ? [{ debugName: "lmDelay" }] : []));
|
|
571
|
+
// State
|
|
572
|
+
isVisible = signal(false, ...(ngDevMode ? [{ debugName: "isVisible" }] : []));
|
|
573
|
+
actualPosition = signal('top', ...(ngDevMode ? [{ debugName: "actualPosition" }] : []));
|
|
574
|
+
tooltipId = `tooltip-${Math.random().toString(36).slice(2, 9)}`;
|
|
575
|
+
tooltipElement = null;
|
|
576
|
+
showTimeout = null;
|
|
577
|
+
classes = computed(() => tooltipVariants({
|
|
578
|
+
position: this.actualPosition(),
|
|
579
|
+
visible: this.isVisible(),
|
|
580
|
+
}), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
581
|
+
constructor() {
|
|
582
|
+
// Create and update tooltip element reactively
|
|
583
|
+
effect(() => {
|
|
584
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
585
|
+
this.ensureTooltipElement();
|
|
586
|
+
this.updateContent();
|
|
587
|
+
this.updateClasses();
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
ensureTooltipElement() {
|
|
592
|
+
if (this.tooltipElement)
|
|
593
|
+
return;
|
|
594
|
+
this.tooltipElement = this.renderer.createElement('div');
|
|
595
|
+
this.renderer.setAttribute(this.tooltipElement, 'id', this.tooltipId);
|
|
596
|
+
this.renderer.setAttribute(this.tooltipElement, 'role', 'tooltip');
|
|
597
|
+
this.renderer.appendChild(this.el.nativeElement, this.tooltipElement);
|
|
598
|
+
}
|
|
599
|
+
updateContent() {
|
|
600
|
+
if (!this.tooltipElement)
|
|
601
|
+
return;
|
|
602
|
+
const content = this.lumaTooltip();
|
|
603
|
+
if (this.lmHtml()) {
|
|
604
|
+
this.tooltipElement.innerHTML = content;
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
this.tooltipElement.textContent = content;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
updateClasses() {
|
|
611
|
+
if (!this.tooltipElement)
|
|
612
|
+
return;
|
|
613
|
+
this.tooltipElement.className = this.classes();
|
|
614
|
+
}
|
|
615
|
+
isTouchDevice() {
|
|
616
|
+
if (!isPlatformBrowser(this.platformId))
|
|
617
|
+
return false;
|
|
618
|
+
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
|
619
|
+
}
|
|
620
|
+
getFlippedPosition(preferred) {
|
|
621
|
+
if (!this.tooltipElement || !isPlatformBrowser(this.platformId)) {
|
|
622
|
+
return preferred;
|
|
623
|
+
}
|
|
624
|
+
const triggerRect = this.el.nativeElement.getBoundingClientRect();
|
|
625
|
+
const tooltipRect = this.tooltipElement.getBoundingClientRect();
|
|
626
|
+
const viewportWidth = window.innerWidth;
|
|
627
|
+
const viewportHeight = window.innerHeight;
|
|
628
|
+
const offset = 8; // --luma-tooltip-offset
|
|
629
|
+
switch (preferred) {
|
|
630
|
+
case 'top':
|
|
631
|
+
if (triggerRect.top - tooltipRect.height - offset < 0) {
|
|
632
|
+
return 'bottom';
|
|
633
|
+
}
|
|
634
|
+
break;
|
|
635
|
+
case 'bottom':
|
|
636
|
+
if (triggerRect.bottom + tooltipRect.height + offset > viewportHeight) {
|
|
637
|
+
return 'top';
|
|
638
|
+
}
|
|
639
|
+
break;
|
|
640
|
+
case 'left':
|
|
641
|
+
if (triggerRect.left - tooltipRect.width - offset < 0) {
|
|
642
|
+
return 'right';
|
|
643
|
+
}
|
|
644
|
+
break;
|
|
645
|
+
case 'right':
|
|
646
|
+
if (triggerRect.right + tooltipRect.width + offset > viewportWidth) {
|
|
647
|
+
return 'left';
|
|
648
|
+
}
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
return preferred;
|
|
652
|
+
}
|
|
653
|
+
onMouseEnter() {
|
|
654
|
+
// Ignore hover on touch devices
|
|
655
|
+
if (this.isTouchDevice())
|
|
656
|
+
return;
|
|
657
|
+
if (this.lmTrigger() !== 'hover')
|
|
658
|
+
return;
|
|
659
|
+
this.show();
|
|
660
|
+
}
|
|
661
|
+
onMouseLeave() {
|
|
662
|
+
if (this.isTouchDevice())
|
|
663
|
+
return;
|
|
664
|
+
if (this.lmTrigger() !== 'hover')
|
|
665
|
+
return;
|
|
666
|
+
this.hide();
|
|
667
|
+
}
|
|
668
|
+
onClick() {
|
|
669
|
+
// On touch devices, click acts as toggle even with trigger='hover'
|
|
670
|
+
if (this.isTouchDevice() || this.lmTrigger() === 'click') {
|
|
671
|
+
this.toggle();
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
onFocus() {
|
|
675
|
+
if (this.lmTrigger() === 'focus' || this.lmTrigger() === 'hover') {
|
|
676
|
+
this.show();
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
onBlur() {
|
|
680
|
+
if (this.lmTrigger() === 'focus' || this.lmTrigger() === 'hover') {
|
|
681
|
+
this.hide();
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
onEscape() {
|
|
685
|
+
this.hide();
|
|
686
|
+
}
|
|
687
|
+
onDocumentClick(event) {
|
|
688
|
+
if (this.lmTrigger() !== 'click' && !this.isTouchDevice())
|
|
689
|
+
return;
|
|
690
|
+
if (!this.el.nativeElement.contains(event.target)) {
|
|
691
|
+
this.hide();
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
onDocumentTouch(event) {
|
|
695
|
+
if (!this.isVisible())
|
|
696
|
+
return;
|
|
697
|
+
if (!this.el.nativeElement.contains(event.target)) {
|
|
698
|
+
this.hide();
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
show() {
|
|
702
|
+
if (this.showTimeout)
|
|
703
|
+
clearTimeout(this.showTimeout);
|
|
704
|
+
const delay = this.lmDelay();
|
|
705
|
+
const showAction = () => {
|
|
706
|
+
this.isVisible.set(true);
|
|
707
|
+
// Calculate position with auto-flip after tooltip is visible
|
|
708
|
+
requestAnimationFrame(() => {
|
|
709
|
+
const actualPosition = this.getFlippedPosition(this.lmPosition());
|
|
710
|
+
this.actualPosition.set(actualPosition);
|
|
711
|
+
this.updateClasses();
|
|
712
|
+
});
|
|
713
|
+
};
|
|
714
|
+
if (delay > 0) {
|
|
715
|
+
this.showTimeout = setTimeout(showAction, delay);
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
showAction();
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
hide() {
|
|
722
|
+
if (this.showTimeout) {
|
|
723
|
+
clearTimeout(this.showTimeout);
|
|
724
|
+
this.showTimeout = null;
|
|
725
|
+
}
|
|
726
|
+
this.isVisible.set(false);
|
|
727
|
+
this.updateClasses();
|
|
728
|
+
}
|
|
729
|
+
toggle() {
|
|
730
|
+
if (this.isVisible()) {
|
|
731
|
+
this.hide();
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
this.show();
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
ngOnDestroy() {
|
|
738
|
+
if (this.showTimeout)
|
|
739
|
+
clearTimeout(this.showTimeout);
|
|
740
|
+
if (this.tooltipElement) {
|
|
741
|
+
this.renderer.removeChild(this.el.nativeElement, this.tooltipElement);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTooltipDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
745
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmTooltipDirective, isStandalone: true, selector: "[lumaTooltip]", inputs: { lumaTooltip: { classPropertyName: "lumaTooltip", publicName: "lumaTooltip", isSignal: true, isRequired: true, transformFunction: null }, lmPosition: { classPropertyName: "lmPosition", publicName: "lmPosition", isSignal: true, isRequired: false, transformFunction: null }, lmHtml: { classPropertyName: "lmHtml", publicName: "lmHtml", isSignal: true, isRequired: false, transformFunction: null }, lmTrigger: { classPropertyName: "lmTrigger", publicName: "lmTrigger", isSignal: true, isRequired: false, transformFunction: null }, lmDelay: { classPropertyName: "lmDelay", publicName: "lmDelay", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "mouseenter": "onMouseEnter()", "mouseleave": "onMouseLeave()", "click": "onClick()", "focus": "onFocus()", "blur": "onBlur()", "document:keydown.escape": "onEscape()", "document:click": "onDocumentClick($event)", "document:touchstart": "onDocumentTouch($event)" }, properties: { "attr.aria-describedby": "tooltipId", "style.position": "\"relative\"" } }, ngImport: i0 });
|
|
746
|
+
}
|
|
747
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTooltipDirective, decorators: [{
|
|
748
|
+
type: Directive,
|
|
749
|
+
args: [{
|
|
750
|
+
selector: '[lumaTooltip]',
|
|
751
|
+
host: {
|
|
752
|
+
'[attr.aria-describedby]': 'tooltipId',
|
|
753
|
+
'[style.position]': '"relative"',
|
|
754
|
+
},
|
|
755
|
+
}]
|
|
756
|
+
}], ctorParameters: () => [], propDecorators: { lumaTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "lumaTooltip", required: true }] }], lmPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmPosition", required: false }] }], lmHtml: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmHtml", required: false }] }], lmTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmTrigger", required: false }] }], lmDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDelay", required: false }] }], onMouseEnter: [{
|
|
757
|
+
type: HostListener,
|
|
758
|
+
args: ['mouseenter']
|
|
759
|
+
}], onMouseLeave: [{
|
|
760
|
+
type: HostListener,
|
|
761
|
+
args: ['mouseleave']
|
|
762
|
+
}], onClick: [{
|
|
763
|
+
type: HostListener,
|
|
764
|
+
args: ['click']
|
|
765
|
+
}], onFocus: [{
|
|
766
|
+
type: HostListener,
|
|
767
|
+
args: ['focus']
|
|
768
|
+
}], onBlur: [{
|
|
769
|
+
type: HostListener,
|
|
770
|
+
args: ['blur']
|
|
771
|
+
}], onEscape: [{
|
|
772
|
+
type: HostListener,
|
|
773
|
+
args: ['document:keydown.escape']
|
|
774
|
+
}], onDocumentClick: [{
|
|
775
|
+
type: HostListener,
|
|
776
|
+
args: ['document:click', ['$event']]
|
|
777
|
+
}], onDocumentTouch: [{
|
|
778
|
+
type: HostListener,
|
|
779
|
+
args: ['document:touchstart', ['$event']]
|
|
780
|
+
}] } });
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Injection token for tabs group
|
|
784
|
+
* Allows child components to access parent tabs state
|
|
785
|
+
*/
|
|
786
|
+
const TABS_GROUP = new InjectionToken('TabsGroup');
|
|
787
|
+
/**
|
|
788
|
+
* Injection token for tabs list
|
|
789
|
+
* Allows indicator to access trigger positions
|
|
790
|
+
*/
|
|
791
|
+
const TABS_LIST = new InjectionToken('TabsList');
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Tabs container component
|
|
795
|
+
*
|
|
796
|
+
* Manages tab selection state, keyboard navigation, and provides context
|
|
797
|
+
* to child components (TabsList, TabsTrigger, TabsPanel).
|
|
798
|
+
*
|
|
799
|
+
* @example
|
|
800
|
+
* ```html
|
|
801
|
+
* <luma-tabs [lmValue]="selectedTab()" (lmValueChange)="onSelect($event)">
|
|
802
|
+
* <div lumaTabsList>
|
|
803
|
+
* <button lumaTabsTrigger="tab-1">Tab 1</button>
|
|
804
|
+
* <button lumaTabsTrigger="tab-2">Tab 2</button>
|
|
805
|
+
* </div>
|
|
806
|
+
* <div lumaTabsPanel="tab-1">Content 1</div>
|
|
807
|
+
* <div lumaTabsPanel="tab-2">Content 2</div>
|
|
808
|
+
* </luma-tabs>
|
|
809
|
+
* ```
|
|
810
|
+
*/
|
|
811
|
+
class LmTabsComponent {
|
|
812
|
+
/** Controlled value - currently selected tab */
|
|
813
|
+
lmValue = input(null, ...(ngDevMode ? [{ debugName: "lmValue" }] : []));
|
|
814
|
+
/** Default value for uncontrolled mode */
|
|
815
|
+
lmDefaultValue = input('', ...(ngDevMode ? [{ debugName: "lmDefaultValue" }] : []));
|
|
816
|
+
/** Visual style: underline, background, or pill */
|
|
817
|
+
lmVariant = input('underline', ...(ngDevMode ? [{ debugName: "lmVariant" }] : []));
|
|
818
|
+
/** Whether to lazy load panel content */
|
|
819
|
+
lmLazy = input(true, ...(ngDevMode ? [{ debugName: "lmLazy" }] : []));
|
|
820
|
+
/** Emits when selected tab changes */
|
|
821
|
+
lmValueChange = output();
|
|
822
|
+
/** Internal state for the selected value */
|
|
823
|
+
value = signal(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
824
|
+
/** Map of registered triggers for keyboard navigation */
|
|
825
|
+
triggers = new Map();
|
|
826
|
+
/** Ordered list of trigger values for navigation */
|
|
827
|
+
triggerOrder = [];
|
|
828
|
+
constructor() {
|
|
829
|
+
// Sync controlled value to internal state
|
|
830
|
+
effect(() => {
|
|
831
|
+
const controlled = this.lmValue();
|
|
832
|
+
const defaultVal = this.lmDefaultValue();
|
|
833
|
+
if (controlled !== null) {
|
|
834
|
+
this.value.set(controlled);
|
|
835
|
+
}
|
|
836
|
+
else if (this.value() === null && defaultVal) {
|
|
837
|
+
this.value.set(defaultVal);
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Select a tab by value
|
|
843
|
+
*/
|
|
844
|
+
select(tabValue) {
|
|
845
|
+
if (this.value() === tabValue)
|
|
846
|
+
return;
|
|
847
|
+
this.value.set(tabValue);
|
|
848
|
+
this.lmValueChange.emit(tabValue);
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Register a trigger element for keyboard navigation
|
|
852
|
+
*/
|
|
853
|
+
registerTrigger(tabValue, element) {
|
|
854
|
+
this.triggers.set(tabValue, element);
|
|
855
|
+
if (!this.triggerOrder.includes(tabValue)) {
|
|
856
|
+
this.triggerOrder.push(tabValue);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Unregister a trigger element
|
|
861
|
+
*/
|
|
862
|
+
unregisterTrigger(tabValue) {
|
|
863
|
+
this.triggers.delete(tabValue);
|
|
864
|
+
this.triggerOrder = this.triggerOrder.filter((v) => v !== tabValue);
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Get all registered triggers
|
|
868
|
+
*/
|
|
869
|
+
getTriggers() {
|
|
870
|
+
return this.triggers;
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Focus next trigger in the list
|
|
874
|
+
*/
|
|
875
|
+
focusNextTrigger() {
|
|
876
|
+
const currentIndex = this.getCurrentTriggerIndex();
|
|
877
|
+
const nextIndex = (currentIndex + 1) % this.triggerOrder.length;
|
|
878
|
+
this.focusTriggerAtIndex(nextIndex);
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Focus previous trigger in the list
|
|
882
|
+
*/
|
|
883
|
+
focusPreviousTrigger() {
|
|
884
|
+
const currentIndex = this.getCurrentTriggerIndex();
|
|
885
|
+
const prevIndex = currentIndex <= 0 ? this.triggerOrder.length - 1 : currentIndex - 1;
|
|
886
|
+
this.focusTriggerAtIndex(prevIndex);
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Focus first trigger
|
|
890
|
+
*/
|
|
891
|
+
focusFirstTrigger() {
|
|
892
|
+
this.focusTriggerAtIndex(0);
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Focus last trigger
|
|
896
|
+
*/
|
|
897
|
+
focusLastTrigger() {
|
|
898
|
+
this.focusTriggerAtIndex(this.triggerOrder.length - 1);
|
|
899
|
+
}
|
|
900
|
+
getCurrentTriggerIndex() {
|
|
901
|
+
const currentValue = this.value();
|
|
902
|
+
return currentValue ? this.triggerOrder.indexOf(currentValue) : 0;
|
|
903
|
+
}
|
|
904
|
+
focusTriggerAtIndex(index) {
|
|
905
|
+
const value = this.triggerOrder[index];
|
|
906
|
+
const element = this.triggers.get(value);
|
|
907
|
+
if (element) {
|
|
908
|
+
element.focus();
|
|
909
|
+
this.select(value);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTabsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
913
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.9", type: LmTabsComponent, isStandalone: true, selector: "luma-tabs", inputs: { lmValue: { classPropertyName: "lmValue", publicName: "lmValue", isSignal: true, isRequired: false, transformFunction: null }, lmDefaultValue: { classPropertyName: "lmDefaultValue", publicName: "lmDefaultValue", isSignal: true, isRequired: false, transformFunction: null }, lmVariant: { classPropertyName: "lmVariant", publicName: "lmVariant", isSignal: true, isRequired: false, transformFunction: null }, lmLazy: { classPropertyName: "lmLazy", publicName: "lmLazy", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { lmValueChange: "lmValueChange" }, host: { properties: { "class": "\"block w-full\"" } }, providers: [
|
|
914
|
+
{
|
|
915
|
+
provide: TABS_GROUP,
|
|
916
|
+
useExisting: LmTabsComponent,
|
|
917
|
+
},
|
|
918
|
+
], ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
919
|
+
}
|
|
920
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTabsComponent, decorators: [{
|
|
921
|
+
type: Component,
|
|
922
|
+
args: [{
|
|
923
|
+
selector: 'luma-tabs',
|
|
924
|
+
template: `<ng-content />`,
|
|
925
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
926
|
+
providers: [
|
|
927
|
+
{
|
|
928
|
+
provide: TABS_GROUP,
|
|
929
|
+
useExisting: LmTabsComponent,
|
|
930
|
+
},
|
|
931
|
+
],
|
|
932
|
+
host: {
|
|
933
|
+
'[class]': '"block w-full"',
|
|
934
|
+
},
|
|
935
|
+
}]
|
|
936
|
+
}], ctorParameters: () => [], propDecorators: { lmValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmValue", required: false }] }], lmDefaultValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDefaultValue", required: false }] }], lmVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmVariant", required: false }] }], lmLazy: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmLazy", required: false }] }], lmValueChange: [{ type: i0.Output, args: ["lmValueChange"] }] } });
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Tabs list directive
|
|
940
|
+
*
|
|
941
|
+
* Container for tab triggers with role="tablist".
|
|
942
|
+
* Provides context for the indicator component.
|
|
943
|
+
*
|
|
944
|
+
* @example
|
|
945
|
+
* ```html
|
|
946
|
+
* <div lumaTabsList>
|
|
947
|
+
* <button lumaTabsTrigger="tab-1">Tab 1</button>
|
|
948
|
+
* <button lumaTabsTrigger="tab-2">Tab 2</button>
|
|
949
|
+
* </div>
|
|
950
|
+
* ```
|
|
951
|
+
*/
|
|
952
|
+
class LmTabsListDirective {
|
|
953
|
+
elementRef = inject((ElementRef));
|
|
954
|
+
tabsGroup = inject(TABS_GROUP);
|
|
955
|
+
/** Whether horizontal scrolling is enabled */
|
|
956
|
+
lmScrollable = false;
|
|
957
|
+
classes = computed(() => tabsListVariants({
|
|
958
|
+
style: this.tabsGroup.lmVariant(),
|
|
959
|
+
scrollable: this.lmScrollable,
|
|
960
|
+
}), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
961
|
+
/**
|
|
962
|
+
* Get the currently active trigger element
|
|
963
|
+
*/
|
|
964
|
+
getActiveTrigger() {
|
|
965
|
+
const currentValue = this.tabsGroup.value();
|
|
966
|
+
if (!currentValue)
|
|
967
|
+
return null;
|
|
968
|
+
const triggers = this.tabsGroup.getTriggers();
|
|
969
|
+
return triggers.get(currentValue) || null;
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Handle mouse wheel for horizontal scroll
|
|
973
|
+
*/
|
|
974
|
+
onWheel(event) {
|
|
975
|
+
if (this.lmScrollable) {
|
|
976
|
+
event.preventDefault();
|
|
977
|
+
this.elementRef.nativeElement.scrollLeft += event.deltaY;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTabsListDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
981
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type: LmTabsListDirective, isStandalone: true, selector: "[lumaTabsList]", host: { attributes: { "role": "tablist" }, listeners: { "wheel": "onWheel($event)" }, properties: { "attr.aria-orientation": "\"horizontal\"", "class": "classes()" } }, providers: [
|
|
982
|
+
{
|
|
983
|
+
provide: TABS_LIST,
|
|
984
|
+
useExisting: LmTabsListDirective,
|
|
985
|
+
},
|
|
986
|
+
], ngImport: i0 });
|
|
987
|
+
}
|
|
988
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTabsListDirective, decorators: [{
|
|
989
|
+
type: Directive,
|
|
990
|
+
args: [{
|
|
991
|
+
selector: '[lumaTabsList]',
|
|
992
|
+
providers: [
|
|
993
|
+
{
|
|
994
|
+
provide: TABS_LIST,
|
|
995
|
+
useExisting: LmTabsListDirective,
|
|
996
|
+
},
|
|
997
|
+
],
|
|
998
|
+
host: {
|
|
999
|
+
role: 'tablist',
|
|
1000
|
+
'[attr.aria-orientation]': '"horizontal"',
|
|
1001
|
+
'[class]': 'classes()',
|
|
1002
|
+
},
|
|
1003
|
+
}]
|
|
1004
|
+
}], propDecorators: { onWheel: [{
|
|
1005
|
+
type: HostListener,
|
|
1006
|
+
args: ['wheel', ['$event']]
|
|
1007
|
+
}] } });
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Tabs trigger directive
|
|
1011
|
+
*
|
|
1012
|
+
* Individual tab button with role="tab" and keyboard navigation support.
|
|
1013
|
+
* Follows WAI-ARIA tabs pattern with roving tabindex.
|
|
1014
|
+
*
|
|
1015
|
+
* @example
|
|
1016
|
+
* ```html
|
|
1017
|
+
* <button lumaTabsTrigger="tab-1">Tab 1</button>
|
|
1018
|
+
* ```
|
|
1019
|
+
*/
|
|
1020
|
+
class LmTabsTriggerDirective {
|
|
1021
|
+
el = inject((ElementRef));
|
|
1022
|
+
tabsGroup = inject(TABS_GROUP);
|
|
1023
|
+
/** Tab value identifier */
|
|
1024
|
+
lumaTabsTrigger = input.required(...(ngDevMode ? [{ debugName: "lumaTabsTrigger" }] : []));
|
|
1025
|
+
/** Whether this trigger is disabled */
|
|
1026
|
+
lmDisabled = input(false, ...(ngDevMode ? [{ debugName: "lmDisabled" }] : []));
|
|
1027
|
+
/** Computed: whether this tab is selected */
|
|
1028
|
+
isSelected = computed(() => this.tabsGroup.value() === this.lumaTabsTrigger(), ...(ngDevMode ? [{ debugName: "isSelected" }] : []));
|
|
1029
|
+
/** Computed: ID for the trigger element */
|
|
1030
|
+
triggerId = computed(() => `tab-trigger-${this.lumaTabsTrigger()}`, ...(ngDevMode ? [{ debugName: "triggerId" }] : []));
|
|
1031
|
+
/** Computed: ID for the corresponding panel */
|
|
1032
|
+
panelId = computed(() => `tab-panel-${this.lumaTabsTrigger()}`, ...(ngDevMode ? [{ debugName: "panelId" }] : []));
|
|
1033
|
+
/** Computed: CSS classes from CVA */
|
|
1034
|
+
classes = computed(() => tabsTriggerVariants({
|
|
1035
|
+
style: this.tabsGroup.lmVariant(),
|
|
1036
|
+
selected: this.isSelected(),
|
|
1037
|
+
}), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
1038
|
+
ngOnInit() {
|
|
1039
|
+
this.tabsGroup.registerTrigger(this.lumaTabsTrigger(), this.el.nativeElement);
|
|
1040
|
+
}
|
|
1041
|
+
ngOnDestroy() {
|
|
1042
|
+
this.tabsGroup.unregisterTrigger(this.lumaTabsTrigger());
|
|
1043
|
+
}
|
|
1044
|
+
onClick() {
|
|
1045
|
+
if (this.lmDisabled())
|
|
1046
|
+
return;
|
|
1047
|
+
this.tabsGroup.select(this.lumaTabsTrigger());
|
|
1048
|
+
}
|
|
1049
|
+
onKeydown(event) {
|
|
1050
|
+
if (this.lmDisabled())
|
|
1051
|
+
return;
|
|
1052
|
+
switch (event.key) {
|
|
1053
|
+
case 'ArrowRight':
|
|
1054
|
+
event.preventDefault();
|
|
1055
|
+
this.tabsGroup.focusNextTrigger();
|
|
1056
|
+
break;
|
|
1057
|
+
case 'ArrowLeft':
|
|
1058
|
+
event.preventDefault();
|
|
1059
|
+
this.tabsGroup.focusPreviousTrigger();
|
|
1060
|
+
break;
|
|
1061
|
+
case 'Home':
|
|
1062
|
+
event.preventDefault();
|
|
1063
|
+
this.tabsGroup.focusFirstTrigger();
|
|
1064
|
+
break;
|
|
1065
|
+
case 'End':
|
|
1066
|
+
event.preventDefault();
|
|
1067
|
+
this.tabsGroup.focusLastTrigger();
|
|
1068
|
+
break;
|
|
1069
|
+
case 'Enter':
|
|
1070
|
+
case ' ':
|
|
1071
|
+
event.preventDefault();
|
|
1072
|
+
this.tabsGroup.select(this.lumaTabsTrigger());
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTabsTriggerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1077
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmTabsTriggerDirective, isStandalone: true, selector: "[lumaTabsTrigger]", inputs: { lumaTabsTrigger: { classPropertyName: "lumaTabsTrigger", publicName: "lumaTabsTrigger", isSignal: true, isRequired: true, transformFunction: null }, lmDisabled: { classPropertyName: "lmDisabled", publicName: "lmDisabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "tab" }, listeners: { "click": "onClick()", "keydown": "onKeydown($event)" }, properties: { "attr.id": "triggerId()", "attr.aria-selected": "isSelected()", "attr.aria-controls": "panelId()", "attr.tabindex": "isSelected() ? 0 : -1", "class": "classes()" } }, ngImport: i0 });
|
|
1078
|
+
}
|
|
1079
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTabsTriggerDirective, decorators: [{
|
|
1080
|
+
type: Directive,
|
|
1081
|
+
args: [{
|
|
1082
|
+
selector: '[lumaTabsTrigger]',
|
|
1083
|
+
host: {
|
|
1084
|
+
role: 'tab',
|
|
1085
|
+
'[attr.id]': 'triggerId()',
|
|
1086
|
+
'[attr.aria-selected]': 'isSelected()',
|
|
1087
|
+
'[attr.aria-controls]': 'panelId()',
|
|
1088
|
+
'[attr.tabindex]': 'isSelected() ? 0 : -1',
|
|
1089
|
+
'[class]': 'classes()',
|
|
1090
|
+
},
|
|
1091
|
+
}]
|
|
1092
|
+
}], propDecorators: { lumaTabsTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "lumaTabsTrigger", required: true }] }], lmDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDisabled", required: false }] }], onClick: [{
|
|
1093
|
+
type: HostListener,
|
|
1094
|
+
args: ['click']
|
|
1095
|
+
}], onKeydown: [{
|
|
1096
|
+
type: HostListener,
|
|
1097
|
+
args: ['keydown', ['$event']]
|
|
1098
|
+
}] } });
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Tabs panel directive
|
|
1102
|
+
*
|
|
1103
|
+
* Content panel with role="tabpanel" and lazy loading support.
|
|
1104
|
+
* When lazy loading is enabled, content is only rendered after
|
|
1105
|
+
* the tab has been selected at least once, then cached.
|
|
1106
|
+
*
|
|
1107
|
+
* @example
|
|
1108
|
+
* ```html
|
|
1109
|
+
* <div lumaTabsPanel="tab-1">Content 1</div>
|
|
1110
|
+
*
|
|
1111
|
+
* <!-- With lazy loading (default when lmLazy=true on parent) -->
|
|
1112
|
+
* <ng-template lumaTabsPanel="tab-1">
|
|
1113
|
+
* <expensive-component />
|
|
1114
|
+
* </ng-template>
|
|
1115
|
+
* ```
|
|
1116
|
+
*/
|
|
1117
|
+
class LmTabsPanelDirective {
|
|
1118
|
+
tabsGroup = inject(TABS_GROUP);
|
|
1119
|
+
templateRef = inject((TemplateRef), {
|
|
1120
|
+
optional: true,
|
|
1121
|
+
});
|
|
1122
|
+
viewContainer = inject(ViewContainerRef);
|
|
1123
|
+
/** Panel value identifier */
|
|
1124
|
+
lumaTabsPanel = input.required(...(ngDevMode ? [{ debugName: "lumaTabsPanel" }] : []));
|
|
1125
|
+
/** Track if panel has ever been selected (for lazy loading cache) */
|
|
1126
|
+
hasBeenSelected = signal(false, ...(ngDevMode ? [{ debugName: "hasBeenSelected" }] : []));
|
|
1127
|
+
/** Computed: whether this panel is currently selected */
|
|
1128
|
+
isSelected = computed(() => this.tabsGroup.value() === this.lumaTabsPanel(), ...(ngDevMode ? [{ debugName: "isSelected" }] : []));
|
|
1129
|
+
/** Computed: whether this panel should be visible/rendered */
|
|
1130
|
+
isVisible = computed(() => {
|
|
1131
|
+
const selected = this.isSelected();
|
|
1132
|
+
const lazy = this.tabsGroup.lmLazy();
|
|
1133
|
+
// If lazy loading is disabled, always show selected panel
|
|
1134
|
+
if (!lazy)
|
|
1135
|
+
return selected;
|
|
1136
|
+
// With lazy loading: render if ever selected, but only show if currently selected
|
|
1137
|
+
return this.hasBeenSelected() && selected;
|
|
1138
|
+
}, ...(ngDevMode ? [{ debugName: "isVisible" }] : []));
|
|
1139
|
+
/** Computed: whether content should be rendered (for lazy loading) */
|
|
1140
|
+
shouldRender = computed(() => {
|
|
1141
|
+
const lazy = this.tabsGroup.lmLazy();
|
|
1142
|
+
return lazy ? this.hasBeenSelected() : true;
|
|
1143
|
+
}, ...(ngDevMode ? [{ debugName: "shouldRender" }] : []));
|
|
1144
|
+
/** Computed: ID for the panel element */
|
|
1145
|
+
panelId = computed(() => `tab-panel-${this.lumaTabsPanel()}`, ...(ngDevMode ? [{ debugName: "panelId" }] : []));
|
|
1146
|
+
/** Computed: ID for the corresponding trigger */
|
|
1147
|
+
triggerId = computed(() => `tab-trigger-${this.lumaTabsPanel()}`, ...(ngDevMode ? [{ debugName: "triggerId" }] : []));
|
|
1148
|
+
/** Computed: CSS classes from CVA */
|
|
1149
|
+
classes = computed(() => tabsPanelVariants({
|
|
1150
|
+
visible: this.isVisible(),
|
|
1151
|
+
}), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
1152
|
+
constructor() {
|
|
1153
|
+
// Track when panel is selected for lazy loading cache
|
|
1154
|
+
effect(() => {
|
|
1155
|
+
if (this.isSelected() && !this.hasBeenSelected()) {
|
|
1156
|
+
this.hasBeenSelected.set(true);
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
// Handle template-based lazy loading
|
|
1160
|
+
effect(() => {
|
|
1161
|
+
if (this.templateRef && this.shouldRender()) {
|
|
1162
|
+
if (this.viewContainer.length === 0) {
|
|
1163
|
+
this.viewContainer.createEmbeddedView(this.templateRef);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTabsPanelDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1169
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmTabsPanelDirective, isStandalone: true, selector: "[lumaTabsPanel]", inputs: { lumaTabsPanel: { classPropertyName: "lumaTabsPanel", publicName: "lumaTabsPanel", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "role": "tabpanel" }, properties: { "attr.id": "panelId()", "attr.aria-labelledby": "triggerId()", "attr.tabindex": "0", "class": "classes()", "hidden": "!isVisible()" } }, ngImport: i0 });
|
|
1170
|
+
}
|
|
1171
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTabsPanelDirective, decorators: [{
|
|
1172
|
+
type: Directive,
|
|
1173
|
+
args: [{
|
|
1174
|
+
selector: '[lumaTabsPanel]',
|
|
1175
|
+
host: {
|
|
1176
|
+
role: 'tabpanel',
|
|
1177
|
+
'[attr.id]': 'panelId()',
|
|
1178
|
+
'[attr.aria-labelledby]': 'triggerId()',
|
|
1179
|
+
'[attr.tabindex]': '0',
|
|
1180
|
+
'[class]': 'classes()',
|
|
1181
|
+
'[hidden]': '!isVisible()',
|
|
1182
|
+
},
|
|
1183
|
+
}]
|
|
1184
|
+
}], ctorParameters: () => [], propDecorators: { lumaTabsPanel: [{ type: i0.Input, args: [{ isSignal: true, alias: "lumaTabsPanel", required: true }] }] } });
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* Tabs indicator component
|
|
1188
|
+
*
|
|
1189
|
+
* Animated indicator that slides between tabs for the underline style.
|
|
1190
|
+
* Uses CSS transform for smooth, GPU-accelerated animation.
|
|
1191
|
+
*
|
|
1192
|
+
* @example
|
|
1193
|
+
* ```html
|
|
1194
|
+
* <div lumaTabsList>
|
|
1195
|
+
* <button lumaTabsTrigger="tab-1">Tab 1</button>
|
|
1196
|
+
* <button lumaTabsTrigger="tab-2">Tab 2</button>
|
|
1197
|
+
* <luma-tabs-indicator />
|
|
1198
|
+
* </div>
|
|
1199
|
+
* ```
|
|
1200
|
+
*/
|
|
1201
|
+
class LmTabsIndicatorComponent {
|
|
1202
|
+
platformId = inject(PLATFORM_ID);
|
|
1203
|
+
tabsGroup = inject(TABS_GROUP);
|
|
1204
|
+
tabsList = inject(TABS_LIST);
|
|
1205
|
+
/** Indicator width in pixels */
|
|
1206
|
+
indicatorWidth = signal(0, ...(ngDevMode ? [{ debugName: "indicatorWidth" }] : []));
|
|
1207
|
+
/** Indicator X or Y position */
|
|
1208
|
+
indicatorPosition = signal(0, ...(ngDevMode ? [{ debugName: "indicatorPosition" }] : []));
|
|
1209
|
+
/** Resize observer for recalculating position */
|
|
1210
|
+
resizeObserver = null;
|
|
1211
|
+
/** Computed: CSS classes from CVA */
|
|
1212
|
+
classes = computed(() => {
|
|
1213
|
+
const style = this.tabsGroup.lmVariant();
|
|
1214
|
+
// Only show indicator for underline style
|
|
1215
|
+
const isVisible = style === 'underline';
|
|
1216
|
+
return tabsIndicatorVariants({
|
|
1217
|
+
visible: isVisible,
|
|
1218
|
+
});
|
|
1219
|
+
}, ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
1220
|
+
/** Computed: CSS transform for positioning */
|
|
1221
|
+
indicatorTransform = computed(() => {
|
|
1222
|
+
return `translateX(${this.indicatorPosition()}px)`;
|
|
1223
|
+
}, ...(ngDevMode ? [{ debugName: "indicatorTransform" }] : []));
|
|
1224
|
+
constructor() {
|
|
1225
|
+
// Update indicator position when selection changes
|
|
1226
|
+
effect(() => {
|
|
1227
|
+
// Read the value to track changes
|
|
1228
|
+
this.tabsGroup.value();
|
|
1229
|
+
// Defer position calculation to ensure DOM is ready
|
|
1230
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
1231
|
+
// Use setTimeout to ensure triggers are registered
|
|
1232
|
+
setTimeout(() => this.updateIndicatorPosition(), 0);
|
|
1233
|
+
}
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
ngAfterViewInit() {
|
|
1237
|
+
if (!isPlatformBrowser(this.platformId))
|
|
1238
|
+
return;
|
|
1239
|
+
// Initial position calculation with delay to ensure triggers are registered
|
|
1240
|
+
setTimeout(() => this.updateIndicatorPosition(), 0);
|
|
1241
|
+
// Watch for container resize
|
|
1242
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
1243
|
+
this.updateIndicatorPosition();
|
|
1244
|
+
});
|
|
1245
|
+
this.resizeObserver.observe(this.tabsList.elementRef.nativeElement);
|
|
1246
|
+
}
|
|
1247
|
+
ngOnDestroy() {
|
|
1248
|
+
this.resizeObserver?.disconnect();
|
|
1249
|
+
}
|
|
1250
|
+
updateIndicatorPosition() {
|
|
1251
|
+
const activeTrigger = this.tabsList.getActiveTrigger();
|
|
1252
|
+
if (!activeTrigger) {
|
|
1253
|
+
this.indicatorWidth.set(0);
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
const triggerRect = activeTrigger.getBoundingClientRect();
|
|
1257
|
+
const listRect = this.tabsList.elementRef.nativeElement.getBoundingClientRect();
|
|
1258
|
+
// Horizontal positioning
|
|
1259
|
+
this.indicatorPosition.set(triggerRect.left - listRect.left);
|
|
1260
|
+
this.indicatorWidth.set(triggerRect.width);
|
|
1261
|
+
}
|
|
1262
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTabsIndicatorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1263
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.9", type: LmTabsIndicatorComponent, isStandalone: true, selector: "luma-tabs-indicator", host: { properties: { "class": "classes()", "style.width.px": "indicatorWidth()", "style.transform": "indicatorTransform()" } }, ngImport: i0, template: ``, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1264
|
+
}
|
|
1265
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTabsIndicatorComponent, decorators: [{
|
|
1266
|
+
type: Component,
|
|
1267
|
+
args: [{
|
|
1268
|
+
selector: 'luma-tabs-indicator',
|
|
1269
|
+
template: ``,
|
|
1270
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1271
|
+
host: {
|
|
1272
|
+
'[class]': 'classes()',
|
|
1273
|
+
'[style.width.px]': 'indicatorWidth()',
|
|
1274
|
+
'[style.transform]': 'indicatorTransform()',
|
|
1275
|
+
},
|
|
1276
|
+
}]
|
|
1277
|
+
}], ctorParameters: () => [] });
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* Injection token for modal context
|
|
1281
|
+
* Allows child components to access parent modal state
|
|
1282
|
+
*/
|
|
1283
|
+
const MODAL_CONTEXT = new InjectionToken('ModalContext');
|
|
1284
|
+
|
|
1285
|
+
let uniqueId = 0;
|
|
1286
|
+
/**
|
|
1287
|
+
* Modal container component
|
|
1288
|
+
*
|
|
1289
|
+
* Manages modal open/close state, escape key handling, and provides context
|
|
1290
|
+
* to child components (ModalOverlay, ModalContainer, etc.).
|
|
1291
|
+
*
|
|
1292
|
+
* Supports both controlled and uncontrolled modes:
|
|
1293
|
+
* - Controlled: Use [lmOpen] and (lmOpenChange)
|
|
1294
|
+
* - Uncontrolled: Use [lmDefaultOpen] and access via template reference
|
|
1295
|
+
*
|
|
1296
|
+
* @example
|
|
1297
|
+
* ```html
|
|
1298
|
+
* <!-- Controlled mode -->
|
|
1299
|
+
* <luma-modal [lmOpen]="isOpen()" (lmOpenChange)="isOpen.set($event)">
|
|
1300
|
+
* <luma-modal-overlay>
|
|
1301
|
+
* <luma-modal-container>
|
|
1302
|
+
* <div lumaModalHeader>
|
|
1303
|
+
* <h2 lumaModalTitle>Title</h2>
|
|
1304
|
+
* <luma-modal-close />
|
|
1305
|
+
* </div>
|
|
1306
|
+
* <div lumaModalContent>Content</div>
|
|
1307
|
+
* <div lumaModalFooter>
|
|
1308
|
+
* <button lumaButton (click)="isOpen.set(false)">Close</button>
|
|
1309
|
+
* </div>
|
|
1310
|
+
* </luma-modal-container>
|
|
1311
|
+
* </luma-modal-overlay>
|
|
1312
|
+
* </luma-modal>
|
|
1313
|
+
*
|
|
1314
|
+
* <!-- Uncontrolled mode -->
|
|
1315
|
+
* <luma-modal #modal [lmDefaultOpen]="false">
|
|
1316
|
+
* ...
|
|
1317
|
+
* </luma-modal>
|
|
1318
|
+
* <button (click)="modal.open()">Open</button>
|
|
1319
|
+
* ```
|
|
1320
|
+
*/
|
|
1321
|
+
class LmModalComponent {
|
|
1322
|
+
platformId = inject(PLATFORM_ID);
|
|
1323
|
+
document = inject(DOCUMENT);
|
|
1324
|
+
/** Controlled open state (null = uncontrolled mode) */
|
|
1325
|
+
lmOpen = input(null, ...(ngDevMode ? [{ debugName: "lmOpen" }] : []));
|
|
1326
|
+
/** Default open state for uncontrolled mode */
|
|
1327
|
+
lmDefaultOpen = input(false, ...(ngDevMode ? [{ debugName: "lmDefaultOpen" }] : []));
|
|
1328
|
+
/** Size variant */
|
|
1329
|
+
lmSize = input('md', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
|
|
1330
|
+
/** Close when clicking the overlay */
|
|
1331
|
+
lmCloseOnOverlay = input(true, ...(ngDevMode ? [{ debugName: "lmCloseOnOverlay" }] : []));
|
|
1332
|
+
/** Close when pressing Escape key */
|
|
1333
|
+
lmCloseOnEscape = input(true, ...(ngDevMode ? [{ debugName: "lmCloseOnEscape" }] : []));
|
|
1334
|
+
/** Emits when open state changes */
|
|
1335
|
+
lmOpenChange = output();
|
|
1336
|
+
/** Internal open state for uncontrolled mode */
|
|
1337
|
+
internalOpen = signal(false, ...(ngDevMode ? [{ debugName: "internalOpen" }] : []));
|
|
1338
|
+
/** Unique modal ID for accessibility */
|
|
1339
|
+
modalId = `modal-${uniqueId++}`;
|
|
1340
|
+
/** Previously focused element for focus restoration */
|
|
1341
|
+
previouslyFocused = null;
|
|
1342
|
+
/** Escape key handler */
|
|
1343
|
+
escapeHandler = null;
|
|
1344
|
+
/** Computed: current open state (controlled or uncontrolled) */
|
|
1345
|
+
isOpen = computed(() => {
|
|
1346
|
+
const controlled = this.lmOpen();
|
|
1347
|
+
return controlled !== null ? controlled : this.internalOpen();
|
|
1348
|
+
}, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
|
|
1349
|
+
/** Computed: size signal for context */
|
|
1350
|
+
size = computed(() => this.lmSize(), ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
1351
|
+
/** Computed: closeOnOverlay signal for context */
|
|
1352
|
+
closeOnOverlay = computed(() => this.lmCloseOnOverlay(), ...(ngDevMode ? [{ debugName: "closeOnOverlay" }] : []));
|
|
1353
|
+
/** Computed: closeOnEscape signal for context */
|
|
1354
|
+
closeOnEscape = computed(() => this.lmCloseOnEscape(), ...(ngDevMode ? [{ debugName: "closeOnEscape" }] : []));
|
|
1355
|
+
constructor() {
|
|
1356
|
+
// Initialize uncontrolled mode with default value
|
|
1357
|
+
effect(() => {
|
|
1358
|
+
if (this.lmOpen() === null) {
|
|
1359
|
+
this.internalOpen.set(this.lmDefaultOpen());
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
// Handle body scroll lock and escape key
|
|
1363
|
+
effect(() => {
|
|
1364
|
+
if (!isPlatformBrowser(this.platformId))
|
|
1365
|
+
return;
|
|
1366
|
+
if (this.isOpen()) {
|
|
1367
|
+
this.lockBodyScroll();
|
|
1368
|
+
this.registerEscapeHandler();
|
|
1369
|
+
this.storeFocus();
|
|
1370
|
+
}
|
|
1371
|
+
else {
|
|
1372
|
+
// Delay scroll unlock to allow fade animation to complete (250ms)
|
|
1373
|
+
setTimeout(() => this.unlockBodyScroll(), 250);
|
|
1374
|
+
this.unregisterEscapeHandler();
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
ngOnDestroy() {
|
|
1379
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
1380
|
+
this.unlockBodyScroll();
|
|
1381
|
+
this.unregisterEscapeHandler();
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Open the modal
|
|
1386
|
+
*/
|
|
1387
|
+
open() {
|
|
1388
|
+
if (this.lmOpen() === null) {
|
|
1389
|
+
this.internalOpen.set(true);
|
|
1390
|
+
}
|
|
1391
|
+
this.lmOpenChange.emit(true);
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Close the modal
|
|
1395
|
+
*/
|
|
1396
|
+
close() {
|
|
1397
|
+
if (this.lmOpen() === null) {
|
|
1398
|
+
this.internalOpen.set(false);
|
|
1399
|
+
}
|
|
1400
|
+
this.lmOpenChange.emit(false);
|
|
1401
|
+
this.restoreFocus();
|
|
1402
|
+
}
|
|
1403
|
+
storeFocus() {
|
|
1404
|
+
this.previouslyFocused = this.document.activeElement;
|
|
1405
|
+
}
|
|
1406
|
+
restoreFocus() {
|
|
1407
|
+
if (this.previouslyFocused &&
|
|
1408
|
+
typeof this.previouslyFocused.focus === 'function') {
|
|
1409
|
+
// Use setTimeout to ensure DOM has updated
|
|
1410
|
+
setTimeout(() => {
|
|
1411
|
+
this.previouslyFocused?.focus();
|
|
1412
|
+
this.previouslyFocused = null;
|
|
1413
|
+
}, 0);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
lockBodyScroll() {
|
|
1417
|
+
const body = this.document.body;
|
|
1418
|
+
const scrollbarWidth = window.innerWidth - this.document.documentElement.clientWidth;
|
|
1419
|
+
body.style.overflow = 'hidden';
|
|
1420
|
+
body.style.paddingRight = `${scrollbarWidth}px`;
|
|
1421
|
+
}
|
|
1422
|
+
unlockBodyScroll() {
|
|
1423
|
+
const body = this.document.body;
|
|
1424
|
+
body.style.overflow = '';
|
|
1425
|
+
body.style.paddingRight = '';
|
|
1426
|
+
}
|
|
1427
|
+
registerEscapeHandler() {
|
|
1428
|
+
if (this.escapeHandler)
|
|
1429
|
+
return;
|
|
1430
|
+
this.escapeHandler = (event) => {
|
|
1431
|
+
if (event.key === 'Escape' && this.lmCloseOnEscape()) {
|
|
1432
|
+
event.preventDefault();
|
|
1433
|
+
this.close();
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
this.document.addEventListener('keydown', this.escapeHandler);
|
|
1437
|
+
}
|
|
1438
|
+
unregisterEscapeHandler() {
|
|
1439
|
+
if (this.escapeHandler) {
|
|
1440
|
+
this.document.removeEventListener('keydown', this.escapeHandler);
|
|
1441
|
+
this.escapeHandler = null;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1445
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.9", type: LmModalComponent, isStandalone: true, selector: "luma-modal", inputs: { lmOpen: { classPropertyName: "lmOpen", publicName: "lmOpen", isSignal: true, isRequired: false, transformFunction: null }, lmDefaultOpen: { classPropertyName: "lmDefaultOpen", publicName: "lmDefaultOpen", isSignal: true, isRequired: false, transformFunction: null }, lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null }, lmCloseOnOverlay: { classPropertyName: "lmCloseOnOverlay", publicName: "lmCloseOnOverlay", isSignal: true, isRequired: false, transformFunction: null }, lmCloseOnEscape: { classPropertyName: "lmCloseOnEscape", publicName: "lmCloseOnEscape", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { lmOpenChange: "lmOpenChange" }, host: { properties: { "attr.data-state": "isOpen() ? \"open\" : \"closed\"" } }, providers: [
|
|
1446
|
+
{
|
|
1447
|
+
provide: MODAL_CONTEXT,
|
|
1448
|
+
useExisting: LmModalComponent,
|
|
1449
|
+
},
|
|
1450
|
+
], ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1451
|
+
}
|
|
1452
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmModalComponent, decorators: [{
|
|
1453
|
+
type: Component,
|
|
1454
|
+
args: [{
|
|
1455
|
+
selector: 'luma-modal',
|
|
1456
|
+
template: `<ng-content />`,
|
|
1457
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1458
|
+
providers: [
|
|
1459
|
+
{
|
|
1460
|
+
provide: MODAL_CONTEXT,
|
|
1461
|
+
useExisting: LmModalComponent,
|
|
1462
|
+
},
|
|
1463
|
+
],
|
|
1464
|
+
host: {
|
|
1465
|
+
'[attr.data-state]': 'isOpen() ? "open" : "closed"',
|
|
1466
|
+
},
|
|
1467
|
+
}]
|
|
1468
|
+
}], ctorParameters: () => [], propDecorators: { lmOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmOpen", required: false }] }], lmDefaultOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDefaultOpen", required: false }] }], lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }], lmCloseOnOverlay: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmCloseOnOverlay", required: false }] }], lmCloseOnEscape: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmCloseOnEscape", required: false }] }], lmOpenChange: [{ type: i0.Output, args: ["lmOpenChange"] }] } });
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Modal overlay component (backdrop)
|
|
1472
|
+
*
|
|
1473
|
+
* Provides a semi-transparent backdrop behind the modal.
|
|
1474
|
+
* Handles click-to-close when enabled on the parent modal.
|
|
1475
|
+
*
|
|
1476
|
+
* @example
|
|
1477
|
+
* ```html
|
|
1478
|
+
* <luma-modal [lmOpen]="isOpen()">
|
|
1479
|
+
* <luma-modal-overlay>
|
|
1480
|
+
* <luma-modal-container>...</luma-modal-container>
|
|
1481
|
+
* </luma-modal-overlay>
|
|
1482
|
+
* </luma-modal>
|
|
1483
|
+
* ```
|
|
1484
|
+
*/
|
|
1485
|
+
class LmModalOverlayComponent {
|
|
1486
|
+
modal = inject(MODAL_CONTEXT);
|
|
1487
|
+
/** Computed classes from CVA */
|
|
1488
|
+
classes = computed(() => modalOverlayVariants({
|
|
1489
|
+
open: this.modal.isOpen(),
|
|
1490
|
+
}), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
1491
|
+
/**
|
|
1492
|
+
* Handle click on overlay (not on children)
|
|
1493
|
+
*/
|
|
1494
|
+
onOverlayClick(event) {
|
|
1495
|
+
// Only close if clicking directly on overlay, not on children
|
|
1496
|
+
if (event.target === event.currentTarget && this.modal.closeOnOverlay()) {
|
|
1497
|
+
this.modal.close();
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmModalOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1501
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.9", type: LmModalOverlayComponent, isStandalone: true, selector: "luma-modal-overlay", host: { listeners: { "click": "onOverlayClick($event)" }, properties: { "class": "classes()" } }, ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1502
|
+
}
|
|
1503
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmModalOverlayComponent, decorators: [{
|
|
1504
|
+
type: Component,
|
|
1505
|
+
args: [{
|
|
1506
|
+
selector: 'luma-modal-overlay',
|
|
1507
|
+
template: `<ng-content />`,
|
|
1508
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1509
|
+
host: {
|
|
1510
|
+
'[class]': 'classes()',
|
|
1511
|
+
'(click)': 'onOverlayClick($event)',
|
|
1512
|
+
},
|
|
1513
|
+
}]
|
|
1514
|
+
}] });
|
|
1515
|
+
|
|
1516
|
+
/** Focusable elements selector */
|
|
1517
|
+
const FOCUSABLE_SELECTOR = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
1518
|
+
/**
|
|
1519
|
+
* Modal container component (dialog box)
|
|
1520
|
+
*
|
|
1521
|
+
* Contains the modal content and handles:
|
|
1522
|
+
* - ARIA attributes for accessibility
|
|
1523
|
+
* - Focus trap when modal is open
|
|
1524
|
+
* - Size variants
|
|
1525
|
+
*
|
|
1526
|
+
* @example
|
|
1527
|
+
* ```html
|
|
1528
|
+
* <luma-modal-overlay>
|
|
1529
|
+
* <luma-modal-container>
|
|
1530
|
+
* <div lumaModalHeader>...</div>
|
|
1531
|
+
* <div lumaModalContent>...</div>
|
|
1532
|
+
* <div lumaModalFooter>...</div>
|
|
1533
|
+
* </luma-modal-container>
|
|
1534
|
+
* </luma-modal-overlay>
|
|
1535
|
+
* ```
|
|
1536
|
+
*/
|
|
1537
|
+
class LmModalContainerComponent {
|
|
1538
|
+
modal = inject(MODAL_CONTEXT);
|
|
1539
|
+
elementRef = inject((ElementRef));
|
|
1540
|
+
platformId = inject(PLATFORM_ID);
|
|
1541
|
+
/** Focus trap keydown handler */
|
|
1542
|
+
focusTrapHandler = null;
|
|
1543
|
+
/** ID for aria-labelledby */
|
|
1544
|
+
titleId = computed(() => `${this.modal.modalId}-title`, ...(ngDevMode ? [{ debugName: "titleId" }] : []));
|
|
1545
|
+
/** Computed classes from CVA */
|
|
1546
|
+
classes = computed(() => modalContainerVariants({
|
|
1547
|
+
size: this.modal.size(),
|
|
1548
|
+
open: this.modal.isOpen(),
|
|
1549
|
+
}), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
1550
|
+
constructor() {
|
|
1551
|
+
// Focus first focusable element when modal opens
|
|
1552
|
+
effect(() => {
|
|
1553
|
+
if (!isPlatformBrowser(this.platformId))
|
|
1554
|
+
return;
|
|
1555
|
+
if (this.modal.isOpen()) {
|
|
1556
|
+
// Use setTimeout to ensure content is rendered
|
|
1557
|
+
setTimeout(() => this.focusFirstElement(), 0);
|
|
1558
|
+
}
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
ngAfterViewInit() {
|
|
1562
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
1563
|
+
this.setupFocusTrap();
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
ngOnDestroy() {
|
|
1567
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
1568
|
+
this.removeFocusTrap();
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Get all focusable elements within the modal
|
|
1573
|
+
*/
|
|
1574
|
+
getFocusableElements() {
|
|
1575
|
+
const elements = this.elementRef.nativeElement.querySelectorAll(FOCUSABLE_SELECTOR);
|
|
1576
|
+
const elementsArray = Array.from(elements);
|
|
1577
|
+
return elementsArray.filter((el) => !el.hasAttribute('disabled') && el.getAttribute('tabindex') !== '-1');
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Focus the first focusable element
|
|
1581
|
+
*/
|
|
1582
|
+
focusFirstElement() {
|
|
1583
|
+
const focusable = this.getFocusableElements();
|
|
1584
|
+
if (focusable.length > 0) {
|
|
1585
|
+
focusable[0].focus();
|
|
1586
|
+
}
|
|
1587
|
+
else {
|
|
1588
|
+
// If no focusable elements, focus the container itself
|
|
1589
|
+
this.elementRef.nativeElement.focus();
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Setup focus trap to keep focus within modal
|
|
1594
|
+
*/
|
|
1595
|
+
setupFocusTrap() {
|
|
1596
|
+
this.focusTrapHandler = (event) => {
|
|
1597
|
+
if (event.key !== 'Tab' || !this.modal.isOpen())
|
|
1598
|
+
return;
|
|
1599
|
+
const focusable = this.getFocusableElements();
|
|
1600
|
+
if (focusable.length === 0)
|
|
1601
|
+
return;
|
|
1602
|
+
const firstElement = focusable[0];
|
|
1603
|
+
const lastElement = focusable[focusable.length - 1];
|
|
1604
|
+
// Shift+Tab on first element -> focus last
|
|
1605
|
+
if (event.shiftKey && document.activeElement === firstElement) {
|
|
1606
|
+
event.preventDefault();
|
|
1607
|
+
lastElement.focus();
|
|
1608
|
+
}
|
|
1609
|
+
// Tab on last element -> focus first
|
|
1610
|
+
else if (!event.shiftKey && document.activeElement === lastElement) {
|
|
1611
|
+
event.preventDefault();
|
|
1612
|
+
firstElement.focus();
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
this.elementRef.nativeElement.addEventListener('keydown', this.focusTrapHandler);
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* Remove focus trap handler
|
|
1619
|
+
*/
|
|
1620
|
+
removeFocusTrap() {
|
|
1621
|
+
if (this.focusTrapHandler) {
|
|
1622
|
+
this.elementRef.nativeElement.removeEventListener('keydown', this.focusTrapHandler);
|
|
1623
|
+
this.focusTrapHandler = null;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmModalContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1627
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.9", type: LmModalContainerComponent, isStandalone: true, selector: "luma-modal-container", host: { attributes: { "role": "dialog" }, properties: { "attr.aria-modal": "true", "attr.aria-labelledby": "titleId()", "class": "classes()", "attr.data-state": "modal.isOpen() ? \"open\" : \"closed\"" } }, ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1628
|
+
}
|
|
1629
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmModalContainerComponent, decorators: [{
|
|
1630
|
+
type: Component,
|
|
1631
|
+
args: [{
|
|
1632
|
+
selector: 'luma-modal-container',
|
|
1633
|
+
template: `<ng-content />`,
|
|
1634
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1635
|
+
host: {
|
|
1636
|
+
role: 'dialog',
|
|
1637
|
+
'[attr.aria-modal]': 'true',
|
|
1638
|
+
'[attr.aria-labelledby]': 'titleId()',
|
|
1639
|
+
'[class]': 'classes()',
|
|
1640
|
+
'[attr.data-state]': 'modal.isOpen() ? "open" : "closed"',
|
|
1641
|
+
},
|
|
1642
|
+
}]
|
|
1643
|
+
}], ctorParameters: () => [] });
|
|
1644
|
+
|
|
1645
|
+
/**
|
|
1646
|
+
* Modal header directive
|
|
1647
|
+
*
|
|
1648
|
+
* Container for modal title and close button.
|
|
1649
|
+
* Provides consistent padding and border styling.
|
|
1650
|
+
*
|
|
1651
|
+
* @example
|
|
1652
|
+
* ```html
|
|
1653
|
+
* <div lumaModalHeader>
|
|
1654
|
+
* <h2 lumaModalTitle>Modal Title</h2>
|
|
1655
|
+
* <luma-modal-close />
|
|
1656
|
+
* </div>
|
|
1657
|
+
* ```
|
|
1658
|
+
*/
|
|
1659
|
+
class LmModalHeaderDirective {
|
|
1660
|
+
/** Computed classes from CVA */
|
|
1661
|
+
classes = computed(() => modalHeaderVariants(), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
1662
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmModalHeaderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1663
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type: LmModalHeaderDirective, isStandalone: true, selector: "[lumaModalHeader]", host: { properties: { "class": "classes()" } }, ngImport: i0 });
|
|
1664
|
+
}
|
|
1665
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmModalHeaderDirective, decorators: [{
|
|
1666
|
+
type: Directive,
|
|
1667
|
+
args: [{
|
|
1668
|
+
selector: '[lumaModalHeader]',
|
|
1669
|
+
host: {
|
|
1670
|
+
'[class]': 'classes()',
|
|
1671
|
+
},
|
|
1672
|
+
}]
|
|
1673
|
+
}] });
|
|
1674
|
+
|
|
1675
|
+
/**
|
|
1676
|
+
* Modal title directive
|
|
1677
|
+
*
|
|
1678
|
+
* Provides consistent typography for modal titles.
|
|
1679
|
+
* Automatically links to aria-labelledby on the modal container.
|
|
1680
|
+
*
|
|
1681
|
+
* @example
|
|
1682
|
+
* ```html
|
|
1683
|
+
* <h2 lumaModalTitle>Modal Title</h2>
|
|
1684
|
+
* <h2 lumaModalTitle lmSize="lg">Large Title</h2>
|
|
1685
|
+
* ```
|
|
1686
|
+
*/
|
|
1687
|
+
class LmModalTitleDirective {
|
|
1688
|
+
modal = inject(MODAL_CONTEXT);
|
|
1689
|
+
/** Title size variant */
|
|
1690
|
+
lmSize = input('md', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
|
|
1691
|
+
/** ID for aria-labelledby connection */
|
|
1692
|
+
titleId = computed(() => `${this.modal.modalId}-title`, ...(ngDevMode ? [{ debugName: "titleId" }] : []));
|
|
1693
|
+
/** Computed classes from CVA */
|
|
1694
|
+
classes = computed(() => modalTitleVariants({
|
|
1695
|
+
size: this.lmSize(),
|
|
1696
|
+
}), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
1697
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmModalTitleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1698
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmModalTitleDirective, isStandalone: true, selector: "[lumaModalTitle]", inputs: { lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.id": "titleId()", "class": "classes()" } }, ngImport: i0 });
|
|
1699
|
+
}
|
|
1700
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmModalTitleDirective, decorators: [{
|
|
1701
|
+
type: Directive,
|
|
1702
|
+
args: [{
|
|
1703
|
+
selector: '[lumaModalTitle]',
|
|
1704
|
+
host: {
|
|
1705
|
+
'[attr.id]': 'titleId()',
|
|
1706
|
+
'[class]': 'classes()',
|
|
1707
|
+
},
|
|
1708
|
+
}]
|
|
1709
|
+
}], propDecorators: { lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }] } });
|
|
1710
|
+
|
|
1711
|
+
/**
|
|
1712
|
+
* Modal content directive
|
|
1713
|
+
*
|
|
1714
|
+
* Container for the main modal content.
|
|
1715
|
+
* Supports scrolling when content exceeds available space.
|
|
1716
|
+
*
|
|
1717
|
+
* @example
|
|
1718
|
+
* ```html
|
|
1719
|
+
* <div lumaModalContent>
|
|
1720
|
+
* Content that doesn't scroll
|
|
1721
|
+
* </div>
|
|
1722
|
+
*
|
|
1723
|
+
* <div lumaModalContent [lmScrollable]="true">
|
|
1724
|
+
* Long content that scrolls...
|
|
1725
|
+
* </div>
|
|
1726
|
+
* ```
|
|
1727
|
+
*/
|
|
1728
|
+
class LmModalContentDirective {
|
|
1729
|
+
/** Enable scroll when content overflows */
|
|
1730
|
+
lmScrollable = input(true, ...(ngDevMode ? [{ debugName: "lmScrollable" }] : []));
|
|
1731
|
+
/** Computed classes from CVA */
|
|
1732
|
+
classes = computed(() => modalContentVariants({
|
|
1733
|
+
scrollable: this.lmScrollable(),
|
|
1734
|
+
}), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
1735
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmModalContentDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1736
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmModalContentDirective, isStandalone: true, selector: "[lumaModalContent]", inputs: { lmScrollable: { classPropertyName: "lmScrollable", publicName: "lmScrollable", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
|
|
1737
|
+
}
|
|
1738
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmModalContentDirective, decorators: [{
|
|
1739
|
+
type: Directive,
|
|
1740
|
+
args: [{
|
|
1741
|
+
selector: '[lumaModalContent]',
|
|
1742
|
+
host: {
|
|
1743
|
+
'[class]': 'classes()',
|
|
1744
|
+
},
|
|
1745
|
+
}]
|
|
1746
|
+
}], propDecorators: { lmScrollable: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmScrollable", required: false }] }] } });
|
|
1747
|
+
|
|
1748
|
+
/**
|
|
1749
|
+
* Modal footer directive
|
|
1750
|
+
*
|
|
1751
|
+
* Container for modal actions (buttons, etc.).
|
|
1752
|
+
* Provides consistent padding and flexible alignment.
|
|
1753
|
+
*
|
|
1754
|
+
* @example
|
|
1755
|
+
* ```html
|
|
1756
|
+
* <div lumaModalFooter>
|
|
1757
|
+
* <button lumaButton lmVariant="ghost">Cancel</button>
|
|
1758
|
+
* <button lumaButton>Confirm</button>
|
|
1759
|
+
* </div>
|
|
1760
|
+
*
|
|
1761
|
+
* <div lumaModalFooter lmAlign="between">
|
|
1762
|
+
* <span>Left content</span>
|
|
1763
|
+
* <button lumaButton>Action</button>
|
|
1764
|
+
* </div>
|
|
1765
|
+
* ```
|
|
1766
|
+
*/
|
|
1767
|
+
class LmModalFooterDirective {
|
|
1768
|
+
/** Alignment of footer content */
|
|
1769
|
+
lmAlign = input('end', ...(ngDevMode ? [{ debugName: "lmAlign" }] : []));
|
|
1770
|
+
/** Computed classes from CVA */
|
|
1771
|
+
classes = computed(() => modalFooterVariants({
|
|
1772
|
+
align: this.lmAlign(),
|
|
1773
|
+
}), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
1774
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmModalFooterDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1775
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmModalFooterDirective, isStandalone: true, selector: "[lumaModalFooter]", inputs: { lmAlign: { classPropertyName: "lmAlign", publicName: "lmAlign", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
|
|
1776
|
+
}
|
|
1777
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmModalFooterDirective, decorators: [{
|
|
1778
|
+
type: Directive,
|
|
1779
|
+
args: [{
|
|
1780
|
+
selector: '[lumaModalFooter]',
|
|
1781
|
+
host: {
|
|
1782
|
+
'[class]': 'classes()',
|
|
1783
|
+
},
|
|
1784
|
+
}]
|
|
1785
|
+
}], propDecorators: { lmAlign: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmAlign", required: false }] }] } });
|
|
1786
|
+
|
|
1787
|
+
/**
|
|
1788
|
+
* Modal close button component
|
|
1789
|
+
*
|
|
1790
|
+
* Provides a styled close button with an X icon.
|
|
1791
|
+
* Can be customized with different content via ng-content.
|
|
1792
|
+
*
|
|
1793
|
+
* @example
|
|
1794
|
+
* ```html
|
|
1795
|
+
* <!-- Default X icon -->
|
|
1796
|
+
* <luma-modal-close />
|
|
1797
|
+
*
|
|
1798
|
+
* <!-- Custom aria label -->
|
|
1799
|
+
* <luma-modal-close lmAriaLabel="Fechar modal" />
|
|
1800
|
+
*
|
|
1801
|
+
* <!-- Custom icon -->
|
|
1802
|
+
* <luma-modal-close>
|
|
1803
|
+
* <svg>...</svg>
|
|
1804
|
+
* </luma-modal-close>
|
|
1805
|
+
* ```
|
|
1806
|
+
*/
|
|
1807
|
+
class LmModalCloseComponent {
|
|
1808
|
+
modal = inject(MODAL_CONTEXT);
|
|
1809
|
+
/** Accessible label for the close button */
|
|
1810
|
+
lmAriaLabel = input('Close modal', ...(ngDevMode ? [{ debugName: "lmAriaLabel" }] : []));
|
|
1811
|
+
/** Computed aria label */
|
|
1812
|
+
ariaLabel = computed(() => this.lmAriaLabel(), ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
1813
|
+
/** Computed classes from CVA */
|
|
1814
|
+
classes = computed(() => modalCloseVariants(), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
1815
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmModalCloseComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1816
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.9", type: LmModalCloseComponent, isStandalone: true, selector: "luma-modal-close", inputs: { lmAriaLabel: { classPropertyName: "lmAriaLabel", publicName: "lmAriaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "contents" }, ngImport: i0, template: `
|
|
1817
|
+
<button
|
|
1818
|
+
type="button"
|
|
1819
|
+
[attr.aria-label]="ariaLabel()"
|
|
1820
|
+
[class]="classes()"
|
|
1821
|
+
(click)="modal.close()"
|
|
1822
|
+
>
|
|
1823
|
+
<ng-content>
|
|
1824
|
+
<svg
|
|
1825
|
+
viewBox="0 0 24 24"
|
|
1826
|
+
class="w-4 h-4"
|
|
1827
|
+
fill="none"
|
|
1828
|
+
stroke="currentColor"
|
|
1829
|
+
aria-hidden="true"
|
|
1830
|
+
>
|
|
1831
|
+
<path
|
|
1832
|
+
stroke-linecap="round"
|
|
1833
|
+
stroke-linejoin="round"
|
|
1834
|
+
stroke-width="2"
|
|
1835
|
+
d="M6 18L18 6M6 6l12 12"
|
|
1836
|
+
/>
|
|
1837
|
+
</svg>
|
|
1838
|
+
</ng-content>
|
|
1839
|
+
</button>
|
|
1840
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1841
|
+
}
|
|
1842
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmModalCloseComponent, decorators: [{
|
|
1843
|
+
type: Component,
|
|
1844
|
+
args: [{
|
|
1845
|
+
selector: 'luma-modal-close',
|
|
1846
|
+
template: `
|
|
1847
|
+
<button
|
|
1848
|
+
type="button"
|
|
1849
|
+
[attr.aria-label]="ariaLabel()"
|
|
1850
|
+
[class]="classes()"
|
|
1851
|
+
(click)="modal.close()"
|
|
1852
|
+
>
|
|
1853
|
+
<ng-content>
|
|
1854
|
+
<svg
|
|
1855
|
+
viewBox="0 0 24 24"
|
|
1856
|
+
class="w-4 h-4"
|
|
1857
|
+
fill="none"
|
|
1858
|
+
stroke="currentColor"
|
|
1859
|
+
aria-hidden="true"
|
|
1860
|
+
>
|
|
1861
|
+
<path
|
|
1862
|
+
stroke-linecap="round"
|
|
1863
|
+
stroke-linejoin="round"
|
|
1864
|
+
stroke-width="2"
|
|
1865
|
+
d="M6 18L18 6M6 6l12 12"
|
|
1866
|
+
/>
|
|
1867
|
+
</svg>
|
|
1868
|
+
</ng-content>
|
|
1869
|
+
</button>
|
|
1870
|
+
`,
|
|
1871
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1872
|
+
host: {
|
|
1873
|
+
class: 'contents',
|
|
1874
|
+
},
|
|
1875
|
+
}]
|
|
1876
|
+
}], propDecorators: { lmAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmAriaLabel", required: false }] }] } });
|
|
1877
|
+
|
|
1878
|
+
// Modal Component & Directives
|
|
1879
|
+
|
|
1880
|
+
/**
|
|
1881
|
+
* Internal toast reference implementation
|
|
1882
|
+
*/
|
|
1883
|
+
class ToastRefImpl {
|
|
1884
|
+
id;
|
|
1885
|
+
dismissFn;
|
|
1886
|
+
_afterDismissed = new Subject();
|
|
1887
|
+
constructor(id, dismissFn) {
|
|
1888
|
+
this.id = id;
|
|
1889
|
+
this.dismissFn = dismissFn;
|
|
1890
|
+
}
|
|
1891
|
+
dismiss() {
|
|
1892
|
+
this.dismissFn(this.id);
|
|
1893
|
+
}
|
|
1894
|
+
get afterDismissed() {
|
|
1895
|
+
return this._afterDismissed.asObservable();
|
|
1896
|
+
}
|
|
1897
|
+
/** @internal Called when toast is actually dismissed */
|
|
1898
|
+
_notifyDismissed() {
|
|
1899
|
+
this._afterDismissed.next();
|
|
1900
|
+
this._afterDismissed.complete();
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Default toast configuration
|
|
1905
|
+
*/
|
|
1906
|
+
const DEFAULT_TOAST_CONFIG = {
|
|
1907
|
+
position: 'top-right',
|
|
1908
|
+
duration: 5000,
|
|
1909
|
+
dismissible: true,
|
|
1910
|
+
maxVisible: 5,
|
|
1911
|
+
pauseOnHover: true,
|
|
1912
|
+
};
|
|
1913
|
+
/**
|
|
1914
|
+
* Injection token for global toast configuration
|
|
1915
|
+
*/
|
|
1916
|
+
const TOAST_CONFIG = new InjectionToken('ToastConfig', {
|
|
1917
|
+
providedIn: 'root',
|
|
1918
|
+
factory: () => DEFAULT_TOAST_CONFIG,
|
|
1919
|
+
});
|
|
1920
|
+
/**
|
|
1921
|
+
* Provider function for custom toast configuration
|
|
1922
|
+
*/
|
|
1923
|
+
function provideToastConfig(config) {
|
|
1924
|
+
return {
|
|
1925
|
+
provide: TOAST_CONFIG,
|
|
1926
|
+
useValue: { ...DEFAULT_TOAST_CONFIG, ...config },
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
/**
|
|
1931
|
+
* ToastCloseComponent
|
|
1932
|
+
*
|
|
1933
|
+
* Close button for toast notifications.
|
|
1934
|
+
* Styled according to the toast variant.
|
|
1935
|
+
*
|
|
1936
|
+
* @internal Used by ToastItemComponent
|
|
1937
|
+
*/
|
|
1938
|
+
class LmToastCloseComponent {
|
|
1939
|
+
/** Toast variant for styling */
|
|
1940
|
+
lmVariant = input('info', ...(ngDevMode ? [{ debugName: "lmVariant" }] : []));
|
|
1941
|
+
/** CSS classes */
|
|
1942
|
+
classes = computed(() => toastCloseVariants({ variant: this.lmVariant() }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
1943
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmToastCloseComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1944
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.9", type: LmToastCloseComponent, isStandalone: true, selector: "luma-toast-close", inputs: { lmVariant: { classPropertyName: "lmVariant", publicName: "lmVariant", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "type": "button", "aria-label": "Close notification" }, properties: { "class": "classes()" } }, ngImport: i0, template: `
|
|
1945
|
+
<svg
|
|
1946
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
1947
|
+
viewBox="0 0 20 20"
|
|
1948
|
+
fill="currentColor"
|
|
1949
|
+
class="lm-size-toast-close"
|
|
1950
|
+
aria-hidden="true"
|
|
1951
|
+
>
|
|
1952
|
+
<path
|
|
1953
|
+
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
1954
|
+
/>
|
|
1955
|
+
</svg>
|
|
1956
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1957
|
+
}
|
|
1958
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmToastCloseComponent, decorators: [{
|
|
1959
|
+
type: Component,
|
|
1960
|
+
args: [{
|
|
1961
|
+
selector: 'luma-toast-close',
|
|
1962
|
+
template: `
|
|
1963
|
+
<svg
|
|
1964
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
1965
|
+
viewBox="0 0 20 20"
|
|
1966
|
+
fill="currentColor"
|
|
1967
|
+
class="lm-size-toast-close"
|
|
1968
|
+
aria-hidden="true"
|
|
1969
|
+
>
|
|
1970
|
+
<path
|
|
1971
|
+
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
1972
|
+
/>
|
|
1973
|
+
</svg>
|
|
1974
|
+
`,
|
|
1975
|
+
host: {
|
|
1976
|
+
type: 'button',
|
|
1977
|
+
'[class]': 'classes()',
|
|
1978
|
+
'aria-label': 'Close notification',
|
|
1979
|
+
},
|
|
1980
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1981
|
+
}]
|
|
1982
|
+
}], propDecorators: { lmVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmVariant", required: false }] }] } });
|
|
1983
|
+
|
|
1984
|
+
/**
|
|
1985
|
+
* ToastItemComponent
|
|
1986
|
+
*
|
|
1987
|
+
* Individual toast notification with timer and icon.
|
|
1988
|
+
*
|
|
1989
|
+
* @internal Created by ToastContainerComponent
|
|
1990
|
+
*/
|
|
1991
|
+
class LmToastItemComponent {
|
|
1992
|
+
/** Toast data */
|
|
1993
|
+
toast = input.required(...(ngDevMode ? [{ debugName: "toast" }] : []));
|
|
1994
|
+
/** Emits when toast should be dismissed */
|
|
1995
|
+
dismiss = output();
|
|
1996
|
+
/** Timer subscription */
|
|
1997
|
+
timerSubscription = null;
|
|
1998
|
+
/** Remaining time in ms */
|
|
1999
|
+
remainingTime = signal(0, ...(ngDevMode ? [{ debugName: "remainingTime" }] : []));
|
|
2000
|
+
/** Whether timer is paused */
|
|
2001
|
+
isPaused = signal(false, ...(ngDevMode ? [{ debugName: "isPaused" }] : []));
|
|
2002
|
+
/** Animation state */
|
|
2003
|
+
animationState = signal('entering', ...(ngDevMode ? [{ debugName: "animationState" }] : []));
|
|
2004
|
+
/** Item CSS classes */
|
|
2005
|
+
itemClasses = computed(() => toastItemVariants({
|
|
2006
|
+
variant: this.toast().variant,
|
|
2007
|
+
state: this.toast().isExiting ? 'exiting' : this.animationState(),
|
|
2008
|
+
}), ...(ngDevMode ? [{ debugName: "itemClasses" }] : []));
|
|
2009
|
+
/** Icon CSS classes */
|
|
2010
|
+
iconClasses = computed(() => toastIconVariants({ variant: this.toast().variant }), ...(ngDevMode ? [{ debugName: "iconClasses" }] : []));
|
|
2011
|
+
/** Content CSS classes */
|
|
2012
|
+
contentClasses = computed(() => toastContentVariants(), ...(ngDevMode ? [{ debugName: "contentClasses" }] : []));
|
|
2013
|
+
/** Title CSS classes */
|
|
2014
|
+
titleClasses = computed(() => toastTitleVariants(), ...(ngDevMode ? [{ debugName: "titleClasses" }] : []));
|
|
2015
|
+
/** Message CSS classes */
|
|
2016
|
+
messageClasses = computed(() => toastMessageVariants(), ...(ngDevMode ? [{ debugName: "messageClasses" }] : []));
|
|
2017
|
+
/** Whether toast has interactive elements */
|
|
2018
|
+
hasInteractiveElements = computed(() => this.toast().dismissible, ...(ngDevMode ? [{ debugName: "hasInteractiveElements" }] : []));
|
|
2019
|
+
ngOnInit() {
|
|
2020
|
+
// Start animation
|
|
2021
|
+
requestAnimationFrame(() => {
|
|
2022
|
+
this.animationState.set('visible');
|
|
2023
|
+
});
|
|
2024
|
+
// Start timer if duration > 0
|
|
2025
|
+
const duration = this.toast().duration;
|
|
2026
|
+
if (duration > 0) {
|
|
2027
|
+
this.startTimer(duration);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
ngOnDestroy() {
|
|
2031
|
+
this.stopTimer();
|
|
2032
|
+
}
|
|
2033
|
+
/** Start auto-close timer */
|
|
2034
|
+
startTimer(duration) {
|
|
2035
|
+
this.remainingTime.set(duration);
|
|
2036
|
+
this.timerSubscription = interval(100)
|
|
2037
|
+
.pipe(takeWhile(() => this.remainingTime() > 0), filter(() => !this.isPaused()))
|
|
2038
|
+
.subscribe(() => {
|
|
2039
|
+
this.remainingTime.update((t) => t - 100);
|
|
2040
|
+
if (this.remainingTime() <= 0) {
|
|
2041
|
+
this.dismissToast();
|
|
2042
|
+
}
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
/** Stop timer */
|
|
2046
|
+
stopTimer() {
|
|
2047
|
+
if (this.timerSubscription) {
|
|
2048
|
+
this.timerSubscription.unsubscribe();
|
|
2049
|
+
this.timerSubscription = null;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
/** Dismiss this toast */
|
|
2053
|
+
dismissToast() {
|
|
2054
|
+
this.dismiss.emit(this.toast().id);
|
|
2055
|
+
}
|
|
2056
|
+
/** Handle close button click */
|
|
2057
|
+
onClose() {
|
|
2058
|
+
this.dismissToast();
|
|
2059
|
+
}
|
|
2060
|
+
/** Pause timer on mouse enter */
|
|
2061
|
+
onMouseEnter() {
|
|
2062
|
+
if (this.toast().pauseOnHover) {
|
|
2063
|
+
this.isPaused.set(true);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
/** Resume timer on mouse leave */
|
|
2067
|
+
onMouseLeave() {
|
|
2068
|
+
this.isPaused.set(false);
|
|
2069
|
+
}
|
|
2070
|
+
/** Pause timer on focus */
|
|
2071
|
+
onFocus() {
|
|
2072
|
+
this.isPaused.set(true);
|
|
2073
|
+
}
|
|
2074
|
+
/** Resume timer on blur */
|
|
2075
|
+
onBlur() {
|
|
2076
|
+
this.isPaused.set(false);
|
|
2077
|
+
}
|
|
2078
|
+
/** Dismiss on Escape key */
|
|
2079
|
+
onEscapeKey() {
|
|
2080
|
+
if (this.toast().dismissible) {
|
|
2081
|
+
this.dismissToast();
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmToastItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2085
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.9", type: LmToastItemComponent, isStandalone: true, selector: "luma-toast-item", inputs: { toast: { classPropertyName: "toast", publicName: "toast", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { dismiss: "dismiss" }, host: { listeners: { "mouseenter": "onMouseEnter()", "mouseleave": "onMouseLeave()", "focus": "onFocus()", "blur": "onBlur()", "keydown.escape": "onEscapeKey()" }, properties: { "class": "itemClasses()", "attr.role": "toast().role", "attr.aria-live": "toast().variant === \"error\" ? \"assertive\" : \"polite\"", "attr.aria-atomic": "\"true\"", "tabindex": "hasInteractiveElements() ? 0 : -1" } }, ngImport: i0, template: `
|
|
2086
|
+
<!-- Icon -->
|
|
2087
|
+
<div [class]="iconClasses()">
|
|
2088
|
+
@switch (toast().variant) {
|
|
2089
|
+
@case ('info') {
|
|
2090
|
+
<svg
|
|
2091
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
2092
|
+
viewBox="0 0 20 20"
|
|
2093
|
+
fill="currentColor"
|
|
2094
|
+
>
|
|
2095
|
+
<path
|
|
2096
|
+
fill-rule="evenodd"
|
|
2097
|
+
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z"
|
|
2098
|
+
clip-rule="evenodd"
|
|
2099
|
+
/>
|
|
2100
|
+
</svg>
|
|
2101
|
+
}
|
|
2102
|
+
@case ('success') {
|
|
2103
|
+
<svg
|
|
2104
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
2105
|
+
viewBox="0 0 20 20"
|
|
2106
|
+
fill="currentColor"
|
|
2107
|
+
>
|
|
2108
|
+
<path
|
|
2109
|
+
fill-rule="evenodd"
|
|
2110
|
+
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
|
2111
|
+
clip-rule="evenodd"
|
|
2112
|
+
/>
|
|
2113
|
+
</svg>
|
|
2114
|
+
}
|
|
2115
|
+
@case ('warning') {
|
|
2116
|
+
<svg
|
|
2117
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
2118
|
+
viewBox="0 0 20 20"
|
|
2119
|
+
fill="currentColor"
|
|
2120
|
+
>
|
|
2121
|
+
<path
|
|
2122
|
+
fill-rule="evenodd"
|
|
2123
|
+
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
|
|
2124
|
+
clip-rule="evenodd"
|
|
2125
|
+
/>
|
|
2126
|
+
</svg>
|
|
2127
|
+
}
|
|
2128
|
+
@case ('error') {
|
|
2129
|
+
<svg
|
|
2130
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
2131
|
+
viewBox="0 0 20 20"
|
|
2132
|
+
fill="currentColor"
|
|
2133
|
+
>
|
|
2134
|
+
<path
|
|
2135
|
+
fill-rule="evenodd"
|
|
2136
|
+
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
|
|
2137
|
+
clip-rule="evenodd"
|
|
2138
|
+
/>
|
|
2139
|
+
</svg>
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
</div>
|
|
2143
|
+
|
|
2144
|
+
<!-- Content -->
|
|
2145
|
+
<div [class]="contentClasses()">
|
|
2146
|
+
@if (toast().title) {
|
|
2147
|
+
<div [class]="titleClasses()">{{ toast().title }}</div>
|
|
2148
|
+
}
|
|
2149
|
+
<div [class]="messageClasses()">{{ toast().message }}</div>
|
|
2150
|
+
</div>
|
|
2151
|
+
|
|
2152
|
+
<!-- Close button -->
|
|
2153
|
+
@if (toast().dismissible) {
|
|
2154
|
+
<luma-toast-close [lmVariant]="toast().variant" (click)="onClose()" />
|
|
2155
|
+
}
|
|
2156
|
+
`, isInline: true, dependencies: [{ kind: "component", type: LmToastCloseComponent, selector: "luma-toast-close", inputs: ["lmVariant"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2157
|
+
}
|
|
2158
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmToastItemComponent, decorators: [{
|
|
2159
|
+
type: Component,
|
|
2160
|
+
args: [{
|
|
2161
|
+
selector: 'luma-toast-item',
|
|
2162
|
+
template: `
|
|
2163
|
+
<!-- Icon -->
|
|
2164
|
+
<div [class]="iconClasses()">
|
|
2165
|
+
@switch (toast().variant) {
|
|
2166
|
+
@case ('info') {
|
|
2167
|
+
<svg
|
|
2168
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
2169
|
+
viewBox="0 0 20 20"
|
|
2170
|
+
fill="currentColor"
|
|
2171
|
+
>
|
|
2172
|
+
<path
|
|
2173
|
+
fill-rule="evenodd"
|
|
2174
|
+
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z"
|
|
2175
|
+
clip-rule="evenodd"
|
|
2176
|
+
/>
|
|
2177
|
+
</svg>
|
|
2178
|
+
}
|
|
2179
|
+
@case ('success') {
|
|
2180
|
+
<svg
|
|
2181
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
2182
|
+
viewBox="0 0 20 20"
|
|
2183
|
+
fill="currentColor"
|
|
2184
|
+
>
|
|
2185
|
+
<path
|
|
2186
|
+
fill-rule="evenodd"
|
|
2187
|
+
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
|
2188
|
+
clip-rule="evenodd"
|
|
2189
|
+
/>
|
|
2190
|
+
</svg>
|
|
2191
|
+
}
|
|
2192
|
+
@case ('warning') {
|
|
2193
|
+
<svg
|
|
2194
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
2195
|
+
viewBox="0 0 20 20"
|
|
2196
|
+
fill="currentColor"
|
|
2197
|
+
>
|
|
2198
|
+
<path
|
|
2199
|
+
fill-rule="evenodd"
|
|
2200
|
+
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
|
|
2201
|
+
clip-rule="evenodd"
|
|
2202
|
+
/>
|
|
2203
|
+
</svg>
|
|
2204
|
+
}
|
|
2205
|
+
@case ('error') {
|
|
2206
|
+
<svg
|
|
2207
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
2208
|
+
viewBox="0 0 20 20"
|
|
2209
|
+
fill="currentColor"
|
|
2210
|
+
>
|
|
2211
|
+
<path
|
|
2212
|
+
fill-rule="evenodd"
|
|
2213
|
+
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
|
|
2214
|
+
clip-rule="evenodd"
|
|
2215
|
+
/>
|
|
2216
|
+
</svg>
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
</div>
|
|
2220
|
+
|
|
2221
|
+
<!-- Content -->
|
|
2222
|
+
<div [class]="contentClasses()">
|
|
2223
|
+
@if (toast().title) {
|
|
2224
|
+
<div [class]="titleClasses()">{{ toast().title }}</div>
|
|
2225
|
+
}
|
|
2226
|
+
<div [class]="messageClasses()">{{ toast().message }}</div>
|
|
2227
|
+
</div>
|
|
2228
|
+
|
|
2229
|
+
<!-- Close button -->
|
|
2230
|
+
@if (toast().dismissible) {
|
|
2231
|
+
<luma-toast-close [lmVariant]="toast().variant" (click)="onClose()" />
|
|
2232
|
+
}
|
|
2233
|
+
`,
|
|
2234
|
+
host: {
|
|
2235
|
+
'[class]': 'itemClasses()',
|
|
2236
|
+
'[attr.role]': 'toast().role',
|
|
2237
|
+
'[attr.aria-live]': 'toast().variant === "error" ? "assertive" : "polite"',
|
|
2238
|
+
'[attr.aria-atomic]': '"true"',
|
|
2239
|
+
'[tabindex]': 'hasInteractiveElements() ? 0 : -1',
|
|
2240
|
+
'(mouseenter)': 'onMouseEnter()',
|
|
2241
|
+
'(mouseleave)': 'onMouseLeave()',
|
|
2242
|
+
'(focus)': 'onFocus()',
|
|
2243
|
+
'(blur)': 'onBlur()',
|
|
2244
|
+
'(keydown.escape)': 'onEscapeKey()',
|
|
2245
|
+
},
|
|
2246
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2247
|
+
imports: [LmToastCloseComponent],
|
|
2248
|
+
}]
|
|
2249
|
+
}], propDecorators: { toast: [{ type: i0.Input, args: [{ isSignal: true, alias: "toast", required: true }] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }] } });
|
|
2250
|
+
|
|
2251
|
+
/**
|
|
2252
|
+
* ToastContainerComponent
|
|
2253
|
+
*
|
|
2254
|
+
* Fixed-position container that renders all active toasts.
|
|
2255
|
+
* Supports all 6 positions simultaneously by grouping toasts by their position.
|
|
2256
|
+
*
|
|
2257
|
+
* @internal This component is created programmatically by ToastService
|
|
2258
|
+
*/
|
|
2259
|
+
class LmToastContainerComponent {
|
|
2260
|
+
/** Toasts signal passed from ToastService */
|
|
2261
|
+
_toasts;
|
|
2262
|
+
/** Dismiss callback passed from ToastService */
|
|
2263
|
+
_onDismiss;
|
|
2264
|
+
/** Group toasts by their position */
|
|
2265
|
+
toastsByPosition = computed(() => {
|
|
2266
|
+
const groups = {
|
|
2267
|
+
'top-left': [],
|
|
2268
|
+
'top-center': [],
|
|
2269
|
+
'top-right': [],
|
|
2270
|
+
'bottom-left': [],
|
|
2271
|
+
'bottom-center': [],
|
|
2272
|
+
'bottom-right': [],
|
|
2273
|
+
};
|
|
2274
|
+
if (!this._toasts)
|
|
2275
|
+
return groups;
|
|
2276
|
+
for (const toast of this._toasts()) {
|
|
2277
|
+
const position = toast.position;
|
|
2278
|
+
if (groups[position]) {
|
|
2279
|
+
groups[position].push(toast);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
return groups;
|
|
2283
|
+
}, ...(ngDevMode ? [{ debugName: "toastsByPosition" }] : []));
|
|
2284
|
+
/** Get CSS classes for a specific position */
|
|
2285
|
+
getPositionClasses(position) {
|
|
2286
|
+
return toastContainerVariants({ position });
|
|
2287
|
+
}
|
|
2288
|
+
/** Handle dismiss event from toast item */
|
|
2289
|
+
onDismiss(id) {
|
|
2290
|
+
this._onDismiss?.(id);
|
|
2291
|
+
}
|
|
2292
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmToastContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2293
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.9", type: LmToastContainerComponent, isStandalone: true, selector: "luma-toast-container", ngImport: i0, template: `
|
|
2294
|
+
<!-- Top Left -->
|
|
2295
|
+
@if (toastsByPosition()['top-left'].length) {
|
|
2296
|
+
<div
|
|
2297
|
+
role="region"
|
|
2298
|
+
aria-label="Top left notifications"
|
|
2299
|
+
[class]="getPositionClasses('top-left')"
|
|
2300
|
+
>
|
|
2301
|
+
@for (toast of toastsByPosition()['top-left']; track toast.id) {
|
|
2302
|
+
<luma-toast-item [toast]="toast" (dismiss)="onDismiss($event)" />
|
|
2303
|
+
}
|
|
2304
|
+
</div>
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
<!-- Top Center -->
|
|
2308
|
+
@if (toastsByPosition()['top-center'].length) {
|
|
2309
|
+
<div
|
|
2310
|
+
role="region"
|
|
2311
|
+
aria-label="Top center notifications"
|
|
2312
|
+
[class]="getPositionClasses('top-center')"
|
|
2313
|
+
>
|
|
2314
|
+
@for (toast of toastsByPosition()['top-center']; track toast.id) {
|
|
2315
|
+
<luma-toast-item [toast]="toast" (dismiss)="onDismiss($event)" />
|
|
2316
|
+
}
|
|
2317
|
+
</div>
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
<!-- Top Right -->
|
|
2321
|
+
@if (toastsByPosition()['top-right'].length) {
|
|
2322
|
+
<div
|
|
2323
|
+
role="region"
|
|
2324
|
+
aria-label="Top right notifications"
|
|
2325
|
+
[class]="getPositionClasses('top-right')"
|
|
2326
|
+
>
|
|
2327
|
+
@for (toast of toastsByPosition()['top-right']; track toast.id) {
|
|
2328
|
+
<luma-toast-item [toast]="toast" (dismiss)="onDismiss($event)" />
|
|
2329
|
+
}
|
|
2330
|
+
</div>
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
<!-- Bottom Left -->
|
|
2334
|
+
@if (toastsByPosition()['bottom-left'].length) {
|
|
2335
|
+
<div
|
|
2336
|
+
role="region"
|
|
2337
|
+
aria-label="Bottom left notifications"
|
|
2338
|
+
[class]="getPositionClasses('bottom-left')"
|
|
2339
|
+
>
|
|
2340
|
+
@for (toast of toastsByPosition()['bottom-left']; track toast.id) {
|
|
2341
|
+
<luma-toast-item [toast]="toast" (dismiss)="onDismiss($event)" />
|
|
2342
|
+
}
|
|
2343
|
+
</div>
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
<!-- Bottom Center -->
|
|
2347
|
+
@if (toastsByPosition()['bottom-center'].length) {
|
|
2348
|
+
<div
|
|
2349
|
+
role="region"
|
|
2350
|
+
aria-label="Bottom center notifications"
|
|
2351
|
+
[class]="getPositionClasses('bottom-center')"
|
|
2352
|
+
>
|
|
2353
|
+
@for (toast of toastsByPosition()['bottom-center']; track toast.id) {
|
|
2354
|
+
<luma-toast-item [toast]="toast" (dismiss)="onDismiss($event)" />
|
|
2355
|
+
}
|
|
2356
|
+
</div>
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
<!-- Bottom Right -->
|
|
2360
|
+
@if (toastsByPosition()['bottom-right'].length) {
|
|
2361
|
+
<div
|
|
2362
|
+
role="region"
|
|
2363
|
+
aria-label="Bottom right notifications"
|
|
2364
|
+
[class]="getPositionClasses('bottom-right')"
|
|
2365
|
+
>
|
|
2366
|
+
@for (toast of toastsByPosition()['bottom-right']; track toast.id) {
|
|
2367
|
+
<luma-toast-item [toast]="toast" (dismiss)="onDismiss($event)" />
|
|
2368
|
+
}
|
|
2369
|
+
</div>
|
|
2370
|
+
}
|
|
2371
|
+
`, isInline: true, dependencies: [{ kind: "component", type: LmToastItemComponent, selector: "luma-toast-item", inputs: ["toast"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2372
|
+
}
|
|
2373
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmToastContainerComponent, decorators: [{
|
|
2374
|
+
type: Component,
|
|
2375
|
+
args: [{
|
|
2376
|
+
selector: 'luma-toast-container',
|
|
2377
|
+
template: `
|
|
2378
|
+
<!-- Top Left -->
|
|
2379
|
+
@if (toastsByPosition()['top-left'].length) {
|
|
2380
|
+
<div
|
|
2381
|
+
role="region"
|
|
2382
|
+
aria-label="Top left notifications"
|
|
2383
|
+
[class]="getPositionClasses('top-left')"
|
|
2384
|
+
>
|
|
2385
|
+
@for (toast of toastsByPosition()['top-left']; track toast.id) {
|
|
2386
|
+
<luma-toast-item [toast]="toast" (dismiss)="onDismiss($event)" />
|
|
2387
|
+
}
|
|
2388
|
+
</div>
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
<!-- Top Center -->
|
|
2392
|
+
@if (toastsByPosition()['top-center'].length) {
|
|
2393
|
+
<div
|
|
2394
|
+
role="region"
|
|
2395
|
+
aria-label="Top center notifications"
|
|
2396
|
+
[class]="getPositionClasses('top-center')"
|
|
2397
|
+
>
|
|
2398
|
+
@for (toast of toastsByPosition()['top-center']; track toast.id) {
|
|
2399
|
+
<luma-toast-item [toast]="toast" (dismiss)="onDismiss($event)" />
|
|
2400
|
+
}
|
|
2401
|
+
</div>
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
<!-- Top Right -->
|
|
2405
|
+
@if (toastsByPosition()['top-right'].length) {
|
|
2406
|
+
<div
|
|
2407
|
+
role="region"
|
|
2408
|
+
aria-label="Top right notifications"
|
|
2409
|
+
[class]="getPositionClasses('top-right')"
|
|
2410
|
+
>
|
|
2411
|
+
@for (toast of toastsByPosition()['top-right']; track toast.id) {
|
|
2412
|
+
<luma-toast-item [toast]="toast" (dismiss)="onDismiss($event)" />
|
|
2413
|
+
}
|
|
2414
|
+
</div>
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
<!-- Bottom Left -->
|
|
2418
|
+
@if (toastsByPosition()['bottom-left'].length) {
|
|
2419
|
+
<div
|
|
2420
|
+
role="region"
|
|
2421
|
+
aria-label="Bottom left notifications"
|
|
2422
|
+
[class]="getPositionClasses('bottom-left')"
|
|
2423
|
+
>
|
|
2424
|
+
@for (toast of toastsByPosition()['bottom-left']; track toast.id) {
|
|
2425
|
+
<luma-toast-item [toast]="toast" (dismiss)="onDismiss($event)" />
|
|
2426
|
+
}
|
|
2427
|
+
</div>
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
<!-- Bottom Center -->
|
|
2431
|
+
@if (toastsByPosition()['bottom-center'].length) {
|
|
2432
|
+
<div
|
|
2433
|
+
role="region"
|
|
2434
|
+
aria-label="Bottom center notifications"
|
|
2435
|
+
[class]="getPositionClasses('bottom-center')"
|
|
2436
|
+
>
|
|
2437
|
+
@for (toast of toastsByPosition()['bottom-center']; track toast.id) {
|
|
2438
|
+
<luma-toast-item [toast]="toast" (dismiss)="onDismiss($event)" />
|
|
2439
|
+
}
|
|
2440
|
+
</div>
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
<!-- Bottom Right -->
|
|
2444
|
+
@if (toastsByPosition()['bottom-right'].length) {
|
|
2445
|
+
<div
|
|
2446
|
+
role="region"
|
|
2447
|
+
aria-label="Bottom right notifications"
|
|
2448
|
+
[class]="getPositionClasses('bottom-right')"
|
|
2449
|
+
>
|
|
2450
|
+
@for (toast of toastsByPosition()['bottom-right']; track toast.id) {
|
|
2451
|
+
<luma-toast-item [toast]="toast" (dismiss)="onDismiss($event)" />
|
|
2452
|
+
}
|
|
2453
|
+
</div>
|
|
2454
|
+
}
|
|
2455
|
+
`,
|
|
2456
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2457
|
+
imports: [LmToastItemComponent],
|
|
2458
|
+
}]
|
|
2459
|
+
}] });
|
|
2460
|
+
|
|
2461
|
+
/**
|
|
2462
|
+
* ToastService
|
|
2463
|
+
*
|
|
2464
|
+
* Injectable service for showing toast notifications programmatically.
|
|
2465
|
+
* Provides convenience methods for info, success, warning, and error toasts.
|
|
2466
|
+
*
|
|
2467
|
+
* @example
|
|
2468
|
+
* ```typescript
|
|
2469
|
+
* private toast = inject(ToastService);
|
|
2470
|
+
*
|
|
2471
|
+
* showSuccess() {
|
|
2472
|
+
* this.toast.success('Changes saved successfully!');
|
|
2473
|
+
* }
|
|
2474
|
+
*
|
|
2475
|
+
* showError() {
|
|
2476
|
+
* this.toast.error('Failed to save', {
|
|
2477
|
+
* title: 'Error',
|
|
2478
|
+
* duration: 0
|
|
2479
|
+
* });
|
|
2480
|
+
* }
|
|
2481
|
+
* ```
|
|
2482
|
+
*/
|
|
2483
|
+
class LmToastService {
|
|
2484
|
+
config = inject(TOAST_CONFIG);
|
|
2485
|
+
appRef = inject(ApplicationRef);
|
|
2486
|
+
injector = inject(Injector);
|
|
2487
|
+
document = inject(DOCUMENT);
|
|
2488
|
+
platformId = inject(PLATFORM_ID);
|
|
2489
|
+
liveAnnouncer = inject(LiveAnnouncer);
|
|
2490
|
+
_toasts = signal([], ...(ngDevMode ? [{ debugName: "_toasts" }] : []));
|
|
2491
|
+
containerRef = null;
|
|
2492
|
+
nextId = 0;
|
|
2493
|
+
toastRefs = new Map();
|
|
2494
|
+
/** Observable list of current toasts */
|
|
2495
|
+
toasts = computed(() => this._toasts(), ...(ngDevMode ? [{ debugName: "toasts" }] : []));
|
|
2496
|
+
ngOnDestroy() {
|
|
2497
|
+
this.destroyContainer();
|
|
2498
|
+
}
|
|
2499
|
+
/**
|
|
2500
|
+
* Show a toast notification
|
|
2501
|
+
* @param options - Toast configuration options
|
|
2502
|
+
* @returns ToastRef for programmatic control
|
|
2503
|
+
*/
|
|
2504
|
+
show(options) {
|
|
2505
|
+
this.ensureContainer();
|
|
2506
|
+
const toast = {
|
|
2507
|
+
id: `toast-${this.nextId++}`,
|
|
2508
|
+
message: options.message,
|
|
2509
|
+
title: options.title ?? '',
|
|
2510
|
+
variant: options.variant ?? 'info',
|
|
2511
|
+
position: options.position ?? this.config.position,
|
|
2512
|
+
duration: options.duration ?? this.config.duration,
|
|
2513
|
+
dismissible: options.dismissible ?? this.config.dismissible,
|
|
2514
|
+
pauseOnHover: options.pauseOnHover ?? this.config.pauseOnHover,
|
|
2515
|
+
role: options.role ?? (options.variant === 'error' ? 'alert' : 'status'),
|
|
2516
|
+
createdAt: Date.now(),
|
|
2517
|
+
isExiting: false,
|
|
2518
|
+
};
|
|
2519
|
+
// Enforce max visible limit
|
|
2520
|
+
const currentToasts = this._toasts();
|
|
2521
|
+
if (currentToasts.length >= this.config.maxVisible) {
|
|
2522
|
+
// Remove oldest toast
|
|
2523
|
+
const oldest = currentToasts[0];
|
|
2524
|
+
this.dismiss(oldest.id);
|
|
2525
|
+
}
|
|
2526
|
+
this._toasts.update((toasts) => [...toasts, toast]);
|
|
2527
|
+
this.announceToast(toast);
|
|
2528
|
+
const toastRef = new ToastRefImpl(toast.id, (id) => this.dismiss(id));
|
|
2529
|
+
this.toastRefs.set(toast.id, toastRef);
|
|
2530
|
+
return toastRef;
|
|
2531
|
+
}
|
|
2532
|
+
/**
|
|
2533
|
+
* Show info toast
|
|
2534
|
+
* @param message - Toast message
|
|
2535
|
+
* @param options - Additional options
|
|
2536
|
+
*/
|
|
2537
|
+
info(message, options) {
|
|
2538
|
+
return this.show({ ...options, message, variant: 'info' });
|
|
2539
|
+
}
|
|
2540
|
+
/**
|
|
2541
|
+
* Show success toast
|
|
2542
|
+
* @param message - Toast message
|
|
2543
|
+
* @param options - Additional options
|
|
2544
|
+
*/
|
|
2545
|
+
success(message, options) {
|
|
2546
|
+
return this.show({ ...options, message, variant: 'success' });
|
|
2547
|
+
}
|
|
2548
|
+
/**
|
|
2549
|
+
* Show warning toast
|
|
2550
|
+
* @param message - Toast message
|
|
2551
|
+
* @param options - Additional options
|
|
2552
|
+
*/
|
|
2553
|
+
warning(message, options) {
|
|
2554
|
+
return this.show({ ...options, message, variant: 'warning' });
|
|
2555
|
+
}
|
|
2556
|
+
/**
|
|
2557
|
+
* Show error toast
|
|
2558
|
+
* @param message - Toast message
|
|
2559
|
+
* @param options - Additional options
|
|
2560
|
+
*/
|
|
2561
|
+
error(message, options) {
|
|
2562
|
+
return this.show({ ...options, message, variant: 'error' });
|
|
2563
|
+
}
|
|
2564
|
+
/**
|
|
2565
|
+
* Dismiss a specific toast
|
|
2566
|
+
* @param id - Toast ID to dismiss
|
|
2567
|
+
*/
|
|
2568
|
+
dismiss(id) {
|
|
2569
|
+
// Mark as exiting for animation
|
|
2570
|
+
this._toasts.update((toasts) => toasts.map((t) => (t.id === id ? { ...t, isExiting: true } : t)));
|
|
2571
|
+
// Remove after animation completes (200ms)
|
|
2572
|
+
setTimeout(() => {
|
|
2573
|
+
this._toasts.update((toasts) => toasts.filter((t) => t.id !== id));
|
|
2574
|
+
// Notify ref
|
|
2575
|
+
const toastRef = this.toastRefs.get(id);
|
|
2576
|
+
if (toastRef) {
|
|
2577
|
+
toastRef._notifyDismissed();
|
|
2578
|
+
this.toastRefs.delete(id);
|
|
2579
|
+
}
|
|
2580
|
+
}, 200);
|
|
2581
|
+
}
|
|
2582
|
+
/**
|
|
2583
|
+
* Dismiss all toasts
|
|
2584
|
+
*/
|
|
2585
|
+
dismissAll() {
|
|
2586
|
+
const ids = this._toasts().map((t) => t.id);
|
|
2587
|
+
ids.forEach((id) => this.dismiss(id));
|
|
2588
|
+
}
|
|
2589
|
+
/**
|
|
2590
|
+
* Ensure toast container exists in DOM
|
|
2591
|
+
*/
|
|
2592
|
+
ensureContainer() {
|
|
2593
|
+
if (!isPlatformBrowser(this.platformId))
|
|
2594
|
+
return;
|
|
2595
|
+
if (this.containerRef)
|
|
2596
|
+
return;
|
|
2597
|
+
this.containerRef = createComponent(LmToastContainerComponent, {
|
|
2598
|
+
environmentInjector: this.appRef.injector,
|
|
2599
|
+
elementInjector: this.injector,
|
|
2600
|
+
});
|
|
2601
|
+
// Pass toasts signal to container
|
|
2602
|
+
this.containerRef.instance._toasts = this._toasts;
|
|
2603
|
+
this.containerRef.instance._onDismiss = (id) => this.dismiss(id);
|
|
2604
|
+
this.appRef.attachView(this.containerRef.hostView);
|
|
2605
|
+
this.document.body.appendChild(this.containerRef.location.nativeElement);
|
|
2606
|
+
}
|
|
2607
|
+
/**
|
|
2608
|
+
* Remove container from DOM
|
|
2609
|
+
*/
|
|
2610
|
+
destroyContainer() {
|
|
2611
|
+
if (this.containerRef) {
|
|
2612
|
+
this.appRef.detachView(this.containerRef.hostView);
|
|
2613
|
+
this.containerRef.destroy();
|
|
2614
|
+
this.containerRef = null;
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
/**
|
|
2618
|
+
* Announce toast to screen readers
|
|
2619
|
+
*/
|
|
2620
|
+
announceToast(toast) {
|
|
2621
|
+
const prefix = this.getVariantPrefix(toast.variant);
|
|
2622
|
+
const message = toast.title
|
|
2623
|
+
? `${prefix}: ${toast.title}. ${toast.message}`
|
|
2624
|
+
: `${prefix}: ${toast.message}`;
|
|
2625
|
+
const politeness = toast.variant === 'error' ? 'assertive' : 'polite';
|
|
2626
|
+
this.liveAnnouncer.announce(message, politeness);
|
|
2627
|
+
}
|
|
2628
|
+
/**
|
|
2629
|
+
* Get announcement prefix for variant
|
|
2630
|
+
*/
|
|
2631
|
+
getVariantPrefix(variant) {
|
|
2632
|
+
const prefixes = {
|
|
2633
|
+
info: 'Information',
|
|
2634
|
+
success: 'Success',
|
|
2635
|
+
warning: 'Warning',
|
|
2636
|
+
error: 'Error',
|
|
2637
|
+
};
|
|
2638
|
+
return prefixes[variant];
|
|
2639
|
+
}
|
|
2640
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2641
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmToastService, providedIn: 'root' });
|
|
2642
|
+
}
|
|
2643
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmToastService, decorators: [{
|
|
2644
|
+
type: Injectable,
|
|
2645
|
+
args: [{ providedIn: 'root' }]
|
|
2646
|
+
}] });
|
|
2647
|
+
|
|
2648
|
+
// Toast public API
|
|
2649
|
+
|
|
120
2650
|
// Button exports
|
|
121
2651
|
|
|
122
2652
|
/**
|
|
123
2653
|
* Generated bundle index. Do not edit.
|
|
124
2654
|
*/
|
|
125
2655
|
|
|
126
|
-
export {
|
|
2656
|
+
export { ACCORDION_ITEM, DEFAULT_TOAST_CONFIG, LmAccordionContentDirective, LmAccordionGroupComponent, LmAccordionIconDirective, LmAccordionItemComponent, LmAccordionTitleDirective, LmAccordionTriggerDirective, LmBadgeDirective, LmButtonDirective, LmCardComponent, LmCardContentDirective, LmCardDescriptionDirective, LmCardHeaderDirective, LmCardTitleDirective, LmModalCloseComponent, LmModalComponent, LmModalContainerComponent, LmModalContentDirective, LmModalFooterDirective, LmModalHeaderDirective, LmModalOverlayComponent, LmModalTitleDirective, LmTabsComponent, LmTabsIndicatorComponent, LmTabsListDirective, LmTabsPanelDirective, LmTabsTriggerDirective, LmToastCloseComponent, LmToastContainerComponent, LmToastItemComponent, LmToastService, LmTooltipDirective, MODAL_CONTEXT, TABS_GROUP, TABS_LIST, TOAST_CONFIG, provideToastConfig };
|
|
127
2657
|
//# sourceMappingURL=lumaui-angular.mjs.map
|