@gitlab/ui 138.0.0 → 138.2.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/README.md +3 -12
- package/dist/components/base/filtered_search/filtered_search_suggestion_list.js +1 -1
- package/dist/components/base/form/form_select/form_select.js +322 -6
- package/dist/components/base/sorting/sorting.js +27 -5
- package/dist/components/base/toggle/toggle.js +24 -1
- package/dist/components/charts/chart/chart.js +22 -0
- package/dist/config.js +14 -2
- package/dist/vendor/bootstrap-vue/src/constants/components.js +1 -4
- package/package.json +3 -11
- package/src/components/base/filtered_search/filtered_search_suggestion_list.vue +2 -1
- package/src/components/base/form/form_select/form_select.vue +389 -9
- package/src/components/base/sorting/sorting.vue +29 -4
- package/src/components/base/toggle/toggle.vue +25 -2
- package/src/components/charts/chart/chart.vue +18 -0
- package/src/config.js +16 -1
- package/src/vendor/bootstrap-vue/src/constants/components.js +0 -3
- package/translations.js +2 -2
- package/dist/vendor/bootstrap-vue/src/components/form-select/form-select-option-group.js +0 -51
- package/dist/vendor/bootstrap-vue/src/components/form-select/form-select-option.js +0 -41
- package/dist/vendor/bootstrap-vue/src/components/form-select/form-select.js +0 -153
- package/dist/vendor/bootstrap-vue/src/components/form-select/helpers/mixin-options.js +0 -63
- package/dist/vendor/bootstrap-vue/src/components/form-select/index.js +0 -3
- package/src/vendor/bootstrap-vue/src/components/form-select/form-select-option-group.js +0 -46
- package/src/vendor/bootstrap-vue/src/components/form-select/form-select-option.js +0 -35
- package/src/vendor/bootstrap-vue/src/components/form-select/form-select.js +0 -156
- package/src/vendor/bootstrap-vue/src/components/form-select/helpers/mixin-options.js +0 -60
- package/src/vendor/bootstrap-vue/src/components/form-select/index.js +0 -5
|
@@ -117,7 +117,8 @@ export default {
|
|
|
117
117
|
</script>
|
|
118
118
|
<template>
|
|
119
119
|
<ul :class="listClasses">
|
|
120
|
-
|
|
120
|
+
<!-- Sentinel used by stepItem() to reset scroll position; not a real suggestion. -->
|
|
121
|
+
<li ref="top" aria-hidden="true" role="presentation"></li>
|
|
121
122
|
<!-- @slot The suggestions (implemented with GlFilteredSearchSuggestion). -->
|
|
122
123
|
<slot></slot>
|
|
123
124
|
</ul>
|
|
@@ -1,17 +1,185 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import { isObject } from 'lodash-es';
|
|
3
|
-
import { BFormSelect } from '../../../../vendor/bootstrap-vue/src/components/form-select/form-select';
|
|
2
|
+
import { isObject, uniqueId, isPlainObject, isBoolean, isUndefined, isNull, get } from 'lodash-es';
|
|
4
3
|
import { formInputWidths } from '../../../../utils/constants';
|
|
4
|
+
import { isVisible } from '../../../../utils/utils';
|
|
5
|
+
import { SafeHtmlDirective as SafeHtml } from '../../../../directives/safe_html/safe_html';
|
|
5
6
|
|
|
6
7
|
export default {
|
|
7
8
|
name: 'GlFormSelect',
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
directives: {
|
|
10
|
+
SafeHtml,
|
|
10
11
|
},
|
|
11
12
|
inheritAttrs: false,
|
|
12
13
|
props: {
|
|
13
14
|
/**
|
|
14
|
-
*
|
|
15
|
+
* Current value of the select. Should be set to an array when the 'multiple' prop is set
|
|
16
|
+
*/
|
|
17
|
+
value: {
|
|
18
|
+
// Value can be any type!
|
|
19
|
+
type: undefined,
|
|
20
|
+
required: false,
|
|
21
|
+
default: undefined,
|
|
22
|
+
},
|
|
23
|
+
/**
|
|
24
|
+
* Array of items to render in the component
|
|
25
|
+
*/
|
|
26
|
+
options: {
|
|
27
|
+
type: [Array, Object],
|
|
28
|
+
required: false,
|
|
29
|
+
default: undefined,
|
|
30
|
+
},
|
|
31
|
+
/**
|
|
32
|
+
* When set to a number larger than 0, will set the number of display option rows. Note not all browser will respect this setting
|
|
33
|
+
* @deprecated Only useful with `multiple`, which is also deprecated.
|
|
34
|
+
*/
|
|
35
|
+
selectSize: {
|
|
36
|
+
type: Number,
|
|
37
|
+
required: false,
|
|
38
|
+
default: 0,
|
|
39
|
+
},
|
|
40
|
+
/**
|
|
41
|
+
* Used to set the `id` attribute on the rendered content, and used as the base to generate any additional element IDs as needed
|
|
42
|
+
*/
|
|
43
|
+
id: {
|
|
44
|
+
type: String,
|
|
45
|
+
required: false,
|
|
46
|
+
default: undefined,
|
|
47
|
+
},
|
|
48
|
+
/**
|
|
49
|
+
* Render the form control in plain mode, rather than custom styled mode
|
|
50
|
+
* @deprecated Use a native <select> element instead.
|
|
51
|
+
*/
|
|
52
|
+
plain: {
|
|
53
|
+
type: Boolean,
|
|
54
|
+
required: false,
|
|
55
|
+
default: false,
|
|
56
|
+
},
|
|
57
|
+
/**
|
|
58
|
+
* Set the size of the component's appearance. 'sm', 'md' (default), or 'lg'
|
|
59
|
+
* @deprecated Has no effect. Use the `width` prop instead.
|
|
60
|
+
*/
|
|
61
|
+
size: {
|
|
62
|
+
type: String,
|
|
63
|
+
required: false,
|
|
64
|
+
default: undefined,
|
|
65
|
+
},
|
|
66
|
+
/**
|
|
67
|
+
* Optional value to set for the 'aria-invalid' attribute. Supported values are 'true' and 'false'. If not set, the 'state' prop will dictate the value
|
|
68
|
+
*/
|
|
69
|
+
ariaInvalid: {
|
|
70
|
+
type: [Boolean, String],
|
|
71
|
+
required: false,
|
|
72
|
+
default: false,
|
|
73
|
+
},
|
|
74
|
+
/**
|
|
75
|
+
* Controls the validation state appearance of the component. `true` for valid, `false` for invalid, or `null` for no validation state
|
|
76
|
+
*/
|
|
77
|
+
state: {
|
|
78
|
+
// Tri-state prop: true, false, null (or undefined)
|
|
79
|
+
type: Boolean,
|
|
80
|
+
required: false,
|
|
81
|
+
default: null,
|
|
82
|
+
},
|
|
83
|
+
/**
|
|
84
|
+
* Field name in the `options` array that should be used for the value
|
|
85
|
+
*/
|
|
86
|
+
valueField: {
|
|
87
|
+
type: String,
|
|
88
|
+
required: false,
|
|
89
|
+
default: 'value',
|
|
90
|
+
},
|
|
91
|
+
/**
|
|
92
|
+
* The key to use from the option object to get the label
|
|
93
|
+
*/
|
|
94
|
+
labelField: {
|
|
95
|
+
type: String,
|
|
96
|
+
required: false,
|
|
97
|
+
default: 'label',
|
|
98
|
+
},
|
|
99
|
+
/**
|
|
100
|
+
* The key to use from the option object to get the options
|
|
101
|
+
*/
|
|
102
|
+
optionsField: {
|
|
103
|
+
type: String,
|
|
104
|
+
required: false,
|
|
105
|
+
default: 'options',
|
|
106
|
+
},
|
|
107
|
+
/**
|
|
108
|
+
* Field name in the `options` array that should be used for the text label
|
|
109
|
+
*/
|
|
110
|
+
textField: {
|
|
111
|
+
type: String,
|
|
112
|
+
required: false,
|
|
113
|
+
default: 'text',
|
|
114
|
+
},
|
|
115
|
+
/**
|
|
116
|
+
* Field name in the `options` array that should be used for the disabled state
|
|
117
|
+
*/
|
|
118
|
+
disabledField: {
|
|
119
|
+
type: String,
|
|
120
|
+
required: false,
|
|
121
|
+
default: 'disabled',
|
|
122
|
+
},
|
|
123
|
+
/**
|
|
124
|
+
* Field name in the `options` array that should be used for the html label instead of text field
|
|
125
|
+
* @deprecated Using the `.html` field (whatever its name) is potentially unsafe.
|
|
126
|
+
*/
|
|
127
|
+
htmlField: {
|
|
128
|
+
type: String,
|
|
129
|
+
required: false,
|
|
130
|
+
default: 'html',
|
|
131
|
+
},
|
|
132
|
+
/**
|
|
133
|
+
* When set, allows multiple options to be selected (multi-select)
|
|
134
|
+
* @deprecated Use GlCollapsibleListbox instead.
|
|
135
|
+
*/
|
|
136
|
+
multiple: {
|
|
137
|
+
type: Boolean,
|
|
138
|
+
required: false,
|
|
139
|
+
default: false,
|
|
140
|
+
},
|
|
141
|
+
/**
|
|
142
|
+
* When set to `true`, attempts to auto-focus the control when it is mounted, or re-activated when in a keep-alive. Does not set the `autofocus` attribute on the control
|
|
143
|
+
*/
|
|
144
|
+
autofocus: {
|
|
145
|
+
type: Boolean,
|
|
146
|
+
required: false,
|
|
147
|
+
default: false,
|
|
148
|
+
},
|
|
149
|
+
/**
|
|
150
|
+
* When set to `true`, disables the component's functionality and places it in a disabled state
|
|
151
|
+
*/
|
|
152
|
+
disabled: {
|
|
153
|
+
type: Boolean,
|
|
154
|
+
required: false,
|
|
155
|
+
default: false,
|
|
156
|
+
},
|
|
157
|
+
/**
|
|
158
|
+
* ID of the form that the form control belongs to. Sets the `form` attribute on the control
|
|
159
|
+
*/
|
|
160
|
+
form: {
|
|
161
|
+
type: String,
|
|
162
|
+
required: false,
|
|
163
|
+
default: undefined,
|
|
164
|
+
},
|
|
165
|
+
/**
|
|
166
|
+
* Sets the value of the `name` attribute on the form control
|
|
167
|
+
*/
|
|
168
|
+
name: {
|
|
169
|
+
type: String,
|
|
170
|
+
required: false,
|
|
171
|
+
default: undefined,
|
|
172
|
+
},
|
|
173
|
+
/**
|
|
174
|
+
* Adds the `required` attribute to the form control
|
|
175
|
+
*/
|
|
176
|
+
required: {
|
|
177
|
+
type: Boolean,
|
|
178
|
+
required: false,
|
|
179
|
+
default: false,
|
|
180
|
+
},
|
|
181
|
+
/**
|
|
182
|
+
* Maximum width of the Select
|
|
15
183
|
*/
|
|
16
184
|
width: {
|
|
17
185
|
type: [String, Object],
|
|
@@ -32,7 +200,32 @@ export default {
|
|
|
32
200
|
default: '',
|
|
33
201
|
},
|
|
34
202
|
},
|
|
203
|
+
data() {
|
|
204
|
+
return {
|
|
205
|
+
localValue: this.value,
|
|
206
|
+
localId: null,
|
|
207
|
+
};
|
|
208
|
+
},
|
|
35
209
|
computed: {
|
|
210
|
+
computedId() {
|
|
211
|
+
return this.id || this.localId;
|
|
212
|
+
},
|
|
213
|
+
computedSelectSize() {
|
|
214
|
+
// Custom selects with a size of zero causes the arrows to be hidden,
|
|
215
|
+
// so dont render the size attribute in this case
|
|
216
|
+
return !this.plain && this.selectSize === 0 ? null : this.selectSize;
|
|
217
|
+
},
|
|
218
|
+
computedAriaInvalid() {
|
|
219
|
+
const { ariaInvalid } = this;
|
|
220
|
+
if (ariaInvalid === true || ariaInvalid === 'true' || ariaInvalid === '') {
|
|
221
|
+
return 'true';
|
|
222
|
+
}
|
|
223
|
+
return this.computedState === false ? 'true' : ariaInvalid;
|
|
224
|
+
},
|
|
225
|
+
computedState() {
|
|
226
|
+
// If not a boolean, ensure that value is null
|
|
227
|
+
return isBoolean(this.state) ? this.state : null;
|
|
228
|
+
},
|
|
36
229
|
cssClasses() {
|
|
37
230
|
if (this.width === null) {
|
|
38
231
|
return [];
|
|
@@ -54,15 +247,202 @@ export default {
|
|
|
54
247
|
// eslint-disable-next-line @gitlab/tailwind-no-interpolation -- Not a CSS utility
|
|
55
248
|
return [`gl-form-select-${this.width}`];
|
|
56
249
|
},
|
|
250
|
+
stateClass() {
|
|
251
|
+
if (this.computedState === true) return 'is-valid';
|
|
252
|
+
if (this.computedState === false) return 'is-invalid';
|
|
253
|
+
return null;
|
|
254
|
+
},
|
|
255
|
+
inputClass() {
|
|
256
|
+
return [
|
|
257
|
+
'gl-form-select',
|
|
258
|
+
this.plain ? 'form-control' : 'custom-select',
|
|
259
|
+
this.size && this.plain ? `form-control-${this.size}` : null,
|
|
260
|
+
this.size && !this.plain ? `custom-select-${this.size}` : null,
|
|
261
|
+
this.stateClass,
|
|
262
|
+
this.selectClass,
|
|
263
|
+
];
|
|
264
|
+
},
|
|
265
|
+
normalizedOptions() {
|
|
266
|
+
return this.normalizeOptions(this.options);
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
watch: {
|
|
270
|
+
value(newValue) {
|
|
271
|
+
this.localValue = newValue;
|
|
272
|
+
},
|
|
273
|
+
localValue(newValue) {
|
|
274
|
+
/**
|
|
275
|
+
* Emitted when the select value changes
|
|
276
|
+
*
|
|
277
|
+
* @event input
|
|
278
|
+
* @type {any}
|
|
279
|
+
*/
|
|
280
|
+
this.$emit('input', newValue);
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
activated() {
|
|
284
|
+
this.handleAutofocus();
|
|
285
|
+
},
|
|
286
|
+
async mounted() {
|
|
287
|
+
this.handleAutofocus();
|
|
288
|
+
await this.$nextTick();
|
|
289
|
+
// Update DOM with auto-generated ID after mount
|
|
290
|
+
// to prevent SSR hydration errors
|
|
291
|
+
this.localId = uniqueId('gl-form-select-');
|
|
292
|
+
},
|
|
293
|
+
methods: {
|
|
294
|
+
focus() {
|
|
295
|
+
this.$refs.select?.focus();
|
|
296
|
+
},
|
|
297
|
+
blur() {
|
|
298
|
+
this.$refs.select?.blur();
|
|
299
|
+
},
|
|
300
|
+
normalizeOptions(options) {
|
|
301
|
+
// Normalize the given options array
|
|
302
|
+
if (Array.isArray(options)) return options.map((option) => this.normalizeOption(option));
|
|
303
|
+
|
|
304
|
+
if (isPlainObject(options)) {
|
|
305
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
306
|
+
// Deprecate the object options format
|
|
307
|
+
// eslint-disable-next-line no-console
|
|
308
|
+
console.warn(
|
|
309
|
+
'[@gitlab/ui] GlFormSelect - Setting prop "options" to an object is deprecated. Use the array format instead.',
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Normalize a `options` object to an array of options
|
|
314
|
+
return Object.keys(options).map((key) => this.normalizeOption(options[key] || {}, key));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// If not an array or object, return an empty array
|
|
318
|
+
return [];
|
|
319
|
+
},
|
|
320
|
+
normalizeOption(option, key = null) {
|
|
321
|
+
// When the option is an object, normalize it
|
|
322
|
+
if (isPlainObject(option)) {
|
|
323
|
+
const value = get(option, this.valueField);
|
|
324
|
+
const text = get(option, this.textField);
|
|
325
|
+
const options = get(option, this.optionsField, null);
|
|
326
|
+
// When it has options, create an <optgroup> object
|
|
327
|
+
if (!isNull(options)) {
|
|
328
|
+
return {
|
|
329
|
+
label: String(get(option, this.labelField) || text),
|
|
330
|
+
options: this.normalizeOptions(options),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
// Otherwise create an <option> object
|
|
334
|
+
return {
|
|
335
|
+
value: isUndefined(value) ? key || text : value,
|
|
336
|
+
text: String(isUndefined(text) ? key : text),
|
|
337
|
+
html: get(option, this.htmlField),
|
|
338
|
+
disabled: Boolean(get(option, this.disabledField)),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
// Otherwise create an <option> object from the given value
|
|
342
|
+
return {
|
|
343
|
+
value: key || option,
|
|
344
|
+
text: String(option),
|
|
345
|
+
disabled: false,
|
|
346
|
+
};
|
|
347
|
+
},
|
|
348
|
+
isOptionGroup(option) {
|
|
349
|
+
return Array.isArray(option.options);
|
|
350
|
+
},
|
|
351
|
+
async onChange(event) {
|
|
352
|
+
const { target } = event;
|
|
353
|
+
const selectedValue = Array.from(target.options)
|
|
354
|
+
.filter((option) => option.selected)
|
|
355
|
+
// NOTE: the _value property is set up by both Vue 2 and 3 explicitly for the
|
|
356
|
+
// purpose of handling non-string values on form elements.
|
|
357
|
+
//
|
|
358
|
+
// See:
|
|
359
|
+
// - https://github.com/vuejs/vue/blob/v2.7.16/src/platforms/web/runtime/modules/dom-props.ts#L42-L44
|
|
360
|
+
// - https://github.com/vuejs/vue/blob/v2.7.16/test/unit/modules/vdom/modules/dom-props.spec.ts#L38
|
|
361
|
+
// - https://github.com/vuejs/core/blob/v3.4.27/packages/runtime-dom/src/modules/props.ts#L48-L50
|
|
362
|
+
// - https://github.com/vuejs/core/blob/v3.4.27/packages/runtime-dom/__tests__/patchProps.spec.ts#L25
|
|
363
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
364
|
+
.map((option) => ('_value' in option ? option._value : option.value));
|
|
365
|
+
|
|
366
|
+
this.localValue = target.multiple ? selectedValue : selectedValue[0];
|
|
367
|
+
|
|
368
|
+
await this.$nextTick();
|
|
369
|
+
/**
|
|
370
|
+
* Emitted when the select value changes via user interaction
|
|
371
|
+
*
|
|
372
|
+
* @event change
|
|
373
|
+
* @type {any}
|
|
374
|
+
*/
|
|
375
|
+
this.$emit('change', this.localValue);
|
|
376
|
+
},
|
|
377
|
+
async handleAutofocus() {
|
|
378
|
+
await this.$nextTick();
|
|
379
|
+
window.requestAnimationFrame(() => {
|
|
380
|
+
if (this.autofocus && isVisible(this.$refs.select)) this.focus();
|
|
381
|
+
});
|
|
382
|
+
},
|
|
57
383
|
},
|
|
58
384
|
};
|
|
59
385
|
</script>
|
|
60
386
|
<template>
|
|
61
387
|
<span class="gl-form-select-wrapper" :class="cssClasses">
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
388
|
+
<select
|
|
389
|
+
:id="computedId"
|
|
390
|
+
ref="select"
|
|
391
|
+
v-model="localValue"
|
|
392
|
+
:aria-invalid="computedAriaInvalid"
|
|
393
|
+
:aria-required="required ? 'true' : null"
|
|
394
|
+
:class="inputClass"
|
|
395
|
+
:disabled="disabled"
|
|
396
|
+
:form="form"
|
|
397
|
+
:multiple="multiple"
|
|
398
|
+
:name="name"
|
|
399
|
+
:required="required"
|
|
400
|
+
:size="computedSelectSize"
|
|
401
|
+
v-bind="$attrs"
|
|
402
|
+
@change="onChange"
|
|
403
|
+
>
|
|
404
|
+
<!-- @slot Slot to place options or option groups above options provided via the 'options' prop -->
|
|
405
|
+
<slot name="first"></slot>
|
|
406
|
+
|
|
407
|
+
<template v-for="(option, index) of normalizedOptions">
|
|
408
|
+
<template v-if="isOptionGroup(option)">
|
|
409
|
+
<optgroup :key="index" :label="option.label">
|
|
410
|
+
<template v-for="(childOption, childIndex) of option.options">
|
|
411
|
+
<option
|
|
412
|
+
v-if="childOption.html"
|
|
413
|
+
:key="`${childIndex}-html`"
|
|
414
|
+
v-safe-html="childOption.html"
|
|
415
|
+
:value="childOption.value"
|
|
416
|
+
:disabled="childOption.disabled"
|
|
417
|
+
></option>
|
|
418
|
+
<option
|
|
419
|
+
v-else
|
|
420
|
+
:key="childIndex"
|
|
421
|
+
:value="childOption.value"
|
|
422
|
+
:disabled="childOption.disabled"
|
|
423
|
+
>
|
|
424
|
+
{{ childOption.text }}
|
|
425
|
+
</option>
|
|
426
|
+
</template>
|
|
427
|
+
</optgroup>
|
|
428
|
+
</template>
|
|
429
|
+
|
|
430
|
+
<template v-else>
|
|
431
|
+
<option
|
|
432
|
+
v-if="option.html"
|
|
433
|
+
:key="`${index}-html`"
|
|
434
|
+
v-safe-html="option.html"
|
|
435
|
+
:value="option.value"
|
|
436
|
+
:disabled="option.disabled"
|
|
437
|
+
></option>
|
|
438
|
+
<option v-else :key="index" :value="option.value" :disabled="option.disabled">
|
|
439
|
+
{{ option.text }}
|
|
440
|
+
</option>
|
|
441
|
+
</template>
|
|
65
442
|
</template>
|
|
66
|
-
|
|
443
|
+
|
|
444
|
+
<!-- @slot Content to place in the form select after 'options' prop options -->
|
|
445
|
+
<slot></slot>
|
|
446
|
+
</select>
|
|
67
447
|
</span>
|
|
68
448
|
</template>
|
|
@@ -98,6 +98,15 @@ export default {
|
|
|
98
98
|
required: false,
|
|
99
99
|
default: true,
|
|
100
100
|
},
|
|
101
|
+
/**
|
|
102
|
+
* Disables the entire sorting control (both the sort-by dropdown and the
|
|
103
|
+
* sort direction toggle) while keeping it visible.
|
|
104
|
+
*/
|
|
105
|
+
disabled: {
|
|
106
|
+
type: Boolean,
|
|
107
|
+
required: false,
|
|
108
|
+
default: false,
|
|
109
|
+
},
|
|
101
110
|
},
|
|
102
111
|
data() {
|
|
103
112
|
return {
|
|
@@ -106,7 +115,14 @@ export default {
|
|
|
106
115
|
};
|
|
107
116
|
},
|
|
108
117
|
computed: {
|
|
118
|
+
unavailableLabel() {
|
|
119
|
+
return translate('GlSorting.sortingUnavailable', 'Sorting is unavailable');
|
|
120
|
+
},
|
|
109
121
|
sortingListboxLabel() {
|
|
122
|
+
if (this.disabled) {
|
|
123
|
+
return this.unavailableLabel;
|
|
124
|
+
}
|
|
125
|
+
|
|
110
126
|
return translate('GlSorting.sortByLabel', 'Sort by:');
|
|
111
127
|
},
|
|
112
128
|
localSortDirection() {
|
|
@@ -119,19 +135,19 @@ export default {
|
|
|
119
135
|
return this.selectedSortOption?.text ?? '';
|
|
120
136
|
},
|
|
121
137
|
isDirectionToggleDisabled() {
|
|
122
|
-
return Boolean(this.selectedSortOption?.directionToggleDisabled);
|
|
138
|
+
return this.disabled || Boolean(this.selectedSortOption?.directionToggleDisabled);
|
|
123
139
|
},
|
|
124
140
|
sortDirectionText() {
|
|
125
|
-
if (this.sortDirectionToolTip) return this.sortDirectionToolTip;
|
|
141
|
+
if (this.sortDirectionToolTip && !this.disabled) return this.sortDirectionToolTip;
|
|
126
142
|
|
|
127
143
|
if (this.isDirectionToggleDisabled) {
|
|
128
144
|
return this.selectedSortText
|
|
129
145
|
? translate(
|
|
130
146
|
'GlSorting.sortDirectionUnavailableFor',
|
|
131
|
-
'
|
|
147
|
+
'Sorting is unavailable for %{sortField}',
|
|
132
148
|
{ sortField: this.selectedSortText },
|
|
133
149
|
)
|
|
134
|
-
:
|
|
150
|
+
: this.unavailableLabel;
|
|
135
151
|
}
|
|
136
152
|
|
|
137
153
|
return this.isAscending
|
|
@@ -139,6 +155,11 @@ export default {
|
|
|
139
155
|
: translate('GlSorting.sortDescending', 'Sort direction: descending');
|
|
140
156
|
},
|
|
141
157
|
},
|
|
158
|
+
watch: {
|
|
159
|
+
disabled(isDisabled) {
|
|
160
|
+
if (isDisabled) this.$refs.listbox.close();
|
|
161
|
+
},
|
|
162
|
+
},
|
|
142
163
|
methods: {
|
|
143
164
|
toggleSortDirection() {
|
|
144
165
|
if (this.isDirectionToggleDisabled) return;
|
|
@@ -156,6 +177,8 @@ export default {
|
|
|
156
177
|
this.$emit('sort-direction-change', newDirection);
|
|
157
178
|
},
|
|
158
179
|
onSortByChanged(event) {
|
|
180
|
+
if (this.disabled) return;
|
|
181
|
+
|
|
159
182
|
/**
|
|
160
183
|
* Emitted when the sort field is changed.
|
|
161
184
|
*
|
|
@@ -174,6 +197,7 @@ export default {
|
|
|
174
197
|
<template>
|
|
175
198
|
<gl-button-group class="gl-sorting gl-flex">
|
|
176
199
|
<gl-collapsible-listbox
|
|
200
|
+
ref="listbox"
|
|
177
201
|
:toggle-text="text"
|
|
178
202
|
:items="sortOptions"
|
|
179
203
|
:selected="sortBy"
|
|
@@ -182,6 +206,7 @@ export default {
|
|
|
182
206
|
:class="dropdownClass"
|
|
183
207
|
placement="bottom-end"
|
|
184
208
|
:block="block"
|
|
209
|
+
:disabled="disabled"
|
|
185
210
|
:toggle-id="toggleId"
|
|
186
211
|
:toggle-aria-labelled-by="`${sortingListboxId} ${toggleId}`"
|
|
187
212
|
@select="onSortByChanged"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { uniqueId } from 'lodash-es';
|
|
3
3
|
|
|
4
|
+
import { glToggleConfig } from '../../../config';
|
|
4
5
|
import { toggleLabelPosition } from '../../../utils/constants';
|
|
5
6
|
import GlIcon from '../icon/icon.vue';
|
|
6
7
|
import GlLoadingIcon from '../loading_icon/loading_icon.vue';
|
|
@@ -43,6 +44,22 @@ export default {
|
|
|
43
44
|
required: false,
|
|
44
45
|
default: false,
|
|
45
46
|
},
|
|
47
|
+
/**
|
|
48
|
+
* Keep the toggle focusable while `disabled` is `true`. Uses `aria-disabled` so the toggle stays in the tab order and is announced by assistive technology.
|
|
49
|
+
*/
|
|
50
|
+
accessibleDisabled: {
|
|
51
|
+
type: Boolean,
|
|
52
|
+
required: false,
|
|
53
|
+
default: () => glToggleConfig.accessibleDisabledToggle,
|
|
54
|
+
},
|
|
55
|
+
/**
|
|
56
|
+
* Space-separated ids of elements that describe the toggle, in addition to its help text.
|
|
57
|
+
*/
|
|
58
|
+
ariaDescribedby: {
|
|
59
|
+
type: String,
|
|
60
|
+
required: false,
|
|
61
|
+
default: undefined,
|
|
62
|
+
},
|
|
46
63
|
/**
|
|
47
64
|
* Whether the toggle is in the loading state.
|
|
48
65
|
*/
|
|
@@ -111,6 +128,12 @@ export default {
|
|
|
111
128
|
helpId() {
|
|
112
129
|
return this.shouldRenderHelp ? `toggle-help-${this.uuid}` : undefined;
|
|
113
130
|
},
|
|
131
|
+
describedBy() {
|
|
132
|
+
return [this.helpId, this.ariaDescribedby].filter(Boolean).join(' ') || undefined;
|
|
133
|
+
},
|
|
134
|
+
isNativeDisabled() {
|
|
135
|
+
return this.disabled && !this.accessibleDisabled;
|
|
136
|
+
},
|
|
114
137
|
isChecked() {
|
|
115
138
|
return this.value ? 'true' : 'false';
|
|
116
139
|
},
|
|
@@ -172,7 +195,7 @@ export default {
|
|
|
172
195
|
role="switch"
|
|
173
196
|
:aria-checked="isChecked"
|
|
174
197
|
:aria-labelledby="labelId"
|
|
175
|
-
:aria-describedby="
|
|
198
|
+
:aria-describedby="describedBy"
|
|
176
199
|
:aria-disabled="disabled"
|
|
177
200
|
:class="{
|
|
178
201
|
'is-checked': value,
|
|
@@ -181,7 +204,7 @@ export default {
|
|
|
181
204
|
}"
|
|
182
205
|
class="gl-toggle gl-shrink-0"
|
|
183
206
|
type="button"
|
|
184
|
-
:disabled="
|
|
207
|
+
:disabled="isNativeDisabled"
|
|
185
208
|
@click.prevent="toggleFeature"
|
|
186
209
|
>
|
|
187
210
|
<gl-loading-icon v-if="isLoading" color="dark" class="toggle-loading" />
|
|
@@ -80,6 +80,15 @@ export default {
|
|
|
80
80
|
required: false,
|
|
81
81
|
default: true,
|
|
82
82
|
},
|
|
83
|
+
/**
|
|
84
|
+
* A name for the chart that screen readers announce. It is added as a hidden title, which
|
|
85
|
+
* ECharts reads into the description it generates. Ignored when `options` sets a title.
|
|
86
|
+
*/
|
|
87
|
+
accessibleTitle: {
|
|
88
|
+
type: String,
|
|
89
|
+
required: false,
|
|
90
|
+
default: '',
|
|
91
|
+
},
|
|
83
92
|
},
|
|
84
93
|
data() {
|
|
85
94
|
return {
|
|
@@ -96,6 +105,10 @@ export default {
|
|
|
96
105
|
options = merge({}, options, { aria: { enabled: true } });
|
|
97
106
|
}
|
|
98
107
|
|
|
108
|
+
if (this.accessibleTitle && !options.title) {
|
|
109
|
+
options = merge({}, options, { title: { text: this.accessibleTitle, show: false } });
|
|
110
|
+
}
|
|
111
|
+
|
|
99
112
|
// Add space at the top to fit the toolbox
|
|
100
113
|
if (options.toolbox?.show === true) {
|
|
101
114
|
const top = (options.grid?.top || 0) + toolboxHeight;
|
|
@@ -111,6 +124,11 @@ export default {
|
|
|
111
124
|
this.draw();
|
|
112
125
|
}
|
|
113
126
|
},
|
|
127
|
+
accessibleTitle() {
|
|
128
|
+
if (this.chart) {
|
|
129
|
+
this.draw();
|
|
130
|
+
}
|
|
131
|
+
},
|
|
114
132
|
width() {
|
|
115
133
|
this.setChartSize();
|
|
116
134
|
},
|
package/src/config.js
CHANGED
|
@@ -7,6 +7,7 @@ export const defaultConfig = {
|
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
export const glButtonConfig = {};
|
|
10
|
+
export const glToggleConfig = {};
|
|
10
11
|
|
|
11
12
|
let configured = false;
|
|
12
13
|
|
|
@@ -18,9 +19,15 @@ let configured = false;
|
|
|
18
19
|
* @property {undefined | Object} translations Generic translations for component labels to fall back to.
|
|
19
20
|
* @property {undefined | Number} firstDayOfWeek Configured first day of the week, from 0 (Sunday) to 6 (Saturday).
|
|
20
21
|
* @property {boolean} [accessibleDisabledButton] Temporary flag to enable the accessible disabled button.
|
|
22
|
+
* @property {boolean} [accessibleDisabledToggle] Temporary flag to enable the accessible disabled toggle.
|
|
21
23
|
*
|
|
22
24
|
*/
|
|
23
|
-
const setConfigs = ({
|
|
25
|
+
const setConfigs = ({
|
|
26
|
+
translations,
|
|
27
|
+
firstDayOfWeek,
|
|
28
|
+
accessibleDisabledButton = false,
|
|
29
|
+
accessibleDisabledToggle = false,
|
|
30
|
+
} = {}) => {
|
|
24
31
|
if (configured) {
|
|
25
32
|
if (process.env.NODE_ENV === 'development') {
|
|
26
33
|
throw new Error('GitLab UI can only be configured once!');
|
|
@@ -71,6 +78,14 @@ const setConfigs = ({ translations, firstDayOfWeek, accessibleDisabledButton = f
|
|
|
71
78
|
accessibleDisabledButton,
|
|
72
79
|
});
|
|
73
80
|
}
|
|
81
|
+
|
|
82
|
+
// Temporary flag for the accessible disabled toggle. Once validated, aria-disabled becomes
|
|
83
|
+
// the default for disabled toggles and this flag is removed.
|
|
84
|
+
if (typeof accessibleDisabledToggle === 'boolean') {
|
|
85
|
+
Object.assign(glToggleConfig, {
|
|
86
|
+
accessibleDisabledToggle,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
74
89
|
};
|
|
75
90
|
|
|
76
91
|
export default setConfigs;
|
|
@@ -14,9 +14,6 @@ export const NAME_FORM = 'BForm'
|
|
|
14
14
|
export const NAME_FORM_GROUP = 'BFormGroup'
|
|
15
15
|
export const NAME_FORM_INVALID_FEEDBACK = 'BFormInvalidFeedback'
|
|
16
16
|
export const NAME_FORM_ROW = 'BFormRow'
|
|
17
|
-
export const NAME_FORM_SELECT = 'BFormSelect'
|
|
18
|
-
export const NAME_FORM_SELECT_OPTION = 'BFormSelectOption'
|
|
19
|
-
export const NAME_FORM_SELECT_OPTION_GROUP = 'BFormSelectOptionGroup'
|
|
20
17
|
export const NAME_FORM_TEXT = 'BFormText'
|
|
21
18
|
export const NAME_FORM_VALID_FEEDBACK = 'BFormValidFeedback'
|
|
22
19
|
export const NAME_LINK = 'BLink'
|
package/translations.js
CHANGED
|
@@ -43,8 +43,8 @@ export default {
|
|
|
43
43
|
'GlSorting.sortAscending': 'Sort direction: ascending',
|
|
44
44
|
'GlSorting.sortByLabel': 'Sort by:',
|
|
45
45
|
'GlSorting.sortDescending': 'Sort direction: descending',
|
|
46
|
-
'GlSorting.
|
|
47
|
-
'GlSorting.
|
|
46
|
+
'GlSorting.sortDirectionUnavailableFor': 'Sorting is unavailable for %{sortField}',
|
|
47
|
+
'GlSorting.sortingUnavailable': 'Sorting is unavailable',
|
|
48
48
|
'GlTabs.GlScrollableTabs.scrollLeftLabel': 'Scroll left',
|
|
49
49
|
'GlTabs.GlScrollableTabs.scrollRightLabel': 'Scroll right',
|
|
50
50
|
'GlToken.closeButtonTitle': 'Remove',
|