#806 Prevent the filter apply button from jumping on open & close

This commit is contained in:
Sebastian
2020-07-16 15:19:35 +02:00
parent 3fa8fc8fd1
commit 386dd9263d
8 changed files with 274 additions and 13 deletions

View File

@@ -38,7 +38,12 @@
</div>
</ng-container>
<div class="apply-filter-wrapper">
<div
class="apply-filter-wrapper"
centerBottomDuringTransition
[inTransition]="inTransition"
className="in-transition"
>
<button
class="isa-btn isa-btn-primary isa-btn-pill btn-apply-filters text-nowrap"
(click)="applyFilters()"

View File

@@ -13,6 +13,13 @@
.apply-filter-wrapper {
text-align: center;
padding-bottom: 5px;
&.in-transition {
position: absolute;
transform: translate(-50%);
bottom: 0;
left: 50%;
}
}
.btn-apply-filters {

View File

@@ -1,15 +1,46 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RemissionListFilterComponent } from './remission-list-filter.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { IsaOverlayRef } from 'apps/sales/src/app/core/overlay';
import { RemissionFilterService } from '../../services/remission-filter.service';
import { RemissionService } from '@isa/remission';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { NgxsModule } from '@ngxs/store';
import { BehaviorSubject } from 'rxjs';
describe('RemissionListFilterComponent', () => {
fdescribe('RemissionListFilterComponent', () => {
let fixture: ComponentFixture<RemissionListFilterComponent>;
let component: RemissionListFilterComponent;
let filterService: RemissionFilterService;
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule, NgxsModule.forRoot([])],
providers: [
{
provide: IsaOverlayRef,
useValue: jasmine.createSpyObj('isaOverlayRef', ['close']),
},
{
provide: RemissionFilterService,
useValue: jasmine.createSpy('remissionFilterService'),
},
{
provide: RemissionService,
useValue: jasmine.createSpy('remissionService'),
},
],
declarations: [RemissionListFilterComponent],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
});
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [RemissionListFilterComponent],
}).compileComponents();
fixture = TestBed.createComponent(RemissionListFilterComponent);
component = fixture.componentInstance;
filterService = TestBed.get(RemissionFilterService);
spyOn(component, 'ngOnInit').and.callFake(() => {});
fixture.detectChanges();
});
@@ -17,4 +48,47 @@ describe('RemissionListFilterComponent', () => {
it('should create the component', () => {
expect(fixture).toBeTruthy();
});
describe('close', () => {
beforeEach(() => {
spyOn(component, 'close').and.callThrough();
filterService.resetFilters$ = new BehaviorSubject(null);
fixture.detectChanges();
});
it('should set inTransition to true', () => {
component.close();
fixture.detectChanges();
const result = component.inTransition;
expect(result).toBeTruthy();
});
});
describe('closeAnimationDone', () => {
beforeEach(() => {
spyOn(component, 'closeAnimationDone').and.callThrough();
fixture.detectChanges();
});
it('should set inTransition to false if animation is complete (in)', () => {
component.animate = 'in';
component.inTransition = true;
fixture.detectChanges();
component.closeAnimationDone();
fixture.detectChanges();
expect(component.inTransition).toBeFalsy();
});
it('should not set in transition if animation out is already complete', () => {
component.animate = 'out';
component.inTransition = true;
fixture.detectChanges();
component.closeAnimationDone();
fixture.detectChanges();
expect(component.inTransition).toBeTruthy();
});
});
});

View File

