Skip to content

Commit f1da11d

Browse files
committed
fix(*): Moved rendering logic back to components
1 parent 87facf2 commit f1da11d

File tree

5 files changed

+378
-405
lines changed

5 files changed

+378
-405
lines changed

src/components/chat/chat-input.ts

Lines changed: 122 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { consume } from '@lit/context';
22
import { html, LitElement, nothing } from 'lit';
33
import { query, state } from 'lit/decorators.js';
4+
import { ifDefined } from 'lit/directives/if-defined.js';
5+
import { createRef, ref } from 'lit/directives/ref.js';
46
import { addThemingController } from '../../theming/theming-controller.js';
57
import IgcIconButtonComponent from '../button/icon-button.js';
68
import IgcChipComponent from '../chip/chip.js';
@@ -19,6 +21,7 @@ import {
1921
attachmentIcon,
2022
fileDocumentIcon,
2123
fileImageIcon,
24+
type IgcMessageAttachment,
2225
sendButtonIcon,
2326
starIcon,
2427
} from './types.js';
@@ -76,6 +79,8 @@ export default class IgcChatInputComponent extends LitElement {
7679

7780
@query('#input_attachments')
7881
protected _inputAttachmentsButton!: IgcIconButtonComponent;
82+
// private readonly _textAreaRef = createRef<IgcTextareaComponent>();
83+
private readonly _attachmentsButtonInputRef = createRef<HTMLInputElement>();
7984

8085
// private readonly _textAreaRef = createRef<IgcTextareaComponent>();
8186

@@ -187,24 +192,131 @@ export default class IgcChatInputComponent extends LitElement {
187192
this.requestUpdate();
188193
}
189194

195+
/**
196+
* Handles input text changes.
197+
* Updates internal inputValue and emits 'igcInputChange' event.
198+
* @param e Input event from the text area
199+
*/
200+
private handleInput = ({ detail }: CustomEvent<string>) => {
201+
if (detail === this._chatState?.inputValue) return;
202+
203+
this._chatState.inputValue = detail;
204+
this._chatState?.emitEvent('igcInputChange', { detail: { value: detail } });
205+
};
206+
207+
private handleFileUpload = (e: Event) => {
208+
const input = e.target as HTMLInputElement;
209+
if (!input.files || input.files.length === 0) return;
210+
211+
const files = Array.from(input.files);
212+
this._chatState?.attachFiles(files);
213+
};
214+
/**
215+
* Default attachments area template used when no custom template is provided.
216+
* Renders the list of input attachments as chips.
217+
* @returns TemplateResult containing the attachments area
218+
*/
219+
private renderAttachmentsArea(attachments: IgcMessageAttachment[]) {
220+
return html`${attachments?.map(
221+
(attachment, index) => html`
222+
<div part="attachment-wrapper" role="listitem">
223+
<igc-chip
224+
removable
225+
@igcRemove=${() => this._chatState?.removeAttachment(index)}
226+
>
227+
<igc-icon
228+
slot="prefix"
229+
name=${this._chatState?.getIconName(
230+
attachment.file?.type ?? attachment.type
231+
)}
232+
collection="material"
233+
></igc-icon>
234+
<span part="attachment-name">${attachment.name}</span>
235+
</igc-chip>
236+
</div>
237+
`
238+
)} `;
239+
}
240+
241+
/**
242+
* Default text area template used when no custom template is provided.
243+
* Renders a text area for user input.
244+
* @returns TemplateResult containing the text area
245+
*/
246+
private renderTextArea() {
247+
return html` <igc-textarea
248+
part="text-input"
249+
.placeholder=${this._chatState?.options?.inputPlaceholder}
250+
resize="auto"
251+
rows="1"
252+
.value=${this._chatState?.inputValue}
253+
@igcInput=${this.handleInput}
254+
@focus=${() => this._chatState?.emitEvent('igcInputFocus')}
255+
@blur=${() => this._chatState?.emitEvent('igcInputBlur')}
256+
></igc-textarea>`;
257+
}
258+
259+
/**
260+
* Default file upload button template used when no custom template is provided.
261+
* Renders a file input for attaching files.
262+
* @returns TemplateResult containing the file upload button
263+
*/
264+
private renderFileUploadButton() {
265+
if (this._chatState?.options?.disableInputAttachments) return html``;
266+
return html`
267+
<label for="input_attachments" part="upload-button">
268+
<igc-icon-button
269+
variant="flat"
270+
name="attachment"
271+
collection="material"
272+
@click=${() => this._attachmentsButtonInputRef?.value?.click()}
273+
></igc-icon-button>
274+
<input
275+
type="file"
276+
id="input_attachments"
277+
name="input_attachments"
278+
${ref(this._attachmentsButtonInputRef)}
279+
multiple
280+
accept=${ifDefined(this._chatState?.options?.acceptedFiles === '' ? undefined : this._chatState?.options?.acceptedFiles)}
281+
@change=${this.handleFileUpload}>
282+
</input>
283+
</label>
284+
`;
285+
}
286+
287+
/**
288+
* Default send button template used when no custom template is provided.
289+
* Renders a send button that submits the current input value and attachments.
290+
* @returns TemplateResult containing the send button
291+
*/
292+
private renderSendButton() {
293+
return html` <igc-icon-button
294+
aria-label="Send message"
295+
name="send-message"
296+
collection="material"
297+
variant="contained"
298+
part="send-button"
299+
?disabled=${!this._chatState?.inputValue.trim() &&
300+
this._chatState?.inputAttachments.length === 0}
301+
@click=${this._chatState?.sendMessage}
302+
></igc-icon-button>`;
303+
}
304+
305+
private renderActionsArea() {
306+
return html` ${this.renderFileUploadButton()} ${this.renderSendButton()}`;
307+
}
308+
190309
protected override render() {
191-
const templates = this._chatState.mergedTemplates;
192310
return html`
193311
<div part="${this.containerPart}">
194312
${this._chatState.inputAttachments &&
195313
this._chatState.inputAttachments.length > 0
196314
? html` <div part="attachments" role="list" aria-label="Attachments">
197-
${templates?.textAreaAttachmentsTemplate(
198-
this._chatState.inputAttachments
199-
)}
315+
${this.renderAttachmentsArea(this._chatState.inputAttachments)}
200316
</div>`
201317
: nothing}
202-
<div part="input-wrapper">
203-
${templates?.textInputTemplate(this._chatState.inputValue ?? '')}
204-
</div>
205-
<div part="buttons-container">
206-
${templates?.textAreaActionsTemplate()}
207-
</div>
318+
<div part="input-wrapper">${this.renderTextArea()}</div>
319+
<div part="buttons-container">${this.renderActionsArea()}</div>
208320
</div>
209321
`;
210322
}

src/components/chat/chat-message.ts

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { consume } from '@lit/context';
22
import { adoptStyles, html, LitElement, nothing } from 'lit';
33
import { property } from 'lit/decorators.js';
4+
import { createRef, ref } from 'lit/directives/ref.js';
45
import { until } from 'lit/directives/until.js';
56
import { addThemingController } from '../../theming/theming-controller.js';
67
import IgcAvatarComponent from '../avatar/avatar.js';
@@ -68,6 +69,123 @@ export default class IgcChatMessageComponent extends LitElement {
6869
adoptPageStyles(this);
6970
}
7071

72+
private showTooltip(
73+
ev: PointerEvent,
74+
tooltip: IgcTooltipComponent,
75+
text: string
76+
) {
77+
if (!tooltip) return;
78+
tooltip.message = text;
79+
tooltip.hideDelay = 300;
80+
tooltip.show(ev.composedPath()[0] as any);
81+
}
82+
83+
private handleMessageActionClick(event: MouseEvent) {
84+
const reaction = (event.target as HTMLElement).getAttribute('name');
85+
this._chatState?.emitEvent('igcMessageReact', {
86+
detail: { this: this.message, reaction },
87+
});
88+
}
89+
90+
private renderHeader() {
91+
return html``;
92+
}
93+
94+
private renderText() {
95+
return this.message
96+
? html`
97+
${until(this._chatState?.chatRenderer?.renderText(this.message))}
98+
`
99+
: nothing;
100+
}
101+
102+
private renderActions() {
103+
const _sharedTooltipRef = createRef<IgcTooltipComponent>();
104+
const tooltip = _sharedTooltipRef.value as IgcTooltipComponent;
105+
const isLastMessage = this.message === this._chatState?.messages.at(-1);
106+
return this.message?.sender !== this._chatState?.currentUserId &&
107+
this.message?.text.trim() &&
108+
(!isLastMessage || !this._chatState?._isTyping)
109+
? html`<div>
110+
<igc-icon-button
111+
id="copy-response-button"
112+
@pointerenter=${(ev: PointerEvent) =>
113+
this.showTooltip(
114+
ev,
115+
tooltip,
116+
this._chatState?.resourceStrings.reactionCopyResponse!
117+
)}
118+
name="copy-response"
119+
collection="material"
120+
variant="flat"
121+
@click=${(e: MouseEvent) => this.handleMessageActionClick(e)}
122+
></igc-icon-button>
123+
<igc-icon-button
124+
id="good-response-button"
125+
@pointerenter=${(ev: PointerEvent) =>
126+
this.showTooltip(
127+
ev,
128+
tooltip,
129+
this._chatState?.resourceStrings.reactionGoodResponse!
130+
)}
131+
name="good-response"
132+
collection="material"
133+
variant="flat"
134+
@click=${(e: MouseEvent) => this.handleMessageActionClick(e)}
135+
></igc-icon-button>
136+
<igc-icon-button
137+
id="bad-response-button"
138+
@pointerenter=${(ev: PointerEvent) =>
139+
this.showTooltip(
140+
ev,
141+
tooltip,
142+
this._chatState?.resourceStrings.reactionBadResponse!
143+
)}
144+
name="bad-response"
145+
variant="flat"
146+
collection="material"
147+
@click=${(e: MouseEvent) => this.handleMessageActionClick(e)}
148+
></igc-icon-button>
149+
<igc-icon-button
150+
id="redo-button"
151+
@pointerenter=${(ev: PointerEvent) =>
152+
this.showTooltip(
153+
ev,
154+
tooltip,
155+
this._chatState?.resourceStrings.reactionRedo!
156+
)}
157+
name="redo"
158+
variant="flat"
159+
collection="material"
160+
@click=${(e: MouseEvent) => this.handleMessageActionClick(e)}
161+
></igc-icon-button>
162+
<igc-tooltip
163+
id="sharedTooltip"
164+
${ref(_sharedTooltipRef)}
165+
></igc-tooltip>
166+
</div>`
167+
: nothing;
168+
}
169+
170+
/**
171+
* Default message template used when no custom template is provided.
172+
* Renders the message text, sanitized for security.
173+
* @param message The chat message to render
174+
* @returns TemplateResult containing the rendered message
175+
*/
176+
private renderMessage() {
177+
if (!this.message) return html``;
178+
179+
return html`
180+
${this.renderHeader()} ${this.renderText()}
181+
${this.message.attachments?.length
182+
? html`<igc-message-attachments .message=${this.message}>
183+
</igc-message-attachments>`
184+
: nothing}
185+
${this.renderActions()}
186+
`;
187+
}
188+
71189
/**
72190
* Renders the chat message template.
73191
* - Applies 'sent' CSS class if the message sender matches current user.
@@ -80,10 +198,18 @@ export default class IgcChatMessageComponent extends LitElement {
80198
sent: this._chatState?.currentUserId === this.message?.sender,
81199
};
82200

201+
const templates = {
202+
messageTemplate: () => this.renderMessage(),
203+
messageAuthorTemplate: () => this.renderHeader(),
204+
messageActionsTemplate: () => this.renderActions(),
205+
};
206+
83207
return this.message && this._chatState?.chatRenderer
84208
? html`
85209
<div part=${partMap(parts)}>
86-
${until(this._chatState.chatRenderer.render(this.message))}
210+
${until(
211+
this._chatState.chatRenderer.render(this.message, { templates })
212+
)}
87213
</div>
88214
`
89215
: nothing;

src/components/chat/chat-renderer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export class DefaultChatRenderer implements ChatMessageRenderer {
1919
};
2020
}
2121

22-
protected async renderText(message: IgcMessage) {
22+
public async renderText(message: IgcMessage) {
2323
const rendered = await this.baseTextRenderer.render(message);
2424
return rendered;
2525
}
@@ -28,7 +28,7 @@ export class DefaultChatRenderer implements ChatMessageRenderer {
2828
message: IgcMessage,
2929
ctx?: { templates?: Partial<IgcChatTemplates> }
3030
): Promise<unknown> {
31-
const templates = { ...ctx?.templates, ...this.templates };
31+
const templates = { ...this.templates, ...ctx?.templates };
3232
if (!message.id) {
3333
// No caching for messages without an ID
3434
const textContent = await this.baseTextRenderer.render(message);

0 commit comments

Comments
 (0)