diff --git a/.changeset/otp-input-delete-key.md b/.changeset/otp-input-delete-key.md new file mode 100644 index 000000000..ad5613ac2 --- /dev/null +++ b/.changeset/otp-input-delete-key.md @@ -0,0 +1,5 @@ +--- +"@tabler/core": patch +--- + +Fixed the Delete key in `.otp` moving the caret to the last slot; it now removes the character under the caret and keeps the caret in place, like Backspace. diff --git a/core/js/src/otp-input.ts b/core/js/src/otp-input.ts index 41e257c16..35ee4d6e5 100644 --- a/core/js/src/otp-input.ts +++ b/core/js/src/otp-input.ts @@ -292,7 +292,7 @@ class OtpInput extends BaseComponent { this._afterValueChange() } - // Intercepts single-character typing and backspace so each slot is + // Intercepts single-character typing, Backspace and Delete so each slot is // overwritten in place rather than inserting and shifting the value. // Anything else (paste, autofill, IME) falls through to `_handleInput`. _handleBeforeInput(event: InputEvent): void { @@ -334,6 +334,26 @@ class OtpInput extends BaseComponent { } this._afterValueChange() + return + } + + if (inputType === 'deleteContentForward') { + event.preventDefault() + + const start = this._input.selectionStart ?? 0 + const end = this._input.selectionEnd ?? start + const chars = [...this._input.value] + + if (end > start) { + chars.splice(start, end - start) + this._input.value = chars.join('') + } else if (start < chars.length) { + chars.splice(start, 1) + this._input.value = chars.join('') + } + + this._selectSlot(start) + this._afterValueChange() } } diff --git a/core/js/tests/unit/otp-input.spec.ts b/core/js/tests/unit/otp-input.spec.ts index cc5ac0995..0bef55516 100644 --- a/core/js/tests/unit/otp-input.spec.ts +++ b/core/js/tests/unit/otp-input.spec.ts @@ -210,6 +210,33 @@ describe('OtpInput', () => { expect(input().value).toBe('') }) + it('should clear a selected slot on delete without shifting the caret', () => { + const instance = new OtpInput(otp()) + instance.setValue('123456') + input().setSelectionRange(3, 4) + beforeInput(input(), { inputType: 'deleteContentForward' }) + expect(input().value).toBe('12356') + expect(input().selectionStart).toBe(3) + }) + + it('should delete the next character on delete with a collapsed caret', () => { + const instance = new OtpInput(otp()) + instance.setValue('123') + input().setSelectionRange(1, 1) + beforeInput(input(), { inputType: 'deleteContentForward' }) + expect(input().value).toBe('13') + expect(input().selectionStart).toBe(1) + }) + + it('should do nothing on delete at the end of the value', () => { + const instance = new OtpInput(otp()) + instance.setValue('123') + input().setSelectionRange(3, 3) + const event = beforeInput(input(), { inputType: 'deleteContentForward' }) + expect(event.defaultPrevented).toBe(true) + expect(input().value).toBe('123') + }) + it('should select the first empty slot on keyboard focus', () => { const instance = new OtpInput(otp()) instance.setValue('12')