@gitlab/ui 56.0.0 → 56.1.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 +32 -4
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/utils/utils.js +13 -1
- package/package.json +3 -3
- package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.spec.js +11 -6
- package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.vue +32 -7
- package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.stories.js +3 -1
- package/src/components/base/new_dropdowns/dropdown.scss +5 -3
- package/src/components/base/new_dropdowns/listbox/listbox.stories.js +6 -1
- package/src/components/charts/chart/chart.md +7 -7
- package/src/utils/utils.js +13 -0
- package/src/utils/utils.spec.js +38 -1
package/dist/utils/utils.js
CHANGED
|
@@ -83,6 +83,18 @@ function isElementFocusable(elt) {
|
|
|
83
83
|
return isValidTag && hasValidType && !isDisabled && hasValidZIndex && !isInvalidAnchorTag;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Receives an element and validates that it is reachable via sequential keyboard navigation
|
|
88
|
+
* @param { HTMLElement } The element to validate
|
|
89
|
+
* @return { boolean } Is the element focusable in a sequential tab order
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
function isElementTabbable(el) {
|
|
93
|
+
if (!el) return false;
|
|
94
|
+
const tabindex = parseInt(el.getAttribute('tabindex'), 10);
|
|
95
|
+
return tabindex > -1;
|
|
96
|
+
}
|
|
97
|
+
|
|
86
98
|
/**
|
|
87
99
|
* Receives an array of HTML elements and focus the first one possible
|
|
88
100
|
* @param { Array.<HTMLElement> } An array of element to potentially focus
|
|
@@ -142,4 +154,4 @@ function filterVisible(els) {
|
|
|
142
154
|
return (els || []).filter(isVisible);
|
|
143
155
|
}
|
|
144
156
|
|
|
145
|
-
export { colorFromBackground, debounceByAnimationFrame, filterVisible, focusFirstFocusableElement, hexToRgba, isDev, isElementFocusable, logWarning, rgbFromHex, rgbFromString, stopEvent, throttle, uid };
|
|
157
|
+
export { colorFromBackground, debounceByAnimationFrame, filterVisible, focusFirstFocusableElement, hexToRgba, isDev, isElementFocusable, isElementTabbable, logWarning, rgbFromHex, rgbFromString, stopEvent, throttle, uid };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitlab/ui",
|
|
3
|
-
"version": "56.
|
|
3
|
+
"version": "56.1.0",
|
|
4
4
|
"description": "GitLab UI Components",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@popperjs/core": "^2.11.2",
|
|
64
64
|
"bootstrap-vue": "2.20.1",
|
|
65
|
-
"dompurify": "^2.4.
|
|
65
|
+
"dompurify": "^2.4.4",
|
|
66
66
|
"echarts": "^5.3.2",
|
|
67
67
|
"iframe-resizer": "^4.3.2",
|
|
68
68
|
"lodash": "^4.17.20",
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"bootstrap-vue-vue3": "npm:bootstrap-vue@2.23.1",
|
|
116
116
|
"cypress": "^11.2.0",
|
|
117
117
|
"emoji-regex": "^10.0.0",
|
|
118
|
-
"eslint": "8.
|
|
118
|
+
"eslint": "8.34.0",
|
|
119
119
|
"eslint-import-resolver-jest": "3.0.2",
|
|
120
120
|
"eslint-plugin-cypress": "2.12.1",
|
|
121
121
|
"eslint-plugin-storybook": "0.6.10",
|
|
@@ -198,7 +198,10 @@ describe('base dropdown', () => {
|
|
|
198
198
|
});
|
|
199
199
|
|
|
200
200
|
describe('Custom toggle', () => {
|
|
201
|
-
const
|
|
201
|
+
const customToggleTestId = 'custom-toggle';
|
|
202
|
+
const toggleContent = `<button data-testid="${customToggleTestId}">Custom toggle</button>`;
|
|
203
|
+
const findFirstToggleElement = () =>
|
|
204
|
+
findCustomDropdownToggle().find(`[data-testid="${customToggleTestId}"]`);
|
|
202
205
|
|
|
203
206
|
beforeEach(() => {
|
|
204
207
|
const slots = { toggle: toggleContent };
|
|
@@ -220,23 +223,25 @@ describe('base dropdown', () => {
|
|
|
220
223
|
describe('toggle visibility', () => {
|
|
221
224
|
it('should toggle menu visibility on toggle button ENTER', async () => {
|
|
222
225
|
const toggle = findCustomDropdownToggle();
|
|
226
|
+
const firstToggleChild = findFirstToggleElement();
|
|
223
227
|
const menu = findDropdownMenu();
|
|
224
228
|
// open menu clicking toggle btn
|
|
225
229
|
await toggle.trigger('keydown', { code: ENTER });
|
|
226
230
|
expect(menu.classes('gl-display-block!')).toBe(true);
|
|
227
|
-
expect(
|
|
231
|
+
expect(firstToggleChild.attributes('aria-expanded')).toBe('true');
|
|
228
232
|
await nextTick();
|
|
229
233
|
expect(wrapper.emitted(GL_DROPDOWN_SHOWN).length).toBe(1);
|
|
230
234
|
|
|
231
235
|
// close menu clicking toggle btn again
|
|
232
236
|
await toggle.trigger('keydown', { code: ENTER });
|
|
233
237
|
expect(menu.classes('gl-display-block!')).toBe(false);
|
|
234
|
-
expect(
|
|
238
|
+
expect(firstToggleChild.attributes('aria-expanded')).toBe('false');
|
|
235
239
|
expect(wrapper.emitted(GL_DROPDOWN_HIDDEN).length).toBe(1);
|
|
236
240
|
});
|
|
237
241
|
|
|
238
|
-
it('should close the menu when Escape is pressed inside menu and focus toggle', async () => {
|
|
242
|
+
it('should close the menu when Escape is pressed inside menu and focus first child in the toggle', async () => {
|
|
239
243
|
const toggle = findCustomDropdownToggle();
|
|
244
|
+
const firstToggleChild = findFirstToggleElement();
|
|
240
245
|
const menu = findDropdownMenu();
|
|
241
246
|
// open menu clicking toggle btn
|
|
242
247
|
await toggle.trigger('click');
|
|
@@ -245,9 +250,9 @@ describe('base dropdown', () => {
|
|
|
245
250
|
// close menu pressing ESC on it
|
|
246
251
|
await menu.trigger('keydown.esc');
|
|
247
252
|
expect(menu.classes('gl-display-block!')).toBe(false);
|
|
248
|
-
expect(
|
|
253
|
+
expect(firstToggleChild.attributes('aria-expanded')).toBe('false');
|
|
249
254
|
expect(wrapper.emitted(GL_DROPDOWN_HIDDEN).length).toBe(1);
|
|
250
|
-
expect(toggle.element).toHaveFocus();
|
|
255
|
+
expect(toggle.find(`[data-testid="${customToggleTestId}"]`).element).toHaveFocus();
|
|
251
256
|
});
|
|
252
257
|
});
|
|
253
258
|
});
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
dropdownVariantOptions,
|
|
9
9
|
} from '../../../../utils/constants';
|
|
10
10
|
import { POPPER_CONFIG, GL_DROPDOWN_SHOWN, GL_DROPDOWN_HIDDEN, ENTER, SPACE } from '../constants';
|
|
11
|
+
import { logWarning, isElementTabbable, isElementFocusable } from '../../../../utils/utils';
|
|
11
12
|
|
|
12
13
|
import GlButton from '../../button/button.vue';
|
|
13
14
|
import GlIcon from '../../icon/icon.vue';
|
|
@@ -119,6 +120,14 @@ export default {
|
|
|
119
120
|
isIconOnly() {
|
|
120
121
|
return Boolean(this.icon && (!this.toggleText?.length || this.textSrOnly));
|
|
121
122
|
},
|
|
123
|
+
ariaAttributes() {
|
|
124
|
+
return {
|
|
125
|
+
'aria-haspopup': this.ariaHaspopup,
|
|
126
|
+
'aria-expanded': this.visible,
|
|
127
|
+
'aria-controls': this.baseDropdownId,
|
|
128
|
+
'aria-labelledby': this.toggleLabelledBy,
|
|
129
|
+
};
|
|
130
|
+
},
|
|
122
131
|
toggleButtonClasses() {
|
|
123
132
|
return [
|
|
124
133
|
this.toggleClass,
|
|
@@ -151,6 +160,7 @@ export default {
|
|
|
151
160
|
disabled: this.disabled,
|
|
152
161
|
loading: this.loading,
|
|
153
162
|
class: this.toggleButtonClasses,
|
|
163
|
+
...this.ariaAttributes,
|
|
154
164
|
listeners: {
|
|
155
165
|
click: () => this.toggle(),
|
|
156
166
|
},
|
|
@@ -159,9 +169,7 @@ export default {
|
|
|
159
169
|
|
|
160
170
|
return {
|
|
161
171
|
is: 'div',
|
|
162
|
-
role: 'button',
|
|
163
172
|
class: 'gl-new-dropdown-custom-toggle',
|
|
164
|
-
tabindex: '0',
|
|
165
173
|
listeners: {
|
|
166
174
|
keydown: (event) => this.onKeydown(event),
|
|
167
175
|
click: () => this.toggle(),
|
|
@@ -169,7 +177,7 @@ export default {
|
|
|
169
177
|
};
|
|
170
178
|
},
|
|
171
179
|
toggleElement() {
|
|
172
|
-
return this.$refs.toggle.$el || this.$refs.toggle;
|
|
180
|
+
return this.$refs.toggle.$el || this.$refs.toggle?.firstElementChild;
|
|
173
181
|
},
|
|
174
182
|
popperConfig() {
|
|
175
183
|
return {
|
|
@@ -178,15 +186,36 @@ export default {
|
|
|
178
186
|
};
|
|
179
187
|
},
|
|
180
188
|
},
|
|
189
|
+
watch: {
|
|
190
|
+
ariaAttributes: {
|
|
191
|
+
deep: true,
|
|
192
|
+
handler(ariaAttributes) {
|
|
193
|
+
if (this.$scopedSlots.toggle) {
|
|
194
|
+
Object.keys(ariaAttributes).forEach((key) => {
|
|
195
|
+
this.toggleElement.setAttribute(key, ariaAttributes[key]);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
181
201
|
mounted() {
|
|
182
202
|
this.$nextTick(() => {
|
|
183
203
|
this.popper = createPopper(this.toggleElement, this.$refs.content, this.popperConfig);
|
|
184
204
|
});
|
|
205
|
+
this.checkToggleFocusable();
|
|
185
206
|
},
|
|
186
207
|
beforeDestroy() {
|
|
187
208
|
this.popper.destroy();
|
|
188
209
|
},
|
|
189
210
|
methods: {
|
|
211
|
+
checkToggleFocusable() {
|
|
212
|
+
if (!isElementFocusable(this.toggleElement) && !isElementTabbable(this.toggleElement)) {
|
|
213
|
+
logWarning(
|
|
214
|
+
`GlDisclosureDropdown/GlCollapsibleListbox: Toggle is missing a 'tabindex' and cannot be focused.
|
|
215
|
+
Use 'a' or 'button' element instead or make sure to add 'role="button"' along with 'tabindex' otherwise.`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
},
|
|
190
219
|
async toggle() {
|
|
191
220
|
this.visible = !this.visible;
|
|
192
221
|
|
|
@@ -249,10 +278,6 @@ export default {
|
|
|
249
278
|
:id="toggleId"
|
|
250
279
|
ref="toggle"
|
|
251
280
|
data-testid="base-dropdown-toggle"
|
|
252
|
-
:aria-haspopup="ariaHaspopup"
|
|
253
|
-
:aria-expanded="visible"
|
|
254
|
-
:aria-labelledby="toggleLabelledBy"
|
|
255
|
-
:aria-controls="baseDropdownId"
|
|
256
281
|
v-on="toggleOptions.listeners"
|
|
257
282
|
>
|
|
258
283
|
<!-- @slot Custom toggle button content -->
|
|
@@ -209,10 +209,12 @@ export const CustomGroupsItemsAndToggle = makeGroupedExample({
|
|
|
209
209
|
template: template(
|
|
210
210
|
`
|
|
211
211
|
<template #toggle>
|
|
212
|
+
<button class="gl-rounded-base gl-border-none gl-p-2 gl-bg-gray-50 ">
|
|
212
213
|
<span class="gl-sr-only">
|
|
213
214
|
Orange Fox user's menu
|
|
214
215
|
</span>
|
|
215
|
-
<gl-avatar :size="32" entity-name="Orange Fox" aria-hidden="true"
|
|
216
|
+
<gl-avatar :size="32" entity-name="Orange Fox" aria-hidden="true"/>
|
|
217
|
+
</button>
|
|
216
218
|
</template>
|
|
217
219
|
<gl-disclosure-dropdown-group>
|
|
218
220
|
<gl-disclosure-dropdown-item>
|
|
@@ -289,7 +289,12 @@ export const CustomToggle = (args, { argTypes }) => ({
|
|
|
289
289
|
template: template(
|
|
290
290
|
`
|
|
291
291
|
<template #toggle>
|
|
292
|
-
|
|
292
|
+
<button class="gl-rounded-base gl-border-none gl-p-2 gl-bg-gray-50 ">
|
|
293
|
+
<span class="gl-sr-only">
|
|
294
|
+
{{selected}}
|
|
295
|
+
</span>
|
|
296
|
+
<gl-avatar :size="32" :entity-name="selected" aria-hidden="true"/>
|
|
297
|
+
</button>
|
|
293
298
|
</template>
|
|
294
299
|
<template #list-item="{ item }">
|
|
295
300
|
<span class="gl-display-flex gl-align-items-center">
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
### ECharts Wrapper
|
|
2
2
|
|
|
3
|
-
The chart component is a Vue component wrapper around
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
by default.
|
|
3
|
+
The chart component is a Vue component wrapper around [Apache ECharts](https://echarts.apache.org/en/api.html#echarts).
|
|
4
|
+
The chart component accepts width and height props in order to allow the user to make it responsive,
|
|
5
|
+
but it is not responsive by default.
|
|
7
6
|
|
|
8
|
-
> Note:
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
> Note: When implementing a chart type that does not already have a GitLab UI component, you can use
|
|
8
|
+
> this component alonside the [ECharts options](https://echarts.apache.org/en/api.html#echarts) to
|
|
9
|
+
> build your chart. Each type of chart should still follow the general guidelines in the
|
|
10
|
+
> [pajamas documentation](https://design.gitlab.com/data-visualization/charts).
|
|
11
11
|
|
|
12
12
|
### EChart Lifecycle
|
|
13
13
|
|
package/src/utils/utils.js
CHANGED
|
@@ -92,6 +92,19 @@ export function isElementFocusable(elt) {
|
|
|
92
92
|
return isValidTag && hasValidType && !isDisabled && hasValidZIndex && !isInvalidAnchorTag;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Receives an element and validates that it is reachable via sequential keyboard navigation
|
|
97
|
+
* @param { HTMLElement } The element to validate
|
|
98
|
+
* @return { boolean } Is the element focusable in a sequential tab order
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
export function isElementTabbable(el) {
|
|
102
|
+
if (!el) return false;
|
|
103
|
+
|
|
104
|
+
const tabindex = parseInt(el.getAttribute('tabindex'), 10);
|
|
105
|
+
return tabindex > -1;
|
|
106
|
+
}
|
|
107
|
+
|
|
95
108
|
/**
|
|
96
109
|
* Receives an array of HTML elements and focus the first one possible
|
|
97
110
|
* @param { Array.<HTMLElement> } An array of element to potentially focus
|
package/src/utils/utils.spec.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isElementFocusable,
|
|
3
|
+
isElementTabbable,
|
|
4
|
+
focusFirstFocusableElement,
|
|
5
|
+
stopEvent,
|
|
6
|
+
} from './utils';
|
|
2
7
|
|
|
3
8
|
describe('isElementFocusable', () => {
|
|
4
9
|
const myBtn = () => document.querySelector('button');
|
|
@@ -53,6 +58,38 @@ describe('isElementFocusable', () => {
|
|
|
53
58
|
});
|
|
54
59
|
});
|
|
55
60
|
|
|
61
|
+
describe('isElementTabbable', () => {
|
|
62
|
+
const myDiv = () => document.querySelector('div');
|
|
63
|
+
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
document.body.innerHTML = '';
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should return false for a div without tabindex', () => {
|
|
69
|
+
document.body.innerHTML = '<div> Fake button </div>';
|
|
70
|
+
|
|
71
|
+
expect(isElementTabbable(myDiv())).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should return false for a div with a tabindex less than 0', () => {
|
|
75
|
+
document.body.innerHTML = '<div tabindex="-1"> Fake button </div>';
|
|
76
|
+
|
|
77
|
+
expect(isElementTabbable(myDiv())).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should return true for a div with a tabindex equal to 0', () => {
|
|
81
|
+
document.body.innerHTML = '<div tabindex="0"> Fake button </div>';
|
|
82
|
+
|
|
83
|
+
expect(isElementTabbable(myDiv())).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should return true for a div with a tabindex greater than 0', () => {
|
|
87
|
+
document.body.innerHTML = '<div tabindex="0"> Fake button </div>';
|
|
88
|
+
|
|
89
|
+
expect(isElementTabbable(myDiv())).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
56
93
|
describe('focusFirstFocusableElement', () => {
|
|
57
94
|
const myBtn = () => document.querySelector('button');
|
|
58
95
|
const myInput = () => document.querySelector('input');
|