@gitlab/ui 60.2.0 → 61.1.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.
Files changed (31) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/components/base/banner/banner.js +11 -1
  3. package/dist/components/base/filtered_search/filtered_search.js +1 -4
  4. package/dist/components/base/filtered_search/filtered_search_suggestion.js +1 -1
  5. package/dist/components/base/filtered_search/filtered_search_suggestion_list.js +30 -16
  6. package/dist/components/base/filtered_search/filtered_search_term.js +4 -6
  7. package/dist/components/base/filtered_search/filtered_search_token.js +16 -17
  8. package/dist/components/base/filtered_search/filtered_search_token_segment.js +23 -14
  9. package/dist/components/base/filtered_search/filtered_search_utils.js +78 -1
  10. package/dist/utils/number_utils.js +22 -1
  11. package/package.json +1 -1
  12. package/src/components/base/banner/banner.vue +11 -9
  13. package/src/components/base/filtered_search/__snapshots__/filtered_search_term.spec.js.snap +2 -0
  14. package/src/components/base/filtered_search/filtered_search.md +5 -3
  15. package/src/components/base/filtered_search/filtered_search.stories.js +8 -5
  16. package/src/components/base/filtered_search/filtered_search.vue +1 -11
  17. package/src/components/base/filtered_search/filtered_search_suggestion.md +8 -2
  18. package/src/components/base/filtered_search/filtered_search_suggestion.vue +1 -0
  19. package/src/components/base/filtered_search/filtered_search_suggestion_list.spec.js +61 -64
  20. package/src/components/base/filtered_search/filtered_search_suggestion_list.vue +39 -20
  21. package/src/components/base/filtered_search/filtered_search_term.spec.js +11 -28
  22. package/src/components/base/filtered_search/filtered_search_term.vue +6 -17
  23. package/src/components/base/filtered_search/filtered_search_token.spec.js +3 -22
  24. package/src/components/base/filtered_search/filtered_search_token.vue +8 -16
  25. package/src/components/base/filtered_search/filtered_search_token_segment.spec.js +18 -1
  26. package/src/components/base/filtered_search/filtered_search_token_segment.stories.js +9 -0
  27. package/src/components/base/filtered_search/filtered_search_token_segment.vue +35 -12
  28. package/src/components/base/filtered_search/filtered_search_utils.js +69 -0
  29. package/src/components/base/filtered_search/filtered_search_utils.spec.js +32 -1
  30. package/src/utils/number_utils.js +21 -0
  31. package/src/utils/number_utils.spec.js +42 -0
@@ -1,11 +1,13 @@
1
1
  <script>
2
2
  import { bannerVariants } from '../../../utils/constants';
3
+ import CloseButton from '../../shared_components/close_button/close_button.vue';
3
4
  import GlButton from '../button/button.vue';
4
5
  import GlCard from '../card/card.vue';
5
6
 
6
7
  export default {
7
8
  name: 'GlBanner',
8
9
  components: {
10
+ CloseButton,
9
11
  GlButton,
10
12
  GlCard,
11
13
  },
@@ -51,6 +53,14 @@ export default {
51
53
  return bannerVariants.includes(value);
52
54
  },
53
55
  },
56
+ /**
57
+ * Dismiss button's aria-label.
58
+ */
59
+ dismissLabel: {
60
+ type: String,
61
+ required: false,
62
+ default: 'Dismiss',
63
+ },
54
64
  /**
55
65
  * Removes the border for banners embedded in content.
56
66
  */
