Skip to content

Commit 73294bb

Browse files
committed
refactor: Starting to clear up internal chat state model
1 parent 6814780 commit 73294bb

File tree

6 files changed

+142
-133
lines changed

6 files changed

+142
-133
lines changed

src/components/chat/chat-input.ts

Lines changed: 79 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
import { consume } from '@lit/context';
22
import { html, LitElement, nothing } from 'lit';
33
import { query, state } from 'lit/decorators.js';
4+
import { cache } from 'lit/directives/cache.js';
45
import { ifDefined } from 'lit/directives/if-defined.js';
5-
import { createRef, ref } from 'lit/directives/ref.js';
66
import { until } from 'lit/directives/until.js';
77
import { addThemingController } from '../../theming/theming-controller.js';
88
import IgcIconButtonComponent from '../button/icon-button.js';
99
import IgcChipComponent from '../chip/chip.js';
1010
import { chatContext, chatUserInputContext } from '../common/context.js';
11-
import { addKeybindings } from '../common/controllers/key-bindings.js';
12-
import { watch } from '../common/decorators/watch.js';
11+
import { enterKey } from '../common/controllers/key-bindings.js';
1312
import { registerComponent } from '../common/definitions/register.js';
1413
import { partMap } from '../common/part-map.js';
15-
import { isEmpty } from '../common/util.js';
14+
import { bindIf, hasFiles, isEmpty } from '../common/util.js';
1615
import IgcIconComponent from '../icon/icon.js';
1716
import IgcTextareaComponent from '../textarea/textarea.js';
1817
import type { ChatState } from './chat-state.js';
@@ -60,7 +59,6 @@ type DefaultInputRenderers = {
6059
*/
6160
export default class IgcChatInputComponent extends LitElement {
6261
public static readonly tagName = 'igc-chat-input';
63-
6462
public static override styles = [styles, shared];
6563

6664
/* blazorSuppress */
@@ -75,36 +73,29 @@ export default class IgcChatInputComponent extends LitElement {
7573
}
7674

7775
@consume({ context: chatContext, subscribe: true })
78-
private readonly _chatState!: ChatState;
76+
private readonly _state!: ChatState;
7977

8078
@consume({ context: chatUserInputContext, subscribe: true })
8179
private readonly _userInputState!: ChatState;
8280

83-
private get _acceptedTypes() {
84-
return this._chatState.acceptedFileTypes;
85-
}
86-
8781
@query(IgcTextareaComponent.tagName)
8882
private readonly _textInputElement!: IgcTextareaComponent;
8983

9084
@query('#input_attachments')
91-
protected readonly _inputAttachmentsButton!: IgcIconButtonComponent;
92-
93-
private readonly _attachmentsButtonInputRef = createRef<HTMLInputElement>();
94-
95-
@watch('acceptedFiles', { waitUntilFirstUpdate: true })
96-
protected acceptedFilesChange(): void {
97-
this._chatState.updateAcceptedTypesCache();
98-
}
85+
protected readonly _fileInput!: HTMLInputElement;
9986

10087
@state()
10188
private _parts = { 'input-container': true, dragging: false };
10289

90+
private get _acceptedTypes() {
91+
return this._state.acceptedFileTypes;
92+
}
93+
10394
private readonly _defaults: Readonly<DefaultInputRenderers> = Object.freeze({
10495
input: () => this._renderTextArea(),
10596
inputActions: () => this.renderActionsArea(),
10697
inputAttachments: (ctx) => this.renderAttachmentsArea(ctx.param),
107-
fileUploadButton: () => this.renderFileUploadButton(),
98+
fileUploadButton: () => this._renderFileUploadButton(),
10899
sendButton: () => this._renderSendButton(),
109100
});
110101

@@ -114,34 +105,36 @@ export default class IgcChatInputComponent extends LitElement {
114105
}
115106

116107
protected override firstUpdated() {
117-
this._chatState.updateAcceptedTypesCache();
118-
this._chatState.textArea = this._textInputElement;
119-
120-
// Use keybindings controller to capture all key events
121-
// Custom skip function that never skips - this captures ALL key events
122-
const keybindings = addKeybindings(this, {
123-
skip: () => false, // Never skip any key events
124-
ref: this._chatState.textAreaRef,
125-
});
126-
127-
// Override the controller's handleEvent to capture all keys
128-
// This is a more direct approach that doesn't require listing specific keys
129-
keybindings.handleEvent = (event: KeyboardEvent) => {
130-
// Call our handler for every key event
131-
this._chatState.handleKeyDown(event);
132-
};
108+
this._state.updateAcceptedTypesCache();
109+
this._state.textArea = this._textInputElement;
133110
}
134111

135112
private _getRenderer(
136113
name: keyof DefaultInputRenderers
137114
): ChatTemplateRenderer<any> {
138-
return this._chatState?.options?.renderers
139-
? (this._chatState.options.renderers[name] ?? this._defaults[name])
115+
return this._state.options?.renderers
116+
? (this._state.options.renderers[name] ?? this._defaults[name])
140117
: this._defaults[name];
141118
}
142119

120+
private _handleKeydown(event: KeyboardEvent): void {
121+
const isSend = event.key === enterKey && !event.shiftKey;
122+
123+
if (isSend) {
124+
event.preventDefault();
125+
this._state.sendMessage();
126+
} else {
127+
// TODO:
128+
this._state.handleKeyDown(event);
129+
}
130+
}
131+
132+
private _handleFileInputClick(): void {
133+
this._fileInput.showPicker();
134+
}
135+
143136
private _handleFocusState(event: FocusEvent): void {
144-
this._chatState.emitEvent(
137+
this._state.emitEvent(
145138
event.type === 'focus' ? 'igcInputFocus' : 'igcInputBlur'
146139
);
147140
}
@@ -152,7 +145,7 @@ export default class IgcChatInputComponent extends LitElement {
152145

153146
const validFiles = getChatAcceptedFiles(event, this._acceptedTypes);
154147
this._parts = { 'input-container': true, dragging: !isEmpty(validFiles) };
155-
this._chatState.emitEvent('igcAttachmentDrag');
148+
this._state.emitEvent('igcAttachmentDrag');
156149
}
157150

158151
private _handleDragOver(event: DragEvent): void {
@@ -185,8 +178,8 @@ export default class IgcChatInputComponent extends LitElement {
185178
this._parts = { 'input-container': true, dragging: false };
186179

187180
const validFiles = getChatAcceptedFiles(event, this._acceptedTypes);
188-
this._chatState.emitEvent('igcAttachmentDrop');
189-
this._chatState.attachFiles(validFiles);
181+
this._state.emitEvent('igcAttachmentDrop');
182+
this._state.attachFiles(validFiles);
190183
this.requestUpdate();
191184
}
192185

@@ -196,20 +189,18 @@ export default class IgcChatInputComponent extends LitElement {
196189
* @param e Input event from the text area
197190
*/
198191
private _handleInput({ detail }: CustomEvent<string>): void {
199-
if (detail === this._chatState.inputValue) return;
192+
if (detail === this._state.inputValue) return;
200193

201-
this._chatState.inputValue = detail;
202-
this._chatState.emitEvent('igcInputChange', { detail: { value: detail } });
194+
this._state.inputValue = detail;
195+
this._state.emitEvent('igcInputChange', { detail: { value: detail } });
203196
}
204197

205198
private _handleFileUpload(event: Event): void {
206199
const input = event.target as HTMLInputElement;
207200

208-
if (!input.files || input.files.length === 0) {
209-
return;
201+
if (hasFiles(input)) {
202+
this._state.attachFiles(Array.from(input.files!));
210203
}
211-
212-
this._chatState.attachFiles(Array.from(input.files));
213204
}
214205
/**
215206
* Default attachments area template used when no custom template is provided.
@@ -222,7 +213,7 @@ export default class IgcChatInputComponent extends LitElement {
222213
<div part="attachment-wrapper" role="listitem">
223214
<igc-chip
224215
removable
225-
@igcRemove=${() => this._chatState?.removeAttachment(index)}
216+
@igcRemove=${() => this._state.removeAttachment(index)}
226217
>
227218
<igc-icon
228219
slot="prefix"
@@ -244,11 +235,12 @@ export default class IgcChatInputComponent extends LitElement {
244235
return html`
245236
<igc-textarea
246237
part="text-input"
247-
placeholder=${ifDefined(this._chatState?.options?.inputPlaceholder)}
238+
placeholder=${ifDefined(this._state.options?.inputPlaceholder)}
248239
resize="auto"
249240
rows="1"
250241
.value=${this._userInputState?.inputValue}
251242
@igcInput=${this._handleInput}
243+
@keydown=${this._handleKeydown}
252244
@focus=${this._handleFocusState}
253245
@blur=${this._handleFocusState}
254246
></igc-textarea>
@@ -260,26 +252,31 @@ export default class IgcChatInputComponent extends LitElement {
260252
* Renders a file input for attaching files.
261253
* @returns TemplateResult containing the file upload button
262254
*/
263-
private renderFileUploadButton() {
264-
if (this._chatState?.options?.disableInputAttachments) return nothing;
265-
return html`
266-
<label for="input_attachments" part="upload-button">
267-
<igc-icon-button
268-
variant="flat"
269-
name="attach_file"
270-
@click=${() => this._attachmentsButtonInputRef?.value?.click()}
271-
></igc-icon-button>
272-
<input
273-
type="file"
274-
id="input_attachments"
275-
name="input_attachments"
276-
${ref(this._attachmentsButtonInputRef)}
277-
multiple
278-
accept=${ifDefined(this._chatState?.options?.acceptedFiles === '' ? undefined : this._chatState?.options?.acceptedFiles)}
279-
@change=${this._handleFileUpload}>
280-
</input>
281-
</label>
282-
`;
255+
private _renderFileUploadButton() {
256+
const accepted = this._state.options?.acceptedFiles;
257+
const attachmentsDisabled = this._state.options?.disableInputAttachments;
258+
259+
return html`${cache(
260+
attachmentsDisabled
261+
? nothing
262+
: html`
263+
<label for="input_attachments" part="upload-button">
264+
<igc-icon-button
265+
variant="flat"
266+
name="attach_file"
267+
@click=${this._handleFileInputClick}
268+
></igc-icon-button>
269+
<input
270+
type="file"
271+
id="input_attachments"
272+
name="input_attachments"
273+
multiple
274+
accept=${bindIf(accepted, accepted)}
275+
@change=${this._handleFileUpload}
276+
/>
277+
</label>
278+
`
279+
)}`;
283280
}
284281

285282
/**
@@ -288,15 +285,17 @@ export default class IgcChatInputComponent extends LitElement {
288285
* @returns TemplateResult containing the send button
289286
*/
290287
private _renderSendButton() {
288+
const enabled =
289+
this._state.hasInputValue || this._state.hasInputAttachments;
290+
291291
return html`
292292
<igc-icon-button
293293
aria-label="Send message"
294294
name="send_message"
295295
variant="contained"
296296
part="send-button"
297-
?disabled=${!this._chatState?.inputValue.trim() &&
298-
this._chatState?.inputAttachments.length === 0}
299-
@click=${this._chatState?.sendMessage}
297+
?disabled=${!enabled}
298+
@click=${this._state.sendMessage}
300299
></igc-icon-button>
301300
`;
302301
}
@@ -305,19 +304,19 @@ export default class IgcChatInputComponent extends LitElement {
305304
return html` ${this._getRenderer('fileUploadButton')({
306305
param: undefined,
307306
defaults: this._defaults,
308-
options: this._chatState.options,
307+
options: this._state.options,
309308
})}
310309
${this._getRenderer('sendButton')({
311310
param: undefined,
312311
defaults: this._defaults,
313-
options: this._chatState.options,
312+
options: this._state.options,
314313
})}`;
315314
}
316315

317316
protected override render() {
318317
const partialCtx = {
319318
defaults: this._defaults,
320-
options: this._chatState.options,
319+
options: this._state.options,
321320
};
322321

323322
return html`
@@ -328,13 +327,13 @@ export default class IgcChatInputComponent extends LitElement {
328327
@dragleave=${this._handleDragLeave}
329328
@drop=${this._handleDrop}
330329
>
331-
${this._chatState.inputAttachments &&
332-
this._chatState.inputAttachments.length > 0
330+
${this._state.inputAttachments &&
331+
this._state.inputAttachments.length > 0
333332
? html` <div part="attachments" role="list" aria-label="Attachments">
334333
${until(
335334
this._getRenderer('inputAttachments')({
336335
...partialCtx,
337-
param: this._chatState.inputAttachments,
336+
param: this._state.inputAttachments,
338337
})
339338
)}
340339
</div>`
@@ -343,7 +342,7 @@ export default class IgcChatInputComponent extends LitElement {
343342
${until(
344343
this._getRenderer('input')({
345344
...partialCtx,
346-
param: this._chatState.inputValue,
345+
param: this._state.inputValue,
347346
})
348347
)}
349348
</div>

0 commit comments

Comments
 (0)