@gitlab/ui 65.0.0 → 65.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [65.1.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v65.0.1...v65.1.0) (2023-08-09)
2
+
3
+
4
+ ### Features
5
+
6
+ * **GlFormFields:** Add validator for emojis ([7f5b9aa](https://gitlab.com/gitlab-org/gitlab-ui/commit/7f5b9aa195b3a62a83b5b395aec2e49d08b87bcb))
7
+
8
+ ## [65.0.1](https://gitlab.com/gitlab-org/gitlab-ui/compare/v65.0.0...v65.0.1) (2023-08-05)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **button:** correctly detect empty slot for icon only ([bbbc03e](https://gitlab.com/gitlab-org/gitlab-ui/commit/bbbc03ef43139bf023573706894cd9ba60cfce0a))
14
+
1
15
  # [65.0.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v64.24.1...v65.0.0) (2023-08-01)
2
16
 
3
17
 
@@ -1,6 +1,7 @@
1
1
  import { BButton } from 'bootstrap-vue/esm/index.js';
2
2
  import { buttonCategoryOptions, buttonVariantOptions, buttonSizeOptions } from '../../../utils/constants';
3
3
  import { logWarning } from '../../../utils/utils';
4
+ import { isSlotEmpty } from '../../../utils/is_slot_empty';
4
5
  import { SafeLinkMixin } from '../../mixins/safe_link_mixin';
5
6
  import GlIcon from '../icon/icon';
6
7
  import GlLoadingIcon from '../loading_icon/loading_icon';
@@ -75,8 +76,7 @@ var script = {
75
76
  return this.icon !== '';
76
77
  },
77
78
  hasIconOnly() {
78
- // eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots
79
- return Object.keys(this.$slots).length === 0 && this.hasIcon;
79
+ return isSlotEmpty(this, 'default') && this.hasIcon;
80
80
  },
81
81
  isButtonDisabled() {
82
82
  return this.disabled || this.loading;
@@ -47,6 +47,7 @@ var script = {
47
47
  GlLoadingIcon
48
48
  },
49
49
  mixins: [ButtonMixin],
50
+ inheritAttrs: false,
50
51
  props: {
51
52
  headerText: {
52
53
  type: String,
@@ -155,6 +156,16 @@ var script = {
155
156
  type: Object,
156
157
  required: false,
157
158
  default: null
159
+ },
160
+ noFlip: {
161
+ type: Boolean,
162
+ required: false,
163
+ default: false
164
+ },
165
+ splitHref: {
166
+ type: String,
167
+ required: false,
168
+ default: ''
158
169
  }
159
170
  },
160
171
  computed: {
@@ -218,7 +229,7 @@ var script = {
218
229
  const __vue_script__ = script;
219
230
 
220
231
  /* template */
221
- var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('b-dropdown',_vm._g(_vm._b({ref:"dropdown",staticClass:"gl-dropdown",attrs:{"split":_vm.split,"variant":_vm.variant,"size":_vm.buttonSize,"toggle-class":[_vm.toggleButtonClasses],"split-class":_vm.splitButtonClasses,"block":_vm.block,"disabled":_vm.disabled || _vm.loading,"right":_vm.right,"popper-opts":_vm.popperOptions},scopedSlots:_vm._u([{key:"button-content",fn:function(){return [_vm._t("button-content",function(){return [(_vm.loading)?_c('gl-loading-icon',{class:{ 'gl-mr-2': !_vm.isIconOnly }}):_vm._e(),_vm._v(" "),(_vm.icon && !(_vm.isIconOnly && _vm.loading))?_c('gl-icon',{staticClass:"dropdown-icon",attrs:{"name":_vm.icon}}):_vm._e(),_vm._v(" "),_c('span',{staticClass:"gl-dropdown-button-text",class:{ 'gl-sr-only': _vm.textSrOnly }},[_vm._t("button-text",function(){return [_vm._v(_vm._s(_vm.buttonText))]})],2),_vm._v(" "),(_vm.renderCaret)?_c('gl-icon',{staticClass:"gl-button-icon dropdown-chevron",attrs:{"name":"chevron-down"}}):_vm._e()]})]},proxy:true}],null,true)},'b-dropdown',_vm.$attrs,false),_vm.$listeners),[_c('div',{staticClass:"gl-dropdown-inner"},[(_vm.hasSlotContents('header') || _vm.headerText)?_c('div',{staticClass:"gl-dropdown-header",class:{ 'gl-border-b-0!': _vm.hideHeaderBorder }},[(_vm.headerText)?_c('p',{staticClass:"gl-dropdown-header-top"},[_vm._v("\n "+_vm._s(_vm.headerText)+"\n ")]):_vm._e(),_vm._v(" "),_vm._t("header")],2):_vm._e(),_vm._v(" "),(_vm.hasHighlightedItemsOrClearAll)?_c('div',{staticClass:"gl-display-flex gl-flex-direction-row gl-justify-content-space-between gl-align-items-center"},[(_vm.hasHighlightedItemsContent && _vm.showHighlightedItemsTitle)?_c('div',{staticClass:"gl-display-flex gl-flex-grow-1 gl-justify-content-flex-start",class:_vm.highlightedItemsTitleClass},[_c('span',{staticClass:"gl-font-weight-bold",attrs:{"data-testid":"highlighted-items-title"}},[_vm._v(_vm._s(_vm.highlightedItemsTitle))])]):_vm._e(),_vm._v(" "),(_vm.showClearAll)?_c('div',{staticClass:"gl-display-flex gl-flex-grow-1 gl-justify-content-end",class:_vm.clearAllTextClass},[_c('gl-button',{attrs:{"size":"small","category":"tertiary","variant":"link","data-testid":"clear-all-button"},on:{"click":function($event){return _vm.$emit('clear-all', $event)}}},[_vm._v(_vm._s(_vm.clearAllText))])],1):_vm._e()]):_vm._e(),_vm._v(" "),_c('div',{staticClass:"gl-dropdown-contents"},[(_vm.hasHighlightedItemsContent)?_c('div',{staticClass:"gl-overflow-visible",attrs:{"data-testid":"highlighted-items"}},[_vm._t("highlighted-items"),_vm._v(" "),_c('gl-dropdown-divider')],2):_vm._e(),_vm._v(" "),_vm._t("default")],2),_vm._v(" "),(_vm.hasSlotContents('footer'))?_c('div',{staticClass:"gl-dropdown-footer"},[_vm._t("footer")],2):_vm._e()])])};
232
+ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('b-dropdown',_vm._g(_vm._b({ref:"dropdown",staticClass:"gl-dropdown",attrs:{"split":_vm.split,"variant":_vm.variant,"size":_vm.buttonSize,"toggle-class":[_vm.toggleButtonClasses],"split-class":_vm.splitButtonClasses,"block":_vm.block,"disabled":_vm.disabled || _vm.loading,"right":_vm.right,"popper-opts":_vm.popperOptions,"no-flip":_vm.noFlip,"split-href":_vm.splitHref},scopedSlots:_vm._u([{key:"button-content",fn:function(){return [_vm._t("button-content",function(){return [(_vm.loading)?_c('gl-loading-icon',{class:{ 'gl-mr-2': !_vm.isIconOnly }}):_vm._e(),_vm._v(" "),(_vm.icon && !(_vm.isIconOnly && _vm.loading))?_c('gl-icon',{staticClass:"dropdown-icon",attrs:{"name":_vm.icon}}):_vm._e(),_vm._v(" "),_c('span',{staticClass:"gl-dropdown-button-text",class:{ 'gl-sr-only': _vm.textSrOnly }},[_vm._t("button-text",function(){return [_vm._v(_vm._s(_vm.buttonText))]})],2),_vm._v(" "),(_vm.renderCaret)?_c('gl-icon',{staticClass:"gl-button-icon dropdown-chevron",attrs:{"name":"chevron-down"}}):_vm._e()]})]},proxy:true}],null,true)},'b-dropdown',_vm.$attrs,false),_vm.$listeners),[_c('div',{staticClass:"gl-dropdown-inner"},[(_vm.hasSlotContents('header') || _vm.headerText)?_c('div',{staticClass:"gl-dropdown-header",class:{ 'gl-border-b-0!': _vm.hideHeaderBorder }},[(_vm.headerText)?_c('p',{staticClass:"gl-dropdown-header-top"},[_vm._v("\n "+_vm._s(_vm.headerText)+"\n ")]):_vm._e(),_vm._v(" "),_vm._t("header")],2):_vm._e(),_vm._v(" "),(_vm.hasHighlightedItemsOrClearAll)?_c('div',{staticClass:"gl-display-flex gl-flex-direction-row gl-justify-content-space-between gl-align-items-center"},[(_vm.hasHighlightedItemsContent && _vm.showHighlightedItemsTitle)?_c('div',{staticClass:"gl-display-flex gl-flex-grow-1 gl-justify-content-flex-start",class:_vm.highlightedItemsTitleClass},[_c('span',{staticClass:"gl-font-weight-bold",attrs:{"data-testid":"highlighted-items-title"}},[_vm._v(_vm._s(_vm.highlightedItemsTitle))])]):_vm._e(),_vm._v(" "),(_vm.showClearAll)?_c('div',{staticClass:"gl-display-flex gl-flex-grow-1 gl-justify-content-end",class:_vm.clearAllTextClass},[_c('gl-button',{attrs:{"size":"small","category":"tertiary","variant":"link","data-testid":"clear-all-button"},on:{"click":function($event){return _vm.$emit('clear-all', $event)}}},[_vm._v(_vm._s(_vm.clearAllText))])],1):_vm._e()]):_vm._e(),_vm._v(" "),_c('div',{staticClass:"gl-dropdown-contents"},[(_vm.hasHighlightedItemsContent)?_c('div',{staticClass:"gl-overflow-visible",attrs:{"data-testid":"highlighted-items"}},[_vm._t("highlighted-items"),_vm._v(" "),_c('gl-dropdown-divider')],2):_vm._e(),_vm._v(" "),_vm._t("default")],2),_vm._v(" "),(_vm.hasSlotContents('footer'))?_c('div',{staticClass:"gl-dropdown-footer"},[_vm._t("footer")],2):_vm._e()])])};
222
233
  var __vue_staticRenderFns__ = [];
223
234
 
224
235
  /* style */
@@ -1,3 +1,7 @@
1
+ import emojiRegex from 'emoji-regex';
2
+
3
+ const EMOJI_REGEX = emojiRegex();
4
+
1
5
  // This contains core validating behavior and **should not** contain
2
6
  // domain-specific validations.
3
7
  //
@@ -11,6 +15,34 @@
11
15
  // export const projectPathIsUnique = ...
12
16
  // ```
13
17
  const factory = (failMessage, isValid) => val => !isValid(val) ? failMessage : '';
18
+
19
+ /**
20
+ * Validator function to check if a string is present and non-empty.
21
+ *
22
+ * Returns an empty string if the input contains a valid string.
23
+ *
24
+ * Returns `failMessage` if the input string is empty, null, or undefined.
25
+ * @param {string} failMessage - The error message to be returned when validation fails.
26
+ * @returns {Function} A validation function that returns either `failMessage` or empty string.
27
+ */
14
28
  const required = failMessage => factory(failMessage, val => val !== '' && val !== null && val !== undefined);
15
29
 
16
- export { factory, required };
30
+ /**
31
+ * Validator function to check if a string contains any emojis.
32
+ *
33
+ * Returns an empty string if the input is empty, null, or undefined, or if no emoji is present.
34
+ *
35
+ * Returns `failMessage` if the input string contains at least one emoji.
36
+ * @param {string} failMessage - The error message to be returned when validation fails.
37
+ * @returns {Function} A validation function that returns either `failMessage` or empty string.
38
+ */
39
+ const noEmojis = failMessage => factory(failMessage, val => {
40
+ var _val$match$length, _val$match;
41
+ if (!val || typeof val !== 'string') {
42
+ return true;
43
+ }
44
+ const resultsLength = (_val$match$length = (_val$match = val.match(EMOJI_REGEX)) === null || _val$match === void 0 ? void 0 : _val$match.length) !== null && _val$match$length !== void 0 ? _val$match$length : 0;
45
+ return resultsLength < 1;
46
+ });
47
+
48
+ export { factory, noEmojis, required };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Tue, 01 Aug 2023 10:25:10 GMT
3
+ * Generated on Wed, 09 Aug 2023 06:12:06 GMT
4
4
  */
5
5
 
6
6
  :root {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Tue, 01 Aug 2023 10:25:10 GMT
3
+ * Generated on Wed, 09 Aug 2023 06:12:06 GMT
4
4
  */
5
5
 
6
6
  :root {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Tue, 01 Aug 2023 10:25:10 GMT
3
+ * Generated on Wed, 09 Aug 2023 06:12:06 GMT
4
4
  */
5
5
 
6
6
  export const BLACK = "#fff";
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Tue, 01 Aug 2023 10:25:10 GMT
3
+ * Generated on Wed, 09 Aug 2023 06:12:06 GMT
4
4
  */
5
5
 
6
6
  export const BLACK = "#000";
@@ -1,6 +1,6 @@
1
1
 
2
2
  // Do not edit directly
3
- // Generated on Tue, 01 Aug 2023 10:25:10 GMT
3
+ // Generated on Wed, 09 Aug 2023 06:12:06 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 Tue, 01 Aug 2023 10:25:10 GMT
3
+ // Generated on Wed, 09 Aug 2023 06:12:06 GMT
4
4
 
5
5
  $brand-gray-05: #2b2838 !default;
6
6
  $brand-gray-04: #45424d !default;
@@ -0,0 +1,34 @@
1
+ import Vue from 'vue';
2
+
3
+ // Fragment will be available only in Vue.js 3
4
+ const {
5
+ Fragment,
6
+ Comment
7
+ } = Vue;
8
+ function callIfNeeded(fnOrResult, args) {
9
+ return fnOrResult instanceof Function ? fnOrResult(args) : fnOrResult;
10
+ }
11
+ function isEmpty(vnode) {
12
+ if (!vnode || Comment && vnode.type === Comment) {
13
+ return true;
14
+ }
15
+ if (Array.isArray(vnode)) {
16
+ return vnode.every(isEmpty);
17
+ }
18
+ if (Fragment && vnode.type === Fragment) {
19
+ // Vue.js 3 fragment, check children
20
+ return vnode.children.every(isEmpty);
21
+ }
22
+ return false;
23
+ }
24
+ function isSlotEmpty(vueInstance, slot, slotArgs) {
25
+ var _vueInstance$$scopedS, _vueInstance$$scopedS2;
26
+ const isVue3 = Boolean(Fragment);
27
+ const slotContent = isVue3 ?
28
+ // we need to check both $slots and $scopedSlots due to https://github.com/vuejs/core/issues/8869
29
+ // additionally, in @vue/compat $slot might be a function instead of array of vnodes (sigh)
30
+ 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);
31
+ return isEmpty(slotContent);
32
+ }
33
+
34
+ export { isSlotEmpty };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "65.0.0",
3
+ "version": "65.1.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -91,7 +91,7 @@
91
91
  "@gitlab/eslint-plugin": "19.0.0",
92
92
  "@gitlab/fonts": "^1.2.0",
93
93
  "@gitlab/stylelint-config": "4.1.0",
94
- "@gitlab/svgs": "3.58.0",
94
+ "@gitlab/svgs": "3.59.0",
95
95
  "@rollup/plugin-commonjs": "^11.1.0",
96
96
  "@rollup/plugin-node-resolve": "^7.1.3",
97
97
  "@rollup/plugin-replace": "^2.3.2",
@@ -118,10 +118,10 @@
118
118
  "babel-loader": "^8.0.5",
119
119
  "babel-plugin-require-context-hook": "^1.0.0",
120
120
  "bootstrap": "4.6.2",
121
- "cypress": "12.17.2",
121
+ "cypress": "12.17.3",
122
122
  "dompurify": "^2.4.7",
123
123
  "emoji-regex": "^10.0.0",
124
- "eslint": "8.45.0",
124
+ "eslint": "8.46.0",
125
125
  "eslint-import-resolver-jest": "3.0.2",
126
126
  "eslint-plugin-cypress": "2.13.3",
127
127
  "eslint-plugin-storybook": "0.6.12",
@@ -211,4 +211,15 @@ describe('button component', () => {
211
211
  });
212
212
  });
213
213
  });
214
+
215
+ it('should correctly detect empty content for icon only mode', () => {
216
+ const DemoComponent = {
217
+ components: { GlButton },
218
+ template: `<gl-button icon="ellipsis_h"><slot><span v-if="false">not-rendered</span></slot></gl-button>`,
219
+ };
220
+
221
+ wrapper = mount(DemoComponent);
222
+
223
+ expect(wrapper.classes()).toContain('btn-icon');
224
+ });
214
225
  });
