ngx-opalbytes-components 1.19.0 → 1.21.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 (95) hide show
  1. package/README.md +103 -0
  2. package/cli/cao-comp.cjs +456 -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/autocomplete/autocomplete.stories.ts +133 -0
  10. package/src/lib/shared/components/base-button/base-button.component.html +33 -0
  11. package/src/lib/shared/components/base-button/base-button.component.scss +14 -0
  12. package/src/lib/shared/components/base-button/base-button.component.spec.ts +64 -0
  13. package/src/lib/shared/components/base-button/base-button.component.ts +48 -0
  14. package/src/lib/shared/components/base-button/base-button.stories.ts +102 -0
  15. package/src/lib/shared/components/base-icon/base-icon.css +0 -0
  16. package/src/lib/shared/components/base-icon/base-icon.html +11 -0
  17. package/src/lib/shared/components/base-icon/base-icon.spec.ts +23 -0
  18. package/src/lib/shared/components/base-icon/base-icon.stories.ts +56 -0
  19. package/src/lib/shared/components/base-icon/base-icon.ts +18 -0
  20. package/src/lib/shared/components/base-input/base-input.css +193 -0
  21. package/src/lib/shared/components/base-input/base-input.html +43 -0
  22. package/src/lib/shared/components/base-input/base-input.spec.ts +146 -0
  23. package/src/lib/shared/components/base-input/base-input.stories.ts +137 -0
  24. package/src/lib/shared/components/base-input/base-input.ts +133 -0
  25. package/src/lib/shared/components/base-time-range/base-time-range.css +27 -0
  26. package/src/lib/shared/components/base-time-range/base-time-range.html +23 -0
  27. package/src/lib/shared/components/base-time-range/base-time-range.spec.ts +27 -0
  28. package/src/lib/shared/components/base-time-range/base-time-range.stories.ts +54 -0
  29. package/src/lib/shared/components/base-time-range/base-time-range.ts +61 -0
  30. package/src/lib/shared/components/base-toggle/base-toggle.component.html +28 -0
  31. package/src/lib/shared/components/base-toggle/base-toggle.component.scss +45 -0
  32. package/src/lib/shared/components/base-toggle/base-toggle.component.spec.ts +22 -0
  33. package/src/lib/shared/components/base-toggle/base-toggle.component.ts +138 -0
  34. package/src/lib/shared/components/base-toggle/base-toggle.stories.ts +63 -0
  35. package/src/lib/shared/components/card/card.component.css +264 -0
  36. package/src/lib/shared/components/card/card.component.html +103 -0
  37. package/src/lib/shared/components/card/card.component.ts +63 -0
  38. package/src/lib/shared/components/card/card.stories.ts +114 -0
  39. package/src/lib/shared/components/date-picker/date-picker.html +82 -0
  40. package/src/lib/shared/components/date-picker/date-picker.scss +8 -0
  41. package/src/lib/shared/components/date-picker/date-picker.spec.ts +23 -0
  42. package/src/lib/shared/components/date-picker/date-picker.stories.ts +91 -0
  43. package/src/lib/shared/components/date-picker/date-picker.ts +165 -0
  44. package/src/lib/shared/components/drop-down/drop-down.css +61 -0
  45. package/src/lib/shared/components/drop-down/drop-down.html +96 -0
  46. package/src/lib/shared/components/drop-down/drop-down.spec.ts +335 -0
  47. package/src/lib/shared/components/drop-down/drop-down.stories.ts +90 -0
  48. package/src/lib/shared/components/drop-down/drop-down.ts +335 -0
  49. package/src/lib/shared/components/footer/footer.css +29 -0
  50. package/src/lib/shared/components/footer/footer.html +10 -0
  51. package/src/lib/shared/components/footer/footer.spec.ts +22 -0
  52. package/src/lib/shared/components/footer/footer.stories.ts +51 -0
  53. package/src/lib/shared/components/footer/footer.ts +15 -0
  54. package/src/lib/shared/components/index.ts +19 -0
  55. package/src/lib/shared/components/kpi/kpi.css +0 -0
  56. package/src/lib/shared/components/kpi/kpi.html +11 -0
  57. package/src/lib/shared/components/kpi/kpi.spec.ts +38 -0
  58. package/src/lib/shared/components/kpi/kpi.stories.ts +65 -0
  59. package/src/lib/shared/components/kpi/kpi.ts +25 -0
  60. package/src/lib/shared/components/links-button/links-button.css +70 -0
  61. package/src/lib/shared/components/links-button/links-button.html +23 -0
  62. package/src/lib/shared/components/links-button/links-button.spec.ts +139 -0
  63. package/src/lib/shared/components/links-button/links-button.stories.ts +75 -0
  64. package/src/lib/shared/components/links-button/links-button.ts +45 -0
  65. package/src/lib/shared/components/paginator/paginator.css +0 -0
  66. package/src/lib/shared/components/paginator/paginator.html +10 -0
  67. package/src/lib/shared/components/paginator/paginator.spec.ts +23 -0
  68. package/src/lib/shared/components/paginator/paginator.stories.ts +56 -0
  69. package/src/lib/shared/components/paginator/paginator.ts +25 -0
  70. package/src/lib/shared/components/stepper/step/step.css +8 -0
  71. package/src/lib/shared/components/stepper/step/step.html +1 -0
  72. package/src/lib/shared/components/stepper/step/step.spec.ts +131 -0
  73. package/src/lib/shared/components/stepper/step/step.ts +14 -0
  74. package/src/lib/shared/components/stepper/stepper.css +17 -0
  75. package/src/lib/shared/components/stepper/stepper.html +11 -0
  76. package/src/lib/shared/components/stepper/stepper.spec.ts +238 -0
  77. package/src/lib/shared/components/stepper/stepper.stories.ts +77 -0
  78. package/src/lib/shared/components/stepper/stepper.ts +57 -0
  79. package/src/lib/shared/components/tab-group/tab-group.component.html +84 -0
  80. package/src/lib/shared/components/tab-group/tab-group.component.scss +151 -0
  81. package/src/lib/shared/components/tab-group/tab-group.spec.ts +22 -0
  82. package/src/lib/shared/components/tab-group/tab-group.stories.ts +102 -0
  83. package/src/lib/shared/components/tab-group/tab-group.ts +218 -0
  84. package/src/lib/shared/components/tab-panel/tab-content.directive.ts +7 -0
  85. package/src/lib/shared/components/tab-panel/tab-label.directive.ts +7 -0
  86. package/src/lib/shared/components/tab-panel/tab-panel.css +0 -0
  87. package/src/lib/shared/components/tab-panel/tab-panel.spec.ts +22 -0
  88. package/src/lib/shared/components/tab-panel/tab-panel.stories.ts +54 -0
  89. package/src/lib/shared/components/tab-panel/tab-panel.ts +31 -0
  90. package/src/lib/shared/components/time-picker/time-picker.css +84 -0
  91. package/src/lib/shared/components/time-picker/time-picker.html +21 -0
  92. package/src/lib/shared/components/time-picker/time-picker.spec.ts +23 -0
  93. package/src/lib/shared/components/time-picker/time-picker.stories.ts +66 -0
  94. package/src/lib/shared/components/time-picker/time-picker.ts +90 -0
  95. package/types/ngx-opalbytes-components.d.ts +23 -4
