Skip to content

Commit ee65681

Browse files
committed
feat(theme): add nested categories in mobile category menu
1 parent f5ec474 commit ee65681

File tree

10 files changed

+249
-122
lines changed

10 files changed

+249
-122
lines changed

packages/theme/components/BottomNavigation.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
</template>
7373
</SfBottomNavigationItem>
7474
</SfBottomNavigation>
75-
<MobileMenuSidebar />
75+
<MobileCategorySidebar />
7676
</div>
7777
</template>
7878

@@ -81,14 +81,14 @@ import { SfBottomNavigation, SfCircleIcon } from '@storefront-ui/vue';
8181
import { useUser } from '@vue-storefront/magento';
8282
import { defineComponent, useRouter, useContext } from '@nuxtjs/composition-api';
8383
import { useUiState } from '~/composables';
84-
import MobileMenuSidebar from '~/components/MobileMenuSidebar.vue';
8584
import SvgImage from '~/components/General/SvgImage.vue';
85+
import MobileCategorySidebar from '~/modules/catalog/category/components/sidebar/MobileCategorySidebar/MobileCategorySidebar.vue';
8686
8787
export default defineComponent({
8888
components: {
8989
SfBottomNavigation,
9090
SfCircleIcon,
91-
MobileMenuSidebar,
91+
MobileCategorySidebar,
9292
SvgImage,
9393
},
9494
setup() {

packages/theme/components/MobileMenuSidebar.vue

Lines changed: 0 additions & 78 deletions
This file was deleted.

packages/theme/lang/de.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,4 +253,5 @@ export default {
253253
"There was some error while trying to fetch shipping methods. We are sorry, please try with different shipping details.": "Beim Versuch, Versandarten abzurufen, ist ein Fehler aufgetreten. Es tut uns leid, bitte versuchen Sie es mit anderen Versanddetails oder später.",
254254
"There was some error while trying to select this shipping method. We are sorry, please try with different shipping method.": "Beim Versuch, diese Versandart auszuwählen, ist ein Fehler aufgetreten. Es tut uns leid, bitte versuchen Sie es mit einer anderen Versandart.",
255255
"We can't find products matching the selection.":"Wir können keine Produkte finden, die der Auswahl entsprechen.",
256+
"AllProductsFromCategory": "Alle {categoryName}"
256257
};

packages/theme/lang/en.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,4 +251,5 @@ export default {
251251
"There was some error while trying to fetch shipping methods. We are sorry, please try with different shipping details.": "There was some error while trying to fetch shipping methods. We are sorry, please try with different shipping details.",
252252
"There was some error while trying to select this shipping method. We are sorry, please try with different shipping method.": "There was some error while trying to select this shipping method. We are sorry, please try with different shipping method.",
253253
"We can't find products matching the selection.":"We can't find products matching the selection.",
254+
"AllProductsFromCategory": "All {categoryName}"
254255
};

packages/theme/modules/catalog/category/components/sidebar/CategorySidebar.vue

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ import {
6565
useRoute,
6666
} from '@nuxtjs/composition-api';
6767
68-
import { useSidebar } from './useSidebar.ts';
6968
import { useUiHelpers } from '~/composables';
69+
import { loadCategoryTree, findActiveCategoryLabel } from '~/modules/catalog/category/helpers/buildCategoryTree';
7070
import SkeletonLoader from '~/components/SkeletonLoader';
7171
7272
export default defineComponent({
@@ -83,11 +83,10 @@ export default defineComponent({
8383
const activeCategory = ref('');
8484
const isLoading = ref(true);
8585
const route = useRoute();
86-
const { loadCategoryTree, findActiveCategory } = useSidebar();
8786
8887
onMounted(async () => {
8988
categoryTree.value = await loadCategoryTree() ?? {};
90-
activeCategory.value = findActiveCategory(categoryTree.value, route.value.fullPath);
89+
activeCategory.value = findActiveCategoryLabel(categoryTree.value, route.value.fullPath);
9190
isLoading.value = false;
9291
});
9392
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<template>
2+
<SfSidebar
3+
:visible="isMobileMenuOpen"
4+
:title="currentCategory && currentCategory.label || $t('Menu')"
5+
class="mobile-menu-sidebar sf-sidebar--left"
6+
@close="toggleMobileMenu"
7+
>
8+
<SfList class="mobile-menu-sidebar__list">
9+
<template v-if="currentCategory">
10+
<SfMenuItem
11+
class="mobile-menu-sidebar__item"
12+
:label="$i18n.t('Go back')"
13+
@click="onGoCategoryUp()"
14+
>
15+
<template #mobile-nav-icon>
16+
<div v-show="false" />
17+
</template>
18+
</SfMenuItem>
19+
20+
<SfMenuItem
21+
class="mobile-menu-sidebar__item"
22+
:label="$i18n.t('AllProductsFromCategory', { categoryName: currentCategory.label })"
23+
:count="currentCategory.count"
24+
@click="navigate(currentCategory)"
25+
/>
26+
</template>
27+
<SfMenuItem
28+
v-for="(category, index) in currentItems || categoryTree"
29+
:key="index"
30+
:label="category.label"
31+
:count="category.count"
32+
class="mobile-menu-sidebar__item"
33+
@click="category.items.length === 0 ? navigate(category) : onGoCategoryDown(category)"
34+
/>
35+
</SfList>
36+
</SfSidebar>
37+
</template>
38+
<script lang="ts">
39+
import findDeep from 'deepdash/findDeep';
40+
import {
41+
SfSidebar, SfList, SfMenuItem,
42+
} from '@storefront-ui/vue';
43+
import {
44+
defineComponent,
45+
computed, useRouter, useContext, useRoute,
46+
} from '@nuxtjs/composition-api';
47+
import { categoryGetters, useCategory } from '@vue-storefront/magento';
48+
import { useUiHelpers, useUiState } from '~/composables';
49+
import { CategoryTreeInterface } from '~/modules/catalog/category/types';
50+
import { findAncestorsInCategoryTree, useMobileCategoryTree } from './logic';
51+
52+
export default defineComponent({
53+
components: {
54+
SfSidebar,
55+
SfList,
56+
SfMenuItem,
57+
},
58+
setup() {
59+
const { categories } = useCategory('AppHeader:CategoryList');
60+
const { isMobileMenuOpen, toggleMobileMenu } = useUiState();
61+
const { getAgnosticCatLink } = useUiHelpers();
62+
const router = useRouter();
63+
const route = useRoute();
64+
const app = useContext();
65+
66+
const categoryTree = computed(
67+
() => categoryGetters.getCategoryTree(categories.value?.[0])?.items.filter((c) => c.count > 0),
68+
);
69+
70+
const navigate = (category: CategoryTreeInterface) => {
71+
toggleMobileMenu();
72+
const path = app.localePath(getAgnosticCatLink(category) as string);
73+
router.push(path);
74+
};
75+
76+
const activeCategory: CategoryTreeInterface = findDeep(categoryTree.value, (value, key) => key === 'slug' && value === route.value.fullPath.replace('/default/c', ''))?.parent;
77+
const initialHistory: CategoryTreeInterface[] = findAncestorsInCategoryTree(categoryTree.value as CategoryTreeInterface[], activeCategory);
78+
79+
const {
80+
current: currentCategory, history, currentItems, onGoCategoryUp, onGoCategoryDown,
81+
} = useMobileCategoryTree(initialHistory);
82+
83+
return {
84+
currentCategory,
85+
currentItems,
86+
onGoCategoryUp,
87+
onGoCategoryDown,
88+
categoryTree,
89+
history,
90+
91+
navigate,
92+
isMobileMenuOpen,
93+
toggleMobileMenu,
94+
};
95+
},
96+
});
97+
</script>
98+
99+
<style lang="scss" scoped>
100+
.mobile-menu-sidebar {
101+
--sidebar-z-index: 3;
102+
--overlay-z-index: 3;
103+
104+
&__list {
105+
.mobile-menu-sidebar__item {
106+
padding: var(--spacer-base) 0;
107+
--menu-item-font-size: 1.75rem;
108+
109+
&:not(:first-of-type) {
110+
border-top: 1px solid var(--c-light);
111+
}
112+
113+
&:not(:last-of-type) {
114+
border-bottom: 1px solid var(--c-light);
115+
}
116+
}
117+
}
118+
119+
::v-deep {
120+
.nuxt-link-active {
121+
--menu-item-label-color: var(--c-primary);
122+
}
123+
}
124+
}
125+
.go-back {
126+
display: flex;
127+
align-items: center;
128+
}
129+
</style>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { CategoryTreeInterface } from '~/modules/catalog/category/types';
2+
import { useMobileCategoryTree } from '../logic';
3+
4+
const createCategoryItem = (label: string): CategoryTreeInterface => ({
5+
label, items: [], isCurrent: false, count: 10,
6+
});
7+
8+
describe('categoryTreeLogic', () => {
9+
it('can go down down a category', () => {
10+
const itemFirst = createCategoryItem('Itemless1');
11+
const { history, current, onGoCategoryDown } = useMobileCategoryTree();
12+
onGoCategoryDown(itemFirst);
13+
expect(current.value.label).toBe(itemFirst.label);
14+
expect(history.value).toHaveLength(1);
15+
});
16+
17+
it('can go up a category', () => {
18+
const itemFirst = createCategoryItem('Itemless1');
19+
const itemSecond = createCategoryItem('Itemless2');
20+
21+
const { current, onGoCategoryDown, onGoCategoryUp } = useMobileCategoryTree();
22+
23+
onGoCategoryDown(itemFirst);
24+
onGoCategoryDown(itemSecond);
25+
onGoCategoryUp();
26+
27+
expect(current.value.label).toBe(itemFirst.label);
28+
});
29+
30+
it('current item is last in history', () => {
31+
const itemFirst = createCategoryItem('Itemless1');
32+
const itemSecond = createCategoryItem('Itemless2');
33+
const { history, current, onGoCategoryDown } = useMobileCategoryTree();
34+
35+
onGoCategoryDown(itemFirst);
36+
onGoCategoryDown(itemSecond);
37+
38+
expect(current.value.label).toBe(itemSecond.label);
39+
expect(history.value).toHaveLength(2);
40+
});
41+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { computed, ref } from '@nuxtjs/composition-api';
2+
import { CategoryTreeInterface } from '~/modules/catalog/category/types';
3+
4+
export const useMobileCategoryTree = (initialHistory: CategoryTreeInterface[] = []) => {
5+
const history = ref<CategoryTreeInterface[]>(initialHistory);
6+
const current = computed<CategoryTreeInterface | null>(() => history.value.at(-1) ?? null);
7+
const currentItems = computed<CategoryTreeInterface[]>(() => current.value?.items);
8+
const onGoCategoryDown = (category: CategoryTreeInterface) => {
9+
history.value.push(category);
10+
};
11+
const onGoCategoryUp = () => history.value.pop();
12+
13+
return {
14+
history,
15+
current,
16+
currentItems,
17+
onGoCategoryUp,
18+
onGoCategoryDown,
19+
};
20+
};
21+
22+
/*
23+
* Finds each category preceding `toFind` in the category tree
24+
* Uses a modified Breadth First Search algorithm to tell you if the toFind node exists how to get to it
25+
* @returns Flat array of categories ([level1Obj, level2Obj, level3Obj])
26+
*/
27+
export const findCategoryAncestors = (node: CategoryTreeInterface, toFind: CategoryTreeInterface, startingArray = []) => {
28+
if (node === toFind) {
29+
// an itemless category can't be "entered into" so it must be removed from the end
30+
const tailHasChildren = startingArray.at(-1)?.items.length;
31+
return tailHasChildren ? startingArray : startingArray.slice(0, -1);
32+
}
33+
if (node.items.length > 0) {
34+
for (let i = 0; i < node.items.length; i += 1) {
35+
const subnode = node.items[0];
36+
const result = findCategoryAncestors(subnode, toFind, [...startingArray, subnode]);
37+
if (result) return result;
38+
}
39+
}
40+
return null;
41+
};
42+
43+
export const findAncestorsInCategoryTree = (categoryTree: CategoryTreeInterface[], toFind: CategoryTreeInterface) => categoryTree
44+
.map((el) => findCategoryAncestors(el, toFind, [el])).find((x) => x);

packages/theme/modules/catalog/category/components/sidebar/useSidebar.ts

Lines changed: 0 additions & 38 deletions
This file was deleted.

0 commit comments

Comments
 (0)