@lion/ui 0.6.0-beta.0 → 0.6.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 (166) hide show
  1. package/components/core/src/SlotMixin.js +107 -65
  2. package/components/core/test/SlotMixin.test.js +64 -1
  3. package/components/core/types/SlotMixinTypes.ts +8 -0
  4. package/components/form-core/src/choice-group/ChoiceGroupMixin.js +10 -1
  5. package/components/form-core/src/registration/FormRegisteringMixin.js +14 -0
  6. package/components/form-core/src/validate/ValidateMixin.js +10 -0
  7. package/components/form-core/src/validate/Validator.js +6 -3
  8. package/components/form-core/test-suites/FormRegistrationMixins.suite.js +64 -9
  9. package/components/form-core/types/FormControlMixinTypes.ts +2 -0
  10. package/components/form-core/types/registration/FormRegisteringMixinTypes.ts +9 -1
  11. package/components/form-core/types/validate/ValidateMixinTypes.ts +2 -0
  12. package/components/form-core/types/validate/validate.ts +2 -2
  13. package/components/input-file/src/LionInputFile.js +9 -2
  14. package/components/input-tel-dropdown/src/LionInputTelDropdown.js +12 -3
  15. package/components/input-tel-dropdown/test/LionInputTelDropdown.test.js +110 -2
  16. package/components/input-tel-dropdown/test-helpers/mimicUserChangingDropdown.js +22 -0
  17. package/components/input-tel-dropdown/test-suites/LionInputTelDropdown.suite.js +2 -39
  18. package/components/localize/src/LocalizeMixin.js +2 -2
  19. package/components/overlays/src/OverlayController.js +1 -1
  20. package/components/select/src/LionSelect.js +5 -0
  21. package/components/validate-messages/src/getLocalizedMessage.js +12 -1
  22. package/components/validate-messages/test/loadDefaultFeedbackMessagesNoSideEffects.test.js +105 -0
  23. package/components/validate-messages/translations/bg.js +4 -0
  24. package/components/validate-messages/translations/cs.js +4 -0
  25. package/components/validate-messages/translations/de.js +4 -0
  26. package/components/validate-messages/translations/en.js +5 -1
  27. package/components/validate-messages/translations/es.js +4 -0
  28. package/components/validate-messages/translations/fr.js +4 -0
  29. package/components/validate-messages/translations/hu.js +4 -0
  30. package/components/validate-messages/translations/it.js +4 -0
  31. package/components/validate-messages/translations/nl.js +4 -0
  32. package/components/validate-messages/translations/pl.js +4 -0
  33. package/components/validate-messages/translations/ro.js +4 -0
  34. package/components/validate-messages/translations/ru.js +4 -0
  35. package/components/validate-messages/translations/sk.js +4 -0
  36. package/components/validate-messages/translations/uk.js +4 -0
  37. package/components/validate-messages/translations/zh.js +4 -0
  38. package/custom-elements.json +5712 -1960
  39. package/dist-types/components/core/src/SlotMixin.d.ts +5 -1
  40. package/dist-types/components/core/src/SlotMixin.d.ts.map +1 -1
  41. package/dist-types/components/core/types/SlotMixinTypes.d.ts +8 -0
  42. package/dist-types/components/core/types/SlotMixinTypes.d.ts.map +1 -1
  43. package/dist-types/components/form-core/src/choice-group/ChoiceGroupMixin.d.ts +1 -0
  44. package/dist-types/components/form-core/src/choice-group/ChoiceGroupMixin.d.ts.map +1 -1
  45. package/dist-types/components/form-core/src/registration/FormRegisteringMixin.d.ts.map +1 -1
  46. package/dist-types/components/form-core/src/validate/ValidateMixin.d.ts +1 -0
  47. package/dist-types/components/form-core/src/validate/ValidateMixin.d.ts.map +1 -1
  48. package/dist-types/components/form-core/src/validate/Validator.d.ts +5 -2
  49. package/dist-types/components/form-core/src/validate/Validator.d.ts.map +1 -1
  50. package/dist-types/components/form-core/test-suites/FormRegistrationMixins.suite.d.ts.map +1 -1
  51. package/dist-types/components/form-core/types/FormControlMixinTypes.d.ts +1 -0
  52. package/dist-types/components/form-core/types/FormControlMixinTypes.d.ts.map +1 -1
  53. package/dist-types/components/form-core/types/registration/FormRegisteringMixinTypes.d.ts +9 -1
  54. package/dist-types/components/form-core/types/registration/FormRegisteringMixinTypes.d.ts.map +1 -1
  55. package/dist-types/components/form-core/types/validate/ValidateMixinTypes.d.ts +1 -0
  56. package/dist-types/components/form-core/types/validate/ValidateMixinTypes.d.ts.map +1 -1
  57. package/dist-types/components/form-core/types/validate/validate.d.ts +2 -2
  58. package/dist-types/components/input-file/src/LionInputFile.d.ts +1 -0
  59. package/dist-types/components/input-file/src/LionInputFile.d.ts.map +1 -1
  60. package/dist-types/components/input-tel-dropdown/src/LionInputTelDropdown.d.ts +1 -0
  61. package/dist-types/components/input-tel-dropdown/src/LionInputTelDropdown.d.ts.map +1 -1
  62. package/dist-types/components/input-tel-dropdown/test-helpers/mimicUserChangingDropdown.d.ts +12 -0
  63. package/dist-types/components/input-tel-dropdown/test-helpers/mimicUserChangingDropdown.d.ts.map +1 -0
  64. package/dist-types/components/input-tel-dropdown/test-suites/LionInputTelDropdown.suite.d.ts.map +1 -1
  65. package/dist-types/components/select/src/LionSelect.d.ts.map +1 -1
  66. package/dist-types/components/validate-messages/src/getLocalizedMessage.d.ts.map +1 -1
  67. package/dist-types/components/validate-messages/test/loadDefaultFeedbackMessagesNoSideEffects.test.d.ts +4 -0
  68. package/dist-types/components/validate-messages/test/loadDefaultFeedbackMessagesNoSideEffects.test.d.ts.map +1 -0
  69. package/dist-types/components/validate-messages/translations/bg-BG.d.ts +4 -0
  70. package/dist-types/components/validate-messages/translations/bg.d.ts +6 -0
  71. package/dist-types/components/validate-messages/translations/cs-CZ.d.ts +4 -0
  72. package/dist-types/components/validate-messages/translations/cs.d.ts +6 -0
  73. package/dist-types/components/validate-messages/translations/de-DE.d.ts +4 -0
  74. package/dist-types/components/validate-messages/translations/de.d.ts +6 -0
  75. package/dist-types/components/validate-messages/translations/en-AU.d.ts +4 -0
  76. package/dist-types/components/validate-messages/translations/en-GB.d.ts +4 -0
  77. package/dist-types/components/validate-messages/translations/en-PH.d.ts +4 -0
  78. package/dist-types/components/validate-messages/translations/en-US.d.ts +4 -0
  79. package/dist-types/components/validate-messages/translations/en.d.ts +6 -0
  80. package/dist-types/components/validate-messages/translations/es-ES.d.ts +4 -0
  81. package/dist-types/components/validate-messages/translations/es.d.ts +6 -0
  82. package/dist-types/components/validate-messages/translations/fr-BE.d.ts +4 -0
  83. package/dist-types/components/validate-messages/translations/fr-FR.d.ts +4 -0
  84. package/dist-types/components/validate-messages/translations/fr.d.ts +6 -0
  85. package/dist-types/components/validate-messages/translations/hu-HU.d.ts +4 -0
  86. package/dist-types/components/validate-messages/translations/hu.d.ts +6 -0
  87. package/dist-types/components/validate-messages/translations/it-IT.d.ts +4 -0
  88. package/dist-types/components/validate-messages/translations/it.d.ts +6 -0
  89. package/dist-types/components/validate-messages/translations/nl-BE.d.ts +4 -0
  90. package/dist-types/components/validate-messages/translations/nl-NL.d.ts +4 -0
  91. package/dist-types/components/validate-messages/translations/nl.d.ts +6 -0
  92. package/dist-types/components/validate-messages/translations/pl-PL.d.ts +4 -0
  93. package/dist-types/components/validate-messages/translations/pl.d.ts +6 -0
  94. package/dist-types/components/validate-messages/translations/ro-RO.d.ts +4 -0
  95. package/dist-types/components/validate-messages/translations/ro.d.ts +6 -0
  96. package/dist-types/components/validate-messages/translations/ru-RU.d.ts +4 -0
  97. package/dist-types/components/validate-messages/translations/ru.d.ts +6 -0
  98. package/dist-types/components/validate-messages/translations/sk-SK.d.ts +4 -0
  99. package/dist-types/components/validate-messages/translations/sk.d.ts +6 -0
  100. package/dist-types/components/validate-messages/translations/uk-UA.d.ts +4 -0
  101. package/dist-types/components/validate-messages/translations/uk.d.ts +6 -0
  102. package/dist-types/components/validate-messages/translations/zh.d.ts +6 -0
  103. package/dist-types/exports/input-tel-dropdown-test-helpers.d.ts +2 -0
  104. package/dist-types/exports/input-tel-dropdown-test-helpers.d.ts.map +1 -0
  105. package/dist-types/tsconfig.tsbuildinfo +1 -1
  106. package/docs/components/checkbox-group/overview.md +2 -2
  107. package/docs/components/collapsible/overview.md +1 -1
  108. package/docs/components/combobox/overview.md +1 -1
  109. package/docs/components/combobox/use-cases.md +1 -1
  110. package/docs/components/dialog/overview.md +1 -1
  111. package/docs/components/fieldset/overview.md +5 -5
  112. package/docs/components/form/overview.md +3 -3
  113. package/docs/components/icon/use-cases.md +1 -1
  114. package/docs/components/input/overview.md +1 -1
  115. package/docs/components/input-amount/overview.md +3 -3
  116. package/docs/components/input-date/overview.md +3 -3
  117. package/docs/components/input-datepicker/overview.md +5 -5
  118. package/docs/components/input-email/overview.md +2 -2
  119. package/docs/components/input-file/overview.md +1 -1
  120. package/docs/components/input-file/use-cases.md +8 -8
  121. package/docs/components/input-iban/overview.md +2 -2
  122. package/docs/components/input-range/overview.md +2 -2
  123. package/docs/components/input-stepper/overview.md +1 -1
  124. package/docs/components/input-tel/overview.md +1 -1
  125. package/docs/components/input-tel/use-cases.md +6 -6
  126. package/docs/components/input-tel-dropdown/overview.md +1 -1
  127. package/docs/components/listbox/use-cases.md +90 -95
  128. package/docs/components/radio-group/overview.md +3 -3
  129. package/docs/components/select/use-cases.md +34 -33
  130. package/docs/components/select-rich/use-cases.md +1 -1
  131. package/docs/components/tabs/examples.md +1 -1
  132. package/docs/fundamentals/index.md +4 -4
  133. package/docs/fundamentals/node-tools/providence-analytics/LocalConfiguration.md +1 -1
  134. package/docs/fundamentals/node-tools/providence-analytics/QueryResult.md +1 -1
  135. package/docs/fundamentals/node-tools/providence-analytics/analyzer.md +1 -1
  136. package/docs/fundamentals/node-tools/providence-analytics/dashboard.md +1 -1
  137. package/docs/fundamentals/node-tools/providence-analytics/overview.md +3 -3
  138. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  139. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  140. package/docs/fundamentals/rationales/accessibility.md +1 -1
  141. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  142. package/docs/fundamentals/systems/core/overview.md +1 -1
  143. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  144. package/docs/fundamentals/systems/form/model-value.md +1 -1
  145. package/docs/fundamentals/systems/form/overview.md +28 -28
  146. package/docs/fundamentals/systems/form/styling.md +34 -39
  147. package/docs/fundamentals/systems/form/validate.md +7 -4
  148. package/docs/fundamentals/systems/localize/overview.md +4 -4
  149. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  150. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  151. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  152. package/docs/fundamentals/tools/ajax/overview.md +2 -1
  153. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  154. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  155. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  156. package/docs/guides/how-to/create-a-form.md +4 -4
  157. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  158. package/docs/guides/index.md +8 -8
  159. package/docs/guides/principles/index.md +5 -5
  160. package/docs/guides/principles/styling.md +1 -1
  161. package/exports/input-tel-dropdown-test-helpers.js +1 -0
  162. package/input-tel-dropdown-test-helpers.js +1 -0
  163. package/package.json +1 -1
  164. package/components/validate-messages/test/loadDefaultFeedbackMessagesNoSideEffects.js +0 -51
  165. package/dist-types/components/validate-messages/test/loadDefaultFeedbackMessagesNoSideEffects.d.ts +0 -2
  166. package/dist-types/components/validate-messages/test/loadDefaultFeedbackMessagesNoSideEffects.d.ts.map +0 -1
