diff --git a/lib/jsdom/living/custom-elements/CustomElementRegistry-impl.js b/lib/jsdom/living/custom-elements/CustomElementRegistry-impl.js index 6f45d4229..6dc501065 100644 --- a/lib/jsdom/living/custom-elements/CustomElementRegistry-impl.js +++ b/lib/jsdom/living/custom-elements/CustomElementRegistry-impl.js @@ -126,6 +126,7 @@ class CustomElementRegistryImpl { this._elementDefinitionIsRunning = true; + let disableInternals = false; let disableShadow = false; let observedAttributes = []; const lifecycleCallbacks = { @@ -167,6 +168,7 @@ class CustomElementRegistryImpl { disabledFeatures = convertToSequenceDOMString(disabledFeaturesIterable); } + disableInternals = disabledFeatures.includes("internals"); disableShadow = disabledFeatures.includes("shadow"); } catch (err) { caughtError = err; @@ -186,6 +188,7 @@ class CustomElementRegistryImpl { observedAttributes, lifecycleCallbacks, disableShadow, + disableInternals, constructionStack: [] }; diff --git a/lib/jsdom/living/custom-elements/ElementInternals-impl.js b/lib/jsdom/living/custom-elements/ElementInternals-impl.js new file mode 100644 index 000000000..6769d7bc9 --- /dev/null +++ b/lib/jsdom/living/custom-elements/ElementInternals-impl.js @@ -0,0 +1,21 @@ +"use strict"; + +class ElementInternalsImpl { + constructor(globalObject, args, { targetElement }) { + this._targetElement = targetElement; + } + + get shadowRoot() { + const shadow = this._targetElement._shadowRoot; + + if (!shadow || !shadow._availableToElementInternals) { + return null; + } + + return shadow; + } +} + +module.exports = { + implementation: ElementInternalsImpl +}; diff --git a/lib/jsdom/living/custom-elements/ElementInternals.webidl b/lib/jsdom/living/custom-elements/ElementInternals.webidl new file mode 100644 index 000000000..ac75c50eb --- /dev/null +++ b/lib/jsdom/living/custom-elements/ElementInternals.webidl @@ -0,0 +1,43 @@ +// https://html.spec.whatwg.org/#the-elementinternals-interface + +[Exposed=Window] +interface ElementInternals { + // Shadow root access + readonly attribute ShadowRoot? shadowRoot; + + // Form-associated custom elements + // undefined setFormValue((File or USVString or FormData)? value, + // optional (File or USVString or FormData)? state); + + // readonly attribute HTMLFormElement? form; + + // undefined setValidity(optional ValidityStateFlags flags = {}, + // optional DOMString message, + // optional HTMLElement anchor); + // readonly attribute boolean willValidate; + // readonly attribute ValidityState validity; + // readonly attribute DOMString validationMessage; + // boolean checkValidity(); + // boolean reportValidity(); + + // readonly attribute NodeList labels; + + // Custom state pseudo-class + // [SameObject] readonly attribute CustomStateSet states; +}; + +// Accessibility semantics +// ElementInternals includes ARIAMixin; + +// dictionary ValidityStateFlags { +// boolean valueMissing = false; +// boolean typeMismatch = false; +// boolean patternMismatch = false; +// boolean tooLong = false; +// boolean tooShort = false; +// boolean rangeUnderflow = false; +// boolean rangeOverflow = false; +// boolean stepMismatch = false; +// boolean badInput = false; +// boolean customError = false; +// }; diff --git a/lib/jsdom/living/helpers/custom-elements.js b/lib/jsdom/living/helpers/custom-elements.js index 90ac7dd82..ab31de3a4 100644 --- a/lib/jsdom/living/helpers/custom-elements.js +++ b/lib/jsdom/living/helpers/custom-elements.js @@ -105,6 +105,8 @@ function upgradeElement(definition, element) { ]); } + element._ceState = "precustomized"; + const constructionResult = C.construct(); const constructionResultImpl = implForWrapper(constructionResult); diff --git a/lib/jsdom/living/interfaces.js b/lib/jsdom/living/interfaces.js index da56db6de..67c95f74c 100644 --- a/lib/jsdom/living/interfaces.js +++ b/lib/jsdom/living/interfaces.js @@ -183,6 +183,7 @@ const generatedInterfaces = { Storage: require("./generated/Storage"), CustomElementRegistry: require("./generated/CustomElementRegistry"), + ElementInternals: require("./generated/ElementInternals"), ShadowRoot: require("./generated/ShadowRoot"), MutationObserver: require("./generated/MutationObserver"), diff --git a/lib/jsdom/living/nodes/Element-impl.js b/lib/jsdom/living/nodes/Element-impl.js index 1148bd831..14f3a5d83 100644 --- a/lib/jsdom/living/nodes/Element-impl.js +++ b/lib/jsdom/living/nodes/Element-impl.js @@ -423,6 +423,10 @@ class ElementImpl extends NodeImpl { host: this }); + if (this._ceState === "precustomized" || this._ceState === "custom") { + shadow._availableToElementInternals = true; + } + this._shadowRoot = shadow; return shadow; diff --git a/lib/jsdom/living/nodes/HTMLElement-impl.js b/lib/jsdom/living/nodes/HTMLElement-impl.js index ce25b8857..3a0a5a0e7 100644 --- a/lib/jsdom/living/nodes/HTMLElement-impl.js +++ b/lib/jsdom/living/nodes/HTMLElement-impl.js @@ -1,7 +1,9 @@ "use strict"; const { mixin } = require("../../utils"); const ElementImpl = require("./Element-impl").implementation; +const DOMException = require("../generated/DOMException"); const MouseEvent = require("../generated/MouseEvent"); +const ElementInternals = require("../generated/ElementInternals"); const ElementCSSInlineStyleImpl = require("./ElementCSSInlineStyle-impl").implementation; const GlobalEventHandlersImpl = require("./GlobalEventHandlers-impl").implementation; const HTMLOrSVGElementImpl = require("./HTMLOrSVGElement-impl").implementation; @@ -9,6 +11,7 @@ const { firstChildWithLocalName } = require("../helpers/traversal"); const { isDisabled } = require("../helpers/form-controls"); const { fireAnEvent } = require("../helpers/events"); const { asciiLowercase } = require("../helpers/strings"); +const { lookupCEDefinition } = require("../helpers/custom-elements"); class HTMLElementImpl extends ElementImpl { constructor(globalObject, args, privateData) { @@ -21,6 +24,9 @@ class HTMLElementImpl extends ElementImpl { // uses HTMLElement and has activation behavior this._hasActivationBehavior = this._localName === "summary"; + + // https://html.spec.whatwg.org/#attached-internals + this._attachedInternals = null; } _activationBehavior() { @@ -117,6 +123,50 @@ class HTMLElementImpl extends ElementImpl { this.setAttributeNS(null, "dir", value); } + // https://html.spec.whatwg.org/#dom-attachinternals + attachInternals() { + if (this._isValue !== null) { + throw DOMException.create(this._globalObject, [ + "Unable to attach ElementInternals to a customized built-in element.", + "NotSupportedError" + ]); + } + + const definition = lookupCEDefinition(this._ownerDocument, this._namespaceURI, this._localName, null); + + if (definition === null) { + throw DOMException.create(this._globalObject, [ + "Unable to attach ElementInternals to non-custom elements.", + "NotSupportedError" + ]); + } + + if (definition.disableInternals === true) { + throw DOMException.create(this._globalObject, [ + "ElementInternals is disabled by disabledFeature static field.", + "NotSupportedError" + ]); + } + + if (this._attachedInternals !== null) { + throw DOMException.create(this._globalObject, [ + "ElementInternals for the specified element was already attached.", + "NotSupportedError" + ]); + } + + if (this._ceState !== "precustomized" && this._ceState !== "custom") { + throw DOMException.create(this._globalObject, [ + "The attachInternals() function cannot be called prior to the execution of the custom element constructor.", + "NotSupportedError" + ]); + } + + this._attachedInternals = ElementInternals.createImpl(this._globalObject, [], { targetElement: this }); + + return this._attachedInternals; + } + // Keep in sync with SVGElement. https://github.com/jsdom/jsdom/issues/2599 _attrModified(name, value, oldValue) { if (name === "style" && value !== oldValue && !this._settingCssText) { diff --git a/lib/jsdom/living/nodes/HTMLElement.webidl b/lib/jsdom/living/nodes/HTMLElement.webidl index 0f347b0b4..4474ab53d 100644 --- a/lib/jsdom/living/nodes/HTMLElement.webidl +++ b/lib/jsdom/living/nodes/HTMLElement.webidl @@ -19,7 +19,7 @@ interface HTMLElement : Element { // [CEReactions] attribute [LegacyNullToEmptyString] DOMString innerText; -// ElementInternals attachInternals(); + ElementInternals attachInternals(); }; HTMLElement includes GlobalEventHandlers; diff --git a/lib/jsdom/living/nodes/ShadowRoot-impl.js b/lib/jsdom/living/nodes/ShadowRoot-impl.js index 820deb220..336203ec4 100644 --- a/lib/jsdom/living/nodes/ShadowRoot-impl.js +++ b/lib/jsdom/living/nodes/ShadowRoot-impl.js @@ -13,6 +13,7 @@ class ShadowRootImpl extends DocumentFragment { const { mode } = privateData; this._mode = mode; + this._availableToElementInternals = false; } _getTheParent(event) { diff --git a/test/web-platform-tests/to-run.yaml b/test/web-platform-tests/to-run.yaml index 583c5753f..97577d3a5 100644 --- a/test/web-platform-tests/to-run.yaml +++ b/test/web-platform-tests/to-run.yaml @@ -164,7 +164,6 @@ Document-createElement.html: "document.createElement must report a NotSupportedError when the local name of the element does not match that of the custom element": [fail, throws TypeError instead] "document.createElement must report an exception thrown by a custom built-in element constructor": [fail, Unknown] ElementInternals-accessibility.html: [fail, attachInternals is not implemented] -HTMLElement-attachInternals.html: [fail, Not implemented] HTMLElement-constructor.html: "HTMLElement constructor must throw a TypeError when NewTarget is equal to itself": [fail, Unknown] "HTMLElement constructor must throw a TypeError when NewTarget is equal to itself via a Proxy object": [fail, webidl2js doesn't deal well with tests using Proxies to verify properties access] @@ -175,7 +174,6 @@ cross-realm-callback-report-exception.html: [fail, No relevant realm support for custom-element-reaction-queue.html: "Upgrading a custom element must invoke attributeChangedCallback and connectedCallback before start upgrading another element": [fail, document.write() implementation is not spec compliant] "Mutating a undefined custom element while upgrading a custom element must not enqueue or invoke reactions on the mutated element": [fail, document.write() implementation is not spec compliant] -element-internals-shadowroot.html: [fail, Not implemented] form-associated/**: [fail-slow, Not implemented] htmlconstructor/newtarget-customized-builtins.html: [fail, unknown] htmlconstructor/newtarget.html: [fail, Currently impossible to get the active function associated realm]