ngx-opalbytes-components 1.19.0 → 1.20.0

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.
Files changed (78) hide show
  1. package/README.md +103 -0
  2. package/cli/cao-comp.cjs +186 -0
  3. package/fesm2022/ngx-opalbytes-components.mjs +23 -7
  4. package/fesm2022/ngx-opalbytes-components.mjs.map +1 -1
  5. package/package.json +4 -1
  6. package/src/lib/shared/components/autocomplete/autocomplete.component.html +126 -0
  7. package/src/lib/shared/components/autocomplete/autocomplete.component.scss +150 -0
  8. package/src/lib/shared/components/autocomplete/autocomplete.component.ts +552 -0
  9. package/src/lib/shared/components/base-button/base-button.component.html +33 -0
  10. package/src/lib/shared/components/base-button/base-button.component.scss +14 -0
  11. package/src/lib/shared/components/base-button/base-button.component.spec.ts +64 -0
  12. package/src/lib/shared/components/base-button/base-button.component.ts +48 -0
  13. package/src/lib/shared/components/base-icon/base-icon.css +0 -0
  14. package/src/lib/shared/components/base-icon/base-icon.html +11 -0
  15. package/src/lib/shared/components/base-icon/base-icon.spec.ts +23 -0
  16. package/src/lib/shared/components/base-icon/base-icon.ts +18 -0
  17. package/src/lib/shared/components/base-input/base-input.css +193 -0
  18. package/src/lib/shared/components/base-input/base-input.html +43 -0
  19. package/src/lib/shared/components/base-input/base-input.spec.ts +146 -0
  20. package/src/lib/shared/components/base-input/base-input.ts +133 -0
  21. package/src/lib/shared/components/base-time-range/base-time-range.css +27 -0
  22. package/src/lib/shared/components/base-time-range/base-time-range.html +23 -0
  23. package/src/lib/shared/components/base-time-range/base-time-range.spec.ts +27 -0
  24. package/src/lib/shared/components/base-time-range/base-time-range.ts +61 -0
  25. package/src/lib/shared/components/base-toggle/base-toggle.component.html +28 -0
  26. package/src/lib/shared/components/base-toggle/base-toggle.component.scss +45 -0
  27. package/src/lib/shared/components/base-toggle/base-toggle.component.spec.ts +22 -0
  28. package/src/lib/shared/components/base-toggle/base-toggle.component.ts +138 -0
  29. package/src/lib/shared/components/card/card.component.css +264 -0
  30. package/src/lib/shared/components/card/card.component.html +103 -0
  31. package/src/lib/shared/components/card/card.component.ts +63 -0
  32. package/src/lib/shared/components/date-picker/date-picker.html +82 -0
  33. package/src/lib/shared/components/date-picker/date-picker.scss +8 -0
  34. package/src/lib/shared/components/date-picker/date-picker.spec.ts +23 -0
  35. package/src/lib/shared/components/date-picker/date-picker.ts +165 -0
  36. package/src/lib/shared/components/drop-down/drop-down.css +61 -0
  37. package/src/lib/shared/components/drop-down/drop-down.html +96 -0
  38. package/src/lib/shared/components/drop-down/drop-down.spec.ts +335 -0
  39. package/src/lib/shared/components/drop-down/drop-down.ts +335 -0
  40. package/src/lib/shared/components/footer/footer.css +29 -0
  41. package/src/lib/shared/components/footer/footer.html +10 -0
  42. package/src/lib/shared/components/footer/footer.spec.ts +22 -0
  43. package/src/lib/shared/components/footer/footer.ts +15 -0
  44. package/src/lib/shared/components/index.ts +19 -0
  45. package/src/lib/shared/components/kpi/kpi.css +0 -0
  46. package/src/lib/shared/components/kpi/kpi.html +11 -0
  47. package/src/lib/shared/components/kpi/kpi.spec.ts +38 -0
  48. package/src/lib/shared/components/kpi/kpi.ts +25 -0
  49. package/src/lib/shared/components/links-button/links-button.css +70 -0
  50. package/src/lib/shared/components/links-button/links-button.html +23 -0
  51. package/src/lib/shared/components/links-button/links-button.spec.ts +139 -0
  52. package/src/lib/shared/components/links-button/links-button.ts +45 -0
  53. package/src/lib/shared/components/paginator/paginator.css +0 -0
  54. package/src/lib/shared/components/paginator/paginator.html +10 -0
  55. package/src/lib/shared/components/paginator/paginator.spec.ts +23 -0
  56. package/src/lib/shared/components/paginator/paginator.ts +25 -0
  57. package/src/lib/shared/components/stepper/step/step.css +8 -0
  58. package/src/lib/shared/components/stepper/step/step.html +1 -0
  59. package/src/lib/shared/components/stepper/step/step.spec.ts +131 -0
  60. package/src/lib/shared/components/stepper/step/step.ts +14 -0
  61. package/src/lib/shared/components/stepper/stepper.css +17 -0
  62. package/src/lib/shared/components/stepper/stepper.html +11 -0
  63. package/src/lib/shared/components/stepper/stepper.spec.ts +238 -0
  64. package/src/lib/shared/components/stepper/stepper.ts +57 -0
  65. package/src/lib/shared/components/tab-group/tab-group.component.html +84 -0
  66. package/src/lib/shared/components/tab-group/tab-group.component.scss +151 -0
  67. package/src/lib/shared/components/tab-group/tab-group.spec.ts +22 -0
  68. package/src/lib/shared/components/tab-group/tab-group.ts +218 -0
  69. package/src/lib/shared/components/tab-panel/tab-content.directive.ts +7 -0
  70. package/src/lib/shared/components/tab-panel/tab-label.directive.ts +7 -0
  71. package/src/lib/shared/components/tab-panel/tab-panel.css +0 -0
  72. package/src/lib/shared/components/tab-panel/tab-panel.spec.ts +22 -0
  73. package/src/lib/shared/components/tab-panel/tab-panel.ts +31 -0
  74. package/src/lib/shared/components/time-picker/time-picker.css +84 -0
  75. package/src/lib/shared/components/time-picker/time-picker.html +21 -0
  76. package/src/lib/shared/components/time-picker/time-picker.spec.ts +23 -0
  77. package/src/lib/shared/components/time-picker/time-picker.ts +90 -0
  78. package/types/ngx-opalbytes-components.d.ts +23 -4