@@ -1,18 +1,37 @@
1
1
  /* eslint-disable class-methods-use-this */
2
+ import { isTemplateResult } from 'lit/directive-helpers.js';
2
3
  import { dedupeMixin } from '@open-wc/dedupe-mixin';
3
4
  import { render } from 'lit';
4
- import { isTemplateResult } from 'lit/directive-helpers.js';
5
5
 
6
6
  /**
7
- * @typedef {import('../types/SlotMixinTypes.js').SlotMixin} SlotMixin
8
- * @typedef {import('../types/SlotMixinTypes.js').SlotsMap} SlotsMap
7
+ * @typedef {{renderBefore:Comment; renderTargetThatRespectsShadowRootScoping: HTMLDivElement}} RenderMetaObj
9
8
  * @typedef {import('../types/SlotMixinTypes.js').SlotFunctionResult} SlotFunctionResult
10
9
  * @typedef {import('../types/SlotMixinTypes.js').SlotRerenderObject} SlotRerenderObject
10
+ * @typedef {import('../types/SlotMixinTypes.js').SlotMixin} SlotMixin
11
+ * @typedef {import('../types/SlotMixinTypes.js').SlotsMap} SlotsMap
11
12
  * @typedef {import('lit').LitElement} LitElement
12
13
  */
