@brightspace-ui/core 2.103.0 → 2.104.1

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.
Files changed (119) hide show
  1. package/README.md +7 -7
  2. package/components/alert/alert.js +1 -1
  3. package/components/breadcrumbs/breadcrumb.js +2 -2
  4. package/components/breadcrumbs/breadcrumbs.js +1 -1
  5. package/components/button/button-icon.js +3 -3
  6. package/components/button/button-mixin.js +1 -1
  7. package/components/button/button-move.js +2 -2
  8. package/components/button/button-subtle.js +1 -1
  9. package/components/button/floating-buttons.js +1 -1
  10. package/components/calendar/calendar.js +1 -1
  11. package/components/card/card-footer-link.js +2 -2
  12. package/components/card/card.js +2 -2
  13. package/components/collapsible-panel/collapsible-panel.js +2 -2
  14. package/components/count-badge/count-badge-icon.js +1 -1
  15. package/components/count-badge/count-badge-mixin.js +1 -1
  16. package/components/count-badge/count-badge.js +1 -1
  17. package/components/dialog/dialog-mixin.js +1 -1
  18. package/components/dropdown/dropdown-button.js +1 -1
  19. package/components/dropdown/dropdown-content-mixin.js +1 -1
  20. package/components/dropdown/dropdown-context-menu.js +1 -1
  21. package/components/dropdown/dropdown-menu.js +1 -1
  22. package/components/dropdown/dropdown-more.js +1 -1
  23. package/components/empty-state/empty-state-simple.js +1 -1
  24. package/components/filter/filter-overflow-group.js +1 -1
  25. package/components/filter/filter.js +2 -2
  26. package/components/focus-trap/focus-trap.js +1 -1
  27. package/components/form/form-errory-summary.js +1 -1
  28. package/components/html-block/demo/html-block.html +1 -1
  29. package/components/html-block/html-block.js +1 -1
  30. package/components/icons/icon-custom.js +1 -1
  31. package/components/icons/icon.js +1 -1
  32. package/components/inputs/README.md +1 -1
  33. package/components/inputs/demo/input-radio-label-simple-test.js +1 -1
  34. package/components/inputs/demo/input-radio-label-test.js +1 -1
  35. package/components/inputs/demo/input-select-test.js +1 -1
  36. package/components/inputs/docs/input-radio.md +1 -1
  37. package/components/inputs/docs/input-select-styles.md +1 -1
  38. package/components/inputs/input-checkbox-spacer.js +1 -1
  39. package/components/inputs/input-checkbox.js +2 -2
  40. package/components/inputs/input-color.js +1 -1
  41. package/components/inputs/input-date-range.js +2 -2
  42. package/components/inputs/input-date-time-range.js +2 -2
  43. package/components/inputs/input-date-time.js +3 -3
  44. package/components/inputs/input-date.js +2 -2
  45. package/components/inputs/input-fieldset.js +1 -1
  46. package/components/inputs/input-number.js +2 -2
  47. package/components/inputs/input-percent.js +3 -3
  48. package/components/inputs/input-radio-spacer.js +1 -1
  49. package/components/inputs/input-search.js +2 -2
  50. package/components/inputs/input-text.js +3 -3
  51. package/components/inputs/input-textarea.js +3 -3
  52. package/components/inputs/input-time-range.js +2 -2
  53. package/components/inputs/input-time.js +2 -2
  54. package/components/link/link.js +1 -1
  55. package/components/list/list-item-drag-handle.js +2 -2
  56. package/components/list/list-item-drag-image.js +1 -1
  57. package/components/list/list-item-generic-layout.js +2 -3
  58. package/components/list/list-item-mixin.js +2 -2
  59. package/components/list/list-item-placement-marker.js +1 -1
  60. package/components/menu/menu-item-checkbox.js +1 -1
  61. package/components/menu/menu-item-radio.js +1 -1
  62. package/components/menu/menu-item-return.js +1 -1
  63. package/components/menu/menu.js +1 -1
  64. package/components/meter/meter-circle.js +1 -1
  65. package/components/meter/meter-linear.js +1 -1
  66. package/components/meter/meter-radial.js +1 -1
  67. package/components/object-property-list/object-property-list-item-link.js +1 -1
  68. package/components/offscreen/offscreen.js +1 -1
  69. package/components/overflow-group/overflow-group.js +1 -1
  70. package/components/paging/pager-load-more.js +1 -1
  71. package/components/scroll-wrapper/demo/scroll-wrapper-test.js +1 -1
  72. package/components/scroll-wrapper/scroll-wrapper.js +1 -1
  73. package/components/selection/selection-action-dropdown.js +1 -1
  74. package/components/selection/selection-action.js +1 -1
  75. package/components/selection/selection-controls.js +1 -1
  76. package/components/selection/selection-input.js +1 -1
  77. package/components/selection/selection-mixin.js +1 -1
  78. package/components/selection/selection-select-all-pages.js +1 -1
  79. package/components/selection/selection-select-all.js +1 -1
  80. package/components/skeleton/skeleton-mixin.js +1 -1
  81. package/components/switch/switch-mixin.js +2 -2
  82. package/components/table/demo/table-test.js +1 -1
  83. package/components/table/table-col-sort-button.js +1 -1
  84. package/components/table/table-wrapper.js +1 -1
  85. package/components/tabs/tab-internal.js +1 -1
  86. package/components/tabs/tabs.js +2 -2
  87. package/components/tag-list/tag-list-item-mixin.js +1 -1
  88. package/components/tag-list/tag-list.js +2 -2
  89. package/components/tooltip/tooltip-help.js +1 -1
  90. package/components/tooltip/tooltip.js +1 -1
  91. package/helpers/demo/prism.html +3 -3
  92. package/mixins/{arrow-keys-mixin.md → arrow-keys/README.md} +2 -2
  93. package/mixins/arrow-keys/arrow-keys-mixin.js +125 -0
  94. package/mixins/{demo → arrow-keys/demo}/arrow-keys-mixin.html +2 -2
  95. package/mixins/{demo → arrow-keys/demo}/arrow-keys-test.js +1 -1
  96. package/mixins/arrow-keys-mixin.js +1 -125
  97. package/mixins/{focus-mixin.md → focus/README.md} +1 -1
  98. package/mixins/focus/focus-mixin.js +43 -0
  99. package/mixins/focus-mixin.js +1 -43
  100. package/mixins/{interactive-mixin.md → interactive/README.md} +1 -1
  101. package/mixins/{interactive-mixin.js → interactive/interactive-mixin.js} +4 -4
  102. package/mixins/{labelled-mixin.md → labelled/README.md} +2 -2
  103. package/mixins/{demo → labelled/demo}/labelled-mixin.html +3 -3
  104. package/mixins/labelled/labelled-mixin.js +215 -0
  105. package/mixins/labelled-mixin.js +1 -215
  106. package/mixins/localize/localize-mixin.js +1 -1
  107. package/mixins/{provider-mixin.md → provider/README.md} +3 -3
  108. package/mixins/provider/provider-mixin.js +35 -0
  109. package/mixins/provider-mixin.js +1 -35
  110. package/mixins/{rtl-mixin.md → rtl/README.md} +1 -1
  111. package/mixins/rtl/rtl-mixin.js +40 -0
  112. package/mixins/rtl-mixin.js +1 -40
  113. package/mixins/theme/theme-mixin.js +19 -0
  114. package/mixins/theme-mixin.js +1 -19
  115. package/mixins/{visible-on-ancestor-mixin.md → visible-on-ancestor/README.md} +1 -1
  116. package/mixins/visible-on-ancestor/visible-on-ancestor-mixin.js +160 -0
  117. package/mixins/visible-on-ancestor-mixin.js +1 -160
  118. package/package.json +1 -1
  119. package/templates/primary-secondary/primary-secondary.js +1 -1
