From 4c2a26f1fc5118cd03bfa08f18c229bbf947d760 Mon Sep 17 00:00:00 2001 From: Andrei Vorobev Date: Wed, 24 Jun 2026 17:14:29 +0300 Subject: [PATCH] Chat: added scroll to bottom button and story --- .../chat/layout/chat-messagelist/_index.scss | 19 +++++++ .../chat/layout/chat-messagelist/_mixins.scss | 6 +++ .../scss/widgets/fluent/chat/_colors.scss | 1 + .../scss/widgets/fluent/chat/_index.scss | 3 ++ .../scss/widgets/generic/chat/_colors.scss | 1 + .../scss/widgets/generic/chat/_index.scss | 3 ++ .../scss/widgets/material/chat/_colors.scss | 1 + .../scss/widgets/material/chat/_index.scss | 3 ++ .../js/__internal/ui/chat/messagelist.ts | 49 +++++++++++++++++++ .../js/localization/messages/en.json | 1 + 10 files changed, 87 insertions(+) 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 0101d8e6b773..69785c8141e3 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 @@ -4,6 +4,7 @@ flex-grow: 1; min-height: 3.57em; overflow: hidden; + position: relative; .dx-scrollable-container { overscroll-behavior: contain; @@ -90,3 +91,21 @@ text-align: center; } +.dx-button.dx-chat-messagelist-scroll-to-bottom-button { + position: absolute; + bottom: 16px; + left: 50%; + transform: translateX(-50%); + z-index: 1; + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: opacity 0.2s ease, visibility 0.2s ease; + + &.dx-chat-messagelist-scroll-to-bottom-button-visible { + opacity: 1; + visibility: visible; + pointer-events: auto; + } +} + diff --git a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagelist/_mixins.scss b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagelist/_mixins.scss index 612aaaad8857..cdf2e00505d4 100644 --- a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagelist/_mixins.scss +++ b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-messagelist/_mixins.scss @@ -70,6 +70,12 @@ } } +@mixin chat-messagelist-scroll-to-bottom-button($box-shadow) { + .dx-chat-messagelist-scroll-to-bottom-button { + box-shadow: $box-shadow; + } +} + @mixin chat-messagelist-contextmenu( $delete-button-color, $delete-button-focused-color, diff --git a/packages/devextreme-scss/scss/widgets/fluent/chat/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/chat/_colors.scss index bbfb29fb2f23..dc4621fdf849 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/chat/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/chat/_colors.scss @@ -192,3 +192,4 @@ $chat-typingindicator-bubble-bg-color: $chat-bubble-background-color-secondary ! $chat-messagelist-contextmenu-delete-button-color: $base-danger !default; $chat-messagelist-contextmenu-delete-button-focused-color: $base-danger !default; $chat-messagelist-contextmenu-delete-button-focused-bg: $base-hover-bg !default; +$chat-scroll-to-bottom-button-box-shadow: $chat-file-container-box-shadow !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss index 4ea1eff3b5c0..975b5e8012a9 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss @@ -103,6 +103,9 @@ $chat-messagelist-contextmenu-delete-button-focused-color, $chat-messagelist-contextmenu-delete-button-focused-bg, ); +@include chat-messagelist-scroll-to-bottom-button( + $chat-scroll-to-bottom-button-box-shadow, +); @include chat-typingindicator( $chat-typingindicator-template, $chat-typingindicator-padding, diff --git a/packages/devextreme-scss/scss/widgets/generic/chat/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/chat/_colors.scss index dfc46957e083..a9d34c18645f 100644 --- a/packages/devextreme-scss/scss/widgets/generic/chat/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/generic/chat/_colors.scss @@ -332,3 +332,4 @@ $chat-typingindicator-bubble-bg-color: $chat-bubble-background-color-secondary ! $chat-messagelist-contextmenu-delete-button-color: $base-danger !default; $chat-messagelist-contextmenu-delete-button-focused-color: $base-inverted-text-color !default; $chat-messagelist-contextmenu-delete-button-focused-bg: $base-danger !default; +$chat-scroll-to-bottom-button-box-shadow: $chat-file-container-box-shadow !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss b/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss index 9c6e1adc05e3..7fcf9a4f9fe5 100644 --- a/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss @@ -106,6 +106,9 @@ $chat-messagelist-contextmenu-delete-button-focused-color, $chat-messagelist-contextmenu-delete-button-focused-bg, ); +@include chat-messagelist-scroll-to-bottom-button( + $chat-scroll-to-bottom-button-box-shadow, +); @include chat-typingindicator( $chat-typingindicator-template, $chat-typingindicator-padding, diff --git a/packages/devextreme-scss/scss/widgets/material/chat/_colors.scss b/packages/devextreme-scss/scss/widgets/material/chat/_colors.scss index d325d8f9ffbe..eaf7a11993b9 100644 --- a/packages/devextreme-scss/scss/widgets/material/chat/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/material/chat/_colors.scss @@ -151,3 +151,4 @@ $chat-suggestions-box-shadow: null !default; $chat-messagelist-contextmenu-delete-button-color: $base-danger !default; $chat-messagelist-contextmenu-delete-button-focused-color: $base-danger !default; $chat-messagelist-contextmenu-delete-button-focused-bg: $base-hover-bg !default; +$chat-scroll-to-bottom-button-box-shadow: $chat-file-container-box-shadow !default; diff --git a/packages/devextreme-scss/scss/widgets/material/chat/_index.scss b/packages/devextreme-scss/scss/widgets/material/chat/_index.scss index 4edb1b5eeaf8..a1c5b5bcc2e0 100644 --- a/packages/devextreme-scss/scss/widgets/material/chat/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/chat/_index.scss @@ -104,6 +104,9 @@ $chat-messagelist-contextmenu-delete-button-focused-color, $chat-messagelist-contextmenu-delete-button-focused-bg, ); +@include chat-messagelist-scroll-to-bottom-button( + $chat-scroll-to-bottom-button-box-shadow, +); @include chat-typingindicator( $chat-typingindicator-template, $chat-typingindicator-padding, diff --git a/packages/devextreme/js/__internal/ui/chat/messagelist.ts b/packages/devextreme/js/__internal/ui/chat/messagelist.ts index cc0e6ccb55c6..f4e531b14d7e 100644 --- a/packages/devextreme/js/__internal/ui/chat/messagelist.ts +++ b/packages/devextreme/js/__internal/ui/chat/messagelist.ts @@ -13,6 +13,8 @@ import { isElementInDom } from '@js/core/utils/dom'; import { getHeight } from '@js/core/utils/size'; import { isDate, isDefined } from '@js/core/utils/type'; import type { DxEvent } from '@js/events'; +import type { Properties as ButtonProperties } from '@js/ui/button'; +import Button from '@js/ui/button'; import type { AttachmentDownloadClickEvent, Message, TextMessage, User, } from '@js/ui/chat'; @@ -48,6 +50,9 @@ const CHAT_MESSAGELIST_CLASS = 'dx-chat-messagelist'; const CHAT_MESSAGELIST_CONTENT_CLASS = 'dx-chat-messagelist-content'; const CHAT_MESSAGELIST_EMPTY_CLASS = 'dx-chat-messagelist-empty'; const CHAT_MESSAGELIST_EMPTY_LOADING_CLASS = 'dx-chat-messagelist-empty-loading'; +const CHAT_MESSAGELIST_SCROLL_TO_BOTTOM_BUTTON_CLASS = 'dx-chat-messagelist-scroll-to-bottom-button'; +const CHAT_MESSAGELIST_SCROLL_TO_BOTTOM_BUTTON_VISIBLE_CLASS = 'dx-chat-messagelist-scroll-to-bottom-button-visible'; +const SCROLL_TO_BOTTOM_THRESHOLD = 0.15; const CHAT_MESSAGELIST_EMPTY_VIEW_CLASS = 'dx-chat-messagelist-empty-view'; export const CHAT_MESSAGELIST_EMPTY_IMAGE_CLASS = 'dx-chat-messagelist-empty-image'; @@ -123,6 +128,8 @@ class MessageList extends Widget { private _contextMenu!: ContextMenu; + private _scrollToBottomButton!: Button; + private _$content!: dxElementWrapper; _getDefaultOptions(): Properties { @@ -163,6 +170,7 @@ class MessageList extends Widget { this._renderMessageGroups(); this._renderTypingIndicator(); this._renderContextMenu(); + this._renderScrollToBottomButton(); this._updateAria(); this._scrollDownContent(); @@ -411,9 +419,50 @@ class MessageList extends Widget { bounceEnabled: false, reachBottomText: '', onReachBottom: noop, + onScroll: () => { + this._updateScrollToBottomButtonVisibility(); + }, }); } + _renderScrollToBottomButton(): void { + const $buttonContainer = $('
') + .addClass(CHAT_MESSAGELIST_SCROLL_TO_BOTTOM_BUTTON_CLASS) + .appendTo(this.$element()); + + this._scrollToBottomButton = this._createComponent( + $buttonContainer, + Button, + { + icon: 'arrowdown', + stylingMode: 'contained', + elementAttr: { + 'aria-label': messageLocalization.format('dxChat-scrollToBottomButtonAriaLabel'), + }, + onClick: () => { + this._scrollDownContent(); + }, + }, + ); + } + + _updateScrollToBottomButtonVisibility(): void { + if (!this._scrollToBottomButton) { + return; + } + + const container = this._scrollableContainer(); + const maxScroll = getScrollTopMax(container); + const threshold = container.clientHeight * SCROLL_TO_BOTTOM_THRESHOLD; + const distanceFromBottom = maxScroll - container.scrollTop; + const isVisible = this._isContentOverflowing() && distanceFromBottom > threshold; + + this._scrollToBottomButton.$element().toggleClass( + CHAT_MESSAGELIST_SCROLL_TO_BOTTOM_BUTTON_VISIBLE_CLASS, + isVisible, + ); + } + _shouldAddDayHeader(timestamp: undefined | string | number | Date): boolean { const { showDayHeaders } = this.option(); diff --git a/packages/devextreme/js/localization/messages/en.json b/packages/devextreme/js/localization/messages/en.json index f7e629931ce9..fb3c4bf8f328 100644 --- a/packages/devextreme/js/localization/messages/en.json +++ b/packages/devextreme/js/localization/messages/en.json @@ -442,6 +442,7 @@ "dxChat-fileViewLabel": "File list", "dxChat-downloadButtonLabel": "Download file {0}", "dxChat-fileLimitReachedWarning": "You selected too many files. Select no more than {0} files and retry.", + "dxChat-scrollToBottomButtonAriaLabel": "Scroll to bottom", "dxColorView-ariaRed": "Red", "dxColorView-ariaGreen": "Green",