@gitlab/ui 43.13.0 → 43.16.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 (25) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/components/base/banner/banner.js +1 -1
  3. package/dist/components/base/new_dropdowns/listbox/listbox.js +34 -19
  4. package/dist/components/base/new_dropdowns/listbox/listbox_group.js +54 -0
  5. package/dist/components/base/new_dropdowns/listbox/mock_data.js +61 -0
  6. package/dist/components/base/new_dropdowns/listbox/utils.js +34 -0
  7. package/dist/utility_classes.css +1 -1
  8. package/dist/utility_classes.css.map +1 -1
  9. package/package.json +3 -3
  10. package/src/components/base/banner/banner.spec.js +1 -1
  11. package/src/components/base/banner/banner.vue +1 -1
  12. package/src/components/base/button/button.stories.js +23 -4
  13. package/src/components/base/new_dropdowns/listbox/listbox.md +56 -13
  14. package/src/components/base/new_dropdowns/listbox/listbox.spec.js +60 -33
  15. package/src/components/base/new_dropdowns/listbox/listbox.stories.js +86 -92
  16. package/src/components/base/new_dropdowns/listbox/listbox.vue +63 -20
  17. package/src/components/base/new_dropdowns/listbox/listbox_group.spec.js +47 -0
  18. package/src/components/base/new_dropdowns/listbox/listbox_group.vue +24 -0
  19. package/src/components/base/new_dropdowns/listbox/mock_data.js +68 -0
  20. package/src/components/base/new_dropdowns/listbox/utils.js +21 -0
  21. package/src/components/base/new_dropdowns/listbox/utils.spec.js +56 -0
  22. package/src/components/base/sorting/sorting.stories.js +1 -1
  23. package/src/components/base/sorting/sorting_item.stories.js +1 -1
  24. package/src/scss/utilities.scss +10 -0
  25. package/src/scss/utility-mixins/spacing.scss +6 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "43.13.0",
3
+ "version": "43.16.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -81,7 +81,7 @@
81
81
  "@babel/preset-env": "^7.18.10",
82
82
  "@gitlab/eslint-plugin": "17.0.0",
83
83
  "@gitlab/stylelint-config": "4.1.0",
84
- "@gitlab/svgs": "3.2.0",
84
+ "@gitlab/svgs": "3.3.0",
85
85
  "@rollup/plugin-commonjs": "^11.1.0",
86
86
  "@rollup/plugin-node-resolve": "^7.1.3",
87
87
  "@rollup/plugin-replace": "^2.3.2",
@@ -104,7 +104,7 @@
104
104
  "bootstrap": "4.5.3",
105
105
  "cypress": "^10.7.0",
106
106
  "emoji-regex": "^10.0.0",
107
- "eslint": "8.22.0",
107
+ "eslint": "8.23.0",
108
108
  "eslint-import-resolver-jest": "3.0.2",
109
109
  "eslint-plugin-cypress": "2.12.1",
110
110
  "eslint-plugin-storybook": "0.6.4",
@@ -25,7 +25,7 @@ describe('banner component', () => {
25
25
  });
26
26
 
27
27
  it('should render the correct title', () => {
28
- expect(wrapper.find('h1').text()).toEqual(propsData.title);
28
+ expect(wrapper.find('h2').text()).toEqual(propsData.title);
29
29
  });
30
30
 
