Skip to content

Commit

Permalink
Merge pull request #6 from criar-art/mode-restricted
Browse files Browse the repository at this point in the history
Implement Restricted Close Mode for UpWindowAngularComponent
  • Loading branch information
lucasferreiralimax authored Oct 14, 2024
2 parents eeb435d + ab5767e commit ac857fb
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@
aria-describedby="dialog-description"
aria-modal="true"
[attr.aria-hidden]="!isOpen()"
(click)="closeWindow('overlay')"
>
<div class="up-window" [ngClass]="getClass()">
<div
class="up-window"
[ngClass]="getClass()"
(click)="$event.stopPropagation()"
>
<button
*ngIf="!restrictMode"
class="close-window"
aria-label="Close window"
(click)="closeWindow()"
Expand All @@ -27,15 +33,15 @@ <h4 id="dialog-description" class="up-window-subtitle">
</div>
<div class="up-window-footer" [ngClass]="'align-' + buttonAlignment">
<button
class="btn"
class="btn btn-cancel"
[ngClass]="getButtonClass(cancelType)"
(click)="onCancel()"
[attr.aria-label]="cancelText"
>
{{ cancelText }}
</button>
<button
class="btn"
class="btn btn-confirm"
[ngClass]="getButtonClass(confirmType)"
(click)="onConfirm()"
[attr.aria-label]="confirmText"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ body {
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin: 1rem;

&.shake {
animation: shake 0.3s !important;
}

&.fade {
animation: up-window-fadeIn 0.3s forwards;
}
Expand Down Expand Up @@ -304,3 +308,11 @@ body {
transform: translateY(-100%);
}
}

