@gitlab/ui 42.4.1 → 42.7.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/drawer/drawer.js +5 -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 +1 -1
- package/src/components/base/drawer/drawer.scss +20 -0
- package/src/components/base/drawer/drawer.spec.js +1 -0
- package/src/components/base/drawer/drawer.stories.js +55 -13
- package/src/components/base/drawer/drawer.vue +10 -0
- 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
|
@@ -10,6 +10,8 @@ $gl-sidebar-width: 290px;
|
|
|
10
10
|
@include gl-shadow-drawer;
|
|
11
11
|
@include gl-font-base;
|
|
12
12
|
@include gl-line-height-normal;
|
|
13
|
+
@include gl-display-flex;
|
|
14
|
+
@include gl-flex-direction-column;
|
|
13
15
|
|
|
14
16
|
.gl-drawer-header-sticky {
|
|
15
17
|
@include gl-bg-white;
|
|
@@ -51,6 +53,20 @@ $gl-sidebar-width: 290px;
|
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
.gl-drawer-footer {
|
|
57
|
+
@include gl-border-t-solid;
|
|
58
|
+
@include gl-border-t-gray-100;
|
|
59
|
+
@include gl-border-t-1;
|
|
60
|
+
@include gl-px-6;
|
|
61
|
+
@include gl-py-5;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.gl-drawer-footer-sticky {
|
|
65
|
+
@include gl-bg-white;
|
|
66
|
+
@include gl-bottom-0;
|
|
67
|
+
@include gl-sticky;
|
|
68
|
+
}
|
|
69
|
+
|
|
54
70
|
.gl-drawer-header {
|
|
55
71
|
@include gl-border-b-solid;
|
|
56
72
|
@include gl-border-b-gray-100;
|
|
@@ -74,6 +90,10 @@ $gl-sidebar-width: 290px;
|
|
|
74
90
|
@include gl-border-none;
|
|
75
91
|
}
|
|
76
92
|
|
|
93
|
+
.gl-drawer-body {
|
|
94
|
+
@include gl-flex-grow-1;
|
|
95
|
+
}
|
|
96
|
+
|
|
77
97
|
.gl-drawer-body > * {
|
|
78
98
|
@include gl-border-b-solid;
|
|
79
99
|
@include gl-border-b-gray-100;
|
|
@@ -90,6 +90,7 @@ describe('drawer component', () => {
|
|
|
90
90
|
${'title'} | ${'.gl-drawer-title'}
|
|
91
91
|
${'header'} | ${'.gl-drawer-header'}
|
|
92
92
|
${'default'} | ${'.gl-drawer-body'}
|
|
93
|
+
${'footer'} | ${'.gl-drawer-footer'}
|
|
93
94
|
`('renders nodes when added to the $slot slot', ({ slot, parentSelector }) => {
|
|
94
95
|
mountWithOpts({
|
|
95
96
|
slots: {
|
|
@@ -4,7 +4,19 @@ import readme from './drawer.md';
|
|
|
4
4
|
|
|
5
5
|
const components = { GlDrawer, GlButton };
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const generateDrawerContent = (items) =>
|
|
8
|
+
items
|
|
9
|
+
.map(
|
|
10
|
+
(str) => `
|
|
11
|
+
<div>
|
|
12
|
+
<label class="gl-font-weight-bold">${str}</label>
|
|
13
|
+
<div>None</div>
|
|
14
|
+
</div>
|
|
15
|
+
`
|
|
16
|
+
)
|
|
17
|
+
.join('');
|
|
18
|
+
|
|
19
|
+
const drawerContent = generateDrawerContent([
|
|
8
20
|
'One',
|
|
9
21
|
'Two',
|
|
10
22
|
'Three',
|
|
@@ -19,16 +31,9 @@ const drawerContent = [
|
|
|
19
31
|
'Twelve',
|
|
20
32
|
'Thirteen',
|
|
21
33
|
'Fourteen',
|
|
22
|
-
]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<div>
|
|
26
|
-
<label class="gl-font-weight-bold">${str}</label>
|
|
27
|
-
<div>None</div>
|
|
28
|
-
</div>
|
|
29
|
-
`
|
|
30
|
-
)
|
|
31
|
-
.join('');
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const drawerContentShortList = generateDrawerContent(['One', 'Two', 'Three']);
|
|
32
37
|
|
|
33
38
|
const createSidebarTemplate = (content) => `
|
|
34
39
|
<gl-drawer
|
|
@@ -106,6 +111,40 @@ export const WithActions = (_args, { viewMode }) => ({
|
|
|
106
111
|
});
|
|
107
112
|
WithActions.args = generateProps();
|
|
108
113
|
|
|
114
|
+
export const WithStickyFooterShortContent = (_args, { viewMode }) => ({
|
|
115
|
+
...storyOptions(viewMode),
|
|
116
|
+
template: `
|
|
117
|
+
<div>
|
|
118
|
+
<gl-button @click="toggle">Toggle Drawer</gl-button>
|
|
119
|
+
${createSidebarTemplate(`
|
|
120
|
+
<template #title>List Settings</template>
|
|
121
|
+
${drawerContentShortList}
|
|
122
|
+
<template #footer>
|
|
123
|
+
Drawer footer
|
|
124
|
+
</template>
|
|
125
|
+
`)}
|
|
126
|
+
</div>`,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
WithStickyFooterShortContent.args = generateProps();
|
|
130
|
+
|
|
131
|
+
export const WithStickyFooter = (_args, { viewMode }) => ({
|
|
132
|
+
...storyOptions(viewMode),
|
|
133
|
+
template: `
|
|
134
|
+
<div>
|
|
135
|
+
<gl-button @click="toggle">Toggle Drawer</gl-button>
|
|
136
|
+
${createSidebarTemplate(`
|
|
137
|
+
<template #title>List Settings</template>
|
|
138
|
+
${drawerContent}
|
|
139
|
+
<template #footer>
|
|
140
|
+
Drawer footer
|
|
141
|
+
</template>
|
|
142
|
+
`)}
|
|
143
|
+
</div>`,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
WithStickyFooter.args = generateProps();
|
|
147
|
+
|
|
109
148
|
export const SidebarVariant = (_args, { viewMode }) => ({
|
|
110
149
|
...storyOptions(viewMode),
|
|
111
150
|
template: `
|
|
@@ -128,7 +167,7 @@ SidebarVariant.args = generateProps({
|
|
|
128
167
|
variant: drawerVariants.sidebar,
|
|
129
168
|
});
|
|
130
169
|
|
|
131
|
-
export const
|
|
170
|
+
export const StickyHeaderFooter = (_args, { viewMode }) => ({
|
|
132
171
|
...storyOptions(viewMode),
|
|
133
172
|
template: `
|
|
134
173
|
<div>
|
|
@@ -136,10 +175,13 @@ export const StickyHeader = (_args, { viewMode }) => ({
|
|
|
136
175
|
${createSidebarTemplate(`
|
|
137
176
|
<template #title>List Settings</template>
|
|
138
177
|
${drawerContent}
|
|
178
|
+
<template #footer>
|
|
179
|
+
Drawer footer
|
|
180
|
+
</template>
|
|
139
181
|
`)}
|
|
140
182
|
</div>`,
|
|
141
183
|
});
|
|
142
|
-
|
|
184
|
+
StickyHeaderFooter.args = generateProps({
|
|
143
185
|
headerSticky: true,
|
|
144
186
|
});
|
|
145
187
|
|
|
@@ -56,6 +56,9 @@ export default {
|
|
|
56
56
|
zIndex: this.headerSticky ? maxZIndex : null,
|
|
57
57
|
};
|
|
58
58
|
},
|
|
59
|
+
shouldRenderFooter() {
|
|
60
|
+
return Boolean(this.$slots.footer);
|
|
61
|
+
},
|
|
59
62
|
variantClass() {
|
|
60
63
|
return `gl-drawer-${this.variant}`;
|
|
61
64
|
},
|
|
@@ -110,6 +113,13 @@ export default {
|
|
|
110
113
|
<div class="gl-drawer-body">
|
|
111
114
|
<slot></slot>
|
|
112
115
|
</div>
|
|
116
|
+
<div
|
|
117
|
+
v-if="shouldRenderFooter"
|
|
118
|
+
class="gl-drawer-footer gl-drawer-footer-sticky"
|
|
119
|
+
:style="{ zIndex }"
|
|
120
|
+
>
|
|
121
|
+
<slot name="footer"></slot>
|
|
122
|
+
</div>
|
|
113
123
|
</aside>
|
|
114
124
|
</transition>
|
|
115
125
|
</template>
|
|
@@ -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',
|