From 3f07daa20d3d1270569209340c13889af7593890 Mon Sep 17 00:00:00 2001 From: Steve Orvell Date: Mon, 6 May 2024 12:13:42 -0700 Subject: [PATCH 01/13] `formAssociated` set to first definition setting --- .../src/scoped-custom-element-registry.js | 35 ++++++++++++++++- .../test/form-associated.test.js | 38 +++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/packages/scoped-custom-element-registry/src/scoped-custom-element-registry.js b/packages/scoped-custom-element-registry/src/scoped-custom-element-registry.js index bbed0154..a1d37be4 100644 --- a/packages/scoped-custom-element-registry/src/scoped-custom-element-registry.js +++ b/packages/scoped-custom-element-registry/src/scoped-custom-element-registry.js @@ -11,12 +11,22 @@ * subject to an additional IP rights grant found at * http://polymer.github.io/PATENTS.txt */ -if (!ShadowRoot.prototype.createElement) { +if (!(ShadowRoot.prototype.createElement && ShadowRoot.prototype.importNode)) { const NativeHTMLElement = window.HTMLElement; const nativeDefine = window.customElements.define; const nativeGet = window.customElements.get; const nativeRegistry = window.customElements; + // Polyfill helper object + window['CustomElementRegistryPolyfill'] = { + // Note, `formAssociated` cannot be properly scoped and can only be set + // once per name. This is determined by how it is set on the first defined + // tag name. However, adding the name to + // `CustomElementsRegistryPolyfill.add(tagName)` reserves the given tag + // so it's always formAssociated. + 'formAssociated': new Set(), + }; + const definitionForElement = new WeakMap(); const pendingRegistryForElement = new WeakMap(); const globalDefinitionForConstructor = new WeakMap(); @@ -81,9 +91,28 @@ if (!ShadowRoot.prototype.createElement) { // Register a stand-in class which will handle the registry lookup & delegation let standInClass = nativeGet.call(nativeRegistry, tagName); if (!standInClass) { + // `formAssociated` cannot be scoped so it's set to true if + // the first defined element sets it or it's reserved in + // `CustomElementRegistryPolyfill.formAssociated`. + if (definition['formAssociated']) { + window['CustomElementRegistryPolyfill']['formAssociated'].add( + tagName + ); + } standInClass = createStandInElement(tagName); nativeDefine.call(nativeRegistry, tagName, standInClass); } + // Sync `formAssociated` to its effective setting: + if ( + window['CustomElementRegistryPolyfill']['formAssociated'].has(tagName) + ) { + definition['formAssociated'] = true; + try { + elementClass['formAssociated'] = true; + } catch (e) { + /** squelch */ + } + } if (this === window.customElements) { globalDefinitionForConstructor.set(elementClass, definition); definition.standInClass = standInClass; @@ -216,7 +245,9 @@ if (!ShadowRoot.prototype.createElement) { const createStandInElement = (tagName) => { return class ScopedCustomElementBase { static get ['formAssociated']() { - return true; + return window['CustomElementRegistryPolyfill']['formAssociated'].has( + tagName + ); } constructor() { // Create a raw HTMLElement first diff --git a/packages/scoped-custom-element-registry/test/form-associated.test.js b/packages/scoped-custom-element-registry/test/form-associated.test.js index a4b08a1a..fa3ad571 100644 --- a/packages/scoped-custom-element-registry/test/form-associated.test.js +++ b/packages/scoped-custom-element-registry/test/form-associated.test.js @@ -1,6 +1,7 @@ import {expect} from '@open-wc/testing'; import { + getTestTagName, getTestElement, getFormAssociatedTestElement, getFormAssociatedErrorTestElement, @@ -117,4 +118,41 @@ export const commonRegistryTests = (registry) => { expect(element.internals.checkValidity()).to.be.false; }); }); + + if (window.CustomElementRegistryPolyfill) { + describe('formAssociated scoping limitations', () => { + it('is formAssociated if set in CustomElementRegistryPolyfill.formAssociated', () => { + const tagName = getTestTagName(); + window.CustomElementRegistryPolyfill.formAssociated.add(tagName); + class El extends HTMLElement {} + customElements.define(tagName, El); + expect(customElements.get(tagName).formAssociated).to.be.true; + }); + it('is always formAssociated if first defined tag is formAssociated', () => { + const tagName = getTestTagName(); + class FormAssociatedEl extends HTMLElement { + static formAssociated = true; + } + class El extends HTMLElement {} + customElements.define(tagName, FormAssociatedEl); + const registry = new CustomElementRegistry(); + registry.define(tagName, El); + expect(customElements.get(tagName).formAssociated).to.be.true; + expect(registry.get(tagName).formAssociated).to.be.true; + }); + }); + } + + describe('When formAssociated is not set', () => { + it('should not prevent clicks when disabled', () => { + const {tagName, CustomElementClass} = getTestElement(); + customElements.define(tagName, CustomElementClass); + const el = document.createElement(tagName); + let clicked = false; + el.setAttribute('disabled', ''); + el.addEventListener('click', () => (clicked = true)); + el.click(); + expect(clicked).to.be.true; + }); + }); }; From d596db5698c86d3097dab1d2527ba44df7a432f7 Mon Sep 17 00:00:00 2001 From: Steve Orvell Date: Tue, 7 May 2024 05:18:07 -0700 Subject: [PATCH 02/13] Revert feature detection change. Will revisit in follow-on. --- .../src/scoped-custom-element-registry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/scoped-custom-element-registry/src/scoped-custom-element-registry.js b/packages/scoped-custom-element-registry/src/scoped-custom-element-registry.js index a1d37be4..37580e83 100644 --- a/packages/scoped-custom-element-registry/src/scoped-custom-element-registry.js +++ b/packages/scoped-custom-element-registry/src/scoped-custom-element-registry.js @@ -11,7 +11,7 @@ * subject to an additional IP rights grant found at * http://polymer.github.io/PATENTS.txt */ -if (!(ShadowRoot.prototype.createElement && ShadowRoot.prototype.importNode)) { +if (!ShadowRoot.prototype.createElement) { const NativeHTMLElement = window.HTMLElement; const nativeDefine = window.customElements.define; const nativeGet = window.customElements.get; From 59e2cfcb8d3dcc80c9e50e734c4957b0ddb82f5b Mon Sep 17 00:00:00 2001 From: Steve Orvell Date: Tue, 7 May 2024 09:46:10 -0700 Subject: [PATCH 03/13] Adds shadowRoot.createElementNS --- .../src/scoped-custom-element-registry.js | 1 + .../test/ShadowRoot.test.html.js | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/packages/scoped-custom-element-registry/src/scoped-custom-element-registry.js b/packages/scoped-custom-element-registry/src/scoped-custom-element-registry.js index 37580e83..c3210b5a 100644 --- a/packages/scoped-custom-element-registry/src/scoped-custom-element-registry.js +++ b/packages/scoped-custom-element-registry/src/scoped-custom-element-registry.js @@ -467,6 +467,7 @@ if (!ShadowRoot.prototype.createElement) { }; }; installScopedCreationMethod(ShadowRoot, 'createElement', document); + installScopedCreationMethod(ShadowRoot, 'createElementNS', document); installScopedCreationMethod(ShadowRoot, 'importNode', document); installScopedCreationMethod(Element, 'insertAdjacentHTML'); diff --git a/packages/scoped-custom-element-registry/test/ShadowRoot.test.html.js b/packages/scoped-custom-element-registry/test/ShadowRoot.test.html.js index ce36342b..20c33cfb 100644 --- a/packages/scoped-custom-element-registry/test/ShadowRoot.test.html.js +++ b/packages/scoped-custom-element-registry/test/ShadowRoot.test.html.js @@ -292,6 +292,34 @@ describe('ShadowRoot', () => { }); }); + describe('createElementNS', () => { + it('should create a regular element', () => { + const shadowRoot = getShadowRoot(); + + const $el = shadowRoot.createElementNS( + 'http://www.w3.org/1999/xhtml', + 'div' + ); + + expect($el).to.not.be.undefined; + expect($el).to.be.instanceof(HTMLDivElement); + }); + + it(`should upgrade an element defined in the global registry`, () => { + const {tagName, CustomElementClass} = getTestElement(); + customElements.define(tagName, CustomElementClass); + const shadowRoot = getShadowRoot(); + + const $el = shadowRoot.createElementNS( + 'http://www.w3.org/1999/xhtml', + tagName + ); + + expect($el).to.not.be.undefined; + expect($el).to.be.instanceof(CustomElementClass); + }); + }); + describe('innerHTML', () => { it(`shouldn't upgrade a defined custom element in a custom registry`, () => { const {tagName, CustomElementClass} = getTestElement(); From f87300a9eaa3de76a8b74b8bf9b4e4d551d7a9cc Mon Sep 17 00:00:00 2001 From: Steve Orvell Date: Wed, 8 May 2024 07:50:50 -0700 Subject: [PATCH 04/13] fix toggleAttribute and attributes for parser created elements --- .../src/scoped-custom-element-registry.js | 65 +++++++++++++++---- .../test/Element.test.html | 21 ++++++ .../test/Element.test.html.js | 30 ++++++++- .../test/utils.js | 4 ++ 4 files changed, 105 insertions(+), 15 deletions(-) diff --git a/packages/scoped-custom-element-registry/src/scoped-custom-element-registry.js b/packages/scoped-custom-element-registry/src/scoped-custom-element-registry.js index c3210b5a..c0815da5 100644 --- a/packages/scoped-custom-element-registry/src/scoped-custom-element-registry.js +++ b/packages/scoped-custom-element-registry/src/scoped-custom-element-registry.js @@ -273,6 +273,7 @@ if (!ShadowRoot.prototype.createElement) { connectedCallback() { const definition = definitionForElement.get(this); + ensureAttributesCustomized(this); if (definition) { // Delegate out to user callback definition.connectedCallback && @@ -354,6 +355,7 @@ if (!ShadowRoot.prototype.createElement) { const setAttribute = elementClass.prototype.setAttribute; if (setAttribute) { elementClass.prototype.setAttribute = function (n, value) { + ensureAttributesCustomized(this); const name = n.toLowerCase(); if (observedAttributes.has(name)) { const old = this.getAttribute(name); @@ -367,6 +369,7 @@ if (!ShadowRoot.prototype.createElement) { const removeAttribute = elementClass.prototype.removeAttribute; if (removeAttribute) { elementClass.prototype.removeAttribute = function (n) { + ensureAttributesCustomized(this); const name = n.toLowerCase(); if (observedAttributes.has(name)) { const old = this.getAttribute(name); @@ -380,12 +383,15 @@ if (!ShadowRoot.prototype.createElement) { const toggleAttribute = elementClass.prototype.toggleAttribute; if (toggleAttribute) { elementClass.prototype.toggleAttribute = function (n, force) { + ensureAttributesCustomized(this); const name = n.toLowerCase(); if (observedAttributes.has(name)) { const old = this.getAttribute(name); toggleAttribute.call(this, name, force); const newValue = this.getAttribute(name); - attributeChangedCallback.call(this, name, old, newValue); + if (old !== newValue) { + attributeChangedCallback.call(this, name, old, newValue); + } } else { toggleAttribute.call(this, name, force); } @@ -407,6 +413,44 @@ if (!ShadowRoot.prototype.createElement) { } }; + // Helper to defer initial attribute processing for parser generated + // custom elements. + let elementsPendingAttributes; + if (document.readyState === 'loading') { + elementsPendingAttributes = new Set(); + document.addEventListener( + 'readystatechange', + () => { + elementsPendingAttributes.forEach((instance) => + customizeAttributes(instance, definitionForElement.get(instance)) + ); + }, + {once: true} + ); + } + + const ensureAttributesCustomized = (instance) => { + if (!elementsPendingAttributes?.has(instance)) { + return; + } + customizeAttributes(instance, definitionForElement.get(instance)); + }; + + // Approximate observedAttributes from the user class, since the stand-in element had none + const customizeAttributes = (instance, definition) => { + elementsPendingAttributes?.delete(instance); + definition.observedAttributes.forEach((attr) => { + if (instance.hasAttribute(attr)) { + definition.attributeChangedCallback.call( + instance, + attr, + null, + instance.getAttribute(attr) + ); + } + }); + }; + // Helper to upgrade an instance with a CE definition using "constructor call trick" const customize = (instance, definition, isUpgrade = false) => { Object.setPrototypeOf(instance, definition.elementClass.prototype); @@ -419,17 +463,14 @@ if (!ShadowRoot.prototype.createElement) { new definition.elementClass(); } if (definition.attributeChangedCallback) { - // Approximate observedAttributes from the user class, since the stand-in element had none - definition.observedAttributes.forEach((attr) => { - if (instance.hasAttribute(attr)) { - definition.attributeChangedCallback.call( - instance, - attr, - null, - instance.getAttribute(attr) - ); - } - }); + if ( + elementsPendingAttributes !== undefined && + !instance.hasAttributes() + ) { + elementsPendingAttributes.add(instance); + } else { + customizeAttributes(instance, definition); + } } if (isUpgrade && definition.connectedCallback && instance.isConnected) { definition.connectedCallback.call(instance); diff --git a/packages/scoped-custom-element-registry/test/Element.test.html b/packages/scoped-custom-element-registry/test/Element.test.html index 36db1e8c..0d48c6a9 100644 --- a/packages/scoped-custom-element-registry/test/Element.test.html +++ b/packages/scoped-custom-element-registry/test/Element.test.html @@ -1,6 +1,27 @@ + + + +