Skip to content

Commit f766c5d

Browse files
committed
[r96943502] fix: address feedback
1 parent 022689b commit f766c5d

File tree

18 files changed

+68
-65
lines changed

18 files changed

+68
-65
lines changed

src/components/ContentNode/SectionTitle.vue

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,51 +10,57 @@
1010

1111
<template>
1212
<component
13-
:id="anchor ? anchor : null"
13+
:id="id"
1414
:is="tag"
1515
class="section-title"
1616
>
1717
<a
18-
v-if="anchor || href"
19-
:href="`#${anchor || href}`"
18+
v-if="anchor"
19+
:href="`#${anchor}`"
2020
class="header-anchor"
2121
aria-label="hidden"
22+
@click="handleFocusAndScroll(anchor)"
2223
>#</a>
2324
<slot />
2425
</component>
2526
</template>
2627

2728
<script>
29+
import scrollToElement from 'docc-render/mixins/scrollToElement';
30+
2831
export default {
2932
name: 'SectionTitle',
33+
mixins: [scrollToElement],
34+
data() {
35+
return {
36+
id: null,
37+
};
38+
},
3039
props: {
3140
anchor: {
3241
type: String,
3342
required: false,
3443
},
35-
href: {
36-
type: String,
37-
required: false,
38-
},
3944
tag: {
4045
type: String,
4146
default: () => 'h2',
4247
},
4348
},
49+
mounted() {
50+
if (!this.anchor) return;
51+
const element = document.getElementById(`${this.anchor}`);
52+
// if there is no element in the document that has an id,
53+
// add it to this component
54+
if (!element) {
55+
this.id = this.anchor;
56+
}
57+
},
4458
};
4559
</script>
4660

