Ver código fonte

Enhance accessibility with correct keyboard navigation and focus style

Bubka 2 anos atrás
pai
commit
4f3fa4ba75

+ 1 - 1
resources/js/components/Footer.vue

@@ -15,7 +15,7 @@
         <div v-else class="content has-text-centered">
             <router-link id="lnkSettings"  :to="{ name: 'settings.options' }" class="has-text-grey">{{ $t('settings.settings') }}</router-link>
             <span v-if="!this.$root.appConfig.proxyAuth || (this.$root.appConfig.proxyAuth && this.$root.appConfig.proxyLogoutUrl)">
-                 - <a id="lnkSignOut" class="has-text-grey" @click="logout">{{ $t('auth.sign_out') }}</a>
+                 - <button id="lnkSignOut" class="button is-text is-like-text has-text-grey" @click="logout">{{ $t('auth.sign_out') }}</button>
             </span>
         </div>
     </footer>

+ 10 - 1
resources/js/components/FormCheckbox.vue

@@ -1,7 +1,7 @@
 <template>
     <div class="field">
         <input :id="fieldName" type="checkbox" :name="fieldName" class="is-checkradio is-info" v-model="form[fieldName]" v-on:change="$emit(fieldName, form[fieldName])" v-bind="$attrs">
-        <label :for="fieldName" class="label" v-html="label"></label>
+        <label tabindex="0" :for="fieldName" class="label" :class="labelClass" v-html="label" v-on:keypress.space.prevent="setCheckbox"></label>
         <p class="help" v-html="help" v-if="help"></p>
     </div>
 </template>
@@ -38,6 +38,15 @@
                 type: String,
                 default: ''
             },
+        },
+
+        methods: {
+            setCheckbox(event) {
+                if (this.$attrs.disabled == false) {
+                    this.form[this.fieldName] = !this.form[this.fieldName]
+                    this.$emit(this.fieldName, this.form[this.fieldName])
+                }
+            }
         }
     }
 </script>

+ 8 - 2
resources/js/components/FormPasswordField.vue

@@ -13,10 +13,10 @@
                 v-on:change="$emit('field-changed', form[fieldName])"
                 v-on:keyup="checkCapsLock"
             />
-            <span v-if="currentType == 'password'" class="icon is-small is-right is-clickable" @click="currentType = 'text'" :title="$t('auth.forms.reveal_password')">
+            <span v-if="currentType == 'password'" role="button" tabindex="0" class="icon is-small is-right is-clickable" @keyup.enter="setFieldType('text')" @click="setFieldType('text')" :title="$t('auth.forms.reveal_password')">
                 <font-awesome-icon :icon="['fas', 'eye-slash']" />
             </span>
-            <span v-else class="icon is-small is-right is-clickable" @click="currentType = 'password'" :title="$t('auth.forms.hide_password')">
+            <span v-else role="button" tabindex="0" class="icon is-small is-right is-clickable" @keyup.enter="setFieldType('password')" @click="setFieldType('password')" :title="$t('auth.forms.hide_password')">
                 <font-awesome-icon :icon="['fas', 'eye']" />
             </span>
         </div>
@@ -121,6 +121,12 @@
             checkCapsLock(event) {
                 this.hasCapsLockOn = event.getModifierState('CapsLock') ? true : false
             },
+
+            setFieldType(event) {
+                if (this.currentType != event) {
+                    this.currentType = event
+                }
+            }
         },
     }
 </script>

+ 31 - 6
resources/js/components/FormToggle.vue

@@ -1,11 +1,29 @@
 <template>
-    <div class="field" :class="{ 'pt-3' : hasOffset }">
-        <label class="label" v-html="label"></label>
+    <div class="field" :class="{ 'pt-3' : hasOffset }" role="radiogroup" :aria-labelledby="inputId('label', fieldName)">
+        <label :id="inputId('label', fieldName)" class="label" v-html="label"></label>
         <div class="is-toggle buttons">
