@gitlab/ui 52.10.0 → 52.12.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 (39) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/components/base/new_dropdowns/base_dropdown/base_dropdown.js +23 -5
  3. package/dist/components/base/new_dropdowns/disclosure/disclosure_dropdown.js +291 -0
  4. package/dist/components/base/new_dropdowns/disclosure/disclosure_dropdown_group.js +90 -0
  5. package/dist/components/base/new_dropdowns/disclosure/disclosure_dropdown_item.js +107 -0
  6. package/dist/components/base/new_dropdowns/disclosure/mock_data.js +128 -0
  7. package/dist/components/base/new_dropdowns/disclosure/utils.js +15 -0
  8. package/dist/components/base/new_dropdowns/listbox/listbox.js +4 -4
  9. package/dist/components/base/new_dropdowns/listbox/listbox_group.js +1 -1
  10. package/dist/components/base/new_dropdowns/listbox/listbox_item.js +1 -1
  11. package/dist/components/regions/empty_state/empty_state.js +12 -1
  12. package/dist/index.css +2 -2
  13. package/dist/index.css.map +1 -1
  14. package/dist/index.js +3 -0
  15. package/package.json +3 -3
  16. package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.spec.js +3 -3
  17. package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.vue +21 -3
  18. package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.md +114 -0
  19. package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.scss +7 -0
  20. package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.spec.js +210 -0
  21. package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.stories.js +306 -0
  22. package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.vue +342 -0
  23. package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown_group.spec.js +82 -0
  24. package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown_group.vue +77 -0
  25. package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown_item.spec.js +94 -0
  26. package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown_item.vue +98 -0
  27. package/src/components/base/new_dropdowns/disclosure/mock_data.js +156 -0
  28. package/src/components/base/new_dropdowns/disclosure/utils.js +18 -0
  29. package/src/components/base/new_dropdowns/disclosure/utils.spec.js +73 -0
  30. package/src/components/base/new_dropdowns/dropdown.scss +6 -0
  31. package/src/components/base/new_dropdowns/listbox/listbox.scss +0 -6
  32. package/src/components/base/new_dropdowns/listbox/listbox.vue +4 -4
  33. package/src/components/base/new_dropdowns/listbox/listbox_group.vue +1 -1
  34. package/src/components/base/new_dropdowns/listbox/listbox_item.vue +1 -1
  35. package/src/components/regions/empty_state/empty_state.spec.js +35 -0
  36. package/src/components/regions/empty_state/empty_state.stories.js +5 -0
  37. package/src/components/regions/empty_state/empty_state.vue +15 -1
  38. package/src/index.js +3 -0
  39. package/src/scss/components.scss +2 -0
package/dist/index.js CHANGED
@@ -46,6 +46,9 @@ export { default as GlDropdownText } from './components/base/dropdown/dropdown_t
46
46
  export { default as GlDropdown } from './components/base/dropdown/dropdown';
47
47
  export { default as GlCollapsibleListbox, default as GlListbox } from './components/base/new_dropdowns/listbox/listbox';
48
48
  export { default as GlListboxItem } from './components/base/new_dropdowns/listbox/listbox_item';
49
+ export { default as GlDisclosureDropdown } from './components/base/new_dropdowns/disclosure/disclosure_dropdown';
50
+ export { default as GlDisclosureDropdownItem } from './components/base/new_dropdowns/disclosure/disclosure_dropdown_item';
51
+ export { default as GlDisclosureDropdownGroup } from './components/base/new_dropdowns/disclosure/disclosure_dropdown_group';
49
52
  export { default as GlPath } from './components/base/path/path';
50
53
  export { default as GlTable } from './components/base/table/table';
51
54
  export { default as GlBreadcrumb } from './components/base/breadcrumb/breadcrumb';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "52.10.0",
3
+ "version": "52.12.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -85,9 +85,9 @@
85
85
  "@babel/core": "^7.20.12",
