@coveo/quantic 3.34.2 → 3.35.0

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.
package/README.md CHANGED
@@ -110,15 +110,15 @@ https://your-salesforce-lws-disabled-scratch-org-instance.force.com/examples
110
110
  Once the community has been deployed, you can deploy the `main` or `example` components to a specific org only when needed by running the corresponding commands:
111
111
 
112
112
  ```bash
113
- pnpm run deploy:main --target-org Quantic__LWS_enabled
114
- pnpm run deploy:examples --target-org Quantic__LWS_enabled
113
+ pnpm run deploy:main Quantic__LWS_enabled
114
+ pnpm run deploy:examples Quantic__LWS_enabled
115
115
  ```
116
116
 
117
117
  You can replace Quantic\_\_LWS_enabled with your target org alias. For example:
118
118
 
119
119
  ```bash
120
- pnpm run deploy:main --target-org MyCustomOrg
121
- pnpm run deploy:examples --target-org MyCustomOrg
120
+ pnpm run deploy:main MyCustomOrg
121
+ pnpm run deploy:examples MyCustomOrg
122
122
  ```
123
123
 
124
124
  ### Run Playwright for Quantic Components
@@ -5,7 +5,6 @@ import {
5
5
  // @ts-ignore
6
6
  import {createElement} from 'lwc';
7
7
  import QuanticCitation from '../quanticCitation';
8
- import {debounce} from '../quanticCitation';
9
8
 
10
9
  const functionsMocks = {
11
10
  eventHandler: jest.fn((event) => event),
@@ -113,67 +112,6 @@ describe('c-quantic-citation', () => {
113
112
  cleanup();
114
113
  });
115
114
 
116
- describe('debounce function', () => {
117
- let mockFnToDebounce;
118
- const DEBOUNCE_DELAY = 200;
119
-
120
- beforeEach(() => {
121
- jest.useFakeTimers();
122
- mockFnToDebounce = jest.fn();
123
- });
124
-
125
- afterEach(() => {
126
- jest.useRealTimers();
127
- });
128
-
129
- // Ensures that rapid calls result in only one execution
130
- test('should execute once after the delay and use the arguments from the last call', () => {
131
- const debouncedFn = debounce(mockFnToDebounce, DEBOUNCE_DELAY);
132
-
133
- debouncedFn('first');
134
- expect(mockFnToDebounce).not.toHaveBeenCalled();
135
-
136
- jest.advanceTimersByTime(DEBOUNCE_DELAY - 100);
137
- debouncedFn('middle');
138
-
139
- jest.advanceTimersByTime(DEBOUNCE_DELAY - 100);
140
- debouncedFn('last');
141
-
142
- expect(mockFnToDebounce).not.toHaveBeenCalled();
143
- jest.advanceTimersByTime(DEBOUNCE_DELAY);
144
-
145
- expect(mockFnToDebounce).toHaveBeenCalledTimes(1);
146
- expect(mockFnToDebounce).toHaveBeenCalledWith('last');
147
- });
148
-
149
- // Ensures calls separated by more than the delay execute independently.
150
- test('should execute multiple times if calls are spaced outside the delay', () => {
151
- const debouncedFn = debounce(mockFnToDebounce, DEBOUNCE_DELAY);
152
-
153
- debouncedFn(100);
154
- jest.advanceTimersByTime(DEBOUNCE_DELAY);
155
- expect(mockFnToDebounce).toHaveBeenCalledTimes(1);
156
- expect(mockFnToDebounce).toHaveBeenCalledWith(100);
157
-
158
- debouncedFn(200, {data: 'second'});
159
- jest.advanceTimersByTime(DEBOUNCE_DELAY);
160
- expect(mockFnToDebounce).toHaveBeenCalledTimes(2);
161
- expect(mockFnToDebounce).toHaveBeenCalledWith(200, {data: 'second'});
162
- });
163
-
164
- // Ensures that the execution can be canceled before the delay expires.
165
- test('should not execute the function if cancel is called before the delay', () => {
166
- const debouncedFn = debounce(mockFnToDebounce, DEBOUNCE_DELAY);
167
-
168
- debouncedFn('should not run');
169
- jest.advanceTimersByTime(DEBOUNCE_DELAY / 2);
170
- debouncedFn.cancel();
171
-
172
- jest.advanceTimersByTime(DEBOUNCE_DELAY * 2);
173
- expect(mockFnToDebounce).not.toHaveBeenCalled();
174
- });
175
- });
176
-
177
115
  it('should properly display the citation', async () => {
178
116
  const element = createTestComponent();
179
117
  await flushPromises();
@@ -222,7 +160,7 @@ describe('c-quantic-citation', () => {
222
160
  jest.useRealTimers();
223
161
  });
224
162
 
225
- it('should display the citation tooltip', async () => {
163
+ it('should display the citation tooltip after 200ms delay', async () => {
226
164
  const element = createTestComponent();
227
165
  await flushPromises();
228
166
 
@@ -236,18 +174,17 @@ describe('c-quantic-citation', () => {
236
174
  expect(citationLink).not.toBeNull();
237
175
  expect(citationTooltip).not.toBeNull();
238
176
 
239
- // Spy on the tooltip's showTooltip method to verify it gets called
240
177
  const showTooltipSpy = jest.spyOn(citationTooltip, 'showTooltip');
241
178
 
242
179
  await citationLink.dispatchEvent(
243
180
  new CustomEvent('mouseenter', {bubbles: true})
244
181
  );
182
+ expect(showTooltipSpy).toHaveBeenCalledTimes(0);
245
183
  jest.advanceTimersByTime(200);
246
-
247
184
  expect(showTooltipSpy).toHaveBeenCalledTimes(1);
248
185
  });
249
186
 
250
- it('should dispatch a citation hover event after hovering over the the citation for more than 1200ms, 200ms debounce duration before hover + 1000ms minimum hover duration', async () => {
187
+ it('should dispatch a citation hover event after hovering over the the citation for more than 1200ms, 200ms delay before hover + 1000ms minimum hover duration', async () => {
251
188
  const element = createTestComponent();
252
189
  await flushPromises();
253
190
  setupEventDispatchTest('quantic__citationhover');
@@ -260,16 +197,19 @@ describe('c-quantic-citation', () => {
260
197
  await citationLink.dispatchEvent(
261
198
  new CustomEvent('mouseenter', {bubbles: true})
262
199
  );
200
+ // Wait for show delay - this triggers the tooltip to show and sets hoverStartTimestamp
201
+ jest.advanceTimersByTime(200);
202
+ // Now wait for minimum hover time
263
203
  jest.advanceTimersByTime(1000);
264
204
  await citationLink.dispatchEvent(
265
205
  new CustomEvent('mouseleave', {bubbles: true})
266
206
  );
267
- // Additional 200ms delay for the new hide tooltip logic
207
+ // Additional 200ms delay for the hide tooltip logic
268
208
  jest.advanceTimersByTime(200);
269
209
 
270
210
  expect(functionsMocks.eventHandler).toHaveBeenCalledTimes(1);
271
211
  expect(functionsMocks.eventHandler).toHaveBeenCalledWith({
272
- citationHoverTimeMs: 1200,
212
+ citationHoverTimeMs: 1100,
273
213
  });
274
214
  });
275
215
 
@@ -313,32 +253,32 @@ describe('c-quantic-citation', () => {
313
253
 
314
254
  const hideTooltipSpy = jest.spyOn(citationTooltip, 'hideTooltip');
315
255
 
316
- // Hover over the citation
256
+ // Hover over the citation - tooltip shows after 200ms delay
317
257
  await citationLink.dispatchEvent(
318
258
  new CustomEvent('mouseenter', {bubbles: true})
319
259
  );
320
260
  jest.advanceTimersByTime(200);
321
261
 
322
- // Move cursor to tooltip in less than 200ms
262
+ // Move cursor to tooltip quickly (before hide delay expires)
323
263
  await citationLink.dispatchEvent(
324
264
  new CustomEvent('mouseleave', {bubbles: true})
325
265
  );
326
- jest.advanceTimersByTime(100);
266
+
327
267
  await citationTooltip.dispatchEvent(
328
268
  new CustomEvent('mouseenter', {bubbles: true})
329
269
  );
330
270
 
331
- // Move cursor back to citation in less than 200ms
271
+ // Move cursor back to citation quickly (before hide delay expires)
332
272
  await citationTooltip.dispatchEvent(
333
273
  new CustomEvent('mouseleave', {bubbles: true})
334
274
  );
335
- jest.advanceTimersByTime(100);
275
+
336
276
  await citationLink.dispatchEvent(
337
277
  new CustomEvent('mouseenter', {bubbles: true})
338
278
  );
339
279
 
340
- // Advance time beyond 200ms to see if tooltip is hidden
341
- jest.advanceTimersByTime(300);
280
+ // Advance time beyond 100ms to see if tooltip is hidden
281
+ jest.advanceTimersByTime(200);
342
282
 
343
283
  expect(hideTooltipSpy).toHaveBeenCalledTimes(0);
344
284
  });
@@ -5,28 +5,10 @@ import {LightningElement, api} from 'lwc';
5
5
  /** @typedef {import("coveo").InteractiveCitation} InteractiveCitation */
6
6
 
7
7
  const minimumTooltipDisplayDurationMs = 1000;
8
- const tooltipHideDelayMs = 200;
8
+ const tooltipDelayMsShow = 200;
9
+ const tooltipDelayMsHide = 100;
9
10
  const supportedFileTypesForTextFragment = ['html', 'SalesforceItem'];
10
11
 
11
- /**
12
- * Debounce function that delays invoking func until after wait milliseconds
13
- * have elapsed since the last time the debounced function was invoked.
14
- * Includes a cancel method to clear any pending timeout.
15
- * @param {Function} fn - The function to debounce
16
- * @param {number} delay - The number of milliseconds to delay
17
- * @returns {Function} The debounced function with cancel method
18
- */
19
- export function debounce(fn, delay) {
20
- let timeout;
21
- const debounced = (...args) => {
22
- clearTimeout(timeout);
23
- // eslint-disable-next-line @lwc/lwc/no-async-operation
24
- timeout = setTimeout(() => fn(...args), delay);
25
- };
26
- debounced.cancel = () => clearTimeout(timeout);
27
- return debounced;
28
- }
29
-
30
12
  /**
31
13
  * The `QuanticCitation` component renders an individual citation.
32
14
  * @fires CustomEvent#quantic__citationhover
@@ -69,8 +51,14 @@ export default class QuanticCitation extends NavigationMixin(LightningElement) {
69
51
  salesforceRecordUrl;
70
52
  /** @type {boolean} */
71
53
  isHrefWithTextFragment = false;
54
+ /** @type {boolean} */
55
+ isCitationHovered = false;
56
+ /** @type {boolean} */
57
+ isTooltipHovered = false;
72
58
  /** @type {Object} */
73
- hideTooltipDebounced;
59
+ showTimer = null;
60
+ /** @type {Object} */
61
+ hideTimer = null;
74
62
 
75
63
  connectedCallback() {
76
64
  const fileType = this.citation?.fields?.filetype;
@@ -78,15 +66,6 @@ export default class QuanticCitation extends NavigationMixin(LightningElement) {
78
66
  !this.disableCitationAnchoring &&
79
67
  supportedFileTypesForTextFragment.includes(fileType) &&
80
68
  !!this.text;
81
-
82
- // Initialize the debounced hide tooltip function
83
- this.hideTooltipDebounced = debounce(() => {
84
- if (this.tooltipIsDisplayed) {
85
- this.dispatchCitationHoverEvent();
86
- }
87
- this.tooltipIsDisplayed = false;
88
- this.tooltipComponent?.hideTooltip();
89
- }, tooltipHideDelayMs);
90
69
  }
91
70
 
92
71
  renderedCallback() {
@@ -111,30 +90,77 @@ export default class QuanticCitation extends NavigationMixin(LightningElement) {
111
90
  disconnectedCallback() {
112
91
  this.removeBindings?.();
113
92
  clearTimeout(this.timeout);
114
- this.hideTooltipDebounced?.cancel();
93
+ this.cancelShow();
94
+ this.cancelHide();
115
95
  }
116
96
 
117
97
  handleCitationMouseEnter() {
118
- this.showTooltip();
98
+ this.isCitationHovered = true;
99
+ this.updateTooltipHideShow();
119
100
  }
120
101
 
121
102
  handleCitationMouseLeave() {
122
- this.hideTooltipDebounced();
103
+ this.isCitationHovered = false;
104
+ this.updateTooltipHideShow();
123
105
  }
124
106
 
125
107
  handleTooltipMouseEnter() {
126
- this.hideTooltipDebounced.cancel();
108
+ this.isTooltipHovered = true;
109
+ this.updateTooltipHideShow();
127
110
  }
128
111
 
129
112
  handleTooltipMouseLeave() {
130
- this.hideTooltipDebounced();
113
+ this.isTooltipHovered = false;
114
+ this.updateTooltipHideShow();
115
+ }
116
+
117
+ isHovering() {
118
+ return this.isCitationHovered || this.isTooltipHovered;
119
+ }
120
+
121
+ updateTooltipHideShow() {
122
+ if (this.isHovering()) {
123
+ this.cancelHide();
124
+ this.scheduleShow();
125
+ } else {
126
+ this.cancelShow();
127
+ this.scheduleHide();
128
+ }
129
+ }
130
+
131
+ scheduleShow() {
132
+ if (this.showTimer !== null) return;
133
+
134
+ // eslint-disable-next-line @lwc/lwc/no-async-operation
135
+ this.showTimer = setTimeout(() => {
136
+ this.showTimer = null;
137
+ if (this.isHovering()) this.showTooltip();
138
+ }, tooltipDelayMsShow);
139
+ }
140
+
141
+ scheduleHide() {
142
+ if (this.hideTimer !== null) return;
143
+
144
+ // eslint-disable-next-line @lwc/lwc/no-async-operation
145
+ this.hideTimer = setTimeout(() => {
146
+ this.hideTimer = null;
147
+ if (!this.isHovering()) this.hideTooltip();
148
+ }, tooltipDelayMsHide);
149
+ }
150
+
151
+ cancelShow() {
152
+ if (this.showTimer === null) return;
153
+ clearTimeout(this.showTimer);
154
+ this.showTimer = null;
155
+ }
156
+
157
+ cancelHide() {
158
+ if (this.hideTimer === null) return;
159
+ clearTimeout(this.hideTimer);
160
+ this.hideTimer = null;
131
161
  }
132
162
 
133
- /**
134
- * Shows the tooltip immediately and cancels any pending hide.
135
- */
136
163
  showTooltip() {
137
- this.hideTooltipDebounced.cancel();
138
164
  if (!this.tooltipIsDisplayed) {
139
165
  this.hoverStartTimestamp = Date.now();
140
166
  this.tooltipIsDisplayed = true;
@@ -142,6 +168,14 @@ export default class QuanticCitation extends NavigationMixin(LightningElement) {
142
168
  }
143
169
  }
144
170
 
171
+ hideTooltip() {
172
+ if (this.tooltipIsDisplayed) {
173
+ this.dispatchCitationHoverEvent();
174
+ this.tooltipIsDisplayed = false;
175
+ this.tooltipComponent?.hideTooltip();
176
+ }
177
+ }
178
+
145
179
  /**
146
180
  * Dispatches the citation hover analytics event if minimum display duration was met.
147
181
  */