@gitlab/ui 66.3.1 → 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.
Files changed (29) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/components/base/table/table.js +7 -2
  3. package/dist/components/base/table_lite/table_lite.js +7 -2
  4. package/dist/components/charts/single_stat/single_stat.js +20 -1
  5. package/dist/components/utilities/animated_number/animated_number.js +13 -1
  6. package/dist/tokens/css/tokens.css +1 -1
  7. package/dist/tokens/css/tokens.dark.css +1 -1
  8. package/dist/tokens/js/tokens.dark.js +1 -1
  9. package/dist/tokens/js/tokens.js +1 -1
  10. package/dist/tokens/scss/_tokens.dark.scss +1 -1
  11. package/dist/tokens/scss/_tokens.scss +1 -1
  12. package/dist/utility_classes.css +1 -1
  13. package/dist/utility_classes.css.map +1 -1
  14. package/dist/utils/number_utils.js +26 -1
  15. package/package.json +5 -3
  16. package/src/components/base/table/table.spec.js +9 -0
  17. package/src/components/base/table/table.vue +6 -1
  18. package/src/components/base/table_lite/table_lite.spec.js +9 -0
  19. package/src/components/base/table_lite/table_lite.vue +9 -2
  20. package/src/components/charts/single_stat/single_stat.spec.js +57 -11
  21. package/src/components/charts/single_stat/single_stat.stories.js +10 -0
  22. package/src/components/charts/single_stat/single_stat.vue +22 -1
  23. package/src/components/utilities/animated_number/animated_number.spec.js +36 -24
  24. package/src/components/utilities/animated_number/animated_number.stories.js +3 -1
  25. package/src/components/utilities/animated_number/animated_number.vue +14 -1
  26. package/src/scss/utilities.scss +6 -0
  27. package/src/scss/utility-mixins/spacing.scss +4 -0
  28. package/src/utils/number_utils.js +24 -0
  29. package/src/utils/number_utils.spec.js +21 -0
@@ -96,4 +96,29 @@ const engineeringNotation = function (value) {
96
96
  return `${scaledMantissa}${allYourBase[scaledPower]}`;
97
97
  };
98
98
 
99
- export { addition, average, engineeringNotation, modulo, sum };
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.1",
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.10",
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
- "cypress": "12.17.4",
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: { tableClass },
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 findIemByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
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 = findIemByTestId(testId);
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 = wrapper.findComponent(GlAnimatedNumber);
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
- wrapper.findComponent(GlAnimatedNumber).vm.$emit(event);
77
+ findAnimatedNumber().vm.$emit(event);
77
78
 
78
79
  await wrapper.vm.$nextTick();
79
80
 
80
- expect(findIemByTestId('unit').classes('gl-opacity-0!')).toBe(expected);
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 = findIemByTestId('meta-icon');
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(findIemByTestId('meta-badge').exists()).toBe(false);
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(findIemByTestId('meta-icon').exists()).toBe(false);
121
+ expect(findItemByTestId('meta-icon').exists()).toBe(false);
121
122
  });
122
123
 
123
124
  it('displays a badge', () => {
124
- const badge = findIemByTestId('meta-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 = findIemByTestId(testId);
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(findIemByTestId('title-icon').classes()).toEqual(
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">{{ value }}</span></span
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 = ({ number = 100, decimalPlaces = 0, animateOnMount = false } = {}) => {
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 | updatedNumber | decimalPlaces | expectedInitialOnMount | expectedInitialOnUpdate
44
- ${false} | ${100} | ${200} | ${0} | ${'100'} | ${'200'}
45
- ${true} | ${100.2} | ${200.2} | ${1} | ${'100.2'} | ${'200.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 === $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 | decimalPlaces | expectedInitial
81
- ${false} | ${100} | ${0} | ${'0'}
82
- ${true} | ${100.2} | ${1} | ${'0.0'}
83
- `('withDecimal === $withDecimal', ({ number, decimalPlaces, expectedInitial }) => {
84
- beforeEach(() => {
85
- createComponent({ number, decimalPlaces, animateOnMount: true });
86
- });
87
-
88
- it('displays the correct intial number', () => {
89
- expect(wrapper.text()).toBe(expectedInitial);
90
- });
91
-
92
- it('displays the correct end number', async () => {
93
- await runOutAnimationTimer();
94
-
95
- expect(wrapper.text()).toBe(`${number}`);
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
- return this.displayNumber.toFixed(this.decimalPlaces);
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() {
@@ -6847,6 +6847,12 @@
6847
6847
  margin-top: #{$gl-spacing-scale-3} !important;
6848
6848
  }
6849
6849
  }
6850
+ .gl-gap-1 {
6851
+ gap: $gl-spacing-scale-1;
6852
+ }
6853
+ .gl-gap-1\! {
6854
+ gap: $gl-spacing-scale-1 !important;
6855
+ }
6850
6856
  .gl-gap-2 {
6851
6857
  gap: $gl-spacing-scale-2;
6852
6858
  }
@@ -868,6 +868,10 @@
868
868
  }
869
869
  }
870
870
 
871
+ @mixin gl-gap-1 {
872
+ gap: $gl-spacing-scale-1;
873
+ }
874
+
871
875
  @mixin gl-gap-2 {
872
876
  gap: $gl-spacing-scale-2;
873
877
  }
@@ -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
  });