13
14
 
14
- const isRerenderConfig = (/** @type {SlotFunctionResult} */ o) =>
15
- !Array.isArray(o) && typeof o === 'object' && 'template' in o;
15
+ /**
16
+ * @param {SlotFunctionResult} slotFunctionResult
17
+ * @returns {'template-result'|'node'|'slot-rerender-object'|null}
18
+ */
19
+ function determineSlotFunctionResultType(slotFunctionResult) {
20
+ if (slotFunctionResult instanceof Node) {
21
+ return 'node';
22
+ }
23
+ if (isTemplateResult(slotFunctionResult)) {
24
+ return 'template-result';
25
+ }
26
+ if (
27
+ !Array.isArray(slotFunctionResult) &&
28
+ typeof slotFunctionResult === 'object' &&
29
+ 'template' in slotFunctionResult
30
+ ) {
31
+ return 'slot-rerender-object';
32
+ }
33
+ return null;
34
+ }
16
35
 
17
36
  /**
18
37
  * All intricacies involved in managing light dom can be delegated to SlotMixin. Amongst others, it automatically:
@@ -45,8 +64,8 @@ const SlotMixinImplementation = superclass =>
45
64
  * so that all interactions work as intended and no focus issues can arise (which would be the case
46
65
  * when (cloned) nodes of a render outcome would be moved around)
47
66
  * @private
48
- * @type { Map<string, HTMLDivElement> } */
49
- this.__scopedRenderRoots = new Map();
67
+ * @type { Map<string, RenderMetaObj> } */
68
+ this.__renderMetaPerSlot = new Map();
50
69
  /**
51
70
  * @private
52
71
  * @type {Set<string>}
@@ -79,9 +98,9 @@ const SlotMixinImplementation = superclass =>
79
98
  __rerenderSlot(slotName) {
80
99
  const slotFunctionResult = /** @type {SlotRerenderObject} */ (this.slots[slotName]());
81
100
  this.__renderTemplateInScopedContext({
101
+ renderAsDirectHostChild: slotFunctionResult.renderAsDirectHostChild,
82
102
  template: slotFunctionResult.template,
83
103
  slotName,
84
- shouldRerender: true,
85
104
  });
86
105
  // TODO: this is deprecated, remove later
87
106
  slotFunctionResult.afterRender?.();
