UNPKG

@vaadin/password-field

Version:
227 lines (199 loc) 6.79 kB
/** * @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'); } } } };