@coveo/quantic 3.41.0 → 3.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/docs/out/quantic-docs.json +9 -0
  2. package/force-app/main/default/labels/CustomLabels.labels-meta.xml +14 -0
  3. package/force-app/main/default/lwc/quanticGeneratedAnswer/__tests__/quanticGeneratedAnswer.test.js +271 -22
  4. package/force-app/main/default/lwc/quanticGeneratedAnswer/quanticGeneratedAnswer.js +92 -31
  5. package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/conversation.css +7 -0
  6. package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/conversation.html +63 -0
  7. package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.css +0 -11
  8. package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.html +2 -2
  9. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/__tests__/quanticGeneratedAnswerBody.test.js +39 -1
  10. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/quanticGeneratedAnswerBody.js +8 -4
  11. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/templates/answer.html +30 -24
  12. package/force-app/main/default/lwc/quanticGeneratedAnswerCopyToClipboard/__tests__/quanticGeneratedAnswerCopyToClipboard.test.js +5 -1
  13. package/force-app/main/default/lwc/quanticGeneratedAnswerCopyToClipboard/quanticGeneratedAnswerCopyToClipboard.js +9 -2
  14. package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/__tests__/quanticGeneratedAnswerFollowUpInput.test.js +109 -55
  15. package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/quanticGeneratedAnswerFollowUpInput.css +59 -11
  16. package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/quanticGeneratedAnswerFollowUpInput.html +14 -13
  17. package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/quanticGeneratedAnswerFollowUpInput.js +40 -2
  18. package/force-app/main/default/lwc/quanticSourceCitations/__tests__/quanticSourceCitations.test.js +22 -2
  19. package/force-app/main/default/lwc/quanticSourceCitations/quanticSourceCitations.js +7 -0
  20. package/force-app/main/default/staticresources/coveoheadless/case-assist/headless.js +15 -15
  21. package/force-app/main/default/staticresources/coveoheadless/definitions/commerce.index.d.ts +1 -1
  22. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/commerce/core/sub-controller/headless-sub-controller.d.ts +3 -1
  23. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/commerce/recent-queries-list/headless-recent-queries-list.d.ts +13 -2
  24. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/commerce/search/did-you-mean/headless-did-you-mean.d.ts +10 -1
  25. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/generated-answer/headless-generated-answer.d.ts +2 -2
  26. package/force-app/main/default/staticresources/coveoheadless/definitions/features/follow-up-answers/follow-up-answers-actions.d.ts +32 -1
  27. package/force-app/main/default/staticresources/coveoheadless/definitions/features/generated-answer/generated-answer-actions.d.ts +26 -1
  28. package/force-app/main/default/staticresources/coveoheadless/definitions/features/generated-answer/generated-answer-state.d.ts +19 -0
  29. package/force-app/main/default/staticresources/coveoheadless/definitions/index.d.ts +1 -1
  30. package/force-app/main/default/staticresources/coveoheadless/definitions/ssr/commerce/controllers/did-you-mean/headless-did-you-mean.ssr.d.ts +4 -3
  31. package/force-app/main/default/staticresources/coveoheadless/definitions/ssr-commerce-next.index.d.ts +1 -1
  32. package/force-app/main/default/staticresources/coveoheadless/definitions/ssr-commerce.index.d.ts +1 -1
  33. package/force-app/main/default/staticresources/coveoheadless/definitions/ssr-next/commerce/controllers/did-you-mean/headless-did-you-mean.ssr.d.ts +4 -3
  34. package/force-app/main/default/staticresources/coveoheadless/headless.js +18 -18
  35. package/force-app/main/default/staticresources/coveoheadless/insight/headless.js +17 -17
  36. package/force-app/main/default/staticresources/coveoheadless/recommendation/headless.js +15 -15
  37. package/package.json +2 -2
