@vaadin/password-field
Version:
vaadin-password-field
227 lines (199 loc) • 6.79 kB
JavaScript
/**
* @license
* Copyright (c) 2021 - 2026 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { DisabledMixin } from '@vaadin/a11y-base/src/disabled-mixin.js';
import { FocusMixin } from '@vaadin/a11y-base/src/focus-mixin.js';
import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
import { SlotStylesMixin } from '@vaadin/component-base/src/slot-styles-mixin.js';
import { InputMixin } from '@vaadin/field-base/src/input-mixin.js';
/**
* @polymerMixin
* @mixes DisabledMixin
* @mixes FocusMixin
* @mixes InputMixin
* @mixes SlotStylesMixin
*/
export const PasswordFieldMixin = (superClass) =>
class PasswordFieldMixinClass extends SlotStylesMixin(DisabledMixin(FocusMixin(InputMixin(superClass)))) {
static get properties() {
return {
/**
* Set to true to hide the eye icon which toggles the password visibility.
* @attr {boolean} reveal-button-hidden
*/
revealButtonHidden: {
type: Boolean,
value: false,
},
/**
* True if the password is visible ([type=text]).
* @attr {boolean} password-visible
*/
passwordVisible: {
type: Boolean,
value: false,
reflectToAttribute: true,
readOnly: true,
},
/**
* An object with translated strings used for localization.
* It has the following structure and default values:
*
* ```js
* {
* // Translation of the reveal icon button accessible label
* reveal: 'Show password'
* }
* ```
*/
i18n: {
type: Object,
value: () => {
return {
reveal: 'Show password',
};
},
},
};
}
/** @override */
static get delegateAttrs() {
// Do not delegate autocapitalize as it should be always set to "off"
return super.delegateAttrs.filter((attr) => attr !== 'autocapitalize');
}
constructor() {
super();
this._setType('password');
this.__boundRevealButtonClick = this._onRevealButtonClick.bind(this);
this.__boundRevealButtonMouseDown = this._onRevealButtonMouseDown.bind(this);
this.__lastChange = '';
}
/** @protected */
get slotStyles() {
const tag = this.localName;
return [
...super.slotStyles,
`
${tag} [slot="input"]::-ms-reveal {
display: none;
}
`,
];
}
/** @protected */
ready() {
super.ready();
this._revealPart = this.shadowRoot.querySelector('[part~="reveal-button"]');
this._revealButtonController = new SlotController(this, 'reveal', 'vaadin-password-field-button', {
initializer: (btn) => {
this._revealNode = btn;
btn.addEventListener('click', this.__boundRevealButtonClick);
btn.addEventListener('mousedown', this.__boundRevealButtonMouseDown);
},
});
this.addController(this._revealButtonController);
if (this.inputElement) {
this.inputElement.autocapitalize = 'off';
}
}
/** @protected */
updated(props) {
super.updated(props);
if (props.has('disabled')) {
this._revealNode.disabled = this.disabled;
}
if (props.has('revealButtonHidden')) {
this._toggleRevealHidden(this.revealButtonHidden);
}
if (props.has('passwordVisible')) {
this._setType(this.passwordVisible ? 'text' : 'password');
this._revealNode.setAttribute('aria-pressed', this.passwordVisible ? 'true' : 'false');
}
if (props.has('i18n') && this.i18n && this.i18n.reveal) {
this._revealNode.setAttribute('aria-label', this.i18n.reveal);
}
}
/**
* 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.
* @param {Event} event
* @return {boolean}
* @protected
*/
_shouldSetFocus(event) {
return event.target === this.inputElement || event.target === this._revealNode;
}
/**
* Override method inherited from `FocusMixin` to not hide password
* when focus moves to the reveal button or back to the input.
* @param {Event} event
* @return {boolean}
* @protected
*/
_shouldRemoveFocus(event) {
return !(
event.relatedTarget === this._revealNode ||
(event.relatedTarget === this.inputElement && event.target === this._revealNode)
);
}
/**
* Override method inherited from `FocusMixin` to toggle password visibility.
* @param {boolean} focused
* @protected
* @override
*/
_setFocused(focused) {
super._setFocused(focused);
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
this.toggleAttribute('focus-ring', this._keyboardActive && !isButtonFocused);
}
}
/** @private */
_onRevealButtonClick() {
this._setPasswordVisible(!this.passwordVisible);
}
/** @private */
_onRevealButtonMouseDown(e) {
// Cancel the following focusout event
e.preventDefault();
// Focus the input to avoid problem with password still visible
// when user clicks the reveal button and then clicks outside.
this.inputElement.focus();
}
/** @private */
_toggleRevealHidden(hidden) {
if (this._revealNode) {
if (hidden) {
this._revealPart.setAttribute('hidden', '');
this._revealNode.setAttribute('tabindex', '-1');
this._revealNode.setAttribute('aria-hidden', 'true');
} else {
this._revealPart.removeAttribute('hidden');
this._revealNode.setAttribute('tabindex', '0');
this._revealNode.removeAttribute('aria-hidden');
}
}
}
};