@@ -0,0 +1,125 @@
1
+ import { html } from 'lit';
2
+
3
+ const keyCodes = Object.freeze({
4
+ END: 35,
5
+ HOME: 36,
6
+ LEFT: 37,
7
+ UP: 38,
8
+ RIGHT: 39,
9
+ DOWN: 40,
10
+ });
11
+
12
+ export const ArrowKeysMixin = superclass => class extends superclass {
13
+
14
+ static get properties() {
15
+ return {
16
+ /**
17
+ * @ignore
18
+ */
19
+ arrowKeysDirection: { type: String, attribute: 'arrow-keys-direction' },
20
+ /**
21
+ * @ignore
22
+ */
23
+ arrowKeysNoWrap: { type: Boolean, attribute: 'arrow-keys-no-wrap' }
24
+ };
25
+ }
26
+
27
+ constructor() {
28
+ super();
29
+ this.arrowKeysDirection = 'leftright';
30
+ this.arrowKeysNoWrap = false;
31
+ }
32
+
33
+ arrowKeysContainer(inner) {
34
+ return html`<div class="arrow-keys-container" @keydown="${this._handleArrowKeys}">
35
+ ${inner}
36
+ </div>`;
37
+ }
38
+
39
+ async arrowKeysFocusablesProvider() {
40
+ return this.shadowRoot ?
41
+ [...this.shadowRoot.querySelectorAll('.d2l-arrowkeys-focusable')]
42
+ : [];
43
+ }
44
+
45
+ async _focus(elem) {
46
+ if (elem) {
47
+ if (this.arrowKeysOnBeforeFocus) await this.arrowKeysOnBeforeFocus(elem);
48
+ elem.focus();
49
+ }
50
+ }
51
+
52
+ async _focusFirst() {
53
+ const elems = await this.arrowKeysFocusablesProvider();
54
+ if (elems && elems.length > 0) return this._focus(elems[0]);
55
+ }
56
+
57
+ async _focusLast() {
58
+ const elems = await this.arrowKeysFocusablesProvider();
59
+ if (elems && elems.length > 0) return this._focus(elems[elems.length - 1]);
60
+ }
61
+
62
+ async _focusNext(elem) {
63
+ const elems = await this.arrowKeysFocusablesProvider();
64
+ const next = this._tryGetNextFocusable(elems, elem);
65
+ return this._focus(next);
66
+ }
67
+
68
+ async _focusPrevious(elem) {
69
+ const elems = await this.arrowKeysFocusablesProvider();
70
+ const previous = this._tryGetPreviousFocusable(elems, elem);
71
+ return this._focus(previous);
72
+ }
73
+
74
+ _handleArrowKeys(e) {
75
+ const target = e.target;
76
+ if (this.arrowKeysDirection.indexOf('left') >= 0 && e.keyCode === keyCodes.LEFT) {
77
+ if (getComputedStyle(this).direction === 'rtl') {
78
+ this._focusNext(target);
79
+ } else {
80
+ this._focusPrevious(target);
81
+ }
82
+ } else if (this.arrowKeysDirection.indexOf('right') >= 0 && e.keyCode === keyCodes.RIGHT) {
83
+ if (getComputedStyle(this).direction === 'rtl') {
84
+ this._focusPrevious(target);
85
+ } else {
86
+ this._focusNext(target);
87
+ }
88
+ } else if (this.arrowKeysDirection.indexOf('up') >= 0 && e.keyCode === keyCodes.UP) {
89
+ this._focusPrevious(target);
90
+ } else if (this.arrowKeysDirection.indexOf('down') >= 0 && e.keyCode === keyCodes.DOWN) {
91
+ this._focusNext(target);
92
+ } else if (e.keyCode === keyCodes.HOME) {
93
+ this._focusFirst();
94
+ } else if (e.keyCode === keyCodes.END) {
95
+ this._focusLast();
96
+ } else {
97
+ return;
98
+ }
99
+ e.stopPropagation();
100
+ e.preventDefault();
101
+ }
102
+
103
+ _tryGetNextFocusable(elems, elem) {
104
+ if (!elems || elems.length === 0) return;
105
+
106
+ const index = elems.indexOf(elem);
107
+ if (index === elems.length - 1) {
108
+ if (this.arrowKeysNoWrap) return;
109
+ return elems[0];
110
+ }
111
+ return elems[index + 1];
112
+ }
113
+
114
+ _tryGetPreviousFocusable(elems, elem) {
115
+ if (!elems || elems.length === 0) return;
116
+
117
+ const index = elems.indexOf(elem);
118
+ if (index === 0) {
119
+ if (this.arrowKeysNoWrap) return;
120
+ return elems[elems.length - 1];
121
+ }
122
+ return elems[index - 1];
123
+ }
124
+
125
+ };
@@ -3,9 +3,9 @@
3
3
  <head>