@@ -0,0 +1,63 @@
1
+ <template>
2
+ <template lwc:if={shouldDisplayGeneratedAnswer}>
3
+ <div
4
+ data-testid="generated-answer__card"
5
+ class="slds-box slds-p-around_none"
6
+ >
7
+ <header
8
+ data-testid="generated-answer__header"
9
+ class={generatedAnswerHeaderClass}
10
+ >
11
+ <div
12
+ class="generated-answer__header-title slds-grid slds-grid_vertical-align-center"
13
+ >
14
+ <lightning-icon
15
+ icon-name="utility:sparkles"
16
+ size="x-small"
17
+ alternative-text={labels.generatedAnswerForYou}
18
+ class="slds-current-color slds-var-m-right_x-small"
19
+ ></lightning-icon>
20
+ <span
21
+ data-testid="generated-answer__header-title"
22
+ class="slds-text-title_bold slds-p-vertical_x-small"
23
+ >{labels.generatedAnswerForYou}
24
+ </span>
25
+ </div>
26
+ <template lwc:if={withToggle}>
27
+ <c-quantic-generated-answer-toggle
28
+ is-generated-answer-visible={isVisible}
29
+ ></c-quantic-generated-answer-toggle>
30
+ </template>
31
+ </header>
32
+ <template lwc:if={isVisible}>
33
+ <div
34
+ class="slds-p-horizontal_large slds-p-top_medium generated-answer__content--scrollable"
35
+ >
36
+ <c-quantic-generated-answer-thread
37
+ engine-id={engineId}
38
+ onquantic__generatedanswerlike={handleLike}
39
+ onquantic__generatedanswerdislike={handleDislike}
40
+ onquantic__citationhover={handleCitationHover}
41
+ generated-answers={allGeneratedAnswers}
42
+ disable-citation-anchoring={disableCitationAnchoring}
43
+ >
44
+ </c-quantic-generated-answer-thread>
45
+ </div>
46
+ <div class="slds-p-horizontal_large slds-p-vertical_medium">
47
+ <c-quantic-generated-answer-follow-up-input
48
+ onquantic__submitfollowup={handleFollowUpSubmit}
49
+ submit-button-disabled={isAnswerGenerationOngoing}
50
+ ></c-quantic-generated-answer-follow-up-input>
51
+ <footer
52
+ data-testid="generated-answer__footer"
53
+ class="generated-answer__disclaimer slds-grid slds-grid_vertical-align-center slds-grid_align-end slds-text-color_weak slds-text-body_small slds-var-m-top_xx-small"
54
+ >
55
+ <div data-testid="generated-answer__disclaimer">
56
+ <slot name="disclaimer">{labels.rgaDisclaimer}</slot>
57
+ </div>
58
+ </footer>
59
+ </div>
60
+ </template>
61
+ </div>
62
+ </template>
63
+ </template>
@@ -9,11 +9,6 @@
9
9
  word-wrap: break-word;
10
10
  }
11
11
 
12
- .generated-answer__content--scrollable {
13
- overflow-y: auto;
14
- height: var(--quantic-generated-answer-content-fixed-height, 50vh);
15
- }
16
-
17
12
  .generated-answer__footer {
18
13
  gap: 1rem;
19
14
  }
@@ -59,9 +54,3 @@
59
54
  opacity: 0.2;
60
55
  visibility: hidden;
61
56
  }
62
-
63
- @keyframes cursor-blink {
64
- 100% {
65
- visibility: hidden;
66
- }
67
- }
@@ -27,7 +27,7 @@
27
27
  <template lwc:if={isVisible}>
28
28
  <section
29
29
  data-testid="generated-answer__body"
30
- class={contentSectionClass}
30
+ class="generated-answer__content slds-p-top_medium slds-p-horizontal_large"
31
31
  >
32
32
  <div
33
33
  data-testid="generated-answer__answer"
@@ -50,7 +50,7 @@
50
50
  engine-id={engineId}
51
51
  data-testid="generated-answer__citations"
52
52
  citations={citations}
53
- citation-hover-handler={handleCitationHover}
53
+ citation-hover-handler={logCitationHover}
54
54
  disable-citation-anchoring={disableCitationAnchoring}
