@gitlab/ui 55.3.0 → 56.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,25 @@
1
+ # [56.0.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v55.3.1...v56.0.0) (2023-02-16)
2
+
3
+
4
+ ### Features
5
+
6
+ * **GlDisclosureDropdown:** wrap custom item content in `button` or `link` ([684386d](https://gitlab.com/gitlab-org/gitlab-ui/commit/684386d4dd66c4ce85538faec1b35b9c5c370926))
7
+
8
+
9
+ ### BREAKING CHANGES
10
+
11
+ * **GlDisclosureDropdown:** It will cause styling and semantic issues
12
+ in the downstream project.
13
+ Wherever the `list-item` slot is used with a `button` or `a` inside
14
+ it wrapping the content, this wrapper `button/a` needs to be removed.
15
+
16
+ ## [55.3.1](https://gitlab.com/gitlab-org/gitlab-ui/compare/v55.3.0...v55.3.1) (2023-02-16)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * **GlDisclosureDropdown:** Improve markup semantics of dropdown wrapper ([79b1922](https://gitlab.com/gitlab-org/gitlab-ui/commit/79b19229c617904cc300a58d54c6ff04b742bb53))
22
+
1
23
  # [55.3.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v55.2.1...v55.3.0) (2023-02-16)
2
24
 
3
25
 
@@ -0,0 +1,4 @@
1
+ const DISCLOSURE_DROPDOWN_ITEM_NAME = 'GlDisclosureDropdownItem';
2
+ const DISCLOSURE_DROPDOWN_GROUP_NAME = 'GlDisclosureDropdownGroup';
3
+
4
+ export { DISCLOSURE_DROPDOWN_GROUP_NAME, DISCLOSURE_DROPDOWN_ITEM_NAME };
@@ -6,7 +6,7 @@ import { buttonCategoryOptions, dropdownVariantOptions, buttonSizeOptions, dropd
6
6
  import GlBaseDropdown from '../base_dropdown/base_dropdown';
7
7
  import GlDisclosureDropdownItem, { ITEM_CLASS } from './disclosure_dropdown_item';
8
8
  import GlDisclosureDropdownGroup from './disclosure_dropdown_group';
9
- import { itemsValidator, isAllItems, isAllGroups, isItem } from './utils';
9
+ import { itemsValidator, hasOnlyListItems, isItem } from './utils';
10
10
  import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
11
11
 
12
12
  var script = {
@@ -157,21 +157,12 @@ var script = {
157
157
  };
158
158
  },
159
159
  computed: {
160
- disclosureOptions() {
161
- if (this.items) {
162
- if (isAllItems(this.items)) {
163
- return {
164
- tag: 'ul'
165
- };
166
- }
167
- if (isAllGroups(this.items)) return {
168
- tag: 'div',
169
- role: 'group'
170
- };
160
+ disclosureTag() {
161
+ var _this$items;
162
+ if ((_this$items = this.items) !== null && _this$items !== void 0 && _this$items.length || hasOnlyListItems(this.$scopedSlots)) {
163
+ return 'ul';
171
164
  }
172
- return {
173
- tag: 'div'
174
- };
165
+ return 'div';
175
166
  },
176
167
  hasCustomToggle() {
177
168
  return Boolean(this.$scopedSlots.toggle);
@@ -265,7 +256,7 @@ var script = {
265
256
  const __vue_script__ = script;
266
257
 
267
258
  /* template */
268
- var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('gl-base-dropdown',{ref:"baseDropdown",staticClass:"gl-disclosure-dropdown",attrs:{"aria-labelledby":_vm.toggleAriaLabelledBy,"toggle-id":_vm.toggleId,"toggle-text":_vm.toggleText,"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,"placement":_vm.placement},on:_vm._d({},[_vm.$options.events.GL_DROPDOWN_SHOWN,_vm.onShow,_vm.$options.events.GL_DROPDOWN_HIDDEN,_vm.onHide]),scopedSlots:_vm._u([(_vm.hasCustomToggle)?{key:"toggle",fn:function(){return [_vm._t("toggle")]},proxy:true}:null],null,true)},[_vm._v(" "),_vm._t("header"),_vm._v(" "),_c(_vm.disclosureOptions.tag,{ref:"content",tag:"component",staticClass:"gl-new-dropdown-contents",attrs:{"id":_vm.disclosureId,"role":_vm.disclosureOptions.role,"aria-labelledby":_vm.listAriaLabelledBy || _vm.toggleId,"data-testid":"disclosure-content","tabindex":"-1"},on:{"keydown":_vm.onKeydown}},[_vm._t("default",function(){return [_vm._l((_vm.items),function(item,index){return [(_vm.isItem(item))?[_c('gl-disclosure-dropdown-item',{key:item.text,attrs:{"item":item},on:{"action":_vm.handleAction}},[_vm._t("list-item",null,{"item":item})],2)]:[_c('gl-disclosure-dropdown-group',{key:item.name,attrs:{"bordered":index !== 0,"group":item},on:{"action":_vm.handleAction},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.$scopedSlots['list-item'])?_vm._l((item.items),function(groupItem){return _c('gl-disclosure-dropdown-item',{key:groupItem.text,attrs:{"item":groupItem},on:{"action":_vm.handleAction}},[_vm._t("list-item",null,{"item":groupItem})],2)}):_vm._e()],2)]]})]})],2),_vm._v(" "),_vm._t("footer")],2)};
259
+ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('gl-base-dropdown',{ref:"baseDropdown",staticClass:"gl-disclosure-dropdown",attrs:{"aria-labelledby":_vm.toggleAriaLabelledBy,"toggle-id":_vm.toggleId,"toggle-text":_vm.toggleText,"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,"placement":_vm.placement},on:_vm._d({},[_vm.$options.events.GL_DROPDOWN_SHOWN,_vm.onShow,_vm.$options.events.GL_DROPDOWN_HIDDEN,_vm.onHide]),scopedSlots:_vm._u([(_vm.hasCustomToggle)?{key:"toggle",fn:function(){return [_vm._t("toggle")]},proxy:true}:null],null,true)},[_vm._v(" "),_vm._t("header"),_vm._v(" "),_c(_vm.disclosureTag,{ref:"content",tag:"component",staticClass:"gl-new-dropdown-contents",attrs:{"id":_vm.disclosureId,"aria-labelledby":_vm.listAriaLabelledBy || _vm.toggleId,"data-testid":"disclosure-content","tabindex":"-1"},on:{"keydown":_vm.onKeydown}},[_vm._t("default",function(){return [_vm._l((_vm.items),function(item,index){return [(_vm.isItem(item))?[_c('gl-disclosure-dropdown-item',{key:item.text,attrs:{"item":item},on:{"action":_vm.handleAction},scopedSlots:_vm._u([{key:"list-item",fn:function(){return [_vm._t("list-item",null,{"item":item})]},proxy:true}],null,true)})]:[_c('gl-disclosure-dropdown-group',{key:item.name,attrs:{"bordered":index !== 0,"group":item},on:{"action":_vm.handleAction},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.$scopedSlots['list-item'])?_vm._l((item.items),function(groupItem){return _c('gl-disclosure-dropdown-item',{key:groupItem.text,attrs:{"item":groupItem},on:{"action":_vm.handleAction},scopedSlots:_vm._u([{key:"list-item",fn:function(){return [_vm._t("list-item",null,{"item":groupItem})]},proxy:true}],null,true)})}):_vm._e()],2)]]})]})],2),_vm._v(" "),_vm._t("footer")],2)};
269
260
  var __vue_staticRenderFns__ = [];
270
261
 
271
262
  /* style */
@@ -1,10 +1,12 @@
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
5
  import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
5
6
 
6
7
  const GROUP_TOP_BORDER_CLASSES = 'gl-border-t gl-pt-2 gl-mt-2';
7
8
  var script = {
9
+ name: DISCLOSURE_DROPDOWN_GROUP_NAME,
8
10
  components: {
9
11
  GlDisclosureDropdownItem
10
12
  },
@@ -54,7 +56,7 @@ var script = {
54
56
  const __vue_script__ = script;
55
57
 
56
58
  /* template */
57
- var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{class:_vm.borderClass},[(_vm.showHeader)?_c('div',{staticClass:"gl-pl-4 gl-py-2 gl-font-sm gl-font-weight-bold",attrs:{"id":_vm.nameId,"aria-hidden":"true"}},[_vm._t("group-label",function(){return [_vm._v(_vm._s(_vm.group.name))]})],2):_vm._e(),_vm._v(" "),_c('ul',{staticClass:"gl-mb-0 gl-pl-0 gl-list-style-none",attrs:{"role":"group","aria-labelledby":_vm.groupLabeledBy}},[_vm._t("default",function(){return _vm._l((_vm.group.items),function(item){return _c('gl-disclosure-dropdown-item',{key:item.text,attrs:{"item":item},on:{"action":_vm.handleAction}},[_vm._t("list-item",null,{"item":item})],2)})})],2)])};
59
+ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('li',{class:_vm.borderClass},[(_vm.showHeader)?_c('div',{staticClass:"gl-pl-4 gl-py-2 gl-font-sm gl-font-weight-bold",attrs:{"id":_vm.nameId,"aria-hidden":"true"}},[_vm._t("group-label",function(){return [_vm._v(_vm._s(_vm.group.name))]})],2):_vm._e(),_vm._v(" "),_c('ul',{staticClass:"gl-mb-0 gl-pl-0 gl-list-style-none",attrs:{"role":"group","aria-labelledby":_vm.groupLabeledBy}},[_vm._t("default",function(){return _vm._l((_vm.group.items),function(item){return _c('gl-disclosure-dropdown-item',{key:item.text,attrs:{"item":item},on:{"action":_vm.handleAction},scopedSlots:_vm._u([{key:"list-item",fn:function(){return [_vm._t("list-item",null,{"item":item})]},proxy:true}],null,true)})})})],2)])};
58
60
  var __vue_staticRenderFns__ = [];
59
61
 
60
62
  /* style */
@@ -1,10 +1,12 @@
1
1
  import { ENTER, SPACE } from '../constants';
2
2
  import { stopEvent } from '../../../../utils/utils';
3
3
  import { isItem } from './utils';
4
+ import { DISCLOSURE_DROPDOWN_ITEM_NAME } from './constants';
4
5
  import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
5
6
 
6
7
  const ITEM_CLASS = 'gl-new-dropdown-item';
7
8
  var script = {
9
+ name: DISCLOSURE_DROPDOWN_ITEM_NAME,
8
10
  ITEM_CLASS,
9
11
  props: {
10
12
  item: {
@@ -26,7 +28,6 @@ var script = {
26
28
  const {
27
29
  item
28
30
  } = this;
29
- if (!item) return null;
30
31
  if (this.isLink) return {
31
32
  is: 'a',
32
33
  attrs: {
@@ -34,22 +35,34 @@ var script = {
34
35
  ...item.extraAttrs
35
36
  },
36
37
  wrapperClass: item.wrapperClass,
37
- listeners: {}
38
+ listeners: {
39
+ click: this.action
40
+ }
38
41
  };
39
42
  return {
40
43
  is: 'button',
41
44
  attrs: {
42
- ...item.extraAttrs,
45
+ ...(item === null || item === void 0 ? void 0 : item.extraAttrs),
43
46
  type: 'button'
44
47
  },
45
48
  listeners: {
46
49
  click: () => {
47
50
  var _item$action;
48
- return (_item$action = item.action) === null || _item$action === void 0 ? void 0 : _item$action.call(undefined, item);
51
+ item === null || item === void 0 ? void 0 : (_item$action = item.action) === null || _item$action === void 0 ? void 0 : _item$action.call(undefined, item);
52
+ this.action();
49
53
  }
50
54
  },
51
- wrapperClass: item.wrapperClass
55
+ wrapperClass: item === null || item === void 0 ? void 0 : item.wrapperClass
56
+ };
57
+ },
58
+ wrapperListeners() {
59
+ const listeners = {
60
+ keydown: this.onKeydown
52
61
  };
62
+ if (this.isCustomContent) {
63
+ listeners.click = this.action;
64
+ }
65
+ return listeners;
53
66
  }
54
67
  },
55
68
  methods: {
@@ -58,17 +71,20 @@ var script = {
58
71
  code
59
72
  } = event;
60
73
  if (code === ENTER || code === SPACE) {
61
- var _this$$refs$item;
62
74
  stopEvent(event);
63
- /** Instead of simply navigating or calling the action, we want
64
- * the `a/button` to be the target of the event as it might have additional attributes.
65
- * E.g. `a` might have `target` attribute.
66
- * `bubbles` is set to `true` as the parent `li` item has this event listener and thus we'll get a loop.
67
- */
68
- (_this$$refs$item = this.$refs.item) === null || _this$$refs$item === void 0 ? void 0 : _this$$refs$item.dispatchEvent(new MouseEvent('click', {
69
- bubbles: false
70
- }));
71
- this.action();
75
+ if (this.isCustomContent) {
76
+ this.action();
77
+ } else {
78
+ var _this$$refs$item;
79
+ /** Instead of simply navigating or calling the action, we want
80
+ * the `a/button` to be the target of the event as it might have additional attributes.
81
+ * E.g. `a` might have `target` attribute.
82
+ */
83
+ (_this$$refs$item = this.$refs.item) === null || _this$$refs$item === void 0 ? void 0 : _this$$refs$item.dispatchEvent(new MouseEvent('click', {
84
+ bubbles: true,
85
+ cancelable: true
86
+ }));
87
+ }
72
88
  }
73
89
  },
74
90
  action() {
@@ -81,7 +97,7 @@ var script = {
81
97
  const __vue_script__ = script;
82
98
 
83
99
  /* template */
84
- var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('li',{class:[_vm.$options.ITEM_CLASS, _vm.itemComponent && _vm.itemComponent.wrapperClass],attrs:{"tabindex":"0","data-testid":"disclosure-dropdown-item"},on:{"click":_vm.action,"keydown":_vm.onKeydown}},[(_vm.isCustomContent)?_c('div',{staticClass:"gl-new-dropdown-item-content"},[_c('div',{staticClass:"gl-new-dropdown-item-text-wrapper"},[_vm._t("default")],2)]):(_vm.itemComponent && _vm.item)?[_c(_vm.itemComponent.is,_vm._g(_vm._b({ref:"item",tag:"component",staticClass:"gl-new-dropdown-item-content",attrs:{"tabindex":"-1"}},'component',_vm.itemComponent.attrs,false),_vm.itemComponent.listeners),[_c('span',{staticClass:"gl-new-dropdown-item-text-wrapper"},[_vm._v("\n "+_vm._s(_vm.item.text)+"\n ")])])]:_vm._e()],2)};
100
+ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('li',_vm._g({class:[_vm.$options.ITEM_CLASS, _vm.itemComponent.wrapperClass],attrs:{"tabindex":"0","data-testid":"disclosure-dropdown-item"}},_vm.wrapperListeners),[_vm._t("default",function(){return [_c(_vm.itemComponent.is,_vm._g(_vm._b({ref:"item",tag:"component",staticClass:"gl-new-dropdown-item-content",attrs:{"tabindex":"-1"}},'component',_vm.itemComponent.attrs,false),_vm.itemComponent.listeners),[_c('span',{staticClass:"gl-new-dropdown-item-text-wrapper"},[_vm._t("list-item",function(){return [_vm._v("\n "+_vm._s(_vm.item.text)+"\n ")]})],2)])]})],2)};
85
101
  var __vue_staticRenderFns__ = [];
86
102
 
87
103
  /* style */
@@ -1,3 +1,6 @@
1
+ import _isFunction from 'lodash/isFunction';
2
+ import { DISCLOSURE_DROPDOWN_ITEM_NAME, DISCLOSURE_DROPDOWN_GROUP_NAME } from './constants';
3
+
1
4
  const itemValidator = item => {
2
5
  var _item$text;
3
6
  return (item === null || item === void 0 ? void 0 : (_item$text = item.text) === null || _item$text === void 0 ? void 0 : _item$text.length) > 0 && !Array.isArray(item === null || item === void 0 ? void 0 : item.items);
@@ -5,7 +8,24 @@ const itemValidator = item => {
5
8
  const isItem = item => Boolean(item) && itemValidator(item);
6
9
  const isGroup = group => Boolean(group) && Array.isArray(group.items) && Boolean(group.items.length) && group.items.every(isItem);
7
10
  const itemsValidator = items => items.every(isItem) || items.every(isGroup);
8
- const isAllItems = items => items.every(isItem);
9
- const isAllGroups = items => items.every(isGroup);
11
+ const isListItem = tag => ['gl-disclosure-dropdown-group', 'gl-disclosure-dropdown-item', 'li'].includes(tag);
12
+ const isValidSlotTagVue2 = vNode => {
13
+ var _vNode$componentOptio;
14
+ return Boolean(vNode) && isListItem(((_vNode$componentOptio = vNode.componentOptions) === null || _vNode$componentOptio === void 0 ? void 0 : _vNode$componentOptio.tag) || vNode.tag);
15
+ };
16
+ const isValidSlotTag = vNode => {
17
+ var _vNode$type;
18
+ return [DISCLOSURE_DROPDOWN_ITEM_NAME, DISCLOSURE_DROPDOWN_GROUP_NAME].includes((_vNode$type = vNode.type) === null || _vNode$type === void 0 ? void 0 : _vNode$type.name) || vNode.type === 'li';
19
+ };
20
+ const hasOnlyListItems = _ref => {
21
+ let {
22
+ default: defaultSlot
23
+ } = _ref;
24
+ if (!_isFunction(defaultSlot)) {
25
+ return false;
26
+ }
27
+ const nodes = defaultSlot();
28
+ return Array.isArray(nodes) && nodes.filter(vNode => vNode.tag).length && (nodes.filter(vNode => vNode.tag).every(isValidSlotTagVue2) || nodes.filter(vNode => vNode.tag).every(isValidSlotTag));
29
+ };
10
30
 
11
- export { isAllGroups, isAllItems, isGroup, isItem, itemsValidator };
31
+ export { hasOnlyListItems, isGroup, isItem, itemsValidator };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "55.3.0",
3
+ "version": "56.0.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,2 @@
1
+ export const DISCLOSURE_DROPDOWN_ITEM_NAME = 'GlDisclosureDropdownItem';
2
+ export const DISCLOSURE_DROPDOWN_GROUP_NAME = 'GlDisclosureDropdownGroup';
@@ -88,25 +88,14 @@ template. If you want to render a custom template for items, use the
88
88
  ```html
89
89
  <gl-disclosure-dropdown :items="items">
90
90
  <template #list-item="{ item }">
91
- <a
92
- class="gl-hover-text-decoration-none gl-text-gray-900"
93
- tabindex="-1"
94
- :href="item.href"
95
- v-bind="item.extraAttrs"
96
- >
97
- {{ item.text }}
98
- <gl-badge v-if="item.count" pill variant="info">{{ item.count }}</gl-badge>
99
- </a>
91
+ <span class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
92
+ {{item.text}}
93
+ <gl-icon v-if="item.icon" :name="item.icon"/>
94
+ </span>
100
95
  </template>
101
96
  </gl-disclosure-dropdown>
102
97
  ```
103
98
 
104
- **Note:** when providing custom content to the item, user should
105
- define the correct tab order inside the disclosure dropdown by setting
106
- the `tabindex` attribute on the elements.
107
- The `li` item will get the focus so you might want elements inside it
108
- not to be focused - this can be done by setting `tabindex="-1"` on them.
109
-
110
99
  #### Groups
111
100
 
112
101
  Actions/links can be contained within groups. A group can have a `name`
@@ -22,6 +22,7 @@ describe('GlDisclosureDropdown', () => {
22
22
  const buildWrapper = (propsData, slots = {}) => {
23
23
  wrapper = mount(GlDisclosureDropdown, {
24
24
  propsData,
25
+ components: { GlDisclosureDropdownItem, GlDisclosureDropdownGroup },
25
26
  slots,
26
27
  attachTo: document.body,
27
28
  });
@@ -216,23 +217,65 @@ describe('GlDisclosureDropdown', () => {
216
217
  });
217
218
  });
218
219
 
219
- describe('disclosure options', () => {
220
- it('should render the `ul` as content tag and not add `role` attribute when it is a list of items only', () => {
220
+ describe('disclosure tag', () => {
221
+ it('should render `ul` as content tag when items is a list of items', () => {
221
222
  buildWrapper({ items: mockItems });
222
223
  expect(findDisclosureContent().element.tagName).toBe('UL');
223
- expect(findDisclosureContent().attributes('role')).toBeUndefined();
224
224
  });
225
225
 
226
- it('should render the `div` as content tag and add `role` attribute when it is a list of groups', () => {
226
+ it('should render `ul` as content tag when items is a list of groups', () => {
227
227
  buildWrapper({ items: mockGroups });
228
+ expect(findDisclosureContent().element.tagName).toBe('UL');
229
+ });
230
+
231
+ it('should render `ul` as content tag when default slot contains only groups', () => {
232
+ const slots = {
233
+ default: `
234
+ <gl-disclosure-dropdown-group>
235
+ <gl-disclosure-dropdown-item>Item</gl-disclosure-dropdown-item>
236
+ <gl-disclosure-dropdown-item>Item</gl-disclosure-dropdown-item>
237
+ </gl-disclosure-dropdown-group>
238
+ <gl-disclosure-dropdown-group>
239
+ <gl-disclosure-dropdown-item>Item</gl-disclosure-dropdown-item>
240
+ </gl-disclosure-dropdown-group>
241
+ `,
242
+ };
243
+
244
+ buildWrapper({}, slots);
245
+ expect(findDisclosureContent().element.tagName).toBe('UL');
246
+ });
247
+
248
+ it('should render `ul` as content tag when default slot contains only items', () => {
249
+ const slots = {
250
+ default: `
251
+ <gl-disclosure-dropdown-item>Item</gl-disclosure-dropdown-item>
252
+ <gl-disclosure-dropdown-item>Item</gl-disclosure-dropdown-item>
253
+ <gl-disclosure-dropdown-item>Item</gl-disclosure-dropdown-item>
254
+ `,
255
+ };
256
+
257
+ buildWrapper({}, slots);
258
+ expect(findDisclosureContent().element.tagName).toBe('UL');
259
+ });
260
+
261
+ it('should render `div` as content tag when default slot does not contain valid list item', () => {
262
+ const slots = {
263
+ default: `
264
+ <div>Item</div>
265
+ <gl-disclosure-dropdown-group>
266
+ <gl-disclosure-dropdown-item>Item</gl-disclosure-dropdown-item>
267
+ <gl-disclosure-dropdown-item>Item</gl-disclosure-dropdown-item>
268
+ </gl-disclosure-dropdown-group>
269
+ `,
270
+ };
271
+
272
+ buildWrapper({}, slots);
228
273
  expect(findDisclosureContent().element.tagName).toBe('DIV');
229
- expect(findDisclosureContent().attributes('role')).toBe('group');
230
274
  });
231
275
 
232
- it('should render the `div` as content tag and NOT add `role` otherwise', () => {
233
- buildWrapper({ items: null }, { default: 'Some other content' });
276
+ it('should render `div` as content tag when slot is not a list item', () => {
277
+ buildWrapper({}, { default: 'Some other content' });
234
278
  expect(findDisclosureContent().element.tagName).toBe('DIV');
235
- expect(findDisclosureContent().attributes('role')).toBeUndefined();
236
279
  });
237
280
  });
238
281
  });
@@ -52,13 +52,16 @@ function openDisclosure(component) {
52
52
  });
53
53
  }
54
54
 
55
- const template = (content, { bindingOverrides = {} } = {}) => `
55
+ const template = (content, { bindingOverrides = {} } = {}, after) => `
56
+ <div>
56
57
  <gl-disclosure-dropdown
57
58
  ref="disclosure"
58
59
  ${makeBindings(bindingOverrides)}
59
60
  >
60
61
  ${content || ''}
61
62
  </gl-disclosure-dropdown>
63
+ ${after || ''}
64
+ </div>
62
65
  `;
63
66
 
64
67
  const TOGGLE_ID = 'custom-toggle-id';
@@ -104,23 +107,17 @@ export const CustomListItem = (args, { argTypes }) => ({
104
107
  openDisclosure(this);
105
108
  }
106
109
  },
107
- methods: {
108
- navigate() {
109
- this.$refs.link.click();
110
- },
111
- },
112
110
  template: template(
113
111
  `
114
112
  <template #list-item="{ item }">
115
- <a tabindex="-1" ref="link" class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-hover-text-gray-900 gl-hover-text-decoration-none gl-text-gray-900" :href="item.href" v-bind="item.extraAttrs">
113
+ <span class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
116
114
  {{ item.text }}
117
115
  <gl-badge pill size="sm" variant="neutral">{{ item.count }}</gl-badge>
118
- </a>
116
+ </span>
119
117
  </template>
120
118
  `,
121
119
  {
122
120
  bindingOverrides: {
123
- '@action': 'navigate',
124
121
  class: 'gl-display-block! gl-text-center',
125
122
  },
126
123
  }
@@ -183,9 +180,6 @@ export const CustomGroupsAndItems = (args, { argTypes }) => ({
183
180
  }
184
181
  },
185
182
  methods: {
186
- navigate() {
187
- this.$refs.link.click();
188
- },
189
183
  getTotalMrs(items) {
190
184
  return items.reduce((acc, item) => acc + item.count, 0);
191
185
  },
@@ -196,17 +190,12 @@ export const CustomGroupsAndItems = (args, { argTypes }) => ({
196
190
  {{ group.name }} <gl-badge pill size="sm" variant="neutral">{{ getTotalMrs(group.items) }}</gl-badge>
197
191
  </template>
198
192
  <template #list-item="{ item }">
199
- <a tabindex="-1" ref="link" class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-hover-text-gray-900 gl-hover-text-decoration-none gl-text-gray-900" :href="item.href" v-bind="item.extraAttrs">
193
+ <span class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
200
194
  {{ item.text }}
201
195
  <gl-badge pill size="sm" variant="neutral">{{ item.count }}</gl-badge>
202
- </a>
196
+ </span>
203
197
  </template>
204
- `,
205
- {
206
- bindingOverrides: {
207
- '@action': 'navigate',
208
- },
209
- }
198
+ `
210
199
  ),
211
200
  });
212
201
 
@@ -217,34 +206,30 @@ CustomGroupsAndItems.args = {
217
206
  CustomGroupsAndItems.decorators = [makeContainer({ height: '200px' })];
218
207
 
219
208
  export const CustomGroupsItemsAndToggle = makeGroupedExample({
220
- template: template(`
221
- <template #toggle>
222
- <span class="gl-sr-only">
223
- Orange Fox user's menu
224
- </span>
225
- <gl-avatar :size="32" entity-name="Orange Fox" aria-hidden="true"></gl-avatar>
226
- </template>
227
-
228
- <div role="group">
209
+ template: template(
210
+ `
211
+ <template #toggle>
212
+ <span class="gl-sr-only">
213
+ Orange Fox user's menu
214
+ </span>
215
+ <gl-avatar :size="32" entity-name="Orange Fox" aria-hidden="true"></gl-avatar>
216
+ </template>
229
217
  <gl-disclosure-dropdown-group>
230
218
  <gl-disclosure-dropdown-item>
231
- <span class="gl-display-flex gl-flex-direction-column">
232
- <span class="gl-font-weight-bold gl-white-space-nowrap">Orange Fox</span>
233
- <span class="gl-text-gray-400">@thefox</span>
234
- </span>
219
+ <template #list-item>
220
+ <span class="gl-display-flex gl-flex-direction-column">
221
+ <span class="gl-font-weight-bold gl-white-space-nowrap">Orange Fox</span>
222
+ <span class="gl-text-gray-400">@thefox</span>
223
+ </span>
224
+ </template>
235
225
  </gl-disclosure-dropdown-item>
236
226
  </gl-disclosure-dropdown-group>
237
227
  <gl-disclosure-dropdown-group bordered :group="$options.groups[0]">
238
228
  <template #list-item="{ item }">
239
- <a
240
- class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-hover-text-gray-900 gl-hover-text-decoration-none gl-text-gray-900"
241
- :href="item.href"
242
- tabindex="-1"
243
- v-bind="item.extraAttrs"
244
- >
229
+ <span class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
245
230
  {{item.text}}
246
231
  <gl-icon v-if="item.icon" :name="item.icon"/>
247
- </a>
232
+ </span>
248
233
  </template>
249
234
  </gl-disclosure-dropdown-group>
250
235
  <gl-disclosure-dropdown-group bordered>
@@ -252,19 +237,26 @@ export const CustomGroupsItemsAndToggle = makeGroupedExample({
252
237
  <span class="gl-font-sm">Navigation redesign</span>
253
238
  <gl-badge size="sm" variant="info">Beta</gl-badge>
254
239
  </template>
255
- <gl-disclosure-dropdown-item>
256
- <gl-toggle label="New navigation" label-position="left" v-model="newNavigation"/>
240
+ <gl-disclosure-dropdown-item @action="toggleNewNavigation">
241
+ <div class="gl-new-dropdown-item-content">
242
+ <div class="gl-new-dropdown-item-text-wrapper">
243
+ <gl-toggle label="New navigation" label-position="left" :value="newNavigation"/>
244
+ </div>
245
+ </div>
257
246
  </gl-disclosure-dropdown-item>
258
247
  <gl-disclosure-dropdown-item @action="toggleModalVisibility(true)">
259
- <a>Provide feedback</a>
248
+ <template #list-item>Provide feedback</template>
260
249
  </gl-disclosure-dropdown-item>
261
250
  </gl-disclosure-dropdown-group>
262
- <gl-disclosure-dropdown-group bordered :group="$options.groups[1]"> </gl-disclosure-dropdown-group>
251
+ <gl-disclosure-dropdown-group bordered :group="$options.groups[1]"/>
252
+ `,
253
+ {},
254
+ `
263
255
  <gl-modal :visible="feedBackModalVisible" @change="toggleModalVisibility" modal-id="feedbackModal" size="sm">
264
256
  <textarea class="gl-w-full">Tell us what you think!</textarea>
265
257
  </gl-modal>
266
- </div>
267
- `),
258
+ `
259
+ ),
268
260
  data() {
269
261
  return {
270
262
  newNavigation: true,
@@ -275,6 +267,9 @@ export const CustomGroupsItemsAndToggle = makeGroupedExample({
275
267
  toggleModalVisibility(value) {
276
268
  this.feedBackModalVisible = value;
277
269
  },
270
+ toggleNewNavigation() {
271
+ this.newNavigation = !this.newNavigation;
272
+ },
278
273
  },
279
274
  groups: mockProfileGroups,
280
275
  });
@@ -19,7 +19,7 @@ import {
19
19
  import GlBaseDropdown from '../base_dropdown/base_dropdown.vue';
20
20
  import GlDisclosureDropdownItem, { ITEM_CLASS } from './disclosure_dropdown_item.vue';
21
21
  import GlDisclosureDropdownGroup from './disclosure_dropdown_group.vue';
22
- import { itemsValidator, isItem, isAllItems, isAllGroups } from './utils';
22
+ import { itemsValidator, isItem, hasOnlyListItems } from './utils';
23
23
 
24
24
  export default {
25
25
  events: {
@@ -169,22 +169,11 @@ export default {
169
169
  };
170
170
  },
171
171
  computed: {
172
- disclosureOptions() {
173
- if (this.items) {
174
- if (isAllItems(this.items)) {
175
- return {
176
- tag: 'ul',
177
- };
178
- }
179
-
180
- if (isAllGroups(this.items))
181
- return {
182
- tag: 'div',
183
- role: 'group',
184
- };
172
+ disclosureTag() {
173
+ if (this.items?.length || hasOnlyListItems(this.$scopedSlots)) {
174
+ return 'ul';
185
175
  }
186
-
187
- return { tag: 'div' };
176
+ return 'div';
188
177
  },
189
178
  hasCustomToggle() {
190
179
  return Boolean(this.$scopedSlots.toggle);
@@ -305,10 +294,9 @@ export default {
305
294
  <slot name="header"></slot>
306
295
 
307
296
  <component
308
- :is="disclosureOptions.tag"
297
+ :is="disclosureTag"
309
298
  :id="disclosureId"
310
299
  ref="content"
311
- :role="disclosureOptions.role"
312
300
  :aria-labelledby="listAriaLabelledBy || toggleId"
313
301
  data-testid="disclosure-content"
314
302
  class="gl-new-dropdown-contents"
@@ -319,8 +307,10 @@ export default {
319
307
  <template v-for="(item, index) in items">
320
308
  <template v-if="isItem(item)">
321
309
  <gl-disclosure-dropdown-item :key="item.text" :item="item" @action="handleAction">
322
- <!-- @slot Custom template of the disclosure dropdown item -->
323
- <slot name="list-item" :item="item"></slot>
310
+ <template #list-item>
311
+ <!-- @slot Custom template of the disclosure dropdown item -->
312
+ <slot name="list-item" :item="item"></slot>
313
+ </template>
324
314
  </gl-disclosure-dropdown-item>
325
315
  </template>
326
316
 
@@ -343,8 +333,10 @@ export default {
343
333
  :item="groupItem"
344
334
  @action="handleAction"
345
335
  >
346
- <!-- @slot Custom template of the disclosure dropdown item -->
347
- <slot name="list-item" :item="groupItem"></slot>
336
+ <template #list-item>
337
+ <!-- @slot Custom template of the disclosure dropdown item -->
338
+ <slot name="list-item" :item="groupItem"></slot>
339
+ </template>
348
340
  </gl-disclosure-dropdown-item>
349
341
  </template>
350
342
  </gl-disclosure-dropdown-group>
@@ -14,6 +14,9 @@ describe('GlDisclosureDropdownGroup', () => {
14
14
  group: mockGroups[0],
15
15
  ...propsData,
16
16
  },
17
+ stubs: {
18
+ GlDisclosureDropdownItem,
19
+ },
17
20
  slots,
18
21
  });
19
22
  };
@@ -38,9 +41,9 @@ describe('GlDisclosureDropdownGroup', () => {
38
41
  expect(findItems()).toHaveLength(mockGroups[0].items.length);
39
42
  });
40
43
 
41
- it('renders `list-item` content in a default slot of `GlDisclosureDropdownItem`', () => {
44
+ it('renders `list-item` content in a `list-item` slot of `GlDisclosureDropdownItem`', () => {
42
45
  buildWrapper({
43
- slots: { 'list-item': '<li data-testid="list-item-content"></li>' },
46
+ slots: { 'list-item': '<span data-testid="list-item-content"></span>' },
44
47
  });
45
48
 
46
49
  expect(findItems()).toHaveLength(mockGroups[0].items.length);
@@ -2,10 +2,12 @@
2
2
  import { uniqueId } from 'lodash';
3
3
  import GlDisclosureDropdownItem from './disclosure_dropdown_item.vue';
4
4
  import { isGroup } from './utils';
5
+ import { DISCLOSURE_DROPDOWN_GROUP_NAME } from './constants';
5
6
 
6
7
  export const GROUP_TOP_BORDER_CLASSES = 'gl-border-t gl-pt-2 gl-mt-2';
7
8
 
8
9
  export default {
10
+ name: DISCLOSURE_DROPDOWN_GROUP_NAME,
9
11
  components: {
10
12
  GlDisclosureDropdownItem,
11
13
  },
@@ -52,7 +54,7 @@ export default {
52
54
  </script>
53
55
 
54
56
  <template>
55
- <div :class="borderClass">
57
+ <li :class="borderClass">
56
58
  <div
57
59
  v-if="showHeader"
58
60
  :id="nameId"
@@ -69,9 +71,11 @@ export default {
69
71
  :item="item"
70
72
  @action="handleAction"
71
73
  >
72
- <slot name="list-item" :item="item"> </slot>
74
+ <template #list-item>
75
+ <slot name="list-item" :item="item"></slot>
76
+ </template>
73
77
  </gl-disclosure-dropdown-item>
74
78
  </slot>
75
79
  </ul>
76
- </div>
80
+ </li>
77
81
  </template>
@@ -37,13 +37,17 @@ describe('GlDisclosureDropdownItem', () => {
37
37
  ${() => findItem().trigger('keydown', { code: SPACE })} | ${'SPACE'}
38
38
  `(`$event should emit 'action' event`, ({ trigger }) => {
39
39
  trigger();
40
- expect(wrapper.emitted('action')).toEqual([[item]]);
40
+ const emittedAction = wrapper.emitted('action');
41
+ expect(emittedAction).toHaveLength(1);
42
+ expect(emittedAction).toEqual([[item]]);
41
43
  });
42
44
  });
43
45
 
44
46
  describe('when item has a `href`', () => {
47
+ const item = mockItems[0];
48
+
45
49
  beforeEach(() => {
46
- buildWrapper({ item: clone(mockItems[0]) });
50
+ buildWrapper({ item });
47
51
  });
48
52
 
49
53
  const findLink = () => wrapper.find('a.gl-new-dropdown-item-content');
@@ -53,28 +57,30 @@ describe('GlDisclosureDropdownItem', () => {
53
57
  });
54
58
 
55
59
  it('should set correct attributes', () => {
56
- expect(findLink().attributes('href')).toBe(mockItems[0].href);
57
- expect(findLink().attributes()).toMatchObject(mockItems[0].extraAttrs);
60
+ expect(findLink().attributes('href')).toBe(item.href);
61
+ expect(findLink().attributes()).toEqual(expect.objectContaining(item.extraAttrs));
58
62
  });
59
63
 
60
64
  it('should apply the default classes to the item wrapper', () => {
61
65
  expect(findItem().classes()).toEqual(['gl-new-dropdown-item']);
62
66
  });
63
67
 
64
- describe('when item has wrapperClass', () => {
65
- const TEST_CLASS = 'just-a-test-class';
66
- beforeEach(() => {
67
- buildWrapper({
68
- item: {
69
- ...mockItems[0],
70
- wrapperClass: TEST_CLASS,
71
- },
72
- });
73
- });
68
+ it('should emit `action` on `click`', () => {
69
+ findLink().trigger('click');
70
+ const emittedAction = wrapper.emitted('action');
71
+ expect(emittedAction).toHaveLength(1);
72
+ expect(emittedAction).toEqual([[item]]);
73
+ });
74
74
 
75
- it('should add the extra class to the item wrapper', () => {
76
- expect(findItem().classes()).toContain(TEST_CLASS);
77
- });
75
+ it.each`
76
+ trigger | event
77
+ ${() => findItem().trigger('keydown', { code: ENTER })} | ${'ENTER'}
78
+ ${() => findItem().trigger('keydown', { code: SPACE })} | ${'SPACE'}
79
+ `(`$event on parent will execute 'action' event`, ({ trigger }) => {
80
+ trigger();
81
+ const emittedAction = wrapper.emitted('action');
82
+ expect(emittedAction).toHaveLength(1);
83
+ expect(emittedAction).toEqual([[item]]);
78
84
  });
79
85
  });
80
86
 
@@ -100,7 +106,7 @@ describe('GlDisclosureDropdownItem', () => {
100
106
  expect(findButton().attributes()).toMatchObject(attrs);
101
107
  });
102
108
 
103
- it('should call `action` on `click`', () => {
109
+ it('should call `action` on `click` and emit `action` event', () => {
104
110
  findButton().trigger('click');
105
111
  expect(action).toHaveBeenCalledTimes(1);
106
112
 
@@ -110,47 +116,43 @@ describe('GlDisclosureDropdownItem', () => {
110
116
  const actionArgs = action.mock.calls[0];
111
117
  expect(actionArgs).toEqual([item]);
112
118
 
113
- expect(wrapper.emitted('action')).toEqual([[item]]);
119
+ const emittedAction = wrapper.emitted('action');
120
+ expect(emittedAction).toHaveLength(1);
121
+ expect(emittedAction).toEqual([[item]]);
114
122
  });
115
123
 
116
124
  it.each`
117
125
  trigger | event
118
- ${() => findItem().trigger('click')} | ${'click'}
119
126
  ${() => findItem().trigger('keydown', { code: ENTER })} | ${'ENTER'}
120
127
  ${() => findItem().trigger('keydown', { code: SPACE })} | ${'SPACE'}
121
- `(`$event will execute action and emit 'action' event`, ({ trigger }) => {
128
+ `(`$event on parent will execute action and emit 'action' event`, ({ trigger }) => {
122
129
  trigger();
123
- expect(wrapper.emitted('action')).toEqual([[item]]);
130
+ expect(action).toHaveBeenCalledTimes(1);
131
+ expect(action.mock.calls[0]).toEqual([item]);
132
+
133
+ const emittedAction = wrapper.emitted('action');
134
+ expect(emittedAction).toHaveLength(1);
135
+ expect(emittedAction).toEqual([[item]]);
124
136
  });
125
137
 
126
138
  it('should apply the default classes to the item wrapper', () => {
127
139
  expect(findItem().classes()).toEqual(['gl-new-dropdown-item']);
128
140
  });
129
-
130
- describe('when item has wrapperClass', () => {
131
- const TEST_CLASS = 'just-a-test-class';
132
- beforeEach(() => {
133
- buildWrapper({
134
- item: {
135
- ...mockItems[1],
136
- wrapperClass: TEST_CLASS,
137
- },
138
- });
139
- });
140
-
141
- it('should add the extra class to the item wrapper', () => {
142
- expect(findItem().classes()).toContain(TEST_CLASS);
143
- });
144
- });
145
141
  });
146
142
 
147
- describe('when item is null', () => {
143
+ describe('when item has wrapperClass', () => {
144
+ const TEST_CLASS = 'just-a-test-class';
148
145
  beforeEach(() => {
149
- buildWrapper({ item: null });
146
+ buildWrapper({
147
+ item: {
148
+ ...mockItems[0],
149
+ wrapperClass: TEST_CLASS,
150
+ },
151
+ });
150
152
  });
151
153
 
152
- it('should not render anything', () => {
153
- expect(wrapper.text()).toBe('');
154
+ it('should add the extra class to the item wrapper', () => {
155
+ expect(findItem().classes()).toContain(TEST_CLASS);
154
156
  });
155
157
  });
156
158
  });
@@ -2,10 +2,12 @@
2
2
  import { ENTER, SPACE } from '../constants';
3
3
  import { stopEvent } from '../../../../utils/utils';
4
4
  import { isItem } from './utils';
5
+ import { DISCLOSURE_DROPDOWN_ITEM_NAME } from './constants';
5
6
 
6
7
  export const ITEM_CLASS = 'gl-new-dropdown-item';
7
8
 
8
9
  export default {
10
+ name: DISCLOSURE_DROPDOWN_ITEM_NAME,
9
11
  ITEM_CLASS,
10
12
  props: {
11
13
  item: {
@@ -25,8 +27,6 @@ export default {
25
27
  itemComponent() {
26
28
  const { item } = this;
27
29
 
28
- if (!item) return null;
29
-
30
30
  if (this.isLink)
31
31
  return {
32
32
  is: 'a',
@@ -35,20 +35,34 @@ export default {
35
35
  ...item.extraAttrs,
36
36
  },
37
37
  wrapperClass: item.wrapperClass,
38
- listeners: {},
38
+ listeners: {
39
+ click: this.action,
40
+ },
39
41
  };
40
42
 
41
43
  return {
42
44
  is: 'button',
43
45
  attrs: {
44
- ...item.extraAttrs,
46
+ ...item?.extraAttrs,
45
47
  type: 'button',
46
48
  },
47
49
  listeners: {
48
- click: () => item.action?.call(undefined, item),
50
+ click: () => {
51
+ item?.action?.call(undefined, item);
52
+ this.action();
53
+ },
49
54
  },
50
- wrapperClass: item.wrapperClass,
55
+ wrapperClass: item?.wrapperClass,
56
+ };
57
+ },
58
+ wrapperListeners() {
59
+ const listeners = {
60
+ keydown: this.onKeydown,
51
61
  };
62
+ if (this.isCustomContent) {
63
+ listeners.click = this.action;
64
+ }
65
+ return listeners;
52
66
  },
53
67
  },
54
68
  methods: {
@@ -57,13 +71,18 @@ export default {
57
71
 
58
72
  if (code === ENTER || code === SPACE) {
59
73
  stopEvent(event);
60
- /** Instead of simply navigating or calling the action, we want
61
- * the `a/button` to be the target of the event as it might have additional attributes.
62
- * E.g. `a` might have `target` attribute.
63
- * `bubbles` is set to `true` as the parent `li` item has this event listener and thus we'll get a loop.
64
- */
65
- this.$refs.item?.dispatchEvent(new MouseEvent('click', { bubbles: false }));
66
- this.action();
74
+
75
+ if (this.isCustomContent) {
76
+ this.action();
77
+ } else {
78
+ /** Instead of simply navigating or calling the action, we want
79
+ * the `a/button` to be the target of the event as it might have additional attributes.
80
+ * E.g. `a` might have `target` attribute.
81
+ */
82
+ this.$refs.item?.dispatchEvent(
83
+ new MouseEvent('click', { bubbles: true, cancelable: true })
84
+ );
85
+ }
67
86
  }
68
87
  },
69
88
  action() {
@@ -76,18 +95,11 @@ export default {
76
95
  <template>
77
96
  <li
78
97
  tabindex="0"
79
- :class="[$options.ITEM_CLASS, itemComponent && itemComponent.wrapperClass]"
98
+ :class="[$options.ITEM_CLASS, itemComponent.wrapperClass]"
80
99
  data-testid="disclosure-dropdown-item"
81
- @click="action"
82
- @keydown="onKeydown"
100
+ v-on="wrapperListeners"
83
101
  >
84
- <div v-if="isCustomContent" class="gl-new-dropdown-item-content">
85
- <div class="gl-new-dropdown-item-text-wrapper">
86
- <slot></slot>
87
- </div>
88
- </div>
89
-
90
- <template v-else-if="itemComponent && item">
102
+ <slot>
91
103
  <component
92
104
  :is="itemComponent.is"
93
105
  v-bind="itemComponent.attrs"
@@ -97,9 +109,11 @@ export default {
97
109
  v-on="itemComponent.listeners"
98
110
  >
99
111
  <span class="gl-new-dropdown-item-text-wrapper">
100
- {{ item.text }}
112
+ <slot name="list-item">
113
+ {{ item.text }}
114
+ </slot>
101
115
  </span>
102
116
  </component>
103
- </template>
117
+ </slot>
104
118
  </li>
105
119
  </template>
@@ -1,3 +1,6 @@
1
+ import { isFunction } from 'lodash';
2
+ import { DISCLOSURE_DROPDOWN_ITEM_NAME, DISCLOSURE_DROPDOWN_GROUP_NAME } from './constants';
3
+
1
4
  const itemValidator = (item) => item?.text?.length > 0 && !Array.isArray(item?.items);
2
5
 
3
6
  const isItem = (item) => Boolean(item) && itemValidator(item);
@@ -10,8 +13,30 @@ const isGroup = (group) =>
10
13
 
11
14
  const itemsValidator = (items) => items.every(isItem) || items.every(isGroup);
12
15
 
13
- const isAllItems = (items) => items.every(isItem);
16
+ const isListItem = (tag) =>
17
+ ['gl-disclosure-dropdown-group', 'gl-disclosure-dropdown-item', 'li'].includes(tag);
18
+
19
+ const isValidSlotTagVue2 = (vNode) =>
20
+ Boolean(vNode) && isListItem(vNode.componentOptions?.tag || vNode.tag);
21
+
22
+ const isValidSlotTag = (vNode) => {
23
+ return (
24
+ [DISCLOSURE_DROPDOWN_ITEM_NAME, DISCLOSURE_DROPDOWN_GROUP_NAME].includes(vNode.type?.name) ||
25
+ vNode.type === 'li'
26
+ );
27
+ };
14
28
 
15
- const isAllGroups = (items) => items.every(isGroup);
29
+ const hasOnlyListItems = ({ default: defaultSlot }) => {
30
+ if (!isFunction(defaultSlot)) {
31
+ return false;
32
+ }
33
+ const nodes = defaultSlot();
34
+ return (
35
+ Array.isArray(nodes) &&
36
+ nodes.filter((vNode) => vNode.tag).length &&
37
+ (nodes.filter((vNode) => vNode.tag).every(isValidSlotTagVue2) ||
38
+ nodes.filter((vNode) => vNode.tag).every(isValidSlotTag))
39
+ );
40
+ };
16
41
 
17
- export { itemsValidator, isItem, isGroup, isAllItems, isAllGroups };
42
+ export { itemsValidator, isItem, isGroup, hasOnlyListItems };