@lion/ui 0.7.5 → 0.7.7

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 (142) hide show
  1. package/README.md +1 -2
  2. package/components/button/test-suites/LionButton.suite.js +3 -1
  3. package/components/combobox/src/LionCombobox.js +33 -9
  4. package/components/combobox/test/lion-combobox.test.js +134 -7
  5. package/components/dialog/test/lion-dialog.test.js +59 -1
  6. package/components/input-datepicker/test/lion-input-datepicker.test.js +3 -3
  7. package/components/input-file/src/FileHandle.js +1 -1
  8. package/components/input-file/src/LionInputFile.js +1 -3
  9. package/components/input-file/test/lion-input-file.test.js +11 -14
  10. package/components/input-stepper/src/LionInputStepper.js +6 -5
  11. package/components/input-stepper/test/lion-input-stepper-integrations.test.js +20 -0
  12. package/components/input-stepper/test/lion-input-stepper.test.js +48 -3
  13. package/components/overlays/src/OverlayController.js +29 -7
  14. package/components/overlays/src/OverlayMixin.js +5 -1
  15. package/components/overlays/src/utils/deep-contains.js +134 -3
  16. package/components/overlays/test/OverlayController.test.js +57 -0
  17. package/components/overlays/test/utils-tests/deep-contains.test.js +98 -0
  18. package/components/select-rich/src/LionSelectRich.js +12 -0
  19. package/components/select-rich/test/lion-select-rich.test.js +26 -0
  20. package/custom-elements.json +140 -115
  21. package/dist-types/components/button/test-suites/LionButton.suite.d.ts.map +1 -1
  22. package/dist-types/components/combobox/src/LionCombobox.d.ts +9 -0
  23. package/dist-types/components/combobox/src/LionCombobox.d.ts.map +1 -1
  24. package/dist-types/components/input-file/src/LionInputFile.d.ts.map +1 -1
  25. package/dist-types/components/input-stepper/src/LionInputStepper.d.ts +4 -1
  26. package/dist-types/components/input-stepper/src/LionInputStepper.d.ts.map +1 -1
  27. package/dist-types/components/input-stepper/test/lion-input-stepper-integrations.test.d.ts +2 -0
  28. package/dist-types/components/input-stepper/test/lion-input-stepper-integrations.test.d.ts.map +1 -0
  29. package/dist-types/components/input-stepper/test/lion-input-stepper.test.d.ts.map +1 -1
  30. package/dist-types/components/overlays/src/OverlayController.d.ts +7 -0
  31. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  32. package/dist-types/components/overlays/src/OverlayMixin.d.ts.map +1 -1
  33. package/dist-types/components/overlays/src/utils/deep-contains.d.ts +15 -1
  34. package/dist-types/components/overlays/src/utils/deep-contains.d.ts.map +1 -1
  35. package/dist-types/components/select-rich/src/LionSelectRich.d.ts.map +1 -1
  36. package/dist-types/tsconfig.tsbuildinfo +1 -1
  37. package/docs/components/button/use-cases.md +2 -2
  38. package/docs/components/calendar/use-cases.md +4 -4
  39. package/docs/components/checkbox-group/api-table.md +49 -55
  40. package/docs/components/checkbox-group/overview.md +5 -5
  41. package/docs/components/checkbox-group/use-cases.md +33 -33
  42. package/docs/components/collapsible/overview.md +1 -1
  43. package/docs/components/collapsible/use-cases.md +3 -3
  44. package/docs/components/combobox/api-table.md +84 -89
  45. package/docs/components/combobox/extensions.md +20 -20
  46. package/docs/components/combobox/overview.md +1 -1
  47. package/docs/components/combobox/use-cases.md +6 -6
  48. package/docs/components/dialog/api-table.md +3 -3
  49. package/docs/components/dialog/overview.md +2 -2
  50. package/docs/components/dialog/src/external-dialog.js +32 -0
  51. package/docs/components/dialog/src/slots-dialog-content.js +1 -1
  52. package/docs/components/dialog/src/styled-dialog-content.js +1 -1
  53. package/docs/components/dialog/use-cases.md +49 -3
  54. package/docs/components/drawer/use-cases.md +3 -3
  55. package/docs/components/fieldset/api-table.md +45 -51
  56. package/docs/components/fieldset/overview.md +7 -7
  57. package/docs/components/fieldset/use-cases.md +15 -15
  58. package/docs/components/form/overview.md +7 -7
  59. package/docs/components/form/use-cases.md +4 -4
  60. package/docs/components/icon/use-cases.md +1 -1
  61. package/docs/components/input/overview.md +1 -1
  62. package/docs/components/input/use-cases.md +3 -3
  63. package/docs/components/input-amount/overview.md +3 -3
  64. package/docs/components/input-amount/use-cases.md +8 -8
  65. package/docs/components/input-date/overview.md +3 -3
  66. package/docs/components/input-date/use-cases.md +8 -8
  67. package/docs/components/input-datepicker/api-table.md +1 -1
  68. package/docs/components/input-datepicker/overview.md +5 -5
  69. package/docs/components/input-datepicker/use-cases.md +5 -5
  70. package/docs/components/input-email/overview.md +2 -2
  71. package/docs/components/input-email/use-cases.md +3 -3
  72. package/docs/components/input-file/overview.md +1 -1
  73. package/docs/components/input-iban/overview.md +2 -2
  74. package/docs/components/input-iban/use-cases.md +8 -8
  75. package/docs/components/input-range/overview.md +2 -2
  76. package/docs/components/input-stepper/api-table.md +65 -69
  77. package/docs/components/input-stepper/overview.md +1 -1
  78. package/docs/components/input-stepper/use-cases.md +21 -0
  79. package/docs/components/input-tel/overview.md +1 -1
  80. package/docs/components/input-tel/use-cases.md +9 -9
  81. package/docs/components/input-tel-dropdown/api-table.md +70 -73
  82. package/docs/components/input-tel-dropdown/extensions.md +1 -1
  83. package/docs/components/input-tel-dropdown/overview.md +1 -1
  84. package/docs/components/input-tel-dropdown/use-cases.md +5 -5
  85. package/docs/components/listbox/api-table.md +64 -70
  86. package/docs/components/listbox/overview.md +5 -5
  87. package/docs/components/listbox/use-cases.md +61 -61
  88. package/docs/components/pagination/use-cases.md +5 -5
  89. package/docs/components/radio-group/api-table.md +49 -55
  90. package/docs/components/radio-group/overview.md +6 -6
  91. package/docs/components/radio-group/use-cases.md +18 -18
  92. package/docs/components/select/use-cases.md +2 -2
  93. package/docs/components/select-rich/api-table.md +79 -85
  94. package/docs/components/select-rich/overview.md +3 -3
  95. package/docs/components/select-rich/use-cases.md +33 -33
  96. package/docs/components/steps/overview.md +4 -4
  97. package/docs/components/steps/use-cases.md +7 -7
  98. package/docs/components/tabs/examples.md +1 -1
  99. package/docs/components/tabs/use-cases.md +1 -1
  100. package/docs/components/textarea/use-cases.md +3 -3
  101. package/docs/components/tooltip/api-table.md +3 -3
  102. package/docs/components/tooltip/use-cases.md +6 -6
  103. package/docs/fundamentals/index.md +4 -4
  104. package/docs/fundamentals/node-tools/providence-analytics/LocalConfiguration.md +1 -1
  105. package/docs/fundamentals/node-tools/providence-analytics/QueryResult.md +1 -1
  106. package/docs/fundamentals/node-tools/providence-analytics/analyzer.md +1 -1
  107. package/docs/fundamentals/node-tools/providence-analytics/dashboard.md +1 -1
  108. package/docs/fundamentals/node-tools/providence-analytics/overview.md +3 -3
  109. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  110. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  111. package/docs/fundamentals/rationales/accessibility.md +1 -1
  112. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  113. package/docs/fundamentals/systems/core/overview.md +1 -1
  114. package/docs/fundamentals/systems/form/formatting-and-parsing.md +8 -8
  115. package/docs/fundamentals/systems/form/model-value.md +1 -1
  116. package/docs/fundamentals/systems/form/overview.md +28 -28
  117. package/docs/fundamentals/systems/form/use-cases.md +19 -19
  118. package/docs/fundamentals/systems/form/validate.md +50 -50
  119. package/docs/fundamentals/systems/localize/api-table.md +10 -17
  120. package/docs/fundamentals/systems/localize/overview.md +4 -4
  121. package/docs/fundamentals/systems/localize/use-cases.md +4 -4
  122. package/docs/fundamentals/systems/overlays/_configuration-positioning.md +4 -4
  123. package/docs/fundamentals/systems/overlays/api-table.md +34 -90
  124. package/docs/fundamentals/systems/overlays/assets/umbrella-form.js +9 -9
  125. package/docs/fundamentals/systems/overlays/configuration.md +55 -29
  126. package/docs/fundamentals/systems/overlays/overview.md +6 -6
  127. package/docs/fundamentals/systems/overlays/use-cases.md +30 -30
  128. package/docs/fundamentals/tools/ajax/use-cases.md +20 -20
  129. package/docs/fundamentals/tools/helpers/action-logger.md +3 -3
  130. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  131. package/docs/fundamentals/tools/singleton-manager/example-complex/demo-app.js +2 -2
  132. package/docs/fundamentals/tools/singleton-manager/example-fail/demo-app.js +2 -2
  133. package/docs/fundamentals/tools/singleton-manager/example-success/demo-app.js +2 -2
  134. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  135. package/docs/guides/how-to/create-a-custom-field.md +7 -7
  136. package/docs/guides/how-to/create-a-form.md +4 -4
  137. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  138. package/docs/guides/index.md +8 -8
  139. package/docs/guides/principles/index.md +5 -5
  140. package/docs/guides/principles/scoped-elements.md +1 -1
  141. package/docs/guides/principles/styling.md +1 -1
  142. package/package.json +1 -1
