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