nuxt / ui

A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.
https://ui.nuxt.com
MIT License
4.09k stars 535 forks source link

[PinInput] Implement component #1123

Closed aloky closed 1 week ago

aloky commented 11 months ago

https://www.radix-vue.com/components/pin-input.html

ddahan commented 10 months ago

I built this kind of component for my own project which uses Nuxt UI:

image

It has theses props:

length: number; // choose the number of digits
autoFocus: boolean;
autoValidation: boolean; // trigger a method automatically when all fields are filled
hide: boolean; // hide digits (like a password)

And it handles:

ddahan commented 10 months ago

@benjamincanac are you interesting in merging this component if I make a PR? Any special recommendations? Thanks.

benjamincanac commented 10 months ago

Not sure it makes sense to work on this now, after the radix-vue migration we'll be able to use this: https://www.radix-vue.com/components/pin-input.html

JoelHutchinson commented 5 months ago

@ddahan Do you have the source code for this component? I would love to try it out in my own Nuxt project!

sebastiandotdev commented 5 months ago

You may want to test with https://[vue-input-otp.vercel.app](https://vue-input-otp.vercel.app/)/.

emwadde commented 5 months ago

@ddahan is it possible to share this component.

ddahan commented 6 days ago

@ddahan is it possible to share this component.

@emwadde @marcbejar @pierresigwalt @JoelHutchinson Sorry I did not receive notifications for this one. Here is the code if it can still help:

<template>
  <UFormGroup>
    <div class="flex gap-2.5">
      <div v-for="i in Array.from({ length: length }, (_, i) => i)" :key="i" class="w-12">
        <div class="flex gap-1">
          <UInput
            :ref="(el) => (uInputComponents[i] = el)"
            size="xl"
            maxlength="1"
            :type="hide ? 'password' : 'text'"
            :ui="{ base: 'text-center', size: { xl: 'text-lg' } }"
            :autofocus="i === 0 && autoFocus"
            v-model="otp[i]"
            @input="autoTab(i)"
            @focus="selectTextOnFocus(i)"
            @paste="handlePaste($event)"
            @keydown.delete="handleBackspace(i, $event)"
          />
        </div>
      </div>
    </div>
  </UFormGroup>
</template>
<script setup lang="ts">
const props = withDefaults(
  defineProps<{
    length?: number;
    autoFocus?: boolean;
    autoValidation?: boolean; // only applied on pasting for better UX
    hide?: boolean;
  }>(),
  { length: 6, autoFocus: true, autoValidation: true, hide: false }
);

const emit = defineEmits(["update:modelValue", "otpAutoSubmit"]);

// State
const otp: Ref<string[]> = ref(Array(props.length).fill(""));
const uInputComponents: Ref<any[]> = ref(Array(props.length).fill(null));

// Send code string to the parent component
const code: Ref<string> = computed(() => Object.values(otp.value).join(""));
watch(code, (newCode) => {
  emit("update:modelValue", newCode);
});

const autoTab = async (i: number) => {
  /* auto-focus next input after typing */

  await nextTick(); // wait until Vue completes the DOM updates.
  if (i < props.length - 1 && otp.value[i]) {
    const nextInput = uInputComponents.value[i + 1].input;
    if (nextInput) {
      nextInput.focus();
    }
  }
};

const selectTextOnFocus = (i: number) => {
  /* auto-select input content when focusing it */

  const input = uInputComponents.value[i].input;
  input.select();
};

const handlePaste = async (event: ClipboardEvent) => {
  /* handle the case where the user paste the code (+ auto validation) */

  event.preventDefault();
  const pastedText = event.clipboardData?.getData("text").trim();
  // Paste is disabled if the text is not the right size
  if (pastedText?.length === props.length) {
    otp.value = [...pastedText].map((char, i) => char || "");
    uInputComponents.value[props.length - 1].input.focus();

    const allFieldsFilled = otp.value.every((x) => x.length === 1);
    if (allFieldsFilled && props.autoValidation) {
      await nextTick(); // prevent otpAutoSubmit to be called before code update
      emit("otpAutoSubmit");
    }
  }
};

const handleBackspace = async (i: number, event: KeyboardEvent) => {
  /* Inteligent erasing that goes to previous field after deletion */

  event.preventDefault(); // avoid a race condition with the focus below
  otp.value[i] = ""; // deletion
  if (i > 0) {
    uInputComponents.value[i - 1].input.focus();
  }
};
</script>