4
4
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
5
5
  <meta charset="UTF-8">
6
- <link rel="stylesheet" href="../../components/demo/styles.css" type="text/css">
6
+ <link rel="stylesheet" href="../../../components/demo/styles.css" type="text/css">
7
7
  <script type="module">
8
- import '../../components/demo/demo-page.js';
8
+ import '../../../components/demo/demo-page.js';
9
9
  import './arrow-keys-test.js';
10
10
  </script>
11
11
  </head>
@@ -1,4 +1,4 @@
1
- import '../../components/colors/colors.js';
1
+ import '../../../components/colors/colors.js';
2
2
  import { css, html, LitElement } from 'lit';
3
3
  import { ArrowKeysMixin } from '../arrow-keys-mixin.js';
4
4
 
@@ -1,125 +1 @@
1
- import { html } from 'lit';
2
-
3
- const keyCodes = Object.freeze({
4
- END: 35,
5
- HOME: 36,
6
- LEFT: 37,
7
- UP: 38,
8
- RIGHT: 39,
9
- DOWN: 40,
10
- });
11
-
12
- export const ArrowKeysMixin = superclass => class extends superclass {
13
-
14
- static get properties() {
15
- return {
16
- /**
17
- * @ignore
18
- */
19
- arrowKeysDirection: { type: String, attribute: 'arrow-keys-direction' },
20
- /**
21
- * @ignore
22
- */
23
- arrowKeysNoWrap: { type: Boolean, attribute: 'arrow-keys-no-wrap' }
24
- };
25
- }
26
-
27
- constructor() {
28
- super();
29
- this.arrowKeysDirection = 'leftright';
30
- this.arrowKeysNoWrap = false;
31
- }
32
-
33
- arrowKeysContainer(inner) {
34
- return html`<div class="arrow-keys-container" @keydown="${this._handleArrowKeys}">
35
- ${inner}
36
- </div>`;
37
- }
38
-
39
- async arrowKeysFocusablesProvider() {
40
- return this.shadowRoot ?
41
- [...this.shadowRoot.querySelectorAll('.d2l-arrowkeys-focusable')]
42
- : [];
43
- }
44
-
45
- async _focus(elem) {
46
- if (elem) {
47
- if (this.arrowKeysOnBeforeFocus) await this.arrowKeysOnBeforeFocus(elem);
48
- elem.focus();
49
- }
50
- }
51
-
52
- async _focusFirst() {
53
- const elems = await this.arrowKeysFocusablesProvider();
54
- if (elems && elems.length > 0) return this._focus(elems[0]);
55
- }
56
-
57
- async _focusLast() {
58
- const elems = await this.arrowKeysFocusablesProvider();
59
- if (elems && elems.length > 0) return this._focus(elems[elems.length - 1]);
60
- }
61
-
62
- async _focusNext(elem) {
63
- const elems = await this.arrowKeysFocusablesProvider();
64
- const next = this._tryGetNextFocusable(elems, elem);
65
- return this._focus(next);
66
- }
67
-
68
- async _focusPrevious(elem) {
69
- const elems = await this.arrowKeysFocusablesProvider();
70
- const previous = this._tryGetPreviousFocusable(elems, elem);
71
- return this._focus(previous);
72
- }
73
-
74
- _handleArrowKeys(e) {
75
- const target = e.target;
76
- if (this.arrowKeysDirection.indexOf('left') >= 0 && e.keyCode === keyCodes.LEFT) {
77
- if (getComputedStyle(this).direction === 'rtl') {
78
- this._focusNext(target);
79
- } else {
80
- this._focusPrevious(target);
81
- }
82
- } else if (this.arrowKeysDirection.indexOf('right') >= 0 && e.keyCode === keyCodes.RIGHT) {
83
- if (getComputedStyle(this).direction === 'rtl') {
84
- this._focusPrevious(target);
85
- } else {
86
- this._focusNext(target);
87
- }
88
- } else if (this.arrowKeysDirection.indexOf('up') >= 0 && e.keyCode === keyCodes.UP) {
89
- this._focusPrevious(target);
90
- } else if (this.arrowKeysDirection.indexOf('down') >= 0 && e.keyCode === keyCodes.DOWN) {
91
- this._focusNext(target);
92
- } else if (e.keyCode === keyCodes.HOME) {
93
- this._focusFirst();
94
- } else if (e.keyCode === keyCodes.END) {
95
- this._focusLast();
96
- } else {
97
- return;
98
- }
99
- e.stopPropagation();
100
- e.preventDefault();
101
- }
102
-
103
- _tryGetNextFocusable(elems, elem) {
104
- if (!elems || elems.length === 0) return;
105
-
106
- const index = elems.indexOf(elem);
107
- if (index === elems.length - 1) {
108
- if (this.arrowKeysNoWrap) return;
109
- return elems[0];
110
- }
111
- return elems[index + 1];
112
- }
113
-
114
- _tryGetPreviousFocusable(elems, elem) {
115
- if (!elems || elems.length === 0) return;
116
-
117
- const index = elems.indexOf(elem);
118
- if (index === 0) {
119
- if (this.arrowKeysNoWrap) return;
120
- return elems[elems.length - 1];
121
- }
122
- return elems[index - 1];
123
- }
124
-
125
- };
1
+ export { ArrowKeysMixin } from './arrow-keys/arrow-keys-mixin.js';
@@ -9,7 +9,7 @@ If the component has yet to render, focus will automatically be applied after `f
9
9
  Apply the mixin and set the static `focusElementSelector` to a CSS query selector which will match the element to which focus should be delegated.
10
10
 
11
11
  ```js