-            <label class="button is-dark" :disabled="isDisabled" v-for="(choice, index) in choices" :key="index" :class="{ 'is-link' : form[fieldName] === choice.value }">
-                <input type="radio" class="is-hidden" :checked="form[fieldName] === choice.value" :value="choice.value" v-model="form[fieldName]" v-on:change="$emit(fieldName, form[fieldName])" :disabled="isDisabled" />
+            <button 
+                role="radio" 
+                type="button"
+                class="button is-dark" 
+                :aria-checked="form[fieldName] === choice.value"
+                :disabled="isDisabled" 
+                v-for="(choice, index) in choices" 
+                :key="index" 
+                :class="{ 'is-link' : form[fieldName] === choice.value }" 
+                v-on:click.stop="setRadio(choice.value)"
+            >
+                <input 
+                    :id="inputId(inputType, choice.value)" 
+                    :type="inputType" 
+                    class="is-hidden" 
+                    :checked="form[fieldName] === choice.value" 
+                    :value="choice.value" 
+                    v-model="form[fieldName]" 
+                    :disabled="isDisabled" 
+                />
                 <font-awesome-icon :icon="['fas', choice.icon]" v-if="choice.icon" class="mr-3" /> {{ choice.text }}
-            </label>
+            </button>
         </div>
         <field-error :form="form" :field="fieldName" />
         <p class="help" v-html="help" v-if="help"></p>
@@ -18,7 +36,7 @@
         
         data() {
             return {
-
+                inputType: 'radio'
             }
         },
 
@@ -58,6 +76,13 @@
                 type: Boolean,
                 default: false
             }
+        },
+
+        methods: {
+            setRadio(event) {
+                this.form[this.fieldName] = event
+                this.$emit(this.fieldName, this.form[this.fieldName])
+            }
         }
     }
 </script>

+ 2 - 2
resources/js/components/Modal.vue

@@ -14,9 +14,9 @@
         </div>
         <div v-if="this.showcloseButton" class="fullscreen-footer">
             <!-- Close button -->
-            <label class="button is-dark is-rounded" @click.stop="closeModal">
+            <button class="button is-dark is-rounded" @click.stop="closeModal">
                 {{ $t('commons.close') }}
-            </label>
+            </button>
         </div>
     </div>
 </template>

+ 1 - 1
resources/js/components/OtpDisplayer.vue

@@ -5,7 +5,7 @@
         </figure>
         <p class="is-size-4 has-text-grey-light has-ellipsis">{{ internal_service }}</p>
         <p class="is-size-6 has-text-grey has-ellipsis">{{ internal_account }}</p>
-        <p class="is-size-1 has-text-white is-clickable" :title="$t('commons.copy_to_clipboard')" v-clipboard="() => internal_password.replace(/ /g, '')" v-clipboard:success="clipboardSuccessHandler">{{ displayedOtp }}</p>
+        <p tabindex="0" class="is-size-1 has-text-white is-clickable" :title="$t('commons.copy_to_clipboard')" v-clipboard="() => internal_password.replace(/ /g, '')" v-clipboard:success="clipboardSuccessHandler">{{ displayedOtp }}</p>
         <ul class="dots" v-show="isTimeBased(internal_otp_type)">
             <li v-for="n in 10" :key="n"></li>
         </ul>

+ 1 - 1
resources/js/components/SettingTabs.vue

@@ -5,7 +5,7 @@
                 <div class="tabs is-centered is-fullwidth">
                     <ul>
                         <li v-for="tab in tabs" :key="tab.view" :class="{ 'is-active': tab.view === activeTab }">
-                            <a :id="tab.id" @click="selectTab(tab.view)">{{ tab.name }}</a>
+                            <a :id="tab.id" tabindex="0" @click="selectTab(tab.view)">{{ tab.name }}</a>
                         </li>
                     </ul>
                 </div>

+ 6 - 0
resources/js/mixins.js

