Skip to content

Commit

Permalink
fix(code-input): handle mobile keyboard clipboard pasting (#1012)
Browse files Browse the repository at this point in the history
  • Loading branch information
Phil147 authored and GitHub Enterprise committed Aug 22, 2023
1 parent b1af230 commit bf095ee
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 117 deletions.
9 changes: 4 additions & 5 deletions projects/ng-aquila/src/code-input/code-input.component.html
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
<input
class="nx-code-input__field"
maxlength="1"
*ngFor="let key of _keyCode; index as i; trackBy: _trackByKeyCode"
[(ngModel)]="_keyCode[i]"
[ngModel]="_keyCode[i]"
[ngClass]="_inputGap(i)"
[attr.aria-label]="getAriaLabel(i)"
[attr.type]="type"
(input)="_selectNextInput($event)"
(paste)="_pasteClipboard($event)"
(input)="_handleInput($event)"
(blur)="_onBlur()"
(focus)="_setFocusState()"
(click)="_selectText($event)"
(mousedown)="_selectText($event)"
(keydown)="_keydownAction($event)"
[attr.tabindex]="tabindex"
[attr.disabled]="disabled ? '' : null"
autocomplete="one-time-code"
/>
126 changes: 60 additions & 66 deletions projects/ng-aquila/src/code-input/code-input.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DOWN_ARROW, RIGHT_ARROW, UP_ARROW } from '@angular/cdk/keycodes';
import { CONTROL, DOWN_ARROW, RIGHT_ARROW, UP_ARROW } from '@angular/cdk/keycodes';
import { ChangeDetectionStrategy, Component, Directive, Injectable, Type, ViewChild } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
Expand All @@ -20,6 +20,8 @@ abstract class CodeInputTest {

negative = false;
disabled = false;
tabindex = 0;
type = 'text';

onSubmit() {}
}
Expand All @@ -42,22 +44,15 @@ describe('NxCodeInputComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [NxCodeInputModule, FormsModule, ReactiveFormsModule],
declarations: [
CodeInputTest1,
CodeInputTest2,
CodeInputTest3,
NumberCodeInput,
ConfigurableCodeInput,
OnPushCodeInput,
OverrideDefaultLabelsCodeInput,
],
declarations: [CodeInputTest1, CodeInputTest2, CodeInputTest3, NumberCodeInput, ConfigurableCodeInput, OverrideDefaultLabelsCodeInput],
providers: [NxCodeInputIntl],
}).compileComponents();
}));

it('creates a 4 character input form', () => {
createTestComponent(CodeInputTest1);
expect(testInstance).toBeTruthy();
expect(testInstance.codeInputInstance._keyCode).toEqual(['', '', '', '']);
});

