From 6f26ecaf3ebbc74862f829372b5d9d0018b4a76b Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Tue, 28 Jul 2026 10:13:06 -0400 Subject: [PATCH 1/2] Add concrete HTML element constructors Assisted-By: devx/ca2f43ab-bbf9-4273-b1bc-0b06f8c90a57 --- .changeset/famous-elements-exist.md | 5 + packages/polyfill/source/Document.ts | 8 +- packages/polyfill/source/HTMLBodyElement.ts | 4 +- packages/polyfill/source/HTMLElements.ts | 198 ++++++++++++++++++ packages/polyfill/source/HTMLHeadElement.ts | 4 +- packages/polyfill/source/HTMLHtmlElement.ts | 4 +- .../polyfill/source/HTMLTemplateElement.ts | 4 +- packages/polyfill/source/Window.ts | 73 ++++++- .../source/tests/HTMLElements.test.ts | 159 ++++++++++++++ 9 files changed, 448 insertions(+), 11 deletions(-) create mode 100644 .changeset/famous-elements-exist.md create mode 100644 packages/polyfill/source/HTMLElements.ts create mode 100644 packages/polyfill/source/tests/HTMLElements.test.ts diff --git a/.changeset/famous-elements-exist.md b/.changeset/famous-elements-exist.md new file mode 100644 index 00000000..1dfa655a --- /dev/null +++ b/.changeset/famous-elements-exist.md @@ -0,0 +1,5 @@ +--- +'@remote-dom/polyfill': minor +--- + +Install concrete HTML element constructors and use them for elements created by the polyfilled document. diff --git a/packages/polyfill/source/Document.ts b/packages/polyfill/source/Document.ts index 688a8896..d67e8037 100644 --- a/packages/polyfill/source/Document.ts +++ b/packages/polyfill/source/Document.ts @@ -21,6 +21,8 @@ import {isParentNode, cloneNode} from './shared.ts'; import {HTMLBodyElement} from './HTMLBodyElement.ts'; import {HTMLHeadElement} from './HTMLHeadElement.ts'; import {HTMLHtmlElement} from './HTMLHtmlElement.ts'; +import {HTML_ELEMENT_CONSTRUCTORS, HTMLUnknownElement} from './HTMLElements.ts'; +import {HTMLElement} from './HTMLElement.ts'; export class Document extends ParentNode { nodeType = NodeType.DOCUMENT_NODE; @@ -108,7 +110,11 @@ export function createElement( element = new HTMLTemplateElement() as any; } else { const CustomElement = ownerDocument.defaultView.customElements.get(name); - element = CustomElement ? (new CustomElement() as any) : new Element(); + const ElementConstructor = + CustomElement ?? + HTML_ELEMENT_CONSTRUCTORS[lowerName] ?? + (lowerName.includes('-') ? HTMLElement : HTMLUnknownElement); + element = new ElementConstructor() as any; } return setupElement(element, ownerDocument, name, namespace); diff --git a/packages/polyfill/source/HTMLBodyElement.ts b/packages/polyfill/source/HTMLBodyElement.ts index 33eb34fb..1675206a 100644 --- a/packages/polyfill/source/HTMLBodyElement.ts +++ b/packages/polyfill/source/HTMLBodyElement.ts @@ -1,3 +1,3 @@ -import {Element} from './Element.ts'; +import {HTMLElement} from './HTMLElement.ts'; -export class HTMLBodyElement extends Element {} +export class HTMLBodyElement extends HTMLElement {} diff --git a/packages/polyfill/source/HTMLElements.ts b/packages/polyfill/source/HTMLElements.ts new file mode 100644 index 00000000..e9e7f336 --- /dev/null +++ b/packages/polyfill/source/HTMLElements.ts @@ -0,0 +1,198 @@ +import {HTMLElement} from './HTMLElement.ts'; +import {HTMLBodyElement} from './HTMLBodyElement.ts'; +import {HTMLHeadElement} from './HTMLHeadElement.ts'; +import {HTMLHtmlElement} from './HTMLHtmlElement.ts'; +import {HTMLTemplateElement} from './HTMLTemplateElement.ts'; + +export {HTMLBodyElement, HTMLHeadElement, HTMLHtmlElement, HTMLTemplateElement}; + +export class HTMLAnchorElement extends HTMLElement {} +export class HTMLAreaElement extends HTMLElement {} +export class HTMLBaseElement extends HTMLElement {} +export class HTMLBRElement extends HTMLElement {} +export class HTMLButtonElement extends HTMLElement {} +export class HTMLCanvasElement extends HTMLElement {} +export class HTMLDListElement extends HTMLElement {} +export class HTMLDataElement extends HTMLElement {} +export class HTMLDataListElement extends HTMLElement {} +export class HTMLDetailsElement extends HTMLElement {} +export class HTMLDialogElement extends HTMLElement {} +export class HTMLDirectoryElement extends HTMLElement {} +export class HTMLDivElement extends HTMLElement {} +export class HTMLEmbedElement extends HTMLElement {} +export class HTMLFieldSetElement extends HTMLElement {} +export class HTMLFontElement extends HTMLElement {} +export class HTMLFormElement extends HTMLElement {} +export class HTMLFrameElement extends HTMLElement {} +export class HTMLFrameSetElement extends HTMLElement {} +export class HTMLHRElement extends HTMLElement {} +export class HTMLHeadingElement extends HTMLElement {} +export class HTMLIFrameElement extends HTMLElement {} +export class HTMLImageElement extends HTMLElement {} +export class HTMLInputElement extends HTMLElement {} +export class HTMLLIElement extends HTMLElement {} +export class HTMLLabelElement extends HTMLElement {} +export class HTMLLegendElement extends HTMLElement {} +export class HTMLLinkElement extends HTMLElement {} +export class HTMLMapElement extends HTMLElement {} +export class HTMLMarqueeElement extends HTMLElement {} +export class HTMLMediaElement extends HTMLElement {} +export class HTMLAudioElement extends HTMLMediaElement {} +export class HTMLVideoElement extends HTMLMediaElement {} +export class HTMLMenuElement extends HTMLElement {} +export class HTMLMetaElement extends HTMLElement {} +export class HTMLMeterElement extends HTMLElement {} +export class HTMLModElement extends HTMLElement {} +export class HTMLOListElement extends HTMLElement {} +export class HTMLObjectElement extends HTMLElement {} +export class HTMLOptGroupElement extends HTMLElement {} +export class HTMLOptionElement extends HTMLElement {} +export class HTMLOutputElement extends HTMLElement {} +export class HTMLParagraphElement extends HTMLElement {} +export class HTMLParamElement extends HTMLElement {} +export class HTMLPictureElement extends HTMLElement {} +export class HTMLPreElement extends HTMLElement {} +export class HTMLProgressElement extends HTMLElement {} +export class HTMLQuoteElement extends HTMLElement {} +export class HTMLScriptElement extends HTMLElement {} +export class HTMLSelectElement extends HTMLElement {} +export class HTMLSlotElement extends HTMLElement {} +export class HTMLSourceElement extends HTMLElement {} +export class HTMLSpanElement extends HTMLElement {} +export class HTMLStyleElement extends HTMLElement {} +export class HTMLTableCaptionElement extends HTMLElement {} +export class HTMLTableCellElement extends HTMLElement {} +export class HTMLTableColElement extends HTMLElement {} +export class HTMLTableElement extends HTMLElement {} +export class HTMLTableRowElement extends HTMLElement {} +export class HTMLTableSectionElement extends HTMLElement {} +export class HTMLTextAreaElement extends HTMLElement {} +export class HTMLTimeElement extends HTMLElement {} +export class HTMLTitleElement extends HTMLElement {} +export class HTMLTrackElement extends HTMLElement {} +export class HTMLUListElement extends HTMLElement {} +export class HTMLUnknownElement extends HTMLElement {} + +type HTMLElementConstructor = new () => HTMLElement; + +export const HTML_ELEMENT_CONSTRUCTORS: Record = + { + a: HTMLAnchorElement, + abbr: HTMLElement, + address: HTMLElement, + area: HTMLAreaElement, + article: HTMLElement, + aside: HTMLElement, + audio: HTMLAudioElement, + b: HTMLElement, + base: HTMLBaseElement, + bdi: HTMLElement, + bdo: HTMLElement, + blockquote: HTMLQuoteElement, + body: HTMLBodyElement, + br: HTMLBRElement, + button: HTMLButtonElement, + canvas: HTMLCanvasElement, + caption: HTMLTableCaptionElement, + col: HTMLTableColElement, + colgroup: HTMLTableColElement, + cite: HTMLElement, + code: HTMLElement, + data: HTMLDataElement, + datalist: HTMLDataListElement, + dd: HTMLElement, + del: HTMLModElement, + details: HTMLDetailsElement, + dfn: HTMLElement, + dialog: HTMLDialogElement, + dir: HTMLDirectoryElement, + div: HTMLDivElement, + dl: HTMLDListElement, + dt: HTMLElement, + em: HTMLElement, + embed: HTMLEmbedElement, + fieldset: HTMLFieldSetElement, + figcaption: HTMLElement, + figure: HTMLElement, + font: HTMLFontElement, + footer: HTMLElement, + form: HTMLFormElement, + frame: HTMLFrameElement, + frameset: HTMLFrameSetElement, + h1: HTMLHeadingElement, + h2: HTMLHeadingElement, + h3: HTMLHeadingElement, + h4: HTMLHeadingElement, + h5: HTMLHeadingElement, + h6: HTMLHeadingElement, + head: HTMLHeadElement, + header: HTMLElement, + hgroup: HTMLElement, + hr: HTMLHRElement, + html: HTMLHtmlElement, + i: HTMLElement, + iframe: HTMLIFrameElement, + img: HTMLImageElement, + input: HTMLInputElement, + ins: HTMLModElement, + kbd: HTMLElement, + label: HTMLLabelElement, + legend: HTMLLegendElement, + li: HTMLLIElement, + link: HTMLLinkElement, + main: HTMLElement, + map: HTMLMapElement, + marquee: HTMLMarqueeElement, + mark: HTMLElement, + menu: HTMLMenuElement, + meta: HTMLMetaElement, + meter: HTMLMeterElement, + nav: HTMLElement, + noscript: HTMLElement, + object: HTMLObjectElement, + ol: HTMLOListElement, + optgroup: HTMLOptGroupElement, + option: HTMLOptionElement, + output: HTMLOutputElement, + p: HTMLParagraphElement, + param: HTMLParamElement, + picture: HTMLPictureElement, + pre: HTMLPreElement, + progress: HTMLProgressElement, + q: HTMLQuoteElement, + rp: HTMLElement, + rt: HTMLElement, + ruby: HTMLElement, + s: HTMLElement, + samp: HTMLElement, + script: HTMLScriptElement, + search: HTMLElement, + section: HTMLElement, + select: HTMLSelectElement, + slot: HTMLSlotElement, + small: HTMLElement, + source: HTMLSourceElement, + span: HTMLSpanElement, + strong: HTMLElement, + style: HTMLStyleElement, + sub: HTMLElement, + summary: HTMLElement, + sup: HTMLElement, + table: HTMLTableElement, + tbody: HTMLTableSectionElement, + td: HTMLTableCellElement, + template: HTMLTemplateElement, + textarea: HTMLTextAreaElement, + tfoot: HTMLTableSectionElement, + th: HTMLTableCellElement, + thead: HTMLTableSectionElement, + time: HTMLTimeElement, + title: HTMLTitleElement, + tr: HTMLTableRowElement, + track: HTMLTrackElement, + u: HTMLElement, + ul: HTMLUListElement, + var: HTMLElement, + video: HTMLVideoElement, + wbr: HTMLElement, + }; diff --git a/packages/polyfill/source/HTMLHeadElement.ts b/packages/polyfill/source/HTMLHeadElement.ts index c1117bc3..7cb989c6 100644 --- a/packages/polyfill/source/HTMLHeadElement.ts +++ b/packages/polyfill/source/HTMLHeadElement.ts @@ -1,3 +1,3 @@ -import {Element} from './Element.ts'; +import {HTMLElement} from './HTMLElement.ts'; -export class HTMLHeadElement extends Element {} +export class HTMLHeadElement extends HTMLElement {} diff --git a/packages/polyfill/source/HTMLHtmlElement.ts b/packages/polyfill/source/HTMLHtmlElement.ts index 570890d5..5ff28bca 100644 --- a/packages/polyfill/source/HTMLHtmlElement.ts +++ b/packages/polyfill/source/HTMLHtmlElement.ts @@ -1,3 +1,3 @@ -import {Element} from './Element.ts'; +import {HTMLElement} from './HTMLElement.ts'; -export class HTMLHtmlElement extends Element {} +export class HTMLHtmlElement extends HTMLElement {} diff --git a/packages/polyfill/source/HTMLTemplateElement.ts b/packages/polyfill/source/HTMLTemplateElement.ts index fb32e21d..372e769b 100644 --- a/packages/polyfill/source/HTMLTemplateElement.ts +++ b/packages/polyfill/source/HTMLTemplateElement.ts @@ -1,9 +1,9 @@ import {CONTENT} from './constants.ts'; import {DocumentFragment} from './DocumentFragment.ts'; -import {Element} from './Element.ts'; +import {HTMLElement} from './HTMLElement.ts'; import {parseHtml, serializeChildren} from './serialization.ts'; -export class HTMLTemplateElement extends Element { +export class HTMLTemplateElement extends HTMLElement { [CONTENT]?: DocumentFragment; get content() { diff --git a/packages/polyfill/source/Window.ts b/packages/polyfill/source/Window.ts index f61a1e94..b85d9e8a 100644 --- a/packages/polyfill/source/Window.ts +++ b/packages/polyfill/source/Window.ts @@ -17,9 +17,9 @@ import {CharacterData} from './CharacterData.ts'; import {Text} from './Text.ts'; import {Comment} from './Comment.ts'; import {DocumentFragment} from './DocumentFragment.ts'; -import {HTMLTemplateElement} from './HTMLTemplateElement.ts'; import {CustomElementRegistryImplementation} from './CustomElementRegistry.ts'; import {MutationObserver} from './MutationObserver.ts'; +import * as HTMLElements from './HTMLElements.ts'; import {HOOKS} from './constants.ts'; import type {Hooks} from './hooks.ts'; @@ -62,8 +62,77 @@ export class Window extends EventTarget { Text = Text; Element = Element; HTMLElement = HTMLElement; + HTMLAnchorElement = HTMLElements.HTMLAnchorElement; + HTMLAreaElement = HTMLElements.HTMLAreaElement; + HTMLAudioElement = HTMLElements.HTMLAudioElement; + HTMLBaseElement = HTMLElements.HTMLBaseElement; + HTMLBodyElement = HTMLElements.HTMLBodyElement; + HTMLBRElement = HTMLElements.HTMLBRElement; + HTMLButtonElement = HTMLElements.HTMLButtonElement; + HTMLCanvasElement = HTMLElements.HTMLCanvasElement; + HTMLDataElement = HTMLElements.HTMLDataElement; + HTMLDataListElement = HTMLElements.HTMLDataListElement; + HTMLDetailsElement = HTMLElements.HTMLDetailsElement; + HTMLDialogElement = HTMLElements.HTMLDialogElement; + HTMLDirectoryElement = HTMLElements.HTMLDirectoryElement; + HTMLDivElement = HTMLElements.HTMLDivElement; + HTMLDListElement = HTMLElements.HTMLDListElement; + HTMLEmbedElement = HTMLElements.HTMLEmbedElement; + HTMLFieldSetElement = HTMLElements.HTMLFieldSetElement; + HTMLFontElement = HTMLElements.HTMLFontElement; + HTMLFormElement = HTMLElements.HTMLFormElement; + HTMLFrameElement = HTMLElements.HTMLFrameElement; + HTMLFrameSetElement = HTMLElements.HTMLFrameSetElement; + HTMLHeadElement = HTMLElements.HTMLHeadElement; + HTMLHeadingElement = HTMLElements.HTMLHeadingElement; + HTMLHRElement = HTMLElements.HTMLHRElement; + HTMLHtmlElement = HTMLElements.HTMLHtmlElement; + HTMLIFrameElement = HTMLElements.HTMLIFrameElement; + HTMLImageElement = HTMLElements.HTMLImageElement; + HTMLInputElement = HTMLElements.HTMLInputElement; + HTMLLabelElement = HTMLElements.HTMLLabelElement; + HTMLLegendElement = HTMLElements.HTMLLegendElement; + HTMLLIElement = HTMLElements.HTMLLIElement; + HTMLLinkElement = HTMLElements.HTMLLinkElement; + HTMLMapElement = HTMLElements.HTMLMapElement; + HTMLMarqueeElement = HTMLElements.HTMLMarqueeElement; + HTMLMediaElement = HTMLElements.HTMLMediaElement; + HTMLMenuElement = HTMLElements.HTMLMenuElement; + HTMLMetaElement = HTMLElements.HTMLMetaElement; + HTMLMeterElement = HTMLElements.HTMLMeterElement; + HTMLModElement = HTMLElements.HTMLModElement; + HTMLObjectElement = HTMLElements.HTMLObjectElement; + HTMLOListElement = HTMLElements.HTMLOListElement; + HTMLOptGroupElement = HTMLElements.HTMLOptGroupElement; + HTMLOptionElement = HTMLElements.HTMLOptionElement; + HTMLOutputElement = HTMLElements.HTMLOutputElement; + HTMLParagraphElement = HTMLElements.HTMLParagraphElement; + HTMLParamElement = HTMLElements.HTMLParamElement; + HTMLPictureElement = HTMLElements.HTMLPictureElement; + HTMLPreElement = HTMLElements.HTMLPreElement; + HTMLProgressElement = HTMLElements.HTMLProgressElement; + HTMLQuoteElement = HTMLElements.HTMLQuoteElement; + HTMLScriptElement = HTMLElements.HTMLScriptElement; + HTMLSelectElement = HTMLElements.HTMLSelectElement; + HTMLSlotElement = HTMLElements.HTMLSlotElement; + HTMLSourceElement = HTMLElements.HTMLSourceElement; + HTMLSpanElement = HTMLElements.HTMLSpanElement; + HTMLStyleElement = HTMLElements.HTMLStyleElement; + HTMLTableCaptionElement = HTMLElements.HTMLTableCaptionElement; + HTMLTableCellElement = HTMLElements.HTMLTableCellElement; + HTMLTableColElement = HTMLElements.HTMLTableColElement; + HTMLTableElement = HTMLElements.HTMLTableElement; + HTMLTableRowElement = HTMLElements.HTMLTableRowElement; + HTMLTableSectionElement = HTMLElements.HTMLTableSectionElement; SVGElement = SVGElement; - HTMLTemplateElement = HTMLTemplateElement; + HTMLTemplateElement = HTMLElements.HTMLTemplateElement; + HTMLTextAreaElement = HTMLElements.HTMLTextAreaElement; + HTMLTimeElement = HTMLElements.HTMLTimeElement; + HTMLTitleElement = HTMLElements.HTMLTitleElement; + HTMLTrackElement = HTMLElements.HTMLTrackElement; + HTMLUListElement = HTMLElements.HTMLUListElement; + HTMLUnknownElement = HTMLElements.HTMLUnknownElement; + HTMLVideoElement = HTMLElements.HTMLVideoElement; MutationObserver = MutationObserver; #currentOnErrorHandler: ((event: any) => void) | null = null; diff --git a/packages/polyfill/source/tests/HTMLElements.test.ts b/packages/polyfill/source/tests/HTMLElements.test.ts new file mode 100644 index 00000000..224614a6 --- /dev/null +++ b/packages/polyfill/source/tests/HTMLElements.test.ts @@ -0,0 +1,159 @@ +import {beforeEach, describe, expect, it} from 'vitest'; + +import {Window} from '../Window.ts'; + +describe('HTML element constructors', () => { + let window: Window; + + beforeEach(() => { + window = new Window(); + }); + + it.each([ + ['a', 'HTMLAnchorElement'], + ['button', 'HTMLButtonElement'], + ['form', 'HTMLFormElement'], + ['img', 'HTMLImageElement'], + ['input', 'HTMLInputElement'], + ['select', 'HTMLSelectElement'], + ['table', 'HTMLTableElement'], + ['td', 'HTMLTableCellElement'], + ['textarea', 'HTMLTextAreaElement'], + ])('creates <%s> with %s identity', (tagName, constructorName) => { + const Constructor = (window as any)[constructorName]; + const element = window.document.createElement(tagName); + + expect(Constructor).toBeTypeOf('function'); + expect(element).toBeInstanceOf(Constructor); + expect(element).toBeInstanceOf(window.HTMLElement); + expect(element).toBeInstanceOf(window.Element); + }); + + it('keeps distinct element constructor identities', () => { + const input = window.document.createElement('input'); + + expect(input).toBeInstanceOf(window.HTMLInputElement); + expect(input).not.toBeInstanceOf(window.HTMLFormElement); + }); + + it('uses the shared media superclass for audio and video', () => { + expect(window.document.createElement('audio')).toBeInstanceOf( + window.HTMLMediaElement, + ); + expect(window.document.createElement('video')).toBeInstanceOf( + window.HTMLMediaElement, + ); + }); + + it('creates generic, unknown, and unregistered custom elements correctly', () => { + expect(window.document.createElement('article').constructor).toBe( + window.HTMLElement, + ); + expect(window.document.createElement('not-an-html-tag')).toBeInstanceOf( + window.HTMLElement, + ); + expect(window.document.createElement('unknown')).toBeInstanceOf( + window.HTMLUnknownElement, + ); + }); + + it('preserves constructors for built-in document elements and templates', () => { + expect(window.document.documentElement).toBeInstanceOf( + window.HTMLHtmlElement, + ); + expect(window.document.head).toBeInstanceOf(window.HTMLHeadElement); + expect(window.document.body).toBeInstanceOf(window.HTMLBodyElement); + expect(window.document.createElement('template')).toBeInstanceOf( + window.HTMLTemplateElement, + ); + }); + + it('continues to construct registered custom elements', () => { + class TestElement extends window.HTMLElement {} + + window.customElements.define('test-element', TestElement as any); + + expect(window.document.createElement('test-element')).toBeInstanceOf( + TestElement, + ); + }); + + it('installs every standard HTML element constructor as a global', () => { + const constructorNames = [ + 'HTMLAnchorElement', + 'HTMLAreaElement', + 'HTMLAudioElement', + 'HTMLBRElement', + 'HTMLBaseElement', + 'HTMLBodyElement', + 'HTMLButtonElement', + 'HTMLCanvasElement', + 'HTMLDListElement', + 'HTMLDataElement', + 'HTMLDataListElement', + 'HTMLDetailsElement', + 'HTMLDialogElement', + 'HTMLDirectoryElement', + 'HTMLDivElement', + 'HTMLEmbedElement', + 'HTMLFieldSetElement', + 'HTMLFontElement', + 'HTMLFormElement', + 'HTMLFrameElement', + 'HTMLFrameSetElement', + 'HTMLHRElement', + 'HTMLHeadElement', + 'HTMLHeadingElement', + 'HTMLHtmlElement', + 'HTMLIFrameElement', + 'HTMLImageElement', + 'HTMLInputElement', + 'HTMLLIElement', + 'HTMLLabelElement', + 'HTMLLegendElement', + 'HTMLLinkElement', + 'HTMLMapElement', + 'HTMLMarqueeElement', + 'HTMLMediaElement', + 'HTMLMenuElement', + 'HTMLMetaElement', + 'HTMLMeterElement', + 'HTMLModElement', + 'HTMLOListElement', + 'HTMLObjectElement', + 'HTMLOptGroupElement', + 'HTMLOptionElement', + 'HTMLOutputElement', + 'HTMLParagraphElement', + 'HTMLParamElement', + 'HTMLPictureElement', + 'HTMLPreElement', + 'HTMLProgressElement', + 'HTMLQuoteElement', + 'HTMLScriptElement', + 'HTMLSelectElement', + 'HTMLSlotElement', + 'HTMLSourceElement', + 'HTMLSpanElement', + 'HTMLStyleElement', + 'HTMLTableCaptionElement', + 'HTMLTableCellElement', + 'HTMLTableColElement', + 'HTMLTableElement', + 'HTMLTableRowElement', + 'HTMLTableSectionElement', + 'HTMLTemplateElement', + 'HTMLTextAreaElement', + 'HTMLTimeElement', + 'HTMLTitleElement', + 'HTMLTrackElement', + 'HTMLUListElement', + 'HTMLUnknownElement', + 'HTMLVideoElement', + ]; + + for (const name of constructorNames) { + expect((window as any)[name], name).toBeTypeOf('function'); + } + }); +}); From 599ac2543fb2c05d29215eff74e0e350f7fb423c Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Tue, 28 Jul 2026 17:49:29 -0400 Subject: [PATCH 2/2] Preserve the flat HTML element model Assisted-By: devx/ca2f43ab-bbf9-4273-b1bc-0b06f8c90a57 --- .changeset/famous-elements-exist.md | 2 +- packages/polyfill/source/Document.ts | 8 +- packages/polyfill/source/HTMLBodyElement.ts | 4 +- packages/polyfill/source/HTMLElement.ts | 17 +- packages/polyfill/source/HTMLElements.ts | 376 +++++++++--------- packages/polyfill/source/HTMLHeadElement.ts | 4 +- packages/polyfill/source/HTMLHtmlElement.ts | 4 +- .../polyfill/source/HTMLTemplateElement.ts | 4 +- packages/polyfill/source/Window.ts | 83 +--- .../source/tests/HTMLElements.test.ts | 60 ++- 10 files changed, 269 insertions(+), 293 deletions(-) diff --git a/.changeset/famous-elements-exist.md b/.changeset/famous-elements-exist.md index 1dfa655a..fb3098ca 100644 --- a/.changeset/famous-elements-exist.md +++ b/.changeset/famous-elements-exist.md @@ -2,4 +2,4 @@ '@remote-dom/polyfill': minor --- -Install concrete HTML element constructors and use them for elements created by the polyfilled document. +Install standard HTML element constructor globals with tag-aware `instanceof` checks while preserving the polyfill's flat element model. diff --git a/packages/polyfill/source/Document.ts b/packages/polyfill/source/Document.ts index d67e8037..688a8896 100644 --- a/packages/polyfill/source/Document.ts +++ b/packages/polyfill/source/Document.ts @@ -21,8 +21,6 @@ import {isParentNode, cloneNode} from './shared.ts'; import {HTMLBodyElement} from './HTMLBodyElement.ts'; import {HTMLHeadElement} from './HTMLHeadElement.ts'; import {HTMLHtmlElement} from './HTMLHtmlElement.ts'; -import {HTML_ELEMENT_CONSTRUCTORS, HTMLUnknownElement} from './HTMLElements.ts'; -import {HTMLElement} from './HTMLElement.ts'; export class Document extends ParentNode { nodeType = NodeType.DOCUMENT_NODE; @@ -110,11 +108,7 @@ export function createElement( element = new HTMLTemplateElement() as any; } else { const CustomElement = ownerDocument.defaultView.customElements.get(name); - const ElementConstructor = - CustomElement ?? - HTML_ELEMENT_CONSTRUCTORS[lowerName] ?? - (lowerName.includes('-') ? HTMLElement : HTMLUnknownElement); - element = new ElementConstructor() as any; + element = CustomElement ? (new CustomElement() as any) : new Element(); } return setupElement(element, ownerDocument, name, namespace); diff --git a/packages/polyfill/source/HTMLBodyElement.ts b/packages/polyfill/source/HTMLBodyElement.ts index 1675206a..33eb34fb 100644 --- a/packages/polyfill/source/HTMLBodyElement.ts +++ b/packages/polyfill/source/HTMLBodyElement.ts @@ -1,3 +1,3 @@ -import {HTMLElement} from './HTMLElement.ts'; +import {Element} from './Element.ts'; -export class HTMLBodyElement extends HTMLElement {} +export class HTMLBodyElement extends Element {} diff --git a/packages/polyfill/source/HTMLElement.ts b/packages/polyfill/source/HTMLElement.ts index 33753d89..2203aa0d 100644 --- a/packages/polyfill/source/HTMLElement.ts +++ b/packages/polyfill/source/HTMLElement.ts @@ -1,3 +1,18 @@ +import {NamespaceURI} from './constants.ts'; import {Element} from './Element.ts'; -export class HTMLElement extends Element {} +export class HTMLElement extends Element { + static [Symbol.hasInstance](value: unknown) { + // Custom element subclasses still use their real prototype chain. The base + // HTMLElement check also recognizes the polyfill's deliberately-flat HTML + // elements without requiring createElement() to construct a second tree of + // tag-specific classes. + if (this !== HTMLElement) { + return Function.prototype[Symbol.hasInstance].call(this, value); + } + + return ( + value instanceof Element && value.namespaceURI === NamespaceURI.XHTML + ); + } +} diff --git a/packages/polyfill/source/HTMLElements.ts b/packages/polyfill/source/HTMLElements.ts index e9e7f336..19dda79d 100644 --- a/packages/polyfill/source/HTMLElements.ts +++ b/packages/polyfill/source/HTMLElements.ts @@ -1,198 +1,186 @@ -import {HTMLElement} from './HTMLElement.ts'; -import {HTMLBodyElement} from './HTMLBodyElement.ts'; -import {HTMLHeadElement} from './HTMLHeadElement.ts'; -import {HTMLHtmlElement} from './HTMLHtmlElement.ts'; -import {HTMLTemplateElement} from './HTMLTemplateElement.ts'; +import {NamespaceURI} from './constants.ts'; +import {Element} from './Element.ts'; -export {HTMLBodyElement, HTMLHeadElement, HTMLHtmlElement, HTMLTemplateElement}; +type ElementConstructor = typeof Element; -export class HTMLAnchorElement extends HTMLElement {} -export class HTMLAreaElement extends HTMLElement {} -export class HTMLBaseElement extends HTMLElement {} -export class HTMLBRElement extends HTMLElement {} -export class HTMLButtonElement extends HTMLElement {} -export class HTMLCanvasElement extends HTMLElement {} -export class HTMLDListElement extends HTMLElement {} -export class HTMLDataElement extends HTMLElement {} -export class HTMLDataListElement extends HTMLElement {} -export class HTMLDetailsElement extends HTMLElement {} -export class HTMLDialogElement extends HTMLElement {} -export class HTMLDirectoryElement extends HTMLElement {} -export class HTMLDivElement extends HTMLElement {} -export class HTMLEmbedElement extends HTMLElement {} -export class HTMLFieldSetElement extends HTMLElement {} -export class HTMLFontElement extends HTMLElement {} -export class HTMLFormElement extends HTMLElement {} -export class HTMLFrameElement extends HTMLElement {} -export class HTMLFrameSetElement extends HTMLElement {} -export class HTMLHRElement extends HTMLElement {} -export class HTMLHeadingElement extends HTMLElement {} -export class HTMLIFrameElement extends HTMLElement {} -export class HTMLImageElement extends HTMLElement {} -export class HTMLInputElement extends HTMLElement {} -export class HTMLLIElement extends HTMLElement {} -export class HTMLLabelElement extends HTMLElement {} -export class HTMLLegendElement extends HTMLElement {} -export class HTMLLinkElement extends HTMLElement {} -export class HTMLMapElement extends HTMLElement {} -export class HTMLMarqueeElement extends HTMLElement {} -export class HTMLMediaElement extends HTMLElement {} -export class HTMLAudioElement extends HTMLMediaElement {} -export class HTMLVideoElement extends HTMLMediaElement {} -export class HTMLMenuElement extends HTMLElement {} -export class HTMLMetaElement extends HTMLElement {} -export class HTMLMeterElement extends HTMLElement {} -export class HTMLModElement extends HTMLElement {} -export class HTMLOListElement extends HTMLElement {} -export class HTMLObjectElement extends HTMLElement {} -export class HTMLOptGroupElement extends HTMLElement {} -export class HTMLOptionElement extends HTMLElement {} -export class HTMLOutputElement extends HTMLElement {} -export class HTMLParagraphElement extends HTMLElement {} -export class HTMLParamElement extends HTMLElement {} -export class HTMLPictureElement extends HTMLElement {} -export class HTMLPreElement extends HTMLElement {} -export class HTMLProgressElement extends HTMLElement {} -export class HTMLQuoteElement extends HTMLElement {} -export class HTMLScriptElement extends HTMLElement {} -export class HTMLSelectElement extends HTMLElement {} -export class HTMLSlotElement extends HTMLElement {} -export class HTMLSourceElement extends HTMLElement {} -export class HTMLSpanElement extends HTMLElement {} -export class HTMLStyleElement extends HTMLElement {} -export class HTMLTableCaptionElement extends HTMLElement {} -export class HTMLTableCellElement extends HTMLElement {} -export class HTMLTableColElement extends HTMLElement {} -export class HTMLTableElement extends HTMLElement {} -export class HTMLTableRowElement extends HTMLElement {} -export class HTMLTableSectionElement extends HTMLElement {} -export class HTMLTextAreaElement extends HTMLElement {} -export class HTMLTimeElement extends HTMLElement {} -export class HTMLTitleElement extends HTMLElement {} -export class HTMLTrackElement extends HTMLElement {} -export class HTMLUListElement extends HTMLElement {} -export class HTMLUnknownElement extends HTMLElement {} +const HTML_ELEMENT_LOCAL_NAMES = { + HTMLAnchorElement: ['a'], + HTMLAreaElement: ['area'], + HTMLAudioElement: ['audio'], + HTMLBaseElement: ['base'], + HTMLBodyElement: ['body'], + HTMLBRElement: ['br'], + HTMLButtonElement: ['button'], + HTMLCanvasElement: ['canvas'], + HTMLDListElement: ['dl'], + HTMLDataElement: ['data'], + HTMLDataListElement: ['datalist'], + HTMLDetailsElement: ['details'], + HTMLDialogElement: ['dialog'], + HTMLDirectoryElement: ['dir'], + HTMLDivElement: ['div'], + HTMLEmbedElement: ['embed'], + HTMLFieldSetElement: ['fieldset'], + HTMLFontElement: ['font'], + HTMLFormElement: ['form'], + HTMLFrameElement: ['frame'], + HTMLFrameSetElement: ['frameset'], + HTMLHeadElement: ['head'], + HTMLHeadingElement: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], + HTMLHRElement: ['hr'], + HTMLHtmlElement: ['html'], + HTMLIFrameElement: ['iframe'], + HTMLImageElement: ['img'], + HTMLInputElement: ['input'], + HTMLLabelElement: ['label'], + HTMLLegendElement: ['legend'], + HTMLLIElement: ['li'], + HTMLLinkElement: ['link'], + HTMLMapElement: ['map'], + HTMLMarqueeElement: ['marquee'], + HTMLMediaElement: ['audio', 'video'], + HTMLMenuElement: ['menu'], + HTMLMetaElement: ['meta'], + HTMLMeterElement: ['meter'], + HTMLModElement: ['del', 'ins'], + HTMLOListElement: ['ol'], + HTMLObjectElement: ['object'], + HTMLOptGroupElement: ['optgroup'], + HTMLOptionElement: ['option'], + HTMLOutputElement: ['output'], + HTMLParagraphElement: ['p'], + HTMLParamElement: ['param'], + HTMLPictureElement: ['picture'], + HTMLPreElement: ['pre', 'listing', 'xmp'], + HTMLProgressElement: ['progress'], + HTMLQuoteElement: ['blockquote', 'q'], + HTMLScriptElement: ['script'], + HTMLSelectElement: ['select'], + HTMLSlotElement: ['slot'], + HTMLSourceElement: ['source'], + HTMLSpanElement: ['span'], + HTMLStyleElement: ['style'], + HTMLTableCaptionElement: ['caption'], + HTMLTableCellElement: ['td', 'th'], + HTMLTableColElement: ['col', 'colgroup'], + HTMLTableElement: ['table'], + HTMLTableRowElement: ['tr'], + HTMLTableSectionElement: ['tbody', 'tfoot', 'thead'], + HTMLTextAreaElement: ['textarea'], + HTMLTimeElement: ['time'], + HTMLTitleElement: ['title'], + HTMLTrackElement: ['track'], + HTMLUListElement: ['ul'], + HTMLVideoElement: ['video'], +} as const; -type HTMLElementConstructor = new () => HTMLElement; +const GENERIC_HTML_LOCAL_NAMES = [ + 'abbr', + 'acronym', + 'address', + 'article', + 'aside', + 'b', + 'basefont', + 'bdi', + 'bdo', + 'big', + 'center', + 'cite', + 'code', + 'dd', + 'dfn', + 'dt', + 'em', + 'figcaption', + 'figure', + 'footer', + 'header', + 'hgroup', + 'i', + 'kbd', + 'main', + 'mark', + 'menuitem', + 'nav', + 'nobr', + 'noembed', + 'noframes', + 'noscript', + 'plaintext', + 'rb', + 'rp', + 'rt', + 'rtc', + 'ruby', + 's', + 'samp', + 'search', + 'section', + 'small', + 'strike', + 'strong', + 'sub', + 'summary', + 'sup', + 'template', + 'tt', + 'u', + 'var', + 'wbr', +] as const; -export const HTML_ELEMENT_CONSTRUCTORS: Record = - { - a: HTMLAnchorElement, - abbr: HTMLElement, - address: HTMLElement, - area: HTMLAreaElement, - article: HTMLElement, - aside: HTMLElement, - audio: HTMLAudioElement, - b: HTMLElement, - base: HTMLBaseElement, - bdi: HTMLElement, - bdo: HTMLElement, - blockquote: HTMLQuoteElement, - body: HTMLBodyElement, - br: HTMLBRElement, - button: HTMLButtonElement, - canvas: HTMLCanvasElement, - caption: HTMLTableCaptionElement, - col: HTMLTableColElement, - colgroup: HTMLTableColElement, - cite: HTMLElement, - code: HTMLElement, - data: HTMLDataElement, - datalist: HTMLDataListElement, - dd: HTMLElement, - del: HTMLModElement, - details: HTMLDetailsElement, - dfn: HTMLElement, - dialog: HTMLDialogElement, - dir: HTMLDirectoryElement, - div: HTMLDivElement, - dl: HTMLDListElement, - dt: HTMLElement, - em: HTMLElement, - embed: HTMLEmbedElement, - fieldset: HTMLFieldSetElement, - figcaption: HTMLElement, - figure: HTMLElement, - font: HTMLFontElement, - footer: HTMLElement, - form: HTMLFormElement, - frame: HTMLFrameElement, - frameset: HTMLFrameSetElement, - h1: HTMLHeadingElement, - h2: HTMLHeadingElement, - h3: HTMLHeadingElement, - h4: HTMLHeadingElement, - h5: HTMLHeadingElement, - h6: HTMLHeadingElement, - head: HTMLHeadElement, - header: HTMLElement, - hgroup: HTMLElement, - hr: HTMLHRElement, - html: HTMLHtmlElement, - i: HTMLElement, - iframe: HTMLIFrameElement, - img: HTMLImageElement, - input: HTMLInputElement, - ins: HTMLModElement, - kbd: HTMLElement, - label: HTMLLabelElement, - legend: HTMLLegendElement, - li: HTMLLIElement, - link: HTMLLinkElement, - main: HTMLElement, - map: HTMLMapElement, - marquee: HTMLMarqueeElement, - mark: HTMLElement, - menu: HTMLMenuElement, - meta: HTMLMetaElement, - meter: HTMLMeterElement, - nav: HTMLElement, - noscript: HTMLElement, - object: HTMLObjectElement, - ol: HTMLOListElement, - optgroup: HTMLOptGroupElement, - option: HTMLOptionElement, - output: HTMLOutputElement, - p: HTMLParagraphElement, - param: HTMLParamElement, - picture: HTMLPictureElement, - pre: HTMLPreElement, - progress: HTMLProgressElement, - q: HTMLQuoteElement, - rp: HTMLElement, - rt: HTMLElement, - ruby: HTMLElement, - s: HTMLElement, - samp: HTMLElement, - script: HTMLScriptElement, - search: HTMLElement, - section: HTMLElement, - select: HTMLSelectElement, - slot: HTMLSlotElement, - small: HTMLElement, - source: HTMLSourceElement, - span: HTMLSpanElement, - strong: HTMLElement, - style: HTMLStyleElement, - sub: HTMLElement, - summary: HTMLElement, - sup: HTMLElement, - table: HTMLTableElement, - tbody: HTMLTableSectionElement, - td: HTMLTableCellElement, - template: HTMLTemplateElement, - textarea: HTMLTextAreaElement, - tfoot: HTMLTableSectionElement, - th: HTMLTableCellElement, - thead: HTMLTableSectionElement, - time: HTMLTimeElement, - title: HTMLTitleElement, - tr: HTMLTableRowElement, - track: HTMLTrackElement, - u: HTMLElement, - ul: HTMLUListElement, - var: HTMLElement, - video: HTMLVideoElement, - wbr: HTMLElement, - }; +const KNOWN_HTML_LOCAL_NAMES = new Set([ + ...GENERIC_HTML_LOCAL_NAMES, + ...Object.values(HTML_ELEMENT_LOCAL_NAMES).flat(), +]); + +function isHTMLElement(value: unknown): value is Element { + return value instanceof Element && value.namespaceURI === NamespaceURI.XHTML; +} + +function createElementConstructor( + name: string, + matches: (element: Element) => boolean, +) { + const Constructor = function () { + throw new TypeError('Illegal constructor'); + } as unknown as ElementConstructor; + + Object.defineProperties(Constructor, { + name: {value: name}, + prototype: {value: Element.prototype}, + [Symbol.hasInstance]: { + value(value: unknown) { + return isHTMLElement(value) && matches(value); + }, + }, + }); + + return Constructor; +} + +const TAG_SPECIFIC_HTML_ELEMENT_GLOBALS = Object.fromEntries( + Object.entries(HTML_ELEMENT_LOCAL_NAMES).map(([name, localNames]) => { + const names = new Set(localNames); + return [ + name, + createElementConstructor(name, (element) => + names.has(element.localName.toLowerCase()), + ), + ]; + }), +) as { + [Name in keyof typeof HTML_ELEMENT_LOCAL_NAMES]: ElementConstructor; +}; + +export const HTML_ELEMENT_GLOBALS = { + ...TAG_SPECIFIC_HTML_ELEMENT_GLOBALS, + HTMLUnknownElement: createElementConstructor( + 'HTMLUnknownElement', + (element) => { + const localName = element.localName.toLowerCase(); + return !localName.includes('-') && !KNOWN_HTML_LOCAL_NAMES.has(localName); + }, + ), +}; diff --git a/packages/polyfill/source/HTMLHeadElement.ts b/packages/polyfill/source/HTMLHeadElement.ts index 7cb989c6..c1117bc3 100644 --- a/packages/polyfill/source/HTMLHeadElement.ts +++ b/packages/polyfill/source/HTMLHeadElement.ts @@ -1,3 +1,3 @@ -import {HTMLElement} from './HTMLElement.ts'; +import {Element} from './Element.ts'; -export class HTMLHeadElement extends HTMLElement {} +export class HTMLHeadElement extends Element {} diff --git a/packages/polyfill/source/HTMLHtmlElement.ts b/packages/polyfill/source/HTMLHtmlElement.ts index 5ff28bca..570890d5 100644 --- a/packages/polyfill/source/HTMLHtmlElement.ts +++ b/packages/polyfill/source/HTMLHtmlElement.ts @@ -1,3 +1,3 @@ -import {HTMLElement} from './HTMLElement.ts'; +import {Element} from './Element.ts'; -export class HTMLHtmlElement extends HTMLElement {} +export class HTMLHtmlElement extends Element {} diff --git a/packages/polyfill/source/HTMLTemplateElement.ts b/packages/polyfill/source/HTMLTemplateElement.ts index 372e769b..fb32e21d 100644 --- a/packages/polyfill/source/HTMLTemplateElement.ts +++ b/packages/polyfill/source/HTMLTemplateElement.ts @@ -1,9 +1,9 @@ import {CONTENT} from './constants.ts'; import {DocumentFragment} from './DocumentFragment.ts'; -import {HTMLElement} from './HTMLElement.ts'; +import {Element} from './Element.ts'; import {parseHtml, serializeChildren} from './serialization.ts'; -export class HTMLTemplateElement extends HTMLElement { +export class HTMLTemplateElement extends Element { [CONTENT]?: DocumentFragment; get content() { diff --git a/packages/polyfill/source/Window.ts b/packages/polyfill/source/Window.ts index b85d9e8a..2e9c1fc3 100644 --- a/packages/polyfill/source/Window.ts +++ b/packages/polyfill/source/Window.ts @@ -17,9 +17,10 @@ import {CharacterData} from './CharacterData.ts'; import {Text} from './Text.ts'; import {Comment} from './Comment.ts'; import {DocumentFragment} from './DocumentFragment.ts'; +import {HTMLTemplateElement} from './HTMLTemplateElement.ts'; import {CustomElementRegistryImplementation} from './CustomElementRegistry.ts'; import {MutationObserver} from './MutationObserver.ts'; -import * as HTMLElements from './HTMLElements.ts'; +import {HTML_ELEMENT_GLOBALS} from './HTMLElements.ts'; import {HOOKS} from './constants.ts'; import type {Hooks} from './hooks.ts'; @@ -33,6 +34,10 @@ type OnErrorHandler = ) => void) | null; +type HTMLElementGlobals = typeof HTML_ELEMENT_GLOBALS; + +export interface Window extends HTMLElementGlobals {} + export class Window extends EventTarget { [HOOKS]: Partial = {}; name = ''; @@ -62,79 +67,15 @@ export class Window extends EventTarget { Text = Text; Element = Element; HTMLElement = HTMLElement; - HTMLAnchorElement = HTMLElements.HTMLAnchorElement; - HTMLAreaElement = HTMLElements.HTMLAreaElement; - HTMLAudioElement = HTMLElements.HTMLAudioElement; - HTMLBaseElement = HTMLElements.HTMLBaseElement; - HTMLBodyElement = HTMLElements.HTMLBodyElement; - HTMLBRElement = HTMLElements.HTMLBRElement; - HTMLButtonElement = HTMLElements.HTMLButtonElement; - HTMLCanvasElement = HTMLElements.HTMLCanvasElement; - HTMLDataElement = HTMLElements.HTMLDataElement; - HTMLDataListElement = HTMLElements.HTMLDataListElement; - HTMLDetailsElement = HTMLElements.HTMLDetailsElement; - HTMLDialogElement = HTMLElements.HTMLDialogElement; - HTMLDirectoryElement = HTMLElements.HTMLDirectoryElement; - HTMLDivElement = HTMLElements.HTMLDivElement; - HTMLDListElement = HTMLElements.HTMLDListElement; - HTMLEmbedElement = HTMLElements.HTMLEmbedElement; - HTMLFieldSetElement = HTMLElements.HTMLFieldSetElement; - HTMLFontElement = HTMLElements.HTMLFontElement; - HTMLFormElement = HTMLElements.HTMLFormElement; - HTMLFrameElement = HTMLElements.HTMLFrameElement; - HTMLFrameSetElement = HTMLElements.HTMLFrameSetElement; - HTMLHeadElement = HTMLElements.HTMLHeadElement; - HTMLHeadingElement = HTMLElements.HTMLHeadingElement; - HTMLHRElement = HTMLElements.HTMLHRElement; - HTMLHtmlElement = HTMLElements.HTMLHtmlElement; - HTMLIFrameElement = HTMLElements.HTMLIFrameElement; - HTMLImageElement = HTMLElements.HTMLImageElement; - HTMLInputElement = HTMLElements.HTMLInputElement; - HTMLLabelElement = HTMLElements.HTMLLabelElement; - HTMLLegendElement = HTMLElements.HTMLLegendElement; - HTMLLIElement = HTMLElements.HTMLLIElement; - HTMLLinkElement = HTMLElements.HTMLLinkElement; - HTMLMapElement = HTMLElements.HTMLMapElement; - HTMLMarqueeElement = HTMLElements.HTMLMarqueeElement; - HTMLMediaElement = HTMLElements.HTMLMediaElement; - HTMLMenuElement = HTMLElements.HTMLMenuElement; - HTMLMetaElement = HTMLElements.HTMLMetaElement; - HTMLMeterElement = HTMLElements.HTMLMeterElement; - HTMLModElement = HTMLElements.HTMLModElement; - HTMLObjectElement = HTMLElements.HTMLObjectElement; - HTMLOListElement = HTMLElements.HTMLOListElement; - HTMLOptGroupElement = HTMLElements.HTMLOptGroupElement; - HTMLOptionElement = HTMLElements.HTMLOptionElement; - HTMLOutputElement = HTMLElements.HTMLOutputElement; - HTMLParagraphElement = HTMLElements.HTMLParagraphElement; - HTMLParamElement = HTMLElements.HTMLParamElement; - HTMLPictureElement = HTMLElements.HTMLPictureElement; - HTMLPreElement = HTMLElements.HTMLPreElement; - HTMLProgressElement = HTMLElements.HTMLProgressElement; - HTMLQuoteElement = HTMLElements.HTMLQuoteElement; - HTMLScriptElement = HTMLElements.HTMLScriptElement; - HTMLSelectElement = HTMLElements.HTMLSelectElement; - HTMLSlotElement = HTMLElements.HTMLSlotElement; - HTMLSourceElement = HTMLElements.HTMLSourceElement; - HTMLSpanElement = HTMLElements.HTMLSpanElement; - HTMLStyleElement = HTMLElements.HTMLStyleElement; - HTMLTableCaptionElement = HTMLElements.HTMLTableCaptionElement; - HTMLTableCellElement = HTMLElements.HTMLTableCellElement; - HTMLTableColElement = HTMLElements.HTMLTableColElement; - HTMLTableElement = HTMLElements.HTMLTableElement; - HTMLTableRowElement = HTMLElements.HTMLTableRowElement; - HTMLTableSectionElement = HTMLElements.HTMLTableSectionElement; SVGElement = SVGElement; - HTMLTemplateElement = HTMLElements.HTMLTemplateElement; - HTMLTextAreaElement = HTMLElements.HTMLTextAreaElement; - HTMLTimeElement = HTMLElements.HTMLTimeElement; - HTMLTitleElement = HTMLElements.HTMLTitleElement; - HTMLTrackElement = HTMLElements.HTMLTrackElement; - HTMLUListElement = HTMLElements.HTMLUListElement; - HTMLUnknownElement = HTMLElements.HTMLUnknownElement; - HTMLVideoElement = HTMLElements.HTMLVideoElement; + HTMLTemplateElement = HTMLTemplateElement; MutationObserver = MutationObserver; + constructor() { + super(); + Object.assign(this, HTML_ELEMENT_GLOBALS); + } + #currentOnErrorHandler: ((event: any) => void) | null = null; #currentOriginalOnErrorHandler: OnErrorHandler = null; #currentOnUnhandledRejectionHandler: WindowEventHandlers['onunhandledrejection'] = diff --git a/packages/polyfill/source/tests/HTMLElements.test.ts b/packages/polyfill/source/tests/HTMLElements.test.ts index 224614a6..6df48ca1 100644 --- a/packages/polyfill/source/tests/HTMLElements.test.ts +++ b/packages/polyfill/source/tests/HTMLElements.test.ts @@ -29,6 +29,15 @@ describe('HTML element constructors', () => { expect(element).toBeInstanceOf(window.Element); }); + it('keeps ordinary elements on the shared Element prototype', () => { + const input = window.document.createElement('input'); + + expect(input.constructor).toBe(window.Element); + expect(Object.getPrototypeOf(input)).toBe(window.Element.prototype); + expect(input).toBeInstanceOf(window.HTMLInputElement); + expect(input).toBeInstanceOf(window.HTMLElement); + }); + it('keeps distinct element constructor identities', () => { const input = window.document.createElement('input'); @@ -45,16 +54,27 @@ describe('HTML element constructors', () => { ); }); - it('creates generic, unknown, and unregistered custom elements correctly', () => { - expect(window.document.createElement('article').constructor).toBe( - window.HTMLElement, - ); - expect(window.document.createElement('not-an-html-tag')).toBeInstanceOf( - window.HTMLElement, - ); - expect(window.document.createElement('unknown')).toBeInstanceOf( - window.HTMLUnknownElement, + it('identifies generic, unknown, and unregistered custom elements', () => { + const article = window.document.createElement('article'); + const custom = window.document.createElement('not-an-html-tag'); + const unknown = window.document.createElement('unknown'); + + expect(article.constructor).toBe(window.Element); + expect(article).toBeInstanceOf(window.HTMLElement); + expect(article).not.toBeInstanceOf(window.HTMLUnknownElement); + expect(custom).toBeInstanceOf(window.HTMLElement); + expect(custom).not.toBeInstanceOf(window.HTMLUnknownElement); + expect(unknown).toBeInstanceOf(window.HTMLUnknownElement); + }); + + it('does not identify SVG elements as HTML elements', () => { + const anchor = window.document.createElementNS( + 'http://www.w3.org/2000/svg' as any, + 'a', ); + + expect(anchor).not.toBeInstanceOf(window.HTMLElement); + expect(anchor).not.toBeInstanceOf(window.HTMLAnchorElement); }); it('preserves constructors for built-in document elements and templates', () => { @@ -73,8 +93,26 @@ describe('HTML element constructors', () => { window.customElements.define('test-element', TestElement as any); - expect(window.document.createElement('test-element')).toBeInstanceOf( - TestElement, + const element = window.document.createElement('test-element'); + + expect(element).toBeInstanceOf(TestElement); + expect(element).toBeInstanceOf(window.HTMLElement); + }); + + it('keeps custom element subclass instanceof checks prototype-based', () => { + class FirstElement extends window.HTMLElement {} + class SecondElement extends window.HTMLElement {} + + window.customElements.define('first-element', FirstElement as any); + const element = window.document.createElement('first-element'); + + expect(element).toBeInstanceOf(FirstElement); + expect(element).not.toBeInstanceOf(SecondElement); + }); + + it('does not construct tag-specific facade classes', () => { + expect(() => new window.HTMLInputElement()).toThrowError( + new TypeError('Illegal constructor'), ); });