@gitlab/ui 42.5.0 → 42.8.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 +21 -0
- package/dist/components/base/accordion/accordion_item.js +10 -1
- package/dist/components/base/form/form_combobox/constants.js +39 -2
- package/dist/components/base/form/form_combobox/form_combobox.js +23 -3
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/utility_classes.css +1 -1
- package/dist/utility_classes.css.map +1 -1
- package/package.json +2 -2
- package/src/components/base/accordion/accordion_item.spec.js +10 -0
- package/src/components/base/accordion/accordion_item.vue +9 -1
- package/src/components/base/form/form_combobox/constants.js +16 -1
- package/src/components/base/form/form_combobox/form_combobox.scss +1 -0
- package/src/components/base/form/form_combobox/form_combobox.spec.js +142 -95
- package/src/components/base/form/form_combobox/form_combobox.stories.js +42 -10
- package/src/components/base/form/form_combobox/form_combobox.vue +28 -9
- package/src/scss/utilities.scss +18 -0
- package/src/scss/utility-mixins/spacing.scss +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitlab/ui",
|
|
3
|
-
"version": "42.
|
|
3
|
+
"version": "42.8.0",
|
|
4
4
|
"description": "GitLab UI Components",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"bootstrap": "4.5.3",
|
|
106
106
|
"cypress": "^6.6.0",
|
|
107
107
|
"emoji-regex": "^10.0.0",
|
|
108
|
-
"eslint": "8.
|
|
108
|
+
"eslint": "8.18.0",
|
|
109
109
|
"eslint-import-resolver-jest": "3.0.2",
|
|
110
110
|
"eslint-plugin-cypress": "2.12.1",
|
|
111
111
|
"eslint-plugin-storybook": "0.5.12",
|
|
@@ -32,6 +32,7 @@ describe('GlAccordionItem', () => {
|
|
|
32
32
|
|
|
33
33
|
const findButton = () => wrapper.findComponent(GlButton);
|
|
34
34
|
const findCollapse = () => wrapper.findComponent(BCollapse);
|
|
35
|
+
const findHeader = () => wrapper.find('.gl-accordion-item-header');
|
|
35
36
|
|
|
36
37
|
it('renders button text', () => {
|
|
37
38
|
createComponent();
|
|
@@ -64,6 +65,15 @@ describe('GlAccordionItem', () => {
|
|
|
64
65
|
expect(wrapper.find('h4.gl-accordion-item-header').exists()).toBeTruthy();
|
|
65
66
|
});
|
|
66
67
|
|
|
68
|
+
it.each(['custom-header-class', ['custom-header-class'], { 'custom-header-class': true }])(
|
|
69
|
+
'applies custom classes to the header',
|
|
70
|
+
(customClassProp) => {
|
|
71
|
+
createComponent({ headerClass: customClassProp }, { defaultHeaderLevel: 3 });
|
|
72
|
+
|
|
73
|
+
expect(findHeader().classes()).toContain('custom-header-class');
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
|
|
67
77
|
it('renders slot text', () => {
|
|
68
78
|
createComponent();
|
|
69
79
|
|
|
@@ -54,6 +54,14 @@ When set, it will ensure the accordion item is initially visible
|
|
|
54
54
|
return value > 0 && value <= 6;
|
|
55
55
|
},
|
|
56
56
|
},
|
|
57
|
+
/**
|
|
58
|
+
* Additional CSS class(es) to be applied to the header.
|
|
59
|
+
*/
|
|
60
|
+
headerClass: {
|
|
61
|
+
type: [String, Object, Array],
|
|
62
|
+
required: false,
|
|
63
|
+
default: '',
|
|
64
|
+
},
|
|
57
65
|
},
|
|
58
66
|
data() {
|
|
59
67
|
return {
|
|
@@ -89,7 +97,7 @@ When set, it will ensure the accordion item is initially visible
|
|
|
89
97
|
|
|
90
98
|
<template>
|
|
91
99
|
<div class="gl-accordion-item">
|
|
92
|
-
<component :is="headerComponent" class="gl-accordion-item-header">
|
|
100
|
+
<component :is="headerComponent" class="gl-accordion-item-header" :class="headerClass">
|
|
93
101
|
<gl-button
|
|
94
102
|
v-gl-collapse-toggle="accordionItemId"
|
|
95
103
|
variant="link"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const
|
|
1
|
+
export const stringTokenList = [
|
|
2
2
|
'giraffe',
|
|
3
3
|
'dog',
|
|
4
4
|
'dodo',
|
|
@@ -14,3 +14,18 @@ export const tokenList = [
|
|
|
14
14
|
];
|
|
15
15
|
|
|
16
16
|
export const labelText = 'Animals We Tolerate';
|
|
17
|
+
|
|
18
|
+
export const objectTokenList = [
|
|
19
|
+
{ id: '1', title: 'giraffe' },
|
|
20
|
+
{ id: '2', title: 'dog' },
|
|
21
|
+
{ id: '3', title: 'dodo' },
|
|
22
|
+
{ id: '4', title: 'komodo dragon' },
|
|
23
|
+
{ id: '5', title: 'hippo' },
|
|
24
|
+
{ id: '6', title: 'platypus' },
|
|
25
|
+
{ id: '7', title: 'jackalope' },
|
|
26
|
+
{ id: '8', title: 'quetzal' },
|
|
27
|
+
{ id: '9', title: 'badger' },
|
|
28
|
+
{ id: '10', title: 'vicuña' },
|
|
29
|
+
{ id: '11', title: 'whale' },
|
|
30
|
+
{ id: '12', title: 'xenarthra' },
|
|
31
|
+
];
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { mount } from '@vue/test-utils';
|
|
2
2
|
import GlDropdownItem from '../../dropdown/dropdown_item.vue';
|
|
3
3
|
import GlFormInput from '../form_input/form_input.vue';
|
|
4
|
-
import {
|
|
4
|
+
import { stringTokenList, labelText, objectTokenList } from './constants';
|
|
5
5
|
import GlFormCombobox from './form_combobox.vue';
|
|
6
6
|
|
|
7
7
|
const partialToken = 'do';
|
|
8
|
-
const
|
|
8
|
+
const partialStringTokenMatch = ['dog', 'dodo', 'komodo dragon'];
|
|
9
|
+
const partialObjectTokenMatch = [
|
|
10
|
+
{ id: '2', title: 'dog' },
|
|
11
|
+
{ id: '3', title: 'dodo' },
|
|
12
|
+
{ id: '4', title: 'komodo dragon' },
|
|
13
|
+
];
|
|
9
14
|
const unlistedToken = 'elephant';
|
|
10
15
|
|
|
11
16
|
const doTimes = (num, fn) => {
|
|
@@ -17,13 +22,14 @@ const doTimes = (num, fn) => {
|
|
|
17
22
|
describe('GlFormCombobox', () => {
|
|
18
23
|
let wrapper;
|
|
19
24
|
|
|
20
|
-
const createComponent = () => {
|
|
25
|
+
const createComponent = ({ tokens = stringTokenList, matchValueToAttr = undefined } = {}) => {
|
|
21
26
|
wrapper = mount({
|
|
22
27
|
data() {
|
|
23
28
|
return {
|
|
24
29
|
inputVal: '',
|
|
25
|
-
tokens
|
|
30
|
+
tokens,
|
|
26
31
|
labelText,
|
|
32
|
+
matchValueToAttr,
|
|
27
33
|
};
|
|
28
34
|
},
|
|
29
35
|
components: { GlFormCombobox },
|
|
@@ -32,7 +38,8 @@ describe('GlFormCombobox', () => {
|
|
|
32
38
|
<gl-form-combobox
|
|
33
39
|
v-model="inputVal"
|
|
34
40
|
:token-list="tokens"
|
|
35
|
-
:
|
|
41
|
+
:label-text="labelText"
|
|
42
|
+
:match-value-to-attr="matchValueToAttr"
|
|
36
43
|
/>
|
|
37
44
|
</div>
|
|
38
45
|
`,
|
|
@@ -48,123 +55,163 @@ describe('GlFormCombobox', () => {
|
|
|
48
55
|
const setInput = (val) => findInput().setValue(val);
|
|
49
56
|
const arrowDown = () => findInput().trigger('keydown.down');
|
|
50
57
|
|
|
51
|
-
describe
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('is open when the input text matches a token', async () => {
|
|
63
|
-
await setInput(partialToken);
|
|
64
|
-
expect(findDropdown().isVisible()).toBe(true);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('shows partial matches at string start and mid-string', async () => {
|
|
68
|
-
await setInput(partialToken);
|
|
69
|
-
expect(findDropdown().isVisible()).toBe(true);
|
|
70
|
-
expect(findDropdownOptions()).toEqual(partialTokenMatch);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('is closed when the text does not match', async () => {
|
|
74
|
-
await setInput(unlistedToken);
|
|
75
|
-
expect(findDropdown().isVisible()).toBe(false);
|
|
76
|
-
});
|
|
77
|
-
});
|
|
58
|
+
describe.each`
|
|
59
|
+
valueType | tokens | matchValueToAttr | partialTokenMatch
|
|
60
|
+
${'string'} | ${stringTokenList} | ${undefined} | ${partialStringTokenMatch}
|
|
61
|
+
${'object'} | ${objectTokenList} | ${'title'} | ${partialObjectTokenMatch}
|
|
62
|
+
`('with value as $valueType', ({ valueType, tokens, matchValueToAttr, partialTokenMatch }) => {
|
|
63
|
+
describe('match and filter functionality', () => {
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
createComponent({ tokens, matchValueToAttr });
|
|
66
|
+
});
|
|
78
67
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
68
|
+
it('is closed when the input is empty', () => {
|
|
69
|
+
expect(findInput().isVisible()).toBe(true);
|
|
70
|
+
expect(findInputValue()).toBe('');
|
|
71
|
+
expect(findDropdown().isVisible()).toBe(false);
|
|
72
|
+
});
|
|
83
73
|
|
|
84
|
-
|
|
85
|
-
it('selects the next item in the list and closes the dropdown', async () => {
|
|
74
|
+
it('is open when the input text matches a token', async () => {
|
|
86
75
|
await setInput(partialToken);
|
|
87
|
-
|
|
88
|
-
await findInput().trigger('keydown.enter');
|
|
89
|
-
expect(findInputValue()).toBe(partialTokenMatch[0]);
|
|
76
|
+
expect(findDropdown().isVisible()).toBe(true);
|
|
90
77
|
});
|
|
91
78
|
|
|
92
|
-
it('
|
|
79
|
+
it('shows partial matches at string start and mid-string', async () => {
|
|
93
80
|
await setInput(partialToken);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
81
|
+
expect(findDropdown().isVisible()).toBe(true);
|
|
82
|
+
|
|
83
|
+
if (valueType === 'string') {
|
|
84
|
+
expect(findDropdownOptions()).toEqual(partialTokenMatch);
|
|
85
|
+
} else {
|
|
86
|
+
findDropdownOptions().forEach((option, index) => {
|
|
87
|
+
expect(option).toContain(partialTokenMatch[index][matchValueToAttr]);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('is closed when the text does not match', async () => {
|
|
93
|
+
await setInput(unlistedToken);
|
|
94
|
+
expect(findDropdown().isVisible()).toBe(false);
|
|
97
95
|
});
|
|
98
96
|
});
|
|
99
97
|
|
|
100
|
-
describe('
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
describe('keyboard navigation in dropdown', () => {
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
createComponent({ tokens, matchValueToAttr });
|
|
101
|
+
});
|
|
103
102
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
describe('on down arrow + enter', () => {
|
|
104
|
+
it('selects the next item in the list and closes the dropdown', async () => {
|
|
105
|
+
await setInput(partialToken);
|
|
106
|
+
findInput().trigger('keydown.down');
|
|
107
|
+
await findInput().trigger('keydown.enter');
|
|
108
|
+
|
|
109
|
+
if (valueType === 'string') {
|
|
110
|
+
expect(findInputValue()).toBe(partialTokenMatch[0]);
|
|
111
|
+
} else {
|
|
112
|
+
expect(findInputValue()).toBe(partialTokenMatch[0][matchValueToAttr]);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
108
115
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
116
|
+
it('loops to the top when it reaches the bottom', async () => {
|
|
117
|
+
await setInput(partialToken);
|
|
118
|
+
doTimes(findDropdownOptions().length + 1, arrowDown);
|
|
119
|
+
await findInput().trigger('keydown.enter');
|
|
120
|
+
|
|
121
|
+
if (valueType === 'string') {
|
|
122
|
+
expect(findInputValue()).toBe(partialTokenMatch[0]);
|
|
123
|
+
} else {
|
|
124
|
+
expect(findInputValue()).toBe(partialTokenMatch[0][matchValueToAttr]);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
112
127
|
});
|
|
113
128
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
findInput().trigger('keydown.up');
|
|
118
|
-
await findInput().trigger('keydown.enter');
|
|
119
|
-
expect(findInputValue()).toBe(partialTokenMatch[partialTokenMatch.length - 1]);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
129
|
+
describe('on up arrow + enter', () => {
|
|
130
|
+
it('selects the previous item in the list and closes the dropdown', async () => {
|
|
131
|
+
setInput(partialToken);
|
|
122
132
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
133
|
+
await wrapper.vm.$nextTick();
|
|
134
|
+
doTimes(3, arrowDown);
|
|
135
|
+
findInput().trigger('keydown.up');
|
|
136
|
+
findInput().trigger('keydown.enter');
|
|
137
|
+
|
|
138
|
+
await wrapper.vm.$nextTick();
|
|
139
|
+
|
|
140
|
+
if (valueType === 'string') {
|
|
141
|
+
expect(findInputValue()).toBe(partialTokenMatch[1]);
|
|
142
|
+
} else {
|
|
143
|
+
expect(findInputValue()).toBe(partialTokenMatch[1][matchValueToAttr]);
|
|
144
|
+
}
|
|
145
|
+
expect(findDropdown().isVisible()).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('loops to the bottom when it reaches the top', async () => {
|
|
149
|
+
await setInput(partialToken);
|
|
150
|
+
findInput().trigger('keydown.down');
|
|
151
|
+
findInput().trigger('keydown.up');
|
|
152
|
+
await findInput().trigger('keydown.enter');
|
|
153
|
+
|
|
154
|
+
if (valueType === 'string') {
|
|
155
|
+
expect(findInputValue()).toBe(partialTokenMatch[partialTokenMatch.length - 1]);
|
|
156
|
+
} else {
|
|
157
|
+
expect(findInputValue()).toBe(
|
|
158
|
+
partialTokenMatch[partialTokenMatch.length - 1][matchValueToAttr]
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
129
162
|
});
|
|
130
|
-
});
|
|
131
163
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
164
|
+
describe('on enter with no item highlighted', () => {
|
|
165
|
+
it('does not select any item and closes the dropdown', async () => {
|
|
166
|
+
await setInput(partialToken);
|
|
167
|
+
await findInput().trigger('keydown.enter');
|
|
168
|
+
expect(findInputValue()).toBe(partialToken);
|
|
169
|
+
expect(findDropdown().isVisible()).toBe(false);
|
|
170
|
+
});
|
|
137
171
|
});
|
|
138
|
-
});
|
|
139
172
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
doTimes(2, arrowDown);
|
|
173
|
+
describe('on click', () => {
|
|
174
|
+
it('selects the clicked item regardless of arrow highlight', async () => {
|
|
175
|
+
await setInput(partialToken);
|
|
176
|
+
await wrapper.find('[data-testid="combobox-dropdown"] button').trigger('click');
|
|
145
177
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
178
|
+
if (valueType === 'string') {
|
|
179
|
+
expect(findInputValue()).toBe(partialTokenMatch[0]);
|
|
180
|
+
} else {
|
|
181
|
+
expect(findInputValue()).toBe(partialTokenMatch[0][matchValueToAttr]);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
149
184
|
});
|
|
150
|
-
});
|
|
151
185
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
it('closes dropdown and does not select anything', async () => {
|
|
186
|
+
describe('on tab', () => {
|
|
187
|
+
it('selects entered text, closes dropdown', async () => {
|
|
155
188
|
await setInput(partialToken);
|
|
156
|
-
|
|
189
|
+
findInput().trigger('keydown.tab');
|
|
190
|
+
doTimes(2, arrowDown);
|
|
191
|
+
|
|
192
|
+
await wrapper.vm.$nextTick();
|
|
157
193
|
expect(findInputValue()).toBe(partialToken);
|
|
158
194
|
expect(findDropdown().isVisible()).toBe(false);
|
|
159
195
|
});
|
|
160
196
|
});
|
|
161
197
|
|
|
162
|
-
describe('
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
198
|
+
describe('on esc', () => {
|
|
199
|
+
describe('when dropdown is open', () => {
|
|
200
|
+
it('closes dropdown and does not select anything', async () => {
|
|
201
|
+
await setInput(partialToken);
|
|
202
|
+
await findInput().trigger('keydown.esc');
|
|
203
|
+
expect(findInputValue()).toBe(partialToken);
|
|
204
|
+
expect(findDropdown().isVisible()).toBe(false);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('when dropdown is closed', () => {
|
|
209
|
+
it('clears the input field', async () => {
|
|
210
|
+
await setInput(unlistedToken);
|
|
211
|
+
expect(findDropdown().isVisible()).toBe(false);
|
|
212
|
+
await findInput().trigger('keydown.esc');
|
|
213
|
+
expect(findInputValue()).toBe('');
|
|
214
|
+
});
|
|
168
215
|
});
|
|
169
216
|
});
|
|
170
217
|
});
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stringTokenList, labelText, objectTokenList } from './constants';
|
|
2
2
|
import readme from './form_combobox.md';
|
|
3
3
|
import GlFormCombobox from './form_combobox.vue';
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const template = `
|
|
6
|
+
<gl-form-combobox
|
|
7
|
+
v-model="value"
|
|
8
|
+
:token-list="tokenList"
|
|
9
|
+
:label-text="labelText"
|
|
10
|
+
:match-value-to-attr="matchValueToAttr"
|
|
11
|
+
/>`;
|
|
12
|
+
|
|
13
|
+
const generateProps = ({ tokenList = stringTokenList, matchValueToAttr = undefined } = {}) => ({
|
|
6
14
|
tokenList,
|
|
7
15
|
labelText,
|
|
16
|
+
matchValueToAttr,
|
|
8
17
|
});
|
|
9
18
|
|
|
10
19
|
const Template = (args) => ({
|
|
@@ -15,17 +24,40 @@ const Template = (args) => ({
|
|
|
15
24
|
};
|
|
16
25
|
},
|
|
17
26
|
props: Object.keys(args),
|
|
18
|
-
template
|
|
19
|
-
<gl-form-combobox
|
|
20
|
-
v-model="value"
|
|
21
|
-
:token-list="tokenList"
|
|
22
|
-
:labelText="labelText"
|
|
23
|
-
/>
|
|
24
|
-
`,
|
|
27
|
+
template,
|
|
25
28
|
});
|
|
26
29
|
|
|
27
30
|
export const Default = Template.bind({});
|
|
28
|
-
Default.args =
|
|
31
|
+
Default.args = generateProps();
|
|
32
|
+
|
|
33
|
+
export const WithObjectValue = (args, { argTypes }) => ({
|
|
34
|
+
components: { GlFormCombobox },
|
|
35
|
+
props: Object.keys(argTypes),
|
|
36
|
+
mounted() {
|
|
37
|
+
document.querySelector('.gl-form-input').focus();
|
|
38
|
+
},
|
|
39
|
+
data: () => {
|
|
40
|
+
return {
|
|
41
|
+
value: '',
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
template: `
|
|
45
|
+
<gl-form-combobox
|
|
46
|
+
v-model="value"
|
|
47
|
+
:token-list="tokenList"
|
|
48
|
+
:label-text="labelText"
|
|
49
|
+
:match-value-to-attr="matchValueToAttr"
|
|
50
|
+
>
|
|
51
|
+
<template #result="{ item }">
|
|
52
|
+
<div class="gl-display-flex">
|
|
53
|
+
<div class="gl-text-gray-400 gl-mr-4">{{ item.id }}</div>
|
|
54
|
+
<div>{{ item.title }}</div>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
</gl-form-combobox>
|
|
58
|
+
`,
|
|
59
|
+
});
|
|
60
|
+
WithObjectValue.args = generateProps({ tokenList: objectTokenList, matchValueToAttr: 'title' });
|
|
29
61
|
|
|
30
62
|
export default {
|
|
31
63
|
title: 'base/form/form-combobox',
|
|
@@ -31,9 +31,19 @@ export default {
|
|
|
31
31
|
required: true,
|
|
32
32
|
},
|
|
33
33
|
value: {
|
|
34
|
-
type: String,
|
|
34
|
+
type: [String, Object],
|
|
35
35
|
required: true,
|
|
36
36
|
},
|
|
37
|
+
matchValueToAttr: {
|
|
38
|
+
type: String,
|
|
39
|
+
required: false,
|
|
40
|
+
default: undefined,
|
|
41
|
+
},
|
|
42
|
+
autofocus: {
|
|
43
|
+
type: Boolean,
|
|
44
|
+
required: false,
|
|
45
|
+
default: false,
|
|
46
|
+
},
|
|
37
47
|
},
|
|
38
48
|
data() {
|
|
39
49
|
return {
|
|
@@ -54,6 +64,11 @@ export default {
|
|
|
54
64
|
showSuggestions() {
|
|
55
65
|
return this.results.length > 0;
|
|
56
66
|
},
|
|
67
|
+
displayedValue() {
|
|
68
|
+
return this.matchValueToAttr && this.value[this.matchValueToAttr]
|
|
69
|
+
? this.value[this.matchValueToAttr]
|
|
70
|
+
: this.value;
|
|
71
|
+
},
|
|
57
72
|
},
|
|
58
73
|
mounted() {
|
|
59
74
|
document.addEventListener('click', this.handleClickOutside);
|
|
@@ -112,9 +127,12 @@ export default {
|
|
|
112
127
|
return;
|
|
113
128
|
}
|
|
114
129
|
|
|
115
|
-
const filteredTokens = this.tokenList.filter((token) =>
|
|
116
|
-
|
|
117
|
-
|
|
130
|
+
const filteredTokens = this.tokenList.filter((token) => {
|
|
131
|
+
if (this.matchValueToAttr) {
|
|
132
|
+
return token[this.matchValueToAttr].toLowerCase().includes(value.toLowerCase());
|
|
133
|
+
}
|
|
134
|
+
return token.toLowerCase().includes(value.toLowerCase());
|
|
135
|
+
});
|
|
118
136
|
|
|
119
137
|
if (filteredTokens.length) {
|
|
120
138
|
this.openSuggestions(filteredTokens);
|
|
@@ -147,13 +165,14 @@ export default {
|
|
|
147
165
|
<gl-form-group :label="labelText" :label-for="inputId" :label-sr-only="labelSrOnly">
|
|
148
166
|
<gl-form-input
|
|
149
167
|
:id="inputId"
|
|
150
|
-
:value="
|
|
168
|
+
:value="displayedValue"
|
|
151
169
|
type="text"
|
|
152
170
|
role="searchbox"
|
|
153
171
|
:autocomplete="showAutocomplete"
|
|
154
172
|
aria-autocomplete="list"
|
|
155
173
|
:aria-controls="suggestionsId"
|
|
156
174
|
aria-haspopup="listbox"
|
|
175
|
+
:autofocus="autofocus"
|
|
157
176
|
@input="onEntry"
|
|
158
177
|
@keydown.down="onArrowDown"
|
|
159
178
|
@keydown.up="onArrowUp"
|
|
@@ -163,11 +182,11 @@ export default {
|
|
|
163
182
|
/>
|
|
164
183
|
</gl-form-group>
|
|
165
184
|
|
|
166
|
-
<
|
|
185
|
+
<ul
|
|
167
186
|
v-show="showSuggestions && !userDismissedResults"
|
|
168
187
|
:id="suggestionsId"
|
|
169
188
|
data-testid="combobox-dropdown"
|
|
170
|
-
class="dropdown-menu dropdown-full-width"
|
|
189
|
+
class="dropdown-menu dropdown-full-width gl-list-style-none gl-pl-0 gl-mb-0 gl-overflow-y-auto"
|
|
171
190
|
:class="{ 'show-dropdown': showSuggestions }"
|
|
172
191
|
>
|
|
173
192
|
<gl-dropdown-item
|
|
@@ -179,8 +198,8 @@ export default {
|
|
|
179
198
|
tabindex="-1"
|
|
180
199
|
@click="selectToken(result)"
|
|
181
200
|
>
|
|
182
|
-
{{ result }}
|
|
201
|
+
<slot name="result" :item="result">{{ result }}</slot>
|
|
183
202
|
</gl-dropdown-item>
|
|
184
|
-
</
|
|
203
|
+
</ul>
|
|
185
204
|
</div>
|
|
186
205
|
</template>
|
package/src/scss/utilities.scss
CHANGED
|
@@ -5592,6 +5592,18 @@
|
|
|
5592
5592
|
.gl-mt-7\! {
|
|
5593
5593
|
margin-top: $gl-spacing-scale-7 !important;
|
|
5594
5594
|
}
|
|
5595
|
+
.gl-mt-8 {
|
|
5596
|
+
margin-top: $gl-spacing-scale-8;
|
|
5597
|
+
}
|
|
5598
|
+
.gl-mt-8\! {
|
|
5599
|
+
margin-top: $gl-spacing-scale-8 !important;
|
|
5600
|
+
}
|
|
5601
|
+
.gl-mt-9 {
|
|
5602
|
+
margin-top: $gl-spacing-scale-9;
|
|
5603
|
+
}
|
|
5604
|
+
.gl-mt-9\! {
|
|
5605
|
+
margin-top: $gl-spacing-scale-9 !important;
|
|
5606
|
+
}
|
|
5595
5607
|
.gl-mt-11 {
|
|
5596
5608
|
margin-top: $gl-spacing-scale-11;
|
|
5597
5609
|
}
|
|
@@ -5742,6 +5754,12 @@
|
|
|
5742
5754
|
.gl-mb-8\! {
|
|
5743
5755
|
margin-bottom: $gl-spacing-scale-8 !important;
|
|
5744
5756
|
}
|
|
5757
|
+
.gl-mb-9 {
|
|
5758
|
+
margin-bottom: $gl-spacing-scale-9;
|
|
5759
|
+
}
|
|
5760
|
+
.gl-mb-9\! {
|
|
5761
|
+
margin-bottom: $gl-spacing-scale-9 !important;
|
|
5762
|
+
}
|
|
5745
5763
|
.gl-ml-auto {
|
|
5746
5764
|
margin-left: auto;
|
|
5747
5765
|
}
|
|
@@ -418,6 +418,14 @@
|
|
|
418
418
|
margin-top: $gl-spacing-scale-7;
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
+
@mixin gl-mt-8 {
|
|
422
|
+
margin-top: $gl-spacing-scale-8;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
@mixin gl-mt-9 {
|
|
426
|
+
margin-top: $gl-spacing-scale-9;
|
|
427
|
+
}
|
|
428
|
+
|
|
421
429
|
@mixin gl-mt-11 {
|
|
422
430
|
margin-top: $gl-spacing-scale-11;
|
|
423
431
|
}
|
|
@@ -518,6 +526,10 @@
|
|
|
518
526
|
margin-bottom: $gl-spacing-scale-8;
|
|
519
527
|
}
|
|
520
528
|
|
|
529
|
+
@mixin gl-mb-9 {
|
|
530
|
+
margin-bottom: $gl-spacing-scale-9;
|
|
531
|
+
}
|
|
532
|
+
|
|
521
533
|
@mixin gl-ml-auto {
|
|
522
534
|
margin-left: auto;
|
|
523
535
|
}
|