Explorar o código

Fix some accessibility issues

Bubka %!s(int64=2) %!d(string=hai) anos
pai
achega
3fcc2b906b

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

@@ -7,7 +7,7 @@
         <div v-if="this.$root.isTestingApp" class="demo has-background-warning has-text-centered is-size-7-mobile">
             {{ $t('commons.testing_do_not_post_sensitive_data') }}
         </div>
-        <notifications id="vueNotification" width="100%" position="top" :duration="4000" :speed="0" :max="1" classes="notification is-radiusless" />
+        <notifications id="vueNotification" role="alert" width="100%" position="top" :duration="4000" :speed="0" :max="1" classes="notification is-radiusless" />
         <main class="main-section">
             <router-view></router-view>
         </main>

+ 3 - 1
resources/js/components/FieldError.vue

@@ -1,5 +1,7 @@
 <template>
-    <p :id="'valError' + field[0].toUpperCase() + field.toLowerCase().slice(1)" class="help is-danger" v-if="form.errors.has(field)" v-html="form.errors.get(field)" />
+    <div role="alert">
+        <p :id="'valError' + field[0].toUpperCase() + field.toLowerCase().slice(1)" class="help is-danger" v-if="form.errors.has(field)" v-html="form.errors.get(field)" />
+    </div>
 </template>
 
 <script>

+ 38 - 32
resources/js/components/OtpDisplayer.vue

@@ -5,7 +5,11 @@
         </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 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>
+        <p>
+            <span role="log" ref="otp" tabindex="0" class="otp is-size-1 has-text-white is-clickable px-3" @click="copyOTP(internal_password)" @keyup.enter="copyOTP(internal_password)" :title="$t('commons.copy_to_clipboard')">
+                {{ displayedOtp }}
+            </span>
+        </p>
         <ul class="dots" v-show="isTimeBased(internal_otp_type)">
             <li v-for="n in 10" :key="n"></li>
         </ul>
@@ -70,8 +74,34 @@
             this.show()
         },
 
+        // created() {
+        // },
+
         methods: {
 
+            copyOTP (otp) {
+                // see https://web.dev/async-clipboard/ for futur Clipboard API usage.
+                // The API should allow to copy the password on each trip without user interaction.
+
+                // For now too many browsers don't support the clipboard-write permission
+                // (see https://developer.mozilla.org/en-US/docs/Web/API/Permissions#browser_support)
+
+                const rawOTP = otp.replace(/ /g, '')
+                const success = this.$clipboard(rawOTP)
+
+                if (success == true) {
+                    if(this.$root.appSettings.kickUserAfter == -1) {
+                        this.appLogout()
+                    }
+                    else if(this.$root.appSettings.closeOtpOnCopy) {
+                        this.$parent.isActive = false
+                        this.clearOTP()
+                    }
+
+                    this.$notify({ type: 'is-success', text: this.$t('commons.copied_to_clipboard') })
+                }
+            },
+
             isTimeBased: function(otp_type) {
                 return (otp_type === 'totp' || otp_type === 'steamtotp')
             },
@@ -133,7 +163,7 @@
                         else this.$router.push({ name: 'genericError', params: { err: this.$t('errors.not_a_supported_otp_type') } });
     
                         this.$parent.isActive = true
-                        this.$parent.$refs.closeModalButton.focus()
+                        this.focusOnOTP()
                     }
                     catch(error) {
                         this.clearOTP()
@@ -181,7 +211,7 @@
 
                     await this.axios(request).then(response => {
                         if(this.$root.appSettings.copyOtpOnDisplay) {
-                            this.copyAndNotify(response.data.password)
+                            this.copyOTP(response.data.password)
                         }
                         password = response.data
                     })
@@ -319,35 +349,11 @@
                 }
             },
 
-
-            clipboardSuccessHandler ({ value, event }) {
-
-                if(this.$root.appSettings.kickUserAfter == -1) {
-                    this.appLogout()
-                }
-                else if(this.$root.appSettings.closeOtpOnCopy) {
-                    this.$parent.isActive = false
-                    this.clearOTP()
-                }
-
-                this.$notify({ type: 'is-success', text: this.$t('commons.copied_to_clipboard') })
-            },
-
-
-            clipboardErrorHandler ({ value, event }) {
-                console.log('error', value)
-            },
-
-            copyAndNotify (strToCopy) {
-                // see https://web.dev/async-clipboard/ for futur Clipboard API usage.
-                // The API should allow to copy the password on each trip without user interaction.
-
-                // For now too many browsers don't support the clipboard-write permission
-                // (see https://developer.mozilla.org/en-US/docs/Web/API/Permissions#browser_support)
-
-                this.$clipboard(strToCopy)
-                this.$notify({ type: 'is-success', text: this.$t('commons.copied_to_clipboard') })
-            },
+            focusOnOTP() {
+                this.$nextTick(() => {
+                    this.$refs.otp.focus()
+                })
+            }
 
         },
 

+ 9 - 8
resources/js/views/About.vue

@@ -1,6 +1,6 @@
 <template>
     <responsive-width-wrapper>
-        <h1 class="title has-text-grey-dark">{{ $t('commons.about') }}</h1>
+        <h1 class="title has-text-grey-dark">{{ pagetitle }}</h1>
         <p class="block">
             <span class="has-text-white"><span class="is-size-5">2FAuth</span> v{{ appVersion }}</span><br />
             {{ $t('commons.2fauth_teaser')}}
