From 568ad47b09154323bda4b8defd8fd5d332642d0c Mon Sep 17 00:00:00 2001 From: Raushen Date: Thu, 10 Sep 2026 15:16:20 +0300 Subject: [PATCH 1/2] T1334896 --- .../ui/__tests__/__mock__/model/date_box.ts | 19 ++++ .../__mock__/model/drop_down_editor.ts | 4 +- .../__tests__/__mock__/model/text_editor.ts | 22 +++++ .../ui/date_box/__tests__/date_box.test.ts | 93 +++++++++++++++++++ .../__internal/ui/date_box/date_box.base.ts | 23 +++++ 5 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 packages/devextreme/js/__internal/ui/__tests__/__mock__/model/date_box.ts create mode 100644 packages/devextreme/js/__internal/ui/__tests__/__mock__/model/text_editor.ts create mode 100644 packages/devextreme/js/__internal/ui/date_box/__tests__/date_box.test.ts diff --git a/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/date_box.ts b/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/date_box.ts new file mode 100644 index 000000000000..ea93bf0fb75c --- /dev/null +++ b/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/date_box.ts @@ -0,0 +1,19 @@ +import DateBox from '@ts/ui/date_box/date_box'; + +import { DropDownEditorModel } from './drop_down_editor'; + +const CLASSES = { + calendarCell: 'dx-calendar-cell', +}; + +export class DateBoxModel extends DropDownEditorModel { + public getInstance(): DateBox { + return DateBox.getInstance(this.root); + } + + public getCalendarCells(): HTMLElement[] { + const overlayContent = this.getOverlay().getElement(); + + return Array.from(overlayContent?.querySelectorAll(`.${CLASSES.calendarCell}`) ?? []); + } +} diff --git a/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/drop_down_editor.ts b/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/drop_down_editor.ts index 6e20ff73eb9d..98f53ecf1ed4 100644 --- a/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/drop_down_editor.ts +++ b/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/drop_down_editor.ts @@ -1,5 +1,5 @@ -import { BaseModel } from './base_model'; import { OverlayModel } from './overlay'; +import { TextEditorModel } from './text_editor'; const CLASSES = { button: 'dx-dropdowneditor-button', @@ -10,7 +10,7 @@ const ATTR = { popupContent: 'aria-owns', }; -export class DropDownEditorModel extends BaseModel { +export class DropDownEditorModel extends TextEditorModel { public open(): void { const button = this.root.querySelector(`.${CLASSES.button}`); const target = button ?? this.root.querySelector(`.${CLASSES.inputWrapper}`); diff --git a/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/text_editor.ts b/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/text_editor.ts new file mode 100644 index 000000000000..7bf8494d7d60 --- /dev/null +++ b/packages/devextreme/js/__internal/ui/__tests__/__mock__/model/text_editor.ts @@ -0,0 +1,22 @@ +import { BaseModel } from './base_model'; + +const CLASSES = { + input: 'dx-texteditor-input', +}; + +export class TextEditorModel extends BaseModel { + public getInputElement(): HTMLInputElement { + return this.root.querySelector(`.${CLASSES.input}`) as HTMLInputElement; + } + + public clearInput(): void { + const input = this.getInputElement(); + + input.value = ''; + input.dispatchEvent(new Event('input', { bubbles: true })); + } + + public blurInput(): void { + this.getInputElement().dispatchEvent(new FocusEvent('focusout', { bubbles: true })); + } +} diff --git a/packages/devextreme/js/__internal/ui/date_box/__tests__/date_box.test.ts b/packages/devextreme/js/__internal/ui/date_box/__tests__/date_box.test.ts new file mode 100644 index 000000000000..30fb1d3e80d6 --- /dev/null +++ b/packages/devextreme/js/__internal/ui/date_box/__tests__/date_box.test.ts @@ -0,0 +1,93 @@ +import { + afterEach, beforeAll, describe, expect, it, jest, +} from '@jest/globals'; +import fx from '@js/common/core/animation/fx'; +import $ from '@js/core/renderer'; +import { DateBoxModel } from '@ts/ui/__tests__/__mock__/model/date_box'; + +import DateBox from '../date_box'; + +const dateBoxes: DateBox[] = []; + +const createDateBox = (options = {}): DateBoxModel => { + const element = $('
').appendTo(document.body).get(0) as HTMLElement; + // @ts-expect-error DOMComponent constructor is not typed for direct instantiation + const instance: DateBox = new DateBox(element, { + type: 'date', + pickerType: 'calendar', + ...options, + }); + + dateBoxes.push(instance); + + return new DateBoxModel(element); +}; + +describe('DateBox commits the input text on focus out when the browser fires no change event', () => { + beforeAll(() => { + fx.off = true; + }); + + afterEach(() => { + dateBoxes.forEach((instance) => instance.dispose()); + dateBoxes.length = 0; + document.body.innerHTML = ''; + }); + + it('resets the value when the input is cleared after a calendar pick (T1334896)', () => { + const dateBox = createDateBox(); + const input = dateBox.getInputElement(); + + dateBox.open(); + dateBox.getCalendarCells()[0].click(); + expect(input.value).not.toBe(''); + + dateBox.clearInput(); + dateBox.blurInput(); + + expect(dateBox.getInstance().option('value')).toBeNull(); + expect(input.value).toBe(''); + }); + + it('resets the value in mask mode when the input is cleared after a calendar pick (T1334896)', () => { + const dateBox = createDateBox({ useMaskBehavior: true }); + + dateBox.open(); + dateBox.getCalendarCells()[0].click(); + + dateBox.clearInput(); + dateBox.blurInput(); + + expect(dateBox.getInstance().option('value')).toBeNull(); + }); + + it('commits the text once when the browser does fire the change event (T1334896)', () => { + const onValueChanged = jest.fn(); + const dateBox = createDateBox({ onValueChanged }); + const input = dateBox.getInputElement(); + + dateBox.open(); + dateBox.getCalendarCells()[0].click(); + onValueChanged.mockClear(); + + dateBox.clearInput(); + input.dispatchEvent(new Event('change', { bubbles: true })); + dateBox.blurInput(); + + expect(dateBox.getInstance().option('value')).toBeNull(); + expect(onValueChanged).toHaveBeenCalledTimes(1); + }); + + it('keeps the value when valueChangeEvent excludes change (T1334896)', () => { + const dateBox = createDateBox({ valueChangeEvent: 'paste' }); + + dateBox.open(); + dateBox.getCalendarCells()[0].click(); + const pickedValue = dateBox.getInstance().option('value'); + + dateBox.clearInput(); + dateBox.blurInput(); + + expect(dateBox.getInstance().option('value')).toEqual(pickedValue); + }); +}); diff --git a/packages/devextreme/js/__internal/ui/date_box/date_box.base.ts b/packages/devextreme/js/__internal/ui/date_box/date_box.base.ts index e9829224e8ae..e9b95b7ae8b9 100644 --- a/packages/devextreme/js/__internal/ui/date_box/date_box.base.ts +++ b/packages/devextreme/js/__internal/ui/date_box/date_box.base.ts @@ -1,3 +1,4 @@ +import eventsEngine from '@js/common/core/events/core/events_engine'; import dateLocalization from '@js/common/core/localization/date'; import messageLocalization from '@js/common/core/localization/message'; import config from '@js/core/config'; @@ -583,6 +584,28 @@ class DateBox< : uiDateUtils.FORMATS_MAP[mode] as string | null; } + _focusOutHandler(e: DxEvent): void { + if (this._shouldCommitTextOnFocusOut()) { + eventsEngine.triggerHandler(this._input(), { type: 'change' }); + } + + super._focusOutHandler(e); + } + + _shouldCommitTextOnFocusOut(): boolean { + const { text, valueChangeEvent } = this.option(); + const includesChangeEvent = valueChangeEvent?.split(' ').includes('change'); + + if (!includesChangeEvent) { + return false; + } + + const currentValue = this.getDateOption('value'); + const displayedText = this._getDisplayedText(currentValue) ?? ''; + + return (text ?? '') !== displayedText; + } + _valueChangeEventHandler( e: InteractionEvent, ): void { From bdb9c38ae198cfbf528e5d483b54f01f6d0f20fa Mon Sep 17 00:00:00 2001 From: Raushen Date: Fri, 11 Sep 2026 01:57:18 +0300 Subject: [PATCH 2/2] Fix test --- .../ui/date_box/__tests__/date_box.test.ts | 21 +++++++++++++++++++ .../__internal/ui/date_box/date_box.base.ts | 6 +++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/ui/date_box/__tests__/date_box.test.ts b/packages/devextreme/js/__internal/ui/date_box/__tests__/date_box.test.ts index 30fb1d3e80d6..f97067713dd7 100644 --- a/packages/devextreme/js/__internal/ui/date_box/__tests__/date_box.test.ts +++ b/packages/devextreme/js/__internal/ui/date_box/__tests__/date_box.test.ts @@ -78,6 +78,27 @@ describe('DateBox commits the input text on focus out when the browser fires no expect(onValueChanged).toHaveBeenCalledTimes(1); }); + it('does not validate the same text again on focus out (T1334896)', () => { + const onOptionChanged = jest.fn<(e: { name: string }) => void>(); + const dateBox = createDateBox({ onOptionChanged }); + const input = dateBox.getInputElement(); + + input.value = 'not a date'; + input.dispatchEvent(new Event('input', { bubbles: true })); + input.dispatchEvent(new Event('change', { bubbles: true })); + + const validationChangesAfterChange = onOptionChanged.mock.calls + .filter(([{ name }]) => name === 'validationError').length; + + dateBox.blurInput(); + + const validationChangesAfterBlur = onOptionChanged.mock.calls + .filter(([{ name }]) => name === 'validationError').length; + + expect(dateBox.getInstance().option('isValid')).toBe(false); + expect(validationChangesAfterBlur).toBe(validationChangesAfterChange); + }); + it('keeps the value when valueChangeEvent excludes change (T1334896)', () => { const dateBox = createDateBox({ valueChangeEvent: 'paste' }); diff --git a/packages/devextreme/js/__internal/ui/date_box/date_box.base.ts b/packages/devextreme/js/__internal/ui/date_box/date_box.base.ts index e9b95b7ae8b9..1e2b58fb1e08 100644 --- a/packages/devextreme/js/__internal/ui/date_box/date_box.base.ts +++ b/packages/devextreme/js/__internal/ui/date_box/date_box.base.ts @@ -115,6 +115,8 @@ class DateBox< _pickerType?: DatePickerType; + _handledText?: string; + _storedPadding?: number; _userOptions?: DateBoxBaseProperties; @@ -596,7 +598,7 @@ class DateBox< const { text, valueChangeEvent } = this.option(); const includesChangeEvent = valueChangeEvent?.split(' ').includes('change'); - if (!includesChangeEvent) { + if (!includesChangeEvent || text === this._handledText) { return false; } @@ -612,6 +614,8 @@ class DateBox< const { text, type = 'date', validationError } = this.option(); const currentValue = this.getDateOption('value'); + this._handledText = text; + if (text === this._getDisplayedText(currentValue)) { this._recallInternalValidation(currentValue, validationError); return;