@gitlab/ui 47.0.0 → 48.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 CHANGED
@@ -1,3 +1,23 @@
1
+ # [48.0.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v47.0.1...v48.0.0) (2022-10-19)
2
+
3
+
4
+ ### Features
5
+
6
+ * **GlListbox:** convert header slot to a prop ([976250e](https://gitlab.com/gitlab-org/gitlab-ui/commit/976250ef04e72f0899762669aa16defb3a599c1e))
7
+
8
+
9
+ ### BREAKING CHANGES
10
+
11
+ * **GlListbox:** GlListbox usages should be updated to pass the header
12
+ via the `headerText` prop instead of the `header` slot.
13
+
14
+ ## [47.0.1](https://gitlab.com/gitlab-org/gitlab-ui/compare/v47.0.0...v47.0.1) (2022-10-18)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * **GlDropdown:** Fix GlDropdown not auto flipping horizontally ([635eca3](https://gitlab.com/gitlab-org/gitlab-ui/commit/635eca3eea58f6ee1d2fd01a17a1db3401657647))
20
+
1
21
  # [47.0.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v46.1.0...v47.0.0) (2022-10-17)
2
22
 
3
23
 
@@ -1,3 +1,4 @@
1
+ import _merge from 'lodash/merge';
1
2
  import Vue from 'vue';
2
3
  import { BDropdown } from 'bootstrap-vue/esm/index.js';
3
4
  import { selectAll, isVisible } from 'bootstrap-vue/esm/utils/dom';
@@ -9,8 +10,6 @@ import GlLoadingIcon from '../loading_icon/loading_icon';
9
10
  import GlDropdownDivider from './dropdown_divider';
10
11
  import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
11
12
 
12
- //
13
-
14
13
  function filterVisible(els) {
15
14
  return (els || []).filter(isVisible);
16
15
  }
@@ -27,6 +26,14 @@ const ExtendedBDropdown = Vue.extend(BDropdown, {
27
26
 
28
27
  }
29
28
  });
29
+ const DefaultPopperOptions = {
30
+ modifiers: {
31
+ flip: {
32
+ flipVariationsByContent: true,
33
+ padding: 28
34
+ }
35
+ }
36
+ };
30
37
  var script = {
31
38
  components: {
32
39
  BDropdown: ExtendedBDropdown,
@@ -139,6 +146,11 @@ var script = {
139
146
  type: Boolean,
140
147
  required: false,
141
148
  default: false
149
+ },
150
+ popperOpts: {
151
+ type: Object,
152
+ required: false,
153
+ default: null
142
154
  }
143
155
  },
144
156
  computed: {
@@ -188,6 +200,10 @@ var script = {
188
200
 
189
201
  hasHighlightedItemsOrClearAll() {
190
202
  return this.hasHighlightedItemsContent && this.showHighlightedItemsTitle || this.showClearAll;
203
+ },
204
+
205
+ popperOptions() {
206
+ return _merge({}, DefaultPopperOptions, this.popperOpts);
191
207
  }
192
208
 
193
209
  },
@@ -212,7 +228,7 @@ var script = {
212
228
  const __vue_script__ = script;
213
229
 
214
230
  /* template */
215
- var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('b-dropdown',_vm._g(_vm._b({ref:"dropdown",staticClass:"gl-new-dropdown",attrs:{"split":_vm.split,"variant":_vm.variant,"size":_vm.buttonSize,"toggle-class":[_vm.toggleButtonClasses],"split-class":_vm.splitButtonClasses,"block":_vm.block,"disabled":_vm.disabled || _vm.loading,"right":_vm.right},scopedSlots:_vm._u([{key:"button-content",fn:function(){return [_vm._t("button-content",function(){return [(_vm.loading)?_c('gl-loading-icon',{class:{ 'gl-mr-2': !_vm.isIconOnly }}):_vm._e(),_vm._v(" "),(_vm.icon && !(_vm.isIconOnly && _vm.loading))?_c('gl-icon',{staticClass:"dropdown-icon",attrs:{"name":_vm.icon}}):_vm._e(),_vm._v(" "),_c('span',{staticClass:"gl-new-dropdown-button-text",class:{ 'gl-sr-only': _vm.textSrOnly }},[_vm._t("button-text",function(){return [_vm._v(_vm._s(_vm.buttonText))]})],2),_vm._v(" "),(_vm.renderCaret)?_c('gl-icon',{staticClass:"gl-button-icon dropdown-chevron",attrs:{"name":"chevron-down"}}):_vm._e()]})]},proxy:true}],null,true)},'b-dropdown',_vm.$attrs,false),_vm.$listeners),[_c('div',{staticClass:"gl-new-dropdown-inner"},[(_vm.hasSlotContents('header') || _vm.headerText)?_c('div',{staticClass:"gl-new-dropdown-header",class:{ 'gl-border-b-0!': _vm.hideHeaderBorder }},[(_vm.headerText)?_c('p',{staticClass:"gl-new-dropdown-header-top"},[_vm._v("\n "+_vm._s(_vm.headerText)+"\n ")]):_vm._e(),_vm._v(" "),_vm._t("header")],2):_vm._e(),_vm._v(" "),(_vm.hasHighlightedItemsOrClearAll)?_c('div',{staticClass:"gl-display-flex gl-flex-direction-row gl-justify-content-space-between gl-align-items-center"},[(_vm.hasHighlightedItemsContent && _vm.showHighlightedItemsTitle)?_c('div',{staticClass:"gl-display-flex gl-flex-grow-1 gl-justify-content-flex-start",class:_vm.highlightedItemsTitleClass},[_c('span',{staticClass:"gl-font-weight-bold",attrs:{"data-testid":"highlighted-items-title"}},[_vm._v(_vm._s(_vm.highlightedItemsTitle))])]):_vm._e(),_vm._v(" "),(_vm.showClearAll)?_c('div',{staticClass:"gl-display-flex gl-flex-grow-1 gl-justify-content-end",class:_vm.clearAllTextClass},[_c('gl-button',{attrs:{"size":"small","category":"tertiary","variant":"link","data-testid":"clear-all-button"},on:{"click":function($event){return _vm.$emit('clear-all', $event)}}},[_vm._v(_vm._s(_vm.clearAllText))])],1):_vm._e()]):_vm._e(),_vm._v(" "),_c('div',{staticClass:"gl-new-dropdown-contents"},[(_vm.hasHighlightedItemsContent)?_c('div',{staticClass:"gl-overflow-visible",attrs:{"data-testid":"highlighted-items"}},[_vm._t("highlighted-items"),_vm._v(" "),_c('gl-dropdown-divider')],2):_vm._e(),_vm._v(" "),_vm._t("default")],2),_vm._v(" "),(_vm.hasSlotContents('footer'))?_c('div',{staticClass:"gl-new-dropdown-footer"},[_vm._t("footer")],2):_vm._e()])])};
231
+ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('b-dropdown',_vm._g(_vm._b({ref:"dropdown",staticClass:"gl-new-dropdown",attrs:{"split":_vm.split,"variant":_vm.variant,"size":_vm.buttonSize,"toggle-class":[_vm.toggleButtonClasses],"split-class":_vm.splitButtonClasses,"block":_vm.block,"disabled":_vm.disabled || _vm.loading,"right":_vm.right,"popper-opts":_vm.popperOptions},scopedSlots:_vm._u([{key:"button-content",fn:function(){return [_vm._t("button-content",function(){return [(_vm.loading)?_c('gl-loading-icon',{class:{ 'gl-mr-2': !_vm.isIconOnly }}):_vm._e(),_vm._v(" "),(_vm.icon && !(_vm.isIconOnly && _vm.loading))?_c('gl-icon',{staticClass:"dropdown-icon",attrs:{"name":_vm.icon}}):_vm._e(),_vm._v(" "),_c('span',{staticClass:"gl-new-dropdown-button-text",class:{ 'gl-sr-only': _vm.textSrOnly }},[_vm._t("button-text",function(){return [_vm._v(_vm._s(_vm.buttonText))]})],2),_vm._v(" "),(_vm.renderCaret)?_c('gl-icon',{staticClass:"gl-button-icon dropdown-chevron",attrs:{"name":"chevron-down"}}):_vm._e()]})]},proxy:true}],null,true)},'b-dropdown',_vm.$attrs,false),_vm.$listeners),[_c('div',{staticClass:"gl-new-dropdown-inner"},[(_vm.hasSlotContents('header') || _vm.headerText)?_c('div',{staticClass:"gl-new-dropdown-header",class:{ 'gl-border-b-0!': _vm.hideHeaderBorder }},[(_vm.headerText)?_c('p',{staticClass:"gl-new-dropdown-header-top"},[_vm._v("\n "+_vm._s(_vm.headerText)+"\n ")]):_vm._e(),_vm._v(" "),_vm._t("header")],2):_vm._e(),_vm._v(" "),(_vm.hasHighlightedItemsOrClearAll)?_c('div',{staticClass:"gl-display-flex gl-flex-direction-row gl-justify-content-space-between gl-align-items-center"},[(_vm.hasHighlightedItemsContent && _vm.showHighlightedItemsTitle)?_c('div',{staticClass:"gl-display-flex gl-flex-grow-1 gl-justify-content-flex-start",class:_vm.highlightedItemsTitleClass},[_c('span',{staticClass:"gl-font-weight-bold",attrs:{"data-testid":"highlighted-items-title"}},[_vm._v(_vm._s(_vm.highlightedItemsTitle))])]):_vm._e(),_vm._v(" "),(_vm.showClearAll)?_c('div',{staticClass:"gl-display-flex gl-flex-grow-1 gl-justify-content-end",class:_vm.clearAllTextClass},[_c('gl-button',{attrs:{"size":"small","category":"tertiary","variant":"link","data-testid":"clear-all-button"},on:{"click":function($event){return _vm.$emit('clear-all', $event)}}},[_vm._v(_vm._s(_vm.clearAllText))])],1):_vm._e()]):_vm._e(),_vm._v(" "),_c('div',{staticClass:"gl-new-dropdown-contents"},[(_vm.hasHighlightedItemsContent)?_c('div',{staticClass:"gl-overflow-visible",attrs:{"data-testid":"highlighted-items"}},[_vm._t("highlighted-items"),_vm._v(" "),_c('gl-dropdown-divider')],2):_vm._e(),_vm._v(" "),_vm._t("default")],2),_vm._v(" "),(_vm.hasSlotContents('footer'))?_c('div',{staticClass:"gl-new-dropdown-footer"},[_vm._t("footer")],2):_vm._e()])])};
216
232
  var __vue_staticRenderFns__ = [];
217
233
 
218
234
  /* style */
@@ -245,3 +261,4 @@ var __vue_staticRenderFns__ = [];
245
261
  );
246
262
 
247
263
  export default __vue_component__;
264
+ export { DefaultPopperOptions };
@@ -12,9 +12,11 @@ import { itemsValidator, isOption, flattenedOptions } from './utils';
12
12
  import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
13
13
 
14
14
  const ITEM_SELECTOR = '[role="option"]';
15
+ const HEADER_ITEMS_BORDER_CLASSES = ['gl-border-b-1', 'gl-border-b-solid', 'gl-border-b-gray-200'];
15
16
  const GROUP_TOP_BORDER_CLASSES = ['gl-border-t', 'gl-pt-3', 'gl-mt-3'];
16
17
  const SEARCH_INPUT_SELECTOR = '.gl-search-box-by-type-input';
17
18
  var script = {
19
+ HEADER_ITEMS_BORDER_CLASSES,
18
20
  events: {
19
21
  GL_DROPDOWN_SHOWN,
20
22
  GL_DROPDOWN_HIDDEN
@@ -77,6 +79,13 @@ var script = {
77
79
  default: false
78
80
  },
79
81
 
82
+ /** The header text */
83
+ headerText: {
84
+ type: String,
85
+ required: false,
86
+ default: ''
87
+ },
88
+
80
89
  /**
81
90
  * Styling option - dropdown's toggle category
82
91
  */
@@ -278,6 +287,10 @@ var script = {
278
287
 
279
288
  announceSRSearchResults() {
280
289
  return this.searchable && !this.showNoResultsText && this.$scopedSlots['search-summary-sr-only'];
290
+ },
291
+
292
+ headerId() {
293
+ return this.headerText && _uniqueId('listbox-header-');
281
294
  }
282
295
 
283
296
  },
@@ -467,7 +480,7 @@ var script = {
467
480
  const __vue_script__ = script;
468
481
 
469
482
  /* template */
470
- var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('gl-base-dropdown',{ref:"baseDropdown",attrs:{"aria-haspopup":"listbox","aria-labelledby":_vm.toggleAriaLabelledBy,"toggle-id":_vm.toggleId,"toggle-text":_vm.listboxToggleText,"toggle-class":_vm.toggleClass,"text-sr-only":_vm.textSrOnly,"category":_vm.category,"variant":_vm.variant,"size":_vm.size,"icon":_vm.icon,"disabled":_vm.disabled,"loading":_vm.loading,"no-caret":_vm.noCaret,"right":_vm.right},on:_vm._d({},[_vm.$options.events.GL_DROPDOWN_SHOWN,_vm.onShow,_vm.$options.events.GL_DROPDOWN_HIDDEN,_vm.onHide])},[_vm._t("header"),_vm._v(" "),(_vm.searchable)?_c('div',{staticClass:"gl-border-b-1 gl-border-b-solid gl-border-b-gray-200"},[_c('gl-search-box-by-type',{ref:"searchBox",attrs:{"aria-owns":_vm.listboxId,"data-testid":"listbox-search-input"},on:{"input":_vm.search,"keydown":_vm.onKeydown},model:{value:(_vm.searchStr),callback:function ($$v) {_vm.searchStr=$$v;},expression:"searchStr"}}),_vm._v(" "),(_vm.searching)?_c('gl-loading-icon',{staticClass:"gl-my-3",attrs:{"data-testid":"listbox-search-loader","size":"md"}}):_vm._e()],1):_vm._e(),_vm._v(" "),(_vm.showList)?_c(_vm.listboxTag,{ref:"list",tag:"component",staticClass:"gl-new-dropdown-contents gl-list-style-none gl-pl-0 gl-mb-0",attrs:{"id":"listbox","aria-labelledby":_vm.listAriaLabelledBy || _vm.toggleId,"role":"listbox","tabindex":"-1"},on:{"keydown":_vm.onKeydown}},[_vm._l((_vm.items),function(item,index){return [(_vm.isOption(item))?[_c('gl-listbox-item',{key:item.value,attrs:{"is-selected":_vm.isSelected(item),"is-focused":_vm.isFocused(item),"is-check-centered":_vm.isCheckCentered},on:{"select":function($event){return _vm.onSelect(item, $event)}}},[_vm._t("list-item",function(){return [_vm._v("\n "+_vm._s(item.text)+"\n ")]},{"item":item})],2)]:[_c('gl-listbox-group',{key:item.text,class:_vm.groupClasses(index),attrs:{"name":item.text},scopedSlots:_vm._u([(_vm.$scopedSlots['group-label'])?{key:"group-label",fn:function(){return [_vm._t("group-label",null,{"group":item})]},proxy:true}:null],null,true)},[_vm._v(" "),_vm._l((item.options),function(option){return _c('gl-listbox-item',{key:option.value,attrs:{"is-selected":_vm.isSelected(option),"is-focused":_vm.isFocused(option),"is-check-centered":_vm.isCheckCentered},on:{"select":function($event){return _vm.onSelect(option, $event)}}},[_vm._t("list-item",function(){return [_vm._v("\n "+_vm._s(option.text)+"\n ")]},{"item":option})],2)})],2)]]})],2):_vm._e(),_vm._v(" "),(_vm.announceSRSearchResults)?_c('span',{staticClass:"gl-sr-only",attrs:{"data-testid":"listbox-number-of-results","aria-live":"assertive"}},[_vm._t("search-summary-sr-only")],2):(_vm.showNoResultsText)?_c('div',{staticClass:"gl-pl-7 gl-pr-5 gl-pt-3 gl-font-base gl-text-gray-600",attrs:{"aria-live":"assertive","data-testid":"listbox-no-results-text"}},[_vm._v("\n "+_vm._s(_vm.noResultsText)+"\n ")]):_vm._e(),_vm._v(" "),_vm._t("footer")],2)};
483
+ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('gl-base-dropdown',{ref:"baseDropdown",attrs:{"aria-haspopup":"listbox","aria-labelledby":_vm.toggleAriaLabelledBy,"toggle-id":_vm.toggleId,"toggle-text":_vm.listboxToggleText,"toggle-class":_vm.toggleClass,"text-sr-only":_vm.textSrOnly,"category":_vm.category,"variant":_vm.variant,"size":_vm.size,"icon":_vm.icon,"disabled":_vm.disabled,"loading":_vm.loading,"no-caret":_vm.noCaret,"right":_vm.right},on:_vm._d({},[_vm.$options.events.GL_DROPDOWN_SHOWN,_vm.onShow,_vm.$options.events.GL_DROPDOWN_HIDDEN,_vm.onHide])},[(_vm.headerText)?_c('div',{staticClass:"gl-display-flex gl-align-items-center gl-p-3",class:_vm.$options.HEADER_ITEMS_BORDER_CLASSES},[_c('div',{staticClass:"gl-flex-grow-1 gl-font-weight-bold gl-font-sm",attrs:{"id":_vm.headerId,"data-testid":"listbox-header-text"}},[_vm._v("\n "+_vm._s(_vm.headerText)+"\n ")])]):_vm._e(),_vm._v(" "),(_vm.searchable)?_c('div',{class:_vm.$options.HEADER_ITEMS_BORDER_CLASSES},[_c('gl-search-box-by-type',{ref:"searchBox",attrs:{"aria-owns":_vm.listboxId,"data-testid":"listbox-search-input"},on:{"input":_vm.search,"keydown":_vm.onKeydown},model:{value:(_vm.searchStr),callback:function ($$v) {_vm.searchStr=$$v;},expression:"searchStr"}}),_vm._v(" "),(_vm.searching)?_c('gl-loading-icon',{staticClass:"gl-my-3",attrs:{"data-testid":"listbox-search-loader","size":"md"}}):_vm._e()],1):_vm._e(),_vm._v(" "),(_vm.showList)?_c(_vm.listboxTag,{ref:"list",tag:"component",staticClass:"gl-new-dropdown-contents gl-list-style-none gl-pl-0 gl-mb-0",attrs:{"id":"listbox","aria-labelledby":_vm.listAriaLabelledBy || _vm.headerId || _vm.toggleId,"role":"listbox","tabindex":"-1"},on:{"keydown":_vm.onKeydown}},[_vm._l((_vm.items),function(item,index){return [(_vm.isOption(item))?[_c('gl-listbox-item',{key:item.value,attrs:{"is-selected":_vm.isSelected(item),"is-focused":_vm.isFocused(item),"is-check-centered":_vm.isCheckCentered},on:{"select":function($event){return _vm.onSelect(item, $event)}}},[_vm._t("list-item",function(){return [_vm._v("\n "+_vm._s(item.text)+"\n ")]},{"item":item})],2)]:[_c('gl-listbox-group',{key:item.text,class:_vm.groupClasses(index),attrs:{"name":item.text},scopedSlots:_vm._u([(_vm.$scopedSlots['group-label'])?{key:"group-label",fn:function(){return [_vm._t("group-label",null,{"group":item})]},proxy:true}:null],null,true)},[_vm._v(" "),_vm._l((item.options),function(option){return _c('gl-listbox-item',{key:option.value,attrs:{"is-selected":_vm.isSelected(option),"is-focused":_vm.isFocused(option),"is-check-centered":_vm.isCheckCentered},on:{"select":function($event){return _vm.onSelect(option, $event)}}},[_vm._t("list-item",function(){return [_vm._v("\n "+_vm._s(option.text)+"\n ")]},{"item":option})],2)})],2)]]})],2):_vm._e(),_vm._v(" "),(_vm.announceSRSearchResults)?_c('span',{staticClass:"gl-sr-only",attrs:{"data-testid":"listbox-number-of-results","aria-live":"assertive"}},[_vm._t("search-summary-sr-only")],2):(_vm.showNoResultsText)?_c('div',{staticClass:"gl-pl-7 gl-pr-5 gl-pt-3 gl-font-base gl-text-gray-600",attrs:{"aria-live":"assertive","data-testid":"listbox-no-results-text"}},[_vm._v("\n "+_vm._s(_vm.noResultsText)+"\n ")]):_vm._e(),_vm._v(" "),_vm._t("footer")],2)};
471
484
  var __vue_staticRenderFns__ = [];
472
485
 
473
486
  /* style */
@@ -43,7 +43,7 @@ var script = {
43
43
  */
44
44
  render(createElement, _ref) {
45
45
  let {
46
- children,
46
+ scopedSlots,
47
47
  data,
48
48
  props = {}
49
49
  } = _ref;
@@ -58,8 +58,15 @@ var script = {
58
58
  });
59
59
  return createElement(GlDropdownItem, { ...data,
60
60
  attrs: { ...props
61
+ },
62
+ scopedSlots: {
63
+ default: () => {
64
+ var _scopedSlots$default;
65
+
66
+ return [icon, (_scopedSlots$default = scopedSlots.default) === null || _scopedSlots$default === void 0 ? void 0 : _scopedSlots$default.call(scopedSlots)];
67
+ }
61
68
  }
62
- }, [icon, children]);
69
+ });
63
70
  }