@@ -94,14 +113,8 @@ const SlotMixinImplementation = superclass =>
94
113
  update(changedProperties) {
95
114
  super.update(changedProperties);
96
115
 
97
- if (this.__slotsThatNeedRerender.size) {
98
- for (const slotName of Array.from(this.__slotsThatNeedRerender)) {
99
- this.__rerenderSlot(slotName);
100
- }
101
- }
102
-
103
- if (this.__isFirstSlotUpdate) {
104
- this.__isFirstSlotUpdate = false;
116
+ for (const slotName of this.__slotsThatNeedRerender) {
117
+ this.__rerenderSlot(slotName);
105
118
  }
106
119
  }
107
120
 
@@ -110,32 +123,59 @@ const SlotMixinImplementation = superclass =>
110
123
  * @param {object} opts
111
124
  * @param {import('lit').TemplateResult} opts.template
112
125
  * @param {string} opts.slotName
113
- * @param {boolean} [opts.shouldRerender] false when TemplateResult, true when SlotRerenderObject
126
+ * @param {boolean} [opts.renderAsDirectHostChild] when false, the render parent (wrapper div) will be kept in the light dom
127
+ * @returns {void}
114
128
  */
115
- __renderTemplateInScopedContext({ template, slotName, shouldRerender }) {
116
- // @ts-expect-error wait for browser support
117
- const supportsScopedRegistry = !!ShadowRoot.prototype.createElement;
118
- const registryRoot = supportsScopedRegistry ? this.shadowRoot : document;
119
-
120
- let renderTarget;
121
- // Reuse the existing offline renderTargets for results consistent with that of rendering to one target (shadow dom)
122
- if (this.__scopedRenderRoots.has(slotName)) {
123
- renderTarget = this.__scopedRenderRoots.get(slotName);
124
- } else {
129
+ __renderTemplateInScopedContext({ template, slotName, renderAsDirectHostChild }) {
130
+ const isFirstRender = !this.__renderMetaPerSlot.has(slotName);
131
+ if (isFirstRender) {
132
+ // @ts-expect-error wait for browser support
133
+ const supportsScopedRegistry = !!ShadowRoot.prototype.createElement;
134
+ const registryRoot = supportsScopedRegistry ? this.shadowRoot || document : document;
135
+
125
136
  // @ts-expect-error wait for browser support
126
- renderTarget = registryRoot.createElement('div');
127
- if (shouldRerender) {
128
- renderTarget.slot = slotName;
129
- this.appendChild(renderTarget);
137
+ const renderTargetThatRespectsShadowRootScoping = registryRoot.createElement('div');
138
+ const startComment = document.createComment(`_start_slot_${slotName}_`);
139
+ const endComment = document.createComment(`_end_slot_${slotName}_`);
140
+
141
+ renderTargetThatRespectsShadowRootScoping.appendChild(startComment);
142
+ renderTargetThatRespectsShadowRootScoping.appendChild(endComment);
143
+
144
+ // Providing all options breaks Safari; keep host and creationScope
145
+ const { creationScope, host } = this.renderOptions;
146
+ render(template, renderTargetThatRespectsShadowRootScoping, {
147
+ renderBefore: endComment,
148
+ creationScope,
149
+ host,
150
+ });
151
+
152
+ if (renderAsDirectHostChild) {
153
+ const nodes = Array.from(renderTargetThatRespectsShadowRootScoping.childNodes);
154
+ this.__appendNodes({ nodes, renderParent: this, slotName });
155
+ } else {
156
+ renderTargetThatRespectsShadowRootScoping.slot = slotName;
157
+ this.appendChild(renderTargetThatRespectsShadowRootScoping);
130
158
  }
131
- this.__scopedRenderRoots.set(slotName, renderTarget);
159
+
160
+ this.__renderMetaPerSlot.set(slotName, {
161
+ renderTargetThatRespectsShadowRootScoping,
162
+ renderBefore: endComment,
163
+ });
164
+
165
+ return;
132
166
  }
133
167
 
134
- // Providing all options breaks Safari; keep host and creationScope
135
- const { creationScope, host } = this.renderOptions;
136
- render(template, renderTarget, { creationScope, host });
168
+ // Rerender
169
+ const { renderBefore, renderTargetThatRespectsShadowRootScoping } =
170
+ /** @type {RenderMetaObj} */ (this.__renderMetaPerSlot.get(slotName));
171
+
172
+ const rerenderTarget = renderAsDirectHostChild
173
+ ? this
174
+ : renderTargetThatRespectsShadowRootScoping;
137
175
 
138
- return renderTarget;
176
+ // Providing all options breaks Safari: we keep host and creationScope
177
+ const { creationScope, host } = this.renderOptions;
178
+ render(template, rerenderTarget, { creationScope, host, renderBefore });
139
179
  }
140
180
 
141
181
  /**
@@ -145,13 +185,8 @@ const SlotMixinImplementation = superclass =>
145
185
  * which can be used as a render target for most
146
186
  * @param {string} options.slotName For the first render, it's best to use slotName
147
187
  */
148
- __appendNodesForOneTimeRender({ nodes, renderParent = this, slotName }) {
188
+ __appendNodes({ nodes, renderParent = this, slotName }) {
149
189
  for (const node of nodes) {
150
- if (!(node instanceof Node)) {
151
- return;
152
- }
153
- // Here, we add the slot name to the node that is an element
154
- // (ignoring helper comment nodes that might be in our nodes array)
155
190
  if (node instanceof Element && slotName && slotName !== '') {
156
191
  node.setAttribute('slot', slotName);
157
192
  }
@@ -182,29 +217,36 @@ const SlotMixinImplementation = superclass =>
182
217
  this.__privateSlots.add(slotName);
183
218
  }
184
219
 
185
- if (isTemplateResult(slotFunctionResult)) {
186
- const renderTarget = this.__renderTemplateInScopedContext({
187
- template: slotFunctionResult,
188
- slotName,
189
- });
190
- const nodes = Array.from(renderTarget.childNodes);
191
- this.__appendNodesForOneTimeRender({ nodes, renderParent: this, slotName });
192
- } else if (slotFunctionResult instanceof Node) {
193
- const nodes = [/** @type {Node} */ (slotFunctionResult)];
194
- this.__appendNodesForOneTimeRender({ nodes, renderParent: this, slotName });
195
- } else if (isRerenderConfig(slotFunctionResult)) {
196
- // Rerenderable slots are scheduled in the "updated loop"
197
- this.__slotsThatNeedRerender.add(slotName);
198
-
199
- // For backw. compat, we allow a first render on connectedCallback
200
- if (slotFunctionResult.firstRenderOnConnected) {
201
- this.__rerenderSlot(slotName);
202
- }
203
- } else {
204
- throw new Error(
205
- `Slot "${slotName}" configured inside "get slots()" (in prototype) of ${this.constructor.name} may return these types: TemplateResult | Node | {template:TemplateResult, afterRender?:function} | undefined.
206
- You provided: ${slotFunctionResult}`,
207
- );
220
+ const slotFunctionResultType = determineSlotFunctionResultType(slotFunctionResult);
221
+
222
+ switch (slotFunctionResultType) {
223
+ case 'template-result':
224
+ this.__renderTemplateInScopedContext({
225
+ template: /** @type {import('lit').TemplateResult} */ (slotFunctionResult),
226
+ renderAsDirectHostChild: true,
227
+ slotName,
228
+ });
229
+ break;
230
+ case 'node':
231
+ this.__appendNodes({
232
+ nodes: [/** @type {Node} */ (slotFunctionResult)],
233
+ renderParent: this,
234
+ slotName,
235
+ });
236
+ break;
237
+ case 'slot-rerender-object':
238
+ // Rerenderable slots are scheduled in the "update loop"
239
+ this.__slotsThatNeedRerender.add(slotName);
240
+ // For backw. compat, we allow a first render on connectedCallback
241
+ if (/** @type {SlotRerenderObject} */ (slotFunctionResult).firstRenderOnConnected) {
242
+ this.__rerenderSlot(slotName);
243
+ }
244
+ break;
245
+ default:
246
+ throw new Error(
247
+ `Slot "${slotName}" configured inside "get slots()" (in prototype) of ${this.constructor.name} may return these types: TemplateResult | Node | {template:TemplateResult, afterRender?:function} | undefined.
248
+ You provided: ${slotFunctionResult}`,
249
+ );
208
250
  }
209
251
  }
210
252
  }
@@ -208,6 +208,7 @@ describe('SlotMixin', () => {
208
208
  ...super.slots,
209
209
  'focusable-node': () => ({
210
210
  template: html`<input /> `,
211
+ renderAsDirectHostChild: false,
211
212
  }),
212
213
  };
213
214
  }
@@ -225,7 +226,6 @@ describe('SlotMixin', () => {
225
226
  },
226
227
  );
227
228
  const el = /** @type {* & SlotHost} */ (await fixture(`<${tag}></${tag}>`));
228
-
229
229
  el._focusableNode.focus();
230
230
  expect(document.activeElement).to.equal(el._focusableNode);
231
231
 
@@ -298,6 +298,69 @@ describe('SlotMixin', () => {
298
298
  expect(el._focusableNode.shadowRoot.activeElement).to.equal(el._focusableNode._buttonNode);
299
299
  });
300
300
 
301
+ it('allows for rerendering complex shadow root into slot as a direct child', async () => {
302
+ const complexSlotTagName = defineCE(
303
+ class extends LitElement {
304
+ render() {
305
+ return html`
306
+ <input />
307
+ <button>I will be focused</button>
308
+ `;
309
+ }
310
+
311
+ get _buttonNode() {
312
+ // @ts-expect-error
313
+ return this.shadowRoot.querySelector('button');
314
+ }
315
+ },
316
+ );
317
+
318
+ const complexSlotTag = unsafeStatic(complexSlotTagName);
319
+
320
+ const tagName = defineCE(
321
+ // @ts-expect-error
322
+ class extends SlotMixin(LitElement) {
323
+ static properties = { currentValue: Number };
324
+
325
+ constructor() {
326
+ super();
327
+ this.currentValue = 0;
328
+ }
329
+
330
+ get slots() {
331
+ return {
332
+ ...super.slots,
333
+ 'focusable-node': () => ({
334
+ template: html`<${complexSlotTag}> </${complexSlotTag}> `,
335
+ renderAsDirectHostChild: true,
336
+ }),
337
+ };
338
+ }
339
+
340
+ render() {
341
+ return html`<slot name="focusable-node"></slot>`;
342
+ }
343
+
344
+ get _focusableNode() {
345
+ return /** @type HTMLSpanElement */ (
346
+ Array.from(this.children).find(elm => elm.slot === 'focusable-node')
347
+ );
348
+ }
349
+ },
350
+ );
351
+ const el = /** @type {* & SlotHost} */ (await fixture(`<${tagName}></${tagName}>`));
352
+
353
+ el._focusableNode._buttonNode.focus();
354
+
355
+ expect(el._focusableNode.shadowRoot.activeElement).to.equal(el._focusableNode._buttonNode);
356
+
357
+ el.currentValue = 1;
358
+ await el.updateComplete;
359
+
360
+ expect(document.activeElement).to.equal(el._focusableNode);
361
+ expect(el._focusableNode.shadowRoot.activeElement).to.equal(el._focusableNode._buttonNode);
362
+ });
363
+
301
364
  describe('firstRenderOnConnected (for backwards compatibility)', () => {
302
365
  it('does render on connected when firstRenderOnConnected:true', async () => {
303
366
  // Start with elem that does not render on connectedCallback
@@ -18,6 +18,14 @@ export type SlotRerenderObject = {
18
18
  * For new components, please align with ReactiveElement/LitElement reactive cycle callbacks.
19
19
  */
20
20
  firstRenderOnConnected?: boolean;
21
+ /**
22
+ * This is recommended to set to true always, as its behavior is usually desired and more in line with slot nodes that
23
+ * are not configured as rerenderable.
24
+ * For backward compatibility, it is set to false by default.
25
+ * When not configured, content is wrapped in a div (this can be problematic for ::slotted css selectors and for
26
+ * querySelectors that expect [slot=x] to have some semantic or (presentational) value).
27
+ */
28
+ renderAsDirectHostChild?: boolean;
21
29
  };
22
30
 
23
31
  export type SlotFunctionResult = TemplateResult | Element | SlotRerenderObject | undefined;
@@ -1,6 +1,7 @@
1
1
  import { dedupeMixin } from '@open-wc/dedupe-mixin';
2
2
  import { FormRegistrarMixin } from '../registration/FormRegistrarMixin.js';
3
3
  import { InteractionStateMixin } from '../InteractionStateMixin.js';
4
+ import { ValidateMixin } from '../validate/ValidateMixin.js';
4
5
 
5
6
  /**
6
7
  * @typedef {import('../../types/choice-group/ChoiceGroupMixinTypes.js').ChoiceGroupMixin} ChoiceGroupMixin
@@ -8,6 +9,7 @@ import { InteractionStateMixin } from '../InteractionStateMixin.js';
8
9
  * @typedef {import('../../types/registration/FormRegistrarMixinTypes.js').ElementWithParentFormGroup} ElementWithParentFormGroup
9
10
  * @typedef {import('../../types/form-group/FormGroupMixinTypes.js').FormControl} FormControl
10
11
  * @typedef {import('../../types/choice-group/ChoiceInputMixinTypes.js').ChoiceInputHost} ChoiceInputHost
12
+ * @typedef {import('../validate/Validator.js').Validator} Validator
11
13
  */
12
14
 
13
15
  /**
@@ -22,7 +24,9 @@ import { InteractionStateMixin } from '../InteractionStateMixin.js';
22
24
  */
23
25
  const ChoiceGroupMixinImplementation = superclass =>
24
26
  // @ts-ignore https://github.com/microsoft/TypeScript/issues/36821#issuecomment-588375051
25
- class ChoiceGroupMixin extends FormRegistrarMixin(InteractionStateMixin(superclass)) {
27
+ class ChoiceGroupMixin extends FormRegistrarMixin(
28
+ ValidateMixin(InteractionStateMixin(superclass)),
29
+ ) {
26
30
  /** @type {any} */
27
31
  static get properties() {
28
32
  return {
@@ -123,6 +127,11 @@ const ChoiceGroupMixinImplementation = superclass =>
123
127
  }
124
128
  }
125
129
 
130
+ get operationMode() {
131
+ // @ts-ignore
132
+ return this._repropagationRole === 'choice-group' ? 'select' : 'enter';
133
+ }
134
+
126
135
  constructor() {
127
136
  super();
128
137
 
@@ -28,6 +28,19 @@ const FormRegisteringMixinImplementation = superclass =>
28
28
  * @type {FormRegistrarHost | undefined}
29
29
  */
30
30
  this._parentFormGroup = undefined;
31
+ /**
32
+ * To encourage accessibility best practices, `form-element-register` events
33
+ * do not pierce through shadow roots. This forces the developer to create form groups and fieldsets that automatically allow the creation of accessible relationships in the same dom tree.
34
+ Use this option if you know what you're doing. It will then be possible to nest FormControls
35
+ inside shadow dom. See https://lion-web.netlify.app/fundamentals/rationales/accessibility/#shadow-roots-and-accessibility
36
+ */
37
+ this.allowCrossRootRegistration = false;
38
+ }
39
+
40
+ static get properties() {
41
+ return {
42
+ allowCrossRootRegistration: { type: Boolean, attribute: 'allow-cross-root-registration' },
43
+ };
31
44
  }
32
45
 
33
46
  connectedCallback() {
@@ -36,6 +49,7 @@ const FormRegisteringMixinImplementation = superclass =>
36
49
  new CustomEvent('form-element-register', {
37
50
  detail: { element: this },
38
51
  bubbles: true,
52
+ composed: Boolean(this.allowCrossRootRegistration),
39
53
  }),
40
54
  );
41
55
  }
@@ -20,6 +20,7 @@ import { FormControlMixin } from '../FormControlMixin.js';
20
20
  * @typedef {import('../../types/validate/ValidateMixinTypes.js').ValidateMixin} ValidateMixin
21
21
  * @typedef {import('../../types/validate/ValidateMixinTypes.js').ValidationType} ValidationType
22
22
  * @typedef {import('../../types/validate/ValidateMixinTypes.js').ValidateHost} ValidateHost
23
+ * @typedef {import('../../types/validate/ValidateMixinTypes.js').OperationMode} OperationMode
23
24
  * @typedef {import('../../types/validate/index.js').ValidatorOutcome} ValidatorOutcome
24
25
  * @typedef {typeof import('../../types/validate/ValidateMixinTypes.js').ValidateHost} ValidateHostConstructor
25
26
  * @typedef {{validator:Validator; outcome:boolean|string}} ValidationResultEntry
@@ -98,6 +99,15 @@ export const ValidateMixinImplementation = superclass =>
98
99
  return ['error'];
99
100
  }
100
101
 
102
+ /**
103
+ * Types of input interaction of the FormControl (for instance 'enter'|'select'|'upload')
104
+ * @overridable
105
+ * @type {OperationMode}
106
+ */
107
+ get operationMode() {
108
+ return 'enter';
109
+ }
110
+
101
111
  /**
102
112
  * Adds "._feedbackNode" as described below
103
113
  * @public
@@ -27,12 +27,15 @@ export class Validator extends EventTarget {
27
27
  this.__param = param;
28
28
  /** @type {ValidatorConfig} */
29
29
  this.__config = config || {};
30
- /** @type {ValidationType} */
31
- this.type = config?.type || 'error'; // Default type supported by ValidateMixin
30
+ /**
31
+ * Default type supported by ValidateMixin
32
+ * @type {ValidationType}
33
+ */
34
+ this.type = config?.type || 'error';
32
35
  }
33
36
 
34
37
  /**
35
- * The name under which validation results get registered. For convience and predictability, this
38
+ * The name under which validation results get registered. For convenience and predictability, this
36
39
  * should always be the same as the constructor name (since it will be obfuscated in js builds,
37
40
  * we need to provide it separately).
38
41
  * @type {ValidatorName}
@@ -1,11 +1,12 @@
1
- import { LitElement } from 'lit';
2
1
  import { uuid } from '@lion/ui/core.js';
3
- import { defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing';
4
2
  import {
5
3
  FormRegisteringMixin,
6
4
  FormRegistrarMixin,
7
5
  FormRegistrarPortalMixin,
8
6
  } from '@lion/ui/form-core.js';
7
+ import { defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing';
8
+ import { LitElement } from 'lit';
9
+ import sinon from 'sinon';
9
10
 
10
11
  /**
11
12
  * @typedef {Object} customConfig
@@ -163,20 +164,20 @@ export const runRegistrationSuite = customConfig => {
163
164
  });
164
165
 
165
166
  it('works for components that have a delayed render', async () => {
166
- class PerformUpdate extends FormRegistrarMixin(LitElement) {
167
- async performUpdate() {
167
+ class ScheduleUpdate extends FormRegistrarMixin(LitElement) {
168
+ async scheduleUpdate() {
168
169
  // eslint-disable-next-line no-promise-executor-return
169
170
  await new Promise(resolve => setTimeout(() => resolve(undefined), 10));
170
- await super.performUpdate();
171
+ await super.scheduleUpdate();
171
172
  }
172
173
 
173
174
  render() {
174
175
  return html`<slot></slot>`;
175
176
  }
176
177
  }
177
- const tagWrapperString = defineCE(PerformUpdate);
178
+ const tagWrapperString = defineCE(ScheduleUpdate);
178
179
  const tagWrapper = unsafeStatic(tagWrapperString);
179
- const el = /** @type {PerformUpdate} */ (
180
+ const el = /** @type {ScheduleUpdate} */ (
180
181
  await fixture(html`
181
182
  <${tagWrapper}>
182
183
  <${childTag}></${childTag}>
@@ -256,6 +257,60 @@ export const runRegistrationSuite = customConfig => {
256
257
  ]);
257
258
  });
258
259
 
260
+ describe('FormRegisteringMixin', () => {
261
+ it('propagates the form-element-register event through the shadowDom', async () => {
262
+ const eventSpy = sinon.spy();
263
+ const withShadowFormControlStr = defineCE(
264
+ class extends FormRegistrarMixin(LitElement) {
265
+ render() {
266
+ return html`
267
+ <${childTag}
268
+ id="child"
269
+ @form-element-register=${eventSpy}
270
+ allow-cross-root-registration
271
+ >
272
+ </${childTag}>`;
273
+ }
274
+ },
275
+ );
276
+ const withShadowFormControlTag = unsafeStatic(withShadowFormControlStr);
277
+
278
+ const el = /** @type {RegistrarClass} */ (
279
+ await fixture(html`
280
+ <${withShadowFormControlTag}>
281
+ </${withShadowFormControlTag}>
282
+ `)
283
+ );
284
+
285
+ expect(eventSpy).to.have.been.calledOnce;
286
+ expect(eventSpy.getCall(0).args[0].composed).to.equal(true);
287
+ expect(el.formElements).to.deep.equal([el.shadowRoot?.querySelector('#child')]);
288
+ });
289
+ it('dispatches the form-element-register event with compose true if allowCrossRootRegistration is set', async () => {
290
+ const eventSpy = sinon.spy();
291
+ /** @type {RegisteringClass} */ (
292
+ await fixture(html`
293
+ <${childTag}
294
+ @form-element-register=${eventSpy}
295
+ allow-cross-root-registration
296
+ >
297
+ </${childTag}>
298
+ `)
299
+ );
300
+ expect(eventSpy).to.have.been.calledOnce;
301
+ expect(eventSpy.getCall(0).args[0].composed).to.equal(true);
302
+ });
303
+ it('dispatches the form-element-register event with compose false if allowCrossRootRegistration is not set', async () => {
304
+ const eventSpy = sinon.spy();
305
+ /** @type {RegisteringClass} */ (
306
+ await fixture(html`
307
+ <${childTag} @form-element-register=${eventSpy}></${childTag}>
308
+ `)
309
+ );
310
+ expect(eventSpy).to.have.been.calledOnce;
311
+ expect(eventSpy.getCall(0).args[0].composed).to.equal(false);
312
+ });
313
+ });
259
314
  describe('FormRegistrarPortalMixin', () => {
260
315
  it('forwards registrations to the .registrationTarget', async () => {
261
316
  const el = /** @type {RegistrarClass} */ (
@@ -359,10 +414,10 @@ export const runRegistrationSuite = customConfig => {
359
414
  it('works for portals that have a delayed render', async () => {
360
415
  const delayedPortalString = defineCE(
361
416
  class extends FormRegistrarPortalMixin(LitElement) {
362
- async performUpdate() {
417
+ async scheduleUpdate() {
363
418
  // eslint-disable-next-line no-promise-executor-return
364
419
  await new Promise(resolve => setTimeout(() => resolve(undefined), 10));
365
- await super.performUpdate();
420
+ await super.scheduleUpdate();
366
421
  }
367
422
 
368
423
  render() {
@@ -115,6 +115,8 @@ export declare class FormControlHost {
115
115
  set fieldName(arg: string);
116
116
  get fieldName(): string;
117
117
 
118
+ get operationMode(): string;
119
+
118
120
  /**
119
121
  * Allows to add extra element references to aria-labelledby attribute.
120
122
  */
@@ -1,5 +1,5 @@
1
- import { LitElement } from 'lit';
2
1
  import { Constructor } from '@open-wc/dedupe-mixin';
2
+ import { LitElement } from 'lit';
3
3
 
4
4
  import { FormRegistrarHost } from './FormRegistrarMixinTypes.js';
5
5
 
@@ -8,6 +8,14 @@ export declare class FormRegisteringHost {
8
8
  * The name the host is registered with to a parent
9
9
  */
10
10
  name: string;
11
+ /**
12
+ * To encourage accessibility best practices, `form-element-register` events
13
+ * do not pierce through shadow roots. This forces the developer to create form groups and fieldsets that
14
+ * automatically allow the creation of accessible relationships in the same dom tree.
15
+ * Use this option if you know what you're doing. It will then be possible to nest FormControls
16
+ * inside shadow dom. See https://lion-web.netlify.app/fundamentals/rationales/accessibility#shadow-roots-and-accessibility
17
+ */
18
+ allowCrossRootRegistration: boolean;
11
19
 
12
20
  /**
13
21
  * The registrar this FormControl registers to, Usually a descendant of FormGroup or
@@ -16,6 +16,8 @@ export type FeedbackMessage = {
16
16
 
17
17
  export type ValidationType = 'error' | 'warning' | 'info' | 'success' | string;
18
18
 
19
+ export type OperationMode = 'enter' | 'select' | 'upload';
20
+
19
21
  export declare class ValidateHost {
20
22
  /**
21
23
  * Used by Application Developers to add Validators to a FormControl.
@@ -2,7 +2,7 @@ import { FormControlHost } from '../../src/FormControlMixin.js';
2
2
  import { ValidationType } from './ValidateMixinTypes.js';
3
3
 
4
4
  /**
5
- * The name under which validation results get registered. For convience and predictability, this
5
+ * The name under which validation results get registered. For convenience and predictability, this
6
6
  * should always be the same as the constructor name (since it will be obfuscated in js builds,
7
7
  * we need to provide it separately).
8
8
  * @example
@@ -34,7 +34,7 @@ export type ValidatorConfig = {
34
34
  };
35
35
 
36
36
  /**
37
- * Output of the `execute` function that returns a validity outcome. When we need to shpw feedback,
37
+ * Output of the `execute` function that returns a validity outcome. When we need to show feedback,
38
38
  * it should return true, otherwise false. So when an error\info|warning|success message
39
39
  * needs to be shown, return true.
40
40
  * It's also possible to return an enum. Let's say that a phone number can have multiple
@@ -84,6 +84,7 @@ export class LionInputFile extends ScopedElementsMixin(LocalizeMixin(LionField))
84
84
  .multiple=${this.multiple}
85
85
  ></lion-selected-file-list>
86
86
  `,
87
+ renderAsDirectHostChild: true,
87
88
  }),
88
89
  };
89
90
  }
@@ -183,7 +184,7 @@ export class LionInputFile extends ScopedElementsMixin(LocalizeMixin(LionField))
183
184
  */
184
185
  get _fileListNode() {
185
186
  return /** @type {LionSelectedFileList} */ (
186
- Array.from(this.children).find(child => child.slot === 'selected-file-list')?.children[0]
187
+ Array.from(this.children).find(child => child.slot === 'selected-file-list')
187
188
  );
188
189
  }
189
190
 
@@ -215,6 +216,11 @@ export class LionInputFile extends ScopedElementsMixin(LocalizeMixin(LionField))
215
216
  }
216
217
  }
217
218
 
219
+ // eslint-disable-next-line class-methods-use-this
220
+ get operationMode() {
221
+ return 'upload';
222
+ }
223
+
218
224
  get _acceptCriteria() {
219
225
  /** @type {string[]} */
220
226
  let allowedFileTypes = [];
@@ -433,7 +439,8 @@ export class LionInputFile extends ScopedElementsMixin(LocalizeMixin(LionField))
433
439
  ];
434
440
  }
435
441
  });
436
- // this._selectedFilesMetaData = [...this._selectedFilesMetaData];
442
+ // TODO: add a unit test for this
443
+ this._selectedFilesMetaData = [...this._selectedFilesMetaData];
437
444
  }
438
445
  });
439
446
  }