add and fix keyboard nav
This commit is contained in:
parent
33bf025fdb
commit
ca031126e1
@ -143,7 +143,12 @@ body
|
|||||||
opacity: 0
|
opacity: 0
|
||||||
|
|
||||||
*:focus
|
*:focus
|
||||||
outline: 1px solid transparent
|
outline: none
|
||||||
|
|
||||||
|
body.user-is-tabbing *:focus
|
||||||
|
outline: none
|
||||||
|
box-shadow: 0 0 0 4px $pink !important
|
||||||
|
border-radius: 5px
|
||||||
|
|
||||||
/*.router-link-active
|
/*.router-link-active
|
||||||
position: relative
|
position: relative
|
||||||
|
|||||||
@ -6,7 +6,17 @@
|
|||||||
aria-label="Breadcrumb"
|
aria-label="Breadcrumb"
|
||||||
>
|
>
|
||||||
<div class="breadcrumb-container">
|
<div class="breadcrumb-container">
|
||||||
<div class="breadcrumb-trigger" @click="toggleOpen" aria-label="Breadcrumb öffnen">
|
<!-- Trigger -->
|
||||||
|
<div
|
||||||
|
class="breadcrumb-trigger"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="toggleOpen"
|
||||||
|
@keydown.space.prevent="toggleOpen"
|
||||||
|
@keydown.enter.prevent="toggleOpen"
|
||||||
|
:aria-expanded="isOpen.toString()"
|
||||||
|
aria-label="Breadcrumb öffnen"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 576 512"
|
viewBox="0 0 576 512"
|
||||||
@ -20,7 +30,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Breadcrumbs selbst (ein- und ausklappbar) -->
|
<!-- Breadcrumbs -->
|
||||||
<ul :class="{ open: isOpen, closed: !isOpen }">
|
<ul :class="{ open: isOpen, closed: !isOpen }">
|
||||||
<li
|
<li
|
||||||
v-for="(crumb, index) in breadcrumbs"
|
v-for="(crumb, index) in breadcrumbs"
|
||||||
@ -30,19 +40,33 @@
|
|||||||
v-if="index < breadcrumbs.length - 1"
|
v-if="index < breadcrumbs.length - 1"
|
||||||
:to="crumb.to"
|
:to="crumb.to"
|
||||||
:title="crumb.labelFull"
|
:title="crumb.labelFull"
|
||||||
>{{ crumb.label }}
|
>
|
||||||
|
{{ crumb.label }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<span v-else :title="crumb.labelFull"><span v-if="breadcrumbs.length > 1" class="between">›</span>{{ crumb.label }}</span>
|
<span v-else :title="crumb.labelFull">
|
||||||
|
<span v-if="breadcrumbs.length > 1" class="between">›</span>
|
||||||
|
{{ crumb.label }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
class="close-btn"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="isOpen = false"
|
||||||
|
@keydown.space.prevent="isOpen = false"
|
||||||
|
@keydown.enter.prevent="isOpen = false"
|
||||||
|
aria-label="Breadcrumb schließen"
|
||||||
|
title="Schließen"
|
||||||
|
></div>
|
||||||
</li>
|
</li>
|
||||||
<li class="close-btn" @click="isOpen = false" title="Schließen"></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { i18nPages } from '~/i18n/i18n-pages'
|
import { i18nPages } from '~/i18n/i18n-pages'
|
||||||
@ -54,11 +78,30 @@ interface Breadcrumb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isOpen = ref(false)
|
const isOpen = ref(false)
|
||||||
|
const route = useRoute()
|
||||||
|
const { locale, t } = useI18n()
|
||||||
|
|
||||||
function toggleOpen() {
|
function toggleOpen() {
|
||||||
isOpen.value = !isOpen.value
|
isOpen.value = !isOpen.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
isOpen.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('keydown', onKeydown)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('keydown', onKeydown)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => route.fullPath, () => {
|
||||||
|
isOpen.value = false
|
||||||
|
})
|
||||||
|
|
||||||
function formatLabel(segment: string): { label: string; labelFull: string } {
|
function formatLabel(segment: string): { label: string; labelFull: string } {
|
||||||
const labelFull = segment.charAt(0).toUpperCase() + segment.slice(1).replace(/-/g, ' ')
|
const labelFull = segment.charAt(0).toUpperCase() + segment.slice(1).replace(/-/g, ' ')
|
||||||
let label = labelFull
|
let label = labelFull
|
||||||
@ -69,14 +112,8 @@ function formatLabel(segment: string): { label: string; labelFull: string } {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildUrl(loc: string, path: string): string {
|
function buildUrl(loc: string, path: string): string {
|
||||||
if (loc === 'de') {
|
return loc === 'de' ? '/' + path.replace(/^\//, '') : `/${loc}/${path.replace(/^\//, '')}`
|
||||||
return path.startsWith('/') ? path : '/' + path
|
|
||||||
}
|
}
|
||||||
return `/${loc}${path.startsWith('/') ? path : '/' + path}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const { locale, t } = useI18n()
|
|
||||||
|
|
||||||
const breadcrumbs = computed<Breadcrumb[]>(() => {
|
const breadcrumbs = computed<Breadcrumb[]>(() => {
|
||||||
const loc = locale.value
|
const loc = locale.value
|
||||||
@ -86,61 +123,32 @@ const breadcrumbs = computed<Breadcrumb[]>(() => {
|
|||||||
if (segments.length === 2 && segments[0] === 'projekt') {
|
if (segments.length === 2 && segments[0] === 'projekt') {
|
||||||
const referencesPath = i18nPages.references?.[loc] || '/references'
|
const referencesPath = i18nPages.references?.[loc] || '/references'
|
||||||
const referencesLabel = t('references') || 'References'
|
const referencesLabel = t('references') || 'References'
|
||||||
|
return [
|
||||||
const first = {
|
{ label: referencesLabel, labelFull: referencesLabel, to: buildUrl(loc, referencesPath) },
|
||||||
label: referencesLabel,
|
{ ...formatLabel(segments[1]), to: route.path }
|
||||||
labelFull: referencesLabel,
|
]
|
||||||
to: buildUrl(loc, referencesPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { label, labelFull } = formatLabel(segments[1])
|
|
||||||
const second = {
|
|
||||||
label,
|
|
||||||
labelFull,
|
|
||||||
to: route.path
|
|
||||||
}
|
|
||||||
|
|
||||||
return [first, second]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (segments.length === 2 && segments[0] === 'artikel') {
|
if (segments.length === 2 && segments[0] === 'artikel') {
|
||||||
const magazinePath = i18nPages.magazin?.[loc] || '/magazin'
|
const magazinePath = i18nPages.magazin?.[loc] || '/magazin'
|
||||||
const magazineLabel = t('magazin') || 'Magazin'
|
const magazineLabel = t('magazin') || 'Magazin'
|
||||||
|
return [
|
||||||
const first = {
|
{ label: magazineLabel, labelFull: magazineLabel, to: buildUrl(loc, magazinePath) },
|
||||||
label: magazineLabel,
|
{ ...formatLabel(segments[1]), to: route.path }
|
||||||
labelFull: magazineLabel,
|
]
|
||||||
to: buildUrl(loc, magazinePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { label, labelFull } = formatLabel(segments[1])
|
|
||||||
const second = {
|
|
||||||
label,
|
|
||||||
labelFull,
|
|
||||||
to: route.path
|
|
||||||
}
|
|
||||||
|
|
||||||
return [first, second]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = ''
|
let path = ''
|
||||||
return segments.map(segment => {
|
return segments.map(segment => {
|
||||||
path += '/' + segment
|
path += '/' + segment
|
||||||
const { label, labelFull } = formatLabel(segment)
|
const { label, labelFull } = formatLabel(segment)
|
||||||
return {
|
return { label, labelFull, to: buildUrl(loc, path) }
|
||||||
label,
|
|
||||||
labelFull,
|
|
||||||
to: buildUrl(loc, path)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => route.fullPath, () => {
|
|
||||||
isOpen.value = false
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="sass" scoped>
|
|
||||||
|
<style lang="sass">
|
||||||
.breadcrumbs
|
.breadcrumbs
|
||||||
position: fixed
|
position: fixed
|
||||||
top: 12vh
|
top: 12vh
|
||||||
@ -165,14 +173,17 @@ watch(() => route.fullPath, () => {
|
|||||||
justify-content: center
|
justify-content: center
|
||||||
width: 1.8rem
|
width: 1.8rem
|
||||||
height: 1.8rem
|
height: 1.8rem
|
||||||
|
background: none
|
||||||
|
border: none
|
||||||
|
margin: 0 0 0 .5rem
|
||||||
|
|
||||||
svg
|
svg
|
||||||
width: 1rem
|
width: 1rem !important
|
||||||
height: 1rem
|
height: 1rem !important
|
||||||
transform: translateX(10px)
|
display: inline-block
|
||||||
|
|
||||||
path
|
path
|
||||||
fill: darken($lightgrey, 20%)
|
fill: darken($lightgrey, 30%) !important
|
||||||
|
|
||||||
&:hover path
|
&:hover path
|
||||||
fill: darken($lightgrey, 30%)
|
fill: darken($lightgrey, 30%)
|
||||||
@ -224,6 +235,8 @@ watch(() => route.fullPath, () => {
|
|||||||
border-radius: 50%
|
border-radius: 50%
|
||||||
position: relative
|
position: relative
|
||||||
margin: .2rem .5rem 0 1rem
|
margin: .2rem .5rem 0 1rem
|
||||||
|
border: none
|
||||||
|
text-transform: none
|
||||||
|
|
||||||
&::before
|
&::before
|
||||||
position: absolute
|
position: absolute
|
||||||
|
|||||||
@ -13,6 +13,8 @@
|
|||||||
role="button"
|
role="button"
|
||||||
aria-label="Toggle contact form"
|
aria-label="Toggle contact form"
|
||||||
@click="toggleContactBubble"
|
@click="toggleContactBubble"
|
||||||
|
@keydown.space.prevent="toggleContactBubble"
|
||||||
|
@keydown.enter.prevent="toggleContactBubble"
|
||||||
>
|
>
|
||||||
<use :xlink:href="`/assets/icons/collection.svg#${isContactBubbleOpen ? 'times' : 'talk'}`" />
|
<use :xlink:href="`/assets/icons/collection.svg#${isContactBubbleOpen ? 'times' : 'talk'}`" />
|
||||||
</svg>
|
</svg>
|
||||||
@ -77,6 +79,7 @@
|
|||||||
class="form-control"
|
class="form-control"
|
||||||
type="text"
|
type="text"
|
||||||
name="name"
|
name="name"
|
||||||
|
ref="firstInput"
|
||||||
required
|
required
|
||||||
autocomplete="name"
|
autocomplete="name"
|
||||||
:aria-invalid="!!errors.name"
|
:aria-invalid="!!errors.name"
|
||||||
@ -188,10 +191,10 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useMainStore } from '@/stores/main';
|
import { useMainStore } from '@/stores/main';
|
||||||
import { ref, reactive, computed } from 'vue';
|
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
const firstInput = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
// i18n Setup
|
// i18n Setup
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@ -291,6 +294,30 @@ const resetForm = () => {
|
|||||||
errors.email = null;
|
errors.email = null;
|
||||||
errors.phone = null;
|
errors.phone = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ESC & Fokusmanagement
|
||||||
|
const handleKeydown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape' && isContactBubbleOpen.value) {
|
||||||
|
toggleContactBubble();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Füge den EventListener beim Mount hinzu
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('keydown', handleKeydown);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('keydown', handleKeydown);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wenn die Bubble geöffnet wird: Fokus setzen
|
||||||
|
watch(isContactBubbleOpen, async (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
await nextTick()
|
||||||
|
firstInput.value?.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -46,6 +46,34 @@ const accordionItems = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
const toggleContactBubble = () => mainStore.toggleContactBubble()
|
const toggleContactBubble = () => mainStore.toggleContactBubble()
|
||||||
|
|
||||||
|
// FAQ JSON-LD
|
||||||
|
|
||||||
|
useHead(() => {
|
||||||
|
if (!accordionItems.value.length) return {}
|
||||||
|
|
||||||
|
const faqJsonLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "FAQPage",
|
||||||
|
"mainEntity": accordionItems.value.map(item => ({
|
||||||
|
"@type": "Question",
|
||||||
|
"name": item.title,
|
||||||
|
"acceptedAnswer": {
|
||||||
|
"@type": "Answer",
|
||||||
|
"text": item.html.replace(/<[^>]*>?/gm, '') // HTML-Tags entfernen
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
script: [
|
||||||
|
{
|
||||||
|
type: 'application/ld+json',
|
||||||
|
children: JSON.stringify(faqJsonLd)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav class="mainNav" :class="[
|
<nav
|
||||||
|
class="mainNav"
|
||||||
|
:class="[
|
||||||
isMenuOpen ? 'active' : '',
|
isMenuOpen ? 'active' : '',
|
||||||
screenWidth < 1350 ? 'mobile-nav' : 'desk-nav',
|
screenWidth < 1350 ? 'mobile-nav' : 'desk-nav',
|
||||||
scrollPosition > 50 ? 'scrolled' : ''
|
scrollPosition > 50 ? 'scrolled' : ''
|
||||||
]">
|
]"
|
||||||
<!-- Burger Icon nur bei Mobile sichtbar -->
|
aria-label="Hauptnavigation"
|
||||||
<div class="burger" @click="toggleMenu" v-if="screenWidth < 1350" :class="{ open: isMenuOpen }">
|
>
|
||||||
|
<!-- Burger Icon als Button -->
|
||||||
|
<button
|
||||||
|
class="burger"
|
||||||
|
@click="toggleMenu"
|
||||||
|
v-if="screenWidth < 1350"
|
||||||
|
:aria-expanded="isMenuOpen"
|
||||||
|
aria-controls="main-navigation"
|
||||||
|
:class="{ open: isMenuOpen }"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span class="visually-hidden">Menü öffnen</span>
|
||||||
<span></span>
|
<span></span>
|
||||||
<span></span>
|
<span></span>
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<ul
|
<ul
|
||||||
|
:id="'main-navigation'"
|
||||||
:class="[
|
:class="[
|
||||||
screenWidth < 1350 && isMenuOpen ? 'mobile-menu active' :
|
screenWidth < 1350 && isMenuOpen ? 'mobile-menu active' :
|
||||||
screenWidth < 1350 ? 'mobile-menu' : 'desk-menu']"
|
screenWidth < 1350 ? 'mobile-menu' : 'desk-menu'
|
||||||
|
]"
|
||||||
v-show="screenWidth >= 1350 || isMenuOpen"
|
v-show="screenWidth >= 1350 || isMenuOpen"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
@ -23,21 +38,38 @@
|
|||||||
class="nav-item"
|
class="nav-item"
|
||||||
@mouseenter="screenWidth >= 1350 && link.subNav && showSubNav(link.label)"
|
@mouseenter="screenWidth >= 1350 && link.subNav && showSubNav(link.label)"
|
||||||
@mouseleave="screenWidth >= 1350 && link.subNav && hideSubNav(link.label)"
|
@mouseleave="screenWidth >= 1350 && link.subNav && hideSubNav(link.label)"
|
||||||
|
|
||||||
>
|
>
|
||||||
<!-- Mit SubNav -->
|
<!-- Mit SubNav -->
|
||||||
<template v-if="link.subNav">
|
<template v-if="link.subNav">
|
||||||
<div @click="screenWidth < 1350 ? toggleMobileSubNav(link.label) : null">
|
<button
|
||||||
|
v-if="screenWidth < 1350"
|
||||||
|
class="subnav-toggle"
|
||||||
|
@click="toggleMobileSubNav(link.label)"
|
||||||
|
:aria-expanded="isActiveSubNav(link.label)"
|
||||||
|
:aria-controls="`submenu-${link.label}`"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
{{ $t(link.label) }}
|
{{ $t(link.label) }}
|
||||||
<span class="arrow" :class="{ open: (screenWidth < 1350 ? isMobileSubNavOpen : isSubNavOpen) === link.label }"></span>
|
<span class="arrow" :class="{ open: isActiveSubNav(link.label) }"></span>
|
||||||
|
<span class="visually-hidden">
|
||||||
|
{{ isActiveSubNav(link.label) ? 'Untermenü schließen' : 'Untermenü öffnen' }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<div v-else>
|
||||||
|
{{ $t(link.label) }}
|
||||||
|
<span class="arrow" :class="{ open: isSubNavOpen === link.label }"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul
|
<ul
|
||||||
|
:id="`submenu-${link.label}`"
|
||||||
class="submenu"
|
class="submenu"
|
||||||
:class="{ open: isActiveSubNav(link.label) }"
|
:class="{ open: isActiveSubNav(link.label) }"
|
||||||
>
|
>
|
||||||
<li v-for="sublink in link.subNav" :key="sublink.label">
|
<li v-for="sublink in link.subNav" :key="sublink.label">
|
||||||
<NuxtLinkLocale :to="sublink.routeKey" @click.native="handleMobileClose">
|
<NuxtLinkLocale
|
||||||
|
:to="sublink.routeKey"
|
||||||
|
@click.native="handleMobileClose"
|
||||||
|
>
|
||||||
{{ $t(sublink.label) }}
|
{{ $t(sublink.label) }}
|
||||||
</NuxtLinkLocale>
|
</NuxtLinkLocale>
|
||||||
</li>
|
</li>
|
||||||
@ -46,7 +78,10 @@
|
|||||||
|
|
||||||
<!-- Ohne SubNav -->
|
<!-- Ohne SubNav -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<NuxtLinkLocale :to="link.routeKey" @click.native="handleMobileClose">
|
<NuxtLinkLocale
|
||||||
|
:to="link.routeKey"
|
||||||
|
@click.native="handleMobileClose"
|
||||||
|
>
|
||||||
{{ $t(link.label) }}
|
{{ $t(link.label) }}
|
||||||
</NuxtLinkLocale>
|
</NuxtLinkLocale>
|
||||||
</template>
|
</template>
|
||||||
@ -56,6 +91,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useMainStore } from '@/stores/main'
|
import { useMainStore } from '@/stores/main'
|
||||||
@ -154,6 +190,15 @@
|
|||||||
right: 0
|
right: 0
|
||||||
background-color: transparent
|
background-color: transparent
|
||||||
|
|
||||||
|
.visually-hidden
|
||||||
|
position: absolute
|
||||||
|
width: 1px
|
||||||
|
height: 1px
|
||||||
|
padding: 0
|
||||||
|
margin: -1px
|
||||||
|
overflow: hidden
|
||||||
|
clip: rect(0, 0, 0, 0)
|
||||||
|
border: 0
|
||||||
|
|
||||||
.burger
|
.burger
|
||||||
width: 4rem
|
width: 4rem
|
||||||
@ -184,9 +229,9 @@
|
|||||||
border: 1px solid transparent
|
border: 1px solid transparent
|
||||||
span
|
span
|
||||||
height: 3px
|
height: 3px
|
||||||
&:nth-child(1)
|
|
||||||
transform: rotate(45deg) translate(2px, 5px)
|
|
||||||
&:nth-child(2)
|
&:nth-child(2)
|
||||||
|
transform: rotate(45deg) translate(2px, 5px)
|
||||||
|
&:nth-child(3)
|
||||||
transform: rotate(-45deg) translate(3px, -6px)
|
transform: rotate(-45deg) translate(3px, -6px)
|
||||||
|
|
||||||
.nav-item
|
.nav-item
|
||||||
@ -257,9 +302,9 @@
|
|||||||
.submenu
|
.submenu
|
||||||
position: absolute
|
position: absolute
|
||||||
top: 100%
|
top: 100%
|
||||||
left: -50%
|
left: -30%
|
||||||
width: 200%
|
width: 200%
|
||||||
background-image: linear-gradient(to bottom, rgba(#fff, 0.1) 0%, rgba(#fff, 0.9) 8%, rgba(#fff, .98) 100%)
|
background-image: linear-gradient(to bottom, rgba(#fff, 0.1) 0%, rgba(#fff, 0.9) 18%, rgba(#fff, .98) 100%)
|
||||||
border-bottom-right-radius: 1rem
|
border-bottom-right-radius: 1rem
|
||||||
border-bottom-left-radius: 1rem
|
border-bottom-left-radius: 1rem
|
||||||
list-style: none
|
list-style: none
|
||||||
@ -294,6 +339,13 @@
|
|||||||
cursor: pointer
|
cursor: pointer
|
||||||
font-size: 1.25rem
|
font-size: 1.25rem
|
||||||
color: white
|
color: white
|
||||||
|
.subnav-toggle
|
||||||
|
background-color: transparent
|
||||||
|
border: none
|
||||||
|
color: white
|
||||||
|
font-size: 1.25rem
|
||||||
|
text-transform: uppercase
|
||||||
|
margin-top: -6px
|
||||||
&::before
|
&::before
|
||||||
content: ''
|
content: ''
|
||||||
width: .7rem
|
width: .7rem
|
||||||
|
|||||||
45
composables/useDeferredTabFocus.ts
Normal file
45
composables/useDeferredTabFocus.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
|
||||||
|
export function useDeferredTabFocus() {
|
||||||
|
const focusableElements = ref<HTMLElement[]>([])
|
||||||
|
|
||||||
|
function register(el: HTMLElement | null) {
|
||||||
|
if (el) {
|
||||||
|
el.setAttribute('tabindex', '-1')
|
||||||
|
focusableElements.value.push(el)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateFocus() {
|
||||||
|
focusableElements.value.forEach(el => el.removeAttribute('tabindex'))
|
||||||
|
document.body.classList.add('user-is-tabbing') // <– das hier neu
|
||||||
|
window.removeEventListener('keydown', onKeyDown)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function onKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
activateFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseDown() {
|
||||||
|
document.body.classList.remove('user-is-tabbing')
|
||||||
|
window.addEventListener('keydown', onKeyDown) // optional: erlaubt Wiederaktivierung nach Maus
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('keydown', onKeyDown)
|
||||||
|
window.addEventListener('mousedown', onMouseDown)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('keydown', onKeyDown)
|
||||||
|
window.removeEventListener('mousedown', onMouseDown)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
register
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,6 +17,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
|
import { useDeferredTabFocus } from '@/composables/useDeferredTabFocus'
|
||||||
|
useDeferredTabFocus()
|
||||||
|
|
||||||
import { useMainStore } from '@/stores/main'
|
import { useMainStore } from '@/stores/main'
|
||||||
const mainStore = useMainStore()
|
const mainStore = useMainStore()
|
||||||
import { usePageMeta } from '~/composables/usePageMeta'
|
import { usePageMeta } from '~/composables/usePageMeta'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user