Skip to content

Commit 5c89246

Browse files
committed
[r96943502] fix: provide a way to prevent default event to avoid jumping effect
1 parent eb97459 commit 5c89246

File tree

3 files changed

+91
-20
lines changed

3 files changed

+91
-20
lines changed

src/components/ContentNode/LinkableHeading.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
:href="`#${anchor}`"
2020
class="header-anchor"
2121
aria-label="hidden"
22-
@click="handleFocusAndScroll(anchor)"
22+
@click="handleFocusAndScroll(anchor, { event: $event })"
2323
>#</a>
2424
<slot />
2525
</component>

src/mixins/scrollToElement.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,25 @@ export default {
3333
* and that it is in view, even if the hash is in the url
3434
* @returns {Promise<Element>}
3535
*/
36-
async handleFocusAndScroll(hash) {
36+
async handleFocusAndScroll(hash, preventDefault) {
3737
const element = document.getElementById(hash);
38+
// if no element to focus or scroll, exit
3839
if (!element) return;
40+
// create route hash
41+
const routeHash = `#${hash}`;
42+
// if preventDefault is true, prevent default to avoid jumping effect
43+
if (preventDefault && preventDefault.event) {
44+
preventDefault.event.preventDefault();
45+
// update the hash since default behavior was prevented
46+
// only if route hash is different that new
47+
if (this.$route.hash !== routeHash) {
48+
this.$router.push({ hash: routeHash });
49+
}
50+
}
3951
// Focus scrolls to the element
4052
element.focus();
4153
// but we need to compensate for the navigation height
42-
await this.scrollToElement(`#${hash}`);
54+
await this.scrollToElement(routeHash);
4355
},
4456
},
4557
};

tests/unit/mixins/scrollToElement.spec.js

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,49 @@
1111
import { shallowMount } from '@vue/test-utils';
1212
import scrollToElement from 'docc-render/mixins/scrollToElement';
1313
import * as loading from 'docc-render/utils/loading';
14+
import { createEvent as createBaseEvent } from '../../../test-utils';
1415

1516
const framesWait = jest.spyOn(loading, 'waitFrames');
1617

1718
describe('scrollToElement', () => {
1819
const scrollOffset = { x: 0, y: 14 };
1920
const anchor = 'heres-why';
20-
21-
const wrapper = shallowMount({
22-
name: 'MyComponent',
23-
mixins: [scrollToElement],
24-
render() {
25-
return `<div id="${anchor}"/>`;
26-
},
27-
}, {
28-
mocks: {
29-
$router: {
30-
resolve: ({ hash }) => ({ route: { hash } }),
31-
options: {
32-
scrollBehavior(to) {
33-
return new Promise(resolve => (
34-
resolve({ selector: to.hash, offset: scrollOffset })
35-
));
21+
const pushMock = jest.fn();
22+
const preventDefault = jest.fn();
23+
let wrapper;
24+
25+
const event = createBaseEvent('click');
26+
event.preventDefault = preventDefault;
27+
document.dispatchEvent(event);
28+
29+
const createWrapper = (extraMocks) => {
30+
wrapper = shallowMount({
31+
name: 'MyComponent',
32+
mixins: [scrollToElement],
33+
render() {
34+
return `<div id="${anchor}"/>`;
35+
},
36+
}, {
37+
mocks: {
38+
$router: {
39+
resolve: ({ hash }) => ({ route: { hash } }),
40+
push: ({ hash }) => pushMock(hash),
41+
options: {
42+
scrollBehavior(to) {
43+
return new Promise(resolve => (
44+
resolve({ selector: to.hash, offset: scrollOffset })
45+
));
46+
},
3647
},
3748
},
49+
...extraMocks,
3850
},
39-
},
51+
});
52+
};
53+
54+
beforeEach(() => {
55+
createWrapper();
56+
jest.clearAllMocks();
4057
});
4158

4259
it('scrolls to the correct element when "scrollToElement" is called', async () => {
@@ -89,6 +106,48 @@ describe('scrollToElement', () => {
89106
getElementSpy.mockRestore();
90107
});
91108

109+
it('prevents default event, if preventDefault provides an event', async () => {
110+
createWrapper({
111+
$route: {
112+
hash: '#foo',
113+
},
114+
});
115+
116+
const mockObject = { focus: jest.fn() };
117+
jest.spyOn(document, 'getElementById').mockReturnValue(mockObject);
118+
119+
await wrapper.vm.handleFocusAndScroll('#blah', { event });
120+
expect(preventDefault).toHaveBeenCalledTimes(1);
121+
});
122+
123+
it('pushes hash on router, if preventDefault is true and hash is different than current one', async () => {
124+
createWrapper({
125+
$route: {
126+
hash: '#foo',
127+
},
128+
});
129+
130+
const mockObject = { focus: jest.fn() };
131+
jest.spyOn(document, 'getElementById').mockReturnValue(mockObject);
132+
133+
await wrapper.vm.handleFocusAndScroll('blah', { event });
134+
expect(pushMock).toHaveBeenCalledTimes(1);
135+
});
136+
137+
it('does not push hash on router, if preventDefault is true and hash is the same as current one', async () => {
138+
createWrapper({
139+
$route: {
140+
hash: '#foo',
141+
},
142+
});
143+
144+
const mockObject = { focus: jest.fn() };
145+
jest.spyOn(document, 'getElementById').mockReturnValue(mockObject);
146+
147+
await wrapper.vm.handleFocusAndScroll('foo', { event });
148+
expect(pushMock).toHaveBeenCalledTimes(0);
149+
});
150+
92151
it('does not focus element and scroll if element is not in the document', async () => {
93152
wrapper.vm.scrollToElement = jest.fn();
94153
const hash = 'foo';

0 commit comments

Comments
 (0)