Skip to content

Commit 591232b

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

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
@@ -24,6 +24,7 @@ import { getDatePartIndexByPosition, renderDateParts } from './date_box.mask.par
2424
const MASK_EVENT_NAMESPACE = 'dateBoxMask';
2525
const FORWARD = 1;
2626
const BACKWARD = -1;
27+
const IME_DIGIT_CODE_REGEXP = /^(?:Digit|Numpad)(\d)$/;
2728

2829
export interface DateBoxMaskProperties extends Properties {
2930
emptyDateValue?: Date;
@@ -45,6 +46,12 @@ class DateBoxMask extends DateBoxBase {
4546

4647
_formatPattern?: string | null;
4748

49+
_pendingIMEDigit?: string | null;
50+
51+
_isIMEDigitProcessed?: boolean;
52+
53+
_isIMECommitPending?: boolean;
54+
4855
_supportedKeys(): Record<string, (e: KeyboardEvent) => boolean | undefined> {
4956
const originalHandlers = super._supportedKeys();
5057
const callOriginalHandler = (e: KeyboardEvent): boolean | undefined => {
@@ -183,7 +190,7 @@ class DateBoxMask extends DateBoxBase {
183190
alt?: boolean;
184191
}): boolean {
185192
const data = e.originalEvent?.data;
186-
return data?.length === 1 && Boolean(parseInt(data, 10));
193+
return data?.length === 1 && !isNaN(parseInt(data, 10));
187194
}
188195

189196
_useBeforeInputEvent(): boolean {
@@ -202,21 +209,32 @@ class DateBoxMask extends DateBoxBase {
202209
}
203210

204211
_keyboardHandler(e: KeyboardKeyDownEvent): boolean {
205-
let { key } = e.originalEvent;
212+
const { key } = e.originalEvent;
206213

207214
const result = super._keyboardHandler(e);
208215

209216
if (!this._useMaskBehavior() || this._useBeforeInputEvent()) {
217+
this._pendingIMEDigit = null;
218+
this._isIMEDigitProcessed = false;
219+
this._isIMECommitPending = false;
220+
210221
return result;
211222
}
212223

213-
if (browser.chrome && e.key === 'Process' && e.code.startsWith('Digit')) {
214-
key = e.code.replace('Digit', '');
215-
this._processInputKey(key);
216-
this._maskInputHandler = (): void => {
217-
this._renderSelectedPart();
218-
};
219-
} else if (this._isSingleCharKey(e)) {
224+
const chromiumDigitCodeMatch = IME_DIGIT_CODE_REGEXP.exec(e.code);
225+
226+
if (browser.chrome && e.key === 'Process' && chromiumDigitCodeMatch) {
227+
const [, digit] = chromiumDigitCodeMatch;
228+
229+
this._pendingIMEDigit = digit;
230+
231+
return result;
232+
}
233+
234+
this._pendingIMEDigit = null;
235+
this._isIMEDigitProcessed = false;
236+
237+
if (this._isSingleCharKey(e)) {
220238
this._keyInputHandler(e.originalEvent, key);
221239
}
222240

@@ -254,12 +272,40 @@ class DateBoxMask extends DateBoxBase {
254272
return true;
255273
}
256274

275+
_syncInputWithMask(): void {
276+
this._input().val(this._getDisplayedText(this._maskValue));
277+
this._caret(this._getActivePartProp('caret'));
278+
}
279+
257280
_keyPressHandler(e: { originalEvent: InputEvent & KeyboardEvent }): void {
258281
const { originalEvent: event } = e;
259-
if (event?.inputType === 'insertCompositionText' && this._isSingleDigitKey(e)) {
260-
this._processInputKey(event.data ?? '');
261-
this._renderDisplayText(this._getDisplayedText(this._maskValue));
262-
this._selectNextPart();
282+
283+
const isCompositionDigit = event?.inputType === 'insertCompositionText'
284+
&& this._isSingleDigitKey(e);
285+
286+
const isIMECommitDigit = event?.inputType === 'insertText'
287+
&& this._isSingleDigitKey(e)
288+
&& this._isIMECommitPending;
289+
290+
if (isCompositionDigit && event.data) {
291+
if (!this._isIMEDigitProcessed) {
292+
this._processInputKey(event.data);
293+
this._isIMEDigitProcessed = true;
294+
this._isIMECommitPending = true;
295+
}
296+
297+
this._syncInputWithMask();
298+
299+
return;
300+
}
301+
302+
if (isIMECommitDigit) {
303+
this._isIMECommitPending = false;
304+
this._pendingIMEDigit = null;
305+
306+
this._syncInputWithMask();
307+
308+
return;
263309
}
264310
super._keyPressHandler(e);
265311

@@ -467,6 +513,7 @@ class DateBoxMask extends DateBoxBase {
467513
this._renderSelectedPart();
468514
});
469515

516+
eventsEngine.on(this._input(), addNamespace('compositionstart', MASK_EVENT_NAMESPACE), this._maskCompositionStartHandler.bind(this));
470517
eventsEngine.on(this._input(), addNamespace('compositionend', MASK_EVENT_NAMESPACE), this._maskCompositionEndHandler.bind(this));
471518

472519
if (this._useBeforeInputEvent()) {
@@ -683,13 +730,16 @@ class DateBoxMask extends DateBoxBase {
683730
}
684731
}
685732

733+
_maskCompositionStartHandler(): void {
734+
this._isIMEDigitProcessed = false;
735+
this._isIMECommitPending = false;
736+
}
737+
686738
_maskCompositionEndHandler(): void {
687739
this._input().val(this._getDisplayedText(this._maskValue));
688-
this._selectNextPart();
740+
this._caret(this._getActivePartProp('caret'));
689741

690-
this._maskInputHandler = (): void => {
691-
this._renderSelectedPart();
692-
};
742+
this._maskInputHandler = null;
693743
}
694744

695745
_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
@@ -21,7 +21,16 @@ QUnit.testStart(() => {
2121
$('#qunit-fixture').html('<div id=\'dateBox\'></div>');
2222
});
2323

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

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

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

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

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

0 commit comments

Comments
 (0)