Skip to content

Commit

Permalink
Form: Hide opened DropDownEditor popup on label click in form (T12579…
Browse files Browse the repository at this point in the history
…45) (#28535)
  • Loading branch information
anna-shakhova authored Dec 17, 2024
1 parent f2bf726 commit da0e742
Show file tree
Hide file tree
Showing 2 changed files with 196 additions and 31 deletions.
153 changes: 135 additions & 18 deletions packages/devextreme/js/__internal/ui/form/m_form.layout_manager.utils.ts
Original file line number Diff line number Diff line change
@@ -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<dxTextBox>;

export function convertToRenderFieldItemOptions({
$parent,
Expand All @@ -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;

Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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<dxTextBox>['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,
Expand Down Expand Up @@ -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,
Expand All @@ -179,6 +274,7 @@ function _convertToEditorOptions({
if (defaultEditorName && !result.name) {
result.name = defaultEditorName;
}

return result;
}

Expand All @@ -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 : {},
Expand All @@ -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()}`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 =
Expand Down Expand Up @@ -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: [ {
Expand Down Expand Up @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand Down Expand Up @@ -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) } ],
Expand Down

0 comments on commit da0e742

Please sign in to comment.