From c2a7b22f31a9e86f49a3794c6a47afa47bbb5cf8 Mon Sep 17 00:00:00 2001 From: "Daniele T." Date: Fri, 11 Oct 2024 10:14:34 +0200 Subject: [PATCH] feat(a11y): add accessibility features to input password Co-authored-by: Francesco Improta Co-authored-by: Andrea Stagi Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: astagi Co-authored-by: Lollo176ITA <116870391+Lollo176ITA@users.noreply.github.com> --- docs/form/input.md | 347 +++++++++++++++++++--------- docs/form/introduzione.md | 14 +- src/js/plugins/input-password.js | 321 +++++++++++++++---------- src/scss/custom/_form-password.scss | 2 +- 4 files changed, 447 insertions(+), 237 deletions(-) diff --git a/docs/form/input.md b/docs/form/input.md index 116b0d8d78..334bd1f4f6 100644 --- a/docs/form/input.md +++ b/docs/form/input.md @@ -35,6 +35,18 @@ Rimosso l'elemento con classe `.input-group-prepend` in favore dell'elemento con classe `.input-group-text`. {% endcapture %}{% include callout.html content=callout type="danger" %} +{% capture callout %} +Breaking feature dalla versione **2.10.0** + +Il campo di input di tipo Password è stato rivisto in chiave accessibilità e robustezza. In particolare cosa cambia: + - L'elemento `input.input-password` ha ora il corretto attributo `aria-describedby` al posto di `aria-labelledby`. + - L'elemento `.password-icon` diventa un `button` con `role=switch` e uso dell'attributo `aria-checked` per lo stato. + - Rivisto l'ordine degli elementi nell'HTML per rispecchiare la struttura corretta degli elementi. + - Alcuni elementi `small` o `span` diventano `p`, ad esempio per la personalizzazione con attributi `data` delle varianti più avanzate ci si rivolgerà verso l'elemento `p.strength-meter-info` e non più al generico `small.form-text`. + - Rimosso il controllo per il Caps-lock inserito, per non interferire con i tasti modificatori delle tecnologie assistive. + - Aggiunta una variante con misuratore di sicurezza e suggerimenti. +{% endcapture %}{% include callout.html content=callout type="danger" %} + {% comment %}Example name: Varianti per tipo {% endcomment %} {% capture example %}
@@ -86,11 +98,8 @@ Si può abbinare all'etichetta un _placeholder_ (testo di esempio) per ulteriore In caso di necessità, è anche possibile utilizzare un ulteriore contenuto testuale sotto il campo di testo, aggiungendo un elemento `` con classe `.form-text` all'interno di `.form-group`. {% capture callout %} - -#### Associazione del testo di aiuto con gli elementi del modulo form - -Il testo di aiuto deve essere esplicitamente associato agli elementi del modulo form a cui si riferisce utilizzando l'attributo `aria-describedby`. Ciò garantirà che le tecnologie assistive, come gli screenreader, leggano questo testo di aiuto quando l'utente avrà il focus sull'elemento. - +#### Accessibilità: associazione del testo di aiuto con con i campi +Il testo di aiuto deve essere esplicitamente associato ai campi a cui si riferisce utilizzando l'attributo `aria-describedby`. Ciò garantirà che le tecnologie assistive, come i lettori di schermo, leggano questo testo di aiuto quando l'utente avrà il focus sull'elemento. {% endcapture %}{% include callout.html content=callout type="accessibility" %} {% comment %}Example name: Con testo di aiuto {% endcomment %} @@ -138,11 +147,161 @@ Il testo di aiuto deve essere esplicitamente associato agli elementi del modulo
{% endcapture %}{% include example.html content=example %} -### Input password +### Disabilitato + +Aggiungi l'attributo `disabled` ad un input per impedire la modifica del valore contenuto e non inviare i dati in esso contenuti. + +{% comment %}Example name: Disabilitato {% endcomment %} +{% capture example %} +
+ + +
+{% endcapture %}{% include example.html content=example %} + +### Readonly + +Aggiungi l'attributo `readonly` ad un input per impedire la modifica del valore contenuto. + +{% comment %}Example name: Solo lettura {% endcomment %} +{% capture example %} +
+ + +
+{% endcapture %}{% include example.html content=example %} + +#### Readonly normalizzato -Per rendere più semplice l'inserimento della password, l'elemento è stato dotato di un visualizzatore dei caratteri digitati. Inoltre è possibile abbinare un controllo per segnalare quanto la password che si sta inserendo sia sicura con l'aggiunta dell'HTML necessario. +Se per qualche motivo vuoi avere gli elementi `` nella forma stilizzata come testo normale usa la classe `.form-control-plaintext` anziché `.form-control`. + +{% comment %}Example name: Solo lettura, normalizzato {% endcomment %} +{% capture example %} +
+
+ + +
+
+{% endcapture %}{% include example.html content=example %} -È possibile personalizzare la componente `strength meter` usando gli attributi data. +## Input password + +Per rendere più semplice l'inserimento della password, il campo Input di tipo password è dotato di un pulsante che permette di mostrare i caratteri inseriti. Inoltre, è possibile abbinare una descrizione estesa che ne aiuti la compilazine, ad esempio in fase di scelta di una nuova password. + +{% comment %}Example name: Password base{% endcomment %} +{% capture example %} + +

