diff --git a/libs/ui/components/src/lib/parental-lock-pin-dialog/parental-lock-pin-dialog.component.spec.ts b/libs/ui/components/src/lib/parental-lock-pin-dialog/parental-lock-pin-dialog.component.spec.ts index 81a87e5fb..f21e3160a 100644 --- a/libs/ui/components/src/lib/parental-lock-pin-dialog/parental-lock-pin-dialog.component.spec.ts +++ b/libs/ui/components/src/lib/parental-lock-pin-dialog/parental-lock-pin-dialog.component.spec.ts @@ -198,6 +198,30 @@ describe('ParentalLockPinDialogComponent', () => { expect(dialog.close).not.toHaveBeenCalled(); }); + it('lets a second refusal shake for its full time', async () => { + const { component } = await openDialog({ mode: 'set' }); + jest.useFakeTimers(); + try { + component.onPinInput('24'); + await component.submit(); + expect(component.shake()).toBe('pin'); + + jest.advanceTimersByTime(300); + component.onPinInput('2468'); + component.onConfirmationInput('2469'); + await component.submit(); + expect(component.shake()).toBe('confirmation'); + + // The first refusal's timer must not end the second shake. + jest.advanceTimersByTime(150); + expect(component.shake()).toBe('confirmation'); + jest.advanceTimersByTime(250); + expect(component.shake()).toBeNull(); + } finally { + jest.useRealTimers(); + } + }); + it('refuses a too-short PIN on Enter with an error on the PIN field', async () => { const dialog = await openDialog({ mode: 'set' }); const pin = () => diff --git a/libs/ui/components/src/lib/parental-lock-pin-dialog/parental-lock-pin-dialog.component.ts b/libs/ui/components/src/lib/parental-lock-pin-dialog/parental-lock-pin-dialog.component.ts index c3a999f26..ce1393e4a 100644 --- a/libs/ui/components/src/lib/parental-lock-pin-dialog/parental-lock-pin-dialog.component.ts +++ b/libs/ui/components/src/lib/parental-lock-pin-dialog/parental-lock-pin-dialog.component.ts @@ -96,6 +96,7 @@ export class ParentalLockPinDialogComponent { private readonly throttle = this.data.throttle ?? createParentalLockPinThrottle(); private cooldownTimer: number | null = null; + private shakeTimer: number | null = null; readonly isSetMode = computed(() => this.data.mode === 'set'); readonly titleKey = computed( @@ -252,8 +253,16 @@ export class ParentalLockPinDialogComponent { } private shakeField(field: PinDialogField): void { + // A refusal inside the previous one's 400ms shakes for its own + // full time; the earlier timer would otherwise end it early. + if (this.shakeTimer !== null) { + window.clearTimeout(this.shakeTimer); + } this.shake.set(field); - window.setTimeout(() => this.shake.set(null), 400); + this.shakeTimer = window.setTimeout(() => { + this.shakeTimer = null; + this.shake.set(null); + }, 400); } private startCooldown(until: number): void {