@gitlab/ui 66.4.0 → 66.5.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 +7 -0
- package/dist/components/base/table/table.js +7 -2
- package/dist/components/base/table_lite/table_lite.js +7 -2
- package/dist/components/charts/single_stat/single_stat.js +20 -1
- package/dist/components/utilities/animated_number/animated_number.js +13 -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/number_utils.js +26 -1
- package/package.json +4 -2
- package/src/components/base/table/table.spec.js +9 -0
- package/src/components/base/table/table.vue +6 -1
- package/src/components/base/table_lite/table_lite.spec.js +9 -0
- package/src/components/base/table_lite/table_lite.vue +9 -2
- package/src/components/charts/single_stat/single_stat.spec.js +57 -11
- package/src/components/charts/single_stat/single_stat.stories.js +10 -0
- package/src/components/charts/single_stat/single_stat.vue +22 -1
- package/src/components/utilities/animated_number/animated_number.spec.js +36 -24
- package/src/components/utilities/animated_number/animated_number.stories.js +3 -1
- package/src/components/utilities/animated_number/animated_number.vue +14 -1
- package/src/utils/number_utils.js +24 -0
- package/src/utils/number_utils.spec.js +21 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [66.5.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v66.4.0...v66.5.0) (2023-09-07)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **GlSingleStat:** Format the displayed number based on locale ([b6a845d](https://gitlab.com/gitlab-org/gitlab-ui/commit/b6a845d4344ad1ecef83738b6ef1b82a45d786a5))
|
|
7
|
+
|
|
1
8
|
# [66.4.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v66.3.1...v66.4.0) (2023-09-04)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -21,7 +21,12 @@ var script = {
|
|
|
21
21
|
},
|
|
22
22
|
inheritAttrs: false,
|
|
23
23
|
props: {
|
|
24
|
-
tableClass
|
|
24
|
+
tableClass,
|
|
25
|
+
fields: {
|
|
26
|
+
type: Array,
|
|
27
|
+
required: false,
|
|
28
|
+
default: null
|
|
29
|
+
}
|
|
25
30
|
},
|
|
26
31
|
computed: {
|
|
27
32
|
localTableClass() {
|
|
@@ -41,7 +46,7 @@ var script = {
|
|
|
41
46
|
const __vue_script__ = script;
|
|
42
47
|
|
|
43
48
|
/* template */
|
|
44
|
-
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('b-table',_vm._g(_vm._b({attrs:{"table-class":_vm.localTableClass},scopedSlots:_vm._u([_vm._l((Object.keys(_vm.$scopedSlots)),function(slot){return {key:slot,fn:function(scope){return [_vm._t(slot,null,null,scope)]}}})],null,true)},'b-table',_vm.$attrs,false),_vm.$listeners))};
|
|
49
|
+
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('b-table',_vm._g(_vm._b({attrs:{"table-class":_vm.localTableClass,"fields":_vm.fields},scopedSlots:_vm._u([_vm._l((Object.keys(_vm.$scopedSlots)),function(slot){return {key:slot,fn:function(scope){return [_vm._t(slot,null,null,scope)]}}})],null,true)},'b-table',_vm.$attrs,false),_vm.$listeners))};
|
|
45
50
|
var __vue_staticRenderFns__ = [];
|
|
46
51
|
|
|
47
52
|
/* style */
|
|
@@ -11,7 +11,12 @@ var script = {
|
|
|
11
11
|
},
|
|
12
12
|
inheritAttrs: false,
|
|
13
13
|
props: {
|
|
14
|
-
tableClass
|
|
14
|
+
tableClass,
|
|
15
|
+
fields: {
|
|
16
|
+
type: Array,
|
|
17
|
+
required: false,
|
|
18
|
+
default: null
|
|
19
|
+
}
|
|
15
20
|
},
|
|
16
21
|
computed: {
|
|
17
22
|
localTableClass() {
|
|
@@ -24,7 +29,7 @@ var script = {
|
|
|
24
29
|
const __vue_script__ = script;
|
|
25
30
|
|
|
26
31
|
/* template */
|
|
27
|
-
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('b-table-lite',_vm._g(_vm._b({attrs:{"table-class":_vm.localTableClass},scopedSlots:_vm._u([_vm._l((Object.keys(_vm.$scopedSlots)),function(slot){return {key:slot,fn:function(scope){return [_vm._t(slot,null,null,scope)]}}})],null,true)},'b-table-lite',_vm.$attrs,false),_vm.$listeners))};
|
|
32
|
+
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('b-table-lite',_vm._g(_vm._b({attrs:{"table-class":_vm.localTableClass,"fields":_vm.fields},scopedSlots:_vm._u([_vm._l((Object.keys(_vm.$scopedSlots)),function(slot){return {key:slot,fn:function(scope){return [_vm._t(slot,null,null,scope)]}}})],null,true)},'b-table-lite',_vm.$attrs,false),_vm.$listeners))};
|
|
28
33
|
var __vue_staticRenderFns__ = [];
|
|
29
34
|
|
|
30
35
|
/* style */
|
|
@@ -2,6 +2,7 @@ import { badgeVariantOptions, variantCssColorMap } from '../../../utils/constant
|
|
|
2
2
|
import GlBadge from '../../base/badge/badge';
|
|
3
3
|
import GlIcon from '../../base/icon/icon';
|
|
4
4
|
import GlAnimatedNumber from '../../utilities/animated_number/animated_number';
|
|
5
|
+
import { formatNumberToLocale } from '../../../utils/number_utils';
|
|
5
6
|
import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
|
|
6
7
|
|
|
7
8
|
var script = {
|
|
@@ -25,6 +26,14 @@ var script = {
|
|
|
25
26
|
required: false,
|
|
26
27
|
default: null
|
|
27
28
|
},
|
|
29
|
+
/**
|
|
30
|
+
* Requires the `value` property to be a valid Number or convertible to one
|
|
31
|
+
*/
|
|
32
|
+
useDelimiters: {
|
|
33
|
+
type: Boolean,
|
|
34
|
+
required: false,
|
|
35
|
+
default: false
|
|
36
|
+
},
|
|
28
37
|
variant: {
|
|
29
38
|
type: String,
|
|
30
39
|
required: false,
|
|
@@ -85,6 +94,16 @@ var script = {
|
|
|
85
94
|
},
|
|
86
95
|
canAnimate() {
|
|
87
96
|
return this.shouldAnimate && !Number.isNaN(Number(this.value));
|
|
97
|
+
},
|
|
98
|
+
statValue() {
|
|
99
|
+
if (this.useDelimiters) {
|
|
100
|
+
var _this$value$toString$;
|
|
101
|
+
const minimumFractionDigits = ((_this$value$toString$ = this.value.toString().split('.')[1]) === null || _this$value$toString$ === void 0 ? void 0 : _this$value$toString$.length) || 0;
|
|
102
|
+
return formatNumberToLocale(this.value, {
|
|
103
|
+
minimumFractionDigits
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return this.value;
|
|
88
107
|
}
|
|
89
108
|
},
|
|
90
109
|
methods: {
|
|
@@ -98,7 +117,7 @@ var script = {
|
|
|
98
117
|
const __vue_script__ = script;
|
|
99
118
|
|
|
100
119
|
/* template */
|
|
101
|
-
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',_vm._g(_vm._b({staticClass:"gl-single-stat gl-display-flex gl-flex-direction-column gl-p-2"},'div',_vm.$attrs,false),_vm.$listeners),[_c('div',{staticClass:"gl-display-flex gl-align-items-center gl-text-gray-700 gl-mb-2"},[(_vm.showTitleIcon)?_c('gl-icon',{class:['gl-mr-2', _vm.titleIconClass],attrs:{"name":_vm.titleIcon,"data-testid":"title-icon"}}):_vm._e(),_vm._v(" "),_c('span',{staticClass:"gl-font-base gl-font-weight-normal",attrs:{"data-testid":"title-text"}},[_vm._v(_vm._s(_vm.title))])],1),_vm._v(" "),_c('div',{staticClass:"gl-display-flex gl-align-items-baseline gl-font-weight-bold gl-text-gray-900"},[_c('span',{staticClass:"gl-font-size-h-display",class:{ 'gl-mr-2': !_vm.unit },attrs:{"data-testid":"displayValue"}},[(_vm.canAnimate)?_c('gl-animated-number',{attrs:{"number":Number(_vm.value),"decimal-places":_vm.animationDecimalPlaces},on:{"animating":function($event){return _vm.setHideUnits(true)},"animated":function($event){return _vm.setHideUnits(false)}}}):_c('span',{attrs:{"data-testid":"non-animated-value"}},[_vm._v(_vm._s(_vm.
|
|
120
|
+
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',_vm._g(_vm._b({staticClass:"gl-single-stat gl-display-flex gl-flex-direction-column gl-p-2"},'div',_vm.$attrs,false),_vm.$listeners),[_c('div',{staticClass:"gl-display-flex gl-align-items-center gl-text-gray-700 gl-mb-2"},[(_vm.showTitleIcon)?_c('gl-icon',{class:['gl-mr-2', _vm.titleIconClass],attrs:{"name":_vm.titleIcon,"data-testid":"title-icon"}}):_vm._e(),_vm._v(" "),_c('span',{staticClass:"gl-font-base gl-font-weight-normal",attrs:{"data-testid":"title-text"}},[_vm._v(_vm._s(_vm.title))])],1),_vm._v(" "),_c('div',{staticClass:"gl-display-flex gl-align-items-baseline gl-font-weight-bold gl-text-gray-900"},[_c('span',{staticClass:"gl-font-size-h-display",class:{ 'gl-mr-2': !_vm.unit },attrs:{"data-testid":"displayValue"}},[(_vm.canAnimate)?_c('gl-animated-number',{attrs:{"number":Number(_vm.value),"decimal-places":_vm.animationDecimalPlaces,"use-delimiters":_vm.useDelimiters},on:{"animating":function($event){return _vm.setHideUnits(true)},"animated":function($event){return _vm.setHideUnits(false)}}}):_c('span',{attrs:{"data-testid":"non-animated-value"}},[_vm._v(_vm._s(_vm.statValue))])],1),_vm._v(" "),(_vm.unit)?_c('span',{staticClass:"gl-font-sm gl-mx-2 gl-transition-medium gl-opacity-10",class:{ 'gl-opacity-0!': _vm.hideUnits },attrs:{"data-testid":"unit"}},[_vm._v(_vm._s(_vm.unit))]):_vm._e(),_vm._v(" "),(_vm.showMetaIcon)?_c('gl-icon',{class:_vm.textColor,attrs:{"name":_vm.metaIcon,"data-testid":"meta-icon"}}):_vm._e(),_vm._v(" "),(_vm.showBadge)?_c('gl-badge',{attrs:{"variant":_vm.variant,"icon":_vm.metaIcon,"data-testid":"meta-badge"}},[_vm._v(_vm._s(_vm.metaText))]):_vm._e()],1)])};
|
|
102
121
|
var __vue_staticRenderFns__ = [];
|
|
103
122
|
|
|
104
123
|
/* style */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatNumberToLocale } from '../../../utils/number_utils';
|
|
1
2
|
import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
|
|
2
3
|
|
|
3
4
|
var script = {
|
|
@@ -23,6 +24,11 @@ var script = {
|
|
|
23
24
|
required: false,
|
|
24
25
|
default: 0
|
|
25
26
|
},
|
|
27
|
+
useDelimiters: {
|
|
28
|
+
type: Boolean,
|
|
29
|
+
required: false,
|
|
30
|
+
default: false
|
|
31
|
+
},
|
|
26
32
|
animateOnMount: {
|
|
27
33
|
type: Boolean,
|
|
28
34
|
required: false,
|
|
@@ -37,7 +43,13 @@ var script = {
|
|
|
37
43
|
},
|
|
38
44
|
computed: {
|
|
39
45
|
animatedNumber() {
|
|
40
|
-
|
|
46
|
+
const number = this.displayNumber.toFixed(this.decimalPlaces);
|
|
47
|
+
if (this.useDelimiters) {
|
|
48
|
+
return formatNumberToLocale(number, {
|
|
49
|
+
minimumFractionDigits: this.decimalPlaces
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return number;
|
|
41
53
|
}
|
|
42
54
|
},
|
|
43
55
|
ready() {
|
package/dist/tokens/js/tokens.js
CHANGED
|
@@ -96,4 +96,29 @@ const engineeringNotation = function (value) {
|
|
|
96
96
|
return `${scaledMantissa}${allYourBase[scaledPower]}`;
|
|
97
97
|
};
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Formats a number as a locale-based string using `Intl.NumberFormat`.
|
|
101
|
+
*
|
|
102
|
+
* 2333 -> 2,333
|
|
103
|
+
* 232324 -> 232,324
|
|
104
|
+
*
|
|
105
|
+
* @param {Number|string} value - number to be converted
|
|
106
|
+
* @param {{}?} options - options to be passed to
|
|
107
|
+
* `Intl.NumberFormat` such as `unit` and `style`.
|
|
108
|
+
* @param {String|String[]} locales - If set, forces a different
|
|
109
|
+
* language code from the one currently in the document.
|
|
110
|
+
*
|
|
111
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
|
|
112
|
+
*
|
|
113
|
+
* @returns {String}
|
|
114
|
+
*/
|
|
115
|
+
const formatNumberToLocale = function (value) {
|
|
116
|
+
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
117
|
+
let locales = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined;
|
|
118
|
+
if (Number.isNaN(Number(value))) {
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
return new Intl.NumberFormat(locales, options).format(value);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export { addition, average, engineeringNotation, formatNumberToLocale, modulo, sum };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitlab/ui",
|
|
3
|
-
"version": "66.
|
|
3
|
+
"version": "66.5.0",
|
|
4
4
|
"description": "GitLab UI Components",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"devDependencies": {
|
|
90
90
|
"@arkweid/lefthook": "0.7.7",
|
|
91
91
|
"@babel/core": "^7.22.11",
|
|
92
|
-
"@babel/preset-env": "^7.22.
|
|
92
|
+
"@babel/preset-env": "^7.22.14",
|
|
93
93
|
"@babel/preset-react": "^7.22.5",
|
|
94
94
|
"@gitlab/eslint-plugin": "19.0.0",
|
|
95
95
|
"@gitlab/fonts": "^1.2.0",
|
|
@@ -117,11 +117,13 @@
|
|
|
117
117
|
"@vue/vue2-jest": "29.0.0",
|
|
118
118
|
"@vue/vue3-jest": "^29.1.1",
|
|
119
119
|
"autoprefixer": "^9.7.6",
|
|
120
|
+
"axe-core": "^4.2.3",
|
|
120
121
|
"babel-jest": "29.0.1",
|
|
121
122
|
"babel-loader": "^8.0.5",
|
|
122
123
|
"babel-plugin-require-context-hook": "^1.0.0",
|
|
123
124
|
"bootstrap": "4.6.2",
|
|
124
125
|
"cypress": "13.1.0",
|
|
126
|
+
"cypress-axe": "^1.4.0",
|
|
125
127
|
"dompurify": "^3.0.0",
|
|
126
128
|
"emoji-regex": "^10.0.0",
|
|
127
129
|
"eslint": "8.48.0",
|
|
@@ -57,4 +57,13 @@ describe('GlTable', () => {
|
|
|
57
57
|
|
|
58
58
|
expect(findBTable().props().tableClass).toEqual(['gl-table', 'test-class']);
|
|
59
59
|
});
|
|
60
|
+
|
|
61
|
+
it('adds gl-table fields to table prop', () => {
|
|
62
|
+
const fields = ['name', 'age'];
|
|
63
|
+
|
|
64
|
+
factory({ props: { fields } });
|
|
65
|
+
|
|
66
|
+
expect(wrapper.props('fields')).toEqual(fields);
|
|
67
|
+
expect(findBTable().props('fields')).toEqual(fields);
|
|
68
|
+
});
|
|
60
69
|
});
|
|
@@ -21,6 +21,11 @@ export default {
|
|
|
21
21
|
inheritAttrs: false,
|
|
22
22
|
props: {
|
|
23
23
|
tableClass,
|
|
24
|
+
fields: {
|
|
25
|
+
type: Array,
|
|
26
|
+
required: false,
|
|
27
|
+
default: null,
|
|
28
|
+
},
|
|
24
29
|
},
|
|
25
30
|
computed: {
|
|
26
31
|
localTableClass() {
|
|
@@ -38,7 +43,7 @@ export default {
|
|
|
38
43
|
</script>
|
|
39
44
|
|
|
40
45
|
<template>
|
|
41
|
-
<b-table :table-class="localTableClass" v-bind="$attrs" v-on="$listeners">
|
|
46
|
+
<b-table :table-class="localTableClass" :fields="fields" v-bind="$attrs" v-on="$listeners">
|
|
42
47
|
<template v-for="slot in Object.keys($scopedSlots)" #[slot]="scope">
|
|
43
48
|
<slot :name="slot" v-bind="scope"></slot>
|
|
44
49
|
</template>
|
|
@@ -18,4 +18,13 @@ describe('GlTableLite', () => {
|
|
|
18
18
|
|
|
19
19
|
expect(findBTableLite().props().tableClass).toEqual(['gl-table', 'test-class']);
|
|
20
20
|
});
|
|
21
|
+
|
|
22
|
+
it('adds gl-table fields to table prop', () => {
|
|
23
|
+
const fields = ['name', 'age'];
|
|
24
|
+
|
|
25
|
+
factory({ fields });
|
|
26
|
+
|
|
27
|
+
expect(wrapper.props('fields')).toEqual(fields);
|
|
28
|
+
expect(findBTableLite().props('fields')).toEqual(fields);
|
|
29
|
+
});
|
|
21
30
|
});
|
|
@@ -9,7 +9,14 @@ export default {
|
|
|
9
9
|
BTableLite,
|
|
10
10
|
},
|
|
11
11
|
inheritAttrs: false,
|
|
12
|
-
props: {
|
|
12
|
+
props: {
|
|
13
|
+
tableClass,
|
|
14
|
+
fields: {
|
|
15
|
+
type: Array,
|
|
16
|
+
required: false,
|
|
17
|
+
default: null,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
13
20
|
computed: {
|
|
14
21
|
localTableClass() {
|
|
15
22
|
return ['gl-table', this.tableClass];
|
|
@@ -19,7 +26,7 @@ export default {
|
|
|
19
26
|
</script>
|
|
20
27
|
|
|
21
28
|
<template>
|
|
22
|
-
<b-table-lite :table-class="localTableClass" v-bind="$attrs" v-on="$listeners">
|
|
29
|
+
<b-table-lite :table-class="localTableClass" :fields="fields" v-bind="$attrs" v-on="$listeners">
|
|
23
30
|
<template v-for="slot in Object.keys($scopedSlots)" #[slot]="scope">
|
|
24
31
|
<!-- @slot See https://bootstrap-vue.org/docs/components/table#comp-ref-b-table-lite-slots for available slots. -->
|
|
25
32
|
<slot :name="slot" v-bind="scope"></slot>
|
|
@@ -25,7 +25,8 @@ describe('GlSingleStat', () => {
|
|
|
25
25
|
});
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
const
|
|
28
|
+
const findItemByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
|
|
29
|
+
const findAnimatedNumber = () => wrapper.findComponent(GlAnimatedNumber);
|
|
29
30
|
|
|
30
31
|
describe('displays the correct default data', () => {
|
|
31
32
|
beforeEach(() => createWrapper());
|
|
@@ -41,7 +42,7 @@ describe('GlSingleStat', () => {
|
|
|
41
42
|
${'meta badge'} | ${'meta-badge'} | ${false} | ${null}
|
|
42
43
|
`('for the $element', ({ shouldDisplay, testId, expected, element }) => {
|
|
43
44
|
it(`${shouldDisplay ? 'displays' : "doesn't display"} the ${element}`, () => {
|
|
44
|
-
const el =
|
|
45
|
+
const el = findItemByTestId(testId);
|
|
45
46
|
|
|
46
47
|
expect(el.exists()).toBe(shouldDisplay);
|
|
47
48
|
|
|
@@ -60,7 +61,7 @@ describe('GlSingleStat', () => {
|
|
|
60
61
|
({ propValue, shouldUseAnimatedComponent }) => {
|
|
61
62
|
createWrapper({ value: propValue, shouldAnimate: true });
|
|
62
63
|
|
|
63
|
-
const el =
|
|
64
|
+
const el = findAnimatedNumber();
|
|
64
65
|
expect(el.exists()).toBe(shouldUseAnimatedComponent);
|
|
65
66
|
}
|
|
66
67
|
);
|
|
@@ -73,11 +74,11 @@ describe('GlSingleStat', () => {
|
|
|
73
74
|
`('$display the units when $event', async ({ event, expected }) => {
|
|
74
75
|
createWrapper({ unit, shouldAnimate: true });
|
|
75
76
|
|
|
76
|
-
|
|
77
|
+
findAnimatedNumber().vm.$emit(event);
|
|
77
78
|
|
|
78
79
|
await wrapper.vm.$nextTick();
|
|
79
80
|
|
|
80
|
-
expect(
|
|
81
|
+
expect(findItemByTestId('unit').classes('gl-opacity-0!')).toBe(expected);
|
|
81
82
|
});
|
|
82
83
|
});
|
|
83
84
|
});
|
|
@@ -92,7 +93,7 @@ describe('GlSingleStat', () => {
|
|
|
92
93
|
beforeEach(() => createWrapper(mockData));
|
|
93
94
|
|
|
94
95
|
it('displays a standalone icon', () => {
|
|
95
|
-
const el =
|
|
96
|
+
const el = findItemByTestId('meta-icon');
|
|
96
97
|
const variantSpecified = Object.keys(mockData).includes('variant');
|
|
97
98
|
|
|
98
99
|
expect(el.exists()).toBe(true);
|
|
@@ -103,7 +104,7 @@ describe('GlSingleStat', () => {
|
|
|
103
104
|
});
|
|
104
105
|
|
|
105
106
|
it('does not display a badge', () => {
|
|
106
|
-
expect(
|
|
107
|
+
expect(findItemByTestId('meta-badge').exists()).toBe(false);
|
|
107
108
|
});
|
|
108
109
|
});
|
|
109
110
|
|
|
@@ -117,11 +118,11 @@ describe('GlSingleStat', () => {
|
|
|
117
118
|
beforeEach(() => createWrapper(mockData));
|
|
118
119
|
|
|
119
120
|
it("doesn't display a standalone icon", () => {
|
|
120
|
-
expect(
|
|
121
|
+
expect(findItemByTestId('meta-icon').exists()).toBe(false);
|
|
121
122
|
});
|
|
122
123
|
|
|
123
124
|
it('displays a badge', () => {
|
|
124
|
-
const badge =
|
|
125
|
+
const badge = findItemByTestId('meta-badge');
|
|
125
126
|
const iconSpecified = Object.keys(mockData).includes('metaIcon');
|
|
126
127
|
const variantSpecified = Object.keys(mockData).includes('variant');
|
|
127
128
|
|
|
@@ -141,7 +142,7 @@ describe('GlSingleStat', () => {
|
|
|
141
142
|
beforeEach(() => createWrapper(mockData));
|
|
142
143
|
|
|
143
144
|
it('displays when specified', () => {
|
|
144
|
-
const el =
|
|
145
|
+
const el = findItemByTestId(testId);
|
|
145
146
|
|
|
146
147
|
expect(el.exists()).toBe(true);
|
|
147
148
|
});
|
|
@@ -161,11 +162,56 @@ describe('GlSingleStat', () => {
|
|
|
161
162
|
titleIconClass: classes,
|
|
162
163
|
});
|
|
163
164
|
|
|
164
|
-
expect(
|
|
165
|
+
expect(findItemByTestId('title-icon').classes()).toEqual(
|
|
165
166
|
expect.arrayContaining(['title-icon-class'])
|
|
166
167
|
);
|
|
167
168
|
}
|
|
168
169
|
);
|
|
169
170
|
});
|
|
171
|
+
|
|
172
|
+
describe.each([true, false])(`when the \`useDelimiters\` prop is %s`, (useDelimiters) => {
|
|
173
|
+
const initialValue = '100000.12';
|
|
174
|
+
|
|
175
|
+
it('should render the value as expected when not animated', () => {
|
|
176
|
+
createWrapper({
|
|
177
|
+
value: initialValue,
|
|
178
|
+
shouldAnimate: false,
|
|
179
|
+
useDelimiters,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const displayValue = useDelimiters ? '100,000.12' : initialValue;
|
|
183
|
+
|
|
184
|
+
expect(findItemByTestId('non-animated-value').text()).toBe(displayValue);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should pass the value and delimiter setting to `GlAnimatedNumber`', () => {
|
|
188
|
+
createWrapper({
|
|
189
|
+
value: initialValue,
|
|
190
|
+
shouldAnimate: true,
|
|
191
|
+
animationDecimalPlaces: 2,
|
|
192
|
+
useDelimiters,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(findAnimatedNumber().props()).toMatchObject({
|
|
196
|
+
number: Number(initialValue),
|
|
197
|
+
decimalPlaces: 2,
|
|
198
|
+
useDelimiters,
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should keep all the decimal points defined by the value', () => {
|
|
203
|
+
const decimalValue = '100000.000';
|
|
204
|
+
|
|
205
|
+
createWrapper({
|
|
206
|
+
value: decimalValue,
|
|
207
|
+
shouldAnimate: false,
|
|
208
|
+
useDelimiters,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const displayValue = useDelimiters ? '100,000.000' : decimalValue;
|
|
212
|
+
|
|
213
|
+
expect(findItemByTestId('non-animated-value').text()).toBe(displayValue);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
170
216
|
});
|
|
171
217
|
});
|
|
@@ -8,6 +8,7 @@ const generateProps = ({
|
|
|
8
8
|
title = 'Single stat',
|
|
9
9
|
value = '100',
|
|
10
10
|
unit = '',
|
|
11
|
+
useDelimiters = false,
|
|
11
12
|
metaText = '',
|
|
12
13
|
metaIcon = null,
|
|
13
14
|
titleIcon = null,
|
|
@@ -19,6 +20,7 @@ const generateProps = ({
|
|
|
19
20
|
title,
|
|
20
21
|
value,
|
|
21
22
|
unit,
|
|
23
|
+
useDelimiters,
|
|
22
24
|
metaText,
|
|
23
25
|
metaIcon,
|
|
24
26
|
titleIcon,
|
|
@@ -41,6 +43,7 @@ const Template = (args, { argTypes }) => ({
|
|
|
41
43
|
:title="title"
|
|
42
44
|
:value="value"
|
|
43
45
|
:unit="unit"
|
|
46
|
+
:use-delimiters="useDelimiters"
|
|
44
47
|
:variant="variant"
|
|
45
48
|
:meta-text="metaText"
|
|
46
49
|
:meta-icon="metaIcon"
|
|
@@ -63,6 +66,9 @@ WithMetaIcon.args = generateProps({ metaIcon });
|
|
|
63
66
|
export const WithTitleIcon = Template.bind({});
|
|
64
67
|
WithTitleIcon.args = generateProps({ titleIcon });
|
|
65
68
|
|
|
69
|
+
export const WithDelimiters = Template.bind({});
|
|
70
|
+
WithDelimiters.args = generateProps({ value: '10000', useDelimiters: true });
|
|
71
|
+
|
|
66
72
|
export default {
|
|
67
73
|
title: 'charts/single-stat',
|
|
68
74
|
component: GlSingleStat,
|
|
@@ -89,5 +95,9 @@ export default {
|
|
|
89
95
|
unit: {
|
|
90
96
|
control: 'text',
|
|
91
97
|
},
|
|
98
|
+
useDelimiters: {
|
|
99
|
+
control: 'boolean',
|
|
100
|
+
description: 'Requires the `value` property to be a valid Number or convertable to one',
|
|
101
|
+
},
|
|
92
102
|
},
|
|
93
103
|
};
|
|
@@ -3,6 +3,7 @@ import { badgeVariantOptions, variantCssColorMap } from '../../../utils/constant
|
|
|
3
3
|
import GlBadge from '../../base/badge/badge.vue';
|
|
4
4
|
import GlIcon from '../../base/icon/icon.vue';
|
|
5
5
|
import GlAnimatedNumber from '../../utilities/animated_number/animated_number.vue';
|
|
6
|
+
import { formatNumberToLocale } from '../../../utils/number_utils';
|
|
6
7
|
|
|
7
8
|
export default {
|
|
8
9
|
name: 'GlSingleStat',
|
|
@@ -25,6 +26,14 @@ export default {
|
|
|
25
26
|
required: false,
|
|
26
27
|
default: null,
|
|
27
28
|
},
|
|
29
|
+
/**
|
|
30
|
+
* Requires the `value` property to be a valid Number or convertible to one
|
|
31
|
+
*/
|
|
32
|
+
useDelimiters: {
|
|
33
|
+
type: Boolean,
|
|
34
|
+
required: false,
|
|
35
|
+
default: false,
|
|
36
|
+
},
|
|
28
37
|
variant: {
|
|
29
38
|
type: String,
|
|
30
39
|
required: false,
|
|
@@ -86,6 +95,17 @@ export default {
|
|
|
86
95
|
canAnimate() {
|
|
87
96
|
return this.shouldAnimate && !Number.isNaN(Number(this.value));
|
|
88
97
|
},
|
|
98
|
+
statValue() {
|
|
99
|
+
if (this.useDelimiters) {
|
|
100
|
+
const minimumFractionDigits = this.value.toString().split('.')[1]?.length || 0;
|
|
101
|
+
|
|
102
|
+
return formatNumberToLocale(this.value, {
|
|
103
|
+
minimumFractionDigits,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return this.value;
|
|
108
|
+
},
|
|
89
109
|
},
|
|
90
110
|
methods: {
|
|
91
111
|
setHideUnits(flag) {
|
|
@@ -116,10 +136,11 @@ export default {
|
|
|
116
136
|
v-if="canAnimate"
|
|
117
137
|
:number="Number(value)"
|
|
118
138
|
:decimal-places="animationDecimalPlaces"
|
|
139
|
+
:use-delimiters="useDelimiters"
|
|
119
140
|
@animating="setHideUnits(true)"
|
|
120
141
|
@animated="setHideUnits(false)"
|
|
121
142
|
/>
|
|
122
|
-
<span v-else data-testid="non-animated-value">{{
|
|
143
|
+
<span v-else data-testid="non-animated-value">{{ statValue }}</span></span
|
|
123
144
|
>
|
|
124
145
|
<span
|
|
125
146
|
v-if="unit"
|
|
@@ -9,11 +9,17 @@ const ACTION_ANIMATING = 'animating';
|
|
|
9
9
|
describe('GlAnimatedNumber', () => {
|
|
10
10
|
let wrapper;
|
|
11
11
|
|
|
12
|
-
const createComponent = ({
|
|
12
|
+
const createComponent = ({
|
|
13
|
+
number = 100,
|
|
14
|
+
decimalPlaces = 0,
|
|
15
|
+
useDelimiters = false,
|
|
16
|
+
animateOnMount = false,
|
|
17
|
+
} = {}) => {
|
|
13
18
|
wrapper = shallowMount(GlAnimatedNumber, {
|
|
14
19
|
propsData: {
|
|
15
20
|
number,
|
|
16
21
|
decimalPlaces,
|
|
22
|
+
useDelimiters,
|
|
17
23
|
duration,
|
|
18
24
|
animateOnMount,
|
|
19
25
|
},
|
|
@@ -40,20 +46,22 @@ describe('GlAnimatedNumber', () => {
|
|
|
40
46
|
|
|
41
47
|
describe('when animateOnMount is false', () => {
|
|
42
48
|
describe.each`
|
|
43
|
-
withDecimal | number
|
|
44
|
-
${false} | ${100}
|
|
45
|
-
${true} | ${100.2}
|
|
49
|
+
withDecimal | number | updatedNumber | decimalPlaces | useDelimiters | expectedInitialOnMount | expectedInitialOnUpdate
|
|
50
|
+
${false} | ${100} | ${200} | ${0} | ${false} | ${'100'} | ${'200'}
|
|
51
|
+
${true} | ${100.2} | ${200.2} | ${1} | ${false} | ${'100.2'} | ${'200.2'}
|
|
52
|
+
${true} | ${100000.2} | ${200000.2} | ${1} | ${true} | ${'100,000.2'} | ${'200,000.2'}
|
|
46
53
|
`(
|
|
47
|
-
'withDecimal
|
|
54
|
+
'when withDecimal = $withDecimal, useDelimiters = $useDelimiters',
|
|
48
55
|
({
|
|
49
56
|
number,
|
|
50
57
|
updatedNumber,
|
|
51
58
|
decimalPlaces,
|
|
59
|
+
useDelimiters,
|
|
52
60
|
expectedInitialOnMount,
|
|
53
61
|
expectedInitialOnUpdate,
|
|
54
62
|
}) => {
|
|
55
63
|
beforeEach(() => {
|
|
56
|
-
createComponent({ number, decimalPlaces });
|
|
64
|
+
createComponent({ number, decimalPlaces, useDelimiters });
|
|
57
65
|
});
|
|
58
66
|
|
|
59
67
|
it('displays the correct number on mount', async () => {
|
|
@@ -77,24 +85,28 @@ describe('GlAnimatedNumber', () => {
|
|
|
77
85
|
|
|
78
86
|
describe('when animateOnMount is true', () => {
|
|
79
87
|
describe.each`
|
|
80
|
-
withDecimal | number
|
|
81
|
-
${false} | ${100}
|
|
82
|
-
${true} | ${100.2}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
88
|
+
withDecimal | number | decimalPlaces | useDelimiters | expectedInitial | expectedEnd
|
|
89
|
+
${false} | ${100} | ${0} | ${false} | ${'0'} | ${'100'}
|
|
90
|
+
${true} | ${100.2} | ${1} | ${false} | ${'0.0'} | ${'100.2'}
|
|
91
|
+
${true} | ${100000.2} | ${1} | ${true} | ${'0.0'} | ${'100,000.2'}
|
|
92
|
+
`(
|
|
93
|
+
'when withDecimal = $withDecimal, useDelimiters = $useDelimiters',
|
|
94
|
+
({ number, decimalPlaces, useDelimiters, expectedInitial, expectedEnd }) => {
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
createComponent({ number, decimalPlaces, useDelimiters, animateOnMount: true });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('displays the correct initial number', () => {
|
|
100
|
+
expect(wrapper.text()).toBe(expectedInitial);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('displays the correct end number', async () => {
|
|
104
|
+
await runOutAnimationTimer();
|
|
105
|
+
|
|
106
|
+
expect(wrapper.text()).toBe(expectedEnd);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
);
|
|
98
110
|
});
|
|
99
111
|
|
|
100
112
|
describe('animation event emissions', () => {
|
|
@@ -4,7 +4,7 @@ import GlAnimatedNumber from './animated_number.vue';
|
|
|
4
4
|
|
|
5
5
|
const template = `
|
|
6
6
|
<div>
|
|
7
|
-
<gl-animated-number :number="updatedNumber" :decimalPlaces="decimalPlaces" :duration="duration" :animateOnMount="animateOnMount"/>
|
|
7
|
+
<gl-animated-number :number="updatedNumber" :decimalPlaces="decimalPlaces" :use-delimiters="useDelimiters" :duration="duration" :animateOnMount="animateOnMount"/>
|
|
8
8
|
<button @click="updateNumber">Update number</button>
|
|
9
9
|
</div>
|
|
10
10
|
`;
|
|
@@ -14,11 +14,13 @@ const defaultValue = (prop) => GlAnimatedNumber.props[prop].default;
|
|
|
14
14
|
const generateProps = ({
|
|
15
15
|
initialNumber = 100,
|
|
16
16
|
decimalPlaces = defaultValue('decimalPlaces'),
|
|
17
|
+
useDelimiters = false,
|
|
17
18
|
duration = 1000,
|
|
18
19
|
animateOnMount = defaultValue('animateOnMount'),
|
|
19
20
|
} = {}) => ({
|
|
20
21
|
initialNumber,
|
|
21
22
|
decimalPlaces,
|
|
23
|
+
useDelimiters,
|
|
22
24
|
duration,
|
|
23
25
|
animateOnMount,
|
|
24
26
|
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script>
|
|
2
|
+
import { formatNumberToLocale } from '../../../utils/number_utils';
|
|
3
|
+
|
|
2
4
|
export default {
|
|
3
5
|
name: 'AnimatedNumber',
|
|
4
6
|
props: {
|
|
@@ -22,6 +24,11 @@ export default {
|
|
|
22
24
|
required: false,
|
|
23
25
|
default: 0,
|
|
24
26
|
},
|
|
27
|
+
useDelimiters: {
|
|
28
|
+
type: Boolean,
|
|
29
|
+
required: false,
|
|
30
|
+
default: false,
|
|
31
|
+
},
|
|
25
32
|
animateOnMount: {
|
|
26
33
|
type: Boolean,
|
|
27
34
|
required: false,
|
|
@@ -36,7 +43,13 @@ export default {
|
|
|
36
43
|
},
|
|
37
44
|
computed: {
|
|
38
45
|
animatedNumber() {
|
|
39
|
-
|
|
46
|
+
const number = this.displayNumber.toFixed(this.decimalPlaces);
|
|
47
|
+
|
|
48
|
+
if (this.useDelimiters) {
|
|
49
|
+
return formatNumberToLocale(number, { minimumFractionDigits: this.decimalPlaces });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return number;
|
|
40
53
|
},
|
|
41
54
|
},
|
|
42
55
|
ready() {
|
|
@@ -94,3 +94,27 @@ export const engineeringNotation = (value, precision = 2) => {
|
|
|
94
94
|
|
|
95
95
|
return `${scaledMantissa}${allYourBase[scaledPower]}`;
|
|
96
96
|
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Formats a number as a locale-based string using `Intl.NumberFormat`.
|
|
100
|
+
*
|
|
101
|
+
* 2333 -> 2,333
|
|
102
|
+
* 232324 -> 232,324
|
|
103
|
+
*
|
|
104
|
+
* @param {Number|string} value - number to be converted
|
|
105
|
+
* @param {{}?} options - options to be passed to
|
|
106
|
+
* `Intl.NumberFormat` such as `unit` and `style`.
|
|
107
|
+
* @param {String|String[]} locales - If set, forces a different
|
|
108
|
+
* language code from the one currently in the document.
|
|
109
|
+
*
|
|
110
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
|
|
111
|
+
*
|
|
112
|
+
* @returns {String}
|
|
113
|
+
*/
|
|
114
|
+
export const formatNumberToLocale = (value, options = {}, locales = undefined) => {
|
|
115
|
+
if (Number.isNaN(Number(value))) {
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return new Intl.NumberFormat(locales, options).format(value);
|
|
120
|
+
};
|
|
@@ -86,4 +86,25 @@ describe('number utils', () => {
|
|
|
86
86
|
expect(numberUtils.modulo(n, divisor)).toBe(result);
|
|
87
87
|
});
|
|
88
88
|
});
|
|
89
|
+
|
|
90
|
+
describe('formatNumberToLocale', () => {
|
|
91
|
+
it('should format the provided string of either an integer or float', () => {
|
|
92
|
+
expect(numberUtils.formatNumberToLocale('1234')).toEqual('1,234');
|
|
93
|
+
expect(numberUtils.formatNumberToLocale('222222.233')).toEqual('222,222.233');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should not format the provided string if it contains no numbers', () => {
|
|
97
|
+
expect(numberUtils.formatNumberToLocale('aaaa')).toEqual('aaaa');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should use the options if they are provided', () => {
|
|
101
|
+
expect(numberUtils.formatNumberToLocale('222222.233', { minimumFractionDigits: 4 })).toEqual(
|
|
102
|
+
'222,222.2330'
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should override the locale if one is provided', () => {
|
|
107
|
+
expect(numberUtils.formatNumberToLocale('222222.233', {}, 'de-de')).toEqual('222.222,233');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
89
110
|
});
|