Skip to content

Commit

Permalink
Implement basic ElementInternals
Browse files Browse the repository at this point in the history
It only supports the shadowRoot property for now.

This extracts the basic infrastructure from #3561, leaving the ARIAMixin work for later (to be reconciled with #3586).

Co-authored-by: Jufeng Zhang <zjffun@gmail.com>
  • Loading branch information
domenic and zjffun committed Jan 5, 2024
1 parent b7683ed commit 60978b6
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class CustomElementRegistryImpl {

this._elementDefinitionIsRunning = true;

let disableInternals = false;
let disableShadow = false;
let observedAttributes = [];
const lifecycleCallbacks = {
Expand Down Expand Up @@ -167,6 +168,7 @@ class CustomElementRegistryImpl {
disabledFeatures = convertToSequenceDOMString(disabledFeaturesIterable);
}

disableInternals = disabledFeatures.includes("internals");
disableShadow = disabledFeatures.includes("shadow");
} catch (err) {
caughtError = err;
Expand All @@ -186,6 +188,7 @@ class CustomElementRegistryImpl {
observedAttributes,
lifecycleCallbacks,
disableShadow,
disableInternals,
constructionStack: []
};

Expand Down
21 changes: 21 additions & 0 deletions lib/jsdom/living/custom-elements/ElementInternals-impl.js
Original file line number Diff line number Diff line change
@@ -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
};
43 changes: 43 additions & 0 deletions lib/jsdom/living/custom-elements/ElementInternals.webidl
Original file line number Diff line number Diff line change
@@ -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;
// };
2 changes: 2 additions & 0 deletions lib/jsdom/living/helpers/custom-elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ function upgradeElement(definition, element) {
]);
}

element._ceState = "precustomized";

const constructionResult = C.construct();
const constructionResultImpl = implForWrapper(constructionResult);

Expand Down
1 change: 1 addition & 0 deletions lib/jsdom/living/interfaces.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
4 changes: 4 additions & 0 deletions lib/jsdom/living/nodes/Element-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
50 changes: 50 additions & 0 deletions lib/jsdom/living/nodes/HTMLElement-impl.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
"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;
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) {
Expand All @@ -21,6 +24,9 @@ class HTMLElementImpl extends ElementImpl {

// <summary> uses HTMLElement and has activation behavior
this._hasActivationBehavior = this._localName === "summary";

// https://html.spec.whatwg.org/#attached-internals
this._attachedInternals = null;
}

_activationBehavior() {
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion lib/jsdom/living/nodes/HTMLElement.webidl
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ interface HTMLElement : Element {

// [CEReactions] attribute [LegacyNullToEmptyString] DOMString innerText;

// ElementInternals attachInternals();
ElementInternals attachInternals();
};

HTMLElement includes GlobalEventHandlers;
Expand Down
1 change: 1 addition & 0 deletions lib/jsdom/living/nodes/ShadowRoot-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class ShadowRootImpl extends DocumentFragment {

const { mode } = privateData;
this._mode = mode;
this._availableToElementInternals = false;
}

_getTheParent(event) {
Expand Down
2 changes: 0 additions & 2 deletions test/web-platform-tests/to-run.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand Down

0 comments on commit 60978b6

Please sign in to comment.