@gitlab/ui 64.9.0 → 64.10.1

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 CHANGED
@@ -1,3 +1,18 @@
1
+ ## [64.10.1](https://gitlab.com/gitlab-org/gitlab-ui/compare/v64.10.0...v64.10.1) (2023-06-14)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **GlCollapsibleListbox:** listbox stays open ([a83355d](https://gitlab.com/gitlab-org/gitlab-ui/commit/a83355d527c483ca6bd6587da64f565ef118b8da))
7
+
8
+ # [64.10.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v64.9.0...v64.10.0) (2023-06-14)
9
+
10
+
11
+ ### Features
12
+
13
+ * **DisclosureDropdown:** borderPosition property ([ea7822e](https://gitlab.com/gitlab-org/gitlab-ui/commit/ea7822e20992c6a0fc074f8ab3766b3145c09c00))
14
+ * **GlKeysetPagination:** add navigation landmark ([3ffd2f7](https://gitlab.com/gitlab-org/gitlab-ui/commit/3ffd2f79b3029504a6e1b831df23904ca8473d06))
15
+
1
16
  # [64.9.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v64.8.0...v64.9.0) (2023-06-14)
2
17
 
3
18
 
@@ -10,6 +10,7 @@ var script = {
10
10
  GlButton,
11
11
  GlIcon
12
12
  },
13
+ inheritAttrs: false,
13
14
  props: {
14
15
  // The following 4 properties match the default names of the
15
16
  // [PageInfo](https://docs.gitlab.com/ee/api/graphql/reference/index.html#pageinfo)
@@ -68,6 +69,15 @@ var script = {
68
69
  required: false,
69
70
  default: null
70
71
  },
72
+ /**
73
+ * The aria-label that needs to be set for the
74
+ * pagination landmark region.
75
+ */
76
+ navigationLabel: {
77
+ type: String,
78
+ required: false,
79
+ default: 'Pagination'
80
+ },
71
81
  /**
72
82
  * The text that will be rendered inside the "Next" button.
73
83
  * It\'s important to provide this parameter since the default text is not translatable.
@@ -101,7 +111,7 @@ var script = {
101
111
  const __vue_script__ = script;
102
112
 
103
113
  /* template */
104
- var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('gl-button-group',_vm._g(_vm._b({staticClass:"gl-keyset-pagination"},'gl-button-group',_vm.$attrs,false),_vm.$listeners),[_c('gl-button',{attrs:{"href":_vm.prevButtonLink,"disabled":_vm.disabled || !_vm.hasPreviousPage,"data-testid":"prevButton"},on:{"click":function($event){return _vm.$emit('prev', _vm.startCursor)}}},[_vm._t("previous-button-content",function(){return [_c('div',{staticClass:"gl-display-flex gl-align-center"},[_c('gl-icon',{attrs:{"name":"chevron-left"}}),_vm._v("\n "+_vm._s(_vm.prevText)+"\n ")],1)]})],2),_vm._v(" "),_c('gl-button',{attrs:{"href":_vm.nextButtonLink,"disabled":_vm.disabled || !_vm.hasNextPage,"data-testid":"nextButton"},on:{"click":function($event){return _vm.$emit('next', _vm.endCursor)}}},[_vm._t("next-button-content",function(){return [_c('div',{staticClass:"gl-display-flex gl-align-center"},[_vm._v("\n "+_vm._s(_vm.nextText)+"\n "),_c('gl-icon',{attrs:{"name":"chevron-right"}})],1)]})],2)],1)};
114
+ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('nav',{staticClass:"gl-pagination",attrs:{"aria-label":_vm.navigationLabel}},[_c('gl-button-group',_vm._g(_vm._b({staticClass:"gl-keyset-pagination"},'gl-button-group',_vm.$attrs,false),_vm.$listeners),[_c('gl-button',{attrs:{"href":_vm.prevButtonLink,"disabled":_vm.disabled || !_vm.hasPreviousPage,"data-testid":"prevButton"},on:{"click":function($event){return _vm.$emit('prev', _vm.startCursor)}}},[_vm._t("previous-button-content",function(){return [_c('div',{staticClass:"gl-display-flex gl-align-center"},[_c('gl-icon',{attrs:{"name":"chevron-left"}}),_vm._v("\n "+_vm._s(_vm.prevText)+"\n ")],1)]})],2),_vm._v(" "),_c('gl-button',{attrs:{"href":_vm.nextButtonLink,"disabled":_vm.disabled || !_vm.hasNextPage,"data-testid":"nextButton"},on:{"click":function($event){return _vm.$emit('next', _vm.endCursor)}}},[_vm._t("next-button-content",function(){return [_c('div',{staticClass:"gl-display-flex gl-align-center"},[_vm._v("\n "+_vm._s(_vm.nextText)+"\n "),_c('gl-icon',{attrs:{"name":"chevron-right"}})],1)]})],2)],1)],1)};
105
115
  var __vue_staticRenderFns__ = [];
106
116
 
107
117
  /* style */
@@ -1,4 +1,8 @@
1
1
  const DISCLOSURE_DROPDOWN_ITEM_NAME = 'GlDisclosureDropdownItem';
2
2
  const DISCLOSURE_DROPDOWN_GROUP_NAME = 'GlDisclosureDropdownGroup';
3
+ const DISCLOSURE_DROPDOWN_GROUP_BORDER_POSITIONS = {
4
+ top: 'top',
5
+ bottom: 'bottom'
6
+ };
3
7
 
4
- export { DISCLOSURE_DROPDOWN_GROUP_NAME, DISCLOSURE_DROPDOWN_ITEM_NAME };
8
+ export { DISCLOSURE_DROPDOWN_GROUP_BORDER_POSITIONS, DISCLOSURE_DROPDOWN_GROUP_NAME, DISCLOSURE_DROPDOWN_ITEM_NAME };
@@ -1,10 +1,13 @@
1
1
  import uniqueId from 'lodash/uniqueId';
2
2
  import GlDisclosureDropdownItem from './disclosure_dropdown_item';
3
3
  import { isGroup } from './utils';
4
- import { DISCLOSURE_DROPDOWN_GROUP_NAME } from './constants';
4
+ import { DISCLOSURE_DROPDOWN_GROUP_BORDER_POSITIONS, DISCLOSURE_DROPDOWN_GROUP_NAME } from './constants';
5
5
  import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
6
6
 
7
- const GROUP_TOP_BORDER_CLASSES = 'gl-border-t gl-pt-2 gl-mt-2';
7
+ const BORDER_CLASSES = {
8
+ [DISCLOSURE_DROPDOWN_GROUP_BORDER_POSITIONS.top]: 'gl-border-t gl-pt-2 gl-mt-2',
9
+ [DISCLOSURE_DROPDOWN_GROUP_BORDER_POSITIONS.bottom]: 'gl-border-b gl-pb-2 gl-mb-2'
10
+ };
8
11
  var script = {
9
12
  name: DISCLOSURE_DROPDOWN_GROUP_NAME,
10
13
  components: {
@@ -22,17 +25,29 @@ var script = {
22
25
  },
23
26
  /**
24
27
  * If 'true', will set top border for the group
25
- * to separate from other groups
28
+ * to separate from other groups. You can control
29
+ * the border position using the `borderPosition`
30
+ * property.
26
31
  */
27
32
  bordered: {
28
33
  type: Boolean,
29
34
  required: false,
30
35
  default: false
36
+ },
37
+ /**
38
+ * Controls the position of the group's border. Valid
39
+ * values are 'top' and 'bottom'.
40
+ */
41
+ borderPosition: {
42
+ type: String,
43
+ required: false,
44
+ default: DISCLOSURE_DROPDOWN_GROUP_BORDER_POSITIONS.top,
45
+ validator: value => Object.keys(DISCLOSURE_DROPDOWN_GROUP_BORDER_POSITIONS).includes(value)
31
46
  }
32
47
  },
33
48
  computed: {
34
49
  borderClass() {
35
- return this.bordered ? GROUP_TOP_BORDER_CLASSES : null;
50
+ return this.bordered ? BORDER_CLASSES[this.borderPosition] : null;
36
51
  },
37
52
  showHeader() {
38
53
  var _this$group;
@@ -92,4 +107,4 @@ var __vue_staticRenderFns__ = [];
92
107
  );
93
108
 
94
109
  export default __vue_component__;
95
- export { GROUP_TOP_BORDER_CLASSES };
110
+ export { BORDER_CLASSES };
@@ -329,7 +329,7 @@ var script = {
329
329
  },
330
330
  computed: {
331
331
  listboxTag() {
332
- if (this.items.length === 0 || isOption(this.items[0])) return 'ul';
332
+ if (!this.hasItems || isOption(this.items[0])) return 'ul';
333
333
  return 'div';
334
334
  },
335
335
  listboxClasses() {
@@ -345,6 +345,9 @@ var script = {
345
345
  flattenedOptions() {
346
346
  return flattenedOptions(this.items);
347
347
  },
348
+ hasItems() {
349
+ return this.items.length > 0;
350
+ },
348
351
  listboxToggleText() {
349
352
  if (!this.toggleText) {
350
353
  if (!this.multiple && this.selectedValues.length) {
@@ -384,6 +387,17 @@ var script = {
384
387
  if (!this.resetButtonLabel) {
385
388
  return false;
386
389
  }
390
+ if (!this.multiple) {
391
+ return false;
392
+ }
393
+
394
+ /**
395
+ * if dropdown has no items
396
+ * reset all should be hidden
397
+ */
398
+ if (!this.hasItems) {
399
+ return false;
400
+ }
387
401
  if (this.multiple) {
388
402
  return this.selected.length > 0;
389
403
  }
@@ -396,6 +410,14 @@ var script = {
396
410
  if (!this.multiple) {
397
411
  return false;
398
412
  }
413
+
414
+ /**
415
+ * if dropdown has no items
416
+ * select all should be hidden
417
+ */
418
+ if (!this.hasItems) {
419
+ return false;
420
+ }
399
421
  return this.selected.length === 0;
400
422
  },
401
423
  showIntersectionObserver() {
@@ -614,7 +636,6 @@ var script = {
614
636
  * @event reset
615
637
  */
616
638
  this.$emit('reset');
617
- this.closeAndFocus();
618
639
  },
619
640
  onSelectAllButtonClicked() {
620
641
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "64.9.0",
3
+ "version": "64.10.1",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -10,6 +10,7 @@ export default {
10
10
  GlButton,
11
11
  GlIcon,
12
12
  },
13
+ inheritAttrs: false,
13
14
  props: {
14
15
  // The following 4 properties match the default names of the
15
16
  // [PageInfo](https://docs.gitlab.com/ee/api/graphql/reference/index.html#pageinfo)
@@ -68,6 +69,15 @@ export default {
68
69
  required: false,
69
70
  default: null,
70
71
  },
72
+ /**
73
+ * The aria-label that needs to be set for the
74
+ * pagination landmark region.
75
+ */
76
+ navigationLabel: {
77
+ type: String,
78
+ required: false,
79
+ default: 'Pagination',
80
+ },
71
81
  /**
72
82
  * The text that will be rendered inside the "Next" button.
73
83
  * It\'s important to provide this parameter since the default text is not translatable.
@@ -99,34 +109,36 @@ export default {
99
109
  </script>
100
110
 
101
111
  <template>
102
- <gl-button-group class="gl-keyset-pagination" v-bind="$attrs" v-on="$listeners">
103
- <gl-button
104
- :href="prevButtonLink"
105
- :disabled="disabled || !hasPreviousPage"
106
- data-testid="prevButton"
107
- @click="$emit('prev', startCursor)"
108
- >
109
- <!-- @slot Used to customize the appearance of the "Prev" button -->
110
- <slot name="previous-button-content">
111
- <div class="gl-display-flex gl-align-center">
112
- <gl-icon name="chevron-left" />
113
- {{ prevText }}
114
- </div>
115
- </slot>
116
- </gl-button>
117
- <gl-button
118
- :href="nextButtonLink"
119
- :disabled="disabled || !hasNextPage"
120
- data-testid="nextButton"
121
- @click="$emit('next', endCursor)"
122
- >
123
- <!-- @slot Used to customize the appearance of the "Next" button -->
124
- <slot name="next-button-content">
125
- <div class="gl-display-flex gl-align-center">
126
- {{ nextText }}
127
- <gl-icon name="chevron-right" />
128
- </div>
129
- </slot>
130
- </gl-button>
131
- </gl-button-group>
112
+ <nav class="gl-pagination" :aria-label="navigationLabel">
113
+ <gl-button-group class="gl-keyset-pagination" v-bind="$attrs" v-on="$listeners">
114
+ <gl-button
115
+ :href="prevButtonLink"
116
+ :disabled="disabled || !hasPreviousPage"
117
+ data-testid="prevButton"
118
+ @click="$emit('prev', startCursor)"
119
+ >
120
+ <!-- @slot Used to customize the appearance of the "Prev" button -->
121
+ <slot name="previous-button-content">
122
+ <div class="gl-display-flex gl-align-center">
123
+ <gl-icon name="chevron-left" />
124
+ {{ prevText }}
125
+ </div>
126
+ </slot>
127
+ </gl-button>
128
+ <gl-button
129
+ :href="nextButtonLink"
130
+ :disabled="disabled || !hasNextPage"
131
+ data-testid="nextButton"
132
+ @click="$emit('next', endCursor)"
133
+ >
134
+ <!-- @slot Used to customize the appearance of the "Next" button -->
135
+ <slot name="next-button-content">
136
+ <div class="gl-display-flex gl-align-center">
137
+ {{ nextText }}
138
+ <gl-icon name="chevron-right" />
139
+ </div>
140
+ </slot>
141
+ </gl-button>
142
+ </gl-button-group>
143
+ </nav>
132
144
  </template>
@@ -1,2 +1,6 @@
1
1
  export const DISCLOSURE_DROPDOWN_ITEM_NAME = 'GlDisclosureDropdownItem';
2
2
  export const DISCLOSURE_DROPDOWN_GROUP_NAME = 'GlDisclosureDropdownGroup';
3
+ export const DISCLOSURE_DROPDOWN_GROUP_BORDER_POSITIONS = {
4
+ top: 'top',
5
+ bottom: 'bottom',
6
+ };
@@ -91,7 +91,7 @@ template. If you want to render a custom template for items, use the
91
91
  <template #list-item="{ item }">
92
92
  <span class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
93
93
  {{item.text}}
94
- <gl-icon v-if="item.icon" :name="item.icon"/>
94
+ <gl-icon v-if="item.icon" :name="item.icon" />
95
95
  </span>
96
96
  </template>
97
97
  </gl-disclosure-dropdown>
@@ -117,6 +117,16 @@ To render custom group labels, use the `group-label` scoped slot:
117
117
  </gl-disclosure-dropdown>
118
118
  ```
119
119
 
120
+ To draw a horizontal line that separates two groups, set the `bordered` property.
121
+ By default, the border appears above the group. You can change the border position
122
+ using the `border-position` property:
123
+
124
+ ```html
125
+ <gl-disclosure-dropdown>
126
+ <gl-disclosure-dropdown-group bordered border-position="bottom" :group="group" />
127
+ </gl-disclosure-dropdown>
128
+ ```
129
+
120
130
  #### Miscellaneous content
121
131
 
122
132
  Besides default components, disclosure dropdown can render miscellaneous content inside it.
@@ -125,7 +135,7 @@ In this case the user is responsible for handling all events and navigation insi
125
135
  #### Dealing with long option texts
126
136
 
127
137
  - Some options might have long non-wrapping text that would overflow the dropdown maximum width. In
128
- such cases, it's recommended to override the `#list-item` slot and to truncate the option text using
129
- `GlTruncate`.
138
+ such cases, it's recommended to override the `#list-item` slot and to truncate the option text using
139
+ `GlTruncate`.
130
140
  - If the toggle text reflects the selected option text, it might be necessary to truncate
131
- it too by overriding the `#toggle` slot.
141
+ it too by overriding the `#toggle` slot.
@@ -14,6 +14,7 @@ import { makeContainer } from '../../../../utils/story_decorators/container';
14
14
  import GlDisclosureDropdown from './disclosure_dropdown.vue';
15
15
  import GlDisclosureDropdownItem from './disclosure_dropdown_item.vue';
16
16
  import GlDisclosureDropdownGroup from './disclosure_dropdown_group.vue';
17
+ import { DISCLOSURE_DROPDOWN_GROUP_BORDER_POSITIONS } from './constants';
17
18
  import readme from './disclosure_dropdown.md';
18
19
  import {
19
20
  mockItems,
@@ -229,7 +230,7 @@ export const CustomGroupsItemsAndToggle = makeGroupedExample({
229
230
  </template>
230
231
  </gl-disclosure-dropdown-item>
231
232
  </gl-disclosure-dropdown-group>
232
- <gl-disclosure-dropdown-group bordered :group="$options.groups[0]" @action="closeDropdown">
233
+ <gl-disclosure-dropdown-group :bordered="bordered" :border-position="borderPosition" :group="$options.groups[0]" @action="closeDropdown">
233
234
  <template #list-item="{ item }">
234
235
  <span class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
235
236
  {{item.text}}
@@ -237,7 +238,7 @@ export const CustomGroupsItemsAndToggle = makeGroupedExample({
237
238
  </span>
238
239
  </template>
239
240
  </gl-disclosure-dropdown-group>
240
- <gl-disclosure-dropdown-group bordered>
241
+ <gl-disclosure-dropdown-group :bordered="bordered" :border-position="borderPosition">
241
242
  <template #group-label>
242
243
  <span class="gl-font-sm">Navigation redesign</span>
243
244
  <gl-badge size="sm" variant="info">Beta</gl-badge>
@@ -253,7 +254,7 @@ export const CustomGroupsItemsAndToggle = makeGroupedExample({
253
254
  <template #list-item>Provide feedback</template>
254
255
  </gl-disclosure-dropdown-item>
255
256
  </gl-disclosure-dropdown-group>
256
- <gl-disclosure-dropdown-group bordered :group="$options.groups[1]" @action="closeDropdown"/>
257
+ <gl-disclosure-dropdown-group :bordered="bordered" :border-position="borderPosition" :group="$options.groups[1]" @action="closeDropdown"/>
257
258
  `,
258
259
  {
259
260
  after: `
@@ -292,7 +293,18 @@ CustomGroupsItemsAndToggle.args = {
292
293
  toggleText: 'User profile menu',
293
294
  textSrOnly: true,
294
295
  autoClose: false,
296
+ bordered: true,
297
+ borderPosition: DISCLOSURE_DROPDOWN_GROUP_BORDER_POSITIONS.top,
295
298
  };
299
+ CustomGroupsItemsAndToggle.argTypes = {
300
+ borderPosition: {
301
+ options: DISCLOSURE_DROPDOWN_GROUP_BORDER_POSITIONS,
302
+ control: {
303
+ type: 'select',
304
+ },
305
+ },
306
+ };
307
+
296
308
  CustomGroupsItemsAndToggle.decorators = [makeContainer({ height: '400px' })];
297
309
 
298
310
  export const MiscellaneousContent = (args, { argTypes }) => ({
@@ -334,26 +346,26 @@ export default {
334
346
  category: {
335
347
  control: {
336
348
  type: 'select',
337
- options: buttonCategoryOptions,
338
349
  },
350
+ options: buttonCategoryOptions,
339
351
  },
340
352
  variant: {
341
353
  control: {
342
354
  type: 'select',
343
- options: buttonVariantOptions,
344
355
  },
356
+ options: buttonVariantOptions,
345
357
  },
346
358
  size: {
347
359
  control: {
348
360
  type: 'select',
349
- options: Object.keys(buttonSizeOptions),
350
361
  },
362
+ options: Object.keys(buttonSizeOptions),
351
363
  },
352
364
  placement: {
353
365
  control: {
354
366
  type: 'select',
355
- options: Object.keys(dropdownPlacements),
356
367
  },
368
+ options: Object.keys(dropdownPlacements),
357
369
  },
358
370
  },
359
371
  args: {
@@ -1,9 +1,8 @@
1
1
  import { shallowMount } from '@vue/test-utils';
2
- import GlDisclosureDropdownGroup, {
3
- GROUP_TOP_BORDER_CLASSES,
4
- } from './disclosure_dropdown_group.vue';
2
+ import GlDisclosureDropdownGroup, { BORDER_CLASSES } from './disclosure_dropdown_group.vue';
5
3
  import GlDisclosureDropdownItem from './disclosure_dropdown_item.vue';
6
4
  import { mockGroups, mockProfileGroups } from './mock_data';
5
+ import { DISCLOSURE_DROPDOWN_GROUP_BORDER_POSITIONS as borderPositions } from './constants';
7
6
 
8
7
  describe('GlDisclosureDropdownGroup', () => {
9
8
  let wrapper;
@@ -69,17 +68,17 @@ describe('GlDisclosureDropdownGroup', () => {
69
68
  });
70
69
  });
71
70
 
72
- describe('separator', () => {
73
- const topBorderClasses = GROUP_TOP_BORDER_CLASSES.split(' ');
74
-
75
- it('should not add top border by default', () => {
76
- buildWrapper();
77
- expect(wrapper.classes()).not.toEqual(expect.arrayContaining(topBorderClasses));
78
- });
79
-
80
- it('should add top border classes when `bordered` props is set to `true`', () => {
81
- buildWrapper({ propsData: { bordered: true } });
82
- expect(wrapper.classes()).toEqual(expect.arrayContaining(topBorderClasses));
83
- });
84
- });
71
+ it.each`
72
+ bordered | borderPosition | classes
73
+ ${true} | ${borderPositions.top} | ${BORDER_CLASSES[borderPositions.top].split(' ')}
74
+ ${true} | ${borderPositions.bottom} | ${BORDER_CLASSES[borderPositions.bottom].split(' ')}
75
+ ${false} | ${borderPositions.top} | ${[]}
76
+ ${false} | ${borderPositions.bottom} | ${[]}
77
+ `(
78
+ 'adds border classes `$classes` when bordered=$bordered and borderPosition=$borderPosition',
79
+ ({ bordered, borderPosition, classes }) => {
80
+ buildWrapper({ propsData: { bordered, borderPosition } });
81
+ expect(wrapper.classes()).toEqual(classes);
82
+ }
83
+ );
85
84
  });
@@ -2,9 +2,15 @@
2
2
  import uniqueId from 'lodash/uniqueId';
3
3
  import GlDisclosureDropdownItem from './disclosure_dropdown_item.vue';
4
4
  import { isGroup } from './utils';
5
- import { DISCLOSURE_DROPDOWN_GROUP_NAME } from './constants';
5
+ import {
6
+ DISCLOSURE_DROPDOWN_GROUP_NAME,
7
+ DISCLOSURE_DROPDOWN_GROUP_BORDER_POSITIONS as borderPositions,
8
+ } from './constants';
6
9
 
7
- export const GROUP_TOP_BORDER_CLASSES = 'gl-border-t gl-pt-2 gl-mt-2';
10
+ export const BORDER_CLASSES = {
11
+ [borderPositions.top]: 'gl-border-t gl-pt-2 gl-mt-2',
12
+ [borderPositions.bottom]: 'gl-border-b gl-pb-2 gl-mb-2',
13
+ };
8
14
 
9
15
  export default {
10
16
  name: DISCLOSURE_DROPDOWN_GROUP_NAME,
@@ -23,17 +29,30 @@ export default {
23
29
  },
24
30
  /**
25
31
  * If 'true', will set top border for the group
26
- * to separate from other groups
32
+ * to separate from other groups. You can control
33
+ * the border position using the `borderPosition`
34
+ * property.
27
35
  */
28
36
  bordered: {
29
37
  type: Boolean,
30
38
  required: false,
31
39
  default: false,
32
40
  },
41
+
42
+ /**
43
+ * Controls the position of the group's border. Valid
44
+ * values are 'top' and 'bottom'.
45
+ */
46
+ borderPosition: {
47
+ type: String,
48
+ required: false,
49
+ default: borderPositions.top,
50
+ validator: (value) => Object.keys(borderPositions).includes(value),
51
+ },
33
52
  },
34
53
  computed: {
35
54
  borderClass() {
36
- return this.bordered ? GROUP_TOP_BORDER_CLASSES : null;
55
+ return this.bordered ? BORDER_CLASSES[this.borderPosition] : null;
37
56
  },
38
57
  showHeader() {
39
58
  return this.$scopedSlots['group-label'] || this.group?.name;
@@ -179,6 +179,15 @@ describe('GlCollapsibleListbox', () => {
179
179
  await nextTick();
180
180
  expect(wrapper.emitted('select')[0][0]).toEqual(mockOptions[2].value);
181
181
  });
182
+
183
+ it('close dropdown for single selection', () => {
184
+ jest.spyOn(wrapper.vm, 'closeAndFocus');
185
+ expect(wrapper.vm.closeAndFocus).not.toHaveBeenCalled();
186
+
187
+ findListboxItems().at(2).vm.$emit('select', true);
188
+
189
+ expect(wrapper.vm.closeAndFocus).toHaveBeenCalled();
190
+ });
182
191
  });
183
192
 
184
193
  describe('with groups', () => {
@@ -475,17 +484,47 @@ describe('GlCollapsibleListbox', () => {
475
484
  expect(wrapper).toHaveLoggedVueErrors();
476
485
  });
477
486
 
487
+ it.each`
488
+ multiple
489
+ ${true}
490
+ ${false}
491
+ `(
492
+ 'shows the reset button if the label is provided and the selection is not empty and mode if multiple mode is $multiple',
493
+ ({ multiple }) => {
494
+ buildWrapper({
495
+ headerText: 'Select assignee',
496
+ resetButtonLabel: 'Unassign',
497
+ selected: mockOptions[1].value,
498
+ items: mockOptions,
499
+ multiple,
500
+ });
501
+
502
+ expect(findResetButton().exists()).toBe(multiple);
503
+ }
504
+ );
505
+
478
506
  it('shows the reset button if the label is provided and the selection is not empty', () => {
479
507
  buildWrapper({
480
508
  headerText: 'Select assignee',
481
509
  resetButtonLabel: 'Unassign',
482
510
  selected: mockOptions[1].value,
483
511
  items: mockOptions,
512
+ multiple: true,
484
513
  });
485
- const button = findResetButton();
486
514
 
487
- expect(button.exists()).toBe(true);
488
- expect(button.text()).toBe('Unassign');
515
+ expect(findResetButton().text()).toBe('Unassign');
516
+ });
517
+
518
+ it('hides reset button if dropdown has no items', () => {
519
+ buildWrapper({
520
+ headerText: 'Select assignee',
521
+ resetButtonLabel: 'Unassign',
522
+ selected: mockOptions[1].value,
523
+ items: [],
524
+ multiple: true,
525
+ });
526
+
527
+ expect(findResetButton().exists()).toBe(false);
489
528
  });
490
529
 
491
530
  it.each`
@@ -503,12 +542,13 @@ describe('GlCollapsibleListbox', () => {
503
542
  expect(findResetButton().exists()).toBe(false);
504
543
  });
505
544
 
506
- it('on click, emits the reset event and calls closeAndFocus()', () => {
545
+ it('on click, emits the reset event does not and call closeAndFocus() for multiple mode', () => {
507
546
  buildWrapper({
508
547
  headerText: 'Select assignee',
509
548
  resetButtonLabel: 'Unassign',
510
549
  selected: mockOptions[1].value,
511
550
  items: mockOptions,
551
+ multiple: true,
512
552
  });
513
553
  jest.spyOn(wrapper.vm, 'closeAndFocus');
514
554
 
@@ -518,7 +558,7 @@ describe('GlCollapsibleListbox', () => {
518
558
  findResetButton().trigger('click');
519
559
 
520
560
  expect(wrapper.emitted('reset')).toHaveLength(1);
521
- expect(wrapper.vm.closeAndFocus).toHaveBeenCalled();
561
+ expect(wrapper.vm.closeAndFocus).not.toHaveBeenCalled();
522
562
  });
523
563
  });
524
564
 
@@ -533,12 +573,12 @@ describe('GlCollapsibleListbox', () => {
533
573
  });
534
574
 
535
575
  it.each`
536
- multiple | expectedResult
537
- ${false} | ${false}
538
- ${true} | ${true}
576
+ multiple | resetVisible | selectAllVisible
577
+ ${false} | ${false} | ${false}
578
+ ${true} | ${false} | ${true}
539
579
  `(
540
580
  'shows the select all button if the label is provided and the selection is empty and multiple option is $multiple',
541
- ({ multiple, expectedResult }) => {
581
+ ({ multiple, resetVisible, selectAllVisible }) => {
542
582
  buildWrapper({
543
583
  headerText: 'Select assignee',
544
584
  resetButtonLabel: 'Unassign',
@@ -548,11 +588,23 @@ describe('GlCollapsibleListbox', () => {
548
588
  multiple,
549
589
  });
550
590
 
551
- expect(findResetButton().exists()).toBe(!expectedResult);
552
- expect(findSelectAllButton().exists()).toBe(expectedResult);
591
+ expect(findResetButton().exists()).toBe(resetVisible);
592
+ expect(findSelectAllButton().exists()).toBe(selectAllVisible);
553
593
  }
554
594
  );
555
595
 
596
+ it('hides select all button if dropdown has no items', () => {
597
+ buildWrapper({
598
+ headerText: 'Select assignee',
599
+ resetButtonLabel: 'Unassign',
600
+ showSelectAllButtonLabel: 'Select All',
601
+ items: [],
602
+ multiple: true,
603
+ });
604
+
605
+ expect(findSelectAllButton().exists()).toBe(false);
606
+ });
607
+
556
608
  it('has the label text "Select All" if the label is provided and the selection is empty', () => {
557
609
  buildWrapper({
558
610
  headerText: 'Select assignee',
@@ -345,7 +345,7 @@ export default {
345
345
  },
346
346
  computed: {
347
347
  listboxTag() {
348
- if (this.items.length === 0 || isOption(this.items[0])) return 'ul';
348
+ if (!this.hasItems || isOption(this.items[0])) return 'ul';
349
349
  return 'div';
350
350
  },
351
351
  listboxClasses() {
@@ -361,6 +361,9 @@ export default {
361
361
  flattenedOptions() {
362
362
  return flattenedOptions(this.items);
363
363
  },
364
+ hasItems() {
365
+ return this.items.length > 0;
366
+ },
364
367
  listboxToggleText() {
365
368
  if (!this.toggleText) {
366
369
  if (!this.multiple && this.selectedValues.length) {
@@ -394,6 +397,19 @@ export default {
394
397
  if (!this.resetButtonLabel) {
395
398
  return false;
396
399
  }
400
+
401
+ if (!this.multiple) {
402
+ return false;
403
+ }
404
+
405
+ /**
406
+ * if dropdown has no items
407
+ * reset all should be hidden
408
+ */
409
+ if (!this.hasItems) {
410
+ return false;
411
+ }
412
+
397
413
  if (this.multiple) {
398
414
  return this.selected.length > 0;
399
415
  }
@@ -408,6 +424,14 @@ export default {
408
424
  return false;
409
425
  }
410
426
 
427
+ /**
428
+ * if dropdown has no items
429
+ * select all should be hidden
430
+ */
431
+ if (!this.hasItems) {
432
+ return false;
433
+ }
434
+
411
435
  return this.selected.length === 0;
412
436
  },
413
437
  showIntersectionObserver() {
@@ -636,7 +660,6 @@ export default {
636
660
  * @event reset
637
661
  */
638
662
  this.$emit('reset');
639
- this.closeAndFocus();
640
663
  },
641
664
  onSelectAllButtonClicked() {
642
665
  /**