Skip to content

Commit

Permalink
DropDownEditors: Fix custom button onClick when fieldTemplate is used…
Browse files Browse the repository at this point in the history
… (T1225549) (#27471)

Co-authored-by: ksercs <[email protected]>
  • Loading branch information
jdvictoria and ksercs authored Jun 4, 2024
1 parent 84553c2 commit 73b23a0
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 63 deletions.
61 changes: 24 additions & 37 deletions packages/devextreme/js/ui/drop_down_editor/ui.drop_down_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ const DropDownEditor = TextBox.inherit({

_renderInput: function() {
this.callBase();
this._renderTemplateWrapper();

this._wrapInput();
this._setDefaultAria();
Expand Down Expand Up @@ -351,18 +352,34 @@ const DropDownEditor = TextBox.inherit({
promise.always(this._renderField.bind(this));
},

_getButtonsContainer() {
const fieldTemplate = this._getFieldTemplate();
return fieldTemplate ? this._$container : this._$textEditorContainer;
},

_renderTemplateWrapper() {
const fieldTemplate = this._getFieldTemplate();
if(!fieldTemplate) {
return;
}

if(!this._$templateWrapper) {
this._$templateWrapper = $('<div>')
.addClass(DROP_DOWN_EDITOR_FIELD_TEMPLATE_WRAPPER)
.prependTo(this.$element());
}
},

_renderTemplatedField: function(fieldTemplate, data) {
const isFocused = focused(this._input());
const $container = this._$container;

this._detachKeyboardEvents();
this._refreshButtonsContainer();
this._detachWrapperContent();
this._detachFocusEvents();
$container.empty();

const $templateWrapper = $('<div>').addClass(DROP_DOWN_EDITOR_FIELD_TEMPLATE_WRAPPER).appendTo($container);
this._$textEditorContainer.remove();
this._$templateWrapper.empty();

const $templateWrapper = this._$templateWrapper;
fieldTemplate.render({
model: data,
container: getPublicElement($templateWrapper),
Expand All @@ -380,35 +397,10 @@ const DropDownEditor = TextBox.inherit({
}

this._integrateInput();

isFocused && eventsEngine.trigger($input, 'focus');
}
});

this._attachWrapperContent($container);
},

_detachWrapperContent() {
const useHiddenSubmitElement = this.option('useHiddenSubmitElement');

useHiddenSubmitElement && this._$submitElement?.detach();

// NOTE: to prevent buttons disposition
const beforeButtonsContainerParent = this._$beforeButtonsContainer?.[0].parentNode;
const afterButtonsContainerParent = this._$afterButtonsContainer?.[0].parentNode;
beforeButtonsContainerParent?.removeChild(this._$beforeButtonsContainer[0]);
afterButtonsContainerParent?.removeChild(this._$afterButtonsContainer[0]);
},

_attachWrapperContent($container) {
const useHiddenSubmitElement = this.option('useHiddenSubmitElement');

$container.prepend(this._$beforeButtonsContainer);
useHiddenSubmitElement && this._$submitElement?.appendTo($container);
$container.append(this._$afterButtonsContainer);
},

_refreshButtonsContainer() {
this._$buttonsContainer = this.$element().children().eq(0);
},

_integrateInput: function() {
Expand Down Expand Up @@ -781,6 +773,7 @@ const DropDownEditor = TextBox.inherit({

_clean: function() {
delete this._openOnFieldClickAction;
delete this._$templateWrapper;

if(this._$popup) {
this._$popup.remove();
Expand Down Expand Up @@ -917,12 +910,6 @@ const DropDownEditor = TextBox.inherit({
this._initPopupInitializedAction();
break;
case 'fieldTemplate':
if(isDefined(args.value)) {
this._renderField();
} else {
this._invalidate();
}
break;
case 'acceptCustomValue':
case 'openOnFieldClick':
this._invalidate();
Expand Down
2 changes: 2 additions & 0 deletions packages/devextreme/js/ui/lookup.js
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,8 @@ const Lookup = DropDownList.inherit({
}
},

_renderButtonContainers: noop,

_renderFieldTemplate: function(template) {
this._$field.empty();
const data = this._fieldRenderData();
Expand Down
15 changes: 9 additions & 6 deletions packages/devextreme/js/ui/text_box/ui.text_editor.base.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ const TextEditorBase = Editor.inherit({
.addClass(TEXTEDITOR_CLASS);

this._renderInput();
this._renderButtonContainers();
this._renderStylingMode();
this._renderInputType();
this._renderPlaceholder();
Expand All @@ -227,16 +228,14 @@ const TextEditorBase = Editor.inherit({
},

_renderInput: function() {
this._$buttonsContainer = this._$textEditorContainer = $('<div>')
this._$textEditorContainer = $('<div>')
.addClass(TEXTEDITOR_CONTAINER_CLASS)
.appendTo(this.$element());

this._$textEditorInputContainer = $('<div>')
.addClass(TEXTEDITOR_INPUT_CONTAINER_CLASS)
.appendTo(this._$textEditorContainer);
this._$textEditorInputContainer.append(this._createInput());

this._renderButtonContainers();
},

_getInputContainer() {
Expand Down Expand Up @@ -282,11 +281,16 @@ const TextEditorBase = Editor.inherit({
this._toggleValidMark();
},

_getButtonsContainer() {
return this._$textEditorContainer;
},

_renderButtonContainers: function() {
const buttons = this.option('buttons');

this._$beforeButtonsContainer = this._buttonCollection.renderBeforeButtons(buttons, this._$buttonsContainer);
this._$afterButtonsContainer = this._buttonCollection.renderAfterButtons(buttons, this._$buttonsContainer);
const $buttonsContainer = this._getButtonsContainer();
this._$beforeButtonsContainer = this._buttonCollection.renderBeforeButtons(buttons, $buttonsContainer);
this._$afterButtonsContainer = this._buttonCollection.renderAfterButtons(buttons, $buttonsContainer);
},

_cleanButtonContainers: function() {
Expand All @@ -302,7 +306,6 @@ const TextEditorBase = Editor.inherit({
this._$beforeButtonsContainer = null;
this._$afterButtonsContainer = null;
this._$textEditorContainer = null;
this._$buttonsContainer = null;
this.callBase();
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const TEXT_EDITOR_INPUT_CLASS = 'dx-texteditor-input';
const TEXT_EDITOR_LABEL_CLASS = 'dx-texteditor-label';
const TEXT_EDITOR_BUTTONS_CONTAINER_CLASS = 'dx-texteditor-buttons-container';
const DROP_DOWN_EDITOR_FIELD_TEMPLATE_WRAPPER = 'dx-dropdowneditor-field-template-wrapper';
const DROP_DOWN_EDITOR_INPUT_WRAPPER = 'dx-dropdowneditor-input-wrapper';
const POPUP_CONTENT = 'dx-popup-content';
const TAB_KEY_CODE = 'Tab';
const ESC_KEY_CODE = 'Escape';
Expand Down Expand Up @@ -246,7 +247,7 @@ QUnit.module('dxDropDownEditor', testEnvironment, () => {
QUnit.test('correct buttons order after option change', function(assert) {
this.dropDownEditor.option('showClearButton', true);

const $buttonsContainer = this.$dropDownEditor.find('.dx-texteditor-buttons-container');
const $buttonsContainer = this.$dropDownEditor.find(`.${TEXT_EDITOR_BUTTONS_CONTAINER_CLASS}`);
const $buttons = $buttonsContainer.children();

assert.equal($buttons.length, 2, 'clear button and drop button were rendered');
Expand Down Expand Up @@ -448,7 +449,7 @@ QUnit.module('focus policy', () => {

assert.ok($dropDownEditor.find('.dx-texteditor').hasClass('dx-state-focused'), 'Widget is focused');

const $buttonsContainer = $dropDownEditor.find('.dx-texteditor-buttons-container');
const $buttonsContainer = $dropDownEditor.find(`.${TEXT_EDITOR_BUTTONS_CONTAINER_CLASS}`);
const $buttons = $buttonsContainer.children();

$buttons.eq(1).trigger('dxclick');
Expand Down Expand Up @@ -1092,7 +1093,7 @@ QUnit.module('Templates', () => {
});
const dropDownEditor = $dropDownEditor.dxDropDownEditor('instance');

$dropDownEditor.find('.dx-texteditor-buttons-container').remove();
$dropDownEditor.find(`.${TEXT_EDITOR_BUTTONS_CONTAINER_CLASS}`).remove();

try {
dropDownEditor.option('value', 2);
Expand All @@ -1102,30 +1103,149 @@ QUnit.module('Templates', () => {
}
});

QUnit.testInActiveWindow('widget should detach focus events before fieldTemplate rerender', function(assert) {
const focusOutSpy = sinon.stub();
const $dropDownEditor = $('#dropDownEditorLazy').dxDropDownEditor({
dataSource: [1, 2],
fieldTemplate(value, container) {
const $textBoxContainer = $('<div>').appendTo(container);
$('<div>').dxTextBox().appendTo($textBoxContainer);

$($textBoxContainer).one('dxremove', () => {
$textBoxContainer.detach();
QUnit.module('fieldTemplate rerendering', {
beforeEach: function() {
const init = (options = {}) => {
this.$dropDownEditor = $('#dropDownEditorLazy').dxDropDownEditor({
acceptCustomValue: true,
fieldTemplate(value, fieldElement) {
const $textBox = $('<div>').dxTextBox({ value });
fieldElement.append($textBox);
return $textBox;
},
buttons: [{
name: 'after',
}],
...options
});
this.instance = this.$dropDownEditor.dxDropDownEditor('instance');
this.$input = this.$dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`);
this.keyboard = keyboardMock(this.$input);
this.$buttonsContainer = this.$dropDownEditor.find(`.${TEXT_EDITOR_BUTTONS_CONTAINER_CLASS}`).eq(1);
};

init();
this.reinit = (options) => {
this.instance.dispose();
init(options);
};
this.triggerFieldTemplateRendering = () => {
this.keyboard
.type('123')
.change();
};
}
}, () => {
QUnit.module('should not recreate', {
beforeEach: function() {
this.mutationCallbacks = [];

this.observer = new MutationObserver((mutationsList) => {
this.mutationCallbacks.forEach(callback => {
callback(mutationsList);
});
});
},
onFocusOut: focusOutSpy,
opened: true
afterEach: function() {
this.observer.disconnect();
}
}, () => {
QUnit.test('buttons container (T1225549)', function(assert) {
assert.expect(0);

this.mutationCallbacks.push((mutationsList) => {
mutationsList.forEach(mutation => {
if(mutation.type === 'childList') {
mutation.removedNodes.forEach(node => {
if(node === this.$buttonsContainer.get(0)) {
assert.ok(false, 'buttons container should not be reattached on field template rendering');
}
});
}
});
});

this.observer.observe(this.$buttonsContainer.parent().get(0), { childList: true });

this.triggerFieldTemplateRendering();
});

QUnit.test('template wrapper, only empty it', function(assert) {
assert.expect(0);

const $templateWrapper = this.$dropDownEditor.find(`.${DROP_DOWN_EDITOR_FIELD_TEMPLATE_WRAPPER}`).eq(0);
this.mutationCallbacks.push((mutationsList) => {
mutationsList.forEach(mutation => {
if(mutation.type === 'childList') {
mutation.removedNodes.forEach(node => {
if(node === $templateWrapper.get(0)) {
assert.ok(false, 'template wrapper should not be recreated');
}
});
}
});
});

this.observer.observe($templateWrapper.parent().get(0), { childList: true });

this.triggerFieldTemplateRendering();
});
});

const $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`);
const keyboard = keyboardMock($input);
QUnit.test('should keep elements correct order when custom buttons are used', function(assert) {
this.reinit({
buttons: [{
name: 'before',
location: 'before'
}, {
name: 'after'
}]
});

$input.focus();
keyboard.press('down');
keyboard.press('enter');
this.triggerFieldTemplateRendering();

const $inputWrapper = this.$dropDownEditor.find(`.${DROP_DOWN_EDITOR_INPUT_WRAPPER}`).eq(0);
const $children = $inputWrapper.children();
assert.strictEqual($children.length, 3, 'element count is correct');
assert.ok($children.eq(0).hasClass(TEXT_EDITOR_BUTTONS_CONTAINER_CLASS), 'before buttons container');
assert.ok($children.eq(1).hasClass(DROP_DOWN_EDITOR_FIELD_TEMPLATE_WRAPPER), 'template wrapper');
assert.ok($children.eq(2).hasClass(TEXT_EDITOR_BUTTONS_CONTAINER_CLASS), 'after buttons container');
});

QUnit.test('should keep elements correct order when hidden input is used', function(assert) {
this.reinit({
useHiddenSubmitElement: true,
buttons: [{
name: 'before',
location: 'before'
}, {
name: 'after'
}]
});

this.triggerFieldTemplateRendering();

assert.strictEqual(focusOutSpy.callCount, 0, 'there\'s no focus outs from deleted field container');
const $inputWrapper = this.$dropDownEditor.find(`.${DROP_DOWN_EDITOR_INPUT_WRAPPER}`).eq(0);
const $children = $inputWrapper.children();
assert.strictEqual($children.length, 4, 'element count is correct');
assert.ok($children.eq(0).hasClass(TEXT_EDITOR_BUTTONS_CONTAINER_CLASS), 'before buttons container');
assert.ok($children.eq(1).hasClass(DROP_DOWN_EDITOR_FIELD_TEMPLATE_WRAPPER), 'template wrapper');
assert.strictEqual($children.get(2).tagName, 'INPUT', 'hidden input');
assert.ok($children.eq(3).hasClass(TEXT_EDITOR_BUTTONS_CONTAINER_CLASS), 'after buttons container');
});

QUnit.testInActiveWindow('should not trigger focusout event (T751314)', function(assert) {
const focusOutStub = sinon.stub();
this.reinit({
onFocusOut: focusOutStub,
});

this.$input.trigger('focus');
this.triggerFieldTemplateRendering();

assert.strictEqual(focusOutStub.callCount, 0, 'there is no focusout from deleted field container');
});
});

QUnit.test('fieldTemplate item element should have 100% width (T826516)', function(assert) {
Expand Down

0 comments on commit 73b23a0

Please sign in to comment.