diff --git a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagelist/_index.scss b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagelist/_index.scss index 33e31e429754..6d7432bb2294 100644 --- a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagelist/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagelist/_index.scss @@ -15,6 +15,11 @@ width: 100%; } + &.dx-chat-messagelist-empty { + align-items: center; + justify-content: center; + } + .dx-scrollable-native { &.dx-scrollable-native-ios { .dx-scrollable-content { diff --git a/packages/devextreme-themebuilder/tests/data/dependencies.ts b/packages/devextreme-themebuilder/tests/data/dependencies.ts index f99b1965535f..50227f3a43bf 100644 --- a/packages/devextreme-themebuilder/tests/data/dependencies.ts +++ b/packages/devextreme-themebuilder/tests/data/dependencies.ts @@ -16,7 +16,7 @@ export const dependencies: FlatStylesDependencies = { buttongroup: ['validation', 'button'], dropdownbutton: ['validation', 'button', 'buttongroup', 'popup', 'loadindicator', 'loadpanel', 'scrollview', 'list'], calendar: ['validation', 'button'], - chat: ['button', 'loadindicator', 'textbox', 'validation'], + chat: ['button', 'loadindicator', 'loadpanel', 'scrollview', 'textbox', 'validation'], checkbox: ['validation'], numberbox: ['validation', 'button', 'loadindicator'], colorbox: ['validation', 'button', 'loadindicator', 'numberbox', 'textbox', 'popup'], diff --git a/packages/devextreme/js/__internal/ui/chat/chat.ts b/packages/devextreme/js/__internal/ui/chat/chat.ts index 4a214ff9b74e..6078ab24cf5e 100644 --- a/packages/devextreme/js/__internal/ui/chat/chat.ts +++ b/packages/devextreme/js/__internal/ui/chat/chat.ts @@ -66,6 +66,10 @@ class Chat extends Widget { this.option('items', newItems.slice()); } + _dataSourceLoadingChangedHandler(isLoading: boolean): void { + this._messageList?.option('isLoading', isLoading); + } + _dataSourceOptions(): DataSourceOptions { return { paginate: false }; } @@ -108,6 +112,8 @@ class Chat extends Widget { this._messageList = this._createComponent($messageList, MessageList, { items, currentUserId, + // @ts-expect-error + isLoading: this._dataController.isLoading(), }); } diff --git a/packages/devextreme/js/__internal/ui/chat/messagelist.ts b/packages/devextreme/js/__internal/ui/chat/messagelist.ts index e21a2fef8232..f30b4b930616 100644 --- a/packages/devextreme/js/__internal/ui/chat/messagelist.ts +++ b/packages/devextreme/js/__internal/ui/chat/messagelist.ts @@ -2,13 +2,14 @@ import Guid from '@js/core/guid'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import resizeObserverSingleton from '@js/core/resize_observer'; +import { noop } from '@js/core/utils/common'; import dateSerialization from '@js/core/utils/date_serialization'; import { isElementInDom } from '@js/core/utils/dom'; import { isDefined } from '@js/core/utils/type'; import messageLocalization from '@js/localization/message'; import { getScrollTopMax } from '@js/renovation/ui/scroll_view/utils/get_scroll_top_max'; import type { Message } from '@js/ui/chat'; -import Scrollable from '@js/ui/scroll_view/ui.scrollable'; +import ScrollView from '@js/ui/scroll_view'; import type { WidgetOptions } from '@js/ui/widget/ui.widget'; import type { OptionChanged } from '@ts/core/widget/types'; import Widget from '@ts/core/widget/widget'; @@ -18,6 +19,7 @@ import type { MessageGroupAlignment } from './messagegroup'; import MessageGroup from './messagegroup'; const CHAT_MESSAGELIST_CLASS = 'dx-chat-messagelist'; +const CHAT_MESSAGELIST_EMPTY_CLASS = 'dx-chat-messagelist-empty'; const CHAT_MESSAGELIST_EMPTY_VIEW_CLASS = 'dx-chat-messagelist-empty-view'; const CHAT_MESSAGELIST_EMPTY_IMAGE_CLASS = 'dx-chat-messagelist-empty-image'; @@ -30,6 +32,7 @@ export const MESSAGEGROUP_TIMEOUT = 5 * 1000 * 60; export interface Properties extends WidgetOptions { items: Message[]; currentUserId: number | string | undefined; + isLoading?: boolean; } class MessageList extends Widget { @@ -37,13 +40,14 @@ class MessageList extends Widget { private _containerClientHeight!: number; - private _scrollable!: Scrollable; + private _scrollView!: ScrollView; _getDefaultOptions(): Properties { return { ...super._getDefaultOptions(), items: [], currentUserId: '', + isLoading: false, }; } @@ -58,7 +62,7 @@ class MessageList extends Widget { super._initMarkup(); - this._renderScrollable(); + this._renderScrollView(); this._renderMessageListContent(); this._updateAria(); } @@ -90,12 +94,12 @@ class MessageList extends Widget { const heightChange = this._containerClientHeight - newHeight; const isHeightDecreasing = heightChange > 0; - let scrollTop = this._scrollable.scrollTop(); + let scrollTop = this._scrollView.scrollTop(); if (isHeightDecreasing) { scrollTop += heightChange; - this._scrollable.scrollTo({ top: scrollTop }); + this._scrollView.scrollTo({ top: scrollTop }); } } @@ -103,6 +107,8 @@ class MessageList extends Widget { } _renderEmptyViewContent(): void { + this.$element().addClass(CHAT_MESSAGELIST_EMPTY_CLASS); + const $emptyView = $('
') .addClass(CHAT_MESSAGELIST_EMPTY_VIEW_CLASS) .attr('id', `dx-${new Guid()}`); @@ -129,6 +135,7 @@ class MessageList extends Widget { } _removeEmptyView(): void { + this.$element().removeClass(CHAT_MESSAGELIST_EMPTY_CLASS); this._$content().empty(); } @@ -159,19 +166,34 @@ class MessageList extends Widget { this._messageGroups?.push(messageGroup); } - _renderScrollable(): void { + _renderScrollView(): void { const $scrollable = $('
') .appendTo(this.$element()); - this._scrollable = this._createComponent($scrollable, Scrollable, { + this._scrollView = this._createComponent($scrollable, ScrollView, { useKeyboard: false, bounceEnabled: false, + onReachBottom: noop, + reachBottomText: '', + indicateLoading: false, }); } + _updateLoadingState(isLoading: boolean): void { + if (!this._scrollView) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._scrollView.release(!isLoading); + } + _renderMessageListContent(): void { - if (this._isEmpty()) { + const { isLoading } = this.option(); + + if (this._isEmpty() && !isLoading) { this._renderEmptyViewContent(); + this._updateLoadingState(false); return; } @@ -204,6 +226,10 @@ class MessageList extends Widget { this._createMessageGroupComponent(currentMessageGroupItems, currentMessageGroupUserId); } }); + + // @ts-expect-error + this._updateLoadingState(isLoading); + this._scrollContentToLastMessage(); } _renderMessage(message: Message): void { @@ -232,17 +258,17 @@ class MessageList extends Widget { } _$content(): dxElementWrapper { - return $(this._scrollable.content()); + return $(this._scrollView.content()); } _scrollContentToLastMessage(): void { - this._scrollable.scrollTo({ + this._scrollView.scrollTo({ top: getScrollTopMax(this._scrollableContainer()), }); } _scrollableContainer(): Element { - return $(this._scrollable.element()).find(`.${SCROLLABLE_CONTAINER_CLASS}`).get(0); + return $(this._scrollView.element()).find(`.${SCROLLABLE_CONTAINER_CLASS}`).get(0); } _isMessageAddedToEnd(value: Message[], previousValue: Message[]): boolean { @@ -326,6 +352,8 @@ class MessageList extends Widget { case 'items': this._processItemsUpdating(value ?? [], previousValue ?? []); break; + case 'isLoading': + break; default: super._optionChanged(args); } diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js index 0fd3daca147e..15c71c619c63 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js @@ -20,6 +20,7 @@ const CHAT_MESSAGEBOX_CLASS = 'dx-chat-messagebox'; const CHAT_MESSAGEBOX_BUTTON_CLASS = 'dx-chat-messagebox-button'; const CHAT_MESSAGEBOX_TEXTAREA_CLASS = 'dx-chat-messagebox-textarea'; const CHAT_MESSAGELIST_EMPTY_VIEW_CLASS = 'dx-chat-messagelist-empty-view'; +const SCROLLVIEW_REACHBOTTOM_INDICATOR = 'dx-scrollview-scrollbottom'; const TEXTEDITOR_INPUT_CLASS = 'dx-texteditor-input'; @@ -29,6 +30,8 @@ export const MOCK_COMPANION_USER_ID = 'COMPANION_USER_ID'; export const MOCK_CURRENT_USER_ID = 'CURRENT_USER_ID'; export const NOW = '1721747399083'; +const RELEASE_TIMEOUT = 800; + export const userFirst = { id: MOCK_COMPANION_USER_ID, name: 'First', @@ -428,13 +431,19 @@ QUnit.module('Chat', moduleConfig, () => { QUnit.module('Data Layer Integration', moduleConfig, () => { QUnit.test('Should render empty view container if dataSource is empty', function(assert) { + const done = assert.async(); + this.reinit({ dataSource: { store: new ArrayStore([]) } }); - assert.strictEqual(this.$element.find(`.${CHAT_MESSAGELIST_EMPTY_VIEW_CLASS}`).length, 1); + setTimeout(() => { + assert.strictEqual(this.$element.find(`.${CHAT_MESSAGELIST_EMPTY_VIEW_CLASS}`).length, 1); + + done(); + }, RELEASE_TIMEOUT); }); QUnit.test('Should remove or render empty view container after dataSource is updated at runtime', function(assert) { @@ -559,87 +568,115 @@ QUnit.module('Chat', moduleConfig, () => { }); QUnit.test('should render all messages correctly when using an asynchronous data source', function(assert) { - const clock = sinon.useFakeTimers(); - - try { - const messages = [{ text: 'message_1' }, { text: 'message_2' }]; - const timeout = 1000; - - const store = new CustomStore({ - load: function() { - const d = $.Deferred(); - setTimeout(function() { - d.resolve(messages); - }, timeout); - return d.promise(); - }, - }); + const done = assert.async(); + const messages = [{ text: 'message_1' }, { text: 'message_2' }]; + const timeout = 1000; + + const store = new CustomStore({ + load: function() { + const d = $.Deferred(); + setTimeout(function() { + d.resolve(messages); + }, timeout); + return d.promise(); + }, + }); - this.reinit({ - dataSource: store, - }); + this.reinit({ + dataSource: store, + }); - assert.strictEqual(this.getEmptyView().length, 1, 'empty messagelist view should be rendered'); - assert.strictEqual(this.getBubbles().length, 0, 'there should be no message bubbles rendered'); + const $indicator = this.$element.find(`.${SCROLLVIEW_REACHBOTTOM_INDICATOR}`); - clock.tick(timeout / 2); + assert.strictEqual(this.getEmptyView().length, 0, 'Empty view should not be rendered'); + assert.strictEqual(this.getBubbles().length, 0, 'No message bubbles should be rendered initially'); + assert.strictEqual($indicator.is(':visible'), true, 'Loading indicator is visible'); - assert.strictEqual(this.getEmptyView().length, 1, 'empty messagelist view should still be visible while data is loading'); + setTimeout(() => { + assert.strictEqual(this.getEmptyView().length, 0, 'empty messagelist view should still be not rendered while data is loading'); assert.strictEqual(this.getBubbles().length, 0, 'should still be no message bubbles rendered while data is loading'); + assert.strictEqual($indicator.is(':visible'), true, 'Loading indicator is visible'); - clock.tick(timeout / 2); - - assert.strictEqual(this.getEmptyView().length, 0, 'empty messagelist view should not be visible when data is loaded'); - assert.strictEqual(this.getBubbles().length, 2, 'message bubbles should be rendered when data is loaded'); + setTimeout(() => { + assert.strictEqual(this.getEmptyView().length, 0, 'empty messagelist view should not be rendered when data is loaded'); + assert.strictEqual(this.getBubbles().length, 2, 'Message bubbles rendered'); + assert.strictEqual($indicator.is(':visible'), false, 'Loading indicator is hidden'); - } finally { - clock.restore(); - } + done(); + }, timeout / 2); + }, timeout / 2); }); QUnit.test('new message should be rendered when using an asynchronous custom store', function(assert) { - const clock = sinon.useFakeTimers(); - - try { - const messages = [{ text: 'message_1' }, { text: 'message_2' }]; - const timeout = 1000; - - const store = new CustomStore({ - load: function() { - const d = $.Deferred(); - setTimeout(function() { - d.resolve(messages); - }, timeout); - return d.promise(); - }, - insert: function(values) { - const d = $.Deferred(); - - setTimeout(() => { - messages.push(values); - d.resolve(values); - }, timeout); + const done = assert.async(); + const messages = [{ text: 'message_1' }, { text: 'message_2' }]; + const timeout = 1000; + + const store = new CustomStore({ + load: function() { + const d = $.Deferred(); + setTimeout(function() { + d.resolve(messages); + }, timeout); + return d.promise(); + }, + insert: function(values) { + const d = $.Deferred(); - return d.promise(); - }, - }); + setTimeout(() => { + messages.push(values); + d.resolve(values); + }, timeout); - this.reinit({ - dataSource: store, - }); + return d.promise(); + }, + }); - clock.tick(timeout); + this.reinit({ + dataSource: store, + }); + setTimeout(() => { const newMessage = { text: 'message_3' }; this.instance.renderMessage(newMessage); - clock.tick(timeout * 2); + setTimeout(() => { + assert.deepEqual(this.instance.option('items'), messages, 'items option should contain all messages including the new one'); + assert.strictEqual(this.getBubbles().length, 3, 'new message should be rendered in list'); - assert.deepEqual(this.instance.option('items'), messages, 'items option should contain all messages including the new one'); - assert.strictEqual(this.getBubbles().length, 3, 'new message should be rendered in list'); - } finally { - clock.restore(); - } + done(); + }, timeout * 2); + }, timeout); + }); + + QUnit.test('Loading and Empty view should not be shown at the same time when the dataSource option changes', function(assert) { + const done = assert.async(); + const messages = [{ text: 'message_1' }, { text: 'message_2' }]; + const timeout = 400; + + const store = new CustomStore({ + load: function() { + const d = $.Deferred(); + setTimeout(function() { + d.resolve(messages); + }, timeout); + return d.promise(); + }, + }); + + this.reinit({ dataSource: store }); + + assert.strictEqual(this.getEmptyView().length, 0, 'empty view is not rendered'); + let $indicator = this.$element.find(`.${SCROLLVIEW_REACHBOTTOM_INDICATOR}`); + assert.strictEqual($indicator.is(':visible'), true, 'loading indicator is visible'); + + setTimeout(() => { + $indicator = this.$element.find(`.${SCROLLVIEW_REACHBOTTOM_INDICATOR}`); + assert.strictEqual($indicator.is(':visible'), false, 'loading indicator is hidden'); + assert.strictEqual(this.getEmptyView().length, 0, 'empty view was removed'); + + done(); + }, timeout * 2); }); }); }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageList.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageList.markup.tests.js index a7b08cf29732..22c10ca9c3d4 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageList.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageList.markup.tests.js @@ -4,8 +4,8 @@ import MessageList from '__internal/ui/chat/messagelist'; const CHAT_MESSAGELIST_CLASS = 'dx-chat-messagelist'; const SCROLLABLE_CLASS = 'dx-scrollable'; -const SCROLLABLE_CONTENT_CLASS = 'dx-scrollable-content'; - +const SCROLLVIEW_CONTENT_CLASS = 'dx-scrollview-content'; +const CHAT_MESSAGELIST_EMPTY_CLASS = 'dx-chat-messagelist-empty'; const CHAT_MESSAGELIST_EMPTY_VIEW_CLASS = 'dx-chat-messagelist-empty-view'; const CHAT_MESSAGELIST_EMPTY_IMAGE_CLASS = 'dx-chat-messagelist-empty-image'; const CHAT_MESSAGELIST_EMPTY_MESSAGE_CLASS = 'dx-chat-messagelist-empty-message'; @@ -40,11 +40,37 @@ QUnit.module('MessageList', moduleConfig, () => { QUnit.test('should contain scrollable element', function(assert) { assert.strictEqual(this.$element.children().first().hasClass(SCROLLABLE_CLASS), true); }); + + QUnit.test('should have empty class if there are no messages', function(assert) { + this.reinit({ + items: [] + }); + + assert.strictEqual(this.$element.hasClass(CHAT_MESSAGELIST_EMPTY_CLASS), true); + }); + + QUnit.test('should not have empty class if there are no messages', function(assert) { + this.reinit({ + items: [{}] + }); + + assert.strictEqual(this.$element.hasClass(CHAT_MESSAGELIST_EMPTY_CLASS), false); + }); + + QUnit.test('empty should be toggled after items are updated at runtime', function(assert) { + this.instance.option('items', [{}]); + + assert.strictEqual(this.$element.hasClass(CHAT_MESSAGELIST_EMPTY_CLASS), false); + + this.instance.option('items', []); + + assert.strictEqual(this.$element.hasClass(CHAT_MESSAGELIST_EMPTY_CLASS), true); + }); }); QUnit.module('Empty view', () => { QUnit.test('element should be placed inside of a scrollable content', function(assert) { - assert.strictEqual(this.$element.find(`.${SCROLLABLE_CONTENT_CLASS}`).children().first().hasClass(CHAT_MESSAGELIST_EMPTY_VIEW_CLASS), true); + assert.strictEqual(this.$element.find(`.${SCROLLVIEW_CONTENT_CLASS}`).children().first().hasClass(CHAT_MESSAGELIST_EMPTY_VIEW_CLASS), true); }); QUnit.test('container should be rendered if there are no messages', function(assert) { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageList.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageList.tests.js index 1bcc29ba0943..151385292a23 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageList.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageList.tests.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import MessageList, { MESSAGEGROUP_TIMEOUT } from '__internal/ui/chat/messagelist'; -import Scrollable from 'ui/scroll_view/ui.scrollable'; +import ScrollView from 'ui/scroll_view'; import { generateMessages, userFirst, @@ -18,8 +18,9 @@ const CHAT_MESSAGEBUBBLE_CLASS = 'dx-chat-messagebubble'; const CHAT_MESSAGELIST_EMPTY_MESSAGE_CLASS = 'dx-chat-messagelist-empty-message'; const CHAT_MESSAGELIST_EMPTY_PROMPT_CLASS = 'dx-chat-messagelist-empty-prompt'; -const SCROLLABLE_CLASS = 'dx-scrollable'; +const SCROLLVIEW_REACHBOTTOM_INDICATOR = 'dx-scrollview-scrollbottom'; +const SCROLLVIEW_CLASS = 'dx-scrollview'; const moduleConfig = { beforeEach: function() { @@ -30,9 +31,9 @@ const moduleConfig = { this.instance = new MessageList($(selector), options); this.$element = $(this.instance.$element()); - this.getScrollable = () => Scrollable.getInstance(this.$element.find(`.${SCROLLABLE_CLASS}`)); + this.getScrollView = () => ScrollView.getInstance(this.$element.find(`.${SCROLLVIEW_CLASS}`)); - this.scrollable = this.getScrollable(); + this.scrollView = this.getScrollView(); }; this.reinit = (options, selector) => { @@ -67,11 +68,11 @@ QUnit.module('MessageList', moduleConfig, () => { } }); - QUnit.test('scrollable should be rendered inside root element', function(assert) { - assert.ok(Scrollable.getInstance(this.$element.children().first()) instanceof Scrollable); + QUnit.test('scrollView should be rendered inside root element', function(assert) { + assert.ok(ScrollView.getInstance(this.$element.children().first()) instanceof ScrollView); }); - QUnit.test('Message Group should be rendered in the scrollable content', function(assert) { + QUnit.test('Message Group should be rendered in the scrollView content', function(assert) { const newMessage = { author: { id: MOCK_CURRENT_USER_ID }, timestamp: NOW, @@ -80,7 +81,7 @@ QUnit.module('MessageList', moduleConfig, () => { this.reinit({ items: [newMessage] }); - const $messageGroups = $(this.scrollable.content()).find(`.${CHAT_MESSAGEGROUP_CLASS}`); + const $messageGroups = $(this.scrollView.content()).find(`.${CHAT_MESSAGEGROUP_CLASS}`); assert.strictEqual($messageGroups.length, 1); }); @@ -94,7 +95,7 @@ QUnit.module('MessageList', moduleConfig, () => { this.instance.option({ items: [newMessage] }); - const $messageGroups = $(this.scrollable.content()).find(`.${CHAT_MESSAGEGROUP_CLASS}`); + const $messageGroups = $(this.scrollView.content()).find(`.${CHAT_MESSAGEGROUP_CLASS}`); assert.strictEqual($messageGroups.length, 1); }); @@ -112,7 +113,7 @@ QUnit.module('MessageList', moduleConfig, () => { this.instance.option({ items: [...items, newMessage] }); - const $messageGroups = $(this.scrollable.content()).find(`.${CHAT_MESSAGEGROUP_CLASS}`); + const $messageGroups = $(this.scrollView.content()).find(`.${CHAT_MESSAGEGROUP_CLASS}`); assert.strictEqual($messageGroups.length, 27); }); @@ -120,11 +121,55 @@ QUnit.module('MessageList', moduleConfig, () => { QUnit.test('Message Group should be rendered in the scrollable content after updating items at runtime', function(assert) { this.instance.option({ items: generateMessages(52) }); - const scrollableContent = this.getScrollable().content(); + const scrollableContent = this.getScrollView().content(); const $messageGroups = $(scrollableContent).find(`.${CHAT_MESSAGEGROUP_CLASS}`); assert.strictEqual($messageGroups.length, 26); }); + + QUnit.test('loading indicator should be hidden if isLoading is set to false', function(assert) { + this.reinit({ + items: [], + isLoading: false + }); + + const $indicator = this.$element.find(`.${SCROLLVIEW_REACHBOTTOM_INDICATOR}`); + assert.strictEqual($indicator.is(':visible'), false); + }); + + QUnit.test('loading indicator should be shown if isLoading is set to true', function(assert) { + this.reinit({ + items: [], + isLoading: true + }); + + const $indicator = this.$element.find(`.${SCROLLVIEW_REACHBOTTOM_INDICATOR}`); + assert.strictEqual($indicator.is(':visible'), true); + }); + + QUnit.test('loading indicator should be hidden if isLoading is set to false and items is not empty', function(assert) { + this.reinit({ + items: [ + { author: { id: 'UserID' } }, + ], + isLoading: false + }); + + const $indicator = this.$element.find(`.${SCROLLVIEW_REACHBOTTOM_INDICATOR}`); + assert.strictEqual($indicator.is(':visible'), false); + }); + + QUnit.test('loading indicator should be shown if isLoading is set to true and items is not empty', function(assert) { + this.reinit({ + items: [ + { author: { id: 'UserID' } }, + ], + isLoading: true + }); + + const $indicator = this.$element.find(`.${SCROLLVIEW_REACHBOTTOM_INDICATOR}`); + assert.strictEqual($indicator.is(':visible'), true); + }); }); QUnit.module('MessageGroup integration', () => { @@ -474,11 +519,11 @@ QUnit.module('MessageList', moduleConfig, () => { }); }); - QUnit.module('Scrollable', { + QUnit.module('ScrollView', { beforeEach: function() { this.getScrollOffsetMax = () => { - const scrollable = this.getScrollable(); - return $(scrollable.content()).height() - $(scrollable.container()).height(); + const scrollView = this.getScrollView(); + return $(scrollView.content()).height() - $(scrollView.container()).height(); }; this._resizeTimeout = 40; }, @@ -487,10 +532,12 @@ QUnit.module('MessageList', moduleConfig, () => { const expectedOptions = { bounceEnabled: false, useKeyboard: false, + indicateLoading: false, + reachBottomText: '', }; Object.entries(expectedOptions).forEach(([key, value]) => { - assert.deepEqual(value, this.scrollable.option(key), `${key} value is correct`); + assert.deepEqual(value, this.scrollView.option(key), `${key} value is correct`); }); }); @@ -504,7 +551,7 @@ QUnit.module('MessageList', moduleConfig, () => { }); setTimeout(() => { - const scrollTop = this.scrollable.scrollTop(); + const scrollTop = this.scrollView.scrollTop(); assert.notEqual(scrollTop, 0, 'scroll position should not be 0 after initialization'); assert.roughEqual(scrollTop, this.getScrollOffsetMax(), 1, 'scroll position should be at the bottom after initialization'); @@ -522,7 +569,7 @@ QUnit.module('MessageList', moduleConfig, () => { this.instance.option('items', generateMessages(52)); setTimeout(() => { - const scrollTop = this.getScrollable().scrollTop(); + const scrollTop = this.getScrollView().scrollTop(); assert.notEqual(scrollTop, 0, 'scroll position should not be 0 after items are updated at runtime'); assert.roughEqual(scrollTop, this.getScrollOffsetMax(), 1, 'scroll position should be at the bottom after items are updated at runtime'); @@ -530,10 +577,30 @@ QUnit.module('MessageList', moduleConfig, () => { }); }); + QUnit.test('should be scrolled to last message if items changed at runtime with an invalidate call', function(assert) { + const done = assert.async(); + this.reinit({ + width: 300, + height: 500, + items: generateMessages(52) + }); + + setTimeout(() => { + this.instance.option('items', generateMessages(30)); + + const scrollTop = this.getScrollView().scrollTop(); + + assert.notEqual(scrollTop, 0, 'scroll position should not be 0 after items are updated at runtime'); + assert.roughEqual(scrollTop, this.getScrollOffsetMax(), 1, 'scroll position should be at the bottom after items are updated at runtime'); + + done(); + }, this._resizeTimeout); + }); + [MOCK_CURRENT_USER_ID, MOCK_COMPANION_USER_ID].forEach(id => { const isCurrentUser = id === MOCK_CURRENT_USER_ID; - QUnit.test(`Scrollable should be scrolled to last message after render ${isCurrentUser ? 'current user' : 'companion'} message`, function(assert) { + QUnit.test(`ScrollView should be scrolled to last message after render ${isCurrentUser ? 'current user' : 'companion'} message`, function(assert) { const done = assert.async(); assert.expect(2); const items = generateMessages(31); @@ -554,7 +621,7 @@ QUnit.module('MessageList', moduleConfig, () => { this.instance.option('items', [...items, newMessage]); setTimeout(() => { - const scrollTop = this.getScrollable().scrollTop(); + const scrollTop = this.getScrollView().scrollTop(); assert.notEqual(scrollTop, 0, 'scroll position should not be 0 after a new message is rendered'); assert.roughEqual(scrollTop, this.getScrollOffsetMax(), 1, 'scroll position should be at the bottom after rendering the new message'); @@ -578,14 +645,14 @@ QUnit.module('MessageList', moduleConfig, () => { this.$element.css('height', 400); setTimeout(() => { - const scrollTop = this.getScrollable().scrollTop(); + const scrollTop = this.getScrollView().scrollTop(); assert.strictEqual(scrollTop, 0, 'scroll position should be 0 when the element is hidden'); $('#qunit-fixture').css('display', 'block'); setTimeout(() => { - const scrollTop = this.getScrollable().scrollTop(); + const scrollTop = this.getScrollView().scrollTop(); assert.notEqual(scrollTop, 0, 'scroll position should change after the element is made visible'); assert.roughEqual(scrollTop, this.getScrollOffsetMax(), 1, 'scroll position should be at the bottom after element becomes visible'); @@ -610,14 +677,14 @@ QUnit.module('MessageList', moduleConfig, () => { this.$element.css('height', 400); setTimeout(() => { - const scrollTop = this.getScrollable().scrollTop(); + const scrollTop = this.getScrollView().scrollTop(); assert.strictEqual(scrollTop, 0, 'scroll position should be 0 while the element is detached'); $messageList.appendTo('#qunit-fixture'); setTimeout(() => { - const scrollTop = this.getScrollable().scrollTop(); + const scrollTop = this.getScrollView().scrollTop(); assert.notEqual(scrollTop, 0, 'scroll position should change after the element is attached to the DOM'); assert.roughEqual(scrollTop, this.getScrollOffsetMax(), 1, 'scroll position should be at the bottom after attachment'); @@ -639,14 +706,14 @@ QUnit.module('MessageList', moduleConfig, () => { }); setTimeout(() => { - const scrollTop = this.getScrollable().scrollTop(); + const scrollTop = this.getScrollView().scrollTop(); assert.roughEqual(scrollTop, this.getScrollOffsetMax(), 1, 'scroll position should be at the bottom after initialization'); this.instance.option('height', 300); setTimeout(() => { - const scrollTop = this.getScrollable().scrollTop(); + const scrollTop = this.getScrollView().scrollTop(); assert.roughEqual(scrollTop, this.getScrollOffsetMax(), 1, 'max scroll position should be saved after reducing height'); @@ -667,14 +734,14 @@ QUnit.module('MessageList', moduleConfig, () => { }); setTimeout(() => { - const scrollTop = this.getScrollable().scrollTop(); + const scrollTop = this.getScrollView().scrollTop(); assert.roughEqual(scrollTop, this.getScrollOffsetMax(), 1, 'scroll position should be at the bottom after initialization'); this.instance.option('height', 700); setTimeout(() => { - const scrollTop = this.getScrollable().scrollTop(); + const scrollTop = this.getScrollView().scrollTop(); assert.roughEqual(scrollTop, this.getScrollOffsetMax(), 1, 'max scroll position should be saved after increasing height'); @@ -695,15 +762,15 @@ QUnit.module('MessageList', moduleConfig, () => { }); setTimeout(() => { - const scrollTop = this.getScrollable().scrollTop(); + const scrollTop = this.getScrollView().scrollTop(); assert.roughEqual(scrollTop, this.getScrollOffsetMax(), 1, 'scroll position should be at the bottom after initialization'); - this.getScrollable().scrollTo({ top: this.getScrollOffsetMax() - 200 }); + this.getScrollView().scrollTo({ top: this.getScrollOffsetMax() - 200 }); this.instance.option('height', 300); setTimeout(() => { - const scrollTop = this.getScrollable().scrollTop(); + const scrollTop = this.getScrollView().scrollTop(); assert.roughEqual(scrollTop, this.getScrollOffsetMax() - 200, 1, 'scroll position should be set correctly after reducing height'); @@ -724,16 +791,16 @@ QUnit.module('MessageList', moduleConfig, () => { }); setTimeout(() => { - const scrollTop = this.getScrollable().scrollTop(); + const scrollTop = this.getScrollView().scrollTop(); assert.roughEqual(scrollTop, this.getScrollOffsetMax(), 1, 'scroll position should be at the bottom after initialization'); const newScrollTop = this.getScrollOffsetMax() - 200; - this.getScrollable().scrollTo({ top: newScrollTop }); + this.getScrollView().scrollTo({ top: newScrollTop }); this.instance.option('height', 600); setTimeout(() => { - const scrollTop = this.getScrollable().scrollTop(); + const scrollTop = this.getScrollView().scrollTop(); assert.roughEqual(scrollTop, newScrollTop, 1, 'scroll position should be saved correctly after increasing height'); @@ -756,11 +823,11 @@ QUnit.module('MessageList', moduleConfig, () => { setTimeout(() => { const newScrollTop = this.getScrollOffsetMax() - 200; - this.getScrollable().scrollTo({ top: newScrollTop }); + this.getScrollView().scrollTo({ top: newScrollTop }); this.instance.option('height', 800); setTimeout(() => { - const scrollTop = this.getScrollable().scrollTop(); + const scrollTop = this.getScrollView().scrollTop(); assert.roughEqual(scrollTop, this.getScrollOffsetMax(), 1.01, 'scroll position should be limited to the max scrollable offset after increasing height'); diff --git a/packages/devextreme/testing/tests/DevExpress.ui/defaultOptions.tests.js b/packages/devextreme/testing/tests/DevExpress.ui/defaultOptions.tests.js index 70c14aae1e52..c41b5f895ddd 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui/defaultOptions.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui/defaultOptions.tests.js @@ -1394,6 +1394,7 @@ testComponentDefaults(ChatMessageList, {}, { currentUserId: '', + isLoading: false, } );