64
71
 
65
72
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "47.0.0",
3
+ "version": "48.0.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -107,7 +107,7 @@
107
107
  "eslint": "8.25.0",
108
108
  "eslint-import-resolver-jest": "3.0.2",
109
109
  "eslint-plugin-cypress": "2.12.1",
110
- "eslint-plugin-storybook": "0.6.5",
110
+ "eslint-plugin-storybook": "0.6.6",
111
111
  "file-loader": "^4.2.0",
112
112
  "glob": "^7.2.0",
113
113
  "identity-obj-proxy": "^3.0.0",
@@ -1,5 +1,6 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
 
3
+ import { BDropdown } from 'bootstrap-vue';
3
4
  import { dropdownVariantOptions } from '../../../utils/constants';
4
5
  import GlLoadingIcon from '../loading_icon/loading_icon.vue';
5
6
  import GlDropdown from './dropdown.vue';
@@ -34,6 +35,7 @@ describe('new dropdown', () => {
34
35
  const findClearAll = () => findByTestId('clear-all-button');
35
36
  const findHighlightedItemsTitle = () => findByTestId('highlighted-items-title');
36
37
  const findHighlightedItems = () => findByTestId('highlighted-items');
38
+ const findDropdown = () => wrapper.findComponent(BDropdown);
37
39
 
38
40
  it('renders when text is null', () => {
39
41
  buildWrapper({ text: null });
@@ -417,4 +419,39 @@ describe('new dropdown', () => {
417
419
  });
418
420
  });
419
421
  });
