@gitlab/ui 43.11.0 → 43.14.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 (27) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/components/base/new_dropdowns/listbox/listbox.js +43 -19
  3. package/dist/components/base/new_dropdowns/listbox/listbox_group.js +54 -0
  4. package/dist/components/base/new_dropdowns/listbox/listbox_item.js +19 -1
  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 +5 -5
  10. package/scss_to_js/scss_variables.js +1 -0
  11. package/scss_to_js/scss_variables.json +5 -0
  12. package/src/components/base/new_dropdowns/listbox/listbox.md +56 -13
  13. package/src/components/base/new_dropdowns/listbox/listbox.spec.js +60 -33
  14. package/src/components/base/new_dropdowns/listbox/listbox.stories.js +94 -92
  15. package/src/components/base/new_dropdowns/listbox/listbox.vue +71 -19
  16. package/src/components/base/new_dropdowns/listbox/listbox_group.spec.js +47 -0
  17. package/src/components/base/new_dropdowns/listbox/listbox_group.vue +24 -0
  18. package/src/components/base/new_dropdowns/listbox/listbox_item.spec.js +25 -0
  19. package/src/components/base/new_dropdowns/listbox/listbox_item.vue +19 -2
  20. package/src/components/base/new_dropdowns/listbox/mock_data.js +68 -0
  21. package/src/components/base/new_dropdowns/listbox/utils.js +21 -0
  22. package/src/components/base/new_dropdowns/listbox/utils.spec.js +56 -0
  23. package/src/components/base/toggle/toggle.md +0 -2
  24. package/src/scss/utilities.scss +20 -0
  25. package/src/scss/utility-mixins/flex.scss +6 -0
  26. package/src/scss/utility-mixins/sizing.scss +4 -0
  27. package/src/scss/variables.scss +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "43.11.0",
3
+ "version": "43.14.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -102,7 +102,7 @@
102
102
  "babel-plugin-require-context-hook": "^1.0.0",
103
103
  "babel-preset-vue": "^2.0.2",
104
104
  "bootstrap": "4.5.3",
105
- "cypress": "^10.6.0",
105
+ "cypress": "^10.7.0",
106
106
  "emoji-regex": "^10.0.0",
107
107
  "eslint": "8.22.0",
108
108
  "eslint-import-resolver-jest": "3.0.2",
@@ -112,9 +112,9 @@
112
112
  "glob": "^7.2.0",
113
113
  "identity-obj-proxy": "^3.0.0",
114
114
  "inquirer-select-directory": "^1.2.0",
115
- "jest": "^29.0.1",
116
- "jest-circus": "29.0.1",
117
- "jest-environment-jsdom": "29.0.1",
115
+ "jest": "^29.0.2",
116
+ "jest-circus": "29.0.2",
117
+ "jest-environment-jsdom": "29.0.2",
118
118
  "jest-serializer-vue": "^2.0.2",
119
119
  "markdownlint-cli": "^0.29.0",
120
120
  "mockdate": "^2.0.5",
@@ -26,6 +26,7 @@ export const breakpointLg = '992px'
26
26
  export const breakpointXl = '1200px'
27
27
  export const breakpoints = '(xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)'
28
28
  export const limitedLayoutWidth = '990px'
29
+ export const containerXl = '1280px'
29
30
  export const black = '#000'
30
31
  export const blackNormal = '#333'
31
32
  export const white = '#fff'
@@ -167,6 +167,11 @@
167
167
  "value": "990px",
168
168
  "compiledValue": "990px"
169
169
  },
