From 64da6bbc4e56d8e12b870a798dc4da8659d2705c Mon Sep 17 00:00:00 2001 From: anliben Date: Tue, 17 Oct 2023 15:45:21 -0300 Subject: [PATCH] =?UTF-8?q?feat(multiselect):=20implementa=20as=20defini?= =?UTF-8?q?=C3=A7=C3=B5es=20do=20AnimaliaDS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementar as definições do handoff do multiselect no PoMultiselect. Fixes DTHFUI-7684 --- .../components/po-field/po-field.module.ts | 10 +- .../po-multiselect-base.component.ts | 17 +- .../po-multiselect-dropdown.component.html | 1 + .../po-multiselect-dropdown.component.spec.ts | 11 +- .../po-multiselect-dropdown.component.ts | 2 - .../po-multiselect-literals.interface.ts | 3 + .../po-multiselect-search.component.html | 13 -- .../po-multiselect-search.component.spec.ts | 99 -------- .../po-multiselect-search.component.ts | 78 ------- .../po-multiselect.component.html | 30 +-- .../po-multiselect.component.spec.ts | 172 ++++++++++++-- .../po-multiselect.component.ts | 217 ++++++++++++++++-- .../po-item-list/po-item-list.component.html | 1 + .../po-item-list/po-item-list.component.ts | 1 - .../ui/src/lib/components/po-tag/index.ts | 1 + .../interfaces/po-tag-literals.interface.ts | 11 + .../po-tag/po-tag-base.component.ts | 72 +++++- .../components/po-tag/po-tag.component.html | 17 +- .../lib/components/po-tag/po-tag.component.ts | 29 +-- projects/ui/src/lib/enums/po-key-code.enum.ts | 8 +- 20 files changed, 489 insertions(+), 304 deletions(-) delete mode 100644 projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-search/po-multiselect-search.component.html delete mode 100644 projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-search/po-multiselect-search.component.spec.ts delete mode 100644 projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect-search/po-multiselect-search.component.ts create mode 100644 projects/ui/src/lib/components/po-tag/interfaces/po-tag-literals.interface.ts 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..4c6188fade 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 { PoTagComponent, 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..7753420ffd 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: 'Выбрать элементы' } }; @@ -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..8509339d80 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 }} - - + [p-disabled]="disabled" + (p-close)="closeTag(disclaimer[fieldValue])" + (p-click)="closeTag(disclaimer[fieldValue], $event)" + >
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..281001c1a0 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 @@ -12,10 +12,11 @@ import { PoFieldContainerComponent } from '../po-field-container/po-field-contai import { PoMultiselectBaseComponent } from '../po-multiselect/po-multiselect-base.component'; import { PoMultiselectComponent } from './po-multiselect.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 { PoKeyCodeEnum } from '../../../enums/po-key-code.enum'; +import { Renderer2 } from '@angular/core'; const poMultiselectFilterServiceStub: PoMultiselectFilter = { getFilteredData: function (params: { property: string; value: string }): Observable> { @@ -33,10 +34,21 @@ describe('PoMultiselectComponent:', () => { let multiSelectService: PoMultiselectFilterService; let httpMock: HttpTestingController; + let disclaimers; + let index; + let renderer: Renderer2; const mockURL = 'rest/tecnologies'; beforeEach(async () => { + disclaimers = document.createElement('div'); + disclaimers.innerHTML = ` +
+
+
+ `; + index = 2; + await TestBed.configureTestingModule({ imports: [HttpClientTestingModule, OverlayModule], declarations: [ @@ -44,14 +56,14 @@ describe('PoMultiselectComponent:', () => { 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'] = () => {}; @@ -115,8 +127,8 @@ describe('PoMultiselectComponent:', () => { component.calculateVisibleItems.call(fakeThis); - expect(fakeThis.visibleDisclaimers.length).toBe(2); - expect(fakeThis.visibleDisclaimers[1].value).toBe(''); + expect(fakeThis.visibleDisclaimers.length).toBe(1); + expect(fakeThis.visibleDisclaimers[1]).toBe(undefined); expect(fakeThis.isCalculateVisibleItems).toBeFalsy(); }); @@ -149,37 +161,77 @@ describe('PoMultiselectComponent:', () => { expect(component.debounceResize).toHaveBeenCalled(); }); - it('should call controlDropdownVisibility arrow to up is pressed', () => { - const fakeEvent = { - preventDefault: () => true, - keyCode: 40 - }; + it('shouuld call focusPreviousDisclaimer', () => { + component['focusPreviousDisclaimer'](disclaimers.children, index); + + const focusedElement = disclaimers.children[index - 1] as HTMLElement; + + expect(disclaimers.contains(focusedElement)).toBe(true); + }); + + it('should call focusNextDisclaimer', () => { + spyOn(component.inputElement.nativeElement, 'focus'); + + component['focusNextDisclaimer'](disclaimers.children, index); + expect(component.inputElement.nativeElement.focus).toHaveBeenCalled(); + }); + + it('should call focusNextDisclaimer when index equals 1', () => { + const fakeEvent = { focus: jasmine.createSpy('focus') }; + spyOn(disclaimers.children[1 + 1], 'querySelector').and.returnValue(fakeEvent); + + component['focusNextDisclaimer'](disclaimers.children, 1); + + expect((disclaimers.children[1] as HTMLElement).outerHTML).toBe('
'); + }); + + it('should call addKeyListener if keyCode entries [37, 39, 40]', () => { + spyOn(component, 'focusPreviousDisclaimer' as any); + spyOn(component, 'focusNextDisclaimer' as any); spyOn(component, 'controlDropdownVisibility'); - component.onKeyDown(fakeEvent); + + const spanElement = document.createElement('span'); + + const eventLeft = new KeyboardEvent('keydown', { keyCode: PoKeyCodeEnum.left }); + component['addKeyListener'](spanElement, disclaimers, index, eventLeft); + spanElement.dispatchEvent(eventLeft); + + const eventRight = new KeyboardEvent('keydown', { keyCode: PoKeyCodeEnum.right }); + component['addKeyListener'](spanElement, disclaimers, index, eventRight); + spanElement.dispatchEvent(eventRight); + + const eventArrowDown = new KeyboardEvent('keydown', { keyCode: PoKeyCodeEnum.arrowDown }); + component['addKeyListener'](spanElement, disclaimers, index, eventArrowDown); + spanElement.dispatchEvent(eventArrowDown); + + expect(component['focusPreviousDisclaimer']).toHaveBeenCalledWith(disclaimers, index); + expect(component['focusNextDisclaimer']).toHaveBeenCalledWith(disclaimers, index); expect(component.controlDropdownVisibility).toHaveBeenCalledWith(true); }); - it('should call controlDropdownVisibility tab is pressed', () => { - const fakeEvent = { - preventDefault: () => true, - keyCode: 9 - }; - + 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); + + component.onKeyDown(event); + + expect(event.preventDefault).toHaveBeenCalled(); expect(component.controlDropdownVisibility).toHaveBeenCalledWith(false); }); - it('shouldn`t call controlDropdownVisibility when another key is pressed', () => { + it('should call controlDropdownVisibility arrow to down is pressed', () => { const fakeEvent = { preventDefault: () => true, - keyCode: 1 + keyCode: 40, + setFocusOnDropdownListbox: () => {} }; - spyOn(component, 'controlDropdownVisibility'); - component.onKeyDown(fakeEvent); - expect(component.controlDropdownVisibility).not.toHaveBeenCalled(); + spyOn(fakeEvent, 'preventDefault'); + const onKeyDown = component.onKeyDown(fakeEvent); + + expect(onKeyDown).toBeUndefined(); + expect(component.controlDropdownVisibility).toHaveBeenCalledWith(true); }); it('should call controlDropdownVisibility when enabled', () => { @@ -228,6 +280,80 @@ describe('PoMultiselectComponent:', () => { expect(component.controlDropdownVisibility).not.toHaveBeenCalled(); }); + it('should call addKeyListener for PoKeyCodeEnum.left or PoKeyCodeEnum.right', () => { + const event = new KeyboardEvent('keydown', { keyCode: PoKeyCodeEnum.left }); + const tagRemovable = document.createElement('span'); + tagRemovable.setAttribute('class', 'po-disclaimer-remove'); + + const disclaimer = document.createElement('span'); + disclaimer.setAttribute('class', 'po-disclaimer'); + disclaimer.appendChild(tagRemovable); + + const disclaimersFake = [disclaimer, disclaimer]; + spyOn(component, 'addKeyListener' as any); + spyOn(component['el'].nativeElement, 'querySelectorAll').and.returnValue(disclaimersFake); + + component.onKeyDown(event); + + expect(component['addKeyListener']).toHaveBeenCalled(); + }); + + it('should return when event keyCode is PoKeyCodeEnum.tab and visibleDisclaimers.length > 1', () => { + const event = new KeyboardEvent('keydown', { keyCode: PoKeyCodeEnum.tab }); + const tagRemovable = document.createElement('span'); + tagRemovable.setAttribute('class', 'po-disclaimer-remove'); + + component.visibleDisclaimers = [tagRemovable, tagRemovable]; + + component.onKeyDown(event); + + expect(component.visibleDisclaimers.length).toEqual(2); + }); + + it('should return when event keyCode is PoKeyCodeEnum.tab and visibleDisclaimers.length < 1', () => { + const event = new KeyboardEvent('keydown', { keyCode: PoKeyCodeEnum.tab }); + component.visibleDisclaimers = []; + + component.onKeyDown(event); + + expect(component.visibleDisclaimers.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.visibleDisclaimers = []; + 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-disclaimer-remove'); + component.visibleDisclaimers = [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 }, 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..a1be98341b 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 '../../../../public-api'; 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 poMultiselectSpaceBetweenDisclaimers = 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,18 @@ export class PoMultiselectComponent @ViewChild('iconElement', { read: ElementRef, static: true }) iconElement: ElementRef; @ViewChild('inputElement', { read: ElementRef, static: true }) inputElement: ElementRef; + literalsTag; disclaimerOffset = 0; dropdownIcon: string = 'po-icon-arrow-down'; dropdownOpen: boolean = false; initialized = false; + hasMoreTag: boolean; positionDisclaimerExtra; timeoutResize; visibleElement = false; - + private subscription: Subscription = new Subscription(); + private enterCloseTag = false; + private initCalculateItems = true; private isCalculateVisibleItems: boolean = true; private cacheOptions: Array; @@ -128,9 +150,15 @@ export class PoMultiselectComponent languageService: PoLanguageService ) { super(languageService); + const language = languageService.getShortLanguage(); + this.literalsTag = { + ...literalsTagRemoveOthers[poLocaleDefault], + ...literalsTagRemoveOthers[language] + }; } ngAfterViewInit() { + // this.literalsTag; if (this.autoFocus) { this.focus(); } @@ -157,6 +185,7 @@ export class PoMultiselectComponent this.removeListeners(); this.getObjectsByValuesSubscription?.unsubscribe(); this.filterSubject?.unsubscribe(); + this.subscription.unsubscribe(); } /** @@ -183,18 +212,19 @@ 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'); + const disclaimers = this.el.nativeElement.querySelectorAll('po-tag'); return Array.from(disclaimers).map(disclaimer => disclaimer['offsetWidth']); } calculateVisibleItems() { + this.hasMoreTag = false; const disclaimersWidth = this.getDisclaimersWidth(); const inputWidth = this.getInputWidth(); - const extraDisclaimerSize = 38; + const extraDisclaimerSize = 63; const disclaimersVisible = disclaimersWidth[0]; this.visibleDisclaimers = []; @@ -203,7 +233,7 @@ export class PoMultiselectComponent let sum = 0; let i = 0; for (i = 0; i < this.selectedOptions.length; i++) { - sum += disclaimersWidth[i]; + sum += disclaimersWidth[i] + poMultiselectSpaceBetweenDisclaimers; this.visibleDisclaimers.push(this.selectedOptions[i]); if (sum > inputWidth) { @@ -219,6 +249,7 @@ export class PoMultiselectComponent return; } + this.hasMoreTag = true; if (sum + extraDisclaimerSize > inputWidth) { this.visibleDisclaimers.splice(-2, 2); const label = '+' + (this.selectedOptions.length + 1 - i).toString(); @@ -229,6 +260,13 @@ export class PoMultiselectComponent this.visibleDisclaimers.push({ [this.fieldValue]: '', [this.fieldLabel]: label }); } } + if (this.initCalculateItems) { + setTimeout(() => { + this.handleKeyboardNavigationTag(); + }, 300); + } + this.initCalculateItems = false; + console.log('Mudei aqui: ', this.literalsTag); } this.changeDetector.markForCheck(); } @@ -241,6 +279,9 @@ export class PoMultiselectComponent this.changeDetector.detectChanges(); this.adjustContainerPosition(); } + setTimeout(() => { + this.handleKeyboardNavigationTag(); + }, 300); } updateVisibleItems() { @@ -271,14 +312,60 @@ export class PoMultiselectComponent } onKeyDown(event?: any) { - if (event.keyCode === PoKeyCodeEnum.arrowUp || event.keyCode === PoKeyCodeEnum.arrowDown) { + if ( + (event.keyCode === PoKeyCodeEnum.tab && this.visibleDisclaimers.length > 1) || + (event.keyCode === PoKeyCodeEnum.tab && this.visibleDisclaimers.length < 1) + ) { + return; + } + + if (event.keyCode === PoKeyCodeEnum.esc) { + event.preventDefault(); + this.controlDropdownVisibility(false); + return; + } + + if (event.keyCode === PoKeyCodeEnum.arrowDown && this.visibleDisclaimers.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.visibleDisclaimers.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; + } + + private focusPreviousDisclaimer(disclaimers: any, index: number) { + const prevDisclaimer = disclaimers[index - 1]; + if (prevDisclaimer) { + prevDisclaimer.querySelector('.po-disclaimer-remove')?.focus(); + } + } + + private focusNextDisclaimer(disclaimers: any, index: number) { + if (index === disclaimers.length - 1) { + this.inputElement.nativeElement.focus(); + } else { + const nextDisclaimer = disclaimers[index + 1]; + if (nextDisclaimer) { + nextDisclaimer.querySelector('.po-disclaimer-remove')?.focus(); + } } } @@ -294,6 +381,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 +428,31 @@ 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 itemsNotInVisibleDisclaimers = this.selectedOptions.filter( + option => !this.visibleDisclaimers.includes(option) + ); + for (const option of this.visibleDisclaimers) { + if (!this.selectedOptions.includes(option)) { + this.selectedOptions.splice(this.visibleDisclaimers.length - 1, itemsNotInVisibleDisclaimers.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 { @@ -365,6 +479,20 @@ export class PoMultiselectComponent ); } + private focusItem() { + this.dropdown.listbox.listboxItemList.nativeElement.focus(); + setTimeout(() => { + let item; + if (this.selectedOptions) { + item = document.querySelector('.po-listbox-item[aria-selected="true"]'); + } else { + item = document.querySelectorAll('.po-listbox-item')[0] as HTMLElement; + } + this.dropdown.listbox.listboxItemList.nativeElement.focus(); + item?.focus(); + }); + } + private applyFilterInFirstClick() { if (this.isFirstFilter) { this.isServerSearching = true; @@ -402,6 +530,53 @@ 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 === 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'); + tagRemoveElements.forEach((tagRemoveElement, index) => { + if (index === initialIndex) { + tagRemoveElements[initialIndex].setAttribute('tabindex', 0); + } else if (tagRemoveElements.length === initialIndex) { + tagRemoveElements[initialIndex - 1].setAttribute('tabindex', 0); + } else { + tagRemoveElements[index].setAttribute('tabindex', -1); + } + this.subscription.add( + fromEvent(tagRemoveElement, 'keydown').subscribe((event: KeyboardEvent) => { + if (event.code === 'Space') { + event.preventDefault(); + event.stopPropagation(); + } + if (event.key === 'ArrowLeft' && index > 0) { + tagRemoveElements[index].setAttribute('tabindex', -1); + tagRemoveElements[index - 1].focus(); + tagRemoveElements[index - 1].setAttribute('tabindex', 0); + } else if (event.key === 'ArrowRight' && index < tagRemoveElements.length - 1) { + tagRemoveElements[index].setAttribute('tabindex', -1); + tagRemoveElements[index + 1].focus(); + tagRemoveElements[index + 1].setAttribute('tabindex', 0); + } + }) + ); + }); + } + 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 }}
+
Object).values(PoColorPaletteEnum); const poTagOrientationDefault = PoTagOrientation.Vertical; +export const PoTagLiteralsDefault = { + en: { + remove: 'Clear' + }, + es: { + remove: 'Eliminar' + }, + pt: { + remove: 'Remover' + }, + ru: { + remove: 'удалять' + } +}; + /** * @description * @@ -59,16 +76,10 @@ export class PoTagBaseComponent { @Input('p-value') value: string; /** - * @deprecated 16.x.x - * * @optional * * @description * - * **Deprecated 16.x.x**. - * - * > Por regras de acessibilidade a tag não terá mais evento de click. Indicamos o uso do `Po-button` ou `Po-link` - * caso deseje esse comportamento. * * Ação que será executada ao clicar sobre o `po-tag` e que receberá como parâmetro um objeto contendo o seu valor e tipo. */ @@ -92,6 +103,8 @@ export class PoTagBaseComponent { private _inverse?: boolean; private _orientation?: PoTagOrientation = poTagOrientationDefault; private _type?: PoTagType; + private _literals: PoTagLiterals; + private language: string; /** * @optional @@ -284,4 +297,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 = { + * noData: 'Sem dados' + * }; + * ``` + * + * 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..73f0a4101f 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.ts b/projects/ui/src/lib/components/po-tag/po-tag.component.ts index 8a5c522ce5..5d4c82bac4 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,9 @@ -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'; const poTagTypeDefault = 'po-tag-' + PoTagType.Info; @@ -36,16 +34,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,16 +81,16 @@ 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(); } } @@ -103,7 +98,7 @@ export class PoTagComponent extends PoTagBaseComponent implements OnInit { 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 }