@@ -8,7 +8,7 @@ import styles from './otpinput.module.scss'
88interface Props extends OTPInputProps {}
99
1010const {
11- name,
11+ name = ' otp ' ,
1212 disabled,
1313 length = 6 ,
1414 groupLength = 0 ,
@@ -24,7 +24,7 @@ const classes = [
2424 className
2525]
2626
27- const inputPlaceholders = Array .from ({ length }, (_ , i ) => i + 1 )
27+ const inputs = Array .from ({ length }, (_ , i ) => i + 1 )
2828 .reduce <(number | string )[]>((acc , num , i ) =>
2929 groupLength > 0 && i % groupLength === 0 && i !== 0
3030 ? [... acc , separator , num ]
@@ -35,59 +35,157 @@ const inputPlaceholders = Array.from({ length }, (_, i) => i + 1)
3535<div class:list ={ classes } >
3636 { label && (
3737 <label
38- for = { name }
38+ for = { ` ${ name }-0 ` }
3939 class = { styles .label }
4040 set :html = { label }
4141 />
4242 )}
4343
44- <div class ={ styles [' input-wrapper' ]} >
45- <Input
46- name ={ name || ' otp' }
47- data-id =" w-input-otp"
48- disabled ={ disabled }
49- maxlength ={ length }
50- required ={ true }
51- {... rest }
52- />
53-
54- <div class ={ styles .placeholders } >
55- { inputPlaceholders .map ((placeholder , index ) => (
56- <div
57- class = { typeof placeholder === ' string' ? styles .separator : styles .placeholder }
58- data-active = { index === 0 ? true : undefined }
59- data-separator = { typeof placeholder === ' string' ? true : undefined }
60- >
61- { typeof placeholder === ' string' ? placeholder : ' ' }
62- </div >
63- ))}
64- </div >
44+ <div class ={ styles [' input-wrapper' ]} data-length ={ length } >
45+ { inputs .map ((input , index ) =>
46+ typeof input === ' string' ? (
47+ <div class = { styles .separator } >{ input } </div >
48+ ) : (
49+ <Input
50+ id = { ` ${name }-${index } ` }
51+ class = { styles .input }
52+ type = " text"
53+ maxlength = " 1"
54+ disabled = { disabled }
55+ inputmode = " numeric"
56+ autocomplete = " one-time-code"
57+ data-id = " w-input-otp"
58+ data-index = { input }
59+ aria-label = { ` OTP digit ${input + 1 } ` }
60+ { ... rest }
61+ />
62+ )
63+ )}
6564 </div >
6665
67- { subText && (
68- <div class = { styles .subtext } set :html = { subText } />
69- )}
66+ { subText && <div class = { styles .subtext } >{ subText } </div >}
7067</div >
7168
7269<script >
7370 import { on } from '../../utils/DOMUtils'
71+ import { dispatch } from '../../utils/event'
72+
73+ const focus = (direction: 'next' | 'prev', wrapper: HTMLElement | null, clear?: boolean) => {
74+ const index = Number(wrapper?.dataset.active)
75+ const nextIndex = direction === 'next' ? index + 1 : index - 1
76+
77+ const input = wrapper?.querySelector(`[data-index="${nextIndex}"]`)
78+
79+ if (input instanceof HTMLInputElement) {
80+ input.focus()
81+
82+ if (clear) {
83+ input.value = ''
84+ }
85+ }
86+ }
7487
7588 const addEventListeners = () => {
76- on('[data-id="w-input-otp"]', 'input ', (event: Event ) => {
89+ on('[data-id="w-input-otp"]', 'keydown ', (event: KeyboardEvent ) => {
7790 const target = event.target as HTMLInputElement
78- const value = target.value
79- const placeholders = Array.from(target.nextElementSibling!.children)
80- .filter(child => !(child as HTMLDivElement).dataset.separator)
8191
92+ if (event.key === 'Backspace' || event.key === 'Delete') {
93+ if (!target.value) {
94+ focus('prev', target.parentElement, true)
95+ }
96+ }
97+
98+ if (event.key === 'ArrowLeft') {
99+ focus('prev', target.parentElement)
100+ }
101+
102+ if (event.key === 'ArrowRight') {
103+ focus('next', target.parentElement)
104+ }
105+ }, true)
106+
107+ on('[data-id="w-input-otp"]', 'input', (event: Event) => {
108+ const target = event.target
109+
110+ if (!(target instanceof HTMLInputElement)) {
111+ return
112+ }
113+
114+ const index = Number(target.dataset.index)
115+ const emptyIndex = Array.from(target.parentElement?.querySelectorAll('input') || [])
116+ .findIndex(element => !element.value) + 1
117+
118+ if (emptyIndex !== 0 && emptyIndex < index) {
119+ const emptyElement = target.parentElement?.querySelector(`[data-index="${emptyIndex}"]`)
120+ const nextFocusElement = target.parentElement?.querySelector(`[data-index="${emptyIndex + 1}"]`)
121+
122+ if (emptyElement instanceof HTMLInputElement) {
123+ emptyElement.value = target.value
124+ }
125+
126+ if (nextFocusElement instanceof HTMLInputElement) {
127+ nextFocusElement.focus()
128+ }
129+
130+ target.value = ''
131+
132+ return
133+ }
134+
135+ if (target.value) {
136+ focus('next', target.parentElement)
137+ }
138+ }, true)
139+
140+ on('[data-id="w-input-otp"]', 'keyup', (event: Event) => {
141+ const target = event.target
142+ const container = target instanceof HTMLInputElement ? target.parentElement : null
143+
144+ if (container) {
145+ const value = Array.from(container.querySelectorAll('input') || [])
146+ .map(input => input.value)
147+ .join('')
148+
149+ dispatch('otpOnChange', value)
150+ }
151+ }, true)
152+
153+ on('[data-id="w-input-otp"]', 'paste', (event: ClipboardEvent) => {
154+ event.preventDefault()
155+
156+ const target = event.target
157+ const container = target instanceof HTMLInputElement ? target.parentElement : null
158+
159+ if (container) {
160+ const inputLength = Number(container.dataset.length)
161+ const paste = event.clipboardData?.getData('text') ?? ''
162+ const nextIndex = Math.min(paste.length + 1, inputLength)
163+ const focusInput = container.querySelector(`[data-index="${nextIndex}"]`)
164+
165+ if (focusInput instanceof HTMLInputElement) {
166+ focusInput.focus()
167+ }
168+
169+ paste.split('').slice(0, inputLength).forEach((char, i) => {
170+ const input = container.querySelector(`[data-index="${i + 1}"]`)
171+
172+ if (input instanceof HTMLInputElement) {
173+ input.value = char
174+ }
175+ })
176+ }
177+ }, true)
178+
179+ on('[data-id="w-input-otp"]', 'focus', (event: Event) => {
180+ const target = event.target
82181
83- placeholders.forEach((placeholder, index) => {
84- const placeholderElement = placeholder as HTMLDivElement
182+ if (target instanceof HTMLInputElement) {
183+ if (target.parentElement) {
184+ target.parentElement.dataset.active = target.dataset.index
185+ }
85186
86- placeholderElement.innerText = value[index] || ''
87- placeholderElement.dataset.active = value.length === index
88- ? 'true'
89- : 'false'
90- })
187+ setTimeout(() => target.select(), 0)
188+ }
91189 }, true)
92190 }
93191
0 commit comments