Skip to content

Commit a13b3cd

Browse files
fix(sref): do not handle clicks if the dom element has a 'target' attribute (#799)
Also do not handle clicks if shift or alt is being held down. Closes #786
1 parent 27610e8 commit a13b3cd

File tree

3 files changed

+96
-25
lines changed

3 files changed

+96
-25
lines changed

src/components/__tests__/UISref.test.tsx

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ describe('<UISref>', () => {
9494

9595
it('calls the child elements onClick function and honors e.preventDefault()', async () => {
9696
const goSpy = jest.spyOn(router.stateService, 'go');
97-
const onClickSpy = jest.fn(e => e.preventDefault());
97+
const onClickSpy = jest.fn((e) => e.preventDefault());
9898
const wrapper = mountInRouter(
9999
<UISref to="state2">
100100
<a onClick={onClickSpy}>state2</a>
@@ -106,7 +106,7 @@ describe('<UISref>', () => {
106106
expect(goSpy).not.toHaveBeenCalled();
107107
});
108108

109-
it("doesn't trigger a transition when middle-clicked/ctrl+clicked", async () => {
109+
it("doesn't trigger a transition when middle-clicked", async () => {
110110
const stateServiceGoSpy = jest.spyOn(router.stateService, 'go');
111111
const wrapper = mountInRouter(
112112
<UISref to="state2">
@@ -119,9 +119,44 @@ describe('<UISref>', () => {
119119
expect(stateServiceGoSpy).toHaveBeenCalledTimes(1);
120120

121121
link.simulate('click', { button: 1 });
122-
link.simulate('click', { metaKey: true });
122+
expect(stateServiceGoSpy).toHaveBeenCalledTimes(1);
123+
});
124+
125+
it("doesn't trigger a transition when ctrl/meta/shift/alt+clicked", async () => {
126+
const stateServiceGoSpy = jest.spyOn(router.stateService, 'go');
127+
const wrapper = mountInRouter(
128+
<UISref to="state2">
129+
<a>state2</a>
130+
</UISref>
131+
);
132+
133+
const link = wrapper.find('a');
134+
link.simulate('click');
135+
expect(stateServiceGoSpy).toHaveBeenCalledTimes(1);
136+
123137
link.simulate('click', { ctrlKey: true });
138+
expect(stateServiceGoSpy).toHaveBeenCalledTimes(1);
124139

140+
link.simulate('click', { metaKey: true });
141+
expect(stateServiceGoSpy).toHaveBeenCalledTimes(1);
142+
143+
link.simulate('click', { shiftKey: true });
144+
expect(stateServiceGoSpy).toHaveBeenCalledTimes(1);
145+
146+
link.simulate('click', { altKey: true });
125147
expect(stateServiceGoSpy).toHaveBeenCalledTimes(1);
126148
});
149+
150+
it("doesn't trigger a transition when the anchor has a 'target' attribute", async () => {
151+
const stateServiceGoSpy = jest.spyOn(router.stateService, 'go');
152+
const wrapper = mountInRouter(
153+
<UISref to="state2">
154+
<a target="_blank">state2</a>
155+
</UISref>
156+
);
157+
158+
const link = wrapper.find('a');
159+
link.simulate('click');
160+
expect(stateServiceGoSpy).not.toHaveBeenCalled();
161+
});
127162
});

src/hooks/__tests__/useSref.test.tsx

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@ const state = { name: 'state', url: '/state' };
1010
const state2 = { name: 'state2', url: '/state2' };
1111
const state3 = { name: 'state3', url: '/state3/:param' };
1212

13-
const Link = ({ to, params = undefined, children = undefined }) => {
13+
const Link = ({ to, params = undefined, children = undefined, target = undefined }) => {
1414
const sref = useSref(to, params);
15-
return <a {...sref}>{children}</a>;
15+
return (
16+
<a {...sref} target={target}>
17+
{children}
18+
</a>
19+
);
1620
};
1721

18-
describe('useUiSref', () => {
22+
describe('useSref', () => {
1923
let { router, routerGo, mountInRouter } = makeTestRouter([]);
2024
beforeEach(() => ({ router, routerGo, mountInRouter } = makeTestRouter([state, state2, state3])));
2125

@@ -29,21 +33,51 @@ describe('useUiSref', () => {
2933
expect(wrapper.html()).toBe('<a href="/state2">state2</a>');
3034
});
3135

32-
it('returns an onClick function which activates the target state', () => {
33-
const spy = jest.spyOn(router.stateService, 'go');
34-
const wrapper = mountInRouter(<Link to="state" />);
35-
wrapper.simulate('click');
36+
describe('onClick function', () => {
37+
it('activates the target state', () => {
38+
const spy = jest.spyOn(router.stateService, 'go');
39+
const wrapper = mountInRouter(<Link to="state" />);
40+
wrapper.simulate('click');
3641

37-
expect(spy).toBeCalledTimes(1);
38-
expect(spy).toBeCalledWith('state', expect.anything(), expect.anything());
39-
});
42+
expect(spy).toBeCalledTimes(1);
43+
expect(spy).toBeCalledWith('state', expect.anything(), expect.anything());
44+
});
4045

41-
it('returns an onClick function which respects defaultPrevented', () => {
42-
const spy = jest.spyOn(router.stateService, 'go');
43-
const wrapper = mountInRouter(<Link to="state" />);
44-
wrapper.simulate('click', { defaultPrevented: true });
46+
it('respects defaultPrevented', () => {
47+
const spy = jest.spyOn(router.stateService, 'go');
48+
const wrapper = mountInRouter(<Link to="state" />);
49+
wrapper.simulate('click', { defaultPrevented: true });
50+
51+
expect(spy).not.toHaveBeenCalled();
52+
});
53+
54+
it('does not get called when middle or right clicked', () => {
55+
const spy = jest.spyOn(router.stateService, 'go');
56+
const wrapper = mountInRouter(<Link to="state" />);
4557

46-
expect(spy).not.toHaveBeenCalled();
58+
wrapper.simulate('click', { button: 1 });
59+
expect(spy).not.toHaveBeenCalled();
60+
});
61+
62+
it('does not get called if any of these modifiers are present: ctrl, meta, shift, alt', () => {
63+
const spy = jest.spyOn(router.stateService, 'go');
64+
const wrapper = mountInRouter(<Link to="state" />);
65+
66+
wrapper.simulate('click', { ctrlKey: true });
67+
wrapper.simulate('click', { metaKey: true });
68+
wrapper.simulate('click', { shiftKey: true });
69+
wrapper.simulate('click', { altKey: true });
70+
71+
expect(spy).not.toHaveBeenCalled();
72+
});
73+
74+
it('does not get called if the underlying DOM element has a "target" attribute', () => {
75+
const spy = jest.spyOn(router.stateService, 'go');
76+
const wrapper = mountInRouter(<Link to="state" target="_blank" />);
77+
78+
wrapper.simulate('click');
79+
expect(spy).not.toHaveBeenCalled();
80+
});
4781
});
4882

4983
it('updates the href when the stateName changes', () => {
@@ -96,7 +130,7 @@ describe('useUiSref', () => {
96130
it('calls go() with the actual string provided (not with the name of the matched future state)', async () => {
97131
router.stateRegistry.register({ name: 'future.**', url: '/future' });
98132

99-
const Link = props => {
133+
const Link = (props) => {
100134
const sref = useSref('future.child', { param: props.param });
101135
return <a {...sref} />;
102136
};
@@ -112,7 +146,7 @@ describe('useUiSref', () => {
112146
it('participates in parent UISrefActive component active state', async () => {
113147
await routerGo('state2');
114148

115-
const State2Link = props => {
149+
const State2Link = (props) => {
116150
const sref = useSref('state2');
117151
return (
118152
<a {...sref} {...props}>
@@ -150,7 +184,7 @@ describe('useUiSref', () => {
150184
it('participates in grandparent UISrefActive component active state', async () => {
151185
await routerGo('state2');
152186

153-
const State2Link = props => {
187+
const State2Link = (props) => {
154188
const sref = useSref('state2');
155189
return (
156190
<a {...sref} className={props.className}>
@@ -178,7 +212,7 @@ describe('useUiSref', () => {
178212
it('stops participating in parent/grandparent UISrefActive component active state when unmounted', async () => {
179213
await routerGo('state2');
180214

181-
const State2Link = props => {
215+
const State2Link = (props) => {
182216
const sref = useSref('state2');
183217
return (
184218
<a {...sref} className={props.className}>
@@ -187,7 +221,7 @@ describe('useUiSref', () => {
187221
);
188222
};
189223

190-
const Component = props => {
224+
const Component = (props) => {
191225
return (
192226
<UISrefActive class="grandparentactive">
193227
<div>

src/hooks/useSref.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** @packageDocumentation @reactapi @module react_hooks */
22

33
import * as React from 'react';
4-
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
4+
import { ReactHTMLElement, useCallback, useContext, useEffect, useMemo, useState } from 'react';
55
import { isString, StateDeclaration, TransitionOptions, UIRouter } from '@uirouter/core';
66
import { UISrefActiveContext } from '../components';
77
import { useDeepObjectDiff } from './useDeepObjectDiff';
@@ -84,7 +84,9 @@ export function useSref(stateName: string, params: object = {}, options: Transit
8484

8585
const onClick = useCallback(
8686
(e: React.MouseEvent) => {
87-
if (!e.defaultPrevented && !(e.button == 1 || e.metaKey || e.ctrlKey)) {
87+
const targetAttr = (e.target as any)?.attributes?.target;
88+
const modifierKey = e.button >= 1 || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey;
89+
if (!e.defaultPrevented && targetAttr == null && !modifierKey) {
8890
e.preventDefault();
8991
router.stateService.go(stateName, paramsMemo, optionsMemo);
9092
}

0 commit comments

Comments
 (0)