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