@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.
- package/CHANGELOG.md +14 -0
- package/dist/components/base/new_dropdowns/base_dropdown/base_dropdown.js +23 -5
- package/dist/components/base/new_dropdowns/disclosure/disclosure_dropdown.js +291 -0
- package/dist/components/base/new_dropdowns/disclosure/disclosure_dropdown_group.js +90 -0
- package/dist/components/base/new_dropdowns/disclosure/disclosure_dropdown_item.js +107 -0
- package/dist/components/base/new_dropdowns/disclosure/mock_data.js +128 -0
- package/dist/components/base/new_dropdowns/disclosure/utils.js +15 -0
- package/dist/components/base/new_dropdowns/listbox/listbox.js +4 -4
- package/dist/components/base/new_dropdowns/listbox/listbox_group.js +1 -1
- package/dist/components/base/new_dropdowns/listbox/listbox_item.js +1 -1
- package/dist/components/regions/empty_state/empty_state.js +12 -1
- package/dist/index.css +2 -2
- package/dist/index.css.map +1 -1
- package/dist/index.js +3 -0
- package/package.json +3 -3
- package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.spec.js +3 -3
- package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.vue +21 -3
- package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.md +114 -0
- package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.scss +7 -0
- package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.spec.js +210 -0
- package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.stories.js +306 -0
- package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.vue +342 -0
- package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown_group.spec.js +82 -0
- package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown_group.vue +77 -0
- package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown_item.spec.js +94 -0
- package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown_item.vue +98 -0
- package/src/components/base/new_dropdowns/disclosure/mock_data.js +156 -0
- package/src/components/base/new_dropdowns/disclosure/utils.js +18 -0
- package/src/components/base/new_dropdowns/disclosure/utils.spec.js +73 -0
- package/src/components/base/new_dropdowns/dropdown.scss +6 -0
- package/src/components/base/new_dropdowns/listbox/listbox.scss +0 -6
- package/src/components/base/new_dropdowns/listbox/listbox.vue +4 -4
- package/src/components/base/new_dropdowns/listbox/listbox_group.vue +1 -1
- package/src/components/base/new_dropdowns/listbox/listbox_item.vue +1 -1
- package/src/components/regions/empty_state/empty_state.spec.js +35 -0
- package/src/components/regions/empty_state/empty_state.stories.js +5 -0
- package/src/components/regions/empty_state/empty_state.vue +15 -1
- package/src/index.js +3 -0
- package/src/scss/components.scss +2 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { shallowMount } from '@vue/test-utils';
|
|
2
|
+
import GlDisclosureDropdownGroup, {
|
|
3
|
+
GROUP_TOP_BORDER_CLASSES,
|
|
4
|
+
} from './disclosure_dropdown_group.vue';
|
|
5
|
+
import GlDisclosureDropdownItem from './disclosure_dropdown_item.vue';
|
|
6
|
+
import { mockGroups, mockProfileGroups } from './mock_data';
|
|
7
|
+
|
|
8
|
+
describe('GlDisclosureDropdownGroup', () => {
|
|
9
|
+
let wrapper;
|
|
10
|
+
|
|
11
|
+
const buildWrapper = ({ propsData, slots } = {}) => {
|
|
12
|
+
wrapper = shallowMount(GlDisclosureDropdownGroup, {
|
|
13
|
+
propsData: {
|
|
14
|
+
group: mockGroups[0],
|
|
15
|
+
...propsData,
|
|
16
|
+
},
|
|
17
|
+
slots,
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const findGroup = () => wrapper.find('ul[role="group"]');
|
|
22
|
+
const findItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
|
|
23
|
+
const findByTestId = (testid, root = wrapper) => root.find(`[data-testid="${testid}"]`);
|
|
24
|
+
const findLabelElement = () => {
|
|
25
|
+
const labelElementId = findGroup().attributes('aria-labelledby');
|
|
26
|
+
return wrapper.find(`#${labelElementId}`);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
it('renders default slot content', () => {
|
|
30
|
+
buildWrapper({ slots: { default: '<li data-testid="default-slot-content"></li>' } });
|
|
31
|
+
|
|
32
|
+
expect(findByTestId('default-slot-content').exists()).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('renders list of items if default content was not provided', () => {
|
|
36
|
+
buildWrapper();
|
|
37
|
+
|
|
38
|
+
expect(findItems()).toHaveLength(mockGroups[0].items.length);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('renders `list-item` content in a default slot of `GlDisclosureDropdownItem`', () => {
|
|
42
|
+
buildWrapper({
|
|
43
|
+
slots: { 'list-item': '<li data-testid="list-item-content"></li>' },
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(findItems()).toHaveLength(mockGroups[0].items.length);
|
|
47
|
+
|
|
48
|
+
expect(findItems().at(0).find('[data-testid="list-item-content"]').exists()).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('label', () => {
|
|
52
|
+
it('labels the group when label provided', () => {
|
|
53
|
+
buildWrapper();
|
|
54
|
+
expect(findLabelElement().text()).toBe(mockGroups[0].name);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('does not label the group when label is not provided', () => {
|
|
58
|
+
buildWrapper({ propsData: { group: mockProfileGroups[0] } });
|
|
59
|
+
expect(findLabelElement().exists()).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('allows arbitrary content for group label', () => {
|
|
63
|
+
buildWrapper({ slots: { 'group-label': '<i data-testid="custom-name"></i>' } });
|
|
64
|
+
|
|
65
|
+
expect(findByTestId('custom-name', findLabelElement()).exists()).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('separator', () => {
|
|
70
|
+
const topBorderClasses = GROUP_TOP_BORDER_CLASSES.split(' ');
|
|
71
|
+
|
|
72
|
+
it('should not add top border by default', () => {
|
|
73
|
+
buildWrapper();
|
|
74
|
+
expect(wrapper.classes()).not.toEqual(expect.arrayContaining(topBorderClasses));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should add top border classes when `bordered` props is set to `true`', () => {
|
|
78
|
+
buildWrapper({ propsData: { bordered: true } });
|
|
79
|
+
expect(wrapper.classes()).toEqual(expect.arrayContaining(topBorderClasses));
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { uniqueId } from 'lodash';
|
|
3
|
+
import GlDisclosureDropdownItem from './disclosure_dropdown_item.vue';
|
|
4
|
+
import { isGroup } from './utils';
|
|
5
|
+
|
|
6
|
+
export const GROUP_TOP_BORDER_CLASSES = 'gl-border-t gl-pt-3 gl-mt-3';
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
components: {
|
|
10
|
+
GlDisclosureDropdownItem,
|
|
11
|
+
},
|
|
12
|
+
props: {
|
|
13
|
+
/**
|
|
14
|
+
* Group of items
|
|
15
|
+
*/
|
|
16
|
+
group: {
|
|
17
|
+
type: Object,
|
|
18
|
+
required: false,
|
|
19
|
+
default: null,
|
|
20
|
+
validator: isGroup,
|
|
21
|
+
},
|
|
22
|
+
/**
|
|
23
|
+
* If 'true', will set top border for the group
|
|
24
|
+
* to separate from other groups
|
|
25
|
+
*/
|
|
26
|
+
bordered: {
|
|
27
|
+
type: Boolean,
|
|
28
|
+
required: false,
|
|
29
|
+
default: false,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
computed: {
|
|
33
|
+
borderClass() {
|
|
34
|
+
return this.bordered ? GROUP_TOP_BORDER_CLASSES : null;
|
|
35
|
+
},
|
|
36
|
+
showHeader() {
|
|
37
|
+
return this.$scopedSlots['group-label'] || this.group?.name;
|
|
38
|
+
},
|
|
39
|
+
groupLabeledBy() {
|
|
40
|
+
return this.showHeader ? this.nameId : null;
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
created() {
|
|
44
|
+
this.nameId = uniqueId('gl-disclosure-dropdown-group-');
|
|
45
|
+
},
|
|
46
|
+
methods: {
|
|
47
|
+
handleAction(action) {
|
|
48
|
+
this.$emit('action', action);
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<div :class="borderClass">
|
|
56
|
+
<div
|
|
57
|
+
v-if="showHeader"
|
|
58
|
+
:id="nameId"
|
|
59
|
+
aria-hidden="true"
|
|
60
|
+
class="gl-pl-5 gl-py-2 gl-font-sm gl-font-weight-bold"
|
|
61
|
+
>
|
|
62
|
+
<slot name="group-label">{{ group.name }}</slot>
|
|
63
|
+
</div>
|
|
64
|
+
<ul role="group" :aria-labelledby="groupLabeledBy" class="gl-mb-0 gl-pl-0 gl-list-style-none">
|
|
65
|
+
<slot>
|
|
66
|
+
<gl-disclosure-dropdown-item
|
|
67
|
+
v-for="item in group.items"
|
|
68
|
+
:key="item.text"
|
|
69
|
+
:item="item"
|
|
70
|
+
@action="handleAction"
|
|
71
|
+
>
|
|
72
|
+
<slot name="list-item" :item="item"> </slot>
|
|
73
|
+
</gl-disclosure-dropdown-item>
|
|
74
|
+
</slot>
|
|
75
|
+
</ul>
|
|
76
|
+
</div>
|
|
77
|
+
</template>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import { ENTER, SPACE } from '../constants';
|
|
3
|
+
import { mockItems } from './mock_data';
|
|
4
|
+
|
|
5
|
+
import GlDisclosureDropdownItem from './disclosure_dropdown_item.vue';
|
|
6
|
+
|
|
7
|
+
describe('GlDisclosureDropdownItem', () => {
|
|
8
|
+
let wrapper;
|
|
9
|
+
|
|
10
|
+
const buildWrapper = (propsData, slots = {}) => {
|
|
11
|
+
wrapper = mount(GlDisclosureDropdownItem, {
|
|
12
|
+
propsData,
|
|
13
|
+
slots,
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
const findItem = () => wrapper.find('[data-testid="disclosure-dropdown-item"]');
|
|
17
|
+
|
|
18
|
+
describe('when default slot content provided', () => {
|
|
19
|
+
const content = 'This is an item';
|
|
20
|
+
const slots = { default: content };
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
buildWrapper({}, slots);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('renders it', () => {
|
|
26
|
+
expect(wrapper.text()).toContain(content);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it.each`
|
|
30
|
+
trigger | event
|
|
31
|
+
${() => findItem().trigger('click')} | ${'click'}
|
|
32
|
+
${() => findItem().trigger('keydown', { code: ENTER })} | ${'ENTER'}
|
|
33
|
+
${() => findItem().trigger('keydown', { code: SPACE })} | ${'SPACE'}
|
|
34
|
+
`(`$event should emit 'action' event`, ({ trigger }) => {
|
|
35
|
+
trigger();
|
|
36
|
+
expect(wrapper.emitted('action')).toHaveLength(1);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('when item has an `href`', () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
buildWrapper({ item: mockItems[0] });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const findLink = () => wrapper.find('a.dropdown-item');
|
|
46
|
+
|
|
47
|
+
it('should render a link', () => {
|
|
48
|
+
expect(findLink().exists()).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should set correct attributes', () => {
|
|
52
|
+
expect(findLink().attributes('href')).toBe(mockItems[0].href);
|
|
53
|
+
expect(findLink().attributes()).toMatchObject(mockItems[0].extraAttrs);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('when item has an `action`', () => {
|
|
58
|
+
const action = jest.spyOn(mockItems[1], 'action');
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
buildWrapper({ item: mockItems[1] });
|
|
62
|
+
action.mockClear();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const findButton = () => wrapper.find('button.dropdown-item');
|
|
66
|
+
|
|
67
|
+
it('should render a button', () => {
|
|
68
|
+
expect(findButton().exists()).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should set correct attributes', () => {
|
|
72
|
+
const attrs = { ...mockItems[1].extraAttrs };
|
|
73
|
+
delete attrs.class;
|
|
74
|
+
expect(findButton().classes()).toContain(mockItems[1].extraAttrs.class);
|
|
75
|
+
expect(findButton().attributes()).toMatchObject(attrs);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should call `action` on `click`', () => {
|
|
79
|
+
findButton().trigger('click');
|
|
80
|
+
expect(action).toHaveBeenCalled();
|
|
81
|
+
expect(wrapper.emitted('action')).toHaveLength(1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it.each`
|
|
85
|
+
trigger | event
|
|
86
|
+
${() => findItem().trigger('click')} | ${'click'}
|
|
87
|
+
${() => findItem().trigger('keydown', { code: ENTER })} | ${'ENTER'}
|
|
88
|
+
${() => findItem().trigger('keydown', { code: SPACE })} | ${'SPACE'}
|
|
89
|
+
`(`$event will execute action and emit 'action' event`, ({ trigger }) => {
|
|
90
|
+
trigger();
|
|
91
|
+
expect(wrapper.emitted('action')).toHaveLength(1);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { ENTER, SPACE } from '../constants';
|
|
3
|
+
import { stopEvent } from '../../../../utils/utils';
|
|
4
|
+
import { isItem } from './utils';
|
|
5
|
+
|
|
6
|
+
export const ITEM_CLASS = 'gl-dropdown-item';
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
ITEM_CLASS,
|
|
10
|
+
props: {
|
|
11
|
+
item: {
|
|
12
|
+
type: Object,
|
|
13
|
+
required: false,
|
|
14
|
+
default: null,
|
|
15
|
+
validator: isItem,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
computed: {
|
|
19
|
+
isActionItem() {
|
|
20
|
+
return this.item?.action;
|
|
21
|
+
},
|
|
22
|
+
isCustomContent() {
|
|
23
|
+
return Boolean(this.$scopedSlots.default);
|
|
24
|
+
},
|
|
25
|
+
itemComponent() {
|
|
26
|
+
if (this.isActionItem)
|
|
27
|
+
return {
|
|
28
|
+
is: 'button',
|
|
29
|
+
attrs: {
|
|
30
|
+
...this.item.extraAttrs,
|
|
31
|
+
},
|
|
32
|
+
listeners: {
|
|
33
|
+
click: () => this.item.action(),
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
is: 'a',
|
|
38
|
+
attrs: {
|
|
39
|
+
href: this.item.href,
|
|
40
|
+
...this.item.extraAttrs,
|
|
41
|
+
},
|
|
42
|
+
listeners: {},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
methods: {
|
|
47
|
+
onKeydown(event) {
|
|
48
|
+
const { code } = event;
|
|
49
|
+
|
|
50
|
+
if (code === ENTER || code === SPACE) {
|
|
51
|
+
stopEvent(event);
|
|
52
|
+
/** Instead of simply navigating or calling the action, we want
|
|
53
|
+
* the `a/button` to be the target of the event as it might have additional attributes.
|
|
54
|
+
* E.g. `a` might have `target` attribute.
|
|
55
|
+
* `bubbles` is set to `true` as the parent `li` item has this event listener and thus we'll get a loop.
|
|
56
|
+
*/
|
|
57
|
+
this.$refs.item?.dispatchEvent(new MouseEvent('click', { bubbles: false }));
|
|
58
|
+
this.action();
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
action() {
|
|
62
|
+
this.$emit('action', this.item);
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<template>
|
|
69
|
+
<li
|
|
70
|
+
tabindex="0"
|
|
71
|
+
:class="$options.ITEM_CLASS"
|
|
72
|
+
class="gl-dropdown-item gl-focusable-dropdown-item"
|
|
73
|
+
data-testid="disclosure-dropdown-item"
|
|
74
|
+
@click="action"
|
|
75
|
+
@keydown="onKeydown"
|
|
76
|
+
>
|
|
77
|
+
<div v-if="isCustomContent" class="dropdown-item">
|
|
78
|
+
<div class="gl-dropdown-item-text-wrapper">
|
|
79
|
+
<slot></slot>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<template v-else>
|
|
84
|
+
<component
|
|
85
|
+
:is="itemComponent.is"
|
|
86
|
+
v-bind="itemComponent.attrs"
|
|
87
|
+
ref="item"
|
|
88
|
+
class="dropdown-item"
|
|
89
|
+
tabindex="-1"
|
|
90
|
+
v-on="itemComponent.listeners"
|
|
91
|
+
>
|
|
92
|
+
<span class="gl-dropdown-item-text-wrapper">
|
|
93
|
+
{{ item.text }}
|
|
94
|
+
</span>
|
|
95
|
+
</component>
|
|
96
|
+
</template>
|
|
97
|
+
</li>
|
|
98
|
+
</template>
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
export const mockItems = [
|
|
2
|
+
{
|
|
3
|
+
text: 'Mark as draft',
|
|
4
|
+
href: 'https://gitlab.com',
|
|
5
|
+
extraAttrs: {
|
|
6
|
+
target: '_blank',
|
|
7
|
+
rel: 'nofollow',
|
|
8
|
+
'data-method': 'put',
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
text: 'Close merge request',
|
|
13
|
+
action: () => {
|
|
14
|
+
// eslint-disable-next-line no-console
|
|
15
|
+
console.log('CLOSED');
|
|
16
|
+
},
|
|
17
|
+
extraAttrs: {
|
|
18
|
+
class: 'gl-text-red-500!',
|
|
19
|
+
rel: 'nofollow',
|
|
20
|
+
'data-method': 'put',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
text: 'Create new',
|
|
25
|
+
href: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/new',
|
|
26
|
+
extraAttrs: {
|
|
27
|
+
rel: 'nofollow',
|
|
28
|
+
target: '_blank',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export const mockItemsCustomItem = [
|
|
34
|
+
{
|
|
35
|
+
text: 'Assigned to you',
|
|
36
|
+
href: 'https://gitlab.com/dashboard/merge_requests',
|
|
37
|
+
count: '2',
|
|
38
|
+
extraAttrs: {
|
|
39
|
+
target: '_blank',
|
|
40
|
+
rel: 'nofollow',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
text: 'Review requests from you',
|
|
45
|
+
href: 'https://gitlab.com/dashboard/merge_requests',
|
|
46
|
+
count: 0,
|
|
47
|
+
extraAttrs: {
|
|
48
|
+
target: '_blank',
|
|
49
|
+
rel: 'nofollow',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
export const mockGroups = [
|
|
55
|
+
{
|
|
56
|
+
name: 'This project',
|
|
57
|
+
items: [
|
|
58
|
+
{
|
|
59
|
+
text: 'New issue',
|
|
60
|
+
href: 'https://gitlab.com/gitlab-org/gitlab/-/issues/new',
|
|
61
|
+
extraAttrs: {
|
|
62
|
+
target: '_blank',
|
|
63
|
+
rel: 'nofollow',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
text: 'New merge request',
|
|
68
|
+
href: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/new',
|
|
69
|
+
extraAttrs: {
|
|
70
|
+
target: '_blank',
|
|
71
|
+
rel: 'nofollow',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
text: 'New snippet',
|
|
76
|
+
href: 'https://gitlab.com/gitlab-org/gitlab/-/snippets/new',
|
|
77
|
+
extraAttrs: {
|
|
78
|
+
target: '_blank',
|
|
79
|
+
rel: 'nofollow',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'GitLab',
|
|
86
|
+
items: [
|
|
87
|
+
{
|
|
88
|
+
text: 'New project',
|
|
89
|
+
href: 'https://gitlab.com/projects/new',
|
|
90
|
+
extraAttrs: {
|
|
91
|
+
target: '_blank',
|
|
92
|
+
rel: 'nofollow',
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
text: 'New group',
|
|
97
|
+
href: 'https://gitlab.com/groups/new',
|
|
98
|
+
extraAttrs: {
|
|
99
|
+
target: '_blank',
|
|
100
|
+
rel: 'nofollow',
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
text: 'New snippet',
|
|
105
|
+
href: 'https://gitlab.com/snippets/new',
|
|
106
|
+
extraAttrs: {
|
|
107
|
+
target: '_blank',
|
|
108
|
+
rel: 'nofollow',
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
export const mockProfileGroups = [
|
|
116
|
+
{
|
|
117
|
+
items: [
|
|
118
|
+
{
|
|
119
|
+
text: 'Set status',
|
|
120
|
+
href: 'https://gitlab.com',
|
|
121
|
+
icon: 'status_success',
|
|
122
|
+
extraAttrs: {
|
|
123
|
+
target: '_blank',
|
|
124
|
+
rel: 'nofollow',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
text: 'Edit profile',
|
|
129
|
+
href: '#',
|
|
130
|
+
extraAttrs: {
|
|
131
|
+
target: '_blank',
|
|
132
|
+
rel: 'nofollow',
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
text: 'Preferences',
|
|
137
|
+
href: '#',
|
|
138
|
+
extraAttrs: {
|
|
139
|
+
target: '_blank',
|
|
140
|
+
rel: 'nofollow',
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
items: [
|
|
147
|
+
{
|
|
148
|
+
text: 'Sign out',
|
|
149
|
+
action: () => {
|
|
150
|
+
// eslint-disable-next-line no-alert
|
|
151
|
+
window.confirm('Are you sure?');
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const itemValidator = ({ text, href, action }) =>
|
|
2
|
+
Boolean(text?.length && (href?.length || typeof action === 'function'));
|
|
3
|
+
|
|
4
|
+
const isItem = (item) => Boolean(item) && itemValidator(item);
|
|
5
|
+
|
|
6
|
+
const isGroup = (group) =>
|
|
7
|
+
Boolean(group) &&
|
|
8
|
+
Array.isArray(group.items) &&
|
|
9
|
+
Boolean(group.items.length) &&
|
|
10
|
+
group.items.every(isItem);
|
|
11
|
+
|
|
12
|
+
const itemsValidator = (items) => items.every(isItem) || items.every(isGroup);
|
|
13
|
+
|
|
14
|
+
const isAllItems = (items) => items.every(isItem);
|
|
15
|
+
|
|
16
|
+
const isAllGroups = (items) => items.every(isGroup);
|
|
17
|
+
|
|
18
|
+
export { itemsValidator, isItem, isGroup, isAllItems, isAllGroups };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { itemsValidator, isItem, isGroup } from './utils';
|
|
2
|
+
import { mockItems, mockGroups } from './mock_data';
|
|
3
|
+
|
|
4
|
+
describe('isItem', () => {
|
|
5
|
+
it.each([null, undefined, {}, { text: null }, { text: 'group', items: [] }])(
|
|
6
|
+
'isItem(%p) === false',
|
|
7
|
+
(notAnItem) => {
|
|
8
|
+
expect(isItem(notAnItem)).toBe(false);
|
|
9
|
+
}
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
it.each([
|
|
13
|
+
{ text: 'Action', href: 'gitlab.com' },
|
|
14
|
+
{
|
|
15
|
+
text: 'Action',
|
|
16
|
+
action: () => {},
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
text: 'Action',
|
|
20
|
+
href: 'gitlab.com',
|
|
21
|
+
action: () => {},
|
|
22
|
+
},
|
|
23
|
+
])('isItem(%p) === true', (item) => {
|
|
24
|
+
expect(isItem(item)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('isGroup', () => {
|
|
29
|
+
it.each([null, undefined, {}, { name: null }, { name: 'group', items: [] }])(
|
|
30
|
+
'isGroup(%p) === false',
|
|
31
|
+
(notAGroup) => {
|
|
32
|
+
expect(isGroup(notAGroup)).toBe(false);
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
it.each([
|
|
37
|
+
{
|
|
38
|
+
name: 'group',
|
|
39
|
+
items: [
|
|
40
|
+
{ text: 'Action', href: 'gitlab.com' },
|
|
41
|
+
{
|
|
42
|
+
text: 'Action',
|
|
43
|
+
action: () => {},
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
items: [
|
|
49
|
+
{ text: 'Action', href: 'gitlab.com' },
|
|
50
|
+
{
|
|
51
|
+
text: 'Action',
|
|
52
|
+
action: () => {},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
])('isGroup(%p) === true', (group) => {
|
|
57
|
+
expect(isGroup(group)).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('itemsValidator', () => {
|
|
62
|
+
it.each`
|
|
63
|
+
description | value | expected
|
|
64
|
+
${'valid flat items'} | ${mockItems} | ${true}
|
|
65
|
+
${'valid grouped items'} | ${mockGroups} | ${true}
|
|
66
|
+
${'empty list'} | ${[]} | ${true}
|
|
67
|
+
${'invalid items'} | ${[{ foo: true }]} | ${false}
|
|
68
|
+
${'group with invalid items'} | ${[{ name: 'foo', items: [{ foo: true }] }]} | ${false}
|
|
69
|
+
${'sibling groups and items'} | ${[...mockItems, ...mockGroups]} | ${false}
|
|
70
|
+
`('returns $expected given $description', ({ value, expected }) => {
|
|
71
|
+
expect(itemsValidator(value)).toBe(expected);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -105,7 +105,7 @@ export default {
|
|
|
105
105
|
type: String,
|
|
106
106
|
required: false,
|
|
107
107
|
default: buttonCategoryOptions.primary,
|
|
108
|
-
validator: (value) =>
|
|
108
|
+
validator: (value) => value in buttonCategoryOptions,
|
|
109
109
|
},
|
|
110
110
|
/**
|
|
111
111
|
* Styling option - dropdown's toggle variant
|
|
@@ -114,7 +114,7 @@ export default {
|
|
|
114
114
|
type: String,
|
|
115
115
|
required: false,
|
|
116
116
|
default: dropdownVariantOptions.default,
|
|
117
|
-
validator: (value) =>
|
|
117
|
+
validator: (value) => value in dropdownVariantOptions,
|
|
118
118
|
},
|
|
119
119
|
/**
|
|
120
120
|
* The size of the dropdown toggle
|
|
@@ -123,7 +123,7 @@ export default {
|
|
|
123
123
|
type: String,
|
|
124
124
|
required: false,
|
|
125
125
|
default: 'medium',
|
|
126
|
-
validator: (value) =>
|
|
126
|
+
validator: (value) => value in buttonSizeOptions,
|
|
127
127
|
},
|
|
128
128
|
/**
|
|
129
129
|
* Icon name that will be rendered in the toggle button
|
|
@@ -610,7 +610,7 @@ export default {
|
|
|
610
610
|
<component
|
|
611
611
|
:is="listboxTag"
|
|
612
612
|
v-if="showList"
|
|
613
|
-
id="
|
|
613
|
+
:id="listboxId"
|
|
614
614
|
ref="list"
|
|
615
615
|
:aria-labelledby="listAriaLabelledBy || headerId || toggleId"
|
|
616
616
|
role="listbox"
|
|
@@ -16,7 +16,7 @@ export default {
|
|
|
16
16
|
|
|
17
17
|
<template>
|
|
18
18
|
<ul role="group" :aria-labelledby="nameId" class="gl-mb-0 gl-pl-0 gl-list-style-none">
|
|
19
|
-
<li :id="nameId" role="presentation" class="gl-pl-5! gl-py-2! gl-font-
|
|
19
|
+
<li :id="nameId" role="presentation" class="gl-pl-5! gl-py-2! gl-font-sm gl-font-weight-bold">
|
|
20
20
|
<slot name="group-label">{{ name }}</slot>
|
|
21
21
|
</li>
|
|
22
22
|
<slot></slot>
|