[ADF-4625] Search Input does not respect RTL mode (#4806)

* configurable animation

* bind state attribute to value

* configure animation state based on direction

* update tests

* lint

* direction style
This commit is contained in:
Cilibiu Bogdan
2019-06-04 11:15:44 +03:00
committed by Denys Vuika
parent 656eeaf017
commit 0cfc5bc1b7
5 changed files with 198 additions and 36 deletions

View File

@@ -0,0 +1,33 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { trigger, transition, animate, style, state, AnimationTriggerMetadata } from '@angular/animations';
export const searchAnimation: AnimationTriggerMetadata = trigger('transitionMessages', [
state('active', style({
'margin-left': '{{ margin-left }}px',
'margin-right': '{{ margin-right }}px',
'transform': '{{ transform }}'
}), { params: { 'margin-left': 0, 'margin-right': 0, 'transform': 'translateX(0%)' } }),
state('inactive', style({
'margin-left': '{{ margin-left }}px',
'margin-right': '{{ margin-right }}px',
'transform': '{{ transform }}'
}), { params: { 'margin-left': 0, 'margin-right': 0, 'transform': 'translateX(0%)' } }),
state('no-animation', style({ transform: 'translateX(0%)', width: '100%' })),
transition('active <=> inactive', animate('400ms cubic-bezier(0.25, 0.8, 0.25, 1)'))
]);

View File

@@ -1,4 +1,4 @@
<div class="adf-search-container" [attr.state]="subscriptAnimationState">
<div class="adf-search-container" [attr.state]="subscriptAnimationState.value">
<div *ngIf="isLoggedIn()" [@transitionMessages]="subscriptAnimationState"
(@transitionMessages.done)="applySearchFocus($event)">
<button mat-icon-button

View File

@@ -15,6 +15,14 @@
left: -13px;
}
[dir='rtl'] .adf-search-button {
right: -13px;
}
[dir='ltr'] .adf-search-button {
left: -13px;
}
.adf {
&-search-fixed-text {

View File

@@ -18,7 +18,7 @@
import { Component, DebugElement, ViewChild } from '@angular/core';
import { async, discardPeriodicTasks, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { AuthenticationService, SearchService, setupTestBed, CoreModule } from '@alfresco/adf-core';
import { AuthenticationService, SearchService, setupTestBed, CoreModule, UserPreferencesService } from '@alfresco/adf-core';
import { ThumbnailService } from '@alfresco/adf-core';
import { noResult, results } from '../../mock';
import { SearchControlComponent } from './search-control.component';
@@ -66,6 +66,7 @@ describe('SearchControlComponent', () => {
let elementCustom: HTMLElement;
let componentCustom: SimpleSearchTestCustomEmptyComponent;
let searchServiceSpy: any;
let userPreferencesService: UserPreferencesService;
setupTestBed({
imports: [
@@ -81,7 +82,8 @@ describe('SearchControlComponent', () => {
],
providers: [
ThumbnailService,
SearchService
SearchService,
UserPreferencesService
]
});
@@ -90,6 +92,7 @@ describe('SearchControlComponent', () => {
debugElement = fixture.debugElement;
searchService = TestBed.get(SearchService);
authService = TestBed.get(AuthenticationService);
userPreferencesService = TestBed.get(UserPreferencesService);
spyOn(authService, 'isEcmLoggedIn').and.returnValue(true);
component = fixture.componentInstance;
element = fixture.nativeElement;
@@ -187,7 +190,7 @@ describe('SearchControlComponent', () => {
});
it('should not have animation', () => {
expect(component.subscriptAnimationState).toBe('no-animation');
expect(component.subscriptAnimationState.value).toBe('no-animation');
});
});
@@ -466,12 +469,12 @@ describe('SearchControlComponent', () => {
tick(100);
const searchButton: DebugElement = debugElement.query(By.css('#adf-search-button'));
component.subscriptAnimationState = 'active';
component.subscriptAnimationState.value = 'active';
fixture.detectChanges();
tick(100);
expect(component.subscriptAnimationState).toBe('active');
expect(component.subscriptAnimationState.value).toBe('active');
searchButton.triggerEventHandler('click', null);
fixture.detectChanges();
@@ -481,7 +484,7 @@ describe('SearchControlComponent', () => {
tick(100);
expect(component.subscriptAnimationState).toBe('inactive');
expect(component.subscriptAnimationState.value).toBe('inactive');
discardPeriodicTasks();
}));
@@ -498,7 +501,7 @@ describe('SearchControlComponent', () => {
tick(100);
expect(component.subscriptAnimationState).toBe('active');
expect(component.subscriptAnimationState.value).toBe('active');
discardPeriodicTasks();
}));
@@ -526,12 +529,12 @@ describe('SearchControlComponent', () => {
tick(100);
const searchButton: DebugElement = debugElement.query(By.css('#adf-search-button'));
component.subscriptAnimationState = 'active';
component.subscriptAnimationState.value = 'active';
fixture.detectChanges();
tick(100);
expect(component.subscriptAnimationState).toBe('active');
expect(component.subscriptAnimationState.value).toBe('active');
searchButton.triggerEventHandler('click', null);
fixture.detectChanges();
@@ -545,7 +548,7 @@ describe('SearchControlComponent', () => {
tick(100);
expect(component.subscriptAnimationState).toBe('inactive');
expect(component.subscriptAnimationState.value).toBe('inactive');
discardPeriodicTasks();
}));
@@ -555,12 +558,12 @@ describe('SearchControlComponent', () => {
tick(100);
const inputDebugElement = debugElement.query(By.css('#adf-control-input'));
component.subscriptAnimationState = 'active';
component.subscriptAnimationState.value = 'active';
fixture.detectChanges();
tick(100);
expect(component.subscriptAnimationState).toBe('active');
expect(component.subscriptAnimationState.value).toBe('active');
inputDebugElement.triggerEventHandler('keyup.escape', {});
@@ -569,7 +572,7 @@ describe('SearchControlComponent', () => {
tick(100);
expect(component.subscriptAnimationState).toBe('inactive');
expect(component.subscriptAnimationState.value).toBe('inactive');
discardPeriodicTasks();
}));
});
@@ -598,7 +601,7 @@ describe('SearchControlComponent', () => {
spyOn(component, 'isSearchBarActive').and.returnValue(true);
searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results))));
const clickDisposable = component.optionClicked.subscribe((item) => {
expect(component.subscriptAnimationState).toBe('inactive');
expect(component.subscriptAnimationState.value).toBe('inactive');
clickDisposable.unsubscribe();
done();
});
@@ -662,4 +665,92 @@ describe('SearchControlComponent', () => {
});
});
});
describe('directionality', () => {
describe('initial animation state', () => {
beforeEach(() => {
component.expandable = true;
});
it('should have positive transform translation', () => {
userPreferencesService.setWithoutStore('textOrientation', 'ltr');
fixture.detectChanges();
expect(component.subscriptAnimationState.params.transform).toBe('translateX(82%)');
});
it('should have negative transform translation ', () => {
userPreferencesService.setWithoutStore('textOrientation', 'rtl');
fixture.detectChanges();
expect(component.subscriptAnimationState.params.transform).toBe('translateX(-82%)');
});
});
describe('toggle animation', () => {
beforeEach(() => {
fixture.detectChanges();
});
it('should have margin-left set when active and direction is ltr', fakeAsync(() => {
userPreferencesService.setWithoutStore('textOrientation', 'ltr');
fixture.detectChanges();
const searchButton: DebugElement = debugElement.query(By.css('#adf-search-button'));
searchButton.triggerEventHandler('click', null);
tick(100);
fixture.detectChanges();
tick(100);
expect(component.subscriptAnimationState.params).toEqual({ 'margin-left': 13 });
discardPeriodicTasks();
}));
it('should have positive transform translateX set when inactive and direction is ltr', fakeAsync(() => {
userPreferencesService.setWithoutStore('textOrientation', 'ltr');
component.subscriptAnimationState.value = 'active';
fixture.detectChanges();
const searchButton: DebugElement = debugElement.query(By.css('#adf-search-button'));
searchButton.triggerEventHandler('click', null);
tick(100);
fixture.detectChanges();
tick(100);
expect(component.subscriptAnimationState.params).toEqual({ 'transform': 'translateX(82%)' });
discardPeriodicTasks();
}));
it('should have margin-right set when active and direction is rtl', fakeAsync(() => {
userPreferencesService.setWithoutStore('textOrientation', 'rtl');
fixture.detectChanges();
const searchButton: DebugElement = debugElement.query(By.css('#adf-search-button'));
searchButton.triggerEventHandler('click', null);
tick(100);
fixture.detectChanges();
tick(100);
expect(component.subscriptAnimationState.params).toEqual({ 'margin-right': 13 });
discardPeriodicTasks();
}));
it('should have negative transform translateX set when inactive and direction is rtl', fakeAsync(() => {
userPreferencesService.setWithoutStore('textOrientation', 'rtl');
component.subscriptAnimationState.value = 'active';
fixture.detectChanges();
const searchButton: DebugElement = debugElement.query(By.css('#adf-search-button'));
searchButton.triggerEventHandler('click', null);
tick(100);
fixture.detectChanges();
tick(100);
expect(component.subscriptAnimationState.params).toEqual({ 'transform': 'translateX(-82%)' });
discardPeriodicTasks();
}));
});
});
});

