Files
speckle-server/packages/ui-components/src/components/form/CodeInput.vue
T
andrewwallacespeckle 91cb011ded feat(fe2): New user onboarding flow (#3932)
* CodeInput. verify-email page

* middleware

* Loading toast

* Countdown only for registration

* Improve middleware

* Fix middleware breaking auth flow

* Remove old notifications

* Remove old onboarding. New segmentation

* Remove skip button

* Block verify email when verified

* useUserEmails composable. Cancel addition

* Move user emails queries

* Fix fragments etc

* redirect updates

* HeaderWithEmptyPage

* Check env before enforcing

* Join workspace

* Updates

* Fix console warnings on login

* Fix register console warnings

* Working cache updates

* Verify secondary email

* Force onboarding off

* EMAIL WIP

* useIsJustRegistered state

* Improve isRequired

* Uneeded change

* Improved slots

* Updates from CR

* CR comments

* Only show message if forced

* Update onboarding middleware

* Update loading bar

* ref > computed to fix onboarding

* Resend tooltip. Better errors

* Add other to form.

* Email changes

* Updates to emails

* Remove force email FF

* Remove FF's

* Hide header on embed

* Update graphql.ts

* Re-add FF

* Update graphql.ts

* GQL Fragments

* Fix build
2025-02-14 10:20:14 +00:00

152 lines
3.8 KiB
Vue

<template>
<div class="flex flex-col items-center">
<div class="flex gap-2">
<div v-for="(_, index) in digitCount" :key="index" class="w-10">
<FormTextInput
ref="inputRefs"
v-model="digits[index]"
class="text-center !text-[14px] py-6 !px-2 font-semibold"
color="foundation"
:name="`code-${index}`"
type="text"
inputmode="numeric"
:disabled="disabled"
:error="internalError"
:custom-error-message="internalError ? ' ' : undefined"
maxlength="1"
size="lg"
@input="onInput(index)"
@keydown="onKeyDown(index, $event)"
@paste="onPaste"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import { FormTextInput } from '~~/src/lib'
const props = withDefaults(
defineProps<{
modelValue: string
digitCount?: number
disabled?: boolean
error?: boolean
clearErrorOnEdit?: boolean
}>(),
{
digitCount: 6,
clearErrorOnEdit: true
}
)
const emit = defineEmits(['update:modelValue', 'complete'])
const inputRefs = ref<Array<HTMLInputElement | null>>([])
const digits = ref<string[]>(new Array(props.digitCount).fill('') as string[])
const internalError = ref(props.error)
const onInput = (index: number) => {
if (props.clearErrorOnEdit) {
internalError.value = false
}
digits.value[index] = digits.value[index].replace(/[^0-9]/g, '')
// Move to next input if available
if (digits.value[index] && index < props.digitCount - 1) {
inputRefs.value[index + 1]?.focus()
}
}
const onKeyDown = (index: number, event: KeyboardEvent) => {
if (event.key === 'Backspace' && !digits.value[index] && index > 0) {
if (props.clearErrorOnEdit) {
internalError.value = false
}
// Move to previous input on backspace if current is empty
digits.value[index - 1] = ''
inputRefs.value[index - 1]?.focus()
} else if (event.key === 'ArrowLeft' && index > 0) {
// Move to previous input on left arrow
inputRefs.value[index - 1]?.focus()
} else if (event.key === 'ArrowRight' && index < props.digitCount - 1) {
// Move to next input on right arrow
inputRefs.value[index + 1]?.focus()
}
}
const onPaste = (event: ClipboardEvent) => {
if (props.clearErrorOnEdit) {
internalError.value = false
}
event.preventDefault()
const pastedData = event.clipboardData?.getData('text')
if (!pastedData) return
const numbers = pastedData.replace(/[^0-9]/g, '').split('')
digits.value = [
...numbers.slice(0, props.digitCount),
...(Array(Math.max(0, props.digitCount - numbers.length)).fill('') as string[])
]
// Focus the next empty input or the last input
const nextEmptyIndex = digits.value.findIndex((d) => !d)
if (nextEmptyIndex !== -1) {
inputRefs.value[nextEmptyIndex]?.focus()
} else {
inputRefs.value[props.digitCount - 1]?.focus()
}
}
// Focus first input on mount
onMounted(() => {
if (inputRefs.value[0]) {
inputRefs.value[0].focus()
}
})
// Watch external error prop changes
watch(
() => props.error,
(newValue) => {
internalError.value = newValue
}
)
// Watch for external value changes
watch(
() => props.modelValue,
(newValue) => {
if (newValue) {
const newDigits = newValue.split('')
digits.value = [
...newDigits,
...(Array(props.digitCount - newDigits.length).fill('') as string[])
]
} else {
digits.value = Array(props.digitCount).fill('') as string[]
}
},
{ immediate: true }
)
// Watch for completion
watch(
digits,
(newDigits) => {
const value = newDigits.join('')
emit('update:modelValue', value)
// Emit complete when all digits are filled
if (value.length === props.digitCount) {
emit('complete', value)
}
},
{ deep: true }
)
</script>