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') }}&nbsp;
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" />