@@ -0,0 +1,114 @@
1
+ import type { Meta, StoryObj } from '@storybook/angular-vite';
2
+ import { fn } from 'storybook/test';
3
+
4
+ import { CaoCardComponent, ICaoCardBadge, ICaoCardMeta } from './card.component';
5
+
6
+ const cardMeta: ICaoCardMeta[] = [
7
+ { icon: 'https://lucide.dev/icons/eye.svg', label: 'Visualizações' },
8
+ { icon: 'https://lucide.dev/icons/clock.svg', label: 'Última atualização' },
9
+ ];
10
+
11
+ const cardBadge: ICaoCardBadge = {
12
+ label: 'Novo',
13
+ color: 'text-white',
14
+ borderColor: 'border-green-600',
15
+ bg: '#22c55e',
16
+ };
17
+
18
+ const meta: Meta<CaoCardComponent> = {
19
+ title: 'Componentes/Card',
20
+ component: CaoCardComponent,
21
+ tags: ['autodocs'],
22
+ argTypes: {
23
+ variant: { control: 'select', options: ['horizontal', 'square'] },
24
+ title: { control: 'text' },
25
+ subtitle: { control: 'text' },
26
+ detail: { control: 'text' },
27
+ detailColor: { control: 'text' },
28
+ icon: { control: 'text' },
29
+ bgColor: { control: 'text' },
30
+ borderColor: { control: 'text' },
31
+ selected: { control: 'boolean' },
32
+ clickable: { control: 'boolean' },
33
+ cardClick: { action: 'cardClicked' },
34
+ },
35
+ args: {
36
+ cardClick: fn(),
37
+ title: 'Projeto Renova',
38
+ subtitle: 'Monitoramento de qualidade',
39
+ },
40
+ };
41
+
42
+ export default meta;
43
+ type Story = StoryObj<CaoCardComponent>;
44
+
45
+ export const Horizontal: Story = {
46
+ args: {
47
+ variant: 'horizontal',
48
+ },
49
+ };
50
+
51
+ export const HorizontalComIcone: Story = {
52
+ args: {
53
+ variant: 'horizontal',
54
+ icon: 'https://lucide.dev/icons/image.svg',
55
+ },
56
+ };
57
+
58
+ export const HorizontalComAvatar: Story = {
59
+ args: {
60
+ variant: 'horizontal',
61
+ avatar: { initials: 'OP', color: 'bg-indigo-500' },
62
+ },
63
+ };
64
+
65
+ export const HorizontalComDetalhe: Story = {
66
+ args: {
67
+ variant: 'horizontal',
68
+ detail: 'Detalhe adicional do card',
69
+ detailColor: 'text-amber-600',
70
+ },
71
+ };
72
+
73
+ export const ComBadge: Story = {
74
+ args: {
75
+ variant: 'horizontal',
76
+ badge: cardBadge,
77
+ },
78
+ };
79
+
80
+ export const ComMeta: Story = {
81
+ args: {
82
+ variant: 'horizontal',
83
+ meta: cardMeta,
84
+ },
85
+ };
86
+
87
+ export const Clicavel: Story = {
88
+ args: {
89
+ variant: 'horizontal',
90
+ clickable: true,
91
+ },
92
+ };
93
+
94
+ export const Selecionado: Story = {
95
+ args: {
96
+ variant: 'horizontal',
97
+ selected: true,
98
+ },
99
+ };
100
+
101
+ export const Quadrado: Story = {
102
+ args: {
103
+ variant: 'square',
104
+ title: 'KPIs',
105
+ },
106
+ };
107
+
108
+ export const QuadradoComIcone: Story = {
109
+ args: {
110
+ variant: 'square',
111
+ title: 'Análises',
112
+ icon: 'https://lucide.dev/icons/image.svg',
113
+ },
114
+ };
@@ -0,0 +1,82 @@
1
+ <div class="form-field input-component" [class.search-mode]="searchMode">
2
+ <mat-form-field class="datepicker-form-field">
3
+ <div
4
+ class="input-wrapper"
5
+ [class.has-leading-icon]="_leadingIcon"
6
+ [class.has-trailing-icon]="_trailingIcon"
7
+ [class.focused]="isFocused">
8
+
9
+ @if (_leadingIcon) {
10
+ @if (isLucideIcon) {
11
+ <div class="icon"
12
+ tabindex="0"
13
+ (click)="onLeadingIconClick($event)"
14
+ (keydown.enter)="onLeadingIconClick($event)"
15
+ >
16
+ <svg [lucideIcon]="_leadingIcon" [style.color]="iconColor"></svg>
17
+ </div>
18
+ } @else {
19
+ <div class="icon"
20
+ tabindex="0"
21
+ (click)="onLeadingIconClick($event)"
22
+ (keydown.enter)="onLeadingIconClick($event)">
23
+ <img alt="ícone de início" [src]="_leadingIcon" />
24
+ </div>
25
+ }
26
+ }
27
+
28
+ <input
29
+ matTooltip="{{ tooltip }}"
30
+ matTooltipPosition="below"
31
+ matTooltipClass="custom-arrow-tooltip"
32
+ matInput
33
+ [disabled]="isDisabled"
34
+ [matDatepicker]="pickerCao"
35
+ [attr.data-cy]="dataCy"
36
+ [placeholder]="placeholder"
37
+ [ngClass]="[
38
+ inputClass,
39
+ control?.touched && control?.invalid ? 'invalid' : '',
40
+ control?.touched && control?.valid && control?.value ? 'valid' : ''
41
+ ]"
42
+ [min]="minDate"
43
+ [max]="maxDate"
44
+ [(ngModel)]="value"
45
+ (input)="onInput($event)"
46
+ (change)="onInputChange($event)"
47
+ (focus)="onFocus()"
48
+ (blur)="onBlur()" />
49
+
50
+ @if (dateHint) {
51
+ <mat-hint>{{ dateHint }}</mat-hint>
52
+ }
53
+
54
+ <mat-datepicker-toggle matIconSuffix [for]="pickerCao" />
55
+
56
+ <mat-datepicker
57
+ #pickerCao
58
+ [startAt]="value || null" />
59
+
60
+ @if (_trailingIcon) {
61
+ @if (isLucideIcon) {
62
+ <div class="icon trailing-icon"
63
+ tabindex="0"
64
+ (click)="onTrailingIconClick($event)"
65
+ (keydown.enter)="onTrailingIconClick($event)"
66
+ >
67
+ <svg [lucideIcon]="_trailingIcon" [style.color]="iconColor"></svg>
68
+ </div>
69
+ } @else {
70
+ <div class="icon trailing-icon"
71
+ tabindex="0"
72
+ (click)="onTrailingIconClick($event)"
73
+ (keydown.enter)="onTrailingIconClick($event)"
74
+ >
75
+ <img alt="ícone de fim" [src]="_trailingIcon" />
76
+ </div>
77
+ }
78
+ }
79
+
80
+ </div>
81
+ </mat-form-field>
82
+ </div>
@@ -0,0 +1,8 @@
1
+ @use '@angular/material' as mat;
2
+
3
+
4
+ ::ng-deep .datepicker-form-field {
5
+ --mat-form-field-outlined-outline-color: transparent;
6
+ --mat-form-field-outlined-hover-outline-color: transparent;
7
+ --mat-form-field-outlined-focus-outline-color: transparent;
8
+ }
@@ -0,0 +1,23 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { CaoDatePicker } from './date-picker';
4
+
5
+ describe('CaoDatePicker', () => {
6
+ let component: CaoDatePicker;
7
+ let fixture: ComponentFixture<CaoDatePicker>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [CaoDatePicker]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(CaoDatePicker);
16
+ component = fixture.componentInstance;
17
+ await fixture.whenStable();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,91 @@
1
+ import type { Meta, StoryObj } from '@storybook/angular-vite';
2
+ import { fn } from 'storybook/test';
3
+
4
+ import { CaoDatePicker } from './date-picker';
5
+
6
+ const meta: Meta<CaoDatePicker> = {
7
+ title: 'Componentes/Date Picker',
8
+ component: CaoDatePicker,
9
+ tags: ['autodocs'],
10
+ argTypes: {
11
+ placeholder: { control: 'text' },
12
+ inputClass: { control: 'text' },
13
+ errorMessage: { control: 'text' },
14
+ isDisabled: { control: 'boolean' },
15
+ searchMode: { control: 'boolean' },
16
+ showBorder: { control: 'boolean' },
17
+ isLucideIcon: { control: 'boolean' },
18
+ iconColor: { control: 'color' },
19
+ tooltip: { control: 'text' },
20
+ dataCy: { control: 'text' },
21
+ dateHint: { control: 'text' },
22
+ controlName: { control: 'text' },
23
+ openOnFocus: { control: 'boolean' },
24
+ minDate: { control: 'date' },
25
+ maxDate: { control: 'date' },
26
+ },
27
+ args: {
28
+ valueChange: fn(),
29
+ searchClick: fn(),
30
+ inputChange: fn(),
31
+ leadingIconClick: fn(),
32
+ trailingIconClick: fn(),
33
+ placeholder: 'Data de nascimento',
34
+ },
35
+ };
36
+
37
+ export default meta;
38
+ type Story = StoryObj<CaoDatePicker>;
39
+
40
+ export const Padrao: Story = {
41
+ args: {
42
+ placeholder: 'Selecione uma data',
43
+ },
44
+ };
45
+
46
+ export const ComLeadingIcon: Story = {
47
+ args: {
48
+ placeholder: 'Selecione uma data',
49
+ leadingIcon: 'Calendar',
50
+ isLucideIcon: true,
51
+ iconColor: '#3b82f6',
52
+ },
53
+ };
54
+
55
+ export const ComTrailingIcon: Story = {
56
+ args: {
57
+ placeholder: 'Selecione uma data',
58
+ trailingIcon: 'X',
59
+ isLucideIcon: true,
60
+ },
61
+ };
62
+
63
+ export const ComDataMinimaEMaxima: Story = {
64
+ args: {
65
+ placeholder: 'Selecione uma data',
66
+ minDate: new Date(2026, 0, 1),
67
+ maxDate: new Date(2026, 11, 31),
68
+ },
69
+ };
70
+
71
+ export const ComDica: Story = {
72
+ args: {
73
+ placeholder: 'Selecione uma data',
74
+ dateHint: 'DD/MM/AAAA',
75
+ },
76
+ };
77
+
78
+ export const Desabilitado: Story = {
79
+ args: {
80
+ placeholder: 'Selecione uma data',
81
+ isDisabled: true,
82
+ value: new Date(2026, 0, 15),
83
+ },
84
+ };
85
+
86
+ export const ComErro: Story = {
87
+ args: {
88
+ placeholder: 'Selecione uma data',
89
+ errorMessage: 'Data obrigatória',
90
+ },
91
+ };
@@ -0,0 +1,165 @@
1
+ import { CommonModule } from '@angular/common';
2
+ import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
3
+ import { AbstractControl, ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
4
+ import { MatDatepicker } from '@angular/material/datepicker';
5
+ import { MatTooltipModule } from '@angular/material/tooltip';
6
+
7
+ import { LucideDynamicIcon } from '@lucide/angular';
8
+
9
+ import { MaterialModule } from "../../../material.module";
10
+
11
+ @Component({
12
+ selector: 'cao-date-picker',
13
+ imports: [CommonModule, FormsModule, MatTooltipModule, LucideDynamicIcon, MaterialModule],
14
+ templateUrl: './date-picker.html',
15
+ styleUrl: './date-picker.scss',
16
+ providers: [
17
+ {
18
+ provide: NG_VALUE_ACCESSOR,
19
+ useExisting: CaoDatePicker,
20
+ multi: true,
21
+ },
22
+ ],
23
+ })
24
+ export class CaoDatePicker implements ControlValueAccessor {
25
+ @Input() placeholder = "";
26
+ @Input() inputClass = "";
27
+ @Input() value: string | number | boolean | Date = "";
28
+ @Input() errorMessage = "";
29
+ @Input() isDisabled = false;
30
+ @Input() maxLength?: number;
31
+ @Input() searchMode = false;
32
+ @Input() showBorder = false;
33
+ @Input() isLucideIcon = false;
34
+ @Input() iconColor = "";
35
+ @Input() control?: AbstractControl | null;
36
+ @Input() tooltip = "";
37
+ @Input() dataCy?: string = "";
38
+ @Input() dateHint?: string;
39
+ @Input() controlName = "";
40
+
41
+ @Input() openOnFocus = true;
42
+
43
+ @Input() minDate: Date | null = null;
44
+ @Input() maxDate: Date | null = null;
45
+
46
+ @Input() set leadingIcon(value: string) {
47
+ this._leadingIcon = value;
48
+ this.icon = true;
49
+ }
50
+
51
+ @Input() set trailingIcon(value: string) {
52
+ this._trailingIcon = value;
53
+ this.icon = true;
54
+ }
55
+
56
+ @Output() readonly valueChange = new EventEmitter<string>();
57
+ @Output() readonly searchClick = new EventEmitter<void>();
58
+ @Output() readonly inputChange = new EventEmitter<string>();
59
+ @Output() readonly leadingIconClick = new EventEmitter<MouseEvent | Event>();
60
+ @Output() readonly trailingIconClick = new EventEmitter<MouseEvent | Event>();
61
+
62
+ @ViewChild('pickerCao') picker!: MatDatepicker<Date>;
63
+
64
+ isFocused = false;
65
+ icon = false;
66
+ _leadingIcon?: string;
67
+ _trailingIcon?: string;
68
+
69
+ onChange: (value: any) => void = () => {};
70
+ onTouched: () => void = () => {};
71
+
72
+ get hasError(): boolean {
73
+ return !!this.errorMessage;
74
+ }
75
+
76
+ get controlNameValue(): string {
77
+ return this.controlName;
78
+ }
79
+
80
+ onInput(event: Event): void {
81
+ const newValue = (event.target as HTMLInputElement).value;
82
+ this.value = newValue;
83
+ this.onChange(newValue);
84
+ this.valueChange.emit(newValue);
85
+ }
86
+
87
+ onInputChange(event: Event): void {
88
+ const newValue = (event.target as HTMLInputElement).value;
89
+ this.value = newValue;
90
+ this.inputChange.emit(newValue);
91
+ }
92
+
93
+ onFocus(): void {
94
+ this.isFocused = true;
95
+
96
+ if (this.openOnFocus && this.picker && !this.picker.opened) {
97
+ this.picker.open();
98
+ }
99
+
100
+ this.onTouched();
101
+ }
102
+
103
+ onBlur(): void {
104
+ this.isFocused = false;
105
+ this.onTouched();
106
+
107
+ if (this.control?.touched && this.control?.valid && this.control?.value) {
108
+ setTimeout(() => {
109
+ const active = document.activeElement as HTMLElement;
110
+ const current = document.querySelector(`[formcontrolname="${this.controlNameValue}"]`);
111
+ if (!current || !active || !current.contains(active)) {
112
+ this.control?.markAsUntouched();
113
+ }
114
+ });
115
+ }
116
+ }
117
+
118
+ onSearchClick(): void {
119
+ this.searchClick.emit();
120
+ }
121
+
122
+ onLeadingIconClick(event: MouseEvent | Event): void {
123
+ event.stopPropagation();
124
+ this.leadingIconClick.emit(event);
125
+ }
126
+ onTrailingIconClick(event: MouseEvent | Event): void {
127
+ event.stopPropagation();
128
+ this.trailingIconClick.emit(event);
129
+ }
130
+
131
+ openPicker(): void {
132
+ if (this.picker && !this.picker.opened) {
133
+ this.picker.open();
134
+ }
135
+ }
136
+
137
+ closePicker(): void {
138
+ if (this.picker?.opened) {
139
+ this.picker.close();
140
+ }
141
+ }
142
+
143
+ clearValue(): void {
144
+ this.value = "";
145
+ this.onChange(null);
146
+ this.onTouched();
147
+ this.valueChange.emit("");
148
+ }
149
+
150
+ writeValue(value: string | null | undefined): void {
151
+ this.value = value || "";
152
+ }
153
+
154
+ registerOnChange(fn: (value: any) => void): void {
155
+ this.onChange = fn;
156
+ }
157
+
158
+ registerOnTouched(fn: () => void): void {
159
+ this.onTouched = fn;
160
+ }
161
+
162
+ setDisabledState(isDisabled: boolean): void {
163
+ this.isDisabled = isDisabled;
164
+ }
165
+ }
@@ -0,0 +1,61 @@
1
+ /* Add these focus states to your existing CSS */
2
+
3
+ .dropdown-header:focus {
4
+ outline: 2px solid var(--primary);
5
+ outline-offset: 2px;
6
+ }
7
+
8
+ .dropdown-header:focus:not(:focus-visible) {
9
+ outline: none;
10
+ }
11
+
12
+ .dropdown-search input:focus {
13
+ outline: 2px solid var(--primary);
14
+ outline-offset: 2px;
15
+ }
16
+
17
+ .dropdown-item:focus {
18
+ outline: 2px solid var(--primary);
19
+ outline-offset: -2px;
20
+ }
21
+
22
+ /* Enhanced focused state */
23
+ .dropdown-item.focused {
24
+ background-color: var(--hover-drop-down);
25
+ font-weight: bold;
26
+ }
27
+
28
+ /* Ensure proper contrast for disabled state */
29
+ .dropdown-container.disabled {
30
+ opacity: 0.6;
31
+ }
32
+
33
+ /* Scrollbar improvements for better UX */
34
+ .dropdown-list::-webkit-scrollbar {
35
+ width: 8px;
36
+ }
37
+
38
+ .dropdown-list::-webkit-scrollbar-track {
39
+ background: rgba(0, 0, 0, 0.05);
40
+ border-radius: 4px;
41
+ }
42
+
43
+ .dropdown-list::-webkit-scrollbar-thumb {
44
+ background: rgba(0, 0, 0, 0.2);
45
+ border-radius: 4px;
46
+ }
47
+
48
+ .dropdown-list::-webkit-scrollbar-thumb:hover {
49
+ background: rgba(0, 0, 0, 0.3);
50
+ }
51
+
52
+ /* Ensure dropdown doesn't go off-screen on mobile */
53
+ @media screen and (max-width: 768px) {
54
+ .dropdown-list {
55
+ max-height: 180px;
56
+ }
57
+
58
+ .dropdown-list.drop-up {
59
+ bottom: 45px;
60
+ }
61
+ }
@@ -0,0 +1,96 @@
1
+ <div
2
+ class="dropdown-container"
3
+ role="combobox"
4
+ [ngClass]="[
5
+ dropDownClass,
6
+ isDisabled ? 'disabled' : '',
7
+ control?.touched && control?.invalid ? 'invalid' : '',
8
+ (control?.touched && control?.valid && selectedItem && isFocused && !removeValidBorder) ? 'valid' : ''
9
+ ]"
10
+ [ngStyle]="styleDropDown"
11
+ [attr.aria-expanded]="isOpen"
12
+ [attr.aria-haspopup]="'listbox'"
13
+ [attr.aria-controls]="isOpen ? ('dropdown-list-' + dataCy) : null"
14
+ [attr.aria-labelledby]="ariaLabelledBy || null"
15
+ [attr.aria-label]="ariaLabel || null"
16
+ [attr.aria-disabled]="isDisabled"
17
+ [attr.aria-activedescendant]="activeDescendant"
18
+ [attr.data-cy]="dataCy">
19
+
20
+ <div
21
+ class="dropdown-header"
22
+ matTooltipPosition="below"
23
+ matTooltipClass="custom-arrow-tooltip"
24
+ tabindex="0"
25
+ role="button"
26
+ [matTooltip]="tooltip"
27
+ [attr.aria-disabled]="isDisabled"
28
+ (click)="!isDisabled && toggleDropdown()"
29
+ (keydown.enter)="!isDisabled && toggleDropdown()"
30
+ (keydown.space)="!isDisabled && toggleDropdown(); $event.preventDefault()"
31
+ (keydown.arrowdown)="!isDisabled && onArrowDown($event)"
32
+ (keydown.arrowup)="!isDisabled && onArrowUp($event)"
33
+ (keydown.escape)="!isDisabled && closeDropdown()">
34
+ <span id="dropdown-label-{{dataCy}}">{{ selectedItem?.nome ?? textHeader }}</span>
35
+ <mat-icon aria-hidden="true">{{ isOpen ? 'expand_less' : 'expand_more' }}</mat-icon>
36
+ </div>
37
+
38
+ @if (isOpen) {
39
+ <ul
40
+ class="dropdown-list"
41
+ role="listbox"
42
+ [id]="'dropdown-list-' + dataCy"
43
+ [ngClass]="{ 'drop-up': dropDirection === 'up', 'dropdown-secundary': dropDownClass === 'dropdown-secundary' }"
44
+ [attr.aria-label]="listAriaLabel || 'Opções'"
45
+ [attr.aria-labelledby]="'dropdown-label-' + dataCy">
46
+
47
+
48
+ <li class="dropdown-search" role="none">
49
+ <input
50
+ type="text"
51
+ placeholder="Buscar por código ou nome..."
52
+ role="searchbox"
53
+ aria-label="Buscar opções"
54
+ [formControl]="searchControl"
55
+ (keydown.arrowdown)="onArrowDown($event)"
56
+ (keydown.arrowup)="onArrowUp($event)"
57
+ (keydown.enter)="onSearchEnter($event)"
58
+ (keydown.escape)="closeDropdown()" />
59
+ </li>
60
+
61
+
62
+ @for (item of filteredOptions; track $index) {
63
+ @if(item && item.nome) {
64
+ <li
65
+ class="dropdown-item"
66
+ role="option"
67
+ [ngClass]="{
68
+ 'icon-right': item.iconPosition === 'right',
69
+ 'selected': item === selectedItem,
70
+ 'focused': $index === focusedIndex
71
+ }"
72
+ [attr.id]="'option-' + dataCy + '-' + $index"
73
+ [attr.aria-selected]="item === selectedItem"
74
+ [attr.tabindex]="$index === focusedIndex ? 0 : -1"
75
+ [attr.data-cy]="item.dataCy"
76
+ (click)="selectItem(item)"
77
+ (keydown.enter)="selectItem(item)"
78
+ (keydown.space)="selectItem(item); $event.preventDefault()"
79
+ (mouseenter)="focusedIndex = $index">
80
+ @if (item.icon && item.iconPosition !== 'right') {
81
+ <mat-icon class="dropdown-icon" aria-hidden="true">{{ item.icon }}</mat-icon>
82
+ }
83
+ <span>{{ item.nome }}</span>
84
+ @if (item.icon && item.iconPosition === 'right') {
85
+ <mat-icon class="dropdown-icon" aria-hidden="true">{{ item.icon }}</mat-icon>
86
+ }
87
+ </li>
88
+ }
89
+ }
90
+ </ul>
91
+ }
92
+
93
+ @if (control?.touched && control?.invalid && errorText) {
94
+ <div class="message-error">{{ errorText }}</div>
95
+ }
96
+ </div>