@gitlab/ui 72.7.0 → 72.8.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/components/base/dropdown/dropdown.js +2 -6
  3. package/dist/components/base/filtered_search/filtered_search_term.js +2 -0
  4. package/dist/components/base/filtered_search/filtered_search_token.js +3 -1
  5. package/dist/components/base/filtered_search/filtered_search_token_segment.js +4 -1
  6. package/dist/components/base/form/form_checkbox_tree/checkbox_tree_node.js +1 -1
  7. package/dist/components/base/new_dropdowns/base_dropdown/base_dropdown.js +9 -0
  8. package/dist/components/base/new_dropdowns/disclosure/utils.js +5 -1
  9. package/dist/components/base/new_dropdowns/listbox/utils.js +6 -0
  10. package/dist/components/base/sorting/sorting.js +1 -0
  11. package/dist/components/base/token_selector/token_selector.js +1 -1
  12. package/dist/components/charts/column/column.js +1 -1
  13. package/dist/components/experimental/duo/chat/components/duo_chat_conversation/duo_chat_conversation.js +2 -0
  14. package/dist/components/experimental/duo/chat/duo_chat.js +2 -0
  15. package/dist/components/utilities/sprintf/sprintf.js +3 -1
  16. package/dist/tokens/css/tokens.css +1 -1
  17. package/dist/tokens/css/tokens.dark.css +1 -1
  18. package/dist/tokens/js/tokens.dark.js +1 -1
  19. package/dist/tokens/js/tokens.js +1 -1
  20. package/dist/tokens/scss/_tokens.dark.scss +1 -1
  21. package/dist/tokens/scss/_tokens.scss +1 -1
  22. package/dist/utils/is_slot_empty.js +4 -0
  23. package/dist/utils/number_utils.js +1 -1
  24. package/dist/utils/utils.js +1 -1
  25. package/package.json +3 -3
  26. package/src/components/base/broadcast_message/broadcast_message.stories.js +1 -0
  27. package/src/components/base/drawer/drawer.spec.js +1 -0
  28. package/src/components/base/dropdown/dropdown.vue +2 -6
  29. package/src/components/base/filtered_search/filtered_search.spec.js +13 -2
  30. package/src/components/base/filtered_search/filtered_search_term.spec.js +2 -4
  31. package/src/components/base/filtered_search/filtered_search_term.vue +1 -0
  32. package/src/components/base/filtered_search/filtered_search_token.vue +1 -0
  33. package/src/components/base/filtered_search/filtered_search_token_segment.spec.js +5 -7
  34. package/src/components/base/filtered_search/filtered_search_token_segment.vue +3 -1
  35. package/src/components/base/form/form_checkbox_tree/checkbox_tree_node.vue +1 -1
  36. package/src/components/base/form/form_checkbox_tree/form_checkbox_tree.spec.js +2 -2
  37. package/src/components/base/form/form_fields/form_fields.spec.js +1 -0
  38. package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.spec.js +41 -4
  39. package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.vue +12 -0
  40. package/src/components/base/new_dropdowns/disclosure/utils.js +2 -0
  41. package/src/components/base/new_dropdowns/listbox/listbox.spec.js +2 -0
  42. package/src/components/base/new_dropdowns/listbox/listbox.stories.js +4 -0
  43. package/src/components/base/new_dropdowns/listbox/utils.js +3 -0
  44. package/src/components/base/search_box_by_click/search_box_by_click.spec.js +2 -4
  45. package/src/components/base/sorting/sorting.vue +1 -0
  46. package/src/components/base/token_selector/token_selector.spec.js +2 -2
  47. package/src/components/base/token_selector/token_selector.vue +1 -1
  48. package/src/components/charts/column/__snapshots__/column_chart.spec.js.snap +1 -1
  49. package/src/components/charts/column/column.stories.js +18 -3
  50. package/src/components/charts/column/column.vue +16 -1
  51. package/src/components/charts/column/column_chart.spec.js +106 -20
  52. package/src/components/experimental/duo/chat/components/duo_chat_conversation/duo_chat_conversation.vue +1 -0
  53. package/src/components/experimental/duo/chat/duo_chat.vue +1 -0
  54. package/src/components/utilities/intersection_observer/intersection_observer.spec.js +1 -0
  55. package/src/components/utilities/intersperse/intersperse.spec.js +1 -0
  56. package/src/components/utilities/sprintf/sprintf.vue +1 -0
  57. package/src/utils/is_slot_empty.js +3 -0
  58. package/src/utils/number_utils.js +1 -1
  59. package/src/utils/utils.js +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [72.8.1](https://gitlab.com/gitlab-org/gitlab-ui/compare/v72.8.0...v72.8.1) (2024-01-18)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **dropdowns:** do not steal focus back from the consumer ([69295fa](https://gitlab.com/gitlab-org/gitlab-ui/commit/69295faa28dfe48b90e75ab86c9d3ba615760c96))
7
+
8
+ # [72.8.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v72.7.0...v72.8.0) (2024-01-16)
9
+
10
+
11
+ ### Features
12
+
13
+ * **GlColumnChart:** allow customising chart tooltip ([ed6ee9e](https://gitlab.com/gitlab-org/gitlab-ui/commit/ed6ee9e3139cba2793d90d955c39c41f6ab2b5af))
14
+
1
15
  # [72.7.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v72.6.0...v72.7.0) (2024-01-14)
2
16
 
3
17
 
@@ -1,8 +1,9 @@
1
1
  import Vue from 'vue';
2
2
  import { BDropdown } from 'bootstrap-vue/esm/index.js';
3
- import { selectAll, isVisible } from 'bootstrap-vue/esm/utils/dom';
3
+ import { selectAll } from 'bootstrap-vue/esm/utils/dom';
4
4
  import merge from 'lodash/merge';
5
5
  import { buttonCategoryOptions, dropdownVariantOptions, buttonSizeOptions } from '../../../utils/constants';
6
+ import { filterVisible } from '../../../utils/utils';
6
7
  import { ButtonMixin } from '../../mixins/button_mixin';
7
8
  import GlButton from '../button/button';
8
9
  import GlIcon from '../icon/icon';
@@ -11,11 +12,6 @@ import GlDropdownDivider from './dropdown_divider';
11
12
  import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
12
13
 
13
14
  //
14
-
15
- // Return an Array of visible items
16
- function filterVisible(els) {
17
- return (els || []).filter(isVisible);
18
- }
19
15
  const Selector = {
20
16
  ITEM_SELECTOR: '.dropdown-item:not(.disabled):not([disabled]),.form-control:not(.disabled):not([disabled])'
21
17
  };
@@ -102,6 +102,8 @@ var script = {
102
102
  title: this.searchTextOptionLabel
103
103
  });
104
104
  }
105
+
106
+ // eslint-disable-next-line unicorn/no-array-callback-reference
105
107
  return tokens.map(tokenToOption);
106
108
  },
