@gitlab/ui 42.22.0 → 42.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "42.22.0",
3
+ "version": "42.23.1",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -83,7 +83,7 @@
83
83
  "@babel/preset-env": "^7.10.2",
84
84
  "@gitlab/eslint-plugin": "14.0.0",
85
85
  "@gitlab/stylelint-config": "4.1.0",
86
- "@gitlab/svgs": "2.28.0",
86
+ "@gitlab/svgs": "2.30.0",
87
87
  "@rollup/plugin-commonjs": "^11.1.0",
88
88
  "@rollup/plugin-node-resolve": "^7.1.3",
89
89
  "@rollup/plugin-replace": "^2.3.2",
@@ -96,8 +96,9 @@
96
96
  "@storybook/theming": "6.5.9",
97
97
  "@storybook/vue": "6.5.9",
98
98
  "@vue/test-utils": "1.3.0",
99
+ "@vue/vue2-jest": "27.0.0",
99
100
  "autoprefixer": "^9.7.6",
100
- "babel-jest": "^26.6.3",
101
+ "babel-jest": "^27.5.1",
101
102
  "babel-loader": "^8.0.5",
102
103
  "babel-plugin-lodash": "^3.3.4",
103
104
  "babel-plugin-require-context-hook": "^1.0.0",
@@ -113,7 +114,9 @@
113
114
  "glob": "^7.2.0",
114
115
  "identity-obj-proxy": "^3.0.0",
115
116
  "inquirer-select-directory": "^1.2.0",
116
- "jest": "^26.6.3",
117
+ "jest": "^27.5.1",
118
+ "jest-circus": "27.5.1",
119
+ "jest-environment-jsdom": "27.5.1",
117
120
  "jest-raw-loader": "^1.0.1",
118
121
  "jest-serializer-vue": "^2.0.2",
119
122
  "markdownlint-cli": "^0.29.0",
@@ -144,7 +147,6 @@
144
147
  "stylelint-config-prettier": "9.0.3",
145
148
  "stylelint-prettier": "2.0.0",
146
149
  "vue": "2.6.11",
147
- "vue-jest": "4.0.0-rc.0",
148
150
  "vue-loader": "^15.8.3",
149
151
  "vue-template-compiler": "2.6.11"
150
152
  },
