lightning-base-components 1.18.1-alpha → 1.18.2-alpha

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 (215) hide show
  1. package/metadata/raptor.json +5 -0
  2. package/package.json +43 -1
  3. package/scopedImports/@salesforce-internal-core.appVersion.js +1 -1
  4. package/src/lightning/accordion/accordion.css +12 -0
  5. package/src/lightning/accordion/accordion.html +3 -1
  6. package/src/lightning/accordion/accordion.js +4 -2
  7. package/src/lightning/accordion/accordion.slds.css +671 -0
  8. package/src/lightning/accordionSection/accordion-section.slds.css +647 -0
  9. package/src/lightning/accordionSection/accordionSection.css +14 -0
  10. package/src/lightning/accordionSection/accordionSection.html +23 -19
  11. package/src/lightning/accordionSection/accordionSection.js +29 -2
  12. package/src/lightning/ariaObserver/__docs__/ariaObserver.md +21 -9
  13. package/src/lightning/ariaObserver/ariaObserver.js +185 -154
  14. package/src/lightning/ariaObserver/polyfill.js +639 -0
  15. package/src/lightning/avatar/avatar.css +2 -0
  16. package/src/lightning/avatar/avatar.html +2 -0
  17. package/src/lightning/avatar/avatar.js +18 -15
  18. package/src/lightning/avatar/avatar.slds.css +272 -0
  19. package/src/lightning/baseCombobox/base-combobox.slds.css +1585 -0
  20. package/src/lightning/baseCombobox/baseCombobox.css +11 -1
  21. package/src/lightning/baseCombobox/baseCombobox.html +154 -146
  22. package/src/lightning/baseCombobox/baseCombobox.js +82 -46
  23. package/src/lightning/baseCombobox/spinner.slds.css +438 -0
  24. package/src/lightning/baseComboboxItem/baseComboboxItem.js +4 -2
  25. package/src/lightning/baseComboboxItem/inline.css +2 -0
  26. package/src/lightning/breadcrumb/breadcrumb.css +2 -2
  27. package/src/lightning/breadcrumb/breadcrumb.js +4 -2
  28. package/src/lightning/breadcrumb/breadcrumb.slds.css +2 -7
  29. package/src/lightning/breadcrumbs/breadcrumbs.css +2 -2
  30. package/src/lightning/breadcrumbs/breadcrumbs.js +3 -2
  31. package/src/lightning/breadcrumbs/breadcrumbs.slds.css +7 -1
  32. package/src/lightning/button/__examples__/inverse/inverse.css +8 -0
  33. package/src/lightning/button/__examples__/inverse/inverse.html +3 -2
  34. package/src/lightning/button/button.css +2 -0
  35. package/src/lightning/button/button.html +4 -2
  36. package/src/lightning/button/button.js +21 -0
  37. package/src/lightning/button/button.slds.css +527 -0
  38. package/src/lightning/buttonGroup/buttonGroup.css +2 -2
  39. package/src/lightning/buttonGroup/buttonGroup.js +3 -2
  40. package/src/lightning/buttonIcon/button-icon.slds.css +215 -453
  41. package/src/lightning/buttonIcon/buttonIcon.css +2 -2
  42. package/src/lightning/buttonIcon/buttonIcon.js +4 -0
  43. package/src/lightning/buttonIconStateful/button-icon-stateful.slds.css +215 -453
  44. package/src/lightning/buttonIconStateful/buttonIconStateful.css +2 -2
  45. package/src/lightning/buttonMenu/{dropdown.slds.css → button-menu.slds.css} +853 -217
  46. package/src/lightning/buttonMenu/buttonMenu.css +2 -2
  47. package/src/lightning/buttonMenu/buttonMenu.html +2 -2
  48. package/src/lightning/buttonMenu/buttonMenu.js +10 -14
  49. package/src/lightning/buttonStateful/button-stateful.slds.css +225 -457
  50. package/src/lightning/buttonStateful/buttonStateful.css +2 -2
  51. package/src/lightning/buttonStateful/buttonStateful.js +3 -2
  52. package/src/lightning/calendar/__examples__/basic/basic.html +7 -0
  53. package/src/lightning/calendar/__examples__/basic/basic.js +3 -0
  54. package/src/lightning/calendar/calendar.css +3 -0
  55. package/src/lightning/calendar/calendar.html +12 -9
  56. package/src/lightning/calendar/calendar.js +15 -1
  57. package/src/lightning/calendar/calendar.slds.css +2048 -0
  58. package/src/lightning/card/card.css +2 -2
  59. package/src/lightning/card/card.js +3 -2
  60. package/src/lightning/card/card.slds.css +141 -88
  61. package/src/lightning/colorPickerCustom/colorPickerCustom.css +2 -2
  62. package/src/lightning/colorPickerCustom/colorPickerCustom.js +3 -2
  63. package/src/lightning/colorPickerPanel/color-picker-panel.slds.css +11 -38
  64. package/src/lightning/colorPickerPanel/colorPickerPanel.css +3 -2
  65. package/src/lightning/colorPickerPanel/colorPickerPanel.js +4 -2
  66. package/src/lightning/colorPickerPanel/popover.slds.css +121 -0
  67. package/src/lightning/combobox/combobox.css +4 -0
  68. package/src/lightning/combobox/combobox.html +31 -29
  69. package/src/lightning/combobox/combobox.js +21 -4
  70. package/src/lightning/combobox/combobox.slds.css +13 -0
  71. package/src/lightning/combobox/form-element.slds.css +281 -0
  72. package/src/lightning/configProvider/defaultConfig.js +2 -1
  73. package/src/lightning/datepicker/datepicker.css +3 -0
  74. package/src/lightning/datepicker/datepicker.html +7 -4
  75. package/src/lightning/datepicker/datepicker.js +73 -19
  76. package/src/lightning/datepicker/form-element.slds.css +281 -0
  77. package/src/lightning/datepicker/input-text.slds.css +398 -0
  78. package/src/lightning/datetimepicker/datetimepicker.css +3 -0
  79. package/src/lightning/datetimepicker/datetimepicker.html +9 -3
  80. package/src/lightning/datetimepicker/datetimepicker.js +39 -35
  81. package/src/lightning/datetimepicker/form-element.slds.css +281 -0
  82. package/src/lightning/datetimepicker/input-text.slds.css +398 -0
  83. package/src/lightning/dualListbox/dualListbox.css +2 -2
  84. package/src/lightning/dualListbox/dualListbox.html +3 -3
  85. package/src/lightning/dualListbox/dualListbox.js +31 -6
  86. package/src/lightning/dualListbox/form-element.slds.css +83 -34
  87. package/src/lightning/dualListbox/keyboard.js +20 -1
  88. package/src/lightning/dynamicIcon/dynamicIcon.js +3 -2
  89. package/src/lightning/dynamicIcon/ellie.css +1 -1
  90. package/src/lightning/dynamicIcon/eq.css +1 -1
  91. package/src/lightning/dynamicIcon/score.css +1 -1
  92. package/src/lightning/dynamicIcon/strength.css +1 -1
  93. package/src/lightning/dynamicIcon/trend.css +1 -1
  94. package/src/lightning/dynamicIcon/waffle.css +1 -1
  95. package/src/lightning/formattedRichText/linkify.js +2 -2
  96. package/src/lightning/helptext/form-element.slds.css +83 -34
  97. package/src/lightning/helptext/help-text.slds.css +215 -453
  98. package/src/lightning/helptext/helptext.css +2 -2
  99. package/src/lightning/helptext/helptext.js +3 -2
  100. package/src/lightning/i18nCldrOptions/README.md +5 -0
  101. package/src/lightning/i18nService/README.md +5 -0
  102. package/src/lightning/icon/icon.css +2 -2
  103. package/src/lightning/icon/icon.js +16 -2
  104. package/src/lightning/icon/icon.slds.css +29 -17
  105. package/src/lightning/icon/iconColors.js +1 -0
  106. package/src/lightning/input/__examples__/text/text.html +0 -1
  107. package/src/lightning/input/form-element.slds.css +281 -0
  108. package/src/lightning/input/input-checkbox.slds.css +3 -12
  109. package/src/lightning/input/input-text.slds.css +239 -128
  110. package/src/lightning/input/input.css +2 -1
  111. package/src/lightning/input/input.html +8 -8
  112. package/src/lightning/input/input.js +107 -73
  113. package/src/lightning/internationalizationLibrary/README.md +24 -0
  114. package/src/lightning/internationalizationLibrary/utils.js +4 -1
  115. package/src/lightning/layout/__docs__/layout.md +1 -1
  116. package/src/lightning/layout/__examples__/simple/simple.css +1 -1
  117. package/src/lightning/layout/layout.css +5 -1
  118. package/src/lightning/layout/layout.js +4 -2
  119. package/src/lightning/layoutItem/__examples__/alignmentBump/alignmentBump.css +1 -1
  120. package/src/lightning/layoutItem/__examples__/sizePerDevice/sizePerDevice.css +0 -1
  121. package/src/lightning/layoutItem/layoutItem.css +5 -0
  122. package/src/lightning/layoutItem/layoutItem.js +4 -2
  123. package/src/lightning/menuDivider/menu-divider.slds.css +15 -0
  124. package/src/lightning/menuDivider/menuDivider.css +3 -0
  125. package/src/lightning/menuDivider/menuDivider.html +1 -1
  126. package/src/lightning/menuDivider/menuDivider.js +4 -2
  127. package/src/lightning/menuItem/menu-item.slds.css +140 -0
  128. package/src/lightning/menuItem/menuItem.css +3 -0
  129. package/src/lightning/menuItem/menuItem.html +43 -41
  130. package/src/lightning/menuItem/menuItem.js +4 -4
  131. package/src/lightning/menuSubheader/menu-subheader.slds.css +22 -0
  132. package/src/lightning/menuSubheader/menuSubheader.css +3 -0
  133. package/src/lightning/menuSubheader/menuSubheader.html +3 -1
  134. package/src/lightning/menuSubheader/menuSubheader.js +4 -6
  135. package/src/lightning/modal/__docs__/modal.md +3 -1
  136. package/src/lightning/modal/__modalUtils__/modalContainerTestConstants.js +267 -0
  137. package/src/lightning/modal/__modalUtils__/modalContainerTestMethods.js +1165 -0
  138. package/src/lightning/modal/__modalUtils__/modalContainerTestMockData.js +131 -0
  139. package/src/lightning/modal/modal.js +1 -1
  140. package/src/lightning/pill/avatar.slds.css +272 -0
  141. package/src/lightning/pill/link.css +3 -0
  142. package/src/lightning/pill/link.html +1 -1
  143. package/src/lightning/pill/pill.js +29 -9
  144. package/src/lightning/pill/pill.slds.css +168 -0
  145. package/src/lightning/pill/plain.css +3 -0
  146. package/src/lightning/pill/plain.html +1 -1
  147. package/src/lightning/pill/plainLink.css +3 -0
  148. package/src/lightning/pill/plainLink.html +1 -1
  149. package/src/lightning/pillContainer/barePillContainer.css +3 -0
  150. package/src/lightning/pillContainer/barePillContainer.html +1 -2
  151. package/src/lightning/pillContainer/listbox.slds.css +267 -0
  152. package/src/lightning/pillContainer/pill-container.slds.css +22 -0
  153. package/src/lightning/pillContainer/pill.slds.css +168 -0
  154. package/src/lightning/pillContainer/pillContainer.js +7 -3
  155. package/src/lightning/pillContainer/standardPillContainer.css +4 -0
  156. package/src/lightning/pillContainer/standardPillContainer.html +2 -2
  157. package/src/lightning/popup/popover.slds.css +119 -119
  158. package/src/lightning/popup/popup.css +1 -2
  159. package/src/lightning/popup/popup.js +3 -2
  160. package/src/lightning/positionLibrary/elementProxy.js +7 -2
  161. package/src/lightning/positionLibrary/util.js +8 -0
  162. package/src/lightning/primitiveBubble/primitiveBubble.css +2 -2
  163. package/src/lightning/primitiveBubble/primitiveBubble.js +4 -2
  164. package/src/lightning/primitiveButton/primitiveButton.js +5 -4
  165. package/src/lightning/primitiveCellFactory/cellWithStandardLayout.html +29 -21
  166. package/src/lightning/primitiveCellFactory/primitiveCellFactory.js +4 -0
  167. package/src/lightning/primitiveColorpickerButton/color-picker-button.slds.css +31 -19
  168. package/src/lightning/primitiveColorpickerButton/primitiveColorpickerButton.css +2 -2
  169. package/src/lightning/primitiveColorpickerButton/primitiveColorpickerButton.js +5 -3
  170. package/src/lightning/primitiveIcon/icon.slds.css +209 -0
  171. package/src/lightning/primitiveIcon/primitiveIcon.css +2 -1
  172. package/src/lightning/primitiveIcon/primitiveIcon.html +1 -1
  173. package/src/lightning/primitiveIcon/primitiveIcon.js +18 -11
  174. package/src/lightning/progressStep/progressStep.js +10 -13
  175. package/src/lightning/radioGroup/radioGroup.css +2 -1
  176. package/src/lightning/radioGroup/radioGroup.js +4 -2
  177. package/src/lightning/select/form-element.slds.css +83 -34
  178. package/src/lightning/select/select.css +2 -2
  179. package/src/lightning/select/select.js +4 -2
  180. package/src/lightning/select/select.slds.css +86 -34
  181. package/src/lightning/sldsCommon/sldsCommon.css +135 -75
  182. package/src/lightning/spinner/spinner.css +2 -2
  183. package/src/lightning/spinner/spinner.js +4 -2
  184. package/src/lightning/tabBar/tab-bar.slds.css +334 -0
  185. package/src/lightning/tabBar/tabBar.css +2 -0
  186. package/src/lightning/tabBar/tabBar.html +4 -3
  187. package/src/lightning/tabBar/tabBar.js +30 -3
  188. package/src/lightning/tabset/tabset.html +5 -4
  189. package/src/lightning/tabset/tabset.js +29 -11
  190. package/src/lightning/timepicker/form-element.slds.css +281 -0
  191. package/src/lightning/timepicker/timepicker.css +3 -0
  192. package/src/lightning/timepicker/timepicker.html +5 -1
  193. package/src/lightning/timepicker/timepicker.js +18 -15
  194. package/src/lightning/timepicker/timepicker.slds.css +18 -0
  195. package/src/lightning/tooltipLibrary/tooltipLibrary.js +21 -19
  196. package/src/lightning/utilsPrivate/browser.js +5 -3
  197. package/src/lightning/utilsPrivate/os.js +6 -4
  198. package/src/lightning/utilsPrivate/ssr.js +4 -0
  199. package/src/lightning/utilsPrivate/utilsPrivate.js +2 -0
  200. package/src/lightning/verticalNavigation/verticalNavigation.css +2 -1
  201. package/src/lightning/verticalNavigation/verticalNavigation.js +3 -2
  202. package/src/lightning/verticalNavigationSection/verticalNavigationSection.css +2 -1
  203. package/src/lightning/verticalNavigationSection/verticalNavigationSection.js +3 -2
  204. package/src/lightning/accordion/__perf__DISABLED/accordion-perf-utils.js +0 -76
  205. package/src/lightning/accordion/__perf__DISABLED/accordion10Multiple25SectionEach.perf.js +0 -57
  206. package/src/lightning/accordion/__perf__DISABLED/accordion10Simple25SectionEach.perf.js +0 -37
  207. package/src/lightning/accordion/__perf__DISABLED/accordionMultiple50Section.perf.js +0 -45
  208. package/src/lightning/accordion/__perf__DISABLED/accordionSimple50Section.perf.js +0 -35
  209. package/src/lightning/accordion/__perf__DISABLED/container/container.html +0 -15
  210. package/src/lightning/accordion/__perf__DISABLED/container/container.js +0 -7
  211. package/src/lightning/positionLibrary/__component__/positionLibraryBounding.spec.js +0 -319
  212. package/src/lightning/positionLibrary/__component__/x/bounding/bounding.css +0 -16
  213. package/src/lightning/positionLibrary/__component__/x/bounding/bounding.html +0 -36
  214. package/src/lightning/positionLibrary/__component__/x/bounding/bounding.js +0 -122
  215. /package/src/lightning/{baseCombobox → baseComboboxItem}/listbox.slds.css +0 -0
