password-validation-inputs.mjs (8048B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { html } from "chrome://global/content/vendor/lit.all.mjs"; 6 import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; 7 8 // eslint-disable-next-line import/no-unassigned-import 9 import "chrome://browser/content/backup/password-rules-tooltip.mjs"; 10 11 /** 12 * The widget for enabling password protection if the backup is not yet 13 * encrypted. 14 */ 15 export default class PasswordValidationInputs extends MozLitElement { 16 static properties = { 17 _hasEmail: { type: Boolean, state: true }, 18 _passwordsMatch: { type: Boolean, state: true }, 19 _passwordsValid: { type: Boolean, state: true }, 20 _tooShort: { type: Boolean, state: true }, 21 createPasswordLabelL10nId: { 22 type: String, 23 reflect: true, 24 attribute: "create-password-label-l10n-id", 25 }, 26 embeddedFxBackupOptIn: { 27 type: Boolean, 28 reflect: true, 29 attribute: "embedded-fx-backup-opt-in", 30 }, 31 }; 32 33 static get queries() { 34 return { 35 formEl: "#password-inputs-form", 36 inputNewPasswordEl: "#new-password-input", 37 inputRepeatPasswordEl: "#repeat-password-input", 38 passwordRulesEl: "#password-rules", 39 repeatPasswordErrorEl: "#repeat-password-error", 40 }; 41 } 42 43 constructor() { 44 super(); 45 this._tooShort = true; 46 this._hasEmail = false; 47 this._passwordsMatch = false; 48 this._passwordsValid = false; 49 } 50 51 connectedCallback() { 52 super.connectedCallback(); 53 this._onKeydown = e => { 54 if (e.key === "Escape" && this.passwordRulesEl.open) { 55 this.passwordRulesEl.hide(); 56 e.stopPropagation(); 57 e.preventDefault(); 58 } 59 }; 60 document.addEventListener("keydown", this._onKeydown, true); 61 } 62 disconnectedCallback() { 63 document.removeEventListener("keydown", this._onKeydown, true); 64 super.disconnectedCallback(); 65 } 66 67 setInputValidity(input, isValid, describedById = null) { 68 input.setAttribute("aria-invalid", isValid ? "false" : "true"); 69 if (describedById) { 70 input.setAttribute("aria-describedby", describedById); 71 } else { 72 input.removeAttribute("aria-describedby"); 73 } 74 } 75 76 reset() { 77 this.formEl?.reset(); 78 if (this.inputNewPasswordEl) { 79 this.inputNewPasswordEl.revealPassword = false; 80 this.setInputValidity(this.inputNewPasswordEl, true); 81 } 82 if (this.inputRepeatPasswordEl) { 83 this.inputRepeatPasswordEl.revealPassword = false; 84 this.setInputValidity(this.inputRepeatPasswordEl, true); 85 } 86 this._hasEmail = false; 87 this._tooShort = true; 88 this._passwordsMatch = false; 89 this._passwordsValid = false; 90 this.passwordRulesEl.hide(); 91 } 92 93 handleFocusNewPassword() { 94 this.passwordRulesEl.show(); 95 } 96 97 handleBlurNewPassword(event) { 98 if (event.target.checkValidity()) { 99 this.passwordRulesEl.hide(); 100 } 101 } 102 103 handleChangeNewPassword() { 104 this.updatePasswordValidity(); 105 } 106 107 handleChangeRepeatPassword() { 108 this.updatePasswordValidity(); 109 } 110 111 updatePasswordValidity() { 112 const emailRegex = /^[\w!#$%&'*+/=?^`{|}~.-]+@[A-Z0-9-]+\.[A-Z0-9.-]+$/i; 113 const l10n = new Localization(["browser/backupSettings.ftl"], true); 114 115 this._hasEmail = emailRegex.test(this.inputNewPasswordEl.value); 116 if (this._hasEmail) { 117 const invalid_password_email_l10n_message = l10n.formatValueSync( 118 "password-validity-has-email" 119 ); 120 121 this.inputNewPasswordEl.setCustomValidity( 122 invalid_password_email_l10n_message 123 ); 124 } else { 125 this.inputNewPasswordEl.setCustomValidity(""); 126 } 127 128 const newPassValidity = this.inputNewPasswordEl.validity; 129 this._tooShort = newPassValidity?.valueMissing || newPassValidity?.tooShort; 130 131 const newInvalid = !newPassValidity?.valid; 132 this.setInputValidity( 133 this.inputNewPasswordEl, 134 !newInvalid, 135 "password-rules-tooltip" 136 ); 137 138 this._passwordsMatch = 139 this.inputNewPasswordEl.value == this.inputRepeatPasswordEl.value; 140 141 if (!this._passwordsMatch) { 142 this.inputRepeatPasswordEl.setCustomValidity( 143 l10n.formatValueSync("password-validity-do-not-match") 144 ); 145 this.setInputValidity( 146 this.inputRepeatPasswordEl, 147 false, 148 "repeat-password-error" 149 ); 150 document.l10n.setAttributes( 151 this.repeatPasswordErrorEl, 152 "password-validity-do-not-match" 153 ); 154 } else { 155 this.inputRepeatPasswordEl.setCustomValidity(""); 156 this.setInputValidity(this.inputRepeatPasswordEl, true); 157 } 158 159 const repeatPassValidity = this.inputRepeatPasswordEl.validity; 160 this._passwordsValid = 161 newPassValidity?.valid && 162 repeatPassValidity?.valid && 163 this._passwordsMatch; 164 } 165 166 /** 167 * Dispatches a custom event whenever validity changes. 168 * 169 * @param {Map<string, any>} changedProperties a Map of recently changed properties and their new values 170 */ 171 updated(changedProperties) { 172 if (!changedProperties.has("_passwordsValid")) { 173 return; 174 } 175 176 if (this._passwordsValid) { 177 this.dispatchEvent( 178 new CustomEvent("ValidPasswordsDetected", { 179 bubbles: true, 180 composed: true, 181 detail: { 182 password: this.inputNewPasswordEl.value, 183 }, 184 }) 185 ); 186 } else { 187 this.dispatchEvent( 188 new CustomEvent("InvalidPasswordsDetected", { 189 bubbles: true, 190 composed: true, 191 }) 192 ); 193 } 194 } 195 196 contentTemplate() { 197 return html` 198 <div id="password-inputs-wrapper" aria-live="polite"> 199 <form id="password-inputs-form"> 200 <!--TODO: (bug 1909983) change first input field label for the "change-password" dialog--> 201 <label id="new-password-label" for="new-password-input"> 202 <div id="new-password-label-wrapper-span-input"> 203 <span 204 id="new-password-span" 205 data-l10n-id=${this.createPasswordLabelL10nId || 206 "enable-backup-encryption-create-password-label"} 207 ></span> 208 <input 209 type="password" 210 id="new-password-input" 211 minlength="8" 212 required 213 aria-describedby="password-rules-tooltip" 214 @input=${this.handleChangeNewPassword} 215 @blur=${this.handleBlurNewPassword} 216 @mouseenter=${this.handleFocusNewPassword} 217 @focus=${this.handleFocusNewPassword} 218 /> 219 <!--TODO: (bug 1909984) improve how we read out the first input field for screen readers--> 220 </div> 221 </label> 222 <!--TODO: (bug 1909984) look into how the tooltip vs dialog behaves when pressing the ESC key--> 223 <password-rules-tooltip 224 id="password-rules" 225 role="tooltip" 226 .hasEmail=${this._hasEmail} 227 .tooShort=${this._tooShort} 228 ?embedded-fx-backup-opt-in=${this.embeddedFxBackupOptIn} 229 ></password-rules-tooltip> 230 <label id="repeat-password-label" for="repeat-password-input"> 231 <span 232 id="repeat-password-span" 233 data-l10n-id="enable-backup-encryption-repeat-password-label" 234 ></span> 235 <input 236 type="password" 237 id="repeat-password-input" 238 required 239 @input=${this.handleChangeRepeatPassword} 240 /> 241 <span 242 id="repeat-password-error" 243 role="alert" 244 class="field-error" 245 ></span> 246 </label> 247 </form> 248 </div> 249 `; 250 } 251 252 render() { 253 return html` 254 <link 255 rel="stylesheet" 256 href="chrome://browser/content/backup/password-validation-inputs.css" 257 /> 258 ${this.contentTemplate()} 259 `; 260 } 261 } 262 263 customElements.define("password-validation-inputs", PasswordValidationInputs);