it('should have a codeLength of 4', () => {
Expand Down Expand Up @@ -89,6 +84,30 @@ describe('NxCodeInputComponent', () => {
expect(codeInputElement).toHaveClass('ng-invalid');
});

it('should set selection range on keydown paste', () => {
createTestComponent(CodeInputTest1);
inputElement.value = '1';
inputElement.focus();
expect(inputElement.selectionStart).toBe(1);
expect(inputElement.selectionEnd).toBe(1);
dispatchKeyboardEvent(inputElement, 'keydown', CONTROL);
fixture.detectChanges();
expect(inputElement.selectionStart).toBe(0);
expect(inputElement.selectionEnd).toBe(1);
});

it('should set selection range on mousedown', () => {
createTestComponent(CodeInputTest1);
inputElement.value = '1';
inputElement.focus();
expect(inputElement.selectionStart).toBe(1);
expect(inputElement.selectionEnd).toBe(1);
inputElement.dispatchEvent(new Event('mousedown'));
fixture.detectChanges();
expect(inputElement.selectionStart).toBe(0);
expect(inputElement.selectionEnd).toBe(1);
});

it('should select second input on right arrow', fakeAsync(() => {
createTestComponent(CodeInputTest1);
inputElement.focus();
Expand Down Expand Up @@ -129,45 +148,50 @@ describe('NxCodeInputComponent', () => {
expect(codeInputElement).toHaveClass('has-error');
}));

it('should paste', fakeAsync(() => {
it('should paste', () => {
createTestComponent(CodeInputTest1);
const data = new DataTransfer();
data.items.add('abcd', 'text/plain');
const clipboard = new ClipboardEvent('paste', {
clipboardData: data,
} as ClipboardEventInit);

inputElement.focus();
inputElement.dispatchEvent(clipboard);
inputElement.dispatchEvent(new InputEvent('input', { data: 'abcd' }));
fixture.detectChanges();
tick(1);
['A', 'B', 'C', 'D'].forEach((char, i) => {
const input = codeInputElement.querySelector(`input:nth-child(${i + 1})`) as HTMLInputElement;
expect(input.value).toBe(char);
});
}));
});

it('should ignore non-number characters on paste on number input', fakeAsync(() => {
createTestComponent(NumberCodeInput);
const data = new DataTransfer();
data.items.add('1a23', 'text/plain');
const clipboard = new ClipboardEvent('paste', {
clipboardData: data,
} as ClipboardEventInit);
it('should work on browser autofill', () => {
/**
* important: this test will not fail if you add maxlength="1" in the template again.
* on android chrome e.g. what will happen then that it will create two input events. first one with the whole pasted value
* and a second one only with the first character. we can't really simulate this behaviour in a test.
*/
createTestComponent(CodeInputTest1);
inputElement.focus();
fixture.detectChanges();

inputElement.dispatchEvent(new InputEvent('input', { data: '1234' }));
fixture.detectChanges();

['1', '2', '3', '4'].forEach((char, i) => {
const input = codeInputElement.querySelector(`input:nth-child(${i + 1})`) as HTMLInputElement;
expect(input.value).toBe(char);
});
});

it('should ignore non-number characters on paste on number input', () => {
createTestComponent(NumberCodeInput);
inputElement.focus();
inputElement.dispatchEvent(clipboard);
inputElement.dispatchEvent(new InputEvent('input', { data: '1a23' }));
fixture.detectChanges();
tick(1);

['1', '2', '3', ''].forEach((char, i) => {
const input = codeInputElement.querySelector(`input:nth-child(${i + 1})`) as HTMLInputElement;
expect(input.value).toBe(char);
});

// @ts-expect-error fix nullability
expect(testInstance.codeInputInstance._keyCode).toEqual(['1', '2', '3', undefined]);
}));
expect(testInstance.codeInputInstance._keyCode).toEqual(['1', '2', '3', '']);
});

it('should prevent default on down arrow press in an empty input', fakeAsync(() => {
createTestComponent(NumberCodeInput);
Expand Down Expand Up @@ -249,8 +273,8 @@ describe('NxCodeInputComponent', () => {
});

it('should set the passed tabindex', () => {
createTestComponent(CodeInputTest1);
testInstance.codeInputInstance.tabindex = 1;
createTestComponent(ConfigurableCodeInput);
testInstance.tabindex = 1;
fixture.detectChanges();
expect(codeInputElement.getAttribute('tabindex')).toBe('-1');

Expand All @@ -261,8 +285,8 @@ describe('NxCodeInputComponent', () => {
});

it('should change the input type', fakeAsync(() => {
createTestComponent(CodeInputTest3);
fixture.componentInstance.codeInputInstance.type = 'number';
createTestComponent(ConfigurableCodeInput);
fixture.componentInstance.type = 'number';
fixture.detectChanges();
const inputEl = fixture.nativeElement.querySelector('.nx-code-input__field') as HTMLInputElement;
expect(inputEl.getAttribute('type')).toBe('number');
Expand All @@ -289,18 +313,6 @@ describe('NxCodeInputComponent', () => {
expect(testInstance.codeInputInstance.negative).toBeFalse();
expect(codeInputElement).not.toHaveClass('is-negative');
});

it('should update on negative change (programmatic change)', () => {
createTestComponent(OnPushCodeInput);

testInstance.codeInputInstance.negative = true;
fixture.detectChanges();
expect(codeInputElement).toHaveClass('is-negative');

testInstance.codeInputInstance.negative = false;
fixture.detectChanges();
expect(codeInputElement).not.toHaveClass('is-negative');
});
});

describe('disabled', () => {
Expand Down Expand Up @@ -333,18 +345,6 @@ describe('NxCodeInputComponent', () => {
});
});

it('should update on disabled change (programmatic change)', () => {
createTestComponent(OnPushCodeInput);

testInstance.codeInputInstance.disabled = true;
fixture.detectChanges();
expect(codeInputElement).toHaveClass('is-disabled');

testInstance.codeInputInstance.disabled = false;
fixture.detectChanges();
expect(codeInputElement).not.toHaveClass('is-disabled');
});

it('should update disabled on formGroup update', () => {
createTestComponent(CodeInputTest1);
expect(testInstance.codeInputInstance.disabled).toBeFalse();
Expand Down Expand Up @@ -436,16 +436,10 @@ class CodeInputTest3 extends CodeInputTest {
class NumberCodeInput extends CodeInputTest {}

@Component({
template: `<nx-code-input [negative]="negative" [disabled]="disabled" [length]="4"> </nx-code-input>`,
template: `<nx-code-input [negative]="negative" [disabled]="disabled" [length]="4" [tabindex]="tabindex" [type]="type"> </nx-code-input>`,
})
class ConfigurableCodeInput extends CodeInputTest {}

@Component({
template: `<nx-code-input [negative]="negative" [length]="4"> </nx-code-input>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
class OnPushCodeInput extends CodeInputTest {}

@Component({
template: `<nx-code-input [length]="4"></nx-code-input>`,
providers: [{ provide: NxCodeInputIntl, useClass: MyIntl }],
Expand Down
Loading

0 comments on commit bf095ee

Please sign in to comment.