Skip to content

Commit 74e36a0

Browse files
committed
fix(ref: 1559): keyboard up arrow key disabled when mask is applied
1 parent 19d6c7c commit 74e36a0

File tree

9 files changed

+383
-8
lines changed

9 files changed

+383
-8
lines changed

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 & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ngx-mask",
3-
"version": "20.0.0",
3+
"version": "20.0.1",
44
"description": "Awesome ngx mask",
55
"license": "MIT",
66
"engines": {
@@ -127,9 +127,6 @@
127127
"cssnano": "7.1.0",
128128
"postcss-scss": "4.0.9"
129129
},
130-
"overrides": {
131-
"stylus": "0.0.1-security"
132-
},
133130
"typeCoverage": {
134131
"atLeast": 91,
135132
"ignoreObject": true,

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.0",
3+
"version": "20.0.1",
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: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
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); // Should clear if doesn't match
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+
// Change mask dynamically
123+
component.mask.set('00/00/0000');
124+
equalTextarea('12345678', '12/34/5678', fixture);
125+
});
126+
127+
it('should handle empty mask in textarea', () => {
128+
component.mask.set('');
129+
equalTextarea('any text', 'any text', fixture);
130+
equalTextarea('123456', '123456', fixture);
131+
equalTextarea('[email protected]', '[email protected]', fixture);
132+
});
133+
134+
it('should handle null mask in textarea', () => {
135+
component.mask.set(null);
136+
equalTextarea('any text', 'any text', fixture);
137+
equalTextarea('123456', '123456', fixture);
138+
});
139+
140+
it('should handle undefined mask in textarea', () => {
141+
component.mask.set(undefined);
142+
equalTextarea('any text', 'any text', fixture);
143+
equalTextarea('123456', '123456', fixture);
144+
});
145+
146+
it('should handle textarea with no mask', () => {
147+
// No mask set
148+
equalTextarea('any text', 'any text', fixture);
149+
equalTextarea('123456', '123456', fixture);
150+
equalTextarea('[email protected]', '[email protected]', fixture);
151+
});
152+
153+
it('should handle textarea with large content', () => {
154+
component.mask.set('A*');
155+
const largeText =
156+
'This is a very long text that should be handled properly by the textarea with mask. '.repeat(
157+
10
158+
);
159+
const expectedText = largeText.replace(/\s/g, ''); // Remove spaces for A* mask
160+
equalTextarea(largeText, expectedText, fixture);
161+
});
162+
163+
it('should handle textarea with special characters in content', () => {
164+
component.mask.set('A*');
165+
const textWithSpecialChars = 'Text with special chars: !@#$%^&*()_+-=[]{}|;:,.<>?';
166+
const expectedText = textWithSpecialChars.replace(/[^a-zA-Z]/g, ''); // Keep only letters for A* mask
167+
equalTextarea(textWithSpecialChars, expectedText, fixture);
168+
});
169+
170+
it('should handle textarea with arrow up key (should not be prevented)', () => {
171+
component.mask.set('0000-0000');
172+
const textarea = fixture.debugElement.query(By.css('#masked')).nativeElement;
173+
174+
// Set some content and focus
175+
textarea.value = '1234-5678';
176+
textarea.focus();
177+
textarea.setSelectionRange(5, 5);
178+
179+
// Test arrow up (should work in textarea for line navigation)
180+
const arrowUpEvent = new KeyboardEvent('keydown', { key: 'ArrowUp' });
181+
textarea.dispatchEvent(arrowUpEvent);
182+
183+
// Arrow up should not be prevented in textarea (this is the key test)
184+
// The event should not have defaultPrevented set to true
185+
});
186+
187+
it('should handle textarea with backspace key', () => {
188+
component.mask.set('0000-0000');
189+
const textarea = fixture.debugElement.query(By.css('#masked')).nativeElement;
190+
191+
textarea.value = '1234-5678';
192+
textarea.focus();
193+
textarea.setSelectionRange(6, 6);
194+
195+
const backspaceEvent = new KeyboardEvent('keydown', { key: 'Backspace' });
196+
textarea.dispatchEvent(backspaceEvent);
197+
198+
// Backspace should work normally in textarea
199+
});
200+
201+
it('should handle textarea with paste event', () => {
202+
component.mask.set('0000-0000');
203+
const textarea = fixture.debugElement.query(By.css('#masked')).nativeElement;
204+
205+
textarea.focus();
206+
207+
// Simulate paste event
208+
const pasteEvent = new ClipboardEvent('paste', {
209+
clipboardData: new DataTransfer(),
210+
});
211+
textarea.dispatchEvent(pasteEvent);
212+
213+
// Paste should be handled
214+
});
215+
});

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)