@@ -40,7 +40,8 @@ import { slideIn } from 'apps/sales/src/app/core/overlay';
animations: [slideIn],
})
export class RemissionListFilterComponent implements OnInit, OnDestroy {
animate = 'in';
animate: 'in' | 'out' = 'in';
inTransition = true;
@ViewChild('selectionContainer', { static: false })
selectionContainer: ElementRef;
@@ -211,11 +212,21 @@ export class RemissionListFilterComponent implements OnInit, OnDestroy {
close() {
this.animate = 'out';
this.filterService.resetFilters$.next();
this.setTransition(true);
}
closeAnimationDone() {
if (this.animate === 'in') {
this.setTransition(false);
}
if (this.animate === 'out') {
this.overlayRef.close();
}
}
private setTransition(isTransitioning: boolean) {
this.inTransition = isTransitioning;
}
}

View File

@@ -0,0 +1,124 @@
import { CenterBottomDuringTransitionDirective } from './center-bottom-transition.directive';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import {
ElementRef,
Renderer2,
Component,
ViewChild,
DebugElement,
Type,
} from '@angular/core';
import { By } from '@angular/platform-browser';
@Component({
template: `<div
centerBottomDuringTransition
className="test-class"
[inTransition]="inTransition"
>
Mock Component
</div>`,
})
class TestHostComponent {
@ViewChild(CenterBottomDuringTransitionDirective, { static: true })
directive: CenterBottomDuringTransitionDirective;
inTransition = false;
setTransition(isTransitioning: boolean) {
this.inTransition = isTransitioning;
}
}
class MockElementRef {
get nativeElement() {
return {};
}
}
fdescribe('Directive: CenterBottomTransition', () => {
let fixture: ComponentFixture<TestHostComponent>;
let component: TestHostComponent;
let directive: CenterBottomDuringTransitionDirective;
let elementRef: ElementRef;
let renderer: Renderer2;
beforeEach(async () => {
TestBed.configureTestingModule({
providers: [Renderer2, { provide: ElementRef, useClass: MockElementRef }],
declarations: [TestHostComponent, CenterBottomDuringTransitionDirective],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TestHostComponent);
component = fixture.componentInstance;
directive = component.directive;
elementRef = TestBed.get(ElementRef);
renderer = fixture.componentRef.injector.get<Renderer2>(
Renderer2 as Type<Renderer2>
);
fixture.detectChanges();
});
it('should be created', () => {
expect(
directive instanceof CenterBottomDuringTransitionDirective
).toBeTruthy();
});
describe('applyPosition', () => {
let mockElement: DebugElement;
beforeEach(() => {
spyOn(directive, 'applyPosition').and.callThrough();
spyOn(renderer, 'addClass').and.callFake(() => {});
spyOn(renderer, 'removeClass').and.callFake(() => {});
mockElement = fixture.debugElement.query(By.css('div'));
fixture.detectChanges();
});
it('should be called when inTransition Input changes', () => {
component.inTransition = true;
fixture.detectChanges();
expect(directive.applyPosition).toHaveBeenCalledWith(true);
});
it('should return if nativeElement is undefined', () => {
spyOn(elementRef, 'nativeElement').and.returnValue(undefined);
directive.applyPosition(true);
});
it('should add the provided class to the native element if inTransition is true', () => {
spyOn(elementRef, 'nativeElement').and.returnValue(
mockElement.nativeElement
);
directive.applyPosition(true);
expect(renderer.addClass).toHaveBeenCalledWith(
mockElement.nativeElement,
'test-class'
);
expect(renderer.removeClass).not.toHaveBeenCalled();
});
it('should remove the provided class from the native element if inTransition is false', () => {
spyOn(elementRef, 'nativeElement').and.returnValue(
mockElement.nativeElement
);
directive.applyPosition(false);
expect(renderer.removeClass).toHaveBeenCalledWith(
mockElement.nativeElement,
'test-class'
);
expect(renderer.addClass).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,29 @@
import { Directive, Input, ElementRef, Renderer2 } from '@angular/core';
// tslint:disable-next-line: directive-selector
@Directive({ selector: '[centerBottomDuringTransition]' })
export class CenterBottomDuringTransitionDirective {
@Input() set inTransition(isTransitioning: boolean) {
this.applyPosition(isTransitioning);
}
@Input() className = 'in-transition';
constructor(private elementRef: ElementRef, private renderer: Renderer2) {}
applyPosition(inTransition: boolean) {
const nativeElement = this.elementRef.nativeElement;
if (!nativeElement) {
return;
}
if (inTransition) {
this.renderer.addClass(nativeElement, this.className);
}
if (!inTransition) {
this.renderer.removeClass(nativeElement, this.className);
}
}
}

View File

@@ -4,5 +4,5 @@ export * from './click-outside.directive';
export * from './header-clicked.directive';
export * from './tooltip.directive';
export * from './var.directive';
export * from './center-bottom-transition.directive';
// end:ng42.barrel

View File

@@ -15,10 +15,21 @@ import { PackageNumberParserPipe } from '../pipes/package-number-parser.pipe';
import { AddClassDirective } from './directives/add-class.directive';
import { ShippingDocumentNumberFormatterPipe } from '../pipes/shipping-document-number-formatter.pipe';
import { RemissionSourcesDisplayPipe } from '../pipes/remission-sources-display.pipe';
import { ClickOutsideDirective, HeaderClickedDirective } from './directives';
import {
ClickOutsideDirective,
HeaderClickedDirective,
CenterBottomDuringTransitionDirective,
} from './directives';
const components = [BackArrowComponent, ModalDialogComponent];
const directives = [VarDirective, TooltipDirective, AddClassDirective, ClickOutsideDirective, HeaderClickedDirective];
const directives = [
VarDirective,
TooltipDirective,
AddClassDirective,
ClickOutsideDirective,
HeaderClickedDirective,
CenterBottomDuringTransitionDirective,
];
const pipes = [
SafeHtmlPipe,
BookPricePipe,
@@ -28,12 +39,12 @@ const pipes = [
TrimPipe,
PackageNumberParserPipe,
ShippingDocumentNumberFormatterPipe,
RemissionSourcesDisplayPipe
RemissionSourcesDisplayPipe,
];
@NgModule({
imports: [CommonModule, IconModule, ButtonModule],
exports: [...components, ...directives, ...pipes],
declarations: [...components, ...directives, ...pipes],
providers: []
providers: [],
})
export class SharedModule { }
export class SharedModule {}