From d9a0fd71af1b6ef2f196bbe0b657f1b675392355 Mon Sep 17 00:00:00 2001 From: Dmitry Levkovskiy Date: Fri, 24 Jan 2020 16:16:16 +0300 Subject: [PATCH] Fix DropDownEditor Popup position depending the "rtlEnabled" option value (T856114) (#11687) * Fix Popup position of the DropDownEditor depending on "rtlEnabled" option value (T856114) * refactoring --- js/core/utils/position.js | 4 +- js/ui/drop_down_editor/ui.drop_down_editor.js | 112 ++++++++++-------- .../DevExpress.core/utils.position.tests.js | 31 +++++ .../dropDownEditor.markup.tests.js | 30 +++++ 4 files changed, 123 insertions(+), 54 deletions(-) create mode 100644 testing/tests/DevExpress.core/utils.position.tests.js diff --git a/js/core/utils/position.js b/js/core/utils/position.js index c6a1f6546b6e..d8390828775b 100644 --- a/js/core/utils/position.js +++ b/js/core/utils/position.js @@ -1,7 +1,7 @@ -const config = require('../config'); +import config from '../config'; const getDefaultAlignment = function(isRtlEnabled) { - const rtlEnabled = isRtlEnabled || config().rtlEnabled; + const rtlEnabled = isRtlEnabled ?? config().rtlEnabled; return rtlEnabled ? 'right' : 'left'; }; diff --git a/js/ui/drop_down_editor/ui.drop_down_editor.js b/js/ui/drop_down_editor/ui.drop_down_editor.js index fb5f27311df6..41ea66cef3c7 100644 --- a/js/ui/drop_down_editor/ui.drop_down_editor.js +++ b/js/ui/drop_down_editor/ui.drop_down_editor.js @@ -1,27 +1,26 @@ -const $ = require('../../core/renderer'); -const AsyncTemplateMixin = require('../shared/async_template_mixin'); -const eventsEngine = require('../../events/core/events_engine'); -const Guid = require('../../core/guid'); -const registerComponent = require('../../core/component_registrator'); -const commonUtils = require('../../core/utils/common'); -const domUtils = require('../../core/utils/dom'); -const focused = require('../widget/selectors').focused; -const each = require('../../core/utils/iterator').each; -const isDefined = require('../../core/utils/type').isDefined; -const extend = require('../../core/utils/extend').extend; -const getPublicElement = require('../../core/utils/dom').getPublicElement; -const errors = require('../widget/ui.errors'); -const positionUtils = require('../../animation/position'); -const getDefaultAlignment = require('../../core/utils/position').getDefaultAlignment; -const DropDownButton = require('./ui.drop_down_button').default; -const Widget = require('../widget/ui.widget'); -const messageLocalization = require('../../localization/message'); -const eventUtils = require('../../events/utils'); -const TextBox = require('../text_box'); -const clickEvent = require('../../events/click'); -const devices = require('../../core/devices'); -const FunctionTemplate = require('../../core/templates/function_template').FunctionTemplate; -const Popup = require('../popup'); +import $ from '../../core/renderer'; +import AsyncTemplateMixin from '../shared/async_template_mixin'; +import eventsEngine from '../../events/core/events_engine'; +import Guid from '../../core/guid'; +import registerComponent from '../../core/component_registrator'; +import { noop, splitPair } from '../../core/utils/common'; +import { focused } from '../widget/selectors'; +import { each } from '../../core/utils/iterator'; +import { isDefined } from '../../core/utils/type'; +import { extend } from '../../core/utils/extend'; +import { getPublicElement } from '../../core/utils/dom'; +import errors from '../widget/ui.errors'; +import { setup as setupPosition } from '../../animation/position'; +import { getDefaultAlignment } from '../../core/utils/position'; +import DropDownButton from './ui.drop_down_button'; +import Widget from '../widget/ui.widget'; +import { format as formatMessage } from '../../localization/message'; +import { addNamespace } from '../../events/utils'; +import TextBox from '../text_box'; +import clickEvent from '../../events/click'; +import devices from '../../core/devices'; +import { FunctionTemplate } from '../../core/templates/function_template'; +import Popup from '../popup'; const DROP_DOWN_EDITOR_CLASS = 'dx-dropdowneditor'; const DROP_DOWN_EDITOR_INPUT_WRAPPER = 'dx-dropdowneditor-input-wrapper'; @@ -137,8 +136,8 @@ const DropDownEditor = TextBox.inherit({ dropDownOptions: {}, popupPosition: this._getDefaultPopupPosition(), onPopupInitialized: null, - applyButtonText: messageLocalization.format('OK'), - cancelButtonText: messageLocalization.format('Cancel'), + applyButtonText: formatMessage('OK'), + cancelButtonText: formatMessage('Cancel'), buttonsLocation: 'default', showPopupTitle: false, useHiddenSubmitElement: false @@ -180,8 +179,8 @@ const DropDownEditor = TextBox.inherit({ }); }, - _getDefaultPopupPosition: function() { - const position = getDefaultAlignment(); + _getDefaultPopupPosition: function(isRtlEnabled) { + const position = getDefaultAlignment(isRtlEnabled); return { offset: { h: 0, v: -1 }, @@ -213,9 +212,17 @@ const DropDownEditor = TextBox.inherit({ this.callBase(); this._initVisibilityActions(); this._initPopupInitializedAction(); + this._updatePopupPosition(this.option('rtlEnabled')); this._options.cache('dropDownOptions', this.option('dropDownOptions')); }, + _updatePopupPosition: function(isRtlEnabled) { + const { my, at } = this._getDefaultPopupPosition(isRtlEnabled); + const currentPosition = this.option('popupPosition'); + + this.option('popupPosition', extend({}, currentPosition, { my, at })); + }, + _initVisibilityActions: function() { this._openAction = this._createActionByOption('onOpened', { excludeValidators: ['disabled', 'readOnly'] @@ -340,7 +347,7 @@ const DropDownEditor = TextBox.inherit({ fieldTemplate.render({ model: data, - container: domUtils.getPublicElement($templateWrapper), + container: getPublicElement($templateWrapper), onRendered: () => { const $input = this._input(); @@ -374,24 +381,23 @@ const DropDownEditor = TextBox.inherit({ }, _renderOpenHandler: function() { - const that = this; - const $inputWrapper = that._inputWrapper(); - const eventName = eventUtils.addNamespace(clickEvent.name, that.NAME); - const openOnFieldClick = that.option('openOnFieldClick'); + const $inputWrapper = this._inputWrapper(); + const eventName = addNamespace(clickEvent.name, this.NAME); + const openOnFieldClick = this.option('openOnFieldClick'); eventsEngine.off($inputWrapper, eventName); - eventsEngine.on($inputWrapper, eventName, that._getInputClickHandler(openOnFieldClick)); - that.$element().toggleClass(DROP_DOWN_EDITOR_FIELD_CLICKABLE, openOnFieldClick); + eventsEngine.on($inputWrapper, eventName, this._getInputClickHandler(openOnFieldClick)); + this.$element().toggleClass(DROP_DOWN_EDITOR_FIELD_CLICKABLE, openOnFieldClick); if(openOnFieldClick) { - that._openOnFieldClickAction = that._createAction(that._openHandler.bind(that)); + this._openOnFieldClickAction = this._createAction(this._openHandler.bind(this)); } }, _attachFocusOutHandler: function() { if(isIOs) { this._detachFocusOutEvents(); - eventsEngine.on(this._inputWrapper(), eventUtils.addNamespace('focusout', this.NAME), function(event) { + eventsEngine.on(this._inputWrapper(), addNamespace('focusout', this.NAME), (event) => { const newTarget = event.relatedTarget; const popupWrapper = this.content ? $(this.content()).closest('.' + DROP_DOWN_EDITOR_OVERLAY) : this._$popup; if(newTarget && this.option('opened')) { @@ -400,20 +406,18 @@ const DropDownEditor = TextBox.inherit({ this.close(); } } - }.bind(this)); + }); } }, _detachFocusOutEvents: function() { - isIOs && eventsEngine.off(this._inputWrapper(), eventUtils.addNamespace('focusout', this.NAME)); + isIOs && eventsEngine.off(this._inputWrapper(), addNamespace('focusout', this.NAME)); }, _getInputClickHandler: function(openOnFieldClick) { - const that = this; - return openOnFieldClick ? - function(e) { that._executeOpenAction(e); } : - function(e) { that._focusInput(); }; + (e) => { this._executeOpenAction(e); } : + (e) => { this._focusInput(); }; }, _openHandler: function() { @@ -503,7 +507,7 @@ const DropDownEditor = TextBox.inherit({ this.setAria('id', this._popupContentId, $popupContent); }, - _contentReadyHandler: commonUtils.noop, + _contentReadyHandler: noop, _popupConfig: function() { @@ -536,16 +540,16 @@ const DropDownEditor = TextBox.inherit({ return; } - return (function(e) { + return (e) => { this._popupInitializedAction({ popup: e.component }); - }).bind(this); + }; }, _popupPositionedHandler: function(e) { e.position && this._popup.overlayContent().toggleClass(DROP_DOWN_EDITOR_OVERLAY_FLIPPED, e.position.v.flip); }, - _popupShowingHandler: commonUtils.noop, + _popupShowingHandler: noop, _popupHidingHandler: function() { this.option('opened', false); @@ -570,8 +574,8 @@ const DropDownEditor = TextBox.inherit({ let positionRequest = 'below'; if(this._popup && this._popup.option('visible')) { - const myTop = positionUtils.setup(this.$element()).top; - const popupTop = positionUtils.setup(this._popup.$content()).top; + const { top: myTop } = setupPosition(this.$element()); + const { top: popupTop } = setupPosition(this._popup.$content()); positionRequest = (myTop + this.option('popupPosition').offset.v) > popupTop ? 'below' : 'above'; } @@ -595,7 +599,7 @@ const DropDownEditor = TextBox.inherit({ $popupContent.empty(); contentTemplate.render({ - container: domUtils.getPublicElement($popupContent), + container: getPublicElement($popupContent), model: templateData }); }, @@ -695,7 +699,7 @@ const DropDownEditor = TextBox.inherit({ const resultConfig = buttonsConfig; if(buttonsLocation !== 'default') { - const position = commonUtils.splitPair(buttonsLocation); + const position = splitPair(buttonsLocation); each(resultConfig, function(_, element) { extend(element, { @@ -718,7 +722,7 @@ const DropDownEditor = TextBox.inherit({ this.option('focusStateEnabled') && this.focus(); }, - _updatePopupWidth: commonUtils.noop, + _updatePopupWidth: noop, _popupOptionChanged: function(args) { const options = Widget.getOptionsFromContainer(args); @@ -807,6 +811,10 @@ const DropDownEditor = TextBox.inherit({ this._renderSubmitElement(); break; + case 'rtlEnabled': + this._updatePopupPosition(args.value); + this.callBase(args); + break; default: this.callBase(args); } diff --git a/testing/tests/DevExpress.core/utils.position.tests.js b/testing/tests/DevExpress.core/utils.position.tests.js new file mode 100644 index 000000000000..2502f44821d4 --- /dev/null +++ b/testing/tests/DevExpress.core/utils.position.tests.js @@ -0,0 +1,31 @@ +import config from 'core/config'; +import { getDefaultAlignment } from 'core/utils/position.js'; + +const { module: testModule, test } = QUnit; + +testModule('getDefaultAlignment', function() { + test('getDefaultAlignment should return an alignment depending on the global "rtlEnabled" config or passed value', function(assert) { + const originalConfig = config(); + + try { + config({ + rtlEnabled: false + }); + + assert.strictEqual(getDefaultAlignment(), 'left', '"isRtlEnabled" argument is undefined, global "rtlEnabled" config is false'); + assert.strictEqual(getDefaultAlignment(true), 'right', '"isRtlEnabled" argument is true, global "rtlEnabled" config is false'); + assert.strictEqual(getDefaultAlignment(false), 'left', '"isRtlEnabled" argument is false, global "rtlEnabled" config is false'); + + config({ + rtlEnabled: true + }); + + assert.strictEqual(getDefaultAlignment(), 'right', '"isRtlEnabled" argument is undefined, global "rtlEnabled" config is true'); + assert.strictEqual(getDefaultAlignment(true), 'right', '"isRtlEnabled" argument is true, global "rtlEnabled" config is true'); + assert.strictEqual(getDefaultAlignment(false), 'left', '"isRtlEnabled" argument is false, global "rtlEnabled" config is true'); + + } finally { + config(originalConfig); + } + }); +}); diff --git a/testing/tests/DevExpress.ui.widgets.editors/dropDownEditor.markup.tests.js b/testing/tests/DevExpress.ui.widgets.editors/dropDownEditor.markup.tests.js index 6f3aacc00e42..eebef1473b9c 100644 --- a/testing/tests/DevExpress.ui.widgets.editors/dropDownEditor.markup.tests.js +++ b/testing/tests/DevExpress.ui.widgets.editors/dropDownEditor.markup.tests.js @@ -127,3 +127,33 @@ module('aria accessibility', () => { }); }); + +module('option change', function() { + const getStartDirection = (isRtlEnabled) => isRtlEnabled ? 'right' : 'left'; + + [false, true].forEach((rtlEnabled) => { + test(`after updating of the "rtlEnabled" option to "${rtlEnabled}" Popup should update its position`, function(assert) { + const dropDownEditor = $('#dropDownEditorLazy').dxDropDownEditor({ rtlEnabled }).dxDropDownEditor('instance'); + const { my: initialMyPosition, at: initialAtPosition } = dropDownEditor.option('popupPosition'); + const initialStartDirection = getStartDirection(rtlEnabled); + + assert.strictEqual(initialAtPosition, `${initialStartDirection} bottom`, 'correct initial "at" position'); + assert.strictEqual(initialMyPosition, `${initialStartDirection} top`, 'correct initial "my" position'); + + dropDownEditor.option('rtlEnabled', !rtlEnabled); + + const { my: newMyPosition, at: newAtPosition } = dropDownEditor.option('popupPosition'); + const newStartDirection = getStartDirection(!rtlEnabled); + + assert.strictEqual(newAtPosition, `${newStartDirection} bottom`, 'correct new "at" position'); + assert.strictEqual(newMyPosition, `${newStartDirection} top`, 'correct new "my" position'); + + dropDownEditor.option('rtlEnabled', rtlEnabled); + + const { my: revertedMyPosition, at: revertedAtPosition } = dropDownEditor.option('popupPosition'); + + assert.strictEqual(revertedAtPosition, `${initialStartDirection} bottom`, 'correct initial "at" position'); + assert.strictEqual(revertedMyPosition, `${initialStartDirection} top`, 'correct initial "my" position'); + }); + }); +});