@@ -13,25 +13,25 @@
             {{ $t('commons.resources') }}
         </h2>
         <div class="buttons">
-            <a class="button is-dark" href="https://github.com/Bubka/2FAuth">
+            <a class="button is-dark" href="https://github.com/Bubka/2FAuth" target="_blank">
                 <span class="icon is-small">
                     <font-awesome-icon :icon="['fab', 'github-alt']" />
                 </span>
                 <span>Github</span>
             </a>
-            <a class="button is-dark" href="https://docs.2fauth.app/">
+            <a class="button is-dark" href="https://docs.2fauth.app/" target="_blank">
                 <span class="icon is-small">
                     <font-awesome-icon :icon="['fas', 'book']" />
                 </span>
                 <span>Docs</span>
             </a>
-            <a class="button is-dark" href="https://demo.2fauth.app/">
+            <a class="button is-dark" href="https://demo.2fauth.app/" target="_blank">
                 <span class="icon is-small">
                     <font-awesome-icon :icon="['fas', 'flask']" />
                 </span>
                 <span>Demo</span>
             </a>
-            <a class="button is-dark" href="https://docs.2fauth.app/resources/rapidoc.html">
+            <a class="button is-dark" href="https://docs.2fauth.app/resources/rapidoc.html" target="_blank">
                 <span class="icon is-small">
                     <font-awesome-icon :icon="['fas', 'code']" />
                 </span>
@@ -52,7 +52,7 @@
             {{ $t('commons.environment') }}
         </h2>
         <div class="box has-background-black-bis is-family-monospace is-size-7">
-            <button class="button is-like-text is-pulled-right is-small is-text" v-clipboard="() => this.$refs.listInfos.innerText" v-clipboard:success="clipboardSuccessHandler">
+            <button :aria-label="$t('commons.copy_to_clipboard')" class="button is-like-text is-pulled-right is-small is-text" v-clipboard="() => this.$refs.listInfos.innerText" v-clipboard:success="clipboardSuccessHandler">
                 <font-awesome-icon :icon="['fas', 'copy']" />
             </button>
             <ul ref="listInfos">
@@ -64,7 +64,7 @@
                 {{ $t('settings.user_options') }}
             </h2>
             <div class="box has-background-black-bis is-family-monospace is-size-7">
-                <button class="button is-like-text is-pulled-right is-small is-text" v-clipboard="() => this.$refs.listUserOptions.innerText" v-clipboard:success="clipboardSuccessHandler">
+                <button :aria-label="$t('commons.copy_to_clipboard')" class="button is-like-text is-pulled-right is-small is-text" v-clipboard="() => this.$refs.listUserOptions.innerText" v-clipboard:success="clipboardSuccessHandler">
                     <font-awesome-icon :icon="['fas', 'copy']" />
                 </button>
                 <ul ref="listUserOptions">
@@ -76,7 +76,7 @@
         <vue-footer :showButtons="true">
             <!-- close button -->
             <p class="control">
-                <router-link  :to="{ name: 'accounts', params: { toRefresh: true } }" class="button is-dark is-rounded">{{ $t('commons.close') }}</router-link>
+                <router-link :to="{ name: 'accounts', params: { toRefresh: true } }" role="button" :aria-label="$t('commons.close_the_x_page', {pagetitle: pagetitle})" class="button is-dark is-rounded">{{ $t('commons.close') }}</router-link>
             </p>
         </vue-footer>
     </responsive-width-wrapper>