422
+
423
+ describe('popperOpts prop', () => {
424
+ it('combines the passed in popperOpts with the default popperOpts', () => {
425
+ const popperOpts = {
426
+ modifiers: {
427
+ flip: {
428
+ flipVariationsByContent: false,
429
+ },
430
+ },
431
+ };
432
+ buildWrapper({ popperOpts });
433
+
434
+ expect(findDropdown().props('popperOpts')).toEqual({
435
+ modifiers: {
436
+ flip: {
437
+ flipVariationsByContent: false,
438
+ padding: 28,
439
+ },
440
+ },
441
+ });
442
+ });
443
+
444
+ it('uses the default popperOpts prop', () => {
445
+ buildWrapper();
446
+
447
+ expect(findDropdown().props('popperOpts')).toEqual({
448
+ modifiers: {
449
+ flip: {
450
+ flipVariationsByContent: true,
451
+ padding: 28,
452
+ },
453
+ },
454
+ });
455
+ });
456
+ });
420
457
  });
@@ -84,7 +84,7 @@ const withContainer = (template, containerHeight = 150) => `
84
84
  ${template}
85
85
  </div>`;
86
86
 
87
- function wrap(template, containerHeight) {
87
+ function wrap(template, containerHeight, cssClass) {
88
88
  return withContainer(
89
89
  `
90
90
  <gl-dropdown
@@ -109,6 +109,7 @@ function wrap(template, containerHeight) {
109
109
  :highlighted-items-title-class="highlightedItemsTitleClass"
110
110
  :loading="loading"
111
111
  :right="right"
112
+ class="${cssClass}"
112
113
  >
113
114
  ${template}
114
115
  </gl-dropdown>`,
@@ -514,6 +515,28 @@ WithHighlightedItems.args = generateProps({
514
515
  highlightedItemsTitleClass: 'gl-px-5',
515
516
  });
516
517
 
518
+ export const OnRightEdge = (args, { argTypes }) => ({
519
+ props: Object.keys(argTypes),
520
+ components,
521
+ template: wrap(
522
+ `
523
+ <gl-dropdown-item>First item</gl-dropdown-item>
524
+ <gl-dropdown-item>Second item</gl-dropdown-item>
525
+ <gl-dropdown-item>Third item</gl-dropdown-item>
526
+ <gl-dropdown-item>Fourth item</gl-dropdown-item>
527
+ `,
528
+ 200,
529
+ 'gl-display-block gl-text-right'
530
+ ),
531
+ mounted() {
532
+ clickDropdown(this);
533
+ },
534
+ updated() {
535
+ addClass(this);
536
+ },
537
+ });
538
+ OnRightEdge.args = generateProps({ text: 'Some dropdown' });
539
+
517
540
  export default {
518
541
  title: 'base/dropdown',
519
542
  component: GlDropdown,
@@ -3,6 +3,7 @@
3
3
  import Vue from 'vue';
4
4
  import { BDropdown } from 'bootstrap-vue';
5
5
  import { isVisible, selectAll } from 'bootstrap-vue/src/utils/dom';
6
+ import { merge } from 'lodash';
6
7
  import {
7
8
  buttonCategoryOptions,
8
9
  buttonSizeOptions,
@@ -33,6 +34,15 @@ const ExtendedBDropdown = Vue.extend(BDropdown, {
33
34
  },
34
35
  });
35
36
 
37
+ export const DefaultPopperOptions = {
38
+ modifiers: {
39
+ flip: {
40
+ flipVariationsByContent: true,
41
+ padding: 28,
42
+ },
43
+ },
44
+ };
45
+
36
46
  export default {
37
47
  components: {
38
48
  BDropdown: ExtendedBDropdown,
@@ -146,6 +156,11 @@ export default {
146
156
  required: false,
147
157
  default: false,
148
158
  },
159
+ popperOpts: {
160
+ type: Object,
161
+ required: false,
162
+ default: null,
163
+ },
149
164
  },
150
165
  computed: {
151
166
  renderCaret() {
@@ -199,6 +214,9 @@ export default {
199
214
  (this.hasHighlightedItemsContent && this.showHighlightedItemsTitle) || this.showClearAll
200
215
  );
201
216
  },
217
+ popperOptions() {
218
+ return merge({}, DefaultPopperOptions, this.popperOpts);
219
+ },
202
220
  },
203
221
  methods: {
204
222
  hasSlotContents(slotName) {
@@ -227,6 +245,7 @@ export default {
227
245
  :block="block"
228
246
  :disabled="disabled || loading"
229
247
  :right="right"
248
+ :popper-opts="popperOptions"
230
249
  v-on="$listeners"
231
250
  >
232
251
  <div class="gl-new-dropdown-inner">
@@ -30,6 +30,7 @@ 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 findHeaderText = () => wrapper.find("[data-testid='listbox-header-text']");
33
34
  const findSearchBox = () => wrapper.find("[data-testid='listbox-search-input']");
34
35
  const findNoResultsText = () => wrapper.find("[data-testid='listbox-no-results-text']");
35
36
  const findLoadingIcon = () => wrapper.find("[data-testid='listbox-search-loader']");
@@ -263,13 +264,28 @@ describe('GlListbox', () => {
263
264
  });
264
265
  });
265
266
 
266
- describe('when the header slot content is provided', () => {
267
+ describe('when the header prop is provided', () => {
267
268
  const headerContent = 'Header Content';
268
- const slots = { header: headerContent };
269
269
 
270
270
  it('renders it', () => {
271
- buildWrapper({}, slots);
272
- expect(wrapper.text()).toContain(headerContent);
271
+ buildWrapper({ headerText: headerContent, items: mockOptions });
272
+
273
+ expect(findHeaderText().text()).toContain(headerContent);
274
+ });
275
+
276
+ it("uses the generated header ID as the list's aria-labelledby attribute", () => {
277
+ buildWrapper({ headerText: headerContent, items: mockOptions });
278
+
279
+ expect(findListContainer().attributes('aria-labelledby')).toBe(
280
+ findHeaderText().attributes('id')
281
+ );
282
+ });
283
+
284
+ it('if a custom list label is passed, it overrides the header ID', () => {
285
+ const listAriaLabelledBy = 'listAriaLabelledBy';
286
+ buildWrapper({ listAriaLabelledBy, headerText: headerContent, items: mockOptions });
287
+
288
+ expect(findListContainer().attributes('aria-labelledby')).toBe(listAriaLabelledBy);
273
289
  });
274
290
  });
275
291
 
@@ -33,6 +33,7 @@ const generateProps = ({
33
33
  right = defaultValue('right'),
34
34
  toggleText,
35
35
  textSrOnly = defaultValue('textSrOnly'),
36
+ headerText = defaultValue('headerText'),
36
37
  icon = '',
37
38
  multiple = defaultValue('multiple'),
38
39
  isCheckCentered = defaultValue('isCheckCentered'),
@@ -53,6 +54,7 @@ const generateProps = ({
53
54
  right,
54
55
  toggleText,
55
56
  textSrOnly,
57
+ headerText,
56
58
  icon,
57
59
  multiple,
58
60
  isCheckCentered,
@@ -76,6 +78,7 @@ const makeBindings = (overrides = {}) =>
76
78
  ':right': 'right',
77
79
  ':toggle-text': 'toggleText',
78
80
  ':text-sr-only': 'textSrOnly',
81
+ ':header-text': 'headerText',
79
82
  ':icon': 'icon',
80
83
  ':multiple': 'multiple',
81
84
  ':is-check-centered': 'isCheckCentered',
@@ -151,10 +154,7 @@ export const HeaderAndFooter = (args, { argTypes }) => ({
151
154
  this.selected.push(mockOptions[index].value);
152
155
  },
153
156
  },
154
- template: template(
155
- `<template #header>
156
- <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>
157
- </template>
157
+ template: template(`
158
158
  <template #footer>
159
159
  <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">
160
160
  <gl-button-group :vertical="false">
@@ -164,11 +164,11 @@ export const HeaderAndFooter = (args, { argTypes }) => ({
164
164
  </gl-button-group>
165
165
  </div>
166
166
  </template>
167
- `
168
- ),
167
+ `),
169
168
  });
170
169
  HeaderAndFooter.args = generateProps({
171
170
  toggleText: 'Header and Footer',
171
+ headerText: 'Assign to department',
172
172
  multiple: true,
173
173
  });
174
174
  HeaderAndFooter.decorators = [makeContainer({ height: '370px' })];
@@ -315,7 +315,6 @@ export const Searchable = (args, { argTypes }) => ({
315
315
  filteredItems: mockOptions,
316
316
  searchInProgress: false,
317
317
  timeoutId: null,
318
- headerId: 'listbox-header',
319
318
  };
320
319
  },
321
320
  mounted() {
@@ -358,19 +357,12 @@ export const Searchable = (args, { argTypes }) => ({
358
357
  },
359
358
  },
360
359
  template: template(
361
- `<template #header>
362
- <p :id="headerId"
363
- 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">
364
- Assign to department</p>
365
- </template>
366
- <template #search-summary-sr-only>
367
- {{ numberOfSearchResults }}
368
- </template>
369
- `,
360
+ `<template #search-summary-sr-only>
361
+ {{ numberOfSearchResults }}
362
+ </template>`,
370
363
  {
371
364
  bindingOverrides: {
372
365
  ':items': 'filteredItems',
373
- ':list-aria-labelled-by': 'headerId',
374
366
  ':toggle-text': 'customToggleText',
375
367
  ':searching': 'searchInProgress',
376
368
  '@search': 'filterList',
@@ -378,7 +370,7 @@ export const Searchable = (args, { argTypes }) => ({
378
370
  }
379
371
  ),
380
372
  });
381
- Searchable.args = generateProps({ searchable: true });
373
+ Searchable.args = generateProps({ headerText: 'Assign to department', searchable: true });
382
374
  Searchable.decorators = [makeContainer({ height: '370px' })];
383
375
 
384
376
  export const SearchableGroups = (args, { argTypes }) => ({
@@ -392,7 +384,6 @@ export const SearchableGroups = (args, { argTypes }) => ({
392
384
  filteredGroupOptions: mockGroups,
393
385
  searchInProgress: false,
394
386
  timeoutId: null,
395
- headerId: 'listbox-header',
396
387
  };
397
388
  },
398
389
  mounted() {
@@ -450,19 +441,12 @@ export const SearchableGroups = (args, { argTypes }) => ({
450
441
  },
451
442
  },
452
443
  template: template(
453
- `<template #header>
454
- <p :id="headerId"
455
- 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">
456
- Assign to department</p>
457
- </template>
458
- <template #search-summary-sr-only>
459
- {{ numberOfSearchResults }}
460
- </template>
461
- `,
444
+ `<template #search-summary-sr-only>
445
+ {{ numberOfSearchResults }}
446
+ </template>`,
462
447
  {
463
448
  bindingOverrides: {
464
449
  ':items': 'filteredGroupOptions',
465
- ':list-aria-labelled-by': 'headerId',
466
450
  ':toggle-text': 'customToggleText',
467
451
  ':searching': 'searchInProgress',
468
452
  '@search': 'filterList',
@@ -470,5 +454,9 @@ export const SearchableGroups = (args, { argTypes }) => ({
470
454
  }
471
455
  ),
472
456
  });
473
- SearchableGroups.args = generateProps({ searchable: true, items: mockGroups });
457
+ SearchableGroups.args = generateProps({
458
+ headerText: 'Select ref',
459
+ searchable: true,
460
+ items: mockGroups,
461
+ });
474
462
  SearchableGroups.decorators = [makeContainer({ height: '370px' })];
@@ -23,10 +23,12 @@ import GlListboxGroup from './listbox_group.vue';
23
23
  import { isOption, itemsValidator, flattenedOptions } from './utils';
24
24
 
25
25
  export const ITEM_SELECTOR = '[role="option"]';
26
+ const HEADER_ITEMS_BORDER_CLASSES = ['gl-border-b-1', 'gl-border-b-solid', 'gl-border-b-gray-200'];
26
27
  const GROUP_TOP_BORDER_CLASSES = ['gl-border-t', 'gl-pt-3', 'gl-mt-3'];
27
28
  export const SEARCH_INPUT_SELECTOR = '.gl-search-box-by-type-input';
28
29
 
29
30
  export default {
31
+ HEADER_ITEMS_BORDER_CLASSES,
30
32
  events: {
31
33
  GL_DROPDOWN_SHOWN,
32
34
  GL_DROPDOWN_HIDDEN,
@@ -84,6 +86,12 @@ export default {
84
86
  required: false,
85
87
  default: false,
86
88
  },
89
+ /** The header text */
90
+ headerText: {
91
+ type: String,
92
+ required: false,
93
+ default: '',
94
+ },
87
95
  /**
88
96
  * Styling option - dropdown's toggle category
89
97
  */
@@ -255,6 +263,9 @@ export default {
255
263
  this.searchable && !this.showNoResultsText && this.$scopedSlots['search-summary-sr-only']
256
264
  );
257
265
  },
266
+ headerId() {
267
+ return this.headerText && uniqueId('listbox-header-');
268
+ },
258
269
  },
259
270
  watch: {
260
271
  selected: {
@@ -431,10 +442,21 @@ export default {
431
442
  @[$options.events.GL_DROPDOWN_SHOWN]="onShow"
432
443
  @[$options.events.GL_DROPDOWN_HIDDEN]="onHide"
433
444
  >
434
- <!-- @slot Content to display in dropdown header -->
435
- <slot name="header"></slot>
445
+ <div
446
+ v-if="headerText"
447
+ class="gl-display-flex gl-align-items-center gl-p-3"
448
+ :class="$options.HEADER_ITEMS_BORDER_CLASSES"
449
+ >
450
+ <div
451
+ :id="headerId"
452
+ class="gl-flex-grow-1 gl-font-weight-bold gl-font-sm"
453
+ data-testid="listbox-header-text"
454
+ >
455
+ {{ headerText }}
456
+ </div>
457
+ </div>
436
458
 
437
- <div v-if="searchable" class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-200">
459
+ <div v-if="searchable" :class="$options.HEADER_ITEMS_BORDER_CLASSES">
438
460
  <gl-search-box-by-type
439
461
  ref="searchBox"
440
462
  v-model="searchStr"
@@ -456,7 +478,7 @@ export default {
456
478
  v-if="showList"
457
479
  id="listbox"
458
480
  ref="list"
459
- :aria-labelledby="listAriaLabelledBy || toggleId"
481
+ :aria-labelledby="listAriaLabelledBy || headerId || toggleId"
460
482
  role="listbox"
461
483
  class="gl-new-dropdown-contents gl-list-style-none gl-pl-0 gl-mb-0"
462
484
  tabindex="-1"
@@ -15,12 +15,14 @@ describe('sorting item component', () => {
15
15
  const createComponent = (propsData) => {
16
16
  wrapper = shallowMount(GlSortingItem, {
17
17
  context: {
18
- children: [sortingItemText],
19
18
  props: {
20
19
  ...defaultProps,
21
20
  ...propsData,
22
21
  },
23
22
  },
23
+ slots: {
24
+ default: sortingItemText,
25
+ },
24
26
  stubs: {
25
27
  GlDropdownItem,
26
28
  },
@@ -39,7 +39,7 @@ export default {
39
39
  * The content of the item.
40
40
  * @slot default
41
41
  */
42
- render(createElement, { children, data, props = {} }) {
42
+ render(createElement, { scopedSlots, data, props = {} }) {
43
43
  const classNames = `gl-sorting-item js-active-icon gl-flex-shrink-0 gl-mr-2 ${
44
44
  props.active ? '' : 'inactive gl-visibility-hidden'
45
45
  }`;
@@ -52,16 +52,15 @@ export default {
52
52
  },
53
53
  });
54
54
 
55
- return createElement(
56
- GlDropdownItem,
57
- {
58
- ...data,
59
- attrs: {
60
- ...props,
61
- },
55
+ return createElement(GlDropdownItem, {
56
+ ...data,
57
+ attrs: {
58
+ ...props,
59
+ },
60
+ scopedSlots: {
61
+ default: () => [icon, scopedSlots.default?.()],
62
62
  },
63
- [icon, children]
64
- );
63
+ });
65
64
  },
66
65
  };
67
66
  </script>