diff --git a/apps/demos/Demos/Chat/AIAndChatbotIntegration/Vue/App.vue b/apps/demos/Demos/Chat/AIAndChatbotIntegration/Vue/App.vue index 420dc2ac68e7..0019a8b15050 100644 --- a/apps/demos/Demos/Chat/AIAndChatbotIntegration/Vue/App.vue +++ b/apps/demos/Demos/Chat/AIAndChatbotIntegration/Vue/App.vue @@ -44,7 +44,7 @@ - + diff --git a/apps/demos/Demos/Pagination/Overview/Angular/app/app.component.html b/apps/demos/Demos/Pagination/Overview/Angular/app/app.component.html index 175a0142f5e6..1445f6a91313 100644 --- a/apps/demos/Demos/Pagination/Overview/Angular/app/app.component.html +++ b/apps/demos/Demos/Pagination/Overview/Angular/app/app.component.html @@ -3,9 +3,8 @@ class="employees" [ngClass]="pageSize === 4 ? 'employees--forth' : 'employees--six'" > - @for (employee of pageEmployees; track employee.ID) { - - } + + { test('Chat: showAvatar set to false', async (t) => { const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + await testScreenshot(t, takeScreenshot, 'Avatar with showAvatar set to false.png', { element: '#chat' }); await t diff --git a/e2e/testcafe-devextreme/tests/chat/etalons/Typing indicator with emptyview (material-blue-light).png b/e2e/testcafe-devextreme/tests/chat/etalons/Typing indicator with emptyview (material-blue-light).png index 32ce7457fc38..9b19f43ba554 100644 Binary files a/e2e/testcafe-devextreme/tests/chat/etalons/Typing indicator with emptyview (material-blue-light).png and b/e2e/testcafe-devextreme/tests/chat/etalons/Typing indicator with emptyview (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/chat/messageBox.ts b/e2e/testcafe-devextreme/tests/chat/messageBox.ts index 8bbfd5bdc130..db29be7f1859 100644 --- a/e2e/testcafe-devextreme/tests/chat/messageBox.ts +++ b/e2e/testcafe-devextreme/tests/chat/messageBox.ts @@ -13,6 +13,7 @@ test('Chat: messagebox', async (t) => { const { takeScreenshot, compareResults } = createScreenshotsComparer(t); const chat = new Chat('#chat'); + const shortText = getShortText(); const longText = getLongText(false, 5); diff --git a/e2e/testcafe-devextreme/tests/chat/messageList.ts b/e2e/testcafe-devextreme/tests/chat/messageList.ts index ad5d7c36a1af..2c5a35f3cbf6 100644 --- a/e2e/testcafe-devextreme/tests/chat/messageList.ts +++ b/e2e/testcafe-devextreme/tests/chat/messageList.ts @@ -116,7 +116,6 @@ test('Messagelist appearance with scrollbar', async (t) => { test('Messagelist should scrolled to the latest messages after being rendered inside an invisible element', async (t) => { const { takeScreenshot, compareResults } = createScreenshotsComparer(t); - const tabPanel = new TabPanel('#container'); await t @@ -185,8 +184,8 @@ test('Messagelist with loadindicator appearance on initial loading', async (t) = }); test('Messagelist with messageTemplate', async (t) => { - const chat = new Chat('#container'); const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const chat = new Chat('#container'); await testScreenshot(t, takeScreenshot, 'Messagelist with message template.png', { element: '#container' }); diff --git a/e2e/testcafe-devextreme/tests/chat/typingIndicator.ts b/e2e/testcafe-devextreme/tests/chat/typingIndicator.ts index 3f1e19c52539..1b4bc4573c42 100644 --- a/e2e/testcafe-devextreme/tests/chat/typingIndicator.ts +++ b/e2e/testcafe-devextreme/tests/chat/typingIndicator.ts @@ -13,9 +13,10 @@ fixture.disablePageReloads`ChatTypingIndicator` test('Chat: typing indicator with emptyview', async (t) => { const { takeScreenshot, compareResults } = createScreenshotsComparer(t); - const chat = new Chat('#container'); + await chat.repaint(); + await testScreenshot(t, takeScreenshot, 'Typing indicator with emptyview.png', { element: '#container', shouldTestInCompact: true, diff --git a/e2e/testcafe-devextreme/tests/scheduler/dragAndDrop/T1263508.ts b/e2e/testcafe-devextreme/tests/scheduler/dragAndDrop/T1263508.ts new file mode 100644 index 000000000000..3dc1a4868bd2 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/scheduler/dragAndDrop/T1263508.ts @@ -0,0 +1,97 @@ +import Scheduler from 'devextreme-testcafe-models/scheduler'; +import { ClientFunction, Selector } from 'testcafe'; +import { MouseAction, MouseUpEvents } from '../../../helpers/mouseUpEvents'; +import { createWidget } from '../../../helpers/createWidget'; +import url from '../../../helpers/getPageUrl'; + +fixture.disablePageReloads`Scheduler Drag-and-Drop Fix` + .page(url(__dirname, '../../container.html')); + +const DRAGGABLE_ITEM_CLASS = 'dx-card'; +const draggingGroupName = 'appointmentsGroup'; + +const initList = ClientFunction(() => { + $('
', { id: 'list' }).appendTo('#parentContainer'); +}); + +const addTasksToList = ClientFunction((tasks) => { + tasks.forEach((task) => { + $('
', { + class: 'dx-card', + text: task.text, + }).appendTo('#list'); + }); +}); + +const createItemElement = async (task) => { + await createWidget('dxDraggable', { + group: draggingGroupName, + data: task, + clone: true, + onDragStart(e) { + e.itemData = e.fromData; + }, + }, `.${DRAGGABLE_ITEM_CLASS}:contains(${task.text})`); +}; + +test('Scheduler - The \'Cannot read properties of undefined (reading \'getTime\')\' error is thrown on an attempt to drag an outside element if the previous drag operation was canceled', async (t) => { + const scheduler = new Scheduler('#container'); + const draggableAppointment = scheduler.getAppointment('Book').element; + const targetCell = scheduler.getDateTableCell(5, 0); + const draggableItem = Selector(`.${DRAGGABLE_ITEM_CLASS}`).withText('Brochures'); + + await t.expect(scheduler.element.exists).ok(); + + await MouseUpEvents.disable(MouseAction.dragToElement); + + await t + .dragToElement(draggableAppointment, targetCell) + .pressKey('esc'); + + await MouseUpEvents.enable(MouseAction.dragToElement); + + await t + .expect(draggableItem.exists) + .ok() + .dragToElement(draggableItem, targetCell); + + const newAppointment = scheduler.getAppointment('Brochures'); + + await t + .expect(newAppointment.element.exists) + .ok(); +}).before(async () => { + const tasks = [ + { text: 'Brochures' }, + ]; + + await initList(); + await addTasksToList(tasks); + await Promise.all(tasks.map((task) => createItemElement(task))); + await createWidget('dxScheduler', { + timeZone: 'America/Los_Angeles', + dataSource: [ + { + text: 'Book', + startDate: new Date('2021-04-26T19:00:00.000Z'), + endDate: new Date('2021-04-26T20:00:00.000Z'), + }, + ], + currentDate: new Date(2021, 3, 26), + startDayHour: 9, + height: 600, + editing: true, + appointmentDragging: { + group: draggingGroupName, + onDragEnd(e) { + e.cancel = e.event.ctrlKey; + }, + onRemove(e) { + e.component.deleteAppointment(e.itemData); + }, + onAdd(e) { + e.component.addAppointment(e.itemData); + }, + }, + }); +}); diff --git a/e2e/testcafe-devextreme/tests/treeList/editing/editing.ts b/e2e/testcafe-devextreme/tests/treeList/editing/editing.ts new file mode 100644 index 000000000000..eaf773334b51 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/treeList/editing/editing.ts @@ -0,0 +1,59 @@ +import TreeList from 'devextreme-testcafe-models/treeList'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; + +fixture`Treelist - Editing`.page(url(__dirname, '../../container.html')); + +// T1247158 +test('TreeList - Insertafterkey doesn\'t work on children nodes', async (t) => { + const treeList = new TreeList('#container'); + const expectedInsertedRowIndex = 2; + + await t + .click(treeList.getDataCell(1, 0).element) + .pressKey('ctrl+enter') + .expect(treeList.getDataRow(expectedInsertedRowIndex).isInserted) + .ok(); +}).before(async () => createWidget('dxTreeList', { + dataSource: [ + { + ID: 1, + Head_ID: -1, + Full_Name: 'John Heart', + }, + { + ID: 2, + Head_ID: 1, + Full_Name: 'Samantha Bright', + }, + ], + rootValue: -1, + keyExpr: 'ID', + parentIdExpr: 'Head_ID', + columns: ['Full_Name'], + editing: { + mode: 'batch', + allowAdding: true, + allowUpdating: true, + useIcons: true, + }, + focusedRowEnabled: true, + expandedRowKeys: [1], + onKeyDown(e) { + if (e.event.ctrlKey && e.event.key === 'Enter') { + const currentSelectedParentTaskId = e.component.getNodeByKey( + e.component.option('focusedRowKey'), + )?.parent?.key; + const key = new (window as any).DevExpress.data.Guid().toString(); + const data = { Head_ID: currentSelectedParentTaskId }; + e.component.option('editing.changes', [ + { + key, + type: 'insert', + insertAfterKey: e.component.option('focusedRowKey'), + data, + }, + ]); + } + }, +})); diff --git a/packages/devextreme/js/__internal/grids/tree_list/editing/m_editing.ts b/packages/devextreme/js/__internal/grids/tree_list/editing/m_editing.ts index 3473089fbc3a..7a02a0424260 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/editing/m_editing.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/editing/m_editing.ts @@ -39,8 +39,11 @@ class EditingController extends editingModule.controllers.editing { } protected _setInsertAfterOrBeforeKey(change, parentKey) { - if (parentKey !== undefined && parentKey !== this.option('rootValue')) { - change.insertAfterKey = parentKey; + const dataSourceAdapter = this._dataController.dataSource(); + const key = parentKey || dataSourceAdapter?.parentKeyOf(change.data); + + if (key !== undefined && key !== this.option('rootValue')) { + change.insertAfterKey = key; } else { // @ts-expect-error super._setInsertAfterOrBeforeKey.apply(this, arguments); @@ -55,7 +58,8 @@ class EditingController extends editingModule.controllers.editing { const rowIndex = gridCoreUtils.getIndexByKey(parentKey, items); // @ts-expect-error if (rowIndex >= 0 && this._dataController.isRowExpanded(parentKey)) { - return rowIndex + 1; + // @ts-expect-error + return super._getLoadedRowIndex.apply(this, arguments); } return -1; } diff --git a/packages/devextreme/js/__internal/scheduler/m_appointment_drag_behavior.ts b/packages/devextreme/js/__internal/scheduler/m_appointment_drag_behavior.ts index 718913429957..2e4643cb3adf 100644 --- a/packages/devextreme/js/__internal/scheduler/m_appointment_drag_behavior.ts +++ b/packages/devextreme/js/__internal/scheduler/m_appointment_drag_behavior.ts @@ -130,7 +130,7 @@ export default class AppointmentDragBehavior { // NOTE: event.cancel may be promise or different type, so we need strict check here. if (e.cancel === true) { - this.removeDroppableClasses(); + options.onDragCancel(e); } if (e.cancel !== true && isSchedulerComponent(e.toComponent)) { diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts index 638f0589743d..61c249d1a754 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts @@ -51,6 +51,7 @@ import { import WidgetObserver from '../base/m_widget_observer'; import AppointmentDragBehavior from '../m_appointment_drag_behavior'; import { + APPOINTMENT_DRAG_SOURCE_CLASS, DATE_TABLE_CLASS, DATE_TABLE_ROW_CLASS, FIXED_CONTAINER_CLASS, @@ -3383,6 +3384,15 @@ const createDragBehaviorConfig = ( removeDroppableCellClass(); }; + const onDragCancel = (e) => { + if (!isDefaultDraggingMode) { + enableDefaultDragging(); + } + + removeDroppableCellClass(); + e.itemElement?.removeClass?.(APPOINTMENT_DRAG_SOURCE_CLASS); + }; + const cursorOffset = options.isSetCursorOffset ? () => { const $dragElement = $(state.dragElement); @@ -3399,6 +3409,7 @@ const createDragBehaviorConfig = ( onDragStart, onDragMove, onDragEnd, + onDragCancel, cursorOffset, filter: options.filter, }; diff --git a/packages/devextreme/js/__internal/ui/form/m_form.layout_manager.utils.ts b/packages/devextreme/js/__internal/ui/form/m_form.layout_manager.utils.ts index 1e529640c675..a39176229f65 100644 --- a/packages/devextreme/js/__internal/ui/form/m_form.layout_manager.utils.ts +++ b/packages/devextreme/js/__internal/ui/form/m_form.layout_manager.utils.ts @@ -1,14 +1,43 @@ import Guid from '@js/core/guid'; +import $ from '@js/core/renderer'; import { extend } from '@js/core/utils/extend'; import { captionize } from '@js/core/utils/inflector'; import { each } from '@js/core/utils/iterator'; -import { isDefined } from '@js/core/utils/type'; +import { isBoolean, isDefined, isFunction } from '@js/core/utils/type'; +import type { dxDropDownEditorOptions } from '@js/ui/drop_down_editor/ui.drop_down_editor'; +import type { FormItemComponent } from '@js/ui/form'; +import type { dxOverlayOptions } from '@js/ui/overlay'; +import type dxTextBox from '@js/ui/text_box'; import { SIMPLE_ITEM_TYPE } from './constants'; -const EDITORS_WITH_ARRAY_VALUE = ['dxTagBox', 'dxRangeSlider', 'dxDateRangeBox']; -const EDITORS_WITH_SPECIFIC_LABELS = ['dxRangeSlider', 'dxSlider']; -export const EDITORS_WITHOUT_LABELS = ['dxCalendar', 'dxCheckBox', 'dxHtmlEditor', 'dxRadioGroup', 'dxRangeSlider', 'dxSlider', 'dxSwitch']; +const EDITORS_WITH_ARRAY_VALUE: FormItemComponent[] = [ + 'dxTagBox', + 'dxRangeSlider', + 'dxDateRangeBox', +]; +const EDITORS_WITH_SPECIFIC_LABELS: FormItemComponent[] = ['dxRangeSlider', 'dxSlider']; +export const EDITORS_WITHOUT_LABELS: FormItemComponent[] = [ + 'dxCalendar', + 'dxCheckBox', + 'dxHtmlEditor', + 'dxRadioGroup', + 'dxRangeSlider', + 'dxSlider', + 'dxSwitch', +]; +const DROP_DOWN_EDITORS: FormItemComponent[] = [ + 'dxSelectBox', + 'dxDropDownBox', + 'dxTagBox', + 'dxLookup', + 'dxAutocomplete', + 'dxColorBox', + 'dxDateBox', + 'dxDateRangeBox', +]; + +type DropDownOptions = dxDropDownEditorOptions; export function convertToRenderFieldItemOptions({ $parent, @@ -33,7 +62,9 @@ export function convertToRenderFieldItemOptions({ labelMode, onLabelTemplateRendered, }) { - const isRequired = isDefined(item.isRequired) ? item.isRequired : !!_hasRequiredRuleInSet(item.validationRules); + const isRequired = isDefined(item.isRequired) + ? item.isRequired + : !!_hasRequiredRuleInSet(item.validationRules); const isSimpleItem = item.itemType === SIMPLE_ITEM_TYPE; const helpID = item.helpText ? `dx-${new Guid()}` : null; @@ -49,11 +80,16 @@ export function convertToRenderFieldItemOptions({ onLabelTemplateRendered, }); - const needRenderLabel = labelOptions.visible && (labelOptions.text || (labelOptions.labelTemplate && isSimpleItem)); + const needRenderLabel = labelOptions.visible + && (labelOptions.text || (labelOptions.labelTemplate && isSimpleItem)); const { location: labelLocation, labelID } = labelOptions; - const labelNeedBaselineAlign = labelLocation !== 'top' && ['dxTextArea', 'dxRadioGroup', 'dxCalendar', 'dxHtmlEditor'].includes(item.editorType); + const labelNeedBaselineAlign = labelLocation !== 'top' + && ['dxTextArea', 'dxRadioGroup', 'dxCalendar', 'dxHtmlEditor'].includes( + item.editorType, + ); const editorOptions = _convertToEditorOptions({ + $parent, editorType: item.editorType, editorValue, defaultEditorName: item.dataField, @@ -70,8 +106,9 @@ export function convertToRenderFieldItemOptions({ }); const needRenderOptionalMarkAsHelpText = labelOptions.markOptions.showOptionalMark - && !labelOptions.visible && editorOptions.labelMode !== 'hidden' - && !isDefined(item.helpText); + && !labelOptions.visible + && editorOptions.labelMode !== 'hidden' + && !isDefined(item.helpText); const helpText = needRenderOptionalMarkAsHelpText ? labelOptions.markOptions.optionalMark @@ -102,18 +139,26 @@ export function convertToRenderFieldItemOptions({ } export function getLabelMarkText({ - showRequiredMark, requiredMark, showOptionalMark, optionalMark, + showRequiredMark, + requiredMark, + showOptionalMark, + optionalMark, }) { if (!showRequiredMark && !showOptionalMark) { return ''; } - return String.fromCharCode(160) + (showRequiredMark ? requiredMark : optionalMark); + return ( + String.fromCharCode(160) + (showRequiredMark ? requiredMark : optionalMark) + ); } -export function convertToLabelMarkOptions({ - showRequiredMark, requiredMark, showOptionalMark, optionalMark, -}, isRequired?: boolean) { +export function convertToLabelMarkOptions( + { + showRequiredMark, requiredMark, showOptionalMark, optionalMark, + }, + isRequired?: boolean, +) { return { showRequiredMark: showRequiredMark && isRequired, requiredMark, @@ -122,8 +167,55 @@ export function convertToLabelMarkOptions({ }; } +// eslint-disable-next-line @typescript-eslint/naming-convention +function _getDropDownEditorOptions( + $parent, + editorType: FormItemComponent, + editorInputId: string, + onContentReadyExternal?: DropDownOptions['onContentReady'], +): DropDownOptions { + const isDropDownEditor = DROP_DOWN_EDITORS.includes(editorType); + + if (!isDropDownEditor) { + return {}; + } + + return { + onContentReady: (e) => { + const { component } = e; + const openOnFieldClick = component.option('openOnFieldClick') as DropDownOptions['openOnFieldClick']; + const initialHideOnOutsideClick = component.option('dropDownOptions.hideOnOutsideClick') as dxOverlayOptions['hideOnOutsideClick']; + + if (openOnFieldClick) { + component.option('dropDownOptions', { + hideOnOutsideClick: (e) => { + if (isBoolean(initialHideOnOutsideClick)) { + return initialHideOnOutsideClick; + } + + const $target = $(e.target); + const $label = $parent.find(`label[for="${editorInputId}"]`); + const isLabelClicked = !!$target.closest($label).length; + + if (!isFunction(initialHideOnOutsideClick)) { + return !isLabelClicked; + } + + return !isLabelClicked && initialHideOnOutsideClick(e); + }, + }); + } + + if (isFunction(onContentReadyExternal)) { + onContentReadyExternal(e); + } + }, + }; +} + // eslint-disable-next-line @typescript-eslint/naming-convention function _convertToEditorOptions({ + $parent, editorType, defaultEditorName, editorValue, @@ -153,10 +245,13 @@ function _convertToEditorOptions({ const stylingMode = externalEditorOptions?.stylingMode || editorStylingMode; const useSpecificLabelOptions = EDITORS_WITH_SPECIFIC_LABELS.includes(editorType); + const dropDownEditorOptions = _getDropDownEditorOptions($parent, editorType, editorInputId, externalEditorOptions?.onContentReady); + const result = extend( true, editorOptionsWithValue, externalEditorOptions, + dropDownEditorOptions, { inputAttr: { id: editorInputId }, validationBoundary: editorValidationBoundary, @@ -179,6 +274,7 @@ function _convertToEditorOptions({ if (defaultEditorName && !result.name) { result.name = defaultEditorName; } + return result; } @@ -201,15 +297,27 @@ function _hasRequiredRuleInSet(rules) { // eslint-disable-next-line @typescript-eslint/naming-convention function _convertToLabelOptions({ - item, id, isRequired, managerMarkOptions, showColonAfterLabel, labelLocation, labelTemplate, formLabelMode, onLabelTemplateRendered, + item, + id, + isRequired, + managerMarkOptions, + showColonAfterLabel, + labelLocation, + labelTemplate, + formLabelMode, + onLabelTemplateRendered, }) { - const isEditorWithoutLabels = EDITORS_WITHOUT_LABELS.includes(item.editorType); + const isEditorWithoutLabels = EDITORS_WITHOUT_LABELS.includes( + item.editorType, + ); const labelOptions = extend( { showColon: showColonAfterLabel, location: labelLocation, id, - visible: formLabelMode === 'outside' || (isEditorWithoutLabels && formLabelMode !== 'hidden'), + visible: + formLabelMode === 'outside' + || (isEditorWithoutLabels && formLabelMode !== 'hidden'), isRequired, }, item ? item.label : {}, @@ -220,7 +328,16 @@ function _convertToLabelOptions({ }, ); - const editorsRequiringIdForLabel = ['dxRadioGroup', 'dxCheckBox', 'dxLookup', 'dxSlider', 'dxRangeSlider', 'dxSwitch', 'dxHtmlEditor', 'dxDateRangeBox']; // TODO: support "dxCalendar" + const editorsRequiringIdForLabel: FormItemComponent[] = [ + 'dxRadioGroup', + 'dxCheckBox', + 'dxLookup', + 'dxSlider', + 'dxRangeSlider', + 'dxSwitch', + 'dxHtmlEditor', + 'dxDateRangeBox', + ]; // TODO: support "dxCalendar" if (editorsRequiringIdForLabel.includes(item.editorType)) { labelOptions.labelID = `dx-label-${new Guid()}`; } diff --git a/packages/devextreme/js/exporter/image_creator.js b/packages/devextreme/js/exporter/image_creator.js index 1b37460bf4ec..098a79c0b52e 100644 --- a/packages/devextreme/js/exporter/image_creator.js +++ b/packages/devextreme/js/exporter/image_creator.js @@ -455,7 +455,7 @@ function applyGradient(context, options, { linearGradients, radialGradients }, e }); if(type === 'linear') { - const angle = gradients[id].transform?.replace(/\D/g, '') * Math.PI / 180 ?? 0; + const angle = (gradients[id].transform?.replace(/\D/g, '') || 0) * Math.PI / 180; context.translate(horizontalCenter, verticalCenter); context.rotate(angle); context.translate(-horizontalCenter, -verticalCenter); diff --git a/packages/devextreme/testing/tests/DevExpress.exporter/imageCreator.tests.js b/packages/devextreme/testing/tests/DevExpress.exporter/imageCreator.tests.js index f40b30623aa4..a1af698c0299 100644 --- a/packages/devextreme/testing/tests/DevExpress.exporter/imageCreator.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.exporter/imageCreator.tests.js @@ -1247,6 +1247,34 @@ QUnit.test('lineargradient with rotation angle', function(assert) { }); }); +QUnit.test('lineargradient with undefined rotation angle', function(assert) { + const done = assert.async(); + const markup = testingMarkupStart + + '' + + + '' + + '' + + '' + + '' + + + '' + + '' + + '' + + testingMarkupEnd; + + const imageBlob = getData(markup); + const context = window.CanvasRenderingContext2D.prototype; + + $.when(imageBlob).done(() => { + try { + assert.strictEqual(context.rotate.callCount, 1, 'rotate call count'); + assert.strictEqual(context.rotate.getCall(0).args[0], 0); + } finally { + done(); + } + }); +}); + QUnit.test('radialgradient', function(assert) { const done = assert.async(); const markup = testingMarkupStart + diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.form/form.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.form/form.tests.js index ce3da955d02c..0a28a0451ea2 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.form/form.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.form/form.tests.js @@ -42,14 +42,11 @@ import { renderLabel, } from '__internal/ui/form/components/m_label'; -const EDITOR_LABEL_CLASS = 'dx-texteditor-label'; -const EDITOR_INPUT_CLASS = 'dx-texteditor-input'; -const FIELD_ITEM_HELP_TEXT_CLASS = 'dx-field-item-help-text'; - import { TOOLBAR_CLASS } from '__internal/ui/toolbar/m_constants'; import 'ui/html_editor'; import '../../helpers/ignoreQuillTimers.js'; +import pointerMock from '../../helpers/pointerMock.js'; import 'ui/lookup'; import 'ui/radio_group'; import 'ui/tag_box'; @@ -66,6 +63,11 @@ const FORM_GROUP_CONTENT_CLASS = 'dx-form-group-content'; const MULTIVIEW_ITEM_CONTENT_CLASS = 'dx-multiview-item-content'; const LAST_COL_CLASS = 'dx-last-col'; const SLIDER_LABEL = 'dx-slider-label'; +const EDITOR_LABEL_CLASS = 'dx-texteditor-label'; +const EDITOR_INPUT_CLASS = 'dx-texteditor-input'; +const FIELD_ITEM_HELP_TEXT_CLASS = 'dx-field-item-help-text'; +const DROP_DOWN_EDITOR_BUTTON_CLASS = 'dx-dropdowneditor-button'; +const TEXTBOX_CLASS = 'dx-textbox'; QUnit.testStart(function() { const markup = @@ -644,7 +646,7 @@ QUnit.test('Check aria-labelledby attribute for editors label', function(assert) }); QUnit.test('field1.required -> form.validate() -> form.option("onFieldDataChanged", "newHandler") -> check form is not re-rendered (T1014577)', function(assert) { - const checkEditorIsInvalid = (form) => form.$element().find('.dx-textbox').hasClass(INVALID_CLASS); + const checkEditorIsInvalid = (form) => form.$element().find(`.${TEXTBOX_CLASS}`).hasClass(INVALID_CLASS); const form = $('#form').dxForm({ formData: { field1: '' }, items: [ { @@ -1855,8 +1857,8 @@ QUnit.test('Align with "" required mark, T1031458', function(assert) { }] }); - const $labelText = $testContainer.find('.dx-field-item-label-text'); - const $textBox = $testContainer.find('.dx-textbox'); + const $labelText = $testContainer.find(`.${FIELD_ITEM_LABEL_TEXT_CLASS}`); + const $textBox = $testContainer.find(`.${TEXTBOX_CLASS}`); assert.roughEqual(getWidth($labelText), 11, 3, 'labelsContent.width'); assert.roughEqual($textBox.offset().left, $labelText.offset().left + 25, 3, 'textBox.left'); @@ -1872,8 +1874,8 @@ QUnit.test('Align with " " required mark, T1031458', function(assert) { }] }); - const $labelText = $testContainer.find('.dx-field-item-label-text'); - const $textBox = $testContainer.find('.dx-textbox'); + const $labelText = $testContainer.find(`.${FIELD_ITEM_LABEL_TEXT_CLASS}`); + const $textBox = $testContainer.find(`.${TEXTBOX_CLASS}`); assert.roughEqual(getWidth($labelText), 11, 3, 'labelsContent.width'); assert.roughEqual($textBox.offset().left, $labelText.offset().left + 25, 3, 'textBox.left'); @@ -1889,8 +1891,8 @@ QUnit.test('Align with "!" required mark, T1031458', function(assert) { }] }); - const $labelText = $testContainer.find('.dx-field-item-label-text'); - const $textBox = $testContainer.find('.dx-textbox'); + const $labelText = $testContainer.find(`.${FIELD_ITEM_LABEL_TEXT_CLASS}`); + const $textBox = $testContainer.find(`.${TEXTBOX_CLASS}`); assert.roughEqual(getWidth($labelText), 11, 3, 'labelsContent.width'); assert.roughEqual($textBox.offset().left, $labelText.offset().left + 29, 3, 'textBox.left'); @@ -1906,8 +1908,8 @@ QUnit.test('Align with "×" required mark, T1031458', function(assert) { }] }); - const $labelText = $testContainer.find('.dx-field-item-label-text'); - const $textBox = $testContainer.find('.dx-textbox'); + const $labelText = $testContainer.find(`.${FIELD_ITEM_LABEL_TEXT_CLASS}`); + const $textBox = $testContainer.find(`.${TEXTBOX_CLASS}`); assert.roughEqual(getWidth($labelText), 11, 3, 'labelsContent.width'); assert.roughEqual($textBox.offset().left, $labelText.offset().left + 35, 3, 'textBox.left'); @@ -4540,6 +4542,52 @@ QUnit.test('form should be dirty when some editors are dirty', function(assert) assert.strictEqual(form.option('isDirty'), false, 'form is not dirty when all editors are back to pristine'); }); +[true, false].forEach((openOnFieldClick) => { + [true, false, undefined].forEach((hideOnOutsideClick) => { + QUnit.test(`Opened DropDownList must hide on input label click, openOnFieldClick: ${openOnFieldClick}, hideOnOutsideClick: ${hideOnOutsideClick} (T1257945)`, function(assert) { + const dropDownOptions = hideOnOutsideClick === undefined ? {} : { hideOnOutsideClick }; + const $form = $('#form').dxForm({ + formData: { CustomerID: 'VINET' }, + items: [{ + itemType: 'group', + colCount: 2, + items: [{ + dataField: 'CustomerID', + editorType: 'dxSelectBox', + editorOptions: { + items: ['VINET', 'VALUE', 'VINS'], + value: '', + openOnFieldClick, + dropDownOptions, + }, + }], + }], + }); + + const $dropDownButton = $form.find(`.${DROP_DOWN_EDITOR_BUTTON_CLASS}`); + + pointerMock($dropDownButton).click(); + + const editorInstance = $form.dxForm('instance').getEditor('CustomerID'); + + assert.true(editorInstance.option('opened'), 'drop down list is visible'); + + const $label = $form.find(`.${FIELD_ITEM_LABEL_TEXT_CLASS}`); + + pointerMock($label).click(); + + // NOTE: In the real environment, clicking the label triggers a click on the editor, + // toggling the popup visibility if openOnFieldClick=true. + // This assertion only takes hideOnOutsideClick into account + if(hideOnOutsideClick === false) { + assert.true(editorInstance.option('opened'), `drop down list ${openOnFieldClick ? 'is hidden by triggered input click' : 'is visible'}`); + } else { + assert.strictEqual(editorInstance.option('opened'), openOnFieldClick, `drop down list is hidden by ${openOnFieldClick ? 'triggered input click' : 'outside click'}`); + } + }); + }); +}); + QUnit.module('reset', () => { [ ['dxCalendar', new Date(2019, 1, 2), { dxCalendar: new Date(2019, 1, 3) } ],