55
55
  ></c-quantic-source-citations>
56
56
  </div>
@@ -67,6 +67,7 @@ const selectors = {
67
67
  error: '[data-testid="generated-answer-body__error"]',
68
68
  noAnswer: '[data-testid="generated-answer-body__no-answer-message"]',
69
69
  content: 'c-quantic-generated-answer-content',
70
+ streamOfThought: 'c-quantic-generated-answer-stream-of-thought',
70
71
  };
71
72
 
72
73
  const createTestComponent = buildCreateTestComponent(
@@ -80,7 +81,7 @@ describe('c-quantic-generated-answer-body', () => {
80
81
  cleanup();
81
82
  });
82
83
 
83
- it('should pass answer and answerContentFormat to the QuanticGeneratedAnswerContent component', async () => {
84
+ it('should pass the answer, answerContentFormat, engineId, and answerId properties to the QuanticGeneratedAnswerContent component', async () => {
84
85
  const element = createTestComponent();
85
86
  await flushPromises();
86
87
 
@@ -90,6 +91,42 @@ describe('c-quantic-generated-answer-body', () => {
90
91
  expect(content.answerContentFormat).toBe(
91
92
  defaultOptions.generatedAnswer.answerContentFormat
92
93
  );
94
+ expect(content.engineId).toBe(defaultOptions.engineId);
95
+ expect(content.answerId).toBe(defaultOptions.generatedAnswer.answerId);
96
+ });
97
+
98
+ it('should pass agentSteps and isStreaming to the QuanticGeneratedAnswerStreamOfThought component', async () => {
99
+ const generationSteps = [{id: 'step-1'}, {id: 'step-2'}];
100
+ const element = createTestComponent({
101
+ ...defaultOptions,
102
+ generatedAnswer: {
103
+ ...defaultOptions.generatedAnswer,
104
+ // @ts-ignore
105
+ generationSteps,
106
+ isStreaming: true,
107
+ },
108
+ });
109
+ await flushPromises();
110
+
111
+ const streamOfThought = element.shadowRoot.querySelector(
112
+ selectors.streamOfThought
113
+ );
114
+
115
+ expect(streamOfThought).not.toBeNull();
116
+ expect(streamOfThought.agentSteps).toEqual(generationSteps);
117
+ expect(streamOfThought.isStreaming).toBe(true);
118
+ });
119
+
120
+ it('should pass an empty agentSteps array to QuanticGeneratedAnswerStreamOfThought when generationSteps is undefined', async () => {
121
+ const element = createTestComponent();
122
+ await flushPromises();
123
+
124
+ const streamOfThought = element.shadowRoot.querySelector(
125
+ selectors.streamOfThought
126
+ );
127
+
128
+ expect(streamOfThought).not.toBeNull();
129
+ expect(streamOfThought.agentSteps).toEqual([]);
93
130
  });
94
131
 
95
132
  it('should send the answerId within event details when dispatching the #quantic__generatedanswerlike event', async () => {
@@ -290,6 +327,7 @@ describe('c-quantic-generated-answer-body', () => {
290
327
  const citations = element.shadowRoot.querySelector(selectors.citations);
291
328
 
292
329
  expect(citations).not.toBeNull();
330
+ expect(citations.answerId).toBe(defaultOptions.generatedAnswer.answerId);
293
331
  });
294
332
 
295
333
  it('should not display citations when citations are empty', async () => {
@@ -1,8 +1,8 @@
1
1
  import couldNotGenerateAnAnswer from '@salesforce/label/c.quantic_CouldNotGenerateAnAnswer';
2
2
  import generatedAnswerErrorTurnLimitReached from '@salesforce/label/c.quantic_GeneratedAnswerErrorTurnLimitReached';
3
3
  import genericErrorTitle from '@salesforce/label/c.quantic_GenericErrorTitle';
4
- import thisAnswerWasHelpful from '@salesforce/label/c.quantic_ThisAnswerWasHelpful';
5
- import thisAnswerWasNotHelpful from '@salesforce/label/c.quantic_ThisAnswerWasNotHelpful';
4
+ import like from '@salesforce/label/c.quantic_Like';
5
+ import dislike from '@salesforce/label/c.quantic_Dislike';
6
6
  import {LightningElement, api} from 'lwc';
7
7
  // @ts-ignore
8
8
  import answerTemplate from './templates/answer.html';
@@ -49,8 +49,8 @@ export default class QuanticGeneratedAnswerBody extends LightningElement {
49
49
  couldNotGenerateAnAnswer,
50
50
  generatedAnswerErrorTurnLimitReached,
51
51
  genericErrorTitle,
52
- thisAnswerWasHelpful,
53
- thisAnswerWasNotHelpful,
52
+ like,
53
+ dislike,
54
54
  };
55
55
 
56
56
  get answer() {
@@ -73,6 +73,10 @@ export default class QuanticGeneratedAnswerBody extends LightningElement {
73
73
  return !!this.generatedAnswer?.isStreaming;
74
74
  }
75
75
 
76
+ get agentSteps() {
77
+ return this.generatedAnswer?.generationSteps || [];
78
+ }
79
+
76
80
  get hasError() {
77
81
  return !!this.generatedAnswer?.error?.code;
78
82
  }
@@ -1,9 +1,14 @@
1
1
  <template>
2
- <section
3
- data-testid="generated-answer-body"
4
- class="generated-answer__body"
5
- >
2
+ <section data-testid="generated-answer-body" class="generated-answer__body">
3
+ <div class="slds-var-m-bottom_small">
4
+ <c-quantic-generated-answer-stream-of-thought
5
+ agent-steps={agentSteps}
6
+ is-streaming={isStreaming}
7
+ ></c-quantic-generated-answer-stream-of-thought>
8
+ </div>
6
9
  <c-quantic-generated-answer-content
10
+ engine-id={engineId}
11
+ answer-id={answerId}
7
12
  answer-content-format={answerContentFormat}
8
13
  answer={answer}
9
14
  is-streaming={isStreaming}
@@ -18,6 +23,7 @@
18
23
  citations={citations}
19
24
  citation-hover-handler={handleCitationHover}
20
25
  disable-citation-anchoring={disableCitationAnchoring}
26
+ answer-id={answerId}
21
27
  ></c-quantic-source-citations>
22
28
  </div>
23
29
  </template>
@@ -26,26 +32,26 @@
26
32
  data-testid="generated-answer-body__actions"
27
33
  class="slds-size_1-of-1 slds-grid slds-grid_vertical-align-center slds-var-m-top_x-small slds-grid_align-start"
28
34
  >
29
- <c-quantic-feedback
30
- state={computedFeedbackState}
31
- onquantic__like={handleLike}
32
- onquantic__dislike={handleDislike}
33
- like-icon-name="utility:like"
34
- like-label={labels.thisAnswerWasHelpful}
35
- dislike-icon-name="utility:dislike"
36
- dislike-label={labels.thisAnswerWasNotHelpful}
37
- size="x-small"
38
- question=""
39
- hide-explain-why-button
40
- hide-labels
41
- ></c-quantic-feedback>
42
- <c-quantic-generated-answer-copy-to-clipboard
43
- data-testid="generated-answer-body__copy-to-clipboard"
44
- answer={answer}
45
- answer-id={answerId}
46
- size="x-small"
47
- class="slds-var-m-horizontal_xx-small"
48
- ></c-quantic-generated-answer-copy-to-clipboard>
35
+ <c-quantic-feedback
36
+ state={computedFeedbackState}
37
+ onquantic__like={handleLike}
38
+ onquantic__dislike={handleDislike}
39
+ like-icon-name="utility:like"
40
+ like-label={labels.like}
41
+ dislike-icon-name="utility:dislike"
42
+ dislike-label={labels.dislike}
43
+ size="x-small"
44
+ question=""
45
+ hide-explain-why-button
46
+ hide-labels
47
+ ></c-quantic-feedback>
48
+ <c-quantic-generated-answer-copy-to-clipboard
49
+ data-testid="generated-answer-body__copy-to-clipboard"
50
+ answer={answer}
51
+ answer-id={answerId}
52
+ size="x-small"
53
+ class="slds-var-m-horizontal_xx-small"
54
+ ></c-quantic-generated-answer-copy-to-clipboard>
49
55
  </div>
50
56
  </template>
51
57
  </div>
@@ -34,6 +34,7 @@ const selectors = {
34
34
 
35
35
  const defaultOptions = {
36
36
  answer: 'Example generated answer',
37
+ answerId: 'example-answer-id',
37
38
  size: defaultSize,
38
39
  };
39
40
 
@@ -109,7 +110,7 @@ describe('c-quantic-generated-answer-copy-to-clipboard', () => {
109
110
  expect(copyButton.iconSize).toBe(defaultSize);
110
111
  });
111
112
 
112
- it('should copy the answer and dispatch the #quantic__generatedanswercopy event', async () => {
113
+ it('should copy the answer and dispatch the #quantic__generatedanswercopy event with the answerId', async () => {
113
114
  const element = createTestComponent();
114
115
  const handler = jest.fn();
115
116
  element.addEventListener('quantic__generatedanswercopy', handler);
@@ -121,5 +122,8 @@ describe('c-quantic-generated-answer-copy-to-clipboard', () => {
121
122
 
122
123
  expect(copyToClipboard).toHaveBeenCalledWith(defaultOptions.answer);
123
124
  expect(handler).toHaveBeenCalledTimes(1);
125
+ expect(handler.mock.calls[0][0].detail).toEqual({
126
+ answerId: defaultOptions.answerId,
127
+ });
124
128
  });
125
129
  });
@@ -15,6 +15,12 @@ export default class QuanticGeneratedAnswerCopyToClipboard extends LightningElem
15
15
  * @type {string}
16
16
  */
17
17
  @api answer = '';
18
+ /**
19
+ * The unique identifier of the generated answer.
20
+ * @api
21
+ * @type {string}
22
+ */
23
+ @api answerId;
18
24
 
19
25
  /**
20
26
  * The size of the copy icon.
@@ -41,11 +47,12 @@ export default class QuanticGeneratedAnswerCopyToClipboard extends LightningElem
41
47
  isSelected = false;
42
48
  tooltip = this.labels.copy;
43
49
 
44
- dispatchCopyToClipboardEvent(option) {
50
+ dispatchCopyToClipboardEvent() {
45
51
  this.dispatchEvent(
46
52
  new CustomEvent('quantic__generatedanswercopy', {
47
- detail: option,
53
+ detail: {answerId: this.answerId},
48
54
  bubbles: true,
55
+ composed: true,
49
56
  })
50
57
  );
51
58
  }
@@ -12,15 +12,18 @@ jest.mock(
12
12
  {virtual: true}
13
13
  );
14
14
  jest.mock('c/quanticUtils', () => ({
15
- keys: {ENTER: 'Enter'},
15
+ keys: {ENTER: 'Enter', ESC: 'Escape'},
16
16
  }));
17
17
 
18
18
  const selectors = {
19
- input: 'lightning-input',
19
+ textarea: 'textarea',
20
20
  submitButton: 'lightning-button-icon',
21
- inputContainer: '.follow-up-input__input-container',
21
+ container: '.follow-up-input__container',
22
+ expander: '.follow-up-input__expander',
22
23
  };
23
24
 
25
+ const expandedClass = 'follow-up-input__expander--expanded';
26
+
24
27
  const createTestComponent = buildCreateTestComponent(
25
28
  QuanticGeneratedAnswerFollowUpInput,
26
29
  'c-quantic-generated-answer-follow-up-input'
@@ -31,17 +34,14 @@ describe('c-quantic-generated-answer-follow-up-input', () => {
31
34
  cleanup();
32
35
  });
33
36
 
34
- it('should render the input and submit button', async () => {
37
+ it('should render the textarea and submit button', async () => {
35
38
  const element = createTestComponent();
36
39
  await flushPromises();
37
40
 
38
- const input = element.shadowRoot.querySelector(selectors.input);
39
- const submitButton = element.shadowRoot.querySelector(
40
- selectors.submitButton
41
- );
42
-
43
- expect(input).not.toBeNull();
44
- expect(submitButton).not.toBeNull();
41
+ expect(element.shadowRoot.querySelector(selectors.textarea)).not.toBeNull();
42
+ expect(
43
+ element.shadowRoot.querySelector(selectors.submitButton)
44
+ ).not.toBeNull();
45
45
  });
46
46
 
47
47
  describe('submitting a follow up', () => {
@@ -52,9 +52,9 @@ describe('c-quantic-generated-answer-follow-up-input', () => {
52
52
  const handler = jest.fn();
53
53
  element.addEventListener('quantic__submitfollowup', handler);
54
54
 
55
- const input = element.shadowRoot.querySelector(selectors.input);
56
- input.value = 'follow up question';
57
- input.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
55
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
56
+ textarea.value = 'follow up question';
57
+ textarea.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
58
58
 
59
59
  expect(handler).toHaveBeenCalledTimes(1);
60
60
  expect(handler.mock.calls[0][0].detail.value).toBe('follow up question');
@@ -67,29 +67,40 @@ describe('c-quantic-generated-answer-follow-up-input', () => {
67
67
  const handler = jest.fn();
68
68
  element.addEventListener('quantic__submitfollowup', handler);
69
69
 
70
- const input = element.shadowRoot.querySelector(selectors.input);
71
- input.value = 'another question';
70
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
71
+ textarea.value = 'another question';
72
72
 
73
- const submitButton = element.shadowRoot.querySelector(
74
- selectors.submitButton
75
- );
76
- submitButton.click();
73
+ element.shadowRoot.querySelector(selectors.submitButton).click();
77
74
 
78
75
  expect(handler).toHaveBeenCalledTimes(1);
79
76
  expect(handler.mock.calls[0][0].detail.value).toBe('another question');
80
77
  });
81
78
 
82
- it('should clear the input and blur it after a successful submit', async () => {
79
+ it('should clear the textarea after a successful submit', async () => {
83
80
  const element = createTestComponent();
84
81
  await flushPromises();
85
82
 
86
- const input = element.shadowRoot.querySelector(selectors.input);
87
- input.value = 'a question';
88
- input.blur = jest.fn();
83
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
84
+ textarea.value = 'a question';
85
+ textarea.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
89
86
 
90
- input.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
87
+ expect(textarea.value).toBe('');
88
+ });
89
+
90
+ it('should collapse after a successful submit', async () => {
91
+ const element = createTestComponent();
92
+ await flushPromises();
93
+
94
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
95
+ textarea.dispatchEvent(new CustomEvent('focus'));
96
+ await flushPromises();
97
+
98
+ textarea.value = 'a question';
99
+ textarea.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
100
+ await flushPromises();
91
101
 
92
- expect(input.value).toBe('');
102
+ const expander = element.shadowRoot.querySelector(selectors.expander);
103
+ expect(expander.classList).not.toContain(expandedClass);
93
104
  });
94
105
  });
95
106
 
@@ -101,9 +112,9 @@ describe('c-quantic-generated-answer-follow-up-input', () => {
101
112
  const handler = jest.fn();
102
113
  element.addEventListener('quantic__submitfollowup', handler);
103
114
 
104
- const input = element.shadowRoot.querySelector(selectors.input);
105
- input.value = 'a question';
106
- input.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
115
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
116
+ textarea.value = 'a question';
117
+ textarea.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
107
118
 
108
119
  expect(handler).not.toHaveBeenCalled();
109
120
  });
@@ -115,9 +126,9 @@ describe('c-quantic-generated-answer-follow-up-input', () => {
115
126
  const handler = jest.fn();
116
127
  element.addEventListener('quantic__submitfollowup', handler);
117
128
 
118
- const input = element.shadowRoot.querySelector(selectors.input);
119
- input.value = ' ';
120
- input.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
129
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
130
+ textarea.value = ' ';
131
+ textarea.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
121
132
 
122
133
  expect(handler).not.toHaveBeenCalled();
123
134
  });
@@ -129,52 +140,95 @@ describe('c-quantic-generated-answer-follow-up-input', () => {
129
140
  const handler = jest.fn();
130
141
  element.addEventListener('quantic__submitfollowup', handler);
131
142
 
132
- const input = element.shadowRoot.querySelector(selectors.input);
133
- input.value = '';
134
-
135
- const submitButton = element.shadowRoot.querySelector(
136
- selectors.submitButton
137
- );
138
- submitButton.click();
143
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
144
+ textarea.value = '';
145
+ element.shadowRoot.querySelector(selectors.submitButton).click();
139
146
 
140
147
  expect(handler).not.toHaveBeenCalled();
141
148
  });
142
149
  });
143
150
 
144
- describe('focus and blur CSS class toggling', () => {
145
- it('should add the focused class when the input is focused', async () => {
151
+ describe('expandable input behaviour', () => {
152
+ it('should expand and add focused class on focus', async () => {
146
153
  const element = createTestComponent();
147
154
  await flushPromises();
148
155
 
149
- const input = element.shadowRoot.querySelector(selectors.input);
150
- input.dispatchEvent(new CustomEvent('focus'));
156
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
157
+ textarea.dispatchEvent(new CustomEvent('focus'));
151
158
  await flushPromises();
152
159
 
153
- const container = element.shadowRoot.querySelector(
154
- selectors.inputContainer
155
- );
160
+ const expander = element.shadowRoot.querySelector(selectors.expander);
161
+ const container = element.shadowRoot.querySelector(selectors.container);
162
+ expect(expander.classList).toContain(expandedClass);
156
163
  expect(container.classList).toContain(
157
- 'follow-up-input__input-container--focused'
164
+ 'follow-up-input__container--focused'
158
165
  );
159
166
  });
160
167
 
161
- it('should remove the focused class when the input is blurred', async () => {
168
+ it('should collapse and remove focused class on blur', async () => {
162
169
  const element = createTestComponent();
163
170
  await flushPromises();
164
171
 
165
- const input = element.shadowRoot.querySelector(selectors.input);
166
- input.dispatchEvent(new CustomEvent('focus'));
172
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
173
+ textarea.dispatchEvent(new CustomEvent('focus'));
167
174
  await flushPromises();
168
175
 
169
- input.dispatchEvent(new CustomEvent('blur'));
176
+ textarea.dispatchEvent(new CustomEvent('blur'));
170
177
  await flushPromises();
171
178
 
172
- const container = element.shadowRoot.querySelector(
173
- selectors.inputContainer
174
- );
179
+ const expander = element.shadowRoot.querySelector(selectors.expander);
180
+ const container = element.shadowRoot.querySelector(selectors.container);
181
+ expect(expander.classList).not.toContain(expandedClass);
175
182
  expect(container.classList).not.toContain(
176
- 'follow-up-input__input-container--focused'
183
+ 'follow-up-input__container--focused'
177
184
  );
178
185
  });
186
+
187
+ it('should sync replica text on input', async () => {
188
+ const element = createTestComponent();
189
+ await flushPromises();
190
+
191
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
192
+ const expander = element.shadowRoot.querySelector(selectors.expander);
193
+
194
+ textarea.value = 'multi\nline\ntext';
195
+ textarea.dispatchEvent(new CustomEvent('input'));
196
+ await flushPromises();
197
+
198
+ expect(expander.dataset.replicatedValue).toBe('multi\nline\ntext');
199
+ });
200
+
201
+ it('should clear replica text after submit', async () => {
202
+ const element = createTestComponent();
203
+ await flushPromises();
204
+
205
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
206
+ const expander = element.shadowRoot.querySelector(selectors.expander);
207
+
208
+ textarea.value = 'some text';
209
+ textarea.dispatchEvent(new CustomEvent('input'));
210
+ await flushPromises();
211
+ expect(expander.dataset.replicatedValue).toBe('some text');
212
+
213
+ textarea.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
214
+ await flushPromises();
215
+
216
+ expect(expander.dataset.replicatedValue).toBeUndefined();
217
+ });
218
+
219
+ it('should collapse the textarea when the Escape key is pressed', async () => {
220
+ const element = createTestComponent();
221
+ await flushPromises();
222
+
223
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
224
+ textarea.blur = jest.fn();
225
+ textarea.dispatchEvent(new CustomEvent('focus'));
226
+ await flushPromises();
227
+
228
+ textarea.dispatchEvent(new KeyboardEvent('keydown', {key: 'Escape'}));
229
+ await flushPromises();
230
+
231
+ expect(textarea.blur).toHaveBeenCalled();
232
+ });
179
233
  });
180
234
  });
@@ -1,17 +1,65 @@
1
- :host {
2
- --slds-c-input-shadow: none;
1
+ .follow-up-input__container {
2
+ min-height: calc(2.5rem + 2px);
3
3
  }
4
4
 
5
- .follow-up-input__input {
5
+ .follow-up-input__container--focused {
6
+ border-color: var(--lwc-brandAccessible, #0176d3);
7
+ }
8
+
9
+ .follow-up-input__expander {
10
+ display: grid;
11
+ overflow: hidden;
12
+ margin-right: 2.5rem;
13
+ }
14
+
15
+ .follow-up-input__expander::after {
16
+ content: attr(data-replicated-value) ' ';
17
+ visibility: hidden;
18
+ }
19
+
20
+ .follow-up-input__expander::after,
21
+ .follow-up-input__textarea {
22
+ grid-area: 1 / 1 / 2 / 2;
23
+ resize: none;
24
+ overflow: hidden;
25
+ white-space: nowrap;
6
26
  border: none;
7
- box-shadow: none;
8
27
  outline: none;
9
- width: 100%;
10
- --slds-c-input-color-border: transparent;
11
- --slds-c-input-shadow-focus: transparent;
12
- --slds-c-input-spacing-border: 0px;
28
+ background: transparent;
29
+ padding: 0.5rem 0.75rem;
30
+ font: inherit;
31
+ min-height: 2.5rem;
13
32
  }
14
33
 
15
- .follow-up-input__input-container.follow-up-input__input-container--focused {
16
- border-color: var(--lwc-brandAccessible, #0176d3);
17
- }
34
+ .follow-up-input__expander--expanded {
35
+ position: absolute;
36
+ bottom: -1px;
37
+ left: -1px;
38
+ right: -1px;
39
+ z-index: 10;
40
+ overflow: visible;
41
+ margin-right: 0;
42
+ background-color: var(--lwc-colorBackground, #fff);
43
+ border: 1px solid var(--lwc-brandAccessible, #0176d3);
44
+ border-radius: 0.25rem;
45
+ }
46
+
47
+ .follow-up-input__expander--expanded::after,
48
+ .follow-up-input__expander--expanded .follow-up-input__textarea {
49
+ white-space: pre-wrap;
50
+ overflow-y: auto;
51
+ max-height: 8rem;
52
+ padding-right: 2.5rem;
53
+ scrollbar-width: none;
54
+ }
55
+
56
+ .follow-up-input__expander--expanded .follow-up-input__textarea::-webkit-scrollbar {
57
+ display: none;
58
+ }
59
+
60
+ .follow-up-input__submit-button {
61
+ position: absolute;
62
+ right: 0.25rem;
63
+ bottom: 0.25rem;
64
+ z-index: 15;
65
+ }