Skip to content

Commit 1109091

Browse files
authored
fix(links): ensure cmd-click always opens in new tab (#5994)
* fix(links): ensure cmd-click always opens in new tab Some links were always preventDefault-ed, which prevented cmd-click from opening them in a new tab. This is now tested for all widgets that have links, and cmd-click will always open in a new tab. * bump bundles
1 parent 1f88430 commit 1109091

File tree

54 files changed

+1654
-10
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+1654
-10
lines changed

bundlesize.config.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@
2222
},
2323
{
2424
"path": "packages/react-instantsearch/dist/umd/ReactInstantSearch.min.js",
25-
"maxSize": "64.5 kB"
25+
"maxSize": "65 kB"
2626
},
2727
{
2828
"path": "packages/vue-instantsearch/vue2/umd/index.js",
29-
"maxSize": "68 kB"
29+
"maxSize": "68.5 kB"
3030
},
3131
{
3232
"path": "packages/vue-instantsearch/vue3/umd/index.js",
33-
"maxSize": "68.5 kB"
33+
"maxSize": "69 kB"
3434
},
3535
{
3636
"path": "packages/vue-instantsearch/vue2/cjs/index.js",

packages/instantsearch.js/src/components/Breadcrumb/Breadcrumb.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { cx } from 'instantsearch-ui-components';
44
import { h } from 'preact';
55

6+
import { isSpecialClick } from '../../lib/utils';
67
import Template from '../Template/Template';
78

89
import type { BreadcrumbConnectorParamsItem } from '../../connectors/breadcrumb/connectBreadcrumb';
@@ -55,6 +56,9 @@ const Breadcrumb = ({
5556
className: cssClasses.link,
5657
href: createURL(null),
5758
onClick: (event: MouseEvent) => {
59+
if (isSpecialClick(event)) {
60+
return;
61+
}
5862
event.preventDefault();
5963
refine(null);
6064
},
@@ -86,6 +90,9 @@ const Breadcrumb = ({
8690
className={cssClasses.link}
8791
href={createURL(item.value)}
8892
onClick={(event) => {
93+
if (isSpecialClick(event)) {
94+
return;
95+
}
8996
event.preventDefault();
9097
refine(item.value);
9198
}}

packages/react-instantsearch/src/ui/Menu.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { cx } from 'instantsearch-ui-components';
22
import React from 'react';
33

4+
import { isModifierClick } from './lib/isModifierClick';
45
import { ShowMoreButton } from './ShowMoreButton';
56

67
import type { ShowMoreButtonTranslations } from './ShowMoreButton';
@@ -102,6 +103,9 @@ export function Menu({
102103
className={cx('ais-Menu-link', classNames.link)}
103104
href={createURL(item.value)}
104105
onClick={(event) => {
106+
if (isModifierClick(event)) {
107+
return;
108+
}
105109
event.preventDefault();
106110
onRefine(item);
107111
}}

packages/vue-instantsearch/src/components/Breadcrumb.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@
2020
v-if="Boolean(state.items.length)"
2121
:href="state.createURL()"
2222
:class="suit('link')"
23-
@click.prevent="state.refine()"
23+
@click.exact.left.prevent="state.refine()"
2424
>
2525
<slot name="rootLabel">Home</slot>
2626
</a>
2727
<a
2828
v-else
2929
:href="state.createURL(null)"
3030
:class="suit('link')"
31-
@click.prevent="state.refine(null)"
31+
@click.exact.left.prevent="state.refine(null)"
3232
>
3333
<slot name="rootLabel">Home</slot>
3434
</a>
@@ -44,7 +44,7 @@
4444
v-if="!isLastItem(index)"
4545
:href="state.createURL(item.value)"
4646
:class="suit('link')"
47-
@click.prevent="state.refine(item.value)"
47+
@click.exact.left.prevent="state.refine(item.value)"
4848
>{{ item.label }}</a
4949
>
5050
<template v-else>{{ item.label }}</template>

packages/vue-instantsearch/src/components/HierarchicalMenuList.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<a
2020
:href="createURL(item.value)"
2121
:class="[suit('link'), item.isRefined && suit('link', 'selected')]"
22-
@click.prevent="refine(item.value)"
22+
@click.exact.left.prevent="refine(item.value)"
2323
>
2424
<span :class="suit('label')">{{ item.label }}</span>
2525
<span :class="suit('count')">{{ item.count }}</span>

packages/vue-instantsearch/src/components/Menu.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
<a
2323
:href="state.createURL(item.value)"
2424
:class="suit('link')"
25-
@click.prevent="state.refine(item.value)"
25+
@click.exact.left.prevent="state.refine(item.value)"
2626
>
2727
<span :class="suit('label')">{{ item.label }}</span>
2828
<span :class="suit('count')">{{ item.count }}</span>
@@ -37,7 +37,7 @@
3737
!state.canToggleShowMore && suit('showMore', 'disabled'),
3838
]"
3939
:disabled="!state.canToggleShowMore"
40-
@click.prevent="state.toggleShowMore()"
40+
@click.prevent="state.toggleShowMore"
4141
>
4242
<slot name="showMoreLabel" :is-showing-more="state.isShowingMore">{{
4343
state.isShowingMore ? 'Show less' : 'Show more'

packages/vue-instantsearch/src/components/RatingMenu.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
:href="state.createURL(item.value)"
3232
:aria-label="`${item.value} & up`"
3333
:class="suit('link')"
34-
@click.prevent="state.refine(item.value)"
34+
@click.exact.left.prevent="state.refine(item.value)"
3535
>
3636
<template v-for="(full, n) in item.stars">
3737
<svg

tests/common/widgets/breadcrumb/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { fakeAct } from '../../common';
22

3+
import { createLinksTests } from './links';
34
import { createOptimisticUiTests } from './optimistic-ui';
45
import { createOptionsTests } from './options';
56

@@ -22,5 +23,6 @@ export function createBreadcrumbWidgetTests(
2223
describe('Breadcrumb widget common tests', () => {
2324
createOptimisticUiTests(setup, { act, skippedTests });
2425
createOptionsTests(setup, { act, skippedTests });
26+
createLinksTests(setup, { act, skippedTests });
2527
});
2628
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import {
2+
createSearchClient,
3+
createMultiSearchResponse,
4+
createSingleSearchResponse,
5+
} from '@instantsearch/mocks';
6+
import { wait } from '@instantsearch/testutils';
7+
import userEvent from '@testing-library/user-event';
8+
9+
import type { BreadcrumbWidgetSetup } from '.';
10+
import type { TestOptions } from '../../common';
11+
12+
export function createLinksTests(
13+
setup: BreadcrumbWidgetSetup,
14+
{ act }: Required<TestOptions>
15+
) {
16+
describe('links', () => {
17+
test("a click on a link with modifier doesn't search", async () => {
18+
const delay = 100;
19+
const margin = 10;
20+
const attributes = ['1', '2'];
21+
const options = {
22+
instantSearchOptions: {
23+
indexName: 'indexName',
24+
initialUiState: {
25+
indexName: {
26+
hierarchicalMenu: {
27+
[attributes[0]]: ['Cameras & Camcorders > Digital Cameras'],
28+
},
29+
},
30+
},
31+
searchClient: createSearchClient({
32+
search: jest.fn(async (requests) => {
33+
await wait(delay);
34+
return createMultiSearchResponse(
35+
...requests.map(() =>
36+
createSingleSearchResponse({
37+
facets: {
38+
[attributes[0]]: {
39+
'Cameras & Camcorders': 1369,
40+
},
41+
[attributes[1]]: {
42+
'Cameras & Camcorders > Digital Cameras': 170,
43+
},
44+
},
45+
})
46+
)
47+
);
48+
}),
49+
}),
50+
},
51+
widgetParams: { attributes },
52+
};
53+
54+
await setup(options);
55+
56+
// Wait for initial results to populate widgets with data
57+
await act(async () => {
58+
await wait(margin + delay);
59+
await wait(0);
60+
});
61+
62+
const container = document.querySelector('.ais-Breadcrumb')!;
63+
64+
// Initial state, before interaction
65+
{
66+
expect(container.querySelectorAll('a')).toHaveLength(2);
67+
expect(
68+
options.instantSearchOptions.searchClient.search
69+
).toHaveBeenCalledTimes(1);
70+
}
71+
72+
// Click all links with a modifier
73+
{
74+
await act(async () => {
75+
container.querySelectorAll('a').forEach((link) => {
76+
userEvent.click(link, { ctrlKey: true });
77+
});
78+
79+
await wait(0);
80+
await wait(0);
81+
});
82+
83+
// No UI has changed
84+
expect(container.querySelectorAll('a')).toHaveLength(2);
85+
expect(
86+
options.instantSearchOptions.searchClient.search
87+
).toHaveBeenCalledTimes(1);
88+
}
89+
90+
// Wait for new results to come in
91+
{
92+
await act(async () => {
93+
await wait(delay + margin);
94+
});
95+
96+
expect(container.querySelectorAll('a')).toHaveLength(2);
97+
expect(
98+
options.instantSearchOptions.searchClient.search
99+
).toHaveBeenCalledTimes(1);
100+
}
101+
});
102+
});
103+
}

tests/common/widgets/clear-refinements/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { fakeAct } from '../../common';
22

3+
import { createLinksTests } from './links';
34
import { createOptionsTests } from './options';
45

56
import type { TestOptions, TestSetup } from '../../common';
@@ -20,5 +21,6 @@ export function createClearRefinementsWidgetTests(
2021

2122
describe('ClearRefinements widget common tests', () => {
2223
createOptionsTests(setup, { act, skippedTests });
24+
createLinksTests(setup, { act, skippedTests });
2325
});
2426
}

0 commit comments

Comments
 (0)