Skip to content

Commit 111c0b9

Browse files
committed
feat(chat): expose suggestions prop & add tests
1 parent 5f69221 commit 111c0b9

File tree

5 files changed

+140
-15
lines changed

5 files changed

+140
-15
lines changed

src/components/chat/chat.spec.ts

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { aTimeout, elementUpdated, expect, fixture } from '@open-wc/testing';
22
import { html } from 'lit';
3-
import { type SinonFakeTimers, useFakeTimers } from 'sinon';
3+
import { type SinonFakeTimers, spy, useFakeTimers } from 'sinon';
44
import { defineComponents } from '../common/definitions/defineComponents.js';
55
import { simulateClick, simulateFocus } from '../common/utils.spec.js';
66
import IgcChatComponent from './chat.js';
@@ -129,6 +129,10 @@ describe('Chat', () => {
129129
</div>
130130
<igc-chat-message-list>
131131
</igc-chat-message-list>
132+
<div class="suggestions-container">
133+
<slot name="suggestions" part="suggestions">
134+
</slot>
135+
</div>
132136
<igc-chat-input>
133137
</igc-chat-input>
134138
</div>`
@@ -450,6 +454,38 @@ describe('Chat', () => {
450454
}
451455
});
452456
});
457+
458+
it('should render suggestions', async () => {
459+
chat.options = {
460+
suggestions: ['Suggestion 1', 'Suggestion 2'],
461+
};
462+
await elementUpdated(chat);
463+
464+
const suggestionsContainer = chat.shadowRoot?.querySelector(
465+
'.suggestions-container'
466+
);
467+
468+
expect(suggestionsContainer).dom.to.equal(
469+
`<div class="suggestions-container">
470+
<slot name="suggestions" part="suggestions">
471+
<slot name="suggestion" part="suggestion">
472+
<igc-chip>
473+
<span>
474+
Suggestion 1
475+
</span>
476+
</igc-chip>
477+
</slot>
478+
<slot name="suggestion" part="suggestion">
479+
<igc-chip>
480+
<span>
481+
Suggestion 2
482+
</span>
483+
</igc-chip>
484+
</slot>
485+
</slot>
486+
</div>`
487+
);
488+
});
453489
});
454490

455491
describe('Slots', () => {
@@ -470,6 +506,7 @@ describe('Chat', () => {
470506
it('should slot header prefix', () => {});
471507
it('should slot header title', () => {});
472508
it('should slot header action buttons area', () => {});
509+
it('should slot suggetions area', () => {});
473510
});
474511

475512
describe('Templates', () => {
@@ -597,6 +634,7 @@ describe('Chat', () => {
597634
describe('Interactions', () => {
598635
describe('Click', () => {
599636
it('should update messages properly on send button click', async () => {
637+
const eventSpy = spy(chat, 'emitEvent');
600638
const inputArea = chat.shadowRoot?.querySelector('igc-chat-input');
601639
const sendButton = inputArea?.shadowRoot?.querySelector(
602640
'igc-icon-button[name="send-message"]'
@@ -610,11 +648,49 @@ describe('Chat', () => {
610648
simulateClick(sendButton);
611649
await elementUpdated(chat);
612650
await clock.tickAsync(500);
651+
652+
expect(eventSpy).calledWith('igcMessageCreated');
653+
const eventArgs = eventSpy.getCall(0).args[1]?.detail;
654+
const args =
655+
eventArgs && typeof eventArgs === 'object'
656+
? { ...eventArgs, text: 'Hello!', sender: 'user' }
657+
: { text: 'Hello!', sender: 'user' };
658+
expect(eventArgs).to.deep.equal(args);
613659
expect(chat.messages.length).to.equal(1);
614660
expect(chat.messages[0].text).to.equal('Hello!');
615661
expect(chat.messages[0].sender).to.equal('user');
616662
}
617663
});
664+
665+
it('should update messages properly on suggestion chip click', async () => {
666+
const eventSpy = spy(chat, 'emitEvent');
667+
chat.options = {
668+
suggestions: ['Suggestion 1', 'Suggestion 2'],
669+
};
670+
await elementUpdated(chat);
671+
672+
const suggestionChips = chat.shadowRoot
673+
?.querySelector('.suggestions-container')
674+
?.querySelectorAll('igc-chip');
675+
676+
expect(suggestionChips?.length).to.equal(2);
677+
if (suggestionChips) {
678+
simulateClick(suggestionChips[0]);
679+
await elementUpdated(chat);
680+
681+
expect(eventSpy).calledWith('igcMessageCreated');
682+
const eventArgs = eventSpy.getCall(0).args[1]?.detail;
683+
const args =
684+
eventArgs && typeof eventArgs === 'object'
685+
? { ...eventArgs, text: 'Suggestion 1', sender: 'user' }
686+
: { text: 'Suggestion 1', sender: 'user' };
687+
expect(eventArgs).to.deep.equal(args);
688+
expect(chat.messages.length).to.equal(1);
689+
expect(chat.messages[0].text).to.equal('Suggestion 1');
690+
expect(chat.messages[0].sender).to.equal('user');
691+
}
692+
});
693+
618694
it('should remove attachement on chip remove button click', () => {});
619695
});
620696

@@ -624,6 +700,7 @@ describe('Chat', () => {
624700

625701
describe('Keyboard', () => {
626702
it('should update messages properly on `Enter` keypress when the textarea is focused', async () => {
703+
const eventSpy = spy(chat, 'emitEvent');
627704
const inputArea = chat.shadowRoot?.querySelector('igc-chat-input');
628705
const sendButton = inputArea?.shadowRoot?.querySelector(
629706
'igc-icon-button[name="send-message"]'
@@ -644,6 +721,14 @@ describe('Chat', () => {
644721
);
645722
await elementUpdated(chat);
646723
await clock.tickAsync(500);
724+
725+
expect(eventSpy).calledWith('igcMessageCreated');
726+
const eventArgs = eventSpy.getCall(0).args[1]?.detail;
727+
const args =
728+
eventArgs && typeof eventArgs === 'object'
729+
? { ...eventArgs, text: 'Hello!', sender: 'user' }
730+
: { text: 'Hello!', sender: 'user' };
731+
expect(eventArgs).to.deep.equal(args);
647732
expect(chat.messages.length).to.equal(1);
648733
expect(chat.messages[0].text).to.equal('Hello!');
649734
expect(chat.messages[0].sender).to.equal('user');
@@ -653,7 +738,6 @@ describe('Chat', () => {
653738
});
654739

655740
describe('Events', () => {
656-
it('emits igcMessageCreated', async () => {});
657741
it('emits igcAttachmentClick', async () => {});
658742
it('emits igcAttachmentChange', async () => {});
659743
it('emits igcTypingChange', async () => {});

src/components/chat/chat.ts

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ContextProvider } from '@lit/context';
2-
import { LitElement, html } from 'lit';
2+
import { html, LitElement } from 'lit';
33
import { property } from 'lit/decorators.js';
44
import IgcButtonComponent from '../button/button.js';
55
import { chatContext } from '../common/context.js';
@@ -98,23 +98,32 @@ export default class IgcChatComponent extends EventEmitterMixin<
9898

9999
if (!text.trim() && attachments.length === 0) return;
100100

101-
const newMessage: IgcMessage = {
102-
id: Date.now().toString(),
103-
text,
104-
sender: 'user',
105-
timestamp: new Date(),
106-
attachments,
107-
};
108-
109-
this.messages = [...this.messages, newMessage];
110-
this.emitEvent('igcMessageCreated', { detail: newMessage });
101+
this.addMessage({ text, attachments });
111102
}
112103

113104
private handleAttachmentClick(e: CustomEvent) {
114105
const attachmentArgs = e.detail.attachment;
115106
this.emitEvent('igcAttachmentClick', { detail: attachmentArgs });
116107
}
117108

109+
private addMessage(message: {
110+
id?: string;
111+
text: string;
112+
sender?: string;
113+
timestamp?: Date;
114+
attachments?: IgcMessageAttachment[];
115+
}) {
116+
const newMessage: IgcMessage = {
117+
id: message.id ?? Date.now().toString(),
118+
text: message.text,
119+
sender: message.sender ?? 'user',
120+
timestamp: message.timestamp ?? new Date(),
121+
attachments: message.attachments || [],
122+
};
123+
this.messages = [...this.messages, newMessage];
124+
this.emitEvent('igcMessageCreated', { detail: newMessage });
125+
}
126+
118127
protected override firstUpdated() {
119128
this._context.setValue(this, true);
120129
}
@@ -132,6 +141,21 @@ export default class IgcChatComponent extends EventEmitterMixin<
132141
</slot>
133142
</div>
134143
<igc-chat-message-list> </igc-chat-message-list>
144+
<div class="suggestions-container">
145+
<slot name="suggestions" part="suggestions">
146+
${this.options?.suggestions?.map(
147+
(suggestion) => html`
148+
<slot name="suggestion" part="suggestion">
149+
<igc-chip
150+
@click=${() => this.addMessage({ text: suggestion })}
151+
>
152+
<span>${suggestion}</span>
153+
</igc-chip>
154+
</slot>
155+
`
156+
)}
157+
</slot>
158+
</div>
135159
<igc-chat-input
136160
@message-created=${this.handleSendMessage}
137161
></igc-chat-input>

src/components/chat/themes/chat.base.scss

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,16 @@
7575

7676
.action-button:hover {
7777
background-color: #E5E5EA;
78+
}
79+
80+
.suggestions-container {
81+
display: flex;
82+
justify-content: end;
83+
84+
igc-chip::part(base) {
85+
background-color: transparent;
86+
border: 1px solid var(--ig-primary-500);
87+
color: var(--ig-primary-500);
88+
}
89+
7890
}

src/components/chat/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export type IgcChatOptions = {
3636
*/
3737
acceptedFiles?: string;
3838
headerText?: string;
39+
suggestions?: string[];
3940
templates?: IgcChatTemplates;
4041
};
4142

stories/chat.stories.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,13 @@ const messageActionsTemplate = (msg: any) => {
101101
></igc-icon-button>
102102
</div>
103103
`
104-
: ''
105-
: '';
104+
: html``
105+
: html``;
106106
};
107107

108108
const ai_chat_options = {
109109
headerText: 'Chat',
110+
suggestions: ['Hello', 'Hi', 'Generate an image of a pig!'],
110111
templates: {
111112
messageActionsTemplate: messageActionsTemplate,
112113
},
@@ -413,6 +414,8 @@ async function handleAIMessageSend(e: CustomEvent) {
413414
return;
414415
}
415416

417+
chat.options = { ...ai_chat_options, suggestions: [] };
418+
416419
let response: any;
417420
let responseText = '';
418421
const attachments: IgcMessageAttachment[] = [];
@@ -495,6 +498,7 @@ async function handleAIMessageSend(e: CustomEvent) {
495498
};
496499
chat.messages = [...chat.messages];
497500
}
501+
chat.options = { ...ai_chat_options, suggestions: ['Thank you!'] };
498502
}
499503
}
500504

0 commit comments

Comments
 (0)