Skip to content

Commit da4733d

Browse files
committed
refactor: Cleaned up chat state model
1 parent 1b0a763 commit da4733d

File tree

5 files changed

+99
-105
lines changed

5 files changed

+99
-105
lines changed

src/components/chat/chat-input.ts

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,8 @@ export default class IgcChatInputComponent extends LitElement {
113113
addThemingController(this, all);
114114
}
115115

116-
protected override firstUpdated() {
117-
this._state.updateAcceptedTypesCache();
118-
this._state.textArea = this._textInputElement;
116+
public focusInput(): void {
117+
this._textInputElement.focus();
119118
}
120119

121120
private _getRenderer<U extends keyof DefaultInputRenderers>(
@@ -127,12 +126,32 @@ export default class IgcChatInputComponent extends LitElement {
127126
: this._defaults[name];
128127
}
129128

129+
private async _sendMessage(): Promise<void> {
130+
if (
131+
!this._userInputState.hasInputValue &&
132+
!this._userInputState.hasInputAttachments
133+
) {
134+
return;
135+
}
136+
137+
this._userInputState.addMessageWithEvent({
138+
text: this._userInputState.inputValue,
139+
attachments: this._userInputState.inputAttachments,
140+
});
141+
142+
this.style.height = 'auto';
143+
144+
await this._userInputState.host.updateComplete;
145+
this.focusInput();
146+
}
147+
130148
private _handleKeydown(event: KeyboardEvent): void {
131-
const isSend = event.key === enterKey && !event.shiftKey;
149+
const isSendRequest =
150+
event.key === enterKey.toLowerCase() && !event.shiftKey;
132151

133-
if (isSend) {
152+
if (isSendRequest) {
134153
event.preventDefault();
135-
this._state.sendMessage();
154+
this._sendMessage();
136155
} else {
137156
// TODO:
138157
this._state.handleKeyDown(event);
@@ -199,7 +218,9 @@ export default class IgcChatInputComponent extends LitElement {
199218
* @param e Input event from the text area
200219
*/
201220
private _handleInput({ detail }: CustomEvent<string>): void {
202-
if (detail === this._state.inputValue) return;
221+
if (detail === this._state.inputValue) {
222+
return;
223+
}
203224

204225
this._state.inputValue = detail;
205226
this._state.emitEvent('igcInputChange', { detail: { value: detail } });
@@ -219,11 +240,11 @@ export default class IgcChatInputComponent extends LitElement {
219240
*/
220241
private _renderAttachmentsArea(attachments: IgcChatMessageAttachment[]) {
221242
return html`${attachments?.map(
222-
(attachment, index) => html`
243+
(attachment) => html`
223244
<div part="attachment-wrapper" role="listitem">
224245
<igc-chip
225246
removable
226-
@igcRemove=${() => this._state.removeAttachment(index)}
247+
@igcRemove=${() => this._state.removeAttachment(attachment)}
227248
>
228249
<igc-icon
229250
slot="prefix"
@@ -281,6 +302,7 @@ export default class IgcChatInputComponent extends LitElement {
281302
<input
282303
type="file"
283304
id="input_attachments"
305+
tabindex="-1"
284306
name="input_attachments"
285307
aria-label="Upload button"
286308
multiple
@@ -308,7 +330,7 @@ export default class IgcChatInputComponent extends LitElement {
308330
variant="contained"
309331
part="send-button"
310332
?disabled=${!enabled}
311-
@click=${this._state.sendMessage}
333+
@click=${this._sendMessage}
312334
></igc-icon-button>
313335
`;
314336
}

src/components/chat/chat-message.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import { chatContext } from '../common/context.js';
99
import { registerComponent } from '../common/definitions/register.js';
1010
import { partMap } from '../common/part-map.js';
1111
import { isEmpty } from '../common/util.js';
12-
import IgcToastComponent from '../toast/toast.js';
13-
import IgcTooltipComponent from '../tooltip/tooltip.js';
1412
import type { ChatState } from './chat-state.js';
1513
import IgcMessageAttachmentsComponent from './message-attachments.js';
1614
import { styles } from './themes/message.base.css.js';
@@ -65,10 +63,7 @@ export default class IgcChatMessageComponent extends LitElement {
6563
registerComponent(
6664
IgcChatMessageComponent,
6765
IgcMessageAttachmentsComponent,
68-
IgcTooltipComponent,
69-
IgcToastComponent,
70-
IgcIconButtonComponent,
71-
IgcTooltipComponent
66+
IgcIconButtonComponent
7267
);
7368
}
7469

@@ -123,7 +118,7 @@ export default class IgcChatMessageComponent extends LitElement {
123118
if (navigator.clipboard?.writeText) {
124119
try {
125120
await navigator.clipboard.writeText(clipboardText);
126-
this._state.showChatActionToast(resourceStrings.messageCopied);
121+
this._state.showActionToast(resourceStrings.messageCopied);
127122
} catch (err) {
128123
throw new Error(`Failed to copy message via Clipboard API: ${err}`);
129124
}
@@ -222,9 +217,9 @@ export default class IgcChatMessageComponent extends LitElement {
222217
name=${name}
223218
variant="flat"
224219
@pointerenter=${({ target }: PointerEvent) =>
225-
this._state.showChatActionsTooltip(target as Element, tooltipMessage)}
220+
this._state.showActionsTooltip(target as Element, tooltipMessage)}
226221
@focus=${({ target }: FocusEvent) =>
227-
this._state.showChatActionsTooltip(target as Element, tooltipMessage)}
222+
this._state.showActionsTooltip(target as Element, tooltipMessage)}
228223
></igc-icon-button>
229224
`;
230225
}

src/components/chat/chat-state.ts

Lines changed: 46 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import { enterKey } from '../common/controllers/key-bindings.js';
21
import { IgcChatResourceStringEN } from '../common/i18n/chat.resources.js';
32
import { isEmpty, nanoid } from '../common/util.js';
4-
import type IgcTextareaComponent from '../textarea/textarea.js';
53
import IgcToastComponent from '../toast/toast.js';
64
import IgcTooltipComponent from '../tooltip/tooltip.js';
75
import type IgcChatComponent from './chat.js';
@@ -29,9 +27,6 @@ export class ChatState {
2927
private readonly _contextUpdateFn: () => unknown;
3028
private readonly _userInputContextUpdateFn: () => unknown;
3129

32-
/** Reference to the text area input component */
33-
private _textArea: IgcTextareaComponent | null = null;
34-
3530
private _actionsTooltip?: IgcTooltipComponent;
3631
private _actionToast?: IgcToastComponent;
3732
/** The current list of messages */
@@ -100,6 +95,7 @@ export class ChatState {
10095
*/
10196
public set options(value: IgcChatOptions) {
10297
this._options = value;
98+
this._setAcceptedTypesCache();
10399
this._contextUpdateFn.call(this._host);
104100
}
105101

@@ -124,20 +120,6 @@ export class ChatState {
124120
return this._options?.stopTypingDelay ?? this._stopTypingDelay;
125121
}
126122

127-
/**
128-
* Gets the text area component.
129-
*/
130-
public get textArea(): IgcTextareaComponent | null {
131-
return this._textArea;
132-
}
133-
134-
/**
135-
* Sets the text area component.
136-
*/
137-
public set textArea(value: IgcTextareaComponent) {
138-
this._textArea = value;
139-
}
140-
141123
/**
142124
* Gets the list of attachments currently attached to input.
143125
*/
@@ -168,10 +150,18 @@ export class ChatState {
168150
this._userInputContextUpdateFn.call(this._host);
169151
}
170152

153+
/**
154+
* Returns whether the default chat input textarea has a trimmed value payload.
155+
* @internal
156+
*/
171157
public get hasInputValue(): boolean {
172-
return this._inputValue.trim() !== '';
158+
return !!this._inputValue.trim();
173159
}
174160

161+
/**
162+
* Returns whether the default file input of the chat has any attached files.
163+
* @internal
164+
*/
175165
public get hasInputAttachments(): boolean {
176166
return !isEmpty(this._inputAttachments);
177167
}
@@ -198,7 +188,10 @@ export class ChatState {
198188
return this._host.emitEvent(name, args);
199189
}
200190

201-
public showChatActionsTooltip(target: Element, message: string): void {
191+
/**
192+
* @internal
193+
*/
194+
public showActionsTooltip(target: Element, message: string): void {
202195
if (!this._actionsTooltip) {
203196
this._actionsTooltip = document.createElement(
204197
IgcTooltipComponent.tagName
@@ -211,7 +204,10 @@ export class ChatState {
211204
this._actionsTooltip.show(target);
212205
}
213206

214-
public showChatActionToast(content: string): void {
207+
/**
208+
* @internal
209+
*/
210+
public showActionToast(content: string): void {
215211
if (!this._actionToast) {
216212
this._actionToast = document.createElement(IgcToastComponent.tagName);
217213
this._host.renderRoot.appendChild(this._actionToast);
@@ -222,6 +218,16 @@ export class ChatState {
222218

223219
//#endregion
224220

221+
/**
222+
* Updates the internal cache for accepted file types.
223+
* Parses the acceptedFiles string option into extensions, mimeTypes, and wildcard types.
224+
*/
225+
private _setAcceptedTypesCache(): void {
226+
this._acceptedTypesCache = this.options?.acceptedFiles
227+
? parseAcceptedFileTypes(this.options.acceptedFiles)
228+
: null;
229+
}
230+
225231
protected _createMessage(message: Partial<IgcChatMessage>): IgcChatMessage {
226232
return {
227233
id: message.id ?? nanoid(),
@@ -296,18 +302,13 @@ export class ChatState {
296302
}
297303
}
298304

299-
public handleKeyDown = (e: KeyboardEvent) => {
300-
if (e.key.toLowerCase() === enterKey.toLowerCase() && !e.shiftKey) {
301-
e.preventDefault();
302-
this.sendMessage();
303-
} else {
304-
this._lastTyped = Date.now();
305-
if (!this._isTyping) {
306-
this.emitEvent('igcTypingChange', {
307-
detail: { isTyping: true },
308-
});
309-
this._isTyping = true;
310-
}
305+
public handleKeyDown = (_: KeyboardEvent) => {
306+
this._lastTyped = Date.now();
307+
if (!this._isTyping) {
308+
this.emitEvent('igcTypingChange', {
309+
detail: { isTyping: true },
310+
});
311+
this._isTyping = true;
311312

312313
const stopTypingDelay = this.stopTypingDelay;
313314
setTimeout(() => {
@@ -325,62 +326,25 @@ export class ChatState {
325326
}
326327
};
327328

328-
public sendMessage = () => {
329-
if (!this.inputValue.trim() && this.inputAttachments.length === 0) return;
330-
331-
this.addMessageWithEvent({
332-
text: this.inputValue,
333-
attachments: this.inputAttachments,
334-
});
335-
this.inputValue = '';
336-
337-
if (this._textArea) {
338-
this._textArea.style.height = 'auto';
339-
}
340-
341-
this._host.updateComplete.then(() => {
342-
this._textArea?.focus();
343-
});
344-
};
345-
346329
/**
347330
* Removes an attachment by index.
348331
* Emits 'igcAttachmentChange' event which can be canceled to prevent removal.
349332
* @param index Index of the attachment to remove
350333
*/
351-
public removeAttachment = (index: number): void => {
352-
const allowed = this.emitEvent('igcAttachmentChange', {
353-
detail: this.inputAttachments.filter((_, i) => i !== index),
354-
cancelable: true,
355-
});
356-
if (allowed) {
357-
this.inputAttachments = this.inputAttachments.filter(
358-
(_, i) => i !== index
359-
);
360-
}
361-
};
334+
public removeAttachment = (attachment: IgcChatMessageAttachment): void => {
335+
const attachments = this.inputAttachments.filter(
336+
(each) => each !== attachment
337+
);
362338

363-
/**
364-
* Handles when a suggestion is clicked.
365-
* Adds the suggestion as a new message and focuses the text area.
366-
* @param suggestion The suggestion string clicked
367-
*/
368-
public handleSuggestionClick = (suggestion: string): void => {
369-
this.addMessageWithEvent({ text: suggestion });
370-
if (this.textArea) {
371-
this.textArea.focus();
339+
if (
340+
this.emitEvent('igcAttachmentChange', {
341+
detail: attachments,
342+
cancelable: true,
343+
})
344+
) {
345+
this.inputAttachments = attachments;
372346
}
373347
};
374348

375-
/**
376-
* Updates the internal cache for accepted file types.
377-
* Parses the acceptedFiles string option into extensions, mimeTypes, and wildcard types.
378-
*/
379-
public updateAcceptedTypesCache(): void {
380-
this._acceptedTypesCache = this.options?.acceptedFiles
381-
? parseAcceptedFileTypes(this.options.acceptedFiles)
382-
: null;
383-
}
384-
385349
//#endregion
386350
}

src/components/chat/chat.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ describe('Chat', () => {
264264
></igc-icon-button>
265265
<input
266266
type="file"
267+
tabindex="-1"
267268
aria-label="Upload button"
268269
id="input_attachments"
269270
name="input_attachments"
@@ -609,6 +610,7 @@ describe('Chat', () => {
609610
></igc-icon-button>
610611
<input
611612
type="file"
613+
tabindex="-1"
612614
aria-label="Upload button"
613615
id="input_attachments"
614616
name="input_attachments"

0 commit comments

Comments
 (0)