From 776d39d833b3b38c628c4b68aeb670153c18c202 Mon Sep 17 00:00:00 2001 From: CSimoesJr Date: Thu, 26 Oct 2023 17:26:21 -0300 Subject: [PATCH] feat(multiselect): ajustes de merge do Carlos --- .../po-multiselect.component.html | 8 +- .../po-multiselect.component.ts | 154 ++++++++++++++---- .../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 | 10 +- .../lib/components/po-tag/po-tag.component.ts | 30 ++-- 7 files changed, 226 insertions(+), 60 deletions(-) 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-multiselect/po-multiselect.component.html b/projects/ui/src/lib/components/po-field/po-multiselect/po-multiselect.component.html index b1dc9be6a..4d2f48602 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 @@ -13,6 +13,7 @@ [class.po-multiselect-show]="dropdownOpen" >
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 171b713d2..d372ab67f 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,36 +3,52 @@ 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 = [ PoMultiselectFilterService, @@ -110,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; - initializedOnKeyDown: boolean = false; + private subscription: Subscription = new Subscription(); + private enterCloseTag = false; + private initCalculateItems = true; private isCalculateVisibleItems: boolean = true; private cacheOptions: Array; @@ -130,23 +150,21 @@ 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(); } this.initialized = true; } - onKeyDownDropdown(event: KeyboardEvent, index: number) { - if (event.key === 'Escape') { - event.preventDefault(); - this.controlDropdownVisibility(false); - this.inputElement.nativeElement.focus(); - } - } - ngOnChanges(changes: SimpleChanges) { if (this.filterService && (changes.filterService || changes.fieldValue || changes.fieldLabel)) { this.setService(this.filterService); @@ -167,6 +185,7 @@ export class PoMultiselectComponent this.removeListeners(); this.getObjectsByValuesSubscription?.unsubscribe(); this.filterSubject?.unsubscribe(); + this.subscription.unsubscribe(); } /** @@ -202,9 +221,10 @@ export class PoMultiselectComponent } calculateVisibleItems() { + this.hasMoreTag = false; const disclaimersWidth = this.getDisclaimersWidth(); const inputWidth = this.getInputWidth(); - const extraDisclaimerSize = 44; + const extraDisclaimerSize = 63; const disclaimersVisible = disclaimersWidth[0]; this.visibleDisclaimers = []; @@ -229,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(); @@ -239,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(); } @@ -251,6 +279,9 @@ export class PoMultiselectComponent this.changeDetector.detectChanges(); this.adjustContainerPosition(); } + setTimeout(() => { + this.handleKeyboardNavigationTag(); + }, 300); } updateVisibleItems() { @@ -294,29 +325,30 @@ export class PoMultiselectComponent return; } - if (event.keyCode === PoKeyCodeEnum.arrowDown) { + if (event.keyCode === PoKeyCodeEnum.arrowDown && this.visibleDisclaimers.length > 0) { event.preventDefault(); this.controlDropdownVisibility(true); this.dropdown?.listbox?.setFocus(); return; } - if (event.keyCode === PoKeyCodeEnum.enter) { + if (event.keyCode === PoKeyCodeEnum.enter && !this.enterCloseTag) { if (this.visibleDisclaimers.length === 0) { - this.controlDropdownVisibility(true); + this.toggleDropdownVisibility(); this.focus(); return; } else { event.preventDefault(); - this.controlDropdownVisibility(true); + this.toggleDropdownVisibility(); return; } } if (event.keyCode === PoKeyCodeEnum.space) { event.preventDefault(); - this.controlDropdownVisibility(true); + this.toggleDropdownVisibility(); } + this.enterCloseTag = false; } toggleDropdownVisibility() { @@ -331,6 +363,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); @@ -370,22 +410,31 @@ export class PoMultiselectComponent setTimeout(() => this.adjustContainerPosition()); } - closeDisclaimer(value) { - // criar funcao que se value estiver vazio remover de visubleOptionsDropdown todos os que nao estao em visibleDisclaimers - if (!value) { - for (let option of this.visibleDisclaimers) { + 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.selectedOptions.indexOf(option), 1); + this.selectedOptions.splice(this.visibleDisclaimers.length - 1, itemsNotInVisibleDisclaimers.length); this.updateVisibleItems(); this.callOnChange(this.selectedOptions); } } } else { - const index = this.selectedOptions.findIndex(option => option[this.fieldValue] === value); + index = this.selectedOptions.findIndex(option => option[this.fieldValue] === value); this.selectedOptions.splice(index, 1); this.updateVisibleItems(); this.callOnChange(this.selectedOptions); } + + setTimeout(() => { + this.focusOnNextTag(index, event); + }, 300); } wasClickedOnToggle(event: MouseEvent): void { @@ -463,6 +512,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-tag/index.ts b/projects/ui/src/lib/components/po-tag/index.ts index 5793904fa..9f2ac47ee 100644 --- a/projects/ui/src/lib/components/po-tag/index.ts +++ b/projects/ui/src/lib/components/po-tag/index.ts @@ -1,5 +1,6 @@ export * from './enums/po-tag-orientation.enum'; export * from './enums/po-tag-type.enum'; +export * from './interfaces/po-tag-literals.interface'; export * from './po-tag.component'; export * from './po-tag.module'; diff --git a/projects/ui/src/lib/components/po-tag/interfaces/po-tag-literals.interface.ts b/projects/ui/src/lib/components/po-tag/interfaces/po-tag-literals.interface.ts new file mode 100644 index 000000000..f4df88045 --- /dev/null +++ b/projects/ui/src/lib/components/po-tag/interfaces/po-tag-literals.interface.ts @@ -0,0 +1,11 @@ +/** + * @usedBy PoTagComponent + * + * @description + * + * Interface para definição das literais usadas no `po-tag`. + */ +export interface PoTagLiterals { + /** Texto exibido no tooltip indicando remoção da tag. */ + remove?: string; +} 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 80e99fe58..2f4a8eb41 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,30 @@ import { Directive, EventEmitter, Input, Output, TemplateRef } from '@angular/co import { PoColorPaletteEnum } from '../../enums/po-color-palette.enum'; import { convertToBoolean } from '../../utils/util'; +import { PoLanguageService, poLocaleDefault } from '../../../public-api'; 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 * @@ -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 @@ -283,4 +296,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 554fc65eb..73f0a4101 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,4 +1,4 @@ -
+
{{ tagOrientation ? label + ':' : label }}
@@ -34,7 +34,7 @@ > -
+
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 efc4a63f3..5d4c82bac 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,17 +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() { @@ -87,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(); } } @@ -104,7 +98,7 @@ export class PoTagComponent extends PoTagBaseComponent implements OnInit { onKeyPressed(event) { event.preventDefault(); event.stopPropagation(); - this.onClick(); + this.onClick('enter'); } styleTag() { @@ -120,7 +114,7 @@ export class PoTagComponent extends PoTagBaseComponent implements OnInit { } getWidthTag() { - return this.poTag.nativeElement.offsetWidth > 155; + return this.tagContainer.nativeElement.offsetWidth > 155; } setAriaLabel() {