@@ -40,6 +40,16 @@ export default {
40
40
required: false ,
41
41
default: true ,
42
42
},
43
+ columnIndex: {
44
+ type: Number ,
45
+ required: false ,
46
+ default: 0 ,
47
+ },
48
+ rowIndex: {
49
+ type: Number ,
50
+ required: false ,
51
+ default: 0 ,
52
+ },
43
53
},
44
54
apollo: {
45
55
// eslint-disable-next-line @gitlab/vue-no-undef-apollo-properties
@@ -85,6 +95,9 @@ export default {
85
95
labels: this .item .labels ? .nodes || [],
86
96
};
87
97
},
98
+ showFocusBackground () {
99
+ return ! this .isActive && ! this .multiSelectVisible ;
100
+ },
88
101
},
89
102
methods: {
90
103
toggleIssue (e ) {
@@ -100,6 +113,7 @@ export default {
100
113
if (isMultiSelect && gon? .features ? .boardMultiSelect ) {
101
114
this .toggleBoardItemMultiSelection (this .item );
102
115
} else {
116
+ e .currentTarget .focus ();
103
117
this .toggleItem ();
104
118
this .track (' click_card' , { label: ' right_sidebar' });
105
119
}
@@ -137,6 +151,54 @@ export default {
137
151
},
138
152
});
139
153
},
154
+ changeFocusInColumn (currentCard , i ) {
155
+ // Building a list using data-col-index instead of just traversing the ul is necessary for swimlanes
156
+ const columnCards = [
157
+ ... document .querySelectorAll (
158
+ ` button.board-card-button[data-col-index="${ this .columnIndex } "]` ,
159
+ ),
160
+ ];
161
+ const currentIndex = columnCards .indexOf (currentCard);
162
+ if (currentIndex + i < 0 || currentIndex + i > columnCards .length - 1 ) {
163
+ return ;
164
+ }
165
+ columnCards[currentIndex + i].focus ();
166
+ },
167
+ focusNext (e ) {
168
+ this .changeFocusInColumn (e .target , 1 );
169
+ },
170
+ focusPrev (e ) {
171
+ this .changeFocusInColumn (e .target , - 1 );
172
+ },
173
+ changeFocusInRow (currentCard , i ) {
174
+ const currentList = currentCard .closest (' ul' );
175
+ // Find next in line list/cell with cards. If none, don't move.
176
+ let listSelector = ' board-list' ;
177
+ // Account for swimlanes using different structure. Swimlanes traverse within their lane.
178
+ if (currentList .classList .contains (' board-cell' )) {
179
+ listSelector = ` board-cell[data-row-index="${ this .rowIndex } "]` ;
180
+ }
181
+ const lists = [
182
+ ... document .querySelectorAll (` ul.${ listSelector} :not(.list-empty):not(.list-collapsed)` ),
183
+ ];
184
+ const currentIndex = lists .indexOf (currentList);
185
+ if (currentIndex + i < 0 || currentIndex + i > lists .length - 1 ) {
186
+ return ;
187
+ }
188
+ // Focus the same index if possible, or last card
189
+ const targetCards = lists[currentIndex + i].querySelectorAll (' button.board-card-button' );
190
+ if (targetCards .length <= this .index ) {
191
+ targetCards[targetCards .length - 1 ].focus ();
192
+ } else {
193
+ targetCards[this .index ].focus ();
194
+ }
195
+ },
196
+ focusLeft (e ) {
197
+ this .changeFocusInRow (e .target , - 1 );
198
+ },
199
+ focusRight (e ) {
200
+ this .changeFocusInRow (e .target , 1 );
201
+ },
140
202
},
141
203
};
142
204
< / script>
@@ -147,30 +209,47 @@ export default {
147
209
{
148
210
'multi-select gl-border-blue-200 gl-bg-blue-50': multiSelectVisible,
149
211
'gl-cursor-grab': isDraggable,
212
+ 'is-active !gl-bg-blue-50 hover:!gl-bg-blue-50': isActive,
150
213
'is-disabled': isDisabled,
151
- 'is-active gl-bg-blue-50 hover:!gl-bg-blue-50': isActive,
152
214
'gl-cursor-not-allowed gl-bg-gray-10': item.isLoading,
153
- 'gl-border-l-4 gl-pl-4 gl-border-l-solid': itemColor,
154
215
},
155
216
]"
156
217
: index= " index"
157
218
: data- item- id= " item.id"
158
219
: data- item- iid= " item.iid"
159
220
: data- item- path= " item.referencePath"
160
- : style= " cardStyle"
161
221
data- testid= " board-card"
162
- class = " board-card gl-border gl-relative gl-mb-3 gl-rounded-base gl-p-4 gl-leading-normal hover:gl-bg-gray-10"
163
- @click= " toggleIssue($event)"
222
+ class = " board-card gl-border gl-relative gl-mb-3 gl-rounded-base gl-leading-normal hover:gl-bg-gray-10"
164
223
>
165
- < board- card- inner
166
- : list= " list"
167
- : item= " formattedItem"
168
- : update- filters= " true"
169
- : index= " index"
170
- : show- work- item- type- icon= " showWorkItemTypeIcon"
171
- @setFilters= " $emit('setFilters', $event)"
224
+ < button
225
+ : class = " [
226
+ {
227
+ 'focus:gl-bg-gray-10': showFocusBackground,
228
+ 'gl-border-l-4 gl-pl-4 gl-border-l-solid': itemColor,
229
+ },
230
+ ]"
231
+ : aria- label= " item.title"
232
+ : data- col- index= " columnIndex"
233
+ : data- row- index= " rowIndex"
234
+ : style= " cardStyle"
235
+ data- testid= " board-card-button"
236
+ class = " board-card-button btn-transparent gl-block gl-h-full gl-w-full gl-rounded-base gl-p-4 gl-text-left gl-outline-none focus:gl-focus"
237
+ @click= " toggleIssue"
238
+ @keydown .left .exact .prevent = " focusLeft"
239
+ @keydown .right .exact .prevent = " focusRight"
240
+ @keydown .down .exact .prevent = " focusNext"
241
+ @keydown .up .exact .prevent = " focusPrev"
172
242
>
173
- < slot>< / slot>
174
- < / board- card- inner>
243
+ < board- card- inner
244
+ : list= " list"
245
+ : item= " formattedItem"
246
+ : update- filters= " true"
247
+ : index= " index"
248
+ : show- work- item- type- icon= " showWorkItemTypeIcon"
249
+ @setFilters= " $emit('setFilters', $event)"
250
+ >
251
+ < slot>< / slot>
252
+ < / board- card- inner>
253
+ < / button>
175
254
< / li>
176
255
< / template>
0 commit comments