Skip to content

Commit 4cfae9a

Browse files
r-farkhutdinovRuslan Farkhutdinov
andauthored
DateBox: Fix DateBox mask input with Numpad IME digits (T1326628) (#33421)
Co-authored-by: Ruslan Farkhutdinov <ruslan.farkhutdinov@devexpress.com>
1 parent 6fa5c53 commit 4cfae9a

2 files changed

Lines changed: 174 additions & 22 deletions

File tree

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

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { getDatePartIndexByPosition, renderDateParts } from './m_date_box.mask.p
2020
const MASK_EVENT_NAMESPACE = 'dateBoxMask';
2121
const FORWARD = 1;
2222
const BACKWARD = -1;
23+
const IME_DIGIT_CODE_REGEXP = /^(?:Digit|Numpad)(\d)$/;
2324

2425
export interface DateBoxMaskProperties extends Properties {
2526
emptyDateValue?: Date;
@@ -41,6 +42,12 @@ class DateBoxMask extends DateBoxBase {
4142

4243
_formatPattern?: unknown;
4344

45+
_pendingIMEDigit?: string | null;
46+
47+
_isIMEDigitProcessed?: boolean;
48+
49+
_isIMECommitPending?: boolean;
50+
4451
_supportedKeys(): Record<string, (e: KeyboardEvent) => boolean> {
4552
const originalHandlers = super._supportedKeys();
4653
const callOriginalHandler = (e) => {
@@ -151,7 +158,7 @@ class DateBoxMask extends DateBoxBase {
151158

152159
_isSingleDigitKey(e) {
153160
const data = e.originalEvent?.data;
154-
return data?.length === 1 && parseInt(data, 10);
161+
return data?.length === 1 && !isNaN(parseInt(data, 10));
155162
}
156163

157164
_useBeforeInputEvent() {
@@ -167,22 +174,33 @@ class DateBoxMask extends DateBoxBase {
167174
isValueChanged && eventsEngine.trigger(this._input(), 'input');
168175
}
169176

170-
_keyboardHandler(e) {
171-
let { key } = e.originalEvent;
177+
_keyboardHandler(e): boolean {
178+
const { key } = e.originalEvent;
172179

173180
const result = super._keyboardHandler(e);
174181

175182
if (!this._useMaskBehavior() || this._useBeforeInputEvent()) {
183+
this._pendingIMEDigit = null;
184+
this._isIMEDigitProcessed = false;
185+
this._isIMECommitPending = false;
186+
176187
return result;
177188
}
178189

179-
if (browser.chrome && e.key === 'Process' && e.code.indexOf('Digit') === 0) {
180-
key = e.code.replace('Digit', '');
181-
this._processInputKey(key);
182-
this._maskInputHandler = () => {
183-
this._renderSelectedPart();
184-
};
185-
} else if (this._isSingleCharKey(e)) {
190+
const chromiumDigitCodeMatch = IME_DIGIT_CODE_REGEXP.exec(e.code);
191+
192+
if (browser.chrome && e.key === 'Process' && chromiumDigitCodeMatch) {
193+
const [, digit] = chromiumDigitCodeMatch;
194+
195+
this._pendingIMEDigit = digit;
196+
197+
return result;
198+
}
199+
200+
this._pendingIMEDigit = null;
201+
this._isIMEDigitProcessed = false;
202+
203+
if (this._isSingleCharKey(e)) {
186204
this._keyInputHandler(e.originalEvent, key);
187205
}
188206

@@ -220,12 +238,40 @@ class DateBoxMask extends DateBoxBase {
220238
return true;
221239
}
222240

223-
_keyPressHandler(e) {
241+
_syncInputWithMask(): void {
242+
this._input().val(this._getDisplayedText(this._maskValue));
243+
this._caret(this._getActivePartProp('caret'));
244+
}
245+
246+
_keyPressHandler(e: { originalEvent: InputEvent & KeyboardEvent }): void {
224247
const { originalEvent: event } = e;
225-
if (event?.inputType === 'insertCompositionText' && this._isSingleDigitKey(e)) {
226-
this._processInputKey(event.data);
227-
this._renderDisplayText(this._getDisplayedText(this._maskValue));
228-
this._selectNextPart();
248+
249+
const isCompositionDigit = event?.inputType === 'insertCompositionText'
250+
&& this._isSingleDigitKey(e);
251+
252+
const isIMECommitDigit = event?.inputType === 'insertText'
253+
&& this._isSingleDigitKey(e)
254+
&& this._isIMECommitPending;
255+
256+
if (isCompositionDigit && event.data) {
257+
if (!this._isIMEDigitProcessed) {
258+
this._processInputKey(event.data);
259+
this._isIMEDigitProcessed = true;
260+
this._isIMECommitPending = true;
261+
}
262+
263+
this._syncInputWithMask();
264+
265+
return;
266+
}
267+
268+
if (isIMECommitDigit) {
269+
this._isIMECommitPending = false;
270+
this._pendingIMEDigit = null;
271+
272+
this._syncInputWithMask();
273+
274+
return;
229275
}
230276
super._keyPressHandler(e);
231277

@@ -429,6 +475,7 @@ class DateBoxMask extends DateBoxBase {
429475
this._renderSelectedPart();
430476
});
431477

478+
eventsEngine.on(this._input(), addNamespace('compositionstart', MASK_EVENT_NAMESPACE), this._maskCompositionStartHandler.bind(this));
432479
eventsEngine.on(this._input(), addNamespace('compositionend', MASK_EVENT_NAMESPACE), this._maskCompositionEndHandler.bind(this));
433480

434481
if (this._useBeforeInputEvent()) {
@@ -616,14 +663,16 @@ class DateBoxMask extends DateBoxBase {
616663
}
617664
}
618665

619-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
620-
_maskCompositionEndHandler(e): void {
666+
_maskCompositionStartHandler(): void {
667+
this._isIMEDigitProcessed = false;
668+
this._isIMECommitPending = false;
669+
}
670+
671+
_maskCompositionEndHandler(): void {
621672
this._input().val(this._getDisplayedText(this._maskValue));
622-
this._selectNextPart();
673+
this._caret(this._getActivePartProp('caret'));
623674

624-
this._maskInputHandler = () => {
625-
this._renderSelectedPart();
626-
};
675+
this._maskInputHandler = null;
627676
}
628677

629678
_maskPasteHandler(e): 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)