Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: Turn pin-input-cells events to event delegation.
Browse files Browse the repository at this point in the history
hossein-nas committed Aug 19, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent a3a96a8 commit 32aa7c5
Showing 14 changed files with 7,268 additions and 6,305 deletions.
12,980 changes: 7,121 additions & 5,859 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/icon-button/icon-button.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { queryAssignedElements } from 'lit/decorators.js';
import { BaseButton } from '../base-button';
import { PropertyValues } from 'lit';
import { TapIcon } from '../icon';
import { Icon } from '@tapsioss/icons/dist/icon/icon';

export class IconButton extends BaseButton {
@queryAssignedElements() private icon!: TapIcon[];
@queryAssignedElements() private icon!: Icon[];

protected updated(_changedProperties: PropertyValues) {
super.updated(_changedProperties);
5 changes: 0 additions & 5 deletions src/pin-input-cell/constants.ts

This file was deleted.

122 changes: 0 additions & 122 deletions src/pin-input-cell/events.ts

This file was deleted.

2 changes: 0 additions & 2 deletions src/pin-input-cell/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { customElement } from 'lit/decorators.js';
import { PinInputCell } from './pin-input-cell.js';
import styles from './pin-input-cell.style.js';
export * from './events.js';
export * from './constants.js';

@customElement('tap-pin-input-cell')
export class TapPinInputCell extends PinInputCell {
219 changes: 13 additions & 206 deletions src/pin-input-cell/pin-input-cell.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,6 @@
import { html, LitElement, PropertyValues } from 'lit';
import { property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import {
isArrowKeyPressed,
isDeletionKeyPressed,
isDeletionKeyWithCtrlOrMetaPressed,
isValidDigit,
persianToEnglish,
} from './util';
import {
PinInputCellArrowKeyPressed,
PinInputCellCleared,
PinInputCellClearedAll,
PinInputCellFilled,
PinInputCellOverflowValue,
} from './events';

export class PinInputCell extends LitElement {
@query('.cell') _cell!: HTMLInputElement;
@@ -35,101 +21,6 @@ export class PinInputCell extends LitElement {

@property({ type: Number }) index: number = null!;

protected updated(changed: PropertyValues) {
if (changed.has('value') && !Number.isNaN(this.value)) {
//
}
}

private async updateInputValue(newValue: string) {
this.value = persianToEnglish(newValue);
this._cell.value = newValue;

if (newValue.length) {
await this.emitValueChanged();
}

if (newValue.length === 0) {
await this.emitValueCleared();
}
}

private async emitValueChanged() {
await this.updateComplete;
const event = new PinInputCellFilled('PinInputCell filled', {
cell: this,
index: this.index,
value: this.value,
});

this.dispatchEvent(event);
}

private async emitValueCleared() {
await this.updateComplete;
const event = new PinInputCellCleared('PinInputCell cleared', {
cell: this,
index: this.index,
value: this.value,
});

this.dispatchEvent(event);
}

private async emitDeletionWithMetaKeys() {
await this.updateComplete;
const event = new PinInputCellClearedAll('PinInputCell cleared all', {
cell: this,
index: this.index,
value: this.value,
});

this.dispatchEvent(event);
}

private async emitArrowKeyPressed(key: 'ArrowLeft' | 'ArrowRight') {
await this.updateComplete;

const event = new PinInputCellArrowKeyPressed<'left' | 'right'>(
'PinInputCell arrow key pressed',
{
cell: this,
index: this.index,
value: key === 'ArrowLeft' ? 'left' : 'right',
},
);

this.dispatchEvent(event);
}

private async emitOverflowedValue(value: string) {
await this.updateComplete;
const event = new PinInputCellOverflowValue(
'PinInputCell Overflowed value',
{
cell: this,
index: this.index,
value: value,
},
);

this.dispatchEvent(event);
}

private async handleInput(event: InputEvent) {
const inputValue = (event.target as HTMLInputElement).value.replace(
/[^\d۰-۹]/g,
'',
);

if (typeof inputValue === 'string' && inputValue.length >= 1) {
const lastCharacterIndex = inputValue.length - 1;
await this.updateInputValue(inputValue[lastCharacterIndex]);
} else {
await this.updateInputValue('');
}
}

protected firstUpdated(_changedProperties: PropertyValues) {
super.firstUpdated(_changedProperties);

@@ -138,123 +29,39 @@ export class PinInputCell extends LitElement {
}
}

private async validatePressedKey(event: KeyboardEvent) {
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 (isArrowKeyPressed(event.key)) {
event.preventDefault();
await this.handleArrowKeyPressed(event.key);
return;
}

if (isValidDigit(event.key) || isDeletionKeyPressed(event.key)) {
return true;
}

if (
[8, 9, 13, 27, 46, 110, 190].includes(event.keyCode) ||
// Allow Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+X
(event.keyCode == 65 &&
(event.ctrlKey === true || event.metaKey === true)) ||
(event.keyCode == 67 &&
(event.ctrlKey === true || event.metaKey === true)) ||
(event.keyCode == 86 &&
(event.ctrlKey === true || event.metaKey === true)) ||
(event.keyCode == 88 &&
(event.ctrlKey === true || event.metaKey === true))
) {
return;
}

event.preventDefault();
}

private async handlePaste(e: ClipboardEvent) {
const text: string = e.clipboardData?.getData('text/plain') || '';
if (text && !this.isValidNumericText(text)) {
e.preventDefault();
}

if (text?.length > 1) {
await this.updateInputValue(text[0]);
await this.emitOverflowedValue(text.slice(1));
e.preventDefault();
return;
}
}

private isValidNumericText(input: string) {
if (typeof input === 'string' && input.length > 0) {
const numericRegex = /^[0-9۰-۹]+$/;
return numericRegex.test(input);
}
}

private async handleEmptyCellBackspace() {
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) {
_target.select();
}
}

override focus() {
if (this._cell) {
this._cell.focus?.();
}
}

private handleInputFocus(e: FocusEvent) {
if (this.value) {
this._cell.select();
}
}

async setValue(value: string) {
if (value.length === 1 && this.value === '') {
await this.updateInputValue(value);
if (value.length === 1) {
this.value = value;
this._cell.value = value;
await this.updateComplete;
}
}

async clearValue() {
await this.updateInputValue('');
this.value = '';
this._cell.value = '';
await this.updateComplete;
}

render() {
return html`
<input
?disabled=${this.disabled}
role="pin-input-cell"
role="text"
aria-label=${this.label}
@input=${(e: InputEvent) => this.handleInput(e)}
@paste=${(e: ClipboardEvent) => this.handlePaste(e)}
@focus=${(e: FocusEvent) => this.handleFocus(e)}
@keydown=${(e: KeyboardEvent) => this.validatePressedKey(e)}
@focus=${(e: FocusEvent) => this.handleInputFocus(e)}
class="cell ${classMap({
'cell-sm': this.size === 'small',
'cell-md': this.size === 'medium',
7 changes: 0 additions & 7 deletions src/pin-input-cell/types.ts

This file was deleted.

58 changes: 2 additions & 56 deletions src/pin-input-cell/util.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,3 @@
export function englishToPersian(input: string): string {
const localInput = `${input}`;
const englishDigits = '0123456789';
const persianDigits = '۰۱۲۳۴۵۶۷۸۹';
let output = '';

for (const char of localInput) {
const index = englishDigits.indexOf(char);
if (index !== -1) {
output += persianDigits[index];
} else {
output += char;
}
}

return output;
}

export function persianToEnglish(input: string): string {
const localInput = `${input}`;
const persianDigits = '۰۱۲۳۴۵۶۷۸۹';
const englishDigits = '0123456789';
let output = '';

for (const char of localInput) {
const index = persianDigits.indexOf(char);
if (index !== -1) {
output += englishDigits[index];
} else {
output += char;
}
}

return output;
}

export function isValidDigit(input: string): boolean {
const englishDigits = '0123456789۰۱۲۳۴۵۶۷۸۹';

if (typeof input === 'string' && input.length === 1) {
return englishDigits.indexOf(input) !== -1;
}

return false;
}

export function isArrowKeyPressed(
input: string,
): input is 'ArrowLeft' | 'ArrowRight' {
@@ -54,16 +8,8 @@ export function isDeletionKeyPressed(input: string): boolean {
return ['Backspace', 'Delete'].includes(input);
}

export function isDeletionKeyWithCtrlOrMetaPressed({
input,
metaKey = false,
ctrlKey = false,
}: {
input: string;
metaKey: boolean;
ctrlKey: boolean;
}) {
if (isDeletionKeyPressed(input) && (metaKey || ctrlKey)) {
export function isDeletionKeyWithCtrlOrMetaPressed(event: KeyboardEvent) {
if (isDeletionKeyPressed(event.key) && (event.metaKey || event.ctrlKey)) {
return true;
}
return false;
2 changes: 1 addition & 1 deletion src/pin-input/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const PIN_INPUT_FILLED_TYPE = 'input-filled';
export const PIN_INPUT_FILLED_TYPE = 'tap-pin-input-filled';
2 changes: 1 addition & 1 deletion src/pin-input/events.ts
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ export class PinInputFilled extends Event {
) {
const _eventInitDict = {
bubbles: true,
composed: false,
composed: true,
...eventInitDict,
};

1 change: 1 addition & 0 deletions src/pin-input/index.ts
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ import { PinInput } from './pin-input.js';
import styles from './pin-input.style.js';
export * from './events.js';
export * from './constants.js';
export * from './types.js';

@customElement('tap-pin-input')
export class TapPinInput extends PinInput {
4 changes: 2 additions & 2 deletions src/pin-input/pin-input.stories.ts
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ const Template: Story<ArgTypes> = (/*{}: ArgTypes*/) => html`
<tap-pin-input
title="عنوان"
description="توضیحات"
@input-filled=${(e: Error) => console.log(e)}
@tap-pin-input-filled=${(e: Error) => console.log(e)}
></tap-pin-input>
`;

@@ -41,7 +41,7 @@ const TemplateWithForm: Story<ArgTypes> = (/*{}: ArgTypes*/) => html`
<tap-pin-input
title="عنوان"
description="توضیحات"
@input-filled=${(e: Error) => console.log(e)}
@tap-pin-input-filled=${(e: Error) => console.log(e)}
></tap-pin-input>
<button type="submit">Submit</button>
</form>
154 changes: 112 additions & 42 deletions src/pin-input/pin-input.ts
Original file line number Diff line number Diff line change
@@ -3,15 +3,18 @@ import { property, queryAll } from 'lit/decorators.js';
import { range } from 'lit/directives/range.js';
import { repeat } from 'lit/directives/repeat.js';
import '../pin-input-cell';
import { PinInputCell } from '../pin-input-cell/pin-input-cell';
import { PinInputCell } from '../pin-input-cell/pin-input-cell.js';
import { PinInputFilled } from './events.js';
import {
PinInputCellArrowKeyPressed,
PinInputCellCleared,
PinInputCellClearedAll,
PinInputCellFilled,
PinInputCellOverflowValue,
} from '../pin-input-cell/events';
import { PinInputFilled } from './events';
isArrowKeyPressed,
isDeletionKeyPressed,
isDeletionKeyWithCtrlOrMetaPressed,
} from '../pin-input-cell/util.js';
import {
HandleArrowKeyPressedArgs,
HandleClearPrevCellsArgs,
HandleOverflowedCellArgs,
} from './types';

export class PinInput extends LitElement {
private internals_: ElementInternals;
@@ -29,12 +32,16 @@ export class PinInput extends LitElement {
@property({ type: Boolean, reflect: true, attribute: 'has-error' })
hasError = false;

@property({ type: Boolean, attribute: 'only-digits' })
onlyDigits = false;

@property({ type: Boolean, attribute: 'auto-focus' })
autoFocus: boolean = true;

@property({ reflect: true, type: String }) _value = '';

@property() label = '';
@property({ reflect: true }) name = '';

@property() title = '';

@@ -52,24 +59,12 @@ export class PinInput extends LitElement {
return this.autoFocus && index === 0;
}

private handleCellFilled(event: PinInputCellFilled) {
this.focusNextElementByIndex(event.details.index);
this.handleCellsFilled();
}

private handleCellCleared(event: PinInputCellCleared) {
this.focusPrevElementByIndex(event.details.index);
}

private handleCellsFilled() {
if (this.inputValue) {
this.emitPinInputFilled();
}
}

private async handleOverflowedCell(event: PinInputCellOverflowValue) {
let overflowedText = event.details.value;
const cellIndex = event.details.index;
private async handleOverflowedCell({
text,
index,
}: HandleOverflowedCellArgs) {
let overflowedText = text;
const cellIndex = index;
const isLongerThanRemainingCells =
overflowedText.length > this.lastCellIndex - cellIndex;

@@ -80,9 +75,9 @@ export class PinInput extends LitElement {
await this.fillCells(overflowedText, cellIndex + 1);
}

private async handleClearPrevCells(event: PinInputCellClearedAll) {
private async handleClearPrevCells({ index }: HandleClearPrevCellsArgs) {
await this.updateComplete;
const currentIndex = event.details.index;
const currentIndex = index;

const isNotFirstItem =
currentIndex > 0 && this.checkIndexIsInRange(currentIndex);
@@ -93,17 +88,19 @@ export class PinInput extends LitElement {
}
}

private async handleArrowKeyPressed(event: PinInputCellArrowKeyPressed) {
private async handleArrowKeyPressed({
index: currentIndex,
arrowDirection,
}: HandleArrowKeyPressedArgs) {
await this.updateComplete;
const currentIndex = event.details.index;

const shouldPrevItemFocus =
event.details.value === 'left' &&
arrowDirection === 'left' &&
this.checkIndexIsInRange(currentIndex) &&
!this.checkIndexIsFirst(currentIndex);

const shouldNextItemFocus =
event.details.value === 'right' &&
arrowDirection === 'right' &&
this.checkIndexIsInRange(currentIndex) &&
!this.checkIndexIsLast(currentIndex);

@@ -122,6 +119,8 @@ export class PinInput extends LitElement {
await this._cells[pos].setValue(char);
index++;
}

this._cells[index].focus();
}
}

@@ -170,6 +169,12 @@ export class PinInput extends LitElement {
}
}

private handleCellsFilled() {
if (this.inputValue) {
this.emitPinInputFilled();
}
}

async formResetCallback() {
await this.fillCells('');
void this.emitPinInputFilled();
@@ -254,6 +259,78 @@ export class PinInput extends LitElement {
return nothing;
}

private async handleInput(event: InputEvent) {
const el = event.currentTarget as PinInputCell;
let inputValue = event.data;
if (this.onlyDigits) {
inputValue = event.data?.replace(/[^\d۰-۹]/g, '') ?? '';
}

if (typeof inputValue === 'string' && inputValue.length === 1) {
await el.setValue(inputValue);
this.focusNextElementByIndex(el.index);
} else {
await el.clearValue();
}

this.handleCellsFilled();
}

private async validatePressedKey(event: KeyboardEvent) {
const el = event.currentTarget as PinInputCell;

if (isDeletionKeyWithCtrlOrMetaPressed(event)) {
event.preventDefault();
await el.clearValue();
await this.handleClearPrevCells({ index: el.index });
event.stopPropagation();
return;
}

if (isDeletionKeyPressed(event.key)) {
event.preventDefault();
await el.clearValue();
this.focusPrevElementByIndex(el.index);
event.stopPropagation();
return;
}

if (isArrowKeyPressed(event.key)) {
event.preventDefault();
await this.handleArrowKeyPressed({
index: el.index,
arrowDirection: this.handleArrowDirection(event.key),
});
event.stopPropagation();
return;
}
}

handleArrowDirection(
eventKey: KeyboardEvent['key'],
): 'left' | 'right' | null {
if (eventKey === 'ArrowLeft') {
return 'left';
} else if (eventKey === 'ArrowRight') {
return 'right';
}

return null;
}

private async handlePaste(e: ClipboardEvent) {
const text: string = e.clipboardData?.getData('text/plain') || '';
const el = e.currentTarget as PinInputCell;

if (text?.length > 1) {
await el.setValue(text[0]);
await this.handleOverflowedCell({ text: text.slice(1), index: el.index });
e.preventDefault();
this.handleCellsFilled();
return;
}
}

private renderInputCells() {
if (typeof this.title === 'string' && this.title.length) {
return html`
@@ -267,16 +344,9 @@ export class PinInput extends LitElement {
index=${index}
?disabled=${this.disabled}
?has-error=${this.hasError}
@cell-filled=${(e: PinInputCellFilled) =>
this.handleCellFilled(e)}
@cell-cleared=${(e: PinInputCellCleared) =>
this.handleCellCleared(e)}
@overflow-value=${(e: PinInputCellOverflowValue) =>
this.handleOverflowedCell(e)}
@cell-cleared-all-with-meta-key=${(e: PinInputCellClearedAll) =>
this.handleClearPrevCells(e)}
@arrow-key-pressed=${(e: PinInputCellArrowKeyPressed) =>
this.handleArrowKeyPressed(e)}
@input=${(e: InputEvent) => this.handleInput(e)}
@paste=${(e: ClipboardEvent) => this.handlePaste(e)}
@keydown=${(e: KeyboardEvent) => this.validatePressedKey(e)}
?auto-focus=${this.isFirstCellShouldAutoFocus(index)}
.size=${this.size}
></tap-pin-input-cell>`;
13 changes: 13 additions & 0 deletions src/pin-input/types.ts
Original file line number Diff line number Diff line change
@@ -3,3 +3,16 @@ export type InputFilledEventParams = {
value: string;
displayValue: string;
};

export type HandleOverflowedCellArgs = {
text: string;
index: number;
};
export type HandleClearPrevCellsArgs = {
index: number;
};

export type HandleArrowKeyPressedArgs = {
index: number;
arrowDirection: 'left' | 'right' | null;
};

0 comments on commit 32aa7c5

Please sign in to comment.