@@ -1,24 +1,28 @@
1
1
  <template>
2
- <section class={computedSectionClasses}>
3
- <div class="slds-accordion__summary">
4
- <h2 aria-level={_privateHeadingAriaLevel} onkeydown={handleKeyDown} class="slds-accordion__summary-heading">
5
- <button class="section-control slds-button slds-button_reset slds-accordion__summary-action"
2
+ <div class="slds-accordion__list-item">
3
+ <section class={computedSectionClasses} part="accordion-section">
4
+ <div class="slds-accordion__summary">
5
+ <h2 aria-level={_privateHeadingAriaLevel} onkeydown={handleKeyDown} class="slds-accordion__summary-heading">
6
+ <button
7
+ class="section-control slds-button slds-button_reset slds-accordion__summary-action"
6
8
  type="button"
7
9
  aria-expanded={computedAriaExpanded}
8
10
  aria-controls='lgt-accordion-section'
9
- onclick={handleSelectSection}>
10
- <lightning-primitive-icon
11
- icon-name="utility:switch"
12
- svg-class="slds-accordion__summary-action-icon slds-button__icon slds-button__icon_left"
13
- size="x-small">
14
- </lightning-primitive-icon>
15
- <span class="slds-accordion__summary-content" title={label}>{label}</span>
16
- </button>
17
- </h2>
18
- <slot name="actions"></slot>
19
- </div>
20
- <div id='lgt-accordion-section' hidden={computedHidden} aria-hidden={computedAriaHidden} class="slds-accordion__content">
21
- <slot></slot>
22
- </div>
23
- </section>
11
+ onclick={handleSelectSection}
12
+ part="button">
13
+ <lightning-primitive-icon
14
+ icon-name={accordionTitleIcon}
15
+ svg-class="slds-button__icon slds-button__icon_left"
16
+ size="x-small">
17
+ </lightning-primitive-icon>
18
+ <span class="slds-accordion__summary-content" title={label}>{label}</span>
19
+ </button>
20
+ </h2>
21
+ <slot name="actions"></slot>
22
+ </div>
23
+ <div id='lgt-accordion-section' hidden={computedHidden} aria-hidden={computedAriaHidden} class="slds-accordion__content">
24
+ <slot></slot>
25
+ </div>
26
+ </section>
27
+ </div>
24
28
  </template>
