Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion apps/react-storybook/stories/chat/Chat.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -985,4 +985,57 @@ export const ControlledMode: Story = {
</div>
);
}
}
}

export const SendButtonOptions: Story = {
args: {
action: 'send',
icon: 'arrowright',
enableOnClick: false,
},
argTypes: {
action: {
control: 'select',
options: ['send', 'custom'],
},
icon: {
control: 'text',
},
enableOnClick: {
name: 'Enable onClick handler',
control: 'boolean',
},
},
render: ({ action, icon, enableOnClick }) => {
const [messages, setMessages] = useState<ChatTypes.Message[]>([...initialMessages]);
const [lastClick, setLastClick] = useState<string>('—');

const onMessageEntered = useCallback(({ message }: ChatTypes.MessageEnteredEvent) => {
setMessages((prev) => [...prev, message]);
}, []);

const sendButtonOptions = useMemo<ChatTypes.SendButtonProperties>(() => ({
action,
icon,
...(enableOnClick && {
onClick: () => setLastClick(new Date().toLocaleTimeString()),
}),
}), [action, icon, enableOnClick]);

return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}>
<div>
Last onClick fired at: <strong>{lastClick}</strong>
</div>
<Chat
width={400}
height={500}
items={messages}
user={secondAuthor}
onMessageEntered={onMessageEntered}
sendButtonOptions={sendButtonOptions}
/>
</div>
);
},
};
32 changes: 32 additions & 0 deletions packages/devextreme/js/__internal/ui/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import type {
MessageUpdatedEvent,
MessageUpdatingEvent,
Properties as ChatProperties,
SendButtonClickEvent,
SendButtonProperties,
TypingEndEvent,
TypingStartEvent,
} from '@js/ui/chat';
Expand Down Expand Up @@ -79,6 +81,8 @@ class Chat extends Widget<ChatProperties> {

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

_sendButtonAction?: (e: Partial<SendButtonClickEvent>) => void;

_getDefaultOptions(): ChatProperties {
return {
...super._getDefaultOptions(),
Expand Down Expand Up @@ -107,6 +111,11 @@ class Chat extends Widget<ChatProperties> {
speechToTextOptions: undefined,
typingUsers: [],
user: { id: new Guid().toString() },
sendButtonOptions: {
icon: 'arrowright',
action: 'send',
onClick: undefined,
},
onMessageDeleted: undefined,
onMessageDeleting: undefined,
onMessageEditCanceled: undefined,
Expand Down Expand Up @@ -138,6 +147,7 @@ class Chat extends Widget<ChatProperties> {
this._createTypingEndAction();
this._createAttachmentDownloadAction();
this._createInputFieldTextChangedAction();
this._createSendButtonAction();
}

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

const sendButtonOptions = this._getSendButtonOptionsWithAction();

const $messageBox = $('<div>');

this.$element().append($messageBox);
Expand All @@ -485,6 +497,7 @@ class Chat extends Widget<ChatProperties> {
text: inputFieldText,
speechToTextEnabled,
speechToTextOptions,
sendButtonOptions,
onMessageEntered: (e) => {
this._messageEnteredHandler(e);
},
Expand Down Expand Up @@ -605,6 +618,21 @@ class Chat extends Widget<ChatProperties> {
);
}

_createSendButtonAction(): void {
const { sendButtonOptions } = this.option();

this._sendButtonAction = this._createAction(sendButtonOptions?.onClick, { excludeValidators: ['disabled'] });
}

_getSendButtonOptionsWithAction(): SendButtonProperties | undefined {
const { sendButtonOptions } = this.option();

return {
...sendButtonOptions,
onClick: this._sendButtonAction,
};
}

_messageEnteredHandler(e: MessageBoxMessageEnteredEvent): void {
const { text, event, attachments } = e;
const { user } = this.option();
Expand Down Expand Up @@ -743,6 +771,10 @@ class Chat extends Widget<ChatProperties> {
break;
case 'reloadOnChange':
break;
case 'sendButtonOptions':
this._createSendButtonAction();
this._messageBox.option(name, this._getSendButtonOptionsWithAction());
break;
default:
super._optionChanged(args);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type {
InitializedEvent,
} from '@js/ui/button';
import type Button from '@js/ui/button';
import type { Attachment } from '@js/ui/chat';
import type { Attachment, SendButtonProperties } from '@js/ui/chat';
import type {
UploadedEvent,
UploadStartedEvent,
Expand Down Expand Up @@ -60,6 +60,8 @@ export type Properties = TextAreaProperties & {

speechToTextOptions?: SpeechToTextProperties;

sendButtonOptions?: SendButtonProperties;

onSend?: (e: SendEvent) => void;
};

Expand Down Expand Up @@ -104,6 +106,14 @@ export const SEND_BUTTON_READY_TO_SEND_STATE: ButtonState = {
disabled: false,
};

export const SEND_BUTTON_CUSTOM_ACTIVE_STATE: ButtonState = {
stylingMode: 'contained',
type: 'default',
disabled: false,
};

const SEND_BUTTON_DEFAULT_ICON = 'arrowright';

const isMobile = (): boolean => devices.current().deviceType !== 'desktop';

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

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

_sendButtonClickAction?: (e: Partial<ClickEvent>) => void;

getAttachments(): Attachment[] | undefined {
if (!this._filesToSend?.size) {
return undefined;
Expand Down Expand Up @@ -201,6 +213,7 @@ class ChatTextArea extends TextArea<Properties> {
super._init();

this._createSendAction();
this._createSendButtonClickAction();
}

_createSendAction(): void {
Expand All @@ -210,6 +223,15 @@ class ChatTextArea extends TextArea<Properties> {
);
}

_createSendButtonClickAction(): void {
const { sendButtonOptions } = this.option();

this._sendButtonClickAction = this._createAction(
sendButtonOptions?.onClick,
{ excludeValidators: ['disabled'] },
);
}

_initMarkup(): void {
this.$element().addClass(CHAT_TEXTAREA_CLASS);
super._initMarkup();
Expand Down Expand Up @@ -378,6 +400,7 @@ class ChatTextArea extends TextArea<Properties> {
activeStateEnabled,
focusStateEnabled,
hoverStateEnabled,
sendButtonOptions,
} = this.option();

const configuration = {
Expand All @@ -387,13 +410,14 @@ class ChatTextArea extends TextArea<Properties> {
activeStateEnabled,
focusStateEnabled,
hoverStateEnabled,
icon: 'arrowright',
icon: sendButtonOptions?.icon ?? SEND_BUTTON_DEFAULT_ICON,
...SEND_BUTTON_INITIAL_STATE,
elementAttr: {
'aria-label': messageLocalization.format('dxChat-sendButtonAriaLabel'),
},
onClick: (e: ClickEvent): void => {
this._processSendButtonActivation(e);
this._sendButtonClickAction?.(e);
},
onInitialized: (e: InitializedEvent): void => {
this._sendButton = e.component;
Expand Down Expand Up @@ -566,6 +590,12 @@ class ChatTextArea extends TextArea<Properties> {
return;
}

if (this._isCustomBehavior()) {
this._speechToTextButton?.option(STT_INITIAL_STATE);
this._sendButton?.option(SEND_BUTTON_CUSTOM_ACTIVE_STATE);
return;
}

this._speechToTextButton?.option(STT_INITIAL_STATE);
this._sendButton?.option(SEND_BUTTON_INITIAL_STATE);
}
Expand Down Expand Up @@ -594,7 +624,18 @@ class ChatTextArea extends TextArea<Properties> {
this._updateButtonsState();
}

_isCustomBehavior(): boolean {
const { sendButtonOptions } = this.option();

return sendButtonOptions?.action === 'custom';
}

_processSendButtonActivation(e: Partial<SendEvent>): void {
if (this._isCustomBehavior()) {
this._updateButtonsState();
return;
}

this._sendAction?.(e);
this.clear();
this.resetFileUploader();
Expand Down Expand Up @@ -640,6 +681,10 @@ class ChatTextArea extends TextArea<Properties> {
this._speechToTextButton?.option(this._getSpeechToTextButtonOptions());
break;

case 'sendButtonOptions':
this._handleSendButtonOptionsChange();
break;

default:
super._optionChanged(args);
}
Expand All @@ -661,6 +706,22 @@ class ChatTextArea extends TextArea<Properties> {
this._fileUploader?.option(options);
}

_handleSendButtonOptionsChange(): void {
const { sendButtonOptions } = this.option();

this._createSendButtonClickAction();

this._sendButton?.option({
onClick: (e: ClickEvent): void => {
this._processSendButtonActivation(e);
this._sendButtonClickAction?.(e);
},
icon: sendButtonOptions?.icon ?? SEND_BUTTON_DEFAULT_ICON,
});

this._updateButtonsState();
}

_isValuableTextEntered(): boolean {
const { text } = this.option();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { NativeEventInfo } from '@js/common/core/events';
import $, { type dxElementWrapper } from '@js/core/renderer';
import type { InteractionEvent } from '@js/events';
import type { Attachment, InputFieldTextChangedEvent } from '@js/ui/chat';
import type { Attachment, InputFieldTextChangedEvent, SendButtonProperties } from '@js/ui/chat';
import type { Properties as FileUploaderProperties } from '@js/ui/file_uploader';
import type { Properties as SpeechToTextProperties } from '@js/ui/speech_to_text';
import type { InputEvent } from '@js/ui/text_area';
Expand Down Expand Up @@ -40,6 +40,8 @@ export interface Properties extends DOMComponentProperties<MessageBox> {

text?: string;

sendButtonOptions?: SendButtonProperties;

onMessageEntered?: (e: MessageEnteredEvent) => void;

onTypingStart?: (e: TypingStartEvent) => void;
Expand Down Expand Up @@ -176,6 +178,7 @@ class MessageBox extends DOMComponent<MessageBox, Properties> {
speechToTextEnabled,
speechToTextOptions,
text,
sendButtonOptions,
} = this.option();

const options = {
Expand All @@ -187,6 +190,7 @@ class MessageBox extends DOMComponent<MessageBox, Properties> {
value: previewText || text,
speechToTextEnabled,
speechToTextOptions,
sendButtonOptions,
onInput: (e: InputEvent): void => {
this._triggerTypingStartAction(e);
this._updateTypingEndTimeout();
Expand Down Expand Up @@ -321,6 +325,10 @@ class MessageBox extends DOMComponent<MessageBox, Properties> {
this._textArea.option('value', value);
break;

case 'sendButtonOptions':
this._textArea.option(fullName, value);
break;

default:
super._optionChanged(args);
}
Expand Down
Loading
Loading