170
+ {
171
+ "name": "$container-xl",
172
+ "value": "1280px",
173
+ "compiledValue": "1280px"
174
+ },
170
175
  {
171
176
  "name": "$black",
172
177
  "value": "#000",
@@ -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'),
@@ -79,6 +31,7 @@ const generateProps = ({
79
31
  textSrOnly = defaultValue('textSrOnly'),
80
32
  icon = '',
81
33
  multiple = defaultValue('multiple'),
34
+ isCheckCentered = defaultValue('isCheckCentered'),
82
35
  ariaLabelledby,
83
36
  startOpened = true,
84
37
  } = {}) => ({
@@ -94,6 +47,7 @@ const generateProps = ({
94
47
  textSrOnly,
95
48
  icon,
96
49
  multiple,
50
+ isCheckCentered,
97
51
  ariaLabelledby,
98
52
  startOpened,
99
53
  });
@@ -120,6 +74,7 @@ const template = (content, label = '') => `
120
74
  :text-sr-only="textSrOnly"
121
75
  :icon="icon"
122
76
  :multiple="multiple"
77
+ :is-check-centered="isCheckCentered"
123
78
  :aria-labelledby="ariaLabelledby"
124
79
  >
125
80
  ${content}
@@ -134,7 +89,7 @@ export const Default = (args, { argTypes }) => ({
134
89
  },
135
90
  data() {
136
91
  return {
137
- selected: defaultItems[1].value,
92
+ selected: mockOptions[1].value,
138
93
  };
139
94
  },
140
95
  mounted() {
@@ -167,23 +122,28 @@ export const HeaderAndFooter = (args, { argTypes }) => ({
167
122
  },
168
123
  methods: {
169
124
  selectItem(index) {
170
- this.selected.push(defaultItems[index].value);
125
+ this.selected.push(mockOptions[index].value);
171
126
  },
172
127
  },
173
- template: template(`<template #header>
174
- <gl-search-box-by-type/>
175
- </template>
176
- <template #footer>
177
- <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">
178
- <gl-button-group :vertical="false">
179
- <gl-button @click="selectItem(0)">1st</gl-button>
180
- <gl-button @click="selectItem(1)">2nd</gl-button>
181
- <gl-button @click="selectItem(2)">3rd</gl-button>
182
- </gl-button-group>
183
- </div>
184
- </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
+ `),
142
+ });
143
+ HeaderAndFooter.args = generateProps({
144
+ toggleText: 'Header and Footer',
145
+ multiple: true,
185
146
  });
186
- HeaderAndFooter.args = generateProps({ toggleText: 'Header and Footer', multiple: true });
187
147
  HeaderAndFooter.decorators = [makeContainer({ height: '370px' })];
188
148
 
189
149
  export const CustomListItem = (args, { argTypes }) => ({
@@ -211,32 +171,33 @@ export const CustomListItem = (args, { argTypes }) => ({
211
171
  },
212
172
  },
213
173
  template: `
214
- <gl-listbox
215
- v-model="selected"
216
- :items="items"
217
- :category="category"
218
- :variant="variant"
219
- :size="size"
220
- :disabled="disabled"
221
- :loading="loading"
222
- :no-caret="noCaret"
223
- :right="right"
224
- :toggle-text="headerText"
225
- :text-sr-only="textSrOnly"
226
- :icon="icon"
227
- :multiple="multiple"
228
- :aria-labelledby="ariaLabelledby"
229
- >
230
- <template #list-item="{ item }">
231
- <span class="gl-display-flex gl-align-items-center">
232
- <gl-avatar :size="32" class-="gl-mr-3"/>
233
- <span class="gl-display-flex gl-flex-direction-column">
234
- <span class="gl-font-weight-bold gl-white-space-nowrap">{{ item.text }}</span>
235
- <span class="gl-text-gray-400"> {{ item.secondaryText }}</span>
236
- </span>
237
- </span>
238
- </template>
239
- </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>
240
201
  `,
241
202
  });
242
203
 
@@ -247,9 +208,50 @@ CustomListItem.args = generateProps({
247
208
  { value: 'markian', text: 'Mark Florian', secondaryText: '@markian', icon: 'bin' },
248
209
  ],
249
210
  multiple: true,
211
+ isCheckCentered: true,
250
212
  });
251
213
  CustomListItem.decorators = [makeContainer({ height: '200px' })];
252
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
+
253
255
  export default {
254
256
  title: 'base/new-dropdowns/listbox',
255
257
  component: GlListbox,