diff --git a/projects/ui/src/lib/components/po-field/po-field.module.ts b/projects/ui/src/lib/components/po-field/po-field.module.ts index 992cb84b0f..d2875d47bc 100644 --- a/projects/ui/src/lib/components/po-field/po-field.module.ts +++ b/projects/ui/src/lib/components/po-field/po-field.module.ts @@ -27,7 +27,6 @@ import { PoListBoxModule } from '../po-listbox/po-listbox.module'; import { PoComboComponent } from './po-combo/po-combo.component'; import { PoComboOptionTemplateDirective } from './po-combo/po-combo-option-template/po-combo-option-template.directive'; import { PoMultiselectOptionTemplateDirective } from './po-multiselect/po-multiselect-option-template/po-multiselect-option-template.directive'; -import { PoDatepickerComponent } from './po-datepicker/po-datepicker.component'; import { PoDatepickerRangeComponent } from './po-datepicker-range/po-datepicker-range.component'; import { PoDecimalComponent } from './po-decimal/po-decimal.component'; import { PoEmailComponent } from './po-email/po-email.component'; @@ -36,7 +35,6 @@ import { PoLookupComponent } from './po-lookup/po-lookup.component'; import { PoLookupModalComponent } from './po-lookup/po-lookup-modal/po-lookup-modal.component'; import { PoMultiselectDropdownComponent } from './po-multiselect/po-multiselect-dropdown/po-multiselect-dropdown.component'; import { PoMultiselectComponent } from './po-multiselect/po-multiselect.component'; -import { PoMultiselectSearchComponent } from './po-multiselect/po-multiselect-search/po-multiselect-search.component'; import { PoRichTextBodyComponent } from './po-rich-text/po-rich-text-body/po-rich-text-body.component'; import { PoRichTextComponent } from './po-rich-text/po-rich-text.component'; import { PoRichTextImageModalComponent } from './po-rich-text/po-rich-text-image-modal/po-rich-text-image-modal.component'; @@ -57,6 +55,7 @@ import { PoUrlComponent } from './po-url/po-url.component'; import { PoCheckboxModule } from './po-checkbox/po-checkbox.module'; import { PoSwitchModule } from './po-switch/po-switch.module'; import { PoLabelModule } from '../po-label'; +import { PoTagModule } from '../po-tag'; /** * @description @@ -98,7 +97,8 @@ import { PoLabelModule } from '../po-label'; PoRadioModule, PoLabelModule, PoListBoxModule, - PoSwitchModule + PoSwitchModule, + PoTagModule ], exports: [ PoCheckboxGroupModule, @@ -127,7 +127,8 @@ import { PoLabelModule } from '../po-label'; PoCheckboxModule, PoRadioModule, PoLabelModule, - PoSwitchModule + PoSwitchModule, + PoTagModule ], declarations: [ PoComboComponent, @@ -142,7 +143,6 @@ import { PoLabelModule } from '../po-label'; PoLookupModalComponent, PoMultiselectComponent, PoMultiselectDropdownComponent, - PoMultiselectSearchComponent, PoNumberComponent, PoPasswordComponent, PoRichTextBodyComponent, diff --git a/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-base.component.ts b/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-base.component.ts index ae9c4372ed..ccf87a5b29 100644 --- a/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-base.component.ts +++ b/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-base.component.ts @@ -29,22 +29,26 @@ export const poMultiselectLiteralsDefault = { en: { noData: 'No data found', placeholderSearch: 'Search', - selectAll: 'Select all' + selectAll: 'Select all', + selectItem: 'Select items' }, es: { noData: 'Datos no encontrados', placeholderSearch: 'Busca', - selectAll: 'Seleccionar todo' + selectAll: 'Seleccionar todo', + selectItem: 'Seleccionar items' }, pt: { noData: 'Nenhum dado encontrado', placeholderSearch: 'Buscar', - selectAll: 'Selecionar todos' + selectAll: 'Selecionar todos', + selectItem: 'Selecionar itens' }, ru: { noData: 'Данные не найдены', placeholderSearch: 'искать', - selectAll: 'Выбрать все' + selectAll: 'Выбрать все', + selectItem: 'Выбрать элементы' } }; @@ -151,7 +155,7 @@ export abstract class PoMultiselectBaseComponent implements ControlValueAccessor selectedOptions: Array = []; visibleOptionsDropdown: Array = []; - visibleDisclaimers = []; + visibleTags = []; isServerSearching = false; isFirstFilter: boolean = true; filterSubject = new Subject(); @@ -276,7 +280,9 @@ export abstract class PoMultiselectBaseComponent implements ControlValueAccessor * ``` * const customLiterals: PoMultiselectLiterals = { * noData: 'Nenhum dado encontrado', - * placeholderSearch: 'Buscar' + * placeholderSearch: 'Buscar', + * selectAll: 'Select all', + * selectItem: 'Select items' * }; * ``` * @@ -310,6 +316,7 @@ export abstract class PoMultiselectBaseComponent implements ControlValueAccessor this._literals = poMultiselectLiteralsDefault[this.language]; } } + get literals() { return this._literals || poMultiselectLiteralsDefault[this.language]; } 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 0cd990e7f1..c9c68f84f5 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 @@ -20,6 +20,7 @@ (p-change)="clickItem($event)" (p-change-all)="onClickSelectAll()" (p-change-search)="callChangeSearch($event)" + (p-close)="closeDropdown.emit()" > diff --git a/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-dropdown/po-multiselect-dropdown.component.spec.ts b/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-dropdown/po-multiselect-dropdown.component.spec.ts index 93eb63cfc4..e9f71b2188 100644 --- a/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-dropdown/po-multiselect-dropdown.component.spec.ts +++ b/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-dropdown/po-multiselect-dropdown.component.spec.ts @@ -5,7 +5,6 @@ import { configureTestSuite } from './../../../../util-test/util-expect.spec'; import { PoMultiselectDropdownComponent } from './po-multiselect-dropdown.component'; import { poMultiselectLiteralsDefault } from '../po-multiselect-base.component'; -import { PoMultiselectSearchComponent } from './../po-multiselect-search/po-multiselect-search.component'; describe('PoMultiselectDropdownComponent:', () => { let component: PoMultiselectDropdownComponent; @@ -13,7 +12,7 @@ describe('PoMultiselectDropdownComponent:', () => { configureTestSuite(() => { TestBed.configureTestingModule({ - declarations: [PoMultiselectDropdownComponent, PoMultiselectSearchComponent] + declarations: [PoMultiselectDropdownComponent] }); }); @@ -292,13 +291,5 @@ describe('PoMultiselectDropdownComponent:', () => { expect(fixture.nativeElement.querySelector('.po-listbox-container-no-data')).toBeNull(); }); - - it('shouldn`t show `po-multiselect-search` if `hideSearch` is `true`', () => { - component.hideSearch = true; - - fixture.detectChanges(); - - expect(fixture.debugElement.query(By.css('po-multiselect-search'))).toEqual(null); - }); }); }); diff --git a/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-dropdown/po-multiselect-dropdown.component.ts b/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-dropdown/po-multiselect-dropdown.component.ts index 8e06a5ee54..310d3ed2b8 100644 --- a/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-dropdown/po-multiselect-dropdown.component.ts +++ b/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-dropdown/po-multiselect-dropdown.component.ts @@ -4,7 +4,6 @@ import { Component, ElementRef, EventEmitter, - HostListener, Input, Output, ViewChild, @@ -13,7 +12,6 @@ import { import { PoMultiselectLiterals } from '../../index'; import { PoMultiselectOption } from '../po-multiselect-option.interface'; -import { PoMultiselectSearchComponent } from './../po-multiselect-search/po-multiselect-search.component'; import { PoListBoxComponent } from './../../../po-listbox/po-listbox.component'; /** diff --git a/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-literals.interface.ts b/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-literals.interface.ts index a38a5b9930..5d7da73264 100644 --- a/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-literals.interface.ts +++ b/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-literals.interface.ts @@ -14,4 +14,7 @@ export interface PoMultiselectLiterals { /** Texto exibido no botão de selecionar todos. */ selectAll?: string; + + /** Texto exibido na propriedade placeholder. */ + selectItem?: string; } diff --git a/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-search/po-multiselect-search.component.html b/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-search/po-multiselect-search.component.html deleted file mode 100644 index c746ffaee0..0000000000 --- a/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-search/po-multiselect-search.component.html +++ /dev/null @@ -1,13 +0,0 @@ - diff --git a/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-search/po-multiselect-search.component.spec.ts b/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-search/po-multiselect-search.component.spec.ts deleted file mode 100644 index edb1dfb2bc..0000000000 --- a/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-search/po-multiselect-search.component.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; - -import { expectPropertiesValues } from '../../../../util-test/util-expect.spec'; -import { poLocaleDefault } from '../../../../services/po-language/po-language.constant'; - -import { poMultiselectLiteralsDefault } from '../po-multiselect-base.component'; -import { PoMultiselectSearchComponent } from './po-multiselect-search.component'; - -describe('PoMultiselectSearchComponent:', () => { - let component: PoMultiselectSearchComponent; - let fixture: ComponentFixture; - - beforeEach( - waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [PoMultiselectSearchComponent] - }).compileComponents(); - }) - ); - - beforeEach(() => { - fixture = TestBed.createComponent(PoMultiselectSearchComponent); - component = fixture.componentInstance; - - component.literals = poMultiselectLiteralsDefault[poLocaleDefault]; - }); - - it('should be created', () => { - expect(component).toBeTruthy(); - }); - - it('should emit onChange', () => { - spyOn(component.change, 'emit'); - component.onChange({}); - expect(component.change.emit).toHaveBeenCalled(); - }); - - it('should set focus on input', () => { - component.setFocus(); - expect(document.activeElement.tagName.toLowerCase()).toBe('input'); - }); - - it('should clean input', () => { - component.inputElement.nativeElement.value = 'abc'; - component.clean(); - expect(component.inputElement.nativeElement.value).toBe(''); - }); - - describe('Properties:', () => { - it('p-placeholder: should update property with default placeholder if is setted with invalid values', () => { - const invalidValues = [undefined, 1, {}, [], true, false]; - const defaultPlaceholderSearch = poMultiselectLiteralsDefault.pt.placeholderSearch; - - expectPropertiesValues(component, 'placeholder', invalidValues, defaultPlaceholderSearch); - }); - - it('p-placeholder: should be in portuguese if literals is setted with `pt`', () => { - component.placeholder = ''; - - expect(component.placeholder).toBe(poMultiselectLiteralsDefault.pt.placeholderSearch); - }); - - it('p-placeholder: should be in english if literals is setted with `en`', () => { - component.literals = poMultiselectLiteralsDefault.en; - - component.placeholder = ''; - - expect(component.placeholder).toBe(poMultiselectLiteralsDefault.en.placeholderSearch); - }); - - it('p-placeholder: should be in spanish if literals is setted with `es`', () => { - component.literals = poMultiselectLiteralsDefault.es; - - component.placeholder = ''; - - expect(component.placeholder).toBe(poMultiselectLiteralsDefault.es.placeholderSearch); - }); - - it('p-placeholder: should be poMultiselectLiteralsDefault.placeholderSearch if _placeholder is null', () => { - component.literals = poMultiselectLiteralsDefault.es; - - component['_placeholder'] = null; - - expect(component.placeholder).toBe(poMultiselectLiteralsDefault.es.placeholderSearch); - }); - - it('p-placeholder: should return the "placeholderString"', () => { - const placeholderString = 'placeholder test'; - - expectPropertiesValues(component, 'placeholder', placeholderString, placeholderString); - }); - - it('inputValue: should be the same value as nativeElement.value', () => { - component.inputElement.nativeElement.value = 'test'; - - expect(component.inputValue).toBe('test'); - }); - }); -}); diff --git a/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-search/po-multiselect-search.component.ts b/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-search/po-multiselect-search.component.ts deleted file mode 100644 index 2d2a59a772..0000000000 --- a/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-search/po-multiselect-search.component.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - ElementRef, - EventEmitter, - Input, - Output, - ViewChild -} from '@angular/core'; - -import { isTypeof } from '../../../../utils/util'; - -import { PoMultiselectLiterals } from '../../index'; - -/** - * @docsPrivate - * - * @description - * - * Componente de pesquisa que será criado dentro do dropdown do `po-multiselect`. - */ -@Component({ - selector: 'po-multiselect-search', - templateUrl: './po-multiselect-search.component.html', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class PoMultiselectSearchComponent { - @ViewChild('inputElement', { read: ElementRef, static: true }) inputElement: ElementRef; - - /** Propriedade que recebe as literais definidas no `po-multiselect`. */ - @Input('p-literals') literals?: PoMultiselectLiterals; - - @Input('p-field-value') fieldValue: string; - - /** Evento que será disparado a cada tecla digitada no campo de busca. */ - @Output('p-change') change = new EventEmitter(); - - private _placeholder?: string; - - constructor(private cd: ChangeDetectorRef) {} - - /** - * @optional - * - * @description - * - * Placeholder do campo de pesquisa. - * - * > Caso o mesmo não seja informado, o valor padrão será traduzido com base no idioma do navegador (pt, es e en). - * - * @default `Buscar` - */ - @Input('p-placeholder') set placeholder(placeholder: string) { - this._placeholder = placeholder && isTypeof(placeholder, 'string') ? placeholder : this.literals.placeholderSearch; - } - - get placeholder() { - return this._placeholder || this.literals.placeholderSearch; - } - - get inputValue() { - return this.inputElement.nativeElement.value; - } - - onChange(event) { - this.change.emit({ event: event, [this.fieldValue]: this.inputElement.nativeElement.value }); - } - - setFocus() { - this.inputElement.nativeElement.focus(); - } - - clean() { - this.inputElement.nativeElement.value = ''; - this.cd.markForCheck(); - } -} diff --git a/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect.component.html b/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect.component.html index 50fc3f56ed..91ecc1ea35 100644 --- a/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect.component.html +++ b/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect.component.html @@ -1,4 +1,5 @@ - - {{ placeholder }} + + {{ placeholder ? placeholder : literals.selectItem }} - - +
diff --git a/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect.component.spec.ts b/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect.component.spec.ts index 1c076e42b9..cbe3e35295 100644 --- a/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect.component.spec.ts +++ b/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect.component.spec.ts @@ -1,21 +1,22 @@ -import { ComponentFixture, fakeAsync, flush, TestBed } from '@angular/core/testing'; +import { OverlayModule } from '@angular/cdk/overlay'; import { HttpClient, HttpHandler } from '@angular/common/http'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { ComponentFixture, fakeAsync, flush, TestBed, tick } from '@angular/core/testing'; import { Observable, of, throwError } from 'rxjs'; -import { OverlayModule } from '@angular/cdk/overlay'; import * as UtilsFunction from '../../../utils/util'; -import { PoDisclaimerComponent } from './../../po-disclaimer/po-disclaimer.component'; -import { PoFieldContainerBottomComponent } from './../po-field-container/po-field-container-bottom/po-field-container-bottom.component'; +import { PoTagComponent } from '../../po-tag/po-tag.component'; +import { Renderer2 } from '@angular/core'; +import { PoKeyCodeEnum } from '../../../enums/po-key-code.enum'; import { PoFieldContainerComponent } from '../po-field-container/po-field-container.component'; import { PoMultiselectBaseComponent } from '../po-multiselect/po-multiselect-base.component'; -import { PoMultiselectComponent } from './po-multiselect.component'; +import { PoFieldContainerBottomComponent } from './../po-field-container/po-field-container-bottom/po-field-container-bottom.component'; import { PoMultiselectDropdownComponent } from './po-multiselect-dropdown/po-multiselect-dropdown.component'; -import { PoMultiselectSearchComponent } from './po-multiselect-search/po-multiselect-search.component'; import { PoMultiselectFilter } from './po-multiselect-filter.interface'; -import { PoMultiselectOption } from './po-multiselect-option.interface'; import { PoMultiselectFilterService } from './po-multiselect-filter.service'; +import { PoMultiselectOption } from './po-multiselect-option.interface'; +import { PoMultiselectComponent } from './po-multiselect.component'; const poMultiselectFilterServiceStub: PoMultiselectFilter = { getFilteredData: function (params: { property: string; value: string }): Observable> { @@ -33,25 +34,36 @@ describe('PoMultiselectComponent:', () => { let multiSelectService: PoMultiselectFilterService; let httpMock: HttpTestingController; + let tags; + let index; + let renderer: Renderer2; const mockURL = 'rest/tecnologies'; beforeEach(async () => { + tags = document.createElement('div'); + tags.innerHTML = ` +
+
+
+ `; + index = 2; + await TestBed.configureTestingModule({ imports: [HttpClientTestingModule, OverlayModule], declarations: [ - PoDisclaimerComponent, + PoTagComponent, PoFieldContainerComponent, PoMultiselectComponent, PoMultiselectDropdownComponent, - PoMultiselectSearchComponent, PoFieldContainerBottomComponent ], - providers: [HttpClient, HttpHandler, PoMultiselectFilterService] + providers: [HttpClient, HttpHandler, Renderer2, PoMultiselectFilterService] }).compileComponents(); fixture = TestBed.createComponent(PoMultiselectComponent); component = fixture.componentInstance; + renderer = TestBed.inject(Renderer2); fnAdjustContainerPosition = component['adjustContainerPosition']; component['adjustContainerPosition'] = () => {}; @@ -88,37 +100,43 @@ describe('PoMultiselectComponent:', () => { expect(component.getInputWidth() > 0).toBeTruthy(); }); - it('should get disclaimers width', () => { - component.visibleDisclaimers = [{ label: 'label', value: 1 }]; + it('should get tags width', () => { + component.visibleTags = [{ label: 'label', value: 1 }]; fixture.detectChanges(); - expect(component.getDisclaimersWidth().length).toBeTruthy(); + expect(component.getTagsWidth().length).toBeTruthy(); }); - it('should calc visible items with a tiny space', () => { + it('should calc visible items with a tiny space', fakeAsync(() => { const selectedOptions = [ { label: 'label', value: 1 }, { label: 'label', value: 2 }, { label: 'label', value: 3 } ]; const fakeThis = { - getDisclaimersWidth: () => [100, 100, 100], + getTagsWidth: () => [100, 100, 100], getInputWidth: () => 150, changeDetector: { markForCheck: () => {} }, - visibleDisclaimers: [], + visibleTags: [], selectedOptions: selectedOptions, isCalculateVisibleItems: true, fieldValue: 'value', - fieldLabel: 'label' + fieldLabel: 'label', + initCalculateItems: true, + handleKeyboardNavigationTag: () => {} }; + spyOn(fakeThis, 'handleKeyboardNavigationTag'); component.calculateVisibleItems.call(fakeThis); - expect(fakeThis.visibleDisclaimers.length).toBe(2); - expect(fakeThis.visibleDisclaimers[1].value).toBe(''); + tick(300); + + expect(fakeThis.visibleTags.length).toBe(1); + expect(fakeThis.visibleTags[1]).toBe(undefined); expect(fakeThis.isCalculateVisibleItems).toBeFalsy(); - }); + expect(fakeThis.handleKeyboardNavigationTag).toHaveBeenCalled(); + })); it('should set focus on input', () => { component.initialized = false; @@ -139,47 +157,42 @@ describe('PoMultiselectComponent:', () => { expect(component.initialized).toBeTruthy(); }); - it('should call debounceResize and set visibleDisclaimers', () => { + it('should call debounceResize and set visibleTags', () => { component.selectedOptions = [{ label: 'label', value: 1 }]; - component.visibleDisclaimers = []; + component.visibleTags = []; spyOn(component, 'debounceResize'); component.updateVisibleItems(); - expect(component.visibleDisclaimers.length).toBe(1); + expect(component.visibleTags.length).toBe(1); expect(component.debounceResize).toHaveBeenCalled(); }); - it('should call controlDropdownVisibility arrow to up is pressed', () => { - const fakeEvent = { - preventDefault: () => true, - keyCode: 40 - }; - + it('should call preventDefault and controlDropdownVisibility(false) when keyCode esc is pressed', () => { + const event = { preventDefault: jasmine.createSpy(), keyCode: 27 }; spyOn(component, 'controlDropdownVisibility'); - component.onKeyDown(fakeEvent); - expect(component.controlDropdownVisibility).toHaveBeenCalledWith(true); - }); - it('should call controlDropdownVisibility tab is pressed', () => { - const fakeEvent = { - preventDefault: () => true, - keyCode: 9 - }; + component.onKeyDown(event); - spyOn(component, 'controlDropdownVisibility'); - component.onKeyDown(fakeEvent); + expect(event.preventDefault).toHaveBeenCalled(); expect(component.controlDropdownVisibility).toHaveBeenCalledWith(false); }); - it('shouldn`t call controlDropdownVisibility when another key is pressed', () => { - const fakeEvent = { - preventDefault: () => true, - keyCode: 1 - }; + it('onKeyDown: should call controlDropdownVisibility arrow to down is pressed', () => { + const event = new KeyboardEvent('keydown', { keyCode: PoKeyCodeEnum.arrowDown }); + const tagRemovable = document.createElement('span'); + + tagRemovable.setAttribute('class', 'po-tag-remove'); + component.visibleTags = [tagRemovable, tagRemovable]; spyOn(component, 'controlDropdownVisibility'); - component.onKeyDown(fakeEvent); - expect(component.controlDropdownVisibility).not.toHaveBeenCalled(); + spyOn(event, 'preventDefault'); + + component.controlDropdownVisibility(true); + const onKeyDown = component.onKeyDown(event); + + expect(onKeyDown).toBeUndefined(); + expect(event.preventDefault).toHaveBeenCalled(); + expect(component.controlDropdownVisibility).toHaveBeenCalledWith(true); }); it('should call controlDropdownVisibility when enabled', () => { @@ -228,6 +241,62 @@ describe('PoMultiselectComponent:', () => { expect(component.controlDropdownVisibility).not.toHaveBeenCalled(); }); + it('should return when event keyCode is PoKeyCodeEnum.tab and visibleTags.length > 1', () => { + const event = new KeyboardEvent('keydown', { keyCode: PoKeyCodeEnum.tab }); + const tagRemovable = document.createElement('span'); + tagRemovable.setAttribute('class', 'po-tag-remove'); + + component.visibleTags = [tagRemovable, tagRemovable]; + + component.onKeyDown(event); + + expect(component.visibleTags.length).toEqual(2); + }); + + it('should return when event keyCode is PoKeyCodeEnum.tab and visibleTags.length < 1', () => { + const event = new KeyboardEvent('keydown', { keyCode: PoKeyCodeEnum.tab }); + component.visibleTags = []; + + component.onKeyDown(event); + + expect(component.visibleTags.length).toEqual(0); + }); + + it('should call preventDefault and controlDropdownVisibility(true) when keyCode space is pressed', () => { + const event = { preventDefault: jasmine.createSpy(), keyCode: 32 }; + spyOn(component, 'controlDropdownVisibility'); + + component.onKeyDown(event); + + expect(event.preventDefault).toHaveBeenCalled(); + expect(component.controlDropdownVisibility).toHaveBeenCalledWith(true); + }); + + it('should call focus and controlDropdownVisibility(true) when keyCode enter is pressed', () => { + const event = { preventDefault: jasmine.createSpy(), keyCode: 13 }; + component.visibleTags = []; + spyOn(component, 'controlDropdownVisibility'); + spyOn(component, 'focus'); + + component.onKeyDown(event); + + expect(component.controlDropdownVisibility).toHaveBeenCalledWith(true); + expect(component.focus).toHaveBeenCalled(); + }); + + it('shouldn`t call focus and controlDropdownVisibility(true) when keyCode enter is pressed', () => { + const event = { preventDefault: jasmine.createSpy(), keyCode: 13 }; + const tagRemovable = document.createElement('span'); + tagRemovable.setAttribute('class', 'po-tag-remove'); + component.visibleTags = [tagRemovable, tagRemovable]; + spyOn(component, 'controlDropdownVisibility'); + + component.onKeyDown(event); + + expect(event.preventDefault).toHaveBeenCalled(); + expect(component.controlDropdownVisibility).toHaveBeenCalledWith(true); + }); + it('should call dropdown.scrollTo', () => { component.options = [ { label: 'label', value: 1 }, @@ -254,7 +323,7 @@ describe('PoMultiselectComponent:', () => { expect(component.visibleOptionsDropdown.length).toBe(1); }); - it('should remove item, call updateVisibleItems and callOnChange', () => { + it('closeTag: shouldnt remove item, call updateVisibleItems and callOnChange', () => { component.selectedOptions = [ { label: 'label', value: 1 }, { label: 'label2', value: 2 } @@ -262,12 +331,40 @@ describe('PoMultiselectComponent:', () => { spyOn(component, 'updateVisibleItems'); spyOn(component, 'callOnChange'); - component.closeDisclaimer(1); + component['closeTag'](1, 'click'); expect(component.updateVisibleItems).toHaveBeenCalled(); expect(component.callOnChange).toHaveBeenCalled(); expect(component.selectedOptions[0].value).toBe(2); }); + it('closeTag: should remove item, call updateVisibleItems and callOnChange', () => { + component.visibleTags = [ + { label: 'label10', value: 10 }, + { label: 'label11', value: 11 }, + { label: 'label12', value: 12 }, + { label: 'label13', value: 13 }, + { label: 'label14', value: 14 } + ]; + component.selectedOptions = [ + { label: 'label', value: 1 }, + { label: 'label2', value: 2 }, + { label: 'label3', value: 3 }, + { label: 'label4', value: 4 }, + { label: 'label5', value: 5 }, + { label: 'label6', value: 6 }, + { label: 'label7', value: 7 }, + { label: 'label8', value: 8 }, + { label: 'label9', value: 9 } + ]; + + spyOn(component, 'updateVisibleItems'); + spyOn(component, 'callOnChange'); + component['closeTag']('1+', 'click'); + expect(component.updateVisibleItems).toHaveBeenCalled(); + expect(component.callOnChange).toHaveBeenCalled(); + expect(component.selectedOptions[0].value).toBe(1); + }); + it('should call controlDropdownVisibility in wasClickedOnToggle', () => { component.dropdownOpen = true; fixture.detectChanges(); @@ -470,44 +567,44 @@ describe('PoMultiselectComponent:', () => { }); it(`calculateVisibleItems: should calc visible items and not set 'isCalculateVisibleItems' to false when - disclaimers width is 0`, () => { + tags width is 0`, () => { const selectedOptions = [ { label: 'label', value: 1 }, { label: 'label', value: 2 }, { label: 'label', value: 3 } ]; const fakeThis = { - getDisclaimersWidth: () => [0, 0, 0], + getTagsWidth: () => [0, 0, 0], getInputWidth: () => 100, changeDetector: { markForCheck: () => {} }, - visibleDisclaimers: [], + visibleTags: [], selectedOptions: selectedOptions, isCalculateVisibleItems: true }; component.calculateVisibleItems.call(fakeThis); - expect(fakeThis.visibleDisclaimers.length).toBe(3); + expect(fakeThis.visibleTags.length).toBe(3); expect(fakeThis.isCalculateVisibleItems).toBeTruthy(); }); it(`calculateVisibleItems: should not set isCalculateVisibleItems to false if inputWidth is 0`, () => { const fakeThis = { - getDisclaimersWidth: () => [0, 0, 0], + getTagsWidth: () => [0, 0, 0], getInputWidth: () => 0, changeDetector: { markForCheck: () => {} }, - visibleDisclaimers: [], + visibleTags: [], selectedOptions: [], isCalculateVisibleItems: false }; component.calculateVisibleItems.call(fakeThis); - expect(fakeThis.visibleDisclaimers.length).toBe(0); + expect(fakeThis.visibleTags.length).toBe(0); expect(fakeThis.isCalculateVisibleItems).toBe(false); }); @@ -518,16 +615,16 @@ describe('PoMultiselectComponent:', () => { { label: 'label', value: 3 } ]; const fakeThis = { - getDisclaimersWidth: () => [100, 100, 100], + getTagsWidth: () => [100, 100, 100], getInputWidth: () => 500, - visibleDisclaimers: [], + visibleTags: [], selectedOptions: selectedOptions, isCalculateVisibleItems: true }; component.calculateVisibleItems.call(fakeThis); - expect(fakeThis.visibleDisclaimers.length).toBe(3); + expect(fakeThis.visibleTags.length).toBe(3); expect(fakeThis.isCalculateVisibleItems).toBeFalsy(); }); @@ -538,12 +635,12 @@ describe('PoMultiselectComponent:', () => { { label: 'label', value: 3 } ]; const fakeThis = { - getDisclaimersWidth: () => [100, 100, 100], + getTagsWidth: () => [100, 100, 100], getInputWidth: () => 200, changeDetector: { markForCheck: () => {} }, - visibleDisclaimers: [], + visibleTags: [], selectedOptions: selectedOptions, isCalculateVisibleItems: true, fieldLabel: 'label', @@ -552,22 +649,22 @@ describe('PoMultiselectComponent:', () => { component.calculateVisibleItems.call(fakeThis); - expect(fakeThis.visibleDisclaimers.length).toBe(2); - expect(fakeThis.visibleDisclaimers[1].value).toBe(''); + expect(fakeThis.visibleTags.length).toBe(2); + expect(fakeThis.visibleTags[1].value).toBe(''); expect(fakeThis.isCalculateVisibleItems).toBeFalsy(); }); it('calculateVisibleItems: shouldn`t calc visible items when not have `selectedOptions`', () => { const fakeThis = { - getDisclaimersWidth: () => [0], + getTagsWidth: () => [0], getInputWidth: () => 200, - visibleDisclaimers: [], + visibleTags: [], selectedOptions: [], isCalculateVisibleItems: true }; component.calculateVisibleItems.call(fakeThis); - expect(fakeThis.visibleDisclaimers.length).toBe(0); + expect(fakeThis.visibleTags.length).toBe(0); expect(fakeThis.isCalculateVisibleItems).toBeFalsy(); }); @@ -587,21 +684,49 @@ describe('PoMultiselectComponent:', () => { expect(closeSpy).toHaveBeenCalled(); }); - it('onBlur: should be called when blur event', () => { - component['onModelTouched '] = () => {}; + it('onKeyDownDropdown: should control dropdown visibility', () => { + const event = new KeyboardEvent('keydown', { key: 'Escape' }); + const controlDropdownVisibilitySpy = spyOn(component, 'controlDropdownVisibility'); + + component.onKeyDownDropdown(event, 0); + + expect(controlDropdownVisibilitySpy).toHaveBeenCalledWith(false); + }); + + it('onKeyDownDropdown: should do nothing for non-Escape key', () => { + const event = new KeyboardEvent('keydown', { key: 'Enter' }); + const preventDefaultSpy = spyOn(event, 'preventDefault'); + const controlDropdownVisibilitySpy = spyOn(component, 'controlDropdownVisibility'); + const focusSpy = spyOn(component.inputElement.nativeElement, 'focus'); + + component.onKeyDownDropdown(event, 0); + + expect(preventDefaultSpy).not.toHaveBeenCalled(); + expect(controlDropdownVisibilitySpy).not.toHaveBeenCalled(); + expect(focusSpy).not.toHaveBeenCalled(); + }); + + it('onBlur: should update aria-label if it contains "Unselected"', () => { + const inputEl = component.inputElement.nativeElement; + inputEl.setAttribute('aria-label', 'Unselected'); + component.label = 'New Label'; spyOn(component, 'onModelTouched'); component.onBlur(); + expect(inputEl.getAttribute('aria-label')).toBe('New Label'); expect(component['onModelTouched']).toHaveBeenCalled(); }); - it('onBlur: shouldn´t throw error if onModelTouched is falsy', () => { - component['onModelTouched'] = null; + it('onBlur: should not update aria-label if it does not contain "Unselected"', () => { + const inputElement = component.inputElement.nativeElement; + inputElement.setAttribute('aria-label', 'Something Selected'); + component.label = 'New Label'; + spyOn(component, 'onModelTouched'); - const fnError = () => component.onBlur(); + component.onBlur(); - expect(fnError).not.toThrow(); + expect(component['onModelTouched']).toHaveBeenCalled(); }); it('debounceResize: should call `calculateVisibleItems` after 200 milliseconds', done => { @@ -643,10 +768,10 @@ describe('PoMultiselectComponent:', () => { }, 210); }); - it('updateVisibleItems: should call `debounceResize`, set `visibleDisclaimers` and set `isCalculateVisibleItems` to true', () => { + it('updateVisibleItems: should call `debounceResize`, set `visibleTags` and set `isCalculateVisibleItems` to true', () => { const fakeThis = { selectedOptions: [{ label: 'label', value: 1 }], - visibleDisclaimers: [], + visibleTags: [], inputElement: { nativeElement: { offsetWidth: 0 @@ -660,15 +785,15 @@ describe('PoMultiselectComponent:', () => { component.updateVisibleItems.call(fakeThis); - expect(fakeThis.visibleDisclaimers.length).toBe(1); + expect(fakeThis.visibleTags.length).toBe(1); expect(fakeThis.debounceResize).toHaveBeenCalled(); expect(fakeThis['isCalculateVisibleItems']).toBeTruthy(); }); - it('updateVisibleItems: should call `debounceResize`, do not set `visibleDisclaimers` and set `isCalculateVisibleItems` to true', () => { + it('updateVisibleItems: should call `debounceResize`, do not set `visibleTags` and set `isCalculateVisibleItems` to true', () => { const fakeThis = { selectedOptions: undefined, - visibleDisclaimers: [], + visibleTags: [], inputElement: { nativeElement: { offsetWidth: 0 @@ -682,7 +807,7 @@ describe('PoMultiselectComponent:', () => { component.updateVisibleItems.call(fakeThis); - expect(fakeThis.visibleDisclaimers.length).not.toBe(1); + expect(fakeThis.visibleTags.length).not.toBe(1); expect(fakeThis.debounceResize).toHaveBeenCalled(); expect(fakeThis['isCalculateVisibleItems']).toBeTruthy(); }); @@ -984,18 +1109,18 @@ describe('PoMultiselectComponent:', () => { }); describe('Templates:', () => { - it(`should show placeholder element if contains 'placeholder' and not contains 'visibleDisclaimers'`, () => { + it(`should show placeholder element if contains 'placeholder' and not contains 'visibleTags'`, () => { component.placeholder = 'Placeholder'; - component.visibleDisclaimers = []; + component.visibleTags = []; fixture.detectChanges(); expect(fixture.debugElement.nativeElement.querySelector('.po-multiselect-input-placeholder')).toBeTruthy(); }); - it(`should not show placeholder element if contains 'visibleDisclaimers'`, () => { + it(`should not show placeholder element if contains 'visibleTags'`, () => { component.placeholder = 'Placeholder'; - component.visibleDisclaimers = [{ label: 'One', value: 1 }]; + component.visibleTags = [{ label: 'One', value: 1 }]; fixture.detectChanges(); diff --git a/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect.component.ts b/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect.component.ts index f3f093d119..98b4be7ae5 100644 --- a/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect.component.ts +++ b/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect.component.ts @@ -3,33 +3,51 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + ContentChild, DoCheck, ElementRef, - forwardRef, OnChanges, OnDestroy, Renderer2, SimpleChanges, ViewChild, - ContentChild + forwardRef } from '@angular/core'; import { NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { Observable, of } from 'rxjs'; -import { tap, catchError } from 'rxjs/operators'; +import { Observable, Subscription, fromEvent, of } from 'rxjs'; +import { catchError, tap } from 'rxjs/operators'; -import { isMobile } from './../../../utils/util'; -import { PoControlPositionService } from './../../../services/po-control-position/po-control-position.service'; -import { PoKeyCodeEnum } from './../../../enums/po-key-code.enum'; import { PoLanguageService } from '../../../services/po-language/po-language.service'; +import { PoKeyCodeEnum } from './../../../enums/po-key-code.enum'; +import { PoControlPositionService } from './../../../services/po-control-position/po-control-position.service'; +import { isMobile } from './../../../utils/util'; +import { poLocaleDefault } from '../../../services/po-language/po-language.constant'; import { PoMultiselectBaseComponent } from './po-multiselect-base.component'; -import { PoMultiselectOption } from './po-multiselect-option.interface'; import { PoMultiselectFilterService } from './po-multiselect-filter.service'; import { PoMultiselectOptionTemplateDirective } from './po-multiselect-option-template/po-multiselect-option-template.directive'; +import { PoMultiselectOption } from './po-multiselect-option.interface'; const poMultiselectContainerOffset = 8; const poMultiselectContainerPositionDefault = 'bottom'; +const poMultiselectInputPaddingRight = 52; +const poMultiselectSpaceBetweenTags = 8; + +const literalsTagRemoveOthers = { + pt: { + remove: 'Limpar todos os itens selecionados' + }, + ru: { + remove: 'Очистить все выбранные элементы' + }, + es: { + remove: 'Borrar todos los itens seleccionados' + }, + en: { + remove: 'Clear all selected itens' + } +}; /* istanbul ignore next */ const providers = [ @@ -108,14 +126,16 @@ export class PoMultiselectComponent @ViewChild('iconElement', { read: ElementRef, static: true }) iconElement: ElementRef; @ViewChild('inputElement', { read: ElementRef, static: true }) inputElement: ElementRef; - disclaimerOffset = 0; + literalsTag; dropdownIcon: string = 'po-icon-arrow-down'; dropdownOpen: boolean = false; initialized = false; - positionDisclaimerExtra; + hasMoreTag: boolean; timeoutResize; visibleElement = false; - + private subscription: Subscription = new Subscription(); + private enterCloseTag = false; + private initCalculateItems = true; private isCalculateVisibleItems: boolean = true; private cacheOptions: Array; @@ -128,6 +148,11 @@ export class PoMultiselectComponent languageService: PoLanguageService ) { super(languageService); + const language = languageService.getShortLanguage(); + this.literalsTag = { + ...literalsTagRemoveOthers[poLocaleDefault], + ...literalsTagRemoveOthers[language] + }; } ngAfterViewInit() { @@ -145,7 +170,7 @@ export class PoMultiselectComponent ngDoCheck() { const inputWidth = this.inputElement.nativeElement.offsetWidth; - // Permite que os disclaimers sejam calculados na primeira vez que o componente torna-se visível, + // Permite que os as tags sejam calculados na primeira vez que o componente torna-se visível, // evitando com isso, problemas com Tabs ou Divs que iniciem escondidas. if ((inputWidth && !this.visibleElement && this.initialized) || (inputWidth && this.isCalculateVisibleItems)) { this.debounceResize(); @@ -157,6 +182,7 @@ export class PoMultiselectComponent this.removeListeners(); this.getObjectsByValuesSubscription?.unsubscribe(); this.filterSubject?.unsubscribe(); + this.subscription.unsubscribe(); } /** @@ -183,52 +209,60 @@ export class PoMultiselectComponent } getInputWidth() { - return this.el.nativeElement.querySelector('.po-input').offsetWidth - 40; + return this.el.nativeElement.querySelector('.po-multiselect-input').offsetWidth - poMultiselectInputPaddingRight; } - getDisclaimersWidth() { - const disclaimers = this.el.nativeElement.querySelectorAll('po-disclaimer'); - return Array.from(disclaimers).map(disclaimer => disclaimer['offsetWidth']); + getTagsWidth() { + const tags = this.el.nativeElement.querySelectorAll('po-tag'); + return Array.from(tags).map(tag => tag['offsetWidth']); } calculateVisibleItems() { - const disclaimersWidth = this.getDisclaimersWidth(); + this.hasMoreTag = false; + const tagsWidth = this.getTagsWidth(); const inputWidth = this.getInputWidth(); - const extraDisclaimerSize = 38; - const disclaimersVisible = disclaimersWidth[0]; + const extraTagSize = 63; + const tagsVisible = tagsWidth[0]; - this.visibleDisclaimers = []; + this.visibleTags = []; if (inputWidth > 0) { let sum = 0; let i = 0; for (i = 0; i < this.selectedOptions.length; i++) { - sum += disclaimersWidth[i]; - this.visibleDisclaimers.push(this.selectedOptions[i]); + sum += tagsWidth[i] + poMultiselectSpaceBetweenTags; + this.visibleTags.push(this.selectedOptions[i]); if (sum > inputWidth) { - sum -= disclaimersWidth[i]; + sum -= tagsWidth[i]; this.isCalculateVisibleItems = false; break; } } - if (disclaimersVisible || !this.selectedOptions.length) { + if (tagsVisible || !this.selectedOptions.length) { if (i === this.selectedOptions.length) { this.isCalculateVisibleItems = false; return; } - if (sum + extraDisclaimerSize > inputWidth) { - this.visibleDisclaimers.splice(-2, 2); + this.hasMoreTag = true; + if (sum + extraTagSize > inputWidth) { + this.visibleTags.splice(-2, 2); const label = '+' + (this.selectedOptions.length + 1 - i).toString(); - this.visibleDisclaimers.push({ [this.fieldValue]: '', [this.fieldLabel]: label }); + this.visibleTags.push({ [this.fieldValue]: '', [this.fieldLabel]: label }); } else { - this.visibleDisclaimers.splice(-1, 1); + this.visibleTags.splice(-1, 1); const label = '+' + (this.selectedOptions.length - i).toString(); - this.visibleDisclaimers.push({ [this.fieldValue]: '', [this.fieldLabel]: label }); + this.visibleTags.push({ [this.fieldValue]: '', [this.fieldLabel]: label }); } } + if (this.initCalculateItems) { + setTimeout(() => { + this.handleKeyboardNavigationTag(); + }, 300); + } + this.initCalculateItems = false; } this.changeDetector.markForCheck(); } @@ -241,11 +275,14 @@ export class PoMultiselectComponent this.changeDetector.detectChanges(); this.adjustContainerPosition(); } + setTimeout(() => { + this.handleKeyboardNavigationTag(); + }, 300); } updateVisibleItems() { if (this.selectedOptions) { - this.visibleDisclaimers = [].concat(this.selectedOptions); + this.visibleTags = [].concat(this.selectedOptions); } this.debounceResize(); @@ -267,19 +304,53 @@ export class PoMultiselectComponent } onBlur() { + if ( + typeof this.inputElement.nativeElement.getAttribute('aria-label') === 'string' && + this.inputElement.nativeElement.getAttribute('aria-label').includes('Unselected') + ) { + this.inputElement.nativeElement.setAttribute('aria-label', this.label ? this.label : ''); + } this.onModelTouched?.(); } onKeyDown(event?: any) { - if (event.keyCode === PoKeyCodeEnum.arrowUp || event.keyCode === PoKeyCodeEnum.arrowDown) { + if ( + (event.keyCode === PoKeyCodeEnum.tab && this.visibleTags.length > 1) || + (event.keyCode === PoKeyCodeEnum.tab && this.visibleTags.length < 1) + ) { + return; + } + + if (event.keyCode === PoKeyCodeEnum.esc) { + event.preventDefault(); + this.controlDropdownVisibility(false); + return; + } + + if (event.keyCode === PoKeyCodeEnum.arrowDown && this.visibleTags.length > 0) { event.preventDefault(); this.controlDropdownVisibility(true); + this.dropdown?.listbox?.setFocus(); return; } - if (event.keyCode === PoKeyCodeEnum.tab) { - this.controlDropdownVisibility(false); + if (event.keyCode === PoKeyCodeEnum.enter && !this.enterCloseTag) { + if (this.visibleTags.length === 0) { + this.toggleDropdownVisibility(); + this.focus(); + return; + } else { + event.preventDefault(); + this.toggleDropdownVisibility(); + return; + } } + + if (event.keyCode === PoKeyCodeEnum.space) { + event.preventDefault(); + this.toggleDropdownVisibility(); + } + this.enterCloseTag = false; } toggleDropdownVisibility() { @@ -294,6 +365,14 @@ export class PoMultiselectComponent this.controlDropdownVisibility(!this.dropdownOpen); } + onKeyDownDropdown(event: KeyboardEvent, index: number) { + if (event.key === 'Escape') { + event.preventDefault(); + this.controlDropdownVisibility(false); + this.inputElement.nativeElement.focus(); + } + } + openDropdown(toOpen) { if (toOpen && !this.disabled) { this.controlDropdownVisibility(true); @@ -333,12 +412,29 @@ export class PoMultiselectComponent setTimeout(() => this.adjustContainerPosition()); } - closeDisclaimer(value) { - const index = this.selectedOptions.findIndex(option => option[this.fieldValue] === value); - this.selectedOptions.splice(index, 1); + closeTag(value, event) { + let index; + this.enterCloseTag = true; + if (!value || (typeof value === 'string' && value.includes('+'))) { + index = null; + const itemsNotInVisibleTags = this.selectedOptions.filter(option => !this.visibleTags.includes(option)); + for (const option of this.visibleTags) { + if (!this.selectedOptions.includes(option)) { + this.selectedOptions.splice(this.visibleTags.length - 1, itemsNotInVisibleTags.length); + this.updateVisibleItems(); + this.callOnChange(this.selectedOptions); + } + } + } else { + index = this.selectedOptions.findIndex(option => option[this.fieldValue] === value); + this.selectedOptions.splice(index, 1); + this.updateVisibleItems(); + this.callOnChange(this.selectedOptions); + } - this.updateVisibleItems(); - this.callOnChange(this.selectedOptions); + setTimeout(() => { + this.focusOnNextTag(index, event); + }, 300); } wasClickedOnToggle(event: MouseEvent): void { @@ -402,6 +498,59 @@ export class PoMultiselectComponent this.removeListeners(); } + private focusOnNextTag(indexClosed: number, clickOrEnter: string) { + if (clickOrEnter === 'enter') { + const tagRemoveElements = this.el.nativeElement.querySelectorAll('.po-tag-remove'); + indexClosed = indexClosed || indexClosed === 0 ? indexClosed : tagRemoveElements.length; + if (tagRemoveElements.length === 0) { + this.inputElement.nativeElement.focus(); + this.inputElement.nativeElement.setAttribute('aria-label', `Unselected items ${this.label ? this.label : ''}`); + this.controlDropdownVisibility(true); + } + if (tagRemoveElements.length === indexClosed) { + tagRemoveElements[indexClosed - 1]?.focus(); + } else { + tagRemoveElements[indexClosed]?.focus(); + } + } else { + indexClosed = 0; + } + this.handleKeyboardNavigationTag(indexClosed); + } + + private handleKeyboardNavigationTag(initialIndex = 0) { + this.subscription.unsubscribe(); + this.subscription = new Subscription(); + const tagRemoveElements = this.el.nativeElement.querySelectorAll('.po-tag-remove'); + + const setTabIndex = (index, tabIndex) => { + tagRemoveElements[index].setAttribute('tabindex', tabIndex); + }; + + tagRemoveElements.forEach((tagRemoveElement, index) => { + setTabIndex(index, index === initialIndex ? 0 : -1); + + this.subscription.add( + fromEvent(tagRemoveElement, 'keydown').subscribe((event: KeyboardEvent) => { + if (event.code === 'Space') { + event.preventDefault(); + event.stopPropagation(); + } + + if (event.key === 'ArrowLeft' && index > 0) { + setTabIndex(index, -1); + setTabIndex(index - 1, 0); + tagRemoveElements[index - 1].focus(); + } else if (event.key === 'ArrowRight' && index < tagRemoveElements.length - 1) { + setTabIndex(index, -1); + setTabIndex(index + 1, 0); + tagRemoveElements[index + 1].focus(); + } + }) + ); + }); + } + private initializeListeners(): void { this.clickOutListener = this.renderer.listen('document', 'click', (event: MouseEvent) => { this.wasClickedOnToggle(event); 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 403e8ffd51..06727c39f1 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 @@ -13,6 +13,7 @@ {{ label }}
+
{ - const component = new PoTagBaseComponent(); + const component = new PoTagBaseComponent(new PoLanguageService()); const poTagColors = (Object).values(PoColorPaletteEnum); it('should be created', () => { @@ -131,5 +133,74 @@ describe('PoTagBaseComponent:', () => { component.textColor = 'deep red'; expect(component.customTextColor).toBe(undefined); }); + + it('p-literals: should be in portuguese if browser is setted with an unsupported language', () => { + component['language'] = 'zw'; + + component.literals = {}; + + expect(component.literals).toEqual(PoTagLiteralsDefault[poLocaleDefault]); + }); + + it('p-literals: should be in portuguese if browser is setted with `pt`', () => { + component['language'] = 'pt'; + + component.literals = {}; + + expect(component.literals).toEqual(PoTagLiteralsDefault.pt); + }); + + it('p-literals: should be in english if browser is setted with `en`', () => { + component['language'] = 'en'; + + component.literals = {}; + + expect(component.literals).toEqual(PoTagLiteralsDefault.en); + }); + + it('p-literals: should be in spanish if browser is setted with `es`', () => { + component['language'] = 'es'; + + component.literals = {}; + + expect(component.literals).toEqual(PoTagLiteralsDefault.es); + }); + + it('p-literals: should be in russian if browser is setted with `ru`', () => { + component['language'] = 'ru'; + + component.literals = {}; + + expect(component.literals).toEqual(PoTagLiteralsDefault.ru); + }); + + it('p-literals: should accept custom literals', () => { + component['language'] = poLocaleDefault; + + const customLiterals = Object.assign({}, PoTagLiteralsDefault[poLocaleDefault]); + + // Custom some literals + customLiterals.remove = 'Remove custom'; + + component.literals = customLiterals; + + expect(component.literals).toEqual(customLiterals); + }); + + it('p-literals: should update property with default literals if is setted with invalid values', () => { + const invalidValues = [null, undefined, false, true, '', 'literals', 0, 10, [], [1, 2], () => {}]; + + component['language'] = poLocaleDefault; + + expectPropertiesValues(component, 'literals', invalidValues, PoTagLiteralsDefault[poLocaleDefault]); + }); + + it('p-literals: should update property with default literals if _literals is undefined', () => { + component['language'] = 'pt'; + + component['_literals'] = undefined; + + expect(component.literals).toEqual(PoTagLiteralsDefault.pt); + }); }); }); diff --git a/projects/ui/src/lib/components/po-tag/po-tag-base.component.ts b/projects/ui/src/lib/components/po-tag/po-tag-base.component.ts index 482f3d6b3e..2af507b575 100644 --- a/projects/ui/src/lib/components/po-tag/po-tag-base.component.ts +++ b/projects/ui/src/lib/components/po-tag/po-tag-base.component.ts @@ -3,13 +3,31 @@ import { Directive, EventEmitter, Input, Output, TemplateRef } from '@angular/co import { PoColorPaletteEnum } from '../../enums/po-color-palette.enum'; import { convertToBoolean } from '../../utils/util'; +import { PoLanguageService } from './../../services/po-language/po-language.service'; +import { poLocaleDefault } from '../../services/po-language/po-language.constant'; import { PoTagOrientation } from './enums/po-tag-orientation.enum'; import { PoTagType } from './enums/po-tag-type.enum'; import { PoTagItem } from './interfaces/po-tag-item.interface'; +import { PoTagLiterals } from './interfaces/po-tag-literals.interface'; const poTagColors = (Object).values(PoColorPaletteEnum); const poTagOrientationDefault = PoTagOrientation.Vertical; +export const PoTagLiteralsDefault = { + en: { + remove: 'Clear' + }, + es: { + remove: 'Eliminar' + }, + pt: { + remove: 'Remover' + }, + ru: { + remove: 'удалять' + } +}; + /** * @description * @@ -81,7 +99,7 @@ export class PoTagBaseComponent { * * Ação que sera executada quando clicar sobre o ícone de remover no `po-tag` */ - @Output('p-close') remove: EventEmitter = new EventEmitter(); + @Output('p-close') remove: EventEmitter = new EventEmitter(); public readonly poTagOrientation = PoTagOrientation; public customColor; @@ -92,6 +110,8 @@ export class PoTagBaseComponent { private _inverse?: boolean; private _orientation?: PoTagOrientation = poTagOrientationDefault; private _type?: PoTagType; + private _literals: PoTagLiterals; + private language: string; /** * @optional @@ -284,4 +304,51 @@ export class PoTagBaseComponent { get type(): PoTagType { return this._type; } + + /** + * @optional + * + * @description + * + * Objeto com as literais usadas no `po-tag`. + * + * + * Para utilizar, basta passar a literal customizada: + * + * ``` + * const customLiterals: PoTagLiterals = { + * remove: 'Remover itens' + * }; + * ``` + * + * E para carregar as literais customizadas, basta apenas passar o objeto para o componente: + * + * ``` + * + * + * ``` + * + * > O objeto padrão de literais será traduzido de acordo com o idioma do + * [`PoI18nService`](/documentation/po-i18n) ou do browser. + */ + @Input('p-literals') set literals(value: PoTagLiterals) { + if (value instanceof Object && !(value instanceof Array)) { + this._literals = { + ...PoTagLiteralsDefault[poLocaleDefault], + ...PoTagLiteralsDefault[this.language], + ...value + }; + } else { + this._literals = PoTagLiteralsDefault[this.language]; + } + } + + get literals() { + return this._literals || PoTagLiteralsDefault[this.language]; + } + + constructor(languageService: PoLanguageService) { + this.language = languageService.getShortLanguage(); + } } diff --git a/projects/ui/src/lib/components/po-tag/po-tag.component.html b/projects/ui/src/lib/components/po-tag/po-tag.component.html index 5e4a24f012..1365fac41b 100644 --- a/projects/ui/src/lib/components/po-tag/po-tag.component.html +++ b/projects/ui/src/lib/components/po-tag/po-tag.component.html @@ -1,16 +1,11 @@ -
+
{{ tagOrientation ? label + ':' : label }}
-
+
diff --git a/projects/ui/src/lib/components/po-tag/po-tag.component.spec.ts b/projects/ui/src/lib/components/po-tag/po-tag.component.spec.ts index 2a293bec79..cf05d9f6d5 100644 --- a/projects/ui/src/lib/components/po-tag/po-tag.component.spec.ts +++ b/projects/ui/src/lib/components/po-tag/po-tag.component.spec.ts @@ -139,7 +139,11 @@ describe('PoTagComponent:', () => { component.onClick(); - expect(component.click.emit).toHaveBeenCalledWith({ 'value': component.value, 'type': component.type }); + expect(component.click.emit).toHaveBeenCalledWith({ + 'value': component.value, + 'type': component.type, + 'event': 'click' + }); }); it('onKeyPressed: should call `onClick` if the event `keydown` is used with `enter` key.', () => { @@ -192,7 +196,7 @@ describe('PoTagComponent:', () => { component.onClose(); - expect(component.click.emit).toHaveBeenCalledWith(null); + expect(component.click.emit).toHaveBeenCalledWith('click'); }); it('onRemove: Should remove the element if not disabled', () => { diff --git a/projects/ui/src/lib/components/po-tag/po-tag.component.ts b/projects/ui/src/lib/components/po-tag/po-tag.component.ts index 8a5c522ce5..9794aa7613 100644 --- a/projects/ui/src/lib/components/po-tag/po-tag.component.ts +++ b/projects/ui/src/lib/components/po-tag/po-tag.component.ts @@ -1,11 +1,10 @@ -import { PoLanguageService } from './../../services/po-language/po-language.service'; import { ChangeDetectionStrategy, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { PoLanguageService } from './../../services/po-language/po-language.service'; -import { PoTagBaseComponent } from './po-tag-base.component'; import { PoTagIcon } from './enums/po-tag-icon.enum'; -import { PoTagItem } from './interfaces/po-tag-item.interface'; import { PoTagType } from './enums/po-tag-type.enum'; -import { PoTagLiterals } from './po-tag.literals'; +import { PoTagBaseComponent } from './po-tag-base.component'; +import { PoTagLiterals } from './interfaces/po-tag-literals.interface'; const poTagTypeDefault = 'po-tag-' + PoTagType.Info; @@ -36,16 +35,13 @@ const poTagTypeDefault = 'po-tag-' + PoTagType.Info; }) export class PoTagComponent extends PoTagBaseComponent implements OnInit { @ViewChild('tagContainer', { static: true }) tagContainer: ElementRef; + @ViewChild('tagClose', { static: true }) tagClose: ElementRef; + @ViewChild('poTag', { static: true }) poTag: ElementRef; isClickable: boolean; - literals: any; - - constructor(private el: ElementRef, private languageService: PoLanguageService) { - super(); - const language = this.languageService.getShortLanguage(); - this.literals = { - ...PoTagLiterals[language] - }; + + constructor(private el: ElementRef, languageService: PoLanguageService) { + super(languageService); } ngOnInit() { @@ -86,24 +82,25 @@ export class PoTagComponent extends PoTagBaseComponent implements OnInit { return this.orientation === this.poTagOrientation.Horizontal; } - onClick() { + onClick(event = 'click') { if (!this.removable && !this.disabled) { - const submittedTagItem: PoTagItem = { value: this.value, type: this.type }; + const submittedTagItem = { value: this.value, type: this.type, event: event }; this.click.emit(submittedTagItem); } } - onClose() { + onClose(event = 'click') { if (!this.disabled) { - this.click.emit(null); + this.click.emit(event); this.onRemove(); + this.remove.emit(event); } } onKeyPressed(event) { event.preventDefault(); event.stopPropagation(); - this.onClick(); + this.onClick('enter'); } styleTag() { diff --git a/projects/ui/src/lib/enums/po-key-code.enum.ts b/projects/ui/src/lib/enums/po-key-code.enum.ts index ac81f42a7e..be21916969 100644 --- a/projects/ui/src/lib/enums/po-key-code.enum.ts +++ b/projects/ui/src/lib/enums/po-key-code.enum.ts @@ -34,5 +34,11 @@ export enum PoKeyCodeEnum { space = 32, /** Tab */ - tab = 9 + tab = 9, + + /** Tela Arrow Left */ + left = 37, + + /** Tela Arrow Right */ + right = 39 }