diff --git a/packages/password-field/src/vaadin-password-field.js b/packages/password-field/src/vaadin-password-field.js index 34156abd989..ddbd856b35c 100644 --- a/packages/password-field/src/vaadin-password-field.js +++ b/packages/password-field/src/vaadin-password-field.js @@ -125,7 +125,8 @@ export class PasswordField extends TextField { super(); this._setType('password'); this.__boundRevealButtonClick = this._onRevealButtonClick.bind(this); - this.__boundRevealButtonTouchend = this._onRevealButtonTouchend.bind(this); + this.__boundRevealButtonMouseDown = this._onRevealButtonMouseDown.bind(this); + this.__lastChange = ''; } /** @protected */ @@ -157,7 +158,7 @@ export class PasswordField extends TextField { btn.disabled = this.disabled; btn.addEventListener('click', this.__boundRevealButtonClick); - btn.addEventListener('touchend', this.__boundRevealButtonTouchend); + btn.addEventListener('mousedown', this.__boundRevealButtonMouseDown); }, }); this.addController(this._revealButtonController); @@ -172,6 +173,19 @@ export class PasswordField extends TextField { } } + /** + * Override an event listener inherited from `InputControlMixin` + * to store the value at the moment of the native `change` event. + * @param {Event} event + * @protected + * @override + */ + _onChange(event) { + super._onChange(event); + + this.__lastChange = this.inputElement.value; + } + /** * Override method inherited from `FocusMixin` to mark field as focused * when focus moves to the reveal button using Shift Tab. @@ -208,6 +222,12 @@ export class PasswordField extends TextField { if (!focused) { this._setPasswordVisible(false); + + // Detect if `focusout` was prevented and if so, dispatch `change` event manually. + if (this.__lastChange !== this.inputElement.value) { + this.__lastChange = this.inputElement.value; + this.dispatchEvent(new CustomEvent('change', { bubbles: true })); + } } else { const isButtonFocused = this.getRootNode().activeElement === this._revealNode; // Remove focus-ring from the field when the reveal button gets focused @@ -243,10 +263,10 @@ export class PasswordField extends TextField { } /** @private */ - _onRevealButtonTouchend(e) { - // Cancel the following click event + _onRevealButtonMouseDown(e) { + // Cancel the following focusout event e.preventDefault(); - this._togglePasswordVisibility(); + // Focus the input to avoid problem with password still visible // when user clicks the reveal button and then clicks outside. this.inputElement.focus(); diff --git a/packages/password-field/test/password-field.test.js b/packages/password-field/test/password-field.test.js index 40ab0c719cc..b0c2949ff86 100644 --- a/packages/password-field/test/password-field.test.js +++ b/packages/password-field/test/password-field.test.js @@ -1,5 +1,5 @@ import { expect } from '@esm-bundle/chai'; -import { fixtureSync, focusout, makeSoloTouchEvent, mousedown, nextRender } from '@vaadin/testing-helpers'; +import { fire, fixtureSync, focusout, makeSoloTouchEvent, mousedown, nextRender } from '@vaadin/testing-helpers'; import { sendKeys } from '@web/test-runner-commands'; import sinon from 'sinon'; import '../src/vaadin-password-field.js'; @@ -46,24 +46,53 @@ describe('password-field', () => { expect(input.type).to.equal('text'); }); - it('should prevent touchend event on reveal button', () => { - const event1 = makeSoloTouchEvent('touchend', null, revealButton); - expect(event1.defaultPrevented).to.be.true; - expect(input.type).to.equal('text'); - - const event2 = makeSoloTouchEvent('touchend', null, revealButton); - expect(event2.defaultPrevented).to.be.true; - expect(input.type).to.equal('password'); + it('should prevent mousedown event on reveal button', () => { + const event = fire(revealButton, 'mousedown'); + expect(event.defaultPrevented).to.be.true; }); - it('should focus the input on reveal button touchend', () => { + it('should focus the input on reveal button mousedown', () => { const spy = sinon.spy(input, 'focus'); - makeSoloTouchEvent('touchend', null, revealButton); + fire(revealButton, 'mousedown'); + + expect(spy.calledOnce).to.be.true; + }); + + it('should dispatch change event on focusout after changing the value', () => { + const spy = sinon.spy(); + passwordField.addEventListener('change', spy); + + input.value = 'test'; + + focusout(input); expect(spy.calledOnce).to.be.true; }); + it('should not dispatch change event on focusout if value is the same', () => { + const spy = sinon.spy(); + passwordField.addEventListener('change', spy); + + focusout(input); + + expect(spy.called).to.be.false; + }); + + it('should not dispatch change event on focusout after native change', () => { + const spy = sinon.spy(); + passwordField.addEventListener('change', spy); + + input.value = 'test'; + fire(input, 'change'); + + spy.resetHistory(); + + focusout(input); + + expect(spy.called).to.be.false; + }); + it('should toggle aria-pressed attribute on reveal button click', () => { revealButton.click(); expect(revealButton.getAttribute('aria-pressed')).to.equal('true');