Skip to content

Commit 6f617ea

Browse files
r-farkhutdinovRuslan Farkhutdinov
andauthored
Chat: Support sendButtonOptions option (#33137)
Co-authored-by: Ruslan Farkhutdinov <ruslan.farkhutdinov@devexpress.com>
1 parent a165032 commit 6f617ea

7 files changed

Lines changed: 438 additions & 4 deletions

File tree

apps/react-storybook/stories/chat/Chat.stories.tsx

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -985,4 +985,57 @@ export const ControlledMode: Story = {
985985
</div>
986986
);
987987
}
988-
}
988+
}
989+
990+
export const SendButtonOptions: Story = {
991+
args: {
992+
action: 'send',
993+
icon: 'arrowright',
994+
enableOnClick: false,
995+
},
996+
argTypes: {
997+
action: {
998+
control: 'select',
999+
options: ['send', 'custom'],
1000+
},
1001+
icon: {
1002+
control: 'text',
1003+
},
1004+
enableOnClick: {
1005+
name: 'Enable onClick handler',
1006+
control: 'boolean',
1007+
},
1008+
},
1009+
render: ({ action, icon, enableOnClick }) => {
1010+
const [messages, setMessages] = useState<ChatTypes.Message[]>([...initialMessages]);
1011+
const [lastClick, setLastClick] = useState<string>('—');
1012+
1013+
const onMessageEntered = useCallback(({ message }: ChatTypes.MessageEnteredEvent) => {
1014+
setMessages((prev) => [...prev, message]);
1015+
}, []);
1016+
1017+
const sendButtonOptions = useMemo<ChatTypes.SendButtonProperties>(() => ({
1018+
action,
1019+
icon,
1020+
...(enableOnClick && {
1021+
onClick: () => setLastClick(new Date().toLocaleTimeString()),
1022+
}),
1023+
}), [action, icon, enableOnClick]);
1024+
1025+
return (
1026+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}>
1027+
<div>
1028+
Last onClick fired at: <strong>{lastClick}</strong>
1029+
</div>
1030+
<Chat
1031+
width={400}
1032+
height={500}
1033+
items={messages}
1034+
user={secondAuthor}
1035+
onMessageEntered={onMessageEntered}
1036+
sendButtonOptions={sendButtonOptions}
1037+
/>
1038+
</div>
1039+
);
1040+
},
1041+
};

