@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 +22 -0
- package/dist/components/base/new_dropdowns/disclosure/constants.js +4 -0
- package/dist/components/base/new_dropdowns/disclosure/disclosure_dropdown.js +7 -16
- package/dist/components/base/new_dropdowns/disclosure/disclosure_dropdown_group.js +3 -1
- package/dist/components/base/new_dropdowns/disclosure/disclosure_dropdown_item.js +32 -16
- package/dist/components/base/new_dropdowns/disclosure/utils.js +23 -3
- package/package.json +1 -1
- package/src/components/base/new_dropdowns/disclosure/constants.js +2 -0
- package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.md +4 -15
- package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.spec.js +51 -8
- package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.stories.js +41 -46
- package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.vue +14 -22
- package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown_group.spec.js +5 -2
- package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown_group.vue +7 -3
- package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown_item.spec.js +44 -42
- package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown_item.vue +39 -25
- package/src/components/base/new_dropdowns/disclosure/utils.js +28 -3
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
|
|
|
@@ -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,
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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.
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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
|
|
9
|
-
const
|
|
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 {
|
|
31
|
+
export { hasOnlyListItems, isGroup, isItem, itemsValidator };
|
package/package.json
CHANGED
|
@@ -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
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
220
|
-
it('should render
|
|
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
|
|
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
|
|
233
|
-
buildWrapper({
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
<
|
|
232
|
-
<span class="gl-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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]"
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
173
|
-
if (this.items) {
|
|
174
|
-
|
|
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="
|
|
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
|
-
|
|
323
|
-
|
|
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
|
-
|
|
347
|
-
|
|
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
|
|
44
|
+
it('renders `list-item` content in a `list-item` slot of `GlDisclosureDropdownItem`', () => {
|
|
42
45
|
buildWrapper({
|
|
43
|
-
slots: { 'list-item': '<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
|
|
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
|
|
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(
|
|
57
|
-
expect(findLink().attributes()).
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
143
|
+
describe('when item has wrapperClass', () => {
|
|
144
|
+
const TEST_CLASS = 'just-a-test-class';
|
|
148
145
|
beforeEach(() => {
|
|
149
|
-
buildWrapper({
|
|
146
|
+
buildWrapper({
|
|
147
|
+
item: {
|
|
148
|
+
...mockItems[0],
|
|
149
|
+
wrapperClass: TEST_CLASS,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
150
152
|
});
|
|
151
153
|
|
|
152
|
-
it('should
|
|
153
|
-
expect(
|
|
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
|
|
46
|
+
...item?.extraAttrs,
|
|
45
47
|
type: 'button',
|
|
46
48
|
},
|
|
47
49
|
listeners: {
|
|
48
|
-
click: () =>
|
|
50
|
+
click: () => {
|
|
51
|
+
item?.action?.call(undefined, item);
|
|
52
|
+
this.action();
|
|
53
|
+
},
|
|
49
54
|
},
|
|
50
|
-
wrapperClass: item
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
98
|
+
:class="[$options.ITEM_CLASS, itemComponent.wrapperClass]"
|
|
80
99
|
data-testid="disclosure-dropdown-item"
|
|
81
|
-
|
|
82
|
-
@keydown="onKeydown"
|
|
100
|
+
v-on="wrapperListeners"
|
|
83
101
|
>
|
|
84
|
-
<
|
|
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
|
-
|
|
112
|
+
<slot name="list-item">
|
|
113
|
+
{{ item.text }}
|
|
114
|
+
</slot>
|
|
101
115
|
</span>
|
|
102
116
|
</component>
|
|
103
|
-
</
|
|
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
|
|
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
|
|
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,
|
|
42
|
+
export { itemsValidator, isItem, isGroup, hasOnlyListItems };
|