@@ -112,14 +122,6 @@ export default {
112
122
  <!-- @slot The banner actions to display -->
113
123
  <slot name="actions"></slot>
114
124
  </div>
115
- <gl-button
116
- :variant="isIntroducing ? 'confirm' : 'default'"
117
- category="tertiary"
118
- size="small"
119
- icon="close"
120
- aria-label="Close banner"
121
- class="gl-banner-close"
122
- @click="handleClose"
123
- />
125
+ <close-button class="gl-banner-close" :label="dismissLabel" @click="handleClose" />
124
126
  </gl-card>
125
127
  </template>
@@ -9,6 +9,7 @@ exports[`Filtered search term renders input with value in active mode 1`] = `
9
9
  active="true"
10
10
  class="gl-filtered-search-term-token"
11
11
  cursor-position="end"
12
+ is-term=""
12
13
  value="test-value"
13
14
  >
14
15
  test-value
@@ -24,6 +25,7 @@ exports[`Filtered search term renders value in inactive mode 1`] = `
24
25
  <div
25
26
  class="gl-filtered-search-term-token"
26
27
  cursor-position="end"
28
+ is-term=""
27
29
  value="test-value"
28
30
  >
29
31
  test-value
@@ -22,9 +22,11 @@ Prepare array of available token configurations with the following fields:
22
22
  It is discouraged to use this together with `unique`, as `unique` is intended for single-select.
23
23
  - `options`: (optional) an array of options which the user can pick after the
24
24
  operator has been selected. The option object can have the following
25
- properties defined: `value: string`, `icon: string`, `title: string` and
26
- `default: boolean`. If the `default` is omitted, the `value` of the first
27
- option will be displayed as a suggestion
25
+ properties defined: `value: string`, `icon: string`, `title: string`,
26
+ `component: Object` and `default: boolean`. If `component` is provided, it is
27
+ is used to render the option in the suggestions list.
28
+ - `optionComponent`: (optional) A component used to render the token option
29
+ itself when adding a new token or replacing an existing one
28
30
  - any additional fields required to configure your component
29
31
 
30
32
  Each token for filtered search is a Vue component with the following props:
@@ -304,8 +304,8 @@ const tokens = [
304
304
  unique: true,
305
305
  token: GlFilteredSearchToken,
306
306
  options: [
307
- { icon: 'eye-slash', value: 'Yes', title: 'Yes' },
308
- { icon: 'eye', value: 'No', title: 'No' },
307
+ { icon: 'eye-slash', value: 'true', title: 'Yes' },
308
+ { icon: 'eye', value: 'false', title: 'No' },
309
309
  ],
310
310
  },
311
311
  ];
@@ -410,8 +410,8 @@ export const WithFriendlyText = () => ({
410
410
  unique: true,
411
411
  token: GlFilteredSearchToken,
412
412
  options: [
413
- { icon: 'eye-slash', value: 'Yes', title: 'Yes' },
414
- { icon: 'eye', value: 'No', title: 'No' },
413
+ { icon: 'eye-slash', value: 'true', title: 'Yes' },
414
+ { icon: 'eye', value: 'false', title: 'No' },
415
415
  ],
416
416
  },
417
417
  ],
@@ -480,6 +480,9 @@ export const WithMultiSelect = () => {
480
480
  isLastUser(index) {
481
481
  return index === this.selectedUsers.length - 1;
482
482
  },
483
+ key(user, index) {
484
+ return `${user.id}-${index}`;
485
+ },
483
486
  },
484
487
  watch: {
485
488
  // eslint-disable-next-line func-names
@@ -514,7 +517,7 @@ export const WithMultiSelect = () => {
514
517
  </template>
515
518
  </template>
516
519
  <template #suggestions>
517
- <gl-filtered-search-suggestion :key="user.id" v-for="user in filteredUsers" :value="user.username">
520
+ <gl-filtered-search-suggestion :key="key(user, index)" v-for="(user, index) in filteredUsers" :value="user.username">
518
521
  <div class="gl-display-flex gl-align-items-center">
519
522
  <gl-icon
520
523
  v-if="config.multiSelect"
@@ -345,9 +345,6 @@ export default {
345
345
  */
346
346
  this.$emit('submit', normalizeTokens(cloneDeep(this.tokens)));
347
347
  },
348
- hasTitleSlot() {
349
- return Boolean(this.$scopedSlots.title);
350
- },
351
348
  },
352
349
  };
