From 46924ebdf90c3ad8640450e6757d4cb5925373a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor?= Date: Thu, 17 Aug 2023 19:57:52 -0300 Subject: [PATCH] feat(combo): implementa uso do listbox implementa o uso do listbox no componente `po-combo` Fixes DTHFUI-7319 --- .../po-combo-literals-default.interface.ts | 20 + .../interfaces/po-combo-literals.interface.ts | 3 + .../po-combo/po-combo-base.component.spec.ts | 6 +- .../po-combo/po-combo-base.component.ts | 21 +- .../po-field/po-combo/po-combo.component.html | 90 +- .../po-combo/po-combo.component.spec.ts | 839 +----------------- .../po-field/po-combo/po-combo.component.ts | 210 +---- .../po-multiselect-dropdown.component.html | 2 +- .../enums/po-item-list-filter-mode.enum.ts | 8 + .../po-item-list-base.component.ts | 17 +- .../po-item-list/po-item-list.component.html | 17 +- .../po-item-list.component.spec.ts | 191 +++- .../po-item-list/po-item-list.component.ts | 76 +- .../po-listbox-base.component.spec.ts | 18 + .../po-listbox/po-listbox-base.component.ts | 36 +- .../po-listbox/po-listbox.component.html | 148 +-- .../po-listbox/po-listbox.component.spec.ts | 218 ++++- .../po-listbox/po-listbox.component.ts | 94 +- .../po-menu/po-menu.component.spec.ts | 1 - .../po-modal-footer.component.spec.ts | 4 - 20 files changed, 825 insertions(+), 1194 deletions(-) create mode 100644 projects/ui/src/lib/components/po-field/po-combo/interfaces/po-combo-literals-default.interface.ts create mode 100644 projects/ui/src/lib/components/po-listbox/enums/po-item-list-filter-mode.enum.ts diff --git a/projects/ui/src/lib/components/po-field/po-combo/interfaces/po-combo-literals-default.interface.ts b/projects/ui/src/lib/components/po-field/po-combo/interfaces/po-combo-literals-default.interface.ts new file mode 100644 index 0000000000..e2136b4f78 --- /dev/null +++ b/projects/ui/src/lib/components/po-field/po-combo/interfaces/po-combo-literals-default.interface.ts @@ -0,0 +1,20 @@ +import { PoComboLiterals } from './po-combo-literals.interface'; + +export const poComboLiteralsDefault = { + en: { + noData: 'No data found', + chooseOption: 'Choose an option' + }, + es: { + noData: 'Datos no encontrados', + chooseOption: 'Elija una opción' + }, + pt: { + noData: 'Nenhum dado encontrado', + chooseOption: 'Escolha uma opção' + }, + ru: { + noData: 'Данные не найдены', + chooseOption: 'Выберите опцию' + } +}; diff --git a/projects/ui/src/lib/components/po-field/po-combo/interfaces/po-combo-literals.interface.ts b/projects/ui/src/lib/components/po-field/po-combo/interfaces/po-combo-literals.interface.ts index bcd217aa00..d511e0a5e5 100644 --- a/projects/ui/src/lib/components/po-field/po-combo/interfaces/po-combo-literals.interface.ts +++ b/projects/ui/src/lib/components/po-field/po-combo/interfaces/po-combo-literals.interface.ts @@ -8,4 +8,7 @@ export interface PoComboLiterals { /** Texto exibido quando não houver itens na lista ou se, a pesquisa do filtro não retornar nenhum item. */ noData?: string; + + /** Texto exibido quando o combo estiver vazio. */ + chooseOption?: string; } diff --git a/projects/ui/src/lib/components/po-field/po-combo/po-combo-base.component.spec.ts b/projects/ui/src/lib/components/po-field/po-combo/po-combo-base.component.spec.ts index e21a44813b..6a8d695163 100644 --- a/projects/ui/src/lib/components/po-field/po-combo/po-combo-base.component.spec.ts +++ b/projects/ui/src/lib/components/po-field/po-combo/po-combo-base.component.spec.ts @@ -10,10 +10,11 @@ import { expectPropertiesValues, expectSettersMethod } from '../../../util-test/ import { PoLanguageService } from '../../../services/po-language/po-language.service'; import { poLocaleDefault } from '../../../services/po-language/po-language.constant'; -import { PoComboBaseComponent, poComboLiteralsDefault } from './po-combo-base.component'; +import { PoComboBaseComponent } from './po-combo-base.component'; import { PoComboFilter } from './interfaces/po-combo-filter.interface'; import { PoComboFilterMode } from './po-combo-filter-mode.enum'; import { PoComboOption } from './interfaces/po-combo-option.interface'; +import { poComboLiteralsDefault } from './interfaces/po-combo-literals-default.interface'; @Directive() class PoComboTest extends PoComboBaseComponent { @@ -223,8 +224,9 @@ describe('PoComboBaseComponent:', () => { }); it('p-placeholder: should update property p-placeholder with empty value if set with invalid values.', () => { + component['language'] = 'pt'; const invalidValues = [null, undefined, '', 0, false]; - expectPropertiesValues(component, 'placeholder', invalidValues, ''); + expectPropertiesValues(component, 'placeholder', invalidValues, 'Escolha uma opção'); }); }); diff --git a/projects/ui/src/lib/components/po-field/po-combo/po-combo-base.component.ts b/projects/ui/src/lib/components/po-field/po-combo/po-combo-base.component.ts index 6d6fec8a61..018572e11c 100644 --- a/projects/ui/src/lib/components/po-field/po-combo/po-combo-base.component.ts +++ b/projects/ui/src/lib/components/po-field/po-combo/po-combo-base.component.ts @@ -14,26 +14,12 @@ import { PoComboGroup } from './interfaces/po-combo-group.interface'; import { PoComboLiterals } from './interfaces/po-combo-literals.interface'; import { PoComboOption } from './interfaces/po-combo-option.interface'; import { PoComboOptionGroup } from './interfaces/po-combo-option-group.interface'; +import { poComboLiteralsDefault } from './interfaces/po-combo-literals-default.interface'; const PO_COMBO_DEBOUNCE_TIME_DEFAULT = 400; const PO_COMBO_FIELD_LABEL_DEFAULT = 'label'; const PO_COMBO_FIELD_VALUE_DEFAULT = 'value'; -export const poComboLiteralsDefault = { - en: { - noData: 'No data found' - }, - es: { - noData: 'Datos no encontrados' - }, - pt: { - noData: 'Nenhum dado encontrado' - }, - ru: { - noData: 'Данные не найдены' - } -}; - /** * @description * @@ -294,7 +280,6 @@ export abstract class PoComboBaseComponent implements ControlValueAccessor, OnIn private language: string; private _infiniteScrollDistance?: number = 100; private _infiniteScroll?: boolean = false; - private _height?: number; // utilizado para fazer o controle de atualizar o model. // não deve forçar a atualização se o gatilho for o writeValue para não deixar o campo dirty. @@ -304,7 +289,7 @@ export abstract class PoComboBaseComponent implements ControlValueAccessor, OnIn /** Mensagem apresentada enquanto o campo estiver vazio. */ @Input('p-placeholder') set placeholder(value: string) { - this._placeholder = value || ''; + this._placeholder = value || this.literals.chooseOption; } get placeholder() { @@ -623,7 +608,7 @@ export abstract class PoComboBaseComponent implements ControlValueAccessor, OnIn } get isOptionGroupList(): boolean { - return this._options.length && this._options[0].hasOwnProperty('options'); + return this._options.length && this._options.some(item => item.hasOwnProperty('options')); } ngOnInit() { diff --git a/projects/ui/src/lib/components/po-field/po-combo/po-combo.component.html b/projects/ui/src/lib/components/po-field/po-combo/po-combo.component.html index 0f70b3d02b..c918ebaab6 100644 --- a/projects/ui/src/lib/components/po-field/po-combo/po-combo.component.html +++ b/projects/ui/src/lib/components/po-field/po-combo/po-combo.component.html @@ -50,74 +50,28 @@
- -
- -
+
- - - - - - - - - - - -
    -
  • - - - - - - - - - - - - - - - - - - - - - - - - - - -
  • -
