mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-31 17:38:48 +00:00
[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:
committed by
Denys Vuika
parent
656eeaf017
commit
0cfc5bc1b7
33
lib/content-services/search/components/animations.ts
Normal file
33
lib/content-services/search/components/animations.ts
Normal 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)'))
|
||||
]);
|
@@ -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
|
||||
|
@@ -15,6 +15,14 @@
|
||||
left: -13px;
|
||||
}
|
||||
|
||||
[dir='rtl'] .adf-search-button {
|
||||
right: -13px;
|
||||
}
|
||||
|
||||
[dir='ltr'] .adf-search-button {
|
||||
left: -13px;
|
||||
}
|
||||
|
||||
.adf {
|
||||
|
||||
&-search-fixed-text {
|
||||
|
@@ -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();
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user