353
350
  </script>
@@ -403,14 +400,7 @@ export default {
403
400
  @split="createTokens(idx, $event)"
404
401
  @previous="activatePreviousToken"
405
402
  @next="activateNextToken"
406
- >
407
- <template
408
- v-if="hasTitleSlot() && getTokenComponent(token.type).name === 'GlFilteredSearchTerm'"
409
- #title="title"
410
- >
411
- <slot name="title" v-bind="title"></slot>
412
- </template>
413
- </component>
403
+ />
414
404
  </div>
415
405
  <portal-target
416
406
  ref="menu"
@@ -3,7 +3,13 @@ suggestions in a top-level suggestion list:
3
3
 
4
4
  ```html
5
5
  <gl-filtered-search-suggestion-list>
6
- <gl-filtered-search-suggestion value="foo">Example suggestion</gl-filtered-search-suggestion>
7
- <gl-filtered-search-suggestion value="bar">Example suggestion 2</gl-filtered-search-suggestion>
6
+ <gl-filtered-search-suggestion value="foo" key="foo-0">Example suggestion</gl-filtered-search-suggestion>
7
+ <gl-filtered-search-suggestion value="bar" key="bar-1">Example suggestion 2</gl-filtered-search-suggestion>
8
8
  </gl-filtered-search-suggestion-list>
9
9
  ```
10
+
11
+ > NOTE: Provide a `key` to suggestions of the form `${value}-${index}` (or
12
+ > similar). While using the index in keys is usually frowned upon for
13
+ > performance reasons, the current implementation relies on all suggestions
14
+ > getting destroyed and recreated to keep rendering order in sync with
15
+ > <kbd>Up</kbd>/<kbd>Down</kbd> keyboard interaction.
@@ -53,6 +53,7 @@ export default {
53
53
  class="gl-filtered-search-suggestion"
54
54
  data-testid="filtered-search-suggestion"
55
55
  :class="{ 'gl-filtered-search-suggestion-active': isActive }"
56
+ tabindex="-1"
56
57
  v-bind="$attrs"
57
58
  href="#"
58
59
  @mousedown.native.prevent="emitValue"
@@ -1,3 +1,4 @@
1
+ import { nextTick } from 'vue';
1
2
  import { shallowMount, mount } from '@vue/test-utils';
2
3
  import FilteredSearchSuggestion from './filtered_search_suggestion.vue';
3
4
  import FilteredSearchSuggestionList from './filtered_search_suggestion_list.vue';
@@ -32,40 +33,42 @@ describe('Filtered search suggestion list component', () => {
32
33
  expect(wrapper.vm.getValue()).toBe(null);
33
34
  });
34
35
 
35
- it('selects first item on nextItem call', () => {
36
+ it('selects first item on nextItem call', async () => {
36
37
  wrapper.vm.nextItem();
37
- return wrapper.vm.$nextTick().then(() => {
38
- expect(wrapper.vm.getValue()).toBe(stubs[0].value);
39
- });
38
+ await nextTick();
39
+ expect(wrapper.vm.getValue()).toBe(stubs[0].value);
40
40
  });
41
41
 
42
- it('deselects first item on prevItem call', () => {
42
+ it('deselects first item on prevItem call', async () => {
43
43
  wrapper.vm.nextItem();
44
44
  wrapper.vm.prevItem();
45
- return wrapper.vm.$nextTick().then(() => {
46
- expect(wrapper.vm.getValue()).toBe(null);
47
- });
45
+ await nextTick();
46
+ expect(wrapper.vm.getValue()).toBe(null);
48
47
  });
49
48
 
50
- it('deselects last item on nextItem call', () => {
49
+ it('deselects last item on nextItem call', async () => {
51
50
  stubs.forEach(() => wrapper.vm.nextItem());
52
51
  wrapper.vm.nextItem();
53
- return wrapper.vm.$nextTick().then(() => {
54
- expect(wrapper.vm.getValue()).toBe(null);
55
- });
52
+ await nextTick();
53
+ expect(wrapper.vm.getValue()).toBe(null);
56
54
  });
57
55
 
58
- it('remove selection if suggestion is unregistered', () => {
56
+ it('remove selection if suggestion is unregistered', async () => {
59
57
  wrapper.vm.nextItem();
60
- return wrapper.vm
61
- .$nextTick()
62
- .then(() => {
63
- wrapper.vm.unregister(stubs[0]);
64
- return wrapper.vm.$nextTick();
65
- })
66
- .then(() => {
67
- expect(wrapper.vm.getValue()).toBe(null);
68
- });
58
+ await nextTick();
59
+ wrapper.vm.unregister(stubs[0]);
60
+ await nextTick();
61
+ expect(wrapper.vm.getValue()).toBe(null);
62
+ });
63
+
64
+ it('selects correct suggestion when item (un)registration is late', async () => {
65
+ // Initially stub2 is at index 1.
66
+ await wrapper.setProps({ initialValue: 'stub2' });
67
+ // Remove item at index 0, so stub2 moves to index 0
68
+ wrapper.vm.unregister(stubs[0]);
69
+ await nextTick();
70
+ // stub2 should still be selected
71
+ expect(wrapper.vm.getValue()).toBe('stub2');
69
72
  });
70
73
  });
71
74
  });
@@ -105,85 +108,79 @@ describe('Filtered search suggestion list component', () => {
105
108
  });
106
109
  });
107
110
 
108
- it('selects first suggestion', () => {
111
+ it('selects first suggestion', async () => {
109
112
  wrapper.vm.nextItem();
110
- return wrapper.vm.$nextTick().then(() => {
111
- expect(wrapper.vm.getValue()).toBe('One');
112
- });
113
+ await nextTick();
114
+ expect(wrapper.vm.getValue()).toBe('One');
113
115
  });
114
116
 
115
- it('selects second suggestion', () => {
117
+ it('selects second suggestion', async () => {
116
118
  wrapper.vm.nextItem();
117
119
  wrapper.vm.nextItem();
118
- return wrapper.vm.$nextTick().then(() => {
119
- expect(wrapper.vm.getValue()).toBe('Two');
120
- });
120
+ await nextTick();
121
+ expect(wrapper.vm.getValue()).toBe('Two');
121
122
  });
122
123
 
123
- it('deselects first suggestion after list end', () => {
124
+ it('deselects first suggestion after list end', async () => {
124
125
  wrapper.vm.nextItem();
125
126
  wrapper.vm.nextItem();
126
127
  wrapper.vm.nextItem();
127
128
  wrapper.vm.nextItem();
128
- return wrapper.vm.$nextTick().then(() => {
129
- expect(wrapper.vm.getValue()).toBe(null);
130
- });
129
+ await nextTick();
130
+ expect(wrapper.vm.getValue()).toBe(null);
131
131
  });
132
132
 
133
- it('deselects first suggestion after list start', () => {
133
+ it('deselects first suggestion after list start', async () => {
134
134
  wrapper.vm.nextItem();
135
135
  wrapper.vm.prevItem();
136
- return wrapper.vm.$nextTick().then(() => {
137
- expect(wrapper.vm.getValue()).toBe(null);
138
- });
136
+ await nextTick();
137
+ expect(wrapper.vm.getValue()).toBe(null);
139
138
  });
140
139
 
141
- it('selects last suggestion in circle when selecting previous item', () => {
140
+ it('selects last suggestion in circle when selecting previous item', async () => {
142
141
  wrapper.vm.nextItem();
143
142
  wrapper.vm.prevItem();
144
143
  wrapper.vm.prevItem();
145
- return wrapper.vm.$nextTick().then(() => {
146
- expect(wrapper.vm.getValue()).toBe(false);
147
- });
144
+ await nextTick();
145
+ expect(wrapper.vm.getValue()).toBe(false);
148
146
  });
149
147
 
150
- it('selects first suggestion in circle when selecting next item', () => {
148
+ it('selects first suggestion in circle when selecting next item', async () => {
151
149
  wrapper.vm.nextItem();
152
150
  wrapper.vm.nextItem();
153
151
  wrapper.vm.nextItem();
154
152
  wrapper.vm.nextItem();
155
153
  wrapper.vm.nextItem();
156
- return wrapper.vm.$nextTick().then(() => {
157
- expect(wrapper.vm.getValue()).toBe('One');
158
- });
154
+ await nextTick();
155
+ expect(wrapper.vm.getValue()).toBe('One');
159
156
  });
160
157
 
161
- it('highlights suggestion if initial-value is provided', () => {
162
- wrapper.setProps({ initialValue: 'Two' });
163
- return wrapper.vm.$nextTick().then(() => {
164
- expect(wrapper.find('.gl-filtered-search-suggestion-active').text()).toBe('Two');
165
- });
158
+ it('highlights suggestion if initial-value is provided', async () => {
159
+ await wrapper.setProps({ initialValue: 'Two' });
160
+ expect(wrapper.find('.gl-filtered-search-suggestion-active').text()).toBe('Two');
166
161
  });
167
162
 
168
163
  it('highlights suggestion if initial-value is provided, regardless of case sensitivity', async () => {
169
- wrapper.setProps({ initialValue: 'two' });
170
- return wrapper.vm.$nextTick().then(() => {
171
- expect(wrapper.find('.gl-filtered-search-suggestion-active').text()).toBe('Two');
172
- });
164
+ await wrapper.setProps({ initialValue: 'two' });
165
+ expect(wrapper.find('.gl-filtered-search-suggestion-active').text()).toBe('Two');
173
166
  });
174
167
 
175
168
  it('highlights suggestion if initial-value is provided, regardless of falsiness', async () => {
176
- wrapper.setProps({ initialValue: false });
177
- return wrapper.vm.$nextTick().then(() => {
178
- expect(wrapper.find('.gl-filtered-search-suggestion-active').text()).toBe('Three');
179
- });
169
+ await wrapper.setProps({ initialValue: false });
170
+ expect(wrapper.find('.gl-filtered-search-suggestion-active').text()).toBe('Three');
180
171
  });
181
172
 
182
- it('does not highlight anything if initial-value matches nothing', () => {
183
- wrapper.setProps({ initialValue: 'missing' });
184
- return wrapper.vm.$nextTick().then(() => {
185
- expect(wrapper.find('.gl-filtered-search-suggestion-active').exists()).toBe(false);
186
- });
173
+ it('highlights first suggestion if initial-value is provided, deselected then selected', async () => {
174
+ await wrapper.setProps({ initialValue: 'One' });
175
+ wrapper.vm.prevItem();
176
+ wrapper.vm.nextItem();
177
+ await nextTick();
178
+ expect(wrapper.find('.gl-filtered-search-suggestion-active').text()).toBe('One');
179
+ });
180
+
181
+ it('does not highlight anything if initial-value matches nothing', async () => {
182
+ await wrapper.setProps({ initialValue: 'missing' });
183
+ expect(wrapper.find('.gl-filtered-search-suggestion-active').exists()).toBe(false);
187
184
  });
188
185
 
189
186
  it('applies the injected suggestion-list-class to the dropdown', () => {
@@ -1,4 +1,9 @@
1
1
  <script>
2
+ import { stepIndexAndWrap } from './filtered_search_utils';
3
+
4
+ const DEFER_TO_INITIAL_VALUE = -1;
5
+ const NO_ACTIVE_ITEM = -2;
6
+
2
7
  export default {
3
8
  name: 'GlFilteredSearchSuggestionList',
4
9
  inject: ['suggestionsListClass'],
@@ -17,18 +22,27 @@ export default {
17
22
  default: null,
18
23
  },
19
24
  },
25
+
20
26
  data() {
21
27
  return {
22
- activeIdx: -1,
28
+ activeIdx: DEFER_TO_INITIAL_VALUE,
23
29
  registeredItems: [],
24
30
  };
25
31
  },
26
32
 
27
33
  computed: {
34
+ initialActiveIdx() {
35
+ return this.registeredItems.findIndex((item) =>
36
+ this.valuesMatch(item.value, this.initialValue)
37
+ );
38
+ },
39
+ initialActiveItem() {
40
+ return this.registeredItems[this.initialActiveIdx];
41
+ },
28
42
  activeItem() {
29
- return this.activeIdx > -1 && this.activeIdx < this.registeredItems.length
30
- ? this.registeredItems[this.activeIdx]
31
- : null;
43
+ if (this.activeIdx === NO_ACTIVE_ITEM) return null;
44
+ if (this.activeIdx === DEFER_TO_INITIAL_VALUE) return this.initialActiveItem;
45
+ return this.registeredItems[this.activeIdx];
32
46
  },
33
47
  listClasses() {
34
48
  return [this.suggestionsListClass(), 'dropdown-menu gl-filtered-search-suggestion-list'];
@@ -36,10 +50,8 @@ export default {
36
50
  },
37
51
 
38
52
  watch: {
39
- initialValue(newValue) {
40
- this.activeIdx = this.registeredItems.findIndex((item) =>
41
- this.valuesMatch(item.value, newValue)
42
- );
53
+ initialValue() {
54
+ this.activeIdx = DEFER_TO_INITIAL_VALUE;
43
55
  },
44
56
  },
45
57
 
@@ -53,31 +65,38 @@ export default {
53
65
  },
54
66
  register(item) {
55
67
  this.registeredItems.push(item);
56
- if (this.valuesMatch(item.value, this.initialValue)) {
57
- this.activeIdx = this.registeredItems.length - 1;
58
- }
59
68
  },
60
69
  unregister(item) {
61
70
  const idx = this.registeredItems.indexOf(item);
62
71
  if (idx !== -1) {
63
72
  this.registeredItems.splice(idx, 1);
64
73
  if (idx === this.activeIdx) {
65
- this.activeIdx = -1;
74
+ this.activeIdx = DEFER_TO_INITIAL_VALUE;
66
75
  }
67
76
  }
68
77
  },
69
78
  nextItem() {
70
- if (this.activeIdx < this.registeredItems.length) {
71
- this.activeIdx += 1;
72
- } else {
73
- this.activeIdx = 0;
74
- }
79
+ this.stepItem(1, this.registeredItems.length - 1);
75
80
  },
76
81
  prevItem() {
77
- if (this.activeIdx >= 0) {
78
- this.activeIdx -= 1;
82
+ this.stepItem(-1, 0);
83
+ },
84
+ stepItem(direction, endIdx) {
85
+ if (
86
+ this.activeIdx === endIdx ||
87
+ (this.activeIdx === DEFER_TO_INITIAL_VALUE && this.initialActiveIdx === endIdx)
88
+ ) {
89
+ // The user wants to move past the end of the list, so ensure nothing is selected.
90
+ this.activeIdx = NO_ACTIVE_ITEM;
79
91
  } else {
80
- this.activeIdx = this.registeredItems.length - 1;
92
+ const index =
93
+ this.activeIdx === DEFER_TO_INITIAL_VALUE
94
+ ? // Currently active item is set by initialValue (i.e., text input matching),
95
+ // so step relative to that.
96
+ this.initialActiveIdx
97
+ : // Otherwise, step relative to the explicitly (via up/down arrows) activated item.
98
+ this.activeIdx;
99
+ this.activeIdx = stepIndexAndWrap(index, direction, this.registeredItems.length);
81
100
  }
82
101
  },
83
102
  getValue() {
@@ -1,6 +1,5 @@
1
1
  import { nextTick } from 'vue';
2
2
  import { shallowMount } from '@vue/test-utils';
3
- import GlFilteredSearchSuggestion from './filtered_search_suggestion.vue';
4
3
  import FilteredSearchTerm from './filtered_search_term.vue';
5
4
  import { INTENT_ACTIVATE_PREVIOUS } from './filtered_search_utils';
6
5
 
@@ -23,39 +22,21 @@ describe('Filtered search term', () => {
23
22
  const segmentStub = {
24
23
  name: 'gl-filtered-search-token-segment-stub',
25
24
  template: '<div><slot name="view"></slot><slot name="suggestions"></slot></div>',
26
- props: ['searchInputAttributes', 'isLastToken', 'currentValue', 'viewOnly'],
25
+ props: ['searchInputAttributes', 'isLastToken', 'currentValue', 'viewOnly', 'options'],
27
26
  };
28
27
 
29
- const createComponent = (props, options = {}) => {
28
+ const createComponent = (props) => {
30
29
  wrapper = shallowMount(FilteredSearchTerm, {
31
30
  propsData: { ...defaultProps, ...props },
32
31
  stubs: {
33
32
  'gl-filtered-search-token-segment': segmentStub,
34
33
  },
35
- ...options,
36
34
  });
37
35
  };
38
36
 
39
37
  const findSearchInput = () => wrapper.find('input');
40
38
  const findTokenSegmentComponent = () => wrapper.findComponent(segmentStub);
41
39
 
42
- it('renders title slot', async () => {
43
- createComponent(
44
- { availableTokens, active: true, value: { data: 'test1' } },
45
- {
46
- scopedSlots: {
47
- title: '<div slot-scope="{ value }">New {{value}}</div>',
48
- },
49
- }
50
- );
51
-
52
- await nextTick();
53
-
54
- expect(wrapper.findAllComponents(GlFilteredSearchSuggestion).at(0).text()).toBe(
55
- 'New test1-foo'
56
- );
57
- });
58
-
59
40
  it('renders value in inactive mode', () => {
60
41
  createComponent({ value: { data: 'test-value' } });
61
42
  expect(wrapper.html()).toMatchSnapshot();
@@ -76,7 +57,7 @@ describe('Filtered search term', () => {
76
57
 
77
58
  await nextTick();
78
59
 
79
- expect(wrapper.findAllComponents(GlFilteredSearchSuggestion)).toHaveLength(2);
60
+ expect(findTokenSegmentComponent().props('options')).toHaveLength(2);
80
61
  });
81
62
 
82
63
  it.each`
@@ -120,12 +101,14 @@ describe('Filtered search term', () => {
120
101
  viewOnly,
121
102
  });
122
103
 
123
- expect(findTokenSegmentComponent().props()).toEqual({
124
- searchInputAttributes,
125
- isLastToken,
126
- currentValue,
127
- viewOnly,
128
- });
104
+ expect(findTokenSegmentComponent().props()).toEqual(
105
+ expect.objectContaining({
106
+ searchInputAttributes,
107
+ isLastToken,
108
+ currentValue,
109
+ viewOnly,
110
+ })
111
+ );
129
112
  });
130
113
 
131
114
  it('by default sets `viewOnly` to false on `GlFilteredSearchTokenSegment`', () => {
@@ -1,13 +1,11 @@
1
1
  <script>
2
- import GlFilteredSearchSuggestion from './filtered_search_suggestion.vue';
3
2
  import GlFilteredSearchTokenSegment from './filtered_search_token_segment.vue';
4
- import { INTENT_ACTIVATE_PREVIOUS } from './filtered_search_utils';
3
+ import { INTENT_ACTIVATE_PREVIOUS, match, tokenToOption } from './filtered_search_utils';
5
4
 
6
5
  export default {
7
6
  name: 'GlFilteredSearchTerm',
8
7
  components: {
9
8
  GlFilteredSearchTokenSegment,
10
- GlFilteredSearchSuggestion,
11
9
  },
12
10
  inheritAttrs: false,
13
11
  props: {
@@ -77,9 +75,9 @@ export default {
77
75
  },
78
76
  computed: {
79
77
  suggestedTokens() {
80
- return this.availableTokens.filter((item) =>
81
- item.title.toLowerCase().includes(this.value.data.toLowerCase())
82
- );
78
+ return this.availableTokens
79
+ .filter((token) => match(token.title, this.value.data))
80
+ .map(tokenToOption);
83
81
  },
84
82
  internalValue: {
85
83
  get() {
@@ -143,6 +141,7 @@ export default {
143
141
  <gl-filtered-search-token-segment
144
142
  ref="segment"
145
143
  v-model="internalValue"
144
+ is-term
146
145
  class="gl-filtered-search-term-token"
147
146
  :active="active"
148
147
  :cursor-position="cursorPosition"
@@ -150,6 +149,7 @@ export default {
150
149
  :is-last-token="isLastToken"
151
150
  :current-value="currentValue"
152
151
  :view-only="viewOnly"
152
+ :options="suggestedTokens"
153
153
  @activate="$emit('activate')"
154
154
  @deactivate="$emit('deactivate')"
155
155
  @complete="$emit('replace', { type: $event })"
@@ -159,17 +159,6 @@ export default {
159
159
  @previous="$emit('previous')"
160
160
  @next="$emit('next')"
161
161
  >
162
- <template #suggestions>
163
- <gl-filtered-search-suggestion
164
- v-for="(item, idx) in suggestedTokens"
165
- :key="idx"
166
- :value="item.type"
167
- :icon-name="item.icon"
168
- >
169
- <slot name="title" v-bind="{ value: item.title }"> {{ item.title }} </slot>
170
- </gl-filtered-search-suggestion>
171
- </template>
172
-
173
162
  <template #view>
174
163
  <input
175
164
  v-if="placeholder"
@@ -33,10 +33,9 @@ describe('Filtered search token', () => {
33
33
  cursorPosition: 'end',
34
34
  };
35
35
 
36
- const createComponent = (props, options = {}) => {
36
+ const createComponent = (props) => {
37
37
  wrapper = shallowMount(GlFilteredSearchToken, {
38
38
  propsData: { ...defaultProps, ...props },
39
- ...options,
40
39
  });
41
40
  };
42
41
 
@@ -190,7 +189,7 @@ describe('Filtered search token', () => {
190
189
  config: availableTokens[0],
191
190
  });
192
191
 
193
- findTitleSegment().vm.$emit('complete', availableTokens[1].title);
192
+ findTitleSegment().vm.$emit('complete', availableTokens[1].type);
194
193
 
195
194
  expect(wrapper.emitted('replace')).toHaveLength(1);
196
195
  expect(wrapper.emitted().replace[0][0].value).toStrictEqual(originalValue);
@@ -204,7 +203,7 @@ describe('Filtered search token', () => {
204
203
  config: availableTokens[0],
205
204
  });
206
205
 
207
- findTitleSegment().vm.$emit('complete', availableTokens[2].title);
206
+ findTitleSegment().vm.$emit('complete', availableTokens[2].type);
208
207
 
209
208
  expect(wrapper.emitted('replace')).toHaveLength(1);
210
209
  expect(wrapper.emitted().replace[0][0].value).toStrictEqual({ data: '' });
@@ -373,22 +372,4 @@ describe('Filtered search token', () => {
373
372
  }
374
373
  );
375
374
  });
376
-
377
- it('renders title options passed via slot', async () => {
378
- createComponent(
379
- {},
380
- {
381
- slots: {
382
- 'title-option': '<div>new title</div>',
383
- },
384
- stubs: {
385
- GlFilteredSearchTokenSegment: {
386
- template: `<div><slot name="option" v-bind='{ option: { value: "default title"} }'></slot></div>`,
387
- },
388
- },
389
- }
390
- );
391
-
392
- expect(findTitleSegment().text()).toBe('new title');
393
- });
394
375
  });