4761
<style scoped lang="scss">
4862
@import 'docc-render/styles/_core.scss';
4963
50-
.section-title {
51-
scroll-margin-top: $nav-height + 1rem;
52-
53-
@include breakpoint(small, $scope: nav) {
54-
scroll-margin-top: $nav-height-small + 1rem;
55-
}
56-
}
57-
5864
.section-title:hover {
5965
.header-anchor {
6066
opacity: 1;
@@ -68,7 +74,7 @@ export default {
6874
opacity: 0;
6975
text-decoration: none;
7076
71-
&:hover {
77+
&:hover, &:focus {
7278
opacity: 1;
7379
}
7480
}

src/components/DocumentationTopic/ContentTable.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
:title="title"
1616
>
1717
<div class="container">
18-
<SectionTitle class="title" :href="anchor">{{ title }}</SectionTitle>
18+
<SectionTitle class="title" :anchor="anchor">{{ title }}</SectionTitle>
1919
<slot />
2020
</div>
2121
</OnThisPageSection>

src/components/DocumentationTopic/ContentTableSection.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
<script>
2727
import SectionTitle from 'docc-render/components/ContentNode/SectionTitle.vue';
28+
import { anchorize } from 'docc-render/utils/strings';
2829
2930
export default {
3031
name: 'ContentTableSection',
@@ -36,7 +37,7 @@ export default {
3637
},
3738
},
3839
computed: {
39-
anchor: ({ title }) => title.replaceAll(' ', '-'),
40+
anchor: ({ title }) => anchorize(title),
4041
},
4142
};
4243
</script>

src/components/DocumentationTopic/PrimaryContent/Declaration.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
class="declaration"
1515
title="Declaration"
1616
>
17-
<SectionTitle href="declaration">Declaration</SectionTitle>
17+
<SectionTitle anchor="declaration">Declaration</SectionTitle>
1818
<template v-if="hasModifiedChanges">
1919
<DeclarationDiff
2020
:class="[changeClasses, multipleLinesClass]"

src/components/DocumentationTopic/PrimaryContent/Parameters.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
class="parameters"
1515
title="Parameters"
1616
>
17-
<SectionTitle href="parameters">Parameters</SectionTitle>
17+
<SectionTitle anchor="parameters">Parameters</SectionTitle>
1818
<dl>
1919
<template v-for="param in parameters">
2020
<dt class="param-name" :key="`${param.name}:name`">

src/components/DocumentationTopic/PrimaryContent/PropertyTable.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
<template>
1212
<OnThisPageSection :anchor="anchor" :title="title">
13-
<SectionTitle :href="anchor">{{ title }}</SectionTitle>
13+
<SectionTitle :anchor="anchor">{{ title }}</SectionTitle>
1414
<ParametersTable :parameters="properties" :changes="propertyChanges" class="property-table">
1515
<template slot="symbol" slot-scope="{ name, type, content, changes, deprecated }">
1616
<div class="property-name" :class="{ deprecated: deprecated }">

src/components/DocumentationTopic/PrimaryContent/RestBody.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
<template>
1212
<OnThisPageSection :anchor="anchor" :title="title">
13-
<SectionTitle :href="anchor">{{ title }}</SectionTitle>
13+
<SectionTitle :anchor="anchor">{{ title }}</SectionTitle>
1414
<ParametersTable :parameters="[bodyParam]" :changes="bodyChanges" keyBy="key">
1515
<template slot="symbol" slot-scope="{ type, content, changes, name }">
1616
<PossiblyChangedType

src/components/DocumentationTopic/PrimaryContent/RestEndpoint.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
<template>
1212
<OnThisPageSection :anchor="anchor" :title="title">
13-
<SectionTitle :href="anchor">{{ title }}</SectionTitle>
13+
<SectionTitle :anchor="anchor">{{ title }}</SectionTitle>
1414
<DeclarationSource :tokens="tokens" />
1515
</OnThisPageSection>
1616
</template>

src/components/DocumentationTopic/PrimaryContent/RestParameters.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
<template>
1212
<OnThisPageSection :anchor="anchor" :title="title">
13-
<SectionTitle :href="anchor">{{ title }}</SectionTitle>
13+
<SectionTitle :anchor="anchor">{{ title }}</SectionTitle>
1414
<ParametersTable :parameters="parameters" :changes="parameterChanges">
1515
<template slot="symbol" slot-scope="{ name, type, content, changes, deprecated }">
1616
<div class="param-name" :class="{ deprecated: deprecated }">

src/components/DocumentationTopic/PrimaryContent/RestResponses.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
<template>
1212
<OnThisPageSection :anchor="anchor" :title="title">
13-
<SectionTitle :href="anchor">{{ title }}</SectionTitle>
13+
<SectionTitle :anchor="anchor">{{ title }}</SectionTitle>
1414
<ParametersTable :parameters="responses" :changes="propertyChanges" key-by="status">
1515
<template slot="symbol" slot-scope="{ status, type, reason, content, changes }">
1616
<div class="response-name">

src/components/DocumentationTopic/TopicsTable.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
:title="section.title"
1717
>
1818
<template v-if="wrapTitle" slot="title">
19-
<SectionTitle tag="h3" :href="anchor" class="title">
19+
<SectionTitle tag="h3" :anchor="anchor" class="title">
2020
<WordBreak>{{ section.title }}</WordBreak>
2121
</SectionTitle>
2222
</template>

src/components/Tutorial/NavigationBar.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,8 @@ export default {
174174
methods: {
175175
onSelectSection(path) {
176176
// Manually scroll to the section to work around a vue-router bug: https://github.com/vuejs/vue-router/issues/1668
177-
const hash = `#${path.split('#')[1]}`;
178-
this.scrollToElement(hash);
177+
const hash = path.split('#')[1];
178+
this.handleFocusAndScroll(hash);
179179
},
180180
},
181181
};

src/components/TutorialsOverview/TutorialsNavigationLink.vue

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
class="tutorials-navigation-link"
1414
:class="{ active }"
1515
:to="fragment"
16-
@click.native="handleFocus"
16+
@click.native="handleFocusAndScroll(fragment.hash)"
1717
>
1818
<slot />
1919
</router-link>
@@ -42,23 +42,6 @@ export default {
4242
fragment: ({ text, $route }) => ({ hash: anchorize(text), query: $route.query }),
4343
text: ({ $slots: { default: [{ text: slotText }] } }) => slotText.trim(),
4444
},
45-
methods: {
46-
/**
47-
* Always scroll to the element and focus it.
48-
* This ensures that the next tab target is inside
49-
* and that it is in view, even if the hash is in the url
50-
* @returns {Promise<void>}
51-
*/
52-
async handleFocus() {
53-
const { hash } = this.fragment;
54-
const element = document.getElementById(hash);
55-
if (!element) return;
56-
// Focus scrolls to the element
57-
element.focus();
58-
// but we need to compensate for the navigation height
59-
await this.scrollToElement(`#${hash}`);
60-
},
61-
},
6245
};
6346
</script>
6447

src/mixins/scrollToElement.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,19 @@ export default {
2727
window.scrollBy(-offset.x, -offset.y);
2828
return element;
2929
},
30+
/**
31+
* Always scroll to the element and focus it.
32+
* This ensures that the next tab target is inside
33+
* and that it is in view, even if the hash is in the url
34+
* @returns {Promise<void>}
35+
*/
36+
async handleFocusAndScroll(hash) {
37+
const element = document.getElementById(hash);
38+
if (!element) return;
39+
// Focus scrolls to the element
40+
element.focus();
41+
// but we need to compensate for the navigation height
42+
await this.scrollToElement(`#${hash}`);
43+
},
3044
},
3145
};

tests/unit/components/ContentNode/SectionTitle.spec.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,26 +30,32 @@ describe('SectionTitle', () => {
3030
expect(wrapper.is('h3')).toBe(true);
3131
});
3232

