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.
- package/README.md +103 -0
- package/cli/cao-comp.cjs +186 -0
- package/fesm2022/ngx-opalbytes-components.mjs +23 -7
- package/fesm2022/ngx-opalbytes-components.mjs.map +1 -1
- package/package.json +4 -1
- package/src/lib/shared/components/autocomplete/autocomplete.component.html +126 -0
- package/src/lib/shared/components/autocomplete/autocomplete.component.scss +150 -0
- package/src/lib/shared/components/autocomplete/autocomplete.component.ts +552 -0
- package/src/lib/shared/components/base-button/base-button.component.html +33 -0
- package/src/lib/shared/components/base-button/base-button.component.scss +14 -0
- package/src/lib/shared/components/base-button/base-button.component.spec.ts +64 -0
- package/src/lib/shared/components/base-button/base-button.component.ts +48 -0
- package/src/lib/shared/components/base-icon/base-icon.css +0 -0
- package/src/lib/shared/components/base-icon/base-icon.html +11 -0
- package/src/lib/shared/components/base-icon/base-icon.spec.ts +23 -0
- package/src/lib/shared/components/base-icon/base-icon.ts +18 -0
- package/src/lib/shared/components/base-input/base-input.css +193 -0
- package/src/lib/shared/components/base-input/base-input.html +43 -0
- package/src/lib/shared/components/base-input/base-input.spec.ts +146 -0
- package/src/lib/shared/components/base-input/base-input.ts +133 -0
- package/src/lib/shared/components/base-time-range/base-time-range.css +27 -0
- package/src/lib/shared/components/base-time-range/base-time-range.html +23 -0
- package/src/lib/shared/components/base-time-range/base-time-range.spec.ts +27 -0
- package/src/lib/shared/components/base-time-range/base-time-range.ts +61 -0
- package/src/lib/shared/components/base-toggle/base-toggle.component.html +28 -0
- package/src/lib/shared/components/base-toggle/base-toggle.component.scss +45 -0
- package/src/lib/shared/components/base-toggle/base-toggle.component.spec.ts +22 -0
- package/src/lib/shared/components/base-toggle/base-toggle.component.ts +138 -0
- package/src/lib/shared/components/card/card.component.css +264 -0
- package/src/lib/shared/components/card/card.component.html +103 -0
- package/src/lib/shared/components/card/card.component.ts +63 -0
- package/src/lib/shared/components/date-picker/date-picker.html +82 -0
- package/src/lib/shared/components/date-picker/date-picker.scss +8 -0
- package/src/lib/shared/components/date-picker/date-picker.spec.ts +23 -0
- package/src/lib/shared/components/date-picker/date-picker.ts +165 -0
- package/src/lib/shared/components/drop-down/drop-down.css +61 -0
- package/src/lib/shared/components/drop-down/drop-down.html +96 -0
- package/src/lib/shared/components/drop-down/drop-down.spec.ts +335 -0
- package/src/lib/shared/components/drop-down/drop-down.ts +335 -0
- package/src/lib/shared/components/footer/footer.css +29 -0
- package/src/lib/shared/components/footer/footer.html +10 -0
- package/src/lib/shared/components/footer/footer.spec.ts +22 -0
- package/src/lib/shared/components/footer/footer.ts +15 -0
- package/src/lib/shared/components/index.ts +19 -0
- package/src/lib/shared/components/kpi/kpi.css +0 -0
- package/src/lib/shared/components/kpi/kpi.html +11 -0
- package/src/lib/shared/components/kpi/kpi.spec.ts +38 -0
- package/src/lib/shared/components/kpi/kpi.ts +25 -0
- package/src/lib/shared/components/links-button/links-button.css +70 -0
- package/src/lib/shared/components/links-button/links-button.html +23 -0
- package/src/lib/shared/components/links-button/links-button.spec.ts +139 -0
- package/src/lib/shared/components/links-button/links-button.ts +45 -0
- package/src/lib/shared/components/paginator/paginator.css +0 -0
- package/src/lib/shared/components/paginator/paginator.html +10 -0
- package/src/lib/shared/components/paginator/paginator.spec.ts +23 -0
- package/src/lib/shared/components/paginator/paginator.ts +25 -0
- package/src/lib/shared/components/stepper/step/step.css +8 -0
- package/src/lib/shared/components/stepper/step/step.html +1 -0
- package/src/lib/shared/components/stepper/step/step.spec.ts +131 -0
- package/src/lib/shared/components/stepper/step/step.ts +14 -0
- package/src/lib/shared/components/stepper/stepper.css +17 -0
- package/src/lib/shared/components/stepper/stepper.html +11 -0
- package/src/lib/shared/components/stepper/stepper.spec.ts +238 -0
- package/src/lib/shared/components/stepper/stepper.ts +57 -0
- package/src/lib/shared/components/tab-group/tab-group.component.html +84 -0
- package/src/lib/shared/components/tab-group/tab-group.component.scss +151 -0
- package/src/lib/shared/components/tab-group/tab-group.spec.ts +22 -0
- package/src/lib/shared/components/tab-group/tab-group.ts +218 -0
- package/src/lib/shared/components/tab-panel/tab-content.directive.ts +7 -0
- package/src/lib/shared/components/tab-panel/tab-label.directive.ts +7 -0
- package/src/lib/shared/components/tab-panel/tab-panel.css +0 -0
- package/src/lib/shared/components/tab-panel/tab-panel.spec.ts +22 -0
- package/src/lib/shared/components/tab-panel/tab-panel.ts +31 -0
- package/src/lib/shared/components/time-picker/time-picker.css +84 -0
- package/src/lib/shared/components/time-picker/time-picker.html +21 -0
- package/src/lib/shared/components/time-picker/time-picker.spec.ts +23 -0
- package/src/lib/shared/components/time-picker/time-picker.ts +90 -0
- package/types/ngx-opalbytes-components.d.ts +23 -4
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
|
2
|
+
import { CaoDropDownComponent } from "./drop-down";
|
|
3
|
+
import { By } from "@angular/platform-browser";
|
|
4
|
+
import { AbstractControl } from "@angular/forms";
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
describe("CaoDropDownComponent", () => {
|
|
8
|
+
let component: CaoDropDownComponent;
|
|
9
|
+
let fixture: ComponentFixture<CaoDropDownComponent>;
|
|
10
|
+
|
|
11
|
+
const mockOptions = [{ nome: "Item 1", icon: "home" }, { nome: "Item 2" }];
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
await TestBed.configureTestingModule({
|
|
15
|
+
imports: [CaoDropDownComponent],
|
|
16
|
+
}).compileComponents();
|
|
17
|
+
|
|
18
|
+
fixture = TestBed.createComponent(CaoDropDownComponent);
|
|
19
|
+
component = fixture.componentInstance;
|
|
20
|
+
component.options = mockOptions;
|
|
21
|
+
component.textHeader = "Selecione";
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
fixture.destroy();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("deve criar o componente", () => {
|
|
29
|
+
fixture.detectChanges();
|
|
30
|
+
expect(component).toBeTruthy();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("deve exibir o texto do cabeçalho quando nenhum item estiver selecionado", () => {
|
|
34
|
+
fixture.detectChanges();
|
|
35
|
+
const header = fixture.debugElement.query(By.css(".dropdown-header span")).nativeElement;
|
|
36
|
+
expect(header.textContent.trim()).toBe("Selecione");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("deve alternar o dropdown ao clicar no cabeçalho", () => {
|
|
40
|
+
fixture.detectChanges();
|
|
41
|
+
const header = fixture.debugElement.query(By.css(".dropdown-header"));
|
|
42
|
+
|
|
43
|
+
// Primeiro clique para abrir
|
|
44
|
+
header.triggerEventHandler("click", new Event('click'));
|
|
45
|
+
fixture.detectChanges();
|
|
46
|
+
|
|
47
|
+
expect(component.isOpen).toBeTruthy();
|
|
48
|
+
|
|
49
|
+
// Verificar se a lista está visível
|
|
50
|
+
const list = fixture.debugElement.query(By.css(".dropdown-list"));
|
|
51
|
+
expect(list).toBeTruthy();
|
|
52
|
+
|
|
53
|
+
// Segundo clique para fechar
|
|
54
|
+
header.triggerEventHandler("click", new Event('click'));
|
|
55
|
+
fixture.detectChanges();
|
|
56
|
+
|
|
57
|
+
expect(component.isOpen).toBeFalsy();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("deve exibir os itens da lista quando o dropdown estiver aberto", () => {
|
|
61
|
+
// Abrir dropdown ANTES do primeiro detectChanges
|
|
62
|
+
component.isOpen = true;
|
|
63
|
+
component["checkAvailableSpace"]();
|
|
64
|
+
|
|
65
|
+
fixture.detectChanges();
|
|
66
|
+
|
|
67
|
+
const items = fixture.debugElement.queryAll(By.css(".dropdown-item"));
|
|
68
|
+
expect(items.length).toBe(mockOptions.length);
|
|
69
|
+
expect(items[0].nativeElement.textContent).toContain("Item 1");
|
|
70
|
+
expect(items[1].nativeElement.textContent).toContain("Item 2");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("deve exibir o ícone quando o item possuir um", () => {
|
|
74
|
+
// Abrir dropdown ANTES do primeiro detectChanges
|
|
75
|
+
component.isOpen = true;
|
|
76
|
+
component["checkAvailableSpace"]();
|
|
77
|
+
|
|
78
|
+
fixture.detectChanges();
|
|
79
|
+
|
|
80
|
+
const icons = fixture.debugElement.queryAll(By.css(".dropdown-icon"));
|
|
81
|
+
// Apenas o primeiro item tem ícone
|
|
82
|
+
expect(icons.length).toBe(1);
|
|
83
|
+
expect(icons[0].nativeElement.textContent.trim()).toBe("home");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("deve selecionar um item e fechar o dropdown", () => {
|
|
87
|
+
const emitSpy = vi.spyOn(component.itemSelected, "emit");
|
|
88
|
+
const markAsTouchedSpy = vi.spyOn(component as any, "markAsTouched");
|
|
89
|
+
|
|
90
|
+
// Abrir dropdown ANTES do primeiro detectChanges
|
|
91
|
+
component.isOpen = true;
|
|
92
|
+
component["checkAvailableSpace"]();
|
|
93
|
+
|
|
94
|
+
fixture.detectChanges();
|
|
95
|
+
|
|
96
|
+
const items = fixture.debugElement.queryAll(By.css(".dropdown-item"));
|
|
97
|
+
items[0].triggerEventHandler("click", new Event('click'));
|
|
98
|
+
fixture.detectChanges();
|
|
99
|
+
|
|
100
|
+
expect(component.selectedItem).toEqual(mockOptions[0]);
|
|
101
|
+
expect(component.isOpen).toBeFalsy();
|
|
102
|
+
expect(emitSpy).toHaveBeenCalledWith(mockOptions[0]);
|
|
103
|
+
expect(markAsTouchedSpy).toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("deve exibir o texto do item selecionado no cabeçalho", () => {
|
|
107
|
+
// Configurar o estado ANTES da primeira detecção
|
|
108
|
+
component.selectedItem = mockOptions[1];
|
|
109
|
+
|
|
110
|
+
// Agora detectar mudanças
|
|
111
|
+
fixture.detectChanges();
|
|
112
|
+
|
|
113
|
+
const header = fixture.debugElement.query(By.css(".dropdown-header span")).nativeElement;
|
|
114
|
+
expect(header.textContent.trim()).toBe("Item 2");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("deve fechar o dropdown ao clicar fora do componente", () => {
|
|
118
|
+
// Abrir dropdown ANTES do primeiro detectChanges
|
|
119
|
+
component.isOpen = true;
|
|
120
|
+
component["checkAvailableSpace"]();
|
|
121
|
+
|
|
122
|
+
fixture.detectChanges();
|
|
123
|
+
|
|
124
|
+
// Simular clique fora
|
|
125
|
+
const clickEvent = new MouseEvent('click');
|
|
126
|
+
document.dispatchEvent(clickEvent);
|
|
127
|
+
|
|
128
|
+
// Angular precisa processar o evento assíncrono
|
|
129
|
+
fixture.detectChanges();
|
|
130
|
+
|
|
131
|
+
expect(component.isOpen).toBeFalsy();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("não deve fechar o dropdown se o clique for dentro do componente", () => {
|
|
135
|
+
// Abrir dropdown ANTES do primeiro detectChanges
|
|
136
|
+
component.isOpen = true;
|
|
137
|
+
component["checkAvailableSpace"]();
|
|
138
|
+
|
|
139
|
+
fixture.detectChanges();
|
|
140
|
+
|
|
141
|
+
// Simular clique no container do dropdown
|
|
142
|
+
const container = fixture.debugElement.query(By.css(".dropdown-container"));
|
|
143
|
+
container.triggerEventHandler("click", new Event('click', { bubbles: true }));
|
|
144
|
+
fixture.detectChanges();
|
|
145
|
+
|
|
146
|
+
expect(component.isOpen).toBeTruthy();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("deve setar o valor corretamente com writeValue para item existente", () => {
|
|
150
|
+
fixture.detectChanges();
|
|
151
|
+
|
|
152
|
+
component.writeValue("Item 2");
|
|
153
|
+
fixture.detectChanges();
|
|
154
|
+
|
|
155
|
+
expect(component.selectedItem).toEqual(mockOptions[1]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("não deve alterar selectedItem com writeValue para item inexistente", () => {
|
|
159
|
+
fixture.detectChanges();
|
|
160
|
+
|
|
161
|
+
component.selectedItem = mockOptions[0];
|
|
162
|
+
component.writeValue("Item inexistente");
|
|
163
|
+
fixture.detectChanges();
|
|
164
|
+
|
|
165
|
+
expect(component.selectedItem).toEqual(mockOptions[0]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("deve chamar onChange ao selecionar um item", () => {
|
|
169
|
+
fixture.detectChanges();
|
|
170
|
+
|
|
171
|
+
const onChangeSpy = vi.fn();
|
|
172
|
+
component.registerOnChange(onChangeSpy);
|
|
173
|
+
|
|
174
|
+
component.selectItem(mockOptions[0]);
|
|
175
|
+
|
|
176
|
+
// O componente está passando o objeto completo, não apenas o nome
|
|
177
|
+
expect(onChangeSpy).toHaveBeenCalledWith(mockOptions[0]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("deve chamar onTouched ao marcar como touched", () => {
|
|
181
|
+
fixture.detectChanges();
|
|
182
|
+
|
|
183
|
+
const onTouchedSpy = vi.fn();
|
|
184
|
+
component.registerOnTouched(onTouchedSpy);
|
|
185
|
+
|
|
186
|
+
// Chamar toggleDropdown para disparar touched
|
|
187
|
+
component.toggleDropdown();
|
|
188
|
+
|
|
189
|
+
expect(onTouchedSpy).toHaveBeenCalled();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("deve retornar o nome do controle através do getter", () => {
|
|
193
|
+
fixture.detectChanges();
|
|
194
|
+
|
|
195
|
+
component.controlName = "dropdown";
|
|
196
|
+
expect(component.controlNameValue).toBe("dropdown");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("deve retornar erro de campo obrigatório se control estiver touched e vazio", () => {
|
|
200
|
+
fixture.detectChanges();
|
|
201
|
+
|
|
202
|
+
const fakeControl = {
|
|
203
|
+
touched: true,
|
|
204
|
+
dirty: false,
|
|
205
|
+
hasError: (err: string) => err === "required",
|
|
206
|
+
get: () => null,
|
|
207
|
+
} as unknown as AbstractControl;
|
|
208
|
+
|
|
209
|
+
component.control = fakeControl;
|
|
210
|
+
component.selectedItem = undefined;
|
|
211
|
+
fixture.detectChanges();
|
|
212
|
+
|
|
213
|
+
expect(component.errorText).toBe("*Campo obrigatório");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("deve retornar null no errorText se não houver erro", () => {
|
|
217
|
+
// Configurar ANTES de detectChanges
|
|
218
|
+
component.selectedItem = mockOptions[0];
|
|
219
|
+
|
|
220
|
+
const fakeControl = {
|
|
221
|
+
touched: false,
|
|
222
|
+
hasError: () => false,
|
|
223
|
+
get: () => null,
|
|
224
|
+
} as unknown as AbstractControl;
|
|
225
|
+
|
|
226
|
+
component.control = fakeControl;
|
|
227
|
+
|
|
228
|
+
fixture.detectChanges();
|
|
229
|
+
|
|
230
|
+
expect(component.errorText).toBeNull();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("markAsTouched deve chamar onTouched e marcar controle como touched", () => {
|
|
234
|
+
fixture.detectChanges();
|
|
235
|
+
|
|
236
|
+
component.isTouched = false;
|
|
237
|
+
|
|
238
|
+
// Criar um spy para markAsTouched
|
|
239
|
+
const markAsTouchedSpy = vi.fn();
|
|
240
|
+
component.control = {
|
|
241
|
+
markAsTouched: markAsTouchedSpy,
|
|
242
|
+
} as any;
|
|
243
|
+
|
|
244
|
+
const onTouchedSpy = vi.fn();
|
|
245
|
+
component.registerOnTouched(onTouchedSpy);
|
|
246
|
+
|
|
247
|
+
component["markAsTouched"]();
|
|
248
|
+
|
|
249
|
+
expect(onTouchedSpy).toHaveBeenCalled();
|
|
250
|
+
expect(markAsTouchedSpy).toHaveBeenCalled();
|
|
251
|
+
expect(component.isTouched).toBeTruthy();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("markAsTouched não deve chamar onTouched se já estiver tocado", () => {
|
|
255
|
+
fixture.detectChanges();
|
|
256
|
+
|
|
257
|
+
component.isTouched = true;
|
|
258
|
+
const onTouchedSpy = vi.fn();
|
|
259
|
+
component.registerOnTouched(onTouchedSpy);
|
|
260
|
+
|
|
261
|
+
component["markAsTouched"]();
|
|
262
|
+
|
|
263
|
+
expect(onTouchedSpy).not.toHaveBeenCalled();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe("checkAvailableSpace", () => {
|
|
267
|
+
it('deve definir dropDirection como "up" se dropDirectionInput for "up"', () => {
|
|
268
|
+
fixture.detectChanges();
|
|
269
|
+
|
|
270
|
+
component.dropDirectionInput = "up";
|
|
271
|
+
component["checkAvailableSpace"]();
|
|
272
|
+
expect(component.dropDirection).toBe("up");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('deve definir dropDirection como "up" se não houver espaço suficiente abaixo', () => {
|
|
276
|
+
fixture.detectChanges();
|
|
277
|
+
|
|
278
|
+
component.dropDirectionInput = "down";
|
|
279
|
+
|
|
280
|
+
// Mock do getBoundingClientRect
|
|
281
|
+
const mockRect = {
|
|
282
|
+
bottom: window.innerHeight - 100,
|
|
283
|
+
top: 0,
|
|
284
|
+
left: 0,
|
|
285
|
+
right: 0,
|
|
286
|
+
height: 50,
|
|
287
|
+
width: 200,
|
|
288
|
+
x: 0,
|
|
289
|
+
y: 0,
|
|
290
|
+
toJSON: () => ({})
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// Mock do elemento
|
|
294
|
+
const mockElement = {
|
|
295
|
+
getBoundingClientRect: vi.fn(() => mockRect)
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// Substituir o elemento nativo
|
|
299
|
+
component["_eref"].nativeElement = mockElement;
|
|
300
|
+
|
|
301
|
+
component["checkAvailableSpace"]();
|
|
302
|
+
expect(component.dropDirection).toBe("up");
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('deve definir dropDirection como "down" se houver espaço suficiente abaixo', () => {
|
|
306
|
+
fixture.detectChanges();
|
|
307
|
+
|
|
308
|
+
component.dropDirectionInput = "down";
|
|
309
|
+
|
|
310
|
+
// Mock do getBoundingClientRect
|
|
311
|
+
const mockRect = {
|
|
312
|
+
bottom: 100,
|
|
313
|
+
top: 0,
|
|
314
|
+
left: 0,
|
|
315
|
+
right: 0,
|
|
316
|
+
height: 50,
|
|
317
|
+
width: 200,
|
|
318
|
+
x: 0,
|
|
319
|
+
y: 0,
|
|
320
|
+
toJSON: () => ({})
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Mock do elemento
|
|
324
|
+
const mockElement = {
|
|
325
|
+
getBoundingClientRect: vi.fn(() => mockRect)
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Substituir o elemento nativo
|
|
329
|
+
component["_eref"].nativeElement = mockElement;
|
|
330
|
+
|
|
331
|
+
component["checkAvailableSpace"]();
|
|
332
|
+
expect(component.dropDirection).toBe("down");
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
});
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { CommonModule } from "@angular/common";
|
|
2
|
+
import {
|
|
3
|
+
Component,
|
|
4
|
+
ElementRef,
|
|
5
|
+
EventEmitter,
|
|
6
|
+
Input,
|
|
7
|
+
Output,
|
|
8
|
+
ViewEncapsulation,
|
|
9
|
+
forwardRef,
|
|
10
|
+
OnInit,
|
|
11
|
+
OnDestroy,
|
|
12
|
+
ChangeDetectorRef,
|
|
13
|
+
} from "@angular/core";
|
|
14
|
+
import {
|
|
15
|
+
AbstractControl,
|
|
16
|
+
ControlValueAccessor,
|
|
17
|
+
NG_VALUE_ACCESSOR,
|
|
18
|
+
ReactiveFormsModule,
|
|
19
|
+
FormControl,
|
|
20
|
+
} from "@angular/forms";
|
|
21
|
+
import { MatIconModule } from "@angular/material/icon";
|
|
22
|
+
import { MatTooltipModule } from "@angular/material/tooltip";
|
|
23
|
+
|
|
24
|
+
export interface ICaoDropDownOption {
|
|
25
|
+
id?: string | number;
|
|
26
|
+
nome: string;
|
|
27
|
+
icon?: string;
|
|
28
|
+
iconPosition?: string;
|
|
29
|
+
dataCy?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Component({
|
|
33
|
+
selector: "cao-drop-down",
|
|
34
|
+
templateUrl: "./drop-down.html",
|
|
35
|
+
styleUrl: "./drop-down.css",
|
|
36
|
+
encapsulation: ViewEncapsulation.None,
|
|
37
|
+
imports: [
|
|
38
|
+
MatIconModule,
|
|
39
|
+
CommonModule,
|
|
40
|
+
MatTooltipModule,
|
|
41
|
+
ReactiveFormsModule,
|
|
42
|
+
],
|
|
43
|
+
providers: [
|
|
44
|
+
{
|
|
45
|
+
provide: NG_VALUE_ACCESSOR,
|
|
46
|
+
useExisting: forwardRef(() => CaoDropDownComponent),
|
|
47
|
+
multi: true,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
})
|
|
51
|
+
export class CaoDropDownComponent implements ControlValueAccessor, OnInit, OnDestroy {
|
|
52
|
+
@Input() textHeader = "SELECIONE UM ITEM";
|
|
53
|
+
@Input() options: ICaoDropDownOption[] = [];
|
|
54
|
+
@Input() dropDownClass = "";
|
|
55
|
+
@Input() styleDropDown: { [klass: string]: unknown } = {};
|
|
56
|
+
@Input() control?: AbstractControl | null;
|
|
57
|
+
@Input() controlName = "";
|
|
58
|
+
@Input() isDisabled = false;
|
|
59
|
+
@Input() tooltip = "";
|
|
60
|
+
@Input() dropDirectionInput: "up" | "down" = "down";
|
|
61
|
+
@Input() dataCy?: string = "";
|
|
62
|
+
|
|
63
|
+
@Input() trackByFn?: (index: number, item: ICaoDropDownOption) => unknown;
|
|
64
|
+
@Input() compareWith?: (option: unknown, value: unknown) => boolean;
|
|
65
|
+
@Input() ariaLabel?: string;
|
|
66
|
+
@Input() ariaLabelledBy?: string;
|
|
67
|
+
@Input() listAriaLabel?: string;
|
|
68
|
+
|
|
69
|
+
@Output() readonly itemSelected = new EventEmitter<ICaoDropDownOption>();
|
|
70
|
+
|
|
71
|
+
isOpen = false;
|
|
72
|
+
selectedItem?: ICaoDropDownOption;
|
|
73
|
+
filteredOptions: ICaoDropDownOption[] = [];
|
|
74
|
+
searchControl = new FormControl("");
|
|
75
|
+
isTouched = false;
|
|
76
|
+
isFocused = false;
|
|
77
|
+
dropDirection: string | undefined;
|
|
78
|
+
removeValidBorder = false;
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
focusedIndex = -1;
|
|
82
|
+
|
|
83
|
+
private _handleClickOutside = this.onClickOutside.bind(this);
|
|
84
|
+
|
|
85
|
+
constructor(
|
|
86
|
+
private _eref: ElementRef,
|
|
87
|
+
private cdr: ChangeDetectorRef
|
|
88
|
+
) { }
|
|
89
|
+
|
|
90
|
+
ngOnInit(): void {
|
|
91
|
+
document.addEventListener("click", this._handleClickOutside, true);
|
|
92
|
+
this.filteredOptions = [...this.options];
|
|
93
|
+
|
|
94
|
+
this.searchControl.valueChanges.subscribe((value) => {
|
|
95
|
+
this.filterOptions(value || "");
|
|
96
|
+
this.focusedIndex = -1
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
ngOnDestroy(): void {
|
|
101
|
+
document.removeEventListener("click", this._handleClickOutside, true);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get activeDescendant(): string | null {
|
|
105
|
+
if (this.isOpen && this.focusedIndex >= 0 && this.filteredOptions[this.focusedIndex]) {
|
|
106
|
+
return `option-${this.dataCy}-${this.focusedIndex}`;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
toggleDropdown(): void {
|
|
112
|
+
if (this.isDisabled) { return };
|
|
113
|
+
|
|
114
|
+
this.checkAvailableSpace();
|
|
115
|
+
this.isOpen = !this.isOpen;
|
|
116
|
+
this.isFocused = this.isOpen;
|
|
117
|
+
this.markAsTouched();
|
|
118
|
+
|
|
119
|
+
if (this.isOpen) {
|
|
120
|
+
this.searchControl.setValue("");
|
|
121
|
+
this.filteredOptions = [...this.options];
|
|
122
|
+
this.removeValidBorder = false;
|
|
123
|
+
this.focusedIndex = -1;
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
setTimeout(() => {
|
|
127
|
+
if (this.isOpen) {
|
|
128
|
+
const searchInput = this._eref.nativeElement.querySelector('.dropdown-search input');
|
|
129
|
+
if (searchInput) {
|
|
130
|
+
searchInput.focus();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
} else {
|
|
135
|
+
this.focusedIndex = -1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.cdr.detectChanges();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
selectItem(option: ICaoDropDownOption): void {
|
|
142
|
+
if (this.isDisabled) { return };
|
|
143
|
+
|
|
144
|
+
this.selectedItem = option;
|
|
145
|
+
this.isOpen = false;
|
|
146
|
+
this.focusedIndex = -1;
|
|
147
|
+
this.itemSelected.emit(option);
|
|
148
|
+
this.onChange(option);
|
|
149
|
+
this.markAsTouched();
|
|
150
|
+
this.cdr.detectChanges();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
clear(): void {
|
|
154
|
+
this.selectedItem = undefined;
|
|
155
|
+
this.onChange(null);
|
|
156
|
+
this.cdr.detectChanges();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
filterOptions(searchTerm: string) {
|
|
160
|
+
if (!searchTerm) {
|
|
161
|
+
this.filteredOptions = [...this.options];
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const lowerCaseSearchTerm = searchTerm.toLowerCase();
|
|
165
|
+
this.filteredOptions = this.options.filter(
|
|
166
|
+
(option) =>
|
|
167
|
+
option.nome.toLowerCase().includes(lowerCaseSearchTerm) ||
|
|
168
|
+
option.id?.toString().toLowerCase().includes(lowerCaseSearchTerm)
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
trackBy(index: number, item: ICaoDropDownOption): unknown {
|
|
173
|
+
return this.trackByFn ? this.trackByFn(index, item) : item.id;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private onClickOutside(event: MouseEvent): void {
|
|
177
|
+
if (!this._eref.nativeElement.contains(event.target)) {
|
|
178
|
+
if (this.isOpen) {
|
|
179
|
+
this.isOpen = false;
|
|
180
|
+
}
|
|
181
|
+
if (this.isFocused) {
|
|
182
|
+
this.isFocused = false;
|
|
183
|
+
}
|
|
184
|
+
this.focusedIndex = -1;
|
|
185
|
+
this.markAsTouched();
|
|
186
|
+
this.removeValidBorder = true;
|
|
187
|
+
this.cdr.detectChanges();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
onArrowDown(event: Event): void {
|
|
192
|
+
if (this.isDisabled) { return };
|
|
193
|
+
|
|
194
|
+
event.preventDefault();
|
|
195
|
+
event.stopPropagation();
|
|
196
|
+
|
|
197
|
+
if (this.filteredOptions.length === 0) { return };
|
|
198
|
+
|
|
199
|
+
if (this.focusedIndex === -1) {
|
|
200
|
+
|
|
201
|
+
this.focusedIndex = 0;
|
|
202
|
+
} else {
|
|
203
|
+
|
|
204
|
+
this.focusedIndex = (this.focusedIndex + 1) % this.filteredOptions.length;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this.scrollIntoView();
|
|
208
|
+
this.cdr.detectChanges();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
onArrowUp(event: Event): void {
|
|
212
|
+
if (this.isDisabled) { return };
|
|
213
|
+
|
|
214
|
+
event.preventDefault();
|
|
215
|
+
event.stopPropagation();
|
|
216
|
+
|
|
217
|
+
if (this.filteredOptions.length === 0) { return };
|
|
218
|
+
|
|
219
|
+
if (this.focusedIndex === -1) {
|
|
220
|
+
|
|
221
|
+
this.focusedIndex = this.filteredOptions.length - 1;
|
|
222
|
+
} else {
|
|
223
|
+
|
|
224
|
+
this.focusedIndex = this.focusedIndex <= 0
|
|
225
|
+
? this.filteredOptions.length - 1
|
|
226
|
+
: this.focusedIndex - 1;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this.scrollIntoView();
|
|
230
|
+
this.cdr.detectChanges();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
onSearchEnter(event: Event): void {
|
|
234
|
+
event.preventDefault();
|
|
235
|
+
event.stopPropagation();
|
|
236
|
+
|
|
237
|
+
if (this.focusedIndex >= 0 && this.focusedIndex < this.filteredOptions.length) {
|
|
238
|
+
this.selectItem(this.filteredOptions[this.focusedIndex]);
|
|
239
|
+
} else if (this.filteredOptions.length > 0) {
|
|
240
|
+
|
|
241
|
+
this.selectItem(this.filteredOptions[0]);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
closeDropdown(): void {
|
|
246
|
+
if (this.isDisabled) { return };
|
|
247
|
+
|
|
248
|
+
this.isOpen = false;
|
|
249
|
+
this.focusedIndex = -1;
|
|
250
|
+
this.isFocused = false;
|
|
251
|
+
this.cdr.detectChanges();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private scrollIntoView(): void {
|
|
255
|
+
if (!this.isOpen || this.focusedIndex === -1) { return };
|
|
256
|
+
|
|
257
|
+
setTimeout(() => {
|
|
258
|
+
const element = document.getElementById(`option-${this.dataCy}-${this.focusedIndex}`);
|
|
259
|
+
if (element) {
|
|
260
|
+
element.scrollIntoView({
|
|
261
|
+
block: 'nearest',
|
|
262
|
+
behavior: 'smooth'
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}, 0);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
checkAvailableSpace(): void {
|
|
269
|
+
if (this.dropDirectionInput === "up") {
|
|
270
|
+
this.dropDirection = "up";
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const rect = this._eref.nativeElement.getBoundingClientRect();
|
|
275
|
+
const windowHeight = window.innerHeight;
|
|
276
|
+
const dropdownHeight = 200
|
|
277
|
+
|
|
278
|
+
const spaceBelow = windowHeight - rect.bottom;
|
|
279
|
+
this.dropDirection = spaceBelow < dropdownHeight ? "up" : "down";
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
writeValue(value: unknown): void {
|
|
284
|
+
if (value !== null && value !== undefined) {
|
|
285
|
+
const match = this.options.find((opt) =>
|
|
286
|
+
this.compareWith
|
|
287
|
+
? this.compareWith(opt, value)
|
|
288
|
+
: opt.id === (typeof value === "object" && value ? (value as any).id : value)
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
if (match) {
|
|
292
|
+
this.selectedItem = match;
|
|
293
|
+
} else {
|
|
294
|
+
const matchByName = this.options.find((opt) => opt.nome === value);
|
|
295
|
+
if (matchByName) {
|
|
296
|
+
this.selectedItem = matchByName;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
this.selectedItem = undefined;
|
|
301
|
+
}
|
|
302
|
+
this.cdr.detectChanges();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
registerOnChange(fn: (value: unknown) => void): void {
|
|
306
|
+
this.onChange = fn;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
registerOnTouched(fn: () => void): void {
|
|
310
|
+
this.onTouched = fn;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
onChange: (value: unknown) => void = () => { };
|
|
314
|
+
onTouched = () => { };
|
|
315
|
+
|
|
316
|
+
get errorText(): string | null {
|
|
317
|
+
if (!this.control) { return null; }
|
|
318
|
+
if (this.control.hasError("required")) {
|
|
319
|
+
return "*Campo obrigatório";
|
|
320
|
+
}
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private markAsTouched() {
|
|
325
|
+
if (!this.isTouched) {
|
|
326
|
+
this.onTouched();
|
|
327
|
+
this.isTouched = true;
|
|
328
|
+
this.control?.markAsTouched();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
get controlNameValue(): string {
|
|
333
|
+
return this.controlName;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
* {
|
|
2
|
+
list-style: none;
|
|
3
|
+
text-decoration: none;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.footer-component{
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: end;
|
|
9
|
+
justify-content: end;
|
|
10
|
+
height: 35px;
|
|
11
|
+
padding: 0 50px 10px 0;
|
|
12
|
+
position: fixed;
|
|
13
|
+
bottom: 0;
|
|
14
|
+
right: 0;
|
|
15
|
+
width: 100%;
|
|
16
|
+
z-index: 100;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.footer-component .flex{
|
|
20
|
+
display: flex;
|
|
21
|
+
|
|
22
|
+
}
|
|
23
|
+
.footer-component.isFixed{
|
|
24
|
+
position: fixed;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.footer-component.col{
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
}
|