@gitlab/ui 91.14.0 → 91.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu.js +83 -15
- package/dist/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_items.js +8 -2
- package/dist/components/experimental/duo/chat/components/duo_chat_context/utils.js +12 -1
- package/package.json +1 -1
- package/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu.vue +86 -13
- package/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_items.vue +8 -1
- package/src/components/experimental/duo/chat/components/duo_chat_context/utils.js +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [91.15.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v91.14.0...v91.15.0) (2024-09-10)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* add duo chat context menu keyboard interactions ([a031d8c](https://gitlab.com/gitlab-org/gitlab-ui/commit/a031d8c823757e7a22138c3bdc7de9ff24c25da9))
|
|
7
|
+
|
|
1
8
|
# [91.14.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v91.13.0...v91.14.0) (2024-09-10)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import debounce from 'lodash/debounce';
|
|
2
2
|
import { translate } from '../../../../../../../utils/i18n';
|
|
3
3
|
import GlCard from '../../../../../../base/card/card';
|
|
4
|
-
import { contextItemsValidator, categoriesValidator } from '../utils';
|
|
5
4
|
import GlDuoChatContextItemSelections from '../duo_chat_context_item_selections/duo_chat_context_item_selections';
|
|
5
|
+
import { contextItemsValidator, categoriesValidator, wrapIndex } from '../utils';
|
|
6
6
|
import GlDuoChatContextItemMenuCategoryItems from './duo_chat_context_item_menu_category_items';
|
|
7
7
|
import GlDuoChatContextItemMenuSearchItems from './duo_chat_context_item_menu_search_items';
|
|
8
8
|
import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
|
|
@@ -66,14 +66,17 @@ var script = {
|
|
|
66
66
|
},
|
|
67
67
|
data() {
|
|
68
68
|
return {
|
|
69
|
-
|
|
69
|
+
selectedCategory: null,
|
|
70
70
|
searchQuery: '',
|
|
71
|
-
|
|
71
|
+
activeIndex: 0
|
|
72
72
|
};
|
|
73
73
|
},
|
|
74
74
|
computed: {
|
|
75
75
|
showCategorySelection() {
|
|
76
76
|
return this.open && !this.selectedCategory;
|
|
77
|
+
},
|
|
78
|
+
allResultsAreDisabled() {
|
|
79
|
+
return this.results.every(result => !result.isEnabled);
|
|
77
80
|
}
|
|
78
81
|
},
|
|
79
82
|
watch: {
|
|
@@ -84,14 +87,22 @@ var script = {
|
|
|
84
87
|
},
|
|
85
88
|
searchQuery(query) {
|
|
86
89
|
this.debouncedSearch(query);
|
|
90
|
+
},
|
|
91
|
+
results(newResults) {
|
|
92
|
+
const firstEnabledIndex = newResults.findIndex(result => result.isEnabled);
|
|
93
|
+
this.activeIndex = firstEnabledIndex >= 0 ? firstEnabledIndex : 0;
|
|
87
94
|
}
|
|
88
95
|
},
|
|
89
96
|
methods: {
|
|
90
97
|
selectCategory(category) {
|
|
91
|
-
this.activeIndex = 0;
|
|
92
98
|
this.searchQuery = '';
|
|
93
99
|
this.selectedCategory = category;
|
|
94
|
-
|
|
100
|
+
this.$emit('search', {
|
|
101
|
+
category: category.value,
|
|
102
|
+
query: ''
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
debouncedSearch: debounce(function search(query) {
|
|
95
106
|
/**
|
|
96
107
|
* Emitted when a search should be performed.
|
|
97
108
|
* @property {Object} filter
|
|
@@ -99,10 +110,10 @@ var script = {
|
|
|
99
110
|
* @property {string} filter.query - The search query
|
|
100
111
|
*/
|
|
101
112
|
this.$emit('search', {
|
|
102
|
-
category:
|
|
103
|
-
query
|
|
113
|
+
category: this.selectedCategory.value,
|
|
114
|
+
query
|
|
104
115
|
});
|
|
105
|
-
},
|
|
116
|
+
}, SEARCH_DEBOUNCE_MS),
|
|
106
117
|
selectItem(item) {
|
|
107
118
|
if (!item.isEnabled) {
|
|
108
119
|
return;
|
|
@@ -127,15 +138,72 @@ var script = {
|
|
|
127
138
|
*/
|
|
128
139
|
this.$emit('remove', item);
|
|
129
140
|
},
|
|
130
|
-
debouncedSearch: debounce(function search(query) {
|
|
131
|
-
this.$emit('search', {
|
|
132
|
-
category: this.selectedCategory.value,
|
|
133
|
-
query
|
|
134
|
-
});
|
|
135
|
-
}, SEARCH_DEBOUNCE_MS),
|
|
136
141
|
resetSelection() {
|
|
137
142
|
this.selectedCategory = null;
|
|
143
|
+
this.searchQuery = '';
|
|
138
144
|
this.activeIndex = 0;
|
|
145
|
+
},
|
|
146
|
+
async scrollActiveItemIntoView() {
|
|
147
|
+
await this.$nextTick();
|
|
148
|
+
const activeItem = document.getElementById(`dropdown-item-${this.activeIndex}`);
|
|
149
|
+
if (activeItem) {
|
|
150
|
+
activeItem.scrollIntoView({
|
|
151
|
+
block: 'nearest',
|
|
152
|
+
inline: 'start'
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
handleKeyUp(e) {
|
|
157
|
+
switch (e.key) {
|
|
158
|
+
case 'ArrowDown':
|
|
159
|
+
case 'ArrowUp':
|
|
160
|
+
e.preventDefault();
|
|
161
|
+
this.moveActiveIndex(e.key === 'ArrowDown' ? 1 : -1);
|
|
162
|
+
this.scrollActiveItemIntoView();
|
|
163
|
+
break;
|
|
164
|
+
case 'Enter':
|
|
165
|
+
e.preventDefault();
|
|
166
|
+
if (this.showCategorySelection) {
|
|
167
|
+
this.selectCategory(this.categories[this.activeIndex]);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (!this.results.length) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.selectItem(this.results[this.activeIndex]);
|
|
174
|
+
break;
|
|
175
|
+
case 'Escape':
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
if (this.showCategorySelection) {
|
|
178
|
+
this.$emit('close');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
this.selectedCategory = null;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
moveActiveIndex(step) {
|
|
186
|
+
if (this.showCategorySelection) {
|
|
187
|
+
// Categories cannot be disabled, so just loop to the next/prev one
|
|
188
|
+
this.activeIndex = wrapIndex(this.activeIndex, step, this.categories.length);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Return early if there are no results or all results are disabled
|
|
193
|
+
if (!this.results.length || this.allResultsAreDisabled) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// contextItems CAN be disabled, so loop to next/prev but ensure we don't land on a disabled one
|
|
198
|
+
let newIndex = this.activeIndex;
|
|
199
|
+
do {
|
|
200
|
+
newIndex = wrapIndex(newIndex, step, this.results.length);
|
|
201
|
+
if (newIndex === this.activeIndex) {
|
|
202
|
+
// If we've looped through all items and found no enabled ones, keep the current index
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
} while (!this.results[newIndex].isEnabled);
|
|
206
|
+
this.activeIndex = newIndex;
|
|
139
207
|
}
|
|
140
208
|
},
|
|
141
209
|
i18n: {
|
|
@@ -147,7 +215,7 @@ var script = {
|
|
|
147
215
|
const __vue_script__ = script;
|
|
148
216
|
|
|
149
217
|
/* template */
|
|
150
|
-
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',[(_vm.selections.length)?_c('gl-duo-chat-context-item-selections',{staticClass:"gl-mb-3",attrs:{"selections":_vm.selections,"removable":true,"title":_vm.$options.i18n.selectedContextItemsTitle,"default-collapsed":false},on:{"remove":_vm.removeItem}}):_vm._e(),_vm._v(" "),(_vm.open)?_c('gl-card',{staticClass:"slash-commands !gl-absolute gl-bottom-0 gl-w-full gl-pl-0 gl-shadow-md",attrs:{"body-class":"!gl-p-2","data-testid":"context-item-menu"}},[(_vm.showCategorySelection)?_c('gl-duo-chat-context-item-menu-category-items',{attrs:{"active-index":_vm.activeIndex,"categories":_vm.categories},on:{"select":_vm.selectCategory,"active-index-change":function($event){_vm.activeIndex = $event;}}}):_c('gl-duo-chat-context-item-menu-search-items',{attrs:{"active-index":_vm.activeIndex,"category":_vm.selectedCategory,"loading":_vm.loading,"error":_vm.error,"results":_vm.results},on:{"select":_vm.selectItem,"active-index-change":function($event){_vm.activeIndex = $event;}},model:{value:(_vm.searchQuery),callback:function ($$v) {_vm.searchQuery=$$v;},expression:"searchQuery"}})],1):_vm._e()],1)};
|
|
218
|
+
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',[(_vm.selections.length)?_c('gl-duo-chat-context-item-selections',{staticClass:"gl-mb-3",attrs:{"selections":_vm.selections,"removable":true,"title":_vm.$options.i18n.selectedContextItemsTitle,"default-collapsed":false},on:{"remove":_vm.removeItem}}):_vm._e(),_vm._v(" "),(_vm.open)?_c('gl-card',{staticClass:"slash-commands !gl-absolute gl-bottom-0 gl-w-full gl-pl-0 gl-shadow-md",attrs:{"body-class":"!gl-p-2","data-testid":"context-item-menu"}},[(_vm.showCategorySelection)?_c('gl-duo-chat-context-item-menu-category-items',{attrs:{"active-index":_vm.activeIndex,"categories":_vm.categories},on:{"select":_vm.selectCategory,"active-index-change":function($event){_vm.activeIndex = $event;}}}):_c('gl-duo-chat-context-item-menu-search-items',{attrs:{"active-index":_vm.activeIndex,"category":_vm.selectedCategory,"loading":_vm.loading,"error":_vm.error,"results":_vm.results},on:{"select":_vm.selectItem,"keyup":_vm.handleKeyUp,"active-index-change":function($event){_vm.activeIndex = $event;}},model:{value:(_vm.searchQuery),callback:function ($$v) {_vm.searchQuery=$$v;},expression:"searchQuery"}})],1):_vm._e()],1)};
|
|
151
219
|
var __vue_staticRenderFns__ = [];
|
|
152
220
|
|
|
153
221
|
/* style */
|
|
@@ -78,11 +78,17 @@ var script = {
|
|
|
78
78
|
this.$emit('select', contextItem);
|
|
79
79
|
this.userInitiatedSearch = false;
|
|
80
80
|
},
|
|
81
|
+
handleKeyUp(e) {
|
|
82
|
+
this.$emit('keyup', e);
|
|
83
|
+
},
|
|
81
84
|
setActiveIndex(index) {
|
|
82
85
|
var _this$results$index;
|
|
83
86
|
if ((_this$results$index = this.results[index]) !== null && _this$results$index !== void 0 && _this$results$index.isEnabled) {
|
|
84
87
|
this.$emit('active-index-change', index);
|
|
85
88
|
}
|
|
89
|
+
},
|
|
90
|
+
isActiveItem(contextItem, index) {
|
|
91
|
+
return index === this.activeIndex && contextItem.isEnabled;
|
|
86
92
|
}
|
|
87
93
|
},
|
|
88
94
|
i18n: {
|
|
@@ -95,9 +101,9 @@ const __vue_script__ = script;
|
|
|
95
101
|
|
|
96
102
|
/* template */
|
|
97
103
|
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',[_c('div',{staticClass:"gl-max-h-31 gl-overflow-y-scroll"},[(_vm.loading)?_c('gl-duo-chat-context-item-menu-search-items-loading',{attrs:{"rows":_vm.numLoadingItems}}):(_vm.error)?_c('gl-alert',{staticClass:"gl-m-3",attrs:{"variant":"danger","dismissible":false,"data-testid":"search-results-error"}},[_vm._v("\n "+_vm._s(_vm.error)+"\n ")]):(_vm.showEmptyState)?_c('div',{staticClass:"gl-rounded-base gl-p-3 gl-text-center gl-text-secondary",attrs:{"data-testid":"search-results-empty-state"}},[_vm._v("\n "+_vm._s(_vm.$options.i18n.emptyStateMessage)+"\n ")]):_c('ul',{staticClass:"gl-mb-1 gl-list-none gl-flex-row gl-pl-0"},_vm._l((_vm.results),function(contextItem,index){return _c('gl-dropdown-item',{key:contextItem.id,staticClass:"duo-chat-context-search-result-item",class:{
|
|
98
|
-
'active-command':
|
|
104
|
+
'active-command': _vm.isActiveItem(contextItem, index),
|
|
99
105
|
'gl-cursor-not-allowed [&>button]:focus-within:!gl-shadow-none': !contextItem.isEnabled,
|
|
100
|
-
},attrs:{"id":("dropdown-item-" + index),"tabindex":!contextItem.isEnabled ? -1 : undefined,"data-testid":"search-result-item"},on:{"click":function($event){return _vm.selectItem(contextItem)}}},[_c('div',{on:{"mouseenter":function($event){return _vm.setActiveIndex(index)}}},[_c('gl-duo-chat-context-item-menu-search-item',{class:{ 'gl-text-secondary': !contextItem.isEnabled },attrs:{"context-item":contextItem,"category":_vm.category,"data-testid":"search-result-item-details"}})],1)])}),1)],1),_vm._v(" "),_c('gl-form-input',{ref:"contextMenuSearchInput",attrs:{"value":_vm.searchQuery,"placeholder":_vm.searchInputPlaceholder,"autofocus":"","data-testid":"context-menu-search-input"},on:{"input":function($event){return _vm.$emit('update:searchQuery', $event)}}})],1)};
|
|
106
|
+
},attrs:{"id":("dropdown-item-" + index),"tabindex":!contextItem.isEnabled ? -1 : undefined,"data-testid":"search-result-item"},on:{"click":function($event){return _vm.selectItem(contextItem)}}},[_c('div',{on:{"mouseenter":function($event){return _vm.setActiveIndex(index)}}},[_c('gl-duo-chat-context-item-menu-search-item',{class:{ 'gl-text-secondary': !contextItem.isEnabled },attrs:{"context-item":contextItem,"category":_vm.category,"data-testid":"search-result-item-details"}})],1)])}),1)],1),_vm._v(" "),_c('gl-form-input',{ref:"contextMenuSearchInput",attrs:{"value":_vm.searchQuery,"placeholder":_vm.searchInputPlaceholder,"autofocus":"","data-testid":"context-menu-search-input"},on:{"input":function($event){return _vm.$emit('update:searchQuery', $event)},"keyup":_vm.handleKeyUp}})],1)};
|
|
101
107
|
var __vue_staticRenderFns__ = [];
|
|
102
108
|
|
|
103
109
|
/* style */
|
|
@@ -22,4 +22,15 @@ function formatMergeRequestId(iid) {
|
|
|
22
22
|
return `!${iid}`;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Calculates a new index within a range. If the new index would fall out of bounds, wraps to the start/end of the range.
|
|
27
|
+
* @param {number} currentIndex - The starting index.
|
|
28
|
+
* @param {number} step - The number of steps to move (positive or negative).
|
|
29
|
+
* @param {number} totalLength - The total number of items in the range.
|
|
30
|
+
* @returns {number} The new index.
|
|
31
|
+
*/
|
|
32
|
+
function wrapIndex(currentIndex, step, totalLength) {
|
|
33
|
+
return (currentIndex + step + totalLength) % totalLength;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { categoriesValidator, categoryValidator, contextItemValidator, contextItemsValidator, formatIssueId, formatMergeRequestId, wrapIndex };
|
package/package.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import debounce from 'lodash/debounce';
|
|
3
3
|
import { translate } from '../../../../../../../utils/i18n';
|
|
4
4
|
import GlCard from '../../../../../../base/card/card.vue';
|
|
5
|
-
import { categoriesValidator, contextItemsValidator } from '../utils';
|
|
6
5
|
import GlDuoChatContextItemSelections from '../duo_chat_context_item_selections/duo_chat_context_item_selections.vue';
|
|
6
|
+
import { categoriesValidator, contextItemsValidator, wrapIndex } from '../utils';
|
|
7
7
|
import GlDuoChatContextItemMenuCategoryItems from './duo_chat_context_item_menu_category_items.vue';
|
|
8
8
|
import GlDuoChatContextItemMenuSearchItems from './duo_chat_context_item_menu_search_items.vue';
|
|
9
9
|
|
|
@@ -67,15 +67,18 @@ export default {
|
|
|
67
67
|
},
|
|
68
68
|
data() {
|
|
69
69
|
return {
|
|
70
|
-
activeIndex: 0,
|
|
71
|
-
searchQuery: '',
|
|
72
70
|
selectedCategory: null,
|
|
71
|
+
searchQuery: '',
|
|
72
|
+
activeIndex: 0,
|
|
73
73
|
};
|
|
74
74
|
},
|
|
75
75
|
computed: {
|
|
76
76
|
showCategorySelection() {
|
|
77
77
|
return this.open && !this.selectedCategory;
|
|
78
78
|
},
|
|
79
|
+
allResultsAreDisabled() {
|
|
80
|
+
return this.results.every((result) => !result.isEnabled);
|
|
81
|
+
},
|
|
79
82
|
},
|
|
80
83
|
watch: {
|
|
81
84
|
open(isOpen) {
|
|
@@ -86,13 +89,22 @@ export default {
|
|
|
86
89
|
searchQuery(query) {
|
|
87
90
|
this.debouncedSearch(query);
|
|
88
91
|
},
|
|
92
|
+
results(newResults) {
|
|
93
|
+
const firstEnabledIndex = newResults.findIndex((result) => result.isEnabled);
|
|
94
|
+
this.activeIndex = firstEnabledIndex >= 0 ? firstEnabledIndex : 0;
|
|
95
|
+
},
|
|
89
96
|
},
|
|
90
97
|
methods: {
|
|
91
98
|
selectCategory(category) {
|
|
92
|
-
this.activeIndex = 0;
|
|
93
99
|
this.searchQuery = '';
|
|
94
100
|
this.selectedCategory = category;
|
|
95
101
|
|
|
102
|
+
this.$emit('search', {
|
|
103
|
+
category: category.value,
|
|
104
|
+
query: '',
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
debouncedSearch: debounce(function search(query) {
|
|
96
108
|
/**
|
|
97
109
|
* Emitted when a search should be performed.
|
|
98
110
|
* @property {Object} filter
|
|
@@ -100,10 +112,10 @@ export default {
|
|
|
100
112
|
* @property {string} filter.query - The search query
|
|
101
113
|
*/
|
|
102
114
|
this.$emit('search', {
|
|
103
|
-
category:
|
|
104
|
-
query
|
|
115
|
+
category: this.selectedCategory.value,
|
|
116
|
+
query,
|
|
105
117
|
});
|
|
106
|
-
},
|
|
118
|
+
}, SEARCH_DEBOUNCE_MS),
|
|
107
119
|
selectItem(item) {
|
|
108
120
|
if (!item.isEnabled) {
|
|
109
121
|
return;
|
|
@@ -131,16 +143,76 @@ export default {
|
|
|
131
143
|
*/
|
|
132
144
|
this.$emit('remove', item);
|
|
133
145
|
},
|
|
134
|
-
debouncedSearch: debounce(function search(query) {
|
|
135
|
-
this.$emit('search', {
|
|
136
|
-
category: this.selectedCategory.value,
|
|
137
|
-
query,
|
|
138
|
-
});
|
|
139
|
-
}, SEARCH_DEBOUNCE_MS),
|
|
140
146
|
resetSelection() {
|
|
141
147
|
this.selectedCategory = null;
|
|
148
|
+
this.searchQuery = '';
|
|
142
149
|
this.activeIndex = 0;
|
|
143
150
|
},
|
|
151
|
+
async scrollActiveItemIntoView() {
|
|
152
|
+
await this.$nextTick();
|
|
153
|
+
|
|
154
|
+
const activeItem = document.getElementById(`dropdown-item-${this.activeIndex}`);
|
|
155
|
+
if (activeItem) {
|
|
156
|
+
activeItem.scrollIntoView({ block: 'nearest', inline: 'start' });
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
handleKeyUp(e) {
|
|
160
|
+
switch (e.key) {
|
|
161
|
+
case 'ArrowDown':
|
|
162
|
+
case 'ArrowUp':
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
this.moveActiveIndex(e.key === 'ArrowDown' ? 1 : -1);
|
|
165
|
+
this.scrollActiveItemIntoView();
|
|
166
|
+
break;
|
|
167
|
+
case 'Enter':
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
if (this.showCategorySelection) {
|
|
170
|
+
this.selectCategory(this.categories[this.activeIndex]);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (!this.results.length) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
this.selectItem(this.results[this.activeIndex]);
|
|
177
|
+
break;
|
|
178
|
+
case 'Escape':
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
if (this.showCategorySelection) {
|
|
181
|
+
this.$emit('close');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.selectedCategory = null;
|
|
186
|
+
break;
|
|
187
|
+
default:
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
moveActiveIndex(step) {
|
|
192
|
+
if (this.showCategorySelection) {
|
|
193
|
+
// Categories cannot be disabled, so just loop to the next/prev one
|
|
194
|
+
this.activeIndex = wrapIndex(this.activeIndex, step, this.categories.length);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Return early if there are no results or all results are disabled
|
|
199
|
+
if (!this.results.length || this.allResultsAreDisabled) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// contextItems CAN be disabled, so loop to next/prev but ensure we don't land on a disabled one
|
|
204
|
+
let newIndex = this.activeIndex;
|
|
205
|
+
do {
|
|
206
|
+
newIndex = wrapIndex(newIndex, step, this.results.length);
|
|
207
|
+
|
|
208
|
+
if (newIndex === this.activeIndex) {
|
|
209
|
+
// If we've looped through all items and found no enabled ones, keep the current index
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
} while (!this.results[newIndex].isEnabled);
|
|
213
|
+
|
|
214
|
+
this.activeIndex = newIndex;
|
|
215
|
+
},
|
|
144
216
|
},
|
|
145
217
|
i18n: {
|
|
146
218
|
selectedContextItemsTitle: translate(
|
|
@@ -184,6 +256,7 @@ export default {
|
|
|
184
256
|
:error="error"
|
|
185
257
|
:results="results"
|
|
186
258
|
@select="selectItem"
|
|
259
|
+
@keyup="handleKeyUp"
|
|
187
260
|
@active-index-change="activeIndex = $event"
|
|
188
261
|
/>
|
|
189
262
|
</gl-card>
|
|
@@ -83,11 +83,17 @@ export default {
|
|
|
83
83
|
this.$emit('select', contextItem);
|
|
84
84
|
this.userInitiatedSearch = false;
|
|
85
85
|
},
|
|
86
|
+
handleKeyUp(e) {
|
|
87
|
+
this.$emit('keyup', e);
|
|
88
|
+
},
|
|
86
89
|
setActiveIndex(index) {
|
|
87
90
|
if (this.results[index]?.isEnabled) {
|
|
88
91
|
this.$emit('active-index-change', index);
|
|
89
92
|
}
|
|
90
93
|
},
|
|
94
|
+
isActiveItem(contextItem, index) {
|
|
95
|
+
return index === this.activeIndex && contextItem.isEnabled;
|
|
96
|
+
},
|
|
91
97
|
},
|
|
92
98
|
i18n: {
|
|
93
99
|
emptyStateMessage: translate('GlDuoChatContextItemMenu.emptyStateMessage', 'No results found'),
|
|
@@ -120,7 +126,7 @@ export default {
|
|
|
120
126
|
:id="`dropdown-item-${index}`"
|
|
121
127
|
:key="contextItem.id"
|
|
122
128
|
:class="{
|
|
123
|
-
'active-command': index
|
|
129
|
+
'active-command': isActiveItem(contextItem, index),
|
|
124
130
|
'gl-cursor-not-allowed [&>button]:focus-within:!gl-shadow-none': !contextItem.isEnabled,
|
|
125
131
|
}"
|
|
126
132
|
:tabindex="!contextItem.isEnabled ? -1 : undefined"
|
|
@@ -146,6 +152,7 @@ export default {
|
|
|
146
152
|
autofocus
|
|
147
153
|
data-testid="context-menu-search-input"
|
|
148
154
|
@input="$emit('update:searchQuery', $event)"
|
|
155
|
+
@keyup="handleKeyUp"
|
|
149
156
|
/>
|
|
150
157
|
</div>
|
|
151
158
|
</template>
|
|
@@ -39,3 +39,14 @@ export function formatMergeRequestId(iid) {
|
|
|
39
39
|
|
|
40
40
|
return `!${iid}`;
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Calculates a new index within a range. If the new index would fall out of bounds, wraps to the start/end of the range.
|
|
45
|
+
* @param {number} currentIndex - The starting index.
|
|
46
|
+
* @param {number} step - The number of steps to move (positive or negative).
|
|
47
|
+
* @param {number} totalLength - The total number of items in the range.
|
|
48
|
+
* @returns {number} The new index.
|
|
49
|
+
*/
|
|
50
|
+
export function wrapIndex(currentIndex, step, totalLength) {
|
|
51
|
+
return (currentIndex + step + totalLength) % totalLength;
|
|
52
|
+
}
|