Repository: capricorn86/happy-dom
Pull request #346 (2022-01-30 16:37:07)

#307@minor: Adds support for HTMLSlotElement. Adds support for the pr…
…operty Element.slot.


Patch:
diff --git a/packages/happy-dom/src/config/ElementTag.ts b/packages/happy-dom/src/config/ElementTag.ts
index 286722f2..6ce0e85a 100644
--- a/packages/happy-dom/src/config/ElementTag.ts
+++ b/packages/happy-dom/src/config/ElementTag.ts
@@ -8,6 +8,7 @@ import HTMLScriptElement from '../nodes/html-script-element/HTMLScriptElement';
 import HTMLImageElement from '../nodes/html-image-element/HTMLImageElement';
 import HTMLLinkElement from '../nodes/html-link-element/HTMLLinkElement';
 import HTMLStyleElement from '../nodes/html-style-element/HTMLStyleElement';
+import HTMLSlotElement from '../nodes/html-slot-element/HTMLSlotElement';
 
 export default {
 	template: HTMLTemplateElement,
@@ -18,6 +19,7 @@ export default {
 	img: HTMLImageElement,
 	link: HTMLLinkElement,
 	style: HTMLStyleElement,
+	slot: HTMLSlotElement,
 	svg: SVGSVGElement,
 	circle: SVGElement,
 	ellipse: SVGElement,
diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts
index 7c0d6f15..69b00201 100644
--- a/packages/happy-dom/src/nodes/element/Element.ts
+++ b/packages/happy-dom/src/nodes/element/Element.ts
@@ -230,6 +230,24 @@ export default class Element extends Node implements IElement {
 		return this.children.length;
 	}
 
+	/**
+	 * Returns slot.
+	 *
+	 * @returns Slot.
+	 */
+	public get slot(): string {
+		return this.getAttributeNS(null, 'slot') || '';
+	}
+
+	/**
+	 * Returns slot.
+	 *
+	 * @param slot Slot.
+	 */
+	public set slot(title: string) {
+		this.setAttributeNS(null, 'slot', title);
+	}
+
 	/**
 	 * Attribute changed callback.
 	 *
diff --git a/packages/happy-dom/src/nodes/element/IElement.ts b/packages/happy-dom/src/nodes/element/IElement.ts
index b5edeed3..c224a6ad 100644
--- a/packages/happy-dom/src/nodes/element/IElement.ts
+++ b/packages/happy-dom/src/nodes/element/IElement.ts
@@ -24,6 +24,7 @@ export default interface IElement extends IChildNode, INonDocumentTypeChildNode,
 	className: string;
 	innerHTML: string;
 	outerHTML: string;
+	slot: string;
 	readonly attributes: { [k: string]: Attr | number };
 
 	/**
diff --git a/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts b/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts
new file mode 100644
index 00000000..5d4d4506
--- /dev/null
+++ b/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts
@@ -0,0 +1,110 @@
+import HTMLElement from '../html-element/HTMLElement';
+import IShadowRoot from '../shadow-root/IShadowRoot';
+import IHTMLSlotElement from './IHTMLSlotElement';
+import IText from '../text/IText';
+import IElement from '../element/IElement';
+import INode from '../node/INode';
+
+/**
+ * HTML Slot Element.
+ *
+ * Reference:
+ * https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement.
+ */
+export default class HTMLSlotElement extends HTMLElement implements IHTMLSlotElement {
+	/**
+	 * Returns name.
+	 *
+	 * @returns Name.
+	 */
+	public get name(): string {
+		return this.getAttributeNS(null, 'name') || '';
+	}
+
+	/**
+	 * Sets name.
+	 *
+	 * @param name Name.
+	 */
+	public set name(name: string) {
+		this.setAttributeNS(null, 'name', name);
+	}
+
+	/**
+	 * Sets the slot's manually assigned nodes to an ordered set of slottables.
+	 *
+	 * @param _nodes Nodes.
+	 */
+	public assign(..._nodes: Array<IText | IElement>): void {
+		// TODO: Do nothing for now. We need to find an example of how it is expected to work before it can be implemented.
+	}
+
+	/**
+	 * Returns assigned nodes.
+	 *
+	 * @param [options] Options.
+	 * @param [options.flatten] A boolean value indicating whether to return the assigned nodes of any available child <slot> elements (true) or not (false). Defaults to false.
+	 * @returns Nodes.
+	 */
+	public assignedNodes(options?: { flatten?: boolean }): INode[] {
+		const host = (<IShadowRoot>this.getRootNode())?.host;
+
+		// TODO: Add support for options.flatten. We need to find an example of how it is expected to work before it can be implemented.
+
+		if (host) {
+			const name = this.name;
+
+			if (name) {
+				return this.assignedElements(options);
+			}
+
+			return host.childNodes.slice();
+		}
+
+		return [];
+	}
+
+	/**
+	 * Returns assigned elements.
+	 *
+	 * @param [_options] Options.
+	 * @param [_options.flatten] A boolean value indicating whether to return the assigned elements of any available child <slot> elements (true) or not (false). Defaults to false.
+	 * @returns Nodes.
+	 */
+	public assignedElements(_options?: { flatten?: boolean }): IElement[] {
+		const host = (<IShadowRoot>this.getRootNode())?.host;
+
+		// TODO: Add support for options.flatten. We need to find an example of how it expected to work before it can be implemented.
+
+		if (host) {
+			const name = this.name;
+
+			if (name) {
+				const assignedElements = [];
+
+				for (const child of host.children) {
+					if (child.slot === name) {
+						assignedElements.push(child);
+					}
+				}
+
+				return assignedElements;
+			}
+
+			return host.children.slice();
+		}
+
+		return [];
+	}
+
+	/**
+	 * Clones a node.
+	 *
+	 * @override
+	 * @param [deep=false] "true" to clone deep.
+	 * @returns Cloned node.
+	 */
+	public cloneNode(deep = false): IHTMLSlotElement {
+		return <HTMLSlotElement>super.cloneNode(deep);
+	}
+}
diff --git a/packages/happy-dom/src/nodes/html-slot-element/IHTMLSlotElement.ts b/packages/happy-dom/src/nodes/html-slot-element/IHTMLSlotElement.ts
new file mode 100644
index 00000000..3a6dcdfe
--- /dev/null
+++ b/packages/happy-dom/src/nodes/html-slot-element/IHTMLSlotElement.ts
@@ -0,0 +1,47 @@
+import IHTMLElement from '../html-element/IHTMLElement';
+import IText from '../text/IText';
+import IElement from '../element/IElement';
+import INode from '../node/INode';
+
+/**
+ * HTML Slot Element.
+ *
+ * Reference:
+ * https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement.
+ */
+export default interface IHTMLSlotElement extends IHTMLElement {
+	name: string;
+
+	/**
+	 * Sets the slot's manually assigned nodes to an ordered set of slottables.
+	 *
+	 * @param nodes Nodes.
+	 */
+	assign(...nodes: Array<IText | IElement>): void;
+
+	/**
+	 * Returns assigned nodes.
+	 *
+	 * @param [options] Options.
+	 * @param [options.flatten] A boolean value indicating whether to return the assigned nodes of any available child <slot> elements (true) or not (false). Defaults to false.
+	 * @returns Nodes.
+	 */
+	assignedNodes(options?: { flatten?: boolean }): INode[];
+
+	/**
+	 * Returns assigned nodes.
+	 *
+	 * @param [options.flatten] A boolean value indicating whether to return the assigned elements of any available child <slot> elements (true) or not (false). Defaults to false.
+	 * @returns Nodes.
+	 */
+	assignedElements(options?: { flatten?: boolean }): IElement[];
+
+	/**
+	 * Clones a node.
+	 *
+	 * @override
+	 * @param [deep=false] "true" to clone deep.
+	 * @returns Cloned node.
+	 */
+	cloneNode(deep: boolean): IHTMLSlotElement;
+}


Test patch:
diff --git a/packages/happy-dom/test/CustomElement.ts b/packages/happy-dom/test/CustomElement.ts
index f372c4a6..fb4e8faa 100644
--- a/packages/happy-dom/test/CustomElement.ts
+++ b/packages/happy-dom/test/CustomElement.ts
@@ -22,7 +22,7 @@ export default class CustomElement extends new Window().HTMLElement {
 	 */
 	constructor() {
 		super();
-		this.attachShadow({ mode: 'closed' });
+		this.attachShadow({ mode: 'open' });
 	}
 
 	/**
diff --git a/packages/happy-dom/test/nodes/element/Element.test.ts b/packages/happy-dom/test/nodes/element/Element.test.ts
index a5c23cd4..2b4a8d8b 100644
--- a/packages/happy-dom/test/nodes/element/Element.test.ts
+++ b/packages/happy-dom/test/nodes/element/Element.test.ts
@@ -63,6 +63,20 @@ describe('Element', () => {
 		});
 	});
 
+	describe('get slot()', () => {
+		it('Returns the element "slot" attribute.', () => {
+			element.setAttribute('slot', 'slot');
+			expect(element.slot).toBe('slot');
+		});
+	});
+
+	describe('set slot()', () => {
+		it('Sets the element "slot" as an attribute.', () => {
+			element.slot = 'slot';
+			expect(element.getAttribute('slot')).toBe('slot');
+		});
+	});
+
 	describe('get className()', () => {
 		it('Returns the element "class" attribute.', () => {
 			element.setAttribute('class', 'class');
diff --git a/packages/happy-dom/test/nodes/html-slot-element/CustomElementWithNamedSlots.ts b/packages/happy-dom/test/nodes/html-slot-element/CustomElementWithNamedSlots.ts
new file mode 100644
index 00000000..feea7bde
--- /dev/null
+++ b/packages/happy-dom/test/nodes/html-slot-element/CustomElementWithNamedSlots.ts
@@ -0,0 +1,26 @@
+import HTMLElement from '../../../src/nodes/html-element/HTMLElement';
+
+/**
+ * Custom element test class.
+ */
+export default class CustomElementWithNamedSlots extends HTMLElement {
+	/**
+	 * Constructor.
+	 */
+	constructor() {
+		super();
+		this.attachShadow({ mode: 'open' });
+	}
+
+	/**
+	 * @override
+	 */
+	public connectedCallback(): void {
+		this.shadowRoot.innerHTML = `
+            <div>
+                <span><slot name="slot1"></slot></span>
+                <span><slot name="slot2"></slot></span>
+            </div>
+        `;
+	}
+}
diff --git a/packages/happy-dom/test/nodes/html-slot-element/CustomElementWithSlot.ts b/packages/happy-dom/test/nodes/html-slot-element/CustomElementWithSlot.ts
new file mode 100644
index 00000000..4901ffaa
--- /dev/null
+++ b/packages/happy-dom/test/nodes/html-slot-element/CustomElementWithSlot.ts
@@ -0,0 +1,25 @@
+import HTMLElement from '../../../src/nodes/html-element/HTMLElement';
+
+/**
+ * Custom element test class.
+ */
+export default class CustomElementWithSlot extends HTMLElement {
+	/**
+	 * Constructor.
+	 */
+	constructor() {
+		super();
+		this.attachShadow({ mode: 'open' });
+	}
+
+	/**
+	 * @override
+	 */
+	public connectedCallback(): void {
+		this.shadowRoot.innerHTML = `
+            <div>
+                <span><slot></slot></span>
+            </div>
+        `;
+	}
+}
diff --git a/packages/happy-dom/test/nodes/html-slot-element/HTMLSlotElement.test.ts b/packages/happy-dom/test/nodes/html-slot-element/HTMLSlotElement.test.ts
new file mode 100644
index 00000000..bc19d067
--- /dev/null
+++ b/packages/happy-dom/test/nodes/html-slot-element/HTMLSlotElement.test.ts
@@ -0,0 +1,135 @@
+import Window from '../../../src/window/Window';
+import Document from '../../../src/nodes/document/Document';
+import IHTMLSlotElement from '../../../src/nodes/html-slot-element/IHTMLSlotElement';
+import CustomElementWithNamedSlots from './CustomElementWithNamedSlots';
+import CustomElementWithSlot from './CustomElementWithSlot';
+
+describe('HTMLSlotElement', () => {
+	let window: Window;
+	let document: Document;
+	let customElementWithNamedSlots: CustomElementWithNamedSlots;
+	let customElementWithSlot: CustomElementWithSlot;
+
+	beforeEach(() => {
+		window = new Window();
+		document = window.document;
+
+		window.customElements.define('custom-element-with-named-slots', CustomElementWithNamedSlots);
+		window.customElements.define('custom-element-with-slot', CustomElementWithSlot);
+
+		customElementWithNamedSlots = <CustomElementWithNamedSlots>(
+			document.createElement('custom-element-with-named-slots')
+		);
+		customElementWithSlot = <CustomElementWithSlot>(
+			document.createElement('custom-element-with-slot')
+		);
+
+		document.body.appendChild(customElementWithNamedSlots);
+		document.body.appendChild(customElementWithSlot);
+	});
+
+	describe('get name()', () => {
+		it('Returns attribute value.', () => {
+			const slot = <IHTMLSlotElement>customElementWithSlot.shadowRoot.querySelector('slot');
+			expect(slot.name).toBe('');
+			slot.setAttribute('name', 'value');
+			expect(slot.name).toBe('value');
+		});
+	});
+
+	describe('set name()', () => {
+		it('Sets attribute value.', () => {
+			const slot = <IHTMLSlotElement>customElementWithSlot.shadowRoot.querySelector('slot');
+			slot.name = 'value';
+			expect(slot.getAttribute('name')).toBe('value');
+		});
+	});
+
+	describe('assign()', () => {
+		it("Sets the slot's manually assigned nodes to an ordered set of slottables.", () => {
+			const slot = <IHTMLSlotElement>customElementWithSlot.shadowRoot.querySelector('slot');
+			// TODO: Do nothing for now. We need to find an example of how it is expected to work before it can be implemented.
+			expect(slot.assign()).toBe(undefined);
+		});
+	});
+
+	describe('assignedNodes()', () => {
+		it('Returns nodes appended to the custom element.', () => {
+			const slot = <IHTMLSlotElement>customElementWithSlot.shadowRoot.querySelector('slot');
+			const text = document.createTextNode('text');
+			const comment = document.createComment('text');
+			const div = document.createElement('div');
+			const span = document.createElement('span');
+
+			customElementWithSlot.appendChild(text);
+			customElementWithSlot.appendChild(comment);
+			customElementWithSlot.appendChild(div);
+			customElementWithSlot.appendChild(span);
+
+			expect(slot.assignedNodes()).toEqual(customElementWithSlot.childNodes);
+		});
+
+		it('Only return elements that has the proeprty "slot" set to the same value as the property "name" of the slot.', () => {
+			const text = document.createTextNode('text');
+			const comment = document.createComment('text');
+			const div = document.createElement('div');
+			const span = document.createElement('span');
+
+			div.slot = 'slot1';
+
+			customElementWithNamedSlots.appendChild(text);
+			customElementWithNamedSlots.appendChild(comment);
+			customElementWithNamedSlots.appendChild(div);
+			customElementWithNamedSlots.appendChild(span);
+
+			const slots = <IHTMLSlotElement[]>(
+				customElementWithNamedSlots.shadowRoot.querySelectorAll('slot')
+			);
+
+			expect(slots[0].assignedNodes()).toEqual([div]);
+			expect(slots[1].assignedNodes()).toEqual([]);
+		});
+	});
+
+	describe('assignedElements()', () => {
+		it('Returns elements appended to the custom element.', () => {
+			const slot = <IHTMLSlotElement>customElementWithSlot.shadowRoot.querySelector('slot');
+			const text = document.createTextNode('text');
+			const comment = document.createComment('text');
+			const div = document.createElement('div');
+			const span = document.createElement('span');
+
+			customElementWithSlot.appendChild(text);
+			customElementWithSlot.appendChild(comment);
+			customElementWithSlot.appendChild(div);
+			customElementWithSlot.appendChild(span);
+
+			expect(slot.assignedElements()).toEqual(customElementWithSlot.children);
+		});
+
+		it('Only return elements that has the proeprty "slot" set to the same value as the property "name" of the slot.', () => {
+			const text = document.createTextNode('text');
+			const comment = document.createComment('text');
+			const div = document.createElement('div');
+			const span1 = document.createElement('span');
+			const span2 = document.createElement('span');
+
+			div.slot = 'slot1';
+			span1.slot = 'slot1';
+			span2.slot = 'slot2';
+
+			customElementWithNamedSlots.appendChild(text);
+			customElementWithNamedSlots.appendChild(comment);
+			customElementWithNamedSlots.appendChild(div);
+			customElementWithNamedSlots.appendChild(span1);
+			customElementWithNamedSlots.appendChild(span2);
+
+			const slots = <IHTMLSlotElement[]>(
+				customElementWithNamedSlots.shadowRoot.querySelectorAll('slot')
+			);
+
+			expect(slots[0].assignedElements()).toEqual([div, span1]);
+			expect(slots[1].assignedElements()).toEqual([span2]);
+		});
+	});
+});
diff --git a/packages/happy-dom/test/nodes/shadow-root/ShadowRoot.test.ts b/packages/happy-dom/test/nodes/shadow-root/ShadowRoot.test.ts
index 3c04476d..7b9045c3 100644
--- a/packages/happy-dom/test/nodes/shadow-root/ShadowRoot.test.ts
+++ b/packages/happy-dom/test/nodes/shadow-root/ShadowRoot.test.ts
@@ -83,8 +83,8 @@ describe('ShadowRoot', () => {
 		it('Clones the value of the "mode" property when cloned.', () => {
 			const shadowRoot = document.createElement('custom-element').shadowRoot;
 			const clone = shadowRoot.cloneNode();
-			expect(shadowRoot.mode).toBe('closed');
-			expect(clone.mode).toBe('closed');
+			expect(shadowRoot.mode).toBe('open');
+			expect(clone.mode).toBe('open');
 		});
 	});
 });