31
31
  it('should render the button', () => {
@@ -98,7 +98,7 @@ export default {
98
98
  <img :src="svgPath" alt="" role="presentation" />
99
99
  </div>
100
100
  <div class="gl-banner-content">
101
- <h1 class="gl-banner-title">{{ title }}</h1>
101
+ <h2 class="gl-banner-title">{{ title }}</h2>
102
102
  <!-- @slot The banner content to display -->
103
103
  <slot></slot>
104
104
  <gl-button
@@ -1,4 +1,12 @@
1
- import { GlButton, GlButtonGroup, GlBadge, GlDropdown, GlDropdownItem } from '../../../index';
1
+ import {
2
+ GlButton,
3
+ GlButtonGroup,
4
+ GlBadge,
5
+ GlDropdown,
6
+ GlDropdownItem,
7
+ GlSorting,
8
+ GlSortingItem,
9
+ } from '../../../index';
2
10
  import {
3
11
  buttonCategoryOptions,
4
12
  buttonVariantOptions,
@@ -478,13 +486,12 @@ export const Badges = (args, { argTypes = {} }) => ({
478
486
  </div>
479
487
  `,
480
488
  });
481
-
482
489
  Badges.parameters = { controls: { disable: true } };
483
490
 
484
491
  export const BadgeWithSROnlyText = (args, { argTypes = {} }) => ({
485
492
  props: Object.keys(argTypes),
486
493
  components: { GlButton, GlBadge },
487
- template: `
494
+ template: `
488
495
  <gl-button variant="confirm" buttonTextClasses="gl-display-flex gl-align-items-center">
489
496
  Submit review
490
497
  <gl-badge size="sm" variant="info" class="gl-ml-2">2</gl-badge>
@@ -492,9 +499,21 @@ export const BadgeWithSROnlyText = (args, { argTypes = {} }) => ({
492
499
  </gl-button>
493
500
  `,
494
501
  });
495
-
496
502
  BadgeWithSROnlyText.parameters = { controls: { disable: true } };
497
503
 
504
+ export const SortingDropdownSplitButton = (args, { argTypes }) => ({
505
+ props: Object.keys(argTypes),
506
+ components: { GlSorting, GlSortingItem },
507
+ template: `
508
+ <gl-sorting text="Sorting options">
509
+ <gl-sorting-item active>First item</gl-sorting-item>
510
+ <gl-sorting-item>Second item</gl-sorting-item>
511
+ <gl-sorting-item>Last item</gl-sorting-item>
512
+ </gl-sorting>
513
+ `,
514
+ });
515
+ SortingDropdownSplitButton.parameters = { controls: { disable: true } };
516
+
498
517
  export default {
499
518
  title: 'base/button',
500
519
  component: GlButton,
@@ -51,21 +51,64 @@ On selection the listbox will emit the `select` event with the selected values.
51
51
 
52
52
  ### Setting listbox options
53
53
 
54
- Provide the list of options for the listbox - each item in the array should have `value` property.
55
- It is used as a primary key.
56
- To render the default listbox item template, the item should also have `text` property.
57
- If you want to use custom template for rendering the listbox item, use the `list-item` template.
54
+ Use the `items` prop to provide options to the listbox. Each item can be
55
+ either an option or a group. Below are the expected shapes of these
56
+ objects:
57
+
58
+ ```typescript
59
+ type Option = {
60
+ value: string
61
+ text?: string
62
+ }
63
+
64
+ type Group = {
65
+ text: string
66
+ options: Array<Option>
67
+ }
68
+
69
+ type ItemsProp = Array<Option> | Array<Group>
70
+ ```
71
+
72
+ #### Options
73
+
74
+ The `value` property of options must be unique across all options
75
+ provided to the listbox, as it's used as a primary key.
76
+
77
+ The optional `text` property is used to render the default listbox item
78
+ template. If you want to render a custom template for items, use the
79
+ `list-item` scoped slot:
58
80
 
59
81
  ```html
60
82
  <gl-listbox :items="items">
61
- <template #list-item="{ item }">
62
- <span class="gl-display-flex gl-align-items-center">
63
- <gl-avatar :size="32" class-="gl-mr-3"/>
64
- <span class="gl-display-flex gl-flex-direction-column">
65
- <span class="gl-font-weight-bold gl-white-space-nowrap">{{ item.text }}</span>
66
- <span class="gl-text-gray-400"> {{ item.secondaryText }}</span>
67
- </span>
68
- </span>
69
- </template>
83
+ <template #list-item="{ item }">
84
+ <span class="gl-display-flex gl-align-items-center">
85
+ <gl-avatar :size="32" class-="gl-mr-3"/>
86
+ <span class="gl-display-flex gl-flex-direction-column">
87
+ <span class="gl-font-weight-bold gl-white-space-nowrap">{{ item.text }}</span>
88
+ <span class="gl-text-gray-400"> {{ item.secondaryText }}</span>
89
+ </span>
90
+ </span>
91
+ </template>
92
+ </gl-listbox>
93
+ ```
94
+
95
+ #### Groups
96
+
97
+ Options can be contained within groups. A group has a required `text`
98
+ property, which must be unique across all groups within the listbox, as
99
+ it's used as a primary key. It also has a required property `items` that
100
+ must be an array of options.
101
+
102
+ Groups can be at most one level deep: a group can only contain options.
103
+ Options and groups _cannot_ be siblings. Either all items are options,
104
+ or they are all groups.
105
+
106
+ To render custom group labels, use the `group-label` scoped slot:
107
+
108
+ ```html
109
+ <gl-listbox :items="groups">
110
+ <template #group-label="{ group }">
111
+ {{ group.text }} <gl-badge size="sm">{{ group.options.length }}</gl-badge>
112
+ </template>
70
113
  </gl-listbox>
71
114
  ```
@@ -11,21 +11,8 @@ import {
11
11
  } from '../constants';
12
12
  import GlListbox, { ITEM_SELECTOR } from './listbox.vue';
13
13
  import GlListboxItem from './listbox_item.vue';
14
-
15
- const mockItems = [
16
- {
17
- value: 'eng',
18
- text: 'Engineering',
19
- },
20
- {
21
- value: 'sales',
22
- text: 'Sales',
23
- },
24
- {
25
- value: 'marketing',
26
- text: 'Marketing',
27
- },
28
- ];
14
+ import GlListboxGroup from './listbox_group.vue';
15
+ import { mockOptions, mockGroups } from './mock_data';
29
16
 
30
17
  describe('GlListbox', () => {
31
18
  let wrapper;
@@ -40,19 +27,20 @@ describe('GlListbox', () => {
40
27
 
41
28
  const findBaseDropdown = () => wrapper.findComponent(GlBaseDropdown);
42
29
  const findListContainer = () => wrapper.find('[role="listbox"]');
43
- const findListboxItems = () => wrapper.findAllComponents(GlListboxItem);
30
+ const findListboxItems = (root = wrapper) => root.findAllComponents(GlListboxItem);
31
+ const findListboxGroups = () => wrapper.findAllComponents(GlListboxGroup);
44
32
  const findListItem = (index) => findListboxItems().at(index).find(ITEM_SELECTOR);
45
33
 
46
34
  describe('toggle text', () => {
47
35
  describe.each`
48
- toggleText | multiple | selected | expectedToggleText
49
- ${'Toggle caption'} | ${true} | ${[mockItems[0].value]} | ${'Toggle caption'}
50
- ${''} | ${true} | ${[mockItems[0]].value} | ${''}
51
- ${''} | ${false} | ${mockItems[0].value} | ${mockItems[0].text}
52
- ${''} | ${false} | ${''} | ${''}
36
+ toggleText | multiple | selected | expectedToggleText
37
+ ${'Toggle caption'} | ${true} | ${[mockOptions[0].value]} | ${'Toggle caption'}
38
+ ${''} | ${true} | ${[mockOptions[0]].value} | ${''}
39
+ ${''} | ${false} | ${mockOptions[0].value} | ${mockOptions[0].text}
40
+ ${''} | ${false} | ${''} | ${''}
53
41
  `('when listbox', ({ toggleText, multiple, selected, expectedToggleText }) => {
54
42
  beforeEach(() => {
55
- buildWrapper({ items: mockItems, toggleText, multiple, selected });
43
+ buildWrapper({ items: mockOptions, toggleText, multiple, selected });
56
44
  });
57
45
 
58
46
  it(`is ${multiple ? 'multi' : 'single'}-select, toggleText is ${
@@ -77,8 +65,8 @@ describe('GlListbox', () => {
77
65
  beforeEach(() => {
78
66
  buildWrapper({
79
67
  multiple: true,
80
- selected: [mockItems[1].value, mockItems[2].value],
81
- items: mockItems,
68
+ selected: [mockOptions[1].value, mockOptions[2].value],
69
+ items: mockOptions,
82
70
  });
83
71
  });
84
72
 
@@ -90,28 +78,29 @@ describe('GlListbox', () => {
90
78
  it('should deselect previously selected', async () => {
91
79
  findListboxItems().at(1).vm.$emit('select', false);
92
80
  await nextTick();
93
- expect(wrapper.emitted('select')[0][0]).toEqual([mockItems[2].value]);
81
+ expect(wrapper.emitted('select')[0][0]).toEqual([mockOptions[2].value]);
94
82
  });
95
83
 
96
84
  it('should add to selection', async () => {
97
85
  findListboxItems().at(0).vm.$emit('select', true);
98
86
  await nextTick();
99
87
  expect(wrapper.emitted('select')[0][0]).toEqual(
100
- expect.arrayContaining(mockItems.map(({ value }) => value))
88
+ // The first three items should now be selected.
89
+ expect.arrayContaining(mockOptions.slice(0, 3).map(({ value }) => value))
101
90
  );
102
91
  });
103
92
  });
104
93
 
105
94
  describe('single-select', () => {
106
95
  beforeEach(() => {
107
- buildWrapper({ selected: mockItems[1].value, items: mockItems });
96
+ buildWrapper({ selected: mockOptions[1].value, items: mockOptions });
108
97
  });
109
98
 
110
99
  it('should throw an error when array of selections is provided', () => {
111
100
  expect(() => {
112
101
  buildWrapper({
113
- selected: [mockItems[1].value, mockItems[2].value],
114
- items: mockItems,
102
+ selected: [mockOptions[1].value, mockOptions[2].value],
103
+ items: mockOptions,
115
104
  });
116
105
  }).toThrowError('To allow multi-selection, please, set "multiple" property to "true"');
117
106
  expect(wrapper).toHaveLoggedVueErrors();
@@ -124,7 +113,25 @@ describe('GlListbox', () => {
124
113
  it('should deselect previously selected and select a new item', async () => {
125
114
  findListboxItems().at(2).vm.$emit('select', true);
126
115
  await nextTick();
127
- expect(wrapper.emitted('select')[0][0]).toEqual(mockItems[2].value);
116
+ expect(wrapper.emitted('select')[0][0]).toEqual(mockOptions[2].value);
117
+ });
118
+ });
119
+
120
+ describe('with groups', () => {
121
+ const selected = mockGroups[1].options[1].value;
122
+
123
+ beforeEach(() => {
124
+ buildWrapper({ selected, items: mockGroups });
125
+ });
126
+
127
+ it('should render item as selected when `selected` provided ', () => {
128
+ expect(findListboxItems().at(3).props('isSelected')).toBe(true);
129
+ });
130
+
131
+ it('should deselect previously selected and select a new item', async () => {
132
+ findListboxItems().at(0).vm.$emit('select', true);
133
+ await nextTick();
134
+ expect(wrapper.emitted('select')[0][0]).toEqual(mockGroups[0].options[0].value);
128
135
  });
129
136
  });
130
137
  });
@@ -133,8 +140,8 @@ describe('GlListbox', () => {
133
140
  beforeEach(async () => {
134
141
  buildWrapper({
135
142
  multiple: true,
136
- items: mockItems,
137
- selected: [mockItems[2].value, mockItems[1].value],
143
+ items: mockOptions,
144
+ selected: [mockOptions[2].value, mockOptions[1].value],
138
145
  });
139
146
  findBaseDropdown().vm.$emit(GL_DROPDOWN_SHOWN);
140
147
  await nextTick();
@@ -166,7 +173,8 @@ describe('GlListbox', () => {
166
173
  let thirdItem;
167
174
 
168
175
  beforeEach(() => {
169
- buildWrapper({ items: mockItems });
176
+ // These tests are more easily written with a small list of items.
177
+ buildWrapper({ items: mockOptions.slice(0, 3) });
170
178
  findBaseDropdown().vm.$emit(GL_DROPDOWN_SHOWN);
171
179
  firstItem = findListItem(0);
172
180
  secondItem = findListItem(1);
@@ -233,4 +241,23 @@ describe('GlListbox', () => {
233
241
  expect(wrapper.text()).toContain(footerContent);
234
242
  });
235
243
  });
244
+
245
+ describe('with groups', () => {
246
+ it('renders groups of items', () => {
247
+ buildWrapper({ items: mockGroups });
248
+
249
+ const groups = findListboxGroups();
250
+
251
+ expect(groups.length).toBe(mockGroups.length);
252
+
253
+ const expectedNameProps = mockGroups.map((group) => group.text);
254
+ const actualNameProps = groups.wrappers.map((group) => group.props('name'));
255
+
256
+ expect(actualNameProps).toEqual(expectedNameProps);
257
+
258
+ mockGroups.forEach((group, i) => {
259
+ expect(findListboxItems(groups.at(i))).toHaveLength(group.options.length);
260
+ });
261
+ });
262
+ });
236
263
  });
@@ -9,65 +9,17 @@ import {
9
9
  GlSearchBoxByType,
10
10
  GlButtonGroup,
11
11
  GlButton,
12
+ GlBadge,
12
13
  GlAvatar,
13
14
  } from '../../../../index';
14
15
  import { makeContainer } from '../../../../utils/story_decorators/container';
15
16
  import readme from './listbox.md';
17
+ import { mockOptions, mockGroups } from './mock_data';
16
18
 
17
19
  const defaultValue = (prop) => GlListbox.props[prop].default;
18
20
 
19
- const defaultItems = [
20
- {
21
- value: 'prod',
22
- text: 'Product',
23
- },
24
- {
25
- value: 'ppl',
26
- text: 'People',
27
- },
28
- {
29
- value: 'fin',
30
- text: 'Finance',
31
- },
32
- {
33
- value: 'leg',
34
- text: 'Legal',
35
- },
36
- {
37
- value: 'eng',
38
- text: 'Engineering',
39
- },
40
- {
41
- value: 'sales',
42
- text: 'Sales',
43
- },
44
- {
45
- value: 'marketing',
46
- text: 'Marketing',
47
- },
48
- {
49
- value: 'acc',
50
- text: 'Accounting',
51
- },
52
- {
53
- value: 'hr',
54
- text: 'Human Resource Management',
55
- },
56
- {
57
- value: 'rnd',
58
- text: 'Research and Development',
59
- },
60
- {
61
- value: 'cust',
62
- text: 'Customer Service',
63
- },
64
- {
65
- value: 'sup',
66
- text: 'Support',
67
- },
68
- ];
69
21
  const generateProps = ({
70
- items = defaultItems,
22
+ items = mockOptions,
71
23
  category = defaultValue('category'),
72
24
  variant = defaultValue('variant'),
73
25
  size = defaultValue('size'),
@@ -137,7 +89,7 @@ export const Default = (args, { argTypes }) => ({
137
89
  },
138
90
  data() {
139
91
  return {
140
- selected: defaultItems[1].value,
92
+ selected: mockOptions[1].value,
141
93
  };
142
94
  },
143
95
  mounted() {
@@ -170,21 +122,23 @@ export const HeaderAndFooter = (args, { argTypes }) => ({
170
122
  },
171
123
  methods: {
172
124
  selectItem(index) {
173
- this.selected.push(defaultItems[index].value);
125
+ this.selected.push(mockOptions[index].value);
174
126
  },
175
127
  },
176
- template: template(`<template #header>
177
- <gl-search-box-by-type/>
178
- </template>
179
- <template #footer>
180
- <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">
181
- <gl-button-group :vertical="false">
182
- <gl-button @click="selectItem(0)">1st</gl-button>
183
- <gl-button @click="selectItem(1)">2nd</gl-button>
184
- <gl-button @click="selectItem(2)">3rd</gl-button>
185
- </gl-button-group>
186
- </div>
187
- </template>`),
128
+ template: template(`
129
+ <template #header>
130
+ <gl-search-box-by-type/>
131
+ </template>
132
+ <template #footer>
133
+ <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">
134
+ <gl-button-group :vertical="false">
135
+ <gl-button @click="selectItem(0)">1st</gl-button>
136
+ <gl-button @click="selectItem(1)">2nd</gl-button>
137
+ <gl-button @click="selectItem(2)">3rd</gl-button>
138
+ </gl-button-group>
139
+ </div>
140
+ </template>
141
+ `),
188
142
  });
189
143
  HeaderAndFooter.args = generateProps({
190
144
  toggleText: 'Header and Footer',
@@ -217,33 +171,33 @@ export const CustomListItem = (args, { argTypes }) => ({
217
171
  },
218
172
  },
219
173
  template: `
220
- <gl-listbox
221
- v-model="selected"
222
- :items="items"
223
- :category="category"
224
- :variant="variant"
225
- :size="size"
226
- :disabled="disabled"
227
- :loading="loading"
228
- :no-caret="noCaret"
229
- :right="right"
230
- :toggle-text="headerText"
231
- :text-sr-only="textSrOnly"
232
- :icon="icon"
233
- :multiple="multiple"
234
- :is-check-centered="isCheckCentered"
235
- :aria-labelledby="ariaLabelledby"
236
- >
237
- <template #list-item="{ item }">
238
- <span class="gl-display-flex gl-align-items-center">
239
- <gl-avatar :size="32" class-="gl-mr-3"/>
240
- <span class="gl-display-flex gl-flex-direction-column">
241
- <span class="gl-font-weight-bold gl-white-space-nowrap">{{ item.text }}</span>
242
- <span class="gl-text-gray-400"> {{ item.secondaryText }}</span>
243
- </span>
244
- </span>
245
- </template>
246
- </gl-listbox>
174
+ <gl-listbox
175
+ v-model="selected"
176
+ :items="items"
177
+ :category="category"
178
+ :variant="variant"
179
+ :size="size"
180
+ :disabled="disabled"
181
+ :loading="loading"
182
+ :no-caret="noCaret"
183
+ :right="right"
184
+ :toggle-text="headerText"
185
+ :text-sr-only="textSrOnly"
186
+ :icon="icon"
187
+ :multiple="multiple"
188
+ :is-check-centered="isCheckCentered"
189
+ :aria-labelledby="ariaLabelledby"
190
+ >
191
+ <template #list-item="{ item }">
192
+ <span class="gl-display-flex gl-align-items-center">
193
+ <gl-avatar :size="32" class-="gl-mr-3"/>
194
+ <span class="gl-display-flex gl-flex-direction-column">
195
+ <span class="gl-font-weight-bold gl-white-space-nowrap">{{ item.text }}</span>
196
+ <span class="gl-text-gray-400"> {{ item.secondaryText }}</span>
197
+ </span>
198
+ </span>
199
+ </template>
200
+ </gl-listbox>
247
201
  `,
248
202
  });
249
203
 
@@ -258,6 +212,46 @@ CustomListItem.args = generateProps({
258
212
  });
259
213
  CustomListItem.decorators = [makeContainer({ height: '200px' })];
260
214
 
215
+ const makeGroupedExample = (changes) => {
216
+ const story = (args, { argTypes }) => ({
217
+ props: Object.keys(argTypes),
218
+ components: {
219
+ GlBadge,
220
+ GlListbox,
221
+ },
222
+ data() {
223
+ return {
224
+ selected: 'v1.0',
225
+ };
226
+ },
227
+ mounted() {
228
+ if (this.startOpened) {
229
+ openListbox(this);
230
+ }
231
+ },
232
+ template: template(''),
233
+ ...changes,
234
+ });
235
+
236
+ story.args = generateProps({ items: mockGroups });
237
+ story.decorators = [makeContainer({ height: '280px' })];
238
+
239
+ return story;
240
+ };
241
+
242
+ export const Groups = makeGroupedExample();
243
+
244
+ export const CustomGroupsAndItems = makeGroupedExample({
245
+ template: template(`
246
+ <template #group-label="{ group }">
247
+ {{ group.text }} <gl-badge size="sm">{{ group.options.length }}</gl-badge>
248
+ </template>
249
+ <template #list-item="{ item }">
250
+ {{ item.text }} <gl-badge v-if="item.value === 'main'" size="sm">default</gl-badge>
251
+ </template>
252
+ `),
253
+ });
254
+
261
255
  export default {
262
256
  title: 'base/new-dropdowns/listbox',
263
257
  component: GlListbox,