From de34ef61e1fecedd40b53f8761f5df697b4be1dc Mon Sep 17 00:00:00 2001
From: Luc Bergeron <10946843+lbergeron@users.noreply.github.com>
Date: Thu, 13 Feb 2025 13:16:47 -0500
Subject: [PATCH] fix(atomic): using `_blank` target on custom recommendation
link template opens two tabs on click (#4953)
[SVCC-4611](https://coveord.atlassian.net/browse/SVCC-4611)
When a recommendation list uses a custom link template to open
recommendations in a new tab, clicking the recommendation opens two tabs
instead of one.
**How to reproduce the issue**
1. Override the `atomic-recs-result-template` as [per
documentation](https://docs.coveo.com/en/atomic/latest/reference/result-template-components/atomic-result-link/#slots).
```html
... recommendation content is there ...
```
2. If you click the content of the recommendation, it open two tabs. If
you click the margin (between the border and the content), then it opens
a single tab.
**Problem**
The recommendation list components rely on
[DisplayGrid](https://github.com/coveo/ui-kit/blob/master/packages/atomic/src/components/common/item-list/display-grid.tsx)
for their rendering. The problem is related to [this
line](https://github.com/coveo/ui-kit/blob/master/packages/atomic/src/components/common/item-list/display-grid.tsx#L26)
where `DisplayGrid` forces a click on the recommendation content. When
the recommendation content overrides the `link` slot, it already has an
event handler for the `click`. It means that, when clicking the
recommendation content, its `click` handler is invoked (opening the
first tab). Then, the event propagates to the parent (`DisplayGrid`)
which forces a `click` event on the content, causing another tab to
open.
**Proposed solution**
The proposed solution is to modify the recommendation lists to detect
whether the `link` slot is modified. If it is, then the
`stopPropagation` property is set on the child `atomic-recs-result`
component. It prevents the `click` event from being propagated to the
parent when clicking a recommendation content. However, when outside the
recommendation content (i.e., the margin), `DisplayGrid` still forces
the `click` on the content, opening a single tab.
[SVCC-4611]:
https://coveord.atlassian.net/browse/SVCC-4611?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
---
...ommerce-recommendation-list.new.stories.tsx | 18 ++++++++++++++++++
.../atomic-commerce-recommendation-list.tsx | 6 +++++-
.../atomic-commerce-recommendation-list.e2e.ts | 18 ++++++++++++++++++
.../atomic-recs-list/atomic-ipx-recs-list.tsx | 7 +++++--
.../atomic-recs-list.new.stories.tsx | 18 ++++++++++++++++++
.../atomic-recs-list/atomic-recs-list.tsx | 7 +++++--
.../e2e/atomic-recs-list.e2e.ts | 18 ++++++++++++++++++
7 files changed, 87 insertions(+), 5 deletions(-)
diff --git a/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/atomic-commerce-recommendation-list.new.stories.tsx b/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/atomic-commerce-recommendation-list.new.stories.tsx
index dd87f78b044..39ace84c054 100644
--- a/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/atomic-commerce-recommendation-list.new.stories.tsx
+++ b/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/atomic-commerce-recommendation-list.new.stories.tsx
@@ -85,6 +85,24 @@ export const WithFullTemplate: Story = {
},
};
+export const RecsOpeningInNewTab: Story = {
+ tags: ['test'],
+ args: {
+ 'slots-default': `
+
+
+
+
+
+
+
+
+
+
+ `,
+ },
+};
+
export const AsCarousel: Story = {
args: {
'attributes-products-per-page': 3,
diff --git a/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/atomic-commerce-recommendation-list.tsx b/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/atomic-commerce-recommendation-list.tsx
index b8b1cd72474..b9bcec17986 100644
--- a/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/atomic-commerce-recommendation-list.tsx
+++ b/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/atomic-commerce-recommendation-list.tsx
@@ -313,6 +313,9 @@ export class AtomicCommerceRecommendationList
}
private getAtomicProductProps(product: Product) {
+ const linkContent =
+ this.productTemplateProvider.getLinkTemplateContent(product);
+
return {
interactiveProduct: this.recommendations.interactiveProduct({
options: {product},
@@ -327,7 +330,8 @@ export class AtomicCommerceRecommendationList
this.imageSize
),
content: this.productTemplateProvider.getTemplateContent(product),
- linkContent: this.productTemplateProvider.getLinkTemplateContent(product),
+ linkContent,
+ stopPropagation: !!linkContent,
store: this.bindings.store,
density: this.density,
display: this.display,
diff --git a/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/e2e/atomic-commerce-recommendation-list.e2e.ts b/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/e2e/atomic-commerce-recommendation-list.e2e.ts
index 5bf5d80ea70..2ea3ea29cb7 100644
--- a/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/e2e/atomic-commerce-recommendation-list.e2e.ts
+++ b/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/e2e/atomic-commerce-recommendation-list.e2e.ts
@@ -103,6 +103,24 @@ test.describe('when there are no enough recommendations for multiple pages', ()
});
});
+test.describe('when recommendations open in a new tab', async () => {
+ test.beforeEach(async ({recommendationList}) => {
+ await recommendationList.load({story: 'recs-opening-in-new-tab'});
+ await recommendationList.hydrated.waitFor();
+ });
+
+ test('should open a single tab when clicking a recommendation', async ({
+ recommendationList,
+ context,
+ }) => {
+ const pagePromise = context.waitForEvent('page');
+ await recommendationList.recommendation.first().click();
+ await pagePromise;
+
+ expect(context.pages().length).toBe(2);
+ });
+});
+
test('with no recommendations returned by the API, should render placeholders', async ({
recommendationList,
}) => {
diff --git a/packages/atomic/src/components/ipx/atomic-ipx-recs-list/atomic-recs-list/atomic-ipx-recs-list.tsx b/packages/atomic/src/components/ipx/atomic-ipx-recs-list/atomic-recs-list/atomic-ipx-recs-list.tsx
index dff76927c4a..1d6841ad0a1 100644
--- a/packages/atomic/src/components/ipx/atomic-ipx-recs-list/atomic-recs-list/atomic-ipx-recs-list.tsx
+++ b/packages/atomic/src/components/ipx/atomic-ipx-recs-list/atomic-recs-list/atomic-ipx-recs-list.tsx
@@ -347,6 +347,9 @@ export class AtomicIPXRecsList implements InitializableComponent {
interactiveResult.select = () => {
this.onSelect(recommendation, originalSelect);
};
+ const linkContent =
+ this.itemTemplateProvider.getLinkTemplateContent(recommendation);
+
return {
interactiveResult,
result: recommendation,
@@ -359,8 +362,8 @@ export class AtomicIPXRecsList implements InitializableComponent {
this.imageSize
),
content: this.itemTemplateProvider.getTemplateContent(recommendation),
- linkContent:
- this.itemTemplateProvider.getLinkTemplateContent(recommendation),
+ linkContent,
+ stopPropagation: !!linkContent,
store: this.bindings.store,
density: this.density,
display: this.display,
diff --git a/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.new.stories.tsx b/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.new.stories.tsx
index b51a2a05823..f8fb62872de 100644
--- a/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.new.stories.tsx
+++ b/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.new.stories.tsx
@@ -74,6 +74,24 @@ export const RecsWithFullTemplate: Story = {
},
};
+export const RecsOpeningInNewTab: Story = {
+ tags: ['test'],
+ args: {
+ 'slots-default': `
+
+
+
+
+
+
+
+
+
+
+ `,
+ },
+};
+
export const RecsAsCarousel: Story = {
args: {
'attributes-number-of-recommendations-per-page': 4,
diff --git a/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.tsx b/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.tsx
index a61b824c19a..7691fbcf98b 100644
--- a/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.tsx
+++ b/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.tsx
@@ -319,6 +319,9 @@ export class AtomicRecsList implements InitializableComponent {
}
private getPropsForAtomicRecsResult(recommendation: RecsResult) {
+ const linkContent =
+ this.itemTemplateProvider.getLinkTemplateContent(recommendation);
+
return {
interactiveResult: buildRecsInteractiveResult(this.bindings.engine, {
options: {result: recommendation},
@@ -333,8 +336,8 @@ export class AtomicRecsList implements InitializableComponent {
this.imageSize
),
content: this.itemTemplateProvider.getTemplateContent(recommendation),
- linkContent:
- this.itemTemplateProvider.getLinkTemplateContent(recommendation),
+ linkContent,
+ stopPropagation: !!linkContent,
store: this.bindings.store,
density: this.density,
display: this.display,
diff --git a/packages/atomic/src/components/recommendations/atomic-recs-list/e2e/atomic-recs-list.e2e.ts b/packages/atomic/src/components/recommendations/atomic-recs-list/e2e/atomic-recs-list.e2e.ts
index e6ad2538fed..bc67dcec607 100644
--- a/packages/atomic/src/components/recommendations/atomic-recs-list/e2e/atomic-recs-list.e2e.ts
+++ b/packages/atomic/src/components/recommendations/atomic-recs-list/e2e/atomic-recs-list.e2e.ts
@@ -101,6 +101,24 @@ test.describe('when there are no enough recommendations for multiple pages', ()
});
});
+test.describe('when recommendations open in a new tab', async () => {
+ test.beforeEach(async ({recsList}) => {
+ await recsList.load({story: 'recs-opening-in-new-tab'});
+ await recsList.hydrated.waitFor();
+ });
+
+ test('should open a single tab when clicking a recommendation', async ({
+ recsList,
+ context,
+ }) => {
+ const pagePromise = context.waitForEvent('page');
+ await recsList.recommendation.first().click();
+ await pagePromise;
+
+ expect(context.pages().length).toBe(2);
+ });
+});
+
test('with no recommendations returned by the API, should render placeholders', async ({
recsList,
page,