Skip to content

Commit 2e11342

Browse files
authored
Remove immediate parent in breadcrumbs if symbol belongs in a group of overloads (#790) rdar://125174518 and rdar://125092351
Remove immediate parent in breadcrumbs if symbol belongs in a group of overloads (#790) rdar://125174518 and rdar://125092351
1 parent 82382b4 commit 2e11342

File tree

10 files changed

+180
-322
lines changed

10 files changed

+180
-322
lines changed

src/components/DocumentationTopic.vue

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@
3131
</template>
3232
<slot name="above-title" />
3333
<Hierarchy
34-
v-if="hierarchyItems.length && !enableMinimized"
34+
v-if="parentTopics.length && !enableMinimized && !isTargetIDE"
3535
:currentTopicTitle="title"
3636
:isSymbolDeprecated="isSymbolDeprecated"
3737
:isSymbolBeta="isSymbolBeta"
38-
:parentTopicIdentifiers="hierarchyItems"
38+
:parentTopics="parentTopics"
3939
:currentTopicTags="tags"
4040
/>
4141
<LanguageSwitcher
@@ -89,7 +89,7 @@
8989
:class="{ 'no-primary-content': !hasPrimaryContent && enhanceBackground }"
9090
>
9191
<div
92-
v-if="hasPrimaryContent || hasOtherDeclarations"
92+
v-if="hasPrimaryContent || showOtherDeclarations"
9393
:class="['container', { 'minimized-container': enableMinimized }]"
9494
>
9595
<div
@@ -111,7 +111,7 @@
111111
<ContentNode :content="downloadNotAvailableSummary" />
112112
</Aside>
113113
</div>
114-
<div v-if="hasOtherDeclarations" class="declaration-list-menu">
114+
<div v-if="showOtherDeclarations" class="declaration-list-menu">
115115
<button
116116
class="declaration-list-toggle"
117117
@click="toggleDeclList"
@@ -174,6 +174,7 @@ import Language from 'docc-render/constants/Language';
174174
import metadata from 'theme/mixins/metadata';
175175
import { buildUrl } from 'docc-render/utils/url-helper';
176176
import { normalizeRelativePath } from 'docc-render/utils/assets';
177+
import { last } from 'docc-render/utils/arrays';
177178
178179
import AppStore from 'docc-render/stores/AppStore';
179180
import Aside from 'docc-render/components/ContentNode/Aside.vue';
@@ -448,6 +449,30 @@ export default {
448449
pageDescription: ({ abstract, extractFirstParagraphText }) => (
449450
abstract ? extractFirstParagraphText(abstract) : null
450451
),
452+
parentTopics: ({
453+
hierarchyItems,
454+
references,
455+
hasOtherDeclarations,
456+
pageTitle,
457+
}) => {
458+
const parentTopics = hierarchyItems.reduce((all, id) => {
459+
const reference = references[id];
460+
if (reference) {
461+
const { title, url } = reference;
462+
return all.concat({ title, url });
463+
}
464+
console.error(`Reference for "${id}" is missing`);
465+
return all;
466+
}, []);
467+
468+
// Overloaded symbols are auto-grouped under a group page with the same title
469+
// We should omit showing their immediate parent to avoid confusion and duplication
470+
const immediateParent = last(parentTopics);
471+
if (hasOtherDeclarations && immediateParent?.title === pageTitle) {
472+
parentTopics.pop();
473+
}
474+
return parentTopics;
475+
},
451476
shouldShowLanguageSwitcher: ({
452477
objcPath,
453478
swiftPath,
@@ -553,9 +578,12 @@ export default {
553578
declarations({ primaryContentSections = [] }) {
554579
return primaryContentSections.filter(({ kind }) => kind === SectionKind.declarations);
555580
},
556-
hasOtherDeclarations({ declarations = [], enableMinimized }) {
581+
showOtherDeclarations({ enableMinimized, hasOtherDeclarations }) {
557582
// disable otherDeclarations in minimized mode
558-
return !enableMinimized && declarations.length
583+
return !enableMinimized && hasOtherDeclarations;
584+
},
585+
hasOtherDeclarations({ declarations = [] }) {
586+
return !!declarations.length
559587
// there's always only 1 `declaration` at this level
560588
&& declarations[0].declarations.some(decl => Object.prototype.hasOwnProperty.call(decl, 'otherDeclarations'));
561589
},

src/components/DocumentationTopic/Hero/Hierarchy.vue

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export default {
116116
type: String,
117117
required: true,
118118
},
119-
parentTopicIdentifiers: {
119+
parentTopics: {
120120
type: Array,
121121
default: () => [],
122122
},
@@ -127,17 +127,6 @@ export default {
127127
},
128128
computed: {
129129
windowWidth: ({ store }) => store.state.contentWidth,
130-
parentTopics() {
131-
return this.parentTopicIdentifiers.reduce((all, id) => {
132-
const reference = this.references[id];
133-
if (reference) {
134-
const { title, url } = reference;
135-
return all.concat({ title, url });
136-
}
137-
console.error(`Reference for "${id}" is missing`);
138-
return all;
139-
}, []);
140-
},
141130
root: ({ parentTopics }) => parentTopics[0],
142131
smallViewport: ({ windowWidth }) => (windowWidth < BreakpointAttributes.default.small.maxWidth),
143132
/**

src/components/Navigator.vue

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
:render-filter-on-top="renderFilterOnTop"
2525
:api-changes="apiChanges"
2626
:navigator-references="navigatorReferences"
27-
:isSpecificOverload="isSpecificOverload"
2827
@close="$emit('close')"
2928
>
3029
<template #filter><slot name="filter" /></template>
@@ -49,7 +48,6 @@ import NavigatorCard from 'theme/components/Navigator/NavigatorCard.vue';
4948
import LoadingNavigatorCard from 'theme/components/Navigator/LoadingNavigatorCard.vue';
5049
import { INDEX_ROOT_KEY } from 'docc-render/constants/sidebar';
5150
import { TopicTypes } from 'docc-render/constants/TopicTypes';
52-
import { last } from 'docc-render/utils/arrays';
5351
5452
/**
5553
* @typedef NavigatorFlatItem
@@ -125,9 +123,6 @@ export default {
125123
type: Object,
126124
default: null,
127125
},
128-
symbolKind: {
129-
default: () => undefined,
130-
},
131126
},
132127
computed: {
133128
// gets the paths for each parent in the breadcrumbs
@@ -154,21 +149,6 @@ export default {
154149
}
155150
return parentTopicReferences.slice(itemsToSlice).map(r => r.url).concat(path);
156151
},
157-
/**
158-
* Symbol pages always have a symbolKind
159-
*/
160-
isSymbol: ({ symbolKind }) => !!symbolKind,
161-
/**
162-
* Only symbol pages with overloads can have a valid hash:
163-
* less than 5 char, only lower case letter and number
164-
*/
165-
isSpecificOverload({ $route: { path }, isSymbol }) {
166-
// Ensure the path does not have a trailing slash
167-
// eslint-disable-next-line no-param-reassign
168-
path = path.replace(/\/$/, '');
169-
const hash = isSymbol ? last(path.split('-')) : '';
170-
return /^[a-z0-9]{1,5}$/.test(hash);
171-
},
172152
/**
173153
* The root item is always a module
174154
*/

src/components/Navigator/NavigatorCard.vue

Lines changed: 17 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -226,10 +226,6 @@ export default {
226226
type: Boolean,
227227
default: false,
228228
},
229-
isSpecificOverload: {
230-
type: Boolean,
231-
default: false,
232-
},
233229
},
234230
mixins: [
235231
keyboardNavigation, filteredChildrenMixin, tagsProvider,
@@ -914,74 +910,37 @@ export default {
914910
if (lastActivePathItem === currentActiveItem.path) {
915911
return;
916912
}
917-
// try to match current open item in its surroundings, starting with active item
918-
// set found match as active item
919-
if (this.matchInSurroundingItems(this.activeUID, lastActivePathItem)) return;
913+
// Get the surrounding items
914+
const siblings = getSiblings(this.activeUID, this.childrenMap, this.children);
915+
const children = getChildren(this.activeUID, this.childrenMap, this.children);
916+
const parents = getParents(this.activeUID, this.childrenMap);
917+
// try to match if any of the `siblings`,`children` or any of the `parents`,
918+
// match the current open item
919+
const matchingItem = [...children, ...siblings, ...parents]
920+
.find(child => child.path === lastActivePathItem);
920921
921-
if (this.isSpecificOverload) {
922-
// if no match, try again to match with generic item
923-
// Needed for continuing to highlight current generic page
924-
// when selecting an overload from dropdown that's also specifically curated in elsewhere
925-
const genericItem = this.getGenericPath(lastActivePathItem);
926-
if (this.matchInSurroundingItems(this.activeUID, genericItem)) return;
922+
// set the match as an active item
923+
if (matchingItem) {
924+
this.setActiveUID(matchingItem.uid);
925+
return;
927926
}
928927
}
929-
// There is no match to base upon, so we need to search the whole tree
930-
// by matching each level of the hierachy in activePath
928+
// There is no match to base upon, so we need to search
929+
// across the activePath for the active item.
931930
const activePathChildren = this.pathsToFlatChildren(activePath);
932-
// if there are items, set new active UID
931+
// if there are items, set the new active UID
933932
if (activePathChildren.length) {
934-
const lastChildrenUID = last(activePathChildren).uid;
935-
936-
if (last(activePathChildren).path !== lastActivePathItem && this.isSpecificOverload) {
937-
// if item is not found in the tree and its a specific overloaded symbol page
938-
// try to match with its generics page instead
939-
const genericItem = this.getGenericPath(lastActivePathItem);
940-
if (this.matchInSurroundingItems(lastChildrenUID, genericItem)) return;
941-
}
942-
943-
// Set new active UID to the last matched item
944-
// Note: if a match is not found, last matched ancestor is highlighted
945-
this.setActiveUID(lastChildrenUID);
933+
this.setActiveUID(activePathChildren[activePathChildren.length - 1].uid);
946934
return;
947935
}
948-
// if there is an activeUID, but still no match found in tree
949-
// unset it, as we probably navigated back to the root
936+
// if there is an activeUID, unset it, as we probably navigated back to the root
950937
if (this.activeUID) {
951938
this.setActiveUID(null);
952939
return;
953940
}
954941
// Just track the open nodes, as setting the activeUID as null wont do anything.
955942
this.trackOpenNodes(this.nodeChangeDeps);
956943
},
957-
/**
958-
* Try to match if any of the `siblings`,`children`, `parents`
959-
* of the given UID match the given path.
960-
* Set active UID once a match is found
961-
*/
962-
matchInSurroundingItems(UID, path) {
963-
// Get the surrounding items
964-
const siblings = getSiblings(UID, this.childrenMap, this.children);
965-
const children = getChildren(UID, this.childrenMap, this.children);
966-
const parents = getParents(UID, this.childrenMap);
967-
968-
const matchingItem = [...children, ...siblings, ...parents]
969-
.find(child => child.path === path);
970-
971-
// set the match as an active item
972-
if (matchingItem) {
973-
this.setActiveUID(matchingItem.uid);
974-
return true;
975-
}
976-
return false;
977-
},
978-
/**
979-
* Parse out the generic path given a
980-
* specific overload path with a valid hash
981-
*/
982-
getGenericPath(path) {
983-
return path.slice(0, path.lastIndexOf('-'));
984-
},
985944
/**
986945
* Updates the current focusIndex, based on where the activeUID is.
987946
* If not in the rendered items, we set it to 0.

src/views/DocumentationTopic.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@
6262
:error-fetching="slotProps.errorFetching"
6363
:api-changes="slotProps.apiChanges"
6464
:references="topicProps.references"
65-
:symbolKind="topicProps.symbolKind"
6665
:navigator-references="slotProps.references"
6766
:scrollLockID="scrollLockID"
6867
:render-filter-on-top="breakpoint !== BreakpointName.large"

tests/unit/components/DocumentationTopic.spec.js

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,26 @@ const sampleCodeDownload = {
108108
},
109109
};
110110

111+
const itemFoo = {
112+
title: 'Foo',
113+
url: '/documentation/foo',
114+
};
115+
116+
const itemBar = {
117+
title: 'Bar',
118+
url: '/documentation/bar',
119+
};
120+
121+
const hierarchyItems = [
122+
'topic://foo',
123+
'topic://bar',
124+
];
125+
126+
const hierarchyItemsReferences = {
127+
'topic://foo': itemFoo,
128+
'topic://bar': itemBar,
129+
};
130+
111131
const propsData = {
112132
abstract: [abstract],
113133
conformance: { constraints: [], availabilityPrefix: [] },
@@ -153,10 +173,7 @@ const propsData = {
153173
path: 'foo',
154174
query: {},
155175
},
156-
hierarchyItems: [
157-
'topic://foo',
158-
'topic://bar',
159-
],
176+
hierarchyItems: [],
160177
};
161178

162179
const hasOtherDeclSection = {
@@ -319,17 +336,71 @@ describe('DocumentationTopic', () => {
319336
});
320337

321338
it('renders a Hierarchy', () => {
339+
wrapper.setProps({
340+
references: hierarchyItemsReferences,
341+
hierarchyItems,
342+
});
322343
const hierarchy = wrapper.find(Hierarchy);
323344
expect(hierarchy.exists()).toBe(true);
324345
expect(hierarchy.props()).toEqual({
325346
currentTopicTitle: propsData.title,
326-
parentTopicIdentifiers: propsData.hierarchyItems,
347+
parentTopics: [itemFoo, itemBar],
327348
isSymbolBeta: false,
328349
isSymbolDeprecated: false,
329350
currentTopicTags: propsData.tags,
330351
});
331352
});
332353

354+
it('does not render `Hierarchy` in IDE', () => {
355+
wrapper = shallowMount(DocumentationTopic, {
356+
propsData: {
357+
...propsData,
358+
references: hierarchyItemsReferences,
359+
hierarchyItems,
360+
},
361+
provide: {
362+
isTargetIDE: true,
363+
store: mockStore,
364+
},
365+
});
366+
367+
const hierarchy = wrapper.find(Hierarchy);
368+
expect(hierarchy.exists()).toBe(false);
369+
});
370+
371+
it('renders `Hierarchy` without its immediate parent if its within overload group', () => {
372+
wrapper.setProps({
373+
references: hierarchyItemsReferences,
374+
hierarchyItems,
375+
primaryContentSections: [
376+
...propsData.primaryContentSections,
377+
hasOtherDeclSection,
378+
],
379+
});
380+
381+
let hierarchy = wrapper.find(Hierarchy);
382+
// Don't hide immediate parent yet if has other declarations but different titles
383+
expect(hierarchy.props()).toHaveProperty('parentTopics', [itemFoo, itemBar]);
384+
385+
// Hide immediate parent if has same title as parent and has other declarations
386+
wrapper.setProps({ title: itemBar.title });
387+
hierarchy = wrapper.find(Hierarchy);
388+
expect(hierarchy.props()).toHaveProperty('parentTopics', [itemFoo]);
389+
});
390+
391+
it('`Hierarchy` continues working, if a reference is missing', () => {
392+
const errorSpy = jest.spyOn(console, 'error').mockReturnValue('');
393+
wrapper.setProps({
394+
references: { 'topic://foo': itemFoo }, // set without `Bar` reference data
395+
hierarchyItems,
396+
});
397+
398+
const hierarchy = wrapper.find(Hierarchy);
399+
expect(hierarchy.exists()).toBe(true);
400+
expect(errorSpy).toHaveBeenCalledTimes(1);
401+
expect(errorSpy).toHaveBeenCalledWith('Reference for "topic://bar" is missing');
402+
});
403+
333404
it('does not render a Hierarchy if hierarchyItems is empty or enableMinimized is true', () => {
334405
wrapper.setProps({ hierarchyItems: [] });
335406
expect(wrapper.find(Hierarchy).exists()).toBe(false);

0 commit comments

Comments
 (0)