@gitlab/ui 55.3.1 → 56.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ ## [56.0.1](https://gitlab.com/gitlab-org/gitlab-ui/compare/v56.0.0...v56.0.1) (2023-02-17)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **deps:** update dependency dompurify to ^2.4.4 ([c90c17b](https://gitlab.com/gitlab-org/gitlab-ui/commit/c90c17b307da010cacb566800f9cf5922124f28e))
7
+
8
+ # [56.0.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v55.3.1...v56.0.0) (2023-02-16)
9
+
10
+
11
+ ### Features
12
+
13
+ * **GlDisclosureDropdown:** wrap custom item content in `button` or `link` ([684386d](https://gitlab.com/gitlab-org/gitlab-ui/commit/684386d4dd66c4ce85538faec1b35b9c5c370926))
14
+
15
+
16
+ ### BREAKING CHANGES
17
+
18
+ * **GlDisclosureDropdown:** It will cause styling and semantic issues
19
+ in the downstream project.
20
+ Wherever the `list-item` slot is used with a `button` or `a` inside
21
+ it wrapping the content, this wrapper `button/a` needs to be removed.
22
+
1
23
  ## [55.3.1](https://gitlab.com/gitlab-org/gitlab-ui/compare/v55.3.0...v55.3.1) (2023-02-16)
2
24
 
3
25
 
@@ -256,7 +256,7 @@ var script = {
256
256
  const __vue_script__ = script;
257
257
 
258
258
  /* template */
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}},[_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)};
260
260
  var __vue_staticRenderFns__ = [];
261
261
 
262
262
  /* style */
@@ -56,7 +56,7 @@ var script = {
56
56
  const __vue_script__ = script;
57
57
 
58
58
  /* template */
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}},[_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)])};
60
60
  var __vue_staticRenderFns__ = [];
61
61
 
62
62
  /* style */
@@ -28,7 +28,6 @@ var script = {
28
28
  const {
29
29
  item
30
30
  } = this;
31
- if (!item) return null;
32
31
  if (this.isLink) return {
33
32
  is: 'a',
34
33
  attrs: {
@@ -36,22 +35,34 @@ var script = {
36
35
  ...item.extraAttrs
37
36
  },
38
37
  wrapperClass: item.wrapperClass,
39
- listeners: {}
38
+ listeners: {
39
+ click: this.action
40
+ }
40
41
  };
41
42
  return {
42
43
  is: 'button',
43
44
  attrs: {
44
- ...item.extraAttrs,
45
+ ...(item === null || item === void 0 ? void 0 : item.extraAttrs),
45
46
  type: 'button'
46
47
  },
47
48
  listeners: {
48
49
  click: () => {
49
50
  var _item$action;
50
- 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();
51
53
  }
52
54
  },
53
- 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
54
61
  };
62
+ if (this.isCustomContent) {
63
+ listeners.click = this.action;
64
+ }
65
+ return listeners;
55
66
  }
56
67
  },
57
68
  methods: {
@@ -60,17 +71,20 @@ var script = {
60
71
  code
61
72
  } = event;
62
73
  if (code === ENTER || code === SPACE) {
63
- var _this$$refs$item;
64
74
  stopEvent(event);
65
- /** Instead of simply navigating or calling the action, we want
66
- * the `a/button` to be the target of the event as it might have additional attributes.
67
- * E.g. `a` might have `target` attribute.
68
- * `bubbles` is set to `true` as the parent `li` item has this event listener and thus we'll get a loop.
69
- */
70
- (_this$$refs$item = this.$refs.item) === null || _this$$refs$item === void 0 ? void 0 : _this$$refs$item.dispatchEvent(new MouseEvent('click', {
71
- bubbles: false
72
- }));
73
- 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
+ }
74
88
  }
75
89
  },
76
90
  action() {
@@ -83,7 +97,7 @@ var script = {
83
97
  const __vue_script__ = script;
84
98
 
85
99
  /* template */
86
- 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)};
87
101
  var __vue_staticRenderFns__ = [];
88
102
 
89
103
  /* style */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "55.3.1",