@@ -1,7 +1,9 @@
1
- import { LightningElement, api, track } from 'lwc';
1
+ import { api, track } from 'lwc';
2
+ import LightningShadowBaseClass from 'lightning/shadowBaseClassPrivate';
2
3
  import { generateUniqueId } from 'lightning/inputUtils';
3
4
  import { keyCodes, isHeadingLevelValid } from 'lightning/utilsPrivate';
4
5
  import { classSet } from 'lightning/utils';
6
+ import DIR from '@salesforce/i18n/dir';
5
7
 
6
8
  /**
7
9
  * A single section that is nested in an accordion component.
@@ -9,7 +11,7 @@ import { classSet } from 'lightning/utils';
9
11
  * Actions are displayed at the top right corner of the accordion section.
10
12
  * @slot default Placeholder for your content in the accordion section.
11
13
  */
12
- export default class LightningAccordionSection extends LightningElement {
14
+ export default class LightningAccordionSection extends LightningShadowBaseClass {
13
15
  /**
14
16
  * The unique section name to use with the active-section-name attribute in the accordion component.
15
17
  * @type {string}
@@ -57,6 +59,7 @@ export default class LightningAccordionSection extends LightningElement {
57
59
  privateUniqueId = generateUniqueId('lgt-accordion-section');
58
60
 
59
61
  connectedCallback() {
62
+ super.connectedCallback();
60
63
  this.setAttribute('role', 'listitem');
61
64
  this.classList.add('slds-accordion__list-item');
62
65
  this.registerSectionWithParent();
@@ -116,6 +119,30 @@ export default class LightningAccordionSection extends LightningElement {
116
119
  this.privateAccordionSectionObserver.notifySectionSelect();
117
120
  }
118
121
 
122
+ get hostDirection() {
123
+ // need to use .parentElement as accordionSection needs parentElement accordion to render
124
+ const host = this.template.host;
125
+ if (host.parentElement) {
126
+ return (
127
+ window
128
+ .getComputedStyle(host.parentElement)
129
+ .direction.toLowerCase() || 'ltr'
130
+ );
131
+ }
132
+
133
+ // if no parentElement, return direction from @salesforce/i18n; else fallback to 'ltr'
134
+ return DIR || 'ltr';
135
+ }
136
+
137
+ get accordionTitleIcon() {
138
+ if (this.privateIsOpen) {
139
+ return 'utility:switch';
140
+ }
141
+ return this.hostDirection === 'ltr'
142
+ ? 'utility:chevronright'
143
+ : 'utility:chevronleft';
144
+ }
145
+
119
146
  registerSectionWithParent() {
120
147
  const detail = {
121
148
  targetId: this.privateUniqueId,
@@ -1,7 +1,7 @@
1
1
  The `lightning/ariaObserver` module provides an easy way for users to write accessible component that works in both synthetic and native shadow.
2
2
 
3
3
  ## Aria ID referencing in native shadow
4
- Use the` AriaObserver` library to write accessible component that works where `ariaLabelledBy` would break native shadow.
4
+ Use the `AriaObserver` library to write accessible component that works where `ariaLabelledBy` would break native shadow.
5
5
 
6
6
  Here's an example that won't work with native shadow. In the following code, we support attribute `ariaLabelledBy` in our component `c-foo`, so the `input` element is labelled by external elements.
7
7
 
@@ -45,9 +45,11 @@ class Foo extends LightningElement {
45
45
  ```
46
46
 
47
47
  Next, use the `connect(options)` method to connect between the internal element and the external reference. It takes an options object with the following keys:
48
+ - `attribute` The name of the aria attribute. Two supported options: `aria-labelledby`, `aria-describedby`, `aria-activedescendant` and `aria-controls`.
48
49
  - `targetSelector` The selector to the internal element where the aria attribute should be attached.
49
- - `attribute` The name of the aria attribute. Two supported options: `aria-labelledby`and `aria-describedby`.
50
- - `id` The ID of the external element. Alternatively, you can use `ids` for multiple IDs.
50
+ - `targetNode` The element where the aria attribute should be attached. If not provided, the `targetSelector` is used.
51
+ - `relatedNodeIds` ID(s) of the external element(s) to which the `targetNode` will be related. Passed as a space separated string `id1 id2 id3` or an Array of strings `['id1', 'id2', 'id3']`. Combined with `relatedNodes` if both are present.
52
+ - `relatedNodes` An Array of HTMLElement element(s) to which the `targetNode` will be related. Combined with `relatedNodeIds` if both are present.
51
53
 
52
54
  This example uses `connect(options)` to display an aria label for the internal `input` element.
53
55
  ``` js
@@ -61,12 +63,15 @@ set ariaLabelledBy(refs) {
61
63
  this.ariaObserver.connect({
62
64
  targetSelector: 'input',
63
65
  attribute: 'aria-labelledby',
64
- id: refs
66
+ relatedNodeIds: refs
65
67
  });
66
68
  }
67
69
  ```
68
70
 
69
- Then use the `sync()` method to synchronize the ID references when the template is re-rendered.
71
+ Then use the `sync(isNativeShadow)` method to synchronize the ID references when the template is re-rendered.
72
+ - `isNativeShadow` An optional parameter that indicates whether the relationships involve components rendered in native shadow.
73
+ Used when the `targetSelector` or `targetNode` is within a shadow boundary, but the parent component where AriaObserver has been
74
+ instantiated is not. Example: `lightning-primitive-input-simple` may be rendered in native shadow, but `lightning-input` may not be.
70
75
 
71
76
  ``` js
72
77
  renderedCallback() {
@@ -74,6 +79,14 @@ renderedCallback() {
74
79
  }
75
80
  ```
76
81
 
82
+ When the containing component is native shadow enabled, AriaObserver observes the component's root node to find and link
83
+ the related elements. This can be overridden in cases where the component's root node does not contain the related elements,
84
+ but one of it's ancestor nodes does. For example, `lightning-base-combobox` is contained within `lightning-combobox` so the root of `lightning-combobox` should be used in place of the root of `lightning-base-combobox`.
85
+
86
+ ```js
87
+ this.ariaObserver.root = parentRootNode;
88
+ ```
89
+
77
90
  Finally, disconnect the aria observer and free the resources at the end of the component lifecycle.
78
91
 
79
92
  ``` js
@@ -117,7 +130,7 @@ export default class Foo extends LightningElement {
117
130
  this.ariaObserver.connect({
118
131
  targetSelector: 'input',
119
132
  attribute: 'aria-labelledby',
120
- id: refs
133
+ relatedNodeIds: refs
121
134
  });
122
135
  }
123
136
 
@@ -134,9 +147,8 @@ export default class Foo extends LightningElement {
134
147
  }
135
148
  ```
136
149
 
137
- ## Limitations
138
- `AriaObserver` only works with text-only aria ID references.
139
-
140
150
  Supported attributes:
141
151
  - `aria-labelledby`
142
152
  - `aria-describedby`
153
+ - `aria-activedescendant`
154
+ - `aria-controls`
@@ -4,14 +4,49 @@ import {
4
4
  isNativeComponent,
5
5
  } from 'lightning/utilsPrivate';
6
6
 
7
- const CONTENT_SEPARATOR = '\n';
8
-
9
- function getAttr(elm, attr) {
10
- if (elm.tagName.match(/lightning/i)) {
11
- return elm[attr];
12
- }
13
- return elm.getAttribute(attr);
14
- }
7
+ import {
8
+ setAriaActiveDescendant,
9
+ setAriaDescribedBy,
10
+ setAriaLabelledBy,
11
+ setAriaControls,
12
+ } from './polyfill.js';
13
+
14
+ /*
15
+ Aria Reflection is used if supported by browser.
16
+ If Aria Reflection is not available, fallback to polyfill
17
+ Aria Reflection: https://wicg.github.io/aom/aria-reflection-explainer.html
18
+ Polyfill: https://git.soma.salesforce.com/lwc/aria-element-reflection
19
+ */
20
+ const SUPPORTED_ATTRIBUTES = new Map([
21
+ [
22
+ 'aria-controls',
23
+ {
24
+ ariaReflection: 'ariaControlsElements',
25
+ polyfill: setAriaControls,
26
+ },
27
+ ],
28
+ [
29
+ 'aria-labelledby',
30
+ {
31
+ ariaReflection: 'ariaLabelledByElements',
32
+ polyfill: setAriaLabelledBy,
33
+ },
34
+ ],
35
+ [
36
+ 'aria-describedby',
37
+ {
38
+ ariaReflection: 'ariaDescribedByElements',
39
+ polyfill: setAriaDescribedBy,
40
+ },
41
+ ],
42
+ [
43
+ 'aria-activedescendant',
44
+ {
45
+ ariaReflection: 'ariaActiveDescendantElement',
46
+ polyfill: setAriaActiveDescendant,
47
+ },
48
+ ],
49
+ ]);
15
50
 
16
51
  function extractElements(root, ids) {
17
52
  if (typeof ids !== 'string' || ids === '') {
@@ -24,80 +59,15 @@ function extractElements(root, ids) {
24
59
  .filter((el) => !!el);
25
60
  }
26
61
 
27
- function extractContent(elements) {
28
- return elements
29
- .map((element) => element.textContent)
30
- .filter((text) => text.length)
31
- .join(CONTENT_SEPARATOR);
32
- }
33
-
34
62
  function splitIds(ids) {
35
- return (ids + '').trim().split(/\s+/);
36
- }
37
-
38
- function hashIds(ids) {
39
- return (ids + '')
40
- .trim()
41
- .split(/\s+/)
42
- .reduce((r, v) => {
43
- r[v] = 1;
44
- return r;
45
- }, {});
46
- }
47
-
48
- // this method should check each individual id from computedIds
49
- // against the existing value of the attrName on elm, and dupe
50
- // them, and add the new ones.
51
- function addAriaRefWhenNeeded(elm, attrName, computedIds) {
52
- const newIds = splitIds(computedIds);
53
- const oldIds = getAttr(elm, attrName) || '';
54
- const oldIdsHash = hashIds(oldIds);
55
- const suffix = [];
56
- for (let i = 0; i < newIds.length; i += 1) {
57
- if (!oldIdsHash[newIds[i]]) {
58
- suffix.push(newIds[i]);
59
- }
60
- }
61
-
62
- if (suffix.length !== 0) {
63
- synchronizeAttrs(elm, {
64
- [attrName]:
65
- oldIds + (oldIds.length === 0 ? '' : ' ') + suffix.join(' '),
66
- });
67
- }
68
- }
69
-
70
- // this method should check each individual id from computedIds
71
- // against the existing value of the attrName on elm, and remove
72
- // them when possible in preparation for some new values.
73
- function removeAriaRefWhenPossible(elm, attrName, computedIds) {
74
- const newIds = splitIds(computedIds);
75
- const oldIds = getAttr(elm, attrName) || '';
76
- const oldIdsHash = hashIds(oldIds);
77
- const newValues = [];
78
- for (let i = 0; i < newIds.length; i += 1) {
79
- if (!oldIdsHash[newIds[i]]) {
80
- newValues.push(newIds[i]);
81
- }
82
- }
83
-
84
- synchronizeAttrs(elm, {
85
- [attrName]: newValues.join(' '),
86
- });
87
- }
88
-
89
- function createPlaceholderContainer() {
90
- const container = document.createElement('span');
91
- container.className = 'slds-assistive-text';
92
- container.setAttribute('placeholder-container', '');
93
- return container;
63
+ return ids ? (ids + '').trim().split(/\s+/) : [];
94
64
  }
95
65
 
96
66
  export default class AriaObserver {
97
67
  constructor(component) {
98
68
  this.component = component;
99
69
  this.template = component.template;
100
- this.isNative = isNativeComponent(component);
70
+ this.isNativeShadow = isNativeComponent(component);
101
71
  this.state = {};
102
72
  this.liveIds = {};
103
73
  this.guid = guid();
@@ -114,54 +84,139 @@ export default class AriaObserver {
114
84
  this.liveIds[refs] = liveId;
115
85
  }
116
86
 
117
- connect({ targetSelector, attribute, id, ids }) {
118
- ids = ids || id;
119
-
87
+ /**
88
+ * Connects the internal element and the external reference. It takes an options object with the following keys:
89
+ * @param {String} attribute The name of the aria attribute. Two supported options: `aria-labelledby`, `aria-describedby`, `aria-activedescendant` and `aria-controls`.
90
+ * @param {String} targetSelector The selector to the internal element where the aria attribute should be attached.
91
+ * @param {HTMLElement} targetNode The element where the aria attribute should be attached. If not provided, the `targetSelector` is used.
92
+ * @param {String|Array[String]} relatedNodeIds ID(s) of the external element(s) to which the `targetNode` will be related. Passed as a space separated string `id1 id2 id3`. Combined with `relatedNodes` if both are present.
93
+ * @param {Array[HTMLElement]} relatedNodes an Array of HTMLElement element(s) to which the `targetNode` will be related. Combined with `relatedNodeIds` if both are present.
94
+ */
95
+ connect({
96
+ attribute,
97
+ targetSelector,
98
+ targetNode,
99
+ relatedNodeIds,
100
+ relatedNodes,
101
+ }) {
120
102
  this.state[attribute] = this.state[attribute] || {};
121
103
  const attrState = this.state[attribute];
122
104
 
123
- // note: we don't support linking to a different targetSelector
124
- attrState.innerSelector = attrState.innerSelector || targetSelector;
105
+ attrState.targetNode = targetNode;
106
+ attrState.targetSelector = targetSelector;
107
+ attrState.relatedNodes = (
108
+ !Array.isArray(relatedNodes) ? [relatedNodes] : relatedNodes
109
+ ).filter(Boolean);
125
110
 
126
- // removing the old ids if possible before setting the new ones
127
- if (!this.isNative && attrState.ids) {
128
- const elm = this.template.querySelector(attrState.innerSelector);
129
- if (elm) {
130
- removeAriaRefWhenPossible(elm, attribute, attrState.ids);
131
- }
132
- }
133
-
134
- attrState.ids = ids;
135
-
136
- if (this.isNative && !attrState.placeholder) {
137
- // create placeholder element for copied content
138
- attrState.placeholder = document.createElement('span');
139
- attrState.placeholder.id = `auto-link-${attribute}-${this.guid}`;
140
- }
111
+ attrState.relatedNodeIds = Array.isArray(relatedNodeIds)
112
+ ? relatedNodeIds.join(' ')
113
+ : relatedNodeIds;
141
114
 
142
115
  if (this.component.isConnected) {
143
116
  this.privateUpdate(attribute);
144
117
  }
145
118
  }
146
119
 
147
- sync() {
120
+ /**
121
+ * Connects the MutationObserver when in native shadow mode and connects the
122
+ * appropriate aria attributes to the correct elements
123
+ * @param {Boolean} isNativeShadow - This flag is used when a subcomponent
124
+ * (like lightning-primitive-input-simple) is in native shadow mode and the parent
125
+ * (lightning-input) that was passed on AriaObserver instantiation is not.
126
+ */
127
+ sync(isNativeShadow) {
128
+ if (isNativeShadow != null) {
129
+ this.isNativeShadow = isNativeShadow;
130
+ }
131
+
148
132
  if (!this.component.isConnected) {
149
133
  throw new Error(
150
134
  `Invalid sync invocation. It can only be invoked during renderedCallback().`
151
135
  );
152
136
  }
153
- if (this.isNative && !this.mo) {
154
- this.privateConnect();
137
+
138
+ if (!this.root) {
139
+ this.root =
140
+ this.template && this.template.host
141
+ ? this.template.host.getRootNode()
142
+ : null;
155
143
  }
144
+
145
+ this.privateUpdateLiveIds();
146
+
156
147
  for (const attrName in this.state) {
157
148
  if (Object.prototype.hasOwnProperty.call(this.state, attrName)) {
158
149
  this.privateUpdate(attrName);
159
150
  }
160
151
  }
152
+ }
153
+
154
+ get privateIsMoRequired() {
155
+ return this.isNativeShadow || Object.keys(this.liveIds).length !== 0;
156
+ }
157
+
158
+ get root() {
159
+ return this._root;
160
+ }
161
+
162
+ /**
163
+ * Sets the specified root element and observes it. The root element should contain
164
+ * Observes the root element. The root element should contain
165
+ * the related node elements. By default, this is the template host's root node, but can be
166
+ * overridden where required.
167
+ */
168
+ set root(root) {
169
+ this._root = root;
170
+ if (this._root && this.privateIsMoRequired) {
171
+ this.privateCreateMutationObserver();
172
+ }
173
+ }
174
+
175
+ privateUpdate(attrName) {
176
+ const {
177
+ targetSelector,
178
+ targetNode = this.template.querySelector(targetSelector),
179
+ relatedNodeIds,
180
+ relatedNodes,
181
+ } = this.state[attrName];
182
+
183
+ if (!targetNode) {
184
+ return; // nothing to update
185
+ }
161
186
 
162
- // live idRef feature is a no-op in native
163
- if (!this.isNative) {
164
- this.privateUpdateLiveIds();
187
+ const attribute = SUPPORTED_ATTRIBUTES.get(attrName);
188
+ if (!attribute) {
189
+ throw new Error(
190
+ `${attrName} is not supported by AriaObserver. Supported attributes: ${Array.from(
191
+ SUPPORTED_ATTRIBUTES.keys()
192
+ )}`
193
+ );
194
+ }
195
+
196
+ if (this.isNativeShadow) {
197
+ const allRelatedNodes = [
198
+ ...relatedNodes,
199
+ ...extractElements(this.root, relatedNodeIds),
200
+ ];
201
+ if (targetNode[attribute.ariaReflection]) {
202
+ /*
203
+ Remove any existing polyfill relationships that might have been created
204
+ before the ariaReflection property was available (initial connection can occur before the property
205
+ exists).
206
+ */
207
+ attribute.polyfill(targetNode, null, attrName);
208
+ // Use Aria Reflection to manage relationships
209
+ targetNode[attribute.ariaReflection] = relatedNodes;
210
+ } else {
211
+ attribute.polyfill(targetNode, allRelatedNodes, attrName);
212
+ }
213
+ } else {
214
+ synchronizeAttrs(targetNode, {
215
+ [attrName]: [
216
+ ...splitIds(relatedNodeIds),
217
+ ...relatedNodes.map((n) => n.id),
218
+ ].join(' '),
219
+ });
165
220
  }
166
221
  }
167
222
 
@@ -175,6 +230,26 @@ export default class AriaObserver {
175
230
  });
176
231
  }
177
232
 
233
+ /**
234
+ * Observes the root element. The root element should contain
235
+ * the related node elements. By default, this is the template host's root node, but can be
236
+ * overridden where required.
237
+ */
238
+ privateCreateMutationObserver() {
239
+ this.disconnect();
240
+ this.mo = new MutationObserver(() => {
241
+ if (!this.component.isConnected) {
242
+ return; // do nothing when the template is not connected
243
+ }
244
+ this.sync();
245
+ });
246
+ this.mo.observe(this.root, {
247
+ characterData: true,
248
+ childList: true,
249
+ subtree: true,
250
+ });
251
+ }
252
+
178
253
  privateExtractIds(elements) {
179
254
  return elements
180
255
  .map((el) => {
@@ -184,7 +259,10 @@ export default class AriaObserver {
184
259
  }
185
260
 
186
261
  privateUpdateLiveIds() {
187
- const root = this.template.host.getRootNode();
262
+ const root =
263
+ this.template && this.template.host
264
+ ? this.template.host.getRootNode()
265
+ : null;
188
266
 
189
267
  // if not connected do nothing
190
268
  if (!root) {
@@ -193,7 +271,7 @@ export default class AriaObserver {
193
271
  for (const liveId in this.liveIds) {
194
272
  if (Object.prototype.hasOwnProperty.call(this.liveIds, liveId)) {
195
273
  const thisId = this.liveIds[liveId];
196
- if (!thisId.elements?.length) {
274
+ if (!thisId.elements || !thisId.elements.length) {
197
275
  const splitRefIds = splitIds(liveId);
198
276
  // element refs are cached
199
277
  const refElements = [
@@ -213,13 +291,15 @@ export default class AriaObserver {
213
291
  );
214
292
  });
215
293
  }
294
+
216
295
  const newThisId = this.privateExtractCorrectElements(
217
296
  thisId.refs,
218
297
  thisId.elements
219
298
  );
220
299
  const newIds = this.privateExtractIds(newThisId);
221
- // only fire calback if the value changed
222
- if (newIds !== thisId.ids) {
300
+
301
+ // only fire callback if the value changed and the root node has been rendered
302
+ if (newIds.length && newIds !== thisId.ids) {
223
303
  thisId.callback(newIds);
224
304
  thisId.ids = newIds;
225
305
  }
@@ -227,55 +307,6 @@ export default class AriaObserver {
227
307
  }
228
308
  }
229
309
 
230
- privateUpdate(attrName) {
231
- const { innerSelector } = this.state[attrName];
232
- const elm = this.template.querySelector(innerSelector);
233
- if (!elm) {
234
- return; // nothing to update
235
- }
236
- let computedIds;
237
- if (this.isNative) {
238
- const { ids, content, placeholder } = this.state[attrName];
239
-
240
- const newContent = extractContent(extractElements(this.root, ids));
241
- if (content !== newContent) {
242
- this.state[attrName].content = placeholder.textContent =
243
- newContent;
244
- }
245
- if (!placeholder.parentNode) {
246
- // create placeholder container at template root, if not already exist
247
- if (!this.placeholderContainer) {
248
- this.placeholderContainer = createPlaceholderContainer();
249
- this.template.appendChild(this.placeholderContainer);
250
- }
251
-
252
- // inserting the placeholder once
253
- this.placeholderContainer.appendChild(placeholder);
254
- }
255
- computedIds = placeholder.id;
256
- } else {
257
- computedIds = this.state[attrName].ids;
258
- }
259
- addAriaRefWhenNeeded(elm, attrName, computedIds);
260
- }
261
-
262
- privateConnect() {
263
- // caching root ref
264
- this.root = this.template.host.getRootNode();
265
- // creating the observer once
266
- this.mo = new MutationObserver(() => {
267
- if (!this.component.isConnected) {
268
- return; // do nothing when the template is not connected
269
- }
270
- this.sync();
271
- });
272
- this.mo.observe(this.root, {
273
- characterData: true,
274
- childList: true,
275
- subtree: true,
276
- });
277
- }
278
-
279
310
  disconnect() {
280
311
  // MutationObservers must be disconnected manually when using @lwc/synthetic-shadow
281
312
  // https://lwc.dev/guide/composition#:~:text=memory%20leak