Skip to content

Commit adf966b

Browse files
committed
Merge remote-tracking branch 'origin/main' into showcase-form
# Conflicts: # opendata.swiss/ui/app/components/headers/OdsHeader.vue # opendata.swiss/ui/i18n/locales/de.json # opendata.swiss/ui/i18n/locales/en.json # opendata.swiss/ui/i18n/locales/fr.json # opendata.swiss/ui/i18n/locales/it.json
2 parents 8debb52 + 6c96b73 commit adf966b

File tree

17 files changed

+564
-105
lines changed

17 files changed

+564
-105
lines changed

opendata.swiss/ui/app/components/LogoSmall.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ file will be provided inline. */
77
src="/img/logo.svg"
88
:alt="t('message.header.ods_logo')"
99
:title="t('message.header.ods_logo')"
10+
fetchpriority="high"
1011
class="logo__freebrand" />
1112
</template>
1213

opendata.swiss/ui/app/components/NamedLogo.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
src="/img/logo_horizontal.svg"
44
:alt="t('message.header.ods_logo')"
55
:title="t('message.header.ods_logo')"
6+
fetchpriority="high"
67
class="logo__freebrand"
78
/>
89
</template>
Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
<script setup lang="ts">
2+
import { ref, onMounted, onBeforeUnmount, type PropType } from 'vue';
3+
import { useI18n } from 'vue-i18n';
4+
5+
import OdsButton from './OdsButton.vue';
6+
import SvgIcon from './SvgIcon.vue';
7+
import type { OdsNavTabItem } from './headers/model/ods-nav-tab-item';
8+
import { NuxtLinkLocale } from '#components';
9+
10+
const props = defineProps({
11+
label: {
12+
type: String,
13+
required: true
14+
},
15+
menu: {
16+
type: Object as PropType<OdsNavTabItem>,
17+
required: true
18+
},
19+
class: {
20+
type: String,
21+
required: false,
22+
default: ''
23+
},
24+
25+
});
26+
27+
const { t } = useI18n()
28+
const isOpen = ref(false)
29+
30+
const dropdownRef = ref<HTMLElement | null>(null)
31+
32+
function toggleDropdown() {
33+
isOpen.value = !isOpen.value;
34+
}
35+
36+
37+
const parentLabelStack = ref<string[]>([props.menu.label])
38+
39+
const menuStack = ref<OdsNavTabItem[][]>([]);
40+
41+
const menuKey = computed(() => menuStack.value.length)
42+
43+
const currentMenu = computed(() => {
44+
45+
if (menuStack.value.length === 0) {
46+
return props.menu.subMenu ?? []
47+
}
48+
return menuStack.value[menuStack.value.length - 1]
49+
});
50+
51+
const direction = ref<'forward' | 'backward'>('forward');
52+
53+
const transitionName = computed(() => direction.value === 'forward' ? 'slide-menu-right' : 'slide-menu-left');
54+
55+
56+
function drillDown(item: OdsNavTabItem) {
57+
if (item.subMenu && item.subMenu.length > 0) {
58+
direction.value = 'forward';
59+
menuStack.value.push(item.subMenu);
60+
parentLabelStack.value.push(item.label);
61+
}
62+
}
63+
64+
function goBack() {
65+
if (menuStack.value.length > 0) {
66+
direction.value = 'backward';
67+
menuStack.value.pop();
68+
parentLabelStack.value.pop();
69+
}
70+
};
71+
72+
// Close dropdown when clicking outside
73+
function handleClickOutside(event: MouseEvent) {
74+
if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
75+
isOpen.value = false;
76+
}
77+
}
78+
79+
// start listening for clicks outside the dropdown
80+
onMounted(() => {
81+
document.addEventListener('mousedown', handleClickOutside);
82+
});
83+
84+
// stop listening when component is unmounted
85+
onBeforeUnmount(() => {
86+
document.removeEventListener('mousedown', handleClickOutside);
87+
});
88+
89+
</script>
90+
91+
<template>
92+
<a
93+
href="#"
94+
:class="props.class"
95+
@click.prevent="toggleDropdown"
96+
@mouseenter="isOpen = true"
97+
>
98+
<span class="activate-btn">{{ t(props.label) }}</span>
99+
</a>
100+
<div ref="dropdownRef" class="ods-dropdown">
101+
<div v-if="isOpen" id="desktop-menu__drawer" class="desktop-menu__drawer ods-drop-down-panel" >
102+
<div class="close-button-container">
103+
<SvgIcon v-if="menuStack.length > 0" icon="ArrowLeft" size="lg" @click.prevent="goBack"/>
104+
<div v-else />
105+
<OdsButton
106+
icon="Cancel"
107+
title="close menu"
108+
variant="bare"
109+
size="sm"
110+
icon-right
111+
class="ods-close"
112+
@click="isOpen = false"
113+
>
114+
<span class="ods-close">{{t('message.header.navigation.close')}}</span>
115+
</OdsButton>
116+
</div>
117+
<Transition :name="transitionName" mode="out-in">
118+
<div :key="menuKey">
119+
<div class="navy">
120+
<h2 class="navy__title">{{ t(parentLabelStack[parentLabelStack.length - 1] ?? '') }}</h2>
121+
122+
<nav class="navy__level-0 navy-menu__level-0">
123+
<ul>
124+
<template v-for="item in currentMenu" :key="item.label">
125+
<li v-if="item.to">
126+
<NuxtLinkLocale
127+
:to="item.to"
128+
active-class="active"
129+
:aria-label="t(item.label)"
130+
@click="isOpen = false"
131+
>
132+
<span>{{t(item.label)}}</span>
133+
</NuxtLinkLocale><div/>
134+
</li>
135+
<li
136+
v-else-if="(item.subMenu?.length ?? -1) > 0"
137+
@click="drillDown(item)"
138+
>
139+
<a>{{t(item.label)}} <SvgIcon size="lg" icon="ArrowRight" /></a>
140+
</li>
141+
</template>
142+
</ul>
143+
</nav>
144+
</div>
145+
</div>
146+
</Transition>
147+
</div>
148+
</div>
149+
</template>
150+
151+
<style lang="scss" scoped>
152+
153+
.activate-btn {
154+
color: var(--color-primary-600);
155+
}
156+
157+
.close-button-container {
158+
display: flex;
159+
justify-content: space-between;
160+
}
161+
162+
.ods-close {
163+
white-space: nowrap;
164+
color: rgb(107 114 128 / var(--tw-text-opacity, 1));
165+
}
166+
167+
.ods-hover:hover {
168+
background-color: var(--color-secondary-50);
169+
color: rgb(75 85 99 / var(--tw-text-opacity, 1));
170+
}
171+
172+
.ods-drop-down-panel {
173+
min-width: 160px;
174+
position: absolute;
175+
top: -17px;
176+
left: 0;
177+
padding-left: 48px;
178+
padding-right: 48px;
179+
padding-bottom: 48px;
180+
padding-top: 24px;
181+
z-index: 100;
182+
}
183+
@media (min-width: 1280px) {
184+
.ods-drop-down-panel {
185+
top: -19px;
186+
}
187+
}
188+
@media (min-width: 1920px) {
189+
.ods-drop-down-panel {
190+
top: -21px;
191+
}
192+
}
193+
.ods-dropdown {
194+
position: relative;
195+
display: inline-block;
196+
}
197+
198+
.ods-dropdown > a {
199+
cursor: pointer;
200+
display: flex;
201+
align-items: center;
202+
}
203+
204+
.ods-dropdown__arrow {
205+
margin-left: 0.5em;
206+
font-size: 0.8em;
207+
}
208+
209+
.ods-dropdown__menu {
210+
position: absolute;
211+
top: 100%;
212+
left: 0;
213+
background: white;
214+
border: 1px solid #ddd;
215+
min-width: 160px;
216+
z-index: 1000;
217+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
218+
padding: 0.5em 0;
219+
}
220+
221+
.ods-dropdown__menu li {
222+
list-style: none;
223+
}
224+
225+
.ods-dropdown__menu a {
226+
display: block;
227+
padding: 0.5em 1em;
228+
color: inherit;
229+
text-decoration: none;
230+
}
231+
232+
.ods-dropdown__menu a:hover {
233+
background: #f5f5f5;
234+
}
235+
236+
// list styles
237+
.navy {
238+
position: unset !important;
239+
perspective: none !important;
240+
}
241+
242+
.navy__level-0 {
243+
position: unset;
244+
ul {
245+
display: block;
246+
}
247+
}
248+
.navy__title {
249+
margin-top: 0;
250+
margin-left: 12px
251+
}
252+
253+
li {
254+
border-bottom-width: 1px;
255+
font-size: 0.875rem;
256+
width: 100%;
257+
cursor: pointer;
258+
line-height: 24px;
259+
height: 46px;
260+
margin-top: 0 !important;
261+
262+
}
263+
li:hover {
264+
background-color: var(--color-secondary-50);
265+
}
266+
267+
#mobile-menu {
268+
width: 100vw;
269+
}
270+
271+
ul {
272+
list-style: none !important;
273+
margin: 0 !important;
274+
padding: 0 !important;
275+
}
276+
277+
a {
278+
text-decoration: none;
279+
display: flex;
280+
align-items: center;
281+
justify-content: space-between;
282+
height: 100%;
283+
padding-left: 16px;
284+
padding-right: 32px;
285+
color: black;
286+
}
287+
288+
.submenu-arrow {
289+
float: right;
290+
font-size: 1.2em;
291+
margin-left: 8px;
292+
}
293+
.back-btn {
294+
font-weight: bold;
295+
color: #2ecc40;
296+
cursor: pointer;
297+
}
298+
.slide-menu-right-enter-active, .slide-menu-right-leave-active {
299+
transition: transform 0.3s cubic-bezier(0.4,0,0.2,1), opacity 0.3s cubic-bezier(0.4,0,0.2,1);
300+
}
301+
.slide-menu-right-enter-from {
302+
transform: translateX(100%);
303+
opacity: 0;
304+
}
305+
.slide-menu-right-enter-to {
306+
transform: translateX(0);
307+
opacity: 1;
308+
}
309+
.slide-menu-right-leave-from {
310+
transform: translateX(0);
311+
opacity: 1;
312+
}
313+
.slide-menu-right-leave-to {
314+
transform: translateX(-100%);
315+
opacity: 0;
316+
}
317+
.slide-menu-left-enter-active, .slide-menu-left-leave-active {
318+
transition: transform 0.3s cubic-bezier(0.4,0,0.2,1), opacity 0.3s cubic-bezier(0.4,0,0.2,1);
319+
}
320+
.slide-menu-left-enter-from {
321+
transform: translateX(-100%);
322+
opacity: 0;
323+
}
324+
.slide-menu-left-enter-to {
325+
transform: translateX(0);
326+
opacity: 1;
327+
}
328+
.slide-menu-left-leave-from {
329+
transform: translateX(0);
330+
opacity: 1;
331+
}
332+
.slide-menu-left-leave-to {
333+
transform: translateX(100%);
334+
opacity: 0;
335+
}
336+
</style>

opendata.swiss/ui/app/components/OdsNavigationPanel.vue

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,13 @@ import SvgIcon from "~/components/SvgIcon.vue";
5050
5151
const emit = defineEmits(['requestClose']);
5252
const { t } = useI18n();
53-
const props = defineProps<{
54-
items: OdsNavTabItem[]
55-
}>()
53+
54+
const props = defineProps({
55+
items: {
56+
type: Array as PropType<OdsNavTabItem[]>,
57+
required: true
58+
}
59+
});
5660
5761
5862

0 commit comments

Comments
 (0)