@gitlab/ui 44.1.0 → 46.0.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 +65 -0
- package/dist/components/base/alert/alert.js +9 -1
- package/dist/components/base/new_dropdowns/base_dropdown/base_dropdown.js +8 -0
- package/dist/components/base/new_dropdowns/listbox/listbox.js +116 -18
- package/dist/utility_classes.css +1 -1
- package/dist/utility_classes.css.map +1 -1
- package/package.json +2 -2
- package/src/components/base/alert/alert.spec.js +13 -14
- package/src/components/base/alert/alert.vue +14 -1
- package/src/components/base/banner/banner.spec.js +4 -4
- package/src/components/base/datepicker/datepicker.spec.js +1 -1
- package/src/components/base/daterange_picker/daterange_picker.spec.js +4 -4
- package/src/components/base/drawer/drawer.spec.js +2 -2
- package/src/components/base/dropdown/dropdown.spec.js +5 -5
- package/src/components/base/filtered_search/filtered_search.spec.js +4 -4
- package/src/components/base/filtered_search/filtered_search_token.spec.js +9 -9
- package/src/components/base/filtered_search/filtered_search_token_segment.spec.js +12 -12
- package/src/components/base/form/form_textarea/form_textarea.spec.js +2 -2
- package/src/components/base/infinite_scroll/infinite_scroll.spec.js +6 -6
- package/src/components/base/label/label.spec.js +5 -5
- package/src/components/base/modal/modal.spec.js +1 -1
- package/src/components/base/nav/nav.spec.js +1 -1
- package/src/components/base/nav/nav_item_dropdown.spec.js +3 -3
- package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.vue +6 -0
- package/src/components/base/new_dropdowns/listbox/listbox.md +22 -0
- package/src/components/base/new_dropdowns/listbox/listbox.spec.js +101 -7
- package/src/components/base/new_dropdowns/listbox/listbox.stories.js +244 -20
- package/src/components/base/new_dropdowns/listbox/listbox.vue +138 -12
- package/src/components/base/paginated_list/paginated_list.spec.js +1 -1
- package/src/components/base/pagination/pagination.spec.js +2 -2
- package/src/components/base/search_box_by_click/search_box_by_click.spec.js +7 -7
- package/src/components/base/search_box_by_type/search_box_by_type.spec.js +2 -2
- package/src/components/base/sorting/sorting.spec.js +1 -1
- package/src/components/base/sorting/sorting_item.spec.js +2 -2
- package/src/components/base/tabs/tabs/scrollable_tabs.spec.js +1 -1
- package/src/components/base/tabs/tabs/tabs.spec.js +6 -6
- package/src/components/base/token/token.spec.js +1 -1
- package/src/components/base/token_selector/token_container.spec.js +1 -1
- package/src/components/base/token_selector/token_selector.spec.js +4 -4
- package/src/components/base/token_selector/token_selector_dropdown.spec.js +1 -1
- package/src/components/charts/column/column_chart.spec.js +1 -1
- package/src/components/charts/sparkline/sparkline.spec.js +2 -2
- package/src/directives/resize_observer/resize_observer.spec.js +5 -5
- package/src/scss/utilities.scss +0 -202
- package/src/scss/utility-mixins/display.scss +0 -35
- package/src/scss/utility-mixins/flex.scss +0 -7
- package/src/scss/utility-mixins/spacing.scss +0 -91
|
@@ -30,6 +30,10 @@ describe('GlListbox', () => {
|
|
|
30
30
|
const findListboxItems = (root = wrapper) => root.findAllComponents(GlListboxItem);
|
|
31
31
|
const findListboxGroups = () => wrapper.findAllComponents(GlListboxGroup);
|
|
32
32
|
const findListItem = (index) => findListboxItems().at(index).find(ITEM_SELECTOR);
|
|
33
|
+
const findSearchBox = () => wrapper.find("[data-testid='listbox-search-input']");
|
|
34
|
+
const findNoResultsText = () => wrapper.find("[data-testid='listbox-no-results-text']");
|
|
35
|
+
const findLoadingIcon = () => wrapper.find("[data-testid='listbox-search-loader']");
|
|
36
|
+
const findSRNumberOfResultsText = () => wrapper.find("[data-testid='listbox-number-of-results']");
|
|
33
37
|
|
|
34
38
|
describe('toggle text', () => {
|
|
35
39
|
describe.each`
|
|
@@ -53,11 +57,17 @@ describe('GlListbox', () => {
|
|
|
53
57
|
|
|
54
58
|
describe('ARIA attributes', () => {
|
|
55
59
|
it('should provide `toggleId` to the base dropdown and reference it in`aria-labelledby` attribute of the list container`', async () => {
|
|
56
|
-
await buildWrapper();
|
|
60
|
+
await buildWrapper({ items: mockOptions });
|
|
57
61
|
expect(findBaseDropdown().props('toggleId')).toBe(
|
|
58
62
|
findListContainer().attributes('aria-labelledby')
|
|
59
63
|
);
|
|
60
64
|
});
|
|
65
|
+
|
|
66
|
+
it('should reference `listAriaLabelledby`', async () => {
|
|
67
|
+
const listAriaLabelledBy = 'first-label-id second-label-id';
|
|
68
|
+
await buildWrapper({ items: mockOptions, listAriaLabelledBy });
|
|
69
|
+
expect(findListContainer().attributes('aria-labelledby')).toBe(listAriaLabelledBy);
|
|
70
|
+
});
|
|
61
71
|
});
|
|
62
72
|
|
|
63
73
|
describe('selecting items', () => {
|
|
@@ -137,23 +147,36 @@ describe('GlListbox', () => {
|
|
|
137
147
|
});
|
|
138
148
|
|
|
139
149
|
describe('onShow', () => {
|
|
140
|
-
|
|
150
|
+
let focusSpy;
|
|
151
|
+
|
|
152
|
+
const showDropdown = async ({ searchable = false } = {}) => {
|
|
141
153
|
buildWrapper({
|
|
142
154
|
multiple: true,
|
|
143
155
|
items: mockOptions,
|
|
144
156
|
selected: [mockOptions[2].value, mockOptions[1].value],
|
|
157
|
+
searchable,
|
|
145
158
|
});
|
|
159
|
+
if (searchable) {
|
|
160
|
+
focusSpy = jest.spyOn(wrapper.vm.$refs.searchBox, 'focusInput');
|
|
161
|
+
}
|
|
146
162
|
findBaseDropdown().vm.$emit(GL_DROPDOWN_SHOWN);
|
|
147
163
|
await nextTick();
|
|
148
|
-
}
|
|
164
|
+
};
|
|
149
165
|
|
|
150
|
-
it('should re-emit the event', () => {
|
|
166
|
+
it('should re-emit the event', async () => {
|
|
167
|
+
await showDropdown();
|
|
151
168
|
expect(wrapper.emitted(GL_DROPDOWN_SHOWN)).toHaveLength(1);
|
|
152
169
|
});
|
|
153
170
|
|
|
154
|
-
it('should focus the first selected item', () => {
|
|
171
|
+
it('should focus the first selected item', async () => {
|
|
172
|
+
await showDropdown();
|
|
155
173
|
expect(findListboxItems().at(1).find(ITEM_SELECTOR).element).toHaveFocus();
|
|
156
174
|
});
|
|
175
|
+
|
|
176
|
+
it('should focus the search input when search is enabled', async () => {
|
|
177
|
+
await showDropdown({ searchable: true });
|
|
178
|
+
expect(focusSpy).toHaveBeenCalled();
|
|
179
|
+
});
|
|
157
180
|
});
|
|
158
181
|
|
|
159
182
|
describe('onHide', () => {
|
|
@@ -181,7 +204,7 @@ describe('GlListbox', () => {
|
|
|
181
204
|
thirdItem = findListItem(2);
|
|
182
205
|
});
|
|
183
206
|
|
|
184
|
-
it('should move the focus down the list of items on `
|
|
207
|
+
it('should move the focus down the list of items on `ARROW_DOWN` and stop on the last item', async () => {
|
|
185
208
|
expect(firstItem.element).toHaveFocus();
|
|
186
209
|
await firstItem.trigger('keydown', { code: ARROW_DOWN });
|
|
187
210
|
expect(secondItem.element).toHaveFocus();
|
|
@@ -191,7 +214,7 @@ describe('GlListbox', () => {
|
|
|
191
214
|
expect(thirdItem.element).toHaveFocus();
|
|
192
215
|
});
|
|
193
216
|
|
|
194
|
-
it('should move the focus up the list of items on `
|
|
217
|
+
it('should move the focus up the list of items on `ARROW_UP` and stop on the first item', async () => {
|
|
195
218
|
await firstItem.trigger('keydown', { code: ARROW_DOWN });
|
|
196
219
|
await secondItem.trigger('keydown', { code: ARROW_DOWN });
|
|
197
220
|
expect(thirdItem.element).toHaveFocus();
|
|
@@ -220,6 +243,23 @@ describe('GlListbox', () => {
|
|
|
220
243
|
await thirdItem.trigger('keydown', { code: HOME });
|
|
221
244
|
expect(firstItem.element).toHaveFocus();
|
|
222
245
|
});
|
|
246
|
+
|
|
247
|
+
describe('when `searchable` is enabled', () => {
|
|
248
|
+
it('should move focus to the first item on search input `ARROW_DOWN`', async () => {
|
|
249
|
+
buildWrapper({ items: mockOptions, searchable: true });
|
|
250
|
+
findBaseDropdown().vm.$emit(GL_DROPDOWN_SHOWN);
|
|
251
|
+
findSearchBox().trigger('keydown', { code: ARROW_DOWN });
|
|
252
|
+
expect(firstItem.element).toHaveFocus();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should move focus to the search input on first item `ARROW_UP', async () => {
|
|
256
|
+
buildWrapper({ items: mockOptions, searchable: true });
|
|
257
|
+
findBaseDropdown().vm.$emit(GL_DROPDOWN_SHOWN);
|
|
258
|
+
const focusSpy = jest.spyOn(wrapper.vm.$refs.searchBox, 'focusInput');
|
|
259
|
+
await firstItem.trigger('keydown', { code: ARROW_UP });
|
|
260
|
+
expect(focusSpy).toHaveBeenCalled();
|
|
261
|
+
});
|
|
262
|
+
});
|
|
223
263
|
});
|
|
224
264
|
|
|
225
265
|
describe('when the header slot content is provided', () => {
|
|
@@ -260,4 +300,58 @@ describe('GlListbox', () => {
|
|
|
260
300
|
});
|
|
261
301
|
});
|
|
262
302
|
});
|
|
303
|
+
|
|
304
|
+
describe('when `searchable` is enabled', () => {
|
|
305
|
+
it('should render the search box', () => {
|
|
306
|
+
buildWrapper({ items: mockOptions, searchable: true });
|
|
307
|
+
|
|
308
|
+
expect(findSearchBox().exists()).toBe(true);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should emit the search value when typing in the search box', async () => {
|
|
312
|
+
buildWrapper({ items: mockOptions, searchable: true });
|
|
313
|
+
|
|
314
|
+
const searchStr = 'search value';
|
|
315
|
+
findSearchBox().vm.$emit('input', searchStr);
|
|
316
|
+
await nextTick();
|
|
317
|
+
expect(wrapper.emitted('search')[0][0]).toEqual(searchStr);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should not render the loading icon and render the list if NOT searching', () => {
|
|
321
|
+
buildWrapper({ items: mockOptions, searchable: true });
|
|
322
|
+
|
|
323
|
+
expect(findLoadingIcon().exists()).toBe(false);
|
|
324
|
+
expect(findListContainer().exists()).toBe(true);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should render the loading icon and NOT render the list when searching', () => {
|
|
328
|
+
buildWrapper({ items: mockOptions, searchable: true, searching: true });
|
|
329
|
+
|
|
330
|
+
expect(findLoadingIcon().exists()).toBe(true);
|
|
331
|
+
expect(findListContainer().exists()).toBe(false);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should display `noResultText` if no items found', () => {
|
|
335
|
+
const noResultsText = 'Nothing found';
|
|
336
|
+
buildWrapper({ items: [], searchable: true, searching: false, noResultsText });
|
|
337
|
+
|
|
338
|
+
expect(findLoadingIcon().exists()).toBe(false);
|
|
339
|
+
expect(findListContainer().exists()).toBe(false);
|
|
340
|
+
expect(findNoResultsText().text()).toBe(noResultsText);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe('Screen reader text with number of search results', () => {
|
|
344
|
+
it('when the #search-summary-sr-only slot content is provided', () => {
|
|
345
|
+
const searchResultsContent = 'Found 5 results';
|
|
346
|
+
const slots = { 'search-summary-sr-only': searchResultsContent };
|
|
347
|
+
buildWrapper({ items: mockOptions, searchable: true, searching: false }, slots);
|
|
348
|
+
expect(findSRNumberOfResultsText().text()).toBe(searchResultsContent);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('should not display SR text when no matching results', () => {
|
|
352
|
+
buildWrapper({ items: [], searchable: true, searching: false });
|
|
353
|
+
expect(findSRNumberOfResultsText().exists()).toBe(false);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
});
|
|
263
357
|
});
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { makeContainer } from '../../../../utils/story_decorators/container';
|
|
16
16
|
import readme from './listbox.md';
|
|
17
17
|
import { mockOptions, mockGroups } from './mock_data';
|
|
18
|
+
import { flattenedOptions } from './utils';
|
|
18
19
|
|
|
19
20
|
const defaultValue = (prop) => GlListbox.props[prop].default;
|
|
20
21
|
|
|
@@ -25,6 +26,9 @@ const generateProps = ({
|
|
|
25
26
|
size = defaultValue('size'),
|
|
26
27
|
disabled = defaultValue('disabled'),
|
|
27
28
|
loading = defaultValue('loading'),
|
|
29
|
+
searchable = defaultValue('searchable'),
|
|
30
|
+
searching = defaultValue('searching'),
|
|
31
|
+
noResultsText = defaultValue('noResultsText'),
|
|
28
32
|
noCaret = defaultValue('noCaret'),
|
|
29
33
|
right = defaultValue('right'),
|
|
30
34
|
toggleText,
|
|
@@ -32,7 +36,8 @@ const generateProps = ({
|
|
|
32
36
|
icon = '',
|
|
33
37
|
multiple = defaultValue('multiple'),
|
|
34
38
|
isCheckCentered = defaultValue('isCheckCentered'),
|
|
35
|
-
|
|
39
|
+
toggleAriaLabelledBy,
|
|
40
|
+
listAriaLabelledBy,
|
|
36
41
|
startOpened = true,
|
|
37
42
|
} = {}) => ({
|
|
38
43
|
items,
|
|
@@ -41,6 +46,9 @@ const generateProps = ({
|
|
|
41
46
|
size,
|
|
42
47
|
disabled,
|
|
43
48
|
loading,
|
|
49
|
+
searchable,
|
|
50
|
+
searching,
|
|
51
|
+
noResultsText,
|
|
44
52
|
noCaret,
|
|
45
53
|
right,
|
|
46
54
|
toggleText,
|
|
@@ -48,12 +56,15 @@ const generateProps = ({
|
|
|
48
56
|
icon,
|
|
49
57
|
multiple,
|
|
50
58
|
isCheckCentered,
|
|
51
|
-
|
|
59
|
+
toggleAriaLabelledBy,
|
|
60
|
+
listAriaLabelledBy,
|
|
52
61
|
startOpened,
|
|
53
62
|
});
|
|
54
63
|
|
|
55
64
|
function openListbox(component) {
|
|
56
|
-
component.$nextTick(() =>
|
|
65
|
+
component.$nextTick(() => {
|
|
66
|
+
component.$refs.listbox.open();
|
|
67
|
+
});
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
const template = (content, label = '') => `
|
|
@@ -61,6 +72,7 @@ const template = (content, label = '') => `
|
|
|
61
72
|
${label}
|
|
62
73
|
<br/>
|
|
63
74
|
<gl-listbox
|
|
75
|
+
ref="listbox"
|
|
64
76
|
v-model="selected"
|
|
65
77
|
:items="items"
|
|
66
78
|
:category="category"
|
|
@@ -68,6 +80,9 @@ const template = (content, label = '') => `
|
|
|
68
80
|
:size="size"
|
|
69
81
|
:disabled="disabled"
|
|
70
82
|
:loading="loading"
|
|
83
|
+
:searchable="searchable"
|
|
84
|
+
:searching="searching"
|
|
85
|
+
:no-results-text="noResultsText"
|
|
71
86
|
:no-caret="noCaret"
|
|
72
87
|
:right="right"
|
|
73
88
|
:toggle-text="toggleText"
|
|
@@ -75,7 +90,8 @@ const template = (content, label = '') => `
|
|
|
75
90
|
:icon="icon"
|
|
76
91
|
:multiple="multiple"
|
|
77
92
|
:is-check-centered="isCheckCentered"
|
|
78
|
-
:aria-
|
|
93
|
+
:toggle-aria-labelled-by="toggleAriaLabelledBy"
|
|
94
|
+
:list-aria-labelled-by="listAriaLabelledBy"
|
|
79
95
|
>
|
|
80
96
|
${content}
|
|
81
97
|
</gl-listbox>
|
|
@@ -99,7 +115,7 @@ export const Default = (args, { argTypes }) => ({
|
|
|
99
115
|
},
|
|
100
116
|
template: template('', `<span class="gl-my-0" id="listbox-label">Select a department</span>`),
|
|
101
117
|
});
|
|
102
|
-
Default.args = generateProps({
|
|
118
|
+
Default.args = generateProps({ toggleAriaLabelledBy: 'listbox-label' });
|
|
103
119
|
Default.decorators = [makeContainer({ height: '370px' })];
|
|
104
120
|
|
|
105
121
|
export const HeaderAndFooter = (args, { argTypes }) => ({
|
|
@@ -125,9 +141,9 @@ export const HeaderAndFooter = (args, { argTypes }) => ({
|
|
|
125
141
|
this.selected.push(mockOptions[index].value);
|
|
126
142
|
},
|
|
127
143
|
},
|
|
128
|
-
template: template(
|
|
129
|
-
|
|
130
|
-
|
|
144
|
+
template: template(
|
|
145
|
+
`<template #header>
|
|
146
|
+
<p class="gl-font-weight-bold gl-font-sm gl-m-0 gl-text-center gl-py-2 gl-border-1 gl-border-b-solid gl-border-gray-200">Assign to department</p>
|
|
131
147
|
</template>
|
|
132
148
|
<template #footer>
|
|
133
149
|
<div class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-100 gl-display-flex gl-justify-content-center gl-p-3">
|
|
@@ -138,7 +154,8 @@ export const HeaderAndFooter = (args, { argTypes }) => ({
|
|
|
138
154
|
</gl-button-group>
|
|
139
155
|
</div>
|
|
140
156
|
</template>
|
|
141
|
-
`
|
|
157
|
+
`
|
|
158
|
+
),
|
|
142
159
|
});
|
|
143
160
|
HeaderAndFooter.args = generateProps({
|
|
144
161
|
toggleText: 'Header and Footer',
|
|
@@ -172,6 +189,7 @@ export const CustomListItem = (args, { argTypes }) => ({
|
|
|
172
189
|
},
|
|
173
190
|
template: `
|
|
174
191
|
<gl-listbox
|
|
192
|
+
ref="listbox"
|
|
175
193
|
v-model="selected"
|
|
176
194
|
:items="items"
|
|
177
195
|
:category="category"
|
|
@@ -179,6 +197,9 @@ export const CustomListItem = (args, { argTypes }) => ({
|
|
|
179
197
|
:size="size"
|
|
180
198
|
:disabled="disabled"
|
|
181
199
|
:loading="loading"
|
|
200
|
+
:searchable="searchable"
|
|
201
|
+
:searching="searching"
|
|
202
|
+
:no-results-text="noResultsText"
|
|
182
203
|
:no-caret="noCaret"
|
|
183
204
|
:right="right"
|
|
184
205
|
:toggle-text="headerText"
|
|
@@ -186,24 +207,30 @@ export const CustomListItem = (args, { argTypes }) => ({
|
|
|
186
207
|
:icon="icon"
|
|
187
208
|
:multiple="multiple"
|
|
188
209
|
:is-check-centered="isCheckCentered"
|
|
189
|
-
:aria-
|
|
210
|
+
:toggle-aria-labelled-by="toggleAriaLabelledBy"
|
|
211
|
+
:list-aria-labelled-by="listAriaLabelledBy"
|
|
190
212
|
>
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
213
|
+
<template #list-item="{ item }">
|
|
214
|
+
<span class="gl-display-flex gl-align-items-center">
|
|
215
|
+
<gl-avatar :size="32" class-="gl-mr-3"/>
|
|
216
|
+
<span class="gl-display-flex gl-flex-direction-column">
|
|
217
|
+
<span class="gl-font-weight-bold gl-white-space-nowrap">{{ item.text }}</span>
|
|
218
|
+
<span class="gl-text-gray-400"> {{ item.secondaryText }}</span>
|
|
219
|
+
</span>
|
|
220
|
+
</span>
|
|
221
|
+
</template>
|
|
200
222
|
</gl-listbox>
|
|
201
223
|
`,
|
|
202
224
|
});
|
|
203
225
|
|
|
204
226
|
CustomListItem.args = generateProps({
|
|
205
227
|
items: [
|
|
206
|
-
{
|
|
228
|
+
{
|
|
229
|
+
value: 'mikegreiling',
|
|
230
|
+
text: 'Mike Greiling',
|
|
231
|
+
secondaryText: '@mikegreiling',
|
|
232
|
+
icon: 'foo',
|
|
233
|
+
},
|
|
207
234
|
{ value: 'ohoral', text: 'Olena Horal-Koretska', secondaryText: '@ohoral', icon: 'bar' },
|
|
208
235
|
{ value: 'markian', text: 'Mark Florian', secondaryText: '@markian', icon: 'bin' },
|
|
209
236
|
],
|
|
@@ -283,3 +310,200 @@ export default {
|
|
|
283
310
|
},
|
|
284
311
|
},
|
|
285
312
|
};
|
|
313
|
+
|
|
314
|
+
export const Searchable = (args, { argTypes }) => ({
|
|
315
|
+
props: Object.keys(argTypes),
|
|
316
|
+
components: {
|
|
317
|
+
GlListbox,
|
|
318
|
+
},
|
|
319
|
+
data() {
|
|
320
|
+
return {
|
|
321
|
+
selected: mockOptions[1].value,
|
|
322
|
+
filteredItems: mockOptions,
|
|
323
|
+
searchInProgress: false,
|
|
324
|
+
timeoutId: null,
|
|
325
|
+
headerId: 'listbox-header',
|
|
326
|
+
};
|
|
327
|
+
},
|
|
328
|
+
mounted() {
|
|
329
|
+
if (this.startOpened) {
|
|
330
|
+
openListbox(this);
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
methods: {
|
|
334
|
+
filterList(searchTerm) {
|
|
335
|
+
if (this.timeoutId) {
|
|
336
|
+
clearTimeout(this.timeoutId);
|
|
337
|
+
}
|
|
338
|
+
this.searchInProgress = true;
|
|
339
|
+
|
|
340
|
+
// eslint-disable-next-line no-restricted-globals
|
|
341
|
+
this.timeoutId = setTimeout(() => {
|
|
342
|
+
this.filteredItems = this.items.filter(({ text }) =>
|
|
343
|
+
text.toLowerCase().includes(searchTerm.toLowerCase())
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
this.searchInProgress = false;
|
|
347
|
+
}, 2000);
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
computed: {
|
|
351
|
+
customToggleText() {
|
|
352
|
+
let toggleText = 'Search for department';
|
|
353
|
+
const selectedValues = Array.isArray(this.selected) ? this.selected : [this.selected];
|
|
354
|
+
|
|
355
|
+
if (selectedValues.length === 1) {
|
|
356
|
+
toggleText = this.items.find(({ value }) => value === selectedValues[0]).text;
|
|
357
|
+
} else {
|
|
358
|
+
toggleText = `Selected ${selectedValues.length} departments`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return toggleText;
|
|
362
|
+
},
|
|
363
|
+
numberOfSearchResults() {
|
|
364
|
+
return this.filteredItems.length === 1 ? '1 result' : `${this.filteredItems.length} results`;
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
template: `
|
|
368
|
+
<gl-listbox
|
|
369
|
+
ref="listbox"
|
|
370
|
+
v-model="selected"
|
|
371
|
+
:items="filteredItems"
|
|
372
|
+
:category="category"
|
|
373
|
+
:variant="variant"
|
|
374
|
+
:size="size"
|
|
375
|
+
:disabled="disabled"
|
|
376
|
+
:loading="loading"
|
|
377
|
+
:no-caret="noCaret"
|
|
378
|
+
:right="right"
|
|
379
|
+
:toggle-text="customToggleText"
|
|
380
|
+
:text-sr-only="textSrOnly"
|
|
381
|
+
:icon="icon"
|
|
382
|
+
:multiple="multiple"
|
|
383
|
+
:is-check-centered="isCheckCentered"
|
|
384
|
+
:toggle-aria-labelled-by="toggleAriaLabelledBy"
|
|
385
|
+
:list-aria-labelled-by="headerId"
|
|
386
|
+
:searchable="searchable"
|
|
387
|
+
:searching="searchInProgress"
|
|
388
|
+
:no-results-text="noResultsText"
|
|
389
|
+
@search="filterList"
|
|
390
|
+
>
|
|
391
|
+
<template #header>
|
|
392
|
+
<p :id="headerId"
|
|
393
|
+
class="gl-font-weight-bold gl-font-sm gl-m-0 gl-text-center gl-py-2 gl-border-1 gl-border-b-solid gl-border-gray-200">
|
|
394
|
+
Assign to department</p>
|
|
395
|
+
</template>
|
|
396
|
+
<template #search-summary-sr-only>
|
|
397
|
+
{{ numberOfSearchResults }}
|
|
398
|
+
</template>
|
|
399
|
+
</gl-listbox>
|
|
400
|
+
`,
|
|
401
|
+
});
|
|
402
|
+
Searchable.args = generateProps({ searchable: true });
|
|
403
|
+
Searchable.decorators = [makeContainer({ height: '370px' })];
|
|
404
|
+
|
|
405
|
+
export const SearchableGroups = (args, { argTypes }) => ({
|
|
406
|
+
props: Object.keys(argTypes),
|
|
407
|
+
components: {
|
|
408
|
+
GlListbox,
|
|
409
|
+
},
|
|
410
|
+
data() {
|
|
411
|
+
return {
|
|
412
|
+
selected: mockGroups[1].options[0].value,
|
|
413
|
+
filteredGroupOptions: mockGroups,
|
|
414
|
+
searchInProgress: false,
|
|
415
|
+
timeoutId: null,
|
|
416
|
+
headerId: 'listbox-header',
|
|
417
|
+
};
|
|
418
|
+
},
|
|
419
|
+
mounted() {
|
|
420
|
+
if (this.startOpened) {
|
|
421
|
+
openListbox(this);
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
computed: {
|
|
425
|
+
flattenedOptions() {
|
|
426
|
+
return flattenedOptions(this.items);
|
|
427
|
+
},
|
|
428
|
+
flattenedFilteredOptions() {
|
|
429
|
+
return flattenedOptions(this.filteredGroupOptions);
|
|
430
|
+
},
|
|
431
|
+
customToggleText() {
|
|
432
|
+
let toggleText = 'Search for department';
|
|
433
|
+
const selectedValues = Array.isArray(this.selected) ? this.selected : [this.selected];
|
|
434
|
+
|
|
435
|
+
if (selectedValues.length === 1) {
|
|
436
|
+
toggleText = this.flattenedOptions.find(({ value }) => value === selectedValues[0]).text;
|
|
437
|
+
} else {
|
|
438
|
+
toggleText = `Selected ${selectedValues.length} departments`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return toggleText;
|
|
442
|
+
},
|
|
443
|
+
numberOfSearchResults() {
|
|
444
|
+
return this.flattenedFilteredOptions.length === 1
|
|
445
|
+
? '1 result'
|
|
446
|
+
: `${this.flattenedFilteredOptions.length} results`;
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
methods: {
|
|
450
|
+
filterList(searchTerm) {
|
|
451
|
+
if (this.timeoutId) {
|
|
452
|
+
clearTimeout(this.timeoutId);
|
|
453
|
+
}
|
|
454
|
+
this.searchInProgress = true;
|
|
455
|
+
|
|
456
|
+
// eslint-disable-next-line no-restricted-globals
|
|
457
|
+
this.timeoutId = setTimeout(() => {
|
|
458
|
+
this.filteredGroupOptions = this.items
|
|
459
|
+
.map(({ text, options }) => {
|
|
460
|
+
return {
|
|
461
|
+
text,
|
|
462
|
+
options: options.filter((option) =>
|
|
463
|
+
option.text.toLowerCase().includes(searchTerm.toLowerCase())
|
|
464
|
+
),
|
|
465
|
+
};
|
|
466
|
+
})
|
|
467
|
+
.filter(({ options }) => options.length);
|
|
468
|
+
|
|
469
|
+
this.searchInProgress = false;
|
|
470
|
+
}, 2000);
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
template: `
|
|
474
|
+
<gl-listbox
|
|
475
|
+
ref="listbox"
|
|
476
|
+
v-model="selected"
|
|
477
|
+
:items="filteredGroupOptions"
|
|
478
|
+
:category="category"
|
|
479
|
+
:variant="variant"
|
|
480
|
+
:size="size"
|
|
481
|
+
:disabled="disabled"
|
|
482
|
+
:loading="loading"
|
|
483
|
+
:no-caret="noCaret"
|
|
484
|
+
:right="right"
|
|
485
|
+
:toggle-text="customToggleText"
|
|
486
|
+
:text-sr-only="textSrOnly"
|
|
487
|
+
:icon="icon"
|
|
488
|
+
:multiple="multiple"
|
|
489
|
+
:is-check-centered="isCheckCentered"
|
|
490
|
+
:toggle-aria-labelled-by="toggleAriaLabelledBy"
|
|
491
|
+
:list-aria-labelled-by="headerId"
|
|
492
|
+
:searching="searchInProgress"
|
|
493
|
+
:no-results-text="noResultsText"
|
|
494
|
+
:searchable="searchable"
|
|
495
|
+
@search="filterList"
|
|
496
|
+
>
|
|
497
|
+
<template #header>
|
|
498
|
+
<p :id="headerId"
|
|
499
|
+
class="gl-font-weight-bold gl-font-sm gl-m-0 gl-text-center gl-py-2 gl-border-1 gl-border-b-solid gl-border-gray-200">
|
|
500
|
+
Assign to department</p>
|
|
501
|
+
</template>
|
|
502
|
+
<template #search-summary-sr-only>
|
|
503
|
+
{{ numberOfSearchResults }}
|
|
504
|
+
</template>
|
|
505
|
+
</gl-listbox>
|
|
506
|
+
`,
|
|
507
|
+
});
|
|
508
|
+
SearchableGroups.args = generateProps({ searchable: true, items: mockGroups });
|
|
509
|
+
SearchableGroups.decorators = [makeContainer({ height: '370px' })];
|