@@ -86,6 +86,7 @@
     export default {
         data() {
             return {
+                pagetitle: this.$t('commons.about'),
                 infos : null,
                 options : null,
                 showUserOptions: false,

+ 112 - 101
resources/js/views/Accounts.vue

@@ -56,12 +56,106 @@
                 </p>
                 <!-- Cancel button -->
                 <p class="control">
-                    <a class="button is-dark is-rounded" @click="showGroupSelector = false">{{ $t('commons.cancel') }}</a>
+                    <button class="button is-dark is-rounded" @click="showGroupSelector = false">{{ $t('commons.cancel') }}</button>
                 </p>
             </vue-footer>
         </div>
+        <!-- header -->
+        <div class="header has-background-black-ter" v-if="this.showAccounts || this.showGroupSwitch">
+            <div class="columns is-gapless is-mobile is-centered">
+                <div v-if="editMode" class="column">
+                    <!-- toolbar -->
+                    <div class="toolbar has-text-centered">
+                        <div class="field is-grouped is-justify-content-center has-text-grey mb-2">
+                            <!-- selected label -->
+                            <p class="control mr-1">
+                                {{ selectedAccounts.length }}&nbsp;{{ $t('commons.selected') }}
+                            </p>
+                            <!-- deselect all -->
+                            <p class="control mr-4">
+                                <button @click="clearSelected" class="clear-selection delete" :style="{visibility: selectedAccounts.length > 0 ? 'visible' : 'hidden'}" :title="$t('commons.clear_selection')"></button>
+                            </p>
+                            <!-- select all button -->
+                            <p class="control mr-5">
+                                <button @click="selectAll" class="button has-line-height p-1 is-ghost has-background-black-ter has-text-grey" :title="$t('commons.select_all')">
+                                    <span>{{ $t('commons.all') }}</span>
+                                    <font-awesome-icon class="ml-1" :icon="['fas', 'check-square']" />
+                                </button>
+                            </p>
+                            <!-- sort asc/desc buttons -->
+                            <p class="control">
+                                <button @click="sortAsc" class="button has-line-height p-1 is-ghost has-background-black-ter has-text-grey" :title="$t('commons.sort_ascending')">
+                                    <font-awesome-icon :icon="['fas', 'sort-alpha-down']" />
+                                </button>
+                            </p>
+                            <p class="control">
+                                <button @click="sortDesc" class="button has-line-height p-1 is-ghost has-background-black-ter has-text-grey" :title="$t('commons.sort_descending')">
+                                    <font-awesome-icon :icon="['fas', 'sort-alpha-up']" />
+                                </button>
+                            </p>
+                        </div>
+                        <div class="field is-grouped is-justify-content-center pb-2">
+                            <!-- Change group button -->
+                            <div v-if="selectedAccounts.length > 0" class="control">
+                                <div tabindex="0" role="button" class="tag-button tag-button-link tags are-medium has-addons is-clickable" @click="showGroupSelector = true" @keyup.enter="showGroupSelector = true">
+                                    <span class="tag is-dark mb-0">
+                                        {{ $t('groups.change_group') }}
+                                    </span>
+                                    <span class="tag is-link mb-0">
+                                        <font-awesome-icon :icon="['fas', 'layer-group']" />
+                                    </span>
+                                </div>
+                            </div>
+                            <!-- delete selected button -->
+                            <div v-if="selectedAccounts.length > 0" class="control">
+                                <div tabindex="0" role="button" class="tag-button tag-button-danger tags are-medium has-addons is-clickable" @click="destroyAccounts" @keyup.enter="destroyAccounts">
+                                    <span class="tag is-dark mb-0">
+                                        {{ $t('commons.delete') }}
+                                    </span>
+                                    <span class="tag is-danger mb-0">
+                                        <font-awesome-icon :icon="['fas', 'trash']" />
+                                    </span>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div v-else class="column is-three-quarters-mobile is-one-third-tablet is-one-quarter-desktop is-one-quarter-widescreen is-one-quarter-fullhd">
+                    <!-- search -->
+                    <div role="search" class="field">
+                        <div class="control has-icons-right">
+                            <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" />
+                                <button tabindex="1" :title="$t('commons.clear_search')" class="clear-selection delete" v-if="search" @click="search = '' "></button>
+                            </span>
+                        </div>
+                    </div>
+                    <!-- group switch toggle -->
+                    <div class="has-text-centered">
+                        <div class="columns">
+                            <div class="column" v-if="!showGroupSwitch">
+                                <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>
+                                <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>
+                </div>
+            </div>
+        </div>
+        <!-- modal -->
+        <modal v-model="showTwofaccountInModal">
+            <otp-displayer ref="OtpDisplayer"></otp-displayer>
+        </modal>
         <!-- show accounts list -->
-        <div class="container" v-if="this.showAccounts">
+        <div class="container" v-if="this.showAccounts" :class="editMode ? 'is-edit-mode' : ''">
             <!-- accounts -->
             <!-- <vue-pull-refresh :on-refresh="onRefresh" :config="{
                 errorLabel: 'error',
@@ -77,7 +171,7 @@
         	                        <div class="tfa-cell tfa-checkbox" v-if="editMode">
         	                            <div class="field">
         	                                <input class="is-checkradio is-small is-white" :id="'ckb_' + account.id" :value="account.id" type="checkbox" :name="'ckb_' + account.id" v-model="selectedAccounts">
-        	                                <label :for="'ckb_' + account.id"></label>
+        	                                <label tabindex="0" :for="'ckb_' + account.id" v-on:keypress.space.prevent="selectAccount(account.id)"></label>
         	                            </div>
         	                        </div>
         	                    </transition>
@@ -135,94 +229,6 @@
                 </p>
             </vue-footer>
         </div>
-        <!-- header -->
-        <div class="header has-background-black-ter" v-if="this.showAccounts || this.showGroupSwitch">
-            <div class="columns is-gapless is-mobile is-centered">
-                <div v-if="editMode" class="column">
-                    <!-- toolbar -->
-                    <div class="toolbar has-text-centered">
-                        <div class="field is-grouped is-justify-content-center">
-                            <div class="control">
-                                <div class="tags has-addons are-medium">
-                                    <span class="tag is-dark has-background-black-ter has-text-grey">
-                                        {{ 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 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>
-                                </div>
-                            </div>
-                            <div class="control">
-                                <div class="tags has-addons are-medium">
-                                    <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 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>
-                            </div>
-                        </div>
-                        <div class="field is-grouped is-justify-content-center pt-1">
-                            <div class="control">
-                                <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>
-                                    <span class="tag is-link">
-                                        <font-awesome-icon :icon="['fas', 'layer-group']" />
-                                    </span>
-                                </div>
-                            </div>
-                            <div class="control">
-                                <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>
-                                    <span class="tag is-danger">
-                                        <font-awesome-icon :icon="['fas', 'trash']" />
-                                    </span>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-                <div v-else class="column is-three-quarters-mobile is-one-third-tablet is-one-quarter-desktop is-one-quarter-widescreen is-one-quarter-fullhd">
-                    <!-- search -->
-                    <div class="field">
-                        <div class="control has-icons-right">
-                            <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>
-                            </span>
-                        </div>
-                    </div>
-                    <!-- group switch toggle -->
-                    <div class="has-text-centered">
-                        <div class="columns">
-                            <div class="column" v-if="!showGroupSwitch">
-                                <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>
-                                <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>
-                </div>
-            </div>
-        </div>
-        <!-- modal -->
-        <modal v-model="showTwofaccountInModal">
-            <otp-displayer ref="OtpDisplayer"></otp-displayer>
-        </modal>
     </div>
 </template>
 
@@ -353,7 +359,6 @@
 
             // stop OTP generation on modal close
             this.$on('modalClose', function() {
-                console.log('modalClose triggered')
                 this.$refs.OtpDisplayer.clearOTP()
             });
 
@@ -416,15 +421,7 @@
             showAccount(account) {
                 // In Edit mode clicking an account do not show the otpDisplayer but select the account
                 if(this.editMode) {
-
-                    for (var i=0 ; i<this.selectedAccounts.length ; i++) {
-                        if ( this.selectedAccounts[i] === account.id ) {
-                            this.selectedAccounts.splice(i,1);
-                            return
-                        }
-                    }
-
-                    this.selectedAccounts.push(account.id)
+                    selectAccount(account.id)
                 }
                 else {
                     this.$refs.OtpDisplayer.show(account.id)
@@ -432,6 +429,20 @@
             },
 
 
+            /**
+             * Select an account while in edit mode
+             */
+            selectAccount(accountId) {
+                for (var i=0 ; i<this.selectedAccounts.length ; i++) {
+                    if ( this.selectedAccounts[i] === accountId ) {
+                        this.selectedAccounts.splice(i,1);
+                        return
+                    }
+                }
+
+                this.selectedAccounts.push(accountId)
+            },
+
             /**
              * Get a fresh OTP for the provided account
              */

+ 2 - 2
resources/js/views/Capture.vue

@@ -24,9 +24,9 @@
         </div>
         <div class="fullscreen-footer">
             <!-- Cancel button -->
-            <label class="button is-large is-warning is-rounded" @click="exitStream()">
+            <button class="button is-large is-warning is-rounded" @click="exitStream()">
                 {{ $t('commons.cancel') }}
-            </label>
+            </button>
         </div>
     </div>
 </template>

+ 1 - 1
resources/js/views/Groups.vue

@@ -19,7 +19,7 @@
                     {{ $t('commons.delete') }}
                 </button>
                 <!-- edit link -->
-                <router-link :to="{ name: 'editGroup', params: { id: group.id, name: group.name }}" class="has-text-grey pl-1" :title="$t('commons.rename')">
+                <router-link :to="{ name: 'editGroup', params: { id: group.id, name: group.name }}" class="has-text-grey px-1" :title="$t('commons.rename')">
                     <font-awesome-icon :icon="['fas', 'pen-square']" />
                 </router-link>
                 <span class="is-family-primary is-size-6 is-size-7-mobile has-text-grey">{{ group.twofaccounts_count }} {{ $t('twofaccounts.accounts') }}</span>

+ 6 - 6
resources/js/views/Start.vue

@@ -11,14 +11,14 @@
             <div class="column is-full quick-uploader-button" >
                 <div class="quick-uploader-centerer">
                     <!-- upload a qr code (with basic file field and backend decoding) -->
-                    <label v-if="$root.appSettings.useBasicQrcodeReader" class="button is-link is-medium is-rounded is-focused" ref="qrcodeInputLabel">
-                        <input class="file-input" type="file" accept="image/*" v-on:change="submitQrCode" ref="qrcodeInput">
+                    <label role="button" tabindex="0" v-if="$root.appSettings.useBasicQrcodeReader" class="button is-link is-medium is-rounded is-focused" ref="qrcodeInputLabel" @keyup.enter="$refs.qrcodeInputLabel.click()">
+                        <input aria-hidden="true" tabindex="-1" class="file-input" type="file" accept="image/*" v-on:change="submitQrCode" ref="qrcodeInput">
                         {{ $t('twofaccounts.forms.upload_qrcode') }}
                     </label>
                     <!-- scan button that launch camera stream -->
-                    <label v-else class="button is-link is-medium is-rounded is-focused" @click="capture()">
+                    <button v-else class="button is-link is-medium is-rounded is-focused is-double-focused" @click="capture()">
                         {{ $t('twofaccounts.forms.scan_qrcode') }}
-                    </label>
+                    </button>
                 </div>
             </div>
             <!-- alternative methods -->
@@ -26,8 +26,8 @@
                 <div class="block has-text-light">{{ $t('twofaccounts.forms.alternative_methods') }}</div>
                 <!-- upload a qr code -->
                 <div class="block has-text-link" v-if="!$root.appSettings.useBasicQrcodeReader">
-                    <label class="button is-link is-outlined is-rounded" ref="qrcodeInputLabel">
-                        <input class="file-input" type="file" accept="image/*" v-on:change="submitQrCode" ref="qrcodeInput">
+                    <label role="button" tabindex="0" class="button is-link is-outlined is-rounded" ref="qrcodeInputLabel" @keyup.enter="$refs.qrcodeInputLabel.click()">
+                        <input aria-hidden="true" tabindex="-1" class="file-input" type="file" accept="image/*" v-on:change="submitQrCode" ref="qrcodeInput">
                         {{ $t('twofaccounts.forms.upload_qrcode') }}
                     </label>
                 </div>

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

@@ -29,9 +29,9 @@
                     <p>{{ $t('auth.forms.dont_have_account_yet') }}&nbsp;<router-link id="lnkRegister" :to="{ name: 'register' }" class="is-link">{{ $t('auth.register') }}</router-link></p>
                 </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.forms.forgot_your_password') }}&nbsp;<router-link id="lnkResetPwd" :to="{ name: 'password.request' }" class="is-link" :aria-label="$t('auth.forms.reset_your_password')">{{ $t('auth.forms.request_password_reset') }}</router-link></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>
+                        <a id="lnkSignWithWebauthn" role="button" class="is-link" @keyup.enter="showWebauthn = true" @click="showWebauthn = true" tabindex="0" :aria-label="$t('auth.sign_in_using_security_device')">{{ $t('auth.webauthn.security_device') }}</a>
                     </p>
                 </div>
             </div>

+ 3 - 1
resources/js/views/auth/webauthn/Recover.vue

@@ -10,7 +10,9 @@
         <div v-else>
             <div class="field">
                 <input id="unique" name="unique" type="checkbox" class="is-checkradio is-info" v-model="unique" >
-                <label for="unique" class="label">{{ $t('auth.webauthn.disable_all_other_devices') }}</label>
+                <label tabindex="0" for="unique" class="label" ref="uniqueLabel" v-on:keypress.space.prevent="unique = true">
+                    {{ $t('auth.webauthn.disable_all_other_devices') }}
+                </label>
             </div>
             <div class="field is-grouped">
                 <div class="control">

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

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

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

@@ -47,7 +47,7 @@
                 <vue-footer :showButtons="true">
                     <!-- close button -->
                     <p class="control">
-                        <router-link :to="{ name: 'accounts', params: { toRefresh: false } }" class="button is-dark is-rounded">{{ $t('commons.close') }}</router-link>
+                        <router-link :to="{ name: 'accounts', params: { toRefresh: false } }" class="button is-dark is-rounded" tabindex="0">{{ $t('commons.close') }}</router-link>
                     </p>
                 </vue-footer>
             </form-wrapper>

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

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

+ 18 - 16
resources/js/views/twofaccounts/Create.vue

@@ -14,8 +14,8 @@
                         </otp-displayer>
                     </div>
                 </div>
-                <div class="columns is-mobile" v-if="form.errors.any()">
-                    <div class="column">
+                <div class="columns is-mobile" role="alert">
+                    <div v-if="form.errors.any()" class="column">
                         <p v-for="(field, index) in form.errors.errors" :key="index" class="help is-danger">
                             <ul>
                                 <li v-for="(error, index) in field" :key="index">{{ error }}</li>
@@ -41,17 +41,19 @@
         <form-wrapper :title="$t('twofaccounts.forms.new_account')" v-if="showAdvancedForm">
             <form @submit.prevent="createAccount" @keydown="form.onKeydown($event)">
                 <!-- qcode fileupload -->
-                <div class="field">
-                    <div class="file is-black is-small">
-                        <label class="file-label" :title="$t('twofaccounts.forms.use_qrcode.title')">
-                            <input class="file-input" type="file" accept="image/*" v-on:change="uploadQrcode" ref="qrcodeInput">
-                            <span class="file-cta">
-                                <span class="file-icon">
-                                    <font-awesome-icon :icon="['fas', 'qrcode']" size="lg" />
+                <div class="field is-grouped">
+                    <div class="control">
+                        <div role="button" tabindex="0" class="file is-black is-small" @keyup.enter="$refs.qrcodeInputLabel.click()">
+                            <label class="file-label" :title="$t('twofaccounts.forms.use_qrcode.title')" ref="qrcodeInputLabel">
+                                <input aria-hidden="true" tabindex="-1" class="file-input" type="file" accept="image/*" v-on:change="uploadQrcode" ref="qrcodeInput">
+                                <span class="file-cta">
+                                    <span class="file-icon">
+                                        <font-awesome-icon :icon="['fas', 'qrcode']" size="lg" />
+                                    </span>
+                                    <span class="file-label">{{ $t('twofaccounts.forms.prefill_using_qrcode') }}</span>
                                 </span>
-                                <span class="file-label">{{ $t('twofaccounts.forms.prefill_using_qrcode') }}</span>
-                            </span>
-                        </label>
+                            </label>
+                        </div>
                     </div>
                 </div>
                 <field-error :form="form" field="qrcode" class="help-for-file" />
@@ -73,9 +75,9 @@
                     </div>
                     <!-- upload button -->
                     <div class="control">
-                        <div class="file is-dark">
-                            <label class="file-label">
-                                <input class="file-input" type="file" accept="image/*" v-on:change="uploadIcon" ref="iconInput">
+                        <div role="button" tabindex="0" class="file is-dark" @keyup.enter="$refs.iconInputLabel.click()">
+                            <label class="file-label" ref="iconInputLabel">
+                                <input aria-hidden="true" tabindex="-1" class="file-input" type="file" accept="image/*" v-on:change="uploadIcon" ref="iconInput">
                                 <span class="file-cta">
                                     <span class="file-icon">
                                         <font-awesome-icon :icon="['fas', 'upload']" />
@@ -85,7 +87,7 @@
                             </label>
                             <span class="tag is-black is-large" v-if="tempIcon">
                                 <img class="icon-preview" :src="'/storage/icons/' + tempIcon" :alt="$t('twofaccounts.icon_to_illustrate_the_account')">
-                                <button class="delete is-small" @click.prevent="deleteIcon" :aria-label="$t('twofaccounts.remove_icon')"></button>
+                                <button class="clear-selection delete is-small" @click.prevent="deleteIcon" :aria-label="$t('twofaccounts.remove_icon')"></button>
                             </span>
                         </div>
                     </div>

+ 12 - 12
resources/js/views/twofaccounts/Edit.vue

@@ -19,9 +19,9 @@
                 </div>
                 <!-- upload button -->
                 <div class="control">
-                    <div class="file is-dark">
-                        <label class="file-label">
-                            <input class="file-input" type="file" accept="image/*" v-on:change="uploadIcon" ref="iconInput">
+                    <div role="button" tabindex="0" class="file is-dark" @keyup.enter="$refs.iconInputLabel.click()">
+                        <label class="file-label" ref="iconInputLabel">
+                            <input aria-hidden="true" tabindex="-1" class="file-input" type="file" accept="image/*" v-on:change="uploadIcon" ref="iconInput">
                             <span class="file-cta">
                                 <span class="file-icon">
                                     <font-awesome-icon :icon="['fas', 'upload']" />
@@ -31,7 +31,7 @@
                         </label>
                         <span class="tag is-black is-large" v-if="tempIcon">
                             <img class="icon-preview" :src="'/storage/icons/' + tempIcon" :alt="$t('twofaccounts.icon_to_illustrate_the_account')">
-                            <button class="delete is-small" @click.prevent="deleteIcon" :aria-label="$t('twofaccounts.remove_icon')"></button>
+                            <button class="clear-selection delete is-small" @click.prevent="deleteIcon" :aria-label="$t('twofaccounts.remove_icon')"></button>
                         </span>
                     </div>
                 </div>
@@ -57,18 +57,18 @@
                         <input :id="this.inputId('text','secret')" class="input" type="text" v-model="form.secret" :disabled="secretIsLocked">
                     </p>
                     <p class="control" v-if="secretIsLocked">
-                        <a class="button is-dark field-lock" @click="secretIsLocked = false" :title="$t('twofaccounts.forms.unlock.title')">
+                        <button type="button" class="button is-dark field-lock" @click.stop="secretIsLocked = false" :title="$t('twofaccounts.forms.unlock.title')">
                             <span class="icon">
                                 <font-awesome-icon :icon="['fas', 'lock']" />
                             </span>
-                        </a>
+                        </button>
                     </p>
                     <p class="control" v-else>
-                        <a class="button is-dark field-unlock"  @click="secretIsLocked = true" :title="$t('twofaccounts.forms.lock.title')">
+                        <button type="button" class="button is-dark field-unlock"  @click.stop="secretIsLocked = true" :title="$t('twofaccounts.forms.lock.title')">
                             <span class="icon has-text-danger">
                                 <font-awesome-icon :icon="['fas', 'lock-open']" />
                             </span>
-                        </a>
+                        </button>
                     </p>
                 </div>
                 <div class="field">
@@ -96,18 +96,18 @@
                                 <input class="input" type="text" placeholder="" v-model="form.counter" :disabled="counterIsLocked" />
                             </div>
                             <div class="control" v-if="counterIsLocked">
-                                <a class="button is-dark field-lock" @click="counterIsLocked = false" :title="$t('twofaccounts.forms.unlock.title')">
+                                <button type="button" class="button is-dark field-lock" @click="counterIsLocked = false" :title="$t('twofaccounts.forms.unlock.title')">
                                     <span class="icon">
                                         <font-awesome-icon :icon="['fas', 'lock']" />
                                     </span>
-                                </a>
+                                </button>
                             </div>
                             <div class="control" v-else>
-                                <a class="button is-dark field-unlock"  @click="counterIsLocked = true" :title="$t('twofaccounts.forms.lock.title')">
+                                <button type="button" class="button is-dark field-unlock"  @click="counterIsLocked = true" :title="$t('twofaccounts.forms.lock.title')">
                                     <span class="icon has-text-danger">
                                         <font-awesome-icon :icon="['fas', 'lock-open']" />
                                     </span>
-                                </a>
+                                </button>
                             </div>
                         </div>
                         <field-error :form="form" field="counter" />

+ 2 - 2
resources/js/views/twofaccounts/QRcode.vue

@@ -8,9 +8,9 @@
         </div>
         <div class="fullscreen-footer">
             <!-- Close button -->
-            <label class="button is-dark is-rounded" @click.stop="$router.push({name: 'accounts', params: {initialEditMode: true}});">
+            <button class="button is-dark is-rounded" @click.stop="$router.push({name: 'accounts', params: {initialEditMode: true}});">
                 {{ $t('commons.close') }}
-            </label>
+            </button>
         </div>
     </div>
 </template>

+ 2 - 0
resources/lang/en/auth.php

@@ -22,6 +22,7 @@ return [
     'sign_out' => 'Sign out',
     'sign_in' => 'Sign in',
     'sign_in_using' => 'Sign in using',
+    'sign_in_using_security_device' => 'Sign in using a security device',
     'login_and_password' => 'login & password',
     'register' => 'Register',
     'welcome_back_x' => 'Welcome back {0}',
@@ -89,6 +90,7 @@ return [
         'authentication_failed' => 'Authentication failed',
         'forgot_your_password' => 'Forgot your password?',
         'request_password_reset' => 'Reset it',
+        'reset_your_password' => 'Reset your password',
         'reset_password' => 'Reset password',
         'disabled_in_demo' => 'Feature disabled in Demo mode',
         'new_password' => 'New password',

+ 2 - 0
resources/lang/en/commons.php

@@ -27,6 +27,7 @@ return [
     'save' => 'Save',
     'close' => 'Close',
     'clear' => 'Clear',
+    'clear_search' => 'Clear search',
     'demo_do_not_post_sensitive_data' => 'This is a demo app, do not post any sensitive data',
     'testing_do_not_post_sensitive_data' => 'This is a testing app, do not post any sensitive data',
     'selected' => 'selected',
@@ -67,4 +68,5 @@ return [
     'image_of_qrcode_to_scan' => 'Image of a QR code to scan',
     'file' => 'File',
     'or' => 'OR',
+    'close_the_x_page' => 'Close the {pagetitle} page',
 ];

+ 135 - 37
resources/sass/app.scss

@@ -35,6 +35,7 @@ a:hover {
   padding-top: 1rem;
   padding-bottom: 1rem;
   width: 100%;
+  z-index: 1000;
 }
 
 @supports (padding-top: env(safe-area-inset-top)) {
@@ -51,6 +52,19 @@ a:hover {
   }
 }
 
+.modal-otp {
+  z-index: 2000;
+}
+
+.otp:focus-visible {
+  outline-offset: 3px;
+  outline: 2px dotted $dark;
+  border-radius: $radius-large;
+}
+.otp:focus:not(:focus-visible) {
+  outline: none;
+}
+
 .group-item {
   border-bottom: 1px solid hsl(0, 0%, 21%);
   padding: 0.75rem;
@@ -65,7 +79,7 @@ a:hover {
 }
 
 .accounts {
-  margin-top: 74px;
+  margin-top: 75px;
 }
 
 .groups {
@@ -82,7 +96,7 @@ a:hover {
 
 @media screen and (min-width: 769px) {
   .accounts {
-    margin-top: 98px;
+    margin-top: 99px;
   }
 }
 
@@ -205,16 +219,19 @@ a:hover {
     flex-grow: 1;
     overflow: hidden;
 }
-.tfa-content:focus, .tfa-content:focus-visible
+.tfa-content:focus-visible
 {
-    outline: 2px solid $grey;
+    outline: 1px solid $grey;
     border: none;
     outline-offset: 7px;
     border-radius: 3px;
 }
+.tfa-content:focus:not(:focus-visible) {
+  outline: none;
+}
 
-.tfa-list .tfa-content {
-    padding-right: 1rem;
+.is-edit-mode .tfa-list .tfa-content {
+    margin-right: 1rem;
 }
 
 .tfa-dots {
@@ -377,6 +394,14 @@ figure.no-icon {
   color: hsl(0, 0%, 21%);
 }
 
+.button.has-line-height {
+  height: inherit !important;
+}
+  .button.has-line-height span {
+    display: inline-block;
+    line-height: 1rem;
+  }
+
 .button.is-dark.field-lock, .button.is-dark.field-unlock {
   color: hsl(0, 0%, 48%);
 }
@@ -409,14 +434,17 @@ figure.no-icon {
 }
 
 
-
-
-.button:focus, .button:focus-visible, .button.is-focused {
+.button:focus-visible, .button.is-focused,
+.file[role=button]:focus-visible {
   border-color: transparent;
   outline-offset: 3px;
+  outline-style: solid;
+  outline-width: 2px;
 }
-a:focus, a:focus-visible {
-  outline-offset: 2px;
+.button:focus:not(:focus-visible),
+.file[role=button]:focus:not(:focus-visible)
+{
+  outline: none;
 }
 .button:focus:not(:active), .button.is-focused:not(:active),
 .button.is-white:focus:not(:active), .button.is-white.is-focused:not(:active),
@@ -434,62 +462,120 @@ a:focus, a:focus-visible {
 {
 	box-shadow: none;
 }
-
 .button.is-white:focus, .button.is-white:focus-visible, .button.is-white.is-focused
 {
-  outline: 2px solid $white;
+  outline-color: $white;
 }
 .button.is-light:focus, .button.is-light:focus-visible, .button.is-light.is-focused
 {
-  outline: 2px solid $grey-lightest;
+  outline-color: $grey-lightest;
 }
-.button.is-dark:focus, .button.is-dark:focus-visible, .button.is-dark.is-focused
+.button.is-dark:focus, .button.is-dark:focus-visible, .button.is-dark.is-focused,
+.file[role=button].is-dark:focus, .file[role=button].is-dark:focus-visible
 {
-  outline: 2px solid $dark;
+  outline-color: $dark;
 }
-.button.is-black:focus, .button.is-black:focus-visible, .button.is-black.is-focused
+.button.is-black:focus, .button.is-black:focus-visible, .button.is-black.is-focused,
+.file[role=button].is-black:focus, .file[role=button].is-black:focus-visible
 {
-  outline: 2px solid $black;
+  outline-color: $black;
 }
 .button.is-text:focus, .button.is-text:focus-visible, .button.is-text.is-focused
 {
-  outline: 2px solid $text;
+  outline-color: $text;
 }
 .button.is-ghost:focus, .button.is-ghost:focus-visible, .button.is-ghost.is-focused
 {
-  outline: 2px solid $text;
+  outline-color: $text;
 }
 .button.is-primary:focus, .button.is-primary:focus-visible, .button.is-primary.is-focused
 {
-  outline: 2px solid $primary;
+  outline-color: $primary;
 }
 .button.is-link:focus, .button.is-link:focus-visible, .button.is-link.is-focused
 {
-  outline: 2px solid $link;
+  outline-color: $link;
 }
 .button.is-info:focus, .button.is-info:focus-visible, .button.is-info.is-focused
 {
-  outline: 2px solid $info;
+  outline-color: $info;
 }
 .button.is-success:focus, .button.is-success:focus-visible, .button.is-success.is-focused
 {
-  outline: 2px solid $success;
+  outline-color: $success;
 }
 .button.is-warning:focus, .button.is-warning:focus-visible, .button.is-warning.is-focused
 {
-  outline: 2px solid $warning;
+  outline-color: $warning;
 }
 .button.is-danger:focus, .button.is-danger:focus-visible, .button.is-danger.is-focused
 {
-  outline: 2px solid $danger;
+  outline-color: $danger;
+}
+
+button.is-double-focused:focus-visible
+{
+  outline-style: double !important;
+  outline-width: 6px !important;
+}
+button.is-double-focused:focus:not(:focus-visible)
+{
+  outline: none;
+}
+
+.file[role=button]:focus-visible {
+  border-radius: $radius;
+}
+.file[role=button].is-small:focus-visible {
+  border-radius: $radius-small;
 }
 
-a:focus, a:focus-visible
+.tag-button:focus-visible
 {
+  border-color: transparent;
   border-radius: 3px;
-  outline: 1px dashed $link;
+  outline-width: 1px;
+  outline-style: solid;
+  outline-offset: 3px;
+}
+.tag-button:focus:not(:focus-visible)
+{
+  outline: none;
+}
+.tag-button-link:focus-visible
+{
+  outline-color: $link;
+}
+.tag-button-danger:focus-visible
+{
+  outline-color: $danger;
+}
+
+.clear-selection
+{
+  vertical-align: bottom;
+}
+.clear-selection:focus-visible
+{
+  border-color: transparent;
+  outline-offset: 1px;
+  outline: 2px solid $text;
+}
+.clear-selection:focus:not(:focus-visible)
+{
+  outline: none;
 }
 
+a:focus-visible
+{
+  outline-offset: 2px;
+  border-radius: 3px;
+  outline: 1px dashed $link;
+}
+a:focus:not(:focus-visible)
+{
+  outline: none;
+}
 a.has-text-black-bis:focus, a.has-text-black-bis:focus-visible {
   outline-color: $black-bis;
 }
@@ -518,19 +604,36 @@ a.has-text-white-bis:focus, a.has-text-white-bis:focus-visible {
   outline-color: $white-bis;
 }
 
-.tabs a:focus, .tabs a:focus-visible {
+a.tag.is-dark.is-rounded:focus-visible
+{
+  outline-offset: 1px;
+  outline: 1px solid $grey;
+}
+a.tag.is-dark.is-rounded:focus:not(:focus-visible)
+{
+  outline: none;
+}
+
+.tabs a:focus-visible {
   outline-offset: -4px;
 }
+.tabs a:focus:not(:focus-visible)
+{
+  outline: none;
+}
 
 .control.has-icons-right > span.icon:focus-visible,
-.control.has-icons-left > span.icon:focus-visible,
-.control.has-icons-right > span.icon:focus,
-.control.has-icons-left > span.icon:focus
+.control.has-icons-left > span.icon:focus-visible
 {
   outline: none;
   border: 1px solid $input-focus-border-color;
   box-shadow: $input-focus-box-shadow-size $input-focus-box-shadow-color;
 }
+.control.has-icons-right > span.icon:focus:not(:focus-visible),
+.control.has-icons-left > span.icon:focus:not(:focus-visible)
+{
+  outline: none;
+}
 
 .is-checkradio[type="checkbox"] + label:focus, 
 .is-checkradio[type="checkbox"] + label:focus-visible
@@ -634,10 +737,6 @@ footer .field.is-grouped {
   }
 }
 
-.notification .notification-title {
-  // Style for title line
-}
-
 .notification .notification-content {
   text-align: center;
 }
@@ -773,7 +872,6 @@ footer .field.is-grouped {
   margin: 0 5px;
 }
 
-
 .fadeInOut-enter-active {
     animation: fadeIn 500ms
 }