@gitlab/ui 49.1.0 → 49.2.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.
@@ -35,6 +35,10 @@ const badgeVariantOptions = {
35
35
  danger: 'danger',
36
36
  tier: 'tier'
37
37
  };
38
+ const badgeIconSizeOptions = {
39
+ sm: 12,
40
+ md: 16
41
+ };
38
42
  const variantCssColorMap = {
39
43
  muted: 'gl-text-gray-500',
40
44
  neutral: 'gl-text-blue-100',
@@ -248,4 +252,4 @@ const loadingIconSizes = {
248
252
  'xl (64x64)': 'xl'
249
253
  };
250
254
 
251
- export { COMMA, LEFT_MOUSE_BUTTON, alertVariantIconMap, alertVariantOptions, alignOptions, avatarShapeOptions, avatarSizeOptions, avatarsInlineSizeOptions, badgeForButtonOptions, badgeSizeOptions, badgeVariantOptions, bannerVariants, buttonCategoryOptions, buttonSizeOptions, buttonSizeOptionsMap, buttonVariantOptions, colorThemes, columnOptions, defaultDateFormat, drawerVariants, dropdownVariantOptions, focusableTags, formInputSizes, formStateOptions, glThemes, iconSizeOptions, keyboard, labelColorOptions, labelSizeOptions, loadingIconSizes, maxZIndex, modalButtonDefaults, modalSizeOptions, popoverPlacements, resizeDebounceTime, tabsButtonDefaults, targetOptions, toggleLabelPosition, tokenVariants, tooltipActionEvents, tooltipDelay, tooltipPlacements, triggerVariantOptions, truncateOptions, variantCssColorMap, variantOptions, variantOptionsWithNoDefault, viewModeOptions };
255
+ export { COMMA, LEFT_MOUSE_BUTTON, alertVariantIconMap, alertVariantOptions, alignOptions, avatarShapeOptions, avatarSizeOptions, avatarsInlineSizeOptions, badgeForButtonOptions, badgeIconSizeOptions, badgeSizeOptions, badgeVariantOptions, bannerVariants, buttonCategoryOptions, buttonSizeOptions, buttonSizeOptionsMap, buttonVariantOptions, colorThemes, columnOptions, defaultDateFormat, drawerVariants, dropdownVariantOptions, focusableTags, formInputSizes, formStateOptions, glThemes, iconSizeOptions, keyboard, labelColorOptions, labelSizeOptions, loadingIconSizes, maxZIndex, modalButtonDefaults, modalSizeOptions, popoverPlacements, resizeDebounceTime, tabsButtonDefaults, targetOptions, toggleLabelPosition, tokenVariants, tooltipActionEvents, tooltipDelay, tooltipPlacements, triggerVariantOptions, truncateOptions, variantCssColorMap, variantOptions, variantOptionsWithNoDefault, viewModeOptions };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "49.1.0",
3
+ "version": "49.2.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -17,13 +17,14 @@ describe('badge', () => {
17
17
 
18
18
  describe('with "icon" prop', () => {
19
19
  describe.each`
20
- scenario | hasSlot | iconName | expectedRole
21
- ${'icon-only'} | ${false} | ${'warning'} | ${'img'}
22
- ${'icon and slot'} | ${true} | ${'warning'} | ${undefined}
23
- `('with $scenario', ({ iconName, hasSlot, expectedRole }) => {
20
+ scenario | hasSlot | iconName | iconSize | expectedIconSize | expectedRole
21
+ ${'icon-only'} | ${false} | ${'warning'} | ${'md'} | ${16} | ${'img'}
22
+ ${'16px icon and slot'} | ${true} | ${'warning'} | ${'md'} | ${16} | ${undefined}
23
+ ${'12px icon and slot'} | ${true} | ${'warning'} | ${'sm'} | ${12} | ${undefined}
24
+ `('with $scenario', ({ iconName, iconSize, expectedIconSize, hasSlot, expectedRole }) => {
24
25
  beforeEach(() => {
25
26
  const slots = hasSlot ? { default: 'slot-content' } : undefined;
26
- createComponent({ propsData: { icon: iconName }, slots });
27
+ createComponent({ propsData: { icon: iconName, iconSize }, slots });
27
28
  });
28
29
 
29
30
  it(`sets badge "role" attribute to ${expectedRole}`, () => {
@@ -42,6 +43,10 @@ describe('badge', () => {
42
43
 
43
44
  expect(icon.classes('gl-mr-2')).toBe(hasSlot);
44
45
  });
46
+
47
+ it('with correct size', () => {
48
+ expect(findIcon().props('size')).toBe(expectedIconSize);
49
+ });
45
50
  });
46
51
  });
47
52
  });
@@ -1,6 +1,10 @@
1
1
  import iconSpriteInfo from '@gitlab/svgs/dist/icons.json';
2
2
  import { GlBadge } from '../../../index';
3
- import { badgeSizeOptions, badgeVariantOptions } from '../../../utils/constants';
3
+ import {
4
+ badgeSizeOptions,
5
+ badgeVariantOptions,
6
+ badgeIconSizeOptions,
7
+ } from '../../../utils/constants';
4
8
  import { disableControls } from '../../../utils/stories_utils';
5
9
  import readme from './badge.md';
6
10
 
@@ -10,6 +14,7 @@ const template = `
10
14
  :variant="variant"
11
15
  :size="size"
12
16
  :icon="icon"
17
+ :iconSize="iconSize"
13
18
  >{{ content }}</gl-badge>
14
19
  `;
15
20
 
@@ -21,12 +26,14 @@ const generateProps = ({
21
26
  href = '',
22
27
  content = 'TestBadge',
23
28
  icon = '',
29
+ iconSize = defaultValue('iconSize'),
24
30
  } = {}) => ({
25
31
  variant,
26
32
  size,
27
33
  href,
28
34
  content,
29
35
  icon,
36
+ iconSize,
30
37
  });
31
38
 
32
39
  const Template = (args, { argTypes }) => ({
@@ -53,6 +60,7 @@ export const Variants = (args, { argTypes }) => ({
53
60
  :variant="variant"
54
61
  :size="size"
55
62
  :icon="icon"
63
+ :iconSize="iconSize"
56
64
  class="gl-mr-3"
57
65
  >{{ variant }}</gl-badge>
58
66
  </div>
@@ -76,6 +84,7 @@ export const Actionable = (args, { argTypes }) => ({
76
84
  :variant="variant"
77
85
  :size="size"
78
86
  :icon="icon"
87
+ :iconSize="iconSize"
79
88
  class="gl-mr-3"
80
89
  >{{ variant }}</gl-badge>
81
90
  </div>
@@ -103,6 +112,7 @@ export const Sizes = (args, { argTypes }) => ({
103
112
  :variant="variant"
104
113
  :size="size"
105
114
  :icon="icon"
115
+ :iconSize="iconSize"
106
116
  class="gl-mr-3"
107
117
  >{{ size }}</gl-badge>
108
118
  </div>
@@ -115,6 +125,18 @@ Sizes.args = generateProps({
115
125
  Sizes.argTypes = disableControls(['content', 'size']);
116
126
 
117
127
  export const BadgeIcon = (args, { argTypes }) => ({
128
+ components: { GlBadge },
129
+ props: Object.keys(argTypes),
130
+ template: `
131
+ <div class="gl-display-flex gl-gap-3">
132
+ <gl-badge variant="tier" icon="license">16px icon</gl-badge>
133
+ <gl-badge variant="tier" icon="license-sm" iconSize="sm">12px icon</gl-badge>
134
+ </div>
135
+ `,
136
+ });
137
+ BadgeIcon.argTypes = disableControls(['content', 'iconSize']);
138
+
139
+ export const IconOnly = (args, { argTypes }) => ({
118
140
  components: { GlBadge },
119
141
  props: Object.keys(argTypes),
120
142
  template: `
@@ -124,20 +146,14 @@ export const BadgeIcon = (args, { argTypes }) => ({
124
146
  :variant="variant"
125
147
  :size="size"
126
148
  :icon="icon"
127
- >{{ content }}</gl-badge>
128
- <gl-badge
129
- :href="href"
130
- :variant="variant"
131
- :size="size"
132
- :icon="icon"
149
+ :iconSize="iconSize"
133
150
  />
134
151
  </div>
135
152
  `,
136
153
  });
137
- BadgeIcon.args = generateProps({
154
+ IconOnly.args = generateProps({
138
155
  variant: badgeVariantOptions.success,
139
156
  icon: 'calendar',
140
- content: 'Badge icon',
141
157
  });
142
158
 
143
159
  export default {
@@ -164,5 +180,9 @@ export default {
164
180
  options: ['', ...iconSpriteInfo.icons],
165
181
  control: 'select',
166
182
  },
183
+ iconSize: {
184
+ options: Object.keys(badgeIconSizeOptions),
185
+ control: 'select',
186
+ },
167
187
  },
168
188
  };
@@ -1,7 +1,11 @@
1
1
  <!-- eslint-disable vue/multi-word-component-names -->
2
2
  <script>
3
3
  import { BBadge } from 'bootstrap-vue';
4
- import { badgeSizeOptions, badgeVariantOptions } from '../../../utils/constants';
4
+ import {
5
+ badgeSizeOptions,
6
+ badgeVariantOptions,
7
+ badgeIconSizeOptions,
8
+ } from '../../../utils/constants';
5
9
  import GlIcon from '../icon/icon.vue';
6
10
 
7
11
  export default {
@@ -41,6 +45,15 @@ export default {
41
45
  required: false,
42
46
  default: null,
43
47
  },
48
+ /**
49
+ * The size of the icon 16 or 12
50
+ */
51
+ iconSize: {
52
+ type: String,
53
+ default: 'md',
54
+ validator: (value) => Object.keys(badgeIconSizeOptions).includes(value),
55
+ required: false,
56
+ },
44
57
  },
45
58
  computed: {
46
59
  hasIconOnly() {
@@ -50,13 +63,22 @@ export default {
50
63
  role() {
51
64
  return this.hasIconOnly ? 'img' : undefined;
52
65
  },
66
+ iconSizeComputed() {
67
+ return badgeIconSizeOptions[this.iconSize];
68
+ },
53
69
  },
54
70
  };
55
71
  </script>
56
72
 
57
73
  <template>
58
74
  <b-badge v-bind="$attrs" :variant="variant" :class="['gl-badge', size]" :role="role" pill>
59
- <gl-icon v-if="icon" class="gl-badge-icon" :class="{ 'gl-mr-2': !hasIconOnly }" :name="icon" />
75
+ <gl-icon
76
+ v-if="icon"
77
+ class="gl-badge-icon"
78
+ :size="iconSizeComputed"
79
+ :class="{ 'gl-mr-2': !hasIconOnly }"
80
+ :name="icon"
81
+ />
60
82
  <!-- @slot The badge content to display. -->
61
83
  <slot></slot>
62
84
  </b-badge>
@@ -49,6 +49,12 @@ Alternatively, you can set `selected` property to the array of selected items
49
49
  `value` properties (for multi-select) or to the selected item `value` property for a single-select.
50
50
  On selection the listbox will emit the `select` event with the selected values.
51
51
 
52
+ ### Resetting the selection
53
+
54
+ `GlListbox` can render a reset button if the `headerText` and `resetButtonLabel` props are provided.
55
+ When clicking on the reset button, a `reset` event is emitted. It is the consumer's responsibility
56
+ to listen to that event and to update the model as needed.
57
+
52
58
  ### Setting listbox options
53
59
 
54
60
  Use the `items` prop to provide options to the listbox. Each item can be
@@ -35,6 +35,7 @@ describe('GlListbox', () => {
35
35
  const findNoResultsText = () => wrapper.find("[data-testid='listbox-no-results-text']");
36
36
  const findLoadingIcon = () => wrapper.find("[data-testid='listbox-search-loader']");
37
37
  const findSRNumberOfResultsText = () => wrapper.find("[data-testid='listbox-number-of-results']");
38
+ const findResetButton = () => wrapper.find("[data-testid='listbox-reset-button']");
38
39
 
39
40
  describe('toggle text', () => {
40
41
  describe.each`
@@ -377,4 +378,61 @@ describe('GlListbox', () => {
377
378
  });
378
379
  });
379
380
  });
381
+
382
+ describe('with a reset action', () => {
383
+ it('throws when enabling the reset action without a header', () => {
384
+ expect(() => {
385
+ buildWrapper({ resetButtonLabel: 'Unassign' });
386
+ }).toThrow(
387
+ 'The reset button cannot be rendered without a header. Either provide a header via the headerText prop, or do not provide the resetButtonLabel prop.'
388
+ );
389
+ expect(wrapper).toHaveLoggedVueErrors();
390
+ });
391
+
392
+ it('shows the reset button if the label is provided and the selection is not empty', () => {
393
+ buildWrapper({
394
+ headerText: 'Select assignee',
395
+ resetButtonLabel: 'Unassign',
396
+ selected: mockOptions[1].value,
397
+ items: mockOptions,
398
+ });
399
+ const button = findResetButton();
400
+
401
+ expect(button.exists()).toBe(true);
402
+ expect(button.text()).toBe('Unassign');
403
+ });
404
+
405
+ it.each`
406
+ description | props
407
+ ${'multi-select'} | ${{ multiple: true, selected: [] }}
408
+ ${'single-select'} | ${{ multiple: false, selected: null }}
409
+ `('hides the button if the selection is empty in $description mode', ({ props }) => {
410
+ buildWrapper({
411
+ headerText: 'Select assignee',
412
+ resetButtonLabel: 'Unassign',
413
+ items: mockOptions,
414
+ ...props,
415
+ });
416
+
417
+ expect(findResetButton().exists()).toBe(false);
418
+ });
419
+
420
+ it('on click, emits the reset event and calls closeAndFocus()', () => {
421
+ buildWrapper({
422
+ headerText: 'Select assignee',
423
+ resetButtonLabel: 'Unassign',
424
+ selected: mockOptions[1].value,
425
+ items: mockOptions,
426
+ });
427
+ jest.spyOn(wrapper.vm, 'closeAndFocus');
428
+
429
+ expect(wrapper.emitted('reset')).toBe(undefined);
430
+ expect(wrapper.vm.closeAndFocus).not.toHaveBeenCalled();
431
+
432
+ findResetButton().trigger('click');
433
+
434
+ expect(wrapper.emitted('reset')).toHaveLength(1);
435
+ expect(wrapper.vm.closeAndFocus).toHaveBeenCalled();
436
+ });
437
+ });
380
438
  });
@@ -39,6 +39,7 @@ const generateProps = ({
39
39
  isCheckCentered = defaultValue('isCheckCentered'),
40
40
  toggleAriaLabelledBy,
41
41
  listAriaLabelledBy,
42
+ resetButtonLabel = defaultValue('resetButtonLabel'),
42
43
  startOpened = true,
43
44
  } = {}) => ({
44
45
  items,
@@ -60,6 +61,7 @@ const generateProps = ({
60
61
  isCheckCentered,
61
62
  toggleAriaLabelledBy,
62
63
  listAriaLabelledBy,
64
+ resetButtonLabel,
63
65
  startOpened,
64
66
  });
65
67
 
@@ -84,6 +86,7 @@ const makeBindings = (overrides = {}) =>
84
86
  ':is-check-centered': 'isCheckCentered',
85
87
  ':toggle-aria-labelled-by': 'toggleAriaLabelledBy',
86
88
  ':list-aria-labelled-by': 'listAriaLabelledBy',
89
+ ':reset-button-label': 'resetButtonLabel',
87
90
  ...overrides,
88
91
  })
89
92
  .map(([key, value]) => `${key}="${value}"`)
@@ -153,8 +156,12 @@ export const HeaderAndFooter = (args, { argTypes }) => ({
153
156
  selectItem(index) {
154
157
  this.selected.push(mockOptions[index].value);
155
158
  },
159
+ onReset() {
160
+ this.selected = [];
161
+ },
156
162
  },
157
- template: template(`
163
+ template: template(
164
+ `
158
165
  <template #footer>
159
166
  <div class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-100 gl-display-flex gl-justify-content-center gl-p-3">
160
167
  <gl-button-group :vertical="false">
@@ -164,11 +171,18 @@ export const HeaderAndFooter = (args, { argTypes }) => ({
164
171
  </gl-button-group>
165
172
  </div>
166
173
  </template>
167
- `),
174
+ `,
175
+ {
176
+ bindingOverrides: {
177
+ '@reset': 'onReset',
178
+ },
179
+ }
180
+ ),
168
181
  });
169
182
  HeaderAndFooter.args = generateProps({
170
183
  toggleText: 'Header and Footer',
171
184
  headerText: 'Assign to department',
185
+ resetButtonLabel: 'Unassign',
172
186
  multiple: true,
173
187
  });
174
188
  HeaderAndFooter.decorators = [makeContainer({ height: '370px' })];
@@ -197,6 +211,11 @@ export const CustomListItem = (args, { argTypes }) => ({
197
211
  : this.items.find(({ value }) => value === this.selected[0]).text;
198
212
  },
199
213
  },
214
+ methods: {
215
+ onReset() {
216
+ this.selected = [];
217
+ },
218
+ },
200
219
  template: template(
201
220
  `<template #list-item="{ item }">
202
221
  <span class="gl-display-flex gl-align-items-center">
@@ -211,6 +230,7 @@ export const CustomListItem = (args, { argTypes }) => ({
211
230
  {
212
231
  bindingOverrides: {
213
232
  ':toggle-text': 'customToggleText',
233
+ '@reset': 'onReset',
214
234
  },
215
235
  }
216
236
  ),
@@ -229,6 +249,8 @@ CustomListItem.args = generateProps({
229
249
  ],
230
250
  multiple: true,
231
251
  isCheckCentered: true,
252
+ headerText: 'Select assignees',
253
+ resetButtonLabel: 'Unassign',
232
254
  });
233
255
  CustomListItem.decorators = [makeContainer({ height: '200px' })];
234
256
 
@@ -15,6 +15,7 @@ import {
15
15
  buttonSizeOptions,
16
16
  dropdownVariantOptions,
17
17
  } from '../../../../utils/constants';
18
+ import GlButton from '../../button/button.vue';
18
19
  import GlLoadingIcon from '../../loading_icon/loading_icon.vue';
19
20
  import GlSearchBoxByType from '../../search_box_by_type/search_box_by_type.vue';
20
21
  import GlBaseDropdown from '../base_dropdown/base_dropdown.vue';
@@ -37,6 +38,7 @@ export default {
37
38
  GlBaseDropdown,
38
39
  GlListboxItem,
39
40
  GlListboxGroup,
41
+ GlButton,
40
42
  GlSearchBoxByType,
41
43
  GlLoadingIcon,
42
44
  },
@@ -219,6 +221,17 @@ export default {
219
221
  required: false,
220
222
  default: 'No results found',
221
223
  },
224
+ /**
225
+ * The reset button's label, to be rendered in the header. If this is omitted, the button is not
226
+ * rendered.
227
+ * The reset button requires a header to be set, so this prop should be used in conjunction with
228
+ * headerText.
229
+ */
230
+ resetButtonLabel: {
231
+ type: String,
232
+ required: false,
233
+ default: '',
234
+ },
222
235
  },
223
236
  data() {
224
237
  return {
@@ -266,6 +279,15 @@ export default {
266
279
  headerId() {
267
280
  return this.headerText && uniqueId('listbox-header-');
268
281
  },
282
+ showResetButton() {
283
+ if (!this.resetButtonLabel) {
284
+ return false;
285
+ }
286
+ if (this.multiple) {
287
+ return this.selected.length > 0;
288
+ }
289
+ return Boolean(this.selected);
290
+ },
269
291
  },
270
292
  watch: {
271
293
  selected: {
@@ -282,6 +304,13 @@ export default {
282
304
  },
283
305
  },
284
306
  },
307
+ mounted() {
308
+ if (process.env.NODE_ENV !== 'production' && this.resetButtonLabel && !this.headerText) {
309
+ throw new Error(
310
+ 'The reset button cannot be rendered without a header. Either provide a header via the headerText prop, or do not provide the resetButtonLabel prop.'
311
+ );
312
+ }
313
+ },
285
314
  methods: {
286
315
  open() {
287
316
  this.$refs.baseDropdown.open();
@@ -392,7 +421,7 @@ export default {
392
421
  this.$emit('select', value);
393
422
  }
394
423
 
395
- this.$refs.baseDropdown.closeAndFocus();
424
+ this.closeAndFocus();
396
425
  },
397
426
  onMultiSelect(value, isSelected) {
398
427
  if (isSelected) {
@@ -413,6 +442,18 @@ export default {
413
442
  */
414
443
  this.$emit('search', searchTerm);
415
444
  },
445
+ onResetButtonClicked() {
446
+ /**
447
+ * Emitted when the reset button is clicked
448
+ *
449
+ * @event reset
450
+ */
451
+ this.$emit('reset');
452
+ this.closeAndFocus();
453
+ },
454
+ closeAndFocus() {
455
+ this.$refs.baseDropdown.closeAndFocus();
456
+ },
416
457
  isOption,
417
458
  },
418
459
  };
@@ -440,16 +481,25 @@ export default {
440
481
  >
441
482
  <div
442
483
  v-if="headerText"
443
- class="gl-display-flex gl-align-items-center gl-p-3"
484
+ class="gl-display-flex gl-align-items-center gl-px-3 gl-py-2! gl-min-h-8"
444
485
  :class="$options.HEADER_ITEMS_BORDER_CLASSES"
445
486
  >
446
487
  <div
447
488
  :id="headerId"
448
- class="gl-flex-grow-1 gl-font-weight-bold gl-font-sm"
489
+ class="gl-flex-grow-1 gl-font-weight-bold gl-font-sm gl-pr-2"
449
490
  data-testid="listbox-header-text"
450
491
  >
451
492
  {{ headerText }}
452
493
  </div>
494
+ <gl-button
495
+ v-if="showResetButton"
496
+ category="tertiary"
497
+ class="gl-focus-inset-border-2-blue-400! gl-flex-shrink-0 gl-font-sm! gl-px-2! gl-py-2!"
498
+ data-testid="listbox-reset-button"
499
+ @click="onResetButtonClicked"
500
+ >
501
+ {{ resetButtonLabel }}
502
+ </gl-button>
453
503
  </div>
454
504
 
455
505
  <div v-if="searchable" :class="$options.HEADER_ITEMS_BORDER_CLASSES">
@@ -1898,6 +1898,22 @@
1898
1898
  box-shadow: inset 0 0 0 $gl-border-size-1 $green-600 !important
1899
1899
  }
1900
1900
 
1901
+ .gl-inset-border-2-blue-400 {
1902
+ box-shadow: inset 0 0 0 $gl-border-size-2 $blue-400
1903
+ }
1904
+
1905
+ .gl-focus-inset-border-2-blue-400:focus {
1906
+ box-shadow: inset 0 0 0 $gl-border-size-2 $blue-400
1907
+ }
1908
+
1909
+ .gl-inset-border-2-blue-400\! {
1910
+ box-shadow: inset 0 0 0 $gl-border-size-2 $blue-400 !important
1911
+ }
1912
+
1913
+ .gl-focus-inset-border-2-blue-400\!:focus {
1914
+ box-shadow: inset 0 0 0 $gl-border-size-2 $blue-400 !important
1915
+ }
1916
+
1901
1917
  .gl-inset-border-2-green-400 {
1902
1918
  box-shadow: inset 0 0 0 $gl-border-size-2 $green-400
1903
1919
  }
@@ -93,6 +93,10 @@
93
93
  box-shadow: inset 0 0 0 $gl-border-size-1 $green-600;
94
94
  }
95
95
 
96
+ @mixin gl-inset-border-2-blue-400($focus: true) {
97
+ box-shadow: inset 0 0 0 $gl-border-size-2 $blue-400;
98
+ }
99
+
96
100
  @mixin gl-inset-border-2-green-400 {
97
101
  box-shadow: inset 0 0 0 $gl-border-size-2 $green-400;
98
102
  }
@@ -39,6 +39,11 @@ export const badgeVariantOptions = {
39
39
  tier: 'tier',
40
40
  };
41
41
 
42
+ export const badgeIconSizeOptions = {
43
+ sm: 12,
44
+ md: 16,
45
+ };
46
+
42
47
  export const variantCssColorMap = {
43
48
  muted: 'gl-text-gray-500',
44
49
  neutral: 'gl-text-blue-100',