@lion/ui 0.6.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 (76) 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/registration/FormRegisteringMixin.js +14 -0
  5. package/components/form-core/test-suites/FormRegistrationMixins.suite.js +57 -2
  6. package/components/form-core/types/registration/FormRegisteringMixinTypes.ts +9 -1
  7. package/components/input-file/src/LionInputFile.js +2 -1
  8. package/components/input-tel-dropdown/src/LionInputTelDropdown.js +1 -0
  9. package/components/input-tel-dropdown/test-suites/LionInputTelDropdown.suite.js +1 -1
  10. package/custom-elements.json +1268 -198
  11. package/dist-types/components/core/src/SlotMixin.d.ts +5 -1
  12. package/dist-types/components/core/src/SlotMixin.d.ts.map +1 -1
  13. package/dist-types/components/core/types/SlotMixinTypes.d.ts +8 -0
  14. package/dist-types/components/core/types/SlotMixinTypes.d.ts.map +1 -1
  15. package/dist-types/components/form-core/src/registration/FormRegisteringMixin.d.ts.map +1 -1
  16. package/dist-types/components/form-core/test-suites/FormRegistrationMixins.suite.d.ts.map +1 -1
  17. package/dist-types/components/form-core/types/registration/FormRegisteringMixinTypes.d.ts +9 -1
  18. package/dist-types/components/form-core/types/registration/FormRegisteringMixinTypes.d.ts.map +1 -1
  19. package/dist-types/components/input-file/src/LionInputFile.d.ts +1 -0
  20. package/dist-types/components/input-file/src/LionInputFile.d.ts.map +1 -1
  21. package/dist-types/components/input-tel-dropdown/src/LionInputTelDropdown.d.ts +1 -0
  22. package/dist-types/components/input-tel-dropdown/src/LionInputTelDropdown.d.ts.map +1 -1
  23. package/dist-types/tsconfig.tsbuildinfo +1 -1
  24. package/docs/components/checkbox-group/overview.md +2 -2
  25. package/docs/components/collapsible/overview.md +1 -1
  26. package/docs/components/combobox/overview.md +1 -1
  27. package/docs/components/combobox/use-cases.md +1 -1
  28. package/docs/components/dialog/overview.md +1 -1
  29. package/docs/components/fieldset/overview.md +5 -5
  30. package/docs/components/form/overview.md +3 -3
  31. package/docs/components/icon/use-cases.md +1 -1
  32. package/docs/components/input/overview.md +1 -1
  33. package/docs/components/input-amount/overview.md +3 -3
  34. package/docs/components/input-date/overview.md +3 -3
  35. package/docs/components/input-datepicker/overview.md +5 -5
  36. package/docs/components/input-email/overview.md +2 -2
  37. package/docs/components/input-file/overview.md +1 -1
  38. package/docs/components/input-iban/overview.md +2 -2
  39. package/docs/components/input-range/overview.md +2 -2
  40. package/docs/components/input-stepper/overview.md +1 -1
  41. package/docs/components/input-tel/overview.md +1 -1
  42. package/docs/components/input-tel/use-cases.md +6 -6
  43. package/docs/components/input-tel-dropdown/overview.md +1 -1
  44. package/docs/components/listbox/use-cases.md +1 -1
  45. package/docs/components/radio-group/overview.md +3 -3
  46. package/docs/components/select/use-cases.md +1 -1
  47. package/docs/components/select-rich/use-cases.md +1 -1
  48. package/docs/components/tabs/examples.md +1 -1
  49. package/docs/fundamentals/index.md +4 -4
  50. package/docs/fundamentals/node-tools/providence-analytics/LocalConfiguration.md +1 -1
  51. package/docs/fundamentals/node-tools/providence-analytics/QueryResult.md +1 -1
  52. package/docs/fundamentals/node-tools/providence-analytics/analyzer.md +1 -1
  53. package/docs/fundamentals/node-tools/providence-analytics/dashboard.md +1 -1
  54. package/docs/fundamentals/node-tools/providence-analytics/overview.md +3 -3
  55. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  56. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  57. package/docs/fundamentals/rationales/accessibility.md +1 -1
  58. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  59. package/docs/fundamentals/systems/core/overview.md +1 -1
  60. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  61. package/docs/fundamentals/systems/form/model-value.md +1 -1
  62. package/docs/fundamentals/systems/form/overview.md +28 -28
  63. package/docs/fundamentals/systems/form/validate.md +2 -2
  64. package/docs/fundamentals/systems/localize/overview.md +4 -4
  65. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  66. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  67. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  68. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  69. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  70. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  71. package/docs/guides/how-to/create-a-form.md +4 -4
  72. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  73. package/docs/guides/index.md +8 -8
  74. package/docs/guides/principles/index.md +5 -5
  75. package/docs/guides/principles/styling.md +1 -1
  76. package/package.json +1 -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;
@@ -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
  }
@@ -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
@@ -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} */ (
@@ -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
@@ -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
 
@@ -180,6 +180,7 @@ export class LionInputTelDropdown extends LionInputTel {
180
180
 
181
181
  return {
182
182
  template: templates.dropdown(this._templateDataDropdown),
183
+ renderAsDirectHostChild: Boolean,
183
184
  };
184
185
  },
185
186
  };
@@ -215,7 +215,7 @@ export function runInputTelDropdownSuite({ klass } = { klass: LionInputTelDropdo
215
215
  it('renders to prefix slot in light dom', async () => {
216
216
  const el = await fixture(html` <${tag} .allowedRegions="${['DE']}"></${tag}> `);
217
217
  const prefixSlot = /** @type {HTMLElement} */ (
218
- /** @type {HTMLElement} */ (el.refs.dropdown.value).parentElement
218
+ /** @type {HTMLElement} */ (el.refs.dropdown.value)
219
219
  );
220
220
  expect(prefixSlot.getAttribute('slot')).to.equal('prefix');
221
221
  expect(prefixSlot.slot).to.equal('prefix');