inputOtp function

BloomNode inputOtp({
  1. required int length,
  2. required String value,
  3. required void onChange(
    1. String value
    ),
  4. bool disabled = false,
  5. bool hasError = false,
  6. String extraClassName = '',
})

Controlled one-time password (OTP) / PIN input primitive.

Implementation

BloomNode inputOtp({
  required int length,
  required String value,
  required void Function(String value) onChange,
  bool disabled = false,
  bool hasError = false,
  String extraClassName = '',
}) {
  final chars = List.generate(length, (i) {
    if (i < value.length) return value[i];
    return '';
  });

  return Div(
    attrs: const {'data-slot': 'input-otp'},
    className: cn([
      'inline-flex items-center gap-1.5 select-none',
      extraClassName,
    ]),
    children: List.generate(length, (index) {
      final char = chars[index];

      return El(
        'input',
        attrs: {
          'type': 'text',
          'inputmode': 'numeric',
          'maxlength': '1',
          'value': char,
          'data-slot': 'input-otp-slot',
          if (disabled) 'disabled': 'true',
          if (hasError) 'aria-invalid': 'true',
        },
        className: cn([
          'w-9 h-10 text-center font-mono font-semibold text-sm rounded-[var(--radius-md)] '
          'bg-[var(--card)] text-[var(--text)] border border-[var(--border)] '
          'focus:outline-none focus:ring-2 focus:ring-[var(--ring)] focus:border-[var(--ring)] '
          'transition-all disabled:opacity-50 disabled:cursor-not-allowed',
          hasError ? 'border-[var(--destructive)] focus:ring-[var(--destructive)]' : '',
        ]),
        on: {
          'input': (BloomEvent e) {
            if (disabled) return;
            final inVal = e.value ?? '';
            final nextChars = List<String>.from(chars);
            if (inVal.isNotEmpty) {
              nextChars[index] = inVal[inVal.length - 1];
            } else {
              nextChars[index] = '';
            }
            onChange(nextChars.join());
          },
          'keydown': (BloomEvent e) {
            if (disabled) return;
            if (e.key == 'Backspace' && char.isEmpty && index > 0) {
              final nextChars = List<String>.from(chars);
              nextChars[index - 1] = '';
              onChange(nextChars.join());
            }
          },
        },
      );
    }),
  );
}