@lumaui/angular 0.4.3 → 0.4.4
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 +1105 -3
- package/fesm2022/lumaui-angular.mjs.map +1 -1
- package/package.json +3 -3
- package/types/lumaui-angular.d.ts +360 -5
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { input, computed, HostBinding, Directive, ChangeDetectionStrategy, Component, output, InjectionToken,
|
|
3
|
-
import { buttonVariants, badgeVariants, cardVariants, cardTitleVariants, cardDescriptionVariants, cardHeaderVariants, cardContentVariants, accordionItemVariants, accordionContentWrapperVariants, accordionTriggerVariants, accordionTitleVariants, accordionIconVariants, accordionContentVariants, tooltipVariants, tabsListVariants, tabsScrollArrowVariants, tabsTriggerVariants, tabsPanelVariants, tabsIndicatorVariants, modalOverlayVariants, modalContainerVariants, modalHeaderVariants, modalTitleVariants, modalContentVariants, modalFooterVariants, modalCloseVariants, toastCloseVariants, toastItemVariants, toastIconVariants, toastContentVariants, toastTitleVariants, toastMessageVariants, toastContainerVariants } from '@lumaui/core';
|
|
2
|
+
import { input, computed, HostBinding, Directive, inject, Injector, ElementRef, DestroyRef, signal, forwardRef, ChangeDetectionStrategy, Component, output, InjectionToken, Renderer2, effect, HostListener, PLATFORM_ID, NgZone, viewChild, TemplateRef, ViewContainerRef, ApplicationRef, createComponent, Injectable } from '@angular/core';
|
|
3
|
+
import { buttonVariants, badgeVariants, inputVariants, labelVariants, helperTextVariants, errorTextVariants, textareaVariants, checkboxVariants, radioVariants, cardVariants, cardTitleVariants, cardDescriptionVariants, cardHeaderVariants, cardContentVariants, accordionItemVariants, accordionContentWrapperVariants, accordionTriggerVariants, accordionTitleVariants, accordionIconVariants, accordionContentVariants, tooltipVariants, tabsListVariants, tabsScrollArrowVariants, tabsTriggerVariants, tabsPanelVariants, tabsIndicatorVariants, modalOverlayVariants, modalContainerVariants, modalHeaderVariants, modalTitleVariants, modalContentVariants, modalFooterVariants, modalCloseVariants, toastCloseVariants, toastItemVariants, toastIconVariants, toastContentVariants, toastTitleVariants, toastMessageVariants, toastContainerVariants, selectTriggerVariants, selectListboxVariants, selectSearchInputVariants, selectChipVariants, selectChipDismissVariants, selectOptionVariants } from '@lumaui/core';
|
|
4
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
5
|
+
import { NgControl, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
4
6
|
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
|
|
5
7
|
import { Subject, interval } from 'rxjs';
|
|
6
8
|
import { takeWhile, filter } from 'rxjs/operators';
|
|
@@ -63,6 +65,668 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
|
|
|
63
65
|
args: ['class']
|
|
64
66
|
}] } });
|
|
65
67
|
|
|
68
|
+
let inputIdCounter = 0;
|
|
69
|
+
/**
|
|
70
|
+
* Input directive with CVA variants and Angular Forms integration
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* <input lumaInput lmSize="md" />
|
|
74
|
+
*/
|
|
75
|
+
class LmInputDirective {
|
|
76
|
+
// Use Injector to avoid circular dependency
|
|
77
|
+
injector = inject(Injector);
|
|
78
|
+
elementRef = inject((ElementRef));
|
|
79
|
+
ngControl = null;
|
|
80
|
+
destroyRef = inject(DestroyRef);
|
|
81
|
+
// Inputs
|
|
82
|
+
lmSize = input('md', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
|
|
83
|
+
lmError = input(false, ...(ngDevMode ? [{ debugName: "lmError" }] : []));
|
|
84
|
+
lmDisabled = input(false, ...(ngDevMode ? [{ debugName: "lmDisabled" }] : []));
|
|
85
|
+
lmRequired = input(false, ...(ngDevMode ? [{ debugName: "lmRequired" }] : []));
|
|
86
|
+
// Internal signal for disabled state coming from FormControl.disable()
|
|
87
|
+
_formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_formDisabled" }] : []));
|
|
88
|
+
// Internal signals for form validation state (bridging non-signal FormControl to signal graph)
|
|
89
|
+
_formInvalid = signal(false, ...(ngDevMode ? [{ debugName: "_formInvalid" }] : []));
|
|
90
|
+
_formTouched = signal(false, ...(ngDevMode ? [{ debugName: "_formTouched" }] : []));
|
|
91
|
+
// Final disabled state: manual lmDisabled OR programmatic FormControl.disable()
|
|
92
|
+
isDisabled = computed(() => this.lmDisabled() || this._formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
|
|
93
|
+
// Auto-generated ID
|
|
94
|
+
elementId = signal(`luma-input-${inputIdCounter++}`, ...(ngDevMode ? [{ debugName: "elementId" }] : []));
|
|
95
|
+
// ARIA describedby (will be set by parent or manually)
|
|
96
|
+
describedBy = signal(null, ...(ngDevMode ? [{ debugName: "describedBy" }] : []));
|
|
97
|
+
// Computed signal — reactive to both manual lmError and FormControl state
|
|
98
|
+
hasError = computed(() => this.lmError() || (this._formInvalid() && this._formTouched()), ...(ngDevMode ? [{ debugName: "hasError" }] : []));
|
|
99
|
+
// Computed classes
|
|
100
|
+
classes = computed(() => inputVariants({
|
|
101
|
+
size: this.lmSize(),
|
|
102
|
+
error: this.hasError(),
|
|
103
|
+
}), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
104
|
+
// noop – replaced at runtime by registerOnChange / registerOnTouched
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
106
|
+
onChange = () => { };
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
108
|
+
onTouched = () => { };
|
|
109
|
+
ngOnInit() {
|
|
110
|
+
// Get NgControl from Injector to avoid circular dependency
|
|
111
|
+
try {
|
|
112
|
+
this.ngControl = this.injector.get(NgControl, null, {
|
|
113
|
+
optional: true,
|
|
114
|
+
self: true,
|
|
115
|
+
});
|
|
116
|
+
if (this.ngControl) {
|
|
117
|
+
this.ngControl.valueAccessor = this;
|
|
118
|
+
const control = this.ngControl.control;
|
|
119
|
+
if (control) {
|
|
120
|
+
// Capture immediate state (e.g. markAsTouched() called before ngOnInit)
|
|
121
|
+
this._formInvalid.set(control.invalid);
|
|
122
|
+
this._formTouched.set(control.touched);
|
|
123
|
+
// React to any future FormControl state changes
|
|
124
|
+
// control.events emits for ALL changes: value, status, touched, pristine
|
|
125
|
+
// statusChanges alone does NOT emit for markAsTouched()
|
|
126
|
+
control.events
|
|
127
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
128
|
+
.subscribe(() => {
|
|
129
|
+
this._formInvalid.set(control.invalid);
|
|
130
|
+
this._formTouched.set(control.touched);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// No NgControl present
|
|
137
|
+
this.ngControl = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
writeValue(value) {
|
|
141
|
+
const element = this.elementRef.nativeElement;
|
|
142
|
+
element.value = value ?? '';
|
|
143
|
+
}
|
|
144
|
+
registerOnChange(fn) {
|
|
145
|
+
this.onChange = fn;
|
|
146
|
+
}
|
|
147
|
+
registerOnTouched(fn) {
|
|
148
|
+
this.onTouched = fn;
|
|
149
|
+
}
|
|
150
|
+
setDisabledState(isDisabled) {
|
|
151
|
+
this._formDisabled.set(isDisabled);
|
|
152
|
+
}
|
|
153
|
+
onInput(event) {
|
|
154
|
+
const target = event.target;
|
|
155
|
+
this.onChange(target.value);
|
|
156
|
+
}
|
|
157
|
+
onBlur() {
|
|
158
|
+
this.onTouched();
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Set the aria-describedby attribute value
|
|
162
|
+
* This is typically called by parent components or directives
|
|
163
|
+
*/
|
|
164
|
+
setDescribedBy(value) {
|
|
165
|
+
this.describedBy.set(value);
|
|
166
|
+
}
|
|
167
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmInputDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
168
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmInputDirective, isStandalone: true, selector: "input[lumaInput]", inputs: { lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null }, lmError: { classPropertyName: "lmError", publicName: "lmError", isSignal: true, isRequired: false, transformFunction: null }, lmDisabled: { classPropertyName: "lmDisabled", publicName: "lmDisabled", isSignal: true, isRequired: false, transformFunction: null }, lmRequired: { classPropertyName: "lmRequired", publicName: "lmRequired", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "input": "onInput($event)", "blur": "onBlur()" }, properties: { "class": "classes()", "attr.id": "elementId()", "attr.aria-invalid": "hasError()", "attr.aria-describedby": "describedBy()", "attr.disabled": "isDisabled() ? \"\" : null", "attr.required": "lmRequired() ? \"\" : null" } }, providers: [
|
|
169
|
+
{
|
|
170
|
+
provide: NG_VALUE_ACCESSOR,
|
|
171
|
+
useExisting: forwardRef(() => LmInputDirective),
|
|
172
|
+
multi: true,
|
|
173
|
+
},
|
|
174
|
+
], ngImport: i0 });
|
|
175
|
+
}
|
|
176
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmInputDirective, decorators: [{
|
|
177
|
+
type: Directive,
|
|
178
|
+
args: [{
|
|
179
|
+
selector: 'input[lumaInput]',
|
|
180
|
+
standalone: true,
|
|
181
|
+
host: {
|
|
182
|
+
'[class]': 'classes()',
|
|
183
|
+
'[attr.id]': 'elementId()',
|
|
184
|
+
'[attr.aria-invalid]': 'hasError()',
|
|
185
|
+
'[attr.aria-describedby]': 'describedBy()',
|
|
186
|
+
'[attr.disabled]': 'isDisabled() ? "" : null',
|
|
187
|
+
'[attr.required]': 'lmRequired() ? "" : null',
|
|
188
|
+
'(input)': 'onInput($event)',
|
|
189
|
+
'(blur)': 'onBlur()',
|
|
190
|
+
},
|
|
191
|
+
providers: [
|
|
192
|
+
{
|
|
193
|
+
provide: NG_VALUE_ACCESSOR,
|
|
194
|
+
useExisting: forwardRef(() => LmInputDirective),
|
|
195
|
+
multi: true,
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
}]
|
|
199
|
+
}], propDecorators: { lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }], lmError: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmError", required: false }] }], lmDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDisabled", required: false }] }], lmRequired: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmRequired", required: false }] }] } });
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Label directive with required indicator support
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* <label lumaLabel for="email" lmRequired>Email</label>
|
|
206
|
+
*/
|
|
207
|
+
class LmLabelDirective {
|
|
208
|
+
for = input('', ...(ngDevMode ? [{ debugName: "for" }] : []));
|
|
209
|
+
lmRequired = input(false, ...(ngDevMode ? [{ debugName: "lmRequired" }] : []));
|
|
210
|
+
lmSize = input('md', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
|
|
211
|
+
lmInline = input(false, ...(ngDevMode ? [{ debugName: "lmInline" }] : []));
|
|
212
|
+
classes = computed(() => labelVariants({
|
|
213
|
+
size: this.lmSize(),
|
|
214
|
+
required: this.lmRequired(),
|
|
215
|
+
inline: this.lmInline(),
|
|
216
|
+
}), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
217
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmLabelDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
218
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmLabelDirective, isStandalone: true, selector: "label[lumaLabel]", inputs: { for: { classPropertyName: "for", publicName: "for", isSignal: true, isRequired: false, transformFunction: null }, lmRequired: { classPropertyName: "lmRequired", publicName: "lmRequired", isSignal: true, isRequired: false, transformFunction: null }, lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null }, lmInline: { classPropertyName: "lmInline", publicName: "lmInline", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.for": "for()" } }, ngImport: i0 });
|
|
219
|
+
}
|
|
220
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmLabelDirective, decorators: [{
|
|
221
|
+
type: Directive,
|
|
222
|
+
args: [{
|
|
223
|
+
selector: 'label[lumaLabel]',
|
|
224
|
+
standalone: true,
|
|
225
|
+
host: {
|
|
226
|
+
'[class]': 'classes()',
|
|
227
|
+
'[attr.for]': 'for()',
|
|
228
|
+
},
|
|
229
|
+
}]
|
|
230
|
+
}], propDecorators: { for: [{ type: i0.Input, args: [{ isSignal: true, alias: "for", required: false }] }], lmRequired: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmRequired", required: false }] }], lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }], lmInline: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmInline", required: false }] }] } });
|
|
231
|
+
|
|
232
|
+
let helperIdCounter = 0;
|
|
233
|
+
/**
|
|
234
|
+
* Helper text directive for displaying hint/description text
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* <span lumaHelperText>We'll never share your email.</span>
|
|
238
|
+
*/
|
|
239
|
+
class LmHelperTextDirective {
|
|
240
|
+
elementId = signal(`luma-helper-${helperIdCounter++}`, ...(ngDevMode ? [{ debugName: "elementId" }] : []));
|
|
241
|
+
lmSize = input('sm', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
|
|
242
|
+
classes = computed(() => helperTextVariants({ size: this.lmSize() }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
243
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmHelperTextDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
244
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmHelperTextDirective, isStandalone: true, selector: "[lumaHelperText]", inputs: { lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.id": "elementId()" } }, ngImport: i0 });
|
|
245
|
+
}
|
|
246
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmHelperTextDirective, decorators: [{
|
|
247
|
+
type: Directive,
|
|
248
|
+
args: [{
|
|
249
|
+
selector: '[lumaHelperText]',
|
|
250
|
+
standalone: true,
|
|
251
|
+
host: {
|
|
252
|
+
'[class]': 'classes()',
|
|
253
|
+
'[attr.id]': 'elementId()',
|
|
254
|
+
},
|
|
255
|
+
}]
|
|
256
|
+
}], propDecorators: { lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }] } });
|
|
257
|
+
|
|
258
|
+
let errorIdCounter = 0;
|
|
259
|
+
/**
|
|
260
|
+
* Error text directive for displaying validation error messages
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* <span lumaErrorText *ngIf="hasError">Password must be at least 8 characters.</span>
|
|
264
|
+
*/
|
|
265
|
+
class LmErrorTextDirective {
|
|
266
|
+
elementId = signal(`luma-error-${errorIdCounter++}`, ...(ngDevMode ? [{ debugName: "elementId" }] : []));
|
|
267
|
+
lmSize = input('sm', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
|
|
268
|
+
classes = computed(() => errorTextVariants({ size: this.lmSize() }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
269
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmErrorTextDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
270
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmErrorTextDirective, isStandalone: true, selector: "[lumaErrorText]", inputs: { lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.id": "elementId()" } }, ngImport: i0 });
|
|
271
|
+
}
|
|
272
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmErrorTextDirective, decorators: [{
|
|
273
|
+
type: Directive,
|
|
274
|
+
args: [{
|
|
275
|
+
selector: '[lumaErrorText]',
|
|
276
|
+
standalone: true,
|
|
277
|
+
host: {
|
|
278
|
+
'[class]': 'classes()',
|
|
279
|
+
'[attr.id]': 'elementId()',
|
|
280
|
+
},
|
|
281
|
+
}]
|
|
282
|
+
}], propDecorators: { lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }] } });
|
|
283
|
+
|
|
284
|
+
let textareaIdCounter = 0;
|
|
285
|
+
/**
|
|
286
|
+
* Textarea directive with CVA variants and Angular Forms integration
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* <textarea lumaTextarea lmSize="md"></textarea>
|
|
290
|
+
*/
|
|
291
|
+
class LmTextareaDirective {
|
|
292
|
+
// Use Injector to avoid circular dependency
|
|
293
|
+
injector = inject(Injector);
|
|
294
|
+
elementRef = inject((ElementRef));
|
|
295
|
+
ngControl = null;
|
|
296
|
+
destroyRef = inject(DestroyRef);
|
|
297
|
+
// Inputs
|
|
298
|
+
lmSize = input('md', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
|
|
299
|
+
lmError = input(false, ...(ngDevMode ? [{ debugName: "lmError" }] : []));
|
|
300
|
+
lmDisabled = input(false, ...(ngDevMode ? [{ debugName: "lmDisabled" }] : []));
|
|
301
|
+
lmRequired = input(false, ...(ngDevMode ? [{ debugName: "lmRequired" }] : []));
|
|
302
|
+
// Internal signal for disabled state coming from FormControl.disable()
|
|
303
|
+
_formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_formDisabled" }] : []));
|
|
304
|
+
// Internal signals for form validation state (bridging non-signal FormControl to signal graph)
|
|
305
|
+
_formInvalid = signal(false, ...(ngDevMode ? [{ debugName: "_formInvalid" }] : []));
|
|
306
|
+
_formTouched = signal(false, ...(ngDevMode ? [{ debugName: "_formTouched" }] : []));
|
|
307
|
+
// Final disabled state: manual lmDisabled OR programmatic FormControl.disable()
|
|
308
|
+
isDisabled = computed(() => this.lmDisabled() || this._formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
|
|
309
|
+
// Auto-generated ID
|
|
310
|
+
elementId = signal(`luma-textarea-${textareaIdCounter++}`, ...(ngDevMode ? [{ debugName: "elementId" }] : []));
|
|
311
|
+
// ARIA describedby (set by parent or manually)
|
|
312
|
+
describedBy = signal(null, ...(ngDevMode ? [{ debugName: "describedBy" }] : []));
|
|
313
|
+
// Computed signal — reactive to both manual lmError and FormControl state
|
|
314
|
+
hasError = computed(() => this.lmError() || (this._formInvalid() && this._formTouched()), ...(ngDevMode ? [{ debugName: "hasError" }] : []));
|
|
315
|
+
// Computed classes
|
|
316
|
+
classes = computed(() => textareaVariants({
|
|
317
|
+
size: this.lmSize(),
|
|
318
|
+
error: this.hasError(),
|
|
319
|
+
}), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
320
|
+
// noop – replaced at runtime by registerOnChange / registerOnTouched
|
|
321
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
322
|
+
onChange = () => { };
|
|
323
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
324
|
+
onTouched = () => { };
|
|
325
|
+
ngOnInit() {
|
|
326
|
+
// Get NgControl from Injector to avoid circular dependency
|
|
327
|
+
try {
|
|
328
|
+
this.ngControl = this.injector.get(NgControl, null, {
|
|
329
|
+
optional: true,
|
|
330
|
+
self: true,
|
|
331
|
+
});
|
|
332
|
+
if (this.ngControl) {
|
|
333
|
+
this.ngControl.valueAccessor = this;
|
|
334
|
+
const control = this.ngControl.control;
|
|
335
|
+
if (control) {
|
|
336
|
+
// Capture immediate state (e.g. markAsTouched() called before ngOnInit)
|
|
337
|
+
this._formInvalid.set(control.invalid);
|
|
338
|
+
this._formTouched.set(control.touched);
|
|
339
|
+
// React to any future FormControl state changes
|
|
340
|
+
// control.events emits for ALL changes: value, status, touched, pristine
|
|
341
|
+
// statusChanges alone does NOT emit for markAsTouched()
|
|
342
|
+
control.events
|
|
343
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
344
|
+
.subscribe(() => {
|
|
345
|
+
this._formInvalid.set(control.invalid);
|
|
346
|
+
this._formTouched.set(control.touched);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
// No NgControl present
|
|
353
|
+
this.ngControl = null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
writeValue(value) {
|
|
357
|
+
const element = this.elementRef.nativeElement;
|
|
358
|
+
element.value = value ?? '';
|
|
359
|
+
}
|
|
360
|
+
registerOnChange(fn) {
|
|
361
|
+
this.onChange = fn;
|
|
362
|
+
}
|
|
363
|
+
registerOnTouched(fn) {
|
|
364
|
+
this.onTouched = fn;
|
|
365
|
+
}
|
|
366
|
+
setDisabledState(isDisabled) {
|
|
367
|
+
this._formDisabled.set(isDisabled);
|
|
368
|
+
}
|
|
369
|
+
onInput(event) {
|
|
370
|
+
const target = event.target;
|
|
371
|
+
this.onChange(target.value);
|
|
372
|
+
}
|
|
373
|
+
onBlur() {
|
|
374
|
+
this.onTouched();
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Set the aria-describedby attribute value
|
|
378
|
+
* This is typically called by parent components or directives
|
|
379
|
+
*/
|
|
380
|
+
setDescribedBy(value) {
|
|
381
|
+
this.describedBy.set(value);
|
|
382
|
+
}
|
|
383
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTextareaDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
384
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmTextareaDirective, isStandalone: true, selector: "textarea[lumaTextarea]", inputs: { lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null }, lmError: { classPropertyName: "lmError", publicName: "lmError", isSignal: true, isRequired: false, transformFunction: null }, lmDisabled: { classPropertyName: "lmDisabled", publicName: "lmDisabled", isSignal: true, isRequired: false, transformFunction: null }, lmRequired: { classPropertyName: "lmRequired", publicName: "lmRequired", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "input": "onInput($event)", "blur": "onBlur()" }, properties: { "class": "classes()", "attr.id": "elementId()", "attr.aria-invalid": "hasError()", "attr.aria-describedby": "describedBy()", "attr.disabled": "isDisabled() ? \"\" : null", "attr.required": "lmRequired() ? \"\" : null" } }, providers: [
|
|
385
|
+
{
|
|
386
|
+
provide: NG_VALUE_ACCESSOR,
|
|
387
|
+
useExisting: forwardRef(() => LmTextareaDirective),
|
|
388
|
+
multi: true,
|
|
389
|
+
},
|
|
390
|
+
], ngImport: i0 });
|
|
391
|
+
}
|
|
392
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTextareaDirective, decorators: [{
|
|
393
|
+
type: Directive,
|
|
394
|
+
args: [{
|
|
395
|
+
selector: 'textarea[lumaTextarea]',
|
|
396
|
+
standalone: true,
|
|
397
|
+
host: {
|
|
398
|
+
'[class]': 'classes()',
|
|
399
|
+
'[attr.id]': 'elementId()',
|
|
400
|
+
'[attr.aria-invalid]': 'hasError()',
|
|
401
|
+
'[attr.aria-describedby]': 'describedBy()',
|
|
402
|
+
'[attr.disabled]': 'isDisabled() ? "" : null',
|
|
403
|
+
'[attr.required]': 'lmRequired() ? "" : null',
|
|
404
|
+
'(input)': 'onInput($event)',
|
|
405
|
+
'(blur)': 'onBlur()',
|
|
406
|
+
},
|
|
407
|
+
providers: [
|
|
408
|
+
{
|
|
409
|
+
provide: NG_VALUE_ACCESSOR,
|
|
410
|
+
useExisting: forwardRef(() => LmTextareaDirective),
|
|
411
|
+
multi: true,
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
}]
|
|
415
|
+
}], propDecorators: { lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }], lmError: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmError", required: false }] }], lmDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDisabled", required: false }] }], lmRequired: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmRequired", required: false }] }] } });
|
|
416
|
+
|
|
417
|
+
let checkboxIdCounter = 0;
|
|
418
|
+
// SVG checkmark encoded as a CSS background-image data URI (white stroke on transparent)
|
|
419
|
+
const CHECKMARK_SVG = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 8l3 3 5-5' stroke='white' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C%2Fsvg%3E\")";
|
|
420
|
+
/**
|
|
421
|
+
* Checkbox directive with CVA variants and Angular Forms integration.
|
|
422
|
+
* Supports boolean ControlValueAccessor (checked/unchecked).
|
|
423
|
+
*
|
|
424
|
+
* @example
|
|
425
|
+
* <input type="checkbox" lumaCheckbox [formControl]="agreedControl" />
|
|
426
|
+
*/
|
|
427
|
+
class LmCheckboxDirective {
|
|
428
|
+
// Use Injector to avoid circular dependency
|
|
429
|
+
injector = inject(Injector);
|
|
430
|
+
elementRef = inject((ElementRef));
|
|
431
|
+
ngControl = null;
|
|
432
|
+
destroyRef = inject(DestroyRef);
|
|
433
|
+
// Inputs
|
|
434
|
+
lmError = input(false, ...(ngDevMode ? [{ debugName: "lmError" }] : []));
|
|
435
|
+
lmDisabled = input(false, ...(ngDevMode ? [{ debugName: "lmDisabled" }] : []));
|
|
436
|
+
lmRequired = input(false, ...(ngDevMode ? [{ debugName: "lmRequired" }] : []));
|
|
437
|
+
// Internal signal for disabled state coming from FormControl.disable()
|
|
438
|
+
_formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_formDisabled" }] : []));
|
|
439
|
+
// Internal signals for form validation state (bridging non-signal FormControl to signal graph)
|
|
440
|
+
_formInvalid = signal(false, ...(ngDevMode ? [{ debugName: "_formInvalid" }] : []));
|
|
441
|
+
_formTouched = signal(false, ...(ngDevMode ? [{ debugName: "_formTouched" }] : []));
|
|
442
|
+
// Internal signal tracking whether the checkbox is currently checked
|
|
443
|
+
isChecked = signal(false, ...(ngDevMode ? [{ debugName: "isChecked" }] : []));
|
|
444
|
+
// Exposed for host binding (readonly reference to the constant)
|
|
445
|
+
checkmarkSvg = CHECKMARK_SVG;
|
|
446
|
+
// Final disabled state: manual lmDisabled OR programmatic FormControl.disable()
|
|
447
|
+
isDisabled = computed(() => this.lmDisabled() || this._formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
|
|
448
|
+
// Preserve static id="..." from template; fall back to auto-generated id.
|
|
449
|
+
// elementRef must be declared before this field so it's available at init time.
|
|
450
|
+
elementId = signal(this.elementRef.nativeElement.getAttribute('id') ||
|
|
451
|
+
`luma-checkbox-${checkboxIdCounter++}`, ...(ngDevMode ? [{ debugName: "elementId" }] : []));
|
|
452
|
+
// ARIA describedby (can be set by parent or manually)
|
|
453
|
+
describedBy = signal(null, ...(ngDevMode ? [{ debugName: "describedBy" }] : []));
|
|
454
|
+
// Computed signal — reactive to both manual lmError and FormControl state
|
|
455
|
+
hasError = computed(() => this.lmError() || (this._formInvalid() && this._formTouched()), ...(ngDevMode ? [{ debugName: "hasError" }] : []));
|
|
456
|
+
// Computed classes
|
|
457
|
+
classes = computed(() => checkboxVariants({ error: this.hasError() }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
458
|
+
// ControlValueAccessor implementation (boolean-typed)
|
|
459
|
+
// noop – replaced at runtime by registerOnChange / registerOnTouched
|
|
460
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
461
|
+
onChange = () => { };
|
|
462
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
463
|
+
onTouched = () => { };
|
|
464
|
+
ngOnInit() {
|
|
465
|
+
// Sync initial checked state from native element.
|
|
466
|
+
// Needed when a static `checked` attribute is used without a FormControl —
|
|
467
|
+
// writeValue() is never called in that case, so isChecked stays false.
|
|
468
|
+
this.isChecked.set(this.elementRef.nativeElement.checked);
|
|
469
|
+
// Get NgControl from Injector to avoid circular dependency
|
|
470
|
+
try {
|
|
471
|
+
this.ngControl = this.injector.get(NgControl, null, {
|
|
472
|
+
optional: true,
|
|
473
|
+
self: true,
|
|
474
|
+
});
|
|
475
|
+
if (this.ngControl) {
|
|
476
|
+
this.ngControl.valueAccessor = this;
|
|
477
|
+
const control = this.ngControl.control;
|
|
478
|
+
if (control) {
|
|
479
|
+
// Capture immediate state (e.g. markAsTouched() called before ngOnInit)
|
|
480
|
+
this._formInvalid.set(control.invalid);
|
|
481
|
+
this._formTouched.set(control.touched);
|
|
482
|
+
// React to any future FormControl state changes
|
|
483
|
+
// control.events emits for ALL changes: value, status, touched, pristine
|
|
484
|
+
// statusChanges alone does NOT emit for markAsTouched()
|
|
485
|
+
control.events
|
|
486
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
487
|
+
.subscribe(() => {
|
|
488
|
+
this._formInvalid.set(control.invalid);
|
|
489
|
+
this._formTouched.set(control.touched);
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
// No NgControl present
|
|
496
|
+
this.ngControl = null;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
writeValue(value) {
|
|
500
|
+
const checked = !!value;
|
|
501
|
+
this.elementRef.nativeElement.checked = checked;
|
|
502
|
+
this.isChecked.set(checked);
|
|
503
|
+
}
|
|
504
|
+
registerOnChange(fn) {
|
|
505
|
+
this.onChange = fn;
|
|
506
|
+
}
|
|
507
|
+
registerOnTouched(fn) {
|
|
508
|
+
this.onTouched = fn;
|
|
509
|
+
}
|
|
510
|
+
setDisabledState(isDisabled) {
|
|
511
|
+
this._formDisabled.set(isDisabled);
|
|
512
|
+
}
|
|
513
|
+
onChangeEvent(event) {
|
|
514
|
+
const checked = event.target.checked;
|
|
515
|
+
this.isChecked.set(checked);
|
|
516
|
+
this.onChange(checked);
|
|
517
|
+
}
|
|
518
|
+
onBlur() {
|
|
519
|
+
this.onTouched();
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Set the aria-describedby attribute value.
|
|
523
|
+
* Typically called by helper/error text directives.
|
|
524
|
+
*/
|
|
525
|
+
setDescribedBy(value) {
|
|
526
|
+
this.describedBy.set(value);
|
|
527
|
+
}
|
|
528
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmCheckboxDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
529
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmCheckboxDirective, isStandalone: true, selector: "input[type=\"checkbox\"][lumaCheckbox]", inputs: { lmError: { classPropertyName: "lmError", publicName: "lmError", isSignal: true, isRequired: false, transformFunction: null }, lmDisabled: { classPropertyName: "lmDisabled", publicName: "lmDisabled", isSignal: true, isRequired: false, transformFunction: null }, lmRequired: { classPropertyName: "lmRequired", publicName: "lmRequired", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "change": "onChangeEvent($event)", "blur": "onBlur()" }, properties: { "class": "classes()", "attr.id": "elementId()", "attr.aria-invalid": "hasError()", "attr.aria-describedby": "describedBy()", "attr.disabled": "isDisabled() ? \"\" : null", "attr.required": "lmRequired() ? \"\" : null", "style.background-image": "isChecked() ? checkmarkSvg : null", "style.background-repeat": "isChecked() ? \"no-repeat\" : null", "style.background-position": "isChecked() ? \"center\" : null", "style.background-size": "isChecked() ? \"75%\" : null" } }, providers: [
|
|
530
|
+
{
|
|
531
|
+
provide: NG_VALUE_ACCESSOR,
|
|
532
|
+
useExisting: forwardRef(() => LmCheckboxDirective),
|
|
533
|
+
multi: true,
|
|
534
|
+
},
|
|
535
|
+
], ngImport: i0 });
|
|
536
|
+
}
|
|
537
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmCheckboxDirective, decorators: [{
|
|
538
|
+
type: Directive,
|
|
539
|
+
args: [{
|
|
540
|
+
selector: 'input[type="checkbox"][lumaCheckbox]',
|
|
541
|
+
standalone: true,
|
|
542
|
+
host: {
|
|
543
|
+
'[class]': 'classes()',
|
|
544
|
+
'[attr.id]': 'elementId()',
|
|
545
|
+
'[attr.aria-invalid]': 'hasError()',
|
|
546
|
+
'[attr.aria-describedby]': 'describedBy()',
|
|
547
|
+
'[attr.disabled]': 'isDisabled() ? "" : null',
|
|
548
|
+
'[attr.required]': 'lmRequired() ? "" : null',
|
|
549
|
+
'[style.background-image]': 'isChecked() ? checkmarkSvg : null',
|
|
550
|
+
'[style.background-repeat]': 'isChecked() ? "no-repeat" : null',
|
|
551
|
+
'[style.background-position]': 'isChecked() ? "center" : null',
|
|
552
|
+
'[style.background-size]': 'isChecked() ? "75%" : null',
|
|
553
|
+
'(change)': 'onChangeEvent($event)',
|
|
554
|
+
'(blur)': 'onBlur()',
|
|
555
|
+
},
|
|
556
|
+
providers: [
|
|
557
|
+
{
|
|
558
|
+
provide: NG_VALUE_ACCESSOR,
|
|
559
|
+
useExisting: forwardRef(() => LmCheckboxDirective),
|
|
560
|
+
multi: true,
|
|
561
|
+
},
|
|
562
|
+
],
|
|
563
|
+
}]
|
|
564
|
+
}], propDecorators: { lmError: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmError", required: false }] }], lmDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDisabled", required: false }] }], lmRequired: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmRequired", required: false }] }] } });
|
|
565
|
+
|
|
566
|
+
let radioIdCounter = 0;
|
|
567
|
+
/**
|
|
568
|
+
* Radio button directive with CVA variants and Angular Forms integration.
|
|
569
|
+
* Emits `lmValue` to the FormControl when checked.
|
|
570
|
+
* The inner dot indicator is rendered via a signal-bound `box-shadow` style.
|
|
571
|
+
*
|
|
572
|
+
* @example
|
|
573
|
+
* <input type="radio" lumaRadio lmValue="option-a" [formControl]="optionControl" />
|
|
574
|
+
*/
|
|
575
|
+
class LmRadioDirective {
|
|
576
|
+
// Use Injector to avoid circular dependency
|
|
577
|
+
injector = inject(Injector);
|
|
578
|
+
elementRef = inject((ElementRef));
|
|
579
|
+
ngControl = null;
|
|
580
|
+
destroyRef = inject(DestroyRef);
|
|
581
|
+
// Inputs
|
|
582
|
+
lmValue = input(undefined, ...(ngDevMode ? [{ debugName: "lmValue" }] : []));
|
|
583
|
+
lmError = input(false, ...(ngDevMode ? [{ debugName: "lmError" }] : []));
|
|
584
|
+
lmDisabled = input(false, ...(ngDevMode ? [{ debugName: "lmDisabled" }] : []));
|
|
585
|
+
lmRequired = input(false, ...(ngDevMode ? [{ debugName: "lmRequired" }] : []));
|
|
586
|
+
// Internal signal for disabled state coming from FormControl.disable()
|
|
587
|
+
_formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_formDisabled" }] : []));
|
|
588
|
+
// Internal signals for form validation state (bridging non-signal FormControl to signal graph)
|
|
589
|
+
_formInvalid = signal(false, ...(ngDevMode ? [{ debugName: "_formInvalid" }] : []));
|
|
590
|
+
_formTouched = signal(false, ...(ngDevMode ? [{ debugName: "_formTouched" }] : []));
|
|
591
|
+
// Internal signal tracking whether this radio is currently selected
|
|
592
|
+
isChecked = signal(false, ...(ngDevMode ? [{ debugName: "isChecked" }] : []));
|
|
593
|
+
// Final disabled state: manual lmDisabled OR programmatic FormControl.disable()
|
|
594
|
+
isDisabled = computed(() => this.lmDisabled() || this._formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
|
|
595
|
+
// Preserve static id="..." from template; fall back to auto-generated id.
|
|
596
|
+
// elementRef must be declared before this field so it's available at init time.
|
|
597
|
+
elementId = signal(this.elementRef.nativeElement.getAttribute('id') ||
|
|
598
|
+
`luma-radio-${radioIdCounter++}`, ...(ngDevMode ? [{ debugName: "elementId" }] : []));
|
|
599
|
+
// ARIA describedby (can be set by parent or manually)
|
|
600
|
+
describedBy = signal(null, ...(ngDevMode ? [{ debugName: "describedBy" }] : []));
|
|
601
|
+
// Computed signal — reactive to both manual lmError and FormControl state
|
|
602
|
+
hasError = computed(() => this.lmError() || (this._formInvalid() && this._formTouched()), ...(ngDevMode ? [{ debugName: "hasError" }] : []));
|
|
603
|
+
// Computed classes
|
|
604
|
+
classes = computed(() => radioVariants({ error: this.hasError() }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
|
|
605
|
+
// noop – replaced at runtime by registerOnChange / registerOnTouched
|
|
606
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
607
|
+
onChange = () => { };
|
|
608
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
609
|
+
onTouched = () => { };
|
|
610
|
+
ngOnInit() {
|
|
611
|
+
// Sync initial checked state from native element.
|
|
612
|
+
// Needed when a static `checked` attribute is used without a FormControl —
|
|
613
|
+
// writeValue() is never called in that case, so isChecked stays false.
|
|
614
|
+
this.isChecked.set(this.elementRef.nativeElement.checked);
|
|
615
|
+
// Get NgControl from Injector to avoid circular dependency
|
|
616
|
+
try {
|
|
617
|
+
this.ngControl = this.injector.get(NgControl, null, {
|
|
618
|
+
optional: true,
|
|
619
|
+
self: true,
|
|
620
|
+
});
|
|
621
|
+
if (this.ngControl) {
|
|
622
|
+
this.ngControl.valueAccessor = this;
|
|
623
|
+
const control = this.ngControl.control;
|
|
624
|
+
if (control) {
|
|
625
|
+
// Capture immediate state (e.g. markAsTouched() called before ngOnInit)
|
|
626
|
+
this._formInvalid.set(control.invalid);
|
|
627
|
+
this._formTouched.set(control.touched);
|
|
628
|
+
// React to any future FormControl state changes
|
|
629
|
+
// control.events emits for ALL changes: value, status, touched, pristine
|
|
630
|
+
// statusChanges alone does NOT emit for markAsTouched()
|
|
631
|
+
control.events
|
|
632
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
633
|
+
.subscribe(() => {
|
|
634
|
+
this._formInvalid.set(control.invalid);
|
|
635
|
+
this._formTouched.set(control.touched);
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
// No NgControl present
|
|
642
|
+
this.ngControl = null;
|
|
643
|
+
}
|
|
644
|
+
// Standalone usage (no FormControl): when the user selects a sibling radio in the
|
|
645
|
+
// same name group, the browser silently sets this radio's .checked = false without
|
|
646
|
+
// firing a change event on it. We listen on document and re-read native state.
|
|
647
|
+
const el = this.elementRef.nativeElement;
|
|
648
|
+
const onSiblingChange = (e) => {
|
|
649
|
+
const target = e.target;
|
|
650
|
+
if (target !== el &&
|
|
651
|
+
target.type === 'radio' &&
|
|
652
|
+
!!el.name &&
|
|
653
|
+
target.name === el.name) {
|
|
654
|
+
this.isChecked.set(el.checked);
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
document.addEventListener('change', onSiblingChange);
|
|
658
|
+
this.destroyRef.onDestroy(() => document.removeEventListener('change', onSiblingChange));
|
|
659
|
+
}
|
|
660
|
+
writeValue(value) {
|
|
661
|
+
// Check the radio if the form value equals this radio's value
|
|
662
|
+
const checked = value === this.lmValue();
|
|
663
|
+
this.elementRef.nativeElement.checked = checked;
|
|
664
|
+
this.isChecked.set(checked);
|
|
665
|
+
}
|
|
666
|
+
registerOnChange(fn) {
|
|
667
|
+
this.onChange = fn;
|
|
668
|
+
}
|
|
669
|
+
registerOnTouched(fn) {
|
|
670
|
+
this.onTouched = fn;
|
|
671
|
+
}
|
|
672
|
+
setDisabledState(isDisabled) {
|
|
673
|
+
this._formDisabled.set(isDisabled);
|
|
674
|
+
}
|
|
675
|
+
onChangeEvent(event) {
|
|
676
|
+
// Only emit when this radio is checked (not when another radio in the group deselects it)
|
|
677
|
+
if (event.target.checked) {
|
|
678
|
+
this.isChecked.set(true);
|
|
679
|
+
this.onChange(this.lmValue());
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
onBlur() {
|
|
683
|
+
this.onTouched();
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Set the aria-describedby attribute value.
|
|
687
|
+
* Typically called by helper/error text directives.
|
|
688
|
+
*/
|
|
689
|
+
setDescribedBy(value) {
|
|
690
|
+
this.describedBy.set(value);
|
|
691
|
+
}
|
|
692
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmRadioDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
693
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmRadioDirective, isStandalone: true, selector: "input[type=\"radio\"][lumaRadio]", inputs: { lmValue: { classPropertyName: "lmValue", publicName: "lmValue", isSignal: true, isRequired: false, transformFunction: null }, lmError: { classPropertyName: "lmError", publicName: "lmError", isSignal: true, isRequired: false, transformFunction: null }, lmDisabled: { classPropertyName: "lmDisabled", publicName: "lmDisabled", isSignal: true, isRequired: false, transformFunction: null }, lmRequired: { classPropertyName: "lmRequired", publicName: "lmRequired", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "change": "onChangeEvent($event)", "blur": "onBlur()" }, properties: { "class": "classes()", "attr.id": "elementId()", "attr.aria-invalid": "hasError()", "attr.aria-describedby": "describedBy()", "attr.disabled": "isDisabled() ? \"\" : null", "attr.required": "lmRequired() ? \"\" : null", "style.box-shadow": "isChecked() ? \"inset 0 0 0 3px var(--color-background)\" : null", "style.background-color": "isChecked() ? \"var(--color-primary)\" : null", "style.border-color": "isChecked() ? \"var(--color-primary)\" : null" } }, providers: [
|
|
694
|
+
{
|
|
695
|
+
provide: NG_VALUE_ACCESSOR,
|
|
696
|
+
useExisting: forwardRef(() => LmRadioDirective),
|
|
697
|
+
multi: true,
|
|
698
|
+
},
|
|
699
|
+
], ngImport: i0 });
|
|
700
|
+
}
|
|
701
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmRadioDirective, decorators: [{
|
|
702
|
+
type: Directive,
|
|
703
|
+
args: [{
|
|
704
|
+
selector: 'input[type="radio"][lumaRadio]',
|
|
705
|
+
standalone: true,
|
|
706
|
+
host: {
|
|
707
|
+
'[class]': 'classes()',
|
|
708
|
+
'[attr.id]': 'elementId()',
|
|
709
|
+
'[attr.aria-invalid]': 'hasError()',
|
|
710
|
+
'[attr.aria-describedby]': 'describedBy()',
|
|
711
|
+
'[attr.disabled]': 'isDisabled() ? "" : null',
|
|
712
|
+
'[attr.required]': 'lmRequired() ? "" : null',
|
|
713
|
+
// Inner dot: background fills circle, inset shadow carves a gap between border and dot
|
|
714
|
+
'[style.box-shadow]': 'isChecked() ? "inset 0 0 0 3px var(--color-background)" : null',
|
|
715
|
+
'[style.background-color]': 'isChecked() ? "var(--color-primary)" : null',
|
|
716
|
+
'[style.border-color]': 'isChecked() ? "var(--color-primary)" : null',
|
|
717
|
+
'(change)': 'onChangeEvent($event)',
|
|
718
|
+
'(blur)': 'onBlur()',
|
|
719
|
+
},
|
|
720
|
+
providers: [
|
|
721
|
+
{
|
|
722
|
+
provide: NG_VALUE_ACCESSOR,
|
|
723
|
+
useExisting: forwardRef(() => LmRadioDirective),
|
|
724
|
+
multi: true,
|
|
725
|
+
},
|
|
726
|
+
],
|
|
727
|
+
}]
|
|
728
|
+
}], propDecorators: { lmValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmValue", required: false }] }], lmError: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmError", required: false }] }], lmDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDisabled", required: false }] }], lmRequired: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmRequired", required: false }] }] } });
|
|
729
|
+
|
|
66
730
|
class LmCardComponent {
|
|
67
731
|
/**
|
|
68
732
|
* Card visual style variant
|
|
@@ -2991,11 +3655,449 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
|
|
|
2991
3655
|
|
|
2992
3656
|
// Toast public API
|
|
2993
3657
|
|
|
3658
|
+
const SELECT_CONTEXT = new InjectionToken('SelectContext');
|
|
3659
|
+
|
|
3660
|
+
let selectIdCounter = 0;
|
|
3661
|
+
class LmSelectComponent {
|
|
3662
|
+
injector = inject(Injector);
|
|
3663
|
+
destroyRef = inject(DestroyRef);
|
|
3664
|
+
elementRef = inject((ElementRef));
|
|
3665
|
+
document = inject(DOCUMENT);
|
|
3666
|
+
platformId = inject(PLATFORM_ID);
|
|
3667
|
+
ngControl = null;
|
|
3668
|
+
// ── Inputs
|
|
3669
|
+
lmSize = input('md', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
|
|
3670
|
+
lmMultiple = input(false, ...(ngDevMode ? [{ debugName: "lmMultiple" }] : []));
|
|
3671
|
+
lmPlaceholder = input('Select…', ...(ngDevMode ? [{ debugName: "lmPlaceholder" }] : []));
|
|
3672
|
+
lmSearchPlaceholder = input('Search…', ...(ngDevMode ? [{ debugName: "lmSearchPlaceholder" }] : []));
|
|
3673
|
+
lmDisabled = input(false, ...(ngDevMode ? [{ debugName: "lmDisabled" }] : []));
|
|
3674
|
+
lmRequired = input(false, ...(ngDevMode ? [{ debugName: "lmRequired" }] : []));
|
|
3675
|
+
lmError = input(false, ...(ngDevMode ? [{ debugName: "lmError" }] : []));
|
|
3676
|
+
lmEmptyMessage = input('No options found', ...(ngDevMode ? [{ debugName: "lmEmptyMessage" }] : []));
|
|
3677
|
+
lmClearable = input(false, ...(ngDevMode ? [{ debugName: "lmClearable" }] : []));
|
|
3678
|
+
lmCompareWith = input(null, ...(ngDevMode ? [{ debugName: "lmCompareWith" }] : []));
|
|
3679
|
+
// ── Outputs
|
|
3680
|
+
lmValueChange = output();
|
|
3681
|
+
// ── IDs: read the host id attribute before host binding removes it
|
|
3682
|
+
selectId = signal(this.elementRef.nativeElement.getAttribute('id') ||
|
|
3683
|
+
`luma-select-${selectIdCounter++}`, ...(ngDevMode ? [{ debugName: "selectId" }] : []));
|
|
3684
|
+
listboxId = computed(() => `${this.selectId()}-listbox`, ...(ngDevMode ? [{ debugName: "listboxId" }] : []));
|
|
3685
|
+
searchInputId = computed(() => `${this.selectId()}-search`, ...(ngDevMode ? [{ debugName: "searchInputId" }] : []));
|
|
3686
|
+
/** ID of the currently focused option for aria-activedescendant */
|
|
3687
|
+
activeDescendantId = computed(() => {
|
|
3688
|
+
const idx = this.focusedOptionIndex();
|
|
3689
|
+
return idx >= 0 ? `${this.selectId()}-opt-${idx}` : null;
|
|
3690
|
+
}, ...(ngDevMode ? [{ debugName: "activeDescendantId" }] : []));
|
|
3691
|
+
/** Whether the select has a value (for clear button visibility) */
|
|
3692
|
+
hasValue = computed(() => {
|
|
3693
|
+
if (this.lmMultiple())
|
|
3694
|
+
return this._multiValue().length > 0;
|
|
3695
|
+
const v = this._singleValue();
|
|
3696
|
+
return v !== null && v !== undefined;
|
|
3697
|
+
}, ...(ngDevMode ? [{ debugName: "hasValue" }] : []));
|
|
3698
|
+
// ── Selection state
|
|
3699
|
+
_singleValue = signal(null, ...(ngDevMode ? [{ debugName: "_singleValue" }] : []));
|
|
3700
|
+
_multiValue = signal([], ...(ngDevMode ? [{ debugName: "_multiValue" }] : []));
|
|
3701
|
+
// ── Open state
|
|
3702
|
+
_internalOpen = signal(false, ...(ngDevMode ? [{ debugName: "_internalOpen" }] : []));
|
|
3703
|
+
isOpen = this._internalOpen.asReadonly();
|
|
3704
|
+
// ── Search + options registry
|
|
3705
|
+
searchQuery = signal('', ...(ngDevMode ? [{ debugName: "searchQuery" }] : []));
|
|
3706
|
+
_options = signal([], ...(ngDevMode ? [{ debugName: "_options" }] : []));
|
|
3707
|
+
// ── Form state
|
|
3708
|
+
_formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_formDisabled" }] : []));
|
|
3709
|
+
_formInvalid = signal(false, ...(ngDevMode ? [{ debugName: "_formInvalid" }] : []));
|
|
3710
|
+
_formTouched = signal(false, ...(ngDevMode ? [{ debugName: "_formTouched" }] : []));
|
|
3711
|
+
// ── Derived state
|
|
3712
|
+
isDisabled = computed(() => this.lmDisabled() || this._formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
|
|
3713
|
+
hasError = computed(() => this.lmError() || (this._formInvalid() && this._formTouched()), ...(ngDevMode ? [{ debugName: "hasError" }] : []));
|
|
3714
|
+
/** SelectContext interface */
|
|
3715
|
+
isMultiple = computed(() => this.lmMultiple(), ...(ngDevMode ? [{ debugName: "isMultiple" }] : []));
|
|
3716
|
+
/** Compare two values using the custom compareWith function or strict equality */
|
|
3717
|
+
compareValues(a, b) {
|
|
3718
|
+
const fn = this.lmCompareWith();
|
|
3719
|
+
return fn ? fn(a, b) : a === b;
|
|
3720
|
+
}
|
|
3721
|
+
/**
|
|
3722
|
+
* Combines filtering with selected-state baking in a single computed.
|
|
3723
|
+
* Avoids creating per-row signal instances; the whole array recomputes
|
|
3724
|
+
* when search query, options, or selection state changes.
|
|
3725
|
+
*/
|
|
3726
|
+
filteredOptionsWithState = computed(() => {
|
|
3727
|
+
const query = this.searchQuery().toLowerCase().trim();
|
|
3728
|
+
const opts = this._options();
|
|
3729
|
+
const filtered = query
|
|
3730
|
+
? opts.filter((o) => o.label().toLowerCase().includes(query))
|
|
3731
|
+
: opts;
|
|
3732
|
+
const multiple = this.lmMultiple();
|
|
3733
|
+
const single = this._singleValue();
|
|
3734
|
+
const multi = this._multiValue();
|
|
3735
|
+
return filtered.map((o) => ({
|
|
3736
|
+
entry: o,
|
|
3737
|
+
isSelected: multiple
|
|
3738
|
+
? multi.some((v) => this.compareValues(v, o.value))
|
|
3739
|
+
: this.compareValues(o.value, single),
|
|
3740
|
+
}));
|
|
3741
|
+
}, ...(ngDevMode ? [{ debugName: "filteredOptionsWithState" }] : []));
|
|
3742
|
+
/** Label shown in the trigger when the dropdown is closed (single-select only) */
|
|
3743
|
+
triggerLabel = computed(() => {
|
|
3744
|
+
// Multi-select displays chips instead of a text label
|
|
3745
|
+
if (this.lmMultiple())
|
|
3746
|
+
return null;
|
|
3747
|
+
const val = this._singleValue();
|
|
3748
|
+
if (val === null || val === undefined)
|
|
3749
|
+
return null;
|
|
3750
|
+
const opts = this._options();
|
|
3751
|
+
return (opts.find((o) => this.compareValues(o.value, val))?.label() ?? String(val));
|
|
3752
|
+
}, ...(ngDevMode ? [{ debugName: "triggerLabel" }] : []));
|
|
3753
|
+
/** Chip data for multi-select: maps selected values to { value, label } */
|
|
3754
|
+
selectedChips = computed(() => {
|
|
3755
|
+
if (!this.lmMultiple())
|
|
3756
|
+
return [];
|
|
3757
|
+
const vals = this._multiValue();
|
|
3758
|
+
const opts = this._options();
|
|
3759
|
+
return vals.map((v) => ({
|
|
3760
|
+
value: v,
|
|
3761
|
+
label: opts.find((o) => this.compareValues(o.value, v))?.label() ?? String(v),
|
|
3762
|
+
}));
|
|
3763
|
+
}, ...(ngDevMode ? [{ debugName: "selectedChips" }] : []));
|
|
3764
|
+
// ── Dropdown flip (one-time check on open — no event listeners)
|
|
3765
|
+
_flipAbove = signal(false, ...(ngDevMode ? [{ debugName: "_flipAbove" }] : []));
|
|
3766
|
+
// ── Keyboard focus within the listbox
|
|
3767
|
+
focusedOptionIndex = signal(-1, ...(ngDevMode ? [{ debugName: "focusedOptionIndex" }] : []));
|
|
3768
|
+
// ── ViewChild refs
|
|
3769
|
+
searchInputRef = viewChild('searchInputEl', ...(ngDevMode ? [{ debugName: "searchInputRef" }] : []));
|
|
3770
|
+
triggerRef = viewChild('triggerEl', ...(ngDevMode ? [{ debugName: "triggerRef" }] : []));
|
|
3771
|
+
listboxRef = viewChild('listboxEl', ...(ngDevMode ? [{ debugName: "listboxRef" }] : []));
|
|
3772
|
+
// ── CVA classes (computed for reactivity)
|
|
3773
|
+
triggerClasses = computed(() => selectTriggerVariants({
|
|
3774
|
+
size: this.lmSize(),
|
|
3775
|
+
multiple: this.lmMultiple(),
|
|
3776
|
+
error: this.hasError(),
|
|
3777
|
+
disabled: this.isDisabled(),
|
|
3778
|
+
}), ...(ngDevMode ? [{ debugName: "triggerClasses" }] : []));
|
|
3779
|
+
listboxClasses = computed(() => selectListboxVariants({ flip: this._flipAbove() }), ...(ngDevMode ? [{ debugName: "listboxClasses" }] : []));
|
|
3780
|
+
searchInputClasses = selectSearchInputVariants();
|
|
3781
|
+
/** Chip classes — reactive to size */
|
|
3782
|
+
chipClasses = computed(() => selectChipVariants({ size: this.lmSize() }), ...(ngDevMode ? [{ debugName: "chipClasses" }] : []));
|
|
3783
|
+
/** Chip dismiss button classes — reactive to size */
|
|
3784
|
+
chipDismissClasses = computed(() => selectChipDismissVariants({ size: this.lmSize() }), ...(ngDevMode ? [{ debugName: "chipDismissClasses" }] : []));
|
|
3785
|
+
optionClasses(isSelected, disabled, isFocused) {
|
|
3786
|
+
const base = selectOptionVariants({ selected: isSelected, disabled });
|
|
3787
|
+
return isFocused && !disabled ? `${base} bg-primary/10` : base;
|
|
3788
|
+
}
|
|
3789
|
+
// noop – replaced at runtime by registerOnChange / registerOnTouched
|
|
3790
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
3791
|
+
onChange = () => { };
|
|
3792
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
3793
|
+
onTouched = () => { };
|
|
3794
|
+
// ── Event listener refs for cleanup
|
|
3795
|
+
clickOutsideHandler = null;
|
|
3796
|
+
constructor() {
|
|
3797
|
+
// React to open state changes in a single effect
|
|
3798
|
+
effect(() => {
|
|
3799
|
+
if (!isPlatformBrowser(this.platformId))
|
|
3800
|
+
return;
|
|
3801
|
+
if (this._internalOpen()) {
|
|
3802
|
+
this.checkFlipPosition();
|
|
3803
|
+
this.registerClickOutside();
|
|
3804
|
+
// Defer focus so the input element has been rendered
|
|
3805
|
+
setTimeout(() => this.searchInputRef()?.nativeElement.focus(), 0);
|
|
3806
|
+
}
|
|
3807
|
+
});
|
|
3808
|
+
// Scroll focused option into view when keyboard-navigating
|
|
3809
|
+
effect(() => {
|
|
3810
|
+
const idx = this.focusedOptionIndex();
|
|
3811
|
+
if (idx < 0)
|
|
3812
|
+
return;
|
|
3813
|
+
const listbox = this.listboxRef()?.nativeElement;
|
|
3814
|
+
if (!listbox)
|
|
3815
|
+
return;
|
|
3816
|
+
const optionEl = listbox.querySelector(`[id$='-opt-${idx}']`);
|
|
3817
|
+
optionEl?.scrollIntoView({ block: 'nearest' });
|
|
3818
|
+
});
|
|
3819
|
+
}
|
|
3820
|
+
ngOnInit() {
|
|
3821
|
+
try {
|
|
3822
|
+
this.ngControl = this.injector.get(NgControl, null, {
|
|
3823
|
+
optional: true,
|
|
3824
|
+
self: true,
|
|
3825
|
+
});
|
|
3826
|
+
if (this.ngControl) {
|
|
3827
|
+
this.ngControl.valueAccessor = this;
|
|
3828
|
+
const control = this.ngControl.control;
|
|
3829
|
+
if (control) {
|
|
3830
|
+
this._formInvalid.set(control.invalid);
|
|
3831
|
+
this._formTouched.set(control.touched);
|
|
3832
|
+
control.events
|
|
3833
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
3834
|
+
.subscribe(() => {
|
|
3835
|
+
this._formInvalid.set(control.invalid);
|
|
3836
|
+
this._formTouched.set(control.touched);
|
|
3837
|
+
});
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
catch {
|
|
3842
|
+
this.ngControl = null;
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
ngOnDestroy() {
|
|
3846
|
+
this.unregisterClickOutside();
|
|
3847
|
+
}
|
|
3848
|
+
// ── SelectContext implementation
|
|
3849
|
+
registerOption(entry) {
|
|
3850
|
+
this._options.update((opts) => [...opts, entry]);
|
|
3851
|
+
}
|
|
3852
|
+
unregisterOption(value) {
|
|
3853
|
+
this._options.update((opts) => opts.filter((o) => o.value !== value));
|
|
3854
|
+
}
|
|
3855
|
+
// ── Open / close
|
|
3856
|
+
open() {
|
|
3857
|
+
if (this.isDisabled())
|
|
3858
|
+
return;
|
|
3859
|
+
this._internalOpen.set(true);
|
|
3860
|
+
}
|
|
3861
|
+
close() {
|
|
3862
|
+
this._internalOpen.set(false);
|
|
3863
|
+
// Reset search state synchronously so tests and consumers see immediate update
|
|
3864
|
+
this.searchQuery.set('');
|
|
3865
|
+
this.focusedOptionIndex.set(-1);
|
|
3866
|
+
this.onTouched();
|
|
3867
|
+
this.unregisterClickOutside();
|
|
3868
|
+
setTimeout(() => this.triggerRef()?.nativeElement.focus(), 0);
|
|
3869
|
+
}
|
|
3870
|
+
toggle() {
|
|
3871
|
+
if (this.isOpen()) {
|
|
3872
|
+
this.close();
|
|
3873
|
+
}
|
|
3874
|
+
else {
|
|
3875
|
+
this.open();
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
// ── Value selection
|
|
3879
|
+
selectValue(value) {
|
|
3880
|
+
if (this.lmMultiple()) {
|
|
3881
|
+
const current = this._multiValue();
|
|
3882
|
+
const idx = current.findIndex((v) => this.compareValues(v, value));
|
|
3883
|
+
const next = idx >= 0 ? current.filter((_, i) => i !== idx) : [...current, value];
|
|
3884
|
+
this._multiValue.set(next);
|
|
3885
|
+
this.onChange(next);
|
|
3886
|
+
this.lmValueChange.emit(next);
|
|
3887
|
+
// In multi-select: keep open, clear the search filter
|
|
3888
|
+
this.searchQuery.set('');
|
|
3889
|
+
}
|
|
3890
|
+
else {
|
|
3891
|
+
this._singleValue.set(value);
|
|
3892
|
+
this.onChange(value);
|
|
3893
|
+
this.lmValueChange.emit(value);
|
|
3894
|
+
this.close();
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
/** Remove a chip by its value (without opening/closing the dropdown) */
|
|
3898
|
+
removeChip(event, value) {
|
|
3899
|
+
event.stopPropagation();
|
|
3900
|
+
// Toggle the value off via selectValue, but preserve open/close state
|
|
3901
|
+
const current = this._multiValue();
|
|
3902
|
+
const next = current.filter((v) => !this.compareValues(v, value));
|
|
3903
|
+
this._multiValue.set(next);
|
|
3904
|
+
this.onChange(next);
|
|
3905
|
+
this.lmValueChange.emit(next);
|
|
3906
|
+
}
|
|
3907
|
+
/** Clear the current selection */
|
|
3908
|
+
clear(event) {
|
|
3909
|
+
event?.stopPropagation();
|
|
3910
|
+
if (this.lmMultiple()) {
|
|
3911
|
+
this._multiValue.set([]);
|
|
3912
|
+
this.onChange([]);
|
|
3913
|
+
this.lmValueChange.emit([]);
|
|
3914
|
+
}
|
|
3915
|
+
else {
|
|
3916
|
+
this._singleValue.set(null);
|
|
3917
|
+
this.onChange(null);
|
|
3918
|
+
this.lmValueChange.emit(null);
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
// ── Keyboard handlers for the closed trigger
|
|
3922
|
+
onTriggerKeydown(event) {
|
|
3923
|
+
if (['Enter', ' ', 'ArrowDown'].includes(event.key)) {
|
|
3924
|
+
event.preventDefault();
|
|
3925
|
+
this.open();
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
// ── Keyboard handlers for the search input (open state)
|
|
3929
|
+
onSearchInput(event) {
|
|
3930
|
+
this.searchQuery.set(event.target.value);
|
|
3931
|
+
this.focusedOptionIndex.set(-1);
|
|
3932
|
+
}
|
|
3933
|
+
onSearchKeydown(event) {
|
|
3934
|
+
const opts = this.filteredOptionsWithState();
|
|
3935
|
+
switch (event.key) {
|
|
3936
|
+
case 'ArrowDown':
|
|
3937
|
+
event.preventDefault();
|
|
3938
|
+
this.focusedOptionIndex.update((i) => Math.min(i + 1, opts.length - 1));
|
|
3939
|
+
break;
|
|
3940
|
+
case 'ArrowUp':
|
|
3941
|
+
event.preventDefault();
|
|
3942
|
+
this.focusedOptionIndex.update((i) => Math.max(i - 1, 0));
|
|
3943
|
+
break;
|
|
3944
|
+
case 'Home':
|
|
3945
|
+
event.preventDefault();
|
|
3946
|
+
if (opts.length > 0)
|
|
3947
|
+
this.focusedOptionIndex.set(0);
|
|
3948
|
+
break;
|
|
3949
|
+
case 'End':
|
|
3950
|
+
event.preventDefault();
|
|
3951
|
+
if (opts.length > 0)
|
|
3952
|
+
this.focusedOptionIndex.set(opts.length - 1);
|
|
3953
|
+
break;
|
|
3954
|
+
case 'Enter': {
|
|
3955
|
+
event.preventDefault();
|
|
3956
|
+
const idx = this.focusedOptionIndex();
|
|
3957
|
+
if (idx >= 0 && idx < opts.length) {
|
|
3958
|
+
const { entry } = opts[idx];
|
|
3959
|
+
if (!entry.disabled())
|
|
3960
|
+
this.selectValue(entry.value);
|
|
3961
|
+
}
|
|
3962
|
+
break;
|
|
3963
|
+
}
|
|
3964
|
+
case 'Escape':
|
|
3965
|
+
event.preventDefault();
|
|
3966
|
+
this.close();
|
|
3967
|
+
break;
|
|
3968
|
+
case 'Tab':
|
|
3969
|
+
this.close();
|
|
3970
|
+
break;
|
|
3971
|
+
case 'Backspace': {
|
|
3972
|
+
// In multi-select: remove last chip when search is empty
|
|
3973
|
+
if (this.lmMultiple() && this.searchQuery() === '') {
|
|
3974
|
+
const vals = this._multiValue();
|
|
3975
|
+
if (vals.length > 0) {
|
|
3976
|
+
this.selectValue(vals[vals.length - 1]);
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
break;
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
}
|
|
3983
|
+
// ── One-time flip check (no event listeners needed)
|
|
3984
|
+
checkFlipPosition() {
|
|
3985
|
+
const trigger = this.triggerRef()?.nativeElement;
|
|
3986
|
+
if (!trigger)
|
|
3987
|
+
return;
|
|
3988
|
+
const rect = trigger.getBoundingClientRect();
|
|
3989
|
+
const dropdownMaxHeight = 240; // matches max-h-60 (15rem = 240px)
|
|
3990
|
+
const gap = 4;
|
|
3991
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
3992
|
+
this._flipAbove.set(spaceBelow < dropdownMaxHeight + gap &&
|
|
3993
|
+
rect.top > dropdownMaxHeight + gap);
|
|
3994
|
+
}
|
|
3995
|
+
registerClickOutside() {
|
|
3996
|
+
if (this.clickOutsideHandler)
|
|
3997
|
+
return;
|
|
3998
|
+
this.clickOutsideHandler = (e) => {
|
|
3999
|
+
if (!this.elementRef.nativeElement.contains(e.target)) {
|
|
4000
|
+
this.close();
|
|
4001
|
+
}
|
|
4002
|
+
};
|
|
4003
|
+
// Small timeout to avoid the current click event triggering close immediately
|
|
4004
|
+
setTimeout(() => {
|
|
4005
|
+
if (this.clickOutsideHandler) {
|
|
4006
|
+
this.document.addEventListener('click', this.clickOutsideHandler);
|
|
4007
|
+
}
|
|
4008
|
+
}, 0);
|
|
4009
|
+
}
|
|
4010
|
+
unregisterClickOutside() {
|
|
4011
|
+
if (this.clickOutsideHandler) {
|
|
4012
|
+
this.document.removeEventListener('click', this.clickOutsideHandler);
|
|
4013
|
+
this.clickOutsideHandler = null;
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
// ── ControlValueAccessor
|
|
4017
|
+
writeValue(value) {
|
|
4018
|
+
if (this.lmMultiple()) {
|
|
4019
|
+
this._multiValue.set(Array.isArray(value) ? value : value != null ? [value] : []);
|
|
4020
|
+
}
|
|
4021
|
+
else {
|
|
4022
|
+
this._singleValue.set(value ?? null);
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
registerOnChange(fn) {
|
|
4026
|
+
this.onChange = fn;
|
|
4027
|
+
}
|
|
4028
|
+
registerOnTouched(fn) {
|
|
4029
|
+
this.onTouched = fn;
|
|
4030
|
+
}
|
|
4031
|
+
setDisabledState(isDisabled) {
|
|
4032
|
+
this._formDisabled.set(isDisabled);
|
|
4033
|
+
}
|
|
4034
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4035
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.9", type: LmSelectComponent, isStandalone: true, selector: "luma-select", inputs: { lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null }, lmMultiple: { classPropertyName: "lmMultiple", publicName: "lmMultiple", isSignal: true, isRequired: false, transformFunction: null }, lmPlaceholder: { classPropertyName: "lmPlaceholder", publicName: "lmPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, lmSearchPlaceholder: { classPropertyName: "lmSearchPlaceholder", publicName: "lmSearchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, lmDisabled: { classPropertyName: "lmDisabled", publicName: "lmDisabled", isSignal: true, isRequired: false, transformFunction: null }, lmRequired: { classPropertyName: "lmRequired", publicName: "lmRequired", isSignal: true, isRequired: false, transformFunction: null }, lmError: { classPropertyName: "lmError", publicName: "lmError", isSignal: true, isRequired: false, transformFunction: null }, lmEmptyMessage: { classPropertyName: "lmEmptyMessage", publicName: "lmEmptyMessage", isSignal: true, isRequired: false, transformFunction: null }, lmClearable: { classPropertyName: "lmClearable", publicName: "lmClearable", isSignal: true, isRequired: false, transformFunction: null }, lmCompareWith: { classPropertyName: "lmCompareWith", publicName: "lmCompareWith", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { lmValueChange: "lmValueChange" }, host: { properties: { "attr.id": "null" }, classAttribute: "block relative w-full" }, providers: [
|
|
4036
|
+
{ provide: SELECT_CONTEXT, useExisting: LmSelectComponent },
|
|
4037
|
+
{
|
|
4038
|
+
provide: NG_VALUE_ACCESSOR,
|
|
4039
|
+
useExisting: forwardRef(() => LmSelectComponent),
|
|
4040
|
+
multi: true,
|
|
4041
|
+
},
|
|
4042
|
+
], viewQueries: [{ propertyName: "searchInputRef", first: true, predicate: ["searchInputEl"], descendants: true, isSignal: true }, { propertyName: "triggerRef", first: true, predicate: ["triggerEl"], descendants: true, isSignal: true }, { propertyName: "listboxRef", first: true, predicate: ["listboxEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Hidden slot: options run lifecycle hooks and register themselves via SELECT_CONTEXT,\n but are never shown from this slot. The visible listbox is rendered below. -->\n<div class=\"hidden\" aria-hidden=\"true\">\n <ng-content />\n</div>\n\n<!-- Trigger (id placed here for <label for=\"\u2026\"> association; host id removed via host binding) -->\n<div\n #triggerEl\n [attr.id]=\"selectId()\"\n [class]=\"triggerClasses()\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-invalid]=\"hasError()\"\n [attr.aria-required]=\"lmRequired()\"\n [attr.aria-disabled]=\"isDisabled()\"\n role=\"combobox\"\n (click)=\"!isOpen() ? toggle() : null\"\n (keydown)=\"!isOpen() ? onTriggerKeydown($event) : null\"\n>\n @if (isOpen()) {\n <!-- Open state -->\n @if (lmMultiple() && selectedChips().length > 0) {\n <!-- Multi-select open: chips + inline search -->\n @for (chip of selectedChips(); track chip.value) {\n <span [class]=\"chipClasses()\">\n <span class=\"truncate\">{{ chip.label }}</span>\n <button\n type=\"button\"\n [class]=\"chipDismissClasses()\"\n [attr.aria-label]=\"'Remove ' + chip.label\"\n (click)=\"removeChip($event, chip.value)\"\n >\n <svg\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n class=\"h-full w-full\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M5 5l6 6M11 5l-6 6\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n </span>\n }\n <input\n #searchInputEl\n type=\"text\"\n [id]=\"searchInputId()\"\n class=\"flex-1 bg-transparent outline-none border-none min-w-15 text-foreground placeholder:text-muted-foreground\"\n [value]=\"searchQuery()\"\n [placeholder]=\"lmSearchPlaceholder()\"\n [attr.aria-label]=\"lmSearchPlaceholder()\"\n aria-autocomplete=\"list\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeDescendantId()\"\n autocomplete=\"off\"\n (input)=\"onSearchInput($event)\"\n (keydown)=\"onSearchKeydown($event)\"\n (click)=\"$event.stopPropagation()\"\n />\n } @else {\n <!-- Single-select or empty multi: just the search input -->\n <input\n #searchInputEl\n type=\"text\"\n [id]=\"searchInputId()\"\n [class]=\"searchInputClasses\"\n [value]=\"searchQuery()\"\n [placeholder]=\"lmSearchPlaceholder()\"\n [attr.aria-label]=\"lmSearchPlaceholder()\"\n aria-autocomplete=\"list\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeDescendantId()\"\n autocomplete=\"off\"\n (input)=\"onSearchInput($event)\"\n (keydown)=\"onSearchKeydown($event)\"\n (click)=\"$event.stopPropagation()\"\n />\n }\n } @else {\n <!-- Closed state -->\n @if (lmMultiple() && selectedChips().length > 0) {\n <!-- Multi-select closed: show chips -->\n <div class=\"flex flex-wrap gap-1 flex-1 min-w-0 max-h-32 overflow-y-auto\">\n @for (chip of selectedChips(); track chip.value) {\n <span [class]=\"chipClasses()\">\n <span class=\"truncate\">{{ chip.label }}</span>\n <button\n type=\"button\"\n [class]=\"chipDismissClasses()\"\n [attr.aria-label]=\"'Remove ' + chip.label\"\n (click)=\"removeChip($event, chip.value)\"\n >\n <svg\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n class=\"h-full w-full\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M5 5l6 6M11 5l-6 6\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n </span>\n }\n </div>\n } @else {\n <!-- Single-select or empty multi: label/placeholder -->\n <span\n class=\"flex-1 truncate min-w-0\"\n [class.text-muted-foreground]=\"!triggerLabel()\"\n >\n {{ triggerLabel() ?? lmPlaceholder() }}\n </span>\n }\n }\n\n <!-- Clear button \u2014 shown when clearable and has value -->\n @if (lmClearable() && hasValue() && !isDisabled() && !isOpen()) {\n <button\n type=\"button\"\n class=\"shrink-0 h-4 w-4 flex items-center justify-center text-muted-foreground hover:text-foreground transition-colors\"\n aria-label=\"Clear selection\"\n (click)=\"clear($event)\"\n >\n <svg\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n class=\"h-3.5 w-3.5\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M4 4l8 8M12 4l-8 8\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n }\n\n <!-- Chevron icon \u2014 rotates 180\u00B0 when open -->\n <svg\n class=\"ml-auto shrink-0 h-4 w-4 text-muted-foreground transition-transform duration-200\"\n [class.rotate-180]=\"isOpen()\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M4 6l4 4 4-4\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n</div>\n\n<!-- Listbox panel (absolute positioning \u2014 CSS-only, no JS tracking) -->\n@if (isOpen()) {\n <div\n #listboxEl\n [id]=\"listboxId()\"\n role=\"listbox\"\n [attr.aria-label]=\"lmPlaceholder()\"\n [attr.aria-multiselectable]=\"lmMultiple()\"\n [class]=\"listboxClasses()\"\n >\n @if (filteredOptionsWithState().length === 0) {\n <div class=\"px-3 py-4 text-sm text-center text-muted-foreground\">\n {{ lmEmptyMessage() }}\n </div>\n }\n\n @for (\n item of filteredOptionsWithState();\n track item.entry.value;\n let i = $index\n ) {\n <div\n [id]=\"selectId() + '-opt-' + i\"\n role=\"option\"\n [attr.aria-selected]=\"item.isSelected\"\n [attr.aria-disabled]=\"item.entry.disabled()\"\n [attr.tabindex]=\"item.entry.disabled() ? -1 : 0\"\n [class]=\"\n optionClasses(\n item.isSelected,\n item.entry.disabled(),\n focusedOptionIndex() === i\n )\n \"\n (click)=\"selectValue(item.entry.value)\"\n (keydown.enter)=\"selectValue(item.entry.value)\"\n (keydown.space)=\"selectValue(item.entry.value)\"\n (mouseenter)=\"focusedOptionIndex.set(i)\"\n >\n @if (lmMultiple()) {\n <!-- Mini checkbox indicator for multi-select -->\n <span\n class=\"h-4 w-4 shrink-0 flex items-center justify-center rounded-(--radius-2) border transition-colors duration-200\"\n [class.bg-primary]=\"item.isSelected\"\n [class.border-primary]=\"item.isSelected\"\n [class.border-gray-5]=\"!item.isSelected\"\n aria-hidden=\"true\"\n >\n @if (item.isSelected) {\n <svg width=\"10\" height=\"10\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path\n d=\"M4 8l3 3 5-5\"\n stroke=\"white\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n }\n </span>\n } @else {\n <!-- Checkmark for single-select, spacer otherwise -->\n @if (item.isSelected) {\n <svg\n class=\"h-4 w-4 shrink-0 text-primary\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n >\n <path\n d=\"M3 8l4 4 6-7\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n }\n }\n\n <span class=\"truncate\">{{ item.entry.label() }}</span>\n </div>\n }\n </div>\n}\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4043
|
+
}
|
|
4044
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmSelectComponent, decorators: [{
|
|
4045
|
+
type: Component,
|
|
4046
|
+
args: [{ selector: 'luma-select', changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
4047
|
+
class: 'block relative w-full',
|
|
4048
|
+
'[attr.id]': 'null',
|
|
4049
|
+
}, providers: [
|
|
4050
|
+
{ provide: SELECT_CONTEXT, useExisting: LmSelectComponent },
|
|
4051
|
+
{
|
|
4052
|
+
provide: NG_VALUE_ACCESSOR,
|
|
4053
|
+
useExisting: forwardRef(() => LmSelectComponent),
|
|
4054
|
+
multi: true,
|
|
4055
|
+
},
|
|
4056
|
+
], template: "<!-- Hidden slot: options run lifecycle hooks and register themselves via SELECT_CONTEXT,\n but are never shown from this slot. The visible listbox is rendered below. -->\n<div class=\"hidden\" aria-hidden=\"true\">\n <ng-content />\n</div>\n\n<!-- Trigger (id placed here for <label for=\"\u2026\"> association; host id removed via host binding) -->\n<div\n #triggerEl\n [attr.id]=\"selectId()\"\n [class]=\"triggerClasses()\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-invalid]=\"hasError()\"\n [attr.aria-required]=\"lmRequired()\"\n [attr.aria-disabled]=\"isDisabled()\"\n role=\"combobox\"\n (click)=\"!isOpen() ? toggle() : null\"\n (keydown)=\"!isOpen() ? onTriggerKeydown($event) : null\"\n>\n @if (isOpen()) {\n <!-- Open state -->\n @if (lmMultiple() && selectedChips().length > 0) {\n <!-- Multi-select open: chips + inline search -->\n @for (chip of selectedChips(); track chip.value) {\n <span [class]=\"chipClasses()\">\n <span class=\"truncate\">{{ chip.label }}</span>\n <button\n type=\"button\"\n [class]=\"chipDismissClasses()\"\n [attr.aria-label]=\"'Remove ' + chip.label\"\n (click)=\"removeChip($event, chip.value)\"\n >\n <svg\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n class=\"h-full w-full\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M5 5l6 6M11 5l-6 6\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n </span>\n }\n <input\n #searchInputEl\n type=\"text\"\n [id]=\"searchInputId()\"\n class=\"flex-1 bg-transparent outline-none border-none min-w-15 text-foreground placeholder:text-muted-foreground\"\n [value]=\"searchQuery()\"\n [placeholder]=\"lmSearchPlaceholder()\"\n [attr.aria-label]=\"lmSearchPlaceholder()\"\n aria-autocomplete=\"list\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeDescendantId()\"\n autocomplete=\"off\"\n (input)=\"onSearchInput($event)\"\n (keydown)=\"onSearchKeydown($event)\"\n (click)=\"$event.stopPropagation()\"\n />\n } @else {\n <!-- Single-select or empty multi: just the search input -->\n <input\n #searchInputEl\n type=\"text\"\n [id]=\"searchInputId()\"\n [class]=\"searchInputClasses\"\n [value]=\"searchQuery()\"\n [placeholder]=\"lmSearchPlaceholder()\"\n [attr.aria-label]=\"lmSearchPlaceholder()\"\n aria-autocomplete=\"list\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeDescendantId()\"\n autocomplete=\"off\"\n (input)=\"onSearchInput($event)\"\n (keydown)=\"onSearchKeydown($event)\"\n (click)=\"$event.stopPropagation()\"\n />\n }\n } @else {\n <!-- Closed state -->\n @if (lmMultiple() && selectedChips().length > 0) {\n <!-- Multi-select closed: show chips -->\n <div class=\"flex flex-wrap gap-1 flex-1 min-w-0 max-h-32 overflow-y-auto\">\n @for (chip of selectedChips(); track chip.value) {\n <span [class]=\"chipClasses()\">\n <span class=\"truncate\">{{ chip.label }}</span>\n <button\n type=\"button\"\n [class]=\"chipDismissClasses()\"\n [attr.aria-label]=\"'Remove ' + chip.label\"\n (click)=\"removeChip($event, chip.value)\"\n >\n <svg\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n class=\"h-full w-full\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M5 5l6 6M11 5l-6 6\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n </span>\n }\n </div>\n } @else {\n <!-- Single-select or empty multi: label/placeholder -->\n <span\n class=\"flex-1 truncate min-w-0\"\n [class.text-muted-foreground]=\"!triggerLabel()\"\n >\n {{ triggerLabel() ?? lmPlaceholder() }}\n </span>\n }\n }\n\n <!-- Clear button \u2014 shown when clearable and has value -->\n @if (lmClearable() && hasValue() && !isDisabled() && !isOpen()) {\n <button\n type=\"button\"\n class=\"shrink-0 h-4 w-4 flex items-center justify-center text-muted-foreground hover:text-foreground transition-colors\"\n aria-label=\"Clear selection\"\n (click)=\"clear($event)\"\n >\n <svg\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n class=\"h-3.5 w-3.5\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M4 4l8 8M12 4l-8 8\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n }\n\n <!-- Chevron icon \u2014 rotates 180\u00B0 when open -->\n <svg\n class=\"ml-auto shrink-0 h-4 w-4 text-muted-foreground transition-transform duration-200\"\n [class.rotate-180]=\"isOpen()\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M4 6l4 4 4-4\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n</div>\n\n<!-- Listbox panel (absolute positioning \u2014 CSS-only, no JS tracking) -->\n@if (isOpen()) {\n <div\n #listboxEl\n [id]=\"listboxId()\"\n role=\"listbox\"\n [attr.aria-label]=\"lmPlaceholder()\"\n [attr.aria-multiselectable]=\"lmMultiple()\"\n [class]=\"listboxClasses()\"\n >\n @if (filteredOptionsWithState().length === 0) {\n <div class=\"px-3 py-4 text-sm text-center text-muted-foreground\">\n {{ lmEmptyMessage() }}\n </div>\n }\n\n @for (\n item of filteredOptionsWithState();\n track item.entry.value;\n let i = $index\n ) {\n <div\n [id]=\"selectId() + '-opt-' + i\"\n role=\"option\"\n [attr.aria-selected]=\"item.isSelected\"\n [attr.aria-disabled]=\"item.entry.disabled()\"\n [attr.tabindex]=\"item.entry.disabled() ? -1 : 0\"\n [class]=\"\n optionClasses(\n item.isSelected,\n item.entry.disabled(),\n focusedOptionIndex() === i\n )\n \"\n (click)=\"selectValue(item.entry.value)\"\n (keydown.enter)=\"selectValue(item.entry.value)\"\n (keydown.space)=\"selectValue(item.entry.value)\"\n (mouseenter)=\"focusedOptionIndex.set(i)\"\n >\n @if (lmMultiple()) {\n <!-- Mini checkbox indicator for multi-select -->\n <span\n class=\"h-4 w-4 shrink-0 flex items-center justify-center rounded-(--radius-2) border transition-colors duration-200\"\n [class.bg-primary]=\"item.isSelected\"\n [class.border-primary]=\"item.isSelected\"\n [class.border-gray-5]=\"!item.isSelected\"\n aria-hidden=\"true\"\n >\n @if (item.isSelected) {\n <svg width=\"10\" height=\"10\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path\n d=\"M4 8l3 3 5-5\"\n stroke=\"white\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n }\n </span>\n } @else {\n <!-- Checkmark for single-select, spacer otherwise -->\n @if (item.isSelected) {\n <svg\n class=\"h-4 w-4 shrink-0 text-primary\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n >\n <path\n d=\"M3 8l4 4 6-7\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n }\n }\n\n <span class=\"truncate\">{{ item.entry.label() }}</span>\n </div>\n }\n </div>\n}\n" }]
|
|
4057
|
+
}], ctorParameters: () => [], propDecorators: { lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }], lmMultiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmMultiple", required: false }] }], lmPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmPlaceholder", required: false }] }], lmSearchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSearchPlaceholder", required: false }] }], lmDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDisabled", required: false }] }], lmRequired: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmRequired", required: false }] }], lmError: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmError", required: false }] }], lmEmptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmEmptyMessage", required: false }] }], lmClearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmClearable", required: false }] }], lmCompareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmCompareWith", required: false }] }], lmValueChange: [{ type: i0.Output, args: ["lmValueChange"] }], searchInputRef: [{ type: i0.ViewChild, args: ['searchInputEl', { isSignal: true }] }], triggerRef: [{ type: i0.ViewChild, args: ['triggerEl', { isSignal: true }] }], listboxRef: [{ type: i0.ViewChild, args: ['listboxEl', { isSignal: true }] }] } });
|
|
4058
|
+
|
|
4059
|
+
/**
|
|
4060
|
+
* Option component for luma-select.
|
|
4061
|
+
* Projects content into a hidden slot (the parent hides it via aria-hidden);
|
|
4062
|
+
* registers its value/label/disabled as signals into the parent's registry.
|
|
4063
|
+
*
|
|
4064
|
+
* @example
|
|
4065
|
+
* <luma-select-option [lmValue]="'apple'">Apple</luma-select-option>
|
|
4066
|
+
*/
|
|
4067
|
+
class LmSelectOptionComponent {
|
|
4068
|
+
context = inject(SELECT_CONTEXT);
|
|
4069
|
+
elementRef = inject((ElementRef));
|
|
4070
|
+
lmValue = input.required(...(ngDevMode ? [{ debugName: "lmValue" }] : []));
|
|
4071
|
+
lmDisabled = input(false, ...(ngDevMode ? [{ debugName: "lmDisabled" }] : []));
|
|
4072
|
+
entry = null;
|
|
4073
|
+
ngOnInit() {
|
|
4074
|
+
this.entry = {
|
|
4075
|
+
value: this.lmValue(),
|
|
4076
|
+
label: () => this.elementRef.nativeElement.textContent?.trim() ?? '',
|
|
4077
|
+
disabled: this.lmDisabled,
|
|
4078
|
+
};
|
|
4079
|
+
this.context.registerOption(this.entry);
|
|
4080
|
+
}
|
|
4081
|
+
ngOnDestroy() {
|
|
4082
|
+
this.context.unregisterOption(this.lmValue());
|
|
4083
|
+
}
|
|
4084
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmSelectOptionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4085
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.9", type: LmSelectOptionComponent, isStandalone: true, selector: "luma-select-option", inputs: { lmValue: { classPropertyName: "lmValue", publicName: "lmValue", isSignal: true, isRequired: true, transformFunction: null }, lmDisabled: { classPropertyName: "lmDisabled", publicName: "lmDisabled", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4086
|
+
}
|
|
4087
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmSelectOptionComponent, decorators: [{
|
|
4088
|
+
type: Component,
|
|
4089
|
+
args: [{
|
|
4090
|
+
selector: 'luma-select-option',
|
|
4091
|
+
template: `<ng-content />`,
|
|
4092
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
4093
|
+
}]
|
|
4094
|
+
}], propDecorators: { lmValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmValue", required: true }] }], lmDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDisabled", required: false }] }] } });
|
|
4095
|
+
|
|
2994
4096
|
// Button exports
|
|
2995
4097
|
|
|
2996
4098
|
/**
|
|
2997
4099
|
* Generated bundle index. Do not edit.
|
|
2998
4100
|
*/
|
|
2999
4101
|
|
|
3000
|
-
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 };
|
|
4102
|
+
export { ACCORDION_ITEM, DEFAULT_TOAST_CONFIG, LmAccordionContentDirective, LmAccordionGroupComponent, LmAccordionIconDirective, LmAccordionItemComponent, LmAccordionTitleDirective, LmAccordionTriggerDirective, LmBadgeDirective, LmButtonDirective, LmCardComponent, LmCardContentDirective, LmCardDescriptionDirective, LmCardHeaderDirective, LmCardTitleDirective, LmCheckboxDirective, LmErrorTextDirective, LmHelperTextDirective, LmInputDirective, LmLabelDirective, LmModalCloseComponent, LmModalComponent, LmModalContainerComponent, LmModalContentDirective, LmModalFooterDirective, LmModalHeaderDirective, LmModalOverlayComponent, LmModalTitleDirective, LmRadioDirective, LmSelectComponent, LmSelectOptionComponent, LmTabsComponent, LmTabsIndicatorComponent, LmTabsListDirective, LmTabsPanelDirective, LmTabsTriggerDirective, LmTextareaDirective, LmToastCloseComponent, LmToastContainerComponent, LmToastItemComponent, LmToastService, LmTooltipDirective, MODAL_CONTEXT, SELECT_CONTEXT, TABS_GROUP, TABS_LIST, TOAST_CONFIG, provideToastConfig };
|
|
3001
4103
|
//# sourceMappingURL=lumaui-angular.mjs.map
|