From def32c9f2a41996cd417d7b68f845c0a9670b4f7 Mon Sep 17 00:00:00 2001 From: Chris Pyles Date: Mon, 1 Jul 2024 21:36:35 -0700 Subject: [PATCH] show url copied message in snackbar instead of alert --- src/app/app.component.html | 4 +++ src/app/app.component.scss | 14 ++++++++++ src/app/app.component.spec.ts | 42 ++++++++++++++++++++++++++++++ src/app/app.component.ts | 20 +++++++++++++- src/app/game-state.service.spec.ts | 4 +++ src/styles.scss | 3 ++- 6 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/app/app.component.html b/src/app/app.component.html index 938ca4c..f965570 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -50,3 +50,7 @@ + +
+ {{ snackBarText() }} +
diff --git a/src/app/app.component.scss b/src/app/app.component.scss index b1b429f..4f7c6bc 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -22,3 +22,17 @@ header { display: flex; } } + +.snack-bar { + background: rgba(255, 255, 255, 0.2); + border-radius: 0.5rem; + bottom: 2rem; + opacity: 0; + padding: 1rem; + position: absolute; + transition: opacity 0.25s; + + &.visible { + opacity: 1; + } +} diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 6de6df4..4e995ce 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -21,6 +21,15 @@ describe('AppComponent', () => { ).and.returnValue(true); }; + beforeAll(() => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 10_000; + jasmine.clock().install(); + }); + + afterAll(() => { + jasmine.clock().uninstall(); + }); + beforeEach(async () => { const maze = new Maze(4, new Chooser(42)); gameStateService = jasmine.createSpyObj( @@ -190,5 +199,38 @@ describe('AppComponent', () => { 'http://example.com/?seed=321', ); }); + + it('should show a snack bar confirming that the URL was copied', (done) => { + fixture.debugElement + .query(By.css('[data-test-id="share-maze-button"]')) + .nativeElement.click(); + fixture.detectChanges(); + + expect( + fixture.nativeElement.querySelector('.snack-bar').textContent.trim(), + ).toBe('URL copied to clipboard'); + expect( + fixture.nativeElement + .querySelector('.snack-bar') + .classList.contains('visible'), + ).toBeTrue(); + + // check that the snackbar is hidden after 2s and emptied after 3s + + jasmine.clock().tick(2000); + fixture.detectChanges(); + expect( + fixture.nativeElement.querySelector('.snack-bar').style.opacity, + ).toBe('0'); + + jasmine.clock().tick(1000); + fixture.detectChanges(); + expect( + fixture.nativeElement + .querySelector('.snack-bar') + .classList.contains('visible'), + ).toBeFalse(); + done(); + }); }); }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 7eb0da3..a3075a8 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -4,6 +4,8 @@ import { inject, InjectionToken, OnInit, + signal, + ElementRef, } from '@angular/core'; import { MazeComponent } from './maze/maze.component'; @@ -29,6 +31,7 @@ export const WINDOW_TOKEN = new InjectionToken('window', { changeDetection: ChangeDetectionStrategy.OnPush, }) export class AppComponent implements OnInit { + private readonly elementRef = inject(ElementRef); private readonly gameStateService = inject(GameStateService); private readonly window = inject(WINDOW_TOKEN); @@ -36,6 +39,9 @@ export class AppComponent implements OnInit { /** The size of the maze, defined as the number of rows/columns in the maze. */ size = 20; + /** Text to show in the snack bar. */ + readonly snackBarText = signal(undefined); + ngOnInit(): void { // Set dark mode if user's OS is using it. if ( @@ -94,7 +100,8 @@ export class AppComponent implements OnInit { shareMaze(): void { const url = this.gameStateService.getShareUrl(); navigator.clipboard.writeText(url); - alert('URL copied to clipboard'); // TODO: snackbar? + this.snackBarText.set('URL copied to clipboard'); + setTimeout(() => this.closeSnackBar(), 2000); } /** Handles a move by the user. */ @@ -102,4 +109,15 @@ export class AppComponent implements OnInit { if (this.gameStateService.inAnimation) return; this.gameStateService.move(dir); } + + private closeSnackBar(): void { + // Set the opacity to 0 so the snack bar fades away before removing its text. + this.elementRef.nativeElement.querySelector('.snack-bar').style.opacity = + '0'; + setTimeout(() => { + this.snackBarText.set(undefined); + this.elementRef.nativeElement.querySelector('.snack-bar').style.opacity = + ''; + }, 1000); + } } diff --git a/src/app/game-state.service.spec.ts b/src/app/game-state.service.spec.ts index c69971e..2e2e9a9 100644 --- a/src/app/game-state.service.spec.ts +++ b/src/app/game-state.service.spec.ts @@ -14,6 +14,10 @@ describe('GameStateService', () => { clock.install(); }); + afterAll(() => { + clock.uninstall(); + }); + beforeEach(() => { TestBed.configureTestingModule({}); service = TestBed.inject(GameStateService); diff --git a/src/styles.scss b/src/styles.scss index 526244d..16fb81f 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1,4 +1,4 @@ -@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200'); +@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&family=Open+Sans:ital,wght@0,300..800;1,300..800'); * { margin: 0; @@ -20,6 +20,7 @@ body { background: var(--background-color); color: var(--text-color); + font-family: 'Open Sans', Arial, Helvetica, sans-serif; &.dark-mode { --background-color: #060606;