From 31414f3a8e44d8fc8048680b03b25c37db5ca5d2 Mon Sep 17 00:00:00 2001 From: "roxanne.baker" Date: Mon, 29 Sep 2025 11:51:31 -0400 Subject: [PATCH 1/9] feat: add support for ElementInternals in synthetic shadow --- .../src/framework/base-lightning-element.ts | 21 ++- packages/@lwc/engine-core/src/framework/vm.ts | 8 - .../syntheticShadowDom.html | 1 + .../syntheticShadowDom/syntheticShadowDom.js | 19 +++ .../api/index.spec.js | 9 +- .../formAssociated/index.spec.js | 144 +++++++++++++----- .../x/formAssociated/formAssociated.html | 3 + .../formAssociated/x/input/input.html | 3 + .../formAssociated/x/input/input.js | 3 + .../sanity/ei/component/component.js | 9 ++ .../elementInternals/sanity/index.spec.js | 74 ++++----- .../component/face-callbacks/index.spec.js | 13 +- 12 files changed, 207 insertions(+), 100 deletions(-) create mode 100644 packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/api/ai/syntheticShadowDom/syntheticShadowDom.html create mode 100644 packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/api/ai/syntheticShadowDom/syntheticShadowDom.js create mode 100644 packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/x/formAssociated/formAssociated.html create mode 100644 packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/x/input/input.html create mode 100644 packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/x/input/input.js diff --git a/packages/@lwc/engine-core/src/framework/base-lightning-element.ts b/packages/@lwc/engine-core/src/framework/base-lightning-element.ts index de65dfd610..ea448bd01d 100644 --- a/packages/@lwc/engine-core/src/framework/base-lightning-element.ts +++ b/packages/@lwc/engine-core/src/framework/base-lightning-element.ts @@ -506,11 +506,26 @@ function warnIfInvokedDuringConstruction(vm: VM, methodOrPropName: string) { ); } + const internals = attachInternals(elm); if (vm.shadowMode === ShadowMode.Synthetic) { - throw new Error('attachInternals API is not supported in synthetic shadow.'); + const handler = { + get(target: ElementInternals, prop: keyof ElementInternals) { + if (prop === 'shadowRoot') { + return vm.shadowRoot; + } + const value = Reflect.get(target, prop); + if (typeof value === 'function') { + return value.bind(target); + } + return value; + }, + set(target: ElementInternals, prop: keyof ElementInternals, value: any) { + return Reflect.set(target, prop, value); + }, + }; + return new Proxy(internals, handler); } - - return attachInternals(elm); + return internals; }, get isConnected(): boolean { diff --git a/packages/@lwc/engine-core/src/framework/vm.ts b/packages/@lwc/engine-core/src/framework/vm.ts index e5c977d9b6..cb8653140d 100644 --- a/packages/@lwc/engine-core/src/framework/vm.ts +++ b/packages/@lwc/engine-core/src/framework/vm.ts @@ -972,14 +972,6 @@ export function forceRehydration(vm: VM) { } export function runFormAssociatedCustomElementCallback(vm: VM, faceCb: () => void, args?: any[]) { - const { renderMode, shadowMode } = vm; - - if (shadowMode === ShadowMode.Synthetic && renderMode !== RenderMode.Light) { - throw new Error( - 'Form associated lifecycle methods are not available in synthetic shadow. Please use native shadow or light DOM.' - ); - } - invokeComponentCallback(vm, faceCb, args); } diff --git a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/api/ai/syntheticShadowDom/syntheticShadowDom.html b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/api/ai/syntheticShadowDom/syntheticShadowDom.html new file mode 100644 index 0000000000..41a40c8d47 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/api/ai/syntheticShadowDom/syntheticShadowDom.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/api/ai/syntheticShadowDom/syntheticShadowDom.js b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/api/ai/syntheticShadowDom/syntheticShadowDom.js new file mode 100644 index 0000000000..d8b072be78 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/api/ai/syntheticShadowDom/syntheticShadowDom.js @@ -0,0 +1,19 @@ +import { LightningElement, api } from 'lwc'; + +export default class extends LightningElement { + internals; + + connectedCallback() { + this.internals = this.attachInternals(); + } + + @api + callAttachInternals() { + this.internals = this.attachInternals(); + } + + @api + hasElementInternalsBeenSet() { + return Boolean(this.internals); + } +} diff --git a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/api/index.spec.js b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/api/index.spec.js index 22c813fd13..4ce782fa75 100644 --- a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/api/index.spec.js +++ b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/api/index.spec.js @@ -1,6 +1,7 @@ import { createElement } from 'lwc'; import ShadowDomCmp from 'ai/shadowDom'; +import SyntheticShadowDomCmp from 'ai/syntheticShadowDom'; import LightDomCmp from 'ai/lightDom'; import BasicCmp from 'ai/basic'; import { @@ -67,13 +68,7 @@ describe.runIf(ENABLE_ELEMENT_INTERNALS_AND_FACE)('ElementInternals', () => { }); describe.skipIf(process.env.NATIVE_SHADOW)('synthetic shadow', () => { - it('should throw error when used inside a component', () => { - const elm = createElement('synthetic-shadow', { is: ShadowDomCmp }); - testConnectedCallbackError( - elm, - 'attachInternals API is not supported in synthetic shadow.' - ); - }); + attachInternalsSanityTest('synthetic-shadow', SyntheticShadowDomCmp); }); describe('light DOM', () => { diff --git a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js index 1d68671a7e..d187ef8ed8 100644 --- a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js +++ b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js @@ -6,43 +6,117 @@ import FormAssociatedFalse from 'x/formAssociatedFalse'; import NotFormAssociatedNoAttachInternals from 'x/notFormAssociatedNoAttachInternals'; import FormAssociatedNoAttachInternals from 'x/formAssociatedNoAttachInternals'; import FormAssociatedFalseNoAttachInternals from 'x/formAssociatedFalseNoAttachInternals'; -import { - ENABLE_ELEMENT_INTERNALS_AND_FACE, - IS_SYNTHETIC_SHADOW_LOADED, -} from '../../../../../helpers/constants.js'; - -describe.runIf( - ENABLE_ELEMENT_INTERNALS_AND_FACE && - typeof ElementInternals !== 'undefined' && - !IS_SYNTHETIC_SHADOW_LOADED -)('should throw an error when duplicate tag name used', () => { - it('with different formAssociated value', () => { - // Register tag with formAssociated = true - createElement('x-form-associated', { is: FormAssociated }); - // Try to register again with formAssociated = false - expect(() => createElement('x-form-associated', { is: FormAssociatedFalse })).toThrowError( - / was already registered with formAssociated=true. It cannot be re-registered with formAssociated=false. Please rename your component to have a different name than / - ); - }); +import { ENABLE_ELEMENT_INTERNALS_AND_FACE } from '../../../../../helpers/constants.js'; - it('should not throw when duplicate tag name used with the same formAssociated value', () => { - // formAssociated = true - createElement('x-form-associated', { is: FormAssociated }); - expect(() => createElement('x-form-associated', { is: FormAssociated })).not.toThrow(); - // formAssociated = false - createElement('x-form-associated-false', { is: FormAssociatedFalse }); - expect(() => - createElement('x-form-associated-false', { is: FormAssociatedFalse }) - ).not.toThrow(); - // formAssociated = undefined - createElement('x-not-form-associated', { is: NotFormAssociated }); - expect(() => - createElement('x-not-form-associated', { is: NotFormAssociated }) - ).not.toThrow(); - }); -}); +describe.runIf(ENABLE_ELEMENT_INTERNALS_AND_FACE && typeof ElementInternals !== 'undefined')( + 'should throw an error when duplicate tag name used', + () => { + it('with different formAssociated value', () => { + // Register tag with formAssociated = true + createElement('x-form-associated', { is: FormAssociated }); + // Try to register again with formAssociated = false + expect(() => + createElement('x-form-associated', { is: FormAssociatedFalse }) + ).toThrowError( + / was already registered with formAssociated=true. It cannot be re-registered with formAssociated=false. Please rename your component to have a different name than / + ); + }); + + it('should not throw when duplicate tag name used with the same formAssociated value', () => { + // formAssociated = true + createElement('x-form-associated', { is: FormAssociated }); + expect(() => createElement('x-form-associated', { is: FormAssociated })).not.toThrow(); + // formAssociated = false + createElement('x-form-associated-false', { is: FormAssociatedFalse }); + expect(() => + createElement('x-form-associated-false', { is: FormAssociatedFalse }) + ).not.toThrow(); + // formAssociated = undefined + createElement('x-not-form-associated', { is: NotFormAssociated }); + expect(() => + createElement('x-not-form-associated', { is: NotFormAssociated }) + ).not.toThrow(); + }); + + it('should throw an error when accessing form related properties on a non-form associated component', () => { + const form = document.createElement('form'); + document.body.appendChild(form); + + const testElements = { + 'x-form-associated-false': FormAssociatedFalse, + 'x-not-form-associated': NotFormAssociated, + }; + let elm; + Object.entries(testElements).forEach(([tagName, ctor]) => { + elm = createElement(`x-${tagName}`, { is: ctor }); + const { internals } = elm; + form.appendChild(elm); + expect(() => internals.form).toThrow(); + expect(() => internals.setFormValue('2019-03-15')).toThrow(); + expect(() => internals.willValidate).toThrow(); + expect(() => internals.validity).toThrow(); + expect(() => internals.checkValidity()).toThrow(); + expect(() => internals.reportValidity()).toThrow(); + expect(() => internals.setValidity('')).toThrow(); + expect(() => internals.validationMessage).toThrow(); + expect(() => internals.labels).toThrow(); + }); + document.body.removeChild(form); + }); + + it('should be able to use internals to validate form associated component', () => { + const elm = createElement('x-form-associated', { is: FormAssociated }); + const { internals } = elm; + expect(internals.willValidate).toBe(true); + expect(internals.validity.valid).toBe(true); + expect(internals.checkValidity()).toBe(true); + expect(internals.reportValidity()).toBe(true); + expect(internals.validationMessage).toBe(''); + + internals.setValidity({ rangeUnderflow: true }, 'pick future date'); + + expect(internals.validity.valid).toBe(false); + expect(internals.checkValidity()).toBe(false); + expect(internals.reportValidity()).toBe(false); + expect(internals.validationMessage).toBe('pick future date'); + }); + + it('should be able to use setFormValue on a form associated component', () => { + const form = document.createElement('form'); + document.body.appendChild(form); + + const elm = createElement('x-form-associated', { is: FormAssociated }); + const { internals } = elm; + form.appendChild(elm); + + expect(internals.form).toBe(form); + + elm.setAttribute('name', 'date'); + const inputElm = elm.shadowRoot + .querySelector('x-input') + .shadowRoot.querySelector('input'); + internals.setFormValue('2019-03-15', '3/15/2019', inputElm); + const formData = new FormData(form); + expect(formData.get('date')).toBe('2019-03-15'); + }); + + it('should be able to associate labels to a form associated component', () => { + const elm = createElement('x-form-associated', { is: FormAssociated }); + document.body.appendChild(elm); + const { internals } = elm; + + expect(internals.labels.length).toBe(0); + elm.id = 'test-id'; + const label = document.createElement('label'); + label.htmlFor = elm.id; + document.body.appendChild(label); + expect(internals.labels.length).toBe(1); + expect(internals.labels[0]).toBe(label); + }); + } +); -it.runIf(typeof ElementInternals !== 'undefined' && !IS_SYNTHETIC_SHADOW_LOADED)( +it.runIf(typeof ElementInternals === 'undefined')( 'disallows form association on older API versions', () => { const isFormAssociated = (elm) => { diff --git a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/x/formAssociated/formAssociated.html b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/x/formAssociated/formAssociated.html new file mode 100644 index 0000000000..b0cd8b1426 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/x/formAssociated/formAssociated.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/x/input/input.html b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/x/input/input.html new file mode 100644 index 0000000000..0b6f2445eb --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/x/input/input.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/x/input/input.js b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/x/input/input.js new file mode 100644 index 0000000000..ca8dce94e0 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/x/input/input.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement {} diff --git a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/sanity/ei/component/component.js b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/sanity/ei/component/component.js index f6509f7110..1c248e676d 100644 --- a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/sanity/ei/component/component.js +++ b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/sanity/ei/component/component.js @@ -19,4 +19,13 @@ export default class extends LightningElement { this.internals[prop] = value; } } + + @api + toggleChecked() { + if (!this.internals.states.has('--checked')) { + this.internals.states.add('--checked'); + } else { + this.internals.states.delete('--checked'); + } + } } diff --git a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/sanity/index.spec.js b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/sanity/index.spec.js index 8bf34045ab..4554943cae 100644 --- a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/sanity/index.spec.js +++ b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/sanity/index.spec.js @@ -9,44 +9,46 @@ beforeEach(() => { document.body.appendChild(elm); }); -afterEach(() => { - document.body.removeChild(elm); -}); - -describe.runIf( - ENABLE_ELEMENT_INTERNALS_AND_FACE && - process.env.NATIVE_SHADOW && - typeof ElementInternals !== 'undefined' -)('ElementInternals', () => { - it('should be associated to the correct element', () => { - // Ensure external and internal views of shadowRoot are the same - expect(elm.internals.shadowRoot).toBe(elm.template); - expect(elm.internals.shadowRoot).toBe(elm.shadowRoot); - }); - - describe('accessibility', () => { - it('should be able to set ARIAMixin properties on ElementInternals', () => { - elm.setAllAriaProps('foo'); - // Verify ElementInternals proxy setter and getter - for (const ariaProp of ariaProperties) { - expect(elm.internals[ariaProp]).toEqual('foo'); - } +describe.runIf(ENABLE_ELEMENT_INTERNALS_AND_FACE && typeof ElementInternals !== 'undefined')( + 'ElementInternals', + () => { + it('should be associated to the correct element', () => { + // Ensure external and internal views of shadowRoot are the same + expect(elm.internals.shadowRoot).toBe(elm.template); + expect(elm.internals.shadowRoot).toBe(elm.shadowRoot); }); - it('should not reflect to aria-* attributes', () => { - elm.setAllAriaProps('foo'); - for (const attr of ariaAttributes) { - expect(elm.getAttribute(attr)).not.toEqual('foo'); - } + it('should be able to toggle states', () => { + elm.toggleChecked(); + expect(elm.internals.states.has('--checked')).toBe(true); + elm.toggleChecked(); + expect(elm.internals.states.has('--checked')).toBe(false); }); - it('aria-* attributes do not reflect to internals', () => { - for (const attr of ariaAttributes) { - elm.setAttribute(attr, 'bar'); - } - for (const prop of ariaProperties) { - expect(elm.internals[prop]).toBeFalsy(); - } + describe('accessibility', () => { + it('should be able to set ARIAMixin properties on ElementInternals', () => { + elm.setAllAriaProps('foo'); + // Verify ElementInternals proxy setter and getter + for (const ariaProp of ariaProperties) { + expect(elm.internals[ariaProp]).toEqual('foo'); + } + }); + + it('should not reflect to aria-* attributes', () => { + elm.setAllAriaProps('foo'); + for (const attr of ariaAttributes) { + expect(elm.getAttribute(attr)).not.toEqual('foo'); + } + }); + + it('aria-* attributes do not reflect to internals', () => { + for (const attr of ariaAttributes) { + elm.setAttribute(attr, 'bar'); + } + for (const prop of ariaProperties) { + expect(elm.internals[prop]).toBeFalsy(); + } + }); }); - }); -}); + } +); diff --git a/packages/@lwc/integration-not-karma/test/component/face-callbacks/index.spec.js b/packages/@lwc/integration-not-karma/test/component/face-callbacks/index.spec.js index d7890187e5..1a12328524 100644 --- a/packages/@lwc/integration-not-karma/test/component/face-callbacks/index.spec.js +++ b/packages/@lwc/integration-not-karma/test/component/face-callbacks/index.spec.js @@ -153,17 +153,8 @@ describe.runIf(typeof ElementInternals !== 'undefined')('ElementInternals', () = notFormAssociatedSanityTest('native-shadow', NotFormAssociated); }); describe.skipIf(process.env.NATIVE_SHADOW)('synthetic shadow', () => { - createFaceTests('synthetic-shadow', FormAssociated, (createFace) => { - it('cannot be used and throws an error', () => { - const face = createFace(); - const form = createFormElement(); - expect(() => - form.appendChild(face) - ).toThrowCallbackReactionErrorEvenInSyntheticLifecycleMode( - 'Form associated lifecycle methods are not available in synthetic shadow. Please use native shadow or light DOM.' - ); - }); - }); + faceSanityTest('synthetic-shadow', FormAssociated); + notFormAssociatedSanityTest('synthetic-shadow', NotFormAssociated); }); describe('light DOM', () => { faceSanityTest('light-dom', LightDomFormAssociated); From 0b3148ae5d71e41cd6fa030c1c3ac4e7487f05da Mon Sep 17 00:00:00 2001 From: Roxanne Baker Date: Mon, 29 Sep 2025 16:36:36 -0400 Subject: [PATCH 2/9] Update packages/@lwc/engine-core/src/framework/base-lightning-element.ts Co-authored-by: Will Harney <62956339+wjhsf@users.noreply.github.com> --- .../@lwc/engine-core/src/framework/base-lightning-element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@lwc/engine-core/src/framework/base-lightning-element.ts b/packages/@lwc/engine-core/src/framework/base-lightning-element.ts index ea448bd01d..d541bf8e5b 100644 --- a/packages/@lwc/engine-core/src/framework/base-lightning-element.ts +++ b/packages/@lwc/engine-core/src/framework/base-lightning-element.ts @@ -508,7 +508,7 @@ function warnIfInvokedDuringConstruction(vm: VM, methodOrPropName: string) { const internals = attachInternals(elm); if (vm.shadowMode === ShadowMode.Synthetic) { - const handler = { + const handler: ProxyHandler = { get(target: ElementInternals, prop: keyof ElementInternals) { if (prop === 'shadowRoot') { return vm.shadowRoot; From b6d245297d9e08c144c45f136eac5db3077af611 Mon Sep 17 00:00:00 2001 From: "roxanne.baker" Date: Mon, 29 Sep 2025 23:08:40 -0400 Subject: [PATCH 3/9] chore: update in response to review comments --- .../integration-not-karma/helpers/setup.js | 1 + .../formAssociated/index.spec.js | 51 ++++++++++--------- .../sanity/ei/component/component.css | 3 ++ .../elementInternals/sanity/index.spec.js | 2 + 4 files changed, 33 insertions(+), 24 deletions(-) create mode 100644 packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/sanity/ei/component/component.css diff --git a/packages/@lwc/integration-not-karma/helpers/setup.js b/packages/@lwc/integration-not-karma/helpers/setup.js index 691332359c..e3651595b5 100644 --- a/packages/@lwc/integration-not-karma/helpers/setup.js +++ b/packages/@lwc/integration-not-karma/helpers/setup.js @@ -65,6 +65,7 @@ hijackGlobal('afterEach', (afterEach) => { // Ensure the DOM is in a clean state document.body.replaceChildren(); document.head.replaceChildren(); + window.__lwcResetGlobalStylesheets(); }); }); diff --git a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js index d187ef8ed8..5d534c1eae 100644 --- a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js +++ b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js @@ -8,6 +8,27 @@ import FormAssociatedNoAttachInternals from 'x/formAssociatedNoAttachInternals'; import FormAssociatedFalseNoAttachInternals from 'x/formAssociatedFalseNoAttachInternals'; import { ENABLE_ELEMENT_INTERNALS_AND_FACE } from '../../../../../helpers/constants.js'; +const formAssociatedFalsyTest = (tagName, ctor) => { + const form = document.createElement('form'); + document.body.appendChild(form); + + const elm = createElement(`x-${tagName}`, { is: ctor }); + form.appendChild(elm); + + const { internals } = elm; + expect(() => internals.form).toThrow(); + expect(() => internals.setFormValue('2019-03-15')).toThrow(); + expect(() => internals.willValidate).toThrow(); + expect(() => internals.validity).toThrow(); + expect(() => internals.checkValidity()).toThrow(); + expect(() => internals.reportValidity()).toThrow(); + expect(() => internals.setValidity('')).toThrow(); + expect(() => internals.validationMessage).toThrow(); + expect(() => internals.labels).toThrow(); + + document.body.removeChild(form); +}; + describe.runIf(ENABLE_ELEMENT_INTERNALS_AND_FACE && typeof ElementInternals !== 'undefined')( 'should throw an error when duplicate tag name used', () => { @@ -38,30 +59,12 @@ describe.runIf(ENABLE_ELEMENT_INTERNALS_AND_FACE && typeof ElementInternals !== ).not.toThrow(); }); - it('should throw an error when accessing form related properties on a non-form associated component', () => { - const form = document.createElement('form'); - document.body.appendChild(form); + it('should throw an error when accessing form related properties when formAssociated is false', () => { + formAssociatedFalsyTest('x-form-associated-false', FormAssociatedFalse); + }); - const testElements = { - 'x-form-associated-false': FormAssociatedFalse, - 'x-not-form-associated': NotFormAssociated, - }; - let elm; - Object.entries(testElements).forEach(([tagName, ctor]) => { - elm = createElement(`x-${tagName}`, { is: ctor }); - const { internals } = elm; - form.appendChild(elm); - expect(() => internals.form).toThrow(); - expect(() => internals.setFormValue('2019-03-15')).toThrow(); - expect(() => internals.willValidate).toThrow(); - expect(() => internals.validity).toThrow(); - expect(() => internals.checkValidity()).toThrow(); - expect(() => internals.reportValidity()).toThrow(); - expect(() => internals.setValidity('')).toThrow(); - expect(() => internals.validationMessage).toThrow(); - expect(() => internals.labels).toThrow(); - }); - document.body.removeChild(form); + it('should throw an error when accessing form related properties when formAssociated is undefined', () => { + formAssociatedFalsyTest('x-not-form-associated', NotFormAssociated); }); it('should be able to use internals to validate form associated component', () => { @@ -116,7 +119,7 @@ describe.runIf(ENABLE_ELEMENT_INTERNALS_AND_FACE && typeof ElementInternals !== } ); -it.runIf(typeof ElementInternals === 'undefined')( +it.runIf(typeof ElementInternals !== 'undefined')( 'disallows form association on older API versions', () => { const isFormAssociated = (elm) => { diff --git a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/sanity/ei/component/component.css b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/sanity/ei/component/component.css new file mode 100644 index 0000000000..50b3acdc76 --- /dev/null +++ b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/sanity/ei/component/component.css @@ -0,0 +1,3 @@ +:host(:state(--checked)) { + color: rgb(255, 0, 0); +} diff --git a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/sanity/index.spec.js b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/sanity/index.spec.js index 4554943cae..24b6ddf451 100644 --- a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/sanity/index.spec.js +++ b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/sanity/index.spec.js @@ -21,8 +21,10 @@ describe.runIf(ENABLE_ELEMENT_INTERNALS_AND_FACE && typeof ElementInternals !== it('should be able to toggle states', () => { elm.toggleChecked(); expect(elm.internals.states.has('--checked')).toBe(true); + expect(getComputedStyle(elm).color).toBe('rgb(255, 0, 0)'); elm.toggleChecked(); expect(elm.internals.states.has('--checked')).toBe(false); + expect(getComputedStyle(elm).color).toBe('rgb(0, 0, 0)'); }); describe('accessibility', () => { From 71eb0c53ff3bf01f4bbeae1fa493cc31e7f8bd27 Mon Sep 17 00:00:00 2001 From: "roxanne.baker" Date: Mon, 29 Sep 2025 23:40:17 -0400 Subject: [PATCH 4/9] test: add test for setting readonly props in ElementInternals --- .../formAssociated/index.spec.js | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js index 5d534c1eae..91cd792bf9 100644 --- a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js +++ b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js @@ -8,6 +8,16 @@ import FormAssociatedNoAttachInternals from 'x/formAssociatedNoAttachInternals'; import FormAssociatedFalseNoAttachInternals from 'x/formAssociatedFalseNoAttachInternals'; import { ENABLE_ELEMENT_INTERNALS_AND_FACE } from '../../../../../helpers/constants.js'; +const readOnlyProperties = [ + 'shadowRoot', + 'states', + 'form', + 'willValidate', + 'validity', + 'validationMessage', + 'labels', +]; + const formAssociatedFalsyTest = (tagName, ctor) => { const form = document.createElement('form'); document.body.appendChild(form); @@ -116,6 +126,34 @@ describe.runIf(ENABLE_ELEMENT_INTERNALS_AND_FACE && typeof ElementInternals !== expect(internals.labels.length).toBe(1); expect(internals.labels[0]).toBe(label); }); + + it('should throw error when trying to set readonly properties on form associated component', () => { + const elm = createElement('x-form-associated', { is: FormAssociated }); + document.body.appendChild(elm); + const { internals } = elm; + + const readOnlyProperties = [ + 'shadowRoot', + 'states', + 'form', + 'willValidate', + 'validity', + 'validationMessage', + 'labels', + ]; + readOnlyProperties.forEach((property) => { + expect(() => (internals[property] = 'test')).toThrow(); + }); + }); + + for (const prop of readOnlyProperties) { + it(`should throw error when trying to set ${prop} on form associated component`, () => { + const elm = createElement('x-form-associated', { is: FormAssociated }); + document.body.appendChild(elm); + const { internals } = elm; + expect(() => (internals[prop] = 'test')).toThrow(); + }); + } } ); From 32b2606db809297c1cd200d4a4081a4dbcc3c3b9 Mon Sep 17 00:00:00 2001 From: Roxanne Baker Date: Tue, 30 Sep 2025 13:12:51 -0400 Subject: [PATCH 5/9] Update packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js Co-authored-by: Will Harney <62956339+wjhsf@users.noreply.github.com> --- .../formAssociated/index.spec.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js index 91cd792bf9..8f87cb3c5e 100644 --- a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js +++ b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js @@ -127,25 +127,6 @@ describe.runIf(ENABLE_ELEMENT_INTERNALS_AND_FACE && typeof ElementInternals !== expect(internals.labels[0]).toBe(label); }); - it('should throw error when trying to set readonly properties on form associated component', () => { - const elm = createElement('x-form-associated', { is: FormAssociated }); - document.body.appendChild(elm); - const { internals } = elm; - - const readOnlyProperties = [ - 'shadowRoot', - 'states', - 'form', - 'willValidate', - 'validity', - 'validationMessage', - 'labels', - ]; - readOnlyProperties.forEach((property) => { - expect(() => (internals[property] = 'test')).toThrow(); - }); - }); - for (const prop of readOnlyProperties) { it(`should throw error when trying to set ${prop} on form associated component`, () => { const elm = createElement('x-form-associated', { is: FormAssociated }); From 318abed864eab3f022146e1a54cde7eb879a58af Mon Sep 17 00:00:00 2001 From: Roxanne Baker Date: Tue, 30 Sep 2025 13:13:58 -0400 Subject: [PATCH 6/9] Update packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js Co-authored-by: Will Harney <62956339+wjhsf@users.noreply.github.com> --- .../elementInternals/formAssociated/index.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js index 8f87cb3c5e..02c43e9872 100644 --- a/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js +++ b/packages/@lwc/integration-not-karma/test/component/LightningElement.attachInternals/elementInternals/formAssociated/index.spec.js @@ -128,7 +128,7 @@ describe.runIf(ENABLE_ELEMENT_INTERNALS_AND_FACE && typeof ElementInternals !== }); for (const prop of readOnlyProperties) { - it(`should throw error when trying to set ${prop} on form associated component`, () => { + it(`should throw error when trying to set readonly ${prop} on form associated component`, () => { const elm = createElement('x-form-associated', { is: FormAssociated }); document.body.appendChild(elm); const { internals } = elm; From 1a8b40b9250dcbb7444060e4a3328b3e08b1490c Mon Sep 17 00:00:00 2001 From: "roxanne.baker" Date: Tue, 30 Sep 2025 16:21:25 -0400 Subject: [PATCH 7/9] fix: fix test --- .../component/face-callbacks/index.spec.js | 31 +++---------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/packages/@lwc/integration-not-karma/test/component/face-callbacks/index.spec.js b/packages/@lwc/integration-not-karma/test/component/face-callbacks/index.spec.js index 1a12328524..162513cafc 100644 --- a/packages/@lwc/integration-not-karma/test/component/face-callbacks/index.spec.js +++ b/packages/@lwc/integration-not-karma/test/component/face-callbacks/index.spec.js @@ -180,33 +180,10 @@ describe.runIf(typeof ElementInternals !== 'undefined')('ElementInternals', () = ); describe.skipIf(scenario === 'lwc.createElement')(name, () => { - const lightOrNativeShadow = name === 'light DOM' || process.env.NATIVE_SHADOW; - - // Face throws error message when synthetic shadow is enabled - it.runIf(lightOrNativeShadow)( - `${name} calls face lifecycle methods when using CustomElementConstructor`, - () => { - // CustomElementConstructor is to be upgraded independently of LWC, it will always use native lifecycle - testFaceLifecycleMethodsCallable(createFace); - } - ); - - // synthetic shadow mode - it.skipIf(lightOrNativeShadow)( - `${name} cannot call face lifecycle methods when using CustomElementConstructor`, - () => { - // this is always a callback reaction error, even in "synthetic lifecycle" mode, - // because synthetic lifecycle mode only includes connected/disconnected callbacks, - // not the FACE callbacks - expect(() => { - const face = createFace(); - const form = createFormElement(); - form.appendChild(face); - }).toThrowCallbackReactionErrorEvenInSyntheticLifecycleMode( - 'Form associated lifecycle methods are not available in synthetic shadow. Please use native shadow or light DOM.' - ); - } - ); + it(`${name} calls face lifecycle methods when using CustomElementConstructor`, () => { + // CustomElementConstructor is to be upgraded independently of LWC, it will always use native lifecycle + testFaceLifecycleMethodsCallable(createFace); + }); }); }); }); From aafc70f81103f5c33da8b1a69313a75f7c3a369c Mon Sep 17 00:00:00 2001 From: "roxanne.baker" Date: Mon, 13 Oct 2025 01:35:28 -0400 Subject: [PATCH 8/9] feat: add enableSyntheticElementInternals flag --- .../actual.js | 3 ++ .../config.json | 3 ++ .../error.json | 0 .../expected.js | 12 ++++++++ .../babel-plugin-component/src/component.ts | 28 ++++++++++++++----- .../babel-plugin-component/src/constants.ts | 2 ++ .../@lwc/babel-plugin-component/src/types.ts | 1 + packages/@lwc/compiler/src/options.ts | 3 ++ .../src/framework/base-lightning-element.ts | 9 ++++-- .../engine-core/src/framework/component.ts | 5 ++++ packages/@lwc/rollup-plugin/src/index.ts | 4 +++ 11 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 packages/@lwc/babel-plugin-component/src/__tests__/fixtures/component/enable-synthetic-element-internals/actual.js create mode 100644 packages/@lwc/babel-plugin-component/src/__tests__/fixtures/component/enable-synthetic-element-internals/config.json create mode 100644 packages/@lwc/babel-plugin-component/src/__tests__/fixtures/component/enable-synthetic-element-internals/error.json create mode 100644 packages/@lwc/babel-plugin-component/src/__tests__/fixtures/component/enable-synthetic-element-internals/expected.js diff --git a/packages/@lwc/babel-plugin-component/src/__tests__/fixtures/component/enable-synthetic-element-internals/actual.js b/packages/@lwc/babel-plugin-component/src/__tests__/fixtures/component/enable-synthetic-element-internals/actual.js new file mode 100644 index 0000000000..d2297592e0 --- /dev/null +++ b/packages/@lwc/babel-plugin-component/src/__tests__/fixtures/component/enable-synthetic-element-internals/actual.js @@ -0,0 +1,3 @@ +import { LightningElement as Component } from "lwc"; + +export default class Test extends Component {} diff --git a/packages/@lwc/babel-plugin-component/src/__tests__/fixtures/component/enable-synthetic-element-internals/config.json b/packages/@lwc/babel-plugin-component/src/__tests__/fixtures/component/enable-synthetic-element-internals/config.json new file mode 100644 index 0000000000..08662910cc --- /dev/null +++ b/packages/@lwc/babel-plugin-component/src/__tests__/fixtures/component/enable-synthetic-element-internals/config.json @@ -0,0 +1,3 @@ +{ + "enableSyntheticElementInternals": true +} diff --git a/packages/@lwc/babel-plugin-component/src/__tests__/fixtures/component/enable-synthetic-element-internals/error.json b/packages/@lwc/babel-plugin-component/src/__tests__/fixtures/component/enable-synthetic-element-internals/error.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/babel-plugin-component/src/__tests__/fixtures/component/enable-synthetic-element-internals/expected.js b/packages/@lwc/babel-plugin-component/src/__tests__/fixtures/component/enable-synthetic-element-internals/expected.js new file mode 100644 index 0000000000..0c8ec2a131 --- /dev/null +++ b/packages/@lwc/babel-plugin-component/src/__tests__/fixtures/component/enable-synthetic-element-internals/expected.js @@ -0,0 +1,12 @@ +import _tmpl from "./test.html"; +import { LightningElement as Component, registerComponent as _registerComponent } from "lwc"; +class Test extends Component { + /*LWC compiler vX.X.X*/ +} +const __lwc_component_class_internal = _registerComponent(Test, { + tmpl: _tmpl, + sel: "lwc-test", + apiVersion: 9999999, + enableSyntheticElementInternals: true +}); +export default __lwc_component_class_internal; \ No newline at end of file diff --git a/packages/@lwc/babel-plugin-component/src/component.ts b/packages/@lwc/babel-plugin-component/src/component.ts index 5ce17a8da1..bd2c364c21 100644 --- a/packages/@lwc/babel-plugin-component/src/component.ts +++ b/packages/@lwc/babel-plugin-component/src/component.ts @@ -14,6 +14,7 @@ import { TEMPLATE_KEY, API_VERSION_KEY, COMPONENT_CLASS_ID, + SYNTHETIC_ELEMENT_INTERNALS_KEY, } from './constants'; import type { types, NodePath, Visitor } from '@babel/core'; import type { BabelAPI, BabelTypes, LwcBabelPluginPass } from './types'; @@ -81,15 +82,28 @@ export default function ({ types: t }: BabelAPI): Visitor { // sel: 'x-foo', // apiVersion: '58' // }) + const properties = [ + t.objectProperty(t.identifier(TEMPLATE_KEY), templateIdentifier), + t.objectProperty(t.identifier(COMPONENT_NAME_KEY), componentRegisteredName), + // It's important that, at this point, we have an APIVersion rather than just a number. + // The client needs to trust the server that it's providing an actual known API version + t.objectProperty(t.identifier(API_VERSION_KEY), t.numericLiteral(apiVersion)), + ]; + // Only include enableSyntheticElementInternals if explicitly defined + if (typeof state.opts.enableSyntheticElementInternals === 'boolean') { + const supportsSyntheticElementInternals = t.booleanLiteral( + state.opts.enableSyntheticElementInternals || false + ); + properties.push( + t.objectProperty( + t.identifier(SYNTHETIC_ELEMENT_INTERNALS_KEY), + supportsSyntheticElementInternals + ) + ); + } const registerComponentExpression = t.callExpression(registerComponentId, [ node as types.Expression, - t.objectExpression([ - t.objectProperty(t.identifier(TEMPLATE_KEY), templateIdentifier), - t.objectProperty(t.identifier(COMPONENT_NAME_KEY), componentRegisteredName), - // It's important that, at this point, we have an APIVersion rather than just a number. - // The client needs to trust the server that it's providing an actual known API version - t.objectProperty(t.identifier(API_VERSION_KEY), t.numericLiteral(apiVersion)), - ]), + t.objectExpression(properties), ]); // Example: diff --git a/packages/@lwc/babel-plugin-component/src/constants.ts b/packages/@lwc/babel-plugin-component/src/constants.ts index 0f4e07d703..3f243a9768 100644 --- a/packages/@lwc/babel-plugin-component/src/constants.ts +++ b/packages/@lwc/babel-plugin-component/src/constants.ts @@ -34,6 +34,7 @@ const TEMPLATE_KEY = 'tmpl'; const COMPONENT_NAME_KEY = 'sel'; const API_VERSION_KEY = 'apiVersion'; const COMPONENT_CLASS_ID = '__lwc_component_class_internal'; +const SYNTHETIC_ELEMENT_INTERNALS_KEY = 'enableSyntheticElementInternals'; export { DECORATOR_TYPES, @@ -46,4 +47,5 @@ export { COMPONENT_NAME_KEY, API_VERSION_KEY, COMPONENT_CLASS_ID, + SYNTHETIC_ELEMENT_INTERNALS_KEY, }; diff --git a/packages/@lwc/babel-plugin-component/src/types.ts b/packages/@lwc/babel-plugin-component/src/types.ts index 0c70254398..e0eb62f61b 100644 --- a/packages/@lwc/babel-plugin-component/src/types.ts +++ b/packages/@lwc/babel-plugin-component/src/types.ts @@ -21,6 +21,7 @@ export interface LwcBabelPluginOptions { name: string; instrumentation?: InstrumentationObject; apiVersion?: number; + enableSyntheticElementInternals?: boolean; } export interface LwcBabelPluginPass extends PluginPass { diff --git a/packages/@lwc/compiler/src/options.ts b/packages/@lwc/compiler/src/options.ts index c386477428..5ed34c15c4 100755 --- a/packages/@lwc/compiler/src/options.ts +++ b/packages/@lwc/compiler/src/options.ts @@ -108,6 +108,8 @@ export interface TransformOptions { experimentalDynamicDirective?: boolean; /** Flag to enable usage of dynamic component(lwc:is) directive in HTML template */ enableDynamicComponents?: boolean; + /** Flag to enable usage of ElementInternals in synthetic shadow DOM */ + enableSyntheticElementInternals?: boolean; // TODO [#3370]: remove experimental template expression flag /** Flag to enable use of (a subset of) JavaScript expressions in place of template bindings. Passed to `@lwc/template-compiler`. */ experimentalComplexExpressions?: boolean; @@ -153,6 +155,7 @@ type OptionalTransformKeys = | 'enableLwcOn' | 'enableLightningWebSecurityTransforms' | 'enableDynamicComponents' + | 'enableSyntheticElementInternals' | 'experimentalDynamicDirective' | 'experimentalDynamicComponent' | 'instrumentation'; diff --git a/packages/@lwc/engine-core/src/framework/base-lightning-element.ts b/packages/@lwc/engine-core/src/framework/base-lightning-element.ts index d541bf8e5b..f8b919c2bc 100644 --- a/packages/@lwc/engine-core/src/framework/base-lightning-element.ts +++ b/packages/@lwc/engine-core/src/framework/base-lightning-element.ts @@ -38,7 +38,11 @@ import { } from '../libs/reflection'; import { HTMLElementOriginalDescriptors } from './html-properties'; -import { getComponentAPIVersion, getWrappedComponentsListener } from './component'; +import { + getComponentAPIVersion, + getWrappedComponentsListener, + supportsSyntheticElementInternals, +} from './component'; import { isBeingConstructed, isInvokingRender, vmBeingConstructed } from './invoker'; import { associateVM, getAssociatedVM, RenderMode, ShadowMode } from './vm'; import { componentValueObserved } from './mutation-tracker'; @@ -493,6 +497,7 @@ function warnIfInvokedDuringConstruction(vm: VM, methodOrPropName: string) { attachInternals(): ElementInternals { const vm = getAssociatedVM(this); const { + def: { ctor }, elm, apiVersion, renderer: { attachInternals }, @@ -507,7 +512,7 @@ function warnIfInvokedDuringConstruction(vm: VM, methodOrPropName: string) { } const internals = attachInternals(elm); - if (vm.shadowMode === ShadowMode.Synthetic) { + if (supportsSyntheticElementInternals(ctor) && vm.shadowMode === ShadowMode.Synthetic) { const handler: ProxyHandler = { get(target: ElementInternals, prop: keyof ElementInternals) { if (prop === 'shadowRoot') { diff --git a/packages/@lwc/engine-core/src/framework/component.ts b/packages/@lwc/engine-core/src/framework/component.ts index c3fc10e7fc..1bf23eed2b 100644 --- a/packages/@lwc/engine-core/src/framework/component.ts +++ b/packages/@lwc/engine-core/src/framework/component.ts @@ -24,6 +24,7 @@ type ComponentConstructorMetadata = { tmpl: Template; sel: string; apiVersion: APIVersion; + enableSyntheticElementInternals?: boolean | undefined; }; const registeredComponentMap: Map = new Map(); @@ -76,6 +77,10 @@ export function getComponentAPIVersion(Ctor: LightningElementConstructor): APIVe return apiVersion; } +export function supportsSyntheticElementInternals(Ctor: LightningElementConstructor): boolean { + return registeredComponentMap.get(Ctor)?.enableSyntheticElementInternals || false; +} + export function getTemplateReactiveObserver(vm: VM): ReactiveObserver { const reactiveObserver = createReactiveObserver(() => { const { isDirty } = vm; diff --git a/packages/@lwc/rollup-plugin/src/index.ts b/packages/@lwc/rollup-plugin/src/index.ts index 7c8cd15781..565c7ee062 100644 --- a/packages/@lwc/rollup-plugin/src/index.ts +++ b/packages/@lwc/rollup-plugin/src/index.ts @@ -52,6 +52,8 @@ export interface RollupLwcOptions { /** The configuration to pass to `@lwc/template-compiler`. */ enableDynamicComponents?: boolean; /** The configuration to pass to `@lwc/compiler`. */ + enableSyntheticElementInternals?: boolean; + /** The configuration to pass to `@lwc/compiler`. */ enableLightningWebSecurityTransforms?: boolean; // TODO [#3370]: remove experimental template expression flag /** The configuration to pass to `@lwc/template-compiler`. */ @@ -181,6 +183,7 @@ export default function lwc(pluginOptions: RollupLwcOptions = {}): Plugin { experimentalDynamicComponent, experimentalDynamicDirective, enableDynamicComponents, + enableSyntheticElementInternals, enableLwcOn, enableLightningWebSecurityTransforms, // TODO [#3370]: remove experimental template expression flag @@ -366,6 +369,7 @@ export default function lwc(pluginOptions: RollupLwcOptions = {}): Plugin { experimentalDynamicComponent, experimentalDynamicDirective, enableDynamicComponents, + enableSyntheticElementInternals, enableLwcOn, enableLightningWebSecurityTransforms, // TODO [#3370]: remove experimental template expression flag From af6b605095467143e99a83a19074ef083f44d436 Mon Sep 17 00:00:00 2001 From: "roxanne.baker" Date: Tue, 14 Oct 2025 14:29:13 -0400 Subject: [PATCH 9/9] chore: post-review updates --- packages/@lwc/babel-plugin-component/src/component.ts | 4 ++-- .../@lwc/engine-core/src/framework/base-lightning-element.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@lwc/babel-plugin-component/src/component.ts b/packages/@lwc/babel-plugin-component/src/component.ts index bd2c364c21..8a7dcb8dab 100644 --- a/packages/@lwc/babel-plugin-component/src/component.ts +++ b/packages/@lwc/babel-plugin-component/src/component.ts @@ -89,8 +89,8 @@ export default function ({ types: t }: BabelAPI): Visitor { // The client needs to trust the server that it's providing an actual known API version t.objectProperty(t.identifier(API_VERSION_KEY), t.numericLiteral(apiVersion)), ]; - // Only include enableSyntheticElementInternals if explicitly defined - if (typeof state.opts.enableSyntheticElementInternals === 'boolean') { + // Only include enableSyntheticElementInternals if set to true + if (state.opts.enableSyntheticElementInternals === true) { const supportsSyntheticElementInternals = t.booleanLiteral( state.opts.enableSyntheticElementInternals || false ); diff --git a/packages/@lwc/engine-core/src/framework/base-lightning-element.ts b/packages/@lwc/engine-core/src/framework/base-lightning-element.ts index f8b919c2bc..db7bb6222b 100644 --- a/packages/@lwc/engine-core/src/framework/base-lightning-element.ts +++ b/packages/@lwc/engine-core/src/framework/base-lightning-element.ts @@ -512,7 +512,7 @@ function warnIfInvokedDuringConstruction(vm: VM, methodOrPropName: string) { } const internals = attachInternals(elm); - if (supportsSyntheticElementInternals(ctor) && vm.shadowMode === ShadowMode.Synthetic) { + if (vm.shadowMode === ShadowMode.Synthetic && supportsSyntheticElementInternals(ctor)) { const handler: ProxyHandler = { get(target: ElementInternals, prop: keyof ElementInternals) { if (prop === 'shadowRoot') {