@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 +14 -0
- package/dist/components/base/button/button.js +2 -2
- package/dist/components/base/dropdown/dropdown.js +12 -1
- package/dist/components/base/form/form_fields/validators.js +33 -1
- package/dist/tokens/css/tokens.css +1 -1
- package/dist/tokens/css/tokens.dark.css +1 -1
- package/dist/tokens/js/tokens.dark.js +1 -1
- package/dist/tokens/js/tokens.js +1 -1
- package/dist/tokens/scss/_tokens.dark.scss +1 -1
- package/dist/tokens/scss/_tokens.scss +1 -1
- package/dist/utils/is_slot_empty.js +34 -0
- package/package.json +4 -4
- package/src/components/base/button/button.spec.js +11 -0
- package/src/components/base/button/button.vue +2 -2
- package/src/components/base/dropdown/dropdown.vue +13 -0
- package/src/components/base/form/form_fields/validators.js +33 -0
- package/src/components/base/form/form_fields/validators.spec.js +28 -1
- package/src/utils/is_slot_empty.js +37 -0
- package/src/utils/is_slot_empty.spec.js +73 -0
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
|
-
|
|
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
|
-
|
|
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 };
|
package/dist/tokens/js/tokens.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
121
|
+
"cypress": "12.17.3",
|
|
122
122
|
"dompurify": "^2.4.7",
|
|
123
123
|
"emoji-regex": "^10.0.0",
|
|
124
|
-
"eslint": "8.
|
|
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
|
-
|
|
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
|
+
});
|