@gitlab/ui 61.1.3 → 61.3.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 +14 -0
- package/dist/components/base/filtered_search/common_story_options.js +2 -1
- package/dist/components/base/filtered_search/filtered_search.js +28 -4
- package/dist/components/base/filtered_search/filtered_search_suggestion_list.js +13 -5
- package/dist/components/base/filtered_search/filtered_search_term.js +51 -4
- package/dist/components/base/filtered_search/filtered_search_token.js +1 -2
- package/dist/components/base/filtered_search/filtered_search_token_segment.js +18 -5
- package/dist/components/base/filtered_search/filtered_search_utils.js +18 -7
- package/dist/utility_classes.css +1 -1
- package/dist/utility_classes.css.map +1 -1
- package/package.json +1 -1
- package/src/components/base/filtered_search/__snapshots__/filtered_search_term.spec.js.snap +2 -5
- package/src/components/base/filtered_search/common_story_options.js +1 -0
- package/src/components/base/filtered_search/filtered_search.md +10 -1
- package/src/components/base/filtered_search/filtered_search.spec.js +14 -2
- package/src/components/base/filtered_search/filtered_search.stories.js +17 -0
- package/src/components/base/filtered_search/filtered_search.vue +29 -1
- package/src/components/base/filtered_search/filtered_search_suggestion_list.spec.js +222 -75
- package/src/components/base/filtered_search/filtered_search_suggestion_list.vue +15 -6
- package/src/components/base/filtered_search/filtered_search_term.spec.js +73 -14
- package/src/components/base/filtered_search/filtered_search_term.vue +64 -6
- package/src/components/base/filtered_search/filtered_search_token.spec.js +1 -0
- package/src/components/base/filtered_search/filtered_search_token.vue +2 -2
- package/src/components/base/filtered_search/filtered_search_token_segment.spec.js +46 -2
- package/src/components/base/filtered_search/filtered_search_token_segment.vue +22 -5
- package/src/components/base/filtered_search/filtered_search_utils.js +20 -7
- package/src/scss/utilities.scss +32 -16
- package/src/scss/utility-mixins/border.scss +16 -8
package/package.json
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
2
|
|
|
3
|
-
exports[`Filtered search term renders
|
|
3
|
+
exports[`Filtered search term renders nothing with value in active mode (delegates to segment) 1`] = `
|
|
4
4
|
<div
|
|
5
5
|
class="gl-h-auto gl-filtered-search-term"
|
|
6
6
|
data-testid="filtered-search-term"
|
|
7
7
|
>
|
|
8
8
|
<div
|
|
9
|
-
active="true"
|
|
10
9
|
class="gl-filtered-search-term-token"
|
|
11
10
|
cursor-position="end"
|
|
12
11
|
is-term=""
|
|
13
12
|
value="test-value"
|
|
14
|
-
|
|
15
|
-
test-value
|
|
16
|
-
</div>
|
|
13
|
+
/>
|
|
17
14
|
</div>
|
|
18
15
|
`;
|
|
19
16
|
|
|
@@ -48,6 +48,15 @@ The token should emit the following events:
|
|
|
48
48
|
- `split`: token requests adding string values after the current token.
|
|
49
49
|
- `complete`: token indicates its editing is completed.
|
|
50
50
|
|
|
51
|
+
### Improve space handling
|
|
52
|
+
|
|
53
|
+
Set the `terms-as-tokens` prop to `true` to enable new term rendering and
|
|
54
|
+
interaction behavior. This makes it easier to input/edit free text tokens, and
|
|
55
|
+
removes the need for quoting values with spaces and other workarounds.
|
|
56
|
+
|
|
57
|
+
In future, this prop will be enabled by default and eventually removed. Opt in
|
|
58
|
+
to this earlier rather than later to ease migration.
|
|
59
|
+
|
|
51
60
|
## Examples
|
|
52
61
|
|
|
53
62
|
Define a list of available tokens:
|
|
@@ -65,5 +74,5 @@ realtime updates:
|
|
|
65
74
|
<!-- Empty initial line is a workaround for https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2102 -->
|
|
66
75
|
```html
|
|
67
76
|
|
|
68
|
-
<gl-filtered-search :available-tokens="tokens" v-model="value" />
|
|
77
|
+
<gl-filtered-search :available-tokens="tokens" v-model="value" terms-as-tokens />
|
|
69
78
|
```
|
|
@@ -95,7 +95,7 @@ describe('Filtered search', () => {
|
|
|
95
95
|
});
|
|
96
96
|
await nextTick();
|
|
97
97
|
|
|
98
|
-
const inputEventArgs = wrapper.emitted().input
|
|
98
|
+
const [inputEventArgs] = wrapper.emitted().input.at(-1);
|
|
99
99
|
expect(inputEventArgs.every((t) => t.type === TERM_TOKEN_TYPE)).toBe(true);
|
|
100
100
|
expect(inputEventArgs.map((t) => t.value.data)).toStrictEqual(['one', 'two', '']);
|
|
101
101
|
});
|
|
@@ -106,10 +106,22 @@ describe('Filtered search', () => {
|
|
|
106
106
|
});
|
|
107
107
|
await nextTick();
|
|
108
108
|
|
|
109
|
-
const inputEventArgs = wrapper.emitted().input
|
|
109
|
+
const [inputEventArgs] = wrapper.emitted().input.at(-1);
|
|
110
110
|
expect(inputEventArgs.every((t) => t.type === TERM_TOKEN_TYPE)).toBe(true);
|
|
111
111
|
expect(inputEventArgs.map((t) => t.value.data)).toStrictEqual(['one', 'two', '']);
|
|
112
112
|
});
|
|
113
|
+
|
|
114
|
+
it('does not split strings with termsAsTokens', async () => {
|
|
115
|
+
createComponent({
|
|
116
|
+
termsAsTokens: true,
|
|
117
|
+
value: ['one two'],
|
|
118
|
+
});
|
|
119
|
+
await nextTick();
|
|
120
|
+
|
|
121
|
+
const [inputEventArgs] = wrapper.emitted().input.at(-1);
|
|
122
|
+
expect(inputEventArgs.every((t) => t.type === TERM_TOKEN_TYPE)).toBe(true);
|
|
123
|
+
expect(inputEventArgs.map((t) => t.value.data)).toStrictEqual(['one two', '']);
|
|
124
|
+
});
|
|
113
125
|
});
|
|
114
126
|
|
|
115
127
|
describe('event handling', () => {
|
|
@@ -329,6 +329,23 @@ export const Default = () => ({
|
|
|
329
329
|
template: `<gl-filtered-search :available-tokens="tokens" :value="value" />`,
|
|
330
330
|
});
|
|
331
331
|
|
|
332
|
+
export const WithTermsAsTokens = () => ({
|
|
333
|
+
data() {
|
|
334
|
+
return {
|
|
335
|
+
tokens,
|
|
336
|
+
value: [
|
|
337
|
+
{ type: 'author', value: { data: 'beta', operator: '=' } },
|
|
338
|
+
{ type: 'label', value: { data: 'Bug', operator: '=' } },
|
|
339
|
+
'raw text',
|
|
340
|
+
],
|
|
341
|
+
};
|
|
342
|
+
},
|
|
343
|
+
components,
|
|
344
|
+
template: `
|
|
345
|
+
<gl-filtered-search :available-tokens="tokens" v-model="value" terms-as-tokens />
|
|
346
|
+
`,
|
|
347
|
+
});
|
|
348
|
+
|
|
332
349
|
export const ViewOnly = () => ({
|
|
333
350
|
data() {
|
|
334
351
|
return {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
normalizeTokens,
|
|
16
16
|
denormalizeTokens,
|
|
17
17
|
needDenormalization,
|
|
18
|
+
termTokenDefinition,
|
|
18
19
|
} from './filtered_search_utils';
|
|
19
20
|
|
|
20
21
|
Vue.use(PortalVue);
|
|
@@ -43,6 +44,7 @@ export default {
|
|
|
43
44
|
// TODO: This can be reverted once https://github.com/vuejs/vue-apollo/pull/1153
|
|
44
45
|
// has been merged and we consume it, or we upgrade to vue-apollo@4.
|
|
45
46
|
suggestionsListClass: () => this.suggestionsListClass,
|
|
47
|
+
termsAsTokens: () => this.termsAsTokens,
|
|
46
48
|
};
|
|
47
49
|
},
|
|
48
50
|
inheritAttrs: false,
|
|
@@ -137,6 +139,28 @@ export default {
|
|
|
137
139
|
required: false,
|
|
138
140
|
default: false,
|
|
139
141
|
},
|
|
142
|
+
/**
|
|
143
|
+
* Render search terms as GlTokens. Ideally, this prop will be as
|
|
144
|
+
* short-lived as possible, and this behavior will become the default and
|
|
145
|
+
* only behavior.
|
|
146
|
+
*
|
|
147
|
+
* This prop is *not* reactive.
|
|
148
|
+
*
|
|
149
|
+
* See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2159.
|
|
150
|
+
*/
|
|
151
|
+
termsAsTokens: {
|
|
152
|
+
type: Boolean,
|
|
153
|
+
required: false,
|
|
154
|
+
default: false,
|
|
155
|
+
},
|
|
156
|
+
/**
|
|
157
|
+
* The title of the text search option. Ignored unless termsAsTokens is enabled.
|
|
158
|
+
*/
|
|
159
|
+
searchTextOptionLabel: {
|
|
160
|
+
type: String,
|
|
161
|
+
required: false,
|
|
162
|
+
default: termTokenDefinition.title,
|
|
163
|
+
},
|
|
140
164
|
},
|
|
141
165
|
data() {
|
|
142
166
|
return {
|
|
@@ -212,7 +236,9 @@ export default {
|
|
|
212
236
|
|
|
213
237
|
methods: {
|
|
214
238
|
applyNewValue(newValue) {
|
|
215
|
-
this.tokens = needDenormalization(newValue)
|
|
239
|
+
this.tokens = needDenormalization(newValue)
|
|
240
|
+
? denormalizeTokens(newValue, this.termsAsTokens)
|
|
241
|
+
: newValue;
|
|
216
242
|
},
|
|
217
243
|
|
|
218
244
|
isActiveToken(idx) {
|
|
@@ -317,6 +343,7 @@ export default {
|
|
|
317
343
|
this.activeTokenIdx = idx;
|
|
318
344
|
},
|
|
319
345
|
|
|
346
|
+
// This method can be deleted once termsAsTokens behavior is the default.
|
|
320
347
|
createTokens(idx, newStrings = ['']) {
|
|
321
348
|
if (!this.isLastTokenActive && newStrings.length === 1 && newStrings[0] === '') {
|
|
322
349
|
this.activeTokenIdx = this.lastTokenIdx;
|
|
@@ -391,6 +418,7 @@ export default {
|
|
|
391
418
|
:view-only="viewOnly"
|
|
392
419
|
:is-last-token="isLastToken(idx)"
|
|
393
420
|
:class="getTokenClassList(idx)"
|
|
421
|
+
:search-text-option-label="searchTextOptionLabel"
|
|
394
422
|
@activate="activate(idx)"
|
|
395
423
|
@deactivate="deactivate(token)"
|
|
396
424
|
@destroy="destroyToken(idx, $event)"
|
|
@@ -7,13 +7,14 @@ describe('Filtered search suggestion list component', () => {
|
|
|
7
7
|
let wrapper;
|
|
8
8
|
|
|
9
9
|
describe('suggestions API', () => {
|
|
10
|
-
|
|
10
|
+
const createComponent = ({ termsAsTokens = false } = {}) => {
|
|
11
11
|
wrapper = shallowMount(FilteredSearchSuggestionList, {
|
|
12
|
-
provide: { suggestionsListClass: () => 'custom-class' },
|
|
12
|
+
provide: { suggestionsListClass: () => 'custom-class', termsAsTokens: () => termsAsTokens },
|
|
13
13
|
});
|
|
14
|
-
}
|
|
14
|
+
};
|
|
15
15
|
|
|
16
16
|
it('has expected public methods', () => {
|
|
17
|
+
createComponent();
|
|
17
18
|
expect(wrapper.vm.register).toBeInstanceOf(Function);
|
|
18
19
|
expect(wrapper.vm.unregister).toBeInstanceOf(Function);
|
|
19
20
|
expect(wrapper.vm.getValue).toBeInstanceOf(Function);
|
|
@@ -24,6 +25,7 @@ describe('Filtered search suggestion list component', () => {
|
|
|
24
25
|
describe('navigation', () => {
|
|
25
26
|
const stubs = [{ value: 'stub1' }, { value: 'stub2' }, { value: 'stub3' }];
|
|
26
27
|
beforeEach(() => {
|
|
28
|
+
createComponent();
|
|
27
29
|
stubs.forEach((s) => {
|
|
28
30
|
wrapper.vm.register(s);
|
|
29
31
|
});
|
|
@@ -71,6 +73,58 @@ describe('Filtered search suggestion list component', () => {
|
|
|
71
73
|
expect(wrapper.vm.getValue()).toBe('stub2');
|
|
72
74
|
});
|
|
73
75
|
});
|
|
76
|
+
|
|
77
|
+
describe('navigation with termsAsTokens', () => {
|
|
78
|
+
const stubs = [{ value: 'stub1' }, { value: 'stub2' }, { value: 'stub3' }];
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
createComponent({ termsAsTokens: true });
|
|
81
|
+
stubs.forEach((s) => {
|
|
82
|
+
wrapper.vm.register(s);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('does not select item by default', () => {
|
|
87
|
+
expect(wrapper.vm.getValue()).toBe(null);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('selects first item on nextItem call', async () => {
|
|
91
|
+
wrapper.vm.nextItem();
|
|
92
|
+
await nextTick();
|
|
93
|
+
expect(wrapper.vm.getValue()).toBe(stubs[0].value);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('wraps to last item on prevItem call', async () => {
|
|
97
|
+
wrapper.vm.nextItem();
|
|
98
|
+
wrapper.vm.prevItem();
|
|
99
|
+
await nextTick();
|
|
100
|
+
expect(wrapper.vm.getValue()).toBe(stubs[2].value);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('wraps to first item on nextItem call', async () => {
|
|
104
|
+
stubs.forEach(() => wrapper.vm.nextItem());
|
|
105
|
+
wrapper.vm.nextItem();
|
|
106
|
+
await nextTick();
|
|
107
|
+
expect(wrapper.vm.getValue()).toBe(stubs[0].value);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('remove selection if suggestion is unregistered', async () => {
|
|
111
|
+
wrapper.vm.nextItem();
|
|
112
|
+
await nextTick();
|
|
113
|
+
wrapper.vm.unregister(stubs[0]);
|
|
114
|
+
await nextTick();
|
|
115
|
+
expect(wrapper.vm.getValue()).toBe(null);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('selects correct suggestion when item (un)registration is late', async () => {
|
|
119
|
+
// Initially stub2 is at index 1.
|
|
120
|
+
await wrapper.setProps({ initialValue: 'stub2' });
|
|
121
|
+
// Remove item at index 0, so stub2 moves to index 0
|
|
122
|
+
wrapper.vm.unregister(stubs[0]);
|
|
123
|
+
await nextTick();
|
|
124
|
+
// stub2 should still be selected
|
|
125
|
+
expect(wrapper.vm.getValue()).toBe('stub2');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
74
128
|
});
|
|
75
129
|
|
|
76
130
|
describe('integration tests', () => {
|
|
@@ -87,6 +141,17 @@ describe('Filtered search suggestion list component', () => {
|
|
|
87
141
|
`,
|
|
88
142
|
};
|
|
89
143
|
|
|
144
|
+
const createComponent = ({ termsAsTokens = false } = {}) => {
|
|
145
|
+
wrapper = mount(FilteredSearchSuggestionList, {
|
|
146
|
+
provide: { suggestionsListClass: () => 'custom-class', termsAsTokens: () => termsAsTokens },
|
|
147
|
+
slots: {
|
|
148
|
+
default: list,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const findActiveSuggestion = () => wrapper.find('.gl-filtered-search-suggestion-active');
|
|
154
|
+
|
|
90
155
|
beforeAll(() => {
|
|
91
156
|
if (!HTMLElement.prototype.scrollIntoView) {
|
|
92
157
|
HTMLElement.prototype.scrollIntoView = jest.fn();
|
|
@@ -99,92 +164,174 @@ describe('Filtered search suggestion list component', () => {
|
|
|
99
164
|
}
|
|
100
165
|
});
|
|
101
166
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
slots: {
|
|
106
|
-
default: list,
|
|
107
|
-
},
|
|
167
|
+
describe('with termsAsTokens = false', () => {
|
|
168
|
+
beforeEach(() => {
|
|
169
|
+
createComponent();
|
|
108
170
|
});
|
|
109
|
-
});
|
|
110
171
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
172
|
+
it('selects first suggestion', async () => {
|
|
173
|
+
wrapper.vm.nextItem();
|
|
174
|
+
await nextTick();
|
|
175
|
+
expect(wrapper.vm.getValue()).toBe('One');
|
|
176
|
+
});
|
|
116
177
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
178
|
+
it('selects second suggestion', async () => {
|
|
179
|
+
wrapper.vm.nextItem();
|
|
180
|
+
wrapper.vm.nextItem();
|
|
181
|
+
await nextTick();
|
|
182
|
+
expect(wrapper.vm.getValue()).toBe('Two');
|
|
183
|
+
});
|
|
123
184
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
185
|
+
it('deselects first suggestion after list end', async () => {
|
|
186
|
+
wrapper.vm.nextItem();
|
|
187
|
+
wrapper.vm.nextItem();
|
|
188
|
+
wrapper.vm.nextItem();
|
|
189
|
+
wrapper.vm.nextItem();
|
|
190
|
+
await nextTick();
|
|
191
|
+
expect(wrapper.vm.getValue()).toBe(null);
|
|
192
|
+
});
|
|
132
193
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
194
|
+
it('deselects first suggestion after list start', async () => {
|
|
195
|
+
wrapper.vm.nextItem();
|
|
196
|
+
wrapper.vm.prevItem();
|
|
197
|
+
await nextTick();
|
|
198
|
+
expect(wrapper.vm.getValue()).toBe(null);
|
|
199
|
+
});
|
|
139
200
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
201
|
+
it('selects last suggestion in circle when selecting previous item', async () => {
|
|
202
|
+
wrapper.vm.nextItem();
|
|
203
|
+
wrapper.vm.prevItem();
|
|
204
|
+
wrapper.vm.prevItem();
|
|
205
|
+
await nextTick();
|
|
206
|
+
expect(wrapper.vm.getValue()).toBe(false);
|
|
207
|
+
});
|
|
147
208
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
209
|
+
it('selects first suggestion in circle when selecting next item', async () => {
|
|
210
|
+
wrapper.vm.nextItem();
|
|
211
|
+
wrapper.vm.nextItem();
|
|
212
|
+
wrapper.vm.nextItem();
|
|
213
|
+
wrapper.vm.nextItem();
|
|
214
|
+
wrapper.vm.nextItem();
|
|
215
|
+
await nextTick();
|
|
216
|
+
expect(wrapper.vm.getValue()).toBe('One');
|
|
217
|
+
});
|
|
157
218
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
219
|
+
it('highlights suggestion if initial-value is provided', async () => {
|
|
220
|
+
await wrapper.setProps({ initialValue: 'Two' });
|
|
221
|
+
expect(findActiveSuggestion().text()).toBe('Two');
|
|
222
|
+
});
|
|
162
223
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
224
|
+
it('highlights suggestion if initial-value is provided, regardless of case sensitivity', async () => {
|
|
225
|
+
await wrapper.setProps({ initialValue: 'two' });
|
|
226
|
+
expect(findActiveSuggestion().text()).toBe('Two');
|
|
227
|
+
});
|
|
167
228
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
229
|
+
it('highlights suggestion if initial-value is provided, regardless of falsiness', async () => {
|
|
230
|
+
await wrapper.setProps({ initialValue: false });
|
|
231
|
+
expect(findActiveSuggestion().text()).toBe('Three');
|
|
232
|
+
});
|
|
172
233
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
234
|
+
it('highlights first suggestion if initial-value is provided, deselected then selected', async () => {
|
|
235
|
+
await wrapper.setProps({ initialValue: 'One' });
|
|
236
|
+
wrapper.vm.prevItem();
|
|
237
|
+
wrapper.vm.nextItem();
|
|
238
|
+
await nextTick();
|
|
239
|
+
expect(findActiveSuggestion().text()).toBe('One');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('does not highlight anything if initial-value matches nothing', async () => {
|
|
243
|
+
await wrapper.setProps({ initialValue: 'missing' });
|
|
244
|
+
expect(findActiveSuggestion().exists()).toBe(false);
|
|
245
|
+
});
|
|
180
246
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
247
|
+
it('applies the injected suggestion-list-class to the dropdown', () => {
|
|
248
|
+
expect(wrapper.classes()).toContain('custom-class');
|
|
249
|
+
});
|
|
184
250
|
});
|
|
185
251
|
|
|
186
|
-
|
|
187
|
-
|
|
252
|
+
describe('with termsAsTokens = true', () => {
|
|
253
|
+
beforeEach(() => {
|
|
254
|
+
createComponent({ termsAsTokens: true });
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('selects first suggestion', async () => {
|
|
258
|
+
wrapper.vm.nextItem();
|
|
259
|
+
await nextTick();
|
|
260
|
+
expect(wrapper.vm.getValue()).toBe('One');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('selects second suggestion', async () => {
|
|
264
|
+
wrapper.vm.nextItem();
|
|
265
|
+
wrapper.vm.nextItem();
|
|
266
|
+
await nextTick();
|
|
267
|
+
expect(wrapper.vm.getValue()).toBe('Two');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('wraps to first suggestion after list end', async () => {
|
|
271
|
+
wrapper.vm.nextItem();
|
|
272
|
+
wrapper.vm.nextItem();
|
|
273
|
+
wrapper.vm.nextItem();
|
|
274
|
+
wrapper.vm.nextItem();
|
|
275
|
+
await nextTick();
|
|
276
|
+
expect(wrapper.vm.getValue()).toBe('One');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('wraps to last suggestion before list start', async () => {
|
|
280
|
+
wrapper.vm.nextItem();
|
|
281
|
+
wrapper.vm.prevItem();
|
|
282
|
+
await nextTick();
|
|
283
|
+
expect(wrapper.vm.getValue()).toBe(false);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('selects second-to-last suggestion in circle when selecting previous item', async () => {
|
|
287
|
+
wrapper.vm.nextItem();
|
|
288
|
+
wrapper.vm.prevItem();
|
|
289
|
+
wrapper.vm.prevItem();
|
|
290
|
+
await nextTick();
|
|
291
|
+
expect(wrapper.vm.getValue()).toBe('Two');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('selects second suggestion in circle when selecting next item', async () => {
|
|
295
|
+
wrapper.vm.nextItem();
|
|
296
|
+
wrapper.vm.nextItem();
|
|
297
|
+
wrapper.vm.nextItem();
|
|
298
|
+
wrapper.vm.nextItem();
|
|
299
|
+
wrapper.vm.nextItem();
|
|
300
|
+
await nextTick();
|
|
301
|
+
expect(wrapper.vm.getValue()).toBe('Two');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('highlights suggestion if initial-value is provided', async () => {
|
|
305
|
+
await wrapper.setProps({ initialValue: 'Two' });
|
|
306
|
+
expect(findActiveSuggestion().text()).toBe('Two');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('highlights suggestion if initial-value is provided, regardless of case sensitivity', async () => {
|
|
310
|
+
await wrapper.setProps({ initialValue: 'two' });
|
|
311
|
+
expect(findActiveSuggestion().text()).toBe('Two');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('highlights suggestion if initial-value is provided, regardless of falsiness', async () => {
|
|
315
|
+
await wrapper.setProps({ initialValue: false });
|
|
316
|
+
expect(findActiveSuggestion().text()).toBe('Three');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('highlights first suggestion if initial-value is provided, deselected then selected', async () => {
|
|
320
|
+
await wrapper.setProps({ initialValue: 'One' });
|
|
321
|
+
wrapper.vm.prevItem();
|
|
322
|
+
wrapper.vm.nextItem();
|
|
323
|
+
await nextTick();
|
|
324
|
+
expect(findActiveSuggestion().text()).toBe('One');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('does not highlight anything if initial-value matches nothing', async () => {
|
|
328
|
+
await wrapper.setProps({ initialValue: 'missing' });
|
|
329
|
+
expect(findActiveSuggestion().exists()).toBe(false);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('applies the injected suggestion-list-class to the dropdown', () => {
|
|
333
|
+
expect(wrapper.classes()).toContain('custom-class');
|
|
334
|
+
});
|
|
188
335
|
});
|
|
189
336
|
});
|
|
190
337
|
});
|
|
@@ -6,7 +6,7 @@ const NO_ACTIVE_ITEM = -2;
|
|
|
6
6
|
|
|
7
7
|
export default {
|
|
8
8
|
name: 'GlFilteredSearchSuggestionList',
|
|
9
|
-
inject: ['suggestionsListClass'],
|
|
9
|
+
inject: ['suggestionsListClass', 'termsAsTokens'],
|
|
10
10
|
provide() {
|
|
11
11
|
return {
|
|
12
12
|
filteredSearchSuggestionListInstance: this,
|
|
@@ -40,7 +40,7 @@ export default {
|
|
|
40
40
|
return this.registeredItems[this.initialActiveIdx];
|
|
41
41
|
},
|
|
42
42
|
activeItem() {
|
|
43
|
-
if (this.activeIdx === NO_ACTIVE_ITEM) return null;
|
|
43
|
+
if (!this.termsAsTokens() && this.activeIdx === NO_ACTIVE_ITEM) return null;
|
|
44
44
|
if (this.activeIdx === DEFER_TO_INITIAL_VALUE) return this.initialActiveItem;
|
|
45
45
|
return this.registeredItems[this.activeIdx];
|
|
46
46
|
},
|
|
@@ -76,15 +76,24 @@ export default {
|
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
78
|
nextItem() {
|
|
79
|
-
this.
|
|
79
|
+
if (this.termsAsTokens()) {
|
|
80
|
+
this.stepItem(1);
|
|
81
|
+
} else {
|
|
82
|
+
this.stepItem(1, this.registeredItems.length - 1);
|
|
83
|
+
}
|
|
80
84
|
},
|
|
81
85
|
prevItem() {
|
|
82
|
-
this.
|
|
86
|
+
if (this.termsAsTokens()) {
|
|
87
|
+
this.stepItem(-1);
|
|
88
|
+
} else {
|
|
89
|
+
this.stepItem(-1, 0);
|
|
90
|
+
}
|
|
83
91
|
},
|
|
84
92
|
stepItem(direction, endIdx) {
|
|
85
93
|
if (
|
|
86
|
-
this.
|
|
87
|
-
(this.activeIdx ===
|
|
94
|
+
!this.termsAsTokens() &&
|
|
95
|
+
(this.activeIdx === endIdx ||
|
|
96
|
+
(this.activeIdx === DEFER_TO_INITIAL_VALUE && this.initialActiveIdx === endIdx))
|
|
88
97
|
) {
|
|
89
98
|
// The user wants to move past the end of the list, so ensure nothing is selected.
|
|
90
99
|
this.activeIdx = NO_ACTIVE_ITEM;
|