-
- - -
-
- - {{ literals.noData }} - -
-
-
diff --git a/projects/ui/src/lib/components/po-field/po-combo/po-combo.component.spec.ts b/projects/ui/src/lib/components/po-field/po-combo/po-combo.component.spec.ts index e2c4ae9589..665e3b2543 100644 --- a/projects/ui/src/lib/components/po-field/po-combo/po-combo.component.spec.ts +++ b/projects/ui/src/lib/components/po-field/po-combo/po-combo.component.spec.ts @@ -14,11 +14,9 @@ import { PoFieldContainerComponent } from '../po-field-container/po-field-contai import { PoIconModule } from '../../po-icon'; import { PoComboComponent } from './po-combo.component'; -import { PoComboFilterMode } from './po-combo-filter-mode.enum'; import { PoComboFilterService } from './po-combo-filter.service'; import { PoComboOption } from './interfaces/po-combo-option.interface'; import { PoCleanComponent } from '../po-clean/po-clean.component'; -import { ComponentFactoryResolver } from '@angular/core'; const eventKeyBoard = document.createEvent('KeyboardEvent'); eventKeyBoard.initEvent('keyup', true, true); @@ -43,7 +41,7 @@ describe('PoComboComponent:', () => { component = fixture.componentInstance; component.label = 'Label de teste'; component.help = 'Help de teste'; - component.poComboBody = fixture.debugElement; + component.poListbox = fixture.debugElement; }); it('should be created', () => { @@ -60,24 +58,6 @@ describe('PoComboComponent:', () => { expect(fixture.debugElement.nativeElement.innerHTML).toContain('Help de teste'); }); - it('should call some functions when typed "tab" and shouldn`t call updateComboList when is service', () => { - const fakeEvent = { - keyCode: 9, - target: { - value: '' - } - }; - component.service = component.defaultService; - - spyOn(component, 'controlComboVisibility'); - spyOn(component, 'verifyValidOption'); - spyOn(component, 'updateComboList'); - component.onKeyDown.call(component, fakeEvent); - expect(component.controlComboVisibility).toHaveBeenCalled(); - expect(component.verifyValidOption).toHaveBeenCalled(); - expect(component.updateComboList).not.toHaveBeenCalled(); - }); - it('should call controlApplyFilter on key up', fakeAsync((): void => { component.service = component.defaultService; @@ -172,72 +152,6 @@ describe('PoComboComponent:', () => { expect(component.isProcessingValueByTab).toBeFalsy(); })); - it('selectPreviousOption: should select a previous value when a selected value already exists', () => { - const previousOption = { value: 1, label: 'Option 1' }; - component.selectedView = { value: 2, label: 'Option 2' }; - - spyOn(component, 'updateSelectedValue'); - spyOn(component, 'getNextOption').and.returnValue(previousOption); - - component.selectPreviousOption(); - - expect(component.updateSelectedValue).toHaveBeenCalledWith(previousOption, true); - expect(component.getNextOption).toHaveBeenCalled(); - }); - - it('should select the last value when not exists a selected value', () => { - component.selectedValue = ''; - component.visibleOptions = [{ label: '1', value: '1' }]; - - spyOn(component, 'updateSelectedValue'); - spyOn(component, 'getNextOption'); - component.selectPreviousOption(); - expect(component.updateSelectedValue).toHaveBeenCalled(); - expect(component.getNextOption).not.toHaveBeenCalled(); - }); - - it('should not update selecting(previous) when not exists selected value and visibleOptions is empty', () => { - component.selectedValue = ''; - component.visibleOptions = []; - - spyOn(component, 'updateSelectedValue'); - component.selectPreviousOption(); - expect(component.updateSelectedValue).not.toHaveBeenCalled(); - }); - - it('selectNextOption: should select a next value when a selected value already exists', () => { - const nextOption = { value: 2, label: 'Option 2' }; - component.selectedView = { value: 1, label: 'Option 1' }; - - spyOn(component, 'updateSelectedValue'); - spyOn(component, 'getNextOption').and.returnValue(nextOption); - - component.selectNextOption(); - - expect(component.updateSelectedValue).toHaveBeenCalledWith(nextOption, true); - expect(component.getNextOption).toHaveBeenCalled(); - }); - - it('should select the first value when not exists a selected value', () => { - component.selectedValue = ''; - component.visibleOptions = [{ label: '1', value: '1' }]; - - spyOn(component, 'updateSelectedValue'); - spyOn(component, 'getNextOption'); - component.selectNextOption(); - expect(component.updateSelectedValue).toHaveBeenCalled(); - expect(component.getNextOption).not.toHaveBeenCalled(); - }); - - it('should not update selecting(next) when not exists selected value and visibleOptions is empty', () => { - component.selectedValue = ''; - component.visibleOptions = []; - - spyOn(component, 'updateSelectedValue'); - component.selectNextOption(); - expect(component.updateSelectedValue).not.toHaveBeenCalled(); - }); - it('should call applyFilter', () => { const fakeThis = { isFirstFilter: true, @@ -333,15 +247,6 @@ describe('PoComboComponent:', () => { expect(component.wasClickedOnToggle).toHaveBeenCalled(); }); - it('should call `checkInfiniteScroll` if infiniteScroll is true', () => { - component.infiniteScroll = true; - - spyOn(component, 'checkInfiniteScroll'); - component['initializeListeners'](); - - expect(component['checkInfiniteScroll']).toHaveBeenCalled(); - }); - it('should hide the combo list when was click out of the input', () => { component.comboOpen = true; @@ -358,11 +263,6 @@ describe('PoComboComponent:', () => { expect(component.controlComboVisibility).not.toHaveBeenCalled(); }); - it('should return a sanitized code', () => { - const html = component.safeHtml('values'); - expect(html['changingThisBreaksApplicationSecurity']).toBe('values'); - }); - it('shouldn`t allow invalid characters to search', () => { expect(component.isValidCharacterToSearch(9)).toBeFalsy(); expect(component.isValidCharacterToSearch(13)).toBeFalsy(); @@ -664,34 +564,6 @@ describe('PoComboComponent:', () => { }); describe('onKeyDown: ', () => { - it('should call `selectPreviousOption` and not call `selectNextOption`', () => { - component.comboOpen = true; - - const event = { ...fakeEvent, keyCode: 38 }; - - const spySelectPreviousOption = spyOn(component, 'selectPreviousOption'); - const spySelectNextOption = spyOn(component, 'selectNextOption'); - - component.onKeyDown(event); - - expect(spySelectNextOption).not.toHaveBeenCalled(); - expect(spySelectPreviousOption).toHaveBeenCalled(); - }); - - it('should call `selectNextOption` and not call `selectPreviousOption`', () => { - component.comboOpen = true; - - const event = { ...fakeEvent, keyCode: 40 }; - - const spySelectNextOption = spyOn(component, 'selectNextOption'); - const spySelectPreviousOption = spyOn(component, 'selectPreviousOption'); - - component.onKeyDown(event); - - expect(spySelectPreviousOption).not.toHaveBeenCalled(); - expect(spySelectNextOption).toHaveBeenCalled(); - }); - it('should call `controlComboVisibility`, `verifyValidOption` and `updateComboList` if typed "esc"', () => { const event = { ...fakeEvent, keyCode: 27 }; @@ -723,53 +595,6 @@ describe('PoComboComponent:', () => { expect(component.selectedView).toBe(undefined); }); - it('shouldn`t call `selectPreviousOption` and should call `controlComboVisibility` if `comboOpen` is false', () => { - const event = { ...fakeEvent, keyCode: 38 }; - - component.changeOnEnter = false; - component.comboOpen = false; - component.isFiltering = true; - - spyOn(component, 'controlComboVisibility'); - spyOn(component, 'selectPreviousOption'); - - component.onKeyDown(event); - - expect(component.selectPreviousOption).not.toHaveBeenCalled(); - expect(component.controlComboVisibility).toHaveBeenCalledWith(true); - expect(component.isFiltering).toBe(false); - }); - - it('should call `controlComboVisibility` and set `isFiltering` with false if `changeOnEnter` is true', () => { - const event = { ...fakeEvent, keyCode: 38 }; - - component.comboOpen = false; - component.changeOnEnter = true; - - spyOn(component, 'controlComboVisibility'); - spyOn(component, 'selectPreviousOption'); - - component.onKeyDown(event); - - expect(component.selectPreviousOption).not.toHaveBeenCalled(); - expect(component.controlComboVisibility).toHaveBeenCalledWith(true); - expect(component.isFiltering).toBe(false); - }); - - it('should call `controlComboVisibility`, `verifyValidOption`, `updateComboList` if typed "tab"', () => { - const event = { ...fakeEvent, keyCode: 9 }; - - component.service = undefined; - - const spyControlComboVisibility = spyOn(component, 'controlComboVisibility'); - const spyVerifyValidOption = spyOn(component, 'verifyValidOption'); - - component.onKeyDown(event); - - expect(spyControlComboVisibility).toHaveBeenCalledWith(false); - expect(spyVerifyValidOption).toHaveBeenCalled(); - }); - it(`should call 'controlComboVisibility', 'updateComboList' and 'updateSelectedValue' with 'selectedView' and 'true' if typed 'enter', 'selectedView' is truthy and 'comboOpen' is true `, () => { const event = { ...fakeEvent, keyCode: 13, target: { value: '' } }; @@ -808,20 +633,6 @@ describe('PoComboComponent:', () => { expect(component.isFiltering).toBe(false); }); - it('shouldn`t call `selectNextOption` and call `controlComboVisibility` if `comboOpen` with false', () => { - const event = { ...fakeEvent, keyCode: 40 }; - - component.comboOpen = false; - - spyOn(component, 'controlComboVisibility'); - spyOn(component, 'selectNextOption'); - - component.onKeyDown(event); - - expect(component.controlComboVisibility).toHaveBeenCalledWith(true); - expect(component.selectNextOption).not.toHaveBeenCalled(); - }); - it('should call `preventDefault` and `stopPropagation` typed "esc" and combo is opened', () => { const event = { ...fakeEvent, keyCode: 27 }; @@ -874,16 +685,6 @@ describe('PoComboComponent:', () => { expect(spyControlComboVisibility).toHaveBeenCalledWith(true); }); - it('should not call `controlComboVisibility` if typed "a"', () => { - const event = { ...fakeEvent, keyCode: 65 }; - - const spyControlComboVisibility = spyOn(component, 'controlComboVisibility'); - - component.onKeyDown(event); - - expect(spyControlComboVisibility).not.toHaveBeenCalled(); - }); - it('should call `updateComboList` if itens service is undefined', () => { const event = { ...fakeEvent, keyCode: 13 }; @@ -911,6 +712,16 @@ describe('PoComboComponent:', () => { expect(component.updateComboList).not.toHaveBeenCalled(); }); + + it('should not call `controlComboVisibility` if typed "a"', () => { + const event = { ...fakeEvent, keyCode: 65 }; + + const spyControlComboVisibility = spyOn(component, 'controlComboVisibility'); + + component.onKeyDown(event); + + expect(spyControlComboVisibility).not.toHaveBeenCalled(); + }); }); it('onOptionClick: should call `stopPropagation` if has an event parameter', () => { @@ -1003,74 +814,6 @@ describe('PoComboComponent:', () => { expect(component.updateComboList).not.toHaveBeenCalled(); }); - it('selectPreviousOption: should call `updateSelectedValue` with nextOption and false if changeOnEnter is true', () => { - const nextOption = { value: 1, label: '1' }; - - component.changeOnEnter = true; - component.selectedView = { value: 2, label: '2' }; - - spyOn(component, 'updateSelectedValue'); - spyOn(component, 'getNextOption').and.returnValue(nextOption); - - component.selectPreviousOption(); - - expect(component.updateSelectedValue).toHaveBeenCalledWith(nextOption, false); - expect(component.getNextOption).toHaveBeenCalled(); - }); - - it('selectNextOption: should call `updateSelectedValue` with nextOption and false if changeOnEnter is true', () => { - const nextOption = { value: 1, label: '1' }; - - component.changeOnEnter = true; - component.selectedView = { value: 2, label: '2' }; - - spyOn(component, 'updateSelectedValue'); - spyOn(component, 'getNextOption').and.returnValue(nextOption); - - component.selectNextOption(); - - expect(component.updateSelectedValue).toHaveBeenCalledWith(nextOption, false); - expect(component.getNextOption).toHaveBeenCalled(); - }); - - it(`selectNextOption: should call 'updateSelectedValue' with first option of visibleOptions and true if - changeOnEnter is false and selectedValue is undefined`, () => { - component.visibleOptions = [ - { value: 1, label: '1' }, - { value: 2, label: '2' } - ]; - - component.changeOnEnter = false; - component.selectedValue = undefined; - - spyOn(component, 'updateSelectedValue'); - spyOn(component, 'getNextOption'); - - component.selectNextOption(); - - expect(component.updateSelectedValue).toHaveBeenCalledWith(component.visibleOptions[0], true); - expect(component.getNextOption).not.toHaveBeenCalled(); - }); - - it(`selectNextOption: should call 'updateSelectedValue' with second option of visibleOptions and false if - changeOnEnter is true and selectedValue is undefined`, () => { - component.visibleOptions = [ - { value: 1, label: '1' }, - { value: 2, label: '2' } - ]; - - component.changeOnEnter = true; - component.selectedValue = undefined; - - spyOn(component, 'updateSelectedValue'); - spyOn(component, 'getNextOption'); - - component.selectNextOption(); - - expect(component.updateSelectedValue).toHaveBeenCalledWith(component.visibleOptions[1], false); - expect(component.getNextOption).not.toHaveBeenCalled(); - }); - it(`wasClickedOnToggle: should call 'controlComboVisibility' with 'false' if 'comboOpen' is 'true', 'verifyValidOption', 'updateComboList' and set selectedView with undefined if changeOnEnter is true and selectedValue is falsy`, () => { component.selectedValue = undefined; @@ -1122,141 +865,6 @@ describe('PoComboComponent:', () => { expect(SpyApplyFilter).toHaveBeenCalledWith('', false); }); - it('scrollTo: should call setScrollTop with -88 ', () => { - const index = 3; - - component.options = [ - { value: '1', label: '1' }, - { value: '2', label: '2' }, - { value: '3', label: '3' } - ]; - component.selectedView = { value: '3', label: '3' }; - - const spySetScrollTop = spyOn(component, 'setScrollTop'); - - fixture.detectChanges(); - - component.scrollTo(index); - expect(spySetScrollTop).toHaveBeenCalledWith(-88); - }); - - it('scrollTo: should call setScrollTop with 0 if index is equal 1', () => { - const index = 1; - - const spySetScrollTop = spyOn(component, 'setScrollTop'); - - component.scrollTo(index); - - expect(spySetScrollTop).toHaveBeenCalledWith(0); - }); - - it('scrollTo: shouldn`t call setScrollTop if `infiniteScroll` is true', () => { - const index = 1; - component.infiniteScroll = true; - - const spySetScrollTop = spyOn(component, 'setScrollTop'); - - component.scrollTo(index); - - expect(spySetScrollTop).not.toHaveBeenCalled(); - }); - - it('scrollTo: should call setScrollTop with 0 if selectedView is undefined', () => { - const index = 13; - - component.selectedView = undefined; - - const spySetScrollTop = spyOn(component, 'setScrollTop'); - - component.scrollTo(index); - - expect(spySetScrollTop).toHaveBeenCalledWith(0); - }); - - it('setScrollTop: should set scrollTop with 44 if `contentElement` if truthy', () => { - const scrollTop = 44; - - component.contentElement = { nativeElement: { scrollTop: 0 } }; - - component['setScrollTop'](scrollTop); - - expect(component.contentElement.nativeElement.scrollTop).toBe(44); - }); - - it('setScrollTop: shouldn`t set scrollTop if `contentElement` if undefined', () => { - const scrollTop = 1; - - component.contentElement = undefined; - - component['setScrollTop'](scrollTop); - - expect(component.contentElement).toBeUndefined(); - }); - - it('checkInfiniteScroll: should call includeInfiniteScroll if height is smaller than scrollHeight', () => { - const spyIncludeInfiniteScroll = spyOn(component, 'includeInfiniteScroll'); - component.poComboBody = { - nativeElement: { offsetHeight: 100, scrollTop: 100, scrollHeight: 200 } - }; - - spyOn(component, 'hasInfiniteScroll').and.returnValue(true); - component['checkInfiniteScroll'](); - - expect(spyIncludeInfiniteScroll).toHaveBeenCalled(); - }); - - it('checkInfiniteScroll: should not call includeInfiniteScroll if poComboBody is undefined', () => { - const spyIncludeInfiniteScroll = spyOn(component, 'includeInfiniteScroll'); - component.poComboBody = undefined; - - spyOn(component, 'hasInfiniteScroll').and.returnValue(true); - component['checkInfiniteScroll'](); - - expect(spyIncludeInfiniteScroll).not.toHaveBeenCalled(); - }); - - it('checkInfiniteScroll: should call includeInfiniteScroll if height is less than scrollHeight', () => { - const spyIncludeInfiniteScroll = spyOn(component, 'includeInfiniteScroll'); - component.poComboBody = { - nativeElement: { offsetHeight: 100, scrollTop: 100, scrollHeight: 150 } - }; - - spyOn(component, 'hasInfiniteScroll').and.returnValue(true); - component['checkInfiniteScroll'](); - - expect(spyIncludeInfiniteScroll).not.toHaveBeenCalled(); - }); - - it('hasInfiniteScroll: should be called when has infiniteScroll and has poComboBody', () => { - component.infiniteScroll = true; - component.poComboBody = { - nativeElement: { offsetHeight: 100, scrollTop: 100, scrollHeight: 150 } - }; - - const test = component['hasInfiniteScroll'](); - - expect(test).toBeTruthy(); - }); - - it('hasInfiniteScroll: should be called when has infiniteScroll and poComboBody is undefined', () => { - component.infiniteScroll = true; - component.poComboBody = undefined; - - const test = component['hasInfiniteScroll'](); - - expect(test).toBeFalsy(); - }); - - it('includeInfiniteScroll: should call `scrollListeneter called when `infiniteScroll` is used', () => { - component.infiniteScroll = true; - const spy = spyOn(component.defaultService, 'scrollListener').and.returnValue( - of({ target: { offsetHeight: 100, scrollTop: 100, scrollHeight: 1 } }) - ); - - component['includeInfiniteScroll'](); - expect(spy).toHaveBeenCalled(); - }); - it('setContainerPosition: should call `controlPosition.setElements` and `adjustContainerPosition`', () => { fixture.detectChanges(); const containerOffset = 8; @@ -1279,64 +887,35 @@ describe('PoComboComponent:', () => { }); it('showMoreInfiniteScroll: should call `onShowMore` if `offsetHeight` + `scrollTop` is greater than `scrollHeight`', () => { - const event = { target: { offsetHeight: 100, scrollTop: 100, scrollHeight: 1 } }; const spyOnShowMore = spyOn(component, 'applyFilter'); component.infiniteScrollDistance = 10; - component.showMoreInfiniteScroll(event); + component.showMoreInfiniteScroll(); expect(spyOnShowMore).toHaveBeenCalled(); }); it('showMoreInfiniteScroll: shouldn`t call `onShowMore` if `offsetHeight` + `scrollTop` is less than `scrollHeight`', () => { - const event = { target: { offsetHeight: 10, scrollTop: 10, scrollHeight: 100 } }; const spyOnShowMore = spyOn(component, 'applyFilter'); component.infiniteScrollDistance = 110; - component.showMoreInfiniteScroll(event); + component.showMoreInfiniteScroll(); - expect(spyOnShowMore).not.toHaveBeenCalled(); - }); - - it('removeListeners: should remove click, resize and scroll listeners', () => { - component['clickoutListener'] = () => {}; - component['eventResizeListener'] = () => {}; - - spyOn(component, 'clickoutListener'); - spyOn(component, 'eventResizeListener'); - spyOn(window, 'removeEventListener'); - - component['removeListeners'](); - - expect(component['clickoutListener']).toHaveBeenCalled(); - expect(component['eventResizeListener']).toHaveBeenCalled(); - expect(window.removeEventListener).toHaveBeenCalled(); - }); - - it('onScroll: should call `adjustContainerPosition()`', () => { - const spyAdjustContainerPosition = spyOn(component, 'adjustContainerPosition'); - - component['onScroll'](); - - expect(spyAdjustContainerPosition).toHaveBeenCalled(); + expect(spyOnShowMore).toHaveBeenCalled(); }); it('initializeListeners: should call removeListeners and initialize click, resize and scroll listeners', () => { component['clickoutListener'] = undefined; component['eventResizeListener'] = undefined; - const spyRemoveListeners = spyOn(component, 'removeListeners'); - const spyAddEventListener = spyOn(window, 'addEventListener'); const spyRendererListen = spyOn(component.renderer, 'listen').and.returnValue(() => {}); component['initializeListeners'](); expect(spyRemoveListeners).toHaveBeenCalled(); - expect(spyAddEventListener).toHaveBeenCalled(); expect(spyRendererListen).toHaveBeenCalled(); - expect(component['clickoutListener']).toBeDefined(); expect(component['eventResizeListener']).toBeDefined(); }); @@ -1345,12 +924,10 @@ describe('PoComboComponent:', () => { and update properties 'comboOpen' to 'true' and 'comboIcon' to 'po-icon-arrow-up'`, () => { component.comboOpen = false; component.comboIcon = 'po-icon-arrow-down'; - component.visibleOptions = [{ value: 'po', label: 'PO' }]; const spyInitializeListeners = spyOn(component, 'initializeListeners'); const spySetContainerPosition = spyOn(component, 'setContainerPosition'); - const spyScrollTo = spyOn(component, 'scrollTo'); const spyDetectChanges = spyOn(component['changeDetector'], 'detectChanges'); const spyInputFocus = spyOn(component.inputEl.nativeElement, 'focus'); @@ -1358,10 +935,8 @@ describe('PoComboComponent:', () => { expect(spyInputFocus).toHaveBeenCalled(); expect(spyDetectChanges).toHaveBeenCalled(); - expect(spyScrollTo).toHaveBeenCalled(); expect(spySetContainerPosition).toHaveBeenCalled(); expect(spyInitializeListeners).toHaveBeenCalled(); - expect(component.comboOpen).toBe(true); expect(component.comboIcon).toBe('po-icon-arrow-up'); }); @@ -1369,24 +944,46 @@ describe('PoComboComponent:', () => { it('open: shouldn`t call `scrollTo` if infiniteScroll is false', () => { component.infiniteScroll = true; - const spyScrollTo = spyOn(component, 'scrollTo'); - component['open'](true); - expect(spyScrollTo).not.toHaveBeenCalled(); + expect(component.comboOpen).toBe(true); }); it('open: should set page and options when has inifity scroll', () => { component.infiniteScroll = true; spyOn(component, 'getInputValue').and.returnValue(undefined); - const spyScrollTo = spyOn(component, 'scrollTo'); - component['open'](false); expect(component.page).toBe(1); expect(component.options).toEqual([]); - expect(spyScrollTo).toHaveBeenCalledWith(0); + }); + + it('calculateScrollTop: should return 0 when selectedItem is empty', () => { + const selectedItem = []; + const index = 1; + + const result = component.calculateScrollTop(selectedItem, index); + + expect(result).toBe(0); + }); + + it('calculateScrollTop: should return 0 when index is less than or equal to 1', () => { + const selectedItem = [{ offsetTop: 100 }]; + const index = 0; + + const result = component.calculateScrollTop(selectedItem, index); + + expect(result).toBe(0); + }); + + it('calculateScrollTop: should return 100 when index is less than or equal to 2', () => { + const selectedItem = [{ offsetTop: 100 }]; + const index = 2; + + const result = component.calculateScrollTop(selectedItem, index); + + expect(result).toBe(100); }); it(`close: should call 'removeListeners' and 'detectChanges' @@ -1480,106 +1077,6 @@ describe('PoComboComponent:', () => { expect(spyAdjustPosition).toHaveBeenCalledWith(poComboContainerPositionDefault); }); - - it('onScroll: should call `adjustContainerPosition`', () => { - const spyAdjustContainerPosition = spyOn(component, 'adjustContainerPosition'); - - component['onScroll'](); - - expect(spyAdjustContainerPosition).toHaveBeenCalled(); - }); - - it('sanitizeTagHTML: should replace < and > with < and > respectively', () => { - const expectedValue = '<input> Testando'; - const value = ' Testando'; - - expect(component['sanitizeTagHTML'](value)).toBe(expectedValue); - }); - - it('sanitizeTagHTML: should return param value if it doesn`t contain < and >', () => { - const expectedValue = 'Testando'; - const value = 'Testando'; - - expect(component['sanitizeTagHTML'](value)).toBe(expectedValue); - }); - - it('sanitizeTagHTML: should return empty value if param value is undefined', () => { - const expectedValue = ''; - const value = undefined; - - expect(component['sanitizeTagHTML'](value)).toBe(expectedValue); - }); - - it('getLabelFormatted: shouldn`t get formatted label with `endsWith` if inputValue isn`t found in label', () => { - const label = 'values'; - const expectedValue = `${label}`; - - component.isFiltering = true; - component.filterMode = PoComboFilterMode.endsWith; - component.safeHtml = (value: any) => value; - component.inputEl.nativeElement.value = 'othervalue'; - - expect(component.getLabelFormatted(label)).not.toBe(expectedValue); - }); - - it('getLabelFormatted: shouldn`t get formatted label with `contains` if inputValue isn`t found in label', () => { - const label = 'values'; - const expectedValue = `${label}`; - - component.isFiltering = true; - component.filterMode = PoComboFilterMode.contains; - component.safeHtml = (value: any) => value; - component.inputEl.nativeElement.value = 'othervalue'; - - expect(component.getLabelFormatted(label)).not.toBe(expectedValue); - }); - - it('getLabelFormatted: should get formatted label with startWith', () => { - component.isFiltering = true; - component.filterMode = PoComboFilterMode.startsWith; - component.safeHtml = (value: any) => value; - component.inputEl.nativeElement.value = 'val'; - - expect(component.getLabelFormatted('values')).toBe('values'); - }); - - it('getLabelFormatted: should get formatted label with contains', () => { - component.isFiltering = true; - component.filterMode = PoComboFilterMode.contains; - component.safeHtml = (value: any) => value; - component.inputEl.nativeElement.value = 'lue'; - - expect(component.getLabelFormatted('values')).toBe('values'); - }); - - it('getLabelFormatted: should get formatted label with endsWith', () => { - component.isFiltering = true; - component.filterMode = PoComboFilterMode.endsWith; - component.safeHtml = (value: any) => value; - component.inputEl.nativeElement.value = 'lues'; - - expect(component.getLabelFormatted('values')).toBe('values'); - }); - - it('getLabelFormatted: should not get formatted label', () => { - component.isFiltering = false; - component.safeHtml = (value: any) => value; - component.inputEl.nativeElement.value = 'lues'; - - expect(component.getLabelFormatted('values')).toBe('values'); - }); - - it('getLabelFormatted: should not get formatted label when shouldMarkLetters is false', () => { - component.isFiltering = false; - component.service = component.defaultService; - component.shouldMarkLetters = false; - component.getInputValue = () => true; - component.compareObjects = (a, b) => false; - component.safeHtml = (value: any) => value; - component.inputEl.nativeElement.value = 'lues'; - - expect(component.getLabelFormatted('values')).toBe('values'); - }); }); describe('Templates:', () => { @@ -1635,56 +1132,6 @@ describe('PoComboComponent:', () => { expect(defaultSpan).toBeNull(); }); - it('should contain a child span tag inside `po-combo-item` if `comboOptionTemplate` is false', () => { - component.comboOptionTemplate = undefined; - component.options = [{ label: '1', value: '1' }]; - - fixture.detectChanges(); - - const defaultSpan = nativeElement.querySelector('.po-combo-item > span'); - - expect(defaultSpan).toBeTruthy(); - }); - - it('should call `onOptionClick` if clicked option isnt`t an option group title', () => { - component.options = [{ label: '1', value: '1' }]; - - const spyOnOptionClick = spyOn(component, 'onOptionClick'); - - fixture.detectChanges(); - - const optionItem = component.contentElement.nativeElement.querySelectorAll('li')[0]; - - optionItem.click(); - - expect(spyOnOptionClick).toHaveBeenCalled(); - }); - - it('shouldn`t call `onOptionClick` if clicked option is an option group title', () => { - component.options = [{ label: '1', options: [{ value: 'value' }] }]; - - const spyOnOptionClick = spyOn(component, 'onOptionClick'); - - fixture.detectChanges(); - - const optionItem = component.contentElement.nativeElement.querySelectorAll('li')[0]; - - optionItem.click(); - - expect(spyOnOptionClick).not.toHaveBeenCalled(); - }); - - it('should contain `po-combo-item` if `comboOptionTemplate` is true and combo options dont`t have groups', () => { - component.comboOptionTemplate = { templateRef: null }; - component.options = [{ label: '1', value: '1' }]; - - fixture.detectChanges(); - - const comboItemLink = nativeElement.querySelector('.po-combo-item'); - - expect(comboItemLink).toBeTruthy(); - }); - it('shouldn`t contain `po-combo-item` if `comboOptionTemplate` is true but combo options have groups', () => { component.comboOptionTemplate = { templateRef: null }; component.options = [{ label: '1', options: [{ value: 'value' }] }]; @@ -1695,47 +1142,6 @@ describe('PoComboComponent:', () => { expect(comboItemLink).toBeFalsy(); }); - - it('should contain a class `po-combo-item-title` if comboOptionTemplate is false and combo options have groups', () => { - component.comboOptionTemplate = undefined; - component.options = [{ label: '1', options: [{ value: 'value' }] }]; - - fixture.detectChanges(); - - const comboItemLink = nativeElement.querySelector('.po-combo-item-title'); - - expect(comboItemLink).toBeTruthy(); - }); - - it('shouldn`t contain a class `po-combo-item-title` if comboOptionTemplate is false and combo options don`t have groups', () => { - component.comboOptionTemplate = undefined; - component.options = [ - { label: '1', value: '2' }, - { label: '2', value: '2' } - ]; - - fixture.detectChanges(); - - const comboItemLink = nativeElement.querySelector('.po-combo-item-title'); - - expect(comboItemLink).toBeFalsy(); - }); - - it('should display `noDataTemplate` if don´t have `visibleOptions` and visibleOptions.length.', () => { - component.visibleOptions = []; - - fixture.detectChanges(); - - const comboInput = nativeElement.querySelector('.po-combo-input'); - comboInput.dispatchEvent(eventClick); - - fixture.detectChanges(); - - const noDataTemplate = nativeElement.querySelector('.po-combo-container-no-data'); - - expect(noDataTemplate).toBeTruthy(); - }); - it('shouldn´t display `noDataTemplate` if have `visibleOptions` and visibleOptions.length.', () => { component.visibleOptions = [{ label: '1', value: '1' }]; @@ -1747,24 +1153,6 @@ describe('PoComboComponent:', () => { expect(noDataTemplate).toBeNull(); }); - it('should display `literals.noData` in Spanish if browser language is `es`.', () => { - component['language'] = 'es'; - - component.visibleOptions = []; - - fixture.detectChanges(); - - const comboInput = nativeElement.querySelector('.po-combo-input'); - comboInput.dispatchEvent(eventClick); - - fixture.detectChanges(); - - const noDataTemplateText = nativeElement.querySelector('.po-combo-container-no-data .po-combo-no-data').innerText; - const noDataTemplateTextCompare = 'Datos no encontrados'; - - expect(noDataTemplateText).toEqual(noDataTemplateTextCompare); - }); - it('should show po-clean if `clean` is true and `disabled` is false', () => { component.clean = true; component.disabled = false; @@ -1791,20 +1179,6 @@ describe('PoComboComponent:', () => { }); describe('Integration:', () => { - it('should return empty array and display `po-combo-container-no-data` if not found searched option', () => { - const searchTerm = 'Acre'; - const keyUpEvent = { target: { value: searchTerm } }; - component.options = [ - { label: 'Santa Catarina', value: 'sc' }, - { label: 'São Paulo', value: 'sp' }, - { label: 'Rio Janeiro', value: 'rj' } - ]; - fixture.debugElement.query(By.css('input')).triggerEventHandler('keyup', keyUpEvent); - - expect(component.visibleOptions).toEqual([]); - expect(fixture.debugElement.query(By.css('.po-combo-container-no-data'))).toBeTruthy(); - }); - it('should return found option and not display `po-combo-container-no-data` if found searched option', () => { const searchTerm = 'Santa'; const keyUpEvent = { target: { value: searchTerm } }; @@ -1816,42 +1190,6 @@ describe('PoComboComponent:', () => { expect(component.visibleOptions).toEqual(optionFound); expect(fixture.debugElement.query(By.css('.po-combo-container-no-data'))).toBeNull(); }); - - it('checkTemplate: should return truthy if visibleOptions has items', () => { - component.visibleOptions = [{ label: '1', value: '1' }]; - - expect(component.checkTemplate()).toBeTruthy(); - }); - - it('checkTemplate: should return false if visibleOptions is empty', () => { - component.visibleOptions = []; - - expect(component.checkTemplate()).toBeFalsy(); - }); - - it('checkTemplate: should return false if cache is false and isServerSearching is true', () => { - component.cache = false; - component.isServerSearching = true; - component.visibleOptions = [{ label: '1', value: '1' }]; - - expect(component.checkTemplate()).toBeFalsy(); - }); - - it('checkTemplate: should return truthy if cache is false and isServerSearching is false', () => { - component.cache = false; - component.isServerSearching = false; - component.visibleOptions = [{ label: '1', value: '1' }]; - - expect(component.checkTemplate()).toBeTruthy(); - }); - - it('checkTemplate: should return falsy if cache is false and isServerSearching is false but visibleOptions is empty', () => { - component.cache = false; - component.isServerSearching = false; - component.visibleOptions = []; - - expect(component.checkTemplate()).toBeFalsy(); - }); }); }); @@ -1887,23 +1225,6 @@ describe('PoComboComponent - with service:', () => { httpMock.verify(); }); - it('should call `getObjectByValue`, `controlComboVisibility` function if typed "tab"', () => { - const fakeEvent = { - keyCode: 9, - target: { - value: 'po' - } - }; - - spyOn(component, 'getObjectByValue'); - spyOn(component, 'controlComboVisibility'); - - component.onKeyDown.call(component, fakeEvent); - - expect(component.getObjectByValue).toHaveBeenCalled(); - expect(component.controlComboVisibility).toHaveBeenCalledWith(false); - }); - it('should call updateOptionByFilteredValue if selectedValue is different of value parameter', () => { component.service = getFakeService([{ label: 'label', value: 1 }]); component.selectedValue = 'po'; @@ -2106,24 +1427,6 @@ describe('PoComboComponent - with service:', () => { expect(spyFocus).not.toHaveBeenCalled(); }); - it('ngAfterViewInit: Should call checkInfiniteScroll if infiniteScroll is true', () => { - component.infiniteScroll = true; - - const spyCheckInfiniteScroll = spyOn(component, 'checkInfiniteScroll'); - component.ngAfterViewInit(); - - expect(spyCheckInfiniteScroll).toHaveBeenCalled(); - }); - - it('ngAfterViewInit: shouldn´t call checkInfiniteScroll if infiniteScroll is false', () => { - component.infiniteScroll = false; - - const spyCheckInfiniteScroll = spyOn(component, 'checkInfiniteScroll'); - component.ngAfterViewInit(); - - expect(spyCheckInfiniteScroll).not.toHaveBeenCalled(); - }); - it('ngOnDestroy: should not unsubscribe if getSubscription is falsy.', () => { component['getSubscription'] = fakeSubscription; @@ -2297,56 +1600,6 @@ describe('PoComboComponent - with service:', () => { expect(spyUpdateComboList).toHaveBeenCalledWith([...component.cacheOptions]); }); - it('includeInfiniteScroll: should unsubscribe from the scroll event', () => { - component.poComboBody = { - nativeElement: { offsetHeight: 100, scrollTop: 100, scrollHeight: 100 } - }; - component['subscriptionScrollEvent'] = fakeSubscription; - spyOn(fakeSubscription, 'unsubscribe'); - spyOn(component['defaultService'], 'scrollListener').and.returnValue( - of({ target: { offsetHeight: 100, scrollTop: 100, scrollHeight: 100 } }) - ); - - component.infiniteScroll = true; - component['includeInfiniteScroll'](); - - expect(fakeSubscription.unsubscribe).toHaveBeenCalled(); - }); - - it('includeInfiniteScroll: should not unsubscribe from the scroll event', () => { - component.poComboBody = { - nativeElement: { offsetHeight: 100, scrollTop: 100, scrollHeight: 100 } - }; - component['subscriptionScrollEvent'] = fakeSubscription; - spyOn(fakeSubscription, 'unsubscribe'); - spyOn(component['defaultService'], 'scrollListener').and.returnValue( - of({ target: { offsetHeight: 100, scrollTop: 100, scrollHeight: 100 } }) - ); - spyOn(component, 'getInputValue').and.returnValue(undefined); - - component['subscriptionScrollEvent'] = undefined; - component.infiniteScroll = true; - component['includeInfiniteScroll'](); - expect(component.getInputValue).toHaveBeenCalled(); - - expect(fakeSubscription.unsubscribe).not.toHaveBeenCalled(); - }); - - it('removeListeners: should unsubscribe from the scroll event when has infinity Scroll', () => { - component.poComboBody = { - nativeElement: { offsetHeight: 100, scrollTop: 100, scrollHeight: 100 } - }; - component['subscriptionScrollEvent'] = fakeSubscription; - - spyOn(fakeSubscription, 'unsubscribe'); - component.infiniteScroll = true; - component['defaultService'].hasNext = false; - - component['removeListeners'](); - - expect(fakeSubscription.unsubscribe).toHaveBeenCalled(); - }); - it('removeListeners: should not unsubscribe from the scroll event when it does not exist and has infinity Scroll', () => { component['subscriptionScrollEvent'] = fakeSubscription; @@ -2415,21 +1668,21 @@ describe('PoComboComponent - with service:', () => { }); describe('Templates:', () => { - it('should display `.po-combo-container-no-data` if error in filtered data', () => { + it('should poCombo close if error in filtered data', () => { const value = 'test'; const error = { 'error': { 'message': 'message' } }; - - component.options = [ + const mockOptions = [ { value: 1, label: 'John Doe' }, { value: 2, label: 'Jane Doe' } ]; + component.options = mockOptions; spyOn(component.service, 'getFilteredData').and.returnValue(throwError(error)); component.applyFilter(value); fixture.detectChanges(); - expect(fixture.debugElement.query(By.css('.po-combo-container-no-data'))).toBeTruthy(); + expect(component.visibleOptions).toEqual(mockOptions); }); }); }); diff --git a/projects/ui/src/lib/components/po-field/po-combo/po-combo.component.ts b/projects/ui/src/lib/components/po-field/po-combo/po-combo.component.ts index 042b39eec4..7d22312d37 100644 --- a/projects/ui/src/lib/components/po-field/po-combo/po-combo.component.ts +++ b/projects/ui/src/lib/components/po-field/po-combo/po-combo.component.ts @@ -13,10 +13,9 @@ import { SimpleChanges, ViewChild } from '@angular/core'; -import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { fromEvent, Observable, Subscription } from 'rxjs'; +import { fromEvent, Subscription } from 'rxjs'; import { debounceTime, distinctUntilChanged, filter, map, tap } from 'rxjs/operators'; import { PoControlPositionService } from '../../../services/po-control-position/po-control-position.service'; @@ -24,7 +23,6 @@ import { PoKeyCodeEnum } from './../../../enums/po-key-code.enum'; import { PoLanguageService } from '../../../services/po-language/po-language.service'; import { PoComboBaseComponent } from './po-combo-base.component'; -import { PoComboFilterMode } from './po-combo-filter-mode.enum'; import { PoComboFilterService } from './po-combo-filter.service'; import { PoComboGroup } from './interfaces/po-combo-group.interface'; import { PoComboOption } from './interfaces/po-combo-option.interface'; @@ -111,7 +109,7 @@ export class PoComboComponent extends PoComboBaseComponent implements AfterViewI @ViewChild('contentElement', { read: ElementRef }) contentElement: ElementRef; @ViewChild('iconArrow', { read: ElementRef, static: true }) iconElement: ElementRef; @ViewChild('inp', { read: ElementRef, static: true }) inputEl: ElementRef; - @ViewChild('poComboBody', { read: ElementRef }) poComboBody: ElementRef; + @ViewChild('poListbox', { read: ElementRef }) poListbox: ElementRef; comboIcon: string = 'po-icon-arrow-down'; comboOpen: boolean = false; @@ -131,7 +129,6 @@ export class PoComboComponent extends PoComboBaseComponent implements AfterViewI private filterSubscription: Subscription; private getSubscription: Subscription; - private scrollEvent$: Observable; private subscriptionScrollEvent: Subscription; constructor( @@ -141,7 +138,6 @@ export class PoComboComponent extends PoComboBaseComponent implements AfterViewI public renderer: Renderer2, private changeDetector: ChangeDetectorRef, private controlPosition: PoControlPositionService, - private sanitized: DomSanitizer, languageService: PoLanguageService ) { super(languageService); @@ -170,9 +166,6 @@ export class PoComboComponent extends PoComboBaseComponent implements AfterViewI if (this.autoFocus) { this.focus(); } - if (this.infiniteScroll) { - this.checkInfiniteScroll(); - } } ngOnChanges(changes: SimpleChanges) { @@ -233,32 +226,8 @@ export class PoComboComponent extends PoComboBaseComponent implements AfterViewI const key = event.keyCode; const inputValue = event.target.value; - // busca um registro quando acionar o tab - if (this.service && key === PoKeyCodeEnum.tab && inputValue && !this.disabledTabFilter) { - this.controlComboVisibility(false); - return this.getObjectByValue(inputValue); - } - - // Teclas "up" e "down" - if (key === PoKeyCodeEnum.arrowUp || key === PoKeyCodeEnum.arrowDown) { - event.preventDefault(); - - if (this.comboOpen) { - if (key === PoKeyCodeEnum.arrowUp) { - this.selectPreviousOption(); - } else { - this.selectNextOption(); - } - } - - this.controlComboVisibility(true); - this.isFiltering = this.changeOnEnter ? this.isFiltering : false; - this.shouldMarkLetters = this.changeOnEnter ? this.shouldMarkLetters : false; - return; - } - // Teclas "tab" ou "esc" - if (key === PoKeyCodeEnum.tab || key === PoKeyCodeEnum.esc) { + if (key === PoKeyCodeEnum.esc) { if (key === PoKeyCodeEnum.esc && this.comboOpen) { event.preventDefault(); event.stopPropagation(); @@ -428,48 +397,6 @@ export class PoComboComponent extends PoComboBaseComponent implements AfterViewI }, this.debounceTime); } - selectPreviousOption() { - const currentViewValue = this.selectedView && this.selectedView[this.dynamicValue]; - - if (currentViewValue) { - const nextOption = this.getNextOption(currentViewValue, this.visibleOptions, true); - - this.updateSelectedValue( - nextOption, - nextOption && nextOption[this.dynamicValue] !== currentViewValue && !this.changeOnEnter - ); - } else if (this.visibleOptions.length) { - const visibleOption = this.visibleOptions[this.visibleOptions.length - 1]; - - this.updateSelectedValue( - visibleOption, - visibleOption[this.dynamicValue] !== currentViewValue && !this.changeOnEnter - ); - } - } - - selectNextOption() { - const currentViewValue = this.selectedView && this.selectedView[this.dynamicValue]; - - if (currentViewValue) { - const nextOption = this.getNextOption(currentViewValue, this.visibleOptions); - - this.updateSelectedValue( - nextOption, - nextOption && nextOption[this.dynamicValue] !== currentViewValue && !this.changeOnEnter - ); - } else if (this.visibleOptions.length) { - const index = this.changeOnEnter ? 1 : 0; - - const visibleOption = this.visibleOptions[index]; - - this.updateSelectedValue( - visibleOption, - visibleOption[this.dynamicValue] !== currentViewValue && !this.changeOnEnter - ); - } - } - toggleComboVisibility(): void { if (this.disabled) { return; @@ -513,12 +440,11 @@ export class PoComboComponent extends PoComboBaseComponent implements AfterViewI this.previousSearchValue = this.selectedView[this.dynamicLabel]; } - scrollTo(index) { - const selectedItem = this.element.nativeElement.querySelectorAll('.po-combo-item-selected'); - const scrollTop = !selectedItem.length || index <= 1 ? 0 : selectedItem[0].offsetTop - 88; - - if (!this.infiniteScroll) { - this.setScrollTop(scrollTop); + calculateScrollTop(selectedItem, index) { + if (!selectedItem.length || index <= 1) { + return 0; + } else { + return selectedItem[0].offsetTop; } } @@ -552,62 +478,6 @@ export class PoComboComponent extends PoComboBaseComponent implements AfterViewI } } - getLabelFormatted(label: string): SafeHtml { - const sanitizedLabel = this.sanitizeTagHTML(label); - let format: string = sanitizedLabel; - - if ( - this.isFiltering || - (this.service && - this.getInputValue() && - !this.compareObjects(this.cacheOptions, this.visibleOptions) && - this.shouldMarkLetters) - ) { - const labelInput = this.sanitizeTagHTML(this.getInputValue().toString().toLowerCase()); - const labelLowerCase = sanitizedLabel.toLowerCase(); - - const openTagBold = ''; - const closeTagBold = ''; - - let startString; - let middleString; - let endString; - - switch (this.filterMode) { - case PoComboFilterMode.startsWith: - case PoComboFilterMode.contains: - const indexOfLabelInput = labelLowerCase.indexOf(labelInput); - - if (indexOfLabelInput > -1) { - startString = sanitizedLabel.substring(0, indexOfLabelInput); - - middleString = sanitizedLabel.substring(indexOfLabelInput, indexOfLabelInput + labelInput.length); - endString = sanitizedLabel.substring(indexOfLabelInput + labelInput.length); - - format = startString + openTagBold + middleString + closeTagBold + endString; - } - - break; - case PoComboFilterMode.endsWith: - const lastIndexOfLabelInput = labelLowerCase.lastIndexOf(labelInput); - - if (lastIndexOfLabelInput > -1) { - startString = sanitizedLabel.substring(0, lastIndexOfLabelInput); - middleString = sanitizedLabel.substring(lastIndexOfLabelInput); - - format = startString + openTagBold + middleString + closeTagBold; - } - break; - } - } - - return this.safeHtml(format); - } - - safeHtml(value): SafeHtml { - return this.sanitized.bypassSecurityTrustHtml(value); - } - isValidCharacterToSearch(keyCode) { return ( keyCode !== 9 && // tab @@ -631,41 +501,12 @@ export class PoComboComponent extends PoComboBaseComponent implements AfterViewI } } - showMoreInfiniteScroll({ target }): void { + showMoreInfiniteScroll(): void { if (this.defaultService.hasNext) { this.infiniteLoading = true; } - const scrollPosition = target.offsetHeight + target.scrollTop; - if (scrollPosition >= target.scrollHeight * (this.infiniteScrollDistance / 110)) { - this.page++; - this.applyFilter('', true); - } - } - - checkTemplate() { - if (this.cache || this.infiniteScroll) { - return this.visibleOptions.length; - } else { - return !this.isServerSearching && this.visibleOptions.length; - } - } - - protected checkInfiniteScroll(): void { - if (this.hasInfiniteScroll() && this.poComboBody?.nativeElement.scrollHeight >= 175) { - this.includeInfiniteScroll(); - } - } - - private hasInfiniteScroll(): boolean { - return this.infiniteScroll && this.poComboBody?.nativeElement.scrollHeight && this.defaultService.hasNext; - } - - private includeInfiniteScroll(): void { - this.subscriptionScrollEvent?.unsubscribe(); - this.scrollEvent$ = this.defaultService.scrollListener(this.poComboBody.nativeElement); - this.subscriptionScrollEvent = this.scrollEvent$.subscribe(event => { - this.showMoreInfiniteScroll(event); - }); + this.page++; + this.applyFilter('', true); } private adjustContainerPosition() { @@ -697,10 +538,6 @@ export class PoComboComponent extends PoComboBaseComponent implements AfterViewI private initializeListeners() { this.removeListeners(); - if (this.infiniteScroll) { - this.checkInfiniteScroll(); - } - this.clickoutListener = this.renderer.listen('document', 'click', (event: MouseEvent) => { this.wasClickedOnToggle(event); }); @@ -709,8 +546,6 @@ export class PoComboComponent extends PoComboBaseComponent implements AfterViewI // timeout necessario pois a animação do po-menu impacta no ajuste da posição do container. setTimeout(() => this.adjustContainerPosition(), 250); }); - - window.addEventListener('scroll', this.onScroll, true); } private onErrorGetObjectByValue() { @@ -725,10 +560,6 @@ export class PoComboComponent extends PoComboBaseComponent implements AfterViewI this.controlComboVisibility(true); } - private onScroll = (): void => { - this.adjustContainerPosition(); - }; - private open(reset: boolean) { this.comboOpen = true; if (!reset && this.infiniteScroll) { @@ -736,7 +567,6 @@ export class PoComboComponent extends PoComboBaseComponent implements AfterViewI this.page = 1; } this.options = this.setOptions(); - this.scrollTo(0); } this.changeDetector.detectChanges(); @@ -746,9 +576,6 @@ export class PoComboComponent extends PoComboBaseComponent implements AfterViewI this.initializeListeners(); this.inputEl.nativeElement.focus(); - if (!this.infiniteScroll) { - this.scrollTo(this.getIndexSelectedView()); - } this.setContainerPosition(); } @@ -760,15 +587,6 @@ export class PoComboComponent extends PoComboBaseComponent implements AfterViewI if (this.eventResizeListener) { this.eventResizeListener(); } - - if (this.infiniteScroll && !this.defaultService.hasNext) { - this.subscriptionScrollEvent?.unsubscribe(); - } - window.removeEventListener('scroll', this.onScroll, true); - } - - private sanitizeTagHTML(value: string = '') { - return value.replace(/\/g, '>'); } private setContainerPosition() { @@ -787,12 +605,6 @@ export class PoComboComponent extends PoComboBaseComponent implements AfterViewI return this.getInputValue() ? this.options : []; } - private setScrollTop(scrollTop: number) { - if (this.contentElement) { - this.contentElement.nativeElement.scrollTop = scrollTop; - } - } - private prepareOptions(items) { return this.infiniteScroll ? [...this.options, ...items] : items; } diff --git a/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-dropdown/po-multiselect-dropdown.component.html b/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-dropdown/po-multiselect-dropdown.component.html index dd6e8391c1..0cd990e7f1 100644 --- a/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-dropdown/po-multiselect-dropdown.component.html +++ b/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-dropdown/po-multiselect-dropdown.component.html @@ -15,7 +15,7 @@ [p-is-searching]="isServerSearching" [p-hide-search]="hideSearch" [p-hide-select-all]="hideSelectAll" - [p-multiselect-template]="multiselectTemplate" + [p-template]="multiselectTemplate" [p-placeholder-search]="placeholderSearch" (p-change)="clickItem($event)" (p-change-all)="onClickSelectAll()" diff --git a/projects/ui/src/lib/components/po-listbox/enums/po-item-list-filter-mode.enum.ts b/projects/ui/src/lib/components/po-listbox/enums/po-item-list-filter-mode.enum.ts new file mode 100644 index 0000000000..982dc9888e --- /dev/null +++ b/projects/ui/src/lib/components/po-listbox/enums/po-item-list-filter-mode.enum.ts @@ -0,0 +1,8 @@ +export enum PoItemListFilterMode { + /** Verifica se o texto *inicia* com o valor pesquisado. Caso não seja especificado um tipo, será esse o utilizado. */ + startsWith, + /** Verifica se o texto *contém* o valor pesquisado. */ + contains, + /** Verifica se o texto *finaliza* com o valor pesquisado. */ + endsWith +} diff --git a/projects/ui/src/lib/components/po-listbox/po-item-list/po-item-list-base.component.ts b/projects/ui/src/lib/components/po-listbox/po-item-list/po-item-list-base.component.ts index 49cc2e77e4..a9f0b72dc3 100644 --- a/projects/ui/src/lib/components/po-listbox/po-item-list/po-item-list-base.component.ts +++ b/projects/ui/src/lib/components/po-listbox/po-item-list/po-item-list-base.component.ts @@ -4,6 +4,7 @@ import { PoItemListType } from '../enums/po-item-list-type.enum'; import { PoItemListAction } from './interfaces/po-item-list-action.interface'; import { PoItemListOptionGroup } from './interfaces/po-item-list-option-group.interface'; import { PoItemListOption } from './interfaces/po-item-list-option.interface'; +import { PoItemListFilterMode } from '../enums/po-item-list-filter-mode.enum'; /** * @description @@ -135,6 +136,8 @@ export class PoItemListBaseComponent { //emissao de evento do checkbox @Output('p-selectcheckbox-item') checkboxItem = new EventEmitter(); + @Output('p-selectcombo-item') comboItem = new EventEmitter(); + //valor do checkbox de selecionar todos @Input('p-checkbox-value') checkboxValue: any; @@ -142,9 +145,21 @@ export class PoItemListBaseComponent { @Input('p-field-label') fieldLabel: string = 'label'; - @Input('p-multiselect-template') multiselectTemplate: TemplateRef | any; + @Input('p-template') template: TemplateRef | any; @Input('p-template-context') templateContext: any; + @Input('p-search-value') searchValue: string = ''; + + @Input('p-filter-mode') filterMode: PoItemListFilterMode = PoItemListFilterMode.contains; + + @Input('p-filtering') isFiltering: boolean = false; + + @Input('p-should-mark-letter') shouldMarkLetters: boolean = true; + + @Input('p-compare-cache') compareCache: boolean = false; + + @Input('p-combo-service') comboService: any; + constructor() {} } diff --git a/projects/ui/src/lib/components/po-listbox/po-item-list/po-item-list.component.html b/projects/ui/src/lib/components/po-listbox/po-item-list/po-item-list.component.html index 65a5af7fff..403e8ffd51 100644 --- a/projects/ui/src/lib/components/po-listbox/po-item-list/po-item-list.component.html +++ b/projects/ui/src/lib/components/po-listbox/po-item-list/po-item-list.component.html @@ -16,15 +16,17 @@
- {{ label }} + + + +
- {{ label }} + {{ label }} - +
diff --git a/projects/ui/src/lib/components/po-listbox/po-item-list/po-item-list.component.spec.ts b/projects/ui/src/lib/components/po-listbox/po-item-list/po-item-list.component.spec.ts index 6a001300cb..b155d1c426 100644 --- a/projects/ui/src/lib/components/po-listbox/po-item-list/po-item-list.component.spec.ts +++ b/projects/ui/src/lib/components/po-listbox/po-item-list/po-item-list.component.spec.ts @@ -1,9 +1,10 @@ +import { DomSanitizer } from '@angular/platform-browser'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { PoItemListComponent } from './po-item-list.component'; -import * as UtilFunctions from './../../../utils/util'; +import { PoItemListFilterMode } from '../enums/po-item-list-filter-mode.enum'; describe('PoItemListComponent', () => { let component: PoItemListComponent; @@ -63,6 +64,30 @@ describe('PoItemListComponent', () => { }); }); + describe('onComboItem:', () => { + it('should emit `comboItem`', () => { + const optionTest = { label: 'testLabel', value: 'testValue' }; + component.value = 'testValue'; + component.label = 'testLabel'; + component.selectedView = optionTest; + spyOn(component.comboItem, 'emit'); + + component.onComboItem(optionTest, ''); + + expect(component.comboItem.emit).toHaveBeenCalledWith({ + value: 'testValue', + label: 'testLabel', + event: '' + }); + }); + + it('should compare objects', () => { + const obj1 = { value: 'value', label: 'label' }; + const obj2 = { value: 'value', label: 'label' }; + expect(component.compareObjects(obj1, obj2)).toBeTruthy(); + }); + }); + describe('onCheckboxItem:', () => { it('should emit `checkboxItem` and set selected', () => { component.value = 'testValue'; @@ -81,6 +106,161 @@ describe('PoItemListComponent', () => { }); }); }); + + describe('onCheckboxItem:', () => { + it('should return true if all conditions are met', () => { + component.comboService = true; + component.searchValue = 'test'; + component.compareCache = false; + component.shouldMarkLetters = true; + + const result = component.validateForOptionsLabel(); + + expect(result).toBeTrue(); + }); + + it('should return false if shouldMarkLetters is falsy', () => { + component.comboService = true; + component.searchValue = 'test'; + component.compareCache = false; + component.shouldMarkLetters = false; + + const result = component.validateForOptionsLabel(); + + expect(result).toBeFalse(); + }); + }); + + it('should return a sanitized code', () => { + const html = component.safeHtml('values'); + expect(html['changingThisBreaksApplicationSecurity']).toBe('values'); + }); + + it('sanitizeTagHTML: should replace < and > with < and > respectively', () => { + const expectedValue = '<input> Testando'; + const value = ' Testando'; + + expect(component['sanitizeTagHTML'](value)).toBe(expectedValue); + }); + + it('sanitizeTagHTML: should return param value if it doesn`t contain < and >', () => { + const expectedValue = 'Testando'; + const value = 'Testando'; + + expect(component['sanitizeTagHTML'](value)).toBe(expectedValue); + }); + + it('sanitizeTagHTML: should return empty value if param value is undefined', () => { + const expectedValue = ''; + const value = undefined; + + expect(component['sanitizeTagHTML'](value)).toBe(expectedValue); + }); + + it('getLabelFormatted: shouldn`t get formatted label with `endsWith` if inputValue isn`t found in label', () => { + const label = 'values'; + const expectedValue = `${label}`; + + component.isFiltering = true; + component.filterMode = PoItemListFilterMode.endsWith; + component.safeHtml = (value: any) => value; + component.searchValue = 'othervalue'; + + expect(component.getLabelFormatted(label)).not.toBe(expectedValue); + }); + + it('getLabelFormatted: shouldn`t get formatted label with `contains` if inputValue isn`t found in label', () => { + const label = 'values'; + const expectedValue = `${label}`; + + component.isFiltering = true; + component.filterMode = PoItemListFilterMode.contains; + component.safeHtml = (value: any) => value; + component.searchValue = 'othervalue'; + + expect(component.getLabelFormatted(label)).not.toBe(expectedValue); + }); + + it('getLabelFormatted: should get formatted label with startWith', () => { + component.isFiltering = true; + component.filterMode = PoItemListFilterMode.startsWith; + component.safeHtml = (value: any) => value; + component.searchValue = 'val'; + + expect(component.getLabelFormatted('values')).toBe('values'); + }); + + it('getLabelFormatted: should get formatted label with contains', () => { + component.isFiltering = true; + component.filterMode = PoItemListFilterMode.contains; + component.safeHtml = (value: any) => value; + component.searchValue = 'lue'; + + expect(component.getLabelFormatted('values')).toBe('values'); + }); + + it('getLabelFormatted: should get formatted label with endsWith', () => { + component.isFiltering = true; + component.filterMode = PoItemListFilterMode.endsWith; + component.safeHtml = (value: any) => value; + component.searchValue = 'lues'; + + expect(component.getLabelFormatted('values')).toBe('values'); + }); + + it('getLabelFormatted: should not get formatted label', () => { + component.isFiltering = false; + component.safeHtml = (value: any) => value; + component.searchValue = 'lues'; + + expect(component.getLabelFormatted('values')).toBe('values'); + }); + + it('getLabelFormatted: should not get formatted label when shouldMarkLetters is false', () => { + component.isFiltering = false; + component.shouldMarkLetters = false; + component.compareObjects = (a, b) => false; + component.safeHtml = (value: any) => value; + component.searchValue = 'lues'; + + expect(component.getLabelFormatted('values')).toBe('values'); + }); + + it('should format label when conditions are met', () => { + component.isFiltering = true; + component.compareCache = false; + component.comboService = true; + component.shouldMarkLetters = true; + component.filterMode = PoItemListFilterMode.startsWith; + component.safeHtml = (value: any) => value; + component.compareObjects = (a, b) => false; + component.searchValue = 'lues'; + const openTagBold = ''; + const closeTagBold = ''; + + expect(component.getLabelFormatted('values')).toBe('values'); + }); + + it('should contain openTagBold and CloseTagBold', () => { + component.isFiltering = true; + component.compareCache = false; + component.shouldMarkLetters = true; + component.filterMode = PoItemListFilterMode.startsWith; + component.safeHtml = (value: any) => value; + component.searchValue = 'lues'; + + expect(component.getLabelFormatted('values')).toBe('values'); + }); + + it('getLabelFormatted: should not get formatted label when shouldMarkLetters is false', () => { + component.isFiltering = false; + component.shouldMarkLetters = false; + component.compareCache = true; + component.compareObjects = (a, b) => false; + component.safeHtml = (value: any) => value; + + expect(component.getLabelFormatted('values')).toBe('values'); + }); }); describe('Templates:', () => { @@ -91,13 +271,7 @@ describe('PoItemListComponent', () => { expect(nativeElement.querySelector('.po-item-list__action')).toBeTruthy(); }); - it('should de set type `option`', () => { - component.type = 'option'; - - fixture.detectChanges(); - expect(nativeElement.querySelector('.po-item-list__option')).toBeTruthy(); - }); it('should de set type `check`', () => { component.type = 'check'; @@ -115,3 +289,6 @@ describe('PoItemListComponent', () => { }); }); }); +function fakeKeypressEvent(arg0: number) { + throw new Error('Function not implemented.'); +} diff --git a/projects/ui/src/lib/components/po-listbox/po-item-list/po-item-list.component.ts b/projects/ui/src/lib/components/po-listbox/po-item-list/po-item-list.component.ts index 3361b10683..02e1955075 100644 --- a/projects/ui/src/lib/components/po-listbox/po-item-list/po-item-list.component.ts +++ b/projects/ui/src/lib/components/po-listbox/po-item-list/po-item-list.component.ts @@ -1,11 +1,11 @@ -import { Component, ElementRef, EventEmitter, HostBinding, Input, Output, ViewChild } from '@angular/core'; +import { Component, ElementRef, ViewChild } from '@angular/core'; import { Router } from '@angular/router'; -import { isExternalLink, isTypeof, openExternalLink } from '../../../utils/util'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; -import { PoItemListAction } from './interfaces/po-item-list-action.interface'; import { PoItemListOptionGroup } from './interfaces/po-item-list-option-group.interface'; import { PoItemListOption } from './interfaces/po-item-list-option.interface'; import { PoItemListBaseComponent } from './po-item-list-base.component'; +import { PoItemListFilterMode } from '../enums/po-item-list-filter-mode.enum'; @Component({ selector: 'po-item-list', @@ -19,7 +19,7 @@ export class PoItemListComponent extends PoItemListBaseComponent { protected param; protected clickListener: () => void; - constructor(private router: Router) { + constructor(private sanitized: DomSanitizer) { super(); } @@ -34,9 +34,77 @@ export class PoItemListComponent extends PoItemListBaseComponent { this.checkboxItem.emit({ option, selected }); } + onComboItem(options: any, event: any) { + const option = { [this.fieldValue]: this.value, [this.fieldLabel]: this.label }; + this.selectedView = options; + this.comboItem.emit({ ...option, event }); + } + + compareObjects(obj1: any, obj2: any) { + return JSON.stringify(obj1) === JSON.stringify(obj2); + } + onCheckboxItemEmit(event: KeyboardEvent) { if ((event && event.code === 'Enter') || event.code === 'Space') { this.onCheckboxItem(); } } + + getLabelFormatted(label: string): SafeHtml { + const sanitizedLabel = this.sanitizeTagHTML(label); + let format: string = sanitizedLabel; + + if (this.isFiltering || this.validateForOptionsLabel()) { + const labelInput = this.sanitizeTagHTML(this.searchValue.toString().toLowerCase()); + const labelLowerCase = sanitizedLabel.toLowerCase(); + + const openTagBold = ''; + const closeTagBold = ''; + + let startString; + let middleString; + let endString; + + switch (this.filterMode) { + case PoItemListFilterMode.startsWith: + case PoItemListFilterMode.contains: + const indexOfLabelInput = labelLowerCase.indexOf(labelInput); + + if (indexOfLabelInput > -1) { + startString = sanitizedLabel.substring(0, indexOfLabelInput); + + middleString = sanitizedLabel.substring(indexOfLabelInput, indexOfLabelInput + labelInput.length); + endString = sanitizedLabel.substring(indexOfLabelInput + labelInput.length); + + format = startString + openTagBold + middleString + closeTagBold + endString; + } + + break; + case PoItemListFilterMode.endsWith: + const lastIndexOfLabelInput = labelLowerCase.lastIndexOf(labelInput); + + if (lastIndexOfLabelInput > -1) { + startString = sanitizedLabel.substring(0, lastIndexOfLabelInput); + middleString = sanitizedLabel.substring(lastIndexOfLabelInput); + + format = startString + openTagBold + middleString + closeTagBold; + } + break; + } + } + + return this.safeHtml(format); + } + + validateForOptionsLabel(): boolean { + return this.comboService && this.searchValue && !this.compareCache && this.shouldMarkLetters; + } + + safeHtml(value): SafeHtml { + return this.sanitized.bypassSecurityTrustHtml(value); + } + + private sanitizeTagHTML(value: string = '') { + return value.replace(/\/g, '>'); + } } diff --git a/projects/ui/src/lib/components/po-listbox/po-listbox-base.component.spec.ts b/projects/ui/src/lib/components/po-listbox/po-listbox-base.component.spec.ts index 3b410dfe6d..6be437b062 100644 --- a/projects/ui/src/lib/components/po-listbox/po-listbox-base.component.spec.ts +++ b/projects/ui/src/lib/components/po-listbox/po-listbox-base.component.spec.ts @@ -109,4 +109,22 @@ describe('PoListboxBaseComponent', () => { }); }); }); + + describe('Methods: ', () => { + it('should return true when items length is greater than 0 and first item has "options"', () => { + component.items = [{ options: ['option1', 'option2'] }]; + + const result = component.isItemListGroup; + + expect(result).toBeTrue(); + }); + + it('should return false when first item does not have "options"', () => { + component.items = [{ someProperty: 'value' }]; + + const result = component.isItemListGroup; + + expect(result).toBeFalse(); + }); + }); }); diff --git a/projects/ui/src/lib/components/po-listbox/po-listbox-base.component.ts b/projects/ui/src/lib/components/po-listbox/po-listbox-base.component.ts index f8324a1960..1ad8286353 100644 --- a/projects/ui/src/lib/components/po-listbox/po-listbox-base.component.ts +++ b/projects/ui/src/lib/components/po-listbox/po-listbox-base.component.ts @@ -9,6 +9,7 @@ import { PoItemListAction } from './po-item-list/interfaces/po-item-list-action. import { PoItemListOptionGroup } from './po-item-list/interfaces/po-item-list-option-group.interface'; import { PoItemListOption } from './po-item-list/interfaces/po-item-list-option.interface'; import { PoListBoxLiterals } from './interfaces/po-listbox-literals.interface'; +import { PoItemListFilterMode } from './enums/po-item-list-filter-mode.enum'; export const poListBoxLiteralsDefault = { en: { @@ -70,27 +71,38 @@ export class PoListBoxBaseComponent { return this._literals || poListBoxLiteralsDefault[this.language]; } + get isItemListGroup(): boolean { + return this.items.length && this.items[0].hasOwnProperty('options'); + } + // parâmetro que pode ser passado para o popup ao clicar em um item @Input('p-param') param?; @Output('p-select-item') selectItem = new EventEmitter(); @Output('p-close') closeEvent = new EventEmitter(); - // MULTISELECT PROPERTIES //output para evento do checkbox @Output('p-change') change = new EventEmitter(); + //output para evento do checkbox + @Output('p-selectcombo-item') selectCombo = new EventEmitter(); + //output para evento do checkbox de selecionar todos @Output('p-change-all') changeAll = new EventEmitter(); + @Output('p-update-infinite-scroll') UpdateInfiniteScroll = new EventEmitter(); + //valor do checkbox de selecionar todos @Input('p-checkboxAllValue') checkboxAllValue: any; // Propriedade que recebe a lista de opções selecionadas. @Input('p-selected-options') selectedOptions: Array = []; + // Propriedade que recebe um item selecionado. + @Input('p-selected-option') selectedOption?: any; + @Input('p-field-value') fieldValue: string = 'value'; @Input('p-field-label') fieldLabel: string = 'label'; @@ -113,10 +125,32 @@ export class PoListBoxBaseComponent { //Propriedades relacionados ao template customizado do multiselect @Input('p-multiselect-template') multiselectTemplate: TemplateRef | any; + @Input('p-template') template: TemplateRef | any; + @Input('p-placeholder-search') placeholderSearch: string; + @Input('p-search-value') searchValue: string; + @Input('p-is-searching') @InputBoolean() isServerSearching: boolean = false; + @Input('p-infinite-loading') @InputBoolean() infiniteLoading: boolean = false; + + @Input('p-infinite-scroll') @InputBoolean() infiniteScroll: boolean = false; + + @Input('p-cache') @InputBoolean() cache: boolean = false; + + @Input('p-infinite-scroll-distance') infiniteScrollDistance: number = 100; + + @Input('p-filter-mode') filterMode: PoItemListFilterMode = PoItemListFilterMode.contains; + + @Input('p-filtering') isFiltering: boolean = false; + + @Input('p-should-mark-letter') shouldMarkLetters: boolean = true; + + @Input('p-compare-cache') compareCache: boolean = false; + + @Input('p-combo-service') comboService: any; + constructor(languageService: PoLanguageService) { this.language = languageService.getShortLanguage(); } diff --git a/projects/ui/src/lib/components/po-listbox/po-listbox.component.html b/projects/ui/src/lib/components/po-listbox/po-listbox.component.html index d9fd93c971..6a6e8b08d7 100644 --- a/projects/ui/src/lib/components/po-listbox/po-listbox.component.html +++ b/projects/ui/src/lib/components/po-listbox/po-listbox.component.html @@ -1,4 +1,10 @@ -
+
-
    -
  • - +
      +
    • - -
    • -
    • - -
    • -
    + + +
  • +
  • + + +
  • +
-
- {{ literals.noItems }} -
+
+ +
+ -
+ + + + -
+ + + +
+ {{ literals.noItems }} +
+
diff --git a/projects/ui/src/lib/components/po-listbox/po-listbox.component.spec.ts b/projects/ui/src/lib/components/po-listbox/po-listbox.component.spec.ts index bfdb94f415..092b8ae215 100644 --- a/projects/ui/src/lib/components/po-listbox/po-listbox.component.spec.ts +++ b/projects/ui/src/lib/components/po-listbox/po-listbox.component.spec.ts @@ -1,8 +1,9 @@ -import { NO_ERRORS_SCHEMA, SimpleChange } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { EventEmitter, NO_ERRORS_SCHEMA, SimpleChange } from '@angular/core'; +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { PoListBoxComponent } from './po-listbox.component'; import * as UtilFunctions from './../../utils/util'; +import { Subscription, debounceTime, fromEvent, of } from 'rxjs'; describe('PoListBoxComponent', () => { let component: PoListBoxComponent; @@ -30,6 +31,102 @@ describe('PoListBoxComponent', () => { }); describe('Methods:', () => { + it('should emit UpdateInfiniteScroll when scroll position is reached', () => { + const target = { + offsetHeight: 100, + scrollTop: 100, + scrollHeight: 10 + }; + component.infiniteScrollDistance = 80; + + const updateInfiniteScroll = spyOn(component['UpdateInfiniteScroll'], 'emit'); + + component.showMoreInfiniteScroll({ target }); + + expect(updateInfiniteScroll).toHaveBeenCalled(); + }); + + it('should call scrollListener and return', done => { + const fakeElement = document.createElement('div'); + const scrollEvent = new Event('scroll'); + spyOn(fromEvent(fakeElement, 'scroll'), 'pipe').and.returnValue(of(scrollEvent).pipe(debounceTime(100))); + + const observable = component.scrollListener(fakeElement); + + observable.subscribe(event => { + expect(event).toBe(scrollEvent); + done(); + }); + + fakeElement.dispatchEvent(scrollEvent); + }); + + it('should include infinite scroll if hasInfiniteScroll returns true', () => { + spyOn(component, 'hasInfiniteScroll').and.returnValue(true); + spyOn(component, 'includeInfiniteScroll'); + + component['checkInfiniteScroll'](); + + expect(component['includeInfiniteScroll']).toHaveBeenCalled(); + }); + + it('should not include infinite scroll if hasInfiniteScroll returns false', () => { + spyOn(component, 'hasInfiniteScroll').and.returnValue(false); + spyOn(component, 'includeInfiniteScroll'); + + component['checkInfiniteScroll'](); + + expect(component['includeInfiniteScroll']).not.toHaveBeenCalled(); + }); + + it('should`nt include infinite scroll and subscribe to scroll event', () => { + const mockScrollEvent = of({}); + spyOn(component, 'scrollListener').and.returnValue(mockScrollEvent); + const showMoreInfiniteScroll = spyOn(component, 'showMoreInfiniteScroll'); + + component['includeInfiniteScroll'](); + + expect(component['subscriptionScrollEvent']).toBeDefined(); + + expect(showMoreInfiniteScroll).toHaveBeenCalled(); + }); + + it('should cancel previous subscription before including infinite scroll', () => { + const mockScrollEvent = of({}); + spyOn(component, 'scrollListener').and.returnValue(mockScrollEvent); + spyOn(component, 'showMoreInfiniteScroll'); + + component['includeInfiniteScroll'](); + + expect(component['subscriptionScrollEvent'].unsubscribe).toHaveBeenCalled(); + expect(component['subscriptionScrollEvent']).toBeDefined(); + expect(component['scrollEvent']).toBe(mockScrollEvent); + expect(component['subscriptionScrollEvent'].closed).toBeTruthy(); + + expect(component.showMoreInfiniteScroll).toHaveBeenCalled(); + }); + + it('should not unsubscribe if there is no previous subscription', () => { + spyOn(component, 'scrollListener').and.returnValue(of({})); + spyOn(component, 'showMoreInfiniteScroll'); + + component['includeInfiniteScroll'](); + + expect(component['subscriptionScrollEvent']).toBeDefined(); + expect(component['scrollEvent']).toBeUndefined(); + }); + + it('should not include infinite scroll if scrollEvent$ is not created', () => { + component['scrollEvent'] = undefined; + spyOn(component, 'scrollListener').and.returnValue(of({})); + spyOn(component, 'showMoreInfiniteScroll'); + spyOn(component, 'includeInfiniteScroll'); + + component['includeInfiniteScroll'](); + + expect(component['includeInfiniteScroll']).toHaveBeenCalled(); + }); + describe('ngAfterViewInit:', () => { it('should have been called', () => { spyOn(component, 'setListBoxMaxHeight'); @@ -239,6 +336,21 @@ describe('PoListBoxComponent', () => { }); }); + describe('ngOnDestroy:', () => { + const mockSubscription: Subscription = new Subscription(); + + it('ngOnDestroy: should unsubscribe if infiniteScroll is true', () => { + component.infiniteScroll = true; + component['subscriptionScrollEvent'] = mockSubscription; + + spyOn(mockSubscription, 'unsubscribe'); + + component.ngOnDestroy(); + + expect(mockSubscription.unsubscribe).toHaveBeenCalled(); + }); + }); + describe('ngOnChanges:', () => { it(`should call 'setListBoxMaxHeight' when has changes`, () => { spyOn(component, 'setListBoxMaxHeight'); @@ -256,17 +368,35 @@ describe('PoListBoxComponent', () => { }); it(`should'n call 'setListBoxMaxHeight' when has changes`, () => { + spyOn(component, 'checkInfiniteScroll'); spyOn(component, 'setListBoxMaxHeight'); component.items = [ { label: 'Item 1', value: 1 }, { label: 'Item 2', value: 2 }, { label: 'Item 3', value: 3 } ]; + component.infiniteScroll = true; component.ngOnChanges(); + expect(component['checkInfiniteScroll']).not.toHaveBeenCalled(); expect(component['setListBoxMaxHeight']).not.toHaveBeenCalled(); }); + + it('should call `checkInfiniteScroll` if infiniteScroll is true', () => { + const checkInfiniteScroll = spyOn(component, 'checkInfiniteScroll'); + component.infiniteScroll = true; + component.visible = true; + component.items = [ + { label: 'Item 1', value: 1 }, + { label: 'Item 2', value: 2 }, + { label: 'Item 3', value: 3 } + ]; + + component.ngOnChanges(); + + expect(checkInfiniteScroll).toHaveBeenCalled(); + }); }); describe('setListBoxMaxHeight', () => { @@ -381,37 +511,91 @@ describe('PoListBoxComponent', () => { }); describe('onKeydown:', () => { - it('should call onSelectItem if event is `enter`', () => { + it('should call onSelectCheckBoxItem if event is `enter` and type is `check`', () => { const item = { label: 'a', value: 'a' }; const eventEnterKey = new KeyboardEvent('keydown', { 'code': 'Enter' }); - - spyOn(component, 'onSelectItem'); + component.type = 'check'; + spyOn(component, 'onSelectCheckBoxItem'); component.onKeyDown(item, eventEnterKey); - expect(component.onSelectItem).toHaveBeenCalled(); + expect(component.onSelectCheckBoxItem).toHaveBeenCalled(); }); - it('should call onSelectCheckBoxItem if event is `enter` and type is `check`', () => { - const item = { label: 'a', value: 'a' }; - const eventEnterKey = new KeyboardEvent('keydown', { 'code': 'Enter' }); - component.type = 'check'; + it('should call onSelectCheckBoxItem when type is "check" and Enter key is pressed', () => { spyOn(component, 'onSelectCheckBoxItem'); - component.onKeyDown(item, eventEnterKey); + const keyboardEvent = new KeyboardEvent('keydown', { + code: 'Enter' + }); - expect(component.onSelectCheckBoxItem).toHaveBeenCalled(); + component.type = 'check'; + component.onKeyDown('item', keyboardEvent); + + expect(component.onSelectCheckBoxItem).toHaveBeenCalledWith('item'); }); - it('should call onSelectItem if event is `space`', () => { - const item = { label: 'a', value: 'a' }; - const eventEnterKey = new KeyboardEvent('keydown', { 'code': 'Space' }); + it('should call comboClicked when type is "option" and Enter key is pressed', () => { + spyOn(component, 'optionClicked'); + + const keyboardEvent = new KeyboardEvent('keydown', { + code: 'Enter' + }); + component.type = 'option'; + component.onKeyDown('item', keyboardEvent); + + expect(component.optionClicked).toHaveBeenCalledWith('item'); + }); + + it('should call onSelectItem when type is "action" and Enter key is pressed', () => { spyOn(component, 'onSelectItem'); - component.onKeyDown(item, eventEnterKey); + const keyboardEvent = new KeyboardEvent('keydown', { + code: 'Enter' + }); + + component.type = 'action'; + component.onKeyDown('item', keyboardEvent); + + expect(component.onSelectItem).toHaveBeenCalledWith('item'); + }); + + it('should emit closeEvent when Escape key is pressed', () => { + spyOn(component.closeEvent, 'emit'); + + const keyboardEvent = new KeyboardEvent('keydown', { + code: 'Escape' + }); + + component.onKeyDown('item', keyboardEvent); + + expect(component.closeEvent.emit).toHaveBeenCalled(); + }); + + it('comboClicked: should emit selectCombo if `p-type` is option', () => { + component.type = 'option'; + spyOn(component.selectCombo, 'emit'); + + component.items = [{ label: 'a', value: 'a' }]; + component.optionClicked(component.items[0]); + + expect(component.selectCombo.emit).toHaveBeenCalled(); + expect(component.items[0]).toEqual({ label: 'a', value: 'a', selected: true }); + }); + + it('comboClicked: should emit selectCombo if `p-type` is option', () => { + spyOn(component.selectCombo, 'emit'); + component.type = 'option'; + component.items = [ + { label: 'option 1', value: 'option 2' }, + { label: 'option 3', value: 'option 4' } + ]; + + component.optionClicked(component.items[1]); - expect(component.onSelectItem).toHaveBeenCalled(); + expect(component.selectCombo.emit).toHaveBeenCalled(); + expect(component.items[0]).toEqual({ label: 'option 1', value: 'option 2', selected: false }); }); it('should`t call onSelectItem if event is not `space` or `enter`', () => { diff --git a/projects/ui/src/lib/components/po-listbox/po-listbox.component.ts b/projects/ui/src/lib/components/po-listbox/po-listbox.component.ts index 4af1643d06..296bed28db 100644 --- a/projects/ui/src/lib/components/po-listbox/po-listbox.component.ts +++ b/projects/ui/src/lib/components/po-listbox/po-listbox.component.ts @@ -1,4 +1,16 @@ -import { AfterViewInit, Component, ElementRef, OnChanges, Renderer2, SimpleChanges, ViewChild } from '@angular/core'; +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + OnChanges, + OnDestroy, + Renderer2, + SimpleChanges, + ViewChild, + forwardRef +} from '@angular/core'; import { Router } from '@angular/router'; import { PoListBoxBaseComponent } from './po-listbox-base.component'; @@ -8,17 +20,27 @@ import { PoItemListOption } from './po-item-list/interfaces/po-item-list-option. import { PoLanguageService } from '../../services/po-language/po-language.service'; import { isExternalLink, isTypeof, openExternalLink } from '../../utils/util'; import { PoSearchListComponent } from './po-search-list/po-search-list.component'; +import { Observable, Subscription, debounceTime, fromEvent, merge } from 'rxjs'; @Component({ selector: 'po-listbox', templateUrl: './po-listbox.component.html' }) -export class PoListBoxComponent extends PoListBoxBaseComponent implements AfterViewInit, OnChanges { +export class PoListBoxComponent extends PoListBoxBaseComponent implements AfterViewInit, OnChanges, OnDestroy { @ViewChild('listbox', { static: true }) listbox: ElementRef; @ViewChild('listboxItemList', { static: false }) listboxItemList: ElementRef; @ViewChild('searchElement') searchElement: PoSearchListComponent; - constructor(private renderer: Renderer2, languageService: PoLanguageService, private router: Router) { + private scrollEvent$: Observable; + private subscriptionScrollEvent: Subscription; + + constructor( + public element: ElementRef, + private renderer: Renderer2, + languageService: PoLanguageService, + private router: Router, + private changeDetector: ChangeDetectorRef + ) { super(languageService); } @@ -30,6 +52,17 @@ export class PoListBoxComponent extends PoListBoxBaseComponent implements AfterV ngOnChanges(changes?: SimpleChanges): void { if (changes?.items) { this.setListBoxMaxHeight(); + this.checkInfiniteScroll(); + } + + if (this.visible && this.infiniteScroll) { + this.checkInfiniteScroll(); + } + } + + ngOnDestroy() { + if (this.subscriptionScrollEvent) { + this.subscriptionScrollEvent?.unsubscribe(); } } @@ -58,11 +91,18 @@ export class PoListBoxComponent extends PoListBoxBaseComponent implements AfterV onKeyDown(itemListAction: PoItemListOption | PoItemListOptionGroup | any, event?: KeyboardEvent) { event.preventDefault(); + if ((event && event.code === 'Enter') || event.code === 'Space') { - if (this.type !== 'check') { - this.onSelectItem(itemListAction); - } else { - this.onSelectCheckBoxItem(itemListAction); + switch (this.type) { + case 'check': + this.onSelectCheckBoxItem(itemListAction); + break; + case 'option': + this.optionClicked(itemListAction); + break; + case 'action': + this.onSelectItem(itemListAction); + break; } } @@ -77,6 +117,15 @@ export class PoListBoxComponent extends PoListBoxBaseComponent implements AfterV } } + optionClicked(option: any) { + if (this.type === 'option') { + this.items.filter(item => + item[this.fieldValue] === option[this.fieldValue] ? (item['selected'] = true) : (item['selected'] = false) + ); + this.selectCombo.emit({ ...option }); + } + } + onSelectCheckBoxItem(option) { const selected = !this.isSelectedItem(option); this.checkboxClicked({ option, selected }); @@ -96,6 +145,37 @@ export class PoListBoxComponent extends PoListBoxBaseComponent implements AfterV this.changeSearch.emit(event); } + showMoreInfiniteScroll({ target }): void { + const scrollPosition = target.offsetHeight + target.scrollTop; + if (scrollPosition >= target.scrollHeight * (this.infiniteScrollDistance / 110)) { + this.UpdateInfiniteScroll.emit(); + } + } + + scrollListener(componentListner: HTMLElement): Observable { + return fromEvent(componentListner, 'scroll').pipe(debounceTime(100)); + } + + protected checkInfiniteScroll(): void { + if (this.hasInfiniteScroll()) { + this.includeInfiniteScroll(); + } + } + + private hasInfiniteScroll(): boolean { + this.changeDetector.detectChanges(); + return this.infiniteScroll && this.listboxItemList?.nativeElement?.scrollHeight; + } + + private includeInfiniteScroll(): void { + this.subscriptionScrollEvent?.unsubscribe(); + this.scrollEvent$ = this.scrollListener(this.listboxItemList?.nativeElement); + + this.subscriptionScrollEvent = this.scrollEvent$.subscribe(event => { + this.showMoreInfiniteScroll(event); + }); + } + protected returnBooleanValue(itemListAction: any, property: string) { return isTypeof(itemListAction[property], 'function') ? itemListAction[property](this.param || itemListAction) diff --git a/projects/ui/src/lib/components/po-menu/po-menu.component.spec.ts b/projects/ui/src/lib/components/po-menu/po-menu.component.spec.ts index 4ef578ff39..71ee5752e6 100644 --- a/projects/ui/src/lib/components/po-menu/po-menu.component.spec.ts +++ b/projects/ui/src/lib/components/po-menu/po-menu.component.spec.ts @@ -185,7 +185,6 @@ describe('PoMenuComponent:', () => { }; component['clickMenuItem'](menuItem); - console.log(component.activeMenuItem); expect(component.activeMenuItem.link).toEqual('./home'); }); diff --git a/projects/ui/src/lib/components/po-modal/po-modal-footer/po-modal-footer.component.spec.ts b/projects/ui/src/lib/components/po-modal/po-modal-footer/po-modal-footer.component.spec.ts index 7dbf689254..020661166e 100644 --- a/projects/ui/src/lib/components/po-modal/po-modal-footer/po-modal-footer.component.spec.ts +++ b/projects/ui/src/lib/components/po-modal/po-modal-footer/po-modal-footer.component.spec.ts @@ -31,8 +31,6 @@ describe('PoModalFooterComponent', () => { const poModalFooter = nativeElement.querySelector('.po-modal-footer-align-right'); - console.log(poModalFooter); - expect(poModalFooter).toBeFalsy(); }); @@ -43,8 +41,6 @@ describe('PoModalFooterComponent', () => { const poModalFooter = nativeElement.querySelector('.po-modal-footer-align-right'); - console.log(poModalFooter); - expect(poModalFooter).toBeTruthy(); }); });