@@ -201,6 +201,8 @@ export class OverlayController extends EventTarget {
201
201
  this.__hasActiveBackdrop = true;
202
202
  /** @private */
203
203
  this.__escKeyHandler = this.__escKeyHandler.bind(this);
204
+ /** @private */
205
+ this.__cancelHandler = this.__cancelHandler.bind(this);
204
206
  }
205
207
 
206
208
  /**
@@ -510,6 +512,8 @@ export class OverlayController extends EventTarget {
510
512
  this.__contentHasBeenInitialized = true;
511
513
  }
512
514
 
515
+ this.__wrappingDialogNode?.addEventListener('cancel', this.__cancelHandler);
516
+
513
517
  // Reset all positioning styles (local, c.q. Popper) and classes (global)
514
518
  this.contentWrapperNode.removeAttribute('style');
515
519
  this.contentWrapperNode.removeAttribute('class');
@@ -636,13 +640,14 @@ export class OverlayController extends EventTarget {
636
640
  __setupTeardownAccessibility({ phase }) {
637
641
  if (phase === 'init') {
638
642
  this.__storeOriginalAttrs(this.contentNode, ['role', 'id']);
643
+ const isModal = this.trapsKeyboardFocus;
639
644
 
640
645
  if (this.invokerNode) {
641
- this.__storeOriginalAttrs(this.invokerNode, [
642
- 'aria-expanded',
643
- 'aria-labelledby',
644
- 'aria-describedby',
645
- ]);
646
+ const attributesToStore = ['aria-labelledby', 'aria-describedby'];
647
+ if (!isModal) {
648
+ attributesToStore.push('aria-expanded');
649
+ }
650
+ this.__storeOriginalAttrs(this.invokerNode, attributesToStore);
646
651
  }
647
652
 
648
653
  if (!this.contentNode.id) {
@@ -657,7 +662,7 @@ export class OverlayController extends EventTarget {
657
662
  }
658
663
  this.contentNode.setAttribute('role', 'tooltip');
659
664
  } else {
660
- if (this.invokerNode) {
665
+ if (this.invokerNode && !isModal) {
661
666
  this.invokerNode.setAttribute('aria-expanded', `${this.isShown}`);
662
667
  }
663
668
  if (!this.contentNode.getAttribute('role')) {
@@ -783,6 +788,10 @@ export class OverlayController extends EventTarget {
783
788
  * @protected
784
789
  */
