diff --git a/packages/devextreme/js/__internal/ui/m_multi_view.ts b/packages/devextreme/js/__internal/ui/m_multi_view.ts index f3e8226a0ab2..2134da9b44ac 100644 --- a/packages/devextreme/js/__internal/ui/m_multi_view.ts +++ b/packages/devextreme/js/__internal/ui/m_multi_view.ts @@ -100,7 +100,7 @@ const MultiView = CollectionWidget.inherit({ return this.option('items').length; }, - _normalizeIndex(index) { + _normalizeIndex(index, direction, loop = true) { const count = this._itemsCount(); if (index < 0) { @@ -110,9 +110,9 @@ const MultiView = CollectionWidget.inherit({ index -= count; } - const step = this._swipeDirection > 0 ? -1 : 1; - - while (!this._isItemVisible(index)) { + const step = direction > 0 ? -1 : 1; + const lastNotLoopedIndex = step === -1 ? 0 : count - 1; + while (!this._isItemVisible(index) && (loop || index !== lastNotLoopedIndex)) { index = (index + step) % count; } @@ -142,14 +142,37 @@ const MultiView = CollectionWidget.inherit({ this._initSwipeable(); }, + _ensureSelectedItemIsVisible(): void { + const { items, loop, selectedIndex: currentSelectedIndex } = this.option(); + + if (this._isItemVisible(currentSelectedIndex)) { + return; + } + + const allItemsHidden = items.every((_, index) => !this._isItemVisible(index)); + if (allItemsHidden) { + this.option('selectedIndex', 0); + return; + } + + const direction = -1 * this._getRTLSignCorrection(); + let newSelectedIndex = this._normalizeIndex(currentSelectedIndex, direction, loop); + if (newSelectedIndex === currentSelectedIndex) { + newSelectedIndex = this._normalizeIndex(currentSelectedIndex, -direction, loop); + } + + this.option('selectedIndex', newSelectedIndex); + }, + _initMarkup() { this._deferredItems = []; this.callBase(); + this._ensureSelectedItemIsVisible(); const selectedItemIndices = this._getSelectedItemIndices(); - this._updateItemsVisibility(selectedItemIndices[0]); + this._setElementAria(); this._setItemsAria(); }, @@ -391,20 +414,14 @@ const MultiView = CollectionWidget.inherit({ e.maxLeftOffset = toNumber(loop || (rtl ? selectedIndex > firstAvailableIndex : selectedIndex < lastAvailableIndex)); e.maxRightOffset = toNumber(loop || (rtl ? selectedIndex < lastAvailableIndex : selectedIndex > firstAvailableIndex)); - - this._swipeDirection = null; }, _swipeUpdateHandler(e) { const { offset } = e; const swipeDirection = sign(offset) * this._getRTLSignCorrection(); - if (swipeDirection !== this._swipeDirection) { - this._swipeDirection = swipeDirection; - } - const selectedIndex = this.option('selectedIndex'); - const newIndex = this._normalizeIndex(selectedIndex - swipeDirection); + const newIndex = this._normalizeIndex(selectedIndex - swipeDirection, swipeDirection); if (selectedIndex === newIndex) { return; @@ -513,6 +530,15 @@ const MultiView = CollectionWidget.inherit({ this.callBase(); }, + _itemOptionChanged(item, property) { + this.callBase(...arguments); + + const { selectedItem } = this.option(); + if (property === 'visible' && item === selectedItem) { + this._ensureSelectedItemIsVisible(); + } + }, + _optionChanged(args) { const { value } = args; @@ -533,6 +559,13 @@ const MultiView = CollectionWidget.inherit({ this._findBoundaryIndices(); this.callBase(args); break; + case 'selectedIndex': + if (this._isItemVisible(value)) { + this.callBase(args); + } else { + this._ensureSelectedItemIsVisible(); + } + break; default: this.callBase(args); } diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/multiView.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/multiView.tests.js index cc8ce89bda5d..b05cdcabe1a6 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/multiView.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/multiView.tests.js @@ -1659,3 +1659,225 @@ QUnit.module('swipeable disabled state', () => { assert.equal(multiView.option('swipeEnabled'), false, 'MultiView.swipeEnabled'); }); }); + +QUnit.module('selectedIndex vs item.visible', () => { + QUnit.module('on init', () => { + QUnit.test('selectedIndex should be updated to the next visible item if initially selected item is hidden', function(assert) { + const $multiView = $('#multiView').dxMultiView({ + items: [ + { text: '1', visible: true }, + { text: '2', visible: false }, + { text: '3', visible: true }, + ], + selectedIndex: 1 + }); + const instance = $multiView.dxMultiView('instance'); + + assert.strictEqual(instance.option('selectedIndex'), 2, 'selectedIndex is updated on proper index'); + }); + + QUnit.test('selectedIndex should be updated to the next visible item to the left if initially selected item is hidden and RTL is enabled', function(assert) { + const $multiView = $('#multiView').dxMultiView({ + items: [ + { text: '1', visible: true }, + { text: '2', visible: false }, + { text: '3', visible: true }, + ], + selectedIndex: 1, + rtlEnabled: true + }); + const instance = $multiView.dxMultiView('instance'); + + assert.strictEqual(instance.option('selectedIndex'), 0, 'selectedIndex is updated on proper index'); + }); + + QUnit.test('selectedIndex should be zero when all items are not visible', function(assert) { + const $multiView = $('#multiView').dxMultiView({ + items: [ + { text: '1', visible: false }, + { text: '2', visible: false }, + { text: '3', visible: false }, + ], + selectedIndex: 1 + }); + const instance = $multiView.dxMultiView('instance'); + + assert.strictEqual(instance.option('selectedIndex'), 0, 'selectedIndex is updated on proper index'); + }); + + QUnit.test('next visible item should be selected if currently selected item is hidden and loop=true', function(assert) { + const $multiView = $('#multiView').dxMultiView({ + items: [ + { text: '1', visible: true }, + { text: '2', visible: false }, + { text: '3', visible: false } + ], + selectedIndex: 2, + loop: true + }); + const instance = $multiView.dxMultiView('instance'); + + assert.strictEqual(instance.option('selectedIndex'), 0, 'selectedIndex is updated on proper index'); + }); + + QUnit.test('first visible item should be selected if current selected item is hidden and it is in the end and loop = true', function(assert) { + const $multiView = $('#multiView').dxMultiView({ + items: [ + { text: '1', visible: true }, + { text: '2', visible: true }, + { text: '3', visible: false } + ], + selectedIndex: 2, + loop: true + }); + const instance = $multiView.dxMultiView('instance'); + + assert.strictEqual(instance.option('selectedIndex'), 0, 'selectedIndex is updated on proper index'); + }); + }); + + QUnit.module('on runtime', () => { + QUnit.test('selectedIndex should be updated to the next visible item if it is changed to a hidden item', function(assert) { + const $multiView = $('#multiView').dxMultiView({ + items: [ + { text: '1', visible: true }, + { text: '2', visible: false }, + { text: '3', visible: true }, + ] + }); + const instance = $multiView.dxMultiView('instance'); + instance.option('selectedIndex', 1); + + assert.strictEqual(instance.option('selectedIndex'), 2, 'selectedIndex is updated on proper index'); + }); + + QUnit.test('selectedIndex should be set to zero if all items became hidden', function(assert) { + const $multiView = $('#multiView').dxMultiView({ + items: [ + { text: '1', visible: true }, + { text: '2', visible: false }, + { text: '3', visible: true }, + ], + selectedIndex: 2 + }); + const instance = $multiView.dxMultiView('instance'); + instance.option({ + items: [ + { text: '1', visible: false }, + { text: '2', visible: false }, + { text: '3', visible: false }, + ] + }); + + assert.strictEqual(instance.option('selectedIndex'), 0, 'selectedIndex is updated on proper index'); + }); + + QUnit.test('when hiding selected item selectedIndex should be set to next visible item', function(assert) { + const $multiView = $('#multiView').dxMultiView({ + items: [ + { text: '1', visible: true }, + { text: '2', visible: true }, + { text: '3', visible: true }, + ], + selectedIndex: 1 + }); + const instance = $multiView.dxMultiView('instance'); + instance.option('items[1].visible', false); + + assert.strictEqual(instance.option('selectedIndex'), 2, 'selectedIndex is updated on proper index'); + }); + + QUnit.test('when hiding non-selected item before selectedIndex, selectedIndex should not change', function(assert) { + const $multiView = $('#multiView').dxMultiView({ + items: [ + { text: '1', visible: true }, + { text: '2', visible: true }, + { text: '3', visible: true }, + ], + selectedIndex: 2 + }); + const instance = $multiView.dxMultiView('instance'); + instance.option('items[1].visible', false); + + assert.strictEqual(instance.option('selectedIndex'), 2, 'selectedIndex is updated on proper index'); + }); + + QUnit.test('when hiding selected item positioned in the end, next visible item to the left is selected', function(assert) { + const $multiView = $('#multiView').dxMultiView({ + items: [ + { text: '1', visible: true }, + { text: '2', visible: true }, + { text: '3', visible: true }, + ], + selectedIndex: 2 + }); + const instance = $multiView.dxMultiView('instance'); + instance.option('items[2].visible', false); + + assert.strictEqual(instance.option('selectedIndex'), 1, 'selectedIndex is updated on proper index'); + }); + + QUnit.test('when showing previously invisible item before selectedIndex, selectedIndex should not change', function(assert) { + const $multiView = $('#multiView').dxMultiView({ + items: [ + { text: '1', visible: true }, + { text: '2', visible: false }, + { text: '3', visible: true }, + ], + selectedIndex: 2 + }); + const instance = $multiView.dxMultiView('instance'); + instance.option('items[1].visible', true); + + assert.strictEqual(instance.option('selectedIndex'), 2, 'selectedIndex is updated on proper index'); + }); + + QUnit.test('when showing previously invisible item after selectedIndex, selectedIndex should not change', function(assert) { + const $multiView = $('#multiView').dxMultiView({ + items: [ + { text: '1', visible: true }, + { text: '2', visible: false }, + { text: '3', visible: true }, + ], + selectedIndex: 0 + }); + const instance = $multiView.dxMultiView('instance'); + instance.option('items[1].visible', true); + + assert.strictEqual(instance.option('selectedIndex'), 0, 'selectedIndex is updated on proper index'); + }); + + QUnit.test('when hiding last visible item selectedIndex should return to 0 index', function(assert) { + const $multiView = $('#multiView').dxMultiView({ + items: [ + { text: '1', visible: false }, + { text: '2', visible: false }, + { text: '3', visible: true }, + ], + selectedIndex: 2 + }); + const instance = $multiView.dxMultiView('instance'); + instance.option('items[2].visible', false); + + assert.strictEqual(instance.option('selectedIndex'), 0, 'selectedIndex is updated on proper index'); + }); + + QUnit.test('after removing selected item selectedIndex should be restored to 0', function(assert) { + const $multiView = $('#multiView').dxMultiView({ + items: [ + { text: '1', visible: true }, + { text: '2', visible: true }, + { text: '3', visible: true }, + ], + selectedIndex: 2 + }); + const instance = $multiView.dxMultiView('instance'); + instance.option('items', [ + { text: '1', visible: true }, + { text: '2', visible: true }, + ]); + + assert.strictEqual(instance.option('selectedIndex'), 0, 'selectedIndex is not changed'); + }); + }); +});