View File

@@ -15,32 +15,23 @@
* limitations under the License.
*/
import { AuthenticationService, ThumbnailService } from '@alfresco/adf-core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { AuthenticationService, ThumbnailService, UserPreferencesService } from '@alfresco/adf-core';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output,
QueryList, ViewEncapsulation, ViewChild, ViewChildren, ElementRef, TemplateRef, ContentChild } from '@angular/core';
import { NodeEntry } from '@alfresco/js-api';
import { Observable, Subject } from 'rxjs';
import { SearchComponent } from './search.component';
import { searchAnimation } from './animations';
import { MatListItem } from '@angular/material';
import { EmptySearchResultComponent } from './empty-search-result.component';
import { debounceTime, filter } from 'rxjs/operators';
import { debounceTime, filter, takeUntil } from 'rxjs/operators';
import { Direction } from '@angular/cdk/bidi';
@Component({
selector: 'adf-search-control',
templateUrl: './search-control.component.html',
styleUrls: ['./search-control.component.scss'],
animations: [
trigger('transitionMessages', [
state('active', style({ transform: 'translateX(0%)', 'margin-left': '13px' })),
state('inactive', style({ transform: 'translateX(82%)'})),
state('no-animation', style({ transform: 'translateX(0%)', width: '100%' })),
transition('inactive => active',
animate('300ms cubic-bezier(0.55, 0, 0.55, 0.2)')),
transition('active => inactive',
animate('300ms cubic-bezier(0.55, 0, 0.55, 0.2)'))
])
],
animations: [searchAnimation],
encapsulation: ViewEncapsulation.None,
host: { class: 'adf-search-control' }
})
@@ -103,20 +94,25 @@ export class SearchControlComponent implements OnInit, OnDestroy {
emptySearchTemplate: EmptySearchResultComponent;
searchTerm: string = '';
subscriptAnimationState: string;
subscriptAnimationState: any;
noSearchResultTemplate: TemplateRef <any> = null;
private toggleSearch = new Subject<any>();
private focusSubject = new Subject<FocusEvent>();
private onDestroy$ = new Subject<boolean>();
private dir = 'ltr';
constructor(public authService: AuthenticationService,
private thumbnailService: ThumbnailService) {
constructor(
public authService: AuthenticationService,
private thumbnailService: ThumbnailService,
private userPreferencesService: UserPreferencesService
) {
this.toggleSearch.asObservable().pipe(debounceTime(200)).subscribe(() => {
if (this.expandable) {
this.subscriptAnimationState = this.subscriptAnimationState === 'inactive' ? 'active' : 'inactive';
this.subscriptAnimationState = this.toggleAnimation();
if (this.subscriptAnimationState === 'inactive') {
if (this.subscriptAnimationState.value === 'inactive') {
this.searchTerm = '';
this.searchAutocomplete.resetResults();
if ( document.activeElement.id === this.searchInput.nativeElement.id) {
@@ -134,7 +130,15 @@ export class SearchControlComponent implements OnInit, OnDestroy {
}
ngOnInit() {
this.subscriptAnimationState = this.expandable ? 'inactive' : 'no-animation';
this.userPreferencesService
.select('textOrientation')
.pipe(takeUntil(this.onDestroy$))
.subscribe((direction: Direction) => {
this.dir = direction;
this.subscriptAnimationState = this.getAnimationState();
});
this.subscriptAnimationState = this.getAnimationState();
this.setupFocusEventHandlers();
}
@@ -152,6 +156,9 @@ export class SearchControlComponent implements OnInit, OnDestroy {
this.toggleSearch.complete();
this.toggleSearch = null;
}
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
isLoggedIn(): boolean {
@@ -189,7 +196,7 @@ export class SearchControlComponent implements OnInit, OnDestroy {
}
isSearchBarActive() {
return this.subscriptAnimationState === 'active' && this.liveSearchEnabled;
return this.subscriptAnimationState.value === 'active' && this.liveSearchEnabled;
}
toggleSearchBar() {
@@ -266,4 +273,27 @@ export class SearchControlComponent implements OnInit, OnDestroy {
return node.previousElementSibling;
}
private toggleAnimation() {
if (this.dir === 'ltr') {
return this.subscriptAnimationState.value === 'inactive' ?
{ value: 'active', params: { 'margin-left': 13 } } :
{ value: 'inactive', params: { 'transform': 'translateX(82%)' } };
} else {
return this.subscriptAnimationState.value === 'inactive' ?
{ value: 'active', params: { 'margin-right': 13 } } :
{ value: 'inactive', params: { 'transform': 'translateX(-82%)' } };
}
}
private getAnimationState() {
if (this.dir === 'ltr') {
return this.expandable ?
{ value: 'inactive', params: { 'transform': 'translateX(82%)' } } :
{ value: 'no-animation' };
} else {
return this.expandable ?
{ value: 'inactive', params: { 'transform': 'translateX(-82%)' } } :
{ value: 'no-animation' };
}
}
}