@keyframes shake {
0% { transform: translate(0); }
25% { transform: translate(-5px); }
50% { transform: translate(5px); }
75% { transform: translate(-5px); }
100% { transform: translate(0); }
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing';
import { UpWindowAngularComponent } from './up-window-angular.component';
import { By } from '@angular/platform-browser';

Expand All @@ -25,14 +30,18 @@ describe('UpWindowAngularComponent', () => {
component.subtitle = 'Test Subtitle';
fixture.detectChanges();

const titleElement = fixture.debugElement.query(By.css('.up-window-title')).nativeElement;
const subtitleElement = fixture.debugElement.query(By.css('.up-window-subtitle')).nativeElement;
const titleElement = fixture.debugElement.query(
By.css('.up-window-title')
).nativeElement;
const subtitleElement = fixture.debugElement.query(
By.css('.up-window-subtitle')
).nativeElement;

expect(titleElement.textContent).toContain('Test Title');
expect(subtitleElement.textContent).toContain('Test Subtitle');
});

it('should apply the correct animation class when opening and closing the window', () => {
it('should apply the correct animation class when opening and closing the window', fakeAsync(() => {
component.animation = 'slide';
component.isOpen.set(true);
fixture.detectChanges();
Expand All @@ -43,26 +52,48 @@ describe('UpWindowAngularComponent', () => {
component.closeWindow();
fixture.detectChanges();

setTimeout(() => {
windowElement = fixture.debugElement.query(By.css('.up-window'));
expect(windowElement.classes['slide-out']).toBeTruthy();
expect(component.isOpen()).toBeFalse();
}, 300);
});
tick(300); // Simulate the passage of time for the animation duration

windowElement = fixture.debugElement.query(By.css('.up-window'));
expect(windowElement.classes['slide-out']).toBeTruthy();
expect(component.isOpen()).toBeFalse();
}));

it('should trigger onConfirm and onCancel when buttons are clicked', () => {
it('should trigger onConfirm and onCancel when buttons are clicked', async () => {
spyOn(component, 'onConfirm').and.callThrough();
spyOn(component, 'onCancel').and.callThrough();

const confirmButton = fixture.debugElement.query(By.css('.btn-confirm')).nativeElement;
const cancelButton = fixture.debugElement.query(By.css('.btn-cancel')).nativeElement;
component.isOpen.set(true); // Open the modal
fixture.detectChanges(); // Trigger change detection

await fixture.whenStable(); // Wait for any asynchronous tasks to finish
fixture.detectChanges(); // Re-render the component

// Check if buttons are present
const confirmButton = fixture.debugElement.query(By.css('.btn-confirm'));
const cancelButton = fixture.debugElement.query(By.css('.btn-cancel'));

expect(confirmButton).toBeTruthy(); // Ensure confirm button exists
expect(cancelButton).toBeTruthy(); // Ensure cancel button exists

confirmButton.click();
// Simulate click on confirm button
confirmButton.nativeElement.click();
await fixture.whenStable(); // Wait for any asynchronous tasks
expect(component.onConfirm).toHaveBeenCalled();
expect(component.isOpen()).toBeFalse(); // Check if modal is closed

cancelButton.click();
// Reset state and check cancel
component.isOpen.set(true); // Re-open the modal
fixture.detectChanges();

await fixture.whenStable(); // Wait for any asynchronous tasks
cancelButton.nativeElement.click();
await fixture.whenStable(); // Wait for any asynchronous tasks
expect(component.onCancel).toHaveBeenCalled();
});
expect(component.isOpen()).toBeFalse(); // Check if modal is closed
});



it('should apply custom class from @Input', () => {
component.class = 'custom-class';
Expand All @@ -77,15 +108,35 @@ describe('UpWindowAngularComponent', () => {
expect(component.isOpen()).toBeTrue();
});

it('should set isOpen to false after closeWindow is called', () => {
it('should set isOpen to false after closeWindow is called', fakeAsync(() => {
component.isOpen.set(true);
fixture.detectChanges();

component.closeWindow();
fixture.detectChanges();

setTimeout(() => {
expect(component.isOpen()).toBeFalse();
}, 300);
tick(300); // Wait for the closing animation
expect(component.isOpen()).toBeFalse();
}));

it('should not display close button in restricted mode', () => {
component.isOpen.set(true);
component.restrictMode = true; // Enable restricted mode
fixture.detectChanges();

const closeButton = fixture.debugElement.query(By.css('.close-window'));
expect(closeButton).toBeFalsy(); // Close button should not be present
});

it('should prevent closing when clicking on the overlay in restricted mode', fakeAsync(() => {
component.isOpen.set(true);
component.restrictMode = true; // Enable restricted mode
fixture.detectChanges();

const overlay = fixture.debugElement.query(
By.css('.overlay')
).nativeElement;
overlay.click(); // Click on the overlay
expect(component.isOpen()).toBeTrue(); // Modal should still be open
}));
});
50 changes: 43 additions & 7 deletions projects/up-window-angular/src/lib/up-window-angular.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
OnDestroy,
ElementRef,
ViewChild,
effect,
} from '@angular/core';

@Component({
Expand All @@ -25,6 +26,7 @@ export class UpWindowAngularComponent implements OnInit, OnDestroy {
@Input() class: string | undefined;
@Input() isOpen: WritableSignal<boolean> = signal(false);
@Input() animation: string = 'fade';
@Input() restrictMode: boolean = false;
@Input() confirmText: string = 'Confirm';
@Input() cancelText: string = 'Cancel';
@Input() confirmType: string = 'primary';
Expand All @@ -37,12 +39,22 @@ export class UpWindowAngularComponent implements OnInit, OnDestroy {
@Output() cancel = new EventEmitter<void>();

closingAnimation: boolean = false;
openingAnimation: boolean = false;
shakeAnimation: boolean = false;
focusableElements!: NodeListOf<HTMLElement>;
firstFocusableElement!: HTMLElement;
lastFocusableElement!: HTMLElement;

@ViewChild('modal') modal!: ElementRef;

constructor() {
effect(() => {
this.isOpen()
? this.startOpeningAnimation()
: this.startClosingAnimation();
});
}

ngOnInit(): void {
document.addEventListener('keydown', this.handleKeydown.bind(this));
}
Expand All @@ -61,15 +73,24 @@ export class UpWindowAngularComponent implements OnInit, OnDestroy {
}

handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape' && this.isOpen()) {
if (event.key === 'Escape' && this.isOpen() && !this.restrictMode) {
this.closeWindow();
} else if (event.key === 'Escape' && this.isOpen() && this.restrictMode) {
this.triggerShakeAnimation();
}

if (event.key === 'Tab' && this.isOpen()) {
this.trapFocus(event);
}
}

triggerShakeAnimation() {
this.shakeAnimation = true;
setTimeout(() => {
this.shakeAnimation = false;
}, 500);
}

trapFocus(event: KeyboardEvent) {
const isShiftPressed = event.shiftKey;
const activeElement = document.activeElement as HTMLElement;
Expand All @@ -87,24 +108,39 @@ export class UpWindowAngularComponent implements OnInit, OnDestroy {
document.removeEventListener('keydown', this.handleKeydown.bind(this));
}

openWindow() {
this.isOpen.set(true);
startOpeningAnimation() {
this.openingAnimation = true;
setTimeout(() => {
this.openingAnimation = false;
}, 300);
}

closeWindow() {
startClosingAnimation() {
this.closingAnimation = true;

setTimeout(() => {
this.isOpen.set(false);
this.closingAnimation = false;
}, 300);
}

closeWindow(from?: string) {
if (from == 'overlay' && this.restrictMode) {
this.triggerShakeAnimation();
} else {
this.closingAnimation = true;

setTimeout(() => {
this.isOpen.set(false);
this.closingAnimation = false;
}, 300);
}
}

getClass() {
return {
...(this.class ? { [this.class]: true } : {}),
[this.animation]: !this.closingAnimation,
[this.animation]: !this.closingAnimation && this.openingAnimation,
[`${this.animation}-out`]: this.closingAnimation,
shake: this.shakeAnimation,
};
}

Expand Down
12 changes: 12 additions & 0 deletions src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,18 @@
>
Scale Example content!
</up-window-angular>
<hr />
<button class="button-modal-example" type="button" (click)="openWindowExample('restrict')">
Open Mode Restrict Example
</button>
<up-window-angular
[isOpen]="isWindowOpenRestrict"
title="Restrict Window"
subtitle="This window mode Restrict."
[restrictMode]="true"
>
Mode Restrict Example content!
</up-window-angular>
</div>
</main>
<app-footer></app-footer>
Expand Down
9 changes: 7 additions & 2 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,18 @@ export class AppComponent {
isWindowOpenSlideUp: WritableSignal<boolean> = signal(false);
isWindowOpenSlideDown: WritableSignal<boolean> = signal(false);
isWindowOpenScale: WritableSignal<boolean> = signal(false);
isWindowOpenRestrict: WritableSignal<boolean> = signal(false);

openWindowExample(animation: string) {
openWindowExample(type: string) {
this.isWindowOpenFade.set(false);
this.isWindowOpenSlideLeft.set(false);
this.isWindowOpenSlideRight.set(false);
this.isWindowOpenSlideUp.set(false);
this.isWindowOpenSlideDown.set(false);
this.isWindowOpenScale.set(false);
this.isWindowOpenRestrict.set(false);

switch (animation) {
switch (type) {
case 'fade':
this.isWindowOpenFade.set(true);
break;
Expand All @@ -62,6 +64,9 @@ export class AppComponent {
case 'scale':
this.isWindowOpenScale.set(true);
break;
case 'restrict':
this.isWindowOpenRestrict.set(true);
break;
}
}

Expand Down

0 comments on commit ac857fb

Please sign in to comment.