Skip to content

Commit

Permalink
fix: delay setting error message on focusout until click
Browse files Browse the repository at this point in the history
  • Loading branch information
web-padawan committed Oct 1, 2024
1 parent f5b1dc8 commit 9e3ab03
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 4 deletions.
1 change: 1 addition & 0 deletions packages/login/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"dependencies": {
"@open-wc/dedupe-mixin": "^1.3.0",
"@polymer/polymer": "^3.0.0",
"@vaadin/a11y-base": "24.6.0-alpha0",
"@vaadin/button": "24.6.0-alpha0",
"@vaadin/component-base": "24.6.0-alpha0",
"@vaadin/overlay": "24.6.0-alpha0",
Expand Down
4 changes: 2 additions & 2 deletions packages/login/src/vaadin-lit-login-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ class LoginForm extends LoginFormMixin(ElementMixin(ThemableMixin(PolylitMixin(L
<vaadin-text-field
name="username"
label="${this.i18n.form.username}"
.errorMessage="${this.i18n.errorMessage.username}"
id="vaadinLoginUsername"
required
@keydown="${this._handleInputKeydown}"
@focusout="${this._handleInputFocusOut}"
autocapitalize="none"
autocorrect="off"
spellcheck="false"
Expand All @@ -71,10 +71,10 @@ class LoginForm extends LoginFormMixin(ElementMixin(ThemableMixin(PolylitMixin(L
<vaadin-password-field
name="password"
.label="${this.i18n.form.password}"
.errorMessage="${this.i18n.errorMessage.password}"
id="vaadinLoginPassword"
required
@keydown="${this._handleInputKeydown}"
@focusout="${this._handleInputFocusOut}"
spellcheck="false"
autocomplete="current-password"
>
Expand Down
41 changes: 41 additions & 0 deletions packages/login/src/vaadin-login-form-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Copyright (c) 2018 - 2024 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { isKeyboardActive } from '@vaadin/a11y-base/src/focus-utils.js';
import { LoginMixin } from './vaadin-login-mixin.js';

/**
Expand Down Expand Up @@ -46,6 +47,10 @@ export const LoginFormMixin = (superClass) =>
const userName = this.$.vaadinLoginUsername;
const password = this.$.vaadinLoginPassword;

// Ensure both fields have error message
this.__setErrorMessage(userName);
this.__setErrorMessage(password);

if (this.disabled || !(userName.validate() && password.validate())) {
return;
}
Expand Down Expand Up @@ -96,10 +101,42 @@ export const LoginFormMixin = (superClass) =>
}
}

/** @private */
__setErrorMessage(field, shouldClear) {
const errorKey = field.id === 'vaadinLoginUsername' ? 'username' : 'password';
field.errorMessage = shouldClear ? '' : this.i18n.errorMessage[errorKey];
}

/** @protected */
_handleInputFocusOut(event) {
const { currentTarget: field } = event;

// Focus moved outside the browser tab, do nothing.
if (!document.hasFocus()) {
return;
}

if (isKeyboardActive()) {
this.__setErrorMessage(field);
} else {
// Postpone setting error message until global click since setting it
// will affect field height and might affect click on other elements
// located below it, including the "forgot password" button.
document.addEventListener(
'click',
() => {
this.__setErrorMessage(field);
},
{ once: true },
);
}
}

/** @protected */
_handleInputKeydown(e) {
if (e.key === 'Enter') {
const { currentTarget: inputActive } = e;
this.__setErrorMessage(inputActive);
const nextInput =
inputActive.id === 'vaadinLoginUsername' ? this.$.vaadinLoginPassword : this.$.vaadinLoginUsername;
if (inputActive.validate()) {
Expand All @@ -117,6 +154,10 @@ export const LoginFormMixin = (superClass) =>
const input = e.currentTarget;
if (e.key === 'Tab' && input instanceof HTMLInputElement) {
input.select();
} else if (e.key !== 'Enter' && input.parentElement.errorMessage) {
// Reset error message when typing anything (including Backspace)
// to prevent click issues handled by the focusout event listener.
this.__setErrorMessage(input.parentElement, true);
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/login/src/vaadin-login-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ class LoginForm extends LoginFormMixin(ElementMixin(ThemableMixin(PolymerElement
<vaadin-text-field
name="username"
label="[[i18n.form.username]]"
error-message="[[i18n.errorMessage.username]]"
id="vaadinLoginUsername"
required
on-keydown="_handleInputKeydown"
on-focusout="_handleInputFocusOut"
autocapitalize="none"
autocorrect="off"
spellcheck="false"
Expand All @@ -82,9 +82,9 @@ class LoginForm extends LoginFormMixin(ElementMixin(ThemableMixin(PolymerElement
<vaadin-password-field
name="password"
label="[[i18n.form.password]]"
error-message="[[i18n.errorMessage.password]]"
id="vaadinLoginPassword"
required
on-focusout="_handleInputFocusOut"
on-keydown="_handleInputKeydown"
spellcheck="false"
autocomplete="current-password"
Expand Down
70 changes: 70 additions & 0 deletions packages/login/test/login-form.common.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect } from '@vaadin/chai-plugins';
import { enter, fixtureSync, nextRender, nextUpdate, tap } from '@vaadin/testing-helpers';
import { resetMouse, sendKeys, sendMouse } from '@web/test-runner-commands';
import sinon from 'sinon';
import { fillUsernameAndPassword } from './helpers.js';

Expand Down Expand Up @@ -228,6 +229,75 @@ describe('login form', () => {
const passwordField = login.$.vaadinLoginPassword;
expect(passwordField.getAttribute('autocomplete')).to.be.equal('current-password');
});

describe('interactions', () => {
let forgotPassword;

beforeEach(() => {
forgotPassword = login.querySelector('[slot="forgot-password"]');
});

afterEach(async () => {
await resetMouse();
});

['username', 'password'].forEach((type) => {
describe(type, () => {
let field;

beforeEach(async () => {
field = login.querySelector(`[name=${type}]`);
field.focus();
await nextRender();
});

it(`should handle forgot password click if ${type} field is focused and invalid`, async () => {
const spy = sinon.spy();
login.addEventListener('forgot-password', spy);

const { x, y } = forgotPassword.getBoundingClientRect();
await sendMouse({ type: 'click', position: [Math.floor(x + 5), Math.floor(y + 5)] });
await nextRender();

expect(spy).to.be.calledOnce;
});

it(`should set error message to ${type} field after the delay on click`, async () => {
const { x, y } = forgotPassword.getBoundingClientRect();
await sendMouse({ type: 'move', position: [Math.floor(x + 5), Math.floor(y + 5)] });

await sendMouse({ type: 'down' });
expect(field.errorMessage).to.be.not.ok;

await sendMouse({ type: 'up' });
expect(field.errorMessage).to.be.equal(login.i18n.errorMessage[type]);
});

it(`should set error message to ${type} field immediately on Tab`, async () => {
await sendKeys({ press: 'Tab' });
expect(field.errorMessage).to.be.equal(login.i18n.errorMessage[type]);
});

it(`should set error message to ${type} field immediately on Enter`, async () => {
await sendKeys({ press: 'Enter' });
expect(field.errorMessage).to.be.equal(login.i18n.errorMessage[type]);
});

it(`should reset error message to ${type} field after typing`, async () => {
await sendKeys({ type: 'a' });
await sendKeys({ press: 'Backspace' });
expect(field.errorMessage).to.be.not.ok;
});

it('should set error message to other field on form submit', () => {
const name = type === 'username' ? 'password' : 'username';
const other = login.querySelector(`[name=${name}]`);
login.submit();
expect(other.errorMessage).to.be.equal(login.i18n.errorMessage[name]);
});
});
});
});
});

describe('no autofocus', () => {
Expand Down

0 comments on commit 9e3ab03

Please sign in to comment.