From 5a8bc989664e35b97b8a82ffbc240e37330de8f6 Mon Sep 17 00:00:00 2001 From: Hossein Nasiri Date: Sat, 11 May 2024 12:17:08 +0330 Subject: [PATCH] feat: Add focus prev and next element and clear all cells with meta keys --- src/pin-input-cell/pin-input-cell.ts | 69 ++++++++++++++++++++++++++-- src/pin-input-cell/types.ts | 4 +- src/pin-input-cell/util.ts | 25 ++++++++-- src/pin-input/pin-input.ts | 49 +++++++++++++++++++- 4 files changed, 138 insertions(+), 9 deletions(-) diff --git a/src/pin-input-cell/pin-input-cell.ts b/src/pin-input-cell/pin-input-cell.ts index 6babd95c..0351d466 100644 --- a/src/pin-input-cell/pin-input-cell.ts +++ b/src/pin-input-cell/pin-input-cell.ts @@ -3,7 +3,9 @@ import { property, query } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { englishToPersian, - isDeletionKey, + isArrowKeyPressed, + isDeletionKeyPressed, + isDeletionKeyWithCtrlOrMetaPressed, isValidDigit, persianToEnglish, } from './util'; @@ -75,6 +77,34 @@ export class PinInputCell extends LitElement { this.dispatchEvent(event); } + private async emitDeletionWithMetaKeys() { + await this.updateComplete; + const event = new CustomEvent('clear-all', { + bubbles: true, + composed: false, + detail: { + cell: this, + index: this.index, + value: this.value, + } as ValueChangedEventParams, + }); + this.dispatchEvent(event); + } + + private async emitArrowKeyPressed(key: 'ArrowLeft' | 'ArrowRight') { + await this.updateComplete; + const event = new CustomEvent('arrow-key-pressed', { + bubbles: true, + composed: false, + detail: { + cell: this, + index: this.index, + value: key === 'ArrowLeft' ? 'left' : 'right', + } as ValueChangedEventParams<'left' | 'right'>, + }); + this.dispatchEvent(event); + } + private async emitOverflowedValue(value: string) { await this.updateComplete; const event = new CustomEvent('overflow-value', { @@ -112,13 +142,33 @@ export class PinInputCell extends LitElement { } private async validatePressedKey(event: KeyboardEvent) { - if (event.key === 'Backspace' && this.value === '') { + if ( + isDeletionKeyWithCtrlOrMetaPressed({ + input: event.key, + metaKey: event.metaKey, + ctrlKey: event.ctrlKey, + }) + ) { + this.value = ''; + event.preventDefault(); + await this.handleDeletionWithMetaKeys(); + return; + } + + if (isDeletionKeyPressed(event.key) && this.value === '') { + this.value = ''; event.preventDefault(); await this.handleEmptyCellBackspace(); return; } - if (isValidDigit(event.key) || isDeletionKey(event.key)) { + if (isArrowKeyPressed(event.key)) { + event.preventDefault(); + await this.handleArrowKeyPressed(event.key); + return; + } + + if (isValidDigit(event.key) || isDeletionKeyPressed(event.key)) { return true; } @@ -165,6 +215,13 @@ export class PinInputCell extends LitElement { await this.emitValueCleared(); } + private async handleDeletionWithMetaKeys() { + await this.emitDeletionWithMetaKeys(); + } + private async handleArrowKeyPressed(key: 'ArrowLeft' | 'ArrowRight') { + await this.emitArrowKeyPressed(key); + } + private handleFocus(e: FocusEvent) { const _target = e.target as HTMLInputElement; if (_target.value?.length > 0) { @@ -181,9 +238,15 @@ export class PinInputCell extends LitElement { async setValue(value: string) { if (value.length === 1 && this.value === '') { await this.updateInputValue(value); + await this.updateComplete; } } + async clearValue() { + await this.updateInputValue(''); + await this.updateComplete; + } + render() { return html` = { cell: PinInputCell; index: number; - value: string; + value: T; }; diff --git a/src/pin-input-cell/util.ts b/src/pin-input-cell/util.ts index 6d0af293..ff883953 100644 --- a/src/pin-input-cell/util.ts +++ b/src/pin-input-cell/util.ts @@ -44,8 +44,27 @@ export function isValidDigit(input: string): boolean { return false; } -const deletionKeys = ['Meta', 'Delete', 'Backspace']; +export function isArrowKeyPressed( + input: string, +): input is 'ArrowLeft' | 'ArrowRight' { + return ['ArrowLeft', 'ArrowRight'].includes(input); +} + +export function isDeletionKeyPressed(input: string): boolean { + return ['Backspace', 'Delete'].includes(input); +} -export function isDeletionKey(key: string): boolean { - return deletionKeys.includes(key); +export function isDeletionKeyWithCtrlOrMetaPressed({ + input, + metaKey = false, + ctrlKey = false, +}: { + input: string; + metaKey: boolean; + ctrlKey: boolean; +}) { + if (isDeletionKeyPressed(input) && (metaKey || ctrlKey)) { + return true; + } + return false; } diff --git a/src/pin-input/pin-input.ts b/src/pin-input/pin-input.ts index f2a97908..770245b5 100644 --- a/src/pin-input/pin-input.ts +++ b/src/pin-input/pin-input.ts @@ -68,6 +68,43 @@ export class PinInput extends LitElement { await this.fillCells(overflowedText, cellIndex + 1); } + private async handleClearPrevCells( + event: CustomEvent, + ) { + await this.updateComplete; + const currentIndex = event.detail.index; + + const isNotFirstItem = + currentIndex > 0 && this.checkIndexIsInRange(currentIndex); + + if (isNotFirstItem) { + await this.clearCellsUntil(currentIndex); + this._cells?.[0].focus(); + } + } + + private async handleArrowKeyPressed( + event: CustomEvent>, + ) { + await this.updateComplete; + const currentIndex = event.detail.index; + + const shouldPrevItemFocus = + event.detail.value === 'left' && + this.checkIndexIsInRange(currentIndex) && + !this.checkIndexIsFirst(currentIndex); + + const shouldNextItemFocus = + event.detail.value === 'right' && + this.checkIndexIsInRange(currentIndex) && + !this.checkIndexIsLast(currentIndex); + + if (shouldPrevItemFocus) { + this.focusPrevElementByIndex(currentIndex); + } else if (shouldNextItemFocus) { + this.focusNextElementByIndex(currentIndex); + } + } private async fillCells(value: string, startingAt: number = 0) { if (startingAt <= this.lastCellIndex) { @@ -75,7 +112,6 @@ export class PinInput extends LitElement { for (const char of value.split('')) { const pos = index + startingAt; await this._cells[pos].setValue(char); - await this._cells[pos].updateComplete; index++; } } @@ -115,6 +151,12 @@ export class PinInput extends LitElement { } } + private async clearCellsUntil(index: number) { + for (let i = 0; i <= index; i++) { + await this._cells?.[i].clearValue(); + } + } + private focusPrevElementByIndex(current: number) { const nextIndex = current - 1; if (this.checkIndexIsInRange(current) && !this.checkIndexIsFirst(current)) { @@ -164,6 +206,11 @@ export class PinInput extends LitElement { this.handleCellCleared(e)} @overflow-value=${(e: CustomEvent) => this.handleOverflowedCell(e)} + @clear-all=${(e: CustomEvent) => + this.handleClearPrevCells(e)} + @arrow-key-pressed=${( + e: CustomEvent>, + ) => this.handleArrowKeyPressed(e)} ?auto-focus=${this.isFirstCellShouldAutoFocus(index)} .size=${this.size} >`;