Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 67 additions & 17 deletions packages/devextreme/js/__internal/ui/date_box/date_box.mask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { getDatePartIndexByPosition, renderDateParts } from './date_box.mask.par
const MASK_EVENT_NAMESPACE = 'dateBoxMask';
const FORWARD = 1;
const BACKWARD = -1;
const IME_DIGIT_CODE_REGEXP = /^(?:Digit|Numpad)(\d)$/;

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

_formatPattern?: string | null;

_pendingIMEDigit?: string | null;

_isIMEDigitProcessed?: boolean;

_isIMECommitPending?: boolean;

_supportedKeys(): Record<string, (e: KeyboardEvent) => unknown> {
const originalHandlers = super._supportedKeys();
const callOriginalHandler = (e: KeyboardEvent): unknown => {
Expand Down Expand Up @@ -171,7 +178,7 @@ class DateBoxMask extends DateBoxBase {
alt?: boolean;
}): boolean {
const data = e.originalEvent?.data;
return data?.length === 1 && Boolean(parseInt(data, 10));
return data?.length === 1 && !isNaN(parseInt(data, 10));
}

_useBeforeInputEvent(): boolean {
Expand All @@ -190,21 +197,32 @@ class DateBoxMask extends DateBoxBase {
}

_keyboardHandler(e: KeyboardKeyDownEvent): boolean {
let { key } = e.originalEvent;
const { key } = e.originalEvent;

const result = super._keyboardHandler(e);

if (!this._useMaskBehavior() || this._useBeforeInputEvent()) {
this._pendingIMEDigit = null;
this._isIMEDigitProcessed = false;
this._isIMECommitPending = false;

return result;
}

if (browser.chrome && e.key === 'Process' && e.code.startsWith('Digit')) {
key = e.code.replace('Digit', '');
this._processInputKey(key);
this._maskInputHandler = (): void => {
this._renderSelectedPart();
};
} else if (this._isSingleCharKey(e)) {
const chromiumDigitCodeMatch = IME_DIGIT_CODE_REGEXP.exec(e.code);

if (browser.chrome && e.key === 'Process' && chromiumDigitCodeMatch) {
const [, digit] = chromiumDigitCodeMatch;

this._pendingIMEDigit = digit;

return result;
}

this._pendingIMEDigit = null;
this._isIMEDigitProcessed = false;

if (this._isSingleCharKey(e)) {
this._keyInputHandler(e.originalEvent, key);
}

Expand Down Expand Up @@ -242,12 +260,40 @@ class DateBoxMask extends DateBoxBase {
return true;
}

_syncInputWithMask(): void {
this._input().val(this._getDisplayedText(this._maskValue));
this._caret(this._getActivePartProp('caret'));
}

_keyPressHandler(e: { originalEvent: InputEvent & KeyboardEvent }): void {
const { originalEvent: event } = e;
if (event?.inputType === 'insertCompositionText' && this._isSingleDigitKey(e)) {
this._processInputKey(event.data ?? '');
this._renderDisplayText(this._getDisplayedText(this._maskValue));
this._selectNextPart();

const isCompositionDigit = event?.inputType === 'insertCompositionText'
&& this._isSingleDigitKey(e);

const isIMECommitDigit = event?.inputType === 'insertText'
&& this._isSingleDigitKey(e)
&& this._isIMECommitPending;

if (isCompositionDigit && event.data) {
if (!this._isIMEDigitProcessed) {
this._processInputKey(event.data);
this._isIMEDigitProcessed = true;
this._isIMECommitPending = true;
}

this._syncInputWithMask();

return;
}

if (isIMECommitDigit) {
this._isIMECommitPending = false;
this._pendingIMEDigit = null;

this._syncInputWithMask();

return;
}
super._keyPressHandler(e);

Expand Down Expand Up @@ -455,6 +501,7 @@ class DateBoxMask extends DateBoxBase {
this._renderSelectedPart();
});

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

if (this._useBeforeInputEvent()) {
Expand Down Expand Up @@ -671,13 +718,16 @@ class DateBoxMask extends DateBoxBase {
}
}

_maskCompositionStartHandler(): void {
this._isIMEDigitProcessed = false;
this._isIMECommitPending = false;
}

_maskCompositionEndHandler(): void {
this._input().val(this._getDisplayedText(this._maskValue));
this._selectNextPart();
this._caret(this._getActivePartProp('caret'));

this._maskInputHandler = (): void => {
this._renderSelectedPart();
};
this._maskInputHandler = null;
}

_maskPasteHandler(e: DxEvent): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,16 @@ QUnit.testStart(() => {
$('#qunit-fixture').html('<div id=\'dateBox\'></div>');
});

const simulateIMEInput = function(eventsData) {
const insertNativeText = ($input, text) => {
const input = $input.get(0);
const start = input.selectionStart || input.value.length;
const end = input.selectionEnd || start;

input.value = input.value.slice(0, start) + text + input.value.slice(end);
input.setSelectionRange(start + text.length, start + text.length);
};

const simulateIMEInput = function(eventsData, shouldFireInsertText) {
this.$input.trigger($.Event('keydown', {
key: 'Process',
code: eventsData.keyDownCode,
Expand Down Expand Up @@ -56,6 +65,28 @@ const simulateIMEInput = function(eventsData) {
}));

this.$input.trigger($.Event('compositionend'));

if(shouldFireInsertText) {
insertNativeText(this.$input, eventsData.inputData);

this.$input.trigger($.Event('input', {
type: 'input',
originalEvent: $.Event('input', {
inputType: 'insertText',
isComposing: false,
data: eventsData.inputData,
}),
}));

this.$input.trigger($.Event('keyup', {
key: eventsData.inputData,
code: eventsData.keyDownCode,
originalEvent: $.Event('keyup', {
key: eventsData.inputData,
code: eventsData.keyDownCode,
}),
}));
}
};

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

test('Typing digits via Numpad IME with final insertText should process every composition cycle once (T1326628)', function(assert) {
this.instance.option({
displayFormat: 'MM/dd/yyyy',
value: new Date(2025, 9, 16),
});

this.keyboard.caret({ start: 0, end: 2 });

const eventsData = {
keyDownCode: 'Numpad1',
inputData: '1',
};

simulateIMEInput.call(this, eventsData, true);
assert.strictEqual(this.$input.val(), '01/16/2025', 'first month digit is applied');

simulateIMEInput.call(this, eventsData, true);
assert.strictEqual(this.$input.val(), '11/16/2025', 'second month digit is applied');

simulateIMEInput.call(this, eventsData, true);
assert.strictEqual(this.$input.val(), '11/01/2025', 'first day digit is applied');

simulateIMEInput.call(this, eventsData, true);
assert.strictEqual(this.$input.val(), '11/11/2025', 'second day digit is applied');

simulateIMEInput.call(this, eventsData, true);
assert.strictEqual(this.$input.val(), '11/11/2021', 'first year digit is applied');

simulateIMEInput.call(this, eventsData, true);
assert.strictEqual(this.$input.val(), '11/11/2011', 'second year digit is applied');

simulateIMEInput.call(this, eventsData, true);
assert.strictEqual(this.$input.val(), '11/11/2111', 'third year digit is applied');

simulateIMEInput.call(this, eventsData, true);
assert.strictEqual(this.$input.val(), '11/11/1111', 'fourth year digit is applied');
});

test('Final insertText after Numpad IME composition should not duplicate digit or corrupt mask value (T1326628)', function(assert) {
this.instance.option({
displayFormat: 'MM/dd/yyyy',
value: new Date(2025, 9, 16),
});

this.keyboard.caret({ start: 0, end: 2 });

simulateIMEInput.call(this, {
keyDownCode: 'Numpad1',
inputData: '1',
}, true);

simulateIMEInput.call(this, {
keyDownCode: 'Numpad1',
inputData: '1',
}, true);

assert.strictEqual(this.$input.val(), '11/16/2025', 'final insertText commit is ignored as duplicate IME commit');
});

test('Typing zero via Numpad IME composition with final insertText should be registered (T1326628)', function(assert) {
this.instance.option({
displayFormat: 'MM/dd/yyyy',
value: new Date(2012, 8, 5),
});

this.keyboard.caret({ start: 0, end: 2 });

simulateIMEInput.call(this, { keyDownCode: 'Numpad1', inputData: '1' }, true);
simulateIMEInput.call(this, { keyDownCode: 'Numpad0', inputData: '0' }, true);

assert.strictEqual(this.$input.val(), '10/05/2012', 'month is correctly set to 10 after typing "1" then "0" via Numpad IME');
});

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