diff --git a/.changeset/happy-jars-joke.md b/.changeset/happy-jars-joke.md new file mode 100644 index 0000000000..69a65c4f89 --- /dev/null +++ b/.changeset/happy-jars-joke.md @@ -0,0 +1,5 @@ +--- +'@lion/ui': patch +--- + +Added support for cross-root registration by adding a flag to composed property of form-element-register event. diff --git a/packages/ui/components/form-core/src/registration/FormRegisteringMixin.js b/packages/ui/components/form-core/src/registration/FormRegisteringMixin.js index 15c14a37c1..c8fb4a31f8 100644 --- a/packages/ui/components/form-core/src/registration/FormRegisteringMixin.js +++ b/packages/ui/components/form-core/src/registration/FormRegisteringMixin.js @@ -28,6 +28,19 @@ const FormRegisteringMixinImplementation = superclass => * @type {FormRegistrarHost | undefined} */ this._parentFormGroup = undefined; + /** + * To encourage accessibility best practices, `form-element-register` events + * 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. + Use this option if you know what you're doing. It will then be possible to nest FormControls + inside shadow dom. See https://lion-web.netlify.app/fundamentals/rationales/accessibility/#shadow-roots-and-accessibility + */ + this.allowCrossRootRegistration = false; + } + + static get properties() { + return { + allowCrossRootRegistration: { type: Boolean, attribute: 'allow-cross-root-registration' }, + }; } connectedCallback() { @@ -36,6 +49,7 @@ const FormRegisteringMixinImplementation = superclass => new CustomEvent('form-element-register', { detail: { element: this }, bubbles: true, + composed: Boolean(this.allowCrossRootRegistration), }), ); } diff --git a/packages/ui/components/form-core/test-suites/FormRegistrationMixins.suite.js b/packages/ui/components/form-core/test-suites/FormRegistrationMixins.suite.js index f873983914..71864f515e 100644 --- a/packages/ui/components/form-core/test-suites/FormRegistrationMixins.suite.js +++ b/packages/ui/components/form-core/test-suites/FormRegistrationMixins.suite.js @@ -1,11 +1,12 @@ -import { LitElement } from 'lit'; import { uuid } from '@lion/ui/core.js'; -import { defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing'; import { FormRegisteringMixin, FormRegistrarMixin, FormRegistrarPortalMixin, } from '@lion/ui/form-core.js'; +import { defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing'; +import { LitElement } from 'lit'; +import sinon from 'sinon'; /** * @typedef {Object} customConfig @@ -256,6 +257,60 @@ export const runRegistrationSuite = customConfig => { ]); }); + describe('FormRegisteringMixin', () => { + it('propagates the form-element-register event through the shadowDom', async () => { + const eventSpy = sinon.spy(); + const withShadowFormControlStr = defineCE( + class extends FormRegistrarMixin(LitElement) { + render() { + return html` + <${childTag} + id="child" + @form-element-register=${eventSpy} + allow-cross-root-registration + > + `; + } + }, + ); + const withShadowFormControlTag = unsafeStatic(withShadowFormControlStr); + + const el = /** @type {RegistrarClass} */ ( + await fixture(html` + <${withShadowFormControlTag}> + + `) + ); + + expect(eventSpy).to.have.been.calledOnce; + expect(eventSpy.getCall(0).args[0].composed).to.equal(true); + expect(el.formElements).to.deep.equal([el.shadowRoot?.querySelector('#child')]); + }); + it('dispatches the form-element-register event with compose true if allowCrossRootRegistration is set', async () => { + const eventSpy = sinon.spy(); + /** @type {RegisteringClass} */ ( + await fixture(html` + <${childTag} + @form-element-register=${eventSpy} + allow-cross-root-registration + > + + `) + ); + expect(eventSpy).to.have.been.calledOnce; + expect(eventSpy.getCall(0).args[0].composed).to.equal(true); + }); + it('dispatches the form-element-register event with compose false if allowCrossRootRegistration is not set', async () => { + const eventSpy = sinon.spy(); + /** @type {RegisteringClass} */ ( + await fixture(html` + <${childTag} @form-element-register=${eventSpy}> + `) + ); + expect(eventSpy).to.have.been.calledOnce; + expect(eventSpy.getCall(0).args[0].composed).to.equal(false); + }); + }); describe('FormRegistrarPortalMixin', () => { it('forwards registrations to the .registrationTarget', async () => { const el = /** @type {RegistrarClass} */ ( diff --git a/packages/ui/components/form-core/types/registration/FormRegisteringMixinTypes.ts b/packages/ui/components/form-core/types/registration/FormRegisteringMixinTypes.ts index df896ddd58..2ec4ba313f 100644 --- a/packages/ui/components/form-core/types/registration/FormRegisteringMixinTypes.ts +++ b/packages/ui/components/form-core/types/registration/FormRegisteringMixinTypes.ts @@ -1,5 +1,5 @@ -import { LitElement } from 'lit'; import { Constructor } from '@open-wc/dedupe-mixin'; +import { LitElement } from 'lit'; import { FormRegistrarHost } from './FormRegistrarMixinTypes.js'; @@ -8,6 +8,14 @@ export declare class FormRegisteringHost { * The name the host is registered with to a parent */ name: string; + /** + * To encourage accessibility best practices, `form-element-register` events + * 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. + * Use this option if you know what you're doing. It will then be possible to nest FormControls + * inside shadow dom. See https://lion-web.netlify.app/fundamentals/rationales/accessibility#shadow-roots-and-accessibility + */ + allowCrossRootRegistration: boolean; /** * The registrar this FormControl registers to, Usually a descendant of FormGroup or