Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Form: Hide opened DropDownEditor popup on label click in form (T1257945) #28535

Open
wants to merge 5 commits into
base: 24_2
Choose a base branch
from
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
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)
ksercs marked this conversation as resolved.
Show resolved Hide resolved
? 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
Loading