33-
it('renders a section title with a header anchor and an id on the wrapper', () => {
33+
it('renders a section title with a header anchor and an id on the wrapper', async () => {
3434
const wrapper = shallowMount(SectionTitle, {
3535
propsData: {
3636
tag: 'h2',
3737
anchor: 'title',
3838
},
3939
slots: { default: 'Title' },
4040
});
41+
await wrapper.vm.$nextTick();
4142
expect(wrapper.text()).toBe('# Title');
4243
expect(wrapper.attributes('id')).toBe('title');
4344
const headerAnchor = wrapper.find('.header-anchor');
4445
expect(headerAnchor.attributes('href')).toBe('#title');
4546
expect(headerAnchor.attributes('aria-label')).toBe('hidden');
4647
});
4748

48-
it('renders a section title with a header anchor and no id on the wrapper', () => {
49+
it('renders a section title with a header anchor and no id on the wrapper if it already exists on the document', () => {
50+
// create element with id outside the component
51+
const div = document.createElement('div');
52+
div.innerHTML = '<div id="title>';
53+
document.body.appendChild(div);
54+
4955
const wrapper = shallowMount(SectionTitle, {
5056
propsData: {
5157
tag: 'h2',
52-
href: 'title',
58+
anchor: 'title',
5359
},
5460
slots: { default: 'Title' },
5561
});
@@ -60,7 +66,7 @@ describe('SectionTitle', () => {
6066
expect(headerAnchor.attributes('aria-label')).toBe('hidden');
6167
});
6268

63-
it('does not render anchor if there is no anchor or href props', () => {
69+
it('does not render anchor if there is no anchor', () => {
6470
const wrapper = shallowMount(SectionTitle);
6571
expect(wrapper.find('.header-anchor').exists()).toBe(false);
6672
});

tests/unit/components/DocumentationTopic/TopicsTable.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,6 @@ describe('TopicsTable', () => {
145145
expect(wordBreak.text()).toEqual(propsData.sections[0].title);
146146
expect(sectionTitle.exists()).toBe(true);
147147
expect(sectionTitle.attributes('tag')).toBe('h3');
148-
expect(sectionTitle.attributes('href')).toBe('topics');
148+
expect(sectionTitle.attributes('anchor')).toBe('topics');
149149
});
150150
});

tests/unit/components/Tutorial/NavigationBar.spec.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { flushPromises } from '../../../../test-utils';
1919

2020
jest.mock('docc-render/mixins/scrollToElement');
2121

22-
scrollToElement.methods.scrollToElement.mockResolvedValue(true);
22+
scrollToElement.methods.handleFocusAndScroll.mockResolvedValue(true);
2323

2424
const {
2525
PrimaryDropdown,
@@ -241,16 +241,16 @@ describe('NavigationBar', () => {
241241
const dropdown = wrapper.find(MobileDropdown);
242242
dropdown.vm.$emit('select-section', 'path/to/item#section-foo');
243243
await flushPromises();
244-
expect(scrollToElement.methods.scrollToElement).toHaveBeenCalledTimes(1);
245-
expect(scrollToElement.methods.scrollToElement).toHaveBeenCalledWith('#section-foo');
244+
expect(scrollToElement.methods.handleFocusAndScroll).toHaveBeenCalledTimes(1);
245+
expect(scrollToElement.methods.handleFocusAndScroll).toHaveBeenCalledWith('section-foo');
246246
});
247247

248248
it('scrolls to a section, on `@select-section`, on SecondaryDropdown', async () => {
249249
const dropdown = wrapper.find(SecondaryDropdown);
250250
dropdown.vm.$emit('select-section', 'path/to/item#section-foo');
251251
await flushPromises();
252-
expect(scrollToElement.methods.scrollToElement).toHaveBeenCalledTimes(1);
253-
expect(scrollToElement.methods.scrollToElement).toHaveBeenCalledWith('#section-foo');
252+
expect(scrollToElement.methods.handleFocusAndScroll).toHaveBeenCalledTimes(1);
253+
expect(scrollToElement.methods.handleFocusAndScroll).toHaveBeenCalledWith('section-foo');
254254
});
255255

256256
it('renders a "Primary Dropdown" with chapters', () => {

tests/unit/components/TutorialsOverview/TutorialsNavigationLink.spec.js

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import TutorialsNavigationLink
1717
import scrollToElement from 'docc-render/mixins/scrollToElement';
1818

1919
jest.mock('docc-render/mixins/scrollToElement', () => ({
20-
methods: { scrollToElement: jest.fn() },
20+
methods: { handleFocusAndScroll: jest.fn() },
2121
}));
2222

2323
describe('TutorialsNavigationLink', () => {
@@ -79,16 +79,9 @@ describe('TutorialsNavigationLink', () => {
7979
});
8080

8181
it('focuses the element when clicked, used for AX', async () => {
82-
const mockObject = { focus: jest.fn() };
83-
const getElementSpy = jest.spyOn(document, 'getElementById').mockReturnValue(mockObject);
84-
const spy = scrollToElement.methods.scrollToElement.mockReturnValueOnce(mockObject);
8582
const link = wrapper.find(RouterLinkStub);
8683
link.trigger('click');
8784
await wrapper.vm.$nextTick();
88-
expect(scrollToElement.methods.scrollToElement).toHaveBeenCalledWith('#hello-world');
89-
expect(mockObject.focus).toHaveBeenCalledTimes(1);
90-
expect(getElementSpy).toHaveBeenCalledTimes(1);
91-
spy.mockRestore();
92-
getElementSpy.mockRestore();
85+
expect(scrollToElement.methods.handleFocusAndScroll).toHaveBeenCalledWith('hello-world');
9386
});
9487
});

0 commit comments

Comments
 (0)