107
109
  internalValue: {
@@ -109,7 +109,9 @@ var script = {
109
109
  return hasData || this.isSegmentActive(SEGMENT_DATA);
110
110
  },
111
111
  availableTokensWithSelf() {
112
- return [this.config, ...this.availableTokens.filter(token => token !== this.config)].map(tokenToOption);
112
+ return [this.config, ...this.availableTokens.filter(token => token !== this.config)].map(
113
+ // eslint-disable-next-line unicorn/no-array-callback-reference
114
+ tokenToOption);
113
115
  },
114
116
  operatorDescription() {
115
117
  const operator = this.operators.find(op => op.value === this.tokenValue.operator);
@@ -28,7 +28,9 @@ const isVue3Fragment = vnode => {
28
28
  const isVNodeEmpty = vnode => {
29
29
  if (isVue3Fragment(vnode)) {
30
30
  // vnode.children might be an array or single node in edge cases
31
- return Array.isArray(vnode.children) ? vnode.children.every(isVNodeEmpty) : isVNodeEmpty(vnode.children);
31
+ return Array.isArray(vnode.children) ?
32
+ // eslint-disable-next-line unicorn/no-array-callback-reference
33
+ vnode.children.every(isVNodeEmpty) : isVNodeEmpty(vnode.children);
32
34
  }
33
35
  if (isVue3Comment(vnode)) {
34
36
  return true;
@@ -40,6 +42,7 @@ const isSlotNotEmpty = slot => {
40
42
  return false;
41
43
  }
42
44
  const vnodes = typeof slot === 'function' ? slot() : slot;
45
+ // eslint-disable-next-line unicorn/no-array-callback-reference
43
46
  return !(Array.isArray(vnodes) ? vnodes.every(isVNodeEmpty) : isVNodeEmpty(vnodes));
44
47
  };
45
48
  var script = {
@@ -44,7 +44,7 @@ var script = {
44
44
  const __vue_script__ = script;
45
45
 
46
46
  /* template */
47
- var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{class:_vm.rootClass,attrs:{"data-qa-selector":("" + (_vm.$options.qaPrefix) + (_vm.option.value))}},[_c('gl-form-checkbox',{class:_vm.checkboxClass,attrs:{"checked":_vm.node.isChecked,"indeterminate":_vm.node.isIndeterminate},on:{"change":function($event){return _vm.tree.toggleOption(_vm.option, $event)}}},[_vm._v("\n "+_vm._s(_vm.label)+"\n ")]),_vm._v(" "),_vm._l((_vm.option.children),function(child){return _c('gl-form-checkbox-tree-node',{key:child.value,attrs:{"option":child,"is-nested":""}})})],2)};
47
+ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{class:_vm.rootClass,attrs:{"data-testid":("" + (_vm.$options.qaPrefix) + (_vm.option.value))}},[_c('gl-form-checkbox',{class:_vm.checkboxClass,attrs:{"checked":_vm.node.isChecked,"indeterminate":_vm.node.isIndeterminate},on:{"change":function($event){return _vm.tree.toggleOption(_vm.option, $event)}}},[_vm._v("\n "+_vm._s(_vm.label)+"\n ")]),_vm._v(" "),_vm._l((_vm.option.children),function(child){return _c('gl-form-checkbox-tree-node',{key:child.value,attrs:{"option":child,"is-nested":""}})})],2)};
48
48
  var __vue_staticRenderFns__ = [];
49
49
 
50
50
  /* style */
@@ -359,11 +359,20 @@ var script = {
359
359
  }
360
360
  this.toggle(event);
361
361
  },
362
+ /**
363
+ * Closes the dropdown and returns the focus to the toggle unless it has has moved outside
364
+ * of the dropdown, meaning that the consumer needed to put some other element in focus.
365
+ *
366
+ * @param {KeyboardEvent?} event The keyboard event that caused the dropdown to close.
367
+ */
362
368
  async closeAndFocus(event) {
363
369
  if (!this.visible) {
364
370
  return;
365
371
  }
366
372
  const hasToggled = await this.toggle(event);
373
+ if (!this.$el.contains(document.activeElement)) {
374
+ return;
375
+ }
367
376
  if (hasToggled) {
368
377
  this.focusToggle();
369
378
  }
@@ -6,7 +6,11 @@ const itemValidator = item => {
6
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);
7
7
  };
8
8
  const isItem = item => Boolean(item) && itemValidator(item);
9
- const isGroup = group => Boolean(group) && Array.isArray(group.items) && Boolean(group.items.length) && group.items.every(isItem);
9
+ const isGroup = group => Boolean(group) && Array.isArray(group.items) && Boolean(group.items.length) &&
10
+ // eslint-disable-next-line unicorn/no-array-callback-reference
11
+ group.items.every(isItem);
12
+
13
+ // eslint-disable-next-line unicorn/no-array-callback-reference
10
14
  const itemsValidator = items => items.every(isItem) || items.every(isGroup);
11
15
  const isListItem = tag => ['gl-disclosure-dropdown-group', 'gl-disclosure-dropdown-item', 'li'].includes(tag);
12
16
  const isValidSlotTagVue2 = vNode => {
@@ -2,6 +2,8 @@ import isNumber from 'lodash/isNumber';
2
2
  import isString from 'lodash/isString';
3
3
 
4
4
  const isOption = item => Boolean(item) && (isString(item.value) || isNumber(item.value));
5
+
6
+ // eslint-disable-next-line unicorn/no-array-callback-reference
5
7
  const isGroup = function () {
6
8
  let {
7
9
  options
@@ -10,6 +12,8 @@ const isGroup = function () {
10
12
  };
11
13
  const hasNoDuplicates = array => array.length === new Set(array).size;
12
14
  const flattenedOptions = items => items.flatMap(item => isOption(item) ? item : item.options);
15
+
16
+ // eslint-disable-next-line unicorn/no-array-callback-reference
13
17
  const isAllOptionsOrAllGroups = items => items.every(isOption) || items.every(isGroup);
14
18
  const hasUniqueValues = items => hasNoDuplicates(flattenedOptions(items).map(_ref => {
15
19
  let {
@@ -17,6 +21,8 @@ const hasUniqueValues = items => hasNoDuplicates(flattenedOptions(items).map(_re
17
21
  } = _ref;
18
22
  return value;
19
23
  }));
24
+
25
+ // eslint-disable-next-line unicorn/no-array-callback-reference
20
26
  const hasUniqueGroups = items => hasNoDuplicates(items.filter(isGroup).map(_ref2 => {
21
27
  let {
22
28
  text
@@ -34,6 +34,7 @@ var script = {
34
34
  type: Array,
35
35
  required: false,
36
36
  default: null,
37
+ // eslint-disable-next-line unicorn/no-array-callback-reference
37
38
  validator: sortOptions => sortOptions.every(isOption)
38
39
  },
39
40
  /**
@@ -91,7 +91,7 @@ var script = {
91
91
  default: null
92
92
  },
93
93
  /**
94
- * HTML attributes to add to the text input. Helpful for adding `data-testid` and `data-qa-selector` attributes
94
+ * HTML attributes to add to the text input. Helpful for adding `data-testid` attributes
95
95
  */
96
96
  textInputAttrs: {
97
97
  type: Object,
@@ -191,7 +191,7 @@ const __vue_script__ = script;
191
191
  /* template */
192
192
  var __vue_render__ = function () {
193
193
  var _obj;
194
- var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:"position-relative",class:( _obj = {}, _obj[_vm.$options.HEIGHT_AUTO_CLASSES] = _vm.autoHeight, _obj )},[_c('chart',_vm._g(_vm._b({class:{ 'gl-flex-grow-1': _vm.autoHeight },attrs:{"height":_vm.height,"options":_vm.options},on:{"created":_vm.onCreated}},'chart',_vm.$attrs,false),_vm.$listeners)),_vm._v(" "),(_vm.chart)?_c('chart-tooltip',{attrs:{"chart":_vm.chart,"use-default-tooltip-formatter":true}}):_vm._e()],1)};
194
+ var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:"position-relative",class:( _obj = {}, _obj[_vm.$options.HEIGHT_AUTO_CLASSES] = _vm.autoHeight, _obj )},[_c('chart',_vm._g(_vm._b({class:{ 'gl-flex-grow-1': _vm.autoHeight },attrs:{"height":_vm.height,"options":_vm.options},on:{"created":_vm.onCreated}},'chart',_vm.$attrs,false),_vm.$listeners)),_vm._v(" "),(_vm.chart)?_c('chart-tooltip',{ref:"dataTooltip",attrs:{"chart":_vm.chart,"use-default-tooltip-formatter":true},scopedSlots:_vm._u([(_vm.$scopedSlots['tooltip-title'])?{key:"title",fn:function(scope){return [_vm._t("tooltip-title",null,null,scope)]}}:null,(_vm.$scopedSlots['tooltip-content'])?{key:"default",fn:function(scope){return [_vm._t("tooltip-content",null,null,scope)]}}:null,(_vm.$scopedSlots['tooltip-value'])?{key:"tooltip-value",fn:function(scope){return [_vm._t("tooltip-value",null,null,scope)]}}:null],null,true)}):_vm._e()],1)};
195
195
  var __vue_staticRenderFns__ = [];
196
196
 
197
197
  /* style */
@@ -5,6 +5,8 @@ const i18n = {
5
5
  CONVERSATION_NEW_CHAT: 'New chat'
6
6
  };
7
7
  const isMessage = item => Boolean(item) && (item === null || item === void 0 ? void 0 : item.role);
8
+
9
+ // eslint-disable-next-line unicorn/no-array-callback-reference
8
10
  const itemsValidator = items => items.every(isMessage);
9
11
  var script = {
10
12
  name: 'GlDuoChatConversation',
@@ -48,6 +48,8 @@ const slashCommands = [{
48
48
  description: 'Explain the selected snippet.'
49
49
  }];
50
50
  const isMessage = item => Boolean(item) && (item === null || item === void 0 ? void 0 : item.role);
51
+
52
+ // eslint-disable-next-line unicorn/no-array-callback-reference
51
53
  const itemsValidator = items => items.every(isMessage);
52
54
  var script = {
53
55
  name: 'GlDuoChat',
@@ -63,7 +63,9 @@ var script = {
63
63
  type: Object,
64
64
  required: false,
65
65
  default: undefined,
66
- validator: value => Object.values(value).every(tagPair => Array.isArray(tagPair) && tagPair.length === 2 && tagPair.every(isString))
66
+ validator: value => Object.values(value).every(
67
+ // eslint-disable-next-line unicorn/no-array-callback-reference
68
+ tagPair => Array.isArray(tagPair) && tagPair.length === 2 && tagPair.every(isString))
67
69
  }
68
70
  },
69
71
  /**
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Sun, 14 Jan 2024 23:39:36 GMT
3
+ * Generated on Thu, 18 Jan 2024 06:11:39 GMT
4
4
  */
5
5
 
6
6
  :root {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Sun, 14 Jan 2024 23:39:37 GMT
3
+ * Generated on Thu, 18 Jan 2024 06:11:39 GMT
4
4
  */
5
5
 
6
6
  :root.gl-dark {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Sun, 14 Jan 2024 23:39:37 GMT
3
+ * Generated on Thu, 18 Jan 2024 06:11:39 GMT
4
4
  */
5
5
 
6
6
  export const DATA_VIZ_GREEN_50 = "#133a03";
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Sun, 14 Jan 2024 23:39:36 GMT
3
+ * Generated on Thu, 18 Jan 2024 06:11:39 GMT
4
4
  */
5
5
 
6
6
  export const DATA_VIZ_GREEN_50 = "#ddfab7";
@@ -1,6 +1,6 @@
1
1
 
2
2
  // Do not edit directly
3
- // Generated on Sun, 14 Jan 2024 23:39:37 GMT
3
+ // Generated on Thu, 18 Jan 2024 06:11:39 GMT
4
4
 
5
5
  $red-950: #fff4f3;
6
6
  $red-900: #fcf1ef;
@@ -1,6 +1,6 @@
1
1
 
2
2
  // Do not edit directly
3
- // Generated on Sun, 14 Jan 2024 23:39:37 GMT
3
+ // Generated on Thu, 18 Jan 2024 06:11:39 GMT
4
4
 
5
5
  $gl-line-height-52: 3.25rem;
6
6
  $gl-line-height-44: 2.75rem;
@@ -13,10 +13,12 @@ function isEmpty(vnode) {
13
13
  return true;
14
14
  }
15
15
  if (Array.isArray(vnode)) {
16
+ // eslint-disable-next-line unicorn/no-array-callback-reference
16
17
  return vnode.every(isEmpty);
17
18
  }
18
19
  if (Fragment && vnode.type === Fragment) {
19
20
  // Vue.js 3 fragment, check children
21
+ // eslint-disable-next-line unicorn/no-array-callback-reference
20
22
  return vnode.children.every(isEmpty);
21
23
  }
22
24
  return false;
@@ -28,6 +30,8 @@ function isSlotEmpty(vueInstance, slot, slotArgs) {
28
30
  // we need to check both $slots and $scopedSlots due to https://github.com/vuejs/core/issues/8869
29
31
  // additionally, in @vue/compat $slot might be a function instead of array of vnodes (sigh)
30
32
  callIfNeeded(vueInstance.$slots[slot] || vueInstance.$scopedSlots[slot], slotArgs) : (_vueInstance$$scopedS = (_vueInstance$$scopedS2 = vueInstance.$scopedSlots)[slot]) === null || _vueInstance$$scopedS === void 0 ? void 0 : _vueInstance$$scopedS.call(_vueInstance$$scopedS2, slotArgs);
33
+
34
+ // eslint-disable-next-line unicorn/no-array-callback-reference
31
35
  return isEmpty(slotContent);
32
36
  }
33
37
 
@@ -14,7 +14,7 @@ const sum = function () {
14
14
  numbers[_key] = arguments[_key];
15
15
  }
16
16
  return numbers.reduce(addition);
17
- };
17
+ }; // eslint-disable-line unicorn/no-array-callback-reference
18
18
 
19
19
  /**
20
20
  * Returns the average of all arguments
@@ -171,7 +171,7 @@ function stopEvent(event) {
171
171
  * Return an Array of visible items
172
172
  */
173
173
  function filterVisible(els) {
174
- return (els || []).filter(isVisible);
174
+ return (els || []).filter(el => isVisible(el));
175
175
  }
176
176
 
177
177
  export { colorFromBackground, debounceByAnimationFrame, filterVisible, focusFirstFocusableElement, hexToRgba, isDev, isElementFocusable, isElementTabbable, logWarning, relativeLuminance, rgbFromHex, rgbFromString, stopEvent, throttle, toSrgb, uid };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "72.7.0",
3
+ "version": "72.8.1",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -93,10 +93,10 @@
93
93
  "@babel/preset-env": "^7.23.8",
94
94
  "@babel/preset-react": "^7.23.3",
95
95
  "@cypress/grep": "^4.0.1",
96
- "@gitlab/eslint-plugin": "19.2.0",
96
+ "@gitlab/eslint-plugin": "19.4.0",
97
97
  "@gitlab/fonts": "^1.3.0",
98
98
  "@gitlab/stylelint-config": "5.0.1",
99
- "@gitlab/svgs": "3.74.0",
99
+ "@gitlab/svgs": "3.75.0",
100
100
  "@rollup/plugin-commonjs": "^11.1.0",
101
101
  "@rollup/plugin-node-resolve": "^7.1.3",
102
102
  "@rollup/plugin-replace": "^2.3.2",
@@ -69,6 +69,7 @@ const StackedStory = (args, { argTypes }) => ({
69
69
  GlBroadcastMessage,
70
70
  },
71
71
  props: Object.keys(argTypes),
72
+ // eslint-disable-next-line unicorn/no-array-callback-reference
72
73
  template: `<div>${Object.keys(colorThemes).map(templateWithTheme).join('')}</div>`,
73
74
  });
74
75
  export const Stacked = StackedStory.bind({});
@@ -98,6 +98,7 @@ describe('drawer component', () => {
98
98
  },
99
99
  });
100
100
 
101
+ // eslint-disable-next-line unicorn/no-array-callback-reference
101
102
  expect(wrapper.find(parentSelector).find(`[data-testid="${slot}"]`).exists()).toBe(true);
102
103
  });
103
104
 
@@ -2,24 +2,20 @@
2
2
  <script>
3
3
  import Vue from 'vue';
4
4
  import { BDropdown } from 'bootstrap-vue';
5
- import { isVisible, selectAll } from 'bootstrap-vue/src/utils/dom';
5
+ import { selectAll } from 'bootstrap-vue/src/utils/dom';
6
6
  import merge from 'lodash/merge';
7
7
  import {
8
8
  buttonCategoryOptions,
9
9
  buttonSizeOptions,
10
10
  dropdownVariantOptions,
11
11
  } from '../../../utils/constants';
12
+ import { filterVisible } from '../../../utils/utils';
12
13
  import { ButtonMixin } from '../../mixins/button_mixin';
13
14
  import GlButton from '../button/button.vue';
14
15
  import GlIcon from '../icon/icon.vue';
15
16
  import GlLoadingIcon from '../loading_icon/loading_icon.vue';
16
17
  import GlDropdownDivider from './dropdown_divider.vue';
17
18
 
18
- // Return an Array of visible items
19
- function filterVisible(els) {
20
- return (els || []).filter(isVisible);
21
- }
22
-
23
19
  const Selector = {
24
20
  ITEM_SELECTOR:
25
21
  '.dropdown-item:not(.disabled):not([disabled]),.form-control:not(.disabled):not([disabled])',
@@ -59,6 +59,7 @@ describe('Filtered search', () => {
59
59
  describe('value manipulation', () => {
60
60
  it('creates term when empty', () => {
61
61
  createComponent();
62
+ // eslint-disable-next-line unicorn/no-array-callback-reference
62
63
  expect(wrapper.emitted().input[0][0].map(stripId)).toStrictEqual([
63
64
  { type: TERM_TOKEN_TYPE, value: { data: '' } },
64
65
  ]);
@@ -202,6 +203,7 @@ describe('Filtered search', () => {
202
203
 
203
204
  await nextTick();
204
205
 
206
+ // eslint-disable-next-line unicorn/no-array-callback-reference
205
207
  expect(wrapper.emitted().input.pop()[0].map(stripId)).toStrictEqual([
206
208
  { type: 'faketoken', value: { data: '' } },
207
209
  { type: TERM_TOKEN_TYPE, value: { data: 'one' } },
@@ -220,6 +222,7 @@ describe('Filtered search', () => {
220
222
 
221
223
  await nextTick();
222
224
 
225
+ // eslint-disable-next-line unicorn/no-array-callback-reference
223
226
  expect(wrapper.emitted().input.pop()[0].map(stripId)).toStrictEqual([
224
227
  { type: TERM_TOKEN_TYPE, value: { data: 'one' } },
225
228
  { type: TERM_TOKEN_TYPE, value: { data: '' } },
@@ -335,6 +338,7 @@ describe('Filtered search', () => {
335
338
 
336
339
  await nextTick();
337
340
 
341
+ // eslint-disable-next-line unicorn/no-array-callback-reference
338
342
  expect(wrapper.emitted().input.pop()[0].map(stripId)).toStrictEqual([
339
343
  { type: TERM_TOKEN_TYPE, value: { data: '' } },
340
344
  ]);
@@ -348,6 +352,7 @@ describe('Filtered search', () => {
348
352
 
349
353
  await nextTick();
350
354
 
355
+ // eslint-disable-next-line unicorn/no-array-callback-reference
351
356
  expect(wrapper.emitted().input.pop()[0].map(stripId)).toStrictEqual([
352
357
  { type: 'faketoken', value: { data: 'test' } },
353
358
  { type: TERM_TOKEN_TYPE, value: { data: '' } },
@@ -369,6 +374,7 @@ describe('Filtered search', () => {
369
374
 
370
375
  await nextTick();
371
376
 
377
+ // eslint-disable-next-line unicorn/no-array-callback-reference
372
378
  expect(wrapper.emitted().input.pop()[0].map(stripId)).toStrictEqual([
373
379
  { type: 'faketoken', value: { data: 'test' } },
374
380
  { type: TERM_TOKEN_TYPE, value: { data: '' } },
@@ -382,6 +388,7 @@ describe('Filtered search', () => {
382
388
 
383
389
  await nextTick();
384
390
 
391
+ // eslint-disable-next-line unicorn/no-array-callback-reference
385
392
  expect(wrapper.emitted().input.pop()[0].map(stripId)).toStrictEqual([
386
393
  { type: TERM_TOKEN_TYPE, value: { data: 'one' } },
387
394
  { type: TERM_TOKEN_TYPE, value: { data: '' } },
@@ -398,6 +405,7 @@ describe('Filtered search', () => {
398
405
  await nextTick();
399
406
 
400
407
  expect(wrapper.findAllComponents(GlFilteredSearchTerm).at(2).props('active')).toBe(true);
408
+ // eslint-disable-next-line unicorn/no-array-callback-reference
401
409
  expect(wrapper.emitted().input.pop()[0].map(stripId)).toStrictEqual([
402
410
  { type: TERM_TOKEN_TYPE, value: { data: 'one' } },
403
411
  { type: TERM_TOKEN_TYPE, value: { data: 'two' } },
@@ -415,6 +423,7 @@ describe('Filtered search', () => {
415
423
 
416
424
  await nextTick();
417
425
 
426
+ // eslint-disable-next-line unicorn/no-array-callback-reference
418
427
  expect(wrapper.emitted().input.pop()[0].map(stripId)).toStrictEqual([
419
428
  { type: TERM_TOKEN_TYPE, value: { data: 'one' } },
420
429
  { type: TERM_TOKEN_TYPE, value: { data: 'foo' } },
@@ -445,6 +454,7 @@ describe('Filtered search', () => {
445
454
  });
446
455
  wrapper.findComponent(GlFilteredSearchTerm).vm.$emit('submit');
447
456
  expect(wrapper.emitted('submit')).toBeDefined();
457
+ // eslint-disable-next-line unicorn/no-array-callback-reference
448
458
  expect(wrapper.emitted().submit[0][0].map(stripId)).toStrictEqual([
449
459
  'one two',
450
460
  { type: 'faketoken', value: { data: 'smth' } },
@@ -466,7 +476,7 @@ describe('Filtered search', () => {
466
476
  });
467
477
 
468
478
  it('passes `searchButtonAttributes` prop to `GlSearchBoxByClick`', () => {
469
- const searchButtonAttributes = { 'data-qa-selector': 'foo-bar' };
479
+ const searchButtonAttributes = { 'data-prop': 'foo-bar' };
470
480
 
471
481
  createComponent({ searchButtonAttributes });
472
482
 
@@ -474,7 +484,7 @@ describe('Filtered search', () => {
474
484
  });
475
485
 
476
486
  it('passes `searchInputAttributes` prop to search term', async () => {
477
- const searchInputAttributes = { 'data-qa-selector': 'foo-bar' };
487
+ const searchInputAttributes = { 'data-prop': 'foo-bar' };
478
488
 
479
489
  createComponent({
480
490
  value: ['one'],
@@ -505,6 +515,7 @@ describe('Filtered search', () => {
505
515
  });
506
516
  await nextTick();
507
517
 
518
+ // eslint-disable-next-line unicorn/no-array-callback-reference
508
519
  expect(wrapper.findComponent(GlFilteredSearchTerm).props('currentValue').map(stripId)).toEqual([
509
520
  { type: 'filtered-search-term', value: { data: 'one' } },
510
521
  { type: 'filtered-search-term', value: { data: '' } },
@@ -15,7 +15,7 @@ const pointerClass = 'gl-cursor-pointer';
15
15
  describe('Filtered search term', () => {
16
16
  let wrapper;
17
17
 
18
- const searchInputAttributes = { 'data-qa-selector': 'foo-bar' };
18
+ const searchInputAttributes = { 'data-testid': 'foo-bar', 'data-prop': 'foo-property' };
19
19
 
20
20
  const defaultProps = {
21
21
  availableTokens: [],
@@ -182,9 +182,7 @@ describe('Filtered search term', () => {
182
182
  searchInputAttributes,
183
183
  });
184
184
 
185
- expect(findSearchInput().attributes('data-qa-selector')).toBe(
186
- searchInputAttributes['data-qa-selector']
187
- );
185
+ expect(findSearchInput().attributes('data-prop')).toBe(searchInputAttributes['data-prop']);
188
186
  });
189
187
 
190
188
  it('activates and deactivates when the input is focused/blurred', async () => {
@@ -108,6 +108,7 @@ export default {
108
108
  });
109
109
  }
110
110
 
111
+ // eslint-disable-next-line unicorn/no-array-callback-reference
111
112
  return tokens.map(tokenToOption);
112
113
  },
113
114
  internalValue: {
@@ -109,6 +109,7 @@ export default {
109
109
 
110
110
  availableTokensWithSelf() {
111
111
  return [this.config, ...this.availableTokens.filter((token) => token !== this.config)].map(
112
+ // eslint-disable-next-line unicorn/no-array-callback-reference
112
113
  tokenToOption
113
114
  );
114
115
  },
@@ -10,7 +10,7 @@ const OPTIONS = [
10
10
  describe('Filtered search token segment', () => {
11
11
  let wrapper;
12
12
 
13
- const searchInputAttributes = { 'data-qa-selector': 'foo-bar' };
13
+ const searchInputAttributes = { 'data-prop': 'foo-bar' };
14
14
 
15
15
  beforeAll(() => {
16
16
  if (!HTMLElement.prototype.scrollIntoView) {
@@ -336,7 +336,7 @@ describe('Filtered search token segment', () => {
336
336
  });
337
337
 
338
338
  it('does not add `searchInputAttributes` prop to search token segment', () => {
339
- expect(wrapper.attributes('data-qa-selector')).toBe(undefined);
339
+ expect(wrapper.attributes('data-prop')).toBe(undefined);
340
340
  });
341
341
  }
342
342
  );
@@ -353,9 +353,7 @@ describe('Filtered search token segment', () => {
353
353
  });
354
354
 
355
355
  it('adds `searchInputAttributes` prop to search token segment', () => {
356
- expect(wrapper.attributes('data-qa-selector')).toBe(
357
- searchInputAttributes['data-qa-selector']
358
- );
356
+ expect(wrapper.attributes('data-prop')).toBe(searchInputAttributes['data-prop']);
359
357
  });
360
358
  });
361
359
 
@@ -363,8 +361,8 @@ describe('Filtered search token segment', () => {
363
361
  it('adds `searchInputAttributes` prop to search token segment input', () => {
364
362
  createComponent({ active: true, value: 'something', searchInputAttributes });
365
363
 
366
- expect(wrapper.find('input').attributes('data-qa-selector')).toBe(
367
- searchInputAttributes['data-qa-selector']
364
+ expect(wrapper.find('input').attributes('data-prop')).toBe(
365
+ searchInputAttributes['data-prop']
368
366
  );
369
367
  });
370
368
 
@@ -24,7 +24,8 @@ const isVNodeEmpty = (vnode) => {
24
24
  if (isVue3Fragment(vnode)) {
25
25
  // vnode.children might be an array or single node in edge cases
26
26
  return Array.isArray(vnode.children)
27
- ? vnode.children.every(isVNodeEmpty)
27
+ ? // eslint-disable-next-line unicorn/no-array-callback-reference
28
+ vnode.children.every(isVNodeEmpty)
28
29
  : isVNodeEmpty(vnode.children);
29
30
  }
30
31
 
@@ -41,6 +42,7 @@ const isSlotNotEmpty = (slot) => {
41
42
  }
42
43
 
43
44
  const vnodes = typeof slot === 'function' ? slot() : slot;
45
+ // eslint-disable-next-line unicorn/no-array-callback-reference
44
46
  return !(Array.isArray(vnodes) ? vnodes.every(isVNodeEmpty) : isVNodeEmpty(vnodes));
45
47
  };
46
48
 
@@ -39,7 +39,7 @@ export default {
39
39
  </script>
40
40
 
41
41
  <template>
42
- <div :class="rootClass" :data-qa-selector="`${$options.qaPrefix}${option.value}`">
42
+ <div :class="rootClass" :data-testid="`${$options.qaPrefix}${option.value}`">
43
43
  <gl-form-checkbox
44
44
  :checked="node.isChecked"
45
45
  :indeterminate="node.isIndeterminate"
@@ -87,7 +87,7 @@ describe('GlFormCheckboxTree', () => {
87
87
  const findCheckboxes = (el = wrapper) => el.findAllComponents(GlFormCheckbox);
88
88
  const countIndeterminate = () => wrapper.findAll('.js-is-indeterminate').length || 0;
89
89
  const countChecked = () => wrapper.findAll('.js-is-checked').length || 0;
90
- const findCheckboxByValue = (value) => wrapper.find(`[data-qa-selector="${QA_PREFIX}${value}"]`);
90
+ const findCheckboxByValue = (value) => wrapper.find(`[data-testid="${QA_PREFIX}${value}"]`);
91
91
  const getCheckboxesCount = (el) => findCheckboxes(el).length;
92
92
  const findCheckboxInput = (checkbox) => checkbox.find('input[type="checkbox"]');
93
93
  const expectCheckboxUnchecked = (checkbox) => {
@@ -152,7 +152,7 @@ describe('GlFormCheckboxTree', () => {
152
152
 
153
153
  beforeEach(async () => {
154
154
  createWrapper({ options: getOptions(shape) });
155
- checkbox = wrapper.find(`[data-qa-selector="${QA_PREFIX}${boxToCheck}"]`);
155
+ checkbox = wrapper.find(`[data-testid="${QA_PREFIX}${boxToCheck}"]`);
156
156
  await checkbox.find('input').setChecked();
157
157
  });
158
158
 
@@ -78,6 +78,7 @@ describe('GlFormFields', () => {
78
78
  };
79
79
  };
80
80
  const findFormGroups = () => wrapper.findAllComponents(GlFormGroup).wrappers;
81
+ // eslint-disable-next-line unicorn/no-array-callback-reference
81
82
  const findFormGroupsAsData = () => findFormGroups().map(mapFormGroupToData);
82
83
  const findFormGroupFromLabel = (label) =>
83
84
  wrapper.findAllComponents(GlFormGroup).wrappers.find((x) => x.attributes('label') === label);
@@ -36,7 +36,7 @@ describe('base dropdown', () => {
36
36
  ...propsData,
37
37
  },
38
38
  slots: {
39
- default: `<div class="${GL_DROPDOWN_CONTENTS_CLASS}" />`,
39
+ default: `<div class="${GL_DROPDOWN_CONTENTS_CLASS}"><button /></div>`,
40
40
  ...slots,
41
41
  },
42
42
  attachTo: document.body,
@@ -57,6 +57,8 @@ describe('base dropdown', () => {
57
57
  const findDropdownToggleText = () => findDefaultDropdownToggle().find('.gl-button-text');
58
58
  const findDropdownMenu = () => wrapper.find('.gl-new-dropdown-panel');
59
59
 
60
+ const moveFocusWithinDropdown = () => findDropdownMenu().find('button').element.focus();
61
+
60
62
  describe('Floating UI instance', () => {
61
63
  it("starts Floating UI's when opening the dropdown", async () => {
62
64
  buildWrapper();
@@ -324,14 +326,47 @@ describe('base dropdown', () => {
324
326
  expect(menu.classes('gl-display-block!')).toBe(true);
325
327
  expect(toggle.attributes('aria-expanded')).toBe('true');
326
328
 
327
- // close menu clicking toggle btn
328
- menu.element.focus();
329
+ moveFocusWithinDropdown();
330
+
331
+ // close menu by pressing ESC key
329
332
  await menu.trigger('keydown.esc');
333
+
330
334
  expect(menu.classes('gl-display-block!')).toBe(false);
331
335
  expect(toggle.attributes('aria-expanded')).toBeUndefined();
332
336
  expect(wrapper.emitted(GL_DROPDOWN_HIDDEN)).toHaveLength(1);
333
337
  expect(toggle.element).toHaveFocus();
334
338
  });
339
+
340
+ describe('when the consumer takes over the focus', () => {
341
+ let consumerButton;
342
+
343
+ beforeEach(() => {
344
+ consumerButton = document.createElement('button');
345
+ document.body.appendChild(consumerButton);
346
+ });
347
+
348
+ afterEach(() => {
349
+ consumerButton.remove();
350
+ });
351
+
352
+ it('does not steal the focus back from the consumer when closing the dropdown', async () => {
353
+ const toggle = findDefaultDropdownToggle();
354
+ const menu = findDropdownMenu();
355
+
356
+ // open menu clicking toggle btn
357
+ await toggle.trigger('click');
358
+
359
+ moveFocusWithinDropdown();
360
+
361
+ // consumer focuses some element
362
+ consumerButton.focus();
363
+
364
+ // close menu by pressing ESC key
365
+ await menu.trigger('keydown.esc');
366
+
367
+ expect(consumerButton).toHaveFocus();
368
+ });
369
+ });
335
370
  });
336
371
 
337
372
  describe('beforeClose event', () => {
@@ -353,7 +388,7 @@ describe('base dropdown', () => {
353
388
 
354
389
  await toggle.trigger('click');
355
390
 
356
- menu.element.focus();
391
+ moveFocusWithinDropdown();
357
392
  await menu.trigger('keydown.esc');
358
393
  expect(menu.classes('gl-display-block!')).toBe(true);
359
394
  expect(toggle.attributes('aria-expanded')).toBeDefined();
@@ -365,6 +400,7 @@ describe('base dropdown', () => {
365
400
  const toggle = findDefaultDropdownToggle();
366
401
  const menu = findDropdownMenu();
367
402
  await toggle.trigger('click');
403
+ moveFocusWithinDropdown();
368
404
  await menu.trigger('keydown.esc');
369
405
  expect(event.type).toBe('keydown');
370
406
  });
@@ -444,6 +480,7 @@ describe('base dropdown', () => {
444
480
  expect(menu.classes('gl-display-block!')).toBe(true);
445
481
 
446
482
  // close menu pressing ESC on it
483
+ moveFocusWithinDropdown();
447
484
  await menu.trigger('keydown.esc');
448
485
  expect(menu.classes('gl-display-block!')).toBe(false);
449
486
  expect(firstToggleChild.attributes('aria-expanded')).toBe('false');
@@ -382,11 +382,23 @@ export default {
382
382
  }
383
383
  this.toggle(event);
384
384
  },
385
+ /**
386
+ * Closes the dropdown and returns the focus to the toggle unless it has has moved outside
387
+ * of the dropdown, meaning that the consumer needed to put some other element in focus.
388
+ *
389
+ * @param {KeyboardEvent?} event The keyboard event that caused the dropdown to close.
390
+ */
385
391
  async closeAndFocus(event) {
386
392
  if (!this.visible) {
387
393
  return;
388
394
  }
395
+
389
396
  const hasToggled = await this.toggle(event);
397
+
398
+ if (!this.$el.contains(document.activeElement)) {
399
+ return;
400
+ }
401
+
390
402
  if (hasToggled) {
391
403
  this.focusToggle();
392
404
  }
@@ -9,8 +9,10 @@ const isGroup = (group) =>
9
9
  Boolean(group) &&
10
10
  Array.isArray(group.items) &&
11
11
  Boolean(group.items.length) &&
12
+ // eslint-disable-next-line unicorn/no-array-callback-reference
12
13
  group.items.every(isItem);
13
14
 
15
+ // eslint-disable-next-line unicorn/no-array-callback-reference
14
16
  const itemsValidator = (items) => items.every(isItem) || items.every(isGroup);
15
17
 
16
18
  const isListItem = (tag) =>
@@ -48,6 +48,7 @@ describe('GlCollapsibleListbox', () => {
48
48
  const findListContainer = () => wrapper.find('[role="listbox"]');
49
49
  const findListboxItems = (root = wrapper) => root.findAllComponents(GlListboxItem);
50
50
  const findListboxGroups = () => wrapper.findAllComponents(GlListboxGroup);
51
+ // eslint-disable-next-line unicorn/no-array-callback-reference
51
52
  const findListItem = (index) => findListboxItems().at(index).find(ITEM_SELECTOR);
52
53
  const findHeaderText = () => wrapper.find("[data-testid='listbox-header-text']");
53
54
  const findSearchBox = () => wrapper.find("[data-testid='listbox-search-input']");
@@ -241,6 +242,7 @@ describe('GlCollapsibleListbox', () => {
241
242
 
242
243
  it('should focus the first selected item', async () => {
243
244
  await showDropdown();
245
+ // eslint-disable-next-line unicorn/no-array-callback-reference
244
246
  expect(findListboxItems().at(1).find(ITEM_SELECTOR).element).toHaveFocus();
245
247
  });
246
248
 
@@ -388,9 +388,13 @@ export const Groups = makeGroupedExample({
388
388
  const isSelected = (option) => this.selected.includes(option.value);
389
389
  const notSelected = (option) => !isSelected(option);
390
390
 
391
+ // eslint-disable-next-line unicorn/no-array-callback-reference
391
392
  const selectedBranches = mockGroups[0].options.filter(isSelected);
393
+ // eslint-disable-next-line unicorn/no-array-callback-reference
392
394
  const availableBranches = mockGroups[0].options.filter(notSelected);
395
+ // eslint-disable-next-line unicorn/no-array-callback-reference
393
396
  const selectedTags = mockGroups[1].options.filter(isSelected);
397
+ // eslint-disable-next-line unicorn/no-array-callback-reference
394
398
  const availableTags = mockGroups[1].options.filter(notSelected);
395
399
 
396
400
  return [
@@ -3,17 +3,20 @@ import isString from 'lodash/isString';
3
3
 
4
4
  const isOption = (item) => Boolean(item) && (isString(item.value) || isNumber(item.value));
5
5
 
6
+ // eslint-disable-next-line unicorn/no-array-callback-reference
6
7
  const isGroup = ({ options } = {}) => Array.isArray(options) && options.every(isOption);
7
8
 
8
9
  const hasNoDuplicates = (array) => array.length === new Set(array).size;
9
10
 
10
11
  const flattenedOptions = (items) => items.flatMap((item) => (isOption(item) ? item : item.options));
11
12
 
13
+ // eslint-disable-next-line unicorn/no-array-callback-reference
12
14
  const isAllOptionsOrAllGroups = (items) => items.every(isOption) || items.every(isGroup);
13
15
 
14
16
  const hasUniqueValues = (items) =>
15
17
  hasNoDuplicates(flattenedOptions(items).map(({ value }) => value));
16
18
 
19
+ // eslint-disable-next-line unicorn/no-array-callback-reference
17
20
  const hasUniqueGroups = (items) => hasNoDuplicates(items.filter(isGroup).map(({ text }) => text));
18
21
 
19
22
  const itemsValidator = (items) =>
@@ -162,12 +162,10 @@ describe('search box by click component', () => {
162
162
  });
163
163
 
164
164
  it('adds `searchButtonAttributes` prop to search button', () => {
165
- const searchButtonAttributes = { 'data-qa-selector': 'foo-bar' };
165
+ const searchButtonAttributes = { 'data-prop': 'foo-bar' };
166
166
 
167
167
  createComponent({ searchButtonAttributes });
168
168
 
169
- expect(findSearchButton().attributes('data-qa-selector')).toBe(
170
- searchButtonAttributes['data-qa-selector']
171
- );
169
+ expect(findSearchButton().attributes('data-prop')).toBe(searchButtonAttributes['data-prop']);
172
170
  });
173
171
  });
@@ -34,6 +34,7 @@ export default {
34
34
  type: Array,
35
35
  required: false,
36
36
  default: null,
37
+ // eslint-disable-next-line unicorn/no-array-callback-reference
37
38
  validator: (sortOptions) => sortOptions.every(isOption),
38
39
  },
39
40
  /**
@@ -254,12 +254,12 @@ describe('GlTokenSelector', () => {
254
254
  createComponent({
255
255
  propsData: {
256
256
  textInputAttrs: {
257
- 'data-qa-selector': 'foo_bar',
257
+ 'data-prop': 'foo_bar',
258
258
  },
259
259
  },
260
260
  });
261
261
 
262
- expect(findTextInput().attributes('data-qa-selector')).toBe('foo_bar');
262
+ expect(findTextInput().attributes('data-prop')).toBe('foo_bar');
263
263
  });
264
264
  });
265
265
 
@@ -91,7 +91,7 @@ export default {
91
91
  default: null,
92
92
  },
93
93
  /**
94
- * HTML attributes to add to the text input. Helpful for adding `data-testid` and `data-qa-selector` attributes
94
+ * HTML attributes to add to the text input. Helpful for adding `data-testid` attributes
95
95
  */
96
96
  textInputAttrs: {
97
97
  type: Object,
@@ -1,6 +1,6 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
- exports[`column chart component should correctly render the chart 1`] = `
3
+ exports[`column chart component mounted should correctly render the chart 1`] = `
4
4
  Object {
5
5
  "grid": Object {
6
6
  "bottom": 44,
@@ -7,7 +7,7 @@ import {
7
7
  } from '../../../utils/charts/mock_data';
8
8
  import { toolbox } from '../../../utils/charts/story_config';
9
9
 
10
- const template = `
10
+ const template = (content = '') => `
11
11
  <gl-column-chart
12
12
  :bars="bars"
13
13
  :lines="lines"
@@ -18,7 +18,9 @@ const template = `
18
18
  :x-axis-title="xAxisTitle"
19
19
  :x-axis-type="xAxisType"
20
20
  :height="height"
21
- />
21
+ >
22
+ ${content}
23
+ </gl-column-chart>
22
24
  `;
23
25
 
24
26
  const generateProps = ({
@@ -46,7 +48,7 @@ const generateProps = ({
46
48
  const Template = (args, { argTypes }) => ({
47
49
  components: { GlColumnChart },
48
50
  props: Object.keys(argTypes),
49
- template,
51
+ template: template(),
50
52
  });
51
53
 
52
54
  export const Default = Template.bind({});
@@ -90,6 +92,19 @@ SecondaryYAxisLine.args = generateProps({
90
92
  secondaryDataTitle: 'New line data',
91
93
  });
92
94
 
95
+ export const WithCustomTooltip = (args, { argTypes }) => ({
96
+ components: { GlColumnChart },
97
+ props: Object.keys(argTypes),
98
+ template: template(`
99
+ <template #tooltip-title="{ params }">Custom tooltip title: {{params && params.value}}</template>
100
+ <template #tooltip-content="{ params }">
101
+ <div v-for="p in params && params.seriesData">Wow so custom: {{p.seriesName}}: {{p.value[1]}}</div>
102
+ </template>
103
+ `),
104
+ });
105
+ WithCustomTooltip.args = generateProps();
106
+ WithCustomTooltip.tags = ['skip-visual-test'];
107
+
93
108
  export default {
94
109
  title: 'charts/column-chart',
95
110
  component: GlColumnChart,
@@ -184,6 +184,21 @@ export default {
184
184
  v-on="$listeners"
185
185
  @created="onCreated"
186
186
  />
187
- <chart-tooltip v-if="chart" :chart="chart" :use-default-tooltip-formatter="true" />
187
+ <chart-tooltip
188
+ v-if="chart"
189
+ ref="dataTooltip"
190
+ :chart="chart"
191
+ :use-default-tooltip-formatter="true"
192
+ >
193
+ <template v-if="$scopedSlots['tooltip-title']" #title="scope">
194
+ <slot name="tooltip-title" v-bind="scope"></slot>
195
+ </template>
196
+ <template v-if="$scopedSlots['tooltip-content']" #default="scope">
197
+ <slot name="tooltip-content" v-bind="scope"></slot>
198
+ </template>
199
+ <template v-if="$scopedSlots['tooltip-value']" #tooltip-value="scope">
200
+ <slot name="tooltip-value" v-bind="scope"></slot>
201
+ </template>
202
+ </chart-tooltip>
188
203
  </div>
189
204
  </template>
@@ -1,5 +1,10 @@
1
+ import { nextTick } from 'vue';
1
2
  import { shallowMount } from '@vue/test-utils';
2
- import { createMockChartInstance } from '~helpers/chart_stubs';
3
+ import {
4
+ createMockChartInstance,
5
+ ChartTooltipStub,
6
+ chartTooltipStubData,
7
+ } from '~helpers/chart_stubs';
3
8
  import { expectHeightAutoClasses } from '~helpers/chart_height';
4
9
  import {
5
10
  mockDefaultLineData,
@@ -8,7 +13,6 @@ import {
8
13
  mockDefaultStackedBarData,
9
14
  } from '../../../utils/charts/mock_data';
10
15
  import Chart from '../chart/chart.vue';
11
- import ChartTooltip from '../tooltip/tooltip.vue';
12
16
  import ColumnChart from './column.vue';
13
17
 
14
18
  let mockChartInstance;
@@ -28,42 +32,45 @@ describe('column chart component', () => {
28
32
 
29
33
  const chartItemClickedSpy = jest.fn();
30
34
  const findChart = () => wrapper.findComponent(Chart);
31
- const findChartTooltip = () => wrapper.findComponent(ChartTooltip);
35
+ const findChartTooltip = () => wrapper.findComponent({ ref: 'dataTooltip' });
32
36
 
33
37
  const emitChartCreated = () => findChart().vm.$emit('created', mockChartInstance);
34
38
 
35
- const factory = (props = defaultChartProps, slots = {}) => {
39
+ const factory = (props = defaultChartProps, options = {}) => {
36
40
  wrapper = shallowMount(ColumnChart, {
37
41
  propsData: { ...props },
38
42
  listeners: {
39
43
  chartItemClicked: chartItemClickedSpy,
40
44
  },
41
- slots,
45
+ ...options,
42
46
  });
43
47
  };
44
48
 
45
- beforeEach(() => {
46
- factory();
49
+ describe('mounted', () => {
50
+ beforeEach(() => {
51
+ factory();
47
52
 
48
- mockChartInstance = createMockChartInstance();
49
- emitChartCreated();
50
- });
53
+ mockChartInstance = createMockChartInstance();
54
+ emitChartCreated();
55
+ });
51
56
 
52
- it('emits "created" when onCreated is called', () => {
53
- expect(wrapper.emitted('created')).toHaveLength(1);
54
- });
57
+ it('emits "created" when onCreated is called', () => {
58
+ expect(wrapper.emitted('created')).toHaveLength(1);
59
+ });
55
60
 
56
- it('calls event listener when "chartItemClicked" is emitted on the Chart component', () => {
57
- findChart().vm.$emit('chartItemClicked');
61
+ it('calls event listener when "chartItemClicked" is emitted on the Chart component', () => {
62
+ findChart().vm.$emit('chartItemClicked');
58
63
 
59
- expect(chartItemClickedSpy).toHaveBeenCalled();
60
- });
64
+ expect(chartItemClickedSpy).toHaveBeenCalled();
65
+ });
61
66
 
62
- it('should correctly render the chart', () => {
63
- const chart = findChart();
67
+ it('should correctly render the chart', () => {
68
+ const chart = findChart();
64
69
 
65
- expect(chart.props('options')).toMatchSnapshot();
70
+ expect(chart.props('options')).toMatchSnapshot();
71
+ });
66
72
  });
73
+
67
74
  describe('with line data provided', () => {
68
75
  beforeEach(() => {
69
76
  factory({
@@ -120,11 +127,90 @@ describe('column chart component', () => {
120
127
 
121
128
  describe('tooltip', () => {
122
129
  it('configures chart tooltip', async () => {
130
+ factory(defaultChartProps, {
131
+ stubs: {
132
+ 'chart-tooltip': ChartTooltipStub,
133
+ },
134
+ });
135
+ mockChartInstance = createMockChartInstance();
136
+ emitChartCreated();
137
+ await nextTick();
138
+
123
139
  expect(findChartTooltip().props()).toMatchObject({
124
140
  chart: mockChartInstance,
125
141
  useDefaultTooltipFormatter: true,
126
142
  });
127
143
  });
144
+
145
+ describe('custom tooltip slots', () => {
146
+ const { params, title, content } = chartTooltipStubData;
147
+
148
+ it('customizes value', async () => {
149
+ const tooltipValueSlot = jest.fn().mockReturnValue('Custom tooltip value');
150
+
151
+ factory(defaultChartProps, {
152
+ stubs: {
153
+ 'chart-tooltip': ChartTooltipStub,
154
+ },
155
+ scopedSlots: {
156
+ 'tooltip-value': tooltipValueSlot,
157
+ },
158
+ });
159
+
160
+ mockChartInstance = createMockChartInstance();
161
+ emitChartCreated();
162
+ await nextTick();
163
+
164
+ expect(tooltipValueSlot).toHaveBeenCalledWith({ value: 9 });
165
+ expect(findChartTooltip().text()).toBe('Custom tooltip value');
166
+ });
167
+
168
+ it('customizes title', async () => {
169
+ const tooltipTitleSlot = jest.fn().mockReturnValue('Custom tooltip title');
170
+
171
+ factory(defaultChartProps, {
172
+ stubs: {
173
+ 'chart-tooltip': ChartTooltipStub,
174
+ },
175
+ scopedSlots: {
176
+ 'tooltip-title': tooltipTitleSlot,
177
+ },
178
+ });
179
+
180
+ mockChartInstance = createMockChartInstance();
181
+ emitChartCreated();
182
+ await nextTick();
183
+
184
+ expect(tooltipTitleSlot).toHaveBeenCalledWith({
185
+ params,
186
+ title,
187
+ });
188
+ expect(findChartTooltip().text()).toBe('Custom tooltip title');
189
+ });
190
+
191
+ it('customizes content', async () => {
192
+ const tooltipContentSlot = jest.fn().mockReturnValue('Custom tooltip content');
193
+
194
+ factory(defaultChartProps, {
195
+ stubs: {
196
+ 'chart-tooltip': ChartTooltipStub,
197
+ },
198
+ scopedSlots: {
199
+ 'tooltip-content': tooltipContentSlot,
200
+ },
201
+ });
202
+
203
+ mockChartInstance = createMockChartInstance();
204
+ emitChartCreated();
205
+ await nextTick();
206
+
207
+ expect(tooltipContentSlot).toHaveBeenCalledWith({
208
+ params,
209
+ content,
210
+ });
211
+ expect(findChartTooltip().text()).toBe('Custom tooltip content');
212
+ });
213
+ });
128
214
  });
129
215
 
130
216
  describe('height', () => {
@@ -7,6 +7,7 @@ const i18n = {
7
7
 
8
8
  const isMessage = (item) => Boolean(item) && item?.role;
9
9
 
10
+ // eslint-disable-next-line unicorn/no-array-callback-reference
10
11
  const itemsValidator = (items) => items.every(isMessage);
11
12
 
12
13
  export default {
@@ -62,6 +62,7 @@ export const slashCommands = [
62
62
 
63
63
  const isMessage = (item) => Boolean(item) && item?.role;
64
64
 
65
+ // eslint-disable-next-line unicorn/no-array-callback-reference
65
66
  const itemsValidator = (items) => items.every(isMessage);
66
67
 
67
68
  export default {
@@ -98,6 +98,7 @@ describe('IntersectionObserver', () => {
98
98
  triggerIntersectionObserver(entry);
99
99
 
100
100
  expect(allWrappers).toHaveLength(2);
101
+ // eslint-disable-next-line unicorn/no-array-callback-reference
101
102
  expect(allWrappers.map((x) => x.emitted())).toEqual(allWrappers.map(createExpectation));
102
103
  });
103
104
  });
@@ -23,6 +23,7 @@ describe('Intersperse Component', () => {
23
23
  createComponent(defaultSlotContent);
24
24
 
25
25
  selectorsToCheck.forEach((selector) => {
26
+ // eslint-disable-next-line unicorn/no-array-callback-reference
26
27
  expect(wrapper.find(selector).exists()).toBe(true);
27
28
  });
28
29
  }
@@ -68,6 +68,7 @@ export default {
68
68
  default: undefined,
69
69
  validator: (value) =>
70
70
  Object.values(value).every(
71
+ // eslint-disable-next-line unicorn/no-array-callback-reference
71
72
  (tagPair) => Array.isArray(tagPair) && tagPair.length === 2 && tagPair.every(isString)
72
73
  ),
73
74
  },
@@ -13,11 +13,13 @@ function isEmpty(vnode) {
13
13
  }
14
14
 
15
15
  if (Array.isArray(vnode)) {
16
+ // eslint-disable-next-line unicorn/no-array-callback-reference
16
17
  return vnode.every(isEmpty);
17
18
  }
18
19
 
19
20
  if (Fragment && vnode.type === Fragment) {
20
21
  // Vue.js 3 fragment, check children
22
+ // eslint-disable-next-line unicorn/no-array-callback-reference
21
23
  return vnode.children.every(isEmpty);
22
24
  }
23
25
 
@@ -33,5 +35,6 @@ export function isSlotEmpty(vueInstance, slot, slotArgs) {
33
35
  callIfNeeded(vueInstance.$slots[slot] || vueInstance.$scopedSlots[slot], slotArgs)
34
36
  : vueInstance.$scopedSlots[slot]?.(slotArgs);
35
37
 
38
+ // eslint-disable-next-line unicorn/no-array-callback-reference
36
39
  return isEmpty(slotContent);
37
40
  }
@@ -9,7 +9,7 @@ export const addition = (a, b) => a + b;
9
9
  * Returns the sum of all arguments
10
10
  * @param {...Number} numbers
11
11
  */
12
- export const sum = (...numbers) => numbers.reduce(addition);
12
+ export const sum = (...numbers) => numbers.reduce(addition); // eslint-disable-line unicorn/no-array-callback-reference
13
13
 
14
14
  /**
15
15
  * Returns the average of all arguments
@@ -182,5 +182,5 @@ export function stopEvent(
182
182
  * Return an Array of visible items
183
183
  */
184
184
  export function filterVisible(els) {
185
- return (els || []).filter(isVisible);
185
+ return (els || []).filter((el) => isVisible(el));
186
186
  }