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..d463514d80be 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 @@ -4,6 +4,8 @@ import { getFormat } from '@js/common/core/localization/ldml/date.format'; import { getRegExpInfo } from '@js/common/core/localization/ldml/date.parser'; import numberLocalization from '@js/common/core/localization/number'; import devices from '@js/core/devices'; +import domAdapter from '@js/core/dom_adapter'; +import type { dxElementWrapper } from '@js/core/renderer'; import browser from '@js/core/utils/browser'; import { clipboardText } from '@js/core/utils/dom'; import { fitIntoRange, inRange, sign } from '@js/core/utils/math'; @@ -15,6 +17,7 @@ import type { DateLike, Properties } from '@js/ui/date_box'; import dateLocalization from '@ts/core/localization/date'; import type { OptionChanged } from '@ts/core/widget/types'; import type { KeyboardKeyDownEvent } from '@ts/events/core/m_keyboard_processor'; +import type { ValueChangedEvent } from '@ts/ui/editor/editor'; import type { DxMouseWheelEvent } from '../scroll_view/types'; import type { DateBoxBaseProperties } from './date_box.base'; @@ -25,6 +28,7 @@ const MASK_EVENT_NAMESPACE = 'dateBoxMask'; const FORWARD = 1; const BACKWARD = -1; const IME_DIGIT_CODE_REGEXP = /^(?:Digit|Numpad)(\d)$/; +const IME_BACKSPACE_INPUT_TYPE = 'deleteContentBackward'; export interface DateBoxMaskProperties extends Properties { emptyDateValue?: Date; @@ -52,6 +56,14 @@ class DateBoxMask extends DateBoxBase { _isIMECommitPending?: boolean; + _isComposing?: boolean; + + _isWindowBlurred?: boolean; + + _hasUserTyped?: boolean; + + _isClearingValue?: boolean; + _supportedKeys(): Record boolean | undefined> { const originalHandlers = super._supportedKeys(); const callOriginalHandler = (e: KeyboardEvent): boolean | undefined => { @@ -307,6 +319,18 @@ class DateBoxMask extends DateBoxBase { return; } + + if (this._useMaskBehavior() + && event?.inputType === IME_BACKSPACE_INPUT_TYPE + && this._isComposing + && this._input().val() !== '' + ) { + this._revertPart(BACKWARD); + this._syncInputWithMask(); + + return; + } + super._keyPressHandler(e); if (this._maskInputHandler) { @@ -316,6 +340,8 @@ class DateBoxMask extends DateBoxBase { } _processInputKey(key: string): void { + this._hasUserTyped = true; + const hasMultipleParts = this._dateParts?.length > 1; if (this._isAllSelected() && hasMultipleParts) { @@ -731,11 +757,13 @@ class DateBoxMask extends DateBoxBase { } _maskCompositionStartHandler(): void { + this._isComposing = true; this._isIMEDigitProcessed = false; this._isIMECommitPending = false; } _maskCompositionEndHandler(): void { + this._isComposing = false; this._input().val(this._getDisplayedText(this._maskValue)); this._caret(this._getActivePartProp('caret')); @@ -792,10 +820,35 @@ class DateBoxMask extends DateBoxBase { } } + _clearValueHandler(e: ValueChangedEvent & DxEvent): void { + try { + super._clearValueHandler(e); + } finally { + this._isClearingValue = false; + } + } + + _focusInHandler(e: DxEvent & { relatedTarget: Element | dxElementWrapper }): void { + super._focusInHandler(e); + if (this._useMaskBehavior() && !e.isDefaultPrevented() && !this._isClearingValue) { + const focusCameFromAnotherElement = isDefined(e.relatedTarget); + const shouldSelectFirstPart = !this._isWindowBlurred + && (focusCameFromAnotherElement || this._hasUserTyped); + + if (shouldSelectFirstPart) { + this._selectFirstPart(); + } + + this._hasUserTyped = false; + this._isWindowBlurred = false; + } + } + _focusOutHandler(e: DxEvent): void { const shouldFireChangeEvent = this._useMaskBehavior() && !e.isDefaultPrevented(); if (shouldFireChangeEvent) { + this._isWindowBlurred = !domAdapter.getDocument().hasFocus(); this._fireChangeEvent(); super._focusOutHandler(e); } else { 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..29e956775ebc 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 @@ -787,6 +787,54 @@ module('Keyboard navigation', setupModule, () => { assert.strictEqual(this.instance.option('value'), null, 'value has been cleared'); }); + 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), // Oct 16, 2025; text = '10/16/2025' + }); + + this.$input.get(0).focus(); + + this.$input.trigger($.Event('compositionstart')); + + this.$input.trigger($.Event('input', { + type: 'input', + originalEvent: $.Event('input', { + inputType: 'deleteContentBackward', + }) + })); + + assert.ok(this.instance.option('text').length > 0, 'text is not cleared after IME composition backspace'); + assert.deepEqual(this.keyboard.caret(), { start: 0, end: 2 }, 'first date part (month) is still active'); + assert.strictEqual(this.instance.option('text'), '01/16/2025', 'month part is reset to minimum value, other parts unchanged'); + }); + + test('deleteContentBackward input event during composition with all text selected should clear the value (Chinese MS IME composition backspace) (T1331089)', function(assert) { + this.instance.option({ + displayFormat: 'MM/dd/yyyy', + value: new Date(2025, 9, 16), // Oct 16, 2025; text = '10/16/2025' + }); + + this.$input.get(0).focus(); + this.keyboard.caret({ start: 0, end: 10 }); + + this.$input.trigger($.Event('compositionstart')); + + this.$input.val(''); + + this.$input.trigger($.Event('input', { + type: 'input', + originalEvent: $.Event('input', { + inputType: 'deleteContentBackward', + }) + })); + + this.$input.change(); + + assert.strictEqual(this.instance.option('text'), '', 'text has been cleared'); + assert.strictEqual(this.instance.option('value'), null, 'value has been cleared'); + }); + QUnit.testInActiveWindow('focusout should clear search value', function(assert) { this.keyboard.type('1'); assert.strictEqual(this.instance.option('text'), 'January 10 2012', 'text has been changed'); @@ -798,6 +846,21 @@ module('Keyboard navigation', setupModule, () => { assert.deepEqual(this.keyboard.caret(), { start: 9, end: 11 }, 'first group has been filled again'); }); + QUnit.testInActiveWindow('first part should be active when re-focusing after all parts are completed (T1331089)', function(assert) { + this.instance.option({ + displayFormat: 'MM/dd/yyyy', + value: new Date(2025, 0, 1), + }); + + this.keyboard.type('10162025'); // Oct 16, 2025 + + this.$input.get(0).blur(); + this.$input.get(0).focus(); + + assert.deepEqual(this.keyboard.caret(), { start: 0, end: 2 }, 'month part is selected after re-focusing'); + + }); + test('enter should clear search value', function(assert) { this.keyboard.type('1');