@@ -177,6 +177,12 @@ Vue.mixin({
                 case 'password':
                     prefix = 'pwd'
                     break
+                case 'radio':
+                    prefix = 'rdo'
+                    break
+                case 'label':
+                    prefix = 'lbl'
+                    break
                 default:
                     prefix = 'txt'
                     break

+ 4 - 4
resources/js/views/About.vue

@@ -50,9 +50,9 @@
                 {{ $t('commons.environment') }}
             </h2>
             <div class="box has-background-black-bis is-family-monospace is-size-7">
-                <span class="is-pulled-right is-clickable" v-clipboard="() => this.$refs.listInfos.innerText" v-clipboard:success="clipboardSuccessHandler">
+                <button class="button copy-text is-pulled-right is-small is-text" v-clipboard="() => this.$refs.listInfos.innerText" v-clipboard:success="clipboardSuccessHandler">
                     <font-awesome-icon :icon="['fas', 'copy']" />
-                </span>
+                </button>
                 <ul ref="listInfos">
                     <li v-for="(value, key) in infos" :value="value" :key="key"><b>{{key}}</b>: {{value}}</li>
                 </ul>
@@ -62,9 +62,9 @@
                     {{ $t('settings.user_options') }}
                 </h2>
                 <div class="box has-background-black-bis is-family-monospace is-size-7">
-                    <span class="is-pulled-right is-clickable" v-clipboard="() => this.$refs.listUserOptions.innerText" v-clipboard:success="clipboardSuccessHandler">
+                    <button class="button copy-text is-pulled-right is-small is-text" v-clipboard="() => this.$refs.listUserOptions.innerText" v-clipboard:success="clipboardSuccessHandler">
                         <font-awesome-icon :icon="['fas', 'copy']" />
-                    </span>
+                    </button>
                     <ul ref="listUserOptions">
                         <li v-for="(value, option) in options" :value="value" :key="option"><b>{{option}}</b>: {{value}}</li>
                     </ul>

+ 17 - 13
resources/js/views/Accounts.vue

@@ -19,7 +19,7 @@
             <vue-footer :showButtons="true">
                 <!-- Close Group switch button -->
                 <p class="control">
-                    <a class="button is-dark is-rounded" @click="closeGroupSwitch()">{{ $t('commons.close') }}</a>
+                    <button class="button is-dark is-rounded" @click="closeGroupSwitch()">{{ $t('commons.close') }}</button>
                 </p>
             </vue-footer>
         </div>
@@ -81,7 +81,7 @@
         	                            </div>
         	                        </div>
         	                    </transition>
-                                <div class="tfa-cell tfa-content is-size-3 is-size-4-mobile" @click.stop="showAccount(account)">  
+                                <div tabindex="0" class="tfa-cell tfa-content is-size-3 is-size-4-mobile" @click="showAccount(account)" @keyup.enter="showAccount(account)" role="button">  
                                     <div class="tfa-text has-ellipsis">
                                         <img :src="'/storage/icons/' + account.icon" v-if="account.icon && $root.appSettings.showAccountsIcons" :alt="$t('twofaccounts.icon_for_account_x_at_service_y', {account: account.account, service: account.service})">
                                         {{ displayService(account.service) }}<font-awesome-icon class="has-text-danger is-size-5 ml-2" v-if="$root.appSettings.useEncryption && account.account === $t('errors.indecipherable')" :icon="['fas', 'exclamation-circle']" />
@@ -148,7 +148,7 @@
                                         {{ selectedAccounts.length }}&nbsp;{{ $t('commons.selected') }}
                                         <button @click="clearSelected" :style="{visibility: selectedAccounts.length > 0 ? 'visible' : 'hidden'}" class="delete" :title="$t('commons.clear_selection')"></button>
                                     </span>
-                                    <span @click="selectAll" class="tag is-dark is-clickable has-background-black-ter has-text-grey" :title="$t('commons.select_all')">
+                                    <span role="button" @click="selectAll" class="tag is-dark is-clickable has-background-black-ter has-text-grey" :title="$t('commons.select_all')">
                                         {{ $t('commons.all') }}
                                         <font-awesome-icon class="ml-1" :icon="['fas', 'check-square']" />
                                     </span>
@@ -156,10 +156,10 @@
                             </div>
                             <div class="control">
                                 <div class="tags has-addons are-medium">
-                                    <span @click="sortAsc" class="tag is-dark is-clickable has-background-black-ter has-text-grey" :title="$t('commons.sort_ascending')">
+                                    <span role="button" @click="sortAsc" class="tag is-dark is-clickable has-background-black-ter has-text-grey" :title="$t('commons.sort_ascending')">
                                         <font-awesome-icon :icon="['fas', 'sort-alpha-down']" />
                                     </span>
-                                    <span @click="sortDesc" class="tag is-dark is-clickable has-background-black-ter has-text-grey" :title="$t('commons.sort_descending')">
+                                    <span role="button" @click="sortDesc" class="tag is-dark is-clickable has-background-black-ter has-text-grey" :title="$t('commons.sort_descending')">
                                         <font-awesome-icon :icon="['fas', 'sort-alpha-up']" />
                                     </span>
                                 </div>
@@ -167,7 +167,7 @@
                         </div>
                         <div class="field is-grouped is-justify-content-center pt-1">
                             <div class="control">
-                                <div class="tags are-medium has-addons is-clickable" v-if="selectedAccounts.length > 0" @click="showGroupSelector = true">
+                                <div role="button" class="tags are-medium has-addons is-clickable" v-if="selectedAccounts.length > 0" @click="showGroupSelector = true">
                                     <span class="tag is-dark">
                                         {{ $t('groups.change_group') }}
                                     </span>
@@ -177,7 +177,7 @@
                                 </div>
                             </div>
                             <div class="control">
-                                <div class="tags are-medium has-addons is-clickable" v-if="selectedAccounts.length > 0" @click="destroyAccounts">
+                                <div role="button" class="tags are-medium has-addons is-clickable" v-if="selectedAccounts.length > 0" @click="destroyAccounts">
                                     <span class="tag is-dark">
                                         {{ $t('commons.delete') }}
                                     </span>
@@ -193,7 +193,7 @@
                     <!-- search -->
                     <div class="field">
                         <div class="control has-icons-right">
-                            <input type="text" :title="$t('commons.search')" class="input is-rounded is-search" v-model="search">
+                            <input id="txtSearch" type="search" tabindex="1" :aria-label="$t('commons.search')" :title="$t('commons.search')" class="input is-rounded is-search" v-model="search">
                             <span class="icon is-small is-right">
                                 <font-awesome-icon :icon="['fas', 'search']"  v-if="!search" />
                                 <a class="delete" v-if="search" @click="search = '' "></a>
@@ -201,14 +201,18 @@
                         </div>
                     </div>
                     <!-- group switch toggle -->
-                    <div class="is-clickable has-text-centered">
-                        <div class="columns" @click="toggleGroupSwitch">
+                    <div class="has-text-centered">
+                        <div class="columns">
                             <div class="column" v-if="!showGroupSwitch">
-                                {{ activeGroupName }} ({{ filteredAccounts.length }})
-                                <font-awesome-icon  :icon="['fas', 'caret-down']" />
+                                <button :title="$t('groups.show_group_selector')" tabindex="1" class="button is-text is-like-text" @click.stop="toggleGroupSwitch">
+                                    {{ activeGroupName }} ({{ filteredAccounts.length }})&nbsp;
+                                    <font-awesome-icon  :icon="['fas', 'caret-down']" />
+                                </button>
                             </div>
                             <div class="column" v-else>
-                                {{ $t('groups.select_accounts_to_show') }}
+                                <button :title="$t('groups.hide_group_selector')" tabindex="1" class="button is-text is-like-text" @click.stop="toggleGroupSwitch">
+                                    {{ $t('groups.select_accounts_to_show') }}
+                                </button>
                             </div>
                         </div>
                     </div>

+ 6 - 2
resources/js/views/auth/Login.vue

@@ -10,7 +10,9 @@
             </div>
             <div class="nav-links">
                 <p>{{ $t('auth.webauthn.lost_your_device') }}&nbsp;<router-link id="lnkRecoverAccount" :to="{ name: 'webauthn.lost' }" class="is-link">{{ $t('auth.webauthn.recover_your_account') }}</router-link></p>
-                <p v-if="!this.$root.appSettings.useWebauthnOnly">{{ $t('auth.sign_in_using') }}&nbsp;<a id="lnkSignWithLegacy" class="is-link" @click="showWebauthn = false">{{ $t('auth.login_and_password') }}</a></p>
+                <p v-if="!this.$root.appSettings.useWebauthnOnly">{{ $t('auth.sign_in_using') }}&nbsp;
+                    <a id="lnkSignWithLegacy" role="button" class="is-link" @keyup.enter="showWebauthn = false" @click="showWebauthn = false" tabindex="0">{{ $t('auth.login_and_password') }}</a>
+                </p>
             </div>
         </form-wrapper>
         <!-- login/password legacy form -->
@@ -28,7 +30,9 @@
                 </div>
                 <div v-else>
                     <p>{{ $t('auth.forms.forgot_your_password') }}&nbsp;<router-link id="lnkResetPwd" :to="{ name: 'password.request' }" class="is-link">{{ $t('auth.forms.request_password_reset') }}</router-link></p>
-                    <p >{{ $t('auth.sign_in_using') }}&nbsp;<a id="lnkSignWithWebauthn" class="is-link" @click="showWebauthn = true">{{ $t('auth.webauthn.security_device') }}</a></p>
+                    <p >{{ $t('auth.sign_in_using') }}&nbsp;
+                        <a id="lnkSignWithWebauthn" role="button" class="is-link" @keyup.enter="showWebauthn = true" @click="showWebauthn = true" tabindex="0">{{ $t('auth.webauthn.security_device') }}</a>
+                    </p>
                 </div>
             </div>
         </form-wrapper>

+ 1 - 1
resources/js/views/settings/Account.vue

@@ -37,7 +37,7 @@
         <vue-footer :showButtons="true">
             <!-- Cancel button -->
             <p class="control">
-                <a class="button is-dark is-rounded" @click.stop="exitSettings">
+                <a role="button" tabindex="0" class="button is-dark is-rounded" @click.stop="exitSettings">
                     {{ $t('commons.close') }}
                 </a>
             </p>

+ 1 - 1
resources/js/views/settings/OAuth.vue

@@ -9,7 +9,7 @@
                     {{ $t('settings.token_legend')}}
                 </div>
                 <div class="mt-3">
-                    <a class="is-link" @click="createToken()">
+                    <a role="button" tabindex="0" class="is-link" @click="createToken()">
                         <font-awesome-icon :icon="['fas', 'plus-circle']" /> {{ $t('settings.generate_new_token')}}
                     </a>
                 </div>

+ 2 - 2
resources/js/views/settings/WebAuthn.vue

@@ -9,8 +9,8 @@
                     {{ $t('auth.webauthn.security_devices_legend')}}
                 </div>
                 <div class="mt-3">
-                    <a class="is-link" @click="register()">
-                        <font-awesome-icon :icon="['fas', 'plus-circle']" /> {{ $t('auth.webauthn.register_a_new_device')}}
+                    <a role="button" tabindex="0"  @click="register()">
+                        <font-awesome-icon :icon="['fas', 'plus-circle']" />&nbsp;{{ $t('auth.webauthn.register_a_new_device')}}
                     </a>
                 </div>
                 <!-- credentials list -->

+ 1 - 1
resources/lang/en/errors.php

@@ -22,7 +22,7 @@ return [
     'Unable_to_decrypt_uri' => 'Unable to decrypt uri',
     'not_a_supported_otp_type' => 'This OTP format is not currently supported',
     'cannot_create_otp_without_secret' => 'Cannot create an OTP without a secret',
-    'data_of_qrcode_is_not_valid_URI' => 'The data of this QR code is not a valid OTP Auth URI:',
+    'data_of_qrcode_is_not_valid_URI' => 'The data of this QR code is not a valid OTP Auth URI. The QR code contains:',
     'wrong_current_password' => 'Wrong current password, nothing has changed',
     'error_during_encryption' => 'Encryption failed, your database remains unprotected.',
     'error_during_decryption' => 'Decryption failed, your database is still protected. This is mainly caused by an integrity issue of encrypted data for one or more accounts.',

+ 3 - 1
resources/lang/en/groups.php

@@ -15,7 +15,9 @@ return [
 
     'groups' => 'Groups',
     'create_group' => 'Create new group',
-    'select_accounts_to_show' => 'Select accounts to show',
+    'show_group_selector' => 'Show group selector',
+    'hide_group_selector' => 'Hide group selector',
+    'select_accounts_to_show' => 'Select accounts group to show',
     'manage_groups' => 'Manage groups',
     'active_group' => 'Active group',
     'manage_groups_legend' => 'You can create groups to organize your accounts the way you want. All accounts remain visible in the pseudo group named \'All\', regardless of the group they belong to.',

+ 25 - 40
resources/sass/app.scss

@@ -205,9 +205,13 @@ a:hover {
     flex-grow: 1;
     overflow: hidden;
 }
-
-// .tfa-grid .tfa-content {
-// }
+.tfa-content:focus, .tfa-content:focus-visible
+{
+    outline: 2px solid $grey;
+    border: none;
+    outline-offset: 7px;
+    border-radius: 3px;
+}
 
 .tfa-list .tfa-content {
     padding-right: 1rem;
@@ -524,43 +528,24 @@ a.has-text-white-bis:focus, a.has-text-white-bis:focus-visible {
   box-shadow: $input-focus-box-shadow-size $input-focus-box-shadow-color;
 }
 
-// .button.is-dark:focus:not(:active), .button.is-dark.is-focused:not(:active),
-// .button.is-link:focus:not(:active), .button.is-link.is-focused:not(:active) {
-// 	box-shadow: 0 0 0 0.125em hsla(0, 0%, 96%, 0.25);
-// }
-// .button.copy-text:focus:not(:active), .button.copy-text.is-focused:not(:active) {
-// 	box-shadow: none;
-//   color: hsl(0, 0%, 86%);
-// }
-
-// a:focus,
-// .button:focus,
-// .control.has-icons-right > span.icon:focus {
-//   outline: none !important;
-// }
-// .button:focus {
-//   box-shadow: none;
-// }
-// a:focus-visible,
-// .control.has-icons-right > span.icon:focus {
-//   outline: 2px solid hsl(217, 71%, 53%) !important;
-//   outline-offset: 3px !important;
-// }
-
-// .button:focus-visible {
-//   box-shadow: none;
-//   outline: 2px solid hsl(217, 71%, 53%) !important;
-//   outline-offset: 3px !important;
-// }
-
-// @supports not selector(:focus-visible) {
-//   a:focus,
-//   button:focus,
-//   .control.has-icons-right > span.icon:focus {
-//     outline: 2px solid hsl(217, 71%, 53%);
-//     outline-offset: 3px;
-//   }
-// }
+.is-checkradio[type="checkbox"] + label:focus, 
+.is-checkradio[type="checkbox"] + label:focus-visible
+{
+  outline: none;
+  border: none;
+}
+.is-checkradio[type="checkbox"] + label:focus::before, 
+.is-checkradio[type="checkbox"] + label:focus-visible::before
+{
+  outline: none;
+  border: 1px solid $input-focus-border-color;
+  box-shadow: $input-focus-box-shadow-size $input-focus-box-shadow-color;
+}
+.is-checkradio[type="checkbox"] + label::before, 
+.is-checkradio[type="checkbox"] + label::before
+{
+  border-color: $grey;
+}
 
 
 .label {