LP1977554 - Add Password visibility toggle on login screens
authorScott Angel <scottangel@mobiusconsortium.org>
Thu, 23 Mar 2023 21:27:05 +0000 (16:27 -0500)
committerGalen Charlton <gmc@equinoxOLI.org>
Fri, 28 Apr 2023 19:43:12 +0000 (19:43 +0000)
Changed the <span> tags to <button> tags so it can be reached via
keyboard navigation.

Added a few more attributes to the password input tag.
autocapitalize="none"
spellcheck="false"
aria-description so screen readers can describe to the user what
the current state is.

Added aria-checked to the button for screen readers

Added a new css file for the login.component with a little styling.

Signed-off-by: Scott Angel <scottangel@mobiusconsortium.org>
Signed-off-by: Stephanie Leary <stephanie.leary@equinoxoli.org>
Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>

Open-ILS/src/eg2/src/app/staff/login.component.css [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/login.component.html
Open-ILS/src/eg2/src/app/staff/login.component.ts
Open-ILS/src/templates-bootstrap/opac/css/style.css.tt2
Open-ILS/src/templates-bootstrap/opac/parts/base.tt2
Open-ILS/src/templates-bootstrap/opac/parts/login/form.tt2
Open-ILS/src/templates-bootstrap/opac/parts/login/login_modal.tt2
Open-ILS/src/templates/opac/parts/js.tt2
Open-ILS/src/templates/opac/parts/login/form.tt2
Open-ILS/src/templates/staff/css/style.css.tt2
Open-ILS/src/templates/staff/t_login.tt2

diff --git a/Open-ILS/src/eg2/src/app/staff/login.component.css b/Open-ILS/src/eg2/src/app/staff/login.component.css
new file mode 100644 (file)
index 0000000..a4e35b7
--- /dev/null
@@ -0,0 +1,8 @@
+#show_password {
+    border-left: 0px;
+    border-radius: 0px 4px 4px 0px;
+}
+
+#show_password span.material-icons {
+   font-size: 21px;
+}
index 987cd6a..d671f22 100644 (file)
@@ -7,54 +7,54 @@
 
         <div class="row row-cols-auto">
           <label class="form-label col-form-label fw-bold col-4 text-end" for="username" i18n>Username</label>
-          <div class="col-8">
-            <input 
-              type="text" 
-              class="form-control"
-              id="username" 
-              name="username"
-              required
-              autocomplete="username"
-              i18n-placeholder
-              placeholder="Username" 
-              [(ngModel)]="args.username"/>
-          </div>
+          <input
+            type="text"
+            class="form-control col-lg-8"
+            id="username"
+            name="username"
+            required
+            autocomplete="username"
+            i18n-placeholder
+            placeholder="Username"
+            [(ngModel)]="args.username"/>
         </div>
 
-        <div class="row row-cols-auto mt-3">
-          <label class="form-label col-form-label fw-bold col-4 text-end" for="password" i18n>Password</label>
-          <div class="col-8">
-          <input 
+        <div class="row row-cols-auto">
+          <label class="col-lg-4 text-right font-weight-bold" for="password" i18n>Password</label>
+          <div class="input-group col-lg-8 p-0">
+          <input
             [type]="passwordVisible ? 'text' : 'password'"
             class="form-control"
             id="password"
+            #password
             name="password"
             required
             autocomplete="current-password"
             i18n-placeholder
             placeholder="Password"
             spellcheck="false"
+            [attr.aria-description]="ariaDescription"
             [(ngModel)]="args.password"/>
-          <span id="show_password" class="input-group-text pointer" (click)="togglePasswordVisibility()">
+          <button id="show_password" class="input-group-text pointer"
+                  type="button" role="switch" aria-label="password visibility" aria-checked="false"
+                  (click)="togglePasswordVisibility()" >
             <span class="material-icons">{{ passwordVisible ? 'visibility' : 'visibility_off' }}</span>
-          </span>
+          </button>
           </div>
         </div>
 
         <div class="row row-cols-auto mt-3" *ngIf="workstations && workstations.length">
           <label class="form-label col-form-label col-4 text-end fw-bold" for="workstation" i18n>Workstation</label>
