Skip to content

Commit 1d1886b

Browse files
Wait for the nav collapse animation to end before changing the language on mobile (#367)
closes rdar://94461604
1 parent e5c2b22 commit 1d1886b

File tree

6 files changed

+101
-20
lines changed

6 files changed

+101
-20
lines changed

src/components/DocumentationTopic/DocumentationNav.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
<span v-else class="nav-title-link inactive">Documentation</span>
4646
</slot>
4747
</template>
48-
<template slot="tray">
48+
<template #tray="{ closeNav }">
4949
<Hierarchy
5050
:currentTopicTitle="title"
5151
:isSymbolDeprecated="isSymbolDeprecated"
@@ -63,6 +63,7 @@
6363
:interfaceLanguage="interfaceLanguage"
6464
:objcPath="objcPath"
6565
:swiftPath="swiftPath"
66+
:closeNav="closeNav"
6667
/>
6768
<slot name="menu-items" />
6869
</NavMenuItems>

src/components/DocumentationTopic/DocumentationNav/LanguageToggle.vue

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,14 @@
6666
>
6767
{{ language.name }}
6868
</span>
69-
<router-link
69+
<a
7070
v-else
71+
href="#"
7172
class="nav-menu-link"
72-
:to="getRoute(language.route)"
73+
@click.prevent="pushRoute(language.route)"
7374
>
7475
{{ language.name }}
75-
</router-link>
76+
</a>
7677
</li>
7778
</ul>
7879
</div>
@@ -111,6 +112,10 @@ export default {
111112
type: String,
112113
required: false,
113114
},
115+
closeNav: {
116+
type: Function,
117+
default: () => {},
118+
},
114119
},
115120
data() {
116121
return {
@@ -160,7 +165,8 @@ export default {
160165
path: this.isCurrentPath(route.path) ? null : this.normalizePath(route.path),
161166
};
162167
},
163-
pushRoute(route) {
168+
async pushRoute(route) {
169+
await this.closeNav();
164170
// Persist the selected language as a preference in the store (backed by
165171
// the browser's local storage so that it can be retrieved later for
166172
// subsequent navigation without the query parameter present)

src/components/NavBase.vue

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
@transitionend.self="onTransitionEnd"
4646
@click="handleTrayClick"
4747
>
48-
<slot name="tray">
48+
<slot name="tray" :closeNav="closeNav">
4949
<NavMenuItems>
5050
<slot name="menu-items" />
5151
</NavMenuItems>
@@ -91,7 +91,7 @@ const NavStateClasses = {
9191
isDark: 'theme-dark',
9292
isOpen: 'nav--is-open',
9393
inBreakpoint: 'nav--in-breakpoint-range',
94-
isTransitioning: 'nav--is-opening',
94+
isTransitioning: 'nav--is-transitioning',
9595
isSticking: 'nav--is-sticking',
9696
hasSolidBackground: 'nav--solid-background',
9797
hasNoBorder: 'nav--noborder',
@@ -215,6 +215,20 @@ export default {
215215
},
216216
closeNav() {
217217
this.isOpen = false;
218+
return this.resolveOnceTransitionsEnd();
219+
},
220+
resolveOnceTransitionsEnd() {
221+
// if outside the breakpoint, resolve as there is no tray animation
222+
if (!this.inBreakpoint) return Promise.resolve();
223+
// enable the transitioning up tracking
224+
this.isTransitioning = true;
225+
// resolve a promise, when we stop transitioning
226+
return new Promise((resolve) => {
227+
const unwatch = this.$watch('isTransitioning', () => {
228+
resolve();
229+
unwatch();
230+
});
231+
});
218232
},
219233
/**
220234
* When the closing animation ends,
@@ -620,7 +634,7 @@ $content-max-width: map-deep-get($breakpoint-attributes, (nav, large, content-wi
620634
visibility: visible;
621635
transition-delay: 0.2s, 0s;
622636
623-
@include unify-selector('.nav--is-opening', $nested: true) {
637+
@include unify-selector('.nav--is-transitioning', $nested: true) {
624638
overflow-y: hidden;
625639
}
626640

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ describe('DocumentationNav', () => {
210210
interfaceLanguage: propsData.interfaceLanguage,
211211
swiftPath: propsData.swiftPath,
212212
objcPath: propsData.objcPath,
213+
closeNav: expect.any(Function),
213214
});
214215
});
215216

tests/unit/components/DocumentationTopic/DocumentationNav/LanguageToggle.spec.js

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ import Language from 'docc-render/constants/Language';
1414
import LanguageToggle
1515
from 'docc-render/components/DocumentationTopic/DocumentationNav/LanguageToggle.vue';
1616
import InlineChevronDownIcon from 'theme/components/Icons/InlineChevronDownIcon.vue';
17-
import { createEvent } from '../../../../../test-utils';
17+
import { createEvent, flushPromises } from '../../../../../test-utils';
1818

1919
const { NavMenuItemBase } = LanguageToggle.components;
2020

21+
const closeNav = jest.fn().mockResolvedValue('');
22+
2123
describe('LanguageToggle', () => {
2224
let wrapper;
2325

@@ -26,6 +28,7 @@ describe('LanguageToggle', () => {
2628
objcPath: 'documentation/foo',
2729
swiftPath: 'documentation/foo',
2830
breadcrumbCount: 1,
31+
closeNav,
2932
};
3033

3134
const mocks = {
@@ -58,6 +61,7 @@ describe('LanguageToggle', () => {
5861

5962
beforeEach(() => {
6063
wrapper = createWrapper();
64+
jest.clearAllMocks();
6165
});
6266

6367
it('renders `NavMenuItemBase` at the root', () => {
@@ -161,13 +165,15 @@ describe('LanguageToggle', () => {
161165
expect(options.at(1).text()).toBe(Language.objectiveC.name);
162166
});
163167

164-
it('calls router and changes v-model when different option is selected', () => {
168+
it('calls router and changes v-model when different option is selected', async () => {
165169
expect(wrapper.find('.current-language').text()).toBe(Language.swift.name);
166170

167171
wrapper.findAll('#language-toggle option').at(1).element.selected = true;
168172
wrapper.find('#language-toggle').trigger('change');
169173

170174
expect(wrapper.find('.current-language').text()).toBe(Language.objectiveC.name);
175+
expect(closeNav).toHaveBeenCalledTimes(1);
176+
await flushPromises();
171177
expect(mocks.$router.push).toHaveBeenCalledWith({ path: null, query: { language: 'objc' } });
172178
expect(provide.store.setPreferredLanguage).toHaveBeenCalledWith('objc');
173179
});
@@ -205,21 +211,37 @@ describe('LanguageToggle', () => {
205211
expect(currentLanguage.text()).toBe(Language.swift.name);
206212
});
207213

208-
it('renders a link for the alternative language inside `language-list-container`', () => {
214+
it('renders a link for the alternative language inside `language-list-container`', async () => {
209215
const link = wrapper.find('.language-list-container').find('a.nav-menu-link');
210216
expect(link.exists()).toBe(true);
211217
expect(link.text()).toBe(Language.objectiveC.name);
212-
expect(link.props('to').query).toEqual({ language: Language.objectiveC.key.url });
218+
link.trigger('click');
219+
expect(closeNav).toHaveBeenCalledTimes(1);
220+
await flushPromises();
221+
expect(mocks.$router.push)
222+
.toHaveBeenCalledWith({
223+
path: null,
224+
query: { language: Language.objectiveC.key.url },
225+
});
213226
});
214227

215-
it('clears out the language query if language is Swift', () => {
228+
it('clears out the language query if language is Swift', async () => {
216229
wrapper.setData({ languageModel: Language.objectiveC.key.api });
217230

218231
const link = wrapper.find('.language-list-container').find('a.nav-menu-link');
219-
expect(link.props('to').query).toEqual({ language: undefined });
232+
link.trigger('click');
233+
expect(closeNav).toHaveBeenCalledTimes(1);
234+
await flushPromises();
235+
expect(mocks.$router.push)
236+
.toHaveBeenCalledWith({
237+
path: null,
238+
query: {
239+
language: undefined,
240+
},
241+
});
220242
});
221243

222-
it('keeps extra query parameters when changing language', () => {
244+
it('keeps extra query parameters when changing language', async () => {
223245
const query = {
224246
foo: 'foo',
225247
bar: 'bar',
@@ -234,15 +256,29 @@ describe('LanguageToggle', () => {
234256
wrapper = createWrapper(undefined, mocksWithQuery);
235257

236258
const link = wrapper.find('.language-list-container').find('a.nav-menu-link');
237-
expect(link.props('to').query).toEqual(expect.objectContaining(query));
259+
link.trigger('click');
260+
expect(closeNav).toHaveBeenCalledTimes(1);
261+
await flushPromises();
262+
expect(mocks.$router.push)
263+
.toHaveBeenCalledWith({
264+
path: null,
265+
query: { ...query, language: Language.objectiveC.key.url },
266+
});
238267
});
239268

240-
it('renders different paths if languages have different paths', () => {
269+
it('renders different paths if languages have different paths', async () => {
241270
// Re-mount component to be able to update data() through new props
242271
wrapper = createWrapper({ ...propsData, objcPath: 'documentation/bar' });
243272

244273
const link = wrapper.find('.language-list-container').find('a.nav-menu-link');
245-
expect(link.props('to').path).toEqual('/documentation/bar');
274+
link.trigger('click');
275+
expect(closeNav).toHaveBeenCalledTimes(1);
276+
await flushPromises();
277+
expect(mocks.$router.push)
278+
.toHaveBeenCalledWith({
279+
path: '/documentation/bar',
280+
query: { language: Language.objectiveC.key.url },
281+
});
246282
});
247283

248284
it('changes the model, if the interfaceLanguage changes', () => {

tests/unit/components/NavBase.spec.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,14 +204,21 @@ describe('NavBase', () => {
204204
});
205205

206206
it('renders the `tray` slot', async () => {
207+
let slotProps = null;
207208
wrapper = await createWrapper({
208-
slots: {
209-
tray: '<div class="tray-slot">Tray slot content</div>',
209+
scopedSlots: {
210+
tray(props) {
211+
slotProps = props;
212+
return this.$createElement('div', { class: 'tray-slot' }, 'Tray slot content');
213+
},
210214
},
211215
});
212216
const tray = wrapper.find({ ref: 'tray' });
213217
expect(tray.find('.tray-slot').text()).toBe('Tray slot content');
214218
expect(wrapper.find(NavMenuItems).exists()).toBe(false);
219+
expect(slotProps).toEqual({
220+
closeNav: expect.any(Function),
221+
});
215222
});
216223

217224
it('renders the `menu-items` slot', async () => {
@@ -551,5 +558,21 @@ describe('NavBase', () => {
551558
wrapper.find(BreakpointEmitter).vm.$emit('change', BreakpointName.large);
552559
expect(wrapper.classes()).not.toContain(NavStateClasses.isOpen);
553560
});
561+
562+
it('resolves closeNav on transitionEnd, only when inside Breakpoint', async () => {
563+
wrapper = await createWrapper({
564+
data: () => ({ inBreakpoint: true, isOpen: true }),
565+
});
566+
expect(wrapper.classes()).toContain(NavStateClasses.isOpen);
567+
let resolved = false;
568+
wrapper.vm.closeNav().then(() => {
569+
resolved = true;
570+
});
571+
await wrapper.vm.$nextTick();
572+
expect(resolved).toBe(false);
573+
emitEndOfTrayTransition(wrapper);
574+
await wrapper.vm.$nextTick();
575+
expect(resolved).toBe(true);
576+
});
554577
});
555578
});

0 commit comments

Comments
 (0)