Skip to content

Commit 91a8803

Browse files
r-farkhutdinovRuslan Farkhutdinov
andauthored
DateBox: Fix DateBox mask input with Numpad IME digits (T1326628) (#33420)
Co-authored-by: Ruslan Farkhutdinov <ruslan.farkhutdinov@devexpress.com>
1 parent e39f084 commit 91a8803

2 files changed

Lines changed: 171 additions & 18 deletions

File tree

packages/devextreme/js/__internal/ui/date_box/date_box.mask.ts

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { getDatePartIndexByPosition, renderDateParts } from './date_box.mask.par
2525
const MASK_EVENT_NAMESPACE = 'dateBoxMask';
2626
const FORWARD = 1;
2727
const BACKWARD = -1;
28+
const IME_DIGIT_CODE_REGEXP = /^(?:Digit|Numpad)(\d)$/;
2829

2930
export interface DateBoxMaskProperties extends Properties {
3031
emptyDateValue?: Date;
@@ -46,6 +47,12 @@ class DateBoxMask extends DateBoxBase {
4647

4748
_formatPattern?: string | null;
4849

50+
_pendingIMEDigit?: string | null;
51+
52+
_isIMEDigitProcessed?: boolean;
53+
54+
_isIMECommitPending?: boolean;
55+
4956
_supportedKeys(): Record<string, (e: KeyboardEvent) => unknown> {
5057
const originalHandlers = super._supportedKeys();
5158
const callOriginalHandler = (e: KeyboardEvent): unknown => {
@@ -171,7 +178,7 @@ class DateBoxMask extends DateBoxBase {
171178
alt?: boolean;
172179
}): boolean {
173180
const data = e.originalEvent?.data;
174-
return data?.length === 1 && Boolean(parseInt(data, 10));
181+
return data?.length === 1 && !isNaN(parseInt(data, 10));
175182
}
176183

177184
_useBeforeInputEvent(): boolean {
@@ -190,21 +197,32 @@ class DateBoxMask extends DateBoxBase {
190197
}
191198

192199
_keyboardHandler(e: KeyboardKeyDownEvent): boolean {
193-
let { key } = e.originalEvent;
200+
const { key } = e.originalEvent;
194201

195202
const result = super._keyboardHandler(e);
196203

197204
if (!this._useMaskBehavior() || this._useBeforeInputEvent()) {
205+
this._pendingIMEDigit = null;
206+
this._isIMEDigitProcessed = false;
207+
this._isIMECommitPending = false;
208+
198209
return result;
199210
}
200211

201-
if (browser.chrome && e.key === 'Process' && e.code.startsWith('Digit')) {
202-
key = e.code.replace('Digit', '');
203-
this._processInputKey(key);
204-
this._maskInputHandler = (): void => {
205-
this._renderSelectedPart();
206-
};
207-
} else if (this._isSingleCharKey(e)) {
212+
const chromiumDigitCodeMatch = IME_DIGIT_CODE_REGEXP.exec(e.code);
213+
214+
if (browser.chrome && e.key === 'Process' && chromiumDigitCodeMatch) {
215+
const [, digit] = chromiumDigitCodeMatch;
216+
217+
this._pendingIMEDigit = digit;
218+
219+
return result;
220+
}
221+
222+
this._pendingIMEDigit = null;
223+
this._isIMEDigitProcessed = false;
224+
225+
if (this._isSingleCharKey(e)) {
208226
this._keyInputHandler(e.originalEvent, key);
209227
}
210228

@@ -242,12 +260,40 @@ class DateBoxMask extends DateBoxBase {
242260
return true;
243261
}
244262

263+
_syncInputWithMask(): void {
264+
this._input().val(this._getDisplayedText(this._maskValue));
265+
this._caret(this._getActivePartProp('caret'));
266+
}
267+
245268
_keyPressHandler(e: { originalEvent: InputEvent & KeyboardEvent }): void {
246269
const { originalEvent: event } = e;
247-
if (event?.inputType === 'insertCompositionText' && this._isSingleDigitKey(e)) {
248-
this._processInputKey(event.data ?? '');
249-
this._renderDisplayText(this._getDisplayedText(this._maskValue));
250-
this._selectNextPart();
270+
271+
const isCompositionDigit = event?.inputType === 'insertCompositionText'
272+
&& this._isSingleDigitKey(e);
273+
274+
const isIMECommitDigit = event?.inputType === 'insertText'
275+
&& this._isSingleDigitKey(e)
276+
&& this._isIMECommitPending;
277+
278+
if (isCompositionDigit && event.data) {
279+
if (!this._isIMEDigitProcessed) {
280+
this._processInputKey(event.data);
281+
this._isIMEDigitProcessed = true;
282+
this._isIMECommitPending = true;
283+
}
284+
285+
this._syncInputWithMask();
286+
287+
return;
288+
}
289+
290+
if (isIMECommitDigit) {
291+
this._isIMECommitPending = false;
292+
this._pendingIMEDigit = null;
293+
294+
this._syncInputWithMask();
295+
296+
return;
251297
}
252298
super._keyPressHandler(e);
253299

@@ -455,6 +501,7 @@ class DateBoxMask extends DateBoxBase {
455501
this._renderSelectedPart();
456502
});
457503

504+
eventsEngine.on(this._input(), addNamespace('compositionstart', MASK_EVENT_NAMESPACE), this._maskCompositionStartHandler.bind(this));
458505
eventsEngine.on(this._input(), addNamespace('compositionend', MASK_EVENT_NAMESPACE), this._maskCompositionEndHandler.bind(this));
459506

460507
if (this._useBeforeInputEvent()) {
@@ -671,13 +718,16 @@ class DateBoxMask extends DateBoxBase {
671718
}
672719
}
673720

721+
_maskCompositionStartHandler(): void {
722+
this._isIMEDigitProcessed = false;
723+
this._isIMECommitPending = false;
724+
}
725+
674726
_maskCompositionEndHandler(): void {
675727
this._input().val(this._getDisplayedText(this._maskValue));
676-
this._selectNextPart();
728+
this._caret(this._getActivePartProp('caret'));
677729

678-
this._maskInputHandler = (): void => {
679-
this._renderSelectedPart();
680-
};
730+
this._maskInputHandler = null;
681731
}
682732

683733
_maskPasteHandler(e: DxEvent): void {

packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.mask.tests.js

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,16 @@ QUnit.testStart(() => {
2020
$('#qunit-fixture').html('<div id=\'dateBox\'></div>');
2121
});
2222

23-
const simulateIMEInput = function(eventsData) {
23+
const insertNativeText = ($input, text) => {
24+
const input = $input.get(0);
25+
const start = input.selectionStart || input.value.length;
26+
const end = input.selectionEnd || start;
27+
28+
input.value = input.value.slice(0, start) + text + input.value.slice(end);
29+
input.setSelectionRange(start + text.length, start + text.length);
30+
};
31+
32+
const simulateIMEInput = function(eventsData, shouldFireInsertText) {
2433
this.$input.trigger($.Event('keydown', {
2534
key: 'Process',
2635
code: eventsData.keyDownCode,
@@ -56,6 +65,28 @@ const simulateIMEInput = function(eventsData) {
5665
}));
5766

5867
this.$input.trigger($.Event('compositionend'));
68+
69+
if(shouldFireInsertText) {
70+
insertNativeText(this.$input, eventsData.inputData);
71+
72+
this.$input.trigger($.Event('input', {
73+
type: 'input',
74+
originalEvent: $.Event('input', {
75+
inputType: 'insertText',
76+
isComposing: false,
77+
data: eventsData.inputData,
78+
}),
79+
}));
80+
81+
this.$input.trigger($.Event('keyup', {
82+
key: eventsData.inputData,
83+
code: eventsData.keyDownCode,
84+
originalEvent: $.Event('keyup', {
85+
key: eventsData.inputData,
86+
code: eventsData.keyDownCode,
87+
}),
88+
}));
89+
}
5990
};
6091

6192
const setupModule = {
@@ -1240,6 +1271,78 @@ module('Search', setupModule, () => {
12401271
assert.strictEqual(this.$input.val(), '5555/05/05', 'year was changed');
12411272
});
12421273

1274+
test('Typing digits via Numpad IME with final insertText should process every composition cycle once (T1326628)', function(assert) {
1275+
this.instance.option({
1276+
displayFormat: 'MM/dd/yyyy',
1277+
value: new Date(2025, 9, 16),
1278+
});
1279+
1280+
this.keyboard.caret({ start: 0, end: 2 });
1281+
1282+
const eventsData = {
1283+
keyDownCode: 'Numpad1',
1284+
inputData: '1',
1285+
};
1286+
1287+
simulateIMEInput.call(this, eventsData, true);
1288+
assert.strictEqual(this.$input.val(), '01/16/2025', 'first month digit is applied');
1289+
1290+
simulateIMEInput.call(this, eventsData, true);
1291+
assert.strictEqual(this.$input.val(), '11/16/2025', 'second month digit is applied');
1292+
1293+
simulateIMEInput.call(this, eventsData, true);
1294+
assert.strictEqual(this.$input.val(), '11/01/2025', 'first day digit is applied');
1295+
1296+
simulateIMEInput.call(this, eventsData, true);
1297+
assert.strictEqual(this.$input.val(), '11/11/2025', 'second day digit is applied');
1298+
1299+
simulateIMEInput.call(this, eventsData, true);
1300+
assert.strictEqual(this.$input.val(), '11/11/2021', 'first year digit is applied');
1301+
1302+
simulateIMEInput.call(this, eventsData, true);
1303+
assert.strictEqual(this.$input.val(), '11/11/2011', 'second year digit is applied');
1304+
1305+
simulateIMEInput.call(this, eventsData, true);
1306+
assert.strictEqual(this.$input.val(), '11/11/2111', 'third year digit is applied');
1307+
1308+
simulateIMEInput.call(this, eventsData, true);
1309+
assert.strictEqual(this.$input.val(), '11/11/1111', 'fourth year digit is applied');
1310+
});
1311+
1312+
test('Final insertText after Numpad IME composition should not duplicate digit or corrupt mask value (T1326628)', function(assert) {
1313+
this.instance.option({
1314+
displayFormat: 'MM/dd/yyyy',
1315+
value: new Date(2025, 9, 16),
1316+
});
1317+
1318+
this.keyboard.caret({ start: 0, end: 2 });
1319+
1320+
simulateIMEInput.call(this, {
1321+
keyDownCode: 'Numpad1',
1322+
inputData: '1',
1323+
}, true);
1324+
1325+
simulateIMEInput.call(this, {
1326+
keyDownCode: 'Numpad1',
1327+
inputData: '1',
1328+
}, true);
1329+
1330+
assert.strictEqual(this.$input.val(), '11/16/2025', 'final insertText commit is ignored as duplicate IME commit');
1331+
});
1332+
1333+
test('Typing zero via Numpad IME composition with final insertText should be registered (T1326628)', function(assert) {
1334+
this.instance.option({
1335+
displayFormat: 'MM/dd/yyyy',
1336+
value: new Date(2012, 8, 5),
1337+
});
1338+
1339+
this.keyboard.caret({ start: 0, end: 2 });
1340+
1341+
simulateIMEInput.call(this, { keyDownCode: 'Numpad1', inputData: '1' }, true);
1342+
simulateIMEInput.call(this, { keyDownCode: 'Numpad0', inputData: '0' }, true);
1343+
1344+
assert.strictEqual(this.$input.val(), '10/05/2012', 'month is correctly set to 10 after typing "1" then "0" via Numpad IME');
1345+
});
12431346

12441347
test('Pasting incorrect value to the date part should not ignore mask rules', function(assert) {
12451348
this.instance.option('displayFormat', 'yyyy/MM/dd');

0 commit comments

Comments
 (0)