-          <div class="col-8">
-            <select 
-              class="form-control" 
-              id="workstation" 
-              name="workstation"
-              required
-              [(ngModel)]="args.workstation">
-              <option *ngFor="let ws of workstations" [value]="ws.name">
-                {{ws.name}}
-              </option>
-            </select>
-          </div>
+          <select
+            class="form-control col-lg-8"
+            id="workstation"
+            name="workstation"
+            required
+            [(ngModel)]="args.workstation">
+            <option *ngFor="let ws of workstations" [value]="ws.name">
+              {{ws.name}}
+            </option>
+          </select>
         </div>
 
         <div class="row row-cols-auto mt-3">
index 8baf99b..fc0d58c 100644 (file)
@@ -1,4 +1,10 @@
-import {Component, OnInit, Renderer2} from '@angular/core';
+import {
+    Component,
+    ElementRef,
+    OnInit,
+    Renderer2,
+    ViewChild
+} from '@angular/core';
 import {Location} from '@angular/common';
 import {Router, ActivatedRoute} from '@angular/router';
 import {AuthService, AuthWsState} from '@eg/core/auth.service';
@@ -7,15 +13,19 @@ import {OrgService} from '@eg/core/org.service';
 import {OfflineService} from '@eg/staff/share/offline.service';
 
 @Component({
+  styleUrls: ['./login.component.css'],
   templateUrl : './login.component.html'
 })
 export class StaffLoginComponent implements OnInit {
 
+    @ViewChild('password')
+    passwordInput: ElementRef;
     workstations: any[];
     loginFailed: boolean;
     routeTo: string;
     pendingXactsDate: Date;
     passwordVisible: boolean;
+    ariaDescription: string = 'Your password is not visible.';
 
     args = {
       username : '',
@@ -124,6 +134,9 @@ export class StaffLoginComponent implements OnInit {
 
     togglePasswordVisibility() {
         this.passwordVisible = !this.passwordVisible;
+        if(this.passwordVisible) this.ariaDescription = "Your password is visible!";
+        else this.ariaDescription = "Your password is not visible.";
+        this.passwordInput.nativeElement.focus();
     }
 
 }
index 92d651b..7cb1d43 100755 (executable)
@@ -4023,3 +4023,10 @@ padding: 15px;
 .pointer {
     cursor: pointer;
 }
+#loginModal #show_password {
+    border-top: 1px solid #888;
+    border-right: 1px solid #888;
+    border-bottom: 1px solid #888;
+    border-radius: 0px 4px 4px 0px;
+    background-color: whitesmoke;
+}
\ No newline at end of file
index 0e8d2b1..b1edba6 100755 (executable)
                 let input = document.getElementById('password_field');
                 let icon = btn.querySelector('i');
                 btn.addEventListener('click', () => {
-                    input.type == 'password' ? [input.type = 'text', icon.setAttribute('class','fas fa-eye')] : [input.type = 'password', icon.setAttribute('class', 'fas fa-eye-slash')];
+                    if(input.type == 'password'){
+                        input.type = 'text';
+                        icon.setAttribute('class', 'fas fa-eye');
+                        btn.setAttribute('aria-checked', 'true');
+                        input.setAttribute('aria-description', 'Your password is visible!');
+                    }else {
+                        input.type = 'password';
+                        icon.setAttribute('class', 'fas fa-eye-slash');
+                        btn.setAttribute('aria-checked', 'false');
+                        input.setAttribute('aria-description', 'Your password is not visible.');
+                    }
                     input.focus();
                 });
                 let loginForm = document.getElementById('login_form');
index 7659464..058ad86 100755 (executable)
                </div>
                <div class="col-sm w-50">
                        <div class="input-group">
-                               <input class="form-control" id="password_field" name="password" type="password" spellcheck="false" autocomplete="false"/>
-                               <span id="show_password" class="input-group-text pointer"><i class="fas fa-eye-slash"></i></span>
+                               <input class="form-control" id="password_field" name="password" type="password"
+                                          spellcheck="false" autocomplete="false" autocapitalize="none" aria-description="Your password is not visible."/>
+                               <button id="show_password" class="input-group-text pointer" type="button" role="checkbox">
+                                       <i class="fas fa-eye-slash"></i>
+                               </button>
                        </div>
                </div>
        </div>
index 5a958e3..fc746bb 100755 (executable)
                        </div>
                        <div class="col-sm w-50">
                 <div class="input-group">
-                    <input class="form-control" id="password_field" name="password" type="password" spellcheck="false" autocomplete="false"/>
-                    <span id="show_password" class="input-group-text pointer"><i class="fas fa-eye-slash"></i></span>
+                    <input class="form-control" id="password_field" name="password" type="password"
+                                                  spellcheck="false" autocomplete="false" autocapitalize="none" aria-description="Your password is not visible."/>
+                                       <span class="input-group-addon">
+                      <button id="show_password" class="btn" type="button" role="switch" aria-label="password visibility"
+                                                         aria-checked="false" ><i class="fas fa-eye-slash"></i>
+                                         </button>
+                    </span>
                 </div>
                        </div>
                </div>
index 9aae9d9..33df990 100644 (file)
     let checkbox = document.getElementById('password_visibility_checkbox');
     let input = document.getElementById('password_field');
     checkbox.addEventListener('change', () => {
-        if(checkbox.checked) input.type = 'text';
-        else input.type = 'password';
+        if(checkbox.checked){
+          input.type = 'text';
+          input.setAttribute('aria-description', 'Your password is visible!');
+        }
+        else {
+          input.type = 'password';
+          input.setAttribute('aria-description', 'Your password is not visible.');
+        }
         input.focus();
     });
     // If the form is submitted revert the password field to a password input 
index ddb9a76..62acac1 100644 (file)
@@ -57,7 +57,9 @@
         <div class='float-left'>
             <label for="password_field" class="lbl1" >[% l('PIN Number or Password') %]</label>
             <div class="input_bg">
-                <input id="password_field" name="password" type="password" spellcheck="false"/>
+                <input id="password_field" name="password" type="password"
+                       spellcheck="false" autocapitalize="false" autocomplete="false"
+                       aria-description="Your password is not visible."/>
             </div>
             <div id="password_visibility" class="input_bg">
                 <input id="password_visibility_checkbox" type="checkbox" />
index 72bac37..a92e80b 100644 (file)
@@ -194,7 +194,9 @@ table.list tr.selected td { /* deprecated? */
 .currency-input {
   width: 8em;
 }
-
+#show_password {
+  border: none;
+}
 /* barcode inputs are everywhere.  Let's have a consistent style. */
 .barcode { width: 16em !important; }
 
index a4f77d9..19edb73 100644 (file)
               <div class="col-md-8">
                 <div class="input-group">
                   <input type="password" id="login-password" class="form-control"
-                    placeholder="Password" spellcheck="false" autocomplete="false" ng-model="args.password"/>
-                  <span id="show_password" class="input-group-addon pointer"><i class="glyphicon glyphicon-eye-close"></i></span>
+                    placeholder="Password" autocapitalize="none" spellcheck="false" autocomplete="false" aria-description="Your password is not visible."
+                         ng-model="args.password"/>
+                  <span class="input-group-addon">
+                    <button id="show_password" type="button" role="switch" aria-label="password visibility" aria-checked="false" ><i class="glyphicon glyphicon-eye-close"></i></button>
+                  </span>
                 </div>
               </div>
             </div>
     let input = document.getElementById('login-password');
     let icon = btn.querySelector('i');
     btn.addEventListener('click', () => {
-      input.type == 'password' ? [input.type = 'text', icon.setAttribute('class', 'glyphicon glyphicon-eye-open')] : [input.type = 'password', icon.setAttribute('class', 'glyphicon glyphicon-eye-close')];
+      if(input.type == 'password'){
+        input.type = 'text';
+        icon.setAttribute('class', 'glyphicon glyphicon-eye-open');
+        btn.setAttribute('aria-checked', 'true');
+        input.setAttribute('aria-description', 'Your password is visible!');
+      }else {
+        input.type = 'password';
+        icon.setAttribute('class', 'glyphicon glyphicon-eye-close');
+        btn.setAttribute('aria-checked', 'false');
+        input.setAttribute('aria-description', 'Your password is not visible.');
+      }
       input.focus();
     });
     $('#login_form').submit(()=>{