@@ -64,6 +64,7 @@ const SHAPE = {
64
64
  ___.4
65
65
  ___.5
66
66
  `,
67
+
67
68
  COMPLEX: `
68
69
  _.1
69
70
  __.2
@@ -149,11 +150,10 @@ describe('GlFormCheckboxTree', () => {
149
150
  `('when checking a parent checkbox in a $description tree', ({ shape, boxToCheck }) => {
150
151
  let checkbox;
151
152
 
152
- beforeEach(() => {
153
+ beforeEach(async () => {
153
154
  createWrapper({ options: getOptions(shape) });
154
155
  checkbox = wrapper.find(`[data-qa-selector="${QA_PREFIX}${boxToCheck}"]`);
155
- checkbox.find('input').trigger('click');
156
- return wrapper.vm.$nextTick();
156
+ await checkbox.find('input').setChecked();
157
157
  });
158
158
 
159
159
  it("checks all of the checkbox's children, if any", () => {
@@ -174,10 +174,11 @@ describe('GlFormCheckboxTree', () => {
174
174
  `(
175
175
  'when checking child checkboxes in a $description tree',
176
176
  ({ shape, boxesToCheck, indeterminateBoxes, checkedParents }) => {
177
- beforeEach(() => {
177
+ beforeEach(async () => {
178
178
  createWrapper({ options: getOptions(shape) });
179
- boxesToCheck.forEach((box) => findCheckboxInput(findCheckboxByValue(box)).trigger('click'));
180
- return wrapper.vm.$nextTick();
179
+ await Promise.allSettled(
180
+ boxesToCheck.map((box) => findCheckboxInput(findCheckboxByValue(box)).setChecked())
181
+ );
181
182
  });
182
183
 
183
184
  it('parents that have remaining unchecked children become indeterminate', () => {
@@ -205,12 +206,11 @@ describe('GlFormCheckboxTree', () => {
205
206
  `(
206
207
  'when unchecking checkboxes in a $description tree',
207
208
  ({ shape, initiallyChecked, boxesToUncheck, indeterminateBoxes, uncheckedBoxes }) => {
208
- beforeEach(() => {
209
+ beforeEach(async () => {
209
210
  createWrapper({ options: getOptions(shape), [V_MODEL.PROP]: initiallyChecked });
210
- boxesToUncheck.forEach((box) =>
211
- findCheckboxInput(findCheckboxByValue(box)).trigger('click')
211
+ await Promise.allSettled(
212
+ boxesToUncheck.map((box) => findCheckboxInput(findCheckboxByValue(box)).setChecked(false))
212
213
  );
213
- return wrapper.vm.$nextTick();
214
214
  });
215
215
 
216
216
  it("unchecks all of the checkbox's children if any", () => {
@@ -253,12 +253,11 @@ describe('GlFormCheckboxTree', () => {
253
253
  });
254
254
 
255
255
  it('once toggled, puts all checkboxes in the correct state', async () => {
256
- await toggleAllCheckbox.trigger('click');
257
- return wrapper.vm.$nextTick(() => {
258
- findCheckboxes().wrappers.forEach((checkbox) => {
259
- expect(findCheckboxInput(checkbox).element.checked).toBe(finallyChecked);
260
- expect(findCheckboxInput(checkbox).element.indeterminate).toBe(false);
261
- });
256
+ await toggleAllCheckbox.setChecked(finallyChecked);
257
+
258
+ findCheckboxes().wrappers.forEach((checkbox) => {
259
+ expect(findCheckboxInput(checkbox).element.checked).toBe(finallyChecked);
260
+ expect(findCheckboxInput(checkbox).element.indeterminate).toBe(false);
262
261
  });
263
262
  });
264
263
  }
@@ -1,9 +1,5 @@
1
1
  .gl-form-combobox {
2
- .dropdown-full-width {
3
- @include gl-w-full;
4
- }
5
-
6
- .show-dropdown {
2
+ .gl-form-combobox-inner {
7
3
  max-height: $gl-max-dropdown-max-height;
8
4
  }
9
5
  }
@@ -1,3 +1,4 @@
1
+ import { nextTick } from 'vue';
1
2
  import { mount } from '@vue/test-utils';
2
3
  import GlDropdownItem from '../../dropdown/dropdown_item.vue';
3
4
  import GlFormInput from '../form_input/form_input.vue';
@@ -65,7 +66,9 @@ describe('GlFormCombobox', () => {
65
66
  const findInput = () => wrapper.findComponent(GlFormInput);
66
67
  const findInputValue = () => findInput().element.value;
67
68
  const setInput = (val) => findInput().setValue(val);
68
- const arrowDown = () => findInput().trigger('keydown.down');
69
+ const arrowDown = () => findDropdown().trigger('keydown.down');
70
+ const arrowUp = () => findDropdown().trigger('keydown.up');
71
+ const enter = () => wrapper.find('[aria-selected="true"]').trigger('keydown.enter');
69
72
  const findFirstAction = () => wrapper.find('[data-testid="combobox-action"]');
70
73
 
71
74
  beforeAll(() => {
@@ -128,8 +131,10 @@ describe('GlFormCombobox', () => {
128
131
  describe('on down arrow + enter', () => {
129
132
  it('selects the next item in the list and closes the dropdown', async () => {
130
133
  await setInput(partialToken);
131
- findInput().trigger('keydown.down');
132
- await findInput().trigger('keydown.enter');
134
+ arrowDown();
135
+ await nextTick();
136
+ enter();
137
+ await nextTick();
133
138
 
134
139
  if (valueType === 'string') {
135
140
  expect(findInputValue()).toBe(partialTokenMatch[0]);
@@ -141,7 +146,9 @@ describe('GlFormCombobox', () => {
141
146
  it('loops to the top when it reaches the bottom', async () => {
142
147
  await setInput(partialToken);
143
148
  doTimes(findDropdownOptions().length + 1, arrowDown);
144
- await findInput().trigger('keydown.enter');
149
+ await nextTick();
150
+ enter();
151
+ await nextTick();
145
152
 
146
153
  if (valueType === 'string') {
147
154
  expect(findInputValue()).toBe(partialTokenMatch[0]);
@@ -157,8 +164,10 @@ describe('GlFormCombobox', () => {
157
164
 
158
165
  await wrapper.vm.$nextTick();
159
166
  doTimes(3, arrowDown);
160
- findInput().trigger('keydown.up');
161
- findInput().trigger('keydown.enter');
167
+ arrowUp();
168
+ await nextTick();
169
+ enter();
170
+ await nextTick();
162
171
 
163
172
  await wrapper.vm.$nextTick();
164
173
 
@@ -172,9 +181,11 @@ describe('GlFormCombobox', () => {
172
181
 
173
182
  it('loops to the bottom when it reaches the top', async () => {
174
183
  await setInput(partialToken);
175
- findInput().trigger('keydown.down');
176
- findInput().trigger('keydown.up');
177
- await findInput().trigger('keydown.enter');
184
+ arrowDown();
185
+ arrowUp();
186
+ await nextTick();
187
+ enter();
188
+ await nextTick();
178
189
 
179
190
  if (valueType === 'string') {
180
191
  expect(findInputValue()).toBe(partialTokenMatch[partialTokenMatch.length - 1]);
@@ -187,11 +198,11 @@ describe('GlFormCombobox', () => {
187
198
  });
188
199
 
189
200
  describe('on enter with no item highlighted', () => {
190
- it('does not select any item and closes the dropdown', async () => {
201
+ it('does nothing', async () => {
191
202
  await setInput(partialToken);
192
203
  await findInput().trigger('keydown.enter');
193
204
  expect(findInputValue()).toBe(partialToken);
194
- expect(findDropdown().isVisible()).toBe(false);
205
+ expect(findDropdown().isVisible()).toBe(true);
195
206
  });
196
207
  });
197
208
 
@@ -268,9 +279,10 @@ describe('GlFormCombobox', () => {
268
279
 
269
280
  it('keyboard navigation and executes function on enter', async () => {
270
281
  await setInput('dog');
271
- findInput().trigger('keydown.down');
272
- findInput().trigger('keydown.down');
273
- await findInput().trigger('keydown.enter');
282
+ doTimes(2, arrowDown);
283
+ await nextTick();
284
+ enter();
285
+ await nextTick();
274
286
 
275
287
  expect(actionSpy).toHaveBeenCalled();
276
288
  expect(findDropdown().isVisible()).toBe(false);
@@ -54,6 +54,14 @@ export default {
54
54
  required: false,
55
55
  default: false,
56
56
  },
57
+ /**
58
+ * Placeholder text for input field
59
+ */
60
+ placeholder: {
61
+ type: String,
62
+ required: false,
63
+ default: undefined,
64
+ },
57
65
  },
58
66
  data() {
59
67
  return {
@@ -88,16 +96,8 @@ export default {
88
96
  },
89
97
  watch: {
90
98
  tokenList(newList) {
91
- const filteredTokens = newList.filter((token) => {
92
- if (this.matchValueToAttr) {
93
- // For API driven tokens, we don't need extra filtering
94
- return token;
95
- }
96
- return token.toLowerCase().includes(this.value.toLowerCase());
97
- });
98
-
99
- if (filteredTokens.length) {
100
- this.openSuggestions(filteredTokens);
99
+ if (newList.length) {
100
+ this.openSuggestions(newList);
101
101
  } else {
102
102
  this.results = [];
103
103
  this.arrowCounter = -1;
@@ -121,35 +121,33 @@ export default {
121
121
  this.closeSuggestions();
122
122
  }
123
123
  },
124
- onArrowDown() {
125
- const newCount = this.arrowCounter + 1;
124
+ focusItem(index) {
125
+ this.$refs.suggestionsMenu
126
+ .querySelectorAll('.gl-new-dropdown-item')
127
+ [index]?.querySelector('button')
128
+ .focus();
129
+ },
130
+ onArrowDown(e) {
131
+ e.preventDefault();
132
+ let newCount = this.arrowCounter + 1;
126
133
 
127
134
  if (newCount >= this.allItems.length) {
128
- this.arrowCounter = 0;
129
- return;
135
+ newCount = 0;
130
136
  }
131
137
 
132
138
  this.arrowCounter = newCount;
133
- this.$refs.results[newCount]?.$el.scrollIntoView(false);
139
+ this.focusItem(newCount);
134
140
  },
135
- onArrowUp() {
136
- const newCount = this.arrowCounter - 1;
141
+ onArrowUp(e) {
142
+ e.preventDefault();
143
+ let newCount = this.arrowCounter - 1;
137
144
 
138
145
  if (newCount < 0) {
139
- this.arrowCounter = this.allItems.length - 1;
140
- return;
146
+ newCount = this.allItems.length - 1;
141
147
  }
142
148
 
143
149
  this.arrowCounter = newCount;
144
- this.$refs.results[newCount]?.$el.scrollIntoView(true);
145
- },
146
- onEnter() {
147
- const focusedItem = this.allItems[this.arrowCounter] || this.value;
148
- if (focusedItem.fn) {
149
- this.selectAction(focusedItem);
150
- } else {
151
- this.selectToken(focusedItem);
152
- }
150
+ this.focusItem(newCount);
153
151
  },
154
152
  onEsc() {
155
153
  if (!this.showSuggestions) {
@@ -198,6 +196,9 @@ export default {
198
196
  this.$emit('input', this.value);
199
197
  this.closeSuggestions();
200
198
  },
199
+ resetCounter() {
200
+ this.arrowCounter = -1;
201
+ },
201
202
  },
202
203
  };
203
204
  </script>
@@ -219,10 +220,11 @@ export default {
219
220
  :aria-controls="suggestionsId"
220
221
  aria-haspopup="listbox"
221
222
  :autofocus="autofocus"
223
+ :placeholder="placeholder"
222
224
  @input="onEntry"
225
+ @focus="resetCounter"
223
226
  @keydown.down="onArrowDown"
224
227
  @keydown.up="onArrowUp"
225
- @keydown.enter.prevent="onEnter"
226
228
  @keydown.esc.stop="onEsc"
227
229
  @keydown.tab="closeSuggestions"
228
230
  />
@@ -231,8 +233,12 @@ export default {
231
233
  <ul
232
234
  v-show="showSuggestions && !userDismissedResults"
233
235
  :id="suggestionsId"
236
+ ref="suggestionsMenu"
234
237
  data-testid="combobox-dropdown"
235
- class="dropdown-menu dropdown-full-width show-dropdown gl-list-style-none gl-pl-0 gl-mb-0 gl-display-flex gl-flex-direction-column"
238
+ class="dropdown-menu gl-w-full gl-form-combobox-inner gl-list-style-none gl-pl-0 gl-mb-0 gl-display-flex gl-flex-direction-column"
239
+ @keydown.down="onArrowDown"
240
+ @keydown.up="onArrowUp"
241
+ @keydown.esc.stop="onEsc"
236
242
  >
237
243
  <li class="gl-overflow-y-auto show-dropdown">
238
244
  <ul class="gl-list-style-none gl-pl-0 gl-mb-0">
@@ -241,10 +247,10 @@ export default {
241
247
  ref="results"
242
248
  :key="i"
243
249
  role="option"
244
- :class="{ 'gl-bg-gray-50': i === arrowCounter }"
245
250
  :aria-selected="i === arrowCounter"
246
251
  tabindex="-1"
247
252
  @click="selectToken(result)"
253
+ @keydown.enter.native="selectToken(result)"
248
254
  >
249
255
  <!-- @slot The suggestion result item to display. -->
250
256
  <slot name="result" :item="result">{{ result }}</slot>
@@ -258,11 +264,11 @@ export default {
258
264
  v-for="(action, i) in actionList"
259
265
  :key="i + resultsLength"
260
266
  role="option"
261
- :class="{ 'gl-bg-gray-50': i + resultsLength === arrowCounter }"
262
267
  :aria-selected="i + resultsLength === arrowCounter"
263
268
  tabindex="-1"
264
269
  data-testid="combobox-action"
265
270
  @click="selectAction(action)"
271
+ @keydown.enter.native="selectAction(action)"
266
272
  >
267
273
  <!-- @slot The action item to display. -->
268
274
  <slot name="action" :item="action">{{ action.label }}</slot>
@@ -1,5 +1,4 @@
1
1
  import { mount } from '@vue/test-utils';
2
- import { nextTick } from 'vue';
3
2
  import GlFormRadio from './form_radio.vue';
4
3
 
5
4
  describe('GlFormRadio', () => {
@@ -64,7 +63,6 @@ describe('GlFormRadio', () => {
64
63
  describe('when the selected value is changed programmatically', () => {
65
64
  beforeEach(() => {
66
65
  wrapper.vm.selected = secondOption.value;
67
- return nextTick();
68
66
  });
69
67
 
70
68
  it('emits an input event, but not a change event on each radio', () => {
@@ -83,11 +81,14 @@ describe('GlFormRadio', () => {
83
81
  describe('when the selected value is changed by the user', () => {
84
82
  let radio;
85
83
 
86
- beforeEach(() => {
84
+ beforeEach(async () => {
87
85
  radio = findRadio(secondOption.value);
88
86
 
89
- radio.trigger('click');
90
- return nextTick();
87
+ // NOTE: We can't use "setChecked" here because
88
+ // it sets value programmatically under the hood and this
89
+ // does not pass value for the "change" event.
90
+ await radio.trigger('click');
91
+ await radio.trigger('change');
91
92
  });
92
93
 
93
94
  it('emits an input event on each radio, and a change event on the newly selected radio', () => {
@@ -1,5 +1,4 @@
1
1
  import { mount } from '@vue/test-utils';
2
- import { nextTick } from 'vue';
3
2
  import GlFormRadioGroup from './form_radio_group.vue';
4
3
 
5
4
  describe('GlFormRadioGroup', () => {
@@ -43,7 +42,6 @@ describe('GlFormRadioGroup', () => {
43
42
  describe('when the selected value is changed programmatically', () => {
44
43
  beforeEach(() => {
45
44
  wrapper.vm.selected = secondOption.value;
46
- return nextTick();
47
45
  });
48
46
 
49
47
  it('emits an input event, but not a change event', () => {
@@ -60,14 +58,17 @@ describe('GlFormRadioGroup', () => {
60
58
  describe('when the selected value is changed by the user', () => {
61
59
  let radio;
62
60
 
63
- beforeEach(() => {
61
+ beforeEach(async () => {
64
62
  radio = findRadio(secondOption.value);
65
63
 
66
- radio.trigger('click');
67
- return nextTick();
64
+ // NOTE: We can't use "setChecked" here because
65
+ // it sets value programmatically under the hood and this
66
+ // does not pass value for the "change" event.
67
+ await radio.trigger('click');
68
+ await radio.trigger('change');
68
69
  });
69
70
 
70
- it('emits an input event and a change event', () => {
71
+ it('emits an input event and a change event', async () => {
71
72
  expect(wrapper.findComponent(GlFormRadioGroup).emitted()).toEqual({
72
73
  input: [[secondOption.value]],
73
74
  change: [[secondOption.value]],
@@ -37,6 +37,10 @@ table.gl-table {
37
37
  }
38
38
  }
39
39
 
40
+ caption {
41
+ @include gl-pt-2;
42
+ }
43
+
40
44
  @mixin gl-tmp-stacked-override {
41
45
  tbody > tr {
42
46
  &::after {
@@ -18,10 +18,16 @@ const tableItems = [
18
18
  },
19
19
  ];
20
20
 
21
- const generateProps = ({ fixed = false, footClone = false, stacked = false } = {}) => ({
21
+ const generateProps = ({
22
+ fixed = false,
23
+ footClone = false,
24
+ stacked = false,
25
+ caption = '',
26
+ } = {}) => ({
22
27
  fixed,
23
28
  footClone,
24
29
  stacked,
30
+ caption,
25
31
  });
26
32
 
27
33
  export const Default = (args, { argTypes }) => ({
@@ -37,7 +43,11 @@ export const Default = (args, { argTypes }) => ({
37
43
  hover
38
44
  selectable
39
45
  selected-variant="primary"
40
- />
46
+ >
47
+ <template v-if="caption" #table-caption>
48
+ {{ caption }}
49
+ </template>
50
+ </gl-table>
41
51
  `,
42
52
  fields: [
43
53
  {
@@ -5,6 +5,12 @@ import { defaultHeight, defaultWidth, validRenderers } from '../../../utils/char
5
5
  import createTheme, { themeName } from '../../../utils/charts/theme';
6
6
  import { GlResizeObserverDirective } from '../../../directives/resize_observer/resize_observer';
7
7
 
8
+ /**
9
+ * Allowed values by eCharts
10
+ * https://echarts.apache.org/en/api.html#echartsInstance.resize
11
+ */
12
+ const sizeValidator = (size) => Number.isFinite(size) || size === 'auto' || size == null;
13
+
8
14
  export default {
9
15
  directives: {
10
16
  resizeObserver: GlResizeObserverDirective,
@@ -25,14 +31,16 @@ export default {
25
31
  default: false,
26
32
  },
27
33
  width: {
28
- type: Number,
34
+ type: [Number, String],
29
35
  required: false,
30
36
  default: null,
37
+ validator: sizeValidator,
31
38
  },
32
39
  height: {
33
- type: Number,
40
+ type: [Number, String],
34
41
  required: false,
35
42
  default: null,
43
+ validator: sizeValidator,
36
44
  },
37
45
  groupId: {
38
46
  type: String,
@@ -6340,6 +6340,16 @@
6340
6340
  margin-bottom: $gl-spacing-scale-5 !important;
6341
6341
  }
6342
6342
  }
6343
+ .gl-md-ml-2 {
6344
+ @include gl-media-breakpoint-up(md) {
6345
+ margin-left: $gl-spacing-scale-2;
6346
+ }
6347
+ }
6348
+ .gl-md-ml-2\! {
6349
+ @include gl-media-breakpoint-up(md) {
6350
+ margin-left: $gl-spacing-scale-2 !important;
6351
+ }
6352
+ }
6343
6353
  .gl-md-ml-3 {
6344
6354
  @include gl-media-breakpoint-up(md) {
6345
6355
  margin-left: $gl-spacing-scale-3;
@@ -911,6 +911,12 @@
911
911
  }
912
912
  }
913
913
 
914
+ @mixin gl-md-ml-2 {
915
+ @include gl-media-breakpoint-up(md) {
916
+ @include gl-ml-2;
917
+ }
918
+ }
919
+
914
920
  @mixin gl-md-ml-3 {
915
921
  @include gl-media-breakpoint-up(md) {
916
922
  @include gl-ml-3;