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
Original file line number Diff line number Diff line change
@@ -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<HTMLElement>(`.${CLASSES.calendarCell}`) ?? []);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BaseModel } from './base_model';
import { OverlayModel } from './overlay';
import { TextEditorModel } from './text_editor';

const CLASSES = {
button: 'dx-dropdowneditor-button',
Expand All @@ -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<HTMLElement>(`.${CLASSES.button}`);
const target = button ?? this.root.querySelector<HTMLElement>(`.${CLASSES.inputWrapper}`);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
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 = $('<div>').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('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' });

dateBox.open();
dateBox.getCalendarCells()[0].click();
const pickedValue = dateBox.getInstance().option('value');

dateBox.clearInput();
dateBox.blurInput();

expect(dateBox.getInstance().option('value')).toEqual(pickedValue);
});
});
27 changes: 27 additions & 0 deletions packages/devextreme/js/__internal/ui/date_box/date_box.base.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -114,6 +115,8 @@ class DateBox<

_pickerType?: DatePickerType;

_handledText?: string;

_storedPadding?: number;

_userOptions?: DateBoxBaseProperties;
Expand Down Expand Up @@ -583,12 +586,36 @@ class DateBox<
: uiDateUtils.FORMATS_MAP[mode] as string | null;
}

_focusOutHandler(e: DxEvent): void {
if (this._shouldCommitTextOnFocusOut()) {
eventsEngine.triggerHandler(this._input(), { type: 'change' });
}
Comment on lines +590 to +592

super._focusOutHandler(e);
}

_shouldCommitTextOnFocusOut(): boolean {
const { text, valueChangeEvent } = this.option();
const includesChangeEvent = valueChangeEvent?.split(' ').includes('change');

if (!includesChangeEvent || text === this._handledText) {
return false;
}

const currentValue = this.getDateOption('value');
const displayedText = this._getDisplayedText(currentValue) ?? '';

return (text ?? '') !== displayedText;
}

_valueChangeEventHandler(
e: InteractionEvent,
): void {
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;
Expand Down
Loading