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>
56import AppNavigationMixin from './AppNavigationMixin';
57
58export default {
59  name: 'AppNavigation',
60  mixins: [AppNavigationMixin],
61  data() {
62    return {
63      isNavigationOpen: false
64    };
65  },
66  watch: {
67    $route: function() {
68      this.isNavigationOpen = false;
69    },
70    isNavigationOpen: function(isNavigationOpen) {
71      this.$root.$emit('change:isNavigationOpen', isNavigationOpen);
72    }
73  },
74  mounted() {
75    this.$root.$on('toggle:navigation', () => this.toggleIsOpen());
76  },
77  methods: {
78    toggleIsOpen() {
79      this.isNavigationOpen = !this.isNavigationOpen;
80    }
81  }
82};
83</script>
84
85<style scoped lang="scss">
86svg {
87  fill: currentColor;
88  height: 1.2rem;
89  width: 1.2rem;
90  margin-left: 0 !important; //!important overriding button specificity
91  vertical-align: text-bottom;
92  &:not(.icon-expand) {
93    margin-right: $spacer;
94  }
95}
96
97.nav {
98  padding-top: $spacer / 4;
99  @include media-breakpoint-up($responsive-layout-bp) {
100    padding-top: $spacer;
101  }
102}
103
104.nav-item__nav {
105  list-style: none;
106  padding-left: 0;
107  margin-left: 0;
108
109  .nav-item {
110    outline: none;
111  }
112
113  .nav-link {
114    padding-left: $spacer * 4;
115    outline: none;
116
117    &:not(.nav-link--current) {
118      font-weight: normal;
119    }
120  }
121}
122
123.btn-link {
124  width: 100%;
125  text-align: left;
126  text-decoration: none !important;
127  border-radius: 0;
128
129  &.collapsed {
130    .icon-expand {
131      transform: rotate(180deg);
132    }
133  }
134}
135
136.icon-expand {
137  float: right;
138  margin-top: $spacer / 4;
139}
140
141.btn-link,
142.nav-link {
143  position: relative;
144  font-weight: $headings-font-weight;
145  padding-left: $spacer; // defining consistent padding for links and buttons
146  padding-right: $spacer;
147  color: theme-color('secondary');
148
149  &:hover {
150    background-color: gray('300');
151    color: theme-color('dark');
152  }
153
154  &:focus {
155    box-shadow: $btn-focus-box-shadow;
156    color: theme-color('dark');
157  }
158}
159
160.nav-link--current,
161.nav-link--current:hover,
162.nav-link--current:focus {
163  font-weight: $headings-font-weight;
164  background-color: theme-color('secondary');
165  color: theme-color('light');
166  cursor: default;
167
168  &::before {
169    content: '';
170    position: absolute;
171    top: 0;
172    bottom: 0;
173    left: 0;
174    width: 4px;
175    background-color: theme-color('primary');
176  }
177}
178
179.nav-container {
180  position: fixed;
181  width: $navigation-width;
182  top: $header-height;
183  bottom: 0;
184  left: 0;
185  z-index: $zindex-fixed;
186  overflow-y: auto;
187  background-color: gray('100');
188  transform: translateX(-$navigation-width);
189  transition: transform $exit-easing--productive $duration--moderate-02;
190  @include media-breakpoint-down(md) {
191    z-index: $zindex-fixed + 2;
192  }
193
194  &.open,
195  &:focus-within {
196    transform: translateX(0);
197    transition-timing-function: $entrance-easing--productive;
198  }
199
200  @include media-breakpoint-up($responsive-layout-bp) {
201    transition-duration: $duration--fast-01;
202    transform: translateX(0);
203  }
204}
205
206.nav-overlay {
207  position: fixed;
208  top: $header-height;
209  bottom: 0;
210  left: 0;
211  right: 0;
212  z-index: $zindex-fixed + 1;
213  background-color: $black;
214  opacity: 0.5;
215
216  &.fade-enter-active {
217    transition: opacity $duration--moderate-02 $entrance-easing--productive;
218  }
219
220  &.fade-leave-active {
221    transition: opacity $duration--fast-02 $exit-easing--productive;
222  }
223
224  &.fade-enter,
225  &.fade-leave-to {
226    opacity: 0;
227  }
228
229  @include media-breakpoint-up($responsive-layout-bp) {
230    display: none;
231  }
232}
233</style>
234