From be6d49f57fc635f9c71c2d21af0e7e763086d63a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marker=20dao=20=C2=AE?= Date: Tue, 14 Apr 2026 16:37:12 +0200 Subject: [PATCH 01/13] Chat: Add Suggestions --- .../scss/widgets/base/chat/_index.scss | 1 + .../scss/widgets/base/chat/_mixins.scss | 1 + .../chat/layout/chat-suggestions/_index.scss | 8 ++ .../chat/layout/chat-suggestions/_mixins.scss | 32 ++++++ .../scss/widgets/fluent/chat/_colors.scss | 6 + .../scss/widgets/fluent/chat/_index.scss | 8 ++ .../scss/widgets/fluent/chat/_sizes.scss | 11 ++ .../scss/widgets/generic/chat/_colors.scss | 18 +++ .../scss/widgets/generic/chat/_index.scss | 8 ++ .../scss/widgets/generic/chat/_sizes.scss | 12 ++ .../scss/widgets/material/chat/_colors.scss | 4 + .../scss/widgets/material/chat/_index.scss | 8 ++ .../scss/widgets/material/chat/_sizes.scss | 12 ++ .../devextreme/js/__internal/ui/chat/chat.ts | 20 ++++ .../js/__internal/ui/chat/suggestions.ts | 66 +++++++++++ .../chat.markup.tests.js | 1 + .../tests/DevExpress.ui.widgets/chat.tests.js | 1 + .../chatParts/suggestions.markup.tests.js | 72 ++++++++++++ .../chatParts/suggestions.tests.js | 105 ++++++++++++++++++ 19 files changed, 394 insertions(+) create mode 100644 packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_index.scss create mode 100644 packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_mixins.scss create mode 100644 packages/devextreme/js/__internal/ui/chat/suggestions.ts create mode 100644 packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/suggestions.markup.tests.js create mode 100644 packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/suggestions.tests.js diff --git a/packages/devextreme-scss/scss/widgets/base/chat/_index.scss b/packages/devextreme-scss/scss/widgets/base/chat/_index.scss index 59b5c0daa056..b0296c093a7e 100644 --- a/packages/devextreme-scss/scss/widgets/base/chat/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/chat/_index.scss @@ -9,6 +9,7 @@ @use "layout/chat-confirmationpopup" as *; @use "layout/chat-messagegroup" as *; @use "layout/chat-messagelist" as *; +@use "layout/chat-suggestions" as *; @use "layout/chat-typingindicator" as *; // adduse diff --git a/packages/devextreme-scss/scss/widgets/base/chat/_mixins.scss b/packages/devextreme-scss/scss/widgets/base/chat/_mixins.scss index b5fb3f0c2b12..7ec875e08825 100644 --- a/packages/devextreme-scss/scss/widgets/base/chat/_mixins.scss +++ b/packages/devextreme-scss/scss/widgets/base/chat/_mixins.scss @@ -7,5 +7,6 @@ @forward 'layout/chat-messagebubble/mixins'; @forward 'layout/chat-messagegroup/mixins'; @forward 'layout/chat-messagelist/mixins'; +@forward 'layout/chat-suggestions/mixins'; @forward 'layout/chat-typingindicator/mixins'; @forward 'layout/chat-confirmationpopup/mixins'; diff --git a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_index.scss b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_index.scss new file mode 100644 index 000000000000..f7b25495eb6f --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_index.scss @@ -0,0 +1,8 @@ +@use "../../../mixins" as *; +@use "./mixins" as *; + +.dx-chat-suggestions { + .dx-buttongroup-wrapper { + flex-wrap: wrap; + } +} diff --git a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_mixins.scss b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_mixins.scss new file mode 100644 index 000000000000..e833c01b346d --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_mixins.scss @@ -0,0 +1,32 @@ +@mixin chat-suggestions( + $padding, + $border-radius, + $border-width, + $max-width, + $gap, + $box-shadow, +) { + .dx-chat-suggestions { + padding: $padding; + + .dx-button { + max-width: $max-width; + box-shadow: $box-shadow; + } + + .dx-buttongroup-wrapper { + gap: $gap; + } + + .dx-buttongroup-item { + &.dx-button-mode-outlined, + &.dx-button-mode-contained { + border-inline-start-width: $border-width; + + &.dx-button { + border-radius: $border-radius; + } + } + } + } +} diff --git a/packages/devextreme-scss/scss/widgets/fluent/chat/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/chat/_colors.scss index 1de5ff46cf48..5f884d9f71ba 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/chat/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/chat/_colors.scss @@ -129,6 +129,8 @@ $chat-file-container-background-color: $base-bg !default; $chat-file-container-box-shadow: null !default; $chat-file-secondary-color: null !default; +$chat-suggestions-box-shadow: null !default; + @if $mode == "light" { $chat-bubble-background-color-secondary: color.adjust($base-bg, $lightness: -3.92%, $space: hsl) !default; $chat-information-author-name-color: color.adjust($base-bg, $lightness: -56.08%, $space: hsl) !default; @@ -146,6 +148,8 @@ $chat-file-secondary-color: null !default; $chat-file-container-box-shadow: 0 0 2px 0 color.change($base-shadow-color, $alpha: 0.12), 0 1px 2px 0 color.change($base-shadow-color, $alpha: 0.14) !default; $chat-file-secondary-color: $base-icon-color !default; + $chat-suggestions-box-shadow: 0 4px 12px 0 color.change($base-shadow-color, $alpha: 0.04), 0 4px 24px 0 color.change($base-shadow-color, $alpha: 0.02) !default; + @if $color == "blue" { $chat-bubble-background-color-primary: color.adjust($base-accent, $hue: 3.8deg, $saturation: -11.4%, $lightness: 55.5%, $space: hsl) !default; $chat-messagelist-empty-icon-background-color: color.adjust($base-bg, $lightness: -3.92%, $space: hsl) !default; @@ -175,6 +179,8 @@ $chat-file-secondary-color: null !default; $chat-file-container-box-shadow: 0 0 2px 0 color.change($base-shadow-color, $alpha: 0.24), 0 1px 2px 0 color.change($base-shadow-color, $alpha: 0.28) !default; $chat-file-secondary-color: $base-icon-color !default; + + $chat-suggestions-box-shadow: 0 4px 24px 0 color.change($base-shadow-color, $alpha: 0.02), 0 4px 12px 0 color.change($base-shadow-color, $alpha: 0.04) !default; } /** diff --git a/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss index 9db2d5e3b77b..4ea1eff3b5c0 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss @@ -136,6 +136,14 @@ $chat-confirmation-popup-toolbar-padding-inline, $chat-confirmation-popup-toolbar-gap, ); +@include chat-suggestions( + $chat-suggestions-padding, + $chat-suggestions-button-border-radius, + $chat-suggestions-border-width, + $chat-suggestions-button-max-width, + $chat-suggestions-gap, + $chat-suggestions-box-shadow, +); .dx-chat-file { @include dx-base-typography(); diff --git a/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss index 5842dcc3bd2f..6c67c9c17d67 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss @@ -4,6 +4,7 @@ @use "../sizes" as *; @use "../typography/sizes" as *; @use "../popup/sizes" as *; +@use "../button/colors" as *; // adduse @@ -36,6 +37,12 @@ $chat-alerts-padding-inline: 8px !default; $chat-alerts-row-gap: null !default; $chat-alert-padding-block: null !default; +$chat-suggestions-padding: null !default; +$chat-suggestions-border-width: $fluent-base-border-width !default; +$chat-suggestions-button-border-radius: $button-border-radius !default; +$chat-suggestions-button-max-width: 200px !default; +$chat-suggestions-gap: 12px !default; + $chat-message-editing-preview-caption-font-size: $fluent-xs-font-size !default; $chat-message-editing-preview-content-row-gap: null !default; $chat-message-editing-preview-content-padding-inline: null !default; @@ -95,6 +102,8 @@ $chat-bubble-gap: 8px !default; $chat-alerts-row-gap: 2px !default; $chat-alert-padding-block: 6px !default; + $chat-suggestions-padding: 20px !default; + $chat-message-editing-preview-content-padding-inline: 12px !default; $chat-message-editing-preview-content-row-gap: 4px !default; @@ -133,6 +142,8 @@ $chat-bubble-gap: 8px !default; $chat-alerts-row-gap: 2px !default; $chat-alert-padding-block: 5px !default; + $chat-suggestions-padding: 16px !default; + $chat-message-editing-preview-content-padding-inline: 8px !default; $chat-message-editing-preview-content-row-gap: 2px !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/chat/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/chat/_colors.scss index 6a6f51cb5d23..dfc46957e083 100644 --- a/packages/devextreme-scss/scss/widgets/generic/chat/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/generic/chat/_colors.scss @@ -129,6 +129,8 @@ $chat-file-container-background-color: $base-bg !default; $chat-file-container-box-shadow: null !default; $chat-file-secondary-color: $base-icon-color !default; +$chat-suggestions-box-shadow: null !default; + @if $color == "light" { $chat-avatar-color: $base-text-color !default; $chat-bubble-color-primary: $base-text-color !default; @@ -148,6 +150,8 @@ $chat-file-secondary-color: $base-icon-color !default; $chat-typingindicator-circle-bg-color: color.adjust($base-bg, $lightness: -31.57%, $space: hsl) !default; $chat-typingindicator-circle-bg-color-center: color.adjust($base-bg, $lightness: -46.57%, $space: hsl) !default; + $chat-suggestions-box-shadow: 0 4px 12px 0 color.change($base-shadow-color, $alpha: 0.04), 0 4px 24px 0 color.change($base-shadow-color, $alpha: 0.02) !default; + $chat-file-container-box-shadow: 0 0 2px 0 color.change($base-shadow-color, $alpha: 0.12), 0 1px 2px 0 color.change($base-shadow-color, $alpha: 0.14) !default; } @@ -170,6 +174,8 @@ $chat-file-secondary-color: $base-icon-color !default; $chat-typingindicator-circle-bg-color: color.adjust($base-bg, $lightness: 32.55%, $space: hsl) !default; $chat-typingindicator-circle-bg-color-center: color.adjust($base-bg, $lightness: 41.07%, $space: hsl) !default; + $chat-suggestions-box-shadow: 0 4px 24px 0 color.change($base-shadow-color, $alpha: 0.02), 0 4px 12px 0 color.change($base-shadow-color, $alpha: 0.04) !default; + $chat-file-container-box-shadow: 0 0 2px 0 color.change($base-shadow-color, $alpha: 0.24), 0 1px 2px 0 color.change($base-shadow-color, $alpha: 0.28) !default; } @@ -192,6 +198,8 @@ $chat-file-secondary-color: $base-icon-color !default; $chat-typingindicator-circle-bg-color: color.adjust($base-bg, $lightness: -31.57%, $space: hsl) !default; $chat-typingindicator-circle-bg-color-center: color.adjust($base-bg, $lightness: -46.57%, $space: hsl) !default; + $chat-suggestions-box-shadow: 0 4px 12px 0 color.change($base-shadow-color, $alpha: 0.04), 0 4px 24px 0 color.change($base-shadow-color, $alpha: 0.02) !default; + $chat-file-container-box-shadow: 0 0 2px 0 color.change($base-shadow-color, $alpha: 0.12), 0 1px 2px 0 color.change($base-shadow-color, $alpha: 0.14) !default; } @@ -214,6 +222,8 @@ $chat-file-secondary-color: $base-icon-color !default; $chat-typingindicator-circle-bg-color: $base-inverted-bg !default; $chat-typingindicator-circle-bg-color-center: $base-inverted-bg !default; + $chat-suggestions-box-shadow: 0 4px 24px 0 color.change($base-shadow-color, $alpha: 0.02), 0 4px 12px 0 color.change($base-shadow-color, $alpha: 0.04) !default; + $chat-file-container-box-shadow: 0 0 2px 0 color.change($base-shadow-color, $alpha: 0.24), 0 1px 2px 0 color.change($base-shadow-color, $alpha: 0.28) !default; } @@ -236,6 +246,8 @@ $chat-file-secondary-color: $base-icon-color !default; $chat-typingindicator-circle-bg-color: color.adjust($base-bg, $lightness: 32.55%, $space: hsl) !default; $chat-typingindicator-circle-bg-color-center: color.adjust($base-bg, $lightness: 41.07%, $space: hsl) !default; + $chat-suggestions-box-shadow: 0 4px 24px 0 color.change($base-shadow-color, $alpha: 0.02), 0 4px 12px 0 color.change($base-shadow-color, $alpha: 0.04) !default; + $chat-file-container-box-shadow: 0 0 2px 0 color.change($base-shadow-color, $alpha: 0.24), 0 1px 2px 0 color.change($base-shadow-color, $alpha: 0.28) !default; } @@ -258,6 +270,8 @@ $chat-file-secondary-color: $base-icon-color !default; $chat-typingindicator-circle-bg-color: color.adjust($base-bg, $lightness: 32.55%, $space: hsl) !default; $chat-typingindicator-circle-bg-color-center: color.adjust($base-bg, $lightness: 41.07%, $space: hsl) !default; + $chat-suggestions-box-shadow: 0 4px 24px 0 color.change($base-shadow-color, $alpha: 0.02), 0 4px 12px 0 color.change($base-shadow-color, $alpha: 0.04) !default; + $chat-file-container-box-shadow: 0 0 2px 0 color.change($base-shadow-color, $alpha: 0.24), 0 1px 2px 0 color.change($base-shadow-color, $alpha: 0.28) !default; } @@ -280,6 +294,8 @@ $chat-file-secondary-color: $base-icon-color !default; $chat-typingindicator-circle-bg-color: color.adjust($base-bg, $lightness: -31.57%, $space: hsl) !default; $chat-typingindicator-circle-bg-color-center: color.adjust($base-bg, $lightness: -46.57%, $space: hsl) !default; + $chat-suggestions-box-shadow: 0 4px 12px 0 color.change($base-shadow-color, $alpha: 0.04), 0 4px 24px 0 color.change($base-shadow-color, $alpha: 0.02) !default; + $chat-file-container-box-shadow: 0 0 2px 0 color.change($base-shadow-color, $alpha: 0.12), 0 1px 2px 0 color.change($base-shadow-color, $alpha: 0.14) !default; } @@ -302,6 +318,8 @@ $chat-file-secondary-color: $base-icon-color !default; $chat-typingindicator-circle-bg-color: color.adjust($base-bg, $lightness: -31.57%, $space: hsl) !default; $chat-typingindicator-circle-bg-color-center: color.adjust($base-bg, $lightness: -46.57%, $space: hsl) !default; + $chat-suggestions-box-shadow: 0 4px 12px 0 color.change($base-shadow-color, $alpha: 0.04), 0 4px 24px 0 color.change($base-shadow-color, $alpha: 0.02) !default; + $chat-file-container-box-shadow: 0 0 2px 0 color.change($base-shadow-color, $alpha: 0.12), 0 1px 2px 0 color.change($base-shadow-color, $alpha: 0.14) !default; } diff --git a/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss b/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss index 0efd050545a1..9c6e1adc05e3 100644 --- a/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss @@ -139,6 +139,14 @@ $chat-confirmation-popup-toolbar-padding-inline, $chat-confirmation-popup-toolbar-gap, ); +@include chat-suggestions( + $chat-suggestions-padding, + $chat-suggestions-button-border-radius, + $chat-suggestions-border-width, + $chat-suggestions-button-max-width, + $chat-suggestions-gap, + $chat-suggestions-box-shadow, +); .dx-chat-file { @include dx-base-typography(); diff --git a/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss index 222e675cabcd..34c56ede3bb6 100644 --- a/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss @@ -4,6 +4,7 @@ @use "../colors" as *; @use "../sizes" as *; @use "../popup/sizes" as *; +@use "../button/colors" as *; // adduse @@ -32,11 +33,18 @@ $chat-messagegroup-alignment-start-gap: 12px !default; $chat-information-font-size: 12px !default; $chat-information-icon-size: 12px !default; $chat-information-edited-gap: 2px !default; + $chat-alerts-padding-block: null !default; $chat-alerts-padding-inline: 8px !default; $chat-alerts-row-gap: null !default; $chat-alert-padding-block: null !default; +$chat-suggestions-padding: null !default; +$chat-suggestions-border-width: $generic-base-border-width !default; +$chat-suggestions-button-border-radius: $button-border-radius !default; +$chat-suggestions-button-max-width: 200px !default; +$chat-suggestions-gap: 12px !default; + $chat-message-editing-preview-caption-font-size: 12px !default; $chat-message-editing-preview-content-row-gap: null !default; $chat-message-editing-preview-content-padding-inline: null !default; @@ -97,6 +105,8 @@ $chat-bubble-gap: 8px !default; $chat-alerts-row-gap: 4px !default; $chat-alert-padding-block: 9px !default; + $chat-suggestions-padding: 20px !default; + $chat-message-editing-preview-content-padding-inline: 12px !default; $chat-message-editing-preview-content-row-gap: 4px !default; @@ -136,6 +146,8 @@ $chat-bubble-gap: 8px !default; $chat-alerts-row-gap: 2px !default; $chat-alert-padding-block: 6px !default; + $chat-suggestions-padding: 16px !default; + $chat-message-editing-preview-content-padding-inline: 8px !default; $chat-message-editing-preview-content-row-gap: 2px !default; diff --git a/packages/devextreme-scss/scss/widgets/material/chat/_colors.scss b/packages/devextreme-scss/scss/widgets/material/chat/_colors.scss index 019f9cf1d893..d325d8f9ffbe 100644 --- a/packages/devextreme-scss/scss/widgets/material/chat/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/material/chat/_colors.scss @@ -134,14 +134,18 @@ $chat-file-container-background-color: $base-bg !default; $chat-file-container-box-shadow: null !default; $chat-file-secondary-color: $base-icon-color !default; +$chat-suggestions-box-shadow: null !default; + @if $mode == "light" { $chat-bubble-background-color-primary: rgba($base-accent, 0.08) !default; $chat-file-container-box-shadow: 0 1px 1px 0 color.change($base-shadow-color, $alpha: 0.14), 0 1px 1px 0 color.change($base-shadow-color, $alpha: 0.12), 0 1px 3px 0 color.change($base-shadow-color, $alpha: 0.2) !default; + $chat-suggestions-box-shadow: 0 4px 12px 0 color.change($base-shadow-color, $alpha: 0.04), 0 4px 24px 0 color.change($base-shadow-color, $alpha: 0.02) !default; } @else if $mode == "dark" { $chat-bubble-background-color-primary: rgba(color.adjust($base-accent, $lightness: 19.22%, $space: hsl), 0.08) !default; $chat-file-container-box-shadow: 0 1px 1px 0 color.change($base-shadow-color, $alpha: 0.28), 0 1px 1px 0 color.change($base-shadow-color, $alpha: 0.24), 0 1px 3px 0 color.change($base-shadow-color, $alpha: 0.4) !default; + $chat-suggestions-box-shadow: 0 4px 24px 0 color.change($base-shadow-color, $alpha: 0.02), 0 4px 12px 0 color.change($base-shadow-color, $alpha: 0.04) !default; } $chat-messagelist-contextmenu-delete-button-color: $base-danger !default; diff --git a/packages/devextreme-scss/scss/widgets/material/chat/_index.scss b/packages/devextreme-scss/scss/widgets/material/chat/_index.scss index bf9701d4f518..4edb1b5eeaf8 100644 --- a/packages/devextreme-scss/scss/widgets/material/chat/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/chat/_index.scss @@ -137,6 +137,14 @@ $chat-confirmation-popup-toolbar-padding-inline, $chat-confirmation-popup-toolbar-gap, ); +@include chat-suggestions( + $chat-suggestions-padding, + $chat-suggestions-button-border-radius, + $chat-suggestions-border-width, + $chat-suggestions-button-max-width, + $chat-suggestions-gap, + $chat-suggestions-box-shadow, +); .dx-chat-file { @include dx-base-typography(); diff --git a/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss index 323b4c671a4b..d4e69b33c4fb 100644 --- a/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss @@ -2,6 +2,7 @@ @use "../sizes" as *; @use "../common/sizes" as *; @use "../typography/sizes" as *; +@use "../button/colors" as *; // adduse @@ -29,11 +30,18 @@ $chat-messagegroup-alignment-start-gap: 12px !default; $chat-information-font-size: 12px !default; $chat-information-icon-size: 12px !default; $chat-information-edited-gap: 2px !default; + $chat-alerts-padding-block: null !default; $chat-alerts-padding-inline: 8px !default; $chat-alerts-row-gap: null !default; $chat-alert-padding-block: null !default; +$chat-suggestions-padding: null !default; +$chat-suggestions-border-width: $material-base-border-width !default; +$chat-suggestions-button-border-radius: $button-border-radius !default; +$chat-suggestions-button-max-width: 200px !default; +$chat-suggestions-gap: 12px !default; + $chat-message-editing-preview-caption-font-size: $material-xs-font-size !default; $chat-message-editing-preview-content-row-gap: null !default; $chat-message-editing-preview-content-padding-inline: null !default; @@ -93,6 +101,8 @@ $chat-bubble-gap: 8px !default; $chat-alerts-row-gap: 4px !default; $chat-alert-padding-block: 6px !default; + $chat-suggestions-padding: 20px !default; + $chat-message-editing-preview-content-padding-inline: 12px !default; $chat-message-editing-preview-content-row-gap: 4px !default; @@ -132,6 +142,8 @@ $chat-bubble-gap: 8px !default; $chat-alerts-row-gap: 2px !default; $chat-alert-padding-block: 5px !default; + $chat-suggestions-padding: 16px !default; + $chat-message-editing-preview-content-padding-inline: 8px !default; $chat-message-editing-preview-content-row-gap: 2px !default; diff --git a/packages/devextreme/js/__internal/ui/chat/chat.ts b/packages/devextreme/js/__internal/ui/chat/chat.ts index 1addafecce01..23d28c54ccf2 100644 --- a/packages/devextreme/js/__internal/ui/chat/chat.ts +++ b/packages/devextreme/js/__internal/ui/chat/chat.ts @@ -41,6 +41,8 @@ import type { Properties as MessageListProperties, } from '@ts/ui/chat/messagelist'; import MessageList from '@ts/ui/chat/messagelist'; +import type { SuggestionsOptions } from '@ts/ui/chat/suggestions'; +import Suggestions from '@ts/ui/chat/suggestions'; import type { DataChange } from '@ts/ui/collection/collection_widget.base'; const CHAT_CLASS = 'dx-chat'; @@ -53,6 +55,8 @@ class Chat extends Widget { _alertList!: AlertList; + _suggestions?: Suggestions; + _messageToEdit?: Message; _deleteConfirmationPopup!: ConfirmationPopup; @@ -116,6 +120,7 @@ class Chat extends Widget { action: 'send', onClick: undefined, }, + suggestions: undefined, onMessageDeleted: undefined, onMessageDeleting: undefined, onMessageEditCanceled: undefined, @@ -185,6 +190,7 @@ class Chat extends Widget { this._renderMessageList(); this._renderAlertList(); + this._renderSuggestions(); this._renderMessageBox(); this._updateRootAria(); @@ -472,6 +478,12 @@ class Chat extends Widget { }); } + _renderSuggestions(): void { + const { suggestions } = this.option(); + + this._suggestions = new Suggestions(this.$element(), suggestions); + } + _renderMessageBox(): void { const { activeStateEnabled, @@ -775,6 +787,9 @@ class Chat extends Widget { this._createSendButtonAction(); this._messageBox.option(name, this._getSendButtonOptionsWithAction()); break; + case 'suggestions': + this._suggestions?.updateOptions(value as SuggestionsOptions); + break; default: super._optionChanged(args); } @@ -791,6 +806,11 @@ class Chat extends Widget { this._insertNewItem(message); } + _clean(): void { + this._suggestions?.dispose(); + this._suggestions = undefined; + } + _dispose(): void { this._deleteConfirmationPopup?.dispose(); super._dispose(); diff --git a/packages/devextreme/js/__internal/ui/chat/suggestions.ts b/packages/devextreme/js/__internal/ui/chat/suggestions.ts new file mode 100644 index 000000000000..aeb618e948fc --- /dev/null +++ b/packages/devextreme/js/__internal/ui/chat/suggestions.ts @@ -0,0 +1,66 @@ +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import ButtonGroup, { type Properties as ButtonGroupProperties } from '@js/ui/button_group'; + +const CHAT_SUGGESTIONS_CLASS = 'dx-chat-suggestions'; + +export type SuggestionsOptions = Omit; + +class Suggestions { + private readonly _$container?: dxElementWrapper; + + private _$element?: dxElementWrapper; + + private _buttonGroup?: InstanceType; + + constructor($container: dxElementWrapper, options: SuggestionsOptions | undefined) { + this._$container = $container; + + this._renderMarkup(); + this._initButtonGroup(options); + } + + private _getConfiguration(options: SuggestionsOptions): ButtonGroupProperties { + const items = options.items?.map((item) => ({ + type: 'default', + ...item, + })) ?? []; + + return { + stylingMode: 'outlined', + ...options, + items, + selectionMode: 'none', + }; + } + + private _renderMarkup(): void { + this._$element = $('
').addClass(CHAT_SUGGESTIONS_CLASS); + this._$container?.append(this._$element); + } + + private _initButtonGroup(options: SuggestionsOptions = {}): void { + const shouldRender = Object.keys(options).length; + + if (shouldRender && this._$element) { + this._buttonGroup = new ButtonGroup(this._$element.get(0), this._getConfiguration(options)); + } + } + + updateOptions(options: SuggestionsOptions): void { + if (!this._buttonGroup) { + this._initButtonGroup(options); + return; + } + + this._buttonGroup?.option(this._getConfiguration(options)); + } + + dispose(): void { + this._buttonGroup?.dispose(); + this._buttonGroup = undefined; + this._$element = undefined; + } +} + +export default Suggestions; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chat.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chat.markup.tests.js index 276b76a2a736..f1374c9cfdf8 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chat.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chat.markup.tests.js @@ -12,6 +12,7 @@ import './chatParts/file.markup.tests.js'; import './chatParts/messageBox.markup.tests.js'; import './chatParts/messageBubble.markup.tests.js'; import './chatParts/confirmationPopup.markup.tests.js'; +import './chatParts/suggestions.markup.tests.js'; import './chatParts/messageGroup.markup.tests.js'; import './chatParts/messageList.markup.tests.js'; import './chatParts/alertList.markup.tests.js'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chat.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chat.tests.js index 63a5b956df22..3f1520ae2c8c 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chat.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chat.tests.js @@ -12,6 +12,7 @@ import './chatParts/file.tests.js'; import './chatParts/messageBox.tests.js'; import './chatParts/messageBubble.tests.js'; import './chatParts/confirmationPopup.tests.js'; +import './chatParts/suggestions.tests.js'; import './chatParts/messageGroup.tests.js'; import './chatParts/messageList.tests.js'; import './chatParts/alertList.tests.js'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/suggestions.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/suggestions.markup.tests.js new file mode 100644 index 000000000000..9057f3066993 --- /dev/null +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/suggestions.markup.tests.js @@ -0,0 +1,72 @@ +import $ from 'jquery'; + +import Suggestions from '__internal/ui/chat/suggestions'; + +const CHAT_SUGGESTIONS_CLASS = 'dx-chat-suggestions'; +const BUTTON_GROUP_CLASS = 'dx-buttongroup'; +const BUTTON_GROUP_ITEM_CLASS = 'dx-buttongroup-item'; +const BUTTON_GROUP_OUTLINED_CLASS = 'dx-buttongroup-mode-outlined'; + +const moduleConfig = { + beforeEach: function() { + const init = (options = {}) => { + this.$element = $('#component'); + this.instance = new Suggestions(this.$element, options); + this.$suggestions = this.$element.find(`.${CHAT_SUGGESTIONS_CLASS}`); + }; + + this.reinit = (options) => { + this.instance.dispose(); + this.$element.empty(); + init(options); + }; + + init({ items: [{ text: 'Item 1' }, { text: 'Item 2' }] }); + }, +}; + +QUnit.module('Suggestions', moduleConfig, () => { + QUnit.module('Render', () => { + QUnit.test('should render suggestions container with correct class', function(assert) { + assert.strictEqual(this.$suggestions.length, 1, 'suggestions container is rendered'); + assert.ok(this.$suggestions.hasClass(CHAT_SUGGESTIONS_CLASS), 'has correct class'); + }); + + QUnit.test('should have ButtonGroup class', function(assert) { + assert.strictEqual(this.$suggestions.hasClass(BUTTON_GROUP_CLASS), true, 'ButtonGroup is rendered'); + }); + + QUnit.test('should render correct number of items', function(assert) { + this.reinit({ items: [{ text: 'Item 1' }, { text: 'Item 2' }, { text: 'Item 3' }] }); + + const $items = this.$element.find(`.${BUTTON_GROUP_ITEM_CLASS}`); + + assert.strictEqual($items.length, 3, 'correct number of items rendered'); + }); + + QUnit.test('should render items with correct text', function(assert) { + this.reinit({ items: [{ text: 'First' }, { text: 'Second' }] }); + + const $items = this.$element.find(`.${BUTTON_GROUP_ITEM_CLASS}`); + + assert.strictEqual($items.eq(0).text(), 'First', 'first item text is correct'); + assert.strictEqual($items.eq(1).text(), 'Second', 'second item text is correct'); + }); + + QUnit.test('should not render ButtonGroup if no options provided', function(assert) { + this.reinit(); + + assert.strictEqual(this.$suggestions.hasClass(BUTTON_GROUP_CLASS), false, 'ButtonGroup is not rendered'); + }); + + QUnit.test('should use outlined stylingMode by default', function(assert) { + assert.ok(this.$suggestions.hasClass(BUTTON_GROUP_OUTLINED_CLASS), 'outlined class applied by default'); + }); + + QUnit.test('should allow overriding stylingMode', function(assert) { + this.reinit({ items: [{ text: 'Item 1' }], stylingMode: 'text' }); + + assert.notOk(this.$suggestions.hasClass(BUTTON_GROUP_OUTLINED_CLASS), 'outlined class is not applied'); + }); + }); +}); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/suggestions.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/suggestions.tests.js new file mode 100644 index 000000000000..b480a5c7dc4e --- /dev/null +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/suggestions.tests.js @@ -0,0 +1,105 @@ +import $ from 'jquery'; + +import Suggestions from '__internal/ui/chat/suggestions'; + +const CHAT_SUGGESTIONS_CLASS = 'dx-chat-suggestions'; +const BUTTON_GROUP_CLASS = 'dx-buttongroup'; +const BUTTON_GROUP_ITEM_CLASS = 'dx-buttongroup-item'; + +const moduleConfig = { + beforeEach: function() { + const init = (options = {}) => { + this.$element = $('#component'); + this.instance = new Suggestions(this.$element, options); + }; + + this.getSuggestions = () => this.$element.find(`.${CHAT_SUGGESTIONS_CLASS}`); + this.getItems = () => this.$element.find(`.${BUTTON_GROUP_ITEM_CLASS}`); + + this.reinit = (options) => { + this.instance.dispose(); + this.$element.empty(); + init(options); + }; + + init({ items: [{ text: 'Item 1' }, { text: 'Item 2' }] }); + }, +}; + +QUnit.module('Suggestions', moduleConfig, () => { + QUnit.module('Render', () => { + QUnit.test('should be initialized correctly', function(assert) { + assert.ok(this.instance instanceof Suggestions); + }); + }); + + QUnit.module('updateOptions', () => { + QUnit.test('should update items list', function(assert) { + this.instance.updateOptions({ items: [{ text: 'New Item 1' }, { text: 'New Item 2' }, { text: 'New Item 3' }] }); + + const $items = this.getItems(); + + assert.strictEqual($items.length, 3, 'items count updated'); + assert.strictEqual($items.eq(0).text(), 'New Item 1', 'first item text updated'); + }); + + QUnit.test('should initialize ButtonGroup on updateOptions if not initialized', function(assert) { + this.reinit(); + + assert.strictEqual(this.getSuggestions().hasClass(BUTTON_GROUP_CLASS), false, 'ButtonGroup not rendered initially'); + + this.instance.updateOptions({ items: [{ text: 'Item 1' }] }); + + assert.strictEqual(this.getSuggestions().hasClass(BUTTON_GROUP_CLASS), true, 'ButtonGroup rendered after updateOptions'); + assert.strictEqual(this.getItems().length, 1, 'item rendered correctly'); + }); + + QUnit.test('should update items to empty list', function(assert) { + this.instance.updateOptions({ items: [] }); + + const $items = this.getItems(); + + assert.strictEqual($items.length, 0, 'all items removed'); + }); + }); + + QUnit.module('dispose', () => { + QUnit.test('should not throw on dispose', function(assert) { + try { + this.instance.dispose(); + assert.ok(true, 'dispose executed without errors'); + } catch(e) { + assert.ok(false, `dispose threw an error: ${e.message}`); + } + }); + + QUnit.test('should dispose without errors even if no items were provided', function(assert) { + this.reinit({ items: [] }); + + try { + this.instance.dispose(); + assert.ok(true, 'dispose executed without errors'); + } catch(e) { + assert.ok(false, `dispose threw an error: ${e.message}`); + } + }); + }); + + QUnit.module('item click', () => { + QUnit.test('should execute onItemClick callback when item is clicked', function(assert) { + assert.expect(1); + + const clickedText = 'Item 1'; + + this.reinit({ + items: [{ text: clickedText }, { text: 'Item 2' }], + onItemClick: (e) => { + assert.strictEqual(e.itemData.text, clickedText, 'correct item passed to callback'); + }, + }); + + const $firstItem = this.getItems().first(); + $firstItem.trigger('dxclick'); + }); + }); +}); From 32daa2d49ab00a8ade74c800587b37b2ce05e67e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marker=20dao=20=C2=AE?= Date: Tue, 14 Apr 2026 16:56:34 +0200 Subject: [PATCH 02/13] feat(sb): Add a story --- .../stories/chat/Chat.stories.tsx | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/apps/react-storybook/stories/chat/Chat.stories.tsx b/apps/react-storybook/stories/chat/Chat.stories.tsx index 3e7ed02d718a..d352ab9d1804 100644 --- a/apps/react-storybook/stories/chat/Chat.stories.tsx +++ b/apps/react-storybook/stories/chat/Chat.stories.tsx @@ -23,6 +23,7 @@ import HTMLReactParser from 'html-react-parser'; import './styles.css'; import { Guid } from 'devextreme-react/cjs/common'; import { Message } from 'devextreme/artifacts/npm/devextreme/ui/chat'; +import type { ItemClickEvent as ButtonGroupItemClickEvent, Item as ButtonGroupItem } from 'devextreme/ui/button_group'; const meta: Meta = { title: 'Components/Chat', @@ -1039,3 +1040,61 @@ export const SendButtonOptions: Story = { ); }, }; + +const suggestionItems: ButtonGroupItem[] = [ + { text: '📦 Track my orders' }, + { text: '⭐ Check in-stock favorites' }, + { text: '🔄 Start a return' }, + { text: '🔍 Find my order' }, + { text: '💳 Payment & billing help with Soul' }, +]; + +export const Suggestions: Story = { + args: { + sendImmediately: false, + }, + argTypes: { + sendImmediately: { + name: 'Send immediately on suggestion click', + control: 'boolean', + }, + }, + render: ({ sendImmediately }) => { + const [messages, setMessages] = useState([]); + const [inputFieldText, setInputFieldText] = useState(''); + + const onMessageEntered = useCallback(({ message }: ChatTypes.MessageEnteredEvent) => { + setMessages((prev) => [...prev, message]); + setInputFieldText(''); + }, []); + + const suggestions = useMemo(() => ({ + items: suggestionItems, + onItemClick: (e: ButtonGroupItemClickEvent) => { + if (sendImmediately) { + setMessages((prev) => [...prev, { + timestamp: new Date(), + author: firstAuthor, + text: e.itemData?.text, + }]); + } else { + setInputFieldText(e.itemData?.text ?? ''); + } + }, + }), [sendImmediately]); + + return ( +
+ +
+ ); + }, +}; From 9e3c08106c302cce4c4690f5bd31782ea4bfe1a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marker=20dao=20=C2=AE?= Date: Tue, 14 Apr 2026 17:17:28 +0200 Subject: [PATCH 03/13] refactor && fix --- .../widgets/base/chat/layout/chat-suggestions/_index.scss | 3 --- packages/devextreme/js/__internal/ui/chat/chat.ts | 1 + packages/devextreme/js/__internal/ui/chat/suggestions.ts | 6 +++--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_index.scss b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_index.scss index f7b25495eb6f..78f54b0c6b2c 100644 --- a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_index.scss @@ -1,6 +1,3 @@ -@use "../../../mixins" as *; -@use "./mixins" as *; - .dx-chat-suggestions { .dx-buttongroup-wrapper { flex-wrap: wrap; diff --git a/packages/devextreme/js/__internal/ui/chat/chat.ts b/packages/devextreme/js/__internal/ui/chat/chat.ts index 23d28c54ccf2..a00dbdd7308b 100644 --- a/packages/devextreme/js/__internal/ui/chat/chat.ts +++ b/packages/devextreme/js/__internal/ui/chat/chat.ts @@ -809,6 +809,7 @@ class Chat extends Widget { _clean(): void { this._suggestions?.dispose(); this._suggestions = undefined; + super._clean(); } _dispose(): void { diff --git a/packages/devextreme/js/__internal/ui/chat/suggestions.ts b/packages/devextreme/js/__internal/ui/chat/suggestions.ts index aeb618e948fc..a3293ba64072 100644 --- a/packages/devextreme/js/__internal/ui/chat/suggestions.ts +++ b/packages/devextreme/js/__internal/ui/chat/suggestions.ts @@ -20,8 +20,8 @@ class Suggestions { this._initButtonGroup(options); } - private _getConfiguration(options: SuggestionsOptions): ButtonGroupProperties { - const items = options.items?.map((item) => ({ + private _getConfiguration(options: SuggestionsOptions | undefined): ButtonGroupProperties { + const items = options?.items?.map((item) => ({ type: 'default', ...item, })) ?? []; @@ -47,7 +47,7 @@ class Suggestions { } } - updateOptions(options: SuggestionsOptions): void { + updateOptions(options: SuggestionsOptions | undefined): void { if (!this._buttonGroup) { this._initButtonGroup(options); return; From b64b7070f23aabc70385b7472e239b83501b2418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marker=20dao=20=C2=AE?= Date: Tue, 14 Apr 2026 17:30:01 +0200 Subject: [PATCH 04/13] fix(tb) --- packages/devextreme-themebuilder/tests/data/dependencies.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme-themebuilder/tests/data/dependencies.ts b/packages/devextreme-themebuilder/tests/data/dependencies.ts index 634355fcd57b..8e7ffd2d604a 100644 --- a/packages/devextreme-themebuilder/tests/data/dependencies.ts +++ b/packages/devextreme-themebuilder/tests/data/dependencies.ts @@ -17,7 +17,7 @@ export const dependencies: FlatStylesDependencies = { dropdownbutton: ['validation', 'button', 'buttongroup', 'popup', 'loadindicator', 'loadpanel', 'scrollview', 'list'], calendar: ['validation', 'button'], cardview: ['box', 'button', 'calendar', 'checkbox', 'contextmenu', 'datebox', 'filterbuilder', 'form', 'list', 'loadindicator', 'loadpanel', 'multiview', 'numberbox', 'popup', 'responsivebox', 'scrollview', 'selectbox', 'sortable', 'tabpanel', 'tabs', 'textbox', 'toast', 'toolbar', 'treeview', 'validation'], - chat: ['button', 'loadindicator', 'loadpanel', 'popup', 'progressbar', 'scrollview', 'speechtotext', 'textbox', 'toolbar', 'validation'], + chat: ['button', 'buttongroup', 'loadindicator', 'loadpanel', 'popup', 'progressbar', 'scrollview', 'speechtotext', 'textbox', 'toolbar', 'validation'], checkbox: ['validation'], numberbox: ['validation', 'button', 'loadindicator'], colorbox: ['validation', 'button', 'loadindicator', 'numberbox', 'textbox', 'popup'], From 4f3a6c6b1eddae872db68e02256e0b48790bb532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marker=20dao=20=C2=AE?= Date: Tue, 14 Apr 2026 18:01:04 +0200 Subject: [PATCH 05/13] refactor && feat() --- .../chat/layout/chat-suggestions/_mixins.scss | 4 +- .../devextreme/js/__internal/ui/chat/chat.ts | 3 +- .../js/__internal/ui/chat/suggestions.ts | 4 +- .../chatParts/chat.tests.js | 56 +++++++++++++++++++ 4 files changed, 62 insertions(+), 5 deletions(-) diff --git a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_mixins.scss b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_mixins.scss index e833c01b346d..369cd8201543 100644 --- a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_mixins.scss +++ b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_mixins.scss @@ -7,7 +7,9 @@ $box-shadow, ) { .dx-chat-suggestions { - padding: $padding; + &:empty { + padding: $padding; + } .dx-button { max-width: $max-width; diff --git a/packages/devextreme/js/__internal/ui/chat/chat.ts b/packages/devextreme/js/__internal/ui/chat/chat.ts index a00dbdd7308b..9452ce882017 100644 --- a/packages/devextreme/js/__internal/ui/chat/chat.ts +++ b/packages/devextreme/js/__internal/ui/chat/chat.ts @@ -41,7 +41,6 @@ import type { Properties as MessageListProperties, } from '@ts/ui/chat/messagelist'; import MessageList from '@ts/ui/chat/messagelist'; -import type { SuggestionsOptions } from '@ts/ui/chat/suggestions'; import Suggestions from '@ts/ui/chat/suggestions'; import type { DataChange } from '@ts/ui/collection/collection_widget.base'; @@ -788,7 +787,7 @@ class Chat extends Widget { this._messageBox.option(name, this._getSendButtonOptionsWithAction()); break; case 'suggestions': - this._suggestions?.updateOptions(value as SuggestionsOptions); + this._suggestions?.updateOptions(value as ChatProperties['suggestions']); break; default: super._optionChanged(args); diff --git a/packages/devextreme/js/__internal/ui/chat/suggestions.ts b/packages/devextreme/js/__internal/ui/chat/suggestions.ts index a3293ba64072..a7867d738fa2 100644 --- a/packages/devextreme/js/__internal/ui/chat/suggestions.ts +++ b/packages/devextreme/js/__internal/ui/chat/suggestions.ts @@ -4,7 +4,7 @@ import ButtonGroup, { type Properties as ButtonGroupProperties } from '@js/ui/bu const CHAT_SUGGESTIONS_CLASS = 'dx-chat-suggestions'; -export type SuggestionsOptions = Omit; +type SuggestionsOptions = Omit; class Suggestions { private readonly _$container?: dxElementWrapper; @@ -20,7 +20,7 @@ class Suggestions { this._initButtonGroup(options); } - private _getConfiguration(options: SuggestionsOptions | undefined): ButtonGroupProperties { + private _getConfiguration(options: SuggestionsOptions = {}): ButtonGroupProperties { const items = options?.items?.map((item) => ({ type: 'default', ...item, 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 1b89749b5360..46f123804824 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 @@ -60,6 +60,10 @@ const CHAT_MESSAGEGROUP_CONTENT_CLASS = 'dx-chat-messagegroup-content'; const CHAT_MESSAGE_EDITED_CLASS = 'dx-chat-message-edited'; const CHAT_MESSAGE_EDITED_HIDING_CLASS = 'dx-chat-message-edited-hiding'; +const CHAT_SUGGESTIONS_CLASS = 'dx-chat-suggestions'; +const BUTTON_GROUP_CLASS = 'dx-buttongroup'; +const BUTTON_GROUP_ITEM_CLASS = 'dx-buttongroup-item'; + const CHAT_LAST_MESSAGEGROUP_ALIGNMENT_START_CLASS = 'dx-chat-last-messagegroup-alignment-start'; const CHAT_LAST_MESSAGEGROUP_ALIGNMENT_END_CLASS = 'dx-chat-last-messagegroup-alignment-end'; @@ -2758,6 +2762,58 @@ QUnit.module('Chat', () => { }); }); + QUnit.module('Suggestions integration', { + beforeEach: function() { + moduleConfig.beforeEach.apply(this, arguments); + + this.getSuggestionsElement = () => this.$element.find(`.${CHAT_SUGGESTIONS_CLASS}`); + this.getItems = () => this.$element.find(`.${BUTTON_GROUP_ITEM_CLASS}`); + } + }, () => { + QUnit.test('should always render suggestions container element', function(assert) { + assert.strictEqual(this.getSuggestionsElement().length, 1, 'suggestions container is always rendered'); + }); + + QUnit.test('suggestions should be rendered before messageBox', function(assert) { + this.reinit({ + suggestions: { items: [{ text: 'Item 1' }] }, + }); + + const $suggestions = this.getSuggestionsElement(); + const $messageBox = this.$element.find(`.${CHAT_MESSAGEBOX_CLASS}`); + + assert.strictEqual($suggestions.next().is($messageBox), true, 'suggestions is rendered before messageBox'); + }); + + QUnit.test('suggestions should be updated at runtime', function(assert) { + this.reinit({ + suggestions: { items: [{ text: 'Item 1' }, { text: 'Item 2' }] }, + }); + + this.instance.option('suggestions', { items: [{ text: 'New 1' }, { text: 'New 2' }, { text: 'New 3' }] }); + + assert.strictEqual(this.getItems().length, 3, 'items count updated'); + assert.strictEqual(this.getItems().eq(0).text(), 'New 1', 'first item text updated'); + }); + + QUnit.test('onItemClick callback should be called when suggestion item is clicked', function(assert) { + assert.expect(1); + + const clickedText = 'Item 1'; + + this.reinit({ + suggestions: { + items: [{ text: clickedText }, { text: 'Item 2' }], + onItemClick: (e) => { + assert.strictEqual(e.itemData.text, clickedText, 'correct item passed to callback'); + }, + }, + }); + + this.getItems().first().trigger('dxclick'); + }); + }); + QUnit.module('Data Layer Integration', { beforeEach: function() { this.clock = sinon.useFakeTimers(); From 767a1a77fe374595a2c316ef443395a86fe4f654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marker=20dao=20=C2=AE?= Date: Tue, 14 Apr 2026 18:01:54 +0200 Subject: [PATCH 06/13] refactor() --- .../testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js | 1 - 1 file changed, 1 deletion(-) 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 46f123804824..bfa404e3c149 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 @@ -61,7 +61,6 @@ const CHAT_MESSAGE_EDITED_CLASS = 'dx-chat-message-edited'; const CHAT_MESSAGE_EDITED_HIDING_CLASS = 'dx-chat-message-edited-hiding'; const CHAT_SUGGESTIONS_CLASS = 'dx-chat-suggestions'; -const BUTTON_GROUP_CLASS = 'dx-buttongroup'; const BUTTON_GROUP_ITEM_CLASS = 'dx-buttongroup-item'; const CHAT_LAST_MESSAGEGROUP_ALIGNMENT_START_CLASS = 'dx-chat-last-messagegroup-alignment-start'; From 136d542fb2ba25807360b45de1b3cc6142d0602c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marker=20dao=20=C2=AE?= Date: Tue, 14 Apr 2026 20:48:34 +0200 Subject: [PATCH 07/13] refactor() --- .../chat/layout/chat-suggestions/_mixins.scss | 4 +- .../appointment_popup.integration.test.ts | 1 + .../devextreme/js/__internal/ui/chat/chat.ts | 7 +-- .../js/__internal/ui/chat/suggestions.ts | 25 ++++++++--- .../chatParts/chat.tests.js | 45 ++++++++++++------- .../chatParts/suggestions.markup.tests.js | 4 +- .../chatParts/suggestions.tests.js | 37 ++++++++++++--- 7 files changed, 85 insertions(+), 38 deletions(-) diff --git a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_mixins.scss b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_mixins.scss index 369cd8201543..02951f094307 100644 --- a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_mixins.scss +++ b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_mixins.scss @@ -7,8 +7,10 @@ $box-shadow, ) { .dx-chat-suggestions { + padding: $padding; + &:empty { - padding: $padding; + padding: 0; } .dx-button { diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.integration.test.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.integration.test.ts index bdf009c2ea61..0f8f7713e743 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.integration.test.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.integration.test.ts @@ -1630,6 +1630,7 @@ describe('Appointment Form', () => { scheduler.showAppointmentPopup(commonAppointment); + // @ts-expect-error POM.popup.dxForm.option('items') const recurrenceGroup = POM.popup.dxForm.option('items')[1] as GroupItem; const allItems = flattenBy( recurrenceGroup.items as SimpleItem[], diff --git a/packages/devextreme/js/__internal/ui/chat/chat.ts b/packages/devextreme/js/__internal/ui/chat/chat.ts index 9452ce882017..4666147a3f96 100644 --- a/packages/devextreme/js/__internal/ui/chat/chat.ts +++ b/packages/devextreme/js/__internal/ui/chat/chat.ts @@ -805,13 +805,8 @@ class Chat extends Widget { this._insertNewItem(message); } - _clean(): void { - this._suggestions?.dispose(); - this._suggestions = undefined; - super._clean(); - } - _dispose(): void { + this._suggestions?.dispose(); this._deleteConfirmationPopup?.dispose(); super._dispose(); } diff --git a/packages/devextreme/js/__internal/ui/chat/suggestions.ts b/packages/devextreme/js/__internal/ui/chat/suggestions.ts index a7867d738fa2..1a966f08d748 100644 --- a/packages/devextreme/js/__internal/ui/chat/suggestions.ts +++ b/packages/devextreme/js/__internal/ui/chat/suggestions.ts @@ -35,30 +35,43 @@ class Suggestions { } private _renderMarkup(): void { - this._$element = $('
').addClass(CHAT_SUGGESTIONS_CLASS); + this._$element = $('
'); this._$container?.append(this._$element); } private _initButtonGroup(options: SuggestionsOptions = {}): void { - const shouldRender = Object.keys(options).length; - - if (shouldRender && this._$element) { + if (this._hasOptions(options) && this._$element) { + this._$element.addClass(CHAT_SUGGESTIONS_CLASS); this._buttonGroup = new ButtonGroup(this._$element.get(0), this._getConfiguration(options)); } } + private _hasOptions(options: SuggestionsOptions | undefined): boolean { + return Boolean(Object.keys(options ?? {}).length); + } + updateOptions(options: SuggestionsOptions | undefined): void { + if (!this._hasOptions(options)) { + this.clean(); + return; + } + if (!this._buttonGroup) { this._initButtonGroup(options); return; } - this._buttonGroup?.option(this._getConfiguration(options)); + this._buttonGroup.option(this._getConfiguration(options)); } - dispose(): void { + clean(): void { this._buttonGroup?.dispose(); this._buttonGroup = undefined; + this._$element?.empty(); + } + + dispose(): void { + this._$element?.remove(); this._$element = undefined; } } 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 bfa404e3c149..f976a7fd1b24 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 @@ -60,7 +60,6 @@ const CHAT_MESSAGEGROUP_CONTENT_CLASS = 'dx-chat-messagegroup-content'; const CHAT_MESSAGE_EDITED_CLASS = 'dx-chat-message-edited'; const CHAT_MESSAGE_EDITED_HIDING_CLASS = 'dx-chat-message-edited-hiding'; -const CHAT_SUGGESTIONS_CLASS = 'dx-chat-suggestions'; const BUTTON_GROUP_ITEM_CLASS = 'dx-buttongroup-item'; const CHAT_LAST_MESSAGEGROUP_ALIGNMENT_START_CLASS = 'dx-chat-last-messagegroup-alignment-start'; @@ -119,10 +118,7 @@ const moduleConfig = { init(options); }; - this.getEmptyView = () => { - return this.$element.find(`.${CHAT_MESSAGELIST_EMPTY_VIEW_CLASS}`); - }; - + this.getEmptyView = () => this.$element.find(`.${CHAT_MESSAGELIST_EMPTY_VIEW_CLASS}`); this.getMessageList = () => MessageList.getInstance(this.$element.find(`.${CHAT_MESSAGELIST_CLASS}`)); this.getMessageGroups = () => this.$element.find(`.${CHAT_MESSAGEGROUP_CLASS}`); this.getDayHeaders = () => this.$element.find(`.${CHAT_MESSAGELIST_DAY_HEADER_CLASS}`); @@ -135,6 +131,9 @@ const moduleConfig = { this.getMessageListEmptyView = () => this.$element.find(`.${CHAT_MESSAGELIST_EMPTY_VIEW_CLASS}`); this.getFileUploader = () => FileUploader.getInstance(this.$element.find(`.${FILEUPLOADER_CLASS}`)); this.getAttachButton = () => Button.getInstance(this.$element.find(`.${CHAT_TEXT_AREA_ATTACH_BUTTON}`)); + // this.getSuggestionsElement = () => $(); + this.getSuggestionsElement = () => this.instance._suggestions._$element; + this.getSuggestionItems = () => this.$element.find(`.${BUTTON_GROUP_ITEM_CLASS}`); init(); }, @@ -2761,16 +2760,17 @@ QUnit.module('Chat', () => { }); }); - QUnit.module('Suggestions integration', { - beforeEach: function() { - moduleConfig.beforeEach.apply(this, arguments); + QUnit.module('Suggestions integration', moduleConfig, () => { + QUnit.test('should render suggestions element if option is not passed', function(assert) { + assert.strictEqual(this.getSuggestionsElement().length, 1, 'suggestions element is rendered without options'); + }); - this.getSuggestionsElement = () => this.$element.find(`.${CHAT_SUGGESTIONS_CLASS}`); - this.getItems = () => this.$element.find(`.${BUTTON_GROUP_ITEM_CLASS}`); - } - }, () => { - QUnit.test('should always render suggestions container element', function(assert) { - assert.strictEqual(this.getSuggestionsElement().length, 1, 'suggestions container is always rendered'); + QUnit.test('should render suggestions element when items are passed', function(assert) { + this.reinit({ + suggestions: { items: [{ text: 'Item 1' }] }, + }); + + assert.strictEqual(this.getSuggestionsElement().length, 1, 'suggestions element is rendered with items'); }); QUnit.test('suggestions should be rendered before messageBox', function(assert) { @@ -2791,8 +2791,8 @@ QUnit.module('Chat', () => { this.instance.option('suggestions', { items: [{ text: 'New 1' }, { text: 'New 2' }, { text: 'New 3' }] }); - assert.strictEqual(this.getItems().length, 3, 'items count updated'); - assert.strictEqual(this.getItems().eq(0).text(), 'New 1', 'first item text updated'); + assert.strictEqual(this.getSuggestionItems().length, 3, 'items count updated'); + assert.strictEqual(this.getSuggestionItems().eq(0).text(), 'New 1', 'first item text updated'); }); QUnit.test('onItemClick callback should be called when suggestion item is clicked', function(assert) { @@ -2809,7 +2809,18 @@ QUnit.module('Chat', () => { }, }); - this.getItems().first().trigger('dxclick'); + this.getSuggestionItems().first().trigger('dxclick'); + }); + + QUnit.test('should keep suggestions container when setting suggestions option to undefined at runtime', function(assert) { + this.reinit({ + suggestions: { items: [{ text: 'Item 1' }, { text: 'Item 2' }] }, + }); + + this.instance.option('suggestions', undefined); + + assert.strictEqual(this.getSuggestionsElement().length, 1, 'suggestions container remains in DOM'); + assert.strictEqual(this.getSuggestionItems().length, 0, 'items are removed'); }); }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/suggestions.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/suggestions.markup.tests.js index 9057f3066993..d2670203387d 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/suggestions.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/suggestions.markup.tests.js @@ -53,10 +53,10 @@ QUnit.module('Suggestions', moduleConfig, () => { assert.strictEqual($items.eq(1).text(), 'Second', 'second item text is correct'); }); - QUnit.test('should not render ButtonGroup if no options provided', function(assert) { + QUnit.test('should not add suggestions class if no options provided', function(assert) { this.reinit(); - assert.strictEqual(this.$suggestions.hasClass(BUTTON_GROUP_CLASS), false, 'ButtonGroup is not rendered'); + assert.strictEqual(this.$suggestions.length, 0, 'suggestions element with class is not rendered'); }); QUnit.test('should use outlined stylingMode by default', function(assert) { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/suggestions.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/suggestions.tests.js index b480a5c7dc4e..91b6de757707 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/suggestions.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/suggestions.tests.js @@ -2,23 +2,22 @@ import $ from 'jquery'; import Suggestions from '__internal/ui/chat/suggestions'; -const CHAT_SUGGESTIONS_CLASS = 'dx-chat-suggestions'; const BUTTON_GROUP_CLASS = 'dx-buttongroup'; const BUTTON_GROUP_ITEM_CLASS = 'dx-buttongroup-item'; const moduleConfig = { beforeEach: function() { const init = (options = {}) => { - this.$element = $('#component'); - this.instance = new Suggestions(this.$element, options); + this.$container = $('#component'); + this.instance = new Suggestions(this.$container, options); }; - this.getSuggestions = () => this.$element.find(`.${CHAT_SUGGESTIONS_CLASS}`); - this.getItems = () => this.$element.find(`.${BUTTON_GROUP_ITEM_CLASS}`); + this.getSuggestions = () => this.$container.children().eq(0); + this.getItems = () => this.$container.find(`.${BUTTON_GROUP_ITEM_CLASS}`); this.reinit = (options) => { this.instance.dispose(); - this.$element.empty(); + this.$container.empty(); init(options); }; @@ -61,6 +60,26 @@ QUnit.module('Suggestions', moduleConfig, () => { assert.strictEqual($items.length, 0, 'all items removed'); }); + + QUnit.test('should clean ButtonGroup when called with undefined', function(assert) { + assert.strictEqual(this.getSuggestions().hasClass(BUTTON_GROUP_CLASS), true, 'ButtonGroup rendered initially'); + + this.instance.updateOptions(undefined); + + const $element = this.$container.children().eq(0); + + assert.strictEqual($element.hasClass(BUTTON_GROUP_CLASS), false, 'ButtonGroup cleaned after updateOptions(undefined)'); + assert.strictEqual($element.length, 1, 'container element remains in DOM'); + }); + + QUnit.test('should clean ButtonGroup when called with empty object', function(assert) { + assert.strictEqual(this.getSuggestions().hasClass(BUTTON_GROUP_CLASS), true, 'ButtonGroup rendered initially'); + + this.instance.updateOptions({}); + + assert.strictEqual(this.getSuggestions().hasClass(BUTTON_GROUP_CLASS), false, 'ButtonGroup cleaned after updateOptions({})'); + assert.strictEqual(this.getSuggestions().length, 1, 'container element remains in DOM'); + }); }); QUnit.module('dispose', () => { @@ -83,6 +102,12 @@ QUnit.module('Suggestions', moduleConfig, () => { assert.ok(false, `dispose threw an error: ${e.message}`); } }); + + QUnit.test('should remove element from DOM after dispose', function(assert) { + this.instance.dispose(); + + assert.strictEqual(this.getSuggestions().length, 0, 'element is removed from DOM after dispose'); + }); }); QUnit.module('item click', () => { From 5d7e86a361b9e2568fbe970b9c0efd3eb71e09da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marker=20dao=20=C2=AE?= Date: Tue, 14 Apr 2026 22:40:21 +0200 Subject: [PATCH 08/13] revert(scheduler) --- .../appointment_popup/appointment_popup.integration.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.integration.test.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.integration.test.ts index 0f8f7713e743..bdf009c2ea61 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.integration.test.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.integration.test.ts @@ -1630,7 +1630,6 @@ describe('Appointment Form', () => { scheduler.showAppointmentPopup(commonAppointment); - // @ts-expect-error POM.popup.dxForm.option('items') const recurrenceGroup = POM.popup.dxForm.option('items')[1] as GroupItem; const allItems = flattenBy( recurrenceGroup.items as SimpleItem[], From 901e9fa52016e8bb8cd8a2d00d62535f04c8429d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marker=20dao=20=C2=AE?= Date: Wed, 15 Apr 2026 10:31:36 +0200 Subject: [PATCH 09/13] feat(suggs): Add clean into dispose && Refactor tests --- .../js/__internal/ui/chat/suggestions.ts | 1 + .../chatParts/suggestions.tests.js | 30 +++++++------------ 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/chat/suggestions.ts b/packages/devextreme/js/__internal/ui/chat/suggestions.ts index 1a966f08d748..f3f46f0daea8 100644 --- a/packages/devextreme/js/__internal/ui/chat/suggestions.ts +++ b/packages/devextreme/js/__internal/ui/chat/suggestions.ts @@ -71,6 +71,7 @@ class Suggestions { } dispose(): void { + this.clean(); this._$element?.remove(); this._$element = undefined; } diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/suggestions.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/suggestions.tests.js index 91b6de757707..d0a67bd9708e 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/suggestions.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/suggestions.tests.js @@ -82,31 +82,21 @@ QUnit.module('Suggestions', moduleConfig, () => { }); }); - QUnit.module('dispose', () => { - QUnit.test('should not throw on dispose', function(assert) { - try { - this.instance.dispose(); - assert.ok(true, 'dispose executed without errors'); - } catch(e) { - assert.ok(false, `dispose threw an error: ${e.message}`); - } - }); - - QUnit.test('should dispose without errors even if no items were provided', function(assert) { - this.reinit({ items: [] }); + QUnit.module('clean', () => { + QUnit.test('should remove ButtonGroup but keep element in DOM', function(assert) { + this.instance.clean(); - try { - this.instance.dispose(); - assert.ok(true, 'dispose executed without errors'); - } catch(e) { - assert.ok(false, `dispose threw an error: ${e.message}`); - } + assert.strictEqual(this.getSuggestions().hasClass(BUTTON_GROUP_CLASS), false, 'ButtonGroup is removed'); + assert.strictEqual(this.$container.children().length, 1, 'element remains in DOM'); }); + }); - QUnit.test('should remove element from DOM after dispose', function(assert) { + QUnit.module('dispose', () => { + QUnit.test('should clean ButtonGroup and remove element from DOM', function(assert) { this.instance.dispose(); - assert.strictEqual(this.getSuggestions().length, 0, 'element is removed from DOM after dispose'); + assert.strictEqual(this.$container.find(`.${BUTTON_GROUP_ITEM_CLASS}`).length, 0, 'ButtonGroup items are cleaned'); + assert.strictEqual(this.$container.children().length, 0, 'element is removed from DOM'); }); }); From 3c318c2c9fabe2449ca94a9af2f489924b8522a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marker=20dao=20=C2=AE?= Date: Wed, 15 Apr 2026 10:38:43 +0200 Subject: [PATCH 10/13] refactor() --- .../testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js | 1 - 1 file changed, 1 deletion(-) 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 f976a7fd1b24..460078a1d63b 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 @@ -131,7 +131,6 @@ const moduleConfig = { this.getMessageListEmptyView = () => this.$element.find(`.${CHAT_MESSAGELIST_EMPTY_VIEW_CLASS}`); this.getFileUploader = () => FileUploader.getInstance(this.$element.find(`.${FILEUPLOADER_CLASS}`)); this.getAttachButton = () => Button.getInstance(this.$element.find(`.${CHAT_TEXT_AREA_ATTACH_BUTTON}`)); - // this.getSuggestionsElement = () => $(); this.getSuggestionsElement = () => this.instance._suggestions._$element; this.getSuggestionItems = () => this.$element.find(`.${BUTTON_GROUP_ITEM_CLASS}`); From 5abfe85616bccc12827f6bcadf621b4a5794ff16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marker=20dao=20=C2=AE?= Date: Wed, 15 Apr 2026 11:32:26 +0200 Subject: [PATCH 11/13] fix(scss) --- .../base/chat/layout/chat-suggestions/_mixins.scss | 11 +++++++++-- .../appointment_popup.integration.test.ts | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_mixins.scss b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_mixins.scss index 02951f094307..be30e36fa098 100644 --- a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_mixins.scss +++ b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-suggestions/_mixins.scss @@ -18,8 +18,15 @@ box-shadow: $box-shadow; } - .dx-buttongroup-wrapper { - gap: $gap; + &.dx-buttongroup { + &.dx-buttongroup-mode-contained { + border-radius: 0; + box-shadow: unset; + } + + .dx-buttongroup-wrapper { + gap: $gap; + } } .dx-buttongroup-item { diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.integration.test.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.integration.test.ts index bdf009c2ea61..352a09c05f46 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.integration.test.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.integration.test.ts @@ -1630,6 +1630,7 @@ describe('Appointment Form', () => { scheduler.showAppointmentPopup(commonAppointment); + // @ts-expect-error POM.popup.dxForm.option('items')[1] const recurrenceGroup = POM.popup.dxForm.option('items')[1] as GroupItem; const allItems = flattenBy( recurrenceGroup.items as SimpleItem[], From b4ac08f278721687f09d8b9e0ef820ec0eb16590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marker=20dao=20=C2=AE?= Date: Wed, 15 Apr 2026 11:33:56 +0200 Subject: [PATCH 12/13] feat(chat.d.ts): Omit selectedItems --- packages/devextreme/js/ui/chat.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/js/ui/chat.d.ts b/packages/devextreme/js/ui/chat.d.ts index 4a31636dc88b..abc934f072a0 100644 --- a/packages/devextreme/js/ui/chat.d.ts +++ b/packages/devextreme/js/ui/chat.d.ts @@ -538,7 +538,7 @@ export interface dxChatOptions extends WidgetOptions { * @type dxButtonGroupOptions * @public */ - suggestions?: Omit; + suggestions?: Omit; /** * @docid * @default [] From ad2b7197fe6659940e888b8a2b90d2b80fd29f6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marker=20dao=20=C2=AE?= Date: Wed, 15 Apr 2026 11:50:51 +0200 Subject: [PATCH 13/13] regenerate --- packages/devextreme/ts/dx.all.d.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index 44b09be25f1b..1c800528da72 100644 --- a/packages/devextreme/ts/dx.all.d.ts +++ b/packages/devextreme/ts/dx.all.d.ts @@ -11487,7 +11487,10 @@ declare module DevExpress.ui { /** * [descr:dxChatOptions.suggestions] */ - suggestions?: Omit; + suggestions?: Omit< + DevExpress.ui.dxButtonGroup.Properties, + 'selectionMode' | 'selectedItemKeys' | 'selectedItems' + >; /** * [descr:dxChatOptions.typingUsers] */