12
- import { FocusMixin } from '@brightspace-ui/core/mixins/focus-mixin.js';
12
+ import { FocusMixin } from '@brightspace-ui/core/mixins/focus/focus-mixin.js';
13
13
 
14
14
  class MyComponent extends FocusMixin(LitElement) {
15
15
 
@@ -0,0 +1,43 @@
1
+ import { dedupeMixin } from '@open-wc/dedupe-mixin';
2
+
3
+ export const FocusMixin = dedupeMixin(superclass => class extends superclass {
4
+
5
+ constructor() {
6
+ super();
7
+ this._focusOnFirstRender = false;
8
+ }
9
+
10
+ static get focusElementSelector() {
11
+ return null;
12
+ }
13
+
14
+ firstUpdated(changedProperties) {
15
+ super.firstUpdated(changedProperties);
16
+ if (this._focusOnFirstRender) {
17
+ this._focusOnFirstRender = false;
18
+ this.focus();
19
+ }
20
+ }
21
+
22
+ focus() {
23
+
24
+ const selector = this.constructor.focusElementSelector;
25
+ if (!selector) {
26
+ throw new Error(`FocusMixin: no static focusElementSelector provided for "${this.tagName}"`);
27
+ }
28
+
29
+ if (!this.hasUpdated) {
30
+ this._focusOnFirstRender = true;
31
+ return;
32
+ }
33
+
34
+ const elem = this.shadowRoot.querySelector(selector);
35
+ if (!elem) {
36
+ throw new Error(`FocusMixin: selector "${selector}" yielded no element for "${this.tagName}"`);
37
+ }
38
+
39
+ elem.focus();
40
+
41
+ }
42
+
43
+ });
@@ -1,43 +1 @@
1
- import { dedupeMixin } from '@open-wc/dedupe-mixin';
2
-
3
- export const FocusMixin = dedupeMixin(superclass => class extends superclass {
4
-
5
- constructor() {
6
- super();
7
- this._focusOnFirstRender = false;
8
- }
9
-
10
- static get focusElementSelector() {
11
- return null;
12
- }
13
-
14
- firstUpdated(changedProperties) {
15
- super.firstUpdated(changedProperties);
16
- if (this._focusOnFirstRender) {
17
- this._focusOnFirstRender = false;
18
- this.focus();
19
- }
20
- }
21
-
22
- focus() {
23
-
24
- const selector = this.constructor.focusElementSelector;
25
- if (!selector) {
26
- throw new Error(`FocusMixin: no static focusElementSelector provided for "${this.tagName}"`);
27
- }
28
-
29
- if (!this.hasUpdated) {
30
- this._focusOnFirstRender = true;
31
- return;
32
- }
33
-
34
- const elem = this.shadowRoot.querySelector(selector);
35
- if (!elem) {
36
- throw new Error(`FocusMixin: selector "${selector}" yielded no element for "${this.tagName}"`);
37
- }
38
-
39
- elem.focus();
40
-
41
- }
42
-
43
- });
1
+ export { FocusMixin } from './focus/focus-mixin.js';
@@ -9,7 +9,7 @@ Apply the mixin, call its `renderInteractiveContainer` method from `render`, pro
9
9
  **Note:** consumers _must_ provide a focus delegate as mentioned. They _should not_ implement a `focus` method, since the mixin manages focus with its own implementation.
10
10
 
11
11
  ```js
12
- import { InteractiveMixin } from '@brightspace-ui/core/mixins/interactive-mixin.js';
12
+ import { InteractiveMixin } from '@brightspace-ui/core/mixins/interactive/interactive-mixin.js';
13
13
 
14
14
  class MyComponent extends InteractiveMixin(LitElement) {
15
15
 
@@ -1,10 +1,10 @@
1
1
  import { css, html } from 'lit';
2
- import { findComposedAncestor, isComposedAncestor } from '../helpers/dom.js';
2
+ import { findComposedAncestor, isComposedAncestor } from '../../helpers/dom.js';
3
3
  import { classMap } from 'lit/directives/class-map.js';
4
- import { getNextFocusable } from '../helpers/focus.js';
4
+ import { getNextFocusable } from '../../helpers/focus.js';
5
5
  import { ifDefined } from 'lit/directives/if-defined.js';
6
- import { LocalizeCoreElement } from '../helpers/localize-core-element.js';
7
- import { offscreenStyles } from '../components/offscreen/offscreen.js';
6
+ import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
7
+ import { offscreenStyles } from '../../components/offscreen/offscreen.js';
8
8
 
9
9
  const keyCodes = {
10
10
  ENTER: 13,
@@ -9,7 +9,7 @@ Custom elements that extend the `LabelledMixin` may be labelled by native elemen
9
9
  Apply the `LabelledMixin` to the component containing the element that requires a label, and apply the `label` property defined by `LabelledMixin` as needed:
10
10
 
11
11
  ```js
12
- import { LabelledMixin } from '@brightspace-ui/core/mixins/labelled-mixin.js';
12
+ import { LabelledMixin } from '@brightspace-ui/core/mixins/labelled/labelled-mixin.js';
13
13
 
14
14
  class CustomInput extends LabelledMixin(LitElement) {
15
15
  render() {
@@ -23,7 +23,7 @@ class CustomInput extends LabelledMixin(LitElement) {
23
23
  Optionally, to enable custom elements to act as labels, extend the `LabelMixin` and call `updateLabel()` to reflect the label value change when needed. Alternatively, a custom element within a labelling element's shadowDOM may dispatch the `d2l-label-change` event to update the label value.
24
24
 
25
25
  ```js
26
- import { LabelMixin } from '@brightspace-ui/core/mixins/labelled-mixin.js';
26
+ import { LabelMixin } from '@brightspace-ui/core/mixins/labelled/labelled-mixin.js';
27
27
 
28
28
  class CustomLabel extends LabelMixin(LitElement) {
29
29
  static get properties() {
@@ -1,10 +1,10 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
- <link rel="stylesheet" href="../../components/demo/styles.css" type="text/css">
4
+ <link rel="stylesheet" href="../../../components/demo/styles.css" type="text/css">
5
5
  <script type="module">
6
- import '../../components/demo/demo-page.js';
7
- import '../../components/inputs/input-checkbox.js';
6
+ import '../../../components/demo/demo-page.js';
7
+ import '../../../components/inputs/input-checkbox.js';
8
8
  import { css, html, LitElement } from 'lit';
9
9
  import { LabelledMixin, LabelMixin } from '../labelled-mixin.js';
10
10
  import { ifDefined } from 'lit/directives/if-defined.js';
@@ -0,0 +1,215 @@
1
+
2
+ import { cssEscape } from '../../helpers/dom.js';
3
+
4
+ const getCommonAncestor = (elem1, elem2) => {
5
+
6
+ const labelledPath = new WeakMap();
7
+ let elem = elem1;
8
+ while (elem) {
9
+ labelledPath.set(elem, elem);
10
+ elem = elem.parentNode;
11
+ }
12
+
13
+ let ancestorElem = elem2.parentNode;
14
+ while (ancestorElem) {
15
+ if (labelledPath.has(ancestorElem)) return ancestorElem;
16
+ ancestorElem = ancestorElem.parentNode;
17
+ }
18
+
19
+ };
20
+
21
+ const isCustomElement = elem => {
22
+ return elem.tagName.includes('-');
23
+ };
24
+
25
+ const getLabel = labelElem => {
26
+ if (isCustomElement(labelElem)) return labelElem._label;
27
+ else return labelElem.textContent;
28
+ };
29
+
30
+ const waitForElement = async(contextElement, selector, timeout) => {
31
+ let elem = contextElement.querySelector(selector);
32
+ if (elem) return elem;
33
+
34
+ return new Promise(resolve => {
35
+ let elapsedTime = 0;
36
+ const intervalId = setInterval(() => {
37
+ elem = contextElement.querySelector(selector);
38
+ if (!elem) elapsedTime += 100;
39
+ if (elem || elapsedTime > timeout) {
40
+ clearInterval(intervalId);
41
+ resolve(elem);
42
+ return;
43
+ }
44
+ }, 100);
45
+ });
46
+ };
47
+
48
+ export const LabelMixin = superclass => class extends superclass {
49
+
50
+ static get properties() {
51
+ return {
52
+ _label: { type: String, reflect: true }
53
+ };
54
+ }
55
+
56
+ connectedCallback() {
57
+ super.connectedCallback();
58
+ this.addEventListener('d2l-label-change', this._handleLabelChange);
59
+ }
60
+
61
+ disconnectedCallback() {
62
+ super.disconnectedCallback();
63
+ this.removeEventListener('d2l-label-change', this._handleLabelChange);
64
+ }
65
+
66
+ updateLabel(text) {
67
+ this._label = text;
68
+ }
69
+
70
+ _handleLabelChange(e) {
71
+ e.stopPropagation();
72
+ this.updateLabel(e.detail);
73
+ }
74
+
75
+ };
76
+
77
+ export const LabelledMixin = superclass => class extends superclass {
78
+
79
+ static get properties() {
80
+ return {
81
+ /**
82
+ * The id of element that provides the label for this element
83
+ * @type {string}
84
+ */
85
+ labelledBy: { type: String, reflect: true, attribute: 'labelled-by' },
86
+ /**
87
+ * REQUIRED: Explicitly defined label for the element
88
+ * @type {string}
89
+ */
90
+ label: { type: String }
91
+ };
92
+ }
93
+
94
+ constructor() {
95
+ super();
96
+ this.labelRequired = true;
97
+ this._labelElem = null;
98
+ 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
+ }
106
+
107
+ async updated(changedProperties) {
108
+
109
+ super.updated(changedProperties);
110
+
111
+ if (changedProperties.has('label')) this._validateLabel();
112
+
113
+ if (!changedProperties.has('labelledBy')) return;
114
+
115
+ if (!this.labelledBy) {
116
+ this._updateLabelElem(null);
117
+ } else {
118
+ const labelElem = await waitForElement(this.getRootNode(), `#${cssEscape(this.labelledBy)}`, 3000);
119
+ if (!labelElem) {
120
+ this._throwError(
121
+ new Error(`LabelledMixin: "${this.tagName.toLowerCase()}" is labelled-by="${this.labelledBy}", but no such element exists`)
122
+ );
123
+ }
124
+ this._updateLabelElem(labelElem);
125
+ }
126
+
127
+ }
128
+
129
+ _throwError(err) {
130
+ if (!this.labelRequired || this._missingLabelErrorHasBeenThrown) return;
131
+ this._missingLabelErrorHasBeenThrown = true;
132
+ setTimeout(() => { throw err; }); // we don't want to prevent rendering
133
+ }
134
+
135
+ _updateLabelElem(labelElem) {
136
+
137
+ // setting textContent doesn't change labelElem but we do need to refetch the label
138
+ if (labelElem === this._labelElem && this._labelElem) {
139
+ this.label = getLabel(this._labelElem);
140
+ return;
141
+ }
142
+
143
+ this._labelElem = labelElem;
144
+
145
+ if (this._labelObserver) this._labelObserver.disconnect();
146
+ if (!this._labelElem) {
147
+ this.label = undefined;
148
+ return;
149
+ }
150
+
151
+ this._labelObserver = new MutationObserver(() => {
152
+ const newElem = this.getRootNode().querySelector(`#${cssEscape(this.labelledBy)}`);
153
+ if (isCustomElement(newElem)) {
154
+ requestAnimationFrame(() => {
155
+ // element often sets its label in its own updated(), so we need to wait
156
+ this._updateLabelElem(newElem);
157
+ });
158
+ } else {
159
+ this._updateLabelElem(newElem);
160
+ }
161
+ });
162
+
163
+ const ancestor = getCommonAncestor(this, this._labelElem);
164
+
165
+ // assumption: the labelling element will not change from a native to a custom element
166
+ // or vice versa, which allows the use of a more optimal observer configuration
167
+ if (isCustomElement(this._labelElem)) {
168
+ this._labelObserver.observe(ancestor, {
169
+ attributes: true, // required for legacy-Edge, otherwise attributeFilter throws a syntax error
170
+ attributeFilter: ['_label'],
171
+ childList: true,
172
+ subtree: true
173
+ });
174
+ } else {
175
+ this._labelObserver.observe(ancestor, {
176
+ characterData: true,
177
+ childList: true,
178
+ subtree: true
179
+ });
180
+ }
181
+
182
+ this.label = getLabel(this._labelElem);
183
+ /** @ignore */
184
+ this.dispatchEvent(new CustomEvent(
185
+ 'd2l-labelled-mixin-label-elem-change', {
186
+ bubbles: false,
187
+ composed: false
188
+ }
189
+ ));
190
+
191
+ }
192
+
193
+ _validateLabel() {
194
+ clearTimeout(this._validatingLabelTimeout);
195
+ // don't error immediately in case it doesn't get set immediately
196
+ this._validatingLabelTimeout = setTimeout(() => {
197
+ this._validatingLabelTimeout = null;
198
+ const hasLabel = (typeof this.label === 'string') && this.label.length > 0;
199
+ if (!hasLabel) {
200
+ if (this.labelledBy) {
201
+ if (this._labelElem) {
202
+ this._throwError(
203
+ new Error(`LabelledMixin: "${this.tagName.toLowerCase()}" is labelled-by="${this.labelledBy}", but its label is empty`)
204
+ );
205
+ }
206
+ } else {
207
+ this._throwError(
208
+ new Error(`LabelledMixin: "${this.tagName.toLowerCase()}" is missing a required "label" attribute`)
209
+ );
210
+ }
211
+ }
212
+ }, 3000);
213
+ }
214
+
215
+ };