diff --git a/packages/devextreme/js/__internal/ui/date_box/date_box.mask.ts b/packages/devextreme/js/__internal/ui/date_box/date_box.mask.ts index b34c85cf1b92..a0b0125ff07f 100644 --- a/packages/devextreme/js/__internal/ui/date_box/date_box.mask.ts +++ b/packages/devextreme/js/__internal/ui/date_box/date_box.mask.ts @@ -52,6 +52,8 @@ class DateBoxMask extends DateBoxBase { _isIMECommitPending?: boolean; + _wasAllSelectedBeforeComposition = false; + _supportedKeys(): Record boolean | undefined> { const originalHandlers = super._supportedKeys(); const callOriginalHandler = (e: KeyboardEvent): boolean | undefined => { @@ -252,16 +254,6 @@ class DateBoxMask extends DateBoxBase { }; } - const isBackwardDeletion = inputType === 'deleteContentBackward'; - const isForwardDeletion = inputType === 'deleteContentForward'; - if (isBackwardDeletion || isForwardDeletion) { - const direction = isBackwardDeletion ? BACKWARD : FORWARD; - this._maskInputHandler = (): void => { - this._revertPart(); - this._selectNextPart(direction); - }; - } - if (!this._useMaskBehavior() || !this._isSingleCharKey(e)) { return false; } @@ -280,6 +272,26 @@ class DateBoxMask extends DateBoxBase { _keyPressHandler(e: { originalEvent: InputEvent & KeyboardEvent }): void { const { originalEvent: event } = e; + const isBackwardDeletion = event?.inputType === 'deleteContentBackward'; + const isForwardDeletion = event?.inputType === 'deleteContentForward'; + + if (this._useMaskBehavior() && (isBackwardDeletion || isForwardDeletion)) { + const isInputCleared = this._input().val() === ''; + + if (this._wasAllSelectedBeforeComposition || isInputCleared) { + super._keyPressHandler(e); + } else { + const direction = isBackwardDeletion ? BACKWARD : FORWARD; + + this._revertPart(direction); + this._syncInputWithMask(); + } + + this._wasAllSelectedBeforeComposition = false; + + return; + } + const isCompositionDigit = event?.inputType === 'insertCompositionText' && this._isSingleDigitKey(e); @@ -289,9 +301,15 @@ class DateBoxMask extends DateBoxBase { if (isCompositionDigit && event.data) { if (!this._isIMEDigitProcessed) { + if (this._wasAllSelectedBeforeComposition || this._isAllSelected()) { + this._clearSearchValue(); + this._selectFirstPart(); + } + this._processInputKey(event.data); this._isIMEDigitProcessed = true; this._isIMECommitPending = true; + this._wasAllSelectedBeforeComposition = false; } this._syncInputWithMask(); @@ -302,17 +320,21 @@ class DateBoxMask extends DateBoxBase { if (isIMECommitDigit) { this._isIMECommitPending = false; this._pendingIMEDigit = null; + this._wasAllSelectedBeforeComposition = false; this._syncInputWithMask(); return; } + super._keyPressHandler(e); if (this._maskInputHandler) { this._maskInputHandler(); this._maskInputHandler = null; } + + this._wasAllSelectedBeforeComposition = false; } _processInputKey(key: string): void { @@ -733,6 +755,7 @@ class DateBoxMask extends DateBoxBase { _maskCompositionStartHandler(): void { this._isIMEDigitProcessed = false; this._isIMECommitPending = false; + this._wasAllSelectedBeforeComposition = this._isAllSelected(); } _maskCompositionEndHandler(): void { @@ -740,6 +763,7 @@ class DateBoxMask extends DateBoxBase { this._caret(this._getActivePartProp('caret')); this._maskInputHandler = null; + this._wasAllSelectedBeforeComposition = false; } _maskPasteHandler(e: DxEvent): void { diff --git a/packages/devextreme/js/__internal/ui/tag_box.ts b/packages/devextreme/js/__internal/ui/tag_box.ts index 7ad9129f7f49..6fd3eb91e9e3 100644 --- a/packages/devextreme/js/__internal/ui/tag_box.ts +++ b/packages/devextreme/js/__internal/ui/tag_box.ts @@ -729,7 +729,7 @@ class TagBox< this.option('text', ''); } - _focusInHandler(e: DxEvent & { relatedTarget: Element | dxElementWrapper }): void { + _focusInHandler(e: DxEvent): void { if (!this._preventNestedFocusEvent(e)) { this._scrollContainer('end'); } diff --git a/packages/devextreme/js/__internal/ui/text_box/text_editor.base.ts b/packages/devextreme/js/__internal/ui/text_box/text_editor.base.ts index 7d8a76d2a93f..92d031764f28 100644 --- a/packages/devextreme/js/__internal/ui/text_box/text_editor.base.ts +++ b/packages/devextreme/js/__internal/ui/text_box/text_editor.base.ts @@ -860,9 +860,7 @@ class TextEditorBase< return this.$element(); } - _focusInHandler(event: DxEvent & { - relatedTarget: Element | dxElementWrapper; - }): void { + _focusInHandler(event: DxEvent): void { this._preventNestedFocusEvent(event); super._focusInHandler(event); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.mask.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.mask.tests.js index e43ab2f2eb97..69b3b7bd9bbb 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.mask.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.mask.tests.js @@ -798,6 +798,26 @@ module('Keyboard navigation', setupModule, () => { assert.deepEqual(this.keyboard.caret(), { start: 9, end: 11 }, 'first group has been filled again'); }); + test('Windows Bopomofo IME changes selection to last active part before insertCompositionText — first date part should still be updated (T1331089)', function(assert) { + this.instance.option({ + displayFormat: 'MM/dd/yyyy', + value: new Date(2025, 9, 16), + }); + + this.keyboard.type('1016'); + + this.keyboard.caret({ start: 0, end: this.instance.option('text').length }); + + this.$input.trigger($.Event('compositionstart')); + + this.keyboard.caret({ start: 6, end: 10 }); + + this.keyboard.input('1', 'insertCompositionText'); + + assert.strictEqual(this.instance.option('text'), '01/16/2025', 'month (first part) is updated, not year'); + assert.deepEqual(this.keyboard.caret(), { start: 0, end: 2 }, 'month part is active'); + }); + test('enter should clear search value', function(assert) { this.keyboard.type('1'); @@ -1355,6 +1375,53 @@ module('Search', setupModule, () => { assert.strictEqual(this.$input.val(), '2012/10/10', 'year was not changed'); }); + + test('deleteContentBackward input event should revert the active date part to its minimum value without clearing the value (Chinese MS IME composition backspace) (T1331089)', function(assert) { + this.instance.option({ + displayFormat: 'MM/dd/yyyy', + value: new Date(2025, 9, 16), + }); + + this.$input.get(0).focus(); + + this.$input.trigger($.Event('compositionstart')); + + this.keyboard.input(null, 'deleteContentBackward'); + + assert.strictEqual(this.instance.option('text'), '01/16/2025', 'text is not cleared, month is reset'); + assert.deepEqual(this.keyboard.caret(), { start: 0, end: 2 }, 'first date part (month) is still active'); + }); + + test('deleteContentBackward during IME composition on non-first date part should revert that part to minimum without affecting other parts (T1331089)', function(assert) { + this.instance.option({ + displayFormat: 'MM/dd/yyyy', + value: new Date(2025, 9, 16), + }); + + this.keyboard.type('10'); + + this.$input.trigger($.Event('compositionstart')); + + this.keyboard.input(null, 'deleteContentBackward'); + + assert.strictEqual(this.instance.option('text'), '10/01/2025', 'day is reverted to minimum, month is unchanged'); + }); + + test('IME digit on non-first date part without all-selected state should update that specific part and not reset to first (T1331089 regression)', function(assert) { + this.instance.option({ + displayFormat: 'MM/dd/yyyy', + value: new Date(2025, 9, 16), + }); + + this.keyboard.type('10'); + + this.$input.trigger($.Event('compositionstart')); + + this.keyboard.input('2', 'insertCompositionText'); + + assert.strictEqual(this.instance.option('text'), '10/02/2025', 'day part is updated, month is not reset to first'); + assert.deepEqual(this.keyboard.caret(), { start: 3, end: 5 }, 'day part remains active'); + }); }); module('Date AM/PM Handling', setupModule, () => {