@@ -7,6 +7,7 @@ import {
7
7
  buttonSizeOptions,
8
8
  } from '../../../utils/constants';
9
9
  import { logWarning } from '../../../utils/utils';
10
+ import { isSlotEmpty } from '../../../utils/is_slot_empty';
10
11
  import { SafeLinkMixin } from '../../mixins/safe_link_mixin';
11
12
  import GlIcon from '../icon/icon.vue';
12
13
  import GlLoadingIcon from '../loading_icon/loading_icon.vue';
@@ -79,8 +80,7 @@ export default {
79
80
  return this.icon !== '';
80
81
  },
81
82
  hasIconOnly() {
82
- // eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots
83
- return Object.keys(this.$slots).length === 0 && this.hasIcon;
83
+ return isSlotEmpty(this, 'default') && this.hasIcon;
84
84
  },
85
85
  isButtonDisabled() {
86
86
  return this.disabled || this.loading;
@@ -54,6 +54,7 @@ export default {
54
54
  GlLoadingIcon,
55
55
  },
56
56
  mixins: [ButtonMixin],
57
+ inheritAttrs: false,
57
58
  props: {
58
59
  headerText: {
59
60
  type: String,
@@ -163,6 +164,16 @@ export default {
163
164
  required: false,
164
165
  default: null,
165
166
  },
167
+ noFlip: {
168
+ type: Boolean,
169
+ required: false,
170
+ default: false,
171
+ },
172
+ splitHref: {
173
+ type: String,
174
+ required: false,
175
+ default: '',
176
+ },
166
177
  },
167
178
  computed: {
168
179
  renderCaret() {
@@ -247,6 +258,8 @@ export default {
247
258
  :disabled="disabled || loading"
248
259
  :right="right"
249
260
  :popper-opts="popperOptions"
261
+ :no-flip="noFlip"
262
+ :split-href="splitHref"
250
263
  v-on="$listeners"
251
264
  >
252
265
  <div class="gl-dropdown-inner">
@@ -1,3 +1,7 @@
1
+ import emojiRegex from 'emoji-regex';
2
+
3
+ const EMOJI_REGEX = emojiRegex();
4
+
1
5
  // This contains core validating behavior and **should not** contain
2
6
  // domain-specific validations.
3
7
  //
@@ -12,5 +16,34 @@
12
16
  // ```
13
17
  export const factory = (failMessage, isValid) => (val) => !isValid(val) ? failMessage : '';
14
18
 
19
+ /**
20
+ * Validator function to check if a string is present and non-empty.
21
+ *
22
+ * Returns an empty string if the input contains a valid string.
23
+ *
24
+ * Returns `failMessage` if the input string is empty, null, or undefined.
25
+ * @param {string} failMessage - The error message to be returned when validation fails.
26
+ * @returns {Function} A validation function that returns either `failMessage` or empty string.
27
+ */
15
28
  export const required = (failMessage) =>
16
29
  factory(failMessage, (val) => val !== '' && val !== null && val !== undefined);
30
+
31
+ /**
32
+ * Validator function to check if a string contains any emojis.
33
+ *
34
+ * Returns an empty string if the input is empty, null, or undefined, or if no emoji is present.
35
+ *
36
+ * Returns `failMessage` if the input string contains at least one emoji.
37
+ * @param {string} failMessage - The error message to be returned when validation fails.
38
+ * @returns {Function} A validation function that returns either `failMessage` or empty string.
39
+ */
40
+ export const noEmojis = (failMessage) =>
41
+ factory(failMessage, (val) => {
42
+ if (!val || typeof val !== 'string') {
43
+ return true;
44
+ }
45
+
46
+ const resultsLength = val.match(EMOJI_REGEX)?.length ?? 0;
47
+
48
+ return resultsLength < 1;
49
+ });
@@ -1,4 +1,4 @@
1
- import { required } from './validators';
1
+ import { noEmojis, required } from './validators';
2
2
 
3
3
  const TEST_FAIL_MESSAGE = 'Yo test failed!';
4
4
 
@@ -26,4 +26,31 @@ describe('components/base/form/form_fields/validators', () => {
26
26
  expect(validator(input)).toBe(output);
27
27
  });
28
28
  });
29
+
30
+ describe('noEmojis', () => {
31
+ let validator;
32
+
33
+ beforeEach(() => {
34
+ validator = noEmojis(TEST_FAIL_MESSAGE);
35
+ });
36
+
37
+ it.each`
38
+ input | output
39
+ ${'123🐱'} | ${TEST_FAIL_MESSAGE}
40
+ ${'0 🍩'} | ${TEST_FAIL_MESSAGE}
41
+ ${'🐟'} | ${TEST_FAIL_MESSAGE}
42
+ ${''} | ${''}
43
+ ${null} | ${''}
44
+ ${undefined} | ${''}
45
+ ${'123'} | ${''}
46
+ ${'0'} | ${''}
47
+ ${{}} | ${''}
48
+ ${0} | ${''}
49
+ ${1} | ${''}
50
+ ${true} | ${''}
51
+ ${false} | ${''}
52
+ `('with $input, returns $output', ({ input, output }) => {
53
+ expect(validator(input)).toBe(output);
54
+ });
55
+ });
29
56
  });
@@ -0,0 +1,37 @@
1
+ import Vue from 'vue';
2
+
3
+ // Fragment will be available only in Vue.js 3
4
+ const { Fragment, Comment } = Vue;
5
+
6
+ function callIfNeeded(fnOrResult, args) {
7
+ return fnOrResult instanceof Function ? fnOrResult(args) : fnOrResult;
8
+ }
9
+
10
+ function isEmpty(vnode) {
11
+ if (!vnode || (Comment && vnode.type === Comment)) {
12
+ return true;
13
+ }
14
+
15
+ if (Array.isArray(vnode)) {
16
+ return vnode.every(isEmpty);
17
+ }
18
+
19
+ if (Fragment && vnode.type === Fragment) {
20
+ // Vue.js 3 fragment, check children
21
+ return vnode.children.every(isEmpty);
22
+ }
23
+
24
+ return false;
25
+ }
26
+
27
+ export function isSlotEmpty(vueInstance, slot, slotArgs) {
28
+ const isVue3 = Boolean(Fragment);
29
+
30
+ const slotContent = isVue3
31
+ ? // we need to check both $slots and $scopedSlots due to https://github.com/vuejs/core/issues/8869
32
+ // additionally, in @vue/compat $slot might be a function instead of array of vnodes (sigh)
33
+ callIfNeeded(vueInstance.$slots[slot] || vueInstance.$scopedSlots[slot], slotArgs)
34
+ : vueInstance.$scopedSlots[slot]?.(slotArgs);
35
+
36
+ return isEmpty(slotContent);
37
+ }
@@ -0,0 +1,73 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { isSlotEmpty } from './is_slot_empty';
3
+
4
+ describe('is slot empty', () => {
5
+ const TestComponent = {
6
+ template: `
7
+ <div>
8
+ <slot></slot>
9
+ </div>
10
+ `,
11
+ };
12
+
13
+ it('should return true for empty slot', () => {
14
+ const PassesNothing = {
15
+ components: { TestComponent },
16
+ template: '<test-component></test-component>',
17
+ };
18
+
19
+ const wrapper = mount(PassesNothing);
20
+
21
+ expect(isSlotEmpty(wrapper.findComponent(TestComponent).vm, 'default')).toBe(true);
22
+ });
23
+
24
+ it('should return true for slot with comment', () => {
25
+ const PassesComment = {
26
+ components: { TestComponent },
27
+ template: '<test-component><!-- comment --></test-component>',
28
+ };
29
+
30
+ const wrapper = mount(PassesComment);
31
+
32
+ expect(isSlotEmpty(wrapper.findComponent(TestComponent).vm, 'default')).toBe(true);
33
+ });
34
+
35
+ it('should return true for slot with multiple comments', () => {
36
+ const PassesComment = {
37
+ components: { TestComponent },
38
+ template: '<test-component><!-- comment --><!-- comment2 --></test-component>',
39
+ };
40
+
41
+ const wrapper = mount(PassesComment);
42
+
43
+ expect(isSlotEmpty(wrapper.findComponent(TestComponent).vm, 'default')).toBe(true);
44
+ });
45
+
46
+ it('should return false for non-empty slot', () => {
47
+ const PassesComment = {
48
+ components: { TestComponent },
49
+ template: '<test-component>non-empty</test-component>',
50
+ };
51
+
52
+ const wrapper = mount(PassesComment);
53
+
54
+ expect(isSlotEmpty(wrapper.findComponent(TestComponent).vm, 'default')).toBe(false);
55
+ });
56
+
57
+ it.each([true, false])(
58
+ 'should return %s for conditional slot contents based on slot-scope',
59
+ (shouldRender) => {
60
+ const PassesComment = {
61
+ components: { TestComponent },
62
+ template:
63
+ '<test-component><template #default="{ shouldRender }"><span v-if="shouldRender">empty</span></template></test-component>',
64
+ };
65
+
66
+ const wrapper = mount(PassesComment);
67
+
68
+ expect(
69
+ isSlotEmpty(wrapper.findComponent(TestComponent).vm, 'default', { shouldRender })
70
+ ).toBe(!shouldRender);
71
+ }
72
+ );
73
+ });