3
+ "version": "56.0.1",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -62,7 +62,7 @@
62
62
  "dependencies": {
63
63
  "@popperjs/core": "^2.11.2",
64
64
  "bootstrap-vue": "2.20.1",
65
- "dompurify": "^2.4.3",
65
+ "dompurify": "^2.4.4",
66
66
  "echarts": "^5.3.2",
67
67
  "iframe-resizer": "^4.3.2",
68
68
  "lodash": "^4.17.20",
@@ -115,7 +115,7 @@
115
115
  "bootstrap-vue-vue3": "npm:bootstrap-vue@2.23.1",
116
116
  "cypress": "^11.2.0",
117
117
  "emoji-regex": "^10.0.0",
118
- "eslint": "8.32.0",
118
+ "eslint": "8.34.0",
119
119
  "eslint-import-resolver-jest": "3.0.2",
120
120
  "eslint-plugin-cypress": "2.12.1",
121
121
  "eslint-plugin-storybook": "0.6.10",
@@ -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`
@@ -107,23 +107,17 @@ export const CustomListItem = (args, { argTypes }) => ({
107
107
  openDisclosure(this);
108
108
  }
109
109
  },
110
- methods: {
111
- navigate() {
112
- this.$refs.link.click();
113
- },
114
- },
115
110
  template: template(
116
111
  `
117
112
  <template #list-item="{ item }">
118
- <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">
119
114
  {{ item.text }}
120
115
  <gl-badge pill size="sm" variant="neutral">{{ item.count }}</gl-badge>
121
- </a>
116
+ </span>
122
117
  </template>
123
118
  `,
124
119
  {
125
120
  bindingOverrides: {
126
- '@action': 'navigate',
127
121
  class: 'gl-display-block! gl-text-center',
128
122
  },
129
123
  }
@@ -186,9 +180,6 @@ export const CustomGroupsAndItems = (args, { argTypes }) => ({
186
180
  }
187
181
  },
188
182
  methods: {
189
- navigate() {
190
- this.$refs.link.click();
191
- },
192
183
  getTotalMrs(items) {
193
184
  return items.reduce((acc, item) => acc + item.count, 0);
194
185
  },
@@ -199,17 +190,12 @@ export const CustomGroupsAndItems = (args, { argTypes }) => ({
199
190
  {{ group.name }} <gl-badge pill size="sm" variant="neutral">{{ getTotalMrs(group.items) }}</gl-badge>
200
191
  </template>
201
192
  <template #list-item="{ item }">
202
- <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">
203
194
  {{ item.text }}
204
195
  <gl-badge pill size="sm" variant="neutral">{{ item.count }}</gl-badge>
205
- </a>
196
+ </span>
206
197
  </template>
207
- `,
208
- {
209
- bindingOverrides: {
210
- '@action': 'navigate',
211
- },
212
- }
198
+ `
213
199
  ),
214
200
  });
215
201
 
@@ -230,23 +216,20 @@ export const CustomGroupsItemsAndToggle = makeGroupedExample({
230
216
  </template>
231
217
  <gl-disclosure-dropdown-group>
232
218
  <gl-disclosure-dropdown-item>
233
- <span class="gl-display-flex gl-flex-direction-column">
234
- <span class="gl-font-weight-bold gl-white-space-nowrap">Orange Fox</span>
235
- <span class="gl-text-gray-400">@thefox</span>
236
- </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>
237
225
  </gl-disclosure-dropdown-item>
238
226
  </gl-disclosure-dropdown-group>
239
227
  <gl-disclosure-dropdown-group bordered :group="$options.groups[0]">
240
228
  <template #list-item="{ item }">
241
- <a
242
- 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"
243
- :href="item.href"
244
- tabindex="-1"
245
- v-bind="item.extraAttrs"
246
- >
229
+ <span class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
247
230
  {{item.text}}
248
231
  <gl-icon v-if="item.icon" :name="item.icon"/>
249
- </a>
232
+ </span>
250
233
  </template>
251
234
  </gl-disclosure-dropdown-group>
252
235
  <gl-disclosure-dropdown-group bordered>
@@ -254,14 +237,18 @@ export const CustomGroupsItemsAndToggle = makeGroupedExample({
254
237
  <span class="gl-font-sm">Navigation redesign</span>
255
238
  <gl-badge size="sm" variant="info">Beta</gl-badge>
256
239
  </template>
257
- <gl-disclosure-dropdown-item>
258
- <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>
259
246
  </gl-disclosure-dropdown-item>
260
247
  <gl-disclosure-dropdown-item @action="toggleModalVisibility(true)">
261
- Provide feedback
248
+ <template #list-item>Provide feedback</template>
262
249
  </gl-disclosure-dropdown-item>
263
250
  </gl-disclosure-dropdown-group>
264
- <gl-disclosure-dropdown-group bordered :group="$options.groups[1]"> </gl-disclosure-dropdown-group>
251
+ <gl-disclosure-dropdown-group bordered :group="$options.groups[1]"/>
265
252
  `,
266
253
  {},
267
254
  `
@@ -280,6 +267,9 @@ export const CustomGroupsItemsAndToggle = makeGroupedExample({
280
267
  toggleModalVisibility(value) {
281
268
  this.feedBackModalVisible = value;
282
269
  },
270
+ toggleNewNavigation() {
271
+ this.newNavigation = !this.newNavigation;
272
+ },
283
273
  },
284
274
  groups: mockProfileGroups,
285
275
  });
@@ -307,8 +307,10 @@ export default {
307
307
  <template v-for="(item, index) in items">
308
308
  <template v-if="isItem(item)">
309
309
  <gl-disclosure-dropdown-item :key="item.text" :item="item" @action="handleAction">
310
- <!-- @slot Custom template of the disclosure dropdown item -->
311
- <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>
312
314
  </gl-disclosure-dropdown-item>
313
315
  </template>
314
316
 
@@ -331,8 +333,10 @@ export default {
331
333
  :item="groupItem"
332
334
  @action="handleAction"
333
335
  >
334
- <!-- @slot Custom template of the disclosure dropdown item -->
335
- <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>
336
340
  </gl-disclosure-dropdown-item>
337
341
  </template>
338
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);
@@ -71,7 +71,9 @@ export default {
71
71
  :item="item"
72
72
  @action="handleAction"
73
73
  >
74
- <slot name="list-item" :item="item"> </slot>
74
+ <template #list-item>
75
+ <slot name="list-item" :item="item"></slot>
76
+ </template>
75
77
  </gl-disclosure-dropdown-item>
76
78
  </slot>
77
79
  </ul>
@@ -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
  });
@@ -27,8 +27,6 @@ export default {
27
27
  itemComponent() {
28
28
  const { item } = this;
29
29
 
30
- if (!item) return null;
31
-
32
30
  if (this.isLink)
33
31
  return {
34
32
  is: 'a',
@@ -37,20 +35,34 @@ export default {
37
35
  ...item.extraAttrs,
38
36
  },
39
37
  wrapperClass: item.wrapperClass,
40
- listeners: {},
38
+ listeners: {
39
+ click: this.action,
40
+ },
41
41
  };
42
42
 
43
43
  return {
44
44
  is: 'button',
45
45
  attrs: {
46
- ...item.extraAttrs,
46
+ ...item?.extraAttrs,
47
47
  type: 'button',
48
48
  },
49
49
  listeners: {
50
- click: () => item.action?.call(undefined, item),
50
+ click: () => {
51
+ item?.action?.call(undefined, item);
52
+ this.action();
53
+ },
51
54
  },
52
- wrapperClass: item.wrapperClass,
55
+ wrapperClass: item?.wrapperClass,
56
+ };
57
+ },
58
+ wrapperListeners() {
59
+ const listeners = {
60
+ keydown: this.onKeydown,
53
61
  };
62
+ if (this.isCustomContent) {
63
+ listeners.click = this.action;
64
+ }
65
+ return listeners;
54
66
  },
55
67
  },
56
68
  methods: {
@@ -59,13 +71,18 @@ export default {
59
71
 
60
72
  if (code === ENTER || code === SPACE) {
61
73
  stopEvent(event);
62
- /** Instead of simply navigating or calling the action, we want
63
- * the `a/button` to be the target of the event as it might have additional attributes.
64
- * E.g. `a` might have `target` attribute.
65
- * `bubbles` is set to `true` as the parent `li` item has this event listener and thus we'll get a loop.
66
- */
67
- this.$refs.item?.dispatchEvent(new MouseEvent('click', { bubbles: false }));
68
- 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
+ }
69
86
  }
70
87
  },
71
88
  action() {
@@ -78,18 +95,11 @@ export default {
78
95
  <template>
79
96
  <li
80
97
  tabindex="0"
81
- :class="[$options.ITEM_CLASS, itemComponent && itemComponent.wrapperClass]"
98
+ :class="[$options.ITEM_CLASS, itemComponent.wrapperClass]"
82
99
  data-testid="disclosure-dropdown-item"
83
- @click="action"
84
- @keydown="onKeydown"
100
+ v-on="wrapperListeners"
85
101
  >
86
- <div v-if="isCustomContent" class="gl-new-dropdown-item-content">
87
- <div class="gl-new-dropdown-item-text-wrapper">
88
- <slot></slot>
89
- </div>
90
- </div>
91
-
92
- <template v-else-if="itemComponent && item">
102
+ <slot>
93
103
  <component
94
104
  :is="itemComponent.is"
95
105
  v-bind="itemComponent.attrs"
@@ -99,9 +109,11 @@ export default {
99
109
  v-on="itemComponent.listeners"
100
110
  >
101
111
  <span class="gl-new-dropdown-item-text-wrapper">
102
- {{ item.text }}
112
+ <slot name="list-item">
113
+ {{ item.text }}
114
+ </slot>
103
115
  </span>
104
116
  </component>
105
- </template>
117
+ </slot>
106
118
  </li>
107
119
  </template>
@@ -1,13 +1,13 @@
1
1
  ### ECharts Wrapper
2
2
 
3
- The chart component is a Vue component wrapper around
4
- [Apache ECharts](https://echarts.apache.org/en/api.html#echarts). The chart component accepts width
5
- and height props in order to allow the user to make it responsive, but it is not responsive
6
- by default.
3
+ The chart component is a Vue component wrapper around [Apache ECharts](https://echarts.apache.org/en/api.html#echarts).
4
+ The chart component accepts width and height props in order to allow the user to make it responsive,
5
+ but it is not responsive by default.
7
6
 
8
- > Note: In every case there should be a specific component for each type of chart
9
- (i.e. Line, Area, Bar, etc.). This component should only need to be used by chart type components
10
- within GitLab UI as opposed to being used directly within any other codebase.
7
+ > Note: When implementing a chart type that does not already have a GitLab UI component, you can use
8
+ > this component alonside the [ECharts options](https://echarts.apache.org/en/api.html#echarts) to
9
+ > build your chart. Each type of chart should still follow the general guidelines in the
10
+ > [pajamas documentation](https://design.gitlab.com/data-visualization/charts).
11
11
 
12
12
  ### EChart Lifecycle
13
13