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 ``` 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,