@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,306 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buttonCategoryOptions,
|
|
3
|
+
buttonSizeOptions,
|
|
4
|
+
buttonVariantOptions,
|
|
5
|
+
} from '../../../../utils/constants';
|
|
6
|
+
import {
|
|
7
|
+
GlDisclosureDropdown,
|
|
8
|
+
GlBadge,
|
|
9
|
+
GlDisclosureDropdownGroup,
|
|
10
|
+
GlDisclosureDropdownItem,
|
|
11
|
+
GlToggle,
|
|
12
|
+
GlAvatar,
|
|
13
|
+
GlModal,
|
|
14
|
+
GlIcon,
|
|
15
|
+
} from '../../../../index';
|
|
16
|
+
import { makeContainer } from '../../../../utils/story_decorators/container';
|
|
17
|
+
import readme from './disclosure_dropdown.md';
|
|
18
|
+
import { mockItems, mockItemsCustomItem, mockGroups, mockProfileGroups } from './mock_data';
|
|
19
|
+
|
|
20
|
+
const defaultValue = (prop) => GlDisclosureDropdown.props[prop].default;
|
|
21
|
+
|
|
22
|
+
const generateProps = ({
|
|
23
|
+
items = mockItems,
|
|
24
|
+
category = defaultValue('category'),
|
|
25
|
+
variant = defaultValue('variant'),
|
|
26
|
+
size = defaultValue('size'),
|
|
27
|
+
disabled = defaultValue('disabled'),
|
|
28
|
+
loading = defaultValue('loading'),
|
|
29
|
+
noCaret = defaultValue('noCaret'),
|
|
30
|
+
right = defaultValue('right'),
|
|
31
|
+
toggleText,
|
|
32
|
+
textSrOnly = defaultValue('textSrOnly'),
|
|
33
|
+
icon = '',
|
|
34
|
+
toggleAriaLabelledBy,
|
|
35
|
+
listAriaLabelledBy,
|
|
36
|
+
startOpened = true,
|
|
37
|
+
} = {}) => ({
|
|
38
|
+
items,
|
|
39
|
+
category,
|
|
40
|
+
variant,
|
|
41
|
+
size,
|
|
42
|
+
disabled,
|
|
43
|
+
loading,
|
|
44
|
+
noCaret,
|
|
45
|
+
right,
|
|
46
|
+
toggleText,
|
|
47
|
+
textSrOnly,
|
|
48
|
+
icon,
|
|
49
|
+
toggleAriaLabelledBy,
|
|
50
|
+
listAriaLabelledBy,
|
|
51
|
+
startOpened,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const makeBindings = (overrides = {}) =>
|
|
55
|
+
Object.entries({
|
|
56
|
+
':items': 'items',
|
|
57
|
+
':category': 'category',
|
|
58
|
+
':variant': 'variant',
|
|
59
|
+
':size': 'size',
|
|
60
|
+
':disabled': 'disabled',
|
|
61
|
+
':loading': 'loading',
|
|
62
|
+
':no-caret': 'noCaret',
|
|
63
|
+
':right': 'right',
|
|
64
|
+
':toggle-text': 'toggleText',
|
|
65
|
+
':text-sr-only': 'textSrOnly',
|
|
66
|
+
':icon': 'icon',
|
|
67
|
+
':toggle-aria-labelled-by': 'toggleAriaLabelledBy',
|
|
68
|
+
':list-aria-labelled-by': 'listAriaLabelledBy',
|
|
69
|
+
...overrides,
|
|
70
|
+
})
|
|
71
|
+
.map(([key, value]) => `${key}="${value}"`)
|
|
72
|
+
.join('\n');
|
|
73
|
+
|
|
74
|
+
function openDisclosure(component) {
|
|
75
|
+
component.$nextTick(() => {
|
|
76
|
+
component.$refs.disclosure.open();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const template = (content, { bindingOverrides = {} } = {}) => `
|
|
81
|
+
<gl-disclosure-dropdown
|
|
82
|
+
ref="disclosure"
|
|
83
|
+
${makeBindings(bindingOverrides)}
|
|
84
|
+
>
|
|
85
|
+
${content || ''}
|
|
86
|
+
</gl-disclosure-dropdown>
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
export const Default = (args, { argTypes }) => ({
|
|
90
|
+
props: Object.keys(argTypes),
|
|
91
|
+
components: {
|
|
92
|
+
GlDisclosureDropdown,
|
|
93
|
+
},
|
|
94
|
+
mounted() {
|
|
95
|
+
if (this.startOpened) {
|
|
96
|
+
openDisclosure(this);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
template: template(),
|
|
100
|
+
});
|
|
101
|
+
Default.args = generateProps({
|
|
102
|
+
icon: 'ellipsis_v',
|
|
103
|
+
noCaret: true,
|
|
104
|
+
toggleText: 'Disclosure',
|
|
105
|
+
textSrOnly: true,
|
|
106
|
+
});
|
|
107
|
+
Default.decorators = [makeContainer({ height: '200px' })];
|
|
108
|
+
|
|
109
|
+
export const CustomListItem = (args, { argTypes }) => ({
|
|
110
|
+
props: Object.keys(argTypes),
|
|
111
|
+
components: {
|
|
112
|
+
GlDisclosureDropdown,
|
|
113
|
+
GlBadge,
|
|
114
|
+
},
|
|
115
|
+
mounted() {
|
|
116
|
+
if (this.startOpened) {
|
|
117
|
+
openDisclosure(this);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
methods: {
|
|
121
|
+
navigate() {
|
|
122
|
+
this.$refs.link.click();
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
template: template(
|
|
126
|
+
`
|
|
127
|
+
<template #list-item="{ item }">
|
|
128
|
+
<a ref="link" class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-hover-text-gray-900 gl-hover-text-decoration-none gl-text-gray-900" :href="item.href" v-bind="item.extraAttrs">
|
|
129
|
+
{{item.text}}
|
|
130
|
+
<gl-badge pill variant="info" v-if="item.count">{{item.count}}</gl-badge>
|
|
131
|
+
</a>
|
|
132
|
+
</template>
|
|
133
|
+
`,
|
|
134
|
+
{
|
|
135
|
+
bindingOverrides: {
|
|
136
|
+
'@action': 'navigate',
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
CustomListItem.args = generateProps({ items: mockItemsCustomItem, toggleText: 'Merge requests' });
|
|
143
|
+
CustomListItem.decorators = [makeContainer({ height: '200px' })];
|
|
144
|
+
|
|
145
|
+
const makeGroupedExample = (changes) => {
|
|
146
|
+
const story = (args, { argTypes }) => ({
|
|
147
|
+
props: Object.keys(argTypes),
|
|
148
|
+
components: {
|
|
149
|
+
GlBadge,
|
|
150
|
+
GlDisclosureDropdown,
|
|
151
|
+
GlDisclosureDropdownGroup,
|
|
152
|
+
GlDisclosureDropdownItem,
|
|
153
|
+
GlToggle,
|
|
154
|
+
GlAvatar,
|
|
155
|
+
GlModal,
|
|
156
|
+
GlIcon,
|
|
157
|
+
},
|
|
158
|
+
mounted() {
|
|
159
|
+
if (this.startOpened) {
|
|
160
|
+
openDisclosure(this);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
...changes,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
story.args = generateProps({ items: mockGroups });
|
|
167
|
+
story.decorators = [makeContainer({ height: '340px' })];
|
|
168
|
+
|
|
169
|
+
return story;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const Groups = makeGroupedExample({
|
|
173
|
+
template: template(''),
|
|
174
|
+
});
|
|
175
|
+
Groups.args = generateProps({
|
|
176
|
+
icon: 'plus-square',
|
|
177
|
+
items: mockGroups,
|
|
178
|
+
toggleText: 'Create new',
|
|
179
|
+
textSrOnly: true,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
export const CustomGroupsItemsAndToggle = makeGroupedExample({
|
|
183
|
+
template: template(`
|
|
184
|
+
<template #toggle>
|
|
185
|
+
<span class="gl-sr-only">
|
|
186
|
+
Orange Fox user's menu
|
|
187
|
+
</span>
|
|
188
|
+
<gl-avatar :size="32" entity-name="Orange Fox" aria-hidden="true"></gl-avatar>
|
|
189
|
+
</template>
|
|
190
|
+
|
|
191
|
+
<div role="group">
|
|
192
|
+
<gl-disclosure-dropdown-group>
|
|
193
|
+
<gl-disclosure-dropdown-item>
|
|
194
|
+
<span class="gl-display-flex gl-flex-direction-column">
|
|
195
|
+
<span class="gl-font-weight-bold gl-white-space-nowrap">Orange Fox</span>
|
|
196
|
+
<span class="gl-text-gray-400">@thefox</span>
|
|
197
|
+
</span>
|
|
198
|
+
</gl-disclosure-dropdown-item>
|
|
199
|
+
</gl-disclosure-dropdown-group>
|
|
200
|
+
<gl-disclosure-dropdown-group bordered :group="$options.groups[0]">
|
|
201
|
+
<template #list-item="{ item }">
|
|
202
|
+
<a
|
|
203
|
+
class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-hover-text-gray-900 gl-hover-text-decoration-none gl-text-gray-900"
|
|
204
|
+
:href="item.href"
|
|
205
|
+
v-bind="item.extraAttrs"
|
|
206
|
+
>
|
|
207
|
+
{{item.text}}
|
|
208
|
+
<gl-icon v-if="item.icon" :name="item.icon"/>
|
|
209
|
+
</a>
|
|
210
|
+
</template>
|
|
211
|
+
</gl-disclosure-dropdown-group>
|
|
212
|
+
<gl-disclosure-dropdown-group bordered>
|
|
213
|
+
<template #group-label>
|
|
214
|
+
<span class="gl-font-sm">Navigation redesign</span>
|
|
215
|
+
<gl-badge size="sm" variant="info">Beta</gl-badge>
|
|
216
|
+
</template>
|
|
217
|
+
<gl-disclosure-dropdown-item>
|
|
218
|
+
<gl-toggle label="New navigation" label-position="left" v-model="newNavigation"/>
|
|
219
|
+
</gl-disclosure-dropdown-item>
|
|
220
|
+
<gl-disclosure-dropdown-item @action="toggleModalVisibility(true)">
|
|
221
|
+
<a>Provide feedback</a>
|
|
222
|
+
</gl-disclosure-dropdown-item>
|
|
223
|
+
</gl-disclosure-dropdown-group>
|
|
224
|
+
<gl-disclosure-dropdown-group bordered :group="$options.groups[1]"> </gl-disclosure-dropdown-group>
|
|
225
|
+
<gl-modal :visible="feedBackModalVisible" @change="toggleModalVisibility" modal-id="feedbackModal" size="sm">
|
|
226
|
+
<textarea class="gl-w-full">Tell us what you think!</textarea>
|
|
227
|
+
</gl-modal>
|
|
228
|
+
</div>
|
|
229
|
+
`),
|
|
230
|
+
data() {
|
|
231
|
+
return {
|
|
232
|
+
newNavigation: true,
|
|
233
|
+
feedBackModalVisible: false,
|
|
234
|
+
};
|
|
235
|
+
},
|
|
236
|
+
methods: {
|
|
237
|
+
toggleModalVisibility(value) {
|
|
238
|
+
this.feedBackModalVisible = value;
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
groups: mockProfileGroups,
|
|
242
|
+
});
|
|
243
|
+
CustomGroupsItemsAndToggle.args = generateProps({
|
|
244
|
+
icon: 'plus-square',
|
|
245
|
+
toggleText: 'User profile menu',
|
|
246
|
+
textSrOnly: true,
|
|
247
|
+
items: null,
|
|
248
|
+
});
|
|
249
|
+
CustomGroupsItemsAndToggle.decorators = [makeContainer({ height: '400px' })];
|
|
250
|
+
|
|
251
|
+
export const MiscellaneousContent = (args, { argTypes }) => ({
|
|
252
|
+
props: Object.keys(argTypes),
|
|
253
|
+
components: {
|
|
254
|
+
GlDisclosureDropdown,
|
|
255
|
+
},
|
|
256
|
+
mounted() {
|
|
257
|
+
if (this.startOpened) {
|
|
258
|
+
openDisclosure(this);
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
template: template(
|
|
262
|
+
`
|
|
263
|
+
<div class="gl-p-3">A disclosure dropdown is a button that toggles a panel containing a list of items and/or links.</div>
|
|
264
|
+
`
|
|
265
|
+
),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
MiscellaneousContent.args = generateProps({
|
|
269
|
+
icon: 'doc-text',
|
|
270
|
+
toggleText: 'Miscellaneous content',
|
|
271
|
+
textSrOnly: true,
|
|
272
|
+
items: null,
|
|
273
|
+
});
|
|
274
|
+
MiscellaneousContent.decorators = [makeContainer({ height: '200px' })];
|
|
275
|
+
|
|
276
|
+
export default {
|
|
277
|
+
title: 'base/new-dropdowns/disclosure',
|
|
278
|
+
component: GlDisclosureDropdown,
|
|
279
|
+
parameters: {
|
|
280
|
+
docs: {
|
|
281
|
+
description: {
|
|
282
|
+
component: readme,
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
argTypes: {
|
|
287
|
+
category: {
|
|
288
|
+
control: {
|
|
289
|
+
type: 'select',
|
|
290
|
+
options: buttonCategoryOptions,
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
variant: {
|
|
294
|
+
control: {
|
|
295
|
+
type: 'select',
|
|
296
|
+
options: buttonVariantOptions,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
size: {
|
|
300
|
+
control: {
|
|
301
|
+
type: 'select',
|
|
302
|
+
options: Object.keys(buttonSizeOptions),
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
};
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
<!-- eslint-disable vue/multi-word-component-names -->
|
|
2
|
+
<script>
|
|
3
|
+
import { clamp, uniqueId } from 'lodash';
|
|
4
|
+
import { stopEvent } from '../../../../utils/utils';
|
|
5
|
+
import {
|
|
6
|
+
GL_DROPDOWN_SHOWN,
|
|
7
|
+
GL_DROPDOWN_HIDDEN,
|
|
8
|
+
HOME,
|
|
9
|
+
END,
|
|
10
|
+
ARROW_DOWN,
|
|
11
|
+
ARROW_UP,
|
|
12
|
+
} from '../constants';
|
|
13
|
+
import {
|
|
14
|
+
buttonCategoryOptions,
|
|
15
|
+
buttonSizeOptions,
|
|
16
|
+
dropdownVariantOptions,
|
|
17
|
+
} from '../../../../utils/constants';
|
|
18
|
+
import GlBaseDropdown from '../base_dropdown/base_dropdown.vue';
|
|
19
|
+
import GlDisclosureDropdownItem, { ITEM_CLASS } from './disclosure_dropdown_item.vue';
|
|
20
|
+
import GlDisclosureDropdownGroup from './disclosure_dropdown_group.vue';
|
|
21
|
+
import { itemsValidator, isItem, isAllItems, isAllGroups } from './utils';
|
|
22
|
+
|
|
23
|
+
export default {
|
|
24
|
+
events: {
|
|
25
|
+
GL_DROPDOWN_SHOWN,
|
|
26
|
+
GL_DROPDOWN_HIDDEN,
|
|
27
|
+
},
|
|
28
|
+
components: {
|
|
29
|
+
GlBaseDropdown,
|
|
30
|
+
GlDisclosureDropdownItem,
|
|
31
|
+
GlDisclosureDropdownGroup,
|
|
32
|
+
},
|
|
33
|
+
props: {
|
|
34
|
+
/**
|
|
35
|
+
* Items to display in the dropdown
|
|
36
|
+
*/
|
|
37
|
+
items: {
|
|
38
|
+
type: Array,
|
|
39
|
+
required: false,
|
|
40
|
+
default: () => [],
|
|
41
|
+
validator: itemsValidator,
|
|
42
|
+
},
|
|
43
|
+
/**
|
|
44
|
+
* Toggle button text
|
|
45
|
+
*/
|
|
46
|
+
toggleText: {
|
|
47
|
+
type: String,
|
|
48
|
+
required: false,
|
|
49
|
+
default: '',
|
|
50
|
+
},
|
|
51
|
+
/**
|
|
52
|
+
* Toggle text to be read by screen readers only
|
|
53
|
+
*/
|
|
54
|
+
textSrOnly: {
|
|
55
|
+
type: Boolean,
|
|
56
|
+
required: false,
|
|
57
|
+
default: false,
|
|
58
|
+
},
|
|
59
|
+
/**
|
|
60
|
+
* Styling option - dropdown's toggle category
|
|
61
|
+
*/
|
|
62
|
+
category: {
|
|
63
|
+
type: String,
|
|
64
|
+
required: false,
|
|
65
|
+
default: buttonCategoryOptions.primary,
|
|
66
|
+
validator: (value) => value in buttonCategoryOptions,
|
|
67
|
+
},
|
|
68
|
+
/**
|
|
69
|
+
* Styling option - dropdown's toggle variant
|
|
70
|
+
*/
|
|
71
|
+
variant: {
|
|
72
|
+
type: String,
|
|
73
|
+
required: false,
|
|
74
|
+
default: dropdownVariantOptions.default,
|
|
75
|
+
validator: (value) => value in dropdownVariantOptions,
|
|
76
|
+
},
|
|
77
|
+
/**
|
|
78
|
+
* The size of the dropdown toggle
|
|
79
|
+
*/
|
|
80
|
+
size: {
|
|
81
|
+
type: String,
|
|
82
|
+
required: false,
|
|
83
|
+
default: 'medium',
|
|
84
|
+
validator: (value) => value in buttonSizeOptions,
|
|
85
|
+
},
|
|
86
|
+
/**
|
|
87
|
+
* Icon name that will be rendered in the toggle button
|
|
88
|
+
*/
|
|
89
|
+
icon: {
|
|
90
|
+
type: String,
|
|
91
|
+
required: false,
|
|
92
|
+
default: '',
|
|
93
|
+
},
|
|
94
|
+
/**
|
|
95
|
+
* Set to "true" to disable the dropdown
|
|
96
|
+
*/
|
|
97
|
+
disabled: {
|
|
98
|
+
type: Boolean,
|
|
99
|
+
required: false,
|
|
100
|
+
default: false,
|
|
101
|
+
},
|
|
102
|
+
/**
|
|
103
|
+
* Set to "true" when dropdown content (items) is loading
|
|
104
|
+
* It will render a small loader in the dropdown toggle and make it disabled
|
|
105
|
+
*/
|
|
106
|
+
loading: {
|
|
107
|
+
type: Boolean,
|
|
108
|
+
required: false,
|
|
109
|
+
default: false,
|
|
110
|
+
},
|
|
111
|
+
/**
|
|
112
|
+
* Additional CSS classes to customize toggle appearance
|
|
113
|
+
*/
|
|
114
|
+
toggleClass: {
|
|
115
|
+
type: [String, Array, Object],
|
|
116
|
+
required: false,
|
|
117
|
+
default: null,
|
|
118
|
+
},
|
|
119
|
+
/**
|
|
120
|
+
* Set to "true" to hide the caret
|
|
121
|
+
*/
|
|
122
|
+
noCaret: {
|
|
123
|
+
type: Boolean,
|
|
124
|
+
required: false,
|
|
125
|
+
default: false,
|
|
126
|
+
},
|
|
127
|
+
/**
|
|
128
|
+
* Right align disclosure dropdown with respect to the toggle button
|
|
129
|
+
*/
|
|
130
|
+
right: {
|
|
131
|
+
type: Boolean,
|
|
132
|
+
required: false,
|
|
133
|
+
default: false,
|
|
134
|
+
},
|
|
135
|
+
/**
|
|
136
|
+
* The `aria-labelledby` attribute value for the toggle button
|
|
137
|
+
* Provide the string of ids seperated by space
|
|
138
|
+
*/
|
|
139
|
+
toggleAriaLabelledBy: {
|
|
140
|
+
type: String,
|
|
141
|
+
required: false,
|
|
142
|
+
default: null,
|
|
143
|
+
},
|
|
144
|
+
/**
|
|
145
|
+
* The `aria-labelledby` attribute value for the list of options
|
|
146
|
+
* Provide the string of ids seperated by space
|
|
147
|
+
*/
|
|
148
|
+
listAriaLabelledBy: {
|
|
149
|
+
type: String,
|
|
150
|
+
required: false,
|
|
151
|
+
default: null,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
data() {
|
|
155
|
+
return {
|
|
156
|
+
toggleId: uniqueId('dropdown-toggle-btn-'),
|
|
157
|
+
disclosureId: uniqueId('disclosure-'),
|
|
158
|
+
nextFocusedItemIndex: null,
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
computed: {
|
|
162
|
+
disclosureOptions() {
|
|
163
|
+
if (this.items) {
|
|
164
|
+
if (isAllItems(this.items)) {
|
|
165
|
+
return {
|
|
166
|
+
tag: 'ul',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (isAllGroups(this.items))
|
|
171
|
+
return {
|
|
172
|
+
tag: 'div',
|
|
173
|
+
role: 'group',
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { tag: 'div' };
|
|
178
|
+
},
|
|
179
|
+
hasCustomToggle() {
|
|
180
|
+
return Boolean(this.$scopedSlots.toggle);
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
methods: {
|
|
184
|
+
open() {
|
|
185
|
+
this.$refs.baseDropdown.open();
|
|
186
|
+
},
|
|
187
|
+
close() {
|
|
188
|
+
this.$refs.baseDropdown.close();
|
|
189
|
+
},
|
|
190
|
+
onShow() {
|
|
191
|
+
const items = this.getFocusableListItemElements();
|
|
192
|
+
|
|
193
|
+
if (items.length) {
|
|
194
|
+
this.focusItem(0, items);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Emitted when dropdown is shown
|
|
199
|
+
*
|
|
200
|
+
* @event shown
|
|
201
|
+
*/
|
|
202
|
+
this.$emit(GL_DROPDOWN_SHOWN);
|
|
203
|
+
},
|
|
204
|
+
onHide() {
|
|
205
|
+
/**
|
|
206
|
+
* Emitted when dropdown is hidden
|
|
207
|
+
*
|
|
208
|
+
* @event hidden
|
|
209
|
+
*/
|
|
210
|
+
this.$emit(GL_DROPDOWN_HIDDEN);
|
|
211
|
+
this.nextFocusedItemIndex = null;
|
|
212
|
+
},
|
|
213
|
+
onKeydown(event) {
|
|
214
|
+
const { code } = event;
|
|
215
|
+
const elements = this.getFocusableListItemElements();
|
|
216
|
+
|
|
217
|
+
if (elements.length < 1) return;
|
|
218
|
+
|
|
219
|
+
let stop = true;
|
|
220
|
+
|
|
221
|
+
if (code === HOME) {
|
|
222
|
+
this.focusItem(0, elements);
|
|
223
|
+
} else if (code === END) {
|
|
224
|
+
this.focusItem(elements.length - 1, elements);
|
|
225
|
+
} else if (code === ARROW_UP) {
|
|
226
|
+
this.focusNextItem(event, elements, -1);
|
|
227
|
+
} else if (code === ARROW_DOWN) {
|
|
228
|
+
this.focusNextItem(event, elements, 1);
|
|
229
|
+
} else {
|
|
230
|
+
stop = false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (stop) {
|
|
234
|
+
stopEvent(event);
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
getFocusableListItemElements() {
|
|
238
|
+
const items = this.$refs.content?.querySelectorAll(`.${ITEM_CLASS}`);
|
|
239
|
+
return Array.from(items || []);
|
|
240
|
+
},
|
|
241
|
+
focusNextItem(event, elements, offset) {
|
|
242
|
+
const { target } = event;
|
|
243
|
+
const currentIndex = elements.indexOf(target);
|
|
244
|
+
const nextIndex = clamp(currentIndex + offset, 0, elements.length - 1);
|
|
245
|
+
|
|
246
|
+
this.focusItem(nextIndex, elements);
|
|
247
|
+
},
|
|
248
|
+
focusItem(index, elements) {
|
|
249
|
+
this.nextFocusedItemIndex = index;
|
|
250
|
+
|
|
251
|
+
elements[index]?.focus();
|
|
252
|
+
},
|
|
253
|
+
closeAndFocus() {
|
|
254
|
+
this.$refs.baseDropdown.closeAndFocus();
|
|
255
|
+
},
|
|
256
|
+
handleAction(action) {
|
|
257
|
+
/**
|
|
258
|
+
* Emitted when one of disclosure dropdown items is clicked
|
|
259
|
+
*
|
|
260
|
+
* @event action
|
|
261
|
+
*/
|
|
262
|
+
this.$emit('action', action);
|
|
263
|
+
},
|
|
264
|
+
isItem,
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
</script>
|
|
268
|
+
|
|
269
|
+
<template>
|
|
270
|
+
<gl-base-dropdown
|
|
271
|
+
ref="baseDropdown"
|
|
272
|
+
:aria-labelledby="toggleAriaLabelledBy"
|
|
273
|
+
:toggle-id="toggleId"
|
|
274
|
+
:toggle-text="toggleText"
|
|
275
|
+
:toggle-class="toggleClass"
|
|
276
|
+
:text-sr-only="textSrOnly"
|
|
277
|
+
:category="category"
|
|
278
|
+
:variant="variant"
|
|
279
|
+
:size="size"
|
|
280
|
+
:icon="icon"
|
|
281
|
+
:disabled="disabled"
|
|
282
|
+
:loading="loading"
|
|
283
|
+
:no-caret="noCaret"
|
|
284
|
+
:right="right"
|
|
285
|
+
class="gl-disclosure-dropdown"
|
|
286
|
+
@[$options.events.GL_DROPDOWN_SHOWN]="onShow"
|
|
287
|
+
@[$options.events.GL_DROPDOWN_HIDDEN]="onHide"
|
|
288
|
+
>
|
|
289
|
+
<template v-if="hasCustomToggle" #toggle>
|
|
290
|
+
<!-- @slot Custom toggle content -->
|
|
291
|
+
<slot name="toggle"></slot>
|
|
292
|
+
</template>
|
|
293
|
+
|
|
294
|
+
<!-- @slot Content to display in dropdown header -->
|
|
295
|
+
<slot name="header"></slot>
|
|
296
|
+
|
|
297
|
+
<component
|
|
298
|
+
:is="disclosureOptions.tag"
|
|
299
|
+
:id="disclosureId"
|
|
300
|
+
ref="content"
|
|
301
|
+
:role="disclosureOptions.role"
|
|
302
|
+
:aria-labelledby="listAriaLabelledBy || toggleId"
|
|
303
|
+
data-testid="disclosure-content"
|
|
304
|
+
class="gl-dropdown-contents gl-list-style-none gl-pl-0 gl-mb-0"
|
|
305
|
+
tabindex="-1"
|
|
306
|
+
@keydown="onKeydown"
|
|
307
|
+
>
|
|
308
|
+
<slot>
|
|
309
|
+
<template v-for="(item, index) in items">
|
|
310
|
+
<template v-if="isItem(item)">
|
|
311
|
+
<gl-disclosure-dropdown-item :key="item.text" :item="item" @action="handleAction">
|
|
312
|
+
<!-- @slot Custom template of the disclosure dropdown item -->
|
|
313
|
+
<slot name="list-item" :item="item"></slot>
|
|
314
|
+
</gl-disclosure-dropdown-item>
|
|
315
|
+
</template>
|
|
316
|
+
|
|
317
|
+
<template v-else>
|
|
318
|
+
<gl-disclosure-dropdown-group
|
|
319
|
+
:key="item.name"
|
|
320
|
+
:bordered="index !== 0"
|
|
321
|
+
:group="item"
|
|
322
|
+
@action="handleAction"
|
|
323
|
+
>
|
|
324
|
+
<template v-if="$scopedSlots['group-label']" #group-label>
|
|
325
|
+
<!-- @slot Custom template for group names -->
|
|
326
|
+
<slot name="group-label" :group="item"></slot>
|
|
327
|
+
</template>
|
|
328
|
+
|
|
329
|
+
<template #list-item>
|
|
330
|
+
<!-- @slot Custom template of the disclosure dropdown item -->
|
|
331
|
+
<slot name="list-item"></slot>
|
|
332
|
+
</template>
|
|
333
|
+
</gl-disclosure-dropdown-group>
|
|
334
|
+
</template>
|
|
335
|
+
</template>
|
|
336
|
+
</slot>
|
|
337
|
+
</component>
|
|
338
|
+
|
|
339
|
+
<!-- @slot Content to display in dropdown footer -->
|
|
340
|
+
<slot name="footer"></slot>
|
|
341
|
+
</gl-base-dropdown>
|
|
342
|
+
</template>
|