From 6736b45425e8e1ba45bcc6829bd66f210493d43a Mon Sep 17 00:00:00 2001 From: "anna.shakhova" Date: Wed, 11 Dec 2024 09:02:48 +0100 Subject: [PATCH 1/5] Hide opened DropDownEditor popup on label click in form (T1257945) --- .../ui/form/m_form.layout_manager.utils.ts | 134 +++++++++++++++--- .../DevExpress.ui.widgets.form/form.tests.js | 93 ++++++++++++ 2 files changed, 210 insertions(+), 17 deletions(-) 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..213851ec9965 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,40 @@ 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 dxTextBox from '@js/ui/text_box'; import { SIMPLE_ITEM_TYPE } from './constants'; -const EDITORS_WITH_ARRAY_VALUE = ['dxTagBox', 'dxRangeSlider', 'dxDateRangeBox']; +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']; +export const EDITORS_WITHOUT_LABELS = [ + 'dxCalendar', + 'dxCheckBox', + 'dxHtmlEditor', + 'dxRadioGroup', + 'dxRangeSlider', + 'dxSlider', + 'dxSwitch', +]; +const DROP_DOWN_EDITORS: FormItemComponent[] = [ + 'dxSelectBox', + 'dxDropDownBox', + 'dxTagBox', + 'dxLookup', + 'dxAutocomplete', + 'dxColorBox', + 'dxDateBox', + 'dxDateRangeBox', +]; export function convertToRenderFieldItemOptions({ $parent, @@ -33,7 +59,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 +77,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 +103,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 +136,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, @@ -124,6 +166,7 @@ export function convertToLabelMarkOptions({ // eslint-disable-next-line @typescript-eslint/naming-convention function _convertToEditorOptions({ + $parent, editorType, defaultEditorName, editorValue, @@ -153,10 +196,46 @@ function _convertToEditorOptions({ const stylingMode = externalEditorOptions?.stylingMode || editorStylingMode; const useSpecificLabelOptions = EDITORS_WITH_SPECIFIC_LABELS.includes(editorType); + let editorOptionsWithDropDown: Partial> = {}; + const useDropDownOptions = DROP_DOWN_EDITORS.includes(editorType); + + if (useDropDownOptions) { + editorOptionsWithDropDown = { + onContentReady: (e) => { + const { component } = e; + const openOnFieldClick = component.option('openOnFieldClick'); + const initialHideOnOutsideClick = component.option('dropDownOptions.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; + const initialResult = isFunction(initialHideOnOutsideClick) ? !!initialHideOnOutsideClick(e) : true; + + return !isLabelClicked && initialResult; + }, + }); + } + + if (isFunction(externalEditorOptions?.onContentReady)) { + externalEditorOptions.onContentReady(e); + } + }, + }; + } + const result = extend( true, editorOptionsWithValue, externalEditorOptions, + editorOptionsWithDropDown, { inputAttr: { id: editorInputId }, validationBoundary: editorValidationBoundary, @@ -201,15 +280,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 +311,16 @@ function _convertToLabelOptions({ }, ); - const editorsRequiringIdForLabel = ['dxRadioGroup', 'dxCheckBox', 'dxLookup', 'dxSlider', 'dxRangeSlider', 'dxSwitch', 'dxHtmlEditor', 'dxDateRangeBox']; // TODO: support "dxCalendar" + const editorsRequiringIdForLabel = [ + '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/testing/tests/DevExpress.ui.widgets.form/form.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.form/form.tests.js index ce3da955d02c..8ea5a86b12e0 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 @@ -50,6 +50,7 @@ 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'; @@ -4540,6 +4541,98 @@ 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].forEach((hideOnOutsideClickValue) => { + QUnit.test(`Opened DropDownList must hide on input label click, openOnFieldClick: ${openOnFieldClick}, hideOnOutsideClick: ${hideOnOutsideClickValue} (T1257945)`, function(assert) { + let initialHideOnOutsideClick; + 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: { + hideOnOutsideClick: () => hideOnOutsideClickValue, + onContentReady: ({ component }) => { + initialHideOnOutsideClick = component.option('hideOnOutsideClick'); + } + } + }, + }], + }], + }); + + const $dropDownButton = $form.find('.dx-dropdowneditor-button'); + $dropDownButton.trigger('click'); + + const hideOnOutsideClickSpy = sinon.spy(initialHideOnOutsideClick); + const formInstance = $form.dxForm('instance'); + const editorInstance = formInstance.getEditor('CustomerID'); + + formInstance.option('items[0].items[0].editorOptions.dropDownOptions.hideOnOutsideClick', hideOnOutsideClickSpy); + + assert.true(editorInstance.option('opened'), 'drop down list is visible'); + + const $label = $form.find('.dx-field-item-label-text'); + pointerMock($label).click(); + + const expected = openOnFieldClick ? false : hideOnOutsideClickValue; + assert.ok(hideOnOutsideClickSpy.returned(expected), `hideOnOutsideClick returned ${expected}`); + + sinon.restore(); + }); + }); + + QUnit.test(`Opened DropDownList must hide on input label click, openOnFieldClick: ${openOnFieldClick}, hideOnOutsideClick not defined (T1257945)`, function(assert) { + let initialHideOnOutsideClick; + 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: { + onContentReady: ({ component }) => { + initialHideOnOutsideClick = component.option('hideOnOutsideClick'); + } + } + }, + }], + }], + }); + + const $dropDownButton = $form.find('.dx-dropdowneditor-button'); + $dropDownButton.trigger('click'); + + const hideOnOutsideClickSpy = sinon.spy(initialHideOnOutsideClick); + const formInstance = $form.dxForm('instance'); + const editorInstance = formInstance.getEditor('CustomerID'); + + formInstance.option('items[0].items[0].editorOptions.dropDownOptions.hideOnOutsideClick', hideOnOutsideClickSpy); + + assert.true(editorInstance.option('opened'), 'drop down list is visible'); + + const $label = $form.find('.dx-field-item-label-text'); + pointerMock($label).click(); + + assert.ok(hideOnOutsideClickSpy.returned(!openOnFieldClick), `hideOnOutsideClick returned ${!openOnFieldClick}`); + + sinon.restore(); + }); +}); + QUnit.module('reset', () => { [ ['dxCalendar', new Date(2019, 1, 2), { dxCalendar: new Date(2019, 1, 3) } ], From 732378c21a8a331d6267aed6023c64b5c32715b6 Mon Sep 17 00:00:00 2001 From: "anna.shakhova" Date: Thu, 12 Dec 2024 11:57:24 +0100 Subject: [PATCH 2/5] Form: test improvements (T1257945) --- .../ui/form/m_form.layout_manager.utils.ts | 19 +++-- .../DevExpress.ui.widgets.form/form.tests.js | 75 ++++++++----------- 2 files changed, 41 insertions(+), 53 deletions(-) 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 213851ec9965..b28691077274 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 @@ -6,17 +6,18 @@ import { each } from '@js/core/utils/iterator'; 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 = [ +const EDITORS_WITH_ARRAY_VALUE: FormItemComponent[] = [ 'dxTagBox', 'dxRangeSlider', 'dxDateRangeBox', ]; -const EDITORS_WITH_SPECIFIC_LABELS = ['dxRangeSlider', 'dxSlider']; -export const EDITORS_WITHOUT_LABELS = [ +const EDITORS_WITH_SPECIFIC_LABELS: FormItemComponent[] = ['dxRangeSlider', 'dxSlider']; +export const EDITORS_WITHOUT_LABELS: FormItemComponent[] = [ 'dxCalendar', 'dxCheckBox', 'dxHtmlEditor', @@ -203,8 +204,8 @@ function _convertToEditorOptions({ editorOptionsWithDropDown = { onContentReady: (e) => { const { component } = e; - const openOnFieldClick = component.option('openOnFieldClick'); - const initialHideOnOutsideClick = component.option('dropDownOptions.hideOnOutsideClick'); + const openOnFieldClick = component.option('openOnFieldClick') as dxDropDownEditorOptions['openOnFieldClick']; + const initialHideOnOutsideClick = component.option('dropDownOptions.hideOnOutsideClick') as dxOverlayOptions['hideOnOutsideClick']; if (openOnFieldClick) { component.option('dropDownOptions', { @@ -215,11 +216,13 @@ function _convertToEditorOptions({ const $target = $(e.target); const $label = $parent.find(`label[for="${editorInputId}"]`); - const isLabelClicked = !!$target.closest($label).length; - const initialResult = isFunction(initialHideOnOutsideClick) ? !!initialHideOnOutsideClick(e) : true; - return !isLabelClicked && initialResult; + if (!isFunction(initialHideOnOutsideClick)) { + return !isLabelClicked; + } + + return !isLabelClicked && initialHideOnOutsideClick(e); }, }); } 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 8ea5a86b12e0..8a7934801d17 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,10 +42,6 @@ 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'; @@ -67,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 = @@ -645,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: [ { @@ -1856,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'); @@ -1873,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'); @@ -1890,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'); @@ -1907,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'); @@ -4542,9 +4543,8 @@ QUnit.test('form should be dirty when some editors are dirty', function(assert) }); [true, false].forEach((openOnFieldClick) => { - [true, false].forEach((hideOnOutsideClickValue) => { - QUnit.test(`Opened DropDownList must hide on input label click, openOnFieldClick: ${openOnFieldClick}, hideOnOutsideClick: ${hideOnOutsideClickValue} (T1257945)`, function(assert) { - let initialHideOnOutsideClick; + [true, false].forEach((hideOnOutsideClick) => { + QUnit.test(`Opened DropDownList must hide on input label click, openOnFieldClick: ${openOnFieldClick}, hideOnOutsideClick: ${hideOnOutsideClick} (T1257945)`, function(assert) { const $form = $('#form').dxForm({ formData: { CustomerID: 'VINET' }, items: [{ @@ -4558,39 +4558,32 @@ QUnit.test('form should be dirty when some editors are dirty', function(assert) value: '', openOnFieldClick, dropDownOptions: { - hideOnOutsideClick: () => hideOnOutsideClickValue, - onContentReady: ({ component }) => { - initialHideOnOutsideClick = component.option('hideOnOutsideClick'); - } + hideOnOutsideClick, } }, }], }], }); - const $dropDownButton = $form.find('.dx-dropdowneditor-button'); - $dropDownButton.trigger('click'); + const $dropDownButton = $form.find(`.${DROP_DOWN_EDITOR_BUTTON_CLASS}`); - const hideOnOutsideClickSpy = sinon.spy(initialHideOnOutsideClick); - const formInstance = $form.dxForm('instance'); - const editorInstance = formInstance.getEditor('CustomerID'); + pointerMock($dropDownButton).click(); - formInstance.option('items[0].items[0].editorOptions.dropDownOptions.hideOnOutsideClick', hideOnOutsideClickSpy); + const editorInstance = $form.dxForm('instance').getEditor('CustomerID'); assert.true(editorInstance.option('opened'), 'drop down list is visible'); - const $label = $form.find('.dx-field-item-label-text'); + const $label = $form.find(`.${FIELD_ITEM_LABEL_TEXT_CLASS}`); + pointerMock($label).click(); - const expected = openOnFieldClick ? false : hideOnOutsideClickValue; - assert.ok(hideOnOutsideClickSpy.returned(expected), `hideOnOutsideClick returned ${expected}`); + const willHideByInput = openOnFieldClick || !hideOnOutsideClick; - sinon.restore(); + assert.equal(editorInstance.option('opened'), willHideByInput, `drop down list is hidden by ${willHideByInput ? 'triggered input click' : 'label click'}`); }); }); QUnit.test(`Opened DropDownList must hide on input label click, openOnFieldClick: ${openOnFieldClick}, hideOnOutsideClick not defined (T1257945)`, function(assert) { - let initialHideOnOutsideClick; const $form = $('#form').dxForm({ formData: { CustomerID: 'VINET' }, items: [{ @@ -4603,33 +4596,25 @@ QUnit.test('form should be dirty when some editors are dirty', function(assert) items: ['VINET', 'VALUE', 'VINS'], value: '', openOnFieldClick, - dropDownOptions: { - onContentReady: ({ component }) => { - initialHideOnOutsideClick = component.option('hideOnOutsideClick'); - } - } }, }], }], }); - const $dropDownButton = $form.find('.dx-dropdowneditor-button'); - $dropDownButton.trigger('click'); + const $dropDownButton = $form.find(`.${DROP_DOWN_EDITOR_BUTTON_CLASS}`); - const hideOnOutsideClickSpy = sinon.spy(initialHideOnOutsideClick); - const formInstance = $form.dxForm('instance'); - const editorInstance = formInstance.getEditor('CustomerID'); + pointerMock($dropDownButton).click(); - formInstance.option('items[0].items[0].editorOptions.dropDownOptions.hideOnOutsideClick', hideOnOutsideClickSpy); + const editorInstance = $form.dxForm('instance').getEditor('CustomerID'); assert.true(editorInstance.option('opened'), 'drop down list is visible'); - const $label = $form.find('.dx-field-item-label-text'); + const $label = $form.find(`.${FIELD_ITEM_LABEL_TEXT_CLASS}`); + pointerMock($label).click(); - assert.ok(hideOnOutsideClickSpy.returned(!openOnFieldClick), `hideOnOutsideClick returned ${!openOnFieldClick}`); + assert.equal(editorInstance.option('opened'), openOnFieldClick, `drop down list is hidden by ${openOnFieldClick ? 'triggered input click' : 'label click'}`); - sinon.restore(); }); }); From 610c7348462d2a00f5d6d2a2687a4bada95114fe Mon Sep 17 00:00:00 2001 From: "anna.shakhova" Date: Thu, 12 Dec 2024 11:57:24 +0100 Subject: [PATCH 3/5] Form: test improvements (T1257945) --- .../js/__internal/ui/form/m_form.layout_manager.utils.ts | 2 +- .../testing/tests/DevExpress.ui.widgets.form/form.tests.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) 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 b28691077274..78f497dfae08 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 @@ -314,7 +314,7 @@ function _convertToLabelOptions({ }, ); - const editorsRequiringIdForLabel = [ + const editorsRequiringIdForLabel: FormItemComponent[] = [ 'dxRadioGroup', 'dxCheckBox', 'dxLookup', 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 8a7934801d17..7242846968ab 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 @@ -4614,7 +4614,6 @@ QUnit.test('form should be dirty when some editors are dirty', function(assert) pointerMock($label).click(); assert.equal(editorInstance.option('opened'), openOnFieldClick, `drop down list is hidden by ${openOnFieldClick ? 'triggered input click' : 'label click'}`); - }); }); From 1e392d254c1da7c49375036faa7ba183b47a61de Mon Sep 17 00:00:00 2001 From: "anna.shakhova" Date: Thu, 12 Dec 2024 12:38:30 +0100 Subject: [PATCH 4/5] Form: combine test cases in forEach (T1257945) --- .../DevExpress.ui.widgets.form/form.tests.js | 42 ++----------------- 1 file changed, 4 insertions(+), 38 deletions(-) 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 7242846968ab..9e6519b0ad3d 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 @@ -4543,8 +4543,9 @@ QUnit.test('form should be dirty when some editors are dirty', function(assert) }); [true, false].forEach((openOnFieldClick) => { - [true, false].forEach((hideOnOutsideClick) => { + [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: [{ @@ -4557,9 +4558,7 @@ QUnit.test('form should be dirty when some editors are dirty', function(assert) items: ['VINET', 'VALUE', 'VINS'], value: '', openOnFieldClick, - dropDownOptions: { - hideOnOutsideClick, - } + dropDownOptions, }, }], }], @@ -4577,44 +4576,11 @@ QUnit.test('form should be dirty when some editors are dirty', function(assert) pointerMock($label).click(); - const willHideByInput = openOnFieldClick || !hideOnOutsideClick; + const willHideByInput = openOnFieldClick || (hideOnOutsideClick === false); assert.equal(editorInstance.option('opened'), willHideByInput, `drop down list is hidden by ${willHideByInput ? 'triggered input click' : 'label click'}`); }); }); - - QUnit.test(`Opened DropDownList must hide on input label click, openOnFieldClick: ${openOnFieldClick}, hideOnOutsideClick not defined (T1257945)`, function(assert) { - const $form = $('#form').dxForm({ - formData: { CustomerID: 'VINET' }, - items: [{ - itemType: 'group', - colCount: 2, - items: [{ - dataField: 'CustomerID', - editorType: 'dxSelectBox', - editorOptions: { - items: ['VINET', 'VALUE', 'VINS'], - value: '', - openOnFieldClick, - }, - }], - }], - }); - - 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(); - - assert.equal(editorInstance.option('opened'), openOnFieldClick, `drop down list is hidden by ${openOnFieldClick ? 'triggered input click' : 'label click'}`); - }); }); QUnit.module('reset', () => { From fdbfb4b1579a9cadbd502d9f9823c89dba408c60 Mon Sep 17 00:00:00 2001 From: "anna.shakhova" Date: Fri, 13 Dec 2024 10:49:52 +0100 Subject: [PATCH 5/5] Form: minor improvements (T1257945) --- .../ui/form/m_form.layout_manager.utils.ts | 88 +++++++++++-------- .../DevExpress.ui.widgets.form/form.tests.js | 11 ++- 2 files changed, 59 insertions(+), 40 deletions(-) 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 78f497dfae08..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 @@ -37,6 +37,8 @@ const DROP_DOWN_EDITORS: FormItemComponent[] = [ 'dxDateRangeBox', ]; +type DropDownOptions = dxDropDownEditorOptions; + export function convertToRenderFieldItemOptions({ $parent, rootElementCssClassList, @@ -165,6 +167,52 @@ 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, @@ -197,48 +245,13 @@ function _convertToEditorOptions({ const stylingMode = externalEditorOptions?.stylingMode || editorStylingMode; const useSpecificLabelOptions = EDITORS_WITH_SPECIFIC_LABELS.includes(editorType); - let editorOptionsWithDropDown: Partial> = {}; - const useDropDownOptions = DROP_DOWN_EDITORS.includes(editorType); - - if (useDropDownOptions) { - editorOptionsWithDropDown = { - onContentReady: (e) => { - const { component } = e; - const openOnFieldClick = component.option('openOnFieldClick') as dxDropDownEditorOptions['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(externalEditorOptions?.onContentReady)) { - externalEditorOptions.onContentReady(e); - } - }, - }; - } + const dropDownEditorOptions = _getDropDownEditorOptions($parent, editorType, editorInputId, externalEditorOptions?.onContentReady); const result = extend( true, editorOptionsWithValue, externalEditorOptions, - editorOptionsWithDropDown, + dropDownEditorOptions, { inputAttr: { id: editorInputId }, validationBoundary: editorValidationBoundary, @@ -261,6 +274,7 @@ function _convertToEditorOptions({ if (defaultEditorName && !result.name) { result.name = defaultEditorName; } + return result; } 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 9e6519b0ad3d..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 @@ -4576,9 +4576,14 @@ QUnit.test('form should be dirty when some editors are dirty', function(assert) pointerMock($label).click(); - const willHideByInput = openOnFieldClick || (hideOnOutsideClick === false); - - assert.equal(editorInstance.option('opened'), willHideByInput, `drop down list is hidden by ${willHideByInput ? 'triggered input click' : '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'}`); + } }); }); });