From d764d1c79444f0745ad7f55adba5c85f06bd17a5 Mon Sep 17 00:00:00 2001 From: Bubka <858858+Bubka@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:02:02 +0100 Subject: [PATCH] Fix & Enhance accessibility --- resources/js/assets/app.scss | 24 +++++++++++--- .../js/components/formElements/FieldError.vue | 6 +++- .../components/formElements/FormCheckbox.vue | 7 +++-- .../js/components/formElements/FormField.vue | 15 +++++++-- .../components/formElements/FormLockField.vue | 15 +++++++-- .../formElements/FormPasswordField.vue | 15 +++++++-- .../js/components/formElements/FormSelect.vue | 23 ++++++++++++-- .../components/formElements/FormTextarea.vue | 19 +++++++++--- .../js/components/formElements/FormToggle.vue | 31 +++++++++++++------ resources/js/composables/helpers.js | 15 +++++++++ resources/js/views/admin/AppSetup.vue | 14 ++++----- resources/js/views/admin/users/Create.vue | 6 ++-- resources/js/views/auth/Login.vue | 4 +-- resources/js/views/auth/Register.vue | 6 ++-- resources/js/views/auth/password/Reset.vue | 2 +- resources/js/views/auth/webauthn/Recover.vue | 2 +- resources/js/views/settings/Account.vue | 20 +++++++----- .../js/views/twofaccounts/CreateUpdate.vue | 10 +++--- 18 files changed, 171 insertions(+), 63 deletions(-) diff --git a/resources/js/assets/app.scss b/resources/js/assets/app.scss index f9b4df0d..480e4f3f 100644 --- a/resources/js/assets/app.scss +++ b/resources/js/assets/app.scss @@ -498,7 +498,7 @@ figure.no-icon { :root[data-theme="dark"] .select select, :root[data-theme="dark"] .textarea { background-color: $grey-darker; - border-color: hsl(0, 0%, 29%); + border-color: $grey-dark; color: hsl(0, 0%, 100%); } @@ -867,9 +867,13 @@ button.button.tag.is-white, .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; + outline-offset: 2px; + outline: 1px solid $dark; +} + +:root[data-theme="dark"] .is-checkradio[type="checkbox"]+label:focus::before, +:root[data-theme="dark"] .is-checkradio[type="checkbox"]+label:focus-visible::before { + outline: 1px solid $grey-light; } .is-checkradio[type="checkbox"]+label::before { @@ -896,6 +900,18 @@ button.button.tag.is-white, color: hsl(0, 0%, 48%); } +.select select:focus, +.select select:focus-visible { + outline: 2px solid $grey-dark; + outline-offset: 3px; + box-shadow: none; +} + +:root[data-theme="dark"] .select select:focus, +:root[data-theme="dark"] .select select:focus-visible { + outline: 2px solid $grey-dark; +} + .is-underscored { border-bottom: 1px solid hsl(0, 0%, 29%); height: 0.6rem; diff --git a/resources/js/components/formElements/FieldError.vue b/resources/js/components/formElements/FieldError.vue index 860a4d0a..a32d0126 100644 --- a/resources/js/components/formElements/FieldError.vue +++ b/resources/js/components/formElements/FieldError.vue @@ -1,4 +1,6 @@ <script setup> + import { useValidationErrorIdGenerator } from '@/composables/helpers' + const props = defineProps({ error: { type: String, @@ -13,11 +15,13 @@ default: 'is-danger' } }) + + const { valErrorId } = useValidationErrorIdGenerator(props.field) </script> <template> <div role="alert"> - <p :id="'valError' + field[0].toUpperCase() + field.toLowerCase().slice(1)" + <p :id="valErrorId" class="help" :class="alertType" v-html="error" /> diff --git a/resources/js/components/formElements/FormCheckbox.vue b/resources/js/components/formElements/FormCheckbox.vue index d070cdb2..14d9d7b0 100644 --- a/resources/js/components/formElements/FormCheckbox.vue +++ b/resources/js/components/formElements/FormCheckbox.vue @@ -1,4 +1,6 @@ <script setup> + import { useIdGenerator } from '@/composables/helpers' + defineOptions({ inheritAttrs: false }) @@ -27,6 +29,7 @@ }) const emit = defineEmits(['update:modelValue']) + const legendId = useIdGenerator('legend', props.fieldName).inputId const attrs = useAttrs() const model = computed({ get() { @@ -50,9 +53,9 @@ <FontAwesomeIcon class="has-text-grey" :icon="['fas', 'chevron-right']" transform="rotate-135"/> </div> <div> - <input :id="fieldName" type="checkbox" :name="fieldName" class="is-checkradio is-info" v-model="model" :disabled="isDisabled" /> + <input :id="fieldName" type="checkbox" :name="fieldName" class="is-checkradio is-info" v-model="model" :disabled="isDisabled" :aria-describedby="help ? legendId : undefined" /> <label tabindex="0" :for="fieldName" class="label" :class="labelClass" v-html="$t(label)" v-on:keypress.space.prevent="toggleModel" /> - <p class="help" v-html="$t(help)" v-if="help" /> + <p :id="legendId" class="help" v-html="$t(help)" v-if="help" /> </div> </div> </template> \ No newline at end of file diff --git a/resources/js/components/formElements/FormField.vue b/resources/js/components/formElements/FormField.vue index b37371fd..a2c29368 100644 --- a/resources/js/components/formElements/FormField.vue +++ b/resources/js/components/formElements/FormField.vue @@ -1,5 +1,5 @@ <script setup> - import { useIdGenerator } from '@/composables/helpers' + import { useIdGenerator, useValidationErrorIdGenerator } from '@/composables/helpers' defineOptions({ inheritAttrs: false @@ -44,9 +44,15 @@ isIndented: Boolean, leftIcon: '', rightIcon: '', + idSuffix: { + type: String, + default: '' + }, }) - const { inputId } = useIdGenerator(props.inputType, props.fieldName) + const { inputId } = useIdGenerator(props.inputType, props.fieldName + props.idSuffix) + const { valErrorId } = useValidationErrorIdGenerator(props.fieldName) + const legendId = useIdGenerator('legend', props.fieldName).inputId </script> <template> @@ -68,6 +74,9 @@ v-on:input="$emit('update:modelValue', $event.target.value)" v-on:change="$emit('change:modelValue', $event.target.value)" :maxlength="maxLength" + :aria-describedby="help ? legendId : undefined" + :aria-invalid="fieldError != undefined" + :aria-errormessage="fieldError != undefined ? valErrorId : undefined" /> <span v-if="leftIcon" class="icon is-small is-left"> <FontAwesomeIcon :icon="['fas', leftIcon]" transform="rotate-75" size="xs" /> @@ -77,7 +86,7 @@ </span> </div> <FieldError v-if="fieldError != undefined" :error="fieldError" :field="fieldName" /> - <p class="help" v-html="$t(help)" v-if="help"></p> + <p :id="legendId" class="help" v-html="$t(help)" v-if="help"></p> </div> </div> </template> diff --git a/resources/js/components/formElements/FormLockField.vue b/resources/js/components/formElements/FormLockField.vue index 96cf3794..dad1a430 100644 --- a/resources/js/components/formElements/FormLockField.vue +++ b/resources/js/components/formElements/FormLockField.vue @@ -1,5 +1,5 @@ <script setup> - import { useIdGenerator } from '@/composables/helpers' + import { useIdGenerator, useValidationErrorIdGenerator } from '@/composables/helpers' import { UseColorMode } from '@vueuse/components' defineOptions({ @@ -50,10 +50,16 @@ maxLength: { type: Number, default: null + }, + idSuffix: { + type: String, + default: '' } }) - const { inputId } = useIdGenerator(props.inputType, props.fieldName) + const { inputId } = useIdGenerator(props.inputType, props.fieldName + props.idSuffix) + const { valErrorId } = useValidationErrorIdGenerator(props.fieldName) + const legendId = useIdGenerator('legend', props.fieldName).inputId const fieldIsLocked = ref(props.isDisabled || props.isEditMode) const hasBeenTrimmed = ref(false) @@ -106,6 +112,9 @@ v-on:change="emitValue" v-on:blur="forceRefresh" :maxlength="maxLength" + :aria-describedby="help ? legendId : undefined" + :aria-invalid="fieldError != undefined" + :aria-errormessage="fieldError != undefined ? valErrorId : undefined" /> </div> <UseColorMode v-slot="{ mode }" v-if="isEditMode"> @@ -127,5 +136,5 @@ </div> <FieldError v-if="hasBeenTrimmed" :error="$t('twofaccounts.forms.spaces_are_ignored')" :field="'spaces'" :alertType="'is-warning'" /> <FieldError v-if="fieldError != undefined" :error="fieldError" :field="fieldName" /> - <p class="help" v-html="$t(help)" v-if="help"></p> + <p :id="legendId" class="help" v-html="$t(help)" v-if="help"></p> </template> diff --git a/resources/js/components/formElements/FormPasswordField.vue b/resources/js/components/formElements/FormPasswordField.vue index 73c1a732..fbe6e732 100644 --- a/resources/js/components/formElements/FormPasswordField.vue +++ b/resources/js/components/formElements/FormPasswordField.vue @@ -1,5 +1,5 @@ <script setup> - import { useIdGenerator } from '@/composables/helpers' + import { useIdGenerator, useValidationErrorIdGenerator } from '@/composables/helpers' defineOptions({ inheritAttrs: true @@ -41,9 +41,15 @@ type: Boolean, default: false }, + idSuffix: { + type: String, + default: '' + }, }) - const { inputId } = useIdGenerator(props.inputType, props.fieldName) + const { inputId } = useIdGenerator(props.inputType, props.fieldName + props.idSuffix) + const { valErrorId } = useValidationErrorIdGenerator(props.fieldName) + const legendId = useIdGenerator('legend', props.fieldName).inputId const currentType = ref(props.inputType) const hasCapsLockOn = ref(false) @@ -90,6 +96,9 @@ v-bind="$attrs" v-on:input="$emit('update:modelValue', $event.target.value)" v-on:keyup="checkCapsLock" + :aria-describedby="help ? legendId : undefined" + :aria-invalid="fieldError != undefined" + :aria-errormessage="fieldError != undefined ? valErrorId : undefined" /> <span v-if="currentType == 'password'" role="button" id="btnTogglePassword" 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']" /> @@ -101,7 +110,7 @@ <p class="help is-warning" v-if="hasCapsLockOn" v-html="$t('auth.forms.caps_lock_is_on')" /> <FieldError v-if="fieldError != undefined" :error="fieldError" :field="fieldName" /> <p class="help" v-html="$t(help)" v-if="help" /> - <div v-if="showRules" class="columns is-mobile is-size-7 mt-0"> + <div v-if="showRules" :id="legendId" class="columns is-mobile is-size-7 mt-0"> <div class="column is-one-third"> <span class="has-text-weight-semibold">{{ $t("auth.forms.mandatory_rules") }}</span><br /> <span class="is-underscored" id="valPwdIsLongEnough" :class="{'is-dot' : IsLongEnough}"></span>{{ $t('auth.forms.is_long_enough') }}<br/> diff --git a/resources/js/components/formElements/FormSelect.vue b/resources/js/components/formElements/FormSelect.vue index 7c5ffa9f..84296152 100644 --- a/resources/js/components/formElements/FormSelect.vue +++ b/resources/js/components/formElements/FormSelect.vue @@ -1,4 +1,6 @@ <script setup> + import { useIdGenerator, useValidationErrorIdGenerator } from '@/composables/helpers' + const props = defineProps({ modelValue: [String, Number, Boolean], label: { @@ -21,9 +23,16 @@ }, isIndented: Boolean, isDisabled: Boolean, + idSuffix: { + type: String, + default: '' + }, }) const selected = ref(props.modelValue) + const { inputId } = useIdGenerator('select', props.fieldName + props.idSuffix) + const { valErrorId } = useValidationErrorIdGenerator(props.fieldName) + const legendId = useIdGenerator('legend', props.fieldName + props.idSuffix).inputId </script> <template> @@ -32,16 +41,24 @@ <FontAwesomeIcon class="has-text-grey" :icon="['fas', 'chevron-right']" transform="rotate-135"/> </div> <div> - <label class="label" v-html="$t(label)" :style="{ 'opacity': isDisabled ? '0.5' : '1' }"></label> + <label :for="inputId" class="label" v-html="$t(label)" :style="{ 'opacity': isDisabled ? '0.5' : '1' }"></label> <div class="control"> <div class="select"> - <select v-model="selected" v-on:change="$emit('update:modelValue', $event.target.value)" :disabled="isDisabled"> + <select + :id="inputId" + v-model="selected" + v-on:change="$emit('update:modelValue', $event.target.value)" + :disabled="isDisabled" + :aria-describedby="help ? legendId : undefined" + :aria-invalid="fieldError != undefined" + :aria-errormessage="fieldError != undefined ? valErrorId : undefined" + > <option v-for="option in options" :value="option.value">{{ $t(option.text) }}</option> </select> </div> </div> <FieldError v-if="fieldError != undefined" :error="fieldError" :field="fieldName" /> - <p class="help" v-html="$t(help)" v-if="help"></p> + <p :id="legendId" class="help" v-html="$t(help)" v-if="help"></p> </div> </div> </template> \ No newline at end of file diff --git a/resources/js/components/formElements/FormTextarea.vue b/resources/js/components/formElements/FormTextarea.vue index 90ade01d..168bb626 100644 --- a/resources/js/components/formElements/FormTextarea.vue +++ b/resources/js/components/formElements/FormTextarea.vue @@ -1,5 +1,5 @@ <script setup> - import { useIdGenerator } from '@/composables/helpers' + import { useIdGenerator, useValidationErrorIdGenerator } from '@/composables/helpers' defineOptions({ inheritAttrs: false @@ -42,9 +42,17 @@ default: null }, isIndented: Boolean, + leftIcon: '', + rightIcon: '', + idSuffix: { + type: String, + default: '' + } }) - const { inputId } = useIdGenerator(props.inputType, props.fieldName) + const { inputId } = useIdGenerator(props.inputType, props.fieldName + props.idSuffix) + const { valErrorId } = useValidationErrorIdGenerator(props.fieldName) + const legendId = useIdGenerator('legend', props.fieldName).inputId </script> <template> @@ -53,7 +61,7 @@ <FontAwesomeIcon class="has-text-grey" :icon="['fas', 'chevron-right']" transform="rotate-135"/> </div> <div class="field" :class="{ 'is-flex-grow-5' : isIndented }"> - <label :for="inputId" class="label" v-html="$t(label)"></label> + <label v-if="label" :for="inputId" class="label" v-html="$t(label)"></label> <div class="control" :class="{ 'has-icons-left' : leftIcon, 'has-icons-right': rightIcon }"> <textarea :disabled="isDisabled" @@ -66,10 +74,13 @@ v-on:input="$emit('update:modelValue', $event.target.value)" v-on:change="$emit('change:modelValue', $event.target.value)" :maxlength="maxLength" + :aria-describedby="help ? legendId : undefined" + :aria-invalid="fieldError != undefined" + :aria-errormessage="fieldError != undefined ? valErrorId : undefined" /> </div> <FieldError v-if="fieldError != undefined" :error="fieldError" :field="fieldName" /> - <p class="help" v-html="$t(help)" v-if="help"></p> + <p :id="legendId" class="help" v-html="$t(help)" v-if="help"></p> </div> </div> </template> diff --git a/resources/js/components/formElements/FormToggle.vue b/resources/js/components/formElements/FormToggle.vue index 82b68dfd..e49b9e7e 100644 --- a/resources/js/components/formElements/FormToggle.vue +++ b/resources/js/components/formElements/FormToggle.vue @@ -1,5 +1,5 @@ <script setup> - import { useIdGenerator } from '@/composables/helpers' + import { useIdGenerator, useValidationErrorIdGenerator } from '@/composables/helpers' import { UseColorMode } from '@vueuse/components' const props = defineProps({ @@ -27,6 +27,8 @@ // defines what events our component emits const emit = defineEmits(['update:modelValue']) + const { valErrorId } = useValidationErrorIdGenerator(props.fieldName) + const legendId = useIdGenerator('legend', props.fieldName).inputId function setRadio(event) { emit('update:modelValue', event) @@ -35,10 +37,16 @@ </script> <template> - <div class="field" :class="{ 'pt-3': hasOffset }" role="radiogroup" - :aria-labelledby="useIdGenerator('label',fieldName).inputId"> - <label v-if="label" :id="useIdGenerator('label',fieldName).inputId" class="label" v-html="$t(label)" /> - <div class="is-toggle buttons"> + <div class="field" :class="{ 'pt-3': hasOffset }"> + <span v-if="label" class="label" v-html="$t(label)" /> + <div + id="rdoGroup" + role="radiogroup" + :aria-describedby="help ? legendId : undefined" + :aria-invalid="fieldError != undefined" + :aria-errormessage="fieldError != undefined ? valErrorId : undefined" + class="is-toggle buttons" + > <UseColorMode v-slot="{ mode }"> <button v-for="choice in choices" @@ -54,21 +62,24 @@ 'is-dark': mode==='dark', 'is-multiline': choice.legend, }" - v-on:click.stop="setRadio(choice.value)" - :title="choice.title? choice.title:''"> + v-on:click.stop="setRadio(choice.value)"> <input :id="useIdGenerator('radio',choice.value).inputId" type="radio" class="is-hidden" :checked="modelValue===choice.value" :value="choice.value" - :disabled="isDisabled" /> + :disabled="isDisabled" + /> <span v-if="choice.legend" v-html="$t(choice.legend)" class="is-block is-size-7" /> - <FontAwesomeIcon :icon="['fas',choice.icon]" v-if="choice.icon" class="mr-2" /> {{ $t(choice.text) }} + <FontAwesomeIcon :icon="['fas',choice.icon]" v-if="choice.icon" class="mr-2" /> + <label :for="useIdGenerator('button',fieldName+choice.value).inputId" class="is-clickable"> + {{ $t(choice.text) }} + </label> </button> </UseColorMode> </div> <FieldError v-if="fieldError != undefined" :error="fieldError" :field="fieldName" /> - <p class="help" v-html="$t(help)" v-if="help" /> + <p :id="legendId" class="help" v-html="$t(help)" v-if="help" /> </div> </template> \ No newline at end of file diff --git a/resources/js/composables/helpers.js b/resources/js/composables/helpers.js index 5b3b6150..5d18f22d 100644 --- a/resources/js/composables/helpers.js +++ b/resources/js/composables/helpers.js @@ -24,6 +24,15 @@ export function useIdGenerator(fieldType, fieldName) { case 'label': prefix = 'lbl' break + case 'select': + prefix = 'sel' + break + case 'legend': + prefix = 'leg' + break + case 'error': + prefix = 'err' + break default: prefix = 'txt' break @@ -34,6 +43,12 @@ export function useIdGenerator(fieldType, fieldName) { } } +export function useValidationErrorIdGenerator(field) { + return { + valErrorId: 'valError' + field[0].toUpperCase() + field.toLowerCase().slice(1) + } +} + export function useDisplayablePassword(pwd, reveal = false) { const user = useUserStore() diff --git a/resources/js/views/admin/AppSetup.vue b/resources/js/views/admin/AppSetup.vue index f6f7cadd..fccfac99 100644 --- a/resources/js/views/admin/AppSetup.vue +++ b/resources/js/views/admin/AppSetup.vue @@ -78,13 +78,13 @@ <VersionChecker /> <!-- email config test --> <div class="field"> - <label class="label" v-html="$t('admin.forms.test_email.label')" /> + <label class="label" for="btnTestEmail" v-html="$t('admin.forms.test_email.label')" /> <p class="help" v-html="$t('admin.forms.test_email.help')" /> <p class="help" v-html="$t('admin.forms.test_email.email_will_be_send_to_x', { email: user.email })" /> </div> <div class="columns is-mobile is-vcentered"> <div class="column is-narrow"> - <button type="button" :class="isSendingTestEmail ? 'is-loading' : ''" class="button is-link is-rounded is-small" @click="sendTestEmail"> + <button id="btnTestEmail" type="button" :class="isSendingTestEmail ? 'is-loading' : ''" class="button is-link is-rounded is-small" @click="sendTestEmail" > <span class="icon is-small"> <FontAwesomeIcon :icon="['far', 'paper-plane']" /> </span> @@ -94,11 +94,11 @@ </div> <!-- healthcheck --> <div class="field"> - <label class="label" v-html="$t('admin.forms.health_endpoint.label')" /> + <label class="label" for="lnkHealthCheck" v-html="$t('admin.forms.health_endpoint.label')" /> <p class="help" v-html="$t('admin.forms.health_endpoint.help')" /> </div> <div class="field mb-5"> - <a target="_blank" :href="healthEndPoint">{{ healthEndPointFullPath }}</a> + <a id="lnkHealthCheck" target="_blank" :href="healthEndPoint">{{ healthEndPointFullPath }}</a> </div> <h4 class="title is-4 pt-5 has-text-grey-light">{{ $t('admin.storage') }}</h4> <!-- store icons in database --> @@ -112,19 +112,19 @@ <!-- cache management --> <div class="field"> <!-- <h5 class="title is-5">{{ $t('settings.security') }}</h5> --> - <label class="label" v-html="$t('admin.forms.cache_management.label')" /> + <label for="btnClearCache" class="label" v-html="$t('admin.forms.cache_management.label')" /> <p class="help" v-html="$t('admin.forms.cache_management.help')" /> </div> <div class="field mb-5 is-grouped"> <p class="control"> - <button type="button" :class="isClearingCache ? 'is-loading' : ''" class="button is-link is-rounded is-small" @click="clearCache"> + <button id="btnClearCache" type="button" :class="isClearingCache ? 'is-loading' : ''" class="button is-link is-rounded is-small" @click="clearCache"> {{ $t('commons.clear') }} </button> </p> </div> <!-- env vars --> <div class="field"> - <label class="label" v-html="$t('admin.variables')" /> + <label for="btnCopyEnvVars" class="label" v-html="$t('admin.variables')" /> </div> <div v-if="infos" class="about-debug box is-family-monospace is-size-7"> <CopyButton id="btnCopyEnvVars" :token="listInfos?.innerText" /> diff --git a/resources/js/views/admin/users/Create.vue b/resources/js/views/admin/users/Create.vue index 7d9f04b9..ae333ec5 100644 --- a/resources/js/views/admin/users/Create.vue +++ b/resources/js/views/admin/users/Create.vue @@ -31,9 +31,9 @@ <div> <FormWrapper title="admin.new_user"> <form @submit.prevent="createUser" @keydown="registerForm.onKeydown($event)"> - <FormField v-model="registerForm.name" fieldName="name" :fieldError="registerForm.errors.get('name')" inputType="text" label="auth.forms.name" :maxLength="255" autofocus /> - <FormField v-model="registerForm.email" fieldName="email" :fieldError="registerForm.errors.get('email')" inputType="email" label="auth.forms.email" :maxLength="255" /> - <FormPasswordField v-model="registerForm.password" fieldName="password" :fieldError="registerForm.errors.get('password')" :showRules="true" label="auth.forms.password" :autocomplete="'new-password'" /> + <FormField v-model="registerForm.name" fieldName="name" :fieldError="registerForm.errors.get('name')" inputType="text" label="auth.forms.name" autocomplete="username" :maxLength="255" autofocus /> + <FormField v-model="registerForm.email" fieldName="email" :fieldError="registerForm.errors.get('email')" inputType="email" label="auth.forms.email" autocomplete="email" :maxLength="255" /> + <FormPasswordField v-model="registerForm.password" fieldName="password" :fieldError="registerForm.errors.get('password')" :showRules="true" label="auth.forms.password" autocomplete="new-password" /> <FormCheckbox v-model="registerForm.is_admin" fieldName="is_admin" label="admin.forms.is_admin.label" help="admin.forms.is_admin.help" /> <FormButtons :isBusy="registerForm.isBusy" :isDisabled="registerForm.isDisabled" :showCancelButton="true" :cancelLandingView="'admin.users'" caption="commons.create" submitId="btnCreateUser" /> </form> diff --git a/resources/js/views/auth/Login.vue b/resources/js/views/auth/Login.vue index 5967de87..b59a1122 100644 --- a/resources/js/views/auth/Login.vue +++ b/resources/js/views/auth/Login.vue @@ -193,8 +193,8 @@ <div v-if="$2fauth.isTestingApp" class="notification is-warning has-text-centered is-radiusless" v-html="$t('auth.forms.welcome_to_testing_app_use_those_credentials')" /> <div v-if="appSettings.enableSso == true && appSettings.useSsoOnly == true" class="notification is-warning has-text-centered" v-html="$t('auth.forms.sso_only_form_restricted_to_admin')" /> <form id="frmLegacyLogin" @submit.prevent="LegacysignIn" @keydown="form.onKeydown($event)"> - <FormField v-model="form.email" fieldName="email" :fieldError="form.errors.get('email')" inputType="email" label="auth.forms.email" autofocus /> - <FormPasswordField v-model="form.password" fieldName="password" :fieldError="form.errors.get('password')" label="auth.forms.password" /> + <FormField v-model="form.email" fieldName="email" :fieldError="form.errors.get('email')" inputType="email" label="auth.forms.email" autocomplete="username" autofocus /> + <FormPasswordField v-model="form.password" fieldName="password" :fieldError="form.errors.get('password')" label="auth.forms.password" autocomplete="current-password" /> <FormButtons :isBusy="form.isBusy" caption="auth.sign_in" submitId="btnSignIn"/> </form> <div class="nav-links"> diff --git a/resources/js/views/auth/Register.vue b/resources/js/views/auth/Register.vue index f6279138..fa4a1646 100644 --- a/resources/js/views/auth/Register.vue +++ b/resources/js/views/auth/Register.vue @@ -100,9 +100,9 @@ <!-- User registration form --> <FormWrapper v-else title="auth.register" punchline="auth.forms.register_punchline"> <form @submit.prevent="register" @keydown="registerForm.onKeydown($event)"> - <FormField v-model="registerForm.name" fieldName="name" :fieldError="registerForm.errors.get('name')" inputType="text" label="auth.forms.name" :maxLength="255" autofocus /> - <FormField v-model="registerForm.email" fieldName="email" :fieldError="registerForm.errors.get('email')" inputType="email" label="auth.forms.email" :maxLength="255" /> - <FormPasswordField v-model="registerForm.password" fieldName="password" :fieldError="registerForm.errors.get('password')" :showRules="true" label="auth.forms.password" /> + <FormField v-model="registerForm.name" fieldName="name" :fieldError="registerForm.errors.get('name')" inputType="text" label="auth.forms.name" autocomplete="username" :maxLength="255" autofocus /> + <FormField v-model="registerForm.email" fieldName="email" :fieldError="registerForm.errors.get('email')" inputType="email" label="auth.forms.email" autocomplete="email" :maxLength="255" /> + <FormPasswordField v-model="registerForm.password" fieldName="password" :fieldError="registerForm.errors.get('password')" :showRules="true" autocomplete="new-password" label="auth.forms.password" /> <FormButtons :isBusy="registerForm.isBusy" :isDisabled="registerForm.isDisabled" caption="auth.register" submitId="btnRegister" /> </form> <div class="nav-links"> diff --git a/resources/js/views/auth/password/Reset.vue b/resources/js/views/auth/password/Reset.vue index 48e384c6..275e7937 100644 --- a/resources/js/views/auth/password/Reset.vue +++ b/resources/js/views/auth/password/Reset.vue @@ -46,7 +46,7 @@ <FormWrapper :title="$t('auth.forms.new_password')"> <form @submit.prevent="resetPassword" @keydown="form.onKeydown($event)"> <FormField v-model="form.email" :isDisabled="true" fieldName="email" :fieldError="form.errors.get('email')" label="auth.forms.email" autofocus /> - <FormPasswordField v-model="form.password" fieldName="password" :fieldError="form.errors.get('password')" :autocomplete="'new-password'" :showRules="true" label="auth.forms.new_password" /> + <FormPasswordField v-model="form.password" fieldName="password" :fieldError="form.errors.get('password')" autocomplete="new-password" :showRules="true" label="auth.forms.new_password" /> <FieldError v-if="form.errors.get('token') != undefined" :error="form.errors.get('token')" :field="form.token" /> <FormButtons v-if="isPending" diff --git a/resources/js/views/auth/webauthn/Recover.vue b/resources/js/views/auth/webauthn/Recover.vue index 6b91f6a0..1180d79e 100644 --- a/resources/js/views/auth/webauthn/Recover.vue +++ b/resources/js/views/auth/webauthn/Recover.vue @@ -48,7 +48,7 @@ <div> <form @submit.prevent="recover" @keydown="form.onKeydown($event)"> <FormCheckbox v-model="form.revokeAll" fieldName="revokeAll" label="auth.webauthn.disable_all_security_devices" help="auth.webauthn.disable_all_security_devices_help" /> - <FormPasswordField v-model="form.password" fieldName="password" :fieldError="form.errors.get('password')" :autocomplete="'current-password'" :showRules="false" label="auth.forms.current_password.label" help="auth.forms.current_password.help" /> + <FormPasswordField v-model="form.password" fieldName="password" :fieldError="form.errors.get('password')" autocomplete="current-password" :showRules="false" label="auth.forms.current_password.label" help="auth.forms.current_password.help" /> <div class="field"> <p> {{ $t('auth.forms.forgot_your_password') }} diff --git a/resources/js/views/settings/Account.vue b/resources/js/views/settings/Account.vue index a95ace54..b7e8f36d 100644 --- a/resources/js/views/settings/Account.vue +++ b/resources/js/views/settings/Account.vue @@ -114,22 +114,26 @@ <div v-if="$2fauth.config.proxyAuth" class="notification is-warning has-text-centered" v-html="$t('auth.user_account_controlled_by_proxy')" /> <h4 class="title is-4 has-text-grey-light">{{ $t('settings.profile') }}</h4> <fieldset :disabled="$2fauth.config.proxyAuth || user.oauth_provider"> - <FormField v-model="formProfile.name" fieldName="name" :fieldError="formProfile.errors.get('name')" label="auth.forms.name" :maxLength="255" autofocus /> - <FormField v-model="formProfile.email" fieldName="email" :fieldError="formProfile.errors.get('email')" inputType="email" label="auth.forms.email" :maxLength="255" autofocus /> - <FormField v-model="formProfile.password" fieldName="password" :fieldError="formProfile.errors.get('password')" inputType="password" label="auth.forms.current_password.label" help="auth.forms.current_password.help" /> + <FormField v-model="formProfile.name" fieldName="name" :fieldError="formProfile.errors.get('name')" label="auth.forms.name" :maxLength="255" autocomplete="username" autofocus /> + <FormField v-model="formProfile.email" fieldName="email" :fieldError="formProfile.errors.get('email')" inputType="email" label="auth.forms.email" autocomplete="email" :maxLength="255" autofocus /> + <FormField v-model="formProfile.password" fieldName="password" :fieldError="formProfile.errors.get('password')" inputType="password" label="auth.forms.current_password.label" autocomplete="current-password" help="auth.forms.current_password.help" /> <FormButtons :isBusy="formProfile.isBusy" caption="commons.update" /> </fieldset> </form> <form @submit.prevent="submitPassword" @keydown="formPassword.onKeydown($event)"> + <input hidden type="text" name="name" :value="formProfile.name" autocomplete="username" /> + <input hidden type="text" name="email" :value="formProfile.email" autocomplete="email" /> <h4 class="title is-4 pt-6 has-text-grey-light">{{ $t('settings.change_password') }}</h4> <fieldset :disabled="$2fauth.config.proxyAuth || user.oauth_provider"> - <FormPasswordField v-model="formPassword.password" fieldName="password" :fieldError="formPassword.errors.get('password')" :autocomplete="'new-password'" :showRules="true" label="auth.forms.new_password" /> - <FormPasswordField v-model="formPassword.password_confirmation" :showRules="false" fieldName="password_confirmation" :fieldError="formPassword.errors.get('password_confirmation')" inputType="password" :autocomplete="'new-password'" label="auth.forms.confirm_new_password" /> - <FormField v-model="formPassword.currentPassword" fieldName="currentPassword" :fieldError="formPassword.errors.get('currentPassword')" inputType="password" label="auth.forms.current_password.label" help="auth.forms.current_password.help" /> - <FormButtons :isBusy="formPassword.isBusy" caption="auth.forms.change_password" /> + <FormPasswordField v-model="formPassword.password" fieldName="password" :fieldError="formPassword.errors.get('password')" idSuffix="ForUpdate" autocomplete="new-password" :showRules="true" label="auth.forms.new_password" /> + <FormPasswordField v-model="formPassword.password_confirmation" :showRules="false" fieldName="password_confirmation" :fieldError="formPassword.errors.get('password_confirmation')" inputType="password" autocomplete="new-password" label="auth.forms.confirm_new_password" /> + <FormField v-model="formPassword.currentPassword" fieldName="currentPassword" :fieldError="formPassword.errors.get('currentPassword')" inputType="password" label="auth.forms.current_password.label" autocomplete="current-password" help="auth.forms.current_password.help" /> + <FormButtons :isBusy="formPassword.isBusy" submitId="btnSubmitChangePwd" caption="auth.forms.change_password" /> </fieldset> </form> <form id="frmDeleteAccount" @submit.prevent="submitDelete" @keydown="formDelete.onKeydown($event)"> + <input hidden type="text" name="name" :value="formProfile.name" autocomplete="username" /> + <input hidden type="text" name="email" :value="formProfile.email" autocomplete="email" /> <h4 class="title is-4 pt-6 has-text-danger">{{ $t('auth.forms.delete_account') }}</h4> <div class="field is-size-7-mobile"> <p class="block">{{ $t('auth.forms.delete_your_account_and_reset_all_data')}}</p> @@ -137,7 +141,7 @@ <p>{{ $t('auth.forms.deleting_2fauth_account_does_not_impact_provider') }}</p> </div> <fieldset :disabled="$2fauth.config.proxyAuth"> - <FormField v-model="formDelete.password" fieldName="password" :fieldError="formDelete.errors.get('password')" inputType="password" autocomplete="new-password" label="auth.forms.current_password.label" help="auth.forms.current_password.help" /> + <FormField v-model="formDelete.password" fieldName="password" :fieldError="formDelete.errors.get('password')" inputType="password" idSuffix="ForDelete" autocomplete="new-password" label="auth.forms.current_password.label" help="auth.forms.current_password.help" /> <FormButtons :isBusy="formDelete.isBusy" caption="auth.forms.delete_your_account" submitId="btnDeleteAccount" color="is-danger" /> </fieldset> </form> diff --git a/resources/js/views/twofaccounts/CreateUpdate.vue b/resources/js/views/twofaccounts/CreateUpdate.vue index afa9e637..b481b531 100644 --- a/resources/js/views/twofaccounts/CreateUpdate.vue +++ b/resources/js/views/twofaccounts/CreateUpdate.vue @@ -486,12 +486,12 @@ <!-- account --> <FormField v-model="form.account" fieldName="account" :fieldError="form.errors.get('account')" label="twofaccounts.account" :placeholder="$t('twofaccounts.forms.account.placeholder')" /> <!-- icon upload --> - <label class="label">{{ $t('twofaccounts.icon') }}</label> + <label for="filUploadIcon" class="label">{{ $t('twofaccounts.icon') }}</label> <div class="field is-grouped"> <!-- Try my luck button --> <div class="control" v-if="user.preferences.getOfficialIcons"> <UseColorMode v-slot="{ mode }"> - <VueButton @click="fetchLogo" :color="mode == 'dark' ? 'is-dark' : ''" :nativeType="'button'" :is-loading="fetchingLogo" :isDisabled="!form.service"> + <VueButton @click="fetchLogo" :color="mode == 'dark' ? 'is-dark' : ''" :nativeType="'button'" :is-loading="fetchingLogo" :isDisabled="!form.service" aria-describedby="lgdTryMyLuck"> <span class="icon is-small"> <FontAwesomeIcon :icon="['fas', 'globe']" /> </span> @@ -503,8 +503,8 @@ <div class="control is-flex"> <UseColorMode v-slot="{ mode }"> <div role="button" tabindex="0" class="file mr-3" :class="mode == 'dark' ? 'is-dark' : 'is-white'" @keyup.enter="iconInputLabel.click()"> - <label class="file-label" ref="iconInputLabel"> - <input tabindex="-1" class="file-input" type="file" accept="image/*" v-on:change="uploadIcon" ref="iconInput"> + <label for="filUploadIcon" class="file-label" ref="iconInputLabel"> + <input id="filUploadIcon" tabindex="-1" class="file-input" type="file" accept="image/*" v-on:change="uploadIcon" ref="iconInput"> <span class="file-cta"> <span class="file-icon"> <FontAwesomeIcon :icon="['fas', 'upload']" /> @@ -522,7 +522,7 @@ </div> <div class="field"> <FieldError v-if="iconForm.errors.hasAny('icon')" :error="iconForm.errors.get('icon')" :field="'icon'" class="help-for-file" /> - <p v-if="user.preferences.getOfficialIcons" class="help" v-html="$t('twofaccounts.forms.i_m_lucky_legend')"></p> + <p id="lgdTryMyLuck" v-if="user.preferences.getOfficialIcons" class="help" v-html="$t('twofaccounts.forms.i_m_lucky_legend')"></p> </div> <!-- group --> <FormSelect v-if="groups.length > 0" v-model="form.group_id" :options="groups" fieldName="group_id" label="twofaccounts.forms.group.label" help="twofaccounts.forms.group.help" />