86
86
  "@babel/preset-env": "^7.20.2",
87
87
  "@gitlab/eslint-plugin": "18.1.0",
88
- "@gitlab/fonts": "^1.0.1",
88
+ "@gitlab/fonts": "^1.1.0",
89
89
  "@gitlab/stylelint-config": "4.1.0",
90
- "@gitlab/svgs": "3.15.0",
90
+ "@gitlab/svgs": "3.16.0",
91
91
  "@rollup/plugin-commonjs": "^11.1.0",
92
92
  "@rollup/plugin-node-resolve": "^7.1.3",
93
93
  "@rollup/plugin-replace": "^2.3.2",
@@ -1,6 +1,6 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import { nextTick } from 'vue';
3
- import { GL_DROPDOWN_HIDDEN, GL_DROPDOWN_SHOWN, POPPER_CONFIG } from '../constants';
3
+ import { ENTER, GL_DROPDOWN_HIDDEN, GL_DROPDOWN_SHOWN, POPPER_CONFIG } from '../constants';
4
4
  import GlBaseDropdown from './base_dropdown.vue';
5
5
 
6
6
  const destroyPopper = jest.fn();
@@ -197,14 +197,14 @@ describe('base dropdown', () => {
197
197
  const toggle = findCustomDropdownToggle();
198
198
  const menu = findDropdownMenu();
199
199
  // open menu clicking toggle btn
200
- await toggle.trigger('keydown.enter');
200
+ await toggle.trigger('keydown', { code: ENTER });
201
201
  expect(menu.classes('show')).toBe(true);
202
202
  expect(toggle.attributes('aria-expanded')).toBe('true');
203
203
  await nextTick();
204
204
  expect(wrapper.emitted(GL_DROPDOWN_SHOWN).length).toBe(1);
205
205
 
206
206
  // close menu clicking toggle btn again
207
- await toggle.trigger('keydown.enter');
207
+ await toggle.trigger('keydown', { code: ENTER });
208
208
  expect(menu.classes('show')).toBe(false);
209
209
  expect(toggle.attributes('aria-expanded')).toBeUndefined();
210
210
  expect(wrapper.emitted(GL_DROPDOWN_HIDDEN).length).toBe(1);
@@ -1,11 +1,12 @@
1
1
  <script>
2
+ import { uniqueId } from 'lodash';
2
3
  import { createPopper } from '@popperjs/core';
3
4
  import {
4
5
  buttonCategoryOptions,
5
6
  buttonSizeOptions,
6
7
  dropdownVariantOptions,
7
8
  } from '../../../../utils/constants';
8
- import { POPPER_CONFIG, GL_DROPDOWN_SHOWN, GL_DROPDOWN_HIDDEN } from '../constants';
9
+ import { POPPER_CONFIG, GL_DROPDOWN_SHOWN, GL_DROPDOWN_HIDDEN, ENTER, SPACE } from '../constants';
9
10
 
10
11
  import GlButton from '../../button/button.vue';
11
12
  import GlIcon from '../../icon/icon.vue';
@@ -107,6 +108,7 @@ export default {
107
108
  data() {
108
109
  return {
109
110
  visible: false,
111
+ baseDropdownId: uniqueId('base-dropdown-'),
110
112
  };
111
113
  },
112
114
  computed: {
@@ -145,13 +147,21 @@ export default {
145
147
  disabled: this.disabled,
146
148
  loading: this.loading,
147
149
  class: this.toggleButtonClasses,
150
+ listeners: {
151
+ click: () => this.toggle(),
152
+ },
148
153
  };
149
154
  }
150
155
 
151
156
  return {
152
157
  is: 'div',
158
+ role: 'button',
153
159
  class: 'gl-dropdown-custom-toggle gl-hover-cursor-pointer',
154
160
  tabindex: '0',
161
+ listeners: {
162
+ keydown: (event) => this.onKeydown(event),
163
+ click: () => this.toggle(),
164
+ },
155
165
  };
156
166
  },
157
167
  toggleElement() {
@@ -216,6 +226,13 @@ export default {
216
226
  focusToggle() {
217
227
  this.toggleElement.focus();
218
228
  },
229
+ onKeydown(event) {
230
+ const { code } = event;
231
+
232
+ if (code === ENTER || code === SPACE) {
233
+ this.toggle();
234
+ }
235
+ },
219
236
  },
220
237
  };
221
238
  </script>
@@ -234,8 +251,8 @@ export default {
234
251
  :aria-haspopup="ariaHaspopup"
235
252
  :aria-expanded="visible"
236
253
  :aria-labelledby="toggleLabelledBy"
237
- @keydown.enter="toggle"
238
- @click="toggle"
254
+ :aria-controls="baseDropdownId"
255
+ v-on="toggleOptions.listeners"
239
256
  >
240
257
  <!-- @slot Custom toggle button content -->
241
258
  <slot name="toggle">
@@ -247,6 +264,7 @@ export default {
247
264
  </component>
248
265
 
249
266
  <div
267
+ :id="baseDropdownId"
250
268
  ref="content"
251
269
  data-testid="base-dropdown-menu"
252
270
  class="dropdown-menu"
@@ -0,0 +1,114 @@
1
+ A disclosure dropdown is a button that toggles a panel containing a list of actions and/or links. Use
2
+ [this decision tree](https://design.gitlab.com/components/dropdown-overview#which-component-should-you-use)
3
+ to make sure this is the right dropdown component for you.
4
+
5
+ ### Basic usage
6
+
7
+ ```html
8
+ <gl-disclosure-dropdown-dropdown
9
+ toggle-text="Actions"
10
+ :items="items"
11
+ />
12
+ ```
13
+
14
+ ### Icon-only disclosure dropdown
15
+
16
+ Icon-only disclosure dropdowns must have an accessible name.
17
+ You can provide this with the combination of `toggleText` and `textSrOnly` props.
18
+
19
+ Optionally, you can use `no-caret` to remove the caret and `category="tertiary"` to remove the border.
20
+
21
+ ```html
22
+ <gl-disclosure-dropdown
23
+ icon="ellipsis_v"
24
+ toggle-text="Actions"
25
+ text-sr-only
26
+ category="tertiary"
27
+ no-caret
28
+ />
29
+ ```
30
+
31
+ ### Opening the disclosure dropdown
32
+
33
+ Disclosure dropdown will open on toggle button click (if it was previously closed).
34
+ On open, `GlDisclosureDropdown` will emit the `shown` event.
35
+
36
+ ### Closing the disclosure dropdown
37
+
38
+ The disclosure dropdown is closed by any of the following:
39
+
40
+ - pressing <kbd>Esc</kbd>
41
+ - clicking anywhere outside the component
42
+
43
+ After closing, `GlDisclosureDropdown` emits a `hidden` event.
44
+
45
+ ### Setting disclosure dropdown items
46
+
47
+ Use the `items` prop to provide actions/links to the disclosure dropdown. Each item can be
48
+ either an item or a group. Below are the expected shapes of these objects:
49
+
50
+ ```typescript
51
+ type Item = {
52
+ text: string
53
+ href?: string,
54
+ action?: function,
55
+ }
56
+
57
+ type Group = {
58
+ name?: string
59
+ items: Array<Item>
60
+ }
61
+
62
+ type ItemsProp = Array<Item> | Array<Group>
63
+ ```
64
+
65
+ #### Actions/links
66
+
67
+ The `text` property is used to render the default disclosure dropdown item
68
+ template. If you want to render a custom template for items, use the
69
+ `list-item` scoped slot:
70
+
71
+ ```html
72
+ <gl-disclosure-dropdown :items="items">
73
+ <template #list-item="{ item }">
74
+ <a class="gl-hover-text-decoration-none gl-text-gray-900"
75
+ tabindex="-1"
76
+ :href="item.href"
77
+ v-bind="item.extraAttrs">
78
+ {{ item.text }}
79
+ <gl-badge pill variant="info" v-if="item.count">{{ item.count }}</gl-badge>
80
+ </a>
81
+ </template>
82
+ </gl-disclosure-dropdown>
83
+ ```
84
+
85
+ **Note:** when providing custom content to the item, user should
86
+ define the correct tab order inside the disclosure dropdown by setting
87
+ the `tabindex` attribute on the elements.
88
+ The `li` item will get the focus so you might want elements inside it
89
+ not to be focused - this can be done by setting `tabindex="-1"` on them.
90
+
91
+ #### Groups
92
+
93
+ Actions/links can be contained within groups. A group can have a `name`
94
+ property, which will be used as the group header if present.
95
+ It also has a required property `items` that must be an array of links/actions.
96
+
97
+ Groups can be at most one level deep: a group can only contain actions/links.
98
+ Items and groups _cannot_ be siblings. Either all items are actions/links,
99
+ or they are all groups.
100
+
101
+ To render custom group labels, use the `group-label` scoped slot:
102
+
103
+ ```html
104
+ <gl-disclosure-dropdown :items="groups">
105
+ <template #group-label="{ group }">
106
+ {{ group.name }} <gl-badge size="sm">{{ group.items.length }}</gl-badge>
107
+ </template>
108
+ </gl-disclosure-dropdown>
109
+ ```
110
+
111
+ #### Miscellaneous content
112
+
113
+ Besides default components, disclosure dropdown can render miscellaneous content inside it.
114
+ In this case the user is responsible for handling all events and navigation inside the disclosure.
@@ -0,0 +1,7 @@
1
+ .gl-disclosure-dropdown {
2
+ &.gl-dropdown {
3
+ .gl-dropdown-inner {
4
+ max-height: none;
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,210 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import GlBaseDropdown from '../base_dropdown/base_dropdown.vue';
3
+ import {
4
+ GL_DROPDOWN_SHOWN,
5
+ GL_DROPDOWN_HIDDEN,
6
+ ARROW_DOWN,
7
+ ARROW_UP,
8
+ HOME,
9
+ END,
10
+ } from '../constants';
11
+ import GlDisclosureDropdown from './disclosure_dropdown.vue';
12
+ import GlDisclosureDropdownItem from './disclosure_dropdown_item.vue';
13
+ import GlDisclosureDropdownGroup from './disclosure_dropdown_group.vue';
14
+ import { mockItems, mockGroups } from './mock_data';
15
+
16
+ const ITEM_SELECTOR = '[data-testid="disclosure-dropdown-item"]';
17
+
18
+ describe('GlDisclosureDropdown', () => {
19
+ let wrapper;
20
+
21
+ const buildWrapper = (propsData, slots = {}) => {
22
+ wrapper = mount(GlDisclosureDropdown, {
23
+ propsData,
24
+ slots,
25
+ attachTo: document.body,
26
+ });
27
+ };
28
+
29
+ const findBaseDropdown = () => wrapper.findComponent(GlBaseDropdown);
30
+ const findDisclosureContent = () => wrapper.find('[data-testid="disclosure-content"]');
31
+ const findDisclosureItems = (root = wrapper) => root.findAllComponents(GlDisclosureDropdownItem);
32
+ const findDisclosureGroups = () => wrapper.findAllComponents(GlDisclosureDropdownGroup);
33
+ const findListItem = (index) => findDisclosureItems().at(index).findComponent(ITEM_SELECTOR);
34
+
35
+ describe('toggle text', () => {
36
+ it('should pass toggle text to base dropdown', () => {
37
+ const toggleText = 'Merge requests';
38
+ buildWrapper({ items: mockItems, toggleText });
39
+ expect(findBaseDropdown().props('toggleText')).toBe(toggleText);
40
+ });
41
+ });
42
+
43
+ describe('ARIA attributes', () => {
44
+ it('should provide `toggleId` to the base dropdown and reference it in`aria-labelledby` attribute of the list container`', async () => {
45
+ await buildWrapper({ items: mockItems });
46
+ expect(findBaseDropdown().props('toggleId')).toBe(
47
+ findDisclosureContent().attributes('aria-labelledby')
48
+ );
49
+ });
50
+
51
+ it('should reference `listAriaLabelledby`', async () => {
52
+ const listAriaLabelledBy = 'first-label-id second-label-id';
53
+ await buildWrapper({ items: mockItems, listAriaLabelledBy });
54
+ expect(findDisclosureContent().attributes('aria-labelledby')).toBe(listAriaLabelledBy);
55
+ });
56
+ });
57
+
58
+ describe('onShow', () => {
59
+ const showDropdown = () => {
60
+ buildWrapper({
61
+ items: mockItems,
62
+ });
63
+ findBaseDropdown().vm.$emit(GL_DROPDOWN_SHOWN);
64
+ };
65
+
66
+ beforeEach(() => {
67
+ showDropdown();
68
+ });
69
+
70
+ it('should re-emit the event', () => {
71
+ expect(wrapper.emitted(GL_DROPDOWN_SHOWN)).toHaveLength(1);
72
+ });
73
+
74
+ it('should focus the first item', () => {
75
+ expect(findDisclosureItems().at(0).find(ITEM_SELECTOR).element).toHaveFocus();
76
+ });
77
+ });
78
+
79
+ describe('onHide', () => {
80
+ beforeEach(() => {
81
+ buildWrapper();
82
+ findBaseDropdown().vm.$emit(GL_DROPDOWN_HIDDEN);
83
+ });
84
+
85
+ it('should re-emit the event', () => {
86
+ expect(wrapper.emitted(GL_DROPDOWN_HIDDEN)).toHaveLength(1);
87
+ });
88
+ });
89
+
90
+ describe('navigating the items', () => {
91
+ let firstItem;
92
+ let secondItem;
93
+ let thirdItem;
94
+
95
+ beforeEach(() => {
96
+ buildWrapper({ items: mockItems });
97
+ findBaseDropdown().vm.$emit(GL_DROPDOWN_SHOWN);
98
+ firstItem = findListItem(0);
99
+ secondItem = findListItem(1);
100
+ thirdItem = findListItem(2);
101
+ });
102
+
103
+ it('should move the focus down the list of items on `ARROW_DOWN` and stop on the last item', async () => {
104
+ expect(firstItem.element).toHaveFocus();
105
+ await firstItem.trigger('keydown', { code: ARROW_DOWN });
106
+ expect(secondItem.element).toHaveFocus();
107
+ await secondItem.trigger('keydown', { code: ARROW_DOWN });
108
+ expect(thirdItem.element).toHaveFocus();
109
+ await thirdItem.trigger('keydown', { code: ARROW_DOWN });
110
+ expect(thirdItem.element).toHaveFocus();
111
+ });
112
+
113
+ it('should move the focus up the list of items on `ARROW_UP` and stop on the first item', async () => {
114
+ await firstItem.trigger('keydown', { code: ARROW_DOWN });
115
+ await secondItem.trigger('keydown', { code: ARROW_DOWN });
116
+ expect(thirdItem.element).toHaveFocus();
117
+ await thirdItem.trigger('keydown', { code: ARROW_UP });
118
+ expect(secondItem.element).toHaveFocus();
119
+ await secondItem.trigger('keydown', { code: ARROW_UP });
120
+ expect(firstItem.element).toHaveFocus();
121
+ await firstItem.trigger('keydown', { code: ARROW_UP });
122
+ expect(firstItem.element).toHaveFocus();
123
+ });
124
+
125
+ it('should move focus to the last item on `END` keydown', async () => {
126
+ expect(firstItem.element).toHaveFocus();
127
+ await firstItem.trigger('keydown', { code: END });
128
+ expect(thirdItem.element).toHaveFocus();
129
+ await thirdItem.trigger('keydown', { code: END });
130
+ expect(thirdItem.element).toHaveFocus();
131
+ });
132
+
133
+ it('should move focus to the first item on `HOME` keydown', async () => {
134
+ await firstItem.trigger('keydown', { code: ARROW_DOWN });
135
+ await secondItem.trigger('keydown', { code: ARROW_DOWN });
136
+ expect(thirdItem.element).toHaveFocus();
137
+ await thirdItem.trigger('keydown', { code: HOME });
138
+ expect(firstItem.element).toHaveFocus();
139
+ await thirdItem.trigger('keydown', { code: HOME });
140
+ expect(firstItem.element).toHaveFocus();
141
+ });
142
+ });
143
+
144
+ describe('slot content', () => {
145
+ const headerContent = 'Header Content';
146
+ const footerContent = 'Footer Content';
147
+ const toggleContent = 'Toggle Content';
148
+ const defaultContent = 'Toggle Content';
149
+ const slots = {
150
+ header: headerContent,
151
+ footer: footerContent,
152
+ toggle: toggleContent,
153
+ default: defaultContent,
154
+ };
155
+
156
+ it('renders all slot content', () => {
157
+ buildWrapper({}, slots);
158
+ expect(wrapper.text()).toContain(headerContent);
159
+ expect(wrapper.text()).toContain(footerContent);
160
+ expect(wrapper.text()).toContain(toggleContent);
161
+ expect(wrapper.text()).toContain(defaultContent);
162
+ });
163
+ });
164
+
165
+ describe('with groups', () => {
166
+ it('renders groups of items', () => {
167
+ buildWrapper({ items: mockGroups });
168
+
169
+ const groups = findDisclosureGroups();
170
+
171
+ expect(groups.length).toBe(mockGroups.length);
172
+
173
+ mockGroups.forEach((group, i) => {
174
+ expect(findDisclosureItems(groups.at(i))).toHaveLength(group.items.length);
175
+ });
176
+ });
177
+ });
178
+
179
+ describe('action', () => {
180
+ it('should re-emit the `action` event when it is emitted on the item for custom handling', () => {
181
+ buildWrapper({
182
+ items: mockItems,
183
+ });
184
+
185
+ findListItem(0).vm.$emit('action', mockItems[0]);
186
+ expect(wrapper.emitted('action')).toHaveLength(1);
187
+ expect(wrapper.emitted('action')[0][0]).toEqual(mockItems[0]);
188
+ });
189
+ });
190
+
191
+ describe('disclosure options', () => {
192
+ it('should render the `ul` as content tag and not add `role` attribute when it is a list of items only', () => {
193
+ buildWrapper({ items: mockItems });
194
+ expect(findDisclosureContent().element.tagName).toBe('UL');
195
+ expect(findDisclosureContent().attributes('role')).toBeUndefined();
196
+ });
197
+
198
+ it('should render the `div` as content tag and add `role` attribute when it is a list of groups', () => {
199
+ buildWrapper({ items: mockGroups });
200
+ expect(findDisclosureContent().element.tagName).toBe('DIV');
201
+ expect(findDisclosureContent().attributes('role')).toBe('group');
202
+ });
203
+
204
+ it('should render the `div` as content tag and NOT add `role` otherwise', () => {
205
+ buildWrapper({ items: null }, { default: 'Some other content' });
206
+ expect(findDisclosureContent().element.tagName).toBe('DIV');
207
+ expect(findDisclosureContent().attributes('role')).toBeUndefined();
208
+ });
209
+ });
210
+ });