785
790
  _keepBodySize({ phase }) {
791
+ if (!this.preventsScroll) {
792
+ return;
793
+ }
794
+
786
795
  switch (phase) {
787
796
  case 'before-show':
788
797
  this.__bodyClientWidth = document.body.clientWidth;
@@ -1126,6 +1135,17 @@ export class OverlayController extends EventTarget {
1126
1135
  }
1127
1136
  }
1128
1137
 
1138
+ /**
1139
+ * When the overlay is a modal dialog hidesOnEsc works out of the box, so we prevent that.
1140
+ *
1141
+ * There is currently a bug in chrome that makes the dialog close when pressing Esc the second time
1142
+ * @private
1143
+ */
1144
+ // eslint-disable-next-line class-methods-use-this
1145
+ __cancelHandler(/** @type {Event} */ ev) {
1146
+ ev.preventDefault();
1147
+ }
1148
+
1129
1149
  /** @private */
1130
1150
  __escKeyHandler(/** @type {KeyboardEvent} */ ev) {
1131
1151
  return ev.key === 'Escape' && this.hide();
@@ -1284,7 +1304,8 @@ export class OverlayController extends EventTarget {
1284
1304
  if (phase === 'init' || phase === 'teardown') {
1285
1305
  this.__setupTeardownAccessibility({ phase });
1286
1306
  }
1287
- if (this.invokerNode && !this.isTooltip) {
1307
+ const isModal = this.trapsKeyboardFocus;
1308
+ if (this.invokerNode && !this.isTooltip && !isModal) {
1288
1309
  this.invokerNode.setAttribute('aria-expanded', `${phase === 'show'}`);
1289
1310
  }
1290
1311
  }
@@ -1292,6 +1313,7 @@ export class OverlayController extends EventTarget {
1292
1313
  teardown() {
1293
1314
  this.__handleOverlayStyles({ phase: 'teardown' });
1294
1315
  this._handleFeatures({ phase: 'teardown' });
1316
+ this.__wrappingDialogNode?.removeEventListener('cancel', this.__cancelHandler);
1295
1317
  }
1296
1318
 
1297
1319
  /** @private */
@@ -72,7 +72,11 @@ export const OverlayMixinImplementation = superclass =>
72
72
  requestUpdate(name, oldValue, options) {
73
73
  super.requestUpdate(name, oldValue, options);
74
74
  if (name === 'opened' && this.opened !== oldValue) {
75
- this.dispatchEvent(new Event('opened-changed'));
75
+ this.dispatchEvent(
76
+ new CustomEvent('opened-changed', {
77
+ detail: { opened: this.opened },
78
+ }),
79
+ );
76
80
  }
77
81
  }
78
82
 
@@ -1,20 +1,149 @@
1
+ /**
2
+ * A number, or a string containing a number.
3
+ * @typedef {{element: HTMLElement; deepContains: boolean} | null} CacheItem
4
+ */
5
+
1
6
  /**
2
7
  * Whether first element contains the second element, also goes through shadow roots
3
8
  * @param {HTMLElement|ShadowRoot} el
4
9
  * @param {HTMLElement|ShadowRoot} targetEl
10
+ * @param {{[key: string]: CacheItem[]}} cache
5
11
  * @returns {boolean}
6
12
  */
7
- export function deepContains(el, targetEl) {
13
+ export function deepContains(el, targetEl, cache = {}) {
14
+ /**
15
+ * @description A `Typescript` `type guard` for `HTMLElement`
16
+ * @param {Element|ShadowRoot} htmlElement
17
+ * @returns {htmlElement is HTMLElement}
18
+ */
19
+ function isHTMLElement(htmlElement) {
20
+ return 'getAttribute' in htmlElement;
21
+ }
22
+
23
+ /**
24
+ * @description Returns a cached item for the given element or null otherwise
25
+ * @param {HTMLElement|ShadowRoot} element
26
+ * @returns {CacheItem|null}
27
+ */
28
+ function getCachedItem(element) {
29
+ if (!isHTMLElement(element)) {
30
+ return null;
31
+ }
32
+ const slotName = element.getAttribute('slot');
33
+ /** @type {CacheItem|null} */
34
+ let result = null;
35
+ if (slotName) {
36
+ const cachedItemsWithSameName = cache[slotName];
37
+ if (cachedItemsWithSameName) {
38
+ result = cachedItemsWithSameName.filter(item => item?.element === element)[0] || null;
39
+ }
40
+ }
41
+ return result;
42
+ }
43
+
44
+ const cachedItem = getCachedItem(el);
45
+ if (cachedItem) {
46
+ return cachedItem.deepContains;
47
+ }
48
+
49
+ /**
50
+ * @description Cache an html element and its `deepContains` status
51
+ * @param {boolean} contains The `deepContains` status for the element
52
+ * @returns {void}
53
+ */
54
+ function cacheItem(contains) {
55
+ if (!isHTMLElement(el)) {
56
+ return;
57
+ }
58
+ const slotName = el.getAttribute('slot');
59
+ if (slotName) {
60
+ // eslint-disable-next-line no-param-reassign
61
+ cache[slotName] = cache[slotName] || [];
62
+ cache[slotName].push({ element: el, deepContains: contains });
63
+ }
64
+ }
65
+
8
66
  let containsTarget = el.contains(targetEl);
9
67
  if (containsTarget) {
68
+ cacheItem(true);
10
69
  return true;
11
70
  }
12
71
 
72
+ /**
73
+ * A `Typescript` `type guard` for `HTMLSlotElement`
74
+ * @param {HTMLElement|HTMLSlotElement} htmlElement
75
+ * @returns {htmlElement is HTMLSlotElement}
76
+ */
77
+ function isSlot(htmlElement) {
78
+ return htmlElement.tagName === 'SLOT';
79
+ }
80
+
81
+ /**
82
+ * Returns a slot projection or it returns `null` if `htmlElement` is not an `HTMLSlotElement`
83
+ * @example
84
+ * Let's say this is a custom element declared as follows:
85
+ * ```
86
+ * <custom-element>
87
+ * shadowRoot
88
+ * <div id="dialog-wrapper">
89
+ * <div id="dialog-header">Header</div>
90
+ * <div id="dialog-content">
91
+ * <slot id="dialog-content-slot" name="content"></slot>
92
+ * </div>
93
+ * </div>
94
+ * <!-- Light DOM -->
95
+ * <div id="my-slot-content" slot="content">my content</div>
96
+ * </custom-element>
97
+ * ```
98
+ * Then for `slot#dialog-content-slot` which is defined in the ShadowDom the function returns `div#my-slot-content` which is defined in the LightDom
99
+ * @param {HTMLElement|HTMLSlotElement} htmlElement
100
+ * @returns {Element[]}
101
+ * */
102
+ function getSlotProjections(htmlElement) {
103
+ return isSlot(htmlElement) ? /** @type {Element[]} */ (htmlElement.assignedElements()) : [];
104
+ }
105
+
106
+ /**
107
+ * @description A `Typescript` `type guard` for `ShadowRoot`
108
+ * @param {Element|ShadowRoot} htmlElement
109
+ * @returns {htmlElement is ShadowRoot}
110
+ */
111
+ function isShadowRoot(htmlElement) {
112
+ return htmlElement.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
113
+ }
114
+
115
+ /**
116
+ * Check whether any element contains target
117
+ * @param {(Element|ShadowRoot|null)[]} elements
118
+ * */
119
+ function checkElements(elements) {
120
+ let contains = false;
121
+ for (let i = 0; i < elements.length; i += 1) {
122
+ const element = elements[i];
123
+ if (
124
+ element &&
125
+ (isHTMLElement(element) || isShadowRoot(element)) &&
126
+ deepContains(element, targetEl, cache)
127
+ ) {
128
+ contains = true;
129
+ break;
130
+ }
131
+ }
132
+ return contains;
133
+ }
134
+
13
135
  /** @param {HTMLElement|ShadowRoot} elem */
14
136
  function checkChildren(elem) {
15
137
  for (let i = 0; i < elem.children.length; i += 1) {
16
138
  const child = /** @type {HTMLElement} */ (elem.children[i]);
17
- if (child.shadowRoot && deepContains(child.shadowRoot, targetEl)) {
139
+ const cachedChild = getCachedItem(child);
140
+ if (cachedChild) {
141
+ containsTarget = cachedChild.deepContains || containsTarget;
142
+ break;
143
+ }
144
+ const slotProjections = getSlotProjections(child);
145
+ const childSubElements = [child.shadowRoot, ...slotProjections];
146
+ if (checkElements(childSubElements)) {
18
147
  containsTarget = true;
19
148
  break;
20
149
  }
@@ -26,11 +155,13 @@ export function deepContains(el, targetEl) {
26
155
 
27
156
  // If element is not shadowRoot itself
28
157
  if (el instanceof HTMLElement && el.shadowRoot) {
29
- containsTarget = deepContains(el.shadowRoot, targetEl);
158
+ containsTarget = deepContains(el.shadowRoot, targetEl, cache);
30
159
  if (containsTarget) {
160
+ cacheItem(true);
31
161
  return true;
32
162
  }
33
163
  }
34
164
  checkChildren(el);
165
+ cacheItem(containsTarget);
35
166
  return containsTarget;
36
167
  }
@@ -585,6 +585,17 @@ describe('OverlayController', () => {
585
585
  expect(ctrl.isShown).to.be.false;
586
586
  });
587
587
 
588
+ it("doesn't hide when [escape] is pressed and hidesOnEsc is set to false", async () => {
589
+ const ctrl = new OverlayController({
590
+ ...withGlobalTestConfig(),
591
+ hidesOnEsc: false,
592
+ });
593
+ await ctrl.show();
594
+ ctrl.contentNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' }));
595
+ await aTimeout(0);
596
+ expect(ctrl.isShown).to.be.true;
597
+ });
598
+
588
599
  it('stays shown when [escape] is pressed on outside element', async () => {
589
600
  const ctrl = new OverlayController({
590
601
  ...withGlobalTestConfig(),
@@ -1517,6 +1528,23 @@ describe('OverlayController', () => {
1517
1528
  expect(ctrl.invokerNode?.getAttribute('aria-expanded')).to.equal('false');
1518
1529
  });
1519
1530
 
1531
+ it('does not synchronize [aria-expanded] on invoker when the overlay is modal', async () => {
1532
+ const invokerNode = /** @type {HTMLElement} */ (
1533
+ await fixture('<div role="button">invoker</div>')
1534
+ );
1535
+ const ctrl = new OverlayController({
1536
+ ...withLocalTestConfig(),
1537
+ trapsKeyboardFocus: true,
1538
+ handlesAccessibility: true,
1539
+ invokerNode,
1540
+ });
1541
+ expect(ctrl.invokerNode?.getAttribute('aria-expanded')).to.equal(null);
1542
+ await ctrl.show();
1543
+ expect(ctrl.invokerNode?.getAttribute('aria-expanded')).to.equal(null);
1544
+ await ctrl.hide();
1545
+ expect(ctrl.invokerNode?.getAttribute('aria-expanded')).to.equal(null);
1546
+ });
1547
+
1520
1548
  it('creates unique id for content', async () => {
1521
1549
  const ctrl = new OverlayController({
1522
1550
  ...withLocalTestConfig(),
@@ -1753,4 +1781,33 @@ describe('OverlayController', () => {
1753
1781
  );
1754
1782
  });
1755
1783
  });
1784
+
1785
+ describe('run _keepBodySize only with scroll prevention', () => {
1786
+ /**
1787
+ * @type {OverlayController}
1788
+ */
1789
+ const overlayControllerNoPrevent = new OverlayController({
1790
+ ...withLocalTestConfig(),
1791
+ preventsScroll: false,
1792
+ });
1793
+
1794
+ const overlayControllerPreventsScroll = new OverlayController({
1795
+ ...withLocalTestConfig(),
1796
+ preventsScroll: true,
1797
+ });
1798
+
1799
+ it('should not run with scroll prevention', async () => {
1800
+ await overlayControllerNoPrevent.show();
1801
+
1802
+ expect(overlayControllerNoPrevent.__bodyMarginRightInline).to.equal(undefined);
1803
+ expect(overlayControllerNoPrevent.__bodyMarginRight).to.equal(undefined);
1804
+ });
1805
+
1806
+ it('should run with scroll prevention', async () => {
1807
+ await overlayControllerPreventsScroll.show();
1808
+
1809
+ expect(overlayControllerPreventsScroll.__bodyMarginRightInline).to.not.equal(undefined);
1810
+ expect(overlayControllerPreventsScroll.__bodyMarginRight).to.not.equal(undefined);
1811
+ });
1812
+ });
1756
1813
  });
@@ -124,4 +124,102 @@ describe('deepContains()', () => {
124
124
  expect(deepContains(element, elementFirstChildShadowChildShadow)).to.be.true;
125
125
  expect(deepContains(element, elementFirstChildShadowChildShadowLastChild)).to.be.true;
126
126
  });
127
+
128
+ it('returns true if the element, which is located in ShadowsRoot, contains a target element, located in the LightDom', async () => {
129
+ const mainElement = /** @type {HTMLElement} */ (await fixture('<div id="main"></div>'));
130
+ mainElement.innerHTML = `
131
+ <div slot="content" id="light-el-content">
132
+ <input type="text" id="light-el-input-1"></input>
133
+ </div>
134
+ <div slot="content" id="light-el-content">
135
+ <input type="text" id="light-el-input-2"></input>
136
+ </div>
137
+ `;
138
+ const shadowRoot = mainElement.attachShadow({ mode: 'open' });
139
+ shadowRoot.innerHTML = `
140
+ <div id="dialog-wrapper">
141
+ <div id="dialog-header">
142
+ Header
143
+ </div>
144
+ <div id="dialog-content">
145
+ <slot name="content" id="shadow-el-content"></slot>
146
+ </div>
147
+ </div>
148
+ `;
149
+ const inputElement = /** @type {HTMLElement} */ (
150
+ mainElement.querySelector('#light-el-input-2')
151
+ );
152
+ const dialogWrapperElement = /** @type {HTMLElement} */ (
153
+ shadowRoot.querySelector('#dialog-wrapper')
154
+ );
155
+ expect(deepContains(dialogWrapperElement, inputElement)).to.be.true;
156
+ });
157
+
158
+ it(`returns true if the element, which is located in ShadowRoot, contains a target element, located in the ShadowRoot element of the LightDom element `, async () => {
159
+ /**
160
+ * The DOM for the `main` element looks as follows:
161
+ *
162
+ * <div id="main">
163
+ * #shadow-root
164
+ * <div id="dialog-wrapper"> // dialogWrapperElement
165
+ * <div id="dialog-header">
166
+ * Header
167
+ * </div>
168
+ * <div id="dialog-content">
169
+ * <slot name="content" id="shadow-el-content"></slot>
170
+ * </div>
171
+ * </div>
172
+ * <div slot="content" id="light-el-content">
173
+ * <div id="conent-wrapper">
174
+ * #shadow-root
175
+ * <div id="conent-wrapper-sub">
176
+ * #shadow-root
177
+ * <input type="type" id="content-input"></input> //inputElement
178
+ * </div>
179
+ * </div>
180
+ * </div>
181
+ * </div>
182
+ */
183
+ const mainElement = /** @type {HTMLElement} */ (await fixture('<div id="main"></div>'));
184
+ mainElement.innerHTML = `
185
+ <div slot="content" id="light-el-content">
186
+ <div id="content-wrapper"></div>
187
+ </div>
188
+ <div slot="content" id="light-el-content">
189
+ <div id="content-wrapper"></div>
190
+ </div>
191
+ `;
192
+ const contentWrapper = /** @type {HTMLElement} */ (
193
+ mainElement.querySelector('#content-wrapper')
194
+ );
195
+ const contentWrapperShadowRoot = contentWrapper.attachShadow({ mode: 'open' });
196
+ contentWrapperShadowRoot.innerHTML = `
197
+ <div id="conent-wrapper-sub"></div>
198
+ `;
199
+ const contentWrapperSub = /** @type {HTMLElement} */ (
200
+ contentWrapperShadowRoot.querySelector('#conent-wrapper-sub')
201
+ );
202
+ const contentWrapperSubShadowRoot = contentWrapperSub.attachShadow({ mode: 'open' });
203
+ contentWrapperSubShadowRoot.innerHTML = `
204
+ <input type="type" id="content-input"></input>
205
+ `;
206
+ const inputElement = /** @type {HTMLElement} */ (
207
+ contentWrapperSubShadowRoot.querySelector('#content-input')
208
+ );
209
+ const mainElementShadowRoot = mainElement.attachShadow({ mode: 'open' });
210
+ mainElementShadowRoot.innerHTML = `
211
+ <div id="dialog-wrapper">
212
+ <div id="dialog-header">
213
+ Header
214
+ </div>
215
+ <div id="dialog-content">
216
+ <slot name="content" id="shadow-el-content"></slot>
217
+ </div>
218
+ </div>
219
+ `;
220
+ const dialogWrapperElement = /** @type {HTMLElement} */ (
221
+ mainElementShadowRoot.querySelector('#dialog-wrapper')
222
+ );
223
+ expect(deepContains(dialogWrapperElement, inputElement)).to.be.true;
224
+ });
127
225
  });
@@ -196,6 +196,18 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
196
196
  }
197
197
  }
198
198
 
199
+ if (changedProperties.has('singleOption')) {
200
+ if (this.singleOption) {
201
+ this._invokerNode.removeAttribute('role');
202
+ this._invokerNode.removeAttribute('aria-haspopup');
203
+ this._invokerNode.removeAttribute('aria-expanded');
204
+ } else {
205
+ this._invokerNode.setAttribute('role', 'button');
206
+ this._invokerNode.setAttribute('aria-haspopup', 'listbox');
207
+ this._invokerNode.setAttribute('aria-expanded', `${this.opened}`);
208
+ }
209
+ }
210
+
199
211
  if (this._inputNode && this._invokerNode) {
200
212
  if (changedProperties.has('_ariaLabelledNodes')) {
201
213
  this._invokerNode.setAttribute(
@@ -148,6 +148,32 @@ describe('lion-select-rich', () => {
148
148
  expect(_invokerNode.hasAttribute('single-option')).to.be.true;
149
149
  });
150
150
 
151
+ it('sets and removes the button role and aria attributes on change of singleOption', async () => {
152
+ const el = await fixture(html`
153
+ <lion-select-rich>
154
+ <lion-option .choiceValue=${10}>Item 1</lion-option>
155
+ <lion-option .choiceValue=${20}>Item 2</lion-option>
156
+ </lion-select-rich>
157
+ `);
158
+ const { _invokerNode } = getSelectRichMembers(el);
159
+
160
+ expect(_invokerNode.hasAttribute('role')).to.be.true;
161
+ expect(_invokerNode.hasAttribute('aria-haspopup')).to.be.true;
162
+ expect(_invokerNode.hasAttribute('aria-expanded')).to.be.true;
163
+
164
+ el.singleOption = true;
165
+ await el.updateComplete;
166
+ expect(_invokerNode.hasAttribute('role')).to.be.false;
167
+ expect(_invokerNode.hasAttribute('aria-haspopup')).to.be.false;
168
+ expect(_invokerNode.hasAttribute('aria-expanded')).to.be.false;
169
+
170
+ el.singleOption = false;
171
+ await el.updateComplete;
172
+ expect(_invokerNode.hasAttribute('role')).to.be.true;
173
+ expect(_invokerNode.hasAttribute('aria-haspopup')).to.be.true;
174
+ expect(_invokerNode.hasAttribute('aria-expanded')).to.be.true;
175
+ });
176
+
151
177
  it('updates the invoker when the selected element is the same but the modelValue was updated asynchronously', async () => {
152
178
  const tagString = defineCE(
153
179
  class LionCustomOption extends LionOption {