Fix some accessibility issues

This commit is contained in:
Bubka 2022-10-12 11:30:20 +02:00
parent 4511df5764
commit 3fcc2b906b
19 changed files with 353 additions and 227 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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()
})
}
},

View File

@ -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,

View File

@ -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
*/

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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" />

View File

@ -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>

View File

@ -22,6 +22,7 @@
'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 @@
'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',

View File

@ -27,6 +27,7 @@
'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 @@
'image_of_qrcode_to_scan' => 'Image of a QR code to scan',
'file' => 'File',
'or' => 'OR',
'close_the_x_page' => 'Close the {pagetitle} page',
];

View File

@ -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;
}
a:focus, a:focus-visible
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;
}
.tag-button:focus-visible
{
border-color: transparent;
border-radius: 3px;
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
}