Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add a11y features to input password #1232

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
57 changes: 30 additions & 27 deletions docs/form/input.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,9 @@ Il testo di aiuto deve essere esplicitamente associato agli elementi del modulo

### Input password

Per rendere più semplice l'inserimento della password, l'elemento è stato dotato di un visualizzatore dei caratteri digitati. Inoltre è possibile abbinare un controllo <!--(grazie alla componente [strength meter](https://www.npmjs.com/package/password-strength-meter))--> per segnalare quanto la password che si sta inserendo sia sicura con l'aggiunta dell'HTML necessario.
Per rendere più semplice l'inserimento della password, l'elemento è dotato di un pulsante che permette di mostrare i caratteri inseriti.

È possibile personalizzare la componente `strength meter` usando gli attributi data.
Inoltre, nel caso di un campo Input password utilizzato per la scelta di una password, è possibile abbinare un controllo per segnalare quanto la password che si sta inserendo sia sicura, con l'aggiunta dell'HTML necessario. È possibile personalizzare alcuni messaggi di questa variante con `strength meter` usando specifici attributi `data`.

<table class="table table-bordered table-striped">
<thead>
Expand All @@ -156,7 +156,7 @@ Per rendere più semplice l'inserimento della password, l'elemento è stato dota
<tr>
<td><code>data-bs-minimum-length</code></td>
<td>Lunghezza minima per il calcolo della forza della password (soglia password molto debole)</td>
<td>4</td>
<td>8</td>
</tr>
</tbody>
</table>
Expand All @@ -175,22 +175,22 @@ Per rendere più semplice l'inserimento della password, l'elemento è stato dota
<tr>
<td><code>data-bs-short-pass</code></td>
<td>Testo per il punteggio di forza della password minimo</td>
<td>Password molto debole</td>
<td>Password molto debole. </td>
</tr>
<tr>
<td><code>data-bs-bad-pass</code></td>
<td>Testo per punteggio di forza della password basso</td>
<td>Password debole</td>
<td>Password debole. </td>
</tr>
<tr>
<td><code>data-bs-good-pass</code></td>
<td>Testo per punteggio di forza della password buono</td>
<td>Password sicura</td>
<td>Password sicura. </td>
</tr>
<tr>
<td><code>data-bs-strong-pass</code></td>
<td>Testo per il punteggio di forza della password massimo</td>
<td>Password molto sicura</td>
<td>Password molto sicura. </td>
</tr>
</tbody>
</table>
Expand All @@ -200,23 +200,24 @@ Per rendere più semplice l'inserimento della password, l'elemento è stato dota
<div>
<div class="form-group">
<label for="exampleInputPassword">Password con label, placeholder e testo di aiuto</label>
<input type="password" data-bs-input class="form-control input-password" id="exampleInputPassword" aria-labelledby="infoPassword">
<span class="password-icon" aria-hidden="true">
<svg class="password-icon-visible icon icon-sm"><use href="{{ site.baseurl }}/dist/svg/sprites.svg#it-password-visible"></use></svg>
<svg class="password-icon-invisible icon icon-sm d-none"><use href="{{ site.baseurl }}/dist/svg/sprites.svg#it-password-invisible"></use></svg>
</span>
<small id="infoPassword" class="form-text">Inserisci almeno 8 caratteri e una lettera maiuscola</small>
<input type="password" data-bs-input class="form-control input-password" id="exampleInputPassword" aria-describedby="infoPassword">
<button type="button" class="password-icon btn" role="switch" aria-checked="false">
<span class="password-sr-text visually-hidden">Mostra/Nascondi Password</span>
<svg class="password-icon-visible icon icon-sm" aria-hidden="true"><use href="{{ site.baseurl }}/dist/svg/sprites.svg#it-password-visible"></use></svg>
<svg class="password-icon-invisible icon icon-sm d-none" aria-hidden="true"><use href="{{ site.baseurl }}/dist/svg/sprites.svg#it-password-invisible"></use></svg>
</button>
<small id="infoPassword" class="form-text">Inserisci almeno 8 caratteri, una lettera maiuscola e un carattere speciale.</small>
</div>
<div class="form-group">
<label for="exampleInputPassword3">Password con strength meter</label>
<input type="password" data-bs-input class="form-control input-password" id="exampleInputPassword3">
<input type="password" data-bs-input class="form-control input-password" id="exampleInputPassword3" aria-describedby="strengthMeter strengthInfo capsLockWarning">
<div class="password-strength-meter">
<small class="form-text text-muted"
data-bs-short-pass="Password molto debole"
data-bs-bad-pas="Password debole"
data-bs-good-pass="Password sicura"
data-bs-strong-pass="Password molto sicura"
>Inserisci almeno 8 caratteri e una lettera maiuscola</small>
<small id="strengthInfo" class="form-text text-muted"
data-bs-short-pass="Password molto debole. "
data-bs-bad-pas="Password debole. "
data-bs-good-pass="Password sicura. "
data-bs-strong-pass="Password molto sicura. "
>Inserisci almeno 8 caratteri, una lettera maiuscola e un carattere speciale.</small>
<div class="password-meter progress rounded-0 position-absolute">
<div class="row position-absolute w-100 m-0">
<div class="col-3 border-start border-end border-white"></div>
Expand All @@ -227,11 +228,13 @@ Per rendere più semplice l'inserimento della password, l'elemento è stato dota
<div class="progress-bar bg-muted" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
<span class="password-icon" aria-hidden="true">
<svg class="password-icon-visible icon icon-sm"><use href="{{ site.baseurl }}/dist/svg/sprites.svg#it-password-visible"></use></svg>
<svg class="password-icon-invisible icon icon-sm d-none"><use href="{{ site.baseurl }}/dist/svg/sprites.svg#it-password-invisible"></use></svg>
</span>
<small class="password-caps form-text text-warning position-absolute bg-white w-100">CAPS LOCK inserito</small>
<button type="button" class="password-icon btn" role="switch" aria-checked="false">
<span class="password-sr-text visually-hidden">Mostra/Nascondi Password</span>
<svg class="password-icon-visible icon icon-sm" aria-hidden="true"><use href="{{ site.baseurl }}/dist/svg/sprites.svg#it-password-visible"></use></svg>
<svg class="password-icon-invisible icon icon-sm d-none" aria-hidden="true"><use href="{{ site.baseurl }}/dist/svg/sprites.svg#it-password-invisible"></use></svg>
</button>
<small id="capsLockWarning" class="password-caps form-text text-warning position-absolute bg-white w-100" style="display: none;" aria-live="polite"></small>
<div id="strengthMeter" class="visually-hidden" aria-live="polite"></div>
</div>
</div>
{% endcapture %}{% include example.html content=example %}
Expand All @@ -243,7 +246,7 @@ Abilitarlo manualmente con:
```js
var inputElement = document.querySelector('#exampleInputPassword'))
var passwordComponent = new bootstrap.InputPassword(inputElement, {
minimumLength: 4,
minimumLength: 8,
})
```

Expand All @@ -261,7 +264,7 @@ var passwordComponent = new bootstrap.InputPassword(inputElement, {
<tr>
<td><code>minimumLength</code></td>
<td>Lunghezza minima per il calcolo della forza della password (soglia password molto debole)</td>
<td>4</td>
<td>8</td>
</tr>
</tbody>
</table>
Expand Down
154 changes: 107 additions & 47 deletions src/js/plugins/input-password.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,19 @@ 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',
shortPass: 'Password molto debole. ',
badPass: 'Password debole. ',
goodPass: 'Password sicura. ',
strongPass: 'Password molto sicura. ',
enterPass: 'Inserisci almeno 8 caratteri, una lettera maiuscola e un carattere speciale. ',
alertCaps: 'Attenzione: CAPS LOCK inserito. ',
showText: true,
minimumLength: 4,
minimumLength: 8,
}

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}`

Expand All @@ -35,7 +34,6 @@ 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'
Expand Down Expand Up @@ -99,71 +97,96 @@ class InputPassword extends BaseComponent {
}
if (this._isCustom) {
this._capsElement = this._element.parentNode.querySelector(SELECTOR_CAPS)
if (this._capsElement) {
// Ensure the element is hidden and empty initially
this._capsElement.style.display = 'none'
this._capsElement.textContent = ''
}
}

this._showPwdElement = SelectorEngine.findOne(SELECTOR_BTN_SHOW_PWD, this._element.parentNode)
}

_bindEvents() {
EventHandler.on(this._element, 'keypress', (evt) => this._preventSpace(evt))

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()
}
})
EventHandler.on(this._element, EVENT_KEYDOWN, (evt) => this._handleKeyDown(evt))
EventHandler.on(this._element, EVENT_KEYUP, (evt) => this._handleKeyUp(evt))
}

if (this._showPwdElement) {
EventHandler.on(this._showPwdElement, EVENT_CLICK, () => this._toggleShowPassword())
}
}

_showCapsMsg() {
if (this._capsElement) {
this._capsElement.classList.add(CLASS_NAME_SHOW)
_preventSpace(evt) {
if (evt.key === ' ' || evt.keyCode === 32) {
evt.preventDefault()
}
}

_handleKeyDown(evt) {
if (evt.key === 'Shift') {
this._isShiftPressed = true
}
this._checkCapsLock(evt)
}

_handleKeyUp(evt) {
if (evt.key === 'Shift') {
this._isShiftPressed = false
}
this._checkCapsLock(evt)
}

_checkCapsLock(evt) {
if (!this._capsElement) return

const capsOn = this._isCapsLockOn(evt)
if (capsOn !== this._isCapsOn) {
this._isCapsOn = capsOn
this._toggleCapsLockWarning(this._isCapsOn)
}
}

_isCapsLockOn(evt) {
if (evt.getModifierState) {
return evt.getModifierState('CapsLock')
}
const charCode = evt.which || evt.keyCode
const isUpperCase = charCode >= 65 && charCode <= 90
const isLowerCase = charCode >= 97 && charCode <= 122
return (isUpperCase && !evt.shiftKey) || (isLowerCase && evt.shiftKey)
}
_hideCapsMsg() {

_toggleCapsLockWarning(show) {
if (this._capsElement) {
this._capsElement.classList.remove(CLASS_NAME_SHOW)
if (show) {
this._capsElement.textContent = this._config.alertCaps || Default.alertCaps
this._capsElement.style.display = 'block'
} else {
this._capsElement.style.display = 'none'
setTimeout(() => {
if (this._capsElement.style.display === 'none') {
this._capsElement.textContent = ''
}
}, 100)
}
}
}

_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() {
Expand Down Expand Up @@ -198,6 +221,43 @@ class InputPassword extends BaseComponent {
EventHandler.trigger(this._element, EVENT_TEXT)
}
}

const strengthMeter = this._element.parentNode.querySelector('#strengthMeter')
if (strengthMeter) {
const requirements = this._getCompletedRequirements(this._element.value)
const strengthText = this._scoreText(score)
let detailedMessage = `${strengthText} ${requirements.completed} su ${requirements.total} requisiti soddisfatti. `

if (requirements.completedDescriptions.length > 0) {
detailedMessage += `Soddisfatti: ${requirements.completedDescriptions.join(' ')} `
}

if (requirements.missingDescriptions.length > 0) {
detailedMessage += `Mancanti: ${requirements.missingDescriptions.join(' ')} `
}

strengthMeter.textContent = detailedMessage
}
}

_getCompletedRequirements(password) {
const requirements = [
{ test: password.length >= 8, description: 'Almeno 8 caratteri.' },
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@astagi due to do secondo me:

  1. C'è da capire se e come spostare le description di questi requirements nei Default iniziali. E se dare la possibilità di cambiarli da data-bs.
  2. C'è da capire se dare la possibilità di mostrare anche a video questi requirement soddisfatti o no in tempo reale. Se lasciarli come in questa versione solo per screen reader. O se metterli come opzione mostrati a vice magari usando un default.showStrengthSuggestions true/false.
  3. C'è da capire se incorporare così i requirements (come già oggi), sia corretto... o sia meglio lasciare allo sviluppatore e qui lasciare solo la parte visiva. O integrare un plugin node nuovo, da trovare.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⬆️ Solo appunti di alcune cose che dovremo discutere al prox hands-on.

{ test: /[A-Z]/.test(password), description: 'Almeno una lettera maiuscola.' },
{ test: /[a-z]/.test(password), description: 'Almeno una lettera minuscola.' },
{ test: /[0-9]/.test(password), description: 'Almeno un numero.' },
{ test: /[^A-Z-a-z0-9]/.test(password), description: 'Almeno un carattere speciale.' },
]

const completedRequirements = requirements.filter((req) => req.test)
const missingRequirements = requirements.filter((req) => !req.test)

return {
completed: completedRequirements.length,
total: requirements.length,
completedDescriptions: completedRequirements.map((req) => req.description),
missingDescriptions: missingRequirements.map((req) => req.description),
}
}

/**
Expand Down
Loading