fix(parental-lock): let a repeated refusal shake for its full time

Each refusal started a 400ms timeout that cleared the shared shake
state. A second refusal inside that window, for example on the other
field, had its shake cut short by the first timer. Starting a shake now
cancels the previous timer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-10-01 06:32:37 +02:00
1 parent 2677b7afaf
commit 9d1f3d34fc
2 files changed
+34 -1

No files matched your search

@@ -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 = () =>
@@ -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 {