1<template> 2 <div> 3 <div class="nav-container" :class="{ open: isNavigationOpen }"> 4 <nav ref="nav" :aria-label="$t('appNavigation.primaryNavigation')"> 5 <b-nav vertical class="mb-4"> 6 <template v-for="(navItem, index) in navigationItems"> 7 <!-- Navigation items with no children --> 8 <b-nav-item 9 v-if="!navItem.children" 10 :key="index" 11 :to="navItem.route" 12 :data-test-id="`nav-item-${navItem.id}`" 13 > 14 <component :is="navItem.icon" /> 15 {{ navItem.label }} 16 </b-nav-item> 17 18 <!-- Navigation items with children --> 19 <li v-else :key="index" class="nav-item"> 20 <b-button 21 v-b-toggle="`${navItem.id}`" 22 variant="link" 23 :data-test-id="`nav-button-${navItem.id}`" 24 > 25 <component :is="navItem.icon" /> 26 {{ navItem.label }} 27 <icon-expand class="icon-expand" /> 28 </b-button> 29 <b-collapse :id="navItem.id" tag="ul" class="nav-item__nav"> 30 <b-nav-item 31 v-for="(subNavItem, i) of navItem.children" 32 :key="i" 33 :to="subNavItem.route" 34 :data-test-id="`nav-item-${subNavItem.id}`" 35 > 36 {{ subNavItem.label }} 37 </b-nav-item> 38 </b-collapse> 39 </li> 40 </template> 41 </b-nav> 42 </nav> 43 </div> 44 <transition name="fade"> 45 <div 46 v-if="isNavigationOpen" 47 id="nav-overlay" 48 class="nav-overlay" 49 @click="toggleIsOpen" 50 ></div> 51 </transition> 52 </div> 53</template> 54 55<script> 56//Do not change Mixin import. 57//Exact match alias set to support 58//dotenv customizations. 59import AppNavigationMixin from './AppNavigationMixin'; 60 61export default { 62 name: 'AppNavigation', 63 mixins: [AppNavigationMixin], 64 data() { 65 return { 66 isNavigationOpen: false, 67 }; 68 }, 69 watch: { 70 $route: function () { 71 this.isNavigationOpen = false; 72 }, 73 isNavigationOpen: function (isNavigationOpen) { 74 this.$root.$emit('change-is-navigation-open', isNavigationOpen); 75 }, 76 }, 77 mounted() { 78 this.$root.$on('toggle-navigation', () => this.toggleIsOpen()); 79 }, 80 methods: { 81 toggleIsOpen() { 82 this.isNavigationOpen = !this.isNavigationOpen; 83 }, 84 }, 85}; 86</script> 87 88<style scoped lang="scss"> 89svg { 90 fill: currentColor; 91 height: 1.2rem; 92 width: 1.2rem; 93 margin-left: 0 !important; //!important overriding button specificity 94 vertical-align: text-bottom; 95 &:not(.icon-expand) { 96 margin-right: $spacer; 97 } 98} 99 100.nav { 101 padding-top: $spacer / 4; 102 @include media-breakpoint-up($responsive-layout-bp) { 103 padding-top: $spacer; 104 } 105} 106 107.nav-item__nav { 108 list-style: none; 109 padding-left: 0; 110 margin-left: 0; 111 112 .nav-item { 113 outline: none; 114 } 115 116 .nav-link { 117 padding-left: $spacer * 4; 118 outline: none; 119 120 &:not(.nav-link--current) { 121 font-weight: normal; 122 } 123 } 124} 125 126.btn-link { 127 display: inline-block; 128 width: 100%; 129 text-align: left; 130 text-decoration: none !important; 131 border-radius: 0; 132 133 &.collapsed { 134 .icon-expand { 135 transform: rotate(180deg); 136 } 137 } 138} 139 140.icon-expand { 141 float: right; 142 margin-top: $spacer / 4; 143} 144 145.btn-link, 146.nav-link { 147 position: relative; 148 font-weight: $headings-font-weight; 149 padding-left: $spacer; // defining consistent padding for links and buttons 150 padding-right: $spacer; 151 color: theme-color('secondary'); 152 153 &:hover { 154 background-color: theme-color-level(dark, -10.5); 155 color: theme-color('dark'); 156 } 157 158 &:focus { 159 background-color: theme-color-level(light, 0); 160 box-shadow: inset 0 0 0 2px theme-color('primary'); 161 color: theme-color('dark'); 162 outline: 0; 163 } 164 165 &:active { 166 background-color: theme-color('secondary'); 167 color: $white; 168 } 169} 170 171.nav-link--current { 172 font-weight: $headings-font-weight; 173 background-color: theme-color('secondary'); 174 color: theme-color('light'); 175 cursor: default; 176 box-shadow: none; 177 178 &::before { 179 content: ''; 180 position: absolute; 181 top: 0; 182 bottom: 0; 183 left: 0; 184 width: 4px; 185 background-color: theme-color('primary'); 186 } 187 188 &:hover, 189 &:focus { 190 background-color: theme-color('secondary'); 191 color: theme-color('light'); 192 } 193} 194 195.nav-container { 196 position: fixed; 197 width: $navigation-width; 198 top: $header-height; 199 bottom: 0; 200 left: 0; 201 z-index: $zindex-fixed; 202 overflow-y: auto; 203 background-color: theme-color('light'); 204 transform: translateX(-$navigation-width); 205 transition: transform $exit-easing--productive $duration--moderate-02; 206 border-right: 1px solid theme-color-level('light', 2.85); 207 208 @include media-breakpoint-down(md) { 209 z-index: $zindex-fixed + 2; 210 } 211 212 &.open, 213 &:focus-within { 214 transform: translateX(0); 215 transition-timing-function: $entrance-easing--productive; 216 } 217 218 @include media-breakpoint-up($responsive-layout-bp) { 219 transition-duration: $duration--fast-01; 220 transform: translateX(0); 221 } 222} 223 224.nav-overlay { 225 position: fixed; 226 top: $header-height; 227 bottom: 0; 228 left: 0; 229 right: 0; 230 z-index: $zindex-fixed + 1; 231 background-color: $black; 232 opacity: 0.5; 233 234 &.fade-enter-active { 235 transition: opacity $duration--moderate-02 $entrance-easing--productive; 236 } 237 238 &.fade-leave-active { 239 transition: opacity $duration--fast-02 $exit-easing--productive; 240 } 241 242 &.fade-enter, // Remove this vue2 based only class when switching to vue3 243 &.fade-enter-from, // This is vue3 based only class modified from 'fade-enter' 244 &.fade-leave-to { 245 opacity: 0; 246 } 247 248 @include media-breakpoint-up($responsive-layout-bp) { 249 display: none; 250 } 251} 252</style> 253