feat: move password to separate component

This commit is contained in:
kolaente 2021-12-26 13:37:33 +01:00
parent 6041ad1482
commit 0322daf4d4
No known key found for this signature in database
GPG key ID: F40E70337AB24C9B
5 changed files with 98 additions and 86 deletions

View file

@ -0,0 +1,85 @@
<template>
<div class="password-field">
<input
class="input"
id="password"
name="password"
:placeholder="$t('user.auth.passwordPlaceholder')"
required
:type="passwordFieldType"
autocomplete="current-password"
@keyup.enter="e => $emit('submit', e)"
:tabindex="props.tabindex"
@focusout="validate"
@input="handleInput"
/>
<a
@click="togglePasswordFieldType"
class="password-field-type-toggle"
aria-label="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')"
v-tooltip="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')">
<icon :icon="passwordFieldType === 'password' ? 'eye' : 'eye-slash'"/>
</a>
</div>
<p class="help is-danger" v-if="!isValid">
{{ $t('user.auth.passwordRequired') }}
</p>
</template>
<script lang="ts" setup>
import {ref, watch} from "vue";
import {useDebounceFn} from "@vueuse/core";
const props = defineProps({
tabindex: String,
modelValue: String,
// This prop is a workaround to trigger validation from the outside when the user never had focus in the input.
validateInitially: Boolean,
})
const emit = defineEmits(['submit', 'update:modelValue'])
const passwordFieldType = ref<String>('password')
const password = ref<String>('')
const isValid = ref<Boolean>(!props.validateInitially)
watch(
() => props.validateInitially,
(doValidate: Boolean) => {
if (doValidate) {
validate()
}
}
)
function validate() {
useDebounceFn(() => {
isValid.value = password.value !== ''
}, 100)()
}
function togglePasswordFieldType() {
passwordFieldType.value = passwordFieldType.value === 'password'
? 'text'
: 'password'
}
function handleInput(e) {
password.value = e.target.value
emit('update:modelValue', e.target.value)
}
</script>
<style scoped>
.password-field {
position: relative;
}
.password-field-type-toggle {
position: absolute;
color: var(--grey-400);
top: 50%;
right: 1rem;
transform: translateY(-50%);
}
</style>

View file

@ -4,4 +4,3 @@
@import "task"; @import "task";
@import "tasks"; @import "tasks";
@import "namespaces"; @import "namespaces";
@import 'password-field-toggle';

View file

@ -1,7 +0,0 @@
.password-field-type-toggle {
position: absolute;
color: var(--grey-400);
top: 50%;
right: 1rem;
transform: translateY(-50%);
}

View file

