Skip to content

Commit e35fe94

Browse files
authored
fix(ref: 1559): keyboard up arrow key disabled when mask is applied
fix(ref: 1559): keyboard up arrow key disabled when mask is applied
2 parents 1d43b72 + 1bd7abb commit e35fe94

File tree

9 files changed

+339
-6
lines changed

9 files changed

+339
-6
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
# 20.0.2(2025-07-31)
2+
3+
### fix
4+
5+
- Fix ([#1559](https://github.com/JsDaddy/ngx-mask/issues/1559))
6+
17
# 20.0.1(2025-07-31)
28

39
### fix
410

511
- Fix ([#1548](https://github.com/JsDaddy/ngx-mask/issues/1548))
612
- Fix ([#1551](https://github.com/JsDaddy/ngx-mask/issues/1551))
713

8-
914
# 20.0.0(2025-07-22)
1015

1116
### Feature

bun.lock

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
"semantic-release": "24.2.7",
2323
"semantic-release-export-data": "1.1.1",
2424
"snyk": "1.1298.1",
25-
"stylus": "^0.0.1-security",
2625
},
2726
"devDependencies": {
2827
"@angular-devkit/build-angular": "20.1.1",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ngx-mask",
3-
"version": "20.0.1",
3+
"version": "20.0.2",
44
"description": "Awesome ngx mask",
55
"license": "MIT",
66
"engines": {

projects/ngx-mask-lib/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ngx-mask",
3-
"version": "20.0.1",
3+
"version": "20.0.2",
44
"description": "awesome ngx mask",
55
"keywords": [
66
"ng2-mask",

projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -835,8 +835,10 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida
835835
this._inputValue.set(el.value);
836836
this._setMask();
837837

838+
const isTextarea = el.tagName.toLowerCase() === 'textarea';
839+
838840
if (el.type !== 'number') {
839-
if (e.key === MaskExpression.ARROW_UP) {
841+
if (e.key === MaskExpression.ARROW_UP && !isTextarea) {
840842
e.preventDefault();
841843
}
842844
if (

projects/ngx-mask-lib/src/lib/ngx-mask.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,7 @@ export class NgxMaskService extends NgxMaskApplierService {
782782

783783
if (
784784
separatorExpression.indexOf('2') > 0 ||
785-
(this.leadZero && Number(separatorPrecision) > 0)
785+
(this.leadZero && Number(separatorPrecision) > 0 && Number.isFinite(separatorPrecision))
786786
) {
787787
if (this.decimalMarker === MaskExpression.COMMA && this.leadZero) {
788788
value = value.replace(',', '.');
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import type { ComponentFixture } from '@angular/core/testing';
2+
import { TestBed } from '@angular/core/testing';
3+
import { ReactiveFormsModule } from '@angular/forms';
4+
5+
import { TestTextareaMaskComponent } from './utils/test-textarea-component.component';
6+
import { equalTextarea } from './utils/test-functions.component';
7+
import { provideNgxMask, NgxMaskDirective } from 'ngx-mask';
8+
import { By } from '@angular/platform-browser';
9+
10+
describe('Directive: Mask with Textarea', () => {
11+
let fixture: ComponentFixture<TestTextareaMaskComponent>;
12+
let component: TestTextareaMaskComponent;
13+
14+
beforeEach(() => {
15+
TestBed.configureTestingModule({
16+
imports: [ReactiveFormsModule, NgxMaskDirective, TestTextareaMaskComponent],
17+
providers: [provideNgxMask()],
18+
});
19+
fixture = TestBed.createComponent(TestTextareaMaskComponent);
20+
component = fixture.componentInstance;
21+
fixture.detectChanges();
22+
});
23+
24+
it('should apply basic mask to textarea', () => {
25+
component.mask.set('0000.0000');
26+
equalTextarea('1', '1', fixture);
27+
equalTextarea('12', '12', fixture);
28+
equalTextarea('1234567', '1234.567', fixture);
29+
});
30+
31+
it('should handle date mask in textarea', () => {
32+
component.mask.set('00/00/0000');
33+
equalTextarea('12', '12', fixture);
34+
equalTextarea('1234', '12/34', fixture);
35+
equalTextarea('12345678', '12/34/5678', fixture);
36+
});
37+
38+
it('should handle phone mask in textarea', () => {
39+
component.mask.set('(000) 000-0000');
40+
equalTextarea('1', '(1', fixture);
41+
equalTextarea('123', '(123', fixture);
42+
equalTextarea('1234567890', '(123) 456-7890', fixture);
43+
});
44+
45+
it('should handle email mask in textarea', () => {
46+
component.mask.set('A*@A*.A*');
47+
equalTextarea('test', 'test', fixture);
48+
equalTextarea('test@', 'test@', fixture);
49+
equalTextarea('test@example', 'test@example', fixture);
50+
equalTextarea('[email protected]', '[email protected]', fixture);
51+
});
52+
53+
it('should handle prefix and suffix in textarea', () => {
54+
component.mask.set('0000');
55+
component.prefix.set('$');
56+
component.suffix.set(' USD');
57+
equalTextarea('123', '$123 USD', fixture);
58+
equalTextarea('1234', '$1234 USD', fixture);
59+
});
60+
61+
it('should handle special characters in textarea', () => {
62+
component.mask.set('0000-0000');
63+
equalTextarea('1234', '1234', fixture);
64+
equalTextarea('12345678', '1234-5678', fixture);
65+
});
66+
67+
it('should handle decimal mask in textarea', () => {
68+
component.mask.set('separator.2');
69+
component.decimalMarker.set('.');
70+
component.thousandSeparator.set(',');
71+
equalTextarea('1234', '1,234', fixture);
72+
equalTextarea('1234.5', '1,234.5', fixture);
73+
equalTextarea('1234.56', '1,234.56', fixture);
74+
});
75+
76+
it('should handle percentage mask in textarea', () => {
77+
component.mask.set('percent');
78+
equalTextarea('50', '50', fixture);
79+
equalTextarea('100', '100', fixture);
80+
});
81+
82+
it('should handle showMaskTyped in textarea', () => {
83+
component.mask.set('0000-0000');
84+
component.showMaskTyped.set(true);
85+
equalTextarea('', '____-____', fixture);
86+
equalTextarea('1', '1___-____', fixture);
87+
equalTextarea('12345678', '1234-5678', fixture);
88+
});
89+
90+
it('should handle clearIfNotMatch in textarea', () => {
91+
component.mask.set('0000-0000');
92+
component.clearIfNotMatch.set(true);
93+
equalTextarea('123', '123', fixture);
94+
equalTextarea('12345678', '1234-5678', fixture);
95+
equalTextarea('abc', '', fixture);
96+
});
97+
98+
it('should handle dropSpecialCharacters in textarea', () => {
99+
component.mask.set('0000-0000');
100+
component.dropSpecialCharacters.set(true);
101+
equalTextarea('1234-5678', '1234-5678', fixture);
102+
equalTextarea('12345678', '1234-5678', fixture);
103+
});
104+
105+
it('should handle validation in textarea', () => {
106+
component.mask.set('0000-0000');
107+
component.validation.set(true);
108+
equalTextarea('1234', '1234', fixture);
109+
equalTextarea('12345678', '1234-5678', fixture);
110+
});
111+
112+
it('should handle multiple lines in textarea with mask', () => {
113+
component.mask.set('A*');
114+
equalTextarea('line1\nline2', 'line1line2', fixture);
115+
equalTextarea('test\nanother\ntest', 'testanothertest', fixture);
116+
});
117+
118+
it('should handle dynamic mask changes in textarea', () => {
119+
component.mask.set('0000.0000');
120+
equalTextarea('1234567', '1234.567', fixture);
121+
122+
component.mask.set('00/00/0000');
123+
equalTextarea('12345678', '12/34/5678', fixture);
124+
});
125+
126+
it('should handle textarea with no mask', () => {
127+
equalTextarea('any text', 'any text', fixture);
128+
equalTextarea('123456', '123456', fixture);
129+
equalTextarea('[email protected]', '[email protected]', fixture);
130+
});
131+
132+
it('should handle textarea with special characters in content', () => {
133+
component.mask.set('A*');
134+
const textWithSpecialChars = 'Text with special chars: !@#$%^&*()_+-=[]{}|;:,.<>?';
135+
const expectedText = textWithSpecialChars.replace(/[^a-zA-Z]/g, '');
136+
equalTextarea(textWithSpecialChars, expectedText, fixture);
137+
});
138+
139+
it('should handle textarea with arrow up key (should not be prevented)', () => {
140+
component.mask.set('0000-0000');
141+
const textarea = fixture.debugElement.query(By.css('#masked')).nativeElement;
142+
143+
textarea.value = '1234-5678';
144+
textarea.focus();
145+
textarea.setSelectionRange(5, 5);
146+
147+
const arrowUpEvent = new KeyboardEvent('keydown', { key: 'ArrowUp' });
148+
textarea.dispatchEvent(arrowUpEvent);
149+
});
150+
151+
it('should handle textarea with backspace key', () => {
152+
component.mask.set('0000-0000');
153+
const textarea = fixture.debugElement.query(By.css('#masked')).nativeElement;
154+
155+
textarea.value = '1234-5678';
156+
textarea.focus();
157+
textarea.setSelectionRange(6, 6);
158+
159+
const backspaceEvent = new KeyboardEvent('keydown', { key: 'Backspace' });
160+
textarea.dispatchEvent(backspaceEvent);
161+
});
162+
163+
it('should handle textarea with paste event', () => {
164+
component.mask.set('0000-0000');
165+
const textarea = fixture.debugElement.query(By.css('#masked')).nativeElement;
166+
167+
textarea.focus();
168+
169+
const pasteEvent = new ClipboardEvent('paste', {
170+
clipboardData: new DataTransfer(),
171+
});
172+
textarea.dispatchEvent(pasteEvent);
173+
});
174+
});

projects/ngx-mask-lib/src/test/utils/test-functions.component.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,46 @@ export function typeTest(inputValue: string, fixture: any): string {
4444
return inputElement.value;
4545
}
4646

47+
// Functions for textarea
48+
export function pasteTestTextarea(inputValue: string, fixture: any): string {
49+
fixture.detectChanges();
50+
51+
fixture.nativeElement.querySelector('textarea').value = inputValue;
52+
53+
fixture.nativeElement.querySelector('textarea').dispatchEvent(new Event('paste'));
54+
fixture.nativeElement.querySelector('textarea').dispatchEvent(new Event('input'));
55+
fixture.nativeElement.querySelector('textarea').dispatchEvent(new Event('ngModelChange'));
56+
57+
return fixture.nativeElement.querySelector('textarea').value;
58+
}
59+
60+
export function typeTestTextarea(inputValue: string, fixture: any): string {
61+
fixture.detectChanges();
62+
const inputArray = inputValue.split('');
63+
const textareaElement = fixture.nativeElement.querySelector('textarea');
64+
65+
textareaElement.value = '';
66+
textareaElement.dispatchEvent(new Event('input'));
67+
textareaElement.dispatchEvent(new Event('ngModelChange'));
68+
69+
{
70+
for (const element of inputArray) {
71+
textareaElement.dispatchEvent(new KeyboardEvent('keydown'), { key: element });
72+
const selectionStart = textareaElement.selectionStart || 0;
73+
const selectionEnd = textareaElement.selectionEnd || 0;
74+
textareaElement.value =
75+
textareaElement.value.slice(0, selectionStart) +
76+
element +
77+
textareaElement.value.slice(selectionEnd);
78+
79+
textareaElement.selectionStart = selectionStart + 1;
80+
textareaElement.dispatchEvent(new Event('input'));
81+
textareaElement.dispatchEvent(new Event('ngModelChange'));
82+
}
83+
}
84+
return textareaElement.value;
85+
}
86+
4787
export function equal(
4888
value: string,
4989
expectedValue: string,
@@ -65,3 +105,25 @@ export function equal(
65105
}
66106
expect(fixture.nativeElement.querySelector('input').value).toBe(expectedValue);
67107
}
108+
109+
export function equalTextarea(
110+
value: string,
111+
expectedValue: string,
112+
fixture: any,
113+
async = false,
114+
testType: typeof Paste | typeof Type = Type
115+
): void {
116+
if (testType === Paste) {
117+
pasteTestTextarea(value, fixture);
118+
} else {
119+
typeTestTextarea(value, fixture);
120+
}
121+
122+
if (async) {
123+
Promise.resolve().then(() => {
124+
expect(fixture.nativeElement.querySelector('textarea').value).toBe(expectedValue);
125+
});
126+
return;
127+
}
128+
expect(fixture.nativeElement.querySelector('textarea').value).toBe(expectedValue);
129+
}

0 commit comments

Comments
 (0)