@gitlab/ui 78.2.2 → 78.3.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 +20 -0
- package/dist/components/base/breadcrumb/breadcrumb.js +103 -29
- package/dist/components/base/new_dropdowns/base_dropdown/base_dropdown.js +3 -2
- package/dist/components/base/new_dropdowns/disclosure/disclosure_dropdown.js +1 -1
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/tokens/css/tokens.css +1 -1
- package/dist/tokens/css/tokens.dark.css +1 -1
- package/dist/tokens/js/tokens.dark.js +1 -1
- package/dist/tokens/js/tokens.js +1 -1
- package/dist/tokens/scss/_tokens.dark.scss +1 -1
- package/dist/tokens/scss/_tokens.scss +1 -1
- package/package.json +2 -2
- package/src/components/base/breadcrumb/breadcrumb.scss +3 -6
- package/src/components/base/breadcrumb/breadcrumb.spec.js +74 -42
- package/src/components/base/breadcrumb/breadcrumb.stories.js +18 -1
- package/src/components/base/breadcrumb/breadcrumb.vue +133 -63
- package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.spec.js +26 -0
- package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.vue +3 -1
- package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.vue +1 -1
- package/src/components/base/new_dropdowns/listbox/listbox.spec.js +14 -0
- package/src/components/experimental/duo/chat/duo_chat.spec.js +33 -46
- package/translations.json +1 -0
package/dist/tokens/js/tokens.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitlab/ui",
|
|
3
|
-
"version": "78.
|
|
3
|
+
"version": "78.3.0",
|
|
4
4
|
"description": "GitLab UI Components",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
"sass-true": "^6.1.0",
|
|
172
172
|
"start-server-and-test": "^1.10.6",
|
|
173
173
|
"storybook": "^7.6.17",
|
|
174
|
-
"storybook-dark-mode": "4.0.
|
|
174
|
+
"storybook-dark-mode": "4.0.1",
|
|
175
175
|
"style-dictionary": "^3.8.0",
|
|
176
176
|
"stylelint": "15.10.2",
|
|
177
177
|
"tailwind-config-viewer": "1.7.3",
|
|
@@ -11,15 +11,15 @@ $breadcrumb-max-width: $grid-size * 16;
|
|
|
11
11
|
@include gl-align-items-center;
|
|
12
12
|
@include gl-line-height-normal;
|
|
13
13
|
@include gl-m-0;
|
|
14
|
-
@include
|
|
15
|
-
|
|
16
|
-
}
|
|
14
|
+
@include gl-flex-nowrap;
|
|
15
|
+
@include gl-max-w-full;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
// bootstrap overrides
|
|
20
19
|
.gl-breadcrumb-item {
|
|
21
20
|
@include gl-font-sm;
|
|
22
21
|
@include gl-line-height-normal;
|
|
22
|
+
@include gl-flex-shrink-0;
|
|
23
23
|
|
|
24
24
|
&:not(:last-child)::after {
|
|
25
25
|
@include gl-text-gray-200;
|
|
@@ -29,9 +29,6 @@ $breadcrumb-max-width: $grid-size * 16;
|
|
|
29
29
|
|
|
30
30
|
> a {
|
|
31
31
|
@include gl-text-gray-700;
|
|
32
|
-
@include media-breakpoint-down(xs) {
|
|
33
|
-
@include str-truncated($breadcrumb-max-width);
|
|
34
|
-
}
|
|
35
32
|
|
|
36
33
|
&:active,
|
|
37
34
|
&:focus,
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { nextTick } from 'vue';
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
3
2
|
import avatarPath1 from '../../../../static/img/avatar.png';
|
|
4
3
|
import avatarPath3 from '../../../../static/img/avatar_1.png';
|
|
5
|
-
import
|
|
4
|
+
import GlDisclosureDropdown from '../new_dropdowns/disclosure/disclosure_dropdown.vue';
|
|
5
|
+
import GlDisclosureDropdownItem from '../new_dropdowns/disclosure/disclosure_dropdown_item.vue';
|
|
6
|
+
import GlBreadcrumb from './breadcrumb.vue';
|
|
6
7
|
import GlBreadcrumbItem from './breadcrumb_item.vue';
|
|
7
8
|
|
|
8
9
|
describe('Breadcrumb component', () => {
|
|
9
10
|
let wrapper;
|
|
10
11
|
|
|
11
12
|
const items = [
|
|
12
|
-
{
|
|
13
|
+
{
|
|
14
|
+
text: 'first_breadcrumb',
|
|
15
|
+
href: 'https://gitlab.com',
|
|
16
|
+
avatarPath: avatarPath1,
|
|
17
|
+
},
|
|
13
18
|
{
|
|
14
19
|
text: 'second_breadcrumb',
|
|
15
20
|
to: 'to_value',
|
|
@@ -21,41 +26,44 @@ describe('Breadcrumb component', () => {
|
|
|
21
26
|
},
|
|
22
27
|
];
|
|
23
28
|
|
|
24
|
-
const extraItems = [
|
|
25
|
-
{ text: 'fourth_breadcrumb', href: 'https://gitlab.com' },
|
|
26
|
-
{
|
|
27
|
-
text: 'fifth_breadcrumb',
|
|
28
|
-
to: 'to_value',
|
|
29
|
-
},
|
|
30
|
-
];
|
|
31
|
-
|
|
32
29
|
const findAllAvatars = () => wrapper.findAll('[data-testid="avatar"]');
|
|
33
30
|
const findBreadcrumbItems = () => wrapper.findAllComponents(GlBreadcrumbItem);
|
|
34
|
-
const
|
|
31
|
+
const findOverflowDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
|
|
35
32
|
|
|
36
33
|
const findVisibleBreadcrumbItems = () =>
|
|
37
34
|
findBreadcrumbItems().wrappers.filter((item) => item.isVisible());
|
|
38
|
-
const findHiddenBreadcrumbItems = () =>
|
|
39
|
-
findBreadcrumbItems().wrappers.filter((item) => !item.isVisible());
|
|
40
35
|
|
|
41
36
|
const createComponent = (propsData = { items }) => {
|
|
42
|
-
wrapper =
|
|
37
|
+
wrapper = mount(GlBreadcrumb, {
|
|
43
38
|
propsData,
|
|
44
39
|
stubs: {
|
|
45
40
|
GlBreadcrumbItem,
|
|
41
|
+
GlDisclosureDropdown,
|
|
46
42
|
},
|
|
47
43
|
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const mockWrapperWidth = (widthInPx) => {
|
|
47
|
+
wrapper.element.style.width = `${widthInPx}px`;
|
|
48
|
+
|
|
49
|
+
Object.defineProperty(wrapper.element, 'clientWidth', {
|
|
50
|
+
get: () => widthInPx,
|
|
51
|
+
configurable: true,
|
|
52
|
+
});
|
|
53
|
+
};
|
|
48
54
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
const mockWideWrapperWidth = () => {
|
|
56
|
+
mockWrapperWidth(1000);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const mockSmallWrapperWidth = () => {
|
|
60
|
+
mockWrapperWidth(1);
|
|
54
61
|
};
|
|
55
62
|
|
|
56
63
|
describe('items', () => {
|
|
57
|
-
it('has one breadcrumb-item for each item in the items props', () => {
|
|
64
|
+
it('has one breadcrumb-item for each item in the items props', async () => {
|
|
58
65
|
createComponent();
|
|
66
|
+
await wrapper.vm.$nextTick();
|
|
59
67
|
|
|
60
68
|
expect(findBreadcrumbItems()).toHaveLength(items.length);
|
|
61
69
|
});
|
|
@@ -75,9 +83,36 @@ describe('Breadcrumb component', () => {
|
|
|
75
83
|
});
|
|
76
84
|
});
|
|
77
85
|
|
|
86
|
+
describe('showMoreLabel', () => {
|
|
87
|
+
describe('when provided', () => {
|
|
88
|
+
beforeEach(async () => {
|
|
89
|
+
createComponent({ items, showMoreLabel: 'More...' });
|
|
90
|
+
mockSmallWrapperWidth();
|
|
91
|
+
await wrapper.vm.$nextTick();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('uses prop', () => {
|
|
95
|
+
expect(findOverflowDropdown().props('toggleText')).toBe('More...');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('when not provided', () => {
|
|
100
|
+
beforeEach(async () => {
|
|
101
|
+
createComponent();
|
|
102
|
+
mockSmallWrapperWidth();
|
|
103
|
+
await wrapper.vm.$nextTick();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('uses default', () => {
|
|
107
|
+
expect(findOverflowDropdown().props('toggleText')).toBe('Show more breadcrumbs');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
78
112
|
describe('avatars', () => {
|
|
79
|
-
it('renders 2 avatars when 2 avatarPaths are passed', () => {
|
|
113
|
+
it('renders 2 avatars when 2 avatarPaths are passed', async () => {
|
|
80
114
|
createComponent();
|
|
115
|
+
await wrapper.vm.$nextTick();
|
|
81
116
|
|
|
82
117
|
expect(findAllAvatars()).toHaveLength(2);
|
|
83
118
|
});
|
|
@@ -86,6 +121,7 @@ describe('Breadcrumb component', () => {
|
|
|
86
121
|
describe('bindings', () => {
|
|
87
122
|
beforeEach(() => {
|
|
88
123
|
createComponent();
|
|
124
|
+
mockWideWrapperWidth();
|
|
89
125
|
});
|
|
90
126
|
|
|
91
127
|
it('first breadcrumb has text, href && ariaCurrent=`false` bound', () => {
|
|
@@ -114,12 +150,14 @@ describe('Breadcrumb component', () => {
|
|
|
114
150
|
});
|
|
115
151
|
|
|
116
152
|
describe('collapsible', () => {
|
|
117
|
-
describe(`when
|
|
153
|
+
describe(`when there is enough room to fit all items`, () => {
|
|
118
154
|
beforeEach(() => {
|
|
119
155
|
createComponent();
|
|
156
|
+
mockWideWrapperWidth();
|
|
120
157
|
});
|
|
158
|
+
|
|
121
159
|
it('should not display collapsed list expander', () => {
|
|
122
|
-
expect(
|
|
160
|
+
expect(findOverflowDropdown().exists()).toBe(false);
|
|
123
161
|
});
|
|
124
162
|
|
|
125
163
|
it('should display all items visible', () => {
|
|
@@ -127,27 +165,21 @@ describe('Breadcrumb component', () => {
|
|
|
127
165
|
});
|
|
128
166
|
});
|
|
129
167
|
|
|
130
|
-
describe(`when
|
|
131
|
-
beforeEach(() => {
|
|
132
|
-
createComponent(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
expect(findCollapsedListExpander().exists()).toBe(true);
|
|
168
|
+
describe(`when there is NOT enough room to fit all items`, () => {
|
|
169
|
+
beforeEach(async () => {
|
|
170
|
+
createComponent();
|
|
171
|
+
mockSmallWrapperWidth();
|
|
172
|
+
await wrapper.vm.$nextTick();
|
|
136
173
|
});
|
|
137
174
|
|
|
138
|
-
it('should display
|
|
139
|
-
|
|
140
|
-
expect(findVisibleBreadcrumbItems()).toHaveLength(alwaysVisibleNum);
|
|
141
|
-
expect(findHiddenBreadcrumbItems()).toHaveLength(
|
|
142
|
-
items.length + extraItems.length - alwaysVisibleNum
|
|
143
|
-
);
|
|
175
|
+
it('should display overflow dropdown', () => {
|
|
176
|
+
expect(findOverflowDropdown().exists()).toBe(true);
|
|
144
177
|
});
|
|
145
178
|
|
|
146
|
-
it('
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
expect(
|
|
150
|
-
expect(findVisibleBreadcrumbItems()).toHaveLength(items.length + extraItems.length);
|
|
179
|
+
it('moves the overflowing items into the dropdown', () => {
|
|
180
|
+
const fittingItems = findBreadcrumbItems().length;
|
|
181
|
+
const overflowingItems = wrapper.findAllComponents(GlDisclosureDropdownItem).length;
|
|
182
|
+
expect(fittingItems + overflowingItems).toEqual(items.length);
|
|
151
183
|
});
|
|
152
184
|
});
|
|
153
185
|
});
|
|
@@ -10,6 +10,15 @@ const template = `
|
|
|
10
10
|
/>
|
|
11
11
|
`;
|
|
12
12
|
|
|
13
|
+
const collapsedTemplate = `
|
|
14
|
+
<div style="max-width: 300px">
|
|
15
|
+
<gl-breadcrumb
|
|
16
|
+
:items="items"
|
|
17
|
+
:aria-label="ariaLabel"
|
|
18
|
+
/>
|
|
19
|
+
</div>
|
|
20
|
+
`;
|
|
21
|
+
|
|
13
22
|
const defaultItems = [
|
|
14
23
|
{
|
|
15
24
|
text: 'First item',
|
|
@@ -45,6 +54,14 @@ const Template = (args, { argTypes }) => ({
|
|
|
45
54
|
export const Default = Template.bind({});
|
|
46
55
|
Default.args = generateProps();
|
|
47
56
|
|
|
57
|
+
const CollapsedTemplate = (args, { argTypes }) => ({
|
|
58
|
+
components: {
|
|
59
|
+
GlBreadcrumb,
|
|
60
|
+
},
|
|
61
|
+
props: Object.keys(argTypes),
|
|
62
|
+
template: collapsedTemplate,
|
|
63
|
+
});
|
|
64
|
+
|
|
48
65
|
export default {
|
|
49
66
|
title: 'base/breadcrumb',
|
|
50
67
|
component: GlBreadcrumb,
|
|
@@ -77,5 +94,5 @@ const extraItems = [
|
|
|
77
94
|
},
|
|
78
95
|
];
|
|
79
96
|
|
|
80
|
-
export const CollapsedItems =
|
|
97
|
+
export const CollapsedItems = CollapsedTemplate.bind({});
|
|
81
98
|
CollapsedItems.args = generateProps({ items: [...defaultItems, ...extraItems] });
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
<!-- eslint-disable vue/multi-word-component-names -->
|
|
2
2
|
<script>
|
|
3
3
|
import { BBreadcrumb } from 'bootstrap-vue';
|
|
4
|
-
import
|
|
4
|
+
import debounce from 'lodash/debounce';
|
|
5
|
+
import { translate } from '../../../utils/i18n';
|
|
5
6
|
import GlAvatar from '../avatar/avatar.vue';
|
|
7
|
+
import GlDisclosureDropdown from '../new_dropdowns/disclosure/disclosure_dropdown.vue';
|
|
6
8
|
import { GlTooltipDirective } from '../../../directives/tooltip';
|
|
7
9
|
import GlBreadcrumbItem from './breadcrumb_item.vue';
|
|
8
10
|
|
|
9
|
-
export const COLLAPSE_AT_SIZE = 4;
|
|
10
|
-
|
|
11
11
|
export default {
|
|
12
12
|
name: 'GlBreadcrumb',
|
|
13
13
|
components: {
|
|
14
14
|
BBreadcrumb,
|
|
15
|
-
GlButton,
|
|
16
15
|
GlBreadcrumbItem,
|
|
17
16
|
GlAvatar,
|
|
17
|
+
GlDisclosureDropdown,
|
|
18
18
|
},
|
|
19
19
|
directives: {
|
|
20
20
|
GlTooltip: GlTooltipDirective,
|
|
@@ -40,47 +40,118 @@ export default {
|
|
|
40
40
|
required: false,
|
|
41
41
|
default: 'Breadcrumb',
|
|
42
42
|
},
|
|
43
|
+
/**
|
|
44
|
+
* The label for the collapsed dropdown toggle. Screen-reader only.
|
|
45
|
+
*/
|
|
46
|
+
showMoreLabel: {
|
|
47
|
+
type: String,
|
|
48
|
+
required: false,
|
|
49
|
+
default: () => translate('GlBreadcrumb.showMoreLabel', 'Show more breadcrumbs'),
|
|
50
|
+
},
|
|
43
51
|
},
|
|
44
52
|
data() {
|
|
45
53
|
return {
|
|
46
|
-
|
|
54
|
+
fittingItems: [...this.items], // array of items that fit on the screen
|
|
55
|
+
overflowingItems: [], // array of items that didn't fit and were put in a dropdown instead
|
|
56
|
+
totalBreadcrumbsWidth: 0, // the total width of all breadcrumb items combined
|
|
57
|
+
widthPerItem: [], // array with the indivudal widths of each breadcrumb item
|
|
58
|
+
resizeDone: false, // to apply some CSS only during/after resizing
|
|
47
59
|
};
|
|
48
60
|
},
|
|
49
61
|
computed: {
|
|
50
|
-
breadcrumbsSize() {
|
|
51
|
-
return this.items.length;
|
|
52
|
-
},
|
|
53
62
|
hasCollapsible() {
|
|
54
|
-
return this.
|
|
63
|
+
return this.overflowingItems.length > 0;
|
|
55
64
|
},
|
|
56
|
-
|
|
57
|
-
return
|
|
65
|
+
breadcrumbStyle() {
|
|
66
|
+
return this.resizeDone ? {} : { opacity: 0 };
|
|
58
67
|
},
|
|
68
|
+
itemStyle() {
|
|
69
|
+
/**
|
|
70
|
+
* If the last/only item, which is always visible, has a very long title,
|
|
71
|
+
* it could overflow the breadcrumb component. This CSS makes sure it
|
|
72
|
+
* shows an ellipsis instead.
|
|
73
|
+
* But this CSS cannot be active while we do the size calculation, as that
|
|
74
|
+
* would then not take the real unshrunk width of that item into account.
|
|
75
|
+
*/
|
|
76
|
+
if (this.resizeDone && this.fittingItems.length === 1) {
|
|
77
|
+
return {
|
|
78
|
+
'flex-shrink': 1,
|
|
79
|
+
'text-overflow': 'ellipsis',
|
|
80
|
+
'overflow-x': 'hidden',
|
|
81
|
+
'text-wrap': 'nowrap',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {};
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
watch: {
|
|
88
|
+
items: {
|
|
89
|
+
handler: 'measureAndMakeBreadcrumbsFit',
|
|
90
|
+
deep: true,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
created() {
|
|
94
|
+
this.debounceMakeBreadcrumbsFit = debounce(this.makeBreadcrumbsFit, 25);
|
|
95
|
+
},
|
|
96
|
+
mounted() {
|
|
97
|
+
window.addEventListener('resize', this.debounceMakeBreadcrumbsFit);
|
|
98
|
+
this.measureAndMakeBreadcrumbsFit();
|
|
99
|
+
},
|
|
100
|
+
beforeDestroy() {
|
|
101
|
+
window.removeEventListener('resize', this.debounceMakeBreadcrumbsFit);
|
|
59
102
|
},
|
|
60
103
|
methods: {
|
|
61
|
-
|
|
62
|
-
|
|
104
|
+
resetItems() {
|
|
105
|
+
this.fittingItems = [...this.items];
|
|
106
|
+
this.overflowingItems = [];
|
|
63
107
|
},
|
|
64
|
-
|
|
65
|
-
|
|
108
|
+
async measureAndMakeBreadcrumbsFit() {
|
|
109
|
+
this.resizeDone = false;
|
|
110
|
+
this.resetItems();
|
|
111
|
+
|
|
112
|
+
// Wait for DOM update so all items get rendered and can be measured.
|
|
113
|
+
await this.$nextTick();
|
|
114
|
+
|
|
115
|
+
this.totalBreadcrumbsWidth = 0;
|
|
116
|
+
this.$refs.breadcrumbs.forEach((b, index) => {
|
|
117
|
+
const width = b.$el.clientWidth;
|
|
118
|
+
this.totalBreadcrumbsWidth += width;
|
|
119
|
+
this.widthPerItem[index] = width;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
this.makeBreadcrumbsFit();
|
|
66
123
|
},
|
|
67
|
-
|
|
68
|
-
this.
|
|
124
|
+
makeBreadcrumbsFit() {
|
|
125
|
+
this.resizeDone = false;
|
|
126
|
+
this.resetItems();
|
|
127
|
+
|
|
128
|
+
const containerWidth = this.$el.clientWidth;
|
|
129
|
+
const buttonWidth = 50; // px
|
|
130
|
+
|
|
131
|
+
if (this.totalBreadcrumbsWidth + buttonWidth > containerWidth) {
|
|
132
|
+
// Not all breadcrumb items fit. Start moving items over to the dropdown.
|
|
133
|
+
const startSlicingAt = 0;
|
|
69
134
|
|
|
70
|
-
|
|
71
|
-
this
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
135
|
+
// The last item will never be moved into the dropdown.
|
|
136
|
+
const stopSlicingAt = this.items.length - 1;
|
|
137
|
+
|
|
138
|
+
let widthNeeded = this.totalBreadcrumbsWidth;
|
|
139
|
+
for (let index = startSlicingAt; index < stopSlicingAt; index += 1) {
|
|
140
|
+
// Move one breadcrumb item into the dropdown
|
|
141
|
+
this.overflowingItems.push(this.fittingItems[startSlicingAt]);
|
|
142
|
+
this.fittingItems.splice(startSlicingAt, 1);
|
|
143
|
+
|
|
144
|
+
widthNeeded -= this.widthPerItem[index];
|
|
145
|
+
|
|
146
|
+
// Does it fit now? Then stop.
|
|
147
|
+
if (widthNeeded + buttonWidth < containerWidth) break;
|
|
148
|
+
}
|
|
75
149
|
}
|
|
150
|
+
|
|
151
|
+
this.resizeDone = true;
|
|
76
152
|
},
|
|
77
|
-
|
|
78
|
-
return index ===
|
|
79
|
-
},
|
|
80
|
-
isItemCollapsed(index) {
|
|
81
|
-
return (
|
|
82
|
-
this.hasCollapsible && this.isListCollapsed && !this.nonCollapsibleIndices.includes(index)
|
|
83
|
-
);
|
|
153
|
+
isLastItem(index) {
|
|
154
|
+
return index === this.fittingItems.length - 1;
|
|
84
155
|
},
|
|
85
156
|
getAriaCurrentAttr(index) {
|
|
86
157
|
return this.isLastItem(index) ? 'page' : false;
|
|
@@ -89,42 +160,41 @@ export default {
|
|
|
89
160
|
};
|
|
90
161
|
</script>
|
|
91
162
|
<template>
|
|
92
|
-
<nav class="gl-breadcrumbs" :aria-label="ariaLabel">
|
|
163
|
+
<nav class="gl-breadcrumbs" :aria-label="ariaLabel" :style="breadcrumbStyle">
|
|
93
164
|
<b-breadcrumb class="gl-breadcrumb-list" v-bind="$attrs" v-on="$listeners">
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
aria-hidden="true"
|
|
108
|
-
class="gl-breadcrumb-avatar-tile gl-border gl-mr-2 gl-rounded-base!"
|
|
109
|
-
shape="rect"
|
|
110
|
-
data-testid="avatar"
|
|
111
|
-
/><span>{{ item.text }}</span>
|
|
112
|
-
</gl-breadcrumb-item>
|
|
165
|
+
<li v-if="hasCollapsible" class="gl-breadcrumb-item">
|
|
166
|
+
<gl-disclosure-dropdown
|
|
167
|
+
:items="overflowingItems"
|
|
168
|
+
:toggle-text="showMoreLabel"
|
|
169
|
+
fluid-width
|
|
170
|
+
text-sr-only
|
|
171
|
+
no-caret
|
|
172
|
+
icon="ellipsis_h"
|
|
173
|
+
size="small"
|
|
174
|
+
style="height: 16px"
|
|
175
|
+
placement="left"
|
|
176
|
+
/>
|
|
177
|
+
</li>
|
|
113
178
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
179
|
+
<!-- eslint-disable-next-line vue/valid-v-for (for @vue/compat) -->
|
|
180
|
+
<gl-breadcrumb-item
|
|
181
|
+
v-for="(item, index) in fittingItems"
|
|
182
|
+
ref="breadcrumbs"
|
|
183
|
+
:text="item.text"
|
|
184
|
+
:href="item.href"
|
|
185
|
+
:style="itemStyle"
|
|
186
|
+
:to="item.to"
|
|
187
|
+
:aria-current="getAriaCurrentAttr(index)"
|
|
188
|
+
><gl-avatar
|
|
189
|
+
v-if="item.avatarPath"
|
|
190
|
+
:src="item.avatarPath"
|
|
191
|
+
:size="16"
|
|
192
|
+
aria-hidden="true"
|
|
193
|
+
class="gl-breadcrumb-avatar-tile gl-border gl-mr-2 gl-rounded-base!"
|
|
194
|
+
shape="rect"
|
|
195
|
+
data-testid="avatar"
|
|
196
|
+
/><span>{{ item.text }}</span>
|
|
197
|
+
</gl-breadcrumb-item>
|
|
128
198
|
</b-breadcrumb>
|
|
129
199
|
</nav>
|
|
130
200
|
</template>
|
|
@@ -337,6 +337,20 @@ describe('base dropdown', () => {
|
|
|
337
337
|
expect(toggle.element).toHaveFocus();
|
|
338
338
|
});
|
|
339
339
|
|
|
340
|
+
it('should close menu on Escape when focus is on toggle', async () => {
|
|
341
|
+
const toggle = findDefaultDropdownToggle();
|
|
342
|
+
const menu = findDropdownMenu();
|
|
343
|
+
|
|
344
|
+
await toggle.trigger('click');
|
|
345
|
+
expect(menu.classes('gl-display-block!')).toBe(true);
|
|
346
|
+
expect(toggle.attributes('aria-expanded')).toBe('true');
|
|
347
|
+
|
|
348
|
+
await toggle.trigger('keydown.esc');
|
|
349
|
+
expect(menu.classes('gl-display-block!')).toBe(false);
|
|
350
|
+
expect(toggle.attributes('aria-expanded')).toBe('false');
|
|
351
|
+
expect(wrapper.emitted(GL_DROPDOWN_HIDDEN)).toHaveLength(1);
|
|
352
|
+
});
|
|
353
|
+
|
|
340
354
|
describe('when the consumer takes over the focus', () => {
|
|
341
355
|
let consumerButton;
|
|
342
356
|
|
|
@@ -487,6 +501,18 @@ describe('base dropdown', () => {
|
|
|
487
501
|
expect(wrapper.emitted(GL_DROPDOWN_HIDDEN)).toHaveLength(1);
|
|
488
502
|
expect(toggle.find(`[data-testid="${customToggleTestId}"]`).element).toHaveFocus();
|
|
489
503
|
});
|
|
504
|
+
|
|
505
|
+
it('should close menu on Escape when focus is on toggle', async () => {
|
|
506
|
+
const toggle = findCustomDropdownToggle();
|
|
507
|
+
const menu = findDropdownMenu();
|
|
508
|
+
|
|
509
|
+
await toggle.trigger('click');
|
|
510
|
+
expect(menu.classes('gl-display-block!')).toBe(true);
|
|
511
|
+
|
|
512
|
+
await toggle.trigger('keydown.esc');
|
|
513
|
+
expect(menu.classes('gl-display-block!')).toBe(false);
|
|
514
|
+
expect(wrapper.emitted(GL_DROPDOWN_HIDDEN)).toHaveLength(1);
|
|
515
|
+
});
|
|
490
516
|
});
|
|
491
517
|
|
|
492
518
|
it('should emit `GL_DROPDOWN_FOCUS_CONTENT` event on `ARROW_DOWN`', () => {
|
|
@@ -400,9 +400,10 @@ export default {
|
|
|
400
400
|
return;
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
+
const hadFocusWithin = this.$el.contains(document.activeElement);
|
|
403
404
|
const hasToggled = await this.toggle(event);
|
|
404
405
|
|
|
405
|
-
if (!
|
|
406
|
+
if (!hadFocusWithin) {
|
|
406
407
|
return;
|
|
407
408
|
}
|
|
408
409
|
|
|
@@ -459,6 +460,7 @@ export default {
|
|
|
459
460
|
ref="toggle"
|
|
460
461
|
data-testid="base-dropdown-toggle"
|
|
461
462
|
v-on="toggleListeners"
|
|
463
|
+
@keydown.esc.stop.prevent="close"
|
|
462
464
|
>
|
|
463
465
|
<!-- @slot Custom toggle button content -->
|
|
464
466
|
<slot name="toggle">
|
|
@@ -404,7 +404,7 @@ export default {
|
|
|
404
404
|
<template v-if="isItem(item)">
|
|
405
405
|
<!-- eslint-disable-next-line vue/valid-v-for -->
|
|
406
406
|
<gl-disclosure-dropdown-item :key="uniqueItemId()" :item="item" @action="handleAction">
|
|
407
|
-
<template #list-item>
|
|
407
|
+
<template v-if="'list-item' in $scopedSlots" #list-item>
|
|
408
408
|
<!-- @slot Custom template of the disclosure dropdown item -->
|
|
409
409
|
<slot name="list-item" :item="item"></slot>
|
|
410
410
|
</template>
|
|
@@ -59,6 +59,7 @@ describe('GlCollapsibleListbox', () => {
|
|
|
59
59
|
const findSelectAllButton = () => wrapper.find("[data-testid='listbox-select-all-button']");
|
|
60
60
|
const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
|
|
61
61
|
const findDropdownMenu = () => wrapper.find("[data-testid='base-dropdown-menu']");
|
|
62
|
+
const findDropdownToggle = () => wrapper.find("[data-testid='base-dropdown-toggle'");
|
|
62
63
|
|
|
63
64
|
it('passes custom offset to the base dropdown', () => {
|
|
64
65
|
const dropdownOffset = { mainAxis: 10, crossAxis: 40 };
|
|
@@ -927,4 +928,17 @@ describe('GlCollapsibleListbox', () => {
|
|
|
927
928
|
expect(findDropdownMenu().classes()).not.toContain('gl-display-block!');
|
|
928
929
|
});
|
|
929
930
|
});
|
|
931
|
+
|
|
932
|
+
it('focuses the toggle when closed by ESC key while item had focus', async () => {
|
|
933
|
+
buildWrapper({
|
|
934
|
+
selected: mockOptions[1].value,
|
|
935
|
+
items: mockOptions,
|
|
936
|
+
startOpened: true,
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
await nextTick();
|
|
940
|
+
findListItem(1).trigger('keydown.esc');
|
|
941
|
+
await nextTick();
|
|
942
|
+
expect(findDropdownToggle().element).toHaveFocus();
|
|
943
|
+
});
|
|
930
944
|
});
|