@gitlab/ui 39.3.1 → 39.5.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 +26 -0
- package/dist/components/base/alert/alert.js +1 -1
- package/dist/components/base/new_dropdowns/base_dropdown/base_dropdown.js +240 -0
- package/dist/components/base/new_dropdowns/constants.js +20 -0
- package/dist/components/base/new_dropdowns/listbox/listbox.js +381 -0
- package/dist/components/base/new_dropdowns/listbox/listbox_item.js +77 -0
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/index.js +2 -0
- package/dist/utility_classes.css +1 -1
- package/dist/utility_classes.css.map +1 -1
- package/dist/utils/utils.js +24 -1
- package/package.json +6 -5
- package/scss_to_js/scss_variables.js +1 -0
- package/scss_to_js/scss_variables.json +5 -0
- package/src/components/base/alert/alert.spec.js +3 -1
- package/src/components/base/alert/alert.vue +1 -1
- package/src/components/base/avatar_labeled/avatar_labeled.stories.js +2 -1
- package/src/components/base/avatar_link/avatar_link.stories.js +2 -3
- package/src/components/base/breadcrumb/breadcrumb.md +1 -1
- package/src/components/base/breadcrumb/breadcrumb.stories.js +2 -1
- package/src/components/base/broadcast_message/broadcast_message.scss +1 -1
- package/src/components/base/button/button.scss +1 -1
- package/src/components/base/dropdown/dropdown.scss +10 -3
- package/src/components/base/dropdown/dropdown_item.scss +1 -0
- package/src/components/base/link/link.stories.js +9 -7
- package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.spec.js +171 -0
- package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.vue +221 -0
- package/src/components/base/new_dropdowns/constants.js +22 -0
- package/src/components/base/new_dropdowns/listbox/listbox.md +71 -0
- package/src/components/base/new_dropdowns/listbox/listbox.spec.js +236 -0
- package/src/components/base/new_dropdowns/listbox/listbox.stories.js +276 -0
- package/src/components/base/new_dropdowns/listbox/listbox.vue +348 -0
- package/src/components/base/new_dropdowns/listbox/listbox_item.spec.js +104 -0
- package/src/components/base/new_dropdowns/listbox/listbox_item.vue +59 -0
- package/src/components/utilities/friendly_wrap/friendly_wrap.stories.js +10 -11
- package/src/components/utilities/sprintf/sprintf.stories.js +11 -9
- package/src/index.js +4 -0
- package/src/scss/utilities.scss +18 -0
- package/src/scss/utility-mixins/color.scss +4 -0
- package/src/scss/utility-mixins/composite.scss +20 -0
- package/src/scss/utility-mixins/index.scss +1 -0
- package/src/scss/variables.scss +1 -0
- package/src/utils/data_utils.js +2 -21
- package/src/utils/utils.js +18 -0
- package/src/utils/utils.spec.js +41 -1
package/dist/utils/utils.js
CHANGED
|
@@ -119,5 +119,28 @@ function logWarning() {
|
|
|
119
119
|
console.warn(message); // eslint-disable-line no-console
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Stop default event handling and propagation
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
function stopEvent(event) {
|
|
127
|
+
let {
|
|
128
|
+
preventDefault = true,
|
|
129
|
+
stopPropagation = true,
|
|
130
|
+
stopImmediatePropagation = false
|
|
131
|
+
} = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
132
|
+
|
|
133
|
+
if (preventDefault) {
|
|
134
|
+
event.preventDefault();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (stopPropagation) {
|
|
138
|
+
event.stopPropagation();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (stopImmediatePropagation) {
|
|
142
|
+
event.stopImmediatePropagation();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
122
145
|
|
|
123
|
-
export { colorFromBackground, debounceByAnimationFrame, focusFirstFocusableElement, hexToRgba, isDev, isElementFocusable, logWarning, rgbFromHex, rgbFromString, throttle, uid };
|
|
146
|
+
export { colorFromBackground, debounceByAnimationFrame, focusFirstFocusableElement, hexToRgba, isDev, isElementFocusable, logWarning, rgbFromHex, rgbFromString, stopEvent, throttle, uid };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitlab/ui",
|
|
3
|
-
"version": "39.
|
|
3
|
+
"version": "39.5.0",
|
|
4
4
|
"description": "GitLab UI Components",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"generate:component": "plop"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
+
"@popperjs/core": "^2.11.2",
|
|
58
59
|
"bootstrap-vue": "2.20.1",
|
|
59
60
|
"dompurify": "^2.3.6",
|
|
60
61
|
"echarts": "^5.2.1",
|
|
@@ -82,7 +83,7 @@
|
|
|
82
83
|
"@babel/preset-env": "^7.10.2",
|
|
83
84
|
"@gitlab/eslint-plugin": "12.0.1",
|
|
84
85
|
"@gitlab/stylelint-config": "4.0.0",
|
|
85
|
-
"@gitlab/svgs": "2.
|
|
86
|
+
"@gitlab/svgs": "2.11.0",
|
|
86
87
|
"@rollup/plugin-commonjs": "^11.1.0",
|
|
87
88
|
"@rollup/plugin-node-resolve": "^7.1.3",
|
|
88
89
|
"@rollup/plugin-replace": "^2.3.2",
|
|
@@ -122,10 +123,10 @@
|
|
|
122
123
|
"npm-run-all": "^4.1.5",
|
|
123
124
|
"pikaday": "^1.8.0",
|
|
124
125
|
"plop": "^2.5.4",
|
|
125
|
-
"postcss": "8.4.
|
|
126
|
+
"postcss": "8.4.12",
|
|
126
127
|
"postcss-loader": "^3.0.0",
|
|
127
|
-
"postcss-scss": "
|
|
128
|
-
"prettier": "2.2
|
|
128
|
+
"postcss-scss": "4.0.4",
|
|
129
|
+
"prettier": "2.6.2",
|
|
129
130
|
"puppeteer": "11.0.0",
|
|
130
131
|
"raw-loader": "^0.5.1",
|
|
131
132
|
"rollup": "^2.53.1",
|
|
@@ -27,6 +27,7 @@ export const limitedLayoutWidth = '990px'
|
|
|
27
27
|
export const black = '#000'
|
|
28
28
|
export const blackNormal = '#333'
|
|
29
29
|
export const white = '#fff'
|
|
30
|
+
export const whiteContrast = '#fff'
|
|
30
31
|
export const whiteNormal = '#f0f0f0'
|
|
31
32
|
export const whiteDark = '#eaeaea'
|
|
32
33
|
export const whiteTransparent = 'rgba(255, 255, 255, 0.8)'
|
|
@@ -71,13 +71,15 @@ describe('Alert component', () => {
|
|
|
71
71
|
expect(findDismissButton().exists()).toBe(false);
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
it('renders the provided title', () => {
|
|
74
|
+
it('renders the provided title with heading level 2', () => {
|
|
75
75
|
const title = 'foo';
|
|
76
76
|
createComponent({ propsData: { title } });
|
|
77
77
|
|
|
78
78
|
const titleWrapper = findTitle();
|
|
79
79
|
expect(titleWrapper.exists()).toBe(true);
|
|
80
80
|
expect(titleWrapper.text()).toContain(title);
|
|
81
|
+
// the title needs to be in a level 2 heading for accessibility reasons
|
|
82
|
+
expect(titleWrapper.element.tagName).toEqual('H2');
|
|
81
83
|
});
|
|
82
84
|
|
|
83
85
|
describe('given primaryButtonText', () => {
|
|
@@ -184,7 +184,7 @@ export default {
|
|
|
184
184
|
/>
|
|
185
185
|
|
|
186
186
|
<div class="gl-alert-content" role="alert">
|
|
187
|
-
<
|
|
187
|
+
<h2 v-if="title" class="gl-alert-title">{{ title }}</h2>
|
|
188
188
|
|
|
189
189
|
<div class="gl-alert-body">
|
|
190
190
|
<!-- @slot The alert message to display. -->
|
|
@@ -2,6 +2,7 @@ import Vue from 'vue';
|
|
|
2
2
|
import { GlAvatarLabeled, GlBadge } from '../../../index';
|
|
3
3
|
import { GlTooltipDirective } from '../../../directives/tooltip';
|
|
4
4
|
import { avatarSizeOptions, avatarShapeOptions, tooltipPlacements } from '../../../utils/constants';
|
|
5
|
+
import avatarPath from '../../../../static/img/avatar.png';
|
|
5
6
|
import readme from './avatar_labeled.md';
|
|
6
7
|
|
|
7
8
|
Vue.directive('gl-tooltip', GlTooltipDirective);
|
|
@@ -13,7 +14,7 @@ const generateProps = ({
|
|
|
13
14
|
subLabel = '@gitlab',
|
|
14
15
|
size = 32,
|
|
15
16
|
shape = 'circle',
|
|
16
|
-
src =
|
|
17
|
+
src = avatarPath,
|
|
17
18
|
} = {}) => ({
|
|
18
19
|
label,
|
|
19
20
|
subLabel,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GlAvatarLink, GlAvatar, GlAvatarLabeled } from '../../../index';
|
|
2
2
|
import { avatarSizeOptions, avatarShapeOptions } from '../../../utils/constants';
|
|
3
|
+
import avatarPath from '../../../../static/img/avatar.png';
|
|
3
4
|
import readme from './avatar_link.md';
|
|
4
5
|
|
|
5
6
|
const components = { GlAvatarLink, GlAvatar, GlAvatarLabeled };
|
|
@@ -19,9 +20,7 @@ const generateLabelsProps = ({ label = 'GitLab User', subLabel = '@gitlab' } = {
|
|
|
19
20
|
subLabel,
|
|
20
21
|
});
|
|
21
22
|
|
|
22
|
-
const generateImageProps = ({
|
|
23
|
-
src = 'https://assets.gitlab-static.net/uploads/-/system/project/avatar/278964/logo-extra-whitespace.png?width=64',
|
|
24
|
-
} = {}) => ({
|
|
23
|
+
const generateImageProps = ({ src = avatarPath } = {}) => ({
|
|
25
24
|
src,
|
|
26
25
|
});
|
|
27
26
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GlBreadcrumb } from '../../../index';
|
|
2
|
+
import avatarPath from '../../../../static/img/avatar.png';
|
|
2
3
|
import readme from './breadcrumb.md';
|
|
3
4
|
|
|
4
5
|
const template = `
|
|
@@ -7,7 +8,7 @@ const template = `
|
|
|
7
8
|
>
|
|
8
9
|
<template #avatar>
|
|
9
10
|
<img alt=""
|
|
10
|
-
class="gl-breadcrumb-avatar-tile" src="
|
|
11
|
+
class="gl-breadcrumb-avatar-tile" src="${avatarPath}"
|
|
11
12
|
width="16"
|
|
12
13
|
height="16" />
|
|
13
14
|
</template>
|
|
@@ -128,8 +128,14 @@
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
.gl-dropdown-toggle
|
|
132
|
-
|
|
131
|
+
.gl-dropdown-toggle {
|
|
132
|
+
&.btn-block {
|
|
133
|
+
@include gl-justify-content-space-between;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.gl-button-text {
|
|
137
|
+
@include gl-display-inline-flex;
|
|
138
|
+
}
|
|
133
139
|
}
|
|
134
140
|
|
|
135
141
|
.gl-new-dropdown-button-text {
|
|
@@ -173,7 +179,8 @@
|
|
|
173
179
|
}
|
|
174
180
|
|
|
175
181
|
.dropdown-icon-only {
|
|
176
|
-
.dropdown-icon
|
|
182
|
+
.dropdown-icon,
|
|
183
|
+
.gl-button-icon.gl-button-icon {
|
|
177
184
|
@include gl-mr-0;
|
|
178
185
|
}
|
|
179
186
|
|
|
@@ -7,13 +7,15 @@ const generateProps = ({ href = '#', target = null } = {}) => ({
|
|
|
7
7
|
target,
|
|
8
8
|
});
|
|
9
9
|
|
|
10
|
-
const makeStory =
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
const makeStory =
|
|
11
|
+
(options) =>
|
|
12
|
+
(args, { argTypes }) => ({
|
|
13
|
+
components: {
|
|
14
|
+
GlLink,
|
|
15
|
+
},
|
|
16
|
+
props: Object.keys(argTypes),
|
|
17
|
+
...options,
|
|
18
|
+
});
|
|
17
19
|
|
|
18
20
|
export const DefaultLink = makeStory({
|
|
19
21
|
components: { GlLink },
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import { nextTick } from 'vue';
|
|
3
|
+
import { GL_DROPDOWN_HIDDEN, GL_DROPDOWN_SHOWN, POPPER_CONFIG } from '../constants';
|
|
4
|
+
import GlBaseDropdown from './base_dropdown.vue';
|
|
5
|
+
|
|
6
|
+
const destroyPopper = jest.fn();
|
|
7
|
+
const updatePopper = jest.fn();
|
|
8
|
+
const mockCreatePopper = jest.fn().mockImplementation(() => ({
|
|
9
|
+
destroy: destroyPopper,
|
|
10
|
+
update: updatePopper,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
jest.mock('@popperjs/core', () => ({
|
|
14
|
+
createPopper: (...args) => mockCreatePopper(...args),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
const DEFAULT_BTN_TOGGLE_CLASSES = [
|
|
18
|
+
'btn',
|
|
19
|
+
'btn-default',
|
|
20
|
+
'btn-md',
|
|
21
|
+
'gl-button',
|
|
22
|
+
'dropdown-toggle',
|
|
23
|
+
'gl-dropdown-toggle',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
describe('base dropdown', () => {
|
|
27
|
+
let wrapper;
|
|
28
|
+
|
|
29
|
+
const buildWrapper = (propsData, slots = {}) => {
|
|
30
|
+
wrapper = mount(GlBaseDropdown, {
|
|
31
|
+
propsData: {
|
|
32
|
+
toggleId: 'dropdown-toggle-btn-1',
|
|
33
|
+
...propsData,
|
|
34
|
+
},
|
|
35
|
+
slots,
|
|
36
|
+
attachTo: document.body,
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
jest.clearAllMocks();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const findDropdownToggle = () => wrapper.find('.btn.gl-dropdown-toggle');
|
|
45
|
+
const findDropdownMenu = () => wrapper.find('.dropdown-menu');
|
|
46
|
+
|
|
47
|
+
describe('popper.js instance', () => {
|
|
48
|
+
it('should initialize popper.js instance with toggle and menu elements and config for left-aligned menu', async () => {
|
|
49
|
+
await buildWrapper();
|
|
50
|
+
expect(mockCreatePopper).toHaveBeenCalledWith(
|
|
51
|
+
findDropdownToggle().element,
|
|
52
|
+
findDropdownMenu().element,
|
|
53
|
+
{ ...POPPER_CONFIG, placement: 'bottom-start' }
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should initialize popper.js instance with toggle and menu elements and config for right-aligned menu', async () => {
|
|
58
|
+
await buildWrapper({ right: true });
|
|
59
|
+
expect(mockCreatePopper).toHaveBeenCalledWith(
|
|
60
|
+
findDropdownToggle().element,
|
|
61
|
+
findDropdownMenu().element,
|
|
62
|
+
{ ...POPPER_CONFIG, placement: 'bottom-end' }
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should update popper instance when component is updated', async () => {
|
|
67
|
+
await buildWrapper();
|
|
68
|
+
await findDropdownToggle().trigger('click');
|
|
69
|
+
await wrapper.setProps({ category: 'tertiary' });
|
|
70
|
+
expect(updatePopper).toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should destroy popper instance when component is destroyed', async () => {
|
|
74
|
+
await buildWrapper();
|
|
75
|
+
wrapper.destroy();
|
|
76
|
+
expect(destroyPopper).toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('renders content to the default slot', () => {
|
|
81
|
+
const defaultContent = 'Some content here';
|
|
82
|
+
const slots = { default: defaultContent };
|
|
83
|
+
|
|
84
|
+
it('renders the content', () => {
|
|
85
|
+
buildWrapper({}, slots);
|
|
86
|
+
expect(wrapper.find('.gl-new-dropdown-inner').html()).toContain(defaultContent);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe.each`
|
|
91
|
+
props | toggleClasses
|
|
92
|
+
${{}} | ${[]}
|
|
93
|
+
${{ toggleText: 'toggleText' }} | ${[]}
|
|
94
|
+
${{ toggleText: 'toggleText', icon: 'close' }} | ${['dropdown-icon-text']}
|
|
95
|
+
${{ icon: 'close' }} | ${['dropdown-icon-only']}
|
|
96
|
+
${{ icon: 'close', toggleText: 'toggleText', textSrOnly: true }} | ${['dropdown-icon-only']}
|
|
97
|
+
${{ icon: 'close', textSrOnly: true }} | ${['dropdown-icon-only']}
|
|
98
|
+
${{ toggleText: 'toggleText', noCaret: true }} | ${['dropdown-toggle-no-caret']}
|
|
99
|
+
`('dropdown with props $props', ({ props, toggleClasses }) => {
|
|
100
|
+
beforeEach(async () => {
|
|
101
|
+
buildWrapper(props);
|
|
102
|
+
|
|
103
|
+
await nextTick();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it(`sets toggle button classes to '${toggleClasses}'`, () => {
|
|
107
|
+
const classes = findDropdownToggle().classes().sort();
|
|
108
|
+
|
|
109
|
+
expect(classes).toEqual([...DEFAULT_BTN_TOGGLE_CLASSES, ...toggleClasses].sort());
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe.each`
|
|
114
|
+
toggleClass | expectedClasses | type
|
|
115
|
+
${'my-class'} | ${[...DEFAULT_BTN_TOGGLE_CLASSES, 'my-class']} | ${'string'}
|
|
116
|
+
${{ 'my-class': true }} | ${[...DEFAULT_BTN_TOGGLE_CLASSES, 'my-class']} | ${'object'}
|
|
117
|
+
${['cls-1', 'cls-2']} | ${[...DEFAULT_BTN_TOGGLE_CLASSES, 'cls-1', 'cls-2']} | ${'array'}
|
|
118
|
+
${null} | ${[...DEFAULT_BTN_TOGGLE_CLASSES]} | ${'null'}
|
|
119
|
+
`('with toggle classes', ({ toggleClass, expectedClasses, type }) => {
|
|
120
|
+
beforeEach(async () => {
|
|
121
|
+
buildWrapper({ toggleClass });
|
|
122
|
+
|
|
123
|
+
await nextTick();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it(`class is inherited from toggle class of type ${type}`, () => {
|
|
127
|
+
expect(findDropdownToggle().classes().sort()).toEqual(
|
|
128
|
+
expect.arrayContaining(expectedClasses.sort())
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('toggle visibility', () => {
|
|
134
|
+
beforeEach(() => {
|
|
135
|
+
buildWrapper();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should toggle menu visibility on toggle button click ', async () => {
|
|
139
|
+
const toggle = findDropdownToggle();
|
|
140
|
+
const menu = findDropdownMenu();
|
|
141
|
+
|
|
142
|
+
// open menu clicking toggle btn
|
|
143
|
+
await toggle.trigger('click');
|
|
144
|
+
expect(menu.classes('show')).toBe(true);
|
|
145
|
+
expect(toggle.attributes('aria-expanded')).toBe('true');
|
|
146
|
+
expect(wrapper.emitted(GL_DROPDOWN_SHOWN).length).toBe(1);
|
|
147
|
+
|
|
148
|
+
// close menu clicking toggle btn again
|
|
149
|
+
await toggle.trigger('click');
|
|
150
|
+
expect(menu.classes('show')).toBe(false);
|
|
151
|
+
expect(toggle.attributes('aria-expanded')).toBeUndefined();
|
|
152
|
+
expect(wrapper.emitted(GL_DROPDOWN_HIDDEN).length).toBe(1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should close the menu when Escape is pressed inside menu and focus toggle', async () => {
|
|
156
|
+
const toggle = findDropdownToggle();
|
|
157
|
+
const menu = findDropdownMenu();
|
|
158
|
+
|
|
159
|
+
// open menu clicking toggle btn
|
|
160
|
+
await toggle.trigger('click');
|
|
161
|
+
expect(menu.classes('show')).toBe(true);
|
|
162
|
+
|
|
163
|
+
// close menu pressing ESC on it
|
|
164
|
+
await menu.trigger('keydown.esc');
|
|
165
|
+
expect(menu.classes('show')).toBe(false);
|
|
166
|
+
expect(toggle.attributes('aria-expanded')).toBeUndefined();
|
|
167
|
+
expect(wrapper.emitted(GL_DROPDOWN_HIDDEN).length).toBe(1);
|
|
168
|
+
expect(toggle.element).toHaveFocus();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { createPopper } from '@popperjs/core';
|
|
3
|
+
import {
|
|
4
|
+
buttonCategoryOptions,
|
|
5
|
+
buttonSizeOptions,
|
|
6
|
+
dropdownVariantOptions,
|
|
7
|
+
} from '../../../../utils/constants';
|
|
8
|
+
import { POPPER_CONFIG, GL_DROPDOWN_SHOWN, GL_DROPDOWN_HIDDEN } from '../constants';
|
|
9
|
+
|
|
10
|
+
import GlButton from '../../button/button.vue';
|
|
11
|
+
import GlIcon from '../../icon/icon.vue';
|
|
12
|
+
import { OutsideDirective } from '../../../../directives/outside/outside';
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
components: {
|
|
16
|
+
GlButton,
|
|
17
|
+
GlIcon,
|
|
18
|
+
},
|
|
19
|
+
directives: { Outside: OutsideDirective },
|
|
20
|
+
props: {
|
|
21
|
+
toggleText: {
|
|
22
|
+
type: String,
|
|
23
|
+
required: false,
|
|
24
|
+
default: '',
|
|
25
|
+
},
|
|
26
|
+
textSrOnly: {
|
|
27
|
+
type: Boolean,
|
|
28
|
+
required: false,
|
|
29
|
+
default: false,
|
|
30
|
+
},
|
|
31
|
+
category: {
|
|
32
|
+
type: String,
|
|
33
|
+
required: false,
|
|
34
|
+
default: buttonCategoryOptions.primary,
|
|
35
|
+
validator: (value) => Object.keys(buttonCategoryOptions).includes(value),
|
|
36
|
+
},
|
|
37
|
+
variant: {
|
|
38
|
+
type: String,
|
|
39
|
+
required: false,
|
|
40
|
+
default: dropdownVariantOptions.default,
|
|
41
|
+
validator: (value) => Object.keys(dropdownVariantOptions).includes(value),
|
|
42
|
+
},
|
|
43
|
+
size: {
|
|
44
|
+
type: String,
|
|
45
|
+
required: false,
|
|
46
|
+
default: buttonSizeOptions.medium,
|
|
47
|
+
validator: (value) => Object.keys(buttonSizeOptions).includes(value),
|
|
48
|
+
},
|
|
49
|
+
icon: {
|
|
50
|
+
type: String,
|
|
51
|
+
required: false,
|
|
52
|
+
default: '',
|
|
53
|
+
},
|
|
54
|
+
disabled: {
|
|
55
|
+
type: Boolean,
|
|
56
|
+
required: false,
|
|
57
|
+
default: false,
|
|
58
|
+
},
|
|
59
|
+
loading: {
|
|
60
|
+
type: Boolean,
|
|
61
|
+
required: false,
|
|
62
|
+
default: false,
|
|
63
|
+
},
|
|
64
|
+
toggleClass: {
|
|
65
|
+
type: [String, Array, Object],
|
|
66
|
+
required: false,
|
|
67
|
+
default: null,
|
|
68
|
+
},
|
|
69
|
+
noCaret: {
|
|
70
|
+
type: Boolean,
|
|
71
|
+
required: false,
|
|
72
|
+
default: false,
|
|
73
|
+
},
|
|
74
|
+
/**
|
|
75
|
+
* Right align dropdown menu with respect to the toggle button
|
|
76
|
+
*/
|
|
77
|
+
right: {
|
|
78
|
+
type: Boolean,
|
|
79
|
+
required: false,
|
|
80
|
+
default: false,
|
|
81
|
+
},
|
|
82
|
+
// ARIA props
|
|
83
|
+
ariaHaspopup: {
|
|
84
|
+
type: [String, Boolean],
|
|
85
|
+
required: false,
|
|
86
|
+
default: false,
|
|
87
|
+
validator: (value) => {
|
|
88
|
+
return ['menu', 'listbox', 'tree', 'grid', 'dialog', true, false].includes(value);
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
/**
|
|
92
|
+
* Id that will be referenced by `aria-labelledby` attribute of the dropdown content`
|
|
93
|
+
*/
|
|
94
|
+
toggleId: {
|
|
95
|
+
type: String,
|
|
96
|
+
required: true,
|
|
97
|
+
},
|
|
98
|
+
/**
|
|
99
|
+
* The `aria-labelledby` attribute value for the toggle `button`
|
|
100
|
+
*/
|
|
101
|
+
ariaLabelledby: {
|
|
102
|
+
type: String,
|
|
103
|
+
required: false,
|
|
104
|
+
default: null,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
data() {
|
|
108
|
+
return {
|
|
109
|
+
visible: false,
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
computed: {
|
|
113
|
+
isIconOnly() {
|
|
114
|
+
return Boolean(this.icon && (!this.toggleText?.length || this.textSrOnly));
|
|
115
|
+
},
|
|
116
|
+
isIconWithText() {
|
|
117
|
+
return Boolean(this.icon && this.toggleText?.length && !this.textSrOnly);
|
|
118
|
+
},
|
|
119
|
+
toggleButtonClasses() {
|
|
120
|
+
return [
|
|
121
|
+
this.toggleClass,
|
|
122
|
+
{
|
|
123
|
+
'gl-dropdown-toggle': true,
|
|
124
|
+
'dropdown-toggle': true,
|
|
125
|
+
'dropdown-icon-only': this.isIconOnly,
|
|
126
|
+
'dropdown-icon-text': this.isIconWithText,
|
|
127
|
+
'dropdown-toggle-no-caret': this.noCaret,
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
},
|
|
131
|
+
toggleLabelledBy() {
|
|
132
|
+
return this.ariaLabelledby ? `${this.ariaLabelledby} ${this.toggleId}` : this.toggleId;
|
|
133
|
+
},
|
|
134
|
+
popperConfig() {
|
|
135
|
+
return {
|
|
136
|
+
placement: this.right ? 'bottom-end' : 'bottom-start',
|
|
137
|
+
...POPPER_CONFIG,
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
updated() {
|
|
142
|
+
if (this.visible) {
|
|
143
|
+
this.popper?.update();
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
mounted() {
|
|
147
|
+
this.$nextTick(() => {
|
|
148
|
+
this.popper = createPopper(this.$refs.toggle.$el, this.$refs.content, this.popperConfig);
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
beforeDestroy() {
|
|
152
|
+
this.popper.destroy();
|
|
153
|
+
},
|
|
154
|
+
methods: {
|
|
155
|
+
toggle() {
|
|
156
|
+
this.visible = !this.visible;
|
|
157
|
+
|
|
158
|
+
if (this.visible) {
|
|
159
|
+
this.popper.update();
|
|
160
|
+
this.$emit(GL_DROPDOWN_SHOWN);
|
|
161
|
+
} else {
|
|
162
|
+
this.$emit(GL_DROPDOWN_HIDDEN);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
close() {
|
|
166
|
+
if (!this.visible) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
this.toggle();
|
|
170
|
+
},
|
|
171
|
+
closeAndFocus() {
|
|
172
|
+
if (!this.visible) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.toggle();
|
|
176
|
+
this.focusToggle();
|
|
177
|
+
},
|
|
178
|
+
focusToggle() {
|
|
179
|
+
this.$refs.toggle.$el.focus();
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
</script>
|
|
184
|
+
|
|
185
|
+
<template>
|
|
186
|
+
<div v-outside="close" class="gl-new-dropdown dropdown btn-group">
|
|
187
|
+
<gl-button
|
|
188
|
+
:id="toggleId"
|
|
189
|
+
ref="toggle"
|
|
190
|
+
data-testid="base-dropdown-toggle"
|
|
191
|
+
:icon="icon"
|
|
192
|
+
:category="category"
|
|
193
|
+
:variant="variant"
|
|
194
|
+
:size="size"
|
|
195
|
+
:disabled="disabled"
|
|
196
|
+
:loading="loading"
|
|
197
|
+
:class="toggleButtonClasses"
|
|
198
|
+
:aria-haspopup="ariaHaspopup"
|
|
199
|
+
:aria-expanded="visible"
|
|
200
|
+
:aria-labelledby="toggleLabelledBy"
|
|
201
|
+
@click="toggle"
|
|
202
|
+
>
|
|
203
|
+
<span class="gl-new-dropdown-button-text" :class="{ 'gl-sr-only': textSrOnly }">
|
|
204
|
+
{{ toggleText }}
|
|
205
|
+
</span>
|
|
206
|
+
<gl-icon v-if="!noCaret" class="gl-button-icon dropdown-chevron" name="chevron-down" />
|
|
207
|
+
</gl-button>
|
|
208
|
+
|
|
209
|
+
<div
|
|
210
|
+
ref="content"
|
|
211
|
+
data-testid="base-dropdown-menu"
|
|
212
|
+
class="dropdown-menu"
|
|
213
|
+
:class="{ show: visible }"
|
|
214
|
+
@keydown.esc.stop.prevent="closeAndFocus"
|
|
215
|
+
>
|
|
216
|
+
<div class="gl-new-dropdown-inner">
|
|
217
|
+
<slot></slot>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</template>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const POPPER_CONFIG = {
|
|
2
|
+
modifiers: [
|
|
3
|
+
{
|
|
4
|
+
name: 'offset',
|
|
5
|
+
options: {
|
|
6
|
+
offset: [0, 4],
|
|
7
|
+
},
|
|
8
|
+
},
|
|
9
|
+
],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// base dropdown events
|
|
13
|
+
export const GL_DROPDOWN_SHOWN = 'shown';
|
|
14
|
+
export const GL_DROPDOWN_HIDDEN = 'hidden';
|
|
15
|
+
|
|
16
|
+
// KEY Codes
|
|
17
|
+
export const HOME = 'Home';
|
|
18
|
+
export const END = 'End';
|
|
19
|
+
export const ARROW_UP = 'ArrowUp';
|
|
20
|
+
export const ARROW_DOWN = 'ArrowDown';
|
|
21
|
+
export const ENTER = 'Enter';
|
|
22
|
+
export const SPACE = 'Space';
|