packages/devextreme/js/__internal/ui/chat/chat.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import type {
1818
MessageUpdatedEvent,
1919
MessageUpdatingEvent,
2020
Properties as ChatProperties,
21+
SendButtonClickEvent,
22+
SendButtonProperties,
2123
TypingEndEvent,
2224
TypingStartEvent,
2325
} from '@js/ui/chat';
@@ -79,6 +81,8 @@ class Chat extends Widget<ChatProperties> {
7981

8082
_inputFieldTextChangedAction?: (e: Partial<InputFieldTextChangedEvent>) => void;
8183

84+
_sendButtonAction?: (e: Partial<SendButtonClickEvent>) => void;
85+
8286
_getDefaultOptions(): ChatProperties {
8387
return {
8488
...super._getDefaultOptions(),
@@ -107,6 +111,11 @@ class Chat extends Widget<ChatProperties> {
107111
speechToTextOptions: undefined,
108112
typingUsers: [],
109113
user: { id: new Guid().toString() },
114+
sendButtonOptions: {
115+
icon: 'arrowright',
116+
action: 'send',
117+
onClick: undefined,
118+
},
110119
onMessageDeleted: undefined,
111120
onMessageDeleting: undefined,
112121
onMessageEditCanceled: undefined,
@@ -138,6 +147,7 @@ class Chat extends Widget<ChatProperties> {
138147
this._createTypingEndAction();
139148
this._createAttachmentDownloadAction();
140149
this._createInputFieldTextChangedAction();
150+
this._createSendButtonAction();
141151
}
142152

143153
_dataSourceLoadErrorHandler(): void {
@@ -473,6 +483,8 @@ class Chat extends Widget<ChatProperties> {
473483
speechToTextOptions,
474484
} = this.option();
475485

486+
const sendButtonOptions = this._getSendButtonOptionsWithAction();
487+
476488
const $messageBox = $('<div>');
477489

478490
this.$element().append($messageBox);
@@ -485,6 +497,7 @@ class Chat extends Widget<ChatProperties> {
485497
text: inputFieldText,
486498
speechToTextEnabled,
487499
speechToTextOptions,
500+
sendButtonOptions,
488501
onMessageEntered: (e) => {
489502
this._messageEnteredHandler(e);
490503
},
@@ -605,6 +618,21 @@ class Chat extends Widget<ChatProperties> {
605618
);
606619
}
607620

621+
_createSendButtonAction(): void {
622+
const { sendButtonOptions } = this.option();
623+
624+
this._sendButtonAction = this._createAction(sendButtonOptions?.onClick, { excludeValidators: ['disabled'] });
625+
}
626+
627+
_getSendButtonOptionsWithAction(): SendButtonProperties | undefined {
628+
const { sendButtonOptions } = this.option();
629+
630+
return {
631+
...sendButtonOptions,
632+
onClick: this._sendButtonAction,
633+
};
634+
}
635+
608636
_messageEnteredHandler(e: MessageBoxMessageEnteredEvent): void {
609637
const { text, event, attachments } = e;
610638
const { user } = this.option();
@@ -743,6 +771,10 @@ class Chat extends Widget<ChatProperties> {
743771
break;
744772
case 'reloadOnChange':
745773
break;
774+
case 'sendButtonOptions':
775+
this._createSendButtonAction();
776+
this._messageBox.option(name, this._getSendButtonOptionsWithAction());
777+
break;
746778
default:
747779
super._optionChanged(args);
748780
}

packages/devextreme/js/__internal/ui/chat/message_box/chat_text_area.ts

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type {
1414
InitializedEvent,
1515
} from '@js/ui/button';
1616
import type Button from '@js/ui/button';
17-
import type { Attachment } from '@js/ui/chat';
17+
import type { Attachment, SendButtonProperties } from '@js/ui/chat';
1818
import type {
1919
UploadedEvent,
2020
UploadStartedEvent,
@@ -60,6 +60,8 @@ export type Properties = TextAreaProperties & {
6060

6161
speechToTextOptions?: SpeechToTextProperties;
6262

63+
sendButtonOptions?: SendButtonProperties;
64+
6365
onSend?: (e: SendEvent) => void;
6466
};
6567

@@ -104,6 +106,14 @@ export const SEND_BUTTON_READY_TO_SEND_STATE: ButtonState = {
104106
disabled: false,
105107
};
106108

109+
export const SEND_BUTTON_CUSTOM_ACTIVE_STATE: ButtonState = {
110+
stylingMode: 'contained',
111+
type: 'default',
112+
disabled: false,
113+
};
114+
115+
const SEND_BUTTON_DEFAULT_ICON = 'arrowright';
116+
107117
const isMobile = (): boolean => devices.current().deviceType !== 'desktop';
108118

109119
export const DEFAULT_ALLOWED_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.pdf', '.docx', '.xlsx', '.pptx', '.txt', '.rtf', '.csv', '.md'];
@@ -135,6 +145,8 @@ class ChatTextArea extends TextArea<Properties> {
135145

136146
_sendAction?: (e: Partial<SendEvent>) => void;
137147

148+
_sendButtonClickAction?: (e: Partial<ClickEvent>) => void;
149+
138150
getAttachments(): Attachment[] | undefined {
139151
if (!this._filesToSend?.size) {
140152
return undefined;
@@ -201,6 +213,7 @@ class ChatTextArea extends TextArea<Properties> {
201213
super._init();
202214

203215
this._createSendAction();
216+
this._createSendButtonClickAction();
204217
}
205218

206219
_createSendAction(): void {
@@ -210,6 +223,15 @@ class ChatTextArea extends TextArea<Properties> {
210223
);
211224
}
212225

226+
_createSendButtonClickAction(): void {
227+
const { sendButtonOptions } = this.option();
228+
229+
this._sendButtonClickAction = this._createAction(
230+
sendButtonOptions?.onClick,
231+
{ excludeValidators: ['disabled'] },
232+
);
233+
}
234+
213235
_initMarkup(): void {
214236
this.$element().addClass(CHAT_TEXTAREA_CLASS);
215237
super._initMarkup();
@@ -378,6 +400,7 @@ class ChatTextArea extends TextArea<Properties> {
378400
activeStateEnabled,
379401
focusStateEnabled,
380402
hoverStateEnabled,
403+
sendButtonOptions,
381404
} = this.option();
382405

383406
const configuration = {
@@ -387,13 +410,14 @@ class ChatTextArea extends TextArea<Properties> {
387410
activeStateEnabled,
388411
focusStateEnabled,
389412
hoverStateEnabled,
390-
icon: 'arrowright',
413+
icon: sendButtonOptions?.icon ?? SEND_BUTTON_DEFAULT_ICON,
391414
...SEND_BUTTON_INITIAL_STATE,
392415
elementAttr: {
393416
'aria-label': messageLocalization.format('dxChat-sendButtonAriaLabel'),
394417
},
395418
onClick: (e: ClickEvent): void => {
396419
this._processSendButtonActivation(e);
420+
this._sendButtonClickAction?.(e);
397421
},
398422
onInitialized: (e: InitializedEvent): void => {
399423
this._sendButton = e.component;
@@ -566,6 +590,12 @@ class ChatTextArea extends TextArea<Properties> {
566590
return;
567591
}
568592

593+
if (this._isCustomBehavior()) {
594+
this._speechToTextButton?.option(STT_INITIAL_STATE);
595+
this._sendButton?.option(SEND_BUTTON_CUSTOM_ACTIVE_STATE);
596+
return;
597+
}
598+
569599
this._speechToTextButton?.option(STT_INITIAL_STATE);
570600
this._sendButton?.option(SEND_BUTTON_INITIAL_STATE);
571601
}
@@ -594,7 +624,18 @@ class ChatTextArea extends TextArea<Properties> {
594624
this._updateButtonsState();
595625
}
596626

627+
_isCustomBehavior(): boolean {
628+
const { sendButtonOptions } = this.option();
629+
630+
return sendButtonOptions?.action === 'custom';
631+
}
632+
597633
_processSendButtonActivation(e: Partial<SendEvent>): void {
634+
if (this._isCustomBehavior()) {
635+
this._updateButtonsState();
636+
return;
637+
}
638+
598639
this._sendAction?.(e);
599640
this.clear();
600641
this.resetFileUploader();
@@ -640,6 +681,10 @@ class ChatTextArea extends TextArea<Properties> {
640681
this._speechToTextButton?.option(this._getSpeechToTextButtonOptions());
641682
break;
642683

684+
case 'sendButtonOptions':
685+
this._handleSendButtonOptionsChange();
686+
break;
687+
643688
default:
644689
super._optionChanged(args);
645690
}
@@ -661,6 +706,22 @@ class ChatTextArea extends TextArea<Properties> {
661706
this._fileUploader?.option(options);
662707
}
663708

709+
_handleSendButtonOptionsChange(): void {
710+
const { sendButtonOptions } = this.option();
711+
712+
this._createSendButtonClickAction();
713+
714+
this._sendButton?.option({
715+
onClick: (e: ClickEvent): void => {
716+
this._processSendButtonActivation(e);
717+
this._sendButtonClickAction?.(e);
718+
},
719+
icon: sendButtonOptions?.icon ?? SEND_BUTTON_DEFAULT_ICON,
720+
});
721+
722+
this._updateButtonsState();
723+
}
724+
664725
_isValuableTextEntered(): boolean {
665726
const { text } = this.option();
666727

packages/devextreme/js/__internal/ui/chat/message_box/message_box.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { NativeEventInfo } from '@js/common/core/events';
22
import $, { type dxElementWrapper } from '@js/core/renderer';
33
import type { InteractionEvent } from '@js/events';
4-
import type { Attachment, InputFieldTextChangedEvent } from '@js/ui/chat';
4+
import type { Attachment, InputFieldTextChangedEvent, SendButtonProperties } from '@js/ui/chat';
55
import type { Properties as FileUploaderProperties } from '@js/ui/file_uploader';
66
import type { Properties as SpeechToTextProperties } from '@js/ui/speech_to_text';
77
import type { InputEvent } from '@js/ui/text_area';
@@ -40,6 +40,8 @@ export interface Properties extends DOMComponentProperties<MessageBox> {
4040

4141
text?: string;
4242

43+
sendButtonOptions?: SendButtonProperties;
44+
4345
onMessageEntered?: (e: MessageEnteredEvent) => void;
4446

4547
onTypingStart?: (e: TypingStartEvent) => void;
@@ -176,6 +178,7 @@ class MessageBox extends DOMComponent<MessageBox, Properties> {
176178
speechToTextEnabled,
177179
speechToTextOptions,
178180
text,
181+
sendButtonOptions,
179182
} = this.option();
180183

181184
const options = {
@@ -187,6 +190,7 @@ class MessageBox extends DOMComponent<MessageBox, Properties> {
187190
value: previewText || text,
188191
speechToTextEnabled,
189192
speechToTextOptions,
193+
sendButtonOptions,
190194
onInput: (e: InputEvent): void => {
191195
this._triggerTypingStartAction(e);
192196
this._updateTypingEndTimeout();
@@ -321,6 +325,10 @@ class MessageBox extends DOMComponent<MessageBox, Properties> {
321325
this._textArea.option('value', value);
322326
break;
323327

328+
case 'sendButtonOptions':
329+
this._textArea.option(fullName, value);
330+
break;
331+
324332
default:
325333
super._optionChanged(args);
326334
}

0 commit comments

Comments
 (0)