@@ -0,0 +1,552 @@
1
+ import { Overlay, OverlayRef, OverlayModule } from '@angular/cdk/overlay';
2
+ import { TemplatePortal } from '@angular/cdk/portal';
3
+ import { CdkScrollable, ScrollDispatcher } from '@angular/cdk/scrolling';
4
+ import {
5
+ CommonModule
6
+ } from "@angular/common";
7
+ import {
8
+ ChangeDetectionStrategy,
9
+ ChangeDetectorRef,
10
+ Component,
11
+ ElementRef,
12
+ EventEmitter,
13
+ HostListener,
14
+ Input,
15
+ OnInit,
16
+ Output,
17
+ ViewChild,
18
+ forwardRef,
19
+ inject,
20
+ OnChanges,
21
+ OnDestroy,
22
+ SimpleChanges,
23
+ Renderer2,
24
+ ViewContainerRef,
25
+ AfterViewInit,
26
+ TemplateRef
27
+ } from "@angular/core";
28
+ import {
29
+ AbstractControl,
30
+ ControlValueAccessor,
31
+ FormControl,
32
+ NG_VALUE_ACCESSOR,
33
+ ReactiveFormsModule,
34
+ } from "@angular/forms";
35
+
36
+ import { LucideDynamicIcon } from "@lucide/angular";
37
+ import {
38
+ Observable,
39
+ Subscription,
40
+ startWith,
41
+ map,
42
+ BehaviorSubject,
43
+ combineLatest
44
+ } from "rxjs";
45
+
46
+
47
+ export interface ICaoAutoCompleteOption {
48
+ id?: string | number;
49
+ nome: string;
50
+ icon?: string;
51
+ iconPosition?: string;
52
+ }
53
+
54
+ @Component({
55
+ selector: "cao-autocomplete",
56
+ templateUrl: "./autocomplete.component.html",
57
+ styleUrls: ["./autocomplete.component.scss"],
58
+ standalone: true,
59
+ imports: [
60
+ CommonModule,
61
+ ReactiveFormsModule,
62
+ LucideDynamicIcon,
63
+ OverlayModule,
64
+ CdkScrollable
65
+ ],
66
+ changeDetection: ChangeDetectionStrategy.OnPush,
67
+ providers: [
68
+ {
69
+ provide: NG_VALUE_ACCESSOR,
70
+ useExisting: forwardRef(() => CaoAutocompleteComponent),
71
+ multi: true,
72
+ },
73
+ ],
74
+ })
75
+ export class CaoAutocompleteComponent implements OnInit, OnChanges, OnDestroy, AfterViewInit, ControlValueAccessor {
76
+ @Input() label = "";
77
+ @Input() placeholder = "";
78
+ @Input() options: ICaoAutoCompleteOption[] = [];
79
+ @Input() control: AbstractControl = new FormControl();
80
+ @Input() dataCy = "";
81
+ @Input() controlName = "";
82
+ @Input() isLucideIcon = false;
83
+ @Input() iconColor = "";
84
+ @Input() _leadingIcon?: string;
85
+ @Input() _trailingIcon?: string;
86
+ @Input() optionIcon?: string;
87
+ @Input() optionIConColor?: string;
88
+ @Input() isLucideOptionIcon?: boolean;
89
+ @Input() autocompleteBgColor = 'white';
90
+ @Input() strokeWidthIcon = 1;
91
+ @Input() isDisabled = false;
92
+ @Input() inputHeigth = '50px';
93
+ @Input() iconSize = 24
94
+
95
+ @Input() checkIcon = '✓';
96
+ @Input() showCheckIcon = true;
97
+ @Input() isLucideCheckIcon = false;
98
+
99
+ @Output() readonly itemSelected = new EventEmitter<ICaoAutoCompleteOption>();
100
+ @Output() readonly blurEvent = new EventEmitter<void>();
101
+ @Output() readonly focusEvent = new EventEmitter<void>();
102
+
103
+ @Output() readonly leadingIconClick = new EventEmitter<MouseEvent | Event>();
104
+ @Output() readonly trailingIconClick = new EventEmitter<MouseEvent | Event>();
105
+
106
+ @ViewChild('inputEl') inputEl!: ElementRef<HTMLInputElement>;
107
+ @ViewChild('wrapper', { static: true }) wrapperEl!: ElementRef<HTMLDivElement>;
108
+
109
+ // CORREÇÃO: ViewChild para o ng-template
110
+ @ViewChild('panel', { static: true }) panelTemplate!: TemplateRef<any>;
111
+
112
+ /** controla apenas o texto digitado / exibido no input */
113
+ internalControl = new FormControl<string>("");
114
+ filteredOptions$!: Observable<ICaoAutoCompleteOption[]>;
115
+ filteredOptionsList: ICaoAutoCompleteOption[] = [];
116
+
117
+ isFocused = false;
118
+ isPanelOpen = false;
119
+ activeIndex = -1;
120
+
121
+ private static uidSeq = 0;
122
+ readonly listboxId = `cao-autocomplete-listbox-${CaoAutocompleteComponent.uidSeq++}`;
123
+
124
+ private selectedOption: ICaoAutoCompleteOption | null = null;
125
+ private options$ = new BehaviorSubject<ICaoAutoCompleteOption[]>([]);
126
+ /** indica se o valor atual do internalControl foi digitado pelo usuário (e não escrito programaticamente) */
127
+ private isUserTyping = false;
128
+ private el = inject(ElementRef<HTMLElement>);
129
+ private cdr = inject(ChangeDetectorRef);
130
+ private renderer = inject(Renderer2);
131
+
132
+ // Injeções do CDK Overlay
133
+ private overlay = inject(Overlay);
134
+ private viewContainerRef = inject(ViewContainerRef);
135
+ private scrollDispatcher = inject(ScrollDispatcher);
136
+
137
+ private sub = new Subscription();
138
+ private overlayRef: OverlayRef | null = null;
139
+ private portal: TemplatePortal<any> | null = null;
140
+
141
+ private onChange: (value: ICaoAutoCompleteOption | string | null) => void = () => { };
142
+ private onTouched: () => void = () => { };
143
+
144
+ ngOnInit(): void {
145
+ this.setInputHeight();
146
+ this.filteredOptions$ = combineLatest([
147
+ this.internalControl.valueChanges.pipe(startWith("")),
148
+ this.options$,
149
+ ]).pipe(
150
+ map(([value]) => this.filter(value || ""))
151
+ );
152
+
153
+ this.sub.add(
154
+ this.filteredOptions$.subscribe((list) => {
155
+ this.filteredOptionsList = list;
156
+ this.activeIndex = list.length ? 0 : -1;
157
+ // Atualiza o overlay se estiver aberto e anexado
158
+ if (this.isPanelOpen && this.portal && this.overlayRef?.hasAttached()) {
159
+ this.overlayRef?.updatePosition();
160
+ }
161
+ })
162
+ );
163
+
164
+ this.sub.add(
165
+ this.control.valueChanges.subscribe((value) => {
166
+ this.writeDisplayValue(value);
167
+ this.selectedOption = value && typeof value === "object" ? value : null;
168
+ })
169
+ );
170
+
171
+ this.setStyle();
172
+ this.setupOverlay();
173
+ }
174
+
175
+ ngAfterViewInit(): void {
176
+ // CORREÇÃO: Usando o TemplateRef obtido do ViewChild
177
+ this.portal = new TemplatePortal(
178
+ this.panelTemplate,
179
+ this.viewContainerRef
180
+ );
181
+ }
182
+
183
+ ngOnChanges(changes: SimpleChanges): void {
184
+ if (changes['options']) {
185
+ this.options$.next(changes['options'].currentValue ?? []);
186
+ }
187
+ }
188
+
189
+ ngOnDestroy(): void {
190
+ this.sub.unsubscribe();
191
+ this.closeOverlay();
192
+ if (this.overlayRef) {
193
+ this.overlayRef.dispose();
194
+ this.overlayRef = null;
195
+ }
196
+ }
197
+
198
+ private setupOverlay(): void {
199
+ const scrollStrategy = this.overlay.scrollStrategies.reposition();
200
+
201
+ this.overlayRef = this.overlay.create({
202
+ positionStrategy: this.overlay.position()
203
+ .flexibleConnectedTo(this.wrapperEl)
204
+ .withPositions([
205
+ {
206
+ originX: 'start',
207
+ originY: 'bottom',
208
+ overlayX: 'start',
209
+ overlayY: 'top',
210
+ offsetY: 4,
211
+ },
212
+ {
213
+ originX: 'start',
214
+ originY: 'top',
215
+ overlayX: 'start',
216
+ overlayY: 'bottom',
217
+ offsetY: -4,
218
+ }
219
+ ])
220
+ .withDefaultOffsetX(0)
221
+ .withFlexibleDimensions(false)
222
+ .withViewportMargin(8),
223
+ scrollStrategy: scrollStrategy,
224
+ hasBackdrop: false,
225
+ backdropClass: '',
226
+ minWidth: 200,
227
+ maxHeight: 260,
228
+ });
229
+
230
+ // Fecha o overlay quando houver scroll fora do elemento
231
+ this.scrollDispatcher.scrolled().subscribe((scrollable) => {
232
+ if (this.isPanelOpen) {
233
+ const overlayEl = this.overlayRef?.overlayElement;
234
+ if (scrollable instanceof CdkScrollable) {
235
+ const scrollableEl = scrollable.getElementRef().nativeElement;
236
+ if (overlayEl?.contains(scrollableEl)) {
237
+ return;
238
+ }
239
+ }
240
+ this.closePanel();
241
+ }
242
+ });
243
+ }
244
+
245
+ private closeOverlay(): void {
246
+ if (this.overlayRef?.hasAttached()) {
247
+ this.overlayRef.detach();
248
+ }
249
+ this.isPanelOpen = false;
250
+ }
251
+
252
+ private openOverlay(): void {
253
+ if (!this.overlayRef || !this.portal) {return;}
254
+
255
+ this.overlayRef.updateSize({
256
+ width: this.wrapperEl.nativeElement.offsetWidth,
257
+ });
258
+
259
+ if (!this.overlayRef.hasAttached()) {
260
+ this.overlayRef.attach(this.portal);
261
+ }
262
+
263
+ this.overlayRef.updatePosition();
264
+
265
+ this.isPanelOpen = true;
266
+ this.cdr.markForCheck();
267
+ }
268
+
269
+ private setStyle(): void {
270
+ this.renderer.setStyle(
271
+ this.el.nativeElement,
272
+ '--autocomplete-bg-color',
273
+ this.autocompleteBgColor
274
+ );
275
+ }
276
+
277
+ // ---------- Painel ----------
278
+
279
+ openPanel(): void {
280
+ if (this.isDisabled || this.isPanelOpen) { return; }
281
+
282
+ if (this.selectedOption && !this.isUserTyping) {
283
+ // Ao reabrir com uma opção já selecionada (e sem edição manual do usuário),
284
+ // exibe todas as opções, com a selecionada no topo.
285
+ this.filteredOptionsList = this.filter("");
286
+ const idx = this.filteredOptionsList.findIndex(o => o.id === this.selectedOption?.id);
287
+ this.activeIndex = idx >= 0 ? idx : (this.filteredOptionsList.length ? 0 : -1);
288
+ }
289
+
290
+ this.openOverlay();
291
+ }
292
+
293
+ closePanel(): void {
294
+ if (!this.isPanelOpen) { return; }
295
+ this.closeOverlay();
296
+ this.cdr.markForCheck();
297
+ }
298
+
299
+ @HostListener('document:click', ['$event'])
300
+ onDocumentClick(event: MouseEvent): void {
301
+ if (!this.isPanelOpen) { return; }
302
+ const target = event.target as Node;
303
+ const clickedInsideHost = this.el.nativeElement.contains(target);
304
+ const clickedInsideOverlay = this.overlayRef?.overlayElement?.contains(target);
305
+
306
+ if (!clickedInsideHost && !clickedInsideOverlay) {
307
+ this.closePanel();
308
+ }
309
+ }
310
+
311
+ // ---------- Eventos do input ----------
312
+
313
+ onFocus(): void {
314
+ this.isFocused = true;
315
+ this.focusEvent.emit();
316
+ this.onTouched();
317
+ this.openPanel();
318
+ }
319
+
320
+ onBlur(): void {
321
+ this.isFocused = false;
322
+ this.blurEvent.emit();
323
+ this.onTouched();
324
+
325
+ // Não fecha o overlay imediatamente no blur para permitir seleção
326
+ setTimeout(() => {
327
+ if (!this.isPanelOpen) {return;}
328
+ const activeElement = document.activeElement;
329
+ const isOverlayElement = this.overlayRef?.overlayElement?.contains(activeElement);
330
+ const isInputElement = this.inputEl?.nativeElement === activeElement;
331
+
332
+ if (!isOverlayElement && !isInputElement) {
333
+ this.closePanel();
334
+ }
335
+ }, 150);
336
+ }
337
+
338
+ onInputKeydown(event: KeyboardEvent): void {
339
+ switch (event.key) {
340
+ case 'ArrowDown':
341
+ event.preventDefault();
342
+ if (!this.isPanelOpen) { this.openPanel(); return; }
343
+ this.moveActiveIndex(1);
344
+ break;
345
+ case 'ArrowUp':
346
+ event.preventDefault();
347
+ if (!this.isPanelOpen) { this.openPanel(); return; }
348
+ this.moveActiveIndex(-1);
349
+ break;
350
+ case 'Enter':
351
+ if (this.isPanelOpen && this.activeIndex >= 0) {
352
+ event.preventDefault();
353
+ const option = this.filteredOptionsList[this.activeIndex];
354
+ if (option) { this.selectItem(option); }
355
+ }
356
+ break;
357
+ case 'Escape':
358
+ if (this.isPanelOpen) {
359
+ event.preventDefault();
360
+ this.closePanel();
361
+ } else {
362
+ this.clearSelection();
363
+ }
364
+ break;
365
+ case 'Tab':
366
+ this.closePanel();
367
+ break;
368
+ }
369
+ }
370
+
371
+ private moveActiveIndex(step: number): void {
372
+ if (!this.filteredOptionsList.length) { return; }
373
+ const total = this.filteredOptionsList.length;
374
+ this.activeIndex = (this.activeIndex + step + total) % total;
375
+ this.cdr.markForCheck();
376
+ }
377
+
378
+ // ---------- Seleção ----------
379
+
380
+ selectItem(option: ICaoAutoCompleteOption): void {
381
+ this.selectedOption = option;
382
+ this.writeDisplayValue(option, { emitEvent: false });
383
+ this.control.setValue(option, { emitEvent: false });
384
+ this.onChange(option);
385
+ this.itemSelected.emit(option);
386
+
387
+ // Reordena a lista filtrada para que a opção selecionada fique no topo
388
+ this.filteredOptionsList = this.moveSelectedToTop(this.filteredOptionsList, option);
389
+
390
+ this.closePanel();
391
+
392
+ // Remove o foco do input após a seleção
393
+ this.inputEl?.nativeElement.blur();
394
+ this.isFocused = false;
395
+
396
+ this.cdr.markForCheck();
397
+ }
398
+
399
+ selectOption(option: ICaoAutoCompleteOption): void {
400
+ this.selectItem(option);
401
+ }
402
+
403
+ clearSelection(): void {
404
+ this.selectedOption = null;
405
+ this.writeDisplayValue("", { emitEvent: false });
406
+ this.control.setValue(null, { emitEvent: true });
407
+ this.onChange(null);
408
+ this.onTouched();
409
+ this.closePanel();
410
+ }
411
+
412
+ onLeadingIconClick(event: MouseEvent | Event): void {
413
+ event.stopPropagation();
414
+ this.onFocus()
415
+ this.leadingIconClick.emit(event);
416
+ }
417
+
418
+ onTrailingIconClick(event: MouseEvent | Event): void {
419
+ event.stopPropagation();
420
+ this.onFocus()
421
+ this.trailingIconClick.emit(event);
422
+ }
423
+
424
+ // ---------- Filtro / display ----------
425
+
426
+ private filter(value: string): ICaoAutoCompleteOption[] {
427
+ const filterValue = (value || "").toLowerCase();
428
+ const currentOptions = this.options$.getValue();
429
+
430
+ if (!currentOptions?.length) {
431
+ return [];
432
+ }
433
+
434
+ const matched = currentOptions.filter((option) => {
435
+ const nomeMatch = option.nome.toLowerCase().includes(filterValue);
436
+ const idMatch = option.id?.toString().toLowerCase().includes(filterValue);
437
+ return nomeMatch || idMatch;
438
+ });
439
+
440
+ return this.selectedOption ? this.moveSelectedToTop(matched, this.selectedOption) : matched;
441
+ }
442
+
443
+ /** Move a opção selecionada para o início da lista, mantendo a ordem das demais */
444
+ private moveSelectedToTop(
445
+ list: ICaoAutoCompleteOption[],
446
+ selected: ICaoAutoCompleteOption
447
+ ): ICaoAutoCompleteOption[] {
448
+ const selectedIndex = list.findIndex((o) => o.id === selected.id);
449
+
450
+ if (selectedIndex <= 0) {
451
+ return list;
452
+ }
453
+
454
+ const reordered = [...list];
455
+ const [selectedItem] = reordered.splice(selectedIndex, 1);
456
+ reordered.unshift(selectedItem);
457
+ return reordered;
458
+ }
459
+
460
+ private writeDisplayValue(
461
+ value: ICaoAutoCompleteOption | string | null | undefined,
462
+ options: { emitEvent?: boolean } = { emitEvent: false }
463
+ ): void {
464
+ const text = value && typeof value === "object" ? this.displayFn(value) : (value ?? "");
465
+ this.isUserTyping = false;
466
+ this.internalControl.setValue(text, options);
467
+ }
468
+
469
+ /** Marca que o usuário está digitando manualmente no campo (ativa o filtro por texto) */
470
+ onUserInput(): void {
471
+ this.isUserTyping = true;
472
+ }
473
+
474
+ displayFn(option: ICaoAutoCompleteOption): string {
475
+ return option && option.nome ? option.nome : "";
476
+ }
477
+
478
+ isOptionSelected(option: ICaoAutoCompleteOption): boolean {
479
+ return this.selectedOption?.id !== undefined && this.selectedOption?.id === option.id;
480
+ }
481
+
482
+ trackByOptionId(_: number, option: ICaoAutoCompleteOption): string | number {
483
+ return option.id ?? option.nome;
484
+ }
485
+
486
+ optionId(option: ICaoAutoCompleteOption): string {
487
+ const key = (option.id ?? option.nome).toString().replace(/[^a-zA-Z0-9_-]/g, '-');
488
+ return `${this.listboxId}-opt-${key}`;
489
+ }
490
+
491
+ get activeOptionId(): string | null {
492
+ if (!this.isPanelOpen || this.activeIndex < 0) {
493
+ return null;
494
+ }
495
+ const option = this.filteredOptionsList[this.activeIndex];
496
+ return option ? this.optionId(option) : null;
497
+ }
498
+
499
+ get errorText(): string | null {
500
+ if (!this.control) { return null; }
501
+
502
+ const isEmpty = this.control.value === null || this.control.value === "";
503
+
504
+ if (this.control.hasError("required") && isEmpty) {
505
+ return "*Campo obrigatório";
506
+ }
507
+
508
+ if (this.control.touched && this.control.invalid) {
509
+ if (this.control.hasError("cpfInvalid")) { return "CPF inválido"; }
510
+ if (this.control.hasError("cellPhoneInvalid")) { return "Número inválido"; }
511
+ if (this.control.hasError("emailInvalid")) { return "Email inválido"; }
512
+ if (this.control.hasError("cnpjInvalid")) { return "CNPJ inválido"; }
513
+ if (this.control.hasError("rgInvalid")) { return "RG inválido"; }
514
+ if (this.control.hasError("dateInvalid")) { return "Data inválida"; }
515
+ if (this.control.hasError("dateStartInvalid")) { return "Data Inicial inválida"; }
516
+ if (this.control.hasError("dateEndInvalid")) { return "Data Final inválida"; }
517
+ return "Formato inválido";
518
+ }
519
+
520
+ return null;
521
+ }
522
+
523
+ get controlNameValue(): string {
524
+ return this.controlName;
525
+ }
526
+
527
+ writeValue(value: ICaoAutoCompleteOption | string): void {
528
+ this.writeDisplayValue(value, { emitEvent: false });
529
+ this.selectedOption = value && typeof value === "object" ? value : null;
530
+ }
531
+
532
+ registerOnChange(fn: (value: ICaoAutoCompleteOption | string | null) => void): void {
533
+ this.onChange = fn;
534
+ }
535
+
536
+ registerOnTouched(fn: () => void): void {
537
+ this.onTouched = fn;
538
+ }
539
+
540
+ setDisabledState?(isDisabled: boolean): void {
541
+ this.isDisabled = isDisabled;
542
+ this.cdr.markForCheck();
543
+ }
544
+
545
+ setInputHeight(): void {
546
+ this.renderer.setStyle(
547
+ this.el.nativeElement,
548
+ "--form-field-height",
549
+ this.inputHeigth
550
+ );
551
+ }
552
+ }
@@ -0,0 +1,33 @@
1
+ <button
2
+ type="button"
3
+ class="base-button"
4
+ matTooltipPosition="below"
5
+ matTooltipClass="custom-arrow-tooltip"
6
+ [matTooltip]="tooltip"
7
+ [matTooltipDisabled]="!tooltip"
8
+ [class.dark-mode]="isDarkMode"
9
+ [class.disabled]="isDisabled"
10
+ [class.loading]="isLoading"
11
+ [disabled]="isDisabled || isLoading"
12
+ [attr.data-cy]="dataCy"
13
+ (click)="onClick()">
14
+ @if (leadingIcon) {
15
+ <span class="leading-icon">
16
+ @if (isLucideIcon) {
17
+ <svg [lucideIcon]="leadingIcon" [size]="iconSize" [style.color]="iconColor" [strokeWidth]="strokeWidthIcon"></svg>
18
+ } @else {
19
+ <img alt="icon" [src]="leadingIcon" [style.width]="iconSize" />
20
+ }
21
+ </span>
22
+ } {{ buttonText }} @if (trailingIcon) {
23
+ <span class="trailing-icon">
24
+ @if (isLucideIcon) {
25
+ <svg [lucideIcon]="trailingIcon" [size]="iconSize" [style.color]="iconColor" [strokeWidth]="strokeWidthIcon"></svg>
26
+ } @else {
27
+ <img alt="icon" [src]="trailingIcon" [style.width]="iconSize" />
28
+ }
29
+ </span>
30
+ } @if (isLoading) {
31
+ <span class="loader"></span>
32
+ }
33
+ </button>
@@ -0,0 +1,14 @@
1
+ .base-button{
2
+ display: flex;
3
+ gap: 5px;
4
+ width: 100%;
5
+ align-items: center;
6
+ justify-content: center;
7
+ cursor: pointer;
8
+ &.disabled {
9
+ cursor: not-allowed;
10
+ }
11
+ &.loading{
12
+ cursor: progress;
13
+ }
14
+ }
@@ -0,0 +1,64 @@
1
+ import { ComponentFixture, TestBed } from "@angular/core/testing";
2
+ import { CaoBaseButtonComponent } from "./base-button.component";
3
+
4
+ describe("CaoBaseButtonComponent", () => {
5
+ let component: CaoBaseButtonComponent;
6
+ let fixture: ComponentFixture<CaoBaseButtonComponent>;
7
+
8
+ beforeEach(async () => {
9
+ await TestBed.configureTestingModule({
10
+ imports: [CaoBaseButtonComponent],
11
+ }).compileComponents();
12
+ });
13
+
14
+ beforeEach(() => {
15
+ fixture = TestBed.createComponent(CaoBaseButtonComponent);
16
+ component = fixture.componentInstance;
17
+ });
18
+
19
+ it("deveria criar", () => {
20
+ fixture.detectChanges();
21
+ expect(component).toBeTruthy();
22
+ });
23
+
24
+ it("deve emitir o evento buttonClick quando clicado", () => {
25
+ vi.spyOn(component.buttonClick, "emit");
26
+ component.onClick();
27
+ expect(component.buttonClick.emit).toHaveBeenCalledWith();
28
+ });
29
+
30
+ it("não deve emitir o evento buttonClick quando desabilitado", () => {
31
+ component.isDisabled = true;
32
+ vi.spyOn(component.buttonClick, "emit");
33
+ component.onClick();
34
+ expect(component.buttonClick.emit).not.toHaveBeenCalled();
35
+ });
36
+
37
+ it("não deve emitir o evento buttonClick ao carregar", () => {
38
+ component.isLoading = true;
39
+ vi.spyOn(component.buttonClick, "emit");
40
+ component.onClick();
41
+ expect(component.buttonClick.emit).not.toHaveBeenCalled();
42
+ });
43
+
44
+ it("deve aplicar a classe de modo escuro quando isDarkMode for verdadeiro", () => {
45
+ component.isDarkMode = true;
46
+ fixture.detectChanges();
47
+ const button = fixture.nativeElement.querySelector("button");
48
+ expect(button.classList.contains("dark-mode")).toBeTruthy();
49
+ });
50
+
51
+ it("deve aplicar a classe desabilitada quando isDisabled for verdadeiro", () => {
52
+ component.isDisabled = true;
53
+ fixture.detectChanges();
54
+ const button = fixture.nativeElement.querySelector("button");
55
+ expect(button.classList.contains("disabled")).toBeTruthy();
56
+ });
57
+
58
+ it("deve aplicar a classe de carregamento quando isLoading for verdadeiro", () => {
59
+ component.isLoading = true;
60
+ fixture.detectChanges();
61
+ const button = fixture.nativeElement.querySelector("button");
62
+ expect(button.classList.contains("loading")).toBeTruthy();
63
+ });
64
+ });
@@ -0,0 +1,48 @@
1
+ import { CommonModule } from "@angular/common";
2
+ import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
3
+ import { MatTooltipModule } from "@angular/material/tooltip";
4
+
5
+ import { LucideDynamicIcon } from "@lucide/angular";
6
+
7
+ @Component({
8
+ standalone: true,
9
+ selector: "cao-button",
10
+ templateUrl: "./base-button.component.html",
11
+ styleUrls: ["./base-button.component.scss"],
12
+ imports: [CommonModule, MatTooltipModule, LucideDynamicIcon],
13
+ })
14
+ export class CaoBaseButtonComponent implements OnInit{
15
+ @Output() readonly buttonClick = new EventEmitter<void>();
16
+
17
+ @Input() buttonText = "Clique!";
18
+ @Input() isDarkMode = false;
19
+ @Input() isDisabled = false;
20
+ @Input() isLoading = false;
21
+ @Input() btnClass = "";
22
+
23
+ @Input() tooltip?:string;
24
+ @Input() trailingIcon?: string;
25
+ @Input() leadingIcon?: string;
26
+ @Input() isLucideIcon = true;
27
+ @Input() dataCy?: string;
28
+ @Input() iconColor?: string;
29
+ @Input() strokeWidthIcon = 1
30
+ @Input() minTextToTooltip = 20
31
+ @Input() iconSize = 24
32
+
33
+ ngOnInit(): void {
34
+ if (!this.dataCy) {
35
+ this.dataCy = `btn-${this.buttonText.replace(/\s+/g, '-').toLowerCase()}`;
36
+ }
37
+
38
+ if(!this.tooltip && this.buttonText.length >= this.minTextToTooltip){
39
+ this.tooltip = this.buttonText
40
+ }
41
+ }
42
+
43
+ onClick(): void {
44
+ if (!this.isDisabled && !this.isLoading) {
45
+ this.buttonClick.emit();
46
+ }
47
+ }
48
+ }