1
1
<template >
2
- <div class =" container" >
2
+ <div
3
+ class =" container"
4
+ tabindex =" 0"
5
+ >
3
6
<sortable
4
7
:list =" modelValue"
5
8
tag =" ul"
9
12
@end =" drag = false"
10
13
item-key =" serviceID"
11
14
>
12
- <template #item =" { element } " >
15
+ <template #item =" { element , index } " >
13
16
<li
14
17
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"
16
26
>
17
27
<input
18
28
type =" checkbox"
29
39
>
30
40
<b >{{ element.title }}</b >
31
41
</span >
32
-
33
- <!-- Drag-Handle Icon -->
34
42
<span
35
43
v-if =" isDraggable"
36
44
class =" drag-handle"
37
45
title =" Element verschieben"
38
46
>
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 >
40
53
</span >
41
54
</li >
42
55
</template >
@@ -63,7 +76,14 @@ import type DummyChecklistItem from "@/api/dummyservice/DummyChecklistItem.ts";
63
76
64
77
import { MucIcon } from " @muenchen/muc-patternlab-vue" ;
65
78
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" ;
67
87
68
88
const props = withDefaults (
69
89
defineProps <{
@@ -76,19 +96,30 @@ const props = withDefaults(
76
96
disabled: false ,
77
97
}
78
98
);
79
- const emit = defineEmits ([" checked" , " label-click" ]);
99
+ const emit = defineEmits ([" checked" , " label-click" , " update:modelValue" ]);
100
+
80
101
const drag = ref (false );
102
+ const focusedIndex = ref <number | null >(null );
103
+ const draggedIndex = ref <number | null >(null );
81
104
82
105
const sortableOptions = computed (() => ({
83
106
animation: 200 ,
84
107
handle: props .isDraggable ? " .drag-handle" : undefined ,
85
108
ghostClass: " drag-ghost" ,
86
- disabled: ! props .isDraggable
109
+ disabled: ! props .isDraggable ,
87
110
}));
88
111
89
112
const dialogVisible = ref (false );
90
113
const dialogItem = ref <DummyChecklistItem | null >(null );
91
114
115
+ onMounted (() => {
116
+ window .addEventListener (" keydown" , handleKeyDown );
117
+ });
118
+
119
+ onBeforeUnmount (() => {
120
+ window .removeEventListener (" keydown" , handleKeyDown );
121
+ });
122
+
92
123
function onSelectChange(serviceID : string ) {
93
124
emit (" checked" , serviceID );
94
125
}
@@ -102,13 +133,74 @@ function openDialog(item: DummyChecklistItem) {
102
133
function closeDialog() {
103
134
dialogVisible .value = false ;
104
135
}
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
+ }
105
191
</script >
106
192
107
193
<style scoped>
108
194
.drag-ghost {
109
195
background-color : #e1f0fc !important ;
110
196
box-shadow : 0 2px 8px #007acc30 ;
111
197
}
198
+
199
+ .keyboard-dragging {
200
+ outline : 2px solid var (--color-brand-main-blue );
201
+ background-color : #d0e7ff ;
202
+ }
203
+
112
204
.container {
113
205
max-width : 600px ;
114
206
margin : 1rem auto ;
@@ -165,7 +257,6 @@ function closeDialog() {
165
257
background-color : #cce4ff ;
166
258
}
167
259
168
- /* blue circle inside on hover (slightly transparent) */
169
260
.radio-look :hover ::before {
170
261
content : " " ;
171
262
position : absolute ;
@@ -181,7 +272,6 @@ function closeDialog() {
181
272
transition : opacity 0.2s ease ;
182
273
}
183
274
184
- /* blue circle with white tick when selected */
185
275
.radio-look :checked {
186
276
border-color : var (--color-brand-main-blue );
187
277
background-color : var (--color-brand-main-blue );
@@ -216,7 +306,7 @@ function closeDialog() {
216
306
user-select : none ;
217
307
font-size : 24px ;
218
308
margin-left : auto ;
219
- color : var ( --color-neutrals-grey ) ;
309
+ color : #617586 ;
220
310
display : flex ;
221
311
align-items : center ;
222
312
}
0 commit comments