Base, login

+
+ + + +
+ +

Con descrizione estesa

+
+ + + +

Inserisci almeno 8 caratteri e alcuni caratteri speciali.

+
+{% endcapture %}{% include example.html content=example %} + +### Password con misuratore sicurezza e suggerimenti + +Nel caso di un campo per la scelta di una nuova password, è possibile abbinare controlli per segnalare quanto la password che si sta inserendo segua alcuni suggerimenti di sicurezza, come la lunghezza minima o l'uso di caratteri speciali. Inoltre, è possibile restituire all'utente una lista dei suggerimenti, con indicati quelli che sono soddistatti. + +{% capture callout %} +#### Importante sulla sicurezza per l'uso in produzione +Le due varianti del componente che seguono, con funzionalità avanzate di suggerimento e guida dell'utente nella scelta della password, sono da considerarsi esempi da usare per studio e ricerca. **Vi consigliamo di coinvolgere un esperto di sicurezza prima di implementarle in ambienti di produzione** e, in ogni caso, di valutate se riscrivere le funzioni di calcolo del punteggio, disponibili nel plugin `input-password.js`, adattandole al contesto o a cambiamenti negli standard internazionali riconosciuti. Ad esempio potreste voler cambiare i calcoli per la forza e aggiustare i relativi suggerimenti, oppure integrare con controlli per password più comuni od oggetto di leak conosciuti. +{% endcapture %}{% include callout.html content=callout type="warning" %} + +{% capture callout %} +#### Accessibilità dei testi di aiuto +Nel caso del campo di tipo password, è molto importante configurare correttamente l'attributo `aria-describedby` dell'elemento `input.input-password`, indicando non solo l'`id` del testo di aiuto alla compilazione se presente, ma tutti gli `id` di eventuali altri elementi con testi utili, come ad esempio i testi di suggerimento o di valutazione della forza della password presenti nelle varianti che seguono. +{% endcapture %}{% include callout.html content=callout type="accessibility" %} + +{% capture callout %} +#### Accessibilità degli annunci di sicurezza +È da notare che l'elemento `p.strength-meter-info` ha l'attributo `aria-live="polite"` per permettere di percepire gli annunci di cambio stato della valutazione sicurezza alle tecnologie assistive, come i lettori di schermo. +{% endcapture %}{% include callout.html content=callout type="accessibility" %} + + +{% comment %}Example name: Password con misuratore sicurezza e suggerimenti{% endcomment %} +{% capture example %} + +

Con descrizione e misuratore sicurezza

+
+ + + +

Inserisci almeno 8 caratteri, combinando maiuscole, numeri e caratteri speciali.

+
+

+
+
+
+
+
+
+
+
+
+
+
+ +

Con misuratore sicurezza e suggerimenti puntuali

+
+ + + +

+
+
+

