fix(parental-lock): tell Enter in the PIN field from a click on Save

Moving on to an empty repeat was decided by document.activeElement in
submit(). WebKit does not focus a clicked button, so a click on Save
with the PIN field focused looked like Enter: focus moved and no error
showed. The advance is now a keydown.enter handler on the PIN field
that prevents the submission, and submit() refuses an empty repeat on
every path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-10-01 06:43:33 +02:00
1 parent 9d1f3d34fc
commit 24fcea999d
5 files changed
+52 -9

No files matched your search

@@ -110,7 +110,16 @@ test.describe('Electron parental-lock PIN dialog', () => {
'new-password'
);
// Enter after the PIN moves on to the empty repeat; Save
// with the repeat still empty is refused.
await pin.fill(PIN);
await pin.press('Enter');
await expect(confirm).toBeFocused();
await expect(mismatch).toBeHidden();
await submit.click();
await expect(mismatch).toBeVisible();
await expect(dialog).toBeVisible();
await confirm.fill(PIN.slice(0, 3));
await expect(mismatch).toBeHidden();
await expect(confirm).toHaveAttribute('aria-invalid', 'false');
+2 -1
View File
@@ -429,7 +429,8 @@ so a cancelled or refused PIN leaves it showing the real state.
disabled default button; `submit()` refuses instead, shakes the field
(no shake under `prefers-reduced-motion: reduce`) and focuses it — except
that Enter in the PIN field with the repeat still empty only moves focus
to the repeat. Set
to the repeat (a `keydown.enter` handler: focus cannot tell Enter from a
click on Save, since WebKit does not focus a clicked button). Set
mode marks both inputs `autocomplete="new-password"`. The Electron
`parental-lock-pin-dialog.e2e.ts` covers the labels and the
mismatch → fix → save flow.
@@ -26,6 +26,7 @@
[errorStateMatcher]="pinErrorState"
[ngModel]="pin()"
(ngModelChange)="onPinInput($event)"
(keydown.enter)="onPinEnter($event)"
name="pin"
/>
<mat-hint>
@@ -171,16 +171,37 @@ describe('ParentalLockPinDialogComponent', () => {
it('moves on to an empty repeat on Enter in the PIN field', async () => {
const dialog = await openDialog({ mode: 'set' });
const enter = new KeyboardEvent('keydown', {
key: 'Enter',
cancelable: true,
});
await dialog.type('parental-lock-pin', '2468');
dialog.query('parental-lock-pin')?.focus();
await dialog.pressEnter();
dialog.query('parental-lock-pin')?.dispatchEvent(enter);
await dialog.fixture.whenStable();
// Handled before the form sees it: no implicit submission.
expect(enter.defaultPrevented).toBe(true);
expect(document.activeElement).toBe(
dialog.query('parental-lock-pin-confirm')
);
expect(dialog.query('parental-lock-pin-mismatch')).toBeNull();
expect(dialog.component.shake()).toBeNull();
});
it('refuses an empty repeat on Save while the PIN field keeps focus', async () => {
// WebKit does not focus a clicked button, so focus says nothing
// about how the form was submitted.
const dialog = await openDialog({ mode: 'set' });
await dialog.type('parental-lock-pin', '2468');
dialog.query('parental-lock-pin')?.focus();
await dialog.pressEnter();
expect(text(dialog.query('parental-lock-pin-mismatch'))).toBe(
KEYS.mismatch
);
expect(dialog.component.shake()).toBe('confirmation');
expect(dialog.close).not.toHaveBeenCalled();
});
@@ -186,6 +186,23 @@ export class ParentalLockPinDialogComponent {
this.clearRefusal('confirmation');
}
/**
* Set mode: Enter after a complete first PIN moves on to an empty repeat
* instead of submitting. Handled on the key, not in `submit()`: focus
* cannot tell Enter from a click on Save, as WebKit does not focus a
* clicked button.
*/
onPinEnter(event: Event): void {
if (
this.isSetMode() &&
!this.confirmation() &&
isValidParentalLockPin(this.pin())
) {
event.preventDefault();
this.confirmationInput()?.nativeElement.focus();
}
}
async submit(): Promise<void> {
if (this.busy() || this.inCooldown()) {
return;
@@ -197,12 +214,6 @@ export class ParentalLockPinDialogComponent {
return;
}
if (this.isSetMode()) {
const pinField = this.pinInput()?.nativeElement;
if (!this.confirmation() && document.activeElement === pinField) {
// Enter after the first PIN moves on to the repeat.
this.confirmationInput()?.nativeElement.focus();
return;
}
if (this.confirmation() !== pin) {
this.refuse('confirmation');
return;