@ -39,31 +39,7 @@
{{ $t('user.auth.forgotPassword') }} {{ $t('user.auth.forgotPassword') }}
</router-link> </router-link>
</div> </div>
<div class="control"> <password tabindex="2" @submit="submit" v-model="password" :validate-initially="validatePasswordInitially"/>
<input
class="input"
id="password"
name="password"
:placeholder="$t('user.auth.passwordPlaceholder')"
ref="password"
required
:type="passwordFieldType"
autocomplete="current-password"
@keyup.enter="submit"
tabindex="2"
@focusout="validateField('password')"
/>
<a
@click="togglePasswordFieldType"
class="password-field-type-toggle"
aria-label="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')"
v-tooltip="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')">
<icon :icon="passwordFieldType === 'password' ? 'eye' : 'eye-slash'"/>
</a>
</div>
<p class="help is-danger" v-if="!passwordValid">
{{ $t('user.auth.passwordRequired') }}
</p>
</div> </div>
<div class="field" v-if="needsTotpPasscode"> <div class="field" v-if="needsTotpPasscode">
<label class="label" for="totpPasscode">{{ $t('user.auth.totpTitle') }}</label> <label class="label" for="totpPasscode">{{ $t('user.auth.totpTitle') }}</label>
@ -87,7 +63,6 @@
@click="submit" @click="submit"
:loading="loading" :loading="loading"
tabindex="4" tabindex="4"
:disabled="!allFieldsValid"
> >
{{ $t('user.auth.login') }} {{ $t('user.auth.login') }}
</x-button> </x-button>
@ -129,9 +104,11 @@ import {getErrorText} from '@/message'
import Message from '@/components/misc/message' import Message from '@/components/misc/message'
import {redirectToProvider} from '../../helpers/redirectToProvider' import {redirectToProvider} from '../../helpers/redirectToProvider'
import {getLastVisited, clearLastVisited} from '../../helpers/saveLastVisited' import {getLastVisited, clearLastVisited} from '../../helpers/saveLastVisited'
import Password from '@/components/input/password'
export default { export default {
components: { components: {
Password,
Message, Message,
}, },
data() { data() {
@ -139,8 +116,8 @@ export default {
confirmedEmailSuccess: false, confirmedEmailSuccess: false,
errorMessage: '', errorMessage: '',
usernameValid: true, usernameValid: true,
passwordValid: true, password: '',
passwordFieldType: 'password', validatePasswordInitially: false,
} }
}, },
beforeMount() { beforeMount() {
@ -185,9 +162,6 @@ export default {
this.openidConnect.providers && this.openidConnect.providers &&
this.openidConnect.providers.length > 0 this.openidConnect.providers.length > 0
}, },
allFieldsValid() {
return this.usernameValid && this.passwordValid
},
...mapState({ ...mapState({
registrationEnabled: state => state.config.registrationEnabled, registrationEnabled: state => state.config.registrationEnabled,
loading: LOADING, loading: LOADING,
@ -215,12 +189,6 @@ export default {
} }
}, },
togglePasswordFieldType() {
this.passwordFieldType = this.passwordFieldType === 'password'
? 'text'
: 'password'
},
async submit() { async submit() {
this.errorMessage = '' this.errorMessage = ''
// Some browsers prevent Vue bindings from working with autofilled values. // Some browsers prevent Vue bindings from working with autofilled values.
@ -228,13 +196,13 @@ export default {
// For more info, see https://kolaente.dev/vikunja/frontend/issues/78 // For more info, see https://kolaente.dev/vikunja/frontend/issues/78
const credentials = { const credentials = {
username: this.$refs.username.value, username: this.$refs.username.value,
password: this.$refs.password.value, password: this.password,
} }
if (credentials.username === '' || credentials.password === '') { if (credentials.username === '' || credentials.password === '') {
// Trigger the validation error messages // Trigger the validation error messages
this.validateField('username') this.validateField('username')
this.validateField('password') this.validatePasswordInitially = true
return return
} }

View file

@ -46,30 +46,7 @@
</div> </div>
<div class="field"> <div class="field">
<label class="label" for="password">{{ $t('user.auth.password') }}</label> <label class="label" for="password">{{ $t('user.auth.password') }}</label>
<div class="control is-relative"> <password @submit="submit" @update:modelValue="v => credentials.password = v" :validate-initially="validatePasswordInitially"/>
<input
class="input"
id="password"
name="password"
:placeholder="$t('user.auth.passwordPlaceholder')"
required
:type="passwordFieldType"
autocomplete="new-password"
v-model="credentials.password"
@keyup.enter="submit"
@focusout="validatePassword"
/>
<a
@click="togglePasswordFieldType"
class="password-field-type-toggle"
aria-label="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')"
v-tooltip="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')">
<icon :icon="passwordFieldType === 'password' ? 'eye' : 'eye-slash'"/>
</a>
</div>
<p class="help is-danger" v-if="!passwordValid">
{{ $t('user.auth.passwordRequired') }}
</p>
</div> </div>
<x-button <x-button
@ -93,12 +70,13 @@
<script setup> <script setup>
import {useDebounceFn} from '@vueuse/core' import {useDebounceFn} from '@vueuse/core'
import {ref, reactive, toRaw, computed, onBeforeMount} from 'vue' import {ref, reactive, toRaw, computed, onBeforeMount, watch} from 'vue'
import router from '@/router' import router from '@/router'
import {store} from '@/store' import {store} from '@/store'
import Message from '@/components/misc/message' import Message from '@/components/misc/message'
import {isEmail} from '@/helpers/isEmail' import {isEmail} from '@/helpers/isEmail'
import Password from '@/components/input/password'
// FIXME: use the `beforeEnter` hook of vue-router // FIXME: use the `beforeEnter` hook of vue-router
// Check if the user is already logged in, if so, redirect them to the homepage // Check if the user is already logged in, if so, redirect them to the homepage
@ -116,6 +94,7 @@ const credentials = reactive({
const loading = computed(() => store.state.loading) const loading = computed(() => store.state.loading)
const errorMessage = ref('') const errorMessage = ref('')
const validatePasswordInitially = ref(false)
const DEBOUNCE_TIME = 100 const DEBOUNCE_TIME = 100
@ -130,29 +109,17 @@ const validateUsername = useDebounceFn(() => {
usernameValid.value = credentials.username !== '' usernameValid.value = credentials.username !== ''
}, DEBOUNCE_TIME) }, DEBOUNCE_TIME)
const passwordValid = ref(true)
const validatePassword = useDebounceFn(() => {
passwordValid.value = credentials.password !== ''
}, DEBOUNCE_TIME)
const everythingValid = computed(() => { const everythingValid = computed(() => {
return credentials.username !== '' && return credentials.username !== '' &&
credentials.email !== '' && credentials.email !== '' &&
credentials.password !== '' && credentials.password !== '' &&
emailValid.value && emailValid.value &&
usernameValid.value && usernameValid.value
passwordValid.value
}) })
const passwordFieldType = ref('password')
const togglePasswordFieldType = () => {
passwordFieldType.value = passwordFieldType.value === 'password'
? 'text'
: 'password'
}
async function submit() { async function submit() {
errorMessage.value = '' errorMessage.value = ''
validatePasswordInitially.value = true
if (!everythingValid.value) { if (!everythingValid.value) {
return return