+
+
+
+
+
+
+
+
+
+
+
+{% endcapture %}{% include example.html content=example %} + +#### Personalizzazione + +È possibile personalizzare le varianti con misuratore di sicurezza usando specifici attributi `data` dell'elemento `p.strength-meter-info`. @@ -155,87 +314,87 @@ Per rendere più semplice l'inserimento della password, l'elemento è stato dota - - + + - -
data-bs-minimum-lengthLunghezza minima per il calcolo della forza della password (soglia password molto debole)4Lunghezza minima per il calcolo della forza della password (soglia password troppo breve)8
- -È possibile personalizzare i testi dei messaggi riguardanti la robustezza della password usando gli attributi data dell'elemento `small.form-text`. - - - - - - - - - - - - + + - + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Attributo dataDescrizioneDefault
data-bs-short-passTesto per il punteggio di forza della password minimoPassword molto deboleTesto per il punteggio di forza della password troppo brevePassword troppo breve.
data-bs-bad-pass Testo per punteggio di forza della password bassoPassword debolePassword debole.
data-bs-good-pass Testo per punteggio di forza della password buonoPassword sicuraPassword abbastanza sicura.
data-bs-strong-pass Testo per il punteggio di forza della password massimoPassword molto sicura
Password sicura.
data-bs-suggestions-labelTesto nascosto visivamente che precede l'elenco di suggerimentiSuggerimenti per una buona password:
data-bs-suggestion-followedTesto singolare per la parte finale di "1 di 5 suggerimenti seguito"suggerimenti seguito
data-bs-suggestion-followed-pluralTesto plurale per la parte finale di "2 di 5 suggerimenti seguiti"suggerimenti seguiti
data-bs-suggestion-ofPreposizione centrale per "2 di 5"di
data-bs-suggestion-met-labelAria-label per l'icona di suggerimento soddisfattoSoddisfatto:
data-bs-suggestion-met-icon-pathPath per la forma dell'icona di suggerimento soddisfattoM9.6 16.9 4 11.4l.8-.7 4.8 4.8 8.5-8.4.7.7-9.2 9.1z
data-bs-suggestion-lengthLunghezza minima della passwordAlmeno {minLength} caratteri.
data-bs-suggestion-uppercaseUso di lettere maiuscoleUna o più maiuscole.
data-bs-suggestion-lowercaseUso di lettere minuscoleUna o più minuscole.
data-bs-suggestion-numberUso di numeriUna o più mumero.
data-bs-suggestion-specialUso di caratteri specialiUno o più caratteri speciali.
-{% comment %}Example name: Password {% endcomment %} -{% capture example %} -
-
- - - - Inserisci almeno 8 caratteri e una lettera maiuscola -
-
- - -
- Inserisci almeno 8 caratteri e una lettera maiuscola -
-
-
-
-
-
-
-
-
-
- - CAPS LOCK inserito -
-
-{% endcapture %}{% include example.html content=example %} - #### Attivazione tramite JavaScript Abilitarlo manualmente con: @@ -243,11 +402,11 @@ Abilitarlo manualmente con: ```js var inputElement = document.querySelector('#exampleInputPassword')) var passwordComponent = new bootstrap.InputPassword(inputElement, { - minimumLength: 4, + minimumLength: 8, }) ``` -#### Opzioni +Opzioni: @@ -260,50 +419,12 @@ var passwordComponent = new bootstrap.InputPassword(inputElement, { - - + +
minimumLengthLunghezza minima per il calcolo della forza della password (soglia password molto debole)4Lunghezza minima per il calcolo della forza della password (soglia password troppo breve)8
-### Disabilitato - -Aggiungi l'attributo `disabled` ad un input per impedire la modifica del valore contenuto e non inviare i dati in esso contenuti. - -{% comment %}Example name: Disabilitato {% endcomment %} -{% capture example %} -
- - -
-{% endcapture %}{% include example.html content=example %} - -### Readonly - -Aggiungi l'attributo `readonly` ad un input per impedire la modifica del valore contenuto. - -{% comment %}Example name: Solo lettura {% endcomment %} -{% capture example %} -
- - -
-{% endcapture %}{% include example.html content=example %} - -#### Readonly normalizzato - -Se per qualche motivo vuoi avere gli elementi `` nella forma stilizzata come testo normale usa la classe `.form-control-plaintext` anziché `.form-control`. - -{% comment %}Example name: Solo lettura, normalizzato {% endcomment %} -{% capture example %} -
-
- - -
-
-{% endcapture %}{% include example.html content=example %} - ## Ricerca con autocompletamento Per ottenere un input con un risultato ricerca o un autocomplete statico è necessario aggiungere all'input la classe `.autocomplete` e l'attributo `data-bs-autocomplete` contenente un JSON da filtrare. diff --git a/docs/form/introduzione.md b/docs/form/introduzione.md index 12bbbeb43e..2ad0e73143 100755 --- a/docs/form/introduzione.md +++ b/docs/form/introduzione.md @@ -67,12 +67,14 @@ Ecco l'esempio di una struttura più complessa creata con il sistema a griglie.
- - - + + + +

