Skip to content

Commit

Permalink
fix(atomic): using _blank target on custom recommendation link temp…
Browse files Browse the repository at this point in the history
…late 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
<atomic-recs-result-template>
  <template slot="link">
    <atomic-result-link>
      <a slot="attributes" target="_blank"></a>
    </atomic-result-link>
  </template>
  <template>... recommendation content is there ...</template>
</atomic-recs-result-template>
```
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
  • Loading branch information
lbergeron authored Feb 13, 2025
1 parent 73341a3 commit de34ef6
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,24 @@ export const WithFullTemplate: Story = {
},
};

export const RecsOpeningInNewTab: Story = {
tags: ['test'],
args: {
'slots-default': ` <atomic-product-template>
<template slot="link">
<atomic-product-link>
<a slot="attributes" target="_blank"></a>
</atomic-product-link>
</template>
<template>
<atomic-product-section-title>
<atomic-product-text field="ec_name"></atomic-product-text>
</atomic-product-section-title>
</template>
</atomic-product-template>`,
},
};

export const AsCarousel: Story = {
args: {
'attributes-products-per-page': 3,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@ export class AtomicCommerceRecommendationList
}

private getAtomicProductProps(product: Product) {
const linkContent =
this.productTemplateProvider.getLinkTemplateContent(product);

return {
interactiveProduct: this.recommendations.interactiveProduct({
options: {product},
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,9 @@ export class AtomicIPXRecsList implements InitializableComponent<RecsBindings> {
interactiveResult.select = () => {
this.onSelect(recommendation, originalSelect);
};
const linkContent =
this.itemTemplateProvider.getLinkTemplateContent(recommendation);

return {
interactiveResult,
result: recommendation,
Expand All @@ -359,8 +362,8 @@ export class AtomicIPXRecsList implements InitializableComponent<RecsBindings> {
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,24 @@ export const RecsWithFullTemplate: Story = {
},
};

export const RecsOpeningInNewTab: Story = {
tags: ['test'],
args: {
'slots-default': `<atomic-recs-result-template>
<template slot="link">
<atomic-result-link>
<a slot="attributes" target="_blank"></a>
</atomic-result-link>
</template>
<template>
<atomic-result-section-title>
<atomic-result-text field="title"></atomic-result-text>
</atomic-result-section-title>
</template>
</atomic-recs-result-template>`,
},
};

export const RecsAsCarousel: Story = {
args: {
'attributes-number-of-recommendations-per-page': 4,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,9 @@ export class AtomicRecsList implements InitializableComponent<RecsBindings> {
}

private getPropsForAtomicRecsResult(recommendation: RecsResult) {
const linkContent =
this.itemTemplateProvider.getLinkTemplateContent(recommendation);

return {
interactiveResult: buildRecsInteractiveResult(this.bindings.engine, {
options: {result: recommendation},
Expand All @@ -333,8 +336,8 @@ export class AtomicRecsList implements InitializableComponent<RecsBindings> {
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit de34ef6

Please sign in to comment.