@brightspace-ui/core 2.162.0 → 2.163.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/components/inputs/input-color.js +9 -22
- package/components/tag-list/tag-list-item-mixin.js +12 -17
- package/helpers/error.js +27 -0
- package/mixins/labelled/labelled-mixin.js +16 -32
- package/mixins/property-required/README.md +105 -0
- package/mixins/property-required/property-required-mixin.js +118 -0
- package/package.json +1 -1
@@ -11,6 +11,7 @@ import { getValidHexColor } from '../../helpers/color.js';
|
|
11
11
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
12
12
|
import { inputLabelStyles } from './input-label-styles.js';
|
13
13
|
import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
|
14
|
+
import { PropertyRequiredMixin } from '../../mixins/property-required/property-required-mixin.js';
|
14
15
|
import { styleMap } from 'lit/directives/style-map.js';
|
15
16
|
|
16
17
|
const DEFAULT_VALUE = '#000000';
|
@@ -80,7 +81,7 @@ const SWATCH_TRANSPARENT = `<svg xmlns="http://www.w3.org/2000/svg" width="24" h
|
|
80
81
|
* This component allows for inputting a HEX color value.
|
81
82
|
* @fires change - Dispatched when an alteration to the value is committed by the user.
|
82
83
|
*/
|
83
|
-
class InputColor extends FocusMixin(FormElementMixin(LocalizeCoreElement(LitElement))) {
|
84
|
+
class InputColor extends PropertyRequiredMixin(FocusMixin(FormElementMixin(LocalizeCoreElement(LitElement)))) {
|
84
85
|
|
85
86
|
static get properties() {
|
86
87
|
return {
|
@@ -103,7 +104,13 @@ class InputColor extends FocusMixin(FormElementMixin(LocalizeCoreElement(LitElem
|
|
103
104
|
* REQUIRED: Label for the input, comes with a default value for background & foreground types.
|
104
105
|
* @type {string}
|
105
106
|
*/
|
106
|
-
label: {
|
107
|
+
label: {
|
108
|
+
type: String,
|
109
|
+
required: {
|
110
|
+
dependentProps: ['type'],
|
111
|
+
validator: (_value, elem, hasValue) => elem.type !== 'custom' || hasValue
|
112
|
+
}
|
113
|
+
},
|
107
114
|
/**
|
108
115
|
* Hides the label visually
|
109
116
|
* @type {boolean}
|
@@ -231,7 +238,6 @@ class InputColor extends FocusMixin(FormElementMixin(LocalizeCoreElement(LitElem
|
|
231
238
|
this._associatedValue = undefined;
|
232
239
|
this._missingLabelErrorHasBeenThrown = false;
|
233
240
|
this._opened = false;
|
234
|
-
this._validatingLabelTimeout = null;
|
235
241
|
this._value = undefined;
|
236
242
|
}
|
237
243
|
|
@@ -258,11 +264,6 @@ class InputColor extends FocusMixin(FormElementMixin(LocalizeCoreElement(LitElem
|
|
258
264
|
return '#opener';
|
259
265
|
}
|
260
266
|
|
261
|
-
firstUpdated(changedProperties) {
|
262
|
-
super.firstUpdated(changedProperties);
|
263
|
-
this._validateLabel();
|
264
|
-
}
|
265
|
-
|
266
267
|
render() {
|
267
268
|
|
268
269
|
const label = !this.labelHidden ? html`<div class="d2l-input-label">${this._getLabel()}</div>` : nothing;
|
@@ -277,8 +278,6 @@ class InputColor extends FocusMixin(FormElementMixin(LocalizeCoreElement(LitElem
|
|
277
278
|
|
278
279
|
super.updated(changedProperties);
|
279
280
|
|
280
|
-
if (changedProperties.has('label') || changedProperties.has('type')) this._validateLabel();
|
281
|
-
|
282
281
|
if (changedProperties.has('value') || changedProperties.has('type') || changedProperties.has('disallowNone')) {
|
283
282
|
this.setFormValue(this.value);
|
284
283
|
}
|
@@ -397,17 +396,5 @@ class InputColor extends FocusMixin(FormElementMixin(LocalizeCoreElement(LitElem
|
|
397
396
|
));
|
398
397
|
}
|
399
398
|
|
400
|
-
_validateLabel() {
|
401
|
-
clearTimeout(this._validatingLabelTimeout);
|
402
|
-
// don't error immediately in case it doesn't get set immediately
|
403
|
-
this._validatingLabelTimeout = setTimeout(() => {
|
404
|
-
this._validatingLabelTimeout = null;
|
405
|
-
const hasLabel = (typeof this.label === 'string') && this.label.length > 0;
|
406
|
-
if (!hasLabel && this.type === 'custom') {
|
407
|
-
throw new Error('<d2l-input-color>: "label" attribute is required when "type" is "custom"');
|
408
|
-
}
|
409
|
-
}, 3000);
|
410
|
-
}
|
411
|
-
|
412
399
|
}
|
413
400
|
customElements.define('d2l-input-color', InputColor);
|
@@ -9,6 +9,7 @@ import { classMap } from 'lit/directives/class-map.js';
|
|
9
9
|
import { getUniqueId } from '../../helpers/uniqueId.js';
|
10
10
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
11
11
|
import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
|
12
|
+
import { PropertyRequiredMixin } from '../../mixins/property-required/property-required-mixin.js';
|
12
13
|
import { RtlMixin } from '../../mixins/rtl/rtl-mixin.js';
|
13
14
|
|
14
15
|
const keyCodes = {
|
@@ -18,7 +19,7 @@ const keyCodes = {
|
|
18
19
|
SPACE: 32
|
19
20
|
};
|
20
21
|
|
21
|
-
export const TagListItemMixin = superclass => class extends LocalizeCoreElement(RtlMixin(superclass)) {
|
22
|
+
export const TagListItemMixin = superclass => class extends LocalizeCoreElement(PropertyRequiredMixin(RtlMixin(superclass))) {
|
22
23
|
|
23
24
|
static get properties() {
|
24
25
|
return {
|
@@ -39,7 +40,16 @@ export const TagListItemMixin = superclass => class extends LocalizeCoreElement(
|
|
39
40
|
/**
|
40
41
|
* @ignore
|
41
42
|
*/
|
42
|
-
keyboardTooltipShown: { type: Boolean, attribute: 'keyboard-tooltip-shown' }
|
43
|
+
keyboardTooltipShown: { type: Boolean, attribute: 'keyboard-tooltip-shown' },
|
44
|
+
/**
|
45
|
+
* @ignore
|
46
|
+
*/
|
47
|
+
_plainText: {
|
48
|
+
state: true,
|
49
|
+
required: {
|
50
|
+
message: (_value, elem) => `TagListItemMixin: "${elem.tagName.toLowerCase()}" called "_renderTag()" with empty "plainText" option`
|
51
|
+
}
|
52
|
+
}
|
43
53
|
};
|
44
54
|
}
|
45
55
|
|
@@ -136,7 +146,6 @@ export const TagListItemMixin = superclass => class extends LocalizeCoreElement(
|
|
136
146
|
this.keyboardTooltipShown = false;
|
137
147
|
this._id = getUniqueId();
|
138
148
|
this._plainText = '';
|
139
|
-
this._validatingPlainTextTimeout = null;
|
140
149
|
}
|
141
150
|
|
142
151
|
firstUpdated(changedProperties) {
|
@@ -217,7 +226,6 @@ export const TagListItemMixin = superclass => class extends LocalizeCoreElement(
|
|
217
226
|
}
|
218
227
|
|
219
228
|
_renderTag(tagContent, options = {}) {
|
220
|
-
this._validatePlainText();
|
221
229
|
this._plainText = options.plainText || '';
|
222
230
|
|
223
231
|
const buttonText = this.localize('components.tag-list.clear', { value: this._plainText });
|
@@ -277,17 +285,4 @@ export const TagListItemMixin = superclass => class extends LocalizeCoreElement(
|
|
277
285
|
`;
|
278
286
|
}
|
279
287
|
|
280
|
-
_validatePlainText() {
|
281
|
-
clearTimeout(this._validatingPlainTextTimeout);
|
282
|
-
// don't error immediately in case it doesn't get set immediately
|
283
|
-
this._validatingPlainTextTimeout = setTimeout(() => {
|
284
|
-
this._validatingPlainTextTimeout = null;
|
285
|
-
if (!this.isConnected) return;
|
286
|
-
const hasPlainText = (this._plainText?.constructor === String) && this._plainText?.length > 0;
|
287
|
-
if (!hasPlainText) {
|
288
|
-
throw new Error(`TagListItemMixin: "${this.tagName.toLowerCase()}" called "_render()" with empty "plainText" option`);
|
289
|
-
}
|
290
|
-
}, 3000);
|
291
|
-
}
|
292
|
-
|
293
288
|
};
|
package/helpers/error.js
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
import { getComposedParent } from './dom.js';
|
2
|
+
|
3
|
+
function getComposedPath(elem, opts) {
|
4
|
+
|
5
|
+
if (!opts?.composedPath) return '';
|
6
|
+
|
7
|
+
const composedParents = [];
|
8
|
+
let parent = elem;
|
9
|
+
while (parent !== null && parent?.tagName?.toLowerCase() !== 'body') {
|
10
|
+
if (parent?.tagName) {
|
11
|
+
composedParents.push(parent.tagName.toLowerCase());
|
12
|
+
}
|
13
|
+
parent = getComposedParent(parent);
|
14
|
+
}
|
15
|
+
|
16
|
+
if (composedParents.length === 0) return '';
|
17
|
+
|
18
|
+
composedParents.reverse();
|
19
|
+
const path = ` Path: "${composedParents.join(' > ')}".`;
|
20
|
+
return path;
|
21
|
+
|
22
|
+
}
|
23
|
+
|
24
|
+
export function createElementErrorMessage(type, elem, message, opts) {
|
25
|
+
const path = getComposedPath(elem, opts);
|
26
|
+
return `<${elem.tagName.toLowerCase()}>: ${message}.${path} (@brightspace-ui/core:${type})`;
|
27
|
+
}
|
@@ -1,5 +1,6 @@
|
|
1
1
|
|
2
2
|
import { cssEscape } from '../../helpers/dom.js';
|
3
|
+
import { PropertyRequiredMixin } from '../property-required/property-required-mixin.js';
|
3
4
|
|
4
5
|
const getCommonAncestor = (elem1, elem2) => {
|
5
6
|
|
@@ -74,7 +75,7 @@ export const LabelMixin = superclass => class extends superclass {
|
|
74
75
|
|
75
76
|
};
|
76
77
|
|
77
|
-
export const LabelledMixin = superclass => class extends superclass {
|
78
|
+
export const LabelledMixin = superclass => class extends PropertyRequiredMixin(superclass) {
|
78
79
|
|
79
80
|
static get properties() {
|
80
81
|
return {
|
@@ -87,7 +88,20 @@ export const LabelledMixin = superclass => class extends superclass {
|
|
87
88
|
* REQUIRED: Explicitly defined label for the element
|
88
89
|
* @type {string}
|
89
90
|
*/
|
90
|
-
label: {
|
91
|
+
label: {
|
92
|
+
type: String,
|
93
|
+
required: {
|
94
|
+
message: (_value, elem, defaultMessage) => {
|
95
|
+
if (!elem.labelledBy) return defaultMessage;
|
96
|
+
return `LabelledMixin: "${elem.tagName.toLowerCase()}" is labelled-by="${elem.labelledBy}", but its label is empty`;
|
97
|
+
},
|
98
|
+
validator: (_value, elem, hasValue) => {
|
99
|
+
if (!elem.labelRequired || hasValue) return true;
|
100
|
+
if (!elem.labelledBy) return false;
|
101
|
+
return elem._labelElem !== null;
|
102
|
+
}
|
103
|
+
}
|
104
|
+
}
|
91
105
|
};
|
92
106
|
}
|
93
107
|
|
@@ -96,20 +110,12 @@ export const LabelledMixin = superclass => class extends superclass {
|
|
96
110
|
this.labelRequired = true;
|
97
111
|
this._labelElem = null;
|
98
112
|
this._missingLabelErrorHasBeenThrown = false;
|
99
|
-
this._validatingLabelTimeout = null;
|
100
|
-
}
|
101
|
-
|
102
|
-
firstUpdated(changedProperties) {
|
103
|
-
super.firstUpdated(changedProperties);
|
104
|
-
this._validateLabel(); // need to check this even if "label" isn't updated in case it's never set
|
105
113
|
}
|
106
114
|
|
107
115
|
async updated(changedProperties) {
|
108
116
|
|
109
117
|
super.updated(changedProperties);
|
110
118
|
|
111
|
-
if (changedProperties.has('label')) this._validateLabel();
|
112
|
-
|
113
119
|
if (!changedProperties.has('labelledBy')) return;
|
114
120
|
|
115
121
|
if (!this.labelledBy) {
|
@@ -201,26 +207,4 @@ export const LabelledMixin = superclass => class extends superclass {
|
|
201
207
|
|
202
208
|
}
|
203
209
|
|
204
|
-
_validateLabel() {
|
205
|
-
clearTimeout(this._validatingLabelTimeout);
|
206
|
-
// don't error immediately in case it doesn't get set immediately
|
207
|
-
this._validatingLabelTimeout = setTimeout(() => {
|
208
|
-
this._validatingLabelTimeout = null;
|
209
|
-
const hasLabel = (typeof this.label === 'string') && this.label.length > 0;
|
210
|
-
if (this.isConnected && !hasLabel) {
|
211
|
-
if (this.labelledBy) {
|
212
|
-
if (this._labelElem) {
|
213
|
-
this._throwError(
|
214
|
-
new Error(`LabelledMixin: "${this.tagName.toLowerCase()}" is labelled-by="${this.labelledBy}", but its label is empty`)
|
215
|
-
);
|
216
|
-
}
|
217
|
-
} else {
|
218
|
-
this._throwError(
|
219
|
-
new Error(`LabelledMixin: "${this.tagName.toLowerCase()}" is missing a required "label" attribute`)
|
220
|
-
);
|
221
|
-
}
|
222
|
-
}
|
223
|
-
}, 3000);
|
224
|
-
}
|
225
|
-
|
226
210
|
};
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# PropertyRequiredMixin
|
2
|
+
|
3
|
+
This mixin will make a component's property "required". If a value is not provided for a required property, an exception will be thrown asynchronously.
|
4
|
+
|
5
|
+
Required properties are useful in situations where a missing value would put the component into an invalid state, such as an accessibility violation.
|
6
|
+
|
7
|
+
Only `String` properties can be required.
|
8
|
+
|
9
|
+
## Making a Property Required
|
10
|
+
|
11
|
+
To require a property, simply set `required` to `true` in the reactive Lit property definition:
|
12
|
+
|
13
|
+
```javascript
|
14
|
+
import { PropertyRequiredMixin } from '@brightspace-ui/core/mixins/property-required/property-required-mixin.js';
|
15
|
+
|
16
|
+
class MyElem extends PropertyRequiredMixin(LitElement) {
|
17
|
+
static properties = {
|
18
|
+
prop: { type: String, required: true }
|
19
|
+
};
|
20
|
+
}
|
21
|
+
```
|
22
|
+
|
23
|
+
If a non-empty `String` value for `prop` is not provided, an exception will be thrown after a few seconds.
|
24
|
+
|
25
|
+
## Custom Validation
|
26
|
+
|
27
|
+
By default, validating a required property involves ensuring that it's a non-empty `String`. To customize this logic, set `required` to an object and provide a `validator` delegate.
|
28
|
+
|
29
|
+
```javascript
|
30
|
+
static properties = {
|
31
|
+
mustBeFoo: {
|
32
|
+
required: {
|
33
|
+
validator: (value, elem, hasValue) => {
|
34
|
+
return value === 'foo';
|
35
|
+
}
|
36
|
+
},
|
37
|
+
type: String
|
38
|
+
}
|
39
|
+
};
|
40
|
+
```
|
41
|
+
|
42
|
+
The `validator` will be passed the current property `value`, a reference to the element and the result of the default validation.
|
43
|
+
|
44
|
+
## Custom Messages
|
45
|
+
|
46
|
+
To customize the exception message that gets thrown when validation fails, set `required` to an object and provide a `message` delegate.
|
47
|
+
|
48
|
+
```javascript
|
49
|
+
static properties = {
|
50
|
+
prop: {
|
51
|
+
required: {
|
52
|
+
message: (value, elem, defaultMessage) => {
|
53
|
+
return `"prop" on "${elem.tagName}" is required.`;
|
54
|
+
}
|
55
|
+
},
|
56
|
+
type: String
|
57
|
+
}
|
58
|
+
};
|
59
|
+
```
|
60
|
+
|
61
|
+
The `message` delegate will be passed the current property `value`, a reference to the element and the default message.
|
62
|
+
|
63
|
+
## Dependent Properties
|
64
|
+
|
65
|
+
The mixin will automatically listen for updates to a required property and re-run the `validator` when the value changes. If custom validation logic relies on multiple properties, list them in `dependentProps`.
|
66
|
+
|
67
|
+
```javascript
|
68
|
+
static properties = {
|
69
|
+
prop: {
|
70
|
+
required: {
|
71
|
+
dependentProps: ['isReallyRequired']
|
72
|
+
validator: (value, elem, hasValue) => {
|
73
|
+
return elem.isReallyRequired && hasValue;
|
74
|
+
}
|
75
|
+
},
|
76
|
+
type: String
|
77
|
+
},
|
78
|
+
isReallyRequired: { type: Boolean }
|
79
|
+
};
|
80
|
+
```
|
81
|
+
|
82
|
+
## Unit Testing
|
83
|
+
|
84
|
+
If no custom `validator` is used, tests that assert the correct required property errors are thrown are unnecessary -- `@brightspace-ui/core` already covers those tests.
|
85
|
+
|
86
|
+
Required property exceptions are thrown asynchronously multiple seconds after the component has rendered. For unit tests, this makes catching them challenging.
|
87
|
+
|
88
|
+
To help, use the `flushRequiredPropertyErrors()` method.
|
89
|
+
|
90
|
+
```javascript
|
91
|
+
const tag = defineCE(
|
92
|
+
class extends PropertyRequiredMixin(LitElement) {
|
93
|
+
static properties = {
|
94
|
+
prop: {
|
95
|
+
type: String,
|
96
|
+
required: {
|
97
|
+
validator: (value) => value === 'valid'
|
98
|
+
}
|
99
|
+
}
|
100
|
+
};
|
101
|
+
}
|
102
|
+
);
|
103
|
+
const elem = await fixture(`<${tag} prop="invalid"></${tag}>`);
|
104
|
+
expect(() => elem.flushRequiredPropertyErrors()).to.throw();
|
105
|
+
```
|
@@ -0,0 +1,118 @@
|
|
1
|
+
import { createElementErrorMessage } from '../../helpers/error.js';
|
2
|
+
import { dedupeMixin } from '@open-wc/dedupe-mixin';
|
3
|
+
|
4
|
+
const TIMEOUT_DURATION = 3000;
|
5
|
+
|
6
|
+
const defaultMessage = (propertyName) => `"${propertyName}" attribute is required`;
|
7
|
+
|
8
|
+
export function createMessage(elem, propertyName, message = defaultMessage(propertyName)) {
|
9
|
+
return createElementErrorMessage(
|
10
|
+
'PropertyRequiredMixin',
|
11
|
+
elem,
|
12
|
+
message,
|
13
|
+
{ composedPath: true }
|
14
|
+
);
|
15
|
+
}
|
16
|
+
|
17
|
+
export function createInvalidPropertyTypeMessage(elem, propertyName) {
|
18
|
+
return createMessage(
|
19
|
+
elem,
|
20
|
+
propertyName,
|
21
|
+
`only String properties can be required (property: "${propertyName}")`
|
22
|
+
);
|
23
|
+
}
|
24
|
+
|
25
|
+
export const PropertyRequiredMixin = dedupeMixin(superclass => class extends superclass {
|
26
|
+
|
27
|
+
constructor() {
|
28
|
+
super();
|
29
|
+
this._requiredProperties = new Map();
|
30
|
+
this._initProperties(Object.getPrototypeOf(this));
|
31
|
+
}
|
32
|
+
|
33
|
+
firstUpdated(changedProperties) {
|
34
|
+
super.firstUpdated(changedProperties);
|
35
|
+
for (const name of this._requiredProperties.keys()) {
|
36
|
+
this._validateRequiredProperty(name);
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
updated(changedProperties) {
|
41
|
+
super.updated(changedProperties);
|
42
|
+
this._requiredProperties.forEach((value, name) => {
|
43
|
+
const doValidate = changedProperties.has(name) || value.dependentProps.includes(name);
|
44
|
+
if (doValidate) this._validateRequiredProperty(name);
|
45
|
+
});
|
46
|
+
}
|
47
|
+
|
48
|
+
flushRequiredPropertyErrors() {
|
49
|
+
for (const name of this._requiredProperties.keys()) {
|
50
|
+
this._flushRequiredPropertyError(name);
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
_addRequiredProperty(name, prop) {
|
55
|
+
|
56
|
+
const opts = {
|
57
|
+
...{
|
58
|
+
dependentProps: [],
|
59
|
+
message: (_value, _elem, defaultMessage) => defaultMessage,
|
60
|
+
validator: (_value, _elem, hasValue) => hasValue
|
61
|
+
},
|
62
|
+
...prop.required
|
63
|
+
};
|
64
|
+
|
65
|
+
this._requiredProperties.set(name, {
|
66
|
+
attrName: prop.attribute || name,
|
67
|
+
dependentProps: opts.dependentProps,
|
68
|
+
message: opts.message,
|
69
|
+
thrown: false,
|
70
|
+
timeout: null,
|
71
|
+
type: prop.type,
|
72
|
+
validator: opts.validator
|
73
|
+
});
|
74
|
+
|
75
|
+
}
|
76
|
+
|
77
|
+
_flushRequiredPropertyError(name) {
|
78
|
+
|
79
|
+
if (!this._requiredProperties.has(name) || !this.isConnected) return;
|
80
|
+
|
81
|
+
const info = this._requiredProperties.get(name);
|
82
|
+
clearTimeout(info.timeout);
|
83
|
+
info.timeout = null;
|
84
|
+
|
85
|
+
if (info.type !== undefined && info.type !== String) {
|
86
|
+
throw new Error(createInvalidPropertyTypeMessage(this, name));
|
87
|
+
}
|
88
|
+
|
89
|
+
const value = this[name];
|
90
|
+
const hasValue = value?.constructor === String && value?.length > 0;
|
91
|
+
const success = info.validator(value, this, hasValue);
|
92
|
+
if (!success) {
|
93
|
+
if (info.thrown) return;
|
94
|
+
info.thrown = true;
|
95
|
+
const message = createMessage(this, info.attrName, info.message(value, this, defaultMessage(info.attrName)));
|
96
|
+
throw new TypeError(message);
|
97
|
+
}
|
98
|
+
|
99
|
+
}
|
100
|
+
|
101
|
+
_initProperties(base) {
|
102
|
+
if (base === null) return;
|
103
|
+
this._initProperties(Object.getPrototypeOf(base));
|
104
|
+
for (const name in base.constructor.properties) {
|
105
|
+
const prop = base.constructor.properties[name];
|
106
|
+
if (prop.required) {
|
107
|
+
this._addRequiredProperty(name, prop);
|
108
|
+
}
|
109
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
_validateRequiredProperty(name) {
|
113
|
+
const info = this._requiredProperties.get(name);
|
114
|
+
clearTimeout(info.timeout);
|
115
|
+
info.timeout = setTimeout(() => this._flushRequiredPropertyError(name), TIMEOUT_DURATION);
|
116
|
+
}
|
117
|
+
|
118
|
+
});
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@brightspace-ui/core",
|
3
|
-
"version": "2.
|
3
|
+
"version": "2.163.0",
|
4
4
|
"description": "A collection of accessible, free, open-source web components for building Brightspace applications",
|
5
5
|
"type": "module",
|
6
6
|
"repository": "https://github.com/BrightspaceUI/core.git",
|