Ontwerp een Vue.js Component met Geavanceerde Toegankelijkheidsfuncties voor Keyboard Navigatie
description
Met deze prompt kun je een Vue.js component maken dat toegankelijk is voor gebruikers die afhankelijk zijn van keyboard navigatie. Dit verbetert de inclusiviteit van je applicatie, voldoet aan webstandaarden en verhoogt de gebruikerservaring voor iedereen. Het richt zich specifiek op keyboard interacties, een aspect dat vaak onderbelicht blijft in frontend development.
prompt
Help me een Vue.js component te ontwerpen dat geavanceerde keyboard navigatie ondersteunt om de toegankelijkheid te verbeteren. Beschrijf hoe ik focusbeheer, sneltoetsen en tabbable elementen implementeer volgens WCAG-richtlijnen ...
try_prompt
generate_helper
disclaimerOnPageApi
tags
ai_answers
provider: openai
model: gpt-4.1-nano
Natuurlijk! Hier is een gedetailleerde uitleg over het ontwerpen van een Vue.js component dat geavanceerde keyboard navigatie ondersteunt voor een toegankelijk navigatiemenu, inclusief focusbeheer, sneltoetsen en tabbable elementen, volgens WCAG-richtlijnen.
---
### 1. Structuur van de component
Maak een Vue.js component, bijvoorbeeld `<AccessibleMenu>`, met de volgende structuur:
```vue
<template>
<nav class="menu" role="navigation" @keydown="onKeydown" @focusout="onFocusOut" tabindex="0">
<ul>
<li v-for="(item, index) in items" :key="index" :ref="'item'+index" :class="{ 'active': activeIndex === index }" :tabindex="activeIndex === index ? 0 : -1" @focus="setActive(index)">
<div v-if="item.dropdown" class="menu-item" @click="toggleDropdown(index)">
{{ item.label }}
<ul v-if="item.open" class="dropdown" role="menu">
<li v-for="(subitem, subindex) in item.dropdown" :key="subindex" :ref="'subitem'+index+subindex" :tabindex="activeSubIndex === subindex && activeIndex === index ? 0 : -1" class="submenu-item" @focus="setActiveSub(index, subindex)">
<a :href="subitem.href" @keydown="onSubitemKeydown" :aria-haspopup="true">{{ subitem.label }}</a>
</li>
</ul>
</div>
<div v-else class="menu-item" @click="activateItem(index)">
<a :href="item.href" @keydown="onItemKeydown">{{ item.label }}</a>
</div>
</li>
</ul>
</nav>
</template>
```
---
### 2. Data en state management
```js
<script>
export default {
data() {
return {
items: [
{ label: 'Home', href: '/', dropdown: null },
{ label: 'Over ons', href: '/about', dropdown: null },
{
label: 'Diensten',
dropdown: [
{ label: 'Consulting', href: '/consulting' },
{ label: 'Development', href: '/development' },
],
open: false,
},
// Voeg meer items toe
],
activeIndex: 0,
activeSubIndex: null,
};
},
methods: {
setActive(index) {
this.activeIndex = index;
this.activeSubIndex = null;
},
setActiveSub(parentIndex, subIndex) {
this.activeIndex = parentIndex;
this.activeSubIndex = subIndex;
},
toggleDropdown(index) {
this.items[index].open = !this.items[index].open;
},
activateItem(index) {
// Navigeer naar href of voer actie uit
},
onKeydown(e) {
// Beheer van navigatie via toetsen
},
onItemKeydown(e) {
// Beheer voor hoofditems
},
onSubitemKeydown(e) {
// Beheer voor dropdown items
},
},
};
</script>
```
---
### 3. Focusbeheer
- **Focus instellen:** Gebruik `tabindex="0"` voor het actieve element en `-1` voor niet-actieve elementen.
- **Focus verplaatsen:** Bij navigatie met pijltjestoetsen of Enter/Spatie verplaats je de focus via `element.focus()`.
- **Focus reset:** Bij openen/close dropdowns zorg dat focus naar het juiste element gaat.
**Voorbeeld:**
```js
methods: {
focusItem(index) {
this.$nextTick(() => {
const el = this.$refs['item'+index][0];
if (el) el.focus();
});
},
}
```
---
### 4. Sneltoetsen en keyboard functies
Implementeer in `onKeydown` en gerelateerde methodes:
- **Tab / Shift+Tab:** Verlaat of betreed het menu.
- **Pijltjestoetsen (Links/Rechts of Omhoog/Omlaag):** Navigeer door menu-items of dropdown-items.
- **Enter / Spatie:** Open dropdowns of activeer links.
- **Escape:** Sluit open dropdowns en focus terug naar het hoofdmenu.
**Voorbeeld `onKeydown`:**
```js
methods: {
onKeydown(e) {
const maxIndex = this.items.length - 1;
switch(e.key) {
case 'ArrowDown':
e.preventDefault();
this.activeIndex = this.activeIndex < maxIndex ? this.activeIndex + 1 : 0;
this.focusItem(this.activeIndex);
break;
case 'ArrowUp':
e.preventDefault();
this.activeIndex = this.activeIndex > 0 ? this.activeIndex -1 : maxIndex;
this.focusItem(this.activeIndex);
break;
case 'Enter':
case ' ':
e.preventDefault();
const currentItem = this.items[this.activeIndex];
if (currentItem.dropdown) {
currentItem.open = !currentItem.open;
} else {
this.activateItem(this.activeIndex);
}
break;
case 'Escape':
e.preventDefault();
// Sluit dropdowns
this.items.forEach(item => item.open = false);
this.focusItem(this.activeIndex);
break;
}
},
}
```
---
### 5. tabbable elementen volgens WCAG
- Zorg dat alleen het actieve element `tabindex="0"` heeft, anderen `-1`.
- Gebruik `ref`s om focus te beheren.
- Bij openen dropdowns: focus automatisch op eerste submenu-item.
- Bij sluiten dropdowns: focus terug naar hoofdmenu-item.
---
### 6. Focus styling
Gebruik CSS voor een opvallende gele rand:
```css
.menu-item:focus, .submenu-item:focus {
outline: none;
border: 2px solid yellow;
border-radius: 4px;
}
```
Of gebruik een class:
```css
.active {
outline: 2px solid yellow;
outline-offset: 2px;
}
```
---
### 7. Samenvatting
- **Focusbeheer:** dynamisch focus verplaatsen via refs en `element.focus()`.
- **Sneltoetsen:** Implementatie in `keydown` handlers, volgens WCAG-richtlijnen.
- **Tab / Shift+Tab:** Beheer focusvolgorde.
- **Tabbable elementen:** Alleen het actieve element is tabbable (`tabindex=0`).
- **Focus styling:** Duidelijke visuele focus indicator (gele rand).
- **Toegankelijkheid:** Gebruik ARIA-rollen en labels waar nodig.
---
Door deze aanpak zorg je voor een toegankelijke, gebruiksvriendelijke Vue.js navigatie die voldoet aan WCAG-richtlijnen.
provider: deepseek
model: deepseek-chat
Hier is een Vue.js component voor een toegankelijk navigatiemenu met geavanceerde keyboard navigatie volgens WCAG-richtlijnen:
```vue
<template>
<nav
class="accessible-navigation"
role="navigation"
aria-label="Hoofdnavigatie"
>
<ul
ref="navList"
class="nav-list"
@keydown="handleKeydown"
>
<li
v-for="(item, index) in menuItems"
:key="item.id"
class="nav-item"
>
<a
v-if="!item.children"
:href="item.href"
class="nav-link"
:tabindex="isCurrentLink(index) ? 0 : -1"
@focus="setActiveIndex(index)"
@click="handleLinkClick"
>
{{ item.text }}
</a>
<div
v-else
class="dropdown-container"
>
<button
:id="`dropdown-button-${item.id}`"
class="dropdown-button"
:tabindex="isCurrentLink(index) ? 0 : -1"
:aria-expanded="isDropdownOpen(index)"
:aria-controls="`dropdown-menu-${item.id}`"
@focus="setActiveIndex(index)"
@click="toggleDropdown(index)"
@keydown="handleDropdownKeydown($event, index)"
>
{{ item.text }}
<span aria-hidden="true">▼</span>
</button>
<ul
v-if="isDropdownOpen(index)"
:id="`dropdown-menu-${item.id}`"
class="dropdown-menu"
role="menu"
:aria-labelledby="`dropdown-button-${item.id}`"
>
<li
v-for="child in item.children"
:key="child.id"
role="none"
>
<a
:href="child.href"
class="dropdown-link"
role="menuitem"
tabindex="-1"
@focus="setActiveChildIndex(index, child.id)"
@click="handleLinkClick"
>
{{ child.text }}
</a>
</li>
</ul>
</div>
</li>
</ul>
</nav>
</template>
<script>
export default {
name: 'AccessibleNavigation',
data() {
return {
activeIndex: 0,
openDropdownIndex: null,
activeChildIndex: null,
menuItems: [
{
id: 1,
text: 'Home',
href: '/home'
},
{
id: 2,
text: 'Diensten',
href: '#',
children: [
{ id: 21, text: 'Webdesign', href: '/webdesign' },
{ id: 22, text: 'Development', href: '/development' }
]
},
{
id: 3,
text: 'Contact',
href: '/contact'
}
]
}
},
mounted() {
// Focus het eerste element bij mount
this.$nextTick(() => {
this.focusCurrentElement();
});
},
methods: {
isCurrentLink(index) {
return index === this.activeIndex;
},
isDropdownOpen(index) {
return index === this.openDropdownIndex;
},
setActiveIndex(index) {
this.activeIndex = index;
this.activeChildIndex = null;
},
setActiveChildIndex(parentIndex, childId) {
this.activeIndex = parentIndex;
this.activeChildIndex = childId;
},
toggleDropdown(index) {
if (this.openDropdownIndex === index) {
this.openDropdownIndex = null;
this.activeChildIndex = null;
} else {
this.openDropdownIndex = index;
this.activeIndex = index;
// Focus het eerste dropdown item wanneer geopend
this.$nextTick(() => {
const firstChild = this.$el.querySelector(`#dropdown-menu-${this.menuItems[index].id} .dropdown-link`);
if (firstChild) {
firstChild.focus();
this.activeChildIndex = this.menuItems[index].children[0].id;
}
});
}
},
handleKeydown(event) {
const items = this.menuItems;
const currentIndex = this.activeIndex;
switch (event.key) {
case 'ArrowRight':
case 'ArrowDown':
event.preventDefault();
this.navigateNext();
break;
case 'ArrowLeft':
case 'ArrowUp':
event.preventDefault();
this.navigatePrevious();
break;
case 'Home':
event.preventDefault();
this.activeIndex = 0;
this.focusCurrentElement();
break;
case 'End':
event.preventDefault();
this.activeIndex = items.length - 1;
this.focusCurrentElement();
break;
case 'Enter':
case ' ':
event.preventDefault();
if (items[currentIndex].children) {
this.toggleDropdown(currentIndex);
} else {
this.activateLink(currentIndex);
}
break;
case 'Escape':
if (this.openDropdownIndex !== null) {
this.closeDropdown();
}
break;
}
},
handleDropdownKeydown(event, index) {
if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
this.toggleDropdown(index);
}
},
navigateNext() {
this.activeIndex = (this.activeIndex + 1) % this.menuItems.length;
this.focusCurrentElement();
},
navigatePrevious() {
this.activeIndex = this.activeIndex === 0
? this.menuItems.length - 1
: this.activeIndex - 1;
this.focusCurrentElement();
},
focusCurrentElement() {
this.$nextTick(() => {
const currentElement = this.$el.querySelector('[tabindex="0"]');
if (currentElement) {
currentElement.focus();
}
});
},
activateLink(index) {
const link = this.menuItems[index];
if (link.href && link.href !== '#') {
// Navigeer naar de link
window.location.href = link.href;
}
},
closeDropdown() {
this.openDropdownIndex = null;
this.activeChildIndex = null;
// Focus terug naar de dropdown button
this.$nextTick(() => {
const dropdownButton = this.$el.querySelector(`#dropdown-button-${this.menuItems[this.activeIndex].id}`);
if (dropdownButton) {
dropdownButton.focus();
}
});
},
handleLinkClick() {
// Optioneel: tracking of andere click handlers
console.log('Link geklikt');
}
}
}
</script>
<style scoped>
.accessible-navigation {
font-family: Arial, sans-serif;
}
.nav-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
gap: 1rem;
}
.nav-item {
margin: 0;
}
.nav-link,
.dropdown-button {
padding: 0.5rem 1rem;
text-decoration: none;
background: none;
border: 2px solid transparent;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.nav-link:focus,
.dropdown-button:focus {
outline: none;
border-color: #ff0;
box-shadow: 0 0 0 3px rgba(255, 255, 0, 0.5);
}
.dropdown-container {
position: relative;
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
list-style: none;
padding: 0.5rem 0;
margin: 0;
min-width: 200px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.dropdown-link {
display: block;
padding: 0.5rem 1rem;
text-decoration: none;
color: #333;
border: 2px solid transparent;
}
.dropdown-link:focus {
outline: none;
border-color: #ff0;
background-color: #f0f0f0;
}
/* Visually hidden class voor screen readers */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>
```
## Belangrijke WCAG-implementatiedetails:
### **Focus Management:**
- `tabindex="0"` voor huidig actief element, `tabindex="-1"` voor andere
- Programmatische focus met `focus()` method
- Logische tab-volgorde behouden
### **Keyboard Shortcuts:**
- **Pijltjestoetsen**: Navigeren tussen menu items
- **Tab/Shift+Tab**: Standaard browser navigatie
- **Enter/Spatie**: Activeren van links/dropdowns
- **Escape**: Sluiten dropdowns
- **Home/End**: Naar eerste/laatste element
### **ARIA Attributes:**
- `aria-label` voor navigatie context
- `aria-expanded` voor dropdown status
- `aria-controls` voor dropdown relatie
- `role="navigation"`, `role="menu"` voor semantiek
### **Focus Styling:**
- Dikke gele rand (`border-color: #ff0`)
- Geen `outline: none` zonder alternatief
- Hoge contrast ratio voor toegankelijkheid
### **WCAG Compliance:**
- **2.1.1 Keyboard**: Volledige keyboard toegang
- **2.4.3 Focus Order**: Logische focus volgorde
- **2.4.7 Focus Visible**: Duidelijke focus indicator
- **4.1.2 Name, Role, Value**: Juiste ARIA attributes
Dit component voldoet aan WCAG 2.1 AA niveau en zorgt voor een optimale toegankelijkheid voor keyboard gebruikers en screen reader gebruikers.
error: Generation failed [deepseek] prompt_id=4289 locale=en err=DeepSeek connection/timeout | Generation failed [deepseek] prompt_id=4289 locale=pt err=DeepSeek connection/timeout | Generation failed [deepseek] prompt_id=4289 locale=zh err=DeepSeek connection/timeout