Skip to content

Commit 011f201

Browse files
author
Dominik Grenz
committed
Add aria attributes and eventhandling for keyboard
1 parent e3b9fa8 commit 011f201

File tree

1 file changed

+102
-12
lines changed

1 file changed

+102
-12
lines changed

personalization-webcomponents/src/components/ChecklistList.vue

Lines changed: 102 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
<template>
2-
<div class="container">
2+
<div
3+
class="container"
4+
tabindex="0"
5+
>
36
<sortable
47
:list="modelValue"
58
tag="ul"
@@ -9,10 +12,17 @@
912
@end="drag = false"
1013
item-key="serviceID"
1114
>
12-
<template #item="{ element }">
15+
<template #item="{ element, index }">
1316
<li
1417
class="list-item"
15-
:class="{ muted: element.checked !== null }"
18+
:class="{
19+
muted: element.checked !== null,
20+
'keyboard-dragging': draggedIndex === index,
21+
}"
22+
:aria-grabbed="draggedIndex === index ? 'true' : 'false'"
23+
tabindex="0"
24+
@focus="focusedIndex = index"
25+
:key="element.serviceID"
1626
>
1727
<input
1828
type="checkbox"
@@ -29,14 +39,17 @@
2939
>
3040
<b>{{ element.title }}</b>
3141
</span>
32-
33-
<!-- Drag-Handle Icon -->
3442
<span
3543
v-if="isDraggable"
3644
class="drag-handle"
3745
title="Element verschieben"
3846
>
39-
<muc-icon icon="drag-vertical" />
47+
<template v-if="draggedIndex === index">
48+
<muc-icon icon="arrow-up-down" />
49+
</template>
50+
<template v-else>
51+
<muc-icon icon="drag-vertical" />
52+
</template>
4053
</span>
4154
</li>
4255
</template>
@@ -63,7 +76,14 @@ import type DummyChecklistItem from "@/api/dummyservice/DummyChecklistItem.ts";
6376
6477
import { MucIcon } from "@muenchen/muc-patternlab-vue";
6578
import { Sortable } from "sortablejs-vue3";
66-
import {computed, defineEmits, ref} from "vue";
79+
import {
80+
computed,
81+
defineEmits,
82+
nextTick,
83+
onBeforeUnmount,
84+
onMounted,
85+
ref,
86+
} from "vue";
6787
6888
const props = withDefaults(
6989
defineProps<{
@@ -76,19 +96,30 @@ const props = withDefaults(
7696
disabled: false,
7797
}
7898
);
79-
const emit = defineEmits(["checked", "label-click"]);
99+
const emit = defineEmits(["checked", "label-click", "update:modelValue"]);
100+
80101
const drag = ref(false);
102+
const focusedIndex = ref<number | null>(null);
103+
const draggedIndex = ref<number | null>(null);
81104
82105
const sortableOptions = computed(() => ({
83106
animation: 200,
84107
handle: props.isDraggable ? ".drag-handle" : undefined,
85108
ghostClass: "drag-ghost",
86-
disabled: !props.isDraggable
109+
disabled: !props.isDraggable,
87110
}));
88111
89112
const dialogVisible = ref(false);
90113
const dialogItem = ref<DummyChecklistItem | null>(null);
91114
115+
onMounted(() => {
116+
window.addEventListener("keydown", handleKeyDown);
117+
});
118+
119+
onBeforeUnmount(() => {
120+
window.removeEventListener("keydown", handleKeyDown);
121+
});
122+
92123
function onSelectChange(serviceID: string) {
93124
emit("checked", serviceID);
94125
}
@@ -102,13 +133,74 @@ function openDialog(item: DummyChecklistItem) {
102133
function closeDialog() {
103134
dialogVisible.value = false;
104135
}
136+
137+
function handleKeyDown(event: KeyboardEvent) {
138+
if (!props.isDraggable) return;
139+
if (focusedIndex.value === null) return;
140+
141+
const maxIndex = props.modelValue.length - 1;
142+
143+
if (event.key === "Enter") {
144+
if (draggedIndex.value === null) {
145+
draggedIndex.value = focusedIndex.value;
146+
} else {
147+
draggedIndex.value = null;
148+
}
149+
} else if (draggedIndex.value !== null) {
150+
if (event.key === "ArrowUp" && draggedIndex.value > 0) {
151+
event.preventDefault();
152+
const list = [...props.modelValue];
153+
const temp = list[draggedIndex.value];
154+
list[draggedIndex.value] = list[draggedIndex.value - 1];
155+
list[draggedIndex.value - 1] = temp;
156+
157+
emit("update:modelValue", list);
158+
draggedIndex.value = draggedIndex.value - 1;
159+
focusedIndex.value = draggedIndex.value;
160+
161+
nextTick(() => {
162+
if (draggedIndex.value !== null) {
163+
const el = document.querySelectorAll(".list-item")[
164+
draggedIndex.value
165+
] as HTMLElement;
166+
el?.focus();
167+
}
168+
});
169+
} else if (event.key === "ArrowDown" && draggedIndex.value < maxIndex) {
170+
event.preventDefault();
171+
const list = [...props.modelValue];
172+
const temp = list[draggedIndex.value];
173+
list[draggedIndex.value] = list[draggedIndex.value + 1];
174+
list[draggedIndex.value + 1] = temp;
175+
176+
emit("update:modelValue", list);
177+
draggedIndex.value = draggedIndex.value + 1;
178+
focusedIndex.value = draggedIndex.value;
179+
180+
nextTick(() => {
181+
if (draggedIndex.value !== null) {
182+
const el = document.querySelectorAll(".list-item")[
183+
draggedIndex.value
184+
] as HTMLElement;
185+
el?.focus();
186+
}
187+
});
188+
}
189+
}
190+
}
105191
</script>
106192

107193
<style scoped>
108194
.drag-ghost {
109195
background-color: #e1f0fc !important;
110196
box-shadow: 0 2px 8px #007acc30;
111197
}
198+
199+
.keyboard-dragging {
200+
outline: 2px solid var(--color-brand-main-blue);
201+
background-color: #d0e7ff;
202+
}
203+
112204
.container {
113205
max-width: 600px;
114206
margin: 1rem auto;
@@ -165,7 +257,6 @@ function closeDialog() {
165257
background-color: #cce4ff;
166258
}
167259
168-
/* blue circle inside on hover (slightly transparent) */
169260
.radio-look:hover::before {
170261
content: "";
171262
position: absolute;
@@ -181,7 +272,6 @@ function closeDialog() {
181272
transition: opacity 0.2s ease;
182273
}
183274
184-
/* blue circle with white tick when selected */
185275
.radio-look:checked {
186276
border-color: var(--color-brand-main-blue);
187277
background-color: var(--color-brand-main-blue);
@@ -216,7 +306,7 @@ function closeDialog() {
216306
user-select: none;
217307
font-size: 24px;
218308
margin-left: auto;
219-
color: var(--color-neutrals-grey);
309+
color: #617586;
220310
display: flex;
221311
align-items: center;
222312
}

0 commit comments

Comments
 (0)