Skip to content
Open
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
48 changes: 48 additions & 0 deletions packages/devextreme/js/__internal/ui/date_box/date_box.mask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 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';
Expand All @@ -15,6 +16,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';
Expand All @@ -25,6 +27,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;
Expand Down Expand Up @@ -52,6 +55,14 @@ class DateBoxMask extends DateBoxBase {

_isIMECommitPending?: boolean;

_isComposing?: boolean;

_isReFocusing?: boolean;

_hasUserTyped?: boolean;

_isClearingValue?: boolean;

_supportedKeys(): Record<string, (e: KeyboardEvent) => boolean | undefined> {
const originalHandlers = super._supportedKeys();
const callOriginalHandler = (e: KeyboardEvent): boolean | undefined => {
Expand Down Expand Up @@ -307,6 +318,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) {
Expand All @@ -316,6 +339,8 @@ class DateBoxMask extends DateBoxBase {
}

_processInputKey(key: string): void {
this._hasUserTyped = true;

const hasMultipleParts = this._dateParts?.length > 1;

if (this._isAllSelected() && hasMultipleParts) {
Expand Down Expand Up @@ -731,11 +756,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'));

Expand Down Expand Up @@ -792,10 +819,31 @@ 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) {
if (this._isReFocusing && this._hasUserTyped) {
this._selectFirstPart();
this._hasUserTyped = false;
}

this._isReFocusing = false;
}
}

_focusOutHandler(e: DxEvent): void {
const shouldFireChangeEvent = this._useMaskBehavior() && !e.isDefaultPrevented();

if (shouldFireChangeEvent) {
this._isReFocusing = true;
this._fireChangeEvent();
super._focusOutHandler(e);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');

Expand Down
Loading