Inserisci almeno 8 caratteri e alcuni caratteri speciali.

diff --git a/src/js/plugins/input-password.js b/src/js/plugins/input-password.js index 8f5cca2f14..a6fc22a3dd 100644 --- a/src/js/plugins/input-password.js +++ b/src/js/plugins/input-password.js @@ -3,7 +3,6 @@ import EventHandler from 'bootstrap/js/src/dom/event-handler' import SelectorEngine from 'bootstrap/js/src/dom/selector-engine' import Manipulator from 'bootstrap/js/src/dom/manipulator' -//import Input from './input' import InputLabel from './input-label' const NAME = 'inputpassword' @@ -12,38 +11,45 @@ const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' const Default = { - shortPass: 'Password molto debole', - badPass: 'Password debole', - goodPass: 'Password sicura', - strongPass: 'Password molto sicura', - enterPass: 'Inserisci almeno 8 caratteri e una lettera maiuscola', - alertCaps: 'CAPS LOCK inserito', - showText: true, - minimumLength: 4, + shortPass: 'Password troppo breve', + badPass: 'Password debole.', + goodPass: 'Password abbastanza sicura.', + strongPass: 'Password sicura.', + minimumLength: 8, + suggestionsLabel: 'Suggerimenti per una buona password:', + suggestionFollowed: 'suggerimenti seguito', + suggestionFollowedPlural: 'suggerimenti seguiti', + suggestionOf: 'di', + suggestionMetLabel: 'Soddisfatto: ', + suggestionMetIconPath: ` + M9.6 16.9 4 11.4l.8-.7 4.8 4.8 8.5-8.4.7.7-9.2 9.1z + `, + suggestionLength: 'Almeno {minLength} caratteri.', + suggestionUppercase: 'Una o più maiuscole.', + suggestionLowercase: 'Una o più minuscole.', + suggestionNumber: 'Uno o più numeri.', + suggestionSpecial: 'Uno o più caratteri speciali.', } const EVENT_CLICK = `click${EVENT_KEY}` const EVENT_KEYUP = `keyup${EVENT_KEY}` -const EVENT_KEYDOWN = `keydown${EVENT_KEY}` -const EVENT_KEYPRESS = `keypress${EVENT_KEY}` const EVENT_SCORE = `score${EVENT_KEY}` const EVENT_TEXT = `text${EVENT_KEY}` +const EVENT_SUGGS = `suggs${EVENT_KEY}` const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}` const EVENT_MOUSEDOWN_DATA_API = `mousedown${EVENT_KEY}${DATA_API_KEY}` const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}` const CLASS_NAME_PASSWORD = 'input-password' -//const CLASS_NAME_METER = 'input-password-strength-meter' -const CLASS_NAME_SHOW = 'show' const SELECTOR_PASSWORD = 'input[data-bs-input][type="password"]' const SELECTOR_BTN_SHOW_PWD = '.password-icon' const SELECTOR_METER = '.password-strength-meter' const SELECTOR_METER_GRAYBAR = '.password-meter' const SELECTOR_METER_COLBAR = '.progress-bar' -const SELECTOR_CAPS = '.password-caps' -const SELECTOR_TEXT = '.form-text' +const SELECTOR_METER_TEXT = '.strength-meter-info' +const SELECTOR_METER_SUGGS = '.strenght-meter-suggestions' class InputPassword extends BaseComponent { constructor(element, config) { @@ -52,31 +58,54 @@ class InputPassword extends BaseComponent { this._config = this._getConfig(config) this._isCustom = this._element.classList.contains(CLASS_NAME_PASSWORD) this._meter = this._element.parentNode.querySelector(SELECTOR_METER) - this._isShiftPressed = false - this._isCapsOn = false this._grayBarElement = null this._colorBarElement = null this._textElement = null - this._capsElement = null + this._suggsElement = null this._showPwdElement = null this._text = {} this._label = new InputLabel(element) + this._suggestions = [ + { + key: 'length', + text: (config) => config.suggestionLength.replace('{minLength}', config.minimumLength.toString()), + test: (password, config) => password.length >= config.minimumLength, + }, + { + key: 'uppercase', + text: (config) => config.suggestionUppercase, + test: (password) => /[A-Z]/.test(password), + }, + { + key: 'lowercase', + text: (config) => config.suggestionLowercase, + test: (password) => /[a-z]/.test(password), + }, + { + key: 'number', + text: (config) => config.suggestionNumber, + test: (password) => /[0-9]/.test(password), + }, + { + key: 'special', + text: (config) => config.suggestionSpecial, + test: (password) => /[^A-Za-z0-9]/.test(password), + }, + ] + this._init() this._bindEvents() } // Getters - static get NAME() { return NAME } - // Public - // Private _getConfig(config) { config = { @@ -88,118 +117,174 @@ class InputPassword extends BaseComponent { } _init() { - if (this._meter) { - this._grayBarElement = this._meter.querySelector(SELECTOR_METER_GRAYBAR) - this._colorBarElement = this._meter.querySelector(SELECTOR_METER_COLBAR) - this._textElement = this._meter.querySelector(SELECTOR_TEXT) - - if (this._textElement) { - this._config = Object.assign({}, this._config, { ...Manipulator.getDataAttributes(this._textElement) }, { enterPass: this._textElement.innerText }) - } - } if (this._isCustom) { - this._capsElement = this._element.parentNode.querySelector(SELECTOR_CAPS) + this._handleAutofill() + this._showPwdElement = SelectorEngine.findOne(SELECTOR_BTN_SHOW_PWD, this._element.parentNode) + if (this._meter) { + this._grayBarElement = this._meter.querySelector(SELECTOR_METER_GRAYBAR) + this._colorBarElement = this._meter.querySelector(SELECTOR_METER_COLBAR) + this._textElement = this._meter.querySelector(SELECTOR_METER_TEXT) + this._suggsElement = this._meter.querySelector(SELECTOR_METER_SUGGS) + if (this._textElement) { + this._config = Object.assign({}, this._config, { ...Manipulator.getDataAttributes(this._textElement) }) + } + if (this._suggsElement) { + this._createsuggestionsList() + } + } } - - this._showPwdElement = SelectorEngine.findOne(SELECTOR_BTN_SHOW_PWD, this._element.parentNode) } _bindEvents() { - if (this._meter) { - EventHandler.on(this._element, EVENT_KEYUP, () => this._checkPassword()) - } - if (this._isCustom) { - EventHandler.on(this._element, EVENT_KEYDOWN, (evt) => { - if (evt.key === 'Shift') { - this._isShiftPressed = true - } - }) - EventHandler.on(this._element, EVENT_KEYUP, (evt) => { - if (evt.key === 'Shift') { - this._isShiftPressed = false - } - if (evt.key === 'CapsLock') { - this._isCapsOn = !this._isCapsOn - if (this._isCapsOn) { - this._showCapsMsg() - } else { - this._hideCapsMsg() - } - } - }) - EventHandler.on(this._element, EVENT_KEYPRESS, (evt) => { - const matches = evt.key.match(/[A-Z]$/) || [] - if (matches.length > 0 && !this._isShiftPressed) { - this._isCapsOn = true - this._showCapsMsg() - } else if (this._isCapsOn) { - this._isCapsOn = false - this._hideCapsMsg() - } - }) - } - - if (this._showPwdElement) { - EventHandler.on(this._showPwdElement, EVENT_CLICK, () => this._toggleShowPassword()) + if (this._showPwdElement) { + EventHandler.on(this._showPwdElement, EVENT_CLICK, () => this._toggleShowPassword()) + } + if (this._meter) { + EventHandler.on(this._element, EVENT_KEYUP, () => this._checkPassword()) + EventHandler.on(this._element, 'input', () => this._checkPassword()) + } } } - _showCapsMsg() { - if (this._capsElement) { - this._capsElement.classList.add(CLASS_NAME_SHOW) - } - } - _hideCapsMsg() { - if (this._capsElement) { - this._capsElement.classList.remove(CLASS_NAME_SHOW) + _handleAutofill() { + const checkAndActivate = () => { + if (this._element.value !== '') { + this._label._labelOut() + this._checkPassword() + } } + checkAndActivate() + setTimeout(checkAndActivate, 100) + EventHandler.on(this._element, 'animationstart', (event) => { + if (event.animationName === 'onAutoFillStart') { + checkAndActivate() + } + }) } _toggleShowPassword() { const toShow = this._element.getAttribute('type') === 'password' SelectorEngine.find('[class^="password-icon"]', this._showPwdElement).forEach((icon) => icon.classList.toggle('d-none')) - if (toShow) { - this._element.setAttribute('type', 'text') - } else { - this._element.setAttribute('type', 'password') - } + this._element.setAttribute('type', toShow ? 'text' : 'password') + this._showPwdElement.setAttribute('aria-checked', toShow.toString()) } _checkPassword() { - const score = this._calculateScore(this._element.value) - const perc = score < 0 ? 0 : score - - this._colorBarElement.classList.forEach((className) => { - if (className.match(/(^|\s)bg-\S+/g)) { - this._colorBarElement.classList.remove(className) - } - }) - this._colorBarElement.classList.add('bg-' + this._scoreColor(score)) - this._colorBarElement.style.width = perc + '%' - this._colorBarElement.setAttribute('aria-valuenow', perc) + const password = this._element.value + const score = this._calculateScore(password) + this._updateMeter(score) + this._updateText(score, password) + this._updateSuggestions(password) + } + _updateMeter(score) { + const perc = score < 0 ? 0 : score + if (this._colorBarElement) { + this._colorBarElement.classList.forEach((className) => { + if (className.match(/(^|\s)bg-\S+/g)) { + this._colorBarElement.classList.remove(className) + } + }) + this._colorBarElement.classList.add(`bg-${this._scoreColor(score)}`) + this._colorBarElement.style.width = `${perc}%` + this._colorBarElement.setAttribute('aria-valuenow', perc) + } EventHandler.trigger(this._element, EVENT_SCORE) + } + _updateText(score, password) { if (this._textElement) { let text = this._scoreText(score) - if (this._element.value.length === 0 && score <= 0) { - text = this._config.enterPass + if (this._suggsElement) { + const { completedCount, totalCount } = this._getCompletedSuggestions(password) + const suggestionText = completedCount === 1 ? this._config.suggestionFollowed : this._config.suggestionFollowedPlural + text += ` ${completedCount} ${this._config.suggestionOf} ${totalCount} ${suggestionText}.` } - - if (this._textElement.innerHTML.search(text) === -1) { - this._textElement.innerHTML = text + if (this._textElement.textContent !== text) { + this._textElement.textContent = text this._textElement.classList.forEach((className) => { if (className.match(/(^|\s)text-\S+/g)) { this._textElement.classList.remove(className) } }) - this._textElement.classList.add('text-' + this._scoreColor(score)) + this._textElement.classList.add(`text-${this._scoreColor(score)}`) EventHandler.trigger(this._element, EVENT_TEXT) } } } + _updateSuggestions(password) { + if (this._suggsElement) { + this._updateSuggestionsList(password) + EventHandler.trigger(this._element, EVENT_SUGGS) + } + } + + _getCompletedSuggestions(password) { + let completedCount = 0 + const totalCount = this._suggestions.length + this._suggestions.forEach((sugg) => { + if (sugg.test(password, this._config)) completedCount++ + }) + return { completedCount, totalCount } + } + + _createsuggestionsList() { + if (this._suggsElement.querySelector('.password-suggestions')) { + return + } + const suggLabel = document.createElement('label') + suggLabel.className = 'visually-hidden' + suggLabel.htmlFor = 'suggestions' + suggLabel.textContent = Default.suggestionsLabel + const suggContainer = document.createElement('div') + suggContainer.id = 'suggestions' + suggContainer.className = 'password-suggestions' + + this._suggestions.forEach((sugg) => { + const suggElement = document.createElement('div') + suggElement.className = 'suggestion' + suggElement.dataset.suggestion = sugg.key + const checkIcon = this._createIconCheck() + const textSpan = document.createElement('span') + textSpan.textContent = sugg.text(this._config) + suggElement.appendChild(checkIcon) + suggElement.appendChild(textSpan) + suggContainer.appendChild(suggElement) + }) + + this._suggsElement.appendChild(suggLabel) + this._suggsElement.appendChild(suggContainer) + } + + _createIconCheck() { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') + svg.setAttribute('class', `icon icon-xs me-1 d-none`) + svg.setAttribute('aria-label', this._config.suggestionMetLabel) + svg.setAttribute('viewBox', '0 0 24 24') + svg.style.width = '1em' + svg.style.height = '1em' + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path') + path.setAttribute('d', this._config.suggestionMetIconPath.trim()) + svg.appendChild(path) + return svg + } + + _updateSuggestionsList(password) { + if (!this._suggsElement) return + this._suggestions.forEach((sugg) => { + const suggElement = this._suggsElement.querySelector(`[data-suggestion="${sugg.key}"]`) + if (suggElement) { + const isMet = sugg.test(password, this._config) + const checkIcon = suggElement.querySelector('.icon') + if (checkIcon) { + checkIcon.classList.toggle('d-none', !isMet) + } + } + }) + } + /** * Returns strings based on the score given. * @@ -211,10 +296,14 @@ class InputPassword extends BaseComponent { return this._config.shortPass } + if (score === -2) { + return '' + } + score = score < 0 ? 0 : score if (score < 26) { - return this._config.shortPass + return this._config.badPass } if (score < 51) { return this._config.badPass @@ -227,16 +316,7 @@ class InputPassword extends BaseComponent { } _scoreColor(score) { - if (score === -1) { - return 'danger' - } - if (score === -2) { - return 'muted' - } - - score = score < 0 ? 0 : score - - if (score < 26) { + if (score === -1 || score === -2 || score < 26) { return 'danger' } if (score < 51) { @@ -245,7 +325,6 @@ class InputPassword extends BaseComponent { if (score < 76) { return 'success' } - return 'success' } @@ -281,7 +360,7 @@ class InputPassword extends BaseComponent { score += 5 } - // password has at least 2 sybols + // password has at least 2 symbols var symbols = '.*[!,@,#,$,%,^,&,*,?,_,~]' symbols = new RegExp('(' + symbols + symbols + ')') if (password.match(symbols)) { @@ -360,11 +439,6 @@ class InputPassword extends BaseComponent { * ------------------------------------------------------------------------ */ -/*const inputs = SelectorEngine.find(SELECTOR_PASSWORD) -inputs.forEach((input) => { - InputPassword.getOrCreateInstance(input) -})*/ - const createInput = (element) => { if (element && element.matches(SELECTOR_PASSWORD)) { return InputPassword.getOrCreateInstance(element) @@ -372,6 +446,19 @@ const createInput = (element) => { return null } +const initInputPassword = () => { + const element = SelectorEngine.findOne(SELECTOR_PASSWORD) + if (element) { + InputPassword.getOrCreateInstance(element) + } +} + +if (document.readyState !== 'loading') { + initInputPassword() +} else { + document.addEventListener('DOMContentLoaded', initInputPassword) +} + EventHandler.on(document, EVENT_MOUSEDOWN_DATA_API, SELECTOR_PASSWORD + ', label', function () { const target = InputLabel.getInputFromLabel(this) || this createInput(target) diff --git a/src/scss/custom/_form-password.scss b/src/scss/custom/_form-password.scss index e6f17e2e28..438f6491e4 100644 --- a/src/scss/custom/_form-password.scss +++ b/src/scss/custom/_form-password.scss @@ -12,7 +12,7 @@ } .password-meter { height: 4px; - left: 10px; + left: 7px; bottom: -6px; width: 100%; max-width: 180px;