All files / components/base BaseInput.vue

100% Statements 49/49
100% Branches 16/16
100% Functions 3/3
100% Lines 49/49

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100  1x 1x 1x 1x 1x   1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 31x 31x 2x 1x 1x                                                                       1x           1x     1x 1x   1x 34x 34x 34x 34x 34x   34x 3x 31x   34x 1x 33x   34x 1x   1x 1x 1x 1x    
<template>
  <div class="w-full">
    <label
      v-if="label"
      :for="inputId"
      class="block text-sm font-medium text-secondary-700 mb-1"
    >
      {{ label }}
      <span v-if="required" class="text-error-500">*</span>
    </label>
    <input
      :id="inputId"
      :type="type"
      :value="modelValue"
      :placeholder="placeholder"
      :required="required"
      :disabled="disabled"
      :class="inputClasses"
      @input="handleInput"
      @blur="emit('blur', $event)"
      @focus="emit('focus', $event)"
    />
    <p v-if="error" class="mt-1 text-sm text-error-600">
      {{ error }}
    </p>
    <p v-else-if="hint" class="mt-1 text-sm text-secondary-500">
      {{ hint }}
    </p>
  </div>
</template>
 
<script setup lang="ts">
/**
 * BaseInput
 * @description Reusable input component with label, error, and hint support
 */
 
interface Props {
  /** v-model binding value */
  modelValue: string
  /** Input type */
  type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'date' | 'time' | 'datetime-local'
  /** Input label */
  label?: string
  /** Placeholder text */
  placeholder?: string
  /** Required field indicator */
  required?: boolean
  /** Disabled state */
  disabled?: boolean
  /** Error message */
  error?: string
  /** Hint text shown below input */
  hint?: string
  /** Custom id (auto-generated if not provided) */
  id?: string
}
 
interface Emits {
  (e: 'update:modelValue', value: string): void
  (e: 'blur', event: FocusEvent): void
  (e: 'focus', event: FocusEvent): void
}
 
const props = withDefaults(defineProps<Props>(), {
  type: 'text',
  required: false,
  disabled: false
})
 
const emit = defineEmits<Emits>()
 
// Use Vue's SSR-safe useId() for consistent hydration
const generatedId = useId()
const inputId = computed(() => props.id || generatedId)
 
const inputClasses = computed(() => {
  const base = [
    'w-full px-3 py-2 rounded-lg border',
    'transition-colors duration-200',
    'focus:outline-none focus:ring-2 focus:ring-offset-0'
  ]
 
  const states = props.error
    ? ['border-error-300', 'focus:border-error-500', 'focus:ring-error-500']
    : ['border-secondary-300', 'focus:border-primary-500', 'focus:ring-primary-500']
 
  const disabled = props.disabled
    ? 'bg-secondary-100 text-secondary-500 cursor-not-allowed'
    : 'bg-white text-secondary-900'
 
  return [...base, ...states, disabled]
})
 
function handleInput(event: Event) {
  const target = event.target as HTMLInputElement
  emit('update:modelValue', target.value)
}
</script>